Skip to content

Use icon button label as the aria-label to improve accessibility. #1066

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 5 commits into from
Jun 19, 2024
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
2 changes: 2 additions & 0 deletions packages/material/modules/widgets/core/MaterialButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ export default (props) => {
const icon = renderIcon?.(iconProps);

if (!label || hideLabelsFor[type]) {
// For icons, use the label as aria-label for accessibility
return (
<IconButton
size="small"
disabled={readonly}
onClick={onClick}
color={typeToColor[type]}
aria-label={label}
>{icon}</IconButton>
);
} else {
Expand Down
10 changes: 8 additions & 2 deletions packages/material/modules/widgets/value/MaterialAutocomplete.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Box from "@material-ui/core/Box";
import { makeStyles } from "@material-ui/core/styles";
import CheckBoxOutlineBlankIcon from "@material-ui/icons/CheckBoxOutlineBlank";
import CheckBoxIcon from "@material-ui/icons/CheckBox";
import { Hooks, Utils } from "@react-awesome-query-builder/ui";
import { Hooks } from "@react-awesome-query-builder/ui";
const { useListValuesAutocomplete } = Hooks;

const nonCheckedIcon = <CheckBoxOutlineBlankIcon fontSize="small" style={{ marginRight: 10, marginTop: 4 }} />;
Expand Down Expand Up @@ -58,7 +58,7 @@ export default (props) => {
uif: "mui"
});

// setings
// settings
const {defaultSelectWidth, defaultSearchWidth} = config.settings;
const {width, ...rest} = customProps || {};
let customInputProps = rest.input || {};
Expand All @@ -73,6 +73,10 @@ export default (props) => {
minWidth: minWidth
};
const placeholder = !readonly ? aPlaceholder : "";

// For accessibility, always give the input field an aria-label
const ariaLabel = placeholder || config.settings.fieldPlaceholder;

const hasValue = selectedValue != null;
// should be simple value to prevent re-render!s
const value = hasValue ? selectedValue : (multiple ? emptyArray : null);
Expand Down Expand Up @@ -120,6 +124,7 @@ export default (props) => {
inputProps={{
...params.inputProps,
value,
"aria-label": ariaLabel,
}}
InputProps={{
...params.InputProps,
Expand All @@ -133,6 +138,7 @@ export default (props) => {
}}
disabled={readonly}
placeholder={placeholder}
aria-label={placeholder}
error={!!errorText}
//onChange={onInputChange}
{...customInputProps}
Expand Down
2 changes: 2 additions & 0 deletions packages/mui/modules/widgets/core/MuiButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ export default memo((props) => {
const icon = renderIcon?.(iconProps);

if (!label || hideLabelsFor[type]) {
// For icons, use the label as aria-label for accessibility
return (
<IconButton
size="small"
disabled={readonly}
onClick={onClick}
color={typeToColor[type]}
aria-label={label}
>{icon}</IconButton>
);
} else {
Expand Down
8 changes: 6 additions & 2 deletions packages/mui/modules/widgets/value/MuiAutocomplete.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Tooltip from "@mui/material/Tooltip";
import { Hooks } from "@react-awesome-query-builder/ui";
import { styled } from "@mui/system";
import { useTheme } from "@mui/material/styles";
const { useListValuesAutocomplete } = Hooks;
const emptyArray = [];
Expand Down Expand Up @@ -63,7 +62,7 @@ export default (props) => {
isFieldAutocomplete,
});

// setings
// settings
const {defaultSelectWidth, defaultSearchWidth} = config.settings;
const {width, ...rest} = customProps || {};
let customInputProps = rest.input || {};
Expand All @@ -78,6 +77,10 @@ export default (props) => {
minWidth: minWidth
};
const placeholder = !readonly ? aPlaceholder : "";

// For accessibility, always give the input field an aria-label
const ariaLabel = placeholder || config.settings.fieldPlaceholder;

const hasValue = selectedValue != null;
// should be simple value to prevent re-render!s
const value = hasValue ? selectedValue : (multiple ? emptyArray : null);
Expand Down Expand Up @@ -106,6 +109,7 @@ export default (props) => {
inputProps={{
...params.inputProps,
value,
"aria-label": ariaLabel,
}}
InputProps={{
...params.InputProps,
Expand Down
64 changes: 64 additions & 0 deletions packages/tests/specs/WidgetsMaterial.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,70 @@ describe("material-ui widgets interactions", () => {
ignoreLog: ignoreLogDatePicker,
});
});
});

describe("material-ui IconButton accessibility - aria-label", () => {
// When deleteLabel is defined it is used as the aria-label for icon buttons
it("delete button label is used as aria-label", async () => {
await with_qb_material(configs.with_modified_delete_label,
inits.with_number,
"JsonLogic",
() => {
const deleteBtn = document.querySelector<HTMLElement>(
".rule--header .MuiIconButton-root"
);
expect(deleteBtn, "deleteBtn").to.exist;
const ariaLabel = deleteBtn?.getAttribute("aria-label");
expect(ariaLabel).to.eq("Delete rule");
}
);
});

// When deleteLabel is not defined icon buttons do not have an aria-label
it("delete button with no label defined has no aria-label", async () => {
await with_qb_material(configs.with_no_delete_label,
inits.with_number,
"JsonLogic",
() => {
const deleteBtn = document.querySelector<HTMLElement>(
".rule--header .MuiIconButton-root"
);
expect(deleteBtn, "deleteBtn").to.exist;
expect(deleteBtn?.hasAttribute("aria-label")).to.eq(false);
}
);
});
});

describe("material-ui Autocomplete accessibility - aria-label", () => {
// Autocomplete uses fieldPlaceholder from config as the aria-label
it("fieldPlaceholder is used as aria-label", async () => {
await with_qb_material(configs.with_modified_field_placeholder,
inits.with_number,
"JsonLogic",
() => {
const fieldCombo = document.querySelector<HTMLElement>(
".rule--field .MuiAutocomplete-input"
);
expect(fieldCombo, "field combobox").to.exist;
const ariaLabel = fieldCombo?.getAttribute("aria-label");
expect(ariaLabel).to.eq("autocomplete placeholder");
}
);
});

// When fieldPlaceholder is not defined Autocompletes do not have an aria-label
it("select field combobox has no aria-label when fieldPlaceholder is not defined", async () => {
await with_qb_material(configs.with_no_field_placeholder,
inits.with_number,
"JsonLogic",
() => {
const fieldCombo = document.querySelector<HTMLElement>(
".rule--field .MuiAutocomplete-input"
);
expect(fieldCombo, "field combobox").to.exist;
expect(fieldCombo?.hasAttribute("aria-label")).to.eq(false);
}
);
});
});
66 changes: 66 additions & 0 deletions packages/tests/specs/WidgetsMui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,69 @@ describe("mui widgets interactions", () => {
});

});

describe("mui IconButton accessibility - aria-label", () => {
// When deleteLabel is defined it is used as the aria-label for icon buttons
it("delete button label is used as aria-label", async () => {
await with_qb_mui(configs.with_modified_delete_label,
inits.with_number,
"JsonLogic",
() => {
const deleteBtn = document.querySelector<HTMLElement>(
".rule--header .MuiIconButton-root"
);
expect(deleteBtn, "deleteBtn").to.exist;
const ariaLabel = deleteBtn?.getAttribute("aria-label");
expect(ariaLabel).to.eq("Delete rule");
}
);
});

// When deleteLabel is not defined icon buttons do not have an aria-label
it("delete button with no label defined has no aria-label", async () => {
await with_qb_mui(configs.with_no_delete_label,
inits.with_number,
"JsonLogic",
() => {
const deleteBtn = document.querySelector<HTMLElement>(
".rule--header .MuiIconButton-root"
);
expect(deleteBtn, "deleteBtn").to.exist;
expect(deleteBtn?.hasAttribute("aria-label")).to.eq(false);
}
);
});
});

describe("mui Autocomplete accessibility - aria-label", () => {
// Autocomplete uses fieldPlaceholder from config as the aria-label
it("fieldPlaceholder is used as aria-label", async () => {
await with_qb_mui(configs.with_modified_field_placeholder,
inits.with_number,
"JsonLogic",
() => {
const fieldCombo = document.querySelector<HTMLElement>(
".rule--field .MuiAutocomplete-input"
);
expect(fieldCombo, "field combobox").to.exist;
const ariaLabel = fieldCombo?.getAttribute("aria-label");
expect(ariaLabel).to.eq("autocomplete placeholder");
}
);
});

// When fieldPlaceholder is not defined Autocompletes do not have an aria-label
it("select field combobox has no aria-label when fieldPlaceholder is not defined", async () => {
await with_qb_mui(configs.with_no_field_placeholder,
inits.with_number,
"JsonLogic",
() => {
const fieldCombo = document.querySelector<HTMLElement>(
".rule--field .MuiAutocomplete-input"
);
expect(fieldCombo, "field combobox").to.exist;
expect(fieldCombo?.hasAttribute("aria-label")).to.eq(false);
}
);
});
});
77 changes: 77 additions & 0 deletions packages/tests/support/configs.js
Original file line number Diff line number Diff line change
Expand Up @@ -1578,3 +1578,80 @@ export const with_dot_in_field = (BasicConfig) => ({
},
},
});

