Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ const CLASSES = {
popupWrapper: 'dx-popup-wrapper',
popupTitle: 'dx-popup-title',
overlayContent: 'dx-overlay-content',
closeButton: 'dx-closebutton',
toolbar: 'dx-toolbar',
button: 'dx-button',
};

const SELECTORS = {
Expand Down Expand Up @@ -37,6 +40,15 @@ export class PopupModel {
return this.getOverlayContent()?.getAttribute('role') ?? null;
}

public getCloseButton(): HTMLElement | null {
return this.getOverlayContent()?.querySelector(`.${CLASSES.closeButton}`) ?? null;
}

public getToolbarButtons(): HTMLElement[] {
const buttons = this.getOverlayContent()?.querySelectorAll<HTMLElement>(`.${CLASSES.toolbar} .${CLASSES.button}`);
return Array.from(buttons ?? []);
}

public isVisible(): boolean {
return !!this.getOverlayContent();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
afterEach, beforeAll, describe, expect, it,
afterAll, afterEach, beforeAll, describe, expect, it,
} from '@jest/globals';
import fx from '@js/common/core/animation/fx';
import $ from '@js/core/renderer';
Expand Down Expand Up @@ -56,4 +56,27 @@ describe('Tooltip overlay content aria role', () => {
expect(model.getRole()).toBe(role);
});
});

describe('on a shown tooltip with focusable toolbar elements', () => {
const originalGetClientRects = Element.prototype.getClientRects;

beforeAll(() => {
// jsdom performs no layout, so without this stub nothing counts as visible
// and the toolbar buttons are never tabbable.
Element.prototype.getClientRects = () => [{}] as unknown as DOMRectList;
});

afterAll(() => {
Element.prototype.getClientRects = originalGetClientRects;
});

it('sets the role on the overlay content, not on the toolbar button, when toolbar items with a button are added', async () => {
const model = await createTooltip({});

model.getInstance().option('toolbarItems', [{ widget: 'dxButton', options: { text: 'OK' } }]);

expect(model.getRole()).toBe('dialog');
expect(model.getToolbarButtons().map((button) => button.getAttribute('role'))).toEqual(['button']);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
afterEach, beforeAll, describe, expect, it,
afterAll, afterEach, beforeAll, describe, expect, it,
} from '@jest/globals';
import fx from '@js/common/core/animation/fx';
import $ from '@js/core/renderer';
Expand Down Expand Up @@ -86,4 +86,72 @@ describe('Popover overlay content aria role', () => {
expect(model.getRole()).toBe('tooltip');
});
});

describe('on a shown popover with focusable toolbar elements', () => {
const originalGetClientRects = Element.prototype.getClientRects;

beforeAll(() => {
// jsdom performs no layout, so without this stub nothing counts as visible
// and the toolbar buttons are never tabbable.
Element.prototype.getClientRects = () => [{}] as unknown as DOMRectList;
});

afterAll(() => {
Element.prototype.getClientRects = originalGetClientRects;
});

it('sets the role on the overlay content, not on the close button, when a title and a close button are enabled', async () => {
const model = await createPopover({});

model.getInstance().option({ showTitle: true, title: 'Title', showCloseButton: true });

expect(model.getRole()).toBe('dialog');
expect(model.getCloseButton()?.getAttribute('role')).toBe('button');
});

it('switches back to "tooltip" when the close button is hidden again', async () => {
const model = await createPopover({});
model.getInstance().option({ showTitle: true, title: 'Title', showCloseButton: true });

model.getInstance().option('showCloseButton', false);

expect(model.getRole()).toBe('tooltip');
expect(model.getCloseButton()).toBeNull();
});

it('sets the role on the overlay content, not on the toolbar button, when toolbar items with a button are added', async () => {
const model = await createPopover({});

model.getInstance().option('toolbarItems', [{ widget: 'dxButton', options: { text: 'OK' } }]);

expect(model.getRole()).toBe('dialog');
expect(model.getToolbarButtons().map((button) => button.getAttribute('role'))).toEqual(['button']);
});

it('applies a forced _popoverContentRole to the overlay content, not to the close button', async () => {
const model = await createPopover({ showTitle: true, title: 'Title', showCloseButton: true });

model.getInstance().option('_popoverContentRole', 'tooltip');

expect(model.getRole()).toBe('tooltip');
expect(model.getCloseButton()?.getAttribute('role')).toBe('button');

model.getInstance().option('_popoverContentRole', undefined);

expect(model.getRole()).toBe('dialog');
expect(model.getCloseButton()?.getAttribute('role')).toBe('button');
});

it('toggles aria-disabled on the overlay content when disabled changes', async () => {
const model = await createPopover({ showTitle: true, title: 'Title', showCloseButton: true });

model.getInstance().option('disabled', true);

expect(model.getOverlayContent().getAttribute('aria-disabled')).toBe('true');

model.getInstance().option('disabled', false);

expect(model.getOverlayContent().getAttribute('aria-disabled')).toBeNull();
});
});
});
4 changes: 4 additions & 0 deletions packages/devextreme/js/__internal/ui/popup/popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,10 @@ class Popup<
});
}

_getAriaTarget(): dxElementWrapper {
return this.$overlayContent();
}

