@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.
- package/.nvmrc +1 -1
- package/README.md +2 -11
- package/dist/react-ui.css +19 -19
- package/dist/react-ui.development.css +1351 -963
- package/dist/react-ui.development.js +187 -87
- package/dist/react-ui.js +1 -1
- package/package.json +16 -5
- package/src/components/Alert/Alert.jsx +7 -9
- package/src/components/Alert/Alert.module.scss +3 -3
- package/src/components/Alert/README.md +18 -32
- package/src/components/Alert/_settings.scss +1 -2
- package/src/components/Badge/Badge.jsx +3 -3
- package/src/components/Button/Button.jsx +3 -3
- package/src/components/ButtonGroup/ButtonGroup.jsx +3 -3
- package/src/components/Card/Card.jsx +7 -7
- package/src/components/Card/Card.module.scss +8 -7
- package/src/components/Card/CardBody.jsx +2 -2
- package/src/components/Card/CardFooter.jsx +2 -2
- package/src/components/Card/README.md +20 -17
- package/src/components/Card/_settings.scss +1 -2
- package/src/components/Card/_theme.scss +1 -0
- package/src/components/CheckboxField/CheckboxField.jsx +11 -5
- package/src/components/CheckboxField/README.md +110 -5
- package/src/components/FileInputField/FileInputField.jsx +148 -22
- package/src/components/FileInputField/FileInputField.module.scss +87 -1
- package/src/components/FileInputField/README.md +83 -2
- package/src/components/FileInputField/_settings.scss +15 -0
- package/src/components/FormLayout/FormLayout.jsx +3 -3
- package/src/components/FormLayout/FormLayoutCustomField.jsx +3 -3
- package/src/components/FormLayout/README.md +1 -0
- package/src/components/Grid/Grid.jsx +2 -2
- package/src/components/Grid/Grid.module.scss +2 -2
- package/src/components/Grid/GridSpan.jsx +2 -2
- package/src/components/InputGroup/InputGroup.jsx +4 -4
- package/src/components/InputGroup/InputGroup.module.scss +12 -8
- package/src/components/InputGroup/README.md +1 -1
- package/src/components/Modal/Modal.jsx +118 -46
- package/src/components/Modal/Modal.module.scss +34 -18
- package/src/components/Modal/ModalBody.jsx +3 -3
- package/src/components/Modal/ModalBody.module.scss +18 -0
- package/src/components/Modal/ModalCloseButton.jsx +4 -6
- package/src/components/Modal/ModalContent.jsx +2 -2
- package/src/components/Modal/ModalFooter.jsx +3 -3
- package/src/components/Modal/ModalFooter.module.scss +6 -2
- package/src/components/Modal/ModalHeader.jsx +3 -3
- package/src/components/Modal/ModalHeader.module.scss +8 -1
- package/src/components/Modal/ModalTitle.jsx +2 -2
- package/src/components/Modal/README.md +407 -187
- package/src/components/Modal/_animations.scss +9 -0
- package/src/components/Modal/_helpers/dialogOnCancelHandler.js +28 -0
- package/src/components/Modal/_helpers/dialogOnClickHandler.js +46 -0
- package/src/components/Modal/_helpers/dialogOnCloseHandler.js +28 -0
- package/src/components/Modal/_helpers/dialogOnKeyDownHandler.js +62 -0
- package/src/components/Modal/_helpers/getPositionClassName.js +1 -1
- package/src/components/Modal/_hooks/useModalFocus.js +24 -91
- package/src/components/Modal/_settings.scss +4 -3
- package/src/components/Modal/_theme.scss +1 -0
- package/src/components/Paper/Paper.jsx +3 -3
- package/src/components/Popover/Popover.jsx +60 -15
- package/src/components/Popover/Popover.module.scss +37 -9
- package/src/components/Popover/PopoverWrapper.jsx +2 -2
- package/src/components/Popover/README.md +60 -3
- package/src/components/Popover/_helpers/cleanPlacementStyle.js +20 -0
- package/src/components/Radio/README.md +103 -0
- package/src/components/Radio/Radio.jsx +11 -5
- package/src/components/Radio/Radio.module.scss +4 -0
- package/src/components/ScrollView/ScrollView.jsx +5 -7
- package/src/components/SelectField/README.md +103 -0
- package/src/components/SelectField/SelectField.jsx +11 -5
- package/src/components/Table/Table.jsx +2 -2
- package/src/components/Tabs/Tabs.jsx +2 -2
- package/src/components/Tabs/TabsItem.jsx +3 -3
- package/src/components/Text/Text.jsx +3 -3
- package/src/components/TextArea/TextArea.jsx +3 -3
- package/src/components/TextField/README.md +14 -2
- package/src/components/TextField/TextField.jsx +3 -3
- package/src/components/TextLink/README.md +10 -3
- package/src/components/TextLink/TextLink.jsx +2 -2
- package/src/components/TextLink/_theme.scss +3 -3
- package/src/components/Toggle/README.md +83 -1
- package/src/components/Toggle/Toggle.jsx +11 -5
- package/src/components/Toolbar/Toolbar.jsx +3 -3
- package/src/components/Toolbar/ToolbarGroup.jsx +3 -3
- package/src/components/Toolbar/ToolbarItem.jsx +3 -3
- package/src/components/_helpers/resolveContextOrProp.js +6 -3
- package/src/helpers/classNames/README.md +65 -0
- package/src/helpers/classNames/classNames.js +11 -0
- package/src/helpers/classNames/index.js +1 -0
- package/src/helpers/transferProps/README.md +46 -0
- package/src/helpers/transferProps/index.js +1 -0
- package/src/index.js +6 -5
- package/src/providers/globalProps/GlobalPropsContext.jsx +5 -0
- package/src/providers/globalProps/GlobalPropsProvider.jsx +33 -0
- package/src/providers/globalProps/index.js +3 -0
- package/src/{provider → providers/globalProps}/withGlobalProps.jsx +16 -16
- package/src/providers/translations/TranslationsContext.jsx +6 -0
- package/src/providers/translations/TranslationsProvider.jsx +33 -0
- package/src/providers/translations/index.js +2 -0
- package/src/styles/elements/_links.scss +2 -9
- package/src/styles/generic/_focus.scss +1 -1
- package/src/styles/theme/_form-fields.scss +19 -0
- package/src/styles/theme/_links.scss +4 -3
- package/src/styles/tools/_accessibility.scss +3 -5
- package/src/styles/tools/_collections.scss +62 -5
- package/src/styles/tools/_links.scss +17 -0
- package/src/styles/tools/form-fields/_box-field-elements.scss +21 -9
- package/src/styles/tools/form-fields/_box-field-layout.scss +2 -2
- package/src/styles/tools/form-fields/_box-field-sizes.scss +6 -10
- package/src/styles/tools/form-fields/_foundation.scss +6 -4
- package/src/styles/tools/form-fields/_variants.scss +12 -8
- package/src/theme.scss +53 -2
- package/src/translations/en.js +5 -0
- package/src/provider/RUIContext.jsx +0 -9
- package/src/provider/RUIProvider.jsx +0 -42
- package/src/provider/index.js +0 -3
- package/src/styles/settings/_z-indexes.scss +0 -2
- package/src/utils/classNames.js +0 -8
- /package/src/{utils → helpers/transferProps}/transferProps.js +0 -0
@@ -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
|
+
};
|
@@ -2,9 +2,8 @@ import { useEffect } from 'react';
|
|
2
2
|
|
3
3
|
export const useModalFocus = (
|
4
4
|
autoFocus,
|
5
|
-
|
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,
|
14
|
+
// elements. In case there are none or `autoFocus` is disabled, dialogElement
|
16
15
|
// (Modal itself) is focused.
|
17
16
|
|
18
|
-
const
|
17
|
+
const dialogElement = dialogRef.current;
|
19
18
|
|
20
|
-
if (
|
19
|
+
if (dialogElement == null) {
|
21
20
|
return () => {};
|
22
21
|
}
|
23
22
|
|
24
23
|
const childrenFocusableElements = Array.from(
|
25
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
return;
|
100
|
-
}
|
35
|
+
const firstFormFieldEl = childrenFocusableElements.find(
|
36
|
+
(element) => ['INPUT', 'TEXTAREA', 'SELECT'].includes(element.nodeName) && !element.disabled,
|
37
|
+
);
|
101
38
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
)
|
107
|
-
) {
|
108
|
-
lastFocusableElement.focus();
|
109
|
-
e.preventDefault();
|
110
|
-
}
|
111
|
-
};
|
39
|
+
if (firstFormFieldEl) {
|
40
|
+
firstFormFieldEl.focus();
|
41
|
+
return () => {};
|
42
|
+
}
|
112
43
|
|
113
|
-
|
44
|
+
if (primaryButtonRef?.current != null && primaryButtonRef?.current?.disabled === false) {
|
45
|
+
primaryButtonRef.current.focus();
|
46
|
+
return () => {};
|
47
|
+
}
|
114
48
|
|
115
|
-
|
49
|
+
firstFocusableElement.focus();
|
116
50
|
|
117
|
-
return () =>
|
51
|
+
return () => {};
|
118
52
|
},
|
119
53
|
[
|
120
54
|
autoFocus,
|
121
|
-
|
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/
|
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 '../../
|
4
|
-
import { classNames } from '../../
|
5
|
-
import { transferProps } from '../../
|
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 {
|
5
|
-
import { classNames } from '../../
|
6
|
-
import {
|
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
|
-
|
21
|
-
{
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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.
|
2
|
-
//
|
3
|
-
//
|
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; //
|
243
|
+
inset: unset; // 4.
|
216
244
|
}
|
217
245
|
|
218
246
|
.isRootControlled.isRootAtTop {
|
219
|
-
transform: translate(0, calc(-1 * #{theme.$arrow-height})); //
|
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}); //
|
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); //
|
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); //
|
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; //
|
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 '../../
|
4
|
-
import { transferProps } from '../../
|
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
|
-
|
293
|
+
placementStyle={{
|
271
294
|
position: strategy,
|
272
|
-
top: y
|
273
|
-
left: 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
|