@lumx/react 3.19.1-alpha.7 → 3.19.1-alpha.9

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 +20 -13
  2. package/_internal/index.js.map +1 -1
  3. package/index.d.ts +5 -6
  4. package/index.js +2400 -2440
  5. package/index.js.map +1 -1
  6. package/package.json +10 -13
  7. package/src/components/alert-dialog/AlertDialog.test.tsx +3 -2
  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 +6 -36
  11. package/src/components/checkbox/Checkbox.test.tsx +3 -3
  12. package/src/components/chip/Chip.test.tsx +19 -17
  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 +6 -5
  19. package/src/components/icon/Icon.stories.tsx +30 -4
  20. package/src/components/icon/Icon.test.tsx +85 -2
  21. package/src/components/icon/Icon.tsx +118 -7
  22. package/src/components/image-lightbox/ImageLightbox.test.tsx +11 -7
  23. package/src/components/link/Link.test.tsx +13 -11
  24. package/src/components/link/Link.tsx +9 -20
  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 +6 -10
  29. package/src/components/navigation/NavigationSection.tsx +4 -3
  30. package/src/components/notification/Notification.test.tsx +4 -3
  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 +8 -7
  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 +22 -27
  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 +9 -8
  41. package/src/components/thumbnail/Thumbnail.test.tsx +29 -5
  42. package/src/components/thumbnail/Thumbnail.tsx +11 -11
  43. package/src/components/tooltip/Tooltip.test.tsx +14 -8
  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 +4 -0
  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/RawClickable.test.tsx +153 -0
  51. package/src/utils/react/RawClickable.tsx +65 -0
  52. package/src/utils/type/HasRequiredLinkHref.ts +1 -0
  53. package/src/utils/type/index.ts +1 -0
  54. package/utils/index.d.ts +1 -1
  55. package/utils/index.js +1 -1
  56. package/src/utils/react/renderButtonOrLink.tsx +0 -16
@@ -1,10 +1,11 @@
1
1
  import React from 'react';
2
2
 
3
+ import { mdiAlertCircle } from '@lumx/icons';
4
+ import { ColorPalette, ColorVariant, Size, Theme } from '@lumx/react';
3
5
  import { commonTestsSuiteRTL, SetupRenderOptions } from '@lumx/react/testing/utils';
4
6
 
5
7
  import { getByClassName, getByTagName } from '@lumx/react/testing/utils/queries';
6
8
  import { render } from '@testing-library/react';
7
- import Tests from '@lumx/core/js/components/Icon/Tests';
8
9
  import { Icon, IconProps } from './Icon';
9
10
 
10
11
  const CLASSNAME = Icon.className as string;
@@ -28,7 +29,89 @@ const setup = (propsOverride: SetupProps = {}, { wrapper }: SetupRenderOptions =
28
29
  };
29
30
 
30
31
  describe(`<${Icon.displayName}>`, () => {
31
- Tests((props: IconProps, { wrapper }: any) => render(<Icon {...props} />, { wrapper }));
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
+ });
32
115
 
33
116
  // Common tests suite.
34
117
  commonTestsSuiteRTL(setup, {
@@ -1,11 +1,52 @@
1
1
  import React from 'react';
2
2
 
3
- import { Icon as UI, IconProps, IconSizes } from '@lumx/core/js/components/Icon';
3
+ import classNames from 'classnames';
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';
5
9
  import { forwardRef } from '@lumx/react/utils/react/forwardRef';
6
10
  import { useTheme } from '@lumx/react/utils/theme/ThemeContext';
7
11
 
8
- export type { IconProps, IconSizes };
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
+
9
50
  /**
10
51
  * Icon component.
11
52
  *
@@ -15,10 +56,80 @@ export type { IconProps, IconSizes };
15
56
  */
16
57
  export const Icon = forwardRef<IconProps, HTMLElement>((props, ref) => {
17
58
  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);
18
71
 
19
- return <UI ref={ref} {...props} theme={props.theme || defaultTheme} />;
20
- });
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
+ }
21
83
 
