@eifi1/ui-kit 0.6.1 → 0.7.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 +69 -33
- package/dist/chart.d.ts +1 -0
- package/dist/components/account-settings.js +3 -2
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +3 -0
- package/dist/components/amount-input.js +6 -4
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +33 -6
- package/dist/components/autocomplete.js +62 -16
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/calculator.d.ts +3 -0
- package/dist/components/calculator.js +2 -1
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.js +2 -1
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/chart.d.ts +33 -2
- package/dist/components/chart.js +20 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +34 -14
- package/dist/components/chip.js +63 -28
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +18 -1
- package/dist/components/choice-card.js +21 -8
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.d.ts +61 -2
- package/dist/components/combobox-core.js +68 -17
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +11 -7
- package/dist/components/combobox.js +51 -28
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.js +3 -3
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +30 -6
- package/dist/components/danger-confirm.js +51 -32
- package/dist/components/danger-confirm.js.map +1 -1
- 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-pagination.d.ts +6 -3
- package/dist/components/data-table-pagination.js +6 -5
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +332 -248
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +17 -7
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +28 -3
- package/dist/components/dialog-frame.js +47 -11
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.d.ts +22 -2
- package/dist/components/disclosure.js +41 -8
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.d.ts +7 -3
- package/dist/components/dropdown.js +6 -2
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +17 -8
- package/dist/components/entity-combobox.js +7 -5
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/file-button.d.ts +36 -5
- package/dist/components/file-button.js +23 -2
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +9 -5
- package/dist/components/file-dropzone.js +14 -19
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/grouped-picker.js +2 -2
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.d.ts +8 -3
- package/dist/components/hover-menu.js +88 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-picker.d.ts +10 -1
- package/dist/components/icon-picker.js +5 -1
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/measured-grid.d.ts +165 -0
- package/dist/components/measured-grid.js +559 -0
- package/dist/components/measured-grid.js.map +1 -0
- package/dist/components/mini-calendar.js +7 -8
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/month-picker.js +13 -9
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +2 -2
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +9 -1
- package/dist/components/multi-select.js +22 -8
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +3 -0
- package/dist/components/number-input.d.ts +3 -0
- package/dist/components/number-input.js +3 -3
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +3 -0
- package/dist/components/numpad-sheet.js +3 -3
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/picker-sheet.js +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.js +11 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/series-chart-ticks.d.ts +13 -0
- package/dist/components/series-chart-ticks.js +29 -0
- package/dist/components/series-chart-ticks.js.map +1 -0
- package/dist/components/series-chart.js +33 -27
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +10 -1
- package/dist/components/swatch-picker.js +2 -0
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +11 -2
- package/dist/components/swipeable-row.js +5 -3
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/tile-radio.js +5 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/toggle-group.d.ts +13 -3
- package/dist/components/toggle-group.js +21 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +1 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/treemap.d.ts +6 -1
- package/dist/components/treemap.js +16 -6
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +31 -7
- package/dist/components/ui.js +26 -18
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-anchor-dir.d.ts +15 -0
- package/dist/components/use-anchor-dir.js +14 -0
- package/dist/components/use-anchor-dir.js.map +1 -0
- package/dist/components/use-mobile-reveal.d.ts +8 -1
- package/dist/components/use-mobile-reveal.js +3 -2
- package/dist/components/use-mobile-reveal.js.map +1 -1
- package/dist/components/use-table-state.d.ts +4 -4
- package/dist/components/use-table-state.js +12 -5
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-DM1auUfH.d.ts → data-table-filters-noy0Abvi.d.ts} +40 -7
- package/dist/data-table.d.ts +1 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +22 -3
- package/dist/feedback/feedback-attachment.js +15 -7
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +3 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-windowed-rows.d.ts +29 -0
- package/dist/hooks/use-windowed-rows.js +31 -0
- package/dist/hooks/use-windowed-rows.js.map +1 -0
- package/dist/i18n/defaults.d.ts +3 -0
- package/dist/i18n/defaults.js +5 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +5 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH.d.ts +43 -0
- package/dist/i18n/locales/de-CH.js +21 -0
- package/dist/i18n/locales/de-CH.js.map +1 -0
- package/dist/i18n/locales/de.d.ts +43 -0
- package/dist/i18n/locales/de.js +344 -0
- package/dist/i18n/locales/de.js.map +1 -0
- package/dist/i18n/locales/es.d.ts +43 -0
- package/dist/i18n/locales/es.js +339 -0
- package/dist/i18n/locales/es.js.map +1 -0
- package/dist/i18n/locales/fr.d.ts +43 -0
- package/dist/i18n/locales/fr.js +344 -0
- package/dist/i18n/locales/fr.js.map +1 -0
- package/dist/i18n/locales/hu.d.ts +43 -0
- package/dist/i18n/locales/hu.js +343 -0
- package/dist/i18n/locales/hu.js.map +1 -0
- package/dist/i18n/locales/it.d.ts +43 -0
- package/dist/i18n/locales/it.js +342 -0
- package/dist/i18n/locales/it.js.map +1 -0
- package/dist/i18n/locales/zh.d.ts +43 -0
- package/dist/i18n/locales/zh.js +338 -0
- package/dist/i18n/locales/zh.js.map +1 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/direction.d.ts +24 -0
- package/dist/lib/direction.js +19 -0
- package/dist/lib/direction.js.map +1 -0
- package/dist/rhf/form.js.map +1 -1
- package/dist/search/command-palette.d.ts +5 -0
- package/dist/search/command-palette.js +80 -50
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +13 -1
- package/dist/shell/app-shell.js +133 -84
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +1 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +7 -3
- package/dist/shell/topbar-controls.js +38 -31
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/theme/chart-palette.d.ts +7 -1
- package/dist/theme/chart-palette.js +2 -1
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/tour/tour.d.ts +11 -5
- package/dist/tour/tour.js +124 -58
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/use-wizard.js +30 -13
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.js +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +12 -2
- package/src/components/account-settings.tsx +6 -2
- package/src/components/amount-input.tsx +10 -8
- package/src/components/autocomplete.tsx +110 -24
- package/src/components/calculator.tsx +5 -1
- package/src/components/chart-zoom.tsx +8 -1
- package/src/components/chart.tsx +68 -7
- package/src/components/chip.tsx +131 -49
- package/src/components/choice-card.tsx +77 -36
- package/src/components/combobox-core.tsx +147 -20
- package/src/components/combobox.tsx +100 -44
- package/src/components/currency-select.tsx +6 -6
- package/src/components/danger-confirm.tsx +90 -36
- package/src/components/data-table-pagination.tsx +32 -19
- package/src/components/data-table.tsx +198 -36
- package/src/components/date-picker.tsx +24 -7
- package/src/components/dialog-frame.tsx +97 -22
- package/src/components/disclosure.tsx +75 -13
- package/src/components/dropdown.tsx +17 -5
- package/src/components/entity-combobox.tsx +26 -13
- package/src/components/file-button.tsx +74 -5
- package/src/components/file-dropzone.tsx +44 -35
- package/src/components/grouped-picker.tsx +2 -2
- package/src/components/hover-menu.tsx +139 -10
- package/src/components/icon-picker.tsx +14 -1
- package/src/components/measured-grid.tsx +852 -0
- package/src/components/mini-calendar.tsx +11 -8
- package/src/components/month-picker.tsx +18 -11
- package/src/components/multi-entity-combobox.tsx +2 -2
- package/src/components/multi-select.tsx +34 -7
- package/src/components/number-input.tsx +4 -4
- package/src/components/numpad-sheet.tsx +5 -3
- package/src/components/picker-sheet.tsx +1 -1
- package/src/components/popover.tsx +18 -3
- package/src/components/series-chart-ticks.ts +52 -0
- package/src/components/series-chart.tsx +39 -31
- package/src/components/swatch-picker.tsx +11 -0
- package/src/components/swipeable-row.tsx +21 -6
- package/src/components/tile-radio.tsx +5 -7
- package/src/components/toggle-group.tsx +39 -3
- package/src/components/toggle-legend.tsx +5 -2
- package/src/components/treemap.tsx +28 -5
- package/src/components/ui.tsx +74 -30
- package/src/components/use-anchor-dir.ts +20 -0
- package/src/components/use-mobile-reveal.ts +10 -2
- package/src/components/use-table-state.ts +44 -7
- package/src/data-table.ts +1 -0
- package/src/feedback/feedback-attachment.tsx +36 -9
- package/src/feedback/feedback-dialog.tsx +3 -2
- package/src/hooks/use-windowed-rows.ts +69 -0
- package/src/i18n/defaults.ts +4 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/de-CH.ts +30 -0
- package/src/i18n/locales/de.ts +365 -0
- package/src/i18n/locales/es.ts +358 -0
- package/src/i18n/locales/fr.ts +371 -0
- package/src/i18n/locales/hu.ts +359 -0
- package/src/i18n/locales/it.ts +360 -0
- package/src/i18n/locales/zh.ts +348 -0
- package/src/index.ts +10 -1
- package/src/lib/direction.ts +37 -0
- package/src/rhf/form.tsx +21 -0
- package/src/search/command-palette.tsx +77 -19
- package/src/shell/app-shell.tsx +200 -129
- package/src/shell/topbar-action-menu.tsx +1 -1
- package/src/shell/topbar-controls.tsx +19 -8
- package/src/theme/chart-palette.ts +9 -2
- package/src/tour/tour.tsx +119 -18
- package/src/wizard/use-wizard.ts +55 -17
- package/src/wizard/wizard-summary.tsx +1 -1
|
@@ -10,8 +10,7 @@ import {
|
|
|
10
10
|
Filter,
|
|
11
11
|
X,
|
|
12
12
|
} from "lucide-react";
|
|
13
|
-
import {
|
|
14
|
-
import { Card } from "./ui";
|
|
13
|
+
import { Card, Spinner } from "./ui";
|
|
15
14
|
import { cn } from "../lib/cn";
|
|
16
15
|
import {
|
|
17
16
|
defaultFilterState,
|
|
@@ -35,6 +34,7 @@ import { useAnnounce } from "../hooks/use-announce";
|
|
|
35
34
|
import { resolveDataTableLabels, type DataTableLabels } from "./data-table-labels";
|
|
36
35
|
import { useKitLabelOverrides, useKitLocale } from "../i18n/kit-labels";
|
|
37
36
|
import { Tooltip } from "./tooltip";
|
|
37
|
+
import { dirOf, isRtl, type Direction } from "../lib/direction";
|
|
38
38
|
|
|
39
39
|
// ---------- Types ----------
|
|
40
40
|
|
|
@@ -78,6 +78,9 @@ interface Resize {
|
|
|
78
78
|
pointerId: number;
|
|
79
79
|
startX: number;
|
|
80
80
|
startWidth: number;
|
|
81
|
+
/** +1 when a drag to the right widens the column, -1 in a right-to-left table,
|
|
82
|
+
* where the handle sits on the column's LEFT edge and widening is a drag left. */
|
|
83
|
+
sign: 1 | -1;
|
|
81
84
|
/** What React last rendered this column at, so a CANCELLED drag can put the header
|
|
82
85
|
* back — React cannot, it never saw the inline width the drag wrote. */
|
|
83
86
|
hadWidth: number | undefined;
|
|
@@ -97,7 +100,19 @@ export interface ServerPagination {
|
|
|
97
100
|
pageSize: number;
|
|
98
101
|
total: number;
|
|
99
102
|
onPageChange: (page: number) => void;
|
|
103
|
+
/** Without it the page size is the server's to choose, and the pager shows no
|
|
104
|
+
* page-size select at all — one that changed nothing was a control that lied. */
|
|
100
105
|
onPageSizeChange?: (pageSize: number) => void;
|
|
106
|
+
/**
|
|
107
|
+
* A page is being fetched. The table marks itself `aria-busy`, dims the rows it is
|
|
108
|
+
* still showing (they are the PREVIOUS page's, and about to go), and replaces the
|
|
109
|
+
* empty state with a loading row when there is nothing to show yet — "no results"
|
|
110
|
+
* while the first page is still in flight is a claim the table cannot make.
|
|
111
|
+
*
|
|
112
|
+
* The pager stays usable. Disabling it would drop focus from the very button the
|
|
113
|
+
* user just pressed (a disabled button cannot hold focus), and a second click
|
|
114
|
+
* during a fetch is a request the owner already has to handle for a slow network.
|
|
115
|
+
*/
|
|
101
116
|
isLoading?: boolean;
|
|
102
117
|
}
|
|
103
118
|
|
|
@@ -191,8 +206,14 @@ export interface DataTableProps<T> {
|
|
|
191
206
|
* Controlled filter/sort state. When the `on*Change` callback is provided
|
|
192
207
|
* the table stops owning that piece of state: it renders `filters`/`sorts`
|
|
193
208
|
* as given and reports user interactions through the callback (including
|
|
194
|
-
* in serverPagination mode, where the UI is otherwise disabled).
|
|
195
|
-
*
|
|
209
|
+
* in serverPagination mode, where the UI is otherwise disabled).
|
|
210
|
+
*
|
|
211
|
+
* Page reset: on a client-side table, a change in the VALUE of `filters` or
|
|
212
|
+
* `sorts` sends the table back to page 1 — whoever made the change, the user
|
|
213
|
+
* through the header or the owner from outside (a saved view, a chip). Compared
|
|
214
|
+
* by value, so passing a fresh but equal object on every render does not reset
|
|
215
|
+
* anything. In `serverPagination` mode the owner's `page` is the page, and
|
|
216
|
+
* resetting it on a filter change stays the owner's job.
|
|
196
217
|
*/
|
|
197
218
|
filters?: FilterState;
|
|
198
219
|
onFiltersChange?: (next: FilterState) => void;
|
|
@@ -263,12 +284,19 @@ export interface DataTableProps<T> {
|
|
|
263
284
|
* `null` (or empty sides) for rows that should not move — a locked row, one whose
|
|
264
285
|
* mutation is in flight, one the user has expanded.
|
|
265
286
|
*
|
|
287
|
+
* Prefer `start`/`end`: actions committed by dragging the row toward the reading
|
|
288
|
+
* START or END of the line. In a left-to-right layout `end` is a drag to the
|
|
289
|
+
* right, and in a right-to-left one it is a drag to the left — so a "drag forward
|
|
290
|
+
* to archive" row stays forward in Arabic or Hebrew without a second definition.
|
|
291
|
+
* `left`/`right` are the physical directions and keep meaning exactly that; each
|
|
292
|
+
* is used only where the logical side for that direction is not given.
|
|
293
|
+
*
|
|
266
294
|
* Mobile only, and deliberately so: the desktop table already has room for an
|
|
267
295
|
* actions column, and a drag gesture on a pointer device is a worse version of a
|
|
268
296
|
* button. Wraps only the row body, so an expansion panel below stays put while the
|
|
269
297
|
* row above it slides.
|
|
270
298
|
*/
|
|
271
|
-
mobileSwipeActions?: (row: T) =>
|
|
299
|
+
mobileSwipeActions?: (row: T) => MobileSwipeActions | null;
|
|
272
300
|
/**
|
|
273
301
|
* Renders as a full-width row above the data rows in the desktop table
|
|
274
302
|
* (mobile has no equivalent list-row slot, so it's desktop-only). For a
|
|
@@ -283,6 +311,59 @@ export interface DataTableProps<T> {
|
|
|
283
311
|
export type FilterState = Record<string, FilterValue>;
|
|
284
312
|
export type { SortState };
|
|
285
313
|
|
|
314
|
+
/** What {@link DataTableProps.mobileSwipeActions} returns for one row. */
|
|
315
|
+
export interface MobileSwipeActions {
|
|
316
|
+
/** Committed by dragging toward the reading start (left in LTR, right in RTL). */
|
|
317
|
+
start?: SwipeAction[];
|
|
318
|
+
/** Committed by dragging toward the reading end (right in LTR, left in RTL). */
|
|
319
|
+
end?: SwipeAction[];
|
|
320
|
+
/** Committed by dragging physically LEFT, whatever the direction. */
|
|
321
|
+
left?: SwipeAction[];
|
|
322
|
+
/** Committed by dragging physically RIGHT, whatever the direction. */
|
|
323
|
+
right?: SwipeAction[];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** The logical sides onto the physical drag directions {@link SwipeableRow} takes. */
|
|
327
|
+
function physicalSwipe(
|
|
328
|
+
actions: MobileSwipeActions,
|
|
329
|
+
dir: Direction,
|
|
330
|
+
): { left?: SwipeAction[]; right?: SwipeAction[] } {
|
|
331
|
+
const rtl = dir === "rtl";
|
|
332
|
+
return {
|
|
333
|
+
left: (rtl ? actions.end : actions.start) ?? actions.left,
|
|
334
|
+
right: (rtl ? actions.start : actions.end) ?? actions.right,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// A bare `text-right` / `text-left` token, with or without variant prefixes
|
|
339
|
+
// (`md:text-right`). Deliberately NOT `text-right-…` or an arbitrary value.
|
|
340
|
+
const PHYSICAL_ALIGN = /(^|\s)((?:[\w-]+:)*)text-(left|right)(?=\s|$)/g;
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* A column's `className`/`headClassName` with physical text alignment rewritten to
|
|
344
|
+
* logical: `text-right` → `text-end`, `text-left` → `text-start`.
|
|
345
|
+
*
|
|
346
|
+
* Every numeric column written before 0.7.0 says `text-right`, and that is what it
|
|
347
|
+
* MEANT: the end of the line, where digits line up. Rendered as written, a
|
|
348
|
+
* right-to-left table puts those numbers on the far side from their own header's
|
|
349
|
+
* sort button. Rewriting keeps each existing call site correct in both directions
|
|
350
|
+
* with no change on its side; a caller who genuinely wants the physical side in RTL
|
|
351
|
+
* too can say `ltr:text-right rtl:text-right`, which this leaves alone.
|
|
352
|
+
*/
|
|
353
|
+
function logicalAlign(className: string | undefined): string | undefined {
|
|
354
|
+
return className?.replace(
|
|
355
|
+
PHYSICAL_ALIGN,
|
|
356
|
+
(token: string, lead: string, variants: string, side: string) =>
|
|
357
|
+
// Already scoped to one direction: that is a caller choosing the physical side.
|
|
358
|
+
/(^|:)(ltr|rtl):/.test(variants)
|
|
359
|
+
? token
|
|
360
|
+
: `${lead}${variants}text-${side === "right" ? "end" : "start"}`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Whether a column is end-aligned — the header then puts its filter button first. */
|
|
365
|
+
const END_ALIGN = /(^|\s)text-end(?=\s|$)/;
|
|
366
|
+
|
|
286
367
|
/** Drop the undefined entries of a {@link DataTableProps.rowAttributes} map, so a
|
|
287
368
|
* caller can write `{ "data-x": cond ? "y" : undefined }` and get no attribute
|
|
288
369
|
* at all rather than the string "undefined" in the DOM. */
|
|
@@ -422,6 +503,7 @@ export function DataTable<T>({
|
|
|
422
503
|
leadingRow,
|
|
423
504
|
}: DataTableProps<T>) {
|
|
424
505
|
const isServer = !!serverPagination;
|
|
506
|
+
const isLoading = !!serverPagination?.isLoading;
|
|
425
507
|
// prop > provider > English, through the table's own resolver rather than
|
|
426
508
|
// `useKitLabels`: that resolver derives `columnsCount` from a translated `columns`,
|
|
427
509
|
// and a plain merge would skip it. `presets` is a record and is merged key by key,
|
|
@@ -443,7 +525,6 @@ export function DataTable<T>({
|
|
|
443
525
|
[labelOverrides, labelsProp],
|
|
444
526
|
);
|
|
445
527
|
const locale = useKitLocale(localeProp);
|
|
446
|
-
const [, setSearchParams] = useSearchParams();
|
|
447
528
|
// Sorting, filtering and paging all change WHICH rows are on screen without
|
|
448
529
|
// moving focus — the header button the user pressed is still the header button
|
|
449
530
|
// they are on — so there is no other channel to say it on. See use-announce.ts.
|
|
@@ -469,7 +550,6 @@ export function DataTable<T>({
|
|
|
469
550
|
storageKey,
|
|
470
551
|
storageKeyPrefix,
|
|
471
552
|
urlSync,
|
|
472
|
-
setSearchParams,
|
|
473
553
|
defaultPageSize,
|
|
474
554
|
sortsProp,
|
|
475
555
|
filtersProp,
|
|
@@ -608,7 +688,10 @@ export function DataTable<T>({
|
|
|
608
688
|
const setFilterValue = (key: string, value: FilterValue) => {
|
|
609
689
|
if (onFiltersChange) {
|
|
610
690
|
onFiltersChange({ ...filters, [key]: value });
|
|
611
|
-
|
|
691
|
+
// No `setPage(0)` here: the page resets when the new value comes back in
|
|
692
|
+
// through the prop — see the controlled reset in useTableState. Resetting now
|
|
693
|
+
// would move to page 1 for a change the owner may yet refuse.
|
|
694
|
+
return;
|
|
612
695
|
}
|
|
613
696
|
setInternalFilters((s) => ({ ...s, [key]: value }));
|
|
614
697
|
setPage(0);
|
|
@@ -767,6 +850,7 @@ export function DataTable<T>({
|
|
|
767
850
|
pointerId: e.pointerId,
|
|
768
851
|
startX: e.clientX,
|
|
769
852
|
startWidth,
|
|
853
|
+
sign: isRtl(e.currentTarget) ? -1 : 1,
|
|
770
854
|
hadWidth: widths[key],
|
|
771
855
|
cells: columnCells(key),
|
|
772
856
|
width: startWidth,
|
|
@@ -780,7 +864,7 @@ export function DataTable<T>({
|
|
|
780
864
|
const moveResize = (e: React.PointerEvent<HTMLElement>) => {
|
|
781
865
|
const drag = resizing.current;
|
|
782
866
|
if (!drag || drag.pointerId !== e.pointerId) return;
|
|
783
|
-
drag.width = Math.max(40, Math.round(drag.startWidth + (e.clientX - drag.startX)));
|
|
867
|
+
drag.width = Math.max(40, Math.round(drag.startWidth + drag.sign * (e.clientX - drag.startX)));
|
|
784
868
|
drag.moved = true;
|
|
785
869
|
applyLiveWidth(drag.cells, drag.width);
|
|
786
870
|
};
|
|
@@ -860,12 +944,24 @@ export function DataTable<T>({
|
|
|
860
944
|
// renderer is opaque to us; a link inside one is the caller's to declare by
|
|
861
945
|
// marking the column it comes from.
|
|
862
946
|
const mobileCardLinkable = mobileColumns.every((c) => !c.noRowLink);
|
|
947
|
+
// The card list's reading direction, for the one thing on it CSS cannot flip: which
|
|
948
|
+
// physical drag a logical `start`/`end` swipe action means. Read off the DOM through
|
|
949
|
+
// a ref callback (it runs at commit, like a layout effect) because `dir` is an
|
|
950
|
+
// attribute of some ancestor the table does not own; LTR until the first commit,
|
|
951
|
+
// which is also before any finger can have touched a row.
|
|
952
|
+
const [mobileDir, setMobileDir] = useState<Direction>("ltr");
|
|
953
|
+
const mobileRootRef = (el: HTMLElement | null) => {
|
|
954
|
+
if (!el) return;
|
|
955
|
+
const d = dirOf(el);
|
|
956
|
+
if (d !== mobileDir) setMobileDir(d);
|
|
957
|
+
};
|
|
863
958
|
|
|
864
959
|
const { mobileSlice, canRevealMoreMobile, loadMoreRef } = useMobileReveal({
|
|
865
960
|
rows: sorted,
|
|
866
961
|
slice,
|
|
867
962
|
isServer,
|
|
868
963
|
isMdUp,
|
|
964
|
+
unpaged,
|
|
869
965
|
defaultPageSize,
|
|
870
966
|
});
|
|
871
967
|
// The row whose selection was last toggled, so Shift+click can select the range up
|
|
@@ -874,6 +970,8 @@ export function DataTable<T>({
|
|
|
874
970
|
// header was clicked addresses a different row afterwards — and the range then ran
|
|
875
971
|
// from a row the user had never touched.
|
|
876
972
|
const selectionAnchor = useRef<string | number | null>(null);
|
|
973
|
+
/** The checkbox click that drew a range, so its `onChange` can be told apart. */
|
|
974
|
+
const rangeClick = useRef<Event | null>(null);
|
|
877
975
|
const canSelect = (row: T) => !!selection && (selection.isSelectable?.(row) ?? true);
|
|
878
976
|
const selectRange = (toIndex: number) => {
|
|
879
977
|
if (!selection || selectionAnchor.current === null) return false;
|
|
@@ -926,13 +1024,14 @@ export function DataTable<T>({
|
|
|
926
1024
|
// uses for exactly this, and it points the way the row actually opens: right into
|
|
927
1025
|
// a sheet, or down into an inline panel that flips it when expanded.
|
|
928
1026
|
const opensDetail = interactive && !!expandedRow;
|
|
929
|
-
const
|
|
1027
|
+
const swipeActions = mobileSwipeActions?.(row);
|
|
1028
|
+
const swipe = swipeActions ? physicalSwipe(swipeActions, mobileDir) : null;
|
|
930
1029
|
// An expanded row never swipes: the editor below it owns the horizontal space,
|
|
931
1030
|
// and dragging the header away from its own form reads as a glitch.
|
|
932
1031
|
const swipeEnabled = !!swipe && !expanded;
|
|
933
1032
|
const href = mobileCardLinkable ? rowHref?.(row) : undefined;
|
|
934
1033
|
const cardClass = cn(
|
|
935
|
-
"w-full px-4 py-3 text-
|
|
1034
|
+
"w-full px-4 py-3 text-start flex items-center gap-3",
|
|
936
1035
|
// Live #320's other half: the card acknowledges the touch before the sheet
|
|
937
1036
|
// arrives. On a cold route the data can take a beat, and an unacknowledged tap
|
|
938
1037
|
// reads as "did that register?" — which is most of what "abrupt" means here.
|
|
@@ -979,7 +1078,11 @@ export function DataTable<T>({
|
|
|
979
1078
|
// `aria-hidden`: the row already announces itself as a button with
|
|
980
1079
|
// `aria-expanded`, so the icon would only add a second, wordless stop.
|
|
981
1080
|
(mobileExpandAsDialog ? (
|
|
982
|
-
|
|
1081
|
+
// Points INTO the sheet, which is the reading-forward direction.
|
|
1082
|
+
<ChevronRight
|
|
1083
|
+
aria-hidden
|
|
1084
|
+
className="size-4 shrink-0 text-[var(--text-placeholder)] rtl:-scale-x-100"
|
|
1085
|
+
/>
|
|
983
1086
|
) : (
|
|
984
1087
|
<ChevronDown
|
|
985
1088
|
aria-hidden
|
|
@@ -1074,7 +1177,7 @@ export function DataTable<T>({
|
|
|
1074
1177
|
inside either one. */}
|
|
1075
1178
|
<span {...regionProps} />
|
|
1076
1179
|
{!isMdUp && (
|
|
1077
|
-
<div>
|
|
1180
|
+
<div ref={mobileRootRef}>
|
|
1078
1181
|
{/* Mobile filter access (feedback #299): the per-column filter popovers
|
|
1079
1182
|
live in the desktop header, which the card list doesn't render — so
|
|
1080
1183
|
expose the same filters through a bottom sheet here. */}
|
|
@@ -1092,7 +1195,14 @@ export function DataTable<T>({
|
|
|
1092
1195
|
locale={locale}
|
|
1093
1196
|
/>
|
|
1094
1197
|
)}
|
|
1095
|
-
<ul
|
|
1198
|
+
<ul
|
|
1199
|
+
className={cn(
|
|
1200
|
+
"divide-y divide-[var(--border)]",
|
|
1201
|
+
// Same busy treatment as the desktop body; see `isLoading` below.
|
|
1202
|
+
isLoading && mobileSlice.length > 0 && "opacity-60 transition-opacity",
|
|
1203
|
+
)}
|
|
1204
|
+
aria-busy={isLoading || undefined}
|
|
1205
|
+
>
|
|
1096
1206
|
{mobileGroups
|
|
1097
1207
|
? mobileGroups.map((g) => (
|
|
1098
1208
|
<Fragment key={`hb-group:${g.key}`}>
|
|
@@ -1105,7 +1215,7 @@ export function DataTable<T>({
|
|
|
1105
1215
|
: mobileSlice.map(renderMobileRow)}
|
|
1106
1216
|
{mobileSlice.length === 0 && (
|
|
1107
1217
|
<li className="px-4 py-6 text-center text-sm text-[var(--text-muted)]">
|
|
1108
|
-
{empty ?? "—"}
|
|
1218
|
+
{isLoading ? <LoadingText label={labels.loading} /> : (empty ?? "—")}
|
|
1109
1219
|
</li>
|
|
1110
1220
|
)}
|
|
1111
1221
|
{/* Endless-scroll sentinel: observed by IntersectionObserver to pull in
|
|
@@ -1128,7 +1238,7 @@ export function DataTable<T>({
|
|
|
1128
1238
|
pageSize={paginationPageSize}
|
|
1129
1239
|
total={paginationTotal}
|
|
1130
1240
|
onPage={goToPage}
|
|
1131
|
-
onPageSize={
|
|
1241
|
+
onPageSize={serverPagination!.onPageSizeChange}
|
|
1132
1242
|
labels={labels}
|
|
1133
1243
|
locale={locale}
|
|
1134
1244
|
/>
|
|
@@ -1160,7 +1270,14 @@ export function DataTable<T>({
|
|
|
1160
1270
|
>
|
|
1161
1271
|
{/* A table with no name is "table" in a screen reader's list of tables, and
|
|
1162
1272
|
an app renders several. `labels.table` is how a call site says which. */}
|
|
1163
|
-
|
|
1273
|
+
{/* `aria-busy` while a server page is in flight: a reader holds off announcing
|
|
1274
|
+
the rows that are about to be replaced, and the dimmed body says the same
|
|
1275
|
+
to the eye. The pager stays live — see `ServerPagination.isLoading`. */}
|
|
1276
|
+
<table
|
|
1277
|
+
className="w-full text-sm"
|
|
1278
|
+
aria-label={labels.table}
|
|
1279
|
+
aria-busy={isLoading || undefined}
|
|
1280
|
+
>
|
|
1164
1281
|
{/* Sticky header. position:sticky pins to the nearest scroll-container
|
|
1165
1282
|
ancestor — so the header can only stick to THIS wrapper, never the
|
|
1166
1283
|
page. For that to actually hold the header in place, the wrapper
|
|
@@ -1171,7 +1288,7 @@ export function DataTable<T>({
|
|
|
1171
1288
|
<thead> isn't reliable across browsers, so we put it on each <th>
|
|
1172
1289
|
(see headClassName below). */}
|
|
1173
1290
|
<thead className="bg-[var(--bg-surface-2)] text-[var(--text-secondary)]">
|
|
1174
|
-
<tr className="text-
|
|
1291
|
+
<tr className="text-start">
|
|
1175
1292
|
{selection && (
|
|
1176
1293
|
<th
|
|
1177
1294
|
scope="col"
|
|
@@ -1199,7 +1316,9 @@ export function DataTable<T>({
|
|
|
1199
1316
|
const active = sortIdx !== -1;
|
|
1200
1317
|
const Icon = active ? (sorts[sortIdx].dir === "asc" ? ArrowUp : ArrowDown) : ArrowUpDown;
|
|
1201
1318
|
const filterActive = filterState && isFilterActive(filterState);
|
|
1202
|
-
|
|
1319
|
+
// Logical, and a legacy `text-right` still counts — see `logicalAlign`.
|
|
1320
|
+
const headClass = logicalAlign(col.headClassName);
|
|
1321
|
+
const isEndAligned = !!headClass && END_ALIGN.test(headClass);
|
|
1203
1322
|
const width = widths[col.key];
|
|
1204
1323
|
return (
|
|
1205
1324
|
<th
|
|
@@ -1236,13 +1355,13 @@ export function DataTable<T>({
|
|
|
1236
1355
|
// Each <th> carries its own bg so the row doesn't render
|
|
1237
1356
|
// transparent over the data rows underneath.
|
|
1238
1357
|
"sticky top-0 z-10 bg-[var(--bg-surface-2)] backdrop-blur-sm",
|
|
1239
|
-
|
|
1358
|
+
headClass,
|
|
1240
1359
|
)}
|
|
1241
1360
|
>
|
|
1242
1361
|
<div
|
|
1243
1362
|
className={cn(
|
|
1244
1363
|
"flex items-center gap-1",
|
|
1245
|
-
|
|
1364
|
+
isEndAligned && "flex-row-reverse",
|
|
1246
1365
|
)}
|
|
1247
1366
|
>
|
|
1248
1367
|
<Tooltip label={sortable ? labels.sortHint : undefined} portal>
|
|
@@ -1251,7 +1370,7 @@ export function DataTable<T>({
|
|
|
1251
1370
|
onClick={(e) => sortable && toggleSort(col.key, e.shiftKey)}
|
|
1252
1371
|
disabled={!sortable}
|
|
1253
1372
|
className={cn(
|
|
1254
|
-
"flex items-center gap-1 text-
|
|
1373
|
+
"flex items-center gap-1 text-start",
|
|
1255
1374
|
sortable && "hover:text-[var(--text-primary)]",
|
|
1256
1375
|
)}
|
|
1257
1376
|
>
|
|
@@ -1321,14 +1440,18 @@ export function DataTable<T>({
|
|
|
1321
1440
|
}}
|
|
1322
1441
|
// `touch-none`: without it the browser claims a finger drag for
|
|
1323
1442
|
// scrolling before the first `pointermove` ever arrives.
|
|
1324
|
-
|
|
1443
|
+
// `end-0` and a translate that flips with it: the handle sits on the column's
|
|
1444
|
+
// trailing edge, which is its left one in a right-to-left table.
|
|
1445
|
+
className="absolute end-0 top-0 z-10 h-full w-1.5 -translate-x-1/2 rtl:translate-x-1/2 cursor-col-resize select-none touch-none hover:bg-[var(--brand-bg-hover)]"
|
|
1325
1446
|
/>
|
|
1326
1447
|
</th>
|
|
1327
1448
|
);
|
|
1328
1449
|
})}
|
|
1329
1450
|
</tr>
|
|
1330
1451
|
</thead>
|
|
1331
|
-
<tbody
|
|
1452
|
+
<tbody
|
|
1453
|
+
className={cn(isLoading && slice.length > 0 && "opacity-60 transition-opacity")}
|
|
1454
|
+
>
|
|
1332
1455
|
{leadingRow && (
|
|
1333
1456
|
<tr className="border-t border-[var(--border)]">
|
|
1334
1457
|
<td colSpan={totalColSpan} className="p-0">
|
|
@@ -1395,12 +1518,27 @@ export function DataTable<T>({
|
|
|
1395
1518
|
className="size-4 accent-indigo-600"
|
|
1396
1519
|
checked={selection.isSelected(row)}
|
|
1397
1520
|
// Shift+click selects the range from the last toggled
|
|
1398
|
-
// row
|
|
1399
|
-
// onChange
|
|
1521
|
+
// row. `preventDefault` puts the box back, but it does NOT
|
|
1522
|
+
// stop React's `onChange`: React derives a checkbox's
|
|
1523
|
+
// change from the `click` itself, after the browser has
|
|
1524
|
+
// already flipped `checked` and before the cancellation
|
|
1525
|
+
// reverts it — so the same click also reported itself as
|
|
1526
|
+
// a single toggle, and the owner got `onToggleMany` and
|
|
1527
|
+
// then `onToggle` for the clicked row. The click is
|
|
1528
|
+
// remembered by identity and its change is dropped; a
|
|
1529
|
+
// boolean flag would outlive a click whose change never
|
|
1530
|
+
// came and swallow the user's next real one.
|
|
1400
1531
|
onClick={(e) => {
|
|
1401
|
-
if (e.shiftKey && selectRange(rowIndex))
|
|
1532
|
+
if (e.shiftKey && selectRange(rowIndex)) {
|
|
1533
|
+
e.preventDefault();
|
|
1534
|
+
rangeClick.current = e.nativeEvent;
|
|
1535
|
+
}
|
|
1402
1536
|
}}
|
|
1403
1537
|
onChange={(e) => {
|
|
1538
|
+
if (rangeClick.current && e.nativeEvent === rangeClick.current) {
|
|
1539
|
+
rangeClick.current = null;
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1404
1542
|
selectionAnchor.current = rowKey(row);
|
|
1405
1543
|
selection.onToggle(row, e.target.checked);
|
|
1406
1544
|
}}
|
|
@@ -1416,7 +1554,11 @@ export function DataTable<T>({
|
|
|
1416
1554
|
key={col.key}
|
|
1417
1555
|
data-col={col.key}
|
|
1418
1556
|
style={width ? { width, minWidth: width, maxWidth: width } : undefined}
|
|
1419
|
-
className={cn(
|
|
1557
|
+
className={cn(
|
|
1558
|
+
"px-3 py-2 align-top",
|
|
1559
|
+
width && "overflow-hidden text-ellipsis",
|
|
1560
|
+
logicalAlign(col.className),
|
|
1561
|
+
)}
|
|
1420
1562
|
>
|
|
1421
1563
|
{col === linkColumn && href ? (
|
|
1422
1564
|
// No `onActivate`: the click is left to bubble to the
|
|
@@ -1459,7 +1601,9 @@ export function DataTable<T>({
|
|
|
1459
1601
|
colSpan={totalColSpan}
|
|
1460
1602
|
className="px-3 py-4 text-center text-[var(--text-muted)]"
|
|
1461
1603
|
>
|
|
1462
|
-
{
|
|
1604
|
+
{/* Nothing is not the same as not-yet: "no results" while the first
|
|
1605
|
+
page is still in flight is a claim the table cannot make. */}
|
|
1606
|
+
{isLoading ? <LoadingText label={labels.loading} /> : (empty ?? "—")}
|
|
1463
1607
|
</td>
|
|
1464
1608
|
</tr>
|
|
1465
1609
|
)}
|
|
@@ -1475,7 +1619,7 @@ export function DataTable<T>({
|
|
|
1475
1619
|
onPage={goToPage}
|
|
1476
1620
|
onPageSize={
|
|
1477
1621
|
isServer
|
|
1478
|
-
?
|
|
1622
|
+
? serverPagination!.onPageSizeChange
|
|
1479
1623
|
: (n) => {
|
|
1480
1624
|
setPageSize(n);
|
|
1481
1625
|
setPage(0);
|
|
@@ -1495,9 +1639,9 @@ export function DataTable<T>({
|
|
|
1495
1639
|
tabIndex={showSettings ? -1 : 0}
|
|
1496
1640
|
className={cn(
|
|
1497
1641
|
"group flex shrink-0 items-start justify-center overflow-hidden pt-3 transition-[width] duration-200 ease-out",
|
|
1498
|
-
"border-
|
|
1642
|
+
"border-s border-[var(--border)]",
|
|
1499
1643
|
"hover:bg-[var(--bg-hover)]",
|
|
1500
|
-
showSettings ? "w-0 border-
|
|
1644
|
+
showSettings ? "w-0 border-s-0" : "w-8 cursor-pointer",
|
|
1501
1645
|
)}
|
|
1502
1646
|
>
|
|
1503
1647
|
<span
|
|
@@ -1514,7 +1658,7 @@ export function DataTable<T>({
|
|
|
1514
1658
|
showSettings ? "w-56" : "w-0",
|
|
1515
1659
|
)}
|
|
1516
1660
|
>
|
|
1517
|
-
<div className="flex w-56 flex-col border-
|
|
1661
|
+
<div className="flex w-56 flex-col border-s border-[var(--border)] p-2">
|
|
1518
1662
|
<div className="mb-2 flex items-center justify-between gap-1">
|
|
1519
1663
|
<div className="text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]">
|
|
1520
1664
|
{columnsCountLabel}
|
|
@@ -1572,6 +1716,16 @@ export function DataTable<T>({
|
|
|
1572
1716
|
);
|
|
1573
1717
|
}
|
|
1574
1718
|
|
|
1719
|
+
/** The loading row's content: a spinner beside the word, the word for everyone. */
|
|
1720
|
+
function LoadingText({ label }: { label: string }) {
|
|
1721
|
+
return (
|
|
1722
|
+
<span className="inline-flex items-center gap-2">
|
|
1723
|
+
<Spinner label={null} className="size-4" />
|
|
1724
|
+
{label}
|
|
1725
|
+
</span>
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1575
1729
|
// ---------- Mobile filter sheet ----------
|
|
1576
1730
|
|
|
1577
1731
|
/**
|
|
@@ -1606,6 +1760,10 @@ function MobileFilters<T>({
|
|
|
1606
1760
|
locale?: string;
|
|
1607
1761
|
}) {
|
|
1608
1762
|
const [open, setOpen] = useState(false);
|
|
1763
|
+
// The sheet is portalled to <body>, out from under whatever `dir` the table sits
|
|
1764
|
+
// in, so it carries the trigger's direction with it — read at the moment of opening,
|
|
1765
|
+
// from the element the user actually pressed.
|
|
1766
|
+
const [dir, setDir] = useState<Direction>("ltr");
|
|
1609
1767
|
const [expanded, setExpanded] = useState<string | null>(null);
|
|
1610
1768
|
const backdropClose = useBackdropClose(() => setOpen(false));
|
|
1611
1769
|
useBodyScrollLock(open);
|
|
@@ -1633,7 +1791,10 @@ function MobileFilters<T>({
|
|
|
1633
1791
|
<div className="flex items-center justify-between gap-2 border-b border-[var(--border)] px-4 py-2">
|
|
1634
1792
|
<button
|
|
1635
1793
|
type="button"
|
|
1636
|
-
onClick={() =>
|
|
1794
|
+
onClick={(e) => {
|
|
1795
|
+
setDir(dirOf(e.currentTarget));
|
|
1796
|
+
setOpen(true);
|
|
1797
|
+
}}
|
|
1637
1798
|
className="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]"
|
|
1638
1799
|
>
|
|
1639
1800
|
<Filter className="size-4" />
|
|
@@ -1659,6 +1820,7 @@ function MobileFilters<T>({
|
|
|
1659
1820
|
className="fixed inset-0 z-50 flex items-end justify-center bg-black/40"
|
|
1660
1821
|
role="dialog"
|
|
1661
1822
|
aria-modal="true"
|
|
1823
|
+
dir={dir}
|
|
1662
1824
|
{...backdropClose}
|
|
1663
1825
|
>
|
|
1664
1826
|
<div className="flex max-h-[85vh] w-full flex-col overflow-hidden rounded-t-2xl bg-[var(--bg-surface)] shadow-xl">
|
|
@@ -1668,7 +1830,7 @@ function MobileFilters<T>({
|
|
|
1668
1830
|
type="button"
|
|
1669
1831
|
onClick={() => setOpen(false)}
|
|
1670
1832
|
aria-label={labels.close}
|
|
1671
|
-
className="-
|
|
1833
|
+
className="-me-1 rounded p-1.5 text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
|
|
1672
1834
|
>
|
|
1673
1835
|
<X className="size-5" />
|
|
1674
1836
|
</button>
|
|
@@ -1684,7 +1846,7 @@ function MobileFilters<T>({
|
|
|
1684
1846
|
type="button"
|
|
1685
1847
|
onClick={() => setExpanded(isItemOpen ? null : col.key)}
|
|
1686
1848
|
aria-expanded={isItemOpen}
|
|
1687
|
-
className="flex w-full items-center justify-between gap-2 px-4 py-3 text-
|
|
1849
|
+
className="flex w-full items-center justify-between gap-2 px-4 py-3 text-start"
|
|
1688
1850
|
>
|
|
1689
1851
|
<span className="flex items-center gap-2 text-sm font-medium text-[var(--text-secondary)]">
|
|
1690
1852
|
{col.header}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useEffect, useId, useMemo, useRef } from "react";
|
|
1
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { ComponentPropsWithoutRef, ComponentType, ReactNode, RefObject } from "react";
|
|
3
3
|
import { Calendar, CalendarClock, ChevronLeft, ChevronRight, X } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
+
import { dirOf, type Direction } from "../lib/direction";
|
|
5
6
|
import { addDaysIso, parseIsoDate } from "../lib/dates";
|
|
6
7
|
import {
|
|
7
8
|
DEFAULT_DATE_PICKER_LABELS,
|
|
@@ -157,7 +158,7 @@ function DateFieldTrigger({
|
|
|
157
158
|
aria-invalid={invalid || aria["aria-invalid"] === true || aria["aria-invalid"] === "true" || undefined}
|
|
158
159
|
className={cn(
|
|
159
160
|
FIELD_TRIGGER,
|
|
160
|
-
"
|
|
161
|
+
"pe-9",
|
|
161
162
|
padded && FIELD_FLOATING_PAD,
|
|
162
163
|
disabled && "cursor-not-allowed opacity-50",
|
|
163
164
|
invalid && FIELD_INVALID,
|
|
@@ -218,6 +219,12 @@ function DateField({
|
|
|
218
219
|
}) {
|
|
219
220
|
const showClear = Boolean(clearable && hasValue && !disabled);
|
|
220
221
|
const [aria, wrapperRest] = splitTriggerAria(rest);
|
|
222
|
+
// The panel is portalled to <body> and leaves the subtree whose `dir` it inherited,
|
|
223
|
+
// so a calendar in an RTL form opened LTR — the grid's columns, its arrow keys and
|
|
224
|
+
// its chevrons all the wrong way round. Read at the moment of opening (the only way
|
|
225
|
+
// the popover opens is through `toggle`) and put back on the panel.
|
|
226
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
227
|
+
const [dir, setDir] = useState<Direction>("ltr");
|
|
221
228
|
const id = useId();
|
|
222
229
|
const labelId = `${id}-label`;
|
|
223
230
|
const valueId = `${id}-value`;
|
|
@@ -229,7 +236,7 @@ function DateField({
|
|
|
229
236
|
return (
|
|
230
237
|
// The caller's attributes land here, on the field's own box — the trigger inside is
|
|
231
238
|
// named by `aria-labelledby` and must keep the id pair it is given.
|
|
232
|
-
<div {...wrapperRest} className={cn("relative", className)}>
|
|
239
|
+
<div {...wrapperRest} ref={rootRef} className={cn("relative", className)}>
|
|
233
240
|
{label !== undefined && <FieldLabel>{label}</FieldLabel>}
|
|
234
241
|
{/* The visible FieldLabel is a plain span, not a `<label htmlFor>`, so it names
|
|
235
242
|
nothing on its own — this hidden twin is what the trigger is named by.
|
|
@@ -249,10 +256,14 @@ function DateField({
|
|
|
249
256
|
// Named for what it is. Unnamed, it fell back to `popover.panel`, and a date
|
|
250
257
|
// field announced its calendar as "Popover" — in English, in every language.
|
|
251
258
|
labels={{ panel: panelLabel }}
|
|
259
|
+
dir={dir}
|
|
252
260
|
trigger={({ open, toggle, ref }) => (
|
|
253
261
|
<DateFieldTrigger
|
|
254
262
|
open={open}
|
|
255
|
-
toggle={
|
|
263
|
+
toggle={() => {
|
|
264
|
+
setDir(dirOf(rootRef.current));
|
|
265
|
+
toggle();
|
|
266
|
+
}}
|
|
256
267
|
triggerRef={ref}
|
|
257
268
|
triggerText={triggerText}
|
|
258
269
|
hasValue={hasValue}
|
|
@@ -292,14 +303,14 @@ function DateField({
|
|
|
292
303
|
type="button"
|
|
293
304
|
aria-label={clearLabel}
|
|
294
305
|
onClick={onClear}
|
|
295
|
-
className="absolute
|
|
306
|
+
className="absolute end-1.5 top-1/2 -translate-y-1/2 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]"
|
|
296
307
|
>
|
|
297
308
|
<X className="size-4" />
|
|
298
309
|
</button>
|
|
299
310
|
) : (
|
|
300
311
|
<Calendar
|
|
301
312
|
aria-hidden
|
|
302
|
-
className="pointer-events-none absolute
|
|
313
|
+
className="pointer-events-none absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]"
|
|
303
314
|
/>
|
|
304
315
|
)}
|
|
305
316
|
</div>
|
|
@@ -312,12 +323,16 @@ function StepButton({
|
|
|
312
323
|
label,
|
|
313
324
|
disabled,
|
|
314
325
|
onClick,
|
|
326
|
+
mirror,
|
|
315
327
|
className,
|
|
316
328
|
}: {
|
|
317
329
|
icon: ComponentType<{ className?: string }>;
|
|
318
330
|
label: string;
|
|
319
331
|
disabled: boolean;
|
|
320
332
|
onClick: () => void;
|
|
333
|
+
/** Flip the icon in RTL — for the ‹ › day steps, whose "previous" points to the
|
|
334
|
+
* reading start. Not for the today icon, which has no direction. */
|
|
335
|
+
mirror?: boolean;
|
|
321
336
|
/** The button's corners in the joined group (see DatePicker). */
|
|
322
337
|
className?: string;
|
|
323
338
|
}) {
|
|
@@ -343,7 +358,7 @@ function StepButton({
|
|
|
343
358
|
className,
|
|
344
359
|
)}
|
|
345
360
|
>
|
|
346
|
-
<Icon className="size-4" />
|
|
361
|
+
<Icon className={cn("size-4", mirror && "rtl:-scale-x-100")} />
|
|
347
362
|
</button>
|
|
348
363
|
</Tooltip>
|
|
349
364
|
);
|
|
@@ -506,6 +521,7 @@ export function DatePicker({
|
|
|
506
521
|
{step && (
|
|
507
522
|
<StepButton
|
|
508
523
|
icon={ChevronLeft}
|
|
524
|
+
mirror
|
|
509
525
|
label={text.previousDay}
|
|
510
526
|
disabled={blocked(-1)}
|
|
511
527
|
onClick={() => onChange(target(-1))}
|
|
@@ -516,6 +532,7 @@ export function DatePicker({
|
|
|
516
532
|
{step && (
|
|
517
533
|
<StepButton
|
|
518
534
|
icon={ChevronRight}
|
|
535
|
+
mirror
|
|
519
536
|
label={text.nextDay}
|
|
520
537
|
disabled={blocked(1)}
|
|
521
538
|
onClick={() => onChange(target(1))}
|