# layout-styling — レイアウト・グルーピング、styleBlock CSS、modalEdit ダイアログ、参照モード disabled

**Scope:** Form.io コンポーネントの視覚的グルーピング (`panel` / `container`)、フォームスコープ CSS 注入 (`styleBlock`)、モーダルダイアログ (`modalEdit: true` panel)、参照モード (`!isEditable()`) での disabled 制御。データのマッピングは [data-binding.md](/docs/data-binding.md)、JS 拡張点 (`customConditional` 等) は [scripting.md](/docs/scripting.md)、サーバ値読出しは [server-data.md](/docs/server-data.md) を参照。

## 1. レイアウトのグルーピングは `panel` を使う — `container` は原則禁止

consentFlow は **Notes 文書のフラットなアイテム構造** (`<prefix><key>`、接頭語は設定値でデフォルト `CF_`、モダナイズ案件では空文字もあり。詳細は [data-binding.md](/docs/data-binding.md) §1) にフォームデータをマップする。Form.io 側で **「データをネストする」コンポーネントは consentFlow の保存モデルと相性が悪い**ので、原則として使わない。

| コンポーネント | データの形 | consentFlow との相性 | 用途判断 |
|---|---|---|---|
| **`panel`** | フラット — 子の `key` がそのまま `data.<key>` (= `<prefix><key>`) | ◎ 完全に一致 | **レイアウト・グルーピングの第一選択** |
| **`columns`** / `tabs` / `well` / `fieldset` | フラット — 同上 | ◎ | レイアウト用途で自由に使ってよい |
| **`container`** + `tree: true` | **ネスト** — `data.<containerKey>.<childKey>` (= Notes 側は `<prefix><containerKey>` に子オブジェクトごと JSON 文字列 1 アイテム。子は個別アイテムにならない) | ✕ | **原則使用禁止** |
| `datagrid` / `editgrid` | 配列ネスト — 子は行配列の各要素にネスト | △ 配列保存が必要なときのみ | 「繰り返し行」を本当に保存したいときのみ |

**なぜ `container` を避けるのか**

`container` (Form.io 4.x の `type: "container"`, `tree: true`) は **子コンポーネントのデータを自身の `key` 配下にネストする** ためのコンポーネント。Form.io 標準では JSON サブオブジェクトを作る用途に使われるが:

1. **consentFlow の保存層はフラットマッピング前提** — `data.<containerKey>.<childKey>` の構造は `<prefix><containerKey>` に **JSON 文字列 1 アイテム** として保存される ([data-binding.md](/docs/data-binding.md) §1)。子フィールドは個別の Notes アイテムにならないので、ビュー列・メールトークン・SQL 連携から子の値を直接参照できない。(旧コアは子キーで `CF_<childKey>` に平坦化しており、他コンポーネントと同名なら後勝ちで衝突していた)
2. **Form.io 側 JS から子コンポーネントの値を参照するときの落とし穴** — `data.<childKey>` で素直に取れず、`data.<containerKey>.<childKey>` を経由しないとアクセスできない。これを忘れた button の `custom` JS が「値が undefined」で fail する事故が多発する。
3. **逆に「レイアウトだけしたい」なら `panel` で十分** — Form.io の `panel` は単なる視覚的グルーピング (`<div class="card">` ラッパ) で、子のデータは親 `form.data` にフラットに乗る。`title`/`label` を空にして `customClass` で CSS を当てればコンテナと同じ見た目を実現できる。

**`panel` 化のテンプレ (旧 `container` からの移行)**

```jsonc
// ❌ NG: container でレイアウトをグルーピング (データがネスト)
{
  "type": "container",
  "key": "StaffCell_p_buchou",
  "tree": true,
  "hideLabel": true,
  "customClass": "lc-staff-cell",
  "components": [ /* ... */ ]
}

// ✅ OK: panel でレイアウトをグルーピング (データがフラット)
{
  "type": "panel",
  "key": "StaffCell_p_buchou",
  "title": "",
  "collapsible": false,
  "hideLabel": true,
  "customClass": "lc-staff-cell",
  "components": [ /* ... */ ]
}
```

panel は Bootstrap の `.card` ラッパを子に出すので、装飾を打ち消したいときは §3 の styleBlock で:

```css
.lc-staff-cell.formio-component-panel > .card {
  border: none !important; background: transparent !important;
  box-shadow: none !important; margin: 0 !important; border-radius: 0 !important;
}
.lc-staff-cell.formio-component-panel > .card > .card-header { display: none !important; }
.lc-staff-cell.formio-component-panel > .card > .card-body  { padding: 0 !important; background: transparent !important; }
```

**`container` の使用が許容されるケース**

- **明確に「子コンポーネントの集合体を 1 つの JSON オブジェクト値として保存したい」業務要件がある** (例: 動的に名前が変わるサブフィールド群)
- **datagrid の `components` セルとして** `container` をネストし、行内でさらにグループ化する場合

これら以外で `container` を新規追加するときは、**「なぜ panel ではダメか」を明示的に説明できるか** を判断基準にする。説明できないなら panel を選ぶ。

**既存 `container` を棚卸ししたいとき**

JSON 内の `"type": "container"` を探す (静的チェックのルール CF001 が検出する)。

ヒットした各々が「データのネストが本当に必要か」を確認。不要なら `panel` に置換 (`type` を変更、`tree`/`lazyLoad` を削除、`title:""` / `collapsible:false` を追加するだけで構造的にはフラットに切り替わる)。

## 2. 参照モードで disabled が当たらない罠 (Choices.js / ACE / Quill / DataGrid)

コアの参照モード処理 (`!currentDocument.isEditable()` のときに走るブロック) で、特定コンポーネントが disabled にならず編集可能なまま残るケースがある。

### 自作再帰で leaf に到達できない
- Form.io v4/v6 の Component インスタンスは Select 等 leaf 型でも `.components` が空配列でないケースがある
- `if (component.components && component.components.length > 0) { walkComponents; continue; }` で leaf を「コンテナ扱い」して `disabled = true` をスキップしてしまう
- → Form.io 公式 API `form.everyComponent(fn)` を使う。`return false` で editgrid 配下スキップ可

### Choices.js (select / tags / selectMultiple) は `component.disabled = true` だけでは UI が止まらない
- 特に `lazyLoad: true` / `dataSrc: "url"` で `.choices` DOM が遅延生成されるパターン
- render イベントハンドラは初回 render では `.choices` が存在せず空振り、遅延初期化後は再 render が走らない
- → CSS で `.formio-read-only .choices { pointer-events: none }` + `.choices__input` / `.choices__button` / `.choices::after` の `display: none` で対処

### ACE / Quill エディタは Form.io の disabled が内部インスタンスに伝わらない
- → `form.everyComponent` 内で `component.editor.setReadOnly(true)` (ACE) と `component.quill.enable(false)` (Quill) を明示的に呼ぶ
- 併せて CSS で `.ql-toolbar` 非表示、`.ql-editor` / `.ace_editor` を `pointer-events: none`、`.ace_text-input` / `.ace_cursor-layer` を `display: none`

### DataGrid 2 行目以降 / columns 配下の input が disabled 属性を持たない
- 真因は 2 つの合わせ技 (現行版では修正済):
  1. `form.submission = {...}` は setSubmission が `formReady.then(...)` にチェーンする**非同期適用** — `if(ro)` の everyComponent disabled 走査は同期実行のため、走査時点で DataGrid には既定 1 行 (`defaultValue:[{}]`) しか存在しない。1 行目のインスタンスは setValue で再利用されるため disabled が残り、「1 行目だけグレーで 2 行目以降が生きている」症状になる
  2. DataGrid の `createRowComponents` は新規行の子に `parentDisabled = !!<datagrid>.disabled` (= **コンテナ自身の disabled**) を継承させる — leaf だけ disabled にしてコンテナをスキップすると、後から生成される行はすべて enabled のまま
