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

Commit b8fbaf1

Browse files
authored
Fix: FocusCycler should ignore invisible views. Closes #209.
T/209: FocusCycler should not consider invisible views.
2 parents 66a30b1 + f9805d1 commit b8fbaf1

File tree

5 files changed

+61
-19
lines changed

5 files changed

+61
-19
lines changed

src/button/buttonview.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ export default class ButtonView extends View {
8484
*/
8585
this.set( 'isEnabled', true );
8686

87+
/**
88+
* Controls whether the button view is visible.
89+
*
90+
* @observable
91+
* @member {Boolean} #isVisible
92+
*/
93+
this.set( 'isVisible', true );
94+
8795
/**
8896
* (Optional) Whether the label of the button is hidden (e.g. button with icon only).
8997
*
@@ -142,6 +150,7 @@ export default class ButtonView extends View {
142150
'ck-button',
143151
bind.if( '_tooltipString', 'ck-tooltip_s' ),
144152
bind.to( 'isEnabled', value => value ? 'ck-enabled' : 'ck-disabled' ),
153+
bind.if( 'isVisible', 'ck-hidden', value => !value ),
145154
bind.to( 'isOn', value => value ? 'ck-on' : 'ck-off' ),
146155
bind.if( 'withText', 'ck-button_with-text' )
147156
],

src/focuscycler.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* @module ui/focuscycler
88
*/
99

10+
import global from '@ckeditor/ckeditor5-utils/src/dom/global';
11+
1012
/**
1113
* Helps cycling over focusable views in a {@link module:ui/viewcollection~ViewCollection}
1214
* when the focus is tracked by {@link module:utils/focustracker~FocusTracker} instance.
@@ -271,5 +273,5 @@ export default class FocusCycler {
271273
// @param {module:ui/view~View} view A view to be checked.
272274
// @returns {Boolean}
273275
function isFocusable( view ) {
274-
return view.focus;
276+
return !!( view.focus && global.window.getComputedStyle( view.element ).display != 'none' );
275277
}

tests/button/buttonview.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe( 'ButtonView', () => {
2525
it( 'is set initially', () => {
2626
expect( view.element.classList ).to.have.length( 3 );
2727
expect( view.element.classList.contains( 'ck-button' ) ).to.true;
28+
expect( view.element.classList.contains( 'ck-enabled' ) ).to.true;
2829
expect( view.element.classList.contains( 'ck-off' ) ).to.true;
2930
} );
3031

@@ -44,6 +45,14 @@ describe( 'ButtonView', () => {
4445
expect( view.element.classList.contains( 'ck-on' ) ).to.false;
4546
} );
4647

48+
it( 'reacts on view#isVisible', () => {
49+
view.isVisible = true;
50+
expect( view.element.classList.contains( 'ck-hidden' ) ).to.be.false;
51+
52+
view.isVisible = false;
53+
expect( view.element.classList.contains( 'ck-hidden' ) ).to.be.true;
54+
} );
55+
4756
it( 'reacts on view#withText', () => {
4857
view.withText = true;
4958
expect( view.element.classList.contains( 'ck-button_with-text' ) ).to.true;

tests/focuscycler.js

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import View from '../src/view';
88
import FocusCycler from '../src/focuscycler';
99
import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
1010
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
11+
import global from '@ckeditor/ckeditor5-utils/src/dom/global';
12+
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
13+
14+
testUtils.createSinonSandbox();
1115

1216
describe( 'FocusCycler', () => {
1317
let focusables, focusTracker, cycler;
@@ -22,6 +26,8 @@ describe( 'FocusCycler', () => {
2226
focusTracker: focusTracker
2327
} );
2428

29+
testUtils.sinon.stub( global.window, 'getComputedStyle' );
30+
2531
focusables.add( nonFocusable() );
2632
focusables.add( focusable() );
2733
focusables.add( focusable() );
@@ -196,11 +202,9 @@ describe( 'FocusCycler', () => {
196202

197203
describe( 'focusFirst()', () => {
198204
it( 'focuses first focusable view', () => {
199-
const spy = sinon.spy( focusables.get( 1 ), 'focus' );
200-
201205
cycler.focusFirst();
202206

203-
sinon.assert.calledOnce( spy );
207+
sinon.assert.calledOnce( focusables.get( 1 ).focus );
204208
} );
205209

206210
it( 'does not throw when no focusable items', () => {
@@ -223,15 +227,27 @@ describe( 'FocusCycler', () => {
223227
cycler.focusFirst();
224228
} ).to.not.throw();
225229
} );
230+
231+
it( 'ignores invisible items', () => {
232+
const item = focusable();
233+
234+
focusables = new ViewCollection();
235+
focusables.add( nonFocusable() );
236+
focusables.add( focusable( true ) );
237+
focusables.add( item );
238+
239+
cycler = new FocusCycler( { focusables, focusTracker } );
240+
241+
cycler.focusFirst();
242+
sinon.assert.calledOnce( item.focus );
243+
} );
226244
} );
227245

228246
describe( 'focusLast()', () => {
229247
it( 'focuses last focusable view', () => {
230-
const spy = sinon.spy( focusables.get( 3 ), 'focus' );
231-
232248
cycler.focusLast();
233249

234-
sinon.assert.calledOnce( spy );
250+
sinon.assert.calledOnce( focusables.get( 3 ).focus );
235251
} );
236252

237253
it( 'does not throw when no focusable items', () => {
@@ -258,12 +274,10 @@ describe( 'FocusCycler', () => {
258274

259275
describe( 'focusNext()', () => {
260276
it( 'focuses next focusable view', () => {
261-
const spy = sinon.spy( focusables.get( 3 ), 'focus' );
262-
263277
focusTracker.focusedElement = focusables.get( 2 ).element;
264278
cycler.focusNext();
265279

266-
sinon.assert.calledOnce( spy );
280+
sinon.assert.calledOnce( focusables.get( 3 ).focus );
267281
} );
268282

269283
it( 'does not throw when no focusable items', () => {
@@ -290,12 +304,10 @@ describe( 'FocusCycler', () => {
290304

291305
describe( 'focusPrevious()', () => {
292306
it( 'focuses previous focusable view', () => {
293-
const spy = sinon.spy( focusables.get( 3 ), 'focus' );
294-
295307
focusTracker.focusedElement = focusables.get( 1 ).element;
296308
cycler.focusPrevious();
297309

298-
sinon.assert.calledOnce( spy );
310+
sinon.assert.calledOnce( focusables.get( 3 ).focus );
299311
} );
300312

301313
it( 'does not throw when no focusable items', () => {
@@ -385,15 +397,23 @@ describe( 'FocusCycler', () => {
385397
} );
386398
} );
387399

388-
function focusable() {
400+
function nonFocusable( isHidden ) {
389401
const view = new View();
402+
view.element = Math.random();
390403

391-
view.focus = () => {};
392-
view.element = {};
404+
global.window.getComputedStyle
405+
.withArgs( view.element )
406+
.returns( {
407+
display: isHidden ? 'none' : 'block'
408+
} );
393409

394410
return view;
395411
}
396412

397-
function nonFocusable() {
398-
return new View();
413+
function focusable( isHidden ) {
414+
const view = nonFocusable( isHidden );
415+
416+
view.focus = sinon.spy();
417+
418+
return view;
399419
}

theme/helpers/_states.scss

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,7 @@
1414
* A class which hides an element in DOM.
1515
*/
1616
.ck-hidden {
17-
display: none;
17+
// Override selector specificity. Otherwise, all elements with some display
18+
// style defined will override this one, which is not a desired result.
19+
display: none !important;
1820
}

0 commit comments

Comments
 (0)