@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/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.2",
10
- "@lumx/icons": "^3.18.2-alpha.2",
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.2"
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
- } = props;
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: 300 }, GridColumn),
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: Object.values(AspectRatio) },
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: 200 }, GridColumn),
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('Events', () => {
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 onClick = jest.fn();
89
- const { uploader } = setup({ onClick });
105
+ const { uploader } = setup({ ...props, onClick });
90
106
 
91
107
  await userEvent.click(uploader);
92
- expect(onClick).toHaveBeenCalled();
108
+ assertClick();
93
109
  });
94
110
 
95
111
  it('should trigger `onClick` when pressing Enter or Escape', async () => {
96
- const onClick = jest.fn();
97
- const { uploader } = setup({ onClick });
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
- expect(onClick).toHaveBeenCalled();
125
+ assertClick();
105
126
 
106
127
  onClick.mockClear();
107
128
 
108
129
  // Activate with Space
109
130
  await userEvent.keyboard('[Space]');
110
- expect(onClick).toHaveBeenCalled();
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
- } = props;
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' as const, props: { htmlFor: inputId } as const }
102
- : { Component: 'button' as const, props: { type: 'button' } as const };
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
  };