@eifi1/ui-kit 0.35.0 → 0.37.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 +35 -5
- package/dist/admin/role-select.d.ts +3 -2
- package/dist/admin/role-select.js +2 -0
- package/dist/admin/role-select.js.map +1 -1
- package/dist/components/amount-input.d.ts +2 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +2 -2
- package/dist/components/autocomplete.js +2 -1
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-look.d.ts +1 -1
- package/dist/components/button-look.js +9 -4
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/checkbox-group.d.ts +5 -2
- package/dist/components/checkbox-group.js +15 -4
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/column-mapper.d.ts +5 -3
- package/dist/components/column-mapper.js +2 -0
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox.d.ts +2 -2
- package/dist/components/combobox.js +2 -1
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/country-select.d.ts +3 -3
- package/dist/components/country-select.js +3 -0
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-select.d.ts +3 -2
- package/dist/components/currency-select.js +10 -1
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/date-field.d.ts +2 -1
- package/dist/components/date-field.js.map +1 -1
- package/dist/components/date-picker.js +3 -0
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.js +3 -0
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +2 -2
- package/dist/components/entity-combobox.js +2 -1
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/field-parts.d.ts +66 -11
- package/dist/components/field-parts.js +32 -10
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-strip.d.ts +3 -2
- package/dist/components/field-strip.js +2 -1
- package/dist/components/field-strip.js.map +1 -1
- package/dist/components/field.d.ts +3 -2
- package/dist/components/field.js +24 -15
- package/dist/components/field.js.map +1 -1
- package/dist/components/help-popover.d.ts +9 -7
- package/dist/components/help-popover.js.map +1 -1
- package/dist/components/icon-picker.d.ts +3 -2
- package/dist/components/icon-picker.js +3 -0
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/inline-entity-combobox.d.ts +2 -2
- package/dist/components/inline-entity-combobox.js +2 -1
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.d.ts +2 -2
- package/dist/components/input.js +2 -1
- package/dist/components/input.js.map +1 -1
- package/dist/components/month-picker.d.ts +2 -1
- package/dist/components/month-picker.js +8 -1
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +2 -2
- package/dist/components/multi-entity-combobox.js +2 -1
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +2 -2
- package/dist/components/multi-select.js +3 -0
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-input.d.ts +2 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numeric-field.d.ts +5 -2
- package/dist/components/numeric-field.js +4 -1
- package/dist/components/numeric-field.js.map +1 -1
- package/dist/components/one-time-code-input.d.ts +2 -1
- package/dist/components/one-time-code-input.js +2 -1
- package/dist/components/one-time-code-input.js.map +1 -1
- package/dist/components/phone-input.d.ts +1 -1
- package/dist/components/phone-input.js +4 -0
- package/dist/components/phone-input.js.map +1 -1
- package/dist/components/popover.js +1 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/select.d.ts +4 -3
- package/dist/components/select.js +2 -1
- package/dist/components/select.js.map +1 -1
- package/dist/components/slider.d.ts +3 -2
- package/dist/components/slider.js +7 -1
- package/dist/components/slider.js.map +1 -1
- package/dist/components/stat-tile.js +2 -11
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +3 -2
- package/dist/components/swatch-picker.js +3 -0
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/text-link.js +3 -1
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/textarea.d.ts +2 -1
- package/dist/components/textarea.js +9 -3
- package/dist/components/textarea.js.map +1 -1
- package/dist/components/toggle-group.d.ts +2 -2
- package/dist/components/toggle-group.js +7 -3
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/eslint/role-utilities.d.ts +34 -2
- package/dist/eslint/role-utilities.js +30 -9
- package/dist/eslint/role-utilities.js.map +1 -1
- package/dist/i18n/defaults.js +5 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +97 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +16 -0
- package/dist/i18n/kit-labels.js +21 -1
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +103 -2
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +95 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +96 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +95 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +96 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +95 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +13 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js +3 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +21 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/device-settings.d.ts +72 -0
- package/dist/lib/device-settings.js +64 -0
- package/dist/lib/device-settings.js.map +1 -0
- package/dist/lib/plain-text.d.ts +11 -0
- package/dist/lib/plain-text.js +16 -0
- package/dist/lib/plain-text.js.map +1 -0
- package/dist/lib/safe-storage.d.ts +6 -0
- package/dist/lib/safe-storage.js +11 -0
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/notifications/notification-parts.d.ts +39 -0
- package/dist/notifications/notification-parts.js +47 -0
- package/dist/notifications/notification-parts.js.map +1 -0
- package/dist/notifications/notification-types-setting.d.ts +62 -0
- package/dist/notifications/notification-types-setting.js +192 -0
- package/dist/notifications/notification-types-setting.js.map +1 -0
- package/dist/notifications/notification-types.d.ts +75 -0
- package/dist/notifications/notification-types.js +2 -0
- package/dist/notifications/notification-types.js.map +1 -0
- package/dist/notifications/notifications-labels.d.ts +131 -0
- package/dist/notifications/notifications-labels.js +83 -0
- package/dist/notifications/notifications-labels.js.map +1 -0
- package/dist/notifications/push-device-setting.d.ts +37 -0
- package/dist/notifications/push-device-setting.js +164 -0
- package/dist/notifications/push-device-setting.js.map +1 -0
- package/dist/notifications/push-devices-setting.d.ts +44 -0
- package/dist/notifications/push-devices-setting.js +203 -0
- package/dist/notifications/push-devices-setting.js.map +1 -0
- package/dist/notifications/quiet-hours-setting.d.ts +47 -0
- package/dist/notifications/quiet-hours-setting.js +131 -0
- package/dist/notifications/quiet-hours-setting.js.map +1 -0
- package/dist/notifications/time-zone-select.d.ts +50 -0
- package/dist/notifications/time-zone-select.js +76 -0
- package/dist/notifications/time-zone-select.js.map +1 -0
- package/dist/notifications/unsubscribe-panel.d.ts +53 -0
- package/dist/notifications/unsubscribe-panel.js +139 -0
- package/dist/notifications/unsubscribe-panel.js.map +1 -0
- package/dist/notifications/use-push-device.d.ts +122 -0
- package/dist/notifications/use-push-device.js +230 -0
- package/dist/notifications/use-push-device.js.map +1 -0
- package/dist/rhf/fields.d.ts +26 -19
- package/dist/rhf/fields.js +51 -22
- package/dist/rhf/fields.js.map +1 -1
- package/dist/settings/device-reset-setting-labels.d.ts +3 -0
- package/dist/settings/device-reset-setting-labels.js +15 -0
- package/dist/settings/device-reset-setting-labels.js.map +1 -0
- package/dist/settings/device-reset-setting.d.ts +58 -0
- package/dist/settings/device-reset-setting.js +51 -0
- package/dist/settings/device-reset-setting.js.map +1 -0
- package/dist/shell/app-shell.js +5 -0
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/theme/contrast-tokens.d.ts +42 -4
- package/dist/theme/contrast-tokens.js +56 -9
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/contrast.d.ts +4 -2
- package/dist/theme/contrast.js +13 -0
- package/dist/theme/contrast.js.map +1 -1
- package/dist/theme/palette-derive.d.ts +15 -1
- package/dist/theme/palette-derive.js +37 -8
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/theme/palette-presets.d.ts +2 -1
- package/dist/theme/palette-presets.js +5 -5
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js +6 -0
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js +6 -0
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +6 -0
- package/dist/theme/theme-store.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/role-select.tsx +4 -1
- package/src/components/amount-input.tsx +2 -1
- package/src/components/autocomplete.tsx +5 -2
- package/src/components/button-look.ts +15 -5
- package/src/components/checkbox-group.tsx +22 -4
- package/src/components/column-mapper.tsx +6 -2
- package/src/components/combobox.tsx +5 -2
- package/src/components/country-select.tsx +6 -1
- package/src/components/currency-select.tsx +12 -3
- package/src/components/date-field.tsx +2 -2
- package/src/components/date-picker.tsx +3 -0
- package/src/components/date-range-picker.tsx +3 -0
- package/src/components/entity-combobox.tsx +5 -2
- package/src/components/field-parts.tsx +107 -19
- package/src/components/field-strip.tsx +4 -3
- package/src/components/field.tsx +28 -15
- package/src/components/help-popover.tsx +9 -7
- package/src/components/icon-picker.tsx +8 -3
- package/src/components/inline-entity-combobox.tsx +5 -2
- package/src/components/input.tsx +4 -2
- package/src/components/month-picker.tsx +10 -3
- package/src/components/multi-entity-combobox.tsx +5 -2
- package/src/components/multi-select.tsx +6 -1
- package/src/components/number-input.tsx +2 -2
- package/src/components/numeric-field.ts +10 -2
- package/src/components/one-time-code-input.tsx +5 -3
- package/src/components/phone-input.tsx +4 -0
- package/src/components/popover.tsx +3 -1
- package/src/components/select.tsx +6 -3
- package/src/components/slider.tsx +10 -3
- package/src/components/stat-tile.tsx +3 -18
- package/src/components/swatch-picker.tsx +9 -4
- package/src/components/text-link.tsx +3 -1
- package/src/components/textarea.tsx +11 -4
- package/src/components/toggle-group.tsx +21 -5
- package/src/eslint/role-utilities.ts +67 -11
- package/src/i18n/defaults.ts +4 -0
- package/src/i18n/german.ts +113 -0
- package/src/i18n/kit-labels.tsx +52 -1
- package/src/i18n/locales/en.ts +6 -0
- package/src/i18n/locales/es.ts +106 -0
- package/src/i18n/locales/fr.ts +106 -0
- package/src/i18n/locales/hu.ts +104 -0
- package/src/i18n/locales/it.ts +105 -0
- package/src/i18n/locales/zh.ts +96 -0
- package/src/i18n/review.ts +13 -0
- package/src/i18n/shared-labels.ts +2 -0
- package/src/index.ts +50 -0
- package/src/lib/device-settings.ts +146 -0
- package/src/lib/plain-text.ts +22 -0
- package/src/lib/safe-storage.ts +16 -0
- package/src/notifications/notification-parts.tsx +83 -0
- package/src/notifications/notification-types-setting.tsx +309 -0
- package/src/notifications/notification-types.ts +80 -0
- package/src/notifications/notifications-labels.ts +238 -0
- package/src/notifications/push-device-setting.tsx +224 -0
- package/src/notifications/push-devices-setting.tsx +306 -0
- package/src/notifications/quiet-hours-setting.tsx +185 -0
- package/src/notifications/time-zone-select.tsx +126 -0
- package/src/notifications/unsubscribe-panel.tsx +203 -0
- package/src/notifications/use-push-device.ts +392 -0
- package/src/rhf/fields.tsx +77 -29
- package/src/settings/device-reset-setting-labels.ts +20 -0
- package/src/settings/device-reset-setting.tsx +119 -0
- package/src/shell/app-shell.tsx +8 -0
- package/src/theme/contrast-tokens.ts +106 -12
- package/src/theme/contrast.ts +30 -4
- package/src/theme/palette-derive.ts +68 -7
- package/src/theme/palette-presets.ts +9 -6
- package/src/theme/palette-store.ts +12 -0
- package/src/theme/text-size.ts +8 -0
- package/src/theme/theme-store.ts +10 -0
- package/tokens.css +38 -4
|
@@ -3,8 +3,11 @@ import type { ComponentPropsWithoutRef, ComponentPropsWithRef, ReactNode } from
|
|
|
3
3
|
import { HelpCircle } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { hasContent } from "../lib/presence";
|
|
6
|
+
import { plainText } from "../lib/plain-text";
|
|
7
|
+
import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
6
8
|
import { useInlineFacts } from "../hooks/use-large-text";
|
|
7
9
|
import { FOCUS_RING } from "./focus-ring";
|
|
10
|
+
import { HelpPopover } from "./help-popover";
|
|
8
11
|
import { Tooltip, type TooltipSide } from "./tooltip";
|
|
9
12
|
import { useCommitReason, type CommitScope } from "./write-lock";
|
|
10
13
|
|
|
@@ -202,16 +205,82 @@ export function FieldHint({
|
|
|
202
205
|
* The kit's fields call it through {@link useFieldHint} / {@link useFieldMessages}; a
|
|
203
206
|
* field that resolves its own `hint` (an app's, or one of the kit's own outside this
|
|
204
207
|
* file) calls it first and then treats the result as it treated a text hint.
|
|
208
|
+
*
|
|
209
|
+
* `withHelp` (round 0.36): the field has a `help`, whose "?" is the field's only one
|
|
210
|
+
* ({@link fieldHelpMark}) — so a FieldHint is its words at EVERY size, not only at Large
|
|
211
|
+
* and on touch.
|
|
205
212
|
*/
|
|
206
|
-
export function useInlineHint(hint: ReactNode): ReactNode {
|
|
213
|
+
export function useInlineHint(hint: ReactNode, withHelp = false): ReactNode {
|
|
207
214
|
const inline = useInlineFacts();
|
|
208
|
-
if (inline && isValidElement(hint) && hint.type === FieldHint) {
|
|
215
|
+
if ((inline || withHelp) && isValidElement(hint) && hint.type === FieldHint) {
|
|
209
216
|
const words = (hint.props as FieldHintProps).label;
|
|
210
217
|
if (words) return words;
|
|
211
218
|
}
|
|
212
219
|
return hint;
|
|
213
220
|
}
|
|
214
221
|
|
|
222
|
+
/* ── help (round 0.36, docs/text-size-harmonization.md §4, §10.23) ─────────── */
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The doc every field's `help` prop points at.
|
|
226
|
+
*
|
|
227
|
+
* A field may carry two kinds of words (§10.23): a FACT — a format, a limit, a
|
|
228
|
+
* consequence — which stays visible, and HELP — why the field exists, how to choose,
|
|
229
|
+
* what the options mean — which the field is usable without and which may sit behind a
|
|
230
|
+
* "?". Until 0.36 a field had one slot, `hint`, so a field with both had to drop one or
|
|
231
|
+
* print a paragraph (Kurvenschmiede's "How it was measured", round 0.36 H). Now:
|
|
232
|
+
*
|
|
233
|
+
* - `hint` is the fact, the caption under the field, on its `aria-describedby`;
|
|
234
|
+
* - `help` is the help, a {@link HelpPopover} "?" on the label line (an unlabelled
|
|
235
|
+
* field's at its end edge, as a hint's), named "About “<label>”" (`common.aboutField`,
|
|
236
|
+
* from the label's text, else the `aria-label`, else `common.help`). Pass a
|
|
237
|
+
* `HelpPopover` as `help` to name it yourself ("What do the statuses mean?").
|
|
238
|
+
*
|
|
239
|
+
* The help's "?" is then the field's ONLY "?": a `FieldHint` passed as `hint` beside it
|
|
240
|
+
* draws none of its own and shows its words as the caption at every size, as it does at
|
|
241
|
+
* Large and on touch; any other `hint` is the caption too. Two "?" side by side would
|
|
242
|
+
* look the same and open different things.
|
|
243
|
+
*/
|
|
244
|
+
export interface FieldHelpProps {
|
|
245
|
+
/**
|
|
246
|
+
* Help the field is usable without — why it exists, how to choose, what the options
|
|
247
|
+
* mean — behind the field's one "?" on its label line: a {@link HelpPopover} named
|
|
248
|
+
* "About “<label>”" that opens on a tap (round 0.36, docs/text-size-harmonization.md §4).
|
|
249
|
+
* Pass a `HelpPopover` to name it yourself. `hint` stays the visible fact: with `help`
|
|
250
|
+
* set it is the caption under the field at every size, a `FieldHint` included.
|
|
251
|
+
*/
|
|
252
|
+
help?: ReactNode;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The "?" for a field's `help` — the field's own name, "About “<label>”", in the reader's
|
|
257
|
+
* language. A component so that a field without `help` asks the labels nothing.
|
|
258
|
+
*/
|
|
259
|
+
function FieldHelp({ name, children }: { name: ReactNode; children: ReactNode }) {
|
|
260
|
+
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
261
|
+
const text = plainText(name);
|
|
262
|
+
return <HelpPopover label={text === undefined ? common.help : common.aboutField(text)}>{children}</HelpPopover>;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* A field's `help` as the "?" for its label line, or `undefined` when there is none. A
|
|
267
|
+
* {@link HelpPopover} is used as it is (it carries its own name); anything else is put
|
|
268
|
+
* in one named "About “<name>”" — `name` is the field's label, else its `aria-label`.
|
|
269
|
+
*/
|
|
270
|
+
export function fieldHelpMark(help: ReactNode, name: ReactNode): ReactNode | undefined {
|
|
271
|
+
if (!hasContent(help)) return undefined;
|
|
272
|
+
if (isValidElement(help) && help.type === HelpPopover) return help;
|
|
273
|
+
return <FieldHelp name={name}>{help}</FieldHelp>;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** What {@link useFieldHint} needs to draw a field's `help`. */
|
|
277
|
+
export interface FieldHelpSource {
|
|
278
|
+
/** The field's `help`. */
|
|
279
|
+
help?: ReactNode;
|
|
280
|
+
/** What names its "?": the field's label, else its `aria-label`. */
|
|
281
|
+
name?: ReactNode;
|
|
282
|
+
}
|
|
283
|
+
|
|
215
284
|
|
|
216
285
|
/**
|
|
217
286
|
* Whether a `hint` is a CAPTION (plain text: a non-empty string or a number) or
|
|
@@ -236,26 +305,43 @@ export interface FieldHintParts {
|
|
|
236
305
|
describedBy: string | undefined;
|
|
237
306
|
/** The caption's id while there is one. */
|
|
238
307
|
captionId: string | undefined;
|
|
239
|
-
/** The caption itself, or
|
|
240
|
-
captionText:
|
|
241
|
-
/**
|
|
242
|
-
*
|
|
308
|
+
/** The caption itself — a text hint, or any hint beside a `help` — or `undefined`. */
|
|
309
|
+
captionText: ReactNode | undefined;
|
|
310
|
+
/** What rides the label line — the `help`'s "?" ({@link fieldHelpMark}), else a
|
|
311
|
+
* non-text hint (a FieldHint) — or, on a field with no label line, the end edge
|
|
312
|
+
* outside the box. `undefined` when there is none. */
|
|
243
313
|
labelHint: ReactNode | undefined;
|
|
244
314
|
}
|
|
245
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Whether a resolved `hint` goes under the field as its caption: text always (see
|
|
318
|
+
* {@link isTextHint}); with a `help` beside it, whatever it is — the label line's "?"
|
|
319
|
+
* is the help's (round 0.36, §4).
|
|
320
|
+
*/
|
|
321
|
+
export function isHintCaption(hint: ReactNode, withHelp: boolean): boolean {
|
|
322
|
+
return isTextHint(hint) || (withHelp && hasContent(hint));
|
|
323
|
+
}
|
|
324
|
+
|
|
246
325
|
/**
|
|
247
326
|
* Split a field's `hint` into the caption under it (attached with `aria-describedby`)
|
|
248
|
-
* or the "?" on its label line. See {@link isTextHint}.
|
|
327
|
+
* or the "?" on its label line. See {@link isTextHint}. With a `help` ({@link
|
|
328
|
+
* FieldHelpProps}), the help's "?" takes the label line and the hint is the caption.
|
|
249
329
|
*/
|
|
250
|
-
export function useFieldHint(
|
|
251
|
-
|
|
330
|
+
export function useFieldHint(
|
|
331
|
+
hintProp: ReactNode,
|
|
332
|
+
describedBy: string | undefined,
|
|
333
|
+
{ help, name }: FieldHelpSource = {},
|
|
334
|
+
): FieldHintParts {
|
|
335
|
+
const helpMark = fieldHelpMark(help, name);
|
|
336
|
+
const withHelp = helpMark !== undefined;
|
|
337
|
+
const hint = useInlineHint(hintProp, withHelp);
|
|
252
338
|
const captionId = useId();
|
|
253
|
-
const
|
|
339
|
+
const caption = isHintCaption(hint, withHelp);
|
|
254
340
|
return {
|
|
255
|
-
describedBy:
|
|
256
|
-
captionId:
|
|
257
|
-
captionText:
|
|
258
|
-
labelHint: !
|
|
341
|
+
describedBy: caption ? mergeDescribedBy(describedBy, captionId) : describedBy,
|
|
342
|
+
captionId: caption ? captionId : undefined,
|
|
343
|
+
captionText: caption ? hint : undefined,
|
|
344
|
+
labelHint: helpMark ?? (!caption && hasContent(hint) ? hint : undefined),
|
|
259
345
|
};
|
|
260
346
|
}
|
|
261
347
|
|
|
@@ -575,24 +661,26 @@ export interface FieldMessages {
|
|
|
575
661
|
describedBy: string | undefined;
|
|
576
662
|
/** The caption and the error, for under the field; `null` when there is neither. */
|
|
577
663
|
below: ReactNode;
|
|
578
|
-
/**
|
|
579
|
-
* caption
|
|
664
|
+
/** What rides the LABEL line — the `help`'s "?", else a `FieldHint` — or `undefined`
|
|
665
|
+
* when the hint is a caption and there is no help. */
|
|
580
666
|
labelHint: ReactNode;
|
|
581
667
|
}
|
|
582
668
|
|
|
583
669
|
/**
|
|
584
|
-
* `hint` / `error` / `invalid` resolved the way `Select` resolves them —
|
|
670
|
+
* `hint` / `help` / `error` / `invalid` resolved the way `Select` resolves them —
|
|
585
671
|
* {@link useFieldHint} and {@link useFieldError} together, for a field that draws what
|
|
586
672
|
* goes under it in one place (a picker, a group, a field built on another).
|
|
587
673
|
*/
|
|
588
674
|
export function useFieldMessages({
|
|
589
675
|
hint,
|
|
676
|
+
help,
|
|
677
|
+
name,
|
|
590
678
|
error,
|
|
591
679
|
invalid,
|
|
592
680
|
describedBy,
|
|
593
681
|
ariaInvalid,
|
|
594
682
|
reasonId,
|
|
595
|
-
}: {
|
|
683
|
+
}: FieldHelpSource & {
|
|
596
684
|
hint?: ReactNode;
|
|
597
685
|
error?: ReactNode;
|
|
598
686
|
invalid?: boolean;
|
|
@@ -602,7 +690,7 @@ export function useFieldMessages({
|
|
|
602
690
|
* caption and before the error, as on Select. */
|
|
603
691
|
reasonId?: string | false;
|
|
604
692
|
}): FieldMessages {
|
|
605
|
-
const hintParts = useFieldHint(hint, describedBy);
|
|
693
|
+
const hintParts = useFieldHint(hint, describedBy, { help, name });
|
|
606
694
|
const field = useFieldError(error, invalid, mergeDescribedBy(hintParts.describedBy, reasonId), ariaInvalid);
|
|
607
695
|
return {
|
|
608
696
|
isInvalid: field.isInvalid,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useId } from "react";
|
|
2
2
|
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
3
|
-
import { LabelStrip, useFieldMessages } from "./field-parts";
|
|
3
|
+
import { LabelStrip, useFieldMessages, type FieldHelpProps } from "./field-parts";
|
|
4
4
|
|
|
5
5
|
/** What a {@link FieldStrip} hands render-prop children. */
|
|
6
6
|
export interface FieldStripIds {
|
|
@@ -18,7 +18,7 @@ export interface FieldStripIds {
|
|
|
18
18
|
* it in 0.34, API-2): the ref is the root — the group, or the plain box under
|
|
19
19
|
* `group={false}` — which a caller scrolls into view or measures.
|
|
20
20
|
*/
|
|
21
|
-
export interface FieldStripProps extends Omit<ComponentPropsWithRef<"div">, "children"
|
|
21
|
+
export interface FieldStripProps extends Omit<ComponentPropsWithRef<"div">, "children">, FieldHelpProps {
|
|
22
22
|
/** The 11px static label, on the line a labelled field's label sits on. */
|
|
23
23
|
label: ReactNode;
|
|
24
24
|
/**
|
|
@@ -86,6 +86,7 @@ export interface FieldStripProps extends Omit<ComponentPropsWithRef<"div">, "chi
|
|
|
86
86
|
export function FieldStrip({
|
|
87
87
|
label,
|
|
88
88
|
hint,
|
|
89
|
+
help,
|
|
89
90
|
error,
|
|
90
91
|
disabled = false,
|
|
91
92
|
group = true,
|
|
@@ -94,7 +95,7 @@ export function FieldStrip({
|
|
|
94
95
|
...rest
|
|
95
96
|
}: FieldStripProps) {
|
|
96
97
|
const labelId = useId();
|
|
97
|
-
const messages = useFieldMessages({ hint, error });
|
|
98
|
+
const messages = useFieldMessages({ hint, help, name: label, error });
|
|
98
99
|
const content = typeof children === "function" ? children({ labelId, describedBy: messages.describedBy }) : children;
|
|
99
100
|
// The caller's own description first, then the caption, then the error — the order
|
|
100
101
|
// every kit field reads them in.
|
package/src/components/field.tsx
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
import { useId, type ReactNode } from "react";
|
|
41
41
|
import { cn } from "../lib/cn";
|
|
42
42
|
import { hasContent } from "../lib/presence";
|
|
43
|
-
import { useInlineHint } from "./field-parts";
|
|
43
|
+
import { fieldHelpMark, useInlineHint, type FieldHelpProps } from "./field-parts";
|
|
44
44
|
import { Label } from "./field-base";
|
|
45
45
|
|
|
46
46
|
/** What a `Field` hands its render-prop children, to spread on the control. */
|
|
@@ -91,7 +91,7 @@ const LABEL_VISIBILITY: Record<FieldLabelVisibility, string | undefined> = {
|
|
|
91
91
|
"below-lg": "lg:sr-only",
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
-
export interface FieldProps {
|
|
94
|
+
export interface FieldProps extends FieldHelpProps {
|
|
95
95
|
/** The label above the control. Omit it for a group whose control is labelled
|
|
96
96
|
* some other way; the hint and error still render. */
|
|
97
97
|
label?: ReactNode;
|
|
@@ -123,6 +123,7 @@ export function Field({
|
|
|
123
123
|
label,
|
|
124
124
|
required,
|
|
125
125
|
hint: hintProp,
|
|
126
|
+
help,
|
|
126
127
|
error,
|
|
127
128
|
htmlFor,
|
|
128
129
|
disabled,
|
|
@@ -138,7 +139,10 @@ export function Field({
|
|
|
138
139
|
const labelId = `${generated}-label`;
|
|
139
140
|
// §4: a FieldHint under the control is its words at Large and on a touch screen, not
|
|
140
141
|
// a lone "?" (DUP-2, refactor audit 2026-10). A HelpPopover stays a "?".
|
|
141
|
-
|
|
142
|
+
// With a `help` (0.36) its "?" follows the label, and a FieldHint is its words at
|
|
143
|
+
// every size: the help's is the field's only "?".
|
|
144
|
+
const helpMark = fieldHelpMark(help, label);
|
|
145
|
+
const hint = useInlineHint(hintProp, helpMark !== undefined);
|
|
142
146
|
const hasHint = hasContent(hint);
|
|
143
147
|
const hasError = hasContent(error);
|
|
144
148
|
const describedBy = [hasHint && hintId, hasError && errorId].filter(Boolean).join(" ");
|
|
@@ -148,6 +152,19 @@ export function Field({
|
|
|
148
152
|
"aria-invalid": hasError || undefined,
|
|
149
153
|
"aria-required": required || undefined,
|
|
150
154
|
};
|
|
155
|
+
const labelEl = label !== undefined && (
|
|
156
|
+
<Label
|
|
157
|
+
id={labelId}
|
|
158
|
+
htmlFor={id}
|
|
159
|
+
required={required}
|
|
160
|
+
disabled={disabled}
|
|
161
|
+
size={labelSize}
|
|
162
|
+
data-error={hasError || undefined}
|
|
163
|
+
className={cn("data-[error=true]:text-danger", LABEL_VISIBILITY[labelVisibility])}
|
|
164
|
+
>
|
|
165
|
+
{label}
|
|
166
|
+
</Label>
|
|
167
|
+
);
|
|
151
168
|
return (
|
|
152
169
|
<div
|
|
153
170
|
data-slot="field"
|
|
@@ -162,18 +179,14 @@ export function Field({
|
|
|
162
179
|
className,
|
|
163
180
|
)}
|
|
164
181
|
>
|
|
165
|
-
{
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
className={cn("data-[error=true]:text-danger", LABEL_VISIBILITY[labelVisibility])}
|
|
174
|
-
>
|
|
175
|
-
{label}
|
|
176
|
-
</Label>
|
|
182
|
+
{helpMark === undefined && labelEl}
|
|
183
|
+
{helpMark !== undefined && (
|
|
184
|
+
// The label and its "?" on one line; the "?" stays visible where the label is
|
|
185
|
+
// hidden (`labelVisibility`) — a hidden button would still take the focus.
|
|
186
|
+
<div className="flex items-center gap-1">
|
|
187
|
+
{labelEl}
|
|
188
|
+
<span className="flex shrink-0 items-center">{helpMark}</span>
|
|
189
|
+
</div>
|
|
177
190
|
)}
|
|
178
191
|
{typeof children === "function"
|
|
179
192
|
? children(control, { labelId: label !== undefined ? labelId : undefined })
|
|
@@ -42,12 +42,14 @@ const HELP_WIDTH: RemLength = "20rem";
|
|
|
42
42
|
* new row on a phone). `useInlineHint` turns only a FieldHint into a caption, so this
|
|
43
43
|
* one stays on the label line at every size.
|
|
44
44
|
*
|
|
45
|
-
* **On a field
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
45
|
+
* **On a field: its `help`** (0.36, round 0.36 decision 1). Every kit field that takes a
|
|
46
|
+
* `hint` takes a `help` beside it, and the `@eifi1/ui-kit/rhf` bindings too: pass the
|
|
47
|
+
* help's words and the field draws this "?" on its label line (an unlabelled one at its
|
|
48
|
+
* end edge, outside the box), named "About “<label>”" — or pass a HelpPopover to name it
|
|
49
|
+
* yourself. `hint` stays the FACT, printed under the field; while `help` is set a
|
|
50
|
+
* FieldHint there draws no "?" of its own, so the field has one. Passed as `hint`, as in
|
|
51
|
+
* 0.34, it still rides the label line. It also stands on its own, beside a heading or a
|
|
52
|
+
* column title.
|
|
51
53
|
*
|
|
52
54
|
* **A popover, not a tooltip**: a tooltip bubble is for a line, and opens on hover a
|
|
53
55
|
* finger never makes. This is the kit {@link Popover}: it opens and closes on a tap or a
|
|
@@ -60,7 +62,7 @@ const HELP_WIDTH: RemLength = "20rem";
|
|
|
60
62
|
* accepted and cleared?", keksdose live #247); "!" is a warning.
|
|
61
63
|
*
|
|
62
64
|
* ```tsx
|
|
63
|
-
* <Select label="Status" hint={
|
|
65
|
+
* <Select label="Status" hint="Cleared and reconciled rows are locked." help={
|
|
64
66
|
* <HelpPopover label="What do the statuses mean?">
|
|
65
67
|
* Pending: … Accepted: … Cleared: … Reconciled: …
|
|
66
68
|
* </HelpPopover>
|
|
@@ -7,7 +7,7 @@ import { TILE_SIZE, TileRadioGroup } from "./tile-radio";
|
|
|
7
7
|
import type { TileItem, TileSize } from "./tile-radio";
|
|
8
8
|
import { hasContent } from "../lib/presence";
|
|
9
9
|
import { foldForSearch } from "../lib/text-match";
|
|
10
|
-
import { LabelStrip, useFieldMessages } from "./field-parts";
|
|
10
|
+
import { LabelStrip, useFieldMessages, type FieldHelpProps } from "./field-parts";
|
|
11
11
|
import { DEFAULT_ICON_PICKER_LABELS } from "./icon-picker-labels";
|
|
12
12
|
|
|
13
13
|
export interface IconPickerLabels {
|
|
@@ -52,7 +52,8 @@ export interface IconOption<T extends string> {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export interface IconPickerProps<T extends string>
|
|
55
|
-
extends Omit<ComponentPropsWithRef<"div">, "onChange" | "children" | "defaultValue"
|
|
55
|
+
extends Omit<ComponentPropsWithRef<"div">, "onChange" | "children" | "defaultValue">,
|
|
56
|
+
FieldHelpProps {
|
|
56
57
|
options: IconOption<T>[];
|
|
57
58
|
/** The chosen icon, or `null` for none. */
|
|
58
59
|
value: T | null;
|
|
@@ -122,6 +123,7 @@ export function IconPicker<T extends string>({
|
|
|
122
123
|
labels,
|
|
123
124
|
label,
|
|
124
125
|
hint,
|
|
126
|
+
help,
|
|
125
127
|
className,
|
|
126
128
|
"aria-label": ariaLabel,
|
|
127
129
|
"aria-labelledby": ariaLabelledBy,
|
|
@@ -134,9 +136,12 @@ export function IconPicker<T extends string>({
|
|
|
134
136
|
const mixedId = useId();
|
|
135
137
|
const labelId = useId();
|
|
136
138
|
const labelled = hasContent(label);
|
|
137
|
-
// The label line's "?", or the caption under the grid (DUP-2). Only with a label
|
|
139
|
+
// The label line's "?", or the caption under the grid (DUP-2). Only with a label, the
|
|
140
|
+
// help's "?" too (0.36).
|
|
138
141
|
const messages = useFieldMessages({
|
|
139
142
|
hint: labelled ? hint : undefined,
|
|
143
|
+
help: labelled ? help : undefined,
|
|
144
|
+
name: label,
|
|
140
145
|
describedBy: ariaDescribedBy,
|
|
141
146
|
reasonId: mixed && mixedId,
|
|
142
147
|
});
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
useFieldHint,
|
|
35
35
|
useLockReason,
|
|
36
36
|
type DisabledReasonDisplay,
|
|
37
|
+
type FieldHelpProps,
|
|
37
38
|
} from "./field-parts";
|
|
38
39
|
import { hasContent } from "../lib/presence";
|
|
39
40
|
import { useCommitReason, type CommitScope } from "./write-lock";
|
|
@@ -47,7 +48,8 @@ const CREATE_ROW_CLASS = "flex items-center gap-2 font-medium text-brand";
|
|
|
47
48
|
/** `id` is the INPUT's and `onChange` is the field's — see {@link ComboboxProps}
|
|
48
49
|
* for why the div's spellings of them are omitted. */
|
|
49
50
|
export interface InlineEntityComboboxProps<V extends string | number, C extends ComboClearValue = null>
|
|
50
|
-
extends Omit<ComponentPropsWithoutRef<"div">, "id" | "onChange"
|
|
51
|
+
extends Omit<ComponentPropsWithoutRef<"div">, "id" | "onChange">,
|
|
52
|
+
FieldHelpProps {
|
|
51
53
|
/** Selected option id; `null` or `clearValue` when nothing is selected. */
|
|
52
54
|
value: V | C | null;
|
|
53
55
|
/** Required and unanswered — see {@link Combobox}'s `invalid`. */
|
|
@@ -226,6 +228,7 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
226
228
|
invalid,
|
|
227
229
|
error,
|
|
228
230
|
hint,
|
|
231
|
+
help,
|
|
229
232
|
ref,
|
|
230
233
|
"aria-label": ariaLabel,
|
|
231
234
|
// Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s
|
|
@@ -239,7 +242,7 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
239
242
|
// The clear value reads as "nothing selected", whichever one the caller picked —
|
|
240
243
|
// so from here down `value` is the id or `null`, as it always was.
|
|
241
244
|
const value: V | null = rawValue == null || rawValue === clearValue ? null : (rawValue as V);
|
|
242
|
-
const hintParts = useFieldHint(hint, ariaDescribedBy);
|
|
245
|
+
const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });
|
|
243
246
|
const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);
|
|
244
247
|
const generated = useId();
|
|
245
248
|
const fieldId = id ?? generated;
|
package/src/components/input.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
LockedReason,
|
|
13
13
|
mergeDescribedBy,
|
|
14
14
|
type DisabledReasonDisplay,
|
|
15
|
+
type FieldHelpProps,
|
|
15
16
|
useFieldError,
|
|
16
17
|
useFieldHint,
|
|
17
18
|
useLockReason,
|
|
@@ -38,7 +39,7 @@ import {
|
|
|
38
39
|
// open the picker on a click anywhere in the field instead (feedback #224).
|
|
39
40
|
const PICKER_TYPES = new Set(["date", "datetime-local", "month", "time", "week"]);
|
|
40
41
|
|
|
41
|
-
export interface InputProps extends InputHTMLAttributes<HTMLInputElement
|
|
42
|
+
export interface InputProps extends InputHTMLAttributes<HTMLInputElement>, FieldHelpProps {
|
|
42
43
|
label?: ReactNode;
|
|
43
44
|
/** Classes for the `<input>` itself, as distinct from `className`, which
|
|
44
45
|
* styles the field WRAPPER once a `label` turns this into a FloatingField.
|
|
@@ -156,6 +157,7 @@ export function Input(props: InputProps) {
|
|
|
156
157
|
invalid,
|
|
157
158
|
error,
|
|
158
159
|
hint,
|
|
160
|
+
help,
|
|
159
161
|
showCount,
|
|
160
162
|
countLabels,
|
|
161
163
|
passwordLabels,
|
|
@@ -171,7 +173,7 @@ export function Input(props: InputProps) {
|
|
|
171
173
|
const generated = useId();
|
|
172
174
|
const fieldId = id ?? generated;
|
|
173
175
|
const countId = useId();
|
|
174
|
-
const hintParts = useFieldHint(hint, rest["aria-describedby"]);
|
|
176
|
+
const hintParts = useFieldHint(hint, rest["aria-describedby"], { help, name: label ?? rest["aria-label"] });
|
|
175
177
|
const maxLength = rest.maxLength;
|
|
176
178
|
const counting = Boolean(showCount) && typeof maxLength === "number" && maxLength > 0;
|
|
177
179
|
const length = useTypedLength(rest.value, rest.defaultValue);
|
|
@@ -13,7 +13,7 @@ import { splitTriggerAria } from "./trigger-aria";
|
|
|
13
13
|
import type { TriggerAria } from "./trigger-aria";
|
|
14
14
|
import { useKitDateFormatter, useKitLabels, useKitLocale, type LabelOverride } from "../i18n/kit-labels";
|
|
15
15
|
import { hasContent } from "../lib/presence";
|
|
16
|
-
import { FieldBox, FieldLabelLine, LABEL_ABOVE_BOX, mergeRefs, useFieldMessages } from "./field-parts";
|
|
16
|
+
import { FieldBox, FieldLabelLine, LABEL_ABOVE_BOX, mergeRefs, useFieldMessages, type FieldHelpProps } from "./field-parts";
|
|
17
17
|
import { DEFAULT_MONTH_PICKER_LABELS } from "./month-picker-labels";
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -63,7 +63,7 @@ function useMonthPickerLabels(partial?: Partial<MonthPickerLabels>): MonthPicker
|
|
|
63
63
|
* a `FormEvent`, and this one answers with a month key. Everything else reaches the
|
|
64
64
|
* field's root, so a test id or a `data-tour` anchor can find it.
|
|
65
65
|
*/
|
|
66
|
-
export interface MonthPickerProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange"
|
|
66
|
+
export interface MonthPickerProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange">, FieldHelpProps {
|
|
67
67
|
/**
|
|
68
68
|
* The selected month as a `"YYYY-MM"` key, or `""` for none.
|
|
69
69
|
*
|
|
@@ -831,6 +831,7 @@ export function MonthPicker(props: MonthPickerProps) {
|
|
|
831
831
|
invalid,
|
|
832
832
|
error,
|
|
833
833
|
hint,
|
|
834
|
+
help,
|
|
834
835
|
size = "md",
|
|
835
836
|
mode = "month",
|
|
836
837
|
formatOptions,
|
|
@@ -877,7 +878,13 @@ export function MonthPicker(props: MonthPickerProps) {
|
|
|
877
878
|
// keksdose K4: the caption and the error, on the trigger after the caller's own
|
|
878
879
|
// description. Painted for `invalid` and a message — not for a bare `aria-invalid`,
|
|
879
880
|
// which this field has never painted for.
|
|
880
|
-
const messages = useFieldMessages({
|
|
881
|
+
const messages = useFieldMessages({
|
|
882
|
+
hint,
|
|
883
|
+
help,
|
|
884
|
+
name: label ?? rest["aria-label"],
|
|
885
|
+
error,
|
|
886
|
+
describedBy: rest["aria-describedby"],
|
|
887
|
+
});
|
|
881
888
|
const [ownAria, wrapperRest] = splitTriggerAria(rest);
|
|
882
889
|
// The field's trigger is described by the caption and the error too; the stepper
|
|
883
890
|
// draws neither, so its heading keeps the caller's own description.
|
|
@@ -28,13 +28,15 @@ import {
|
|
|
28
28
|
useFieldHint,
|
|
29
29
|
useLockReason,
|
|
30
30
|
type DisabledReasonDisplay,
|
|
31
|
+
type FieldHelpProps,
|
|
31
32
|
} from "./field-parts";
|
|
32
33
|
import { useCommitReason, type CommitScope } from "./write-lock";
|
|
33
34
|
|
|
34
35
|
/** `onChange` is the kit's — "a selection was made", carrying values — rather
|
|
35
36
|
* than the div's form event, so the DOM's spelling of it is omitted. */
|
|
36
37
|
export interface MultiEntityComboboxProps<V extends string | number>
|
|
37
|
-
extends Omit<ComponentPropsWithoutRef<"div">, "onChange"
|
|
38
|
+
extends Omit<ComponentPropsWithoutRef<"div">, "onChange">,
|
|
39
|
+
FieldHelpProps {
|
|
38
40
|
/** Selected ids. */
|
|
39
41
|
value: V[];
|
|
40
42
|
onChange: (value: V[]) => void;
|
|
@@ -144,6 +146,7 @@ export function MultiEntityCombobox<V extends string | number>(props: MultiEntit
|
|
|
144
146
|
invalid,
|
|
145
147
|
error,
|
|
146
148
|
hint,
|
|
149
|
+
help,
|
|
147
150
|
filter,
|
|
148
151
|
minChars,
|
|
149
152
|
debounceMs,
|
|
@@ -169,7 +172,7 @@ export function MultiEntityCombobox<V extends string | number>(props: MultiEntit
|
|
|
169
172
|
minChars,
|
|
170
173
|
debounceMs,
|
|
171
174
|
});
|
|
172
|
-
const hintParts = useFieldHint(hint, ariaDescribedBy);
|
|
175
|
+
const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });
|
|
173
176
|
const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);
|
|
174
177
|
// The props are the per-instance overrides, the provider the app-wide ones; a
|
|
175
178
|
// prop left `undefined` falls through to the provider rather than masking it.
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
useFieldMessages,
|
|
14
14
|
useLockReason,
|
|
15
15
|
type DisabledReasonDisplay,
|
|
16
|
+
type FieldHelpProps,
|
|
16
17
|
} from "./field-parts";
|
|
17
18
|
import type { CommitScope } from "./write-lock";
|
|
18
19
|
import {
|
|
@@ -46,7 +47,8 @@ export interface MultiSelectOption<V extends string | number = string | number>
|
|
|
46
47
|
* inferred from `options` and `value`, so `onChange` hands back `number[]` for number
|
|
47
48
|
* ids and `string[]` for string ones, and no call site casts. */
|
|
48
49
|
export interface MultiSelectProps<V extends string | number = string | number>
|
|
49
|
-
extends Omit<ComponentPropsWithoutRef<"div">, "onChange"
|
|
50
|
+
extends Omit<ComponentPropsWithoutRef<"div">, "onChange">,
|
|
51
|
+
FieldHelpProps {
|
|
50
52
|
options: MultiSelectOption<V>[];
|
|
51
53
|
/** The chosen values — the select family's `value` (API-4). */
|
|
52
54
|
value?: V[];
|
|
@@ -140,6 +142,7 @@ export function MultiSelect<V extends string | number = string | number>(props:
|
|
|
140
142
|
invalid,
|
|
141
143
|
error,
|
|
142
144
|
hint,
|
|
145
|
+
help,
|
|
143
146
|
disabled,
|
|
144
147
|
disabledReason,
|
|
145
148
|
disabledReasonDisplay,
|
|
@@ -195,6 +198,8 @@ export function MultiSelect<V extends string | number = string | number>(props:
|
|
|
195
198
|
// The caller's ids, the caption, a lock's reason, the error — Select's order.
|
|
196
199
|
const field = useFieldMessages({
|
|
197
200
|
hint,
|
|
201
|
+
help,
|
|
202
|
+
name: label ?? ariaLabel,
|
|
198
203
|
error,
|
|
199
204
|
invalid,
|
|
200
205
|
describedBy: ariaDescribedBy,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "./field-base";
|
|
12
12
|
// The data alone (0.35): not the picker, which this field does not render.
|
|
13
13
|
import { CURRENCIES } from "../currencies";
|
|
14
|
-
import { FieldCaption, LockedReason, mergeRefs, type DisabledReasonDisplay } from "./field-parts";
|
|
14
|
+
import { FieldCaption, LockedReason, mergeRefs, type DisabledReasonDisplay, type FieldHelpProps } from "./field-parts";
|
|
15
15
|
import { useEndWidth, useNumericField } from "./numeric-field";
|
|
16
16
|
import type { CommitScope } from "./write-lock";
|
|
17
17
|
import { cn } from "../lib/cn";
|
|
@@ -19,7 +19,7 @@ import { commitExpression, formatResult } from "../lib/calc";
|
|
|
19
19
|
|
|
20
20
|
/** {@link NumberInput}'s props — exported since 0.34 (refactor audit 2026-10, API-9): a
|
|
21
21
|
* wrapper typed itself off `ComponentProps<typeof NumberInput>` until then. */
|
|
22
|
-
export interface NumberInputProps {
|
|
22
|
+
export interface NumberInputProps extends FieldHelpProps {
|
|
23
23
|
/** The text, DOT-decimal ("1234.5", or a calculation being typed: "12+5"), as
|
|
24
24
|
* `onChange` reports it — so a caller's `Number(value)` reads it. The field SHOWS it
|
|
25
25
|
* in the locale's mark (see `locale`). A comma here is read as the decimal mark too,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useFieldError,
|
|
9
9
|
useFieldHint,
|
|
10
10
|
useLockReason,
|
|
11
|
+
type FieldHelpProps,
|
|
11
12
|
type FieldHintParts,
|
|
12
13
|
type LockReason,
|
|
13
14
|
} from "./field-parts";
|
|
@@ -28,7 +29,9 @@ import type { CommitScope } from "./write-lock";
|
|
|
28
29
|
*/
|
|
29
30
|
|
|
30
31
|
/** The props both fields take and this engine reads. */
|
|
31
|
-
export interface NumericFieldProps {
|
|
32
|
+
export interface NumericFieldProps extends FieldHelpProps {
|
|
33
|
+
/** What names the `help` "?" — "About “<label>”" — with the `aria-label`. */
|
|
34
|
+
label?: ReactNode;
|
|
32
35
|
hint?: ReactNode;
|
|
33
36
|
error?: ReactNode;
|
|
34
37
|
invalid?: boolean;
|
|
@@ -39,6 +42,7 @@ export interface NumericFieldProps {
|
|
|
39
42
|
calculator?: boolean;
|
|
40
43
|
variant?: "field" | "display";
|
|
41
44
|
locale?: string;
|
|
45
|
+
"aria-label"?: string;
|
|
42
46
|
"aria-describedby"?: string;
|
|
43
47
|
"aria-invalid"?: boolean | "true" | "false";
|
|
44
48
|
}
|
|
@@ -94,7 +98,11 @@ export function useNumericField(props: NumericFieldProps, { settle, display }: N
|
|
|
94
98
|
const lock = useLockReason(props.commit, props.disabledReason);
|
|
95
99
|
const locked = lock.locked;
|
|
96
100
|
// §4: a FieldHint is the caption at Large and on touch, as on every field.
|
|
97
|
-
|
|
101
|
+
// A `help` is the label line's one "?" and turns the hint into the caption (0.36).
|
|
102
|
+
const hint = useFieldHint(props.hint, props["aria-describedby"], {
|
|
103
|
+
help: props.help,
|
|
104
|
+
name: props.label ?? props["aria-label"],
|
|
105
|
+
});
|
|
98
106
|
// The standing advice first, the news second: the caller's ids, the caption, the
|
|
99
107
|
// error — a lock's reason between the last two, as on Select.
|
|
100
108
|
const field = useFieldError(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useId, useState } from "react";
|
|
2
2
|
import type { ChangeEvent, ClipboardEvent, FocusEvent, InputHTMLAttributes, ReactNode, Ref } from "react";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
|
-
import { EndHintRow, FieldCaption, useFieldError, useFieldHint } from "./field-parts";
|
|
4
|
+
import { EndHintRow, FieldCaption, useFieldError, useFieldHint, type FieldHelpProps } from "./field-parts";
|
|
5
5
|
import {
|
|
6
6
|
FIELD_BASE,
|
|
7
7
|
FIELD_INVALID,
|
|
@@ -60,7 +60,8 @@ export interface OneTimeCodeInputProps
|
|
|
60
60
|
extends Omit<
|
|
61
61
|
InputHTMLAttributes<HTMLInputElement>,
|
|
62
62
|
"value" | "defaultValue" | "onChange" | "type" | "inputMode" | "autoComplete" | "maxLength" | "pattern"
|
|
63
|
-
|
|
63
|
+
>,
|
|
64
|
+
FieldHelpProps {
|
|
64
65
|
/** The digits so far — a string, so leading zeros survive. */
|
|
65
66
|
value: string;
|
|
66
67
|
/** The cleaned code after every edit: digits only, never longer than `length`. */
|
|
@@ -130,6 +131,7 @@ export function OneTimeCodeInput(props: OneTimeCodeInputProps) {
|
|
|
130
131
|
onComplete,
|
|
131
132
|
label,
|
|
132
133
|
hint,
|
|
134
|
+
help,
|
|
133
135
|
error,
|
|
134
136
|
invalid: invalidProp,
|
|
135
137
|
className,
|
|
@@ -151,7 +153,7 @@ export function OneTimeCodeInput(props: OneTimeCodeInputProps) {
|
|
|
151
153
|
// Text is a caption under the field; a FieldHint rides the label line, and is that
|
|
152
154
|
// caption at Large and on touch (§4). See `hint`. Through useFieldHint and
|
|
153
155
|
// useFieldError since 0.34, not copies of their rules (DUP-2, DUP-8).
|
|
154
|
-
const hintParts = useFieldHint(hint, ariaDescribedBy);
|
|
156
|
+
const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? rest["aria-label"] });
|
|
155
157
|
const {
|
|
156
158
|
isInvalid: invalid,
|
|
157
159
|
describedBy,
|