@lumx/react 3.19.1-alpha.9 → 3.20.1-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 (56) hide show
  1. package/_internal/index.js +13 -20
  2. package/_internal/index.js.map +1 -1
  3. package/index.d.ts +6 -5
  4. package/index.js +2439 -2399
  5. package/index.js.map +1 -1
  6. package/package.json +13 -10
  7. package/src/components/alert-dialog/AlertDialog.test.tsx +2 -3
  8. package/src/components/autocomplete/Autocomplete.test.tsx +3 -3
  9. package/src/components/button/Button.test.tsx +9 -9
  10. package/src/components/button/ButtonRoot.tsx +36 -6
  11. package/src/components/checkbox/Checkbox.test.tsx +3 -3
  12. package/src/components/chip/Chip.test.tsx +17 -19
  13. package/src/components/date-picker/DatePicker.test.tsx +3 -3
  14. package/src/components/date-picker/DatePickerControlled.test.tsx +6 -6
  15. package/src/components/date-picker/DatePickerField.test.tsx +3 -3
  16. package/src/components/dialog/Dialog.test.tsx +4 -4
  17. package/src/components/dropdown/Dropdown.test.tsx +3 -3
  18. package/src/components/expansion-panel/ExpansionPanel.test.tsx +5 -6
  19. package/src/components/icon/Icon.stories.tsx +4 -30
  20. package/src/components/icon/Icon.test.tsx +2 -85
  21. package/src/components/icon/Icon.tsx +7 -118
  22. package/src/components/image-lightbox/ImageLightbox.test.tsx +7 -11
  23. package/src/components/link/Link.test.tsx +11 -13
  24. package/src/components/link/Link.tsx +20 -9
  25. package/src/components/list/ListItem.test.tsx +5 -5
  26. package/src/components/message/Message.test.tsx +1 -1
  27. package/src/components/mosaic/Mosaic.test.tsx +3 -3
  28. package/src/components/navigation/NavigationItem.tsx +10 -6
  29. package/src/components/navigation/NavigationSection.tsx +3 -4
  30. package/src/components/notification/Notification.test.tsx +3 -4
  31. package/src/components/popover-dialog/PopoverDialog.test.tsx +1 -1
  32. package/src/components/radio-button/RadioButton.test.tsx +3 -3
  33. package/src/components/select/Select.test.tsx +7 -8
  34. package/src/components/select/SelectMultiple.test.tsx +5 -5
  35. package/src/components/side-navigation/SideNavigationItem.test.tsx +2 -2
  36. package/src/components/side-navigation/SideNavigationItem.tsx +27 -22
  37. package/src/components/slider/Slider.test.tsx +1 -1
  38. package/src/components/switch/Switch.test.tsx +5 -5
  39. package/src/components/table/TableCell.test.tsx +1 -1
  40. package/src/components/text-field/TextField.test.tsx +8 -9
  41. package/src/components/thumbnail/Thumbnail.test.tsx +5 -29
  42. package/src/components/thumbnail/Thumbnail.tsx +11 -11
  43. package/src/components/tooltip/Tooltip.test.tsx +8 -14
  44. package/src/components/uploader/Uploader.test.tsx +2 -2
  45. package/src/components/user-block/UserBlock.test.tsx +1 -1
  46. package/src/untypped-modules.d.ts +0 -4
  47. package/src/utils/Portal/PortalProvider.test.tsx +1 -1
  48. package/src/utils/date/getYearDisplayName.test.ts +1 -1
  49. package/src/utils/disabled/useDisableStateProps.test.tsx +2 -2
  50. package/src/utils/react/renderButtonOrLink.tsx +16 -0
  51. package/src/utils/type/index.ts +0 -1
  52. package/utils/index.d.ts +1 -1
  53. package/utils/index.js +1 -1
  54. package/src/utils/react/RawClickable.test.tsx +0 -153
  55. package/src/utils/react/RawClickable.tsx +0 -65
  56. package/src/utils/type/HasRequiredLinkHref.ts +0 -1
@@ -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;
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
 
3
3
  import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
