Skip to content

Commit

Permalink
Compat: Upgrade admin notices to use Notices module at runtime (#11604)
Browse files Browse the repository at this point in the history
* Notices: Normalize notice type to use WP prefix

* Notices: Support Notice object as argument of createNotice

* Edit Post: Upgrade admin notices to notice module

* Notices: Extract default status to constant

* Docs: Add unstable API convention to coding guidelines

See: https://wordpress.slack.com/archives/C5UNMSU4R/p1541690775295000

(Registration: https://make.wordpress.org/chat/)

* Notices: Support __unstableHTML action property

* Components: Pass through __unstableHTML as Notice RawHTML

* Edit Post: Pass through notice HTML from admin notices

* Notices: Enforce string-y content by cast

* Notices: Add speak option

* Edit Post: Add missing AdminNotices classes

* Edit Post: Derive all AdminNotices upgraded notice children

* Edit Post: Fix AdminNotices missing text nodes content

* Edit Post: Reverse order of AdminNotice upgraded notices

* Notices: Mark as __unstableHTML via option

Content is used for both

* Edit Post: Limit upgraded notices to wpbody-content ID
  • Loading branch information
aduth authored Nov 19, 2018
1 parent 221afa1 commit 5dbc641
Show file tree
Hide file tree
Showing 15 changed files with 294 additions and 14 deletions.
5 changes: 5 additions & 0 deletions docs/data/data-core-notices.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ Yields action objects used in signalling that a notice is to be created.
* options.isDismissible: Whether the notice can
be dismissed by user.
Defaults to `true`.
* options.speak: Whether the notice
content should be
announced to screen
readers. Defaults to
`true`.
* options.actions: User actions to be
presented with notice.

Expand Down
10 changes: 7 additions & 3 deletions docs/reference/coding-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,13 @@ Exposed APIs that are still being tested, discussed and are subject to change sh
Example:

```js
export {
internalApi as __experimentalExposedApi
} from './internalApi.js';
export { __experimentalDoAction } from './api';
```

If an API must be exposed but is clearly not intended to be supported into the future, you may also use `__unstable` as a prefix to differentiate it from an experimental API. Unstable APIs should serve an immediate and temporary purpose. They should _never_ be used by plugin developers as they can be removed at any point without notice, and thus should be omitted from public-facing documentation. The inline code documentation should clearly caution their use.

```js
export { __unstableDoAction } from './api';
```

### Variable Naming
Expand Down
1 change: 1 addition & 0 deletions lib/packages-dependencies.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
'wp-embed',
'wp-i18n',
'wp-keycodes',
'wp-notices',
'wp-nux',
'wp-plugins',
'wp-url',
Expand Down
6 changes: 6 additions & 0 deletions packages/components/src/notice/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import classnames from 'classnames';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { RawHTML } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -21,11 +22,16 @@ function Notice( {
onRemove = noop,
isDismissible = true,
actions = [],
__unstableHTML,
} ) {
const classes = classnames( className, 'components-notice', 'is-' + status, {
'is-dismissible': isDismissible,
} );

if ( __unstableHTML ) {
children = <RawHTML>{ children }</RawHTML>;
}

return (
<div className={ classes }>
<div className="components-notice__content">
Expand Down
6 changes: 5 additions & 1 deletion packages/components/src/notice/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ function NoticeList( { notices, onRemove = noop, className = 'components-notice-
<div className={ className }>
{ children }
{ [ ...notices ].reverse().map( ( notice ) => (
<Notice { ...omit( notice, 'content' ) } key={ notice.id } onRemove={ removeNotice( notice.id ) }>
<Notice
{ ...omit( notice, [ 'content' ] ) }
key={ notice.id }
onRemove={ removeNotice( notice.id ) }
>
{ notice.content }
</Notice>
) ) }
Expand Down
6 changes: 6 additions & 0 deletions packages/edit-post/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 3.1.0 (Unreleased)

### New Feature

- The new `AdminNotices` component will transparently upgrade any `.notice` elements on the page to the equivalent `@wordpress/notices` module notice state.

## 3.0.2 (2018-11-15)

## 3.0.1 (2018-11-12)
Expand Down
105 changes: 105 additions & 0 deletions packages/edit-post/src/components/admin-notices/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { withDispatch } from '@wordpress/data';

/**
* Mapping of server-supported notice class names to an equivalent notices
* module status.
*
* @type {Map}
*/
const NOTICE_CLASS_STATUSES = {
'notice-success': 'success',
updated: 'success',
'notice-warning': 'warning',
'notice-error': 'error',
error: 'error',
'notice-info': 'info',
};

/**
* Returns an array of admin notice Elements.
*
* @return {Element[]} Admin notice elements.
*/
function getAdminNotices() {
// The order is reversed to match expectations of rendered order, since a
// NoticesList is itself rendered in reverse order (newest to oldest).
return [ ...document.querySelectorAll( '#wpbody-content > .notice' ) ].reverse();
}

/**
* Given an admin notice Element, returns the relevant notice content HTML.
*
* @param {Element} element Admin notice element.
*
* @return {Element} Upgraded notice HTML.
*/
function getNoticeHTML( element ) {
const fragments = [];

for ( const child of element.childNodes ) {
if ( child.nodeType !== window.Node.ELEMENT_NODE ) {
const value = child.nodeValue.trim();
if ( value ) {
fragments.push( child.nodeValue );
}
} else if ( ! child.classList.contains( 'notice-dismiss' ) ) {
fragments.push( child.outerHTML );
}
}

return fragments.join( '' );
}

/**
* Given an admin notice Element, returns the upgraded status type, or
* undefined if one cannot be determined (i.e. one is not assigned).
*
* @param {Element} element Admin notice element.
*
* @return {?string} Upgraded status type.
*/
function getNoticeStatus( element ) {
for ( const className of element.classList ) {
if ( NOTICE_CLASS_STATUSES.hasOwnProperty( className ) ) {
return NOTICE_CLASS_STATUSES[ className ];
}
}
}

export class AdminNotices extends Component {
componentDidMount() {
this.convertNotices();
}

convertNotices() {
const { createNotice } = this.props;
getAdminNotices().forEach( ( element ) => {
// Convert and create.
const status = getNoticeStatus( element );
const content = getNoticeHTML( element );
const isDismissible = element.classList.contains( 'is-dismissible' );
createNotice( status, content, {
speak: false,
__unstableHTML: true,
isDismissible,
} );

// Remove (now-redundant) admin notice element.
element.parentNode.removeChild( element );
} );
}

render() {
return null;
}
}

export default withDispatch( ( dispatch ) => {
const { createNotice } = dispatch( 'core/notices' );

return { createNotice };
} )( AdminNotices );
61 changes: 61 additions & 0 deletions packages/edit-post/src/components/admin-notices/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* External dependencies
*/
import renderer from 'react-test-renderer';

/**
* Internal dependencies
*/
import { AdminNotices } from '../';

describe( 'AdminNotices', () => {
beforeEach( () => {
// The superfluous whitespace is intentional in verifying expected
// outputs of (a) non-element first child of the element (whitespace
// text node) and (b) untrimmed content.
document.body.innerHTML = `
<div id="wpbody-content">
<div class="notice updated is-dismissible">
<p>My <strong>notice</strong> text</p>
<p>My second line of text</p>
<button type="button" class="notice-dismiss">
<span class="screen-reader-text">Dismiss this notice.</span>
</button>
</div>
<div class="notice notice-warning">Warning</div>
<aside class="elsewhere">
<div class="notice">Ignore me</div>
</aside>
</div>
`;
} );

it( 'should upgrade notices', () => {
const createNotice = jest.fn();

renderer.create( <AdminNotices createNotice={ createNotice } /> );

expect( createNotice ).toHaveBeenCalledTimes( 2 );
expect( createNotice.mock.calls[ 0 ] ).toEqual( [
'warning',
'Warning',
{
speak: false,
__unstableHTML: true,
isDismissible: false,
},
] );
expect( createNotice.mock.calls[ 1 ] ).toEqual( [
'success',
'<p>My <strong>notice</strong> text</p><p>My second line of text</p>',
{
speak: false,
__unstableHTML: true,
isDismissible: true,
},
] );

// Verify all but `<aside>` are removed.
expect( document.getElementById( 'wpbody-content' ).childElementCount ).toBe( 1 );
} );
} );
2 changes: 2 additions & 0 deletions packages/edit-post/src/components/layout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import Sidebar from '../sidebar';
import PluginPostPublishPanel from '../sidebar/plugin-post-publish-panel';
import PluginPrePublishPanel from '../sidebar/plugin-pre-publish-panel';
import FullscreenMode from '../fullscreen-mode';
import AdminNotices from '../admin-notices';

function Layout( {
mode,
Expand Down Expand Up @@ -69,6 +70,7 @@ function Layout( {
<BrowserURL />
<UnsavedChangesWarning />
<AutosaveMonitor />
<AdminNotices />
<Header />
<div
className="edit-post-layout__content"
Expand Down
1 change: 1 addition & 0 deletions packages/edit-post/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import '@wordpress/core-data';
import '@wordpress/editor';
import '@wordpress/nux';
import '@wordpress/viewport';
import '@wordpress/notices';
import { registerCoreBlocks } from '@wordpress/block-library';
import { render, unmountComponentAtNode } from '@wordpress/element';
import { dispatch } from '@wordpress/data';
Expand Down
11 changes: 11 additions & 0 deletions packages/notices/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## 1.1.0 (Unreleased)

### New Feature

- The `createNotice` can now optionally accept a WPNotice object as the sole argument.
- New option `speak` enables control as to whether the notice content is announced to screen readers (defaults to `true`)

### Bug Fixes

- While `createNotice` only explicitly supported content of type `string`, it was not previously enforced. This has been corrected.

## 1.0.5 (2018-11-15)

## 1.0.4 (2018-11-09)
Expand Down
21 changes: 18 additions & 3 deletions packages/notices/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { uniqueId } from 'lodash';
/**
* Internal dependencies
*/
import { DEFAULT_CONTEXT } from './constants';
import { DEFAULT_CONTEXT, DEFAULT_STATUS } from './constants';

/**
* Yields action objects used in signalling that a notice is to be created.
Expand All @@ -23,18 +23,32 @@ import { DEFAULT_CONTEXT } from './constants';
* @param {?boolean} options.isDismissible Whether the notice can
* be dismissed by user.
* Defaults to `true`.
* @param {?boolean} options.speak Whether the notice
* content should be
* announced to screen
* readers. Defaults to
* `true`.
* @param {?Array<WPNoticeAction>} options.actions User actions to be
* presented with notice.
*/
export function* createNotice( status = 'info', content, options = {} ) {
export function* createNotice( status = DEFAULT_STATUS, content, options = {} ) {
const {
speak = true,
isDismissible = true,
context = DEFAULT_CONTEXT,
id = uniqueId( context ),
actions = [],
__unstableHTML,
} = options;

yield { type: 'SPEAK', message: content };
// The supported value shape of content is currently limited to plain text
// strings. To avoid setting expectation that e.g. a WPElement could be
// supported, cast to a string.
content = String( content );

if ( speak ) {
yield { type: 'SPEAK', message: content };
}

yield {
type: 'CREATE_NOTICE',
Expand All @@ -43,6 +57,7 @@ export function* createNotice( status = 'info', content, options = {} ) {
id,
status,
content,
__unstableHTML,
isDismissible,
actions,
},
Expand Down
7 changes: 7 additions & 0 deletions packages/notices/src/store/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@
* @type {string}
*/
export const DEFAULT_CONTEXT = 'global';

/**
* Default notice status.
*
* @type {string}
*/
export const DEFAULT_STATUS = 'info';
9 changes: 7 additions & 2 deletions packages/notices/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@ const DEFAULT_NOTICES = [];
* `info`, `error`, or `warning`. Defaults
* to `info`.
* @property {string} content Notice message.
* @property {string} __unstableHTML Notice message as raw HTML. Intended to
* serve primarily for compatibility of
* server-rendered notices, and SHOULD NOT
* be used for notices. It is subject to
* removal without notice.
* @property {boolean} isDismissible Whether the notice can be dismissed by
* user. Defaults to `true`.
* @property {WPNoticeAction[]} actions User actions to present with notice.
*
* @typedef {Notice}
* @typedef {WPNotice}
*/

/**
Expand All @@ -48,7 +53,7 @@ const DEFAULT_NOTICES = [];
* @param {Object} state Notices state.
* @param {?string} context Optional grouping context.
*
* @return {Notice[]} Array of notices.
* @return {WPNotice[]} Array of notices.
*/
export function getNotices( state, context = DEFAULT_CONTEXT ) {
return state[ context ] || DEFAULT_NOTICES;
Expand Down
Loading

0 comments on commit 5dbc641

Please sign in to comment.