diff --git a/README.md b/README.md index 9b500c1..fbcfc2d 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ One way to ensure all dependencies are loaded is to use the [`@wordpress/depende - [`ConditionalComponent`](src/components/ConditionalComponent) - [`FetchAllTermSelectControl`](src/components/FetchAllTermSelectControl) - [`FileControls`](src/components/FileControls) +- [`HeadingSelectControl`](src/components/HeadingSelectControl) - [`ImageControl`](src/components/ImageControl) - [`InnerBlockSlider`](src/components/InnerBlockSlider) - [`LinkToolbar`](src/components/LinkToolbar) diff --git a/assets/images/heading-select-control--custom.png b/assets/images/heading-select-control--custom.png new file mode 100644 index 0000000..53395aa Binary files /dev/null and b/assets/images/heading-select-control--custom.png differ diff --git a/assets/images/heading-select-control--default.png b/assets/images/heading-select-control--default.png new file mode 100644 index 0000000..e5d3356 Binary files /dev/null and b/assets/images/heading-select-control--default.png differ diff --git a/src/components/HeadingSelectControl/README.md b/src/components/HeadingSelectControl/README.md new file mode 100644 index 0000000..19eae9f --- /dev/null +++ b/src/components/HeadingSelectControl/README.md @@ -0,0 +1,153 @@ +# HeadingSelectControl + +The `HeadingSelectControl` component allows for choosing one of a pre-defined list of heading levels. +It is intended to be used for blocks or plugin sidebars where some text element needs to be rendered as a user-defined heading. +The component wraps a regular [`SelectControl`](https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/select-control/index.tsx) component. + +| ![heading-select-control--default.png](../../../assets/images/heading-select-control--default.png) | +|----------------------------------------------------------------------------------------------------| +| _`HeadingSelectControl` component._ | + +| ![heading-select-control--custom.png](../../../assets/images/heading-select-control--custom.png) | +|--------------------------------------------------------------------------------------------------| +| _`HeadingSelectControl` component with custom `min` and `max` value specified._ | + +## Usage + +For a minimum working setup, all you need to do is pass a heading level as `value` to `HeadingSelectControl`, as well as an `onChange` callback that accepts a heading level. + +```js +import { HeadingSelectControl } from '@humanmade/block-editor-components'; +import { InspectorControls } from '@wordpress/block-editor'; + +function BlockEdit( props ) { + const { attributes, setAttributes } = props; + const { deckLevel } = attributes; + + return ( + + setAttributes( { deckLevel } ) } + /> + + ); +} +``` + +Additionally, you can also specify a minimum and/or maximum heading level by passing `min` and `max`, respectively. +For accessibility reasons, the default minimum heading level is set to `2`, so if you want to allow for selecting a Heading 1, you have to explicitly pass `min={ 1 }`. + +```js +import { HeadingSelectControl } from '@humanmade/block-editor-components'; +import { InspectorControls } from '@wordpress/block-editor'; + +function BlockEdit( props ) { + const { attributes, setAttributes } = props; + const { deckLevel } = attributes; + + return ( + + setAttributes( { deckLevel } ) } + /> + + ); +} +``` + +Since the component only allows for selecting a heading level, but does not actually render any heading element, you can also allow for something like a Heading 7 or Heading 8, if you really need to. +The HTML specification includes dedicated tags for 6 headings only, but sometimes editorial teams use special fake or pseudo headings, which will then end up in them having more than just 6 heading levels. +By passing a number greater than 6 to `max`, you can allow for that, and then handle rendering a Heading 7 or so, for example, as a paragraph with a custom class. + +```js +import { HeadingSelectControl } from '@humanmade/block-editor-components'; +import { InspectorControls } from '@wordpress/block-editor'; + +function BlockEdit( props ) { + const { attributes, setAttributes } = props; + const { level } = attributes; + + return ( + + setAttributes( { level } ) } + /> + + ); +} +``` + +Also, you can pass a custom `createLabel` function that takes a numeric heading level and returns the label to use for the heading. + +```js +import { HeadingSelectControl } from '@humanmade/block-editor-components'; +import { InspectorControls } from '@wordpress/block-editor'; + +function createLabel( level ) { + if ( level === 8 ) { + return 'Subheading'; + } + + return `Heading ${ level }`; +} + +function BlockEdit( props ) { + const { attributes, setAttributes } = props; + const { level } = attributes; + + return ( + + setAttributes( { level } ) } + /> + + ); +} +``` + +## Props + +The `HeadingSelectControl` component does not have any custom props other than the optional `max` and `min`, but you can pass anything that is supported by the nested [`SelectControl`](https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/select-control/index.tsx) component. + +### `createLabel` + +An optional function to create the label for the heading with the given level. +The first and only argument passed to the function is the numeric heading level. + +| Type | Required | Default | +|------------------------------|------------------------------|------------------------------------------------------| +| `Function` | no | `( level ) => sprintf( __( 'Heading %d' ), level )` | + +### `max` + +An optional maximum heading level. + +| Type | Required | Default | +|--------------------------------------|--------------------------------------|--------------------------------------| +| `number` | no | `6` | + +### `min` + +An optional minimum heading level. +This value is also being used as default value. + +| Type | Required | Default | +|--------------------------------------|--------------------------------------|--------------------------------------| +| `number` | no | `2` | + +## Dependencies + +The `HeadingSelectControl` component requires the following dependencies, which are expected to be available: + +- `@wordpress/components` +- `@wordpress/i18n` diff --git a/src/components/HeadingSelectControl/index.js b/src/components/HeadingSelectControl/index.js new file mode 100644 index 0000000..7271e4f --- /dev/null +++ b/src/components/HeadingSelectControl/index.js @@ -0,0 +1,64 @@ +import React, { ReactNode, useMemo } from 'react'; + +import { SelectControl } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Get the label for the heading with the given level. + * + * @param {number} level - Heading level. + * @returns {string} Label. + */ +function _createLabel( level ) { + return sprintf( + // translators: %s: heading level (e.g.: 1, 2, 3). + __( 'Heading %d', 'block-editor-components' ), + level + ); +} + +/** + * A dropdown control that allows for selecting a heading level. + * + * @param {object} props - Component props. + * @returns {ReactNode} Component. + */ +function HeadingSelectControl( props ) { + const { + createLabel = _createLabel, + max = 6, + min = 2, + onChange, + value = min, + ...selectProps + } = props; + + const options = useMemo( + () => { + if ( min > max ) { + return undefined; + } + + return Array( max - min + 1 ).fill().map( ( _, index ) => { + const level = min + index; + + return { + label: createLabel( level ), + value: level, + }; + } ); + }, + [ createLabel, max, min ] + ); + + return ( + onChange( Number( value ) ) } + /> + ); +} + +export default HeadingSelectControl;