Skip to content

Commit

Permalink
feat(modal): implement "@coveord/plasma-mantine/Modal" (#4001)
Browse files Browse the repository at this point in the history
* feat(modal): wip modal

* feat(modal): implement modal

* feat(modal): update modal used in prompt

* feat(modal): fix test

* feat(modal): fix comments

* feat(modal): address comments

* feat(modal): remove comments
  • Loading branch information
sfeng-coveo authored Jan 23, 2025
1 parent 056f015 commit 85d2568
Show file tree
Hide file tree
Showing 14 changed files with 167 additions and 26 deletions.
4 changes: 0 additions & 4 deletions packages/components-props-analyzer/src/ComponentsList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,6 @@ const components: Component[] = [
name: 'Modal',
packageName: '@coveord/plasma-mantine',
},
{
name: 'ModalWizard',
packageName: '@coveord/plasma-mantine',
},
{
name: 'StickyFooter',
packageName: '@coveord/plasma-mantine',
Expand Down
1 change: 1 addition & 0 deletions packages/mantine/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './ellipsis-text';
export * from './header';
export * from './inline-confirm';
export * from './menu';
export * from './modal';
export * from './prompt';
export * from './read-only';
export * from './sticky-footer';
Expand Down
9 changes: 9 additions & 0 deletions packages/mantine/src/components/modal/Modal.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.footer {
border-top: 1px solid var(--mantine-color-gray-3);
}

.modalFooterSticky {
padding-bottom: 0;
margin: var(--mb-padding) calc(-1 * var(--mb-padding)) calc(var(--mantine-spacing-md) - var(--mb-padding))
calc(-1 * var(--mb-padding));
}
33 changes: 33 additions & 0 deletions packages/mantine/src/components/modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
factory,
Modal as MantineModal,
ModalFactory as MantineModalFactory,
ModalProps as MantineModalProps,
} from '@mantine/core';
import {ModalFooter, ModalFooter as PlasmaModalFooter} from './ModalFooter';

// Need to redeclare the factory to override and add footer to the props type
type PlasmaModalFactory = Omit<MantineModalFactory, 'staticComponents'> & {
staticComponents: MantineModalFactory['staticComponents'] & {
Footer: typeof ModalFooter;
};
};

const PlasmaModal = factory<PlasmaModalFactory>((props: MantineModalProps, ref) => (
<MantineModal ref={ref} {...props} />
));

PlasmaModal.displayName = '@coveord/plasma-mantine/Modal';
PlasmaModal.Root = MantineModal.Root;
PlasmaModal.Body = MantineModal.Body;
PlasmaModal.Overlay = MantineModal.Overlay;
PlasmaModal.Content = MantineModal.Content;
PlasmaModal.Header = MantineModal.Header;
PlasmaModal.Title = MantineModal.Title;
PlasmaModal.CloseButton = MantineModal.CloseButton;
PlasmaModal.Stack = MantineModal.Stack;
PlasmaModal.Footer = PlasmaModalFooter;

export const Modal = PlasmaModal;

export type ModalFactory = PlasmaModalFactory;
47 changes: 47 additions & 0 deletions packages/mantine/src/components/modal/ModalFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {useRef, useEffect} from 'react';
import clsx from 'clsx';
import {Factory, factory} from '@mantine/core';
import {StickyFooter, StickyFooterProps, StickyFooterStylesNames} from '../sticky-footer';
import classes from './Modal.module.css';

export interface ModalFooterProps extends Omit<StickyFooterProps, 'variant'> {
/**
* If the footer is sticky, its margin will be adjusted to counteract the padding of the Modal.Body, ensuring the footer visually sticks to the bottom of the modal.
*/
sticky?: boolean;
}

export type ModalFooterStylesNames = StickyFooterStylesNames;

export type ModalFooterFactory = Factory<{
props: ModalFooterProps;
ref: HTMLDivElement;
stylesNames: ModalFooterStylesNames;
}>;

const ensuresFooterHasEvenHeight = (footer: HTMLElement) => {
const remainder = footer.offsetHeight % 2;
footer.style.height = `${footer.offsetHeight - remainder + 2}px`;
};

export const ModalFooter = factory<ModalFooterFactory>(({sticky, ...props}, ref) => {
const _ref = useRef<HTMLDivElement>();

const footerRef = ref || _ref;

useEffect(() => {
if (typeof footerRef !== 'function' && footerRef.current) {
ensuresFooterHasEvenHeight(footerRef.current);
}

// if ref === 'function', this is a callback ref. Haven't found any solution for adjusting the height in this case
}, [ref, props.h]);

return (
<StickyFooter
className={clsx(classes.footer, {[classes.modalFooterSticky]: !!sticky})}
ref={footerRef}
{...props}
/>
);
});
25 changes: 25 additions & 0 deletions packages/mantine/src/components/modal/__tests__/Modal.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {render, screen} from '@test-utils';

import {Modal} from '../Modal';

// Since most part of the modal component directly inherits @mantine/core, many tests are likely covered by Mantine, so we only add tests about our customizations
describe('Modal', () => {
it('renders footer', () => {
render(
<Modal opened={true} onClose={vi.fn()}>
<Modal.Footer sticky>im the footer</Modal.Footer>
</Modal>,
);

expect(screen.getByText('im the footer')).toBeInTheDocument();
});
it('renders footer in root', () => {
render(
<Modal.Root opened={true} onClose={vi.fn()}>
<Modal.Footer>im the footer</Modal.Footer>
</Modal.Root>,
);

expect(screen.getByText('im the footer')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {render, screen} from '@test-utils';

import {Modal} from '../Modal';

describe('ModalFooter', () => {
it('renders children', () => {
render(
<Modal.Footer>
<div>im the children</div>
</Modal.Footer>,
);

expect(screen.getByText('im the children')).toBeInTheDocument();
});
it('includes the .modalFooterSticky class styling if set to sticky', () => {
render(
<Modal.Footer sticky>
<div>im the children</div>
</Modal.Footer>,
);

const footer = screen.getByText('im the children').parentElement;
expect(footer.className).contains('modalFooterSticky');
});
it('has an even height value to ensure the footer sticks completely to the bottom as a workaround to the footer positioning issue when height value is odd', () => {
render(
<Modal.Footer h={99}>
<div>im the children</div>
</Modal.Footer>,
);

const footer = screen.getByText('im the children').parentElement;
expect(footer.offsetHeight % 2).lessThanOrEqual(Number.EPSILON);
});
});
2 changes: 2 additions & 0 deletions packages/mantine/src/components/modal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Modal';
export * from './ModalFooter';
11 changes: 5 additions & 6 deletions packages/mantine/src/components/prompt/Prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import {
factory,
Factory,
Image,
Modal,
ModalRootProps,
ModalStylesNames,
StylesApiProps,
useProps,
useStyles,
} from '@mantine/core';
import {Children, ReactElement, ReactNode} from 'react';
import {Modal} from '../modal';
import Critical from './icons/critical.svg';
import Info from './icons/info.svg';
import Success from './icons/success.svg';
Expand All @@ -20,7 +20,6 @@ import {PromptContextProvider} from './Prompt.context';
import classes from './Prompt.module.css';
import {PromptCancelButton, PromptCancelButtonStylesNamesVariant} from './PromptCancelButton';
import {PromptConfirmButton, PromptConfirmButtonStylesNamesVariant} from './PromptConfirmButton';
import {PromptFooter} from './PromptFooter';

export type PromptVariant = 'success' | 'warning' | 'critical' | 'info';
export type PromptVars = {root: '--prompt-icon-size'};
Expand Down Expand Up @@ -53,7 +52,7 @@ export type PromptFactory = Factory<{
staticComponents: {
CancelButton: typeof PromptCancelButton;
ConfirmButton: typeof PromptConfirmButton;
Footer: typeof PromptFooter;
Footer: typeof Modal.Footer;
};
}>;

Expand Down Expand Up @@ -94,8 +93,8 @@ export const Prompt = factory<PromptFactory>((_props, ref) => {

const convertedChildren = Children.toArray(children) as ReactElement[];

const otherChildren = convertedChildren.filter((child) => child.type !== PromptFooter);
const footer = convertedChildren.find((child) => child.type === PromptFooter);
const otherChildren = convertedChildren.filter((child) => child.type !== Modal.Footer);
const footer = convertedChildren.find((child) => child.type === Modal.Footer);

return (
<PromptContextProvider value={{variant, getStyles}}>
Expand Down Expand Up @@ -128,4 +127,4 @@ export const Prompt = factory<PromptFactory>((_props, ref) => {

Prompt.CancelButton = PromptCancelButton;
Prompt.ConfirmButton = PromptConfirmButton;
Prompt.Footer = PromptFooter;
Prompt.Footer = Modal.Footer;
10 changes: 0 additions & 10 deletions packages/mantine/src/components/prompt/PromptFooter.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export interface StickyFooterProps
*
* The 'modal-footer' removes the modal's default padding so that the footer properly hugs the bottom of the modal.
* It also adds a border on top of the footer.
*
* @deprecated Use Modal.Footer from @coveord/plasma-mantine/Modal for modal footers. For other cases, the 'default' variant should suffice.
*/
variant?: 'default' | 'modal-footer';
}
Expand Down
2 changes: 2 additions & 0 deletions packages/mantine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export {
CopyToClipboard,
Header,
Menu,
Modal,
PasswordInput,
Select,
Table,
Expand All @@ -29,6 +30,7 @@ export {
type CopyToClipboardProps,
type HeaderProps,
type MenuItemProps,
type ModalFactory,
type TableProps,
type TableState,
} from './components';
Expand Down
6 changes: 3 additions & 3 deletions packages/website/src/examples/layout/Modal/Modal.demo.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Button, Header, Modal, StickyFooter} from '@coveord/plasma-mantine';
import {Button, Header, Modal} from '@coveord/plasma-mantine';
import {useState} from 'react';

const Demo = () => {
Expand All @@ -21,12 +21,12 @@ const Demo = () => {
sit amet risus. Praesent finibus sapien vel dolor bibendum, eget euismod metus dignissim. Phasellus
lacinia sem nunc, vel dapibus odio suscipit id. Aenean lobortis sollicitudin suscipit. Cras vitae ipsum
sit amet nibh efficitur imperdiet. Praesent scelerisque erat est. Cras dictum sodales tellus sed pretium
<StickyFooter variant="modal-footer">
<Modal.Footer sticky>
<Button variant="outline" onClick={() => setOpened(false)}>
Cancel
</Button>
<Button onClick={() => setOpened(false)}>Accept</Button>
</StickyFooter>
</Modal.Footer>
</Modal>
<Button onClick={() => setOpened(true)}>Open Modal</Button>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Button, Header, Modal, StickyFooter, Tabs} from '@coveord/plasma-mantine';
import {Button, Header, Modal, Tabs} from '@coveord/plasma-mantine';
import {useState} from 'react';
import classes from './ModalWithTabs.module.css';

Expand Down Expand Up @@ -30,12 +30,12 @@ const Demo = () => {
<Tabs.Panel value="tab-3">Tab 3 content</Tabs.Panel>
</Modal.Body>
</Tabs>
<StickyFooter borderTop>
<Modal.Footer>
<Button variant="outline" onClick={() => setOpened(false)}>
Cancel
</Button>
<Button>Save</Button>
</StickyFooter>
</Modal.Footer>
</Modal.Content>
</Modal.Root>
<Button onClick={() => setOpened(true)}>Open Modal</Button>
Expand Down

0 comments on commit 85d2568

Please sign in to comment.