@elabs-ai/components-ui 4.0.0
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/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/index.d.ts +4497 -0
- package/dist/index.js +15192 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cn.d.ts +10 -0
- package/dist/lib/cn.js +20 -0
- package/dist/lib/cn.js.map +1 -0
- package/package.json +106 -0
- package/src/blocks/sidebar-02/app-sidebar.tsx +160 -0
- package/src/blocks/sidebar-02/logo.tsx +18 -0
- package/src/blocks/sidebar-02/nav-main.tsx +13 -0
- package/src/blocks/sidebar-02/nav-notifications.tsx +7 -0
- package/src/blocks/sidebar-02/sidebar-02.stories.tsx +23 -0
- package/src/blocks/sidebar-02/team-switcher.tsx +7 -0
- package/src/blocks/sidebar-04/app-sidebar.tsx +236 -0
- package/src/blocks/sidebar-04/mail-context.tsx +29 -0
- package/src/blocks/sidebar-04/nav-user.tsx +6 -0
- package/src/blocks/sidebar-04/sidebar-04.stories.tsx +150 -0
- package/src/blocks/sidebar-05/app-sidebar.tsx +375 -0
- package/src/blocks/sidebar-05/sidebar-05.stories.tsx +18 -0
- package/src/blocks/sidebar-05/team-switcher.tsx +6 -0
- package/src/blocks-comparison-table.stories.tsx +165 -0
- package/src/components/accordion/accordion.stories.tsx +71 -0
- package/src/components/accordion/accordion.test.tsx +18 -0
- package/src/components/accordion/accordion.tsx +49 -0
- package/src/components/accordion/index.ts +1 -0
- package/src/components/advanced-group/advanced-group.stories.tsx +143 -0
- package/src/components/advanced-group/advanced-group.test.tsx +81 -0
- package/src/components/advanced-group/advanced-group.tsx +111 -0
- package/src/components/advanced-group/index.ts +2 -0
- package/src/components/alert/alert.stories.tsx +156 -0
- package/src/components/alert/alert.test.tsx +57 -0
- package/src/components/alert/alert.tsx +91 -0
- package/src/components/alert/index.ts +8 -0
- package/src/components/alert-dialog/alert-dialog.stories.tsx +73 -0
- package/src/components/alert-dialog/alert-dialog.tsx +104 -0
- package/src/components/alert-dialog/index.ts +12 -0
- package/src/components/app-shell/app-shell.stories.tsx +61 -0
- package/src/components/app-shell/app-shell.tsx +31 -0
- package/src/components/app-shell/index.ts +1 -0
- package/src/components/app-sidebar/app-sidebar.stories.tsx +121 -0
- package/src/components/app-sidebar/app-sidebar.test.tsx +55 -0
- package/src/components/app-sidebar/app-sidebar.tsx +53 -0
- package/src/components/app-sidebar/index.ts +1 -0
- package/src/components/aspect-ratio/aspect-ratio.stories.tsx +46 -0
- package/src/components/aspect-ratio/aspect-ratio.tsx +3 -0
- package/src/components/aspect-ratio/index.ts +1 -0
- package/src/components/attribution-panel/attribution-panel.stories.tsx +83 -0
- package/src/components/attribution-panel/attribution-panel.test.tsx +123 -0
- package/src/components/attribution-panel/attribution-panel.tsx +191 -0
- package/src/components/attribution-panel/attribution-types.ts +44 -0
- package/src/components/attribution-panel/attributions.generated.ts +1422 -0
- package/src/components/attribution-panel/index.ts +3 -0
- package/src/components/avatar/avatar.stories.tsx +45 -0
- package/src/components/avatar/avatar.test.tsx +13 -0
- package/src/components/avatar/avatar.tsx +46 -0
- package/src/components/avatar/index.ts +1 -0
- package/src/components/badge/badge.stories.tsx +44 -0
- package/src/components/badge/badge.test.tsx +10 -0
- package/src/components/badge/badge.tsx +28 -0
- package/src/components/badge/index.ts +1 -0
- package/src/components/bento-grid/bento-grid.stories.tsx +165 -0
- package/src/components/bento-grid/bento-grid.test.tsx +119 -0
- package/src/components/bento-grid/bento-grid.tsx +339 -0
- package/src/components/bento-grid/index.ts +9 -0
- package/src/components/bounded-number/bounded-number.stories.tsx +118 -0
- package/src/components/bounded-number/bounded-number.test.tsx +73 -0
- package/src/components/bounded-number/bounded-number.tsx +110 -0
- package/src/components/bounded-number/index.ts +1 -0
- package/src/components/breadcrumb/breadcrumb.stories.tsx +47 -0
- package/src/components/breadcrumb/breadcrumb.test.tsx +17 -0
- package/src/components/breadcrumb/breadcrumb.tsx +86 -0
- package/src/components/breadcrumb/index.ts +9 -0
- package/src/components/button/button.stories.tsx +70 -0
- package/src/components/button/button.test.tsx +50 -0
- package/src/components/button/button.tsx +63 -0
- package/src/components/button/index.ts +1 -0
- package/src/components/button-group/button-group.stories.tsx +45 -0
- package/src/components/button-group/button-group.tsx +70 -0
- package/src/components/button-group/index.ts +6 -0
- package/src/components/calendar/calendar.stories.tsx +95 -0
- package/src/components/calendar/calendar.test.tsx +140 -0
- package/src/components/calendar/calendar.tsx +70 -0
- package/src/components/calendar/index.ts +1 -0
- package/src/components/card/card.stories.tsx +438 -0
- package/src/components/card/card.test.tsx +153 -0
- package/src/components/card/card.tsx +332 -0
- package/src/components/card/index.ts +13 -0
- package/src/components/carousel/carousel.stories.tsx +102 -0
- package/src/components/carousel/carousel.tsx +225 -0
- package/src/components/carousel/index.ts +8 -0
- package/src/components/change-review/change-review.stories.tsx +288 -0
- package/src/components/change-review/change-review.test.tsx +261 -0
- package/src/components/change-review/change-review.tsx +637 -0
- package/src/components/change-review/index.ts +23 -0
- package/src/components/checkbox/checkbox.stories.tsx +119 -0
- package/src/components/checkbox/checkbox.test.tsx +40 -0
- package/src/components/checkbox/checkbox.tsx +34 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/collapsible/collapsible.stories.tsx +62 -0
- package/src/components/collapsible/collapsible.tsx +22 -0
- package/src/components/collapsible/index.ts +1 -0
- package/src/components/collapsible-panel/index.ts +8 -0
- package/src/components/collapsible-panel/use-collapsible-panel.test.tsx +93 -0
- package/src/components/collapsible-panel/use-collapsible-panel.ts +159 -0
- package/src/components/color-picker/color-picker.stories.tsx +93 -0
- package/src/components/color-picker/color-picker.test.tsx +79 -0
- package/src/components/color-picker/color-picker.tsx +341 -0
- package/src/components/color-picker/index.ts +1 -0
- package/src/components/combobox/combobox.stories.tsx +154 -0
- package/src/components/combobox/combobox.test.tsx +246 -0
- package/src/components/combobox/combobox.tsx +210 -0
- package/src/components/combobox/index.ts +1 -0
- package/src/components/command/command.stories.tsx +371 -0
- package/src/components/command/command.test.tsx +280 -0
- package/src/components/command/command.tsx +301 -0
- package/src/components/command/index.ts +13 -0
- package/src/components/confirm-dialog/confirm-dialog.stories.tsx +183 -0
- package/src/components/confirm-dialog/confirm-dialog.test.tsx +147 -0
- package/src/components/confirm-dialog/confirm-dialog.tsx +146 -0
- package/src/components/confirm-dialog/index.ts +2 -0
- package/src/components/context-menu/context-menu.stories.tsx +70 -0
- package/src/components/context-menu/context-menu.tsx +172 -0
- package/src/components/context-menu/index.ts +16 -0
- package/src/components/copyable-value/copyable-value.stories.tsx +80 -0
- package/src/components/copyable-value/copyable-value.test.tsx +128 -0
- package/src/components/copyable-value/copyable-value.tsx +101 -0
- package/src/components/copyable-value/index.ts +1 -0
- package/src/components/date-picker/date-picker.stories.tsx +42 -0
- package/src/components/date-picker/date-picker.tsx +49 -0
- package/src/components/date-picker/index.ts +1 -0
- package/src/components/date-range-picker/date-range-picker.stories.tsx +151 -0
- package/src/components/date-range-picker/date-range-picker.test.tsx +62 -0
- package/src/components/date-range-picker/date-range-picker.tsx +126 -0
- package/src/components/date-range-picker/index.ts +6 -0
- package/src/components/descriptions/descriptions.stories.tsx +43 -0
- package/src/components/descriptions/descriptions.test.tsx +41 -0
- package/src/components/descriptions/descriptions.tsx +95 -0
- package/src/components/descriptions/index.ts +6 -0
- package/src/components/dialog/dialog.stories.tsx +728 -0
- package/src/components/dialog/dialog.test.tsx +285 -0
- package/src/components/dialog/dialog.tsx +340 -0
- package/src/components/dialog/index.ts +18 -0
- package/src/components/dialog/use-dialog-dismiss-guard.test.tsx +80 -0
- package/src/components/dialog/use-dialog-dismiss-guard.ts +109 -0
- package/src/components/drawer/drawer.stories.tsx +76 -0
- package/src/components/drawer/drawer.tsx +85 -0
- package/src/components/drawer/index.ts +11 -0
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +176 -0
- package/src/components/dropdown-menu/dropdown-menu.test.tsx +109 -0
- package/src/components/dropdown-menu/dropdown-menu.tsx +179 -0
- package/src/components/dropdown-menu/index.ts +17 -0
- package/src/components/empty-state/empty-state.stories.tsx +54 -0
- package/src/components/empty-state/empty-state.test.tsx +11 -0
- package/src/components/empty-state/empty-state.tsx +30 -0
- package/src/components/empty-state/index.ts +1 -0
- package/src/components/error-state/error-state.stories.tsx +51 -0
- package/src/components/error-state/error-state.tsx +27 -0
- package/src/components/error-state/index.ts +1 -0
- package/src/components/expand-dialog/expand-dialog.stories.tsx +246 -0
- package/src/components/expand-dialog/expand-dialog.test.tsx +185 -0
- package/src/components/expand-dialog/expand-dialog.tsx +387 -0
- package/src/components/expand-dialog/index.ts +11 -0
- package/src/components/field-row/field-row.stories.tsx +114 -0
- package/src/components/field-row/field-row.test.tsx +188 -0
- package/src/components/field-row/field-row.tsx +115 -0
- package/src/components/field-row/index.ts +1 -0
- package/src/components/file-upload/file-upload.stories.tsx +183 -0
- package/src/components/file-upload/file-upload.test.tsx +169 -0
- package/src/components/file-upload/file-upload.tsx +459 -0
- package/src/components/file-upload/index.ts +13 -0
- package/src/components/form/form.stories.tsx +71 -0
- package/src/components/form/form.tsx +141 -0
- package/src/components/form/index.ts +10 -0
- package/src/components/hover-card/hover-card.stories.tsx +57 -0
- package/src/components/hover-card/hover-card.tsx +25 -0
- package/src/components/hover-card/index.ts +1 -0
- package/src/components/icon-button/icon-button.stories.tsx +108 -0
- package/src/components/icon-button/icon-button.test.tsx +93 -0
- package/src/components/icon-button/icon-button.tsx +139 -0
- package/src/components/icon-button/index.ts +1 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input/input.stories.tsx +44 -0
- package/src/components/input/input.test.tsx +19 -0
- package/src/components/input/input.tsx +26 -0
- package/src/components/input-group/index.ts +11 -0
- package/src/components/input-group/input-group.stories.tsx +87 -0
- package/src/components/input-group/input-group.test.tsx +48 -0
- package/src/components/input-group/input-group.tsx +170 -0
- package/src/components/input-otp/index.ts +1 -0
- package/src/components/input-otp/input-otp.stories.tsx +66 -0
- package/src/components/input-otp/input-otp.tsx +68 -0
- package/src/components/kbd/index.ts +1 -0
- package/src/components/kbd/kbd.stories.tsx +45 -0
- package/src/components/kbd/kbd.tsx +20 -0
- package/src/components/key-value-editor/index.ts +1 -0
- package/src/components/key-value-editor/key-value-editor.stories.tsx +130 -0
- package/src/components/key-value-editor/key-value-editor.test.tsx +231 -0
- package/src/components/key-value-editor/key-value-editor.tsx +263 -0
- package/src/components/label/index.ts +1 -0
- package/src/components/label/label.stories.tsx +35 -0
- package/src/components/label/label.tsx +19 -0
- package/src/components/link-preview/index.ts +2 -0
- package/src/components/link-preview/link-preview.stories.tsx +178 -0
- package/src/components/link-preview/link-preview.test.tsx +123 -0
- package/src/components/link-preview/link-preview.tsx +194 -0
- package/src/components/list-editor/index.ts +1 -0
- package/src/components/list-editor/list-editor.stories.tsx +119 -0
- package/src/components/list-editor/list-editor.test.tsx +96 -0
- package/src/components/list-editor/list-editor.tsx +192 -0
- package/src/components/loading-state/index.ts +1 -0
- package/src/components/loading-state/loading-state.stories.tsx +38 -0
- package/src/components/loading-state/loading-state.tsx +26 -0
- package/src/components/locale-provider/index.ts +8 -0
- package/src/components/locale-provider/locale-provider.stories.tsx +83 -0
- package/src/components/locale-provider/locale-provider.test.tsx +238 -0
- package/src/components/locale-provider/locale-provider.tsx +187 -0
- package/src/components/locale-provider/messages.ts +300 -0
- package/src/components/match-highlight/index.ts +7 -0
- package/src/components/match-highlight/match-highlight.stories.tsx +146 -0
- package/src/components/match-highlight/match-highlight.test.tsx +202 -0
- package/src/components/match-highlight/match-highlight.tsx +145 -0
- package/src/components/mention-input/index.ts +28 -0
- package/src/components/mention-input/mention-input-mirror.stories.tsx +171 -0
- package/src/components/mention-input/mention-input.stories.tsx +463 -0
- package/src/components/mention-input/mention-input.test.tsx +617 -0
- package/src/components/mention-input/mention-input.tsx +1102 -0
- package/src/components/mention-input/mention-value.test.ts +253 -0
- package/src/components/mention-input/mention-value.ts +193 -0
- package/src/components/menubar/index.ts +17 -0
- package/src/components/menubar/menubar.stories.tsx +67 -0
- package/src/components/menubar/menubar.tsx +198 -0
- package/src/components/metric-card/index.ts +1 -0
- package/src/components/metric-card/metric-card.stories.tsx +224 -0
- package/src/components/metric-card/metric-card.test.tsx +198 -0
- package/src/components/metric-card/metric-card.tsx +229 -0
- package/src/components/model-picker/index.ts +11 -0
- package/src/components/model-picker/model-picker-state.ts +85 -0
- package/src/components/model-picker/model-picker.stories.tsx +128 -0
- package/src/components/model-picker/model-picker.test.tsx +241 -0
- package/src/components/model-picker/model-picker.tsx +270 -0
- package/src/components/nav-main/index.ts +1 -0
- package/src/components/nav-main/nav-main.tsx +147 -0
- package/src/components/nav-notifications/index.ts +5 -0
- package/src/components/nav-notifications/nav-notifications.test.tsx +15 -0
- package/src/components/nav-notifications/nav-notifications.tsx +70 -0
- package/src/components/nav-user/index.ts +1 -0
- package/src/components/nav-user/nav-user.test.tsx +29 -0
- package/src/components/nav-user/nav-user.tsx +114 -0
- package/src/components/navigation-menu/index.ts +10 -0
- package/src/components/navigation-menu/navigation-menu.stories.tsx +92 -0
- package/src/components/navigation-menu/navigation-menu.tsx +99 -0
- package/src/components/number-input/index.ts +1 -0
- package/src/components/number-input/number-input.stories.tsx +120 -0
- package/src/components/number-input/number-input.test.tsx +96 -0
- package/src/components/number-input/number-input.tsx +219 -0
- package/src/components/page-shell/index.ts +1 -0
- package/src/components/page-shell/page-shell.stories.tsx +105 -0
- package/src/components/page-shell/page-shell.test.tsx +57 -0
- package/src/components/page-shell/page-shell.tsx +62 -0
- package/src/components/pagination/index.ts +9 -0
- package/src/components/pagination/pagination.stories.tsx +57 -0
- package/src/components/pagination/pagination.tsx +77 -0
- package/src/components/popover/index.ts +1 -0
- package/src/components/popover/popover.stories.tsx +28 -0
- package/src/components/popover/popover.test.tsx +16 -0
- package/src/components/popover/popover.tsx +29 -0
- package/src/components/progress/index.ts +1 -0
- package/src/components/progress/progress.stories.tsx +80 -0
- package/src/components/progress/progress.test.tsx +35 -0
- package/src/components/progress/progress.tsx +50 -0
- package/src/components/radio-group/index.ts +1 -0
- package/src/components/radio-group/radio-group.stories.tsx +75 -0
- package/src/components/radio-group/radio-group.test.tsx +16 -0
- package/src/components/radio-group/radio-group.tsx +33 -0
- package/src/components/rating/index.ts +1 -0
- package/src/components/rating/rating.stories.tsx +111 -0
- package/src/components/rating/rating.test.tsx +41 -0
- package/src/components/rating/rating.tsx +226 -0
- package/src/components/resizable/index.ts +1 -0
- package/src/components/resizable/resizable.stories.tsx +25 -0
- package/src/components/resizable/resizable.tsx +40 -0
- package/src/components/reveal/index.ts +7 -0
- package/src/components/reveal/reveal.stories.tsx +90 -0
- package/src/components/reveal/reveal.test.tsx +43 -0
- package/src/components/reveal/reveal.tsx +95 -0
- package/src/components/revision-timeline/index.ts +10 -0
- package/src/components/revision-timeline/revision-timeline.stories.tsx +785 -0
- package/src/components/revision-timeline/revision-timeline.test.tsx +509 -0
- package/src/components/revision-timeline/revision-timeline.tsx +1107 -0
- package/src/components/scroll-area/index.ts +1 -0
- package/src/components/scroll-area/scroll-area.stories.tsx +45 -0
- package/src/components/scroll-area/scroll-area.tsx +43 -0
- package/src/components/section-header/index.ts +1 -0
- package/src/components/section-header/section-header.stories.tsx +20 -0
- package/src/components/section-header/section-header.tsx +36 -0
- package/src/components/segmented-field/index.ts +5 -0
- package/src/components/segmented-field/segmented-field.stories.tsx +121 -0
- package/src/components/segmented-field/segmented-field.test.tsx +163 -0
- package/src/components/segmented-field/segmented-field.tsx +235 -0
- package/src/components/select/index.ts +10 -0
- package/src/components/select/select.stories.tsx +106 -0
- package/src/components/select/select.test.tsx +98 -0
- package/src/components/select/select.tsx +230 -0
- package/src/components/separator/index.ts +2 -0
- package/src/components/separator/separator.stories.tsx +87 -0
- package/src/components/separator/separator.tsx +51 -0
- package/src/components/sheet/index.ts +12 -0
- package/src/components/sheet/sheet.stories.tsx +65 -0
- package/src/components/sheet/sheet.tsx +109 -0
- package/src/components/sidebar/index.ts +27 -0
- package/src/components/sidebar/sidebar.stories.tsx +69 -0
- package/src/components/sidebar/sidebar.test.tsx +43 -0
- package/src/components/sidebar/sidebar.tsx +688 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/skeleton/skeleton.stories.tsx +28 -0
- package/src/components/skeleton/skeleton.tsx +15 -0
- package/src/components/slider/index.ts +1 -0
- package/src/components/slider/slider.stories.tsx +211 -0
- package/src/components/slider/slider.test.tsx +168 -0
- package/src/components/slider/slider.tsx +136 -0
- package/src/components/slider-number/index.ts +1 -0
- package/src/components/slider-number/slider-number.stories.tsx +123 -0
- package/src/components/slider-number/slider-number.test.tsx +165 -0
- package/src/components/slider-number/slider-number.tsx +191 -0
- package/src/components/sonner/index.ts +1 -0
- package/src/components/sonner/sonner.stories.tsx +124 -0
- package/src/components/sonner/sonner.test.tsx +76 -0
- package/src/components/sonner/sonner.tsx +101 -0
- package/src/components/spinner/index.ts +1 -0
- package/src/components/spinner/spinner.stories.tsx +27 -0
- package/src/components/spinner/spinner.tsx +19 -0
- package/src/components/split-panel/index.ts +1 -0
- package/src/components/split-panel/split-panel.stories.tsx +60 -0
- package/src/components/split-panel/split-panel.test.tsx +38 -0
- package/src/components/split-panel/split-panel.tsx +95 -0
- package/src/components/state-panel/index.ts +1 -0
- package/src/components/state-panel/state-panel.stories.tsx +80 -0
- package/src/components/state-panel/state-panel.test.tsx +64 -0
- package/src/components/state-panel/state-panel.tsx +167 -0
- package/src/components/status-badge/index.ts +18 -0
- package/src/components/status-badge/status-badge.stories.tsx +132 -0
- package/src/components/status-badge/status-badge.test.tsx +189 -0
- package/src/components/status-badge/status-badge.tsx +330 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.stories.tsx +58 -0
- package/src/components/switch/switch.test.tsx +12 -0
- package/src/components/switch/switch.tsx +29 -0
- package/src/components/table/index.ts +10 -0
- package/src/components/table/table.stories.tsx +53 -0
- package/src/components/table/table.test.tsx +17 -0
- package/src/components/table/table.tsx +106 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/tabs/tabs.stories.tsx +267 -0
- package/src/components/tabs/tabs.test.tsx +94 -0
- package/src/components/tabs/tabs.tsx +170 -0
- package/src/components/tag-input/index.ts +1 -0
- package/src/components/tag-input/tag-input.stories.tsx +120 -0
- package/src/components/tag-input/tag-input.test.tsx +97 -0
- package/src/components/tag-input/tag-input.tsx +253 -0
- package/src/components/team-switcher/index.ts +1 -0
- package/src/components/team-switcher/team-switcher.test.tsx +39 -0
- package/src/components/team-switcher/team-switcher.tsx +100 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/textarea.stories.tsx +42 -0
- package/src/components/textarea/textarea.tsx +24 -0
- package/src/components/theme-switcher/index.ts +2 -0
- package/src/components/theme-switcher/theme-switcher.stories.tsx +146 -0
- package/src/components/theme-switcher/theme-switcher.test.tsx +253 -0
- package/src/components/theme-switcher/theme-switcher.tsx +280 -0
- package/src/components/theme-switcher/use-theme-transition.ts +62 -0
- package/src/components/timeline/index.ts +13 -0
- package/src/components/timeline/timeline.stories.tsx +99 -0
- package/src/components/timeline/timeline.test.tsx +196 -0
- package/src/components/timeline/timeline.tsx +188 -0
- package/src/components/toggle/index.ts +1 -0
- package/src/components/toggle/toggle.stories.tsx +116 -0
- package/src/components/toggle/toggle.test.tsx +119 -0
- package/src/components/toggle/toggle.tsx +64 -0
- package/src/components/toggle-group/index.ts +1 -0
- package/src/components/toggle-group/toggle-group.stories.tsx +110 -0
- package/src/components/toggle-group/toggle-group.tsx +59 -0
- package/src/components/toolbar/index.ts +14 -0
- package/src/components/toolbar/toolbar.stories.tsx +126 -0
- package/src/components/toolbar/toolbar.test.tsx +73 -0
- package/src/components/toolbar/toolbar.tsx +177 -0
- package/src/components/tooltip/index.ts +1 -0
- package/src/components/tooltip/tooltip.stories.tsx +67 -0
- package/src/components/tooltip/tooltip.tsx +27 -0
- package/src/components/top-nav/index.ts +1 -0
- package/src/components/top-nav/top-nav.stories.tsx +18 -0
- package/src/components/top-nav/top-nav.tsx +34 -0
- package/src/components/transfer/index.ts +2 -0
- package/src/components/transfer/transfer.stories.tsx +162 -0
- package/src/components/transfer/transfer.test.tsx +177 -0
- package/src/components/transfer/transfer.tsx +481 -0
- package/src/components/tree/index.ts +6 -0
- package/src/components/tree/tree.stories.tsx +618 -0
- package/src/components/tree/tree.test.tsx +699 -0
- package/src/components/tree/tree.tsx +1149 -0
- package/src/components/tree/use-tree-keyboard.ts +314 -0
- package/src/components/tree-select/index.ts +1 -0
- package/src/components/tree-select/tree-select.stories.tsx +127 -0
- package/src/components/tree-select/tree-select.test.tsx +40 -0
- package/src/components/tree-select/tree-select.tsx +137 -0
- package/src/components/typography/index.ts +31 -0
- package/src/components/typography/prose.test.tsx +48 -0
- package/src/components/typography/prose.tsx +194 -0
- package/src/components/typography/typography.stories.tsx +244 -0
- package/src/components/typography/typography.test.tsx +140 -0
- package/src/components/typography/typography.tsx +126 -0
- package/src/components/view-toolbar/index.ts +10 -0
- package/src/components/view-toolbar/view-toolbar.stories.tsx +342 -0
- package/src/components/view-toolbar/view-toolbar.test.tsx +262 -0
- package/src/components/view-toolbar/view-toolbar.tsx +320 -0
- package/src/components/virtual-select/index.ts +2 -0
- package/src/components/virtual-select/use-virtual-listbox.ts +132 -0
- package/src/components/virtual-select/virtual-select.stories.tsx +203 -0
- package/src/components/virtual-select/virtual-select.test.tsx +106 -0
- package/src/components/virtual-select/virtual-select.tsx +361 -0
- package/src/components/wizard/index.ts +12 -0
- package/src/components/wizard/wizard.stories.tsx +148 -0
- package/src/components/wizard/wizard.test.tsx +67 -0
- package/src/components/wizard/wizard.tsx +354 -0
- package/src/decoration.stories.tsx +236 -0
- package/src/index.ts +174 -0
- package/src/lib/cn.test.ts +39 -0
- package/src/lib/cn.ts +31 -0
- package/src/lib/compact-number.ts +61 -0
- package/src/lib/document-address.test.ts +75 -0
- package/src/lib/document-address.ts +265 -0
- package/src/lib/download.ts +93 -0
- package/src/lib/file-icon.test.ts +45 -0
- package/src/lib/file-icon.ts +58 -0
- package/src/lib/file-kind.test.ts +98 -0
- package/src/lib/file-kind.ts +303 -0
- package/src/lib/file-source.test.ts +217 -0
- package/src/lib/file-source.ts +269 -0
- package/src/lib/merge-refs.test.ts +37 -0
- package/src/lib/merge-refs.ts +17 -0
- package/src/lib/streamdown-translations.ts +99 -0
- package/src/lib/use-copy-to-clipboard.ts +75 -0
- package/src/lib/use-mobile.ts +16 -0
- package/src/motion.stories.tsx +266 -0
- package/src/taste-profile.test.tsx +252 -0
- package/src/templates-object-detail-hub.stories.tsx +466 -0
- package/src/templates-screen-states.stories.tsx +306 -0
- package/src/templates-settings.stories.tsx +245 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import { createRef, useState } from "react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import {
|
|
6
|
+
MentionInput,
|
|
7
|
+
MentionInputContent,
|
|
8
|
+
MentionInputEmpty,
|
|
9
|
+
MentionInputItem,
|
|
10
|
+
MentionInputList,
|
|
11
|
+
MentionInputTextarea,
|
|
12
|
+
type MentionOption,
|
|
13
|
+
type MentionValue,
|
|
14
|
+
} from "./mention-input";
|
|
15
|
+
|
|
16
|
+
const ROSTER: MentionOption[] = [
|
|
17
|
+
{ id: "u1", label: "Ada Lovelace", keywords: ["math"] },
|
|
18
|
+
{ id: "u2", label: "Grace Hopper" },
|
|
19
|
+
{ id: "u3", label: "Alan Turing", disabled: true },
|
|
20
|
+
{ id: "u4", label: "Katherine Johnson" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/** The full compound arrangement, exactly as a consumer composes it. */
|
|
24
|
+
function Harness({
|
|
25
|
+
options = ROSTER,
|
|
26
|
+
...props
|
|
27
|
+
}: Partial<React.ComponentProps<typeof MentionInput>> & { options?: MentionOption[] }) {
|
|
28
|
+
return (
|
|
29
|
+
<MentionInput options={options} {...props}>
|
|
30
|
+
<MentionInputTextarea aria-label="Comment" />
|
|
31
|
+
<MentionInputContent>
|
|
32
|
+
<MentionInputList>
|
|
33
|
+
{(option) => (
|
|
34
|
+
<MentionInputItem key={option.id} option={option}>
|
|
35
|
+
{option.label}
|
|
36
|
+
</MentionInputItem>
|
|
37
|
+
)}
|
|
38
|
+
</MentionInputList>
|
|
39
|
+
<MentionInputEmpty />
|
|
40
|
+
</MentionInputContent>
|
|
41
|
+
</MentionInput>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const field = () => screen.getByRole("textbox", { name: "Comment" }) as HTMLTextAreaElement;
|
|
46
|
+
|
|
47
|
+
/** Put the caret at `index` and let React observe the selection change. */
|
|
48
|
+
async function setCaret(el: HTMLTextAreaElement, index: number) {
|
|
49
|
+
const user = userEvent.setup();
|
|
50
|
+
await user.click(el);
|
|
51
|
+
el.setSelectionRange(index, index);
|
|
52
|
+
// `setSelectionRange` fires no event; `select` is what React listens for.
|
|
53
|
+
el.dispatchEvent(new Event("select", { bubbles: true }));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// T3 — trigger boundary
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
describe("T3 trigger boundary", () => {
|
|
61
|
+
it("opens the popup for a trigger at index 0", async () => {
|
|
62
|
+
const user = userEvent.setup();
|
|
63
|
+
render(<Harness />);
|
|
64
|
+
await user.type(field(), "@");
|
|
65
|
+
expect(field()).toHaveAttribute("data-state", "open");
|
|
66
|
+
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("opens the popup for a trigger after a space", async () => {
|
|
70
|
+
const user = userEvent.setup();
|
|
71
|
+
render(<Harness />);
|
|
72
|
+
await user.type(field(), "hello @");
|
|
73
|
+
expect(field()).toHaveAttribute("data-state", "open");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does NOT open for a trigger mid-word (an email address)", async () => {
|
|
77
|
+
const user = userEvent.setup();
|
|
78
|
+
render(<Harness />);
|
|
79
|
+
await user.type(field(), "ada@");
|
|
80
|
+
expect(field()).toHaveAttribute("data-state", "closed");
|
|
81
|
+
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("closes again once the query runs into whitespace", async () => {
|
|
85
|
+
const user = userEvent.setup();
|
|
86
|
+
render(<Harness />);
|
|
87
|
+
await user.type(field(), "@ada");
|
|
88
|
+
expect(field()).toHaveAttribute("data-state", "open");
|
|
89
|
+
await user.type(field(), " ");
|
|
90
|
+
expect(field()).toHaveAttribute("data-state", "closed");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("filters the roster as the query grows", async () => {
|
|
94
|
+
const user = userEvent.setup();
|
|
95
|
+
render(<Harness />);
|
|
96
|
+
await user.type(field(), "@grace");
|
|
97
|
+
const options = screen.getAllByRole("option");
|
|
98
|
+
expect(options).toHaveLength(1);
|
|
99
|
+
expect(options[0]).toHaveTextContent("Grace Hopper");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("shows the empty state when nothing matches", async () => {
|
|
103
|
+
const user = userEvent.setup();
|
|
104
|
+
render(<Harness />);
|
|
105
|
+
await user.type(field(), "@zzzz");
|
|
106
|
+
expect(screen.queryAllByRole("option")).toHaveLength(0);
|
|
107
|
+
expect(screen.getByText("No results.")).toBeInTheDocument();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// T4 — chip atomicity (the headline acceptance criterion)
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
describe("T4 chip atomicity", () => {
|
|
116
|
+
it("Backspace with the caret right after a mention removes the WHOLE token", async () => {
|
|
117
|
+
const user = userEvent.setup();
|
|
118
|
+
const onValueChange = vi.fn();
|
|
119
|
+
render(<Harness onValueChange={onValueChange} />);
|
|
120
|
+
|
|
121
|
+
await user.type(field(), "@ada");
|
|
122
|
+
await user.keyboard("{Enter}");
|
|
123
|
+
expect(field().value).toBe("@Ada Lovelace ");
|
|
124
|
+
|
|
125
|
+
// Caret currently sits after the trailing space; step back onto the token end.
|
|
126
|
+
await user.keyboard("{Backspace}"); // eats the trailing space
|
|
127
|
+
expect(field().value).toBe("@Ada Lovelace");
|
|
128
|
+
|
|
129
|
+
await user.keyboard("{Backspace}"); // must eat the entire token, not one char
|
|
130
|
+
expect(field().value).toBe("");
|
|
131
|
+
expect(onValueChange).toHaveBeenLastCalledWith({ text: "", mentions: [] });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("Delete with the caret right before a mention removes the WHOLE token", async () => {
|
|
135
|
+
const user = userEvent.setup();
|
|
136
|
+
render(<Harness />);
|
|
137
|
+
await user.type(field(), "@ada");
|
|
138
|
+
await user.keyboard("{Enter}");
|
|
139
|
+
expect(field().value).toBe("@Ada Lovelace ");
|
|
140
|
+
|
|
141
|
+
await setCaret(field(), 0);
|
|
142
|
+
await user.keyboard("{Delete}");
|
|
143
|
+
expect(field().value).toBe(" ");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("ArrowLeft from the end of a mention lands on its START boundary, never inside", async () => {
|
|
147
|
+
const user = userEvent.setup();
|
|
148
|
+
render(<Harness />);
|
|
149
|
+
await user.type(field(), "@ada");
|
|
150
|
+
await user.keyboard("{Enter}");
|
|
151
|
+
// "@Ada Lovelace " — token is [0, 13), caret is at 14.
|
|
152
|
+
await setCaret(field(), 13);
|
|
153
|
+
await user.keyboard("{ArrowLeft}");
|
|
154
|
+
expect(field().selectionStart).toBe(0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("ArrowRight from the start of a mention lands on its END boundary", async () => {
|
|
158
|
+
const user = userEvent.setup();
|
|
159
|
+
render(<Harness />);
|
|
160
|
+
await user.type(field(), "@ada");
|
|
161
|
+
await user.keyboard("{Enter}");
|
|
162
|
+
await setCaret(field(), 0);
|
|
163
|
+
await user.keyboard("{ArrowRight}");
|
|
164
|
+
expect(field().selectionStart).toBe(13);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("editing INSIDE a mention drops it from the value", async () => {
|
|
168
|
+
const user = userEvent.setup();
|
|
169
|
+
const onValueChange = vi.fn();
|
|
170
|
+
render(<Harness onValueChange={onValueChange} />);
|
|
171
|
+
await user.type(field(), "@ada");
|
|
172
|
+
await user.keyboard("{Enter}");
|
|
173
|
+
expect(onValueChange).toHaveBeenLastCalledWith({
|
|
174
|
+
text: "@Ada Lovelace ",
|
|
175
|
+
mentions: [{ id: "u1", label: "Ada Lovelace", start: 0 }],
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Type a character into the middle of the label.
|
|
179
|
+
await setCaret(field(), 6);
|
|
180
|
+
await user.keyboard("x");
|
|
181
|
+
expect(onValueChange).toHaveBeenLastCalledWith({ text: "@Ada Lxovelace ", mentions: [] });
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// T5 — keyboard model
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
describe("T5 keyboard", () => {
|
|
190
|
+
it("ArrowDown / ArrowUp move aria-activedescendant, wrapping and skipping disabled", async () => {
|
|
191
|
+
const user = userEvent.setup();
|
|
192
|
+
render(<Harness />);
|
|
193
|
+
await user.type(field(), "@");
|
|
194
|
+
|
|
195
|
+
const ids = screen.getAllByRole("option").map((o) => o.id);
|
|
196
|
+
expect(ids).toHaveLength(4);
|
|
197
|
+
expect(field()).toHaveAttribute("aria-activedescendant", ids[0]);
|
|
198
|
+
|
|
199
|
+
await user.keyboard("{ArrowDown}");
|
|
200
|
+
expect(field()).toHaveAttribute("aria-activedescendant", ids[1]);
|
|
201
|
+
|
|
202
|
+
// ids[2] is `disabled` — ArrowDown must skip straight past it.
|
|
203
|
+
await user.keyboard("{ArrowDown}");
|
|
204
|
+
expect(field()).toHaveAttribute("aria-activedescendant", ids[3]);
|
|
205
|
+
|
|
206
|
+
// …and wrap back to the first.
|
|
207
|
+
await user.keyboard("{ArrowDown}");
|
|
208
|
+
expect(field()).toHaveAttribute("aria-activedescendant", ids[0]);
|
|
209
|
+
|
|
210
|
+
await user.keyboard("{ArrowUp}");
|
|
211
|
+
expect(field()).toHaveAttribute("aria-activedescendant", ids[3]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("Home / End jump to the first / last enabled option", async () => {
|
|
215
|
+
const user = userEvent.setup();
|
|
216
|
+
render(<Harness />);
|
|
217
|
+
await user.type(field(), "@");
|
|
218
|
+
const ids = screen.getAllByRole("option").map((o) => o.id);
|
|
219
|
+
|
|
220
|
+
await user.keyboard("{End}");
|
|
221
|
+
expect(field()).toHaveAttribute("aria-activedescendant", ids[3]);
|
|
222
|
+
await user.keyboard("{Home}");
|
|
223
|
+
expect(field()).toHaveAttribute("aria-activedescendant", ids[0]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("Enter inserts the highlighted option", async () => {
|
|
227
|
+
const user = userEvent.setup();
|
|
228
|
+
render(<Harness />);
|
|
229
|
+
await user.type(field(), "@");
|
|
230
|
+
await user.keyboard("{ArrowDown}{Enter}");
|
|
231
|
+
expect(field().value).toBe("@Grace Hopper ");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("Tab inserts the highlighted option", async () => {
|
|
235
|
+
const user = userEvent.setup();
|
|
236
|
+
render(<Harness />);
|
|
237
|
+
await user.type(field(), "@ada");
|
|
238
|
+
await user.keyboard("{Tab}");
|
|
239
|
+
expect(field().value).toBe("@Ada Lovelace ");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("a disabled option cannot be inserted, and Enter falls through to the host", async () => {
|
|
243
|
+
const user = userEvent.setup();
|
|
244
|
+
render(<Harness />);
|
|
245
|
+
await user.type(field(), "@alan");
|
|
246
|
+
expect(screen.getAllByRole("option")).toHaveLength(1);
|
|
247
|
+
// The only match is disabled, so nothing is highlighted…
|
|
248
|
+
expect(field()).not.toHaveAttribute("aria-activedescendant");
|
|
249
|
+
|
|
250
|
+
await user.keyboard("{Enter}");
|
|
251
|
+
// …the option is not inserted, AND the key is NOT intercepted — it does
|
|
252
|
+
// what it would have done anyway (a newline here; a submit in a composer).
|
|
253
|
+
// That fall-through is what lets `PromptInput` keep Enter when the roster
|
|
254
|
+
// has nothing to offer.
|
|
255
|
+
expect(field().value).not.toContain("Alan Turing");
|
|
256
|
+
expect(field().value).toBe("@alan\n");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("Escape closes the popup WITHOUT clearing the textarea", async () => {
|
|
260
|
+
const user = userEvent.setup();
|
|
261
|
+
render(<Harness />);
|
|
262
|
+
await user.type(field(), "@ada");
|
|
263
|
+
expect(field()).toHaveAttribute("data-state", "open");
|
|
264
|
+
|
|
265
|
+
await user.keyboard("{Escape}");
|
|
266
|
+
expect(field()).toHaveAttribute("data-state", "closed");
|
|
267
|
+
expect(field().value).toBe("@ada");
|
|
268
|
+
expect(field()).toHaveFocus();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("leaves Enter alone when the popup is closed", async () => {
|
|
272
|
+
const user = userEvent.setup();
|
|
273
|
+
render(<Harness />);
|
|
274
|
+
await user.type(field(), "hello{Enter}world");
|
|
275
|
+
expect(field().value).toBe("hello\nworld");
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// T6 — the ARIA invariant (the screen-reader acceptance criterion)
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
describe("T6 ARIA invariant", () => {
|
|
284
|
+
/**
|
|
285
|
+
* `aria-activedescendant` must always name the element that is actually
|
|
286
|
+
* carrying `aria-selected="true"` in the SAME commit — the generalised
|
|
287
|
+
* lesson from cmdk's `selectedItemId` hole (see command.tsx).
|
|
288
|
+
*/
|
|
289
|
+
function expectActiveDescendantInvariant() {
|
|
290
|
+
const el = field();
|
|
291
|
+
const active = el.getAttribute("aria-activedescendant");
|
|
292
|
+
const selected = screen
|
|
293
|
+
.queryAllByRole("option")
|
|
294
|
+
.filter((o) => o.getAttribute("aria-selected") === "true");
|
|
295
|
+
expect(selected).toHaveLength(active === null ? 0 : 1);
|
|
296
|
+
if (active !== null) {
|
|
297
|
+
expect(selected[0]!.id).toBe(active);
|
|
298
|
+
expect(document.getElementById(active)).not.toBeNull();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
it("keeps the field a spec-valid textbox and wires the autocomplete properties", async () => {
|
|
303
|
+
const user = userEvent.setup();
|
|
304
|
+
render(<Harness />);
|
|
305
|
+
const el = field();
|
|
306
|
+
expect(el.tagName).toBe("TEXTAREA");
|
|
307
|
+
|
|
308
|
+
// `<textarea>` permits NO role override (ARIA in HTML), and `aria-expanded`
|
|
309
|
+
// is not in `textbox`'s supported set — axe flags the first as
|
|
310
|
+
// `aria-allowed-role` and the second as `aria-allowed-attr` (critical).
|
|
311
|
+
// Everything the field DOES carry is in `textbox`'s supported set.
|
|
312
|
+
expect(el).not.toHaveAttribute("role");
|
|
313
|
+
expect(el).not.toHaveAttribute("aria-expanded");
|
|
314
|
+
expect(el).toHaveAttribute("aria-autocomplete", "list");
|
|
315
|
+
expect(el).toHaveAttribute("aria-haspopup", "listbox");
|
|
316
|
+
expect(el).toHaveAttribute("data-state", "closed");
|
|
317
|
+
// Nothing to control while the portalled listbox is unmounted.
|
|
318
|
+
expect(el).not.toHaveAttribute("aria-controls");
|
|
319
|
+
|
|
320
|
+
await user.type(el, "@");
|
|
321
|
+
expect(field()).toHaveAttribute("data-state", "open");
|
|
322
|
+
expect(field().getAttribute("aria-controls")).toBe(screen.getByRole("listbox").id);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("holds the invariant on open, after a filter keystroke, and after an arrow move", async () => {
|
|
326
|
+
const user = userEvent.setup();
|
|
327
|
+
render(<Harness />);
|
|
328
|
+
|
|
329
|
+
await user.type(field(), "@");
|
|
330
|
+
expectActiveDescendantInvariant(); // on open
|
|
331
|
+
|
|
332
|
+
await user.type(field(), "a");
|
|
333
|
+
expectActiveDescendantInvariant(); // after a filter keystroke
|
|
334
|
+
|
|
335
|
+
await user.type(field(), "d");
|
|
336
|
+
expectActiveDescendantInvariant();
|
|
337
|
+
|
|
338
|
+
await user.keyboard("{ArrowDown}");
|
|
339
|
+
expectActiveDescendantInvariant(); // after an arrow move
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("drops aria-activedescendant entirely when nothing is highlighted", async () => {
|
|
343
|
+
const user = userEvent.setup();
|
|
344
|
+
render(<Harness />);
|
|
345
|
+
await user.type(field(), "@zzzz");
|
|
346
|
+
expect(field()).not.toHaveAttribute("aria-activedescendant");
|
|
347
|
+
expectActiveDescendantInvariant();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("never leaves a dangling aria-activedescendant when the list shrinks", async () => {
|
|
351
|
+
const user = userEvent.setup();
|
|
352
|
+
render(<Harness />);
|
|
353
|
+
await user.type(field(), "@");
|
|
354
|
+
await user.keyboard("{End}"); // highlight index 3
|
|
355
|
+
await user.type(field(), "grace"); // list collapses to one option
|
|
356
|
+
expectActiveDescendantInvariant();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("names the listbox and keeps focus on the textarea", async () => {
|
|
360
|
+
const user = userEvent.setup();
|
|
361
|
+
render(<Harness />);
|
|
362
|
+
await user.type(field(), "@");
|
|
363
|
+
expect(screen.getByRole("listbox", { name: "Mention suggestions" })).toBeInTheDocument();
|
|
364
|
+
expect(field()).toHaveFocus();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
// T7 — controlled/uncontrolled parity + ref
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
describe("T7 controlled / uncontrolled parity", () => {
|
|
373
|
+
function Controlled({ onValueChange }: { onValueChange: (v: MentionValue) => void }) {
|
|
374
|
+
const [value, setValue] = useState<MentionValue>({ text: "", mentions: [] });
|
|
375
|
+
return (
|
|
376
|
+
<Harness
|
|
377
|
+
value={value}
|
|
378
|
+
onValueChange={(v) => {
|
|
379
|
+
setValue(v);
|
|
380
|
+
onValueChange(v);
|
|
381
|
+
}}
|
|
382
|
+
/>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
it("produces an identical MentionValue in both modes", async () => {
|
|
387
|
+
const uncontrolled = vi.fn();
|
|
388
|
+
const { unmount } = render(<Harness onValueChange={uncontrolled} />);
|
|
389
|
+
const user = userEvent.setup();
|
|
390
|
+
await user.type(field(), "hi @ada");
|
|
391
|
+
await user.keyboard("{Enter}");
|
|
392
|
+
const uncontrolledValue = uncontrolled.mock.calls.at(-1)?.[0];
|
|
393
|
+
unmount();
|
|
394
|
+
|
|
395
|
+
const controlled = vi.fn();
|
|
396
|
+
render(<Controlled onValueChange={controlled} />);
|
|
397
|
+
await user.type(field(), "hi @ada");
|
|
398
|
+
await user.keyboard("{Enter}");
|
|
399
|
+
const controlledValue = controlled.mock.calls.at(-1)?.[0];
|
|
400
|
+
|
|
401
|
+
expect(uncontrolledValue).toEqual({
|
|
402
|
+
text: "hi @Ada Lovelace ",
|
|
403
|
+
mentions: [{ id: "u1", label: "Ada Lovelace", start: 3 }],
|
|
404
|
+
});
|
|
405
|
+
expect(controlledValue).toEqual(uncontrolledValue);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("a controlled value the parent refuses to update never mutates the field", async () => {
|
|
409
|
+
const user = userEvent.setup();
|
|
410
|
+
const onValueChange = vi.fn();
|
|
411
|
+
render(<Harness value={{ text: "frozen", mentions: [] }} onValueChange={onValueChange} />);
|
|
412
|
+
await user.type(field(), "abc");
|
|
413
|
+
expect(onValueChange).toHaveBeenCalled();
|
|
414
|
+
expect(field().value).toBe("frozen");
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("seeds an uncontrolled field from defaultValue", () => {
|
|
418
|
+
render(<Harness defaultValue={{ text: "hello", mentions: [] }} />);
|
|
419
|
+
expect(field().value).toBe("hello");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("forwards a ref to the real HTMLTextAreaElement", () => {
|
|
423
|
+
const ref = createRef<HTMLTextAreaElement>();
|
|
424
|
+
render(
|
|
425
|
+
<MentionInput options={ROSTER}>
|
|
426
|
+
<MentionInputTextarea ref={ref} aria-label="Comment" />
|
|
427
|
+
</MentionInput>,
|
|
428
|
+
);
|
|
429
|
+
expect(ref.current).toBeInstanceOf(HTMLTextAreaElement);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("forwards a ref to the real HTMLTextAreaElement THROUGH asChild", () => {
|
|
433
|
+
const ref = createRef<HTMLTextAreaElement>();
|
|
434
|
+
render(
|
|
435
|
+
<MentionInput options={ROSTER}>
|
|
436
|
+
<MentionInputTextarea ref={ref} asChild>
|
|
437
|
+
<textarea aria-label="Comment" name="message" />
|
|
438
|
+
</MentionInputTextarea>
|
|
439
|
+
</MentionInput>,
|
|
440
|
+
);
|
|
441
|
+
expect(ref.current).toBeInstanceOf(HTMLTextAreaElement);
|
|
442
|
+
// The child's own props survive the Slot merge…
|
|
443
|
+
expect(ref.current).toHaveAttribute("name", "message");
|
|
444
|
+
// …and so do the slot's, which is what makes the composer binding work.
|
|
445
|
+
expect(ref.current).toHaveAttribute("aria-autocomplete", "list");
|
|
446
|
+
expect(ref.current).toHaveAttribute("data-slot", "mention-input-textarea");
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// T8 — the Slot handler-order contract (the `onKeyDownCapture` requirement)
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
describe("T8 Slot handler-order contract", () => {
|
|
455
|
+
/**
|
|
456
|
+
* `asChild` around a child that binds `onKeyDown` on a HOST ELEMENT — the
|
|
457
|
+
* shape Radix `Slot`'s child-first merge actually breaks.
|
|
458
|
+
*
|
|
459
|
+
* The child mirrors `PromptInputTextarea`'s contract: run, then bail if an
|
|
460
|
+
* outer handler already claimed the key. With the interception bound as
|
|
461
|
+
* `onKeyDownCapture` it runs in the capture pass, so `defaultPrevented` is
|
|
462
|
+
* already true and the host bails. Bound as `onKeyDown`, `Slot` composes
|
|
463
|
+
* child-then-slot, the host runs FIRST with `defaultPrevented === false`, and
|
|
464
|
+
* it submits — which is the regression this test exists to catch.
|
|
465
|
+
*/
|
|
466
|
+
function HostBindingHarness({ onHostEnter }: { onHostEnter: () => void }) {
|
|
467
|
+
return (
|
|
468
|
+
<MentionInput options={ROSTER}>
|
|
469
|
+
<MentionInputTextarea asChild>
|
|
470
|
+
<textarea
|
|
471
|
+
aria-label="Comment"
|
|
472
|
+
onKeyDown={(event) => {
|
|
473
|
+
if (event.defaultPrevented) return;
|
|
474
|
+
if (event.key === "Enter") onHostEnter();
|
|
475
|
+
}}
|
|
476
|
+
/>
|
|
477
|
+
</MentionInputTextarea>
|
|
478
|
+
<MentionInputContent>
|
|
479
|
+
<MentionInputList>
|
|
480
|
+
{(option) => <MentionInputItem key={option.id} option={option} />}
|
|
481
|
+
</MentionInputList>
|
|
482
|
+
</MentionInputContent>
|
|
483
|
+
</MentionInput>
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
it("intercepts BEFORE the child's own onKeyDown when the roster is open", async () => {
|
|
488
|
+
const user = userEvent.setup();
|
|
489
|
+
const onHostEnter = vi.fn();
|
|
490
|
+
render(<HostBindingHarness onHostEnter={onHostEnter} />);
|
|
491
|
+
|
|
492
|
+
await user.type(field(), "@ada");
|
|
493
|
+
expect(field()).toHaveAttribute("data-state", "open");
|
|
494
|
+
|
|
495
|
+
await user.keyboard("{Enter}");
|
|
496
|
+
|
|
497
|
+
expect(field().value).toBe("@Ada Lovelace ");
|
|
498
|
+
// THE discriminating assertion: bound as `onKeyDown`, the child would have
|
|
499
|
+
// run first and fired.
|
|
500
|
+
expect(onHostEnter).not.toHaveBeenCalled();
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("leaves the child's onKeyDown alone when the roster is closed", async () => {
|
|
504
|
+
const user = userEvent.setup();
|
|
505
|
+
const onHostEnter = vi.fn();
|
|
506
|
+
render(<HostBindingHarness onHostEnter={onHostEnter} />);
|
|
507
|
+
|
|
508
|
+
await user.type(field(), "hello");
|
|
509
|
+
expect(field()).toHaveAttribute("data-state", "closed");
|
|
510
|
+
|
|
511
|
+
await user.keyboard("{Enter}");
|
|
512
|
+
expect(onHostEnter).toHaveBeenCalledTimes(1);
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// ---------------------------------------------------------------------------
|
|
517
|
+
// Consumer prop composition (props spread must not clobber internal wiring)
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
|
|
520
|
+
describe("consumer prop composition", () => {
|
|
521
|
+
it("keeps tracking the value when the consumer ALSO passes onChange", async () => {
|
|
522
|
+
const user = userEvent.setup();
|
|
523
|
+
const onChange = vi.fn();
|
|
524
|
+
const onValueChange = vi.fn();
|
|
525
|
+
render(
|
|
526
|
+
<MentionInput options={ROSTER} onValueChange={onValueChange}>
|
|
527
|
+
<MentionInputTextarea aria-label="Comment" onChange={onChange} />
|
|
528
|
+
</MentionInput>,
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
await user.type(field(), "@ada");
|
|
532
|
+
|
|
533
|
+
// Spreading `...props` after the handlers used to replace `onValueInput`
|
|
534
|
+
// outright, leaving the provider-controlled field permanently empty.
|
|
535
|
+
expect(field().value).toBe("@ada");
|
|
536
|
+
expect(onValueChange).toHaveBeenLastCalledWith({ text: "@ada", mentions: [] });
|
|
537
|
+
// …and the consumer's handler still runs.
|
|
538
|
+
expect(onChange).toHaveBeenCalled();
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it("composes the other handlers rather than replacing them", async () => {
|
|
542
|
+
const user = userEvent.setup();
|
|
543
|
+
const onKeyDownCapture = vi.fn();
|
|
544
|
+
const onClick = vi.fn();
|
|
545
|
+
const onBlur = vi.fn();
|
|
546
|
+
render(
|
|
547
|
+
<MentionInput options={ROSTER}>
|
|
548
|
+
<MentionInputTextarea
|
|
549
|
+
aria-label="Comment"
|
|
550
|
+
onKeyDownCapture={onKeyDownCapture}
|
|
551
|
+
onClick={onClick}
|
|
552
|
+
onBlur={onBlur}
|
|
553
|
+
/>
|
|
554
|
+
<MentionInputContent>
|
|
555
|
+
<MentionInputList>
|
|
556
|
+
{(option) => <MentionInputItem key={option.id} option={option} />}
|
|
557
|
+
</MentionInputList>
|
|
558
|
+
</MentionInputContent>
|
|
559
|
+
</MentionInput>,
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
await user.click(field());
|
|
563
|
+
expect(onClick).toHaveBeenCalled();
|
|
564
|
+
|
|
565
|
+
// The internal keyboard model still runs (the roster opens and Enter
|
|
566
|
+
// inserts) AND the consumer's capture handler is called.
|
|
567
|
+
await user.keyboard("@ada");
|
|
568
|
+
expect(field()).toHaveAttribute("data-state", "open");
|
|
569
|
+
await user.keyboard("{Enter}");
|
|
570
|
+
expect(field().value).toBe("@Ada Lovelace ");
|
|
571
|
+
expect(onKeyDownCapture).toHaveBeenCalled();
|
|
572
|
+
|
|
573
|
+
await user.tab();
|
|
574
|
+
expect(onBlur).toHaveBeenCalled();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("lets consumer NON-handler props win, so <Label htmlFor> keeps working", () => {
|
|
578
|
+
render(
|
|
579
|
+
<MentionInput options={ROSTER}>
|
|
580
|
+
<MentionInputTextarea
|
|
581
|
+
id="my-field"
|
|
582
|
+
name="comment"
|
|
583
|
+
placeholder="Say something…"
|
|
584
|
+
aria-label="Comment"
|
|
585
|
+
aria-describedby="my-hint"
|
|
586
|
+
data-testid="field"
|
|
587
|
+
/>
|
|
588
|
+
</MentionInput>,
|
|
589
|
+
);
|
|
590
|
+
const el = screen.getByTestId("field");
|
|
591
|
+
expect(el).toHaveAttribute("id", "my-field");
|
|
592
|
+
expect(el).toHaveAttribute("name", "comment");
|
|
593
|
+
expect(el).toHaveAttribute("placeholder", "Say something…");
|
|
594
|
+
expect(el).toHaveAttribute("aria-describedby", "my-hint");
|
|
595
|
+
// The component's own wiring is still present alongside them.
|
|
596
|
+
expect(el).toHaveAttribute("aria-autocomplete", "list");
|
|
597
|
+
expect(el).toHaveAttribute("data-slot", "mention-input-textarea");
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
602
|
+
// Async rosters / query reporting
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
|
|
605
|
+
describe("onQueryChange", () => {
|
|
606
|
+
it("reports the query while open and null when the popup closes", async () => {
|
|
607
|
+
const user = userEvent.setup();
|
|
608
|
+
const onQueryChange = vi.fn();
|
|
609
|
+
render(<Harness onQueryChange={onQueryChange} filter={() => true} />);
|
|
610
|
+
|
|
611
|
+
await user.type(field(), "@ad");
|
|
612
|
+
expect(onQueryChange).toHaveBeenLastCalledWith("ad");
|
|
613
|
+
|
|
614
|
+
await user.keyboard("{Escape}");
|
|
615
|
+
expect(onQueryChange).toHaveBeenLastCalledWith(null);
|
|
616
|
+
});
|
|
617
|
+
});
|