From e13f07ef6e592bd8c409db5179db104521a86455 Mon Sep 17 00:00:00 2001 From: Peter Wilson <519727+peterwilsoncc@users.noreply.github.com> Date: Mon, 10 Jul 2023 10:56:04 +1000 Subject: [PATCH] Revert "Remove the nux package (#46110)" This reverts commit 9ea80f6f363f847b31c5faa3bac6be1c05f5ef63. # Conflicts: # lib/client-assets.php --- .github/CODEOWNERS | 1 + docs/contributors/code/scripts.md | 1 + docs/manifest.json | 12 ++ docs/reference-guides/README.md | 1 + docs/reference-guides/data/README.md | 1 + docs/reference-guides/data/data-core-nux.md | 99 ++++++++++++ docs/toc.json | 1 + lib/client-assets.php | 13 +- package-lock.json | 14 ++ package.json | 1 + packages/base-styles/_z-index.scss | 5 +- packages/nux/.npmrc | 1 + packages/nux/CHANGELOG.md | 124 +++++++++++++++ packages/nux/README.md | 114 ++++++++++++++ packages/nux/package.json | 50 ++++++ packages/nux/src/components/dot-tip/README.md | 38 +++++ packages/nux/src/components/dot-tip/index.js | 93 +++++++++++ .../nux/src/components/dot-tip/style.scss | 123 +++++++++++++++ .../dot-tip/test/__snapshots__/index.js.snap | 46 ++++++ .../nux/src/components/dot-tip/test/index.js | 78 ++++++++++ packages/nux/src/index.js | 13 ++ packages/nux/src/store/actions.js | 52 +++++++ packages/nux/src/store/index.js | 36 +++++ packages/nux/src/store/reducer.js | 70 +++++++++ packages/nux/src/store/selectors.js | 81 ++++++++++ packages/nux/src/store/test/actions.js | 40 +++++ packages/nux/src/store/test/reducer.js | 69 +++++++++ packages/nux/src/store/test/selectors.js | 146 ++++++++++++++++++ packages/nux/src/style.scss | 1 + .../local-storage-overrides.json | 6 + 30 files changed, 1327 insertions(+), 3 deletions(-) create mode 100644 docs/reference-guides/data/data-core-nux.md create mode 100644 packages/nux/.npmrc create mode 100644 packages/nux/CHANGELOG.md create mode 100644 packages/nux/README.md create mode 100644 packages/nux/package.json create mode 100644 packages/nux/src/components/dot-tip/README.md create mode 100644 packages/nux/src/components/dot-tip/index.js create mode 100644 packages/nux/src/components/dot-tip/style.scss create mode 100644 packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap create mode 100644 packages/nux/src/components/dot-tip/test/index.js create mode 100644 packages/nux/src/index.js create mode 100644 packages/nux/src/store/actions.js create mode 100644 packages/nux/src/store/index.js create mode 100644 packages/nux/src/store/reducer.js create mode 100644 packages/nux/src/store/selectors.js create mode 100644 packages/nux/src/store/test/actions.js create mode 100644 packages/nux/src/store/test/reducer.js create mode 100644 packages/nux/src/store/test/selectors.js create mode 100644 packages/nux/src/style.scss diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7ba9e884259ea4..257624c8f9862f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -88,6 +88,7 @@ /packages/compose @ajitbohra /packages/element @ajitbohra /packages/notices @ajitbohra +/packages/nux @ajitbohra /packages/viewport @ajitbohra /packages/base-styles /packages/icons diff --git a/docs/contributors/code/scripts.md b/docs/contributors/code/scripts.md index 5cd7efd2fffdad..1483a409a4d08f 100644 --- a/docs/contributors/code/scripts.md +++ b/docs/contributors/code/scripts.md @@ -31,6 +31,7 @@ The editor includes a number of packages to enable various pieces of functionali | [Is Shallow Equal](/packages/is-shallow-equal/README.md) | wp-is-shallow-equal | A function for performing a shallow comparison between two objects or arrays | | [Keycodes](/packages/keycodes/README.md) | wp-keycodes | Keycodes utilities for WordPress, used to check the key pressed in events like `onKeyDown` | | [List Reusable blocks](/packages/list-reusable-blocks/README.md) | wp-list-reusable-blocks | Package used to add import/export links to the listing page of the reusable blocks | +| [NUX](/packages/nux/README.md) | wp-nux | Components, and wp.data methods useful for onboarding a new user to the WordPress admin interface | | [Plugins](/packages/plugins/README.md) | wp-plugins | Plugins module for WordPress | | [Redux Routine](/packages/redux-routine/README.md) | wp-redux-routine | Redux middleware for generator coroutines | | [Rich Text](/packages/rich-text/README.md) | wp-rich-text | Helper functions to convert HTML or a DOM tree into a rich text value and back | diff --git a/docs/manifest.json b/docs/manifest.json index ec55132cd7d4b2..5f75e49924f355 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1757,6 +1757,12 @@ "markdown_source": "../packages/npm-package-json-lint-config/README.md", "parent": "packages" }, + { + "title": "@wordpress/nux", + "slug": "packages-nux", + "markdown_source": "../packages/nux/README.md", + "parent": "packages" + }, { "title": "@wordpress/plugins", "slug": "packages-plugins", @@ -2003,6 +2009,12 @@ "markdown_source": "../docs/reference-guides/data/data-core-notices.md", "parent": "data" }, + { + "title": "The NUX (New User Experience) Data", + "slug": "data-core-nux", + "markdown_source": "../docs/reference-guides/data/data-core-nux.md", + "parent": "data" + }, { "title": "Preferences", "slug": "data-core-preferences", diff --git a/docs/reference-guides/README.md b/docs/reference-guides/README.md index f13c838697f2de..33fdd9aa602414 100644 --- a/docs/reference-guides/README.md +++ b/docs/reference-guides/README.md @@ -63,6 +63,7 @@ - [**core/editor**: The Post Editor’s Data](/docs/reference-guides/data/data-core-editor.md) - [**core/keyboard-shortcuts**: The Keyboard Shortcuts Data](/docs/reference-guides/data/data-core-keyboard-shortcuts.md) - [**core/notices**: Notices Data](/docs/reference-guides/data/data-core-notices.md) + - [**core/nux**: The NUX (New User Experience) Data](/docs/reference-guides/data/data-core-nux.md) - [**core/preferences**: Preferences](/docs/reference-guides/data/data-core-preferences.md) - [**core/reusable-blocks**: Reusable blocks](/docs/reference-guides/data/data-core-reusable-blocks.md) - [**core/rich-text**: Rich Text](/docs/reference-guides/data/data-core-rich-text.md) diff --git a/docs/reference-guides/data/README.md b/docs/reference-guides/data/README.md index 5f4d8d92d4bd49..1134c1d5ddd307 100644 --- a/docs/reference-guides/data/README.md +++ b/docs/reference-guides/data/README.md @@ -12,6 +12,7 @@ - [**core/editor**: The Post Editor’s Data](/docs/reference-guides/data/data-core-editor.md) - [**core/keyboard-shortcuts**: The Keyboard Shortcuts Data](/docs/reference-guides/data/data-core-keyboard-shortcuts.md) - [**core/notices**: Notices Data](/docs/reference-guides/data/data-core-notices.md) +- [**core/nux**: The NUX (New User Experience) Data](/docs/reference-guides/data/data-core-nux.md) - [**core/preferences**: Preferences](/docs/reference-guides/data/data-core-preferences.md) - [**core/reusable-blocks**: Reusable blocks](/docs/reference-guides/data/data-core-reusable-blocks.md) - [**core/rich-text**: Rich Text](/docs/reference-guides/data/data-core-rich-text.md) diff --git a/docs/reference-guides/data/data-core-nux.md b/docs/reference-guides/data/data-core-nux.md new file mode 100644 index 00000000000000..4d2e8a0d98d546 --- /dev/null +++ b/docs/reference-guides/data/data-core-nux.md @@ -0,0 +1,99 @@ +# The NUX (New User Experience) Data + +Namespace: `core/nux`. + +## Selectors + + + +### areTipsEnabled + +Returns whether or not tips are globally enabled. + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `boolean`: Whether tips are globally enabled. + +### getAssociatedGuide + +Returns an object describing the guide, if any, that the given tip is a part +of. + +_Parameters_ + +- _state_ `Object`: Global application state. +- _tipId_ `string`: The tip to query. + +_Returns_ + +- `?NUXGuideInfo`: Information about the associated guide. + +### isTipVisible + +Determines whether or not the given tip is showing. Tips are hidden if they +are disabled, have been dismissed, or are not the current tip in any +guide that they have been added to. + +_Parameters_ + +- _state_ `Object`: Global application state. +- _tipId_ `string`: The tip to query. + +_Returns_ + +- `boolean`: Whether or not the given tip is showing. + + + +## Actions + + + +### disableTips + +Returns an action object that, when dispatched, prevents all tips from +showing again. + +_Returns_ + +- `Object`: Action object. + +### dismissTip + +Returns an action object that, when dispatched, dismisses the given tip. A +dismissed tip will not show again. + +_Parameters_ + +- _id_ `string`: The tip to dismiss. + +_Returns_ + +- `Object`: Action object. + +### enableTips + +Returns an action object that, when dispatched, makes all tips show again. + +_Returns_ + +- `Object`: Action object. + +### triggerGuide + +Returns an action object that, when dispatched, presents a guide that takes +the user through a series of tips step by step. + +_Parameters_ + +- _tipIds_ `string[]`: Which tips to show in the guide. + +_Returns_ + +- `Object`: Action object. + + diff --git a/docs/toc.json b/docs/toc.json index 085bbb536ece2b..1660afdcc29497 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -284,6 +284,7 @@ "docs/reference-guides/data/data-core-keyboard-shortcuts.md": [] }, { "docs/reference-guides/data/data-core-notices.md": [] }, + { "docs/reference-guides/data/data-core-nux.md": [] }, { "docs/reference-guides/data/data-core-preferences.md": [] }, diff --git a/lib/client-assets.php b/lib/client-assets.php index 99aa7f147ecbfc..966c469f3319c3 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -275,7 +275,7 @@ function gutenberg_register_packages_styles( $styles ) { $styles, 'wp-editor', gutenberg_url( 'build/editor/style.css' ), - array( 'wp-components', 'wp-block-editor', 'wp-reusable-blocks' ), + array( 'wp-components', 'wp-block-editor', 'wp-nux', 'wp-reusable-blocks' ), $version ); $styles->add_data( 'wp-editor', 'rtl', 'replace' ); @@ -284,7 +284,7 @@ function gutenberg_register_packages_styles( $styles ) { $styles, 'wp-edit-post', gutenberg_url( 'build/edit-post/style.css' ), - array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-block-library', 'wp-commands' ), + array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-block-library', 'wp-commands', 'wp-nux' ), $version ); $styles->add_data( 'wp-edit-post', 'rtl', 'replace' ); @@ -368,6 +368,15 @@ function gutenberg_register_packages_styles( $styles ) { ); $styles->add_data( 'wp-edit-blocks', 'rtl', 'replace' ); + gutenberg_override_style( + $styles, + 'wp-nux', + gutenberg_url( 'build/nux/style.css' ), + array( 'wp-components' ), + $version + ); + $styles->add_data( 'wp-nux', 'rtl', 'replace' ); + gutenberg_override_style( $styles, 'wp-block-library-theme', diff --git a/package-lock.json b/package-lock.json index 3c96f25259cbce..c3994a2be134e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18259,6 +18259,20 @@ "version": "file:packages/npm-package-json-lint-config", "dev": true }, + "@wordpress/nux": { + "version": "file:packages/nux", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/components": "file:packages/components", + "@wordpress/compose": "file:packages/compose", + "@wordpress/data": "file:packages/data", + "@wordpress/deprecated": "file:packages/deprecated", + "@wordpress/element": "file:packages/element", + "@wordpress/i18n": "file:packages/i18n", + "@wordpress/icons": "file:packages/icons", + "rememo": "^4.0.0" + } + }, "@wordpress/plugins": { "version": "file:packages/plugins", "requires": { diff --git a/package.json b/package.json index 5032f589b592fd..5bdef1e9e589e0 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@wordpress/list-reusable-blocks": "file:packages/list-reusable-blocks", "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", + "@wordpress/nux": "file:packages/nux", "@wordpress/plugins": "file:packages/plugins", "@wordpress/preferences": "file:packages/preferences", "@wordpress/preferences-persistence": "file:packages/preferences-persistence", diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index cc4a42df98f0aa..7987e1b4b5b4dc 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -144,7 +144,10 @@ $z-layers: ( // The focus styles of the region navigation containers should be above their content. ".is-focusing-regions {region} :focus::after": 1000000, - // Show tooltips above wp-admin menus, submenus, and sidebar: + // Show NUX tips above popovers, wp-admin menus, submenus, and sidebar: + ".nux-dot-tip": 1000001, + + // Show tooltips above NUX tips, wp-admin menus, submenus, and sidebar: ".components-tooltip": 1000002, // Keep template popover underneath 'Create custom template' modal overlay. diff --git a/packages/nux/.npmrc b/packages/nux/.npmrc new file mode 100644 index 00000000000000..43c97e719a5a82 --- /dev/null +++ b/packages/nux/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/nux/CHANGELOG.md b/packages/nux/CHANGELOG.md new file mode 100644 index 00000000000000..edbf4c88a21f0f --- /dev/null +++ b/packages/nux/CHANGELOG.md @@ -0,0 +1,124 @@ + + +## Unreleased + +### Breaking Changes + +- Updated dependencies to require React 18 ([45235](https://github.com/WordPress/gutenberg/pull/45235)) + +## 5.20.0 (2022-11-16) + +## 5.19.0 (2022-11-02) + +## 5.18.0 (2022-10-19) + +## 5.17.0 (2022-10-05) + +## 5.16.0 (2022-09-21) + +## 5.15.0 (2022-09-13) + +## 5.14.0 (2022-08-24) + +## 5.13.0 (2022-08-10) + +## 5.12.0 (2022-07-27) + +## 5.11.0 (2022-07-13) + +## 5.10.0 (2022-06-29) + +## 5.9.0 (2022-06-15) + +## 5.8.0 (2022-06-01) + +## 5.7.0 (2022-05-18) + +## 5.6.0 (2022-05-04) + +## 5.5.0 (2022-04-21) + +## 5.4.0 (2022-04-08) + +## 5.3.0 (2022-03-23) + +## 5.2.0 (2022-03-11) + +## 5.1.0 (2022-01-27) + +## 5.0.0 (2021-07-29) + +### Breaking Change + +- Upgraded React components to work with v17.0 ([#29118](https://github.com/WordPress/gutenberg/pull/29118)). There are no new features in React v17.0 as explained in the [blog post](https://reactjs.org/blog/2020/10/20/react-v17.html). + +## 4.2.0 (2021-07-21) + +## 4.1.0 (2021-05-20) + +## 4.0.0 (2021-05-14) + +### Breaking Changes + +- Drop support for Internet Explorer 11 ([#31110](https://github.com/WordPress/gutenberg/pull/31110)). Learn more at https://make.wordpress.org/core/2021/04/22/ie-11-support-phase-out-plan/. +- Increase the minimum Node.js version to v12 matching Long Term Support releases ([#31270](https://github.com/WordPress/gutenberg/pull/31270)). Learn more at https://nodejs.org/en/about/releases/. + +## 3.25.0 (2021-03-17) + +## 3.24.0 (2020-12-17) + +### New Feature + +- Added a store definition `store` for the core data namespace to use with `@wordpress/data` API ([#26655](https://github.com/WordPress/gutenberg/pull/26655)). + +# 3.1.0 (2019-06-03) + +- The `@wordpress/nux` package has been deprecated. Please use the `Guide` component in `@wordpress/components` to show a user guide. + +## 3.0.6 (2019-01-03) + +## 3.0.5 (2018-12-12) + +## 3.0.4 (2018-11-30) + +## 3.0.3 (2018-11-22) + +## 3.0.2 (2018-11-21) + +## 3.0.1 (2018-11-20) + +## 3.0.0 (2018-11-15) + +### Breaking Changes + +- The id prop of DotTip has been removed. Please use the tipId prop instead. + +## 2.0.13 (2018-11-12) + +## 2.0.12 (2018-11-12) + +## 2.0.11 (2018-11-09) + +## 2.0.10 (2018-11-09) + +## 2.0.9 (2018-11-03) + +## 2.0.8 (2018-10-30) + +## 2.0.7 (2018-10-29) + +### Deprecations + +- The id prop of DotTip has been deprecated. Please use the tipId prop instead. + +## 2.0.6 (2018-10-22) + +## 2.0.5 (2018-10-19) + +## 2.0.4 (2018-10-18) + +## 2.0.0 (2018-09-05) + +### Breaking Change + +- Change how required built-ins are polyfilled with Babel 7 ([#9171](https://github.com/WordPress/gutenberg/pull/9171)). If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. diff --git a/packages/nux/README.md b/packages/nux/README.md new file mode 100644 index 00000000000000..c0941ddd0c5f2a --- /dev/null +++ b/packages/nux/README.md @@ -0,0 +1,114 @@ +# New User eXperience (NUX) + +The NUX module exposes components, and `wp.data` methods useful for onboarding a new user to the WordPress admin interface. Specifically, it exposes _tips_ and _guides_. + +A _tip_ is a component that points to an element in the UI and contains text that explains the element's functionality. The user can dismiss a tip, in which case it never shows again. The user can also disable tips entirely. Information about tips is persisted between sessions using `localStorage`. + +A _guide_ allows a series of tips to be presented to the user one by one. When a user dismisses a tip that is in a guide, the next tip in the guide is shown. + +## Installation + +Install the module + +```bash +npm install @wordpress/nux --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._ + +## DotTip + +`DotTip` is a React component that renders a single _tip_ on the screen. The tip will point to the React element that `DotTip` is nested within. Each tip is uniquely identified by a string passed to `tipId`. + +See [the component's README][dot-tip-readme] for more information. + +[dot-tip-readme]: https://github.com/WordPress/gutenberg/tree/HEAD/packages/nux/src/components/dot-tip/README.md + +```jsx + +} +``` + +## Determining if a tip is visible + +You can programmatically determine if a tip is visible using the `isTipVisible` select method. + +```jsx +const isVisible = select( 'core/nux' ).isTipVisible( 'acme/add-to-cart' ); +console.log( isVisible ); // true or false +``` + +## Manually dismissing a tip + +`dismissTip` is a dispatch method that allows you to programmatically dismiss a tip. + +```jsx + +``` + +## Disabling and enabling tips + +Tips can be programatically disabled or enabled using the `disableTips` and `enableTips` dispatch methods. You can query the current setting by using the `areTipsEnabled` select method. + +Calling `enableTips` will also un-dismiss all previously dismissed tips. + +```jsx +const areTipsEnabled = select( 'core/nux' ).areTipsEnabled(); +return ( + +); +``` + +## Triggering a guide + +You can group a series of tips into a guide by calling the `triggerGuide` dispatch method. The given tips will then appear one by one. + +A tip cannot be added to more than one guide. + +```jsx +dispatch( 'core/nux' ).triggerGuide( [ + 'acme/product-info', + 'acme/add-to-cart', + 'acme/checkout', +] ); +``` + +## Getting information about a guide + +`getAssociatedGuide` is a select method that returns useful information about the state of the guide that a tip is associated with. + +```jsx +const guide = select( 'core/nux' ).getAssociatedGuide( 'acme/add-to-cart' ); +console.log( 'Tips in this guide:', guide.tipIds ); +console.log( 'Currently showing:', guide.currentTipId ); +console.log( 'Next to show:', guide.nextTipId ); +``` + +## 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). + +

