@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
3
|
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
|
3
4
|
import { createPortal } from "react-dom";
|
|
@@ -9,6 +10,7 @@ import { useMediaQuery } from "../hooks/use-media-query.js";
|
|
|
9
10
|
import { PHONE_QUERY } from "./ui.js";
|
|
10
11
|
import { useAnchoredPanel } from "../hooks/use-anchored-panel.js";
|
|
11
12
|
import { useEscapeKey, useOutsideClick } from "../hooks/use-dismiss.js";
|
|
13
|
+
import { DEFAULT_COMBOBOX_LABELS, useKitLabels } from "../i18n/kit-labels.js";
|
|
12
14
|
function useComboboxCore({
|
|
13
15
|
options,
|
|
14
16
|
loadOptions,
|
|
@@ -27,11 +29,15 @@ function useComboboxCore({
|
|
|
27
29
|
loadRef.current = loadOptions;
|
|
28
30
|
const reqId = useRef(0);
|
|
29
31
|
const close = () => setOpen(false);
|
|
32
|
+
const closeToTrigger = () => {
|
|
33
|
+
setOpen(false);
|
|
34
|
+
triggerRef.current?.focus();
|
|
35
|
+
};
|
|
30
36
|
const placement = useAnchoredPanel(triggerRef, open);
|
|
31
37
|
const rect = placement.rect;
|
|
32
38
|
const isPhone = useMediaQuery(PHONE_QUERY, false);
|
|
33
39
|
useOutsideClick([triggerRef, panelRef], close, open && !isPhone);
|
|
34
|
-
useEscapeKey(
|
|
40
|
+
useEscapeKey(closeToTrigger, open);
|
|
35
41
|
useEffect(() => {
|
|
36
42
|
if (!open) return;
|
|
37
43
|
setQuery("");
|
|
@@ -88,11 +94,19 @@ function useComboboxCore({
|
|
|
88
94
|
rect,
|
|
89
95
|
placement,
|
|
90
96
|
cacheRef,
|
|
91
|
-
resolve
|
|
97
|
+
resolve,
|
|
98
|
+
closeToTrigger
|
|
92
99
|
};
|
|
93
100
|
}
|
|
101
|
+
function useActiveOptionScroll(activeId) {
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!activeId) return;
|
|
104
|
+
document.getElementById(activeId)?.scrollIntoView?.({ block: "nearest" });
|
|
105
|
+
}, [activeId]);
|
|
106
|
+
}
|
|
94
107
|
function ComboboxPanel({
|
|
95
108
|
core,
|
|
109
|
+
listboxId,
|
|
96
110
|
searchPlaceholder,
|
|
97
111
|
emptyLabel,
|
|
98
112
|
multi,
|
|
@@ -102,11 +116,30 @@ function ComboboxPanel({
|
|
|
102
116
|
onCreate,
|
|
103
117
|
createContent,
|
|
104
118
|
sheetTitle,
|
|
105
|
-
closeLabel
|
|
119
|
+
closeLabel,
|
|
120
|
+
className,
|
|
121
|
+
style,
|
|
122
|
+
...rest
|
|
106
123
|
}) {
|
|
107
|
-
const {
|
|
124
|
+
const {
|
|
125
|
+
rect,
|
|
126
|
+
placement,
|
|
127
|
+
panelRef,
|
|
128
|
+
inputRef,
|
|
129
|
+
query,
|
|
130
|
+
setQuery,
|
|
131
|
+
results,
|
|
132
|
+
busy,
|
|
133
|
+
active,
|
|
134
|
+
setActive,
|
|
135
|
+
closeToTrigger
|
|
136
|
+
} = core;
|
|
108
137
|
const isPhone = useMediaQuery(PHONE_QUERY, false);
|
|
109
138
|
const rowCount = results.length + (showCreate ? 1 : 0);
|
|
139
|
+
const optionId = (index) => `${listboxId}-option-${index}`;
|
|
140
|
+
const activeId = active >= 0 && active < rowCount ? optionId(active) : void 0;
|
|
141
|
+
useActiveOptionScroll(activeId);
|
|
142
|
+
const labels = useKitLabels("combobox", DEFAULT_COMBOBOX_LABELS);
|
|
110
143
|
const onKeyDown = (e) => {
|
|
111
144
|
if (e.key === "ArrowDown") {
|
|
112
145
|
e.preventDefault();
|
|
@@ -114,22 +147,39 @@ function ComboboxPanel({
|
|
|
114
147
|
} else if (e.key === "ArrowUp") {
|
|
115
148
|
e.preventDefault();
|
|
116
149
|
setActive((i) => Math.max(i - 1, 0));
|
|
150
|
+
} else if (e.key === "Home") {
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
setActive(0);
|
|
153
|
+
} else if (e.key === "End") {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
setActive(rowCount - 1);
|
|
117
156
|
} else if (e.key === "Enter") {
|
|
118
157
|
e.preventDefault();
|
|
119
158
|
if (active < results.length) onChoose(results[active]);
|
|
120
159
|
else if (showCreate) onCreate();
|
|
160
|
+
} else if (e.key === "Tab") {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
closeToTrigger();
|
|
121
163
|
}
|
|
122
164
|
};
|
|
123
165
|
if (!core.open || typeof document === "undefined") return null;
|
|
124
166
|
const list = /* @__PURE__ */ jsxs(
|
|
125
167
|
"ul",
|
|
126
168
|
{
|
|
169
|
+
id: listboxId,
|
|
127
170
|
role: "listbox",
|
|
128
171
|
"aria-multiselectable": multi,
|
|
129
172
|
className: cn("min-h-0 flex-1 overflow-y-auto py-1", isPhone && "flex-none"),
|
|
130
173
|
children: [
|
|
131
|
-
busy && results.length === 0 &&
|
|
132
|
-
|
|
174
|
+
busy && results.length === 0 && // The ellipsis is the picture; the words are for a reader, who otherwise met
|
|
175
|
+
// an empty-looking list with a lone "…" in it — or, in most readers, nothing
|
|
176
|
+
// at all, since punctuation alone is skipped. `relative` so the sr-only text
|
|
177
|
+
// has a local containing block (see sr-only-containment.test).
|
|
178
|
+
/* @__PURE__ */ jsxs("li", { className: "relative px-3 py-2 text-sm text-[var(--text-muted)]", children: [
|
|
179
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\u2026" }),
|
|
180
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: labels.loading })
|
|
181
|
+
] }),
|
|
182
|
+
!busy && results.length === 0 && !showCreate && /* @__PURE__ */ jsx("li", { className: "px-3 py-2 text-sm text-[var(--text-muted)]", children: emptyLabel ?? labels.noResults }),
|
|
133
183
|
results.map((o, i) => {
|
|
134
184
|
const selected = isSelected(o.value);
|
|
135
185
|
const startsGroup = o.group != null && o.group !== results[i - 1]?.group;
|
|
@@ -143,16 +193,20 @@ function ComboboxPanel({
|
|
|
143
193
|
{
|
|
144
194
|
role: "presentation",
|
|
145
195
|
className: cn(
|
|
146
|
-
"font-semibold uppercase tracking-wide text-
|
|
196
|
+
"font-semibold uppercase tracking-wide text-[var(--text-muted)]",
|
|
147
197
|
isPhone ? "px-4 pb-1 pt-3 text-xs" : "px-3 pb-0.5 pt-2 text-[11px] first:pt-1"
|
|
148
198
|
),
|
|
149
199
|
children: o.group
|
|
150
200
|
}
|
|
151
201
|
),
|
|
152
|
-
/* @__PURE__ */ jsx("li", { role: "
|
|
202
|
+
/* @__PURE__ */ jsx("li", { role: "presentation", children: /* @__PURE__ */ jsxs(
|
|
153
203
|
"button",
|
|
154
204
|
{
|
|
155
205
|
type: "button",
|
|
206
|
+
id: optionId(i),
|
|
207
|
+
role: "option",
|
|
208
|
+
"aria-selected": selected,
|
|
209
|
+
tabIndex: -1,
|
|
156
210
|
onMouseDown: (e) => {
|
|
157
211
|
e.preventDefault();
|
|
158
212
|
onChoose(o);
|
|
@@ -160,35 +214,40 @@ function ComboboxPanel({
|
|
|
160
214
|
onMouseEnter: () => setActive(i),
|
|
161
215
|
className: cn(
|
|
162
216
|
isPhone ? SHEET_ROW_CLASS : "flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm",
|
|
163
|
-
i === active && !isPhone ? "bg-
|
|
217
|
+
i === active && !isPhone ? "bg-[var(--bg-active)]" : !isPhone && "hover:bg-[var(--bg-hover)]"
|
|
164
218
|
),
|
|
165
219
|
children: [
|
|
166
220
|
multi && /* @__PURE__ */ jsx(
|
|
167
221
|
"span",
|
|
168
222
|
{
|
|
223
|
+
"aria-hidden": true,
|
|
169
224
|
className: cn(
|
|
170
225
|
"flex size-4 shrink-0 items-center justify-center rounded border",
|
|
171
|
-
selected ? "border-
|
|
226
|
+
selected ? "border-[var(--bg-inverse)] bg-[var(--bg-inverse)] text-[var(--text-inverse)]" : "border-[var(--border-strong)] bg-[var(--bg-surface)]"
|
|
172
227
|
),
|
|
173
228
|
children: selected && /* @__PURE__ */ jsx(Check, { className: "size-3" })
|
|
174
229
|
}
|
|
175
230
|
),
|
|
176
231
|
o.icon && /* @__PURE__ */ jsx("span", { className: "shrink-0", children: o.icon }),
|
|
177
232
|
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
178
|
-
/* @__PURE__ */ jsx("span", { className: "block truncate text-
|
|
179
|
-
o.sublabel && /* @__PURE__ */ jsx("span", { className: "block truncate text-xs text-
|
|
233
|
+
/* @__PURE__ */ jsx("span", { className: "block truncate text-[var(--text-primary)]", children: o.label }),
|
|
234
|
+
o.sublabel && /* @__PURE__ */ jsx("span", { className: "block truncate text-xs text-[var(--text-placeholder)]", children: o.sublabel })
|
|
180
235
|
] }),
|
|
181
|
-
!multi && selected && /* @__PURE__ */ jsx(Check, { className: "size-4 shrink-0 text-
|
|
236
|
+
!multi && selected && /* @__PURE__ */ jsx(Check, { className: "size-4 shrink-0 text-[var(--brand)]" })
|
|
182
237
|
]
|
|
183
238
|
}
|
|
184
239
|
) })
|
|
185
240
|
] }, `${o.group ?? ""}|${String(o.value)}`)
|
|
186
241
|
);
|
|
187
242
|
}),
|
|
188
|
-
showCreate && /* @__PURE__ */ jsx("li", { role: "
|
|
243
|
+
showCreate && /* @__PURE__ */ jsx("li", { role: "presentation", children: /* @__PURE__ */ jsxs(
|
|
189
244
|
"button",
|
|
190
245
|
{
|
|
191
246
|
type: "button",
|
|
247
|
+
id: optionId(results.length),
|
|
248
|
+
role: "option",
|
|
249
|
+
"aria-selected": false,
|
|
250
|
+
tabIndex: -1,
|
|
192
251
|
onMouseDown: (e) => {
|
|
193
252
|
e.preventDefault();
|
|
194
253
|
onCreate();
|
|
@@ -196,8 +255,8 @@ function ComboboxPanel({
|
|
|
196
255
|
onMouseEnter: () => setActive(results.length),
|
|
197
256
|
className: cn(
|
|
198
257
|
isPhone ? SHEET_ROW_CLASS : "flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm",
|
|
199
|
-
"text-
|
|
200
|
-
active === results.length && !isPhone ? "bg-
|
|
258
|
+
"text-[var(--text-secondary)]",
|
|
259
|
+
active === results.length && !isPhone ? "bg-[var(--bg-active)]" : !isPhone && "hover:bg-[var(--bg-hover)]"
|
|
201
260
|
),
|
|
202
261
|
children: [
|
|
203
262
|
/* @__PURE__ */ jsx(Plus, { className: "size-4 shrink-0" }),
|
|
@@ -212,12 +271,15 @@ function ComboboxPanel({
|
|
|
212
271
|
return /* @__PURE__ */ jsx(
|
|
213
272
|
PickerSheet,
|
|
214
273
|
{
|
|
274
|
+
...rest,
|
|
275
|
+
className,
|
|
276
|
+
style,
|
|
215
277
|
open: true,
|
|
216
278
|
onClose: () => core.setOpen(false),
|
|
217
279
|
title: sheetTitle,
|
|
218
280
|
query,
|
|
219
281
|
onQueryChange: setQuery,
|
|
220
|
-
searchPlaceholder,
|
|
282
|
+
searchPlaceholder: searchPlaceholder ?? labels.search,
|
|
221
283
|
inputRef,
|
|
222
284
|
closeLabel,
|
|
223
285
|
children: /* @__PURE__ */ jsx("div", { onKeyDown, children: list })
|
|
@@ -229,9 +291,11 @@ function ComboboxPanel({
|
|
|
229
291
|
/* @__PURE__ */ jsxs(
|
|
230
292
|
"div",
|
|
231
293
|
{
|
|
294
|
+
...rest,
|
|
232
295
|
ref: panelRef,
|
|
233
296
|
onKeyDown,
|
|
234
297
|
style: {
|
|
298
|
+
...style,
|
|
235
299
|
position: "fixed",
|
|
236
300
|
top: placement.top,
|
|
237
301
|
left: rect.left,
|
|
@@ -239,7 +303,10 @@ function ComboboxPanel({
|
|
|
239
303
|
minWidth: 220,
|
|
240
304
|
maxHeight: placement.maxHeight
|
|
241
305
|
},
|
|
242
|
-
className:
|
|
306
|
+
className: cn(
|
|
307
|
+
"z-50 flex flex-col rounded-md border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg",
|
|
308
|
+
className
|
|
309
|
+
),
|
|
243
310
|
children: [
|
|
244
311
|
/* @__PURE__ */ jsx(
|
|
245
312
|
DropdownSearchHeader,
|
|
@@ -247,7 +314,9 @@ function ComboboxPanel({
|
|
|
247
314
|
query,
|
|
248
315
|
onQueryChange: setQuery,
|
|
249
316
|
inputRef,
|
|
250
|
-
placeholder: searchPlaceholder
|
|
317
|
+
placeholder: searchPlaceholder ?? labels.search,
|
|
318
|
+
listboxId,
|
|
319
|
+
activeId
|
|
251
320
|
}
|
|
252
321
|
),
|
|
253
322
|
list
|
|
@@ -259,6 +328,7 @@ function ComboboxPanel({
|
|
|
259
328
|
}
|
|
260
329
|
export {
|
|
261
330
|
ComboboxPanel,
|
|
331
|
+
useActiveOptionScroll,
|
|
262
332
|
useComboboxCore
|
|
263
333
|
};
|
|
264
334
|
//# sourceMappingURL=combobox-core.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/combobox-core.tsx"],"sourcesContent":["import { Fragment, useEffect, useMemo, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type {\n Dispatch,\n KeyboardEvent as ReactKeyboardEvent,\n ReactNode,\n RefObject,\n SetStateAction,\n} from \"react\";\nimport { Check, Plus } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { DropdownSearchHeader } from \"./dropdown\";\nimport { PickerSheet, SHEET_ROW_CLASS } from \"./picker-sheet\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { PHONE_QUERY } from \"./ui\";\nimport { type AnchorRect } from \"../hooks/use-anchored-rect\";\nimport { useAnchoredPanel, type AnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useEscapeKey, useOutsideClick } from \"../hooks/use-dismiss\";\n\nexport interface ComboOption<V extends string | number> {\n value: V;\n label: string;\n sublabel?: string;\n icon?: ReactNode;\n /**\n * Section this option belongs to. Where a consumer renders it as a heading with the\n * members indented beneath — {@link InlineEntityCombobox} does — this is a genuine\n * grouping, not decoration: the name appears once per section rather than trailing\n * every row in small grey type, which is barely legible on a phone (feedback #136).\n * Searchable like `sublabel`.\n */\n group?: string;\n}\n\nexport interface ComboboxCoreOptions<V extends string | number> {\n /** Already-loaded options (client-side filtered), also used to resolve labels. */\n options?: ComboOption<V>[];\n /** Async option source, debounced and race-safe; stale responses are ignored. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** External loading flag, OR-ed with the internal async state. */\n loading?: boolean;\n}\n\nexport interface ComboboxCore<V extends string | number> {\n triggerRef: RefObject<HTMLButtonElement | null>;\n panelRef: RefObject<HTMLDivElement | null>;\n inputRef: RefObject<HTMLInputElement | null>;\n open: boolean;\n setOpen: Dispatch<SetStateAction<boolean>>;\n query: string;\n setQuery: Dispatch<SetStateAction<string>>;\n active: number;\n setActive: Dispatch<SetStateAction<number>>;\n results: ComboOption<V>[];\n busy: boolean;\n rect: AnchorRect | null;\n /** Where the dropdown goes, clamped to the visible viewport (feedback #135). */\n placement: AnchoredPanel;\n cacheRef: RefObject<Map<V, ComboOption<V>>>;\n resolve: (v: V) => ComboOption<V> | null;\n}\n\n/**\n * Shared plumbing for the entity pickers: open/query/active state, portalled\n * anchoring + outside-click/Escape dismissal, focus-on-open, a debounced\n * race-safe async (or client-side) result list, and a label cache so a selected\n * value still renders its label after the async list has moved on. The single-\n * and multi-value comboboxes render their triggers on top of this and share\n * {@link ComboboxPanel} for the dropdown.\n */\nexport function useComboboxCore<V extends string | number>({\n options,\n loadOptions,\n loading,\n}: ComboboxCoreOptions<V>): ComboboxCore<V> {\n const triggerRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [active, setActive] = useState(0);\n\n const isAsync = typeof loadOptions === \"function\";\n const [asyncResults, setAsyncResults] = useState<ComboOption<V>[]>([]);\n const [asyncLoading, setAsyncLoading] = useState(false);\n const loadRef = useRef(loadOptions);\n loadRef.current = loadOptions;\n const reqId = useRef(0);\n\n const close = () => setOpen(false);\n // Placement, not just the anchor rect: the panel has to dodge the on-screen\n // keyboard its own search box summons (feedback #135).\n const placement = useAnchoredPanel(triggerRef, open);\n const rect = placement.rect;\n // Outside-click dismissal belongs to the ANCHORED panel, and only to it. On a\n // phone `ComboboxPanel` renders a full-screen `PickerSheet` instead (live #200),\n // portalled to <body> and carrying its own ways out — the close X, Escape, and\n // Back through `useOverlayHistory` — so there is no outside left to press.\n //\n // Leaving the listener on there was not merely redundant, it was the bug:\n // `panelRef` is attached to the desktop panel alone, so with the sheet up every\n // press inside it answered \"outside\". While `useOutsideClick` listened for\n // `mousedown` the rows survived by accident — they commit on `onMouseDown` too,\n // and won the race — but `pointerdown` PRECEDES `mousedown`, so the sheet closed\n // at finger-down and the commit landed on an unmounted tree: a tapped row kept the\n // old value, and tapping the search box or the X was equally fatal. `PickerSheet`\n // stops `mousedown` for exactly this reason, but that guard was written for\n // `useDropdown`'s listener and never covered this hook.\n const isPhone = useMediaQuery(PHONE_QUERY, false);\n useOutsideClick([triggerRef, panelRef], close, open && !isPhone);\n useEscapeKey(close, open);\n\n // Reset the query + focus the search box each time the panel opens.\n useEffect(() => {\n if (!open) return;\n setQuery(\"\");\n const id = requestAnimationFrame(() => inputRef.current?.focus());\n return () => cancelAnimationFrame(id);\n }, [open]);\n\n // Debounced, race-safe async search (only when loadOptions is provided).\n useEffect(() => {\n if (!open || !isAsync) return;\n const id = ++reqId.current;\n setAsyncLoading(true);\n const run = async () => {\n try {\n const r = await loadRef.current!(query);\n if (reqId.current === id) setAsyncResults(r);\n } finally {\n if (reqId.current === id) setAsyncLoading(false);\n }\n };\n const t = setTimeout(run, query ? 150 : 0);\n return () => clearTimeout(t);\n }, [query, open, isAsync]);\n\n const clientResults = useMemo(() => {\n if (isAsync) return [];\n const src = options ?? [];\n const q = query.trim().toLowerCase();\n if (!q) return src;\n return src.filter(\n (o) => o.label.toLowerCase().includes(q) || (o.sublabel ?? \"\").toLowerCase().includes(q),\n );\n }, [isAsync, options, query]);\n\n const results = isAsync ? asyncResults : clientResults;\n const busy = Boolean(loading) || (isAsync && asyncLoading);\n\n // Accumulate every option we've seen so a selected value can render its label\n // even after the async list has moved on to other query results.\n const cacheRef = useRef(new Map<V, ComboOption<V>>());\n useEffect(() => {\n for (const o of options ?? []) cacheRef.current.set(o.value, o);\n for (const o of results) cacheRef.current.set(o.value, o);\n }, [options, results]);\n const resolve = (v: V): ComboOption<V> | null =>\n options?.find((o) => o.value === v) ?? cacheRef.current.get(v) ?? null;\n\n // Reset the highlighted row whenever the visible set changes.\n useEffect(() => {\n setActive(0);\n }, [query, open]);\n\n return {\n triggerRef,\n panelRef,\n inputRef,\n open,\n setOpen,\n query,\n setQuery,\n active,\n setActive,\n results,\n busy,\n rect,\n placement,\n cacheRef,\n resolve,\n };\n}\n\n/**\n * The portalled dropdown (search header + result rows + optional create row)\n * shared by the single- and multi-value comboboxes. Left-aligned to the trigger\n * and sized to its width; `multi` swaps the trailing check for a leading\n * checkbox. `onChoose` decides whether to close (single) or stay open (multi).\n */\nexport function ComboboxPanel<V extends string | number>({\n core,\n searchPlaceholder,\n emptyLabel,\n multi,\n isSelected,\n onChoose,\n showCreate,\n onCreate,\n createContent,\n sheetTitle,\n closeLabel,\n}: {\n core: ComboboxCore<V>;\n searchPlaceholder?: string;\n emptyLabel: string;\n multi?: boolean;\n isSelected: (v: V) => boolean;\n onChoose: (o: ComboOption<V>) => void;\n showCreate: boolean;\n onCreate: () => void;\n createContent: ReactNode;\n /** What the sheet calls itself on a phone — the field's label. Ignored on\n * desktop, where the panel sits under the field that already says it. */\n sheetTitle?: ReactNode;\n closeLabel?: string;\n}) {\n const { rect, placement, panelRef, inputRef, query, setQuery, results, busy, active, setActive } =\n core;\n // The phone gets a full-screen sheet instead of an anchored panel (live #200).\n // Same core, same results, same handlers — only the container differs, so the\n // two presentations cannot drift in what they offer.\n const isPhone = useMediaQuery(PHONE_QUERY, false);\n const rowCount = results.length + (showCreate ? 1 : 0);\n\n const onKeyDown = (e: ReactKeyboardEvent) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((i) => Math.min(i + 1, rowCount - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n if (active < results.length) onChoose(results[active]);\n else if (showCreate) onCreate();\n }\n };\n\n if (!core.open || typeof document === \"undefined\") return null;\n\n const list = (\n <ul\n role=\"listbox\"\n aria-multiselectable={multi}\n className={cn(\"min-h-0 flex-1 overflow-y-auto py-1\", isPhone && \"flex-none\")}\n >\n {busy && results.length === 0 && (\n <li className=\"px-3 py-2 text-sm text-slate-500 dark:text-slate-400\">…</li>\n )}\n {!busy && results.length === 0 && !showCreate && (\n <li className=\"px-3 py-2 text-sm text-slate-500 dark:text-slate-400\">{emptyLabel}</li>\n )}\n {results.map((o, i) => {\n const selected = isSelected(o.value);\n // One heading per group rather than a grey suffix on every row (#136), and\n // the thing the native <select>'s <optgroup> used to give the account\n // picker for free (live #200). `results` keeps its source order, so a\n // boundary is simply \"different from the row above\".\n const startsGroup = o.group != null && o.group !== results[i - 1]?.group;\n return (\n // Keyed by group AND value: an option may deliberately appear twice —\n // Keksdose repeats recently-used categories in a \"Recent\" group at the\n // top (live #203) — and a bare value key would collide.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {startsGroup && (\n <li\n role=\"presentation\"\n className={cn(\n \"font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400\",\n isPhone ? \"px-4 pb-1 pt-3 text-xs\" : \"px-3 pb-0.5 pt-2 text-[11px] first:pt-1\",\n )}\n >\n {o.group}\n </li>\n )}\n <li role=\"option\" aria-selected={selected}>\n <button\n type=\"button\"\n onMouseDown={(e) => {\n e.preventDefault();\n onChoose(o);\n }}\n onMouseEnter={() => setActive(i)}\n className={cn(\n isPhone\n ? SHEET_ROW_CLASS\n : \"flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm\",\n i === active && !isPhone\n ? \"bg-slate-100 dark:bg-slate-800\"\n : !isPhone && \"hover:bg-slate-50 dark:hover:bg-slate-800\",\n )}\n >\n {multi && (\n <span\n className={cn(\n \"flex size-4 shrink-0 items-center justify-center rounded border\",\n selected\n ? \"border-slate-900 bg-slate-900 text-white dark:border-slate-100 dark:bg-slate-100 dark:text-slate-900\"\n : \"border-slate-300 bg-white dark:border-slate-600 dark:bg-slate-900\",\n )}\n >\n {selected && <Check className=\"size-3\" />}\n </span>\n )}\n {o.icon && <span className=\"shrink-0\">{o.icon}</span>}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-slate-900 dark:text-slate-100\">{o.label}</span>\n {o.sublabel && (\n <span className=\"block truncate text-xs text-slate-400 dark:text-slate-500\">\n {o.sublabel}\n </span>\n )}\n </span>\n {!multi && selected && <Check className=\"size-4 shrink-0 text-sky-600 dark:text-sky-400\" />}\n </button>\n </li>\n </Fragment>\n );\n })}\n {showCreate && (\n <li role=\"option\" aria-selected={active === results.length}>\n <button\n type=\"button\"\n onMouseDown={(e) => {\n e.preventDefault();\n onCreate();\n }}\n onMouseEnter={() => setActive(results.length)}\n className={cn(\n isPhone\n ? SHEET_ROW_CLASS\n : \"flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm\",\n \"text-slate-700 dark:text-slate-200\",\n active === results.length && !isPhone\n ? \"bg-slate-100 dark:bg-slate-800\"\n : !isPhone && \"hover:bg-slate-50 dark:hover:bg-slate-800\",\n )}\n >\n <Plus className=\"size-4 shrink-0\" />\n <span className=\"truncate\">{createContent}</span>\n </button>\n </li>\n )}\n </ul>\n );\n\n if (isPhone) {\n return (\n <PickerSheet\n open\n onClose={() => core.setOpen(false)}\n title={sheetTitle}\n query={query}\n onQueryChange={setQuery}\n searchPlaceholder={searchPlaceholder}\n inputRef={inputRef}\n closeLabel={closeLabel}\n >\n <div onKeyDown={onKeyDown}>{list}</div>\n </PickerSheet>\n );\n }\n\n if (!rect) return null;\n\n return createPortal(\n <div\n ref={panelRef}\n onKeyDown={onKeyDown}\n // `placement` keeps the panel inside the region actually on screen: on a phone\n // the search box below pulls up the keyboard, and a panel pinned under a\n // low trigger would otherwise sit entirely behind it (feedback #135).\n style={{\n position: \"fixed\",\n top: placement.top,\n left: rect.left,\n width: rect.width,\n minWidth: 220,\n maxHeight: placement.maxHeight,\n }}\n className=\"z-50 flex flex-col rounded-md border border-slate-200 bg-white shadow-lg dark:border-slate-700 dark:bg-slate-900\"\n >\n <DropdownSearchHeader\n query={query}\n onQueryChange={setQuery}\n inputRef={inputRef}\n placeholder={searchPlaceholder}\n />\n {/* One list, two containers: the desktop panel and the phone sheet render\n the same rows through the same handlers (live #200). */}\n {list}\n </div>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/combobox-core.tsx"],"sourcesContent":["import { Fragment, useEffect, useMemo, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type {\n ComponentPropsWithoutRef,\n Dispatch,\n KeyboardEvent as ReactKeyboardEvent,\n ReactNode,\n RefObject,\n SetStateAction,\n} from \"react\";\nimport { Check, Plus } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { DropdownSearchHeader } from \"./dropdown\";\nimport { PickerSheet, SHEET_ROW_CLASS } from \"./picker-sheet\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { PHONE_QUERY } from \"./ui\";\nimport { type AnchorRect } from \"../hooks/use-anchored-rect\";\nimport { useAnchoredPanel, type AnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useEscapeKey, useOutsideClick } from \"../hooks/use-dismiss\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\nexport interface ComboOption<V extends string | number> {\n value: V;\n label: string;\n sublabel?: string;\n icon?: ReactNode;\n /**\n * Section this option belongs to. Where a consumer renders it as a heading with the\n * members indented beneath — {@link InlineEntityCombobox} does — this is a genuine\n * grouping, not decoration: the name appears once per section rather than trailing\n * every row in small grey type, which is barely legible on a phone (feedback #136).\n * Searchable like `sublabel`.\n */\n group?: string;\n}\n\nexport interface ComboboxCoreOptions<V extends string | number> {\n /** Already-loaded options (client-side filtered), also used to resolve labels. */\n options?: ComboOption<V>[];\n /** Async option source, debounced and race-safe; stale responses are ignored. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** External loading flag, OR-ed with the internal async state. */\n loading?: boolean;\n}\n\nexport interface ComboboxCore<V extends string | number> {\n triggerRef: RefObject<HTMLButtonElement | null>;\n panelRef: RefObject<HTMLDivElement | null>;\n inputRef: RefObject<HTMLInputElement | null>;\n open: boolean;\n setOpen: Dispatch<SetStateAction<boolean>>;\n query: string;\n setQuery: Dispatch<SetStateAction<string>>;\n active: number;\n setActive: Dispatch<SetStateAction<number>>;\n results: ComboOption<V>[];\n busy: boolean;\n rect: AnchorRect | null;\n /** Where the dropdown goes, clamped to the visible viewport (feedback #135). */\n placement: AnchoredPanel;\n cacheRef: RefObject<Map<V, ComboOption<V>>>;\n resolve: (v: V) => ComboOption<V> | null;\n /** Close and hand focus back to the trigger — the keyboard paths only. See\n * {@link useDropdown} for why a pointer dismissal must not do this. */\n closeToTrigger: () => void;\n}\n\n/**\n * Shared plumbing for the entity pickers: open/query/active state, portalled\n * anchoring + outside-click/Escape dismissal, focus-on-open, a debounced\n * race-safe async (or client-side) result list, and a label cache so a selected\n * value still renders its label after the async list has moved on. The single-\n * and multi-value comboboxes render their triggers on top of this and share\n * {@link ComboboxPanel} for the dropdown.\n */\nexport function useComboboxCore<V extends string | number>({\n options,\n loadOptions,\n loading,\n}: ComboboxCoreOptions<V>): ComboboxCore<V> {\n const triggerRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [active, setActive] = useState(0);\n\n const isAsync = typeof loadOptions === \"function\";\n const [asyncResults, setAsyncResults] = useState<ComboOption<V>[]>([]);\n const [asyncLoading, setAsyncLoading] = useState(false);\n const loadRef = useRef(loadOptions);\n loadRef.current = loadOptions;\n const reqId = useRef(0);\n\n const close = () => setOpen(false);\n // Escape unmounts the panel the user is typing in; without this the browser drops\n // focus on <body>, so the caret disappears and the next Tab restarts at the top of\n // the page. A pointer dismissal deliberately does NOT do this — the press has\n // already decided where focus belongs.\n const closeToTrigger = () => {\n setOpen(false);\n triggerRef.current?.focus();\n };\n // Placement, not just the anchor rect: the panel has to dodge the on-screen\n // keyboard its own search box summons (feedback #135).\n const placement = useAnchoredPanel(triggerRef, open);\n const rect = placement.rect;\n // Outside-click dismissal belongs to the ANCHORED panel, and only to it. On a\n // phone `ComboboxPanel` renders a full-screen `PickerSheet` instead (live #200),\n // portalled to <body> and carrying its own ways out — the close X, Escape, and\n // Back through `useOverlayHistory` — so there is no outside left to press.\n //\n // Leaving the listener on there was not merely redundant, it was the bug:\n // `panelRef` is attached to the desktop panel alone, so with the sheet up every\n // press inside it answered \"outside\". While `useOutsideClick` listened for\n // `mousedown` the rows survived by accident — they commit on `onMouseDown` too,\n // and won the race — but `pointerdown` PRECEDES `mousedown`, so the sheet closed\n // at finger-down and the commit landed on an unmounted tree: a tapped row kept the\n // old value, and tapping the search box or the X was equally fatal. `PickerSheet`\n // stops `mousedown` for exactly this reason, but that guard was written for\n // `useDropdown`'s listener and never covered this hook.\n const isPhone = useMediaQuery(PHONE_QUERY, false);\n useOutsideClick([triggerRef, panelRef], close, open && !isPhone);\n useEscapeKey(closeToTrigger, open);\n\n // Reset the query + focus the search box each time the panel opens.\n useEffect(() => {\n if (!open) return;\n setQuery(\"\");\n const id = requestAnimationFrame(() => inputRef.current?.focus());\n return () => cancelAnimationFrame(id);\n }, [open]);\n\n // Debounced, race-safe async search (only when loadOptions is provided).\n useEffect(() => {\n if (!open || !isAsync) return;\n const id = ++reqId.current;\n setAsyncLoading(true);\n const run = async () => {\n try {\n const r = await loadRef.current!(query);\n if (reqId.current === id) setAsyncResults(r);\n } finally {\n if (reqId.current === id) setAsyncLoading(false);\n }\n };\n const t = setTimeout(run, query ? 150 : 0);\n return () => clearTimeout(t);\n }, [query, open, isAsync]);\n\n const clientResults = useMemo(() => {\n if (isAsync) return [];\n const src = options ?? [];\n const q = query.trim().toLowerCase();\n if (!q) return src;\n return src.filter(\n (o) => o.label.toLowerCase().includes(q) || (o.sublabel ?? \"\").toLowerCase().includes(q),\n );\n }, [isAsync, options, query]);\n\n const results = isAsync ? asyncResults : clientResults;\n const busy = Boolean(loading) || (isAsync && asyncLoading);\n\n // Accumulate every option we've seen so a selected value can render its label\n // even after the async list has moved on to other query results.\n const cacheRef = useRef(new Map<V, ComboOption<V>>());\n useEffect(() => {\n for (const o of options ?? []) cacheRef.current.set(o.value, o);\n for (const o of results) cacheRef.current.set(o.value, o);\n }, [options, results]);\n const resolve = (v: V): ComboOption<V> | null =>\n options?.find((o) => o.value === v) ?? cacheRef.current.get(v) ?? null;\n\n // Reset the highlighted row whenever the visible set changes.\n useEffect(() => {\n setActive(0);\n }, [query, open]);\n\n return {\n triggerRef,\n panelRef,\n inputRef,\n open,\n setOpen,\n query,\n setQuery,\n active,\n setActive,\n results,\n busy,\n rect,\n placement,\n cacheRef,\n resolve,\n closeToTrigger,\n };\n}\n\n/**\n * Keep the option the keyboard is on inside the visible part of the list.\n *\n * `aria-activedescendant` moves a highlight WITHOUT moving focus, which is the whole\n * point of it — and the cost is that the browser scrolls nothing on its own. In a\n * panel capped at a few hundred pixels over an account list two hundred long, Down\n * walked the highlight straight out of the box and the list sat still.\n *\n * Optional-called because jsdom does not implement `scrollIntoView`: three consumers\n * run this component in their own suites, and a picker that throws there is a worse\n * trade than a list that does not scroll in a test.\n */\nexport function useActiveOptionScroll(activeId: string | undefined): void {\n useEffect(() => {\n if (!activeId) return;\n document.getElementById(activeId)?.scrollIntoView?.({ block: \"nearest\" });\n }, [activeId]);\n}\n\nexport interface ComboboxPanelProps<V extends string | number>\n extends ComponentPropsWithoutRef<\"div\"> {\n core: ComboboxCore<V>;\n /** What the trigger's `aria-controls` names. Owned by the component that renders\n * the trigger, not by the core: one id has to reach both, and only one of them\n * can be the origin. */\n listboxId: string;\n /** Default: `combobox.search` from the {@link UiKitProvider}, else English. */\n searchPlaceholder?: string;\n /** Default: `combobox.noResults` from the {@link UiKitProvider}, else English. */\n emptyLabel?: string;\n multi?: boolean;\n isSelected: (v: V) => boolean;\n onChoose: (o: ComboOption<V>) => void;\n showCreate: boolean;\n onCreate: () => void;\n createContent: ReactNode;\n /** What the sheet calls itself on a phone — the field's label. Ignored on\n * desktop, where the panel sits under the field that already says it. */\n sheetTitle?: ReactNode;\n closeLabel?: string;\n}\n\n/**\n * The portalled dropdown (search header + result rows + optional create row)\n * shared by the single- and multi-value comboboxes. Left-aligned to the trigger\n * and sized to its width; `multi` swaps the trailing check for a leading\n * checkbox. `onChoose` decides whether to close (single) or stay open (multi).\n *\n * ## The ARIA, which is `CommandPalette`'s\n *\n * This rendered a `role=\"listbox\"` of `role=\"option\"` rows and stopped there, so a\n * reader was handed a text box and silence: the highlight the arrow keys move lived\n * only in a background colour, and nothing tied the box being typed in to the list\n * being filtered. `CommandPalette` has had the whole model since it was written —\n * `aria-controls` from the field to the list, `aria-activedescendant` naming the row\n * the keyboard is on — and it was the only thing in the package that did.\n *\n * Two distinct states, deliberately not conflated:\n * - `aria-selected` is CHOSEN. It is the check mark, and in `multi` the checkbox.\n * - `aria-activedescendant` is where the KEYBOARD is. Announcing that as selected\n * would tell a reader that the row they are merely passing over is the answer.\n *\n * `role=\"option\"` sits on the `<button>`, not on the `<li>` around it, for the same\n * reason it does in `CommandPalette`: an option may not contain a separately\n * focusable control, and the button is the thing a pointer presses. The rows are\n * `tabIndex={-1}` because focus never leaves the search box — that is what\n * `aria-activedescendant` is FOR.\n */\nexport function ComboboxPanel<V extends string | number>({\n core,\n listboxId,\n searchPlaceholder,\n emptyLabel,\n multi,\n isSelected,\n onChoose,\n showCreate,\n onCreate,\n createContent,\n sheetTitle,\n closeLabel,\n className,\n style,\n ...rest\n}: ComboboxPanelProps<V>) {\n const {\n rect,\n placement,\n panelRef,\n inputRef,\n query,\n setQuery,\n results,\n busy,\n active,\n setActive,\n closeToTrigger,\n } = core;\n // The phone gets a full-screen sheet instead of an anchored panel (live #200).\n // Same core, same results, same handlers — only the container differs, so the\n // two presentations cannot drift in what they offer.\n const isPhone = useMediaQuery(PHONE_QUERY, false);\n const rowCount = results.length + (showCreate ? 1 : 0);\n const optionId = (index: number) => `${listboxId}-option-${index}`;\n const activeId = active >= 0 && active < rowCount ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n // The consumers resolve their own props against the provider already; this is\n // for the loading row, which no consumer names, and for a caller that renders the\n // panel directly and leaves the two optional strings out.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS);\n\n // The WAI-ARIA APG's listbox keyboard, in full: Up/Down step, Home/End jump, Enter\n // commits, Tab leaves. Escape is the one key handled elsewhere — `useComboboxCore`\n // registers it on the document, so it answers wherever focus has ended up.\n const onKeyDown = (e: ReactKeyboardEvent) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((i) => Math.min(i + 1, rowCount - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Home\") {\n e.preventDefault();\n setActive(0);\n } else if (e.key === \"End\") {\n e.preventDefault();\n setActive(rowCount - 1);\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n if (active < results.length) onChoose(results[active]);\n else if (showCreate) onCreate();\n } else if (e.key === \"Tab\") {\n // The panel is PORTALLED to <body>, so the browser's own Tab would move from\n // here to whatever follows the portal — i.e. off the end of the document. The\n // trade is one extra Tab (trigger, then onward) against never stranding focus\n // at the bottom of the page.\n e.preventDefault();\n closeToTrigger();\n }\n };\n\n if (!core.open || typeof document === \"undefined\") return null;\n\n const list = (\n <ul\n id={listboxId}\n role=\"listbox\"\n aria-multiselectable={multi}\n className={cn(\"min-h-0 flex-1 overflow-y-auto py-1\", isPhone && \"flex-none\")}\n >\n {busy && results.length === 0 && (\n // The ellipsis is the picture; the words are for a reader, who otherwise met\n // an empty-looking list with a lone \"…\" in it — or, in most readers, nothing\n // at all, since punctuation alone is skipped. `relative` so the sr-only text\n // has a local containing block (see sr-only-containment.test).\n <li className=\"relative px-3 py-2 text-sm text-[var(--text-muted)]\">\n <span aria-hidden>…</span>\n <span className=\"sr-only\">{labels.loading}</span>\n </li>\n )}\n {!busy && results.length === 0 && !showCreate && (\n <li className=\"px-3 py-2 text-sm text-[var(--text-muted)]\">\n {emptyLabel ?? labels.noResults}\n </li>\n )}\n {results.map((o, i) => {\n const selected = isSelected(o.value);\n // One heading per group rather than a grey suffix on every row (#136), and\n // the thing the native <select>'s <optgroup> used to give the account\n // picker for free (live #200). `results` keeps its source order, so a\n // boundary is simply \"different from the row above\".\n const startsGroup = o.group != null && o.group !== results[i - 1]?.group;\n return (\n // Keyed by group AND value: an option may deliberately appear twice —\n // Keksdose repeats recently-used categories in a \"Recent\" group at the\n // top (live #203) — and a bare value key would collide.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {startsGroup && (\n <li\n role=\"presentation\"\n className={cn(\n \"font-semibold uppercase tracking-wide text-[var(--text-muted)]\",\n isPhone ? \"px-4 pb-1 pt-3 text-xs\" : \"px-3 pb-0.5 pt-2 text-[11px] first:pt-1\",\n )}\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n role=\"option\"\n aria-selected={selected}\n tabIndex={-1}\n onMouseDown={(e) => {\n e.preventDefault();\n onChoose(o);\n }}\n onMouseEnter={() => setActive(i)}\n className={cn(\n isPhone\n ? SHEET_ROW_CLASS\n : \"flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm\",\n i === active && !isPhone\n ? \"bg-[var(--bg-active)]\"\n : !isPhone && \"hover:bg-[var(--bg-hover)]\",\n )}\n >\n {multi && (\n <span\n // The checkbox is a PICTURE of `aria-selected` above, so a reader\n // that met both would be told twice.\n aria-hidden\n className={cn(\n \"flex size-4 shrink-0 items-center justify-center rounded border\",\n selected\n ? \"border-[var(--bg-inverse)] bg-[var(--bg-inverse)] text-[var(--text-inverse)]\"\n : \"border-[var(--border-strong)] bg-[var(--bg-surface)]\",\n )}\n >\n {selected && <Check className=\"size-3\" />}\n </span>\n )}\n {o.icon && <span className=\"shrink-0\">{o.icon}</span>}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-[var(--text-primary)]\">{o.label}</span>\n {o.sublabel && (\n <span className=\"block truncate text-xs text-[var(--text-placeholder)]\">\n {o.sublabel}\n </span>\n )}\n </span>\n {!multi && selected && <Check className=\"size-4 shrink-0 text-[var(--brand)]\" />}\n </button>\n </li>\n </Fragment>\n );\n })}\n {showCreate && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(results.length)}\n role=\"option\"\n // Never chosen — it is an action, and it disappears the moment it has\n // been taken. Whether the keyboard is ON it is `aria-activedescendant`'s\n // job to say.\n aria-selected={false}\n tabIndex={-1}\n onMouseDown={(e) => {\n e.preventDefault();\n onCreate();\n }}\n onMouseEnter={() => setActive(results.length)}\n className={cn(\n isPhone\n ? SHEET_ROW_CLASS\n : \"flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm\",\n \"text-[var(--text-secondary)]\",\n active === results.length && !isPhone\n ? \"bg-[var(--bg-active)]\"\n : !isPhone && \"hover:bg-[var(--bg-hover)]\",\n )}\n >\n <Plus className=\"size-4 shrink-0\" />\n <span className=\"truncate\">{createContent}</span>\n </button>\n </li>\n )}\n </ul>\n );\n\n if (isPhone) {\n return (\n <PickerSheet\n // First, so the sheet's own props below win: `rest` is typed as div\n // attributes, and the DOM's `title` is a tooltip string where the sheet's is\n // its heading — one name, two meanings, and the sheet's is the one the caller\n // asked for.\n {...rest}\n className={className}\n style={style}\n open\n onClose={() => core.setOpen(false)}\n title={sheetTitle}\n query={query}\n onQueryChange={setQuery}\n searchPlaceholder={searchPlaceholder ?? labels.search}\n inputRef={inputRef}\n closeLabel={closeLabel}\n >\n <div onKeyDown={onKeyDown}>{list}</div>\n </PickerSheet>\n );\n }\n\n if (!rect) return null;\n\n return createPortal(\n <div\n // Spread FIRST: `onKeyDown` below is the listbox keyboard (Up/Down/Home/End/\n // Enter/Tab) and the placement is measured rather than chosen, so neither is a\n // caller's to replace by accident — while a `data-*` anchor or an\n // `aria-describedby` has nothing to collide with.\n {...rest}\n ref={panelRef}\n onKeyDown={onKeyDown}\n // `placement` keeps the panel inside the region actually on screen: on a phone\n // the search box below pulls up the keyboard, and a panel pinned under a\n // low trigger would otherwise sit entirely behind it (feedback #135).\n style={{\n ...style,\n position: \"fixed\",\n top: placement.top,\n left: rect.left,\n width: rect.width,\n minWidth: 220,\n maxHeight: placement.maxHeight,\n }}\n className={cn(\n \"z-50 flex flex-col rounded-md border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg\",\n className,\n )}\n >\n <DropdownSearchHeader\n query={query}\n onQueryChange={setQuery}\n inputRef={inputRef}\n placeholder={searchPlaceholder ?? labels.search}\n // Focus is in this box while the list is up, so this is what has to name the\n // list and the row the arrows are on.\n listboxId={listboxId}\n activeId={activeId}\n />\n {/* One list, two containers: the desktop panel and the phone sheet render\n the same rows through the same handlers (live #200). */}\n {list}\n </div>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
3
|
import { ComboOption } from './combobox-core.js';
|
|
4
4
|
import '../hooks/use-anchored-rect.js';
|
|
5
5
|
import '../hooks/use-anchored-panel.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Use this instead of an `<Input list="…">` + `<datalist>` so the control is
|
|
14
|
-
* visually and behaviourally consistent with the other dropdowns (feedback
|
|
15
|
-
* #230 — the payee field looked/behaved differently from every other select).
|
|
8
|
+
* Four of the div's own attributes are omitted because this component already owns
|
|
9
|
+
* the name, with a different meaning: `id` is the INPUT's (a caller labels or
|
|
10
|
+
* automates the field, not the box around it), and `onChange`/`onBlur`/`onSubmit`
|
|
11
|
+
* are the field's — a value, a departure, and "I mean what I typed" — rather than
|
|
12
|
+
* the DOM events of the wrapper. Everything else a wrapper needs reaches the root.
|
|
16
13
|
*/
|
|
17
|
-
|
|
14
|
+
interface ComboboxProps extends Omit<ComponentPropsWithoutRef<"div">, "id" | "onChange" | "onBlur" | "onSubmit"> {
|
|
18
15
|
value: string;
|
|
19
16
|
onChange: (v: string) => void;
|
|
20
17
|
/** Suggestion pool (e.g. existing payee names). */
|
|
@@ -59,7 +56,6 @@ declare function Combobox({ value, onChange, options, label, id, placeholder, cl
|
|
|
59
56
|
/** Enter pressed with no row highlighted, i.e. "I mean what I typed". The list
|
|
60
57
|
* closes either way; this is for a caller whose Enter also submits a row editor. */
|
|
61
58
|
onSubmit?: () => void;
|
|
62
|
-
"aria-label"?: string;
|
|
63
59
|
/** Rows to offer when the field is empty. 8 on a dropdown, where that is all
|
|
64
60
|
* that fits; the phone sheet asks for more because it has a screen. */
|
|
65
61
|
maxSuggestions?: number;
|
|
@@ -81,26 +77,21 @@ declare function Combobox({ value, onChange, options, label, id, placeholder, cl
|
|
|
81
77
|
* values are all supposed to already exist.
|
|
82
78
|
*/
|
|
83
79
|
createLabel?: (value: string) => string;
|
|
84
|
-
}
|
|
80
|
+
}
|
|
85
81
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* panel picker ({@link EntityCombobox}) never matches an input field exactly.
|
|
91
|
-
*
|
|
92
|
-
* Text-vs-value reconciliation: the input's text is transient. Picking a row or
|
|
93
|
-
* typing an exact (unique) label commits that option; emptying the text commits
|
|
94
|
-
* a clear; anything else reverts to the selected option's label on blur/Escape.
|
|
82
|
+
* Free-text combobox that looks like the shared {@link Select} (same FIELD_BASE
|
|
83
|
+
* styling, chevron and static floating label) but lets the user type a value
|
|
84
|
+
* that isn't in the list — e.g. naming a brand-new payee. Picking from the
|
|
85
|
+
* filtered suggestion list fills the value; typing keeps whatever was entered.
|
|
95
86
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* rather than reusing `text`: `text` is what {@link reconcile} judges on blur, so a
|
|
100
|
-
* sheet that emptied it to show the full list would read as "the user cleared the
|
|
101
|
-
* field" the moment it closed.
|
|
87
|
+
* Use this instead of an `<Input list="…">` + `<datalist>` so the control is
|
|
88
|
+
* visually and behaviourally consistent with the other dropdowns (feedback
|
|
89
|
+
* #230 — the payee field looked/behaved differently from every other select).
|
|
102
90
|
*/
|
|
103
|
-
declare function
|
|
91
|
+
declare function Combobox({ value, onChange, options, label, id, placeholder, className, groupBy, maxSuggestions, searchPlaceholder, closeLabel, createLabel, invalid, optionAdornment, autoFocus, onBlur, onSubmit, "aria-label": ariaLabel, ...rest }: ComboboxProps): react.JSX.Element;
|
|
92
|
+
/** `id` is the INPUT's and `onChange` is the field's — see {@link ComboboxProps}
|
|
93
|
+
* for why the div's spellings of them are omitted. */
|
|
94
|
+
interface InlineEntityComboboxProps<V extends string | number> extends Omit<ComponentPropsWithoutRef<"div">, "id" | "onChange"> {
|
|
104
95
|
/** Selected option id, or null when nothing is selected. */
|
|
105
96
|
value: V | null;
|
|
106
97
|
/** Required and unanswered — see {@link Combobox}'s `invalid`. */
|
|
@@ -119,10 +110,11 @@ declare function InlineEntityCombobox<V extends string | number>({ value, onChan
|
|
|
119
110
|
* without a passthrough this component's fixed prop list puts the `<input>`
|
|
120
111
|
* out of a caller's reach entirely (Keksdose feedback live #218). */
|
|
121
112
|
autoFocus?: boolean;
|
|
122
|
-
/** Phone sheet only — the anchored list has the field itself to type in.
|
|
113
|
+
/** Phone sheet only — the anchored list has the field itself to type in. Falls
|
|
114
|
+
* back to `placeholder`, then to `combobox.search` from the {@link UiKitProvider}. */
|
|
123
115
|
searchPlaceholder?: string;
|
|
124
116
|
/** Phone sheet only: a full screen showing nothing has to say why. The anchored
|
|
125
|
-
* list simply does not open. */
|
|
117
|
+
* list simply does not open. Default: `combobox.noResults` from the provider. */
|
|
126
118
|
emptyLabel?: string;
|
|
127
119
|
closeLabel?: string;
|
|
128
120
|
/** Offer a clear "×" in place of the chevron whenever something is selected.
|
|
@@ -135,7 +127,25 @@ declare function InlineEntityCombobox<V extends string | number>({ value, onChan
|
|
|
135
127
|
* affordance both shells share, and it clears without opening anything. */
|
|
136
128
|
clearable?: boolean;
|
|
137
129
|
clearLabel?: string;
|
|
138
|
-
|
|
139
|
-
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The id-keyed sibling of {@link Combobox}: identical anatomy (a real text input
|
|
133
|
+
* with the shared field styling, chevron and inline as-you-type filtering) but
|
|
134
|
+
* the value is an option id, not free text. Built so an entity picker can sit
|
|
135
|
+
* next to a free-text combobox without any visible difference — a button-trigger
|
|
136
|
+
* panel picker ({@link EntityCombobox}) never matches an input field exactly.
|
|
137
|
+
*
|
|
138
|
+
* Text-vs-value reconciliation: the input's text is transient. Picking a row or
|
|
139
|
+
* typing an exact (unique) label commits that option; emptying the text commits
|
|
140
|
+
* a clear; anything else reverts to the selected option's label on blur/Escape.
|
|
141
|
+
*
|
|
142
|
+
* **On a phone the list opens as a full-screen sheet**, exactly like {@link Combobox}
|
|
143
|
+
* beside it (Keksdose live #200, extended by dev#477 — the account picker moved onto
|
|
144
|
+
* this component and must not lose its sheet). The sheet carries its OWN query state
|
|
145
|
+
* rather than reusing `text`: `text` is what {@link reconcile} judges on blur, so a
|
|
146
|
+
* sheet that emptied it to show the full list would read as "the user cleared the
|
|
147
|
+
* field" the moment it closed.
|
|
148
|
+
*/
|
|
149
|
+
declare function InlineEntityCombobox<V extends string | number>({ value, onChange, options, label, id, placeholder, className, disabled, autoFocus, searchPlaceholder, emptyLabel, closeLabel, clearable, clearLabel, invalid, "aria-label": ariaLabel, ...rest }: InlineEntityComboboxProps<V>): react.JSX.Element;
|
|
140
150
|
|
|
141
|
-
export { Combobox, InlineEntityCombobox };
|
|
151
|
+
export { Combobox, type ComboboxProps, InlineEntityCombobox, type InlineEntityComboboxProps };
|