@godxjp/ui 18.13.0 → 18.13.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/data-entry/form-field.js +6 -2
- package/dist/components/data-entry/input.js +9 -2
- package/dist/components/data-entry/search-select.js +11 -4
- package/dist/components/data-entry/select.js +24 -8
- package/dist/components/layout/flex.js +17 -1
- package/dist/lib/field-a11y.d.ts +29 -1
- package/dist/lib/field-a11y.js +16 -1
- package/package.json +2 -2
|
@@ -3,7 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Label } from "../data-entry/label.js";
|
|
5
5
|
import { cn } from "../../lib/utils.js";
|
|
6
|
-
import { mergeAriaIds } from "../../lib/field-a11y.js";
|
|
6
|
+
import { FieldNameContext, mergeAriaIds } from "../../lib/field-a11y.js";
|
|
7
7
|
import { useFormLayout } from "./form.js";
|
|
8
8
|
const toCssLength = (v) => typeof v === "number" ? `${v}px` : v;
|
|
9
9
|
const FOCUSABLE_SELECTOR = 'input:not([type="hidden"]), select, textarea, button, [tabindex]:not([tabindex="-1"])';
|
|
@@ -39,6 +39,10 @@ function FormField({
|
|
|
39
39
|
"FormField expects a single React element child to receive aria-describedby/aria-errormessage; the helper text and error message will not be associated with the control. Pass plain text via `staticText` instead of `children` for a read-only value row."
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
|
+
const fieldNameContext = React.useMemo(
|
|
43
|
+
() => ({ labelId, label: typeof label === "string" ? label : void 0 }),
|
|
44
|
+
[labelId, label]
|
|
45
|
+
);
|
|
42
46
|
const childProps = React.isValidElement(children) ? children.props : void 0;
|
|
43
47
|
const mergeIds = mergeAriaIds;
|
|
44
48
|
const childWithA11y = isStatic ? (
|
|
@@ -112,7 +116,7 @@ function FormField({
|
|
|
112
116
|
labelAddon
|
|
113
117
|
] }),
|
|
114
118
|
/* @__PURE__ */ jsxs("div", { "data-slot": "form-field-control", className: "ui-form-field-control", children: [
|
|
115
|
-
childWithA11y,
|
|
119
|
+
isStatic ? childWithA11y : /* @__PURE__ */ jsx(FieldNameContext.Provider, { value: fieldNameContext, children: childWithA11y }),
|
|
116
120
|
helper ? /* @__PURE__ */ jsx("p", { id: helperId, className: "text-muted-foreground text-xs", children: helper }) : null,
|
|
117
121
|
error ? /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "text-destructive text-xs", children: error }) : null
|
|
118
122
|
] })
|
|
@@ -3,6 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
5
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
6
|
+
import { useFieldNameFallback } from "../../lib/field-a11y.js";
|
|
6
7
|
import { cn } from "../../lib/utils.js";
|
|
7
8
|
const inputBaseClass = [
|
|
8
9
|
"ui-control border-input bg-background w-full min-w-0 rounded-[var(--control-radius)] transition-[color,box-shadow] outline-none",
|
|
@@ -27,6 +28,10 @@ const Input = React.forwardRef(
|
|
|
27
28
|
...props
|
|
28
29
|
}, ref) => {
|
|
29
30
|
const { t } = useTranslation();
|
|
31
|
+
const nameFallback = useFieldNameFallback({
|
|
32
|
+
"aria-label": props["aria-label"],
|
|
33
|
+
"aria-labelledby": props["aria-labelledby"]
|
|
34
|
+
});
|
|
30
35
|
const innerRef = React.useRef(null);
|
|
31
36
|
const setRefs = React.useCallback(
|
|
32
37
|
(node) => {
|
|
@@ -71,7 +76,8 @@ const Input = React.forwardRef(
|
|
|
71
76
|
defaultValue,
|
|
72
77
|
onChange,
|
|
73
78
|
className: cn(inputBaseClass, className),
|
|
74
|
-
...props
|
|
79
|
+
...props,
|
|
80
|
+
...nameFallback
|
|
75
81
|
}
|
|
76
82
|
);
|
|
77
83
|
}
|
|
@@ -112,7 +118,8 @@ const Input = React.forwardRef(
|
|
|
112
118
|
(showClear || trailingIcon != null) && "pe-9",
|
|
113
119
|
className
|
|
114
120
|
),
|
|
115
|
-
...props
|
|
121
|
+
...props,
|
|
122
|
+
...nameFallback
|
|
116
123
|
}
|
|
117
124
|
),
|
|
118
125
|
trailing != null ? /* @__PURE__ */ jsx("span", { className: "absolute inset-y-0 end-2 inline-flex items-center", children: trailing }) : null
|
|
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { ChevronsUpDown, Loader2, X } from "lucide-react";
|
|
5
5
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
6
|
+
import { useFieldNameFallback } from "../../lib/field-a11y.js";
|
|
6
7
|
import { cn } from "../../lib/utils.js";
|
|
7
8
|
import { controlOpenRingClass } from "../../lib/control-styles.js";
|
|
8
9
|
import { Button } from "../general/button.js";
|
|
@@ -49,6 +50,12 @@ function SearchSelect({
|
|
|
49
50
|
"aria-required": ariaRequired
|
|
50
51
|
}) {
|
|
51
52
|
const { t } = useTranslation();
|
|
53
|
+
const nameFallback = useFieldNameFallback({
|
|
54
|
+
"aria-label": ariaLabel,
|
|
55
|
+
"aria-labelledby": ariaLabelledby
|
|
56
|
+
});
|
|
57
|
+
const triggerAriaLabel = ariaLabel ?? nameFallback["aria-label"];
|
|
58
|
+
const triggerAriaLabelledby = ariaLabelledby ?? nameFallback["aria-labelledby"];
|
|
52
59
|
const reactId = React.useId();
|
|
53
60
|
const listId = `${reactId}-listbox`;
|
|
54
61
|
const optionDomId = (optionValue) => `${reactId}-opt-${optionValue}`;
|
|
@@ -238,8 +245,8 @@ function SearchSelect({
|
|
|
238
245
|
size,
|
|
239
246
|
"aria-expanded": open,
|
|
240
247
|
"aria-controls": open ? listId : void 0,
|
|
241
|
-
"aria-label":
|
|
242
|
-
"aria-labelledby":
|
|
248
|
+
"aria-label": triggerAriaLabel,
|
|
249
|
+
"aria-labelledby": triggerAriaLabelledby,
|
|
243
250
|
"aria-describedby": ariaDescribedby,
|
|
244
251
|
"aria-errormessage": ariaErrorMessage,
|
|
245
252
|
"aria-invalid": ariaInvalid,
|
|
@@ -272,8 +279,8 @@ function SearchSelect({
|
|
|
272
279
|
/* @__PURE__ */ jsx(
|
|
273
280
|
PopoverContent,
|
|
274
281
|
{
|
|
275
|
-
"aria-label":
|
|
276
|
-
"aria-labelledby":
|
|
282
|
+
"aria-label": triggerAriaLabelledby ? void 0 : triggerAriaLabel ?? resolvedPlaceholder,
|
|
283
|
+
"aria-labelledby": triggerAriaLabelledby,
|
|
277
284
|
align: "start",
|
|
278
285
|
sideOffset: 4,
|
|
279
286
|
collisionPadding: 12,
|
|
@@ -5,7 +5,12 @@ import * as SelectPrimitive from "@radix-ui/react-select";
|
|
|
5
5
|
import { ChevronDown, ChevronUp, X } from "lucide-react";
|
|
6
6
|
import { cn } from "../../lib/utils.js";
|
|
7
7
|
import { controlTriggerClass } from "../../lib/control-styles.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
mergeAriaIds,
|
|
10
|
+
omitFieldA11y,
|
|
11
|
+
pickFieldA11y,
|
|
12
|
+
useFieldNameFallback
|
|
13
|
+
} from "../../lib/field-a11y.js";
|
|
9
14
|
import { SearchSelect } from "./search-select.js";
|
|
10
15
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
11
16
|
function isDataSelect(props) {
|
|
@@ -32,13 +37,24 @@ function SelectValue(props) {
|
|
|
32
37
|
const SelectTrigger = React.forwardRef(({ className, children, size = "md", showIndicator = true, ...props }, ref) => {
|
|
33
38
|
const field = React.useContext(SelectFieldA11yContext);
|
|
34
39
|
const ownsName = props["aria-label"] !== void 0 || props["aria-labelledby"] !== void 0;
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"aria-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const fieldOwnsName = field?.["aria-label"] !== void 0 || field?.["aria-labelledby"] !== void 0;
|
|
41
|
+
const nameFallback = useFieldNameFallback({
|
|
42
|
+
"aria-label": ownsName ? props["aria-label"] : field?.["aria-label"],
|
|
43
|
+
"aria-labelledby": ownsName ? props["aria-labelledby"] : field?.["aria-labelledby"]
|
|
44
|
+
});
|
|
45
|
+
const fieldA11y = field || nameFallback["aria-labelledby"] !== void 0 ? {
|
|
46
|
+
id: props.id ?? field?.id,
|
|
47
|
+
...ownsName ? {} : fieldOwnsName ? {
|
|
48
|
+
"aria-label": field?.["aria-label"],
|
|
49
|
+
"aria-labelledby": field?.["aria-labelledby"]
|
|
50
|
+
} : nameFallback,
|
|
51
|
+
"aria-describedby": mergeAriaIds(props["aria-describedby"], field?.["aria-describedby"]),
|
|
52
|
+
"aria-errormessage": mergeAriaIds(
|
|
53
|
+
props["aria-errormessage"],
|
|
54
|
+
field?.["aria-errormessage"]
|
|
55
|
+
),
|
|
56
|
+
"aria-required": props["aria-required"] ?? field?.["aria-required"],
|
|
57
|
+
"aria-invalid": props["aria-invalid"] ?? field?.["aria-invalid"]
|
|
42
58
|
} : void 0;
|
|
43
59
|
return /* @__PURE__ */ jsxs(
|
|
44
60
|
SelectPrimitive.Trigger,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { mergeAriaIds } from "../../lib/field-a11y.js";
|
|
2
4
|
import { cn } from "../../lib/utils.js";
|
|
3
5
|
import { flexGapClass } from "../../lib/variants.js";
|
|
4
6
|
function Flex({
|
|
@@ -13,6 +15,20 @@ function Flex({
|
|
|
13
15
|
children,
|
|
14
16
|
...props
|
|
15
17
|
}) {
|
|
18
|
+
let domProps = props;
|
|
19
|
+
if (props.role === void 0 && (props["aria-label"] !== void 0 || props["aria-labelledby"] !== void 0)) {
|
|
20
|
+
const {
|
|
21
|
+
"aria-required": _ariaRequired,
|
|
22
|
+
"aria-invalid": _ariaInvalid,
|
|
23
|
+
"aria-errormessage": ariaErrorMessage,
|
|
24
|
+
...allowed
|
|
25
|
+
} = props;
|
|
26
|
+
domProps = {
|
|
27
|
+
...allowed,
|
|
28
|
+
role: "group",
|
|
29
|
+
"aria-describedby": mergeAriaIds(props["aria-describedby"], ariaErrorMessage)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
16
32
|
return /* @__PURE__ */ jsx(
|
|
17
33
|
"div",
|
|
18
34
|
{
|
|
@@ -23,7 +39,7 @@ function Flex({
|
|
|
23
39
|
"data-hide-below": hideBelow,
|
|
24
40
|
"data-hide-from": hideFrom,
|
|
25
41
|
className: cn("ui-flex", flexGapClass[gap], className),
|
|
26
|
-
...
|
|
42
|
+
...domProps,
|
|
27
43
|
children
|
|
28
44
|
}
|
|
29
45
|
);
|
package/dist/lib/field-a11y.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as React from "react";
|
|
2
2
|
/**
|
|
3
3
|
* The accessible-name / description / validation contract that {@link FormField} injects onto its
|
|
4
4
|
* single control child (via `cloneElement`). EVERY form-capable `@godxjp/ui` component accepts
|
|
@@ -53,6 +53,34 @@ export declare function omitFieldA11y<T extends FieldA11yProps>(props: T): Omit<
|
|
|
53
53
|
* both — `aria-labelledby` takes precedence and the redundant `aria-label` only adds noise).
|
|
54
54
|
*/
|
|
55
55
|
export declare function resolveFieldA11y(props: FieldA11yProps, intrinsicAriaLabel?: string): FieldA11yProps;
|
|
56
|
+
/**
|
|
57
|
+
* The FormField label made reachable by NESTED controls (gh#303).
|
|
58
|
+
*
|
|
59
|
+
* `cloneElement` can only wire the field-a11y contract onto FormField's single direct child. When
|
|
60
|
+
* that child is a layout wrapper (a `Flex` holding a range from/to pair, a 年/月 input+select
|
|
61
|
+
* combo), the naming attributes stop on the wrapper `div` and every control inside is left with no
|
|
62
|
+
* accessible name at all (axe: `label` on the inputs, `button-name` on select/combobox triggers).
|
|
63
|
+
*
|
|
64
|
+
* FormField therefore also publishes its label through this context, and each control's semantic
|
|
65
|
+
* focus target picks it up as a LAST-RESORT name via {@link useFieldNameFallback}: a control that
|
|
66
|
+
* already has a name — its own `aria-label`/`aria-labelledby`, or the one FormField cloned onto it
|
|
67
|
+
* as the direct child — keeps it untouched. Multiple nested controls then all announce the field's
|
|
68
|
+
* label; a consumer wanting distinct names (e.g. "開始日" / "終了日") sets `aria-label` per
|
|
69
|
+
* control, which always wins.
|
|
70
|
+
*/
|
|
71
|
+
export interface FieldNameContextValue {
|
|
72
|
+
/** DOM id of FormField's visible label element (the `aria-labelledby` target). */
|
|
73
|
+
labelId: string;
|
|
74
|
+
/** The label's plain-text content, when it is a string (belt-and-suspenders `aria-label`). */
|
|
75
|
+
label?: string;
|
|
76
|
+
}
|
|
77
|
+
export declare const FieldNameContext: React.Context<FieldNameContextValue | null>;
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the last-resort accessible name for a control's semantic focus target (see
|
|
80
|
+
* {@link FieldNameContext}). Returns `{}` — never clobbering anything — unless the control is
|
|
81
|
+
* inside a FormField AND still nameless after its own props and the cloned contract are applied.
|
|
82
|
+
*/
|
|
83
|
+
export declare function useFieldNameFallback(name: Pick<FieldA11yProps, "aria-label" | "aria-labelledby">): Pick<FieldA11yProps, "aria-label" | "aria-labelledby">;
|
|
56
84
|
/**
|
|
57
85
|
* The field-a11y attributes valid on a **group container** (`role="group"`), used by composite
|
|
58
86
|
* controls that have no single labelable focus target — CheckboxGroup, and the two-input range
|
package/dist/lib/field-a11y.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
1
3
|
const FIELD_A11Y_KEYS = [
|
|
2
4
|
"aria-label",
|
|
3
5
|
"aria-labelledby",
|
|
@@ -34,6 +36,17 @@ function resolveFieldA11y(props, intrinsicAriaLabel) {
|
|
|
34
36
|
}
|
|
35
37
|
return picked;
|
|
36
38
|
}
|
|
39
|
+
const FieldNameContext = React.createContext(null);
|
|
40
|
+
function useFieldNameFallback(name) {
|
|
41
|
+
const field = React.useContext(FieldNameContext);
|
|
42
|
+
if (!field || name["aria-label"] !== void 0 || name["aria-labelledby"] !== void 0) {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
"aria-labelledby": field.labelId,
|
|
47
|
+
...field.label !== void 0 ? { "aria-label": field.label } : {}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
37
50
|
function pickGroupFieldA11y(props) {
|
|
38
51
|
const describedBy = mergeAriaIds(props["aria-describedby"], props["aria-errormessage"]);
|
|
39
52
|
return {
|
|
@@ -42,9 +55,11 @@ function pickGroupFieldA11y(props) {
|
|
|
42
55
|
};
|
|
43
56
|
}
|
|
44
57
|
export {
|
|
58
|
+
FieldNameContext,
|
|
45
59
|
mergeAriaIds,
|
|
46
60
|
omitFieldA11y,
|
|
47
61
|
pickFieldA11y,
|
|
48
62
|
pickGroupFieldA11y,
|
|
49
|
-
resolveFieldA11y
|
|
63
|
+
resolveFieldA11y,
|
|
64
|
+
useFieldNameFallback
|
|
50
65
|
};
|