@onewelcome/react-lib-components 0.1.0-alpha → 0.1.1-alpha

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 (100) hide show
  1. package/dist/Button/IconButton.d.ts +1 -1
  2. package/dist/Form/Fieldset/Fieldset.d.ts +6 -4
  3. package/dist/Form/FormControl/FormControl.d.ts +2 -1
  4. package/dist/Form/Wrapper/SelectWrapper/SelectWrapper.d.ts +2 -1
  5. package/dist/{BaseModal → Notifications/BaseModal}/BaseModal.d.ts +4 -2
  6. package/dist/{BaseModal → Notifications/BaseModal}/BaseModalActions/BaseModalActions.d.ts +0 -0
  7. package/dist/{BaseModal → Notifications/BaseModal}/BaseModalContent/BaseModalContent.d.ts +0 -0
  8. package/dist/{BaseModal → Notifications/BaseModal}/BaseModalContext.d.ts +0 -0
  9. package/dist/{BaseModal → Notifications/BaseModal}/BaseModalHeader/BaseModalHeader.d.ts +1 -1
  10. package/dist/{Dialog → Notifications/Dialog}/Dialog.d.ts +4 -3
  11. package/dist/{Dialog → Notifications/Dialog}/DialogActions/DialogActions.d.ts +0 -0
  12. package/dist/{Dialog → Notifications/Dialog}/DialogTitle/DialogTitle.d.ts +0 -0
  13. package/dist/Notifications/DiscardChangesModal/DiscardChangesDialog/DiscardChangesDialog.d.ts +12 -0
  14. package/dist/Notifications/DiscardChangesModal/DiscardChangesModal.d.ts +11 -0
  15. package/dist/{Modal → Notifications/Modal}/Modal.d.ts +0 -0
  16. package/dist/{Modal → Notifications/Modal}/ModalActions/ModalActions.d.ts +0 -0
  17. package/dist/{Modal → Notifications/Modal}/ModalContent/ModalContent.d.ts +0 -0
  18. package/dist/{Modal → Notifications/Modal}/ModalHeader/ModalHeader.d.ts +0 -0
  19. package/dist/{Snackbar → Notifications/Snackbar}/SnackbarContainer/SnackbarContainer.d.ts +0 -0
  20. package/dist/{Snackbar → Notifications/Snackbar}/SnackbarItem/SnackbarItem.d.ts +0 -0
  21. package/dist/{Snackbar → Notifications/Snackbar}/SnackbarProvider/SnackbarProvider.d.ts +1 -1
  22. package/dist/{Snackbar → Notifications/Snackbar}/SnackbarProvider/SnackbarStateProvider.d.ts +0 -0
  23. package/dist/{Snackbar → Notifications/Snackbar}/interfaces.d.ts +0 -0
  24. package/dist/{Snackbar → Notifications/Snackbar}/useSnackbar.d.ts +0 -0
  25. package/dist/Tiles/Tile.d.ts +4 -5
  26. package/dist/index.d.ts +8 -7
  27. package/dist/react-lib-components.cjs.development.js +226 -125
  28. package/dist/react-lib-components.cjs.development.js.map +1 -1
  29. package/dist/react-lib-components.cjs.production.min.js +1 -1
  30. package/dist/react-lib-components.cjs.production.min.js.map +1 -1
  31. package/dist/react-lib-components.esm.js +226 -126
  32. package/dist/react-lib-components.esm.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/Button/IconButton.tsx +8 -4
  35. package/src/Form/Checkbox/Checkbox.module.scss +4 -0
  36. package/src/Form/Checkbox/Checkbox.tsx +11 -6
  37. package/src/Form/Fieldset/Fieldset.module.scss +11 -1
  38. package/src/Form/Fieldset/Fieldset.test.tsx +2 -2
  39. package/src/Form/Fieldset/Fieldset.tsx +22 -10
  40. package/src/Form/FormControl/FormControl.tsx +3 -0
  41. package/src/Form/Radio/Radio.module.scss +4 -0
  42. package/src/Form/Radio/Radio.tsx +12 -2
  43. package/src/Form/Wrapper/CheckboxWrapper/CheckboxWrapper.test.tsx +1 -1
  44. package/src/Form/Wrapper/CheckboxWrapper/CheckboxWrapper.tsx +1 -1
  45. package/src/Form/Wrapper/RadioWrapper/RadioWrapper.test.tsx +1 -1
  46. package/src/Form/Wrapper/RadioWrapper/RadioWrapper.tsx +1 -1
  47. package/src/Form/Wrapper/SelectWrapper/SelectWrapper.tsx +2 -1
  48. package/src/{BaseModal → Notifications/BaseModal}/BaseModal.module.scss +0 -0
  49. package/src/{BaseModal → Notifications/BaseModal}/BaseModal.test.tsx +8 -15
  50. package/src/{BaseModal → Notifications/BaseModal}/BaseModal.tsx +11 -24
  51. package/src/{BaseModal → Notifications/BaseModal}/BaseModalActions/BaseModalActions.module.scss +0 -0
  52. package/src/{BaseModal → Notifications/BaseModal}/BaseModalActions/BaseModalActions.test.tsx +0 -0
  53. package/src/{BaseModal → Notifications/BaseModal}/BaseModalActions/BaseModalActions.tsx +0 -0
  54. package/src/{BaseModal → Notifications/BaseModal}/BaseModalContent/BaseModalContent.module.scss +0 -0
  55. package/src/{BaseModal → Notifications/BaseModal}/BaseModalContent/BaseModalContent.test.tsx +0 -0
  56. package/src/{BaseModal → Notifications/BaseModal}/BaseModalContent/BaseModalContent.tsx +0 -0
  57. package/src/{BaseModal → Notifications/BaseModal}/BaseModalContext.ts +0 -0
  58. package/src/{BaseModal → Notifications/BaseModal}/BaseModalHeader/BaseModalHeader.module.scss +0 -0
  59. package/src/{BaseModal → Notifications/BaseModal}/BaseModalHeader/BaseModalHeader.test.tsx +0 -0
  60. package/src/{BaseModal → Notifications/BaseModal}/BaseModalHeader/BaseModalHeader.tsx +6 -6
  61. package/src/{Dialog → Notifications/Dialog}/Dialog.module.scss +0 -0
  62. package/src/{Dialog → Notifications/Dialog}/Dialog.test.tsx +13 -16
  63. package/src/{Dialog → Notifications/Dialog}/Dialog.tsx +17 -6
  64. package/src/{Dialog → Notifications/Dialog}/DialogActions/DialogActions.module.scss +0 -0
  65. package/src/{Dialog → Notifications/Dialog}/DialogActions/DialogActions.test.tsx +0 -0
  66. package/src/{Dialog → Notifications/Dialog}/DialogActions/DialogActions.tsx +0 -0
  67. package/src/{Dialog → Notifications/Dialog}/DialogTitle/DialogTitle.module.scss +0 -0
  68. package/src/{Dialog → Notifications/Dialog}/DialogTitle/DialogTitle.test.tsx +0 -0
  69. package/src/{Dialog → Notifications/Dialog}/DialogTitle/DialogTitle.tsx +3 -3
  70. package/src/Notifications/DiscardChangesModal/DiscardChangesDialog/DiscardChangesDialog.test.tsx +55 -0
  71. package/src/Notifications/DiscardChangesModal/DiscardChangesDialog/DiscardChangesDialog.tsx +48 -0
  72. package/src/Notifications/DiscardChangesModal/DiscardChangesModal.test.tsx +111 -0
  73. package/src/Notifications/DiscardChangesModal/DiscardChangesModal.tsx +56 -0
  74. package/src/{Modal → Notifications/Modal}/Modal.test.tsx +0 -0
  75. package/src/{Modal → Notifications/Modal}/Modal.tsx +0 -0
  76. package/src/{Modal → Notifications/Modal}/ModalActions/ModalActions.tsx +0 -0
  77. package/src/{Modal → Notifications/Modal}/ModalContent/ModalContent.tsx +0 -0
  78. package/src/{Modal → Notifications/Modal}/ModalHeader/ModalHeader.tsx +0 -0
  79. package/src/{Snackbar → Notifications/Snackbar}/SnackbarContainer/SnackbarContainer.module.scss +0 -0
  80. package/src/{Snackbar → Notifications/Snackbar}/SnackbarContainer/SnackbarContainer.test.tsx +0 -0
  81. package/src/{Snackbar → Notifications/Snackbar}/SnackbarContainer/SnackbarContainer.tsx +0 -0
  82. package/src/{Snackbar → Notifications/Snackbar}/SnackbarItem/SnackbarItem.module.scss +1 -1
  83. package/src/{Snackbar → Notifications/Snackbar}/SnackbarItem/SnackbarItem.test.tsx +0 -0
  84. package/src/{Snackbar → Notifications/Snackbar}/SnackbarItem/SnackbarItem.tsx +5 -6
  85. package/src/{Snackbar → Notifications/Snackbar}/SnackbarProvider/SnackbarProvider.test.tsx +0 -0
  86. package/src/{Snackbar → Notifications/Snackbar}/SnackbarProvider/SnackbarProvider.tsx +2 -2
  87. package/src/{Snackbar → Notifications/Snackbar}/SnackbarProvider/SnackbarStateProvider.tsx +0 -0
  88. package/src/{Snackbar → Notifications/Snackbar}/interfaces.ts +0 -0
  89. package/src/{Snackbar → Notifications/Snackbar}/useSnackbar.ts +0 -0
  90. package/src/Popover/Popover.test.tsx +3 -3
  91. package/src/Tiles/Tile.module.scss +1 -1
  92. package/src/Tiles/Tile.test.tsx +21 -11
  93. package/src/Tiles/Tile.tsx +52 -15
  94. package/src/Tiles/Tiles.test.tsx +11 -9
  95. package/src/Tiles/Tiles.tsx +3 -3
  96. package/src/Wizard/BaseWizardSteps/BaseWizardSteps.tsx +3 -3
  97. package/src/Wizard/Wizard.tsx +2 -2
  98. package/src/Wizard/WizardActions/WizardActions.tsx +3 -3
  99. package/src/hooks/usePosition.test.tsx +3 -3
  100. package/src/index.ts +8 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onewelcome/react-lib-components",
