Skip to content
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

BC-7520 - a11y messages #3295

Merged
merged 23 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a01639e
initial implementation
muratmerdoglu-dp Jun 19, 2024
7fbee59
chore: Refactor useAriaLiveNotifier to improve readability and mainta…
hoeppner-dataport Jun 19, 2024
2a0c79c
fix: cardTitle issue
hoeppner-dataport Jun 19, 2024
77d3fc7
Merge branch 'main' of github.com:hpi-schul-cloud/nuxt-client into BC…
hoeppner-dataport Jun 20, 2024
22f4fe6
chore: refactoring
hoeppner-dataport Jun 20, 2024
894433f
chore: added some tests
hoeppner-dataport Jun 20, 2024
3530a22
chore: unify wording
hoeppner-dataport Jun 20, 2024
8e6bd7e
chore: fix typo
hoeppner-dataport Jun 20, 2024
a006bf0
chore: replace onBlur to onNoKeyForOneSecond notification-output
hoeppner-dataport Jun 20, 2024
5e84a0a
chore: fix function names
hoeppner-dataport Jun 20, 2024
62b302f
fix tests
hoeppner-dataport Jun 20, 2024
d7e242b
chore: renamed onKeyboard in CKEditor.vue to onInput
hoeppner-dataport Jun 20, 2024
0048d5c
add editing detection for ckeditor
hoeppner-dataport Jun 21, 2024
ba16372
Merge branch 'main' of github.com:hpi-schul-cloud/nuxt-client into BC…
hoeppner-dataport Jun 21, 2024
53c2984
fix: unit tests
hoeppner-dataport Jun 21, 2024
1ccc4c8
refactor: inner code of ariaLiveNotifier.ts
hoeppner-dataport Jun 21, 2024
c339456
chore: move code to avoid manipulation of RichTextContentElementEdit.vue
hoeppner-dataport Jun 21, 2024
a8cc7af
add test for aria-live element not being present
hoeppner-dataport Jun 21, 2024
f28c875
fix: avoid errors on other pages that use DefaultWireframe component
hoeppner-dataport Jun 24, 2024
ff181fd
chore: remove unneeded test
hoeppner-dataport Jun 24, 2024
c3d93bd
chore: improve coverage
hoeppner-dataport Jun 24, 2024
6ed5b29
chore: remove unneeded code
hoeppner-dataport Jun 25, 2024
e3d46b3
chore: fix defaultWireframe
hoeppner-dataport Jun 25, 2024
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
96 changes: 81 additions & 15 deletions src/composables/ariaLiveNotifier.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,94 @@
import { computed, ref } from "vue";
import { useDebounceFn } from "@vueuse/core";

type Importance = "polite" | "assertive";
let mode: "write" | "queue" = "write";
const notifications = ref({
polite: <string[]>[],
assertive: <string[]>[],
});
let handle: NodeJS.Timeout | null = null;

