Skip to content

Commit

Permalink
Merge pull request #12320 from ckeditor/ck/12193-keyboard-navigation-…
Browse files Browse the repository at this point in the history
…for-table-color-picker

Fix (table): Made color pickers in table and table cell properties forms accessible for keyboard users. Closes #12193.
  • Loading branch information
oleq authored Aug 31, 2022
2 parents 4ec664f + d29c226 commit 8162d92
Show file tree
Hide file tree
Showing 9 changed files with 324 additions and 129 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -409,8 +409,10 @@ export default class TableCellPropertiesView extends View {
[
this.borderStyleDropdown,
this.borderColorInput,
this.borderColorInput.fieldView.dropdownView.buttonView,
this.borderWidthInput,
this.backgroundInput,
this.backgroundInput.fieldView.dropdownView.buttonView,
this.widthInput,
this.heightInput,
this.paddingInput,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,8 +360,10 @@ export default class TablePropertiesView extends View {
[
this.borderStyleDropdown,
this.borderColorInput,
this.borderColorInput.fieldView.dropdownView.buttonView,
this.borderWidthInput,
this.backgroundInput,
this.backgroundInput.fieldView.dropdownView.buttonView,
this.widthInput,
this.heightInput,
this.alignmentToolbar,
Expand Down
139 changes: 88 additions & 51 deletions packages/ckeditor5-table/src/ui/colorinputview.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
* @module table/ui/colorinputview
*/

import { View, InputTextView, ButtonView, createDropdown, ColorGridView } from 'ckeditor5/src/ui';
import { View, InputTextView, ButtonView, createDropdown, ColorGridView, FocusCycler, ViewCollection } from 'ckeditor5/src/ui';
import { icons } from 'ckeditor5/src/core';
import { FocusTracker, KeystrokeHandler } from 'ckeditor5/src/utils';

import '../../theme/colorinput.css';

Expand All @@ -34,8 +35,6 @@ export default class ColorInputView extends View {
constructor( locale, options ) {
super( locale );

const bind = this.bindTemplate;

/**
* The value of the input.
*
Expand All @@ -45,14 +44,6 @@ export default class ColorInputView extends View {
*/
this.set( 'value', '' );

/**
* The `id` attribute of the input (i.e. to pair with the `<label>` element).
*
* @observable
* @member {String} #id
*/
this.set( 'id' );

/**
* Controls whether the input view is in read-only mode.
*
Expand All @@ -62,16 +53,6 @@ export default class ColorInputView extends View {
*/
this.set( 'isReadOnly', false );

/**
* Set to `true` when the field has some error. Usually controlled via
* {@link module:ui/labeledinput/labeledinputview~LabeledInputView#errorText}.
*
* @observable
* @member {Boolean} #hasError
* @default false
*/
this.set( 'hasError', false );

/**
* An observable flag set to `true` when the input is focused by the user.
* `false` otherwise.
Expand All @@ -94,77 +75,127 @@ export default class ColorInputView extends View {
this.set( 'isEmpty', true );

/**
* The `id` of the element describing this field. When the field has
* some error, it helps screen readers read the error text.
* A cached reference to the options passed to the constructor.
*
* @member {Object}
*/
this.options = options;

/**
* Tracks information about the DOM focus in the view.
*
* @observable
* @member {String} #ariaDescribedById
* @readonly
* @member {module:utils/focustracker~FocusTracker}
*/
this.set( 'ariaDescribedById' );
this.focusTracker = new FocusTracker();

/**
* A cached reference to the options passed to the constructor.
* A collection of views that can be focused in the view.
*
* @member {Object}
* @readonly
* @protected
* @member {module:ui/viewcollection~ViewCollection}
*/
this.options = options;
this._focusables = new ViewCollection();

/**
* An instance of the dropdown allowing to select a color from a grid.
*
* @protected
* @member {module:ui/dropdown/dropdown~DropdownView}
*/
this._dropdownView = this._createDropdownView();
this.dropdownView = this._createDropdownView();

/**
* An instance of the input allowing the user to type a color value.
*
* @protected
* @member {module:ui/inputtext/inputtextview~InputTextView}
*/
this._inputView = this._createInputTextView();
this.inputView = this._createInputTextView();

/**
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
*
* @readonly
* @member {module:utils/keystrokehandler~KeystrokeHandler}
*/
this.keystrokes = new KeystrokeHandler();

/**
* The flag that indicates whether the user is still typing.
* If set to true, it means that the text input field ({@link #_inputView}) still has the focus.
* If set to true, it means that the text input field ({@link #inputView}) still has the focus.
* So, we should interrupt the user by replacing the input's value.
*
* @protected
* @member {Boolean}
*/
this._stillTyping = false;

/**
* Helps cycling over focusable items in the view.
*
* @readonly
* @protected
* @member {module:ui/focuscycler~FocusCycler}
*/
this._focusCycler = new FocusCycler( {
focusables: this._focusables,
focusTracker: this.focusTracker,
keystrokeHandler: this.keystrokes,
actions: {
// Navigate items backwards using the <kbd>Shift</kbd> + <kbd>Tab</kbd> keystroke.
focusPrevious: 'shift + tab',

// Navigate items forwards using the <kbd>Tab</kbd> key.
focusNext: 'tab'
}
} );

this.setTemplate( {
tag: 'div',
attributes: {
class: [
'ck',
'ck-input-color',
bind.if( 'hasError', 'ck-error' )
],
id: bind.to( 'id' ),
'aria-invalid': bind.if( 'hasError', true ),
'aria-describedby': bind.to( 'ariaDescribedById' )
'ck-input-color'
]
},
children: [
this._dropdownView,
this._inputView
this.dropdownView,
this.inputView
]
} );

this.on( 'change:value', ( evt, name, inputValue ) => this._setInputValue( inputValue ) );
}

/**
* @inheritDoc
*/
render() {
super.render();

// Start listening for the keystrokes coming from the dropdown panel view.
this.keystrokes.listenTo( this.dropdownView.panelView.element );
}

/**
* Focuses the input.
*/
focus() {
this._inputView.focus();
this.inputView.focus();
}

/**
* Creates and configures the {@link #_dropdownView}.
* @inheritDoc
*/
destroy() {
super.destroy();

this.focusTracker.destroy();
this.keystrokes.destroy();
}

/**
* Creates and configures the {@link #dropdownView}.
*
* @private
*/
Expand Down Expand Up @@ -215,14 +246,20 @@ export default class ColorInputView extends View {
dropdown.panelView.children.add( colorGrid );
dropdown.bind( 'isEnabled' ).to( this, 'isReadOnly', value => !value );

this._focusables.add( removeColorButton );
this._focusables.add( colorGrid );

this.focusTracker.add( removeColorButton.element );
this.focusTracker.add( colorGrid.element );

return dropdown;
}

/**
* Creates and configures an instance of {@link module:ui/inputtext/inputtextview~InputTextView}.
*
* @private
* @returns {module:ui/inputtext/inputtextview~InputTextView} A configured instance to be set as {@link #_inputView}.
* @returns {module:ui/inputtext/inputtextview~InputTextView} A configured instance to be set as {@link #inputView}.
*/
_createInputTextView() {
const locale = this.locale;
Expand Down Expand Up @@ -275,15 +312,15 @@ export default class ColorInputView extends View {
removeColorButton.label = removeColorButtonLabel;
removeColorButton.on( 'execute', () => {
this.value = defaultColor;
this._dropdownView.isOpen = false;
this.dropdownView.isOpen = false;
this.fire( 'input' );
} );

return removeColorButton;
}

/**
* Creates and configures the color grid inside the {@link #_dropdownView}.
* Creates and configures the color grid inside the {@link #dropdownView}.
*
* @private
*/
Expand All @@ -295,7 +332,7 @@ export default class ColorInputView extends View {

colorGrid.on( 'execute', ( evtData, data ) => {
this.value = data.value;
this._dropdownView.isOpen = false;
this.dropdownView.isOpen = false;
this.fire( 'input' );
} );
colorGrid.bind( 'selectedColor' ).to( this, 'value' );
Expand All @@ -304,7 +341,7 @@ export default class ColorInputView extends View {
}

/**
* Sets {@link #_inputView}'s value property to the color value or color label,
* Sets {@link #inputView}'s value property to the color value or color label,
* if there is one and the user is not typing.
*
* Handles cases like:
Expand All @@ -322,9 +359,9 @@ export default class ColorInputView extends View {
const mappedColor = this.options.colorDefinitions.find( def => normalizedInputValue === normalizeColor( def.color ) );

if ( mappedColor ) {
this._inputView.value = mappedColor.label;
this.inputView.value = mappedColor.label;
} else {
this._inputView.value = inputValue || '';
this.inputView.value = inputValue || '';
}
}
}
Expand Down
14 changes: 7 additions & 7 deletions packages/ckeditor5-table/src/utils/ui/table-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -372,29 +372,29 @@ export const defaultColors = [
*/
export function getLabeledColorInputCreator( options ) {
return ( labeledFieldView, viewUid, statusUid ) => {
const inputView = new ColorInputView( labeledFieldView.locale, {
const colorInputView = new ColorInputView( labeledFieldView.locale, {
colorDefinitions: colorConfigToColorGridDefinitions( options.colorConfig ),
columns: options.columns,
defaultColorValue: options.defaultColorValue
} );

inputView.set( {
colorInputView.inputView.set( {
id: viewUid,
ariaDescribedById: statusUid
} );

inputView.bind( 'isReadOnly' ).to( labeledFieldView, 'isEnabled', value => !value );
inputView.bind( 'hasError' ).to( labeledFieldView, 'errorText', value => !!value );
colorInputView.bind( 'isReadOnly' ).to( labeledFieldView, 'isEnabled', value => !value );
colorInputView.bind( 'hasError' ).to( labeledFieldView, 'errorText', value => !!value );

inputView.on( 'input', () => {
colorInputView.on( 'input', () => {
// UX: Make the error text disappear and disable the error indicator as the user
// starts fixing the errors.
labeledFieldView.errorText = null;
} );

labeledFieldView.bind( 'isEmpty', 'isFocused' ).to( inputView );
labeledFieldView.bind( 'isEmpty', 'isFocused' ).to( colorInputView );

return inputView;
return colorInputView;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -681,8 +681,10 @@ describe( 'table cell properties', () => {
expect( view._focusables.map( f => f ) ).to.have.members( [
view.borderStyleDropdown,
view.borderColorInput,
view.borderColorInput.fieldView.dropdownView.buttonView,
view.borderWidthInput,
view.backgroundInput,
view.backgroundInput.fieldView.dropdownView.buttonView,
view.widthInput,
view.heightInput,
view.paddingInput,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -616,8 +616,10 @@ describe( 'table properties', () => {
expect( view._focusables.map( f => f ) ).to.have.members( [
view.borderStyleDropdown,
view.borderColorInput,
view.borderColorInput.fieldView.dropdownView.buttonView,
view.borderWidthInput,
view.backgroundInput,
view.backgroundInput.fieldView.dropdownView.buttonView,
view.widthInput,
view.heightInput,
view.alignmentToolbar,
Expand Down Expand Up @@ -810,7 +812,7 @@ describe( 'table properties', () => {

it( 'should replace "Remove color" with the "Restore default" label', () => {
const { borderColorInput } = view;
const { panelView } = borderColorInput.fieldView._dropdownView;
const { panelView } = borderColorInput.fieldView.dropdownView;

expect( panelView.children.first.label ).to.equal( 'Restore default' );
} );
Expand All @@ -820,7 +822,7 @@ describe( 'table properties', () => {
describe( 'background row', () => {
it( 'should replace "Remove color" with the "Restore default" label', () => {
const { backgroundInput } = view;
const { panelView } = backgroundInput.fieldView._dropdownView;
const { panelView } = backgroundInput.fieldView.dropdownView;

expect( panelView.children.first.label ).to.equal( 'Restore default' );
} );
Expand Down
Loading

0 comments on commit 8162d92

Please sign in to comment.