@westpac/ui 1.16.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 (79) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/component-type.json +1 -1
  3. package/dist/components/button/button.component.d.ts +2 -17
  4. package/dist/components/button/button.component.js +9 -4
  5. package/dist/components/button/button.types.d.ts +16 -6
  6. package/dist/components/button-group/button-group.component.d.ts +2 -1
  7. package/dist/components/button-group/button-group.component.js +6 -3
  8. package/dist/components/checkbox-group/checkbox-group.component.d.ts +11 -2
  9. package/dist/components/checkbox-group/checkbox-group.component.js +10 -3
  10. package/dist/components/checkbox-group/components/checkbox-group-checkbox/checkbox-group-checkbox.component.d.ts +1 -1
  11. package/dist/components/checkbox-group/components/checkbox-group-checkbox/checkbox-group-checkbox.component.js +5 -6
  12. package/dist/components/date-picker/components/date-field/components/date-segment/date-segment.component.d.ts +6 -5
  13. package/dist/components/date-picker/components/date-field/components/date-segment/date-segment.component.js +6 -4
  14. package/dist/components/date-picker/components/date-field/date-field.component.d.ts +5 -5
  15. package/dist/components/date-picker/components/date-field/date-field.component.js +7 -3
  16. package/dist/components/date-picker/date-picker.component.d.ts +19 -2
  17. package/dist/components/date-picker/date-picker.component.js +10 -5
  18. package/dist/components/date-picker/date-picker.styles.js +5 -5
  19. package/dist/components/link/link.component.d.ts +4 -12
  20. package/dist/components/link/link.component.js +30 -7
  21. package/dist/components/link/link.types.d.ts +26 -3
  22. package/dist/components/modal/components/modal-dialog/components/modal-dialog-footer/modal-dialog-footer.component.d.ts +1 -1
  23. package/dist/components/modal/components/modal-dialog/components/modal-dialog-footer/modal-dialog-footer.component.js +14 -7
  24. package/dist/components/modal/components/modal-dialog/components/modal-dialog-footer/modal-dialog-footer.types.d.ts +11 -0
  25. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.component.js +3 -7
  26. 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
  27. package/dist/components/multi-select/components/multi-select-popover/multi-select-popover.component.js +5 -2
  28. package/dist/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.component.js +4 -7
  29. package/dist/components/multi-select/multi-select.component.d.ts +14 -3
  30. package/dist/components/multi-select/multi-select.component.js +10 -16
  31. package/dist/components/radio-group/components/radio-group-radio/radio-group-radio.component.d.ts +1 -1
  32. package/dist/components/radio-group/components/radio-group-radio/radio-group-radio.component.js +5 -6
  33. package/dist/components/radio-group/radio-group.component.d.ts +10 -2
  34. package/dist/components/radio-group/radio-group.component.js +10 -3
  35. package/dist/components/selector/components/selector-button-group/selector-button-group.component.d.ts +9 -2
  36. package/dist/components/selector/components/selector-button-group/selector-button-group.component.js +8 -3
  37. package/dist/components/selector/components/selector-checkbox-group/selector-checkbox-group.component.d.ts +6 -2
  38. package/dist/components/selector/components/selector-checkbox-group/selector-checkbox-group.component.js +8 -3
  39. package/dist/components/selector/components/selector-radio-group/selector-radio-group.component.d.ts +6 -2
  40. package/dist/components/selector/components/selector-radio-group/selector-radio-group.component.js +8 -3
  41. package/dist/components/selector/selector.component.d.ts +3 -1
  42. package/dist/components/selector/selector.component.js +16 -5
  43. package/dist/components/switch/switch.component.d.ts +8 -2
  44. package/dist/components/switch/switch.component.js +6 -4
  45. package/dist/css/westpac-ui.css +12 -12
  46. package/dist/css/westpac-ui.min.css +12 -12
  47. package/dist/hook/focus-manager-ref.hook.d.ts +29 -0
  48. package/dist/hook/focus-manager-ref.hook.js +23 -0
  49. package/dist/hook/index.d.ts +1 -0
  50. package/dist/index.d.ts +1 -0
  51. package/package.json +5 -3
  52. package/src/components/button/button.component.tsx +10 -9
  53. package/src/components/button/button.types.ts +21 -8
  54. package/src/components/button-group/button-group.component.tsx +26 -16
  55. package/src/components/checkbox-group/checkbox-group.component.tsx +24 -14
  56. package/src/components/checkbox-group/components/checkbox-group-checkbox/checkbox-group-checkbox.component.tsx +7 -7
  57. package/src/components/date-picker/components/date-field/components/date-segment/date-segment.component.tsx +10 -4
  58. package/src/components/date-picker/components/date-field/date-field.component.tsx +17 -3
  59. package/src/components/date-picker/date-picker.component.tsx +32 -21
  60. package/src/components/date-picker/date-picker.styles.ts +5 -5
  61. package/src/components/link/link.component.tsx +60 -12
  62. package/src/components/link/link.types.ts +31 -4
  63. package/src/components/modal/components/modal-dialog/components/modal-dialog-footer/modal-dialog-footer.component.tsx +16 -2
  64. package/src/components/modal/components/modal-dialog/components/modal-dialog-footer/modal-dialog-footer.types.ts +12 -0
  65. package/src/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.component.tsx +3 -5
  66. 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
  67. package/src/components/multi-select/components/multi-select-popover/multi-select-popover.component.tsx +8 -2
  68. package/src/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.component.tsx +3 -6
  69. package/src/components/multi-select/multi-select.component.tsx +33 -28
  70. package/src/components/radio-group/components/radio-group-radio/radio-group-radio.component.tsx +10 -7
  71. package/src/components/radio-group/radio-group.component.tsx +24 -14
  72. package/src/components/selector/components/selector-button-group/selector-button-group.component.tsx +23 -14
  73. package/src/components/selector/components/selector-checkbox-group/selector-checkbox-group.component.tsx +12 -3
  74. package/src/components/selector/components/selector-radio-group/selector-radio-group.component.tsx +21 -11
  75. package/src/components/selector/selector.component.tsx +16 -5
  76. package/src/components/switch/switch.component.tsx +10 -12
  77. package/src/hook/focus-manager-ref.hook.ts +56 -0
  78. package/src/hook/index.ts +1 -0
  79. package/src/index.ts +1 -0
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.16.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
262
  "@westpac/test-config": "~0.0.0",
