diff --git a/docs/manifest.json b/docs/manifest.json index ede0e009f199fe..f1d91901cfdac5 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -875,6 +875,12 @@ "markdown_source": "../packages/components/src/flex/flex/README.md", "parent": "components" }, + { + "title": "Flyout", + "slug": "flyout", + "markdown_source": "../packages/components/src/flyout/flyout/README.md", + "parent": "components" + }, { "title": "FocalPointPicker", "slug": "focal-point-picker", diff --git a/packages/components/src/flyout/context.js b/packages/components/src/flyout/context.js new file mode 100644 index 00000000000000..f1678c99e3efd5 --- /dev/null +++ b/packages/components/src/flyout/context.js @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { createContext, useContext } from '@wordpress/element'; + +/** + * @type {import('react').Context} + */ +export const FlyoutContext = createContext( {} ); +export const useFlyoutContext = () => useContext( FlyoutContext ); diff --git a/packages/components/src/flyout/flyout-content/component.js b/packages/components/src/flyout/flyout-content/component.js new file mode 100644 index 00000000000000..aa12340a678049 --- /dev/null +++ b/packages/components/src/flyout/flyout-content/component.js @@ -0,0 +1,53 @@ +/** + * Internal dependencies + */ +import { useFlyoutContext } from '../context'; +import { FlyoutContentView, CardView } from '../styles'; +import { contextConnect, useContextSystem } from '../../ui/context'; + +/** + * + * @param {import('../../ui/context').PolymorphicComponentProps} props + * @param {import('react').Ref} forwardedRef + */ +function FlyoutContent( props, forwardedRef ) { + const { + children, + elevation, + maxWidth, + style = {}, + ...otherProps + } = useContextSystem( props, 'FlyoutContent' ); + + const { label, flyoutState } = useFlyoutContext(); + + if ( ! flyoutState ) { + throw new Error( + '`FlyoutContent` must only be used inside a `Flyout`.' + ); + } + + const showContent = flyoutState.visible || flyoutState.animating; + + return ( + + { showContent && ( + + { children } + + ) } + + ); +} + +const ConnectedFlyoutContent = contextConnect( FlyoutContent, 'FlyoutContent' ); + +export default ConnectedFlyoutContent; diff --git a/packages/components/src/flyout/flyout-content/index.js b/packages/components/src/flyout/flyout-content/index.js new file mode 100644 index 00000000000000..b404d7fd44a81a --- /dev/null +++ b/packages/components/src/flyout/flyout-content/index.js @@ -0,0 +1 @@ +export { default } from './component'; diff --git a/packages/components/src/flyout/flyout/README.md b/packages/components/src/flyout/flyout/README.md new file mode 100644 index 00000000000000..7c2937af607c1a --- /dev/null +++ b/packages/components/src/flyout/flyout/README.md @@ -0,0 +1,98 @@ +# Flyout + +
+This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. +
+ +`Flyout` is a component to render a floating content modal. It is similar in purpose to a tooltip, but renders content of any sort, not only simple text. + +## Usage + +```jsx +import { Button, __experimentalFlyout as Flyout, __experimentalText as } from '@wordpress/components'; + +function Example() { + return ( + Show/Hide content }> + Code is Poetry + + ); +} +``` + +## Props + +### `state`: `PopoverStateReturn` + +- Required: No + +### `label`: `string` + +- Required: No + +### `animated`: `boolean` + +Determines if `Flyout` has animations. + +- Required: No +- Default: `true` + +### `animationDuration`: `boolean` + +The duration of `Flyout` animations. + +- Required: No +- Default: `160` + +### `baseId`: `string` + +ID that will serve as a base for all the items IDs. See https://reakit.io/docs/popover/#usepopoverstate + +- Required: No +- Default: `160` + +### `elevation`: `number` + +Size of the elevation shadow. For more information, check out [`Card`](/packages/components/src/card/card/README.md#props). + +- Required: No +- Default: `5` + +### `maxWidth`: `CSSProperties[ 'maxWidth' ]` + +Max-width for the `Flyout` element. + +- Required: No +- Default: `360` + +### `onVisibleChange`: `( ...args: any ) => void` + +Callback for when the `visible` state changes. + +- Required: No + +### `trigger`: `FunctionComponentElement< any >` + +Element that triggers the `visible` state of `Flyout` when clicked. + +```jsx +Greet}> + Hi! I'm Olaf! + +``` + +- Required: Yes + +### `visible`: `boolean` + +Whether `Flyout` is visible. See [the `Reakit` docs](https://reakit.io/docs/popover/#usepopoverstate) for more information. + +- Required: No +- Default: `false` + +### `placement`: `PopperPlacement` + +Position of the popover element. See [the `popper` docs](https://popper.js.org/docs/v1/#popperplacements--codeenumcode) for more information. + +- Required: No +- Default: `auto` diff --git a/packages/components/src/flyout/flyout/component.js b/packages/components/src/flyout/flyout/component.js new file mode 100644 index 00000000000000..e1d8de13888566 --- /dev/null +++ b/packages/components/src/flyout/flyout/component.js @@ -0,0 +1,111 @@ +/** + * External dependencies + */ +// eslint-disable-next-line no-restricted-imports +import { PopoverDisclosure, Portal } from 'reakit'; + +/** + * WordPress dependencies + */ +import { useCallback, useMemo, cloneElement } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { contextConnect } from '../../ui/context'; +import { FlyoutContext } from '../context'; +import { useFlyoutResizeUpdater } from '../utils'; +import FlyoutContent from '../flyout-content'; +import { useUpdateEffect } from '../../utils/hooks'; +import { useFlyout } from './hook'; + +/** + * + * @param {import('../../ui/context').PolymorphicComponentProps} props + * @param {import('react').Ref} forwardedRef + */ +function Flyout( props, forwardedRef ) { + const { + children, + elevation, + label, + maxWidth, + onVisibleChange, + trigger, + flyoutState, + ...otherProps + } = useFlyout( props ); + + const resizeListener = useFlyoutResizeUpdater( { + onResize: flyoutState.unstable_update, + } ); + + const uniqueId = `flyout-${ flyoutState.baseId }`; + const labelId = label || uniqueId; + + const contextProps = useMemo( + () => ( { + label: labelId, + flyoutState, + } ), + [ labelId, flyoutState ] + ); + + const triggerContent = useCallback( + ( triggerProps ) => { + return cloneElement( trigger, triggerProps ); + }, + [ trigger ] + ); + + useUpdateEffect( () => { + onVisibleChange?.( flyoutState.visible ); + }, [ flyoutState.visible ] ); + + return ( + + { trigger && ( + + { triggerContent } + + ) } + + + { resizeListener } + { children } + + + + ); +} + +/** + * `Flyout` is a component to render a floating content modal. + * It is similar in purpose to a tooltip, but renders content of any sort, + * not only simple text. + * + * @example + * ```jsx + * import { Button, __experimentalFlyout as Flyout, __experimentalText as } from '@wordpress/components'; + * + * function Example() { + * return ( + * Show/Hide content }> + * Code is Poetry + * + * ); + * } + * ``` + */ +const ConnectedFlyout = contextConnect( Flyout, 'Flyout' ); + +export default ConnectedFlyout; diff --git a/packages/components/src/flyout/flyout/hook.js b/packages/components/src/flyout/flyout/hook.js new file mode 100644 index 00000000000000..4cdb0299019758 --- /dev/null +++ b/packages/components/src/flyout/flyout/hook.js @@ -0,0 +1,45 @@ +/** + * External dependencies + */ +// eslint-disable-next-line no-restricted-imports +import { usePopoverState } from 'reakit'; + +/** + * Internal dependencies + */ +import { useContextSystem } from '../../ui/context'; + +/** + * @param {import('../../ui/context').PolymorphicComponentProps} props + */ +export function useFlyout( props ) { + const { + animated = true, + animationDuration = 160, + baseId, + elevation = 5, + id, + maxWidth = 360, + placement, + state, + visible, + ...otherProps + } = useContextSystem( props, 'Flyout' ); + + const _flyoutState = usePopoverState( { + animated: animated ? animationDuration : undefined, + baseId: baseId || id, + placement, + visible, + ...otherProps, + } ); + + const flyoutState = state || _flyoutState; + + return { + ...otherProps, + elevation, + maxWidth, + flyoutState, + }; +} diff --git a/packages/components/src/flyout/flyout/index.js b/packages/components/src/flyout/flyout/index.js new file mode 100644 index 00000000000000..ef5d7d27828f85 --- /dev/null +++ b/packages/components/src/flyout/flyout/index.js @@ -0,0 +1,2 @@ +export { default } from './component'; +export { useFlyout } from './hook'; diff --git a/packages/components/src/flyout/index.js b/packages/components/src/flyout/index.js new file mode 100644 index 00000000000000..28e34de445ba03 --- /dev/null +++ b/packages/components/src/flyout/index.js @@ -0,0 +1 @@ +export { default as Flyout } from './flyout'; diff --git a/packages/components/src/ui/popover/stories/index.js b/packages/components/src/flyout/stories/index.js similarity index 53% rename from packages/components/src/ui/popover/stories/index.js rename to packages/components/src/flyout/stories/index.js index d42bbae04e0316..df8f41b8f279ea 100644 --- a/packages/components/src/ui/popover/stories/index.js +++ b/packages/components/src/flyout/stories/index.js @@ -1,24 +1,24 @@ /** * Internal dependencies */ -import { CardBody, CardHeader } from '../../../card'; -import Button from '../../../button'; -import { Popover } from '..'; +import { CardBody, CardHeader } from '../../card'; +import Button from '../../button'; +import { Flyout } from '..'; export default { - component: Popover, - title: 'G2 Components (Experimental)/Popover', + component: Flyout, + title: 'Components (Experimental)/Flyout', }; export const _default = () => { return ( - Click } visible placement="bottom-start" > Go Stuff - + ); }; diff --git a/packages/components/src/ui/popover/styles.js b/packages/components/src/flyout/styles.ts similarity index 50% rename from packages/components/src/ui/popover/styles.js rename to packages/components/src/flyout/styles.ts index b3bb67def80ab9..b18cf6524d83f9 100644 --- a/packages/components/src/ui/popover/styles.js +++ b/packages/components/src/flyout/styles.ts @@ -1,20 +1,19 @@ /** * External dependencies */ -// Disable reason: Temporarily disable for existing usages -// until we remove them as part of https://github.com/WordPress/gutenberg/issues/30503#deprecating-emotion-css +import styled from '@emotion/styled'; // eslint-disable-next-line no-restricted-imports -import { css } from '@emotion/css'; +import { Popover as ReakitPopover } from 'reakit'; /** * Internal dependencies */ -import { CardBody } from '../../card'; -import * as ZIndex from '../../utils/z-index'; -import CONFIG from '../../utils/config-values'; +import { Card, CardBody } from '../card'; +import * as ZIndex from '../utils/z-index'; +import CONFIG from '../utils/config-values'; -export const PopoverContent = css` - z-index: ${ ZIndex.Popover }; +export const FlyoutContentView = styled( ReakitPopover )` + z-index: ${ ZIndex.Flyout }; box-sizing: border-box; opacity: 0; outline: none; @@ -33,7 +32,7 @@ export const PopoverContent = css` } `; -export const cardStyle = css` +export const CardView = styled( Card )` ${ CardBody.selector } { max-height: 80vh; } diff --git a/packages/components/src/ui/popover/test/__snapshots__/index.js.snap b/packages/components/src/flyout/test/__snapshots__/index.js.snap similarity index 76% rename from packages/components/src/ui/popover/test/__snapshots__/index.js.snap rename to packages/components/src/flyout/test/__snapshots__/index.js.snap index 2a58dfaa6aea1d..9f2bbea548705c 100644 --- a/packages/components/src/ui/popover/test/__snapshots__/index.js.snap +++ b/packages/components/src/flyout/test/__snapshots__/index.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`props should render correctly 1`] = ` -.emotion-0 { +.emotion-1 { background-color: #fff; color: #000; position: relative; @@ -10,15 +10,15 @@ exports[`props should render correctly 1`] = ` border-radius: 2px; } -.emotion-0 .components-card-body { +.emotion-2 .components-card-body { max-height: 80vh; } -.emotion-3 { +.emotion-4 { height: 100%; } -.emotion-6 { +.emotion-7 { height: 100%; overflow-x: hidden; overflow-y: auto; @@ -28,21 +28,21 @@ exports[`props should render correctly 1`] = ` } @media only screen and ( min-device-width: 40em ) { - .emotion-6::-webkit-scrollbar { + .emotion-7::-webkit-scrollbar { height: 12px; width: 12px; } - .emotion-6::-webkit-scrollbar-track { + .emotion-7::-webkit-scrollbar-track { background-color: transparent; } - .emotion-6::-webkit-scrollbar-track { + .emotion-7::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.04); border-radius: 8px; } - .emotion-6::-webkit-scrollbar-thumb { + .emotion-7::-webkit-scrollbar-thumb { -webkit-background-clip: padding-box; background-clip: padding-box; background-color: rgba(0, 0, 0, 0.2); @@ -50,22 +50,22 @@ exports[`props should render correctly 1`] = ` border-radius: 7px; } - .emotion-6:hover::-webkit-scrollbar-thumb { + .emotion-7:hover::-webkit-scrollbar-thumb { background-color: rgba(0, 0, 0, 0.5); } } -.emotion-6:first-of-type { +.emotion-7:first-of-type { border-top-left-radius: 2px; border-top-right-radius: 2px; } -.emotion-6:last-of-type { +.emotion-7:last-of-type { border-bottom-left-radius: 2px; border-bottom-right-radius: 2px; } -.emotion-9 { +.emotion-10 { background: transparent; display: block; margin: 0!important; @@ -84,7 +84,7 @@ exports[`props should render correctly 1`] = ` border-radius: 2px; } -.emotion-12 { +.emotion-13 { background: transparent; display: block; margin: 0!important; @@ -104,12 +104,12 @@ exports[`props should render correctly 1`] = ` }