4
- import { render, within, screen, waitFor } from '@testing-library/react';
4
+ import { render, within, screen } from '@testing-library/react';
5
5
  import { getByClassName, queryByClassName } from '@lumx/react/testing/utils/queries';
6
6
  import userEvent from '@testing-library/user-event';
7
7
  import { useImageSize } from '@lumx/react/hooks/useImageSize';
@@ -18,8 +18,8 @@ import Meta, {
18
18
  WithMosaicTrigger,
19
19
  } from './ImageLightbox.stories';
20
20
 
21
- vi.mock('@lumx/react/hooks/useImageSize');
22
- vi.mock('@lumx/react/hooks/useSizeOnWindowResize');
21
+ jest.mock('@lumx/react/hooks/useImageSize');
22
+ jest.mock('@lumx/react/hooks/useSizeOnWindowResize');
23
23
 
24
24
  const CLASSNAME = ImageLightbox.className as string;
25
25
  const baseProps = Meta.args;
@@ -56,7 +56,7 @@ const queries = {
56
56
  describe(`<${ImageLightbox.displayName}>`, () => {
57
57
  beforeEach(() => {
58
58
  (useImageSize as any).mockReturnValue(null);
59
- (useSizeOnWindowResize as any).mockReturnValue([null, vi.fn()]);
59
+ (useSizeOnWindowResize as any).mockReturnValue([null, jest.fn()]);
60
60
  });
61
61
 
62
62
  describe('render', () => {
@@ -159,9 +159,7 @@ describe(`<${ImageLightbox.displayName}>`, () => {
159
159
 
160
160
  // Close on escape
161
161
  await userEvent.keyboard('{escape}');
162
- await waitFor(() => {
163
- expect(imageLightbox).not.toBeInTheDocument();
164
- });
162
+ expect(imageLightbox).not.toBeInTheDocument();
165
163
 
166
164
  // Focus moved back to the trigger button
167
165
  expect(buttonTrigger).toHaveFocus();
@@ -187,9 +185,7 @@ describe(`<${ImageLightbox.displayName}>`, () => {
187
185
 
188
186
  // Close on escape
189
187
  await userEvent.keyboard('{escape}');
190
- await waitFor(() => {
191
- expect(imageLightbox).not.toBeInTheDocument();
192
- });
188
+ expect(imageLightbox).not.toBeInTheDocument();
193
189
 
194
190
  // Focus moved back to the trigger button
195
191
  expect(buttonTrigger).toHaveFocus();
@@ -200,7 +196,7 @@ describe(`<${ImageLightbox.displayName}>`, () => {
200
196
  const scrollAreaSize = { width: 600, height: 600 };
201
197
  beforeEach(() => {
202
198
  (useImageSize as any).mockImplementation((_: any, getInitialSize: any) => getInitialSize?.() || null);
203
- (useSizeOnWindowResize as any).mockReturnValue([scrollAreaSize, vi.fn()]);
199
+ (useSizeOnWindowResize as any).mockReturnValue([scrollAreaSize, jest.fn()]);
204
200
  });
205
201
 
206
202
  it('should use the image initial size', () => {
@@ -51,7 +51,7 @@ describe(`<${Link.displayName}>`, () => {
51
51
 
52
52
  it('should render a button', () => {
53
53
  const name = 'Link';
54
- const onClick = vi.fn();
54
+ const onClick = jest.fn();
55
55
  const { link } = setup({ onClick, children: name });
56
56
  expect(link).toBe(screen.queryByRole('button', { name }));
57
57
  });
@@ -75,7 +75,7 @@ describe(`<${Link.displayName}>`, () => {
75
75
 
76
76
  describe('Disabled state', () => {
77
77
  it('should render disabled button', async () => {
78
- const onClick = vi.fn();
78
+ const onClick = jest.fn();
79
79
  const { link } = setup({ children: 'Label', isDisabled: true, onClick });
80
80
  expect(link).toHaveAttribute('disabled');
81
81
  await userEvent.click(link);
@@ -83,28 +83,25 @@ describe(`<${Link.displayName}>`, () => {
83
83
  });
84
84
 
85
85
  it('should render disabled link', async () => {
86
- const onClick = vi.fn();
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
  });
95
94
 
96
95
  it('should render aria-disabled button', async () => {
97
- const onClick = vi.fn();
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
  });
105
102
 
106
103
  it('should render aria-disabled link', async () => {
107
- const onClick = vi.fn();
104
+ const onClick = jest.fn();
108
105
  const { link } = setup({
109
106
  children: 'Label',
110
107
  'aria-disabled': true,
@@ -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;
@@ -29,7 +29,7 @@ describe(`<${ListItem.displayName}>`, () => {
29
29
  });
30
30
 
31
31
  it('should render as a button', () => {
32
- setup({ children: 'Label', onItemSelected: vi.fn() });
32
+ setup({ children: 'Label', onItemSelected: jest.fn() });
33
33
  expect(screen.getByRole('button', { name: 'Label' })).toBeInTheDocument();
34
34
  });
35
35
 
@@ -41,7 +41,7 @@ describe(`<${ListItem.displayName}>`, () => {
41
41
 
42
42
  describe('Disabled state', () => {
43
43
  it('should render disabled list item button', async () => {
44
- const onItemSelected = vi.fn();
44
+ const onItemSelected = jest.fn();
45
45
  const { link } = setup({ children: 'Label', isDisabled: true, onItemSelected });
46
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.
@@ -50,7 +50,7 @@ describe(`<${ListItem.displayName}>`, () => {
50
50
  });
51
51
 
52
52
  it('should render disabled list item link', async () => {
53
- const onItemSelected = vi.fn();
53
+ const onItemSelected = jest.fn();
54
54
  const { link } = setup({
55
55
  children: 'Label',
56
56
  isDisabled: true,
@@ -64,7 +64,7 @@ describe(`<${ListItem.displayName}>`, () => {
64
64
  });
65
65
 
66
66
  it('should render aria-disabled list item button', async () => {
67
- const onItemSelected = vi.fn();
67
+ const onItemSelected = jest.fn();
68
68
  const { link } = setup({ children: 'Label', 'aria-disabled': true, onItemSelected });
69
69
  expect(link).toHaveAttribute('aria-disabled', 'true');
70
70
  if (link) await userEvent.click(link);
@@ -72,7 +72,7 @@ describe(`<${ListItem.displayName}>`, () => {
72
72
  });
73
73
 
74
74
  it('should render aria-disabled list item link', async () => {
75
- const onItemSelected = vi.fn();
75
+ const onItemSelected = jest.fn();
76
76
  const { link } = setup({
77
77
  children: 'Label',
78
78
  'aria-disabled': true,
@@ -54,7 +54,7 @@ describe(`<${Message.displayName}>`, () => {
54
54
  });
55
55
 
56
56
  it('should render close button', async () => {
57
- const onClick = vi.fn();
57
+ const onClick = jest.fn();
58
58
  const { closeButton } = setup({
59
59
  hasBackground: true,
60
60
  kind: 'info',
@@ -32,7 +32,7 @@ describe(`<${Mosaic.displayName}>`, () => {
32
32
  expect(mosaic).toHaveClass(`${CLASSNAME}--has-${count}-thumbnail${count > 1 ? 's' : ''}`);
33
33
  expect(thumbnails.length).toBe(count);
34
34
  for (const thumbnail of thumbnails) {
35
- expect(within(thumbnail).queryByAltText('')).toBeInTheDocument();
35
+ expect(within(thumbnail).queryByRole('img')).toBeInTheDocument();
36
36
  }
37
37
  });
38
38
 
@@ -47,8 +47,8 @@ describe(`<${Mosaic.displayName}>`, () => {
47
47
  });
48
48
 
49
49
  it('should render clickable', async () => {
50
- const onClick = vi.fn();
51
- const onImageClick = vi.fn();
50
+ const onClick = jest.fn();
51
+ const onImageClick = jest.fn();
52
52
  const { thumbnails } = setup({
53
53
  thumbnails: generateThumbnails(6),
54
54
  onImageClick,
@@ -1,12 +1,11 @@
1
1
  import React, { ElementType, ReactNode } from 'react';
2
2
  import { Icon, Placement, Size, Tooltip, Text } from '@lumx/react';
3
3
  import { getRootClassName, handleBasicClasses } from '@lumx/core/js/utils/className';
4
- import { ComponentRef, HasClassName, HasPolymorphicAs, HasRequiredLinkHref, HasTheme } from '@lumx/react/utils/type';
4
+ import { ComponentRef, HasClassName, HasPolymorphicAs, HasTheme } from '@lumx/react/utils/type';
5
5
  import classNames from 'classnames';
6
6
  import { forwardRefPolymorphic } from '@lumx/react/utils/react/forwardRefPolymorphic';
7
7
  import { useTheme } from '@lumx/react/utils/theme/ThemeContext';
8
8
  import { useOverflowTooltipLabel } from '@lumx/react/hooks/useOverflowTooltipLabel';
9
- import { RawClickable } from '@lumx/react/utils/react/RawClickable';
10
9
 
11
10
  type BaseNavigationItemProps = {
12
11
  /** Icon (SVG path). */
@@ -17,6 +16,9 @@ type BaseNavigationItemProps = {
17
16
  isCurrentPage?: boolean;
18
17
  };
19
18
 
19
+ /** Make `href` required when `as` is `a` */
20
+ type RequiredLinkHref<E> = E extends 'a' ? { href: string } : Record<string, unknown>;
21
+
20
22
  /**
21
23
  * Navigation item props
22
24
  */
@@ -24,7 +26,7 @@ export type NavigationItemProps<E extends ElementType = 'a'> = HasPolymorphicAs<
24
26
  HasTheme &
25
27
  HasClassName &
26
28
  BaseNavigationItemProps &
27
- HasRequiredLinkHref<E>;
29
+ RequiredLinkHref<E>;
28
30
 
29
31
  /**
30
32
  * Component display name.
@@ -42,6 +44,8 @@ export const NavigationItem = Object.assign(
42
44
  const theme = useTheme();
43
45
  const { tooltipLabel, labelRef } = useOverflowTooltipLabel(label);
44
46
 
47
+ const buttonProps = Element === 'button' ? { type: 'button' } : {};
48
+
45
49
  return (
46
50
  <li
47
51
  className={classNames(
@@ -53,14 +57,14 @@ export const NavigationItem = Object.assign(
53
57
  )}
54
58
  >
55
59
  <Tooltip label={tooltipLabel} placement={Placement.TOP}>
56
- <RawClickable
57
- as={Element}
60
+ <Element
58
61
  className={handleBasicClasses({
59
62
  prefix: `${CLASSNAME}__link`,
60
63
  isSelected: isCurrentPage,
61
64
  })}
62
65
  ref={ref as React.Ref<any>}
63
66
  aria-current={isCurrentPage ? 'page' : undefined}
67
+ {...buttonProps}
64
68
  {...forwardedProps}
65
69
  >
66
70
  {icon ? (
@@ -70,7 +74,7 @@ export const NavigationItem = Object.assign(
70
74
  <Text as="span" truncate className={`${CLASSNAME}__label`} ref={labelRef}>
71
75
  {label}
72
76
  </Text>
73
- </RawClickable>
77
+ </Element>
74
78
  </Tooltip>
75
79
  </li>
76
80
  );
@@ -9,7 +9,6 @@ import { ThemeProvider, useTheme } from '@lumx/react/utils/theme/ThemeContext';
9
9
  import { useId } from '@lumx/react/hooks/useId';
10
10
  import { forwardRef } from '@lumx/react/utils/react/forwardRef';
11
11
 
12
- import { RawClickable } from '@lumx/react/utils/react/RawClickable';
13
12
  import { CLASSNAME as ITEM_CLASSNAME } from './NavigationItem';
14
13
  import { NavigationContext } from './context';
15
14
 
@@ -53,8 +52,7 @@ export const NavigationSection = forwardRef<NavigationSectionProps, HTMLLIElemen
53
52
  )}
54
53
  ref={ref}
55
54
  >
56
- <RawClickable<'button'>
57
- as="button"
55
+ <button
58
56
  {...forwardedProps}
59
57
  aria-controls={sectionId}
60
58
  aria-expanded={isOpen}
@@ -64,6 +62,7 @@ export const NavigationSection = forwardRef<NavigationSectionProps, HTMLLIElemen
64
62
  setIsOpen(!isOpen);
65
63
  event.stopPropagation();
66
64
  }}
65
+ type="button"
67
66
  >
68
67
  {icon ? <Icon className={`${ITEM_CLASSNAME}__icon`} icon={icon} size={Size.xs} /> : null}
69
68
 
@@ -74,7 +73,7 @@ export const NavigationSection = forwardRef<NavigationSectionProps, HTMLLIElemen
74
73
  className={classNames(`${ITEM_CLASSNAME}__icon`, `${CLASSNAME}__chevron`)}
75
74
  icon={isOpen ? mdiChevronUp : mdiChevronDown}
76
75
  />
77
- </RawClickable>
76
+ </button>
78
77
  {isOpen &&
79
78
  (isDropdown ? (
80
79
  <Popover
@@ -44,8 +44,8 @@ describe(`<${Notification.displayName}>`, () => {
44
44
  });
45
45
 
46
46
  it('should render content & action', async () => {
47
- const onClick = vi.fn();
48
- const onActionClick = vi.fn();
47
+ const onClick = jest.fn();
48
+ const onActionClick = jest.fn();
49
49
  const content = 'Content';
50
50
  const actionLabel = 'actionLabel';
51
51
  const { notification, action, actionButton } = setup({ content, actionLabel, onClick, onActionClick });
@@ -81,9 +81,8 @@ describe(`<${Notification.displayName}>`, () => {
81
81
 
82
82
  it('should forward styles', () => {
83
83
  const { notification } = setup({ style: { color: 'red' } });
84
-
85
84
  expect(notification).toBeInTheDocument();
86
- expect(notification).toHaveStyle('color: rgb(255, 0, 0)');
85
+ expect(notification).toHaveStyle('color: red');
87
86
  });
88
87
 
89
88
  // Common tests suite.
@@ -6,7 +6,7 @@ import { Heading, HeadingLevelProvider } from '@lumx/react';
6
6
  import { WithButtonTrigger, WithIconButtonTrigger } from './PopoverDialog.stories';
7
7
  import { PopoverDialog } from './PopoverDialog';
8
8
 
9
- vi.mock('@lumx/react/utils/browser/isFocusVisible');
9
+ jest.mock('@lumx/react/utils/browser/isFocusVisible');
10
10
 
11
11
  describe(`<${PopoverDialog.displayName}>`, () => {
12
12
  it('should open and init focus', async () => {
@@ -86,7 +86,7 @@ describe(`<${RadioButton.displayName}>`, () => {
86
86
  });
87
87
 
88
88
  describe('Events', () => {
89
- const onChange = vi.fn();
89
+ const onChange = jest.fn();
90
90
 
91
91
  it('should trigger `onChange` when radioButton is clicked', async () => {
92
92
  const value = 'value';
@@ -102,7 +102,7 @@ describe(`<${RadioButton.displayName}>`, () => {
102
102
 
103
103
  describe('Disabled state', () => {
104
104
  it('should be disabled with isDisabled', async () => {
105
- const onChange = vi.fn();
105
+ const onChange = jest.fn();
106
106
  const { radioButton, input } = setup({ isDisabled: true, onChange });
107
107
 
108
108
  expect(radioButton).toHaveClass('lumx-radio-button--is-disabled');
@@ -115,7 +115,7 @@ describe(`<${RadioButton.displayName}>`, () => {
115
115
  });
116
116
 
117
117
  it('should be disabled with aria-disabled', async () => {
118
- const onChange = vi.fn();
118
+ const onChange = jest.fn();
119
119
  const { radioButton, input } = setup({ 'aria-disabled': true, onChange });
120
120
 
121
121
  expect(radioButton).toHaveClass('lumx-radio-button--is-disabled');