- コア対策 (参照モード処理): (a) datagrid / tree **コンテナ自身**も `disabled = true` にする (行生成タイミング非依存の根本対策)、(b) 走査を関数化し `form.submissionReady.then()` (setValue 完了後に解決) で再適用 + redraw、(c) 下記 checkbox/radio CSS 保険
- 見た目の保険は従来どおり `.formio-read-only input, .formio-read-only textarea` への `input:disabled` 相当 CSS (`background-color: #f8f9fa !important` / box-shadow / padding)。editgrid 用ルール (より詳細度高) は影響を受けない

### checkbox / radio は blanket CSS (`input { pointer-events: none }`) では止まらない
- ネイティブチェックボックスは `background-color` を描画しない → 保険 CSS を当ててもグレーに見えない
- Form.io の checkbox は input が `<label class="form-check-label">` の**内側**にある → label クリックの activation は input 側の `pointer-events: none` では止まらない。Tab フォーカス + Space のトグルも素通し
- → 対策 CSS: `.formio-read-only .form-check-label { pointer-events: none !important; }` で label 経由クリックを遮断 + `.formio-read-only input[type="checkbox"]:not(:disabled), input[type="radio"]:not(:disabled) { filter: grayscale(1); opacity: .5; }` で disabled 属性が付かなかった残存ケース (editgrid 展開行内等) を視覚グレー化 (`:not(:disabled)` で正規 disabled との二重減光を回避)。native select (choicesjs 以外 widget) にも `pointer-events: none` 保険を追加済み

### 参照モードの選択肢ラベル配色 (選択中=黒太字 / 非選択=#ccc) はコア共通 CSS の `radio-selected` 機構
- Radio / selectboxes は Form.io の `setSelectedClasses()` が**選択中オプションの wrapper div に `radio-selected` クラスを付与**し、コア共通 CSS の `.formio-read-only div.radio-selected input:disabled + span { color:#000; font-weight:bold }` が黒太字化。非選択は `.formio-read-only div input:disabled + span { color:#ccc }` で薄グレー
- **単一 checkbox コンポーネントは Radio 系ではないため `radio-selected` が付かない** → チェック済みでも一律 #ccc になり読みにくい。コア対策: コア共通 CSS に `:checked` 擬似クラスの状態ベースルール `.formio-read-only div input[type="checkbox"]:disabled:checked + span, ...input[type="radio"]:disabled:checked + span { color:#000 !important; font-weight:bold }` を追加 (radio も併記してクラス付与タイミング非依存の保険)

### Why / How to apply

Form.io の disabled setter は Choices.js / ACE / Quill / DataGrid 動的 row などに対しては不完全。内部実装の細部に依存せず、**JS (native API 直叩き) + CSS (DOM タイミング非依存) の二重防御**に倒すのが安定。コアの参照モード処理内で `form.everyComponent` 走査 + `.formio-read-only` クラス前提の CSS スタイル注入の組み合わせを維持する。

新規コンポーネント型が追加されて参照モードで編集可能なまま残る報告があったら、まず DevTools で `.formio-disabled-input` クラスや `disabled` 属性の有無を確認し、無ければこのパターンに該当する可能性が高い。

### 2.1 参照モードのテキストコピー許可 (WebFormReadOnlyCopyFlg)

HTML の `disabled` 属性付き input はブラウザ仕様で**テキスト選択自体が不可** (Form.io 標準の `readOnly` オプションでも disabled 属性描画になるため同じ)。加えて上記 blanket CSS の `pointer-events: none` がマウス選択を遮断する。参照モードで値をコピーしたい要件には、アプリケーション設定「WEBフォーム設定 → **参照時のテキストコピーを許可する**」(WFConfig アイテム `WebFormReadOnlyCopyFlg`、既定 OFF) を ON にする。

実装 (コアの参照モード処理内、設定 ON 時のみ):

- **disabled → readonly の DOM スワップ**: Form.io モデル層の `component.disabled = true` は維持したまま、テキスト系 input (`text`/`email`/`tel`/`url`/`search`/`number` + textarea) の DOM 属性のみ `disabled` を外して `readOnly = true` + マーカー `data-cf-selectable="1"` を付与。readonly input はブラウザ標準で選択・コピー可 / 打鍵・ペースト不可
- **opt-in CSS**: blanket `pointer-events:none` は残し、`.formio-read-only input[data-cf-selectable] { pointer-events: auto !important; user-select: text !important; cursor: text !important; }` (詳細度 0-2-1 + 後方宣言) で解除。フェイルセーフは「選択不可」側に倒れる
- **MutationObserver** (`attributeFilter:['disabled']` + childList) で customConditional / logic 由来の部分再レンダーに追随。readonly/マーカー付与は filter 対象外なので無限ループしない
- **除外**: flatpickr (`.flatpickr-input` / datetime・day・time wrapper — readonly input にカレンダー開放クリックがバインドされるため)、Choices.js (`.choices` 配下)、ACE (コピー経路が隠し textarea のため)。Quill は `quill.enable(false)` 済みなので CSS の pointer-events 解放のみで選択可


## 3. styleBlock (フォーム内カスタム CSS)

consentFlow はフォーム JSON 内に **`key: "styleBlock"` の `content` 型コンポーネント**を見つけると、その `content` を CSS として `<style>` タグで `document.head` に注入する。注入時に各セレクタの先頭へ `.formio-form` を自動付与してフォーム内スコープに閉じ込めるので、書いたセレクタは **すべてフォーム内 (`.formio-form` 配下) にスコープ** され、フォーム外 (左メニュー / アプリヘッダ / フォームビルダー UI / ボタン等) には影響しない。

実装は consentFlow の styleBlock 注入処理。

### 標準形

```json
{
  "type": "content",
  "key": "styleBlock",
  "label": "Custom CSS",
  "input": false,
  "content": ".formio-component-textfield input { background: #fafafa; }\n.required-mark { color: #c00; }"
}
```

### ルール

- `content` には **生 CSS ルールだけ** を書く。`<style>` タグは書かない (consentFlow が自動で付ける)
- `key` は厳密に `styleBlock` (他の名前だと無効)
- **1 フォームに 1 つだけ** (`form.getComponent("styleBlock")` が最初にヒットした 1 件しか適用されない)
- 既存フォーム編集時は既存 styleBlock を**そのまま温存**し、必要な追加だけ追記する (既存スタイルの上書き/削除は慎重に)
- セレクタに `.formio-form` を**自分で付ける必要はない** (システムが自動付与。付けても二重にはならないが冗長)

### 禁止事項・注意

- `body` / `html` / `.navbar` / `#fp-header` / `.menu-*` 等の **アプリ本体要素** を狙うセレクタは書かない (自動スコープで `.formio-form body` 等に変換され無効化される)
- `h1, h2, h3 { ... }` / `.btn-primary { ... }` / `.panel { ... }` のような **裸のタグ・汎用クラスセレクタ** を多用しない。フォーム内でも見出しやボタンが想定外に変わる。装飾するなら `.formio-component-content h3 { ... }` のように **コンポーネント単位** で書く
- 既存 consentFlow デザイン (Bootstrap ベース) を尊重。`linear-gradient` / 派手な `box-shadow` / フォントファミリ全置換などの **過剰な装飾は避ける**。フォーム単体の視認性向上に必要な最小限の指定にとどめる
- **`@keyframes` / `@font-face` はスコープ対象外でグローバルに出る**。名前衝突に注意 (もしくは使用しない)

### フォーム外の XPages DOM は `styleBlockGlobal` で狙う (承認フロー/承認履歴/親子文書パネル等)

自動スコープの帰結として、**Form.io フォーム外の XPages レンダリング要素** (承認フロー / 承認履歴 / 親子文書 / 添付ファイルパネル、アクションバー等) を **`styleBlock` の CSS で隠す/装飾することは原理的に不可能** (`.formio-form div[...]` に書き換えられマッチ 0 件)。CSS は無言で死ぬ — 同じセレクタを使う button の `custom` JS は動くので「JS は効くのに CSS だけ効かない」症状になったらこれを疑う。

