Popover: support content reading on opening - #34311
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves accessibility behavior for Popover-family overlays by ensuring assistive technologies can consistently read popover content via aria-describedby when appropriate, while keeping role/aria-labelledby behavior correct across different interaction modes (tooltip-like vs dialog-like).
Changes:
- Add Popover-side syncing of overlay content
idand targetaria-describedby, including cleanup on option changes and dispose. - Refine
role/aria-labelledbybehavior for Popover/Tooltip/Popup (e.g., dialog-mode vs tooltip-mode, and titleTemplate edge cases). - Extend unit (QUnit) and e2e a11y coverage for the new role/label/description behaviors across Popover, Tooltip, Popup, ActionSheet, and Lookup.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/devextreme/testing/tests/DevExpress.ui.widgets/tooltip.tests.js | Adds a11y regression tests for Tooltip role behavior with title/close button and toolbar items. |
| packages/devextreme/testing/tests/DevExpress.ui.widgets/popup.tests.js | Adds tests ensuring aria-labelledby isn’t set when the title label element is missing due to a custom titleTemplate. |
| packages/devextreme/testing/tests/DevExpress.ui.widgets/popover.tests.js | Adds comprehensive tests for target aria-describedby wiring, stability, cleanup, and role/label interactions. |
| packages/devextreme/testing/tests/DevExpress.ui.widgets/actionSheet.tests.js | Verifies ActionSheet popover mode does not add aria-describedby to its target. |
| packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/lookup.tests.js | Ensures Lookup popover mode stays dialog-role and does not describe the input element. |
| packages/devextreme/js/__internal/ui/tooltip.ts | Adjusts Tooltip to preserve legacy role rules and avoid Popover aria-describedby syncing (but needs a fix to keep role syncing). |
| packages/devextreme/js/__internal/ui/popup/popup.ts | Prevents dangling aria-labelledby when a custom titleTemplate doesn’t render a label element; resync on titleTemplate changes. |
| packages/devextreme/js/__internal/ui/popover/popover.ts | Implements aria role computation/sync, stable overlay content id generation, and target aria-describedby sync/cleanup. |
| packages/devextreme/js/__internal/ui/lookup.ts | Switches Lookup popover role forcing to internal _overlayContentRole instead of direct DOM mutation. |
| packages/devextreme/js/__internal/ui/action_sheet.ts | Switches ActionSheet popover role forcing to internal _overlayContentRole instead of direct DOM mutation. |
| packages/devextreme/js/__internal/scheduler/header/calendar.ts | Disables target description (_describeTarget: false) for an interactive calendar overlay to avoid noisy descriptions. |
| e2e/testcafe-devextreme/tests/accessibility/popover.ts | Updates option matrix and a11y configuration/selector to cover new behaviors for visible and hidden popovers. |
| // NOTE: Tooltip manages the target relationship itself (_toggleAriaAttributes), | ||
| // so the inherited Popover sync must stay disabled to avoid a second describedby id. | ||
| _syncAriaAttributes(): void {} |
| } else if (previousElements.has(element)) { | ||
| // NOTE: A pre-existing token this instance did not add is foreign, | ||
| // so it is never claimed and cleanup cannot strip a manual description. | ||
| describedElements.push(element); | ||
| } |
| // NOTE: dialog-mode popovers (toolbarItems or showTitle + showCloseButton) have no | ||
| // accessible name unless a title is set. Providing a default dialog name is a separate | ||
| // dialog-labeling task (see dialog-labeling-known-issue.md), so the best-practice rule | ||
| // is disabled for the combinations where the name is intentionally absent. | ||
| // Re-enable this rule once that task lands. |
869b7fe to
5ccace2
Compare
| return isDialog ? 'dialog' : 'tooltip'; | ||
| } | ||
|
|
||
| _getEffectiveAriaRole(): string { |
There was a problem hiding this comment.
Should it be on the parent level? Popup or overlay?
There was a problem hiding this comment.
we remane overlayContentRole to popoverContent role and save this method here
| // technologies, so the title labels the overlay only in dialog mode. | ||
| _toggleAriaLabel(): void { | ||
| if (this._getEffectiveAriaRole() === 'tooltip') { | ||
| this.$overlayContent().attr('aria-labelledby', null); |
There was a problem hiding this comment.
We need to use setAria() method
| _toggleAriaLabel(): void { | ||
| const { title, showTitle } = this.option(); | ||
| const shouldSetAriaLabel = showTitle && Boolean(title); | ||
| const $label = this._$topToolbar?.find(`.${TOOLBAR_LABEL_CLASS}`).eq(0); |
There was a problem hiding this comment.
Why we need to search in DOM? Toolbar should provide its own method like getLabel()
There was a problem hiding this comment.
| const titleId = shouldSetAriaLabel ? new Guid().toString() : null; | ||
|
|
||
| this._$topToolbar?.find(`.${TOOLBAR_LABEL_CLASS}`).eq(0).attr('id', titleId); | ||
| $label?.attr('id', titleId); |
There was a problem hiding this comment.
Why we mutate DOM through private API in other component?
There was a problem hiding this comment.
| id: this._contentId, | ||
| }); | ||
| // NOTE: dxTooltip keeps its legacy role behavior: only toolbarItems make it | ||
| // a dialog; the Popover showTitle/showCloseButton predicate does not apply. |
There was a problem hiding this comment.
Do we really need comments here?
How about set protected modifier so it would clear we overload this method in other places?
There was a problem hiding this comment.
set protected and remove comment
|
|
||
| _getEffectiveAriaRole(): string { | ||
| // eslint-disable-next-line @typescript-eslint/naming-convention | ||
| const { _overlayContentRole } = this.option(); |
There was a problem hiding this comment.
Or we need to rename the property?
There was a problem hiding this comment.
we have to remane to _popoverContentRole
|
|
||
| _ensurePopoverContentId(): string { | ||
| const $overlayContent = this.$overlayContent(); | ||
| const existingId = $overlayContent.attr('id'); |
There was a problem hiding this comment.
We should not rely buisness logic to DOM
There was a problem hiding this comment.
we use lazyInit here instead of DOM manipulation
| } | ||
|
|
||
| _getActualAriaRole(): string | undefined { | ||
| return this.$overlayContent().attr('role') ?? undefined; |
There was a problem hiding this comment.
Why we do not use private _overlayContentRole here and we have to override the logig?
There was a problem hiding this comment.
I assume the original idea was to have a sort of safeguard. If the user set a custom role via elementAttr, we would check for its existence here and avoid overwriting it. However, this only worked with the _lastAppliedRole variable. Now we remove it
| const { target } = this.option(); | ||
| const elements: Element[] = []; | ||
|
|
||
| $(target).each((_, node) => { |
There was a problem hiding this comment.
we have to update it, check only one target
| return Boolean(target) && Boolean(_describeTarget) && this._getEffectiveAriaRole() === 'tooltip'; | ||
| } | ||
|
|
||
| _getAriaDescriptionTargets(): dxElementWrapper { |
There was a problem hiding this comment.
We have to reuse getTargetElement logic from here.
There was a problem hiding this comment.
let's create getTargetElement
|
|
||
| const id = this._ensurePopoverContentId(); | ||
|
|
||
| if (this._ariaDescriptionId && this._ariaDescriptionId !== id) { |
There was a problem hiding this comment.
analiz and refactor _syncAriaAttributes
| this.option('fullScreen', false); | ||
| } | ||
| break; | ||
| case 'target': |
There was a problem hiding this comment.
Should we sync aria attributes when target changed?
No description provided.