@lumx/react 3.7.6-test.0 → 3.8.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 +6 -4
- package/index.js +39 -9
- package/index.js.map +1 -1
- package/package.json +3 -4
- package/src/components/checkbox/Checkbox.stories.tsx +9 -0
- package/src/components/checkbox/Checkbox.test.tsx +12 -0
- package/src/components/checkbox/Checkbox.tsx +25 -9
- package/src/components/thumbnail/Thumbnail.stories.tsx +35 -0
- package/src/components/thumbnail/Thumbnail.tsx +21 -2
- package/src/hooks/useOnResize.ts +0 -41
package/package.json
CHANGED
|
@@ -6,9 +6,8 @@
|
|
|
6
6
|
"url": "https://github.com/lumapps/design-system/issues"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@
|
|
10
|
-
"@lumx/
|
|
11
|
-
"@lumx/icons": "^3.7.6-test.0",
|
|
9
|
+
"@lumx/core": "^3.8.0",
|
|
10
|
+
"@lumx/icons": "^3.8.0",
|
|
12
11
|
"@popperjs/core": "^2.5.4",
|
|
13
12
|
"body-scroll-lock": "^3.1.5",
|
|
14
13
|
"classnames": "^2.3.2",
|
|
@@ -112,5 +111,5 @@
|
|
|
112
111
|
"build:storybook": "storybook build"
|
|
113
112
|
},
|
|
114
113
|
"sideEffects": false,
|
|
115
|
-
"version": "3.
|
|
114
|
+
"version": "3.8.0"
|
|
116
115
|
}
|
|
@@ -39,6 +39,7 @@ describe(`<${Checkbox.displayName}>`, () => {
|
|
|
39
39
|
|
|
40
40
|
expect(input).toBeInTheDocument();
|
|
41
41
|
expect(input).not.toBeChecked();
|
|
42
|
+
expect(input).toHaveAttribute('aria-checked', 'false');
|
|
42
43
|
expect(input).not.toBeDisabled();
|
|
43
44
|
});
|
|
44
45
|
|
|
@@ -51,9 +52,20 @@ describe(`<${Checkbox.displayName}>`, () => {
|
|
|
51
52
|
expect(checkbox).toHaveClass('lumx-checkbox--is-checked');
|
|
52
53
|
|
|
53
54
|
expect(input).toBeChecked();
|
|
55
|
+
expect(input).toHaveAttribute('aria-checked', 'true');
|
|
54
56
|
expect(input).toBeDisabled();
|
|
55
57
|
});
|
|
56
58
|
|
|
59
|
+
it('should render intermediate state', () => {
|
|
60
|
+
const { checkbox, input } = setup({
|
|
61
|
+
isChecked: 'intermediate',
|
|
62
|
+
});
|
|
63
|
+
expect(checkbox).toHaveClass('lumx-checkbox--is-checked');
|
|
64
|
+
|
|
65
|
+
expect(input).toBeChecked();
|
|
66
|
+
expect(input).toHaveAttribute('aria-checked', 'mixed');
|
|
67
|
+
});
|
|
68
|
+
|
|
57
69
|
it('should render helper and label', () => {
|
|
58
70
|
const id = 'checkbox1';
|
|
59
71
|
const { props, helper, label, input } = setup({
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { forwardRef, InputHTMLAttributes, ReactNode, SyntheticEvent, useMemo } from 'react';
|
|
2
2
|
|
|
3
3
|
import classNames from 'classnames';
|
|
4
4
|
import { uid } from 'uid';
|
|
5
5
|
|
|
6
|
-
import { mdiCheck } from '@lumx/icons';
|
|
6
|
+
import { mdiCheck, mdiMinus } from '@lumx/icons';
|
|
7
7
|
|
|
8
8
|
import { Icon, InputHelper, InputLabel, Theme } from '@lumx/react';
|
|
9
9
|
import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
|
|
10
10
|
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
|
|
11
|
+
import { useMergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Intermediate state of checkbox.
|
|
15
|
+
*/
|
|
16
|
+
const INTERMEDIATE_STATE = 'intermediate';
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Defines the props of the component.
|
|
@@ -19,8 +25,8 @@ export interface CheckboxProps extends GenericProps, HasTheme {
|
|
|
19
25
|
id?: string;
|
|
20
26
|
/** Native input ref. */
|
|
21
27
|
inputRef?: React.Ref<HTMLInputElement>;
|
|
22
|
-
/** Whether it is checked or not. */
|
|
23
|
-
isChecked?: boolean;
|
|
28
|
+
/** Whether it is checked or not or intermediate. */
|
|
29
|
+
isChecked?: boolean | 'intermediate';
|
|
24
30
|
/** Whether the component is disabled or not. */
|
|
25
31
|
isDisabled?: boolean;
|
|
26
32
|
/** Label text. */
|
|
@@ -29,10 +35,10 @@ export interface CheckboxProps extends GenericProps, HasTheme {
|
|
|
29
35
|
name?: string;
|
|
30
36
|
/** Native input value property. */
|
|
31
37
|
value?: string;
|
|
32
|
-
/** On change callback. */
|
|
33
|
-
onChange?(isChecked: boolean, value?: string, name?: string, event?: SyntheticEvent): void;
|
|
34
38
|
/** optional props for input */
|
|
35
39
|
inputProps?: InputHTMLAttributes<HTMLInputElement>;
|
|
40
|
+
/** On change callback. */
|
|
41
|
+
onChange?(isChecked: boolean, value?: string, name?: string, event?: SyntheticEvent): void;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
/**
|
|
@@ -77,6 +83,7 @@ export const Checkbox: Comp<CheckboxProps, HTMLDivElement> = forwardRef((props,
|
|
|
77
83
|
inputProps = {},
|
|
78
84
|
...forwardedProps
|
|
79
85
|
} = props;
|
|
86
|
+
const localInputRef = React.useRef<HTMLInputElement>(null);
|
|
80
87
|
const inputId = useMemo(() => id || `${CLASSNAME.toLowerCase()}-${uid()}`, [id]);
|
|
81
88
|
|
|
82
89
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -85,6 +92,13 @@ export const Checkbox: Comp<CheckboxProps, HTMLDivElement> = forwardRef((props,
|
|
|
85
92
|
}
|
|
86
93
|
};
|
|
87
94
|
|
|
95
|
+
const intermediateState = isChecked === INTERMEDIATE_STATE;
|
|
96
|
+
|
|
97
|
+
React.useEffect(() => {
|
|
98
|
+
const input = localInputRef.current;
|
|
99
|
+
if (input) input.indeterminate = intermediateState;
|
|
100
|
+
}, [intermediateState]);
|
|
101
|
+
|
|
88
102
|
return (
|
|
89
103
|
<div
|
|
90
104
|
ref={ref}
|
|
@@ -92,7 +106,8 @@ export const Checkbox: Comp<CheckboxProps, HTMLDivElement> = forwardRef((props,
|
|
|
92
106
|
className={classNames(
|
|
93
107
|
className,
|
|
94
108
|
handleBasicClasses({
|
|
95
|
-
|
|
109
|
+
// Whether state is intermediate class name will "-checked"
|
|
110
|
+
isChecked: intermediateState ? true : isChecked,
|
|
96
111
|
isDisabled,
|
|
97
112
|
isUnchecked: !isChecked,
|
|
98
113
|
prefix: CLASSNAME,
|
|
@@ -102,7 +117,7 @@ export const Checkbox: Comp<CheckboxProps, HTMLDivElement> = forwardRef((props,
|
|
|
102
117
|
>
|
|
103
118
|
<div className={`${CLASSNAME}__input-wrapper`}>
|
|
104
119
|
<input
|
|
105
|
-
ref={inputRef}
|
|
120
|
+
ref={useMergeRefs(inputRef, localInputRef)}
|
|
106
121
|
type="checkbox"
|
|
107
122
|
id={inputId}
|
|
108
123
|
className={`${CLASSNAME}__input-native`}
|
|
@@ -113,6 +128,7 @@ export const Checkbox: Comp<CheckboxProps, HTMLDivElement> = forwardRef((props,
|
|
|
113
128
|
checked={isChecked}
|
|
114
129
|
onChange={handleChange}
|
|
115
130
|
aria-describedby={helper ? `${inputId}-helper` : undefined}
|
|
131
|
+
aria-checked={intermediateState ? 'mixed' : Boolean(isChecked)}
|
|
116
132
|
{...inputProps}
|
|
117
133
|
/>
|
|
118
134
|
|
|
@@ -120,7 +136,7 @@ export const Checkbox: Comp<CheckboxProps, HTMLDivElement> = forwardRef((props,
|
|
|
120
136
|
<div className={`${CLASSNAME}__input-background`} />
|
|
121
137
|
|
|
122
138
|
<div className={`${CLASSNAME}__input-indicator`}>
|
|
123
|
-
<Icon icon={mdiCheck} />
|
|
139
|
+
<Icon icon={intermediateState ? mdiMinus : mdiCheck} />
|
|
124
140
|
</div>
|
|
125
141
|
</div>
|
|
126
142
|
</div>
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
+
import uniqueId from 'lodash/uniqueId';
|
|
4
|
+
|
|
3
5
|
import { mdiAbTesting } from '@lumx/icons';
|
|
4
6
|
import {
|
|
5
7
|
Alignment,
|
|
6
8
|
AspectRatio,
|
|
7
9
|
Badge,
|
|
10
|
+
Button,
|
|
8
11
|
FlexBox,
|
|
9
12
|
GridColumn,
|
|
10
13
|
Icon,
|
|
@@ -411,3 +414,35 @@ export const ObjectFit = {
|
|
|
411
414
|
withWrapper({ maxColumns: 3, itemMinWidth: 350 }, GridColumn),
|
|
412
415
|
],
|
|
413
416
|
};
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Demonstrate loading a small image and then use it as the loading placeholder image when loading a bigger image
|
|
420
|
+
*/
|
|
421
|
+
export const LoadingPlaceholderImage = () => {
|
|
422
|
+
const [isShown, setShown] = React.useState(false);
|
|
423
|
+
const imgRef = React.useRef() as React.RefObject<HTMLImageElement>;
|
|
424
|
+
return (
|
|
425
|
+
<>
|
|
426
|
+
<Button onClick={() => setShown((shown) => !shown)}>
|
|
427
|
+
Display bigger image using the small image as a placeholder
|
|
428
|
+
</Button>
|
|
429
|
+
<FlexBox orientation="horizontal">
|
|
430
|
+
<Thumbnail alt="Small image" imgRef={imgRef} image="https://picsum.photos/id/15/128/85" />
|
|
431
|
+
{isShown && (
|
|
432
|
+
<div style={{ maxHeight: 400 }}>
|
|
433
|
+
<Thumbnail
|
|
434
|
+
image={`https://picsum.photos/id/15/2500/1667?cacheBust${uniqueId()}`}
|
|
435
|
+
alt="Large image"
|
|
436
|
+
// Loading placeholder image
|
|
437
|
+
loadingPlaceholderImageRef={imgRef}
|
|
438
|
+
// Reserve space
|
|
439
|
+
imgProps={{ width: 2500, height: 1667 }}
|
|
440
|
+
/>
|
|
441
|
+
</div>
|
|
442
|
+
)}
|
|
443
|
+
</FlexBox>
|
|
444
|
+
</>
|
|
445
|
+
);
|
|
446
|
+
};
|
|
447
|
+
// Disables Chromatic snapshot (not relevant for this story).
|
|
448
|
+
LoadingPlaceholderImage.parameters = { chromatic: { disable: true } };
|
|
@@ -17,7 +17,7 @@ import { Comp, Falsy, GenericProps, HasTheme } from '@lumx/react/utils/type';
|
|
|
17
17
|
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
|
|
18
18
|
|
|
19
19
|
import { mdiImageBroken } from '@lumx/icons';
|
|
20
|
-
import {
|
|
20
|
+
import { useMergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
21
21
|
import { useImageLoad } from '@lumx/react/components/thumbnail/useImageLoad';
|
|
22
22
|
import { useFocusPointStyle } from '@lumx/react/components/thumbnail/useFocusPointStyle';
|
|
23
23
|
import { FocusPoint, ThumbnailSize, ThumbnailVariant } from './types';
|
|
@@ -58,6 +58,8 @@ export interface ThumbnailProps extends GenericProps, HasTheme {
|
|
|
58
58
|
size?: ThumbnailSize;
|
|
59
59
|
/** Image loading mode. */
|
|
60
60
|
loading?: ImgHTMLProps['loading'];
|
|
61
|
+
/** Ref of an existing placeholder image to display while loading. */
|
|
62
|
+
loadingPlaceholderImageRef?: React.RefObject<HTMLImageElement>;
|
|
61
63
|
/** On click callback. */
|
|
62
64
|
onClick?: MouseEventHandler<HTMLDivElement>;
|
|
63
65
|
/** On key press callback. */
|
|
@@ -115,6 +117,7 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
|
|
|
115
117
|
isLoading: isLoadingProp,
|
|
116
118
|
objectFit,
|
|
117
119
|
loading,
|
|
120
|
+
loadingPlaceholderImageRef,
|
|
118
121
|
size,
|
|
119
122
|
theme,
|
|
120
123
|
variant,
|
|
@@ -159,6 +162,16 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
|
|
|
159
162
|
wrapperProps['aria-label'] = forwardedProps['aria-label'] || alt;
|
|
160
163
|
}
|
|
161
164
|
|
|
165
|
+
// If we have a loading placeholder image that is really loaded (complete)
|
|
166
|
+
const loadingPlaceholderImage =
|
|
167
|
+
(isLoading && loadingPlaceholderImageRef?.current?.complete && loadingPlaceholderImageRef?.current) ||
|
|
168
|
+
undefined;
|
|
169
|
+
|
|
170
|
+
// Set loading placeholder image as background
|
|
171
|
+
const loadingStyle = loadingPlaceholderImage
|
|
172
|
+
? { backgroundImage: `url(${loadingPlaceholderImage.src})` }
|
|
173
|
+
: undefined;
|
|
174
|
+
|
|
162
175
|
return (
|
|
163
176
|
<Wrapper
|
|
164
177
|
{...wrapperProps}
|
|
@@ -186,13 +199,19 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
|
|
|
186
199
|
>
|
|
187
200
|
<span className={`${CLASSNAME}__background`}>
|
|
188
201
|
<img
|
|
202
|
+
// Use placeholder image size
|
|
203
|
+
width={loadingPlaceholderImage?.naturalWidth}
|
|
204
|
+
height={loadingPlaceholderImage?.naturalHeight}
|
|
189
205
|
{...imgProps}
|
|
190
206
|
style={{
|
|
207
|
+
// Reserve space while loading (when possible)
|
|
208
|
+
width: isLoading ? imgProps?.width || loadingPlaceholderImage?.naturalWidth : undefined,
|
|
191
209
|
...imgProps?.style,
|
|
192
210
|
...imageErrorStyle,
|
|
193
211
|
...focusPointStyle,
|
|
212
|
+
...loadingStyle,
|
|
194
213
|
}}
|
|
195
|
-
ref={
|
|
214
|
+
ref={useMergeRefs(setImgElement, propImgRef)}
|
|
196
215
|
className={classNames(
|
|
197
216
|
handleBasicClasses({
|
|
198
217
|
prefix: `${CLASSNAME}__image`,
|
package/src/hooks/useOnResize.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { Callback, Falsy } from '@lumx/react/utils/type';
|
|
2
|
-
import { MutableRefObject, RefObject, useEffect, useRef } from 'react';
|
|
3
|
-
import { WINDOW } from '@lumx/react/constants';
|
|
4
|
-
import { ResizeObserver as Polyfill } from '@juggle/resize-observer';
|
|
5
|
-
|
|
6
|
-
const ResizeObserver: typeof Polyfill = (WINDOW as any)?.ResizeObserver || Polyfill;
|
|
7
|
-
|
|
8
|
-
export function useOnResize(element: HTMLElement | Falsy, update: RefObject<Callback>): void {
|
|
9
|
-
const observerRef = useRef(null) as MutableRefObject<Polyfill | null>;
|
|
10
|
-
const previousSize = useRef<{ width: number; height: number }>();
|
|
11
|
-
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
if (!element || !update) {
|
|
14
|
-
return undefined;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
previousSize.current = undefined;
|
|
18
|
-
const observer =
|
|
19
|
-
observerRef.current ||
|
|
20
|
-
new ResizeObserver(([entry]) => {
|
|
21
|
-
const updateFunction = update.current;
|
|
22
|
-
if (!updateFunction) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const { width, height } = entry.contentRect;
|
|
27
|
-
if (previousSize.current?.width === width && previousSize.current?.height === height) {
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
window.requestAnimationFrame(() => updateFunction());
|
|
32
|
-
previousSize.current = entry.contentRect;
|
|
33
|
-
});
|
|
34
|
-
if (!observerRef.current) observerRef.current = observer;
|
|
35
|
-
|
|
36
|
-
observer.observe(element);
|
|
37
|
-
return () => {
|
|
38
|
-
observer.unobserve(element);
|
|
39
|
-
};
|
|
40
|
-
}, [element, update]);
|
|
41
|
-
}
|