export const with_modified_delete_label = (BasicConfig) => ({
...BasicConfig,
fields: {
num: {
label: "Number",
type: "number",
preferWidgets: ["number"],
fieldSettings: {
min: -1,
max: 5
},
},
},
settings: {
...BasicConfig.settings,
deleteLabel: "Delete rule",
}
});

export const with_no_delete_label = (BasicConfig) => ({
...BasicConfig,
fields: {
num: {
label: "Number",
type: "number",
preferWidgets: ["number"],
fieldSettings: {
min: -1,
max: 5
},
},
},
settings: {
...BasicConfig.settings,
deleteLabel: null,
}
});


export const with_modified_field_placeholder = (BasicConfig) => ({
...BasicConfig,
fields: {
num: {
label: "Number",
type: "number",
preferWidgets: ["number"],
fieldSettings: {
min: -1,
max: 5
},
},
},
settings: {
...BasicConfig.settings,
fieldPlaceholder: "autocomplete placeholder",
}
});

export const with_no_field_placeholder = (BasicConfig) => ({
...BasicConfig,
fields: {
num: {
label: "Number",
type: "number",
preferWidgets: ["number"],
fieldSettings: {
min: -1,
max: 5
},
},
},
settings: {
...BasicConfig.settings,
fieldPlaceholder: null,
}
});