Skip to content

Interactive controls must not be nested #6933

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 2, 2025
4 changes: 3 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ copy-vrt-tests.js
creator-themes-import.js
creator-themes-update-fallback.js
figma-variables-opacity-converter.js
visualRegressionTests-V2/tests/
visualRegressionTests-V2/tests/
**/test-results/
**/playwright-report/
46 changes: 33 additions & 13 deletions accessibilityTests/designer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,52 @@ test.describe("designer a11y", () => {
await injectAxe(page);
await page.setViewportSize({ width: 1920, height: 1080 });
});

const jsonWithMultiplePages = {
"pages": [
{
"name": "page1",
"elements": [{ "type": "text", "name": "question1" }]
},
{
"name": "page2",
"elements": [{ "type": "text", "name": "question2" }]
}
],
};
test("Check designer tab empty", async ({ page }) => {
await checkA11y(page, ".svc-creator", { axeOptions });
});
test("Check designer tab with multiple pages", async ({ page }) => {
await setJSON(page, jsonWithMultiplePages);
await checkA11y(page, ".svc-creator", { axeOptions });
});
test("Check page navigator", async ({ page }) => {
await setJSON(page, jsonWithMultiplePages);
await page.click(".svc-page-navigator__selector");
await checkA11y(page, ".svc-tab-designer__page-navigator", { axeOptions });
});
test("Check sidebar header", async ({ page }) => {
await setJSON(page, jsonWithMultiplePages);
await page.click(".svc-sidebar__header-content .svc-menu-action__button");
await checkA11y(page, ".svc-sidebar__header", { axeOptions });
});
test("Check radiogroup and rating", async ({ page }) => {
await setJSON(page, {
"pages": [
{
"name": "page1",
"elements": [
{
"type": "text",
"name": "question1"
}
]
},
{
"name": "page2",
"elements": [
{
"type": "text",
"name": "question2"
}
"type": "radiogroup",
"name": "question1",
"choices": ["Item 1", "Item 2", "Item 3"]
},
{ "type": "rating", "name": "question2" }
]
}
],
});
await checkA11y(page, ".svc-creator", { axeOptions });
await checkA11y(page, ".svc-tab-designer_content", { axeOptions });
});
});
21 changes: 21 additions & 0 deletions accessibilityTests/propertyGrid.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { test, expect } from "@playwright/test";
import { injectAxe, checkA11y } from "axe-playwright";
import { axeOptions, setJSON, updateCreatorModel, url } from "./helper";

