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

Allow editable to accept aria attributes #3106

Merged
merged 6 commits into from
Oct 29, 2017
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
28 changes: 28 additions & 0 deletions blocks/editable/aria.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* External dependencies
*/

import {
difference,
isEqual,
pickBy,
startsWith,
} from 'lodash';

const isAriaPropName = ( name ) =>
startsWith( name, 'aria-' );

const getAriaKeys = ( props ) =>
Object.keys( props ).filter( isAriaPropName );

export const pickAriaProps = ( props ) =>
pickBy( props, ( value, key ) => isAriaPropName( key ) );

export const diffAriaProps = ( props, nextProps ) => {
const prevAriaKeys = getAriaKeys( props );
const nextAriaKeys = getAriaKeys( nextProps );
const removedKeys = difference( prevAriaKeys, nextAriaKeys );
const updatedKeys = nextAriaKeys.filter( ( key ) =>
! isEqual( props[ key ], nextProps[ key ] ) );
return { removedKeys, updatedKeys };
};
6 changes: 5 additions & 1 deletion blocks/editable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -656,6 +657,8 @@ export default class Editable extends Component {
formatters,
} = 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.
Expand Down Expand Up @@ -693,7 +696,8 @@ export default class Editable extends Component {
style={ style }
defaultValue={ value }
isPlaceholderVisible={ isPlaceholderVisible }
label={ placeholder }
aria-label={ placeholder }
{ ...ariaProps }
className={ className }
key={ key }
/>
Expand Down
110 changes: 110 additions & 0 deletions blocks/editable/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { shallow } from 'enzyme';
* Internal dependencies
*/
import Editable from '../';
import { diffAriaProps, pickAriaProps } from '../aria';

describe( 'Editable', () => {
describe( '.propTypes', () => {
Expand Down Expand Up @@ -40,4 +41,113 @@ describe( 'Editable', () => {
} );
/* eslint-enable no-console */
} );
describe( 'pickAriaProps()', () => {
it( 'should should filter all properties to only those begining with "aria-"', () => {
expect( pickAriaProps( {
tagName: 'p',
className: 'class1 class2',
'aria-label': 'my label',
style: {
backgroundColor: 'white',
color: 'black',
fontSize: '12px',
textAlign: 'left',
},
'aria-owns': 'some-id',
'not-aria-prop': 'value',
ariaWithoutDash: 'value',
} ) ).toEqual( {
'aria-label': 'my label',
'aria-owns': 'some-id',
} );
} );
} );
describe( 'diffAriaProps()', () => {
it( 'should report empty arrays for no props', () => {
expect( diffAriaProps( {}, {} ) ).toEqual( {
removedKeys: [],
updatedKeys: [],
} );
} );
it( 'should report empty arrays for non-aria props', () => {
expect( diffAriaProps( {
'non-aria-prop': 'old value',
'removed-prop': 'value',
}, {
'non-aria-prop': 'new value',
'added-prop': 'value',
} ) ).toEqual( {
removedKeys: [],
updatedKeys: [],
} );
} );
it( 'should report added aria props', () => {
expect( diffAriaProps( {
}, {
'aria-prop': 'value',
} ) ).toEqual( {
removedKeys: [],
updatedKeys: [ 'aria-prop' ],
} );
} );
it( 'should report removed aria props', () => {
expect( diffAriaProps( {
'aria-prop': 'value',
}, {
} ) ).toEqual( {
removedKeys: [ 'aria-prop' ],
updatedKeys: [],
} );
} );
it( 'should report changed aria props', () => {
expect( diffAriaProps( {
'aria-prop': 'old value',
}, {
'aria-prop': 'new value',
} ) ).toEqual( {
removedKeys: [],
updatedKeys: [ 'aria-prop' ],
} );
} );
it( 'should not report unchanged aria props', () => {
expect( diffAriaProps( {
'aria-prop': 'value',
}, {
'aria-prop': 'value',
} ) ).toEqual( {
removedKeys: [],
updatedKeys: [],
} );
} );
it( 'should work with a mixture of aria and non-aria props', () => {
expect( diffAriaProps( {
tagName: 'p',
className: 'class1 class2',
'aria-label': 'my label',
style: {
backgroundColor: 'white',
color: 'black',
fontSize: '12px',
textAlign: 'left',
},
'aria-owns': 'some-id',
'aria-active': 'some-active-id',
'not-aria-prop': 'old value',
}, {
tagName: 'div',
className: 'class1 class2',
style: {
backgroundColor: 'red',
color: 'black',
fontSize: '12px',
},
'aria-active': 'some-other-active-id',
'not-aria-prop': 'new value',
'aria-label': 'my label',
} ) ).toEqual( {
removedKeys: [ 'aria-owns' ],
updatedKeys: [ 'aria-active' ],
} );
} );
} );
} );
16 changes: 14 additions & 2 deletions blocks/editable/tinymce.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import classnames from 'classnames';
*/
import { Component, Children, createElement } from '@wordpress/element';

/**
* Internal dependencies
*/
import { diffAriaProps, pickAriaProps } from './aria';

export default class TinyMCE extends Component {
componentDidMount() {
this.initialize();
Expand Down Expand Up @@ -39,6 +44,12 @@ export default class TinyMCE extends Component {
if ( ! isEqual( this.props.className, nextProps.className ) ) {
this.editorNode.className = classnames( nextProps.className, 'blocks-editable__tinymce' );
}

const { removedKeys, updatedKeys } = diffAriaProps( this.props, nextProps );
removedKeys.forEach( ( key ) =>
this.editorNode.removeAttribute( key ) );
updatedKeys.forEach( ( key ) =>
this.editorNode.setAttribute( key, nextProps[ key ] ) );
}

componentWillUnmount() {
Expand Down Expand Up @@ -88,7 +99,8 @@ export default class TinyMCE extends Component {
}

render() {
const { tagName = 'div', style, defaultValue, label, 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
Expand All @@ -104,7 +116,7 @@ export default class TinyMCE extends Component {
suppressContentEditableWarning: true,
className: classnames( className, 'blocks-editable__tinymce' ),
style,
'aria-label': label,
...ariaProps,
}, children );
}
}