diff --git a/packages/ui-components/src/Common/ChangeHistory/Details.tsx b/packages/ui-components/src/Common/ChangeHistory/Details.tsx new file mode 100644 index 0000000000000..63772565fa458 --- /dev/null +++ b/packages/ui-components/src/Common/ChangeHistory/Details.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { + useRef, + useEffect, + type FC, + type ComponentProps, + type ReactNode, +} from 'react'; + +type ChangeHistoryDetailsProps = ComponentProps<'details'> & { + children?: ReactNode; +}; + +export const ChangeHistoryDetails: FC = ({ + children, + ...props +}) => { + const detailsRef = useRef(null); + + useEffect(() => { + const handlePointerDown = (e: PointerEvent) => { + const details = detailsRef.current; + if (details && details.open && !details.contains(e.target as Node)) { + details.removeAttribute('open'); + } + }; + + document.addEventListener('pointerdown', handlePointerDown); + return () => { + document.removeEventListener('pointerdown', handlePointerDown); + }; + }, []); + + return ( +
+ {children} +
+ ); +}; diff --git a/packages/ui-components/src/Common/ChangeHistory/__tests__/index.test.jsx b/packages/ui-components/src/Common/ChangeHistory/__tests__/index.test.jsx new file mode 100644 index 0000000000000..d74baa7bbe0a7 --- /dev/null +++ b/packages/ui-components/src/Common/ChangeHistory/__tests__/index.test.jsx @@ -0,0 +1,72 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; + +import { render, screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import ChangeHistory from '../index'; + +const sampleChanges = [ + { + label: 'v18.0.0', + versions: ['v18.0.0'], + content: 'Initial implementation', + url: 'https://github.com/nodejs/node/pull/1', + }, + { + label: 'v16.0.0', + versions: ['v16.0.0'], + content: 'Deprecated feature', + }, +]; + +describe('ChangeHistory', () => { + it('renders summary label and changes properly', () => { + render(); + + assert.ok(screen.getByText('History')); + assert.ok(screen.getByText('Initial implementation')); + assert.ok(screen.getByText('Deprecated feature')); + }); + + it('closes dropdown when clicking outside', async () => { + const { container } = render( +
+ + +
+ ); + + const details = container.querySelector('details'); + assert.ok(details); + + // Open details + details.setAttribute('open', ''); + assert.equal(details.hasAttribute('open'), true); + + // Click outside + const outsideButton = screen.getByRole('button', { name: 'Outside Button' }); + fireEvent.pointerDown(outsideButton); + + assert.equal(details.hasAttribute('open'), false); + }); + + it('does not close when clicking inside the details dropdown', async () => { + const { container } = render( + + ); + + const details = container.querySelector('details'); + assert.ok(details); + + // Open details + details.setAttribute('open', ''); + assert.equal(details.hasAttribute('open'), true); + + // Click inside + const insideItem = screen.getByText('Initial implementation'); + fireEvent.pointerDown(insideItem); + + assert.equal(details.hasAttribute('open'), true); + }); +}); diff --git a/packages/ui-components/src/Common/ChangeHistory/index.tsx b/packages/ui-components/src/Common/ChangeHistory/index.tsx index c3fc91498ce0c..87bd0ed34b7ba 100644 --- a/packages/ui-components/src/Common/ChangeHistory/index.tsx +++ b/packages/ui-components/src/Common/ChangeHistory/index.tsx @@ -4,6 +4,8 @@ import classNames from 'classnames'; import type { LinkLike } from '#ui/types'; import type { FC, ComponentProps, ReactNode } from 'react'; +import { ChangeHistoryDetails } from './Details'; + import styles from './index.module.css'; export type HistoryChange = { @@ -28,7 +30,7 @@ const ChangeHistory: FC = ({ ...props }) => (
-
+ {label} @@ -63,7 +65,7 @@ const ChangeHistory: FC = ({ })}
- + );