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

Commit f9e3bb7

Browse files
authored
Fix: BalloonPanelView should not be focusable. Closes #206.
T/206a: BalloonPanelView should not be focusable
2 parents b8fbaf1 + 270a1a4 commit f9e3bb7

File tree

8 files changed

+134
-14
lines changed

8 files changed

+134
-14
lines changed

src/bindings/preventdefault.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
/**
7+
* @module ui/bindings/preventdefault
8+
*/
9+
10+
/**
11+
* Returns a {module:ui/template~TemplateToBinding} resulting in a native `event#preventDefault`
12+
* for the DOM event if `event#target` equals {@link module:ui/view~View#element}.
13+
*
14+
* @param {module:ui/view~View} view View instance that uses the template.
15+
* @returns {module:ui/template~TemplateToBinding}
16+
*/
17+
export default function preventDefault( view ) {
18+
return view.bindTemplate.to( evt => {
19+
if ( evt.target === view.element ) {
20+
evt.preventDefault();
21+
}
22+
} );
23+
}

src/focuscycler.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,13 +241,19 @@ export default class FocusCycler {
241241
*/
242242
_getFocusableItem( step ) {
243243
// Cache for speed.
244-
const current = this.current;
244+
let current = this.current;
245245
const collectionLength = this.focusables.length;
246246

247-
if ( !collectionLength || current === null ) {
247+
if ( !collectionLength ) {
248248
return null;
249249
}
250250

251+
// Start from the beginning if no view is focused.
252+
// https://github.com/ckeditor/ckeditor5-ui/issues/206
253+
if ( current === null ) {
254+
return this[ step === 1 ? 'first' : 'last' ];
255+
}
256+
251257
// Cycle in both directions.
252258
let index = ( current + collectionLength + step ) % collectionLength;
253259

src/panel/balloon/balloonpanelview.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
1313
import isRange from '@ckeditor/ckeditor5-utils/src/dom/isrange';
1414
import isElement from '@ckeditor/ckeditor5-utils/src/lib/lodash/isElement';
1515
import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
16+
import preventDefault from '../../bindings/preventdefault.js';
1617
import global from '@ckeditor/ckeditor5-utils/src/dom/global';
1718

1819
const toPx = toUnit( 'px' );
@@ -134,13 +135,15 @@ export default class BalloonPanelView extends View {
134135
top: bind.to( 'top', toPx ),
135136
left: bind.to( 'left', toPx ),
136137
maxWidth: bind.to( 'maxWidth', toPx )
137-
},
138-
139-
// Make this element `focusable` to be available for adding to FocusTracker.
140-
tabindex: -1
138+
}
141139
},
142140

143-
children: this.content
141+
children: this.content,
142+
143+
on: {
144+
// https://github.com/ckeditor/ckeditor5-ui/issues/206
145+
mousedown: preventDefault( this )
146+
}
144147
} );
145148
}
146149

src/toolbar/toolbarview.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
1313
import FocusCycler from '../focuscycler';
1414
import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
1515
import ToolbarSeparatorView from './toolbarseparatorview';
16+
import preventDefault from '../bindings/preventdefault.js';
1617

