diff --git a/src/components/Modal/Modal.module.css b/src/components/Modal/Modal.module.css index 3d4a0e894..321f55041 100755 --- a/src/components/Modal/Modal.module.css +++ b/src/components/Modal/Modal.module.css @@ -1,3 +1,5 @@ +@import '../../design-tokens/mixins.css'; + /*------------------------------------*\ # MODAL \*------------------------------------*/ @@ -160,3 +162,153 @@ .modal__close-icon--brand { color: var(--eds-theme-color-icon-neutral-default-inverse); } + +/*------------------------------------*\ + # MODAL BODY +\*------------------------------------*/ + +/** + * The body of the modal + */ + .modal-body { + padding: var(--eds-size-4) var(--modal-horizontal-padding); + flex: 1; +} + +.modal-body:focus-visible { + @mixin focusInside; +} + +@supports not selector(:focus-visible) { + .modal-body:focus { + @mixin focusInside; + } +} + +/*------------------------------------*\ + # MODAL FOOTER +\*------------------------------------*/ + +/** + * Footer for the modal. + */ + .modal-footer { + padding-top: var(--eds-size-2-and-half); + padding-bottom: var(--eds-size-2-and-half); + padding-left: var(--modal-horizontal-padding); + padding-right: var(--eds-size-3); + + width: 100%; + background-color: var(--eds-theme-color-background-neutral-default); + + border-bottom-left-radius: var(--eds-border-radius-lg); + border-bottom-right-radius: var(--eds-border-radius-lg); + + z-index: var(--eds-z-index-100); +} + +/** + * Sticky variant of the footer. + * Used when the modal variant is scrollable. + */ +.modal-footer--sticky { + position: sticky; + bottom: 0; + box-shadow: var(--eds-box-shadow-xl); +} + +/*------------------------------------*\ + # MODAL HEADER +\*------------------------------------*/ + +/** + * Header for the modal. + */ + .modal-header { + width: 100%; + background-color: var(--eds-theme-color-background-neutral-default); + padding-top: var(--eds-size-7); + padding-left: var(--modal-horizontal-padding); + padding-right: var(--modal-horizontal-padding); +} + +/** + * Brand variant for the header. + */ +.modal-header--brand { + display: flex; + flex-direction: column; + + min-height: 10.75rem; + + @media all and (min-height: $eds-bp-sm-2) { + min-height: 19.75rem; + } + @media all and (min-width: $eds-bp-md) and (min-height: $eds-bp-sm-2) { + flex-direction: row; + } + + flex-shrink: 0; + + color: var(--eds-theme-color-text-neutral-default-inverse); + background-color: var(--eds-theme-color-modal-brand-header-background); + h2 { + /** + * Brand specific font for the title. + */ + flex: 1; + font: var(--eds-theme-typography-headline-secondary-lg); + + @media all and (min-width: $eds-bp-sm-2) { + margin-bottom: var(--eds-size-3); + } + @media all and (min-width: $eds-bp-xl) { + font: var(--eds-theme-typography-headline-secondary-md); + + } + } +} + +/** + * Specific asset placement for brand. + */ +.modal-header__brand-asset { + align-self: flex-end; + position: relative; + top: var(--eds-size-2); + left: var(--eds-size-2); + + width: 8.5rem; + height: 8.5rem; + + /* For mobile landscape orientation. */ + @media all and (min-width: $eds-bp-sm-2) { + display: none; + } + + @media all and (min-width: $eds-bp-md) and (min-height: $eds-bp-sm-2) { + display: block; + width: 14rem; + height: 14rem; + left: var(--eds-size-3); + } + + @media all and (min-width: $eds-bp-xl) { + width: 16.5rem; + height: 16.5rem; + left: 0; + } +} + +/*------------------------------------*\ + # MODAL STEPPER +\*------------------------------------*/ + +/** + * Stepper that resides in the modal footer. + */ + .modal-stepper { + color: var(--eds-theme-color-icon-neutral-default); + display: flex; + gap: var(--eds-size-1-and-half); +} diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 8f042580a..2265adab3 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -2,15 +2,10 @@ import { Dialog, Transition } from '@headlessui/react'; import clsx from 'clsx'; import type { MutableRefObject, ReactNode } from 'react'; import React from 'react'; +import type { ExtractProps } from '../../util/utility-types'; +import type { HeadingSize } from '../Heading'; +import Heading from '../Heading'; import { Icon } from '../Icon/Icon'; -import type { Props as ModalBodyProps } from '../ModalBody/ModalBody'; -import { ModalBody } from '../ModalBody/ModalBody'; -import type { Props as ModalFooterProps } from '../ModalFooter/ModalFooter'; -import { ModalFooter } from '../ModalFooter/ModalFooter'; -import type { Props as ModalHeaderProps } from '../ModalHeader/ModalHeader'; -import { ModalHeader } from '../ModalHeader/ModalHeader'; -import { ModalStepper } from '../ModalStepper/ModalStepper'; -import { ModalTitle } from '../ModalTitle/ModalTitle'; import styles from './Modal.module.css'; type Variant = 'brand' | undefined; @@ -40,13 +35,14 @@ type ModalContentProps = { * * If undefined, the first focusable element (usually the close button) will be used. * - * @example + * ``` * const inputFieldRef = useRef(); * * * ... * * + * ``` */ initialFocus?: MutableRefObject; /** @@ -86,6 +82,96 @@ type ModalProps = ModalContentProps & { modalContainerClassName?: string; }; +type ModalTitleProps = Omit, 'size'> & { + /** + * Text for the modal title. + */ + children: ReactNode; + /** + * CSS class names that can be appended to the component. + */ + className?: string; + /** + * Modal Title Heading size. Defaults to 'headline-md' + */ + size?: HeadingSize; +}; + +type ModalBodyProps = { + /** + * Child node(s) that can be nested inside component. `ModalHeader`, `ModalBody`, and `ModelFooter` are the only permissible children of the Modal + */ + children: ReactNode; + /** + * CSS class names that can be appended to the component. + */ + className?: string; + /** + * Toggles focusable variant of the modal. Used to attach a tabIndex for keyboard scrolling + * and focus indicator outline. + * Scrolling functionality exists on Modal since the header also needs to scroll. + * Defaults to false since modal default is not scrollable. + */ + isFocusable?: boolean; +}; + +type ModalHeaderProps = { + /** + * Child node(s) to place inside the Modal header. + * Should include the + */ + children: ReactNode; + /** + * CSS class names that can be appended to the component. + */ + className?: string; + /** + * Adjusts height, color, and text of the header. + */ + variant?: Variant; + /** + * Placeholder for brand asset. + */ + brandAsset?: ReactNode; + /** + * CSS class names that can be appended to the brand asset. + */ + assetClassName?: string; +}; + +type ModalFooterProps = { + /** + * Child node(s) to place inside the Modal footer. + */ + children: ReactNode; + /** + * CSS class names that can be appended to the component. + */ + className?: string; + /** + * Toggles sticky variant of the footer. If modal is scrollable, footer is sticky. + * Also adds border and shadow to indicate sticky status. + * Defaults to false since modal default is not scrollable. + */ + isSticky?: boolean; +}; + +type ModalStepperProps = { + /** + * Indicates which step is the active step. Must be one or more. + */ + activeStep: number; + /** + * CSS class names that can be appended to the component. + */ + className?: string; + /** + * Indicates how many steps to represent. Must be one or more and + * greater than or equal to activeStep. + */ + totalSteps: number; +}; + type Context = { isScrollable?: boolean; variant?: Variant; @@ -234,6 +320,139 @@ export const Modal = (props: ModalProps) => { ); }; +/** + * Component defines the body of the modal. + */ +const ModalBody = ({ + children, + className, + isFocusable, + ...other +}: ModalBodyProps) => ( +
+ {children} +
+); + +/** + * Component defines the Footer section of the modal. + */ +const ModalFooter = ({ + children, + className, + isSticky = false, + ...other +}: ModalFooterProps) => { + return ( +
+ {children} +
+ ); +}; + +/** + * Component defines the Header section of the modal. + */ +const ModalHeader = ({ + assetClassName, + brandAsset, + children, + className, + variant, + ...other +}: ModalHeaderProps) => { + const componentClassName = clsx( + styles['modal-header'], + variant === 'brand' && styles['modal-header--brand'], + className, + ); + const brandAssetClassName = clsx( + styles['modal-header__brand-asset'], + assetClassName, + ); + return ( +
+ {children} + {variant === 'brand' && brandAsset && ( +
{brandAsset}
+ )} +
+ ); +}; + +/** + * Stepper for the modal to indicate page status. + */ +const ModalStepper = ({ + activeStep, + className, + totalSteps, + ...other +}: ModalStepperProps) => { + const componentClassName = clsx(styles['modal-stepper'], className); + if (process.env.NODE_ENV !== 'production') { + if (totalSteps < 1) { + throw new Error('Must have more than one step in totalSteps.'); + } + if (activeStep < 1) { + throw new Error('activeStep must be one or more.'); + } + if (totalSteps < activeStep) { + throw new Error('activeStep cannot exceed totalSteps'); + } + } + + const stepIcons = []; + for (let i = 0; i < totalSteps; i++) { + const isActivestep = i + 1 === activeStep; + const name = isActivestep ? 'circle' : 'empty-circle'; + const title = isActivestep ? `Active Step ${i + 1}` : `Step ${i + 1}`; + stepIcons.push( + , + ); + } + return ( +
+ {stepIcons} +
+ ); +}; + +/** + * Component defines the Title section of the modal. + */ +const ModalTitle = ({ + children, + className, + size = 'headline-md', + ...other +}: ModalTitleProps) => ( + + + {children} + + +); + /** * Variations of the subcomponent to pass props from parent Modal component. * Same prop passed directly to subcomponent has priority over prop passed from Modal component. diff --git a/src/components/ModalBody/ModalBody.module.css b/src/components/ModalBody/ModalBody.module.css deleted file mode 100644 index 122ae50de..000000000 --- a/src/components/ModalBody/ModalBody.module.css +++ /dev/null @@ -1,23 +0,0 @@ -@import '../../design-tokens/mixins.css'; - -/*------------------------------------*\ - # MODAL BODY -\*------------------------------------*/ - -/** - * The body of the modal - */ -.modal-body { - padding: var(--eds-size-4) var(--modal-horizontal-padding); - flex: 1; -} - -.modal-body:focus-visible { - @mixin focusInside; -} - -@supports not selector(:focus-visible) { - .modal-body:focus { - @mixin focusInside; - } -} diff --git a/src/components/ModalBody/ModalBody.tsx b/src/components/ModalBody/ModalBody.tsx deleted file mode 100644 index 9ad052ab1..000000000 --- a/src/components/ModalBody/ModalBody.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import clsx from 'clsx'; -import type { ReactNode } from 'react'; -import React from 'react'; -import styles from './ModalBody.module.css'; - -export type Props = { - /** - * Child node(s) that can be nested inside component. `ModalHeader`, `ModalBody`, and `ModelFooter` are the only permissible children of the Modal - */ - children: ReactNode; - /** - * CSS class names that can be appended to the component. - */ - className?: string; - /** - * Toggles focusable variant of the modal. Used to attach a tabIndex for keyboard scrolling - * and focus indicator outline. - * Scrolling functionality exists on Modal since the header also needs to scroll. - * Defaults to false since modal default is not scrollable. - */ - isFocusable?: boolean; -}; - -/** - * Component defines the body of the modal. - */ - -export const ModalBody = ({ - children, - className, - isFocusable, - ...other -}: Props) => ( -
- {children} -
-); diff --git a/src/components/ModalBody/index.ts b/src/components/ModalBody/index.ts deleted file mode 100644 index 60ac9e884..000000000 --- a/src/components/ModalBody/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ModalBody as default } from './ModalBody'; diff --git a/src/components/ModalFooter/ModalFooter.module.css b/src/components/ModalFooter/ModalFooter.module.css deleted file mode 100644 index 7dc7bee9c..000000000 --- a/src/components/ModalFooter/ModalFooter.module.css +++ /dev/null @@ -1,31 +0,0 @@ -/*------------------------------------*\ - # MODAL FOOTER -\*------------------------------------*/ - -/** - * Footer for the modal. - */ -.modal-footer { - padding-top: var(--eds-size-2-and-half); - padding-bottom: var(--eds-size-2-and-half); - padding-left: var(--modal-horizontal-padding); - padding-right: var(--eds-size-3); - - width: 100%; - background-color: var(--eds-theme-color-background-neutral-default); - - border-bottom-left-radius: var(--eds-border-radius-lg); - border-bottom-right-radius: var(--eds-border-radius-lg); - - z-index: var(--eds-z-index-100); -} - -/** - * Sticky variant of the footer. - * Used when the modal variant is scrollable. - */ -.modal-footer--sticky { - position: sticky; - bottom: 0; - box-shadow: var(--eds-box-shadow-xl); -} diff --git a/src/components/ModalFooter/ModalFooter.tsx b/src/components/ModalFooter/ModalFooter.tsx deleted file mode 100644 index 3e3a710de..000000000 --- a/src/components/ModalFooter/ModalFooter.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import clsx from 'clsx'; -import type { ReactNode } from 'react'; -import React from 'react'; -import styles from './ModalFooter.module.css'; - -export type Props = { - /** - * Child node(s) to place inside the Modal footer. - */ - children: ReactNode; - /** - * CSS class names that can be appended to the component. - */ - className?: string; - /** - * Toggles sticky variant of the footer. If modal is scrollable, footer is sticky. - * Also adds border and shadow to indicate sticky status. - * Defaults to false since modal default is not scrollable. - */ - isSticky?: boolean; -}; - -/** - * Component defines the Footer section of the modal. - */ - -export const ModalFooter = ({ - children, - className, - isSticky = false, - ...other -}: Props) => { - return ( -
- {children} -
- ); -}; diff --git a/src/components/ModalFooter/index.ts b/src/components/ModalFooter/index.ts deleted file mode 100644 index 61b89e80e..000000000 --- a/src/components/ModalFooter/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ModalFooter as default } from './ModalFooter'; diff --git a/src/components/ModalHeader/ModalHeader.module.css b/src/components/ModalHeader/ModalHeader.module.css deleted file mode 100644 index b3add3589..000000000 --- a/src/components/ModalHeader/ModalHeader.module.css +++ /dev/null @@ -1,82 +0,0 @@ -/*------------------------------------*\ - # MODAL HEADER -\*------------------------------------*/ - -/** - * Header for the modal. - */ -.modal-header { - width: 100%; - background-color: var(--eds-theme-color-background-neutral-default); - padding-top: var(--eds-size-7); - padding-left: var(--modal-horizontal-padding); - padding-right: var(--modal-horizontal-padding); -} - -/** - * Brand variant for the header. - */ -.modal-header--brand { - display: flex; - flex-direction: column; - - min-height: 10.75rem; - - @media all and (min-height: $eds-bp-sm-2) { - min-height: 19.75rem; - } - @media all and (min-width: $eds-bp-md) and (min-height: $eds-bp-sm-2) { - flex-direction: row; - } - - flex-shrink: 0; - - color: var(--eds-theme-color-text-neutral-default-inverse); - background-color: var(--eds-theme-color-modal-brand-header-background); - h2 { - /** - * Brand specific font for the title. - */ - flex: 1; - font: var(--eds-theme-typography-headline-secondary-lg); - - @media all and (min-width: $eds-bp-sm-2) { - margin-bottom: var(--eds-size-3); - } - @media all and (min-width: $eds-bp-xl) { - font: var(--eds-theme-typography-headline-secondary-md); - - } - } -} - -/** - * Specific asset placement for brand. - */ -.modal-header__brand-asset { - align-self: flex-end; - position: relative; - top: var(--eds-size-2); - left: var(--eds-size-2); - - width: 8.5rem; - height: 8.5rem; - - /* For mobile landscape orientation. */ - @media all and (min-width: $eds-bp-sm-2) { - display: none; - } - - @media all and (min-width: $eds-bp-md) and (min-height: $eds-bp-sm-2) { - display: block; - width: 14rem; - height: 14rem; - left: var(--eds-size-3); - } - - @media all and (min-width: $eds-bp-xl) { - width: 16.5rem; - height: 16.5rem; - left: 0; - } -} diff --git a/src/components/ModalHeader/ModalHeader.tsx b/src/components/ModalHeader/ModalHeader.tsx deleted file mode 100644 index 5d5859051..000000000 --- a/src/components/ModalHeader/ModalHeader.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import clsx from 'clsx'; -import type { ReactNode } from 'react'; -import React from 'react'; -import styles from './ModalHeader.module.css'; - -export type Props = { - /** - * Child node(s) to place inside the Modal header. - * Should include the - */ - children: ReactNode; - /** - * CSS class names that can be appended to the component. - */ - className?: string; - /** - * Adjusts height, color, and text of the header. - */ - variant?: 'brand'; - /** - * Placeholder for brand asset. - */ - brandAsset?: ReactNode; - /** - * CSS class names that can be appended to the brand asset. - */ - assetClassName?: string; -}; - -/** - * Component defines the Header section of the modal. - */ - -export const ModalHeader = ({ - assetClassName, - brandAsset, - children, - className, - variant, - ...other -}: Props) => { - const componentClassName = clsx( - styles['modal-header'], - variant === 'brand' && styles['modal-header--brand'], - className, - ); - const brandAssetClassName = clsx( - styles['modal-header__brand-asset'], - assetClassName, - ); - return ( -
- {children} - {variant === 'brand' && brandAsset && ( -
{brandAsset}
- )} -
- ); -}; diff --git a/src/components/ModalHeader/index.ts b/src/components/ModalHeader/index.ts deleted file mode 100644 index ebf7e00c0..000000000 --- a/src/components/ModalHeader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ModalHeader as default } from './ModalHeader'; diff --git a/src/components/ModalStepper/ModalStepper.module.css b/src/components/ModalStepper/ModalStepper.module.css deleted file mode 100644 index 58787da41..000000000 --- a/src/components/ModalStepper/ModalStepper.module.css +++ /dev/null @@ -1,12 +0,0 @@ -/*------------------------------------*\ - # MODAL STEPPER -\*------------------------------------*/ - -/** - * Stepper that resides in the modal footer. - */ -.modal-stepper { - color: var(--eds-theme-color-icon-neutral-default); - display: flex; - gap: var(--eds-size-1-and-half); -} diff --git a/src/components/ModalStepper/ModalStepper.tsx b/src/components/ModalStepper/ModalStepper.tsx deleted file mode 100644 index 66ab5632c..000000000 --- a/src/components/ModalStepper/ModalStepper.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import clsx from 'clsx'; -import React from 'react'; -import { Icon } from '../Icon/Icon'; -import styles from './ModalStepper.module.css'; - -export interface Props { - /** - * Indicates which step is the active step. Must be one or more. - */ - activeStep: number; - /** - * CSS class names that can be appended to the component. - */ - className?: string; - /** - * Indicates how many steps to represent. Must be one or more and - * greater than or equal to activeStep. - */ - totalSteps: number; -} - -/** - * `import {ModalStepper} from "@chanzuckerberg/eds";` - * - * Stepper for the modal to indicate page status. - */ -export const ModalStepper = ({ - activeStep, - className, - totalSteps, - ...other -}: Props) => { - const componentClassName = clsx(styles['modal-stepper'], className); - if (process.env.NODE_ENV !== 'production') { - if (totalSteps < 1) { - throw new Error('Must have more than one step in totalSteps.'); - } - if (activeStep < 1) { - throw new Error('activeStep must be one or more.'); - } - if (totalSteps < activeStep) { - throw new Error('activeStep cannot exceed totalSteps'); - } - } - - const stepIcons = []; - for (let i = 0; i < totalSteps; i++) { - const isActivestep = i + 1 === activeStep; - const name = isActivestep ? 'circle' : 'empty-circle'; - const title = isActivestep ? `Active Step ${i + 1}` : `Step ${i + 1}`; - stepIcons.push( - , - ); - } - return ( -
- {stepIcons} -
- ); -}; diff --git a/src/components/ModalStepper/index.ts b/src/components/ModalStepper/index.ts deleted file mode 100644 index 7c889e462..000000000 --- a/src/components/ModalStepper/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ModalStepper as default } from './ModalStepper'; diff --git a/src/components/ModalTitle/ModalTitle.tsx b/src/components/ModalTitle/ModalTitle.tsx deleted file mode 100644 index ef6b429e9..000000000 --- a/src/components/ModalTitle/ModalTitle.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Dialog } from '@headlessui/react'; -import type { ReactNode } from 'react'; -import React from 'react'; -import type { ExtractProps } from '../../util/utility-types'; -import type { HeadingSize } from '../Heading'; -import Heading from '../Heading'; - -type HeadingProps = ExtractProps; - -type Props = Omit & { - /** - * Text for the modal title. - */ - children: ReactNode; - /** - * CSS class names that can be appended to the component. - */ - className?: string; - /** - * Modal Title Heading size. Defaults to 'headline-md' - */ - size?: HeadingSize; -}; - -/** - * Component defines the Title section of the modal. - */ - -export const ModalTitle = ({ - children, - className, - size = 'headline-md', - ...other -}: Props) => ( - - - {children} - - -); diff --git a/src/components/ModalTitle/index.ts b/src/components/ModalTitle/index.ts deleted file mode 100644 index b20fa5a6e..000000000 --- a/src/components/ModalTitle/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ModalTitle as default } from './ModalTitle'; diff --git a/src/index.ts b/src/index.ts index 7d2c97594..60f86ee67 100644 --- a/src/index.ts +++ b/src/index.ts @@ -54,11 +54,6 @@ export { default as Link } from './components/Link'; export { default as LoadingIndicator } from './components/LoadingIndicator'; export { default as Menu } from './components/Menu'; export { default as Modal } from './components/Modal'; -export { default as ModalBody } from './components/ModalBody'; -export { default as ModalFooter } from './components/ModalFooter'; -export { default as ModalHeader } from './components/ModalHeader'; -export { default as ModalStepper } from './components/ModalStepper'; -export { default as ModalTitle } from './components/ModalTitle'; export { default as NumberIcon } from './components/NumberIcon'; export { default as PageHeader } from './components/PageHeader'; export { default as PageLevelBanner } from './components/PageLevelBanner';