@eifi1/ui-kit 0.20.1 → 0.22.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/dist/chart.d.ts +10 -8
- package/dist/components/account-chips.d.ts +10 -3
- package/dist/components/account-chips.js +5 -2
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/account-settings.d.ts +33 -2
- package/dist/components/account-settings.js +43 -21
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +27 -8
- package/dist/components/amount-input.js +15 -28
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +8 -0
- package/dist/components/autocomplete.js +9 -4
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +10 -8
- package/dist/components/calculator.d.ts +10 -8
- package/dist/components/checkbox-group.d.ts +104 -0
- package/dist/components/checkbox-group.js +111 -0
- package/dist/components/checkbox-group.js.map +1 -0
- package/dist/components/checkbox.d.ts +19 -0
- package/dist/components/checkbox.js +30 -7
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +18 -0
- package/dist/components/chip.js +47 -10
- package/dist/components/chip.js.map +1 -1
- package/dist/components/combobox.d.ts +13 -2
- package/dist/components/combobox.js +17 -8
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +66 -161
- package/dist/components/copy-button.d.ts +10 -8
- package/dist/components/country-select.d.ts +124 -0
- package/dist/components/country-select.js +237 -0
- package/dist/components/country-select.js.map +1 -0
- package/dist/components/currency-select.d.ts +17 -1
- package/dist/components/currency-select.js +56 -23
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +66 -166
- package/dist/components/danger-confirm.js +133 -49
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +1 -2
- package/dist/components/data-table-filter-popover.d.ts +1 -2
- package/dist/components/data-table-filters.d.ts +1 -2
- package/dist/components/data-table-labels.d.ts +4 -97
- package/dist/components/data-table-labels.js +9 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table.d.ts +1 -2
- package/dist/components/data-table.js +98 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +25 -5
- package/dist/components/date-picker.js +110 -59
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +8 -1
- package/dist/components/entity-combobox.js +9 -4
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +10 -8
- package/dist/components/field-anatomy.d.ts +95 -0
- package/dist/components/field-anatomy.js +84 -0
- package/dist/components/field-anatomy.js.map +1 -0
- package/dist/components/field-parts.d.ts +114 -0
- package/dist/components/field-parts.js +72 -0
- package/dist/components/field-parts.js.map +1 -0
- package/dist/components/file-button.d.ts +10 -8
- package/dist/components/file-button.js +13 -3
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +32 -9
- package/dist/components/file-dropzone.js +57 -34
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +10 -8
- package/dist/components/form-actions.js +61 -3
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/hover-menu.d.ts +3 -0
- package/dist/components/hover-menu.js +2 -2
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/iban-input.d.ts +66 -0
- package/dist/components/iban-input.js +125 -0
- package/dist/components/iban-input.js.map +1 -0
- package/dist/components/icon-picker.d.ts +12 -2
- package/dist/components/icon-picker.js +9 -2
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/inline-edit-field.d.ts +167 -0
- package/dist/components/inline-edit-field.js +293 -0
- package/dist/components/inline-edit-field.js.map +1 -0
- package/dist/components/language-select.d.ts +127 -0
- package/dist/components/language-select.js +18 -0
- package/dist/components/language-select.js.map +1 -0
- package/dist/components/line-items.d.ts +25 -3
- package/dist/components/line-items.js +55 -46
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +25 -0
- package/dist/components/list.js +16 -5
- package/dist/components/list.js.map +1 -1
- package/dist/components/money-field.d.ts +20 -8
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.d.ts +52 -4
- package/dist/components/month-picker.js +290 -86
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-select.d.ts +8 -1
- package/dist/components/multi-select.js +9 -4
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +25 -13
- package/dist/components/number-field.js +6 -20
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +70 -13
- package/dist/components/number-input.js +90 -38
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +10 -8
- package/dist/components/one-time-code-input.d.ts +100 -0
- package/dist/components/one-time-code-input.js +137 -0
- package/dist/components/one-time-code-input.js.map +1 -0
- package/dist/components/phone-input.d.ts +66 -0
- package/dist/components/phone-input.js +171 -0
- package/dist/components/phone-input.js.map +1 -0
- package/dist/components/series-chart.d.ts +10 -8
- package/dist/components/settings-fields.d.ts +10 -8
- package/dist/components/share-card.d.ts +8 -6
- package/dist/components/sign-chip.d.ts +66 -0
- package/dist/components/sign-chip.js +39 -0
- package/dist/components/sign-chip.js.map +1 -0
- package/dist/components/swatch-picker.d.ts +27 -2
- package/dist/components/swatch-picker.js +73 -31
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/switch.d.ts +19 -0
- package/dist/components/switch.js +24 -6
- package/dist/components/switch.js.map +1 -1
- package/dist/components/text-link.d.ts +10 -8
- package/dist/components/tile-radio.d.ts +68 -13
- package/dist/components/tile-radio.js +3 -3
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +15 -9
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toggle-group.d.ts +47 -1
- package/dist/components/toggle-group.js +110 -69
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/ui.d.ts +10 -8
- package/dist/components/ui.js +280 -78
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -2
- package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
- package/dist/data-table.d.ts +1 -2
- package/dist/data-table.js +2 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +210 -3
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +1 -1
- package/dist/feedback/feedback-thread.d.ts +37 -3
- package/dist/feedback/feedback-thread.js +38 -9
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-NhUm7Zga.d.ts} +70 -5
- package/dist/feedback.d.ts +2 -2
- package/dist/hooks/use-file-drop.d.ts +10 -8
- package/dist/i18n/defaults.d.ts +10 -8
- package/dist/i18n/defaults.js +17 -2
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +10 -8
- package/dist/i18n/german.js +50 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +10 -8
- package/dist/i18n/kit-labels.js +8 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +10 -8
- package/dist/i18n/locales/de-CH.d.ts +10 -8
- package/dist/i18n/locales/en.d.ts +10 -8
- package/dist/i18n/locales/en.js +9 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +10 -8
- package/dist/i18n/locales/es.js +48 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +10 -8
- package/dist/i18n/locales/fr.js +48 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +10 -8
- package/dist/i18n/locales/hu.js +49 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +10 -8
- package/dist/i18n/locales/it.js +49 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +10 -8
- package/dist/i18n/locales/zh.js +48 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +10 -8
- package/dist/i18n/review.js +8 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +17 -10
- package/dist/index.js +57 -2
- package/dist/index.js.map +1 -1
- package/dist/{kit-labels-D1J2_jyD.d.ts → kit-labels-R8mIAc9N.d.ts} +811 -8
- package/dist/lib/countries.d.ts +43 -0
- package/dist/lib/countries.js +39 -0
- package/dist/lib/countries.js.map +1 -0
- package/dist/lib/decimal-marks.d.ts +46 -0
- package/dist/lib/decimal-marks.js +40 -0
- package/dist/lib/decimal-marks.js.map +1 -0
- package/dist/lib/iban.d.ts +98 -0
- package/dist/lib/iban.js +154 -0
- package/dist/lib/iban.js.map +1 -0
- package/dist/lib/isin.d.ts +30 -0
- package/dist/lib/isin.js +36 -0
- package/dist/lib/isin.js.map +1 -0
- package/dist/lib/phone.d.ts +69 -0
- package/dist/lib/phone.js +121 -0
- package/dist/lib/phone.js.map +1 -0
- package/dist/rhf/fields.d.ts +148 -11
- package/dist/rhf/fields.js +332 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +10 -8
- package/dist/rhf.d.ts +13 -9
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +10 -8
- package/dist/shell/top-bar-brand.d.ts +10 -8
- package/dist/shell/topbar-action-menu.d.ts +13 -1
- package/dist/shell/topbar-action-menu.js +31 -28
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +10 -8
- package/dist/tour/tour.js +20 -24
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +10 -8
- package/dist/wizard.d.ts +10 -8
- package/package.json +1 -1
- package/src/components/account-chips.tsx +17 -3
- package/src/components/account-settings.tsx +92 -26
- package/src/components/amount-input.tsx +49 -56
- package/src/components/autocomplete.tsx +133 -110
- package/src/components/checkbox-group.tsx +229 -0
- package/src/components/checkbox.tsx +72 -8
- package/src/components/chip.tsx +80 -10
- package/src/components/combobox.tsx +292 -253
- package/src/components/country-select.tsx +443 -0
- package/src/components/currency-select.tsx +109 -33
- package/src/components/danger-confirm.tsx +345 -77
- package/src/components/data-table-labels.ts +16 -1
- package/src/components/data-table.tsx +175 -6
- package/src/components/date-picker.tsx +173 -62
- package/src/components/entity-combobox.tsx +107 -87
- package/src/components/field-anatomy.tsx +190 -0
- package/src/components/field-parts.tsx +184 -0
- package/src/components/file-button.tsx +36 -3
- package/src/components/file-dropzone.tsx +106 -38
- package/src/components/form-actions.tsx +115 -2
- package/src/components/hover-menu.tsx +7 -4
- package/src/components/iban-input.tsx +231 -0
- package/src/components/icon-picker.tsx +26 -4
- package/src/components/inline-edit-field.tsx +533 -0
- package/src/components/language-select.tsx +78 -0
- package/src/components/line-items.tsx +35 -2
- package/src/components/list.tsx +55 -6
- package/src/components/money-field.tsx +10 -0
- package/src/components/month-picker.tsx +399 -63
- package/src/components/multi-select.tsx +80 -58
- package/src/components/number-field.tsx +28 -46
- package/src/components/number-input.tsx +210 -43
- package/src/components/one-time-code-input.tsx +288 -0
- package/src/components/phone-input.tsx +299 -0
- package/src/components/sign-chip.tsx +103 -0
- package/src/components/swatch-picker.tsx +118 -30
- package/src/components/switch.tsx +58 -7
- package/src/components/tile-radio.tsx +71 -16
- package/src/components/time-input.tsx +8 -1
- package/src/components/toggle-group.tsx +206 -73
- package/src/components/ui.tsx +472 -105
- package/src/data-table.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +313 -9
- package/src/feedback/feedback-dialog.tsx +2 -0
- package/src/feedback/feedback-thread.tsx +66 -5
- package/src/i18n/defaults.ts +16 -1
- package/src/i18n/german.ts +47 -0
- package/src/i18n/kit-labels.tsx +78 -2
- package/src/i18n/locales/en.ts +10 -0
- package/src/i18n/locales/es.ts +46 -0
- package/src/i18n/locales/fr.ts +45 -0
- package/src/i18n/locales/hu.ts +46 -0
- package/src/i18n/locales/it.ts +47 -0
- package/src/i18n/locales/zh.ts +44 -0
- package/src/i18n/review.ts +7 -0
- package/src/index.ts +53 -3
- package/src/lib/countries.ts +91 -0
- package/src/lib/decimal-marks.ts +99 -0
- package/src/lib/iban.ts +171 -0
- package/src/lib/isin.ts +62 -0
- package/src/lib/phone.ts +211 -0
- package/src/rhf/fields.tsx +606 -9
- package/src/rhf.ts +2 -1
- package/src/shell/topbar-action-menu.tsx +53 -29
- package/src/tour/tour.tsx +66 -28
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { forwardRef, useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import type { ChangeEvent, FocusEvent, ReactNode } from "react";
|
|
3
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { IBAN_LENGTHS, compactIban, formatIban, ibanProblem } from "../lib/iban";
|
|
6
|
+
import type { IbanKind, IbanProblem } from "../lib/iban";
|
|
7
|
+
import { assignRef, hasMessage } from "./choice-parts";
|
|
8
|
+
import { Input, type InputProps } from "./ui";
|
|
9
|
+
|
|
10
|
+
// ── Labels ────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/** The messages {@link IbanInput} shows under itself, one per {@link IbanProblem}. */
|
|
13
|
+
export interface IbanInputLabels {
|
|
14
|
+
/** The value does not start with a two-letter country code and two check digits. */
|
|
15
|
+
format: string;
|
|
16
|
+
/** The first two letters are no country in the IBAN registry. `code` is those two
|
|
17
|
+
* letters, as typed (upper case). */
|
|
18
|
+
country: (code: string) => string;
|
|
19
|
+
/** The wrong length for the country: `actual` characters typed, `expected` for that
|
|
20
|
+
* country (spaces not counted in either). */
|
|
21
|
+
length: (actual: number, expected: number) => string;
|
|
22
|
+
/** The mod-97 check digits do not match — a mistyped or swapped character. */
|
|
23
|
+
checksum: string;
|
|
24
|
+
/** `kind="qr"`: a valid IBAN, but not a QR-IBAN. */
|
|
25
|
+
qrRequired: string;
|
|
26
|
+
/** `kind="plain"`: a QR-IBAN, which cannot receive an ordinary transfer. */
|
|
27
|
+
qrNotAllowed: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_IBAN_INPUT_LABELS: IbanInputLabels = {
|
|
31
|
+
format: "An IBAN starts with a two-letter country code and two check digits.",
|
|
32
|
+
country: (code) => `“${code}” is not the country code of an IBAN.`,
|
|
33
|
+
length: (actual, expected) =>
|
|
34
|
+
`An IBAN from this country has ${expected} characters — this one has ${actual}.`,
|
|
35
|
+
checksum: "The check digits don’t match — a character is probably mistyped.",
|
|
36
|
+
qrRequired: "This is a regular IBAN. A QR-bill needs the account’s QR-IBAN.",
|
|
37
|
+
qrNotAllowed: "This is a QR-IBAN, which only receives QR-bill payments. Enter the account’s regular IBAN.",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The `ibanInput` namespace: prop > `<UiKitProvider labels>` > English (0.22.0).
|
|
42
|
+
*/
|
|
43
|
+
function useIbanInputLabels(prop: Partial<IbanInputLabels> | undefined): IbanInputLabels {
|
|
44
|
+
const labels: IbanInputLabels = useKitLabels("ibanInput", DEFAULT_IBAN_INPUT_LABELS, prop);
|
|
45
|
+
return labels;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
const alnumCount = (text: string) => text.replace(/[^0-9A-Za-z]/g, "").length;
|
|
51
|
+
|
|
52
|
+
/** The index just after the `count`-th letter or digit of `text` — where the caret
|
|
53
|
+
* goes once the grouping has moved the characters it was between. */
|
|
54
|
+
function indexAfter(text: string, count: number): number {
|
|
55
|
+
if (count <= 0) return 0;
|
|
56
|
+
let seen = 0;
|
|
57
|
+
for (let i = 0; i < text.length; i++) {
|
|
58
|
+
if (/[0-9A-Za-z]/.test(text[i])) seen += 1;
|
|
59
|
+
if (seen === count) return i + 1;
|
|
60
|
+
}
|
|
61
|
+
return text.length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The problem worth SHOWING right now. Settled (not focused): whatever is wrong. While
|
|
66
|
+
* typing: only what more typing cannot fix — a broken start or an unknown country from
|
|
67
|
+
* the fourth character, and the rest once the country's full length is there. A
|
|
68
|
+
* half-typed IBAN is not wrong, and a field that says "wrong length" from the first
|
|
69
|
+
* keystroke is a field people learn to ignore.
|
|
70
|
+
*/
|
|
71
|
+
function visibleProblem(compact: string, kind: IbanKind, focused: boolean): IbanProblem | null {
|
|
72
|
+
if (compact === "") return null;
|
|
73
|
+
const problem = ibanProblem(compact, kind);
|
|
74
|
+
if (problem === null || !focused) return problem;
|
|
75
|
+
if (compact.length < 4) return null;
|
|
76
|
+
if (problem === "format" || problem === "country") return problem;
|
|
77
|
+
return compact.length >= IBAN_LENGTHS[compact.slice(0, 2)]! ? problem : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function messageFor(problem: IbanProblem, compact: string, labels: IbanInputLabels): string {
|
|
81
|
+
switch (problem) {
|
|
82
|
+
case "format":
|
|
83
|
+
return labels.format;
|
|
84
|
+
case "country":
|
|
85
|
+
return labels.country(compact.slice(0, 2));
|
|
86
|
+
case "length":
|
|
87
|
+
return labels.length(compact.length, IBAN_LENGTHS[compact.slice(0, 2)]!);
|
|
88
|
+
case "checksum":
|
|
89
|
+
return labels.checksum;
|
|
90
|
+
case "qrRequired":
|
|
91
|
+
return labels.qrRequired;
|
|
92
|
+
case "qrNotAllowed":
|
|
93
|
+
return labels.qrNotAllowed;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Component ─────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export interface IbanInputProps extends Omit<InputProps, "type" | "value" | "defaultValue"> {
|
|
100
|
+
/** The IBAN, compact or not — it is shown in groups of four either way. `""` is
|
|
101
|
+
* "no IBAN". */
|
|
102
|
+
value: string;
|
|
103
|
+
/** The compact, upper-case IBAN (the electronic format, what to store) on every
|
|
104
|
+
* change — valid or not: a half-typed IBAN is still the field's value. Plain
|
|
105
|
+
* `onChange` still fires alongside it with the raw event, whose text is the GROUPED
|
|
106
|
+
* one, so bind this rather than `onChange` to a form. */
|
|
107
|
+
onValueChange?: (value: string) => void;
|
|
108
|
+
/** Which IBANs the field accepts. Default `"any"`. `"qr"` / `"plain"`: see
|
|
109
|
+
* {@link IbanKind} — and why the difference matters, {@link isQrIban}. */
|
|
110
|
+
kind?: IbanKind;
|
|
111
|
+
/** The messages, per field. See {@link IbanInputLabels}. */
|
|
112
|
+
labels?: Partial<IbanInputLabels>;
|
|
113
|
+
/** The caller's own message, shown INSTEAD of the built-in one while it is set (a
|
|
114
|
+
* server's "this account is already on file"). `null`, `false` and `""` are no
|
|
115
|
+
* message, so `touched && errors.iban` can be passed as it is. */
|
|
116
|
+
error?: ReactNode;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* An IBAN field (kastlan, keksdose): typed in any shape, shown in groups of four,
|
|
121
|
+
* reported compact.
|
|
122
|
+
*
|
|
123
|
+
* Both apps had a text field and a regex. What that left out is everything between
|
|
124
|
+
* the keyboard and the stored value: a pasted "ch09 0000-0000…" stayed lower case
|
|
125
|
+
* with its hyphens, the grouping that makes 21 characters checkable by eye was
|
|
126
|
+
* missing, and a wrong IBAN was found out by the bank. Here:
|
|
127
|
+
*
|
|
128
|
+
* - **Grouped as you type.** The field shows the paper format ({@link formatIban});
|
|
129
|
+
* spaces, hyphens and lower case in what is typed or pasted are folded away, and
|
|
130
|
+
* the caret stays between the same two characters while the groups move around it.
|
|
131
|
+
* - **Compact out.** `onValueChange` gets the electronic format — upper case, no
|
|
132
|
+
* spaces — which is the one to store and to compare.
|
|
133
|
+
* - **Checked, never blocked.** A wrong country, length or checksum shows a message
|
|
134
|
+
* under the field, through the kit's `error` (so it is the field's
|
|
135
|
+
* `aria-describedby` and paints it), but every keystroke still lands: a field that
|
|
136
|
+
* refuses characters cannot be corrected one character at a time. While the field
|
|
137
|
+
* has focus only what more typing cannot fix is shown; the rest waits for the full
|
|
138
|
+
* length or for blur. Whether the field is REQUIRED stays the form's rule — an empty
|
|
139
|
+
* field says nothing.
|
|
140
|
+
* - **QR-IBAN or not** (kastlan): `kind="qr"` refuses a regular IBAN and
|
|
141
|
+
* `kind="plain"` refuses a QR-IBAN — a QR-bill can only be paid to a QR-IBAN, and a
|
|
142
|
+
* QR-IBAN can receive nothing else. See {@link isQrIban}.
|
|
143
|
+
*
|
|
144
|
+
* It is {@link Input} underneath, so `label`, `hint`, `error`, `invalid` and the rest
|
|
145
|
+
* are Input's own and the field reads like every other. A form that must not submit
|
|
146
|
+
* an invalid IBAN checks {@link isValidIban} / {@link ibanProblem} itself — the field
|
|
147
|
+
* shows the problem but does not own the submit.
|
|
148
|
+
*/
|
|
149
|
+
export const IbanInput = forwardRef<HTMLInputElement, IbanInputProps>(function IbanInput(
|
|
150
|
+
{ value, onValueChange, onChange, onFocus, onBlur, kind = "any", labels, error, inputClassName, ...rest },
|
|
151
|
+
ref,
|
|
152
|
+
) {
|
|
153
|
+
const text = useIbanInputLabels(labels);
|
|
154
|
+
const compact = compactIban(value);
|
|
155
|
+
const shown = formatIban(compact);
|
|
156
|
+
const [focused, setFocused] = useState(false);
|
|
157
|
+
const innerRef = useRef<HTMLInputElement | null>(null);
|
|
158
|
+
// Letters and digits before the caret, kept across the re-render that regroups.
|
|
159
|
+
const caretRef = useRef<number | null>(null);
|
|
160
|
+
const setRefs = useCallback(
|
|
161
|
+
(el: HTMLInputElement | null) => {
|
|
162
|
+
innerRef.current = el;
|
|
163
|
+
assignRef(ref, el);
|
|
164
|
+
},
|
|
165
|
+
[ref],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Every render: when a change moved the groups, put the caret back after the same
|
|
169
|
+
// character it followed. Only while focused — a value set from outside moves nothing.
|
|
170
|
+
useLayoutEffect(() => {
|
|
171
|
+
const el = innerRef.current;
|
|
172
|
+
const count = caretRef.current;
|
|
173
|
+
if (el === null || count === null) return;
|
|
174
|
+
caretRef.current = null;
|
|
175
|
+
if (el.ownerDocument.activeElement !== el) return;
|
|
176
|
+
const at = indexAfter(el.value, count);
|
|
177
|
+
el.setSelectionRange(at, at);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
181
|
+
onChange?.(e);
|
|
182
|
+
const el = e.target;
|
|
183
|
+
const raw = el.value;
|
|
184
|
+
const next = compactIban(raw);
|
|
185
|
+
// Characters compactIban dropped from the front (a pasted "IBAN " label) are not
|
|
186
|
+
// in the value, so they are not counted before the caret either.
|
|
187
|
+
const dropped = alnumCount(raw) - next.length;
|
|
188
|
+
const count = Math.max(0, alnumCount(raw.slice(0, el.selectionStart ?? raw.length)) - dropped);
|
|
189
|
+
if (next === compact) {
|
|
190
|
+
// Nothing the value holds changed — a space typed, a group's space deleted — so
|
|
191
|
+
// no re-render will come to regroup it. Regroup now, and keep the caret: after
|
|
192
|
+
// a Backspace on a group's space, that is just before it.
|
|
193
|
+
el.value = shown;
|
|
194
|
+
const at = indexAfter(shown, count);
|
|
195
|
+
el.setSelectionRange(at, at);
|
|
196
|
+
} else {
|
|
197
|
+
caretRef.current = count;
|
|
198
|
+
}
|
|
199
|
+
onValueChange?.(next);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const problem = visibleProblem(compact, kind, focused);
|
|
203
|
+
return (
|
|
204
|
+
<Input
|
|
205
|
+
ref={setRefs}
|
|
206
|
+
{...rest}
|
|
207
|
+
type="text"
|
|
208
|
+
value={shown}
|
|
209
|
+
onChange={handleChange}
|
|
210
|
+
onFocus={(e: FocusEvent<HTMLInputElement>) => {
|
|
211
|
+
setFocused(true);
|
|
212
|
+
onFocus?.(e);
|
|
213
|
+
}}
|
|
214
|
+
onBlur={(e: FocusEvent<HTMLInputElement>) => {
|
|
215
|
+
setFocused(false);
|
|
216
|
+
onBlur?.(e);
|
|
217
|
+
}}
|
|
218
|
+
autoComplete={rest.autoComplete ?? "off"}
|
|
219
|
+
autoCapitalize={rest.autoCapitalize ?? "characters"}
|
|
220
|
+
autoCorrect={rest.autoCorrect ?? "off"}
|
|
221
|
+
spellCheck={rest.spellCheck ?? false}
|
|
222
|
+
// ALWAYS passed, even while there is no message: a field that takes `error`
|
|
223
|
+
// keeps its box, so the message coming and going under the caret never remounts
|
|
224
|
+
// the input (see Input's FieldGroup) — this one comes and goes as you type.
|
|
225
|
+
error={hasMessage(error) ? error : problem ? messageFor(problem, compact, text) : undefined}
|
|
226
|
+
// Groups of four line up only when every digit is the same width.
|
|
227
|
+
inputClassName={cn("tabular-nums", inputClassName)}
|
|
228
|
+
/>
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
IbanInput.displayName = "IbanInput";
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useId, useMemo, useState } from "react";
|
|
2
|
-
import type { ComponentPropsWithoutRef, ComponentType } from "react";
|
|
2
|
+
import type { ComponentPropsWithoutRef, ComponentType, ReactNode } from "react";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
5
5
|
import { SearchField } from "./search-field";
|
|
6
6
|
import { TILE_SIZE, TileRadioGroup } from "./tile-radio";
|
|
7
7
|
import type { TileItem, TileSize } from "./tile-radio";
|
|
8
|
+
import { hasContent, LabelStrip } from "./field-anatomy";
|
|
8
9
|
|
|
9
10
|
export interface IconPickerLabels {
|
|
10
11
|
/** The "no icon" tile's name, when `allowNone` is set. */
|
|
@@ -76,6 +77,16 @@ export interface IconPickerProps<T extends string>
|
|
|
76
77
|
tileClassName?: string;
|
|
77
78
|
disabled?: boolean;
|
|
78
79
|
labels?: Partial<IconPickerLabels>;
|
|
80
|
+
/**
|
|
81
|
+
* An 11px static label in a strip over the picker, naming the grid — keksdose K2,
|
|
82
|
+
* {@link SwatchPickerProps.label}'s twin: a symbol picker standing in a form row
|
|
83
|
+
* beside labelled fields, with its label on their line. With `searchable` the strip
|
|
84
|
+
* is over the search box too, which keeps its own name. With a label, `className`
|
|
85
|
+
* styles the wrapper.
|
|
86
|
+
*/
|
|
87
|
+
label?: ReactNode;
|
|
88
|
+
/** A {@link FieldHint} beside the label. Only with `label`. */
|
|
89
|
+
hint?: ReactNode;
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
/** Case- and accent-insensitive, so "cafe" finds "Café". */
|
|
@@ -105,6 +116,8 @@ export function IconPicker<T extends string>({
|
|
|
105
116
|
tileClassName,
|
|
106
117
|
disabled = false,
|
|
107
118
|
labels,
|
|
119
|
+
label,
|
|
120
|
+
hint,
|
|
108
121
|
className,
|
|
109
122
|
"aria-label": ariaLabel,
|
|
110
123
|
"aria-labelledby": ariaLabelledBy,
|
|
@@ -115,6 +128,8 @@ export function IconPicker<T extends string>({
|
|
|
115
128
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
116
129
|
const locale = useKitLocale();
|
|
117
130
|
const mixedId = useId();
|
|
131
|
+
const labelId = useId();
|
|
132
|
+
const labelled = hasContent(label);
|
|
118
133
|
const [query, setQuery] = useState("");
|
|
119
134
|
const needle = fold(query.trim(), locale);
|
|
120
135
|
|
|
@@ -135,8 +150,8 @@ export function IconPicker<T extends string>({
|
|
|
135
150
|
...visible.map((o) => ({ key: o.value, label: o.label, note: o.note, disabled: o.disabled })),
|
|
136
151
|
];
|
|
137
152
|
|
|
138
|
-
|
|
139
|
-
<div {...rest} className={cn("space-y-2", className)}>
|
|
153
|
+
const picker = (
|
|
154
|
+
<div {...rest} className={cn("space-y-2", !labelled && className)}>
|
|
140
155
|
{searchable && (
|
|
141
156
|
<SearchField
|
|
142
157
|
value={query}
|
|
@@ -149,7 +164,8 @@ export function IconPicker<T extends string>({
|
|
|
149
164
|
<div
|
|
150
165
|
role="radiogroup"
|
|
151
166
|
aria-label={ariaLabel}
|
|
152
|
-
|
|
167
|
+
// The strip's label names the GRID, unless the caller named it already.
|
|
168
|
+
aria-labelledby={labelled && ariaLabel === undefined && ariaLabelledBy === undefined ? labelId : ariaLabelledBy}
|
|
153
169
|
aria-describedby={[ariaDescribedBy, mixed && mixedId].filter(Boolean).join(" ") || undefined}
|
|
154
170
|
aria-disabled={disabled || undefined}
|
|
155
171
|
className="flex flex-wrap items-center gap-1.5"
|
|
@@ -191,4 +207,10 @@ export function IconPicker<T extends string>({
|
|
|
191
207
|
)}
|
|
192
208
|
</div>
|
|
193
209
|
);
|
|
210
|
+
if (!labelled) return picker;
|
|
211
|
+
return (
|
|
212
|
+
<LabelStrip labelId={labelId} label={label} hint={hint} disabled={disabled} pad="tiles" className={className}>
|
|
213
|
+
{picker}
|
|
214
|
+
</LabelStrip>
|
|
215
|
+
);
|
|
194
216
|
}
|