@eifi1/ui-kit 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/chip.d.ts +19 -5
- package/dist/components/chip.js.map +1 -1
- package/dist/components/currency-select.js +10 -2
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/date-picker.js +19 -7
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/month-picker.js +16 -5
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/popover.js +4 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/trigger-aria.d.ts +18 -0
- package/dist/components/trigger-aria.js +25 -0
- package/dist/components/trigger-aria.js.map +1 -0
- package/dist/components/ui.d.ts +14 -6
- package/dist/components/ui.js +11 -12
- package/dist/components/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chip.tsx +19 -5
- package/src/components/currency-select.tsx +16 -2
- package/src/components/date-picker.tsx +31 -6
- package/src/components/month-picker.tsx +22 -4
- package/src/components/popover.tsx +4 -1
- package/src/components/trigger-aria.ts +42 -0
- package/src/components/ui.tsx +27 -16
|
@@ -5,6 +5,8 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import { monthKey, pad } from "../lib/dates";
|
|
6
6
|
import { FieldLabel, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER } from "./ui";
|
|
7
7
|
import { Popover } from "./popover";
|
|
8
|
+
import { splitTriggerAria } from "./trigger-aria";
|
|
9
|
+
import type { TriggerAria } from "./trigger-aria";
|
|
8
10
|
import { useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -156,6 +158,7 @@ function MonthFieldTrigger({
|
|
|
156
158
|
disabled,
|
|
157
159
|
invalid,
|
|
158
160
|
className,
|
|
161
|
+
aria,
|
|
159
162
|
}: {
|
|
160
163
|
open: boolean;
|
|
161
164
|
toggle: () => void;
|
|
@@ -163,6 +166,8 @@ function MonthFieldTrigger({
|
|
|
163
166
|
triggerText: string;
|
|
164
167
|
hasValue: boolean;
|
|
165
168
|
labelledBy: string;
|
|
169
|
+
/** The caller's naming attributes — see `trigger-aria.ts`. */
|
|
170
|
+
aria: TriggerAria;
|
|
166
171
|
valueId: string;
|
|
167
172
|
panelId: string;
|
|
168
173
|
padded: boolean;
|
|
@@ -191,8 +196,11 @@ function MonthFieldTrigger({
|
|
|
191
196
|
aria-haspopup="dialog"
|
|
192
197
|
aria-controls={panelId}
|
|
193
198
|
aria-expanded={open}
|
|
194
|
-
|
|
195
|
-
aria-
|
|
199
|
+
id={aria.id}
|
|
200
|
+
aria-invalid={invalid || aria["aria-invalid"] === true || aria["aria-invalid"] === "true" || undefined}
|
|
201
|
+
aria-labelledby={aria["aria-label"] && !aria["aria-labelledby"] ? undefined : labelledBy}
|
|
202
|
+
aria-label={aria["aria-label"]}
|
|
203
|
+
aria-describedby={aria["aria-describedby"]}
|
|
196
204
|
className={cn(
|
|
197
205
|
FIELD_TRIGGER,
|
|
198
206
|
"pe-9",
|
|
@@ -488,6 +496,7 @@ export function MonthPicker({
|
|
|
488
496
|
: (placeholder ?? "");
|
|
489
497
|
|
|
490
498
|
const id = useId();
|
|
499
|
+
const [aria, wrapperRest] = splitTriggerAria(rest);
|
|
491
500
|
const labelId = `${id}-label`;
|
|
492
501
|
const valueId = `${id}-value`;
|
|
493
502
|
const panelId = `${id}-panel`;
|
|
@@ -495,7 +504,7 @@ export function MonthPicker({
|
|
|
495
504
|
const named = typeof label === "string";
|
|
496
505
|
|
|
497
506
|
return (
|
|
498
|
-
<div {...
|
|
507
|
+
<div {...wrapperRest} className={cn("relative", className)}>
|
|
499
508
|
{label !== undefined && <FieldLabel>{label}</FieldLabel>}
|
|
500
509
|
{/* The hidden twin the trigger is named by — see DateField. `sr-only-fixed`,
|
|
501
510
|
and inside this `relative` root either way. */}
|
|
@@ -515,7 +524,16 @@ export function MonthPicker({
|
|
|
515
524
|
triggerRef={ref}
|
|
516
525
|
triggerText={triggerText}
|
|
517
526
|
hasValue={selected != null}
|
|
518
|
-
|
|
527
|
+
// As DatePicker: a caller's reference first; an `id` without a label of our
|
|
528
|
+
// own references the trigger itself, so an external <label htmlFor> names it.
|
|
529
|
+
labelledBy={[
|
|
530
|
+
aria["aria-labelledby"],
|
|
531
|
+
named ? labelId : !aria["aria-labelledby"] && aria.id ? aria.id : undefined,
|
|
532
|
+
valueId,
|
|
533
|
+
]
|
|
534
|
+
.filter(Boolean)
|
|
535
|
+
.join(" ")}
|
|
536
|
+
aria={aria}
|
|
519
537
|
valueId={valueId}
|
|
520
538
|
panelId={panelId}
|
|
521
539
|
padded={label !== undefined}
|
|
@@ -19,7 +19,10 @@ export interface PopoverLabels {
|
|
|
19
19
|
// Exported since the kit grew one label tree (`UiKitLabels`, src/i18n): the complete
|
|
20
20
|
// English reference a translator works from has to be able to name every namespace.
|
|
21
21
|
export const DEFAULT_POPOVER_LABELS: PopoverLabels = {
|
|
22
|
-
|
|
22
|
+
// "Pop-up", not "Popover": the panel's accessible name is read to USERS, and
|
|
23
|
+
// "popover" is a developer's word for it (reported by keksdose, 0.5.0). Only a
|
|
24
|
+
// bare Popover falls back to this — the kit's own pickers name their panels.
|
|
25
|
+
panel: "Pop-up",
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
/**
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Internal — not re-exported from the barrel. Shared by the pickers whose trigger is
|
|
2
|
+
// a `role="combobox"` button inside a wrapper: DatePicker, DateRangePicker, MonthPicker.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The attributes that NAME or DESCRIBE a field, which belong on its trigger — the
|
|
6
|
+
* element a `<label htmlFor>`, a form library's control slot or an error message has
|
|
7
|
+
* to reach. They used to land on the wrapper `<div>` with the rest of the caller's
|
|
8
|
+
* props, so an external label named nothing and a form library's `aria-describedby`
|
|
9
|
+
* pointed a screen reader at an element with no role (kastlan, 0.5.0).
|
|
10
|
+
*/
|
|
11
|
+
export interface TriggerAria {
|
|
12
|
+
id?: string;
|
|
13
|
+
"aria-label"?: string;
|
|
14
|
+
"aria-labelledby"?: string;
|
|
15
|
+
"aria-describedby"?: string;
|
|
16
|
+
"aria-invalid"?: boolean | "true" | "false" | "grammar" | "spelling";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Split a field's props into what its trigger takes and what its wrapper takes. */
|
|
20
|
+
export function splitTriggerAria<T extends TriggerAria>(
|
|
21
|
+
props: T,
|
|
22
|
+
): [TriggerAria, Omit<T, keyof TriggerAria>] {
|
|
23
|
+
const {
|
|
24
|
+
id,
|
|
25
|
+
"aria-label": label,
|
|
26
|
+
"aria-labelledby": labelledBy,
|
|
27
|
+
"aria-describedby": describedBy,
|
|
28
|
+
"aria-invalid": invalid,
|
|
29
|
+
...rest
|
|
30
|
+
} = props;
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
id,
|
|
34
|
+
"aria-label": label,
|
|
35
|
+
"aria-labelledby": labelledBy,
|
|
36
|
+
"aria-describedby": describedBy,
|
|
37
|
+
"aria-invalid": invalid,
|
|
38
|
+
},
|
|
39
|
+
rest,
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
package/src/components/ui.tsx
CHANGED
|
@@ -968,9 +968,17 @@ export function CardFooter({ className, ...props }: CardFooterProps) {
|
|
|
968
968
|
/** A `<span>`'s props plus the words a reader hears — see {@link CardHeaderProps}.
|
|
969
969
|
* The ring is drawn with a border, so there is nothing inside it to put children in. */
|
|
970
970
|
export interface SpinnerProps extends Omit<ComponentPropsWithoutRef<"span">, "children"> {
|
|
971
|
-
/**
|
|
972
|
-
*
|
|
973
|
-
|
|
971
|
+
/**
|
|
972
|
+
* What the spinner means, for a screen reader. Default: `common.loading` from the
|
|
973
|
+
* {@link UiKitProvider}, else "Loading…".
|
|
974
|
+
*
|
|
975
|
+
* `null` makes it DECORATIVE — no role, no text, hidden from assistive tech. Use it
|
|
976
|
+
* whenever words are already there: beside visible "Loading…" text (otherwise it is
|
|
977
|
+
* announced twice), inside a labelled button (otherwise its text joins the button's
|
|
978
|
+
* name), or inside a live region of the app's own (otherwise that region and this
|
|
979
|
+
* one both announce).
|
|
980
|
+
*/
|
|
981
|
+
label?: string | null;
|
|
974
982
|
}
|
|
975
983
|
|
|
976
984
|
/**
|
|
@@ -984,22 +992,25 @@ export interface SpinnerProps extends Omit<ComponentPropsWithoutRef<"span">, "ch
|
|
|
984
992
|
* ignore a name on one.
|
|
985
993
|
*
|
|
986
994
|
* `relative` so the `sr-only` text has a local containing block (see
|
|
987
|
-
* sr-only-containment.test).
|
|
988
|
-
*
|
|
989
|
-
*
|
|
995
|
+
* sr-only-containment.test). Where the words are already on screen, pass
|
|
996
|
+
* `label={null}`: announcing is right for a spinner standing alone and wrong for one
|
|
997
|
+
* beside text, inside a button, or inside the app's own live region.
|
|
990
998
|
*/
|
|
991
999
|
export function Spinner({ className, label, ...rest }: SpinnerProps) {
|
|
992
|
-
const common = useKitLabels("common", DEFAULT_COMMON_LABELS, {
|
|
1000
|
+
const common = useKitLabels("common", DEFAULT_COMMON_LABELS, {
|
|
1001
|
+
loading: label ?? undefined,
|
|
1002
|
+
});
|
|
1003
|
+
const ring = cn(
|
|
1004
|
+
"relative inline-block h-5 w-5 animate-spin rounded-full border-2 border-[var(--border)] border-t-[var(--text-primary)]",
|
|
1005
|
+
className,
|
|
1006
|
+
);
|
|
1007
|
+
if (label === null) return <span aria-hidden {...rest} className={ring} />;
|
|
993
1008
|
return (
|
|
994
|
-
<span
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
className,
|
|
1000
|
-
)}
|
|
1001
|
-
>
|
|
1002
|
-
<span className="sr-only">{common.loading}</span>
|
|
1009
|
+
<span role="status" {...rest} className={ring}>
|
|
1010
|
+
{/* The trailing space is a separator for the case the caller forgot `label={null}`
|
|
1011
|
+
inside a button: a name is the concatenation of its text, and without it the
|
|
1012
|
+
spinner's word ran straight into the button's — "Loadingconfirm". */}
|
|
1013
|
+
<span className="sr-only">{common.loading} </span>
|
|
1003
1014
|
</span>
|
|
1004
1015
|
);
|
|
1005
1016
|
}
|