@lumx/react 3.7.6-test.1 → 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/package.json CHANGED
@@ -6,9 +6,8 @@
6
6
  "url": "https://github.com/lumapps/design-system/issues"
7
7
  },
8
8
  "dependencies": {
9
- "@juggle/resize-observer": "^3.2.0",
10
- "@lumx/core": "^3.7.6-test.1",
11
- "@lumx/icons": "^3.7.6-test.1",
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.7.6-test.1"
114
+ "version": "3.8.0"
116
115
  }
@@ -33,6 +33,15 @@ export const LabelAndHelper = {
33
33
  },
34
34
  };
35
35
 
36
+ /**
37
+ * With intermediate state
38
+ */
39
+ export const IntermediateState = {
40
+ args: {
41
+ isChecked: 'intermediate',
42
+ },
43
+ };
44
+
36
45
  /**
37
46
  * Disabled
38
47
  */
@@ -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, { useMemo, forwardRef, ReactNode, SyntheticEvent, InputHTMLAttributes } from '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
- isChecked,
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 { mergeRefs } from '@lumx/react/utils/mergeRefs';
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={mergeRefs(setImgElement, propImgRef)}
214
+ ref={useMergeRefs(setImgElement, propImgRef)}
196
215
  className={classNames(
197
216
  handleBasicClasses({
198
217
  prefix: `${CLASSNAME}__image`,
@@ -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
- }