Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modal: add scrollable functionality v2 #367

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/weak-shirts-give.md
@@ -0,0 +1,5 @@
---
"@cambly/syntax-core": minor
---

Modal: add scrollable functionality
6 changes: 4 additions & 2 deletions packages/syntax-core/src/Modal/FocusTrap.tsx
@@ -1,4 +1,5 @@
import React, { useEffect, useRef, type ReactElement } from "react";
import useIsHydrated from "../useIsHydrated";

function queryFocusableAll(el: HTMLDivElement): NodeListOf<HTMLElement> {
// Focusable, interactive elements that could possibly be in children
Expand Down Expand Up @@ -38,6 +39,7 @@ export default function FocusTrap({
}): ReactElement {
const elRef = useRef<HTMLDivElement | null>(null);
const previouslyFocusedElRef = useRef<HTMLElement | null>(null);
const isHydrated = useIsHydrated();

useEffect(() => {
const { current: element } = elRef;
Expand Down Expand Up @@ -68,7 +70,7 @@ export default function FocusTrap({

event.stopPropagation();
event.preventDefault();
focusFirstChild();
isHydrated && focusFirstChild();
};

// If an element has focus currently, keep a reference to that element
Expand All @@ -84,7 +86,7 @@ export default function FocusTrap({
focusElement(previouslyFocusedEl);
}
};
}, [elRef, previouslyFocusedElRef]);
}, [isHydrated, elRef, previouslyFocusedElRef]);

return (
<div data-testid="syntax-focus-trap" ref={elRef}>
Expand Down
8 changes: 3 additions & 5 deletions packages/syntax-core/src/Modal/Modal.module.css
@@ -1,11 +1,9 @@
.backdrop {
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
top: 0;
justify-content: center;
position: absolute;
inset: 0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, i didnt know this was a thing. this is how we remove all my magic numbers

}

.backdropClassic {
Expand Down
59 changes: 52 additions & 7 deletions packages/syntax-core/src/Modal/Modal.stories.tsx
Expand Up @@ -4,6 +4,7 @@ import { type StoryObj, type Meta } from "@storybook/react";
import Button from "../Button/Button";
import Typography from "../Typography/Typography";
import Modal from "./Modal";
import Box from "../Box/Box";

export default {
title: "Components/Modal",
Expand Down Expand Up @@ -99,13 +100,7 @@ export const WithImage: StoryObj<typeof Modal> = {
args: {
...Default.args,
header: "With Image",
image: (
<img
src="https://placehold.co/600x200"
alt="placeholder image"
style={{ width: "100%" }}
/>
),
image: <img src="https://placehold.co/600x200" alt="placeholder image" />,
},
render: function WithImageExample({ ...args }): JSX.Element {
const [isOpen, setIsOpen] = useState(false);
Expand Down Expand Up @@ -272,3 +267,53 @@ export const NoButtons: StoryObj<typeof Modal> = {
);
},
};

export const Scrollable: StoryObj<typeof Modal> = {
args: {
...Default.args,
header: "Scrollable",
},
render: function WithImageExample({ ...args }): JSX.Element {
const [isOpen, setIsOpen] = useState(false);

return (
<>
<Button
onClick={() => {
setIsOpen(true);
}}
text={"Click here to open Modal"}
/>
{isOpen && (
<Modal
{...args}
onDismiss={() => setIsOpen(false)}
footer={
<>
<Button
text="Cancel"
color="secondary"
onClick={() => {
action("cancel");
setIsOpen(false);
}}
/>
<Button
text="Confirm"
onClick={() => {
action("confirm");
setIsOpen(false);
}}
/>
</>
}
>
<Box height={1800}>
{Array(100).fill(<Typography>Content</Typography>)}
</Box>
</Modal>
)}
</>
);
},
};
2 changes: 1 addition & 1 deletion packages/syntax-core/src/Modal/Modal.test.tsx
Expand Up @@ -205,7 +205,7 @@ describe("modal", () => {
);
expect(screen.getByTestId("modal-sm")).toHaveStyle({
width: "100%",
maxWidth: "400px",
maxWidth: "600px",
});
});

Expand Down
114 changes: 40 additions & 74 deletions packages/syntax-core/src/Modal/Modal.tsx
Expand Up @@ -11,16 +11,10 @@ import styles from "./Modal.module.css";
import { useTheme } from "../ThemeProvider/ThemeProvider";
import IconButton from "../IconButton/IconButton";

function XIcon({ color = "#000" }: { color?: string }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill={color}>
<path
fill="inherit"
d="M11.25.758a.83.83 0 0 0-1.175 0L6 4.825 1.925.75A.83.83 0 1 0 .75 1.925L4.825 6 .75 10.075a.83.83 0 1 0 1.175 1.175L6 7.175l4.075 4.075a.83.83 0 1 0 1.175-1.175L7.175 6l4.075-4.075a.835.835 0 0 0 0-1.167Z"
/>
</svg>
);
}
const sizeWidth = {
sm: 600,
lg: 600,
} as const;

function XIconCambio({ className }: { className?: string }) {
return (
Expand All @@ -36,13 +30,6 @@ function XIconCambio({ className }: { className?: string }) {
);
}

// Note: Only sm + lg size currently. design thinks there should only be two sizes.
// If there IS a md size at some point, we should use the "size" const.
const sizeWidth = {
sm: 400,
lg: 600,
} as const;

/**
* [Modal](https://cambly-syntax.vercel.app/?path=/docs/components-modal--docs) is a dialog that appears on top of the main content and locks user interaction within the modal.
*
Expand Down Expand Up @@ -137,16 +124,12 @@ export default function Modal({
accessibilityCloseLabel?: string;
/**
* The size of the card
*
* Classic:
* * `sm`: 400px (Classic only)
* * `lg`: 600px
*
* Cambio:
* * `sm`: 600px (deprecated)
* * `lg`: 600px
*
*
* @defaultValue sm
* @deprecated
*/
size?: keyof typeof sizeWidth;
/**
Expand Down Expand Up @@ -180,77 +163,60 @@ export default function Modal({
<Box
data-testid={dataTestId}
backgroundColor="white"
rounding={themeName === "classic" ? "xl" : "md"}
rounding="md"
display="flex"
direction="column"
marginStart={4}
marginEnd={4}
marginTop={8}
marginBottom={8}
minWidth={240}
maxWidth={sizeWidth[themeName === "classic" ? size : "lg"]}
maxHeight="calc(100vh - 64px)"
maxWidth={sizeWidth[size]}
overflow="hidden"
position="relative"
width="100%"
dangerouslySetInlineStyle={{ __style: { overflow: "hidden" } }}
>
<Box position="relative">
{themeName === "classic" ? (
<button
aria-label={accessibilityCloseLabel}
type="button"
className={classnames(
styles.closeButton,
styles.closeButtonClassic,
{
[styles.closeButtonWithImage]: !!image,
},
)}
onClick={onDismiss}
>
<XIcon color={image ? "#fff" : "#000"} />
</button>
) : (
<Box
position="absolute"
dangerouslySetInlineStyle={{
__style: { top: 4, right: 4 },
}}
>
<IconButton
accessibilityLabel={accessibilityCloseLabel}
color={image ? "primary" : "tertiary"}
on={image ? "darkBackground" : "lightBackground"}
onClick={onDismiss}
size="sm"
icon={XIconCambio}
/>
</Box>
)}
</Box>
{image && <Box>{image}</Box>}
<Box
display="flex"
gap={themeName === "classic" ? 3 : 4}
direction="column"
padding={themeName === "classic" ? 9 : 6}
position="absolute"
dangerouslySetInlineStyle={{
__style: { top: 4, right: 4 },
}}
>
<Heading
as="h1"
size={themeName === "classic" ? 500 : 600}
fontStyle={themeName === "classic" ? "sans-serif" : "serif"}
<IconButton
accessibilityLabel={accessibilityCloseLabel}
color={image ? "primary" : "tertiary"}
on={image ? "darkBackground" : "lightBackground"}
onClick={onDismiss}
size="sm"
icon={XIconCambio}
/>
</Box>

<Box display="flex" direction="column" width="100%">
{image && <Box>{image}</Box>}
<Box padding={6}>
<Heading as="h1" size={600} fontStyle="serif">
{header}
</Heading>
</Box>
<Box
height="100%"
overflowY="auto"
paddingX={6}
marginBottom={footer ? 0 : 6}
>
{header}
</Heading>
<Box marginBottom={themeName === "classic" ? 4 : 0}>
{children}
</Box>
{footer && (
<Box
display="flex"
direction="column"
gap={3}
marginTop={themeName === "classic" ? 0 : 2}
smDirection="row"
smJustifyContent="end"
lgDirection="row"
lgJustifyContent="end"
padding={6}
>
{footer}
</Box>
Expand Down