@lumx/react 3.18.1-alpha.0 → 3.18.2-alpha.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 (39) hide show
  1. package/index.d.ts +13 -8
  2. package/index.js +217 -152
  3. package/index.js.map +1 -1
  4. package/package.json +3 -3
  5. package/src/components/autocomplete/Autocomplete.tsx +5 -4
  6. package/src/components/autocomplete/AutocompleteMultiple.tsx +5 -3
  7. package/src/components/button/Button.stories.tsx +1 -0
  8. package/src/components/button/Button.test.tsx +41 -2
  9. package/src/components/button/ButtonRoot.tsx +10 -11
  10. package/src/components/checkbox/Checkbox.stories.tsx +13 -2
  11. package/src/components/checkbox/Checkbox.test.tsx +29 -0
  12. package/src/components/checkbox/Checkbox.tsx +8 -7
  13. package/src/components/chip/Chip.stories.tsx +17 -0
  14. package/src/components/chip/Chip.test.tsx +44 -0
  15. package/src/components/chip/Chip.tsx +10 -9
  16. package/src/components/date-picker/DatePickerField.stories.tsx +18 -0
  17. package/src/components/date-picker/DatePickerField.tsx +4 -4
  18. package/src/components/link/Link.stories.tsx +4 -1
  19. package/src/components/link/Link.test.tsx +45 -6
  20. package/src/components/link/Link.tsx +7 -6
  21. package/src/components/list/ListItem.stories.tsx +14 -48
  22. package/src/components/list/ListItem.test.tsx +78 -7
  23. package/src/components/list/ListItem.tsx +11 -9
  24. package/src/components/progress-tracker/ProgressTrackerStep.tsx +7 -7
  25. package/src/components/radio-button/RadioButton.stories.tsx +32 -0
  26. package/src/components/radio-button/RadioButton.test.tsx +30 -0
  27. package/src/components/radio-button/RadioButton.tsx +8 -7
  28. package/src/components/slider/Slider.tsx +6 -7
  29. package/src/components/switch/Switch.stories.tsx +11 -1
  30. package/src/components/switch/Switch.test.tsx +30 -0
  31. package/src/components/switch/Switch.tsx +8 -7
  32. package/src/components/table/TableRow.tsx +8 -6
  33. package/src/components/tabs/Tab.tsx +12 -9
  34. package/src/components/text-field/TextField.stories.tsx +22 -0
  35. package/src/components/text-field/TextField.test.tsx +56 -0
  36. package/src/components/text-field/TextField.tsx +12 -10
  37. package/src/utils/disabled/index.ts +1 -0
  38. package/src/utils/disabled/useDisableStateProps.tsx +34 -0
  39. package/src/utils/type/HasAriaDisabled.ts +6 -0
@@ -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
- } = props;
83
+ } = otherProps;
83
84
  const [color, colorVariant] = resolveColorWithVariants(propColor, propColorVariant);
84
85
 
85
86
  const isLink = linkAs || href;
86
- const Component = isLink && !isDisabled ? linkAs || 'a' : 'button';
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.disabled = isDisabled;
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 NonClickable = {
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 SizeAndStates = {
85
- ...Button,
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
- const setup = (props: Partial<ListItemProps> = {}) => {
12
- render(<ListItem {...(props as any)} />);
13
- const listItem = queryByClassName(document.body, CLASSNAME);
14
- return { props, listItem };
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
- } = props;
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: !isDisabled && role === 'button' ? 0 : undefined,
129
+ tabIndex: !disabledStateProps.disabled ? 0 : undefined,
128
130
  role,
129
- 'aria-disabled': isDisabled,
131
+ 'aria-disabled': isAnyDisabled,
130
132
  ...linkProps,
131
- href: isDisabled ? undefined : linkProps.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: isDisabled ? undefined : onItemSelected,
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
- } = props;
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 (isDisabled) {
79
+ if (isAnyDisabled) {
80
80
  return;
81
81
  }
82
82
  state?.changeToTab();
83
- }, [isDisabled, state]);
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 && !isDisabled,
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={isDisabled}
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
- } = props;
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
- disabled={isDisabled}
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
- } = props;
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 (isDisabled || !slider) return;
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
- disabled={isDisabled}
286
+ onKeyDown={isAnyDisabled ? undefined : handleKeyDown}
287
+ {...disabledStateProps}
289
288
  />
290
289
  </div>
291
290
  {!hideMinMaxLabel && (