**対処**: `key: "styleBlockGlobal"` のコンポーネントに書く。この content は **スコープ化されずそのまま head に注入**される。

```json
{
  "type": "content",
  "key": "styleBlockGlobal",
  "label": "Global CSS (フォーム外パネル)",
  "input": false,
  "hideLabel": true,
  "content": ".cf-panel-childDocsHier { display: none; }"
}
```

**セレクタは `.cf-panel-<panelOrderキー>` を第一選択にする。** 各パネル部品の `panelKey` 設定が外枠 div に `cf-panel cf-panel-<キー>` を出す (テーマ `card`/`box` 非依存、生成 id 非依存):

| セレクタ | パネル |
|---|---|
| `.cf-panel-webForm` | 申請フォーム |
| `.cf-panel-childDocsHier` | 関連子文書 (親子文書) |
| `.cf-panel-approvalFlow` | 承認ワークフロー |
| `.cf-panel-approvalHistory` | 承認履歴 (通常/チャット表示の両方に当たる。チャット版のみ狙うなら `.cf-panel-approvalHistoryChatStyle`) |
| `.cf-panel-documentRevisionHistory` | 改訂文書 |
| `.cf-panel-circulationStatus` | 回覧状況 |
| `.cf-panel-attachments` | ファイル添付 |
| `.cf-panel-additionalViewerAgg` | 追加閲覧者 |
| `.cf-panel-selectSendToForFinalApproval` | 最終決裁・否認の送信先 |

**ガードがある** — `styleBlockGlobal` は未スコープなので 1 行でアプリ全体を壊せる。コアが 3 階層で防御する:

- `body` / `html` / `:root` / `*` を**単独セレクタ**として書くと drop + `console.error` (`body .approvalHistoryTable` のような子孫指定は通る)
- `.navbar` / `.main-sidebar` / `.wrapper` 等のアプリシェルは `console.warn` を出して**適用される** — 原則書かない
- どうしても文書ルートを狙う必要があるときのみ `"customProperties": { "unsafeGlobal": "true" }` でブロック解除
- CSS パースに失敗したブロックは**丸ごと破棄**される (`styleBlock` 側と違いラップ・フォールバックしない)

**制約**:

- **formDesigner / WEBフォーム管理のプレビューでは適用されない** (`applyGlobal: false`)。プレビューは iframe ではなく編集画面と同一 document なので、効かせると編集画面自体が壊れるため。効果確認は実際の文書画面で行う
- 承認履歴は履歴 0 件時に `rendered=false` で **DOM 自体が無い**。JS 側から触るなら null ガード必須
- 1 フォームに `styleBlock` と `styleBlockGlobal` を両方置ける (別々の `<style>` に注入され、global → scoped の順で head に入る)
- **管理系画面と共有しているシェルクラス (`.headerActionArea` 等) を狙わない**。WEBフォーム管理画面は同一 document 上に `Formio.createForm` でプレビューを描画し、管理画面自身も `.headerActionArea` (保存ボタン等) を持つため、こうした CSS は **WEBフォーム管理画面の操作に悪影響を与える**ことが実運用で確認されている (実運用で採用→撤去の実績あり)。狙ってよいのは `.cf-panel-<キー>` のような文書表示ページ固有のフックに限る

**旧方式 (互換のため残置。新規では使わない)**: 専用 htmlelement の `customConditional` から未スコープ `<style>` を直接注入するハック。旧バージョンの consentFlow ではこちらしか動かない。

```js
// 旧方式 — 新規実装では styleBlockGlobal を使うこと
(function () {
  if (!document.getElementById('cfApprovalPanelsHideStyle')) {
    var st = document.createElement('style');
    st.id = 'cfApprovalPanelsHideStyle';
    st.textContent = 'div[id$="approvalFlowPanel"]{display:none;} div.card-outline:has(.approvalHistoryTable){display:none;}';
    document.head.appendChild(st);
  }
})();
show = false;   // ヘルパー自体は常に非表示
```

- `cf-panel-*` が無い旧バージョンでは、承認フローは `id$="approvalFlowPanel"`、承認履歴は外枠 div に id が無いため内部 table の `.approvalHistoryTable` を `:has()` (CSS) / `closest('.card-outline')` (JS) で遡るのが唯一の hook だった
- **横展開の定石**: 「`styleBlockGlobal` による初期非表示 + 表示切替ボタン」の 2 部品構成にし、新旧セレクタを OR で併記すると旧バージョンでも動く

### 実装挙動の詳細 (デバッグ時に役立つ)

コアの styleBlock 注入処理は CSSOM (`CSSStyleSheet.replaceSync` + `cssRules` 走査) でルール単位にパースして再シリアライズする:

| ルール種別 | 番号 | 挙動 |
|---|---|---|
| STYLE (通常ルール) | 1 | 各セレクタ先頭に `.formio-form` を付与。ただし既に `.formio-form` で始まっていればそのまま (`scopePrefixRegExp` で判定) |
| MEDIA | 4 | `@media ... { ... }` の内側ルールを再帰的にスコープ化 |
| SUPPORTS | 12 | `@supports ... { ... }` の内側ルールを再帰的にスコープ化 |
| FONT_FACE | 5 | スコープ化されず原文のまま `<style>` に出る (= グローバル) |
| KEYFRAMES | 7 | スコープ化されず原文のまま `<style>` に出る (= グローバル) |

- セレクタ判定の正規表現は `^\.formio-form(?=[\s>+~.:\[]|$)` — 直後が組み合わせ子・擬似クラス・属性セレクタ・終端なら「既に scope 済み」とみなす
- **CSS パース失敗時のフォールバック**: 全文を `.formio-form {\n<元 CSS>\n}` で包んで吐く。ネストルールを正しく解釈できるブラウザでないと装飾が崩れる可能性があるので、構文エラーを起こさないこと
- 注入された `<style>` には `data-cf-styleblock="1"` 属性が付く。既存除去オプション付きの再注入で除去・再注入できる (プレビュー再描画パス)

### スコープ化されない要素を狙うアンチパターン

```css
/* ❌ ダメ: .navbar はアプリヘッダ。自動的に .formio-form .navbar になり、フォーム内にそんな要素は無いので無効 */
.navbar { background: navy; }

/* ❌ ダメ: 裸の h2 はフォーム内のすべての h2 を変える。プレビューや panel ヘッダーまで巻き込む */
h2 { color: red; }

/* ✅ OK: 特定コンポーネントだけに効かせる */
.formio-component-content2 h4 { background: linear-gradient(...); }

/* ✅ OK: key で絞り込み (`formio-component-<key>` クラスが付与される) */
.formio-component-applyButton button { background: #007bff; }
```

### 3.1 styleBlock テンプレート運用 (推奨)

フォーム横展開を見据え、styleBlock の CSS は「テンプレートファイル名 = テンプレート ID」で管理するテンプレートライブラリに集約するのが推奨。新規フォームでは採用テンプレートを 1 つ選び、CSS 全文を styleBlock コンポーネントの `content` フィールドにコピペする運用。提供 class は `lc-*` プレフィックスの customClass 規約 (例: `.lc-field-label` / `.lc-section-header` / `.lc-staff-cell` / `.lc-segmented`) で統一すると横展開しやすい。

将来的には JSON 側に `customProperties.templateRef: "<テンプレートID>"` のような参照を書くだけで読み込み時に CSS を注入する「ランタイム解決」方式に移行する想定だが、現段階は手動コピペ運用 (ファイル名 = テンプレート ID として運用しておくことで将来の移行が容易)。

## 4. `modalEdit: true` Panel をダイアログとして使う

