@react-ui-org/react-ui 0.57.0 → 0.59.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 (118) hide show
  1. package/.nvmrc +1 -1
  2. package/README.md +2 -11
  3. package/dist/react-ui.css +19 -19
  4. package/dist/react-ui.development.css +1351 -963
  5. package/dist/react-ui.development.js +187 -87
  6. package/dist/react-ui.js +1 -1
  7. package/package.json +16 -5
  8. package/src/components/Alert/Alert.jsx +7 -9
  9. package/src/components/Alert/Alert.module.scss +3 -3
  10. package/src/components/Alert/README.md +18 -32
  11. package/src/components/Alert/_settings.scss +1 -2
  12. package/src/components/Badge/Badge.jsx +3 -3
  13. package/src/components/Button/Button.jsx +3 -3
  14. package/src/components/ButtonGroup/ButtonGroup.jsx +3 -3
  15. package/src/components/Card/Card.jsx +7 -7
  16. package/src/components/Card/Card.module.scss +8 -7
  17. package/src/components/Card/CardBody.jsx +2 -2
  18. package/src/components/Card/CardFooter.jsx +2 -2
  19. package/src/components/Card/README.md +20 -17
  20. package/src/components/Card/_settings.scss +1 -2
  21. package/src/components/Card/_theme.scss +1 -0
  22. package/src/components/CheckboxField/CheckboxField.jsx +11 -5
  23. package/src/components/CheckboxField/README.md +110 -5
  24. package/src/components/FileInputField/FileInputField.jsx +148 -22
  25. package/src/components/FileInputField/FileInputField.module.scss +87 -1
  26. package/src/components/FileInputField/README.md +83 -2
  27. package/src/components/FileInputField/_settings.scss +15 -0
  28. package/src/components/FormLayout/FormLayout.jsx +3 -3
  29. package/src/components/FormLayout/FormLayoutCustomField.jsx +3 -3
  30. package/src/components/FormLayout/README.md +1 -0
  31. package/src/components/Grid/Grid.jsx +2 -2
  32. package/src/components/Grid/Grid.module.scss +2 -2
  33. package/src/components/Grid/GridSpan.jsx +2 -2
  34. package/src/components/InputGroup/InputGroup.jsx +4 -4
  35. package/src/components/InputGroup/InputGroup.module.scss +12 -8
  36. package/src/components/InputGroup/README.md +1 -1
  37. package/src/components/Modal/Modal.jsx +118 -46
  38. package/src/components/Modal/Modal.module.scss +34 -18
  39. package/src/components/Modal/ModalBody.jsx +3 -3
  40. package/src/components/Modal/ModalBody.module.scss +18 -0
  41. package/src/components/Modal/ModalCloseButton.jsx +4 -6
  42. package/src/components/Modal/ModalContent.jsx +2 -2
  43. package/src/components/Modal/ModalFooter.jsx +3 -3
  44. package/src/components/Modal/ModalFooter.module.scss +6 -2
  45. package/src/components/Modal/ModalHeader.jsx +3 -3
  46. package/src/components/Modal/ModalHeader.module.scss +8 -1
  47. package/src/components/Modal/ModalTitle.jsx +2 -2
  48. package/src/components/Modal/README.md +407 -187
  49. package/src/components/Modal/_animations.scss +9 -0
  50. package/src/components/Modal/_helpers/dialogOnCancelHandler.js +28 -0
  51. package/src/components/Modal/_helpers/dialogOnClickHandler.js +46 -0
  52. package/src/components/Modal/_helpers/dialogOnCloseHandler.js +28 -0
  53. package/src/components/Modal/_helpers/dialogOnKeyDownHandler.js +62 -0
  54. package/src/components/Modal/_helpers/getPositionClassName.js +1 -1
  55. package/src/components/Modal/_hooks/useModalFocus.js +24 -91
  56. package/src/components/Modal/_settings.scss +4 -3
  57. package/src/components/Modal/_theme.scss +1 -0
  58. package/src/components/Paper/Paper.jsx +3 -3
  59. package/src/components/Popover/Popover.jsx +60 -15
  60. package/src/components/Popover/Popover.module.scss +37 -9
  61. package/src/components/Popover/PopoverWrapper.jsx +2 -2
  62. package/src/components/Popover/README.md +60 -3
  63. package/src/components/Popover/_helpers/cleanPlacementStyle.js +20 -0
  64. package/src/components/Radio/README.md +103 -0
  65. package/src/components/Radio/Radio.jsx +11 -5
  66. package/src/components/Radio/Radio.module.scss +4 -0
  67. package/src/components/ScrollView/ScrollView.jsx +5 -7
  68. package/src/components/SelectField/README.md +103 -0
  69. package/src/components/SelectField/SelectField.jsx +11 -5
  70. package/src/components/Table/Table.jsx +2 -2
  71. package/src/components/Tabs/Tabs.jsx +2 -2
  72. package/src/components/Tabs/TabsItem.jsx +3 -3
  73. package/src/components/Text/Text.jsx +3 -3
  74. package/src/components/TextArea/TextArea.jsx +3 -3
  75. package/src/components/TextField/README.md +14 -2
  76. package/src/components/TextField/TextField.jsx +3 -3
  77. package/src/components/TextLink/README.md +10 -3
  78. package/src/components/TextLink/TextLink.jsx +2 -2
  79. package/src/components/TextLink/_theme.scss +3 -3
  80. package/src/components/Toggle/README.md +83 -1
  81. package/src/components/Toggle/Toggle.jsx +11 -5
  82. package/src/components/Toolbar/Toolbar.jsx +3 -3
  83. package/src/components/Toolbar/ToolbarGroup.jsx +3 -3
  84. package/src/components/Toolbar/ToolbarItem.jsx +3 -3
  85. package/src/components/_helpers/resolveContextOrProp.js +6 -3
  86. package/src/helpers/classNames/README.md +65 -0
  87. package/src/helpers/classNames/classNames.js +11 -0
  88. package/src/helpers/classNames/index.js +1 -0
  89. package/src/helpers/transferProps/README.md +46 -0
  90. package/src/helpers/transferProps/index.js +1 -0
  91. package/src/index.js +6 -5
  92. package/src/providers/globalProps/GlobalPropsContext.jsx +5 -0
  93. package/src/providers/globalProps/GlobalPropsProvider.jsx +33 -0
  94. package/src/providers/globalProps/index.js +3 -0
  95. package/src/{provider → providers/globalProps}/withGlobalProps.jsx +16 -16
  96. package/src/providers/translations/TranslationsContext.jsx +6 -0
  97. package/src/providers/translations/TranslationsProvider.jsx +33 -0
  98. package/src/providers/translations/index.js +2 -0
  99. package/src/styles/elements/_links.scss +2 -9
  100. package/src/styles/generic/_focus.scss +1 -1
  101. package/src/styles/theme/_form-fields.scss +19 -0
  102. package/src/styles/theme/_links.scss +4 -3
  103. package/src/styles/tools/_accessibility.scss +3 -5
  104. package/src/styles/tools/_collections.scss +62 -5
  105. package/src/styles/tools/_links.scss +17 -0
  106. package/src/styles/tools/form-fields/_box-field-elements.scss +21 -9
  107. package/src/styles/tools/form-fields/_box-field-layout.scss +2 -2
  108. package/src/styles/tools/form-fields/_box-field-sizes.scss +6 -10
  109. package/src/styles/tools/form-fields/_foundation.scss +6 -4
  110. package/src/styles/tools/form-fields/_variants.scss +12 -8
  111. package/src/theme.scss +53 -2
  112. package/src/translations/en.js +5 -0
  113. package/src/provider/RUIContext.jsx +0 -9
  114. package/src/provider/RUIProvider.jsx +0 -42
  115. package/src/provider/index.js +0 -3
  116. package/src/styles/settings/_z-indexes.scss +0 -2
  117. package/src/utils/classNames.js +0 -8
  118. /package/src/{utils → helpers/transferProps}/transferProps.js +0 -0
