@qijenchen/design-system 0.1.0-beta.145 → 0.1.0-beta.147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Accordion/accordion.d.ts +1 -1
- package/dist/components/Accordion/accordion.d.ts.map +1 -1
- package/dist/components/Accordion/accordion.js +8 -3
- package/dist/components/Accordion/accordion.js.map +1 -1
- package/dist/components/AccountMenu/account-menu.d.ts.map +1 -1
- package/dist/components/AccountMenu/account-menu.js.map +1 -1
- package/dist/components/AgentPanel/agent-panel-fab.d.ts +7 -1
- package/dist/components/AgentPanel/agent-panel-fab.d.ts.map +1 -1
- package/dist/components/AgentPanel/agent-panel-fab.js +36 -14
- package/dist/components/AgentPanel/agent-panel-fab.js.map +1 -1
- package/dist/components/AgentPanel/agent-panel-logo.d.ts +3 -3
- package/dist/components/AgentPanel/agent-panel-logo.js.map +1 -1
- package/dist/components/AgentPanel/agent-panel.d.ts +1 -1
- package/dist/components/AgentPanel/agent-panel.d.ts.map +1 -1
- package/dist/components/AgentPanel/agent-panel.js +28 -22
- package/dist/components/AgentPanel/agent-panel.js.map +1 -1
- package/dist/components/AppShell/app-shell.d.ts +1 -1
- package/dist/components/AppShell/app-shell.js +1 -1
- package/dist/components/AppShell/app-shell.js.map +1 -1
- package/dist/components/Avatar/avatar.d.ts +95 -1
- package/dist/components/Avatar/avatar.d.ts.map +1 -1
- package/dist/components/Avatar/avatar.js +106 -15
- package/dist/components/Avatar/avatar.js.map +1 -1
- package/dist/components/Avatar/index.js +14 -2
- package/dist/components/Breadcrumb/breadcrumb.d.ts +4 -2
- package/dist/components/Breadcrumb/breadcrumb.d.ts.map +1 -1
- package/dist/components/Breadcrumb/breadcrumb.js +2 -2
- package/dist/components/Breadcrumb/breadcrumb.js.map +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.d.ts.map +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.js +27 -2
- package/dist/components/BulkActionBar/bulk-action-bar.js.map +1 -1
- package/dist/components/Button/button.d.ts +2 -2
- package/dist/components/Button/button.d.ts.map +1 -1
- package/dist/components/Button/button.js +118 -22
- package/dist/components/Button/button.js.map +1 -1
- package/dist/components/Calendar/calendar.d.ts +34 -10
- package/dist/components/Calendar/calendar.d.ts.map +1 -1
- package/dist/components/Calendar/calendar.js +210 -36
- package/dist/components/Calendar/calendar.js.map +1 -1
- package/dist/components/Carousel/carousel.d.ts +1 -1
- package/dist/components/Carousel/carousel.d.ts.map +1 -1
- package/dist/components/Carousel/carousel.js +3 -2
- package/dist/components/Carousel/carousel.js.map +1 -1
- package/dist/components/Chart/chart.d.ts +1 -0
- package/dist/components/Chart/chart.d.ts.map +1 -1
- package/dist/components/Chart/chart.js +8 -0
- package/dist/components/Chart/chart.js.map +1 -1
- package/dist/components/Checkbox/checkbox.d.ts +1 -1
- package/dist/components/Checkbox/checkbox.d.ts.map +1 -1
- package/dist/components/Checkbox/checkbox.js +10 -2
- package/dist/components/Checkbox/checkbox.js.map +1 -1
- package/dist/components/Chip/chip.d.ts +1 -1
- package/dist/components/Chip/chip.d.ts.map +1 -1
- package/dist/components/Chip/chip.js +4 -2
- package/dist/components/Chip/chip.js.map +1 -1
- package/dist/components/Combobox/combobox.d.ts +1 -1
- package/dist/components/Combobox/combobox.d.ts.map +1 -1
- package/dist/components/Combobox/combobox.js +37 -11
- package/dist/components/Combobox/combobox.js.map +1 -1
- package/dist/components/Command/command.d.ts +1 -1
- package/dist/components/Command/command.d.ts.map +1 -1
- package/dist/components/Command/command.js +106 -8
- package/dist/components/Command/command.js.map +1 -1
- package/dist/components/DataTable/cell-registry.d.ts.map +1 -1
- package/dist/components/DataTable/cell-registry.js +3 -2
- package/dist/components/DataTable/cell-registry.js.map +1 -1
- package/dist/components/DataTable/data-table-column-visibility-panel.d.ts.map +1 -1
- package/dist/components/DataTable/data-table-column-visibility-panel.js +24 -24
- package/dist/components/DataTable/data-table-column-visibility-panel.js.map +1 -1
- package/dist/components/DataTable/data-table-interaction-layer.d.ts +2 -1
- package/dist/components/DataTable/data-table-interaction-layer.d.ts.map +1 -1
- package/dist/components/DataTable/data-table-interaction-layer.js +3 -4
- package/dist/components/DataTable/data-table-interaction-layer.js.map +1 -1
- package/dist/components/DataTable/data-table-sort-manager.d.ts.map +1 -1
- package/dist/components/DataTable/data-table-sort-manager.js +20 -20
- package/dist/components/DataTable/data-table-sort-manager.js.map +1 -1
- package/dist/components/DataTable/data-table.d.ts +8 -5
- package/dist/components/DataTable/data-table.d.ts.map +1 -1
- package/dist/components/DataTable/data-table.js +197 -71
- package/dist/components/DataTable/data-table.js.map +1 -1
- package/dist/components/DateGrid/date-grid.d.ts +1 -1
- package/dist/components/DateGrid/date-grid.d.ts.map +1 -1
- package/dist/components/DateGrid/date-grid.js +358 -225
- package/dist/components/DateGrid/date-grid.js.map +1 -1
- package/dist/components/DatePicker/date-picker.d.ts.map +1 -1
- package/dist/components/DatePicker/date-picker.js +16 -1
- package/dist/components/DatePicker/date-picker.js.map +1 -1
- package/dist/components/Dialog/dialog.d.ts.map +1 -1
- package/dist/components/Dialog/dialog.js +55 -43
- package/dist/components/Dialog/dialog.js.map +1 -1
- package/dist/components/DropdownMenu/dropdown-menu-keyboard.d.ts +86 -0
- package/dist/components/DropdownMenu/dropdown-menu-keyboard.d.ts.map +1 -0
- package/dist/components/DropdownMenu/dropdown-menu-keyboard.js +172 -0
- package/dist/components/DropdownMenu/dropdown-menu-keyboard.js.map +1 -0
- package/dist/components/DropdownMenu/dropdown-menu.d.ts +6 -1
- package/dist/components/DropdownMenu/dropdown-menu.d.ts.map +1 -1
- package/dist/components/DropdownMenu/dropdown-menu.js +50 -39
- package/dist/components/DropdownMenu/dropdown-menu.js.map +1 -1
- package/dist/components/Field/field-context.d.ts +4 -0
- package/dist/components/Field/field-context.d.ts.map +1 -1
- package/dist/components/Field/field-context.js +1 -2
- package/dist/components/Field/field-context.js.map +1 -1
- package/dist/components/Field/field-edit-keys.d.ts +8 -0
- package/dist/components/Field/field-edit-keys.d.ts.map +1 -1
- package/dist/components/Field/field-edit-keys.js +3 -0
- package/dist/components/Field/field-edit-keys.js.map +1 -1
- package/dist/components/Field/field-wrapper.d.ts +18 -1
- package/dist/components/Field/field-wrapper.d.ts.map +1 -1
- package/dist/components/Field/field-wrapper.js +39 -10
- package/dist/components/Field/field-wrapper.js.map +1 -1
- package/dist/components/Field/field.d.ts +3 -2
- package/dist/components/Field/field.d.ts.map +1 -1
- package/dist/components/Field/field.js +2 -1
- package/dist/components/Field/field.js.map +1 -1
- package/dist/components/FileItem/file-item.d.ts +26 -2
- package/dist/components/FileItem/file-item.d.ts.map +1 -1
- package/dist/components/FileItem/file-item.js +82 -25
- package/dist/components/FileItem/file-item.js.map +1 -1
- package/dist/components/FileItem/index.js +4 -2
- package/dist/components/FileUpload/file-upload.d.ts +1 -1
- package/dist/components/FileUpload/file-upload.d.ts.map +1 -1
- package/dist/components/FileUpload/file-upload.js +132 -55
- package/dist/components/FileUpload/file-upload.js.map +1 -1
- package/dist/components/FileViewer/file-viewer.d.ts +1 -1
- package/dist/components/FileViewer/file-viewer.d.ts.map +1 -1
- package/dist/components/FileViewer/file-viewer.js +9 -4
- package/dist/components/FileViewer/file-viewer.js.map +1 -1
- package/dist/components/InlineEdit/inline-edit.d.ts.map +1 -1
- package/dist/components/InlineEdit/inline-edit.js +1 -1
- package/dist/components/InlineEdit/inline-edit.js.map +1 -1
- package/dist/components/Input/input.d.ts.map +1 -1
- package/dist/components/Input/input.js +6 -1
- package/dist/components/Input/input.js.map +1 -1
- package/dist/components/LinkInput/link-input.d.ts.map +1 -1
- package/dist/components/LinkInput/link-input.js +20 -4
- package/dist/components/LinkInput/link-input.js.map +1 -1
- package/dist/components/Notice/notice.js +1 -1
- package/dist/components/Notice/notice.js.map +1 -1
- package/dist/components/NumberInput/number-input.d.ts.map +1 -1
- package/dist/components/NumberInput/number-input.js +6 -1
- package/dist/components/NumberInput/number-input.js.map +1 -1
- package/dist/components/OverflowIndicator/index.js +2 -1
- package/dist/components/OverflowIndicator/overflow-indicator.d.ts +19 -1
- package/dist/components/OverflowIndicator/overflow-indicator.d.ts.map +1 -1
- package/dist/components/OverflowIndicator/overflow-indicator.js +47 -27
- package/dist/components/OverflowIndicator/overflow-indicator.js.map +1 -1
- package/dist/components/Pagination/pagination.d.ts +1 -1
- package/dist/components/Pagination/pagination.js +1 -1
- package/dist/components/Pagination/pagination.js.map +1 -1
- package/dist/components/PeoplePicker/avatar-stack-overflow.d.ts +7 -6
- package/dist/components/PeoplePicker/avatar-stack-overflow.d.ts.map +1 -1
- package/dist/components/PeoplePicker/avatar-stack-overflow.js +6 -7
- package/dist/components/PeoplePicker/avatar-stack-overflow.js.map +1 -1
- package/dist/components/PeoplePicker/people-picker-helpers.d.ts.map +1 -1
- package/dist/components/PeoplePicker/people-picker-helpers.js +1 -1
- package/dist/components/PeoplePicker/people-picker-helpers.js.map +1 -1
- package/dist/components/PeoplePicker/people-picker.d.ts.map +1 -1
- package/dist/components/PeoplePicker/people-picker.js +2 -2
- package/dist/components/PeoplePicker/people-picker.js.map +1 -1
- package/dist/components/PeoplePicker/person-display.d.ts.map +1 -1
- package/dist/components/PeoplePicker/person-display.js +26 -28
- package/dist/components/PeoplePicker/person-display.js.map +1 -1
- package/dist/components/ProfileCard/profile-card.d.ts.map +1 -1
- package/dist/components/ProfileCard/profile-card.js.map +1 -1
- package/dist/components/RadioGroup/radio-group.d.ts +1 -1
- package/dist/components/RadioGroup/radio-group.d.ts.map +1 -1
- package/dist/components/RadioGroup/radio-group.js +10 -4
- package/dist/components/RadioGroup/radio-group.js.map +1 -1
- package/dist/components/Rating/rating.d.ts +28 -6
- package/dist/components/Rating/rating.d.ts.map +1 -1
- package/dist/components/Rating/rating.js +97 -80
- package/dist/components/Rating/rating.js.map +1 -1
- package/dist/components/ScrollArea/scroll-area.d.ts +1 -1
- package/dist/components/ScrollArea/scroll-area.js +1 -1
- package/dist/components/ScrollArea/scroll-area.js.map +1 -1
- package/dist/components/SegmentedControl/segmented-control.d.ts +1 -1
- package/dist/components/SegmentedControl/segmented-control.d.ts.map +1 -1
- package/dist/components/SegmentedControl/segmented-control.js +3 -2
- package/dist/components/SegmentedControl/segmented-control.js.map +1 -1
- package/dist/components/Select/select.d.ts.map +1 -1
- package/dist/components/Select/select.js +15 -8
- package/dist/components/Select/select.js.map +1 -1
- package/dist/components/SelectMenu/index.js +2 -1
- package/dist/components/SelectMenu/index.js.map +1 -1
- package/dist/components/SelectMenu/select-menu-keyboard.d.ts +59 -0
- package/dist/components/SelectMenu/select-menu-keyboard.d.ts.map +1 -0
- package/dist/components/SelectMenu/select-menu-keyboard.js +107 -0
- package/dist/components/SelectMenu/select-menu-keyboard.js.map +1 -0
- package/dist/components/SelectMenu/select-menu.d.ts +1 -22
- package/dist/components/SelectMenu/select-menu.d.ts.map +1 -1
- package/dist/components/SelectMenu/select-menu.js +18 -34
- package/dist/components/SelectMenu/select-menu.js.map +1 -1
- package/dist/components/SelectionControl/selection-item.d.ts.map +1 -1
- package/dist/components/SelectionControl/selection-item.js +1 -0
- package/dist/components/SelectionControl/selection-item.js.map +1 -1
- package/dist/components/Sidebar/sidebar.d.ts +7 -9
- package/dist/components/Sidebar/sidebar.d.ts.map +1 -1
- package/dist/components/Sidebar/sidebar.js +171 -50
- package/dist/components/Sidebar/sidebar.js.map +1 -1
- package/dist/components/Slider/slider.d.ts +1 -1
- package/dist/components/Slider/slider.d.ts.map +1 -1
- package/dist/components/Slider/slider.js +9 -4
- package/dist/components/Slider/slider.js.map +1 -1
- package/dist/components/Steps/steps.d.ts.map +1 -1
- package/dist/components/Steps/steps.js +54 -26
- package/dist/components/Steps/steps.js.map +1 -1
- package/dist/components/Switch/switch.d.ts +1 -1
- package/dist/components/Switch/switch.d.ts.map +1 -1
- package/dist/components/Switch/switch.js +7 -2
- package/dist/components/Switch/switch.js.map +1 -1
- package/dist/components/Tabs/tabs.d.ts +1 -1
- package/dist/components/Tabs/tabs.d.ts.map +1 -1
- package/dist/components/Tabs/tabs.js +39 -4
- package/dist/components/Tabs/tabs.js.map +1 -1
- package/dist/components/Tag/tag.d.ts +1 -1
- package/dist/components/Tag/tag.d.ts.map +1 -1
- package/dist/components/Tag/tag.js +3 -2
- package/dist/components/Tag/tag.js.map +1 -1
- package/dist/components/Textarea/textarea.d.ts.map +1 -1
- package/dist/components/Textarea/textarea.js +8 -2
- package/dist/components/Textarea/textarea.js.map +1 -1
- package/dist/components/TimePicker/time-columns.d.ts.map +1 -1
- package/dist/components/TimePicker/time-columns.js +8 -4
- package/dist/components/TimePicker/time-columns.js.map +1 -1
- package/dist/components/TimePicker/time-picker.d.ts +1 -1
- package/dist/components/TimePicker/time-picker.js +1 -1
- package/dist/components/TimePicker/time-picker.js.map +1 -1
- package/dist/components/TreeView/tree-view.d.ts +9 -5
- package/dist/components/TreeView/tree-view.d.ts.map +1 -1
- package/dist/components/TreeView/tree-view.js +266 -214
- package/dist/components/TreeView/tree-view.js.map +1 -1
- package/dist/hooks/use-input-modality.d.ts +12 -17
- package/dist/hooks/use-input-modality.d.ts.map +1 -1
- package/dist/hooks/use-input-modality.js +4 -34
- package/dist/hooks/use-input-modality.js.map +1 -1
- package/dist/hooks/use-overflow-items.d.ts.map +1 -1
- package/dist/hooks/use-overflow-items.js +2 -0
- package/dist/hooks/use-overflow-items.js.map +1 -1
- package/dist/hooks/use-truncated.d.ts.map +1 -1
- package/dist/hooks/use-truncated.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/drag-announcements.d.ts +32 -0
- package/dist/lib/drag-announcements.d.ts.map +1 -1
- package/dist/lib/drag-announcements.js +39 -1
- package/dist/lib/drag-announcements.js.map +1 -1
- package/dist/lib/focus-after-trigger.d.ts +45 -0
- package/dist/lib/focus-after-trigger.d.ts.map +1 -0
- package/dist/lib/focus-after-trigger.js +66 -0
- package/dist/lib/focus-after-trigger.js.map +1 -0
- package/dist/lib/overlay-coexistence.js +1 -1
- package/dist/lib/roving-list-keyboard.d.ts +208 -0
- package/dist/lib/roving-list-keyboard.d.ts.map +1 -0
- package/dist/lib/roving-list-keyboard.js +174 -0
- package/dist/lib/roving-list-keyboard.js.map +1 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +6 -1
- package/dist/lib/utils.js.map +1 -1
- package/dist/patterns/element-anatomy/item-anatomy.d.ts +4 -4
- package/dist/patterns/element-anatomy/item-anatomy.d.ts.map +1 -1
- package/dist/patterns/element-anatomy/item-anatomy.js +14 -4
- package/dist/patterns/element-anatomy/item-anatomy.js.map +1 -1
- package/dist/patterns/horizontal-overflow/horizontal-overflow.d.ts.map +1 -1
- package/dist/patterns/horizontal-overflow/horizontal-overflow.js +25 -0
- package/dist/patterns/horizontal-overflow/horizontal-overflow.js.map +1 -1
- package/dist/patterns/overlay-surface/overlay-surface.d.ts.map +1 -1
- package/dist/patterns/overlay-surface/overlay-surface.js.map +1 -1
- package/dist/tokens/categorical-color.d.ts +6 -1
- package/dist/tokens/categorical-color.d.ts.map +1 -1
- package/dist/tokens/categorical-color.js.map +1 -1
- package/dist/tokens/uiSize/icon-size.d.ts +1 -1
- package/dist/tokens/uiSize/stack-gap.d.ts +18 -0
- package/dist/tokens/uiSize/stack-gap.d.ts.map +1 -0
- package/dist/tokens/uiSize/stack-gap.js +5 -0
- package/dist/tokens/uiSize/stack-gap.js.map +1 -0
- package/ds-canonical/fork/codex/agents/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/fork/codex/hooks.json +1 -1
- package/ds-canonical/fork/consumer/lock.json +17 -14
- package/ds-canonical/fork/consumer/managed-files/423729840acdd43c812f17c245b871716c6b60f76e4d3e94864d0e34a815bf8d.blob +9 -0
- package/ds-canonical/fork/consumer/managed-files/46f10bc55cdfdf834d0749eebff42ea0bb8576f361b36d2b6ba1851354c50150.blob +47 -9
- package/ds-canonical/fork/consumer/managed-files/5a0d134a2645114c8caac20458070b00a7f0fd55b3e8805b7325cb4cf52fa15e.blob +110 -0
- package/ds-canonical/fork/consumer/managed-files/5af0a46760af338d51fd0f7950675f8e97f7d2f09386b8818fcd7a58e6654e75.blob +790 -0
- package/ds-canonical/fork/consumer/managed-files/68bfcddff9c4a16388c248cd34fb66163e9e64b3d809a48ef3984f6ae4ff6d12.blob +211 -0
- package/ds-canonical/fork/consumer/managed-files/7eb6cc60abfdabaf6617c336058fad1454ccde08889295767bfd9b7fd153cc6e.blob +45 -6
- package/ds-canonical/fork/consumer/managed-files/92e4828cdcb96ff0d01daa16f6885f24045a8e72820665650f2a1890dc3a446c.blob +42 -2
- package/ds-canonical/fork/consumer/managed-files/ab072e7f2eca51f0e18a5e18c4b333260ee5d219a12cc6df5bb2d683f0b2734d.blob +27 -1
- package/ds-canonical/fork/consumer/managed-files/e71c65f45c6af7ce12b6b3c0d8028f185da5b719e2050c9b2a6b243d41ae4878.blob +62 -14
- package/ds-canonical/fork/governance.lock +41 -17
- package/ds-canonical/fork/hooks/check_consumer_app_invariants.sh +16 -4
- package/ds-canonical/fork/hooks/check_consumer_code_quality.sh +3 -1
- package/ds-canonical/fork/hooks/check_pattern_invariants.sh +13 -9
- package/ds-canonical/fork/launchers/settings-hooks.json +1 -1
- package/ds-canonical/fork/manifest.json +17 -11
- package/ds-canonical/fork/providers/claude/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/fork/providers/codex/hook-config.json +1 -1
- package/ds-canonical/fork/providers/codex/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/fork/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/hooks/check_consumer_app_invariants.sh +16 -4
- package/ds-canonical/hooks/check_consumer_code_quality.sh +3 -1
- package/ds-canonical/hooks/check_datatable_invariants.sh +4 -1
- package/ds-canonical/hooks/check_pattern_invariants.sh +13 -9
- package/ds-canonical/hooks/check_plugin_fork_health.sh +8 -1
- package/ds-canonical/hooks/check_propose_discipline.sh +8 -1
- package/ds-canonical/hooks/check_storybook_addon_packaging.sh +8 -1
- package/ds-canonical/hooks/check_substantive_edit_approval_preflight.sh +64 -2
- package/ds-canonical/hooks/lib/_overlay_handcraft.sh +5 -2
- package/ds-canonical/hooks/registrations.json +12 -7
- package/ds-canonical/hooks/session_start_governance_check.sh +16 -0
- package/ds-canonical/hooks/tests/test_check_consumer_app_invariants.sh +25 -0
- package/ds-canonical/hooks/tests/test_check_consumer_code_quality.sh +4 -0
- package/ds-canonical/hooks/tests/test_check_consumer_ds_primitive_misuse.sh +14 -0
- package/ds-canonical/hooks/tests/test_check_datatable_invariants.sh +24 -0
- package/ds-canonical/hooks/tests/test_check_overlay_handcraft.sh +32 -0
- package/ds-canonical/hooks/tests/test_check_pattern_invariants.sh +59 -0
- package/ds-canonical/hooks/tests/test_check_plugin_fork_health.sh +32 -1
- package/ds-canonical/hooks/tests/test_check_propose_discipline.sh +21 -0
- package/ds-canonical/hooks/tests/test_check_storybook_addon_packaging.sh +21 -0
- package/ds-canonical/hooks/tests/test_check_substantive_edit_approval_preflight.sh +97 -0
- package/ds-canonical/hooks/tests/test_session_start_governance_check.sh +49 -0
- package/ds-canonical/references/README.md +12 -6
- package/ds-canonical/references/drag-canonical.md +3 -1
- package/ds-canonical/references/focus-canonical.md +26 -13
- package/ds-canonical/references/governance-audit-coverage.md +1 -1
- package/ds-canonical/references/hit-area-canonical.md +361 -0
- package/ds-canonical/references/item-anatomy-recipe.md +4 -2
- package/ds-canonical/references/keyboard-model-canonical.md +559 -0
- package/ds-canonical/rules/meta-patterns.md +4 -4
- package/ds-canonical/rules/self-verify.md +4 -4
- package/ds-canonical/skills/design-system-audit/references/audit-prompts.md +1 -1
- package/ds-canonical/skills/design-system-audit/references/historical-bugs.md +96 -0
- package/ds-canonical/skills/knowledge-prune/SKILL.md +4 -1
- package/ds-canonical/skills/performance-audit/SKILL.md +1 -1
- package/ds-story-manifest.json +19 -7
- package/llms-full.txt +2 -2
- package/llms.txt +2 -2
- package/package.json +3 -1
- package/src/components/Accordion/accordion.anatomy.stories.tsx +3 -3
- package/src/components/Accordion/accordion.spec.md +1 -1
- package/src/components/Accordion/accordion.tsx +8 -3
- package/src/components/AccountMenu/account-menu.spec.md +2 -3
- package/src/components/AccountMenu/account-menu.tsx +0 -1
- package/src/components/AgentPanel/agent-decision-card.spec.md +88 -0
- package/src/components/AgentPanel/agent-panel-fab.spec.md +151 -0
- package/src/components/AgentPanel/agent-panel-fab.tsx +67 -24
- package/src/components/AgentPanel/agent-panel-logo.spec.md +87 -0
- package/src/components/AgentPanel/agent-panel-logo.tsx +3 -3
- package/src/components/AgentPanel/agent-panel.anatomy.stories.tsx +1 -1
- package/src/components/AgentPanel/agent-panel.principles.stories.tsx +1 -1
- package/src/components/AgentPanel/agent-panel.spec.md +30 -275
- package/src/components/AgentPanel/agent-panel.stories.tsx +113 -8
- package/src/components/AgentPanel/agent-panel.tsx +59 -22
- package/src/components/Alert/alert.anatomy.stories.tsx +1 -1
- package/src/components/AppShell/app-shell.tsx +1 -1
- package/src/components/AspectRatio/aspect-ratio.anatomy.stories.tsx +10 -15
- package/src/components/Avatar/avatar.anatomy.stories.tsx +23 -26
- package/src/components/Avatar/avatar.principles.stories.tsx +10 -10
- package/src/components/Avatar/avatar.spec.md +45 -6
- package/src/components/Avatar/avatar.stories.tsx +8 -8
- package/src/components/Avatar/avatar.tsx +227 -21
- package/src/components/Badge/badge.anatomy.stories.tsx +16 -21
- package/src/components/Breadcrumb/breadcrumb.anatomy.stories.tsx +3 -3
- package/src/components/Breadcrumb/breadcrumb.spec.md +3 -3
- package/src/components/Breadcrumb/breadcrumb.tsx +5 -3
- package/src/components/BulkActionBar/bulk-action-bar.anatomy.stories.tsx +1 -1
- package/src/components/BulkActionBar/bulk-action-bar.principles.stories.tsx +1 -1
- package/src/components/BulkActionBar/bulk-action-bar.spec.md +1 -0
- package/src/components/BulkActionBar/bulk-action-bar.stories.tsx +57 -2
- package/src/components/BulkActionBar/bulk-action-bar.tsx +32 -2
- package/src/components/Button/button.anatomy.stories.tsx +174 -33
- package/src/components/Button/button.spec.md +23 -8
- package/src/components/Button/button.tsx +116 -21
- package/src/components/Calendar/calendar.anatomy.stories.tsx +34 -11
- package/src/components/Calendar/calendar.principles.stories.tsx +44 -1
- package/src/components/Calendar/calendar.spec.md +97 -20
- package/src/components/Calendar/calendar.stories.tsx +101 -4
- package/src/components/Calendar/calendar.tsx +379 -74
- package/src/components/Carousel/carousel.anatomy.stories.tsx +16 -17
- package/src/components/Carousel/carousel.spec.md +48 -2
- package/src/components/Carousel/carousel.tsx +9 -3
- package/src/components/Chart/chart.anatomy.stories.tsx +7 -5
- package/src/components/Chart/chart.spec.md +32 -7
- package/src/components/Chart/chart.tsx +9 -0
- package/src/components/Checkbox/checkbox.anatomy.stories.tsx +21 -28
- package/src/components/Checkbox/checkbox.spec.md +4 -2
- package/src/components/Checkbox/checkbox.tsx +10 -2
- package/src/components/Chip/chip.anatomy.stories.tsx +1 -1
- package/src/components/Chip/chip.spec.md +2 -1
- package/src/components/Chip/chip.tsx +4 -2
- package/src/components/Combobox/combobox.anatomy.stories.tsx +30 -37
- package/src/components/Combobox/combobox.spec.md +11 -3
- package/src/components/Combobox/combobox.tsx +43 -12
- package/src/components/Command/command.spec.md +11 -0
- package/src/components/Command/command.tsx +143 -10
- package/src/components/DataTable/cell-registry.tsx +5 -1
- package/src/components/DataTable/data-table-column-visibility-panel.tsx +5 -6
- package/src/components/DataTable/data-table-interaction-layer.tsx +27 -14
- package/src/components/DataTable/data-table-known-defects.spec.md +72 -0
- package/src/components/DataTable/data-table-scroll-performance.spec.md +295 -0
- package/src/components/DataTable/data-table-sort-manager.tsx +5 -6
- package/src/components/DataTable/data-table.anatomy.stories.tsx +1 -1
- package/src/components/DataTable/data-table.css +64 -0
- package/src/components/DataTable/data-table.spec.md +70 -346
- package/src/components/DataTable/data-table.stories.tsx +56 -9
- package/src/components/DataTable/data-table.tsx +392 -103
- package/src/components/DateGrid/date-grid.anatomy.stories.tsx +2 -2
- package/src/components/DateGrid/date-grid.spec.md +163 -1
- package/src/components/DateGrid/date-grid.tsx +270 -7
- package/src/components/DatePicker/date-picker.anatomy.stories.tsx +18 -27
- package/src/components/DatePicker/date-picker.spec.md +6 -2
- package/src/components/DatePicker/date-picker.tsx +20 -1
- package/src/components/Dialog/dialog.spec.md +6 -2
- package/src/components/Dialog/dialog.stories.tsx +18 -9
- package/src/components/Dialog/dialog.tsx +83 -48
- package/src/components/DropdownMenu/dropdown-menu-keyboard.ts +255 -0
- package/src/components/DropdownMenu/dropdown-menu.anatomy.stories.tsx +32 -33
- package/src/components/DropdownMenu/dropdown-menu.spec.md +18 -5
- package/src/components/DropdownMenu/dropdown-menu.stories.tsx +58 -1
- package/src/components/DropdownMenu/dropdown-menu.tsx +70 -33
- package/src/components/Empty/empty.spec.md +1 -2
- package/src/components/Field/field-context.ts +9 -4
- package/src/components/Field/field-controls.spec.md +5 -2
- package/src/components/Field/field-edit-keys.ts +12 -0
- package/src/components/Field/field-wrapper.tsx +76 -10
- package/src/components/Field/field.anatomy.stories.tsx +1 -1
- package/src/components/Field/field.spec.md +11 -6
- package/src/components/Field/field.stories.tsx +24 -1
- package/src/components/Field/field.tsx +10 -4
- package/src/components/FileItem/file-item.anatomy.stories.tsx +20 -10
- package/src/components/FileItem/file-item.principles.stories.tsx +14 -7
- package/src/components/FileItem/file-item.spec.md +50 -48
- package/src/components/FileItem/file-item.stories.tsx +41 -28
- package/src/components/FileItem/file-item.tsx +184 -50
- package/src/components/FileUpload/file-upload.anatomy.stories.tsx +11 -15
- package/src/components/FileUpload/file-upload.spec.md +27 -1
- package/src/components/FileUpload/file-upload.stories.tsx +33 -0
- package/src/components/FileUpload/file-upload.tsx +173 -43
- package/src/components/FileViewer/file-viewer.tsx +9 -4
- package/src/components/HoverCard/hover-card.stories.tsx +2 -2
- package/src/components/InlineEdit/inline-edit.spec.md +2 -0
- package/src/components/InlineEdit/inline-edit.tsx +8 -3
- package/src/components/Input/input.anatomy.stories.tsx +23 -31
- package/src/components/Input/input.spec.md +11 -0
- package/src/components/Input/input.tsx +6 -1
- package/src/components/LinkInput/link-input.anatomy.stories.tsx +18 -21
- package/src/components/LinkInput/link-input.principles.stories.tsx +4 -4
- package/src/components/LinkInput/link-input.spec.md +12 -1
- package/src/components/LinkInput/link-input.tsx +37 -4
- package/src/components/Menu/menu-item.anatomy.stories.tsx +50 -56
- package/src/components/Menu/menu-item.spec.md +1 -1
- package/src/components/Menu/menu-item.stories.tsx +48 -42
- package/src/components/Notice/notice.anatomy.stories.tsx +8 -16
- package/src/components/Notice/notice.spec.md +1 -1
- package/src/components/Notice/notice.tsx +1 -1
- package/src/components/NumberInput/number-input.anatomy.stories.tsx +21 -28
- package/src/components/NumberInput/number-input.spec.md +2 -0
- package/src/components/NumberInput/number-input.tsx +8 -1
- package/src/components/OverflowIndicator/overflow-indicator.anatomy.stories.tsx +28 -48
- package/src/components/OverflowIndicator/overflow-indicator.spec.md +13 -1
- package/src/components/OverflowIndicator/overflow-indicator.stories.tsx +12 -9
- package/src/components/OverflowIndicator/overflow-indicator.tsx +32 -12
- package/src/components/Pagination/pagination.spec.md +1 -0
- package/src/components/Pagination/pagination.tsx +1 -1
- package/src/components/PeoplePicker/avatar-stack-overflow.ts +14 -13
- package/src/components/PeoplePicker/people-picker-helpers.ts +5 -2
- package/src/components/PeoplePicker/people-picker.anatomy.stories.tsx +1 -1
- package/src/components/PeoplePicker/people-picker.spec.md +4 -2
- package/src/components/PeoplePicker/people-picker.tsx +1 -4
- package/src/components/PeoplePicker/person-display.tsx +45 -28
- package/src/components/Popover/popover.anatomy.stories.tsx +1 -1
- package/src/components/Popover/popover.principles.stories.tsx +3 -12
- package/src/components/Popover/popover.spec.md +7 -3
- package/src/components/ProfileCard/profile-card.anatomy.stories.tsx +1 -1
- package/src/components/ProfileCard/profile-card.spec.md +1 -1
- package/src/components/ProfileCard/profile-card.tsx +0 -3
- package/src/components/ProgressBar/progress-bar.anatomy.stories.tsx +11 -21
- package/src/components/ProgressBar/progress-bar.spec.md +1 -1
- package/src/components/RadioGroup/radio-group.anatomy.stories.tsx +17 -24
- package/src/components/RadioGroup/radio-group.spec.md +1 -1
- package/src/components/RadioGroup/radio-group.tsx +22 -4
- package/src/components/Rating/rating.anatomy.stories.tsx +131 -124
- package/src/components/Rating/rating.principles.stories.tsx +69 -43
- package/src/components/Rating/rating.spec.md +142 -63
- package/src/components/Rating/rating.stories.tsx +11 -15
- package/src/components/Rating/rating.tsx +131 -117
- package/src/components/ScrollArea/scroll-area.anatomy.stories.tsx +1 -1
- package/src/components/ScrollArea/scroll-area.principles.stories.tsx +6 -3
- package/src/components/ScrollArea/scroll-area.spec.md +3 -1
- package/src/components/ScrollArea/scroll-area.stories.tsx +5 -2
- package/src/components/ScrollArea/scroll-area.tsx +1 -1
- package/src/components/SegmentedControl/segmented-control.anatomy.stories.tsx +1 -1
- package/src/components/SegmentedControl/segmented-control.spec.md +2 -2
- package/src/components/SegmentedControl/segmented-control.tsx +3 -2
- package/src/components/Select/select.anatomy.stories.tsx +26 -35
- package/src/components/Select/select.spec.md +4 -3
- package/src/components/Select/select.tsx +11 -4
- package/src/components/SelectMenu/select-menu-keyboard.ts +218 -0
- package/src/components/SelectMenu/select-menu.anatomy.stories.tsx +6 -17
- package/src/components/SelectMenu/select-menu.spec.md +12 -4
- package/src/components/SelectMenu/select-menu.tsx +21 -56
- package/src/components/SelectionControl/selection-item.anatomy.stories.tsx +9 -33
- package/src/components/SelectionControl/selection-item.spec.md +2 -2
- package/src/components/SelectionControl/selection-item.tsx +3 -1
- package/src/components/Separator/separator.stories.tsx +4 -4
- package/src/components/Sheet/sheet.principles.stories.tsx +31 -13
- package/src/components/Sidebar/sidebar.anatomy.stories.tsx +1 -1
- package/src/components/Sidebar/sidebar.spec.md +73 -6
- package/src/components/Sidebar/sidebar.stories.tsx +82 -4
- package/src/components/Sidebar/sidebar.tsx +281 -64
- package/src/components/Skeleton/skeleton.spec.md +1 -1
- package/src/components/Slider/slider.anatomy.stories.tsx +1 -1
- package/src/components/Slider/slider.spec.md +6 -5
- package/src/components/Slider/slider.tsx +13 -8
- package/src/components/Steps/steps-state-visuals.spec.md +157 -0
- package/src/components/Steps/steps.anatomy.stories.tsx +1 -0
- package/src/components/Steps/steps.spec.md +59 -142
- package/src/components/Steps/steps.stories.tsx +1 -1
- package/src/components/Steps/steps.tsx +125 -33
- package/src/components/Switch/switch.spec.md +3 -3
- package/src/components/Switch/switch.tsx +6 -1
- package/src/components/Tabs/tabs.spec.md +5 -1
- package/src/components/Tabs/tabs.tsx +42 -3
- package/src/components/Tag/tag.anatomy.stories.tsx +24 -28
- package/src/components/Tag/tag.spec.md +10 -11
- package/src/components/Tag/tag.tsx +3 -3
- package/src/components/Textarea/textarea.tsx +7 -1
- package/src/components/TimePicker/time-columns.tsx +9 -4
- package/src/components/TimePicker/time-picker.spec.md +4 -1
- package/src/components/TimePicker/time-picker.tsx +1 -1
- package/src/components/Tooltip/tooltip.anatomy.stories.tsx +6 -15
- package/src/components/Tooltip/tooltip.principles.stories.tsx +4 -16
- package/src/components/Tooltip/tooltip.spec.md +2 -1
- package/src/components/TreeView/tree-view.anatomy.stories.tsx +35 -13
- package/src/components/TreeView/tree-view.spec.md +67 -38
- package/src/components/TreeView/tree-view.stories.tsx +221 -12
- package/src/components/TreeView/tree-view.tsx +463 -356
- package/src/hooks/use-input-modality.ts +17 -46
- package/src/hooks/use-overflow-items.ts +4 -0
- package/src/hooks/use-truncated.ts +3 -2
- package/src/index.ts +17 -0
- package/src/lib/README.md +2 -0
- package/src/lib/drag-announcements.ts +78 -0
- package/src/lib/focus-after-trigger.ts +102 -0
- package/src/lib/roving-list-keyboard.ts +400 -0
- package/src/lib/utils.ts +5 -0
- package/src/patterns/action-bar/action-bar.spec.md +2 -0
- package/src/patterns/element-anatomy/inline-action.spec.md +16 -12
- package/src/patterns/element-anatomy/item-anatomy.spec.md +98 -6
- package/src/patterns/element-anatomy/item-anatomy.stories.tsx +75 -70
- package/src/patterns/element-anatomy/item-anatomy.tsx +17 -6
- package/src/patterns/element-anatomy/truncated-text.spec.md +1 -0
- package/src/patterns/header-canonical/header-canonical.spec.md +3 -2
- package/src/patterns/horizontal-overflow/horizontal-overflow.spec.md +2 -0
- package/src/patterns/horizontal-overflow/horizontal-overflow.stories.tsx +36 -0
- package/src/patterns/horizontal-overflow/horizontal-overflow.tsx +28 -0
- package/src/patterns/overlay-surface/overlay-chrome-sizing.spec.md +180 -0
- package/src/patterns/overlay-surface/overlay-surface.spec.md +8 -162
- package/src/patterns/overlay-surface/overlay-surface.tsx +5 -3
- package/src/patterns/resize-handle/resize-handle.spec.md +3 -3
- package/src/styles/base.css +1 -1
- package/src/tokens/categorical-color.ts +6 -1
- package/src/tokens/color/color.spec.md +91 -24
- package/src/tokens/color/color.stories.tsx +50 -2
- package/src/tokens/color/primitives.css +41 -38
- package/src/tokens/color/semantic.css +105 -36
- package/src/tokens/layoutSpace/layoutSpace.spec.md +2 -0
- package/src/tokens/motion/motion.spec.md +31 -8
- package/src/tokens/orphan-tokens.spec.md +1 -1
- package/src/tokens/uiSize/icon-size.ts +1 -1
- package/src/tokens/uiSize/stack-gap.ts +17 -0
- package/src/tokens/uiSize/uiSize.css +12 -0
- package/src/tokens/uiSize/uiSize.spec.md +17 -16
- package/src/tokens/uiSize/uiSize.stories.tsx +1 -1
- package/dist/node_modules/aria-hidden/dist/es2015/index.js +0 -147
- package/dist/node_modules/aria-hidden/dist/es2015/index.js.map +0 -1
|
@@ -7,6 +7,7 @@ import type { Meta } from '@storybook/react'
|
|
|
7
7
|
import { useState, useEffect } from 'react'
|
|
8
8
|
import { Flag } from 'lucide-react'
|
|
9
9
|
import { Select } from './select'
|
|
10
|
+
import { SegmentedControl, SegmentedControlItem } from '@/design-system/components/SegmentedControl/segmented-control'
|
|
10
11
|
|
|
11
12
|
const meta: Meta = {
|
|
12
13
|
title: 'Design System/Components/Select/設計規格',
|
|
@@ -165,18 +166,6 @@ const TokenAnnotation = ({ colors }: { colors: ColorSpec }) => (
|
|
|
165
166
|
</div>
|
|
166
167
|
)
|
|
167
168
|
|
|
168
|
-
const Tab = ({ active, onClick, disabled, children }: { active: boolean; onClick: () => void; disabled?: boolean; children: React.ReactNode }) => {
|
|
169
|
-
if (disabled) return <span className="px-2.5 py-1 text-[12px] font-mono rounded-md text-fg-disabled bg-neutral-hover cursor-not-allowed">{children}</span>
|
|
170
|
-
return (
|
|
171
|
-
<button type="button" onClick={onClick}
|
|
172
|
-
className={`px-2.5 py-1 text-[12px] font-mono rounded-md cursor-pointer transition-colors ${
|
|
173
|
-
active ? 'bg-primary text-white font-semibold' : 'bg-neutral-hover text-fg-secondary hover:bg-neutral-active'
|
|
174
|
-
}`}>
|
|
175
|
-
{children}
|
|
176
|
-
</button>
|
|
177
|
-
)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
169
|
const PropRow = ({ label, dot, children }: { label: string; dot?: string; children: React.ReactNode }) => (
|
|
181
170
|
<div className="flex items-start gap-3 py-2 border-b border-divider last:border-b-0">
|
|
182
171
|
<span className="text-[11px] text-fg-muted font-medium w-[72px] shrink-0 pt-0.5 flex items-center gap-1.5">
|
|
@@ -373,47 +362,49 @@ const InspectorInner = () => {
|
|
|
373
362
|
|
|
374
363
|
return (
|
|
375
364
|
<div className="flex flex-col gap-6">
|
|
376
|
-
{/* Controls
|
|
365
|
+
{/* Controls — 互斥切換消費 SegmentedControl(segmented-control.spec.md「何時用」2–5 個互斥選項;
|
|
366
|
+
取代手刻 Tab:它靜止借 neutral-hover、hover 借 neutral-active,是 color.spec.md 成對 token 的錯配) */}
|
|
377
367
|
<div className="flex flex-col gap-2.5">
|
|
378
368
|
<div className="flex items-center gap-2">
|
|
379
369
|
<span className="text-[11px] text-fg-muted w-16 shrink-0">Mode</span>
|
|
380
|
-
<
|
|
381
|
-
{MODES.map((m) => <
|
|
382
|
-
</
|
|
370
|
+
<SegmentedControl size="sm" aria-label="Mode" value={mode} onValueChange={(v) => setMode(v as ModeKey)}>
|
|
371
|
+
{MODES.map((m) => <SegmentedControlItem key={m} value={m}>{m}</SegmentedControlItem>)}
|
|
372
|
+
</SegmentedControl>
|
|
383
373
|
</div>
|
|
384
374
|
<div className="flex items-center gap-2">
|
|
385
375
|
<span className="text-[11px] text-fg-muted w-16 shrink-0">Display</span>
|
|
386
|
-
<
|
|
387
|
-
{DISPLAYS.map((d) => <
|
|
388
|
-
</
|
|
376
|
+
<SegmentedControl size="sm" aria-label="Display" value={display} onValueChange={(v) => setDisplay(v as DisplayKey)}>
|
|
377
|
+
{DISPLAYS.map((d) => <SegmentedControlItem key={d} value={d}>{d}</SegmentedControlItem>)}
|
|
378
|
+
</SegmentedControl>
|
|
389
379
|
</div>
|
|
390
380
|
<div className="flex items-center gap-2">
|
|
391
381
|
<span className="text-[11px] text-fg-muted w-16 shrink-0">Size</span>
|
|
392
|
-
<
|
|
393
|
-
{SIZES.map((sz) => <
|
|
394
|
-
</
|
|
382
|
+
<SegmentedControl size="sm" aria-label="Size" value={size} onValueChange={(v) => setSize(v as SizeKey)}>
|
|
383
|
+
{SIZES.map((sz) => <SegmentedControlItem key={sz} value={sz}>{sz}</SegmentedControlItem>)}
|
|
384
|
+
</SegmentedControl>
|
|
395
385
|
</div>
|
|
396
386
|
<div className="flex items-center gap-2">
|
|
397
387
|
<span className="text-[11px] text-fg-muted w-16 shrink-0">Error</span>
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
<
|
|
401
|
-
|
|
388
|
+
{/* 停用項用元件自身 disabled(segmented-control.spec.md「disabled」);停用項不得是當前值 → value 取實際生效值 */}
|
|
389
|
+
<SegmentedControl size="sm" aria-label="Error" value={error && mode === 'edit' ? 'on' : 'off'} onValueChange={(v) => setError(v === 'on')}>
|
|
390
|
+
<SegmentedControlItem value="off">off</SegmentedControlItem>
|
|
391
|
+
<SegmentedControlItem value="on" disabled={mode !== 'edit'}>on</SegmentedControlItem>
|
|
392
|
+
</SegmentedControl>
|
|
402
393
|
{mode !== 'edit' && <span className="text-[11px] text-fg-muted">僅 edit 模式有效</span>}
|
|
403
394
|
</div>
|
|
404
395
|
<div className="flex items-center gap-2">
|
|
405
396
|
<span className="text-[11px] text-fg-muted w-16 shrink-0">Clearable</span>
|
|
406
|
-
<
|
|
407
|
-
<
|
|
408
|
-
<
|
|
409
|
-
</
|
|
397
|
+
<SegmentedControl size="sm" aria-label="Clearable" value={clearable ? 'on' : 'off'} onValueChange={(v) => setClearable(v === 'on')}>
|
|
398
|
+
<SegmentedControlItem value="off">off</SegmentedControlItem>
|
|
399
|
+
<SegmentedControlItem value="on">on</SegmentedControlItem>
|
|
400
|
+
</SegmentedControl>
|
|
410
401
|
</div>
|
|
411
402
|
<div className="flex items-center gap-2">
|
|
412
403
|
<span className="text-[11px] text-fg-muted w-16 shrink-0">startIcon</span>
|
|
413
|
-
<
|
|
414
|
-
<
|
|
415
|
-
<
|
|
416
|
-
</
|
|
404
|
+
<SegmentedControl size="sm" aria-label="startIcon" value={hasStartIcon && !isTag ? 'on' : 'off'} onValueChange={(v) => setHasStartIcon(v === 'on')}>
|
|
405
|
+
<SegmentedControlItem value="off">off</SegmentedControlItem>
|
|
406
|
+
<SegmentedControlItem value="on" disabled={isTag}>on</SegmentedControlItem>
|
|
407
|
+
</SegmentedControl>
|
|
417
408
|
{display === 'tag' && <span className="text-[11px] text-fg-muted">tag 模式不支援 startIcon</span>}
|
|
418
409
|
</div>
|
|
419
410
|
</div>
|
|
@@ -835,7 +826,7 @@ export const Accessibility = {
|
|
|
835
826
|
render: () => (
|
|
836
827
|
<div className="max-w-3xl text-body text-fg-secondary">
|
|
837
828
|
<h3 className="text-h5 text-foreground mb-2">無障礙設計</h3>
|
|
838
|
-
<p className="whitespace-pre-line">{"無障礙行為依裝置分兩條路徑:\n\n ARIA / Pattern :桌機(非觸控)觸發點是一個容器,標記 role=\"combobox\" + aria-expanded / aria-haspopup=\"listbox\" ,選項在浮層的 listbox 裡;searchable 模式時容器內另放一個可打字篩選的 <input> 。手機(觸控)改用瀏覽器原生的 <select> element,直接取得作業系統內建的無障礙與 picker。Field wrapper 兩邊都補 aria-invalid / aria-required / aria-describedby / aria-errormessage ;手機原生 <select> 另由 FieldLabel 的 htmlFor 關聯取得 accessible name。\n\n Keyboard 行為 :\n\n- Tab — 聚焦到觸發點\n- Enter / Space — 展開選單(searchable 模式則直接進入打字篩選)\n- ↑ / ↓ — 選單展開後在選項間移動\n- 字母鍵 — searchable 模式為打字篩選;手機原生 <select> 為平台內建 type-to-jump 逐字定位(桌機非 searchable combobox 無逐字定位)\n- Enter — 選定目前 highlight 的選項並關閉\n- Esc —
|
|
829
|
+
<p className="whitespace-pre-line">{"無障礙行為依裝置分兩條路徑:\n\n ARIA / Pattern :桌機(非觸控)觸發點是一個容器,標記 role=\"combobox\" + aria-expanded / aria-haspopup=\"listbox\" ,選項在浮層的 listbox 裡;searchable 模式時容器內另放一個可打字篩選的 <input> 。手機(觸控)改用瀏覽器原生的 <select> element,直接取得作業系統內建的無障礙與 picker。Field wrapper 兩邊都補 aria-invalid / aria-required / aria-describedby / aria-errormessage ;手機原生 <select> 另由 FieldLabel 的 htmlFor 關聯取得 accessible name。\n\n Keyboard 行為 :\n\n- Tab — 聚焦到觸發點\n- Enter / Space — 展開選單(searchable 模式則直接進入打字篩選)\n- ↑ / ↓ — 選單展開後在選項間移動\n- 字母鍵 — searchable 模式為打字篩選;手機原生 <select> 為平台內建 type-to-jump 逐字定位(桌機非 searchable combobox 無逐字定位)\n- Enter — 選定目前 highlight 的選項並關閉\n- Tab / Shift+Tab(選單展開時)— 同樣選定目前 highlight 的選項並關閉,焦點走到觸發點的下一個 / 上一個可 Tab 元素(= 選單關著時按 Tab 會到的同一格;待辦總帳 B11,規則住 select-menu.spec.md「A11y 預設」)\n- Esc — 關閉選單、不選定(清除值走右側 clear 按鈕,非 Esc)\n\n Focus :DS focus 藍框( focus-within:!border-primary )由 Field wrapper 提供;手機原生 <select> 另有系統 focus ring。\n\n 驗證 :Storybook a11y addon panel 應 0 critical violation;鍵盤完整可操作(無需滑鼠)。WCAG AA contrast ≥ 4.5:1(text)/ 3:1(UI)。"}</p>
|
|
839
830
|
</div>
|
|
840
831
|
),
|
|
841
832
|
}
|
|
@@ -350,9 +350,10 @@ Select 是 **Field Controls family 成員**——互動狀態(focus / invalid /
|
|
|
350
350
|
- Enter / Space — 展開選單(searchable 模式則直接進入打字篩選)
|
|
351
351
|
- ↓ — 選單關閉時亦可直接展開(APG combobox 展開鍵,與 Combobox 同 pattern;展開後不攔,讓方向鍵導覽選單)
|
|
352
352
|
- ↑ / ↓ — 選單展開後在選項間移動
|
|
353
|
-
-
|
|
353
|
+
- Tab / Shift+Tab(選單展開時,searchable 與否皆同)— 選定反白那一項 → 收起 → 焦點落到觸發點的下一個 / 上一個可 Tab 元素(= 選單關著時從觸發點按 Tab / Shift+Tab 會到的那一格;在對話框 / 小面板裡則只在那一層裡走、到邊緣繞回)。2026-09-25 前(實測):不可打字時 Tab 卡在浮層裡或落到頁面外、Shift+Tab 跳到頁尾,都不選定;可打字時 Tab 收起但不選定、Shift+Tab 停在觸發點本身且選單不關。W3C 出處與逐字規則見 `../SelectMenu/select-menu.spec.md`「A11y 預設」(單選那一條);來源 = 待辦總帳 B11
|
|
354
|
+
- Esc — 關閉選單、不選定(清除值走右側 clear 按鈕,非 Esc)
|
|
354
355
|
|
|
355
|
-
**Focus**:Field 家族的焦點指示 = 邊框轉主色,**不分開著關著、不分滑鼠鍵盤**(owner = `ds-canonical/references/focus-canonical.md` 規則二「Field 家族控件本身」列 + 「問題一之二」C 類;2026-09-10 第二次更正:上午先寫成「關閉時鍵盤模態再加全域外框」,下午依一致性收斂 —— Combobox 焦點留在輸入框本來就沒有外框,Select 類關閉後焦點回 wrapper 若再疊外框就是同一家族兩種長相)。**開啟時**焦點在搜尋輸入框(插入點控件)→ Field wrapper 邊框轉色;**關閉時**觸發器本身是焦點站(`tabIndex=0`,選完 / Esc 後 Radix
|
|
356
|
+
**Focus**:Field 家族的焦點指示 = 邊框轉主色,**不分開著關著、不分滑鼠鍵盤**(owner = `ds-canonical/references/focus-canonical.md` 規則二「Field 家族控件本身」列 + 「問題一之二」C 類;2026-09-10 第二次更正:上午先寫成「關閉時鍵盤模態再加全域外框」,下午依一致性收斂 —— Combobox 焦點留在輸入框本來就沒有外框,Select 類關閉後焦點回 wrapper 若再疊外框就是同一家族兩種長相)。**開啟時**焦點在搜尋輸入框(插入點控件)→ Field wrapper 邊框轉色;**關閉時**觸發器本身是焦點站(`tabIndex=0`,選完 / Esc 後 Radix 把焦點還給它;按 Tab 收起時焦點直接走到下一格,不回觸發器)→ 同樣只有邊框轉色,全域 `:focus-visible` 外框由 `fieldWrapperStyles` 的 `focus-visible:outline-none` 抑制。手機原生 `<select>` 另有系統 focus ring。閘:`virtual-cursor-modality-invariant.mjs` G 段(Select / SelectMenu / PeoplePicker)、H 段(DatePicker / TimePicker / Combobox 觸發器)。
|
|
356
357
|
|
|
357
358
|
**驗證**:Storybook a11y addon panel 應 0 critical violation;鍵盤完整可操作(無需滑鼠)。WCAG AA contrast ≥ 4.5:1(text)/ 3:1(UI)。
|
|
358
359
|
|
|
@@ -362,7 +363,7 @@ Select 是 **Field Controls family 成員**——互動狀態(focus / invalid /
|
|
|
362
363
|
|
|
363
364
|
- `checkbox.spec.md`
|
|
364
365
|
- `combobox.spec.md`
|
|
365
|
-
- `command.spec.md`
|
|
366
366
|
- `people-picker.spec.md`
|
|
367
367
|
- `radio-group.spec.md`
|
|
368
368
|
- `select-menu.spec.md`
|
|
369
|
+
- `uiSize.spec.md`
|
|
@@ -492,7 +492,8 @@ function ReadonlyDisplay({
|
|
|
492
492
|
const tagVariant = selectedOpt?.tagVariant as 'blue' | 'green' | 'red' | 'yellow' | 'neutral' | undefined
|
|
493
493
|
|
|
494
494
|
return (
|
|
495
|
-
|
|
495
|
+
// 停用:Tag 自帶 cursor-text,指到 tag 上會蓋掉外框的 cursor-not-allowed(2026-09-29 全站掃出)→ 整塊釘成禁止符號(同 Combobox 非 edit 分支)
|
|
496
|
+
<div className={cn(fieldWrapperStyles({ mode: resolvedMode, variant, width, size: sz }), value && tagPadding[sz], resolvedMode === 'disabled' && '[&_*]:cursor-not-allowed', className)} style={{ paddingRight: 'var(--field-px)' }} data-field-mode={resolvedMode} aria-disabled={ariaDisabled}>
|
|
496
497
|
{value ? <Tag size={sz} color={tagVariant}>{label}</Tag> : <span className={emptyColorCls}>{emptyText}</span>}
|
|
497
498
|
{showIndicator && <ItemSuffix className="pointer-events-none"><ChevronDown size={iconSize} className={cn('shrink-0', iconColor)} aria-hidden /></ItemSuffix>}
|
|
498
499
|
</div>
|
|
@@ -660,7 +661,13 @@ const CustomSelect = React.forwardRef<HTMLDivElement, SelectProps>(
|
|
|
660
661
|
|
|
661
662
|
const [open, setOpen] = React.useState(defaultOpen)
|
|
662
663
|
const [search, setSearchState] = React.useState('')
|
|
663
|
-
|
|
664
|
+
// onSearchChange 走 ref(同 agent-panel.tsx onModeChangeRef 的寫法):setSearch 身分因此穩定,下方「關閉時清搜尋」
|
|
665
|
+
// 的 effect 才能把 setSearch 列進相依而不多跑。若改成 useCallback 隨 onSearchChange 換身分,消費端傳 inline 箭頭函式
|
|
666
|
+
// (select.stories.tsx RemoteSearchDemo 就是)時,關著的每次 render 都會重發 onSearchChange('') → 消費端 setOptions([])
|
|
667
|
+
// 換新陣列 → 再 render → 再重發,無限更新;ref 讀的永遠是最新一版回呼,也沒有過期閉包。
|
|
668
|
+
const onSearchChangeRef = React.useRef(onSearchChange)
|
|
669
|
+
onSearchChangeRef.current = onSearchChange
|
|
670
|
+
const setSearch = React.useCallback((next: string) => { setSearchState(next); onSearchChangeRef.current?.(next) }, [])
|
|
664
671
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
665
672
|
// a11y(2026-07-04):listbox 容器 id——trigger aria-controls 指向 SelectMenu PopoverContent
|
|
666
673
|
// (對齊姊妹元件 combobox.tsx:677 既有 canonical;React.useId SSR/CSR 穩定)。
|
|
@@ -669,8 +676,8 @@ const CustomSelect = React.forwardRef<HTMLDivElement, SelectProps>(
|
|
|
669
676
|
// (機制詳 select-menu.tsx useActiveDescendant docblock;必在 early return 前呼叫 — React #310 hook 順序)。
|
|
670
677
|
const activeOptionId = useActiveDescendant(listboxId, open)
|
|
671
678
|
|
|
672
|
-
// 關閉時清搜尋
|
|
673
|
-
React.useEffect(() => { if (!open) setSearch('') }, [open])
|
|
679
|
+
// 關閉時清搜尋(setSearch 身分穩定 → 這個 effect 實際只在 open 變動時跑,與列相依前的次數相同)
|
|
680
|
+
React.useEffect(() => { if (!open) setSearch('') }, [open, setSearch])
|
|
674
681
|
|
|
675
682
|
// **React #310 fix(2026-05-04)**:所有 hooks 必在任何 early return 前 call,
|
|
676
683
|
// 否則 disabled→edit 切換時 hook count 變動 → React 死亡。
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal — SelectMenu 的鍵盤橋接:觸發欄位與浮層清單分屬兩棵 DOM 子樹(浮層 portal 到 body 最後),
|
|
3
|
+
* 這裡收三件事,由 select-menu.tsx 轉出(Select / Combobox 既有 import 路徑不變):
|
|
4
|
+
* 1. `forwardKeyToListbox` —— 觸發欄位內的輸入框把 ↑ ↓ Enter 轉送給 cmdk(2026-07-05 D4,原住 select-menu.tsx,原樣搬來)
|
|
5
|
+
* 2. `useActiveDescendant` —— 觸發欄位輸入框的 aria-activedescendant(同上,原樣搬來)
|
|
6
|
+
* 3. `useSelectMenuPopupKeys` —— 開著按 Tab / Shift+Tab(2026-09-25 待辦總帳 B11)與不可打字單選的空白鍵(2026-09-26,L7)
|
|
7
|
+
* 2026-09-25 搬家理由(AI 推導):select-menu.tsx 已 799 行,加 3 會超過 `scripts/code-quality-audit.mjs` 的 800 行上限;
|
|
8
|
+
* 三者同一件事(跨子樹的鍵盤 / 焦點接線),與清單渲染無關。
|
|
9
|
+
* 2026-09-26:「從觸發欄位算下一站」與 DropdownMenu 合成一份 `lib/focus-after-trigger.ts`(待辦總帳〇節「按鍵規則合併」)。
|
|
10
|
+
*/
|
|
11
|
+
import * as React from 'react'
|
|
12
|
+
import { flushSync } from 'react-dom'
|
|
13
|
+
import { focusFromTrigger, tabbableOrder } from '@/design-system/lib/focus-after-trigger'
|
|
14
|
+
import { isTextEntryElement } from '@/design-system/lib/roving-list-keyboard'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 2026-07-05 D4 P0 修(searchable 鍵盤死路):trigger 內的裸 <input> 與 portal 內的 cmdk root
|
|
18
|
+
* 在不同 DOM 子樹 — 鍵盤事件永遠 bubble 不到 cmdk 的 ArrowUp/Down/Enter handler([cmdk-root]
|
|
19
|
+
* onKeyDown)→ searchable Select / PeoplePicker single / Combobox searchIn='trigger' 開啟後
|
|
20
|
+
* 只能 Esc。修法 = APG combobox-with-list:trigger input 把三鍵 re-dispatch 給 cmdk root
|
|
21
|
+
* (native KeyboardEvent bubbles 經 React root delegation 觸發 cmdk synthetic handler)。
|
|
22
|
+
* Home/End 刻意不轉送(文字輸入的 caret 語意優先,對齊 MUI/Ant Autocomplete)。
|
|
23
|
+
* aria-activedescendant 綁回 trigger input → 見下方 useActiveDescendant(2026-07-05 D4 補齊)。
|
|
24
|
+
*/
|
|
25
|
+
export function forwardKeyToListbox(contentId: string | undefined, e: React.KeyboardEvent): boolean {
|
|
26
|
+
if (!contentId) return false
|
|
27
|
+
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp' && e.key !== 'Enter') return false
|
|
28
|
+
const root = document.getElementById(contentId)?.querySelector<HTMLElement>('[cmdk-root]')
|
|
29
|
+
if (!root) return false
|
|
30
|
+
e.preventDefault()
|
|
31
|
+
root.dispatchEvent(new KeyboardEvent('keydown', { key: e.key, bubbles: true, cancelable: true }))
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 2026-07-05 D4 補齊(APG combobox aria-activedescendant):追蹤 cmdk 目前 virtual-focus item
|
|
37
|
+
* 的 DOM id,供 trigger 端搜尋 input 綁 `aria-activedescendant` —— SR 才會在方向鍵導覽 /
|
|
38
|
+
* 打字過濾時播報 active option 名。機制:trigger 與 portal 內 cmdk 分屬不同 DOM 子樹,cmdk
|
|
39
|
+
* 只把 active id 綁在自己的 Command.Input / List(cmdk source:item 自帶 auto-generated id +
|
|
40
|
+
* `data-selected="true"` 標記 virtual focus)→ trigger 端用 MutationObserver 監聽 popover 容器
|
|
41
|
+
* (contentId = PopoverContent id)內 `data-selected` 屬性變化 + childList(打字過濾 re-render
|
|
42
|
+
* 換 item 節點),單一機制涵蓋全部更新路徑:開啟初始 auto-highlight / forwardKeyToListbox
|
|
43
|
+
* 方向鍵轉送 / 搜尋過濾後 cmdk 自動移 cursor / pointer hover。
|
|
44
|
+
* 關閉時清 undefined —— ARIA 要求 id 必指向存在於 DOM 的節點,不可留 stale id。
|
|
45
|
+
*/
|
|
46
|
+
export function useActiveDescendant(contentId: string | undefined, open: boolean): string | undefined {
|
|
47
|
+
const [activeId, setActiveId] = React.useState<string | undefined>(undefined)
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
if (!open || !contentId) {
|
|
50
|
+
setActiveId(undefined)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
// PopoverContent 與 trigger 同一個 React commit mount(open state 同批 render)→ effect 跑時已在 DOM
|
|
54
|
+
const container = document.getElementById(contentId)
|
|
55
|
+
if (!container) return
|
|
56
|
+
const read = () => {
|
|
57
|
+
setActiveId(container.querySelector<HTMLElement>('[cmdk-item][data-selected="true"]')?.id || undefined)
|
|
58
|
+
}
|
|
59
|
+
// 初始補讀:MutationObserver 只看「觀察開始後」的變化;cmdk 初始 auto-highlight(layout effect
|
|
60
|
+
// 排程)可能已 commit → rAF 讀當下狀態兜底,與 observer 互補、誰先到都不漏。
|
|
61
|
+
const raf = requestAnimationFrame(read)
|
|
62
|
+
const observer = new MutationObserver(read)
|
|
63
|
+
observer.observe(container, { subtree: true, childList: true, attributes: true, attributeFilter: ['data-selected'] })
|
|
64
|
+
return () => {
|
|
65
|
+
cancelAnimationFrame(raf)
|
|
66
|
+
observer.disconnect()
|
|
67
|
+
}
|
|
68
|
+
}, [open, contentId])
|
|
69
|
+
return activeId
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── 開著按 Tab / Shift+Tab(2026-09-25 待辦總帳 B11;SSOT = select-menu.spec.md「A11y 預設」)─────────────
|
|
73
|
+
//
|
|
74
|
+
// 兩種浮層、兩條規則 —— W3C 下拉規範依「彈出的是什麼」分流:
|
|
75
|
+
//
|
|
76
|
+
// (1) 單選 = 清單型(listbox):選定反白那一項 → 收起 → 焦點落到觸發欄位的下一個(Shift:上一個)可 Tab 元素,
|
|
77
|
+
// 也就是「關著時從觸發欄位按 Tab 會到的同一格」。
|
|
78
|
+
// W3C 單選下拉範例 Tab 列:「Sets the value to the content of the focused option in the listbox. / Closes the listbox. /
|
|
79
|
+
// Performs the default action, moving focus to the next focusable element.」
|
|
80
|
+
// https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/combobox/examples/combobox-select-only.html#L187-L199
|
|
81
|
+
// Shift+Tab 同樣選定:該範例是在觸發欄位失焦時選定,不分方向
|
|
82
|
+
// https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/combobox/examples/js/select-only.js#L249-L260
|
|
83
|
+
// Fluent 單選 `case 'Tab': !multiselect && activeOption && selectOption(e, activeOption);`(不分 Shift)
|
|
84
|
+
// https://github.com/microsoft/fluentui/blob/d27922755bebae866d9ffe86b7da44c27ec801ee/packages/react-components/react-combobox/library/src/utils/useTriggerSlot.ts#L198-L200
|
|
85
|
+
//
|
|
86
|
+
// (2) 多選 = 小面板型(dialog:面板裡有搜尋框 / 全選鈕,DOM 焦點會進到面板):Tab 留在面板裡繞圈,行為不變。
|
|
87
|
+
// W3C:dialog 型彈出「implements the keyboard interaction defined in the modal dialog pattern」
|
|
88
|
+
// https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/combobox/combobox-pattern.html#L391
|
|
89
|
+
// 對話框「contain their tab sequence」
|
|
90
|
+
// https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/dialog-modal/dialog-modal-pattern.html#L29-L30
|
|
91
|
+
// 這裡只補一個洞:焦點停在「程式落點」(cmdk 殼,tabIndex -1,不在 Tab 序裡)時,Shift+Tab 照 DOM 順序往前
|
|
92
|
+
// 找到的是頁面最後一個元素(浮層掛在 body 最後)→ 跳出面板到頁尾(R15 實測 S-PAGE-END)。
|
|
93
|
+
//
|
|
94
|
+
// 為什麼自己算「下一格」,不把焦點先丟回觸發欄位再讓瀏覽器照走(AI 推導,2026-09-25):
|
|
95
|
+
// (a) 焦點在觸發欄位上停一下,欄位的截斷 tooltip 會因 focus 打開、又立刻因 blur 關閉 → 閃一下淡出動畫;
|
|
96
|
+
// (b) 觸發欄位在另一個小面板 / 對話框裡時,那一層的 Radix FocusScope 正被本浮層「暫停」
|
|
97
|
+
// (`@radix-ui/react-focus-scope` 1.1.7 dist:`if (focusScope.paused) return`),不會替我們繞回 ——
|
|
98
|
+
// 交給瀏覽器照走,觸發欄位若是那一層的第一 / 最後一格,焦點就跑出去、把外層面板一起關掉。
|
|
99
|
+
// 「下一格」的算法與選單同一份(lib/focus-after-trigger.ts):對話框 / 面板裡繞圈;頁面上已沒有下一格 → 留在觸發欄位(X20)。
|
|
100
|
+
// 2026-09-26 之前這裡另有一份(不認正 tabindex、頁面邊緣交還瀏覽器 —— 會落在 Radix 關閉動畫期間仍掛著的隱形焦點護欄上)。
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 多選小面板:焦點在程式落點(不在 Tab 序裡)時,Tab / Shift+Tab 仍留在面板裡(規則 (2))。
|
|
104
|
+
* 焦點已在可 Tab 元素上 → 不插手:Radix FocusScope(`loop: true`,`@radix-ui/react-popover` 1.1.15 dist/index.mjs:225-226)
|
|
105
|
+
* 在邊緣自己繞回,中間交給瀏覽器照 DOM 順序走,本來就不會出面板。
|
|
106
|
+
*/
|
|
107
|
+
function keepTabInsidePanel(e: React.KeyboardEvent, content: HTMLElement | null) {
|
|
108
|
+
if (!content) return
|
|
109
|
+
const active = content.ownerDocument.activeElement as HTMLElement | null
|
|
110
|
+
if (!active || !content.contains(active)) return // 焦點不在面板裡(例:搜尋框在觸發欄位內)→ 不歸這裡管
|
|
111
|
+
const list = tabbableOrder(content)
|
|
112
|
+
if (list.includes(active)) return
|
|
113
|
+
e.preventDefault()
|
|
114
|
+
if (list.length === 0) return // 面板裡沒有可 Tab 的東西:焦點留在原地(仍不出面板)
|
|
115
|
+
const preceding = list.filter((el) => active.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING)
|
|
116
|
+
const following = list.filter((el) => active.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING)
|
|
117
|
+
const target = e.shiftKey ? (preceding[preceding.length - 1] ?? list[list.length - 1]) : (following[0] ?? list[0])
|
|
118
|
+
target.focus()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface SelectMenuPopupKeysParams {
|
|
122
|
+
open: boolean
|
|
123
|
+
multiple: boolean
|
|
124
|
+
/** cmdk 反白列的 `data-value`(cmdk 會 trim)→ 對應的可選選項值;不是可選選項(建立列 / disabled)回 undefined */
|
|
125
|
+
resolveOptionValue: (dataValue: string) => string | undefined
|
|
126
|
+
isSelected: (value: string) => boolean
|
|
127
|
+
/** 單選:選定 + 收起(SelectMenu `handleSelect`) */
|
|
128
|
+
commit: (value: string) => void
|
|
129
|
+
close: () => void
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 焦點在文字輸入上(可打字搜尋的搜尋框):空白鍵是打字,不是選取(判準 = lib/roving-list-keyboard.ts `isTextEntryElement`,全 DS 一份) */
|
|
133
|
+
const isTextEntryTarget = (target: EventTarget | null) => target instanceof Element && isTextEntryElement(target)
|
|
134
|
+
|
|
135
|
+
/** 反白列(cmdk 的 data-selected)對應的可選選項值;建立列 / 停用 / 沒有反白 → undefined */
|
|
136
|
+
function highlightedOptionValue(content: HTMLElement | null, resolve: (dataValue: string) => string | undefined) {
|
|
137
|
+
const dataValue = content
|
|
138
|
+
?.querySelector<HTMLElement>('[cmdk-item][data-selected="true"]:not([data-disabled="true"])')
|
|
139
|
+
?.getAttribute('data-value')
|
|
140
|
+
return dataValue == null ? undefined : resolve(dataValue)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 回傳:
|
|
145
|
+
* - `triggerRef` → PopoverTrigger;`contentRef` → PopoverContent
|
|
146
|
+
* - `onKeyDown`:掛在觸發欄位(可打字搜尋時焦點在欄位內的輸入框)與 cmdk 根(不可打字時焦點在浮層裡)兩處
|
|
147
|
+
* - `onCloseAutoFocus` → PopoverContent:Tab 收起時擋掉 Radix「關閉後把焦點還給觸發欄位」——那一步在 FocusScope 卸載
|
|
148
|
+
* (關閉動畫結束後的 setTimeout 0)才跑,會把剛走到下一格的焦點搶回來(與 B11 點名的
|
|
149
|
+
* `agent-panel-fab.tsx:702-705`「關選單時硬搶焦點」同一類)
|
|
150
|
+
*/
|
|
151
|
+
export function useSelectMenuPopupKeys(params: SelectMenuPopupKeysParams) {
|
|
152
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null)
|
|
153
|
+
const contentRef = React.useRef<HTMLDivElement>(null)
|
|
154
|
+
const latest = React.useRef(params)
|
|
155
|
+
latest.current = params
|
|
156
|
+
const closedByTabRef = React.useRef(false)
|
|
157
|
+
|
|
158
|
+
// 重新開啟 = 新的一輪,舊的「由 Tab 收起」記號作廢
|
|
159
|
+
React.useEffect(() => {
|
|
160
|
+
if (params.open) closedByTabRef.current = false
|
|
161
|
+
}, [params.open])
|
|
162
|
+
|
|
163
|
+
const onKeyDown = React.useCallback((e: React.KeyboardEvent) => {
|
|
164
|
+
const p = latest.current
|
|
165
|
+
if (e.altKey || e.ctrlKey || e.metaKey) return
|
|
166
|
+
// IME 組字中不動(同 DataTable handleEditTab 的 isComposing / 229 守衛)
|
|
167
|
+
if (e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229) return
|
|
168
|
+
if (!p.open || e.defaultPrevented) return
|
|
169
|
+
const content = contentRef.current
|
|
170
|
+
|
|
171
|
+
// ── 不可打字的單選:空白鍵 = 選這一項(同 Enter)──
|
|
172
|
+
// 2026-09-26 待辦總帳〇節 L7 第 1 條,主線正本定義:「不能打字的選項清單,Enter 與空白鍵都是選這一項」;
|
|
173
|
+
// 同意 = 同檔「09-26 同意清單回覆」,user 逐字:「確保符合我們一致的設計語言且不違背世界級的設計且都有確保整個ds 是SSOT,避免漂移就照你建議做」。
|
|
174
|
+
// W3C 單選下拉範例「Listbox Popup」表的 Space 列,與 Enter 列逐字相同:「Sets the value to the content of the focused option
|
|
175
|
+
// in the listbox. / Closes the listbox. / Sets visual focus on the combobox.」
|
|
176
|
+
// (https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/combobox/examples/combobox-select-only.html#L177-L186)。
|
|
177
|
+
// 可打字(焦點在搜尋框)時空白鍵是打字,不動;多選(面板)不在本條。cmdk 自己不處理空白鍵,所以不會重複觸發。
|
|
178
|
+
if (e.key === ' ') {
|
|
179
|
+
if (p.multiple || isTextEntryTarget(e.target)) return
|
|
180
|
+
e.preventDefault()
|
|
181
|
+
const value = highlightedOptionValue(content, p.resolveOptionValue)
|
|
182
|
+
// 與 Enter 同一個動作(cmdk Enter → CommandItem onSelect → handleSelect):選定 + 收起,焦點由 Radix 還給觸發欄位
|
|
183
|
+
if (value !== undefined) p.commit(value)
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
if (e.key !== 'Tab') return
|
|
187
|
+
|
|
188
|
+
// B11 多選(有全選):行為不變,Tab 留在面板裡;只修程式落點上 Shift+Tab 跳頁尾
|
|
189
|
+
if (p.multiple) {
|
|
190
|
+
keepTabInsidePanel(e, content)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// B11 單選:Tab / Shift+Tab = 選定反白那一項 + 收起 + 從觸發欄位往下 / 往上走
|
|
195
|
+
const value = highlightedOptionValue(content, p.resolveOptionValue)
|
|
196
|
+
const trigger = triggerRef.current
|
|
197
|
+
if (!trigger) return
|
|
198
|
+
e.preventDefault()
|
|
199
|
+
closedByTabRef.current = true
|
|
200
|
+
// flushSync:先讓收起生效(可打字搜尋的輸入框卸載、觸發欄位換回顯示值),再算「下一格」,
|
|
201
|
+
// 否則算到的會是即將消失的輸入框。已是目前值 → 不重發 onValueChange,只收起。
|
|
202
|
+
flushSync(() => {
|
|
203
|
+
if (value !== undefined && !p.isSelected(value)) p.commit(value)
|
|
204
|
+
else p.close()
|
|
205
|
+
})
|
|
206
|
+
// 下一格 = 觸發欄位關著時按同一個鍵會到的那一格(lib/focus-after-trigger.ts;與選單同一份)。
|
|
207
|
+
// 本浮層關閉動畫期間還在 DOM 裡,裡面的捲動區可 Tab → 排除。
|
|
208
|
+
focusFromTrigger(trigger, e.shiftKey, (el) => !!content?.contains(el))
|
|
209
|
+
}, [])
|
|
210
|
+
|
|
211
|
+
const onCloseAutoFocus = React.useCallback((e: Event) => {
|
|
212
|
+
if (!closedByTabRef.current) return
|
|
213
|
+
closedByTabRef.current = false
|
|
214
|
+
e.preventDefault()
|
|
215
|
+
}, [])
|
|
216
|
+
|
|
217
|
+
return { triggerRef, contentRef, onKeyDown, onCloseAutoFocus }
|
|
218
|
+
}
|
|
@@ -10,6 +10,7 @@ import { useState } from 'react'
|
|
|
10
10
|
import { Mail, Folder, Users, Settings } from 'lucide-react'
|
|
11
11
|
import { SelectMenu, type SelectMenuOption, type SelectMenuGroupConfig } from './select-menu'
|
|
12
12
|
import { Button } from '@/design-system/components/Button/button'
|
|
13
|
+
import { SegmentedControl, SegmentedControlItem } from '@/design-system/components/SegmentedControl/segmented-control'
|
|
13
14
|
import { H3, Desc, Td, Th } from '@/design-system/stories-helpers/anatomy/anatomy-utils'
|
|
14
15
|
|
|
15
16
|
const meta: Meta = {
|
|
@@ -348,22 +349,10 @@ function SelectMenuInspector() {
|
|
|
348
349
|
<div className="flex flex-col gap-6">
|
|
349
350
|
<div>
|
|
350
351
|
<H3>Size</H3>
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
type="button"
|
|
356
|
-
onClick={() => setSize(s)}
|
|
357
|
-
className={`px-2.5 py-1 text-caption rounded-md font-mono cursor-pointer ${
|
|
358
|
-
s === size
|
|
359
|
-
? 'bg-primary text-white'
|
|
360
|
-
: 'bg-neutral-hover text-fg-secondary hover:bg-neutral-active'
|
|
361
|
-
}`}
|
|
362
|
-
>
|
|
363
|
-
{s}
|
|
364
|
-
</button>
|
|
365
|
-
))}
|
|
366
|
-
</div>
|
|
352
|
+
{/* 互斥切換消費 SegmentedControl(segmented-control.spec.md「何時用」2–5 個互斥選項);取代手刻 pill:它靜止借 neutral-hover、hover 借 neutral-active(color.spec.md 成對 token 錯配) */}
|
|
353
|
+
<SegmentedControl size="sm" aria-label="Size" value={size} onValueChange={(v) => setSize(v as SizeKey)}>
|
|
354
|
+
{SIZES.map((s) => <SegmentedControlItem key={s} value={s}>{s}</SegmentedControlItem>)}
|
|
355
|
+
</SegmentedControl>
|
|
367
356
|
</div>
|
|
368
357
|
|
|
369
358
|
<div>
|
|
@@ -848,7 +837,7 @@ export const Accessibility = {
|
|
|
848
837
|
render: () => (
|
|
849
838
|
<div className="max-w-3xl text-body text-fg-secondary">
|
|
850
839
|
<h3 className="text-h5 text-foreground mb-2">無障礙設計</h3>
|
|
851
|
-
<p className="whitespace-pre-line">{"詳 `select-menu.spec.md` 「A11y 預設」段。摘要:\n\n ARIA / Pattern :基於 cmdk library a11y(combobox / listbox / option role + aria-activedescendant)。詳 [cmdk a11y](https://cmdk.paco.me/#accessibility)。\n\n Keyboard 行為 :\n\n- Tab — focus trigger\n- Enter / Space — 開啟 menu(trigger 由 consumer 經 PopoverTrigger asChild 提供:Select / Combobox 的 trigger 是 role=\"combobox\" 容器自綁 Enter / Space handler;若 consumer 用 DS Button 則由 native click 觸發)\n- ↑/↓ — 導覽 options(menu 開啟後)\n- Enter — 選擇\n- 字母鍵 — type-ahead 過濾(search 模式)\n- Tab(menu 開啟 +
|
|
840
|
+
<p className="whitespace-pre-line">{"詳 `select-menu.spec.md` 「A11y 預設」段。摘要:\n\n ARIA / Pattern :基於 cmdk library a11y(combobox / listbox / option role + aria-activedescendant)。詳 [cmdk a11y](https://cmdk.paco.me/#accessibility)。\n\n Keyboard 行為 :\n\n- Tab — focus trigger\n- Enter / Space — 開啟 menu(trigger 由 consumer 經 PopoverTrigger asChild 提供:Select / Combobox 的 trigger 是 role=\"combobox\" 容器自綁 Enter / Space handler;若 consumer 用 DS Button 則由 native click 觸發)\n- ↑/↓ — 導覽 options(menu 開啟後)\n- Enter — 選擇\n- Space(menu 開啟 + 單選、不可打字)— 與 Enter 相同:選定反白那一項、收起、焦點回觸發欄位(待辦總帳 L7)\n- 字母鍵 — type-ahead 過濾(search 模式)\n- Tab / Shift+Tab(menu 開啟 + 單選)— 選定反白那一項、收起,焦點走到觸發欄位的下一個 / 上一個可 Tab 元素(= 選單關著時按 Tab 會到的同一格;頁面上已沒有下一格時留在觸發欄位;待辦總帳 B11)\n- Tab / Shift+Tab(menu 開啟 + 多選)— 行為不變:在面板裡繞圈(浮層內搜尋框 → 清單 → footer 的全選按鈕 → 回第一格);全選是真的 <button>,天生在 Tab 序裡(2026-09-17 起取代原本的 role=\"checkbox\" 全選列)。觸發欄位因此宣告 aria-haspopup=\"dialog\"(焦點會進到面板),單選宣告 listbox\n- Enter / Space(focus 在全選按鈕)— 切換全選 / 全清(瀏覽器原生按鈕行為)\n- Esc — 關閉、不選定\n\n Focus :menu 開啟時 active-descendant 虛擬焦點落在第一個 / 已選 option(aria-activedescendant 高亮,非 DOM focus);searchable 時 DOM focus 給搜尋 input;非 searchable 時 DOM focus 移到 cmdk 的 [cmdk-root](Command 元素,見 handleNonSearchableAutoFocus),讓 cmdk 內建方向鍵 / Enter / Home / End 導覽生效;option 無 tabIndex,DOM focus 不落在 option 上。選完 / Esc 關閉時 focus 回 trigger;單選按 Tab 收起時焦點直接走到下一格、不回 trigger。\n\n 驗證 :Storybook a11y addon panel 應 0 critical violation;鍵盤完整可操作(無需滑鼠)。WCAG AA contrast ≥ 4.5:1(text)/ 3:1(UI)。"}</p>
|
|
852
841
|
</div>
|
|
853
842
|
),
|
|
854
843
|
}
|
|
@@ -436,16 +436,20 @@ SelectMenu 是 **composite**(Popover trigger + Command search + 滾動 MenuItem
|
|
|
436
436
|
|
|
437
437
|
**Keyboard 行為**:
|
|
438
438
|
|
|
439
|
-
- Tab — focus trigger
|
|
439
|
+
- Tab — focus trigger(選單關著時)
|
|
440
440
|
- Enter / Space — 開啟 menu(trigger 由 consumer 經 `PopoverTrigger asChild` 提供,開啟行為由 consumer trigger 負責:Select / Combobox 的 trigger 是 `role="combobox"` 容器,自綁 Enter / Space handler 觸發開啟;若 consumer 用 DS `<Button>`(native button)則由 native click 觸發)
|
|
441
441
|
- ↑/↓ — 導覽 options(menu 開啟後)
|
|
442
|
-
- Enter — 選擇
|
|
442
|
+
- Enter — 選擇(不可打字的單選:Space 與 Enter 相同,見下方 Space 列)
|
|
443
443
|
- 字母鍵 — type-ahead 過濾(search 模式)
|
|
444
|
-
- Tab(menu 開啟 +
|
|
444
|
+
- Tab / Shift+Tab(menu 開啟 + **單選**)— **選定反白那一項**(已是目前值就不重發 `onValueChange`;反白在建立列 / 沒有反白時只收起,Tab 不替使用者建立新選項)→ **收起** → 焦點落到**觸發欄位的下一個 / 上一個**可 Tab 元素,也就是「選單關著時從觸發欄位按 Tab / Shift+Tab 會到的同一格」;觸發欄位在對話框 / 小面板(`role="dialog"`)裡時只在那一層裡走、到邊緣繞回(那一層的 Tab 留在裡面,見 `../Popover/popover.spec.md`「A11y 預設」)。依據:W3C 單選下拉範例 Tab 列「Sets the value to the content of the focused option in the listbox. / Closes the listbox. / Performs the default action, moving focus to the next focusable element.」(<https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/combobox/examples/combobox-select-only.html#L187-L199>),Shift+Tab 同樣選定 —— 該範例在觸發欄位失焦時選定、不分方向(<https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/combobox/examples/js/select-only.js#L249-L260>);Fluent 單選 `case 'Tab': !multiselect && activeOption && selectOption(e, activeOption);`(<https://github.com/microsoft/fluentui/blob/d27922755bebae866d9ffe86b7da44c27ec801ee/packages/react-components/react-combobox/library/src/utils/useTriggerSlot.ts#L198-L200>)。來源:待辦總帳 `governance/planning/2026-09-25-interaction-and-hover-remediation.md` B11(user #25 附條件同意 + #32 一致性原則;單選下拉與選單一起改屬 AI 依一致性推導,user 可於預覽否決)。實作 `select-menu-keyboard.ts` `useSelectMenuPopupKeys`;「下一格」與選單同一份 `../../lib/focus-after-trigger.ts`(2026-09-26 兩份合一):頁面上已經沒有下一格時,焦點**留在觸發欄位**(與 `DropdownMenu` 同一句,`ds-canonical/references/keyboard-model-canonical.md`「彈出框開著時的 Tab 與 Esc」;2026-09-26 前這裡交還瀏覽器,會落在 Radix 關閉動畫期間仍掛在 `<body>` 首尾的隱形焦點護欄上)
|
|
445
|
+
- Space(menu 開啟 + **單選、不可打字**)— **選這一項**,與 Enter 相同:選定反白那一項 → 收起 → 焦點回觸發欄位(2026-09-26 待辦總帳 L7 第 1 條,主線定義「不能打字的選項清單,Enter 與空白鍵都是選這一項」;同意 = 同帳〇節「09-26 同意清單回覆」)。依據:W3C 單選下拉範例「Listbox Popup」表 Space 列與 Enter 列逐字相同「Sets the value to the content of the focused option in the listbox. / Closes the listbox. / Sets visual focus on the combobox.」(<https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/combobox/examples/combobox-select-only.html#L177-L186>)。可打字時空白鍵是打字,不選;多選(面板)不在本條。實作 `select-menu-keyboard.ts` `useSelectMenuPopupKeys`
|
|
446
|
+
- Tab / Shift+Tab(menu 開啟 + **多選**)— **行為不變:Tab 在面板裡繞圈**(浮層內搜尋框 → 清單捲動區 → footer 的全選按鈕 → 回到第一格;全選按鈕是真的 `<button>`,天生在 Tab 序裡;它在 CommandList 之外、非 cmdk-item,cmdk 方向鍵導覽不涵蓋)。多選面板裡有搜尋框 / 全選鈕、DOM 焦點會進到面板,是 W3C 的 dialog 型彈出,照對話框規則把 Tab 留在裡面(<https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/combobox/combobox-pattern.html#L391>、<https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/dialog-modal/dialog-modal-pattern.html#L29-L30>)。焦點停在開啟時的程式落點(`[cmdk-root]`,tabIndex -1)時也一樣留在面板裡 —— 2026-09-25 前這一格按 Shift+Tab 會照 DOM 順序跳到**頁尾**並關掉面板(浮層掛在 body 最後;R15 實測)。搜尋框在觸發欄位內(Combobox `searchIn='trigger'`)時焦點不在面板裡,Tab 照頁面順序離開、面板隨焦點離開而關
|
|
445
447
|
- Enter / Space(focus 在全選按鈕)— 切換全選 / 全清。瀏覽器原生按鈕行為,不需自寫 handler;cmdk root 的 Enter 只對 `[cmdk-item]` 的反白項派送,不會重複觸發
|
|
446
448
|
- Esc — 關閉
|
|
447
449
|
|
|
448
|
-
|
|
450
|
+
**觸發欄位宣告的彈出型別**(owner = consumer trigger,Select / Combobox 各自宣告;2026-09-25 B11「多選下拉(有全選)行為不變、只改宣告」):宣告必須等於「開著時 Tab 怎麼走」—— Tab 收起、往頁面下一格走(清單型)= `aria-haspopup="listbox"`(Select / PeoplePicker 單選、Combobox `searchIn='trigger'`);Tab 在面板裡繞圈(對話框型,面板裡有搜尋框 / 全選鈕、DOM 焦點進到面板)= `aria-haspopup="dialog"`(Combobox `searchIn='menu'`、PeoplePicker 多選預設)。已知落差(未處理,另登記):不可打字的 Select 開啟時 DOM 焦點進到浮層的 cmdk 殼,而 W3C 單選下拉範例是焦點留在觸發欄位、由觸發欄位的 `aria-activedescendant` 指向選項(<https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/combobox/examples/js/select-only.js#L327-L328>)。W3C:popup 不是 listbox 時 `aria-haspopup` 要寫出型別(<https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/combobox/combobox-pattern.html#L410-L411>),dialog 型「DOM focus moves into the dialog」(同檔 #L395)。
|
|
451
|
+
|
|
452
|
+
**Focus**:menu 開啟時 active-descendant 虛擬焦點落在第一個 / 已選 option(`aria-activedescendant` 高亮,非 DOM focus;cmdk listbox 模式);searchable 時 DOM focus 給搜尋 input,非 searchable 時 DOM focus 移到 cmdk 的 `[cmdk-root]`(`Command` 元素,見 `handleNonSearchableAutoFocus`),讓 cmdk 內建方向鍵 / Enter / Home / End 導覽生效。option 為 `role="option"` 無 tabIndex,DOM focus 不落在 option 上。選完 / Esc 關閉時 focus 回 trigger;**單選按 Tab 收起時焦點走到下一格、不回 trigger**(Radix 關閉後「還焦點給 trigger」那一步由 `onCloseAutoFocus` 擋掉,否則關閉動畫結束後會把焦點搶回來)。
|
|
449
453
|
|
|
450
454
|
**驗證**:Storybook a11y addon panel 應 0 critical violation;鍵盤完整可操作(無需滑鼠)。WCAG AA contrast ≥ 4.5:1(text)/ 3:1(UI)。
|
|
451
455
|
|
|
@@ -453,8 +457,12 @@ SelectMenu 是 **composite**(Popover trigger + Command search + 滾動 MenuItem
|
|
|
453
457
|
|
|
454
458
|
> 本節由 `scripts/add-reciprocal-pointers.mjs` 自動維護,列出在 SSOT 語境下指向本 spec 的其他 spec。若要手動補充,寫在本節之前。
|
|
455
459
|
|
|
460
|
+
- `combobox.spec.md`
|
|
456
461
|
- `command.spec.md`
|
|
457
462
|
- `dropdown-menu.spec.md`
|
|
463
|
+
- `empty.spec.md`
|
|
464
|
+
- `field-controls.spec.md`
|
|
458
465
|
- `menu-item.spec.md`
|
|
466
|
+
- `people-picker.spec.md`
|
|
459
467
|
- `popover.spec.md`
|
|
460
468
|
- `select.spec.md`
|
|
@@ -19,6 +19,8 @@ import { Button } from '@/design-system/components/Button/button'
|
|
|
19
19
|
import { OVERLAY_SIDE_OFFSET } from '@/design-system/tokens/elevation/overlay-geometry'
|
|
20
20
|
import { RowSizeProvider } from '@/design-system/patterns/element-anatomy/item-anatomy'
|
|
21
21
|
import { applySelectAll, clearSelection } from '@/design-system/lib/multi-select-ordering'
|
|
22
|
+
// 觸發欄位 ↔ 清單的鍵盤橋接(方向鍵轉送 / aria-activedescendant / 開著按 Tab / 不可打字單選的空白鍵)住在 select-menu-keyboard.ts
|
|
23
|
+
import { useSelectMenuPopupKeys } from '@/design-system/components/SelectMenu/select-menu-keyboard'
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* SelectMenu — Popover + Command 組成的完整下拉選單
|
|
@@ -475,16 +477,29 @@ const SelectMenu = React.forwardRef<HTMLElement, SelectMenuProps>(function Selec
|
|
|
475
477
|
root?.focus({ preventScroll: true })
|
|
476
478
|
}, [])
|
|
477
479
|
|
|
480
|
+
// 開著按 Tab / Shift+Tab(2026-09-25 待辦總帳 B11;W3C / Fluent 出處與理由見 select-menu-keyboard.ts):
|
|
481
|
+
// 單選 = 選定反白那一項 + 收起 + 焦點從觸發欄位往下 / 往上走;多選(有全選的小面板)= Tab 留在面板裡,行為不變。
|
|
482
|
+
// 不可打字的單選:空白鍵 = 選這一項,同 Enter(2026-09-26 待辦總帳 L7;出處同檔)。
|
|
483
|
+
const popupKeys = useSelectMenuPopupKeys({
|
|
484
|
+
open, multiple, isSelected, commit: handleSelect, close: () => setOpen(false),
|
|
485
|
+
// 反白列的 data-value(cmdk 已 trim)→ 可選選項值;建立列 / disabled 不算 —— Tab 不替使用者建立新選項
|
|
486
|
+
resolveOptionValue: (dataValue) => visibleOptions.find((o) => !o.disabled && o.value.trim() === dataValue)?.value,
|
|
487
|
+
})
|
|
488
|
+
|
|
478
489
|
// RowSizeProvider 讓 PopoverContent 子樹內任何 <ItemIcon> / <ItemAvatar> /
|
|
479
490
|
// <ItemInlineAction> 都自動讀到對的 size,跟 SidebarProvider / TreeView 同一條規則。
|
|
480
491
|
// (注:Popover 透過 Portal 渲染,context 仍然會跨 portal 傳遞——React context 是 tree-based
|
|
481
492
|
// 不是 DOM-based,Portal 不影響 context propagation)
|
|
482
493
|
return (
|
|
483
494
|
<Popover open={open} onOpenChange={setOpen}>
|
|
484
|
-
|
|
495
|
+
{/* B11:可打字搜尋時焦點在觸發欄位內的輸入框,Tab 規則要掛在這裡(不可打字時掛在下方 Command) */}
|
|
496
|
+
<PopoverTrigger asChild ref={popupKeys.triggerRef} onKeyDown={popupKeys.onKeyDown}>{children}</PopoverTrigger>
|
|
485
497
|
<RowSizeProvider value={size}>
|
|
486
498
|
<PopoverContent
|
|
499
|
+
ref={popupKeys.contentRef}
|
|
487
500
|
id={contentId}
|
|
501
|
+
// B11:Tab 收起時不讓 Radix 在關閉動畫後把焦點搶回觸發欄位(焦點已走到下一格)
|
|
502
|
+
onCloseAutoFocus={popupKeys.onCloseAutoFocus}
|
|
488
503
|
// 2026-07-17 Dim 10 a11y 修:role="dialog" 浮層 accessible name(Radix Popover 無自動命名)
|
|
489
504
|
aria-label={ariaLabel}
|
|
490
505
|
// w-auto override PopoverContent default w-72(rich-popover canonical)— SelectMenu 走「跟 trigger 同寬」
|
|
@@ -528,6 +543,8 @@ const SelectMenu = React.forwardRef<HTMLElement, SelectMenuProps>(function Selec
|
|
|
528
543
|
}}
|
|
529
544
|
>
|
|
530
545
|
<Command
|
|
546
|
+
// B11 / L7:不可打字時焦點在浮層裡(cmdk 殼 / 清單),Tab 與空白鍵的規則掛在這裡
|
|
547
|
+
onKeyDown={popupKeys.onKeyDown}
|
|
531
548
|
shouldFilter={searchable && filterOption}
|
|
532
549
|
// 2026-07-06 cursor 起點修:單選已有值時 cmdk virtual focus 落在已選項而非第一項。
|
|
533
550
|
// cmdk 1.1.1 初始 state 取 defaultValue、item mount 的 selectFirstItem 有
|
|
@@ -740,60 +757,8 @@ export const selectMenuMeta = {
|
|
|
740
757
|
},
|
|
741
758
|
} as const
|
|
742
759
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
* onKeyDown)→ searchable Select / PeoplePicker single / Combobox searchIn='trigger' 開啟後
|
|
747
|
-
* 只能 Esc。修法 = APG combobox-with-list:trigger input 把三鍵 re-dispatch 給 cmdk root
|
|
748
|
-
* (native KeyboardEvent bubbles 經 React root delegation 觸發 cmdk synthetic handler)。
|
|
749
|
-
* Home/End 刻意不轉送(文字輸入的 caret 語意優先,對齊 MUI/Ant Autocomplete)。
|
|
750
|
-
* aria-activedescendant 綁回 trigger input → 見下方 useActiveDescendant(2026-07-05 D4 補齊)。
|
|
751
|
-
*/
|
|
752
|
-
export function forwardKeyToListbox(contentId: string | undefined, e: React.KeyboardEvent): boolean {
|
|
753
|
-
if (!contentId) return false
|
|
754
|
-
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp' && e.key !== 'Enter') return false
|
|
755
|
-
const root = document.getElementById(contentId)?.querySelector<HTMLElement>('[cmdk-root]')
|
|
756
|
-
if (!root) return false
|
|
757
|
-
e.preventDefault()
|
|
758
|
-
root.dispatchEvent(new KeyboardEvent('keydown', { key: e.key, bubbles: true, cancelable: true }))
|
|
759
|
-
return true
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
/**
|
|
763
|
-
* 2026-07-05 D4 補齊(APG combobox aria-activedescendant):追蹤 cmdk 目前 virtual-focus item
|
|
764
|
-
* 的 DOM id,供 trigger 端搜尋 input 綁 `aria-activedescendant` —— SR 才會在方向鍵導覽 /
|
|
765
|
-
* 打字過濾時播報 active option 名。機制:trigger 與 portal 內 cmdk 分屬不同 DOM 子樹,cmdk
|
|
766
|
-
* 只把 active id 綁在自己的 Command.Input / List(cmdk source:item 自帶 auto-generated id +
|
|
767
|
-
* `data-selected="true"` 標記 virtual focus)→ trigger 端用 MutationObserver 監聽 popover 容器
|
|
768
|
-
* (contentId = PopoverContent id)內 `data-selected` 屬性變化 + childList(打字過濾 re-render
|
|
769
|
-
* 換 item 節點),單一機制涵蓋全部更新路徑:開啟初始 auto-highlight / forwardKeyToListbox
|
|
770
|
-
* 方向鍵轉送 / 搜尋過濾後 cmdk 自動移 cursor / pointer hover。
|
|
771
|
-
* 關閉時清 undefined —— ARIA 要求 id 必指向存在於 DOM 的節點,不可留 stale id。
|
|
772
|
-
*/
|
|
773
|
-
export function useActiveDescendant(contentId: string | undefined, open: boolean): string | undefined {
|
|
774
|
-
const [activeId, setActiveId] = React.useState<string | undefined>(undefined)
|
|
775
|
-
React.useEffect(() => {
|
|
776
|
-
if (!open || !contentId) {
|
|
777
|
-
setActiveId(undefined)
|
|
778
|
-
return
|
|
779
|
-
}
|
|
780
|
-
// PopoverContent 與 trigger 同一個 React commit mount(open state 同批 render)→ effect 跑時已在 DOM
|
|
781
|
-
const container = document.getElementById(contentId)
|
|
782
|
-
if (!container) return
|
|
783
|
-
const read = () => {
|
|
784
|
-
setActiveId(container.querySelector<HTMLElement>('[cmdk-item][data-selected="true"]')?.id || undefined)
|
|
785
|
-
}
|
|
786
|
-
// 初始補讀:MutationObserver 只看「觀察開始後」的變化;cmdk 初始 auto-highlight(layout effect
|
|
787
|
-
// 排程)可能已 commit → rAF 讀當下狀態兜底,與 observer 互補、誰先到都不漏。
|
|
788
|
-
const raf = requestAnimationFrame(read)
|
|
789
|
-
const observer = new MutationObserver(read)
|
|
790
|
-
observer.observe(container, { subtree: true, childList: true, attributes: true, attributeFilter: ['data-selected'] })
|
|
791
|
-
return () => {
|
|
792
|
-
cancelAnimationFrame(raf)
|
|
793
|
-
observer.disconnect()
|
|
794
|
-
}
|
|
795
|
-
}, [open, contentId])
|
|
796
|
-
return activeId
|
|
797
|
-
}
|
|
760
|
+
// forwardKeyToListbox / useActiveDescendant 原樣搬到 select-menu-keyboard.ts(2026-09-25,與 B11 的 Tab 規則同住一個
|
|
761
|
+
// 鍵盤模組;本檔已近 800 行上限)。從這裡轉出,Select / Combobox 既有的 import 路徑不變。
|
|
762
|
+
export { forwardKeyToListbox, useActiveDescendant } from '@/design-system/components/SelectMenu/select-menu-keyboard'
|
|
798
763
|
|
|
799
764
|
export { SelectMenu }
|