Form.io の `panel` コンポーネントに `"modalEdit": true` を付与すると、その panel は **モーダルダイアログとしてレンダー** される。インライン領域に「開く」ボタンとプレビューだけが表示され、ボタンクリックで panel 中身がモーダル内に展開する。複雑な選択 UI (有資格者ピッカー、マスタ検索、サブフォーム的なフィールド群) を「親フォームを汚さずに小さく押し込む」用途に有用。

実例 (いずれも実運用フォーム):
- タスク担当者ピッカー 16 セル (`Picker_p_<role>`)
- 既設機器選択パネル
- 科目マスタピッカー

### 4.1 最小定義

```jsonc
{
  "type": "panel",
  "modalEdit": true,
  "key": "Picker_p_buchou",
  "title": "所属長を選択",   // モーダルのタイトル + 開くボタンのラベル
  "label": "所属長を選択",   // title と揃える
  "input": false,
  "collapsible": false,
  "customClass": "lc-staff-picker",
  "components": [
    /* モーダル中身 (Select / button / 任意のコンポーネント) */
  ]
}
```

### 4.2 自動生成される DOM 構造

panel 本体の `<div class="formio-component-panel">` に加え、Form.io が **以下のラッパ要素群を自動生成** する:

```
.formio-component-panel
  └ [ref="openModalWrapper"]
       ├ label.control-label          ← panel.label の表示
       ├ <br>
       ├ button.open-modal-button     ← 「開く」ボタン (自動生成)
       ├ [ref="modalLabelValue"]      ← 現在値のプレビュー (text)
       └ [ref="modalPreviewLiveRegion"] ← a11y 用 (visually hidden)
.formio-dialog (モーダル本体、document.body 直下ではなく panel の子として描画される)
  ├ .formio-dialog-overlay
  └ .formio-dialog-content
       ├ [ref="modalContents"]        ← panel.components がレンダーされる場所
       └ .formio-dialog-buttons
            ├ button[ref="modalSave"]   ← 「保存」ボタン (自動生成)
            └ button[ref="modalClose"]  ← 「閉じる」ボタン (自動生成)
```

ポイント:

- **モーダルは `document.body` 直下に portal されない** — `.formio-component-panel` の子として DOM に存在する。これにより §3 styleBlock の自動スコープ (`.formio-form` 配下) が **モーダル内部にも効く**
- 「開く」ボタンは自動生成。ラベルは `panel.label` (空にすると「Edit」になる)
- 「保存」「閉じる」ボタンも自動生成。**プログラム的に閉じたいときは必ず `[ref="modalSave"]` を click する** (§4.6 参照) — `[ref="modalClose"]` を click すると「変更内容は保存されません」確認ダイアログが出る
- `.formio-dialog-buttons` (「保存」ボタンを包む) と `.lc-staff-picker` (panel) は **DOM 上は sibling**。子孫セレクタで hide しようとしても効かない (§4.6)

### 4.3 モーダル内のボタンから親フォームへ値を書く

モーダル中身に置いた button (`action: "custom"`) の `custom` JS スコープには **ルートフォームの `form` が注入される** ([scripting.md](/docs/scripting.md) の button.action=custom スコープ参照)。これを使えばモーダル内の Select で選んだ値を親フォームの textfield に setValue して、続けてモーダルを閉じる、という典型パターンが書ける。

```javascript
// === modalEdit Panel 内「セットする」ボタンの custom 例 ===
XSP.allowSubmit();

// モーダル中身の Select を取得 — instance.parent を遡って同じ panel 内を探す
var panel = instance.parent;
while (panel && panel.type !== 'panel') panel = panel.parent;
var sel = null;
if (panel) {
  panel.everyComponent(function (c) {
    if (c.component && c.component.key === 'pickerSelect_p_buchou') { sel = c; return false; }
  });
}

// Select の値 (dataType:"object" 前提で行オブジェクト) を取得して検証
var picked = sel ? sel.dataValue : null;
if (!picked || typeof picked !== 'object' || picked['@category'] === true || !picked.shimei) {
  alert('有資格者を選択してください');
  return;
}

// 親フォームの textfield に書き込む
form.getComponent('p_buchou').setValue(picked.shimei || '');
form.getComponent('p_buchou_1').setValue(picked.bumon1 || '');

// ★ ダイアログ内ピッカーの値をリセット — サーバーに送信されないようにする (詳細は下記)
var selComp = form.getComponent('pickerSelect_p_buchou');
if (selComp) { try { selComp.setValue(null); } catch(e){} }
try { delete data.pickerSelect_p_buchou; } catch(e){}
try { if (root && root.data) delete root.data.pickerSelect_p_buchou; } catch(e){}

// モーダルを閉じる — 自動生成の [ref="modalSave"] を programmatic click。
// 注: [ref="modalClose"] (X ボタン / .formio-dialog-close) を click すると
// 「変更内容は保存されません。よろしいですか？」確認ダイアログが出てしまうので必ず modalSave を使う (§4.6 参照)。
var modalRoot = instance.element ? instance.element.closest('.formio-dialog-content') : null;
var saveBtn = modalRoot ? modalRoot.querySelector('[ref="modalSave"]') : null;
if (saveBtn) saveBtn.click();
```

ポイント:

- `instance.parent` を `type === 'panel'` まで遡れば、同じモーダルの panel ハンドルが得られる
- `panel.everyComponent(fn)` でモーダル中身を走査して目的の Select を取得
- 親フォームの textfield へは `form.getComponent(key).setValue(value)` で書き込み (config も `data.<key>` に反映される)
- **「親画面に値をセットするためだけの一時フィールド」は modalSave 直前にリセットする** (§4.3.1 — 忘れると重い行オブジェクトがそのままサーバーへ送信される)
- **モーダルクローズは必ず `[ref="modalSave"]` を click する** — `[ref="modalClose"]` (X ボタン) を click すると Form.io が「未保存の変更を破棄しますか?」の確認ダイアログを出してしまう (§4.6)

### 4.3.1 ダイアログ内の一時フィールドはセット後にリセットする (サーバー永続化を避ける)

`modalEdit` Panel 内の Select / textfield などが **「親画面にセットするだけの一時フィールド」** (= Notes 側に独立アイテムとして保存したくないフィールド) の場合、modalSave click の **直前** に必ず値をクリアする。これを忘れると `data.<tmpKey>` がそのままコアの保存処理で `CF_<tmpKey>` に永続化されてしまう。

特に問題になるのは Select を `dataType: "object"` で使うケース ([server-data.md](/docs/server-data.md) の行オブジェクト保持パターン)。選択した 1 行分の全カラム (`shimei`, `bumon1`, `@unid`, …) が丸ごと文字列化されて Notes に書き込まれるため、文書サイズが膨れ、再オープン時に Form.io が予期しない型 (object) を受け取って描画が壊れることもある。

**リセットパターン (3 段必須)**:

```javascript
// 1) コンポーネント API — UI 表示と Form.io 内部状態の両方をクリア
var selComp = form.getComponent('pickerSelect_p_buchou');
if (selComp) { try { selComp.setValue(null); } catch(e){} }

// 2) ボタン JS の data スコープから削除 — submission 直前の値を消す
try { delete data.pickerSelect_p_buchou; } catch(e){}

// 3) ルート data からも削除 — 入れ子フォーム / panel 越しに残る経路を塞ぐ
try { if (root && root.data) delete root.data.pickerSelect_p_buchou; } catch(e){}
```

3 つとも必要な理由:
- `setValue(null)` だけだと Form.io バージョンによっては `data.<key>` に空の object/string が残ることがある
- `delete data.<key>` だけでも submission には載らないが、`root.data` (= 全体 submission) 側に同名キーがあると Form.io がそちらを優先する経路がある
- 両方 delete してから modalSave すると、確実に payload から消える

