diff --git a/src/components/quick-search/QuickSearchInput.tsx b/src/components/quick-search/QuickSearchInput.tsx index 67f888d5..4db03040 100644 --- a/src/components/quick-search/QuickSearchInput.tsx +++ b/src/components/quick-search/QuickSearchInput.tsx @@ -34,7 +34,7 @@ export const QuickSearchInput = ({ return ( <>
- {!loading && search} + {!loading && } {loading && (
diff --git a/src/components/share/access/AccessRoleDropdown.tsx b/src/components/share/access/AccessRoleDropdown.tsx index 98358df7..0cfbdf66 100644 --- a/src/components/share/access/AccessRoleDropdown.tsx +++ b/src/components/share/access/AccessRoleDropdown.tsx @@ -17,6 +17,7 @@ type AccessRoleDropdownProps = { roleTopMessage?: DropdownMenuProps["topMessage"]; onDelete?: () => void; canDelete?: boolean; + "aria-label"?: string; }; export const AccessRoleDropdown = ({ @@ -29,6 +30,7 @@ export const AccessRoleDropdown = ({ roleTopMessage, onDelete, canDelete = true, + "aria-label": ariaLabel, }: AccessRoleDropdownProps) => { const { t } = useCunningham(); @@ -83,7 +85,7 @@ export const AccessRoleDropdown = ({ color="brand" variant="tertiary" icon={ - + } @@ -91,6 +93,7 @@ export const AccessRoleDropdown = ({ onClick={() => { onOpenChange?.(!isOpen); }} + aria-label={ariaLabel} > {currentRoleString?.label} diff --git a/src/components/share/modal/ShareModal.tsx b/src/components/share/modal/ShareModal.tsx index 115bdaa4..8ce56ff5 100644 --- a/src/components/share/modal/ShareModal.tsx +++ b/src/components/share/modal/ShareModal.tsx @@ -158,6 +158,7 @@ export const ShareModal = ({ const { t } = useCunningham(); const { isMobile } = useResponsive(); const searchUserTimeoutRef = useRef(null); + const searchContainerRef = useRef(null); const [listHeight, setListHeight] = useState("400px"); const selectedUsersRef = useRef(null); const [inputValue, setInputValue] = useState(""); @@ -168,6 +169,7 @@ export const ShareModal = ({ const [selectedInvitationRole, setSelectedInvitationRole] = useState( props.invitationRoles?.[0]?.value ?? "" ); + const [pendingAnnouncement, setPendingAnnouncement] = useState(""); /** * The height of the modal content @@ -199,6 +201,22 @@ export const ShareModal = ({ onSearchUser(str); }; + const focusSearchInput = useCallback(() => { + requestAnimationFrame(() => { + const input = searchContainerRef.current?.querySelector( + "input.quick-search-input", + ); + input?.focus(); + }); + }, []); + + // On vide puis on remet le message pour forcer l'annonce SR. + // Petit délai pour éviter qu'il soit noyé à la fermeture du menu. + const announce = useCallback((message: string) => { + setPendingAnnouncement(""); + setTimeout(() => setPendingAnnouncement(message), 250); + }, []); + const showSearchUsers = searchQuery !== "" || pendingInvitationUsers.length > 0; @@ -208,13 +226,63 @@ export const ShareModal = ({ setInputValue(""); setSearchQuery(""); props.onSearchUsers!(""); + announce( + t("components.share.user.added", { + name: user.full_name || user.email, + }), + ); + focusSearchInput(); }, - [props] + [props, focusSearchInput, t, announce] ); - const onRemoveUser = (user: UserData) => { - setPendingInvitationUsers((prev) => prev.filter((u) => u.id !== user.id)); - }; + const onRemoveUser = useCallback( + (user: UserData) => { + setPendingInvitationUsers((prev) => prev.filter((u) => u.id !== user.id)); + announce( + t("components.share.user.removed", { + name: user.full_name || user.email, + }), + ); + focusSearchInput(); + }, + [focusSearchInput, t, announce], + ); + + const handleUpdateInvitation = useCallback( + ( + invitation: InvitationData, + role: string, + ) => { + props.onUpdateInvitation?.(invitation, role); + const roleLabel = + props.invitationRoles?.find((r) => r.value === role)?.label ?? role; + announce( + t("components.share.access.role_changed", { + name: invitation.email, + role: roleLabel, + }), + ); + }, + [props, t, announce], + ); + + const handleUpdateAccess = useCallback( + (access: AccessData, role: string) => { + props.onUpdateAccess?.(access, role); + const accessRoles = + props.getAccessRoles?.(access) ?? props.invitationRoles ?? []; + const roleLabel = + accessRoles.find((r) => r.value === role)?.label ?? role; + announce( + t("components.share.access.role_changed", { + name: access.user.full_name || access.user.email, + role: roleLabel, + }), + ); + }, + [props, t, announce], + ); const usersData: QuickSearchData> = useMemo(() => { const searchMemberResult = searchUsersResult?.filter( @@ -315,7 +383,15 @@ export const ShareModal = ({ closeOnClickOutside size={isMobile ? ModalSize.FULL : ModalSize.LARGE} > -
+
+
+ {pendingAnnouncement} +
{canUpdate && pendingInvitationUsers.length > 0 && (
({ className="c__share-modal__invitations" data-testid="invitations-list" > - +

{t("components.share.invitations.title")} - +

{invitations.map((invitation) => ( ({
)} @@ -421,7 +498,7 @@ export const ShareModal = ({ className="c__share-modal__members" data-testid="members-list" > - +

{t( members.length > 1 ? "components.share.members.title_plural" @@ -430,7 +507,7 @@ export const ShareModal = ({ count: members.length, } )} - +

{members.map((member) => ( ({ roles={ props.getAccessRoles?.(member) ?? props.invitationRoles! } - updateRole={props.onUpdateAccess} + updateRole={handleUpdateAccess} deleteAccess={props.onDeleteAccess} /> ))}
)} @@ -485,9 +563,10 @@ export const ShareModal = ({ type ShowMoreButtonProps = { show: boolean; onShowMore?: () => void; + "aria-label"?: string; }; -const ShowMoreButton = ({ show, onShowMore }: ShowMoreButtonProps) => { +const ShowMoreButton = ({ show, onShowMore, "aria-label": ariaLabel }: ShowMoreButtonProps) => { const { t } = useCunningham(); if (!show) return null; return ( @@ -495,8 +574,9 @@ const ShowMoreButton = ({ show, onShowMore }: ShowMoreButtonProps) => { diff --git a/src/components/share/modal/items/SearchUserItem.tsx b/src/components/share/modal/items/SearchUserItem.tsx index 5564dfb1..e9bdb9ee 100644 --- a/src/components/share/modal/items/SearchUserItem.tsx +++ b/src/components/share/modal/items/SearchUserItem.tsx @@ -20,7 +20,7 @@ export const SearchUserItem = ({ right={
{t("components.share.item.add")} - add +
} /> diff --git a/src/components/share/modal/items/ShareInvitationItem.tsx b/src/components/share/modal/items/ShareInvitationItem.tsx index c2505e3c..8cab3fee 100644 --- a/src/components/share/modal/items/ShareInvitationItem.tsx +++ b/src/components/share/modal/items/ShareInvitationItem.tsx @@ -3,6 +3,7 @@ import { InvitationData } from "../../types"; import { UserRow } from ":/components/users/rows/UserRow"; import { DropdownMenuOption, useDropdownMenu } from ":/components/dropdown-menu"; import { AccessRoleDropdown } from "../../access/AccessRoleDropdown"; +import { useCunningham } from "@gouvfr-lasuite/cunningham-react"; export type ShareInvitationItemProps = { invitation: InvitationData; @@ -27,6 +28,11 @@ export const ShareInvitationItem = ({ roleTopMessage, }: ShareInvitationItemProps) => { const roleDropdown = useDropdownMenu(); + const { t } = useCunningham(); + const displayName = invitation.email; + const currentRoleLabel = roles.find( + (r) => r.value === invitation.role, + )?.label; return (
@@ -49,6 +55,10 @@ export const ShareInvitationItem = ({ ? () => deleteInvitation(invitation) : undefined } + aria-label={t("components.share.access.role_label", { + name: displayName, + role: currentRoleLabel ?? "", + })} />
} diff --git a/src/components/share/modal/items/ShareLinkSettings.tsx b/src/components/share/modal/items/ShareLinkSettings.tsx index 1c64054f..be5f99a3 100644 --- a/src/components/share/modal/items/ShareLinkSettings.tsx +++ b/src/components/share/modal/items/ShareLinkSettings.tsx @@ -147,7 +147,7 @@ export const ShareLinkSettings = ({ }} > {selectedLinkReachChoice?.label} - + @@ -184,7 +184,7 @@ export const ShareLinkSettings = ({ variant="tertiary" data-testid="share-link-role-dropdown-button" icon={ - +