diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 6605987c..fd440e66 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -109,6 +109,13 @@ "text": "Viewing this content requires strong identification. Log out and try another login method.", "title": "Strong identification is required" }, + "table": { + "accessibility": { + "noResultsFound": "Your search returned no results.", + "resultsFoundText": "{{count}} search result found", + "resultsFoundText_other": "{{count}} search results found" + } + }, "titleServerErrorSummary": "Form contains following errors", "total": "In total", "validation": { diff --git a/public/locales/fi/common.json b/public/locales/fi/common.json index 2788d234..699a4bef 100644 --- a/public/locales/fi/common.json +++ b/public/locales/fi/common.json @@ -110,6 +110,13 @@ "text": "Tämän sisällön näkeminen edellyttää vahvaa tunnistautumista. Kirjaudu ulos ja kokeile toista kirjautumistapaa.", "title": "Vahva tunnistautuminen vaaditaan" }, + "table": { + "accessibility": { + "noResultsFound": "Hakusi ei tuottanut yhtään tuloksia.", + "resultsFoundText": "{{count}} hakutulos löytynyt", + "resultsFoundText_other": "{{count}} hakutulosta löytynyt" + } + }, "titleServerErrorSummary": "Lomakkeella on seuraavat virheet", "total": "Yhteensä", "validation": { diff --git a/public/locales/sv/common.json b/public/locales/sv/common.json index 3ef8f968..f185dcd6 100644 --- a/public/locales/sv/common.json +++ b/public/locales/sv/common.json @@ -110,6 +110,13 @@ "text": "Att se detta innehåll kräver stark identifiering. Logga ut och prova en annan inloggningsmetod.", "title": "Stark identifiering krävs" }, + "table": { + "accessibility": { + "noResultsFound": "Din sökning gav inga resultat.", + "resultsFoundText": "{{count}} sökresultat hittades", + "resultsFoundText_other": "{{count}} sökresultat hittades" + } + }, "titleServerErrorSummary": "Formuläret innehåller följande fel", "total": "Totalt", "validation": { diff --git a/src/common/components/accessibilityNotificationContext/AccessibilityNotificationContext.tsx b/src/common/components/accessibilityNotificationContext/AccessibilityNotificationContext.tsx new file mode 100644 index 00000000..eadd5a78 --- /dev/null +++ b/src/common/components/accessibilityNotificationContext/AccessibilityNotificationContext.tsx @@ -0,0 +1,81 @@ +import uniqueId from 'lodash/uniqueId'; +import React, { + createContext, + FC, + PropsWithChildren, + useCallback, + useMemo, + useState, +} from 'react'; + +import styles from './accessibilityNotification.module.scss'; + +export type SetAccessibilityTextFn = (text: string) => void; + +export type AccessibilityNotificationProps = { id: string; text: string }; + +export type AccessibilityNotificationContextProps = { + setAccessibilityText: SetAccessibilityTextFn; +}; + +export const AccessibilityNotificationContext = createContext< + AccessibilityNotificationContextProps | undefined +>(undefined); + +export const AccessibilityNotificationProvider: FC = ({ + children, +}) => { + const [notifications, setNotifications] = useState< + AccessibilityNotificationProps[] + >([]); + + const removeNotification = (notificationId: string) => { + setNotifications((items) => + items.filter(({ id }) => id !== notificationId) + ); + }; + + const updateNotificationText = (text: string, notificationId: string) => + setNotifications((items) => + items.map((notification) => + notification.id === notificationId + ? { ...notification, text } + : notification + ) + ); + + const setAccessibilityText = useCallback((text: string) => { + const notificationId = uniqueId('accessibility-notification-'); + + setNotifications((items) => [...items, { id: notificationId, text: '' }]); + // Change notification text after 100ms to force screen reader + // to read the notification + setTimeout(() => { + updateNotificationText(text, notificationId); + }, 100); + + // Clear notification area after 1000ms + setTimeout(() => { + removeNotification(notificationId); + }, 1000); + }, []); + + const value = useMemo( + () => ({ + setAccessibilityText, + }), + [setAccessibilityText] + ); + + return ( + + {notifications.map(({ text, id }) => ( + + {text} + + ))} + + {children} + + ); +}; diff --git a/src/common/components/accessibilityNotificationContext/accessibilityNotification.module.scss b/src/common/components/accessibilityNotificationContext/accessibilityNotification.module.scss new file mode 100644 index 00000000..a3ba2d97 --- /dev/null +++ b/src/common/components/accessibilityNotificationContext/accessibilityNotification.module.scss @@ -0,0 +1,5 @@ +@import '../../../styles/mixins.scss'; + +.accessibilityNotification { + @include hidden-from-screen; +} diff --git a/src/common/components/accessibilityNotificationContext/hooks/useAccessibilityNotificationContext.tsx b/src/common/components/accessibilityNotificationContext/hooks/useAccessibilityNotificationContext.tsx new file mode 100644 index 00000000..88cb41b3 --- /dev/null +++ b/src/common/components/accessibilityNotificationContext/hooks/useAccessibilityNotificationContext.tsx @@ -0,0 +1,24 @@ +/* eslint @typescript-eslint/explicit-function-return-type: 0 */ +import { useContext } from 'react'; + +import { + AccessibilityNotificationContext, + AccessibilityNotificationContextProps, +} from '../AccessibilityNotificationContext'; + +export const useAccessibilityNotificationContext = + (): AccessibilityNotificationContextProps => { + const context = useContext< + AccessibilityNotificationContextProps | undefined + >(AccessibilityNotificationContext); + + /* istanbul ignore next */ + if (!context) { + throw new Error( + // eslint-disable-next-line max-len + 'AccessibilityNotificationContext context is undefined, please verify you are calling useAccessibilityNotificationContext() as child of a component.' + ); + } + + return context; + }; diff --git a/src/common/components/searchStatus/SearchStatus.tsx b/src/common/components/searchStatus/SearchStatus.tsx new file mode 100644 index 00000000..b51d8195 --- /dev/null +++ b/src/common/components/searchStatus/SearchStatus.tsx @@ -0,0 +1,25 @@ +/* eslint-disable max-len */ +import { useTranslation } from 'next-i18next'; +import { FC, useEffect } from 'react'; + +import { useAccessibilityNotificationContext } from '../accessibilityNotificationContext/hooks/useAccessibilityNotificationContext'; + +type Props = { count: number; loading: boolean }; + +const SearchStatus: FC = ({ count, loading }) => { + const { t } = useTranslation('common'); + const { setAccessibilityText } = useAccessibilityNotificationContext(); + + useEffect(() => { + if (!loading) { + setAccessibilityText( + count + ? t('common:table.accessibility.resultsFoundText', { count }) + : t('common:table.accessibility.noResultsFound') + ); + } + }, [count, loading, setAccessibilityText, t]); + return null; +}; + +export default SearchStatus; diff --git a/src/domain/attendanceList/attendeeList/AttendeeList.tsx b/src/domain/attendanceList/attendeeList/AttendeeList.tsx index 89ae2ed2..8c7b9a70 100644 --- a/src/domain/attendanceList/attendeeList/AttendeeList.tsx +++ b/src/domain/attendanceList/attendeeList/AttendeeList.tsx @@ -6,6 +6,7 @@ import React, { useState } from 'react'; import Checkbox from '../../../common/components/checkbox/Checkbox'; import { useNotificationsContext } from '../../../common/components/notificationsContext/hooks/useNotificationsContext'; +import SearchStatus from '../../../common/components/searchStatus/SearchStatus'; import useHandleError from '../../../hooks/useHandleError'; import { ExtendedSession } from '../../../types'; import skipFalsyType from '../../../utils/skipFalsyType'; @@ -106,9 +107,10 @@ const AttendeeList: React.FC = ({ registration }) => { return ( <> + - - - - - - - - - - - + + + + + + + + + + + + + + ); }; diff --git a/src/utils/testUtils.tsx b/src/utils/testUtils.tsx index 91f588cd..c94a88f6 100644 --- a/src/utils/testUtils.tsx +++ b/src/utils/testUtils.tsx @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable no-console */ @@ -19,6 +20,7 @@ import { SessionProvider } from 'next-auth/react'; import React, { useMemo } from 'react'; import wait from 'waait'; +import { AccessibilityNotificationProvider } from '../common/components/accessibilityNotificationContext/AccessibilityNotificationContext'; import { testId } from '../common/components/loadingSpinner/LoadingSpinner'; import { NotificationsProvider } from '../common/components/notificationsContext/NotificationsContext'; import { registration } from '../domain/registration/__mocks__/registration'; @@ -61,16 +63,18 @@ const customRender: CustomRender = ( ); return ( - - - - {/* @ts-ignore */} - - {children as React.ReactElement} - - - - + + + + + {/* @ts-ignore */} + + {children as React.ReactElement} + + + + + ); };