@godxjp/ui 19.0.0 → 19.1.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.
@@ -2,7 +2,7 @@ import * as React from "react";
2
2
  import { type AppDateFormat, type AppLocale, type AppTimeFormat, type AppTimezone } from "./types.js";
3
3
  import type { AppContextValue, AppProviderProp } from "../props/components/app.prop.js";
4
4
  export type { AppProviderProp, AppContextValue } from "../props/components/app.prop.js";
5
- export declare function AppProvider({ children, defaultLocale, fallbackLocale, defaultTimezone, systemTimezone, defaultTimeFormat, defaultDateFormat, timezoneOptions, storageKey, persist, theme: initialTheme, brand: initialBrand, density: initialDensity, fontSize: initialFontSize, scaling: initialScaling, onLocaleChange, onTimezoneChange, onTimeFormatChange, onDateFormatChange, onThemeChange, onBrandChange, onDensityChange, onFontSizeChange, onScalingChange, }: AppProviderProp): React.JSX.Element;
5
+ export declare function AppProvider({ children, defaultLocale, fallbackLocale, defaultTimezone, systemTimezone, defaultTimeFormat, defaultDateFormat, timezoneOptions, storageKey, persist, theme: initialTheme, brand: initialBrand, density: initialDensity, fontSize: initialFontSize, scaling: initialScaling, emitFieldNames, onLocaleChange, onTimezoneChange, onTimeFormatChange, onDateFormatChange, onThemeChange, onBrandChange, onDensityChange, onFontSizeChange, onScalingChange, }: AppProviderProp): React.JSX.Element;
6
6
  export declare function useAppContext(): AppContextValue;
7
7
  /** Returns null outside AppProvider — used by pickers for optional context. */
8
8
  export declare function useOptionalAppContext(): AppContextValue | null;
