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

Compat: Upgrade admin notices to use Notices module at runtime #11604

Merged
merged 16 commits into from
Nov 19, 2018
Merged
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
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