**鉄則**: 「ダイアログ内の値が親フォームの textfield に転記されたら、その元データはダイアログ閉じる前に毎回捨てる」を常にセットで実装する。modalSave の click はリセット **後** に置くこと (前に置くと delete が反映されないことがある)。

Select を `dataType: "object"` で使う他のフォームでも同じ罠が再発する。実際に発生した事故では、選択行の全カラム (`{shimei, bumon1, @unid, ...}`) が `CF_<tmpKey>` に丸ごと文字列化保存され、文書サイズ膨張と再オープン時の型不一致 (object を期待しないコンポーネントが object を受け取って描画破綻) の二重被害になった (現行版では修正済)。

実例: 担当者ピッカー 16 セルの `pickerSetBtn_p_<role>` すべてが本パターン (実運用フォーム)。サーバーに送信はされるが値はブランクになることを実機で確認済。

### 4.4 Bootstrap `.card` 装飾の打ち消し + 推奨レイアウト (Choices.js dropdown overflow)

Form.io は panel を **自動的に `.mb-2.card.border > .card-header + .card-body` で wrap** する。`modalEdit` モーダル内では、dialog の白いボックス + 内側の panel の `.card` ボックスで **入れ子の二重ボックス** に見える (§4.6 の screenshot 例)。

**推奨レイアウト (担当者ピッカー 16 セルで実機確定)**:

- dialog は中身に合わせて **コンパクトに auto-size** させる (min-height は付けない)
- dropdown は `overflow: visible` を介して **dialog の枠を超えて自然に外側へ展開させる** ← これが最も見やすい UX (実機で確認済)
- dropdown 自身の `max-height` は 220px (≈5〜6 行) でキャップ、それ以上は dropdown 内部スクロール

「dropdown を dialog 内に押し込もう」とすると dialog が縦に大きくなり、それでも dropdown は flow 外なので clip は完全には消えず半端な見た目になる。**ベストは「dialog はコンパクト、dropdown は dialog の外まで伸びてよい」**。dialog 高さは「中身の flow 高さ」でしか決まらないので、dropdown サイズで dialog が膨らむ発想は最初から成立しない (`position: absolute` で flow 外に出るため)。

打ち消す CSS は **panel 自身 (`.<customClass>`) の直接の子としての `.card`** を狙う:

```css
/* panel の .card 装飾を打ち消し (panel = .lc-staff-picker, .card は直接の子) */
.lc-staff-picker > .card,
.lc-staff-picker > .mb-2.card,
[class*="formio-component-Picker_p_"] > .card {
  border: none !important;
  box-shadow: none !important;
  margin: 0 !important;
  background: transparent !important;
  border-radius: 0 !important;
}
/* card-header を「dialog タイトル」風に restyle (default はグラデの濃い箱型) */
.lc-staff-picker > .card > .card-header {
  background: #fff !important; background-image: none !important;
  color: #2d3748 !important; font-size: 15px !important; font-weight: 600 !important;
  padding: 14px 20px !important;
  border: none !important; border-bottom: 2px solid #4a5568 !important;
  border-radius: 0 !important; box-shadow: none !important;
}
.lc-staff-picker > .card > .card-body {
  padding: 18px 20px 16px 20px !important;
  background: #fff !important;
}
/* dialog 本体: コンパクトに auto-size + dropdown を外側に逃がす設定。
   - min-height は付けない (dialog は中身ぴったりに縮ませる)
   - max-height 90vh: 小画面でも viewport を越えないための上限のみ
   - overflow: visible: dropdown が dialog の枠を超えて外側へ展開できるようにする ←★ 推奨 UX の核
   ★セレクタ方向に注意: dialog は panel の **祖先**。
     `.lc-staff-picker .formio-dialog-content` は逆向きで何もマッチしないので
     `:has()` で逆走査する (modern browser 全対応) */
.formio-dialog:has(.lc-staff-picker) .formio-dialog-content {
  max-height: 90vh !important;
  overflow: visible !important;
}
/* overflow: visible を間の Bootstrap wrapper にも伝播。
   どこか 1 段でも overflow:hidden が残ると dropdown が dialog 外に出ようとした時点で clip される。
   .card は border-radius の都合で overflow:hidden になっているテーマがあり要注意。
   [ref="modalContents"] は dialog 内の wrapper なので :has() 経由で取る。
   .card / .card-body は panel (.lc-staff-picker) の子なので直接 > でOK。 */
.formio-dialog:has(.lc-staff-picker) [ref="modalContents"],
.lc-staff-picker > .card,
.lc-staff-picker > .mb-2.card,
.lc-staff-picker > .card > .card-body,
.lc-staff-picker > .mb-2.card > .card-body {
  overflow: visible !important;
}
/* dropdown の上限行数だけ抑える (dialog 内に収めるためではない! dropdown 自体が
   画面の半分を覆ってしまわないためのキャップ)。
   220px / ~38px-per-row ≈ 5〜6 行表示。それ以上は dropdown 内部スクロール。
   dialog の外に飛び出すこと自体は許容 (= 推奨レイアウト)。
   dropdown は panel の **内側** に展開されるので素直な descendant でOK。 */
.lc-staff-picker .choices__list--dropdown {
  max-height: 220px !important;
}
/* 自動「保存」ボタンを隠す (sibling 結合子で! descendant ではマッチしない) */
.lc-staff-picker ~ .formio-dialog-buttons {
  display: none !important;
}
```

**鉄則**:
- `customClass` (上の例では `lc-staff-picker`) で **必ずスコープを限定**。`.formio-dialog .card { ... }` を裸で書くと **全モーダルの装飾を破壊** する
- `.<customClass> > .card` のように **直接の子コンビネータ `>` を使う** (§4.6 のセレクタ罠参照)
- `.formio-dialog-buttons` の hide は **sibling 結合子 `~`** が必須 (descendant ではマッチしない、同 §4.6)
- **セレクタ方向の罠** (実機で踏んだ): modalEdit Panel の dialog は panel の **祖先** として描画される (DOM 構造: `.formio-dialog > .formio-dialog-content > [ref="modalContents"] > .<panel-customClass>`)。直感に反して `.<panel-customClass> .formio-dialog-content { ... }` は descendant 方向が **逆** で何もマッチしない (Form.io 既定の `width:80%; background:#f0f0f0` が勝ち続ける)。正解は `.formio-dialog:has(.<panel-customClass>) .formio-dialog-content { ... }`。panel 内 (`.<customClass>` 配下) のコンポーネント (dropdown / .card / .card-body 等) を狙うのは普通の descendant でOK

### 4.5 「開く」ボタンをアイコン化したい

`.open-modal-button` を CSS で 26×24px の地味なアイコンボタンに置き換える例 (担当者ピッカー 16 セルで実機確認済)。

**★ セレクタは panel の customClass (`.lc-staff-picker`) ではなく「panel の祖先に必ず存在するクラス」(ここでは親セルの `customClass="lc-staff-cell"`) でアンカーする。** 「開く」ボタンと openModalWrapper は `formio-component-<key>` / `customClass` ラッパの *外側* に描画されるため、`.lc-staff-picker .open-modal-button` は 1 件もマッチしない (下の ⚠️ 落とし穴を必ず読むこと、実機で確認済):

```css
.lc-staff-cell .open-modal-button {
  width: 26px; height: 24px; min-height: 24px;
  padding: 0; font-size: 0; line-height: 0;
  background: #4a5568; color: #fff;
  border: 1px solid #2d3748; border-radius: 3px;
  position: relative; display: inline-block;
}
.lc-staff-cell .open-modal-button::before {
  content: '⋯';   /* テキストにしたいなら content: '選択'; font-size:13px; などに変える */
  font-size: 14px; line-height: 1; color: #fff;
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
}
/* プレビュー (現在値表示) とラベルを隠す */
.lc-staff-cell [ref="openModalWrapper"] > label.control-label,
.lc-staff-cell [ref="openModalWrapper"] > br,
.lc-staff-cell [ref="modalLabelValue"],
.lc-staff-cell [ref="modalPreviewLiveRegion"] { display: none !important; }
```

