From 700f2a140fd09d592f6e44a46b8a1619c817efd9 Mon Sep 17 00:00:00 2001 From: Miguel Fonseca Date: Wed, 25 Oct 2017 14:34:12 +0100 Subject: [PATCH] Editable: Pass all aria-* props to TinyMCE With this change, Editable expects ARIA attributes to be provided as individual props: ``. This replaces the approach whereby a single `aria` object prop would be passed. This allows Editable's interface to be kept similar to any native element's interface. One benefit we now get for free is that the linter is able to warn the author if they pass an invalid ARIA attribute: aria-foo: This attribute is an invalid ARIA attribute. (jsx-a11y/aria-props) [eslint] --- blocks/editable/aria.js | 17 +++++++++++++++++ blocks/editable/index.js | 7 +++++-- blocks/editable/tinymce.js | 30 +++++++++++++++++++----------- 3 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 blocks/editable/aria.js diff --git a/blocks/editable/aria.js b/blocks/editable/aria.js new file mode 100644 index 00000000000000..b88424f50fc1cd --- /dev/null +++ b/blocks/editable/aria.js @@ -0,0 +1,17 @@ +/** + * External dependencies + */ + +import { + pickBy, + startsWith, +} from 'lodash'; + +const isAriaPropName = ( name ) => + startsWith( name, 'aria-' ); + +export const getAriaKeys = ( props ) => + Object.keys( props ).filter( isAriaPropName ); + +export const pickAriaProps = ( props ) => + pickBy( props, ( value, key ) => isAriaPropName( key ) ); diff --git a/blocks/editable/index.js b/blocks/editable/index.js index 9ac46f65d098d5..2da5289e2fe1c6 100644 --- a/blocks/editable/index.js +++ b/blocks/editable/index.js @@ -31,6 +31,7 @@ import './style.scss'; import { pasteHandler } from '../api'; import FormatToolbar from './format-toolbar'; import TinyMCE from './tinymce'; +import { pickAriaProps } from './aria'; import patterns from './patterns'; import { EVENTS } from './constants'; @@ -609,11 +610,12 @@ export default class Editable extends Component { inlineToolbar = false, formattingControls, placeholder, - aria, multiline: MultilineTag, keepPlaceholderOnFocus = false, } = this.props; + const ariaProps = pickAriaProps( this.props ); + // Generating a key that includes `tagName` ensures that if the tag // changes, we unmount and destroy the previous TinyMCE element, then // mount and initialize a new child element in its place. @@ -650,7 +652,8 @@ export default class Editable extends Component { style={ style } defaultValue={ value } isPlaceholderVisible={ isPlaceholderVisible } - aria={ { label: placeholder, ...aria } } + aria-label={ placeholder } + { ...ariaProps } className={ className } key={ key } /> diff --git a/blocks/editable/tinymce.js b/blocks/editable/tinymce.js index 600acbcd01e751..91d5fc5c2aa7e4 100644 --- a/blocks/editable/tinymce.js +++ b/blocks/editable/tinymce.js @@ -2,7 +2,7 @@ * External dependencies */ import tinymce from 'tinymce'; -import { difference, filter, forEach, isEqual, keys, mapKeys } from 'lodash'; +import { difference, isEqual } from 'lodash'; import classnames from 'classnames'; /** @@ -10,6 +10,11 @@ import classnames from 'classnames'; */ import { Component, Children, createElement } from '@wordpress/element'; +/** + * Internal dependencies + */ +import { getAriaKeys, pickAriaProps } from './aria'; + export default class TinyMCE extends Component { componentDidMount() { this.initialize(); @@ -40,14 +45,16 @@ export default class TinyMCE extends Component { this.editorNode.className = classnames( nextProps.className, 'blocks-editable__tinymce' ); } - if ( ! isEqual( this.props.aria, nextProps.aria ) ) { - const before = keys( this.props.aria ); - const after = keys( nextProps.aria ); - const removed = difference( before, after ); - const updated = filter( after, ( key ) => ! isEqual( this.props.aria[ key ], nextProps.aria[ key ] ) ); - forEach( removed, ( key ) => this.editorNode.removeAttribute( 'aria-' + key ) ); - forEach( updated, ( key ) => this.editorNode.setAttribute( 'aria-' + key, nextProps.aria[ key ] ) ); - } + const prevAriaKeys = getAriaKeys( this.props ); + const nextAriaKeys = getAriaKeys( nextProps ); + const removedKeys = difference( prevAriaKeys, nextAriaKeys ); + const updatedKeys = nextAriaKeys.filter( ( key ) => + ! isEqual( this.props[ key ], nextProps[ key ] ) ); + + removedKeys.forEach( ( key ) => + this.editorNode.removeAttribute( key ) ); + updatedKeys.forEach( ( key ) => + this.editorNode.setAttribute( key, nextProps[ key ] ) ); } componentWillUnmount() { @@ -97,7 +104,8 @@ export default class TinyMCE extends Component { } render() { - const { tagName = 'div', style, defaultValue, aria, className } = this.props; + const { tagName = 'div', style, defaultValue, className } = this.props; + const ariaProps = pickAriaProps( this.props ); // If a default value is provided, render it into the DOM even before // TinyMCE finishes initializing. This avoids a short delay by allowing @@ -113,7 +121,7 @@ export default class TinyMCE extends Component { suppressContentEditableWarning: true, className: classnames( className, 'blocks-editable__tinymce' ), style, - ...( mapKeys( aria, ( value, key ) => 'aria-' + key ) ), + ...ariaProps, }, children ); } }