@eifi1/ui-kit 0.4.0 → 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 +91 -20
- 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 +96 -20
- package/dist/components/dropdown.js +77 -33
- 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 +38 -6
- package/dist/components/file-dropzone.js +34 -9
- 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 +28 -2
- package/dist/components/number-input.js +12 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +32 -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 +15 -4
- package/dist/components/user-avatar.js +5 -4
- 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 +27 -5
- package/dist/theme/palette-presets.js +231 -33
- 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 +67 -14
- package/src/chart.ts +22 -0
- package/src/components/account-settings.tsx +275 -0
- package/src/components/alert-banner.tsx +75 -0
- package/src/components/amount-input.tsx +471 -0
- package/src/components/calculator.tsx +252 -0
- package/src/components/chart-zoom.tsx +499 -0
- package/src/components/chart.tsx +363 -0
- package/src/components/checkbox.tsx +222 -0
- package/src/components/chip.tsx +570 -0
- package/src/components/choice-parts.ts +33 -0
- package/src/components/combobox-core.tsx +540 -0
- package/src/components/combobox.tsx +1026 -0
- package/src/components/currency-select.tsx +269 -0
- package/src/components/data-table-filter-popover.tsx +252 -0
- package/src/components/data-table-filters.ts +176 -0
- package/src/components/data-table-labels.ts +190 -0
- package/src/components/data-table-pagination.tsx +126 -0
- package/src/components/data-table-sort.ts +70 -0
- package/src/components/data-table.tsx +1740 -0
- package/src/components/date-picker.tsx +631 -0
- package/src/components/dropdown.tsx +341 -0
- package/src/components/entity-combobox.tsx +215 -0
- package/src/components/facing-pair.ts +86 -0
- package/src/components/field-sync.tsx +472 -0
- package/src/components/file-dropzone.tsx +181 -0
- package/src/components/full-bleed-dialog.tsx +179 -0
- package/src/components/grouped-picker.tsx +157 -0
- package/src/components/hover-menu.tsx +183 -0
- package/src/components/mini-calendar.tsx +554 -0
- package/src/components/modal.tsx +287 -0
- package/src/components/month-picker.tsx +549 -0
- package/src/components/multi-entity-combobox.tsx +226 -0
- package/src/components/multi-select.tsx +303 -0
- package/src/components/number-field.tsx +225 -0
- package/src/components/number-input.tsx +211 -0
- package/src/components/numpad-sheet.tsx +264 -0
- package/src/components/page-contents.tsx +289 -0
- package/src/components/password-strength.tsx +258 -0
- package/src/components/picker-sheet.tsx +251 -0
- package/src/components/popover.tsx +195 -0
- package/src/components/search-field.tsx +146 -0
- package/src/components/series-chart-labels.ts +26 -0
- package/src/components/series-chart.tsx +481 -0
- package/src/components/settings-fields.tsx +68 -0
- package/src/components/signature-pad.tsx +630 -0
- package/src/components/slider.tsx +313 -0
- package/src/components/sparkline.tsx +329 -0
- package/src/components/stat-tile.tsx +448 -0
- package/src/components/swipeable-row.tsx +271 -0
- package/src/components/switch.tsx +170 -0
- package/src/components/time-input.tsx +107 -0
- package/src/components/toggle-group.tsx +153 -0
- package/src/components/toggle-legend.tsx +229 -0
- package/src/components/tooltip.tsx +477 -0
- package/src/components/treemap.tsx +390 -0
- package/src/components/ui.tsx +1284 -0
- package/src/components/use-mobile-reveal.ts +54 -0
- package/src/components/use-table-state.ts +295 -0
- package/src/components/user-avatar.tsx +61 -0
- package/src/components/wizard-stepper.tsx +37 -0
- package/src/data-table.ts +19 -0
- package/src/feedback/feedback-attachment.tsx +246 -0
- package/src/feedback/feedback-dialog.tsx +209 -0
- package/src/feedback/feedback-inbox.tsx +615 -0
- package/src/feedback.ts +8 -0
- package/src/hooks/use-anchored-panel.ts +169 -0
- package/src/hooks/use-anchored-rect.ts +78 -0
- package/src/hooks/use-announce.ts +85 -0
- package/src/hooks/use-body-scroll-lock.ts +94 -0
- package/src/hooks/use-close-transition.ts +99 -0
- package/src/hooks/use-dismiss.ts +77 -0
- package/src/hooks/use-focus-trap.ts +160 -0
- package/src/hooks/use-media-query.ts +23 -0
- package/src/hooks/use-overlay-history.ts +345 -0
- package/src/hooks/use-row-swipe.ts +212 -0
- package/src/i18n/defaults.ts +68 -0
- package/src/i18n/kit-labels.tsx +447 -0
- package/src/index.ts +169 -0
- package/src/lib/calc.ts +309 -0
- package/src/lib/cn.ts +5 -0
- package/src/lib/dates.ts +187 -0
- package/src/lib/logger.ts +73 -0
- package/src/lib/safe-storage.ts +37 -0
- package/src/search/command-palette.tsx +342 -0
- package/src/search.ts +3 -0
- package/src/shell/app-shell.tsx +772 -0
- package/src/shell/option-switcher-menu.tsx +72 -0
- package/src/shell/role-switcher.tsx +46 -0
- package/src/shell/top-bar.tsx +39 -0
- package/src/shell/topbar-action-menu.tsx +91 -0
- package/src/shell/topbar-controls.tsx +229 -0
- package/src/shell.ts +10 -0
- package/src/theme/chart-palette.ts +145 -0
- package/src/theme/color.ts +266 -0
- package/src/theme/palette-derive.ts +681 -0
- package/src/theme/palette-presets.ts +760 -0
- package/src/theme/palette-store.ts +94 -0
- package/src/theme/theme-store.ts +145 -0
- package/src/tour/tour.tsx +586 -0
- package/src/tour.ts +3 -0
- package/src/vite-env.d.ts +26 -0
- package/src/wizard/stepper-nav.tsx +221 -0
- package/src/wizard/types.ts +178 -0
- package/src/wizard/use-wizard.ts +345 -0
- package/src/wizard/validation.ts +37 -0
- package/src/wizard/wizard-context.tsx +29 -0
- package/src/wizard/wizard-step.tsx +14 -0
- package/src/wizard/wizard-summary.tsx +69 -0
- package/src/wizard.ts +14 -0
- package/tokens.css +222 -20
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { useAnchoredPanel } from "../hooks/use-anchored-panel";
|
|
5
|
+
import { useEscapeKey, useOutsideClick } from "../hooks/use-dismiss";
|
|
6
|
+
import { useFocusTrap } from "../hooks/use-focus-trap";
|
|
7
|
+
import { cn } from "../lib/cn";
|
|
8
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
9
|
+
|
|
10
|
+
/** The panel's accessible name. A `role="dialog"` with no name announces as "dialog"
|
|
11
|
+
* and nothing else, which is worse than the unlabelled div it replaced — so it is a
|
|
12
|
+
* prop with an English default, like every other string this package renders. Callers
|
|
13
|
+
* should pass the control's own name ("Filter", "Choose a date"); the default is only
|
|
14
|
+
* there so an unmigrated call site is still named. */
|
|
15
|
+
export interface PopoverLabels {
|
|
16
|
+
panel: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Exported since the kit grew one label tree (`UiKitLabels`, src/i18n): the complete
|
|
20
|
+
// English reference a translator works from has to be able to name every namespace.
|
|
21
|
+
export const DEFAULT_POPOVER_LABELS: PopoverLabels = {
|
|
22
|
+
panel: "Popover",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Exported, and extending the `<div>` attributes, so a consumer wrapping this — which is
|
|
27
|
+
* what `DateField` and `FilterPopover` in this very package do — can hand anything
|
|
28
|
+
* through to the PANEL: a `data-tour` anchor for the kit's own guided tour, a test id,
|
|
29
|
+
* an `aria-describedby`, or an `aria-label` naming this particular popover.
|
|
30
|
+
*
|
|
31
|
+
* `children` is omitted from those attributes rather than inherited: here it is a render
|
|
32
|
+
* prop taking the panel's `close`, not a `ReactNode`, so the two cannot be reconciled.
|
|
33
|
+
*/
|
|
34
|
+
export interface PopoverProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
35
|
+
trigger: (state: { open: boolean; toggle: () => void; ref: RefObject<HTMLButtonElement | null> }) => ReactNode;
|
|
36
|
+
children: (close: () => void) => ReactNode;
|
|
37
|
+
width?: number;
|
|
38
|
+
labels?: Partial<PopoverLabels>;
|
|
39
|
+
/** Id for the panel, so a trigger wearing `role="combobox"` can `aria-controls` it.
|
|
40
|
+
* Required by ARIA on that role, and the panel is rendered here rather than by the
|
|
41
|
+
* caller, so the id has to come in from outside. */
|
|
42
|
+
panelId?: string;
|
|
43
|
+
/** Extra classes for the PANEL. The trigger is the caller's own element. */
|
|
44
|
+
className?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const POPOVER_WIDTH = 288;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Portal-rendered popover anchored under its trigger button. The panel is fixed
|
|
51
|
+
* to the viewport (so table/overflow ancestors can't clip it) and re-aligns on
|
|
52
|
+
* scroll/resize; it closes on outside-click and Escape.
|
|
53
|
+
*
|
|
54
|
+
* ## Trapped, not merely focused — and why, because it is the closer call
|
|
55
|
+
*
|
|
56
|
+
* The audit's finding was that this panel *"portals out of the tab order with no role
|
|
57
|
+
* and no focus move"*: Tab from the trigger went straight past the panel into the page
|
|
58
|
+
* behind it, so the calendar, the calculator keypad and every column filter in the kit
|
|
59
|
+
* were mouse-only. Two shapes fix that, and a popover is not automatically a dialog, so
|
|
60
|
+
* this was chosen rather than inherited:
|
|
61
|
+
*
|
|
62
|
+
* - **The light one** — move focus in on open, hand it back on close, and let Tab walk
|
|
63
|
+
* OUT of the panel into whatever follows the trigger. That is the right shape for a
|
|
64
|
+
* non-modal popover sitting inline in the document.
|
|
65
|
+
* - **The trap** — Tab cycles inside the panel; Escape and a click outside are the
|
|
66
|
+
* ways out. What is here.
|
|
67
|
+
*
|
|
68
|
+
* The deciding fact is the portal. This panel is rendered to the END of `<body>`, not
|
|
69
|
+
* beside its trigger, so "let Tab walk out" does not mean "carry on from the trigger" —
|
|
70
|
+
* it means land at the end of the document, several hundred controls from where the
|
|
71
|
+
* user was. Tabbing out of it would be a worse place to arrive than the one the finding
|
|
72
|
+
* complains about, and fixing that properly needs a sentinel-and-return dance that
|
|
73
|
+
* nothing in this package has. The trap gets the user back to the trigger every time,
|
|
74
|
+
* because `useFocusTrap` restores focus on close.
|
|
75
|
+
*
|
|
76
|
+
* And the panels this kit actually opens are self-contained: pick a date, tap out a
|
|
77
|
+
* sum, narrow a column. Each of them commits and closes, and each already has two
|
|
78
|
+
* documented ways out wired above — Escape and an outside press — so containment costs
|
|
79
|
+
* a keyboard user nothing they had.
|
|
80
|
+
*
|
|
81
|
+
* **It stays non-modal in every other respect**, deliberately: no backdrop, no body
|
|
82
|
+
* scroll lock, and no `aria-modal`, so the page behind is still readable by a screen
|
|
83
|
+
* reader, still scrollable, and still clickable — clicking it is what dismisses this.
|
|
84
|
+
* Only Tab is contained. A popover that took the page away as well would be a
|
|
85
|
+
* {@link Modal}, and there is one of those.
|
|
86
|
+
*/
|
|
87
|
+
export function Popover({
|
|
88
|
+
trigger,
|
|
89
|
+
children,
|
|
90
|
+
width = POPOVER_WIDTH,
|
|
91
|
+
labels,
|
|
92
|
+
panelId,
|
|
93
|
+
className,
|
|
94
|
+
style,
|
|
95
|
+
"aria-label": ariaLabel,
|
|
96
|
+
...rest
|
|
97
|
+
}: PopoverProps) {
|
|
98
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
99
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
100
|
+
const [open, setOpen] = useState(false);
|
|
101
|
+
const close = () => setOpen(false);
|
|
102
|
+
const toggle = () => setOpen((v) => !v);
|
|
103
|
+
// prop > `<UiKitProvider labels={{ popover }}>` > English. The provider's
|
|
104
|
+
// `popover.panel` is only the last-resort name for a panel no caller named.
|
|
105
|
+
const text = useKitLabels("popover", DEFAULT_POPOVER_LABELS, labels);
|
|
106
|
+
|
|
107
|
+
// Align the panel's right edge to the trigger's and clamp it horizontally; the
|
|
108
|
+
// hook owns the vertical half — below by default, flipped above (and height-capped)
|
|
109
|
+
// when that is where the room is, e.g. once a mobile keyboard has eaten the bottom
|
|
110
|
+
// of the screen (feedback #135). It re-measures on scroll/resize.
|
|
111
|
+
const placement = useAnchoredPanel(triggerRef, open);
|
|
112
|
+
const rect = placement.rect;
|
|
113
|
+
const pos = rect
|
|
114
|
+
? {
|
|
115
|
+
top: placement.top,
|
|
116
|
+
left: Math.min(Math.max(8, rect.right - width), window.innerWidth - width - 8),
|
|
117
|
+
}
|
|
118
|
+
: null;
|
|
119
|
+
|
|
120
|
+
useOutsideClick([triggerRef, panelRef], close, open);
|
|
121
|
+
useEscapeKey(close, open);
|
|
122
|
+
|
|
123
|
+
useFocusTrap(panelRef, {
|
|
124
|
+
active: open && pos !== null,
|
|
125
|
+
// The panel keeps a focus one of its own children has already taken. `FilterPopover`
|
|
126
|
+
// autofocuses its text box on mount — that is what the press on "Filter" was FOR,
|
|
127
|
+
// and it is a prop there precisely so the popover keeps it — and a trap that
|
|
128
|
+
// unconditionally focused the container would take it straight back off again,
|
|
129
|
+
// one effect later and invisibly.
|
|
130
|
+
initialFocus: () => {
|
|
131
|
+
const panel = panelRef.current;
|
|
132
|
+
const active = document.activeElement;
|
|
133
|
+
return panel?.contains(active) ? (active as HTMLElement) : panel;
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
138
|
+
if (e.key !== "Escape") return;
|
|
139
|
+
// `useEscapeKey` above listens on the document and still covers the case where
|
|
140
|
+
// focus somehow is not in here. This handler is for the case that focus IS, which
|
|
141
|
+
// is now the normal one — and it exists to STOP the key, because a popover opened
|
|
142
|
+
// from inside a `Modal` (the calculator on an amount field is exactly that) would
|
|
143
|
+
// otherwise have its Escape travel on up the React tree and close the dialog
|
|
144
|
+
// underneath as well. Closing twice is harmless — `close` is a state setter, not a
|
|
145
|
+
// caller's callback — so the two paths cannot double-fire anything.
|
|
146
|
+
e.stopPropagation();
|
|
147
|
+
close();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
{trigger({ open, toggle, ref: triggerRef })}
|
|
153
|
+
{open && pos &&
|
|
154
|
+
createPortal(
|
|
155
|
+
<div
|
|
156
|
+
// `...rest` first: the role, the id the trigger's `aria-controls` points at,
|
|
157
|
+
// the `tabIndex` the trap needs and the measured position are what make this
|
|
158
|
+
// a popover, and a caller reaching for a `data-tour` anchor must not be able
|
|
159
|
+
// to remove one of them by accident.
|
|
160
|
+
{...rest}
|
|
161
|
+
ref={panelRef}
|
|
162
|
+
role="dialog"
|
|
163
|
+
id={panelId}
|
|
164
|
+
// The caller's own `aria-label` wins over {@link PopoverLabels.panel}, which
|
|
165
|
+
// is the package's English fallback for an unnamed panel. Both name the same
|
|
166
|
+
// element; the DOM spelling is the one being standardised on, so where both
|
|
167
|
+
// are present it is the one that survives.
|
|
168
|
+
aria-label={ariaLabel ?? text.panel}
|
|
169
|
+
// Without it the panel cannot take focus at all, and the trap above would
|
|
170
|
+
// silently do nothing on a panel whose children are not yet tabbable.
|
|
171
|
+
tabIndex={-1}
|
|
172
|
+
onKeyDown={handleKeyDown}
|
|
173
|
+
style={{
|
|
174
|
+
// Merged over the caller's, never replaced by it: these four are measured
|
|
175
|
+
// against the trigger every scroll and resize, and a panel that dropped
|
|
176
|
+
// them would paint at the top-left of the viewport.
|
|
177
|
+
...style,
|
|
178
|
+
position: "fixed",
|
|
179
|
+
top: pos.top,
|
|
180
|
+
left: pos.left,
|
|
181
|
+
width,
|
|
182
|
+
maxHeight: placement.maxHeight,
|
|
183
|
+
}}
|
|
184
|
+
className={cn(
|
|
185
|
+
"z-50 overflow-y-auto rounded-md border border-[var(--border)] bg-[var(--bg-surface)] p-3 shadow-lg outline-none",
|
|
186
|
+
className,
|
|
187
|
+
)}
|
|
188
|
+
>
|
|
189
|
+
{children(close)}
|
|
190
|
+
</div>,
|
|
191
|
+
document.body,
|
|
192
|
+
)}
|
|
193
|
+
</>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { forwardRef, useRef } from "react";
|
|
2
|
+
import type { InputHTMLAttributes } from "react";
|
|
3
|
+
import { Search, X } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { FIELD_BASE } from "./ui";
|
|
6
|
+
|
|
7
|
+
interface SearchFieldOwnProps
|
|
8
|
+
extends Omit<
|
|
9
|
+
InputHTMLAttributes<HTMLInputElement>,
|
|
10
|
+
"value" | "onChange" | "type" | "aria-label"
|
|
11
|
+
> {
|
|
12
|
+
value: string;
|
|
13
|
+
/** The typed text. Emitted as a plain string, not an event: every caller of the
|
|
14
|
+
* four this replaced wanted the string, and one of them held it in a URL param
|
|
15
|
+
* rather than in state. */
|
|
16
|
+
onChange: (value: string) => void;
|
|
17
|
+
/**
|
|
18
|
+
* The field's accessible name.
|
|
19
|
+
*
|
|
20
|
+
* @deprecated Pass `aria-label` instead — the DOM spelling this kit standardised on
|
|
21
|
+
* when it found three names for one idea (`ariaLabel`, `aria-label`, and this).
|
|
22
|
+
* Unchanged and still supported; it names the field only when `aria-label` is
|
|
23
|
+
* absent, so a consumer can migrate one call site at a time.
|
|
24
|
+
*/
|
|
25
|
+
label?: string;
|
|
26
|
+
/** Names the clear button. Omit it and no clear button is rendered — which is a
|
|
27
|
+
* decision, not a default: a filter you can type into and not untype is the
|
|
28
|
+
* complaint that produced the button on three of these four screens. */
|
|
29
|
+
clearLabel?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One of the two spellings is REQUIRED, and the union is how that survives the
|
|
34
|
+
* deprecation. Making `label` merely optional would have been the easy change and the
|
|
35
|
+
* wrong one: the whole reason this component exists is that two of the four boxes it
|
|
36
|
+
* replaced were named by nothing but their placeholder — which disappears the moment
|
|
37
|
+
* someone types, i.e. exactly when a screen-reader user is working in the field. A
|
|
38
|
+
* caller now has a choice of spelling; it still has no option to pass neither.
|
|
39
|
+
*/
|
|
40
|
+
export type SearchFieldProps = SearchFieldOwnProps &
|
|
41
|
+
({ "aria-label": string; label?: string } | { label: string; "aria-label"?: string });
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The page-level filter box: a search icon, a `type="search"` input, and a clear
|
|
45
|
+
* "×" that appears once there is something to clear.
|
|
46
|
+
*
|
|
47
|
+
* ## Why it is a component
|
|
48
|
+
*
|
|
49
|
+
* Keksdose had four of these and no two agreed. Two were character-identical
|
|
50
|
+
* twenty-line copies (the settings catalogue filter and the tour list filter) —
|
|
51
|
+
* the same absolute icon at `left-3`, the same `pl-9 pr-9`, the same
|
|
52
|
+
* `[&::-webkit-search-cancel-button]:appearance-none`, the same conditional clear
|
|
53
|
+
* button at `right-2` — differing only in which state setter they called. The
|
|
54
|
+
* third (the admin support queue) was the same idea at `left-2`/`size-3.5`/`pl-7`
|
|
55
|
+
* with no clear button and no `type="search"`, so it had neither the `searchbox`
|
|
56
|
+
* role nor a way to empty itself. The fourth (the bank picker) was a bare `<Input
|
|
57
|
+
* placeholder="Search">`: no icon, no clear, and no accessible name at all once
|
|
58
|
+
* the placeholder went away.
|
|
59
|
+
*
|
|
60
|
+
* None of that is a design decision anyone made; it is four people solving the
|
|
61
|
+
* same problem on four days. A shared component is how the fourth screen gets the
|
|
62
|
+
* clear button the first one earned.
|
|
63
|
+
*
|
|
64
|
+
* ## The two X's
|
|
65
|
+
*
|
|
66
|
+
* `type="search"` is what gives the field its `searchbox` role — and, in Chrome, a
|
|
67
|
+
* NATIVE clear cross that paints right next to ours, so a non-empty box shows two
|
|
68
|
+
* X's an inch apart. Ours is the one that survives: it carries an accessible name
|
|
69
|
+
* and matches the app's other icon buttons, and it returns focus to the input so
|
|
70
|
+
* the next query can be typed straight away.
|
|
71
|
+
*/
|
|
72
|
+
export const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(function SearchField(
|
|
73
|
+
{ value, onChange, label, clearLabel, className, placeholder, "aria-label": ariaLabel, ...rest },
|
|
74
|
+
ref,
|
|
75
|
+
) {
|
|
76
|
+
const innerRef = useRef<HTMLInputElement>(null);
|
|
77
|
+
// Whichever spelling the caller used. It is the accessible name AND the placeholder
|
|
78
|
+
// fallback, so a box named only by `aria-label` still says what it is before anyone
|
|
79
|
+
// has typed in it — the same deal `label` has always had.
|
|
80
|
+
const name = ariaLabel ?? label;
|
|
81
|
+
return (
|
|
82
|
+
<div className={cn("relative", className)}>
|
|
83
|
+
<Search
|
|
84
|
+
aria-hidden
|
|
85
|
+
className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]"
|
|
86
|
+
/>
|
|
87
|
+
<input
|
|
88
|
+
ref={(node) => {
|
|
89
|
+
innerRef.current = node;
|
|
90
|
+
if (typeof ref === "function") ref(node);
|
|
91
|
+
else if (ref) ref.current = node;
|
|
92
|
+
}}
|
|
93
|
+
type="search"
|
|
94
|
+
value={value}
|
|
95
|
+
onChange={(e) => onChange(e.target.value)}
|
|
96
|
+
aria-label={name}
|
|
97
|
+
placeholder={placeholder ?? name}
|
|
98
|
+
// Keksdose live #333: *"Entering the search here — is that set as password?
|
|
99
|
+
// … I got prompted for remembering my name or mail address"*, typed into the
|
|
100
|
+
// settings filter on a phone.
|
|
101
|
+
//
|
|
102
|
+
// A `type="search"` input with no `name` and no `autocomplete` is a field
|
|
103
|
+
// Chrome's autofill classifies by GUESSWORK, from the labels around it and
|
|
104
|
+
// the other fields on the page — and a settings page has an email and a
|
|
105
|
+
// password on it. Naming the field and declaring the intent is what takes it
|
|
106
|
+
// out of that guess: `autocomplete="off"` says it is not part of any profile,
|
|
107
|
+
// and a `name` of "search" is the strongest hint the heuristics take.
|
|
108
|
+
//
|
|
109
|
+
// The three that follow are not about autofill but about the same keyboard:
|
|
110
|
+
// a filter box that capitalises the first letter and autocorrects "2fa" to
|
|
111
|
+
// "2FA" is fighting the person typing into it. Declared BEFORE `{...rest}` so
|
|
112
|
+
// a caller with a reason can still override any of them.
|
|
113
|
+
name="search"
|
|
114
|
+
autoComplete="off"
|
|
115
|
+
autoCapitalize="none"
|
|
116
|
+
autoCorrect="off"
|
|
117
|
+
spellCheck={false}
|
|
118
|
+
{...rest}
|
|
119
|
+
className={cn(
|
|
120
|
+
FIELD_BASE,
|
|
121
|
+
"pl-9",
|
|
122
|
+
// Room for our own "×" only when there is one; a field that can't be
|
|
123
|
+
// cleared has no reason to reserve the space.
|
|
124
|
+
clearLabel === undefined ? "pr-3" : "pr-9",
|
|
125
|
+
"[&::-webkit-search-cancel-button]:appearance-none",
|
|
126
|
+
)}
|
|
127
|
+
/>
|
|
128
|
+
{clearLabel !== undefined && value !== "" && (
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
onClick={() => {
|
|
132
|
+
onChange("");
|
|
133
|
+
// Clearing is the start of the next query far more often than it is the
|
|
134
|
+
// end of this one, so the caret stays where it can be typed into.
|
|
135
|
+
innerRef.current?.focus();
|
|
136
|
+
}}
|
|
137
|
+
aria-label={clearLabel}
|
|
138
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]"
|
|
139
|
+
>
|
|
140
|
+
<X className="size-4" />
|
|
141
|
+
</button>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
SearchField.displayName = "SearchField";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// All user-facing strings for the series chart family — `SeriesChart`, its drag-to-zoom
|
|
2
|
+
// layer and `ToggleLegend` — as the `seriesChart` namespace of `<UiKitProvider labels>`,
|
|
3
|
+
// and per chart the `labels` prop over that.
|
|
4
|
+
//
|
|
5
|
+
// A module of its own, with no recharts in it, so the label registry (src/i18n) can
|
|
6
|
+
// import the defaults without pulling the chart library in behind them.
|
|
7
|
+
|
|
8
|
+
export interface SeriesChartLabels {
|
|
9
|
+
/** The button that puts a zoomed chart back to its full extent. Visible text. */
|
|
10
|
+
resetZoom: string;
|
|
11
|
+
/** Accessible name of the drag surface laid over the plot. It is the only place the
|
|
12
|
+
* gesture is explained, so it says how the drag is read and how to undo it. */
|
|
13
|
+
zoomHint: string;
|
|
14
|
+
/** What a chart with nothing to draw says, when the caller passes no `empty`. */
|
|
15
|
+
empty: string;
|
|
16
|
+
/** Accessible name of a `ToggleLegend` — a group of switches, one per series. */
|
|
17
|
+
legend: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_SERIES_CHART_LABELS: SeriesChartLabels = {
|
|
21
|
+
resetZoom: "Reset zoom",
|
|
22
|
+
zoomHint:
|
|
23
|
+
"Drag to zoom: a roughly square selection zooms both axes, a long thin one only its own. Double-click to reset.",
|
|
24
|
+
empty: "No data",
|
|
25
|
+
legend: "Series",
|
|
26
|
+
};
|