@lumx/react 3.18.1 → 3.18.2-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_internal/index.js +236 -0
- package/_internal/index.js.map +1 -0
- package/index.d.ts +13 -8
- package/index.js +219 -226
- package/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/autocomplete/Autocomplete.tsx +5 -4
- package/src/components/autocomplete/AutocompleteMultiple.tsx +5 -3
- package/src/components/button/Button.stories.tsx +1 -0
- package/src/components/button/Button.test.tsx +41 -2
- package/src/components/button/ButtonRoot.tsx +10 -11
- package/src/components/checkbox/Checkbox.stories.tsx +13 -2
- package/src/components/checkbox/Checkbox.test.tsx +29 -0
- package/src/components/checkbox/Checkbox.tsx +8 -7
- package/src/components/chip/Chip.stories.tsx +17 -0
- package/src/components/chip/Chip.test.tsx +44 -0
- package/src/components/chip/Chip.tsx +10 -9
- package/src/components/date-picker/DatePickerField.stories.tsx +18 -0
- package/src/components/date-picker/DatePickerField.tsx +4 -4
- package/src/components/link/Link.stories.tsx +4 -1
- package/src/components/link/Link.test.tsx +45 -6
- package/src/components/link/Link.tsx +7 -6
- package/src/components/list/ListItem.stories.tsx +14 -48
- package/src/components/list/ListItem.test.tsx +78 -7
- package/src/components/list/ListItem.tsx +11 -9
- package/src/components/progress-tracker/ProgressTrackerStep.tsx +7 -7
- package/src/components/radio-button/RadioButton.stories.tsx +32 -0
- package/src/components/radio-button/RadioButton.test.tsx +30 -0
- package/src/components/radio-button/RadioButton.tsx +8 -7
- package/src/components/slider/Slider.tsx +6 -7
- package/src/components/switch/Switch.stories.tsx +11 -1
- package/src/components/switch/Switch.test.tsx +30 -0
- package/src/components/switch/Switch.tsx +8 -7
- package/src/components/table/TableRow.tsx +8 -6
- package/src/components/tabs/Tab.tsx +12 -9
- package/src/components/text-field/TextField.stories.tsx +22 -0
- package/src/components/text-field/TextField.test.tsx +56 -0
- package/src/components/text-field/TextField.tsx +12 -10
- package/src/utils/disabled/DisabledStateContext.tsx +29 -0
- package/src/utils/disabled/DisabledStateProvider.stories.tsx +88 -0
- package/src/utils/disabled/index.ts +2 -0
- package/src/utils/disabled/useDisableStateProps.tsx +37 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/type/HasAriaDisabled.ts +6 -0
- package/utils/index.d.ts +20 -1
- package/utils/index.js +1 -134
- package/utils/index.js.map +1 -1
|
@@ -4,6 +4,7 @@ import { ColorPalette, ColorVariant, Icon, Typography } from '@lumx/react';
|
|
|
4
4
|
import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
|
|
5
5
|
import { getByClassName, queryAllByClassName, queryByClassName } from '@lumx/react/testing/utils/queries';
|
|
6
6
|
import { render, screen } from '@testing-library/react';
|
|
7
|
+
import userEvent from '@testing-library/user-event';
|
|
7
8
|
import { mdiCheck, mdiPlus } from '@lumx/icons';
|
|
8
9
|
import { Link, LinkProps } from './Link';
|
|
9
10
|
|
|
@@ -55,12 +56,6 @@ describe(`<${Link.displayName}>`, () => {
|
|
|
55
56
|
expect(link).toBe(screen.queryByRole('button', { name }));
|
|
56
57
|
});
|
|
57
58
|
|
|
58
|
-
it('should render disabled link as button', () => {
|
|
59
|
-
const name = 'Link';
|
|
60
|
-
const { link } = setup({ href: 'https://google.com', isDisabled: true, children: name });
|
|
61
|
-
expect(link).toBe(screen.queryByRole('button', { name }));
|
|
62
|
-
});
|
|
63
|
-
|
|
64
59
|
it('should render with icons', () => {
|
|
65
60
|
const { link } = setup({
|
|
66
61
|
leftIcon: mdiCheck,
|
|
@@ -78,6 +73,50 @@ describe(`<${Link.displayName}>`, () => {
|
|
|
78
73
|
});
|
|
79
74
|
});
|
|
80
75
|
|
|
76
|
+
describe('Disabled state', () => {
|
|
77
|
+
it('should render disabled button', async () => {
|
|
78
|
+
const onClick = jest.fn();
|
|
79
|
+
const { link } = setup({ children: 'Label', isDisabled: true, onClick });
|
|
80
|
+
expect(link).toHaveAttribute('disabled');
|
|
81
|
+
await userEvent.click(link);
|
|
82
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should render disabled link', async () => {
|
|
86
|
+
const onClick = jest.fn();
|
|
87
|
+
const { link } = setup({ children: 'Label', isDisabled: true, href: 'https://example.com', onClick });
|
|
88
|
+
// Disabled link do not exist so we fallback to a button
|
|
89
|
+
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
|
90
|
+
expect(link).toHaveAttribute('disabled');
|
|
91
|
+
await userEvent.click(link);
|
|
92
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should render aria-disabled button', async () => {
|
|
96
|
+
const onClick = jest.fn();
|
|
97
|
+
const { link } = setup({ children: 'Label', 'aria-disabled': true, onClick });
|
|
98
|
+
expect(link).toHaveAttribute('aria-disabled');
|
|
99
|
+
await userEvent.click(link);
|
|
100
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should render aria-disabled link', async () => {
|
|
104
|
+
const onClick = jest.fn();
|
|
105
|
+
const { link } = setup({
|
|
106
|
+
children: 'Label',
|
|
107
|
+
'aria-disabled': true,
|
|
108
|
+
href: 'https://example.com',
|
|
109
|
+
onClick,
|
|
110
|
+
});
|
|
111
|
+
expect(link).toHaveAccessibleName('Label');
|
|
112
|
+
// Disabled link do not exist so we fallback to a button
|
|
113
|
+
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
|
114
|
+
expect(link).toHaveAttribute('aria-disabled', 'true');
|
|
115
|
+
await userEvent.click(link);
|
|
116
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
81
120
|
// Common tests suite.
|
|
82
121
|
commonTestsSuiteRTL(setup, {
|
|
83
122
|
baseClassName: CLASSNAME,
|
|
@@ -12,13 +12,15 @@ import {
|
|
|
12
12
|
} from '@lumx/react/utils/className';
|
|
13
13
|
import { forwardRef } from '@lumx/react/utils/react/forwardRef';
|
|
14
14
|
import { wrapChildrenIconWithSpaces } from '@lumx/react/utils/react/wrapChildrenIconWithSpaces';
|
|
15
|
+
import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps';
|
|
16
|
+
import { HasAriaDisabled } from '@lumx/react/utils/type/HasAriaDisabled';
|
|
15
17
|
|
|
16
18
|
type HTMLAnchorProps = React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Defines the props of the component.
|
|
20
22
|
*/
|
|
21
|
-
export interface LinkProps extends GenericProps {
|
|
23
|
+
export interface LinkProps extends GenericProps, HasAriaDisabled {
|
|
22
24
|
/** Color variant. */
|
|
23
25
|
color?: ColorWithVariants;
|
|
24
26
|
/** Lightened or darkened variant of the selected icon color. */
|
|
@@ -65,13 +67,12 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
|
|
|
65
67
|
* @return React element.
|
|
66
68
|
*/
|
|
67
69
|
export const Link = forwardRef<LinkProps, HTMLAnchorElement | HTMLButtonElement>((props, ref) => {
|
|
70
|
+
const { isAnyDisabled, disabledStateProps, otherProps } = useDisableStateProps(props);
|
|
68
71
|
const {
|
|
69
72
|
children,
|
|
70
73
|
className,
|
|
71
74
|
color: propColor,
|
|
72
75
|
colorVariant: propColorVariant,
|
|
73
|
-
disabled,
|
|
74
|
-
isDisabled = disabled,
|
|
75
76
|
href,
|
|
76
77
|
leftIcon,
|
|
77
78
|
linkAs,
|
|
@@ -79,15 +80,15 @@ export const Link = forwardRef<LinkProps, HTMLAnchorElement | HTMLButtonElement>
|
|
|
79
80
|
target,
|
|
80
81
|
typography,
|
|
81
82
|
...forwardedProps
|
|
82
|
-
} =
|
|
83
|
+
} = otherProps;
|
|
83
84
|
const [color, colorVariant] = resolveColorWithVariants(propColor, propColorVariant);
|
|
84
85
|
|
|
85
86
|
const isLink = linkAs || href;
|
|
86
|
-
const Component = isLink && !
|
|
87
|
+
const Component = isLink && !isAnyDisabled ? linkAs || 'a' : 'button';
|
|
87
88
|
const baseProps: React.ComponentProps<typeof Component> = {};
|
|
88
89
|
if (Component === 'button') {
|
|
89
90
|
baseProps.type = 'button';
|
|
90
|
-
baseProps
|
|
91
|
+
Object.assign(baseProps, disabledStateProps);
|
|
91
92
|
} else if (isLink) {
|
|
92
93
|
baseProps.href = href;
|
|
93
94
|
baseProps.target = target;
|
|
@@ -4,6 +4,7 @@ import { withWrapper } from '@lumx/react/stories/decorators/withWrapper';
|
|
|
4
4
|
import { CustomLink } from '@lumx/react/stories/utils/CustomLink';
|
|
5
5
|
import { withCombinations } from '@lumx/react/stories/decorators/withCombinations';
|
|
6
6
|
import { getSelectArgType } from '@lumx/react/stories/controls/selectArgType';
|
|
7
|
+
import { isEqual } from '@lumx/react/utils/object/isEqual';
|
|
7
8
|
import { ListItem } from './ListItem';
|
|
8
9
|
|
|
9
10
|
const sizes: ListItemSize[] = [Size.tiny, Size.regular, Size.big];
|
|
@@ -21,53 +22,10 @@ export default {
|
|
|
21
22
|
/**
|
|
22
23
|
* Default list item with text
|
|
23
24
|
*/
|
|
24
|
-
export const
|
|
25
|
+
export const Default = {
|
|
25
26
|
args: { children: 'List item' },
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
-
* Button list item (onClick)
|
|
30
|
-
*/
|
|
31
|
-
export const Button = {
|
|
32
|
-
args: {
|
|
33
|
-
children: 'List item button',
|
|
34
|
-
},
|
|
35
|
-
argTypes: {
|
|
36
|
-
onItemSelected: { action: true },
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Disabled button
|
|
42
|
-
*/
|
|
43
|
-
export const ButtonDisabled = {
|
|
44
|
-
...Button,
|
|
45
|
-
args: {
|
|
46
|
-
...Button.args,
|
|
47
|
-
isDisabled: true,
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Link list item (href)
|
|
53
|
-
*/
|
|
54
|
-
export const Link = {
|
|
55
|
-
args: {
|
|
56
|
-
linkProps: { href: '#' },
|
|
57
|
-
children: 'List item link',
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Disabled link
|
|
63
|
-
*/
|
|
64
|
-
export const LinkDisabled = {
|
|
65
|
-
args: {
|
|
66
|
-
...Link.args,
|
|
67
|
-
isDisabled: true,
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
|
|
71
29
|
/**
|
|
72
30
|
* Inject a custom link component
|
|
73
31
|
*/
|
|
@@ -79,22 +37,30 @@ export const CustomLink_ = {
|
|
|
79
37
|
};
|
|
80
38
|
|
|
81
39
|
/**
|
|
82
|
-
* Combination of size and states
|
|
40
|
+
* Combination of all states (size, clickable and non-clickable states)
|
|
83
41
|
*/
|
|
84
|
-
export const
|
|
85
|
-
|
|
42
|
+
export const AllStates = {
|
|
43
|
+
args: { children: 'List item' },
|
|
86
44
|
decorators: [
|
|
87
|
-
withWrapper({}, List),
|
|
88
45
|
withCombinations({
|
|
89
46
|
combinations: {
|
|
90
47
|
rows: { key: 'size', options: sizes },
|
|
91
48
|
cols: {
|
|
92
49
|
Default: {},
|
|
93
50
|
Disabled: { isDisabled: true },
|
|
51
|
+
'ARIA Disabled': { 'aria-disabled': true },
|
|
94
52
|
Selected: { isSelected: true },
|
|
95
53
|
Highlighted: { isHighlighted: true },
|
|
96
54
|
},
|
|
55
|
+
sections: {
|
|
56
|
+
Default: {},
|
|
57
|
+
'As button': { onItemSelected: () => {} },
|
|
58
|
+
'As link': { linkProps: { href: '#' } },
|
|
59
|
+
},
|
|
97
60
|
},
|
|
61
|
+
// Only keep size variants for non clickable list items
|
|
62
|
+
excludeCombination: (props) =>
|
|
63
|
+
!props.onItemSelected && !props.linkProps?.href && !isEqual(Object.keys(props), ['size', 'children']),
|
|
98
64
|
}),
|
|
99
65
|
],
|
|
100
66
|
};
|
|
@@ -1,20 +1,91 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
|
|
3
|
+
import { commonTestsSuiteRTL, SetupRenderOptions } from '@lumx/react/testing/utils';
|
|
4
|
+
import { render, screen } from '@testing-library/react';
|
|
5
|
+
import { getByClassName, queryByClassName } from '@lumx/react/testing/utils/queries';
|
|
6
|
+
import userEvent from '@testing-library/user-event';
|
|
4
7
|
|
|
5
|
-
import { render } from '@testing-library/react';
|
|
6
|
-
import { queryByClassName } from '@lumx/react/testing/utils/queries';
|
|
7
8
|
import { ListItem, ListItemProps } from './ListItem';
|
|
8
9
|
|
|
9
10
|
const CLASSNAME = ListItem.className as string;
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Mounts the component and returns common DOM elements / data needed in multiple tests further down.
|
|
14
|
+
*/
|
|
15
|
+
const setup = (props: Partial<ListItemProps> = {}, { wrapper }: SetupRenderOptions = {}) => {
|
|
16
|
+
render(<ListItem {...(props as any)} />, { wrapper });
|
|
17
|
+
const listItem = getByClassName(document.body, CLASSNAME);
|
|
18
|
+
const link = queryByClassName(listItem, `${CLASSNAME}__link`);
|
|
19
|
+
return { props, listItem, link };
|
|
15
20
|
};
|
|
16
21
|
|
|
17
22
|
describe(`<${ListItem.displayName}>`, () => {
|
|
23
|
+
describe('Props', () => {
|
|
24
|
+
it('should render default', () => {
|
|
25
|
+
const { listItem, link } = setup({ children: 'Label' });
|
|
26
|
+
expect(listItem).toBeInTheDocument();
|
|
27
|
+
expect(link).not.toBeInTheDocument();
|
|
28
|
+
expect(listItem).toHaveTextContent('Label');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should render as a button', () => {
|
|
32
|
+
setup({ children: 'Label', onItemSelected: jest.fn() });
|
|
33
|
+
expect(screen.getByRole('button', { name: 'Label' })).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should render as a link', () => {
|
|
37
|
+
setup({ children: 'Label', linkProps: { href: '#' } });
|
|
38
|
+
expect(screen.getByRole('link', { name: 'Label' })).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('Disabled state', () => {
|
|
43
|
+
it('should render disabled list item button', async () => {
|
|
44
|
+
const onItemSelected = jest.fn();
|
|
45
|
+
const { link } = setup({ children: 'Label', isDisabled: true, onItemSelected });
|
|
46
|
+
expect(link).toHaveAttribute('aria-disabled', 'true');
|
|
47
|
+
// The `renderLink` util removes the onClick handler but `user-event` will also not fire events on disabled elements.
|
|
48
|
+
if (link) await userEvent.click(link);
|
|
49
|
+
expect(onItemSelected).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should render disabled list item link', async () => {
|
|
53
|
+
const onItemSelected = jest.fn();
|
|
54
|
+
const { link } = setup({
|
|
55
|
+
children: 'Label',
|
|
56
|
+
isDisabled: true,
|
|
57
|
+
linkProps: { href: 'https://example.com' },
|
|
58
|
+
onItemSelected,
|
|
59
|
+
});
|
|
60
|
+
expect(link).not.toHaveAttribute('href');
|
|
61
|
+
expect(link).toHaveAttribute('aria-disabled', 'true');
|
|
62
|
+
if (link) await userEvent.click(link);
|
|
63
|
+
expect(onItemSelected).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should render aria-disabled list item button', async () => {
|
|
67
|
+
const onItemSelected = jest.fn();
|
|
68
|
+
const { link } = setup({ children: 'Label', 'aria-disabled': true, onItemSelected });
|
|
69
|
+
expect(link).toHaveAttribute('aria-disabled', 'true');
|
|
70
|
+
if (link) await userEvent.click(link);
|
|
71
|
+
expect(onItemSelected).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should render aria-disabled list item link', async () => {
|
|
75
|
+
const onItemSelected = jest.fn();
|
|
76
|
+
const { link } = setup({
|
|
77
|
+
children: 'Label',
|
|
78
|
+
'aria-disabled': true,
|
|
79
|
+
linkProps: { href: 'https://example.com' },
|
|
80
|
+
onItemSelected,
|
|
81
|
+
});
|
|
82
|
+
expect(link).not.toHaveAttribute('href');
|
|
83
|
+
expect(link).toHaveAttribute('aria-disabled', 'true');
|
|
84
|
+
if (link) await userEvent.click(link);
|
|
85
|
+
expect(onItemSelected).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
18
89
|
// Common tests suite.
|
|
19
90
|
commonTestsSuiteRTL(setup, {
|
|
20
91
|
baseClassName: CLASSNAME,
|
|
@@ -9,13 +9,15 @@ import { onEnterPressed, onButtonPressed } from '@lumx/react/utils/browser/event
|
|
|
9
9
|
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
|
|
10
10
|
import { renderLink } from '@lumx/react/utils/react/renderLink';
|
|
11
11
|
import { forwardRef } from '@lumx/react/utils/react/forwardRef';
|
|
12
|
+
import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps';
|
|
13
|
+
import { HasAriaDisabled } from '@lumx/react/utils/type/HasAriaDisabled';
|
|
12
14
|
|
|
13
15
|
export type ListItemSize = Extract<Size, 'tiny' | 'regular' | 'big' | 'huge'>;
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Defines the props of the component.
|
|
17
19
|
*/
|
|
18
|
-
export interface ListItemProps extends GenericProps {
|
|
20
|
+
export interface ListItemProps extends GenericProps, HasAriaDisabled {
|
|
19
21
|
/** A component to be rendered after the content. */
|
|
20
22
|
after?: ReactNode;
|
|
21
23
|
/** A component to be rendered before the content. */
|
|
@@ -76,6 +78,7 @@ export function isClickable({ linkProps, onItemSelected }: Partial<ListItemProps
|
|
|
76
78
|
* @return React element.
|
|
77
79
|
*/
|
|
78
80
|
export const ListItem = forwardRef<ListItemProps, HTMLLIElement>((props, ref) => {
|
|
81
|
+
const { isAnyDisabled, disabledStateProps, otherProps } = useDisableStateProps(props);
|
|
79
82
|
const {
|
|
80
83
|
after,
|
|
81
84
|
before,
|
|
@@ -83,14 +86,13 @@ export const ListItem = forwardRef<ListItemProps, HTMLLIElement>((props, ref) =>
|
|
|
83
86
|
className,
|
|
84
87
|
isHighlighted,
|
|
85
88
|
isSelected,
|
|
86
|
-
isDisabled,
|
|
87
89
|
linkAs,
|
|
88
90
|
linkProps = {},
|
|
89
91
|
linkRef,
|
|
90
92
|
onItemSelected,
|
|
91
93
|
size = DEFAULT_PROPS.size,
|
|
92
94
|
...forwardedProps
|
|
93
|
-
} =
|
|
95
|
+
} = otherProps;
|
|
94
96
|
|
|
95
97
|
const role = linkAs || linkProps.href ? 'link' : 'button';
|
|
96
98
|
const onKeyDown = useMemo(() => {
|
|
@@ -124,21 +126,21 @@ export const ListItem = forwardRef<ListItemProps, HTMLLIElement>((props, ref) =>
|
|
|
124
126
|
renderLink(
|
|
125
127
|
{
|
|
126
128
|
linkAs,
|
|
127
|
-
tabIndex: !
|
|
129
|
+
tabIndex: !disabledStateProps.disabled ? 0 : undefined,
|
|
128
130
|
role,
|
|
129
|
-
'aria-disabled':
|
|
131
|
+
'aria-disabled': isAnyDisabled,
|
|
130
132
|
...linkProps,
|
|
131
|
-
href:
|
|
133
|
+
href: isAnyDisabled ? undefined : linkProps.href,
|
|
132
134
|
className: classNames(
|
|
133
135
|
handleBasicClasses({
|
|
134
136
|
prefix: `${CLASSNAME}__link`,
|
|
135
137
|
isHighlighted,
|
|
136
138
|
isSelected,
|
|
137
|
-
isDisabled,
|
|
139
|
+
isDisabled: isAnyDisabled,
|
|
138
140
|
}),
|
|
139
141
|
),
|
|
140
|
-
onClick:
|
|
141
|
-
onKeyDown,
|
|
142
|
+
onClick: isAnyDisabled ? undefined : onItemSelected,
|
|
143
|
+
onKeyDown: isAnyDisabled ? undefined : onKeyDown,
|
|
142
144
|
ref: linkRef,
|
|
143
145
|
},
|
|
144
146
|
content,
|
|
@@ -8,6 +8,7 @@ import { GenericProps } from '@lumx/react/utils/type';
|
|
|
8
8
|
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
|
|
9
9
|
import { forwardRef } from '@lumx/react/utils/react/forwardRef';
|
|
10
10
|
|
|
11
|
+
import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps';
|
|
11
12
|
import { useTabProviderContext } from '../tabs/state';
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -57,30 +58,29 @@ const DEFAULT_PROPS: Partial<ProgressTrackerStepProps> = {};
|
|
|
57
58
|
* @return React element.
|
|
58
59
|
*/
|
|
59
60
|
export const ProgressTrackerStep = forwardRef<ProgressTrackerStepProps, HTMLButtonElement>((props, ref) => {
|
|
61
|
+
const { isAnyDisabled, otherProps } = useDisableStateProps(props);
|
|
60
62
|
const {
|
|
61
63
|
className,
|
|
62
|
-
disabled,
|
|
63
64
|
hasError,
|
|
64
65
|
helper,
|
|
65
66
|
id,
|
|
66
67
|
isActive: propIsActive,
|
|
67
68
|
isComplete,
|
|
68
|
-
isDisabled = disabled,
|
|
69
69
|
label,
|
|
70
70
|
onFocus,
|
|
71
71
|
onKeyPress,
|
|
72
72
|
tabIndex = -1,
|
|
73
73
|
...forwardedProps
|
|
74
|
-
} =
|
|
74
|
+
} = otherProps;
|
|
75
75
|
const state = useTabProviderContext('tab', id);
|
|
76
76
|
const isActive = propIsActive || state?.isActive;
|
|
77
77
|
|
|
78
78
|
const changeToCurrentTab = useCallback(() => {
|
|
79
|
-
if (
|
|
79
|
+
if (isAnyDisabled) {
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
82
|
state?.changeToTab();
|
|
83
|
-
}, [
|
|
83
|
+
}, [isAnyDisabled, state]);
|
|
84
84
|
|
|
85
85
|
const handleFocus: FocusEventHandler = useCallback(
|
|
86
86
|
(event) => {
|
|
@@ -127,7 +127,7 @@ export const ProgressTrackerStep = forwardRef<ProgressTrackerStepProps, HTMLButt
|
|
|
127
127
|
prefix: CLASSNAME,
|
|
128
128
|
hasError,
|
|
129
129
|
isActive,
|
|
130
|
-
isClickable: state && !
|
|
130
|
+
isClickable: state && !isAnyDisabled,
|
|
131
131
|
isComplete,
|
|
132
132
|
}),
|
|
133
133
|
)}
|
|
@@ -136,7 +136,7 @@ export const ProgressTrackerStep = forwardRef<ProgressTrackerStepProps, HTMLButt
|
|
|
136
136
|
onFocus={handleFocus}
|
|
137
137
|
role="tab"
|
|
138
138
|
tabIndex={isActive ? 0 : tabIndex}
|
|
139
|
-
aria-disabled={
|
|
139
|
+
aria-disabled={isAnyDisabled}
|
|
140
140
|
aria-selected={isActive}
|
|
141
141
|
aria-controls={state?.tabPanelId}
|
|
142
142
|
>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { RadioButton } from '@lumx/react';
|
|
2
2
|
import { withValueOnChange } from '@lumx/react/stories/decorators/withValueOnChange';
|
|
3
3
|
import { loremIpsum } from '@lumx/react/stories/utils/lorem';
|
|
4
|
+
import { withCombinations } from '@lumx/react/stories/decorators/withCombinations';
|
|
5
|
+
import uniqueId from 'lodash/uniqueId';
|
|
4
6
|
|
|
5
7
|
export default {
|
|
6
8
|
title: 'LumX components/radio-button/Radio button',
|
|
@@ -37,3 +39,33 @@ export const LabelAndHelper = {
|
|
|
37
39
|
helper: loremIpsum('tiny'),
|
|
38
40
|
},
|
|
39
41
|
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* All state combinations
|
|
45
|
+
*/
|
|
46
|
+
export const AllStates = {
|
|
47
|
+
args: { ...LabelAndHelper.args, helper: 'Radio button helper' },
|
|
48
|
+
decorators: [
|
|
49
|
+
withCombinations({
|
|
50
|
+
combinations: {
|
|
51
|
+
rows: {
|
|
52
|
+
Default: {},
|
|
53
|
+
Checked: { isChecked: true },
|
|
54
|
+
},
|
|
55
|
+
cols: {
|
|
56
|
+
Default: {},
|
|
57
|
+
Disabled: { isDisabled: true },
|
|
58
|
+
'ARIA Disabled': { 'aria-disabled': true },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
combinator(a, b) {
|
|
62
|
+
return Object.assign(a, b, {
|
|
63
|
+
// Injecting a unique name for each radio buttons to make sure they can be individually focused
|
|
64
|
+
name: uniqueId('name'),
|
|
65
|
+
// Disabling
|
|
66
|
+
onChange: undefined,
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
],
|
|
71
|
+
};
|
|
@@ -100,6 +100,36 @@ describe(`<${RadioButton.displayName}>`, () => {
|
|
|
100
100
|
});
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
describe('Disabled state', () => {
|
|
104
|
+
it('should be disabled with isDisabled', async () => {
|
|
105
|
+
const onChange = jest.fn();
|
|
106
|
+
const { radioButton, input } = setup({ isDisabled: true, onChange });
|
|
107
|
+
|
|
108
|
+
expect(radioButton).toHaveClass('lumx-radio-button--is-disabled');
|
|
109
|
+
expect(input).toBeDisabled();
|
|
110
|
+
expect(input).toHaveAttribute('readOnly');
|
|
111
|
+
|
|
112
|
+
// Should not trigger onChange.
|
|
113
|
+
await userEvent.click(input);
|
|
114
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should be disabled with aria-disabled', async () => {
|
|
118
|
+
const onChange = jest.fn();
|
|
119
|
+
const { radioButton, input } = setup({ 'aria-disabled': true, onChange });
|
|
120
|
+
|
|
121
|
+
expect(radioButton).toHaveClass('lumx-radio-button--is-disabled');
|
|
122
|
+
// Note: input is not disabled (so it can be focused) but it's readOnly.
|
|
123
|
+
expect(input).not.toBeDisabled();
|
|
124
|
+
expect(input).toHaveAttribute('aria-disabled', 'true');
|
|
125
|
+
expect(input).toHaveAttribute('readOnly');
|
|
126
|
+
|
|
127
|
+
// Should not trigger onChange.
|
|
128
|
+
await userEvent.click(input);
|
|
129
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
103
133
|
// Common tests suite.
|
|
104
134
|
commonTestsSuiteRTL(setup, {
|
|
105
135
|
baseClassName: CLASSNAME,
|
|
@@ -8,11 +8,13 @@ import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/classNam
|
|
|
8
8
|
import { useId } from '@lumx/react/hooks/useId';
|
|
9
9
|
import { useTheme } from '@lumx/react/utils/theme/ThemeContext';
|
|
10
10
|
import { forwardRef } from '@lumx/react/utils/react/forwardRef';
|
|
11
|
+
import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps';
|
|
12
|
+
import { HasAriaDisabled } from '@lumx/react/utils/type/HasAriaDisabled';
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Defines the props of the component.
|
|
14
16
|
*/
|
|
15
|
-
export interface RadioButtonProps extends GenericProps, HasTheme {
|
|
17
|
+
export interface RadioButtonProps extends GenericProps, HasTheme, HasAriaDisabled {
|
|
16
18
|
/** Helper text. */
|
|
17
19
|
helper?: string;
|
|
18
20
|
/** Native input id property. */
|
|
@@ -58,16 +60,15 @@ const DEFAULT_PROPS: Partial<RadioButtonProps> = {};
|
|
|
58
60
|
* @return React element.
|
|
59
61
|
*/
|
|
60
62
|
export const RadioButton = forwardRef<RadioButtonProps, HTMLDivElement>((props, ref) => {
|
|
63
|
+
const { isAnyDisabled, disabledStateProps, otherProps } = useDisableStateProps(props);
|
|
61
64
|
const defaultTheme = useTheme() || Theme.light;
|
|
62
65
|
const {
|
|
63
66
|
checked,
|
|
64
67
|
className,
|
|
65
|
-
disabled,
|
|
66
68
|
helper,
|
|
67
69
|
id,
|
|
68
70
|
inputRef,
|
|
69
71
|
isChecked = checked,
|
|
70
|
-
isDisabled = disabled,
|
|
71
72
|
label,
|
|
72
73
|
name,
|
|
73
74
|
onChange,
|
|
@@ -75,7 +76,7 @@ export const RadioButton = forwardRef<RadioButtonProps, HTMLDivElement>((props,
|
|
|
75
76
|
value,
|
|
76
77
|
inputProps,
|
|
77
78
|
...forwardedProps
|
|
78
|
-
} =
|
|
79
|
+
} = otherProps;
|
|
79
80
|
const generatedInputId = useId();
|
|
80
81
|
const inputId = id || generatedInputId;
|
|
81
82
|
|
|
@@ -93,7 +94,7 @@ export const RadioButton = forwardRef<RadioButtonProps, HTMLDivElement>((props,
|
|
|
93
94
|
className,
|
|
94
95
|
handleBasicClasses({
|
|
95
96
|
isChecked,
|
|
96
|
-
isDisabled,
|
|
97
|
+
isDisabled: isAnyDisabled,
|
|
97
98
|
isUnchecked: !isChecked,
|
|
98
99
|
prefix: CLASSNAME,
|
|
99
100
|
theme,
|
|
@@ -104,14 +105,14 @@ export const RadioButton = forwardRef<RadioButtonProps, HTMLDivElement>((props,
|
|
|
104
105
|
<input
|
|
105
106
|
ref={inputRef}
|
|
106
107
|
className={`${CLASSNAME}__input-native`}
|
|
107
|
-
|
|
108
|
+
{...disabledStateProps}
|
|
108
109
|
id={inputId}
|
|
109
|
-
tabIndex={isDisabled ? -1 : 0}
|
|
110
110
|
type="radio"
|
|
111
111
|
name={name}
|
|
112
112
|
value={value}
|
|
113
113
|
checked={isChecked}
|
|
114
114
|
onChange={handleChange}
|
|
115
|
+
readOnly={inputProps?.readOnly || isAnyDisabled}
|
|
115
116
|
aria-describedby={helper ? `${inputId}-helper` : undefined}
|
|
116
117
|
{...inputProps}
|
|
117
118
|
/>
|
|
@@ -11,6 +11,7 @@ import { clamp } from '@lumx/react/utils/number/clamp';
|
|
|
11
11
|
import { useId } from '@lumx/react/hooks/useId';
|
|
12
12
|
import { useTheme } from '@lumx/react/utils/theme/ThemeContext';
|
|
13
13
|
import { forwardRef } from '@lumx/react/utils/react/forwardRef';
|
|
14
|
+
import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Defines the props of the component.
|
|
@@ -91,14 +92,13 @@ const computePercentFromValue = (value: number, min: number, max: number): numbe
|
|
|
91
92
|
* @return React element.
|
|
92
93
|
*/
|
|
93
94
|
export const Slider = forwardRef<SliderProps, HTMLDivElement>((props, ref) => {
|
|
95
|
+
const { isAnyDisabled, disabledStateProps, otherProps } = useDisableStateProps(props);
|
|
94
96
|
const defaultTheme = useTheme() || Theme.light;
|
|
95
97
|
const {
|
|
96
98
|
className,
|
|
97
|
-
disabled,
|
|
98
99
|
helper,
|
|
99
100
|
hideMinMaxLabel,
|
|
100
101
|
id,
|
|
101
|
-
isDisabled = disabled,
|
|
102
102
|
label,
|
|
103
103
|
max,
|
|
104
104
|
min,
|
|
@@ -110,7 +110,7 @@ export const Slider = forwardRef<SliderProps, HTMLDivElement>((props, ref) => {
|
|
|
110
110
|
theme = defaultTheme,
|
|
111
111
|
value,
|
|
112
112
|
...forwardedProps
|
|
113
|
-
} =
|
|
113
|
+
} = otherProps;
|
|
114
114
|
const generatedId = useId();
|
|
115
115
|
const sliderId = id || generatedId;
|
|
116
116
|
const sliderLabelId = useMemo(() => `label-${sliderId}`, [sliderId]);
|
|
@@ -222,7 +222,7 @@ export const Slider = forwardRef<SliderProps, HTMLDivElement>((props, ref) => {
|
|
|
222
222
|
onMouseDown?.(event);
|
|
223
223
|
|
|
224
224
|
const { current: slider } = sliderRef;
|
|
225
|
-
if (
|
|
225
|
+
if (isAnyDisabled || !slider) return;
|
|
226
226
|
const newValue = getPercentValue(event, slider);
|
|
227
227
|
if (onChange) {
|
|
228
228
|
onChange(computeValueFromPercent(newValue, min, max, precision), name, event);
|
|
@@ -242,7 +242,6 @@ export const Slider = forwardRef<SliderProps, HTMLDivElement>((props, ref) => {
|
|
|
242
242
|
handleBasicClasses({ prefix: CLASSNAME, theme, hasLabel: Boolean(label) }),
|
|
243
243
|
)}
|
|
244
244
|
onMouseDown={handleMouseDown}
|
|
245
|
-
aria-disabled={isDisabled}
|
|
246
245
|
>
|
|
247
246
|
{label && (
|
|
248
247
|
<InputLabel id={sliderLabelId} htmlFor={sliderId} className={`${CLASSNAME}__label`} theme={theme}>
|
|
@@ -284,8 +283,8 @@ export const Slider = forwardRef<SliderProps, HTMLDivElement>((props, ref) => {
|
|
|
284
283
|
id={sliderId}
|
|
285
284
|
className={`${CLASSNAME}__handle`}
|
|
286
285
|
style={{ left: percentString }}
|
|
287
|
-
onKeyDown={handleKeyDown}
|
|
288
|
-
|
|
286
|
+
onKeyDown={isAnyDisabled ? undefined : handleKeyDown}
|
|
287
|
+
{...disabledStateProps}
|
|
289
288
|
/>
|
|
290
289
|
</div>
|
|
291
290
|
{!hideMinMaxLabel && (
|