@lumx/react 3.19.1-alpha.0 → 3.19.1-alpha.10

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/package.json CHANGED
@@ -6,8 +6,8 @@
6
6
  "url": "https://github.com/lumapps/design-system/issues"
7
7
  },
8
8
  "dependencies": {
9
- "@lumx/core": "^3.19.1-alpha.0",
10
- "@lumx/icons": "^3.19.1-alpha.0",
9
+ "@lumx/core": "^3.19.1-alpha.10",
10
+ "@lumx/icons": "^3.19.1-alpha.10",
11
11
  "@popperjs/core": "^2.5.4",
12
12
  "body-scroll-lock": "^3.1.5",
13
13
  "classnames": "^2.3.2",
@@ -30,6 +30,7 @@
30
30
  "@rollup/plugin-babel": "^6.0.4",
31
31
  "@rollup/plugin-commonjs": "^19.0.2",
32
32
  "@rollup/plugin-node-resolve": "16.0.0",
33
+ "@rollup/pluginutils": "5.2.0",
33
34
  "@storybook/addon-a11y": "^9.1.4",
34
35
  "@storybook/addon-docs": "^9.1.4",
35
36
  "@storybook/react-vite": "^9.1.4",
@@ -74,8 +75,8 @@
74
75
  },
75
76
  "peerDependencies": {
76
77
  "lodash": "4.17.21",
77
- "react": ">= 16.13.0",
78
- "react-dom": ">= 16.13.0"
78
+ "react": ">= 17.0.0",
79
+ "react-dom": ">= 17.0.0"
79
80
  },
80
81
  "description": "The official LumApps Design System (LumX) for React applications",
81
82
  "homepage": "https://github.com/lumapps/design-system",
@@ -105,5 +106,5 @@
105
106
  "build:storybook": "storybook build"
106
107
  },
107
108
  "sideEffects": false,
108
- "version": "3.19.1-alpha.0"
109
+ "version": "3.19.1-alpha.10"
109
110
  }
@@ -78,10 +78,9 @@ describe(`<${Button.displayName}>`, () => {
78
78
  it('should render disabled link', async () => {
79
79
  const onClick = jest.fn();
80
80
  const { button } = setup({ children: 'Label', disabled: true, href: 'https://example.com', onClick });
81
- expect(screen.queryByRole('link')).toBeInTheDocument();
82
- expect(button).toHaveAttribute('aria-disabled', 'true');
83
- // Simulate standard disabled state (not focusable)
84
- expect(button).toHaveAttribute('tabindex', '-1');
81
+ // Disabled link do not exist so we fallback to a button
82
+ expect(screen.queryByRole('link')).not.toBeInTheDocument();
83
+ expect(button).toHaveAttribute('disabled');
85
84
  await userEvent.click(button);
86
85
  expect(onClick).not.toHaveBeenCalled();
87
86
  });
@@ -103,7 +102,8 @@ describe(`<${Button.displayName}>`, () => {
103
102
  onClick,
104
103
  });
105
104
  expect(button).toHaveAccessibleName('Label');
106
- expect(screen.queryByRole('link')).toBeInTheDocument();
105
+ // Disabled link do not exist so we fallback to a button
106
+ expect(screen.queryByRole('link')).not.toBeInTheDocument();
107
107
  expect(button).toHaveAttribute('aria-disabled', 'true');
108
108
  await userEvent.click(button);
109
109
  expect(onClick).not.toHaveBeenCalled();
@@ -1,15 +1,17 @@
1
1
  import React, { AriaAttributes, ButtonHTMLAttributes, DetailedHTMLProps, RefObject } from 'react';
2
2
 
3
+ import isEmpty from 'lodash/isEmpty';
4
+
3
5
  import classNames from 'classnames';
4
6
 
5
7
  import { ColorPalette, Emphasis, Size, Theme } from '@lumx/react';
6
8
  import { CSS_PREFIX } from '@lumx/react/constants';
7
9
  import { GenericProps, HasTheme } from '@lumx/react/utils/type';
8
10
  import { handleBasicClasses } from '@lumx/core/js/utils/className';
11
+ import { renderLink } from '@lumx/react/utils/react/renderLink';
9
12
  import { forwardRef } from '@lumx/react/utils/react/forwardRef';
13
+ import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps';
10
14
  import { HasAriaDisabled } from '@lumx/react/utils/type/HasAriaDisabled';
11
- import { RawClickable } from '@lumx/react/utils/react/RawClickable';
12
- import { useDisableStateProps } from '@lumx/react/utils/disabled';
13
15
 
14
16
  type HTMLButtonProps = DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;
15
17
 
@@ -105,14 +107,18 @@ export const ButtonRoot = forwardRef<ButtonRootProps, HTMLButtonElement | HTMLAn
105
107
  color,
106
108
  emphasis,
107
109
  hasBackground,
110
+ href,
108
111
  isSelected,
109
112
  isActive,
110
113
  isFocused,
111
114
  isHovered,
112
115
  linkAs,
116
+ name,
113
117
  size,
118
+ target,
114
119
  theme,
115
120
  variant,
121
+ type = 'button',
116
122
  fullWidth,
117
123
  ...forwardedProps
118
124
  } = otherProps;
@@ -133,7 +139,7 @@ export const ButtonRoot = forwardRef<ButtonRootProps, HTMLButtonElement | HTMLAn
133
139
  color: adaptedColor,
134
140
  emphasis,
135
141
  isSelected,
136
- isDisabled: props.isDisabled || props['aria-disabled'],
142
+ isDisabled: isAnyDisabled,
137
143
  isActive,
138
144
  isFocused,
139
145
  isHovered,
@@ -145,18 +151,42 @@ export const ButtonRoot = forwardRef<ButtonRootProps, HTMLButtonElement | HTMLAn
145
151
  }),
