@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.
@@ -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": ariaLabel,
242
- "aria-labelledby": ariaLabelledby,
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": ariaLabelledby ? void 0 : ariaLabel ?? resolvedPlaceholder,
276
- "aria-labelledby": ariaLabelledby,
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 { mergeAriaIds, omitFieldA11y, pickFieldA11y } from "../../lib/field-a11y.js";
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 fieldA11y = field ? {
36
- id: props.id ?? field.id,
37
- ...ownsName ? {} : { "aria-label": field["aria-label"], "aria-labelledby": field["aria-labelledby"] },
38
- "aria-describedby": mergeAriaIds(props["aria-describedby"], field["aria-describedby"]),
39
- "aria-errormessage": mergeAriaIds(props["aria-errormessage"], field["aria-errormessage"]),
40
- "aria-required": props["aria-required"] ?? field["aria-required"],
41
- "aria-invalid": props["aria-invalid"] ?? field["aria-invalid"]
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
- ...props,
42
+ ...domProps,
27
43
  children
28
44
  }
29
45
  );
@@ -1,4 +1,4 @@
1
- import type * as React from "react";
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
@@ -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
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "18.13.0",
4
- "godxUiMcp": "18.13.0",
3
+ "version": "18.13.1",
4
+ "godxUiMcp": "18.13.1",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",