@eifi1/ui-kit 0.4.1 → 0.5.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 +32 -0
- package/README.md +498 -57
- package/dist/chart.d.ts +10 -0
- package/dist/chart.js +10 -0
- package/dist/chart.js.map +1 -0
- package/dist/components/account-settings.d.ts +11 -0
- package/dist/components/account-settings.js +19 -12
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/alert-banner.d.ts +10 -6
- package/dist/components/alert-banner.js +20 -16
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +30 -4
- package/dist/components/amount-input.js +36 -14
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/calculator.d.ts +24 -7
- package/dist/components/calculator.js +37 -20
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.d.ts +156 -0
- package/dist/components/chart-zoom.js +262 -0
- package/dist/components/chart-zoom.js.map +1 -0
- package/dist/components/chart.d.ts +28 -3
- package/dist/components/chart.js +71 -32
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox.d.ts +51 -0
- package/dist/components/checkbox.js +123 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/chip.d.ts +128 -0
- package/dist/components/chip.js +357 -0
- package/dist/components/chip.js.map +1 -0
- package/dist/components/choice-parts.d.ts +24 -0
- package/dist/components/choice-parts.js +18 -0
- package/dist/components/choice-parts.js.map +1 -0
- package/dist/components/combobox-core.d.ts +52 -9
- package/dist/components/combobox-core.js +89 -19
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +43 -33
- package/dist/components/combobox.js +409 -340
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.d.ts +48 -8
- package/dist/components/currency-select.js +107 -65
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +15 -3
- package/dist/components/data-table-filter-popover.js +23 -17
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-filters.js +3 -2
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +53 -2
- package/dist/components/data-table-labels.js +31 -2
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table-pagination.js +16 -10
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-sort.js +1 -0
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +201 -85
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +25 -9
- package/dist/components/date-picker.js +232 -94
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dropdown.d.ts +74 -19
- package/dist/components/dropdown.js +72 -22
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +5 -4
- package/dist/components/entity-combobox.js +107 -76
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +36 -0
- package/dist/components/facing-pair.js +40 -0
- package/dist/components/facing-pair.js.map +1 -0
- package/dist/components/field-sync.d.ts +164 -0
- package/dist/components/field-sync.js +264 -0
- package/dist/components/field-sync.js.map +1 -0
- package/dist/components/file-dropzone.d.ts +21 -7
- package/dist/components/file-dropzone.js +28 -7
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/full-bleed-dialog.d.ts +37 -26
- package/dist/components/full-bleed-dialog.js +20 -5
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/grouped-picker.d.ts +21 -10
- package/dist/components/grouped-picker.js +66 -52
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.d.ts +20 -4
- package/dist/components/hover-menu.js +15 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +102 -14
- package/dist/components/mini-calendar.js +256 -70
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +9 -3
- package/dist/components/modal.js +34 -45
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +79 -0
- package/dist/components/month-picker.js +342 -0
- package/dist/components/month-picker.js.map +1 -0
- package/dist/components/multi-entity-combobox.d.ts +7 -6
- package/dist/components/multi-entity-combobox.js +112 -72
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +13 -11
- package/dist/components/multi-select.js +161 -84
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +92 -0
- package/dist/components/number-field.js +111 -0
- package/dist/components/number-field.js.map +1 -0
- package/dist/components/number-input.d.ts +21 -2
- package/dist/components/number-input.js +2 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +25 -4
- package/dist/components/numpad-sheet.js +30 -13
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-contents.d.ts +90 -0
- package/dist/components/page-contents.js +170 -0
- package/dist/components/page-contents.js.map +1 -0
- package/dist/components/password-strength.d.ts +106 -0
- package/dist/components/password-strength.js +132 -0
- package/dist/components/password-strength.js.map +1 -0
- package/dist/components/picker-sheet.d.ts +23 -15
- package/dist/components/picker-sheet.js +34 -14
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.d.ts +62 -4
- package/dist/components/popover.js +52 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/search-field.d.ts +25 -7
- package/dist/components/search-field.js +7 -5
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +14 -0
- package/dist/components/series-chart-labels.js +11 -0
- package/dist/components/series-chart-labels.js.map +1 -0
- package/dist/components/series-chart.d.ts +207 -0
- package/dist/components/series-chart.js +223 -0
- package/dist/components/series-chart.js.map +1 -0
- package/dist/components/settings-fields.d.ts +14 -4
- package/dist/components/settings-fields.js +19 -8
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/signature-pad.d.ts +119 -0
- package/dist/components/signature-pad.js +405 -0
- package/dist/components/signature-pad.js.map +1 -0
- package/dist/components/slider.d.ts +80 -0
- package/dist/components/slider.js +155 -0
- package/dist/components/slider.js.map +1 -0
- package/dist/components/sparkline.d.ts +93 -0
- package/dist/components/sparkline.js +205 -0
- package/dist/components/sparkline.js.map +1 -0
- package/dist/components/stat-tile.d.ts +151 -0
- package/dist/components/stat-tile.js +236 -0
- package/dist/components/stat-tile.js.map +1 -0
- package/dist/components/swipeable-row.d.ts +25 -2
- package/dist/components/swipeable-row.js +31 -0
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/switch.d.ts +60 -0
- package/dist/components/switch.js +81 -0
- package/dist/components/switch.js.map +1 -0
- package/dist/components/time-input.d.ts +58 -0
- package/dist/components/time-input.js +50 -0
- package/dist/components/time-input.js.map +1 -0
- package/dist/components/toggle-group.d.ts +18 -3
- package/dist/components/toggle-group.js +10 -6
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +115 -0
- package/dist/components/toggle-legend.js +114 -0
- package/dist/components/toggle-legend.js.map +1 -0
- package/dist/components/tooltip.d.ts +40 -11
- package/dist/components/tooltip.js +77 -20
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/treemap.d.ts +132 -0
- package/dist/components/treemap.js +179 -0
- package/dist/components/treemap.js.map +1 -0
- package/dist/components/ui.d.ts +159 -46
- package/dist/components/ui.js +220 -115
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-mobile-reveal.js +1 -0
- package/dist/components/use-mobile-reveal.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/components/use-table-state.js +38 -17
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.d.ts +5 -3
- package/dist/components/user-avatar.js +4 -2
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/components/wizard-stepper.js +3 -2
- package/dist/components/wizard-stepper.js.map +1 -1
- package/dist/{data-table-filters-aXyf0Xub.d.ts → data-table-filters-DM1auUfH.d.ts} +27 -5
- package/dist/data-table.d.ts +7 -0
- package/dist/data-table.js +12 -0
- package/dist/data-table.js.map +1 -0
- package/dist/feedback/feedback-attachment.js +12 -11
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.js +2 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +33 -7
- package/dist/feedback/feedback-inbox.js +49 -37
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback.d.ts +5 -0
- package/dist/feedback.js +5 -0
- package/dist/feedback.js.map +1 -0
- package/dist/hooks/use-anchored-panel.js +1 -0
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-anchored-rect.js +1 -0
- package/dist/hooks/use-anchored-rect.js.map +1 -1
- package/dist/hooks/use-announce.d.ts +46 -0
- package/dist/hooks/use-announce.js +37 -0
- package/dist/hooks/use-announce.js.map +1 -0
- package/dist/hooks/use-body-scroll-lock.js +1 -0
- package/dist/hooks/use-body-scroll-lock.js.map +1 -1
- package/dist/hooks/use-close-transition.js +1 -0
- package/dist/hooks/use-close-transition.js.map +1 -1
- package/dist/hooks/use-dismiss.js +1 -0
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +16 -0
- package/dist/hooks/use-focus-trap.js +67 -0
- package/dist/hooks/use-focus-trap.js.map +1 -0
- package/dist/hooks/use-media-query.js +1 -0
- package/dist/hooks/use-media-query.js.map +1 -1
- package/dist/hooks/use-overlay-history.js +27 -1
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/hooks/use-row-swipe.js +1 -0
- package/dist/hooks/use-row-swipe.js.map +1 -1
- package/dist/i18n/defaults.d.ts +31 -0
- package/dist/i18n/defaults.js +63 -0
- package/dist/i18n/defaults.js.map +1 -0
- package/dist/i18n/kit-labels.d.ts +250 -0
- package/dist/i18n/kit-labels.js +184 -0
- package/dist/i18n/kit-labels.js.map +1 -0
- package/dist/index.d.ts +47 -22
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/calc.js +1 -0
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/cn.js +1 -0
- package/dist/lib/cn.js.map +1 -1
- package/dist/lib/dates.js +1 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/logger.js +1 -0
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/safe-storage.js +1 -0
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/search/command-palette.d.ts +11 -1
- package/dist/search/command-palette.js +21 -15
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search.d.ts +2 -0
- package/dist/search.js +3 -0
- package/dist/search.js.map +1 -0
- package/dist/shell/app-shell.d.ts +33 -4
- package/dist/shell/app-shell.js +275 -78
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/option-switcher-menu.js +3 -2
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +6 -0
- package/dist/shell/role-switcher.js +7 -4
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/top-bar.d.ts +15 -7
- package/dist/shell/top-bar.js +3 -5
- package/dist/shell/top-bar.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +3 -2
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +5 -2
- package/dist/shell/topbar-controls.js +20 -15
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell.d.ts +13 -0
- package/dist/shell.js +8 -0
- package/dist/shell.js.map +1 -0
- package/dist/theme/chart-palette.d.ts +19 -12
- package/dist/theme/chart-palette.js +24 -30
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/theme/color.d.ts +78 -0
- package/dist/theme/color.js +148 -0
- package/dist/theme/color.js.map +1 -0
- package/dist/theme/palette-derive.d.ts +140 -0
- package/dist/theme/palette-derive.js +376 -0
- package/dist/theme/palette-derive.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +14 -1
- package/dist/theme/palette-presets.js +231 -3
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js +1 -0
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/theme-store.js +1 -0
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour.d.ts +2 -1
- package/dist/tour/tour.js +15 -9
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour.d.ts +2 -0
- package/dist/tour.js +3 -0
- package/dist/tour.js.map +1 -0
- package/dist/wizard/stepper-nav.js +10 -8
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +18 -2
- package/dist/wizard/types.js +4 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +12 -4
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.js +1 -0
- package/dist/wizard/validation.js.map +1 -1
- package/dist/wizard/wizard-context.js +1 -0
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.js +1 -0
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard/wizard-summary.js +5 -3
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +8 -0
- package/dist/wizard.js +9 -0
- package/dist/wizard.js.map +1 -0
- package/package.json +66 -13
- package/src/chart.ts +22 -0
- package/src/components/account-settings.tsx +275 -0
- package/src/components/alert-banner.tsx +75 -0
- package/src/components/amount-input.tsx +471 -0
- package/src/components/calculator.tsx +252 -0
- package/src/components/chart-zoom.tsx +499 -0
- package/src/components/chart.tsx +363 -0
- package/src/components/checkbox.tsx +222 -0
- package/src/components/chip.tsx +570 -0
- package/src/components/choice-parts.ts +33 -0
- package/src/components/combobox-core.tsx +540 -0
- package/src/components/combobox.tsx +1026 -0
- package/src/components/currency-select.tsx +269 -0
- package/src/components/data-table-filter-popover.tsx +252 -0
- package/src/components/data-table-filters.ts +176 -0
- package/src/components/data-table-labels.ts +190 -0
- package/src/components/data-table-pagination.tsx +126 -0
- package/src/components/data-table-sort.ts +70 -0
- package/src/components/data-table.tsx +1740 -0
- package/src/components/date-picker.tsx +631 -0
- package/src/components/dropdown.tsx +341 -0
- package/src/components/entity-combobox.tsx +215 -0
- package/src/components/facing-pair.ts +86 -0
- package/src/components/field-sync.tsx +472 -0
- package/src/components/file-dropzone.tsx +181 -0
- package/src/components/full-bleed-dialog.tsx +179 -0
- package/src/components/grouped-picker.tsx +157 -0
- package/src/components/hover-menu.tsx +183 -0
- package/src/components/mini-calendar.tsx +554 -0
- package/src/components/modal.tsx +287 -0
- package/src/components/month-picker.tsx +549 -0
- package/src/components/multi-entity-combobox.tsx +226 -0
- package/src/components/multi-select.tsx +303 -0
- package/src/components/number-field.tsx +225 -0
- package/src/components/number-input.tsx +211 -0
- package/src/components/numpad-sheet.tsx +264 -0
- package/src/components/page-contents.tsx +289 -0
- package/src/components/password-strength.tsx +258 -0
- package/src/components/picker-sheet.tsx +251 -0
- package/src/components/popover.tsx +195 -0
- package/src/components/search-field.tsx +146 -0
- package/src/components/series-chart-labels.ts +26 -0
- package/src/components/series-chart.tsx +481 -0
- package/src/components/settings-fields.tsx +68 -0
- package/src/components/signature-pad.tsx +630 -0
- package/src/components/slider.tsx +313 -0
- package/src/components/sparkline.tsx +329 -0
- package/src/components/stat-tile.tsx +448 -0
- package/src/components/swipeable-row.tsx +271 -0
- package/src/components/switch.tsx +170 -0
- package/src/components/time-input.tsx +107 -0
- package/src/components/toggle-group.tsx +153 -0
- package/src/components/toggle-legend.tsx +229 -0
- package/src/components/tooltip.tsx +477 -0
- package/src/components/treemap.tsx +390 -0
- package/src/components/ui.tsx +1284 -0
- package/src/components/use-mobile-reveal.ts +54 -0
- package/src/components/use-table-state.ts +295 -0
- package/src/components/user-avatar.tsx +61 -0
- package/src/components/wizard-stepper.tsx +37 -0
- package/src/data-table.ts +19 -0
- package/src/feedback/feedback-attachment.tsx +246 -0
- package/src/feedback/feedback-dialog.tsx +209 -0
- package/src/feedback/feedback-inbox.tsx +615 -0
- package/src/feedback.ts +8 -0
- package/src/hooks/use-anchored-panel.ts +169 -0
- package/src/hooks/use-anchored-rect.ts +78 -0
- package/src/hooks/use-announce.ts +85 -0
- package/src/hooks/use-body-scroll-lock.ts +94 -0
- package/src/hooks/use-close-transition.ts +99 -0
- package/src/hooks/use-dismiss.ts +77 -0
- package/src/hooks/use-focus-trap.ts +160 -0
- package/src/hooks/use-media-query.ts +23 -0
- package/src/hooks/use-overlay-history.ts +345 -0
- package/src/hooks/use-row-swipe.ts +212 -0
- package/src/i18n/defaults.ts +68 -0
- package/src/i18n/kit-labels.tsx +447 -0
- package/src/index.ts +169 -0
- package/src/lib/calc.ts +309 -0
- package/src/lib/cn.ts +5 -0
- package/src/lib/dates.ts +187 -0
- package/src/lib/logger.ts +73 -0
- package/src/lib/safe-storage.ts +37 -0
- package/src/search/command-palette.tsx +342 -0
- package/src/search.ts +3 -0
- package/src/shell/app-shell.tsx +772 -0
- package/src/shell/option-switcher-menu.tsx +72 -0
- package/src/shell/role-switcher.tsx +46 -0
- package/src/shell/top-bar.tsx +39 -0
- package/src/shell/topbar-action-menu.tsx +91 -0
- package/src/shell/topbar-controls.tsx +229 -0
- package/src/shell.ts +10 -0
- package/src/theme/chart-palette.ts +145 -0
- package/src/theme/color.ts +266 -0
- package/src/theme/palette-derive.ts +681 -0
- package/src/theme/palette-presets.ts +760 -0
- package/src/theme/palette-store.ts +94 -0
- package/src/theme/theme-store.ts +145 -0
- package/src/tour/tour.tsx +586 -0
- package/src/tour.ts +3 -0
- package/src/vite-env.d.ts +26 -0
- package/src/wizard/stepper-nav.tsx +221 -0
- package/src/wizard/types.ts +178 -0
- package/src/wizard/use-wizard.ts +345 -0
- package/src/wizard/validation.ts +37 -0
- package/src/wizard/wizard-context.tsx +29 -0
- package/src/wizard/wizard-step.tsx +14 -0
- package/src/wizard/wizard-summary.tsx +69 -0
- package/src/wizard.ts +14 -0
- package/tokens.css +222 -20
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useId, useRef, useState } from "react";
|
|
4
|
+
import { X } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { FIELD_INVALID } from "./ui.js";
|
|
7
|
+
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
8
|
+
const TONE = {
|
|
9
|
+
neutral: {
|
|
10
|
+
idle: "border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-secondary)]",
|
|
11
|
+
selected: "border-[var(--border-strong)] bg-[var(--bg-active)] text-[var(--text-primary)]"
|
|
12
|
+
},
|
|
13
|
+
brand: {
|
|
14
|
+
idle: "border-[var(--border)] bg-[var(--brand-bg)] text-[var(--brand-muted)]",
|
|
15
|
+
selected: "border-[var(--brand)] bg-[var(--brand)] text-[var(--brand-contrast)]"
|
|
16
|
+
},
|
|
17
|
+
danger: {
|
|
18
|
+
idle: "border-[var(--danger-border)] bg-[var(--danger-bg)] text-[var(--danger)]",
|
|
19
|
+
selected: "border-[var(--danger)] bg-[var(--danger)] text-[var(--danger-contrast)]"
|
|
20
|
+
},
|
|
21
|
+
warning: {
|
|
22
|
+
idle: "border-[var(--warning-border)] bg-[var(--warning-bg)] text-[var(--warning)]",
|
|
23
|
+
selected: "border-[var(--warning)] bg-[var(--warning-bg)] text-[var(--warning)]"
|
|
24
|
+
},
|
|
25
|
+
success: {
|
|
26
|
+
idle: "border-[var(--success-border)] bg-[var(--success-bg)] text-[var(--success)]",
|
|
27
|
+
selected: "border-[var(--success)] bg-[var(--success-bg)] text-[var(--success)]"
|
|
28
|
+
},
|
|
29
|
+
info: {
|
|
30
|
+
idle: "border-[var(--info-border)] bg-[var(--info-bg)] text-[var(--info)]",
|
|
31
|
+
selected: "border-[var(--info)] bg-[var(--info-bg)] text-[var(--info)]"
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const SIZE = {
|
|
35
|
+
sm: { body: "gap-1 px-2 py-0.5 text-xs", icon: "size-3", remove: "size-3" },
|
|
36
|
+
md: { body: "gap-1.5 px-2.5 py-1 text-sm", icon: "size-3.5", remove: "size-3.5" }
|
|
37
|
+
};
|
|
38
|
+
const CHIP_BASE = "inline-flex max-w-full items-center rounded-full border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-surface)]";
|
|
39
|
+
const Chip = forwardRef(function Chip2({
|
|
40
|
+
children,
|
|
41
|
+
tone = "neutral",
|
|
42
|
+
size = "md",
|
|
43
|
+
icon: Icon,
|
|
44
|
+
selected = false,
|
|
45
|
+
href,
|
|
46
|
+
onClick,
|
|
47
|
+
onRemove,
|
|
48
|
+
removeLabel,
|
|
49
|
+
disabled = false,
|
|
50
|
+
className,
|
|
51
|
+
tabIndex,
|
|
52
|
+
role,
|
|
53
|
+
onKeyDown,
|
|
54
|
+
onFocus,
|
|
55
|
+
...rest
|
|
56
|
+
}, ref) {
|
|
57
|
+
const common = useKitLabels("common", DEFAULT_COMMON_LABELS, { remove: removeLabel });
|
|
58
|
+
const s = SIZE[size];
|
|
59
|
+
const palette = TONE[tone];
|
|
60
|
+
const interactive = !!href || !!onClick;
|
|
61
|
+
const look = cn(
|
|
62
|
+
CHIP_BASE,
|
|
63
|
+
s.body,
|
|
64
|
+
selected ? palette.selected : palette.idle,
|
|
65
|
+
interactive && !disabled && "cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110",
|
|
66
|
+
disabled && "cursor-default opacity-50",
|
|
67
|
+
className
|
|
68
|
+
);
|
|
69
|
+
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
70
|
+
Icon && /* @__PURE__ */ jsx(Icon, { className: cn(s.icon, "shrink-0"), "aria-hidden": true }),
|
|
71
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children })
|
|
72
|
+
] });
|
|
73
|
+
const remove = onRemove ? /* @__PURE__ */ jsx(
|
|
74
|
+
"button",
|
|
75
|
+
{
|
|
76
|
+
type: "button",
|
|
77
|
+
"aria-label": typeof children === "string" && children ? common.fieldValue(common.remove, children) : common.remove,
|
|
78
|
+
onClick: (e) => {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
onRemove();
|
|
82
|
+
},
|
|
83
|
+
disabled,
|
|
84
|
+
className: cn(
|
|
85
|
+
"-mr-0.5 ml-0.5 shrink-0 rounded-full p-0.5 transition-colors",
|
|
86
|
+
"hover:bg-[var(--bg-active)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
|
|
87
|
+
disabled && "pointer-events-none"
|
|
88
|
+
),
|
|
89
|
+
children: /* @__PURE__ */ jsx(X, { className: s.remove, "aria-hidden": true })
|
|
90
|
+
}
|
|
91
|
+
) : null;
|
|
92
|
+
if (href && !disabled) {
|
|
93
|
+
const link = /* @__PURE__ */ jsxs(
|
|
94
|
+
"a",
|
|
95
|
+
{
|
|
96
|
+
ref,
|
|
97
|
+
href,
|
|
98
|
+
"aria-current": selected ? "true" : void 0,
|
|
99
|
+
className: cn(look, remove && "pr-1.5"),
|
|
100
|
+
...rest,
|
|
101
|
+
children: [
|
|
102
|
+
body,
|
|
103
|
+
!remove && null
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
return remove ? /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center", children: [
|
|
108
|
+
link,
|
|
109
|
+
remove
|
|
110
|
+
] }) : link;
|
|
111
|
+
}
|
|
112
|
+
if (onClick) {
|
|
113
|
+
const button = /* @__PURE__ */ jsx(
|
|
114
|
+
"button",
|
|
115
|
+
{
|
|
116
|
+
ref,
|
|
117
|
+
type: "button",
|
|
118
|
+
onClick,
|
|
119
|
+
disabled,
|
|
120
|
+
"aria-pressed": selected,
|
|
121
|
+
className: cn(look, remove && "pr-1.5"),
|
|
122
|
+
...rest,
|
|
123
|
+
children: body
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
return remove ? /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center", children: [
|
|
127
|
+
button,
|
|
128
|
+
remove
|
|
129
|
+
] }) : button;
|
|
130
|
+
}
|
|
131
|
+
return /* @__PURE__ */ jsxs(
|
|
132
|
+
"span",
|
|
133
|
+
{
|
|
134
|
+
ref,
|
|
135
|
+
className: look,
|
|
136
|
+
tabIndex,
|
|
137
|
+
role,
|
|
138
|
+
onKeyDown,
|
|
139
|
+
onFocus,
|
|
140
|
+
...rest,
|
|
141
|
+
children: [
|
|
142
|
+
body,
|
|
143
|
+
remove
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
const DEFAULT_CHIP_INPUT_LABELS = {
|
|
149
|
+
added: (v) => `${v} added`,
|
|
150
|
+
removed: (v) => `${v} removed`,
|
|
151
|
+
remove: "Remove",
|
|
152
|
+
atLimit: (max) => `Limit of ${max} reached`,
|
|
153
|
+
duplicate: (v) => `${v} is already in the list`
|
|
154
|
+
};
|
|
155
|
+
function resolveChipInputLabels(labels) {
|
|
156
|
+
return labels ? { ...DEFAULT_CHIP_INPUT_LABELS, ...labels } : DEFAULT_CHIP_INPUT_LABELS;
|
|
157
|
+
}
|
|
158
|
+
function ChipInput({
|
|
159
|
+
value,
|
|
160
|
+
onChange,
|
|
161
|
+
placeholder,
|
|
162
|
+
label,
|
|
163
|
+
ariaLabel,
|
|
164
|
+
validate,
|
|
165
|
+
separators = [","],
|
|
166
|
+
allowDuplicates = false,
|
|
167
|
+
max,
|
|
168
|
+
tone = "neutral",
|
|
169
|
+
size = "md",
|
|
170
|
+
disabled = false,
|
|
171
|
+
invalid = false,
|
|
172
|
+
error,
|
|
173
|
+
labels,
|
|
174
|
+
className,
|
|
175
|
+
"aria-label": ariaLabelAttr,
|
|
176
|
+
...rest
|
|
177
|
+
}) {
|
|
178
|
+
const text = useKitLabels("chipInput", DEFAULT_CHIP_INPUT_LABELS, labels);
|
|
179
|
+
const locale = useKitLocale();
|
|
180
|
+
const [draft, setDraft] = useState("");
|
|
181
|
+
const [message, setMessage] = useState("");
|
|
182
|
+
const [focusedChip, setFocusedChip] = useState(null);
|
|
183
|
+
const inputRef = useRef(null);
|
|
184
|
+
const chipRefs = useRef([]);
|
|
185
|
+
const id = useId();
|
|
186
|
+
const listId = `${id}-list`;
|
|
187
|
+
const errorId = `${id}-error`;
|
|
188
|
+
const isInvalid = invalid || !!error;
|
|
189
|
+
const focusChip = (index) => {
|
|
190
|
+
setFocusedChip(index);
|
|
191
|
+
if (index === null) inputRef.current?.focus();
|
|
192
|
+
else chipRefs.current[index]?.focus();
|
|
193
|
+
};
|
|
194
|
+
const commitMany = (raws) => {
|
|
195
|
+
const next = [...value];
|
|
196
|
+
const added = [];
|
|
197
|
+
let rejection = null;
|
|
198
|
+
for (const raw of raws) {
|
|
199
|
+
const candidate = raw.trim();
|
|
200
|
+
if (!candidate) continue;
|
|
201
|
+
if (max !== void 0 && next.length >= max) {
|
|
202
|
+
rejection ??= text.atLimit(max);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
if (!allowDuplicates && next.includes(candidate)) {
|
|
206
|
+
rejection ??= text.duplicate(candidate);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const invalidReason = validate?.(candidate);
|
|
210
|
+
if (invalidReason) {
|
|
211
|
+
rejection ??= invalidReason;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
next.push(candidate);
|
|
215
|
+
added.push(candidate);
|
|
216
|
+
}
|
|
217
|
+
if (added.length) onChange(next);
|
|
218
|
+
const addedList = new Intl.ListFormat(locale, { type: "conjunction", style: "long" });
|
|
219
|
+
setMessage(rejection ?? (added.length ? text.added(addedList.format(added)) : ""));
|
|
220
|
+
return { added, rejection };
|
|
221
|
+
};
|
|
222
|
+
const commit = (raw) => commitMany([raw]).added.length > 0;
|
|
223
|
+
const removeAt = (index) => {
|
|
224
|
+
const removed = value[index];
|
|
225
|
+
onChange(value.filter((_, i) => i !== index));
|
|
226
|
+
setMessage(text.removed(removed));
|
|
227
|
+
const next = index >= value.length - 1 ? null : index;
|
|
228
|
+
requestAnimationFrame(() => focusChip(value.length - 1 === 0 ? null : next));
|
|
229
|
+
};
|
|
230
|
+
const onInputKeyDown = (e) => {
|
|
231
|
+
if (e.key === "Enter") {
|
|
232
|
+
if (draft.trim()) {
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
if (commit(draft)) setDraft("");
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (separators.includes(e.key)) {
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
if (commit(draft)) setDraft("");
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (e.key === "Escape" && draft) {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
setDraft("");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (e.key === "Backspace" && !draft && value.length) {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
focusChip(value.length - 1);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const onChipKeyDown = (e, index) => {
|
|
254
|
+
if (e.key === "Backspace" || e.key === "Delete") {
|
|
255
|
+
e.preventDefault();
|
|
256
|
+
removeAt(index);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (e.key === "ArrowLeft" && index > 0) {
|
|
260
|
+
e.preventDefault();
|
|
261
|
+
focusChip(index - 1);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (e.key === "ArrowRight") {
|
|
265
|
+
e.preventDefault();
|
|
266
|
+
focusChip(index === value.length - 1 ? null : index + 1);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (e.key === "Escape") {
|
|
270
|
+
e.preventDefault();
|
|
271
|
+
focusChip(null);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
return (
|
|
275
|
+
// The rest lands on the OUTER box — the thing a `data-tour` step or a test id
|
|
276
|
+
// wants to point at — while the accessible name below goes on the `<input>`,
|
|
277
|
+
// because that is what a screen-reader user actually lands in. Spreading the two
|
|
278
|
+
// to the same element would have named a decorative wrapper and left the field
|
|
279
|
+
// anonymous.
|
|
280
|
+
/* @__PURE__ */ jsxs("div", { ...rest, className: cn("space-y-1", className), children: [
|
|
281
|
+
label && /* @__PURE__ */ jsx("label", { htmlFor: `${id}-input`, className: "block text-sm text-[var(--text-secondary)]", children: label }),
|
|
282
|
+
/* @__PURE__ */ jsxs(
|
|
283
|
+
"div",
|
|
284
|
+
{
|
|
285
|
+
className: cn(
|
|
286
|
+
"relative flex flex-wrap items-center gap-1.5 rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1.5 shadow-sm",
|
|
287
|
+
"focus-within:border-[var(--brand)] focus-within:ring-1 focus-within:ring-[var(--brand)]",
|
|
288
|
+
disabled && "cursor-default bg-[var(--bg-surface-2)]",
|
|
289
|
+
isInvalid && FIELD_INVALID
|
|
290
|
+
),
|
|
291
|
+
onClick: () => !disabled && inputRef.current?.focus(),
|
|
292
|
+
children: [
|
|
293
|
+
/* @__PURE__ */ jsx("ul", { id: listId, role: "list", className: "contents", children: value.map((v, i) => /* @__PURE__ */ jsx("li", { role: "presentation", className: "contents", children: /* @__PURE__ */ jsx(
|
|
294
|
+
Chip,
|
|
295
|
+
{
|
|
296
|
+
ref: (el) => {
|
|
297
|
+
chipRefs.current[i] = el;
|
|
298
|
+
},
|
|
299
|
+
tone,
|
|
300
|
+
size,
|
|
301
|
+
disabled,
|
|
302
|
+
removeLabel: text.remove,
|
|
303
|
+
onRemove: disabled ? void 0 : () => removeAt(i),
|
|
304
|
+
tabIndex: focusedChip === i ? 0 : -1,
|
|
305
|
+
role: "listitem",
|
|
306
|
+
"aria-label": v,
|
|
307
|
+
onKeyDown: (e) => onChipKeyDown(e, i),
|
|
308
|
+
onFocus: () => setFocusedChip(i),
|
|
309
|
+
"data-chip-index": i,
|
|
310
|
+
children: v
|
|
311
|
+
}
|
|
312
|
+
) }, `${v}-${i}`)) }),
|
|
313
|
+
/* @__PURE__ */ jsx(
|
|
314
|
+
"input",
|
|
315
|
+
{
|
|
316
|
+
id: `${id}-input`,
|
|
317
|
+
ref: inputRef,
|
|
318
|
+
value: draft,
|
|
319
|
+
onChange: (e) => {
|
|
320
|
+
setDraft(e.target.value);
|
|
321
|
+
setMessage("");
|
|
322
|
+
},
|
|
323
|
+
onKeyDown: onInputKeyDown,
|
|
324
|
+
onBlur: () => {
|
|
325
|
+
if (draft.trim() && commit(draft)) setDraft("");
|
|
326
|
+
},
|
|
327
|
+
onPaste: (e) => {
|
|
328
|
+
const pasted = e.clipboardData.getData("text");
|
|
329
|
+
const pattern = new RegExp(`[${separators.map((s) => `\\${s}`).join("")}
|
|
330
|
+
\r]`);
|
|
331
|
+
if (!pattern.test(pasted)) return;
|
|
332
|
+
e.preventDefault();
|
|
333
|
+
if (commitMany(pasted.split(pattern)).added.length) setDraft("");
|
|
334
|
+
},
|
|
335
|
+
placeholder: value.length ? void 0 : placeholder,
|
|
336
|
+
disabled,
|
|
337
|
+
"aria-label": label ? void 0 : ariaLabelAttr ?? ariaLabel,
|
|
338
|
+
"aria-describedby": error ? errorId : void 0,
|
|
339
|
+
"aria-invalid": isInvalid || void 0,
|
|
340
|
+
className: "min-w-[6rem] flex-1 bg-transparent py-0.5 text-sm text-[var(--text-primary)] outline-none placeholder:text-[var(--text-placeholder)] disabled:cursor-default"
|
|
341
|
+
}
|
|
342
|
+
),
|
|
343
|
+
/* @__PURE__ */ jsx("span", { role: "status", "aria-live": "polite", "aria-atomic": true, className: "sr-only-fixed", children: message })
|
|
344
|
+
]
|
|
345
|
+
}
|
|
346
|
+
),
|
|
347
|
+
error && /* @__PURE__ */ jsx("p", { id: errorId, className: "text-xs text-[var(--danger)]", children: error })
|
|
348
|
+
] })
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
export {
|
|
352
|
+
Chip,
|
|
353
|
+
ChipInput,
|
|
354
|
+
DEFAULT_CHIP_INPUT_LABELS,
|
|
355
|
+
resolveChipInputLabels
|
|
356
|
+
};
|
|
357
|
+
//# sourceMappingURL=chip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/chip.tsx"],"sourcesContent":["import { forwardRef, useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_INVALID } from \"./ui\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\n\n/**\n * A chip: a compact pill carrying one value.\n *\n * Three jobs, deliberately one component. A chip can be inert (a tag on a row), a link\n * (a jump to a section), or a toggle (a filter that is on or off) — and the difference is\n * which prop you pass, not which component you pick. Keeping them together is what stops\n * an app growing three near-identical pills that drift apart; it is the shape this kit\n * already uses for `Button`, which is a link when it is given an href.\n *\n * A chip is not a `Button`. It is smaller, it is rounded to a pill, and it reads as a\n * VALUE rather than as an action — which matters because a row of eight buttons says\n * \"choose one of eight things to do\" and a row of eight chips says \"here are eight\n * things\". Using `Button` for both is the mistake this exists to prevent.\n */\n\nexport type ChipTone = \"neutral\" | \"brand\" | \"danger\" | \"warning\" | \"success\" | \"info\";\nexport type ChipSize = \"sm\" | \"md\";\n\nconst TONE: Record<ChipTone, { idle: string; selected: string }> = {\n neutral: {\n idle: \"border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-secondary)]\",\n selected: \"border-[var(--border-strong)] bg-[var(--bg-active)] text-[var(--text-primary)]\",\n },\n brand: {\n idle: \"border-[var(--border)] bg-[var(--brand-bg)] text-[var(--brand-muted)]\",\n selected: \"border-[var(--brand)] bg-[var(--brand)] text-[var(--brand-contrast)]\",\n },\n danger: {\n idle: \"border-[var(--danger-border)] bg-[var(--danger-bg)] text-[var(--danger)]\",\n selected: \"border-[var(--danger)] bg-[var(--danger)] text-[var(--danger-contrast)]\",\n },\n warning: {\n idle: \"border-[var(--warning-border)] bg-[var(--warning-bg)] text-[var(--warning)]\",\n selected: \"border-[var(--warning)] bg-[var(--warning-bg)] text-[var(--warning)]\",\n },\n success: {\n idle: \"border-[var(--success-border)] bg-[var(--success-bg)] text-[var(--success)]\",\n selected: \"border-[var(--success)] bg-[var(--success-bg)] text-[var(--success)]\",\n },\n info: {\n idle: \"border-[var(--info-border)] bg-[var(--info-bg)] text-[var(--info)]\",\n selected: \"border-[var(--info)] bg-[var(--info-bg)] text-[var(--info)]\",\n },\n};\n\nconst SIZE: Record<ChipSize, { body: string; icon: string; remove: string }> = {\n sm: { body: \"gap-1 px-2 py-0.5 text-xs\", icon: \"size-3\", remove: \"size-3\" },\n md: { body: \"gap-1.5 px-2.5 py-1 text-sm\", icon: \"size-3.5\", remove: \"size-3.5\" },\n};\n\nconst CHIP_BASE =\n \"inline-flex max-w-full items-center rounded-full border transition-colors \" +\n // `focus-visible`, not `focus`: a chip commonly receives focus programmatically (the\n // ChipInput moves focus onto one after a removal) and a ring that appears on a\n // pointer click reads as a stuck selection.\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-surface)]\";\n\nexport interface ChipProps {\n children: ReactNode;\n tone?: ChipTone;\n size?: ChipSize;\n icon?: LucideIcon;\n /** Marks the chip as the current one — `aria-current` on a link, `aria-pressed` on a toggle. */\n selected?: boolean;\n /** Renders the chip as a link. Mutually exclusive with `onClick`. */\n href?: string;\n /** Renders the chip as a toggle button. Mutually exclusive with `href`. */\n onClick?: () => void;\n /** Renders a dismiss affordance. Works alongside `href`/`onClick` — see the note below. */\n onRemove?: () => void;\n /** Accessible name for the dismiss button. Default: `common.remove` from the\n * {@link UiKitProvider}, else \"Remove\". */\n removeLabel?: string;\n disabled?: boolean;\n className?: string;\n /**\n * Roving focus, for a container that manages a row of chips (see {@link ChipInput}).\n * A chip with no `href`/`onClick` is inert and unfocusable by default; give it a\n * `tabIndex` and a `role` and it becomes the thing arrow keys move between.\n */\n tabIndex?: number;\n role?: string;\n onKeyDown?: (e: KeyboardEvent<HTMLElement>) => void;\n onFocus?: () => void;\n /** Accessible name, when the visible text is not the whole story. */\n \"aria-label\"?: string;\n /** Escape hatch for a test id, a `data-tour` anchor, or an `aria-describedby`. */\n [key: `data-${string}`]: unknown;\n}\n\n/**\n * Note on `onRemove` with `href`/`onClick`: a button cannot be nested inside a button or\n * a link, so when both are present the chip renders a wrapper holding TWO siblings — the\n * interactive body and the dismiss control. That is why the outer element is not always\n * the interactive one, and why the focus ring is drawn on the body rather than the\n * wrapper.\n */\nexport const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(\n {\n children,\n tone = \"neutral\",\n size = \"md\",\n icon: Icon,\n selected = false,\n href,\n onClick,\n onRemove,\n removeLabel,\n disabled = false,\n className,\n tabIndex,\n role,\n onKeyDown,\n onFocus,\n ...rest\n },\n ref,\n) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS, { remove: removeLabel });\n const s = SIZE[size];\n const palette = TONE[tone];\n const interactive = !!href || !!onClick;\n const look = cn(\n CHIP_BASE,\n s.body,\n selected ? palette.selected : palette.idle,\n interactive && !disabled && \"cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110\",\n disabled && \"cursor-default opacity-50\",\n className,\n );\n\n const body = (\n <>\n {Icon && <Icon className={cn(s.icon, \"shrink-0\")} aria-hidden />}\n <span className=\"min-w-0 truncate\">{children}</span>\n </>\n );\n\n const remove = onRemove ? (\n <button\n type=\"button\"\n // The name carries WHAT is being removed, not just \"Remove\" — a screen-reader user\n // tabbing a row of eight dismiss buttons otherwise hears the same word eight times\n // with no way to tell which one they are on.\n // Composed through `common.fieldValue` because the punctuation between the two\n // halves is the language's, not ours. A chip whose content is not plain text\n // has no value to name, and gets the bare verb rather than a dangling colon.\n aria-label={\n typeof children === \"string\" && children\n ? common.fieldValue(common.remove, children)\n : common.remove\n }\n onClick={(e) => {\n // The chip may itself be a link; removing it must not also follow it.\n e.preventDefault();\n e.stopPropagation();\n onRemove();\n }}\n disabled={disabled}\n className={cn(\n \"-mr-0.5 ml-0.5 shrink-0 rounded-full p-0.5 transition-colors\",\n \"hover:bg-[var(--bg-active)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]\",\n disabled && \"pointer-events-none\",\n )}\n >\n <X className={s.remove} aria-hidden />\n </button>\n ) : null;\n\n // ── The three shapes.\n if (href && !disabled) {\n const link = (\n <a\n ref={ref as React.Ref<HTMLAnchorElement>}\n href={href}\n aria-current={selected ? \"true\" : undefined}\n className={cn(look, remove && \"pr-1.5\")}\n {...rest}\n >\n {body}\n {!remove && null}\n </a>\n );\n return remove ? (\n <span className=\"inline-flex items-center\">\n {link}\n {remove}\n </span>\n ) : (\n link\n );\n }\n\n if (onClick) {\n const button = (\n <button\n ref={ref as React.Ref<HTMLButtonElement>}\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n aria-pressed={selected}\n className={cn(look, remove && \"pr-1.5\")}\n {...rest}\n >\n {body}\n </button>\n );\n return remove ? (\n <span className=\"inline-flex items-center\">\n {button}\n {remove}\n </span>\n ) : (\n button\n );\n }\n\n return (\n <span\n ref={ref as React.Ref<HTMLSpanElement>}\n className={look}\n tabIndex={tabIndex}\n role={role}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n {...rest}\n >\n {body}\n {remove}\n </span>\n );\n});\n\n/* ── ChipInput ────────────────────────────────────────────────────────────── */\n\nexport interface ChipInputLabels {\n /** Announced after a value is added. Receives the value. */\n added: (value: string) => string;\n /** Announced after a value is removed. Receives the value. */\n removed: (value: string) => string;\n /** Accessible name for each chip's dismiss button. */\n remove: string;\n /** Shown when `max` is reached. Receives the limit. */\n atLimit: (max: number) => string;\n /** Shown when a duplicate is rejected. Receives the value. */\n duplicate: (value: string) => string;\n}\n\nexport const DEFAULT_CHIP_INPUT_LABELS: ChipInputLabels = {\n added: (v) => `${v} added`,\n removed: (v) => `${v} removed`,\n remove: \"Remove\",\n atLimit: (max) => `Limit of ${max} reached`,\n duplicate: (v) => `${v} is already in the list`,\n};\n\nexport function resolveChipInputLabels(labels?: Partial<ChipInputLabels>): ChipInputLabels {\n return labels ? { ...DEFAULT_CHIP_INPUT_LABELS, ...labels } : DEFAULT_CHIP_INPUT_LABELS;\n}\n\n/**\n * `onChange` is the field's own — the whole LIST, not a DOM event — so the wrapper\n * div's `onChange` is omitted rather than shadowed. `placeholder` is likewise this\n * component's, forwarded to the inner `<input>`; the div has none of its own.\n */\nexport interface ChipInputProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"placeholder\" | \"children\"> {\n value: string[];\n onChange: (next: string[]) => void;\n placeholder?: string;\n /** Visible label, rendered above the field. Without one, name the field with\n * `aria-label`. */\n label?: string;\n /**\n * @deprecated Pass `aria-label` instead — the DOM spelling the rest of this kit\n * now uses. Still honoured, and still second to `aria-label` when both are given.\n */\n ariaLabel?: string;\n /** Reject or rewrite a value on commit. Return a string to reject WITH that message. */\n validate?: (raw: string) => string | null;\n /** Characters that commit the current text, besides Enter. Default: comma. */\n separators?: string[];\n allowDuplicates?: boolean;\n max?: number;\n tone?: ChipTone;\n size?: ChipSize;\n disabled?: boolean;\n invalid?: boolean;\n /** Error text. Wires `aria-describedby` and implies `invalid`. */\n error?: ReactNode;\n labels?: Partial<ChipInputLabels>;\n className?: string;\n}\n\n/**\n * A field whose value is a LIST, entered as chips.\n *\n * The keyboard model is the whole component; the pills are the easy part. Per the WAI\n * pattern for a \"tag\" input:\n *\n * Enter, or any separator commit the typed text\n * Backspace on empty text move focus to the last chip (does NOT delete it)\n * ← → while on a chip move between chips\n * Backspace / Delete remove the focused chip, focus its neighbour\n * Escape clear what is typed but keep the committed chips\n * paste splits on the separators, so a pasted CSV becomes chips\n *\n * The Backspace-on-empty behaviour is the one worth arguing about. Deleting immediately\n * is more common and is a trap: it is a destructive action with no feedback on a key\n * people hit by reflex, and the value it destroys may have taken a while to type. Moving\n * focus first makes the second Backspace a deliberate one, and it is what lets a\n * keyboard user reach a chip at all.\n */\nexport function ChipInput({\n value,\n onChange,\n placeholder,\n label,\n ariaLabel,\n validate,\n separators = [\",\"],\n allowDuplicates = false,\n max,\n tone = \"neutral\",\n size = \"md\",\n disabled = false,\n invalid = false,\n error,\n labels,\n className,\n \"aria-label\": ariaLabelAttr,\n ...rest\n}: ChipInputProps) {\n // `labels` over the provider's `chipInput` over English — the same order\n // `resolveChipInputLabels` gives outside a provider.\n const text = useKitLabels(\"chipInput\", DEFAULT_CHIP_INPUT_LABELS, labels);\n // A batch of pasted values is announced as one list, joined the way the language\n // joins a list in a sentence (\"a, b and c added\") — hence the long conjunction,\n // where a trigger's summary would use the narrow one.\n const locale = useKitLocale();\n const [draft, setDraft] = useState(\"\");\n const [message, setMessage] = useState(\"\");\n const [focusedChip, setFocusedChip] = useState<number | null>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const chipRefs = useRef<Array<HTMLElement | null>>([]);\n const id = useId();\n const listId = `${id}-list`;\n const errorId = `${id}-error`;\n const isInvalid = invalid || !!error;\n\n const focusChip = (index: number | null) => {\n setFocusedChip(index);\n if (index === null) inputRef.current?.focus();\n else chipRefs.current[index]?.focus();\n };\n\n /**\n * Try to add every candidate, as ONE change.\n *\n * Batched rather than looped for a reason that bites immediately: `commit` closes over\n * `value`, so calling it once per pasted item makes every call compute its result from\n * the list as it was BEFORE the paste, and each `onChange` overwrites the last. A\n * pasted \"one,two,three\" left only \"three\". Accumulating locally and emitting once is\n * also the correct behaviour for an undo stack — a paste is one action.\n */\n const commitMany = (raws: string[]): { added: string[]; rejection: string | null } => {\n const next = [...value];\n const added: string[] = [];\n let rejection: string | null = null;\n\n for (const raw of raws) {\n const candidate = raw.trim();\n if (!candidate) continue;\n if (max !== undefined && next.length >= max) {\n rejection ??= text.atLimit(max);\n break;\n }\n if (!allowDuplicates && next.includes(candidate)) {\n rejection ??= text.duplicate(candidate);\n continue;\n }\n const invalidReason = validate?.(candidate);\n if (invalidReason) {\n rejection ??= invalidReason;\n continue;\n }\n next.push(candidate);\n added.push(candidate);\n }\n\n if (added.length) onChange(next);\n // A rejection is the more useful thing to say: \"three added\" is obvious from the\n // chips appearing, whereas \"already in the list\" is the only sign of what did not.\n const addedList = new Intl.ListFormat(locale, { type: \"conjunction\", style: \"long\" });\n setMessage(rejection ?? (added.length ? text.added(addedList.format(added)) : \"\"));\n return { added, rejection };\n };\n\n const commit = (raw: string): boolean => commitMany([raw]).added.length > 0;\n\n const removeAt = (index: number) => {\n const removed = value[index];\n onChange(value.filter((_, i) => i !== index));\n setMessage(text.removed(removed));\n // Focus the neighbour that takes its place, or the input when the list empties —\n // dropping focus to <body> here is how a keyboard user loses the field entirely.\n const next = index >= value.length - 1 ? null : index;\n requestAnimationFrame(() => focusChip(value.length - 1 === 0 ? null : next));\n };\n\n const onInputKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\") {\n // Only when there is something to commit: an empty Enter belongs to the form.\n if (draft.trim()) {\n e.preventDefault();\n if (commit(draft)) setDraft(\"\");\n }\n return;\n }\n if (separators.includes(e.key)) {\n e.preventDefault();\n if (commit(draft)) setDraft(\"\");\n return;\n }\n if (e.key === \"Escape\" && draft) {\n e.preventDefault();\n setDraft(\"\");\n return;\n }\n if (e.key === \"Backspace\" && !draft && value.length) {\n e.preventDefault();\n focusChip(value.length - 1);\n }\n };\n\n const onChipKeyDown = (e: KeyboardEvent<HTMLElement>, index: number) => {\n if (e.key === \"Backspace\" || e.key === \"Delete\") {\n e.preventDefault();\n removeAt(index);\n return;\n }\n if (e.key === \"ArrowLeft\" && index > 0) {\n e.preventDefault();\n focusChip(index - 1);\n return;\n }\n if (e.key === \"ArrowRight\") {\n e.preventDefault();\n focusChip(index === value.length - 1 ? null : index + 1);\n return;\n }\n if (e.key === \"Escape\") {\n e.preventDefault();\n focusChip(null);\n }\n };\n\n return (\n // The rest lands on the OUTER box — the thing a `data-tour` step or a test id\n // wants to point at — while the accessible name below goes on the `<input>`,\n // because that is what a screen-reader user actually lands in. Spreading the two\n // to the same element would have named a decorative wrapper and left the field\n // anonymous.\n <div {...rest} className={cn(\"space-y-1\", className)}>\n {label && (\n <label htmlFor={`${id}-input`} className=\"block text-sm text-[var(--text-secondary)]\">\n {label}\n </label>\n )}\n <div\n // `relative` so the live region below can be `sr-only` without escaping to the\n // initial containing block and inflating the document height.\n className={cn(\n \"relative flex flex-wrap items-center gap-1.5 rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1.5 shadow-sm\",\n \"focus-within:border-[var(--brand)] focus-within:ring-1 focus-within:ring-[var(--brand)]\",\n disabled && \"cursor-default bg-[var(--bg-surface-2)]\",\n isInvalid && FIELD_INVALID,\n )}\n onClick={() => !disabled && inputRef.current?.focus()}\n >\n {/* `display: contents` on the wrappers so the chips participate in the field's\n own flex wrap rather than forming a nested row. The list semantics are\n restored with explicit roles, because `contents` removes the ul/li boxes from\n the accessibility tree in several engines. */}\n {/* eslint-disable-next-line jsx-a11y/no-redundant-roles -- NOT redundant here: the\n `contents` display below removes the ul and li boxes from the box tree, and\n several engines drop the list semantics with them. The explicit role is what\n puts \"list, 3 items\" back. */}\n <ul id={listId} role=\"list\" className=\"contents\">\n {value.map((v, i) => (\n <li key={`${v}-${i}`} role=\"presentation\" className=\"contents\">\n <Chip\n ref={(el: HTMLElement | null) => {\n chipRefs.current[i] = el;\n }}\n tone={tone}\n size={size}\n disabled={disabled}\n removeLabel={text.remove}\n onRemove={disabled ? undefined : () => removeAt(i)}\n // A ROVING tabIndex: only the focused chip is tabbable, so Tab leaves\n // the field instead of walking through every value in it — the reason a\n // list of twenty tags is otherwise a twenty-stop detour for a keyboard.\n tabIndex={focusedChip === i ? 0 : -1}\n role=\"listitem\"\n aria-label={v}\n onKeyDown={(e) => onChipKeyDown(e, i)}\n onFocus={() => setFocusedChip(i)}\n data-chip-index={i}\n >\n {v}\n </Chip>\n </li>\n ))}\n </ul>\n <input\n id={`${id}-input`}\n ref={inputRef}\n value={draft}\n onChange={(e) => {\n setDraft(e.target.value);\n setMessage(\"\");\n }}\n onKeyDown={onInputKeyDown}\n onBlur={() => {\n // Commit on blur: a value typed and then clicked away from is a value the\n // user meant to add, and losing it silently is the commonest complaint\n // about this pattern.\n if (draft.trim() && commit(draft)) setDraft(\"\");\n }}\n onPaste={(e) => {\n const pasted = e.clipboardData.getData(\"text\");\n const pattern = new RegExp(`[${separators.map((s) => `\\\\${s}`).join(\"\")}\\n\\r]`);\n if (!pattern.test(pasted)) return;\n e.preventDefault();\n if (commitMany(pasted.split(pattern)).added.length) setDraft(\"\");\n }}\n placeholder={value.length ? undefined : placeholder}\n disabled={disabled}\n // A visible `label` is already wired through `htmlFor`, so adding a name\n // here would give the field two. Failing that, the DOM spelling wins and\n // the deprecated `ariaLabel` is the fallback.\n aria-label={label ? undefined : (ariaLabelAttr ?? ariaLabel)}\n aria-describedby={error ? errorId : undefined}\n aria-invalid={isInvalid || undefined}\n className=\"min-w-[6rem] flex-1 bg-transparent py-0.5 text-sm text-[var(--text-primary)] outline-none placeholder:text-[var(--text-placeholder)] disabled:cursor-default\"\n />\n {/* Additions, removals and rejections move no focus, so nothing would announce\n them. `sr-only-fixed` rather than `sr-only`: this sits inside a consumer's\n markup and must not extend the document height. */}\n <span role=\"status\" aria-live=\"polite\" aria-atomic className=\"sr-only-fixed\">\n {message}\n </span>\n </div>\n {error && (\n <p id={errorId} className=\"text-xs text-[var(--danger)]\">\n {error}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["Chip"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Ref, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Small pieces the native choice controls (`Checkbox`, `Switch`) share. Private to
|
|
5
|
+
* the package — not re-exported from the barrel.
|
|
6
|
+
*
|
|
7
|
+
* `mergeDescribedBy` and `hasMessage` restate two rules ui.tsx's `useFieldError`
|
|
8
|
+
* keeps for Input/Select/Textarea; that hook is module-private and shaped around a
|
|
9
|
+
* single error node, while these controls also describe themselves with a
|
|
10
|
+
* `description`, so they compose the ids themselves under the same rules.
|
|
11
|
+
*/
|
|
12
|
+
/** Ids to describe the control by, merged in reading order: whatever the caller
|
|
13
|
+
* already pointed at (standing advice), then the description, then the error (the
|
|
14
|
+
* news). Merge, never replace — the rule ui.tsx's `useFieldError` keeps for Input.
|
|
15
|
+
* An empty list is `undefined`, never `""`: a dangling attribute describes the
|
|
16
|
+
* control as nothing at all. */
|
|
17
|
+
declare function mergeDescribedBy(...ids: Array<string | false | null | undefined>): string | undefined;
|
|
18
|
+
/** `null`, `false` and `""` are what `touched && errors.x` evaluates to on the happy
|
|
19
|
+
* path. None of them is a message. */
|
|
20
|
+
declare function hasMessage(node: ReactNode): boolean;
|
|
21
|
+
/** Point a forwarded ref and a local one at the same node. */
|
|
22
|
+
declare function assignRef<T>(ref: Ref<T> | undefined, node: T | null): void;
|
|
23
|
+
|
|
24
|
+
export { assignRef, hasMessage, mergeDescribedBy };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
function mergeDescribedBy(...ids) {
|
|
3
|
+
const list = ids.filter(Boolean);
|
|
4
|
+
return list.length ? list.join(" ") : void 0;
|
|
5
|
+
}
|
|
6
|
+
function hasMessage(node) {
|
|
7
|
+
return node !== void 0 && node !== null && node !== false && node !== "";
|
|
8
|
+
}
|
|
9
|
+
function assignRef(ref, node) {
|
|
10
|
+
if (typeof ref === "function") ref(node);
|
|
11
|
+
else if (ref) ref.current = node;
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
assignRef,
|
|
15
|
+
hasMessage,
|
|
16
|
+
mergeDescribedBy
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=choice-parts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/choice-parts.ts"],"sourcesContent":["import type { ReactNode, Ref } from \"react\";\n\n/**\n * Small pieces the native choice controls (`Checkbox`, `Switch`) share. Private to\n * the package — not re-exported from the barrel.\n *\n * `mergeDescribedBy` and `hasMessage` restate two rules ui.tsx's `useFieldError`\n * keeps for Input/Select/Textarea; that hook is module-private and shaped around a\n * single error node, while these controls also describe themselves with a\n * `description`, so they compose the ids themselves under the same rules.\n */\n\n/** Ids to describe the control by, merged in reading order: whatever the caller\n * already pointed at (standing advice), then the description, then the error (the\n * news). Merge, never replace — the rule ui.tsx's `useFieldError` keeps for Input.\n * An empty list is `undefined`, never `\"\"`: a dangling attribute describes the\n * control as nothing at all. */\nexport function mergeDescribedBy(...ids: Array<string | false | null | undefined>): string | undefined {\n const list = ids.filter(Boolean);\n return list.length ? list.join(\" \") : undefined;\n}\n\n/** `null`, `false` and `\"\"` are what `touched && errors.x` evaluates to on the happy\n * path. None of them is a message. */\nexport function hasMessage(node: ReactNode): boolean {\n return node !== undefined && node !== null && node !== false && node !== \"\";\n}\n\n/** Point a forwarded ref and a local one at the same node. */\nexport function assignRef<T>(ref: Ref<T> | undefined, node: T | null) {\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as { current: T | null }).current = node;\n}\n"],"mappings":";AAiBO,SAAS,oBAAoB,KAAmE;AACrG,QAAM,OAAO,IAAI,OAAO,OAAO;AAC/B,SAAO,KAAK,SAAS,KAAK,KAAK,GAAG,IAAI;AACxC;AAIO,SAAS,WAAW,MAA0B;AACnD,SAAO,SAAS,UAAa,SAAS,QAAQ,SAAS,SAAS,SAAS;AAC3E;AAGO,SAAS,UAAa,KAAyB,MAAgB;AACpE,MAAI,OAAO,QAAQ,WAAY,KAAI,IAAI;AAAA,WAC9B,IAAK,CAAC,IAA8B,UAAU;AACzD;","names":[]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode, RefObject, Dispatch, SetStateAction } from 'react';
|
|
2
|
+
import { ReactNode, RefObject, Dispatch, SetStateAction, ComponentPropsWithoutRef } from 'react';
|
|
3
3
|
import { AnchorRect } from '../hooks/use-anchored-rect.js';
|
|
4
4
|
import { AnchoredPanel } from '../hooks/use-anchored-panel.js';
|
|
5
5
|
|
|
@@ -42,6 +42,9 @@ interface ComboboxCore<V extends string | number> {
|
|
|
42
42
|
placement: AnchoredPanel;
|
|
43
43
|
cacheRef: RefObject<Map<V, ComboOption<V>>>;
|
|
44
44
|
resolve: (v: V) => ComboOption<V> | null;
|
|
45
|
+
/** Close and hand focus back to the trigger — the keyboard paths only. See
|
|
46
|
+
* {@link useDropdown} for why a pointer dismissal must not do this. */
|
|
47
|
+
closeToTrigger: () => void;
|
|
45
48
|
}
|
|
46
49
|
/**
|
|
47
50
|
* Shared plumbing for the entity pickers: open/query/active state, portalled
|
|
@@ -53,15 +56,28 @@ interface ComboboxCore<V extends string | number> {
|
|
|
53
56
|
*/
|
|
54
57
|
declare function useComboboxCore<V extends string | number>({ options, loadOptions, loading, }: ComboboxCoreOptions<V>): ComboboxCore<V>;
|
|
55
58
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
59
|
+
* Keep the option the keyboard is on inside the visible part of the list.
|
|
60
|
+
*
|
|
61
|
+
* `aria-activedescendant` moves a highlight WITHOUT moving focus, which is the whole
|
|
62
|
+
* point of it — and the cost is that the browser scrolls nothing on its own. In a
|
|
63
|
+
* panel capped at a few hundred pixels over an account list two hundred long, Down
|
|
64
|
+
* walked the highlight straight out of the box and the list sat still.
|
|
65
|
+
*
|
|
66
|
+
* Optional-called because jsdom does not implement `scrollIntoView`: three consumers
|
|
67
|
+
* run this component in their own suites, and a picker that throws there is a worse
|
|
68
|
+
* trade than a list that does not scroll in a test.
|
|
60
69
|
*/
|
|
61
|
-
declare function
|
|
70
|
+
declare function useActiveOptionScroll(activeId: string | undefined): void;
|
|
71
|
+
interface ComboboxPanelProps<V extends string | number> extends ComponentPropsWithoutRef<"div"> {
|
|
62
72
|
core: ComboboxCore<V>;
|
|
73
|
+
/** What the trigger's `aria-controls` names. Owned by the component that renders
|
|
74
|
+
* the trigger, not by the core: one id has to reach both, and only one of them
|
|
75
|
+
* can be the origin. */
|
|
76
|
+
listboxId: string;
|
|
77
|
+
/** Default: `combobox.search` from the {@link UiKitProvider}, else English. */
|
|
63
78
|
searchPlaceholder?: string;
|
|
64
|
-
|
|
79
|
+
/** Default: `combobox.noResults` from the {@link UiKitProvider}, else English. */
|
|
80
|
+
emptyLabel?: string;
|
|
65
81
|
multi?: boolean;
|
|
66
82
|
isSelected: (v: V) => boolean;
|
|
67
83
|
onChoose: (o: ComboOption<V>) => void;
|
|
@@ -72,6 +88,33 @@ declare function ComboboxPanel<V extends string | number>({ core, searchPlacehol
|
|
|
72
88
|
* desktop, where the panel sits under the field that already says it. */
|
|
73
89
|
sheetTitle?: ReactNode;
|
|
74
90
|
closeLabel?: string;
|
|
75
|
-
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The portalled dropdown (search header + result rows + optional create row)
|
|
94
|
+
* shared by the single- and multi-value comboboxes. Left-aligned to the trigger
|
|
95
|
+
* and sized to its width; `multi` swaps the trailing check for a leading
|
|
96
|
+
* checkbox. `onChoose` decides whether to close (single) or stay open (multi).
|
|
97
|
+
*
|
|
98
|
+
* ## The ARIA, which is `CommandPalette`'s
|
|
99
|
+
*
|
|
100
|
+
* This rendered a `role="listbox"` of `role="option"` rows and stopped there, so a
|
|
101
|
+
* reader was handed a text box and silence: the highlight the arrow keys move lived
|
|
102
|
+
* only in a background colour, and nothing tied the box being typed in to the list
|
|
103
|
+
* being filtered. `CommandPalette` has had the whole model since it was written —
|
|
104
|
+
* `aria-controls` from the field to the list, `aria-activedescendant` naming the row
|
|
105
|
+
* the keyboard is on — and it was the only thing in the package that did.
|
|
106
|
+
*
|
|
107
|
+
* Two distinct states, deliberately not conflated:
|
|
108
|
+
* - `aria-selected` is CHOSEN. It is the check mark, and in `multi` the checkbox.
|
|
109
|
+
* - `aria-activedescendant` is where the KEYBOARD is. Announcing that as selected
|
|
110
|
+
* would tell a reader that the row they are merely passing over is the answer.
|
|
111
|
+
*
|
|
112
|
+
* `role="option"` sits on the `<button>`, not on the `<li>` around it, for the same
|
|
113
|
+
* reason it does in `CommandPalette`: an option may not contain a separately
|
|
114
|
+
* focusable control, and the button is the thing a pointer presses. The rows are
|
|
115
|
+
* `tabIndex={-1}` because focus never leaves the search box — that is what
|
|
116
|
+
* `aria-activedescendant` is FOR.
|
|
117
|
+
*/
|
|
118
|
+
declare function ComboboxPanel<V extends string | number>({ core, listboxId, searchPlaceholder, emptyLabel, multi, isSelected, onChoose, showCreate, onCreate, createContent, sheetTitle, closeLabel, className, style, ...rest }: ComboboxPanelProps<V>): react.JSX.Element | null;
|
|
76
119
|
|
|
77
|
-
export { type ComboOption, type ComboboxCore, type ComboboxCoreOptions, ComboboxPanel, useComboboxCore };
|
|
120
|
+
export { type ComboOption, type ComboboxCore, type ComboboxCoreOptions, ComboboxPanel, type ComboboxPanelProps, useActiveOptionScroll, useComboboxCore };
|