> 注: テキストボタン (「選択」等) にする場合は `::before` を absolute ではなく inline (デフォルト) のまま `content:'選択'; font-size:13px;` とし、ボタンは `width:auto; padding:0 12px; line-height:24px;` にすると文字幅にフィットする (font-size:0 でネイティブの「開く」を隠し、::before が幅を作る)。

> ⚠️ **重大な落とし穴 — 「開く」ボタンは component の `formio-component-<key>` / `customClass` ラッパの *外* にある** (担当者ピッカー 16 セルで実機確認済)
>
> modalEdit panel の実 DOM は次の二層構造になっており、**`customClass` (例 `lc-staff-picker`) と `formio-component-<key>` クラスは「ダイアログ内側の複製 component」にしか付かない**:
> ```
> div.formio-component-modal-wrapper-panel        ← ここに customClass は付かない
>   ├ [ref="openModalWrapper"]
>   │   ├ label.control-label                      ← パネルラベル (「開く」前の見出し)
>   │   └ button.open-modal-button  「開く」        ← トリガー (これを restyle したい)
>   └ [ref="modalWrapper"].formio-dialog
>       └ [ref="modalContents"]
>           └ div.formio-component-panel.formio-component-<key>.<customClass>  ← ここだけに付く
> ```
> したがって `.lc-staff-picker .open-modal-button` や `[class*="formio-component-Picker_p_"] .open-modal-button` のように **panel 自身の key/customClass を経由する子孫セレクタはトリガーボタンに 1 件もマッチしない** (ボタンは「開く」のまま、ラベル隠しも効かない)。
>
> ✅ **対策: トリガーボタン/openModalWrapper を狙うときは「panel の *祖先* に必ず存在するクラス」をアンカーにする。** この例では親の担当者セル panel の `customClass="lc-staff-cell"` を使う:
> ```css
> .lc-staff-cell .open-modal-button { /* font-size:0 + ::before でラベル差し替え */ }
> .lc-staff-cell [ref="openModalWrapper"] > label.control-label,
> .lc-staff-cell [ref="modalLabelValue"],
> .lc-staff-cell [ref="modalPreviewLiveRegion"] { display: none !important; }
> ```
> 親に都合のよい customClass が無いケースは、ピッカー panel を `lc-staff-cell` 相当の薄いラッパ panel で包んでアンカー用クラスを与えるか、`.formio-component-modal-wrapper-panel` を起点にする。
>
> なお、`§4.4` で扱う **ダイアログ内側** の装飾 (`.<customClass> > .card` 等) は customClass がダイアログ内 component に付くので従来どおり `.lc-staff-picker ...` で正しく効く。**`customClass` が効く/効かないは「ダイアログの内か外か」で分かれる** のがポイント。

### 4.6 落とし穴と注意点

**`input: false` を明示する**
- panel は本来 input ではないが、`modalEdit: true` を付けると Form.io が「panel 全体の値を保存する」気になってしまうことがある (バージョン依存)
- `input: false` を明示しておくとモーダル内の各 Component が個別に親 `data.<childKey>` に書き込む挙動になり、consentFlow のフラット保存モデルと整合する

**モーダルを閉じるときは必ず `[ref="modalSave"]` を click する (`modalClose` ではない)**
- ❌ `[ref="modalClose"]` (= 右上の X ボタン / `.formio-dialog-close`) を `.click()` すると、Form.io が **「変更内容は保存されません。よろしいですか?」確認ダイアログを表示してしまう** (Form.io が「ユーザーが破棄しようとしている」と解釈するため)
- ✅ `[ref="modalSave"]` (= 自動生成の「保存」ボタン) を `.click()` すれば、確認なしで panel 状態を commit して閉じる挙動になる
- 自動生成の「保存」ボタンは §4.4 の styleBlock CSS で `display: none` にして UI 上は見えなくし、独自の「セットする」等のボタンから `modalSave` を programmatic click する構成が定石
- **Why**: Form.io 内部で modalClose は「破棄」、modalSave は「commit」と意味付けされている。前者は dirty 状態だと確認 dialog を出す

```javascript
// ✅ OK — 確認ダイアログを出さずにモーダルを閉じる
var modalRoot = instance.element.closest('.formio-dialog-content');
var saveBtn = modalRoot && modalRoot.querySelector('[ref="modalSave"]');
if (saveBtn) saveBtn.click();

// ❌ NG — 「変更内容は保存されません」確認ダイアログが出てしまう
var dlg = instance.element.closest('.formio-dialog');
var closeBtn = dlg && dlg.querySelector('[ref="modalClose"]');
if (closeBtn) closeBtn.click();
```

実例: 担当者ピッカー 16 ボタンすべてが modalSave 方式に統一 (実運用フォーム)。

**datagrid/editgrid 行内ボタンから modalSave を click するときは「参照確保 → click → 後始末」の順**
- 行内ボタンの `custom` JS で、`grid.setValue([])` 等の**自分自身の行を含む再描画**を先に実行してから `instance.element.closest('.formio-dialog-content')` を呼ぶと、行の再構築で `instance.element` が DOM から外れて **closest() が null → modalSave click が空振り → ダイアログが閉じない**
- タイミング依存 (再描画が非同期なら偶然動く) なので「1 回目は閉じるのに 2 回目の検索後は閉じない」という再現になりやすい (ダイアログ内 View 検索ピッカーで実際に発生)
- ✅ 正解の順序: ① `modalRoot` / `saveBtn` への参照を**ハンドラ冒頭で確保** ② 親フィールドへ setValue ③ `saveBtn.click()` ④ グリッドクリア等の後始末は **click の後** (グリッドが `persistent:false` でも既定のアプリ設定では保存されるので、残骸行を保存したくなければクリアは必要。設定「persistent:false / Client-only の項目を保存しない」が はい の環境なら保存に影響しない)
- 補足: DataGrid は `setValue([])` 後も最低 1 行の空行を残すため、残骸を見せたくなければ `customConditional` で「実データがある時のみ表示」にする (`show = !!(data.<grid> && data.<grid>.length && data.<grid>[0].unid);`)
- ⚠ **自値参照の customConditional には `clearOnHide: false` が必須** — Form.io の `dataValue` setter は「非表示 + clearOnHide 有効」のコンポーネントへの書き込みを**黙って拒否**する (`!this.visible && this.component.clearOnHide` ガード)。既定 (clearOnHide:true) のままだと「空 → 非表示 → setValue 無視 → 値が入らず条件が真にならない」**デッドロックで永久に表示されない** (検索ステータスだけ更新されグリッドが出ない症状)

**Choices.js dropdown は dialog の外に overflow させるのがベスト** (実機確定)
- モーダル内に Select (Choices.js widget) を置くと、dropdown 部分は `position: absolute` で flow 外に展開される
- `.formio-dialog-content` のデフォルトは `overflow: hidden` 系なので、何もしないと dropdown が **dialog の下辺で切れて見えなくなる**
- ❌ **dialog 側を vh で大きくするアプローチは効かない**: dropdown が absolute で flow 外なので、**dialog の高さは dropdown 高さに追従しない**。`min-height: 60vh` を指定しても縦に空き padding が残るだけで dropdown clip は解消しない
- ❌ **「dropdown を dialog 内に押し込む」も最適解ではない**: dialog が縦に大きくなる割に dropdown の選択肢可視数は増えず、半端な見た目になる
- ✅ **正解は「dialog はコンパクトに保ち、dropdown は dialog の外に伸びてよい」**:
  - dialog: `.formio-dialog:has(.<customClass>) .formio-dialog-content { max-height: 90vh !important; overflow: visible !important; }` (auto-size + 小画面保険のみ。min-height は付けない。`overflow: visible` で dropdown を外側に逃がす)
  - 間の Bootstrap wrapper にも overflow 伝播: `.<customClass> > .card`, `.<customClass> > .card > .card-body`, `.formio-dialog:has(.<customClass>) [ref="modalContents"]` 全てに `overflow: visible !important`。1 段でも `overflow:hidden` が残るとそこで dropdown が clip される
  - dropdown 自身は **画面全体を覆わない上限**として `.lc-staff-picker .choices__list--dropdown { max-height: 220px !important; }` で ≈5〜6 行にキャップ (dialog 内に収めるためではない)
  - **★ セレクタ方向の罠**: dialog (`.formio-dialog` / `.formio-dialog-content`) は panel の **外側 (祖先)** に描画される。`.<customClass> .formio-dialog-content` のような子孫セレクタは方向が逆で**マッチしない** (Form.io の `width:80%; background:#f0f0f0` がそのまま残る)。 `:has()` で逆走査するのが modern browser 標準対応の解。dropdown / .card / .card-body は panel の **内側** なので普通の descendant でOK。dialog 自体や `[ref="modalContents"]` を狙う時だけ `:has()` を使う

