diff --git a/docs/manifest.json b/docs/manifest.json index b65c0aead24a5f..80b5154a995ecd 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1229,6 +1229,12 @@ "markdown_source": "../packages/components/src/visually-hidden/README.md", "parent": "components" }, + { + "title": "ZStack", + "slug": "z-stack", + "markdown_source": "../packages/components/src/z-stack/README.md", + "parent": "components" + }, { "title": "Package Reference", "slug": "packages", diff --git a/packages/components/src/index.js b/packages/components/src/index.js index 0378e2e69e038a..233bc8c56f7762 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -146,6 +146,7 @@ export { useSlot as __experimentalUseSlot, } from './slot-fill'; export { default as __experimentalStyleProvider } from './style-provider'; +export { ZStack as __experimentalZStack } from './z-stack'; // Higher-Order Components export { diff --git a/packages/components/src/ui/utils/get-valid-children.ts b/packages/components/src/ui/utils/get-valid-children.ts index 47daa26bfc0b1a..cd16f9afa85e10 100644 --- a/packages/components/src/ui/utils/get-valid-children.ts +++ b/packages/components/src/ui/utils/get-valid-children.ts @@ -12,9 +12,9 @@ import { Children, isValidElement } from '@wordpress/element'; /** * Gets a collection of available children elements from a React component's children prop. * - * @param {import('react').ReactNode} children + * @param children * - * @return {import('react').ReactNodeArray} An array of available children. + * @return An array of available children. */ export function getValidChildren( children: ReactNode ): ReactNodeArray { if ( typeof children === 'string' ) return [ children ]; diff --git a/packages/components/src/z-stack/README.md b/packages/components/src/z-stack/README.md new file mode 100644 index 00000000000000..6a4a31fb38b9f9 --- /dev/null +++ b/packages/components/src/z-stack/README.md @@ -0,0 +1,39 @@ +# ZStack + +> **Experimental!** + +## Usage + +`ZStack` allows you to stack things along the Z-axis. + +```jsx +import { __experimentalZStack as ZStack } from '@wordpress/components'; + +function Example() { + return ( + + + + + + ); +} +``` + +## Props + +### `isLayered`: `boolean` + +When `true`, the children are stacked on top of each other. When `false`, the children follow the normal flow of the layout. Defaults to `true`. + +### `isReversed`: `boolean` + +Reverse the layer ordering. When `true`, the first child has the lowest `z-index` and the last child has the highest `z-index`. When `false`, the first child has the highest `z-index` and the last child has the lowest `z-index`. Defaults to `false`. + +### `offset`: `number` + +The amount of space between each child element. Defaults to `0`. + +### `children`: `ReactNode` + +The children to stack. diff --git a/packages/components/src/z-stack/component.tsx b/packages/components/src/z-stack/component.tsx new file mode 100644 index 00000000000000..a85b9e51753692 --- /dev/null +++ b/packages/components/src/z-stack/component.tsx @@ -0,0 +1,104 @@ +/** + * External dependencies + */ +import { css, cx } from 'emotion'; +// eslint-disable-next-line no-restricted-imports +import type { Ref, ReactNode } from 'react'; + +/** + * WordPress dependencies + */ +import { isValidElement } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { getValidChildren } from '../ui/utils/get-valid-children'; +import { contextConnect, useContextSystem } from '../ui/context'; +// eslint-disable-next-line no-duplicate-imports +import type { PolymorphicComponentProps } from '../ui/context'; +import { View } from '../view'; +import * as styles from './styles'; +const { ZStackView } = styles; + +export interface ZStackProps { + /** + * Layers children elements on top of each other (first: highest z-index, last: lowest z-index). + * + * @default true + */ + isLayered?: boolean; + /** + * Reverse the layer ordering (first: lowest z-index, last: highest z-index). + * + * @default false + */ + isReversed?: boolean; + /** + * The amount of offset between each child element. + * + * @default 0 + */ + offset?: number; + /** + * Child elements. + */ + children: ReactNode; +} + +function ZStack( + props: PolymorphicComponentProps< ZStackProps, 'div' >, + forwardedRef: Ref< any > +) { + const { + children, + className, + isLayered = true, + isReversed = false, + offset = 0, + ...otherProps + } = useContextSystem( props, 'ZStack' ); + + const validChildren = getValidChildren( children ); + const childrenLastIndex = validChildren.length - 1; + + const clonedChildren = validChildren.map( ( child, index ) => { + const zIndex = isReversed ? childrenLastIndex - index : index; + const offsetAmount = offset * index; + + const classes = cx( + isLayered ? styles.positionAbsolute : styles.positionRelative, + css( { + ...( isLayered + ? { marginLeft: offsetAmount } + : { right: offsetAmount * -1 } ), + } ) + ); + + const key = isValidElement( child ) ? child.key : index; + + return ( + + { child } + + ); + } ); + + return ( + + { clonedChildren } + + ); +} + +export default contextConnect( ZStack, 'ZStack' ); diff --git a/packages/components/src/z-stack/index.ts b/packages/components/src/z-stack/index.ts new file mode 100644 index 00000000000000..ec29793b39db84 --- /dev/null +++ b/packages/components/src/z-stack/index.ts @@ -0,0 +1 @@ +export { default as ZStack } from './component'; diff --git a/packages/components/src/z-stack/stories/index.js b/packages/components/src/z-stack/stories/index.js new file mode 100644 index 00000000000000..c87909e0033b09 --- /dev/null +++ b/packages/components/src/z-stack/stories/index.js @@ -0,0 +1,67 @@ +/** + * External dependencies + */ +import { boolean, number } from '@storybook/addon-knobs'; + +/** + * Internal dependencies + */ +import { Elevation } from '../../ui/elevation'; +import { HStack } from '../../h-stack'; +import { View } from '../../view'; +import { ZStack } from '..'; + +export default { + component: ZStack, + title: 'Components (Experimental)/ZStack', +}; + +const Avatar = ( { backgroundColor } ) => { + return ( + + + + + ); +}; + +const AnimatedAvatars = () => { + const props = { + offset: number( 'offset', 20 ), + isLayered: boolean( 'isLayered', true ), + isReversed: boolean( 'isReversed', false ), + }; + + return ( + + + + + + + + + + + ); +}; + +export const _default = () => { + return ( + + + + ); +}; diff --git a/packages/components/src/z-stack/styles.ts b/packages/components/src/z-stack/styles.ts new file mode 100644 index 00000000000000..8d585664042440 --- /dev/null +++ b/packages/components/src/z-stack/styles.ts @@ -0,0 +1,18 @@ +/** + * External dependencies + */ +import { css } from 'emotion'; +import styled from '@emotion/styled'; + +export const ZStackView = styled.div` + display: flex; + position: relative; +`; + +export const positionAbsolute = css` + position: absolute; +`; + +export const positionRelative = css` + position: relative; +`; diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json index ad80a95934e818..df78104420ca6a 100644 --- a/packages/components/tsconfig.json +++ b/packages/components/tsconfig.json @@ -36,6 +36,7 @@ "src/__next/**/*", "src/h-stack/**/*", "src/v-stack/**/*", + "src/z-stack/**/*", "src/view/**/*" ], "exclude": [