Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #444 from ckeditor/t/ckeditor5-media-embed/35
Browse files Browse the repository at this point in the history
Feature: Implemented `LabeledInputView#infoText` to display useful hints next to the input (see ckeditor/ckeditor5-media-embed#35).
  • Loading branch information
oleq authored Oct 29, 2018
2 parents 3d0b138 + fd94542 commit 6ac03ea
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 35 deletions.
76 changes: 56 additions & 20 deletions src/labeledinput/labeledinputview.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default class LabeledInputView extends View {
super( locale );

const inputUid = `ck-input-${ uid() }`;
const errorUid = `ck-error-${ uid() }`;
const statusUid = `ck-status-${ uid() }`;

/**
* The text of the label.
Expand Down Expand Up @@ -72,6 +72,21 @@ export default class LabeledInputView extends View {
*/
this.set( 'errorText', null );

/**
* The additional information text displayed next to the {@link #inputView} which can
* be used to inform the user about the purpose of the input, provide help or hints.
*
* Set it to `null` to hide the message.
*
* **Note:** This text will be displayed in the same place as {@link #errorText} but the
* latter always takes precedence: if the {@link #errorText} is set, it replaces
* {@link #errorText} for as long as the value of the input is invalid.
*
* @observable
* @member {String|null} #infoText
*/
this.set( 'infoText', null );

/**
* The label view.
*
Expand All @@ -84,14 +99,33 @@ export default class LabeledInputView extends View {
*
* @member {module:ui/inputtext/inputtextview~InputTextView} #inputView
*/
this.inputView = this._createInputView( InputView, inputUid, errorUid );
this.inputView = this._createInputView( InputView, inputUid, statusUid );

/**
* The status view for the {@link #inputView}. It displays {@link #errorText} and
* {@link #infoText}.
*
* @member {module:ui/view~View} #statusView
*/
this.statusView = this._createStatusView( statusUid );

/**
* The error view for the {@link #inputView}.
* The combined status text made of {@link #errorText} and {@link #infoText}.
* Note that when present, {@link #errorText} always takes precedence in the
* status.
*
* @member {module:ui/view~View} #errorView
* @see #errorText
* @see #infoText
* @see #statusView
* @private
* @observable
* @member {String|null} #_statusText
*/
this.errorView = this._createErrorView( errorUid );
this.bind( '_statusText' ).to(
this, 'errorText',
this, 'infoText',
( errorText, infoText ) => errorText || infoText
);

const bind = this.bindTemplate;

Expand All @@ -107,7 +141,7 @@ export default class LabeledInputView extends View {
children: [
this.labelView,
this.inputView,
this.errorView
this.statusView
]
} );
}
Expand All @@ -134,14 +168,14 @@ export default class LabeledInputView extends View {
* @private
* @param {Function} InputView Input view constructor.
* @param {String} inputUid Unique id to set as inputView#id attribute.
* @param {String} errorUid Unique id of the error for the input's `aria-describedby` attribute.
* @param {String} statusUid Unique id of the status for the input's `aria-describedby` attribute.
* @returns {module:ui/inputtext/inputtextview~InputTextView}
*/
_createInputView( InputView, inputUid, errorUid ) {
const inputView = new InputView( this.locale, errorUid );
_createInputView( InputView, inputUid, statusUid ) {
const inputView = new InputView( this.locale, statusUid );

inputView.id = inputUid;
inputView.ariaDesribedById = errorUid;
inputView.ariaDesribedById = statusUid;
inputView.bind( 'value' ).to( this );
inputView.bind( 'isReadOnly' ).to( this );
inputView.bind( 'hasError' ).to( this, 'errorText', value => !!value );
Expand All @@ -156,34 +190,36 @@ export default class LabeledInputView extends View {
}

/**
* Creates the error view instance.
* Creates the status view instance. It displays {@link #errorText} and {@link #infoText}
* next to the {@link #inputView}. See {@link #_statusText}.
*
* @private
* @param {String} errorUid Unique id of the error, shared with the input's `aria-describedby` attribute.
* @param {String} statusUid Unique id of the status, shared with the input's `aria-describedby` attribute.
* @returns {module:ui/view~View}
*/
_createErrorView( errorUid ) {
const errorView = new View( this.locale );
_createStatusView( statusUid ) {
const statusView = new View( this.locale );
const bind = this.bindTemplate;

errorView.setTemplate( {
statusView.setTemplate( {
tag: 'div',
attributes: {
class: [
'ck',
'ck-labeled-input__error',
bind.if( 'errorText', 'ck-hidden', value => !value )
'ck-labeled-input__status',
bind.if( 'errorText', 'ck-labeled-input__status_error' ),
bind.if( '_statusText', 'ck-hidden', value => !value )
],
id: errorUid
id: statusUid
},
children: [
{
text: bind.to( 'errorText' )
text: bind.to( '_statusText' )
}
]
} );

return errorView;
return statusView;
}

/**
Expand Down
50 changes: 35 additions & 15 deletions tests/labeledinput/labeledinputview.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ describe( 'LabeledInputView', () => {
expect( view.errorText ).to.be.null;
} );

it( 'should set view#infoText', () => {
expect( view.infoText ).to.be.null;
} );

it( 'should create view#inputView', () => {
expect( view.inputView ).to.instanceOf( InputView );
} );
Expand All @@ -36,20 +40,20 @@ describe( 'LabeledInputView', () => {
expect( view.labelView ).to.instanceOf( LabelView );
} );

it( 'should create view#errorView', () => {
expect( view.errorView ).to.instanceOf( View );
it( 'should create view#statusView', () => {
expect( view.statusView ).to.instanceOf( View );

expect( view.errorView.element.tagName ).to.equal( 'DIV' );
expect( view.errorView.element.classList.contains( 'ck' ) ).to.be.true;
expect( view.errorView.element.classList.contains( 'ck-labeled-input__error' ) ).to.be.true;
expect( view.statusView.element.tagName ).to.equal( 'DIV' );
expect( view.statusView.element.classList.contains( 'ck' ) ).to.be.true;
expect( view.statusView.element.classList.contains( 'ck-labeled-input__status' ) ).to.be.true;
} );

it( 'should pair #inputView and #labelView by unique id', () => {
expect( view.labelView.for ).to.equal( view.inputView.id );
} );

it( 'should pair #inputView and #errorView by unique id', () => {
expect( view.inputView.ariaDesribedById ).to.equal( view.errorView.element.id );
it( 'should pair #inputView and #statusView by unique id', () => {
expect( view.inputView.ariaDesribedById ).to.equal( view.statusView.element.id );
} );
} );

Expand All @@ -67,8 +71,8 @@ describe( 'LabeledInputView', () => {
expect( view.template.children[ 1 ] ).to.equal( view.inputView );
} );

it( 'should have the error container', () => {
expect( view.template.children[ 2 ] ).to.equal( view.errorView );
it( 'should have the status container', () => {
expect( view.template.children[ 2 ] ).to.equal( view.statusView );
} );

describe( 'DOM bindings', () => {
Expand All @@ -82,17 +86,33 @@ describe( 'LabeledInputView', () => {
} );
} );

describe( 'error container', () => {
describe( 'status container', () => {
it( 'should react on view#errorText', () => {
const errorContainer = view.element.lastChild;
const statusElement = view.statusView.element;

view.errorText = '';
expect( errorContainer.classList.contains( 'ck-hidden' ) ).to.be.true;
expect( errorContainer.innerHTML ).to.equal( '' );
expect( statusElement.classList.contains( 'ck-hidden' ) ).to.be.true;
expect( statusElement.classList.contains( 'ck-labeled-input__status_error' ) ).to.be.false;
expect( statusElement.innerHTML ).to.equal( '' );

view.errorText = 'foo';
expect( errorContainer.classList.contains( 'ck-hidden' ) ).to.be.false;
expect( errorContainer.innerHTML ).to.equal( 'foo' );
expect( statusElement.classList.contains( 'ck-hidden' ) ).to.be.false;
expect( statusElement.classList.contains( 'ck-labeled-input__status_error' ) ).to.be.true;
expect( statusElement.innerHTML ).to.equal( 'foo' );
} );

it( 'should react on view#infoText', () => {
const statusElement = view.statusView.element;

view.infoText = '';
expect( statusElement.classList.contains( 'ck-hidden' ) ).to.be.true;
expect( statusElement.classList.contains( 'ck-labeled-input__status_error' ) ).to.be.false;
expect( statusElement.innerHTML ).to.equal( '' );

view.infoText = 'foo';
expect( statusElement.classList.contains( 'ck-hidden' ) ).to.be.false;
expect( statusElement.classList.contains( 'ck-labeled-input__status_error' ) ).to.be.false;
expect( statusElement.innerHTML ).to.equal( 'foo' );
} );
} );
} );
Expand Down

0 comments on commit 6ac03ea

Please sign in to comment.