diff --git a/CHANGELOG.md b/CHANGELOG.md index d32b485..51fbff2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ and this project adheres to - 🐛(dimail) improve handling of dimail errors on failed mailbox creation #377 - 💬(frontend) fix group member removal text #382 - 💬(frontend) fix add mail domain text #382 -- 🐛(frontend) fix keyboard navigation on language picker #379 +- 🐛(frontend) fix keyboard navigation #379 ## [1.0.2] - 2024-08-30 diff --git a/src/frontend/apps/desk/src/components/Modal.tsx b/src/frontend/apps/desk/src/components/Modal.tsx new file mode 100644 index 0000000..963d879 --- /dev/null +++ b/src/frontend/apps/desk/src/components/Modal.tsx @@ -0,0 +1,42 @@ +import { + Modal as CunninghamModal, + ModalProps, +} from '@openfun/cunningham-react'; +import React, { useEffect } from 'react'; + +// Define a wrapper component that extends ModalProps to accept the same props as the Modal +export const Modal: React.FC = ({ children, ...props }) => { + // Apply the hook here once for all modals + usePreventFocusVisible(['.c__modal__content']); + + return {children}; +}; + +/** + * @description used to prevent elements to be navigable by keyboard when only a DOM mutation causes the elements to be + * in the document + * @see https://github.com/numerique-gouv/people/pull/379 + */ +export const usePreventFocusVisible = (elements: string[]) => { + useEffect(() => { + const observer = new MutationObserver((mutationsList) => { + mutationsList.forEach(() => { + elements.forEach((selector) => + document.querySelector(selector)?.setAttribute('tabindex', '-1'), + ); + observer.disconnect(); + }); + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); + + return () => { + observer.disconnect(); + }; + }, [elements]); + + return null; +}; diff --git a/src/frontend/apps/desk/src/components/__tests__/Modal.test.tsx b/src/frontend/apps/desk/src/components/__tests__/Modal.test.tsx new file mode 100644 index 0000000..1d7b2ae --- /dev/null +++ b/src/frontend/apps/desk/src/components/__tests__/Modal.test.tsx @@ -0,0 +1,80 @@ +import { ModalSize } from '@openfun/cunningham-react'; +import { render, screen, waitFor } from '@testing-library/react'; +import React from 'react'; + +import { AppWrapper } from '@/tests/utils'; + +import { Modal, usePreventFocusVisible } from '../Modal'; + +describe('usePreventFocusVisible hook', () => { + const TestComponent = () => { + usePreventFocusVisible(['.test-element']); + + return ( +
+
Test Element
+
+ ); + }; + + const originalMutationObserver = global.MutationObserver; + + const mockDisconnect = jest.fn(); + const mutationObserverMock = jest.fn(function MutationObserver( + callback: MutationCallback, + ) { + this.observe = () => { + callback([{ type: 'childList' }] as MutationRecord[], this); + }; + + this.disconnect = mockDisconnect; + }); + + afterEach(() => jest.clearAllMocks()); + + beforeAll( + () => + (global.MutationObserver = + mutationObserverMock as unknown as typeof MutationObserver), + ); + + afterAll(() => (global.MutationObserver = originalMutationObserver)); + + test('sets tabindex to -1 on the target elements', () => { + const { unmount } = render(); + + const targetElement = screen.getByText('Test Element'); + + expect(targetElement).toHaveAttribute('tabindex', '-1'); + + unmount(); + + expect(mockDisconnect).toHaveBeenCalled(); + }); +}); + +describe('Modal', () => { + test('applies usePreventFocusVisible and sets tabindex', async () => { + render( + {}} + size={ModalSize.MEDIUM} + title={

Test Modal Title

} + leftActions={} + rightActions={} + > +

Modal content

+
, + { wrapper: AppWrapper }, + ); + + /* eslint-disable testing-library/no-node-access */ + const modalContent = document.querySelector('.c__modal__content'); + /* eslint-enable testing-library/no-node-access */ + + await waitFor(() => { + expect(modalContent).toHaveAttribute('tabindex', '-1'); + }); + }); +}); diff --git a/src/frontend/apps/desk/src/cunningham/cunningham-style.css b/src/frontend/apps/desk/src/cunningham/cunningham-style.css index 5b5d147..01dc71b 100644 --- a/src/frontend/apps/desk/src/cunningham/cunningham-style.css +++ b/src/frontend/apps/desk/src/cunningham/cunningham-style.css @@ -484,11 +484,6 @@ input:-webkit-autofill:focus { /** * Modal */ -.c__modal:focus-visible { - outline: none; - box-shadow: none; -} - .c__modal__backdrop { z-index: 1000; } diff --git a/src/frontend/apps/desk/src/features/language/LanguagePicker.tsx b/src/frontend/apps/desk/src/features/language/LanguagePicker.tsx index 63ca5e4..d2a41c3 100644 --- a/src/frontend/apps/desk/src/features/language/LanguagePicker.tsx +++ b/src/frontend/apps/desk/src/features/language/LanguagePicker.tsx @@ -60,19 +60,17 @@ export const LanguagePicker = () => { })); }, [languages]); + /** + * @description prevent select div to receive focus on keyboard navigation so the focus goes directly to inner button + * @see https://github.com/numerique-gouv/people/pull/379 + */ useEffect(() => { if (!document) { return; } - const languagePickerDom = document.querySelector( - '.c__select-language-picker', - ); - - if (!languagePickerDom?.firstChild?.firstChild) { - return; - } - - (languagePickerDom.firstChild.firstChild as HTMLElement).tabIndex = -1; + document + .querySelector('.c__select-language-picker .c__select__wrapper') + ?.setAttribute('tabindex', '-1'); }, []); return ( diff --git a/src/frontend/apps/desk/src/features/mail-domains/components/ModalAddMailDomain.tsx b/src/frontend/apps/desk/src/features/mail-domains/components/ModalAddMailDomain.tsx index e327329..df2b58f 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/components/ModalAddMailDomain.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/components/ModalAddMailDomain.tsx @@ -1,11 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod'; -import { - Button, - Input, - Loader, - Modal, - ModalSize, -} from '@openfun/cunningham-react'; +import { Button, Input, Loader, ModalSize } from '@openfun/cunningham-react'; import { useRouter } from 'next/navigation'; import React from 'react'; import { Controller, FormProvider, useForm } from 'react-hook-form'; @@ -13,6 +7,7 @@ import { useTranslation } from 'react-i18next'; import { z } from 'zod'; import { Box, Text, TextErrors } from '@/components'; +import { Modal } from '@/components/Modal'; import { useCreateMailDomain } from '@/features/mail-domains'; import { default as MailDomainsLogo } from '../assets/mail-domains-logo.svg'; diff --git a/src/frontend/apps/desk/src/features/mail-domains/components/forms/CreateMailboxForm.tsx b/src/frontend/apps/desk/src/features/mail-domains/components/forms/CreateMailboxForm.tsx index e8c95f8..dc0925c 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/components/forms/CreateMailboxForm.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/components/forms/CreateMailboxForm.tsx @@ -2,7 +2,6 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Button, Input, - Modal, ModalSize, VariantType, useToastProvider, @@ -19,6 +18,7 @@ import { createGlobalStyle } from 'styled-components'; import { z } from 'zod'; import { Box, Text, TextErrors } from '@/components'; +import { Modal } from '@/components/Modal'; import { CreateMailboxParams, useCreateMailbox } from '../../api'; import { MailDomain } from '../../types'; diff --git a/src/frontend/apps/desk/src/features/teams/member-add/components/ModalAddMembers.tsx b/src/frontend/apps/desk/src/features/teams/member-add/components/ModalAddMembers.tsx index 0dfc899..6c0f6f4 100644 --- a/src/frontend/apps/desk/src/features/teams/member-add/components/ModalAddMembers.tsx +++ b/src/frontend/apps/desk/src/features/teams/member-add/components/ModalAddMembers.tsx @@ -1,6 +1,5 @@ import { Button, - Modal, ModalSize, VariantType, useToastProvider, @@ -11,6 +10,7 @@ import { createGlobalStyle } from 'styled-components'; import { APIError } from '@/api'; import { Box, Text } from '@/components'; +import { Modal } from '@/components/Modal'; import { useCunninghamTheme } from '@/cunningham'; import { ChooseRole } from '@/features/teams/member-management'; import { Role, Team } from '@/features/teams/team-management'; diff --git a/src/frontend/apps/desk/src/features/teams/member-management/components/ModalDelete.tsx b/src/frontend/apps/desk/src/features/teams/member-management/components/ModalDelete.tsx index 92b436e..cd8e42a 100644 --- a/src/frontend/apps/desk/src/features/teams/member-management/components/ModalDelete.tsx +++ b/src/frontend/apps/desk/src/features/teams/member-management/components/ModalDelete.tsx @@ -1,6 +1,5 @@ import { Button, - Modal, ModalSize, VariantType, useToastProvider, @@ -10,6 +9,7 @@ import { useRouter } from 'next/navigation'; import IconUser from '@/assets/icons/icon-user.svg'; import { Box, Text, TextErrors } from '@/components'; +import { Modal } from '@/components/Modal'; import { useCunninghamTheme } from '@/cunningham'; import { Role, Team } from '@/features/teams/team-management'; diff --git a/src/frontend/apps/desk/src/features/teams/member-management/components/ModalRole.tsx b/src/frontend/apps/desk/src/features/teams/member-management/components/ModalRole.tsx index e7729d7..0d970cf 100644 --- a/src/frontend/apps/desk/src/features/teams/member-management/components/ModalRole.tsx +++ b/src/frontend/apps/desk/src/features/teams/member-management/components/ModalRole.tsx @@ -1,6 +1,5 @@ import { Button, - Modal, ModalSize, VariantType, useToastProvider, @@ -9,6 +8,7 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Box, Text, TextErrors } from '@/components'; +import { Modal } from '@/components/Modal'; import { Role } from '@/features/teams/team-management'; import { useUpdateTeamAccess } from '../api/useUpdateTeamAccess'; diff --git a/src/frontend/apps/desk/src/features/teams/team-management/components/ModalRemoveTeam.tsx b/src/frontend/apps/desk/src/features/teams/team-management/components/ModalRemoveTeam.tsx index 9b1ae5c..dcfd70e 100644 --- a/src/frontend/apps/desk/src/features/teams/team-management/components/ModalRemoveTeam.tsx +++ b/src/frontend/apps/desk/src/features/teams/team-management/components/ModalRemoveTeam.tsx @@ -1,6 +1,5 @@ import { Button, - Modal, ModalSize, VariantType, useToastProvider, @@ -10,6 +9,7 @@ import { useRouter } from 'next/navigation'; import IconGroup from '@/assets/icons/icon-group.svg'; import { Box, Text, TextErrors } from '@/components'; +import { Modal } from '@/components/Modal'; import useCunninghamTheme from '@/cunningham/useCunninghamTheme'; import { useRemoveTeam } from '../api/useRemoveTeam'; diff --git a/src/frontend/apps/desk/src/features/teams/team-management/components/ModalUpdateTeam.tsx b/src/frontend/apps/desk/src/features/teams/team-management/components/ModalUpdateTeam.tsx index 1aba02e..992eee0 100644 --- a/src/frontend/apps/desk/src/features/teams/team-management/components/ModalUpdateTeam.tsx +++ b/src/frontend/apps/desk/src/features/teams/team-management/components/ModalUpdateTeam.tsx @@ -1,6 +1,5 @@ import { Button, - Modal, ModalSize, VariantType, useToastProvider, @@ -9,6 +8,7 @@ import { t } from 'i18next'; import { useState } from 'react'; import { Box, Text } from '@/components'; +import { Modal } from '@/components/Modal'; import useCunninghamTheme from '@/cunningham/useCunninghamTheme'; import { useUpdateTeam } from '../api';