@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,6 +1,26 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode, RefObject } from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode, RefObject } from 'react';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Two of the div's own attributes are omitted because this component already owns
|
|
6
|
+
* the name: `title` here is the sheet's HEADING (and a ReactNode), not the browser's
|
|
7
|
+
* tooltip string, and `onClose` is "the sheet was dismissed", not the DOM's
|
|
8
|
+
* `<dialog>` close event. Everything else a caller can pass reaches the sheet.
|
|
9
|
+
*/
|
|
10
|
+
interface PickerSheetProps extends Omit<ComponentPropsWithoutRef<"div">, "title" | "onClose"> {
|
|
11
|
+
open: boolean;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
/** The field's own label — a sheet that fills the screen has to say what it is
|
|
14
|
+
* asking for, which the anchored panel got for free by sitting under it. */
|
|
15
|
+
title?: ReactNode;
|
|
16
|
+
query: string;
|
|
17
|
+
onQueryChange: (value: string) => void;
|
|
18
|
+
searchPlaceholder?: string;
|
|
19
|
+
inputRef?: RefObject<HTMLInputElement | null>;
|
|
20
|
+
/** Default: `pickerSheet.close` from the {@link UiKitProvider}, else "Close". */
|
|
21
|
+
closeLabel?: string;
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}
|
|
4
24
|
/**
|
|
5
25
|
* The PHONE presentation of a picker: a full-screen dialog with a search box at
|
|
6
26
|
* the top and the list filling everything below it (Keksdose live #200).
|
|
@@ -24,21 +44,9 @@ import { ReactNode, RefObject } from 'react';
|
|
|
24
44
|
* scrolls, the header does not, and the body behind it is scroll-locked so a drag
|
|
25
45
|
* that overshoots the list does not move the page underneath.
|
|
26
46
|
*/
|
|
27
|
-
declare function PickerSheet({ open, onClose, title, query, onQueryChange, searchPlaceholder, inputRef, closeLabel, children, }:
|
|
28
|
-
open: boolean;
|
|
29
|
-
onClose: () => void;
|
|
30
|
-
/** The field's own label — a sheet that fills the screen has to say what it is
|
|
31
|
-
* asking for, which the anchored panel got for free by sitting under it. */
|
|
32
|
-
title?: ReactNode;
|
|
33
|
-
query: string;
|
|
34
|
-
onQueryChange: (value: string) => void;
|
|
35
|
-
searchPlaceholder?: string;
|
|
36
|
-
inputRef?: RefObject<HTMLInputElement | null>;
|
|
37
|
-
closeLabel?: string;
|
|
38
|
-
children: ReactNode;
|
|
39
|
-
}): react.ReactPortal | null;
|
|
47
|
+
declare function PickerSheet({ open, onClose, title, query, onQueryChange, searchPlaceholder, inputRef, closeLabel, children, "aria-label": ariaLabel, className, style, ...rest }: PickerSheetProps): react.ReactPortal | null;
|
|
40
48
|
/** A row inside a {@link PickerSheet} — the phone-sized version of a dropdown row,
|
|
41
49
|
* so a list is comfortable to hit with a thumb rather than merely legible. */
|
|
42
50
|
declare const SHEET_ROW_CLASS: string;
|
|
43
51
|
|
|
44
|
-
export { PickerSheet, SHEET_ROW_CLASS };
|
|
52
|
+
export { PickerSheet, type PickerSheetProps, SHEET_ROW_CLASS };
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
+
import { useRef } from "react";
|
|
3
4
|
import { createPortal } from "react-dom";
|
|
4
5
|
import { X } from "lucide-react";
|
|
5
6
|
import { cn } from "../lib/cn.js";
|
|
6
7
|
import { DropdownSearchHeader } from "./dropdown.js";
|
|
7
8
|
import { useBodyScrollLock } from "../hooks/use-body-scroll-lock.js";
|
|
8
9
|
import { useOverlayHistory } from "../hooks/use-overlay-history.js";
|
|
10
|
+
import { useFocusTrap } from "../hooks/use-focus-trap.js";
|
|
9
11
|
import { useVisualViewport } from "../hooks/use-anchored-panel.js";
|
|
12
|
+
import { DEFAULT_PICKER_SHEET_LABELS, useKitLabels } from "../i18n/kit-labels.js";
|
|
10
13
|
function PickerSheet({
|
|
11
14
|
open,
|
|
12
15
|
onClose,
|
|
@@ -15,38 +18,55 @@ function PickerSheet({
|
|
|
15
18
|
onQueryChange,
|
|
16
19
|
searchPlaceholder,
|
|
17
20
|
inputRef,
|
|
18
|
-
closeLabel
|
|
19
|
-
children
|
|
21
|
+
closeLabel,
|
|
22
|
+
children,
|
|
23
|
+
"aria-label": ariaLabel,
|
|
24
|
+
className,
|
|
25
|
+
style,
|
|
26
|
+
...rest
|
|
20
27
|
}) {
|
|
28
|
+
const labels = useKitLabels("pickerSheet", DEFAULT_PICKER_SHEET_LABELS, { close: closeLabel });
|
|
21
29
|
useBodyScrollLock(open);
|
|
22
30
|
useOverlayHistory(open, onClose);
|
|
23
31
|
const vv = useVisualViewport(open);
|
|
24
32
|
const ownRef = useRef(null);
|
|
25
33
|
const searchRef = inputRef ?? ownRef;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
const sheetRef = useRef(null);
|
|
35
|
+
useFocusTrap(sheetRef, {
|
|
36
|
+
active: open,
|
|
37
|
+
restoreFocus: false,
|
|
38
|
+
initialFocus: () => searchRef.current
|
|
39
|
+
});
|
|
40
|
+
const handleKeyDown = (e) => {
|
|
41
|
+
if (e.key !== "Escape") return;
|
|
42
|
+
e.stopPropagation();
|
|
43
|
+
onClose();
|
|
44
|
+
};
|
|
29
45
|
if (!open || typeof document === "undefined") return null;
|
|
30
46
|
return createPortal(
|
|
31
47
|
/* @__PURE__ */ jsxs(
|
|
32
48
|
"div",
|
|
33
49
|
{
|
|
50
|
+
...rest,
|
|
51
|
+
ref: sheetRef,
|
|
34
52
|
role: "dialog",
|
|
35
53
|
"aria-modal": "true",
|
|
36
|
-
|
|
54
|
+
tabIndex: -1,
|
|
55
|
+
onKeyDown: handleKeyDown,
|
|
56
|
+
"aria-label": ariaLabel ?? (typeof title === "string" ? title : void 0),
|
|
37
57
|
onMouseDown: (e) => e.stopPropagation(),
|
|
38
|
-
style: vv ? { top: vv.top, height: vv.height, bottom: "auto" } :
|
|
39
|
-
className: "fixed inset-0 z-50 flex flex-col bg-
|
|
58
|
+
style: vv ? { ...style, top: vv.top, height: vv.height, bottom: "auto" } : style,
|
|
59
|
+
className: cn("fixed inset-0 z-50 flex flex-col bg-[var(--bg-surface)] outline-none", className),
|
|
40
60
|
children: [
|
|
41
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-
|
|
42
|
-
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm font-semibold text-
|
|
61
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-[var(--border)] px-3 py-2", children: [
|
|
62
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm font-semibold text-[var(--text-primary)]", children: title }),
|
|
43
63
|
/* @__PURE__ */ jsx(
|
|
44
64
|
"button",
|
|
45
65
|
{
|
|
46
66
|
type: "button",
|
|
47
|
-
"aria-label":
|
|
67
|
+
"aria-label": labels.close,
|
|
48
68
|
onClick: onClose,
|
|
49
|
-
className: "rounded p-1.5 text-
|
|
69
|
+
className: "rounded p-1.5 text-[var(--text-muted)] hover:bg-[var(--bg-hover)]",
|
|
50
70
|
children: /* @__PURE__ */ jsx(X, { className: "size-5" })
|
|
51
71
|
}
|
|
52
72
|
)
|
|
@@ -69,7 +89,7 @@ function PickerSheet({
|
|
|
69
89
|
}
|
|
70
90
|
const SHEET_ROW_CLASS = cn(
|
|
71
91
|
"flex min-h-11 w-full items-center gap-2 px-4 py-2 text-left text-base",
|
|
72
|
-
"text-
|
|
92
|
+
"text-[var(--text-primary)] hover:bg-[var(--bg-hover)]"
|
|
73
93
|
);
|
|
74
94
|
export {
|
|
75
95
|
PickerSheet,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/picker-sheet.tsx"],"sourcesContent":["import { useEffect, useRef } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { X } from \"lucide-react\";\nimport type { ReactNode, RefObject } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DropdownSearchHeader } from \"./dropdown\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { useVisualViewport } from \"../hooks/use-anchored-panel\";\n\n/**\n * The PHONE presentation of a picker: a full-screen dialog with a search box at\n * the top and the list filling everything below it (Keksdose live #200).\n *\n * *\"Paid as full screen dialog with input. Similar to the account select that\n * already appears as full screen.\"* The account field is a native `<select>`, and\n * a phone browser renders that as a full-screen list — so the app's own pickers,\n * which are anchored dropdown panels, were the odd ones out: a 320px-tall panel\n * squeezed between the field and the keyboard, showing three or four rows of a\n * list that might have two hundred entries.\n *\n * What this fixes beyond size: the panel had to be *placed* (above/below, tracking\n * the visual viewport as the keyboard opened — see `useAnchoredPanel`), and every\n * one of those decisions is a chance to be wrong on a screen this small. A sheet\n * has no placement. It also gives the search input somewhere unambiguous to live,\n * which is the other half of the request: *\"add filler possibility to the account\n * select or even better right to the hoc the account select is derived from\"* —\n * so this lives in the shared picker, not in one form's copy of it.\n *\n * Rows are `min-h-11` (44px): the touch target the rest of the app uses. The list\n * scrolls, the header does not, and the body behind it is scroll-locked so a drag\n * that overshoots the list does not move the page underneath.\n */\nexport function PickerSheet({\n open,\n onClose,\n title,\n query,\n onQueryChange,\n searchPlaceholder,\n inputRef,\n closeLabel = \"Close\",\n children,\n}: {\n open: boolean;\n onClose: () => void;\n /** The field's own label — a sheet that fills the screen has to say what it is\n * asking for, which the anchored panel got for free by sitting under it. */\n title?: ReactNode;\n query: string;\n onQueryChange: (value: string) => void;\n searchPlaceholder?: string;\n inputRef?: RefObject<HTMLInputElement | null>;\n closeLabel?: string;\n children: ReactNode;\n}) {\n useBodyScrollLock(open);\n /**\n * Back closes THE SHEET, not the dialog it was opened from (Keksdose live #309:\n * *\"Mouse Back does not only close the select but also the whole edit or create\n * dialog which is cumbersome.\"*).\n *\n * The sheet is a full-screen overlay over whatever opened it, and on a phone that\n * is almost always the row editor — which DOES push a history entry\n * (`data-table.tsx`). So the sheet was invisible to Back: one press popped the\n * editor's entry and took the whole dialog with it, discarding an edit in progress\n * in order to dismiss a list.\n *\n * `useOverlayHistory` was built for exactly this nesting — its stack unwinds\n * last-in-first-out, and the U-12 regression test in its own suite is written\n * around \"a Modal and a PickerSheet inside it\". The sheet simply never called it.\n *\n * Nothing else has to change: the hook is a no-op while `open` is false, and it\n * pops its own entry when the sheet closes some other way (the X, a row, the\n * backdrop) so history does not accumulate husks.\n */\n useOverlayHistory(open, onClose);\n // The visible region, while the sheet is up — see the `style` below for why a\n // full-screen sheet cannot simply be `inset-0` on a phone (live #328).\n const vv = useVisualViewport(open);\n // The sheet focuses its OWN search box when it opens (Keksdose live #212: *\"make\n // the text input filter active on select click\"*).\n //\n // The callers tried: `Combobox`'s field does `setOpen(true); sheetInputRef.current\n // ?.focus()` in one `onFocus`, so the focus call runs while the sheet is still\n // unmounted and the ref is null — and the chevron's `onMouseDown` preventDefaults\n // to keep focus on the field, so opening that way never even attempted it. Neither\n // is fixable from outside: only the sheet knows when its input exists.\n //\n // A fallback ref means a caller that does not need the handle still gets the\n // focus, rather than the behaviour depending on whether a prop was passed.\n const ownRef = useRef<HTMLInputElement | null>(null);\n const searchRef = inputRef ?? ownRef;\n useEffect(() => {\n if (open) searchRef.current?.focus();\n }, [open, searchRef]);\n\n if (!open || typeof document === \"undefined\") return null;\n return createPortal(\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={typeof title === \"string\" ? title : undefined}\n // The sheet is PORTALLED to <body>, so it is not inside the field's own\n // wrapper — and `useDropdown` closes on any document mousedown landing\n // outside that wrapper. Tapping a row therefore unmounted the row (open →\n // false) before its CLICK could fire, so the tap selected nothing at all and\n // the sheet just vanished (Keksdose dev#477).\n //\n // It bit whichever sheet commits on `onClick` — the payee field's, since live\n // #200, where it hid behind the free-text value the search box was already\n // setting; and the account field's the moment dev#477 moved it onto the inline\n // picker. `ComboboxPanel`'s rows happen to commit on `onMouseDown`, so they\n // beat the document listener and were never affected — which is luck, not a\n // design, and exactly why the fix belongs to the sheet.\n //\n // Stopping the mousedown here rather than teaching every caller about the\n // portal: the sheet is modal and full-screen, so \"a click in here is not an\n // outside click\" is a property of the sheet, not of whoever opened it.\n onMouseDown={(e) => e.stopPropagation()}\n // ⚠️ `inset-0` was the whole of Keksdose live #328: *\"When the keyboard overlays\n // the entries I cannot scroll past them … to see also the last entries. Happens\n // when browsing the account select.\"*\n //\n // This sheet FOCUSES ITS OWN SEARCH BOX on open (see above), so the keyboard is\n // up every single time it is used on a phone. On Android that shrinks the visual\n // viewport and leaves the LAYOUT viewport alone — `inset-0` is the layout\n // viewport — so the sheet kept its full height with its bottom third behind the\n // keyboard. The list below is `flex-1 overflow-y-auto`, so it sized itself to\n // that hidden height too: it scrolled to ITS end while the last rows were still\n // under the keys, and no gesture could bring them out. Nothing was clipped and\n // nothing looked broken, which is why it reads as \"I cannot scroll past them\".\n //\n // `visualViewport` is the API that answers rather than hints (the live #292\n // lesson): it reports the region actually on screen, keyboard and pinch-pan\n // included, and it fires on the keyboard opening and closing. Pinned to that box,\n // the sheet ends where the keys begin and the list's own scroll covers the rest.\n // `useVisualViewport` returns null when there is nothing to correct, and then\n // this is character-for-character the sheet that was here before.\n style={vv ? { top: vv.top, height: vv.height, bottom: \"auto\" } : undefined}\n className=\"fixed inset-0 z-50 flex flex-col bg-white dark:bg-slate-900\"\n >\n <div className=\"flex items-center gap-2 border-b border-slate-200 px-3 py-2 dark:border-slate-700\">\n <span className=\"min-w-0 flex-1 truncate text-sm font-semibold text-slate-900 dark:text-slate-100\">\n {title}\n </span>\n <button\n type=\"button\"\n aria-label={closeLabel}\n onClick={onClose}\n className=\"rounded p-1.5 text-slate-500 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-800\"\n >\n <X className=\"size-5\" />\n </button>\n </div>\n <DropdownSearchHeader\n query={query}\n onQueryChange={onQueryChange}\n inputRef={searchRef}\n placeholder={searchPlaceholder}\n />\n {/* min-h-0 so the LIST scrolls rather than the dialog growing past the\n viewport — a flex child defaults to min-height:auto. */}\n <div className=\"min-h-0 flex-1 overflow-y-auto overscroll-contain\">{children}</div>\n </div>,\n document.body,\n );\n}\n\n/** A row inside a {@link PickerSheet} — the phone-sized version of a dropdown row,\n * so a list is comfortable to hit with a thumb rather than merely legible. */\nexport const SHEET_ROW_CLASS = cn(\n \"flex min-h-11 w-full items-center gap-2 px-4 py-2 text-left text-base\",\n \"text-slate-900 hover:bg-slate-50 dark:text-slate-100 dark:hover:bg-slate-800\",\n);\n"],"mappings":"AA8IM,SACE,KADF;AA9IN,SAAS,WAAW,cAAc;AAClC,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,UAAU;AACnB,SAAS,4BAA4B;AACrC,SAAS,yBAAyB;AAClC,SAAS,yBAAyB;AAClC,SAAS,yBAAyB;AAyB3B,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AACF,GAYG;AACD,oBAAkB,IAAI;AAoBtB,oBAAkB,MAAM,OAAO;AAG/B,QAAM,KAAK,kBAAkB,IAAI;AAYjC,QAAM,SAAS,OAAgC,IAAI;AACnD,QAAM,YAAY,YAAY;AAC9B,YAAU,MAAM;AACd,QAAI,KAAM,WAAU,SAAS,MAAM;AAAA,EACrC,GAAG,CAAC,MAAM,SAAS,CAAC;AAEpB,MAAI,CAAC,QAAQ,OAAO,aAAa,YAAa,QAAO;AACrD,SAAO;AAAA,IACL;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,cAAW;AAAA,QACX,cAAY,OAAO,UAAU,WAAW,QAAQ;AAAA,QAiBhD,aAAa,CAAC,MAAM,EAAE,gBAAgB;AAAA,QAoBtC,OAAO,KAAK,EAAE,KAAK,GAAG,KAAK,QAAQ,GAAG,QAAQ,QAAQ,OAAO,IAAI;AAAA,QACjE,WAAU;AAAA,QAEV;AAAA,+BAAC,SAAI,WAAU,qFACb;AAAA,gCAAC,UAAK,WAAU,oFACb,iBACH;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,cAAY;AAAA,gBACZ,SAAS;AAAA,gBACT,WAAU;AAAA,gBAEV,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,YACxB;AAAA,aACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA,UAAU;AAAA,cACV,aAAa;AAAA;AAAA,UACf;AAAA,UAGA,oBAAC,SAAI,WAAU,qDAAqD,UAAS;AAAA;AAAA;AAAA,IAC/E;AAAA,IACA,SAAS;AAAA,EACX;AACF;AAIO,MAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/picker-sheet.tsx"],"sourcesContent":["import { useRef } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { X } from \"lucide-react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DropdownSearchHeader } from \"./dropdown\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { useVisualViewport } from \"../hooks/use-anchored-panel\";\nimport { DEFAULT_PICKER_SHEET_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\n/**\n * Two of the div's own attributes are omitted because this component already owns\n * the name: `title` here is the sheet's HEADING (and a ReactNode), not the browser's\n * tooltip string, and `onClose` is \"the sheet was dismissed\", not the DOM's\n * `<dialog>` close event. Everything else a caller can pass reaches the sheet.\n */\nexport interface PickerSheetProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\" | \"onClose\"> {\n open: boolean;\n onClose: () => void;\n /** The field's own label — a sheet that fills the screen has to say what it is\n * asking for, which the anchored panel got for free by sitting under it. */\n title?: ReactNode;\n query: string;\n onQueryChange: (value: string) => void;\n searchPlaceholder?: string;\n inputRef?: RefObject<HTMLInputElement | null>;\n /** Default: `pickerSheet.close` from the {@link UiKitProvider}, else \"Close\". */\n closeLabel?: string;\n children: ReactNode;\n}\n\n/**\n * The PHONE presentation of a picker: a full-screen dialog with a search box at\n * the top and the list filling everything below it (Keksdose live #200).\n *\n * *\"Paid as full screen dialog with input. Similar to the account select that\n * already appears as full screen.\"* The account field is a native `<select>`, and\n * a phone browser renders that as a full-screen list — so the app's own pickers,\n * which are anchored dropdown panels, were the odd ones out: a 320px-tall panel\n * squeezed between the field and the keyboard, showing three or four rows of a\n * list that might have two hundred entries.\n *\n * What this fixes beyond size: the panel had to be *placed* (above/below, tracking\n * the visual viewport as the keyboard opened — see `useAnchoredPanel`), and every\n * one of those decisions is a chance to be wrong on a screen this small. A sheet\n * has no placement. It also gives the search input somewhere unambiguous to live,\n * which is the other half of the request: *\"add filler possibility to the account\n * select or even better right to the hoc the account select is derived from\"* —\n * so this lives in the shared picker, not in one form's copy of it.\n *\n * Rows are `min-h-11` (44px): the touch target the rest of the app uses. The list\n * scrolls, the header does not, and the body behind it is scroll-locked so a drag\n * that overshoots the list does not move the page underneath.\n */\nexport function PickerSheet({\n open,\n onClose,\n title,\n query,\n onQueryChange,\n searchPlaceholder,\n inputRef,\n closeLabel,\n children,\n \"aria-label\": ariaLabel,\n className,\n style,\n ...rest\n}: PickerSheetProps) {\n // Every picker in the kit opens one of these on a phone, and most of them pass\n // their own `closeLabel` through as `undefined` — so this lookup is what makes the\n // one way out of a full-screen sheet speak the app's language without each of\n // them having to (see the prop's note in `EntityCombobox`).\n const labels = useKitLabels(\"pickerSheet\", DEFAULT_PICKER_SHEET_LABELS, { close: closeLabel });\n useBodyScrollLock(open);\n /**\n * Back closes THE SHEET, not the dialog it was opened from (Keksdose live #309:\n * *\"Mouse Back does not only close the select but also the whole edit or create\n * dialog which is cumbersome.\"*).\n *\n * The sheet is a full-screen overlay over whatever opened it, and on a phone that\n * is almost always the row editor — which DOES push a history entry\n * (`data-table.tsx`). So the sheet was invisible to Back: one press popped the\n * editor's entry and took the whole dialog with it, discarding an edit in progress\n * in order to dismiss a list.\n *\n * `useOverlayHistory` was built for exactly this nesting — its stack unwinds\n * last-in-first-out, and the U-12 regression test in its own suite is written\n * around \"a Modal and a PickerSheet inside it\". The sheet simply never called it.\n *\n * Nothing else has to change: the hook is a no-op while `open` is false, and it\n * pops its own entry when the sheet closes some other way (the X, a row, the\n * backdrop) so history does not accumulate husks.\n */\n useOverlayHistory(open, onClose);\n // The visible region, while the sheet is up — see the `style` below for why a\n // full-screen sheet cannot simply be `inset-0` on a phone (live #328).\n const vv = useVisualViewport(open);\n // A fallback ref so a caller that does not need the handle still gets the opening\n // focus, rather than the behaviour depending on whether a prop was passed. What that\n // focus is FOR is on the trap below.\n const ownRef = useRef<HTMLInputElement | null>(null);\n const searchRef = inputRef ?? ownRef;\n\n const sheetRef = useRef<HTMLDivElement>(null);\n /**\n * The sheet said `aria-modal=\"true\"` and then left focus on the field behind it, so\n * a screen reader was told to hide the page the user's cursor was still standing in\n * (the audit's *\"Only Modal manages focus\"*). Through the shared hook rather than\n * the bare `.focus()` that used to live here: the hook re-reads the tabbable list on\n * every Tab, which this panel needs more than most — its list is filtered as you\n * type, so the rows a trap captured at open time are gone by the second keystroke.\n *\n * **`initialFocus` is the search box**, not the container, and this is the one place\n * in the package that overrides `Modal`'s rule. The sheet exists in order to be typed\n * into (live #212: *\"make the text input filter active on select click\"*), and the\n * callers cannot do it themselves — `Combobox`'s field runs\n * `setOpen(true); sheetInputRef.current?.focus()` in a single `onFocus`, so the focus\n * call happens while this sheet is still unmounted and the ref is null. Only the\n * sheet knows when its own input exists. Popping the keyboard is the POINT here,\n * where in a form dialog it is the cost.\n *\n * **`restoreFocus` is off**, which is the uncomfortable half. Every caller in this\n * kit opens the sheet from the field's own `onFocus` handler, so handing focus back\n * to that field on close re-opens the sheet the user has just dismissed — a row tap\n * would select a value and immediately put the list back over it. The field that\n * opened it is the only thing that knows whether re-focusing it means \"open\"; a sheet\n * that cannot tell must not guess. The cost is that focus lands on `<body>` after a\n * dismissal, which on the phone this shape is for costs nothing, and on a pointer\n * device is what happened before this change too.\n */\n useFocusTrap(sheetRef, {\n active: open,\n restoreFocus: false,\n initialFocus: () => searchRef.current,\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key !== \"Escape\") return;\n // The sheet is almost always the INNERMOST overlay — it opens from a field inside a\n // `Modal` or a `FullBleedDialog`, both of which take Escape on their own panel. A\n // React portal's events still travel up the React tree, so without stopping it here\n // one press would dismiss this list and the dialog underneath with it: Keksdose\n // live #309 (*\"Mouse Back does not only close the select but also the whole edit or\n // create dialog which is cumbersome\"*), which the Back gesture already learned, on\n // the key a desktop user reaches for instead.\n e.stopPropagation();\n onClose();\n };\n\n if (!open || typeof document === \"undefined\") return null;\n return createPortal(\n <div\n // Spread FIRST, and every attribute below it deliberately wins: `style`,\n // `onMouseDown` and `onKeyDown` here are not styling choices, they are live\n // #328, dev#477 and live #309 respectively, and a caller who passed one by\n // accident would re-open a bug three apps have already paid for. `style` is\n // MERGED rather than won outright (see below), which is the one case where a\n // caller has something to add.\n {...rest}\n ref={sheetRef}\n role=\"dialog\"\n aria-modal=\"true\"\n // Without it the container cannot take focus, and `useFocusTrap` has nothing to\n // fall back to when the list is empty and the search box is the only thing in\n // here — see the hook's \"nothing tabbable\" branch.\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n // The caller's own name wins over the heading: a sheet titled \"Account\" in a\n // form with two of them needs to say WHICH, and `title` is what the user reads\n // rather than what a reader announces.\n aria-label={ariaLabel ?? (typeof title === \"string\" ? title : undefined)}\n // The sheet is PORTALLED to <body>, so it is not inside the field's own\n // wrapper — and `useDropdown` closes on any document mousedown landing\n // outside that wrapper. Tapping a row therefore unmounted the row (open →\n // false) before its CLICK could fire, so the tap selected nothing at all and\n // the sheet just vanished (Keksdose dev#477).\n //\n // It bit whichever sheet commits on `onClick` — the payee field's, since live\n // #200, where it hid behind the free-text value the search box was already\n // setting; and the account field's the moment dev#477 moved it onto the inline\n // picker. `ComboboxPanel`'s rows happen to commit on `onMouseDown`, so they\n // beat the document listener and were never affected — which is luck, not a\n // design, and exactly why the fix belongs to the sheet.\n //\n // Stopping the mousedown here rather than teaching every caller about the\n // portal: the sheet is modal and full-screen, so \"a click in here is not an\n // outside click\" is a property of the sheet, not of whoever opened it.\n onMouseDown={(e) => e.stopPropagation()}\n // ⚠️ `inset-0` was the whole of Keksdose live #328: *\"When the keyboard overlays\n // the entries I cannot scroll past them … to see also the last entries. Happens\n // when browsing the account select.\"*\n //\n // This sheet FOCUSES ITS OWN SEARCH BOX on open (see above), so the keyboard is\n // up every single time it is used on a phone. On Android that shrinks the visual\n // viewport and leaves the LAYOUT viewport alone — `inset-0` is the layout\n // viewport — so the sheet kept its full height with its bottom third behind the\n // keyboard. The list below is `flex-1 overflow-y-auto`, so it sized itself to\n // that hidden height too: it scrolled to ITS end while the last rows were still\n // under the keys, and no gesture could bring them out. Nothing was clipped and\n // nothing looked broken, which is why it reads as \"I cannot scroll past them\".\n //\n // `visualViewport` is the API that answers rather than hints (the live #292\n // lesson): it reports the region actually on screen, keyboard and pinch-pan\n // included, and it fires on the keyboard opening and closing. Pinned to that box,\n // the sheet ends where the keys begin and the list's own scroll covers the rest.\n // `useVisualViewport` returns null when there is nothing to correct, and then\n // this is character-for-character the sheet that was here before.\n // A caller's `style` is kept and the correction laid OVER it: the three values\n // below are the whole of live #328, so they are not negotiable, but a z-index\n // or a transition from outside has nothing to do with them. Without a visual\n // viewport to correct for this is character-for-character what the caller passed.\n style={vv ? { ...style, top: vv.top, height: vv.height, bottom: \"auto\" } : style}\n className={cn(\"fixed inset-0 z-50 flex flex-col bg-[var(--bg-surface)] outline-none\", className)}\n >\n <div className=\"flex items-center gap-2 border-b border-[var(--border)] px-3 py-2\">\n <span className=\"min-w-0 flex-1 truncate text-sm font-semibold text-[var(--text-primary)]\">\n {title}\n </span>\n <button\n type=\"button\"\n aria-label={labels.close}\n onClick={onClose}\n className=\"rounded p-1.5 text-[var(--text-muted)] hover:bg-[var(--bg-hover)]\"\n >\n <X className=\"size-5\" />\n </button>\n </div>\n <DropdownSearchHeader\n query={query}\n onQueryChange={onQueryChange}\n inputRef={searchRef}\n placeholder={searchPlaceholder}\n />\n {/* min-h-0 so the LIST scrolls rather than the dialog growing past the\n viewport — a flex child defaults to min-height:auto. */}\n <div className=\"min-h-0 flex-1 overflow-y-auto overscroll-contain\">{children}</div>\n </div>,\n document.body,\n );\n}\n\n/** A row inside a {@link PickerSheet} — the phone-sized version of a dropdown row,\n * so a list is comfortable to hit with a thumb rather than merely legible. */\nexport const SHEET_ROW_CLASS = cn(\n \"flex min-h-11 w-full items-center gap-2 px-4 py-2 text-left text-base\",\n \"text-[var(--text-primary)] hover:bg-[var(--bg-hover)]\",\n);\n"],"mappings":";AA0NM,SACE,KADF;AA1NN,SAAS,cAAc;AACvB,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,UAAU;AACnB,SAAS,4BAA4B;AACrC,SAAS,yBAAyB;AAClC,SAAS,yBAAyB;AAClC,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,6BAA6B,oBAAoB;AA+CnD,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAqB;AAKnB,QAAM,SAAS,aAAa,eAAe,6BAA6B,EAAE,OAAO,WAAW,CAAC;AAC7F,oBAAkB,IAAI;AAoBtB,oBAAkB,MAAM,OAAO;AAG/B,QAAM,KAAK,kBAAkB,IAAI;AAIjC,QAAM,SAAS,OAAgC,IAAI;AACnD,QAAM,YAAY,YAAY;AAE9B,QAAM,WAAW,OAAuB,IAAI;AA2B5C,eAAa,UAAU;AAAA,IACrB,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,cAAc,MAAM,UAAU;AAAA,EAChC,CAAC;AAED,QAAM,gBAAgB,CAAC,MAAqC;AAC1D,QAAI,EAAE,QAAQ,SAAU;AAQxB,MAAE,gBAAgB;AAClB,YAAQ;AAAA,EACV;AAEA,MAAI,CAAC,QAAQ,OAAO,aAAa,YAAa,QAAO;AACrD,SAAO;AAAA,IACL;AAAA,MAAC;AAAA;AAAA,QAOE,GAAG;AAAA,QACJ,KAAK;AAAA,QACL,MAAK;AAAA,QACL,cAAW;AAAA,QAIX,UAAU;AAAA,QACV,WAAW;AAAA,QAIX,cAAY,cAAc,OAAO,UAAU,WAAW,QAAQ;AAAA,QAiB9D,aAAa,CAAC,MAAM,EAAE,gBAAgB;AAAA,QAwBtC,OAAO,KAAK,EAAE,GAAG,OAAO,KAAK,GAAG,KAAK,QAAQ,GAAG,QAAQ,QAAQ,OAAO,IAAI;AAAA,QAC3E,WAAW,GAAG,wEAAwE,SAAS;AAAA,QAE/F;AAAA,+BAAC,SAAI,WAAU,qEACb;AAAA,gCAAC,UAAK,WAAU,4EACb,iBACH;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,cAAY,OAAO;AAAA,gBACnB,SAAS;AAAA,gBACT,WAAU;AAAA,gBAEV,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,YACxB;AAAA,aACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA,UAAU;AAAA,cACV,aAAa;AAAA;AAAA,UACf;AAAA,UAGA,oBAAC,SAAI,WAAU,qDAAqD,UAAS;AAAA;AAAA;AAAA,IAC/E;AAAA,IACA,SAAS;AAAA,EACX;AACF;AAIO,MAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AACF;","names":[]}
|
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { RefObject, ReactNode } from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, RefObject, ReactNode } from 'react';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/** The panel's accessible name. A `role="dialog"` with no name announces as "dialog"
|
|
5
|
+
* and nothing else, which is worse than the unlabelled div it replaced — so it is a
|
|
6
|
+
* prop with an English default, like every other string this package renders. Callers
|
|
7
|
+
* should pass the control's own name ("Filter", "Choose a date"); the default is only
|
|
8
|
+
* there so an unmigrated call site is still named. */
|
|
9
|
+
interface PopoverLabels {
|
|
10
|
+
panel: string;
|
|
11
|
+
}
|
|
12
|
+
declare const DEFAULT_POPOVER_LABELS: PopoverLabels;
|
|
13
|
+
/**
|
|
14
|
+
* Exported, and extending the `<div>` attributes, so a consumer wrapping this — which is
|
|
15
|
+
* what `DateField` and `FilterPopover` in this very package do — can hand anything
|
|
16
|
+
* through to the PANEL: a `data-tour` anchor for the kit's own guided tour, a test id,
|
|
17
|
+
* an `aria-describedby`, or an `aria-label` naming this particular popover.
|
|
18
|
+
*
|
|
19
|
+
* `children` is omitted from those attributes rather than inherited: here it is a render
|
|
20
|
+
* prop taking the panel's `close`, not a `ReactNode`, so the two cannot be reconciled.
|
|
21
|
+
*/
|
|
22
|
+
interface PopoverProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
5
23
|
trigger: (state: {
|
|
6
24
|
open: boolean;
|
|
7
25
|
toggle: () => void;
|
|
@@ -9,12 +27,52 @@ interface PopoverProps {
|
|
|
9
27
|
}) => ReactNode;
|
|
10
28
|
children: (close: () => void) => ReactNode;
|
|
11
29
|
width?: number;
|
|
30
|
+
labels?: Partial<PopoverLabels>;
|
|
31
|
+
/** Id for the panel, so a trigger wearing `role="combobox"` can `aria-controls` it.
|
|
32
|
+
* Required by ARIA on that role, and the panel is rendered here rather than by the
|
|
33
|
+
* caller, so the id has to come in from outside. */
|
|
34
|
+
panelId?: string;
|
|
35
|
+
/** Extra classes for the PANEL. The trigger is the caller's own element. */
|
|
36
|
+
className?: string;
|
|
12
37
|
}
|
|
13
38
|
/**
|
|
14
39
|
* Portal-rendered popover anchored under its trigger button. The panel is fixed
|
|
15
40
|
* to the viewport (so table/overflow ancestors can't clip it) and re-aligns on
|
|
16
41
|
* scroll/resize; it closes on outside-click and Escape.
|
|
42
|
+
*
|
|
43
|
+
* ## Trapped, not merely focused — and why, because it is the closer call
|
|
44
|
+
*
|
|
45
|
+
* The audit's finding was that this panel *"portals out of the tab order with no role
|
|
46
|
+
* and no focus move"*: Tab from the trigger went straight past the panel into the page
|
|
47
|
+
* behind it, so the calendar, the calculator keypad and every column filter in the kit
|
|
48
|
+
* were mouse-only. Two shapes fix that, and a popover is not automatically a dialog, so
|
|
49
|
+
* this was chosen rather than inherited:
|
|
50
|
+
*
|
|
51
|
+
* - **The light one** — move focus in on open, hand it back on close, and let Tab walk
|
|
52
|
+
* OUT of the panel into whatever follows the trigger. That is the right shape for a
|
|
53
|
+
* non-modal popover sitting inline in the document.
|
|
54
|
+
* - **The trap** — Tab cycles inside the panel; Escape and a click outside are the
|
|
55
|
+
* ways out. What is here.
|
|
56
|
+
*
|
|
57
|
+
* The deciding fact is the portal. This panel is rendered to the END of `<body>`, not
|
|
58
|
+
* beside its trigger, so "let Tab walk out" does not mean "carry on from the trigger" —
|
|
59
|
+
* it means land at the end of the document, several hundred controls from where the
|
|
60
|
+
* user was. Tabbing out of it would be a worse place to arrive than the one the finding
|
|
61
|
+
* complains about, and fixing that properly needs a sentinel-and-return dance that
|
|
62
|
+
* nothing in this package has. The trap gets the user back to the trigger every time,
|
|
63
|
+
* because `useFocusTrap` restores focus on close.
|
|
64
|
+
*
|
|
65
|
+
* And the panels this kit actually opens are self-contained: pick a date, tap out a
|
|
66
|
+
* sum, narrow a column. Each of them commits and closes, and each already has two
|
|
67
|
+
* documented ways out wired above — Escape and an outside press — so containment costs
|
|
68
|
+
* a keyboard user nothing they had.
|
|
69
|
+
*
|
|
70
|
+
* **It stays non-modal in every other respect**, deliberately: no backdrop, no body
|
|
71
|
+
* scroll lock, and no `aria-modal`, so the page behind is still readable by a screen
|
|
72
|
+
* reader, still scrollable, and still clickable — clicking it is what dismisses this.
|
|
73
|
+
* Only Tab is contained. A popover that took the page away as well would be a
|
|
74
|
+
* {@link Modal}, and there is one of those.
|
|
17
75
|
*/
|
|
18
|
-
declare function Popover({ trigger, children, width }: PopoverProps): react.JSX.Element;
|
|
76
|
+
declare function Popover({ trigger, children, width, labels, panelId, className, style, "aria-label": ariaLabel, ...rest }: PopoverProps): react.JSX.Element;
|
|
19
77
|
|
|
20
|
-
export { Popover };
|
|
78
|
+
export { DEFAULT_POPOVER_LABELS, Popover, type PopoverLabels, type PopoverProps };
|
|
@@ -1,15 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
3
|
import { useRef, useState } from "react";
|
|
3
4
|
import { createPortal } from "react-dom";
|
|
4
5
|
import { useAnchoredPanel } from "../hooks/use-anchored-panel.js";
|
|
5
6
|
import { useEscapeKey, useOutsideClick } from "../hooks/use-dismiss.js";
|
|
7
|
+
import { useFocusTrap } from "../hooks/use-focus-trap.js";
|
|
8
|
+
import { cn } from "../lib/cn.js";
|
|
9
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
10
|
+
const DEFAULT_POPOVER_LABELS = {
|
|
11
|
+
panel: "Popover"
|
|
12
|
+
};
|
|
6
13
|
const POPOVER_WIDTH = 288;
|
|
7
|
-
function Popover({
|
|
14
|
+
function Popover({
|
|
15
|
+
trigger,
|
|
16
|
+
children,
|
|
17
|
+
width = POPOVER_WIDTH,
|
|
18
|
+
labels,
|
|
19
|
+
panelId,
|
|
20
|
+
className,
|
|
21
|
+
style,
|
|
22
|
+
"aria-label": ariaLabel,
|
|
23
|
+
...rest
|
|
24
|
+
}) {
|
|
8
25
|
const triggerRef = useRef(null);
|
|
9
26
|
const panelRef = useRef(null);
|
|
10
27
|
const [open, setOpen] = useState(false);
|
|
11
28
|
const close = () => setOpen(false);
|
|
12
29
|
const toggle = () => setOpen((v) => !v);
|
|
30
|
+
const text = useKitLabels("popover", DEFAULT_POPOVER_LABELS, labels);
|
|
13
31
|
const placement = useAnchoredPanel(triggerRef, open);
|
|
14
32
|
const rect = placement.rect;
|
|
15
33
|
const pos = rect ? {
|
|
@@ -18,21 +36,52 @@ function Popover({ trigger, children, width = POPOVER_WIDTH }) {
|
|
|
18
36
|
} : null;
|
|
19
37
|
useOutsideClick([triggerRef, panelRef], close, open);
|
|
20
38
|
useEscapeKey(close, open);
|
|
39
|
+
useFocusTrap(panelRef, {
|
|
40
|
+
active: open && pos !== null,
|
|
41
|
+
// The panel keeps a focus one of its own children has already taken. `FilterPopover`
|
|
42
|
+
// autofocuses its text box on mount — that is what the press on "Filter" was FOR,
|
|
43
|
+
// and it is a prop there precisely so the popover keeps it — and a trap that
|
|
44
|
+
// unconditionally focused the container would take it straight back off again,
|
|
45
|
+
// one effect later and invisibly.
|
|
46
|
+
initialFocus: () => {
|
|
47
|
+
const panel = panelRef.current;
|
|
48
|
+
const active = document.activeElement;
|
|
49
|
+
return panel?.contains(active) ? active : panel;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const handleKeyDown = (e) => {
|
|
53
|
+
if (e.key !== "Escape") return;
|
|
54
|
+
e.stopPropagation();
|
|
55
|
+
close();
|
|
56
|
+
};
|
|
21
57
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
22
58
|
trigger({ open, toggle, ref: triggerRef }),
|
|
23
59
|
open && pos && createPortal(
|
|
24
60
|
/* @__PURE__ */ jsx(
|
|
25
61
|
"div",
|
|
26
62
|
{
|
|
63
|
+
...rest,
|
|
27
64
|
ref: panelRef,
|
|
65
|
+
role: "dialog",
|
|
66
|
+
id: panelId,
|
|
67
|
+
"aria-label": ariaLabel ?? text.panel,
|
|
68
|
+
tabIndex: -1,
|
|
69
|
+
onKeyDown: handleKeyDown,
|
|
28
70
|
style: {
|
|
71
|
+
// Merged over the caller's, never replaced by it: these four are measured
|
|
72
|
+
// against the trigger every scroll and resize, and a panel that dropped
|
|
73
|
+
// them would paint at the top-left of the viewport.
|
|
74
|
+
...style,
|
|
29
75
|
position: "fixed",
|
|
30
76
|
top: pos.top,
|
|
31
77
|
left: pos.left,
|
|
32
78
|
width,
|
|
33
79
|
maxHeight: placement.maxHeight
|
|
34
80
|
},
|
|
35
|
-
className:
|
|
81
|
+
className: cn(
|
|
82
|
+
"z-50 overflow-y-auto rounded-md border border-[var(--border)] bg-[var(--bg-surface)] p-3 shadow-lg outline-none",
|
|
83
|
+
className
|
|
84
|
+
),
|
|
36
85
|
children: children(close)
|
|
37
86
|
}
|
|
38
87
|
),
|
|
@@ -41,6 +90,7 @@ function Popover({ trigger, children, width = POPOVER_WIDTH }) {
|
|
|
41
90
|
] });
|
|
42
91
|
}
|
|
43
92
|
export {
|
|
93
|
+
DEFAULT_POPOVER_LABELS,
|
|
44
94
|
Popover
|
|
45
95
|
};
|
|
46
96
|
//# sourceMappingURL=popover.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/popover.tsx"],"sourcesContent":["import { useRef, useState } from \"react\";\nimport type { ReactNode, RefObject } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useEscapeKey, useOutsideClick } from \"../hooks/use-dismiss\";\n\ninterface PopoverProps {\n trigger: (state: { open: boolean; toggle: () => void; ref: RefObject<HTMLButtonElement | null> }) => ReactNode;\n children: (close: () => void) => ReactNode;\n width?: number;\n}\n\nconst POPOVER_WIDTH = 288;\n\n/**\n * Portal-rendered popover anchored under its trigger button. The panel is fixed\n * to the viewport (so table/overflow ancestors can't clip it) and re-aligns on\n * scroll/resize; it closes on outside-click and Escape.\n */\nexport function Popover({ trigger, children, width = POPOVER_WIDTH }: PopoverProps) {\n const triggerRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const [open, setOpen] = useState(false);\n const close = () => setOpen(false);\n const toggle = () => setOpen((v) => !v);\n\n // Align the panel's right edge to the trigger's and clamp it horizontally; the\n // hook owns the vertical half — below by default, flipped above (and height-capped)\n // when that is where the room is, e.g. once a mobile keyboard has eaten the bottom\n // of the screen (feedback #135). It re-measures on scroll/resize.\n const placement = useAnchoredPanel(triggerRef, open);\n const rect = placement.rect;\n const pos = rect\n ? {\n top: placement.top,\n left: Math.min(Math.max(8, rect.right - width), window.innerWidth - width - 8),\n }\n : null;\n\n useOutsideClick([triggerRef, panelRef], close, open);\n useEscapeKey(close, open);\n\n return (\n <>\n {trigger({ open, toggle, ref: triggerRef })}\n {open && pos &&\n createPortal(\n <div\n ref={panelRef}\n style={{\n position: \"fixed\",\n top: pos.top,\n left: pos.left,\n width,\n maxHeight: placement.maxHeight,\n }}\n className=\"z-50 overflow-y-auto rounded-md border border-slate-200 bg-white p-3 shadow-lg dark:border-slate-700 dark:bg-slate-900\"\n >\n {children(close)}\n </div>,\n document.body,\n )}\n </>\n );\n}\n"],"mappings":"AA2CI,mBAIM,KAJN;AA3CJ,SAAS,QAAQ,gBAAgB;AAEjC,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AACjC,SAAS,cAAc,uBAAuB;AAQ9C,MAAM,gBAAgB;AAOf,SAAS,QAAQ,EAAE,SAAS,UAAU,QAAQ,cAAc,GAAiB;AAClF,QAAM,aAAa,OAA0B,IAAI;AACjD,QAAM,WAAW,OAAuB,IAAI;AAC5C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,QAAQ,MAAM,QAAQ,KAAK;AACjC,QAAM,SAAS,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;AAMtC,QAAM,YAAY,iBAAiB,YAAY,IAAI;AACnD,QAAM,OAAO,UAAU;AACvB,QAAM,MAAM,OACR;AAAA,IACE,KAAK,UAAU;AAAA,IACf,MAAM,KAAK,IAAI,KAAK,IAAI,GAAG,KAAK,QAAQ,KAAK,GAAG,OAAO,aAAa,QAAQ,CAAC;AAAA,EAC/E,IACA;AAEJ,kBAAgB,CAAC,YAAY,QAAQ,GAAG,OAAO,IAAI;AACnD,eAAa,OAAO,IAAI;AAExB,SACE,iCACG;AAAA,YAAQ,EAAE,MAAM,QAAQ,KAAK,WAAW,CAAC;AAAA,IACzC,QAAQ,OACP;AAAA,MACE;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UACL,OAAO;AAAA,YACL,UAAU;AAAA,YACV,KAAK,IAAI;AAAA,YACT,MAAM,IAAI;AAAA,YACV;AAAA,YACA,WAAW,UAAU;AAAA,UACvB;AAAA,UACA,WAAU;AAAA,UAET,mBAAS,KAAK;AAAA;AAAA,MACjB;AAAA,MACA,SAAS;AAAA,IACX;AAAA,KACJ;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/popover.tsx"],"sourcesContent":["import { useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useEscapeKey, useOutsideClick } from \"../hooks/use-dismiss\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\n/** The panel's accessible name. A `role=\"dialog\"` with no name announces as \"dialog\"\n * and nothing else, which is worse than the unlabelled div it replaced — so it is a\n * prop with an English default, like every other string this package renders. Callers\n * should pass the control's own name (\"Filter\", \"Choose a date\"); the default is only\n * there so an unmigrated call site is still named. */\nexport interface PopoverLabels {\n panel: string;\n}\n\n// Exported since the kit grew one label tree (`UiKitLabels`, src/i18n): the complete\n// English reference a translator works from has to be able to name every namespace.\nexport const DEFAULT_POPOVER_LABELS: PopoverLabels = {\n panel: \"Popover\",\n};\n\n/**\n * Exported, and extending the `<div>` attributes, so a consumer wrapping this — which is\n * what `DateField` and `FilterPopover` in this very package do — can hand anything\n * through to the PANEL: a `data-tour` anchor for the kit's own guided tour, a test id,\n * an `aria-describedby`, or an `aria-label` naming this particular popover.\n *\n * `children` is omitted from those attributes rather than inherited: here it is a render\n * prop taking the panel's `close`, not a `ReactNode`, so the two cannot be reconciled.\n */\nexport interface PopoverProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n trigger: (state: { open: boolean; toggle: () => void; ref: RefObject<HTMLButtonElement | null> }) => ReactNode;\n children: (close: () => void) => ReactNode;\n width?: number;\n labels?: Partial<PopoverLabels>;\n /** Id for the panel, so a trigger wearing `role=\"combobox\"` can `aria-controls` it.\n * Required by ARIA on that role, and the panel is rendered here rather than by the\n * caller, so the id has to come in from outside. */\n panelId?: string;\n /** Extra classes for the PANEL. The trigger is the caller's own element. */\n className?: string;\n}\n\nconst POPOVER_WIDTH = 288;\n\n/**\n * Portal-rendered popover anchored under its trigger button. The panel is fixed\n * to the viewport (so table/overflow ancestors can't clip it) and re-aligns on\n * scroll/resize; it closes on outside-click and Escape.\n *\n * ## Trapped, not merely focused — and why, because it is the closer call\n *\n * The audit's finding was that this panel *\"portals out of the tab order with no role\n * and no focus move\"*: Tab from the trigger went straight past the panel into the page\n * behind it, so the calendar, the calculator keypad and every column filter in the kit\n * were mouse-only. Two shapes fix that, and a popover is not automatically a dialog, so\n * this was chosen rather than inherited:\n *\n * - **The light one** — move focus in on open, hand it back on close, and let Tab walk\n * OUT of the panel into whatever follows the trigger. That is the right shape for a\n * non-modal popover sitting inline in the document.\n * - **The trap** — Tab cycles inside the panel; Escape and a click outside are the\n * ways out. What is here.\n *\n * The deciding fact is the portal. This panel is rendered to the END of `<body>`, not\n * beside its trigger, so \"let Tab walk out\" does not mean \"carry on from the trigger\" —\n * it means land at the end of the document, several hundred controls from where the\n * user was. Tabbing out of it would be a worse place to arrive than the one the finding\n * complains about, and fixing that properly needs a sentinel-and-return dance that\n * nothing in this package has. The trap gets the user back to the trigger every time,\n * because `useFocusTrap` restores focus on close.\n *\n * And the panels this kit actually opens are self-contained: pick a date, tap out a\n * sum, narrow a column. Each of them commits and closes, and each already has two\n * documented ways out wired above — Escape and an outside press — so containment costs\n * a keyboard user nothing they had.\n *\n * **It stays non-modal in every other respect**, deliberately: no backdrop, no body\n * scroll lock, and no `aria-modal`, so the page behind is still readable by a screen\n * reader, still scrollable, and still clickable — clicking it is what dismisses this.\n * Only Tab is contained. A popover that took the page away as well would be a\n * {@link Modal}, and there is one of those.\n */\nexport function Popover({\n trigger,\n children,\n width = POPOVER_WIDTH,\n labels,\n panelId,\n className,\n style,\n \"aria-label\": ariaLabel,\n ...rest\n}: PopoverProps) {\n const triggerRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const [open, setOpen] = useState(false);\n const close = () => setOpen(false);\n const toggle = () => setOpen((v) => !v);\n // prop > `<UiKitProvider labels={{ popover }}>` > English. The provider's\n // `popover.panel` is only the last-resort name for a panel no caller named.\n const text = useKitLabels(\"popover\", DEFAULT_POPOVER_LABELS, labels);\n\n // Align the panel's right edge to the trigger's and clamp it horizontally; the\n // hook owns the vertical half — below by default, flipped above (and height-capped)\n // when that is where the room is, e.g. once a mobile keyboard has eaten the bottom\n // of the screen (feedback #135). It re-measures on scroll/resize.\n const placement = useAnchoredPanel(triggerRef, open);\n const rect = placement.rect;\n const pos = rect\n ? {\n top: placement.top,\n left: Math.min(Math.max(8, rect.right - width), window.innerWidth - width - 8),\n }\n : null;\n\n useOutsideClick([triggerRef, panelRef], close, open);\n useEscapeKey(close, open);\n\n useFocusTrap(panelRef, {\n active: open && pos !== null,\n // The panel keeps a focus one of its own children has already taken. `FilterPopover`\n // autofocuses its text box on mount — that is what the press on \"Filter\" was FOR,\n // and it is a prop there precisely so the popover keeps it — and a trap that\n // unconditionally focused the container would take it straight back off again,\n // one effect later and invisibly.\n initialFocus: () => {\n const panel = panelRef.current;\n const active = document.activeElement;\n return panel?.contains(active) ? (active as HTMLElement) : panel;\n },\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key !== \"Escape\") return;\n // `useEscapeKey` above listens on the document and still covers the case where\n // focus somehow is not in here. This handler is for the case that focus IS, which\n // is now the normal one — and it exists to STOP the key, because a popover opened\n // from inside a `Modal` (the calculator on an amount field is exactly that) would\n // otherwise have its Escape travel on up the React tree and close the dialog\n // underneath as well. Closing twice is harmless — `close` is a state setter, not a\n // caller's callback — so the two paths cannot double-fire anything.\n e.stopPropagation();\n close();\n };\n\n return (\n <>\n {trigger({ open, toggle, ref: triggerRef })}\n {open && pos &&\n createPortal(\n <div\n // `...rest` first: the role, the id the trigger's `aria-controls` points at,\n // the `tabIndex` the trap needs and the measured position are what make this\n // a popover, and a caller reaching for a `data-tour` anchor must not be able\n // to remove one of them by accident.\n {...rest}\n ref={panelRef}\n role=\"dialog\"\n id={panelId}\n // The caller's own `aria-label` wins over {@link PopoverLabels.panel}, which\n // is the package's English fallback for an unnamed panel. Both name the same\n // element; the DOM spelling is the one being standardised on, so where both\n // are present it is the one that survives.\n aria-label={ariaLabel ?? text.panel}\n // Without it the panel cannot take focus at all, and the trap above would\n // silently do nothing on a panel whose children are not yet tabbable.\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n style={{\n // Merged over the caller's, never replaced by it: these four are measured\n // against the trigger every scroll and resize, and a panel that dropped\n // them would paint at the top-left of the viewport.\n ...style,\n position: \"fixed\",\n top: pos.top,\n left: pos.left,\n width,\n maxHeight: placement.maxHeight,\n }}\n className={cn(\n \"z-50 overflow-y-auto rounded-md border border-[var(--border)] bg-[var(--bg-surface)] p-3 shadow-lg outline-none\",\n className,\n )}\n >\n {children(close)}\n </div>,\n document.body,\n )}\n </>\n );\n}\n"],"mappings":";AAsJI,mBAIM,KAJN;AAtJJ,SAAS,QAAQ,gBAAgB;AAEjC,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AACjC,SAAS,cAAc,uBAAuB;AAC9C,SAAS,oBAAoB;AAC7B,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAatB,MAAM,yBAAwC;AAAA,EACnD,OAAO;AACT;AAwBA,MAAM,gBAAgB;AAwCf,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,GAAG;AACL,GAAiB;AACf,QAAM,aAAa,OAA0B,IAAI;AACjD,QAAM,WAAW,OAAuB,IAAI;AAC5C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,QAAQ,MAAM,QAAQ,KAAK;AACjC,QAAM,SAAS,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;AAGtC,QAAM,OAAO,aAAa,WAAW,wBAAwB,MAAM;AAMnE,QAAM,YAAY,iBAAiB,YAAY,IAAI;AACnD,QAAM,OAAO,UAAU;AACvB,QAAM,MAAM,OACR;AAAA,IACE,KAAK,UAAU;AAAA,IACf,MAAM,KAAK,IAAI,KAAK,IAAI,GAAG,KAAK,QAAQ,KAAK,GAAG,OAAO,aAAa,QAAQ,CAAC;AAAA,EAC/E,IACA;AAEJ,kBAAgB,CAAC,YAAY,QAAQ,GAAG,OAAO,IAAI;AACnD,eAAa,OAAO,IAAI;AAExB,eAAa,UAAU;AAAA,IACrB,QAAQ,QAAQ,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMxB,cAAc,MAAM;AAClB,YAAM,QAAQ,SAAS;AACvB,YAAM,SAAS,SAAS;AACxB,aAAO,OAAO,SAAS,MAAM,IAAK,SAAyB;AAAA,IAC7D;AAAA,EACF,CAAC;AAED,QAAM,gBAAgB,CAAC,MAAqC;AAC1D,QAAI,EAAE,QAAQ,SAAU;AAQxB,MAAE,gBAAgB;AAClB,UAAM;AAAA,EACR;AAEA,SACE,iCACG;AAAA,YAAQ,EAAE,MAAM,QAAQ,KAAK,WAAW,CAAC;AAAA,IACzC,QAAQ,OACP;AAAA,MACE;AAAA,QAAC;AAAA;AAAA,UAKE,GAAG;AAAA,UACJ,KAAK;AAAA,UACL,MAAK;AAAA,UACL,IAAI;AAAA,UAKJ,cAAY,aAAa,KAAK;AAAA,UAG9B,UAAU;AAAA,UACV,WAAW;AAAA,UACX,OAAO;AAAA;AAAA;AAAA;AAAA,YAIL,GAAG;AAAA,YACH,UAAU;AAAA,YACV,KAAK,IAAI;AAAA,YACT,MAAM,IAAI;AAAA,YACV;AAAA,YACA,WAAW,UAAU;AAAA,UACvB;AAAA,UACA,WAAW;AAAA,YACT;AAAA,YACA;AAAA,UACF;AAAA,UAEC,mBAAS,KAAK;AAAA;AAAA,MACjB;AAAA,MACA,SAAS;AAAA,IACX;AAAA,KACJ;AAEJ;","names":[]}
|
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { InputHTMLAttributes } from 'react';
|
|
3
3
|
|
|
4
|
-
interface
|
|
4
|
+
interface SearchFieldOwnProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "onChange" | "type" | "aria-label"> {
|
|
5
5
|
value: string;
|
|
6
6
|
/** The typed text. Emitted as a plain string, not an event: every caller of the
|
|
7
7
|
* four this replaced wanted the string, and one of them held it in a URL param
|
|
8
8
|
* rather than in state. */
|
|
9
9
|
onChange: (value: string) => void;
|
|
10
|
-
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The field's accessible name.
|
|
12
|
+
*
|
|
13
|
+
* @deprecated Pass `aria-label` instead — the DOM spelling this kit standardised on
|
|
14
|
+
* when it found three names for one idea (`ariaLabel`, `aria-label`, and this).
|
|
15
|
+
* Unchanged and still supported; it names the field only when `aria-label` is
|
|
16
|
+
* absent, so a consumer can migrate one call site at a time.
|
|
17
|
+
*/
|
|
18
|
+
label?: string;
|
|
15
19
|
/** Names the clear button. Omit it and no clear button is rendered — which is a
|
|
16
20
|
* decision, not a default: a filter you can type into and not untype is the
|
|
17
21
|
* complaint that produced the button on three of these four screens. */
|
|
18
22
|
clearLabel?: string;
|
|
19
|
-
className?: string;
|
|
20
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* One of the two spellings is REQUIRED, and the union is how that survives the
|
|
26
|
+
* deprecation. Making `label` merely optional would have been the easy change and the
|
|
27
|
+
* wrong one: the whole reason this component exists is that two of the four boxes it
|
|
28
|
+
* replaced were named by nothing but their placeholder — which disappears the moment
|
|
29
|
+
* someone types, i.e. exactly when a screen-reader user is working in the field. A
|
|
30
|
+
* caller now has a choice of spelling; it still has no option to pass neither.
|
|
31
|
+
*/
|
|
32
|
+
type SearchFieldProps = SearchFieldOwnProps & ({
|
|
33
|
+
"aria-label": string;
|
|
34
|
+
label?: string;
|
|
35
|
+
} | {
|
|
36
|
+
label: string;
|
|
37
|
+
"aria-label"?: string;
|
|
38
|
+
});
|
|
21
39
|
/**
|
|
22
40
|
* The page-level filter box: a search icon, a `type="search"` input, and a clear
|
|
23
41
|
* "×" that appears once there is something to clear.
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
3
|
import { forwardRef, useRef } from "react";
|
|
3
4
|
import { Search, X } from "lucide-react";
|
|
4
5
|
import { cn } from "../lib/cn.js";
|
|
5
6
|
import { FIELD_BASE } from "./ui.js";
|
|
6
|
-
const SearchField = forwardRef(function SearchField2({ value, onChange, label, clearLabel, className, placeholder, ...rest }, ref) {
|
|
7
|
+
const SearchField = forwardRef(function SearchField2({ value, onChange, label, clearLabel, className, placeholder, "aria-label": ariaLabel, ...rest }, ref) {
|
|
7
8
|
const innerRef = useRef(null);
|
|
9
|
+
const name = ariaLabel ?? label;
|
|
8
10
|
return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), children: [
|
|
9
11
|
/* @__PURE__ */ jsx(
|
|
10
12
|
Search,
|
|
11
13
|
{
|
|
12
14
|
"aria-hidden": true,
|
|
13
|
-
className: "pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-
|
|
15
|
+
className: "pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]"
|
|
14
16
|
}
|
|
15
17
|
),
|
|
16
18
|
/* @__PURE__ */ jsx(
|
|
@@ -24,8 +26,8 @@ const SearchField = forwardRef(function SearchField2({ value, onChange, label, c
|
|
|
24
26
|
type: "search",
|
|
25
27
|
value,
|
|
26
28
|
onChange: (e) => onChange(e.target.value),
|
|
27
|
-
"aria-label":
|
|
28
|
-
placeholder: placeholder ??
|
|
29
|
+
"aria-label": name,
|
|
30
|
+
placeholder: placeholder ?? name,
|
|
29
31
|
name: "search",
|
|
30
32
|
autoComplete: "off",
|
|
31
33
|
autoCapitalize: "none",
|
|
@@ -51,7 +53,7 @@ const SearchField = forwardRef(function SearchField2({ value, onChange, label, c
|
|
|
51
53
|
innerRef.current?.focus();
|
|
52
54
|
},
|
|
53
55
|
"aria-label": clearLabel,
|
|
54
|
-
className: "absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-
|
|
56
|
+
className: "absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]",
|
|
55
57
|
children: /* @__PURE__ */ jsx(X, { className: "size-4" })
|
|
56
58
|
}
|
|
57
59
|
)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/search-field.tsx"],"sourcesContent":["import { forwardRef, useRef } from \"react\";\nimport type { InputHTMLAttributes } from \"react\";\nimport { Search, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_BASE } from \"./ui\";\n\
|
|
1
|
+
{"version":3,"sources":["../../src/components/search-field.tsx"],"sourcesContent":["import { forwardRef, useRef } from \"react\";\nimport type { InputHTMLAttributes } from \"react\";\nimport { Search, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_BASE } from \"./ui\";\n\ninterface SearchFieldOwnProps\n extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n \"value\" | \"onChange\" | \"type\" | \"aria-label\"\n > {\n value: string;\n /** The typed text. Emitted as a plain string, not an event: every caller of the\n * four this replaced wanted the string, and one of them held it in a URL param\n * rather than in state. */\n onChange: (value: string) => void;\n /**\n * The field's accessible name.\n *\n * @deprecated Pass `aria-label` instead — the DOM spelling this kit standardised on\n * when it found three names for one idea (`ariaLabel`, `aria-label`, and this).\n * Unchanged and still supported; it names the field only when `aria-label` is\n * absent, so a consumer can migrate one call site at a time.\n */\n label?: string;\n /** Names the clear button. Omit it and no clear button is rendered — which is a\n * decision, not a default: a filter you can type into and not untype is the\n * complaint that produced the button on three of these four screens. */\n clearLabel?: string;\n}\n\n/**\n * One of the two spellings is REQUIRED, and the union is how that survives the\n * deprecation. Making `label` merely optional would have been the easy change and the\n * wrong one: the whole reason this component exists is that two of the four boxes it\n * replaced were named by nothing but their placeholder — which disappears the moment\n * someone types, i.e. exactly when a screen-reader user is working in the field. A\n * caller now has a choice of spelling; it still has no option to pass neither.\n */\nexport type SearchFieldProps = SearchFieldOwnProps &\n ({ \"aria-label\": string; label?: string } | { label: string; \"aria-label\"?: string });\n\n/**\n * The page-level filter box: a search icon, a `type=\"search\"` input, and a clear\n * \"×\" that appears once there is something to clear.\n *\n * ## Why it is a component\n *\n * Keksdose had four of these and no two agreed. Two were character-identical\n * twenty-line copies (the settings catalogue filter and the tour list filter) —\n * the same absolute icon at `left-3`, the same `pl-9 pr-9`, the same\n * `[&::-webkit-search-cancel-button]:appearance-none`, the same conditional clear\n * button at `right-2` — differing only in which state setter they called. The\n * third (the admin support queue) was the same idea at `left-2`/`size-3.5`/`pl-7`\n * with no clear button and no `type=\"search\"`, so it had neither the `searchbox`\n * role nor a way to empty itself. The fourth (the bank picker) was a bare `<Input\n * placeholder=\"Search\">`: no icon, no clear, and no accessible name at all once\n * the placeholder went away.\n *\n * None of that is a design decision anyone made; it is four people solving the\n * same problem on four days. A shared component is how the fourth screen gets the\n * clear button the first one earned.\n *\n * ## The two X's\n *\n * `type=\"search\"` is what gives the field its `searchbox` role — and, in Chrome, a\n * NATIVE clear cross that paints right next to ours, so a non-empty box shows two\n * X's an inch apart. Ours is the one that survives: it carries an accessible name\n * and matches the app's other icon buttons, and it returns focus to the input so\n * the next query can be typed straight away.\n */\nexport const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(function SearchField(\n { value, onChange, label, clearLabel, className, placeholder, \"aria-label\": ariaLabel, ...rest },\n ref,\n) {\n const innerRef = useRef<HTMLInputElement>(null);\n // Whichever spelling the caller used. It is the accessible name AND the placeholder\n // fallback, so a box named only by `aria-label` still says what it is before anyone\n // has typed in it — the same deal `label` has always had.\n const name = ariaLabel ?? label;\n return (\n <div className={cn(\"relative\", className)}>\n <Search\n aria-hidden\n className=\"pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]\"\n />\n <input\n ref={(node) => {\n innerRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n }}\n type=\"search\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n aria-label={name}\n placeholder={placeholder ?? name}\n // Keksdose live #333: *\"Entering the search here — is that set as password?\n // … I got prompted for remembering my name or mail address\"*, typed into the\n // settings filter on a phone.\n //\n // A `type=\"search\"` input with no `name` and no `autocomplete` is a field\n // Chrome's autofill classifies by GUESSWORK, from the labels around it and\n // the other fields on the page — and a settings page has an email and a\n // password on it. Naming the field and declaring the intent is what takes it\n // out of that guess: `autocomplete=\"off\"` says it is not part of any profile,\n // and a `name` of \"search\" is the strongest hint the heuristics take.\n //\n // The three that follow are not about autofill but about the same keyboard:\n // a filter box that capitalises the first letter and autocorrects \"2fa\" to\n // \"2FA\" is fighting the person typing into it. Declared BEFORE `{...rest}` so\n // a caller with a reason can still override any of them.\n name=\"search\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n autoCorrect=\"off\"\n spellCheck={false}\n {...rest}\n className={cn(\n FIELD_BASE,\n \"pl-9\",\n // Room for our own \"×\" only when there is one; a field that can't be\n // cleared has no reason to reserve the space.\n clearLabel === undefined ? \"pr-3\" : \"pr-9\",\n \"[&::-webkit-search-cancel-button]:appearance-none\",\n )}\n />\n {clearLabel !== undefined && value !== \"\" && (\n <button\n type=\"button\"\n onClick={() => {\n onChange(\"\");\n // Clearing is the start of the next query far more often than it is the\n // end of this one, so the caret stays where it can be typed into.\n innerRef.current?.focus();\n }}\n aria-label={clearLabel}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </button>\n )}\n </div>\n );\n});\nSearchField.displayName = \"SearchField\";\n"],"mappings":";AAiFI,SACE,KADF;AAjFJ,SAAS,YAAY,cAAc;AAEnC,SAAS,QAAQ,SAAS;AAC1B,SAAS,UAAU;AACnB,SAAS,kBAAkB;AAmEpB,MAAM,cAAc,WAA+C,SAASA,aACjF,EAAE,OAAO,UAAU,OAAO,YAAY,WAAW,aAAa,cAAc,WAAW,GAAG,KAAK,GAC/F,KACA;AACA,QAAM,WAAW,OAAyB,IAAI;AAI9C,QAAM,OAAO,aAAa;AAC1B,SACE,qBAAC,SAAI,WAAW,GAAG,YAAY,SAAS,GACtC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,eAAW;AAAA,QACX,WAAU;AAAA;AAAA,IACZ;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,CAAC,SAAS;AACb,mBAAS,UAAU;AACnB,cAAI,OAAO,QAAQ,WAAY,KAAI,IAAI;AAAA,mBAC9B,IAAK,KAAI,UAAU;AAAA,QAC9B;AAAA,QACA,MAAK;AAAA,QACL;AAAA,QACA,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,QACxC,cAAY;AAAA,QACZ,aAAa,eAAe;AAAA,QAgB5B,MAAK;AAAA,QACL,cAAa;AAAA,QACb,gBAAe;AAAA,QACf,aAAY;AAAA,QACZ,YAAY;AAAA,QACX,GAAG;AAAA,QACJ,WAAW;AAAA,UACT;AAAA,UACA;AAAA;AAAA;AAAA,UAGA,eAAe,SAAY,SAAS;AAAA,UACpC;AAAA,QACF;AAAA;AAAA,IACF;AAAA,IACC,eAAe,UAAa,UAAU,MACrC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS,MAAM;AACb,mBAAS,EAAE;AAGX,mBAAS,SAAS,MAAM;AAAA,QAC1B;AAAA,QACA,cAAY;AAAA,QACZ,WAAU;AAAA,QAEV,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,IACxB;AAAA,KAEJ;AAEJ,CAAC;AACD,YAAY,cAAc;","names":["SearchField"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface SeriesChartLabels {
|
|
2
|
+
/** The button that puts a zoomed chart back to its full extent. Visible text. */
|
|
3
|
+
resetZoom: string;
|
|
4
|
+
/** Accessible name of the drag surface laid over the plot. It is the only place the
|
|
5
|
+
* gesture is explained, so it says how the drag is read and how to undo it. */
|
|
6
|
+
zoomHint: string;
|
|
7
|
+
/** What a chart with nothing to draw says, when the caller passes no `empty`. */
|
|
8
|
+
empty: string;
|
|
9
|
+
/** Accessible name of a `ToggleLegend` — a group of switches, one per series. */
|
|
10
|
+
legend: string;
|
|
11
|
+
}
|
|
12
|
+
declare const DEFAULT_SERIES_CHART_LABELS: SeriesChartLabels;
|
|
13
|
+
|
|
14
|
+
export { DEFAULT_SERIES_CHART_LABELS, type SeriesChartLabels };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const DEFAULT_SERIES_CHART_LABELS = {
|
|
3
|
+
resetZoom: "Reset zoom",
|
|
4
|
+
zoomHint: "Drag to zoom: a roughly square selection zooms both axes, a long thin one only its own. Double-click to reset.",
|
|
5
|
+
empty: "No data",
|
|
6
|
+
legend: "Series"
|
|
7
|
+
};
|
|
8
|
+
export {
|
|
9
|
+
DEFAULT_SERIES_CHART_LABELS
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=series-chart-labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/series-chart-labels.ts"],"sourcesContent":["// All user-facing strings for the series chart family — `SeriesChart`, its drag-to-zoom\n// layer and `ToggleLegend` — as the `seriesChart` namespace of `<UiKitProvider labels>`,\n// and per chart the `labels` prop over that.\n//\n// A module of its own, with no recharts in it, so the label registry (src/i18n) can\n// import the defaults without pulling the chart library in behind them.\n\nexport interface SeriesChartLabels {\n /** The button that puts a zoomed chart back to its full extent. Visible text. */\n resetZoom: string;\n /** Accessible name of the drag surface laid over the plot. It is the only place the\n * gesture is explained, so it says how the drag is read and how to undo it. */\n zoomHint: string;\n /** What a chart with nothing to draw says, when the caller passes no `empty`. */\n empty: string;\n /** Accessible name of a `ToggleLegend` — a group of switches, one per series. */\n legend: string;\n}\n\nexport const DEFAULT_SERIES_CHART_LABELS: SeriesChartLabels = {\n resetZoom: \"Reset zoom\",\n zoomHint:\n \"Drag to zoom: a roughly square selection zooms both axes, a long thin one only its own. Double-click to reset.\",\n empty: \"No data\",\n legend: \"Series\",\n};\n"],"mappings":";AAmBO,MAAM,8BAAiD;AAAA,EAC5D,WAAW;AAAA,EACX,UACE;AAAA,EACF,OAAO;AAAA,EACP,QAAQ;AACV;","names":[]}
|