import { fireEvent, render, screen } from '@testing-library/react' import { useState } from 'react' import { describe, expect, it, vi } from 'vitest' import '../styles/tokens.css' import { animationKeyframes, declaredStyle, reducedMotionStyle } from '../test/cssRules.ts' import { Sheet } from './Sheet.tsx' import styles from './Sheet.module.css' function Harness({ withControls = true }: { withControls?: boolean }) { const [open, setOpen] = useState(false) return ( <> setOpen(false)}>

Grouped by dinner

{withControls && ( <> )}
) } function openSheet() { const opener = screen.getByRole('button', { name: 'Open Supply run' }) opener.focus() fireEvent.click(opener) return { opener, dialog: screen.getByRole('dialog', { name: 'Supply run' }) } } function scrim() { const element = document.querySelector(`.${styles.scrim}`) if (!element) throw new Error('scrim not rendered') return element } describe('Sheet', () => { it('renders nothing while closed', () => { render() expect(screen.queryByRole('dialog')).not.toBeInTheDocument() }) it('is a labelled modal dialog', () => { render() const { dialog } = openSheet() expect(dialog).toHaveAttribute('aria-modal', 'true') }) it('accepts aria-labelledby as its accessible name', () => { render( <>

Quest log

{}}>

Nothing yet

, ) expect(screen.getByRole('dialog', { name: 'Quest log' })).toBeInTheDocument() }) it('moves focus to the first control and restores it to the opener on close', () => { render() const { opener } = openSheet() expect(screen.getByRole('button', { name: 'Try it' })).toHaveFocus() fireEvent.keyDown(document.activeElement!, { key: 'Escape' }) expect(screen.queryByRole('dialog')).not.toBeInTheDocument() expect(opener).toHaveFocus() }) it('focuses the dialog itself when it has no focusable control', () => { render() const { dialog } = openSheet() expect(dialog).toHaveFocus() }) it('skips controls that cannot take keyboard focus when moving focus in', () => { render( {}}> , ) expect(screen.getByRole('button', { name: 'Try it' })).toHaveFocus() }) it('focuses the dialog itself when every control is hidden', () => { render( {}}> , ) expect(screen.getByRole('dialog', { name: 'Quest log' })).toHaveFocus() }) it('does not stop the Tab trap on a hidden control', () => { render( {}}> , ) const first = screen.getByRole('button', { name: 'Try it' }) const last = screen.getByRole('button', { name: 'Pass' }) last.focus() fireEvent.keyDown(last, { key: 'Tab' }) expect(first).toHaveFocus() fireEvent.keyDown(first, { key: 'Tab', shiftKey: true }) expect(last).toHaveFocus() }) it('does not stop the Tab trap on a control disabled by its fieldset', () => { render( {}}>
, ) const first = screen.getByRole('button', { name: 'Try it' }) const last = screen.getByRole('button', { name: 'Pass' }) expect(first).toHaveFocus() last.focus() fireEvent.keyDown(last, { key: 'Tab' }) expect(first).toHaveFocus() fireEvent.keyDown(first, { key: 'Tab', shiftKey: true }) expect(last).toHaveFocus() }) it('wraps Tab and Shift+Tab inside the sheet', () => { render() openSheet() const first = screen.getByRole('button', { name: 'Try it' }) const last = screen.getByRole('button', { name: 'Pass' }) last.focus() fireEvent.keyDown(last, { key: 'Tab' }) expect(first).toHaveFocus() fireEvent.keyDown(first, { key: 'Tab', shiftKey: true }) expect(last).toHaveFocus() }) it('keeps focus on the dialog when Tab has nowhere to go', () => { render() const { dialog } = openSheet() const tab = fireEvent.keyDown(dialog, { key: 'Tab' }) expect(tab).toBe(false) // default prevented expect(dialog).toHaveFocus() }) it('closes on Escape', () => { const onClose = vi.fn() render( , ) fireEvent.keyDown(screen.getByRole('button', { name: 'Try it' }), { key: 'Escape' }) expect(onClose).toHaveBeenCalledTimes(1) }) it('closes on a scrim click but not on a panel click', () => { const onClose = vi.fn() render(

Grouped by dinner

, ) fireEvent.click(screen.getByRole('dialog')) fireEvent.click(screen.getByText('Grouped by dinner')) expect(onClose).not.toHaveBeenCalled() fireEvent.click(scrim()) expect(onClose).toHaveBeenCalledTimes(1) }) it('makes the background inert while open and restores it on close', () => { const { container } = render() openSheet() const root = screen.getByRole('dialog').parentElement! expect(root.parentElement).toBe(document.body) expect(container).toHaveAttribute('inert') expect(root).not.toHaveAttribute('inert') fireEvent.click(scrim()) expect(container).not.toHaveAttribute('inert') }) it('leaves an already-inert background element inert after close', () => { const other = document.createElement('div') other.setAttribute('inert', '') document.body.append(other) try { render() openSheet() fireEvent.click(scrim()) expect(other).toHaveAttribute('inert') } finally { other.remove() } }) it('rises from 100% to 0 over 320 ms with the soft ease-out', () => { render() const { dialog } = openSheet() expect(declaredStyle(dialog).animation.split(' ').slice(1)).toEqual(['320ms', 'var(--ease-out-soft)']) const easing = getComputedStyle(document.documentElement).getPropertyValue('--ease-out-soft') expect(easing.replace(/\s/g, '')).toBe('cubic-bezier(.2,.8,.2,1)') const rise = animationKeyframes(dialog) expect(rise.findRule('from')?.style.transform).toBe('translateY(100%)') expect(rise.findRule('to')?.style.transform).toBe('translateY(0)') }) it('fades the scrim in to 50% neutral-900', () => { render() openSheet() const style = declaredStyle(scrim()) expect(style.background).toBe('color-mix(in srgb, var(--color-neutral-900) 50%, transparent)') expect(style.animation.split(' ').slice(1)).toEqual(['320ms', 'var(--ease-out-soft)']) const fade = animationKeyframes(scrim()) expect(fade.findRule('from')?.style.opacity).toBe('0') expect(fade.findRule('to')?.style.opacity).toBe('1') }) it('opens in place with no rise or fade under reduced motion', () => { render() const { dialog } = openSheet() expect(reducedMotionStyle(dialog).animation).toBe('none') expect(reducedMotionStyle(scrim()).animation).toBe('none') expect(declaredStyle(dialog).transform).toBe('translateY(0)') }) })