@sproutsocial/seeds-react-popout 1.0.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/dist/index.d.mts +100 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.js +287 -0
- package/dist/index.mjs +250 -0
- package/package.json +40 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { TypeBoxProps } from '@sproutsocial/seeds-react-box';
|
|
3
|
+
import { PopperProps } from 'react-popper';
|
|
4
|
+
import { TypeStyledComponentsCommonProps, TypeSystemCommonProps, TypeSystemLayoutProps } from '@sproutsocial/seeds-react-system-props';
|
|
5
|
+
|
|
6
|
+
type EnumPlacements = "auto" | "top" | "right" | "bottom" | "left" | "top-start" | "right-start" | "bottom-start" | "left-start" | "top-end" | "right-end" | "bottom-end" | "left-end";
|
|
7
|
+
interface TypeFocusLockProps {
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
returnFocus?: boolean | {
|
|
10
|
+
preventScroll?: boolean;
|
|
11
|
+
};
|
|
12
|
+
persistentFocus?: boolean;
|
|
13
|
+
autoFocus?: boolean;
|
|
14
|
+
noFocusGuards?: boolean | "tail";
|
|
15
|
+
group?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
onActivation?: (node: HTMLElement) => void;
|
|
18
|
+
onDeactivation?: (node: HTMLElement) => void;
|
|
19
|
+
as?: TypeStyledComponentsCommonProps["as"];
|
|
20
|
+
lockProps?: any;
|
|
21
|
+
ref?: any;
|
|
22
|
+
whiteList?: (activeElement: HTMLElement) => boolean;
|
|
23
|
+
shards?: Array<any>;
|
|
24
|
+
children?: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
interface TypePopoutProps extends TypeStyledComponentsCommonProps, TypeSystemCommonProps, TypeSystemLayoutProps, Omit<React.ComponentPropsWithoutRef<"div">, "color" | "children" | "content"> {
|
|
27
|
+
/**
|
|
28
|
+
* Whether Popout should automatically add aria-attributes to its wrapped elements
|
|
29
|
+
*/
|
|
30
|
+
disableWrapperAria?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Is the popout open? This prop is optional. By using it, you will lose some automatic handling of the logic for opening and closing the popout.
|
|
33
|
+
*/
|
|
34
|
+
isOpen?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* If you use the isOpen prop you will need to pass a handler to change the state of the popout
|
|
37
|
+
*/
|
|
38
|
+
setIsOpen?: (isOpen: boolean, event?: MouseEvent | KeyboardEvent) => void;
|
|
39
|
+
/**
|
|
40
|
+
* The content to be shown in the popout. If there is no content, just the children are rendered
|
|
41
|
+
*/
|
|
42
|
+
content: (React.ReactElement<any> | null | undefined) | (((opts: {
|
|
43
|
+
hide: () => void;
|
|
44
|
+
}) => React.ReactElement<any>) | null | undefined);
|
|
45
|
+
/**
|
|
46
|
+
* The content that the popout should be attached to
|
|
47
|
+
*/
|
|
48
|
+
children: React.ReactElement<any> | ((opts: {
|
|
49
|
+
ref: (arg0: React.ElementRef<any> | HTMLElement) => void;
|
|
50
|
+
toggle: () => void;
|
|
51
|
+
show: () => void;
|
|
52
|
+
hide: () => void;
|
|
53
|
+
ariaProps: Record<string, any>;
|
|
54
|
+
}) => React.ReactNode);
|
|
55
|
+
/** How the popped-out content should be placed, in relationship to the children */
|
|
56
|
+
placement?: EnumPlacements;
|
|
57
|
+
/**
|
|
58
|
+
* Should the popped-out content inherit its width from the children?
|
|
59
|
+
* This is useful for typeaheads and other places where a popout needs to match the width of a given element.
|
|
60
|
+
*/
|
|
61
|
+
fullWidth?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* When the popout has opened, should we focus on its content?
|
|
64
|
+
* Focus will be brought inside by looking for elements with [autofocus] first, [tabindex] second and buttons/links/other natively-focusable elements last.
|
|
65
|
+
* Upon closing the popout, focus will be returned to the popout's target.
|
|
66
|
+
* If nothing within your popout's content is focusable, this prop will do nothing.
|
|
67
|
+
*/
|
|
68
|
+
focusOnContent?: boolean;
|
|
69
|
+
qa?: Record<string, string>;
|
|
70
|
+
zIndex?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Override react-popper per the API documentation here: https://github.com/FezVrasta/react-popper#api-documentation
|
|
73
|
+
*/
|
|
74
|
+
popperProps?: Partial<PopperProps>;
|
|
75
|
+
onClose?: () => void;
|
|
76
|
+
onOpen?: () => void;
|
|
77
|
+
/**
|
|
78
|
+
* And optional callback to receive the scheduleUpdate function.
|
|
79
|
+
* Use the function to recalculate the popout position in relation to the contents.
|
|
80
|
+
*/
|
|
81
|
+
scheduleUpdateRef?: (arg0: () => void) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Mount the popout at the bottom of the DOM (using React.Portal) instead of inside the current DOM tree
|
|
84
|
+
*/
|
|
85
|
+
appendToBody?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Override `FocusLock` props, see the API documentation for more details: https://github.com/theKashey/react-focus-lock#api
|
|
88
|
+
*/
|
|
89
|
+
focusLockProps?: TypeFocusLockProps;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
declare function Popout({ isOpen, setIsOpen, content, children, placement, fullWidth, zIndex, focusOnContent, onOpen, onClose, qa, popperProps, scheduleUpdateRef, appendToBody, focusLockProps, color, "aria-haspopup": ariaHasPopup, disableWrapperAria, id, ...rest }: TypePopoutProps): React.JSX.Element;
|
|
93
|
+
declare namespace Popout {
|
|
94
|
+
var Content: {
|
|
95
|
+
({ children, ...rest }: TypeBoxProps): React.JSX.Element;
|
|
96
|
+
displayName: string;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { type EnumPlacements, Popout, type TypeFocusLockProps, type TypePopoutProps, Popout as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { TypeBoxProps } from '@sproutsocial/seeds-react-box';
|
|
3
|
+
import { PopperProps } from 'react-popper';
|
|
4
|
+
import { TypeStyledComponentsCommonProps, TypeSystemCommonProps, TypeSystemLayoutProps } from '@sproutsocial/seeds-react-system-props';
|
|
5
|
+
|
|
6
|
+
type EnumPlacements = "auto" | "top" | "right" | "bottom" | "left" | "top-start" | "right-start" | "bottom-start" | "left-start" | "top-end" | "right-end" | "bottom-end" | "left-end";
|
|
7
|
+
interface TypeFocusLockProps {
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
returnFocus?: boolean | {
|
|
10
|
+
preventScroll?: boolean;
|
|
11
|
+
};
|
|
12
|
+
persistentFocus?: boolean;
|
|
13
|
+
autoFocus?: boolean;
|
|
14
|
+
noFocusGuards?: boolean | "tail";
|
|
15
|
+
group?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
onActivation?: (node: HTMLElement) => void;
|
|
18
|
+
onDeactivation?: (node: HTMLElement) => void;
|
|
19
|
+
as?: TypeStyledComponentsCommonProps["as"];
|
|
20
|
+
lockProps?: any;
|
|
21
|
+
ref?: any;
|
|
22
|
+
whiteList?: (activeElement: HTMLElement) => boolean;
|
|
23
|
+
shards?: Array<any>;
|
|
24
|
+
children?: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
interface TypePopoutProps extends TypeStyledComponentsCommonProps, TypeSystemCommonProps, TypeSystemLayoutProps, Omit<React.ComponentPropsWithoutRef<"div">, "color" | "children" | "content"> {
|
|
27
|
+
/**
|
|
28
|
+
* Whether Popout should automatically add aria-attributes to its wrapped elements
|
|
29
|
+
*/
|
|
30
|
+
disableWrapperAria?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Is the popout open? This prop is optional. By using it, you will lose some automatic handling of the logic for opening and closing the popout.
|
|
33
|
+
*/
|
|
34
|
+
isOpen?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* If you use the isOpen prop you will need to pass a handler to change the state of the popout
|
|
37
|
+
*/
|
|
38
|
+
setIsOpen?: (isOpen: boolean, event?: MouseEvent | KeyboardEvent) => void;
|
|
39
|
+
/**
|
|
40
|
+
* The content to be shown in the popout. If there is no content, just the children are rendered
|
|
41
|
+
*/
|
|
42
|
+
content: (React.ReactElement<any> | null | undefined) | (((opts: {
|
|
43
|
+
hide: () => void;
|
|
44
|
+
}) => React.ReactElement<any>) | null | undefined);
|
|
45
|
+
/**
|
|
46
|
+
* The content that the popout should be attached to
|
|
47
|
+
*/
|
|
48
|
+
children: React.ReactElement<any> | ((opts: {
|
|
49
|
+
ref: (arg0: React.ElementRef<any> | HTMLElement) => void;
|
|
50
|
+
toggle: () => void;
|
|
51
|
+
show: () => void;
|
|
52
|
+
hide: () => void;
|
|
53
|
+
ariaProps: Record<string, any>;
|
|
54
|
+
}) => React.ReactNode);
|
|
55
|
+
/** How the popped-out content should be placed, in relationship to the children */
|
|
56
|
+
placement?: EnumPlacements;
|
|
57
|
+
/**
|
|
58
|
+
* Should the popped-out content inherit its width from the children?
|
|
59
|
+
* This is useful for typeaheads and other places where a popout needs to match the width of a given element.
|
|
60
|
+
*/
|
|
61
|
+
fullWidth?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* When the popout has opened, should we focus on its content?
|
|
64
|
+
* Focus will be brought inside by looking for elements with [autofocus] first, [tabindex] second and buttons/links/other natively-focusable elements last.
|
|
65
|
+
* Upon closing the popout, focus will be returned to the popout's target.
|
|
66
|
+
* If nothing within your popout's content is focusable, this prop will do nothing.
|
|
67
|
+
*/
|
|
68
|
+
focusOnContent?: boolean;
|
|
69
|
+
qa?: Record<string, string>;
|
|
70
|
+
zIndex?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Override react-popper per the API documentation here: https://github.com/FezVrasta/react-popper#api-documentation
|
|
73
|
+
*/
|
|
74
|
+
popperProps?: Partial<PopperProps>;
|
|
75
|
+
onClose?: () => void;
|
|
76
|
+
onOpen?: () => void;
|
|
77
|
+
/**
|
|
78
|
+
* And optional callback to receive the scheduleUpdate function.
|
|
79
|
+
* Use the function to recalculate the popout position in relation to the contents.
|
|
80
|
+
*/
|
|
81
|
+
scheduleUpdateRef?: (arg0: () => void) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Mount the popout at the bottom of the DOM (using React.Portal) instead of inside the current DOM tree
|
|
84
|
+
*/
|
|
85
|
+
appendToBody?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Override `FocusLock` props, see the API documentation for more details: https://github.com/theKashey/react-focus-lock#api
|
|
88
|
+
*/
|
|
89
|
+
focusLockProps?: TypeFocusLockProps;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
declare function Popout({ isOpen, setIsOpen, content, children, placement, fullWidth, zIndex, focusOnContent, onOpen, onClose, qa, popperProps, scheduleUpdateRef, appendToBody, focusLockProps, color, "aria-haspopup": ariaHasPopup, disableWrapperAria, id, ...rest }: TypePopoutProps): React.JSX.Element;
|
|
93
|
+
declare namespace Popout {
|
|
94
|
+
var Content: {
|
|
95
|
+
({ children, ...rest }: TypeBoxProps): React.JSX.Element;
|
|
96
|
+
displayName: string;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { type EnumPlacements, Popout, type TypeFocusLockProps, type TypePopoutProps, Popout as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
Popout: () => Popout_default,
|
|
34
|
+
default: () => src_default
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(src_exports);
|
|
37
|
+
|
|
38
|
+
// src/Popout.tsx
|
|
39
|
+
var React = __toESM(require("react"));
|
|
40
|
+
var import_react = require("react");
|
|
41
|
+
var import_react_spring = require("react-spring");
|
|
42
|
+
var import_react_focus_lock = __toESM(require("react-focus-lock"));
|
|
43
|
+
var import_react_popper = require("react-popper");
|
|
44
|
+
var import_seeds_motion_unitless = require("@sproutsocial/seeds-motion/dist/seeds-motion-unitless");
|
|
45
|
+
var import_seeds_react_hooks = require("@sproutsocial/seeds-react-hooks");
|
|
46
|
+
var import_seeds_react_portal = __toESM(require("@sproutsocial/seeds-react-portal"));
|
|
47
|
+
var import_seeds_react_box = __toESM(require("@sproutsocial/seeds-react-box"));
|
|
48
|
+
|
|
49
|
+
// src/styles.ts
|
|
50
|
+
var import_styled_components = __toESM(require("styled-components"));
|
|
51
|
+
var import_seeds_react_system_props = require("@sproutsocial/seeds-react-system-props");
|
|
52
|
+
var TargetWrapper = import_styled_components.default.div`
|
|
53
|
+
display: inline-block;
|
|
54
|
+
${import_seeds_react_system_props.COMMON}
|
|
55
|
+
${import_seeds_react_system_props.LAYOUT}
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
// src/Popout.tsx
|
|
59
|
+
var doesRefContainEventTarget = (ref, event) => {
|
|
60
|
+
return ref.current && event.target instanceof Node && ref.current.contains(event.target);
|
|
61
|
+
};
|
|
62
|
+
var transitionConfig = {
|
|
63
|
+
from: {
|
|
64
|
+
opacity: 0
|
|
65
|
+
},
|
|
66
|
+
enter: {
|
|
67
|
+
opacity: 1
|
|
68
|
+
},
|
|
69
|
+
leave: {
|
|
70
|
+
opacity: 0
|
|
71
|
+
},
|
|
72
|
+
config: {
|
|
73
|
+
duration: import_seeds_motion_unitless.MOTION_DURATION_FAST * 1e3
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
function Popout({
|
|
77
|
+
isOpen,
|
|
78
|
+
setIsOpen,
|
|
79
|
+
content,
|
|
80
|
+
children,
|
|
81
|
+
placement = "auto",
|
|
82
|
+
fullWidth = false,
|
|
83
|
+
zIndex = 7,
|
|
84
|
+
focusOnContent = true,
|
|
85
|
+
onOpen,
|
|
86
|
+
onClose,
|
|
87
|
+
qa = {},
|
|
88
|
+
popperProps,
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
90
|
+
scheduleUpdateRef = () => {
|
|
91
|
+
},
|
|
92
|
+
appendToBody = true,
|
|
93
|
+
focusLockProps = {},
|
|
94
|
+
color,
|
|
95
|
+
"aria-haspopup": ariaHasPopup,
|
|
96
|
+
disableWrapperAria = false,
|
|
97
|
+
id,
|
|
98
|
+
...rest
|
|
99
|
+
}) {
|
|
100
|
+
const PopoutComponentWrapper = appendToBody ? import_seeds_react_portal.default : React.Fragment;
|
|
101
|
+
const [isInternalShown, setIsInternalShown] = (0, import_react.useState)(false);
|
|
102
|
+
const isControlled = typeof isOpen === "boolean";
|
|
103
|
+
const isShown = isControlled ? isOpen : isInternalShown;
|
|
104
|
+
const setIsShown = (0, import_react.useMemo)(
|
|
105
|
+
() => isControlled && setIsOpen ? setIsOpen : (nextIsShown) => setIsInternalShown(nextIsShown),
|
|
106
|
+
[isControlled, setIsOpen]
|
|
107
|
+
);
|
|
108
|
+
const targetRef = (0, import_react.useRef)();
|
|
109
|
+
const popoutRef = (0, import_react.useRef)();
|
|
110
|
+
const isFirstRender = (0, import_react.useRef)(true);
|
|
111
|
+
const scheduleUpdateCallback = (0, import_react.useRef)(() => {
|
|
112
|
+
});
|
|
113
|
+
(0, import_seeds_react_hooks.useMutationObserver)(
|
|
114
|
+
popoutRef.current ?? null,
|
|
115
|
+
{
|
|
116
|
+
childList: true,
|
|
117
|
+
characterData: true,
|
|
118
|
+
subtree: true
|
|
119
|
+
},
|
|
120
|
+
scheduleUpdateCallback.current
|
|
121
|
+
);
|
|
122
|
+
const {
|
|
123
|
+
autoFocus = true,
|
|
124
|
+
returnFocus = true,
|
|
125
|
+
...restFocusLockProps
|
|
126
|
+
} = focusLockProps;
|
|
127
|
+
const isInvalidContent = content === null || content === void 0;
|
|
128
|
+
const show = (0, import_react.useCallback)(() => setIsShown(true), [setIsShown]);
|
|
129
|
+
const hide = (0, import_react.useCallback)(() => setIsShown(false), [setIsShown]);
|
|
130
|
+
const toggle = (0, import_react.useCallback)(() => setIsShown(!isShown), [isShown, setIsShown]);
|
|
131
|
+
(0, import_react.useEffect)(() => {
|
|
132
|
+
const documentBody = document.body;
|
|
133
|
+
if (isShown && documentBody) {
|
|
134
|
+
const bodyClick = (e) => {
|
|
135
|
+
if (doesRefContainEventTarget(targetRef, e) || doesRefContainEventTarget(popoutRef, e)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
setIsShown(false, e);
|
|
139
|
+
};
|
|
140
|
+
const onEsc = (e) => {
|
|
141
|
+
if (["Escape", "Esc"].includes(e.key)) {
|
|
142
|
+
e.stopPropagation();
|
|
143
|
+
setIsShown(false, e);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
documentBody.addEventListener("click", bodyClick, false);
|
|
147
|
+
documentBody.addEventListener("keydown", onEsc, false);
|
|
148
|
+
return () => {
|
|
149
|
+
documentBody.removeEventListener("click", bodyClick, false);
|
|
150
|
+
documentBody.removeEventListener("keydown", onEsc, false);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}, [isShown, setIsShown]);
|
|
154
|
+
(0, import_react.useEffect)(
|
|
155
|
+
() => {
|
|
156
|
+
if (isFirstRender.current) {
|
|
157
|
+
isFirstRender.current = false;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!onOpen && !onClose) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (isShown && onOpen) {
|
|
164
|
+
onOpen();
|
|
165
|
+
}
|
|
166
|
+
if (!isShown && onClose) {
|
|
167
|
+
onClose();
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
// eslint-disable-next-line
|
|
171
|
+
[isShown]
|
|
172
|
+
);
|
|
173
|
+
const transitions = (0, import_react_spring.useTransition)(isShown, null, transitionConfig);
|
|
174
|
+
const ariaProps = (0, import_react.useMemo)(
|
|
175
|
+
() => disableWrapperAria ? {} : {
|
|
176
|
+
"aria-expanded": isShown,
|
|
177
|
+
"aria-haspopup": ariaHasPopup ? ariaHasPopup : true
|
|
178
|
+
},
|
|
179
|
+
[isShown, ariaHasPopup, disableWrapperAria]
|
|
180
|
+
);
|
|
181
|
+
const [shouldRenderPopout, setShouldRenderPopout] = (0, import_react.useState)(
|
|
182
|
+
// Only trigger this shouldRenderPopout logic when using a controlled component.
|
|
183
|
+
// The reason for that is because controlled components may render the popout
|
|
184
|
+
// immediately before the targetRef has a value set to it.
|
|
185
|
+
!isControlled
|
|
186
|
+
);
|
|
187
|
+
const childrenRef = (el) => {
|
|
188
|
+
targetRef.current = el;
|
|
189
|
+
if (targetRef.current) {
|
|
190
|
+
setShouldRenderPopout(true);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
return /* @__PURE__ */ React.createElement(React.Fragment, null, typeof children === "function" ? children({
|
|
194
|
+
ref: childrenRef,
|
|
195
|
+
toggle,
|
|
196
|
+
show,
|
|
197
|
+
hide,
|
|
198
|
+
ariaProps
|
|
199
|
+
}) : /* @__PURE__ */ React.createElement(TargetWrapper, { ...qa, id, ...rest, ref: childrenRef }, React.cloneElement(children, {
|
|
200
|
+
...ariaProps,
|
|
201
|
+
...!isControlled ? {
|
|
202
|
+
onClick: toggle
|
|
203
|
+
} : void 0
|
|
204
|
+
})), shouldRenderPopout && !isInvalidContent && transitions.map(
|
|
205
|
+
({ item, key, props }) => item && /* @__PURE__ */ React.createElement(PopoutComponentWrapper, { key }, /* @__PURE__ */ React.createElement(
|
|
206
|
+
import_react_popper.Popper,
|
|
207
|
+
{
|
|
208
|
+
referenceElement: targetRef.current,
|
|
209
|
+
placement,
|
|
210
|
+
modifiers: {
|
|
211
|
+
preventOverflow: {
|
|
212
|
+
boundariesElement: "viewport"
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
...popperProps
|
|
216
|
+
},
|
|
217
|
+
({
|
|
218
|
+
ref,
|
|
219
|
+
style,
|
|
220
|
+
placement: placement2,
|
|
221
|
+
outOfBoundaries,
|
|
222
|
+
scheduleUpdate
|
|
223
|
+
}) => {
|
|
224
|
+
const interceptRef = (el) => {
|
|
225
|
+
popoutRef.current = el;
|
|
226
|
+
ref(el);
|
|
227
|
+
};
|
|
228
|
+
scheduleUpdateCallback.current = scheduleUpdate;
|
|
229
|
+
scheduleUpdateRef(scheduleUpdate);
|
|
230
|
+
return /* @__PURE__ */ React.createElement(
|
|
231
|
+
"div",
|
|
232
|
+
{
|
|
233
|
+
ref: interceptRef,
|
|
234
|
+
style: {
|
|
235
|
+
...style,
|
|
236
|
+
zIndex,
|
|
237
|
+
width: fullWidth && targetRef.current ? targetRef.current.offsetWidth : "initial"
|
|
238
|
+
},
|
|
239
|
+
"data-placement": placement2,
|
|
240
|
+
"data-qa-popout": "",
|
|
241
|
+
"data-qa-popout-isopen": isOpen === true,
|
|
242
|
+
color,
|
|
243
|
+
...rest
|
|
244
|
+
},
|
|
245
|
+
item && !outOfBoundaries && /* @__PURE__ */ React.createElement(import_react_spring.animated.div, { style: props }, /* @__PURE__ */ React.createElement(
|
|
246
|
+
import_react_focus_lock.default,
|
|
247
|
+
{
|
|
248
|
+
autoFocus,
|
|
249
|
+
returnFocus,
|
|
250
|
+
disabled: !focusOnContent,
|
|
251
|
+
...restFocusLockProps
|
|
252
|
+
},
|
|
253
|
+
typeof content === "function" && content({
|
|
254
|
+
hide
|
|
255
|
+
}),
|
|
256
|
+
typeof content !== "function" && content
|
|
257
|
+
))
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
))
|
|
261
|
+
));
|
|
262
|
+
}
|
|
263
|
+
var PopoutContent = ({ children, ...rest }) => /* @__PURE__ */ React.createElement(
|
|
264
|
+
import_seeds_react_box.default,
|
|
265
|
+
{
|
|
266
|
+
bg: "container.background.base",
|
|
267
|
+
color: "text.body",
|
|
268
|
+
border: 500,
|
|
269
|
+
borderColor: "container.border.base",
|
|
270
|
+
borderRadius: "outer",
|
|
271
|
+
boxShadow: "medium",
|
|
272
|
+
p: 400,
|
|
273
|
+
m: 300,
|
|
274
|
+
...rest
|
|
275
|
+
},
|
|
276
|
+
children
|
|
277
|
+
);
|
|
278
|
+
PopoutContent.displayName = "Popout.Content";
|
|
279
|
+
Popout.Content = PopoutContent;
|
|
280
|
+
var Popout_default = Popout;
|
|
281
|
+
|
|
282
|
+
// src/index.ts
|
|
283
|
+
var src_default = Popout_default;
|
|
284
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
285
|
+
0 && (module.exports = {
|
|
286
|
+
Popout
|
|
287
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// src/Popout.tsx
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
|
4
|
+
import { useTransition, animated } from "react-spring";
|
|
5
|
+
import FocusLock from "react-focus-lock";
|
|
6
|
+
import { Popper } from "react-popper";
|
|
7
|
+
import { MOTION_DURATION_FAST } from "@sproutsocial/seeds-motion/dist/seeds-motion-unitless";
|
|
8
|
+
import { useMutationObserver } from "@sproutsocial/seeds-react-hooks";
|
|
9
|
+
import Portal from "@sproutsocial/seeds-react-portal";
|
|
10
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
11
|
+
|
|
12
|
+
// src/styles.ts
|
|
13
|
+
import styled from "styled-components";
|
|
14
|
+
import { COMMON, LAYOUT } from "@sproutsocial/seeds-react-system-props";
|
|
15
|
+
var TargetWrapper = styled.div`
|
|
16
|
+
display: inline-block;
|
|
17
|
+
${COMMON}
|
|
18
|
+
${LAYOUT}
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
// src/Popout.tsx
|
|
22
|
+
var doesRefContainEventTarget = (ref, event) => {
|
|
23
|
+
return ref.current && event.target instanceof Node && ref.current.contains(event.target);
|
|
24
|
+
};
|
|
25
|
+
var transitionConfig = {
|
|
26
|
+
from: {
|
|
27
|
+
opacity: 0
|
|
28
|
+
},
|
|
29
|
+
enter: {
|
|
30
|
+
opacity: 1
|
|
31
|
+
},
|
|
32
|
+
leave: {
|
|
33
|
+
opacity: 0
|
|
34
|
+
},
|
|
35
|
+
config: {
|
|
36
|
+
duration: MOTION_DURATION_FAST * 1e3
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
function Popout({
|
|
40
|
+
isOpen,
|
|
41
|
+
setIsOpen,
|
|
42
|
+
content,
|
|
43
|
+
children,
|
|
44
|
+
placement = "auto",
|
|
45
|
+
fullWidth = false,
|
|
46
|
+
zIndex = 7,
|
|
47
|
+
focusOnContent = true,
|
|
48
|
+
onOpen,
|
|
49
|
+
onClose,
|
|
50
|
+
qa = {},
|
|
51
|
+
popperProps,
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
53
|
+
scheduleUpdateRef = () => {
|
|
54
|
+
},
|
|
55
|
+
appendToBody = true,
|
|
56
|
+
focusLockProps = {},
|
|
57
|
+
color,
|
|
58
|
+
"aria-haspopup": ariaHasPopup,
|
|
59
|
+
disableWrapperAria = false,
|
|
60
|
+
id,
|
|
61
|
+
...rest
|
|
62
|
+
}) {
|
|
63
|
+
const PopoutComponentWrapper = appendToBody ? Portal : React.Fragment;
|
|
64
|
+
const [isInternalShown, setIsInternalShown] = useState(false);
|
|
65
|
+
const isControlled = typeof isOpen === "boolean";
|
|
66
|
+
const isShown = isControlled ? isOpen : isInternalShown;
|
|
67
|
+
const setIsShown = useMemo(
|
|
68
|
+
() => isControlled && setIsOpen ? setIsOpen : (nextIsShown) => setIsInternalShown(nextIsShown),
|
|
69
|
+
[isControlled, setIsOpen]
|
|
70
|
+
);
|
|
71
|
+
const targetRef = useRef();
|
|
72
|
+
const popoutRef = useRef();
|
|
73
|
+
const isFirstRender = useRef(true);
|
|
74
|
+
const scheduleUpdateCallback = useRef(() => {
|
|
75
|
+
});
|
|
76
|
+
useMutationObserver(
|
|
77
|
+
popoutRef.current ?? null,
|
|
78
|
+
{
|
|
79
|
+
childList: true,
|
|
80
|
+
characterData: true,
|
|
81
|
+
subtree: true
|
|
82
|
+
},
|
|
83
|
+
scheduleUpdateCallback.current
|
|
84
|
+
);
|
|
85
|
+
const {
|
|
86
|
+
autoFocus = true,
|
|
87
|
+
returnFocus = true,
|
|
88
|
+
...restFocusLockProps
|
|
89
|
+
} = focusLockProps;
|
|
90
|
+
const isInvalidContent = content === null || content === void 0;
|
|
91
|
+
const show = useCallback(() => setIsShown(true), [setIsShown]);
|
|
92
|
+
const hide = useCallback(() => setIsShown(false), [setIsShown]);
|
|
93
|
+
const toggle = useCallback(() => setIsShown(!isShown), [isShown, setIsShown]);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const documentBody = document.body;
|
|
96
|
+
if (isShown && documentBody) {
|
|
97
|
+
const bodyClick = (e) => {
|
|
98
|
+
if (doesRefContainEventTarget(targetRef, e) || doesRefContainEventTarget(popoutRef, e)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
setIsShown(false, e);
|
|
102
|
+
};
|
|
103
|
+
const onEsc = (e) => {
|
|
104
|
+
if (["Escape", "Esc"].includes(e.key)) {
|
|
105
|
+
e.stopPropagation();
|
|
106
|
+
setIsShown(false, e);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
documentBody.addEventListener("click", bodyClick, false);
|
|
110
|
+
documentBody.addEventListener("keydown", onEsc, false);
|
|
111
|
+
return () => {
|
|
112
|
+
documentBody.removeEventListener("click", bodyClick, false);
|
|
113
|
+
documentBody.removeEventListener("keydown", onEsc, false);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}, [isShown, setIsShown]);
|
|
117
|
+
useEffect(
|
|
118
|
+
() => {
|
|
119
|
+
if (isFirstRender.current) {
|
|
120
|
+
isFirstRender.current = false;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!onOpen && !onClose) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (isShown && onOpen) {
|
|
127
|
+
onOpen();
|
|
128
|
+
}
|
|
129
|
+
if (!isShown && onClose) {
|
|
130
|
+
onClose();
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
// eslint-disable-next-line
|
|
134
|
+
[isShown]
|
|
135
|
+
);
|
|
136
|
+
const transitions = useTransition(isShown, null, transitionConfig);
|
|
137
|
+
const ariaProps = useMemo(
|
|
138
|
+
() => disableWrapperAria ? {} : {
|
|
139
|
+
"aria-expanded": isShown,
|
|
140
|
+
"aria-haspopup": ariaHasPopup ? ariaHasPopup : true
|
|
141
|
+
},
|
|
142
|
+
[isShown, ariaHasPopup, disableWrapperAria]
|
|
143
|
+
);
|
|
144
|
+
const [shouldRenderPopout, setShouldRenderPopout] = useState(
|
|
145
|
+
// Only trigger this shouldRenderPopout logic when using a controlled component.
|
|
146
|
+
// The reason for that is because controlled components may render the popout
|
|
147
|
+
// immediately before the targetRef has a value set to it.
|
|
148
|
+
!isControlled
|
|
149
|
+
);
|
|
150
|
+
const childrenRef = (el) => {
|
|
151
|
+
targetRef.current = el;
|
|
152
|
+
if (targetRef.current) {
|
|
153
|
+
setShouldRenderPopout(true);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
return /* @__PURE__ */ React.createElement(React.Fragment, null, typeof children === "function" ? children({
|
|
157
|
+
ref: childrenRef,
|
|
158
|
+
toggle,
|
|
159
|
+
show,
|
|
160
|
+
hide,
|
|
161
|
+
ariaProps
|
|
162
|
+
}) : /* @__PURE__ */ React.createElement(TargetWrapper, { ...qa, id, ...rest, ref: childrenRef }, React.cloneElement(children, {
|
|
163
|
+
...ariaProps,
|
|
164
|
+
...!isControlled ? {
|
|
165
|
+
onClick: toggle
|
|
166
|
+
} : void 0
|
|
167
|
+
})), shouldRenderPopout && !isInvalidContent && transitions.map(
|
|
168
|
+
({ item, key, props }) => item && /* @__PURE__ */ React.createElement(PopoutComponentWrapper, { key }, /* @__PURE__ */ React.createElement(
|
|
169
|
+
Popper,
|
|
170
|
+
{
|
|
171
|
+
referenceElement: targetRef.current,
|
|
172
|
+
placement,
|
|
173
|
+
modifiers: {
|
|
174
|
+
preventOverflow: {
|
|
175
|
+
boundariesElement: "viewport"
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
...popperProps
|
|
179
|
+
},
|
|
180
|
+
({
|
|
181
|
+
ref,
|
|
182
|
+
style,
|
|
183
|
+
placement: placement2,
|
|
184
|
+
outOfBoundaries,
|
|
185
|
+
scheduleUpdate
|
|
186
|
+
}) => {
|
|
187
|
+
const interceptRef = (el) => {
|
|
188
|
+
popoutRef.current = el;
|
|
189
|
+
ref(el);
|
|
190
|
+
};
|
|
191
|
+
scheduleUpdateCallback.current = scheduleUpdate;
|
|
192
|
+
scheduleUpdateRef(scheduleUpdate);
|
|
193
|
+
return /* @__PURE__ */ React.createElement(
|
|
194
|
+
"div",
|
|
195
|
+
{
|
|
196
|
+
ref: interceptRef,
|
|
197
|
+
style: {
|
|
198
|
+
...style,
|
|
199
|
+
zIndex,
|
|
200
|
+
width: fullWidth && targetRef.current ? targetRef.current.offsetWidth : "initial"
|
|
201
|
+
},
|
|
202
|
+
"data-placement": placement2,
|
|
203
|
+
"data-qa-popout": "",
|
|
204
|
+
"data-qa-popout-isopen": isOpen === true,
|
|
205
|
+
color,
|
|
206
|
+
...rest
|
|
207
|
+
},
|
|
208
|
+
item && !outOfBoundaries && /* @__PURE__ */ React.createElement(animated.div, { style: props }, /* @__PURE__ */ React.createElement(
|
|
209
|
+
FocusLock,
|
|
210
|
+
{
|
|
211
|
+
autoFocus,
|
|
212
|
+
returnFocus,
|
|
213
|
+
disabled: !focusOnContent,
|
|
214
|
+
...restFocusLockProps
|
|
215
|
+
},
|
|
216
|
+
typeof content === "function" && content({
|
|
217
|
+
hide
|
|
218
|
+
}),
|
|
219
|
+
typeof content !== "function" && content
|
|
220
|
+
))
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
))
|
|
224
|
+
));
|
|
225
|
+
}
|
|
226
|
+
var PopoutContent = ({ children, ...rest }) => /* @__PURE__ */ React.createElement(
|
|
227
|
+
Box,
|
|
228
|
+
{
|
|
229
|
+
bg: "container.background.base",
|
|
230
|
+
color: "text.body",
|
|
231
|
+
border: 500,
|
|
232
|
+
borderColor: "container.border.base",
|
|
233
|
+
borderRadius: "outer",
|
|
234
|
+
boxShadow: "medium",
|
|
235
|
+
p: 400,
|
|
236
|
+
m: 300,
|
|
237
|
+
...rest
|
|
238
|
+
},
|
|
239
|
+
children
|
|
240
|
+
);
|
|
241
|
+
PopoutContent.displayName = "Popout.Content";
|
|
242
|
+
Popout.Content = PopoutContent;
|
|
243
|
+
var Popout_default = Popout;
|
|
244
|
+
|
|
245
|
+
// src/index.ts
|
|
246
|
+
var src_default = Popout_default;
|
|
247
|
+
export {
|
|
248
|
+
Popout_default as Popout,
|
|
249
|
+
src_default as default
|
|
250
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sproutsocial/seeds-react-popout",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Seeds React Popout",
|
|
5
|
+
"author": "Sprout Social, Inc.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@sproutsocial/seeds-motion": "^1.6.0",
|
|
17
|
+
"@sproutsocial/seeds-react-box": "^1.0.0",
|
|
18
|
+
"@sproutsocial/seeds-react-button": "^1.0.0",
|
|
19
|
+
"@sproutsocial/seeds-react-hooks": "^2.0.1",
|
|
20
|
+
"@sproutsocial/seeds-react-portal": "*",
|
|
21
|
+
"@sproutsocial/seeds-react-system-props": "^2.1.0",
|
|
22
|
+
"react-focus-lock": "^2.0.3",
|
|
23
|
+
"react-popper": "^1.3.11",
|
|
24
|
+
"react-spring": "^8.0.25"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/react": "^17.0.0",
|
|
28
|
+
"@types/styled-components": "^5.1.26",
|
|
29
|
+
"@sproutsocial/eslint-config-seeds": "*",
|
|
30
|
+
"react": "^17.0.2",
|
|
31
|
+
"styled-components": "^5.2.3",
|
|
32
|
+
"typescript": "^5.1.6"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"styled-components": "^5.2.3"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
}
|
|
40
|
+
}
|