@eifi1/ui-kit 0.6.0 → 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 +36 -7
- package/dist/components/autocomplete.js +68 -22
- 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/checkbox.d.ts +9 -0
- package/dist/components/checkbox.js +7 -2
- package/dist/components/checkbox.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 +42 -3
- 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/switch.d.ts +9 -0
- package/dist/components/switch.js +7 -2
- package/dist/components/switch.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 +123 -33
- 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/checkbox.tsx +16 -0
- 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 +102 -6
- 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/switch.tsx +16 -0
- 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
|
@@ -2,6 +2,7 @@ import { useId, useRef } from "react";
|
|
|
2
2
|
import type { KeyboardEvent, ReactNode } from "react";
|
|
3
3
|
import { Ban, Check } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
+
import { horizontalStep } from "../lib/direction";
|
|
5
6
|
import { Tooltip } from "./tooltip";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -97,14 +98,11 @@ export function TileRadioGroup<T extends string>({
|
|
|
97
98
|
// Left and right are VISUAL directions: in a right-to-left page the row runs the
|
|
98
99
|
// other way. Up and down follow reading order either way — the APG's radio group
|
|
99
100
|
// treats them as previous/next, and the tiles wrap, so there is no column to keep.
|
|
100
|
-
const
|
|
101
|
+
const horizontal = horizontalStep(e.key, e.currentTarget);
|
|
101
102
|
let step: number | "first" | "last";
|
|
102
|
-
switch (e.key) {
|
|
103
|
-
case "
|
|
104
|
-
step =
|
|
105
|
-
break;
|
|
106
|
-
case "ArrowLeft":
|
|
107
|
-
step = rtl ? 1 : -1;
|
|
103
|
+
switch (horizontal ? "horizontal" : e.key) {
|
|
104
|
+
case "horizontal":
|
|
105
|
+
step = horizontal;
|
|
108
106
|
break;
|
|
109
107
|
case "ArrowDown":
|
|
110
108
|
step = 1;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { ComponentPropsWithoutRef } from "react";
|
|
1
|
+
import type { ComponentPropsWithoutRef, KeyboardEvent, ReactElement } from "react";
|
|
2
2
|
import { cn } from "../lib/cn";
|
|
3
|
+
import { horizontalStep } from "../lib/direction";
|
|
3
4
|
|
|
4
5
|
export interface ToggleOption<T extends string> {
|
|
5
6
|
value: T;
|
|
@@ -74,7 +75,19 @@ export type ToggleGroupProps<T extends string> =
|
|
|
74
75
|
| ToggleGroupRequiredProps<T>
|
|
75
76
|
| ToggleGroupClearableProps<T>;
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Overloaded rather than typed by the union alone (keksdose, "Gaps found adopting
|
|
80
|
+
* 0.6.0" #7). Inferring `T` through a union of prop shapes let TypeScript settle on
|
|
81
|
+
* `string`, so `<ToggleGroup allowEmpty value={filter} onChange={setFilter} />` over a
|
|
82
|
+
* `useState<Status | null>` did not compile unless the caller spelled
|
|
83
|
+
* `<ToggleGroup<Status>>`. One signature per mode lets each infer `T` from its own
|
|
84
|
+
* `value`/`onChange`/`options`; the third keeps a caller that forwards a
|
|
85
|
+
* {@link ToggleGroupProps} union (a wrapper component) compiling.
|
|
86
|
+
*/
|
|
87
|
+
export function ToggleGroup<T extends string>(props: ToggleGroupClearableProps<T>): ReactElement;
|
|
88
|
+
export function ToggleGroup<T extends string>(props: ToggleGroupRequiredProps<T>): ReactElement;
|
|
89
|
+
export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): ReactElement;
|
|
90
|
+
export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): ReactElement {
|
|
78
91
|
const {
|
|
79
92
|
value,
|
|
80
93
|
options,
|
|
@@ -93,6 +106,27 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>) {
|
|
|
93
106
|
else props.onChange(next);
|
|
94
107
|
};
|
|
95
108
|
const clearable = props.allowEmpty === true;
|
|
109
|
+
// A radio group is ONE tab stop (the checked radio, else the first) and arrows move
|
|
110
|
+
// the choice — the pattern `role="radiogroup"` promises a screen-reader user. Before
|
|
111
|
+
// 0.7.0 each segment was its own tab stop with no arrow keys. The clearable mode is a
|
|
112
|
+
// row of toggle buttons, where separate tab stops are the pattern.
|
|
113
|
+
const tabStop = options.some((o) => o.value === value) ? value : options[0]?.value;
|
|
114
|
+
const onRadioKey = (e: KeyboardEvent<HTMLButtonElement>, index: number) => {
|
|
115
|
+
const last = options.length - 1;
|
|
116
|
+
let next: number | null = null;
|
|
117
|
+
const step = horizontalStep(e.key, e.currentTarget);
|
|
118
|
+
if (step !== 0) next = index + step;
|
|
119
|
+
else if (e.key === "ArrowDown") next = index + 1;
|
|
120
|
+
else if (e.key === "ArrowUp") next = index - 1;
|
|
121
|
+
else if (e.key === "Home") next = 0;
|
|
122
|
+
else if (e.key === "End") next = last;
|
|
123
|
+
if (next === null) return;
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
next = next < 0 ? last : next > last ? 0 : next;
|
|
126
|
+
const buttons = e.currentTarget.parentElement?.querySelectorAll<HTMLButtonElement>(":scope > button");
|
|
127
|
+
buttons?.[next]?.focus();
|
|
128
|
+
choose(options[next].value);
|
|
129
|
+
};
|
|
96
130
|
return (
|
|
97
131
|
<div
|
|
98
132
|
// The audit's named example of a closed prop list (§"Public API design"): the
|
|
@@ -135,7 +169,7 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>) {
|
|
|
135
169
|
className,
|
|
136
170
|
)}
|
|
137
171
|
>
|
|
138
|
-
{options.map((opt) => {
|
|
172
|
+
{options.map((opt, index) => {
|
|
139
173
|
const active = opt.value === value;
|
|
140
174
|
return (
|
|
141
175
|
<button
|
|
@@ -145,6 +179,8 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>) {
|
|
|
145
179
|
aria-checked={clearable ? undefined : active}
|
|
146
180
|
aria-pressed={clearable ? active : undefined}
|
|
147
181
|
disabled={disabled}
|
|
182
|
+
tabIndex={clearable ? undefined : opt.value === tabStop ? 0 : -1}
|
|
183
|
+
onKeyDown={clearable ? undefined : (e) => onRadioKey(e, index)}
|
|
148
184
|
onClick={() => choose(opt.value)}
|
|
149
185
|
className={cn(
|
|
150
186
|
// `truncate` (which carries whitespace-nowrap) rather than letting a
|
|
@@ -198,7 +198,11 @@ export function LegendGroup({ title, children }: { title: ReactNode; children: R
|
|
|
198
198
|
* front of it.
|
|
199
199
|
*
|
|
200
200
|
* A stroke is an SVG line with the chart's own `strokeDasharray`, not a CSS gradient
|
|
201
|
-
* approximating it: a gradient can say "solid or dashed" and nothing more.
|
|
201
|
+
* approximating it: a gradient can say "solid or dashed" and nothing more.
|
|
202
|
+
*
|
|
203
|
+
* A hidden stroke is NOT faded here: the button around it already is (`opacity-35`),
|
|
204
|
+
* and the line used to take its own 0.35 on top — 0.35 × 0.35, about 12 %, a mark
|
|
205
|
+
* gone rather than dimmed, while the square beside it read at the full 35 %. */
|
|
202
206
|
function LegendMark({ entry, off }: { entry: LegendEntry; off: boolean }) {
|
|
203
207
|
if (entry.marker === "stroke") {
|
|
204
208
|
return (
|
|
@@ -211,7 +215,6 @@ function LegendMark({ entry, off }: { entry: LegendEntry; off: boolean }) {
|
|
|
211
215
|
stroke={entry.color}
|
|
212
216
|
strokeWidth={2}
|
|
213
217
|
strokeDasharray={strokeDash(entry.dash ?? 0)}
|
|
214
|
-
opacity={off ? 0.35 : 1}
|
|
215
218
|
/>
|
|
216
219
|
</svg>
|
|
217
220
|
);
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
// chart. The one thing it cannot take from a CSS var is the tile colour: the label ink
|
|
9
9
|
// is MEASURED against the fill, and a `var(--chart-3)` string measures as nothing — see
|
|
10
10
|
// `useChartTokenHex` below for how the concrete hex is found instead.
|
|
11
|
-
import { useSyncExternalStore } from "react";
|
|
11
|
+
import { useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
12
12
|
import type { ComponentProps, ComponentPropsWithoutRef, KeyboardEvent, ReactNode } from "react";
|
|
13
13
|
import { Treemap as RechartsTreemap } from "recharts";
|
|
14
14
|
import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "./chart";
|
|
15
15
|
import { PALETTE_HEX, textOn } from "../theme/chart-palette";
|
|
16
16
|
import { parseHex } from "../theme/color";
|
|
17
|
+
import { dirOf, type Direction } from "../lib/direction";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* One tile.
|
|
@@ -174,6 +175,10 @@ export interface TreemapCellProps {
|
|
|
174
175
|
onNodeClick?: (id: string, name: string) => void;
|
|
175
176
|
redactNames?: boolean;
|
|
176
177
|
nodeNote?: (id: string) => string | undefined;
|
|
178
|
+
/** The page's reading direction, which `Treemap` reads off its own root. In `rtl`
|
|
179
|
+
* the label stands in the tile's top RIGHT corner and is shaped right-to-left, so a
|
|
180
|
+
* clipped name ends in its ellipsis on the reading end. Default `ltr`. */
|
|
181
|
+
dir?: Direction;
|
|
177
182
|
}
|
|
178
183
|
|
|
179
184
|
/**
|
|
@@ -199,6 +204,7 @@ export function TreemapCell({
|
|
|
199
204
|
onNodeClick,
|
|
200
205
|
redactNames,
|
|
201
206
|
nodeNote,
|
|
207
|
+
dir = "ltr",
|
|
202
208
|
}: TreemapCellProps) {
|
|
203
209
|
// Depth 0 is recharts' synthetic root, which spans the whole chart.
|
|
204
210
|
if (!depth) return null;
|
|
@@ -220,6 +226,11 @@ export function TreemapCell({
|
|
|
220
226
|
const rawNote = id != null && nodeNote ? nodeNote(id) : undefined;
|
|
221
227
|
const note =
|
|
222
228
|
label != null && rawNote != null && ph >= 36 ? fitLabel(rawNote, pw, NOTE_FONT_SIZE) : null;
|
|
229
|
+
// The label's START edge, 6px in from the tile's. The tiles themselves are laid out
|
|
230
|
+
// physically (the plot is not mirrored, see `ChartContainer`) and the chart's SVG is
|
|
231
|
+
// pinned `ltr`, so the direction is set on each <text> explicitly: `start` is then
|
|
232
|
+
// the right end in RTL, and the label grows leftwards from the tile's right edge.
|
|
233
|
+
const textX = dir === "rtl" ? px + pw - 6 : px + 6;
|
|
223
234
|
const activate = id != null && onNodeClick ? () => onNodeClick(id, name ?? "") : undefined;
|
|
224
235
|
const onKeyDown = activate
|
|
225
236
|
? (e: KeyboardEvent<SVGGElement>) => {
|
|
@@ -261,8 +272,9 @@ export function TreemapCell({
|
|
|
261
272
|
// A plain attribute, so the host's `[data-private]` rule reaches an SVG
|
|
262
273
|
// <text> exactly as it reaches a table cell.
|
|
263
274
|
data-private={redactNames ? "" : undefined}
|
|
264
|
-
x={
|
|
275
|
+
x={textX}
|
|
265
276
|
y={py + 15}
|
|
277
|
+
direction={dir}
|
|
266
278
|
fill={ink}
|
|
267
279
|
stroke={tileFill}
|
|
268
280
|
strokeWidth={2.5}
|
|
@@ -277,8 +289,9 @@ export function TreemapCell({
|
|
|
277
289
|
)}
|
|
278
290
|
{note != null && (
|
|
279
291
|
<text
|
|
280
|
-
x={
|
|
292
|
+
x={textX}
|
|
281
293
|
y={py + 29}
|
|
294
|
+
direction={dir}
|
|
282
295
|
fill={ink}
|
|
283
296
|
fillOpacity={0.85}
|
|
284
297
|
stroke={tileFill}
|
|
@@ -350,6 +363,14 @@ export function Treemap({
|
|
|
350
363
|
...rest
|
|
351
364
|
}: TreemapProps) {
|
|
352
365
|
const tokenHex = useChartTokenHex();
|
|
366
|
+
// Read off the DOM after mount: the `dir` a treemap sits under is an ancestor's
|
|
367
|
+
// attribute, and SVG text anchoring cannot follow it by CSS (see TreemapCell). Again
|
|
368
|
+
// on new data, because a map that had nothing to draw rendered no root to read.
|
|
369
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
370
|
+
const [dir, setDir] = useState<Direction>("ltr");
|
|
371
|
+
useLayoutEffect(() => {
|
|
372
|
+
setDir(dirOf(rootRef.current));
|
|
373
|
+
}, [data]);
|
|
353
374
|
const ramp = colors && colors.length > 0 ? colors : tokenHex;
|
|
354
375
|
// Coloured by index into `data` AS GIVEN, before anything is dropped: a caller
|
|
355
376
|
// painting a legend or a second map from the same list must land on the same hue
|
|
@@ -373,7 +394,7 @@ export function Treemap({
|
|
|
373
394
|
};
|
|
374
395
|
const Content = redactNames ? RedactedTooltipContent : ChartTooltipContent;
|
|
375
396
|
return (
|
|
376
|
-
<ChartContainer {...rest} config={TREEMAP_CONFIG} style={{ ...style, height }}>
|
|
397
|
+
<ChartContainer {...rest} ref={rootRef} config={TREEMAP_CONFIG} style={{ ...style, height }}>
|
|
377
398
|
{/* No chart-level `stroke`: recharts spreads it onto the <svg> root, from where
|
|
378
399
|
it inherits into every label (see TreemapCell). Each cell strokes its own rect. */}
|
|
379
400
|
<RechartsTreemap
|
|
@@ -381,7 +402,9 @@ export function Treemap({
|
|
|
381
402
|
dataKey="size"
|
|
382
403
|
fill="var(--chart-1)"
|
|
383
404
|
isAnimationActive={false}
|
|
384
|
-
content={
|
|
405
|
+
content={
|
|
406
|
+
<TreemapCell onNodeClick={onNodeClick} redactNames={redactNames} nodeNote={nodeNote} dir={dir} />
|
|
407
|
+
}
|
|
385
408
|
>
|
|
386
409
|
<ChartTooltip content={<Content hideLabel valueFormatter={format} />} />
|
|
387
410
|
</RechartsTreemap>
|
package/src/components/ui.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { forwardRef, useId, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import { ChevronDown, Eye, EyeOff, HelpCircle, Plus, X } from "lucide-react";
|
|
3
|
-
import type { ButtonHTMLAttributes, ComponentPropsWithoutRef, InputHTMLAttributes, KeyboardEvent, MouseEvent, ReactNode, SelectHTMLAttributes, TextareaHTMLAttributes } from "react";
|
|
3
|
+
import type { ButtonHTMLAttributes, ComponentPropsWithoutRef, InputHTMLAttributes, KeyboardEvent, MouseEvent, ReactNode, Ref, SelectHTMLAttributes, TextareaHTMLAttributes } from "react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
+
import { horizontalStep } from "../lib/direction";
|
|
5
6
|
import { useMediaQuery } from "../hooks/use-media-query";
|
|
6
7
|
import { Tooltip } from "./tooltip";
|
|
7
8
|
import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
@@ -49,6 +50,10 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
49
50
|
/** In a flex row next to a taller labelled field, fill the field's height so the
|
|
50
51
|
* two line up. No effect outside a flex row. */
|
|
51
52
|
stretch?: boolean;
|
|
53
|
+
/** The `<button>` element. React 19 passes `ref` to a function component as an
|
|
54
|
+
* ordinary prop, so it rides `...rest` onto the element with no `forwardRef` —
|
|
55
|
+
* declared here only because `ButtonHTMLAttributes` does not carry it. */
|
|
56
|
+
ref?: Ref<HTMLButtonElement>;
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
export function Button({ variant = "primary", stretch, className, ...rest }: ButtonProps) {
|
|
@@ -275,7 +280,7 @@ export const FIELD_DISPLAY =
|
|
|
275
280
|
// button the way the native Select does.
|
|
276
281
|
export const FIELD_TRIGGER = cn(
|
|
277
282
|
FIELD_BASE,
|
|
278
|
-
"relative flex items-center justify-between gap-2 text-
|
|
283
|
+
"relative flex items-center justify-between gap-2 text-start hover:bg-[var(--bg-hover)]",
|
|
279
284
|
);
|
|
280
285
|
|
|
281
286
|
/** An alias rather than an interface: the chevron adds nothing of its own to an
|
|
@@ -290,7 +295,10 @@ export type FieldChevronProps = Omit<ComponentPropsWithoutRef<"svg">, "children"
|
|
|
290
295
|
* it centres on the *content* box instead, and `FIELD_FLOATING_PAD` (pt-4 pb-1)
|
|
291
296
|
* pushes that box's midline down — so a labelled currency/multi-select chevron
|
|
292
297
|
* sat visibly lower than the native select's right beside it (feedback #400).
|
|
293
|
-
* Pair it with `
|
|
298
|
+
* Pair it with `pe-9` on the trigger so the value can't run underneath. At the
|
|
299
|
+
* inline END, not the right: a right-to-left form reads the value from the right,
|
|
300
|
+
* and a chevron parked on top of its first word hid exactly the part that says
|
|
301
|
+
* what was picked. */
|
|
294
302
|
export function FieldChevron({ className, ...rest }: FieldChevronProps) {
|
|
295
303
|
return (
|
|
296
304
|
<ChevronDown
|
|
@@ -300,7 +308,7 @@ export function FieldChevron({ className, ...rest }: FieldChevronProps) {
|
|
|
300
308
|
// announcement of the same field.
|
|
301
309
|
aria-hidden
|
|
302
310
|
className={cn(
|
|
303
|
-
"pointer-events-none absolute
|
|
311
|
+
"pointer-events-none absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]",
|
|
304
312
|
className,
|
|
305
313
|
)}
|
|
306
314
|
/>
|
|
@@ -311,7 +319,7 @@ export function FieldChevron({ className, ...rest }: FieldChevronProps) {
|
|
|
311
319
|
// floats up INSIDE the top strip on focus or once the field has a value. Sits on
|
|
312
320
|
// the field's own surface, so no background chip and nothing to mismatch the card.
|
|
313
321
|
export const FLOATING_LABEL_CLASS = cn(
|
|
314
|
-
"pointer-events-none absolute
|
|
322
|
+
"pointer-events-none absolute start-3 top-2.5 text-sm text-[var(--text-placeholder)] transition-all",
|
|
315
323
|
"max-w-[calc(100%-1.5rem)] truncate",
|
|
316
324
|
"peer-focus:top-1 peer-focus:text-[11px] peer-focus:leading-tight peer-focus:text-[var(--text-secondary)]",
|
|
317
325
|
"peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:text-[11px] peer-[:not(:placeholder-shown)]:leading-tight peer-[:not(:placeholder-shown)]:text-[var(--text-secondary)]",
|
|
@@ -324,13 +332,13 @@ export const FLOATING_LABEL_CLASS = cn(
|
|
|
324
332
|
// selector, so the classes have to sit on the element that is actually a sibling
|
|
325
333
|
// of the input, and a label nested inside a wrapper is not one.
|
|
326
334
|
//
|
|
327
|
-
// Wider
|
|
335
|
+
// Wider end clearance than the label alone takes (`3rem` rather than `1.5rem`),
|
|
328
336
|
// because the controls that carry an animated label are the ones with something
|
|
329
|
-
// at the
|
|
337
|
+
// at the end edge of the field — NumberInput's calculator is the case this was
|
|
330
338
|
// written for. The label truncates a little sooner; the alternative was the "?"
|
|
331
339
|
// sitting on top of a button (steering-design feedback #48).
|
|
332
340
|
const FLOATING_ROW_CLASS = cn(
|
|
333
|
-
"pointer-events-none absolute
|
|
341
|
+
"pointer-events-none absolute start-3 top-2.5 flex items-center gap-1 transition-all",
|
|
334
342
|
"max-w-[calc(100%-3rem)] text-sm text-[var(--text-placeholder)]",
|
|
335
343
|
"peer-focus:top-1 peer-focus:text-[11px] peer-focus:leading-tight peer-focus:text-[var(--text-secondary)]",
|
|
336
344
|
"peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:text-[11px] peer-[:not(:placeholder-shown)]:leading-tight peer-[:not(:placeholder-shown)]:text-[var(--text-secondary)]",
|
|
@@ -348,7 +356,7 @@ const STATIC_LABEL_TYPE =
|
|
|
348
356
|
// A field that always has a value (select / dropdown trigger) keeps the label
|
|
349
357
|
// permanently in the floated position — small, in the top strip, value below.
|
|
350
358
|
export const FLOATING_LABEL_STATIC = cn(
|
|
351
|
-
"pointer-events-none absolute
|
|
359
|
+
"pointer-events-none absolute start-3 top-1",
|
|
352
360
|
STATIC_LABEL_TYPE,
|
|
353
361
|
"max-w-[calc(100%-1.5rem)] truncate",
|
|
354
362
|
);
|
|
@@ -377,7 +385,7 @@ export interface FloatingFieldProps extends ComponentPropsWithoutRef<"div"> {
|
|
|
377
385
|
* It rides an ANIMATED label too, and the two cases place it differently on
|
|
378
386
|
* purpose. A static label has a field-wide strip to itself, so the hint sits
|
|
379
387
|
* at the far end of it and a long label truncates into the gap. An animated
|
|
380
|
-
* one belongs to a control with something at the
|
|
388
|
+
* one belongs to a control with something at the end edge of the field — a
|
|
381
389
|
* calculator, a stepper — so the hint follows the label instead, and it is
|
|
382
390
|
* the label that gives way (steering-design feedback #48). */
|
|
383
391
|
hint?: ReactNode;
|
|
@@ -422,8 +430,8 @@ export function FloatingField({
|
|
|
422
430
|
<div {...rest} className={cn("relative", className)}>
|
|
423
431
|
{children}
|
|
424
432
|
{withHint ? (
|
|
425
|
-
// Static: `inset-x-3` rather than `
|
|
426
|
-
// the field's own
|
|
433
|
+
// Static: `inset-x-3` rather than `start-3`, so a long label truncates at
|
|
434
|
+
// the field's own end padding instead of running under the chevron.
|
|
427
435
|
// Animated: the row floats with the label and is only as wide as it needs
|
|
428
436
|
// to be. Either way the hint keeps its width (`shrink-0`) and the label
|
|
429
437
|
// is the one that gives way.
|
|
@@ -765,7 +773,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|
|
765
773
|
disabled={rest.disabled}
|
|
766
774
|
aria-label={revealed ? passwordText.hide : passwordText.show}
|
|
767
775
|
aria-pressed={revealed}
|
|
768
|
-
className="absolute inset-y-0
|
|
776
|
+
className="absolute inset-y-0 end-0 flex items-center rounded-e-md px-2.5 text-[var(--text-placeholder)] transition-colors hover:text-[var(--text-secondary)] focus:outline-none focus:ring-2 focus:ring-[var(--brand)] disabled:cursor-default disabled:opacity-50"
|
|
769
777
|
>
|
|
770
778
|
{revealed ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
|
771
779
|
</button>
|
|
@@ -804,7 +812,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|
|
804
812
|
{...rest}
|
|
805
813
|
aria-invalid={isInvalid || rest["aria-invalid"] || undefined}
|
|
806
814
|
aria-describedby={describedBy}
|
|
807
|
-
className={cn(FIELD_BASE, "
|
|
815
|
+
className={cn(FIELD_BASE, "pe-9", inputClassName, isInvalid && FIELD_INVALID)}
|
|
808
816
|
/>
|
|
809
817
|
{revealToggle}
|
|
810
818
|
</div>
|
|
@@ -831,7 +839,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|
|
831
839
|
asDisplay
|
|
832
840
|
? cn(FIELD_DISPLAY, "text-xl font-semibold leading-snug")
|
|
833
841
|
: FLOATING_INPUT_CLASS,
|
|
834
|
-
isPassword && "
|
|
842
|
+
isPassword && "pe-9",
|
|
835
843
|
inputClassName,
|
|
836
844
|
isInvalid && FIELD_INVALID,
|
|
837
845
|
)}
|
|
@@ -858,7 +866,9 @@ export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement
|
|
|
858
866
|
* the tall box to float in, so a labelled Select ignores `"sm"`.
|
|
859
867
|
*
|
|
860
868
|
* A NUMBER is still the native attribute (the rows of a list box) and is passed
|
|
861
|
-
* straight through, so the one HTML meaning of `size` keeps working.
|
|
869
|
+
* straight through, so the one HTML meaning of `size` keeps working. Above 1 (as
|
|
870
|
+
* with `multiple`) the browser draws a list box, and the Select drops its chevron
|
|
871
|
+
* and the end padding reserved for it.
|
|
862
872
|
*/
|
|
863
873
|
size?: "sm" | "md" | number;
|
|
864
874
|
/** Classes for the `<select>` itself. `className` styles the WRAPPER — the box
|
|
@@ -870,7 +880,7 @@ export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement
|
|
|
870
880
|
// The compact select: the field's colours, a toolbar button's height. `py-0` and a
|
|
871
881
|
// fixed height rather than a smaller padding, so the box is 28px whatever line
|
|
872
882
|
// height the caller's type brings with it.
|
|
873
|
-
const SELECT_SM = "h-7 py-0
|
|
883
|
+
const SELECT_SM = "h-7 py-0 ps-2 pe-7 text-xs";
|
|
874
884
|
|
|
875
885
|
export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(
|
|
876
886
|
{ className, label, id, children, invalid, error, hint, size, selectClassName, ...rest },
|
|
@@ -879,6 +889,12 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
|
|
|
879
889
|
const generated = useId();
|
|
880
890
|
// Only a number reaches the DOM; the two words are this component's own.
|
|
881
891
|
const nativeSize = typeof size === "number" ? size : undefined;
|
|
892
|
+
// A numeric size above 1 (or `multiple`) makes the browser draw a LIST BOX — all
|
|
893
|
+
// the rows in the field, no menu to drop. The dropdown dress did not fit it: the
|
|
894
|
+
// chevron was centred on a box several rows tall, sat on top of the rows' text
|
|
895
|
+
// at the end edge, and `pe-9` clipped every option nine units early. A list box
|
|
896
|
+
// gets the plain field instead — its own native scroll, no chevron, even padding.
|
|
897
|
+
const listBox = (nativeSize !== undefined && nativeSize > 1) || Boolean(rest.multiple);
|
|
882
898
|
const small = size === "sm" && label === undefined;
|
|
883
899
|
const fieldId = id ?? generated;
|
|
884
900
|
const { isInvalid, describedBy, errorEl } = useFieldError(
|
|
@@ -888,13 +904,16 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
|
|
|
888
904
|
rest["aria-invalid"],
|
|
889
905
|
);
|
|
890
906
|
// Custom chevron (native arrow hidden via appearance-none) so it sits a touch
|
|
891
|
-
// in from the
|
|
907
|
+
// in from the end border and matches both themes — feedback #223. A DISABLED
|
|
892
908
|
// select has no menu to drop, so it drops the chevron too: the arrow is the one
|
|
893
909
|
// thing on the control that promises a choice (Keksdose dev#474, where the
|
|
894
910
|
// account type became read-only and still looked exactly like a picker).
|
|
895
|
-
const chevron =
|
|
896
|
-
|
|
897
|
-
|
|
911
|
+
const chevron =
|
|
912
|
+
rest.disabled || listBox ? null : (
|
|
913
|
+
<FieldChevron className={small ? "end-1.5 size-3.5" : undefined} />
|
|
914
|
+
);
|
|
915
|
+
// `pe-9` is the room the chevron takes; a list box has none to make room for.
|
|
916
|
+
const dress = listBox ? "overflow-y-auto" : "appearance-none pe-9";
|
|
898
917
|
if (label === undefined) {
|
|
899
918
|
return (
|
|
900
919
|
<FieldGroup errorEl={errorEl}>
|
|
@@ -919,7 +938,7 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
|
|
|
919
938
|
aria-describedby={describedBy}
|
|
920
939
|
className={cn(
|
|
921
940
|
FIELD_BASE,
|
|
922
|
-
|
|
941
|
+
dress,
|
|
923
942
|
small && SELECT_SM,
|
|
924
943
|
selectClassName,
|
|
925
944
|
isInvalid && FIELD_INVALID,
|
|
@@ -944,8 +963,11 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
|
|
|
944
963
|
aria-describedby={describedBy}
|
|
945
964
|
className={cn(
|
|
946
965
|
FIELD_BASE,
|
|
947
|
-
|
|
948
|
-
|
|
966
|
+
// The floated label takes the top strip of a list box too, so its first
|
|
967
|
+
// row starts under the label rather than behind it.
|
|
968
|
+
listBox ? "pt-5 pb-1" : FIELD_FLOATING_PAD,
|
|
969
|
+
"peer",
|
|
970
|
+
dress,
|
|
949
971
|
selectClassName,
|
|
950
972
|
isInvalid && FIELD_INVALID,
|
|
951
973
|
)}
|
|
@@ -1320,7 +1342,8 @@ export interface TabsProps<T extends string>
|
|
|
1320
1342
|
*/
|
|
1321
1343
|
panelId?: string;
|
|
1322
1344
|
/**
|
|
1323
|
-
* Makes the tabs removable: an × on the OPEN tab, and Delete on a focused tab
|
|
1345
|
+
* Makes the tabs removable: an × on the OPEN tab, and Delete on a focused tab
|
|
1346
|
+
* (only on the open one with `removeOn="active"`).
|
|
1324
1347
|
*
|
|
1325
1348
|
* The × is on the open tab only — a row of tabs each carrying one is a row of
|
|
1326
1349
|
* things to hit by accident, and the tab you can remove should be the one you
|
|
@@ -1328,7 +1351,7 @@ export interface TabsProps<T extends string>
|
|
|
1328
1351
|
* tab never widens it and never pushes the rest of the strip along (lenkbank
|
|
1329
1352
|
* feedback #72). It is named "Remove <tab>" and is not a tab stop of its own: the
|
|
1330
1353
|
* strip stays one stop, and the keyboard path is Delete (announced through
|
|
1331
|
-
* `aria-keyshortcuts`), which removes whichever tab has focus.
|
|
1354
|
+
* `aria-keyshortcuts`), which removes whichever tab has focus (see `removeOn`).
|
|
1332
1355
|
*
|
|
1333
1356
|
* The strip only ASKS: it calls this, and the tab goes when the caller drops it
|
|
1334
1357
|
* from `tabs` — after a confirmation, a request, whatever it needs. When it does,
|
|
@@ -1336,6 +1359,20 @@ export interface TabsProps<T extends string>
|
|
|
1336
1359
|
* removed one was open, else to its neighbour, instead of falling to the page.
|
|
1337
1360
|
*/
|
|
1338
1361
|
onRemove?: (id: T) => void;
|
|
1362
|
+
/**
|
|
1363
|
+
* Which tabs the Delete key removes.
|
|
1364
|
+
*
|
|
1365
|
+
* - `"every"` (default): any focused removable tab — the ARIA pattern's reading.
|
|
1366
|
+
* - `"active"`: only the OPEN tab, the same one that wears the ×. For a strip whose
|
|
1367
|
+
* remove is immediate and unconfirmed (lenkbank's sheet tabs delete a measurement
|
|
1368
|
+
* sheet at once), so arrowing across the strip and pressing Delete cannot take
|
|
1369
|
+
* out a sheet the user is not looking at. Delete on a closed tab does nothing,
|
|
1370
|
+
* and only the open tab announces the shortcut (`aria-keyshortcuts`).
|
|
1371
|
+
*
|
|
1372
|
+
* Either way the × is drawn on the open tab only and its room stays reserved on
|
|
1373
|
+
* every removable tab, so the strip never relayouts when the selection moves.
|
|
1374
|
+
*/
|
|
1375
|
+
removeOn?: "every" | "active";
|
|
1339
1376
|
/** Renders an "add" button after the last tab, outside the tablist — it is an
|
|
1340
1377
|
* action, not a tab, and a tablist may own only tabs. */
|
|
1341
1378
|
onAdd?: () => void;
|
|
@@ -1401,6 +1438,7 @@ export function Tabs<T extends string>({
|
|
|
1401
1438
|
label,
|
|
1402
1439
|
panelId,
|
|
1403
1440
|
onRemove,
|
|
1441
|
+
removeOn = "every",
|
|
1404
1442
|
onAdd,
|
|
1405
1443
|
addLabel,
|
|
1406
1444
|
labels,
|
|
@@ -1411,6 +1449,9 @@ export function Tabs<T extends string>({
|
|
|
1411
1449
|
const text = useKitLabels("tabs", DEFAULT_TABS_LABELS, labels);
|
|
1412
1450
|
const stripRef = useRef<HTMLDivElement>(null);
|
|
1413
1451
|
const isRemovable = (tab: TabItem<T>) => onRemove !== undefined && tab.removable !== false;
|
|
1452
|
+
// Whether Delete reaches this tab: every removable tab, or only the open one.
|
|
1453
|
+
const deletesOnKey = (tab: TabItem<T>) =>
|
|
1454
|
+
isRemovable(tab) && (removeOn === "every" || tab.id === active);
|
|
1414
1455
|
const nameOf = (tab: TabItem<T>) =>
|
|
1415
1456
|
tab.name ?? (typeof tab.label === "string" ? tab.label : tab.id);
|
|
1416
1457
|
|
|
@@ -1463,13 +1504,16 @@ export function Tabs<T extends string>({
|
|
|
1463
1504
|
const onTabKeyDown = (e: KeyboardEvent<HTMLElement>, tab: TabItem<T>) => {
|
|
1464
1505
|
// Delete removes the FOCUSED tab (the ARIA pattern's optional key for deletable
|
|
1465
1506
|
// tabs) — the keyboard's way to the × that is not a tab stop. `busy` swallows it
|
|
1466
|
-
// rather than letting it fall through to the page.
|
|
1467
|
-
|
|
1507
|
+
// rather than letting it fall through to the page. With `removeOn="active"` only
|
|
1508
|
+
// the open tab answers; a closed one lets the key pass untouched.
|
|
1509
|
+
if (e.key === "Delete" && deletesOnKey(tab)) {
|
|
1468
1510
|
e.preventDefault();
|
|
1469
1511
|
if (!busy) requestRemove(tab.id);
|
|
1470
1512
|
return;
|
|
1471
1513
|
}
|
|
1472
|
-
|
|
1514
|
+
// Along the reading direction: the strip is a flex row, so in RTL the NEXT tab sits
|
|
1515
|
+
// to the left, and ArrowLeft has to reach it.
|
|
1516
|
+
const step = horizontalStep(e.key, e.currentTarget);
|
|
1473
1517
|
if (step === 0 && e.key !== "Home" && e.key !== "End") return;
|
|
1474
1518
|
const strip = e.currentTarget.closest('[role="tablist"]');
|
|
1475
1519
|
if (!strip) return;
|
|
@@ -1526,7 +1570,7 @@ export function Tabs<T extends string>({
|
|
|
1526
1570
|
// Only the open tab: see `panelId`.
|
|
1527
1571
|
"aria-controls": isActive ? panelId : undefined,
|
|
1528
1572
|
id: isActive && panelId ? `${panelId}-tab` : undefined,
|
|
1529
|
-
"aria-keyshortcuts":
|
|
1573
|
+
"aria-keyshortcuts": deletesOnKey(tab) ? "Delete" : undefined,
|
|
1530
1574
|
onKeyDown: (e: KeyboardEvent<HTMLElement>) => onTabKeyDown(e, tab),
|
|
1531
1575
|
className: cn(
|
|
1532
1576
|
wrap ? TAB_WRAP_CLASSES : TAB_CLASSES,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useLayoutEffect, useState } from "react";
|
|
2
|
+
import type { RefObject } from "react";
|
|
3
|
+
import { dirOf, type Direction } from "../lib/direction";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The reading direction at a portalled panel's anchor — internal, not exported from
|
|
7
|
+
* the barrel. The field pickers' panels are portalled to `<body>`, out of the subtree
|
|
8
|
+
* their `dir` came from, so they carry it across on their own root.
|
|
9
|
+
*
|
|
10
|
+
* Read in a layout effect while `open`, rather than off the ref during render: the
|
|
11
|
+
* anchored rect these panels also wait for is measured the same way, so the panel's
|
|
12
|
+
* first painted frame already has both, and a render never reads a ref.
|
|
13
|
+
*/
|
|
14
|
+
export function useAnchorDir(ref: RefObject<Element | null>, open: boolean): Direction {
|
|
15
|
+
const [dir, setDir] = useState<Direction>("ltr");
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
if (open) setDir(dirOf(ref.current));
|
|
18
|
+
}, [open, ref]);
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
@@ -11,6 +11,11 @@ import { useEffect, useRef, useState } from "react";
|
|
|
11
11
|
* is why nothing resets it: narrowing a filter cannot leave the limit pointing past the
|
|
12
12
|
* end, and widening one does not have to re-reveal what the user already scrolled past.
|
|
13
13
|
*
|
|
14
|
+
* `paginated={false}` opts out entirely: a table that is short by construction (an
|
|
15
|
+
* invoice's line items) renders every row on a phone too. It used to be chunked here
|
|
16
|
+
* regardless, so a twelve-line invoice showed its first ten lines and a "Loading…"
|
|
17
|
+
* sentinel for rows that were already in memory.
|
|
18
|
+
*
|
|
14
19
|
* Lifted out of `DataTable` with the rest of its state; the sentinel ref belongs to the
|
|
15
20
|
* caller's `<li>`.
|
|
16
21
|
*/
|
|
@@ -19,6 +24,7 @@ export function useMobileReveal<T>({
|
|
|
19
24
|
slice,
|
|
20
25
|
isServer,
|
|
21
26
|
isMdUp,
|
|
27
|
+
unpaged = false,
|
|
22
28
|
defaultPageSize,
|
|
23
29
|
}: {
|
|
24
30
|
/** The whole sorted list — what there is to reveal. */
|
|
@@ -27,11 +33,13 @@ export function useMobileReveal<T>({
|
|
|
27
33
|
slice: T[];
|
|
28
34
|
isServer: boolean;
|
|
29
35
|
isMdUp: boolean;
|
|
36
|
+
/** `paginated={false}` on a client-side table: show every row, reveal nothing. */
|
|
37
|
+
unpaged?: boolean;
|
|
30
38
|
defaultPageSize: number;
|
|
31
39
|
}) {
|
|
32
40
|
const [mobileLimit, setMobileLimit] = useState(defaultPageSize);
|
|
33
|
-
const mobileSlice = isServer ? slice : rows.slice(0, mobileLimit);
|
|
34
|
-
const canRevealMoreMobile = !isServer && !isMdUp && mobileLimit < rows.length;
|
|
41
|
+
const mobileSlice = isServer ? slice : unpaged ? rows : rows.slice(0, mobileLimit);
|
|
42
|
+
const canRevealMoreMobile = !isServer && !unpaged && !isMdUp && mobileLimit < rows.length;
|
|
35
43
|
const loadMoreRef = useRef<HTMLLIElement | null>(null);
|
|
36
44
|
|
|
37
45
|
useEffect(() => {
|