@lumx/react 3.18.2-alpha.3 → 3.19.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/index.d.ts +3 -1
- package/index.js +45 -27
- package/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/navigation/NavigationItem.tsx +1 -1
- package/src/components/text/Text.stories.tsx +23 -1
- package/src/components/text/Text.tsx +1 -1
- 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/hooks/useOverflowTooltipLabel.tsx +21 -18
- package/src/utils/disabled/DisabledStateProvider.stories.tsx +2 -0
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.
|
|
10
|
-
"@lumx/icons": "^3.
|
|
9
|
+
"@lumx/core": "^3.19.0",
|
|
10
|
+
"@lumx/icons": "^3.19.0",
|
|
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.
|
|
108
|
+
"version": "3.19.0"
|
|
109
109
|
}
|
|
@@ -42,7 +42,7 @@ export const NavigationItem = Object.assign(
|
|
|
42
42
|
forwardRefPolymorphic(<E extends ElementType = 'a'>(props: NavigationItemProps<E>, ref: ComponentRef<E>) => {
|
|
43
43
|
const { className, icon, label, isCurrentPage, as: Element = 'a', ...forwardedProps } = props;
|
|
44
44
|
const theme = useTheme();
|
|
45
|
-
const { tooltipLabel, labelRef } = useOverflowTooltipLabel();
|
|
45
|
+
const { tooltipLabel, labelRef } = useOverflowTooltipLabel(label);
|
|
46
46
|
|
|
47
47
|
const buttonProps = Element === 'button' ? { type: 'button' } : {};
|
|
48
48
|
|
|
@@ -6,7 +6,7 @@ import { textElementArgType } from '@lumx/react/stories/controls/element';
|
|
|
6
6
|
import { withUndefined } from '@lumx/react/stories/controls/withUndefined';
|
|
7
7
|
import { loremIpsum } from '@lumx/react/stories/utils/lorem';
|
|
8
8
|
import { withCombinations } from '@lumx/react/stories/decorators/withCombinations';
|
|
9
|
-
import { ColorPalette, ColorVariant, Icon, WhiteSpace } from '@lumx/react';
|
|
9
|
+
import { Button, ColorPalette, ColorVariant, Icon, WhiteSpace } from '@lumx/react';
|
|
10
10
|
import { mdiEarth, mdiHeart } from '@lumx/icons';
|
|
11
11
|
import { withResizableBox } from '@lumx/react/stories/decorators/withResizableBox';
|
|
12
12
|
import { getSelectArgType } from '@lumx/react/stories/controls/selectArgType';
|
|
@@ -107,6 +107,28 @@ export const Truncate = {
|
|
|
107
107
|
},
|
|
108
108
|
};
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Test the update of the `title` attribute when text overflows
|
|
112
|
+
*/
|
|
113
|
+
export const TestUpdateTruncateTitleLabel = {
|
|
114
|
+
render(args: any) {
|
|
115
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
116
|
+
const [content, setContent] = React.useState<string>('Some text');
|
|
117
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
118
|
+
const lengthen = React.useCallback(() => setContent((prevContent) => `${prevContent} ${prevContent}`), []);
|
|
119
|
+
return (
|
|
120
|
+
<>
|
|
121
|
+
<Button onClick={lengthen}>Lengthen text</Button>
|
|
122
|
+
<Text as="p" truncate style={{ maxWidth: 300 }} {...args}>
|
|
123
|
+
{content}
|
|
124
|
+
</Text>
|
|
125
|
+
</>
|
|
126
|
+
);
|
|
127
|
+
},
|
|
128
|
+
// Disables Chromatic snapshot (not relevant for this story).
|
|
129
|
+
parameters: { chromatic: { disable: true } },
|
|
130
|
+
};
|
|
131
|
+
|
|
110
132
|
/**
|
|
111
133
|
* Long text with multi line truncate ellipsis
|
|
112
134
|
*/
|
|
@@ -112,7 +112,7 @@ export const Text = forwardRef<TextProps>((props, ref) => {
|
|
|
112
112
|
!(isTruncated && !isTruncatedMultiline) &&
|
|
113
113
|
whiteSpace && { '--lumx-text-white-space': whiteSpace };
|
|
114
114
|
|
|
115
|
-
const { tooltipLabel, labelRef } = useOverflowTooltipLabel();
|
|
115
|
+
const { tooltipLabel, labelRef } = useOverflowTooltipLabel(children);
|
|
116
116
|
|
|
117
117
|
return (
|
|
118
118
|
<Component
|
|
@@ -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}
|
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useTooltipContext } from '@lumx/react/components/tooltip/context';
|
|
3
|
+
import { VISUALLY_HIDDEN } from '@lumx/react/constants';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Compute a tooltip label based on a label element `innerText` if the text overflows.
|
|
6
|
-
*
|
|
7
|
-
* Warning: only works on first render, does not update on label element resize.
|
|
7
|
+
* Updates dynamically on content changes (but not on resize!)
|
|
8
8
|
*/
|
|
9
|
-
export const useOverflowTooltipLabel = () => {
|
|
9
|
+
export const useOverflowTooltipLabel = (content: React.ReactNode) => {
|
|
10
10
|
const parentTooltip = useTooltipContext();
|
|
11
11
|
const [tooltipLabel, setTooltipLabel] = React.useState<string | undefined>(undefined);
|
|
12
|
-
const
|
|
13
|
-
(labelElement: HTMLElement | null) => {
|
|
14
|
-
if (!labelElement || !!parentTooltip) {
|
|
15
|
-
// Skip if label element is unknown
|
|
16
|
-
// Skip if the parent has a tooltip
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
12
|
+
const [labelElement, setLabelElement] = React.useState<HTMLElement | null>(null);
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
React.useLayoutEffect(() => {
|
|
15
|
+
if (
|
|
16
|
+
// Not inside a tooltip
|
|
17
|
+
!parentTooltip &&
|
|
18
|
+
labelElement &&
|
|
19
|
+
// Not inside a visually hidden
|
|
20
|
+
!labelElement?.closest(`.${VISUALLY_HIDDEN}`) &&
|
|
21
|
+
// Text overflows
|
|
22
|
+
labelElement.offsetWidth < labelElement.scrollWidth
|
|
23
|
+
) {
|
|
24
|
+
// Set tooltip label
|
|
25
|
+
setTooltipLabel(labelElement.innerText);
|
|
26
|
+
} else {
|
|
27
|
+
setTooltipLabel(undefined);
|
|
28
|
+
}
|
|
29
|
+
}, [labelElement, parentTooltip, content]);
|
|
27
30
|
|
|
28
|
-
return { labelRef, tooltipLabel };
|
|
31
|
+
return { labelRef: setLabelElement, tooltipLabel };
|
|
29
32
|
};
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
Switch,
|
|
14
14
|
TextField,
|
|
15
15
|
Thumbnail,
|
|
16
|
+
Uploader,
|
|
16
17
|
} from '@lumx/react';
|
|
17
18
|
import { DisabledStateProvider } from '@lumx/react/utils';
|
|
18
19
|
import { getSelectArgType } from '@lumx/react/stories/controls/selectArgType';
|
|
@@ -87,6 +88,7 @@ export const AllComponents = {
|
|
|
87
88
|
<Switch>Switch</Switch>
|
|
88
89
|
<TextField label="texfield" onChange={() => {}} value="" />
|
|
89
90
|
<Thumbnail alt="Thumbnail" image={LANDSCAPE_IMAGES.landscape1s200} onClick={() => {}} />
|
|
91
|
+
<Uploader label="Upload a file" fileInputProps={{ onChange: () => {} }} />
|
|
90
92
|
</DisabledStateProvider>
|
|
91
93
|
),
|
|
92
94
|
};
|