1718
/**
1819
* The toolbar view class.
@@ -78,7 +79,12 @@ export default class ToolbarView extends View {
7879
]
7980
},
8081

81-
children: this.items
82+
children: this.items,
83+
84+
on: {
85+
// https://github.com/ckeditor/ckeditor5-ui/issues/206
86+
mousedown: preventDefault( this )
87+
}
8288
} );
8389

8490
this.items.on( 'add', ( evt, item ) => {

tests/bindings/preventdefault.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
/* global Event */
7+
8+
import preventDefault from '../../src/bindings/preventdefault';
9+
import View from '../../src/view';
10+
import Template from '../../src/template';
11+
12+
describe( 'preventDefault', () => {
13+
it( 'prevents default of a native DOM event', () => {
14+
const view = new View();
15+
16+
view.template = new Template( {
17+
tag: 'div',
18+
19+
on: {
20+
foo: preventDefault( view )
21+
}
22+
} );
23+
24+
const evt = new Event( 'foo', { bubbles: true } );
25+
const spy = sinon.spy( evt, 'preventDefault' );
26+
27+
// Render to enable bubbling.
28+
view.element;
29+
30+
view.element.dispatchEvent( evt );
31+
sinon.assert.calledOnce( spy );
32+
} );
33+
34+
it( 'prevents only when target is view#element', () => {
35+
const view = new View();
36+
const child = new View();
37+
38+
child.template = new Template( {
39+
tag: 'a'
40+
} );
41+
42+
view.template = new Template( {
43+
tag: 'div',
44+
45+
on: {
46+
foo: preventDefault( view )
47+
},
48+
49+
children: [
50+
child
51+
]
52+
} );
53+
54+
const evt = new Event( 'foo', { bubbles: true } );
55+
const spy = sinon.spy( evt, 'preventDefault' );
56+
57+
// Render to enable bubbling.
58+
view.element;
59+
60+
child.element.dispatchEvent( evt );
61+
sinon.assert.notCalled( spy );
62+
} );
63+
} );

tests/focuscycler.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,10 @@ describe( 'FocusCycler', () => {
112112
expect( cycler.next ).to.equal( focusables.get( 2 ) );
113113
} );
114114

115-
it( 'returns null when no view is focused', () => {
115+
it( 'focuses the first focusable view when no view is focused', () => {
116116
focusTracker.focusedElement = null;
117117

118-
expect( cycler.next ).to.be.null;
118+
expect( cycler.next ).to.equal( focusables.get( 1 ) );
119119
} );
120120

121121
it( 'returns null when no items', () => {
@@ -162,10 +162,10 @@ describe( 'FocusCycler', () => {
162162
expect( cycler.previous ).to.equal( focusables.get( 2 ) );
163163
} );
164164

165-
it( 'returns null when no view is focused', () => {
165+
it( 'focuses the last focusable view when no view is focused', () => {
166166
focusTracker.focusedElement = null;
167167

168-
expect( cycler.previous ).to.be.null;
168+
expect( cycler.previous ).to.equal( focusables.get( 3 ) );
169169
} );
170170

171171
it( 'returns null when no items', () => {

tests/panel/balloon/balloonpanelview.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ describe( 'BalloonPanelView', () => {
3535
it( 'should create element from template', () => {
3636
expect( view.element.tagName ).to.equal( 'DIV' );
3737
expect( view.element.classList.contains( 'ck-balloon-panel' ) ).to.true;
38-
expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' );
3938
} );
4039

4140
it( 'should set default values', () => {
@@ -129,6 +128,16 @@ describe( 'BalloonPanelView', () => {
129128
} );
130129
} );
131130
} );
131+
132+
describe( 'event listeners', () => {
133+
it( 'prevent default on #mousedown', () => {
134+
const evt = new Event( 'mousedown', { bubbles: true } );
135+
const spy = sinon.spy( evt, 'preventDefault' );
136+
137+
view.element.dispatchEvent( evt );
138+
sinon.assert.calledOnce( spy );
139+
} );
140+
} );
132141
} );
133142

134143
describe( 'show()', () => {

tests/toolbar/toolbarview.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* For licensing, see LICENSE.md.
44
*/
55

6-
/* global document */
6+
/* global document, Event */
77

88
import ToolbarView from '../../src/toolbar/toolbarview';
99
import ToolbarSeparatorView from '../../src/toolbar/toolbarseparatorview';
@@ -64,6 +64,16 @@ describe( 'ToolbarView', () => {
6464
it( 'should create element from template', () => {
6565
expect( view.element.classList.contains( 'ck-toolbar' ) ).to.true;
6666
} );
67+
68+
describe( 'event listeners', () => {
69+
it( 'prevent default on #mousedown', () => {
70+
const evt = new Event( 'mousedown', { bubbles: true } );
71+
const spy = sinon.spy( evt, 'preventDefault' );
72+
73+
view.element.dispatchEvent( evt );
74+
sinon.assert.calledOnce( spy );
75+
} );
76+
} );
6777
} );
6878

6979
describe( 'init()', () => {

0 commit comments

Comments
 (0)