@@ -0,0 +1,9 @@
1
+ @keyframes fade-in {
2
+ 0% {
3
+ opacity: 0;
4
+ }
5
+
6
+ 100% {
7
+ opacity: 1;
8
+ }
9
+ }
@@ -0,0 +1,28 @@
1
+ // Disable coverage for the following function
2
+ /* istanbul ignore next */
3
+
4
+ /**
5
+ * Handles the cancel event of the dialog which is fired when the user presses the Escape key or triggers cancel event
6
+ * by native dialog mechanism.
7
+ *
8
+ * It prevents the default behaviour of the native dialog and closes the dialog manually by clicking the close button,
9
+ * if the close button is not disabled.
10
+ *
11
+ * @param e
12
+ * @param closeButtonRef
13
+ * @param onCancelHandler
14
+ */
15
+ export const dialogOnCancelHandler = (e, closeButtonRef, onCancelHandler = undefined) => {
16
+ // Prevent the default behaviour of the event as we want to close dialog manually.
17
+ e.preventDefault();
18
+
19
+ // If the close button is not disabled, close the modal.
20
+ if (closeButtonRef?.current != null && closeButtonRef?.current?.disabled === false) {
21
+ closeButtonRef.current.click();
22
+ }
23
+
24
+ // This is a custom handler that is passed as a prop to the Modal component
25
+ if (onCancelHandler) {
26
+ onCancelHandler(e);
27
+ }
28
+ };
@@ -0,0 +1,46 @@
1
+ // Disable coverage for the following function
2
+ /* istanbul ignore next */
3
+
4
+ /**
5
+ * Handles the click event of the dialog which is fired when the user clicks on the dialog or on its descendants.
6
+ *
7
+ * This handler is used to close the dialog when the user clicks on the backdrop, if it is allowed to close
8
+ * on backdrop click and the close button is not disabled.
9
+ *
10
+ * @param e
11
+ * @param closeButtonRef
12
+ * @param dialogRef
13
+ * @param allowCloseOnBackdropClick
14
+ */
15
+ export const dialogOnClickHandler = (
16
+ e,
17
+ closeButtonRef,
18
+ dialogRef,
19
+ allowCloseOnBackdropClick,
20
+ ) => {
21
+ // If it is not allowed to close modal on backdrop click, do nothing.
22
+ if (!allowCloseOnBackdropClick) {
23
+ return;
24
+ }
25
+
26
+ // Detection of the click on the backdrop is based on the following conditions:
27
+ // 1. The click target is the dialog itself. This prevents detection of clicks on the dialog's children.
28
+ // 2. The click is outside the dialog's boundaries.
29
+ const dialogRect = dialogRef.current.getBoundingClientRect();
30
+ const isClickedOnBackdrop = dialogRef.current === e.target && (
31
+ e.clientX < dialogRect.left
32
+ || e.clientX > dialogRect.right
33
+ || e.clientY < dialogRect.top
34
+ || e.clientY > dialogRect.bottom
35
+ );
36
+
37
+ // If user does not click on the backdrop, do nothing.
38
+ if (!isClickedOnBackdrop) {
39
+ return;
40
+ }
41
+
42
+ // If the close button is not disabled, close the modal.
43
+ if (closeButtonRef?.current != null && closeButtonRef?.current?.disabled === false) {
44
+ closeButtonRef.current.click();
45
+ }
46
+ };
@@ -0,0 +1,28 @@
1
+ // Disable coverage for the following function
2
+ /* istanbul ignore next */
3
+
4
+ /**
5
+ * Handles the close event of the dialog which is fired when the user presses the Escape key or triggers close event
6
+ * by native dialog mechanism.
7
+ *
8
+ * It prevents the default behaviour of the native dialog and closes the dialog manually by clicking the close button,
9
+ * if the close button is not disabled.
10
+ *
11
+ * @param e
12
+ * @param closeButtonRef
13
+ * @param onCloseHandler
14
+ */
15
+ export const dialogOnCloseHandler = (e, closeButtonRef, onCloseHandler = undefined) => {
16
+ // Prevent the default behaviour of the event as we want to close dialog manually.
17
+ e.preventDefault();
18
+
19
+ // If the close button is not disabled, close the modal.
20
+ if (closeButtonRef?.current != null && closeButtonRef?.current?.disabled === false) {
21
+ closeButtonRef.current.click();
22
+ }
23
+
24
+ // This is a custom handler that is passed as a prop to the Modal component
25
+ if (onCloseHandler) {
26
+ onCloseHandler(e);
27
+ }
28
+ };
@@ -0,0 +1,62 @@
1
+ // Disable coverage for the following function
2
+ /* istanbul ignore next */
3
+
4
+ /**
5
+ * Handles the keydown event of the dialog which is fired when the user presses a key within the dialog.
6
+ *
7
+ * This handler is used to stop propagation of the Escape key press, if it is not allowed to close
8
+ * on Escape key and the close button is disabled.
9
+ *
10
+ * It is also used to trigger the primary action when the user presses the Enter key, if it is allowed to trigger
11
+ * the primary action on Enter key and the primary button is not disabled. This applies only when the focused
12
+ * element is an input or select as other elements should not trigger the primary action. Textarea is omitted
13
+ * as Enter key is used for new line.
14
+ *
15
+ * @param e
16
+ * @param closeButtonRef
17
+ * @param primaryButtonRef
18
+ * @param allowCloseOnEscapeKey
19
+ * @param allowPrimaryActionOnEnterKey
20
+ */
21
+ export const dialogOnKeyDownHandler = (
22
+ e,
23
+ closeButtonRef,
24
+ primaryButtonRef,
25
+ allowCloseOnEscapeKey,
26
+ allowPrimaryActionOnEnterKey,
27
+ ) => {
28
+ if (e.key === 'Escape') {
29
+ // Prevent closing the modal using the Escape key when one of the following conditions is met:
30
+ // 1. The close button is not present
31
+ // 2. The close button is disabled
32
+ // 3. `allowCloseOnEscapeKey` is set to `false`
33
+ //
34
+ // ⚠️ Else-if statement calling `closeButtonRef.current.click()` is necessary due to missing support
35
+ // of close event in happy-dom library. When this is fixed, the `else` statement can be removed
36
+ // as the `closeButtonRef.current.click()` will be handled by `dialogOnCancelHandler.js`.
37
+ if (
38
+ closeButtonRef?.current == null
39
+ || closeButtonRef?.current?.disabled === true
40
+ || !allowCloseOnEscapeKey
41
+ ) {
42
+ e.preventDefault();
43
+ } else if (process?.env?.NODE_ENV === 'test') {
44
+ closeButtonRef.current.click();
45
+ }
46
+ }
47
+
48
+ // Trigger the primary action when the Enter key is pressed and the following conditions are met:
49
+ // 1. The primary button is present
50
+ // 2. The primary button is not disabled
51
+ // 3. `allowPrimaryActionOnEnterKey` is set to `true`
52
+ // 4. The focused element is an input or select (text area is omitted as Enter key is used for new line)
53
+ if (
54
+ e.key === 'Enter'
55
+ && primaryButtonRef?.current != null
56
+ && primaryButtonRef?.current?.disabled === false
57
+ && allowPrimaryActionOnEnterKey
58
+ && ['INPUT', 'SELECT'].includes(e.target.nodeName)
59
+ ) {
60
+ primaryButtonRef.current.click();
61
+ }
62
+ };
@@ -3,5 +3,5 @@ export const getPositionClassName = (modalPosition, styles) => {
3
3
  return styles.isRootPositionTop;
4
4
  }