**Panel の Bootstrap `.card` 装飾はモーダル内で「二重ボックス」になる**
- Form.io は panel コンポーネントを自動的に `.mb-2.card.border > .card-header + .card-body` で wrap する
- モーダル内に panel を入れると、dialog の白いボックス + 内側の panel の `.card` ボックスで **入れ子の二重ボックス** に見える
- 対処: §4.4 の打ち消し CSS。**セレクタは `.<customClass> > .card`** (= panel 自身を `.<customClass>` でつかみ、その直接の子の `.card`) で書く。`.<customClass> .formio-dialog-content > .card` のような書き方は **`.formio-dialog-content` が panel の外側にあるため絶対にマッチしない** ので注意

**`.formio-dialog-buttons` は panel の sibling — 子孫セレクタでは hide できない**
- 自動生成の「保存」ボタンを含む `.formio-dialog-buttons` は、DOM 上 panel (`.<customClass>`) と **同階層 (sibling)** に置かれる
- `.<customClass> .formio-dialog-buttons { display: none }` は **descendant 結合子なのでマッチしない**
- 正しくは sibling 結合子 `~` を使う: `.<customClass> ~ .formio-dialog-buttons { display: none !important; }`

**`refreshOn` で再 fetch しても以前の値が残る場合**
- モーダル内 Select に `refreshOn: "jigyo"` を付けて親フィールド変更時に再 fetch する場合、`clearOnRefresh: true` を併設しないと **古い選択値が新しいオプションリストに無いまま残る**
- Choices.js のレンダリングが破綻するので、`clearOnRefresh: true` は基本的にセットで指定

**参照モードでも「開く」ボタンが押せてしまう**
- §2 の disabled 処理は modalEdit Panel の `.open-modal-button` を対象に含めていない
- 必要なら styleBlock で `.formio-read-only .open-modal-button { pointer-events: none; opacity: 0.5; }` を追加するか、Panel 自体に `customConditional: "show = cfGlobal.editable;"` を付けて参照モードでは丸ごと隠す ([scripting.md](/docs/scripting.md) の `customConditional` 参照)

## 5. radio / selectboxes を「ボタン群 (segmented control)」に CSS で見せるとき

likert (5段階) や単一選択の radio を、横並びのボタン群として見せたいことがある (例: `.lc-segmented`)。標準形は `customClass` をコンポーネントに付けて CSS で各選択肢を装飾する:

```jsonc
{ "type": "radio", "customClass": "lc-segmented", "inline": true,
  "values": [ {"label":"とてもそう思う","value":"5"}, ... ] }
```

### 落とし穴: 選択状態が出ない (= 「選択が押せない」ように見える)

Form.io 4.x の radio/checkbox は **`<label class="form-check-label">` の内側に `<input class="form-check-input">` がある入れ子構造**でレンダリングされる (古い版や widget によっては input が label の兄弟のこともある)。このため:

```css
/* ❌ 兄弟セレクタ (+) は input が label の内側だとマッチしない → 選択しても色が変わらない */
.lc-segmented .form-check-input:checked + .form-check-label { ... }
```

クリック自体はネイティブ (label 内 input) で効いて**値は入っている**が、見た目が変わらないのでユーザーには「押せない / 反応しない」不具合に見える。

**How to apply:** `:has()` と兄弟 `+` の**両方**を指定してバージョン差を吸収する:

```css
.lc-segmented .form-check-input:checked + .form-check-label,
.lc-segmented .form-check-label:has(.form-check-input:checked) { /* selected style */ }
.lc-segmented .form-check-input:focus + .form-check-label,
.lc-segmented .form-check-label:has(.form-check-input:focus) { /* focus ring */ }
```

`:has()` は 2023 以降の Chromium/Edge/Firefox/Safari で利用可。input は `position:absolute; opacity:0; width:1px; height:1px; pointer-events:none;` で視覚的に隠し、label をボタンとして装飾する (`display:block; width:100%`)。input を隠してもクリックは label が受けるので選択は動く。

> 関連: 複数選択の件数制限を `validate.custom` で書くときは `input` が string|array 両方来るので配列正規化が必須 ([scripting.md](/docs/scripting.md) §4)。

## 6. 「ステップ切替」を panel × `customConditional` で作るときの素通りゲート

wizard 表示を使わず、トップレベル `panel` 4 枚を隠しフィールド (例 `panelSWFlag`) の値で出し分ける「自作ステップ」は、次の 3 点が重なると **必須項目が空のまま提出が通る**。

1. **非表示パネルは検証されない** — Form.io の `checkValidity` は `checkCondition()` が偽のパネルを丸ごと skip し、子も `shouldSkipValidation` (`!visible`) で valid 扱い。「提出」ボタンの `form.checkValidity(...)` は**表示中のステップしか見ない**
2. **切替フラグは保存される** — `panelSWFlag` が persistent なら下書き再オープン時に**保存したステップに着地**し、前のステップの「次へ」検証を再び通ることはない
3. **`clearOnHide` は親パネルの設定に関係なく子へ伝播** — `NestedComponent.clearOnHide()` は常に `children.forEach(c => c.clearOnHide())` を呼ぶ。パネル自身が `clearOnHide:false` でも、子が `clearOnHide:true` (既定) なら「戻る」でパネルが隠れた瞬間に `deleteValue()` される。削除後は `undefined` になるため、simple 条件 `when: X, eq: "false"` 型の required ロジックは `String('') === 'false'` が偽で発火しない

実例: 複数ステップの申請フォームで、一時保存 → 再オープン (最終ステップに着地) → 提出、で税ラジオ・通勤手段・健康診断 (checkbox/file/selectboxes は clearOnHide:true) が空のまま提出された。

**How to apply (定石)**:

```js
// panelStepN.customConditional — ビルダー / 検証中フラグ / 参照モード / 申請後 は全ステップ表示
show = (typeof cfGlobal === 'undefined') || (window.__cfShowAllSteps === true) || !cfGlobal.editable
    || (cfGlobal.statusNo !== "" && cfGlobal.statusNo !== "0") || data.panelSWFlag == "stepN"
```

```js
// 提出ボタン (action:"custom") — 全ステップを表示状態にしてから全体検証。OK なら全表示のまま XPages 提出を click
if (event) { event.preventDefault(); event.stopPropagation(); }
window.__cfShowAllSteps = true;
form.checkConditions(form.data);
setTimeout(function () {
  if (!form.checkValidity(null, true, null, false)) { form.showErrors(); window.scroll({ top: 0, behavior: 'instant' }); return; }
  var btn = Array.from(document.querySelectorAll("button[id$='button1']")).find(function (b) { return b.textContent.trim() === "提出"; });
  if (btn) btn.click();
}, 100);
```