264
263
  "@westpac/ts-config": "~0.0.0",
265
- "@westpac/style-config": "~1.0.2"
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",
@@ -280,6 +281,7 @@
280
281
  },
281
282
  "peerDependencies": {
282
283
  "react": ">=18.2.0",
284
+ "react-dom": ">=18.2.0",
283
285
  "tailwindcss": "~4.1.12"
284
286
  },
285
287
  "repository": {
@@ -1,13 +1,13 @@
1
1
  'use client';
2
2
 
3
- import React, { Ref, forwardRef, useMemo } from 'react';
3
+ import React, { ForwardedRef, forwardRef, useMemo } from 'react';
4
4
  import { mergeProps, useFocusRing } from 'react-aria';
5
5
 
6
6
  import { useBreakpoint } from '../../hook/breakpoints.hook.js';
7
7
  import { resolveResponsiveVariant } from '../../utils/breakpoint.util.js';
8
8
 
9
9
  import { styles as buttonStyles } from './button.styles.js';
10
- import { type ButtonProps, ButtonRef } from './button.types.js';
10
+ import { ButtonImplementationProps, ButtonComponent } from './button.types.js';
11
11
  import { getIconSize } from './button.utils.js';
12
12
 
13
13
  function BaseButton(
@@ -18,7 +18,7 @@ function BaseButton(
18
18
  soft,
19
19
  block = false,
20
20
  justify,
21
- tag: Tag = 'button',
21
+ tag: Tag,
22
22
  iconBefore: IconBefore,
23
23
  iconAfter: IconAfter,
24
24
  iconLook,
@@ -26,11 +26,12 @@ function BaseButton(
26
26
  iconSize,
27
27
  children,
28
28
  removeLinkPadding,
29
- type = Tag === 'button' ? 'button' : undefined,
30
29
  ...props
31
- }: ButtonProps,
32
- ref: Ref<ButtonRef>,
30
+ }: ButtonImplementationProps,
31
+ ref: ForwardedRef<unknown>,
33
32
  ) {
33
+ const Component = Tag ?? 'button';
34
+ const renderProps = Component === 'button' ? { ...props, type: props.type ?? 'button' } : props;
34
35
  const { isFocusVisible, focusProps } = useFocusRing();
35
36
  const btnIconSize = useMemo(() => iconSize || getIconSize(size), [iconSize, size]);
36
37
  const breakpoint = useBreakpoint();
@@ -76,7 +77,7 @@ function BaseButton(
76
77
  }, [iconColor, look, soft]);
77
78
 
78
79
  return (
79
- <Tag ref={ref} className={styles.base({ className })} {...mergeProps(props, focusProps)} type={type}>
80
+ <Component ref={ref} className={styles.base({ className })} {...mergeProps(renderProps, focusProps)}>
80
81
  {IconBefore && (
81
82
  <IconBefore
82
83
  look={iconLook}
@@ -96,8 +97,8 @@ function BaseButton(
96
97
  aria-hidden
97
98
  />
98
99
  )}
99
- </Tag>
100
+ </Component>
100
101
  );
101
102
  }
102
103
 
103
- export const Button = forwardRef(BaseButton);
104
+ export const Button = forwardRef(BaseButton) as unknown as ButtonComponent;
@@ -1,4 +1,4 @@
1
- import { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from 'react';
1
+ import { ReactNode } from 'react';
2
2
  import { type VariantProps } from 'tailwind-variants';
3
3
 
4
4
  import { ResponsiveVariants } from '../../types/responsive-variants.types.js';
@@ -8,9 +8,7 @@ import { styles } from './button.styles.js';
8
8
 
9
9
  type Variants = VariantProps<typeof styles>;
10
10
 
11
- export type ButtonRef = HTMLButtonElement & HTMLAnchorElement & HTMLSpanElement & HTMLDivElement;
12
-
13
- export type ButtonProps = {
11
+ export type BaseButtonProps = {
14
12
  /**
15
13
  * Fit button width to its parent width.
16
14
  * @default false
@@ -69,10 +67,25 @@ export type ButtonProps = {
69
67
  * Removes background colour and adjusts text colour.
70
68
  */
71
69
  soft?: ResponsiveVariants<Variants['soft']>;
70
+ };
71
+
72
+ export type ButtonRef<C extends React.ElementType = 'button'> = React.ComponentRef<C>;
73
+
74
+ export type PolymorphicRef<C extends React.ElementType> = React.ComponentPropsWithRef<C>['ref'];
75
+
76
+ export type ButtonProps<C extends React.ElementType = 'button'> = BaseButtonProps & {
72
77
  /**
73
- * Tag to render
78
+ * Type to render
74
79
  * @default button
75
80
  */
76
- tag?: keyof Pick<JSX.IntrinsicElements, 'a' | 'span' | 'button' | 'div'>;
77
- } & ButtonHTMLAttributes<Element> &
78
- AnchorHTMLAttributes<Element>;
81
+ tag?: C;
82
+ } & Omit<React.ComponentPropsWithoutRef<C>, keyof BaseButtonProps | 'tag'>;
83
+
84
+ export type ButtonImplementationProps = BaseButtonProps & {
85
+ className?: string;
86
+ tag?: React.ElementType;
87
+ } & Record<string, unknown>;
88
+
89
+ export type ButtonComponent = <C extends React.ElementType = 'button'>(
90
+ props: ButtonProps<C> & { ref?: PolymorphicRef<C> },
91
+ ) => React.ReactElement | null;
@@ -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';
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { DateValue, getDayOfWeek, isWeekend } from '@internationalized/date';
4
4
  import { Breakpoint } from '@westpac/style-config/constants';
5
- import React, { useMemo, useRef } from 'react';
5
+ import React, { ForwardedRef, forwardRef, useMemo, useRef } from 'react';
6
6
  import { useButton, useDatePicker, useLocale } from 'react-aria';
7
7
  import { useDatePickerState } from 'react-stately';
8
8
 
@@ -19,19 +19,24 @@ import { styles as datePickerStyles } from './date-picker.styles.js';
19
19
  import { type DatePickerProps } from './date-picker.types.js';
20
20
 
21
21
  const BREAKPOINTS_DECRECENT = ['xl', 'lg', 'md', 'sm', 'xsl', 'initial'] as const;
22
- export function DatePicker({
23
- size = 'medium',
24
- className,
25
- bottomSheetView = { initial: true, xsl: false },
26
- block,
27
- isDateUnavailable,
28
- disableDaysOfWeek,
29
- disableWeekends,
30
- separator,
31
- portalContainer,
32
- placement = 'bottom left',
33
- ...props
34
- }: DatePickerProps) {
22
+ function BaseDatePicker(
23
+ {
24
+ size = 'medium',
25
+ className,
26
+ bottomSheetView = { initial: true, xsl: false },
27
+ block,
28
+ isDateUnavailable,
29
+ disableDaysOfWeek,
30
+ disableWeekends,
31
+ separator,
32
+ portalContainer,
33
+ placement = 'bottom left',
34
+ ...props
35
+ }: DatePickerProps,
36
+ // Attached to the first editable date segment, so `ref.current.focus()` (e.g. react-hook-form
37
+ // focusing a field with a validation error) lands on a focusable control.
38
+ forwardedRef: ForwardedRef<HTMLSpanElement>,
39
+ ) {
35
40
  const { locale } = useLocale();
36
41
 
37
42
  const enhancedIsDateUnavailable = useMemo(() => {
@@ -60,7 +65,7 @@ export function DatePicker({
60
65
  isReadOnly: props.isReadOnly,
61
66
  isDisabled: props.isDisabled,
62
67
  });
63
- const ref = useRef(null);
68
+ const ref = useRef<HTMLDivElement>(null);
64
69
 
65
70
  const { groupProps, labelProps, fieldProps, buttonProps, dialogProps, calendarProps } = useDatePicker(
66
71
  { isDateUnavailable: enhancedIsDateUnavailable, ...props },
@@ -84,12 +89,15 @@ export function DatePicker({
84
89
 
85
90
  const { buttonProps: newButtonProps } = useButton(buttonProps, buttonRef);
86
91
 
92
+ // This is required so branding applies correctly by default due to portal location, can be overridden with portalContainer prop
87
93
  const brandContainer = useMemo(() => {
88
- return (
89
- document.querySelector('[data-brand]') ||
90
- document.querySelector('[class^="theme-"], [class*=" theme-"]') ||
91
- undefined
92
- );
94
+ if (typeof window !== 'undefined') {
95
+ return (
96
+ document.querySelector('[data-brand]') ||
97
+ document.querySelector('[class^="theme-"], [class*=" theme-"]') ||
98
+ undefined
99
+ );
100
+ }
93
101
  }, []);
94
102
 
95
103
  return (
@@ -107,7 +115,7 @@ export function DatePicker({
107
115
  }}
108
116
  className={styles.input({ className })}
109
117
  >
110
- <DateField className={styles.dateField()} separator={separator} {...fieldProps} />
118
+ <DateField className={styles.dateField()} separator={separator} {...fieldProps} ref={forwardedRef} />
111
119
  <Button
112
120
  look="faint"
113
121
  className={styles.button()}
@@ -134,3 +142,6 @@ export function DatePicker({
134
142
  </>
135
143
  );
136
144
  }
145
+
146
+ export const DatePicker = forwardRef(BaseDatePicker);
147
+ DatePicker.displayName = 'DatePicker';
@@ -3,26 +3,26 @@ import { tv } from 'tailwind-variants';
3
3
  export const styles = tv({
4
4
  slots: {
5
5
  input: 'form-control flex items-center border-0',
6
- dateField: 'rounded rounded-r-none border border-r-0 border-border-muted-strong',
6
+ dateField: 'rounded rounded-r-none border border-r-0 border-border-muted-strong whitespace-nowrap',
7
7
  button: 'flex items-center justify-center rounded-l-none border-border-muted-strong bg-surface-muted-pale',
8
8
  },
9
9
  variants: {
10
10
  size: {
11
11
  small: {
12
12
  input: 'max-h-5',
13
- dateField: 'h-5 w-[120px] form-control-small',
13
+ dateField: 'h-5 min-w-[120px] form-control-small',
14
14
  },
15
15
  medium: {
16
16
  input: 'max-h-6',
17
- dateField: 'h-6 w-[140px] form-control-medium',
17
+ dateField: 'h-6 min-w-[140px] form-control-medium',
18
18
  },
19
19
  large: {
20
20
  input: 'max-h-7',
21
- dateField: 'h-7 w-[145px] form-control-large',
21
+ dateField: 'h-7 min-w-[145px] form-control-large',
22
22
  },
23
23
  xlarge: {
24
24
  input: 'max-h-8',
25
- dateField: 'h-8 w-[165px] form-control-xlarge',
25
+ dateField: 'h-8 min-w-[165px] form-control-xlarge',
26
26
  },
27
27
  },
28
28
  block: {
@@ -1,30 +1,77 @@
1
- // TODO: This component will not work with the Next Link component in Next.js 16+ as legacyBehavior as has been deprecated in 15. Need to find a solution.
2
1
  'use client';
3
2
 
4
- import React, { ForwardedRef, RefObject, forwardRef } from 'react';
5
- import { mergeProps, useFocusRing, useLink } from 'react-aria';
3
+ import { type FocusableElement } from '@react-types/shared';
4
+ import React, { ForwardedRef, forwardRef } from 'react';
5
+ import { mergeProps, useFocusRing, useLink, useObjectRef } from 'react-aria';
6
6
 
7
7
  import { ArrowRightIcon } from '../icon/index.js';
8
8
 
9
9
  import { styles as linkStyles } from './link.styles.js';
10
- import { type LinkProps } from './link.types.js';
10
+ import { LinkComponent, LinkImplementationProps } from './link.types.js';
11
11
 
12
12
  export function BaseLink(
13
13
  {
14
14
  className,
15
15
  children,
16
16
  href,
17
+ tag: Tag,
17
18
  iconBefore: IconBefore,
18
19
  iconAfter: IconAfter,
19
20
  iconSize = 'small',
20
21
  target,
21
22
  type = 'standalone',
22
23
  underline = true,
23
- ...props
24
- }: LinkProps,
25
- ref: ForwardedRef<HTMLAnchorElement>,
24
+ autoFocus,
25
+ elementType,
26
+ onBlur,
27
+ onClick,
28
+ onFocus,
29
+ onFocusChange,
30
+ onKeyDown,
31
+ onKeyUp,
32
+ onPress,
33
+ onPressChange,
34
+ onPressEnd,
35
+ onPressStart,
36
+ onPressUp,
37
+ download,
38
+ ping,
39
+ referrerPolicy,
40
+ rel,
41
+ routerOptions,
42
+ ...componentProps
43
+ }: LinkImplementationProps,
44
+ ref: ForwardedRef<unknown>,
26
45
  ) {
27
- const { linkProps } = useLink({ ...props, elementType: 'a' }, ref as RefObject<HTMLAnchorElement>);
46
+ const Component = Tag ?? 'a';
47
+ const ariaElementType = typeof Component === 'string' ? Component : (elementType ?? 'a');
48
+ const linkRef = useObjectRef(ref as ForwardedRef<FocusableElement>);
49
+ const { linkProps } = useLink(
50
+ {
51
+ ...componentProps,
52
+ autoFocus,
53
+ download,
54
+ elementType: ariaElementType,
55
+ href: typeof href === 'string' ? href : undefined,
56
+ onBlur,
57
+ onClick,
58
+ onFocus,
59
+ onFocusChange,
60
+ onKeyDown,
61
+ onKeyUp,
62
+ onPress,
63
+ onPressChange,
64
+ onPressEnd,
65
+ onPressStart,
66
+ onPressUp,
67
+ ping,
68
+ referrerPolicy,
69
+ rel,
70
+ routerOptions,
71
+ target,
72
+ },
73
+ linkRef,
74
+ );
28
75
  const { isFocusVisible, focusProps } = useFocusRing();
29
76
  const styles = linkStyles({ type, underline, isFocusVisible });
30
77
 
@@ -33,9 +80,10 @@ export function BaseLink(
33
80
  }
34
81
 
35
82
  return (
36
- <a
83
+ <Component
84
+ {...componentProps}
37
85
  {...mergeProps(linkProps, focusProps)}
38
- ref={ref}
86
+ ref={linkRef}
39
87
  href={href}
40
88
  target={target}
41
89
  className={styles.base({ className })}
@@ -43,8 +91,8 @@ export function BaseLink(
43
91
  {IconBefore && <IconBefore size={iconSize} color="primary" className={styles.iconBefore()} />}
44
92
  <span>{children}</span>
45
93
  {IconAfter && <IconAfter size={iconSize} color="primary" className={styles.iconAfter()} />}
46
- </a>
94
+ </Component>
47
95
  );
48
96
  }
49
97
 
50
- export const Link = forwardRef(BaseLink);
98
+ export const Link = forwardRef(BaseLink) as unknown as LinkComponent;