22
- Icon.displayName = UI.displayName;
23
- Icon.className = UI.className;
24
- Icon.defaultProps = UI.defaultProps;
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
+ );
132
+ });
133
+ Icon.displayName = COMPONENT_NAME;
134
+ Icon.className = CLASSNAME;
135
+ Icon.defaultProps = DEFAULT_PROPS;
@@ -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 } from '@testing-library/react';
4
+ import { render, within, screen, waitFor } 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
- jest.mock('@lumx/react/hooks/useImageSize');
22
- jest.mock('@lumx/react/hooks/useSizeOnWindowResize');
21
+ vi.mock('@lumx/react/hooks/useImageSize');
22
+ vi.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, jest.fn()]);
59
+ (useSizeOnWindowResize as any).mockReturnValue([null, vi.fn()]);
60
60
  });
61
61
 
62
62
  describe('render', () => {
@@ -159,7 +159,9 @@ describe(`<${ImageLightbox.displayName}>`, () => {
159
159
 
160
160
  // Close on escape
161
161
  await userEvent.keyboard('{escape}');
162
- expect(imageLightbox).not.toBeInTheDocument();
162
+ await waitFor(() => {
163
+ expect(imageLightbox).not.toBeInTheDocument();
164
+ });
163
165
 
164
166
  // Focus moved back to the trigger button
165
167
  expect(buttonTrigger).toHaveFocus();
@@ -185,7 +187,9 @@ describe(`<${ImageLightbox.displayName}>`, () => {
185
187
 
186
188
  // Close on escape
187
189
  await userEvent.keyboard('{escape}');
188
- expect(imageLightbox).not.toBeInTheDocument();
190
+ await waitFor(() => {
191
+ expect(imageLightbox).not.toBeInTheDocument();
192
+ });
189
193
 
190
194
  // Focus moved back to the trigger button
191
195
  expect(buttonTrigger).toHaveFocus();
@@ -196,7 +200,7 @@ describe(`<${ImageLightbox.displayName}>`, () => {
196
200
  const scrollAreaSize = { width: 600, height: 600 };
197
201
  beforeEach(() => {
198
202
  (useImageSize as any).mockImplementation((_: any, getInitialSize: any) => getInitialSize?.() || null);
199
- (useSizeOnWindowResize as any).mockReturnValue([scrollAreaSize, jest.fn()]);
203
+ (useSizeOnWindowResize as any).mockReturnValue([scrollAreaSize, vi.fn()]);
200
204
  });
201
205
 
202
206
  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 = jest.fn();
54
+ const onClick = vi.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 = jest.fn();
78
+ const onClick = vi.fn();
79
79
  const { link } = setup({ children: 'Label', isDisabled: true, onClick });
80
80
  expect(link).toHaveAttribute('disabled');
81
81
  await userEvent.click(link);
@@ -83,25 +83,28 @@ describe(`<${Link.displayName}>`, () => {
83
83
  });
84
84
 
85
85
  it('should render disabled link', async () => {
86
- const onClick = jest.fn();
86
+ const onClick = vi.fn();
87
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');
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');
91
92
  await userEvent.click(link);
92
93
  expect(onClick).not.toHaveBeenCalled();
93
94
  });
94
95
 
95
96
  it('should render aria-disabled button', async () => {
96
- const onClick = jest.fn();
97
+ const onClick = vi.fn();
97
98
  const { link } = setup({ children: 'Label', 'aria-disabled': true, onClick });
98
- expect(link).toHaveAttribute('aria-disabled');
99
+ expect(screen.queryByRole('button')).toBeInTheDocument();
100
+ expect(link).toHaveAttribute('aria-disabled', 'true');
101
+ expect(link).not.toHaveAttribute('tabindex');
99
102
  await userEvent.click(link);
100
103
  expect(onClick).not.toHaveBeenCalled();
101
104
  });
102
105
 
103
106
  it('should render aria-disabled link', async () => {
104
- const onClick = jest.fn();
107
+ const onClick = vi.fn();
105
108
  const { link } = setup({
106
109
  children: 'Label',
107
110
  'aria-disabled': true,
@@ -109,8 +112,7 @@ describe(`<${Link.displayName}>`, () => {
109
112
  onClick,
110
113
  });
111
114
  expect(link).toHaveAccessibleName('Label');
112
- // Disabled link do not exist so we fallback to a button
113
- expect(screen.queryByRole('link')).not.toBeInTheDocument();
115
+ expect(screen.queryByRole('link')).toBeInTheDocument();
114
116
  expect(link).toHaveAttribute('aria-disabled', 'true');
115
117
  await userEvent.click(link);
116
118
  expect(onClick).not.toHaveBeenCalled();
@@ -12,8 +12,9 @@ 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';
16
15
  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';
17
18
 
18
19
  type HTMLAnchorProps = React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
19
20
 
@@ -67,38 +68,26 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
67
68
  * @return React element.
68
69
  */
69
70
  export const Link = forwardRef<LinkProps, HTMLAnchorElement | HTMLButtonElement>((props, ref) => {
70
- const { isAnyDisabled, disabledStateProps, otherProps } = useDisableStateProps(props);
71
+ const { disabledStateProps, otherProps } = useDisableStateProps(props);
71
72
  const {
72
73
  children,
73
74
  className,
74
75
  color: propColor,
75
76
  colorVariant: propColorVariant,
76
- href,
77
77
  leftIcon,
78
- linkAs,
79
78
  rightIcon,
80
- target,
81
79
  typography,
80
+ linkAs,
82
81
  ...forwardedProps
83
82
  } = otherProps;
84
83
  const [color, colorVariant] = resolveColorWithVariants(propColor, propColorVariant);
85
84
 
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
-
97
85
  return (
98
- <Component
99
- ref={ref}
86
+ <RawClickable
87
+ ref={ref as any}
88
+ as={linkAs || (forwardedProps.href ? 'a' : 'button')}
100
89
  {...forwardedProps}
101
- {...baseProps}
90
+ {...disabledStateProps}
102
91
  className={classNames(
103
92
  className,
104
93
  handleBasicClasses({ prefix: CLASSNAME, color, colorVariant, hasTypography: !!typography }),
@@ -112,7 +101,7 @@ export const Link = forwardRef<LinkProps, HTMLAnchorElement | HTMLButtonElement>
112
101
  {rightIcon && <Icon icon={rightIcon} className={`${CLASSNAME}__right-icon`} />}
113
102
  </>,
114
103
  )}
115
- </Component>
104
+ </RawClickable>
116
105
  );
117
106
  });
118
107
  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: jest.fn() });
32
+ setup({ children: 'Label', onItemSelected: vi.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 = jest.fn();
44
+ const onItemSelected = vi.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 = jest.fn();
53
+ const onItemSelected = vi.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 = jest.fn();
67
+ const onItemSelected = vi.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 = jest.fn();
75
+ const onItemSelected = vi.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 = jest.fn();
57
+ const onClick = vi.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).queryByRole('img')).toBeInTheDocument();
35
+ expect(within(thumbnail).queryByAltText('')).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 = jest.fn();
51
- const onImageClick = jest.fn();
50
+ const onClick = vi.fn();
51
+ const onImageClick = vi.fn();
52
52
  const { thumbnails } = setup({
53
53
  thumbnails: generateThumbnails(6),
54
54
  onImageClick,
@@ -1,11 +1,12 @@
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, HasTheme } from '@lumx/react/utils/type';
4
+ import { ComponentRef, HasClassName, HasPolymorphicAs, HasRequiredLinkHref, 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';
9
10
 
10
11
  type BaseNavigationItemProps = {
11
12
  /** Icon (SVG path). */
@@ -16,9 +17,6 @@ type BaseNavigationItemProps = {
16
17
  isCurrentPage?: boolean;
17
18
  };
18
19
 
19
- /** Make `href` required when `as` is `a` */
20
- type RequiredLinkHref<E> = E extends 'a' ? { href: string } : Record<string, unknown>;
21
-
22
20
  /**
23
21
  * Navigation item props
24
22
  */
@@ -26,7 +24,7 @@ export type NavigationItemProps<E extends ElementType = 'a'> = HasPolymorphicAs<
26
24
  HasTheme &
27
25
  HasClassName &
28
26
  BaseNavigationItemProps &
29
- RequiredLinkHref<E>;
27
+ HasRequiredLinkHref<E>;
30
28
 
31
29
  /**
32
30
  * Component display name.
@@ -44,8 +42,6 @@ export const NavigationItem = Object.assign(
44
42
  const theme = useTheme();
45
43
  const { tooltipLabel, labelRef } = useOverflowTooltipLabel(label);
46
44
 
47
- const buttonProps = Element === 'button' ? { type: 'button' } : {};
48
-
49
45
  return (
50
46
  <li
51
47
  className={classNames(
@@ -57,14 +53,14 @@ export const NavigationItem = Object.assign(
57
53
  )}
58
54
  >
59
55
  <Tooltip label={tooltipLabel} placement={Placement.TOP}>
60
- <Element
56
+ <RawClickable
57
+ as={Element}
61
58
  className={handleBasicClasses({
62
59
  prefix: `${CLASSNAME}__link`,
63
60
  isSelected: isCurrentPage,
64
61
  })}
65
62
  ref={ref as React.Ref<any>}
66
63
  aria-current={isCurrentPage ? 'page' : undefined}
67
- {...buttonProps}
68
64
  {...forwardedProps}
69
65
  >
70
66
  {icon ? (
@@ -74,7 +70,7 @@ export const NavigationItem = Object.assign(
74
70
  <Text as="span" truncate className={`${CLASSNAME}__label`} ref={labelRef}>
75
71
  {label}
76
72
  </Text>
77
- </Element>
73
+ </RawClickable>
78
74
  </Tooltip>
79
75
  </li>
80
76
  );
@@ -9,6 +9,7 @@ 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';
12
13
  import { CLASSNAME as ITEM_CLASSNAME } from './NavigationItem';
13
14
  import { NavigationContext } from './context';
14
15
 
@@ -52,7 +53,8 @@ export const NavigationSection = forwardRef<NavigationSectionProps, HTMLLIElemen
52
53
  )}
53
54
  ref={ref}
54
55
  >
55
- <button
56
+ <RawClickable<'button'>
57
+ as="button"
56
58
  {...forwardedProps}
57
59
  aria-controls={sectionId}
58
60
  aria-expanded={isOpen}
@@ -62,7 +64,6 @@ export const NavigationSection = forwardRef<NavigationSectionProps, HTMLLIElemen
62
64
  setIsOpen(!isOpen);
63
65
  event.stopPropagation();
64
66
  }}
65
- type="button"
66
67
  >
67
68
  {icon ? <Icon className={`${ITEM_CLASSNAME}__icon`} icon={icon} size={Size.xs} /> : null}
68
69
 
@@ -73,7 +74,7 @@ export const NavigationSection = forwardRef<NavigationSectionProps, HTMLLIElemen
73
74
  className={classNames(`${ITEM_CLASSNAME}__icon`, `${CLASSNAME}__chevron`)}
74
75
  icon={isOpen ? mdiChevronUp : mdiChevronDown}
75
76
  />
76
- </button>
77
+ </RawClickable>
77
78
  {isOpen &&
78
79
  (isDropdown ? (
79
80
  <Popover
@@ -44,8 +44,8 @@ describe(`<${Notification.displayName}>`, () => {
44
44
  });
45
45
 
46
46
  it('should render content & action', async () => {
47
- const onClick = jest.fn();
48
- const onActionClick = jest.fn();
47
+ const onClick = vi.fn();
48
+ const onActionClick = vi.fn();
49
49
  const content = 'Content';
50
50
  const actionLabel = 'actionLabel';
51
51
  const { notification, action, actionButton } = setup({ content, actionLabel, onClick, onActionClick });
@@ -81,8 +81,9 @@ describe(`<${Notification.displayName}>`, () => {
81
81
 
82
82
  it('should forward styles', () => {
83
83
  const { notification } = setup({ style: { color: 'red' } });
84
+
84
85
  expect(notification).toBeInTheDocument();
85
- expect(notification).toHaveStyle('color: red');
86
+ expect(notification).toHaveStyle('color: rgb(255, 0, 0)');
86
87
  });
87
88
 
88
89
  // 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
- jest.mock('@lumx/react/utils/browser/isFocusVisible');
9
+ vi.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 = jest.fn();
89
+ const onChange = vi.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 = jest.fn();
105
+ const onChange = vi.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 = jest.fn();
118
+ const onChange = vi.fn();
119
119
  const { radioButton, input } = setup({ 'aria-disabled': true, onChange });
120
120
 
121
121
  expect(radioButton).toHaveClass('lumx-radio-button--is-disabled');