@eifi1/ui-kit 0.12.0 → 0.13.1
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 +10 -10
- package/dist/components/amount-input.d.ts +23 -1
- package/dist/components/amount-input.js +78 -13
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +10 -2
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +1 -1
- package/dist/components/calculator.d.ts +1 -1
- package/dist/components/chart.js +14 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.js +20 -15
- package/dist/components/chip.js.map +1 -1
- package/dist/components/combobox-core.d.ts +3 -1
- package/dist/components/combobox-core.js +16 -5
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +1 -1
- package/dist/components/data-table-filter-popover.js +8 -1
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table.js +28 -20
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/entity-combobox.js +22 -17
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +202 -13
- package/dist/components/error-boundary.js +225 -21
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/field-sync.js +90 -83
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/file-button.d.ts +1 -1
- package/dist/components/file-dropzone.d.ts +1 -1
- package/dist/components/form-actions.d.ts +1 -1
- package/dist/components/full-bleed-dialog.js +4 -0
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/hover-menu.js +64 -56
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/list.d.ts +14 -2
- package/dist/components/list.js +3 -1
- package/dist/components/list.js.map +1 -1
- package/dist/components/modal.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +23 -17
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +1 -1
- package/dist/components/number-input.d.ts +1 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +10 -2
- package/dist/components/numpad-sheet.js +6 -4
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-header.d.ts +35 -6
- package/dist/components/page-header.js +7 -1
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/picker-sheet.js +3 -0
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.js +3 -0
- package/dist/components/popover.js.map +1 -1
- package/dist/components/settings-fields.d.ts +1 -1
- package/dist/components/signed-amount.d.ts +2 -1
- package/dist/components/signed-amount.js +1 -1
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/stat-tile.js +1 -1
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swipeable-row.js +4 -3
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/text-link.d.ts +73 -11
- package/dist/components/text-link.js +45 -10
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/text.d.ts +26 -2
- package/dist/components/text.js +16 -2
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +1 -1
- package/dist/components/toggle-group.d.ts +22 -0
- package/dist/components/toggle-group.js +8 -8
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +17 -0
- package/dist/components/toggle-legend.js +6 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +51 -1
- package/dist/components/tooltip.js +59 -35
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +1 -1
- package/dist/components/ui.js +166 -28
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.js +4 -6
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.js +5 -3
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js +86 -75
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +43 -3
- package/dist/feedback/feedback-thread.js +53 -26
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-file-drop.d.ts +1 -1
- package/dist/i18n/defaults.d.ts +1 -1
- package/dist/i18n/kit-labels.d.ts +1 -1
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +1 -1
- package/dist/i18n/locales/de-CH.d.ts +1 -1
- package/dist/i18n/locales/de-informal.d.ts +1 -1
- package/dist/i18n/locales/de.d.ts +1 -1
- package/dist/i18n/locales/de.js +9 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +1 -1
- package/dist/i18n/locales/es.js +9 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +1 -1
- package/dist/i18n/locales/fr.js +9 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +1 -1
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +1 -1
- package/dist/i18n/locales/it.js +9 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +1 -1
- package/dist/i18n/locales/zh.js +9 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/{kit-labels-v3biUF1L.d.ts → kit-labels-Mz8zZHIF.d.ts} +102 -3
- package/dist/lib/format.d.ts +23 -5
- package/dist/lib/format.js +4 -2
- package/dist/lib/format.js.map +1 -1
- package/dist/rhf/fields.d.ts +59 -15
- package/dist/rhf/fields.js +41 -11
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +1 -1
- package/dist/rhf.d.ts +2 -2
- package/dist/rhf.js.map +1 -1
- package/dist/search/command-palette.js +20 -14
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +1 -1
- package/dist/shell/app-shell.js +62 -54
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/top-bar-brand.d.ts +1 -1
- package/dist/shell/topbar-action-menu.d.ts +9 -3
- package/dist/shell/topbar-action-menu.js +2 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +1 -1
- package/dist/tour/tour.js +10 -4
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +1 -1
- package/dist/wizard/use-wizard.js +4 -2
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +11 -1
- package/dist/wizard/wizard-step.js +24 -6
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +1 -1
- package/package.json +6 -6
- package/src/components/amount-input.tsx +165 -12
- package/src/components/bulk-action-bar.tsx +16 -3
- package/src/components/chart.tsx +19 -8
- package/src/components/chip.tsx +6 -0
- package/src/components/combobox-core.tsx +28 -9
- package/src/components/combobox.tsx +2 -0
- package/src/components/data-table-filter-popover.tsx +13 -5
- package/src/components/data-table.tsx +6 -0
- package/src/components/entity-combobox.tsx +7 -4
- package/src/components/error-boundary.tsx +470 -30
- package/src/components/field-sync.tsx +48 -40
- package/src/components/full-bleed-dialog.tsx +4 -0
- package/src/components/hover-menu.tsx +26 -17
- package/src/components/list.tsx +17 -3
- package/src/components/modal.tsx +4 -0
- package/src/components/multi-entity-combobox.tsx +8 -4
- package/src/components/number-input.tsx +1 -0
- package/src/components/numpad-sheet.tsx +17 -3
- package/src/components/page-header.tsx +42 -5
- package/src/components/picker-sheet.tsx +3 -0
- package/src/components/popover.tsx +3 -0
- package/src/components/signed-amount.tsx +12 -3
- package/src/components/stat-tile.tsx +3 -1
- package/src/components/swipeable-row.tsx +7 -7
- package/src/components/text-link.tsx +126 -18
- package/src/components/text.tsx +47 -2
- package/src/components/toggle-group.tsx +35 -10
- package/src/components/toggle-legend.tsx +22 -1
- package/src/components/tooltip.tsx +79 -8
- package/src/components/ui.tsx +307 -39
- package/src/feedback/feedback-attachment.tsx +10 -7
- package/src/feedback/feedback-dialog.tsx +7 -4
- package/src/feedback/feedback-inbox.tsx +7 -0
- package/src/feedback/feedback-thread.tsx +109 -33
- package/src/i18n/kit-labels.tsx +16 -0
- package/src/i18n/locales/de.ts +8 -0
- package/src/i18n/locales/es.ts +8 -0
- package/src/i18n/locales/fr.ts +8 -0
- package/src/i18n/locales/hu.ts +8 -0
- package/src/i18n/locales/it.ts +8 -0
- package/src/i18n/locales/zh.ts +8 -0
- package/src/lib/format.ts +32 -7
- package/src/rhf/fields.tsx +99 -16
- package/src/rhf.ts +1 -1
- package/src/search/command-palette.tsx +29 -19
- package/src/shell/app-shell.tsx +6 -1
- package/src/shell/topbar-action-menu.tsx +11 -4
- package/src/tour/tour.tsx +16 -5
- package/src/wizard/use-wizard.ts +8 -4
- package/src/wizard/wizard-step.tsx +38 -6
|
@@ -150,6 +150,108 @@ interface AmountInputProps {
|
|
|
150
150
|
* it left a 36px figure 16px to live in.
|
|
151
151
|
*/
|
|
152
152
|
align?: "start" | "center";
|
|
153
|
+
/**
|
|
154
|
+
* Decimals the committed amount is rounded to (kastlan 0.12: "12.345" and a
|
|
155
|
+
* calculator's "100/3" reached a `Numeric(10,2)` column unrounded and failed with a
|
|
156
|
+
* 422). Default: the currency's minor unit, from `Intl` (CHF and EUR 2, JPY 0). With
|
|
157
|
+
* no `currency`, nothing is rounded unless this is set. Rounding is half away from
|
|
158
|
+
* zero (12.345 → 12.35, -12.345 → -12.35) and happens when the figure settles: on
|
|
159
|
+
* blur, on Enter, and on every result the calculator writes back. Keystrokes are left
|
|
160
|
+
* alone, so "12.3" can still become "12.34".
|
|
161
|
+
*
|
|
162
|
+
* ⚠️ **A unit price or a rate needs `digits`.** The default is right for an amount of
|
|
163
|
+
* money and silently wrong for a price per unit: a fund's NAV of 123.4567 or fuel at
|
|
164
|
+
* 1.789 €/l is stored as 123.46 / 1.79, and no test fails (keksdose G2, a
|
|
165
|
+
* `Numeric(18,6)` holding price and a `Numeric(18,4)` unit price). Pass the column's
|
|
166
|
+
* scale: `digits={4}`, `digits={6}`.
|
|
167
|
+
*/
|
|
168
|
+
digits?: number;
|
|
169
|
+
/** The smallest amount the field settles on. A lower figure is raised to it when it
|
|
170
|
+
* settles, like `digits`. Compared with the signed figure. */
|
|
171
|
+
min?: number;
|
|
172
|
+
/** The largest amount the field settles on (a deposit release capped at the
|
|
173
|
+
* balance). A higher figure is lowered to it when it settles. */
|
|
174
|
+
max?: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The currency's minor unit (CHF 2, JPY 0), or `undefined` for no currency or a code
|
|
178
|
+
* `Intl` does not accept. */
|
|
179
|
+
function currencyDigits(code: string | undefined): number | undefined {
|
|
180
|
+
if (!code) return undefined;
|
|
181
|
+
try {
|
|
182
|
+
return new Intl.NumberFormat("en", { style: "currency", currency: code }).resolvedOptions()
|
|
183
|
+
.maximumFractionDigits;
|
|
184
|
+
} catch {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A settled figure, rounded to `digits` (half away from zero) and clamped to
|
|
191
|
+
* `min`/`max`. Only a plain number is touched. A half-typed draft or an expression
|
|
192
|
+
* comes back as it was. Rounding works in decimal exponent notation, because
|
|
193
|
+
* `Math.round(1.005 * 100)` is 100 (1.005 is 1.00499… in binary).
|
|
194
|
+
*/
|
|
195
|
+
function settleAmount(text: string, digits: number | undefined, min?: number, max?: number): string {
|
|
196
|
+
const t = text.trim();
|
|
197
|
+
if (!/^-?\d*\.?\d*$/.test(t) || !/\d/.test(t)) return text;
|
|
198
|
+
let n = Number(t);
|
|
199
|
+
if (!Number.isFinite(n)) return text;
|
|
200
|
+
if (digits !== undefined) {
|
|
201
|
+
const magnitude = Number(`${Math.round(Number(`${Math.abs(n)}e${digits}`))}e-${digits}`);
|
|
202
|
+
n = n < 0 ? -magnitude : magnitude;
|
|
203
|
+
}
|
|
204
|
+
if (min !== undefined && n < min) n = min;
|
|
205
|
+
if (max !== undefined && n > max) n = max;
|
|
206
|
+
const out = formatResult(n);
|
|
207
|
+
// Unchanged figures keep their own spelling ("12.50" stays "12.50").
|
|
208
|
+
return Number(out) === Number(t) ? text : out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The locale's decimal mark, as far as this field can type it: "," or "." (kastlan 40).
|
|
213
|
+
* The same answer `NumberField` gives, for the same reason: a mark outside those two
|
|
214
|
+
* (Arabic's "٫") is not one {@link sanitizeLive} lets through, so those locales keep
|
|
215
|
+
* the dot rather than get a field that eats the key it shows.
|
|
216
|
+
*/
|
|
217
|
+
function decimalMark(locale: string | undefined): "," | "." {
|
|
218
|
+
try {
|
|
219
|
+
const part = new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === "decimal");
|
|
220
|
+
return part?.value === "," ? "," : ".";
|
|
221
|
+
} catch {
|
|
222
|
+
// An invalid tag throws a RangeError; a typo in a locale must not take the form down.
|
|
223
|
+
return ".";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Which typed mark is the decimal one (keksdose G1). {@link sanitizeLive} folds every
|
|
229
|
+
* "," into ".", so on its own it read the German "1.234,56" as 1.23456 — and the
|
|
230
|
+
* settle then made that 1.23, final. This decides per operand, from the typist's own
|
|
231
|
+
* text, before that fold:
|
|
232
|
+
*
|
|
233
|
+
* - both marks present: the one that is not the locale's is grouping, and goes;
|
|
234
|
+
* - in a "," locale, only "." present: grouping when it cannot be a decimal as a
|
|
235
|
+
* German reader writes it — twice ("1.234.567"), or in thousands shape ("1.234",
|
|
236
|
+
* "12.500") — otherwise a decimal ("1.5", typed by a dot-decimal habit);
|
|
237
|
+
* - in a "." locale, only "," present: grouping when it appears twice ("1,234,567");
|
|
238
|
+
* once it stays a decimal, because Swiss and German typists write "1,5" in a
|
|
239
|
+
* de-CH form (kastlan 40) and a unit price "1,789" is a decimal.
|
|
240
|
+
*
|
|
241
|
+
* Only a keystroke's text comes through here. A calculator result is dot-decimal
|
|
242
|
+
* already, and "1.234" from `100/81.03…` is never a thousand.
|
|
243
|
+
*/
|
|
244
|
+
function normalizeTypedMarks(raw: string, mark: "," | "."): string {
|
|
245
|
+
const other = mark === "," ? "." : ",";
|
|
246
|
+
return raw.replace(/[0-9.,]+/g, (operand) => {
|
|
247
|
+
const hasMark = operand.includes(mark);
|
|
248
|
+
const count = operand.split(other).length - 1;
|
|
249
|
+
if (count === 0) return operand;
|
|
250
|
+
const grouping = hasMark
|
|
251
|
+
? true
|
|
252
|
+
: count > 1 || (mark === "," && /^\d{1,3}(\.\d{3})+$/.test(operand));
|
|
253
|
+
return grouping ? operand.split(other).join("") : operand;
|
|
254
|
+
});
|
|
153
255
|
}
|
|
154
256
|
|
|
155
257
|
// The consuming app's ONE money palette (`--money-expense` / `--money-income`),
|
|
@@ -186,13 +288,14 @@ const DISPLAY_INPUT_CLASS = cn(
|
|
|
186
288
|
* sum that comes out positive still takes the chip with it; "-20+50" → "7" is not,
|
|
187
289
|
* and "-20+" evaluates to nothing at all.
|
|
188
290
|
*/
|
|
189
|
-
function isResultOf(previous: string, text: string): boolean {
|
|
291
|
+
function isResultOf(previous: string, text: string, settle: (text: string) => string): boolean {
|
|
190
292
|
const n = evaluateExpression(previous);
|
|
191
|
-
|
|
293
|
+
// Compared settled, since the field rounds what the calculator hands it.
|
|
294
|
+
return n !== null && settle(formatResult(n)) === text.trim();
|
|
192
295
|
}
|
|
193
296
|
|
|
194
297
|
export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
195
|
-
({ value, onChange, currency, onCurrencyChange, placeholder, label, disabled, invalid: invalidProp, className, id, ariaLabel, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, autoFocus, tone = "neutral", negative = false, onNegativeChange, variant = "field", align = "start", labels, currencyNames }, ref) => {
|
|
298
|
+
({ value, onChange, currency, onCurrencyChange, placeholder, label, disabled, invalid: invalidProp, className, id, ariaLabel, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, autoFocus, tone = "neutral", negative = false, onNegativeChange, variant = "field", align = "start", labels, currencyNames, digits: digitsProp, min, max }, ref) => {
|
|
196
299
|
const generatedId = useId();
|
|
197
300
|
const fieldId = id ?? generatedId;
|
|
198
301
|
const invalid = Boolean(invalidProp) || ariaInvalid === true || ariaInvalid === "true";
|
|
@@ -230,6 +333,36 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
230
333
|
// minus and has to survive keystroke-for-keystroke.
|
|
231
334
|
const signOwned = onNegativeChange !== undefined;
|
|
232
335
|
const shown = signOwned && negative && isBareAmount(value) ? `-${value}` : value;
|
|
336
|
+
// What the user SEES is `shown` with the locale's decimal mark (kastlan 40: a
|
|
337
|
+
// fr-CH user typed "1,5" and watched it turn into "1.5" under their fingers). Display only — `value`, `onChange`, the sign split and the settle all
|
|
338
|
+
// stay dot-decimal, because that string is the contract: RhfMoneyField's
|
|
339
|
+
// `parseAmount`, every app's `Number(value)` and the calculator's evaluator read
|
|
340
|
+
// it. The way back is free: {@link sanitizeLive} already folds a typed "," into
|
|
341
|
+
// ".", so a comma typed, pasted or tapped on the numpad lands as a dot.
|
|
342
|
+
//
|
|
343
|
+
// One character for one character, and no digit grouping: the caret stays where
|
|
344
|
+
// the typist put it, and "1.000" cannot be read back as a thousand in one locale
|
|
345
|
+
// and as one in the other. de-CH and it-CH are "." — CLDR says so, and this
|
|
346
|
+
// follows `Intl` rather than a guess about a country.
|
|
347
|
+
const locale = useKitLocale();
|
|
348
|
+
const mark = decimalMark(locale);
|
|
349
|
+
// While the typist is typing, the field shows THEIR text ("1.234,5" stays exactly
|
|
350
|
+
// that), not the value re-spelt: re-spelling turned a grouping "." into a "," under
|
|
351
|
+
// their fingers, after which no rule could tell the two apart (keksdose G1). The
|
|
352
|
+
// draft is shown only while it still spells what the field holds; a value set from
|
|
353
|
+
// outside, a calculator result or a commit falls back to the derived spelling.
|
|
354
|
+
const [draft, setDraft] = useState<string | null>(null);
|
|
355
|
+
const derived = mark === "," ? shown.replace(/\./g, ",") : shown;
|
|
356
|
+
const draftAs = draft === null ? "" : sanitizeLive(normalizeTypedMarks(draft, mark));
|
|
357
|
+
const display =
|
|
358
|
+
draft === null
|
|
359
|
+
? derived
|
|
360
|
+
: draftAs === shown
|
|
361
|
+
? draft
|
|
362
|
+
: // The caller owns the sign: the typed "12" is shown as the "-12" it is.
|
|
363
|
+
signOwned && negative && `-${draftAs}` === shown
|
|
364
|
+
? `-${draft}`
|
|
365
|
+
: derived;
|
|
233
366
|
// Every route into the field — typing, the numpad sheet, the desktop
|
|
234
367
|
// calculator, the blur/Enter commit — funnels through here, so the split is
|
|
235
368
|
// written once and the four entry paths cannot drift.
|
|
@@ -239,6 +372,8 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
239
372
|
// edit the field's own text that is simply what the field was showing; the
|
|
240
373
|
// desktop calculator keeps its expression in a popover the field never sees,
|
|
241
374
|
// so it passes it in (see `CalculatorButton`'s `onChange`).
|
|
375
|
+
const digits = digitsProp ?? currencyDigits(currency);
|
|
376
|
+
const settle = (text: string) => settleAmount(text, digits, min, max);
|
|
242
377
|
const handleText = (raw: string, previous: string = shown) => {
|
|
243
378
|
const text = sanitizeLive(raw);
|
|
244
379
|
if (!signOwned) {
|
|
@@ -249,7 +384,7 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
249
384
|
if (sign) {
|
|
250
385
|
// SET, not flip: re-reporting the same sign on every keystroke is a no-op.
|
|
251
386
|
onNegativeChange(sign === "-");
|
|
252
|
-
} else if (looksLikeExpression(previous) && isBareAmount(text) && isResultOf(previous, text)) {
|
|
387
|
+
} else if (looksLikeExpression(previous) && isBareAmount(text) && isResultOf(previous, text, settle)) {
|
|
253
388
|
// A calculation that just RESOLVED states its sign in both directions. The
|
|
254
389
|
// figure it ran on is the SIGNED one the user could see — "-12+30" is +18 —
|
|
255
390
|
// so a positive result has to move the chip too, or the field would answer
|
|
@@ -262,7 +397,14 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
262
397
|
}
|
|
263
398
|
onChange(sign ? rest : text);
|
|
264
399
|
};
|
|
265
|
-
const commit = () =>
|
|
400
|
+
const commit = () => {
|
|
401
|
+
setDraft(null);
|
|
402
|
+
handleText(settle(commitExpression(shown)));
|
|
403
|
+
};
|
|
404
|
+
const typed = (raw: string) => {
|
|
405
|
+
setDraft(raw);
|
|
406
|
+
handleText(normalizeTypedMarks(raw, mark));
|
|
407
|
+
};
|
|
266
408
|
const { open, setOpen, wrapperRef, panelRef, query, setQuery, inputRef } = useDropdownSearch();
|
|
267
409
|
// The chip the currency list hangs off (Keksdose dev#548). It is portalled now, so
|
|
268
410
|
// the panel needs a real trigger rect rather than a relative parent — see
|
|
@@ -281,7 +423,6 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
281
423
|
}, [labels?.currency, labels?.currencySearch]);
|
|
282
424
|
const currencyText = useKitLabels("currency", DEFAULT_CURRENCY_LABELS, currencyOverrides);
|
|
283
425
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
284
|
-
const locale = useKitLocale();
|
|
285
426
|
|
|
286
427
|
const selected = getCurrency(currency);
|
|
287
428
|
const filtered = useMemo(() => {
|
|
@@ -321,8 +462,8 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
321
462
|
// needs: a zero to aim at, at full size. The caller supplies it because
|
|
322
463
|
// only the app knows the locale's decimal separator.
|
|
323
464
|
placeholder={asDisplay ? (placeholder ?? "0") : label !== undefined ? " " : placeholder}
|
|
324
|
-
value={
|
|
325
|
-
onChange={(e) =>
|
|
465
|
+
value={display}
|
|
466
|
+
onChange={(e) => typed(e.target.value)}
|
|
326
467
|
onFocus={() => setFocused(true)}
|
|
327
468
|
onBlur={() => {
|
|
328
469
|
commit();
|
|
@@ -387,8 +528,13 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
387
528
|
>
|
|
388
529
|
{showCalc && (
|
|
389
530
|
<CalculatorButton
|
|
390
|
-
|
|
391
|
-
|
|
531
|
+
// The mark the field shows, as NumberInput hands its calculator; the
|
|
532
|
+
// evaluator reads either, and its result comes back dot-decimal.
|
|
533
|
+
value={display}
|
|
534
|
+
onChange={(result, expression) => {
|
|
535
|
+
setDraft(null);
|
|
536
|
+
handleText(settle(result), expression);
|
|
537
|
+
}}
|
|
392
538
|
className="px-1.5"
|
|
393
539
|
ariaLabel={labels?.calculatorTrigger}
|
|
394
540
|
labels={labels?.calculator}
|
|
@@ -470,11 +616,18 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
470
616
|
</div>
|
|
471
617
|
{showNumpad && (
|
|
472
618
|
<NumberPadSheet
|
|
473
|
-
|
|
474
|
-
|
|
619
|
+
// Localised like the field it mirrors. Keys are typing, read by the
|
|
620
|
+
// locale's marks like the keyboard's; "=" is a result, dot-decimal.
|
|
621
|
+
value={display}
|
|
622
|
+
onChange={typed}
|
|
623
|
+
onResult={(result) => {
|
|
624
|
+
setDraft(null);
|
|
625
|
+
handleText(result);
|
|
626
|
+
}}
|
|
475
627
|
onDone={() => innerRef.current?.blur()}
|
|
476
628
|
label={label}
|
|
477
629
|
labels={labels?.pad}
|
|
630
|
+
decimalMark={mark}
|
|
478
631
|
/>
|
|
479
632
|
)}
|
|
480
633
|
</div>
|
|
@@ -99,8 +99,16 @@ export interface BulkActionBarProps extends Omit<ComponentPropsWithoutRef<"div">
|
|
|
99
99
|
open?: boolean;
|
|
100
100
|
/** Clear the selection / leave selection mode. */
|
|
101
101
|
onClear: () => void;
|
|
102
|
-
/**
|
|
103
|
-
*
|
|
102
|
+
/**
|
|
103
|
+
* The actions: buttons (`IconButton size="lg"` on a phone, `Button` from md up),
|
|
104
|
+
* in the caller's order. They are the toolbar's arrow-key stops, after the clear.
|
|
105
|
+
*
|
|
106
|
+
* LINKS rove too (keksdose F8): a `<Button href>` — "View report", which navigates
|
|
107
|
+
* rather than acts — is an `<a href>`, and it is one more arrow-key stop in its place
|
|
108
|
+
* among the buttons, not a second Tab stop beside the toolbar's one. A disabled
|
|
109
|
+
* `<Button href disabled>` has no `href` and is skipped, as a disabled button is. Any
|
|
110
|
+
* other focusable of your own joins with `data-toolbar-item`.
|
|
111
|
+
*/
|
|
104
112
|
children?: ReactNode;
|
|
105
113
|
/** Default `floating`. See {@link BulkActionBarVariant}; an object picks one per
|
|
106
114
|
* breakpoint, see {@link ResponsiveBulkActionBarVariant}. */
|
|
@@ -135,7 +143,12 @@ const FLOATING_STYLE: CSSProperties = {
|
|
|
135
143
|
|
|
136
144
|
/** What the arrow keys walk: the toolbar's enabled controls. Matched by element, not by
|
|
137
145
|
* `tabindex`, because the roving below sets every one but the current to `-1` — a
|
|
138
|
-
* custom control joins with `data-toolbar-item`.
|
|
146
|
+
* custom control joins with `data-toolbar-item`.
|
|
147
|
+
*
|
|
148
|
+
* `a[href]` is here on purpose (keksdose F8, a "View report" `Button href` in the bar):
|
|
149
|
+
* a link in a toolbar is a toolbar item like any other, and left out it would keep its
|
|
150
|
+
* own Tab stop, so Tab would stop twice in a bar that promises one. `[href]` rather than `a`: the kit's disabled link is an `<a>`
|
|
151
|
+
* WITHOUT one, and is skipped as a disabled button is. */
|
|
139
152
|
const ITEMS =
|
|
140
153
|
"button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[data-toolbar-item]";
|
|
141
154
|
|
package/src/components/chart.tsx
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// The value on show is the *structure*: config-driven colors injected as CSS
|
|
9
9
|
// vars, muted axis/grid styling, and a polished tooltip/legend — the things
|
|
10
10
|
// that make shadcn charts read as "designed" rather than "default Recharts".
|
|
11
|
-
import { createContext, useContext, useId, useRef } from "react";
|
|
11
|
+
import { createContext, useContext, useId, useLayoutEffect, useRef, useState } from "react";
|
|
12
12
|
import type { ComponentProps, CSSProperties, ReactNode, RefObject } from "react";
|
|
13
13
|
import {
|
|
14
14
|
Legend as RechartsLegend,
|
|
@@ -263,7 +263,7 @@ export function ChartTooltipContent({
|
|
|
263
263
|
// `ChartTooltip` keeps null values (so they can print "—"), which also keeps the
|
|
264
264
|
// series a legend toggle switched off with `hide`; those go here instead.
|
|
265
265
|
const items = includeHidden ? payload : payload?.filter((item) => item.hide !== true);
|
|
266
|
-
|
|
266
|
+
const visible = Boolean(active && items?.length);
|
|
267
267
|
// Recharts anchors the tooltip at coordinate.x inside the full (scrolled) chart
|
|
268
268
|
// width; subtract the container's scrollLeft to get its on-screen x, then flip
|
|
269
269
|
// left if the tooltip's own width would run past the visible right edge.
|
|
@@ -272,16 +272,27 @@ export function ChartTooltipContent({
|
|
|
272
272
|
// `ChartContainer`), but an RTL scroller counts `scrollLeft` from its right edge —
|
|
273
273
|
// 0 at the start, negative going left — so there the content's left edge sits
|
|
274
274
|
// `scrollWidth - clientWidth + scrollLeft` px to the left of the visible one.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
275
|
+
//
|
|
276
|
+
// Measured in a layout effect (the DOM is not readable during render), so it commits
|
|
277
|
+
// before paint. `payload` and `label` are deps because they change the tooltip's own
|
|
278
|
+
// width, and recharts hands a fresh `payload` on every tooltip render, so a scroll
|
|
279
|
+
// followed by any re-render is re-measured just as it was when this ran in render.
|
|
280
|
+
const [flip, setFlip] = useState(false);
|
|
281
|
+
const x = coordinate?.x;
|
|
282
|
+
useLayoutEffect(() => {
|
|
283
|
+
const boundary = boundaryRef?.current;
|
|
284
|
+
if (!visible || !boundary || x == null) {
|
|
285
|
+
setFlip(false);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
278
288
|
const offset = isRtl(boundary)
|
|
279
289
|
? boundary.scrollWidth - boundary.clientWidth + boundary.scrollLeft
|
|
280
290
|
: boundary.scrollLeft;
|
|
281
|
-
const visibleX =
|
|
291
|
+
const visibleX = x - offset;
|
|
282
292
|
const tipWidth = tipRef.current?.offsetWidth ?? 160;
|
|
283
|
-
|
|
284
|
-
}
|
|
293
|
+
setFlip(visibleX + tipWidth + 12 > boundary.clientWidth);
|
|
294
|
+
}, [visible, x, boundaryRef, payload, label]);
|
|
295
|
+
if (!visible || !items) return null;
|
|
285
296
|
return (
|
|
286
297
|
<div
|
|
287
298
|
ref={tipRef}
|
package/src/components/chip.tsx
CHANGED
|
@@ -621,6 +621,9 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
|
|
|
621
621
|
}
|
|
622
622
|
|
|
623
623
|
return (
|
|
624
|
+
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- the handlers are
|
|
625
|
+
the roving-focus hooks, live only when the container passes `role` and `tabIndex`
|
|
626
|
+
(ChipInput does); the role is a prop, so the rule cannot see it. */
|
|
624
627
|
<span
|
|
625
628
|
ref={ref as React.Ref<HTMLSpanElement>}
|
|
626
629
|
className={look}
|
|
@@ -893,6 +896,9 @@ export function ChipInput({
|
|
|
893
896
|
{label}
|
|
894
897
|
</label>
|
|
895
898
|
)}
|
|
899
|
+
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- a
|
|
900
|
+
pointer convenience that widens the input's hit area to the whole field; the
|
|
901
|
+
keyboard path is the <input> itself, which is in the tab order. */}
|
|
896
902
|
<div
|
|
897
903
|
// `relative` so the live region below can be `sr-only` without escaping to the
|
|
898
904
|
// initial containing block and inflating the document height.
|
|
@@ -261,7 +261,9 @@ export interface ComboboxCore<V extends string | number> {
|
|
|
261
261
|
rect: AnchorRect | null;
|
|
262
262
|
/** Where the dropdown goes, clamped to the visible viewport (feedback #135). */
|
|
263
263
|
placement: AnchoredPanel;
|
|
264
|
-
|
|
264
|
+
/** Put an option in the label cache, so {@link ComboboxCore.resolve} still knows a
|
|
265
|
+
* value the caller just chose after the result list has moved on. */
|
|
266
|
+
rememberOption: (o: ComboOption<V>) => void;
|
|
265
267
|
resolve: (v: V) => ComboOption<V> | null;
|
|
266
268
|
/** Close and hand focus back to the trigger — the keyboard paths only. See
|
|
267
269
|
* {@link useDropdown} for why a pointer dismissal must not do this. */
|
|
@@ -319,10 +321,23 @@ export function useComboboxCore<V extends string | number>({
|
|
|
319
321
|
useOutsideClick([triggerRef, panelRef], close, open && !isPhone);
|
|
320
322
|
useEscapeKey(closeToTrigger, open);
|
|
321
323
|
|
|
322
|
-
// Reset the query
|
|
324
|
+
// Reset the query each time the panel opens, and the highlighted row whenever the
|
|
325
|
+
// visible set changes (a new query, or the panel opening or closing). Adjusted while
|
|
326
|
+
// rendering against the previous values rather than in an effect, so the render that
|
|
327
|
+
// opens the panel already searches for "" and highlights the first row.
|
|
328
|
+
const [prevOpen, setPrevOpen] = useState(open);
|
|
329
|
+
const [prevQuery, setPrevQuery] = useState(query);
|
|
330
|
+
if (open !== prevOpen || query !== prevQuery) {
|
|
331
|
+
const opened = open && !prevOpen;
|
|
332
|
+
setPrevOpen(open);
|
|
333
|
+
setPrevQuery(opened ? "" : query);
|
|
334
|
+
if (opened) setQuery("");
|
|
335
|
+
setActive(0);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Focus the search box each time the panel opens.
|
|
323
339
|
useEffect(() => {
|
|
324
340
|
if (!open) return;
|
|
325
|
-
setQuery("");
|
|
326
341
|
const id = requestAnimationFrame(() => inputRef.current?.focus());
|
|
327
342
|
return () => cancelAnimationFrame(id);
|
|
328
343
|
}, [open]);
|
|
@@ -346,11 +361,9 @@ export function useComboboxCore<V extends string | number>({
|
|
|
346
361
|
}, [options, results]);
|
|
347
362
|
const resolve = (v: V): ComboOption<V> | null =>
|
|
348
363
|
options?.find((o) => o.value === v) ?? cacheRef.current.get(v) ?? null;
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
setActive(0);
|
|
353
|
-
}, [query, open]);
|
|
364
|
+
const rememberOption = (o: ComboOption<V>) => {
|
|
365
|
+
cacheRef.current.set(o.value, o);
|
|
366
|
+
};
|
|
354
367
|
|
|
355
368
|
return {
|
|
356
369
|
triggerRef,
|
|
@@ -369,7 +382,7 @@ export function useComboboxCore<V extends string | number>({
|
|
|
369
382
|
minChars,
|
|
370
383
|
rect,
|
|
371
384
|
placement,
|
|
372
|
-
|
|
385
|
+
rememberOption,
|
|
373
386
|
resolve,
|
|
374
387
|
closeToTrigger,
|
|
375
388
|
};
|
|
@@ -777,6 +790,9 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
777
790
|
inputRef={inputRef}
|
|
778
791
|
closeLabel={closeLabel}
|
|
779
792
|
>
|
|
793
|
+
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- never focused
|
|
794
|
+
itself: it catches the listbox keys bubbling from the sheet's search <input>,
|
|
795
|
+
which holds focus while the list is up. */}
|
|
780
796
|
<div onKeyDown={onKeyDown} className="relative">
|
|
781
797
|
{list}
|
|
782
798
|
{announcement}
|
|
@@ -796,6 +812,9 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
796
812
|
: { left: rect.left };
|
|
797
813
|
|
|
798
814
|
return createPortal(
|
|
815
|
+
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- never focused
|
|
816
|
+
itself: it catches the listbox keys bubbling from the search <input> inside it,
|
|
817
|
+
which holds focus while the panel is open. */
|
|
799
818
|
<div
|
|
800
819
|
// Spread FIRST: `onKeyDown` below is the listbox keyboard (Up/Down/Home/End/
|
|
801
820
|
// Enter/Tab) and the placement is measured rather than chosen, so neither is a
|
|
@@ -420,6 +420,7 @@ export function Combobox({
|
|
|
420
420
|
aria-required={ariaRequired}
|
|
421
421
|
autoComplete="off"
|
|
422
422
|
disabled={disabled}
|
|
423
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.
|
|
423
424
|
autoFocus={autoFocus}
|
|
424
425
|
onBlur={onBlur}
|
|
425
426
|
onMouseDown={primaryOnly.onMouseDown}
|
|
@@ -913,6 +914,7 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
913
914
|
aria-required={ariaRequired}
|
|
914
915
|
autoComplete="off"
|
|
915
916
|
disabled={disabled}
|
|
917
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.
|
|
916
918
|
autoFocus={autoFocus}
|
|
917
919
|
// `inputMode="none"` rather than readOnly, for the same reason Combobox
|
|
918
920
|
// above gives: the sheet carries the keyboard, and a readOnly field would
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
1
2
|
import { X } from "lucide-react";
|
|
2
3
|
import { cn } from "../lib/cn";
|
|
3
4
|
import { MiniCalendar } from "./mini-calendar";
|
|
@@ -53,6 +54,15 @@ export function FilterPopover<T>({
|
|
|
53
54
|
// No `"en"` fallback any more: that pinned the calendar to English in a table that
|
|
54
55
|
// was never handed a locale, under a provider that had one.
|
|
55
56
|
const locale = useKitLocale(localeProp);
|
|
57
|
+
// What `autoFocus` did, done by hand: a callback ref runs in the same commit phase
|
|
58
|
+
// React's own autofocus does, once when the input mounts (it is stable across
|
|
59
|
+
// renders, so a re-render does not re-take focus).
|
|
60
|
+
const focusOnMount = useCallback(
|
|
61
|
+
(el: HTMLInputElement | null) => {
|
|
62
|
+
if (el && autoFocus) el.focus();
|
|
63
|
+
},
|
|
64
|
+
[autoFocus],
|
|
65
|
+
);
|
|
56
66
|
const filter = resolveFilter(column);
|
|
57
67
|
if (!filter) return null;
|
|
58
68
|
|
|
@@ -80,11 +90,9 @@ export function FilterPopover<T>({
|
|
|
80
90
|
<input
|
|
81
91
|
type="text"
|
|
82
92
|
// A prop now, defaulting to what the popover needs; see `autoFocus` on the
|
|
83
|
-
// props above.
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
// shrink the backlog without emptying it.
|
|
87
|
-
autoFocus={autoFocus}
|
|
93
|
+
// props above. Focused by `focusOnMount` rather than the `autoFocus`
|
|
94
|
+
// attribute, because here taking focus is the default, not an opt-in.
|
|
95
|
+
ref={focusOnMount}
|
|
88
96
|
value={v.q}
|
|
89
97
|
onChange={(e) => onChange({ type: "text", q: e.target.value })}
|
|
90
98
|
placeholder={labels.filterPlaceholder}
|
|
@@ -1548,6 +1548,9 @@ export function DataTable<T>({
|
|
|
1548
1548
|
{cardInner}
|
|
1549
1549
|
</RowLink>
|
|
1550
1550
|
) : (
|
|
1551
|
+
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- the handlers are
|
|
1552
|
+
only attached when `interactive`, and then so are role="button", tabIndex 0 and the
|
|
1553
|
+
Enter/Space handler below; the rule cannot follow the conditional role. */
|
|
1551
1554
|
<div
|
|
1552
1555
|
role={interactive ? "button" : undefined}
|
|
1553
1556
|
tabIndex={interactive ? 0 : undefined}
|
|
@@ -1918,6 +1921,9 @@ export function DataTable<T>({
|
|
|
1918
1921
|
)}
|
|
1919
1922
|
</div>
|
|
1920
1923
|
{resizable && col.key !== ROW_ACTIONS_KEY && (
|
|
1924
|
+
/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a
|
|
1925
|
+
pointer-only drag handle for a visual preference (column width); it is
|
|
1926
|
+
deliberately not a tab stop, and no content or action depends on it. */
|
|
1921
1927
|
<span
|
|
1922
1928
|
role="separator"
|
|
1923
1929
|
aria-orientation="vertical"
|
|
@@ -137,7 +137,7 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
|
|
|
137
137
|
create: createLabel,
|
|
138
138
|
});
|
|
139
139
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
140
|
-
const { open, results, resolve, setOpen } = core;
|
|
140
|
+
const { open, results, resolve, setOpen, rememberOption, query, triggerRef } = core;
|
|
141
141
|
// One id per instance, generated here rather than in the core: `aria-controls` on
|
|
142
142
|
// the trigger has to name the list while the list is still closed, so the id
|
|
143
143
|
// belongs to whoever renders both ends of it.
|
|
@@ -146,7 +146,7 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
|
|
|
146
146
|
// The clear value is "nothing selected" too, whichever one the caller picked.
|
|
147
147
|
const chosen: V | null = value == null || value === clearValue ? null : (value as V);
|
|
148
148
|
const selectedOption = chosen == null ? null : resolve(chosen);
|
|
149
|
-
const q =
|
|
149
|
+
const q = query.trim();
|
|
150
150
|
const showCreate =
|
|
151
151
|
Boolean(onCreate) && q.length > 0 && !results.some((o) => o.label.toLowerCase() === q.toLowerCase());
|
|
152
152
|
const showClear = Boolean(clearable && chosen != null && !disabled);
|
|
@@ -154,7 +154,7 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
|
|
|
154
154
|
const triggerText = selectedOption?.label ?? placeholder ?? "";
|
|
155
155
|
|
|
156
156
|
const choose = (o: ComboOption<V>) => {
|
|
157
|
-
|
|
157
|
+
rememberOption(o);
|
|
158
158
|
onChange(o.value);
|
|
159
159
|
// Back to the trigger, not to <body>: the panel that held focus is about to
|
|
160
160
|
// unmount, and a keyboard user who just answered this field should be standing
|
|
@@ -170,7 +170,7 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
|
|
|
170
170
|
<div {...rest} className={cn("relative", className)}>
|
|
171
171
|
{label !== undefined && <FieldLabel>{label}</FieldLabel>}
|
|
172
172
|
<button
|
|
173
|
-
ref={
|
|
173
|
+
ref={triggerRef}
|
|
174
174
|
id={id}
|
|
175
175
|
type="button"
|
|
176
176
|
// A combobox, not a button. The distinction is not pedantry: this control
|
|
@@ -237,6 +237,9 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
|
|
|
237
237
|
</span>
|
|
238
238
|
</span>
|
|
239
239
|
{showClear ? (
|
|
240
|
+
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside
|
|
241
|
+
the trigger <button>, so it never holds focus and a key handler here could
|
|
242
|
+
never fire; the keys go to the trigger. A pointer shortcut only. */
|
|
240
243
|
<span
|
|
241
244
|
role="button"
|
|
242
245
|
tabIndex={-1}
|