@lumx/react 3.0.5-alpha.1 → 3.0.5

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
@@ -7,8 +7,8 @@
7
7
  },
8
8
  "dependencies": {
9
9
  "@juggle/resize-observer": "^3.2.0",
10
- "@lumx/core": "^3.0.5-alpha.1",
11
- "@lumx/icons": "^3.0.5-alpha.1",
10
+ "@lumx/core": "^3.0.5",
11
+ "@lumx/icons": "^3.0.5",
12
12
  "@popperjs/core": "^2.5.4",
13
13
  "body-scroll-lock": "^3.1.5",
14
14
  "classnames": "^2.2.6",
@@ -114,6 +114,6 @@
114
114
  "build:storybook": "cd storybook && ./build"
115
115
  },
116
116
  "sideEffects": false,
117
- "version": "3.0.5-alpha.1",
118
- "gitHead": "e05f8419842b4c8ebdb9b21026e17d18ec6ebbbb"
117
+ "version": "3.0.5",
118
+ "gitHead": "83eccabdc29dbf61fd519c88729002a5bb8d4790"
119
119
  }
@@ -1,15 +1,11 @@
1
- import React, { ReactElement } from 'react';
1
+ import React from 'react';
2
2
 
3
- import { mount, shallow } from 'enzyme';
4
- import 'jest-enzyme';
3
+ import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
4
+ import { queryByRole, render } from '@testing-library/react';
5
+ import { getByClassName, queryByClassName } from '@lumx/react/testing/utils/queries';
6
+ import userEvent from '@testing-library/user-event';
7
+ import { ExpansionPanel, ExpansionPanelProps } from '.';
5
8
 
6
- import { commonTestsSuite, Wrapper } from '@lumx/react/testing/utils';
7
- import { getBasicClass } from '@lumx/react/utils/className';
8
-
9
- import { Theme } from '@lumx/react';
10
- import { ExpansionPanel, ExpansionPanelProps } from './ExpansionPanel';
11
-
12
- const DEFAULT_PROPS = ExpansionPanel.defaultProps as any;
13
9
  const CLASSNAME = ExpansionPanel.className as string;
14
10
 
15
11
  type SetupProps = Partial<ExpansionPanelProps>;
@@ -17,63 +13,68 @@ type SetupProps = Partial<ExpansionPanelProps>;
17
13
  /**
18
14
  * Mounts the component and returns common DOM elements / data needed in multiple tests further down.
19
15
  */
