diff --git a/README.md b/README.md index 4325538..b996b03 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,23 @@ function App() { /> ``` +### Modal + +`className`, `panelClassName`, and `style` target the modal panel. `overlayClassName` targets the backdrop. + +```tsx + + + Review the selected documents before continuing. + +``` + ### FilterTabs ```tsx diff --git a/src/Modal/Modal.stories.tsx b/src/Modal/Modal.stories.tsx index 6cbd603..0f95095 100644 --- a/src/Modal/Modal.stories.tsx +++ b/src/Modal/Modal.stories.tsx @@ -25,6 +25,12 @@ const meta: Meta = { closeOnEscape: { control: 'boolean', }, + panelClassName: { + control: 'text', + }, + overlayClassName: { + control: 'text', + }, }, }; @@ -241,6 +247,81 @@ export const WithOverlayClassName: Story = { }, }; +export const WithCustomPanelWidth: Story = { + render: () => { + const [open, setOpen] = useState(false); + + return ( + <> + + setOpen(true)} + style={{ + padding: '10px 20px', + background: '#E85A4F', + color: 'white', + border: 'none', + borderRadius: '8px', + cursor: 'pointer', + fontWeight: 500, + }} + > + Open Modal with Custom Panel + + + setOpen(false)} + size="sm" + panelClassName="batch-run-panel" + style={{ maxWidth: 520 }} + > + setOpen(false)} + /> + + + This modal uses panelClassName to apply a 520px max-width + without targeting the internal .oc-modal selector. + + + + setOpen(false)} + style={{ + padding: '8px 16px', + background: 'transparent', + color: '#1A1A1A', + border: '1px solid #E5E5E5', + borderRadius: '8px', + cursor: 'pointer', + }} + > + Cancel + + setOpen(false)} + style={{ + padding: '8px 16px', + background: '#E85A4F', + color: 'white', + border: 'none', + borderRadius: '8px', + cursor: 'pointer', + fontWeight: 500, + }} + > + Run + + + + > + ); + }, +}; + export const Confirmation: Story = { render: () => { const [open, setOpen] = useState(false); diff --git a/src/Modal/Modal.test.tsx b/src/Modal/Modal.test.tsx new file mode 100644 index 0000000..0df546d --- /dev/null +++ b/src/Modal/Modal.test.tsx @@ -0,0 +1,52 @@ +// @vitest-environment jsdom +import React from 'react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { cleanup, render, screen } from '@testing-library/react'; +import { Modal, ModalBody } from './Modal'; + +afterEach(() => { + cleanup(); +}); + +function renderOpenModal(props: Partial> = {}) { + const onClose = vi.fn(); + + render( + + Modal content + + ); + + return { onClose }; +} + +describe('Modal panel API', () => { + it('applies className, panelClassName, and style to the dialog panel', () => { + renderOpenModal({ + className: 'consumer-panel', + panelClassName: 'batch-run-panel', + style: { maxWidth: 520 }, + }); + + const panel = screen.getByRole('dialog'); + + expect(panel.classList.contains('oc-modal')).toBe(true); + expect(panel.classList.contains('consumer-panel')).toBe(true); + expect(panel.classList.contains('batch-run-panel')).toBe(true); + expect((panel as HTMLElement).style.maxWidth).toBe('520px'); + }); + + it('keeps overlayClassName scoped to the backdrop overlay', () => { + renderOpenModal({ + panelClassName: 'consumer-panel', + overlayClassName: 'consumer-overlay', + }); + + const panel = screen.getByRole('dialog'); + const overlay = panel.parentElement; + + expect(panel.classList.contains('consumer-overlay')).toBe(false); + expect(overlay?.classList.contains('consumer-overlay')).toBe(true); + expect(overlay?.classList.contains('consumer-panel')).toBe(false); + }); +}); diff --git a/src/Modal/Modal.tsx b/src/Modal/Modal.tsx index fdc1d50..0591e19 100644 --- a/src/Modal/Modal.tsx +++ b/src/Modal/Modal.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, ReactNode, HTMLAttributes, useEffect, useCallback } from 'react'; +import React, { forwardRef, ReactNode, HTMLAttributes, CSSProperties, useEffect, useCallback } from 'react'; import { createPortal } from 'react-dom'; export type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'fullscreen'; @@ -9,6 +9,13 @@ export interface ModalProps extends Omit, 'title' size?: ModalSize; closeOnOverlay?: boolean; closeOnEscape?: boolean; + /** Applies to the modal panel. Use this for panel styling without targeting .oc-modal. */ + className?: string; + /** Applies to the modal panel. Supports arbitrary panel sizing such as maxWidth. */ + style?: CSSProperties; + /** Additional class name applied to the modal panel. */ + panelClassName?: string; + /** Class name applied to the backdrop overlay. */ overlayClassName?: string; children?: ReactNode; } @@ -35,6 +42,7 @@ export const Modal = forwardRef( closeOnEscape = true, overlayClassName = '', className = '', + panelClassName = '', children, ...props }, @@ -72,6 +80,7 @@ export const Modal = forwardRef( 'oc-modal', `oc-modal--${size}`, className, + panelClassName, ].filter(Boolean).join(' '); const overlayClasses = [
+ This modal uses panelClassName to apply a 520px max-width + without targeting the internal .oc-modal selector. +
panelClassName
.oc-modal