@godxjp/ui 17.0.1 → 18.0.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.
Files changed (74) hide show
  1. package/dist/components/data-display/card.d.ts +37 -1
  2. package/dist/components/data-display/card.js +7 -1
  3. package/dist/components/data-display/data-table.js +19 -4
  4. package/dist/components/data-display/empty-state.d.ts +1 -1
  5. package/dist/components/data-display/empty-state.js +5 -1
  6. package/dist/components/data-display/scroll-area.js +1 -1
  7. package/dist/components/data-display/table.d.ts +20 -1
  8. package/dist/components/data-display/table.js +23 -9
  9. package/dist/components/data-entry/calendar.js +4 -0
  10. package/dist/components/data-entry/cascader.d.ts +1 -1
  11. package/dist/components/data-entry/cascader.js +10 -1
  12. package/dist/components/data-entry/checkbox-group.d.ts +1 -1
  13. package/dist/components/data-entry/checkbox-group.js +13 -4
  14. package/dist/components/data-entry/color-picker.d.ts +1 -1
  15. package/dist/components/data-entry/color-picker.js +5 -2
  16. package/dist/components/data-entry/command.d.ts +2 -2
  17. package/dist/components/data-entry/date-picker.d.ts +1 -1
  18. package/dist/components/data-entry/date-picker.js +11 -1
  19. package/dist/components/data-entry/date-range-picker.d.ts +1 -1
  20. package/dist/components/data-entry/date-range-picker.js +11 -3
  21. package/dist/components/data-entry/form-field.js +7 -3
  22. package/dist/components/data-entry/month-picker.d.ts +1 -1
  23. package/dist/components/data-entry/month-picker.js +13 -1
  24. package/dist/components/data-entry/month-range-picker.d.ts +1 -1
  25. package/dist/components/data-entry/month-range-picker.js +11 -3
  26. package/dist/components/data-entry/number-input.d.ts +19 -2
  27. package/dist/components/data-entry/number-input.js +5 -7
  28. package/dist/components/data-entry/radio.d.ts +1 -1
  29. package/dist/components/data-entry/radio.js +11 -3
  30. package/dist/components/data-entry/search-input.d.ts +3 -2
  31. package/dist/components/data-entry/search-input.js +5 -2
  32. package/dist/components/data-entry/search-select.d.ts +1 -1
  33. package/dist/components/data-entry/search-select.js +51 -10
  34. package/dist/components/data-entry/select.d.ts +7 -0
  35. package/dist/components/data-entry/select.js +21 -3
  36. package/dist/components/data-entry/time-picker.d.ts +1 -1
  37. package/dist/components/data-entry/time-picker.js +11 -1
  38. package/dist/components/data-entry/transfer.d.ts +1 -1
  39. package/dist/components/data-entry/transfer.js +73 -59
  40. package/dist/components/data-entry/tree-select.d.ts +1 -1
  41. package/dist/components/data-entry/tree-select.js +10 -1
  42. package/dist/components/data-entry/upload.d.ts +1 -1
  43. package/dist/components/data-entry/upload.js +7 -2
  44. package/dist/components/layout/app-shell.d.ts +1 -1
  45. package/dist/components/layout/app-shell.js +42 -2
  46. package/dist/components/layout/breadcrumb.d.ts +7 -1
  47. package/dist/components/layout/breadcrumb.js +6 -2
  48. package/dist/components/layout/page-container.d.ts +1 -1
  49. package/dist/components/layout/page-container.js +32 -22
  50. package/dist/components/layout/responsive-grid.js +1 -1
  51. package/dist/components/layout/sidebar.d.ts +1 -1
  52. package/dist/components/layout/sidebar.js +44 -11
  53. package/dist/components/layout/split-pane.js +2 -2
  54. package/dist/components/navigation/app-setting-picker.js +15 -7
  55. package/dist/components/navigation/pagination.d.ts +1 -1
  56. package/dist/components/navigation/pagination.js +84 -95
  57. package/dist/components/navigation/tabs.js +21 -4
  58. package/dist/components/ui/tag-input.js +1 -0
  59. package/dist/i18n/messages/en.json +3 -1
  60. package/dist/i18n/messages/ja.json +3 -1
  61. package/dist/i18n/messages/vi.json +3 -1
  62. package/dist/lib/field-a11y.d.ts +66 -0
  63. package/dist/lib/field-a11y.js +44 -0
  64. package/dist/props/components/app.prop.d.ts +9 -5
  65. package/dist/props/components/data-display.prop.d.ts +16 -1
  66. package/dist/props/components/data-entry.prop.d.ts +69 -19
  67. package/dist/props/components/layout.prop.d.ts +42 -0
  68. package/dist/props/components/navigation.prop.d.ts +16 -0
  69. package/dist/props/registry.d.ts +8 -3
  70. package/dist/props/registry.js +21 -3
  71. package/dist/props/vocabulary/data.prop.d.ts +9 -0
  72. package/dist/styles/layout.css +46 -9
  73. package/dist/styles/shell-layout.css +19 -0
  74. package/package.json +8 -4
