@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.
- package/CHANGELOG.md +37 -0
- package/dist/component-type.json +1 -1
- package/dist/components/button/button.component.d.ts +2 -17
- package/dist/components/button/button.component.js +9 -4
- package/dist/components/button/button.types.d.ts +16 -6
- package/dist/components/button-group/button-group.component.d.ts +2 -1
- package/dist/components/button-group/button-group.component.js +6 -3
- package/dist/components/checkbox-group/checkbox-group.component.d.ts +11 -2
- package/dist/components/checkbox-group/checkbox-group.component.js +10 -3
- package/dist/components/checkbox-group/components/checkbox-group-checkbox/checkbox-group-checkbox.component.d.ts +1 -1
- package/dist/components/checkbox-group/components/checkbox-group-checkbox/checkbox-group-checkbox.component.js +5 -6
- package/dist/components/date-picker/components/date-field/components/date-segment/date-segment.component.d.ts +6 -5
- package/dist/components/date-picker/components/date-field/components/date-segment/date-segment.component.js +6 -4
- package/dist/components/date-picker/components/date-field/date-field.component.d.ts +5 -5
- package/dist/components/date-picker/components/date-field/date-field.component.js +7 -3
- package/dist/components/date-picker/date-picker.component.d.ts +19 -2
- package/dist/components/date-picker/date-picker.component.js +10 -5
- package/dist/components/date-picker/date-picker.styles.js +5 -5
- package/dist/components/link/link.component.d.ts +4 -12
- package/dist/components/link/link.component.js +30 -7
- package/dist/components/link/link.types.d.ts +26 -3
- package/dist/components/modal/components/modal-dialog/components/modal-dialog-footer/modal-dialog-footer.component.d.ts +1 -1
- package/dist/components/modal/components/modal-dialog/components/modal-dialog-footer/modal-dialog-footer.component.js +14 -7
- package/dist/components/modal/components/modal-dialog/components/modal-dialog-footer/modal-dialog-footer.types.d.ts +11 -0
- package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.component.js +3 -7
- 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
- package/dist/components/multi-select/components/multi-select-popover/multi-select-popover.component.js +5 -2
- package/dist/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.component.js +4 -7
- package/dist/components/multi-select/multi-select.component.d.ts +14 -3
- package/dist/components/multi-select/multi-select.component.js +10 -16
- package/dist/components/radio-group/components/radio-group-radio/radio-group-radio.component.d.ts +1 -1
- package/dist/components/radio-group/components/radio-group-radio/radio-group-radio.component.js +5 -6
- package/dist/components/radio-group/radio-group.component.d.ts +10 -2
- package/dist/components/radio-group/radio-group.component.js +10 -3
- package/dist/components/selector/components/selector-button-group/selector-button-group.component.d.ts +9 -2
- package/dist/components/selector/components/selector-button-group/selector-button-group.component.js +8 -3
- package/dist/components/selector/components/selector-checkbox-group/selector-checkbox-group.component.d.ts +6 -2
- package/dist/components/selector/components/selector-checkbox-group/selector-checkbox-group.component.js +8 -3
- package/dist/components/selector/components/selector-radio-group/selector-radio-group.component.d.ts +6 -2
- package/dist/components/selector/components/selector-radio-group/selector-radio-group.component.js +8 -3
- package/dist/components/selector/selector.component.d.ts +3 -1
- package/dist/components/selector/selector.component.js +16 -5
- package/dist/components/switch/switch.component.d.ts +8 -2
- package/dist/components/switch/switch.component.js +6 -4
- package/dist/css/westpac-ui.css +12 -12
- package/dist/css/westpac-ui.min.css +12 -12
- package/dist/hook/focus-manager-ref.hook.d.ts +29 -0
- package/dist/hook/focus-manager-ref.hook.js +23 -0
- package/dist/hook/index.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/package.json +5 -3
- package/src/components/button/button.component.tsx +10 -9
- package/src/components/button/button.types.ts +21 -8
- package/src/components/button-group/button-group.component.tsx +26 -16
- package/src/components/checkbox-group/checkbox-group.component.tsx +24 -14
- package/src/components/checkbox-group/components/checkbox-group-checkbox/checkbox-group-checkbox.component.tsx +7 -7
- package/src/components/date-picker/components/date-field/components/date-segment/date-segment.component.tsx +10 -4
- package/src/components/date-picker/components/date-field/date-field.component.tsx +17 -3
- package/src/components/date-picker/date-picker.component.tsx +32 -21
- package/src/components/date-picker/date-picker.styles.ts +5 -5
- package/src/components/link/link.component.tsx +60 -12
- package/src/components/link/link.types.ts +31 -4
- package/src/components/modal/components/modal-dialog/components/modal-dialog-footer/modal-dialog-footer.component.tsx +16 -2
- package/src/components/modal/components/modal-dialog/components/modal-dialog-footer/modal-dialog-footer.types.ts +12 -0
- package/src/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.component.tsx +3 -5
- 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
- package/src/components/multi-select/components/multi-select-popover/multi-select-popover.component.tsx +8 -2
- package/src/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.component.tsx +3 -6
- package/src/components/multi-select/multi-select.component.tsx +33 -28
- package/src/components/radio-group/components/radio-group-radio/radio-group-radio.component.tsx +10 -7
- package/src/components/radio-group/radio-group.component.tsx +24 -14
- package/src/components/selector/components/selector-button-group/selector-button-group.component.tsx +23 -14
- package/src/components/selector/components/selector-checkbox-group/selector-checkbox-group.component.tsx +12 -3
- package/src/components/selector/components/selector-radio-group/selector-radio-group.component.tsx +21 -11
- package/src/components/selector/selector.component.tsx +16 -5
- package/src/components/switch/switch.component.tsx +10 -12
- package/src/hook/focus-manager-ref.hook.ts +56 -0
- package/src/hook/index.ts +1 -0
- package/src/index.ts +1 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
2
|
import { AriaLinkOptions } from 'react-aria';
|
|
3
3
|
|
|
4
4
|
import { IconProps } from '../icon/index.js';
|
|
5
5
|
|
|
6
|
-
export type
|
|
6
|
+
export type BaseLinkProps = {
|
|
7
7
|
/**
|
|
8
8
|
* Link text or component
|
|
9
9
|
*/
|
|
@@ -35,5 +35,32 @@ export type LinkProps = {
|
|
|
35
35
|
* @default true
|
|
36
36
|
*/
|
|
37
37
|
underline?: boolean;
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Specifies DOM element rendered by a custom component
|
|
40
|
+
* Required when the custom component does not render an '<a>' element
|
|
41
|
+
* @default 'a'
|
|
42
|
+
*/
|
|
43
|
+
elementType?: AriaLinkOptions['elementType'];
|
|
44
|
+
} & Omit<AriaLinkOptions, 'elementType' | 'href' | 'isDisabled'>;
|
|
45
|
+
|
|
46
|
+
export type LinkRef<C extends React.ElementType = 'a'> = React.ComponentRef<C>;
|
|
47
|
+
|
|
48
|
+
export type PolymorphicRef<C extends React.ElementType> = React.ComponentPropsWithRef<C>['ref'];
|
|
49
|
+
|
|
50
|
+
export type LinkProps<C extends React.ElementType = 'a'> = BaseLinkProps & {
|
|
51
|
+
/**
|
|
52
|
+
* Type to render
|
|
53
|
+
* @default a
|
|
54
|
+
*/
|
|
55
|
+
tag?: C;
|
|
56
|
+
} & Omit<React.ComponentPropsWithoutRef<C>, keyof BaseLinkProps | 'tag'>;
|
|
57
|
+
|
|
58
|
+
export type LinkImplementationProps = BaseLinkProps & {
|
|
59
|
+
className?: string;
|
|
60
|
+
href?: unknown;
|
|
61
|
+
tag?: React.ElementType;
|
|
62
|
+
} & Record<string, unknown>;
|
|
63
|
+
|
|
64
|
+
export type LinkComponent = <C extends React.ElementType = 'a'>(
|
|
65
|
+
props: LinkProps<C> & { ref?: PolymorphicRef<C> },
|
|
66
|
+
) => React.ReactElement | null;
|
|
@@ -12,8 +12,10 @@ import { type ModalDialogFooterProps } from './modal-dialog-footer.types.js';
|
|
|
12
12
|
|
|
13
13
|
export function ModalDialogFooter({
|
|
14
14
|
className,
|
|
15
|
+
primaryButtonProps,
|
|
15
16
|
primaryLabel,
|
|
16
17
|
primaryOnClick,
|
|
18
|
+
secondaryButtonProps,
|
|
17
19
|
secondaryLabel,
|
|
18
20
|
secondaryOnClick,
|
|
19
21
|
...props
|
|
@@ -29,11 +31,23 @@ export function ModalDialogFooter({
|
|
|
29
31
|
|
|
30
32
|
return (
|
|
31
33
|
<div className={styles.base({ className })} {...props}>
|
|
32
|
-
<Button
|
|
34
|
+
<Button
|
|
35
|
+
{...primaryButtonProps}
|
|
36
|
+
look={primaryButtonProps?.look ?? 'primary'}
|
|
37
|
+
size={primaryButtonProps?.size ?? 'large'}
|
|
38
|
+
className={styles.primaryBtn({ className: primaryButtonProps?.className })}
|
|
39
|
+
onClick={primaryOnClick}
|
|
40
|
+
>
|
|
33
41
|
{primaryLabel}
|
|
34
42
|
</Button>
|
|
35
43
|
{secondaryLabel && (
|
|
36
|
-
<Button
|
|
44
|
+
<Button
|
|
45
|
+
{...secondaryButtonProps}
|
|
46
|
+
look={secondaryButtonProps?.look ?? 'link'}
|
|
47
|
+
size={secondaryButtonProps?.size ?? 'large'}
|
|
48
|
+
className={styles.secondaryBtn({ className: secondaryButtonProps?.className })}
|
|
49
|
+
onClick={secondaryOnClick}
|
|
50
|
+
>
|
|
37
51
|
{secondaryLabel}
|
|
38
52
|
</Button>
|
|
39
53
|
)}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { HTMLAttributes, ReactNode } from 'react';
|
|
2
2
|
|
|
3
|
+
import { type ButtonProps } from '../../../../../button/index.js';
|
|
4
|
+
|
|
5
|
+
type ModalFooterButtonProps = Omit<ButtonProps, 'children' | 'onClick'>;
|
|
6
|
+
|
|
3
7
|
export type ModalDialogFooterProps = {
|
|
4
8
|
/**
|
|
5
9
|
* Footer content
|
|
6
10
|
*/
|
|
7
11
|
children?: ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Props passed to the primary button
|
|
14
|
+
*/
|
|
15
|
+
primaryButtonProps?: ModalFooterButtonProps;
|
|
8
16
|
/**
|
|
9
17
|
* Label for primary button
|
|
10
18
|
*/
|
|
@@ -13,6 +21,10 @@ export type ModalDialogFooterProps = {
|
|
|
13
21
|
* onClick for primary button
|
|
14
22
|
*/
|
|
15
23
|
primaryOnClick: () => void;
|
|
24
|
+
/**
|
|
25
|
+
* Props passed to the secondary button
|
|
26
|
+
*/
|
|
27
|
+
secondaryButtonProps?: ModalFooterButtonProps;
|
|
16
28
|
/**
|
|
17
29
|
* Label for secondary button
|
|
18
30
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { focusWithoutScrolling } from '@react-aria/utils';
|
|
3
4
|
import React, { useCallback, useContext, useRef, KeyboardEvent } from 'react';
|
|
4
5
|
import { mergeProps, useFocusRing, useOption } from 'react-aria';
|
|
5
6
|
|
|
@@ -27,11 +28,8 @@ export function MultiSelectOption<T>({ item }: MultiSelectOptionProps<T>) {
|
|
|
27
28
|
(e: KeyboardEvent<HTMLLIElement>) => {
|
|
28
29
|
if (e.key === 'ArrowUp' && item.index === 0) {
|
|
29
30
|
e.preventDefault();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} else {
|
|
33
|
-
inputRef.current?.focus();
|
|
34
|
-
}
|
|
31
|
+
const target = selectAllRef.current || inputRef.current;
|
|
32
|
+
if (target) focusWithoutScrolling(target);
|
|
35
33
|
}
|
|
36
34
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
37
35
|
e.preventDefault();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { focusWithoutScrolling } from '@react-aria/utils';
|
|
3
4
|
import React, { useCallback, useContext, useMemo, KeyboardEvent } from 'react';
|
|
4
5
|
import { useOption, useFocusRing, mergeProps } from 'react-aria';
|
|
5
6
|
|
|
@@ -54,12 +55,12 @@ export function MultiSelectSelectAllOption() {
|
|
|
54
55
|
(e: KeyboardEvent<HTMLDivElement>) => {
|
|
55
56
|
if (e.key === 'ArrowDown') {
|
|
56
57
|
e.preventDefault();
|
|
57
|
-
const firstItem = listBoxRef.current?.querySelector('[data-key]') as HTMLElement;
|
|
58
|
-
firstItem
|
|
58
|
+
const firstItem = listBoxRef.current?.querySelector('[data-key]') as HTMLElement | null;
|
|
59
|
+
if (firstItem) focusWithoutScrolling(firstItem);
|
|
59
60
|
}
|
|
60
61
|
if (e.key === 'ArrowUp') {
|
|
61
62
|
e.preventDefault();
|
|
62
|
-
inputRef.current
|
|
63
|
+
if (inputRef.current) focusWithoutScrolling(inputRef.current);
|
|
63
64
|
}
|
|
64
65
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
65
66
|
e.preventDefault();
|
|
@@ -24,6 +24,13 @@ export function MultiSelectPopover({ children, className, ...props }: MultiSelec
|
|
|
24
24
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
25
25
|
}, []);
|
|
26
26
|
|
|
27
|
+
// react-aria assumes a non-modal popover never receives DOM focus, so it closes it whenever the page
|
|
28
|
+
// scrolls (useOverlayPosition -> useCloseOnScroll via `state.close`). Focus does move into this popover
|
|
29
|
+
// (filter input / options), and browsers may scroll the page when it does, which closed the dropdown
|
|
30
|
+
// mid keyboard navigation. Dismissal (blur, Escape, Tab, DismissButton) is handled explicitly below, so
|
|
31
|
+
// give react-aria a state it cannot close.
|
|
32
|
+
const positioningState = { ...overlayState, close: () => undefined };
|
|
33
|
+
|
|
27
34
|
const { popoverProps } = usePopover(
|
|
28
35
|
{
|
|
29
36
|
...props,
|
|
@@ -35,7 +42,7 @@ export function MultiSelectPopover({ children, className, ...props }: MultiSelec
|
|
|
35
42
|
shouldCloseOnInteractOutside: () => false, // need to manage accessibility manually due to complexity of component
|
|
36
43
|
offset: 6,
|
|
37
44
|
},
|
|
38
|
-
|
|
45
|
+
positioningState,
|
|
39
46
|
);
|
|
40
47
|
|
|
41
48
|
// This is required so branding applies correctly by default due to portal location, can be overridden with portalContainer prop
|
|
@@ -80,7 +87,6 @@ export function MultiSelectPopover({ children, className, ...props }: MultiSelec
|
|
|
80
87
|
}
|
|
81
88
|
}}
|
|
82
89
|
role="dialog"
|
|
83
|
-
aria-modal="true"
|
|
84
90
|
aria-label="Options list with filter"
|
|
85
91
|
>
|
|
86
92
|
{children}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { focusWithoutScrolling } from '@react-aria/utils';
|
|
1
2
|
import React, { useContext, useCallback, KeyboardEvent } from 'react';
|
|
2
3
|
|
|
3
4
|
import { Button } from '../../../../components/button/index.js';
|
|
@@ -21,12 +22,8 @@ export function MultiSelectSearchbar({
|
|
|
21
22
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
22
23
|
if (e.key === 'ArrowDown') {
|
|
23
24
|
e.preventDefault();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} else {
|
|
27
|
-
const firstItem = listBoxRef.current?.querySelector('[data-key]') as HTMLElement;
|
|
28
|
-
firstItem?.focus();
|
|
29
|
-
}
|
|
25
|
+
const target = selectAllRef.current || (listBoxRef.current?.querySelector('[data-key]') as HTMLElement | null);
|
|
26
|
+
if (target) focusWithoutScrolling(target);
|
|
30
27
|
}
|
|
31
28
|
if (e.key === 'Escape' && filterText.length > 0) {
|
|
32
29
|
e.stopPropagation();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { focusWithoutScrolling } from '@react-aria/utils';
|
|
3
4
|
import { Node } from '@react-types/shared';
|
|
4
|
-
import React, { useRef, useState, memo, createContext } from 'react';
|
|
5
|
-
import { useFilter, useOverlayTrigger } from 'react-aria';
|
|
5
|
+
import React, { ForwardedRef, forwardRef, useRef, useState, memo, createContext } from 'react';
|
|
6
|
+
import { useFilter, useObjectRef, useOverlayTrigger } from 'react-aria';
|
|
6
7
|
import { Item, useListState, useOverlayTriggerState } from 'react-stately';
|
|
7
8
|
|
|
8
9
|
import { MultiSelectDropdown } from './components/multi-select-dropdown/multi-select-dropdown.component.js';
|
|
@@ -30,22 +31,25 @@ export const MultiSelectContext = createContext<MultiSelectContextProps>({
|
|
|
30
31
|
hideSelectAll: false,
|
|
31
32
|
});
|
|
32
33
|
|
|
33
|
-
export function BaseMultiSelect<T extends MultiSelectValue = MultiSelectValue>(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
34
|
+
export function BaseMultiSelect<T extends MultiSelectValue = MultiSelectValue>(
|
|
35
|
+
{
|
|
36
|
+
size = 'medium',
|
|
37
|
+
listBoxProps,
|
|
38
|
+
selectionMode = 'multiple',
|
|
39
|
+
selectedKeys,
|
|
40
|
+
onSelectionChange,
|
|
41
|
+
placeholder = 'Select',
|
|
42
|
+
showSingleSectionTitle = false,
|
|
43
|
+
placement = 'bottom left',
|
|
44
|
+
portalContainer,
|
|
45
|
+
id,
|
|
46
|
+
hideFilter = false,
|
|
47
|
+
hideSelectAll = false,
|
|
48
|
+
width = 'full',
|
|
49
|
+
...props
|
|
50
|
+
}: MultiSelectProps<T>,
|
|
51
|
+
forwardedRef: ForwardedRef<HTMLButtonElement>,
|
|
52
|
+
) {
|
|
49
53
|
const [filterText, setFilterText] = useState('');
|
|
50
54
|
const filter = useFilter({ sensitivity: 'base' });
|
|
51
55
|
|
|
@@ -62,7 +66,7 @@ export function BaseMultiSelect<T extends MultiSelectValue = MultiSelectValue>({
|
|
|
62
66
|
|
|
63
67
|
// refs
|
|
64
68
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
65
|
-
const buttonRef =
|
|
69
|
+
const buttonRef = useObjectRef(forwardedRef);
|
|
66
70
|
const popoverRef = useRef<HTMLDivElement>(null);
|
|
67
71
|
const selectAllRef = useRef<HTMLInputElement>(null);
|
|
68
72
|
const listBoxRef = useRef<HTMLUListElement>(null);
|
|
@@ -70,15 +74,14 @@ export function BaseMultiSelect<T extends MultiSelectValue = MultiSelectValue>({
|
|
|
70
74
|
const overlayState = useOverlayTriggerState({
|
|
71
75
|
onOpenChange: isOpen => {
|
|
72
76
|
if (isOpen) {
|
|
77
|
+
// Focus moves into the popover without scrolling the page (react-aria's own focus helper), so the
|
|
78
|
+
// dropdown does not jump around, and page scroll cannot dismiss it while the user navigates.
|
|
73
79
|
requestAnimationFrame(() => {
|
|
74
|
-
|
|
75
|
-
inputRef.current
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const firstItem = listBoxRef.current?.querySelector('[data-key]') as HTMLElement;
|
|
80
|
-
firstItem?.focus();
|
|
81
|
-
}
|
|
80
|
+
const target =
|
|
81
|
+
(!hideFilter && inputRef.current) ||
|
|
82
|
+
(selectionMode === 'multiple' && !hideSelectAll && selectAllRef.current) ||
|
|
83
|
+
(listBoxRef.current?.querySelector('[data-key]') as HTMLElement | null);
|
|
84
|
+
if (target) focusWithoutScrolling(target);
|
|
82
85
|
});
|
|
83
86
|
}
|
|
84
87
|
if (!isOpen) {
|
|
@@ -124,7 +127,9 @@ export function BaseMultiSelect<T extends MultiSelectValue = MultiSelectValue>({
|
|
|
124
127
|
</MultiSelectContext.Provider>
|
|
125
128
|
);
|
|
126
129
|
}
|
|
127
|
-
|
|
130
|
+
|
|
131
|
+
export const MultiSelect = memo(forwardRef(BaseMultiSelect));
|
|
132
|
+
MultiSelect.displayName = 'MultiSelect';
|
|
128
133
|
|
|
129
134
|
// Exporting react-stately's Item with custom props/naming and Section with custom naming to align with other components
|
|
130
135
|
export const MultiSelectItem = Item as (props: MultiSelectItemProps) => JSX.Element;
|
package/src/components/radio-group/components/radio-group-radio/radio-group-radio.component.tsx
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, {
|
|
4
|
-
import { VisuallyHidden, mergeProps, useFocusRing, useRadio } from 'react-aria';
|
|
3
|
+
import React, { ForwardedRef, forwardRef, useContext } from 'react';
|
|
4
|
+
import { VisuallyHidden, mergeProps, useFocusRing, useObjectRef, useRadio } from 'react-aria';
|
|
5
5
|
|
|
6
6
|
import { RadioGroupContext } from '../../radio-group.component.js';
|
|
7
7
|
|
|
8
8
|
import { styles as radioStyles } from './radio-group-radio.styles.js';
|
|
9
9
|
import { type RadioGroupRadioProps } from './radio-group-radio.types.js';
|
|
10
10
|
|
|
11
|
-
function BaseRadioGroupRadio(
|
|
11
|
+
function BaseRadioGroupRadio(
|
|
12
|
+
{ className, hint, label, ...props }: RadioGroupRadioProps,
|
|
13
|
+
ref: ForwardedRef<HTMLInputElement>,
|
|
14
|
+
) {
|
|
12
15
|
const { state, size, orientation } = useContext(RadioGroupContext);
|
|
13
|
-
const
|
|
14
|
-
const { inputProps, labelProps, isSelected, isDisabled } = useRadio({ ...props, children: label }, state,
|
|
16
|
+
const inputRef = useObjectRef(ref);
|
|
17
|
+
const { inputProps, labelProps, isSelected, isDisabled } = useRadio({ ...props, children: label }, state, inputRef);
|
|
15
18
|
const { isFocusVisible, focusProps } = useFocusRing();
|
|
16
19
|
const styles = radioStyles({ isDisabled, isSelected, isFocusVisible, size, orientation });
|
|
17
20
|
|
|
18
21
|
return (
|
|
19
|
-
<label className={styles.base({ className })}
|
|
22
|
+
<label className={styles.base({ className })} {...labelProps}>
|
|
20
23
|
<VisuallyHidden elementType="span">
|
|
21
|
-
<input {...mergeProps(inputProps, focusProps)} ref={
|
|
24
|
+
<input {...mergeProps(inputProps, focusProps)} ref={inputRef} />
|
|
22
25
|
</VisuallyHidden>
|
|
23
26
|
<span className={styles.selector()} />
|
|
24
27
|
<span className={styles.textWrapper()}>
|
|
@@ -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 { useFocusRing, useRadioGroup } from 'react-aria';
|
|
5
5
|
import { useRadioGroupState } 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';
|
|
@@ -75,17 +76,23 @@ export const RadioGroupContext = createContext<RadioGroupContextState>({
|
|
|
75
76
|
},
|
|
76
77
|
});
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
79
|
+
function BaseRadioGroup(
|
|
80
|
+
{
|
|
81
|
+
className,
|
|
82
|
+
radios,
|
|
83
|
+
label,
|
|
84
|
+
orientation = 'vertical',
|
|
85
|
+
showAmount = 0,
|
|
86
|
+
size = 'medium',
|
|
87
|
+
errorMessage,
|
|
88
|
+
hintMessage,
|
|
89
|
+
...props
|
|
90
|
+
}: RadioGroupProps,
|
|
91
|
+
// `ref.current.focus()` (e.g. react-hook-form focusing a field with a validation error) moves focus
|
|
92
|
+
// to the group's tab stop: the selected radio, or the first enabled radio when nothing is selected.
|
|
93
|
+
ref: ForwardedRef<FocusHandle>,
|
|
94
|
+
) {
|
|
95
|
+
const wrapperRef = useFocusManagerRef(ref, { accept: acceptInputs });
|
|
89
96
|
const breakpoint = useBreakpoint();
|
|
90
97
|
const resolvedOrientation = resolveResponsiveVariant(orientation, breakpoint);
|
|
91
98
|
const resolvedSize = resolveResponsiveVariant(size, breakpoint);
|
|
@@ -96,7 +103,7 @@ export function RadioGroup({
|
|
|
96
103
|
);
|
|
97
104
|
const { isFocusVisible, focusProps } = useFocusRing();
|
|
98
105
|
const [hiddenOptions, setHiddenOptions] = useState<boolean>(showAmount > 0);
|
|
99
|
-
const firstNewRadioRef = useRef<
|
|
106
|
+
const firstNewRadioRef = useRef<HTMLInputElement>(null);
|
|
100
107
|
const revealAmount = radios && radios.length - showAmount;
|
|
101
108
|
const styles = radioGroupStyles({ orientation: resolvedOrientation, isFocusVisible });
|
|
102
109
|
const panelId = useId();
|
|
@@ -119,7 +126,7 @@ export function RadioGroup({
|
|
|
119
126
|
<Label {...labelProps}>{label}</Label>
|
|
120
127
|
{hintMessage && <Hint {...descriptionProps}>{hintMessage}</Hint>}
|
|
121
128
|
{errorMessage && state.isInvalid && <ErrorMessage {...errorMessageProps} message={errorMessage} />}
|
|
122
|
-
<div className={styles.radioWrapper()} id={panelId}>
|
|
129
|
+
<div className={styles.radioWrapper()} id={panelId} ref={wrapperRef}>
|
|
123
130
|
<RadioGroupContext.Provider value={{ state, orientation: resolvedOrientation, size: resolvedSize }}>
|
|
124
131
|
{childrenToRender}
|
|
125
132
|
</RadioGroupContext.Provider>
|
|
@@ -142,3 +149,6 @@ export function RadioGroup({
|
|
|
142
149
|
</div>
|
|
143
150
|
);
|
|
144
151
|
}
|
|
152
|
+
|
|
153
|
+
export const RadioGroup = forwardRef(BaseRadioGroup);
|
|
154
|
+
RadioGroup.displayName = 'RadioGroup';
|
package/src/components/selector/components/selector-button-group/selector-button-group.component.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { createContext, useCallback, useMemo, useState } from 'react';
|
|
3
|
+
import React, { ForwardedRef, createContext, forwardRef, useCallback, useMemo, useState } from 'react';
|
|
4
4
|
import { useField } from 'react-aria';
|
|
5
5
|
|
|
6
6
|
import { useBreakpoint } from '../../../../hook/breakpoints.hook.js';
|
|
7
|
+
import { FocusHandle, 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
|
|
|
@@ -17,18 +18,23 @@ export const SelectorButtonContext = createContext<SelectorButtonGroupContextSta
|
|
|
17
18
|
isDisabled: undefined,
|
|
18
19
|
});
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
function BaseSelectorButtonGroup(
|
|
22
|
+
{
|
|
23
|
+
className,
|
|
24
|
+
children,
|
|
25
|
+
label,
|
|
26
|
+
orientation = 'vertical',
|
|
27
|
+
errorMessage,
|
|
28
|
+
description,
|
|
29
|
+
value = '',
|
|
30
|
+
onChange,
|
|
31
|
+
isDisabled,
|
|
32
|
+
...props
|
|
33
|
+
}: SelectorButtonGroupProps,
|
|
34
|
+
// `ref.current.focus()` moves focus to the first tabbable option button.
|
|
35
|
+
ref: ForwardedRef<FocusHandle>,
|
|
36
|
+
) {
|
|
37
|
+
const wrapperRef = useFocusManagerRef(ref);
|
|
32
38
|
const isControlled = onChange !== undefined;
|
|
33
39
|
const onChangeCallback = onChange as ((value: string) => void) | undefined;
|
|
34
40
|
const [selected, setSelected] = useState(value);
|
|
@@ -69,9 +75,12 @@ export function SelectorButtonGroup({
|
|
|
69
75
|
{label && <Label {...labelProps}>{label}</Label>}
|
|
70
76
|
{description && <Hint {...descriptionProps}>{description}</Hint>}
|
|
71
77
|
{errorMessage && <ErrorMessage {...errorMessageProps} message={errorMessage} />}
|
|
72
|
-
<div className={styles({ className, orientation: resolvedOrientation })} {...fieldProps}>
|
|
78
|
+
<div className={styles({ className, orientation: resolvedOrientation })} {...fieldProps} ref={wrapperRef}>
|
|
73
79
|
<SelectorButtonContext.Provider value={state}>{children}</SelectorButtonContext.Provider>
|
|
74
80
|
</div>
|
|
75
81
|
</>
|
|
76
82
|
);
|
|
77
83
|
}
|
|
84
|
+
|
|
85
|
+
export const SelectorButtonGroup = forwardRef(BaseSelectorButtonGroup);
|
|
86
|
+
SelectorButtonGroup.displayName = 'SelectorButtonGroup';
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { createContext } from 'react';
|
|
3
|
+
import React, { ForwardedRef, createContext, forwardRef } from 'react';
|
|
4
4
|
import { useCheckboxGroup } from 'react-aria';
|
|
5
5
|
import { useCheckboxGroupState } from 'react-stately';
|
|
6
6
|
|
|
7
7
|
import { FUNCTION_NOT_IMPLEMENTED } from '../../../../constants/message.js';
|
|
8
|
+
import { FocusHandle, useFocusManagerRef } from '../../../../hook/focus-manager-ref.hook.js';
|
|
8
9
|
import { ErrorMessage, Hint, Label } from '../../../index.js';
|
|
9
10
|
|
|
10
11
|
import { styles } from './selector-checkbox-group.styles.js';
|
|
@@ -74,8 +75,13 @@ export const SelectorCheckboxGroupContext = createContext<SelectorCheckboxGroupC
|
|
|
74
75
|
},
|
|
75
76
|
});
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
function BaseSelectorCheckboxGroup(
|
|
79
|
+
props: SelectorCheckboxGroupProps,
|
|
80
|
+
// `ref.current.focus()` moves focus to the first tabbable checkbox input.
|
|
81
|
+
ref: ForwardedRef<FocusHandle>,
|
|
82
|
+
) {
|
|
78
83
|
const { children, label, description, errorMessage } = props;
|
|
84
|
+
const wrapperRef = useFocusManagerRef(ref);
|
|
79
85
|
const state = useCheckboxGroupState(props);
|
|
80
86
|
const { groupProps, labelProps, descriptionProps, errorMessageProps } = useCheckboxGroup(props, state);
|
|
81
87
|
|
|
@@ -84,9 +90,12 @@ export function SelectorCheckboxGroup(props: SelectorCheckboxGroupProps) {
|
|
|
84
90
|
{label && <Label {...labelProps}>{label}</Label>}
|
|
85
91
|
{description && <Hint {...descriptionProps}>{description}</Hint>}
|
|
86
92
|
{errorMessage && state.isInvalid && <ErrorMessage {...errorMessageProps} message={errorMessage} />}
|
|
87
|
-
<div {...groupProps} className={styles({ className: groupProps.className })}>
|
|
93
|
+
<div {...groupProps} className={styles({ className: groupProps.className })} ref={wrapperRef}>
|
|
88
94
|
<SelectorCheckboxGroupContext.Provider value={state}>{children}</SelectorCheckboxGroupContext.Provider>
|
|
89
95
|
</div>
|
|
90
96
|
</>
|
|
91
97
|
);
|
|
92
98
|
}
|
|
99
|
+
|
|
100
|
+
export const SelectorCheckboxGroup = forwardRef(BaseSelectorCheckboxGroup);
|
|
101
|
+
SelectorCheckboxGroup.displayName = 'SelectorCheckboxGroup';
|
package/src/components/selector/components/selector-radio-group/selector-radio-group.component.tsx
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { createContext } from 'react';
|
|
3
|
+
import React, { ForwardedRef, createContext, forwardRef } from 'react';
|
|
4
4
|
import { useRadioGroup } from 'react-aria';
|
|
5
5
|
import { useRadioGroupState } 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, useFocusManagerRef } from '../../../../hook/focus-manager-ref.hook.js';
|
|
9
10
|
import { resolveResponsiveVariant } from '../../../../utils/breakpoint.util.js';
|
|
10
11
|
import { ErrorMessage, Hint, Label } from '../../../index.js';
|
|
11
12
|
|
|
@@ -73,15 +74,21 @@ export const SelectorRadioGroupContext = createContext<SelectorRadioGroupContext
|
|
|
73
74
|
},
|
|
74
75
|
});
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
function BaseSelectorRadioGroup(
|
|
78
|
+
{
|
|
79
|
+
className,
|
|
80
|
+
children,
|
|
81
|
+
label,
|
|
82
|
+
orientation = 'vertical',
|
|
83
|
+
errorMessage,
|
|
84
|
+
description,
|
|
85
|
+
...props
|
|
86
|
+
}: SelectorRadioGroupProps,
|
|
87
|
+
// `ref.current.focus()` moves focus to the group's tab stop: the selected radio, or the first
|
|
88
|
+
// enabled radio when nothing is selected.
|
|
89
|
+
ref: ForwardedRef<FocusHandle>,
|
|
90
|
+
) {
|
|
91
|
+
const wrapperRef = useFocusManagerRef(ref);
|
|
85
92
|
const breakpoint = useBreakpoint();
|
|
86
93
|
const resolvedOrientation = resolveResponsiveVariant(orientation, breakpoint);
|
|
87
94
|
const state = useRadioGroupState({ ...props, errorMessage, label, orientation: resolvedOrientation });
|
|
@@ -95,7 +102,7 @@ export function SelectorRadioGroup({
|
|
|
95
102
|
{label && <Label {...labelProps}>{label}</Label>}
|
|
96
103
|
{description && <Hint {...descriptionProps}>{description}</Hint>}
|
|
97
104
|
{errorMessage && state.isInvalid && <ErrorMessage {...errorMessageProps} message={errorMessage} />}
|
|
98
|
-
<div className={styles({ className, orientation: resolvedOrientation })} {...radioGroupProps}>
|
|
105
|
+
<div className={styles({ className, orientation: resolvedOrientation })} {...radioGroupProps} ref={wrapperRef}>
|
|
99
106
|
<SelectorRadioGroupContext.Provider value={{ state, orientation: resolvedOrientation }}>
|
|
100
107
|
{children}
|
|
101
108
|
</SelectorRadioGroupContext.Provider>
|
|
@@ -103,3 +110,6 @@ export function SelectorRadioGroup({
|
|
|
103
110
|
</>
|
|
104
111
|
);
|
|
105
112
|
}
|
|
113
|
+
|
|
114
|
+
export const SelectorRadioGroup = forwardRef(BaseSelectorRadioGroup);
|
|
115
|
+
SelectorRadioGroup.displayName = 'SelectorRadioGroup';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
3
|
+
import React, { ForwardedRef, forwardRef } from 'react';
|
|
4
|
+
|
|
5
|
+
import { FocusHandle } from '../../hook/focus-manager-ref.hook.js';
|
|
4
6
|
|
|
5
7
|
import {
|
|
6
8
|
SelectorButtonGroup,
|
|
@@ -11,17 +13,26 @@ import {
|
|
|
11
13
|
import { type SelectorProps } from './selector.types.js';
|
|
12
14
|
|
|
13
15
|
// TODO: react-aria doesn't consider the click as focus. so the focusRing will appeared just with the tab.
|
|
14
|
-
|
|
16
|
+
function BaseSelector(
|
|
17
|
+
props: SelectorProps,
|
|
18
|
+
// `ref.current.focus()` (e.g. react-hook-form focusing a field with a validation error) moves
|
|
19
|
+
// focus to the first tabbable option (radio/checkbox input or button).
|
|
20
|
+
// The `link` type is navigation, not a form field, and does not receive the ref.
|
|
21
|
+
ref: ForwardedRef<FocusHandle>,
|
|
22
|
+
) {
|
|
15
23
|
if (props.type === 'checkbox') {
|
|
16
|
-
return <SelectorCheckboxGroup {...props} />;
|
|
24
|
+
return <SelectorCheckboxGroup {...props} ref={ref} />;
|
|
17
25
|
}
|
|
18
26
|
if (props.type === 'radio') {
|
|
19
|
-
return <SelectorRadioGroup {...props} />;
|
|
27
|
+
return <SelectorRadioGroup {...props} ref={ref} />;
|
|
20
28
|
}
|
|
21
29
|
if (props.type === 'button') {
|
|
22
|
-
return <SelectorButtonGroup {...props} />;
|
|
30
|
+
return <SelectorButtonGroup {...props} ref={ref} />;
|
|
23
31
|
}
|
|
24
32
|
if (props.type === 'link') {
|
|
25
33
|
return <SelectorLinkGroup {...props} />;
|
|
26
34
|
}
|
|
27
35
|
}
|
|
36
|
+
|
|
37
|
+
export const Selector = forwardRef(BaseSelector);
|
|
38
|
+
Selector.displayName = 'Selector';
|