Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HeadingSelectControl component #24

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Binary file added assets/images/heading-select-control--custom.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/heading-select-control--default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
153 changes: 153 additions & 0 deletions src/components/HeadingSelectControl/README.md
Original file line number Diff line number Diff line change
@@ -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 (
<InspectorControls>
<HeadingSelectControl
value={ deckLevel }
onChange={ ( deckLevel ) => setAttributes( { deckLevel } ) }
/>
</InspectorControls>
);
}
```

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 (
<InspectorControls>
<HeadingSelectControl
min={ 1 }
value={ deckLevel }
onChange={ ( deckLevel ) => setAttributes( { deckLevel } ) }
/>
</InspectorControls>
);
}
```

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 (
<InspectorControls>
<HeadingSelectControl
max={ 8 }
min={ 6 }
value={ level }
onChange={ ( level ) => setAttributes( { level } ) }
/>
</InspectorControls>
);
}
```

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 (
<InspectorControls>
<HeadingSelectControl
createLabel={ createLabel }
max={ 8 }
min={ 6 }
value={ level }
onChange={ ( level ) => setAttributes( { level } ) }
/>
</InspectorControls>
);
}
```

## 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`
64 changes: 64 additions & 0 deletions src/components/HeadingSelectControl/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<SelectControl
{ ...selectProps }
options={ options }
value={ value }
onChange={ ( value ) => onChange( Number( value ) ) }
/>
);
}

export default HeadingSelectControl;