+ );
+};
+
+export const useRemoveModalScrollLock = (show: boolean, isDocsView?: boolean) => {
+ useEffect(() => {
+ if (show && document.body.attributes.getNamedItem("data-scroll-locked") && isDocsView) {
+ document.body.attributes.removeNamedItem("data-scroll-locked");
+ document.documentElement.addEventListener(
+ "wheel",
+ e => {
+ e.stopImmediatePropagation();
+ },
+ true
+ );
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- this is intended to run once, on mount
+ }, []);
+};
+
+export function withOpenedModalPreview(
+ Story: React.FunctionComponent<{ show: boolean; setShow: (show: boolean) => void }>,
+ { size, isDocsView }: { size?: "small" | "medium" | "large"; isDocsView: boolean }
+) {
+ const [show, setShow] = useState(true);
+ useRemoveModalScrollLock(show, isDocsView); // internal hook, for documentation purposes, to enable scroll on first load
+
+ return (
+ // internal component, for documentation purposes, to open modal inside a container
+ setShow(true)} isDocsView={isDocsView}>
+
+
+ );
+}
+
+export const ModalTip = () => (
+
+
+ Since the modal is used for short and non-frequent tasks, consider using the main flow for common tasks. For
+ creating a popover positioned next to other components, like customized menus, check out our{" "}
+
+ Dialog
+ {" "}
+ component.
+
+
+);
diff --git a/packages/core/src/components/ModalNew/Modal/__stories__/Modal.stories.module.scss b/packages/core/src/components/ModalNew/Modal/__stories__/Modal.stories.module.scss
new file mode 100644
index 0000000000..c54af9e71a
--- /dev/null
+++ b/packages/core/src/components/ModalNew/Modal/__stories__/Modal.stories.module.scss
@@ -0,0 +1,47 @@
+.largeComponentRule {
+ height: fit-content !important;
+ width: fit-content;
+ padding: var(--sb-spacing-large);
+}
+
+.preview {
+ padding-inline-start: 32px;
+ padding-block-start: 40px;
+ height: 360px;
+ width: 100%;
+ container-type: inline-size;
+
+ &.small {
+ height: 360px;
+ }
+
+ &.medium {
+ height: 416px;
+ }
+
+ &.large {
+ height: 530px;
+ }
+
+ /**
+ * The following css is to override the default dimensions of the modal component
+ * this is necessary because in the documentation, we're "trapping" the modal component inside the preview component
+ * so the modal component width and height be relative to the preview component and not the viewport
+ */
+ [aria-modal][role="dialog"] {
+ &[class*="sizeSmall"] {
+ --modal-max-height: 50%;
+ --modal-width: 480px;
+ }
+
+ &[class*="sizeMedium"] {
+ --modal-max-height: 80%;
+ --modal-width: 580px;
+ }
+
+ &[class*="sizeLarge"] {
+ --modal-max-height: 80%;
+ --modal-width: 840px;
+ }
+ }
+}
diff --git a/packages/core/src/components/ModalNew/Modal/__stories__/assets/backdrop-do.png b/packages/core/src/components/ModalNew/Modal/__stories__/assets/backdrop-do.png
new file mode 100644
index 0000000000..b2948973c8
Binary files /dev/null and b/packages/core/src/components/ModalNew/Modal/__stories__/assets/backdrop-do.png differ
diff --git a/packages/core/src/components/ModalNew/Modal/__stories__/assets/backdrop-dont.png b/packages/core/src/components/ModalNew/Modal/__stories__/assets/backdrop-dont.png
new file mode 100644
index 0000000000..6202c99e72
Binary files /dev/null and b/packages/core/src/components/ModalNew/Modal/__stories__/assets/backdrop-dont.png differ
diff --git a/packages/core/src/components/ModalNew/Modal/__stories__/assets/cta-do.png b/packages/core/src/components/ModalNew/Modal/__stories__/assets/cta-do.png
new file mode 100644
index 0000000000..a30f744271
Binary files /dev/null and b/packages/core/src/components/ModalNew/Modal/__stories__/assets/cta-do.png differ
diff --git a/packages/core/src/components/ModalNew/Modal/__stories__/assets/cta-dont.png b/packages/core/src/components/ModalNew/Modal/__stories__/assets/cta-dont.png
new file mode 100644
index 0000000000..7f2d324869
Binary files /dev/null and b/packages/core/src/components/ModalNew/Modal/__stories__/assets/cta-dont.png differ
diff --git a/packages/core/src/components/ModalNew/Modal/__stories__/assets/loading-do.png b/packages/core/src/components/ModalNew/Modal/__stories__/assets/loading-do.png
new file mode 100644
index 0000000000..0fa869ab5f
Binary files /dev/null and b/packages/core/src/components/ModalNew/Modal/__stories__/assets/loading-do.png differ
diff --git a/packages/core/src/components/ModalNew/Modal/__stories__/assets/loading-dont.png b/packages/core/src/components/ModalNew/Modal/__stories__/assets/loading-dont.png
new file mode 100644
index 0000000000..5a71ee77ed
Binary files /dev/null and b/packages/core/src/components/ModalNew/Modal/__stories__/assets/loading-dont.png differ
diff --git a/packages/core/src/components/ModalNew/ModalContent/ModalContent.types.ts b/packages/core/src/components/ModalNew/ModalContent/ModalContent.types.ts
index d50def5bd6..9484a843cb 100644
--- a/packages/core/src/components/ModalNew/ModalContent/ModalContent.types.ts
+++ b/packages/core/src/components/ModalNew/ModalContent/ModalContent.types.ts
@@ -2,5 +2,8 @@ import React from "react";
import { VibeComponentProps } from "../../../types";
export interface ModalContentProps extends VibeComponentProps {
+ /**
+ * Main content of the modal.
+ */
children?: React.ReactNode;
}
diff --git a/packages/core/src/components/ModalNew/ModalHeader/ModalHeader.types.ts b/packages/core/src/components/ModalNew/ModalHeader/ModalHeader.types.ts
index 8e5f9a750f..61aa13250c 100644
--- a/packages/core/src/components/ModalNew/ModalHeader/ModalHeader.types.ts
+++ b/packages/core/src/components/ModalNew/ModalHeader/ModalHeader.types.ts
@@ -7,7 +7,14 @@ interface WithoutDescription {
}
interface WithDescription {
+ /**
+ * Descriptive text or content below the title.
+ * When supplied, would also add an aria-describedby attribute to the modal dialog element.
+ */
description: string | React.ReactNode;
+ /**
+ * Icon to display before the description. Can only be passed when description is supplied.
+ */
descriptionIcon?:
| SubIcon
| {
@@ -16,4 +23,11 @@ interface WithDescription {
};
}
-export type ModalHeaderProps = { title: string } & (WithoutDescription | WithDescription) & VibeComponentProps;
+export type ModalHeaderProps = {
+ /**
+ * Main heading text of the modal.
+ * When supplied, would also add an aria-labelledby attribute to the modal dialog element.
+ */
+ title: string;
+} & (WithDescription | WithoutDescription) &
+ VibeComponentProps;
diff --git a/packages/core/src/components/ModalNew/ModalTopActions/ModalTopActions.tsx b/packages/core/src/components/ModalNew/ModalTopActions/ModalTopActions.tsx
index e44eddac2d..e381325439 100644
--- a/packages/core/src/components/ModalNew/ModalTopActions/ModalTopActions.tsx
+++ b/packages/core/src/components/ModalNew/ModalTopActions/ModalTopActions.tsx
@@ -1,17 +1,17 @@
import React from "react";
import styles from "./ModalTopActions.module.scss";
-import { ModalTopActionsButtonColor, ModalTopActionsColor, ModalTopActionsProps } from "./ModalTopActions.types";
+import { ModalTopActionsButtonColor, ModalTopActionsTheme, ModalTopActionsProps } from "./ModalTopActions.types";
import IconButton from "../../IconButton/IconButton";
import { CloseMedium } from "@vibe/icons";
import { ButtonColor } from "../../Button/ButtonConstants";
-const colorToButtonColor: Record = {
+const colorToButtonColor: Record = {
dark: ButtonColor.ON_INVERTED_BACKGROUND,
light: ButtonColor.ON_PRIMARY_COLOR
};
-const ModalTopActions = ({ renderAction, color, closeButtonAriaLabel, onClose }: ModalTopActionsProps) => {
- const buttonColor = colorToButtonColor[color] || ButtonColor.PRIMARY;
+const ModalTopActions = ({ renderAction, theme, closeButtonAriaLabel, onClose }: ModalTopActionsProps) => {
+ const buttonColor = colorToButtonColor[theme] || ButtonColor.PRIMARY;
return (
diff --git a/packages/core/src/components/ModalNew/ModalTopActions/ModalTopActions.types.ts b/packages/core/src/components/ModalNew/ModalTopActions/ModalTopActions.types.ts
index 9ee221d54f..56bf075f89 100644
--- a/packages/core/src/components/ModalNew/ModalTopActions/ModalTopActions.types.ts
+++ b/packages/core/src/components/ModalNew/ModalTopActions/ModalTopActions.types.ts
@@ -3,7 +3,7 @@ import MenuButton from "../../MenuButton/MenuButton";
import IconButton from "../../IconButton/IconButton";
import { ButtonColor } from "../../Button/ButtonConstants";
-export type ModalTopActionsColor = "light" | "dark";
+export type ModalTopActionsTheme = "light" | "dark";
export type ModalTopActionsButtonColor =
| ButtonColor.PRIMARY
| ButtonColor.ON_PRIMARY_COLOR
@@ -11,13 +11,22 @@ export type ModalTopActionsButtonColor =
export interface ModalTopActionsProps {
/**
- * action can be passed either as a function or direct
- * it allows passing back to consumer the color he chose, so he won't have to define it twice
+ * Action element or render function for the top-right area.
+ * When provided as a function, receives the current button color theme
*/
renderAction?:
| React.ReactElement
| ((color?: ModalTopActionsButtonColor) => React.ReactElement);
- color?: ModalTopActionsColor;
+ /**
+ * Color theme for the top actions
+ */
+ theme?: ModalTopActionsTheme;
+ /**
+ * Accessibility label for the close button
+ */
closeButtonAriaLabel?: string;
+ /**
+ * Callback fired when the close button is clicked
+ */
onClose?: (event: React.MouseEvent) => void;
}
diff --git a/packages/core/src/components/ModalNew/ModalTopActions/__tests__/ModalTopActions.test.tsx b/packages/core/src/components/ModalNew/ModalTopActions/__tests__/ModalTopActions.test.tsx
index 8901e0e9de..38584d9fd9 100644
--- a/packages/core/src/components/ModalNew/ModalTopActions/__tests__/ModalTopActions.test.tsx
+++ b/packages/core/src/components/ModalNew/ModalTopActions/__tests__/ModalTopActions.test.tsx
@@ -41,7 +41,7 @@ describe("ModalTopActions", () => {
it("calls renderAction with correct color argument", () => {
const renderAction = jest.fn(color => );
- render();
+ render();
expect(renderAction).toHaveBeenCalledWith(ButtonColor.ON_INVERTED_BACKGROUND);
});
@@ -56,12 +56,12 @@ describe("ModalTopActions", () => {
});
it("applies the correct color when 'dark' is passed", () => {
- const { getByLabelText } = render();
+ const { getByLabelText } = render();
expect(getByLabelText(closeButtonAriaLabel)).toHaveClass(camelCase("color-" + ButtonColor.ON_INVERTED_BACKGROUND));
});
it("applies the correct color when 'light' is passed", () => {
- const { getByLabelText } = render();
+ const { getByLabelText } = render();
expect(getByLabelText(closeButtonAriaLabel)).toHaveClass(camelCase("color-" + ButtonColor.ON_PRIMARY_COLOR));
});
diff --git a/packages/core/src/components/ModalNew/context/ModalContext.types.ts b/packages/core/src/components/ModalNew/context/ModalContext.types.ts
index 9ec22d54e9..4903b75bbd 100644
--- a/packages/core/src/components/ModalNew/context/ModalContext.types.ts
+++ b/packages/core/src/components/ModalNew/context/ModalContext.types.ts
@@ -3,12 +3,29 @@ import React from "react";
export type ModalContextProps = ModalProviderValue;
export type ModalProviderValue = {
+ /**
+ * Unique identifier for the modal.
+ * In use to set the modal title and description IDs to be unique.
+ */
modalId: string;
+ /**
+ * Callback to set the title element ID for accessibility.
+ */
setTitleId: (id: string) => void;
+ /**
+ * Callback to set the description element ID for accessibility.
+ */
setDescriptionId: (id: string) => void;
};
export interface ModalProviderProps {
+ /**
+ * Context value containing modal state and handlers.
+ */
value: ModalProviderValue;
+ /**
+ * Modal provider children.
+ * Should be the Modal root.
+ */
children: React.ReactNode;
}
diff --git a/packages/core/src/components/ModalNew/footers/ModalFooter/ModalFooter.types.ts b/packages/core/src/components/ModalNew/footers/ModalFooter/ModalFooter.types.ts
index 1ce64b56ca..cd21581f4f 100644
--- a/packages/core/src/components/ModalNew/footers/ModalFooter/ModalFooter.types.ts
+++ b/packages/core/src/components/ModalNew/footers/ModalFooter/ModalFooter.types.ts
@@ -2,5 +2,8 @@ import React from "react";
import { ModalFooterBaseProps } from "../ModalFooterBase/ModalFooterBase.types";
export interface ModalFooterProps extends Omit {
+ /**
+ * Optional content to render on the left side of the footer.
+ */
renderSideAction?: React.ReactNode;
}
diff --git a/packages/core/src/components/ModalNew/footers/ModalFooterBase/ModalFooterBase.types.ts b/packages/core/src/components/ModalNew/footers/ModalFooterBase/ModalFooterBase.types.ts
index f9e2944392..4607862e8f 100644
--- a/packages/core/src/components/ModalNew/footers/ModalFooterBase/ModalFooterBase.types.ts
+++ b/packages/core/src/components/ModalNew/footers/ModalFooterBase/ModalFooterBase.types.ts
@@ -3,11 +3,23 @@ import React from "react";
import { VibeComponentProps } from "../../../../types";
export interface ModalFooterActionProps extends Omit {
+ /**
+ * Text to display as the Button's content.
+ */
text: string;
}
export interface ModalFooterBaseProps extends VibeComponentProps {
+ /**
+ * Props for the primary action button.
+ */
primaryButton: ModalFooterActionProps;
+ /**
+ * Props for the optional secondary action button.
+ */
secondaryButton?: ModalFooterActionProps;
+ /**
+ * Additional content to render in the footer.
+ */
renderAction?: React.ReactNode;
}
diff --git a/packages/core/src/components/ModalNew/footers/ModalFooterWizard/ModalFooterWizard.tsx b/packages/core/src/components/ModalNew/footers/ModalFooterWizard/ModalFooterWizard.tsx
index ab15e90a7e..844a4f941d 100644
--- a/packages/core/src/components/ModalNew/footers/ModalFooterWizard/ModalFooterWizard.tsx
+++ b/packages/core/src/components/ModalNew/footers/ModalFooterWizard/ModalFooterWizard.tsx
@@ -1,20 +1,11 @@
import React, { forwardRef } from "react";
-import { VibeComponentProps } from "../../../../types";
import cx from "classnames";
import ModalFooterBase from "../ModalFooterBase/ModalFooterBase";
-import { ModalFooterBaseProps } from "../ModalFooterBase/ModalFooterBase.types";
import { getTestId } from "../../../../tests/test-ids-utils";
import { ComponentDefaultTestId } from "../../../../tests/constants";
import styles from "./ModalFooterWizard.module.scss";
import { StepsGalleryHeader } from "../../../Steps/StepsGalleryHeader";
-
-export interface ModalFooterWizardProps
- extends Required>,
- VibeComponentProps {
- stepCount: number;
- activeStep: number;
- onStepClick: (stepIndex: number) => void;
-}
+import { ModalFooterWizardProps } from "./ModalFooterWizard.types";
const ModalFooterWizard = forwardRef(
(
diff --git a/packages/core/src/components/ModalNew/footers/ModalFooterWizard/ModalFooterWizard.types.ts b/packages/core/src/components/ModalNew/footers/ModalFooterWizard/ModalFooterWizard.types.ts
new file mode 100644
index 0000000000..beb070e853
--- /dev/null
+++ b/packages/core/src/components/ModalNew/footers/ModalFooterWizard/ModalFooterWizard.types.ts
@@ -0,0 +1,21 @@
+import { ModalFooterBaseProps } from "../ModalFooterBase/ModalFooterBase.types";
+import { VibeComponentProps } from "../../../../types";
+
+export interface ModalFooterWizardProps
+ extends Required>,
+ VibeComponentProps {
+ /**
+ * Total number of steps in the wizard.
+ * This would render the appropriate number of step indicators ("dots") in the footer.
+ */
+ stepCount: number;
+ /**
+ * Current active step (0-based index).
+ * This would highlight the corresponding step indicator ("dot") in the footer.
+ */
+ activeStep: number;
+ /**
+ * Callback fired when a step indicator ("dot") is clicked.
+ */
+ onStepClick: (stepIndex: number) => void;
+}
diff --git a/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/ModalBasicLayout.types.ts b/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/ModalBasicLayout.types.ts
index 67c237cdee..dc620c6948 100644
--- a/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/ModalBasicLayout.types.ts
+++ b/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/ModalBasicLayout.types.ts
@@ -2,5 +2,10 @@ import React from "react";
import { VibeComponentProps } from "../../../../types";
export interface ModalBasicLayoutProps extends VibeComponentProps {
+ /**
+ * Layout children in the following order:
+ * 1. Header content
+ * 2. Main content
+ */
children: React.ReactNode;
}
diff --git a/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/ModalBasicLayout.mdx b/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/ModalBasicLayout.mdx
new file mode 100644
index 0000000000..fa7654dc84
--- /dev/null
+++ b/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/ModalBasicLayout.mdx
@@ -0,0 +1,129 @@
+import { Meta } from "@storybook/blocks";
+import * as ModalBasicLayoutStories from "./ModalBasicLayout.stories";
+import actionDo from "./assets/action-do.png";
+import actionDont from "./assets/action-dont.png";
+import styles from "../../../Modal/__stories__/Modal.stories.module.scss";
+import {
+ MODAL_MEDIA_LAYOUT,
+ MODAL_SIDE_BY_SIDE_LAYOUT,
+ TIPSEEN
+} from "../../../../../storybook/components/related-components/component-description-map";
+import { BasicModalTip } from "./ModalBasicLayout.stories.helpers";
+
+
+
+# Basic modal
+
+- [Overview](#overview)
+- [Props](#props)
+- [Usage](#usage)
+- [Variants](#variants)
+- [Scroll](#scroll)
+- [Use cases and examples](#use-cases-and-examples)
+- [Do's and dont's](do's-and-don'ts)
+- [Related components](#related-components)
+- [Feedback](#feedback)
+
+## Overview
+
+The Basic Modal is intended for straightforward tasks, like selecting items or gathering basic information. Basic Modals help users focus on a single task without distractions. These modals do not support images or videos.
+
+
+
+## Props
+
+
+
+## Usage
+
+
+
+
+
+## Variants
+
+### Sizes
+
+The modal component has three sizes - small, medium, and large. The modal width is responsive and adjust in width based on screen size. Each size also has a maximum height to keep harmonic window ratio, while the content area adapting to fit.
+
+
+
+### Alert Modal
+
+Use the "alertModal" boolean prop in order to allow closing the modal only by the close buttons and not by ESC or by clicking outside. Use this variant in case of sensitive or important messages, and in modals that requires data from the user, such as forms.
+
+
+
+## Scroll
+
+When the content of the modal is too large to fit within the viewport, the modal content should become scrollable while the header and footer stay sticky. If the scroll is too long, consider switching to a different modal size or a different layout.
+
+
+
+## Use cases and examples
+
+### Wizard footer
+
+When multi steps modal, use the "wizard footer". For more guidelines about the footer we recommend to check our ModalFooter page.
+
+
+
+### Footer with side action
+
+The footer has an option to include additional content on the left side when needed. This extra content can consist of a button, checkbox, or simple text for notes. Note that this option is only available with the default footer.
+
+
+
+### Header with extra icon button
+
+In case of a need of an icon button in the modal header, you can use our default header "Action slot".
+You can also use it as a menu button component.
+
+
+
+### Animation
+
+Each modal includes an animation type based on its entrance point, with wizard modals also featuring transition animations. The default is the element trigger animation, which can be replaced with a center pop animation if there's no specific trigger. Transition animation is used exclusively for wizard modals and cannot be changed or removed.
+
+
+
+## Do's and don'ts
+
+
+ ),
+ description: "Use button, checkbox, or simple text for notes as an extra content to the footer."
+ },
+ negative: {
+ component: (
+
+ ),
+ description: "Don't use images, inputs or any kind of content that can overload the user."
+ }
+ }
+ ]}
+/>
+
+## Related components
+
+
diff --git a/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/ModalBasicLayout.stories.helpers.tsx b/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/ModalBasicLayout.stories.helpers.tsx
new file mode 100644
index 0000000000..aec3ffc3f6
--- /dev/null
+++ b/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/ModalBasicLayout.stories.helpers.tsx
@@ -0,0 +1,18 @@
+import React from "react";
+import { StorybookLink, Tip } from "vibe-storybook-components";
+
+export const BasicModalTip = () => (
+
+
+ If your content is not scrollable and you need to add media as supporting element, consider using{" "}
+
+ Side-by-side modal
+ {" "}
+ or{" "}
+
+ Media modal
+ {" "}
+ depends on your use case.
+
+
+);
diff --git a/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/ModalBasicLayout.stories.tsx b/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/ModalBasicLayout.stories.tsx
new file mode 100644
index 0000000000..5bf644c9f5
--- /dev/null
+++ b/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/ModalBasicLayout.stories.tsx
@@ -0,0 +1,468 @@
+import React, { useRef, useState } from "react";
+import { Meta, StoryObj } from "@storybook/react";
+import Modal from "../../../Modal/Modal";
+import { createStoryMetaSettingsDecorator } from "../../../../../storybook";
+import ModalBasicLayout from "../ModalBasicLayout";
+import ModalHeader from "../../../ModalHeader/ModalHeader";
+import ModalContent from "../../../ModalContent/ModalContent";
+import ModalFooter from "../../../footers/ModalFooter/ModalFooter";
+import Flex from "../../../../Flex/Flex";
+import Button from "../../../../Button/Button";
+import { createPortal } from "react-dom";
+import Text from "../../../../Text/Text";
+import Link from "../../../../Link/Link";
+import TransitionView from "../../../../TransitionView/TransitionView";
+import ModalFooterWizard from "../../../footers/ModalFooterWizard/ModalFooterWizard";
+import useWizard from "../../../../../hooks/useWizard/useWizard";
+import { Checkbox } from "../../../../Checkbox";
+import IconButton from "../../../../IconButton/IconButton";
+import { Menu } from "@vibe/icons";
+import { withOpenedModalPreview } from "../../../Modal/__stories__/Modal.stories.helpers";
+
+type Story = StoryObj;
+
+const metaSettings = createStoryMetaSettingsDecorator({
+ component: Modal
+});
+
+export default {
+ title: "Internal/Components/Modal [New]/Basic modal",
+ component: Modal,
+ subcomponents: { ModalBasicLayout, ModalHeader, ModalContent, ModalFooter, ModalFooterWizard, TransitionView },
+ argTypes: metaSettings.argTypes,
+ decorators: metaSettings.decorators,
+ tags: ["internal"]
+} satisfies Meta;
+
+export const Overview: Story = {
+ decorators: [(Story, context) => withOpenedModalPreview(Story, { isDocsView: context.viewMode === "docs" })],
+ render: (args, { show, setShow }) => {
+ return (
+ setShow(false)} {...args}>
+
+
+ Modal subtitle, can come with icon
+
+ }
+ />
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs. Please
+ make sure that the content is clear for completing the relevant task.
+
+
+
+ setShow(false) }}
+ secondaryButton={{ text: "Cancel", onClick: () => setShow(false) }}
+ />
+
+ );
+ },
+ parameters: {
+ docs: {
+ liveEdit: {
+ isEnabled: false
+ }
+ }
+ }
+};
+
+export const Sizes: Story = {
+ render: () => {
+ const [showSmall, setShowSmall] = useState(false);
+ const [showMedium, setShowMedium] = useState(false);
+ const [showLarge, setShowLarge] = useState(false);
+
+ return (
+ <>
+
+
+
+
+
+ {createPortal(
+ setShowSmall(false)}>
+
+
+ Modal subtitle, can come with icon
+
+ }
+ />
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs.
+ Please make sure that the content is clear for completing the relevant task.
+
+
+
+ setShowSmall(false) }}
+ secondaryButton={{ text: "Cancel", onClick: () => setShowSmall(false) }}
+ />
+ ,
+ document.body
+ )}
+ {createPortal(
+ setShowMedium(false)}>
+
+
+ Modal subtitle, can come with icon
+
+ }
+ />
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs.
+ Please make sure that the content is clear for completing the relevant task.
+
+
+
+ setShowMedium(false) }}
+ secondaryButton={{ text: "Cancel", onClick: () => setShowMedium(false) }}
+ />
+ ,
+ document.body
+ )}
+ {createPortal(
+ setShowLarge(false)}>
+
+
+ Modal subtitle, can come with icon
+
+ }
+ />
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs.
+ Please make sure that the content is clear for completing the relevant task.
+
+
+
+ setShowLarge(false) }}
+ secondaryButton={{ text: "Cancel", onClick: () => setShowLarge(false) }}
+ />
+ ,
+ document.body
+ )}
+ >
+ );
+ }
+};
+
+export const AlertModal: Story = {
+ decorators: [(Story, context) => withOpenedModalPreview(Story, { isDocsView: context.viewMode === "docs" })],
+ render: (_, { show, setShow }) => {
+ return (
+ setShow(false)}>
+
+
+
+ This will allow closing the modal only by the close buttons and not by ESC or by clicking outside.
+
+
+ setShow(false) }}
+ secondaryButton={{ text: "Cancel", onClick: () => setShow(false) }}
+ />
+
+ );
+ }
+};
+
+export const Scroll: Story = {
+ decorators: [(Story, context) => withOpenedModalPreview(Story, { isDocsView: context.viewMode === "docs" })],
+ render: (_, { show, setShow }) => {
+ return (
+ setShow(false)}>
+
+
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs. Please
+ make sure that the content is clear for completing the relevant task. The Basic Modal is intended for
+ straightforward tasks, like selecting items or gathering basic information. Basic Modals help users focus
+ on a single task without distractions. These modals do not support images or videos. When the content of
+ the modal is too large to fit within the viewport, the modal content should become scrollable while the
+ header and footer stay sticky. If the scroll is too long, consider switching to a different modal size or
+ a different layout. Modal content will appear here, you can custom it however you want, according to the
+ user needs. Please make sure that the content is clear for completing the relevant task.
+
+
+
+ setShow(false) }}
+ secondaryButton={{ text: "Cancel", onClick: () => setShow(false) }}
+ />
+
+ );
+ }
+};
+
+export const Wizard: Story = {
+ decorators: [(Story, context) => withOpenedModalPreview(Story, { isDocsView: context.viewMode === "docs" })],
+ render: (_, { show, setShow }) => {
+ const steps = [
+
+
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs. Please make
+ sure that the content is clear for completing the relevant task.
+
+
+ ,
+
+
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs. Please make
+ sure that the content is clear for completing the relevant task.
+
+
+ ,
+
+
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs. Please make
+ sure that the content is clear for completing the relevant task.
+
+
+
+ ];
+
+ const { activeStep, direction, next, back, isFirstStep, goToStep } = useWizard({
+ stepCount: steps.length
+ });
+
+ return (
+ setShow(false)}>
+
+ {steps}
+
+
+
+ );
+ }
+};
+
+export const FooterWithSideAction: Story = {
+ decorators: [(Story, context) => withOpenedModalPreview(Story, { isDocsView: context.viewMode === "docs" })],
+ render: (_, { show, setShow }) => {
+ return (
+ setShow(false)}>
+
+
+ Modal subtitle, can come with icon
+
+ }
+ />
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs. Please
+ make sure that the content is clear for completing the relevant task.
+
+
+
+ setShow(false) }}
+ secondaryButton={{ text: "Cancel", onClick: () => setShow(false) }}
+ renderSideAction={}
+ />
+
+ );
+ }
+};
+
+export const HeaderWithExtraIconButton: Story = {
+ decorators: [(Story, context) => withOpenedModalPreview(Story, { isDocsView: context.viewMode === "docs" })],
+ render: (_, { show, setShow }) => {
+ return (
+ }
+ size="medium"
+ onClose={() => setShow(false)}
+ >
+
+
+ Modal subtitle, can come with icon
+
+ }
+ />
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs. Please
+ make sure that the content is clear for completing the relevant task.
+
+
+
+ setShow(false) }}
+ secondaryButton={{ text: "Cancel", onClick: () => setShow(false) }}
+ renderSideAction={}
+ />
+
+ );
+ }
+};
+
+export const Animation: Story = {
+ render: () => {
+ const [showAnchor, setShowAnchor] = useState(false);
+ const [showCenterPop, setShowCenterPop] = useState(false);
+ const [showTransition, setShowTransition] = useState(false);
+
+ const anchorButtonRef = useRef(null);
+
+ const transitionSteps = [
+
+
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs. Please make
+ sure that the content is clear for completing the relevant task.
+
+
+ ,
+
+
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs. Please make
+ sure that the content is clear for completing the relevant task.
+
+
+ ,
+
+
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs. Please make
+ sure that the content is clear for completing the relevant task.
+
+
+
+ ];
+
+ const { activeStep, direction, next, back, isFirstStep, goToStep } = useWizard({
+ stepCount: transitionSteps.length
+ });
+
+ return (
+ <>
+
+
+
+
+
+ {createPortal(
+ setShowAnchor(false)}
+ >
+
+
+ Modal subtitle, can come with icon
+
+ }
+ />
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs.
+ Please make sure that the content is clear for completing the relevant task.
+
+
+
+ setShowAnchor(false) }}
+ secondaryButton={{ text: "Cancel", onClick: () => setShowAnchor(false) }}
+ />
+ ,
+ document.body
+ )}
+ {createPortal(
+ setShowCenterPop(false)}>
+
+
+ Modal subtitle, can come with icon
+
+ }
+ />
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs.
+ Please make sure that the content is clear for completing the relevant task.
+
+
+
+ setShowCenterPop(false) }}
+ secondaryButton={{ text: "Cancel", onClick: () => setShowCenterPop(false) }}
+ />
+ ,
+ document.body
+ )}
+ {createPortal(
+ setShowTransition(false)}
+ >
+
+ {transitionSteps}
+
+
+ ,
+ document.body
+ )}
+ >
+ );
+ }
+};
diff --git a/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/ModalBasicLayoutRelatedComponent.tsx b/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/ModalBasicLayoutRelatedComponent.tsx
new file mode 100644
index 0000000000..82946034da
--- /dev/null
+++ b/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/ModalBasicLayoutRelatedComponent.tsx
@@ -0,0 +1,24 @@
+import React, { useMemo } from "react";
+import { RelatedComponent } from "vibe-storybook-components";
+import relatedComponentImage from "./assets/related-component.png";
+
+export const ModalBasicLayoutRelatedComponent = () => {
+ const component = useMemo(() => {
+ return (
+
+ );
+ }, []);
+
+ return (
+
+ );
+};
diff --git a/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/assets/action-do.png b/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/assets/action-do.png
new file mode 100644
index 0000000000..ed743c16ab
Binary files /dev/null and b/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/assets/action-do.png differ
diff --git a/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/assets/action-dont.png b/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/assets/action-dont.png
new file mode 100644
index 0000000000..8f58cdc90b
Binary files /dev/null and b/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/assets/action-dont.png differ
diff --git a/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/assets/related-component.png b/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/assets/related-component.png
new file mode 100644
index 0000000000..b0f588188e
Binary files /dev/null and b/packages/core/src/components/ModalNew/layouts/ModalBasicLayout/__stories__/assets/related-component.png differ
diff --git a/packages/core/src/components/ModalNew/layouts/ModalFooterShadow.types.ts b/packages/core/src/components/ModalNew/layouts/ModalFooterShadow.types.ts
index f3d4d656aa..8319aef7fb 100644
--- a/packages/core/src/components/ModalNew/layouts/ModalFooterShadow.types.ts
+++ b/packages/core/src/components/ModalNew/layouts/ModalFooterShadow.types.ts
@@ -1,3 +1,6 @@
export interface ModalFooterShadowProps {
+ /**
+ * Controls the visibility of the shadow.
+ */
show: boolean;
}
diff --git a/packages/core/src/components/ModalNew/layouts/ModalLayoutScrollableContent.types.ts b/packages/core/src/components/ModalNew/layouts/ModalLayoutScrollableContent.types.ts
index 7fa753ff9a..2f83533c05 100644
--- a/packages/core/src/components/ModalNew/layouts/ModalLayoutScrollableContent.types.ts
+++ b/packages/core/src/components/ModalNew/layouts/ModalLayoutScrollableContent.types.ts
@@ -1,7 +1,16 @@
import { ReactNode, UIEventHandler } from "react";
export interface ModalLayoutScrollableContentProps {
+ /**
+ * Callback fired when the content is scrolled.
+ */
onScroll?: UIEventHandler;
+ /**
+ * Additional class name.
+ */
className?: string;
+ /**
+ * Scrollable content.
+ */
children: ReactNode;
}
diff --git a/packages/core/src/components/ModalNew/layouts/ModalMedia.module.scss b/packages/core/src/components/ModalNew/layouts/ModalMedia.module.scss
index 9eb13e01cf..67197d4106 100644
--- a/packages/core/src/components/ModalNew/layouts/ModalMedia.module.scss
+++ b/packages/core/src/components/ModalNew/layouts/ModalMedia.module.scss
@@ -4,7 +4,7 @@
display: flex;
justify-content: center;
align-items: center;
- flex-shrink: 0;
+ flex: 1 0;
overflow: hidden;
position: relative;
}
diff --git a/packages/core/src/components/ModalNew/layouts/ModalMedia.types.ts b/packages/core/src/components/ModalNew/layouts/ModalMedia.types.ts
index 550b46dc33..6584a726b4 100644
--- a/packages/core/src/components/ModalNew/layouts/ModalMedia.types.ts
+++ b/packages/core/src/components/ModalNew/layouts/ModalMedia.types.ts
@@ -2,5 +2,8 @@ import React from "react";
import { VibeComponentProps } from "../../../types";
export interface ModalMediaProps extends VibeComponentProps {
+ /**
+ * Media content to be displayed in the modal (image, video, Lottie, etc.).
+ */
children: React.ReactNode;
}
diff --git a/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/ModalMediaLayout.module.scss b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/ModalMediaLayout.module.scss
index d729853637..80609ce63f 100644
--- a/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/ModalMediaLayout.module.scss
+++ b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/ModalMediaLayout.module.scss
@@ -11,17 +11,6 @@
position: relative;
width: 100%;
flex-shrink: 0;
-
- height: var(--modal-top-media-height, 240px);
- @media (min-width: 1280px) {
- --modal-top-media-height: 260px;
- }
- @media (min-width: 1440px) {
- --modal-top-media-height: 260px;
- }
- @media (min-width: 1720px) {
- --modal-top-media-height: 320px;
- }
}
.header {
diff --git a/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/ModalMediaLayout.types.ts b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/ModalMediaLayout.types.ts
index 3d9481f36f..509075b87f 100644
--- a/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/ModalMediaLayout.types.ts
+++ b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/ModalMediaLayout.types.ts
@@ -2,5 +2,11 @@ import React from "react";
import { VibeComponentProps } from "../../../../types";
export interface ModalMediaLayoutProps extends VibeComponentProps {
+ /**
+ * Layout children in the following order:
+ * 1. Media content
+ * 2. Header content
+ * 3. Main content
+ */
children: React.ReactNode;
}
diff --git a/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/ModalMediaLayout.mdx b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/ModalMediaLayout.mdx
new file mode 100644
index 0000000000..d17590f2fa
--- /dev/null
+++ b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/ModalMediaLayout.mdx
@@ -0,0 +1,101 @@
+import { Meta } from "@storybook/blocks";
+import * as MediaModalStories from "./ModalMediaLayout.stories";
+import ratioDo from "./assets/ratio-do.png";
+import ratioDont from "./assets/ratio-dont.png";
+
+import styles from "../../../Modal/__stories__/Modal.stories.module.scss";
+import {
+ MODAL_BASIC_LAYOUT,
+ MODAL_SIDE_BY_SIDE_LAYOUT,
+ TIPSEEN
+} from "../../../../../storybook/components/related-components/component-description-map";
+import { MediaModalTip } from "./ModalMediaLayout.stories.helpers";
+
+
+
+# Media Modal
+
+- [Overview](#overview)
+- [Props](#props)
+- [Usage](#usage)
+- [Use cases and examples](#use-cases-and-examples)
+- [Do's and dont's](do's-and-don'ts)
+- [Related components](#related-components)
+- [Feedback](#feedback)
+
+## Overview
+
+The Media Modal includes a highlighted media section, followed by a textual content area. This modal is intended for cases when you need to catch the user’s attention using visual elements before they interact with the text. It’s ideal for introducing new features or short onboarding flows.
+
+
+
+## Props
+
+
+
+## Usage
+
+
+
+
+
+## Use cases and examples
+
+### Wizard footer
+
+When multi steps modal, use the "wizard footer". For more guidelines about the footer we recommend to check our ModalFooter page.
+
+
+
+### Header with extra icon button
+
+In case of a need of an icon button in the modal header, you can use our default header "Action slot".
+You can also use it as a menu button component.
+
+
+
+### Animation
+
+Each modal includes an animation type based on its entrance point, with wizard modals also featuring transition animations. The default is the element trigger animation, which can be replaced with a center pop animation if there's no specific trigger. Transition animation is used exclusively for wizard modals and cannot be changed or removed.
+
+
+
+## Do's and don'ts
+
+
+ ),
+ description: "Keep a balanced ratio between the media section, to the content and footer section."
+ },
+ negative: {
+ component: (
+
+ ),
+ description:
+ "Don't create a media that will be too small or too big for the modal width, as it create unbalanced look."
+ }
+ }
+ ]}
+/>
+
+## Related components
+
+
diff --git a/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/ModalMediaLayout.stories.helpers.tsx b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/ModalMediaLayout.stories.helpers.tsx
new file mode 100644
index 0000000000..bc860367fc
--- /dev/null
+++ b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/ModalMediaLayout.stories.helpers.tsx
@@ -0,0 +1,14 @@
+import React from "react";
+import { StorybookLink, Tip } from "vibe-storybook-components";
+
+export const MediaModalTip = () => (
+
+
+ If your content is scrollable or wide (you need more space), consider using{" "}
+
+ Basic modal
+
+ .
+
+
+);
diff --git a/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/ModalMediaLayout.stories.tsx b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/ModalMediaLayout.stories.tsx
new file mode 100644
index 0000000000..0f3662479c
--- /dev/null
+++ b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/ModalMediaLayout.stories.tsx
@@ -0,0 +1,252 @@
+import React, { useState } from "react";
+import { Meta, StoryObj } from "@storybook/react";
+import Modal from "../../../Modal/Modal";
+import { createStoryMetaSettingsDecorator } from "../../../../../storybook";
+import ModalHeader from "../../../ModalHeader/ModalHeader";
+import ModalContent from "../../../ModalContent/ModalContent";
+import ModalMedia from "../../ModalMedia";
+import mediaImage from "./assets/media-image.png";
+import ModalFooter from "../../../footers/ModalFooter/ModalFooter";
+import ModalMediaLayout from "../ModalMediaLayout";
+import Text from "../../../../Text/Text";
+import Link from "../../../../Link/Link";
+import useWizard from "../../../../../hooks/useWizard/useWizard";
+import TransitionView from "../../../../TransitionView/TransitionView";
+import ModalFooterWizard from "../../../footers/ModalFooterWizard/ModalFooterWizard";
+import IconButton from "../../../../IconButton/IconButton";
+import { Menu } from "@vibe/icons";
+import Flex from "../../../../Flex/Flex";
+import Button from "../../../../Button/Button";
+import { createPortal } from "react-dom";
+import { withOpenedModalPreview } from "../../../Modal/__stories__/Modal.stories.helpers";
+
+type Story = StoryObj;
+
+const metaSettings = createStoryMetaSettingsDecorator({
+ component: Modal
+});
+
+export default {
+ title: "Internal/Components/Modal [New]/Media modal",
+ component: Modal,
+ subcomponents: {
+ ModalMediaLayout,
+ ModalMedia,
+ ModalHeader,
+ ModalContent,
+ ModalFooter,
+ ModalFooterWizard,
+ TransitionView
+ },
+ argTypes: metaSettings.argTypes,
+ decorators: metaSettings.decorators,
+ tags: ["internal"]
+} satisfies Meta;
+
+export const Overview: Story = {
+ decorators: [
+ (Story, context) => withOpenedModalPreview(Story, { size: "large", isDocsView: context.viewMode === "docs" })
+ ],
+ render: (args, { show, setShow }) => {
+ return (
+ setShow(false)} {...args}>
+
+
+
+
+
+
+
+ The media modal is ideal for introducing new features or onboarding, the user can also{" "}
+ .
+
+
+
+ setShow(false) }}
+ secondaryButton={{ text: "Cancel", onClick: () => setShow(false) }}
+ />
+
+ );
+ },
+ parameters: {
+ docs: {
+ liveEdit: {
+ isEnabled: false
+ }
+ }
+ }
+};
+
+export const Wizard: Story = {
+ decorators: [
+ (Story, context) => withOpenedModalPreview(Story, { size: "large", isDocsView: context.viewMode === "docs" })
+ ],
+ render: (_, { show, setShow }) => {
+ const steps = [
+
+
+
+
+
+
+
+ We have made some changes to our modal component. Keep reading to see what improvements we made.
+
+
+ ,
+
+
+
+
+
+
+
+ Now the modal can also allow wizard process, when including stepper in the modal footer, it also contain an
+ animation.
+
+
+
+ ];
+
+ const { activeStep, direction, next, back, isFirstStep, goToStep } = useWizard({
+ stepCount: steps.length
+ });
+
+ return (
+ setShow(false)}>
+
+ {steps}
+
+
+
+ );
+ }
+};
+
+export const HeaderWithExtraIconButton: Story = {
+ decorators: [
+ (Story, context) => withOpenedModalPreview(Story, { size: "large", isDocsView: context.viewMode === "docs" })
+ ],
+ render: (_, { show, setShow }) => {
+ return (
+ }
+ size="medium"
+ onClose={() => setShow(false)}
+ >
+
+
+
+
+
+
+
+ The media modal is ideal for introducing new features or onboarding, the user can also{" "}
+ .
+
+
+
+ setShow(false) }}
+ secondaryButton={{ text: "Cancel", onClick: () => setShow(false) }}
+ />
+
+ );
+ }
+};
+
+export const Animation: Story = {
+ render: () => {
+ const [showCenterPop, setShowCenterPop] = useState(false);
+ const [showTransition, setShowTransition] = useState(false);
+
+ const transitionSteps = [
+
+
+
+
+
+
+
+ We have made some changes to our modal component. Keep reading to see what improvements we made.
+
+
+ ,
+
+
+
+
+
+
+
+ Now the modal can also allow wizard process, when including stepper in the modal footer, it also contain an
+ animation.
+
+
+
+ ];
+
+ const { activeStep, direction, next, back, isFirstStep, goToStep } = useWizard({
+ stepCount: transitionSteps.length
+ });
+
+ return (
+ <>
+
+
+
+
+ {createPortal(
+ setShowCenterPop(false)}>
+
+
+
+
+
+
+
+ The media modal is ideal for introducing new features or onboarding, the user can also{" "}
+ .
+
+
+
+ setShowCenterPop(false) }}
+ secondaryButton={{ text: "Cancel", onClick: () => setShowCenterPop(false) }}
+ />
+ ,
+ document.body
+ )}
+ {createPortal(
+ setShowTransition(false)}
+ >
+
+ {transitionSteps}
+
+
+ ,
+ document.body
+ )}
+ >
+ );
+ }
+};
diff --git a/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/ModalMediaLayoutRelatedComponent.tsx b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/ModalMediaLayoutRelatedComponent.tsx
new file mode 100644
index 0000000000..6dbc919d70
--- /dev/null
+++ b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/ModalMediaLayoutRelatedComponent.tsx
@@ -0,0 +1,24 @@
+import React, { useMemo } from "react";
+import { RelatedComponent } from "vibe-storybook-components";
+import relatedComponentImage from "./assets/related-component.png";
+
+export const ModalMediaLayoutRelatedComponent = () => {
+ const component = useMemo(() => {
+ return (
+
+ );
+ }, []);
+
+ return (
+
+ );
+};
diff --git a/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/assets/media-image.png b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/assets/media-image.png
new file mode 100644
index 0000000000..838801c9cf
Binary files /dev/null and b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/assets/media-image.png differ
diff --git a/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/assets/ratio-do.png b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/assets/ratio-do.png
new file mode 100644
index 0000000000..8394ed580c
Binary files /dev/null and b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/assets/ratio-do.png differ
diff --git a/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/assets/ratio-dont.png b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/assets/ratio-dont.png
new file mode 100644
index 0000000000..8a65236f3c
Binary files /dev/null and b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/assets/ratio-dont.png differ
diff --git a/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/assets/related-component.png b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/assets/related-component.png
new file mode 100644
index 0000000000..9e5479fe31
Binary files /dev/null and b/packages/core/src/components/ModalNew/layouts/ModalMediaLayout/__stories__/assets/related-component.png differ
diff --git a/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/ModalSideBySideLayout.types.ts b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/ModalSideBySideLayout.types.ts
index 1534fd491b..205179396c 100644
--- a/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/ModalSideBySideLayout.types.ts
+++ b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/ModalSideBySideLayout.types.ts
@@ -2,5 +2,11 @@ import { VibeComponentProps } from "../../../../types";
import React from "react";
export interface ModalSideBySideLayoutProps extends VibeComponentProps {
+ /**
+ * Layout children in the following order:
+ * 1. Header content
+ * 2. Main content
+ * 3. Media content
+ */
children: React.ReactNode;
}
diff --git a/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/ModalSideBySideLayout.mdx b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/ModalSideBySideLayout.mdx
new file mode 100644
index 0000000000..ad23b65fb8
--- /dev/null
+++ b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/ModalSideBySideLayout.mdx
@@ -0,0 +1,157 @@
+import { Meta } from "@storybook/blocks";
+import * as SideBySideModalStories from "./ModalSideBySideLayout.stories";
+import breakdownDo from "./assets/breakdown-do.png";
+import breakdownDont from "./assets/breakdown-dont.png";
+import columnsDo from "./assets/columns-do.png";
+import columnsDont from "./assets/columns-dont.png";
+import wizardDo from "./assets/wizard-do.png";
+import wizardDont from "./assets/wizard-dont.png";
+import styles from "../../../Modal/__stories__/Modal.stories.module.scss";
+import {
+ MODAL_BASIC_LAYOUT,
+ MODAL_MEDIA_LAYOUT,
+ TIPSEEN
+} from "../../../../../storybook/components/related-components/component-description-map";
+import { SideBySideModalTip } from "./ModalSideBySideLayout.stories.helpers";
+
+
+
+# Side-by-side modal
+
+- [Overview](#overview)
+- [Props](#props)
+- [Usage](#usage)
+- [Use cases and examples](#use-cases-and-examples)
+- [Do's and dont's](do's-and-don'ts)
+- [Related components](#related-components)
+- [Feedback](#feedback)
+
+## Overview
+
+The Side-by-side Modal offers a layout with two distinct sections. The left side is reserved for providing information or inputs, like text fields or dropdown. The right side is reserved for visual media that supports the content on the left, like an image that adds context. This layout works best when users need to reference visual elements alongside textual information.
+
+
+
+## Props
+
+
+
+## Usage
+
+
+
+
+
+## Use cases and examples
+
+### Wizard footer
+
+When multi steps modal, use the "wizard footer". For more guidelines about the footer we recommend to check our ModalFooter page.
+
+
+
+### Header with extra icon button
+
+In case of a need of an icon button in the modal header, you can use our default header "Action slot".
+You can also use it as a menu button component.
+
+
+
+### Animation
+
+Each modal includes an animation type based on its entrance point, with wizard modals also featuring transition animations. The default is the element trigger animation, which can be replaced with a center pop animation if there's no specific trigger. Transition animation is used exclusively for wizard modals and cannot be changed or removed.
+
+
+
+## Do's and don'ts
+
+
+ ),
+ description: "Split up processes with several tasks into distinct steps using our wizard modal footer."
+ },
+ negative: {
+ component: (
+
+ ),
+ description: "Don't use scrolling for side-by-side modals in case of several tasks."
+ }
+ },
+ {
+ componentContainerClassName: styles.largeComponentRule,
+ positive: {
+ component: (
+
+ ),
+ description: "The right side of the modal is for media content. You can remove it if you don't need it."
+ },
+ negative: {
+ component: (
+
+ ),
+ description: (
+ <>
+ Don't turn this modal into a two-column grid. If you don't need an image, consider using the{" "}
+ basic modal.
+ >
+ )
+ }
+ },
+ {
+ componentContainerClassName: styles.largeComponentRule,
+ positive: {
+ component: (
+
+ ),
+ description:
+ "When using a wizard modal, allow the user to complete the process and close the modal by leaving the last CTA enabled. "
+ },
+ negative: {
+ component: (
+
+ ),
+ description:
+ "Don’t finish the Tipseen wizard process with a disabled CTA. Also, when in first step, make sure the “Back” button is disabled."
+ }
+ }
+ ]}
+/>
+
+## Related components
+
+
diff --git a/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/ModalSideBySideLayout.stories.helpers.tsx b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/ModalSideBySideLayout.stories.helpers.tsx
new file mode 100644
index 0000000000..78e1aac6ff
--- /dev/null
+++ b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/ModalSideBySideLayout.stories.helpers.tsx
@@ -0,0 +1,14 @@
+import React from "react";
+import { StorybookLink, Tip } from "vibe-storybook-components";
+
+export const SideBySideModalTip = () => (
+
+
+ If your content is scrollable consider using{" "}
+
+ Basic modal
+
+ .
+
+
+);
diff --git a/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/ModalSideBySideLayout.stories.tsx b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/ModalSideBySideLayout.stories.tsx
new file mode 100644
index 0000000000..8090d9e792
--- /dev/null
+++ b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/ModalSideBySideLayout.stories.tsx
@@ -0,0 +1,369 @@
+import React, { useRef, useState } from "react";
+import { Meta, StoryObj } from "@storybook/react";
+import Modal from "../../../Modal/Modal";
+import { createStoryMetaSettingsDecorator } from "../../../../../storybook";
+import { withOpenedModalPreview } from "../../../Modal/__stories__/Modal.stories.helpers";
+import ModalHeader from "../../../ModalHeader/ModalHeader";
+import ModalContent from "../../../ModalContent/ModalContent";
+import ModalSideBySideLayout from "../ModalSideBySideLayout";
+import ModalMedia from "../../ModalMedia";
+import mediaImage from "./assets/media-image.png";
+import useWizard from "../../../../../hooks/useWizard/useWizard";
+import TransitionView from "../../../../TransitionView/TransitionView";
+import ModalFooterWizard from "../../../footers/ModalFooterWizard/ModalFooterWizard";
+import TextField from "../../../../TextField/TextField";
+import Flex from "../../../../Flex/Flex";
+import Dropdown from "../../../../Dropdown/Dropdown";
+import FieldLabel from "../../../../FieldLabel/FieldLabel";
+import IconButton from "../../../../IconButton/IconButton";
+import { Menu } from "@vibe/icons";
+import ModalFooter from "../../../footers/ModalFooter/ModalFooter";
+import Button from "../../../../Button/Button";
+import { createPortal } from "react-dom";
+import Text from "../../../../Text/Text";
+import Link from "../../../../Link/Link";
+
+type Story = StoryObj;
+
+const metaSettings = createStoryMetaSettingsDecorator({
+ component: Modal
+});
+
+export default {
+ title: "Internal/Components/Modal [New]/Side by side modal",
+ component: Modal,
+ subcomponents: {
+ ModalSideBySideLayout,
+ ModalMedia,
+ ModalHeader,
+ ModalContent,
+ ModalFooter,
+ ModalFooterWizard,
+ TransitionView
+ },
+ argTypes: metaSettings.argTypes,
+ decorators: metaSettings.decorators,
+ tags: ["internal"]
+} satisfies Meta;
+
+export const Overview: Story = {
+ decorators: [
+ (Story, context) => withOpenedModalPreview(Story, { size: "medium", isDocsView: context.viewMode === "docs" })
+ ],
+ render: (args, { show, setShow }) => {
+ const steps = [
+
+
+ Modal subtitle, can come with icon
+
+ }
+ />
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs. Please make
+ sure that the content is clear for completing the relevant task.
+
+
+
+
+
+ ,
+
+
+ Modal subtitle, can come with icon
+
+ }
+ />
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs. Please make
+ sure that the content is clear for completing the relevant task.
+
+
+
+
+
+
+ ];
+
+ const { activeStep, direction, next, back, isFirstStep, goToStep } = useWizard({
+ stepCount: steps.length
+ });
+
+ return (
+ setShow(false)} style={{ height: 400 }} {...args}>
+
+ {steps}
+
+
+
+ );
+ },
+ parameters: {
+ docs: {
+ liveEdit: {
+ isEnabled: false
+ }
+ }
+ }
+};
+
+export const Wizard: Story = {
+ decorators: [
+ (Story, context) => withOpenedModalPreview(Story, { size: "medium", isDocsView: context.viewMode === "docs" })
+ ],
+ render: (_, { show, setShow }) => {
+ const dropdownOptions = [
+ {
+ label: "English",
+ value: "en"
+ },
+ {
+ label: "Hebrew",
+ value: "he"
+ }
+ ];
+
+ const steps = [
+
+
+
+
+
+
+
+
+
+
+
+ ,
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ];
+
+ const { activeStep, direction, next, back, isFirstStep, goToStep } = useWizard({
+ stepCount: steps.length
+ });
+
+ return (
+ setShow(false)} style={{ height: 400 }}>
+
+ {steps}
+
+
+
+ );
+ }
+};
+
+export const HeaderWithExtraIconButton: Story = {
+ decorators: [
+ (Story, context) => withOpenedModalPreview(Story, { size: "medium", isDocsView: context.viewMode === "docs" })
+ ],
+ render: (_, { show, setShow }) => {
+ return (
+ }
+ size="large"
+ onClose={() => setShow(false)}
+ style={{ height: 400 }}
+ >
+
+
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs. Please
+ make sure that the content is clear for completing the relevant task.
+
+
+
+
+
+
+ setShow(false) }}
+ secondaryButton={{ text: "Cancel", onClick: () => setShow(false) }}
+ />
+
+ );
+ }
+};
+
+export const Animation: Story = {
+ render: () => {
+ const [showAnchor, setShowAnchor] = useState(false);
+ const [showCenterPop, setShowCenterPop] = useState(false);
+ const [showTransition, setShowTransition] = useState(false);
+
+ const anchorButtonRef = useRef(null);
+
+ const transitionSteps = [
+
+
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs. Please make
+ sure that the content is clear for completing the relevant task.
+
+
+
+
+
+ ,
+
+
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs. Please make
+ sure that the content is clear for completing the relevant task.
+
+
+
+
+
+ ,
+
+
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs. Please make
+ sure that the content is clear for completing the relevant task.
+
+
+
+
+
+
+ ];
+
+ const { activeStep, direction, next, back, isFirstStep, goToStep } = useWizard({
+ stepCount: transitionSteps.length
+ });
+
+ return (
+ <>
+
+
+
+
+
+ {createPortal(
+ setShowAnchor(false)}
+ style={{ height: 400 }}
+ >
+
+
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs.
+ Please make sure that the content is clear for completing the relevant task.
+
+
+
+
+
+
+ setShowAnchor(false) }}
+ secondaryButton={{ text: "Cancel", onClick: () => setShowAnchor(false) }}
+ />
+ ,
+ document.body
+ )}
+ {createPortal(
+ setShowCenterPop(false)}
+ style={{ height: 400 }}
+ >
+
+
+
+
+ Modal content will appear here, you can custom it however you want, according to the user needs.
+ Please make sure that the content is clear for completing the relevant task.
+
+
+
+
+
+
+ setShowCenterPop(false) }}
+ secondaryButton={{ text: "Cancel", onClick: () => setShowCenterPop(false) }}
+ />
+ ,
+ document.body
+ )}
+ {createPortal(
+ setShowTransition(false)}
+ style={{ height: 400 }}
+ >
+
+ {transitionSteps}
+
+
+ ,
+ document.body
+ )}
+ >
+ );
+ }
+};
diff --git a/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/ModalSideBySideLayoutRelatedComponent.tsx b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/ModalSideBySideLayoutRelatedComponent.tsx
new file mode 100644
index 0000000000..33bd78654f
--- /dev/null
+++ b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/ModalSideBySideLayoutRelatedComponent.tsx
@@ -0,0 +1,24 @@
+import React, { useMemo } from "react";
+import { RelatedComponent } from "vibe-storybook-components";
+import relatedComponentImage from "./assets/related-component.png";
+
+export const ModalSideBySideLayoutRelatedComponent = () => {
+ const component = useMemo(() => {
+ return (
+
+ );
+ }, []);
+
+ return (
+
+ );
+};
diff --git a/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/breakdown-do.png b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/breakdown-do.png
new file mode 100644
index 0000000000..31c8267988
Binary files /dev/null and b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/breakdown-do.png differ
diff --git a/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/breakdown-dont.png b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/breakdown-dont.png
new file mode 100644
index 0000000000..ff93a91e29
Binary files /dev/null and b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/breakdown-dont.png differ
diff --git a/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/columns-do.png b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/columns-do.png
new file mode 100644
index 0000000000..694392b327
Binary files /dev/null and b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/columns-do.png differ
diff --git a/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/columns-dont.png b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/columns-dont.png
new file mode 100644
index 0000000000..97197cecb6
Binary files /dev/null and b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/columns-dont.png differ
diff --git a/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/media-image.png b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/media-image.png
new file mode 100644
index 0000000000..9981ce2496
Binary files /dev/null and b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/media-image.png differ
diff --git a/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/related-component.png b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/related-component.png
new file mode 100644
index 0000000000..bb2f2af2b1
Binary files /dev/null and b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/related-component.png differ
diff --git a/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/wizard-do.png b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/wizard-do.png
new file mode 100644
index 0000000000..84b83a886f
Binary files /dev/null and b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/wizard-do.png differ
diff --git a/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/wizard-dont.png b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/wizard-dont.png
new file mode 100644
index 0000000000..fca73f6772
Binary files /dev/null and b/packages/core/src/components/ModalNew/layouts/ModalSideBySideLayout/__stories__/assets/wizard-dont.png differ
diff --git a/packages/core/src/storybook/components/related-components/component-description-map.tsx b/packages/core/src/storybook/components/related-components/component-description-map.tsx
index 6f218e0c5c..74029fd604 100644
--- a/packages/core/src/storybook/components/related-components/component-description-map.tsx
+++ b/packages/core/src/storybook/components/related-components/component-description-map.tsx
@@ -59,6 +59,9 @@ import { BoxDescription } from "./descriptions/box-description";
import { TableDescription } from "./descriptions/table-description";
import { VirtualizedGridDescription } from "./descriptions/virtualized-grid-description/virtualized-grid-description";
import { MenuGridItemDescription } from "./descriptions/menu-grid-item-description";
+import { ModalMediaLayoutRelatedComponent } from "../../../components/ModalNew/layouts/ModalMediaLayout/__stories__/ModalMediaLayoutRelatedComponent";
+import { ModalSideBySideLayoutRelatedComponent } from "../../../components/ModalNew/layouts/ModalSideBySideLayout/__stories__/ModalSideBySideLayoutRelatedComponent";
+import { ModalBasicLayoutRelatedComponent } from "../../../components/ModalNew/layouts/ModalBasicLayout/__stories__/ModalBasicLayoutRelatedComponent";
export const SPLIT_BUTTON = "split-button";
export const BUTTON_GROUP = "button-group";
@@ -81,6 +84,9 @@ export const TOAST = "toast";
export const BADGE = "badge";
export const MULTI_STEP_INDICATOR = "wizard";
export const TIPSEEN = "tipseen";
+export const MODAL_BASIC_LAYOUT = "modal-basic-layout";
+export const MODAL_SIDE_BY_SIDE_LAYOUT = "modal-side-by-side-layout";
+export const MODAL_MEDIA_LAYOUT = "modal-media-layout";
export const TEXT_FIELD = "text-field";
export const SEARCH = "search";
export const COMBOBOX = "combobox";
@@ -153,6 +159,9 @@ const COMPONENTS_DESCRIPTIONS_ENTRIES: [string, JSX.Element][] = [
[STEPS, ],
[SPINNER, ],
[SKELETON, ],
+ [MODAL_BASIC_LAYOUT, ],
+ [MODAL_SIDE_BY_SIDE_LAYOUT, ],
+ [MODAL_MEDIA_LAYOUT, ],
[SLIDER, ],
[ICON_BUTTON, ],
[MENU_BUTTON, ],