@@ -65,6 +65,7 @@ function AppProvider({
65
65
  density: initialDensity = "default",
66
66
  fontSize: initialFontSize = "default",
67
67
  scaling: initialScaling = null,
68
+ emitFieldNames = false,
68
69
  onLocaleChange,
69
70
  onTimezoneChange,
70
71
  onTimeFormatChange,
@@ -299,6 +300,7 @@ function AppProvider({
299
300
  density,
300
301
  fontSize,
301
302
  scaling,
303
+ emitFieldNames,
302
304
  setLocale,
303
305
  setTimezone,
304
306
  setTimeFormat,
@@ -323,6 +325,7 @@ function AppProvider({
323
325
  density,
324
326
  fontSize,
325
327
  scaling,
328
+ emitFieldNames,
326
329
  setLocale,
327
330
  setTimezone,
328
331
  setTimeFormat,
@@ -4,7 +4,7 @@ import * as React from "react";
4
4
  import { Check, ChevronRight, ChevronsUpDown, Minus, X } from "lucide-react";
5
5
  import { useTranslation } from "../../i18n/use-translation.js";
6
6
  import { cn } from "../../lib/utils.js";
7
- import { pickFieldA11y } from "../../lib/field-a11y.js";
7
+ import { pickFieldA11y, useFieldIdentity } from "../../lib/field-a11y.js";
8
8
  import { controlOpenRingClass } from "../../lib/control-styles.js";
9
9
  import { Button } from "../general/button.js";
10
10
  import { Popover, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
@@ -86,6 +86,7 @@ function Cascader({
86
86
  }) {
87
87
  const { t } = useTranslation();
88
88
  const fieldA11y = pickFieldA11y(ariaProps);
89
+ const identity = useFieldIdentity({ id, "data-field": fieldA11y["data-field"] });
89
90
  const reactId = React.useId();
90
91
  const panelId = `${id ?? reactId}-panel`;
91
92
  const options = React.useMemo(
@@ -240,6 +241,7 @@ function Cascader({
240
241
  Button,
241
242
  {
242
243
  id,
244
+ "data-field": fieldA11y["data-field"] ?? identity["data-field"],
243
245
  type: "button",
244
246
  variant: "outline",
245
247
  role: "combobox",
@@ -2,7 +2,7 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { cn } from "../../lib/utils.js";
5
- import { pickGroupFieldA11y } from "../../lib/field-a11y.js";
5
+ import { pickGroupFieldA11y, useFieldIdentity } from "../../lib/field-a11y.js";
6
6
  import { Checkbox } from "./checkbox.js";
7
7
  import { Field } from "./field.js";
8
8
  import { choiceGroupClassName } from "./choice-option.js";
@@ -33,6 +33,10 @@ function CheckboxGroup({
33
33
  const reactId = React.useId();
34
34
  const [value, setValue] = useControllableArray(controlledValue, defaultValue);
35
35
  const groupA11y = pickGroupFieldA11y(ariaProps);
36
+ const identity = useFieldIdentity({ id, name, "data-field": groupA11y["data-field"] });
37
+ const resolvedName = name ?? identity.name;
38
+ const resolvedField = groupA11y["data-field"] ?? identity["data-field"];
39
+ const optionDomId = (optionValue, index) => id ? `${id}-${optionValue}` : `${reactId}-${optionValue}-${index}`;
36
40
  const toggle = (optionValue) => {
37
41
  const next = value.includes(optionValue) ? value.filter((v) => v !== optionValue) : [...value, optionValue];
38
42
  setValue(next);
@@ -45,17 +49,19 @@ function CheckboxGroup({
45
49
  role: "group",
46
50
  id,
47
51
  ...groupA11y,
52
+ "data-field": resolvedField,
48
53
  "aria-disabled": disabled ? true : void 0,
49
54
  "data-orientation": orientation,
50
55
  className: choiceGroupClassName(orientation, className),
51
56
  children: options.map((opt, index) => {
52
- const id2 = `${reactId}-${opt.value}-${index}`;
57
+ const optionId = optionDomId(opt.value, index);
53
58
  const checked = value.includes(opt.value);
54
- return /* @__PURE__ */ jsx(Field, { id: id2, label: opt.label, description: opt.description, children: /* @__PURE__ */ jsx(
59
+ return /* @__PURE__ */ jsx(Field, { id: optionId, label: opt.label, description: opt.description, children: /* @__PURE__ */ jsx(
55
60
  Checkbox,
56
61
  {
57
- id: id2,
58
- name,
62
+ id: optionId,
63
+ name: resolvedName,
64
+ "data-field": resolvedField,
59
65
  value: opt.value,
60
66
  checked,
61
67
  disabled: Boolean(disabled) || Boolean(opt.disabled),
@@ -74,6 +80,7 @@ function CheckboxGroup({
74
80
  role: "group",
75
81
  id,
76
82
  ...groupA11y,
83
+ "data-field": resolvedField,
77
84
  "aria-disabled": disabled ? true : void 0,
78
85
  "data-orientation": orientation,
79
86
  className: cn(choiceGroupClassName(orientation), className),
@@ -4,28 +4,37 @@ import * as React from "react";
4
4
  import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
5
5
  import { Check } from "lucide-react";
6
6
  import { cn } from "../../lib/utils.js";
7
+ import { useFieldIdentity } from "../../lib/field-a11y.js";
7
8
  import { CheckboxGroup } from "./checkbox-group.js";
8
- const CheckboxRoot = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
9
- CheckboxPrimitive.Root,
10
- {
11
- ref,
12
- "data-slot": "checkbox",
13
- className: cn(
14
- // `disabled:cursor-not-allowed disabled:opacity-50` and the checked fill are DELETED, not
15
- // moved (#319): `.ui-checkbox:disabled, .ui-checkbox[data-disabled]` and
16
- // `.ui-checkbox[data-state="checked"]` in styles/control.css already declare both, reading
17
- // --disabled-opacity and --checkbox-checked-background. Utilities are layered AFTER
18
- // components in Tailwind v4, so these literals were silently OUTRANKING those knobs — a
19
- // service overriding --checkbox-checked-background got no fill change at all. Radix sets
20
- // `data-state`/`data-disabled` on the root, so the CSS rules match. Rendering is unchanged:
21
- // both knobs default to exactly the values these utilities hard-coded.
22
- "peer ui-checkbox aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground shrink-0 shadow-xs transition-shadow outline-none",
23
- className
24
- ),
25
- ...props,
26
- children: /* @__PURE__ */ jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", className: "ui-choice-indicator", children: /* @__PURE__ */ jsx(Check, { className: "ui-checkbox-icon", "aria-hidden": "true" }) })
27
- }
28
- ));
9
+ const CheckboxRoot = React.forwardRef(({ className, ...props }, ref) => {
10
+ const identity = useFieldIdentity({
11
+ id: props.id,
12
+ name: props.name,
13
+ "data-field": props["data-field"]
14
+ });
15
+ return /* @__PURE__ */ jsx(
16
+ CheckboxPrimitive.Root,
17
+ {
18
+ ref,
19
+ "data-slot": "checkbox",
20
+ className: cn(
21
+ // `disabled:cursor-not-allowed disabled:opacity-50` and the checked fill are DELETED, not
22
+ // moved (#319): `.ui-checkbox:disabled, .ui-checkbox[data-disabled]` and
23
+ // `.ui-checkbox[data-state="checked"]` in styles/control.css already declare both, reading
24
+ // --disabled-opacity and --checkbox-checked-background. Utilities are layered AFTER
25
+ // components in Tailwind v4, so these literals were silently OUTRANKING those knobs — a
26
+ // service overriding --checkbox-checked-background got no fill change at all. Radix sets
27
+ // `data-state`/`data-disabled` on the root, so the CSS rules match. Rendering is unchanged:
28
+ // both knobs default to exactly the values these utilities hard-coded.
29
+ "peer ui-checkbox aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground shrink-0 shadow-xs transition-shadow outline-none",
30
+ className
31
+ ),
32
+ ...props,
33
+ ...identity,
34
+ children: /* @__PURE__ */ jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", className: "ui-choice-indicator", children: /* @__PURE__ */ jsx(Check, { className: "ui-checkbox-icon", "aria-hidden": "true" }) })
35
+ }
36
+ );
37
+ });
29
38
  CheckboxRoot.displayName = CheckboxPrimitive.Root.displayName;
30
39
  const Checkbox = Object.assign(CheckboxRoot, {
31
40
  Group: CheckboxGroup
@@ -5,7 +5,7 @@ import { CalendarIcon, X } from "lucide-react";
5
5
  import { usePickerLocales, useTranslation } from "../../i18n/use-translation.js";
6
6
  import { parseDateInput, toIsoDate } from "../../lib/datetime/parse.js";
7
7
  import { useControlledLatch } from "../../lib/hooks.js";
8
- import { pickFieldA11y } from "../../lib/field-a11y.js";
8
+ import { pickFieldA11y, useFieldIdentity } from "../../lib/field-a11y.js";
9
9
  import { cn } from "../../lib/utils.js";
10
10
  import { Input } from "./input.js";
11
11
  import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
@@ -30,6 +30,9 @@ function DatePicker({
30
30
  const { dayPickerLocale } = usePickerLocales(localeProp);
31
31
  const [open, setOpen] = React.useState(false);
32
32
  const fieldA11y = pickFieldA11y(ariaProps);
33
+ const identity = useFieldIdentity({ id, name, "data-field": fieldA11y["data-field"] });
34
+ const resolvedName = name ?? identity.name;
35
+ const resolvedField = fieldA11y["data-field"] ?? identity["data-field"];
33
36
  const reactId = React.useId();
34
37
  const dialogId = `${id ?? reactId}-dialog`;
35
38
  const isControlled = useControlledLatch(valueProp !== void 0);
@@ -66,7 +69,8 @@ function DatePicker({
66
69
  Input,
67
70
  {
68
71
  id,
69
- name,
72
+ name: resolvedName,
73
+ "data-field": resolvedField,
70
74
  value: text,
71
75
  disabled,
72
76
  placeholder: resolvedPlaceholder,
@@ -5,7 +5,7 @@ import { ArrowRight, CalendarIcon, X } from "lucide-react";
5
5
  import { usePickerLocales, useTranslation } from "../../i18n/use-translation.js";
6
6
  import { parseDateInput, toIsoDate } from "../../lib/datetime/index.js";
7
7
  import { useControlledLatch } from "../../lib/hooks.js";
8
- import { pickGroupFieldA11y } from "../../lib/field-a11y.js";
8
+ import { pickGroupFieldA11y, useFieldIdentity } from "../../lib/field-a11y.js";
9
9
  import { cn } from "../../lib/utils.js";
10
10
  import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
11
11
  import { Calendar } from "./calendar.js";
@@ -33,6 +33,9 @@ function DateRangePicker({
33
33
  const groupId = id ?? autoId;
34
34
  const fromId = `${groupId}-from`;
35
35
  const toId = `${groupId}-to`;
36
+ const identity = useFieldIdentity({ id: groupId, name, "data-field": groupA11y["data-field"] });
37
+ const rangeField = groupA11y["data-field"] ?? identity["data-field"];
38
+ const rangeName = name ?? identity.name;
36
39
  const isControlled = useControlledLatch(valueProp !== void 0);
37
40
  const [internalValue, setInternalValue] = React.useState(defaultValue);
38
41
  const value = isControlled ? valueProp : internalValue;
@@ -77,6 +80,7 @@ function DateRangePicker({
77
80
  role: "group",
78
81
  id: groupId,
79
82
  ...groupA11y,
83
+ "data-field": rangeField,
80
84
  "aria-disabled": disabled ? true : void 0,
81
85
  className: cn(
82
86
  // One input-styled shell for the whole range — the shared composite-field box, so
@@ -94,7 +98,8 @@ function DateRangePicker({
94
98
  "input",
95
99
  {
96
100
  id: fromId,
97
- name: name ? `${name}_from` : void 0,
101
+ "data-field": rangeField ? `${rangeField}_from` : void 0,
102
+ name: rangeName ? `${rangeName}_from` : void 0,
98
103
  value: fromText,
99
104
  disabled,
100
105
  placeholder: resolvedPlaceholder,
@@ -118,7 +123,8 @@ function DateRangePicker({
118
123
  "input",
119
124
  {
120
125
  id: toId,
121
- name: name ? `${name}_to` : void 0,
126
+ "data-field": rangeField ? `${rangeField}_to` : void 0,
127
+ name: rangeName ? `${rangeName}_to` : void 0,
122
128
  value: toText,
123
129
  disabled,
124
130
  placeholder: resolvedPlaceholder,
@@ -1,4 +1,4 @@
1
1
  import * as React from "react";
2
2
  import type { FormFieldProp } from "../../props/components/data-entry.prop.js";
3
3
  export type { FormFieldProp, FormFieldProp as FormFieldProps, } from "../../props/components/data-entry.prop.js";
4
- export declare function FormField({ id, name, label, required, helper, error: errorProp, labelAddon, layout: layoutProp, labelWidth: labelWidthProp, controlWidth: controlWidthProp, colSpan, className, children, staticText, }: FormFieldProp): React.JSX.Element;
4
+ export declare function FormField({ id, name, field, label, required, helper, error: errorProp, labelAddon, layout: layoutProp, labelWidth: labelWidthProp, controlWidth: controlWidthProp, colSpan, className, children, staticText, }: FormFieldProp): React.JSX.Element;
@@ -3,7 +3,8 @@ 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 { FieldNameContext, mergeAriaIds } from "../../lib/field-a11y.js";
6
+ import { FieldIdentityContext, FieldNameContext, mergeAriaIds } from "../../lib/field-a11y.js";
7
+ import { useOptionalAppContext } from "../../app/app-provider.js";
7
8
  import { useFormLayout } from "./form.js";
8
9
  import { firstBagMessage, useClaimErrorKey, useFormErrorsRegistry } from "./form-errors.js";
9
10
  const toCssLength = (v) => typeof v === "number" ? `${v}px` : v;
@@ -11,6 +12,7 @@ const FOCUSABLE_SELECTOR = 'input:not([type="hidden"]), select, textarea, button
11
12
  function FormField({
12
13
  id,
13
14
  name,
15
+ field,
14
16
  label,
15
17
  required,
16
18
  helper,
@@ -38,6 +40,8 @@ function FormField({
38
40
  const labelId = `${resolvedId}-label`;
39
41
  const helperId = helper ? `${resolvedId}-helper` : void 0;
40
42
  const errorId = error ? `${resolvedId}-error` : void 0;
43
+ const fieldKey = field ?? name ?? id;
44
+ const emitFieldNames = useOptionalAppContext()?.emitFieldNames ?? false;
41
45
  const isStatic = staticText !== void 0;
42
46
  if (!isStatic && typeof process !== "undefined" && process.env?.NODE_ENV !== "production" && !React.isValidElement(children)) {
43
47
  console.warn(
@@ -48,6 +52,10 @@ function FormField({
48
52
  () => ({ labelId, label: typeof label === "string" ? label : void 0 }),
49
53
  [labelId, label]
50
54
  );
55
+ const fieldIdentityContext = React.useMemo(
56
+ () => fieldKey === void 0 ? null : { emitName: emitFieldNames },
57
+ [fieldKey, emitFieldNames]
58
+ );
51
59
  const childProps = React.isValidElement(children) ? children.props : void 0;
52
60
  const mergeIds = mergeAriaIds;
53
61
  const childWithA11y = isStatic ? (
@@ -63,6 +71,11 @@ function FormField({
63
71
  // controls (Radio.Group, checkbox lists, range pairs) have no labelable root,
64
72
  // and a dangling `for` triggers Chrome's "Incorrect use of <label>" issue.
65
73
  id: childProps?.id ?? resolvedId,
74
+ // gh#337 — the machine key. Read the child's own value first in BOTH cases: cloneElement
75
+ // overwrites every key present in the config bag, `undefined` included, so a bare
76
+ // `"data-field": fieldKey` would erase a value the control set for itself.
77
+ "data-field": childProps?.["data-field"] ?? fieldKey,
78
+ ...emitFieldNames ? { name: childProps?.name ?? fieldKey } : void 0,
66
79
  "aria-labelledby": childProps?.["aria-labelledby"] ?? labelId,
67
80
  // Redundant `aria-label` fallback (belt-and-suspenders): the accessible name is the
68
81
  // SAME string as the visible label, just reachable even if an aria-labelledby lookup
@@ -125,7 +138,7 @@ function FormField({
125
138
  labelAddon
126
139
  ] }),
127
140
  /* @__PURE__ */ jsxs("div", { "data-slot": "form-field-control", className: "ui-form-field-control", children: [
128
- isStatic ? childWithA11y : /* @__PURE__ */ jsx(FieldNameContext.Provider, { value: fieldNameContext, children: childWithA11y }),
141
+ isStatic ? childWithA11y : /* @__PURE__ */ jsx(FieldNameContext.Provider, { value: fieldNameContext, children: /* @__PURE__ */ jsx(FieldIdentityContext.Provider, { value: fieldIdentityContext, children: childWithA11y }) }),
129
142
  helper ? /* @__PURE__ */ jsx("p", { id: helperId, className: "text-muted-foreground text-xs", children: helper }) : null,
130
143
  error ? /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "text-destructive text-xs", children: error }) : null
131
144
  ] })
@@ -3,7 +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
+ import { useFieldIdentity, useFieldNameFallback } from "../../lib/field-a11y.js";
7
7
  import { cn } from "../../lib/utils.js";
8
8
  const inputBaseClass = [
9
9
  "ui-control ui-input border-input bg-background w-full rounded-[var(--control-radius)] transition-[color,box-shadow] outline-none",
@@ -29,6 +29,11 @@ const Input = React.forwardRef(
29
29
  "aria-label": props["aria-label"],
30
30
  "aria-labelledby": props["aria-labelledby"]
31
31
  });
32
+ const identity = useFieldIdentity({
33
+ id: props.id,
34
+ name: props.name,
35
+ "data-field": props["data-field"]
36
+ });
32
37
  const innerRef = React.useRef(null);
33
38
  const setRefs = React.useCallback(
34
39
  (node) => {
@@ -74,7 +79,8 @@ const Input = React.forwardRef(
74
79
  onChange,
75
80
  className: cn(inputBaseClass, className),
76
81
  ...props,
77
- ...nameFallback
82
+ ...nameFallback,
83
+ ...identity
78
84
  }
79
85
  );
80
86
  }
@@ -108,7 +114,8 @@ const Input = React.forwardRef(
108
114
  className
109
115
  ),
110
116
  ...props,
111
- ...nameFallback
117
+ ...nameFallback,
118
+ ...identity
112
119
  }
113
120
  ),
114
121
  trailing != null ? /* @__PURE__ */ jsx("span", { className: "ui-input-trailing", children: trailing }) : null
@@ -4,7 +4,7 @@ import * as React from "react";
4
4
  import { CalendarIcon, ChevronLeft, ChevronRight, X } from "lucide-react";
5
5
  import { usePickerLocales, useTranslation } from "../../i18n/use-translation.js";
6
6
  import { useControlledLatch } from "../../lib/hooks.js";
7
- import { pickFieldA11y } from "../../lib/field-a11y.js";
7
+ import { pickFieldA11y, useFieldIdentity } from "../../lib/field-a11y.js";
8
8
  import { cn } from "../../lib/utils.js";
9
9
  import { Button } from "../general/button.js";
10
10
  import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
@@ -38,6 +38,7 @@ function MonthPicker({
38
38
  const inputId = id ?? autoId;
39
39
  const dialogId = `${inputId}-dialog`;
40
40
  const fieldA11y = pickFieldA11y(ariaProps);
41
+ const identity = useFieldIdentity({ id, name, "data-field": fieldA11y["data-field"] });
41
42
  const isControlled = useControlledLatch(valueProp !== void 0);
42
43
  const [internalValue, setInternalValue] = React.useState(defaultValue);
43
44
  const value = isControlled ? valueProp : internalValue;
@@ -76,7 +77,8 @@ function MonthPicker({
76
77
  "input",
77
78
  {
78
79
  id: inputId,
79
- name,
80
+ name: name ?? identity.name,
81
+ "data-field": fieldA11y["data-field"] ?? identity["data-field"],
80
82
  value: text,
81
83
  disabled,
82
84
  placeholder: placeholder ?? t("dataEntry.monthPicker.placeholder") ?? YM_HINT,
@@ -4,7 +4,7 @@ import * as React from "react";
4
4
  import { ArrowRight, CalendarIcon, ChevronLeft, ChevronRight, X } from "lucide-react";
5
5
  import { usePickerLocales, useTranslation } from "../../i18n/use-translation.js";
6
6
  import { useControlledLatch } from "../../lib/hooks.js";
7
- import { pickGroupFieldA11y } from "../../lib/field-a11y.js";
7
+ import { pickGroupFieldA11y, useFieldIdentity } from "../../lib/field-a11y.js";
8
8
  import { cn } from "../../lib/utils.js";
9
9
  import { Button } from "../general/button.js";
10
10
  import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
@@ -40,6 +40,9 @@ function MonthRangePicker({
40
40
  const groupId = id ?? autoId;
41
41
  const fromId = `${groupId}-from`;
42
42
  const toId = `${groupId}-to`;
43
+ const identity = useFieldIdentity({ id: groupId, name, "data-field": groupA11y["data-field"] });
44
+ const rangeField = groupA11y["data-field"] ?? identity["data-field"];
45
+ const rangeName = name ?? identity.name;
43
46
  const isControlled = useControlledLatch(valueProp !== void 0);
44
47
  const [internalValue, setInternalValue] = React.useState(defaultValue);
45
48
  const value = isControlled ? valueProp : internalValue;
@@ -107,6 +110,7 @@ function MonthRangePicker({
107
110
  role: "group",
108
111
  id: groupId,
109
112
  ...groupA11y,
113
+ "data-field": rangeField,
110
114
  "aria-disabled": disabled ? true : void 0,
111
115
  "data-disabled": disabled ? "" : void 0,
112
116
  className: cn(
@@ -123,7 +127,8 @@ function MonthRangePicker({
123
127
  "input",
124
128
  {
125
129
  id: fromId,
126
- name: name ? `${name}_from` : void 0,
130
+ "data-field": rangeField ? `${rangeField}_from` : void 0,
131
+ name: rangeName ? `${rangeName}_from` : void 0,
127
132
  value: fromText,
128
133
  disabled,
129
134
  placeholder: resolvedPlaceholder,
@@ -144,7 +149,8 @@ function MonthRangePicker({
144
149
  "input",
145
150
  {
146
151
  id: toId,
147
- name: name ? `${name}_to` : void 0,
152
+ "data-field": rangeField ? `${rangeField}_to` : void 0,
153
+ name: rangeName ? `${rangeName}_to` : void 0,
148
154
  value: toText,
149
155
  disabled,
150
156
  placeholder: resolvedPlaceholder,
@@ -4,7 +4,7 @@ import * as React from "react";
4
4
  import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
5
5
  import { Circle } from "lucide-react";
6
6
  import { cn } from "../../lib/utils.js";
7
- import { pickFieldA11y } from "../../lib/field-a11y.js";
7
+ import { pickFieldA11y, useFieldIdentity } from "../../lib/field-a11y.js";
8
8
  import { Field } from "./field.js";
9
9
  import { choiceGroupClassName } from "./choice-option.js";
10
10
  const RadioGroupRoot = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
@@ -58,6 +58,10 @@ function RadioGroupOptions({
58
58
  }) {
59
59
  const reactId = React.useId();
60
60
  const groupA11y = pickFieldA11y(ariaProps);
61
+ const identity = useFieldIdentity({ id, name, "data-field": groupA11y["data-field"] });
62
+ const resolvedName = name ?? identity.name;
63
+ const resolvedField = groupA11y["data-field"] ?? identity["data-field"];
64
+ const optionDomId = (optionValue, index) => id ? `${id}-${optionValue}` : `${reactId}-${optionValue}-${index}`;
61
65
  if (options && options.length > 0) {
62
66
  return /* @__PURE__ */ jsx(
63
67
  RadioGroupRoot,
@@ -66,14 +70,23 @@ function RadioGroupOptions({
66
70
  defaultValue,
67
71
  onValueChange,
68
72
  disabled,
69
- name,
73
+ name: resolvedName,
70
74
  id,
71
75
  ...groupA11y,
76
+ "data-field": resolvedField,
72
77
  "data-orientation": orientation,
73
78
  className: choiceGroupClassName(orientation, className),
74
79
  children: options.map((opt, index) => {
75
- const id2 = `${reactId}-${opt.value}-${index}`;
76
- return /* @__PURE__ */ jsx(Field, { id: id2, label: opt.label, description: opt.description, children: /* @__PURE__ */ jsx(RadioItem, { id: id2, value: opt.value, disabled: opt.disabled }) }, opt.value);
80
+ const optionId = optionDomId(opt.value, index);
81
+ return /* @__PURE__ */ jsx(Field, { id: optionId, label: opt.label, description: opt.description, children: /* @__PURE__ */ jsx(
82
+ RadioItem,
83
+ {
84
+ id: optionId,
85
+ value: opt.value,
86
+ disabled: opt.disabled,
87
+ "data-field": resolvedField
88
+ }
89
+ ) }, opt.value);
77
90
  })
78
91
  }
79
92
  );
@@ -85,9 +98,10 @@ function RadioGroupOptions({
85
98
  defaultValue,
86
99
  onValueChange,
87
100
  disabled,
88
- name,
101
+ name: resolvedName,
89
102
  id,
90
103
  ...groupA11y,
104
+ "data-field": resolvedField,
91
105
  "data-orientation": orientation,
92
106
  className: choiceGroupClassName(orientation, className),
93
107
  children
@@ -9,4 +9,4 @@ export type { SearchSelectProp, SearchSelectProp as SearchSelectProps, SearchSel
9
9
  * Custom per-option rendering via `renderOption` (Ant-Design style). Form-submittable via
10
10
  * `name`; e2e-testable by the trigger's `data-testid` + each option's `${data-testid}-option-${value}`.
11
11
  */
12
- export declare function SearchSelect({ value: valueProp, defaultValue, onValueChange, options: staticOptions, loadOptions, renderOption, labelRender, selectedLabel, selectedIcon, placeholder, searchPlaceholder, emptyMessage, loadingMessage, errorMessage, clearLabel, clearable, disabled, readOnly, size, open: openProp, onOpenChange, search: searchProp, onSearchChange, filterOption, renderError, renderLoadMore, name, id, className, "data-testid": dataTestId, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-errormessage": ariaErrorMessage, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, }: SearchSelectProp): React.JSX.Element;
12
+ export declare function SearchSelect({ value: valueProp, defaultValue, onValueChange, options: staticOptions, loadOptions, renderOption, labelRender, selectedLabel, selectedIcon, placeholder, searchPlaceholder, emptyMessage, loadingMessage, errorMessage, clearLabel, clearable, disabled, readOnly, size, open: openProp, onOpenChange, search: searchProp, onSearchChange, filterOption, renderError, renderLoadMore, name, id, className, "data-testid": dataTestId, "data-field": dataField, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-errormessage": ariaErrorMessage, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, }: SearchSelectProp): React.JSX.Element;
@@ -3,7 +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
+ import { useFieldIdentity, useFieldNameFallback } from "../../lib/field-a11y.js";
7
7
  import { cn } from "../../lib/utils.js";
8
8
  import { controlOpenRingClass } from "../../lib/control-styles.js";
9
9
  import { Button } from "../general/button.js";
@@ -42,6 +42,7 @@ function SearchSelect({
42
42
  id,
43
43
  className,
44
44
  "data-testid": dataTestId,
45
+ "data-field": dataField,
45
46
  "aria-label": ariaLabel,
46
47
  "aria-labelledby": ariaLabelledby,
47
48
  "aria-describedby": ariaDescribedby,
@@ -54,6 +55,9 @@ function SearchSelect({
54
55
  "aria-label": ariaLabel,
55
56
  "aria-labelledby": ariaLabelledby
56
57
  });
58
+ const identity = useFieldIdentity({ id, name, "data-field": dataField });
59
+ const resolvedName = name ?? identity.name;
60
+ const resolvedField = dataField ?? identity["data-field"];
57
61
  const triggerAriaLabel = ariaLabel ?? nameFallback["aria-label"];
58
62
  const triggerAriaLabelledby = ariaLabelledby ?? nameFallback["aria-labelledby"];
59
63
  const reactId = React.useId();
@@ -254,6 +258,8 @@ function SearchSelect({
254
258
  "aria-readonly": readOnly || void 0,
255
259
  disabled,
256
260
  "data-testid": dataTestId,
261
+ "data-field": resolvedField,
262
+ "data-value": value || void 0,
257
263
  className: cn(
258
264
  "w-full justify-start font-normal",
259
265
  controlOpenRingClass,
@@ -275,7 +281,7 @@ function SearchSelect({
275
281
  )
276
282
  }
277
283
  ) }),
278
- name ? /* @__PURE__ */ jsx("input", { type: "hidden", name, value, readOnly: true }) : null,
284
+ resolvedName ? /* @__PURE__ */ jsx("input", { type: "hidden", name: resolvedName, value, readOnly: true }) : null,
279
285
  /* @__PURE__ */ jsx(
280
286
  PopoverContent,
281
287
  {
@@ -27,6 +27,14 @@ export declare const SelectTrigger: React.ForwardRefExoticComponent<Omit<SelectP
27
27
  * descendant CSS is needed to remove it.
28
28
  */
29
29
  showIndicator?: boolean;
30
+ /** Stable machine key (gh#337) — normally inherited from `FormField` through the Select. */
31
+ "data-field"?: string;
32
+ /**
33
+ * The selected CODE (gh#337). Normally supplied by `Select` itself — the trigger shows the
34
+ * option's LABEL, and this is the only place the underlying value is readable from the DOM
35
+ * without reaching into Radix's aria-hidden native `<select>`.
36
+ */
37
+ "data-value"?: string;
30
38
  } & React.RefAttributes<HTMLButtonElement>>;
31
39
  export declare const SelectScrollUpButton: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectScrollUpButtonProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
32
40
  export declare const SelectScrollDownButton: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectScrollDownButtonProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
@@ -7,6 +7,7 @@ import { cn } from "../../lib/utils.js";
7
7
  import { controlTriggerClass } from "../../lib/control-styles.js";
8
8
  import {
9
9
  mergeAriaIds,
10
+ useFieldIdentity,
10
11
  omitFieldA11y,
11
12
  pickFieldA11y,
12
13
  useFieldNameFallback
@@ -26,7 +27,36 @@ function Select(props) {
26
27
  function CompoundSelect({ id, ...props }) {
27
28
  const fieldA11y = pickFieldA11y(props);
28
29
  const rootProps = omitFieldA11y(props);
29
- return /* @__PURE__ */ jsx(SelectFieldA11yContext.Provider, { value: { ...fieldA11y, id }, children: /* @__PURE__ */ jsx(SelectPrimitive.Root, { "data-slot": "select", ...rootProps }) });
30
+ const [uncontrolled, setUncontrolled] = React.useState(props.defaultValue);
31
+ const value = props.value ?? uncontrolled;
32
+ const identity = useFieldIdentity({
33
+ id,
34
+ name: props.name,
35
+ "data-field": fieldA11y["data-field"]
36
+ });
37
+ return /* @__PURE__ */ jsx(
38
+ SelectFieldA11yContext.Provider,
39
+ {
40
+ value: {
41
+ ...fieldA11y,
42
+ "data-field": identity["data-field"] ?? fieldA11y["data-field"],
43
+ id,
44
+ value
45
+ },
46
+ children: /* @__PURE__ */ jsx(
47
+ SelectPrimitive.Root,
48
+ {
49
+ "data-slot": "select",
50
+ ...rootProps,
51
+ name: props.name ?? identity.name,
52
+ onValueChange: (next) => {
53
+ setUncontrolled(next);
54
+ props.onValueChange?.(next);
55
+ }
56
+ }
57
+ )
58
+ }
59
+ );
30
60
  }
31
61
  function SelectGroup(props) {
32
62
  return /* @__PURE__ */ jsx(SelectPrimitive.Group, { "data-slot": "select-group", ...props });
@@ -54,8 +84,10 @@ const SelectTrigger = React.forwardRef(({ className, children, size = "md", show
54
84
  field?.["aria-errormessage"]
55
85
  ),
56
86
  "aria-required": props["aria-required"] ?? field?.["aria-required"],
57
- "aria-invalid": props["aria-invalid"] ?? field?.["aria-invalid"]
87
+ "aria-invalid": props["aria-invalid"] ?? field?.["aria-invalid"],
88
+ "data-field": props["data-field"] ?? field?.["data-field"]
58
89
  } : void 0;
90
+ const dataValue = (props["data-value"] ?? field?.value) || void 0;
59
91
  return /* @__PURE__ */ jsxs(
60
92
  SelectPrimitive.Trigger,
61
93
  {
@@ -69,6 +101,7 @@ const SelectTrigger = React.forwardRef(({ className, children, size = "md", show
69
101
  ),
70
102
  ...props,
71
103
  ...fieldA11y,
104
+ "data-value": dataValue,
72
105
  children: [
73
106
  children,
74
107
  showIndicator ? /* @__PURE__ */ jsx(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx(
@@ -212,12 +245,18 @@ function DataSelect({
212
245
  id,
213
246
  className,
214
247
  "data-testid": dataTestId,
248
+ "data-field": dataField,
215
249
  ...rest
216
250
  }) {
217
251
  const ariaProps = Object.fromEntries(
218
252
  Object.entries(rest).filter(([key]) => key.startsWith("aria-"))
219
253
  );
220
254
  const { t } = useTranslation();
255
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
256
+ const currentValue = value ?? uncontrolledValue;
257
+ const identity = useFieldIdentity({ id, name, "data-field": dataField });
258
+ const resolvedName = name ?? identity.name;
259
+ const resolvedField = dataField ?? identity["data-field"];
221
260
  const searchable = showSearch ?? Boolean(loadOptions);
222
261
  const hasOptions = options.length > 0;
223
262
  if (searchable) {
@@ -250,10 +289,11 @@ function DataSelect({
250
289
  filterOption,
251
290
  renderError,
252
291
  renderLoadMore,
253
- name,
292
+ name: resolvedName,
254
293
  id,
255
294
  className,
256
295
  "data-testid": dataTestId,
296
+ "data-field": resolvedField,
257
297
  ...ariaProps
258
298
  }
259
299
  );
@@ -278,18 +318,23 @@ function DataSelect({
278
318
  "data-slot": "select",
279
319
  value: isControlled ? value : void 0,
280
320
  defaultValue: isControlled ? void 0 : defaultValue || void 0,
281
- onValueChange: (next) => onValueChange?.(
282
- next,
283
- options.find((option) => option.value === next)
284
- ),
321
+ onValueChange: (next) => {
322
+ setUncontrolledValue(next);
323
+ onValueChange?.(
324
+ next,
325
+ options.find((option) => option.value === next)
326
+ );
327
+ },
285
328
  disabled: disabled || !hasOptions,
286
- name,
329
+ name: resolvedName,
287
330
  children: [
288
331
  /* @__PURE__ */ jsx(
289
332
  SelectTrigger,
290
333
  {
291
334
  id,
292
335
  "data-testid": dataTestId,
336
+ "data-field": resolvedField,
337
+ "data-value": currentValue || void 0,
293
338
  className: cn(showClear && "ui-control-trigger-affixed", canClear ? void 0 : className),
294
339
  showIndicator: !showClear,
295
340
  ...ariaProps,
@@ -3,8 +3,15 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as SwitchPrimitive from "@radix-ui/react-switch";
5
5
  import { cn } from "../../lib/utils.js";
6
+ import { useFieldIdentity } from "../../lib/field-a11y.js";
6
7
  const Switch = React.forwardRef(
7
8
  ({ className, size = "md", name, checked, defaultChecked = false, onCheckedChange, ...props }, ref) => {
9
+ const identity = useFieldIdentity({
10
+ id: props.id,
11
+ name,
12
+ "data-field": props["data-field"]
13
+ });
14
+ const resolvedName = name ?? identity.name;
8
15
  const [internalChecked, setInternalChecked] = React.useState(defaultChecked);
9
16
  const isControlled = checked !== void 0;
10
17
  const isChecked = isControlled ? checked : internalChecked;
@@ -15,7 +22,7 @@ const Switch = React.forwardRef(
15
22
  onCheckedChange?.(next);
16
23
  };
17
24
  return /* @__PURE__ */ jsxs(Fragment, { children: [
18
- name ? /* @__PURE__ */ jsx("input", { type: "hidden", name, value: isChecked ? "1" : "0", readOnly: true }) : null,
25
+ resolvedName ? /* @__PURE__ */ jsx("input", { type: "hidden", name: resolvedName, value: isChecked ? "1" : "0", readOnly: true }) : null,
19
26
  /* @__PURE__ */ jsx(
20
27
  SwitchPrimitive.Root,
21
28
  {
@@ -35,6 +42,7 @@ const Switch = React.forwardRef(
35
42
  className
36
43
  ),
37
44
  ...props,
45
+ "data-field": identity["data-field"] ?? props["data-field"],
38
46
  children: /* @__PURE__ */ jsx(SwitchPrimitive.Thumb, { "data-slot": "switch-thumb", className: "ui-switch-thumb" })
39
47
  }
40
48
  )
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import { X } from "lucide-react";
5
5
  import { useTranslation } from "../../i18n/use-translation.js";
6
6
  import { cn } from "../../lib/utils.js";
7
+ import { useFieldIdentity } from "../../lib/field-a11y.js";
7
8
  import { controlMultilineClass, controlMultilineGhostClass } from "../../lib/control-styles.js";
8
9
  const UNBOUNDED_ROWS = "infinity";
9
10
  const Textarea = React.forwardRef(
@@ -26,6 +27,11 @@ const Textarea = React.forwardRef(
26
27
  }, ref) => {
27
28
  const { t } = useTranslation();
28
29
  const base = variant === "ghost" ? controlMultilineGhostClass : controlMultilineClass;
30
+ const identity = useFieldIdentity({
31
+ id: props.id,
32
+ name: props.name,
33
+ "data-field": props["data-field"]
34
+ });
29
35
  const innerRef = React.useRef(null);
30
36
  const setRefs = React.useCallback(
31
37
  (node) => {
@@ -110,7 +116,8 @@ const Textarea = React.forwardRef(
110
116
  onCompositionEnd: handleCompositionEnd,
111
117
  style,
112
118
  className: cn(base, showClear && "ui-input--trailing-affix", className),
113
- ...props
119
+ ...props,
120
+ ...identity
114
121
  }
115
122
  );
116
123
  if (!needsWrapper) return field;
@@ -4,7 +4,7 @@ import * as React from "react";
4
4
  import { ChevronDown, ChevronRight, ChevronsUpDown, X } from "lucide-react";
5
5
  import { useTranslation } from "../../i18n/use-translation.js";
6
6
  import { cn } from "../../lib/utils.js";
7
- import { pickFieldA11y } from "../../lib/field-a11y.js";
7
+ import { pickFieldA11y, useFieldIdentity } from "../../lib/field-a11y.js";
8
8
  import { controlOpenRingClass } from "../../lib/control-styles.js";
9
9
  import { Button } from "../general/button.js";
10
10
  import { Popover, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
@@ -63,6 +63,7 @@ function TreeSelectRoot({
63
63
  }) {
64
64
  const { t } = useTranslation();
65
65
  const fieldA11y = pickFieldA11y(ariaProps);
66
+ const identity = useFieldIdentity({ id, "data-field": fieldA11y["data-field"] });
66
67
  const reactId = React.useId();
67
68
  const treeId = `${id ?? reactId}-tree`;
68
69
  const options = React.useMemo(
@@ -179,6 +180,7 @@ function TreeSelectRoot({
179
180
  Button,
180
181
  {
181
182
  id,
183
+ "data-field": fieldA11y["data-field"] ?? identity["data-field"],
182
184
  type: "button",
183
185
  variant: "outline",
184
186
  role: "combobox",
@@ -13,6 +13,12 @@ import * as React from "react";
13
13
  * - `aria-errormessage` + `aria-invalid` — the validation message, announced when invalid.
14
14
  * - `aria-required` — required-field semantics.
15
15
  * - `aria-label` — a name supplied directly (used when there is no visible label element).
16
+ *
17
+ * `data-field` travels the SAME route (gh#337) and is therefore part of this bag even though it is
18
+ * not an ARIA attribute. It is the field's stable machine key — the `data-testid` role, standardised
19
+ * on one attribute so a control never has to be found by a generated id or by its visible Japanese
20
+ * label. It is inert (read-only metadata) and must land on the same semantic focus target the aria
21
+ * relationships do; routing it through a second, parallel mechanism is how a control gets missed.
16
22
  */
17
23
  export interface FieldA11yProps {
18
24
  "aria-label"?: string;
@@ -21,6 +27,8 @@ export interface FieldA11yProps {
21
27
  "aria-errormessage"?: string;
22
28
  "aria-invalid"?: React.AriaAttributes["aria-invalid"];
23
29
  "aria-required"?: React.AriaAttributes["aria-required"];
30
+ /** Stable machine key of the field (gh#337) — see the interface doc above. */
31
+ "data-field"?: string;
24
32
  }
25
33
  /**
26
34
  * Merge one or more space-separated id-reference lists (`aria-describedby`, `aria-labelledby`,
@@ -81,6 +89,49 @@ export declare const FieldNameContext: React.Context<FieldNameContextValue | nul
81
89
  * inside a FormField AND still nameless after its own props and the cloned contract are applied.
82
90
  */
83
91
  export declare function useFieldNameFallback(name: Pick<FieldA11yProps, "aria-label" | "aria-labelledby">): Pick<FieldA11yProps, "aria-label" | "aria-labelledby">;
92
+ /**
93
+ * The enclosing `FormField`, made reachable by NESTED controls (gh#337).
94
+ *
95
+ * `cloneElement` reaches FormField's single DIRECT child only. In the real screens this library
96
+ * serves, 322 of 1,410 controls (23%) sit one level deeper — the direct child is a `Flex` holding a
97
+ * from/to pair, a 年/月 combo, or a value + 「不明」 checkbox — so the machine key stopped on the
98
+ * wrapper `div` and the controls inside stayed anonymous. The customer's acceptance condition is
99
+ * 「画面に見えている入力欄の100%に付与されていること」; 77% does not pass.
100
+ *
101
+ * The key for such a control is its OWN `id`, and that is a finding, not a convention invented
102
+ * here: of those 322, **250 already carry a static id** and the ambiguous case the wrapper creates
103
+ * — two controls under one field — is already distinguished at the call site
104
+ * (`search_billing_date_from` / `..._to`, `fax` / `fax_unknown`). So a nested control names itself
105
+ * and no two controls can ever end up sharing a key. The remaining 72 (48 computed ids, 24 with no
106
+ * id at all) get NOTHING: a fabricated key is worse than a missing one, because automation would
107
+ * bind to it and break silently on the next render.
108
+ */
109
+ export interface FieldIdentityContextValue {
110
+ /** Whether the app opted into a native `name` (@see AppProviderProp.emitFieldNames). */
111
+ emitName: boolean;
112
+ }
113
+ export declare const FieldIdentityContext: React.Context<FieldIdentityContextValue | null>;
114
+ /**
115
+ * Resolve `data-field` / `name` for a control nested under a `FormField` (see
116
+ * {@link FieldIdentityContext}). Returns `{}` — adding nothing — in every case but the one it
117
+ * exists for, which is what keeps it safe to call from every control:
118
+ *
119
+ * - **outside a FormField** → `{}`. A control elsewhere on the page is untouched.
120
+ * - **`data-field` already present** → `{}`. Either `FormField` cloned it onto its direct child, or
121
+ * a composite (DatePicker, Select) already resolved this field and is passing it down. Whoever
122
+ * owns the field owns BOTH attributes, so `name` is not second-guessed here either — that is what
123
+ * keeps DatePicker's ISO mirror (`name` belongs to the hidden `yyyy-MM-dd` input, not the visible
124
+ * `yyyy/MM/dd` one) and Select's native `<select>` intact.
125
+ * - **no `id` of its own** → `{}`. Nothing to derive a stable key from; see the interface doc.
126
+ */
127
+ export declare function useFieldIdentity(own: {
128
+ id?: string;
129
+ name?: string;
130
+ "data-field"?: string;
131
+ }): {
132
+ "data-field"?: string;
133
+ name?: string;
134
+ };
84
135
  /**
85
136
  * The field-a11y attributes valid on a **group container** (`role="group"`), used by composite
86
137
  * controls that have no single labelable focus target — CheckboxGroup, and the two-input range
@@ -98,4 +149,5 @@ export declare function useFieldNameFallback(name: Pick<FieldA11yProps, "aria-la
98
149
  export declare function pickGroupFieldA11y(props: FieldA11yProps): {
99
150
  "aria-labelledby"?: string;
100
151
  "aria-describedby"?: string;
152
+ "data-field"?: string;
101
153
  };
@@ -6,7 +6,8 @@ const FIELD_A11Y_KEYS = [
6
6
  "aria-describedby",
7
7
  "aria-errormessage",
8
8
  "aria-invalid",
9
- "aria-required"
9
+ "aria-required",
10
+ "data-field"
10
11
  ];
11
12
  function mergeAriaIds(...values) {
12
13
  return Array.from(new Set(values.flatMap((value) => value?.split(/\s+/).filter(Boolean) ?? []))).join(
@@ -47,19 +48,33 @@ function useFieldNameFallback(name) {
47
48
  ...field.label !== void 0 ? { "aria-label": field.label } : {}
48
49
  };
49
50
  }
51
+ const FieldIdentityContext = React.createContext(null);
52
+ function useFieldIdentity(own) {
53
+ const ctx = React.useContext(FieldIdentityContext);
54
+ if (!ctx || own["data-field"] !== void 0 || own.id === void 0) return {};
55
+ return {
56
+ "data-field": own.id,
57
+ ...ctx.emitName && own.name === void 0 ? { name: own.id } : {}
58
+ };
59
+ }
50
60
  function pickGroupFieldA11y(props) {
51
61
  const describedBy = mergeAriaIds(props["aria-describedby"], props["aria-errormessage"]);
52
62
  return {
53
63
  ...props["aria-labelledby"] !== void 0 ? { "aria-labelledby": props["aria-labelledby"] } : {},
54
- ...describedBy !== void 0 ? { "aria-describedby": describedBy } : {}
64
+ ...describedBy !== void 0 ? { "aria-describedby": describedBy } : {},
65
+ // Not an ARIA attribute and so not subject to the role="group" restriction above — a group
66
+ // still has to be findable by its field key (gh#337).
67
+ ...props["data-field"] !== void 0 ? { "data-field": props["data-field"] } : {}
55
68
  };
56
69
  }
57
70
  export {
71
+ FieldIdentityContext,
58
72
  FieldNameContext,
59
73
  mergeAriaIds,
60
74
  omitFieldA11y,
61
75
  pickFieldA11y,
62
76
  pickGroupFieldA11y,
63
77
  resolveFieldA11y,
78
+ useFieldIdentity,
64
79
  useFieldNameFallback
65
80
  };
@@ -49,6 +49,19 @@ export type AppProviderProp = {
49
49
  * number (e.g. `0.95`) overrides it. Type is a separate axis — not scaled.
50
50
  */
51
51
  scaling?: number | null;
52
+ /**
53
+ * Emit a native `name` on every control a `FormField` wraps, taken from the field's key
54
+ * (`field` → `name` → `id`). Default `false`, and deliberately opt-in (gh#337).
55
+ *
56
+ * `data-field` is inert metadata and is always emitted; `name` is NOT — it changes what a
57
+ * native `<form>` submit sends. An app whose controls have never carried a `name` would start
58
+ * posting extra keys to its backend the moment it upgraded the library. That is a behaviour
59
+ * change no shared package may make by default, so the app that WANTS it (a screen-automation /
60
+ * RPA contract, native form posts) turns it on here, once, and every FormField in it obeys.
61
+ *
62
+ * A `name` written on the control itself always wins over the injected one.
63
+ */
64
+ emitFieldNames?: boolean;
52
65
  onLocaleChange?: (locale: AppLocale) => void;
53
66
  onTimezoneChange?: (timezone: AppTimezone) => void;
54
67
  onTimeFormatChange?: (timeFormat: AppTimeFormat) => void;
@@ -117,6 +130,8 @@ export type AppContextValue = {
117
130
  density: AppDensity;
118
131
  fontSize: AppFontSize;
119
132
  scaling: number | null;
133
+ /** Whether `FormField` injects a native `name` onto its control (gh#337). Default `false`. */
134
+ emitFieldNames: boolean;
120
135
  setLocale: (locale: AppLocale) => void;
121
136
  setTimezone: (timezone: AppTimezone) => void;
122
137
  setTimeFormat: (timeFormat: AppTimeFormat) => void;
@@ -130,10 +130,22 @@ export type FormFieldProp = {
130
130
  /**
131
131
  * Error-bag key of this field. When the surrounding `Form` carries `errors`, the field
132
132
  * resolves its message from `errors[name]` automatically (an explicit `error` prop wins)
133
- * and CLAIMS the key so `<FormErrors />` does not repeat it. Not injected into the child —
134
- * pass `name` on the control itself for native form submission.
133
+ * and CLAIMS the key so `<FormErrors />` does not repeat it. Also the default source of
134
+ * {@link FormFieldProp.field} (below), so a field that already names its error key does not
135
+ * repeat itself.
135
136
  */
136
137
  name?: NameProp;
138
+ /**
139
+ * Stable MACHINE key of this field — the bare column/parameter name (`project_name`), not a
140
+ * bracketed legacy path (gh#337). Rendered on the control as `data-field`, and as a native
141
+ * `name` when the app opted in via `<AppProvider emitFieldNames>`.
142
+ *
143
+ * Defaults to `name`, then `id` — which is why an app whose fields already carry a
144
+ * column-named `id` gets the attribute on every control without editing a single screen.
145
+ * Set it explicitly only where `id` is a DOM-uniqueness artefact rather than the field key
146
+ * (`id="source_slip_field"` for `field="source_slip_id"`).
147
+ */
148
+ field?: NameProp;
137
149
  label: LabelProp;
138
150
  required?: RequiredProp;
139
151
  helper?: HelperProp;
@@ -160,6 +172,8 @@ export type FormFieldProp = {
160
172
  * and CLAIMS the key so `<FormErrors />` does not repeat it.
161
173
  */
162
174
  name?: NameProp;
175
+ /** Stable machine key — see the `children` variant above. Unused on a read-only row. */
176
+ field?: NameProp;
163
177
  label: LabelProp;
164
178
  required?: RequiredProp;
165
179
  helper?: HelperProp;
@@ -490,6 +504,8 @@ export type SearchSelectProp = {
490
504
  "aria-invalid"?: boolean | "true" | "false";
491
505
  "aria-required"?: boolean | "true" | "false";
492
506
  "data-testid"?: string;
507
+ /** Stable machine key, forwarded to the trigger (gh#337). Normally injected by `FormField`. */
508
+ "data-field"?: string;
493
509
  };
494
510
  /**
495
511
  * Data-driven (Ant-style) form of {@link Select} — one component covering static `options` or
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "19.0.0",
4
- "godxUiMcp": "19.0.0",
3
+ "version": "19.1.0",
4
+ "godxUiMcp": "19.1.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -333,10 +333,10 @@
333
333
  "check:final-touch-rtl": "node scripts/check-final-touch-rtl.mjs",
334
334
  "check:frame-runtime": "pnpm check:data-entry-frame-runtime && pnpm check:data-entry-touch-aria && pnpm check:layout-nav-frames && pnpm check:provider-feedback-query-runtime && pnpm check:layout-nav-closure && pnpm check:final-touch-rtl && pnpm check:data-table-pagination-wrap && pnpm check:button-icon-xs",
335
335
  "pretest": "pnpm check:frame-coverage",
336
- "verify": "pnpm typecheck && pnpm lint && pnpm format && pnpm build && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:token-scale-bypass && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-token-sync && pnpm check:email-token-sync && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:frame-coverage && pnpm test",
336
+ "verify": "pnpm typecheck && pnpm lint && pnpm format && pnpm build && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:no-external-assets && pnpm check:token-scale-bypass && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-token-sync && pnpm check:email-token-sync && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:frame-coverage && pnpm test",
337
337
  "verify:static": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:token-scale-bypass && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-token-sync && pnpm check:email-token-sync && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:mcp-prop-sync && pnpm check:contrast && pnpm check:visual-audit && pnpm test",
338
338
  "verify:ci": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:token-scale-bypass && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-token-sync && pnpm check:email-token-sync && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:mcp-prop-sync && pnpm test",
339
- "verify:ci:static": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:token-scale-bypass && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-token-sync && pnpm check:email-token-sync && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:mcp-prop-sync",
339
+ "verify:ci:static": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:no-external-assets && pnpm check:token-scale-bypass && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-token-sync && pnpm check:email-token-sync && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:mcp-prop-sync",
340
340
  "verify:browser": "pnpm check:contrast && pnpm check:visual-audit",
341
341
  "verify:release": "pnpm verify:static && pnpm check:frame-contracts && pnpm check:frame-coverage && pnpm check:frame-axe",
342
342
  "verify:publish-tree": "pnpm build && pnpm check:packed-public-contract && pnpm check:use-client && pnpm check:dist-tokens-resolve && pnpm check:mcp-lockstep",
@@ -357,6 +357,7 @@
357
357
  "check:token-scale-bypass": "node scripts/check-token-scale-bypass.mjs",
358
358
  "check:dist-tokens-resolve": "node scripts/check-dist-tokens-resolve.mjs",
359
359
  "check:no-hardcoded-geometry": "node scripts/check-no-hardcoded-geometry.mjs",
360
+ "check:no-external-assets": "node scripts/check-no-external-assets.mjs",
360
361
  "check:no-hardcoded-css-values": "node scripts/check-no-hardcoded-css-values.mjs",
361
362
  "check:no-inline-magic-numbers": "node scripts/check-no-inline-magic-numbers.mjs",
362
363
  "check:no-tailwind-class-assertions": "node scripts/check-no-tailwind-class-assertions.mjs",