3
- "version": "0.1.0-alpha",
3
+ "version": "0.1.1-alpha",
4
4
  "license": "Apache-2.0",
5
5
  "author": "OneWelcome B.V.",
6
6
  "main": "dist/index.js",
@@ -1,4 +1,4 @@
1
- import React, { RefObject } from 'react';
1
+ import React, { Fragment, RefObject } from 'react';
2
2
  import { BaseButton, Props as BaseButtonProps } from './BaseButton';
3
3
  import classes from './IconButton.module.scss';
4
4
  import readyclasses from '../readyclasses.module.scss';
@@ -7,22 +7,26 @@ export interface Props extends Omit<BaseButtonProps, 'ref'> {
7
7
  children?: React.ReactNode;
8
8
  iconSize?: 's' | 'm' | 'l';
9
9
  className?: string;
10
- title: string;
10
+ title?: string;
11
11
  ref?: RefObject<HTMLButtonElement>;
12
12
  }
13
13
 
14
14
  export const IconButton = React.forwardRef<HTMLButtonElement, Props>(
15
15
  ({ children, color = 'primary', iconSize = 'm', title, ...rest }, ref) => {
16
+ if (!title) {
17
+ console.error("Please make sure to specify a 'title' prop to your IconButton component! ");
18
+ }
19
+
16
20
  return (
17
21
  <BaseButton
18
22
  {...rest}
19
23
  ref={ref}
20
24
  className={`${classes['icon-button']} ${classes[color]} ${classes['button-' + iconSize]}`}
21
25
  >
22
- <>
26
+ <Fragment>
23
27
  {children}
24
28
  <span className={readyclasses['sr-only']}>{title}</span>
25
- </>
29
+ </Fragment>
26
30
  </BaseButton>
27
31
  );
28
32
  }
@@ -63,3 +63,7 @@
63
63
  font-size: 1.25rem;
64
64
  pointer-events: none;
65
65
  }
