Skip to content

docs(Modal): stories enhancements, add a11y section #2686

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

Merged
merged 8 commits into from
Dec 30, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions packages/core/src/components/Modal/Modal/__stories__/Modal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@ import {
- [Overview](#overview)
- [Props](#props)
- [Usage](#usage)
- [Do's and dont's](do's-and-don'ts)
- [Accessibility](#accessibility)
- [Do's and dont's](#dos-and-donts)
- [Related components](#related-components)
- [Feedback](#feedback)

## Overview

Modals help users focus on a single task or a piece of information by popping up and blocking the rest of the page’s content. Modals disappear when user complete a required action or dismiss it. Use modals for quick, infrequent tasks.

We have 3 different modal component, each one provide a different layout for a different use case:
<p style={{ marginBlockEnd: "var(--sb-spacing-xxxl)" }}>
Modals help users focus on a single task or a piece of information by popping up and blocking the rest of the page’s
content. Modals disappear when user complete a required action or dismiss it. Use modals for quick, infrequent tasks.
We have 3 different modal component, each one provide a different layout for a different use case:
</p>

### Basic modal

Expand Down Expand Up @@ -66,6 +69,27 @@ The <StorybookLink page="Components/Modal [New]/Media modal">Media Modal</Storyb

<ModalTip />

## Accessibility

The Modal component provides several built-in enhancements to simplify usage and improve accessibility:

<UsageGuidelines
guidelines={[
<span>
<b>Scroll lock:</b> While the modal is open, it prevents background content from scrolling, ensuring user focus
remains on the modal.",
</span>,
<span>
<b>Focus lock:</b> Keeps focus within the modal elements, preventing users from tabbing outside of the modal while
it is open. Focus also automatically returns to the last focused element upon closing.
</span>,
<span>
<b>Aria attributes:</b> For better screen reader support, while using the <code>ModalHeader</code> it
automatically sets <code>aria-labelledby</code> and <code>aria-describedby</code> on the modal.
</span>
]}
/>

## Do's and don'ts

<ComponentRules
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { forwardRef, useEffect, useRef, useState } from "react";
import React, { forwardRef, useEffect, useState } from "react";
import Button from "../../../Button/Button";
import IconButton from "../../../IconButton/IconButton";
import { Fullscreen } from "@vibe/icons";
import { StorybookLink, Tip } from "vibe-storybook-components";
import cx from "classnames";
import styles from "./Modal.stories.module.scss";
Expand All @@ -10,12 +12,18 @@ export const OpenedModalPreview = forwardRef(
{
onOpenModalClick,
isDocsView,
isFullView,
size = "small",
showFullPreviewButton,
onFullPreviewClick,
children: modal
}: {
onOpenModalClick: () => void;
isDocsView?: boolean;
isFullView?: boolean;
size?: "small" | "medium" | "large";
showFullPreviewButton?: boolean;
onFullPreviewClick: () => void;
children: React.ReactNode;
},
ref: React.ForwardedRef<HTMLDivElement>
Expand All @@ -24,19 +32,30 @@ export const OpenedModalPreview = forwardRef(
<div
className={cx(styles.preview, { [getStyle(styles, size)]: isDocsView })}
ref={ref}
// workaround to prevent modal from autofocusing on page load
{...(isDocsView && { "data-no-autofocus": true })}
// workaround to prevent modal from autofocusing on page load (unless on full view)
{...(isDocsView && !isFullView && { "data-no-autofocus": true })}
>
<Button onClick={onOpenModalClick}>Open Modal</Button>
{modal}
{showFullPreviewButton && (
<IconButton
wrapperClassName={styles.fullPreviewButtonWrapper}
className={styles.fullPreviewButton}
kind="secondary"
icon={Fullscreen}
color="primary"
onClick={onFullPreviewClick}
ariaLabel="Open modal in full preview mode"
/>
)}
</div>
);
}
);

export const useRemoveModalScrollLock = (show: boolean, isDocsView?: boolean) => {
export const useRemoveModalScrollLock = (show: boolean, isDocsView?: boolean, isFullView?: boolean) => {
useEffect(() => {
if (show && document.body.attributes.getNamedItem("data-scroll-locked") && isDocsView) {
if (show && document.body.attributes.getNamedItem("data-scroll-locked") && isDocsView && !isFullView) {
requestAnimationFrame(() => {
document.body.attributes.removeNamedItem("data-scroll-locked");
document.documentElement.addEventListener(
Expand All @@ -48,32 +67,49 @@ export const useRemoveModalScrollLock = (show: boolean, isDocsView?: boolean) =>
);
});
}
}, [show, isDocsView]);
}, [show, isDocsView, isFullView]);
};

export function withOpenedModalPreview(
Story: React.FunctionComponent<{
show: boolean;
setShow: (show: boolean) => void;
container?: React.RefObject<HTMLElement>;
container?: Element | DocumentFragment;
}>,
{ size, isDocsView }: { size?: "small" | "medium" | "large"; isDocsView: boolean }
{
size,
isDocsView,
allowFullViewInDocs
}: { size?: "small" | "medium" | "large"; isDocsView: boolean; allowFullViewInDocs?: boolean }
) {
const [show, setShow] = useState(true);
const container = useRef<HTMLElement>(null);
useRemoveModalScrollLock(show, isDocsView); // internal hook, for documentation purposes, to enable page scroll on docs view
const [isFullView, setFullView] = useState(false);
useRemoveModalScrollLock(show, isDocsView, isFullView); // internal hook, for documentation purposes, to enable page scroll on docs view

const [modalContainer, setModalContainer] = useState<Element | DocumentFragment>(null);

return (
// internal component, for documentation purposes, to open modal inside a container
<OpenedModalPreview
size={size}
onOpenModalClick={() => setShow(true)}
onOpenModalClick={() => {
setShow(true);
setFullView(false);
}}
isDocsView={isDocsView}
isFullView={isFullView}
showFullPreviewButton={allowFullViewInDocs && isDocsView && !isFullView && show}
onFullPreviewClick={() => {
setShow(false);
setFullView(true);
setTimeout(() => setShow(true), 250);
}}
ref={element => {
isDocsView ? (container.current = element) : (container.current = document.body);
if (!element || !isDocsView || isFullView) return;
setModalContainer(element);
}}
>
<Story show={show} setShow={setShow} container={container} />
<Story show={show} setShow={setShow} container={isFullView ? document.body : modalContainer} />
</OpenedModalPreview>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,26 @@
width: 100%;
container-type: inline-size;

.fullPreviewButtonWrapper {
position: absolute;
right: 8px;
top: 8px;
z-index: 10001;
}

.fullPreviewButton {
background: var(--sb-dark-background-color);

&:hover {
background: linear-gradient(
to right,
var(--sb-primary-background-hover-color),
var(--sb-primary-background-hover-color)
),
linear-gradient(to right, var(--sb-dark-background-color), var(--sb-dark-background-color)) !important;
}
}

&.small {
height: 360px;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const usePortalTarget = (portalTarget?: PortalTarget): Element | DocumentFragmen

const target = resolveTarget();
setResolvedTarget(target);
}, [portalTarget, resolvedTarget]);
}, [portalTarget]);

return resolvedTarget;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ export default {
} satisfies Meta<typeof Modal>;

export const Overview: Story = {
decorators: [(Story, context) => withOpenedModalPreview(Story, { isDocsView: context.viewMode === "docs" })],
decorators: [
(Story, context) =>
withOpenedModalPreview(Story, { isDocsView: context.viewMode === "docs", allowFullViewInDocs: true })
],
render: (args, { show, setShow, container }) => {
return (
<Modal id="modal-basic" show={show} size="medium" onClose={() => setShow(false)} container={container} {...args}>
Expand Down Expand Up @@ -255,10 +258,12 @@ export const Wizard: Story = {
</ModalBasicLayout>
];

const { activeStep, direction, next, back, isFirstStep, goToStep } = useWizard({
const { activeStep, direction, next, back, isFirstStep, isLastStep, goToStep } = useWizard({
stepCount: steps.length
});

const primaryButtonText = isLastStep ? "Confirm" : "Next";

return (
<Modal id="modal-basic" show={show} size="medium" onClose={() => setShow(false)} container={container}>
<TransitionView activeStep={activeStep} direction={direction}>
Expand All @@ -268,7 +273,7 @@ export const Wizard: Story = {
activeStep={activeStep}
stepCount={steps.length}
onStepClick={goToStep}
primaryButton={{ text: "Next", onClick: next }}
primaryButton={{ text: primaryButtonText, onClick: next }}
secondaryButton={{ text: "Back", onClick: back, disabled: isFirstStep }}
/>
</Modal>
Expand Down Expand Up @@ -308,7 +313,9 @@ export const FooterWithExtraContent: Story = {
};

export const Confirmation: Story = {
decorators: [(Story, context) => withOpenedModalPreview(Story, { isDocsView: context.viewMode === "docs" })],
decorators: [
(Story, context) => withOpenedModalPreview(Story, { size: "large", isDocsView: context.viewMode === "docs" })
],
render: (_, { show, setShow, container }) => {
return (
<Modal id="modal-basic" show={show} size="small" onClose={() => setShow(false)} container={container}>
Expand Down Expand Up @@ -406,7 +413,7 @@ export const Animation: Story = {
</ModalBasicLayout>
];

const { activeStep, direction, next, back, isFirstStep, goToStep } = useWizard({
const { activeStep, direction, next, back, isFirstStep, isLastStep, goToStep } = useWizard({
stepCount: transitionSteps.length
});

Expand Down Expand Up @@ -480,7 +487,7 @@ export const Animation: Story = {
activeStep={activeStep}
stepCount={transitionSteps.length}
onStepClick={goToStep}
primaryButton={{ text: "Next", onClick: next }}
primaryButton={{ text: isLastStep ? "Confirm" : "Next", onClick: next }}
secondaryButton={{ text: "Back", onClick: back, disabled: isFirstStep }}
/>
</Modal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ export default {

export const Overview: Story = {
decorators: [
(Story, context) => withOpenedModalPreview(Story, { size: "large", isDocsView: context.viewMode === "docs" })
(Story, context) =>
withOpenedModalPreview(Story, {
size: "large",
isDocsView: context.viewMode === "docs",
allowFullViewInDocs: true
})
],
render: (args, { show, setShow, container }) => {
return (
Expand Down Expand Up @@ -115,10 +120,12 @@ export const Wizard: Story = {
</ModalMediaLayout>
];

const { activeStep, direction, next, back, isFirstStep, goToStep } = useWizard({
const { activeStep, direction, next, back, isFirstStep, isLastStep, goToStep } = useWizard({
stepCount: steps.length
});

const primaryButtonText = isLastStep ? "Confirm" : "Next";

return (
<Modal id="modal-media" show={show} size="medium" onClose={() => setShow(false)} container={container}>
<TransitionView activeStep={activeStep} direction={direction}>
Expand All @@ -128,7 +135,7 @@ export const Wizard: Story = {
activeStep={activeStep}
stepCount={steps.length}
onStepClick={goToStep}
primaryButton={{ text: "Next", onClick: next }}
primaryButton={{ text: primaryButtonText, onClick: next }}
secondaryButton={{ text: "Back", onClick: back, disabled: isFirstStep }}
/>
</Modal>
Expand Down Expand Up @@ -230,7 +237,7 @@ export const Animation: Story = {
</ModalMediaLayout>
];

const { activeStep, direction, next, back, isFirstStep, goToStep } = useWizard({
const { activeStep, direction, next, back, isFirstStep, isLastStep, goToStep } = useWizard({
stepCount: transitionSteps.length
});

Expand Down Expand Up @@ -268,7 +275,7 @@ export const Animation: Story = {
activeStep={activeStep}
stepCount={transitionSteps.length}
onStepClick={goToStep}
primaryButton={{ text: "Next", onClick: next }}
primaryButton={{ text: isLastStep ? "Confirm" : "Next", onClick: next }}
secondaryButton={{ text: "Back", onClick: back, disabled: isFirstStep }}
/>
</Modal>
Expand Down
Loading
Loading