@westpac/ui 1.18.0 → 1.19.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/component-type.json +1 -1
  3. package/dist/components/button-group/button-group.component.d.ts +2 -1
  4. package/dist/components/button-group/button-group.component.js +6 -3
  5. package/dist/components/checkbox-group/checkbox-group.component.d.ts +11 -2
  6. package/dist/components/checkbox-group/checkbox-group.component.js +10 -3
  7. package/dist/components/checkbox-group/components/checkbox-group-checkbox/checkbox-group-checkbox.component.d.ts +1 -1
  8. package/dist/components/checkbox-group/components/checkbox-group-checkbox/checkbox-group-checkbox.component.js +5 -6
  9. package/dist/components/date-picker/components/date-field/components/date-segment/date-segment.component.d.ts +6 -5
  10. package/dist/components/date-picker/components/date-field/components/date-segment/date-segment.component.js +6 -4
  11. package/dist/components/date-picker/components/date-field/date-field.component.d.ts +5 -5
  12. package/dist/components/date-picker/components/date-field/date-field.component.js +7 -3
  13. package/dist/components/date-picker/date-picker.component.d.ts +19 -2
  14. package/dist/components/date-picker/date-picker.component.js +10 -5
  15. package/dist/components/date-picker/date-picker.styles.js +5 -5
  16. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.component.js +3 -7
  17. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-select-all-option/multi-select-select-all-option.component.js +3 -3
  18. package/dist/components/multi-select/components/multi-select-popover/multi-select-popover.component.js +5 -2
  19. package/dist/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.component.js +4 -7
  20. package/dist/components/multi-select/multi-select.component.d.ts +14 -3
  21. package/dist/components/multi-select/multi-select.component.js +10 -16
  22. package/dist/components/radio-group/components/radio-group-radio/radio-group-radio.component.d.ts +1 -1
  23. package/dist/components/radio-group/components/radio-group-radio/radio-group-radio.component.js +5 -6
  24. package/dist/components/radio-group/radio-group.component.d.ts +10 -2
  25. package/dist/components/radio-group/radio-group.component.js +10 -3
  26. package/dist/components/selector/components/selector-button-group/selector-button-group.component.d.ts +9 -2
  27. package/dist/components/selector/components/selector-button-group/selector-button-group.component.js +8 -3
  28. package/dist/components/selector/components/selector-checkbox-group/selector-checkbox-group.component.d.ts +6 -2
  29. package/dist/components/selector/components/selector-checkbox-group/selector-checkbox-group.component.js +8 -3
  30. package/dist/components/selector/components/selector-radio-group/selector-radio-group.component.d.ts +6 -2
  31. package/dist/components/selector/components/selector-radio-group/selector-radio-group.component.js +8 -3
  32. package/dist/components/selector/selector.component.d.ts +3 -1
  33. package/dist/components/selector/selector.component.js +16 -5
  34. package/dist/components/switch/switch.component.d.ts +8 -2
  35. package/dist/components/switch/switch.component.js +6 -4
  36. package/dist/css/westpac-ui.css +12 -12
  37. package/dist/css/westpac-ui.min.css +12 -12
  38. package/dist/hook/focus-manager-ref.hook.d.ts +29 -0
  39. package/dist/hook/focus-manager-ref.hook.js +23 -0
  40. package/dist/hook/index.d.ts +1 -0
  41. package/dist/index.d.ts +1 -0
  42. package/package.json +5 -4
  43. package/src/components/button-group/button-group.component.tsx +26 -16
  44. package/src/components/checkbox-group/checkbox-group.component.tsx +24 -14
  45. package/src/components/checkbox-group/components/checkbox-group-checkbox/checkbox-group-checkbox.component.tsx +7 -7
  46. package/src/components/date-picker/components/date-field/components/date-segment/date-segment.component.tsx +10 -4
  47. package/src/components/date-picker/components/date-field/date-field.component.tsx +17 -3
  48. package/src/components/date-picker/date-picker.component.tsx +32 -21
  49. package/src/components/date-picker/date-picker.styles.ts +5 -5
  50. package/src/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.component.tsx +3 -5
  51. package/src/components/multi-select/components/multi-select-list-box/components/multi-select-select-all-option/multi-select-select-all-option.component.tsx +4 -3
  52. package/src/components/multi-select/components/multi-select-popover/multi-select-popover.component.tsx +8 -2
  53. package/src/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.component.tsx +3 -6
  54. package/src/components/multi-select/multi-select.component.tsx +33 -28
  55. package/src/components/radio-group/components/radio-group-radio/radio-group-radio.component.tsx +10 -7
  56. package/src/components/radio-group/radio-group.component.tsx +24 -14
  57. package/src/components/selector/components/selector-button-group/selector-button-group.component.tsx +23 -14
  58. package/src/components/selector/components/selector-checkbox-group/selector-checkbox-group.component.tsx +12 -3
  59. package/src/components/selector/components/selector-radio-group/selector-radio-group.component.tsx +21 -11
  60. package/src/components/selector/selector.component.tsx +16 -5
  61. package/src/components/switch/switch.component.tsx +10 -12
  62. package/src/hook/focus-manager-ref.hook.ts +56 -0
  63. package/src/hook/index.ts +1 -0
  64. package/src/index.ts +1 -0