146
152
  );
147
153
 
154
+ /**
155
+ * If the linkAs prop is used, we use the linkAs component instead of a <button>.
156
+ * If there is an href attribute, we display an <a> instead of a <button>.
157
+ *
158
+ * However, in any case, if the component is disabled, we returned a <button> since disabled is not compatible with <a>.
159
+ */
160
+ if ((linkAs || !isEmpty(props.href)) && !isAnyDisabled) {
161
+ return renderLink(
162
+ {
163
+ linkAs,
164
+ ...forwardedProps,
165
+ 'aria-label': ariaLabel,
166
+ href,
167
+ target,
168
+ className: buttonClassName,
169
+ ref: ref as RefObject<HTMLAnchorElement>,
170
+ },
171
+ children,
172
+ );
173
+ }
148
174
  return (
149
- <RawClickable
150
- as={linkAs || forwardedProps.href ? 'a' : 'button'}
175
+ <button
151
176
  {...forwardedProps}
152
177
  {...disabledStateProps}
153
178
  aria-disabled={isAnyDisabled}
154
179
  aria-label={ariaLabel}
155
180
  ref={ref as RefObject<HTMLButtonElement>}
156
181
  className={buttonClassName}
182
+ name={name}
183
+ type={
184
+ // eslint-disable-next-line react/button-has-type
185
+ type
186
+ }
157
187
  >
158
188
  {children}
159
- </RawClickable>
189
+ </button>
160
190
  );
161
191
  });
162
192
  ButtonRoot.displayName = COMPONENT_NAME;
@@ -1,47 +1,21 @@
1
+ import DefaultStory, { SizeAndShape as DefaultSizeAndShape } from '@lumx/core/js/components/Icon/Stories';
1
2
  import { mdiEmail } from '@lumx/icons';
2
- import { ColorPalette, ColorVariant, GridColumn, Icon, IconSizes, Size } from '@lumx/react';
3
+ import { ColorPalette, ColorVariant, GridColumn, Icon, Size } from '@lumx/react';
3
4
  import { withCombinations } from '@lumx/react/stories/decorators/withCombinations';
4
5
  import { withUndefined } from '@lumx/react/stories/controls/withUndefined';
5
- import { iconArgType } from '@lumx/react/stories/controls/icons';
6
- import { colorArgType, colorVariantArgType } from '@lumx/react/stories/controls/color';
7
6
  import { withWrapper } from '@lumx/react/stories/decorators/withWrapper';
8
7
 
