diff --git a/.changeset/shaky-eagles-rescue.md b/.changeset/shaky-eagles-rescue.md new file mode 100644 index 00000000..ccc614d2 --- /dev/null +++ b/.changeset/shaky-eagles-rescue.md @@ -0,0 +1,5 @@ +--- +"reshaped": minor +--- + +Modal/Overlay: Adding the contained prop to control whether the component should be contained within the container element. diff --git a/packages/reshaped/src/components/Modal/Modal.tsx b/packages/reshaped/src/components/Modal/Modal.tsx index bb7ceecc..70e9a866 100644 --- a/packages/reshaped/src/components/Modal/Modal.tsx +++ b/packages/reshaped/src/components/Modal/Modal.tsx @@ -80,6 +80,7 @@ const Modal: React.FC = (props) => { disableSwipeGesture, disableCloseOnOutsideClick, containerRef, + contained, overlayClassName, className, attributes, @@ -99,6 +100,7 @@ const Modal: React.FC = (props) => { const [dragDistance, setDragDistance] = React.useState(0); const [hideProgress, setHideProgress] = React.useState(0); const mixinStyles = resolveMixin({ padding }); + const shouldBeContained = containerRef && contained !== false; const value = React.useMemo( () => ({ @@ -247,6 +249,7 @@ const Modal: React.FC = (props) => { blurred={blurredOverlay} overflow={clientPosition === "center" ? "auto" : "hidden"} className={overlayClassName} + contained={contained} containerRef={containerRef} attributes={{ onTouchStart: handleDragStart, @@ -259,7 +262,7 @@ const Modal: React.FC = (props) => { active && s["--active"], dragging && s["--dragging"], overflow && s[`--overflow-${overflow}`], - containerRef && s["--contained"], + shouldBeContained && s["--contained"], responsiveClassNames(s, "--position", position), mixinStyles.classNames ); diff --git a/packages/reshaped/src/components/Modal/Modal.types.ts b/packages/reshaped/src/components/Modal/Modal.types.ts index c90ba397..bce6089c 100644 --- a/packages/reshaped/src/components/Modal/Modal.types.ts +++ b/packages/reshaped/src/components/Modal/Modal.types.ts @@ -53,4 +53,7 @@ export type Props = { overlayClassName?: G.ClassName; /** Additional attributes for the root element */ attributes?: G.Attributes<"div"> & { ref?: React.RefObject }; -} & Pick; +} & Pick< + OverlayProps, + "onOpen" | "onAfterOpen" | "onAfterClose" | "active" | "containerRef" | "contained" +>; diff --git a/packages/reshaped/src/components/Modal/tests/Modal.stories.tsx b/packages/reshaped/src/components/Modal/tests/Modal.stories.tsx index 813e7588..dd04e973 100644 --- a/packages/reshaped/src/components/Modal/tests/Modal.stories.tsx +++ b/packages/reshaped/src/components/Modal/tests/Modal.stories.tsx @@ -212,8 +212,10 @@ export const containerRef = { render: () => { const containerRef = React.useRef(null); const containerRef2 = React.useRef(null); + const containerRef3 = React.useRef(null); const toggle = useToggle(); const toggle2 = useToggle(); + const toggle3 = useToggle(); return ( @@ -260,6 +262,27 @@ export const containerRef = { + + + + + + + + ); }, diff --git a/packages/reshaped/src/components/Overlay/Overlay.tsx b/packages/reshaped/src/components/Overlay/Overlay.tsx index 9cea2431..293d0e55 100644 --- a/packages/reshaped/src/components/Overlay/Overlay.tsx +++ b/packages/reshaped/src/components/Overlay/Overlay.tsx @@ -31,6 +31,7 @@ const Overlay: React.FC = (props) => { onAfterOpen, disableCloseOnClick, containerRef, + contained, className, attributes, } = props; @@ -57,6 +58,7 @@ const Overlay: React.FC = (props) => { const { active: rendered, activate: render, deactivate: remove } = useToggle(active || false); const { active: visible, activate: show, deactivate: hide } = useToggle(active || false); const isDismissible = useIsDismissible({ active, contentRef, hasTrigger: false }); + const shouldBeContained = containerRef && contained !== false; const rootClassNames = classNames( s.root, @@ -64,7 +66,7 @@ const Overlay: React.FC = (props) => { isTransparent && s["--click-through"], blurred && s["--blurred"], animated && s["--animated"], - containerRef && s["--contained"], + shouldBeContained && s["--contained"], overflow === "auto" && s["--overflow-auto"], className ); diff --git a/packages/reshaped/src/components/Overlay/Overlay.types.ts b/packages/reshaped/src/components/Overlay/Overlay.types.ts index 823a4e43..abc4b995 100644 --- a/packages/reshaped/src/components/Overlay/Overlay.types.ts +++ b/packages/reshaped/src/components/Overlay/Overlay.types.ts @@ -26,6 +26,8 @@ export type Props = { disableCloseOnClick?: boolean; /** Element to render the component in */ containerRef?: React.RefObject; + /** Contain the component within the container element. Defaults to true when containerRef is provided */ + contained?: boolean; /** Additional classname for the root element */ className?: G.ClassName; /** Additional attributes for the root element */ diff --git a/packages/reshaped/src/components/Overlay/tests/Overlay.stories.tsx b/packages/reshaped/src/components/Overlay/tests/Overlay.stories.tsx index b48e146a..f75440c5 100644 --- a/packages/reshaped/src/components/Overlay/tests/Overlay.stories.tsx +++ b/packages/reshaped/src/components/Overlay/tests/Overlay.stories.tsx @@ -264,6 +264,29 @@ export const containerRef: StoryObj = { }, }; +export const containerRefNotContained: StoryObj = { + name: "containerRef with contained={false}", + render: () => { + const containerRef = React.useRef(null); + + return ( + <> +
+ + Content + + + ); + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement.ownerDocument.body); + const container = canvas.getByTestId("test-id"); + const overlay = canvas.getByText("Content"); + + expect(container).toContainElement(overlay); + }, +}; + export const className: StoryObj = { name: "className, attributes", render: () => (