@@ -1,7 +1,8 @@
1
1
  'use client';
2
- import React, { createContext, useCallback, useMemo, useState } from 'react';
2
+ import React, { createContext, forwardRef, useCallback, useMemo, useState } from 'react';
3
3
  import { useField } from 'react-aria';
4
4
  import { useBreakpoint } from '../../../../hook/breakpoints.hook.js';
5
+ import { useFocusManagerRef } from '../../../../hook/focus-manager-ref.hook.js';
5
6
  import { resolveResponsiveVariant } from '../../../../utils/breakpoint.util.js';
6
7
  import { ErrorMessage, Hint, Label } from '../../../index.js';
7
8
  import { styles } from './selector-button-group.styles.js';
@@ -11,7 +12,8 @@ export const SelectorButtonContext = createContext({
11
12
  validationState: 'valid',
12
13
  isDisabled: undefined
13
14
  });
14
- export function SelectorButtonGroup({ className, children, label, orientation = 'vertical', errorMessage, description, value = '', onChange, isDisabled, ...props }) {
15
+ function BaseSelectorButtonGroup({ className, children, label, orientation = 'vertical', errorMessage, description, value = '', onChange, isDisabled, ...props }, ref) {
16
+ const wrapperRef = useFocusManagerRef(ref);
15
17
  const isControlled = onChange !== undefined;
16
18
  const onChangeCallback = onChange;
17
19
  const [selected, setSelected] = useState(value);
@@ -55,8 +57,11 @@ export function SelectorButtonGroup({ className, children, label, orientation =
55
57
  className,
56
58
  orientation: resolvedOrientation
57
59
  }),
58
- ...fieldProps
60
+ ...fieldProps,
61
+ ref: wrapperRef
59
62
  }, React.createElement(SelectorButtonContext.Provider, {
60
63
  value: state
61
64
  }, children)));
62
65
  }
66
+ export const SelectorButtonGroup = forwardRef(BaseSelectorButtonGroup);
67
+ SelectorButtonGroup.displayName = 'SelectorButtonGroup';
@@ -1,4 +1,8 @@
1
1
  import React from 'react';
2
- import { type SelectorCheckboxGroupProps } from './selector-checkbox-group.types.js';
2
+ import { FocusHandle } from '../../../../hook/focus-manager-ref.hook.js';
3
3
  export declare const SelectorCheckboxGroupContext: React.Context<import("react-stately").CheckboxGroupState>;
4
- export declare function SelectorCheckboxGroup(props: SelectorCheckboxGroupProps): import("react/jsx-runtime").JSX.Element;
4
+ export declare const SelectorCheckboxGroup: React.ForwardRefExoticComponent<{
5
+ className?: string;
6
+ orientation?: "horizontal" | "vertical";
7
+ size?: "medium" | "large";
8
+ } & import("react-aria").AriaCheckboxGroupProps & Omit<React.HTMLAttributes<Element>, "onChange"> & React.RefAttributes<FocusHandle>>;
@@ -1,8 +1,9 @@
1
1
  'use client';
2
- import React, { createContext } from 'react';
2
+ import React, { createContext, forwardRef } from 'react';
3
3
  import { useCheckboxGroup } from 'react-aria';
4
4
  import { useCheckboxGroupState } from 'react-stately';
5
5
  import { FUNCTION_NOT_IMPLEMENTED } from '../../../../constants/message.js';
6
+ import { useFocusManagerRef } from '../../../../hook/focus-manager-ref.hook.js';
6
7
  import { ErrorMessage, Hint, Label } from '../../../index.js';
7
8
  import { styles } from './selector-checkbox-group.styles.js';