protected _getAriaRole(): string {
return 'dialog';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const POPOVER_WITHOUT_TITLE_CLASS = 'dx-popover-without-title';
const POPOVER_TITLE_CLASS = 'dx-popup-title';
const POPUP_CONTENT_CLASS = 'dx-popup-content';
const OVERLAY_CONTENT_CLASS = 'dx-overlay-content';
const POPUP_CLOSE_BUTTON_CLASS = 'dx-closebutton';
const TOOLBAR_CLASS = 'dx-toolbar';
const BUTTON_CLASS = 'dx-button';

const positionAtWindowCenter = function(element) {
positionUtils.setup(element, {
Expand Down Expand Up @@ -3314,5 +3317,71 @@ QUnit.module('accessibility', {
assert.strictEqual(instance.option('tabFocusLoopEnabled'), false, 'tabFocusLoopEnabled remains false due to _preventDialogContainerFocus');
});
});

QUnit.module('shown popover with focusable toolbar elements', () => {
const createShownPopover = (options) => new Popover($('#what'), $.extend({
target: '#where',
visible: true,
animation: null,
}, options));

const getCloseButton = (popover) => popover.$overlayContent().find(`.${POPUP_CLOSE_BUTTON_CLASS}`);

QUnit.test('enabling showTitle and showCloseButton at runtime should set the role on the overlay content and keep the close button role', function(assert) {
const popover = createShownPopover();

popover.option({ showTitle: true, title: 'Title', showCloseButton: true });

assert.strictEqual(popover.$overlayContent().attr('role'), 'dialog', 'overlay content role is dialog');
assert.strictEqual(getCloseButton(popover).attr('role'), 'button', 'close button keeps its role');
});

QUnit.test('hiding the close button at runtime should restore role="tooltip" on the overlay content', function(assert) {
const popover = createShownPopover({ showTitle: true, title: 'Title', showCloseButton: true });

popover.option('showCloseButton', false);

assert.strictEqual(popover.$overlayContent().attr('role'), 'tooltip', 'overlay content role is tooltip');
assert.strictEqual(getCloseButton(popover).length, 0, 'close button is removed');
});

QUnit.test('adding toolbar items with a button at runtime should set the role on the overlay content and keep the button role', function(assert) {
const popover = createShownPopover();

popover.option('toolbarItems', [{ widget: 'dxButton', options: { text: 'OK' } }]);

const $button = popover.$overlayContent().find(`.${TOOLBAR_CLASS} .${BUTTON_CLASS}`);

assert.strictEqual(popover.$overlayContent().attr('role'), 'dialog', 'overlay content role is dialog');
assert.strictEqual($button.length, 1, 'toolbar button is rendered');
assert.strictEqual($button.attr('role'), 'button', 'toolbar button keeps its role');
});

QUnit.test('runtime _popoverContentRole change should apply to the overlay content and keep the close button role', function(assert) {
const popover = createShownPopover({ showTitle: true, title: 'Title', showCloseButton: true });

popover.option('_popoverContentRole', 'tooltip');

assert.strictEqual(popover.$overlayContent().attr('role'), 'tooltip', 'forced role is applied to the overlay content');
assert.strictEqual(getCloseButton(popover).attr('role'), 'button', 'close button keeps its role');

popover.option('_popoverContentRole', undefined);

assert.strictEqual(popover.$overlayContent().attr('role'), 'dialog', 'computed role is restored on the overlay content');
assert.strictEqual(getCloseButton(popover).attr('role'), 'button', 'close button still keeps its role');
});

QUnit.test('runtime disabled change should toggle aria-disabled on the overlay content', function(assert) {
const popover = createShownPopover({ showTitle: true, title: 'Title', showCloseButton: true });

popover.option('disabled', true);

assert.strictEqual(popover.$overlayContent().attr('aria-disabled'), 'true', 'overlay content is marked disabled');

popover.option('disabled', false);

assert.strictEqual(popover.$overlayContent().attr('aria-disabled'), undefined, 'overlay content is not marked disabled');
});
});
});

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const TOOLTIP_CLASS = 'dx-tooltip';
const TOOLTIP_WRAPPER_CLASS = 'dx-tooltip-wrapper';
const DX_INVISIBILITY_CLASS = 'dx-state-invisible';
const OVERLAY_CONTENT_CLASS = 'dx-overlay-content';
const TOOLBAR_CLASS = 'dx-toolbar';
const BUTTON_CLASS = 'dx-button';

const wrapper = function() {
return $('body').find('.' + TOOLTIP_WRAPPER_CLASS);
Expand Down Expand Up @@ -208,6 +210,22 @@ QUnit.module('accessibility', () => {
assert.equal($overlayContent.attr('role'), 'dialog');
});

QUnit.test('adding toolbar items with a button at runtime should set the role on the overlay content and keep the button role', function(assert) {
const tooltip = new Tooltip($('#tooltip'), {
target: '#target',
visible: true,
animation: null
});

tooltip.option('toolbarItems', [{ widget: 'dxButton', options: { text: 'OK' } }]);

const $overlayContent = tooltip.$overlayContent();
const $button = $overlayContent.find(`.${TOOLBAR_CLASS} .${BUTTON_CLASS}`);

assert.equal($overlayContent.attr('role'), 'dialog', 'overlay content role is dialog');
assert.equal($button.attr('role'), 'button', 'toolbar button keeps its role');
});

QUnit.module('WCAG - dismissible', () => {
QUnit.test('should hide visible tooltip on Escape key press', function(assert) {
const tooltip = new Tooltip($('#tooltip'), {
Expand Down
Loading