20
- const setup = ({ ...propsOverride }: SetupProps = {}, shallowRendering = true) => {
16
+ const setup = (propsOverride: SetupProps = {}) => {
21
17
  const props: ExpansionPanelProps = {
22
18
  toggleButtonProps: { label: 'Toggle' },
23
19
  ...propsOverride,
24
20
  };
25
- const renderer: (el: ReactElement) => Wrapper = shallowRendering ? shallow : mount;
26
- const wrapper: Wrapper = renderer(<ExpansionPanel {...props} />);
21
+ const { container } = render(<ExpansionPanel {...props} />);
27
22
 
28
23
  return {
29
- content: wrapper.find(`.${CLASSNAME}__wrapper`),
30
- header: wrapper.find(`.${CLASSNAME}__header`),
31
- label: wrapper.find(`.${CLASSNAME}__label`),
24
+ container,
25
+ element: getByClassName(container, CLASSNAME),
26
+ query: {
27
+ toggleButton: () => queryByRole(container, 'button', { name: /Toggle/i }),
28
+ header: () => queryByClassName(container, `${CLASSNAME}__header`),
29
+ content: () => queryByClassName(container, `${CLASSNAME}__wrapper`),
30
+ },
32
31
  props,
33
- root: wrapper.find('section'),
34
- wrapper,
35
32
  };
36
33
  };
37
34
 
38
35
  describe(`<${ExpansionPanel.displayName}>`, () => {
39
- // 1. Test render via snapshot (default states of component).
40
- describe('Snapshots and structure', () => {
41
- // Here is an example of a basic rendering check, with snapshot.
36
+ describe('Render', () => {
37
+ it('should render default', () => {
38
+ const { element, query } = setup();
39
+ expect(element).toBeInTheDocument();
40
+ expect(element).toHaveClass(CLASSNAME);
41
+ expect(element).toHaveClass(`${CLASSNAME}--is-close`);
42
+ expect(element).toHaveClass(`${CLASSNAME}--theme-light`);
43
+
44
+ // Header is visible
45
+ expect(query.header()).toBeInTheDocument();
42
46
 
43
- it('should render correctly', () => {
44
- const { root, wrapper } = setup();
45
- expect(wrapper).toMatchSnapshot();
47
+ // Content is not visible
48
+ expect(query.content()).not.toBeInTheDocument();
46
49
 
47
- expect(root).toExist();
48
- expect(root).toHaveClassName(CLASSNAME);
50
+ expect(query.toggleButton()).toHaveAttribute('aria-expanded', 'false');
49
51
  });
50
- });
51
52
 
52
- // 2. Test defaultProps value and important props custom values.
53
- describe('Props', () => {
54
- // Here are some examples of basic props check.
53
+ it('should render open', () => {
54
+ const { query } = setup({ isOpen: true });
55
+
56
+ // Content is visible
57
+ expect(query.content()).toBeInTheDocument();
55
58
 
56
- it('should use default props', () => {
57
- const { root } = setup();
59
+ expect(query.toggleButton()).toHaveAttribute('aria-expanded', 'true');
60
+ });
61
+
62
+ it('should show label', () => {
63
+ const labelText = 'Label text';
64
+ const { query } = setup({ label: labelText });
58
65
 
59
- for (const prop of Object.keys(DEFAULT_PROPS)) {
60
- expect(root).toHaveClassName(
61
- getBasicClass({ prefix: CLASSNAME, type: prop, value: DEFAULT_PROPS[prop] }),
62
- );
63
- }
66
+ expect(query.header()).toHaveTextContent(labelText);
64
67
  });
65
68
 
66
- it('should ignore incorrect theme', () => {
67
- const { root } = setup({ theme: 'not_a_valid_theme' as Theme });
69
+ it('should show header instead of label', () => {
70
+ const labelText = 'Label text';
71
+ const headerText = 'Header text';
72
+ const { query } = setup({ label: labelText, children: <header>{headerText}</header> });
68
73
 
69
- // Correct classes are applied
70
- root.hasClass('lumx-color-background-dark-L6');
71
- root.hasClass('lumx-expansion-panel--theme-light');
72
- root.hasClass('lumx-color-font-dark-N');
74
+ expect(query.header()).toHaveTextContent(headerText);
73
75
  });
74
76
  });
75
77
 
76
- // 3. Test events.
77
78
  describe('Events', () => {
78
79
  const onOpen: jest.Mock = jest.fn();
79
80
  const onClose: jest.Mock = jest.fn();
@@ -83,63 +84,33 @@ describe(`<${ExpansionPanel.displayName}>`, () => {
83
84
  beforeEach(onClose.mockClear);
84
85
  beforeEach(onToggleOpen.mockClear);
85
86
 
86
- it('should trigger `onOpen`', () => {
87
- const { header } = setup({ onOpen }, false);
88
- header.simulate('click');
89
- expect(onOpen).toHaveBeenCalled();
90
- });
87
+ it('should open on click', async () => {
88
+ const { query } = setup({ isOpen: false, onOpen, onClose, onToggleOpen });
91
89
 
92
- it('should trigger `onClose`', () => {
93
- const { header } = setup({ isOpen: true, onClose }, false);
94
- header.simulate('click');
95
- expect(onClose).toHaveBeenCalled();
96
- });
90
+ // Content is not visible
91
+ expect(query.content()).not.toBeInTheDocument();
97
92
 
98
- it('should trigger `onToggleOpen`', () => {
99
- const { header } = setup({ onToggleOpen }, false);
100
- header.simulate('click');
101
- header.simulate('click');
102
- expect(onToggleOpen).toHaveBeenCalledTimes(2);
103
- });
104
- });
105
-
106
- // 4. Test conditions (i.e. things that display or not in the UI based on props).
107
- describe('Conditions', () => {
108
- // Here is an example of children types check.
109
-
110
- it('should hide content when `isOpen` == false', () => {
111
- const { content } = setup({ isOpen: false });
112
-
113
- expect(content.exists()).toBe(false);
93
+ // Click on toggle button
94
+ await userEvent.click(query.toggleButton() as any);
95
+ expect(onOpen).toHaveBeenCalled();
96
+ expect(onClose).not.toHaveBeenCalled();
97
+ expect(onToggleOpen).toHaveBeenCalledWith(true);
114
98
  });
115
99
 
116
- it('should show content when `isOpen` == true', () => {
117
- const { content } = setup({ isOpen: true });
100
+ it('should close on click', async () => {
101
+ const { query } = setup({ isOpen: true, onOpen, onClose, onToggleOpen });
118
102
 
119
- expect(content.exists()).toBe(true);
120
- });
103
+ // Content is visible
104
+ expect(query.content()).toBeInTheDocument();
121
105
 
122
- it('should show label', () => {
123
- const labelText = 'Label text';
124
- const { header } = setup({ label: labelText });
125
-
126
- expect(header.text()).toContain(labelText);
127
- });
128
-
129
- it('should show header instead of label', () => {
130
- const labelText = 'Label text';
131
- const headerText = 'Header text';
132
- const { header } = setup({ label: labelText, children: <header>{headerText}</header> });
133
-
134
- expect(header.text()).toContain(headerText);
106
+ // Click on header
107
+ await userEvent.click(query.header() as any);
108
+ expect(onOpen).not.toHaveBeenCalled();
109
+ expect(onClose).toHaveBeenCalled();
110
+ expect(onToggleOpen).toHaveBeenCalledWith(false);
135
111
  });
136
112
  });
137
113
 
138
- // 5. Test state.
139
- describe('State', () => {
140
- // Nothing to do here.
141
- });
142
-
143
114
  // Common tests suite.
144
- commonTestsSuite(setup, { className: 'root', prop: 'root' }, { className: CLASSNAME });
115
+ commonTestsSuiteRTL(setup, { baseClassName: CLASSNAME, forwardClassName: 'element', forwardAttributes: 'element' });
145
116
  });
@@ -148,6 +148,7 @@ export const ExpansionPanel: Comp<ExpansionPanelProps, HTMLDivElement> = forward
148
148
  color={color}
149
149
  emphasis={Emphasis.low}
150
150
  icon={isOpen ? mdiChevronUp : mdiChevronDown}
151
+ aria-expanded={isOpen || 'false'}
151
152
  />
152
153
  </div>
153
154
  </header>
@@ -1,4 +1,4 @@
1
- import React, { useMemo, forwardRef, ReactNode, SyntheticEvent } from 'react';
1
+ import React, { useMemo, forwardRef, ReactNode, SyntheticEvent, InputHTMLAttributes } from 'react';
2
2
 
3
3
  import classNames from 'classnames';
4
4
  import { uid } from 'uid';
@@ -30,6 +30,8 @@ export interface RadioButtonProps extends GenericProps, HasTheme {
30
30
  value?: string;
31
31
  /** On change callback. */
32
32
  onChange?(value?: string, name?: string, event?: SyntheticEvent): void;
33
+ /** optional props for input */
34
+ inputProps?: InputHTMLAttributes<HTMLInputElement>;
33
35
  }
34
36
 
35
37
  /**
@@ -71,9 +73,10 @@ export const RadioButton: Comp<RadioButtonProps, HTMLDivElement> = forwardRef((p
71
73
  onChange,
72
74
  theme,
73
75
  value,
76
+ inputProps,
74
77
  ...forwardedProps
75
78
  } = props;
76
- const radioButtonId = useMemo(() => id || `${CLASSNAME.toLowerCase()}-${uid()}`, [id]);
79
+ const inputId = useMemo(() => id || `${CLASSNAME.toLowerCase()}-${uid()}`, [id]);
77
80
 
78
81
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
79
82
  if (onChange) {
@@ -101,13 +104,15 @@ export const RadioButton: Comp<RadioButtonProps, HTMLDivElement> = forwardRef((p
101
104
  ref={inputRef}
102
105
  className={`${CLASSNAME}__input-native`}
103
106
  disabled={isDisabled}
104
- id={radioButtonId}
107
+ id={inputId}
105
108
  tabIndex={isDisabled ? -1 : 0}
106
109
  type="radio"
107
110
  name={name}
108
111
  value={value}
109
112
  checked={isChecked}
110
113
  onChange={handleChange}
114
+ aria-describedby={helper ? `${inputId}-helper` : undefined}
115
+ {...inputProps}
111
116
  />
112
117
 
113
118
  <div className={`${CLASSNAME}__input-placeholder`}>
@@ -118,12 +123,12 @@ export const RadioButton: Comp<RadioButtonProps, HTMLDivElement> = forwardRef((p
118
123
 
119
124
  <div className={`${CLASSNAME}__content`}>
120
125
  {label && (
121
- <InputLabel htmlFor={radioButtonId} theme={theme} className={`${CLASSNAME}__label`}>
126
+ <InputLabel htmlFor={inputId} theme={theme} className={`${CLASSNAME}__label`}>
122
127
  {label}
123
128
  </InputLabel>
124
129
  )}
125
130
  {helper && (
126
- <InputHelper theme={theme} className={`${CLASSNAME}__helper`}>
131
+ <InputHelper id={`${inputId}-helper`} theme={theme} className={`${CLASSNAME}__helper`}>
127
132
  {helper}
128
133
  </InputHelper>
129
134
  )}
@@ -72,6 +72,7 @@ exports[`<RadioButton> Snapshots and structure should render label & helper 1`]
72
72
  className="lumx-radio-button__input-wrapper"
73
73
  >
74
74
  <input
75
+ aria-describedby="fixedId-helper"
75
76
  className="lumx-radio-button__input-native"
76
77
  id="fixedId"
77
78
  onChange={[Function]}
@@ -101,6 +102,7 @@ exports[`<RadioButton> Snapshots and structure should render label & helper 1`]
101
102
  </InputLabel>
102
103
  <InputHelper
103
104
  className="lumx-radio-button__helper"
105
+ id="fixedId-helper"
104
106
  kind="info"
105
107
  theme="light"
106
108
  >
@@ -75,7 +75,7 @@ export const Switch: Comp<SwitchProps, HTMLDivElement> = forwardRef((props, ref)
75
75
  inputProps = {},
76
76
  ...forwardedProps
77
77
  } = props;
78
- const switchId = useMemo(() => id || `switch-${uid()}`, [id]);
78
+ const inputId = useMemo(() => id || `switch-${uid()}`, [id]);
79
79
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
80
80
  if (onChange) {
81
81
  onChange(!isChecked, value, name, event);
@@ -103,7 +103,7 @@ export const Switch: Comp<SwitchProps, HTMLDivElement> = forwardRef((props, ref)
103
103
  <input
104
104
  type="checkbox"
105
105
  role="switch"
106
- id={switchId}
106
+ id={inputId}
107
107
  className={`${CLASSNAME}__input-native`}
108
108
  name={name}
109
109
  value={value}
@@ -111,6 +111,7 @@ export const Switch: Comp<SwitchProps, HTMLDivElement> = forwardRef((props, ref)
111
111
  checked={isChecked}
112
112
  aria-checked={Boolean(isChecked)}
113
113
  onChange={handleChange}
114
+ aria-describedby={helper ? `${inputId}-helper` : undefined}
114
115
  {...inputProps}
115
116
  />
116
117
 
@@ -122,11 +123,11 @@ export const Switch: Comp<SwitchProps, HTMLDivElement> = forwardRef((props, ref)
122
123
 
123
124
  {Children.count(children) > 0 && (
124
125
  <div className={`${CLASSNAME}__content`}>
125
- <InputLabel htmlFor={switchId} theme={theme} className={`${CLASSNAME}__label`}>
126
+ <InputLabel htmlFor={inputId} theme={theme} className={`${CLASSNAME}__label`}>
126
127
  {children}
127
128
  </InputLabel>
128
129
  {!isEmpty(helper) && (
129
- <InputHelper theme={theme} className={`${CLASSNAME}__helper`}>
130
+ <InputHelper id={`${inputId}-helper`} theme={theme} className={`${CLASSNAME}__helper`}>
130
131
  {helper}
131
132
  </InputHelper>
132
133
  )}
@@ -9,6 +9,7 @@ exports[`<Switch> Conditions should not display the \`helper\` if no \`label\` i
9
9
  >
10
10
  <input
11
11
  aria-checked={false}
12
+ aria-describedby="switch-uid-helper"
12
13
  className="lumx-switch__input-native"
13
14
  id="switch-uid"
14
15
  onChange={[Function]}
@@ -68,6 +69,7 @@ exports[`<Switch> Snapshots and structure should render correctly with a \`label
68
69
  >
69
70
  <input
70
71
  aria-checked={false}
72
+ aria-describedby="switch-uid-helper"
71
73
  className="lumx-switch__input-native"
72
74
  id="switch-uid"
73
75
  onChange={[Function]}
@@ -97,6 +99,7 @@ exports[`<Switch> Snapshots and structure should render correctly with a \`label
97
99
  </InputLabel>
98
100
  <InputHelper
99
101
  className="lumx-switch__helper"
102
+ id="switch-uid-helper"
100
103
  kind="info"
101
104
  theme="light"
102
105
  >
@@ -1,4 +1,5 @@
1
1
  import { RefObject, useEffect, useRef, useState } from 'react';
2
+ import { userHasReducedMotion } from '@lumx/react/utils/userHasReducedMotion';
2
3
 
3
4
  /**
4
5
  * Returns true if the component is visible tracking the opacity transition.
@@ -16,29 +17,22 @@ export const useTransitionVisibility = (
16
17
  const [isVisible, setVisible] = useState(isComponentVisible);
17
18
  const previousVisibility = useRef(isVisible);
18
19
 
20
+ // On component visibility change.
19
21
  useEffect(() => {
20
22
  if (isComponentVisible) {
21
23
  setVisible(true);
22
- } else if (!window.TransitionEvent) {
23
- // Transition event is not available so visibility is set to false directly.
24
- setVisible(false);
25
- }
26
- }, [isComponentVisible]);
27
-
28
- useEffect(() => {
29
- if (onVisibilityChange && previousVisibility.current !== isVisible) {
30
- onVisibilityChange(isVisible);
31
- previousVisibility.current = isVisible;
24
+ return undefined;
32
25
  }
33
- }, [isVisible, onVisibilityChange]);
34
-
35
- useEffect(() => {
36
26
  const { current: element } = ref;
37
- if (!element) {
27
+
28
+ // Transition event is not supported or the user prefers reduced motion.
29
+ // => Skip `transitionend` event listening and set visibility to false directly.
30
+ if (!element || !window.TransitionEvent || userHasReducedMotion()) {
31
+ setVisible(false);
38
32
  return undefined;
39
33
  }
40
34
 
41
- // Listen opacity transition
35
+ // Update visibility on opacity transition end.
42
36
  const onTransitionEnd = (e: TransitionEvent) => {
43
37
  if (e.target !== ref.current || e.propertyName !== 'opacity') return;
44
38
  setVisible((wasVisible) => !wasVisible);
@@ -47,7 +41,14 @@ export const useTransitionVisibility = (
47
41
  return () => {
48
42
  element.removeEventListener('transitionend', onTransitionEnd);
49
43
  };
50
- });
44
+ }, [isComponentVisible, ref]);
45
+
46
+ useEffect(() => {
47
+ if (onVisibilityChange && previousVisibility.current !== isVisible) {
48
+ onVisibilityChange(isVisible);
49
+ previousVisibility.current = isVisible;
50
+ }
51
+ }, [isVisible, onVisibilityChange]);
51
52
 
52
53
  return isVisible || isComponentVisible;
53
54
  };
@@ -0,0 +1,7 @@
1
+ export const userHasReducedMotion = () => {
2
+ try {
3
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
4
+ } catch (e) {
5
+ return false;
6
+ }
7
+ };
@@ -1,32 +0,0 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
- exports[`<ExpansionPanel> Snapshots and structure should render correctly 1`] = `
4
- <section
5
- className="lumx-expansion-panel lumx-expansion-panel--is-close lumx-expansion-panel--theme-light"
6
- >
7
- <header
8
- className="lumx-expansion-panel__header"
9
- onClick={[Function]}
10
- >
11
- <div
12
- className="lumx-expansion-panel__header-content"
13
- >
14
- <span
15
- className="lumx-expansion-panel__label"
16
- />
17
- </div>
18
- <div
19
- className="lumx-expansion-panel__header-toggle"
20
- >
21
- <IconButton
22
- color="dark"
23
- emphasis="low"
24
- icon="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z"
25
- label="Toggle"
26
- size="m"
27
- theme="light"
28
- />
29
- </div>
30
- </header>
31
- </section>
32
- `;