@eifi1/ui-kit 0.10.0 → 0.12.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/README.md +66 -37
- package/dist/chart.d.ts +3 -1
- package/dist/chart.js +2 -0
- package/dist/chart.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +105 -0
- package/dist/components/account-settings-labels.js +81 -0
- package/dist/components/account-settings-labels.js.map +1 -0
- package/dist/components/account-settings.d.ts +36 -52
- package/dist/components/account-settings.js +44 -8
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/alert-banner.d.ts +26 -1
- package/dist/components/alert-banner.js +10 -7
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +26 -4
- package/dist/components/amount-input.js +5 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +68 -0
- package/dist/components/authed-image.js +117 -0
- package/dist/components/authed-image.js.map +1 -0
- package/dist/components/breadcrumbs.d.ts +2 -1
- package/dist/components/breadcrumbs.js +5 -2
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +51 -5
- package/dist/components/bulk-action-bar.js +88 -41
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +107 -3
- package/dist/components/button-group.js +60 -5
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +17 -4
- package/dist/components/calendar-heatmap.d.ts +162 -0
- package/dist/components/calendar-heatmap.js +336 -0
- package/dist/components/calendar-heatmap.js.map +1 -0
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +6 -3
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +66 -2
- package/dist/components/choice-card.js +108 -1
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.js +2 -0
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +2 -2
- package/dist/components/combobox.js +31 -9
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +52 -70
- package/dist/components/copy-button.js +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +63 -0
- package/dist/components/data-table-cells.js +64 -0
- package/dist/components/data-table-cells.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-filters.js +31 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +13 -0
- package/dist/components/data-table-labels.js +8 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +104 -12
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +3 -1
- package/dist/components/date-picker.js +15 -2
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +23 -3
- package/dist/components/description-list.js +23 -3
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +1 -0
- package/dist/components/entity-combobox.d.ts +1 -1
- package/dist/components/entity-combobox.js +18 -3
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +92 -0
- package/dist/components/error-boundary.js +111 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/field.d.ts +69 -0
- package/dist/components/field.js +75 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/file-button.d.ts +51 -208
- package/dist/components/file-dropzone.d.ts +50 -2
- package/dist/components/floating-panel.d.ts +156 -4
- package/dist/components/floating-panel.js +193 -29
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +53 -0
- package/dist/components/form-actions.js +98 -0
- package/dist/components/form-actions.js.map +1 -0
- package/dist/components/image-grid.d.ts +67 -0
- package/dist/components/image-grid.js +112 -0
- package/dist/components/image-grid.js.map +1 -0
- package/dist/components/lightbox.d.ts +104 -0
- package/dist/components/lightbox.js +229 -0
- package/dist/components/lightbox.js.map +1 -0
- package/dist/components/line-items.d.ts +114 -0
- package/dist/components/line-items.js +238 -0
- package/dist/components/line-items.js.map +1 -0
- package/dist/components/list.d.ts +37 -3
- package/dist/components/list.js +39 -14
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +24 -0
- package/dist/components/loading-state.js +32 -0
- package/dist/components/loading-state.js.map +1 -0
- package/dist/components/menu-item.d.ts +13 -2
- package/dist/components/menu-item.js +11 -2
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +82 -5
- package/dist/components/mini-calendar.js +212 -64
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +20 -1
- package/dist/components/modal.js +26 -2
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +21 -1
- package/dist/components/month-picker.js +139 -26
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +1 -1
- package/dist/components/multi-entity-combobox.js +19 -3
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/nav-pills.d.ts +67 -0
- package/dist/components/nav-pills.js +69 -0
- package/dist/components/nav-pills.js.map +1 -0
- package/dist/components/number-field.d.ts +26 -5
- package/dist/components/number-field.js +5 -7
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +27 -5
- package/dist/components/number-input.js +7 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +17 -4
- package/dist/components/passkeys-setting.d.ts +67 -0
- package/dist/components/passkeys-setting.js +214 -0
- package/dist/components/passkeys-setting.js.map +1 -0
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +20 -0
- package/dist/components/pie-chart-labels.js +12 -0
- package/dist/components/pie-chart-labels.js.map +1 -0
- package/dist/components/pie-chart.d.ts +101 -0
- package/dist/components/pie-chart.js +283 -0
- package/dist/components/pie-chart.js.map +1 -0
- package/dist/components/progress-bar.d.ts +73 -3
- package/dist/components/progress-bar.js +110 -8
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.d.ts +34 -0
- package/dist/components/qr-code.js +57 -0
- package/dist/components/qr-code.js.map +1 -0
- package/dist/components/search-field.d.ts +15 -3
- package/dist/components/search-field.js +5 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/series-chart.d.ts +16 -0
- package/dist/components/series-chart.js +4 -1
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +50 -1
- package/dist/components/signed-amount.d.ts +121 -0
- package/dist/components/signed-amount.js +152 -0
- package/dist/components/signed-amount.js.map +1 -0
- package/dist/components/skeleton.d.ts +10 -3
- package/dist/components/skeleton.js +12 -2
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/stat-tile.d.ts +2 -1
- package/dist/components/stat-tile.js +5 -2
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +6 -2
- package/dist/components/status-dot.js +6 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/table.d.ts +77 -11
- package/dist/components/table.js +85 -17
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +176 -0
- package/dist/components/text-link.js +109 -0
- package/dist/components/text-link.js.map +1 -0
- package/dist/components/text.d.ts +8 -1
- package/dist/components/text.js +1 -0
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +50 -1
- package/dist/components/toggle-group.d.ts +28 -0
- package/dist/components/toggle-group.js +58 -9
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +36 -3
- package/dist/components/toggle-legend.js +49 -0
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +11 -3
- package/dist/components/tooltip.js +2 -0
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +52 -729
- package/dist/components/ui.js +296 -27
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +3 -52
- package/dist/components/use-table-state.js +128 -30
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.d.ts +25 -3
- package/dist/components/user-avatar.js +32 -3
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
- package/dist/data-table.d.ts +2 -1
- package/dist/data-table.js +5 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +26 -3
- package/dist/feedback/feedback-dialog.js +16 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +133 -0
- package/dist/feedback/feedback-thread.js +237 -0
- package/dist/feedback/feedback-thread.js.map +1 -0
- package/dist/feedback.d.ts +2 -1
- package/dist/feedback.js +1 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-authed-src.d.ts +71 -0
- package/dist/hooks/use-authed-src.js +63 -0
- package/dist/hooks/use-authed-src.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +50 -2
- package/dist/hooks/use-hotkey.d.ts +59 -0
- package/dist/hooks/use-hotkey.js +105 -0
- package/dist/hooks/use-hotkey.js.map +1 -0
- package/dist/hooks/use-search-param-state.d.ts +83 -0
- package/dist/hooks/use-search-param-state.js +94 -0
- package/dist/hooks/use-search-param-state.js.map +1 -0
- package/dist/i18n/defaults.d.ts +17 -4
- package/dist/i18n/defaults.js +33 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +47 -310
- package/dist/i18n/kit-labels.js +20 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +17 -4
- package/dist/i18n/locales/de-CH.d.ts +17 -4
- package/dist/i18n/locales/de-informal.d.ts +17 -4
- package/dist/i18n/locales/de.d.ts +17 -4
- package/dist/i18n/locales/de.js +165 -4
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +17 -4
- package/dist/i18n/locales/es.js +162 -4
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +17 -4
- package/dist/i18n/locales/fr.js +162 -4
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +17 -4
- package/dist/i18n/locales/hu.js +162 -4
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +17 -4
- package/dist/i18n/locales/it.js +162 -4
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +17 -4
- package/dist/i18n/locales/zh.js +162 -4
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +45 -21
- package/dist/index.js +43 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
- package/dist/lib/clipping.d.ts +21 -5
- package/dist/lib/clipping.js +3 -0
- package/dist/lib/clipping.js.map +1 -1
- package/dist/lib/dates.d.ts +17 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/format.d.ts +144 -0
- package/dist/lib/format.js +149 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/qr-encode.d.ts +44 -0
- package/dist/lib/qr-encode.js +338 -0
- package/dist/lib/qr-encode.js.map +1 -0
- package/dist/rhf/fields.d.ts +241 -0
- package/dist/rhf/fields.js +601 -0
- package/dist/rhf/fields.js.map +1 -0
- package/dist/rhf/form.d.ts +50 -1
- package/dist/rhf/line-items.d.ts +31 -0
- package/dist/rhf/line-items.js +59 -0
- package/dist/rhf/line-items.js.map +1 -0
- package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
- package/dist/rhf/use-rhf-wizard-step.js +38 -0
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
- package/dist/rhf.d.ts +63 -1
- package/dist/rhf.js +3 -0
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +99 -2
- package/dist/shell/app-shell.js +58 -3
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.d.ts +74 -0
- package/dist/shell/auth-layout.js +97 -0
- package/dist/shell/auth-layout.js.map +1 -0
- package/dist/shell/top-bar-brand.d.ts +81 -0
- package/dist/shell/top-bar-brand.js +47 -0
- package/dist/shell/top-bar-brand.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +84 -3
- package/dist/shell/topbar-action-menu.js +115 -27
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +58 -2
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +49 -1
- package/dist/wizard/wizard-context.d.ts +30 -1
- package/dist/wizard/wizard-context.js +23 -2
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +20 -5
- package/dist/wizard/wizard-step.js +16 -3
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +51 -3
- package/package.json +1 -1
- package/src/chart.ts +4 -0
- package/src/components/account-settings-labels.ts +190 -0
- package/src/components/account-settings.tsx +110 -63
- package/src/components/alert-banner.tsx +36 -7
- package/src/components/amount-input.tsx +13 -1
- package/src/components/authed-image.tsx +190 -0
- package/src/components/breadcrumbs.tsx +8 -2
- package/src/components/bulk-action-bar.tsx +163 -57
- package/src/components/button-group.tsx +129 -4
- package/src/components/calendar-heatmap.tsx +599 -0
- package/src/components/chip.tsx +8 -4
- package/src/components/choice-card.tsx +208 -1
- package/src/components/combobox-core.tsx +5 -0
- package/src/components/combobox.tsx +31 -9
- package/src/components/copy-button.tsx +3 -2
- package/src/components/data-table-cells.tsx +119 -0
- package/src/components/data-table-filters.ts +77 -0
- package/src/components/data-table-labels.ts +23 -0
- package/src/components/data-table.tsx +231 -13
- package/src/components/date-picker.tsx +19 -3
- package/src/components/description-list.tsx +52 -3
- package/src/components/entity-combobox.tsx +18 -3
- package/src/components/error-boundary.tsx +216 -0
- package/src/components/field.tsx +188 -0
- package/src/components/floating-panel.tsx +380 -16
- package/src/components/form-actions.tsx +191 -0
- package/src/components/image-grid.tsx +181 -0
- package/src/components/lightbox.tsx +374 -0
- package/src/components/line-items.tsx +385 -0
- package/src/components/list.tsx +93 -18
- package/src/components/loading-state.tsx +47 -0
- package/src/components/menu-item.tsx +29 -3
- package/src/components/mini-calendar.tsx +317 -57
- package/src/components/modal.tsx +61 -3
- package/src/components/month-picker.tsx +181 -26
- package/src/components/multi-entity-combobox.tsx +19 -3
- package/src/components/nav-pills.tsx +152 -0
- package/src/components/number-field.tsx +15 -10
- package/src/components/number-input.tsx +16 -1
- package/src/components/passkeys-setting.tsx +331 -0
- package/src/components/picker-sheet.tsx +7 -1
- package/src/components/pie-chart-labels.ts +32 -0
- package/src/components/pie-chart.tsx +479 -0
- package/src/components/progress-bar.tsx +241 -13
- package/src/components/qr-code.tsx +83 -0
- package/src/components/search-field.tsx +20 -4
- package/src/components/series-chart.tsx +21 -0
- package/src/components/signed-amount.tsx +286 -0
- package/src/components/skeleton.tsx +21 -3
- package/src/components/stat-tile.tsx +8 -2
- package/src/components/status-dot.tsx +10 -2
- package/src/components/table.tsx +186 -23
- package/src/components/text-link.tsx +248 -0
- package/src/components/text.tsx +9 -1
- package/src/components/toggle-group.tsx +106 -13
- package/src/components/toggle-legend.tsx +87 -2
- package/src/components/tooltip.tsx +17 -3
- package/src/components/ui.tsx +572 -44
- package/src/components/use-table-state.ts +265 -33
- package/src/components/user-avatar.tsx +57 -3
- package/src/data-table.ts +10 -0
- package/src/feedback/feedback-dialog.tsx +42 -3
- package/src/feedback/feedback-thread.tsx +434 -0
- package/src/feedback.ts +1 -0
- package/src/hooks/use-authed-src.ts +161 -0
- package/src/hooks/use-hotkey.ts +172 -0
- package/src/hooks/use-search-param-state.ts +187 -0
- package/src/i18n/defaults.ts +32 -0
- package/src/i18n/kit-labels.tsx +104 -8
- package/src/i18n/locales/de.ts +162 -0
- package/src/i18n/locales/es.ts +159 -0
- package/src/i18n/locales/fr.ts +162 -0
- package/src/i18n/locales/hu.ts +160 -0
- package/src/i18n/locales/it.ts +160 -0
- package/src/i18n/locales/zh.ts +159 -0
- package/src/index.ts +64 -0
- package/src/lib/clipping.ts +22 -4
- package/src/lib/dates.ts +37 -0
- package/src/lib/format.ts +325 -0
- package/src/lib/qr-encode.ts +420 -0
- package/src/rhf/fields.tsx +1054 -0
- package/src/rhf/line-items.tsx +130 -0
- package/src/rhf/use-rhf-wizard-step.ts +113 -0
- package/src/rhf.ts +8 -0
- package/src/shell/app-shell.tsx +147 -2
- package/src/shell/auth-layout.tsx +190 -0
- package/src/shell/top-bar-brand.tsx +76 -0
- package/src/shell/topbar-action-menu.tsx +260 -49
- package/src/shell.ts +2 -0
- package/src/wizard/wizard-context.tsx +50 -1
- package/src/wizard/wizard-step.tsx +43 -6
- package/tokens.css +12 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { CSSProperties, ReactNode, TdHTMLAttributes, ThHTMLAttributes } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import {
|
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
ChevronDown,
|
|
9
9
|
ChevronRight,
|
|
10
10
|
Filter,
|
|
11
|
+
Pencil,
|
|
12
|
+
Trash2,
|
|
11
13
|
X,
|
|
12
14
|
} from "lucide-react";
|
|
13
|
-
import { Card, Spinner } from "./ui";
|
|
15
|
+
import { Card, IconButton, Spinner } from "./ui";
|
|
14
16
|
import { cn } from "../lib/cn";
|
|
15
17
|
import {
|
|
16
18
|
defaultFilterState,
|
|
@@ -239,8 +241,11 @@ export interface DataTableProps<T> {
|
|
|
239
241
|
* shareable and bookmarkable. URL params used: `f.<column>`, `sort`, `p`, `ps`.
|
|
240
242
|
* URL state takes precedence over localStorage on initial load. Requires the
|
|
241
243
|
* consuming app to render inside a react-router Router.
|
|
244
|
+
*
|
|
245
|
+
* `{ prefix }` namespaces them, for two URL-synced tables on one page:
|
|
246
|
+
* `urlSync={{ prefix: "inv." }}` uses `inv.f.<column>`, `inv.sort`, `inv.p`, `inv.ps`.
|
|
242
247
|
*/
|
|
243
|
-
urlSync?:
|
|
248
|
+
urlSync?: TableUrlSync;
|
|
244
249
|
/**
|
|
245
250
|
* When true (desktop only), the table fills its parent's height and scrolls
|
|
246
251
|
* INTERNALLY, with the header pinned — so the page itself doesn't add a second,
|
|
@@ -431,6 +436,98 @@ export interface DataTableProps<T> {
|
|
|
431
436
|
/** Extra classes for the table's root element (the card, or with `frame={false}`
|
|
432
437
|
* the plain wrapper). Merged last, so a caller's margin or width wins. */
|
|
433
438
|
className?: string;
|
|
439
|
+
/**
|
|
440
|
+
* Per-row actions: an actions column of icon buttons at the row's end on desktop,
|
|
441
|
+
* and the same actions as swipe actions on a phone card (where the column is not
|
|
442
|
+
* shown). kastlan's `ResourceListPage` appended this column by hand to every CRUD
|
|
443
|
+
* list — the edit pencil, the delete bin, a `useConfirm` before the delete, and a
|
|
444
|
+
* `stopPropagation` on each so a press did not also open the row.
|
|
445
|
+
*
|
|
446
|
+
* `kind: "edit"` / `"delete"` bring the icon, the label (`labels.edit` /
|
|
447
|
+
* `labels.delete`), the tone and the swipe side; any other action states its own
|
|
448
|
+
* `label` and `icon`. `confirm` is the hook-in for a confirmation: pass the kit's
|
|
449
|
+
* `useConfirm()` (or anything answering a boolean) and the action runs only on a yes.
|
|
450
|
+
*
|
|
451
|
+
* const confirm = useConfirm();
|
|
452
|
+
* rowActions={[
|
|
453
|
+
* { kind: "edit", onAction: openEditor },
|
|
454
|
+
* { kind: "delete", onAction: (r) => remove(r.id),
|
|
455
|
+
* confirm: (r) => confirm({ title: t("delete", { name: r.name }), tone: "danger" }) },
|
|
456
|
+
* ]}
|
|
457
|
+
*
|
|
458
|
+
* A function receives the row, for actions that differ per row. An explicit
|
|
459
|
+
* `mobileSwipeActions` wins over the swipes derived from here.
|
|
460
|
+
*/
|
|
461
|
+
rowActions?: DataTableRowAction<T>[] | ((row: T) => DataTableRowAction<T>[]);
|
|
462
|
+
/**
|
|
463
|
+
* The bar above the table — a search field, filter chips, a create button. Inside
|
|
464
|
+
* the table's frame, above the header, on desktop and phone alike (resource-list
|
|
465
|
+
* pages set a search box in a `div` above the card, outside the frame it filters).
|
|
466
|
+
*/
|
|
467
|
+
toolbar?: ReactNode;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** One entry of {@link DataTableProps.rowActions}. */
|
|
471
|
+
export interface DataTableRowAction<T> {
|
|
472
|
+
/** A preset: `"edit"` (pencil, `labels.edit`, swipes toward the END) or `"delete"`
|
|
473
|
+
* (bin, `labels.delete`, danger tone, swipes toward the START). */
|
|
474
|
+
kind?: "edit" | "delete";
|
|
475
|
+
/** The action's name — its tooltip, accessible name and swipe label. Required
|
|
476
|
+
* without a `kind`. */
|
|
477
|
+
label?: string;
|
|
478
|
+
/** The glyph. Required without a `kind`. */
|
|
479
|
+
icon?: ReactNode;
|
|
480
|
+
/** Runs after `confirm` (when given) has said yes. */
|
|
481
|
+
onAction: (row: T) => void;
|
|
482
|
+
/** Asked before `onAction`; a `false` (or a promise of one) cancels. */
|
|
483
|
+
confirm?: (row: T) => boolean | Promise<boolean>;
|
|
484
|
+
/** Default: `"danger"` for `kind: "delete"`, else `"default"`. */
|
|
485
|
+
tone?: "default" | "danger";
|
|
486
|
+
/** Leave the action out for this row. */
|
|
487
|
+
hidden?: (row: T) => boolean;
|
|
488
|
+
/** Why the action is locked for this row — the button stays focusable and says so,
|
|
489
|
+
* and the row gets no swipe for it. See `IconButton`'s `disabledReason`. */
|
|
490
|
+
disabledReason?: (row: T) => ReactNode;
|
|
491
|
+
/** Which way the phone card is dragged to commit it, or `false` for no swipe.
|
|
492
|
+
* Default: `"start"` for a danger action, `"end"` otherwise. */
|
|
493
|
+
swipe?: "start" | "end" | false;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** The actions column's key. Not a column a caller declares, so it is kept out of the
|
|
497
|
+
* column-settings panel. */
|
|
498
|
+
const ROW_ACTIONS_KEY = "__rowActions";
|
|
499
|
+
|
|
500
|
+
interface ResolvedRowAction<T> {
|
|
501
|
+
key: string;
|
|
502
|
+
label: string;
|
|
503
|
+
icon: ReactNode;
|
|
504
|
+
tone: "default" | "danger";
|
|
505
|
+
swipe: "start" | "end" | false;
|
|
506
|
+
action: DataTableRowAction<T>;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function resolveRowAction<T>(
|
|
510
|
+
action: DataTableRowAction<T>,
|
|
511
|
+
index: number,
|
|
512
|
+
labels: DataTableLabels,
|
|
513
|
+
): ResolvedRowAction<T> {
|
|
514
|
+
const tone = action.tone ?? (action.kind === "delete" ? "danger" : "default");
|
|
515
|
+
return {
|
|
516
|
+
key: action.kind ?? `${index}:${action.label ?? ""}`,
|
|
517
|
+
label: action.label ?? (action.kind === "edit" ? labels.edit : action.kind === "delete" ? labels.delete : ""),
|
|
518
|
+
icon:
|
|
519
|
+
action.icon ??
|
|
520
|
+
(action.kind === "edit" ? <Pencil /> : action.kind === "delete" ? <Trash2 /> : null),
|
|
521
|
+
tone,
|
|
522
|
+
swipe: action.swipe ?? (tone === "danger" ? "start" : "end"),
|
|
523
|
+
action,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Ask, then act. */
|
|
528
|
+
async function runRowAction<T>(action: DataTableRowAction<T>, row: T): Promise<void> {
|
|
529
|
+
if (action.confirm && !(await action.confirm(row))) return;
|
|
530
|
+
action.onAction(row);
|
|
434
531
|
}
|
|
435
532
|
|
|
436
533
|
/** See {@link DataTableProps.chrome}. */
|
|
@@ -516,7 +613,8 @@ function cleanAttrs(
|
|
|
516
613
|
* header is not a string and whose key is not human-readable is a call site that
|
|
517
614
|
* should pass a string header, and that is visible in what gets announced.
|
|
518
615
|
*/
|
|
519
|
-
function columnLabel<T>(col: DataTableColumn<T
|
|
616
|
+
function columnLabel<T>(col: DataTableColumn<T>, labels?: DataTableLabels): string {
|
|
617
|
+
if (col.key === ROW_ACTIONS_KEY && labels) return labels.actions;
|
|
520
618
|
return typeof col.header === "string" ? col.header : col.key;
|
|
521
619
|
}
|
|
522
620
|
|
|
@@ -528,6 +626,20 @@ function columnLabel<T>(col: DataTableColumn<T>): string {
|
|
|
528
626
|
const OWN_CONTROL =
|
|
529
627
|
'a[href],button,input,select,textarea,label,summary,[role="button"],[role="link"],[role="checkbox"],[role="switch"],[role="menuitem"],[role="option"],[contenteditable=""],[contenteditable="true"],[tabindex]:not([tabindex="-1"])';
|
|
530
628
|
|
|
629
|
+
/**
|
|
630
|
+
* Whether a click that reached `row`'s handler did not start in the row's own DOM.
|
|
631
|
+
*
|
|
632
|
+
* React bubbles an event through the COMPONENT tree, portals included: a popover, a
|
|
633
|
+
* menu or a dialog a cell (or the expansion under it) opens renders at `<body>` but
|
|
634
|
+
* still reports its clicks to every React ancestor — the row among them. So a click
|
|
635
|
+
* in a cell's portalled date picker opened the row it came from, and a page wrapped
|
|
636
|
+
* such content in `<div onClick={(e) => e.stopPropagation()}>` to stop it (kastlan's
|
|
637
|
+
* feedback page did, round its comments). The row owns the clicks on the row.
|
|
638
|
+
*/
|
|
639
|
+
function fromOutside(target: EventTarget | null, row: HTMLElement): boolean {
|
|
640
|
+
return !(target instanceof Node) || !row.contains(target);
|
|
641
|
+
}
|
|
642
|
+
|
|
531
643
|
/** Whether an event on `row` started on a control nested inside it. */
|
|
532
644
|
function fromOwnControl(target: EventTarget | null, row: HTMLElement): boolean {
|
|
533
645
|
if (!(target instanceof Element)) return false;
|
|
@@ -617,9 +729,13 @@ function RowLink({
|
|
|
617
729
|
// here happens before any descendant can silence the event; `stopPropagation`
|
|
618
730
|
// does not undo a `preventDefault`.
|
|
619
731
|
onClickCapture={(e) => {
|
|
732
|
+
// A click in a portal a descendant opened is not a click on the link: leave
|
|
733
|
+
// it (and a link inside it) alone. See `fromOutside`.
|
|
734
|
+
if (fromOutside(e.target, e.currentTarget)) return;
|
|
620
735
|
if (isPlainLeftClick(e)) e.preventDefault();
|
|
621
736
|
}}
|
|
622
737
|
onClick={(e) => {
|
|
738
|
+
if (fromOutside(e.target, e.currentTarget)) return;
|
|
623
739
|
if (!isPlainLeftClick(e)) {
|
|
624
740
|
e.stopPropagation();
|
|
625
741
|
return;
|
|
@@ -639,12 +755,12 @@ function RowLink({
|
|
|
639
755
|
|
|
640
756
|
|
|
641
757
|
|
|
642
|
-
import { DEFAULT_PERSIST_PREFIX, useTableState } from "./use-table-state";
|
|
758
|
+
import { DEFAULT_PERSIST_PREFIX, useTableState, type TableUrlSync } from "./use-table-state";
|
|
643
759
|
import { useMobileReveal } from "./use-mobile-reveal";
|
|
644
760
|
|
|
645
761
|
export function DataTable<T>({
|
|
646
762
|
rows,
|
|
647
|
-
columns,
|
|
763
|
+
columns: columnsProp,
|
|
648
764
|
rowKey,
|
|
649
765
|
defaultPageSize = 25,
|
|
650
766
|
onRowClick,
|
|
@@ -682,6 +798,8 @@ export function DataTable<T>({
|
|
|
682
798
|
multiSort: multiSortProp,
|
|
683
799
|
frame = true,
|
|
684
800
|
className,
|
|
801
|
+
rowActions,
|
|
802
|
+
toolbar,
|
|
685
803
|
}: DataTableProps<T>) {
|
|
686
804
|
const compact = density === "compact";
|
|
687
805
|
const minimal = chrome === "minimal";
|
|
@@ -711,6 +829,74 @@ export function DataTable<T>({
|
|
|
711
829
|
[labelOverrides, labelsProp],
|
|
712
830
|
);
|
|
713
831
|
const locale = useKitLocale(localeProp);
|
|
832
|
+
|
|
833
|
+
// ---- Row actions ----
|
|
834
|
+
// The actions column is appended to the caller's columns rather than drawn as a
|
|
835
|
+
// second special case beside the selection column: it then gets the cell padding,
|
|
836
|
+
// the widths and the phone rules (`mobileHidden`) every other column gets.
|
|
837
|
+
const rowActionsFor = useCallback(
|
|
838
|
+
(row: T): ResolvedRowAction<T>[] =>
|
|
839
|
+
rowActions
|
|
840
|
+
? (typeof rowActions === "function" ? rowActions(row) : rowActions)
|
|
841
|
+
.filter((a) => !a.hidden?.(row))
|
|
842
|
+
.map((a, i) => resolveRowAction(a, i, labels))
|
|
843
|
+
: [],
|
|
844
|
+
[rowActions, labels],
|
|
845
|
+
);
|
|
846
|
+
const columns = useMemo<DataTableColumn<T>[]>(
|
|
847
|
+
() =>
|
|
848
|
+
rowActions
|
|
849
|
+
? [
|
|
850
|
+
...columnsProp,
|
|
851
|
+
{
|
|
852
|
+
key: ROW_ACTIONS_KEY,
|
|
853
|
+
header: <span className="sr-only">{labels.actions}</span>,
|
|
854
|
+
cell: (row) => (
|
|
855
|
+
<div className="flex items-center justify-end gap-0.5">
|
|
856
|
+
{rowActionsFor(row).map((a) => (
|
|
857
|
+
<IconButton
|
|
858
|
+
key={a.key}
|
|
859
|
+
size={density === "compact" ? "2xs" : "xs"}
|
|
860
|
+
label={a.label}
|
|
861
|
+
tone={a.tone === "danger" ? "danger" : undefined}
|
|
862
|
+
disabledReason={a.action.disabledReason?.(row)}
|
|
863
|
+
// The row's click handler already skips its own controls; this is for
|
|
864
|
+
// the caller who wraps the table in a clickable element of their own.
|
|
865
|
+
stopPropagation
|
|
866
|
+
onClick={() => void runRowAction(a.action, row)}
|
|
867
|
+
>
|
|
868
|
+
{a.icon}
|
|
869
|
+
</IconButton>
|
|
870
|
+
))}
|
|
871
|
+
</div>
|
|
872
|
+
),
|
|
873
|
+
className: "w-px whitespace-nowrap text-end",
|
|
874
|
+
headClassName: "w-px",
|
|
875
|
+
// On a phone the same actions are the card's swipes.
|
|
876
|
+
mobileHidden: true,
|
|
877
|
+
noRowLink: true,
|
|
878
|
+
},
|
|
879
|
+
]
|
|
880
|
+
: columnsProp,
|
|
881
|
+
[columnsProp, rowActions, rowActionsFor, labels.actions, density],
|
|
882
|
+
);
|
|
883
|
+
const swipesFromActions = (row: T): MobileSwipeActions | null => {
|
|
884
|
+
const swipes = rowActionsFor(row).filter((a) => a.swipe && !a.action.disabledReason?.(row));
|
|
885
|
+
if (!swipes.length) return null;
|
|
886
|
+
const toSwipe = (a: ResolvedRowAction<T>): SwipeAction => ({
|
|
887
|
+
label: a.label,
|
|
888
|
+
icon: a.icon,
|
|
889
|
+
onCommit: () => void runRowAction(a.action, row),
|
|
890
|
+
className: a.tone === "danger" ? "bg-[var(--danger)]" : "bg-[var(--brand)]",
|
|
891
|
+
armedClassName: a.tone === "danger" ? "bg-[var(--danger-hover)]" : "bg-[var(--brand-hover)]",
|
|
892
|
+
});
|
|
893
|
+
return {
|
|
894
|
+
start: swipes.filter((a) => a.swipe === "start").map(toSwipe),
|
|
895
|
+
end: swipes.filter((a) => a.swipe === "end").map(toSwipe),
|
|
896
|
+
};
|
|
897
|
+
};
|
|
898
|
+
const swipeActionsFor = mobileSwipeActions ?? (rowActions ? swipesFromActions : undefined);
|
|
899
|
+
|
|
714
900
|
// Sorting, filtering and paging all change WHICH rows are on screen without
|
|
715
901
|
// moving focus — the header button the user pressed is still the header button
|
|
716
902
|
// they are on — so there is no other channel to say it on. See use-announce.ts.
|
|
@@ -875,7 +1061,7 @@ export function DataTable<T>({
|
|
|
875
1061
|
// criterion BEHIND the existing one, and answering "Sorted by Date" to a click on
|
|
876
1062
|
// Name misreports what the click did. The third click drops the column entirely,
|
|
877
1063
|
// which is the state a user is most likely to have reached by accident.
|
|
878
|
-
const name = col ? columnLabel(col) : key;
|
|
1064
|
+
const name = col ? columnLabel(col, labels) : key;
|
|
879
1065
|
const entry = next.find((s) => s.key === key);
|
|
880
1066
|
announce(
|
|
881
1067
|
!entry
|
|
@@ -1122,7 +1308,10 @@ export function DataTable<T>({
|
|
|
1122
1308
|
const totalCount = columns.length;
|
|
1123
1309
|
// Spans the full row width including the optional leading selection column.
|
|
1124
1310
|
const totalColSpan = visibleCount + (selection ? 1 : 0);
|
|
1125
|
-
|
|
1311
|
+
// The actions column is the table's own, not one the user can hide: out of the count.
|
|
1312
|
+
const columnsCountLabel = rowActions
|
|
1313
|
+
? labels.columnsCount(visibleCount - 1, totalCount - 1)
|
|
1314
|
+
: labels.columnsCount(visibleCount, totalCount);
|
|
1126
1315
|
// Match Tailwind's `md` breakpoint: we render either the table or the card
|
|
1127
1316
|
// list — never both — so we don't double up DOM nodes that screen readers and
|
|
1128
1317
|
// integration tests would have to disambiguate.
|
|
@@ -1252,7 +1441,7 @@ export function DataTable<T>({
|
|
|
1252
1441
|
// uses for exactly this, and it points the way the row actually opens: right into
|
|
1253
1442
|
// a sheet, or down into an inline panel that flips it when expanded.
|
|
1254
1443
|
const opensDetail = interactive && !!expandedRow;
|
|
1255
|
-
const swipeActions =
|
|
1444
|
+
const swipeActions = swipeActionsFor?.(row);
|
|
1256
1445
|
const swipe = swipeActions ? physicalSwipe(swipeActions, mobileDir) : null;
|
|
1257
1446
|
// An expanded row never swipes: the editor below it owns the horizontal space,
|
|
1258
1447
|
// and dragging the header away from its own form reads as a glitch.
|
|
@@ -1362,7 +1551,14 @@ export function DataTable<T>({
|
|
|
1362
1551
|
<div
|
|
1363
1552
|
role={interactive ? "button" : undefined}
|
|
1364
1553
|
tabIndex={interactive ? 0 : undefined}
|
|
1365
|
-
onClick={
|
|
1554
|
+
onClick={
|
|
1555
|
+
interactive
|
|
1556
|
+
? (e) => {
|
|
1557
|
+
if (fromOutside(e.target, e.currentTarget)) return;
|
|
1558
|
+
onRowClick!(row);
|
|
1559
|
+
}
|
|
1560
|
+
: undefined
|
|
1561
|
+
}
|
|
1366
1562
|
onKeyDown={
|
|
1367
1563
|
interactive
|
|
1368
1564
|
? (e) => {
|
|
@@ -1427,6 +1623,11 @@ export function DataTable<T>({
|
|
|
1427
1623
|
// density (`group-data-[density=compact]/table:…`) without being told twice.
|
|
1428
1624
|
data-density={density}
|
|
1429
1625
|
data-frame={frame ? undefined : "none"}
|
|
1626
|
+
// The frame clips (`overflow-clip`), so it is a clipping container for a
|
|
1627
|
+
// Tooltip's auto-portal — said as an attribute as well, because jsdom computes no
|
|
1628
|
+
// classes and a table-cell tooltip must portal under test just as it does in the
|
|
1629
|
+
// browser (see `CLIPS_ATTRIBUTE`). The bare root clips nothing and is not marked.
|
|
1630
|
+
data-clips={frame ? "" : undefined}
|
|
1430
1631
|
className={cn(
|
|
1431
1632
|
"group/table",
|
|
1432
1633
|
frame ? "overflow-clip" : "min-w-0",
|
|
@@ -1440,6 +1641,19 @@ export function DataTable<T>({
|
|
|
1440
1641
|
card list for the table wholesale, which would remount a region living
|
|
1441
1642
|
inside either one. */}
|
|
1442
1643
|
<span {...regionProps} />
|
|
1644
|
+
{toolbar != null && toolbar !== false && (
|
|
1645
|
+
<div
|
|
1646
|
+
data-table-toolbar=""
|
|
1647
|
+
className={cn(
|
|
1648
|
+
"flex flex-wrap items-center gap-2",
|
|
1649
|
+
frame
|
|
1650
|
+
? cn("border-b border-[var(--border)]", compact ? "px-2 py-1.5" : "px-3 py-2")
|
|
1651
|
+
: "pb-3",
|
|
1652
|
+
)}
|
|
1653
|
+
>
|
|
1654
|
+
{toolbar}
|
|
1655
|
+
</div>
|
|
1656
|
+
)}
|
|
1443
1657
|
{!isMdUp && (
|
|
1444
1658
|
<div ref={mobileRootRef}>
|
|
1445
1659
|
{/* Mobile filter access (feedback #299): the per-column filter popovers
|
|
@@ -1484,7 +1698,7 @@ export function DataTable<T>({
|
|
|
1484
1698
|
compact ? "px-3 py-3 text-xs" : "px-4 py-6 text-sm",
|
|
1485
1699
|
)}
|
|
1486
1700
|
>
|
|
1487
|
-
{isLoading ? <LoadingText label={labels.loading} /> : (empty ??
|
|
1701
|
+
{isLoading ? <LoadingText label={labels.loading} /> : (empty ?? labels.empty)}
|
|
1488
1702
|
</li>
|
|
1489
1703
|
)}
|
|
1490
1704
|
{/* Endless-scroll sentinel: observed by IntersectionObserver to pull in
|
|
@@ -1535,6 +1749,8 @@ export function DataTable<T>({
|
|
|
1535
1749
|
other half of the `w-0 min-w-full` fix on the expansion cell below
|
|
1536
1750
|
(feedback #104: "expanding an item resizes the columns"). */}
|
|
1537
1751
|
<div
|
|
1752
|
+
// The scroller: marked for the Tooltip's auto-portal (see the root above).
|
|
1753
|
+
data-clips=""
|
|
1538
1754
|
className={cn("overflow-auto [scrollbar-gutter:stable]", fillHeight && "flex-1 min-h-0")}
|
|
1539
1755
|
style={fillHeight ? undefined : { maxHeight: maxBodyHeight }}
|
|
1540
1756
|
>
|
|
@@ -1701,7 +1917,7 @@ export function DataTable<T>({
|
|
|
1701
1917
|
</Popover>
|
|
1702
1918
|
)}
|
|
1703
1919
|
</div>
|
|
1704
|
-
{resizable && (
|
|
1920
|
+
{resizable && col.key !== ROW_ACTIONS_KEY && (
|
|
1705
1921
|
<span
|
|
1706
1922
|
role="separator"
|
|
1707
1923
|
aria-orientation="vertical"
|
|
@@ -1793,6 +2009,7 @@ export function DataTable<T>({
|
|
|
1793
2009
|
// not also open the row. Cells no longer have to
|
|
1794
2010
|
// `stopPropagation` to get that.
|
|
1795
2011
|
if (fromOwnControl(e.target, e.currentTarget)) return;
|
|
2012
|
+
if (fromOutside(e.target, e.currentTarget)) return;
|
|
1796
2013
|
// Modifier-clicks anywhere in the row drive
|
|
1797
2014
|
// selection instead of expanding it (feedback #289):
|
|
1798
2015
|
// Ctrl/Cmd toggles a single row, Shift selects the
|
|
@@ -1928,7 +2145,7 @@ export function DataTable<T>({
|
|
|
1928
2145
|
>
|
|
1929
2146
|
{/* Nothing is not the same as not-yet: "no results" while the first
|
|
1930
2147
|
page is still in flight is a claim the table cannot make. */}
|
|
1931
|
-
{isLoading ? <LoadingText label={labels.loading} /> : (empty ??
|
|
2148
|
+
{isLoading ? <LoadingText label={labels.loading} /> : (empty ?? labels.empty)}
|
|
1932
2149
|
</td>
|
|
1933
2150
|
</tr>
|
|
1934
2151
|
)}
|
|
@@ -2011,6 +2228,7 @@ export function DataTable<T>({
|
|
|
2011
2228
|
</button>
|
|
2012
2229
|
<div className="flex flex-col gap-0.5">
|
|
2013
2230
|
{columns.map((col) => {
|
|
2231
|
+
if (col.key === ROW_ACTIONS_KEY) return null;
|
|
2014
2232
|
const checked = !hiddenCols.has(col.key);
|
|
2015
2233
|
const headerLabel = columnLabel(col);
|
|
2016
2234
|
return (
|
|
@@ -91,6 +91,8 @@ export interface DateTriggerAttributes {
|
|
|
91
91
|
"aria-label"?: string;
|
|
92
92
|
"aria-describedby"?: string;
|
|
93
93
|
"aria-invalid"?: true;
|
|
94
|
+
/** The field's required state — `Field required` hands it down with the id pair. */
|
|
95
|
+
"aria-required"?: boolean | "true" | "false";
|
|
94
96
|
disabled?: boolean;
|
|
95
97
|
onClick: () => void;
|
|
96
98
|
className: string;
|
|
@@ -132,6 +134,7 @@ function DateFieldTrigger({
|
|
|
132
134
|
padded,
|
|
133
135
|
disabled,
|
|
134
136
|
invalid,
|
|
137
|
+
required,
|
|
135
138
|
panelId,
|
|
136
139
|
aria,
|
|
137
140
|
renderTrigger,
|
|
@@ -153,6 +156,8 @@ function DateFieldTrigger({
|
|
|
153
156
|
padded: boolean;
|
|
154
157
|
disabled?: boolean;
|
|
155
158
|
invalid?: boolean;
|
|
159
|
+
/** `aria-required`, on the trigger with the rest of the field's ARIA. */
|
|
160
|
+
required?: boolean | "true" | "false";
|
|
156
161
|
}) {
|
|
157
162
|
const wasOpen = useRef(false);
|
|
158
163
|
useEffect(() => {
|
|
@@ -177,6 +182,7 @@ function DateFieldTrigger({
|
|
|
177
182
|
// One attribute object, worn either by the kit's own button below or by a caller's
|
|
178
183
|
// (`renderTrigger`) — so a custom trigger cannot quietly fall out of step with the
|
|
179
184
|
// naming and state this one announces.
|
|
185
|
+
const isInvalid = Boolean(invalid) || aria["aria-invalid"] === true || aria["aria-invalid"] === "true";
|
|
180
186
|
const attrs: DateTriggerAttributes = {
|
|
181
187
|
ref: triggerRef,
|
|
182
188
|
type: "button",
|
|
@@ -209,14 +215,15 @@ function DateFieldTrigger({
|
|
|
209
215
|
"aria-haspopup": "dialog",
|
|
210
216
|
"aria-controls": panelId,
|
|
211
217
|
"aria-expanded": open,
|
|
212
|
-
"aria-invalid":
|
|
213
|
-
|
|
218
|
+
"aria-invalid": isInvalid || undefined,
|
|
219
|
+
"aria-required": required,
|
|
214
220
|
className: cn(
|
|
215
221
|
FIELD_TRIGGER,
|
|
216
222
|
"pe-9",
|
|
217
223
|
padded && FIELD_FLOATING_PAD,
|
|
218
224
|
disabled && "cursor-not-allowed opacity-50",
|
|
219
|
-
invalid
|
|
225
|
+
// An `aria-invalid` from a Field spread paints too, as on Input and AmountInput.
|
|
226
|
+
isInvalid && FIELD_INVALID,
|
|
220
227
|
),
|
|
221
228
|
};
|
|
222
229
|
if (renderTrigger) return <CustomTrigger render={renderTrigger} attrs={attrs} valueId={valueId} />;
|
|
@@ -279,6 +286,10 @@ function DateField({
|
|
|
279
286
|
sheetBackCloses = true,
|
|
280
287
|
renderTrigger,
|
|
281
288
|
children,
|
|
289
|
+
// The trigger's, not the wrapper's: `Field`'s render-prop spreads it with the id
|
|
290
|
+
// pair, and a required date read as optional while it sat on the role-less div.
|
|
291
|
+
// Taken here rather than in `splitTriggerAria`, which MonthPicker shares.
|
|
292
|
+
"aria-required": ariaRequired,
|
|
282
293
|
...rest
|
|
283
294
|
}: Omit<ComponentPropsWithoutRef<"div">, "children"> & {
|
|
284
295
|
triggerText: string;
|
|
@@ -393,6 +404,7 @@ function DateField({
|
|
|
393
404
|
padded={label !== undefined}
|
|
394
405
|
disabled={disabled}
|
|
395
406
|
invalid={invalid}
|
|
407
|
+
required={ariaRequired}
|
|
396
408
|
renderTrigger={renderTrigger}
|
|
397
409
|
/>
|
|
398
410
|
);
|
|
@@ -577,6 +589,9 @@ export function DatePicker({
|
|
|
577
589
|
clearLabel,
|
|
578
590
|
disabled,
|
|
579
591
|
invalid,
|
|
592
|
+
// Named so it reaches the field (and so its trigger) when a step/today row is the
|
|
593
|
+
// root and takes the rest of the caller's props.
|
|
594
|
+
"aria-required": ariaRequired,
|
|
580
595
|
...rest
|
|
581
596
|
}: DatePickerProps) {
|
|
582
597
|
const locale = useKitLocale(localeProp);
|
|
@@ -593,6 +608,7 @@ export function DatePicker({
|
|
|
593
608
|
// With step/today buttons the flex row is the root and takes the caller's props —
|
|
594
609
|
// except the ones that name the field, which still belong on its trigger.
|
|
595
610
|
{...(wrapped ? triggerAria : rest)}
|
|
611
|
+
aria-required={ariaRequired}
|
|
596
612
|
label={label}
|
|
597
613
|
clearable={clearable}
|
|
598
614
|
clearLabel={text.clear}
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
2
|
import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from "react";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
4
5
|
import { FieldHint } from "./ui";
|
|
5
6
|
|
|
7
|
+
/** The words a {@link DescriptionList} renders on its own behalf. */
|
|
8
|
+
export interface DescriptionListLabels {
|
|
9
|
+
/** What an item with no value shows (see {@link DescriptionItemProps.placeholder}).
|
|
10
|
+
* Default "—", an em dash. */
|
|
11
|
+
empty: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_DESCRIPTION_LIST_LABELS: DescriptionListLabels = {
|
|
15
|
+
empty: "—",
|
|
16
|
+
};
|
|
17
|
+
|
|
6
18
|
export type DescriptionListLayout = "rows" | "cards" | "stacked";
|
|
7
19
|
export type DescriptionListDensity = "comfortable" | "compact" | "tight";
|
|
8
20
|
/** How many columns a `stacked` or `cards` list may use. */
|
|
@@ -14,6 +26,7 @@ interface ListContextValue {
|
|
|
14
26
|
prose: boolean;
|
|
15
27
|
density: DescriptionListDensity;
|
|
16
28
|
columns: DescriptionListColumns;
|
|
29
|
+
placeholder?: ReactNode;
|
|
17
30
|
}
|
|
18
31
|
|
|
19
32
|
const ListContext = createContext<ListContextValue>({
|
|
@@ -103,6 +116,9 @@ export interface DescriptionListProps extends ComponentPropsWithoutRef<"dl"> {
|
|
|
103
116
|
* all would not be an improvement. Per item: {@link DescriptionItemProps.prose}.
|
|
104
117
|
*/
|
|
105
118
|
prose?: boolean;
|
|
119
|
+
/** What an item with no value shows, for every item that does not set its own —
|
|
120
|
+
* see {@link DescriptionItemProps.placeholder}. */
|
|
121
|
+
placeholder?: ReactNode;
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
/**
|
|
@@ -129,6 +145,7 @@ export function DescriptionList({
|
|
|
129
145
|
density = "comfortable",
|
|
130
146
|
minCardWidth = "10rem",
|
|
131
147
|
columns,
|
|
148
|
+
placeholder,
|
|
132
149
|
className,
|
|
133
150
|
style,
|
|
134
151
|
...rest
|
|
@@ -147,7 +164,7 @@ export function DescriptionList({
|
|
|
147
164
|
const grid: CSSProperties | undefined = cards
|
|
148
165
|
? { gridTemplateColumns: `repeat(auto-fill, minmax(min(100%, ${track}), 1fr))` }
|
|
149
166
|
: undefined;
|
|
150
|
-
const ctx: ListContextValue = { layout, numeric, prose, density, columns: columns ?? 2 };
|
|
167
|
+
const ctx: ListContextValue = { layout, numeric, prose, density, columns: columns ?? 2, placeholder };
|
|
151
168
|
const text = density === "tight" ? "text-[11px] leading-4" : density === "compact" ? "text-xs" : "text-sm";
|
|
152
169
|
|
|
153
170
|
if (stacked) {
|
|
@@ -215,6 +232,20 @@ export interface DescriptionItemProps extends Omit<ComponentPropsWithoutRef<"div
|
|
|
215
232
|
* because a card grid's column count is not known to CSS ahead of layout.
|
|
216
233
|
*/
|
|
217
234
|
span?: number | "full";
|
|
235
|
+
/**
|
|
236
|
+
* Shown in place of a value that is not there — `children` of `null`, `undefined`,
|
|
237
|
+
* `false` or `""` (a `0` is a value and is shown). Default: the list's own
|
|
238
|
+
* `placeholder`, else `descriptionList.empty` from the provider, else "—". `null`
|
|
239
|
+
* shows nothing, as an empty detail did before. kastlan's DetailField put its
|
|
240
|
+
* `EMPTY_VALUE` into every one of its call sites' values by hand.
|
|
241
|
+
*
|
|
242
|
+
* Drawn in the muted ink, so an absent value does not read as a value of "—".
|
|
243
|
+
*/
|
|
244
|
+
placeholder?: ReactNode;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function isEmptyValue(node: ReactNode): boolean {
|
|
248
|
+
return node === undefined || node === null || node === false || node === "";
|
|
218
249
|
}
|
|
219
250
|
|
|
220
251
|
const ROW_PAD: Record<DescriptionListDensity, string> = {
|
|
@@ -236,11 +267,23 @@ export function DescriptionItem({
|
|
|
236
267
|
numeric: numericProp,
|
|
237
268
|
prose: proseProp,
|
|
238
269
|
span,
|
|
270
|
+
placeholder: placeholderProp,
|
|
239
271
|
detailClassName,
|
|
240
272
|
className,
|
|
241
273
|
...rest
|
|
242
274
|
}: DescriptionItemProps) {
|
|
243
|
-
const {
|
|
275
|
+
const {
|
|
276
|
+
layout,
|
|
277
|
+
numeric: listNumeric,
|
|
278
|
+
prose: listProse,
|
|
279
|
+
density,
|
|
280
|
+
columns,
|
|
281
|
+
placeholder: listPlaceholder,
|
|
282
|
+
} = useContext(ListContext);
|
|
283
|
+
const labels = useKitLabels("descriptionList", DEFAULT_DESCRIPTION_LIST_LABELS);
|
|
284
|
+
const empty = isEmptyValue(children);
|
|
285
|
+
const placeholder =
|
|
286
|
+
placeholderProp !== undefined ? placeholderProp : listPlaceholder !== undefined ? listPlaceholder : labels.empty;
|
|
244
287
|
const numeric = numericProp ?? listNumeric;
|
|
245
288
|
const prose = proseProp ?? listProse;
|
|
246
289
|
const cards = layout === "cards";
|
|
@@ -277,7 +320,13 @@ export function DescriptionItem({
|
|
|
277
320
|
detailClassName,
|
|
278
321
|
)}
|
|
279
322
|
>
|
|
280
|
-
{
|
|
323
|
+
{empty && !isEmptyValue(placeholder) ? (
|
|
324
|
+
<span data-slot="description-empty" className="font-normal text-[var(--text-muted)]">
|
|
325
|
+
{placeholder}
|
|
326
|
+
</span>
|
|
327
|
+
) : (
|
|
328
|
+
children
|
|
329
|
+
)}
|
|
281
330
|
</dd>
|
|
282
331
|
);
|
|
283
332
|
|
|
@@ -105,6 +105,14 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
|
|
|
105
105
|
debounceMs,
|
|
106
106
|
loadErrorLabel,
|
|
107
107
|
"aria-label": ariaLabel,
|
|
108
|
+
// The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than
|
|
109
|
+
// the wrapper — see MultiEntityCombobox: `Field`'s render-prop spreads `{ id,
|
|
110
|
+
// aria-describedby, aria-invalid, aria-required }`, and on the role-less wrapper
|
|
111
|
+
// div the label's `htmlFor` named nothing and the hint described nothing.
|
|
112
|
+
id,
|
|
113
|
+
"aria-describedby": ariaDescribedBy,
|
|
114
|
+
"aria-invalid": ariaInvalid,
|
|
115
|
+
"aria-required": ariaRequired,
|
|
108
116
|
...rest
|
|
109
117
|
}: EntityComboboxProps<V, C>) {
|
|
110
118
|
const core = useComboboxCore<V>({
|
|
@@ -115,7 +123,11 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
|
|
|
115
123
|
minChars,
|
|
116
124
|
debounceMs,
|
|
117
125
|
});
|
|
118
|
-
const field = useComboboxFieldError(
|
|
126
|
+
const field = useComboboxFieldError(
|
|
127
|
+
error,
|
|
128
|
+
invalid || ariaInvalid === true || ariaInvalid === "true",
|
|
129
|
+
ariaDescribedBy,
|
|
130
|
+
);
|
|
119
131
|
// The props are the per-instance overrides, the provider the app-wide ones; a
|
|
120
132
|
// prop left `undefined` falls through to the provider rather than masking it.
|
|
121
133
|
const labels = useKitLabels("combobox", DEFAULT_COMBOBOX_LABELS, {
|
|
@@ -151,13 +163,15 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
|
|
|
151
163
|
};
|
|
152
164
|
|
|
153
165
|
return (
|
|
154
|
-
// `rest` dresses the wrapper, which has no role; the accessible NAME
|
|
155
|
-
//
|
|
166
|
+
// `rest` dresses the wrapper, which has no role; the accessible NAME — and the
|
|
167
|
+
// id, description, invalid and required state above — go on the trigger, which
|
|
168
|
+
// has one. Spread FIRST so the trigger's ARIA and the
|
|
156
169
|
// handlers that open the panel cannot be clobbered from outside.
|
|
157
170
|
<div {...rest} className={cn("relative", className)}>
|
|
158
171
|
{label !== undefined && <FieldLabel>{label}</FieldLabel>}
|
|
159
172
|
<button
|
|
160
173
|
ref={core.triggerRef}
|
|
174
|
+
id={id}
|
|
161
175
|
type="button"
|
|
162
176
|
// A combobox, not a button. The distinction is not pedantry: this control
|
|
163
177
|
// carried `aria-invalid`, which `button` does not support, so a required
|
|
@@ -185,6 +199,7 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
|
|
|
185
199
|
disabled={disabled}
|
|
186
200
|
aria-invalid={field.isInvalid || undefined}
|
|
187
201
|
aria-describedby={field.describedBy}
|
|
202
|
+
aria-required={ariaRequired}
|
|
188
203
|
onClick={() => !disabled && setOpen((o) => !o)}
|
|
189
204
|
// Down/Up opens the list from the closed trigger, per the APG. Enter and
|
|
190
205
|
// Space already do it through the button's own click.
|