8
9
  export const SelectorCheckboxGroupContext = createContext({
@@ -65,8 +66,9 @@ export const SelectorCheckboxGroupContext = createContext({
65
66
  throw new Error(FUNCTION_NOT_IMPLEMENTED);
66
67
  }
67
68
  });
68
- export function SelectorCheckboxGroup(props) {
69
+ function BaseSelectorCheckboxGroup(props, ref) {
69
70
  const { children, label, description, errorMessage } = props;
71
+ const wrapperRef = useFocusManagerRef(ref);
70
72
  const state = useCheckboxGroupState(props);
71
73
  const { groupProps, labelProps, descriptionProps, errorMessageProps } = useCheckboxGroup(props, state);
72
74
  return React.createElement(React.Fragment, null, label && React.createElement(Label, labelProps, label), description && React.createElement(Hint, descriptionProps, description), errorMessage && state.isInvalid && React.createElement(ErrorMessage, {
@@ -76,8 +78,11 @@ export function SelectorCheckboxGroup(props) {
76
78
  ...groupProps,
77
79
  className: styles({
78
80
  className: groupProps.className
79
- })
81
+ }),
82
+ ref: wrapperRef
80
83
  }, React.createElement(SelectorCheckboxGroupContext.Provider, {
81
84
  value: state
82
85
  }, children)));
83
86
  }
87
+ export const SelectorCheckboxGroup = forwardRef(BaseSelectorCheckboxGroup);
88
+ SelectorCheckboxGroup.displayName = 'SelectorCheckboxGroup';
@@ -1,4 +1,8 @@
1
1
  import React from 'react';
2
- import { type SelectorRadioGroupContextState, type SelectorRadioGroupProps } from './selector-radio-group.types.js';
2
+ import { FocusHandle } from '../../../../hook/focus-manager-ref.hook.js';
3
+ import { type SelectorRadioGroupContextState } from './selector-radio-group.types.js';
3
4
  export declare const SelectorRadioGroupContext: React.Context<SelectorRadioGroupContextState>;
4
- export declare function SelectorRadioGroup({ className, children, label, orientation, errorMessage, description, ...props }: SelectorRadioGroupProps): import("react/jsx-runtime").JSX.Element;
5
+ export declare const SelectorRadioGroup: React.ForwardRefExoticComponent<{
6
+ orientation?: "horizontal" | "vertical" | undefined;
7
+ tag?: keyof JSX.IntrinsicElements;
8
+ } & import("react-aria").AriaRadioGroupProps & Omit<React.HTMLAttributes<Element>, "onChange"> & React.RefAttributes<FocusHandle>>;
@@ -1,9 +1,10 @@
1
1
  'use client';
2
- import React, { createContext } from 'react';
2
+ import React, { createContext, forwardRef } from 'react';
3
3
  import { useRadioGroup } from 'react-aria';
4
4
  import { useRadioGroupState } from 'react-stately';
5
5
  import { FUNCTION_NOT_IMPLEMENTED } from '../../../../constants/message.js';
6
6
  import { useBreakpoint } from '../../../../hook/breakpoints.hook.js';
7
+ import { useFocusManagerRef } from '../../../../hook/focus-manager-ref.hook.js';
7
8
  import { resolveResponsiveVariant } from '../../../../utils/breakpoint.util.js';
8
9
  import { ErrorMessage, Hint, Label } from '../../../index.js';
9
10
  import { styles } from './selector-radio-group.styles.js';
@@ -66,7 +67,8 @@ export const SelectorRadioGroupContext = createContext({
66
67
  }
67
68
  }
68
69
  });
