Skip to content

Commit 4b4ca89

Browse files
Ruslan FarkhutdinovRuslan Farkhutdinov
andauthored
Popover: Support WCAG - Dismissible (#32887)
Co-authored-by: Ruslan Farkhutdinov <ruslan.farkhutdinov@devexpress.com>
1 parent edb1a98 commit 4b4ca89

3 files changed

Lines changed: 124 additions & 4 deletions

File tree

‎packages/devextreme/js/__internal/ui/popover/m_popover.ts‎

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import positionUtils from '@js/common/core/animation/position';
22
import { move } from '@js/common/core/animation/translator';
33
import eventsEngine from '@js/common/core/events/core/events_engine';
4-
import { addNamespace } from '@js/common/core/events/utils';
4+
import { addNamespace, normalizeKeyName } from '@js/common/core/events/utils';
55
import registerComponent from '@js/core/component_registrator';
66
import domAdapter from '@js/core/dom_adapter';
77
import { getPublicElement } from '@js/core/element';
@@ -47,6 +47,8 @@ const POSITION_FLIP_MAP = {
4747
center: 'center',
4848
};
4949

50+
const ESC_KEY_NAME = 'escape';
51+
5052
type PopoverTarget = string | dxElementWrapper | Element | undefined;
5153

5254
export interface PopoverProperties extends Omit<Properties,
@@ -70,6 +72,8 @@ class Popover<
7072

7173
_$arrow!: dxElementWrapper;
7274

75+
_documentEscapeKeyHandler!: (e: KeyboardEvent) => void;
76+
7377
_timeouts!: Record<string, ReturnType<typeof setTimeout>>;
7478

7579
_getDefaultOptions(): TProperties {
@@ -153,15 +157,46 @@ class Popover<
153157
super._init();
154158

155159
this._renderArrow();
160+
this._initEscapeKeyHandler();
156161
this._timeouts = {};
157162

158163
this.$element().addClass(POPOVER_CLASS);
159164
this.$wrapper()?.addClass(POPOVER_WRAPPER_CLASS);
160165

161-
const { toolbarItems } = this.option();
166+
const { toolbarItems, visible } = this.option();
162167

163168
const isInteractive = toolbarItems?.length;
164169
this.setAria('role', isInteractive ? 'dialog' : 'tooltip');
170+
171+
if (visible) {
172+
this._attachEscapeKeyHandler();
173+
}
174+
}
175+
176+
_initEscapeKeyHandler(): void {
177+
this._documentEscapeKeyHandler = (e: KeyboardEvent): void => {
178+
const { visible } = this.option();
179+
const overlayStack = this._overlayStack();
180+
const isTopOverlay = overlayStack[overlayStack.length - 1] === this;
181+
182+
if (normalizeKeyName(e) === ESC_KEY_NAME && visible && isTopOverlay) {
183+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
184+
this.hide();
185+
}
186+
};
187+
}
188+
189+
_attachEscapeKeyHandler(): void {
190+
const eventName = addNamespace('keydown', this.NAME as string);
191+
192+
eventsEngine.off(domAdapter.getDocument(), eventName, this._documentEscapeKeyHandler);
193+
eventsEngine.on(domAdapter.getDocument(), eventName, this._documentEscapeKeyHandler);
194+
}
195+
196+
_detachEscapeKeyHandler(): void {
197+
const eventName = addNamespace('keydown', this.NAME as string);
198+
199+
eventsEngine.off(domAdapter.getDocument(), eventName, this._documentEscapeKeyHandler);
165200
}
166201

167202
_render(): void {
@@ -529,11 +564,17 @@ class Popover<
529564
}
530565

531566
_clean(): void {
567+
this._detachEscapeKeyHandler();
532568
this._detachEvents(this.option('target'));
533569
// @ts-expect-error ts-error
534570
super._clean.apply(this, arguments);
535571
}
536572

573+
_dispose(): void {
574+
this._detachEscapeKeyHandler();
575+
super._dispose();
576+
}
577+
537578
_optionChanged(args: OptionChanged<TProperties>): void {
538579
const { name, value, previousValue } = args;
539580
switch (name) {
@@ -565,6 +606,11 @@ class Popover<
565606
break;
566607
}
567608
case 'visible':
609+
if (value) {
610+
this._attachEscapeKeyHandler();
611+
} else {
612+
this._detachEscapeKeyHandler();
613+
}
568614
this._clearEventTimeout(value ? 'show' : 'hide');
569615
super._optionChanged(args);
570616
break;

‎packages/devextreme/testing/tests/DevExpress.ui.widgets/popover.tests.js‎

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import $ from 'jquery';
33
import fixtures from '../../helpers/positionFixtures.js';
44
import fx from 'common/core/animation/fx';
55
import pointerMock from '../../helpers/pointerMock.js';
6+
import keyboardMock from '../../helpers/keyboardMock.js';
67
import positionUtils from 'common/core/animation/position';
78
import uiErrors from 'ui/widget/ui.errors';
89
import Popover from 'ui/popover';
@@ -2320,7 +2321,7 @@ QUnit.module('disabled option', {
23202321
});
23212322
});
23222323

