Skip to content

Commit b9b0614

Browse files
r-farkhutdinovRuslan Farkhutdinov
andauthored
Overlay: Should focus next focusable element if target is deleted (T1248343) (DevExpress#29343)
Co-authored-by: Ruslan Farkhutdinov <[email protected]>
1 parent 5c7b5cb commit b9b0614

File tree

2 files changed

+71
-19
lines changed

2 files changed

+71
-19
lines changed

packages/devextreme/js/__internal/ui/overlay/m_overlay.ts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -695,42 +695,43 @@ const Overlay: typeof OverlayInstance = Widget.inherit({
695695
_findTabbableBounds() {
696696
const $elements = this._$wrapper.find('*');
697697
const elementsCount = $elements.length - 1;
698-
const result = { first: null, last: null };
699698

700-
for (let i = 0; i <= elementsCount; i++) {
701-
if (!result.first && $elements.eq(i).is(tabbable)) {
702-
result.first = $elements.eq(i);
703-
}
699+
let first = null;
700+
let last = null;
704701

705-
if (!result.last && $elements.eq(elementsCount - i).is(tabbable)) {
706-
result.last = $elements.eq(elementsCount - i);
702+
for (let i = 0; i <= elementsCount; i += 1) {
703+
if (!first && $elements.eq(i).is(tabbable)) {
704+
first = $elements.eq(i);
705+
}
706+
if (!last && $elements.eq(elementsCount - i).is(tabbable)) {
707+
last = $elements.eq(elementsCount - i);
707708
}
708709

709-
if (result.first && result.last) {
710+
if (first && last) {
710711
break;
711712
}
712713
}
713714

714-
return result;
715+
return { first, last };
715716
},
716717

717-
_tabKeyHandler(e) {
718+
_tabKeyHandler(e: KeyboardEvent): void {
718719
if (normalizeKeyName(e) !== TAB_KEY || !this._isTopOverlay()) {
719720
return;
720721
}
721722

722-
const tabbableElements = this._findTabbableBounds();
723+
const wrapper = this._$wrapper.get(0) as HTMLElement;
724+
const activeElement = domAdapter.getActiveElement(wrapper);
725+
726+
const { first: $firstTabbable, last: $lastTabbable } = this._findTabbableBounds();
723727

724-
const $firstTabbable = tabbableElements.first;
725-
const $lastTabbable = tabbableElements.last;
728+
const isTabOnLast = !e.shiftKey && activeElement === $lastTabbable?.get(0);
729+
const isShiftTabOnFirst = e.shiftKey && activeElement === $firstTabbable?.get(0);
730+
const isOutsideTarget = !wrapper.contains(activeElement);
726731

727-
const isTabOnLast = !e.shiftKey && e.target === $lastTabbable.get(0);
728-
const isShiftTabOnFirst = e.shiftKey && e.target === $firstTabbable.get(0);
729-
const isEmptyTabList = tabbableElements.length === 0;
730-
const isOutsideTarget = !contains(this._$wrapper.get(0), e.target);
732+
const shouldPreventDefault = isTabOnLast || isShiftTabOnFirst || isOutsideTarget;
731733

732-
if (isTabOnLast || isShiftTabOnFirst
733-
|| isEmptyTabList || isOutsideTarget) {
734+
if (shouldPreventDefault) {
734735
e.preventDefault();
735736

736737
const $focusElement = e.shiftKey ? $lastTabbable : $firstTabbable;

packages/devextreme/testing/tests/DevExpress.ui.widgets/overlay.tests.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3391,6 +3391,57 @@ testModule('focus policy', {
33913391

33923392
assert.strictEqual(contentFocusHandler.callCount, 1, 'focus has been triggered once from keyboardMock');
33933393
});
3394+
3395+
test('tab behavior should remain default when event.target is removed from DOM (T1248343)', function(assert) {
3396+
const overlay = new Overlay($('<div>').appendTo('#qunit-fixture'), {
3397+
visible: true,
3398+
shading: true,
3399+
contentTemplate: $('<div>')
3400+
.append('<input class="templateField">')
3401+
.append('<input class="anotherField">')
3402+
.append('<input class="thirdField">'),
3403+
});
3404+
3405+
const $content = $(overlay.$content());
3406+
const $templateField = $content.find('.templateField');
3407+
const $anotherField = $content.find('.anotherField');
3408+
3409+
$templateField.trigger('focus');
3410+
3411+
assert.strictEqual(getActiveElement(), $templateField.get(0), 'templatedField is focused');
3412+
3413+
$templateField.remove();
3414+
3415+
$anotherField.trigger('focus');
3416+
3417+
const tabEvent = $.Event('keydown', { key: 'Tab', shiftKey: false, target: $templateField.get(0) });
3418+
3419+
$(document).trigger(tabEvent);
3420+
assert.strictEqual(tabEvent.isDefaultPrevented(), false, 'event is not prevented');
3421+
});
3422+
3423+
test('tab event is prevented when removed target was outside overlay', function(assert) {
3424+
const overlay = new Overlay($('<div>').appendTo('#qunit-fixture'), {
3425+
visible: true,
3426+
shading: true,
3427+
contentTemplate: $('<div>')
3428+
.append('<input class="templateField">')
3429+
});
3430+
3431+
const $content = $(overlay.$content());
3432+
const $templateField = $content.find('.templateField');
3433+
const $outerField = $('<input class="outerField">').appendTo('#qunit-fixture');
3434+
$outerField.trigger('focus');
3435+
assert.strictEqual(getActiveElement(), $outerField.get(0), 'outerField is focused');
3436+
3437+
$outerField.remove();
3438+
3439+
const tabEventOutside = $.Event('keydown', { key: 'Tab', shiftKey: false, target: $outerField.get(0) });
3440+
$(document).trigger(tabEventOutside);
3441+
3442+
assert.strictEqual(tabEventOutside.isDefaultPrevented(), true, 'event is prevented when target removed outside overlay');
3443+
assert.strictEqual(getActiveElement(), $templateField.get(0), 'focus is returned to Popup');
3444+
});
33943445
});
33953446

33963447
testModule('preventScrollEvents', () => {

0 commit comments

Comments
 (0)