@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.
- package/_internal/index.js +13 -20
- package/_internal/index.js.map +1 -1
- package/index.d.ts +6 -5
- package/index.js +2439 -2399
- package/index.js.map +1 -1
- package/package.json +13 -10
- package/src/components/alert-dialog/AlertDialog.test.tsx +2 -3
- package/src/components/autocomplete/Autocomplete.test.tsx +3 -3
- package/src/components/button/Button.test.tsx +9 -9
- package/src/components/button/ButtonRoot.tsx +36 -6
- package/src/components/checkbox/Checkbox.test.tsx +3 -3
- package/src/components/chip/Chip.test.tsx +17 -19
- 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 +5 -6
- package/src/components/icon/Icon.stories.tsx +4 -30
- package/src/components/icon/Icon.test.tsx +2 -85
- package/src/components/icon/Icon.tsx +7 -118
- package/src/components/image-lightbox/ImageLightbox.test.tsx +7 -11
- package/src/components/link/Link.test.tsx +11 -13
- package/src/components/link/Link.tsx +20 -9
- 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 +10 -6
- package/src/components/navigation/NavigationSection.tsx +3 -4
- package/src/components/notification/Notification.test.tsx +3 -4
- 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 +7 -8
- 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 +27 -22
- 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 +8 -9
- package/src/components/thumbnail/Thumbnail.test.tsx +5 -29
- package/src/components/thumbnail/Thumbnail.tsx +11 -11
- package/src/components/tooltip/Tooltip.test.tsx +8 -14
- 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 +0 -4
- 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/renderButtonOrLink.tsx +16 -0
- package/src/utils/type/index.ts +0 -1
- package/utils/index.d.ts +1 -1
- package/utils/index.js +1 -1
- package/src/utils/react/RawClickable.test.tsx +0 -153
- package/src/utils/react/RawClickable.tsx +0 -65
- 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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
Icon.
|
|
135
|
-
Icon.
|
|
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
|
|
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
|
-
|
|
22
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
86
|
+
const onClick = jest.fn();
|
|
87
87
|
const { link } = setup({ children: 'Label', isDisabled: true, href: 'https://example.com', onClick });
|
|
88
|
-
|
|
89
|
-
expect(
|
|
90
|
-
|
|
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 =
|
|
96
|
+
const onClick = jest.fn();
|
|
98
97
|
const { link } = setup({ children: 'Label', 'aria-disabled': true, onClick });
|
|
99
|
-
expect(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
<
|
|
87
|
-
ref={ref
|
|
88
|
-
as={linkAs || (forwardedProps.href ? 'a' : 'button')}
|
|
98
|
+
<Component
|
|
99
|
+
ref={ref}
|
|
89
100
|
{...forwardedProps}
|
|
90
|
-
{...
|
|
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
|
-
</
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
75
|
+
const onItemSelected = jest.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).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 =
|
|
51
|
-
const onImageClick =
|
|
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,
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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 =
|
|
48
|
-
const onActionClick =
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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');
|