Skip to content

Commit 6a14ca1

Browse files
authored
Merge pull request #2233 from dxc-technology/Mil4n0r/fix_toast_keyboard_interaction
Added keyboard support for `DxcToast`
2 parents 51b0da8 + e0bb43d commit 6a14ca1

File tree

1 file changed

+63
-14
lines changed

1 file changed

+63
-14
lines changed

packages/lib/src/toast/Toast.tsx

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { memo, useContext, useState } from "react";
1+
import { memo, useContext, useState, useRef, useEffect } from "react";
22
import { keyframes } from "@emotion/react";
33
import styled from "@emotion/styled";
44
import DxcActionIcon from "../action-icon/ActionIcon";
@@ -52,6 +52,10 @@ const Toast = styled.output<{ semantic: ToastPropsType["semantic"]; isClosing: b
5252
@media (max-width: ${responsiveSizes.medium}rem) {
5353
max-width: 100%;
5454
}
55+
56+
&:focus {
57+
outline: none;
58+
}
5559
`;
5660

5761
const ContentContainer = styled.div<{ loading: ToastPropsType["loading"]; semantic: ToastPropsType["semantic"] }>`
@@ -88,24 +92,69 @@ const DxcToast = ({
8892
semantic,
8993
}: ToastPropsType) => {
9094
const [isClosing, setIsClosing] = useState(false);
95+
const toastRef = useRef<HTMLOutputElement>(null);
96+
const previouslyFocusedElement = useRef<HTMLElement | null>(null);
9197
const translatedLabels = useContext(HalstackLanguageContext);
9298

93-
const clearClosingAnimationTimer = useTimeout(
94-
() => {
95-
setIsClosing(true);
96-
},
97-
loading ? undefined : duration - 300
98-
);
99+
// Timeouts
100+
const clearClosingAnimationTimer = useTimeout(() => setIsClosing(true), loading ? undefined : duration - 300);
99101

100-
const clearTimer = useTimeout(
101-
() => {
102-
onClear();
103-
},
104-
loading ? undefined : duration
105-
);
102+
const clearTimer = useTimeout(() => onClear(), loading ? undefined : duration);
103+
104+
useEffect(() => {
105+
previouslyFocusedElement.current = document.activeElement as HTMLElement;
106+
107+
toastRef.current?.focus();
108+
109+
return () => {
110+
previouslyFocusedElement.current?.focus?.();
111+
};
112+
}, []);
113+
114+
const handleOnKeyDown = (event: React.KeyboardEvent<HTMLOutputElement>) => {
115+
if (event.key === "Tab") {
116+
event.preventDefault();
117+
118+
const focusableElements = toastRef.current?.querySelectorAll<HTMLElement>(
119+
'button, [tabindex]:not([tabindex="-1"])'
120+
);
121+
if (!focusableElements || focusableElements.length === 0) return;
122+
123+
const firstElement = focusableElements?.[0];
124+
const lastElement = focusableElements?.[focusableElements.length - 1];
125+
const activeElement = document.activeElement;
126+
127+
const elementsArray = Array.from(focusableElements);
128+
129+
if (!event.shiftKey) {
130+
if (activeElement === lastElement) {
131+
previouslyFocusedElement.current?.focus?.();
132+
} else {
133+
const currentIndex = elementsArray.indexOf(activeElement as HTMLElement);
134+
const nextElement = focusableElements[currentIndex + 1];
135+
nextElement?.focus();
136+
}
137+
} else {
138+
if (activeElement === firstElement) {
139+
previouslyFocusedElement.current?.focus?.();
140+
} else {
141+
const currentIndex = elementsArray.indexOf(activeElement as HTMLElement);
142+
const prevElement = focusableElements[currentIndex - 1];
143+
prevElement?.focus();
144+
}
145+
}
146+
}
147+
};
106148

107149
return (
108-
<Toast isClosing={isClosing} role="status" semantic={semantic}>
150+
<Toast
151+
onKeyDown={handleOnKeyDown}
152+
isClosing={isClosing}
153+
role="status"
154+
semantic={semantic}
155+
tabIndex={-1}
156+
ref={toastRef}
157+
>
109158
<ContentContainer loading={loading} semantic={semantic}>
110159
<ToastIcon hideSemanticIcon={hideSemanticIcon} icon={icon} loading={loading} semantic={semantic} />
111160
<Message>{message}</Message>

0 commit comments

Comments
 (0)