2323-
QUnit.module('aria accessibility', {
2324+
QUnit.module('accessibility', {
23242325
beforeEach: function() {
23252326
fixtures.simple.create();
23262327
},
@@ -2343,4 +2344,63 @@ QUnit.module('aria accessibility', {
23432344

23442345
assert.strictEqual($overlay.attr('role'), 'dialog');
23452346
});
2347+
2348+
QUnit.test('should hide visible popover on esc press', function(assert) {
2349+
const popover = new Popover($('#what'), {
2350+
target: '#where',
2351+
visible: true,
2352+
});
2353+
const $target = $('#where').attr('tabindex', 0);
2354+
const keyboard = keyboardMock($target);
2355+
2356+
keyboard.keyDown('esc');
2357+
2358+
assert.strictEqual(popover.option('visible'), false, 'popover is hidden');
2359+
});
2360+
2361+
QUnit.test('should hide only topmost popover on esc press', function(assert) {
2362+
const $markup = $('<div id="popover1"></div>' +
2363+
'<div id="popover2"></div>' +
2364+
'<div id="target1" tabindex="0"></div>' +
2365+
'<div id="target2" tabindex="0"></div>')
2366+
.appendTo('body');
2367+
2368+
2369+
const bottomPopover = new Popover($('#popover1'), {
2370+
target: '#target1',
2371+
visible: true,
2372+
});
2373+
const topPopover = new Popover($('#popover2'), {
2374+
target: '#target2',
2375+
visible: true,
2376+
});
2377+
2378+
const keyboard = keyboardMock($('#target2'));
2379+
2380+
keyboard.keyDown('esc');
2381+
2382+
assert.strictEqual(topPopover.option('visible'), false, 'top popover is hidden');
2383+
assert.strictEqual(bottomPopover.option('visible'), true, 'bottom popover is still visible');
2384+
2385+
$markup.remove();
2386+
});
2387+
2388+
QUnit.test('should not call hide for hidden popover on esc press', function(assert) {
2389+
const popover = new Popover($('#what'), {
2390+
target: '#where',
2391+
visible: true,
2392+
animation: null,
2393+
});
2394+
const hideSpy = sinon.spy(popover, 'hide');
2395+
const $target = $('#where').attr('tabindex', 0);
2396+
const keyboard = keyboardMock($target);
2397+
2398+
popover.hide();
2399+
hideSpy.resetHistory();
2400+
2401+
keyboard.keyDown('esc');
2402+
2403+
assert.strictEqual(hideSpy.callCount, 0, 'hide is not called');
2404+
assert.strictEqual(popover.option('visible'), false, 'popover remains hidden');
2405+
});
23462406
});

‎packages/devextreme/testing/tests/DevExpress.ui.widgets/tooltip.tests.js‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fx from 'common/core/animation/fx';
33
import Tooltip from 'ui/tooltip';
44
import renderer from 'core/renderer';
55
import uiErrors from 'ui/widget/ui.errors';
6+
import keyboardMock from '../../helpers/keyboardMock.js';
67

78
const TOOLTIP_CLASS = 'dx-tooltip';
89
const TOOLTIP_WRAPPER_CLASS = 'dx-tooltip-wrapper';
@@ -162,7 +163,7 @@ QUnit.module('base z-index', () => {
162163
});
163164
});
164165

165-
QUnit.module('aria accessibility', () => {
166+
QUnit.module('accessibility', () => {
166167
QUnit.test('role="tooltip" attribute should be added to tooltip', function(assert) {
167168
const $tooltip = $('#tooltip');
168169
new Tooltip($tooltip);
@@ -181,4 +182,17 @@ QUnit.module('aria accessibility', () => {
181182
assert.equal($target.attr('aria-describedby'), $overlay.attr('id'), 'aria-describedby and overlay\'s id are equal');
182183

183184
});
185+
186+
QUnit.test('visible tooltip should be hidden on Escape key press', function(assert) {
187+
const tooltip = new Tooltip($('#tooltip'), {
188+
target: '#target',
189+
visible: true,
190+
});
191+
const $target = $('#target').attr('tabindex', 0);
192+
const keyboard = keyboardMock($target);
193+
194+
keyboard.keyDown('esc');
195+
196+
assert.strictEqual(tooltip.option('visible'), false, 'tooltip is hidden');
197+
});
184198
});

0 commit comments

Comments
 (0)