export const useAriaLiveNotifier = () => {
const numberOfNotifications = computed(
() =>
notifications.value["polite"].length +
notifications.value["assertive"].length
);

const notifyOnScreenReader = (
message: string,
importance: "off" | "polite" | "assertive" = "polite"
importance: Importance = "polite"
) => {
// should be a div with aria-live="polite | assertive" attribute
// and should be appended to the upper level of the DOM tree
// the aria-live attribute should be set polite or assertive based on the importance of the message
const element = document.getElementById(
importance === "polite"
? "notify-screen-reader-polite"
: "notify-screen-reader-assertive"
);
notifications.value[importance].push(message);
handleNotificationWriting();
};

if (!element) {
console.error(
`Element with id 'notify-screen-reader-${importance}' not found`
);
return;
const ensurePoliteNotifications = () => {
mode = "queue";
processNotificationsDebounced();
};

const processNotificationsDebounced = useDebounceFn(
() => {
mode = "write";
handleNotificationWriting(); // explicit call needed for test
},
1500,
{ maxWait: 30000 }
);

const handleNotificationWriting = () => {
if (numberOfNotifications.value > 0) {
if (mode === "write") {
stopPeriodicRetry();
writeAllNotifications();
return;
} else {
startPeriodicRetry();
}
}
};

const startPeriodicRetry = () => {
if (handle === null) {
handle = setInterval(handleNotificationWriting, 1000);
}
};

const stopPeriodicRetry = () => {
if (handle) {
clearInterval(handle);
handle = null;
}
};

const writeAllNotifications = () => {
writeNotifications("polite");
writeNotifications("assertive");
};

const writeNotifications = (importance: Importance) => {
const element = getElement(importance);

if (element && notifications.value[importance].length > 0) {
element.innerHTML = notifications.value[importance]
.map((m) => `<span>${m}</span>`)
.join("");
notifications.value[importance] = [];
}
};

const getElement = (importance: Importance): HTMLElement | null => {
const element = document.getElementById(
`notify-screen-reader-${importance}`
);

element.innerHTML += `<span>${message}</span>`;
return element;
};

return {
notifyOnScreenReader,
ensurePoliteNotifications,
};
};
29 changes: 27 additions & 2 deletions src/composables/ariaLiveNotifier.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ describe("useAriaLiveNotifier", () => {
<div id="notify-screen-reader-polite"></div>
<div id="notify-screen-reader-assertive"></div>
</div>`;

jest.useFakeTimers();
});

it("should notify on screen reader on 'aria-live=assertive' mode", () => {
jest.useFakeTimers();
const { notifyOnScreenReader } = useAriaLiveNotifier();
const element = document.getElementById("notify-screen-reader-assertive");
const message = "Assertive screen reader message";
Expand All @@ -20,7 +22,6 @@ describe("useAriaLiveNotifier", () => {
});

it("should notify on screen reader on 'aria-live=polite' mode", () => {
jest.useFakeTimers();
const { notifyOnScreenReader } = useAriaLiveNotifier();
const element = document.getElementById("notify-screen-reader-polite");
const message = "Polite screen reader message";
Expand All @@ -29,4 +30,28 @@ describe("useAriaLiveNotifier", () => {
jest.advanceTimersByTime(3000);
expect(element?.innerHTML).toBe(`<span>${message}</span>`);
});

describe("ensurePoliteNotifications", () => {
describe("when politeNotifications are ensured", () => {
it("should notify all collected messages after some time without user interaction", () => {
const { notifyOnScreenReader, ensurePoliteNotifications } =
useAriaLiveNotifier();
const element = document.getElementById("notify-screen-reader-polite");
const message1 = "Polite screen reader message 1";
const message2 = "Polite screen reader message 2";

ensurePoliteNotifications();
notifyOnScreenReader(message1, "polite");
notifyOnScreenReader(message2, "polite");

expect(element?.innerHTML).toBe("");

jest.advanceTimersByTime(3000);

expect(element?.innerHTML).toBe(
`<span>${message1}</span><span>${message2}</span>`
);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const useBoardSocketApi = () => {
),
];

const ariaLiveNotification = [
const ariaLiveNotifications = [
on(BoardActions.createCardSuccess, notifyCreateCardSuccess),
on(CardActions.deleteCardSuccess, notifyDeleteCardSuccess),
on(BoardActions.createColumnSuccess, notifyCreateColumnSuccess),
Expand All @@ -93,7 +93,7 @@ export const useBoardSocketApi = () => {
action,
...successActions,
...failureActions,
...ariaLiveNotification,
...ariaLiveNotifications,
on(BoardActions.disconnectSocket, disconnectSocketRequest)
);
};
Expand Down
23 changes: 20 additions & 3 deletions src/modules/feature/board-text-element/RichTextContentElement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
class="rich_text"
:autofocus="autofocus"
:value="modelValue.text"
@update:value="($event) => (modelValue.text = $event)"
@update:value="onUpdateElement"
@delete:element="onDeleteElement"
@blur="() => (autofocus = false)"
@blur="onBlur"
@keyup.capture="onKeyUp"
/>
</div>
</template>
Expand All @@ -23,6 +24,7 @@ import { useBoardFocusHandler, useContentElementState } from "@data-board";
import { defineComponent, PropType, ref, toRef } from "vue";
import RichTextContentElementDisplay from "./RichTextContentElementDisplay.vue";
import RichTextContentElementEdit from "./RichTextContentElementEdit.vue";
import { useAriaLiveNotifier } from "@/composables/ariaLiveNotifier";
export default defineComponent({
name: "RichTextContentElement",
Expand All @@ -40,6 +42,8 @@ export default defineComponent({
emits: ["delete:element"],
setup(props, { emit }) {
const { modelValue } = useContentElementState(props);
const { ensurePoliteNotifications } = useAriaLiveNotifier();
const autofocus = ref(false);
const element = toRef(props, "element");
useBoardFocusHandler(element.value.id, ref(null), () => {
Expand All @@ -50,10 +54,23 @@ export default defineComponent({
emit("delete:element", element.value.id);
};
const onUpdateElement = (text: string) => {
modelValue.value.text = text;
};
const onBlur = () => {
autofocus.value = false;
};
const onKeyUp = () => ensurePoliteNotifications();
return {
autofocus,
modelValue,
onBlur,
onDeleteElement,
autofocus,
onKeyUp,
onUpdateElement,
};
},
});
Expand Down
2 changes: 1 addition & 1 deletion src/modules/feature/board/card/CardTitle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default defineComponent({
props: {
value: {
type: String,
required: true,
default: "",
},
isEditMode: {
type: Boolean,
Expand Down
9 changes: 8 additions & 1 deletion src/modules/feature/editor/CKEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ export default defineComponent({
components: {
ckeditor: CKEditor.component,
},
emits: ["ready", "focus", "update:value", "blur", "keyboard:delete"],
emits: [
"ready",
"focus",
"update:value",
"blur",
"keyboard",
"keyboard:delete",
],
props: {
value: {
type: String,
Expand Down
Loading