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 new @wordpress/preferences package #38873

Merged
merged 10 commits into from
Feb 25, 2022
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"@wordpress/notices": "file:packages/notices",
"@wordpress/nux": "file:packages/nux",
"@wordpress/plugins": "file:packages/plugins",
"@wordpress/preferences": "file:packages/preferences",
"@wordpress/primitives": "file:packages/primitives",
"@wordpress/priority-queue": "file:packages/priority-queue",
"@wordpress/react-i18n": "file:packages/react-i18n",
Expand Down
1 change: 1 addition & 0 deletions packages/base-styles/_z-index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ $z-layers: (
".components-popover.edit-post-more-menu__content": 99998,
".components-popover.edit-site-more-menu__content": 99998,
".components-popover.edit-widgets-more-menu__content": 99998,
".components-popover.preferences-more-menu__content": 99998,
".components-popover.block-editor-rich-text__inline-format-toolbar": 99998,
".components-popover.block-editor-warning__dropdown": 99998,
".components-popover.edit-navigation-menu-actions__switcher-dropdown": 99998,
Expand Down
1 change: 1 addition & 0 deletions packages/preferences/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
7 changes: 7 additions & 0 deletions packages/preferences/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!-- Learn how to maintain this file at https://github.com/WordPress/gutenberg/tree/HEAD/packages#maintaining-changelogs. -->

## Unreleased

## 1.0.0

- Initial version of the package.
174 changes: 174 additions & 0 deletions packages/preferences/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Preferences

Utilities for storing WordPress preferences.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be "editor" instead of "WordPress" preferences?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should we limit this to "editor"? That's for sure the main use-case for us but does it prevent us to make this package ready for any client-side rendered WP-Admin page?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, but should we restrict it to WordPress? This package isn't inherently tied to WP, unless I'm missing something. It could potentially be used by non-WP block editors too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true. It may depend on how the persistence functionality is implemented in the future, as at some point we'll want to persist settings to the database via the REST API rather than localstorage.

If that can be done in a way where it's configurable or separate to the package (which I think it probably should be), it would be fine to drop the 'WordPress' from this, but for now I don't think it's harmful.


## Installation

Install the module

```bash
npm install @wordpress/preferences --save
```

_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for such language features and APIs, you should include [the polyfill shipped in `@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default#polyfill) in your code._

## Examples

### Data store

Preferences are persisted values of any kind.

Set the default preferences for any features on initialization by dispatching an action:

```js
import { dispatch } from '@wordpress/data';
import { store as preferencesStore } from '@wordpress/preferences';

function initialize() {
// ...

dispatch( preferencesStore ).setDefaults(
'namespace/editor-or-plugin-name',
{
myBooleanFeature: true,
}
);

// ...
}
```

Or the `get` selector to get a preference value, and the `set` action to update a preference to any value:

```js
wp.data
.select( 'core/preferences' )
.get( 'namespace/editor-or-plugin-name', 'myPreferenceName' ); // 1
wp.data
.dispatch( 'core/preferences' )
.set( 'namespace/editor-or-plugin-name', 'myPreferenceName', 2 );
wp.data
.select( 'core/preferences' )
.get( 'namespace/editor-or-plugin-name', 'myPreferenceName' ); // 2
```

Use the `toggle` action to flip a boolean preference between `true` and `false`:

```js
wp.data
.select( 'core/preferences' )
.get( 'namespace/editor-or-plugin-name', 'myPreferenceName' ); // true
wp.data
.dispatch( 'core/preferences' )
.toggle( 'namespace/editor-or-plugin-name', 'myPreferenceName' );
wp.data
.select( 'core/preferences' )
.get( 'namespace/editor-or-plugin-name', 'myPreferenceName' ); // false
```

### Components

The `PreferenceToggleMenuItem` components can be used with a `DropdownMenu` to implement a menu for changing preferences.

Also see the `MoreMenuDropdown` component from the `@wordpress/interface` package for implementing a more menu.

```jsx
function MyEditorMenu() {
return (
<MoreMenuDropdown>
{ () => (
<MenuGroup label={ __( 'Features' ) }>
<PreferenceToggleMenuItem
scope="namespace/editor-or-plugin-name"
name="myPreferenceName"
label={ __( 'My feature' ) }
info={ __( 'A really awesome feature' ) }
messageActivated={ __( 'My feature activated' ) }
messageDeactivated={ __( 'My feature deactivated' ) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is not new to this PR but in other components that needed configurable labels, we had a single object prop like messages in FormTokenField. Feels like something we'd need to normalize at some point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can take a look at that in a follow-up. 👍

/>
</MenuGroup>
) }
</MoreMenuDropdown>
);
}
```

## API Reference
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we take this opportunity to simplify further. What I have in mind could be something like:

  • drop the "feature" name (as it's a bit weird) and just call them preferences.
  • Basically the main selectors could be just get and set

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the 'feature' thing doesn't really fit for a generic preferences package. I'll work on the naming. 👍

I think it might good to have a toggle too, most of the preferences are booleans, so it seems like a handy function to have.

On this subject, I've been wondering if we want any built-in type safety for these preferences. Or whether it should just be a freeform key/value store. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On this subject, I've been wondering if we want any built-in type safety for these preferences. Or whether it should just be a freeform key/value store. What do you think?

If we go the type safety road, it almost feels like we should "register" preferences. I'm not sure it's worth the added complexity. I understand that if we don't do it from the start it might harder to do later. Any use-cases that benefits from the type safety?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any use-cases that benefits from the type safety?

There definitely aren't many in the editor, so I think you're right that it's not worth the complexity. The only one really is the Code Editor/Visual Editor option.

There is also the option of building more complex behavior around the existing actions and selectors. For example, if an editor needed a preference with some kind of validation, it could add that validation in its own action that calls through to dispatch( 'core/preferences' ).set.


### Actions

The following set of dispatching action creators are available on the object returned by `wp.data.dispatch( 'core/preferences' )`:

<!-- START TOKEN(Autogenerated actions|src/store/actions.js) -->

#### set

Returns an action object used in signalling that a preference should be set
to a value

_Parameters_

- _scope_ `string`: The preference scope (e.g. core/edit-post).
- _name_ `string`: The preference name.
- _value_ `*`: The value to set.

_Returns_

- `Object`: Action object.

#### setDefaults

Returns an action object used in signalling that preference defaults should
be set.

_Parameters_

- _scope_ `string`: The preference scope (e.g. core/edit-post).
- _defaults_ `Object<string, *>`: A key/value map of preference names to values.

_Returns_

- `Object`: Action object.

#### toggle

Returns an action object used in signalling that a preference should be
toggled.

_Parameters_

- _scope_ `string`: The preference scope (e.g. core/edit-post).
- _name_ `string`: The preference name.

<!-- END TOKEN(Autogenerated actions|src/store/actions.js) -->

### Selectors

The following selectors are available on the object returned by `wp.data.select( 'core/preferences' )`:

<!-- START TOKEN(Autogenerated selectors|src/store/selectors.js) -->

#### get

Returns a boolean indicating whether a prefer is active for a particular
scope.

_Parameters_

- _state_ `Object`: The store state.
- _scope_ `string`: The scope of the feature (e.g. core/edit-post).
- _name_ `string`: The name of the feature.

_Returns_

- `*`: Is the feature enabled?

<!-- END TOKEN(Autogenerated selectors|src/store/selectors.js) -->

## Contributing to this package

This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects.

To find out more about contributing to this package or Gutenberg as a whole, please read the project's main [contributor guide](https://github.com/WordPress/gutenberg/tree/HEAD/CONTRIBUTING.md).

<br /><br /><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p>
44 changes: 44 additions & 0 deletions packages/preferences/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@wordpress/preferences",
"version": "1.0.0-prerelease",
"private": true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other public packages depend on this package so it shouldn't be private. See #39390.

"description": "Utilities for managing WordPress preferences.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
"keywords": [
"wordpress",
"gutenberg",
"preferences",
"settings",
"options"
],
"homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/preferences/README.md",
"repository": {
"type": "git",
"url": "https://github.com/WordPress/gutenberg.git",
"directory": "packages/preferences"
},
"bugs": {
"url": "https://github.com/WordPress/gutenberg/issues"
},
"engines": {
"node": ">=12"
},
"main": "build/index.js",
"module": "build-module/index.js",
"react-native": "src/index",
"types": "build-types",
"sideEffects": false,
"dependencies": {
"@babel/runtime": "^7.16.0",
"@wordpress/a11y": "file:../a11y",
"@wordpress/components": "file:../components",
"@wordpress/data": "file:../data",
"@wordpress/i18n": "file:../i18n",
"@wordpress/icons": "file:../icons",
"classnames": "^2.3.1"
},
"publishConfig": {
"access": "public"
}
}
1 change: 1 addition & 0 deletions packages/preferences/src/components/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as PreferenceToggleMenuItem } from './preference-toggle-menu-item';
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# PreferenceToggleMenuItem

`PreferenceToggleMenuItem` renders a menu item that is connected to the preference package's store, and will toggle the value of a 'preference' between true and false.

This component implements a `MenuItem` component from the `@wordpress/components` package.

## Props

### scope

The 'scope' of the feature. This is usually a namespaced string that represents the name of the editor (e.g. 'core/edit-post'), and often matches the name of the store for the editor.

- Type: `String`
- Required: Yes

### name

The name of the preference to toggle (e.g. 'fixedToolbar').

- Type: `String`
- Required: Yes

### label

A human readable label for the feature.

- Type: `String`
- Required: Yes

### info

A human readable description of what this toggle does.

- Type: `Object`
- Required: No

### messageActivated

A message read by a screen reader when the feature is activated. (e.g. 'Fixed toolbar activated')

- Type: `String`
- Required: No

### messageDeactivated

A message read by a screen reader when the feature is deactivated. (e.g. 'Fixed toolbar deactivated')

- Type: `String`
- Required: No

### shortcut

A keyboard shortcut for the feature. This is just used for display purposes and the implementation of the shortcut should be handled separately.

Consider using the `displayShortcut` helper from the `@wordpress/keycodes` package for this prop.

- Type: `Array`
- Required: No
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* WordPress dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { MenuItem } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { check } from '@wordpress/icons';
import { speak } from '@wordpress/a11y';

/**
* Internal dependencies
*/
import { store as preferencesStore } from '../../store';

export default function PreferenceToggleMenuItem( {
scope,
name,
label,
info,
messageActivated,
messageDeactivated,
shortcut,
} ) {
const isActive = useSelect(
( select ) => !! select( preferencesStore ).get( scope, name ),
[ name ]
);
const { toggle } = useDispatch( preferencesStore );
const speakMessage = () => {
if ( isActive ) {
const message =
messageDeactivated ||
sprintf(
/* translators: %s: preference name, e.g. 'Fullscreen mode' */
__( 'Preference deactivated - %s' ),
label
);
speak( message );
} else {
const message =
messageActivated ||
sprintf(
/* translators: %s: preference name, e.g. 'Fullscreen mode' */
__( 'Preference activated - %s' ),
label
);
speak( message );
}
};

return (
<MenuItem
icon={ isActive && check }
isSelected={ isActive }
onClick={ () => {
toggle( scope, name );
speakMessage();
} }
role="menuitemcheckbox"
info={ info }
shortcut={ shortcut }
>
{ label }
</MenuItem>
);
}
2 changes: 2 additions & 0 deletions packages/preferences/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './components';
export { store } from './store';
Loading