@eifi1/ui-kit 0.21.0 → 0.23.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 +42 -25
- package/dist/chart.d.ts +19 -14
- 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 +53 -14
- package/dist/components/amount-input.js +17 -28
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +29 -0
- package/dist/components/autocomplete.js +134 -104
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +19 -14
- package/dist/components/calculator.d.ts +19 -14
- 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/column-mapper.d.ts +182 -0
- package/dist/components/column-mapper.js +375 -0
- package/dist/components/column-mapper.js.map +1 -0
- package/dist/components/combobox-core.d.ts +39 -2
- package/dist/components/combobox-core.js +29 -6
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +91 -2
- package/dist/components/combobox.js +454 -308
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +69 -161
- package/dist/components/confirm-dialog.js +7 -8
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +16 -11
- package/dist/components/country-select.d.ts +167 -0
- package/dist/components/country-select.js +293 -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 +69 -166
- package/dist/components/danger-confirm.js +144 -50
- 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 +66 -5
- package/dist/components/date-picker.js +378 -66
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +42 -1
- package/dist/components/entity-combobox.js +154 -113
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +19 -14
- package/dist/components/field-parts.d.ts +224 -0
- package/dist/components/field-parts.js +156 -0
- package/dist/components/field-parts.js.map +1 -0
- package/dist/components/field-strip.d.ts +105 -0
- package/dist/components/field-strip.js +46 -0
- package/dist/components/field-strip.js.map +1 -0
- package/dist/components/file-button.d.ts +19 -14
- package/dist/components/file-button.js +13 -3
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +41 -15
- package/dist/components/file-dropzone.js +57 -34
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +18 -13
- package/dist/components/form-actions.js +69 -5
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +69 -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 +21 -2
- package/dist/components/icon-picker.js +19 -4
- 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 +130 -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 +32 -14
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.d.ts +52 -4
- package/dist/components/month-picker.js +291 -86
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +26 -1
- package/dist/components/multi-entity-combobox.js +143 -102
- package/dist/components/multi-entity-combobox.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 +34 -19
- package/dist/components/number-field.js +6 -20
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +79 -19
- package/dist/components/number-input.js +90 -38
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +19 -14
- 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 +69 -0
- package/dist/components/phone-input.js +171 -0
- package/dist/components/phone-input.js.map +1 -0
- package/dist/components/reauth-dialog.d.ts +2 -2
- package/dist/components/reauth-dialog.js +6 -8
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/series-chart.d.ts +19 -14
- package/dist/components/settings-fields.d.ts +19 -14
- package/dist/components/share-card.d.ts +16 -11
- 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 +43 -2
- package/dist/components/swatch-picker.js +80 -32
- 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 +18 -13
- package/dist/components/tile-radio.d.ts +88 -13
- package/dist/components/tile-radio.js +28 -11
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +24 -15
- 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/trigger-aria.d.ts +3 -0
- package/dist/components/trigger-aria.js +3 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/ui.d.ts +16 -11
- package/dist/components/ui.js +284 -88
- 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 +348 -41
- 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 +3 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +68 -253
- package/dist/feedback/feedback-thread.js +46 -10
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
- package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
- package/dist/feedback.d.ts +66 -2
- package/dist/hooks/use-file-drop.d.ts +19 -14
- package/dist/i18n/defaults.d.ts +19 -14
- package/dist/i18n/defaults.js +19 -2
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +19 -14
- package/dist/i18n/german.js +84 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +15 -10
- package/dist/i18n/kit-labels.js +8 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +19 -14
- package/dist/i18n/locales/de-CH.d.ts +19 -14
- package/dist/i18n/locales/en.d.ts +19 -14
- package/dist/i18n/locales/en.js +16 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +19 -14
- package/dist/i18n/locales/es.js +83 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +19 -14
- package/dist/i18n/locales/fr.js +82 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +19 -14
- package/dist/i18n/locales/hu.js +85 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +19 -14
- package/dist/i18n/locales/it.js +84 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +19 -14
- package/dist/i18n/locales/zh.js +80 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +19 -14
- package/dist/i18n/review.js +27 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +21 -10
- package/dist/index.js +73 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/column-mapping.d.ts +81 -0
- package/dist/lib/column-mapping.js +108 -0
- package/dist/lib/column-mapping.js.map +1 -0
- 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/lib/table-text.d.ts +109 -1
- package/dist/lib/table-text.js +122 -1
- package/dist/lib/table-text.js.map +1 -1
- package/dist/rhf/fields.d.ts +233 -18
- package/dist/rhf/fields.js +456 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +19 -14
- package/dist/rhf.d.ts +22 -15
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +18 -13
- package/dist/shell/top-bar-brand.d.ts +19 -14
- package/dist/shell/topbar-action-menu.d.ts +36 -3
- package/dist/shell/topbar-action-menu.js +74 -33
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +18 -13
- package/dist/table-text.d.ts +1 -1
- package/dist/tour/tour.js +20 -24
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +49 -16
- package/dist/wizard/stepper-nav.js +4 -0
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +19 -14
- 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 +68 -56
- package/src/components/autocomplete.tsx +198 -114
- 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/column-mapper.tsx +666 -0
- package/src/components/combobox-core.tsx +82 -9
- package/src/components/combobox.tsx +661 -340
- package/src/components/confirm-dialog.tsx +12 -8
- package/src/components/country-select.tsx +556 -0
- package/src/components/currency-select.tsx +109 -33
- package/src/components/danger-confirm.tsx +429 -82
- package/src/components/data-table-labels.ts +16 -1
- package/src/components/data-table.tsx +175 -6
- package/src/components/date-picker.tsx +595 -71
- package/src/components/entity-combobox.tsx +235 -124
- package/src/components/field-parts.tsx +403 -0
- package/src/components/field-strip.tsx +149 -0
- package/src/components/file-button.tsx +36 -3
- package/src/components/file-dropzone.tsx +106 -38
- package/src/components/form-actions.tsx +147 -6
- package/src/components/iban-input.tsx +231 -0
- package/src/components/icon-picker.tsx +47 -6
- 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 +13 -0
- package/src/components/month-picker.tsx +400 -63
- package/src/components/multi-entity-combobox.tsx +207 -120
- 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/reauth-dialog.tsx +17 -18
- package/src/components/sign-chip.tsx +103 -0
- package/src/components/swatch-picker.tsx +143 -31
- package/src/components/switch.tsx +58 -7
- package/src/components/tile-radio.tsx +145 -29
- package/src/components/time-input.tsx +8 -1
- package/src/components/toggle-group.tsx +206 -73
- package/src/components/trigger-aria.ts +5 -0
- package/src/components/ui.tsx +479 -138
- package/src/data-table.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +597 -46
- package/src/feedback/feedback-dialog.tsx +9 -3
- package/src/feedback/feedback-inbox.tsx +3 -2
- package/src/feedback/feedback-thread.tsx +113 -9
- package/src/i18n/defaults.ts +18 -1
- package/src/i18n/german.ts +89 -0
- package/src/i18n/kit-labels.tsx +82 -2
- package/src/i18n/locales/en.ts +36 -4
- package/src/i18n/locales/es.ts +87 -0
- package/src/i18n/locales/fr.ts +87 -0
- package/src/i18n/locales/hu.ts +83 -0
- package/src/i18n/locales/it.ts +88 -0
- package/src/i18n/locales/zh.ts +76 -0
- package/src/i18n/review.ts +26 -0
- package/src/index.ts +71 -3
- package/src/lib/column-mapping.ts +234 -0
- 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/lib/table-text.ts +271 -0
- package/src/rhf/fields.tsx +860 -9
- package/src/rhf.ts +3 -1
- package/src/shell/topbar-action-menu.tsx +134 -38
- package/src/tour/tour.tsx +66 -28
- package/src/wizard/stepper-nav.tsx +35 -1
|
@@ -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-parts";
|
|
8
9
|
|
|
9
10
|
export interface IconPickerLabels {
|
|
10
11
|
/** The "no icon" tile's name, when `allowNone` is set. */
|
|
@@ -40,7 +41,16 @@ export interface IconOption<T extends string> {
|
|
|
40
41
|
/** A remark read after the name and shown under it in the bubble ("used by
|
|
41
42
|
* Leisure"). The tile wears a dot while it has one. */
|
|
42
43
|
note?: string;
|
|
44
|
+
/** Out of the choice and out of reach — skipped by the arrow keys and Tab. With a
|
|
45
|
+
* `disabledReason` the tile stays reachable instead and says why. */
|
|
43
46
|
disabled?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Why THIS symbol cannot be chosen right now — {@link SwatchOption.disabledReason}'s
|
|
49
|
+
* twin: the tile is `aria-disabled` but reachable by Tab and the arrow keys, the reason
|
|
50
|
+
* shows in its bubble under its name and describes it, and choosing it does nothing.
|
|
51
|
+
* Wins over `disabled`. Kept through a search like any other tile.
|
|
52
|
+
*/
|
|
53
|
+
disabledReason?: ReactNode;
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
export interface IconPickerProps<T extends string>
|
|
@@ -76,6 +86,16 @@ export interface IconPickerProps<T extends string>
|
|
|
76
86
|
tileClassName?: string;
|
|
77
87
|
disabled?: boolean;
|
|
78
88
|
labels?: Partial<IconPickerLabels>;
|
|
89
|
+
/**
|
|
90
|
+
* An 11px static label in a strip over the picker, naming the grid — keksdose K2,
|
|
91
|
+
* {@link SwatchPickerProps.label}'s twin: a symbol picker standing in a form row
|
|
92
|
+
* beside labelled fields, with its label on their line. With `searchable` the strip
|
|
93
|
+
* is over the search box too, which keeps its own name. With a label, `className`
|
|
94
|
+
* styles the wrapper.
|
|
95
|
+
*/
|
|
96
|
+
label?: ReactNode;
|
|
97
|
+
/** A {@link FieldHint} beside the label. Only with `label`. */
|
|
98
|
+
hint?: ReactNode;
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
/** Case- and accent-insensitive, so "cafe" finds "Café". */
|
|
@@ -105,6 +125,8 @@ export function IconPicker<T extends string>({
|
|
|
105
125
|
tileClassName,
|
|
106
126
|
disabled = false,
|
|
107
127
|
labels,
|
|
128
|
+
label,
|
|
129
|
+
hint,
|
|
108
130
|
className,
|
|
109
131
|
"aria-label": ariaLabel,
|
|
110
132
|
"aria-labelledby": ariaLabelledBy,
|
|
@@ -115,6 +137,8 @@ export function IconPicker<T extends string>({
|
|
|
115
137
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
116
138
|
const locale = useKitLocale();
|
|
117
139
|
const mixedId = useId();
|
|
140
|
+
const labelId = useId();
|
|
141
|
+
const labelled = hasContent(label);
|
|
118
142
|
const [query, setQuery] = useState("");
|
|
119
143
|
const needle = fold(query.trim(), locale);
|
|
120
144
|
|
|
@@ -132,11 +156,17 @@ export function IconPicker<T extends string>({
|
|
|
132
156
|
// "None" stays through a search: it is the way back to no icon, and it matches
|
|
133
157
|
// no word anybody would type for it.
|
|
134
158
|
...(allowNone ? [{ key: null, label: text.none }] : []),
|
|
135
|
-
...visible.map((o) => ({
|
|
159
|
+
...visible.map((o) => ({
|
|
160
|
+
key: o.value,
|
|
161
|
+
label: o.label,
|
|
162
|
+
note: o.note,
|
|
163
|
+
disabled: o.disabled,
|
|
164
|
+
disabledReason: o.disabledReason,
|
|
165
|
+
})),
|
|
136
166
|
];
|
|
137
167
|
|
|
138
|
-
|
|
139
|
-
<div {...rest} className={cn("space-y-2", className)}>
|
|
168
|
+
const picker = (
|
|
169
|
+
<div {...rest} className={cn("space-y-2", !labelled && className)}>
|
|
140
170
|
{searchable && (
|
|
141
171
|
<SearchField
|
|
142
172
|
value={query}
|
|
@@ -149,7 +179,8 @@ export function IconPicker<T extends string>({
|
|
|
149
179
|
<div
|
|
150
180
|
role="radiogroup"
|
|
151
181
|
aria-label={ariaLabel}
|
|
152
|
-
|
|
182
|
+
// The strip's label names the GRID, unless the caller named it already.
|
|
183
|
+
aria-labelledby={labelled && ariaLabel === undefined && ariaLabelledBy === undefined ? labelId : ariaLabelledBy}
|
|
153
184
|
aria-describedby={[ariaDescribedBy, mixed && mixedId].filter(Boolean).join(" ") || undefined}
|
|
154
185
|
aria-disabled={disabled || undefined}
|
|
155
186
|
className="flex flex-wrap items-center gap-1.5"
|
|
@@ -157,7 +188,11 @@ export function IconPicker<T extends string>({
|
|
|
157
188
|
<TileRadioGroup
|
|
158
189
|
items={items}
|
|
159
190
|
checked={mixed ? undefined : value}
|
|
160
|
-
|
|
191
|
+
// A locked tile is refused by the tiles themselves; asked again here so a
|
|
192
|
+
// reason can never become a change, whatever the tiles do.
|
|
193
|
+
onSelect={(next) => {
|
|
194
|
+
if (!hasContent(next === null ? undefined : byValue.get(next)?.disabledReason)) onChange(next);
|
|
195
|
+
}}
|
|
161
196
|
activation={activation}
|
|
162
197
|
disabled={disabled}
|
|
163
198
|
size={size}
|
|
@@ -191,4 +226,10 @@ export function IconPicker<T extends string>({
|
|
|
191
226
|
)}
|
|
192
227
|
</div>
|
|
193
228
|
);
|
|
229
|
+
if (!labelled) return picker;
|
|
230
|
+
return (
|
|
231
|
+
<LabelStrip labelId={labelId} label={label} hint={hint} disabled={disabled} pad="clear" className={className}>
|
|
232
|
+
{picker}
|
|
233
|
+
</LabelStrip>
|
|
234
|
+
);
|
|
194
235
|
}
|