66
+
67
+ .disabled {
68
+ color: var(--disabled);
69
+ }
@@ -77,16 +77,17 @@ export const Checkbox = ({
77
77
 
78
78
  const renderNestedCheckboxes = () => (
79
79
  <ul className={classes['checkbox-list']}>
80
- {React.Children.map(children as ReactElement[], (child) => {
80
+ {React.Children.map(children as ReactNode[], (child) => {
81
81
  return (
82
82
  <li>
83
83
  <Checkbox
84
- {...child.props}
84
+ {...(child as ReactElement).props}
85
85
  parentHelperId={parentHelperId}
86
86
  parentErrorId={parentErrorId}
87
87
  error={error}
88
+ disabled={disabled ? disabled : (child as CheckboxProps).disabled}
88
89
  >
89
- {child.props.children}
90
+ {(child as ReactElement).props.children}
90
91
  </Checkbox>
91
92
  </li>
92
93
  );
@@ -103,6 +104,8 @@ export const Checkbox = ({
103
104
 
104
105
  const renderToggle = () => React.Children.toArray(children).filter(isToggle);
105
106
 
107
+ const iconClasses = [classes['input'], disabled ? classes['disabled'] : ''];
108
+
106
109
  /** Default return value is the default checkbox */
107
110
  return (
108
111
  <FormSelectorWrapper
@@ -136,9 +139,11 @@ export const Checkbox = ({
136
139
  />
137
140
  {renderToggle()}
138
141
 
139
- {indeterminate && <Icon className={classes.input} icon={Icons.MinusSquare} />}
140
- {checked && !indeterminate && <Icon className={classes.input} icon={Icons.CheckmarkSquare} />}
141
- {!checked && !indeterminate && <Icon className={classes.input} icon={Icons.Square} />}
142
+ {indeterminate && <Icon className={iconClasses.join(' ')} icon={Icons.MinusSquare} />}
143
+ {checked && !indeterminate && (
144
+ <Icon className={iconClasses.join(' ')} icon={Icons.CheckmarkSquare} />
145
+ )}
146
+ {!checked && !indeterminate && <Icon className={iconClasses.join(' ')} icon={Icons.Square} />}
142
147
  <label htmlFor={`${identifier}-checkbox`}>{determineLabel()}</label>
143
148
  </FormSelectorWrapper>
144
149
  );
@@ -13,7 +13,17 @@
13
13
  margin-top: 1rem;
14
14
  }
15
15
 
16
- .title {
16
+ .legend {
17
17
  display: block;
18
+
19
+ &.required {
20
+ &:after {
21
+ content: ' *';
22
+ }
23
+ }
24
+
25
+ &.error {
26
+ color: var(--error);
27
+ }
18
28
  }
19
29
  }
@@ -7,8 +7,8 @@ import { Select } from '../Select/Select';
7
7
  import { Option } from '../Select/Option';
8
8
 
9
9
  const defaultParams: Props = {
10
- title: 'Example',
11
- titleVariant: 'h2',
10
+ legend: 'Example',
11
+ legendStyle: 'h2',
12
12
  children: [
13
13
  <FormControl data-testid="form-control">
14
14
  <Input placeholder="This is a placeholder" name="example" type="text" />
@@ -6,24 +6,28 @@ import { Typography, Variant } from '../../Typography/Typography';
6
6
 
7
7
  export interface Props extends HTMLProps<HTMLFieldSetElement> {
8
8
  children?: ReactElement | ReactElement[];
9
- title: string;
10
- titleVariant?: Variant;
11
- hideTitle?: boolean;
9
+ legend: string;
10
+ legendStyle?: Variant;
11
+ hideLegend?: boolean;
12
12
  background?: string;
13
13
  noPadding?: boolean;
14
14
  noBackground?: boolean;
15
+ required?: boolean;
16
+ error?: boolean;
15
17
  }
16
18
 
17
19
  export const Fieldset = ({
18
20
  children,
19
21
  className,
20
- title,
21
- titleVariant = 'body',
22
- hideTitle = false,
22
+ legend,
23
+ legendStyle = 'body',
24
+ hideLegend = false,
23
25
  noBackground,
24
26
  background = noBackground ? '' : '#FFF',
25
27
  noPadding = false,
26
28
  disabled = false,
29
+ required = false,
30
+ error = false,
27
31
  ...rest
28
32
  }: Props) => {
29
33
  const renderChildren = () => {
@@ -32,6 +36,7 @@ export const Fieldset = ({
32
36
  return React.Children.map(children, (child: ReactElement) =>
33
37
  React.cloneElement(child, {
34
38
  disabled: child.props.disabled !== undefined ? child.props.disabled : disabled,
39
+ error: child.props.error !== undefined ? child.props.error : error,
35
40
  })
36
41
  );
37
42
  };
@@ -43,10 +48,17 @@ export const Fieldset = ({
43
48
  style={{ backgroundColor: background, ...rest.style }}
44
49
  className={`${classes.fieldset} ${noPadding ? classes['no-padding'] : ''} ${className ?? ''}`}
45
50
  >
46
- {title && <legend className={readyclasses['sr-only']}>{title}</legend>}
47
- {title && !hideTitle && (
48
- <Typography variant={titleVariant} tag="span" aria-hidden="true" className={classes.title}>
49
- {title}
51
+ {legend && <legend className={readyclasses['sr-only']}>{legend}</legend>}
52
+ {legend && !hideLegend && (
53
+ <Typography
54
+ variant={legendStyle}
55
+ tag="span"
56
+ aria-hidden="true"
57
+ className={`${classes['legend']} ${required ? classes['required'] : ''} ${
58
+ error ? classes['error'] : ''
59
+ }`}
60
+ >
61
+ {legend}
50
62
  </Typography>
51
63
  )}
52
64
  {renderChildren()}
@@ -6,11 +6,13 @@ export interface Props extends HTMLProps<HTMLDivElement> {
6
6
  children: ReactElement | ReactElement[];
7
7
  grid?: 1 | 2 | 3;
8
8
  align?: 'top' | 'start' | 'middle' | 'center' | 'bottom' | 'end' | 'stretch';
9
+ error?: boolean;
9
10
  }
10
11
 
11
12
  export const FormControl = ({
12
13
  children,
13
14
  disabled,
15
+ error,
14
16
  className,
15
17
  grid,
16
18
  align = 'center',
@@ -24,6 +26,7 @@ export const FormControl = ({
24
26
 
25
27
  const childElement = React.cloneElement(child, {
26
28
  disabled: child.props.disabled !== undefined ? child.props.disabled : disabled,
29
+ error: child.props.error !== undefined ? child.props.error : error,
27
30
  });
28
31
 
29
32
  if (grid && grid > 1) {
@@ -98,3 +98,7 @@
98
98
  color: var(--disabled);
99
99
  }
100
100
  }
101
+
102
+ .disabled {
103
+ color: var(--disabled);
104
+ }
@@ -87,8 +87,18 @@ export const Radio = ({
87
87
  type="radio"
88
88
  />
89
89
 
90
- {checked && <Icon className={classes.input} icon={Icons.Radio} />}
91
- {!checked && <Icon className={classes.input} icon={Icons.Circle} />}
90
+ {checked && (
91
+ <Icon
92
+ className={`${classes['input']} ${disabled ? classes['disabled'] : ''}`}
93
+ icon={Icons.Radio}
94
+ />
95
+ )}
96
+ {!checked && (
97
+ <Icon
98
+ className={`${classes['input']} ${disabled ? classes['disabled'] : ''}`}
99
+ icon={Icons.Circle}
100
+ />
101
+ )}
92
102
 
93
103
  <label onClick={onChangeHandler} htmlFor={`${identifier}-radio`}>
94
104
  {children}
@@ -21,7 +21,7 @@ const defaultParams: Props = {
21
21
  error: false,
22
22
  helperText: 'Helpertext',
23
23
  name: 'Checkboxwrapper',
24
- fieldsetProps: { title: 'Example title' },
24
+ fieldsetProps: { legend: 'Example title' },
25
25
  label: 'Label',
26
26
  children: [],
27
27
  };
@@ -21,7 +21,7 @@ export const CheckboxWrapper = ({
21
21
  const { errorId, helperId } = useWrapper();
22
22
 
23
23
  useEffect(() => {
24
- if (fieldsetProps.title === undefined) {
24
+ if (fieldsetProps.legend === undefined) {
25
25
  console.error(
26
26
  `You should give your Fieldset component a title prop so a legend element is rendered. This error was thrown in CheckboxWrapper. You can add this title prop through the fieldsetProps prop by passing an object (fieldsetProps={{ title: "title here" }})`
27
27
  );
@@ -12,7 +12,7 @@ const defaultParams: Props = {
12
12
  helperText: 'Helper text',
13
13
  error: false,
14
14
  onChange: onChangeHandler,
15
- fieldsetProps: { title: 'Example title' },
15
+ fieldsetProps: { legend: 'Example title' },
16
16
  value: checkedOptionValue,
17
17
  name: 'my-group',
18
18
  children: [
@@ -27,7 +27,7 @@ export const RadioWrapper = ({
27
27
  const { errorId, helperId } = useWrapper(value);
28
28
 
29
29
  useEffect(() => {
30
- if (fieldsetProps.title === undefined) {
30
+ if (fieldsetProps.legend === undefined) {
31
31
  console.error(
32
32
  `You should give your Fieldset component a title prop so a legend element is rendered. This error was thrown in RadioWrapper. You can add this title prop through the fieldsetProps prop by passing an object (fieldsetProps={{ title: "title here" }})`
33
33
  );
@@ -4,10 +4,11 @@ import { Wrapper, WrapperProps } from '../Wrapper/Wrapper';
4
4
  import { Select, Props as SelectProps } from '../../Select/Select';
5
5
  import { useWrapper } from '../../../hooks/useWrapper';
6
6
 
7
- export interface Props extends Omit<WrapperProps, 'onChange'> {
7
+ export interface Props extends Omit<WrapperProps, 'onChange' | 'error'> {
8
8
  children: ReactChild | ReactChild[];
9
9
  placeholder?: string;
10
10
  value: string;
11
+ error?: boolean;
11
12
  selectProps?: SelectProps;
12
13
  onChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void;
13
14
  onClear?: () => void;
@@ -1,13 +1,6 @@
1
1
  import React from 'react';
2
2
  import { BaseModal, Props } from './BaseModal';
3
- import {
4
- render,
5
- getByRole,
6
- getByText,
7
- queryByText,
8
- queryByRole,
9
- fireEvent,
10
- } from '@testing-library/react';
3
+ import { render, getByText, queryByText, fireEvent } from '@testing-library/react';
11
4
  import userEvent from '@testing-library/user-event';
12
5
 
13
6
  const classNames = ['class11', 'class12'];
@@ -24,8 +17,8 @@ const initParams: Props = {
24
17
 
25
18
  describe('BaseModal', () => {
26
19
  it('renders without crashing', () => {
27
- const { container } = render(<BaseModal {...initParams} />);
28
- const dialog = getByRole(container, 'dialog');
20
+ const { getByRole } = render(<BaseModal {...initParams} />);
21
+ const dialog = getByRole('dialog');
29
22
  expect(dialog).toHaveAttribute('aria-modal', 'true');
30
23
  expect(dialog).toHaveAttribute('aria-labelledby', 'modal-label');
31
24
  expect(dialog).toHaveAttribute('aria-describedby', 'modal-description');
@@ -36,17 +29,17 @@ describe('BaseModal', () => {
36
29
  });
37
30
 
38
31
  it('should render close modal without content', () => {
39
- const { container } = render(<BaseModal {...initParams} open={false} />);
40
- const dialogByRole = queryByRole(container, 'dialog');
41
- const dialog = container.children[0] as HTMLElement;
32
+ const { queryByRole } = render(<BaseModal {...initParams} open={false} />);
33
+ const dialogByRole = queryByRole('dialog');
34
+ const dialog = document.body.children[1] as HTMLElement;
42
35
  expect(dialogByRole).toBeNull();
43
36
  expect(dialog).toHaveAttribute('aria-hidden', 'true');
44
37
  expect(queryByText(dialog, initParams.children as string)).toBeNull();
45
38
  });
46
39
 
47
40
  it('should handle clicking on backdrop & ESC key', () => {
48
- const { container } = render(<BaseModal {...initParams} />);
49
- const modal = getByRole(container, 'dialog');
41
+ const { getByRole } = render(<BaseModal {...initParams} />);
42
+ const modal = getByRole('dialog');
50
43
  const backdrop = modal.querySelector('.backdrop') as HTMLElement;
51
44
  expect(initParams.onClose).toHaveBeenCalledTimes(0);
52
45
 
@@ -1,4 +1,6 @@
1
- import React, { HTMLAttributes, useEffect, useRef } from 'react';
1
+ import React, { useEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { HTMLAttributes } from '../../interfaces';
2
4
  import classes from './BaseModal.module.scss';
3
5
  import { labelId, descriptionId } from './BaseModalContext';
4
6
 
@@ -17,27 +19,9 @@ export interface Props extends HTMLAttributes<HTMLDivElement> {
17
19
  disableEscapeKeyDown?: boolean;
18
20
  disableBackdrop?: boolean;
19
21
  zIndex?: number;
22
+ domRoot?: HTMLElement;
20
23
  }
21
24
 
22
- const useBackdropOnCloseClick = (
23
- disableBackdrop: boolean,
24
- onClose?: (event?: React.MouseEvent<HTMLElement>) => unknown
25
- ) => {
26
- const backdropRef = useRef<HTMLDivElement>(null);
27
- const onBackdropClick = () => onClose && onClose();
28
-
29
- useEffect(() => {
30
- !disableBackdrop && backdropRef.current?.addEventListener('click', onBackdropClick);
31
- return () => {
32
- !disableBackdrop && backdropRef.current?.removeEventListener('click', onBackdropClick);
33
- };
34
- }, []);
35
-
36
- return {
37
- backdropRef,
38
- };
39
- };
40
-
41
25
  export const useSetBodyScroll = (open: boolean) => {
42
26
  const hideBodyScroll = () => {
43
27
  document.body.style[SCROLL_PROPERTY_NAME] = SCROLL_PROPERTY_VALUE;
@@ -72,9 +56,9 @@ export const BaseModal = ({
72
56
  disableEscapeKeyDown = false,
73
57
  disableBackdrop = false,
74
58
  zIndex,
59
+ domRoot = document.body,
75
60
  ...restProps
76
61
  }: Props) => {
77
- const { backdropRef } = useBackdropOnCloseClick(disableBackdrop, onClose);
78
62
  useSetBodyScroll(open);
79
63
 
80
64
  const handleEscKeyPress = (event: React.KeyboardEvent<HTMLDivElement>) => {
@@ -84,7 +68,9 @@ export const BaseModal = ({
84
68
  }
85
69
  };
86
70
 
87
- return (
71
+ const handleBackdropClick = () => !disableBackdrop && onClose && onClose();
72
+
73
+ return createPortal(
88
74
  <div
89
75
  {...restProps}
90
76
  id={id}
@@ -99,7 +85,7 @@ export const BaseModal = ({
99
85
  onKeyDown={handleEscKeyPress}
100
86
  style={{ zIndex }}
101
87
  >
102
- <div ref={backdropRef} className={classes['backdrop']}></div>
88
+ <div className={classes['backdrop']} onClick={handleBackdropClick}></div>
103
89
  {open && (
104
90
  <div
105
91
  style={{ zIndex: zIndex && zIndex + 1 }}
@@ -108,6 +94,7 @@ export const BaseModal = ({
108
94
  {children}
109
95
  </div>
110
96
  )}
111
- </div>
97
+ </div>,
98
+ domRoot
112
99
  );
113
100
  };
@@ -1,10 +1,10 @@
1
1
  import React, { HTMLAttributes } from 'react';
2
2
  import classes from './BaseModalHeader.module.scss';
3
- import { IconButton } from '../../Button/IconButton';
4
- import { Icon, Icons } from '../../Icon/Icon';
5
- import { Typography } from '../../Typography/Typography';
3
+ import { IconButton } from '../../../Button/IconButton';
4
+ import { Icon, Icons } from '../../../Icon/Icon';
5
+ import { Typography } from '../../../Typography/Typography';
6
6
 
7
- export interface Props extends HTMLAttributes<HTMLElement> {
7
+ export interface Props extends HTMLAttributes<HTMLDivElement> {
8
8
  id: string;
9
9
  title: string;
10
10
  children?: React.ReactNode;
@@ -13,7 +13,7 @@ export interface Props extends HTMLAttributes<HTMLElement> {
13
13
 
14
14
  export const BaseModalHeader = ({ id, title, children, onClose, ...restProps }: Props) => {
15
15
  return (
16
- <header {...restProps} className={classes['header']}>
16
+ <div {...restProps} className={classes['header']}>
17
17
  <div className={classes['headline']}>
18
18
  <Typography id={id} className={classes['title']} tag="h1" variant="h4">
19
19
  {title}
@@ -23,6 +23,6 @@ export const BaseModalHeader = ({ id, title, children, onClose, ...restProps }:
23
23
  </IconButton>
24
24
  </div>
25
25
  {children}
26
- </header>
26
+ </div>
27
27
  );
28
28
  };
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Dialog, Props } from './Dialog';
3
- import { render, getAllByRole, getByRole, fireEvent, getByText } from '@testing-library/react';
3
+ import { render, getAllByRole } from '@testing-library/react';
4
4
  import userEvent from '@testing-library/user-event';
5
5
 
6
6
  const initParams: Props = {
@@ -24,11 +24,11 @@ const getButtons = (container: HTMLElement) => getAllByRole(container, 'button')
24
24
 
25
25
  describe('Dialog', () => {
26
26
  it('renders without crashing', () => {
27
- const { container } = render(<Dialog {...initParams} />);
28
- const [primaryButton, secondaryButton] = getButtons(container);
27
+ const { getByText } = render(<Dialog {...initParams} />);
28
+ const [primaryButton, secondaryButton] = getButtons(document.body);
29
29
 
30
- expect(getByText(container, initParams.title)).toBeDefined();
31
- expect(getByText(container, initParams.children as string)).toBeDefined();
30
+ expect(getByText(initParams.title)).toBeDefined();
31
+ expect(getByText(initParams.children as string)).toBeDefined();
32
32
  const actionsDiv = primaryButton.closest('footer');
33
33
  expect(actionsDiv).toHaveClass('left');
34
34
  expect(actionsDiv?.children[0]).toEqual(primaryButton);
@@ -38,8 +38,8 @@ describe('Dialog', () => {
38
38
  });
39
39
 
40
40
  it('renders action aligned to right', () => {
41
- const { container } = render(<Dialog {...initParams} alignActions="right" />);
42
- const [secondaryButton, primaryButton] = getButtons(container);
41
+ render(<Dialog {...initParams} alignActions="right" />);
42
+ const [secondaryButton, primaryButton] = getButtons(document.body);
43
43
 
44
44
  const actionsDiv = primaryButton.closest('footer');
45
45
  expect(actionsDiv).not.toHaveClass('left');
@@ -50,27 +50,24 @@ describe('Dialog', () => {
50
50
  });
51
51
 
52
52
  it('renders only one button', () => {
53
- const { container } = render(<Dialog {...initParams} secondaryAction={undefined} />);
54
- const buttons = getButtons(container);
53
+ render(<Dialog {...initParams} secondaryAction={undefined} />);
54
+ const buttons = getButtons(document.body);
55
55
 
56
56
  expect(buttons).toHaveLength(1);
57
57
  expect(buttons[0]).toHaveClass('fill');
58
58
  });
59
59
 
60
- it('should handle clicking on buttons, ESC and ENTER keys', () => {
61
- const { container } = render(<Dialog {...initParams} />);
62
- const [primaryButton, secondaryButton] = getButtons(container);
60
+ it('should handle clicking on buttons and ENTER press', () => {
61
+ render(<Dialog {...initParams} />);
62
+ const [primaryButton, secondaryButton] = getButtons(document.body);
63
63
  expect(initParams.primaryAction.onClick).toHaveBeenCalledTimes(0);
64
64
  expect(initParams.secondaryAction?.onClick).toHaveBeenCalledTimes(0);
65
65
  expect(initParams.onClose).toHaveBeenCalledTimes(0);
66
66
 
67
- const autoSummissionInput = container.querySelector('input') as HTMLElement;
67
+ const autoSummissionInput = document.body.querySelector('input') as HTMLElement;
68
68
  userEvent.type(autoSummissionInput, '{enter}');
69
69
  expect(initParams.primaryAction.onClick).toHaveBeenCalledTimes(1);
70
70
 
71
- fireEvent.keyDown(getByRole(container, 'dialog'), { key: 'Escape' });
72
- expect(initParams.onClose).toHaveBeenCalledTimes(1);
73
-
74
71
  userEvent.click(primaryButton);
75
72
  expect(initParams.primaryAction.onClick).toHaveBeenCalledTimes(2);
76
73
  userEvent.click(secondaryButton);
@@ -1,14 +1,15 @@
1
- import React, { HTMLAttributes } from 'react';
1
+ import React, { HTMLAttributes, useState } from 'react';
2
2
  import { BaseModal } from '../BaseModal/BaseModal';
3
3
  import { BaseModalContent } from '../BaseModal/BaseModalContent/BaseModalContent';
4
4
  import { DialogActions } from './DialogActions/DialogActions';
5
5
  import classes from './Dialog.module.scss';
6
6
  import { DialogTitle } from './DialogTitle/DialogTitle';
7
- import { Button, Props as ButtonProps } from '../Button/Button';
7
+ import { Button, Props as ButtonProps } from '../../Button/Button';
8
8
  import { labelId, descriptionId } from '../BaseModal/BaseModalContext';
9
+ import { generateID } from '../../util/helper';
9
10
 
10
11
  export interface Props extends HTMLAttributes<HTMLDivElement> {
11
- id: string;
12
+ id?: string;
12
13
  open: boolean;
13
14
  children: React.ReactNode;
14
15
  alignActions: 'left' | 'right';
@@ -17,6 +18,7 @@ export interface Props extends HTMLAttributes<HTMLDivElement> {
17
18
  primaryAction: Action;
18
19
  secondaryAction?: Action;
19
20
  zIndex?: number;
21
+ disableEscapeKeyDown?: boolean;
20
22
  }
21
23
 
22
24
  export interface Action extends Omit<ButtonProps, 'variant' | 'ref'> {
@@ -34,8 +36,10 @@ export const Dialog = ({
34
36
  primaryAction,
35
37
  secondaryAction,
36
38
  zIndex,
39
+ disableEscapeKeyDown = true,
37
40
  ...restProps
38
41
  }: Props) => {
42
+ const [dialogId] = useState(id ?? generateID(20));
39
43
  const { label: primaryLabel, ...restOfPrimaryAction } = primaryAction;
40
44
  const PrimaryButton = (
41
45
  <Button key="primary" {...restOfPrimaryAction}>
@@ -63,16 +67,21 @@ export const Dialog = ({
63
67
  return (
64
68
  <BaseModal
65
69
  {...restProps}
66
- id={id}
70
+ id={dialogId}
67
71
  className={classes['dialog']}
68
72
  containerClassName={classes['container']}
69
73
  open={open}
70
74
  disableBackdrop
71
75
  onClose={onClose}
72
76
  zIndex={zIndex}
77
+ disableEscapeKeyDown={disableEscapeKeyDown}
73
78
  >
74
- <DialogTitle id={labelId(id)} title={title} />
75
- <BaseModalContent id={descriptionId(id)} className={classes['content']} disableAutoFocus>
79
+ <DialogTitle id={labelId(dialogId)} title={title} />
80
+ <BaseModalContent
81
+ id={descriptionId(dialogId)}
82
+ className={classes['content']}
83
+ disableAutoFocus
84
+ >
76
85
  {children}
77
86
  </BaseModalContent>
78
87
  <DialogActions align={alignActions}>
@@ -82,12 +91,14 @@ export const Dialog = ({
82
91
  </DialogActions>
83
92
  <input
84
93
  autoFocus
94
+ aria-hidden={true}
85
95
  style={{
86
96
  position: 'absolute',
87
97
  width: 0,
88
98
  height: 0,
89
99
  opacity: 0,
90
100
  }}
101
+ maxLength={0}
91
102
  tabIndex={-1}
92
103
  onKeyPress={onHiddenInputKeyPress}
93
104
  />