69
- export function SelectorRadioGroup({ className, children, label, orientation = 'vertical', errorMessage, description, ...props }) {
70
+ function BaseSelectorRadioGroup({ className, children, label, orientation = 'vertical', errorMessage, description, ...props }, ref) {
71
+ const wrapperRef = useFocusManagerRef(ref);
70
72
  const breakpoint = useBreakpoint();
71
73
  const resolvedOrientation = resolveResponsiveVariant(orientation, breakpoint);
72
74
  const state = useRadioGroupState({
@@ -88,7 +90,8 @@ export function SelectorRadioGroup({ className, children, label, orientation = '
88
90
  className,
89
91
  orientation: resolvedOrientation
90
92
  }),
91
- ...radioGroupProps
93
+ ...radioGroupProps,
94
+ ref: wrapperRef
92
95
  }, React.createElement(SelectorRadioGroupContext.Provider, {
93
96
  value: {
94
97
  state,
@@ -96,3 +99,5 @@ export function SelectorRadioGroup({ className, children, label, orientation = '
96
99
  }
97
100
  }, children)));
98
101
  }
102
+ export const SelectorRadioGroup = forwardRef(BaseSelectorRadioGroup);
103
+ SelectorRadioGroup.displayName = 'SelectorRadioGroup';
@@ -1,2 +1,4 @@
1
+ import React from 'react';
2
+ import { FocusHandle } from '../../hook/focus-manager-ref.hook.js';
1
3
  import { type SelectorProps } from './selector.types.js';
2
- export declare function Selector(props: SelectorProps): import("react/jsx-runtime").JSX.Element | undefined;
4
+ export declare const Selector: React.ForwardRefExoticComponent<SelectorProps & React.RefAttributes<FocusHandle>>;
@@ -1,17 +1,28 @@
1
1
  'use client';
2
- import React from 'react';
2
+ import React, { forwardRef } from 'react';
3
3
  import { SelectorButtonGroup, SelectorCheckboxGroup, SelectorLinkGroup, SelectorRadioGroup } from './components/index.js';
4
- export function Selector(props) {
4
+ function BaseSelector(props, ref) {
5
5
  if (props.type === 'checkbox') {
6
- return React.createElement(SelectorCheckboxGroup, props);
6
+ return React.createElement(SelectorCheckboxGroup, {
7
+ ...props,
8
+ ref: ref
9
+ });
7
10
  }
8
11
  if (props.type === 'radio') {
9
- return React.createElement(SelectorRadioGroup, props);
12
+ return React.createElement(SelectorRadioGroup, {
13
+ ...props,
14
+ ref: ref
15
+ });
10
16
  }
11
17
  if (props.type === 'button') {
12
- return React.createElement(SelectorButtonGroup, props);
18
+ return React.createElement(SelectorButtonGroup, {
19
+ ...props,
20
+ ref: ref
21
+ });
13
22
  }
14
23
  if (props.type === 'link') {
15
24
  return React.createElement(SelectorLinkGroup, props);
16
25
  }
17
26
  }
27
+ export const Selector = forwardRef(BaseSelector);
28
+ Selector.displayName = 'Selector';
@@ -1,2 +1,8 @@
1
- import { type SwitchProps } from './switch.types.js';
2
- export declare function Switch({ className, label, size, block, checked, isDisabled, ...props }: SwitchProps): import("react/jsx-runtime").JSX.Element;
1
+ import React from 'react';
2
+ export declare const Switch: React.ForwardRefExoticComponent<{
3
+ block?: import("../../types/responsive-variants.types.js").ResponsiveVariants<boolean | undefined>;
4
+ checked?: boolean;
5
+ className?: string;
6
+ label: string;
7
+ size?: import("../../types/responsive-variants.types.js").ResponsiveVariants<"small" | "xlarge" | "medium" | "large" | undefined>;
8
+ } & Omit<import("react-aria").AriaCheckboxProps, "children"> & React.RefAttributes<HTMLInputElement>>;
@@ -1,17 +1,17 @@
1
1
  'use client';
2
- import React, { useId, useRef } from 'react';
3
- import { VisuallyHidden, mergeProps, useCheckbox, useFocusRing } from 'react-aria';
2
+ import React, { forwardRef, useId } from 'react';
3
+ import { VisuallyHidden, mergeProps, useCheckbox, useFocusRing, useObjectRef } from 'react-aria';
4
4
  import { useToggleState } from 'react-stately';
5
5
  import { useBreakpoint } from '../../hook/breakpoints.hook.js';
6
6
  import { resolveResponsiveVariant } from '../../utils/breakpoint.util.js';
7
7
  import { styles as switchStyles } from './switch.styles.js';
8
- export function Switch({ className, label, size = 'medium', block = false, checked = false, isDisabled, ...props }) {
8
+ function BaseSwitch({ className, label, size = 'medium', block = false, checked = false, isDisabled, ...props }, forwardedRef) {
9
9
  const state = useToggleState({
10
10
  ...props,
11
11
  defaultSelected: checked
12
12
  });
13
13
  const labelId = useId();
14
- const ref = useRef(null);
14
+ const ref = useObjectRef(forwardedRef);
15
15
  const { isSelected } = state;
16
16
  const { inputProps, labelProps } = useCheckbox({
17
17
  isDisabled,
@@ -45,3 +45,5 @@ export function Switch({ className, label, size = 'medium', block = false, check
45
45
  className: styles.switchDiv()
46
46
  }));
47
47
  }
48
+ export const Switch = forwardRef(BaseSwitch);
49
+ Switch.displayName = 'Switch';
@@ -1093,27 +1093,15 @@
1093
1093
  .w-\[116px\] {
1094
1094
  width: 116px;
1095
1095
  }
1096
- .w-\[120px\] {
1097
- width: 120px;
1098
- }
1099
1096
  .w-\[122px\] {
1100
1097
  width: 122px;
1101
1098
  }
1102
1099
  .w-\[135px\] {
1103
1100
  width: 135px;
1104
1101
  }
1105
- .w-\[140px\] {
1106
- width: 140px;
1107
- }
1108
- .w-\[145px\] {
1109
- width: 145px;
1110
- }
1111
1102
  .w-\[150px\] {
1112
1103
  width: 150px;
1113
1104
  }
1114
- .w-\[165px\] {
1115
- width: 165px;
1116
- }
1117
1105
  .w-\[170px\] {
1118
1106
  width: 170px;
1119
1107
  }
@@ -1339,6 +1327,18 @@
1339
1327
  .min-w-\[100vw\] {
1340
1328
  min-width: 100vw;
1341
1329
  }
1330
+ .min-w-\[120px\] {
1331
+ min-width: 120px;
1332
+ }
1333
+ .min-w-\[140px\] {
1334
+ min-width: 140px;
1335
+ }
1336
+ .min-w-\[145px\] {
1337
+ min-width: 145px;
1338
+ }
1339
+ .min-w-\[165px\] {
1340
+ min-width: 165px;
1341
+ }
1342
1342
  .min-w-\[200px\] {
1343
1343
  min-width: 200px;
1344
1344
  }
@@ -1093,27 +1093,15 @@
1093
1093
  .w-\[116px\] {
1094
1094
  width: 116px;
1095
1095
  }
1096
- .w-\[120px\] {
1097
- width: 120px;
1098
- }
1099
1096
  .w-\[122px\] {
1100
1097
  width: 122px;
1101
1098
  }
1102
1099
  .w-\[135px\] {
1103
1100
  width: 135px;
1104
1101
  }
1105
- .w-\[140px\] {
1106
- width: 140px;
1107
- }
1108
- .w-\[145px\] {
1109
- width: 145px;
1110
- }
1111
1102
  .w-\[150px\] {
1112
1103
  width: 150px;
1113
1104
  }
1114
- .w-\[165px\] {
1115
- width: 165px;
1116
- }
1117
1105
  .w-\[170px\] {
1118
1106
  width: 170px;
1119
1107
  }
@@ -1339,6 +1327,18 @@
1339
1327
  .min-w-\[100vw\] {
1340
1328
  min-width: 100vw;
1341
1329
  }
1330
+ .min-w-\[120px\] {
1331
+ min-width: 120px;
1332
+ }
1333
+ .min-w-\[140px\] {
1334
+ min-width: 140px;
1335
+ }
1336
+ .min-w-\[145px\] {
1337
+ min-width: 145px;
1338
+ }
1339
+ .min-w-\[165px\] {
1340
+ min-width: 165px;
1341
+ }
1342
1342
  .min-w-\[200px\] {
1343
1343
  min-width: 200px;
1344
1344
  }
@@ -0,0 +1,29 @@
1
+ import { ForwardedRef, RefObject } from 'react';
2
+ /**
3
+ * Imperative handle exposed by group fields (`RadioGroup`, `CheckboxGroup`, `Selector`, `ButtonGroup`) whose
4
+ * focusable control is not a single element the group can point a DOM ref at.
5
+ *
6
+ * `focus()` moves focus to the group's first tabbable option at call time, which is what form libraries
7
+ * such as react-hook-form need (`field.ref` → `ref.focus()` on validation error).
8
+ */
9
+ export type FocusHandle = {
10
+ focus: () => void;
11
+ };
12
+ export type UseFocusManagerRefOptions = {
13
+ /**
14
+ * Restricts which descendants of the wrapper may receive focus, e.g. only `<input>`s when the wrapper also
15
+ * contains other controls (such as a "show more" button).
16
+ */
17
+ accept?: (node: Element) => boolean;
18
+ };
19
+ /**
20
+ * Bridges a forwarded {@link FocusHandle} ref to a wrapper element. Returns the ref to attach to the
21
+ * wrapper; `focus()` on the forwarded handle focuses the first tabbable descendant via react-aria's
22
+ * focus manager. Resolution happens when `focus()` is called, so options that render, enable or
23
+ * disable after mount are handled, and roving-tabindex groups (radios) focus their tab stop
24
+ * (the selected option) rather than the first option. When no option is tabbable (e.g. a radio
25
+ * group whose `value` matches no radio, such as `''`), the first focusable option is used instead.
26
+ */
27
+ export declare function useFocusManagerRef<T extends HTMLElement = HTMLDivElement>(forwardedRef: ForwardedRef<FocusHandle>, { accept }?: UseFocusManagerRefOptions): RefObject<T>;
28
+ /** `accept` predicate for groups whose options are `<input>` elements. */
29
+ export declare const acceptInputs: (node: Element) => boolean;
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+ import { createFocusManager } from '@react-aria/focus';
3
+ import { useImperativeHandle, useRef } from 'react';
4
+ export function useFocusManagerRef(forwardedRef, { accept } = {}) {
5
+ const wrapperRef = useRef(null);
6
+ useImperativeHandle(forwardedRef, ()=>({
7
+ focus: ()=>{
8
+ const focusManager = createFocusManager(wrapperRef);
9
+ if (!focusManager.focusFirst({
10
+ tabbable: true,
11
+ accept
12
+ })) {
13
+ focusManager.focusFirst({
14
+ accept
15
+ });
16
+ }
17
+ }
18
+ }), [
19
+ accept
20
+ ]);
21
+ return wrapperRef;
22
+ }
23
+ export const acceptInputs = (node)=>node.tagName === 'INPUT';
@@ -1,4 +1,5 @@
1
1
  export * from './dark-mode.hook.js';
2
2
  export * from './breakpoints.hook.js';
3
+ export { type FocusHandle } from './focus-manager-ref.hook.js';
3
4
  export * from '../components/pagination/pagination.hooks.js';
4
5
  export * from './breakpoints.hook.js';
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './components/index.js';
2
2
  export * from './types/index.js';
3
+ export { type FocusHandle } from './hook/focus-manager-ref.hook.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@westpac/ui",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "license": "MIT",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -259,14 +259,15 @@
259
259
  "typescript": "^5.5.4",
260
260
  "vite": "^7.3.5",
261
261
  "vitest": "^3.2.6",
262
- "@westpac/eslint-config": "~1.1.0",
263
- "@westpac/style-config": "~1.0.2",
264
262
  "@westpac/test-config": "~0.0.0",
265
- "@westpac/ts-config": "~0.0.0"
263
+ "@westpac/ts-config": "~0.0.0",
264
+ "@westpac/style-config": "~1.0.2",
265
+ "@westpac/eslint-config": "~1.1.0"
266
266
  },
267
267
  "dependencies": {
268
268
  "@internationalized/date": "^3.12.0",
269
269
  "@react-aria/accordion": "3.0.0-alpha.37",
270
+ "@react-aria/focus": "~3.21.5",
270
271
  "@react-aria/utils": "~3.29.1",
271
272
  "@react-spectrum/utils": "~3.11.5",
272
273
  "clsx": "^1.2.1",
@@ -1,9 +1,11 @@
1
1
  'use client';
2
2
 
3
- import React, { useCallback, useMemo } from 'react';
3
+ import React, { ForwardedRef, forwardRef, useCallback, useMemo } from 'react';
4
4
  import { useToggleButtonGroup } from 'react-aria';
5
5
  import { useToggleGroupState } from 'react-stately';
6
6
 
7
+ import { FocusHandle, useFocusManagerRef } from '../../hook/focus-manager-ref.hook.js';
8
+
7
9
  import { styles as buttonGroupStyles } from './button-group.styles.js';
8
10
 
9
11
  import type { ButtonGroupProps } from './button-group.types.js';
@@ -19,20 +21,26 @@ export const ToggleButtonGroupContext = React.createContext<
19
21
  | null
20
22
  >(null);
21
23
 
22
- export function ButtonGroup({
23
- size,
24
- look,
25
- block,
26
- children,
27
- onSelect,
28
- orientation = 'horizontal',
29
- selectionMode,
30
- selectedKeys,
31
- defaultSelectedKeys,
32
- onSelectionChange,
33
- className,
34
- ...props
35
- }: ButtonGroupProps) {
24
+ function BaseButtonGroup(
25
+ {
26
+ size,
27
+ look,
28
+ block,
29
+ children,
30
+ onSelect,
31
+ orientation = 'horizontal',
32
+ selectionMode,
33
+ selectedKeys,
34
+ defaultSelectedKeys,
35
+ onSelectionChange,
36
+ className,
37
+ ...props
38
+ }: ButtonGroupProps,
39
+ // `ref.current.focus()` (e.g. react-hook-form focusing a field with a validation error) moves
40
+ // focus to the first tabbable button in the group.
41
+ forwardedRef: ForwardedRef<FocusHandle>,
42
+ ) {
43
+ const ref = useFocusManagerRef(forwardedRef);
36
44
  /**
37
45
  * Normalizes key sets depending on selection mode.
38
46
  */
@@ -71,7 +79,6 @@ export function ButtonGroup({
71
79
  selectedKeys: finalSelectedKeys,
72
80
  });
73
81
 
74
- const ref = React.useRef<HTMLDivElement | null>(null);
75
82
  const { groupProps } = useToggleButtonGroup(
76
83
  {
77
84
  ...props,
@@ -95,3 +102,6 @@ export function ButtonGroup({
95
102
  </div>
96
103
  );
97
104
  }
105
+
106
+ export const ButtonGroup = forwardRef(BaseButtonGroup);
107
+ ButtonGroup.displayName = 'ButtonGroup';
@@ -1,11 +1,12 @@
1
1
  'use client';
2
2
 
3
- import React, { createContext, useEffect, useId, useMemo, useRef, useState } from 'react';
3
+ import React, { ForwardedRef, createContext, forwardRef, useEffect, useId, useMemo, useRef, useState } from 'react';
4
4
  import { useCheckboxGroup, useFocusRing } from 'react-aria';
5
5
  import { useCheckboxGroupState } from 'react-stately';
6
6
 
7
7
  import { FUNCTION_NOT_IMPLEMENTED } from '../../constants/message.js';
8
8
  import { useBreakpoint } from '../../hook/breakpoints.hook.js';
9
+ import { FocusHandle, acceptInputs, useFocusManagerRef } from '../../hook/focus-manager-ref.hook.js';
9
10
  import { resolveResponsiveVariant } from '../../utils/breakpoint.util.js';
10
11
  import { Button } from '../button/index.js';
11
12
  import { ExpandMoreIcon } from '../icon/index.js';
@@ -79,22 +80,28 @@ export const CheckboxGroupContext = createContext<CheckboxGroupContextState>({
79
80
  },
80
81
  },
81
82
  });
82
- export function CheckboxGroup({
83
- className,
84
- checkboxes,
85
- label,
86
- orientation = 'vertical',
87
- showAmount = 0,
88
- size = 'medium',
89
- errorMessage,
90
- hintMessage,
91
- ...props
92
- }: CheckboxGroupProps) {
83
+ function BaseCheckboxGroup(
84
+ {
85
+ className,
86
+ checkboxes,
87
+ label,
88
+ orientation = 'vertical',
89
+ showAmount = 0,
90
+ size = 'medium',
91
+ errorMessage,
92
+ hintMessage,
93
+ ...props
94
+ }: CheckboxGroupProps,
95
+ // `ref.current.focus()` (e.g. react-hook-form focusing a field with a validation error) moves focus
96
+ // to the first enabled checkbox.
97
+ ref: ForwardedRef<FocusHandle>,
98
+ ) {
99
+ const wrapperRef = useFocusManagerRef(ref, { accept: acceptInputs });
93
100
  const state = useCheckboxGroupState({ ...props, label });
94
101
  const { groupProps, labelProps, errorMessageProps, descriptionProps } = useCheckboxGroup({ ...props, label }, state);
95
102
  const { isFocusVisible, focusProps } = useFocusRing();
96
103
  const [hiddenOptions, setHiddenOptions] = useState<boolean>(showAmount > 0);
97
- const firstNewCheckboxRef = useRef<HTMLLabelElement>(null);
104
+ const firstNewCheckboxRef = useRef<HTMLInputElement>(null);
98
105
  const revealAmount = checkboxes && checkboxes.length - showAmount;
99
106
  const breakpoint = useBreakpoint();
100
107
  const styles = checkboxStyles({ orientation: resolveResponsiveVariant(orientation, breakpoint), isFocusVisible });
@@ -118,7 +125,7 @@ export function CheckboxGroup({
118
125
  <Label {...labelProps}>{label}</Label>
119
126
  {hintMessage && <Hint {...descriptionProps}>{hintMessage}</Hint>}
120
127
  {errorMessage && state.isInvalid && <ErrorMessage {...errorMessageProps} message={errorMessage} />}
121
- <div className={styles.itemWrapper()} id={panelId}>
128
+ <div className={styles.itemWrapper()} id={panelId} ref={wrapperRef}>
122
129
  <CheckboxGroupContext.Provider
123
130
  value={{
124
131
  state,
@@ -147,3 +154,6 @@ export function CheckboxGroup({
147
154
  </div>
148
155
  );
149
156
  }
157
+
158
+ export const CheckboxGroup = forwardRef(BaseCheckboxGroup);
159
+ CheckboxGroup.displayName = 'CheckboxGroup';
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
- import React, { Ref, forwardRef, useContext, useRef } from 'react';
4
- import { VisuallyHidden, mergeProps, useCheckboxGroupItem, useFocusRing } from 'react-aria';
3
+ import React, { ForwardedRef, forwardRef, useContext } from 'react';
4
+ import { VisuallyHidden, mergeProps, useCheckboxGroupItem, useFocusRing, useObjectRef } from 'react-aria';
5
5
 
6
6
  import { Icon } from '../../../icon/icon.component.js';
7
7
  import { IconProps } from '../../../icon/index.js';
@@ -33,22 +33,22 @@ function CheckIcon({ copyrightYear = '2025', size, ...props }: IconProps) {
33
33
 
34
34
  function BaseCheckbox(
35
35
  { className, hint, label, value, ...props }: CheckboxGroupCheckboxProps,
36
- ref: Ref<HTMLLabelElement>,
36
+ ref: ForwardedRef<HTMLInputElement>,
37
37
  ) {
38
38
  const { state, size, orientation } = useContext(CheckboxGroupContext);
39
- const localRef = useRef(null);
39
+ const inputRef = useObjectRef(ref);
40
40
  const { inputProps, labelProps, isDisabled, isSelected } = useCheckboxGroupItem(
41
41
  { ...props, value, children: label },
42
42
  state,
43
- localRef,
43
+ inputRef,
44
44
  );
45
45
  const { isFocusVisible, focusProps } = useFocusRing();
46
46
  const styles = checkboxItemStyles({ isDisabled, size, orientation, isFocusVisible });
47
47
 
48
48
  return (
49
- <label className={styles.base({ className })} ref={ref} {...labelProps}>
49
+ <label className={styles.base({ className })} {...labelProps}>
50
50
  <VisuallyHidden elementType="span">
51
- <input {...mergeProps(inputProps, focusProps)} ref={localRef} />
51
+ <input {...mergeProps(inputProps, focusProps)} ref={inputRef} />
52
52
  </VisuallyHidden>
53
53
  <span className={styles.checkbox()}>
54
54
  {isSelected && <CheckIcon className={styles.checkIcon()} size={size} color={isDisabled ? 'muted' : 'hero'} />}
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
- import React, { useRef } from 'react';
4
- import { mergeProps, useDateSegment, useFocusRing } from 'react-aria';
3
+ import React, { ForwardedRef, forwardRef } from 'react';
4
+ import { mergeProps, useDateSegment, useFocusRing, useObjectRef } from 'react-aria';
5
5
 
6
6
  import { styles as dateSegmentStyles } from './date-segment.styles.js';
7
7
  import { DateSegmentProps } from './date-segment.types.js';
@@ -9,8 +9,11 @@ import { DateSegmentProps } from './date-segment.types.js';
9
9
  /**
10
10
  * @private
11
11
  */
12
- export function DateSegment({ segment, state, separator, ...props }: DateSegmentProps) {
13
- const ref = useRef(null);
12
+ function BaseDateSegment(
13
+ { segment, state, separator, ...props }: DateSegmentProps,
14
+ forwardedRef: ForwardedRef<HTMLSpanElement>,
15
+ ) {
16
+ const ref = useObjectRef(forwardedRef);
14
17
  const { focusProps, isFocusVisible } = useFocusRing();
15
18
  const { segmentProps } = useDateSegment(segment, state, ref);
16
19
  const styles = dateSegmentStyles({
@@ -23,3 +26,6 @@ export function DateSegment({ segment, state, separator, ...props }: DateSegment
23
26
  </span>
24
27
  );
25
28
  }
29
+
30
+ export const DateSegment = forwardRef(BaseDateSegment);
31
+ DateSegment.displayName = 'DateSegment';
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { createCalendar } from '@internationalized/date';
4
- import React, { useRef } from 'react';
4
+ import React, { ForwardedRef, forwardRef, useRef } from 'react';
5
5
  import { useDateField, useLocale } from 'react-aria';
6
6
  import { useDateFieldState } from 'react-stately';
7
7
 
@@ -11,7 +11,11 @@ import { type DateFieldProps } from './date-field.types.js';
11
11
  /**
12
12
  * @private
13
13
  */
14
- export function DateField({ separator, className, ...props }: DateFieldProps) {
14
+ function BaseDateField(
15
+ { separator, className, ...props }: DateFieldProps,
16
+ // Attached to the first editable segment, the same element react-aria focuses for this field.
17
+ forwardedRef: ForwardedRef<HTMLSpanElement>,
18
+ ) {
15
19
  const { locale } = useLocale();
16
20
  const state = useDateFieldState({
17
21
  ...props,
@@ -21,12 +25,22 @@ export function DateField({ separator, className, ...props }: DateFieldProps) {
21
25
 
22
26
  const ref = useRef(null);
23
27
  const { fieldProps } = useDateField(props, state, ref);
28
+ const firstEditableIndex = state.segments.findIndex(segment => segment.isEditable);
24
29
 
25
30
  return (
26
31
  <div {...fieldProps} className={className} ref={ref}>
27
32
  {state.segments.map((segment, i) => (
28
- <DateSegment separator={separator} key={i} segment={segment} state={state} />
33
+ <DateSegment
34
+ separator={separator}
35
+ key={i}
36
+ segment={segment}
37
+ state={state}
38
+ ref={i === firstEditableIndex ? forwardedRef : null}
39
+ />
29
40
  ))}
30
41
  </div>
31
42
  );
32
43
  }
44
+
45
+ export const DateField = forwardRef(BaseDateField);
46
+ DateField.displayName = 'DateField';