9
- const iconSizes: Array<IconSizes> = [Size.xxs, Size.xs, Size.s, Size.m, Size.l, Size.xl, Size.xxl];
10
-
11
8
  export default {
12
9
  title: 'LumX components/icon/Icon',
13
10
  component: Icon,
14
- args: Icon.defaultProps,
15
- argTypes: {
16
- icon: iconArgType,
17
- hasShape: { control: 'boolean' },
18
- color: colorArgType,
19
- colorVariant: colorVariantArgType,
20
- },
11
+ ...DefaultStory,
21
12
  };
22
13
 
23
14
  /**
24
15
  * All combinations of size and shape
25
16
  */
26
17
  export const SizeAndShape = {
27
- args: {
28
- icon: mdiEmail,
29
- },
30
- argTypes: {
31
- hasShape: { control: false },
32
- size: { control: false },
33
- },
34
- decorators: [
35
- withCombinations({
36
- combinations: {
37
- cols: { key: 'size', options: withUndefined(iconSizes) },
38
- rows: {
39
- Default: {},
40
- 'Has shape': { hasShape: true },
41
- },
42
- },
43
- }),
44
- ],
18
+ ...DefaultSizeAndShape,
45
19
  };
46
20
 
47
21
  /**
@@ -1,11 +1,10 @@
1
1
  import React from 'react';
2
2
 
3
- import { mdiAlertCircle } from '@lumx/icons';
4
- import { ColorPalette, ColorVariant, Size, Theme } from '@lumx/react';
5
3
  import { commonTestsSuiteRTL, SetupRenderOptions } from '@lumx/react/testing/utils';
6
4
 
7
5
  import { getByClassName, getByTagName } from '@lumx/react/testing/utils/queries';
8
6
  import { render } from '@testing-library/react';
7
+ import Tests from '@lumx/core/js/components/Icon/Tests';
9
8
  import { Icon, IconProps } from './Icon';
10
9
 
11
10
  const CLASSNAME = Icon.className as string;
@@ -29,89 +28,7 @@ const setup = (propsOverride: SetupProps = {}, { wrapper }: SetupRenderOptions =
29
28
  };
30
29
 
31
30
  describe(`<${Icon.displayName}>`, () => {
32
- describe('Props', () => {
33
- it('should render default', () => {
34
- const { i, svg, path, props } = setup();
35
-
36
- expect(i).toBeInTheDocument();
37
- expect(i).toHaveClass(CLASSNAME);
38
- expect(i?.className).toMatchInlineSnapshot('"lumx-icon lumx-icon--no-shape lumx-icon--path"');
39
-
40
- expect(svg).toBeInTheDocument();
41
- expect(svg).toHaveAttribute('aria-hidden', 'true');
42
- expect(svg).not.toHaveAttribute('role');
43
-
44
- expect(path).toBeInTheDocument();
45
- expect(path).toHaveAttribute('d', props.icon);
46
- });
47
-
48
- it('should adapt svg with alternate text', () => {
49
- const { svg, props } = setup({ alt: 'Alternate text' });
50
- expect(svg).toHaveAttribute('aria-label', props.alt);
51
- expect(svg).not.toHaveAttribute('aria-hidden');
52
- expect(svg).toHaveAttribute('role');
53
- });
54
-
55
- describe('size', () => {
56
- it('should render size', () => {
57
- const { i } = setup({ size: Size.s });
58
- expect(i).toHaveClass('lumx-icon--size-s');
59
- });
60
-
61
- it('should adapt xxs size with hasShape', () => {
62
- const { i } = setup({ hasShape: true, size: Size.xxs });
63
- expect(i).toHaveClass('lumx-icon--size-s');
64
- });
65
-
66
- it('should adapt xs size with hasShape', () => {
67
- const { i } = setup({ hasShape: true, size: Size.xs });
68
- expect(i).toHaveClass('lumx-icon--size-s');
69
- });
70
-
71
- it('should adapt xxl size with hasShape', () => {
72
- const { i } = setup({ hasShape: true, size: Size.xxl });
73
- expect(i).toHaveClass('lumx-icon--size-xl');
74
- });
75
-
76
- it('should add default size with hasShape', () => {
77
- const { i } = setup({ hasShape: true });
78
- expect(i).toHaveClass('lumx-icon--size-m');
79
- });
80
- });
81
-
82
- describe('color', () => {
83
- it('should render color and color variant', () => {
84
- const { i } = setup({
85
- color: ColorPalette.primary,
86
- colorVariant: ColorVariant.D1,
87
- });
88
- expect(i).toHaveClass('lumx-icon--color-primary lumx-icon--color-variant-D1');
89
- });
90
-
91
- it('should improve yellow icon color contrast with alert circle icon', () => {
92
- const { i } = setup({
93
- color: ColorPalette.yellow,
94
- icon: mdiAlertCircle,
95
- });
96
- expect(i).toHaveClass('lumx-icon--color-yellow lumx-icon--has-dark-layer');
97
- });
98
-
99
- it('should set a default color on dark theme', () => {
100
- const { i } = setup({ theme: Theme.dark });
101
- expect(i).toHaveClass('lumx-icon--color-light lumx-icon--theme-dark');
102
- });
103
-
104
- it('should set a default color on has shape', () => {
105
- const { i } = setup({ hasShape: true });
106
- expect(i).toHaveClass('lumx-icon--color-dark lumx-icon--has-shape');
107
- });
108
-
109
- it('should set a default color variant on has shape & dark color', () => {
110
- const { i } = setup({ color: ColorPalette.dark, hasShape: true });
111
- expect(i).toHaveClass('lumx-icon--color-variant-L2 lumx-icon--color-dark lumx-icon--has-shape');
112
- });
113
- });
114
- });
31
+ Tests((props: IconProps, { wrapper }: any) => render(<Icon {...props} />, { wrapper }));
115
32
 
116
33
  // Common tests suite.
117
34
  commonTestsSuiteRTL(setup, {
@@ -1,52 +1,11 @@
1
1
  import React from 'react';
2
2
 
3
- import classNames from 'classnames';
3
+ import { Icon as UI, IconProps, IconSizes } from '@lumx/core/js/components/Icon';
4
4
 
5
- import { mdiAlertCircle } from '@lumx/icons';
6
- import { ColorPalette, ColorVariant, ColorWithVariants, Size, Theme } from '@lumx/react';
7
- import { GenericProps, HasTheme } from '@lumx/react/utils/type';
8
- import { getRootClassName, handleBasicClasses, resolveColorWithVariants } from '@lumx/core/js/utils/className';
9
5
  import { forwardRef } from '@lumx/react/utils/react/forwardRef';
10
6
  import { useTheme } from '@lumx/react/utils/theme/ThemeContext';
11
7
 
12
- export type IconSizes = Extract<Size, 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl'>;
13
-
14
- /**
15
- * Defines the props of the component.
16
- */
17
- export interface IconProps extends GenericProps, HasTheme {
18
- /** Color variant. */
19
- color?: ColorWithVariants;
20
- /** Lightened or darkened variant of the selected icon color. */
21
- colorVariant?: ColorVariant;
22
- /** Whether the icon has a shape. */
23
- hasShape?: boolean;
24
- /**
25
- * Icon (SVG path) draw code (`d` property of the `<path>` SVG element).
26
- * See https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths
27
- */
28
- icon: string;
29
- /** Size variant. */
30
- size?: IconSizes;
31
- /** Sets an alternative text on the svg. Will set an `img` role to the svg. */
32
- alt?: string;
33
- }
34
-
35
- /**
36
- * Component display name.
37
- */
38
- const COMPONENT_NAME = 'Icon';
39
-
40
- /**
41
- * Component default class name and class prefix.
42
- */
43
- const CLASSNAME = getRootClassName(COMPONENT_NAME);
44
-
45
- /**
46
- * Component default props.
47
- */
48
- const DEFAULT_PROPS: Partial<IconProps> = {};
49
-
8
+ export type { IconProps, IconSizes };
50
9
  /**
51
10
  * Icon component.
52
11
  *
@@ -56,80 +15,10 @@ const DEFAULT_PROPS: Partial<IconProps> = {};
56
15
  */