@@ -1,6 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  import type { LucideIcon } from "lucide-react";
4
+ import type { HeadingLevelProp } from "../../props/vocabulary/index.js";
4
5
  type CardSize = "md" | "compact";
5
6
  /** Semantic leading-edge accent stripe (border-inline-start; width via the
6
7
  * --card-accent-rail-width token, default 6px). */
@@ -37,7 +38,42 @@ export declare const CardHeader: React.ForwardRefExoticComponent<React.HTMLAttri
37
38
  /** Muted background + border-bottom — section band (mirror footer `separated`). */
38
39
  banded?: boolean;
39
40
  } & React.RefAttributes<HTMLDivElement>>;
40
- export declare const CardTitle: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLHeadingElement> & React.RefAttributes<HTMLHeadingElement>>;
41
+ export type CardTitleProps = React.HTMLAttributes<HTMLHeadingElement> & {
42
+ /**
43
+ * Semantic heading level (`h1`–`h4`). Default `3`. Choose it to keep the
44
+ * document outline valid (`h1 → h2 → h3`, no skipped levels) for the page the
45
+ * card sits on — NOT for visual size: the title's size is fixed by the
46
+ * `--card-title-*` tokens and does not change with `level`. A section card
47
+ * directly under a page `h1` uses `level={2}`; a card nested inside an
48
+ * already-`h2` section uses `level={3}` (the default).
49
+ */
50
+ level?: HeadingLevelProp;
51
+ /**
52
+ * Render a different element than the `level` heading. Use `"p"` / `"div"`
53
+ * when the card title is a styled label rather than a section heading in the
54
+ * outline (so it is not announced as a heading and cannot skip a level).
55
+ * Overrides `level`.
56
+ */
57
+ as?: "h1" | "h2" | "h3" | "h4" | "p" | "div";
58
+ };
59
+ export declare const CardTitle: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLHeadingElement> & {
60
+ /**
61
+ * Semantic heading level (`h1`–`h4`). Default `3`. Choose it to keep the
62
+ * document outline valid (`h1 → h2 → h3`, no skipped levels) for the page the
63
+ * card sits on — NOT for visual size: the title's size is fixed by the
64
+ * `--card-title-*` tokens and does not change with `level`. A section card
65
+ * directly under a page `h1` uses `level={2}`; a card nested inside an
66
+ * already-`h2` section uses `level={3}` (the default).
67
+ */
68
+ level?: HeadingLevelProp;
69
+ /**
70
+ * Render a different element than the `level` heading. Use `"p"` / `"div"`
71
+ * when the card title is a styled label rather than a section heading in the
72
+ * outline (so it is not announced as a heading and cannot skip a level).
73
+ * Overrides `level`.
74
+ */
75
+ as?: "h1" | "h2" | "h3" | "h4" | "p" | "div";
76
+ } & React.RefAttributes<HTMLHeadingElement>>;
41
77
  export declare const CardDescription: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLParagraphElement> & React.RefAttributes<HTMLParagraphElement>>;
42
78
  export type CardContentProps = React.HTMLAttributes<HTMLDivElement> & {
43
79
  /** Edge-to-edge body (tables, tabs list). Horizontal padding removed. */
@@ -44,7 +44,13 @@ const CardHeader = React.forwardRef(
44
44
  )
45
45
  );
46
46
  CardHeader.displayName = "CardHeader";
47
- const CardTitle = React.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsx("h3", { ref, "data-slot": "card-title", className, ...props, children }));
47
+ const CardTitle = React.forwardRef(
48
+ ({ className, children, level = 3, as, ...props }, ref) => React.createElement(
49
+ as ?? `h${level}`,
50
+ { ref, "data-slot": "card-title", className, ...props },
51
+ children
52
+ )
53
+ );
48
54
  CardTitle.displayName = "CardTitle";
49
55
  const CardDescription = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("p", { ref, "data-slot": "card-description", className, ...props }));
50
56
  CardDescription.displayName = "CardDescription";
@@ -284,6 +284,9 @@ DataTable.ViewOptions = function DataTableViewOptions({ className }) {
284
284
  ] });
285
285
  };
286
286
  DataTable.ViewOptions.displayName = "DataTable.ViewOptions";
