UNPKG

@payfit/unity-components

Version:

349 lines (286 loc) 10.3 kB
--- name: unity-overlays description: > Load when adding Unity dialogs, side panels, bottom sheets, tooltips, popovers, or menus. Use it to choose modal versus non-modal overlays and keep disabled-control tooltip behavior accessible. type: core library: '@payfit/unity-components' library_version: '2.x' sources: - 'PayFit/hr-apps:libs/shared/unity/components/src/components/dialog/Dialog.tsx' - 'PayFit/hr-apps:libs/shared/unity/components/src/components/side-panel/SidePanel.tsx' - 'PayFit/hr-apps:libs/shared/unity/components/src/components/tooltip/Tooltip.tsx' - 'PayFit/hr-apps:libs/shared/unity/components/src/components/popover/Popover.tsx' - 'PayFit/hr-apps:libs/shared/unity/components/src/components/menu/Menu.tsx' - 'PayFit/hr-apps:libs/shared/unity/components/src/components/promo-dialog/PromoDialog.tsx' - 'PayFit/hr-apps:libs/shared/unity/components/src/components/bottom-sheet/BottomSheet.tsx' --- ## Setup Overlays do not require a global provider. Each overlay owns its open state through its own trigger (`DialogTrigger`, `PopoverTrigger`, `MenuTrigger`, `TooltipTrigger`) or via the controlled `isOpen` / `onOpenChange` pair. Mount the trigger and the overlay side-by-side as siblings — `DialogTrigger` reads exactly two children: the trigger element and the overlay. ```tsx import { Button, Dialog, DialogActions, DialogButton, DialogContent, DialogTitle, DialogTrigger, } from '@payfit/unity-components' export function ConfirmDelete({ onConfirm }: { onConfirm: () => void }) { return ( <DialogTrigger> <Button variant="danger">Delete</Button> <Dialog> <DialogTitle>Delete employee?</DialogTitle> <DialogContent>This cannot be undone.</DialogContent> <DialogActions> <DialogButton variant="close">Cancel</DialogButton> <DialogButton variant="danger" onPress={onConfirm}> Delete </DialogButton> </DialogActions> </Dialog> </DialogTrigger> ) } ``` ## Modal vs non-modal — picking the right one | Need | Component | Reason | | ------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------ | | Confirmation, blocking form, decision required | `Dialog` | modal: focus trap, scroll lock, backdrop, Escape closes | | Detail view that should not interrupt page flow | `SidePanel` | modal but side-anchored; same focus trap and Escape behavior | | Mobile-first sheet, edit on small screens | `BottomSheet` | modal anchored to viewport bottom | | Feature announcement with hero illustration | `PromoDialog` | modal with required `PromoDialogHero` slot | | Hover/focus hint on an enabled control | `Tooltip` | non-interactive, hover/focus activated, single string | | Interactive informational content (links, fields) | `Popover` | non-modal, focusable content, requires `title` | | Dropdown list of actions | `Menu` (+ `MenuTrigger`/`MenuContent`/`RawMenuItem`) | non-modal, keyboard-navigable list | | Inline term definition | `DefinitionTooltip` | non-modal, inline anchor | Modal overlays all build on `react-aria-components`' `ModalOverlay` — they manage focus, restoration, scroll lock, and Escape automatically. ## Core Patterns ### Dialog with DialogTrigger + DialogActions ```tsx import { Button, Dialog, DialogActions, DialogButton, DialogContent, DialogTitle, DialogTrigger, } from '@payfit/unity-components' export function ArchiveDialog({ onArchive }: { onArchive: () => void }) { return ( <DialogTrigger> <Button>Archive</Button> <Dialog size="md"> <DialogTitle>Archive this employee?</DialogTitle> <DialogContent> They will no longer appear in active lists. You can restore them later. </DialogContent> <DialogActions> <DialogButton variant="close">Cancel</DialogButton> <DialogButton variant="confirm" onPress={onArchive}> Archive </DialogButton> </DialogActions> </Dialog> </DialogTrigger> ) } ``` ### SidePanel for detail views ```tsx import { useState } from 'react' import { Button, SidePanel, SidePanelContent, SidePanelFooter, SidePanelHeader, } from '@payfit/unity-components' export function EmployeeDetailPanel({ employee }: { employee: Employee }) { const [isOpen, setIsOpen] = useState(false) return ( <> <Button onPress={() => setIsOpen(true)}>View details</Button> <SidePanel isOpen={isOpen} onOpenChange={setIsOpen}> <SidePanelHeader>{employee.name}</SidePanelHeader> <SidePanelContent> <p>{employee.position}</p> <p>{employee.team}</p> </SidePanelContent> <SidePanelFooter> <Button variant="secondary" onPress={() => setIsOpen(false)}> Close </Button> </SidePanelFooter> </SidePanel> </> ) } ``` ### Popover for interactive informational content `title` is required (use `isTitleSrOnly` if you don't want it visible). Children may be focusable; the Popover is non-modal so the page stays interactive behind it. ```tsx import { Button, Popover, PopoverTrigger } from '@payfit/unity-components' export function HelpPopover() { return ( <PopoverTrigger> <Button variant="tertiary">What is a working agreement?</Button> <Popover title="Working agreement"> <p> A working agreement defines when, where and how an employee works. See the <a href="/handbook/working-agreement">handbook</a> for details. </p> </Popover> </PopoverTrigger> ) } ``` ### Menu for action lists `Menu` expects exactly two children: a trigger and a content block. Use `MenuTrigger` / `MenuContent` / `RawMenuItem` for the standard composition. ```tsx import { IconButton, Menu, MenuContent, MenuTrigger, RawMenuItem, } from '@payfit/unity-components' export function RowActions({ onEdit, onArchive, }: { onEdit: () => void onArchive: () => void }) { return ( <Menu> <MenuTrigger> <IconButton icon="MoreFilled" title="Actions" /> </MenuTrigger> <MenuContent> <RawMenuItem onAction={onEdit}>Edit</RawMenuItem> <RawMenuItem onAction={onArchive}>Archive</RawMenuItem> </MenuContent> </Menu> ) } ``` ## Common Mistakes ### CRITICAL Wrap a disabled control in Tooltip Wrong: ```tsx <Tooltip title="Disabled because …"> <Button isDisabled>Submit</Button> </Tooltip> ``` Correct: ```tsx <Button isDisabled>Submit</Button> <Text variant="bodySmall" color="content.neutral.low">Disabled because …</Text> ``` Unity enforces the WCAG rule that disabled controls must not carry tooltips (the disabled element is not keyboard-focusable, so the tooltip is unreachable). This is enforced at the library level, not just policy. Source: components/tooltip/Tooltip.tsx; maintainer interview (enforced by Unity) ### MEDIUM Use Dialog for a non-blocking hint Wrong: ```tsx <DialogTrigger> <Button>Info</Button> <Dialog> <DialogContent>FYI…</DialogContent> </Dialog> </DialogTrigger> ``` Correct: ```tsx <PopoverTrigger> <Button>Info</Button> <Popover title="Info">FYI…</Popover> </PopoverTrigger> // or, for hover-only: <Tooltip title="FYI…"><Button>Info</Button></Tooltip> ``` Dialog is modal (focus trap, scroll lock, backdrop) — heavy for a simple informational popover. Source: components/dialog/Dialog.tsx:162-196 (ModalOverlay) ### MEDIUM Use Tooltip for interactive content Wrong: ```tsx <Tooltip title={ <> <Button>Delete</Button> <Button>Edit</Button> </> } > <IconButton icon={MoreFilled} /> </Tooltip> ``` Correct: ```tsx <Menu> <MenuTrigger> <IconButton icon={MoreFilled} /> </MenuTrigger> <MenuContent> <RawMenuItem>Delete</RawMenuItem> <RawMenuItem>Edit</RawMenuItem> </MenuContent> </Menu> ``` Tooltip is display-only; placing interactive elements inside breaks focus and event handling. Source: components/tooltip/Tooltip.tsx (no interactive support); components/menu/Menu.tsx ### MEDIUM Fight the modal focus trap with custom useEffect focus moves Wrong: ```tsx useEffect(() => { if (open) triggerRef.current?.focus() }, [open]) ``` Correct: ```tsx // Let Dialog manage focus; use autoFocus on a specific element inside if needed: <Dialog isOpen={open} onOpenChange={setOpen}> <input autoFocus /> </Dialog> ``` Dialog manages focus and restoration via React Aria. Calling triggerRef.focus() inside a useEffect produces unpredictable jumps. Source: components/dialog/Dialog.tsx:162-196 ### MEDIUM PromoDialog without PromoDialogHero Wrong: ```tsx <PromoDialog> <PromoDialogTitle>Feature</PromoDialogTitle> <PromoDialogContent></PromoDialogContent> </PromoDialog> ``` Correct: ```tsx <PromoDialog> <PromoDialogHero> <Illustration src={MyIllustration} /> </PromoDialogHero> <PromoDialogTitle>Feature</PromoDialogTitle> <PromoDialogContent></PromoDialogContent> </PromoDialog> ``` PromoDialog validates at render and console.errors if the hero is missing; dialog still renders but the layout breaks. Source: components/promo-dialog/PromoDialog.tsx:230-236 (console.error guard) ## See also - `unity-migrate-from-midnight` — the disabled+tooltip trap is the most common Midnight-era pattern that Unity blocks; the migration skill covers the rewrite. - `unity-layout-and-styling` — overlay content uses the same `uy:` utility classes (Flex/Grid, spacing, `uy:data-[hovered=true]:…`).