@lumx/react 3.8.2-alpha.1 → 3.9.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.
Files changed (38) hide show
  1. package/index.d.ts +1 -1
  2. package/index.js +55 -41
  3. package/index.js.map +1 -1
  4. package/package.json +4 -5
  5. package/src/components/alert-dialog/AlertDialog.test.tsx +1 -1
  6. package/src/components/alert-dialog/AlertDialog.tsx +3 -2
  7. package/src/components/checkbox/Checkbox.tsx +4 -3
  8. package/src/components/date-picker/DatePickerControlled.tsx +2 -3
  9. package/src/components/image-lightbox/ImageLightbox.test.tsx +10 -18
  10. package/src/components/image-lightbox/internal/ImageSlide.tsx +2 -2
  11. package/src/components/message/Message.tsx +1 -1
  12. package/src/components/navigation/NavigationSection.tsx +4 -3
  13. package/src/components/notification/constants.ts +1 -1
  14. package/src/components/radio-button/RadioButton.tsx +4 -3
  15. package/src/components/select/Select.test.tsx +1 -1
  16. package/src/components/select/SelectMultiple.test.tsx +1 -1
  17. package/src/components/select/WithSelectContext.tsx +4 -3
  18. package/src/components/side-navigation/SideNavigationItem.test.tsx +1 -1
  19. package/src/components/side-navigation/SideNavigationItem.tsx +2 -2
  20. package/src/components/slider/Slider.test.tsx +1 -1
  21. package/src/components/slider/Slider.tsx +3 -2
  22. package/src/components/switch/Switch.test.tsx +1 -1
  23. package/src/components/switch/Switch.tsx +4 -3
  24. package/src/components/tabs/state.ts +4 -6
  25. package/src/components/text-field/TextField.tsx +6 -15
  26. package/src/components/tooltip/Tooltip.test.tsx +1 -1
  27. package/src/components/tooltip/Tooltip.tsx +4 -4
  28. package/src/components/uploader/Uploader.tsx +3 -2
  29. package/src/components/user-block/UserBlock.stories.tsx +1 -1
  30. package/src/components/user-block/UserBlock.tsx +2 -2
  31. package/src/hooks/useId.test.tsx +23 -0
  32. package/src/hooks/useId.ts +15 -0
  33. package/src/hooks/{useElementSizeDependentOfWindowSize.ts → useSizeOnWindowResize.ts} +1 -3
  34. package/src/hooks/useSlideshowControls.ts +7 -4
  35. package/src/stories/generated/UserBlock/Demos.stories.tsx +1 -0
  36. package/src/utils/date/formatDayNumber.test.ts +12 -0
  37. package/src/utils/date/formatDayNumber.ts +5 -0
  38. package/src/utils/unref.ts +0 -0
package/package.json CHANGED
@@ -6,14 +6,13 @@
6
6
  "url": "https://github.com/lumapps/design-system/issues"
7
7
  },
8
8
  "dependencies": {
9
- "@lumx/core": "^3.8.2-alpha.1",
10
- "@lumx/icons": "^3.8.2-alpha.1",
9
+ "@lumx/core": "^3.9.0",
10
+ "@lumx/icons": "^3.9.0",
11
11
  "@popperjs/core": "^2.5.4",
12
12
  "body-scroll-lock": "^3.1.5",
13
13
  "classnames": "^2.3.2",
14
14
  "react-is": ">=16.13.0",
15
- "react-popper": "^2.2.4",
16
- "uid": "^2.0.0"
15
+ "react-popper": "^2.2.4"
17
16
  },
18
17
  "devDependencies": {
19
18
  "@babel/core": "^7.18.13",
@@ -111,5 +110,5 @@
111
110
  "build:storybook": "storybook build"
112
111
  },
113
112
  "sideEffects": false,
114
- "version": "3.8.2-alpha.1"
113
+ "version": "3.9.0"
115
114
  }
@@ -5,7 +5,7 @@ import { queryByClassName } from '@lumx/react/testing/utils/queries';
5
5
  import { render } from '@testing-library/react';
6
6
  import { AlertDialog, AlertDialogProps } from './AlertDialog';
7
7
 
8
- jest.mock('uid', () => ({ uid: () => 'uid' }));
8
+ jest.mock('@lumx/react/hooks/useId', () => ({ useId: () => ':r1:' }));
9
9
 