287
+ function isEmptyHeader(header) {
288
+ return header == null || header === "" || header === false;
289
+ }
287
290
  function columnLabel(column) {
288
291
  const header = column.columnDef.meta?.lean?.header;
289
292
  if (typeof header === "string" && header.length > 0) return header;
@@ -386,6 +389,15 @@ DataTable.Content = function DataTableContent() {
386
389
  const visibleColumns = table.getVisibleLeafColumns().map((c) => c.columnDef.meta?.lean).filter((c) => !!c);
387
390
  const emptyColSpan = visibleColumns.length + (selectable ? 1 : 0);
388
391
  const hasPinEnd = visibleColumns.some((col) => col.pin === "end");
392
+ const missingHeaderNames = visibleColumns.filter((col) => isEmptyHeader(col.header) && !col.ariaLabel).map((col) => col.key).join("|");
393
+ React.useEffect(() => {
394
+ if (process.env.NODE_ENV === "production" || !missingHeaderNames) return;
395
+ for (const key of missingHeaderNames.split("|")) {
396
+ console.warn(
397
+ `[DataTable] Column "${key}" renders a <th> with no visible or accessible text. Give it a visible \`header\`, or set \`ariaLabel\` (e.g. "Actions"/"Select") so screen readers can announce the column and axe reports no empty-table-header violation.`
398
+ );
399
+ }
400
+ }, [missingHeaderNames]);
389
401
  const activeSort = sort ?? sortingStateToSort(table.getState().sorting);
390
402
  const isControlledSort = sort !== void 0 || !!onSortChange;
391
403
  const onHeaderClick = (col) => {
@@ -409,27 +421,30 @@ DataTable.Content = function DataTableContent() {
409
421
  {
410
422
  className: cn("ui-data-table-scroll", hasPinEnd && "ui-data-table-has-pin-end"),
411
423
  "aria-busy": loading,
424
+ tabIndex: 0,
412
425
  children: /* @__PURE__ */ jsx(
413
426
  "div",
414
427
  {
415
428
  className: "ui-data-table-surface min-w-[640px] sm:min-w-0",
416
429
  "data-striped": striped ? "" : void 0,
417
430
  "data-hoverable": hoverable ? "" : void 0,
418
- children: /* @__PURE__ */ jsxs(Table, { children: [
431
+ children: /* @__PURE__ */ jsxs(Table, { scrollable: false, children: [
419
432
  /* @__PURE__ */ jsx(TableHeader, { className: cn("bg-secondary", stickyHeader && "sticky top-0 z-10"), children: /* @__PURE__ */ jsxs(TableRow, { children: [
420
433
  selectable && /* @__PURE__ */ jsx(TableHead, { className: "w-10", children: /* @__PURE__ */ jsx(DataTable.SelectAll, {}) }),
421
434
  visibleColumns.map((col) => {
422
435
  const isSortable = !!col.sortable;
423
436
  const isActiveSort = isSortable && activeSort?.key === col.key;
424
437
  const sortIndicator = isSortable ? isActiveSort ? activeSort?.direction === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { className: "size-3", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ArrowDown, { className: "size-3", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronsUpDown, { className: "text-muted-foreground size-3", "aria-hidden": "true" }) : null;
438
+ const headerEmpty = isEmptyHeader(col.header);
439
+ const headerContent = headerEmpty && col.ariaLabel ? /* @__PURE__ */ jsx("span", { className: "sr-only", children: col.ariaLabel }) : col.header;
425
440
  const label = /* @__PURE__ */ jsxs("span", { className: "ui-data-table-sort-label", children: [
426
- col.header,
441
+ headerContent,
427
442
  sortIndicator
428
443
  ] });
429
444
  return /* @__PURE__ */ jsx(
430
445
  TableHead,
431
446
  {
432
- "data-empty": !col.header || void 0,
447
+ "data-empty": headerEmpty || void 0,
433
448
  "aria-sort": isSortable ? isActiveSort ? activeSort?.direction === "asc" ? "ascending" : "descending" : "none" : void 0,
434
449
  className: cn(
435
450
  col.width,
@@ -494,7 +509,7 @@ DataTable.Content = function DataTableContent() {
494
509
  colSpan: emptyColSpan,
495
510
  className: "ui-data-table-empty",
496
511
  "aria-live": "polite",
497
- children: empty ?? /* @__PURE__ */ jsx(EmptyState, { title: t("dataTable.empty") })
512
+ children: empty ?? /* @__PURE__ */ jsx(EmptyState, { title: t("dataTable.empty"), titleAs: "p" })
498
513
  }
499
514
  ) }) : dataRows.map((row) => {
500
515
  const original = row.original;
@@ -1,3 +1,3 @@
1
1
  import type { EmptyStateProp } from "../../props/components/data-display.prop.js";
2
2
  export type { EmptyStateProp, EmptyStateProp as EmptyStateProps, } from "../../props/components/data-display.prop.js";
3
- export declare function EmptyState({ icon: Icon, title, description, action, variant, tone, className, }: EmptyStateProp): import("react/jsx-runtime").JSX.Element;
3
+ export declare function EmptyState({ icon: Icon, title, description, action, variant, tone, titleLevel, titleAs, className, }: EmptyStateProp): import("react/jsx-runtime").JSX.Element;
@@ -1,4 +1,5 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { createElement } from "react";
2
3
  import { cn } from "../../lib/utils.js";
3
4
  function EmptyState({
4
5
  icon: Icon,
@@ -7,8 +8,11 @@ function EmptyState({
7
8
  action,
8
9
  variant = "page",
9
10
  tone = "muted",
11
+ titleLevel = 3,
12
+ titleAs,
10
13
  className
11
14
  }) {
15
+ const TitleTag = titleAs ?? `h${titleLevel}`;
12
16
  return /* @__PURE__ */ jsxs(
13
17
  "div",
14
18
  {
@@ -19,7 +23,7 @@ function EmptyState({
19
23
  className: cn("ui-empty-state", className),
20
24
  children: [
21
25
  Icon && variant !== "compact" && /* @__PURE__ */ jsx("div", { className: "ui-empty-state-icon", children: /* @__PURE__ */ jsx(Icon, { className: "text-muted-foreground size-6", "aria-hidden": "true" }) }),
22
- /* @__PURE__ */ jsx("h3", { className: "ui-empty-state-title", children: title }),
26
+ createElement(TitleTag, { className: "ui-empty-state-title" }, title),
23
27
  description && /* @__PURE__ */ jsx("p", { className: "ui-empty-state-description", children: description }),
24
28
  action && /* @__PURE__ */ jsx("div", { children: action })
25
29
  ]
@@ -10,7 +10,7 @@ const ScrollArea = React.forwardRef(({ className, children, ...props }, ref) =>
10
10
  className: cn("relative overflow-hidden", className),
11
11
  ...props,
12
12
  children: [
13
- /* @__PURE__ */ jsx(ScrollAreaPrimitive.Viewport, { className: "size-full rounded-[inherit]", children }),
13
+ /* @__PURE__ */ jsx(ScrollAreaPrimitive.Viewport, { tabIndex: 0, className: "size-full rounded-[inherit]", children }),
14
14
  /* @__PURE__ */ jsx(ScrollBar, {}),
15
15
  /* @__PURE__ */ jsx(ScrollAreaPrimitive.Corner, {})
16
16
  ]
@@ -1,5 +1,24 @@
1
1
  import * as React from "react";
2
- export declare const Table: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLTableElement> & React.RefAttributes<HTMLTableElement>>;
2
+ export type TableProps = React.HTMLAttributes<HTMLTableElement> & {
3
+ /**
4
+ * Whether the Table owns its own horizontal-scroll region (default `true`). When `true` a
5
+ * table wider than its container scrolls inside a keyboard-reachable wrapper (WCAG 2.1.1 / axe
6
+ * `scrollable-region-focusable`). Set `false` when an ancestor already provides the scroll
7
+ * region (e.g. `DataTable`, whose `.ui-data-table-scroll` owns the overflow + tab stop) — this
8
+ * avoids a redundant NESTED scroll container and a duplicate keyboard focus stop for one table.
9
+ */
10
+ scrollable?: boolean;
11
+ };
12
+ export declare const Table: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLTableElement> & {
13
+ /**
14
+ * Whether the Table owns its own horizontal-scroll region (default `true`). When `true` a
15
+ * table wider than its container scrolls inside a keyboard-reachable wrapper (WCAG 2.1.1 / axe
16
+ * `scrollable-region-focusable`). Set `false` when an ancestor already provides the scroll
17
+ * region (e.g. `DataTable`, whose `.ui-data-table-scroll` owns the overflow + tab stop) — this
18
+ * avoids a redundant NESTED scroll container and a duplicate keyboard focus stop for one table.
19
+ */
20
+ scrollable?: boolean;
21
+ } & React.RefAttributes<HTMLTableElement>>;
3
22
  export declare const TableHeader: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLTableSectionElement> & React.RefAttributes<HTMLTableSectionElement>>;
4
23
  export declare const TableBody: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLTableSectionElement> & React.RefAttributes<HTMLTableSectionElement>>;
5
24
  export declare const TableRow: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLTableRowElement> & React.RefAttributes<HTMLTableRowElement>>;
@@ -3,15 +3,29 @@ import * as React from "react";
3
3
  import { tableHeadHeightClass } from "../../lib/control-styles.js";
4
4
  import { cn } from "../../lib/utils.js";
5
5
  const Table = React.forwardRef(
6
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { className: "relative w-full overflow-auto", children: /* @__PURE__ */ jsx(
7
- "table",
8
- {
9
- ref,
10
- "data-slot": "table",
11
- className: cn("w-full caption-bottom text-sm", className),
12
- ...props
13
- }
14
- ) })
6
+ ({ className, scrollable = true, ...props }, ref) => (
7
+ // A table wider than its container scrolls horizontally in this wrapper; keep it
8
+ // keyboard-reachable so it can be scrolled without a pointer (WCAG 2.1.1 / axe
9
+ // scrollable-region-focusable). No landmark role — avoids landmark-unique collisions.
10
+ // When `scrollable` is false an ancestor owns the scroll region, so this is a bare
11
+ // positioning box (no `overflow`, no tab stop) to avoid a redundant nested scroller.
12
+ /* @__PURE__ */ jsx(
13
+ "div",
14
+ {
15
+ className: scrollable ? "relative w-full overflow-auto" : "relative w-full",
16
+ ...scrollable ? { tabIndex: 0 } : {},
17
+ children: /* @__PURE__ */ jsx(
18
+ "table",
19
+ {
20
+ ref,
21
+ "data-slot": "table",
22
+ className: cn("w-full caption-bottom text-sm", className),
23
+ ...props
24
+ }
25
+ )
26
+ }
27
+ )
28
+ )
15
29
  );
16
30
  Table.displayName = "Table";
17
31
  const TableHeader = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("thead", { ref, className: cn("[&_tr]:border-b", className), ...props }));
@@ -70,6 +70,10 @@ function Calendar({
70
70
  ),
71
71
  selected: cn(
72
72
  "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
73
+ // The day is a ghost <button> that sets its OWN (dark --foreground) text colour, which
74
+ // overrides the cell's text-primary-foreground and fails contrast on the blue fill
75
+ // (axe color-contrast). Force the button label to primary-foreground through hover/focus.
76
+ "[&>button]:text-primary-foreground [&>button:hover]:bg-primary [&>button:hover]:text-primary-foreground [&>button:focus]:text-primary-foreground",
73
77
  classNames?.selected
74
78
  ),
75
79
  today: cn("bg-accent text-accent-foreground", classNames?.today),
@@ -1,4 +1,4 @@
1
1
  import type { CascaderProp } from "../../props/components/data-entry.prop.js";
2
2
  export type { CascaderProp, CascaderProp as CascaderProps, } from "../../props/components/data-entry.prop.js";
3
3
  export type { TreeOption, TreeFieldNames } from "./tree-utils.js";
4
- export declare function Cascader({ options: optionsProp, value, defaultValue, onValueChange, multiple, changeOnSelect, showSearch, placeholder, disabled, className, id, expandTrigger, fieldNames, allowClear, }: CascaderProp): import("react/jsx-runtime").JSX.Element;
4
+ export declare function Cascader({ options: optionsProp, value, defaultValue, onValueChange, multiple, changeOnSelect, showSearch, placeholder, disabled, className, id, expandTrigger, fieldNames, allowClear, ...ariaProps }: CascaderProp): import("react/jsx-runtime").JSX.Element;
@@ -4,6 +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
8
  import { controlOpenRingClass } from "../../lib/control-styles.js";
8
9
  import { Button } from "../general/button.js";
9
10
  import { Popover, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
@@ -80,9 +81,13 @@ function Cascader({
80
81
  id,
81
82
  expandTrigger = "click",
82
83
  fieldNames,
83
- allowClear = true
84
+ allowClear = true,
85
+ ...ariaProps
84
86
  }) {
85
87
  const { t } = useTranslation();
88
+ const fieldA11y = pickFieldA11y(ariaProps);
89
+ const reactId = React.useId();
90
+ const panelId = `${id ?? reactId}-panel`;
86
91
  const options = React.useMemo(
87
92
  () => normalizeTreeOptions(optionsProp, fieldNames),
88
93
  [optionsProp, fieldNames]
@@ -239,6 +244,9 @@ function Cascader({
239
244
  variant: "outline",
240
245
  role: "combobox",
241
246
  "aria-expanded": open,
247
+ "aria-haspopup": "listbox",
248
+ "aria-controls": open ? panelId : void 0,
249
+ ...fieldA11y,
242
250
  disabled,
243
251
  className: cn(
244
252
  "w-full justify-start font-normal",
@@ -253,6 +261,7 @@ function Cascader({
253
261
  /* @__PURE__ */ jsxs(
254
262
  PopoverContent,
255
263
  {
264
+ id: panelId,
256
265
  className: "w-auto min-w-[var(--radix-popover-trigger-width)] p-0",
257
266
  align: "start",
258
267
  children: [
@@ -1,3 +1,3 @@
1
1
  import type { CheckboxGroupProp } from "../../props/components/data-entry.prop.js";
2
2
  export type { CheckboxGroupProp, CheckboxGroupProp as CheckboxGroupProps, } from "../../props/components/data-entry.prop.js";
3
- export declare function CheckboxGroup({ value: controlledValue, defaultValue, onValueChange, options, orientation, disabled, name, className, children, }: CheckboxGroupProp): import("react/jsx-runtime").JSX.Element;
3
+ export declare function CheckboxGroup({ value: controlledValue, defaultValue, onValueChange, options, orientation, disabled, name, id, className, children, ...ariaProps }: CheckboxGroupProp): import("react/jsx-runtime").JSX.Element;
@@ -2,6 +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
6
  import { Checkbox } from "./checkbox.js";
6
7
  import { Field } from "./field.js";
7
8
  import { choiceGroupClassName } from "./choice-option.js";
@@ -24,11 +25,14 @@ function CheckboxGroup({
24
25
  orientation = "vertical",
25
26
  disabled,
26
27
  name,
28
+ id,
27
29
  className,
28
- children
30
+ children,
31
+ ...ariaProps
29
32
  }) {
30
33
  const reactId = React.useId();
31
34
  const [value, setValue] = useControllableArray(controlledValue, defaultValue);
35
+ const groupA11y = pickGroupFieldA11y(ariaProps);
32
36
  const toggle = (optionValue) => {
33
37
  const next = value.includes(optionValue) ? value.filter((v) => v !== optionValue) : [...value, optionValue];
34
38
  setValue(next);
@@ -39,16 +43,18 @@ function CheckboxGroup({
39
43
  "div",
40
44
  {
41
45
  role: "group",
46
+ id,
47
+ ...groupA11y,
42
48
  "aria-disabled": disabled ? true : void 0,
43
49
  "data-orientation": orientation,
44
50
  className: choiceGroupClassName(orientation, className),
45
51
  children: options.map((opt, index) => {
46
- const id = `${reactId}-${opt.value}-${index}`;
52
+ const id2 = `${reactId}-${opt.value}-${index}`;
47
53
  const checked = value.includes(opt.value);
48
- return /* @__PURE__ */ jsx(Field, { id, label: opt.label, description: opt.description, children: /* @__PURE__ */ jsx(
54
+ return /* @__PURE__ */ jsx(Field, { id: id2, label: opt.label, description: opt.description, children: /* @__PURE__ */ jsx(
49
55
  Checkbox,
50
56
  {
51
- id,
57
+ id: id2,
52
58
  name,
53
59
  value: opt.value,
54
60
  checked,
@@ -66,6 +72,9 @@ function CheckboxGroup({
66
72
  "div",
67
73
  {
68
74
  role: "group",
75
+ id,
76
+ ...groupA11y,
77
+ "aria-disabled": disabled ? true : void 0,
69
78
  "data-orientation": orientation,
70
79
  className: cn(choiceGroupClassName(orientation), className),
71
80
  children
@@ -1,3 +1,3 @@
1
1
  import type { ColorPickerProp } from "../../props/components/data-entry.prop.js";
2
2
  export type { ColorPickerProp, ColorPickerProp as ColorPickerProps, } from "../../props/components/data-entry.prop.js";
3
- export declare function ColorPicker({ value, onValueChange, disabled, className, id, showHexInput, }: ColorPickerProp): import("react/jsx-runtime").JSX.Element;
3
+ export declare function ColorPicker({ value, onValueChange, disabled, className, id, showHexInput, ...ariaProps }: ColorPickerProp): import("react/jsx-runtime").JSX.Element;
@@ -3,6 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { useTranslation } from "../../i18n/use-translation.js";
5
5
  import { cn } from "../../lib/utils.js";
6
+ import { resolveFieldA11y } from "../../lib/field-a11y.js";
6
7
  import { controlIconClass } from "../../lib/control-styles.js";
7
8
  import { Input } from "./input.js";
8
9
  const HEX_PATTERN = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
@@ -16,11 +17,13 @@ function ColorPicker({
16
17
  disabled,
17
18
  className,
18
19
  id,
19
- showHexInput = true
20
+ showHexInput = true,
21
+ ...ariaProps
20
22
  }) {
21
23
  const { t } = useTranslation();
22
24
  const [draft, setDraft] = React.useState(null);
23
25
  const display = draft ?? value;
26
+ const swatchA11y = resolveFieldA11y(ariaProps, t("dataEntry.colorPicker.ariaLabel"));
24
27
  const commit = (next) => {
25
28
  const normalized = normalizeHex(next);
26
29
  if (!HEX_PATTERN.test(normalized)) {
@@ -49,7 +52,7 @@ function ColorPicker({
49
52
  disabled,
50
53
  onChange: (event) => commit(event.target.value),
51
54
  className: "ui-color-picker-input",
52
- "aria-label": t("dataEntry.colorPicker.ariaLabel")
55
+ ...swatchA11y
53
56
  }
54
57
  )
55
58
  ] }),
@@ -46,7 +46,7 @@ export declare const CommandGroup: React.ForwardRefExoticComponent<Omit<{
46
46
  ref?: React.Ref<HTMLDivElement>;
47
47
  } & {
48
48
  asChild?: boolean;
49
- }, keyof React.HTMLAttributes<HTMLDivElement> | "key" | "asChild">, "heading" | "value"> & {
49
+ }, keyof React.HTMLAttributes<HTMLDivElement> | "key" | "asChild">, "value" | "heading"> & {
50
50
  heading?: React.ReactNode;
51
51
  value?: string;
52
52
  forceMount?: boolean;
@@ -57,7 +57,7 @@ export declare const CommandItem: React.ForwardRefExoticComponent<Omit<{
57
57
  ref?: React.Ref<HTMLDivElement>;
58
58
  } & {
59
59
  asChild?: boolean;
60
- }, keyof React.HTMLAttributes<HTMLDivElement> | "key" | "asChild">, "onSelect" | "disabled" | "value"> & {
60
+ }, keyof React.HTMLAttributes<HTMLDivElement> | "key" | "asChild">, "onSelect" | "value" | "disabled"> & {
61
61
  disabled?: boolean;
62
62
  onSelect?: (value: string) => void;
63
63
  value?: string;
@@ -6,4 +6,4 @@ export type { DatePickerProp, DatePickerProp as DatePickerProps, } from "../../p
6
6
  * `name`), screen-reader friendly, and e2e-testable by simply filling the input. The calendar
7
7
  * popover is the visual affordance; typing and the calendar stay in sync.
8
8
  */
9
- export declare function DatePicker({ value: valueProp, defaultValue, onValueChange, placeholder, disabled, className, id, name, locale: localeProp, fromDate, toDate, allowClear, }: DatePickerProp): import("react/jsx-runtime").JSX.Element;
9
+ export declare function DatePicker({ value: valueProp, defaultValue, onValueChange, placeholder, disabled, className, id, name, locale: localeProp, fromDate, toDate, allowClear, ...ariaProps }: DatePickerProp): import("react/jsx-runtime").JSX.Element;
@@ -5,6 +5,7 @@ import { CalendarIcon } 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
9
  import { cn } from "../../lib/utils.js";
9
10
  import { Input } from "./input.js";
10
11
  import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
@@ -22,11 +23,15 @@ function DatePicker({
22
23
  locale: localeProp,
23
24
  fromDate,
24
25
  toDate,
25
- allowClear = true
26
+ allowClear = true,
27
+ ...ariaProps
26
28
  }) {
27
29
  const { t } = useTranslation();
28
30
  const { dayPickerLocale } = usePickerLocales(localeProp);
29
31
  const [open, setOpen] = React.useState(false);
32
+ const fieldA11y = pickFieldA11y(ariaProps);
33
+ const reactId = React.useId();
34
+ const dialogId = `${id ?? reactId}-dialog`;
30
35
  const isControlled = useControlledLatch(valueProp !== void 0);
31
36
  const [internalValue, setInternalValue] = React.useState(defaultValue);
32
37
  const value = isControlled ? valueProp : internalValue;
@@ -69,6 +74,8 @@ function DatePicker({
69
74
  role: "combobox",
70
75
  "aria-expanded": open,
71
76
  "aria-haspopup": "dialog",
77
+ "aria-controls": open ? dialogId : void 0,
78
+ ...fieldA11y,
72
79
  allowClear,
73
80
  onClear: clear,
74
81
  trailingIcon: /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
@@ -106,6 +113,9 @@ function DatePicker({
106
113
  /* @__PURE__ */ jsx(
107
114
  PopoverContent,
108
115
  {
116
+ id: dialogId,
117
+ role: "dialog",
118
+ "aria-label": t("dataEntry.datePicker.openCalendar") ?? "Calendar",
109
119
  className: "w-auto p-0",
110
120
  align: "start",
111
121
  onOpenAutoFocus: (event) => event.preventDefault(),
@@ -7,4 +7,4 @@ export type { DateRangePickerProp, DateRangePickerProp as DateRangePickerProps,
7
7
  * `${name}_to`, screen-reader friendly, e2e-testable by filling either input); the
8
8
  * range calendar is the visual affordance.
9
9
  */
10
- export declare function DateRangePicker({ value: valueProp, defaultValue, onValueChange, placeholder, disabled, className, id, name, locale: localeProp, fromDate, toDate, allowClear, }: DateRangePickerProp): import("react/jsx-runtime").JSX.Element;
10
+ export declare function DateRangePicker({ value: valueProp, defaultValue, onValueChange, placeholder, disabled, className, id, name, locale: localeProp, fromDate, toDate, allowClear, ...ariaProps }: DateRangePickerProp): import("react/jsx-runtime").JSX.Element;
@@ -5,6 +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
9
  import { cn } from "../../lib/utils.js";
9
10
  import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
10
11
  import { Calendar } from "./calendar.js";
@@ -21,14 +22,17 @@ function DateRangePicker({
21
22
  locale: localeProp,
22
23
  fromDate,
23
24
  toDate,
24
- allowClear = true
25
+ allowClear = true,
26
+ ...ariaProps
25
27
  }) {
26
28
  const { t } = useTranslation();
27
29
  const { dayPickerLocale } = usePickerLocales(localeProp);
28
30
  const [open, setOpen] = React.useState(false);
31
+ const groupA11y = pickGroupFieldA11y(ariaProps);
29
32
  const autoId = React.useId();
30
- const fromId = id ?? autoId;
31
- const toId = `${fromId}-to`;
33
+ const groupId = id ?? autoId;
34
+ const fromId = `${groupId}-from`;
35
+ const toId = `${groupId}-to`;
32
36
  const isControlled = useControlledLatch(valueProp !== void 0);
33
37
  const [internalValue, setInternalValue] = React.useState(defaultValue);
34
38
  const value = isControlled ? valueProp : internalValue;
@@ -70,6 +74,10 @@ function DateRangePicker({
70
74
  return /* @__PURE__ */ jsx(Popover, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsx(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsxs(
71
75
  "div",
72
76
  {
77
+ role: "group",
78
+ id: groupId,
79
+ ...groupA11y,
80
+ "aria-disabled": disabled ? true : void 0,
73
81
  className: cn(
74
82
  // One input-styled shell for the whole range — mirrors Input's control
75
83
  // tokens (border/radius/ring) so it reads as a single form field.
@@ -3,6 +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
7
  import { useFormLayout } from "./form.js";
7
8
  const toCssLength = (v) => typeof v === "number" ? `${v}px` : v;
8
9
  const FOCUSABLE_SELECTOR = 'input:not([type="hidden"]), select, textarea, button, [tabindex]:not([tabindex="-1"])';
@@ -37,15 +38,18 @@ function FormField({
37
38
  );
38
39
  }
39
40
  const childProps = React.isValidElement(children) ? children.props : void 0;
40
- const mergeIds = (...values) => Array.from(new Set(values.flatMap((value) => value?.split(/\s+/).filter(Boolean) ?? []))).join(
41
- " "
42
- ) || void 0;
41
+ const mergeIds = mergeAriaIds;
43
42
  const childWithA11y = React.isValidElement(children) ? React.cloneElement(children, {
44
43
  // The label is associated via aria-labelledby (not <label for>): composite
45
44
  // controls (Radio.Group, checkbox lists, range pairs) have no labelable root,
46
45
  // and a dangling `for` triggers Chrome's "Incorrect use of <label>" issue.
47
46
  id: childProps?.id ?? resolvedId,
48
47
  "aria-labelledby": childProps?.["aria-labelledby"] ?? labelId,
48
+ // Redundant `aria-label` fallback (belt-and-suspenders): the accessible name is the
49
+ // SAME string as the visible label, just reachable even if an aria-labelledby lookup
50
+ // ever comes back empty (id-ref timing, AT quirks). Only when `label` is plain text and
51
+ // the child hasn't already set its own aria-label.
52
+ "aria-label": childProps?.["aria-label"] ?? (typeof label === "string" ? label : void 0),
49
53
  // Helper and error can coexist: helper stays on aria-describedby, the error on
50
54
  // aria-errormessage (surfaced when aria-invalid is true).
51
55
  "aria-describedby": mergeIds(
@@ -6,4 +6,4 @@ export type { MonthPickerProp, MonthPickerProp as MonthPickerProps, } from "../.
6
6
  * The field stays a real, typeable input (form-submittable via `name` as
7
7
  * `yyyy-MM`); the grid is the visual affordance.
8
8
  */
9
- export declare function MonthPicker({ value: valueProp, defaultValue, onValueChange, placeholder, disabled, className, id, name, fromYear, toYear, allowClear, }: MonthPickerProp): import("react/jsx-runtime").JSX.Element;
9
+ export declare function MonthPicker({ value: valueProp, defaultValue, onValueChange, placeholder, disabled, className, id, name, fromYear, toYear, allowClear, ...ariaProps }: MonthPickerProp): import("react/jsx-runtime").JSX.Element;
@@ -4,6 +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
8
  import { cn } from "../../lib/utils.js";
8
9
  import { Button } from "../general/button.js";
9
10
  import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
@@ -27,13 +28,16 @@ function MonthPicker({
27
28
  name,
28
29
  fromYear,
29
30
  toYear,
30
- allowClear = true
31
+ allowClear = true,
32
+ ...ariaProps
31
33
  }) {
32
34
  const { t } = useTranslation();
33
35
  const { locale } = usePickerLocales();
34
36
  const [open, setOpen] = React.useState(false);
35
37
  const autoId = React.useId();
36
38
  const inputId = id ?? autoId;
39
+ const dialogId = `${inputId}-dialog`;
40
+ const fieldA11y = pickFieldA11y(ariaProps);
37
41
  const isControlled = useControlledLatch(valueProp !== void 0);
38
42
  const [internalValue, setInternalValue] = React.useState(defaultValue);
39
43
  const value = isControlled ? valueProp : internalValue;
@@ -78,6 +82,11 @@ function MonthPicker({
78
82
  placeholder: placeholder ?? t("dataEntry.monthPicker.placeholder") ?? YM_HINT,
79
83
  inputMode: "numeric",
80
84
  autoComplete: "off",
85
+ role: "combobox",
86
+ "aria-expanded": open,
87
+ "aria-haspopup": "dialog",
88
+ "aria-controls": open ? dialogId : void 0,
89
+ ...fieldA11y,
81
90
  className: "placeholder:text-muted-foreground min-w-0 flex-1 bg-transparent tabular-nums outline-none disabled:cursor-not-allowed",
82
91
  onKeyDown: (event) => {
83
92
  if (event.key === "ArrowDown") {
@@ -124,6 +133,9 @@ function MonthPicker({
124
133
  /* @__PURE__ */ jsxs(
125
134
  PopoverContent,
126
135
  {
136
+ id: dialogId,
137
+ role: "dialog",
138
+ "aria-label": t("dataEntry.monthPicker.openGrid") ?? "Month grid",
127
139
  className: "w-auto p-3",
128
140
  align: "start",
129
141
  onOpenAutoFocus: (event) => event.preventDefault(),
@@ -8,4 +8,4 @@ export type { MonthRangePickerProp, MonthRangePickerProp as MonthRangePickerProp
8
8
  * affordance. Grid picks are two-step (from, then to — swapped when picked
9
9
  * backwards) and a pick on a COMPLETE range starts a new one (reset-on-complete).
10
10
  */
11
- export declare function MonthRangePicker({ value: valueProp, defaultValue, onValueChange, placeholder, disabled, className, id, name, fromYear, toYear, allowClear, }: MonthRangePickerProp): import("react/jsx-runtime").JSX.Element;
11
+ export declare function MonthRangePicker({ value: valueProp, defaultValue, onValueChange, placeholder, disabled, className, id, name, fromYear, toYear, allowClear, ...ariaProps }: MonthRangePickerProp): import("react/jsx-runtime").JSX.Element;