@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.
- package/_internal/index.js +20 -13
- package/_internal/index.js.map +1 -1
- package/index.d.ts +5 -6
- package/index.js +2400 -2440
- package/index.js.map +1 -1
- package/package.json +10 -13
- package/src/components/alert-dialog/AlertDialog.test.tsx +3 -2
- package/src/components/autocomplete/Autocomplete.test.tsx +3 -3
- package/src/components/button/Button.test.tsx +9 -9
- package/src/components/button/ButtonRoot.tsx +6 -36
- package/src/components/checkbox/Checkbox.test.tsx +3 -3
- package/src/components/chip/Chip.test.tsx +19 -17
- package/src/components/date-picker/DatePicker.test.tsx +3 -3
- package/src/components/date-picker/DatePickerControlled.test.tsx +6 -6
- package/src/components/date-picker/DatePickerField.test.tsx +3 -3
- package/src/components/dialog/Dialog.test.tsx +4 -4
- package/src/components/dropdown/Dropdown.test.tsx +3 -3
- package/src/components/expansion-panel/ExpansionPanel.test.tsx +6 -5
- package/src/components/icon/Icon.stories.tsx +30 -4
- package/src/components/icon/Icon.test.tsx +85 -2
- package/src/components/icon/Icon.tsx +118 -7
- package/src/components/image-lightbox/ImageLightbox.test.tsx +11 -7
- package/src/components/link/Link.test.tsx +13 -11
- package/src/components/link/Link.tsx +9 -20
- package/src/components/list/ListItem.test.tsx +5 -5
- package/src/components/message/Message.test.tsx +1 -1
- package/src/components/mosaic/Mosaic.test.tsx +3 -3
- package/src/components/navigation/NavigationItem.tsx +6 -10
- package/src/components/navigation/NavigationSection.tsx +4 -3
- package/src/components/notification/Notification.test.tsx +4 -3
- package/src/components/popover-dialog/PopoverDialog.test.tsx +1 -1
- package/src/components/radio-button/RadioButton.test.tsx +3 -3
- package/src/components/select/Select.test.tsx +8 -7
- package/src/components/select/SelectMultiple.test.tsx +5 -5
- package/src/components/side-navigation/SideNavigationItem.test.tsx +2 -2
- package/src/components/side-navigation/SideNavigationItem.tsx +22 -27
- package/src/components/slider/Slider.test.tsx +1 -1
- package/src/components/switch/Switch.test.tsx +5 -5
- package/src/components/table/TableCell.test.tsx +1 -1
- package/src/components/text-field/TextField.test.tsx +9 -8
- package/src/components/thumbnail/Thumbnail.test.tsx +29 -5
- package/src/components/thumbnail/Thumbnail.tsx +11 -11
- package/src/components/tooltip/Tooltip.test.tsx +14 -8
- package/src/components/uploader/Uploader.test.tsx +2 -2
- package/src/components/user-block/UserBlock.test.tsx +1 -1
- package/src/untypped-modules.d.ts +4 -0
- package/src/utils/Portal/PortalProvider.test.tsx +1 -1
- package/src/utils/date/getYearDisplayName.test.ts +1 -1
- package/src/utils/disabled/useDisableStateProps.test.tsx +2 -2
- package/src/utils/react/RawClickable.test.tsx +153 -0
- package/src/utils/react/RawClickable.tsx +65 -0
- package/src/utils/type/HasRequiredLinkHref.ts +1 -0
- package/src/utils/type/index.ts +1 -0
- package/utils/index.d.ts +1 -1
- package/utils/index.js +1 -1
- 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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
86
|
+
const onClick = vi.fn();
|
|
87
87
|
const { link } = setup({ children: 'Label', isDisabled: true, href: 'https://example.com', onClick });
|
|
88
|
-
|
|
89
|
-
expect(
|
|
90
|
-
|
|
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 =
|
|
97
|
+
const onClick = vi.fn();
|
|
97
98
|
const { link } = setup({ children: 'Label', 'aria-disabled': true, onClick });
|
|
98
|
-
expect(
|
|
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 =
|
|
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
|
-
|
|
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 {
|
|
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
|
-
<
|
|
99
|
-
ref={ref}
|
|
86
|
+
<RawClickable
|
|
87
|
+
ref={ref as any}
|
|
88
|
+
as={linkAs || (forwardedProps.href ? 'a' : 'button')}
|
|
100
89
|
{...forwardedProps}
|
|
101
|
-
{...
|
|
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
|
-
</
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
75
|
+
const onItemSelected = vi.fn();
|
|
76
76
|
const { link } = setup({
|
|
77
77
|
children: 'Label',
|
|
78
78
|
'aria-disabled': true,
|
|
@@ -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).
|
|
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 =
|
|
51
|
-
const onImageClick =
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
</
|
|
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 =
|
|
48
|
-
const onActionClick =
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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');
|