57
16
  export const Icon = forwardRef<IconProps, HTMLElement>((props, ref) => {
58
17
  const defaultTheme = useTheme();
59
- const {
60
- className,
61
- color: propColor,
62
- colorVariant: propColorVariant,
63
- hasShape,
64
- icon,
65
- size,
66
- theme = defaultTheme,
67
- alt,
68
- ...forwardedProps
69
- } = props;
70
- const [color, colorVariant] = resolveColorWithVariants(propColor, propColorVariant);
71
18
 
72
- // Color
73
- let iconColor = color;
74
- if (!iconColor && (hasShape || theme)) {
75
- iconColor = theme === Theme.dark ? ColorPalette.light : ColorPalette.dark;
76
- }
77
-
78
- // Color variant
79
- let iconColorVariant = colorVariant;
80
- if (!iconColorVariant && hasShape && iconColor === ColorPalette.dark) {
81
- iconColorVariant = 'L2';
82
- }
83
-
84
- // Size
85
- let iconSize = size;
86
- if (size && hasShape) {
87
- if (size === Size.xxs || size === Size.xs) {
88
- iconSize = Size.s;
89
- } else if (size === Size.xxl) {
90
- iconSize = Size.xl;
91
- }
92
- } else if (hasShape) {
93
- iconSize = Size.m;
94
- }
95
-
96
- return (
97
- <i
98
- ref={ref}
99
- {...forwardedProps}
100
- className={classNames(
101
- className,
102
- handleBasicClasses({
103
- color: iconColor,
104
- colorVariant: iconColorVariant,
105
- hasShape,
106
- prefix: CLASSNAME,
107
- theme,
108
- size: iconSize,
109
- }),
110
- !hasShape && `${CLASSNAME}--no-shape`,
111
- !hasShape &&
112
- iconColor === ColorPalette.yellow &&
113
- icon === mdiAlertCircle &&
114
- `${CLASSNAME}--has-dark-layer`,
115
- `${CLASSNAME}--path`,
116
- )}
117
- >
118
- <svg
119
- aria-hidden={alt ? undefined : 'true'}
120
- role={alt ? 'img' : undefined}
121
- aria-label={alt}
122
- height="1em"
123
- preserveAspectRatio="xMidYMid meet"
124
- style={{ verticalAlign: '-0.125em' }}
125
- viewBox="0 0 24 24"
126
- width="1em"
127
- >
128
- <path d={icon} fill="currentColor" />
129
- </svg>
130
- </i>
131
- );
19
+ return <UI ref={ref} {...props} theme={props.theme || defaultTheme} />;
132
20
  });
