feat: wds modal improvements (#28829)
## Description - Add the ability to use a dialog without a trigger component. - Add the ability to pass a triggerRef to the component that helps to set the correct and aria attributes to trigger. #### PR fixes following issue(s) Fixes #28814 #### Type of change - New feature (non-breaking change which adds functionality) #### How Has This Been Tested? > Please describe the tests that you ran to verify your changes. Also list any relevant details for your test configuration. > Delete anything that is not relevant - [x] Manual - [ ] JUnit - [ ] Jest - [ ] Cypress ## Checklist: #### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag
This commit is contained in:
parent
90bb8532a0
commit
3196b9c452
|
|
@ -1,9 +1,8 @@
|
||||||
import React, { forwardRef } from "react";
|
import React, { forwardRef, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
FloatingFocusManager,
|
FloatingFocusManager,
|
||||||
FloatingPortal,
|
FloatingPortal,
|
||||||
useMergeRefs,
|
useMergeRefs,
|
||||||
FloatingOverlay,
|
|
||||||
useTransitionStatus,
|
useTransitionStatus,
|
||||||
} from "@floating-ui/react";
|
} from "@floating-ui/react";
|
||||||
import { usePopoverContext } from "./PopoverContext";
|
import { usePopoverContext } from "./PopoverContext";
|
||||||
|
|
@ -16,53 +15,32 @@ const _PopoverContent = (props: PopoverContentProps, ref: Ref<HTMLElement>) => {
|
||||||
children,
|
children,
|
||||||
closeOnFocusOut = true,
|
closeOnFocusOut = true,
|
||||||
contentClassName,
|
contentClassName,
|
||||||
overlayClassName,
|
|
||||||
style,
|
style,
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
const { context, descriptionId, duration, getFloatingProps, labelId, modal } =
|
const {
|
||||||
usePopoverContext();
|
context,
|
||||||
|
descriptionId,
|
||||||
|
duration,
|
||||||
|
getFloatingProps,
|
||||||
|
labelId,
|
||||||
|
onClose,
|
||||||
|
} = usePopoverContext();
|
||||||
const refs = useMergeRefs([context.refs.setFloating, ref]);
|
const refs = useMergeRefs([context.refs.setFloating, ref]);
|
||||||
const { isMounted, status } = useTransitionStatus(context, { duration });
|
const { isMounted, status } = useTransitionStatus(context, { duration });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMounted && status === "close" && onClose) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, [isMounted, status]);
|
||||||
|
|
||||||
if (!Boolean(isMounted)) return null;
|
if (!Boolean(isMounted)) return null;
|
||||||
|
|
||||||
const root = context.refs.domReference.current?.closest(
|
const root = context.refs.domReference.current?.closest(
|
||||||
"[data-theme-provider]",
|
"[data-theme-provider]",
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
|
|
||||||
const content = (
|
|
||||||
<div
|
|
||||||
aria-describedby={descriptionId}
|
|
||||||
aria-labelledby={labelId}
|
|
||||||
className={contentClassName}
|
|
||||||
data-status={status}
|
|
||||||
ref={refs}
|
|
||||||
style={{
|
|
||||||
...(modal ? {} : context.floatingStyles),
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
{...getFloatingProps(rest)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (modal) {
|
|
||||||
return (
|
|
||||||
<FloatingPortal root={root}>
|
|
||||||
<FloatingOverlay className={overlayClassName} data-status={status}>
|
|
||||||
<FloatingFocusManager
|
|
||||||
closeOnFocusOut={closeOnFocusOut}
|
|
||||||
context={context}
|
|
||||||
modal
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</FloatingFocusManager>
|
|
||||||
</FloatingOverlay>
|
|
||||||
</FloatingPortal>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
return (
|
||||||
<FloatingPortal root={root}>
|
<FloatingPortal root={root}>
|
||||||
<FloatingFocusManager
|
<FloatingFocusManager
|
||||||
|
|
@ -70,11 +48,23 @@ const _PopoverContent = (props: PopoverContentProps, ref: Ref<HTMLElement>) => {
|
||||||
context={context}
|
context={context}
|
||||||
modal={false}
|
modal={false}
|
||||||
>
|
>
|
||||||
{content}
|
<div
|
||||||
|
aria-describedby={descriptionId}
|
||||||
|
aria-labelledby={labelId}
|
||||||
|
className={contentClassName}
|
||||||
|
data-status={status}
|
||||||
|
ref={refs}
|
||||||
|
style={{
|
||||||
|
...context.floatingStyles,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
{...getFloatingProps(rest)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</FloatingFocusManager>
|
</FloatingFocusManager>
|
||||||
</FloatingPortal>
|
</FloatingPortal>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PopoverContent = forwardRef(_PopoverContent);
|
export const PopoverContent = forwardRef(_PopoverContent);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
import React, { forwardRef, useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
FloatingFocusManager,
|
||||||
|
FloatingPortal,
|
||||||
|
useMergeRefs,
|
||||||
|
FloatingOverlay,
|
||||||
|
useTransitionStatus,
|
||||||
|
} from "@floating-ui/react";
|
||||||
|
import { usePopoverContext } from "./PopoverContext";
|
||||||
|
|
||||||
|
import type { Ref } from "react";
|
||||||
|
import type { PopoverModalContentProps } from "./types";
|
||||||
|
|
||||||
|
const setAriaAttrs = (
|
||||||
|
triggerElem: HTMLElement,
|
||||||
|
referenceProps: Record<string, unknown>,
|
||||||
|
) => {
|
||||||
|
if (Boolean(referenceProps["aria-controls"])) {
|
||||||
|
triggerElem?.setAttribute(
|
||||||
|
"aria-controls",
|
||||||
|
referenceProps["aria-controls"] as string,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerElem?.setAttribute(
|
||||||
|
"aria-expanded",
|
||||||
|
referenceProps["aria-expanded"] as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
triggerElem?.setAttribute(
|
||||||
|
"aria-haspopup",
|
||||||
|
referenceProps["aria-haspopup"] as string,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _PopoverModalContent = (
|
||||||
|
props: PopoverModalContentProps,
|
||||||
|
ref: Ref<HTMLElement>,
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
closeOnFocusOut = true,
|
||||||
|
contentClassName,
|
||||||
|
overlayClassName,
|
||||||
|
style,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
const {
|
||||||
|
context,
|
||||||
|
descriptionId,
|
||||||
|
duration,
|
||||||
|
getFloatingProps,
|
||||||
|
getReferenceProps,
|
||||||
|
initialFocus,
|
||||||
|
labelId,
|
||||||
|
onClose,
|
||||||
|
triggerRef,
|
||||||
|
} = usePopoverContext();
|
||||||
|
const refs = useMergeRefs([context.refs.setFloating, ref]);
|
||||||
|
const [root, setRoot] = useState<Element | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (triggerRef?.current != null) {
|
||||||
|
setRoot(triggerRef.current?.closest("[data-theme-provider]"));
|
||||||
|
} else {
|
||||||
|
setRoot(document.body.querySelector("[data-theme-provider]"));
|
||||||
|
}
|
||||||
|
}, [triggerRef?.current]);
|
||||||
|
|
||||||
|
const referenceProps = getReferenceProps({ ref: refs });
|
||||||
|
useEffect(() => {
|
||||||
|
if (triggerRef?.current != null) {
|
||||||
|
setAriaAttrs(triggerRef?.current, referenceProps);
|
||||||
|
}
|
||||||
|
}, [referenceProps, triggerRef?.current]);
|
||||||
|
|
||||||
|
const { isMounted, status } = useTransitionStatus(context, { duration });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMounted && status === "close" && onClose) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, [isMounted, status]);
|
||||||
|
|
||||||
|
if (!Boolean(isMounted)) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FloatingPortal root={root as HTMLElement}>
|
||||||
|
<FloatingOverlay className={overlayClassName} data-status={status}>
|
||||||
|
<FloatingFocusManager
|
||||||
|
closeOnFocusOut={closeOnFocusOut}
|
||||||
|
context={context}
|
||||||
|
initialFocus={initialFocus}
|
||||||
|
modal
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-describedby={descriptionId}
|
||||||
|
aria-labelledby={labelId}
|
||||||
|
className={contentClassName}
|
||||||
|
data-status={status}
|
||||||
|
ref={refs}
|
||||||
|
style={style}
|
||||||
|
{...getFloatingProps(rest)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</FloatingFocusManager>
|
||||||
|
</FloatingOverlay>
|
||||||
|
</FloatingPortal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PopoverModalContent = forwardRef(_PopoverModalContent);
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export { Popover } from "./Popover";
|
export { Popover } from "./Popover";
|
||||||
export { PopoverContent } from "./PopoverContent";
|
export { PopoverContent } from "./PopoverContent";
|
||||||
|
export { PopoverModalContent } from "./PopoverModalContent";
|
||||||
export { PopoverTrigger } from "./PopoverTrigger";
|
export { PopoverTrigger } from "./PopoverTrigger";
|
||||||
export { usePopoverContext } from "./PopoverContext";
|
export { usePopoverContext } from "./PopoverContext";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { MutableRefObject } from "react";
|
||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
|
@ -42,6 +43,10 @@ export interface PopoverProps {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
/** Open and close animation durations. */
|
/** Open and close animation durations. */
|
||||||
duration?: number;
|
duration?: number;
|
||||||
|
/** Ref of trigger element. This ref is necessary for adding aria attributes to trigger */
|
||||||
|
triggerRef?: MutableRefObject<HTMLElement | null>;
|
||||||
|
/** Which element to initially focus. Can be either a number (tabbable index as specified by the order) or a ref. */
|
||||||
|
initialFocus?: number | MutableRefObject<HTMLElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PopoverContentProps {
|
export interface PopoverContentProps {
|
||||||
|
|
@ -54,12 +59,15 @@ export interface PopoverContentProps {
|
||||||
closeOnFocusOut?: boolean;
|
closeOnFocusOut?: boolean;
|
||||||
/** Sets inline [style](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style) for the element. Only use as a **last resort**. Use style props instead. */
|
/** Sets inline [style](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style) for the element. Only use as a **last resort**. Use style props instead. */
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
/** Sets the CSS className for the overlay. Only use as a **last resort**. */
|
|
||||||
overlayClassName?: string;
|
|
||||||
/** Sets the CSS className for the content popover. Only use as a **last resort**. */
|
/** Sets the CSS className for the content popover. Only use as a **last resort**. */
|
||||||
contentClassName?: string;
|
contentClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PopoverModalContentProps extends PopoverContentProps {
|
||||||
|
/** Sets the CSS className for the overlay. Only use as a **last resort**. */
|
||||||
|
overlayClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PopoverTriggerProps {
|
export interface PopoverTriggerProps {
|
||||||
/** The children of the component. */
|
/** The children of the component. */
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { UseFloatingOptions } from "@floating-ui/react/src/types";
|
import type { UseFloatingOptions } from "@floating-ui/react/src/types";
|
||||||
import React, { useState } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
autoUpdate,
|
autoUpdate,
|
||||||
flip,
|
flip,
|
||||||
|
|
@ -18,17 +18,18 @@ const DEFAULT_POPOVER_OFFSET = 10;
|
||||||
export function usePopover({
|
export function usePopover({
|
||||||
defaultOpen = false,
|
defaultOpen = false,
|
||||||
duration = 0,
|
duration = 0,
|
||||||
|
initialFocus,
|
||||||
isOpen: controlledOpen,
|
isOpen: controlledOpen,
|
||||||
modal = false,
|
modal = false,
|
||||||
offset: offsetProp = DEFAULT_POPOVER_OFFSET,
|
offset: offsetProp = DEFAULT_POPOVER_OFFSET,
|
||||||
onClose,
|
onClose,
|
||||||
placement = "bottom",
|
placement = "bottom",
|
||||||
setOpen: setControlledOpen,
|
setOpen: setControlledOpen,
|
||||||
|
triggerRef,
|
||||||
}: PopoverProps = {}) {
|
}: PopoverProps = {}) {
|
||||||
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
||||||
const [labelId, setLabelId] = useState<string | undefined>();
|
const [labelId, setLabelId] = useState<string | undefined>();
|
||||||
const [descriptionId, setDescriptionId] = useState<string | undefined>();
|
const [descriptionId, setDescriptionId] = useState<string | undefined>();
|
||||||
|
|
||||||
const open = controlledOpen ?? uncontrolledOpen;
|
const open = controlledOpen ?? uncontrolledOpen;
|
||||||
const setOpen = setControlledOpen ?? setUncontrolledOpen;
|
const setOpen = setControlledOpen ?? setUncontrolledOpen;
|
||||||
|
|
||||||
|
|
@ -48,13 +49,6 @@ export function usePopover({
|
||||||
const data = useFloating({
|
const data = useFloating({
|
||||||
open,
|
open,
|
||||||
onOpenChange: setOpen,
|
onOpenChange: setOpen,
|
||||||
whileElementsMounted: () => {
|
|
||||||
return () => {
|
|
||||||
if (onClose) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
...(modal ? {} : config),
|
...(modal ? {} : config),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -66,7 +60,7 @@ export function usePopover({
|
||||||
const role = useRole(context);
|
const role = useRole(context);
|
||||||
const interactions = useInteractions([click, dismiss, role]);
|
const interactions = useInteractions([click, dismiss, role]);
|
||||||
|
|
||||||
return React.useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
|
|
@ -78,7 +72,21 @@ export function usePopover({
|
||||||
setLabelId,
|
setLabelId,
|
||||||
setDescriptionId,
|
setDescriptionId,
|
||||||
duration,
|
duration,
|
||||||
|
triggerRef,
|
||||||
|
initialFocus,
|
||||||
|
onClose,
|
||||||
}),
|
}),
|
||||||
[open, setOpen, interactions, data, modal, labelId, descriptionId],
|
[
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
interactions,
|
||||||
|
data,
|
||||||
|
modal,
|
||||||
|
labelId,
|
||||||
|
descriptionId,
|
||||||
|
triggerRef,
|
||||||
|
initialFocus,
|
||||||
|
onClose,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
|
PopoverModalContent,
|
||||||
Button,
|
Button,
|
||||||
} from "@design-system/headless";
|
} from "@design-system/headless";
|
||||||
import { ControlledPopover } from "./ControlledPopover";
|
import { ControlledPopover } from "./ControlledPopover";
|
||||||
|
|
@ -46,16 +47,31 @@ Based on the [headless Popover component](/?path=/docs/design-system-headless-po
|
||||||
|
|
||||||
A modal is a floating element that displays information that requires immediate attention, appearing over the page content and blocking interactions with the page until it is dismissed.
|
A modal is a floating element that displays information that requires immediate attention, appearing over the page content and blocking interactions with the page until it is dismissed.
|
||||||
|
|
||||||
<Canvas>
|
## PopoverModalContent props
|
||||||
<Story name="Modal">
|
|
||||||
<Popover modal>
|
<ArgsTable of={PopoverModalContent} />
|
||||||
<PopoverTrigger>
|
|
||||||
<Button>Modal trigger</Button>
|
<Source
|
||||||
</PopoverTrigger>
|
dark
|
||||||
<PopoverContent>My modal content</PopoverContent>
|
code={`
|
||||||
|
export const Modal = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onPress={() => setIsOpen(!isOpen)} ref={ref}>
|
||||||
|
Controlled popover
|
||||||
|
</Button>
|
||||||
|
<Popover isOpen={isOpen} setOpen={setIsOpen} triggerRef={ref}>
|
||||||
|
<PopoverContent>Controlled popover content</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Story>
|
</>
|
||||||
</Canvas>
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
|
||||||
# Placement
|
# Placement
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
import React, { Children } from "react";
|
import React from "react";
|
||||||
import {
|
import { Popover, PopoverModalContent } from "@design-system/headless";
|
||||||
Popover,
|
|
||||||
PopoverTrigger,
|
|
||||||
PopoverContent,
|
|
||||||
} from "@design-system/headless";
|
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
import type { ModalProps } from "./types";
|
import type { ModalProps } from "./types";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
@ -11,23 +7,21 @@ import clsx from "clsx";
|
||||||
export const Modal = (props: ModalProps) => {
|
export const Modal = (props: ModalProps) => {
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
contentClassName,
|
|
||||||
overlayClassName,
|
overlayClassName,
|
||||||
size = "medium",
|
size = "medium",
|
||||||
|
triggerRef,
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
const [trigger, ...content] = Children.toArray(children);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover duration={200} modal {...rest}>
|
// don't forget to change the transition-duration CSS as well
|
||||||
<PopoverTrigger>{trigger}</PopoverTrigger>
|
<Popover duration={200} modal triggerRef={triggerRef} {...rest}>
|
||||||
<PopoverContent
|
<PopoverModalContent
|
||||||
contentClassName={clsx(styles.content, contentClassName)}
|
|
||||||
data-size={size}
|
data-size={size}
|
||||||
overlayClassName={clsx(styles.overlay, overlayClassName)}
|
overlayClassName={clsx(styles.overlay, overlayClassName)}
|
||||||
>
|
>
|
||||||
{content}
|
{children}
|
||||||
</PopoverContent>
|
</PopoverModalContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from "react";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import type { ModalContentProps } from "./types";
|
||||||
|
|
||||||
|
export const ModalContent = (props: ModalContentProps) => {
|
||||||
|
const { children, className } = props;
|
||||||
|
return <div className={clsx(styles.content, className)}>{children}</div>;
|
||||||
|
};
|
||||||
|
|
@ -26,7 +26,7 @@ export const ModalFooter = (props: ModalFooterProps) => {
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{onSubmit && (
|
{onSubmit && (
|
||||||
<Button autoFocus isLoading={isLoading} onPress={handleSubmit}>
|
<Button isLoading={isLoading} onPress={handleSubmit}>
|
||||||
{submitText}
|
{submitText}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,4 @@ export * from "./Modal";
|
||||||
export * from "./ModalHeader";
|
export * from "./ModalHeader";
|
||||||
export * from "./ModalFooter";
|
export * from "./ModalFooter";
|
||||||
export * from "./ModalBody";
|
export * from "./ModalBody";
|
||||||
|
export * from "./ModalContent";
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
background: var(--color-bg-neutral-opacity);
|
background: var(--color-bg-neutral-opacity);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
z-index: var(--z-index-99);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
|
@ -18,15 +19,15 @@
|
||||||
padding-block: var(--outer-spacing-4);
|
padding-block: var(--outer-spacing-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content[data-size="small"] {
|
[data-size="small"] .content {
|
||||||
width: var(--sizing-120);
|
width: var(--sizing-120);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content[data-size="medium"] {
|
[data-size="medium"] .content {
|
||||||
width: var(--sizing-180);
|
width: var(--sizing-180);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content[data-size="large"] {
|
[data-size="large"] .content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
@ -47,6 +48,7 @@
|
||||||
.content[data-status="close"],
|
.content[data-status="close"],
|
||||||
.overlay[data-status="open"],
|
.overlay[data-status="open"],
|
||||||
.overlay[data-status="close"] {
|
.overlay[data-status="close"] {
|
||||||
|
/* don't forget to change the duration Modal.tsx as well */
|
||||||
transition-duration: 200ms;
|
transition-duration: 200ms;
|
||||||
}
|
}
|
||||||
.content[data-status="initial"],
|
.content[data-status="initial"],
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import type {
|
import type {
|
||||||
PopoverContentProps,
|
PopoverModalContentProps,
|
||||||
PopoverProps,
|
PopoverProps,
|
||||||
} from "@design-system/headless";
|
} from "@design-system/headless";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
export interface ModalProps
|
export interface ModalProps
|
||||||
extends Pick<PopoverProps, "defaultOpen" | "isOpen" | "setOpen" | "onClose">,
|
extends Pick<
|
||||||
Pick<PopoverContentProps, "overlayClassName" | "contentClassName"> {
|
PopoverProps,
|
||||||
|
"isOpen" | "setOpen" | "onClose" | "triggerRef" | "initialFocus"
|
||||||
|
>,
|
||||||
|
Pick<PopoverModalContentProps, "overlayClassName"> {
|
||||||
/** Size of the Modal
|
/** Size of the Modal
|
||||||
* @default medium
|
* @default medium
|
||||||
*/
|
*/
|
||||||
|
|
@ -15,6 +18,13 @@ export interface ModalProps
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ModalContentProps {
|
||||||
|
/** The children of the component. */
|
||||||
|
children: ReactNode;
|
||||||
|
/** Sets the CSS className for the overlay. Only use as a **last resort**. */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ModalHeaderProps {
|
export interface ModalHeaderProps {
|
||||||
/** Adds a header modal Title and the necessary aria attributes. */
|
/** Adds a header modal Title and the necessary aria attributes. */
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Modal, ModalBody, Button } from "@design-system/widgets";
|
|
||||||
|
|
||||||
export const ControlledModal = () => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
return (
|
|
||||||
<Modal isOpen={isOpen} setOpen={setIsOpen}>
|
|
||||||
<Button>My modal trigger</Button>
|
|
||||||
<ModalBody>
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Doloribus,
|
|
||||||
vero!
|
|
||||||
</ModalBody>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import { Modal, ModalBody, ModalContent, Button } from "@design-system/widgets";
|
||||||
|
|
||||||
|
export const CustomModal = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onPress={() => setIsOpen(!isOpen)} ref={ref}>
|
||||||
|
Modal trigger
|
||||||
|
</Button>
|
||||||
|
<Modal isOpen={isOpen} setOpen={setIsOpen} triggerRef={ref}>
|
||||||
|
<div style={{ border: "4px solid rgb(255, 155, 78)" }}>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalBody>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias
|
||||||
|
amet animi corporis laboriosam libero voluptas! A, reiciendis,
|
||||||
|
veniam?
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,24 +1,18 @@
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Modal,
|
Modal,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
ButtonGroup,
|
ModalContent,
|
||||||
ButtonGroupItem,
|
|
||||||
} from "@design-system/widgets";
|
} from "@design-system/widgets";
|
||||||
import { Canvas, Meta, Story, ArgsTable, Source } from "@storybook/addon-docs";
|
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
|
||||||
import { ControlledModal } from "./ControlledModal";
|
import { SimpleModal } from "./SimpleModal";
|
||||||
|
import { ModalExamples } from "./ModalExamples";
|
||||||
|
import { CustomModal } from "./CustomModal";
|
||||||
|
|
||||||
<Meta title="Design-system/Widgets/Modal" component={Modal} />
|
<Meta title="Design-system/Widgets/Modal" component={Modal} />
|
||||||
|
|
||||||
export const Template = (args) => {
|
export const Template = (args) => {
|
||||||
return (
|
return <SimpleModal {...args} />;
|
||||||
<Modal {...args}>
|
|
||||||
<Button>My modal trigger</Button>
|
|
||||||
<div>My modal content</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# Modal
|
# Modal
|
||||||
|
|
@ -35,13 +29,17 @@ Modal developed on basis of Popover headless component. Additional information a
|
||||||
|
|
||||||
<ArgsTable story="Modal" of={Modal} />
|
<ArgsTable story="Modal" of={Modal} />
|
||||||
|
|
||||||
|
## ModalContent props
|
||||||
|
|
||||||
|
<ArgsTable of={ModalContent} />
|
||||||
|
|
||||||
## ModalHeader props
|
## ModalHeader props
|
||||||
|
|
||||||
<ArgsTable of={ModalHeader} />
|
<ArgsTable of={ModalHeader} />
|
||||||
|
|
||||||
## ModalBody props
|
## ModalBody props
|
||||||
|
|
||||||
ModalBody component has no props, it is responsible for the correct layout and supports the built-in browser scroll.
|
`ModalBody` component has no props, it is responsible for the correct layout and supports the built-in browser scroll.
|
||||||
|
|
||||||
## ModalFooter props
|
## ModalFooter props
|
||||||
|
|
||||||
|
|
@ -49,101 +47,12 @@ ModalBody component has no props, it is responsible for the correct layout and s
|
||||||
|
|
||||||
# ModalExamples
|
# ModalExamples
|
||||||
|
|
||||||
export const fakeSubmit = async () => {
|
|
||||||
return new Promise((resolve) =>
|
|
||||||
setTimeout(() => {
|
|
||||||
alert("Submitted");
|
|
||||||
resolve();
|
|
||||||
}, 500),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
<Canvas>
|
<Canvas>
|
||||||
<Story name="Modal Examples">
|
<Story name="Modal Examples">{<ModalExamples />}</Story>
|
||||||
<ButtonGroup>
|
|
||||||
<Modal size="small">
|
|
||||||
<ButtonGroupItem>Small</ButtonGroupItem>
|
|
||||||
<ModalHeader title="Modal title" />
|
|
||||||
<ModalBody>
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ad amet
|
|
||||||
aperiam assumenda cupiditate dicta dolore error expedita explicabo ipsum
|
|
||||||
iure mollitia nam quo, reiciendis repellat reprehenderit vel vitae.
|
|
||||||
Delectus dolores illo labore laudantium nihil nobis placeat soluta.
|
|
||||||
Dolorum esse et laboriosam libero nesciunt non placeat similique? Alias
|
|
||||||
animi at commodi cum delectus, ducimus earum enim esse exercitationem
|
|
||||||
facere facilis fugit illo illum laborum laudantium modi numquam officia
|
|
||||||
pariatur praesentium quos rem suscipit vel voluptas. Ab adipisci
|
|
||||||
asperiores blanditiis corporis, cum eligendi et, incidunt laborum nulla
|
|
||||||
odit quaerat quibusdam ut, velit. Animi assumenda at doloremque eius
|
|
||||||
facilis harum, libero praesentium quo.
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter onSubmit={fakeSubmit} />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal size="medium">
|
|
||||||
<ButtonGroupItem>Medium</ButtonGroupItem>
|
|
||||||
<ModalHeader title="Modal title" />
|
|
||||||
<ModalBody>
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ad amet
|
|
||||||
aperiam assumenda cupiditate dicta dolore error expedita explicabo ipsum
|
|
||||||
iure mollitia nam quo, reiciendis repellat reprehenderit vel vitae.
|
|
||||||
Delectus dolores illo labore laudantium nihil nobis placeat soluta.
|
|
||||||
Dolorum esse et laboriosam libero nesciunt non placeat similique? Alias
|
|
||||||
animi at commodi cum delectus, ducimus earum enim esse exercitationem
|
|
||||||
facere facilis fugit illo illum laborum laudantium modi numquam officia
|
|
||||||
pariatur praesentium quos rem suscipit vel voluptas. Ab adipisci
|
|
||||||
asperiores blanditiis corporis, cum eligendi et, incidunt laborum nulla
|
|
||||||
odit quaerat quibusdam ut, velit. Animi assumenda at doloremque eius
|
|
||||||
facilis harum, libero praesentium quo.
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter onSubmit={fakeSubmit} />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal size="large">
|
|
||||||
<ButtonGroupItem>Large</ButtonGroupItem>
|
|
||||||
<ModalHeader title="Modal title" />
|
|
||||||
<ModalBody>
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ad amet
|
|
||||||
aperiam assumenda cupiditate dicta dolore error expedita explicabo ipsum
|
|
||||||
iure mollitia nam quo, reiciendis repellat reprehenderit vel vitae.
|
|
||||||
Delectus dolores illo labore laudantium nihil nobis placeat soluta.
|
|
||||||
Dolorum esse et laboriosam libero nesciunt non placeat similique? Alias
|
|
||||||
animi at commodi cum delectus, ducimus earum enim esse exercitationem
|
|
||||||
facere facilis fugit illo illum laborum laudantium modi numquam officia
|
|
||||||
pariatur praesentium quos rem suscipit vel voluptas. Ab adipisci
|
|
||||||
asperiores blanditiis corporis, cum eligendi et, incidunt laborum nulla
|
|
||||||
odit quaerat quibusdam ut, velit. Animi assumenda at doloremque eius
|
|
||||||
facilis harum, libero praesentium quo.
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter onSubmit={fakeSubmit} />
|
|
||||||
</Modal>
|
|
||||||
</ButtonGroup>
|
|
||||||
|
|
||||||
</Story>
|
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
# Controlled Modal
|
# Custom Modal
|
||||||
|
|
||||||
<Canvas>
|
<Canvas>
|
||||||
<Story name="Controlled Modal">
|
<Story name="Custom Modal">{<CustomModal />}</Story>
|
||||||
<ControlledModal />
|
|
||||||
</Story>
|
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
<Source
|
|
||||||
dark
|
|
||||||
code={`
|
|
||||||
export const ControlledModal = () => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
return (
|
|
||||||
<Modal isOpen={isOpen} setOpen={setIsOpen}>
|
|
||||||
<Button>My modal trigger</Button>
|
|
||||||
<ModalBody>
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Doloribus,
|
|
||||||
vero!
|
|
||||||
</ModalBody>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
ButtonGroup,
|
||||||
|
ButtonGroupItem,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
} from "@design-system/widgets";
|
||||||
|
|
||||||
|
const fakeSubmit = async () => {
|
||||||
|
return new Promise<void>((resolve) =>
|
||||||
|
setTimeout(() => {
|
||||||
|
alert("Submitted");
|
||||||
|
resolve();
|
||||||
|
}, 500),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModalExamples = () => {
|
||||||
|
const [isSmallOpen, setSmallOpen] = useState(false);
|
||||||
|
const [isMediumOpen, setMediumOpen] = useState(false);
|
||||||
|
const [isLargeOpen, setLargeOpen] = useState(false);
|
||||||
|
const smallRef = useRef(null);
|
||||||
|
const mediumRef = useRef(null);
|
||||||
|
const largeRef = useRef(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonGroup>
|
||||||
|
<ButtonGroupItem
|
||||||
|
onPress={() => setSmallOpen(!isSmallOpen)}
|
||||||
|
ref={smallRef}
|
||||||
|
>
|
||||||
|
Small
|
||||||
|
</ButtonGroupItem>
|
||||||
|
<Modal
|
||||||
|
initialFocus={2}
|
||||||
|
isOpen={isSmallOpen}
|
||||||
|
setOpen={setSmallOpen}
|
||||||
|
size="small"
|
||||||
|
triggerRef={smallRef}
|
||||||
|
>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader title="Small modal title" />
|
||||||
|
<ModalBody>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias amet
|
||||||
|
animi corporis laboriosam libero voluptas! A, reiciendis, veniam?
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter onSubmit={fakeSubmit} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<ButtonGroupItem
|
||||||
|
onPress={() => setMediumOpen(!isMediumOpen)}
|
||||||
|
ref={mediumRef}
|
||||||
|
>
|
||||||
|
Medium
|
||||||
|
</ButtonGroupItem>
|
||||||
|
<Modal
|
||||||
|
initialFocus={2}
|
||||||
|
isOpen={isMediumOpen}
|
||||||
|
setOpen={setMediumOpen}
|
||||||
|
triggerRef={mediumRef}
|
||||||
|
>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader title="Medium modal title" />
|
||||||
|
<ModalBody>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias amet
|
||||||
|
animi corporis laboriosam libero voluptas! A, reiciendis, veniam?
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter onSubmit={fakeSubmit} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<ButtonGroupItem
|
||||||
|
onPress={() => setLargeOpen(!isLargeOpen)}
|
||||||
|
ref={largeRef}
|
||||||
|
>
|
||||||
|
Large
|
||||||
|
</ButtonGroupItem>
|
||||||
|
<Modal
|
||||||
|
initialFocus={2}
|
||||||
|
isOpen={isLargeOpen}
|
||||||
|
setOpen={setLargeOpen}
|
||||||
|
size="large"
|
||||||
|
triggerRef={largeRef}
|
||||||
|
>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader title="Large modal title" />
|
||||||
|
<ModalBody>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aut ex
|
||||||
|
illo ipsa iste mollitia non nulla qui, sed. Amet aperiam aspernatur
|
||||||
|
at autem beatae blanditiis commodi, cum delectus dignissimos ea enim
|
||||||
|
ex hic illum ipsam ipsum iure iusto magnam mollitia nobis nulla odit
|
||||||
|
pariatur possimus praesentium quaerat quia quo repellat repellendus
|
||||||
|
sequi similique soluta sunt tempora tempore temporibus? Accusamus
|
||||||
|
accusantium ad cumque deserunt dolorum enim error, excepturi
|
||||||
|
exercitationem facere fugiat impedit in ipsum labore laboriosam
|
||||||
|
minus modi mollitia neque nulla officiis porro, quo quos, sapiente
|
||||||
|
totam veritatis vitae voluptas voluptatibus? Aliquid amet asperiores
|
||||||
|
aut exercitationem facilis ipsa itaque magni nam odio reiciendis
|
||||||
|
repellendus rerum tempore ullam, vero voluptatem! Animi cupiditate
|
||||||
|
et minus porro recusandae, temporibus tenetur! Aliquid aperiam
|
||||||
|
aspernatur beatae dolore eius ex exercitationem expedita fuga iste
|
||||||
|
iusto laboriosam laudantium modi necessitatibus nemo nulla odio
|
||||||
|
optio perferendis, placeat praesentium quae quidem rem rerum soluta
|
||||||
|
tempore tenetur unde velit voluptas? At consequuntur corporis
|
||||||
|
delectus earum eos nihil odio officiis, quae quis sed. Asperiores
|
||||||
|
excepturi hic molestiae nesciunt nostrum quae temporibus. Commodi
|
||||||
|
corporis eos illo, ipsum laboriosam molestias neque numquam rerum
|
||||||
|
veniam veritatis. Doloribus impedit iste nulla quia. Assumenda et
|
||||||
|
facilis id minima praesentium quaerat similique. Ad adipisci
|
||||||
|
assumenda aut blanditiis dicta dignissimos eligendi ipsa mollitia
|
||||||
|
natus nobis, obcaecati possimus quam quia recusandae repellat sed
|
||||||
|
sit veniam. Animi consectetur libero praesentium temporibus velit!
|
||||||
|
Amet atque culpa, debitis deleniti eius harum libero maxime odit
|
||||||
|
officia officiis quibusdam, repellat sunt tempora? Accusantium
|
||||||
|
atque, cumque doloribus eveniet laudantium magni molestias officia,
|
||||||
|
sequi temporibus vel velit veritatis vero voluptatibus! Consequuntur
|
||||||
|
delectus eaque minus obcaecati repellat repudiandae sapiente,
|
||||||
|
tempora unde. Ab ad autem beatae commodi, culpa cupiditate debitis
|
||||||
|
dolores doloribus earum eos eveniet ex excepturi expedita explicabo
|
||||||
|
fuga incidunt inventore maxime minus modi molestias nulla odio,
|
||||||
|
perspiciatis quam quisquam quo ratione sapiente voluptatem? Autem
|
||||||
|
inventore quae velit.
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter onSubmit={fakeSubmit} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
ModalContent,
|
||||||
|
Button,
|
||||||
|
} from "@design-system/widgets";
|
||||||
|
import type { ModalProps } from "../src/types";
|
||||||
|
|
||||||
|
const fakeSubmit = async () => {
|
||||||
|
return new Promise<void>((resolve) =>
|
||||||
|
setTimeout(() => {
|
||||||
|
alert("Submitted");
|
||||||
|
resolve();
|
||||||
|
}, 500),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SimpleModal = (props: Omit<ModalProps, "children">) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onPress={() => setIsOpen(!isOpen)}>Modal trigger</Button>
|
||||||
|
<Modal initialFocus={2} isOpen={isOpen} setOpen={setIsOpen} {...props}>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader title="Modal title" />
|
||||||
|
<ModalBody>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias amet
|
||||||
|
animi corporis laboriosam libero voluptas! A, reiciendis, veniam?
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter onSubmit={fakeSubmit} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user