diff --git a/apps/admin-x-framework/package.json b/apps/admin-x-framework/package.json index 4c74d3c9c28..9e937596725 100644 --- a/apps/admin-x-framework/package.json +++ b/apps/admin-x-framework/package.json @@ -95,7 +95,6 @@ "vitest": "catalog:" }, "dependencies": { - "@ebay/nice-modal-react": "catalog:", "@sentry/react": "catalog:", "@tanstack/react-query": "catalog:", "@tinybirdco/charts": "0.3.0", diff --git a/apps/admin/package.json b/apps/admin/package.json index e778f87b099..532e69fc8f8 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -25,7 +25,6 @@ "@codemirror/state": "catalog:", "@codemirror/theme-one-dark": "catalog:", "@dnd-kit/sortable": "catalog:", - "@ebay/nice-modal-react": "catalog:", "@sentry/react": "catalog:", "@svg-maps/world": "2.0.0", "@tanstack/react-query": "catalog:", diff --git a/apps/admin/src/settings/app/app.tsx b/apps/admin/src/settings/app/app.tsx index 992ac686b99..3db6609d3d5 100644 --- a/apps/admin/src/settings/app/app.tsx +++ b/apps/admin/src/settings/app/app.tsx @@ -1,5 +1,4 @@ import MainContent from './main-content'; -import NiceModal from '@ebay/nice-modal-react'; import SettingsAppProvider, {type UpgradeStatusType} from './components/providers/settings-app-provider'; import {ConfirmationProvider} from './components/providers/confirmation-provider'; import {DialogPortalProvider} from './components/providers/dialog-portal'; @@ -31,11 +30,9 @@ export function App({upgradeStatus}: AppProps) {
- - - - - + + +
diff --git a/apps/admin/src/settings/app/components/providers/confirmation-provider.tsx b/apps/admin/src/settings/app/components/providers/confirmation-provider.tsx index b22fb12d597..bb19070ffe8 100644 --- a/apps/admin/src/settings/app/components/providers/confirmation-provider.tsx +++ b/apps/admin/src/settings/app/components/providers/confirmation-provider.tsx @@ -30,9 +30,8 @@ export const ConfirmationProvider: React.FC<{children: React.ReactNode}> = ({chi const show = useCallback((request: Omit): ConfirmationHandle => { nextId.current += 1; const id = nextId.current; - // One request per kind, matching NiceModal's per-component keying: a - // second show replaces the first instead of stacking (StrictMode - // double-effects depend on this). + // One request per kind: a second show replaces the first instead of + // stacking (StrictMode double-effects depend on this). setRequests(current => [...current.filter(r => r.kind !== request.kind), {...request, id} as ConfirmationRequest]); return {remove: () => setRequests(current => current.filter(r => r.id !== id))}; }, []); diff --git a/apps/admin/src/settings/app/components/settings/preview-modal.tsx b/apps/admin/src/settings/app/components/settings/preview-modal.tsx index 5b6e7e1a4b5..347825af7f5 100644 --- a/apps/admin/src/settings/app/components/settings/preview-modal.tsx +++ b/apps/admin/src/settings/app/components/settings/preview-modal.tsx @@ -1,6 +1,5 @@ import React, {useEffect} from 'react'; import {ExternalLink} from 'lucide-react'; -import {useModal} from '@ebay/nice-modal-react'; import {Box, Inline, Text, type TextElement, type TextLeading, type TextSize} from '@tryghost/shade/primitives'; import {Button, type ButtonProps, Separator} from '@tryghost/shade/components'; @@ -37,8 +36,8 @@ const headingLeading: Record = { }; /** - * Compatibility shell for settings preview modals while the legacy NiceModal - * flows are migrated to consumer-controlled Shade compositions. + * Consumer-controlled shell for the settings preview dialogs (design, portal, + * newsletters, offers). New modal flows should use Shade compositions directly. */ export interface PreviewModalProps { testId?: string; @@ -72,15 +71,12 @@ export interface PreviewModalProps { onCancel?: () => void; onOk?: () => void; - /** Supersedes the NiceModal close path; without it the modal must be mounted through NiceModal. Keep its presence stable across renders — toggling defined/undefined remounts the modal subtree. */ - onClose?: () => void; + onClose: () => void; afterClose?: () => void; } -type PreviewModalContentBaseProps = Omit & {requestClose: () => void}; - -const PreviewModalContentBase: React.FC = ({ - requestClose, +export const PreviewModalContent: React.FC = ({ + onClose, testId, title, titleHeadingLevel = 4, @@ -188,7 +184,7 @@ const PreviewModalContentBase: React.FC = ({ const handleCancel = onCancel || (() => { confirm(dirty, () => { - requestClose(); + onClose(); afterClose?.(); }); }); @@ -207,7 +203,7 @@ const PreviewModalContentBase: React.FC = ({ title='' width={width} hideXOnMobile - onClose={requestClose} + onClose={onClose} > = ({ ); }; - -const NicePreviewModalContent: React.FC> = (props) => { - const modal = useModal(); - return modal.remove()} />; -}; - -export const PreviewModalContent: React.FC = ({onClose, ...props}) => { - if (onClose) { - return ; - } - return ; -}; diff --git a/apps/shade/package.json b/apps/shade/package.json index 98aa4f16b86..b2cf27bb1b8 100644 --- a/apps/shade/package.json +++ b/apps/shade/package.json @@ -107,7 +107,6 @@ "@dnd-kit/core": "catalog:", "@dnd-kit/sortable": "catalog:", "@dnd-kit/utilities": "catalog:", - "@ebay/nice-modal-react": "catalog:", "@hookform/resolvers": "5.4.0", "@number-flow/react": "0.6.2", "@radix-ui/react-accordion": "1.2.15", diff --git a/apps/shade/src/components/patterns/settings-modal.stories.tsx b/apps/shade/src/components/patterns/settings-modal.stories.tsx index 0c322108e01..81542e10575 100644 --- a/apps/shade/src/components/patterns/settings-modal.stories.tsx +++ b/apps/shade/src/components/patterns/settings-modal.stories.tsx @@ -1,16 +1,17 @@ -import NiceModal from '@ebay/nice-modal-react'; +import {useState} from 'react'; import type {Meta, StoryObj} from '@storybook/react-vite'; import {Button} from '@/components/ui/button'; import {Box} from '@/components/primitives/box'; import {SettingsModal, type SettingsModalProps} from '@/components/patterns/settings-modal'; -const SettingsModalStory = (props: SettingsModalProps) => { - const StoryModal = NiceModal.create(() => ); +const SettingsModalStory = (props: Omit) => { + const [open, setOpen] = useState(false); return ( - + + {open && setOpen(false)} />} ); }; @@ -22,15 +23,10 @@ const meta = { parameters: { docs: { description: { - component: 'Transitional compatibility shell for the existing settings NiceModal flows. New modal flows should use Shade Dialog primitives directly.' + component: 'Consumer-controlled shell for the legacy full-page settings dialogs. New modal flows should use Shade Dialog primitives directly.' } } - }, - decorators: [Story => ( - - - - )] + } } satisfies Meta; export default meta; diff --git a/apps/shade/src/components/patterns/settings-modal.tsx b/apps/shade/src/components/patterns/settings-modal.tsx index 74d78553fb0..a8c802ff6f8 100644 --- a/apps/shade/src/components/patterns/settings-modal.tsx +++ b/apps/shade/src/components/patterns/settings-modal.tsx @@ -1,4 +1,3 @@ -import {useModal} from '@ebay/nice-modal-react'; import {cva} from 'class-variance-authority'; import {X} from 'lucide-react'; import React, {forwardRef, useEffect, useState} from 'react'; @@ -14,8 +13,8 @@ import useGlobalDirtyState from '@/hooks/use-global-dirty-state'; import {cn} from '@/lib/utils'; /** - * Compatibility shell for settings modals while the legacy NiceModal flows are - * migrated to Shade's consumer-controlled Dialog primitives. + * Consumer-controlled settings modal shell. Legacy full-page settings dialogs + * still render through it; new modal flows should use Shade Dialog primitives. */ export type SettingsModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'bleed'; @@ -44,8 +43,7 @@ export interface SettingsModalProps { onCancel?: () => void; topRightContent?: 'close' | React.ReactNode; hideXOnMobile?: boolean; - /** Supersedes the NiceModal close path; without it the modal must be mounted through NiceModal. Keep its presence stable across renders — toggling defined/undefined remounts the modal subtree. */ - onClose?: () => void; + onClose: () => void; afterClose?: () => void; children?: React.ReactNode; backDrop?: boolean; @@ -124,9 +122,7 @@ const headerOffsets: Record = { bleed: '-inset-x-10' }; -type SettingsModalContentProps = Omit & {requestClose: () => void}; - -const SettingsModalContent = forwardRef(({ +const SettingsModal = forwardRef(({ 'aria-label': ariaLabel, className, size = 'md', @@ -150,7 +146,7 @@ const SettingsModalContent = forwardRef( onCancel, topRightContent, hideXOnMobile = false, - requestClose, + onClose, afterClose, children, backDrop = true, @@ -173,7 +169,7 @@ const SettingsModalContent = forwardRef( const removeModal = () => { confirm(dirty, () => { - requestClose(); + onClose(); afterClose?.(); }); }; @@ -359,22 +355,6 @@ const SettingsModalContent = forwardRef( ); }); -SettingsModalContent.displayName = 'SettingsModalContent'; - -const NiceSettingsModal = forwardRef>((props, ref) => { - const modal = useModal(); - return modal.remove()} />; -}); - -NiceSettingsModal.displayName = 'NiceSettingsModal'; - -const SettingsModal = forwardRef(({onClose, ...props}, ref) => { - if (onClose) { - return ; - } - return ; -}); - SettingsModal.displayName = 'SettingsModal'; export {SettingsModal, settingsModalVariants}; diff --git a/apps/shade/test/unit/components/patterns/settings-modal.test.tsx b/apps/shade/test/unit/components/patterns/settings-modal.test.tsx index 42cfbc834ce..2bd7a34dc37 100644 --- a/apps/shade/test/unit/components/patterns/settings-modal.test.tsx +++ b/apps/shade/test/unit/components/patterns/settings-modal.test.tsx @@ -1,15 +1,8 @@ -import NiceModal from '@ebay/nice-modal-react'; -import {act, fireEvent, render, screen, waitFor} from '@testing-library/react'; +import {fireEvent, render, screen, waitFor} from '@testing-library/react'; import {describe, expect, it, vi} from 'vitest'; import {SettingsModal, settingsModalVariants, type SettingsModalSize} from '@/components/patterns/settings-modal'; -const TestSettingsModal = NiceModal.create(() => ( - undefined}> - Modal content - -)); - describe('SettingsModal', () => { it.each(['sm', 'md', 'lg', 'xl', 'full'])('uses the standard dialog radius for the %s size', (size) => { expect(settingsModalVariants({size})).toContain('rounded-lg'); @@ -21,27 +14,23 @@ describe('SettingsModal', () => { expect(settingsModalVariants({size})).not.toContain('rounded-lg'); }); - it('uses content-sized outline and primary actions by default', async () => { - render(); - - act(() => { - void NiceModal.show(TestSettingsModal); - }); + it('uses content-sized outline and primary actions by default', () => { + render( + undefined} onOk={() => undefined}> + Modal content + + ); - const cancelButton = await screen.findByRole('button', {name: 'Cancel'}); + const cancelButton = screen.getByRole('button', {name: 'Cancel'}); const okButton = screen.getByRole('button', {name: 'OK'}); expect(cancelButton.className).toContain('border-control-border'); expect(cancelButton.className).toContain('bg-transparent'); expect(cancelButton.className).not.toContain('hover:bg-accent'); expect(okButton.className).not.toContain('min-w-20'); - - act(() => { - void NiceModal.remove(TestSettingsModal); - }); }); - it('renders without a NiceModal context and closes through onClose', () => { + it('closes through onClose', () => { const onClose = vi.fn(); render( @@ -71,23 +60,4 @@ describe('SettingsModal', () => { expect(onClose).toHaveBeenCalledTimes(1); }); }); - - it('still closes through NiceModal when no onClose is passed', async () => { - const BridgeModal = NiceModal.create(() => ( - - Modal content - - )); - - render(); - act(() => { - void NiceModal.show(BridgeModal); - }); - - fireEvent.click(await screen.findByTestId('close-modal')); - - await waitFor(() => { - expect(screen.queryByTestId('close-modal')).toBeNull(); - }); - }); }); diff --git a/apps/shade/test/unit/components/ui/overlay-escape.test.tsx b/apps/shade/test/unit/components/ui/overlay-escape.test.tsx index ad3c3d04776..71cc091f921 100644 --- a/apps/shade/test/unit/components/ui/overlay-escape.test.tsx +++ b/apps/shade/test/unit/components/ui/overlay-escape.test.tsx @@ -1,5 +1,4 @@ -import NiceModal from '@ebay/nice-modal-react'; -import {act, fireEvent, render, screen, waitFor} from '@testing-library/react'; +import {fireEvent, render, screen, waitFor} from '@testing-library/react'; import {useState} from 'react'; import {beforeAll, describe, expect, it, vi} from 'vitest'; @@ -55,17 +54,11 @@ describe('nested overlay Escape behavior', () => { it.each(overlayCases)('closes an uncontrolled %s before its parent SettingsModal', async (_name, Overlay) => { const onCancel = vi.fn(); const onOpenChange = vi.fn(); - const TestModal = NiceModal.create(() => ( - + render( + - )); - - render(); - - act(() => { - void NiceModal.show(TestModal); - }); + ); await screen.findByText(/Menu item|Option one|Popover content|Combobox content/); fireEvent.keyDown(document, {key: 'Escape'}); @@ -80,10 +73,6 @@ describe('nested overlay Escape behavior', () => { await waitFor(() => { expect(onCancel).toHaveBeenCalledOnce(); }); - - act(() => { - void NiceModal.remove(TestModal); - }); }); it('preserves controlled overlay state behavior', async () => { @@ -102,16 +91,11 @@ describe('nested overlay Escape behavior', () => { ); }; - const TestModal = NiceModal.create(() => ( - + render( + - )); - - render(); - act(() => { - void NiceModal.show(TestModal); - }); + ); await screen.findByText('Controlled content'); fireEvent.keyDown(document, {key: 'Escape'}); @@ -126,8 +110,8 @@ describe('nested overlay Escape behavior', () => { const onCancel = vi.fn(); const onOuterOpenChange = vi.fn(); const onInnerOpenChange = vi.fn(); - const TestModal = NiceModal.create(() => ( - + render( + Open outer @@ -141,12 +125,7 @@ describe('nested overlay Escape behavior', () => { - )); - - render(); - act(() => { - void NiceModal.show(TestModal); - }); + ); await screen.findByText('Inner item'); fireEvent.keyDown(document, {key: 'Escape'}); @@ -179,16 +158,11 @@ describe('nested overlay Escape behavior', () => { it('keeps the modal open when a later document listener handles Escape', async () => { const onCancel = vi.fn(); - const TestModal = NiceModal.create(() => ( - + render( + Modal content - )); - - render(); - act(() => { - void NiceModal.show(TestModal); - }); + ); await screen.findByText('Modal content'); const handleNestedEscape = (event: KeyboardEvent) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a1fd2f281d..27751ee72b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,9 +57,6 @@ catalogs: '@dnd-kit/utilities': specifier: 3.2.2 version: 3.2.2 - '@ebay/nice-modal-react': - specifier: 1.2.13 - version: 1.2.13 '@eslint/compat': specifier: 2.1.0 version: 2.1.0 @@ -748,9 +745,6 @@ importers: '@dnd-kit/sortable': specifier: 'catalog:' version: 7.0.2(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) - '@ebay/nice-modal-react': - specifier: 'catalog:' - version: 1.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@sentry/react': specifier: 'catalog:' version: 7.120.4(react@18.3.1) @@ -990,9 +984,6 @@ importers: apps/admin-x-framework: dependencies: - '@ebay/nice-modal-react': - specifier: 'catalog:' - version: 1.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@sentry/react': specifier: 'catalog:' version: 7.120.4(react@18.3.1) @@ -1724,9 +1715,6 @@ importers: '@dnd-kit/utilities': specifier: 'catalog:' version: 3.2.2(react@18.3.1) - '@ebay/nice-modal-react': - specifier: 'catalog:' - version: 1.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@hookform/resolvers': specifier: 5.4.0 version: 5.4.0(react-hook-form@7.80.0(react@18.3.1)) @@ -5282,12 +5270,6 @@ packages: react: '>=17.0.2' react-dom: '>=17.0.2' - '@ebay/nice-modal-react@1.2.13': - resolution: {integrity: sha512-jx8xIWe/Up4tpNuM02M+rbnLoxdngTGk3Y8LjJsLGXXcSoKd/+eZStZcAlIO/jwxyz/bhPZnpqPJZWAmhOofuA==} - peerDependencies: - react: '>16.8.0' - react-dom: '>16.8.0' - '@elastic/elasticsearch@8.19.2': resolution: {integrity: sha512-LMJCju/+AZkDlJArd/MYABWTDrHi4U7j3qGTKi1hYC7+67SaiYmYFItiueACQVrj2j3ECPMcguZ+7WrZeB+Z5g==} engines: {node: '>=20'} @@ -24237,11 +24219,6 @@ snapshots: react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - '@ebay/nice-modal-react@1.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - '@elastic/elasticsearch@8.19.2(supports-color@10.2.2)': dependencies: '@elastic/transport': 8.10.1(supports-color@10.2.2) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d2de342fea6..8b714738fe2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -54,7 +54,6 @@ catalog: '@codemirror/state': 6.7.1 '@codemirror/theme-one-dark': 6.1.3 '@codemirror/view': 6.43.6 - '@ebay/nice-modal-react': 1.2.13 '@eslint/compat': 2.1.0 '@eslint/js': 9.39.5 '@faker-js/faker': 10.5.0