5
5
 
6
- return styles.isRootPositionCenter;
6
+ return null;
7
7
  };
@@ -2,9 +2,8 @@ import { useEffect } from 'react';
2
2
 
3
3
  export const useModalFocus = (
4
4
  autoFocus,
5
- childrenWrapperRef,
5
+ dialogRef,
6
6
  primaryButtonRef,
7
- closeButtonRef,
8
7
  ) => {
9
8
  useEffect(
10
9
  () => {
@@ -12,115 +11,49 @@ export const useModalFocus = (
12
11
  // field element (input, textarea or select) or primary button and focuses it. This is
13
12
  // necessary to have focus on one of those elements to be able to submit the form
14
13
  // by pressing Enter key. If there are neither, it tries to focus any other focusable
15
- // elements. In case there are none or `autoFocus` is disabled, childrenWrapperElement
14
+ // elements. In case there are none or `autoFocus` is disabled, dialogElement
16
15
  // (Modal itself) is focused.
17
16
 
18
- const childrenWrapperElement = childrenWrapperRef.current;
17
+ const dialogElement = dialogRef.current;
19
18
 
20
- if (childrenWrapperElement == null) {
19
+ if (dialogElement == null) {
21
20
  return () => {};
22
21
  }
23
22
 
24
23
  const childrenFocusableElements = Array.from(
25
- childrenWrapperElement.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'),
24
+ dialogElement.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'),
26
25
  );
27
26
 
28
27
  const firstFocusableElement = childrenFocusableElements[0];
29
- const lastFocusableElement = childrenFocusableElements[childrenFocusableElements.length - 1];
30
28
 
31
- const resolveFocusBeforeListener = () => {
32
- if (!autoFocus || childrenFocusableElements.length === 0) {
33
- childrenWrapperElement.tabIndex = -1;
34
- childrenWrapperElement.focus();
35
- return;
36
- }
37
-
38
- const firstFormFieldEl = childrenFocusableElements.find(
39
- (element) => ['INPUT', 'TEXTAREA', 'SELECT'].includes(element.nodeName) && !element.disabled,
40
- );
41
-
42
- if (firstFormFieldEl) {
43
- firstFormFieldEl.focus();
44
- return;
45
- }
46
-
47
- if (primaryButtonRef?.current != null) {
48
- primaryButtonRef.current.focus();
49
- return;
50
- }
51
-
52
- firstFocusableElement.focus();
53
- };
54
-
55
- const keyPressHandler = (e) => {
56
- if (e.key === 'Escape' && closeButtonRef?.current != null) {
57
- closeButtonRef.current.click();
58
- return;
59
- }
60
-
61
- if (
62
- e.key === 'Enter'
63
- && e.target.nodeName !== 'BUTTON'
64
- && e.target.nodeName !== 'TEXTAREA'
65
- && e.target.nodeName !== 'A'
66
- && primaryButtonRef?.current != null
67
- ) {
68
- primaryButtonRef.current.click();
69
- return;
70
- }
71
-
72
- // Following code traps focus inside Modal
73
-
74
- if (e.key !== 'Tab') {
75
- return;
76
- }
77
-
78
- if (childrenFocusableElements.length === 0) {
79
- childrenWrapperElement.focus();
80
- e.preventDefault();
81
- return;
82
- }
83
-
84
- if (
85
- ![
86
- ...childrenFocusableElements,
87
- childrenWrapperElement,
88
- ]
89
- .includes(window.document.activeElement)
90
- ) {
91
- firstFocusableElement.focus();
92
- e.preventDefault();
93
- return;
94
- }
29
+ if (!autoFocus || childrenFocusableElements.length === 0) {
30
+ dialogElement.tabIndex = -1;
31
+ dialogElement.focus();
32
+ return () => {};
33
+ }
95
34
 
96
- if (!e.shiftKey && window.document.activeElement === lastFocusableElement) {
97
- firstFocusableElement.focus();
98
- e.preventDefault();
99
- return;
100
- }
35
+ const firstFormFieldEl = childrenFocusableElements.find(
36
+ (element) => ['INPUT', 'TEXTAREA', 'SELECT'].includes(element.nodeName) && !element.disabled,
37
+ );
101
38
 
102
- if (e.shiftKey
103
- && (
104
- window.document.activeElement === firstFocusableElement
105
- || window.document.activeElement === childrenWrapperElement
106
- )
107
- ) {
108
- lastFocusableElement.focus();
109
- e.preventDefault();
110
- }
111
- };
39
+ if (firstFormFieldEl) {
40
+ firstFormFieldEl.focus();
41
+ return () => {};
42
+ }
112
43
 
113
- resolveFocusBeforeListener();
44
+ if (primaryButtonRef?.current != null && primaryButtonRef?.current?.disabled === false) {
45
+ primaryButtonRef.current.focus();
46
+ return () => {};
47
+ }
114
48
 
115
- window.document.addEventListener('keydown', keyPressHandler, false);
49
+ firstFocusableElement.focus();
116
50
 
117
- return () => window.document.removeEventListener('keydown', keyPressHandler, false);
51
+ return () => {};
118
52
  },
119
53
  [
120
54
  autoFocus,
121
- childrenWrapperRef,
55
+ dialogRef,
122
56
  primaryButtonRef,
123
- closeButtonRef,
124
57
  ],
125
58
  );
126
59
  };
@@ -1,9 +1,10 @@
1
1
  @use "sass:map";
2
- @use "../../styles/settings/z-indexes";
2
+ @use "../../styles/settings/collections";
3
3
  @use "../../styles/theme/borders";
4
4
  @use "../../styles/theme/typography";
5
5
 
6
+ $border-width: borders.$width;
6
7
  $border-radius: borders.$radius-2;
7
- $z-index: z-indexes.$modal;
8
- $backdrop-z-index: z-indexes.$modal-backdrop;
9
8
  $title-font-size: map.get(typography.$font-size-values, 2);
9
+ $colors: collections.$feedback-colors;
10
+ $themeable-properties: border-color, background-color;
@@ -10,6 +10,7 @@ $footer-gap: var(--rui-Modal__footer__gap);
10
10
  $backdrop-background: var(--rui-Modal__backdrop__background);
11
11
  $outer-spacing-xs: var(--rui-Modal__outer-spacing--xs);
12
12
  $outer-spacing-sm: var(--rui-Modal__outer-spacing--sm);
13
+ $animation-duration: var(--rui-Modal__animation__duration);
13
14
 
14
15
  $sizes: (
15
16
  auto: (
@@ -1,8 +1,8 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React from 'react';
3
- import { withGlobalProps } from '../../provider';
4
- import { classNames } from '../../utils/classNames';
5
- import { transferProps } from '../../utils/transferProps';
3
+ import { withGlobalProps } from '../../providers/globalProps';
4
+ import { classNames } from '../../helpers/classNames/classNames';
5
+ import { transferProps } from '../../helpers/transferProps';
6
6
  import styles from './Paper.module.scss';
7
7
 
8
8
  export const Paper = ({
@@ -1,9 +1,10 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React from 'react';
3
3
  import { createPortal } from 'react-dom';
4
- import { withGlobalProps } from '../../provider';
5
- import { classNames } from '../../utils/classNames';
6
- import { transferProps } from '../../utils/transferProps';
4
+ import { transferProps } from '../../helpers/transferProps';
5
+ import { classNames } from '../../helpers/classNames';
6
+ import { withGlobalProps } from '../../providers/globalProps';
7
+ import cleanPlacementStyle from './_helpers/cleanPlacementStyle';
7
8
  import getRootSideClassName from './_helpers/getRootSideClassName';
8
9
  import getRootAlignmentClassName from './_helpers/getRootAlignmentClassName';
9
10
  import styles from './Popover.module.scss';
@@ -12,24 +13,42 @@ export const Popover = React.forwardRef((props, ref) => {
12
13
  const {
13
14
  placement,
14
15
  children,
16
+ placementStyle,
17
+ popoverTargetId,
15
18
  portalId,
16
19
  ...restProps
17
20
  } = props;
18
21
 
19
22
  const PopoverEl = (
20
- <div
21
- {...transferProps(restProps)}
22
- className={classNames(
23
- styles.root,
24
- ref && styles.isRootControlled,
25
- getRootSideClassName(placement, styles),
26
- getRootAlignmentClassName(placement, styles),
23
+ <>
24
+ {/**
25
+ * This hack is needed because the default behavior of the Popover API is to place the popover into a
26
+ * top-layer. It is currently not possible to position an element in the top-layer relative to a normal element.
27
+ * This will create a hidden browser popover, then with CSS it will open and close the RUI popover.
28
+ */}
29
+ {!!popoverTargetId && (
30
+ <div
31
+ className={styles.helper}
32
+ id={popoverTargetId}
33
+ popover="auto"
34
+ />
27
35
  )}
28
- ref={ref}
29
- >
30
- {children}
31
- <span className={styles.arrow} />
32
- </div>
36
+ <div
37
+ {...transferProps(restProps)}
38
+ className={classNames(
39
+ styles.root,
40
+ ref && styles.isRootControlled,
41
+ popoverTargetId && styles.controlledPopover,
42
+ getRootSideClassName(placement, styles),
43
+ getRootAlignmentClassName(placement, styles),
44
+ )}
45
+ ref={ref}
46
+ style={placementStyle ? cleanPlacementStyle(placementStyle) : null}
47
+ >
48
+ {children}
49
+ <span className={styles.arrow} />
50
+ </div>
51
+ </>
33
52
  );
34
53
 
35
54
  if (portalId === null) {
@@ -41,6 +60,8 @@ export const Popover = React.forwardRef((props, ref) => {
41
60
 
42
61
  Popover.defaultProps = {
43
62
  placement: 'bottom',
63
+ placementStyle: null,
64
+ popoverTargetId: null,
44
65
  portalId: null,
45
66
  };
46
67
 
@@ -67,6 +88,30 @@ Popover.propTypes = {
67
88
  'left-start',
68
89
  'left-end',
69
90
  ]),
91
+ /**
92
+ * Used for positioning the popover with a library like Floating UI. It is filtered,
93
+ * then passed to the popover as the `style` prop.
94
+ */
95
+ placementStyle: PropTypes.shape({
96
+ bottom: PropTypes.string,
97
+ inset: PropTypes.string,
98
+ 'inset-block-end': PropTypes.string,
99
+ 'inset-block-start': PropTypes.string,
100
+ 'inset-inline-end': PropTypes.string,
101
+ 'inset-inline-start': PropTypes.string,
102
+ left: PropTypes.string,
103
+ position: PropTypes.string,
104
+ right: PropTypes.string,
105
+ top: PropTypes.string,
106
+ 'transform-origin': PropTypes.string,
107
+ translate: PropTypes.string,
108
+ }),
109
+ /**
110
+ * If set, the popover will become controlled, meaning it will be hidden by default and will need a trigger to open.
111
+ * This sets the ID of the internal helper element for the popover.
112
+ * Assign the same ID to `popovertarget` of a trigger to make it open and close.
113
+ */
114
+ popoverTargetId: PropTypes.string,
70
115
  /**
71
116
  * If set, popover is rendered in the React Portal with that ID.
72
117
  */
@@ -1,6 +1,12 @@
1
- // 1. Reset positioning for controlled variant.
2
- // 2. Shift Popover so there is space for the arrow between Popover and reference element.
3
- // 3. Add top offset in case it's not defined by external library.
1
+ // 1. Hide the popover by default. This is needed because the popover is
2
+ // controlled via CSS with the help of the helper popover. The popover can't
3
+ // be displayed directly, because relative positioning doesn't work with
4
+ // elements on the top-layer, so this CSS hack is needed.
5
+ // 2. Hide the popover helper element.
6
+ // 3. If the popover helper is open, show the actual popover.
7
+ // 4. Reset positioning for controlled variant.
8
+ // 5. Shift Popover so there is space for the arrow between Popover and reference element.
9
+ // 6. Add top offset in case it's not defined by external library.
4
10
 
5
11
  @use "theme";
6
12
 
@@ -49,6 +55,28 @@
49
55
  }
50
56
  }
51
57
 
58
+ // Controlled popover
59
+ .controlledPopover {
60
+ display: none; // 1.
61
+ }
62
+
63
+ .helper {
64
+ position: fixed; // 2.
65
+ inset: unset;
66
+ top: 0;
67
+ right: 0;
68
+ width: auto;
69
+ height: auto;
70
+ padding: 0;
71
+ border: none;
72
+ background: transparent;
73
+ pointer-events: none;
74
+ }
75
+
76
+ .helper:popover-open ~ .controlledPopover {
77
+ display: block; // 3.
78
+ }
79
+
52
80
  // Sides
53
81
  .isRootAtTop {
54
82
  bottom: calc(100% + #{theme.$arrow-gap} - #{theme.$arrow-safe-rendering-overlap});
@@ -212,27 +240,27 @@
212
240
  .isRootControlled.isRootAtBottom,
213
241
  .isRootControlled.isRootAtLeft,
214
242
  .isRootControlled.isRootAtRight {
215
- inset: unset; // 1.
243
+ inset: unset; // 4.
216
244
  }
217
245
 
218
246
  .isRootControlled.isRootAtTop {
219
- transform: translate(0, calc(-1 * #{theme.$arrow-height})); // 2.
247
+ transform: translate(0, calc(-1 * #{theme.$arrow-height})); // 5.
220
248
  }
221
249
 
222
250
  .isRootControlled.isRootAtBottom {
223
- transform: translate(0, #{theme.$arrow-height}); // 2.
251
+ transform: translate(0, #{theme.$arrow-height}); // 5.
224
252
  }
225
253
 
226
254
  .isRootControlled.isRootAtLeft {
227
- transform: translate(calc(-1 * #{theme.$arrow-height}), 0); // 2.
255
+ transform: translate(calc(-1 * #{theme.$arrow-height}), 0); // 5.
228
256
  }
229
257
 
230
258
  .isRootControlled.isRootAtRight {
231
- transform: translate(#{theme.$arrow-height}, 0); // 2.
259
+ transform: translate(#{theme.$arrow-height}, 0); // 5.
232
260
  }
233
261
 
234
262
  .isRootControlled.isRootAtLeft.isRootAtStart,
235
263
  .isRootControlled.isRootAtRight.isRootAtStart {
236
- top: 0; // 3.
264
+ top: 0; // 6.
237
265
  }
238
266
  }
@@ -1,7 +1,7 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React from 'react';
3
- import { withGlobalProps } from '../../provider';
4
- import { transferProps } from '../../utils/transferProps';
3
+ import { withGlobalProps } from '../../providers/globalProps';
4
+ import { transferProps } from '../../helpers/transferProps';
5
5
  import styles from './PopoverWrapper.module.scss';
6
6
 
7
7
  export const PopoverWrapper = ({
@@ -162,6 +162,29 @@ automatically, including smart position updates to ensure Popover visibility,
162
162
  we recommend to involve an external library designed specifically for this
163
163
  purpose.
164
164
 
165
+ To position the popover, you need to provide the `placementStyle` prop with the
166
+ style you want to apply to the popover. This prop should only be used to
167
+ position the popover. The allowed props are:
168
+
169
+ - `position`
170
+ - `inset`
171
+ - `inset-inline-start`
172
+ - `inset-inline-end`
173
+ - `inset-block-start`
174
+ - `inset-block-end`
175
+ - `top`
176
+ - `right`
177
+ - `bottom`
178
+ - `left`
179
+ - `translate`
180
+ - `transform-origin`
181
+
182
+ ⚠️ [`inset`][mdn-inset] is a shorthand for `top right bottom left`, not for
183
+ `inset-*` properties.
184
+
185
+ As opposed to `top right bottom left` and the `inset` shorthand, `inset-*`
186
+ properties are writing-direction aware.
187
+
165
188
  ℹ️ The following example is using external library [Floating UI]. To use
166
189
  Floating UI, install it first:
167
190
 
@@ -267,10 +290,10 @@ React.createElement(() => {
267
290
  <Popover
268
291
  id="my-advanced-popover"
269
292
  placement={finalPlacement}
270
- style={{
293
+ placementStyle={{
271
294
  position: strategy,
272
- top: y ? y : '',
273
- left: x ? x : '',
295
+ top: `${y}px`,
296
+ left: `${x}px`,
274
297
  }}
275
298
  ref={floating}
276
299
  >
@@ -284,6 +307,39 @@ React.createElement(() => {
284
307
  });
285
308
  ```
286
309
 
310
+ ## Controlled Popover
311
+
312
+ Popover API can be used to control visibility of Popover component. You need to
313
+ set `id` on the trigger element and matching `popoverTargetId` attribute on the
314
+ Popover component. This leverages the browser's Popover API to control the
315
+ popover, automatically closing it when the trigger or the backdrop is pressed.
316
+
317
+ ```docoff-react-preview
318
+ React.createElement(() => {
319
+ // All inline styles in this example are for demonstration purposes only.
320
+ return (
321
+ <div
322
+ style={{
323
+ display: 'grid',
324
+ placeContent: 'center',
325
+ minWidth: '20rem',
326
+ minHeight: '10rem',
327
+ }}
328
+ >
329
+ <PopoverWrapper>
330
+ <Button
331
+ label="Want to see a popover? Click me!"
332
+ popovertarget="my-popover-helper"
333
+ />
334
+ <Popover id="my-popover" popoverTargetId="my-popover-helper">
335
+ Hello there!
336
+ </Popover>
337
+ </PopoverWrapper>
338
+ </div>
339
+ );
340
+ });
341
+ ```
342
+
287
343
  ## Forwarding HTML Attributes
288
344
 
289
345
  In addition to the options below in the [component's API](#api) section, you
@@ -326,5 +382,6 @@ which enables [Advanced Positioning](#advanced-positioning).
326
382
 
327
383
  [div-attributes]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div#attributes
328
384
  [Floating UI]: https://floating-ui.com/docs/react-dom
385
+ [mdn-inset]: https://developer.mozilla.org/en-US/docs/Web/CSS/inset
329
386
  [React common props]: https://react.dev/reference/react-dom/components/common#common-props
330
387
  [ref]: https://reactjs.org/docs/refs-and-the-dom.html