Skip to content

Commit

Permalink
feat(core/Modal): make Modal composable
Browse files Browse the repository at this point in the history
  • Loading branch information
Gonzalo Uceda committed Apr 14, 2023
1 parent 617b2d7 commit ef20159
Show file tree
Hide file tree
Showing 23 changed files with 364 additions and 170 deletions.
27 changes: 26 additions & 1 deletion packages/core/src/components/Modal/Modal.cases.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import { Button, Modal } from '..';
const meta: Meta<typeof Modal> = {
title: 'Components/Core/Layout/Modal/Cases',
component: Modal,
parameters: {
controls: false,
},
args: {
headerStyle: 'default',
status: 'base',
shouldCloseOnOverlayClick: true,
windowSize: 'medium',
},
Expand Down Expand Up @@ -107,3 +110,25 @@ export const Animated: Story = {
);
})(),
};

export const Custom: Story = {
render: () =>
(() => {
const [isOpen, setOpen] = React.useState<boolean>(false);

return (
<>
{isOpen && (
<Modal.Container onRequestClose={() => setOpen(false)}>
<Modal.Header>Header Content</Modal.Header>
<Modal.Body>Body Content</Modal.Body>
<Modal.Footer>Footer Content</Modal.Footer>
</Modal.Container>
)}
<Button onClick={() => setOpen(true)} colorScheme="accent-high">
Open modal
</Button>
</>
);
})(),
};
6 changes: 6 additions & 0 deletions packages/core/src/components/Modal/Modal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ For example, you can use `framer-motion` library along with a simple fade animat

<Canvas of={CaseStories.Animated} />

<Heading.H3>Custom modal</Heading.H3>

If you want to build a custom modal, you can use the internal subcomponents.

<Canvas of={CaseStories.Custom} />

<Heading.H2>Props</Heading.H2>