133
- Icon.displayName = COMPONENT_NAME;
134
- Icon.className = CLASSNAME;
135
- Icon.defaultProps = DEFAULT_PROPS;
21
+
22
+ Icon.displayName = UI.displayName;
23
+ Icon.className = UI.className;
24
+ Icon.defaultProps = UI.defaultProps;
@@ -85,10 +85,9 @@ describe(`<${Link.displayName}>`, () => {
85
85
  it('should render disabled link', async () => {
86
86
  const onClick = jest.fn();
87
87
  const { link } = setup({ children: 'Label', isDisabled: true, href: 'https://example.com', onClick });
88
- expect(screen.queryByRole('link')).toBeInTheDocument();
89
- expect(link).toHaveAttribute('aria-disabled');
90
- // Simulate standard disabled state (not focusable)
91
- expect(link).toHaveAttribute('tabindex', '-1');
88
+ // Disabled link do not exist so we fallback to a button
89
+ expect(screen.queryByRole('link')).not.toBeInTheDocument();
90
+ expect(link).toHaveAttribute('disabled');
92
91
  await userEvent.click(link);
93
92
  expect(onClick).not.toHaveBeenCalled();
94
93
  });
@@ -96,9 +95,7 @@ describe(`<${Link.displayName}>`, () => {
96
95
  it('should render aria-disabled button', async () => {
97
96
  const onClick = jest.fn();
98
97
  const { link } = setup({ children: 'Label', 'aria-disabled': true, onClick });
99
- expect(screen.queryByRole('button')).toBeInTheDocument();
100
- expect(link).toHaveAttribute('aria-disabled', 'true');
101
- expect(link).not.toHaveAttribute('tabindex');
98
+ expect(link).toHaveAttribute('aria-disabled');
102
99
  await userEvent.click(link);
103
100
  expect(onClick).not.toHaveBeenCalled();
104
101
  });
@@ -112,7 +109,8 @@ describe(`<${Link.displayName}>`, () => {
112
109
  onClick,
113
110
  });
114
111
  expect(link).toHaveAccessibleName('Label');
115
- expect(screen.queryByRole('link')).toBeInTheDocument();
112
+ // Disabled link do not exist so we fallback to a button
113
+ expect(screen.queryByRole('link')).not.toBeInTheDocument();
116
114
  expect(link).toHaveAttribute('aria-disabled', 'true');
117
115
  await userEvent.click(link);
118
116
  expect(onClick).not.toHaveBeenCalled();
@@ -12,9 +12,8 @@ import {
12
12
  } from '@lumx/core/js/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';
15
16
  import { HasAriaDisabled } from '@lumx/react/utils/type/HasAriaDisabled';
16
- import { RawClickable } from '@lumx/react/utils/react/RawClickable';
17
- import { useDisableStateProps } from '@lumx/react/utils/disabled';
18
17
 
19
18
  type HTMLAnchorProps = React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
20
19
 
@@ -68,26 +67,38 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
68
67
  * @return React element.
69
68
  */
70
69
  export const Link = forwardRef<LinkProps, HTMLAnchorElement | HTMLButtonElement>((props, ref) => {
71
- const { disabledStateProps, otherProps } = useDisableStateProps(props);
70
+ const { isAnyDisabled, disabledStateProps, otherProps } = useDisableStateProps(props);
72
71
  const {
73
72
  children,
74
73
  className,
75
74
  color: propColor,
76
75
  colorVariant: propColorVariant,
76
+ href,
77
77
  leftIcon,
78
+ linkAs,
78
79
  rightIcon,
80
+ target,
79
81
  typography,
80
- linkAs,
81
82
  ...forwardedProps
82
83
  } = otherProps;
83
84
  const [color, colorVariant] = resolveColorWithVariants(propColor, propColorVariant);
84
85
 
86
+ const isLink = linkAs || href;
87
+ const Component = isLink && !isAnyDisabled ? linkAs || 'a' : 'button';
88
+ const baseProps: React.ComponentProps<typeof Component> = {};
89
+ if (Component === 'button') {
90
+ baseProps.type = 'button';
91
+ Object.assign(baseProps, disabledStateProps);
92
+ } else if (isLink) {
93
+ baseProps.href = href;
94
+ baseProps.target = target;
95
+ }
96
+
85
97
  return (
86
- <RawClickable
87
- ref={ref as any}
88
- as={linkAs || forwardedProps.href ? 'a' : 'button'}
98
+ <Component
99
+ ref={ref}
89
100
  {...forwardedProps}
90
- {...disabledStateProps}
101
+ {...baseProps}
91
102
  className={classNames(
92
103
  className,
93
104
  handleBasicClasses({ prefix: CLASSNAME, color, colorVariant, hasTypography: !!typography }),
@@ -101,7 +112,7 @@ export const Link = forwardRef<LinkProps, HTMLAnchorElement | HTMLButtonElement>
101
112
  {rightIcon && <Icon icon={rightIcon} className={`${CLASSNAME}__right-icon`} />}
102
113
  </>,
103
114
  )}
104
- </RawClickable>
115
+ </Component>
105
116
  );
106
117
  });
107
118
  Link.displayName = COMPONENT_NAME;
@@ -43,7 +43,7 @@ describe(`<${ListItem.displayName}>`, () => {
43
43
  it('should render disabled list item button', async () => {
44
44
  const onItemSelected = jest.fn();
45
45
  const { link } = setup({ children: 'Label', isDisabled: true, onItemSelected });
46
- expect(link).toBeDisabled();
46
+ expect(link).toHaveAttribute('aria-disabled', 'true');
47
47
  // The `renderLink` util removes the onClick handler but `user-event` will also not fire events on disabled elements.
48
48
  if (link) await userEvent.click(link);
49
49
  expect(onItemSelected).not.toHaveBeenCalled();
@@ -57,6 +57,7 @@ describe(`<${ListItem.displayName}>`, () => {
57
57
  linkProps: { href: 'https://example.com' },
58
58
  onItemSelected,
59
59
  });
60
+ expect(link).not.toHaveAttribute('href');
60
61
  expect(link).toHaveAttribute('aria-disabled', 'true');
61
62
  if (link) await userEvent.click(link);
62
63
  expect(onItemSelected).not.toHaveBeenCalled();
@@ -78,6 +79,7 @@ describe(`<${ListItem.displayName}>`, () => {
78
79
  linkProps: { href: 'https://example.com' },
79
80
  onItemSelected,
80
81
  });
82
+ expect(link).not.toHaveAttribute('href');
81
83
  expect(link).toHaveAttribute('aria-disabled', 'true');
82
84
  if (link) await userEvent.click(link);
83
85
  expect(onItemSelected).not.toHaveBeenCalled();
@@ -1,15 +1,16 @@
1
- import React, { ReactNode, Ref, SyntheticEvent } from 'react';
1
+ import React, { ReactNode, Ref, SyntheticEvent, useMemo } from 'react';
2
2
 
3
3
  import classNames from 'classnames';
4
4
  import isEmpty from 'lodash/isEmpty';
5
5
 
6
6
  import { ListProps, Size } from '@lumx/react';
7
7
  import { GenericProps } from '@lumx/react/utils/type';
8
+ import { onEnterPressed, onButtonPressed } from '@lumx/core/js/utils';
8
9
  import { getRootClassName, handleBasicClasses } from '@lumx/core/js/utils/className';
10
+ import { renderLink } from '@lumx/react/utils/react/renderLink';
9
11
  import { forwardRef } from '@lumx/react/utils/react/forwardRef';
10
12
  import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps';
11
13
  import { HasAriaDisabled } from '@lumx/react/utils/type/HasAriaDisabled';
12
- import { RawClickable } from '@lumx/react/utils/react/RawClickable';
13
14
 
14
15
  export type ListItemSize = Extract<Size, 'tiny' | 'regular' | 'big' | 'huge'>;
15
16
 
@@ -93,6 +94,13 @@ export const ListItem = forwardRef<ListItemProps, HTMLLIElement>((props, ref) =>
93
94
  ...forwardedProps
94
95
  } = otherProps;
95
96
 
97
+ const role = linkAs || linkProps.href ? 'link' : 'button';
98
+ const onKeyDown = useMemo(() => {
99
+ if (onItemSelected && role === 'link') return onEnterPressed(onItemSelected as any);
100
+ if (onItemSelected && role === 'button') return onButtonPressed(onItemSelected as any);
101
+ return undefined;
102
+ }, [role, onItemSelected]);
103
+
96
104
  const content = (
97
105
  <>
98
106
  {before && <div className={`${CLASSNAME}__before`}>{before}</div>}
@@ -115,23 +123,28 @@ export const ListItem = forwardRef<ListItemProps, HTMLLIElement>((props, ref) =>
115
123
  >
116
124
  {isClickable({ linkProps, onItemSelected }) ? (
117
125
  /* Clickable list item */
118
- <RawClickable
119
- as={linkAs || linkProps.href ? 'a' : 'button'}
120
- {...linkProps}
121
- {...disabledStateProps}
122
- className={classNames(
123
- handleBasicClasses({
124
- prefix: `${CLASSNAME}__link`,
125
- isHighlighted,
126
- isSelected,
127
- isDisabled: isAnyDisabled,
128
- }),
129
- )}
130
- onClick={onItemSelected}
131
- ref={linkRef}
132
- >
133
- {content}
134
- </RawClickable>
126
+ renderLink(
127
+ {
128
+ linkAs,
129
+ tabIndex: !disabledStateProps.disabled ? 0 : undefined,
130
+ role,
131
+ 'aria-disabled': isAnyDisabled,
132
+ ...linkProps,
133
+ href: isAnyDisabled ? undefined : linkProps.href,
134
+ className: classNames(
135
+ handleBasicClasses({
136
+ prefix: `${CLASSNAME}__link`,
137
+ isHighlighted,
138
+ isSelected,
139
+ isDisabled: isAnyDisabled,
140
+ }),
141
+ ),
142
+ onClick: isAnyDisabled ? undefined : onItemSelected,
143
+ onKeyDown: isAnyDisabled ? undefined : onKeyDown,
144
+ ref: linkRef,
145
+ },
146
+ content,
147
+ )
135
148
  ) : (
136
149
  /* Non clickable list item */
137
150
  <div className={`${CLASSNAME}__wrapper`}>{content}</div>