- ステップ配下の data 型コンポーネントは **すべて `clearOnHide:false`** にする (ビルダー既定は true。checkbox / file / selectboxes / 隠し helper が漏れやすい)
- required を条件付きにするなら trigger は javascript `result = (data.X !== true)` にし、`undefined` も required 側に倒す
- 提出後に flag を戻すと非表示化で `clearOnHide` が走るので、戻さずに XPages 提出へ渡す
- 参照モードでの全表示 (`!cfGlobal.editable`) は承認者の `checkValidity` にも効く。承認 (approve) は参照モードでも全体検証される ([scripting.md](/docs/scripting.md) §8) ため、隠しステップ側の必須欠落を承認者が見落とすことも防げる

### 6.1 切替フラグは「保存値」ではなく**ページ変数**を読む — 開くたび先頭ステップに戻す

上記 2. (切替フラグが persistent) の根本対処。`panelStepN.customConditional` が **保存済みフラグを一切読まない**ようにすれば、再オープンは必ず先頭ステップに着地する。

```js
// panelStepN.customConditional — data.<flag> ではなくページ変数を読む
show = (typeof cfGlobal === 'undefined') || (window.__cfShowAllSteps === true) || !cfGlobal.editable
    || (cfGlobal.statusNo !== "" && cfGlobal.statusNo !== "0") || ((window.__cfStep || "step1") === "stepN")
```

```js
// 次へ / 戻る ボタン — ページ変数を更新し、data 書き込みは「変更検知トリガ」として残す
window.__cfStep  = 'step2';
data.<flag>      = 'step2';
```

- **`calculateValue` で保存フラグを先頭ステップに書き戻す案より安全**。コアのフォーム生成処理は `Formio.createForm(...).then(form => form.submission = {data: FieldJson})` と **createForm の後から非同期で submission を再適用**するため、ロード時に値を入れる実装はこの再適用と競合する ([scripting.md](/docs/scripting.md) §9 鉄則 (d))。条件式から直接ページ変数を読めば競合が構造的に起きず、ステップがちらつかない
- 保存値を書き換えないので **dirty フラグにも触れない**。`<prefix><flag>` は「最後に居たステップ」の記録として残り、障害解析に使える
- Form.io の `NestedComponent.checkConditions` は配下の全コンポーネントの条件式を毎回再評価する。条件式が `data` を参照しなくても、ボタンの `data.<flag>` 書き込みで確実に再描画される
- **前提**: この方式は開いた直後に後続ステップを非表示にするので、§6 の `clearOnHide:false` 徹底が**必須**。怠ると「開いた瞬間に後続ステップの値が消える」逆効果になる

### 6.2 ⚠ この修正はフォームの再エクスポートで消えることがある — 機械ガードを置く

実運用フォームで、§6 の修正一式 (全ステップ検証 / `clearOnHide:false` 18 項目 / 条件付き required の logic アクション / 不正エスケープ修復) が **フォームビルダーからの再エクスポートで丸ごと巻き戻った**実績がある (JSON をバージョン管理していなかったため 10 日間気付かれなかった)。

これらは「機能追加」ではなく**見た目に出ない安全機構**なので、次の編集者が意図せず落としても誰も気付かない。横展開するときは必ずセットで用意する:

1. **フォーム内の変更履歴コンポーネント** (`hidden:true` の content) に「この N 点は必須ブランク提出の防止機構。再エクスポート時に消さないこと」と明記する。変更履歴は JSON の中に居るのでビルダーの往復でも一緒に運ばれ、次の編集者の目に入る
2. **不変条件チェックスクリプト** (Python / Node の小さな検査スクリプト) を JSON と一緒に保管し、エクスポートし直すたびに流す運用にする。検査項目は「条件式にフラグ名が含まれるか」「対象キーの `clearOnHide` が false か」「ナビボタンがページ変数を更新しているか」「提出ボタンが全体検証版か」「条件付き required の logic アクションが残っているか」「strict JSON.parse が通るか」。終了コードで CI/手動どちらからも判定できるようにしておく

## Red Flags

- ❌ レイアウト用に `"type": "container"` を使う → データがネストしてフラット保存モデル (`<prefix><key>`) と不整合。`"type": "panel"` を使え (§1)
- ❌ radio/checkbox をボタン化するとき選択状態を兄弟セレクタ (`:checked + label`) だけで書く → Form.io 4.x は input が label の内側なのでマッチせず「選択しても色が変わらない」。`:has()` を併用しろ (§5)
- ❌ modalEdit panel を JS から閉じるのに `[ref="modalClose"]` を click → 「変更内容は保存されません」確認ダイアログ。`[ref="modalSave"]` を click (§4.3 / §4.6)
- ❌ 行内ボタンでグリッドをクリアしてから `instance.element.closest(...)` で modalSave を探す → 再描画で element が外れて null、ダイアログが閉じない (タイミング依存で 2 回目以降に顕在化)。参照確保 → click → 後始末の順にする (§4.6)
- ❌ styleBlock のセレクタに `body` / `.navbar` / 裸の `h1`〜`h6` 等のアプリ本体要素を書く → 自動スコープで `.formio-form body` 等になり無効化、または想定外要素を巻き込む (§3)
- ❌ styleBlock でフォーム外の XPages DOM (承認フロー/承認履歴/親子文書パネル等) を隠そうとする → 自動スコープでマッチ 0 件のまま無言で死ぬ。`key: "styleBlockGlobal"` のコンポーネントに `.cf-panel-<panelOrderキー>` で書く (§3)。旧バージョンで動いている styleBlock 直書きはコピーしても効かない
- ❌ フォーム外パネルを `[id$="…"]` や `:has(.approvalHistoryTable)` の逆引きで狙う → 生成 id は設計変更で動く / 内部要素依存は壊れやすい。コア提供の `.cf-panel-*` を使う (§3)
- ❌ `styleBlockGlobal` に `body` / `html` / `:root` / `*` を単独セレクタで書く → コアが drop して `console.error`。`.navbar` / `.main-sidebar` 等のアプリシェルは警告付きで通ってしまうので原則書かない (§3)
- ❌ modalEdit 内一時フィールドを modalSave 前にリセットしない → 行オブジェクトが `CF_<tmpKey>` に永続化される。`setValue(null)` + `delete data.<key>` + `delete root.data.<key>` の 3 段必須 (§4.3.1)
- ❌ modalEdit ダイアログの dropdown を dialog 内に押し込む → 中途半端な見た目。`overflow:visible` で dialog 外に逃がすのがベスト UX (§4.4)
- ❌ `.formio-dialog-buttons` を子孫セレクタ (`.lc-staff-picker .formio-dialog-buttons`) で hide しようとする → sibling なのでマッチしない。`.lc-staff-picker ~ .formio-dialog-buttons` を使え (§4.4 / §4.6)
- ❌ 自作再帰で leaf に到達しようとする → leaf 型でも `.components` が空でないことがある。`form.everyComponent(fn)` を使え (§2)
- ❌ panel × `customConditional` の自作ステップで、提出ボタンの `form.checkValidity` を表示中ステップだけに掛ける / ステップ配下に `clearOnHide:true` (既定) の子を残す → 非表示ステップの必須欠落が素通りし、「戻る」で子の値が消える。全ステップ表示フラグで検証 + 全 data 項目 `clearOnHide:false` (§6)
- ❌ 自作ステップの切替フラグを persistent のまま `customConditional` から読む → 下書き/差戻しの再オープンで保存ステップに着地し、前段ステップが未検証のまま提出できる。条件式はページ変数 (`window.__cfStep`) を読ませ、保存値は記録用に留める (§6.1)
- ❌ §6 の安全機構を入れたまま機械チェックを用意しない → 見た目に出ないのでフォームビルダーの再エクスポートで巻き戻っても誰も気付かない (実運用で 10 日間気付かれなかった実績あり)。フォーム内変更履歴への明記 + 不変条件チェックスクリプトをセットで置く (§6.2)