10
10
  const CLASSNAME = AlertDialog.className as string;
11
11
 
@@ -16,9 +16,9 @@ import {
16
16
  } from '@lumx/react';
17
17
 
18
18
  import { mdiAlert, mdiAlertCircle, mdiCheckCircle, mdiInformation } from '@lumx/icons';
19
- import { uid } from 'uid';
20
19
  import { Comp } from '@lumx/react/utils/type';
21
20
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
21
+ import { useId } from '@lumx/react/hooks/useId';
22
22
 
23
23
  export interface AlertDialogProps extends Omit<DialogProps, 'header' | 'footer'> {
24
24
  /** Message variant. */
@@ -86,7 +86,8 @@ export const AlertDialog: Comp<AlertDialogProps, HTMLDivElement> = forwardRef((p
86
86
  const { color, icon } = CONFIG[kind as Kind] || {};
87
87
 
88
88
  // Define a unique ID to target title and description for aria attributes.
89
- const uniqueId = React.useMemo(() => id || uid(), [id]);
89
+ const generatedId = useId();
90
+ const uniqueId = id || generatedId;
90
91
  const titleId = `${uniqueId}-title`;
91
92
  const descriptionId = `${uniqueId}-description`;
92
93
 
@@ -1,13 +1,13 @@
1
- import React, { forwardRef, InputHTMLAttributes, ReactNode, SyntheticEvent, useMemo } from 'react';
1
+ import React, { forwardRef, InputHTMLAttributes, ReactNode, SyntheticEvent } from 'react';
2
2
 
3
3
  import classNames from 'classnames';
4
- import { uid } from 'uid';
5
4
 
6
5
  import { mdiCheck, mdiMinus } from '@lumx/icons';
7
6
 
8
7
  import { Icon, InputHelper, InputLabel, Theme } from '@lumx/react';
9
8
  import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
10
9
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
10
+ import { useId } from '@lumx/react/hooks/useId';
11
11
  import { useMergeRefs } from '@lumx/react/utils/mergeRefs';
12
12
 
13
13
  /**
@@ -84,7 +84,8 @@ export const Checkbox: Comp<CheckboxProps, HTMLDivElement> = forwardRef((props,
84
84
  ...forwardedProps
85
85
  } = props;
86
86
  const localInputRef = React.useRef<HTMLInputElement>(null);
87
- const inputId = useMemo(() => id || `${CLASSNAME.toLowerCase()}-${uid()}`, [id]);
87
+ const generatedInputId = useId();
88
+ const inputId = id || generatedInputId;
88
89
 
89
90
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
90
91
  if (onChange) {
@@ -12,6 +12,7 @@ import { usePreviousValue } from '@lumx/react/hooks/usePreviousValue';
12
12
  import { getYearDisplayName } from '@lumx/react/utils/date/getYearDisplayName';
13
13
  import { onEnterPressed } from '@lumx/react/utils/event';
14
14
  import { addMonthResetDay } from '@lumx/react/utils/date/addMonthResetDay';
15
+ import { formatDayNumber } from '@lumx/react/utils/date/formatDayNumber';
15
16
  import { CLASSNAME } from './constants';
16
17
 
17
18
  /**
@@ -198,9 +199,7 @@ export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElemen
198
199
  type="button"
199
200
  onClick={() => onChange(date)}
200
201
  >
201
- <span aria-hidden>
202
- {date.toLocaleDateString(locale, { day: 'numeric' })}
203
- </span>
202
+ <span aria-hidden>{formatDayNumber(locale, date)}</span>
204
203
  <span className="visually-hidden">
205
204
  {date.toLocaleDateString(locale, {
206
205
  day: 'numeric',
@@ -5,7 +5,7 @@ import { render, within, screen } from '@testing-library/react';
5
5
  import { getByClassName, queryByClassName } from '@lumx/react/testing/utils/queries';
6
6
  import userEvent from '@testing-library/user-event';
7
7
  import { useImageSize } from '@lumx/react/hooks/useImageSize';
8
- import { useElementSizeDependentOfWindowSize } from '@lumx/react/hooks/useElementSizeDependentOfWindowSize';
8
+ import { useSizeOnWindowResize } from '@lumx/react/hooks/useSizeOnWindowResize';
9
9
 
10
10
  import { ImageLightbox } from './ImageLightbox';
11
11
  import { ImageLightboxProps } from './types';
@@ -19,7 +19,7 @@ import Meta, {
19
19
  } from './ImageLightbox.stories';
20
20
 
21
21
  jest.mock('@lumx/react/hooks/useImageSize');
22
- jest.mock('@lumx/react/hooks/useElementSizeDependentOfWindowSize');
22
+ jest.mock('@lumx/react/hooks/useSizeOnWindowResize');
23
23
 
24
24
  const CLASSNAME = ImageLightbox.className as string;
25
25
  const baseProps = Meta.args;
@@ -56,7 +56,7 @@ const queries = {
56
56
  describe(`<${ImageLightbox.displayName}>`, () => {
57
57
  beforeEach(() => {
58
58
  (useImageSize as any).mockReturnValue(null);
59
- (useElementSizeDependentOfWindowSize as any).mockReturnValue([null, jest.fn()]);
59
+ (useSizeOnWindowResize as any).mockReturnValue([null, jest.fn()]);
60
60
  });
61
61
 
62
62
  describe('render', () => {
@@ -189,7 +189,7 @@ describe(`<${ImageLightbox.displayName}>`, () => {
189
189
  const scrollAreaSize = { width: 600, height: 600 };
190
190
  beforeEach(() => {
191
191
  (useImageSize as any).mockImplementation((_: any, getInitialSize: any) => getInitialSize?.() || null);
192
- (useElementSizeDependentOfWindowSize as any).mockReturnValue([scrollAreaSize, jest.fn()]);
192
+ (useSizeOnWindowResize as any).mockReturnValue([scrollAreaSize, jest.fn()]);
193
193
  });
194
194
 
195
195
  it('should use the image initial size', () => {
@@ -207,23 +207,15 @@ describe(`<${ImageLightbox.displayName}>`, () => {
207
207
  });
208
208
 
209
209
  it('should zoom on zoom button pressed', async () => {
210
- const { rerender } = setup(SingleImageWithZoom.args);
211
- const imageLightbox = queries.getImageLightbox();
212
-
213
- // Initial image style
214
- const image = queries.queryImage(imageLightbox, 'Image 1');
215
-
216
- expect(image).toHaveStyle({
217
- maxHeight: `${scrollAreaSize.height}px`,
218
- maxWidth: `${scrollAreaSize.width}px`,
219
- });
220
-
221
- // Update image size (simulate image loaded)
210
+ // Set image size (simulate image loaded)
222
211
  const imageSize = { width: 500, height: 300 };
223
212
  (useImageSize as any).mockReturnValue(imageSize);
224
- rerender();
225
213
 
226
- // Image style updated
214
+ setup(SingleImageWithZoom.args);
215
+ const imageLightbox = queries.getImageLightbox();
216
+
217
+ // Image style
218
+ const image = queries.queryImage(imageLightbox, 'Image 1');
227
219
  expect(image).toHaveStyle({ width: `${imageSize.width}px`, height: `${imageSize.height}px` });
228
220
 
229
221
  // Scroll area is bigger than the image, it should not be focusable
@@ -2,7 +2,7 @@ import React from 'react';
2
2
 
3
3
  import { SlideshowItem, Thumbnail } from '@lumx/react';
4
4
  import { useMergeRefs } from '@lumx/react/utils/mergeRefs';
5
- import { useElementSizeDependentOfWindowSize } from '@lumx/react/hooks/useElementSizeDependentOfWindowSize';
5
+ import { useSizeOnWindowResize } from '@lumx/react/hooks/useSizeOnWindowResize';
6
6
  import { useImageSize } from '@lumx/react/hooks/useImageSize';
7
7
  import { getPrefersReducedMotion } from '@lumx/react/utils/browser/getPrefersReducedMotion';
8
8
  import { isEqual } from '@lumx/react/utils/object/isEqual';
@@ -30,7 +30,7 @@ export const ImageSlide = React.memo((props: ImageSlideProps) => {
30
30
 
31
31
  // Get scroll area size
32
32
  const scrollAreaRef = React.useRef<HTMLDivElement>(null);
33
- const [scrollAreaSize, updateSize] = useElementSizeDependentOfWindowSize(scrollAreaRef);
33
+ const [scrollAreaSize, updateSize] = useSizeOnWindowResize(scrollAreaRef);
34
34
  React.useEffect(() => {
35
35
  // Update size when active
36
36
  if (isActive) updateSize();
@@ -45,7 +45,7 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
45
45
  */
46
46
  const CONFIG = {
47
47
  [Kind.error]: { color: ColorPalette.red, icon: mdiAlert },
48
- [Kind.info]: { color: ColorPalette.dark, icon: mdiInformation },
48
+ [Kind.info]: { color: ColorPalette.blue, icon: mdiInformation },
49
49
  [Kind.success]: { color: ColorPalette.green, icon: mdiCheckCircle },
50
50
  [Kind.warning]: { color: ColorPalette.yellow, icon: mdiAlertCircle },
51
51
  };
@@ -1,12 +1,13 @@
1
- import React, { forwardRef, ReactNode, useRef, useState, useMemo, useContext } from 'react';
1
+ import React, { forwardRef, ReactNode, useRef, useState, useContext } from 'react';
2
2
 
3
3
  import { mdiChevronDown, mdiChevronUp } from '@lumx/icons';
4
4
  import { Icon, Size, Text, Orientation, Popover, Placement, Theme } from '@lumx/react';
5
- import uniqueId from 'lodash/uniqueId';
6
5
  import classNames from 'classnames';
7
6
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
8
7
  import { HasClassName } from '@lumx/react/utils/type';
9
8
  import { ThemeContext } from '@lumx/react/utils/ThemeContext';
9
+ import { useId } from '@lumx/react/hooks/useId';
10
+
10
11
  import { CLASSNAME as ITEM_CLASSNAME } from './NavigationItem';
11
12
  import { NavigationContext } from './context';
12
13
 
@@ -34,7 +35,7 @@ export const NavigationSection = Object.assign(
34
35
  const { children, className, label, icon, ...forwardedProps } = props;
35
36
  const [isOpen, setIsOpen] = useState(false);
36
37
  const buttonRef = useRef<HTMLButtonElement>(null);
37
- const sectionId = useMemo(() => uniqueId('section'), []);
38
+ const sectionId = useId();
38
39
  const { orientation } = useContext(NavigationContext) || {};
39
40
  const theme = useContext(ThemeContext);
40
41
  const isDropdown = orientation === Orientation.horizontal;
@@ -14,7 +14,7 @@ export const NOTIFICATION_CONFIGURATION = {
14
14
  icon: mdiAlert,
15
15
  },
16
16
  info: {
17
- color: 'dark',
17
+ color: 'blue',
18
18
  icon: mdiInformation,
19
19
  },
20
20
  success: {
@@ -1,12 +1,12 @@
1
- import React, { useMemo, forwardRef, ReactNode, SyntheticEvent, InputHTMLAttributes } from 'react';
1
+ import React, { forwardRef, ReactNode, SyntheticEvent, InputHTMLAttributes } from 'react';
2
2
 
3
3
  import classNames from 'classnames';
4
- import { uid } from 'uid';
5
4
 
6
5
  import { InputHelper, InputLabel, Theme } from '@lumx/react';
7
6
 
8
7
  import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
9
8
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
9
+ import { useId } from '@lumx/react/hooks/useId';
10
10
 
11
11
  /**
12
12
  * Defines the props of the component.
@@ -76,7 +76,8 @@ export const RadioButton: Comp<RadioButtonProps, HTMLDivElement> = forwardRef((p
76
76
  inputProps,
77
77
  ...forwardedProps
78
78
  } = props;
79
- const inputId = useMemo(() => id || `${CLASSNAME.toLowerCase()}-${uid()}`, [id]);
79
+ const generatedInputId = useId();
80
+ const inputId = id || generatedInputId;
80
81
 
81
82
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
82
83
  if (onChange) {
@@ -14,7 +14,7 @@ import { Select, SelectProps, SelectVariant } from './Select';
14
14
  const CLASSNAME = Select.className as string;
15
15
 
16
16
  jest.mock('@lumx/react/utils/isFocusVisible');
17
- jest.mock('uid', () => ({ uid: () => 'uid' }));
17
+ jest.mock('@lumx/react/hooks/useId', () => ({ useId: () => ':r1:' }));
18
18
 
19
19
  /**
20
20
  * Mounts the component and returns common DOM elements / data needed in multiple tests further down.
@@ -18,7 +18,7 @@ import { SelectVariant } from './constants';
18
18
 
19
19
  const CLASSNAME = SelectMultiple.className as string;
20
20
 
21
- jest.mock('uid', () => ({ uid: () => 'uid' }));
21
+ jest.mock('@lumx/react/hooks/useId', () => ({ useId: () => ':r1:' }));
22
22
 
23
23
  /**
24
24
  * Mounts the component and returns common DOM elements / data needed in multiple tests further down.
@@ -1,6 +1,5 @@
1
1
  import classNames from 'classnames';
2
- import React, { Ref, useCallback, useMemo, useRef } from 'react';
3
- import { uid } from 'uid';
2
+ import React, { Ref, useCallback, useRef } from 'react';
4
3
 
5
4
  import { Placement } from '@lumx/react';
6
5
  import { Kind, Theme } from '@lumx/react/components';
@@ -11,6 +10,7 @@ import { useListenFocus } from '@lumx/react/hooks/useListenFocus';
11
10
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
12
11
  import { mergeRefs } from '@lumx/react/utils/mergeRefs';
13
12
 
13
+ import { useId } from '@lumx/react/hooks/useId';
14
14
  import { CoreSelectProps, SelectVariant } from './constants';
15
15
 
16
16
  /** The display name of the component. */
@@ -56,7 +56,8 @@ export const WithSelectContext = (
56
56
  }: CoreSelectProps,
57
57
  ref: Ref<HTMLDivElement>,
58
58
  ): React.ReactElement => {
59
- const selectId = useMemo(() => id || `select-${uid()}`, [id]);
59
+ const generatedSelectId = useId();
60
+ const selectId = id || generatedSelectId;
60
61
  const anchorRef = useRef<HTMLElement>(null);
61
62
  const selectRef = useRef<HTMLDivElement>(null);
62
63
  const dropdownRef = useRef<HTMLDivElement>(null);
@@ -10,7 +10,7 @@ const CLASSNAME = SideNavigationItem.className as string;
10
10
 
11
11
  const toggleButtonProps = { label: 'Toggle' };
12
12
 
13
- jest.mock('uid', () => ({ uid: () => 'id' }));
13
+ jest.mock('@lumx/react/hooks/useId', () => ({ useId: () => ':r1:' }));
14
14
 
15
15
  /**
16
16
  * Mounts the component and returns common DOM elements / data needed in multiple tests further down.
@@ -2,7 +2,6 @@ import React, { Children, forwardRef, ReactNode } from 'react';
2
2
 
3
3
  import classNames from 'classnames';
4
4
  import isEmpty from 'lodash/isEmpty';
5
- import { uid } from 'uid';
6
5
 
7
6
  import { mdiChevronDown, mdiChevronUp } from '@lumx/icons';
8
7
 
@@ -12,6 +11,7 @@ import { Comp, GenericProps, isComponent } from '@lumx/react/utils/type';
12
11
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
13
12
  import { renderLink } from '@lumx/react/utils/renderLink';
14
13
  import { renderButtonOrLink } from '@lumx/react/utils/renderButtonOrLink';
14
+ import { useId } from '@lumx/react/hooks/useId';
15
15
 
16
16
  /**
17
17
  * Defines the props of the component.
@@ -95,7 +95,7 @@ export const SideNavigationItem: Comp<SideNavigationItemProps, HTMLLIElement> =
95
95
  const shouldSplitActions = Boolean(onActionClick);
96
96
  const showChildren = hasContent && isOpen;
97
97
 
98
- const contentId = React.useMemo(uid, []);
98
+ const contentId = useId();
99
99
  const ariaProps: any = {};
100
100
  if (hasContent) {
101
101
  ariaProps['aria-expanded'] = !!showChildren;
@@ -7,7 +7,7 @@ import { Slider, SliderProps } from './Slider';
7
7
 
8
8
  const CLASSNAME = Slider.className as string;
9
9
 
10
- jest.mock('uid', () => ({ uid: () => 'uid' }));
10
+ jest.mock('@lumx/react/hooks/useId', () => ({ useId: () => ':r1:' }));
11
11
 
12
12
  const setup = (props: Partial<SliderProps> = {}) => {
13
13
  render(<Slider {...(props as any)} />);
@@ -9,8 +9,8 @@ import useEventCallback from '@lumx/react/hooks/useEventCallback';
9
9
  import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
10
10
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
11
11
 
12
- import { uid } from 'uid';
13
12
  import { clamp } from '@lumx/react/utils/clamp';
13
+ import { useId } from '@lumx/react/hooks/useId';
14
14
 
15
15
  /**
16
16
  * Defines the props of the component.
@@ -111,7 +111,8 @@ export const Slider: Comp<SliderProps, HTMLDivElement> = forwardRef((props, ref)
111
111
  value,
112
112
  ...forwardedProps
113
113
  } = props;
114
- const sliderId = useMemo(() => id || `slider-${uid()}`, [id]);
114
+ const generatedId = useId();
115
+ const sliderId = id || generatedId;
115
116
  const sliderLabelId = useMemo(() => `label-${sliderId}`, [sliderId]);
116
117
  const sliderRef = useRef<HTMLDivElement>(null);
117
118
 
@@ -24,7 +24,7 @@ const setup = (propsOverride: SetupProps = {}) => {
24
24
  return { switchWrapper, input, helper, label, props };
25
25
  };
26
26
 
27
- jest.mock('uid', () => ({ uid: () => 'uid' }));
27
+ jest.mock('@lumx/react/hooks/useId', () => ({ useId: () => ':r1:' }));
28
28
 
29
29
  describe(`<${Switch.displayName}>`, () => {
30
30
  describe('Props', () => {
@@ -1,7 +1,6 @@
1
- import React, { Children, forwardRef, InputHTMLAttributes, SyntheticEvent, useMemo } from 'react';
1
+ import React, { Children, forwardRef, InputHTMLAttributes, SyntheticEvent } from 'react';
2
2
 
3
3
  import classNames from 'classnames';
4
- import { uid } from 'uid';
5
4
 
6
5
  import isEmpty from 'lodash/isEmpty';
7
6
 
@@ -9,6 +8,7 @@ import { Alignment, InputHelper, InputLabel, Theme } from '@lumx/react';
9
8
 
10
9
  import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
11
10
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
11
+ import { useId } from '@lumx/react/hooks/useId';
12
12
 
13
13
  /**
14
14
  * Defines the props of the component.
@@ -75,7 +75,8 @@ export const Switch: Comp<SwitchProps, HTMLDivElement> = forwardRef((props, ref)
75
75
  inputProps = {},
76
76
  ...forwardedProps
77
77
  } = props;
78
- const inputId = useMemo(() => id || `switch-${uid()}`, [id]);
78
+ const generatedInputId = useId();
79
+ const inputId = id || generatedInputId;
79
80
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
80
81
  if (onChange) {
81
82
  onChange(!isChecked, value, name, event);
@@ -1,5 +1,5 @@
1
1
  import { Dispatch, createContext, useCallback, useContext, useEffect, useMemo } from 'react';
2
- import { uid } from 'uid';
2
+ import { useId } from '@lumx/react/hooks/useId';
3
3
 
4
4
  type TabType = 'tab' | 'tabPanel';
5
5
 
@@ -76,11 +76,9 @@ export const useTabProviderContext = (type: TabType, originalId?: string): undef
76
76
  const [state, dispatch] = context;
77
77
 
78
78
  // Current tab or tab panel id.
79
- const id = useMemo(
80
- () => originalId || `${type}-${uid()}`,
81
- // eslint-disable-next-line react-hooks/exhaustive-deps
82
- [],
83
- );
79
+ const generatedId = useId();
80
+ const id = originalId || generatedId;
81
+
84
82
  useEffect(
85
83
  () => {
86
84
  // On mount: register tab or tab panel id.
@@ -1,18 +1,7 @@
1
- import React, {
2
- forwardRef,
3
- ReactNode,
4
- Ref,
5
- RefObject,
6
- SyntheticEvent,
7
- useEffect,
8
- useMemo,
9
- useRef,
10
- useState,
11
- } from 'react';
1
+ import React, { forwardRef, ReactNode, Ref, RefObject, SyntheticEvent, useEffect, useRef, useState } from 'react';
12
2
 
13
3
  import classNames from 'classnames';
14
4
  import get from 'lodash/get';
15
- import { uid } from 'uid';
16
5
 
17
6
  import { mdiAlertCircle, mdiCheckCircle, mdiCloseCircle } from '@lumx/icons';
18
7
  import {
@@ -30,6 +19,7 @@ import {
30
19
  import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
31
20
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
32
21
  import { mergeRefs } from '@lumx/react/utils/mergeRefs';
22
+ import { useId } from '@lumx/react/hooks/useId';
33
23
 
34
24
  /**
35
25
  * Defines the props of the component.
@@ -296,7 +286,8 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
296
286
  afterElement,
297
287
  ...forwardedProps
298
288
  } = props;
299
- const textFieldId = useMemo(() => id || `text-field-${uid()}`, [id]);
289
+ const generatedTextFieldId = useId();
290
+ const textFieldId = id || generatedTextFieldId;
300
291
  /** Keep a clean local input ref to manage focus */
301
292
  const localInputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);
302
293
  /** Merge prop input ref and local input ref */
@@ -307,8 +298,8 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
307
298
  * we want to first use the most important one, which is the errorId. That way, screen readers will read first
308
299
  * the error and then the helper
309
300
  */
310
- const helperId = helper ? `text-field-helper-${uid()}` : undefined;
311
- const errorId = error ? `text-field-error-${uid()}` : undefined;
301
+ const helperId = helper ? `text-field-helper-${generatedTextFieldId}` : undefined;
302
+ const errorId = error ? `text-field-error-${generatedTextFieldId}` : undefined;
312
303
  const describedByIds = [errorId, helperId, forwardedProps['aria-describedby']].filter(Boolean);
313
304
  const describedById = describedByIds.length === 0 ? undefined : describedByIds.join(' ');
314
305
 
@@ -12,7 +12,7 @@ import { Tooltip, TooltipProps } from './Tooltip';
12
12
  const CLASSNAME = Tooltip.className as string;
13
13
 
14
14
  jest.mock('@lumx/react/utils/isFocusVisible');
15
- jest.mock('uid', () => ({ uid: () => 'uid' }));
15
+ jest.mock('@lumx/react/hooks/useId', () => ({ useId: () => ':r1:' }));
16
16
  // Skip delays
17
17
  jest.mock('@lumx/react/constants', () => ({
18
18
  ...jest.requireActual('@lumx/react/constants'),
@@ -1,8 +1,7 @@
1
1
  /* eslint-disable react-hooks/rules-of-hooks */
2
- import React, { forwardRef, ReactNode, useMemo, useState } from 'react';
2
+ import React, { forwardRef, ReactNode, useState } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
4
  import { usePopper } from 'react-popper';
5
- import { uid } from 'uid';
6
5
 
7
6
  import classNames from 'classnames';
8
7
 
@@ -11,8 +10,9 @@ import { Comp, GenericProps } from '@lumx/react/utils/type';
11
10
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
12
11
  import { mergeRefs } from '@lumx/react/utils/mergeRefs';
13
12
  import { Placement } from '@lumx/react/components/popover';
14
-
15
13
  import { TooltipContextProvider } from '@lumx/react/components/tooltip/context';
14
+ import { useId } from '@lumx/react/hooks/useId';
15
+
16
16
  import { useInjectTooltipRef } from './useInjectTooltipRef';
17
17
  import { useTooltipOpen } from './useTooltipOpen';
18
18
 
@@ -71,7 +71,7 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re
71
71
  return <TooltipContextProvider>{children}</TooltipContextProvider>;
72
72
  }
73
73
 
74
- const id = useMemo(() => `tooltip-${uid()}`, []);
74
+ const id = useId();
75
75
 
76
76
  const [popperElement, setPopperElement] = useState<null | HTMLElement>(null);
77
77
  const [anchorElement, setAnchorElement] = useState<null | HTMLElement>(null);
@@ -1,11 +1,11 @@
1
1
  import React, { forwardRef, MouseEventHandler } from 'react';
2
2
  import classNames from 'classnames';
3
- import uniqueId from 'lodash/uniqueId';
4
3
 
5
4
  import { AspectRatio, Icon, Size, Theme } from '@lumx/react';
6
5
  import { Comp, GenericProps, HasTheme, ValueOf } from '@lumx/react/utils/type';
7
6
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
8
7
  import { useBooleanState } from '@lumx/react/hooks/useBooleanState';
8
+ import { useId } from '@lumx/react/hooks/useId';
9
9
 
10
10
  /**
11
11
  * Uploader variants.
@@ -81,7 +81,8 @@ export const Uploader: Comp<UploaderProps> = forwardRef((props, ref) => {
81
81
  // Adjust to square aspect ratio when using circle variants.
82
82
  const adjustedAspectRatio = variant === UploaderVariant.circle ? AspectRatio.square : aspectRatio;
83
83
 
84
- const inputId = React.useMemo(() => fileInputProps?.id || uniqueId('uploader-input-'), [fileInputProps?.id]);
84
+ const generatedInputId = useId();
85
+ const inputId = fileInputProps?.id || generatedInputId;
85
86
  const [isDragHovering, unsetDragHovering, setDragHovering] = useBooleanState(false);
86
87
  const wrapper = fileInputProps
87
88
  ? { Component: 'label' as const, props: { htmlFor: inputId } as const }
@@ -9,7 +9,7 @@ import { withCombinations } from '@lumx/react/stories/decorators/withCombination
9
9
  import { getSelectArgType } from '@lumx/react/stories/controls/selectArgType';
10
10
  import { UserBlock } from './UserBlock';
11
11
 
12
- const sizes = [Size.s, Size.m, Size.l];
12
+ const sizes = [Size.xs, Size.s, Size.m, Size.l];
13
13
 
14
14
  export default {
15
15
  title: 'LumX components/user-block/UserBlock',
@@ -12,7 +12,7 @@ import { AvatarProps } from '../avatar/Avatar';
12
12
  /**
13
13
  * User block sizes.
14
14
  */
15
- export type UserBlockSize = Extract<Size, 's' | 'm' | 'l'>;
15
+ export type UserBlockSize = Extract<Size, 'xs' | 's' | 'm' | 'l'>;
16
16
 
17
17
  /**
18
18
  * Defines the props of the component.
@@ -128,7 +128,7 @@ export const UserBlock: Comp<UserBlockProps, HTMLDivElement> = forwardRef((props
128
128
  return <NameComponent {...nProps}>{name}</NameComponent>;
129
129
  }, [avatarProps, isClickable, linkAs, linkProps, name, nameProps, onClick]);
130
130
 
131
- const fieldsBlock: ReactNode = fields && componentSize !== Size.s && (
131
+ const fieldsBlock: ReactNode = fields && componentSize !== Size.s && componentSize !== Size.xs && (
132
132
  <div className={`${CLASSNAME}__fields`}>
133
133
  {fields.map((field: string, idx: number) => (
134
134
  <span key={idx} className={`${CLASSNAME}__field`}>
@@ -0,0 +1,23 @@
1
+ import { useId } from '@lumx/react/hooks/useId';
2
+ import { render } from '@testing-library/react';
3
+ import React from 'react';
4
+
5
+ function setup() {
6
+ const Component = (): any => useId();
7
+ const result = render(<Component />);
8
+ const rerender = () => result.rerender(<Component />);
9
+ return { ...result, rerender };
10
+ }
11
+
12
+ describe(useId, () => {
13
+ it('should render a unique id stable after re-renders', () => {
14
+ const result = setup();
15
+ // Id generated
16
+ const initialId = result.container.textContent;
17
+ expect(initialId).toMatch(/:lumx\d+:/);
18
+
19
+ // Id is stable after re-render
20
+ result.rerender();
21
+ expect(result.container.textContent).toEqual(initialId);
22
+ });
23
+ });
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+
3
+ let i = 0;
4
+
5
+ /**
6
+ * Generate a unique id (for use in a11y or other id based DOM linking).
7
+ *
8
+ * (Tries to emulate React 18 useId hook, to remove once we upgrade React)
9
+ */
10
+ export function useId() {
11
+ return React.useMemo(() => {
12
+ i += 1;
13
+ return `:lumx${i}:`;
14
+ }, []);
15
+ }
@@ -11,9 +11,7 @@ import { RectSize } from '@lumx/react/utils/type';
11
11
  * @param elementRef Element to observe
12
12
  * @return the size and a manual update callback
13
13
  */
14
- export function useElementSizeDependentOfWindowSize(
15
- elementRef: React.RefObject<HTMLElement>,
16
- ): [RectSize | null, () => void] {
14
+ export function useSizeOnWindowResize(elementRef: React.RefObject<HTMLElement>): [RectSize | null, () => void] {
17
15
  const [size, setSize] = React.useState<null | RectSize>(null);
18
16
  const updateSize = React.useMemo(
19
17
  () =>