Code is Poetry.

diff --git a/packages/nux/package.json b/packages/nux/package.json new file mode 100644 index 00000000000000..9089f865e729de --- /dev/null +++ b/packages/nux/package.json @@ -0,0 +1,50 @@ +{ + "name": "@wordpress/nux", + "version": "5.20.0", + "description": "NUX (New User eXperience) module for WordPress.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "gutenberg", + "nux" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/nux/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git", + "directory": "packages/nux" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "engines": { + "node": ">=12" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "react-native": "src/index", + "sideEffects": [ + "build-style/**", + "src/**/*.scss", + "{src,build,build-module}/{index.js,store/index.js}" + ], + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/components": "file:../components", + "@wordpress/compose": "file:../compose", + "@wordpress/data": "file:../data", + "@wordpress/deprecated": "file:../deprecated", + "@wordpress/element": "file:../element", + "@wordpress/i18n": "file:../i18n", + "@wordpress/icons": "file:../icons", + "rememo": "^4.0.0" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/nux/src/components/dot-tip/README.md b/packages/nux/src/components/dot-tip/README.md new file mode 100644 index 00000000000000..f143a22a222588 --- /dev/null +++ b/packages/nux/src/components/dot-tip/README.md @@ -0,0 +1,38 @@ +# DotTip + +`DotTip` is a React component that renders a single _tip_ on the screen. The tip will point to the React element that `DotTip` is nested within. Each tip is uniquely identified by a string passed to `tipId`. + +## Usage + +```jsx + +} +``` + +## Props + +The component accepts the following props: + +### tipId + +A string that uniquely identifies the tip. Identifiers should be prefixed with the name of the plugin, followed by a `/`. For example, `acme/add-to-cart`. + +- Type: `string` +- Required: Yes + +### position + +The direction in which the popover should open relative to its parent node. Specify y- and x-axis as a space-separated string. Supports `"top"`, `"middle"`, `"bottom"` y axis, and `"left"`, `"center"`, `"right"` x axis. + +- Type: `String` +- Required: No +- Default: `"middle right"` + +### children + +Any React element or elements can be passed as children. They will be rendered within the tip bubble. diff --git a/packages/nux/src/components/dot-tip/index.js b/packages/nux/src/components/dot-tip/index.js new file mode 100644 index 00000000000000..50de7ddb3be9df --- /dev/null +++ b/packages/nux/src/components/dot-tip/index.js @@ -0,0 +1,93 @@ +/** + * WordPress dependencies + */ +import { compose } from '@wordpress/compose'; +import { Popover, Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { useCallback, useRef } from '@wordpress/element'; +import { close } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { store as nuxStore } from '../../store'; + +function onClick( event ) { + // Tips are often nested within buttons. We stop propagation so that clicking + // on a tip doesn't result in the button being clicked. + event.stopPropagation(); +} + +export function DotTip( { + position = 'middle right', + children, + isVisible, + hasNextTip, + onDismiss, + onDisable, +} ) { + const anchorParent = useRef( null ); + const onFocusOutsideCallback = useCallback( + ( event ) => { + if ( ! anchorParent.current ) { + return; + } + if ( anchorParent.current.contains( event.relatedTarget ) ) { + return; + } + onDisable(); + }, + [ onDisable, anchorParent ] + ); + if ( ! isVisible ) { + return null; + } + + return ( + +

{ children }

+

+ +

+ +

+ + + +`; diff --git a/packages/nux/src/components/dot-tip/test/index.js b/packages/nux/src/components/dot-tip/test/index.js new file mode 100644 index 00000000000000..7917b044fc3e2d --- /dev/null +++ b/packages/nux/src/components/dot-tip/test/index.js @@ -0,0 +1,78 @@ +/** + * External dependencies + */ +import { act, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +/** + * Internal dependencies + */ +import { DotTip } from '..'; + +const noop = () => {}; + +describe( 'DotTip', () => { + it( 'should not render anything if invisible', async () => { + render( + + It looks like you’re writing a letter. Would you like help? + + ); + + await act( () => Promise.resolve() ); + + expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument(); + } ); + + it( 'should render correctly', async () => { + render( + + It looks like you’re writing a letter. Would you like help? + + ); + + await act( () => Promise.resolve() ); + + expect( screen.getByRole( 'dialog' ) ).toMatchSnapshot(); + } ); + + it( 'should call onDismiss when the dismiss button is clicked', async () => { + const user = userEvent.setup( { + advanceTimers: jest.advanceTimersByTime, + } ); + const onDismiss = jest.fn(); + + render( + + It looks like you’re writing a letter. Would you like help? + + ); + + await act( () => Promise.resolve() ); + + await user.click( screen.getByRole( 'button', { name: 'Got it' } ) ); + + expect( onDismiss ).toHaveBeenCalled(); + } ); + + it( 'should call onDisable when the X button is clicked', async () => { + const user = userEvent.setup( { + advanceTimers: jest.advanceTimersByTime, + } ); + const onDisable = jest.fn(); + + render( + + It looks like you’re writing a letter. Would you like help? + + ); + + await act( () => Promise.resolve() ); + + await user.click( + screen.getByRole( 'button', { name: 'Disable tips' } ) + ); + + expect( onDisable ).toHaveBeenCalled(); + } ); +} ); diff --git a/packages/nux/src/index.js b/packages/nux/src/index.js new file mode 100644 index 00000000000000..a0b3e073503750 --- /dev/null +++ b/packages/nux/src/index.js @@ -0,0 +1,13 @@ +/** + * WordPress dependencies + */ +import deprecated from '@wordpress/deprecated'; + +export { store } from './store'; +export { default as DotTip } from './components/dot-tip'; + +deprecated( 'wp.nux', { + since: '5.4', + hint: 'wp.components.Guide can be used to show a user guide.', + version: '6.2', +} ); diff --git a/packages/nux/src/store/actions.js b/packages/nux/src/store/actions.js new file mode 100644 index 00000000000000..ad8adb79c5530d --- /dev/null +++ b/packages/nux/src/store/actions.js @@ -0,0 +1,52 @@ +/** + * Returns an action object that, when dispatched, presents a guide that takes + * the user through a series of tips step by step. + * + * @param {string[]} tipIds Which tips to show in the guide. + * + * @return {Object} Action object. + */ +export function triggerGuide( tipIds ) { + return { + type: 'TRIGGER_GUIDE', + tipIds, + }; +} + +/** + * Returns an action object that, when dispatched, dismisses the given tip. A + * dismissed tip will not show again. + * + * @param {string} id The tip to dismiss. + * + * @return {Object} Action object. + */ +export function dismissTip( id ) { + return { + type: 'DISMISS_TIP', + id, + }; +} + +/** + * Returns an action object that, when dispatched, prevents all tips from + * showing again. + * + * @return {Object} Action object. + */ +export function disableTips() { + return { + type: 'DISABLE_TIPS', + }; +} + +/** + * Returns an action object that, when dispatched, makes all tips show again. + * + * @return {Object} Action object. + */ +export function enableTips() { + return { + type: 'ENABLE_TIPS', + }; +} diff --git a/packages/nux/src/store/index.js b/packages/nux/src/store/index.js new file mode 100644 index 00000000000000..39fef6c78c7911 --- /dev/null +++ b/packages/nux/src/store/index.js @@ -0,0 +1,36 @@ +/** + * WordPress dependencies + */ +import { createReduxStore, registerStore } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import reducer from './reducer'; +import * as actions from './actions'; +import * as selectors from './selectors'; + +const STORE_NAME = 'core/nux'; + +/** + * Store definition for the nux namespace. + * + * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/data/README.md#createReduxStore + * + * @type {Object} + */ +export const store = createReduxStore( STORE_NAME, { + reducer, + actions, + selectors, + persist: [ 'preferences' ], +} ); + +// Once we build a more generic persistence plugin that works across types of stores +// we'd be able to replace this with a register call. +registerStore( STORE_NAME, { + reducer, + actions, + selectors, + persist: [ 'preferences' ], +} ); diff --git a/packages/nux/src/store/reducer.js b/packages/nux/src/store/reducer.js new file mode 100644 index 00000000000000..373e4781f52353 --- /dev/null +++ b/packages/nux/src/store/reducer.js @@ -0,0 +1,70 @@ +/** + * WordPress dependencies + */ +import { combineReducers } from '@wordpress/data'; + +/** + * Reducer that tracks which tips are in a guide. Each guide is represented by + * an array which contains the tip identifiers contained within that guide. + * + * @param {Array} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Array} Updated state. + */ +export function guides( state = [], action ) { + switch ( action.type ) { + case 'TRIGGER_GUIDE': + return [ ...state, action.tipIds ]; + } + + return state; +} + +/** + * Reducer that tracks whether or not tips are globally enabled. + * + * @param {boolean} state Current state. + * @param {Object} action Dispatched action. + * + * @return {boolean} Updated state. + */ +export function areTipsEnabled( state = true, action ) { + switch ( action.type ) { + case 'DISABLE_TIPS': + return false; + + case 'ENABLE_TIPS': + return true; + } + + return state; +} + +/** + * Reducer that tracks which tips have been dismissed. If the state object + * contains a tip identifier, then that tip is dismissed. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function dismissedTips( state = {}, action ) { + switch ( action.type ) { + case 'DISMISS_TIP': + return { + ...state, + [ action.id ]: true, + }; + + case 'ENABLE_TIPS': + return {}; + } + + return state; +} + +const preferences = combineReducers( { areTipsEnabled, dismissedTips } ); + +export default combineReducers( { guides, preferences } ); diff --git a/packages/nux/src/store/selectors.js b/packages/nux/src/store/selectors.js new file mode 100644 index 00000000000000..e87cf688a1ba32 --- /dev/null +++ b/packages/nux/src/store/selectors.js @@ -0,0 +1,81 @@ +/** + * External dependencies + */ +import createSelector from 'rememo'; + +/** + * An object containing information about a guide. + * + * @typedef {Object} NUXGuideInfo + * @property {string[]} tipIds Which tips the guide contains. + * @property {?string} currentTipId The guide's currently showing tip. + * @property {?string} nextTipId The guide's next tip to show. + */ + +/** + * Returns an object describing the guide, if any, that the given tip is a part + * of. + * + * @param {Object} state Global application state. + * @param {string} tipId The tip to query. + * + * @return {?NUXGuideInfo} Information about the associated guide. + */ +export const getAssociatedGuide = createSelector( + ( state, tipId ) => { + for ( const tipIds of state.guides ) { + if ( tipIds.includes( tipId ) ) { + const nonDismissedTips = tipIds.filter( + ( tId ) => + ! Object.keys( + state.preferences.dismissedTips + ).includes( tId ) + ); + const [ currentTipId = null, nextTipId = null ] = + nonDismissedTips; + return { tipIds, currentTipId, nextTipId }; + } + } + + return null; + }, + ( state ) => [ state.guides, state.preferences.dismissedTips ] +); + +/** + * Determines whether or not the given tip is showing. Tips are hidden if they + * are disabled, have been dismissed, or are not the current tip in any + * guide that they have been added to. + * + * @param {Object} state Global application state. + * @param {string} tipId The tip to query. + * + * @return {boolean} Whether or not the given tip is showing. + */ +export function isTipVisible( state, tipId ) { + if ( ! state.preferences.areTipsEnabled ) { + return false; + } + + if ( state.preferences.dismissedTips?.hasOwnProperty( tipId ) ) { + return false; + } + + const associatedGuide = getAssociatedGuide( state, tipId ); + if ( associatedGuide && associatedGuide.currentTipId !== tipId ) { + return false; + } + + return true; +} + +/** + * Returns whether or not tips are globally enabled. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether tips are globally enabled. + */ +export function areTipsEnabled( state ) { + return state.preferences.areTipsEnabled; +} diff --git a/packages/nux/src/store/test/actions.js b/packages/nux/src/store/test/actions.js new file mode 100644 index 00000000000000..4e22afe03c8b82 --- /dev/null +++ b/packages/nux/src/store/test/actions.js @@ -0,0 +1,40 @@ +/** + * Internal dependencies + */ +import { triggerGuide, dismissTip, disableTips, enableTips } from '../actions'; + +describe( 'actions', () => { + describe( 'triggerGuide', () => { + it( 'should return a TRIGGER_GUIDE action', () => { + expect( triggerGuide( [ 'test/tip-1', 'test/tip-2' ] ) ).toEqual( { + type: 'TRIGGER_GUIDE', + tipIds: [ 'test/tip-1', 'test/tip-2' ], + } ); + } ); + } ); + + describe( 'dismissTip', () => { + it( 'should return an DISMISS_TIP action', () => { + expect( dismissTip( 'test/tip' ) ).toEqual( { + type: 'DISMISS_TIP', + id: 'test/tip', + } ); + } ); + } ); + + describe( 'disableTips', () => { + it( 'should return an DISABLE_TIPS action', () => { + expect( disableTips() ).toEqual( { + type: 'DISABLE_TIPS', + } ); + } ); + } ); + + describe( 'enableTips', () => { + it( 'should return an ENABLE_TIPS action', () => { + expect( enableTips() ).toEqual( { + type: 'ENABLE_TIPS', + } ); + } ); + } ); +} ); diff --git a/packages/nux/src/store/test/reducer.js b/packages/nux/src/store/test/reducer.js new file mode 100644 index 00000000000000..49172442d8f379 --- /dev/null +++ b/packages/nux/src/store/test/reducer.js @@ -0,0 +1,69 @@ +/** + * Internal dependencies + */ +import { guides, areTipsEnabled, dismissedTips } from '../reducer'; + +describe( 'reducer', () => { + describe( 'guides', () => { + it( 'should start out empty', () => { + expect( guides( undefined, {} ) ).toEqual( [] ); + } ); + + it( 'should add a guide when it is triggered', () => { + const state = guides( [], { + type: 'TRIGGER_GUIDE', + tipIds: [ 'test/tip-1', 'test/tip-2' ], + } ); + expect( state ).toEqual( [ [ 'test/tip-1', 'test/tip-2' ] ] ); + } ); + } ); + + describe( 'areTipsEnabled', () => { + it( 'should default to true', () => { + expect( areTipsEnabled( undefined, {} ) ).toBe( true ); + } ); + + it( 'should flip when tips are disabled', () => { + const state = areTipsEnabled( true, { + type: 'DISABLE_TIPS', + } ); + expect( state ).toBe( false ); + } ); + + it( 'should flip when tips are enabled', () => { + const state = areTipsEnabled( false, { + type: 'ENABLE_TIPS', + } ); + expect( state ).toBe( true ); + } ); + } ); + + describe( 'dismissedTips', () => { + it( 'should start out empty', () => { + expect( dismissedTips( undefined, {} ) ).toEqual( {} ); + } ); + + it( 'should mark tips as dismissed', () => { + const state = dismissedTips( + {}, + { + type: 'DISMISS_TIP', + id: 'test/tip', + } + ); + expect( state ).toEqual( { + 'test/tip': true, + } ); + } ); + + it( 'should reset if tips are enabled', () => { + const initialState = { + 'test/tip': true, + }; + const state = dismissedTips( initialState, { + type: 'ENABLE_TIPS', + } ); + expect( state ).toEqual( {} ); + } ); + } ); +} ); diff --git a/packages/nux/src/store/test/selectors.js b/packages/nux/src/store/test/selectors.js new file mode 100644 index 00000000000000..e2a06c74e08b68 --- /dev/null +++ b/packages/nux/src/store/test/selectors.js @@ -0,0 +1,146 @@ +/** + * Internal dependencies + */ +import { getAssociatedGuide, isTipVisible, areTipsEnabled } from '../selectors'; + +describe( 'selectors', () => { + describe( 'getAssociatedGuide', () => { + const state = { + guides: [ + [ 'test/tip-1', 'test/tip-2', 'test/tip-3' ], + [ 'test/tip-a', 'test/tip-b', 'test/tip-c' ], + [ 'test/tip-α', 'test/tip-β', 'test/tip-γ' ], + ], + preferences: { + dismissedTips: { + 'test/tip-1': true, + 'test/tip-a': true, + 'test/tip-b': true, + 'test/tip-α': true, + 'test/tip-β': true, + 'test/tip-γ': true, + }, + }, + }; + + it( 'should return null when there is no associated guide', () => { + expect( getAssociatedGuide( state, 'test/unknown' ) ).toBeNull(); + } ); + + it( 'should return the associated guide', () => { + expect( getAssociatedGuide( state, 'test/tip-2' ) ).toEqual( { + tipIds: [ 'test/tip-1', 'test/tip-2', 'test/tip-3' ], + currentTipId: 'test/tip-2', + nextTipId: 'test/tip-3', + } ); + } ); + + it( 'should indicate when there is no next tip', () => { + expect( getAssociatedGuide( state, 'test/tip-b' ) ).toEqual( { + tipIds: [ 'test/tip-a', 'test/tip-b', 'test/tip-c' ], + currentTipId: 'test/tip-c', + nextTipId: null, + } ); + } ); + + it( 'should indicate when there is no current or next tip', () => { + expect( getAssociatedGuide( state, 'test/tip-β' ) ).toEqual( { + tipIds: [ 'test/tip-α', 'test/tip-β', 'test/tip-γ' ], + currentTipId: null, + nextTipId: null, + } ); + } ); + } ); + + describe( 'isTipVisible', () => { + it( 'is tolerant to individual preferences being undefined', () => { + // See: https://github.com/WordPress/gutenberg/issues/14580 + const state = { + guides: [], + preferences: {}, + }; + expect( isTipVisible( state, 'test/tip' ) ).toBe( false ); + } ); + + it( 'is tolerant to undefined dismissedTips', () => { + // See: https://github.com/WordPress/gutenberg/issues/14580 + const state = { + guides: [], + preferences: { + areTipsEnabled: true, + }, + }; + expect( isTipVisible( state, 'test/tip' ) ).toBe( true ); + } ); + + it( 'should return true by default', () => { + const state = { + guides: [], + preferences: { + areTipsEnabled: true, + dismissedTips: {}, + }, + }; + expect( isTipVisible( state, 'test/tip' ) ).toBe( true ); + } ); + + it( 'should return false if tips are disabled', () => { + const state = { + guides: [], + preferences: { + areTipsEnabled: false, + dismissedTips: {}, + }, + }; + expect( isTipVisible( state, 'test/tip' ) ).toBe( false ); + } ); + + it( 'should return false if the tip is dismissed', () => { + const state = { + guides: [], + preferences: { + areTipsEnabled: true, + dismissedTips: { + 'test/tip': true, + }, + }, + }; + expect( isTipVisible( state, 'test/tip' ) ).toBe( false ); + } ); + + it( 'should return false if the tip is in a guide and it is not the current tip', () => { + const state = { + guides: [ [ 'test/tip-1', 'test/tip-2', 'test/tip-3' ] ], + preferences: { + areTipsEnabled: true, + dismissedTips: {}, + }, + }; + expect( isTipVisible( state, 'test/tip-2' ) ).toBe( false ); + } ); + } ); + + describe( 'areTipsEnabled', () => { + it( 'should return true if tips are enabled', () => { + const state = { + guides: [], + preferences: { + areTipsEnabled: true, + dismissedTips: {}, + }, + }; + expect( areTipsEnabled( state ) ).toBe( true ); + } ); + + it( 'should return false if tips are disabled', () => { + const state = { + guides: [], + preferences: { + areTipsEnabled: false, + dismissedTips: {}, + }, + }; + expect( areTipsEnabled( state ) ).toBe( false ); + } ); + } ); +} ); diff --git a/packages/nux/src/style.scss b/packages/nux/src/style.scss new file mode 100644 index 00000000000000..0df73ff851e9f9 --- /dev/null +++ b/packages/nux/src/style.scss @@ -0,0 +1 @@ +@import "./components/dot-tip/style.scss"; diff --git a/packages/react-native-bridge/common/gutenberg-web-single-block/local-storage-overrides.json b/packages/react-native-bridge/common/gutenberg-web-single-block/local-storage-overrides.json index 28c0c1b17b4adc..f9cee4142d11ac 100644 --- a/packages/react-native-bridge/common/gutenberg-web-single-block/local-storage-overrides.json +++ b/packages/react-native-bridge/common/gutenberg-web-single-block/local-storage-overrides.json @@ -13,5 +13,11 @@ "hiddenBlockTypes": [], "preferredStyleVariations": {} } + }, + "core/nux": { + "preferences": { + "areTipsEnabled": false, + "dismissedTips": {} + } } }