diff --git a/src/component/base/DraftEditor.react.js b/src/component/base/DraftEditor.react.js index 490db7e8f5..43cb792c6f 100644 --- a/src/component/base/DraftEditor.react.js +++ b/src/component/base/DraftEditor.react.js @@ -134,6 +134,7 @@ class UpdateDraftEditorFlags extends React.Component<{ */ class DraftEditor extends React.Component { static defaultProps: DraftEditorDefaultProps = { + ariaDescribedBy: '{{editor_id_placeholder}}', blockRenderMap: DefaultDraftBlockRenderMap, blockRendererFn: function() { return null; @@ -314,6 +315,22 @@ class DraftEditor extends React.Component { return null; } + /** + * returns ariaDescribedBy prop with '{{editor_id_placeholder}}' replaced with + * the DOM id of the placeholder (if it exists) + * @returns aria-describedby attribute value + */ + _renderARIADescribedBy(): ?string { + const describedBy = this.props.ariaDescribedBy || ''; + const placeholderID = this._showPlaceholder() + ? this._placeholderAccessibilityID + : ''; + return ( + describedBy.replace('{{editor_id_placeholder}}', placeholderID) || + undefined + ); + } + render(): React.Node { const { blockRenderMap, @@ -381,9 +398,7 @@ class DraftEditor extends React.Component { } aria-autocomplete={readOnly ? null : this.props.ariaAutoComplete} aria-controls={readOnly ? null : this.props.ariaControls} - aria-describedby={ - this.props.ariaDescribedBy || this._placeholderAccessibilityID - } + aria-describedby={this._renderARIADescribedBy()} aria-expanded={readOnly ? null : ariaExpanded} aria-label={this.props.ariaLabel} aria-labelledby={this.props.ariaLabelledBy} diff --git a/src/component/base/DraftEditorProps.js b/src/component/base/DraftEditorProps.js index 592b9cde85..317a73c9e6 100644 --- a/src/component/base/DraftEditorProps.js +++ b/src/component/base/DraftEditorProps.js @@ -89,6 +89,12 @@ export type DraftEditorProps = { ariaActiveDescendantID?: string, ariaAutoComplete?: string, ariaControls?: string, + /** + * aria-describedby attribute. should point to the id of a descriptive + * element. The substring, "{{editor_id_placeholder}}" will be replaced with + * the DOM id of the placeholder element if it exists. + * @default "{{editor_id_placeholder}}" + */ ariaDescribedBy?: string, ariaExpanded?: boolean, ariaLabel?: string, @@ -175,6 +181,7 @@ export type DraftEditorProps = { }; export type DraftEditorDefaultProps = { + ariaDescribedBy: string, blockRenderMap: DraftBlockRenderMap, blockRendererFn: (block: BlockNodeRecord) => ?Object, blockStyleFn: (block: BlockNodeRecord) => string, diff --git a/src/component/base/__tests__/DraftEditor.react-test.js b/src/component/base/__tests__/DraftEditor.react-test.js index 0e57bd0e2e..2d4488106c 100644 --- a/src/component/base/__tests__/DraftEditor.react-test.js +++ b/src/component/base/__tests__/DraftEditor.react-test.js @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @emails oncall+draft_js + * @flow * @format */ @@ -13,18 +14,22 @@ jest.mock('generateRandomKey'); const DraftEditor = require('DraftEditor.react'); +const EditorState = require('EditorState'); const React = require('React'); +// $FlowFixMe const ReactShallowRenderer = require('react-test-renderer/shallow'); let shallow; +let editorState; beforeEach(() => { shallow = new ReactShallowRenderer(); + editorState = EditorState.createEmpty(); }); test('must has generated editorKey', () => { - shallow.render(); + shallow.render( {}} />); // internally at Facebook we use a newer version of the shallowRenderer // which has a different level of wrapping of the '_instance' @@ -36,7 +41,13 @@ test('must has generated editorKey', () => { }); test('must has editorKey same as props', () => { - shallow.render(); + shallow.render( + {}} + editorKey="hash" + />, + ); // internally at Facebook we use a newer version of the shallowRenderer // which has a different level of wrapping of the '_instance' @@ -46,3 +57,82 @@ test('must has editorKey same as props', () => { shallow._instance.getEditorKey || shallow._instance._instance.getEditorKey; expect(getEditorKey()).toMatchSnapshot(); }); + +describe('ariaDescribedBy', () => { + function getProps(elem) { + const r = shallow.render(elem); + const ec = r.props.children[1].props.children; + return ec.props; + } + + describe('without placeholder', () => { + test('undefined by default', () => { + const props = getProps( + {}} />, + ); + expect(props).toHaveProperty('aria-describedby', undefined); + }); + + test('can be set to something arbitrary', () => { + const props = getProps( + {}} + ariaDescribedBy="abc" + />, + ); + expect(props).toHaveProperty('aria-describedby', 'abc'); + }); + + test('can use special token', () => { + const props = getProps( + {}} + ariaDescribedBy="abc {{editor_id_placeholder}} xyz" + />, + ); + expect(props).toHaveProperty('aria-describedby', 'abc xyz'); + }); + }); + + describe('with placeholder', () => { + test('has placeholder id by default', () => { + const props = getProps( + {}} + editorKey="X" + placeholder="place" + />, + ); + expect(props).toHaveProperty('aria-describedby', 'placeholder-X'); + }); + + test('can be set to something arbitrary', () => { + const props = getProps( + {}} + editorKey="X" + placeholder="place" + ariaDescribedBy="abc" + />, + ); + expect(props).toHaveProperty('aria-describedby', 'abc'); + }); + + test('can use special token', () => { + const props = getProps( + {}} + editorKey="X" + placeholder="place" + ariaDescribedBy="abc {{editor_id_placeholder}} xyz" + />, + ); + expect(props).toHaveProperty('aria-describedby', 'abc placeholder-X xyz'); + }); + }); +}); diff --git a/src/component/base/__tests__/__snapshots__/DraftEditor.react-test.js.snap b/src/component/base/__tests__/__snapshots__/DraftEditor.react-test.js.snap index 87095681e3..e66c254924 100644 --- a/src/component/base/__tests__/__snapshots__/DraftEditor.react-test.js.snap +++ b/src/component/base/__tests__/__snapshots__/DraftEditor.react-test.js.snap @@ -2,4 +2,4 @@ exports[`must has editorKey same as props 1`] = `"hash"`; -exports[`must has generated editorKey 1`] = `"key0"`; +exports[`must has generated editorKey 1`] = `"key1"`;