UNPKG

selection-popover

Version:

Easy-to-use, composable react selection popover.

304 lines (250 loc) 16.5 kB
<p align="center"> <a align='center' href="https://selection-popover.vercel.app"> <img src="./website/public/og.png" /> </a> </p> <p align="center"> Easy-to-use, composable react selection popover </p> <div align="center"> <a href="https://www.npmjs.com/package/selection-popover">![npm version](https://img.shields.io/npm/v/selection-popover.svg)</a> <a href="https://www.npmjs.com/package/selection-popover">![npm downloads](https://img.shields.io/npm/dm/selection-popover.svg)</a> </div> ## Install ```bash npm install selection-popover ``` ## Content - [Anatomy](#anatomy) - [API Reference](#api-reference) - [Root](#root) - [Trigger](#trigger) - [Portal](#portal) - [Content](#content) - [Arrow](#arrow) - [Examples](#examples) - [Origin-aware animations](#origin-aware-animations) - [Collision-aware animations](#collision-aware-animations) - [Unmount animations](#unmount-animations) - [Use with Radix Toolbar](#use-with-radix-toolbar) - [Acknowledgements](#acknowledgements) ## Anatomy Import all parts and piece them together. ```jsx import * as Selection from 'selection-popover' export default () => ( <Selection.Root> <Selection.Trigger /> <Selection.Portal> <Selection.Content> <Selection.Arrow /> </Selection.Content> </Selection.Portal> </Selection.Root> ) ``` ## API Reference ### Root Contains all the parts of a selection. | Prop | Type | Default | Description | | -------------- | ------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------- | | `defaultOpen` | `boolean` | - | The open state of the hover card when it is initially rendered. Use when you do not need to control its open state. | | `open` | `boolean` | - | The controlled open state of the popover. Must be used in conjunction with `onOpenChange`. | | `onOpenChange` | `(open: boolean) => void` | - | Event handler called when the open state of the popover changes. | | `whileSelect` | `boolean` | `false` | When `true`, the popover will open while the text is selected, otherwise only when the mouse up. | | `disabled` | `boolean` | `false` | When true, the popover won't open when text is selected. | | `openDelay` | `number` | `0` | The duration from when release the mouse until the content opens. In `whileSelect` is when you start the selection. | | `closeDelay` | `number` | `0` | The duration from when you click outside of the content until the content closes. | ### Trigger The area that opens the popover. Wrap it around the target you want the popover to open when a text is selected. | Prop | Type | Default | Description | | --------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `asChild` | `boolean` | `false` | Change the component to the HTML tag or custom component of the only child. This will merge the original component props with the props of the supplied element/component and change the underlying DOM node. | ### Portal When used, portals the content part into the `body`. | Prop | Type | Default | Description | | ------------ | ------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `forceMount` | `boolean` | - | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. If used on this part, it will be inherited by `Selection.Content`. | | `container` | `HTMLElement` | `document.body` | Specify a container element to portal the content into. | ### Content The component that pops out when a text is selected. | Prop | Type | Default | Description | | ---------------------- | --------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `asChild` | `boolean` | false | Change the component to the HTML tag or custom component of the only child. This will merge the original component props with the props of the supplied element/component and change the underlying DOM node. | | `forceMount` | `boolean` | - | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. It inherits from `Selection.Portal`. | | `side` | `"top" \| "right" \| "bottom" \| "left"` | `top` | The preferred side of the selection to render against when open. Will be reversed when collisions occur and `avoidCollisions` is enabled. | | `sideOffset` | `number` | `0` | The distance in pixels from the selection. | | `align` | `"start" \| "center" \| "end"` | `center` | The preferred alignment against the selection. May change when collisions occur. | | `alignOffset` | `number` | `0` | An offset in pixels from the `"start"` or `"end"` alignment options. | | `avoidCollisions` | `boolean` | `true` | When `true`, overrides the `side` and `align` preferences to prevent collisions with boundary edges. | | `collisionBoundary` | `Element \| null \| Array<Element \| null>` | `[]` | The element used as the collision boundary. By default this is the viewport, though you can provide additional element(s) to be included in this check. | | `collisionPadding` | `number \| Partial<Record<Side, number>>` | `0` | The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for all sides), or a partial padding object, for example: `{ top: 20, left: 20 }`. | | `arrowPadding` | `number` | `0` | The padding between the arrow and the edges of the content. If your content has `border-radius`, this will prevent it from overflowing the corners. | | `sticky` | `"partial" \| "always"` | `partial` | The sticky behavior on the align axis. `"partial"` will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst `"always"` will keep the content in the boundary regardless. | | `hideWhenDetached` | `boolean` | `false` | Whether to hide the content when the text becomes fully occluded. | | `onEscapeKeyDown` | `(event: KeyboardEvent) => void` | - | Event handler called when the escape key is down. It can be prevented by calling `event.preventDefault`. | | `onPointerDownOutside` | `(event: PointerDownOutsideEvent) => void` | - | Event handler called when a pointer event occurs outside the bounds of the component. It can be prevented by calling `event.preventDefault`. | | `onFocusOutside` | `(event: FocusOutsideEvent) => void` | - | Event handler called when focus moves outside the bounds of the component. It can be prevented by calling `event.preventDefault`. | | `onInteractOutside` | `(event: PointerDownOutsideEvent \| FocusOutsideEvent) => void` | - | Event handler called when an interaction (pointer or focus event) happens outside the bounds of the component. It can be prevented by calling `event.preventDefault`. | | Data Attribute | Values | | -------------- | ---------------------------------------- | | `[data-state]` | `"open" \| "closed"` | | `[data-side]` | `"left" \| "right" \| "bottom" \| "top"` | | `[data-align]` | `"start" \| "end" \| "center"` | | CSS Variable | Description | | ---------------------------------------------- | ----------------------------------------------------------------------------- | | `--selection-popover-content-transform-origin` | The `transform-origin` computed from the content and arrow positions/offsets. | | `--selection-popover-select-width` | The width of the select. | | `--selection-popover-select-height` | The height of the select. | ### Arrow An optional arrow element to render alongside the popover. This can be used to help visually link the selected text with the `Selection.Content`. Must be rendered inside `Selection.Content`. | Prop | Type | Default | Description | | --------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `asChild` | `boolean` | `false` | Change the component to the HTML tag or custom component of the only child. This will merge the original component props with the props of the supplied element/component and change the underlying DOM node. | | `width` | `number` | `10` | The width of the arrow in pixels. | | `height` | `number` | `5` | The height of the arrow in pixels. | ## Examples ### Origin-aware animations ```jsx // index.jsx import * as Selection from 'selection-popover' import './styles.css' export default () => ( <Selection.Root> <Selection.Trigger>...</Selection.Trigger> <Selection.Portal> <Selection.Content className="SelectionContent">...</Selection.Content> </Selection.Portal> </Selection.Root> ) ``` ```css /* styles.css */ .SelectionContent { transform-origin: var(--selection-popover-content-transform-origin); animation: scaleIn 500ms cubic-bezier(0.16, 1, 0.3, 1); } @keyframes scaleIn { from { opacity: 0; transform: scale(0); } to { opacity: 1; transform: scale(1); } } ``` ### Collision-aware animations ```jsx // index.jsx import * as Selection from 'selection-popover' import './styles.css' export default () => ( <Selection.Root> <Selection.Trigger>...</Selection.Trigger> <Selection.Portal> <Selection.Content className="SelectionContent">...</Selection.Content> </Selection.Portal> </Selection.Root> ) ``` ```css /* styles.css */ .SelectionContent { animation-duration: 400ms; animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); } .SelectionContent[data-state='open'][data-side='top'] { animation-name: slideDownAndFade; } .SelectionContent[data-state='open'][data-side='bottom'] { animation-name: slideUpAndFade; } @keyframes slideDownAndFade { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: translateY(0); } } @keyframes slideUpAndFade { from { opacity: 0; transform: translateY(2px)); } to { opacity: 1; transform: translateY(0); } } ``` ### Unmount animations ```jsx // index.jsx import * as Selection from 'selection-popover' import './styles.css' export default () => ( <Selection.Root> <Selection.Trigger>...</Selection.Trigger> <Selection.Portal> <Selection.Content className="SelectionContent">...</Selection.Content> </Selection.Portal> </Selection.Root> ) ``` ```css /* styles.css */ .SelectionContent { animation-duration: 400ms; animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); } .SelectionContent[data-state='open'] { animation-name: slideDownAndFade; } .SelectionContent[data-state='closed'] { animation-name: slideUpAndFade; } @keyframes slideDownAndFade { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: translateY(0); } } @keyframes slideUpAndFade { from { opacity: 1; transform: translateY(0)); } to { opacity: 0; transform: translateY(-2px); } } ``` ### Use with [Radix Toolbar](https://www.radix-ui.com/docs/primitives/components/toolbar) ```jsx import * as Selection from 'selection-popover' import * as Toolbar from '@radix-ui/react-toolbar' export default () => ( <Selection.Root> <Selection.Trigger>...</Selection.Trigger> <Selection.Portal> <Selection.Content asChild> <Toolbar.Root>...</Toolbar.Root> <Selection.Arrow /> </Selection.Content> </Selection.Portal> </Selection.Root> ) ``` ## Acknowledgements - API heavily inspired on [Radix UI](https://www.radix-ui.com/) - Inspired by this [tweet](https://twitter.com/TobiasWhetton/status/1612821266242715648) from [Tobias Whetton](https://twitter.com/TobiasWhetton)