cascader —— カスケードセレクト
カスケードセレクトは「省 > 市 > 区」のような階層を追って選ぶデータを扱います:上の階層を選んで初めて下の階層が現れます。core/cascader は share で最も重い core モジュールの一つです——ツリーデータの検索、パス復元、パネル生成、検索、正規化をすべて純関数にし、UI 層はパネル項目を列としてレンダリングするだけです。
- 各端の
LhCascader実装:多階層カテゴリ、地域選択、組織構造。 - データがツリー(またはツリーに正規化可能)で、各階層が排他的に選ばれる。
- 使うべきでない:単一階層の選択なら
select;親をチェックすれば子もチェックされるカスケードチェックが必要ならtree(checkStrictlyセマンティクスあり)。
インポート方法
Section titled “インポート方法”import { findNode, findPathByValues, getField, getChildren, getPanelItems, searchNodes, getSelectedLabelPath, getCascaderClassNames, getPanelClassNames, expandLastValue, isLeafNode, normalizeCascaderNodes, flattenNodes, getCascaderTriggerAria, getCascaderColumnAria, getCascaderItemAria,} from '@lh-ui/share/core/cascader'import type { LhCascaderNode, LhCascaderPath, LhCascaderPanelItem, LhCascaderState, LhCascaderSearchResult, LhCascaderItemConfig,} from '@lh-ui/share/core/cascader'いくつかの重要な設計:
- 検索重複排除は祖先チェインで行い、グローバルな visited は使わない:ツリー操作では一般に循環参照の保護が必要です。ここでは「祖先チェイン」で重複を判定し(
ancestorsパラメータを階層ごとに受け渡す)、モジュールドローやグローバルな Set は使いません——代償は保護が単一呼び出し内でしか有効でないこと、利点は DAG で正当に共有されるサブツリーを誤ってスキップしないことと、インスタンスを跨ぐ状態漏洩がないことです。 getFieldでフィールドのエイリアスを本当に効かせる:バックエンドのデータはvalue/label/childrenという名前とは限りません(id/name/subListの可能性もある)。LhCascaderItemConfigがエイリアスマッピングを宣言し、すべてのツリー操作がgetField経由で読み取ります。- 正規化は 1 回、あとは標準構造:
normalizeCascaderNodesが任意のフィールド構造の生ツリーをLhCascaderNode[]に変換し、以降のすべての関数は標準構造を扱います。正規化せずに itemConfig を直接渡すこともでき、両経路は等価です(ただし正規化後のほうがパフォーマンスが良く、階層ごとのエイリアス検索が不要です)。 - 検索結果は完全な祖先チェインを携える:
searchNodesが返す各結果にはpath(値の配列)、nodes(ノード配列)、pathLabels(ラベル配列)が含まれ、UI は「浙江省 / 杭州市 / 西湖」の 1 行をそのままレンダリングでき、再検索の必要がありません。 - プログラマティックな値反映は無効項目をフィルタしない:
findPathByValuesは意図的に無効な子ノードをフィルタしません——無効化は UI 層のクリック制約であり、プログラマティックな値反映(v-modelの初期値など)は任意のパスを復元できるべきです。さもなくば「後から無効化した値が反映時に失われる」ことが起きます。
| 関数 | シグネチャ | 戻り値 | 役割 |
|---|---|---|---|
findNode |
(nodes, value, strict?, ancestors?, itemConfig?) => { node, path } | null |
ノード + 祖先チェイン | 再帰検索 |
getField |
<T>(node, standard: string, configKey?: string) => T |
フィールド値 | エイリアスでノードの属性を読み取る |
findPathByValues |
(nodes, values, itemConfig?) => LhCascaderPath | null |
パス | 値の配列から階層を追って復元 |
getChildren |
(node, itemConfig?) => LhCascaderNode[] |
子の配列 | ダーティーデータは [] にフォールバック |
getPanelItems |
(nodes, expandedValues?, itemConfig?) => LhCascaderPanelItem[] |
パネル項目 | ツリー → パネル項目マッピング |
searchNodes |
(nodes, keyword, results?, itemConfig?, maxResults?) => LhCascaderSearchResult[] |
検索結果 | 大文字小文字を区別しない、デフォルト上限 50 |
getSelectedLabelPath |
(nodes, values, itemConfig?) => string |
"浙江省 / 杭州市" |
選択中ラベル文字列 |
expandLastValue |
(nodes, value, itemConfig?) => (string|number)[] | null |
パス配列 | emitPath: false のとき単一値からパスを復元 |
isLeafNode |
(node, itemConfig?) => boolean |
真偽値 | 明示的な leaf を優先、さもなくば children で判定 |
normalizeCascaderNodes |
(nodes: readonly unknown[], itemConfig?) => LhCascaderNode[] |
標準ツリー | 一括で正規化 |
flattenNodes |
(nodes, itemConfig?) => {node, path, pathValues}[] |
平坦化リスト | 各項目がパスを携える |
getCascaderClassNames |
(state) => string[] |
クラス名 | コンテナ |
getPanelClassNames |
(state) => string[] |
クラス名 | パネル(--hover-mode) |
export interface LhCascaderNode { value: string | number label: string disabled?: boolean leaf?: boolean children?: LhCascaderNode[]}
export interface LhCascaderPath { path: (string | number)[] // 選択された値の配列 nodes: LhCascaderNode[] // 対応するノードの配列}
export interface LhCascaderPanelItem { value: string | number label: string isDisabled: boolean isExpanded: boolean hasChildren: boolean}
export interface LhCascaderSearchResult { path: (string | number)[] nodes: LhCascaderNode[] pathLabels: string[]}
export interface LhCascaderItemConfig { value?: string // value フィールドのエイリアス label?: string children?: string disabled?: string leaf?: string}
export interface LhCascaderState { options: LhCascaderNode[] size: LhComponentSize disabled: boolean clearable: boolean placeholder: string visible: boolean checkStrictly: boolean // 親子を連動させない(任意の階層を選択可能) emitPath: boolean // true なら値はパス配列、false なら最下層の値のみ multiple: boolean cascading: boolean value: (string | number)[] selectedValue: (string | number)[] | null}アクセシビリティ
Section titled “アクセシビリティ”3 層の属性パック:
// トリガー:listbox セマンティクスでポップアップgetCascaderTriggerAria(true, 'my-panel')// { 'aria-expanded': true, 'aria-controls': 'my-panel', 'aria-haspopup': 'listbox' }
// 各列:role="listbox" + aria-level で深さを示す(1 始まり)getCascaderColumnAria('col-2', 2) // { role: 'listbox', id: 'col-2', 'aria-level': 2 }
// 項目:role="option" + 必要に応じて aria-expanded / selected / disabledgetCascaderItemAria({ selected: true, expanded: true, hasChildren: true })// { role: 'option', 'aria-expanded': true, 'aria-selected': true }getCascaderItemAria は子がないとき aria-expanded を出力しない点に注意してください——葉の選択肢に aria-expanded="false" を宣言するのは APG としては許容されるものの冗長で、スクリーンリーダーに「展開できるが今は展開されていない」と誤認させます。
import { findPathByValues, getSelectedLabelPath, getPanelItems } from '@lh-ui/share/core/cascader'
const tree = [ { value: 'zj', label: '浙江省', children: [ { value: 'hz', label: '杭州市', children: [{ value: 'xh', label: '西湖' }] }, ], },]
findPathByValues(tree, ['zj', 'hz', 'xh'])// { path: ['zj','hz','xh'], nodes: [浙江省, 杭州市, 西湖] }
getSelectedLabelPath(tree, ['zj', 'hz', 'xh']) // '浙江省 / 杭州市 / 西湖'getSelectedLabelPath(tree, ['nope']) // ''(見つからない場合は空文字列)
getPanelItems(tree, new Set(['zj']))// [{ value:'zj', label:'浙江省', isExpanded:true, hasChildren:true },// { value:'hz', label:'杭州市', isExpanded:false, hasChildren:true }]フィールドのエイリアス:
const raw = [{ id: 'zj', name: '浙江省', subList: [{ id: 'hz', name: '杭州市' }] }]const cfg = { value: 'id', label: 'name', children: 'subList' }const standard = normalizeCascaderNodes(raw, cfg)// [{ value:'zj', label:'浙江省', children:[{ value:'hz', label:'杭州市' }] }]findPathByValues(standard, ['zj', 'hz']) // 正常に動作<!-- Vue3:各列が 1 階層の panelItems をレンダリング --><div :class="getCascaderClassNames({ size, disabled, visible, readonly })"> <button class="lh-cascader__input" v-bind="getCascaderTriggerAria(visible, panelId)"> {{ getSelectedLabelPath(options, value) || placeholder }} </button> <div v-if="visible" class="lh-cascader-panel" :class="getPanelClassNames({ hoverMode })"> <div v-for="(col, ci) in columns" :key="ci" class="lh-cascader-panel__column" v-bind="getCascaderColumnAria(`col-${ci}`, ci + 1)" > <div v-for="item in col" class="lh-cascader-panel__item" v-bind="getCascaderItemAria({ selected: ..., expanded: item.isExpanded, disabled: item.isDisabled, hasChildren: item.hasChildren })" @click="select(item, ci)" >{{ item.label }}</div> </div> </div></div>スタイルとトークン
Section titled “スタイルとトークン”| クラス名 | 意味 |
|---|---|
.lh-cascader / --disabled / --visible |
コンテナ |
.lh-cascader__input / __input-inner |
トリガーの入力領域 |
.lh-cascader__tags / __tag / __tag-more |
複数選択タグ(はみ出しを折りたたむ) |
.lh-cascader__clear |
クリアボタン |
.lh-cascader-panel / --hover-mode |
パネル(hover モード:ホバーで次階層を展開) |
.lh-cascader-panel__column / __column-header |
列と列ヘッダー |
.lh-cascader-panel__item / --active / --disabled |
列項目 |
.lh-cascader-panel__search / __search-item |
検索結果領域 |
.lh-cascader-panel__empty |
結果なしのプレースホルダー |
.lh-cascader-panel-* トランジションクラス |
パネルの出入りアニメーション |
searchNodesのデフォルト上限は 50:巨大なツリーで高頻度の入力があると、1 キーストロークごとに O(全ノード)かかりカクつくため、上限はパフォーマンスの保険です。さらに結果が必要な場合は UI 層が「結果が多すぎます。キーワードを絞り込んでください」と案内します。- 循環参照保護のスコープ:祖先チェインによる重複排除は単一呼び出し内でのみ有効で、呼び出しを跨いではキャッシュされません。
findNodeの結果をキャッシュして再利用しようとしないでください——ツリーが変われば再計算です。 expandLastValueはemitPath: false専用:このモードではv-modelが最下層の値しか渡さないため、値反映時に単一値から完全な展開パスを復元する必要があります。getChildrenのダーティーデータのフォールバック:null ノード、非オブジェクトノード、children が配列でない場合はすべて[]を返し、TypeError を投げません。これによりカスケードはバックエンドのダーティーデータをのみ込めます(children の空を null で書くのはよくあることです)。checkStrictlyとemitPathの組み合わせ:checkStrictlyは任意の階層の選択を許可するもので、その際はよくemitPath: false(最下層の値のみ)と組み合わせます。両者は直交します。
@lh-ui/share/core/cascader → getSelectedLabelPath( )返回值
(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue