@lumx/react 3.18.2-alpha.2 → 3.18.2-alpha.4
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/index.d.ts +3 -1
- package/index.js +37 -16
- package/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/button/ButtonRoot.tsx +1 -0
- package/src/components/thumbnail/Thumbnail.test.tsx +40 -3
- package/src/components/thumbnail/Thumbnail.tsx +6 -4
- package/src/components/uploader/Uploader.stories.tsx +12 -3
- package/src/components/uploader/Uploader.test.tsx +31 -10
- package/src/components/uploader/Uploader.tsx +29 -7
- package/src/utils/disabled/DisabledStateProvider.stories.tsx +7 -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.18.2-alpha.
|
|
10
|
-
"@lumx/icons": "^3.18.2-alpha.
|
|
9
|
+
"@lumx/core": "^3.18.2-alpha.4",
|
|
10
|
+
"@lumx/icons": "^3.18.2-alpha.4",
|
|
11
11
|
"@popperjs/core": "^2.5.4",
|
|
12
12
|
"body-scroll-lock": "^3.1.5",
|
|
13
13
|
"classnames": "^2.3.2",
|
|
@@ -105,5 +105,5 @@
|
|
|
105
105
|
"build:storybook": "storybook build"
|
|
106
106
|
},
|
|
107
107
|
"sideEffects": false,
|
|
108
|
-
"version": "3.18.2-alpha.
|
|
108
|
+
"version": "3.18.2-alpha.4"
|
|
109
109
|
}
|
|
@@ -175,6 +175,7 @@ export const ButtonRoot = forwardRef<ButtonRootProps, HTMLButtonElement | HTMLAn
|
|
|
175
175
|
<button
|
|
176
176
|
{...forwardedProps}
|
|
177
177
|
{...disabledStateProps}
|
|
178
|
+
aria-disabled={isAnyDisabled}
|
|
178
179
|
aria-label={ariaLabel}
|
|
179
180
|
ref={ref as RefObject<HTMLButtonElement>}
|
|
180
181
|
className={buttonClassName}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
+
import { DisabledStateProvider } from '@lumx/react/utils';
|
|
3
4
|
import { commonTestsSuiteRTL, SetupRenderOptions } from '@lumx/react/testing/utils';
|
|
4
5
|
import { queryByClassName } from '@lumx/react/testing/utils/queries';
|
|
5
|
-
import { render } from '@testing-library/react';
|
|
6
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
6
7
|
import { Thumbnail, ThumbnailProps } from './Thumbnail';
|
|
7
8
|
|
|
8
9
|
const CLASSNAME = Thumbnail.className as string;
|
|
9
10
|
|
|
10
11
|
const setup = (props: Partial<ThumbnailProps> = {}, { wrapper }: SetupRenderOptions = {}) => {
|
|
11
|
-
render(<Thumbnail {...(props as any)} />, { wrapper });
|
|
12
|
+
const { container } = render(<Thumbnail {...(props as any)} />, { wrapper });
|
|
12
13
|
const thumbnail = queryByClassName(document.body, CLASSNAME);
|
|
13
|
-
return { props, thumbnail };
|
|
14
|
+
return { props, thumbnail, container };
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
describe(`<${Thumbnail.displayName}>`, () => {
|
|
@@ -26,4 +27,40 @@ describe(`<${Thumbnail.displayName}>`, () => {
|
|
|
26
27
|
defaultTheme: 'light',
|
|
27
28
|
},
|
|
28
29
|
});
|
|
30
|
+
|
|
31
|
+
describe('disabled state', () => {
|
|
32
|
+
it('should not be clickable when disabled from context', () => {
|
|
33
|
+
const onClick = jest.fn();
|
|
34
|
+
const { thumbnail, container } = setup(
|
|
35
|
+
{ onClick, 'aria-label': 'thumbnail' },
|
|
36
|
+
{
|
|
37
|
+
wrapper: ({ children }) => (
|
|
38
|
+
<DisabledStateProvider state="disabled">{children}</DisabledStateProvider>
|
|
39
|
+
),
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Should render a div instead of a button.
|
|
44
|
+
expect(container.querySelector('button')).toBe(null);
|
|
45
|
+
expect(thumbnail?.tagName).toBe('DIV');
|
|
46
|
+
|
|
47
|
+
fireEvent.click(thumbnail as HTMLElement);
|
|
48
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should have no href when disabled from context', () => {
|
|
52
|
+
const { container, thumbnail } = setup(
|
|
53
|
+
{ linkAs: 'a', linkProps: { href: '#' }, 'aria-label': 'thumbnail' },
|
|
54
|
+
{
|
|
55
|
+
wrapper: ({ children }) => (
|
|
56
|
+
<DisabledStateProvider state="disabled">{children}</DisabledStateProvider>
|
|
57
|
+
),
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Should render a div instead of a link.
|
|
62
|
+
expect(container.querySelector('a')).toBe(null);
|
|
63
|
+
expect(thumbnail?.tagName).toBe('DIV');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
29
66
|
});
|
|
@@ -21,6 +21,7 @@ import { useFocusPointStyle } from '@lumx/react/components/thumbnail/useFocusPoi
|
|
|
21
21
|
import { useTheme } from '@lumx/react/utils/theme/ThemeContext';
|
|
22
22
|
import { forwardRef } from '@lumx/react/utils/react/forwardRef';
|
|
23
23
|
|
|
24
|
+
import { useDisableStateProps } from '@lumx/react/utils/disabled';
|
|
24
25
|
import { FocusPoint, ThumbnailSize, ThumbnailVariant } from './types';
|
|
25
26
|
|
|
26
27
|
type ImgHTMLProps = ImgHTMLAttributes<HTMLImageElement>;
|
|
@@ -99,6 +100,7 @@ const DEFAULT_PROPS: Partial<ThumbnailProps> = {
|
|
|
99
100
|
* @return React element.
|
|
100
101
|
*/
|
|
101
102
|
export const Thumbnail = forwardRef<ThumbnailProps>((props, ref) => {
|
|
103
|
+
const { isAnyDisabled, otherProps } = useDisableStateProps(props);
|
|
102
104
|
const defaultTheme = useTheme() || Theme.light;
|
|
103
105
|
const {
|
|
104
106
|
align,
|
|
@@ -125,7 +127,7 @@ export const Thumbnail = forwardRef<ThumbnailProps>((props, ref) => {
|
|
|
125
127
|
linkProps,
|
|
126
128
|
linkAs,
|
|
127
129
|
...forwardedProps
|
|
128
|
-
} =
|
|
130
|
+
} = otherProps;
|
|
129
131
|
const [imgElement, setImgElement] = useState<HTMLImageElement>();
|
|
130
132
|
|
|
131
133
|
// Image loading state.
|
|
@@ -150,14 +152,14 @@ export const Thumbnail = forwardRef<ThumbnailProps>((props, ref) => {
|
|
|
150
152
|
|
|
151
153
|
const isLink = Boolean(linkProps?.href || linkAs);
|
|
152
154
|
const isButton = !!forwardedProps.onClick;
|
|
153
|
-
const isClickable = isButton || isLink;
|
|
155
|
+
const isClickable = !isAnyDisabled && (isButton || isLink);
|
|
154
156
|
|
|
155
157
|
let Wrapper: any = 'div';
|
|
156
158
|
const wrapperProps = { ...forwardedProps };
|
|
157
|
-
if (isLink) {
|
|
159
|
+
if (!isAnyDisabled && isLink) {
|
|
158
160
|
Wrapper = linkAs || 'a';
|
|
159
161
|
Object.assign(wrapperProps, linkProps);
|
|
160
|
-
} else if (isButton) {
|
|
162
|
+
} else if (!isAnyDisabled && isButton) {
|
|
161
163
|
Wrapper = 'button';
|
|
162
164
|
wrapperProps.type = forwardedProps.type || 'button';
|
|
163
165
|
wrapperProps['aria-label'] = forwardedProps['aria-label'] || alt;
|
|
@@ -8,6 +8,8 @@ import { withWrapper } from '@lumx/react/stories/decorators/withWrapper';
|
|
|
8
8
|
|
|
9
9
|
import { AspectRatio, GridColumn, Size, Uploader, UploaderVariant } from '@lumx/react';
|
|
10
10
|
import { mdiTextBoxPlus } from '@lumx/icons';
|
|
11
|
+
import { getSelectArgType } from '@lumx/react/stories/controls/selectArgType';
|
|
12
|
+
import { withUndefined } from '@lumx/react/stories/controls/withUndefined';
|
|
11
13
|
|
|
12
14
|
export default {
|
|
13
15
|
title: 'LumX components/uploader/Uploader',
|
|
@@ -15,11 +17,13 @@ export default {
|
|
|
15
17
|
argTypes: {
|
|
16
18
|
onClick: { action: true },
|
|
17
19
|
icon: iconArgType,
|
|
20
|
+
aspectRatio: getSelectArgType(AspectRatio),
|
|
18
21
|
},
|
|
19
22
|
};
|
|
20
23
|
|
|
21
24
|
const UPLOADER_VARIANTS = [UploaderVariant.square, UploaderVariant.rounded, UploaderVariant.circle];
|
|
22
25
|
const UPLOADER_SIZES = [Size.xl, Size.xxl];
|
|
26
|
+
const ASPECT_RATIOS = [AspectRatio.wide, AspectRatio.horizontal, AspectRatio.vertical, AspectRatio.square];
|
|
23
27
|
|
|
24
28
|
export const WithLabel = {
|
|
25
29
|
args: { label: 'Pick a file' },
|
|
@@ -71,13 +75,18 @@ export const Variants = {
|
|
|
71
75
|
withCombinations({
|
|
72
76
|
combinations: {
|
|
73
77
|
rows: { key: 'variant', options: UPLOADER_VARIANTS },
|
|
78
|
+
cols: {
|
|
79
|
+
Default: {},
|
|
80
|
+
Disabled: { isDisabled: true },
|
|
81
|
+
'Aria Disabled': { 'aria-disabled': true },
|
|
82
|
+
},
|
|
74
83
|
sections: {
|
|
75
84
|
Button: {},
|
|
76
85
|
'File input': { fileInputProps: {} },
|
|
77
86
|
},
|
|
78
87
|
},
|
|
79
88
|
}),
|
|
80
|
-
withWrapper({ maxColumns: 2, itemMinWidth:
|
|
89
|
+
withWrapper({ maxColumns: 2, itemMinWidth: 470 }, GridColumn),
|
|
81
90
|
],
|
|
82
91
|
};
|
|
83
92
|
|
|
@@ -88,13 +97,13 @@ export const RatioAndSize = {
|
|
|
88
97
|
withCombinations({
|
|
89
98
|
combinations: {
|
|
90
99
|
cols: { key: 'size', options: UPLOADER_SIZES },
|
|
91
|
-
rows: { key: 'aspectRatio', options:
|
|
100
|
+
rows: { key: 'aspectRatio', options: withUndefined(ASPECT_RATIOS) },
|
|
92
101
|
sections: {
|
|
93
102
|
Button: {},
|
|
94
103
|
'File input': { fileInputProps: {} },
|
|
95
104
|
},
|
|
96
105
|
},
|
|
97
106
|
}),
|
|
98
|
-
withWrapper({ maxColumns: 2, itemMinWidth:
|
|
107
|
+
withWrapper({ maxColumns: 2, itemMinWidth: 470 }, GridColumn),
|
|
99
108
|
],
|
|
100
109
|
};
|
|
@@ -22,8 +22,9 @@ const setup = (propsOverride: SetupProps = {}, { wrapper }: SetupRenderOptions =
|
|
|
22
22
|
const uploader = getByClassName(document.body, CLASSNAME);
|
|
23
23
|
const label = queryByClassName(uploader, `${CLASSNAME}__label`);
|
|
24
24
|
const icon = queryByClassName(uploader, `${CLASSNAME}__icon`);
|
|
25
|
+
const input = queryByClassName(uploader, `${CLASSNAME}__input`);
|
|
25
26
|
|
|
26
|
-
return { props, uploader, label, icon };
|
|
27
|
+
return { props, uploader, label, icon, input };
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
describe(`<${Uploader.displayName}>`, () => {
|
|
@@ -83,31 +84,51 @@ describe(`<${Uploader.displayName}>`, () => {
|
|
|
83
84
|
});
|
|
84
85
|
});
|
|
85
86
|
|
|
86
|
-
describe
|
|
87
|
+
describe.each`
|
|
88
|
+
name | props
|
|
89
|
+
${'button'} | ${{}}
|
|
90
|
+
${'button isDisabled '} | ${{ isDisabled: true }}
|
|
91
|
+
${'button aria-disabled'} | ${{ 'aria-disabled': true }}
|
|
92
|
+
${'file input '} | ${{ fileInputProps: { onChange: jest.fn() } }}
|
|
93
|
+
`('Events $name', ({ props }) => {
|
|
94
|
+
const onClick = jest.fn();
|
|
95
|
+
beforeEach(() => onClick.mockClear());
|
|
96
|
+
const assertClick = () => {
|
|
97
|
+
if (props.isDisabled || props['aria-disabled']) {
|
|
98
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
99
|
+
} else {
|
|
100
|
+
expect(onClick).toHaveBeenCalled();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
87
104
|
it('should trigger `onClick` when clicked', async () => {
|
|
88
|
-
const
|
|
89
|
-
const { uploader } = setup({ onClick });
|
|
105
|
+
const { uploader } = setup({ ...props, onClick });
|
|
90
106
|
|
|
91
107
|
await userEvent.click(uploader);
|
|
92
|
-
|
|
108
|
+
assertClick();
|
|
93
109
|
});
|
|
94
110
|
|
|
95
111
|
it('should trigger `onClick` when pressing Enter or Escape', async () => {
|
|
96
|
-
const
|
|
97
|
-
|
|
112
|
+
const { uploader, input } = setup({ ...props, onClick });
|
|
113
|
+
|
|
114
|
+
if (props.isDisabled) {
|
|
115
|
+
expect(props.fileInputProps ? input : uploader).toBeDisabled();
|
|
116
|
+
//Cannot test focus or activation
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
98
119
|
|
|
99
120
|
await userEvent.tab();
|
|
100
|
-
expect(uploader).toHaveFocus();
|
|
121
|
+
expect(props.fileInputProps ? input : uploader).toHaveFocus();
|
|
101
122
|
|
|
102
123
|
// Activate with Enter
|
|
103
124
|
await userEvent.keyboard('[Enter]');
|
|
104
|
-
|
|
125
|
+
assertClick();
|
|
105
126
|
|
|
106
127
|
onClick.mockClear();
|
|
107
128
|
|
|
108
129
|
// Activate with Space
|
|
109
130
|
await userEvent.keyboard('[Space]');
|
|
110
|
-
|
|
131
|
+
assertClick();
|
|
111
132
|
});
|
|
112
133
|
});
|
|
113
134
|
|
|
@@ -9,6 +9,9 @@ import { useBooleanState } from '@lumx/react/hooks/useBooleanState';
|
|
|
9
9
|
import { useId } from '@lumx/react/hooks/useId';
|
|
10
10
|
import { useTheme } from '@lumx/react/utils/theme/ThemeContext';
|
|
11
11
|
import { forwardRef } from '@lumx/react/utils/react/forwardRef';
|
|
12
|
+
import { HasAriaDisabled } from '@lumx/react/utils/type/HasAriaDisabled';
|
|
13
|
+
import { useDisableStateProps } from '@lumx/react/utils/disabled';
|
|
14
|
+
import { VISUALLY_HIDDEN } from '@lumx/react/constants';
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Uploader variants.
|
|
@@ -35,11 +38,13 @@ interface FileInputProps extends Omit<React.ComponentProps<'input'>, 'onChange'>
|
|
|
35
38
|
/**
|
|
36
39
|
* Defines the props of the component.
|
|
37
40
|
*/
|
|
38
|
-
export interface UploaderProps extends GenericProps, HasTheme {
|
|
41
|
+
export interface UploaderProps extends GenericProps, HasTheme, HasAriaDisabled {
|
|
39
42
|
/** Image aspect ratio. */
|
|
40
43
|
aspectRatio?: AspectRatio;
|
|
41
44
|
/** Icon (SVG path). */
|
|
42
45
|
icon?: string;
|
|
46
|
+
/** Disabled state */
|
|
47
|
+
isDisabled?: boolean;
|
|
43
48
|
/** Label text. */
|
|
44
49
|
label?: string;
|
|
45
50
|
/** Size variant. */
|
|
@@ -79,6 +84,7 @@ const DEFAULT_PROPS: Partial<UploaderProps> = {
|
|
|
79
84
|
* @return React element.
|
|
80
85
|
*/
|
|
81
86
|
export const Uploader = forwardRef<UploaderProps>((props, ref) => {
|
|
87
|
+
const { disabledStateProps, otherProps, isAnyDisabled } = useDisableStateProps(props);
|
|
82
88
|
const defaultTheme = useTheme() || Theme.light;
|
|
83
89
|
const {
|
|
84
90
|
aspectRatio = DEFAULT_PROPS.aspectRatio,
|
|
@@ -89,32 +95,45 @@ export const Uploader = forwardRef<UploaderProps>((props, ref) => {
|
|
|
89
95
|
theme = defaultTheme,
|
|
90
96
|
variant = DEFAULT_PROPS.variant,
|
|
91
97
|
fileInputProps,
|
|
98
|
+
onClick,
|
|
92
99
|
...forwardedProps
|
|
93
|
-
} =
|
|
100
|
+
} = otherProps;
|
|
94
101
|
// Adjust to square aspect ratio when using circle variants.
|
|
95
102
|
const adjustedAspectRatio = variant === UploaderVariant.circle ? AspectRatio.square : aspectRatio;
|
|
96
103
|
|
|
104
|
+
const handleClick: React.MouseEventHandler = React.useCallback(
|
|
105
|
+
(evt) => {
|
|
106
|
+
if (isAnyDisabled) {
|
|
107
|
+
evt.preventDefault();
|
|
108
|
+
} else {
|
|
109
|
+
onClick?.(evt);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
[isAnyDisabled, onClick],
|
|
113
|
+
);
|
|
114
|
+
|
|
97
115
|
const generatedInputId = useId();
|
|
98
116
|
const inputId = fileInputProps?.id || generatedInputId;
|
|
99
117
|
const [isDragHovering, unsetDragHovering, setDragHovering] = useBooleanState(false);
|
|
100
118
|
const wrapper = fileInputProps
|
|
101
|
-
? { Component: 'label'
|
|
102
|
-
: { Component: 'button'
|
|
119
|
+
? ({ Component: 'label', props: { htmlFor: inputId } } as const)
|
|
120
|
+
: ({ Component: 'button', props: { type: 'button', ...disabledStateProps } } as const);
|
|
103
121
|
|
|
104
122
|
const onChange = React.useMemo(() => {
|
|
105
|
-
if (!fileInputProps?.onChange) return undefined;
|
|
123
|
+
if (isAnyDisabled || !fileInputProps?.onChange) return undefined;
|
|
106
124
|
return (evt: React.ChangeEvent<HTMLInputElement>) => {
|
|
107
125
|
const fileList = evt.target.files;
|
|
108
126
|
const files = fileList ? Array.from(fileList) : [];
|
|
109
127
|
fileInputProps.onChange(files, evt);
|
|
110
128
|
};
|
|
111
|
-
}, [fileInputProps]);
|
|
129
|
+
}, [isAnyDisabled, fileInputProps]);
|
|
112
130
|
|
|
113
131
|
return (
|
|
114
132
|
<wrapper.Component
|
|
115
133
|
ref={ref as any}
|
|
116
134
|
{...wrapper.props}
|
|
117
135
|
{...forwardedProps}
|
|
136
|
+
onClick={handleClick}
|
|
118
137
|
className={classNames(
|
|
119
138
|
className,
|
|
120
139
|
handleBasicClasses({
|
|
@@ -124,6 +143,7 @@ export const Uploader = forwardRef<UploaderProps>((props, ref) => {
|
|
|
124
143
|
theme,
|
|
125
144
|
variant,
|
|
126
145
|
isDragHovering,
|
|
146
|
+
isDisabled: isAnyDisabled,
|
|
127
147
|
}),
|
|
128
148
|
)}
|
|
129
149
|
>
|
|
@@ -139,8 +159,10 @@ export const Uploader = forwardRef<UploaderProps>((props, ref) => {
|
|
|
139
159
|
<input
|
|
140
160
|
type="file"
|
|
141
161
|
id={inputId}
|
|
142
|
-
className={`${CLASSNAME}__input`}
|
|
162
|
+
className={`${CLASSNAME}__input ${VISUALLY_HIDDEN}`}
|
|
163
|
+
{...disabledStateProps}
|
|
143
164
|
{...fileInputProps}
|
|
165
|
+
readOnly={isAnyDisabled}
|
|
144
166
|
onChange={onChange}
|
|
145
167
|
onDragEnter={setDragHovering}
|
|
146
168
|
onDragLeave={unsetDragHovering}
|
|
@@ -12,11 +12,14 @@ import {
|
|
|
12
12
|
RadioButton,
|
|
13
13
|
Switch,
|
|
14
14
|
TextField,
|
|
15
|
+
Thumbnail,
|
|
16
|
+
Uploader,
|
|
15
17
|
} from '@lumx/react';
|
|
16
18
|
import { DisabledStateProvider } from '@lumx/react/utils';
|
|
17
19
|
import { getSelectArgType } from '@lumx/react/stories/controls/selectArgType';
|
|
18
20
|
import { disableArgTypes } from '@lumx/react/stories/utils/disableArgTypes';
|
|
19
21
|
import { mdiFoodApple } from '@lumx/icons';
|
|
22
|
+
import { LANDSCAPE_IMAGES } from '@lumx/react/stories/controls/image';
|
|
20
23
|
|
|
21
24
|
export default {
|
|
22
25
|
title: 'LumX components/DisabledStateProvider',
|
|
@@ -71,6 +74,7 @@ export const AllComponents = {
|
|
|
71
74
|
<Checkbox label="Checkbox" />
|
|
72
75
|
<Chip onClick={() => {}}>Chip</Chip>
|
|
73
76
|
<DatePickerField
|
|
77
|
+
label="Date picker field"
|
|
74
78
|
nextButtonProps={{ label: 'Next' }}
|
|
75
79
|
previousButtonProps={{ label: 'Previous' }}
|
|
76
80
|
value={new Date()}
|
|
@@ -82,7 +86,9 @@ export const AllComponents = {
|
|
|
82
86
|
</List>
|
|
83
87
|
<RadioButton label="Radio button" />
|
|
84
88
|
<Switch>Switch</Switch>
|
|
85
|
-
<TextField onChange={() => {}} value="" />
|
|
89
|
+
<TextField label="texfield" onChange={() => {}} value="" />
|
|
90
|
+
<Thumbnail alt="Thumbnail" image={LANDSCAPE_IMAGES.landscape1s200} onClick={() => {}} />
|
|
91
|
+
<Uploader label="Upload a file" fileInputProps={{ onChange: () => {} }} />
|
|
86
92
|
</DisabledStateProvider>
|
|
87
93
|
),
|
|
88
94
|
};
|