@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,104 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/** One box of a {@link CheckboxGroup}. */
|
|
5
|
+
interface CheckboxGroupOption<T extends string = string> {
|
|
6
|
+
value: T;
|
|
7
|
+
/** The words beside the box — its accessible name. */
|
|
8
|
+
label: ReactNode;
|
|
9
|
+
/** A line under the label ("Imprint, Privacy Policy and Terms only"), attached to the
|
|
10
|
+
* box with `aria-describedby` — {@link Checkbox}'s `description`. */
|
|
11
|
+
hint?: ReactNode;
|
|
12
|
+
/** This box cannot be changed; the others can. */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Whether the legend is drawn. `"sr-only"` keeps it as the group's name for a screen
|
|
16
|
+
* reader where the dialog's title already says it to the eye. */
|
|
17
|
+
type CheckboxGroupLegendVisibility = "visible" | "sr-only";
|
|
18
|
+
interface CheckboxGroupProps<T extends string = string> {
|
|
19
|
+
/** The question the boxes answer — the fieldset's `<legend>`, which names the group.
|
|
20
|
+
* Without one, pass `aria-label`. */
|
|
21
|
+
legend?: ReactNode;
|
|
22
|
+
/** Default `"visible"`. */
|
|
23
|
+
legendVisibility?: CheckboxGroupLegendVisibility;
|
|
24
|
+
"aria-label"?: string;
|
|
25
|
+
/** One box per option, in this order. */
|
|
26
|
+
options: CheckboxGroupOption<T>[];
|
|
27
|
+
/** The ticked values. Controlled. */
|
|
28
|
+
value: T[];
|
|
29
|
+
/**
|
|
30
|
+
* The new set on every tick. In the ORDER OF `options`, not the order the boxes were
|
|
31
|
+
* ticked, so a list saved from it does not change with the order someone clicked in
|
|
32
|
+
* (ChoiceCardGroup's rule). A value in `value` that no option carries — a grant for a
|
|
33
|
+
* locale the app no longer offers — is kept, after the known ones: a picker that
|
|
34
|
+
* dropped what it could not show would delete it on the next unrelated tick.
|
|
35
|
+
*/
|
|
36
|
+
onChange: (value: T[]) => void;
|
|
37
|
+
/**
|
|
38
|
+
* `"vertical"` (default): one box per line. `"horizontal"`: a wrapping row, for a few
|
|
39
|
+
* short labels. With `columns`, vertical becomes a grid of that many columns from the
|
|
40
|
+
* `sm` breakpoint up and stays one column on a phone — the reviewer dialog's shape.
|
|
41
|
+
*/
|
|
42
|
+
layout?: "vertical" | "horizontal";
|
|
43
|
+
/** Columns for a vertical group, from `sm` up (one on a phone). Default 1. */
|
|
44
|
+
columns?: 1 | 2 | 3 | 4;
|
|
45
|
+
/** Shared by every box — what a plain form submits the values under. */
|
|
46
|
+
name?: string;
|
|
47
|
+
/** Every box: the fieldset's own `disabled` (which disables them all natively), each
|
|
48
|
+
* box faded as a disabled Checkbox is, and the legend with them. */
|
|
49
|
+
disabled?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* At least one box must be ticked. Draws the required star after the legend, and puts
|
|
52
|
+
* the native `required` on every box while NONE is ticked — so a form refuses an
|
|
53
|
+
* empty set — and on none once one is (ChoiceCardGroup's rule: checkboxes cannot say
|
|
54
|
+
* "at least one" natively).
|
|
55
|
+
*/
|
|
56
|
+
required?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Standing advice for the group, under the boxes, attached to the fieldset with
|
|
59
|
+
* `aria-describedby` before the error — the field anatomy every kit field keeps.
|
|
60
|
+
*/
|
|
61
|
+
hint?: ReactNode;
|
|
62
|
+
/** What is wrong with the set, in the caller's words. Under the boxes, on the
|
|
63
|
+
* fieldset's `aria-describedby`, and paints every box invalid. */
|
|
64
|
+
error?: ReactNode;
|
|
65
|
+
/** Paints every box invalid without a message (the message lives elsewhere). */
|
|
66
|
+
invalid?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* The group SAVES on change — each tick is written straight away (a reviewer's
|
|
69
|
+
* per-locale grants). Under a locked {@link WriteLockProvider} every box is locked
|
|
70
|
+
* the focusable way with the lock's reason. See {@link Checkbox}'s `commit`.
|
|
71
|
+
*/
|
|
72
|
+
commit?: boolean;
|
|
73
|
+
/** Why no box can be changed — every box gets it, and it wins over `disabled` (the
|
|
74
|
+
* boxes stay focusable to say it). See {@link Checkbox}'s `disabledReason`. */
|
|
75
|
+
disabledReason?: ReactNode;
|
|
76
|
+
/** The fieldset's id. */
|
|
77
|
+
id?: string;
|
|
78
|
+
/** Classes for the fieldset — the outermost element, where layout belongs. */
|
|
79
|
+
className?: string;
|
|
80
|
+
/** Classes for the box list (the grid or the row). */
|
|
81
|
+
listClassName?: string;
|
|
82
|
+
/** Ids of something else on the page that describes the group; merged before the
|
|
83
|
+
* group's own hint and error. */
|
|
84
|
+
"aria-describedby"?: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A set of kit {@link Checkbox}es answering one question, in a `<fieldset>`.
|
|
88
|
+
*
|
|
89
|
+
* Kurvenschmiede's reviewer dialog (features/admin/reviewer-dialog.tsx) builds this by
|
|
90
|
+
* hand — a fieldset, an `sr-only` legend, a grid of checkboxes, a `toggle(code, on)`
|
|
91
|
+
* that adds or filters a `string[]` — and keksdose's and kastlan's reviewer grants build
|
|
92
|
+
* the same picker for the same per-locale grant. The hand-built ones differ in the parts
|
|
93
|
+
* that are easy to get wrong: whether the legend names the group, whether a hint or an
|
|
94
|
+
* error is attached to anything, what order the saved list comes out in, and whether a
|
|
95
|
+
* value the picker cannot show survives a tick.
|
|
96
|
+
*
|
|
97
|
+
* Every box is a kit {@link Checkbox}, so the native checkbox semantics stay (each box
|
|
98
|
+
* submits under `name` in a plain form). The legend is the group's name; the hint and
|
|
99
|
+
* error describe the fieldset. For a choice of ONE, use radios ({@link ChoiceCardGroup},
|
|
100
|
+
* {@link ToggleGroup}).
|
|
101
|
+
*/
|
|
102
|
+
declare function CheckboxGroup<T extends string = string>({ legend, legendVisibility, "aria-label": ariaLabel, options, value, onChange, layout, columns, name, disabled, required, hint, error, invalid, commit, disabledReason, id, className, listClassName, "aria-describedby": ariaDescribedBy, }: CheckboxGroupProps<T>): react.JSX.Element;
|
|
103
|
+
|
|
104
|
+
export { CheckboxGroup, type CheckboxGroupLegendVisibility, type CheckboxGroupOption, type CheckboxGroupProps };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { Checkbox } from "./checkbox.js";
|
|
6
|
+
import { hasMessage, mergeDescribedBy } from "./choice-parts.js";
|
|
7
|
+
import { useCommitReason } from "./write-lock.js";
|
|
8
|
+
import { RequiredStarOnLegend } from "./field-parts.js";
|
|
9
|
+
const COLUMNS = {
|
|
10
|
+
1: "flex flex-col gap-2",
|
|
11
|
+
2: "grid grid-cols-1 gap-2 sm:grid-cols-2",
|
|
12
|
+
3: "grid grid-cols-1 gap-2 sm:grid-cols-3",
|
|
13
|
+
4: "grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4"
|
|
14
|
+
};
|
|
15
|
+
function CheckboxGroup({
|
|
16
|
+
legend,
|
|
17
|
+
legendVisibility = "visible",
|
|
18
|
+
"aria-label": ariaLabel,
|
|
19
|
+
options,
|
|
20
|
+
value,
|
|
21
|
+
onChange,
|
|
22
|
+
layout = "vertical",
|
|
23
|
+
columns = 1,
|
|
24
|
+
name,
|
|
25
|
+
disabled,
|
|
26
|
+
required,
|
|
27
|
+
hint,
|
|
28
|
+
error,
|
|
29
|
+
invalid,
|
|
30
|
+
commit,
|
|
31
|
+
disabledReason,
|
|
32
|
+
id,
|
|
33
|
+
className,
|
|
34
|
+
listClassName,
|
|
35
|
+
"aria-describedby": ariaDescribedBy
|
|
36
|
+
}) {
|
|
37
|
+
const generated = useId();
|
|
38
|
+
const hintId = `${generated}-hint`;
|
|
39
|
+
const errorId = `${generated}-error`;
|
|
40
|
+
const showHint = hasMessage(hint);
|
|
41
|
+
const showError = hasMessage(error);
|
|
42
|
+
const isInvalid = Boolean(invalid) || showError;
|
|
43
|
+
const ticked = new Set(value);
|
|
44
|
+
const reason = useCommitReason(commit, disabledReason);
|
|
45
|
+
const locked = hasMessage(reason);
|
|
46
|
+
const toggle = (v, on) => {
|
|
47
|
+
const next = new Set(value);
|
|
48
|
+
if (on) next.add(v);
|
|
49
|
+
else next.delete(v);
|
|
50
|
+
const known = new Set(options.map((o) => o.value));
|
|
51
|
+
onChange([
|
|
52
|
+
...options.map((o) => o.value).filter((x) => next.has(x)),
|
|
53
|
+
// Values no option carries, kept in their own order — see `onChange`.
|
|
54
|
+
...value.filter((x) => !known.has(x))
|
|
55
|
+
]);
|
|
56
|
+
};
|
|
57
|
+
return /* @__PURE__ */ jsxs(
|
|
58
|
+
"fieldset",
|
|
59
|
+
{
|
|
60
|
+
id,
|
|
61
|
+
"aria-label": ariaLabel,
|
|
62
|
+
"aria-describedby": mergeDescribedBy(ariaDescribedBy, showHint && hintId, showError && errorId),
|
|
63
|
+
disabled: disabled && !locked,
|
|
64
|
+
className: cn("relative m-0 min-w-0 border-0 p-0", className),
|
|
65
|
+
children: [
|
|
66
|
+
legend !== void 0 && /* @__PURE__ */ jsxs(
|
|
67
|
+
"legend",
|
|
68
|
+
{
|
|
69
|
+
className: legendVisibility === "sr-only" ? "sr-only" : cn("mb-2 p-0 text-sm font-medium text-[var(--text-primary)]", (disabled || locked) && "opacity-60"),
|
|
70
|
+
children: [
|
|
71
|
+
legend,
|
|
72
|
+
required && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "ms-0.5 text-[var(--danger)]", children: "*" })
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
),
|
|
76
|
+
/* @__PURE__ */ jsx(RequiredStarOnLegend.Provider, { value: true, children: /* @__PURE__ */ jsx(
|
|
77
|
+
"div",
|
|
78
|
+
{
|
|
79
|
+
className: cn(
|
|
80
|
+
layout === "horizontal" ? "flex flex-wrap gap-x-4 gap-y-2" : COLUMNS[columns],
|
|
81
|
+
listClassName
|
|
82
|
+
),
|
|
83
|
+
children: options.map((o) => /* @__PURE__ */ jsx(
|
|
84
|
+
Checkbox,
|
|
85
|
+
{
|
|
86
|
+
name,
|
|
87
|
+
value: o.value,
|
|
88
|
+
label: o.label,
|
|
89
|
+
description: o.hint,
|
|
90
|
+
checked: ticked.has(o.value),
|
|
91
|
+
onCheckedChange: (on) => toggle(o.value, on),
|
|
92
|
+
disabled: disabled || o.disabled,
|
|
93
|
+
invalid: isInvalid || void 0,
|
|
94
|
+
required: required && value.length === 0 ? true : void 0,
|
|
95
|
+
disabledReason: reason
|
|
96
|
+
},
|
|
97
|
+
o.value
|
|
98
|
+
))
|
|
99
|
+
}
|
|
100
|
+
) }),
|
|
101
|
+
showHint && // A `div`, not a `p`: a hint may be a node with a link in it.
|
|
102
|
+
/* @__PURE__ */ jsx("div", { id: hintId, className: "mt-1.5 text-[11px] leading-tight text-[var(--text-muted)]", children: hint }),
|
|
103
|
+
showError && /* @__PURE__ */ jsx("p", { id: errorId, className: "mt-1 text-[11px] leading-tight text-[var(--danger)]", children: error })
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
CheckboxGroup
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=checkbox-group.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/checkbox-group.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { Checkbox } from \"./checkbox\";\nimport { hasMessage, mergeDescribedBy } from \"./choice-parts\";\nimport { useCommitReason } from \"./write-lock\";\nimport { RequiredStarOnLegend } from \"./field-parts\";\n\n/** One box of a {@link CheckboxGroup}. */\nexport interface CheckboxGroupOption<T extends string = string> {\n value: T;\n /** The words beside the box — its accessible name. */\n label: ReactNode;\n /** A line under the label (\"Imprint, Privacy Policy and Terms only\"), attached to the\n * box with `aria-describedby` — {@link Checkbox}'s `description`. */\n hint?: ReactNode;\n /** This box cannot be changed; the others can. */\n disabled?: boolean;\n}\n\n/** Whether the legend is drawn. `\"sr-only\"` keeps it as the group's name for a screen\n * reader where the dialog's title already says it to the eye. */\nexport type CheckboxGroupLegendVisibility = \"visible\" | \"sr-only\";\n\nexport interface CheckboxGroupProps<T extends string = string> {\n /** The question the boxes answer — the fieldset's `<legend>`, which names the group.\n * Without one, pass `aria-label`. */\n legend?: ReactNode;\n /** Default `\"visible\"`. */\n legendVisibility?: CheckboxGroupLegendVisibility;\n \"aria-label\"?: string;\n /** One box per option, in this order. */\n options: CheckboxGroupOption<T>[];\n /** The ticked values. Controlled. */\n value: T[];\n /**\n * The new set on every tick. In the ORDER OF `options`, not the order the boxes were\n * ticked, so a list saved from it does not change with the order someone clicked in\n * (ChoiceCardGroup's rule). A value in `value` that no option carries — a grant for a\n * locale the app no longer offers — is kept, after the known ones: a picker that\n * dropped what it could not show would delete it on the next unrelated tick.\n */\n onChange: (value: T[]) => void;\n /**\n * `\"vertical\"` (default): one box per line. `\"horizontal\"`: a wrapping row, for a few\n * short labels. With `columns`, vertical becomes a grid of that many columns from the\n * `sm` breakpoint up and stays one column on a phone — the reviewer dialog's shape.\n */\n layout?: \"vertical\" | \"horizontal\";\n /** Columns for a vertical group, from `sm` up (one on a phone). Default 1. */\n columns?: 1 | 2 | 3 | 4;\n /** Shared by every box — what a plain form submits the values under. */\n name?: string;\n /** Every box: the fieldset's own `disabled` (which disables them all natively), each\n * box faded as a disabled Checkbox is, and the legend with them. */\n disabled?: boolean;\n /**\n * At least one box must be ticked. Draws the required star after the legend, and puts\n * the native `required` on every box while NONE is ticked — so a form refuses an\n * empty set — and on none once one is (ChoiceCardGroup's rule: checkboxes cannot say\n * \"at least one\" natively).\n */\n required?: boolean;\n /**\n * Standing advice for the group, under the boxes, attached to the fieldset with\n * `aria-describedby` before the error — the field anatomy every kit field keeps.\n */\n hint?: ReactNode;\n /** What is wrong with the set, in the caller's words. Under the boxes, on the\n * fieldset's `aria-describedby`, and paints every box invalid. */\n error?: ReactNode;\n /** Paints every box invalid without a message (the message lives elsewhere). */\n invalid?: boolean;\n /**\n * The group SAVES on change — each tick is written straight away (a reviewer's\n * per-locale grants). Under a locked {@link WriteLockProvider} every box is locked\n * the focusable way with the lock's reason. See {@link Checkbox}'s `commit`.\n */\n commit?: boolean;\n /** Why no box can be changed — every box gets it, and it wins over `disabled` (the\n * boxes stay focusable to say it). See {@link Checkbox}'s `disabledReason`. */\n disabledReason?: ReactNode;\n /** The fieldset's id. */\n id?: string;\n /** Classes for the fieldset — the outermost element, where layout belongs. */\n className?: string;\n /** Classes for the box list (the grid or the row). */\n listClassName?: string;\n /** Ids of something else on the page that describes the group; merged before the\n * group's own hint and error. */\n \"aria-describedby\"?: string;\n}\n\n// Literal class strings so Tailwind's scanner sees each one.\nconst COLUMNS: Record<NonNullable<CheckboxGroupProps[\"columns\"]>, string> = {\n 1: \"flex flex-col gap-2\",\n 2: \"grid grid-cols-1 gap-2 sm:grid-cols-2\",\n 3: \"grid grid-cols-1 gap-2 sm:grid-cols-3\",\n 4: \"grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4\",\n};\n\n/**\n * A set of kit {@link Checkbox}es answering one question, in a `<fieldset>`.\n *\n * Kurvenschmiede's reviewer dialog (features/admin/reviewer-dialog.tsx) builds this by\n * hand — a fieldset, an `sr-only` legend, a grid of checkboxes, a `toggle(code, on)`\n * that adds or filters a `string[]` — and keksdose's and kastlan's reviewer grants build\n * the same picker for the same per-locale grant. The hand-built ones differ in the parts\n * that are easy to get wrong: whether the legend names the group, whether a hint or an\n * error is attached to anything, what order the saved list comes out in, and whether a\n * value the picker cannot show survives a tick.\n *\n * Every box is a kit {@link Checkbox}, so the native checkbox semantics stay (each box\n * submits under `name` in a plain form). The legend is the group's name; the hint and\n * error describe the fieldset. For a choice of ONE, use radios ({@link ChoiceCardGroup},\n * {@link ToggleGroup}).\n */\nexport function CheckboxGroup<T extends string = string>({\n legend,\n legendVisibility = \"visible\",\n \"aria-label\": ariaLabel,\n options,\n value,\n onChange,\n layout = \"vertical\",\n columns = 1,\n name,\n disabled,\n required,\n hint,\n error,\n invalid,\n commit,\n disabledReason,\n id,\n className,\n listClassName,\n \"aria-describedby\": ariaDescribedBy,\n}: CheckboxGroupProps<T>) {\n const generated = useId();\n const hintId = `${generated}-hint`;\n const errorId = `${generated}-error`;\n const showHint = hasMessage(hint);\n const showError = hasMessage(error);\n const isInvalid = Boolean(invalid) || showError;\n const ticked = new Set(value);\n // Resolved here rather than per box: a reason has to keep the fieldset from being\n // natively `disabled` too, or it would take every box out of the tab order — the\n // thing a reason exists to prevent.\n const reason = useCommitReason(commit, disabledReason);\n const locked = hasMessage(reason);\n\n const toggle = (v: T, on: boolean) => {\n const next = new Set(value);\n if (on) next.add(v);\n else next.delete(v);\n const known = new Set(options.map((o) => o.value));\n onChange([\n ...options.map((o) => o.value).filter((x) => next.has(x)),\n // Values no option carries, kept in their own order — see `onChange`.\n ...value.filter((x) => !known.has(x)),\n ]);\n };\n\n return (\n <fieldset\n id={id}\n aria-label={ariaLabel}\n aria-describedby={mergeDescribedBy(ariaDescribedBy, showHint && hintId, showError && errorId)}\n disabled={disabled && !locked}\n // `relative` holds an `sr-only` legend (sr-only-containment.test); `min-w-0`\n // undoes a fieldset's `min-width: min-content`, which would otherwise push a long\n // label past a phone's edge instead of wrapping it.\n className={cn(\"relative m-0 min-w-0 border-0 p-0\", className)}\n >\n {legend !== undefined && (\n <legend\n className={\n legendVisibility === \"sr-only\"\n ? \"sr-only\"\n : cn(\"mb-2 p-0 text-sm font-medium text-[var(--text-primary)]\", (disabled || locked) && \"opacity-60\")\n }\n >\n {legend}\n {required && (\n <span aria-hidden className=\"ms-0.5 text-[var(--danger)]\">\n *\n </span>\n )}\n </legend>\n )}\n <RequiredStarOnLegend.Provider value={true}>\n <div\n className={cn(\n layout === \"horizontal\" ? \"flex flex-wrap gap-x-4 gap-y-2\" : COLUMNS[columns],\n listClassName,\n )}\n >\n {options.map((o) => (\n <Checkbox\n key={o.value}\n name={name}\n value={o.value}\n label={o.label}\n description={o.hint}\n checked={ticked.has(o.value)}\n onCheckedChange={(on) => toggle(o.value, on)}\n disabled={disabled || o.disabled}\n invalid={isInvalid || undefined}\n required={required && value.length === 0 ? true : undefined}\n disabledReason={reason}\n />\n ))}\n </div>\n </RequiredStarOnLegend.Provider>\n {showHint && (\n // A `div`, not a `p`: a hint may be a node with a link in it.\n <div id={hintId} className=\"mt-1.5 text-[11px] leading-tight text-[var(--text-muted)]\">\n {hint}\n </div>\n )}\n {showError && (\n <p id={errorId} className=\"mt-1 text-[11px] leading-tight text-[var(--danger)]\">\n {error}\n </p>\n )}\n </fieldset>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -44,6 +44,25 @@ interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "typ
|
|
|
44
44
|
* label WITHOUT a literal "*". No mark without a label.
|
|
45
45
|
*/
|
|
46
46
|
required?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Why the box cannot be changed — {@link Button}'s `disabledReason`, for a checkbox
|
|
49
|
+
* that SAVES on change (keksdose K3: a setting ticked straight into the server, kept
|
|
50
|
+
* behind a hand-rolled `SaveGuard` that forced a native `disabled` — out of the tab
|
|
51
|
+
* order, so the reason never reached a keyboard).
|
|
52
|
+
*
|
|
53
|
+
* With a reason the box is `aria-disabled` instead: still focusable, a click or Space
|
|
54
|
+
* changes nothing and fires no `onChange` / `onCheckedChange`, the reason is in the
|
|
55
|
+
* kit {@link Tooltip} on the box and on its `aria-describedby`, and the row fades as a
|
|
56
|
+
* disabled one does. It wins over `disabled`. Changing the lock mounts the box anew (it
|
|
57
|
+
* moves in or out of the Tooltip), as a locked Button is.
|
|
58
|
+
*/
|
|
59
|
+
disabledReason?: ReactNode;
|
|
60
|
+
/**
|
|
61
|
+
* This box COMMITS — ticking it saves. Under a locked {@link WriteLockProvider} it is
|
|
62
|
+
* locked the `disabledReason` way with the lock's reason (which wins over its own).
|
|
63
|
+
* No provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3.
|
|
64
|
+
*/
|
|
65
|
+
commit?: boolean;
|
|
47
66
|
}
|
|
48
67
|
/**
|
|
49
68
|
* A native checkbox in the kit's tokens, with an optional label, description and
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { forwardRef, useCallback, useEffect, useId, useRef } from "react";
|
|
3
|
+
import { forwardRef, useCallback, useContext, useEffect, useId, useRef } from "react";
|
|
4
4
|
import { Check, Minus } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
6
|
import { assignRef, hasMessage, mergeDescribedBy } from "./choice-parts.js";
|
|
7
|
+
import { LockedReason, RequiredStarOnLegend, useLockReason } from "./field-parts.js";
|
|
7
8
|
const BOX = "peer size-4 shrink-0 cursor-pointer appearance-none rounded-[4px] border border-[var(--border-strong)] bg-[var(--bg-surface)] shadow-sm transition-colors checked:border-[var(--brand)] checked:bg-[var(--brand)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-surface)] disabled:cursor-not-allowed";
|
|
8
9
|
const BOX_MIXED = "border-[var(--brand)] bg-[var(--brand)]";
|
|
9
10
|
const BOX_INVALID = "border-[var(--danger-border-strong)] ring-1 ring-[var(--danger-border-strong)] checked:border-[var(--danger-border-strong)] focus-visible:ring-[var(--danger)]";
|
|
@@ -21,10 +22,17 @@ const Checkbox = forwardRef(function Checkbox2({
|
|
|
21
22
|
id,
|
|
22
23
|
disabled,
|
|
23
24
|
required,
|
|
25
|
+
disabledReason,
|
|
26
|
+
commit,
|
|
27
|
+
onClick,
|
|
24
28
|
...rest
|
|
25
29
|
}, ref) {
|
|
26
30
|
const generated = useId();
|
|
27
31
|
const inputId = id ?? generated;
|
|
32
|
+
const lock = useLockReason(commit, disabledReason);
|
|
33
|
+
const locked = lock.locked;
|
|
34
|
+
const looksDisabled = Boolean(disabled) || locked;
|
|
35
|
+
const starOnLegend = useContext(RequiredStarOnLegend);
|
|
28
36
|
const descriptionId = `${inputId}-description`;
|
|
29
37
|
const errorId = `${inputId}-error`;
|
|
30
38
|
const showError = hasMessage(error);
|
|
@@ -42,27 +50,41 @@ const Checkbox = forwardRef(function Checkbox2({
|
|
|
42
50
|
useEffect(() => {
|
|
43
51
|
if (local.current) local.current.indeterminate = indeterminate;
|
|
44
52
|
});
|
|
45
|
-
const
|
|
53
|
+
const control = /* @__PURE__ */ jsxs("span", { className: cn("relative inline-flex shrink-0", bare && !locked && className), children: [
|
|
46
54
|
/* @__PURE__ */ jsx(
|
|
47
55
|
"input",
|
|
48
56
|
{
|
|
49
57
|
ref: setRef,
|
|
50
58
|
id: bare ? id : inputId,
|
|
51
|
-
disabled,
|
|
59
|
+
disabled: locked ? void 0 : disabled,
|
|
52
60
|
required,
|
|
53
61
|
...rest,
|
|
54
62
|
type: "checkbox",
|
|
63
|
+
"aria-disabled": locked || rest["aria-disabled"] || void 0,
|
|
55
64
|
"aria-invalid": isInvalid || rest["aria-invalid"] || void 0,
|
|
56
65
|
"aria-describedby": mergeDescribedBy(
|
|
57
66
|
rest["aria-describedby"],
|
|
58
67
|
showDescription && descriptionId,
|
|
68
|
+
locked && lock.reasonId,
|
|
59
69
|
showError && errorId
|
|
60
70
|
),
|
|
71
|
+
onClick: locked ? void 0 : onClick,
|
|
61
72
|
onChange: (e) => {
|
|
73
|
+
if (locked) {
|
|
74
|
+
e.currentTarget.checked = !e.currentTarget.checked;
|
|
75
|
+
e.currentTarget.indeterminate = indeterminate;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
62
78
|
onChange?.(e);
|
|
63
79
|
onCheckedChange?.(e.target.checked);
|
|
64
80
|
},
|
|
65
|
-
className: cn(
|
|
81
|
+
className: cn(
|
|
82
|
+
BOX,
|
|
83
|
+
indeterminate && BOX_MIXED,
|
|
84
|
+
isInvalid && BOX_INVALID,
|
|
85
|
+
locked && "cursor-not-allowed",
|
|
86
|
+
inputClassName
|
|
87
|
+
)
|
|
66
88
|
}
|
|
67
89
|
),
|
|
68
90
|
indeterminate ? /* @__PURE__ */ jsx(
|
|
@@ -81,6 +103,7 @@ const Checkbox = forwardRef(function Checkbox2({
|
|
|
81
103
|
}
|
|
82
104
|
)
|
|
83
105
|
] });
|
|
106
|
+
const box = locked ? /* @__PURE__ */ jsx(LockedReason, { lock, className: cn("shrink-0", bare && className), children: control }) : control;
|
|
84
107
|
if (bare) return box;
|
|
85
108
|
return /* @__PURE__ */ jsxs(
|
|
86
109
|
"div",
|
|
@@ -92,7 +115,7 @@ const Checkbox = forwardRef(function Checkbox2({
|
|
|
92
115
|
"flex items-start gap-2",
|
|
93
116
|
// The whole row fades, the way ToggleGroup and the fields do, so the label does
|
|
94
117
|
// not stay at full strength beside a box you cannot change.
|
|
95
|
-
|
|
118
|
+
looksDisabled && "opacity-60",
|
|
96
119
|
className
|
|
97
120
|
),
|
|
98
121
|
children: [
|
|
@@ -106,11 +129,11 @@ const Checkbox = forwardRef(function Checkbox2({
|
|
|
106
129
|
"block text-sm leading-5 text-[var(--text-primary)]",
|
|
107
130
|
// `select-none`: a double-click to toggle twice would otherwise select the
|
|
108
131
|
// words, and a drag that starts on them starts a native text drag.
|
|
109
|
-
|
|
132
|
+
looksDisabled ? "cursor-not-allowed" : "cursor-pointer select-none"
|
|
110
133
|
),
|
|
111
134
|
children: [
|
|
112
135
|
label,
|
|
113
|
-
required && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "ms-0.5 text-[var(--danger)]", children: "*" })
|
|
136
|
+
required && !starOnLegend && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "ms-0.5 text-[var(--danger)]", children: "*" })
|
|
114
137
|
]
|
|
115
138
|
}
|
|
116
139
|
),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/checkbox.tsx"],"sourcesContent":["import { forwardRef, useCallback, useEffect, useId, useRef } from \"react\";\nimport { Check, Minus } from \"lucide-react\";\nimport type { ChangeEvent, InputHTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { assignRef, hasMessage, mergeDescribedBy } from \"./choice-parts\";\n\n/**\n * A checkbox that is still `<input type=\"checkbox\">`.\n *\n * The three consuming apps had three answers: ~30 raw inputs in Keksdose leaning on\n * `tokens.css`'s `accent-color`, a label-plus-box `CheckboxField` in Lenkbank, and a\n * Radix `<button role=\"checkbox\">` in Kastlan. The native element is the one that\n * needs no ARIA to be a checkbox — it submits with its form, answers to `required`,\n * resets with the form and takes a `<label for>` click — so it stays underneath, and\n * only its paint is replaced (`appearance-none`, a token-coloured box, and a glyph\n * laid over it that ignores the pointer so the click still lands on the input).\n *\n * `accent-color` alone was not enough: it tints the native box but cannot change its\n * border, so an INVALID checkbox had no way to say so, and the unchecked box kept the\n * browser's own grey outline in both themes.\n */\n\n// The box. 16px, like every raw checkbox it replaces, so a migration moves nothing.\n//\n// `focus-visible`, with an offset in the surface colour: a --brand ring touching a\n// --brand-filled (checked) box would be a ring painted in the colour it sits on —\n// present in the DOM and invisible on screen. The offset is what separates the two.\nconst BOX =\n \"peer size-4 shrink-0 cursor-pointer appearance-none rounded-[4px] border border-[var(--border-strong)] bg-[var(--bg-surface)] shadow-sm transition-colors \" +\n \"checked:border-[var(--brand)] checked:bg-[var(--brand)] \" +\n \"focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-surface)] \" +\n \"disabled:cursor-not-allowed\";\n\n// A mixed box wears the checked fill whether or not the underlying input is checked:\n// `indeterminate` is a property the browser keeps separately from `checked`, and the\n// dash means \"some\", which is neither answer.\nconst BOX_MIXED = \"border-[var(--brand)] bg-[var(--brand)]\";\n\n// The same geometry as FIELD_INVALID — a border plus a 1px ring, so the highlight\n// survives fractional display scaling on every edge (see the note in ui.tsx). The\n// focus ring turns danger too, so focusing a wrong box does not repaint it as fine.\nconst BOX_INVALID =\n \"border-[var(--danger-border-strong)] ring-1 ring-[var(--danger-border-strong)] checked:border-[var(--danger-border-strong)] focus-visible:ring-[var(--danger)]\";\n\nconst ERROR_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--danger)]\";\n\nexport interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\"> {\n /**\n * The words beside the box. Rendered as a `<label for>` rather than a label that\n * wraps the input, so `description` and `error` can sit under it without becoming\n * part of the accessible NAME — a wrapping label would have the screen reader read\n * the whole paragraph as the checkbox's name.\n */\n label?: ReactNode;\n /** Secondary text under the label (\"Used when no account is chosen\"). Pointed at by\n * `aria-describedby`, merged with any the caller passed. Kastlan's\n * `CheckboxField` `description` variant. */\n description?: ReactNode;\n /**\n * \"Some but not all\" — the header box of a list whose rows are partly selected.\n * A DOM property with no HTML attribute, so it is set on the element after every\n * render; it does not touch `checked`, and a click clears it (the browser's rule,\n * and the caller's `onChange` decides what the click means).\n */\n indeterminate?: boolean;\n /** See {@link Input}'s `invalid`: paints the box and sets `aria-invalid` together,\n * so the two cannot be spelled separately. */\n invalid?: boolean;\n /** What is wrong, in the caller's words (\"Accept the terms to continue\"). Rendered\n * under the label, attached with `aria-describedby`, and implies `invalid`. */\n error?: ReactNode;\n /** The checked state as a boolean — the `(next) => …` shape Lenkbank's\n * `CheckboxField` and Radix's `onCheckedChange` both used, so a migration does not\n * have to unwrap `event.target.checked` at every call site. Fires alongside\n * `onChange`, never instead of it. */\n onCheckedChange?: (checked: boolean) => void;\n /** Classes for the `<input>` itself. `className` styles the OUTERMOST element —\n * the row when there is a label, the box's own wrapper when there is not — which\n * is where layout (`self-end pb-2` to sit on a field's baseline) belongs. */\n inputClassName?: string;\n /**\n * Must be ticked to submit — a consent, an acceptance before paying. Reaches the\n * `<input>` as the native `required` (so a `<form>` refuses to submit without it and a\n * screen reader announces \"required\"), and draws the kit's required mark after the\n * label, the same `aria-hidden` star as {@link Label}'s: the word is announced from\n * the control, so a star inside the name would only be read out as noise. Write the\n * label WITHOUT a literal \"*\". No mark without a label.\n */\n required?: boolean;\n}\n\n/**\n * A native checkbox in the kit's tokens, with an optional label, description and\n * error.\n *\n * Without `label`, `description` or `error` it renders the box and nothing else, so\n * it drops into a table cell or a caller's own `<label>` exactly as the raw input\n * did — give it an `aria-label` there. Every other prop reaches the `<input>`\n * (`checked`, `defaultChecked`, `name`, `required`, `data-*`, `aria-*`), so it works\n * controlled, uncontrolled, and inside a plain `<form>`.\n */\nexport const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(\n {\n label,\n description,\n indeterminate = false,\n invalid,\n error,\n onCheckedChange,\n onChange,\n className,\n inputClassName,\n id,\n disabled,\n required,\n ...rest\n },\n ref,\n) {\n const generated = useId();\n const inputId = id ?? generated;\n const descriptionId = `${inputId}-description`;\n const errorId = `${inputId}-error`;\n const showError = hasMessage(error);\n const showDescription = hasMessage(description);\n const isInvalid = Boolean(invalid) || showError;\n // Nothing to lay out beside the box: render the box alone, as the raw input was.\n const bare = label === undefined && !showDescription && !showError;\n\n const local = useRef<HTMLInputElement | null>(null);\n const setRef = useCallback(\n (node: HTMLInputElement | null) => {\n local.current = node;\n assignRef(ref, node);\n },\n [ref],\n );\n // After EVERY render, not only when the prop changes: a click clears the property\n // in the DOM, and a controlled caller that keeps `indeterminate` true expects the\n // dash back on the next render even though the prop never changed.\n useEffect(() => {\n if (local.current) local.current.indeterminate = indeterminate;\n });\n\n const box = (\n <span className={cn(\"relative inline-flex shrink-0\", bare && className)}>\n <input\n ref={setRef}\n id={bare ? id : inputId}\n disabled={disabled}\n required={required}\n {...rest}\n // After the spread, like Switch: a props object spread at a checkbox must not\n // be able to turn it into something else.\n type=\"checkbox\"\n // OR-ed with the spread so a caller's own `aria-invalid` survives — Input's rule.\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={mergeDescribedBy(\n rest[\"aria-describedby\"],\n showDescription && descriptionId,\n showError && errorId,\n )}\n onChange={(e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n onCheckedChange?.(e.target.checked);\n }}\n className={cn(BOX, indeterminate && BOX_MIXED, isInvalid && BOX_INVALID, inputClassName)}\n />\n {/* The glyph is drawn over the box, not inside it (an input has no children),\n and ignores the pointer so a click on the tick still toggles the input. The\n tick is shown by `peer-checked`, so an UNCONTROLLED box shows it too; the\n dash replaces it outright while `indeterminate` holds. */}\n {indeterminate ? (\n <Minus\n aria-hidden\n strokeWidth={3}\n className=\"pointer-events-none absolute inset-0 m-auto size-3 text-[var(--brand-contrast)]\"\n />\n ) : (\n <Check\n aria-hidden\n strokeWidth={3}\n className=\"pointer-events-none invisible absolute inset-0 m-auto size-3 text-[var(--brand-contrast)] peer-checked:visible\"\n />\n )}\n </span>\n );\n\n if (bare) return box;\n\n return (\n <div\n className={cn(\n // `items-start` + a 20px cell for the box: the box centres on the label's FIRST\n // line (text-sm is 20px tall), so a label that wraps keeps the box at the top\n // rather than floating to the middle of a paragraph.\n \"flex items-start gap-2\",\n // The whole row fades, the way ToggleGroup and the fields do, so the label does\n // not stay at full strength beside a box you cannot change.\n disabled && \"opacity-60\",\n className,\n )}\n >\n <span className=\"flex h-5 shrink-0 items-center\">{box}</span>\n <span className=\"min-w-0\">\n {label !== undefined && (\n <label\n htmlFor={inputId}\n className={cn(\n \"block text-sm leading-5 text-[var(--text-primary)]\",\n // `select-none`: a double-click to toggle twice would otherwise select the\n // words, and a drag that starts on them starts a native text drag.\n disabled ? \"cursor-not-allowed\" : \"cursor-pointer select-none\",\n )}\n >\n {label}\n {required && (\n <span aria-hidden className=\"ms-0.5 text-[var(--danger)]\">\n *\n </span>\n )}\n </label>\n )}\n {showDescription && (\n <span id={descriptionId} className=\"mt-0.5 block text-xs text-[var(--text-muted)]\">\n {description}\n </span>\n )}\n {showError && (\n <span id={errorId} className={cn(\"block\", ERROR_CLASS)}>\n {error}\n </span>\n )}\n </span>\n </div>\n );\n});\nCheckbox.displayName = \"Checkbox\";\n"],"mappings":"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","names":["Checkbox"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/checkbox.tsx"],"sourcesContent":["import { forwardRef, useCallback, useContext, useEffect, useId, useRef } from \"react\";\nimport { Check, Minus } from \"lucide-react\";\nimport type { ChangeEvent, InputHTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { assignRef, hasMessage, mergeDescribedBy } from \"./choice-parts\";\nimport { LockedReason, RequiredStarOnLegend, useLockReason } from \"./field-parts\";\n\n/**\n * A checkbox that is still `<input type=\"checkbox\">`.\n *\n * The three consuming apps had three answers: ~30 raw inputs in Keksdose leaning on\n * `tokens.css`'s `accent-color`, a label-plus-box `CheckboxField` in Lenkbank, and a\n * Radix `<button role=\"checkbox\">` in Kastlan. The native element is the one that\n * needs no ARIA to be a checkbox — it submits with its form, answers to `required`,\n * resets with the form and takes a `<label for>` click — so it stays underneath, and\n * only its paint is replaced (`appearance-none`, a token-coloured box, and a glyph\n * laid over it that ignores the pointer so the click still lands on the input).\n *\n * `accent-color` alone was not enough: it tints the native box but cannot change its\n * border, so an INVALID checkbox had no way to say so, and the unchecked box kept the\n * browser's own grey outline in both themes.\n */\n\n// The box. 16px, like every raw checkbox it replaces, so a migration moves nothing.\n//\n// `focus-visible`, with an offset in the surface colour: a --brand ring touching a\n// --brand-filled (checked) box would be a ring painted in the colour it sits on —\n// present in the DOM and invisible on screen. The offset is what separates the two.\nconst BOX =\n \"peer size-4 shrink-0 cursor-pointer appearance-none rounded-[4px] border border-[var(--border-strong)] bg-[var(--bg-surface)] shadow-sm transition-colors \" +\n \"checked:border-[var(--brand)] checked:bg-[var(--brand)] \" +\n \"focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-surface)] \" +\n \"disabled:cursor-not-allowed\";\n\n// A mixed box wears the checked fill whether or not the underlying input is checked:\n// `indeterminate` is a property the browser keeps separately from `checked`, and the\n// dash means \"some\", which is neither answer.\nconst BOX_MIXED = \"border-[var(--brand)] bg-[var(--brand)]\";\n\n// The same geometry as FIELD_INVALID — a border plus a 1px ring, so the highlight\n// survives fractional display scaling on every edge (see the note in ui.tsx). The\n// focus ring turns danger too, so focusing a wrong box does not repaint it as fine.\nconst BOX_INVALID =\n \"border-[var(--danger-border-strong)] ring-1 ring-[var(--danger-border-strong)] checked:border-[var(--danger-border-strong)] focus-visible:ring-[var(--danger)]\";\n\nconst ERROR_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--danger)]\";\n\nexport interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\"> {\n /**\n * The words beside the box. Rendered as a `<label for>` rather than a label that\n * wraps the input, so `description` and `error` can sit under it without becoming\n * part of the accessible NAME — a wrapping label would have the screen reader read\n * the whole paragraph as the checkbox's name.\n */\n label?: ReactNode;\n /** Secondary text under the label (\"Used when no account is chosen\"). Pointed at by\n * `aria-describedby`, merged with any the caller passed. Kastlan's\n * `CheckboxField` `description` variant. */\n description?: ReactNode;\n /**\n * \"Some but not all\" — the header box of a list whose rows are partly selected.\n * A DOM property with no HTML attribute, so it is set on the element after every\n * render; it does not touch `checked`, and a click clears it (the browser's rule,\n * and the caller's `onChange` decides what the click means).\n */\n indeterminate?: boolean;\n /** See {@link Input}'s `invalid`: paints the box and sets `aria-invalid` together,\n * so the two cannot be spelled separately. */\n invalid?: boolean;\n /** What is wrong, in the caller's words (\"Accept the terms to continue\"). Rendered\n * under the label, attached with `aria-describedby`, and implies `invalid`. */\n error?: ReactNode;\n /** The checked state as a boolean — the `(next) => …` shape Lenkbank's\n * `CheckboxField` and Radix's `onCheckedChange` both used, so a migration does not\n * have to unwrap `event.target.checked` at every call site. Fires alongside\n * `onChange`, never instead of it. */\n onCheckedChange?: (checked: boolean) => void;\n /** Classes for the `<input>` itself. `className` styles the OUTERMOST element —\n * the row when there is a label, the box's own wrapper when there is not — which\n * is where layout (`self-end pb-2` to sit on a field's baseline) belongs. */\n inputClassName?: string;\n /**\n * Must be ticked to submit — a consent, an acceptance before paying. Reaches the\n * `<input>` as the native `required` (so a `<form>` refuses to submit without it and a\n * screen reader announces \"required\"), and draws the kit's required mark after the\n * label, the same `aria-hidden` star as {@link Label}'s: the word is announced from\n * the control, so a star inside the name would only be read out as noise. Write the\n * label WITHOUT a literal \"*\". No mark without a label.\n */\n required?: boolean;\n /**\n * Why the box cannot be changed — {@link Button}'s `disabledReason`, for a checkbox\n * that SAVES on change (keksdose K3: a setting ticked straight into the server, kept\n * behind a hand-rolled `SaveGuard` that forced a native `disabled` — out of the tab\n * order, so the reason never reached a keyboard).\n *\n * With a reason the box is `aria-disabled` instead: still focusable, a click or Space\n * changes nothing and fires no `onChange` / `onCheckedChange`, the reason is in the\n * kit {@link Tooltip} on the box and on its `aria-describedby`, and the row fades as a\n * disabled one does. It wins over `disabled`. Changing the lock mounts the box anew (it\n * moves in or out of the Tooltip), as a locked Button is.\n */\n disabledReason?: ReactNode;\n /**\n * This box COMMITS — ticking it saves. Under a locked {@link WriteLockProvider} it is\n * locked the `disabledReason` way with the lock's reason (which wins over its own).\n * No provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3.\n */\n commit?: boolean;\n}\n\n/**\n * A native checkbox in the kit's tokens, with an optional label, description and\n * error.\n *\n * Without `label`, `description` or `error` it renders the box and nothing else, so\n * it drops into a table cell or a caller's own `<label>` exactly as the raw input\n * did — give it an `aria-label` there. Every other prop reaches the `<input>`\n * (`checked`, `defaultChecked`, `name`, `required`, `data-*`, `aria-*`), so it works\n * controlled, uncontrolled, and inside a plain `<form>`.\n */\nexport const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(\n {\n label,\n description,\n indeterminate = false,\n invalid,\n error,\n onCheckedChange,\n onChange,\n className,\n inputClassName,\n id,\n disabled,\n required,\n disabledReason,\n commit,\n onClick,\n ...rest\n },\n ref,\n) {\n const generated = useId();\n const inputId = id ?? generated;\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n // Locked is disabled the focusable way: the row fades and the words stop inviting a\n // click exactly as they do for `disabled`.\n const looksDisabled = Boolean(disabled) || locked;\n // In a CheckboxGroup the legend carries the star; the box keeps `required` itself.\n const starOnLegend = useContext(RequiredStarOnLegend);\n const descriptionId = `${inputId}-description`;\n const errorId = `${inputId}-error`;\n const showError = hasMessage(error);\n const showDescription = hasMessage(description);\n const isInvalid = Boolean(invalid) || showError;\n // Nothing to lay out beside the box: render the box alone, as the raw input was.\n const bare = label === undefined && !showDescription && !showError;\n\n const local = useRef<HTMLInputElement | null>(null);\n const setRef = useCallback(\n (node: HTMLInputElement | null) => {\n local.current = node;\n assignRef(ref, node);\n },\n [ref],\n );\n // After EVERY render, not only when the prop changes: a click clears the property\n // in the DOM, and a controlled caller that keeps `indeterminate` true expects the\n // dash back on the next render even though the prop never changed.\n useEffect(() => {\n if (local.current) local.current.indeterminate = indeterminate;\n });\n\n const control = (\n <span className={cn(\"relative inline-flex shrink-0\", bare && !locked && className)}>\n <input\n ref={setRef}\n id={bare ? id : inputId}\n // A reason wins over `disabled`, as on Button: the point of giving one is that\n // the box stays reachable.\n disabled={locked ? undefined : disabled}\n required={required}\n {...rest}\n // After the spread, like Switch: a props object spread at a checkbox must not\n // be able to turn it into something else.\n type=\"checkbox\"\n aria-disabled={locked || rest[\"aria-disabled\"] || undefined}\n // OR-ed with the spread so a caller's own `aria-invalid` survives — Input's rule.\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={mergeDescribedBy(\n rest[\"aria-describedby\"],\n showDescription && descriptionId,\n locked && lock.reasonId,\n showError && errorId,\n )}\n // A locked box's click is swallowed, as a locked Button's is.\n onClick={locked ? undefined : onClick}\n onChange={(e: ChangeEvent<HTMLInputElement>) => {\n if (locked) {\n // Put the tick back, and report nothing. Undone here rather than by\n // cancelling the click: a cancelled click is reverted by the browser AFTER\n // React has restored a controlled box, and jsdom reverts it by toggling, which\n // turned a restored tick back over (the suites the apps run would have seen a\n // locked box change). Setting the property also keeps React's own record of\n // the value in step, so an uncontrolled box reports its next real change.\n // Covers a click on the label and Space too — both arrive as a click.\n e.currentTarget.checked = !e.currentTarget.checked;\n e.currentTarget.indeterminate = indeterminate;\n return;\n }\n onChange?.(e);\n onCheckedChange?.(e.target.checked);\n }}\n className={cn(\n BOX,\n indeterminate && BOX_MIXED,\n isInvalid && BOX_INVALID,\n locked && \"cursor-not-allowed\",\n inputClassName,\n )}\n />\n {/* The glyph is drawn over the box, not inside it (an input has no children),\n and ignores the pointer so a click on the tick still toggles the input. The\n tick is shown by `peer-checked`, so an UNCONTROLLED box shows it too; the\n dash replaces it outright while `indeterminate` holds. */}\n {indeterminate ? (\n <Minus\n aria-hidden\n strokeWidth={3}\n className=\"pointer-events-none absolute inset-0 m-auto size-3 text-[var(--brand-contrast)]\"\n />\n ) : (\n <Check\n aria-hidden\n strokeWidth={3}\n className=\"pointer-events-none invisible absolute inset-0 m-auto size-3 text-[var(--brand-contrast)] peer-checked:visible\"\n />\n )}\n </span>\n );\n // The reason's Tooltip goes round the BOX, not the row: the row is the caller's\n // layout (`className`), and a wrapper round it would move that into a span. A bare\n // box hands its `className` to the wrapper, which is then the outermost element.\n const box = locked ? (\n <LockedReason lock={lock} className={cn(\"shrink-0\", bare && className)}>\n {control}\n </LockedReason>\n ) : (\n control\n );\n\n if (bare) return box;\n\n return (\n <div\n className={cn(\n // `items-start` + a 20px cell for the box: the box centres on the label's FIRST\n // line (text-sm is 20px tall), so a label that wraps keeps the box at the top\n // rather than floating to the middle of a paragraph.\n \"flex items-start gap-2\",\n // The whole row fades, the way ToggleGroup and the fields do, so the label does\n // not stay at full strength beside a box you cannot change.\n looksDisabled && \"opacity-60\",\n className,\n )}\n >\n <span className=\"flex h-5 shrink-0 items-center\">{box}</span>\n <span className=\"min-w-0\">\n {label !== undefined && (\n <label\n htmlFor={inputId}\n className={cn(\n \"block text-sm leading-5 text-[var(--text-primary)]\",\n // `select-none`: a double-click to toggle twice would otherwise select the\n // words, and a drag that starts on them starts a native text drag.\n looksDisabled ? \"cursor-not-allowed\" : \"cursor-pointer select-none\",\n )}\n >\n {label}\n {required && !starOnLegend && (\n <span aria-hidden className=\"ms-0.5 text-[var(--danger)]\">\n *\n </span>\n )}\n </label>\n )}\n {showDescription && (\n <span id={descriptionId} className=\"mt-0.5 block text-xs text-[var(--text-muted)]\">\n {description}\n </span>\n )}\n {showError && (\n <span id={errorId} className={cn(\"block\", ERROR_CLASS)}>\n {error}\n </span>\n )}\n </span>\n </div>\n );\n});\nCheckbox.displayName = \"Checkbox\";\n"],"mappings":"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","names":["Checkbox"]}
|
|
@@ -209,6 +209,8 @@ type ChipProps = ChipBaseProps & ({
|
|
|
209
209
|
* href, whichever element draws it.
|
|
210
210
|
*/
|
|
211
211
|
renderLink?: (props: ChipLinkProps) => ReactElement;
|
|
212
|
+
commit?: never;
|
|
213
|
+
disabledReason?: never;
|
|
212
214
|
} | {
|
|
213
215
|
/** The link shape again, with an `onClick`. `href` is a required string here,
|
|
214
216
|
* which is what lets TypeScript tell this shape from the button's: a bare
|
|
@@ -221,12 +223,28 @@ type ChipProps = ChipBaseProps & ({
|
|
|
221
223
|
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
222
224
|
/** See the link shape above. */
|
|
223
225
|
renderLink?: (props: ChipLinkProps) => ReactElement;
|
|
226
|
+
commit?: never;
|
|
227
|
+
disabledReason?: never;
|
|
224
228
|
} | {
|
|
225
229
|
href?: never;
|
|
226
230
|
renderLink?: never;
|
|
227
231
|
/** Renders the chip as a toggle button. Mutually exclusive with `href`. Receives
|
|
228
232
|
* the click, so a chip inside a clickable row can `stopPropagation()`. */
|
|
229
233
|
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
234
|
+
/**
|
|
235
|
+
* Why the toggle cannot be pressed right now — {@link Button}'s
|
|
236
|
+
* `disabledReason`, on the chip's button shape (keksdose K3: a filter or flag
|
|
237
|
+
* chip that saves on press, under a read-only lock). The chip stays focusable
|
|
238
|
+
* and `aria-disabled`, a press (and its `onRemove` ×) does nothing, and the
|
|
239
|
+
* reason shows in the kit {@link Tooltip} and describes the chip. Wins over
|
|
240
|
+
* `disabled`. A link chip navigates rather than commits, so it takes neither
|
|
241
|
+
* this nor `commit`; an inert chip has nothing to refuse.
|
|
242
|
+
*/
|
|
243
|
+
disabledReason?: ReactNode;
|
|
244
|
+
/** This chip COMMITS — pressing it saves. Under a locked
|
|
245
|
+
* {@link WriteLockProvider} it takes the `disabledReason` path with the lock's
|
|
246
|
+
* reason, as {@link Button}'s `commit` does. */
|
|
247
|
+
commit?: boolean;
|
|
230
248
|
});
|
|
231
249
|
/**
|
|
232
250
|
* Note on `onRemove` with `href`/`onClick`: a button cannot be nested inside a button or
|
package/dist/components/chip.js
CHANGED
|
@@ -7,6 +7,8 @@ import { horizontalStep } from "../lib/direction.js";
|
|
|
7
7
|
import { FIELD_INVALID } from "./ui.js";
|
|
8
8
|
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink, useKitLocale } from "../i18n/kit-labels.js";
|
|
9
9
|
import { pickLinkRenderer } from "./text-link.js";
|
|
10
|
+
import { Tooltip } from "./tooltip.js";
|
|
11
|
+
import { useCommitReason } from "./write-lock.js";
|
|
10
12
|
const TONE = {
|
|
11
13
|
neutral: {
|
|
12
14
|
idle: "border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-secondary)]",
|
|
@@ -310,9 +312,14 @@ const Chip = forwardRef(function Chip2({
|
|
|
310
312
|
onKeyDown,
|
|
311
313
|
onFocus,
|
|
312
314
|
snapEdges = false,
|
|
315
|
+
disabledReason: ownDisabledReason,
|
|
316
|
+
commit,
|
|
313
317
|
...rest
|
|
314
318
|
}, ref) {
|
|
315
319
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS, { remove: removeLabel });
|
|
320
|
+
const reasonId = useId();
|
|
321
|
+
const disabledReason = useCommitReason(commit, ownDisabledReason);
|
|
322
|
+
const locked = Boolean(onClick) && !href && hasReason(disabledReason);
|
|
316
323
|
const kitLink = useKitLink();
|
|
317
324
|
const s = SIZE[size];
|
|
318
325
|
const interactive = !!href || !!onClick;
|
|
@@ -321,7 +328,10 @@ const Chip = forwardRef(function Chip2({
|
|
|
321
328
|
const type = cn(caps && CAPS[size], variant === "solid" && "justify-center tabular-nums");
|
|
322
329
|
const surface = cn(
|
|
323
330
|
surfaceOf(tone, variant, selected),
|
|
324
|
-
disabled
|
|
331
|
+
// A reason wins over `disabled`: the focusable kind of disabled, with the cursor
|
|
332
|
+
// that says "refused" rather than "nothing here".
|
|
333
|
+
disabled && !locked && "cursor-default opacity-50",
|
|
334
|
+
locked && "cursor-not-allowed opacity-50"
|
|
325
335
|
);
|
|
326
336
|
const snap = snapEdges && !dot;
|
|
327
337
|
const snapRef = useSnapEdges(snap);
|
|
@@ -338,7 +348,7 @@ const Chip = forwardRef(function Chip2({
|
|
|
338
348
|
type,
|
|
339
349
|
radius,
|
|
340
350
|
surface,
|
|
341
|
-
interactive && !disabled && "cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110",
|
|
351
|
+
interactive && !disabled && !locked && "cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110",
|
|
342
352
|
// An inert dot chip is text in a column: no inset, so it aligns with the header.
|
|
343
353
|
dot && !interactive && "px-0",
|
|
344
354
|
// The under-4px a snap adds is split between the two sides, not stacked at the end.
|
|
@@ -381,15 +391,20 @@ const Chip = forwardRef(function Chip2({
|
|
|
381
391
|
onClick: (e) => {
|
|
382
392
|
e.preventDefault();
|
|
383
393
|
e.stopPropagation();
|
|
394
|
+
if (locked) return;
|
|
384
395
|
onRemove();
|
|
385
396
|
},
|
|
386
|
-
disabled: disabled || removeDisabled,
|
|
397
|
+
disabled: disabled && !locked || removeDisabled,
|
|
398
|
+
"aria-disabled": locked || void 0,
|
|
399
|
+
"aria-describedby": locked ? reasonId : void 0,
|
|
387
400
|
className: cn(
|
|
388
401
|
// Logical margins: the × sits at the END of the pill, which is the left in RTL.
|
|
389
402
|
"-me-0.5 ms-0.5 shrink-0 p-0.5 transition-colors",
|
|
390
403
|
shape === "square" ? "rounded-sm" : "rounded-full",
|
|
391
|
-
"
|
|
392
|
-
|
|
404
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
|
|
405
|
+
!locked && "hover:bg-[var(--bg-active)]",
|
|
406
|
+
locked && "cursor-not-allowed",
|
|
407
|
+
(disabled && !locked || removeDisabled) && "pointer-events-none",
|
|
393
408
|
// A disabled chip is already dimmed as a whole; dimming the × again on top
|
|
394
409
|
// would take it to a quarter. `removeDisabled` alone dims just the ×.
|
|
395
410
|
removeDisabled && !disabled && "opacity-40"
|
|
@@ -407,8 +422,9 @@ const Chip = forwardRef(function Chip2({
|
|
|
407
422
|
CHIP_RING,
|
|
408
423
|
s.split,
|
|
409
424
|
type,
|
|
410
|
-
!disabled && "cursor-pointer hover:bg-current/10",
|
|
411
|
-
disabled && "cursor-default"
|
|
425
|
+
!disabled && !locked && "cursor-pointer hover:bg-current/10",
|
|
426
|
+
disabled && !locked && "cursor-default",
|
|
427
|
+
locked && "cursor-not-allowed"
|
|
412
428
|
);
|
|
413
429
|
if (href && !disabled) {
|
|
414
430
|
const onLinkClick = onClick;
|
|
@@ -442,22 +458,40 @@ const Chip = forwardRef(function Chip2({
|
|
|
442
458
|
}
|
|
443
459
|
if (onClick && !href) {
|
|
444
460
|
const onButtonClick = onClick;
|
|
461
|
+
const ownDescribedBy = rest["aria-describedby"];
|
|
445
462
|
const button = /* @__PURE__ */ jsx(
|
|
446
463
|
"button",
|
|
447
464
|
{
|
|
448
465
|
ref: remove ? ref : ownRef,
|
|
449
466
|
type: "button",
|
|
450
|
-
onClick:
|
|
451
|
-
|
|
467
|
+
onClick: (e) => {
|
|
468
|
+
if (locked) {
|
|
469
|
+
e.preventDefault();
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
onButtonClick(e);
|
|
473
|
+
},
|
|
474
|
+
disabled: disabled && !locked,
|
|
452
475
|
role: isCheckbox ? "checkbox" : void 0,
|
|
453
476
|
"aria-pressed": isCheckbox ? void 0 : selected,
|
|
454
477
|
"aria-checked": isCheckbox ? !!selected : void 0,
|
|
455
478
|
className: remove ? inner : look,
|
|
456
479
|
...rest,
|
|
480
|
+
"aria-disabled": locked || rest["aria-disabled"],
|
|
481
|
+
"aria-describedby": locked ? [ownDescribedBy, reasonId].filter(Boolean).join(" ") : ownDescribedBy,
|
|
457
482
|
children: body
|
|
458
483
|
}
|
|
459
484
|
);
|
|
460
|
-
|
|
485
|
+
const shaped = remove ? pill(button) : button;
|
|
486
|
+
if (!locked) return shaped;
|
|
487
|
+
return (
|
|
488
|
+
// The chip in a FRAGMENT, as a locked Button is: the bubble is visual, and the
|
|
489
|
+
// hidden copy beside it is what the chip (and its ×) is described by.
|
|
490
|
+
/* @__PURE__ */ jsx(Tooltip, { label: disabledReason, children: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
491
|
+
shaped,
|
|
492
|
+
/* @__PURE__ */ jsx("span", { id: reasonId, hidden: true, children: disabledReason })
|
|
493
|
+
] }) })
|
|
494
|
+
);
|
|
461
495
|
}
|
|
462
496
|
return (
|
|
463
497
|
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- the handlers are
|
|
@@ -481,6 +515,9 @@ const Chip = forwardRef(function Chip2({
|
|
|
481
515
|
)
|
|
482
516
|
);
|
|
483
517
|
});
|
|
518
|
+
function hasReason(node) {
|
|
519
|
+
return node !== void 0 && node !== null && node !== false && node !== "";
|
|
520
|
+
}
|
|
484
521
|
function RenderedLink({
|
|
485
522
|
render,
|
|
486
523
|
...props
|