<ArgTypes of={Stories} />
2 changes: 1 addition & 1 deletion packages/core/src/components/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const meta: Meta<typeof Modal> = {
title: 'Components/Core/Layout/Modal',
component: Modal,
args: {
headerStyle: 'default',
status: 'base',
shouldCloseOnOverlayClick: true,
windowSize: 'medium',
},
Expand Down
215 changes: 91 additions & 124 deletions packages/core/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,41 @@
import * as React from 'react';
import { useTheme } from 'styled-components';

import {
Box,
DecoratorBar,
IconButton,
IconButtonClose,
IconButtonGoToDocs,
Overlay,
} from '../';
import { useContainerDimensions } from '../../hooks';
import {
StyledModal,
StyledModalActions,
StyledModalContent,
StyledModalFooter,
StyledModalHeader,
StyledModalIcon,
} from './styled';
import { statusIconMap } from './icons';
import { Box, IconButton, IconButtonClose, IconButtonGoToDocs } from '../';
import { StyledModalActions } from './styled';
import { Heading } from '../Typography/components/block';
import { updateHasScroll } from './scroll';
import { HeaderAction, WindowSize } from './declarations';
import { ActiveStatus } from '../../declarations';
import {
ModalFooter,
ModalBody,
ModalHeader,
ModalContainer,
ModalIcon,
} from './subcomponents';

export interface ModalProps {
import type { HeaderAction } from './declarations';
import type { GlobalStatus } from '../../declarations';
import type {
ModalFooterProps,
ModalBodyProps,
ModalHeaderProps,
ModalContainerProps,
ModalIconProps,
} from './subcomponents';
import { useDetectBodyScroll } from './hooks';

export interface ModalProps
extends Omit<ModalContainerProps, 'children'>,
Omit<ModalHeaderProps, 'children' | 'hasScroll'>,
Omit<ModalIconProps, 'children' | 'hasScroll'>,
Omit<ModalBodyProps, 'children' | 'hasScroll'>,
Omit<ModalFooterProps, 'children' | 'hasScroll'> {
/** Sets array of buttons displayed on the bottom */
buttons?: React.ReactNode;
/** Modal content */
children?: React.ReactNode;
/** Hide Close Button (Show by default) */
contentPadding?: string;
/** Set window options button (close button excluded) */
headerActions?: HeaderAction[];
/** Defines the header Title */
headerTitle?: string;
/** Define the header Style */
headerStyle?: 'default' | 'dialog';
/** Height */
height?: string;
/** The title for the docs help link on the footer of the Modal */
Expand All @@ -45,129 +44,97 @@ export interface ModalProps {
helpUrl?: string;
/** Hides close button (x), displayed by default */
hideCloseButton?: boolean;
/** Custom id */
id?: string;
/** Function that will be called right after the modal is open */
onAfterOpen?: () => void;
/** Manages closing action by pressing close button*/
onRequestClose?: () => void;
/** Disabling overlay exit click */
shouldCloseOnOverlayClick?: boolean;
/** Manages dialog status **/
status?: ActiveStatus;
status?: GlobalStatus;
/** Custom size */
width?: string;
/** Set the Window Size between one of the following presets */
/** Aurea proportion size */
windowSize?: WindowSize;
}

export const Modal: React.FC<ModalProps> = ({
export const InternalModal: React.FC<ModalProps> = ({
buttons,
children,
contentPadding,
headerActions,
headerTitle,
headerStyle = 'default',
height,
helpTitle = 'Go to Docs',
helpUrl,
hideCloseButton,
id,
onRequestClose,
status,
shouldCloseOnOverlayClick = true,
status = 'base',
windowSize = 'medium',
}) => {
const tokens = useTheme();
const { setRef: modalContentRef, size: measures } = useContainerDimensions();
const [hasScroll, setHasScroll] = React.useState(false);
React.useLayoutEffect(
() => updateHasScroll(measures, hasScroll, setHasScroll),
[measures, hasScroll, setHasScroll]
);
const { hasScroll, targetElRef } = useDetectBodyScroll();

return (
// Modal overlay needs a DIV wrapper to set the onClick handler, as Overlay component does
// not support it and the DIV allows the overlay to expand over the entire screen.
<div
onClick={shouldCloseOnOverlayClick ? () => onRequestClose?.() : undefined}
style={{
position: 'fixed',
width: '100%',
height: '100%',
top: 0,
left: 0,
zIndex: 1,
}}
>
<Overlay bgColorScheme={'dark'}>
<StyledModal id={id} height={height} windowSize={windowSize}>
<StyledModalHeader hasScroll={hasScroll} status={status}>
{headerTitle &&
(headerStyle !== 'dialog' ? (
<>
<DecoratorBar
size={tokens.cmp.modal.headerDecoratorBar.size.height}
/>
<Heading size="h4" truncateLine={1}>
{headerTitle}
</Heading>
</>
) : (
<>
{status && (
<StyledModalIcon
status={status}
className={statusIconMap[status]}
/>
)}
<Heading size="h5" truncateLine={1}>
{headerTitle}
</Heading>
</>
))}
<StyledModalActions>
{headerActions &&
headerActions.map((el, idx) => (
<li key={idx}>
<IconButton
size="sm"
title={el.tooltip}
icon={el.icon}
onClick={el.onClick}
/>
</li>
))}
{!hideCloseButton && (
<li>
<IconButtonClose onClick={onRequestClose} title="Close" />
</li>
)}
</StyledModalActions>
</StyledModalHeader>
<ModalContainer id={id} height={height} windowSize={windowSize}>
<ModalHeader hasBoxShadow={hasScroll} status={status}>
{headerTitle && (
<>
<ModalIcon status={status} />
<Heading size={status === 'base' ? 'h4' : 'h5'} truncateLine={1}>
{headerTitle}
</Heading>
</>
)}
<StyledModalActions>
{headerActions &&
headerActions.map((el, idx) => (
<li key={idx}>
<IconButton
size="sm"
title={el.tooltip}
icon={el.icon}
onClick={el.onClick}
/>
</li>
))}
{!hideCloseButton && (
<li>
<IconButtonClose onClick={onRequestClose} title="Close" />
</li>
)}
</StyledModalActions>
</ModalHeader>

<StyledModalContent
ref={modalContentRef}
contentPadding={contentPadding}
height={height}
hasScroll={hasScroll}
>
{children}
</StyledModalContent>
<ModalBody
modalBodyRef={targetElRef}
contentPadding={contentPadding}
height={height}
hasScroll={hasScroll}
>
{children}
</ModalBody>

{buttons && (
<StyledModalFooter hasScroll={hasScroll}>
{helpUrl && (
<Box marginRight="auto">
<IconButtonGoToDocs href={helpUrl} title={helpTitle} />
</Box>
)}
{buttons}
</StyledModalFooter>
{buttons && (
<ModalFooter hasBoxShadow={hasScroll}>
{helpUrl && (
<Box marginRight="auto">
<IconButtonGoToDocs href={helpUrl} title={helpTitle} />
</Box>
)}
</StyledModal>
</Overlay>
</div>
{buttons}
</ModalFooter>
)}
</ModalContainer>
);
};

export const Modal = InternalModal as typeof InternalModal & {
Container: typeof ModalContainer;
Header: typeof ModalHeader;
Body: typeof ModalBody;
Footer: typeof ModalFooter;
Icon: typeof ModalIcon;
};

Modal.Container = ModalContainer;
Modal.Header = ModalHeader;
Modal.Body = ModalBody;
Modal.Footer = ModalFooter;
Modal.Icon = ModalIcon;
2 changes: 1 addition & 1 deletion packages/core/src/components/Modal/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CSSProperties, DOMAttributes } from 'react';
import { GlobalStatus } from '../../declarations';
export interface StyledModalProps {
headerStyle?: string;
hasScroll?: boolean;
hasBoxShadow?: boolean;
contentPadding?: string;
height?: CSSProperties['height'];
windowSize?: string;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/Modal/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useDetectBodyScroll';
16 changes: 16 additions & 0 deletions packages/core/src/components/Modal/hooks/useDetectBodyScroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';
import { updateHasScroll } from '../scroll';
import { useContainerDimensions } from '../../../hooks';

export const useDetectBodyScroll = () => {
const { setRef: targetElRef, size: measures } = useContainerDimensions();

const [hasScroll, setHasScroll] = React.useState(false);

React.useLayoutEffect(
() => updateHasScroll(measures, hasScroll, setHasScroll),
[measures, hasScroll, setHasScroll]
);

return { targetElRef, hasScroll };
};
10 changes: 0 additions & 10 deletions packages/core/src/components/Modal/icons.ts

This file was deleted.

2 changes: 2 additions & 0 deletions packages/core/src/components/Modal/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './Modal';
export * from './hooks';
export type * from './subcomponents';
11 changes: 10 additions & 1 deletion packages/core/src/components/Modal/styled/StyledModal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import styled, { css } from 'styled-components';

import { StyledModalProps, WindowSize } from '../declarations';
import { WindowSize } from '../declarations';
import { CSSProperties } from 'react';

const windowSizeMap: { [key in WindowSize]: string } = {
small: 'sm',
Expand All @@ -10,6 +11,14 @@ const windowSizeMap: { [key in WindowSize]: string } = {
default: 'md',
} as const;

export interface StyledModalProps {
headerStyle?: string;
hasBoxShadow?: boolean;
contentPadding?: string;
height?: CSSProperties['height'];
windowSize?: string;
}

export const StyledModal = styled.div<StyledModalProps>`
${({ theme, height, headerStyle, windowSize }) => {
const aliasTokens = theme.alias;
Expand Down
Loading

0 comments on commit ef20159

Please sign in to comment.