test.describe("property grid a11y", () => {
test.beforeEach(async ({ page }) => {
await page.goto(`${url}`);
await updateCreatorModel(page, { showOneCategoryInPropertyGrid: true });
await injectAxe(page);
await page.setViewportSize({ width: 1920, height: 1080 });
});

test("Check data category", async ({ page }) => {
await setJSON(page, { "pages": [{ "name": "page1", "elements": [{ "type": "text", "name": "question1" }] }], });
await page.click(".svc-sidebar__header-content .svc-menu-action__button");
await page.locator(".svc-list__item-body", { hasText: "question1" }).click();
await page.click(".svc-menu-action__button[title='Data']");
await expect(page.getByRole("button", { name: "Set Default Answer" })).toBeVisible();
await checkA11y(page, ".spg-page", { axeOptions });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
[attr.title]="adorner.dragTooltip" sv-ng-svg-icon></svg>
</span>
<span *ngIf="adorner.allowAdd" class="svc-item-value-controls__button svc-item-value-controls__add" [key2click]
(click)="adorner.add(adorner)" [attr.aria-label]="undefined"><svg [iconName]="'icon-add_16x16'" [size]="'auto'"
[attr.title]="undefined" sv-ng-svg-icon></svg></span>
(click)="adorner.add(adorner)" role="button" [attr.aria-label]="adorner.tooltip"><svg [iconName]="'icon-add_16x16'" [size]="'auto'"
[attr.title]="adorner.tooltip" sv-ng-svg-icon></svg></span>
<span *ngIf="adorner.allowRemove" class="svc-item-value-controls__button svc-item-value-controls__remove"
[key2click] (click)="adorner.remove(adorner)" (blur)="onBlur($event)" [attr.aria-label]="undefined"><svg
[iconName]="'icon-remove_16x16'" [size]="'auto'" [attr.title]="undefined" sv-ng-svg-icon></svg></span>
[key2click] (click)="adorner.remove(adorner)" (blur)="onBlur($event)" role="button" [attr.aria-label]="adorner.tooltip"><svg
[iconName]="'icon-remove_16x16'" [size]="'auto'" [attr.title]="adorner.tooltip" sv-ng-svg-icon></svg></span>
</div>

<div class="svc-item-value__item" (click)="adorner.select(adorner, $event)">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<ng-template #template>
<div class="svc-rating-question-content">
<div [class]="adorner.controlsClassNames">
<svg *ngIf="adorner.allowRemove" [iconName]="'icon-remove_16x16'" [size]="'auto'" [key2click] (click)="adorner.removeItem(adorner)"
[class]="adorner.removeClassNames" [attr.title]="adorner.removeTooltip" [attr.aria-label]="adorner.removeTooltip" sv-ng-svg-icon></svg>
<svg *ngIf="adorner.allowAdd" [iconName]="'icon-add_16x16'" [size]="'auto'" [key2click] (click)="adorner.addItem(adorner)"
[class]="adorner.addClassNames" [attr.title]="adorner.addTooltip" [attr.aria-label]="adorner.addTooltip"sv-ng-svg-icon></svg>
<span *ngIf="adorner.allowRemove" [class]="adorner.removeClassNames" [key2click]
(click)="adorner.removeItem(adorner)" role="button" [attr.aria-label]="adorner.removeTooltip">
<svg [iconName]="'icon-remove_16x16'" [size]="'auto'" [attr.title]="adorner.removeTooltip" sv-ng-svg-icon></svg>
</span>
<span *ngIf="adorner.allowAdd" [class]="adorner.addClassNames" [key2click]
(click)="adorner.addItem(adorner)" role="button" [attr.aria-label]="adorner.addTooltip">
<svg [iconName]="'icon-add_16x16'" [size]="'auto'" [attr.title]="adorner.addTooltip" sv-ng-svg-icon></svg>
</span>
</div>
<ng-container *ngTemplateOutlet="contentTempl"></ng-container>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<ng-template #template>
<div class="svc-page-navigator" #container [visible]="model.visible">
<div role="button" class="svc-page-navigator__selector svc-page-navigator__button"
(click)="model.togglePageSelector($event)" [key2click] [attr.title]="model.pageSelectorCaption" [class.svc-page-navigator__button--pressed]="model.isPopupOpened">
<svg class="svc-page-navigator__button-icon" [iconName]="model.icon" [size]="'auto'" sv-ng-svg-icon></svg>
<sv-ng-popup [popupModel]="model.popupModel"></sv-ng-popup>
<div>
<div role="button" class="svc-page-navigator__selector svc-page-navigator__button"
(click)="model.togglePageSelector($event)" [key2click] [attr.title]="model.pageSelectorCaption" [class.svc-page-navigator__button--pressed]="model.isPopupOpened">
<svg class="svc-page-navigator__button-icon" [iconName]="model.icon" [size]="'auto'" sv-ng-svg-icon></svg>
</div>
<sv-ng-popup [popupModel]="model.popupModel"></sv-ng-popup>
</div>
<div>
<svc-page-navigator-item *ngFor="let item of model.visibleItems" [model]="item"></svc-page-navigator-item>
Expand Down
8 changes: 7 additions & 1 deletion packages/survey-creator-core/src/components/link-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,14 @@ export class QuestionLinkValueModel extends Question {
this.setPropertyValue("isClickable", !this.isReadOnly || (!!this.isClickableCallback && this.isClickableCallback()));
}
public get ariaRole(): string {
return "button";
return "presentation";
}
public get ariaLabel(): string { return null; }
public get ariaRequired() { return null; }
public get ariaInvalid() { return null; }
public get ariaLabelledBy() { return null; }
public get ariaDescribedBy() { return null; }
public get ariaErrormessage() { return null; }

public get tooltip() {
return this.showTooltip ? this.linkValueText : undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export class PageNavigatorViewModel extends Base {
this.popupModel.hide();
},
cssClasses: listComponentCss,
allowSelection: true
allowSelection: true,
listRole: "menu",
listItemRole: "menuitemradio"
});
this.popupModel = new PopupModel("sv-list", { model: this.pageListModel }, { cssClass: "svc-creator-popup" });
this.popupModel.focusFirstInputSelector = ".svc-list__item--selected";
Expand Down Expand Up @@ -299,7 +301,7 @@ export class PageNavigatorViewModel extends Base {
// this._updateVisibility();
setTimeout(() => {
this._updateVisibility();
}, 10);
}, 100);
}
public get visibleItems() {
if (this.items.length <= this.visibleItemsCount) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,9 @@ export class ObjectSelectorModel extends Base {
onSelectionChanged: (item: IAction) => { onClose(item.data); },
allowSelection: true,
cssClasses: listComponentCss,
selectedItem: selectedItem
selectedItem: selectedItem,
listRole: "menu",
listItemRole: "menuitemradio"
});
this.listModelValue.setOnFilterStringChangedCallback((text: string) => { this.selector.filterByText(text); });
} else {
Expand Down
2 changes: 2 additions & 0 deletions packages/survey-creator-react/src/ItemValueWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export class ItemValueAdornerComponent extends CreatorModelElement<
this.model.item = this.props.item;
const button = this.model.allowAdd ? (
attachKey2click(<span
role="button"
className="svc-item-value-controls__button svc-item-value-controls__add"
aria-label={this.model.tooltip}
onClick={() => {
Expand All @@ -83,6 +84,7 @@ export class ItemValueAdornerComponent extends CreatorModelElement<
</span>
) : null}
{this.model.allowRemove ? attachKey2click(<span
role="button"
className="svc-item-value-controls__button svc-item-value-controls__remove"
aria-label={this.model.tooltip}
onClick={() => this.model.remove(this.model)}
Expand Down
27 changes: 14 additions & 13 deletions packages/survey-creator-react/src/PageNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,24 +61,25 @@ export class SurveyPageNavigator extends CreatorModelElement<
className += " svc-page-navigator__button--pressed";
return (
<div className="svc-page-navigator" ref={this.containerRef} style={{ display: this.model.visible ? "flex" : "none" }}>
{attachKey2click(<div
role="button"
className={className}
onClick={() => this.model.togglePageSelector()}
title={this.model.pageSelectorCaption}
>
<SvgIcon
className="svc-page-navigator__button-icon"
iconName={this.model.icon}
size={"auto"}
<div>
{attachKey2click(<div
role="button"
className={className}
onClick={() => this.model.togglePageSelector()}
title={this.model.pageSelectorCaption}
></SvgIcon>

>
<SvgIcon
className="svc-page-navigator__button-icon"
iconName={this.model.icon}
size={"auto"}
title={this.model.pageSelectorCaption}
></SvgIcon>
</div>)}
<Popup
model={this.model.popupModel}
//className="svc-page-navigator__popup"
></Popup>
</div>)}
</div>
<div>
{this.model.visibleItems.map((item) => (
<SurveyPageNavigatorItem
Expand Down
2 changes: 2 additions & 0 deletions packages/survey-creator-react/src/adorners/QuestionRating.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ export class QuestionRatingAdornerComponent extends CreatorModelElement<Question
<div className="svc-rating-question-content">
<div className={model.controlsClassNames}>
{model.allowRemove ? attachKey2click(<span
role="button"
className={model.removeClassNames}
aria-label={model.removeTooltip}
onClick={() => model.removeItem(model)}
>
<SvgIcon size={"auto"} iconName={"icon-remove_16x16"} title={model.removeTooltip}></SvgIcon>
</span>) : null}
{model.allowAdd ? attachKey2click(<span
role="button"
className={model.addClassNames}
aria-label={model.addTooltip}
onClick={() => model.addItem(model)}
Expand Down
10 changes: 6 additions & 4 deletions packages/survey-creator-vue/src/adorners/ItemValue.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@
class="svc-item-value-controls__button svc-item-value-controls__add"
v-key2click
@click="adorner.add(adorner)"
:aria-label="undefined"
role="button"
:aria-label="adorner.tooltip"
><SvComponent
:is="'sv-svg-icon'"
:iconName="'icon-add_16x16'"
:size="'auto'"
:title="undefined"
:title="adorner.tooltip"
></SvComponent
></span>
<span
Expand All @@ -45,12 +46,13 @@
v-key2click
@click="adorner.remove(adorner)"
@blur="adorner.onFocusOut($event)"
:aria-label="undefined"
role="button"
:aria-label="adorner.tooltip"
><SvComponent
:is="'sv-svg-icon'"
:iconName="'icon-remove_16x16'"
:size="'auto'"
:title="undefined"
:title="adorner.tooltip"
></SvComponent
></span>
</div>
Expand Down
34 changes: 20 additions & 14 deletions packages/survey-creator-vue/src/adorners/Rating.vue
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
<template>
<div class="svc-rating-question-content">
<div :class="model?.controlsClassNames">
<SvComponent
:is="'sv-svg-icon'"
<span
v-if="model?.allowRemove"
:iconName="'icon-remove_16x16'"
:size="'auto'"
v-key2click
@click="model?.removeItem(model)"
:class="model?.removeClassNames"
:title="model?.removeTooltip"
@click="model?.removeItem(model)"
role="button"
:aria-label="model?.removeTooltip"
></SvComponent>
<SvComponent
:is="'sv-svg-icon'"
><SvComponent
:is="'sv-svg-icon'"
:iconName="'icon-remove_16x16'"
:size="'auto'"
:title="model?.removeTooltip"
></SvComponent
></span>
<span
v-if="model?.allowAdd"
:iconName="'icon-add_16x16'"
:size="'auto'"
v-key2click
@click="model?.addItem(model)"
:class="model?.addClassNames"
:title="model?.addTooltip"
@click="model?.addItem(model)"
role="button"
:aria-label="model?.addTooltip"
></SvComponent>
><SvComponent
:is="'sv-svg-icon'"
:iconName="'icon-add_16x16'"
:size="'auto'"
:title="model?.addTooltip"
></SvComponent
></span>
</div>
<slot></slot>
</div>
Expand Down
30 changes: 16 additions & 14 deletions packages/survey-creator-vue/src/page-navigator/PageNavigator.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
<template>
<div class="svc-page-navigator" v-show="model.visible" ref="root">
<div
role="button"
class="svc-page-navigator__selector svc-page-navigator__button"
@click="model.togglePageSelector($event)"
v-key2click
:title="model.pageSelectorCaption"
:class="{ 'svc-page-navigator__button--pressed': model.isPopupOpened }"
>
<SvComponent
:is="'sv-svg-icon'"
class="svc-page-navigator__button-icon"
:iconName="model.icon"
:size="'auto'"
></SvComponent>
<div>
<div
role="button"
class="svc-page-navigator__selector svc-page-navigator__button"
@click="model.togglePageSelector($event)"
v-key2click
:title="model.pageSelectorCaption"
:class="{ 'svc-page-navigator__button--pressed': model.isPopupOpened }"
>
<SvComponent
:is="'sv-svg-icon'"
class="svc-page-navigator__button-icon"
:iconName="model.icon"
:size="'auto'"
></SvComponent>
</div>
<SvComponent :is="'sv-popup'" :model="model.popupModel"></SvComponent>
</div>
<div>
Expand Down
2 changes: 1 addition & 1 deletion visualRegressionTests/tests/designer/page-navigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ test("On the right side opened popup", async (t) => {
await t.click(".svc-page-navigator__selector");
await t.click(".svc-page-navigator__selector");
await t.click(".svc-page-navigator__selector");
await takeElementScreenshot("page-navigator-popup.png", Selector(".svc-page-navigator__selector .sv-popup__container"), t, comparer);
await takeElementScreenshot("page-navigator-popup.png", Selector(".svc-page-navigator .sv-popup__container"), t, comparer);
await takeElementScreenshot("page-navigator-with-popup.png", Selector(".svc-creator-tab"), t, comparer);
});
});
Expand Down