@lumx/react 3.19.1-alpha.1 → 3.19.1-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_internal/index.js +13 -20
- package/_internal/index.js.map +1 -1
- package/index.d.ts +6 -5
- package/index.js +2501 -2418
- package/index.js.map +1 -1
- package/package.json +6 -5
- package/src/components/button/Button.test.tsx +5 -5
- package/src/components/button/ButtonRoot.tsx +36 -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/link/Link.test.tsx +6 -8
- package/src/components/link/Link.tsx +20 -9
- package/src/components/list/ListItem.test.tsx +3 -1
- package/src/components/list/ListItem.tsx +32 -19
- package/src/components/navigation/NavigationItem.tsx +10 -6
- package/src/components/navigation/NavigationSection.tsx +3 -4
- package/src/components/side-navigation/SideNavigationItem.tsx +27 -22
- package/src/components/thumbnail/Thumbnail.test.tsx +4 -28
- package/src/components/thumbnail/Thumbnail.tsx +11 -10
- package/src/utils/react/renderButtonOrLink.tsx +16 -0
- package/src/utils/react/renderLink.tsx +17 -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
package/package.json
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"url": "https://github.com/lumapps/design-system/issues"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@lumx/core": "^3.19.1-alpha.
|
|
10
|
-
"@lumx/icons": "^3.19.1-alpha.
|
|
9
|
+
"@lumx/core": "^3.19.1-alpha.10",
|
|
10
|
+
"@lumx/icons": "^3.19.1-alpha.10",
|
|
11
11
|
"@popperjs/core": "^2.5.4",
|
|
12
12
|
"body-scroll-lock": "^3.1.5",
|
|
13
13
|
"classnames": "^2.3.2",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"@rollup/plugin-babel": "^6.0.4",
|
|
31
31
|
"@rollup/plugin-commonjs": "^19.0.2",
|
|
32
32
|
"@rollup/plugin-node-resolve": "16.0.0",
|
|
33
|
+
"@rollup/pluginutils": "5.2.0",
|
|
33
34
|
"@storybook/addon-a11y": "^9.1.4",
|
|
34
35
|
"@storybook/addon-docs": "^9.1.4",
|
|
35
36
|
"@storybook/react-vite": "^9.1.4",
|
|
@@ -74,8 +75,8 @@
|
|
|
74
75
|
},
|
|
75
76
|
"peerDependencies": {
|
|
76
77
|
"lodash": "4.17.21",
|
|
77
|
-
"react": ">=
|
|
78
|
-
"react-dom": ">=
|
|
78
|
+
"react": ">= 17.0.0",
|
|
79
|
+
"react-dom": ">= 17.0.0"
|
|
79
80
|
},
|
|
80
81
|
"description": "The official LumApps Design System (LumX) for React applications",
|
|
81
82
|
"homepage": "https://github.com/lumapps/design-system",
|
|
@@ -105,5 +106,5 @@
|
|
|
105
106
|
"build:storybook": "storybook build"
|
|
106
107
|
},
|
|
107
108
|
"sideEffects": false,
|
|
108
|
-
"version": "3.19.1-alpha.
|
|
109
|
+
"version": "3.19.1-alpha.10"
|
|
109
110
|
}
|
|
@@ -78,10 +78,9 @@ describe(`<${Button.displayName}>`, () => {
|
|
|
78
78
|
it('should render disabled link', async () => {
|
|
79
79
|
const onClick = jest.fn();
|
|
80
80
|
const { button } = setup({ children: 'Label', disabled: true, href: 'https://example.com', onClick });
|
|
81
|
-
|
|
82
|
-
expect(
|
|
83
|
-
|
|
84
|
-
expect(button).toHaveAttribute('tabindex', '-1');
|
|
81
|
+
// Disabled link do not exist so we fallback to a button
|
|
82
|
+
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
|
83
|
+
expect(button).toHaveAttribute('disabled');
|
|
85
84
|
await userEvent.click(button);
|
|
86
85
|
expect(onClick).not.toHaveBeenCalled();
|
|
87
86
|
});
|
|
@@ -103,7 +102,8 @@ describe(`<${Button.displayName}>`, () => {
|
|
|
103
102
|
onClick,
|
|
104
103
|
});
|
|
105
104
|
expect(button).toHaveAccessibleName('Label');
|
|
106
|
-
|
|
105
|
+
// Disabled link do not exist so we fallback to a button
|
|
106
|
+
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
|
107
107
|
expect(button).toHaveAttribute('aria-disabled', 'true');
|
|
108
108
|
await userEvent.click(button);
|
|
109
109
|
expect(onClick).not.toHaveBeenCalled();
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import React, { AriaAttributes, ButtonHTMLAttributes, DetailedHTMLProps, RefObject } from 'react';
|
|
2
2
|
|
|
3
|
+
import isEmpty from 'lodash/isEmpty';
|
|
4
|
+
|
|
3
5
|
import classNames from 'classnames';
|
|
4
6
|
|
|
5
7
|
import { ColorPalette, Emphasis, Size, Theme } from '@lumx/react';
|
|
6
8
|
import { CSS_PREFIX } from '@lumx/react/constants';
|
|
7
9
|
import { GenericProps, HasTheme } from '@lumx/react/utils/type';
|
|
8
10
|
import { handleBasicClasses } from '@lumx/core/js/utils/className';
|
|
11
|
+
import { renderLink } from '@lumx/react/utils/react/renderLink';
|
|
9
12
|
import { forwardRef } from '@lumx/react/utils/react/forwardRef';
|
|
13
|
+
import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps';
|
|
10
14
|
import { HasAriaDisabled } from '@lumx/react/utils/type/HasAriaDisabled';
|
|
11
|
-
import { RawClickable } from '@lumx/react/utils/react/RawClickable';
|
|
12
|
-
import { useDisableStateProps } from '@lumx/react/utils/disabled';
|
|
13
15
|
|
|
14
16
|
type HTMLButtonProps = DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;
|
|
15
17
|
|
|
@@ -105,14 +107,18 @@ export const ButtonRoot = forwardRef<ButtonRootProps, HTMLButtonElement | HTMLAn
|
|
|
105
107
|
color,
|
|
106
108
|
emphasis,
|
|
107
109
|
hasBackground,
|
|
110
|
+
href,
|
|
108
111
|
isSelected,
|
|
109
112
|
isActive,
|
|
110
113
|
isFocused,
|
|
111
114
|
isHovered,
|
|
112
115
|
linkAs,
|
|
116
|
+
name,
|
|
113
117
|
size,
|
|
118
|
+
target,
|
|
114
119
|
theme,
|
|
115
120
|
variant,
|
|
121
|
+
type = 'button',
|
|
116
122
|
fullWidth,
|
|
117
123
|
...forwardedProps
|
|
118
124
|
} = otherProps;
|
|
@@ -133,7 +139,7 @@ export const ButtonRoot = forwardRef<ButtonRootProps, HTMLButtonElement | HTMLAn
|
|
|
133
139
|
color: adaptedColor,
|
|
134
140
|
emphasis,
|
|
135
141
|
isSelected,
|
|
136
|
-
isDisabled:
|
|
142
|
+
isDisabled: isAnyDisabled,
|
|
137
143
|
isActive,
|
|
138
144
|
isFocused,
|
|
139
145
|
isHovered,
|
|
@@ -145,18 +151,42 @@ export const ButtonRoot = forwardRef<ButtonRootProps, HTMLButtonElement | HTMLAn
|
|
|
145
151
|
}),
|
|
146
152
|
);
|
|
147
153
|
|
|
154
|
+
/**
|
|
155
|
+
* If the linkAs prop is used, we use the linkAs component instead of a <button>.
|
|
156
|
+
* If there is an href attribute, we display an <a> instead of a <button>.
|
|
157
|
+
*
|
|
158
|
+
* However, in any case, if the component is disabled, we returned a <button> since disabled is not compatible with <a>.
|
|
159
|
+
*/
|
|
160
|
+
if ((linkAs || !isEmpty(props.href)) && !isAnyDisabled) {
|
|
161
|
+
return renderLink(
|
|
162
|
+
{
|
|
163
|
+
linkAs,
|
|
164
|
+
...forwardedProps,
|
|
165
|
+
'aria-label': ariaLabel,
|
|
166
|
+
href,
|
|
167
|
+
target,
|
|
168
|
+
className: buttonClassName,
|
|
169
|
+
ref: ref as RefObject<HTMLAnchorElement>,
|
|
170
|
+
},
|
|
171
|
+
children,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
148
174
|
return (
|
|
149
|
-
<
|
|
150
|
-
as={linkAs || (forwardedProps.href ? 'a' : 'button')}
|
|
175
|
+
<button
|
|
151
176
|
{...forwardedProps}
|
|
152
177
|
{...disabledStateProps}
|
|
153
178
|
aria-disabled={isAnyDisabled}
|
|
154
179
|
aria-label={ariaLabel}
|
|
155
180
|
ref={ref as RefObject<HTMLButtonElement>}
|
|
156
181
|
className={buttonClassName}
|
|
182
|
+
name={name}
|
|
183
|
+
type={
|
|
184
|
+
// eslint-disable-next-line react/button-has-type
|
|
185
|
+
type
|
|
186
|
+
}
|
|
157
187
|
>
|
|
158
188
|
{children}
|
|
159
|
-
</
|
|
189
|
+
</button>
|
|
160
190
|
);
|
|
161
191
|
});
|
|
162
192
|
ButtonRoot.displayName = COMPONENT_NAME;
|
|
@@ -1,47 +1,21 @@
|
|
|
1
|
+
import DefaultStory, { SizeAndShape as DefaultSizeAndShape } from '@lumx/core/js/components/Icon/Stories';
|
|
1
2
|
import { mdiEmail } from '@lumx/icons';
|
|
2
|
-
import { ColorPalette, ColorVariant, GridColumn, Icon,
|
|
3
|
+
import { ColorPalette, ColorVariant, GridColumn, Icon, Size } from '@lumx/react';
|
|
3
4
|
import { withCombinations } from '@lumx/react/stories/decorators/withCombinations';
|
|
4
5
|
import { withUndefined } from '@lumx/react/stories/controls/withUndefined';
|
|
5
|
-
import { iconArgType } from '@lumx/react/stories/controls/icons';
|
|
6
|
-
import { colorArgType, colorVariantArgType } from '@lumx/react/stories/controls/color';
|
|
7
6
|
import { withWrapper } from '@lumx/react/stories/decorators/withWrapper';
|
|
8
7
|
|
|
9
|
-
const iconSizes: Array<IconSizes> = [Size.xxs, Size.xs, Size.s, Size.m, Size.l, Size.xl, Size.xxl];
|
|
10
|
-
|
|
11
8
|
export default {
|
|
12
9
|
title: 'LumX components/icon/Icon',
|
|
13
10
|
component: Icon,
|
|
14
|
-
|
|
15
|
-
argTypes: {
|
|
16
|
-
icon: iconArgType,
|
|
17
|
-
hasShape: { control: 'boolean' },
|
|
18
|
-
color: colorArgType,
|
|
19
|
-
colorVariant: colorVariantArgType,
|
|
20
|
-
},
|
|
11
|
+
...DefaultStory,
|
|
21
12
|
};
|
|
22
13
|
|
|
23
14
|
/**
|
|
24
15
|
* All combinations of size and shape
|
|
25
16
|
*/
|
|
26
17
|
export const SizeAndShape = {
|
|
27
|
-
|
|
28
|
-
icon: mdiEmail,
|
|
29
|
-
},
|
|
30
|
-
argTypes: {
|
|
31
|
-
hasShape: { control: false },
|
|
32
|
-
size: { control: false },
|
|
33
|
-
},
|
|
34
|
-
decorators: [
|
|
35
|
-
withCombinations({
|
|
36
|
-
combinations: {
|
|
37
|
-
cols: { key: 'size', options: withUndefined(iconSizes) },
|
|
38
|
-
rows: {
|
|
39
|
-
Default: {},
|
|
40
|
-
'Has shape': { hasShape: true },
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
}),
|
|
44
|
-
],
|
|
18
|
+
...DefaultSizeAndShape,
|
|
45
19
|
};
|
|
46
20
|
|
|
47
21
|
/**
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import { mdiAlertCircle } from '@lumx/icons';
|
|
4
|
-
import { ColorPalette, ColorVariant, Size, Theme } from '@lumx/react';
|
|
5
3
|
import { commonTestsSuiteRTL, SetupRenderOptions } from '@lumx/react/testing/utils';
|
|
6
4
|
|
|
7
5
|
import { getByClassName, getByTagName } from '@lumx/react/testing/utils/queries';
|
|
8
6
|
import { render } from '@testing-library/react';
|
|
7
|
+
import Tests from '@lumx/core/js/components/Icon/Tests';
|
|
9
8
|
import { Icon, IconProps } from './Icon';
|
|
10
9
|
|
|
11
10
|
const CLASSNAME = Icon.className as string;
|
|
@@ -29,89 +28,7 @@ const setup = (propsOverride: SetupProps = {}, { wrapper }: SetupRenderOptions =
|
|
|
29
28
|
};
|
|
30
29
|
|
|
31
30
|
describe(`<${Icon.displayName}>`, () => {
|
|
32
|
-
|
|
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;
|
|
@@ -85,10 +85,9 @@ describe(`<${Link.displayName}>`, () => {
|
|
|
85
85
|
it('should render disabled link', async () => {
|
|
86
86
|
const onClick = jest.fn();
|
|
87
87
|
const { link } = setup({ children: 'Label', isDisabled: true, href: 'https://example.com', onClick });
|
|
88
|
-
|
|
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
|
});
|
|
@@ -96,9 +95,7 @@ describe(`<${Link.displayName}>`, () => {
|
|
|
96
95
|
it('should render aria-disabled button', async () => {
|
|
97
96
|
const onClick = jest.fn();
|
|
98
97
|
const { link } = setup({ children: 'Label', 'aria-disabled': true, onClick });
|
|
99
|
-
expect(
|
|
100
|
-
expect(link).toHaveAttribute('aria-disabled', 'true');
|
|
101
|
-
expect(link).not.toHaveAttribute('tabindex');
|
|
98
|
+
expect(link).toHaveAttribute('aria-disabled');
|
|
102
99
|
await userEvent.click(link);
|
|
103
100
|
expect(onClick).not.toHaveBeenCalled();
|
|
104
101
|
});
|
|
@@ -112,7 +109,8 @@ describe(`<${Link.displayName}>`, () => {
|
|
|
112
109
|
onClick,
|
|
113
110
|
});
|
|
114
111
|
expect(link).toHaveAccessibleName('Label');
|
|
115
|
-
|
|
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;
|
|
@@ -43,7 +43,7 @@ describe(`<${ListItem.displayName}>`, () => {
|
|
|
43
43
|
it('should render disabled list item button', async () => {
|
|
44
44
|
const onItemSelected = jest.fn();
|
|
45
45
|
const { link } = setup({ children: 'Label', isDisabled: true, onItemSelected });
|
|
46
|
-
expect(link).
|
|
46
|
+
expect(link).toHaveAttribute('aria-disabled', 'true');
|
|
47
47
|
// The `renderLink` util removes the onClick handler but `user-event` will also not fire events on disabled elements.
|
|
48
48
|
if (link) await userEvent.click(link);
|
|
49
49
|
expect(onItemSelected).not.toHaveBeenCalled();
|
|
@@ -57,6 +57,7 @@ describe(`<${ListItem.displayName}>`, () => {
|
|
|
57
57
|
linkProps: { href: 'https://example.com' },
|
|
58
58
|
onItemSelected,
|
|
59
59
|
});
|
|
60
|
+
expect(link).not.toHaveAttribute('href');
|
|
60
61
|
expect(link).toHaveAttribute('aria-disabled', 'true');
|
|
61
62
|
if (link) await userEvent.click(link);
|
|
62
63
|
expect(onItemSelected).not.toHaveBeenCalled();
|
|
@@ -78,6 +79,7 @@ describe(`<${ListItem.displayName}>`, () => {
|
|
|
78
79
|
linkProps: { href: 'https://example.com' },
|
|
79
80
|
onItemSelected,
|
|
80
81
|
});
|
|
82
|
+
expect(link).not.toHaveAttribute('href');
|
|
81
83
|
expect(link).toHaveAttribute('aria-disabled', 'true');
|
|
82
84
|
if (link) await userEvent.click(link);
|
|
83
85
|
expect(onItemSelected).not.toHaveBeenCalled();
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import React, { ReactNode, Ref, SyntheticEvent } from 'react';
|
|
1
|
+
import React, { ReactNode, Ref, SyntheticEvent, useMemo } from 'react';
|
|
2
2
|
|
|
3
3
|
import classNames from 'classnames';
|
|
4
4
|
import isEmpty from 'lodash/isEmpty';
|
|
5
5
|
|
|
6
6
|
import { ListProps, Size } from '@lumx/react';
|
|
7
7
|
import { GenericProps } from '@lumx/react/utils/type';
|
|
8
|
+
import { onEnterPressed, onButtonPressed } from '@lumx/core/js/utils';
|
|
8
9
|
import { getRootClassName, handleBasicClasses } from '@lumx/core/js/utils/className';
|
|
10
|
+
import { renderLink } from '@lumx/react/utils/react/renderLink';
|
|
9
11
|
import { forwardRef } from '@lumx/react/utils/react/forwardRef';
|
|
10
12
|
import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps';
|
|
11
13
|
import { HasAriaDisabled } from '@lumx/react/utils/type/HasAriaDisabled';
|
|
12
|
-
import { RawClickable } from '@lumx/react/utils/react/RawClickable';
|
|
13
14
|
|
|
14
15
|
export type ListItemSize = Extract<Size, 'tiny' | 'regular' | 'big' | 'huge'>;
|
|
15
16
|
|
|
@@ -93,6 +94,13 @@ export const ListItem = forwardRef<ListItemProps, HTMLLIElement>((props, ref) =>
|
|
|
93
94
|
...forwardedProps
|
|
94
95
|
} = otherProps;
|
|
95
96
|
|
|
97
|
+
const role = linkAs || linkProps.href ? 'link' : 'button';
|
|
98
|
+
const onKeyDown = useMemo(() => {
|
|
99
|
+
if (onItemSelected && role === 'link') return onEnterPressed(onItemSelected as any);
|
|
100
|
+
if (onItemSelected && role === 'button') return onButtonPressed(onItemSelected as any);
|
|
101
|
+
return undefined;
|
|
102
|
+
}, [role, onItemSelected]);
|
|
103
|
+
|
|
96
104
|
const content = (
|
|
97
105
|
<>
|
|
98
106
|
{before && <div className={`${CLASSNAME}__before`}>{before}</div>}
|
|
@@ -115,23 +123,28 @@ export const ListItem = forwardRef<ListItemProps, HTMLLIElement>((props, ref) =>
|
|
|
115
123
|
>
|
|
116
124
|
{isClickable({ linkProps, onItemSelected }) ? (
|
|
117
125
|
/* Clickable list item */
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
renderLink(
|
|
127
|
+
{
|
|
128
|
+
linkAs,
|
|
129
|
+
tabIndex: !disabledStateProps.disabled ? 0 : undefined,
|
|
130
|
+
role,
|
|
131
|
+
'aria-disabled': isAnyDisabled,
|
|
132
|
+
...linkProps,
|
|
133
|
+
href: isAnyDisabled ? undefined : linkProps.href,
|
|
134
|
+
className: classNames(
|
|
135
|
+
handleBasicClasses({
|
|
136
|
+
prefix: `${CLASSNAME}__link`,
|
|
137
|
+
isHighlighted,
|
|
138
|
+
isSelected,
|
|
139
|
+
isDisabled: isAnyDisabled,
|
|
140
|
+
}),
|
|
141
|
+
),
|
|
142
|
+
onClick: isAnyDisabled ? undefined : onItemSelected,
|
|
143
|
+
onKeyDown: isAnyDisabled ? undefined : onKeyDown,
|
|
144
|
+
ref: linkRef,
|
|
145
|
+
},
|
|
146
|
+
content,
|
|
147
|
+
)
|
|
135
148
|
) : (
|
|
136
149
|
/* Non clickable list item */
|
|
137
150
|
<div className={`${CLASSNAME}__wrapper`}>{content}</div>
|