Skip to content

Commit

Permalink
feat(Modal): layouts for new Modal component (#2552)
Browse files Browse the repository at this point in the history
  • Loading branch information
YossiSaadi authored Dec 2, 2024
1 parent 43b6b42 commit 97d81fa
Show file tree
Hide file tree
Showing 27 changed files with 373 additions and 44 deletions.
8 changes: 8 additions & 0 deletions packages/core/src/components/ModalNew/Modal/Modal.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
}

.modal {
--top-actions-spacing: var(--spacing-large);
--top-actions-width: var(--spacing-xl);
--modal-inline-padding: var(--spacing-xl);

position: fixed;
top: 50%;
left: 50%;
Expand All @@ -18,6 +22,10 @@
border-radius: var(--border-radius-big);
box-shadow: var(--box-shadow-large);

&.withHeaderAction {
--top-actions-width: calc(var(--spacing-xl) * 2);
}

&.sizeSmall {
--modal-max-height: 50%;
--modal-width: 45%;
Expand Down
41 changes: 23 additions & 18 deletions packages/core/src/components/ModalNew/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import ModalTopActions from "../ModalTopActions/ModalTopActions";
import { getStyle } from "../../../helpers/typesciptCssModulesHelper";
import { camelCase } from "lodash-es";
import { ModalProvider } from "../context/ModalContext";
import { ModalContextProps } from "../context/ModalContext.types";
import { ModalProviderValue } from "../context/ModalContext.types";
import useKeyEvent from "../../../hooks/useKeyEvent";
import { keyCodes } from "../../../constants";
import {
Expand Down Expand Up @@ -43,7 +43,7 @@ const Modal = forwardRef(
const setTitleIdCallback = useCallback((id: string) => setTitleId(id), []);
const setDescriptionIdCallback = useCallback((id: string) => setDescriptionId(id), []);

const contextValue = useMemo<ModalContextProps>(
const contextValue = useMemo<ModalProviderValue>(
() => ({
modalId: id,
setTitleId: setTitleIdCallback,
Expand Down Expand Up @@ -82,43 +82,48 @@ const Modal = forwardRef(
<AnimatePresence>
{show && (
<ModalProvider value={contextValue}>
<RemoveScroll>
<motion.div
variants={modalAnimationOverlayVariants}
initial="exit"
animate="enter"
exit="exit"
data-testid={getTestId(ComponentDefaultTestId.MODAL_NEXT_OVERLAY, id)}
className={styles.overlay}
onClick={onBackdropClick}
aria-hidden
/>
<FocusLock returnFocus>
<motion.div
variants={modalAnimationOverlayVariants}
initial="exit"
animate="enter"
exit="exit"
data-testid={getTestId(ComponentDefaultTestId.MODAL_NEXT_OVERLAY, id)}
className={styles.overlay}
onClick={onBackdropClick}
aria-hidden
/>
<FocusLock returnFocus>
<RemoveScroll>
<motion.div
variants={modalAnimationVariants}
initial="exit"
animate="enter"
exit="exit"
custom={anchorElementRef}
ref={ref}
className={cx(styles.modal, getStyle(styles, camelCase("size-" + size)), className)}
className={cx(
styles.modal,
getStyle(styles, camelCase("size-" + size)),
{ [styles.withHeaderAction]: !!renderHeaderAction },
className
)}
id={id}
data-testid={dataTestId || getTestId(ComponentDefaultTestId.MODAL_NEXT, id)}
role="dialog"
aria-modal
aria-labelledby={titleId}
aria-describedby={descriptionId}
>
{children}
<ModalTopActions
renderAction={renderHeaderAction}
color={closeButtonTheme}
closeButtonAriaLabel={closeButtonAriaLabel}
onClose={onClose}
/>
{children}
</motion.div>
</FocusLock>
</RemoveScroll>
</RemoveScroll>
</FocusLock>
</ModalProvider>
)}
</AnimatePresence>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ describe("Modal", () => {
userEvent.tab();
const closeButton = getByLabelText(closeButtonAriaLabel);
expect(closeButton).toHaveFocus();

userEvent.tab();
expect(getByText("Focusable 1")).toHaveFocus();
});

it("traps and moves focus to focusable element inside ModalContent and cycle through full focus flow", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import React, { forwardRef } from "react";
import cx from "classnames";
import { getTestId } from "../../../tests/test-ids-utils";
import { ComponentDefaultTestId } from "../../../tests/constants";
import styles from "./ModalContent.module.scss";
import { ModalContentProps } from "./ModalContent.types";

const ModalContent = forwardRef(
Expand All @@ -13,7 +11,7 @@ const ModalContent = forwardRef(
return (
<div
ref={ref}
className={cx(styles.content, className)}
className={className}
id={id}
data-testid={dataTestId || getTestId(ComponentDefaultTestId.MODAL_NEXT_CONTENT, id)}
data-autofocus-inside={true}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { render } from "@testing-library/react";
import ModalHeader from "../ModalHeader";
import { Text as TextIcon } from "@vibe/icons";
import { useModal } from "../../context/ModalContext";
import { ModalContextProps } from "../../context/ModalContext.types";

jest.mock("../../context/ModalContext", () => ({
useModal: jest.fn()
Expand All @@ -14,7 +15,7 @@ describe("ModalHeader", () => {
const simpleDescription = "This is a description";
const descriptionIcon = TextIcon;

const useModalMockedReturnedValue = {
const useModalMockedReturnedValue: ModalContextProps = {
modalId: "modal-id",
setTitleId: jest.fn(),
setDescriptionId: jest.fn()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
display: flex;
align-items: center;
position: absolute;
right: var(--spacing-large);
top: var(--spacing-large);
right: var(--top-actions-spacing);
top: var(--top-actions-spacing);
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from "react";

export type ModalContextProps = ModalProviderValue;

export type ModalProviderValue = {
modalId: string;
setTitleId: (id: string) => void;
setDescriptionId: (id: string) => void;
};

export type ModalContextProps = ModalProviderValue;

export interface ModalProviderProps {
value: ModalProviderValue;
children: React.ReactNode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.layout {
width: 100%;
height: 100%;
overflow: hidden;

.header {
width: 100%;
margin-block: var(--spacing-xl) var(--spacing-large);
padding-inline-end: calc(var(--top-actions-spacing) + var(--top-actions-width) + var(--spacing-medium));
padding-inline-start: var(--modal-inline-padding);
}

.divider {
flex-shrink: 0;
}

.content {
padding-block-end: var(--spacing-xl);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { forwardRef } from "react";
import cx from "classnames";
import { getTestId } from "../../../../tests/test-ids-utils";
import { ComponentDefaultTestId } from "../../../../tests/constants";
import styles from "./ModalBasicLayout.module.scss";
import { ModalBasicLayoutProps } from "./ModalBasicLayout.types";
import Flex from "../../../Flex/Flex";
import Divider from "../../../Divider/Divider";
import ModalFooterShadow from "../ModalFooterShadow";
import ModalLayoutScrollableContent from "../ModalLayoutScrollableContent";
import useLayoutScrolledContent from "../useLayoutScrolledContent";

const ModalBasicLayout = forwardRef(
(
{ children, className, id, "data-testid": dataTestId }: ModalBasicLayoutProps,
ref: React.ForwardedRef<HTMLDivElement>
) => {
const { isContentScrolled, onScroll } = useLayoutScrolledContent();
const [header, content] = React.Children.toArray(children);

return (
<>
<Flex
direction={Flex.directions.COLUMN}
align={Flex.align.START}
ref={ref}
className={cx(styles.layout, className)}
id={id}
data-testid={dataTestId || getTestId(ComponentDefaultTestId.MODAL_NEXT_BASIC_LAYOUT, id)}
>
<div className={styles.header}>{header}</div>
{isContentScrolled && <Divider className={styles.divider} withoutMargin />}
<ModalLayoutScrollableContent onScroll={onScroll} className={styles.content}>
{content}
</ModalLayoutScrollableContent>
</Flex>
{isContentScrolled && <ModalFooterShadow />}
</>
);
}
);

export default ModalBasicLayout;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from "react";
import { VibeComponentProps } from "../../../../types";

export interface ModalBasicLayoutProps extends VibeComponentProps {
children: React.ReactNode;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.shadowWrapper::after {
content: "";
position: absolute;
width: 100%;
height: 10px;
box-shadow: var(--box-shadow-medium);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from "react";
import styles from "./ModalFooterShadow.module.scss";

const ModalFooterShadow = () => {
return <div className={styles.shadowWrapper} />;
};

export default ModalFooterShadow;
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
@import "~monday-ui-style/dist/mixins";

.content {
padding-block-end: var(--spacing-xl);
width: 100%;
height: 100%;
padding-inline: var(--modal-inline-padding);
overflow: auto;
@include scroller;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from "react";
import cx from "classnames";
import styles from "./ModalLayoutScrollableContent.module.scss";
import { ModalLayoutScrollableContentProps } from "./ModalLayoutScrollableContent.types";

const ModalLayoutScrollableContent = ({ onScroll, className, children }: ModalLayoutScrollableContentProps) => {
return (
<div className={cx(styles.content, className)} onScroll={onScroll}>
{children}
</div>
);
};

export default ModalLayoutScrollableContent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ReactNode, UIEventHandler } from "react";

export interface ModalLayoutScrollableContentProps {
onScroll?: UIEventHandler<HTMLDivElement>;
className?: string;
children: ReactNode;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.media {
max-width: 100%;
max-height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
overflow: hidden;
position: relative;
}
28 changes: 28 additions & 0 deletions packages/core/src/components/ModalNew/layouts/ModalMedia.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { forwardRef } from "react";
import Flex from "../../Flex/Flex";
import { ModalMediaProps } from "./ModalMedia.types";
import styles from "./ModalMedia.module.scss";
import cx from "classnames";
import { ComponentDefaultTestId, getTestId } from "../../../tests/test-ids-utils";

const ModalMedia = forwardRef(
(
{ children, className, id, "data-testid": dataTestId }: ModalMediaProps,
ref: React.ForwardedRef<HTMLDivElement>
) => {
return (
<Flex
id={id}
data-testid={dataTestId || getTestId(ComponentDefaultTestId.MODAL_NEXT_MEDIA, id)}
justify={Flex.justify.CENTER}
align={Flex.align.STRETCH}
ref={ref}
className={cx(styles.media, className)}
>
{children}
</Flex>
);
}
);

export default ModalMedia;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from "react";
import { VibeComponentProps } from "../../../types";

export interface ModalMediaProps extends VibeComponentProps {
children: React.ReactNode;
}
Loading

0 comments on commit 97d81fa

Please sign in to comment.