Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion src/components/quick-search/QuickSearchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const QuickSearchInput = ({
return (
<>
<div className="quick-search-input-container">
{!loading && <span className="material-icons">search</span>}
{!loading && <span className="material-icons" aria-hidden="true">search</span>}
{loading && (
<div>
<Spinner size="md" />
Expand Down
5 changes: 4 additions & 1 deletion src/components/share/access/AccessRoleDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type AccessRoleDropdownProps = {
roleTopMessage?: DropdownMenuProps["topMessage"];
onDelete?: () => void;
canDelete?: boolean;
"aria-label"?: string;
};

export const AccessRoleDropdown = ({
Expand All @@ -29,6 +30,7 @@ export const AccessRoleDropdown = ({
roleTopMessage,
onDelete,
canDelete = true,
"aria-label": ariaLabel,
}: AccessRoleDropdownProps) => {
const { t } = useCunningham();

Expand Down Expand Up @@ -83,14 +85,15 @@ export const AccessRoleDropdown = ({
color="brand"
variant="tertiary"
icon={
<span className="material-icons">
<span className="material-icons" aria-hidden="true">
{isOpen ? "arrow_drop_up" : "arrow_drop_down"}
</span>
}
iconPosition="right"
onClick={() => {
onOpenChange?.(!isOpen);
}}
aria-label={ariaLabel}
>
{currentRoleString?.label}
</Button>
Expand Down
106 changes: 93 additions & 13 deletions src/components/share/modal/ShareModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export const ShareModal = <UserType, InvitationType, AccessType>({
const { t } = useCunningham();
const { isMobile } = useResponsive();
const searchUserTimeoutRef = useRef<NodeJS.Timeout>(null);
const searchContainerRef = useRef<HTMLDivElement>(null);
const [listHeight, setListHeight] = useState<string>("400px");
const selectedUsersRef = useRef<HTMLDivElement>(null);
const [inputValue, setInputValue] = useState<string>("");
Expand All @@ -168,6 +169,7 @@ export const ShareModal = <UserType, InvitationType, AccessType>({
const [selectedInvitationRole, setSelectedInvitationRole] = useState<string>(
props.invitationRoles?.[0]?.value ?? ""
);
const [pendingAnnouncement, setPendingAnnouncement] = useState<string>("");

/**
* The height of the modal content
Expand Down Expand Up @@ -199,6 +201,22 @@ export const ShareModal = <UserType, InvitationType, AccessType>({
onSearchUser(str);
};

const focusSearchInput = useCallback(() => {
requestAnimationFrame(() => {
const input = searchContainerRef.current?.querySelector<HTMLInputElement>(
"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;

Expand All @@ -208,13 +226,63 @@ export const ShareModal = <UserType, InvitationType, AccessType>({
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<UserType>) => {
setPendingInvitationUsers((prev) => prev.filter((u) => u.id !== user.id));
};
const onRemoveUser = useCallback(
(user: UserData<UserType>) => {
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<UserType, InvitationType>,
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<UserType, AccessType>, 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<UserData<UserType>> = useMemo(() => {
const searchMemberResult = searchUsersResult?.filter(
Expand Down Expand Up @@ -315,7 +383,15 @@ export const ShareModal = <UserType, InvitationType, AccessType>({
closeOnClickOutside
size={isMobile ? ModalSize.FULL : ModalSize.LARGE}
>
<div className="c__share-modal no-padding">
<div className="c__share-modal no-padding" ref={searchContainerRef}>
<div
className="c__share-modal__sr-status"
role="status"
aria-live="polite"
aria-atomic="true"
>
{pendingAnnouncement}
</div>
{canUpdate && pendingInvitationUsers.length > 0 && (
<div
className="c__share-modal__selected-users"
Expand Down Expand Up @@ -392,15 +468,15 @@ export const ShareModal = <UserType, InvitationType, AccessType>({
className="c__share-modal__invitations"
data-testid="invitations-list"
>
<span className="c__share-modal__invitations-title">
<h3 className="c__share-modal__invitations-title">
{t("components.share.invitations.title")}
</span>
</h3>
{invitations.map((invitation) => (
<ShareInvitationItem
key={invitation.id}
invitation={invitation}
roles={props.invitationRoles!}
updateRole={props.onUpdateInvitation}
updateRole={handleUpdateInvitation}
deleteInvitation={props.onDeleteInvitation}
canUpdate={canUpdate}
roleTopMessage={props.invitationRoleTopMessage?.(
Expand All @@ -411,6 +487,7 @@ export const ShareModal = <UserType, InvitationType, AccessType>({
<ShowMoreButton
show={hasNextInvitations}
onShowMore={props.onLoadNextInvitations}
aria-label={t("components.share.invitations.load_more_aria")}
/>
</div>
)}
Expand All @@ -421,7 +498,7 @@ export const ShareModal = <UserType, InvitationType, AccessType>({
className="c__share-modal__members"
data-testid="members-list"
>
<span className="c__share-modal__members-title">
<h3 className="c__share-modal__members-title">
{t(
members.length > 1
? "components.share.members.title_plural"
Expand All @@ -430,7 +507,7 @@ export const ShareModal = <UserType, InvitationType, AccessType>({
count: members.length,
}
)}
</span>
</h3>
{members.map((member) => (
<ShareMemberItem
key={member.id}
Expand All @@ -441,13 +518,14 @@ export const ShareModal = <UserType, InvitationType, AccessType>({
roles={
props.getAccessRoles?.(member) ?? props.invitationRoles!
}
updateRole={props.onUpdateAccess}
updateRole={handleUpdateAccess}
deleteAccess={props.onDeleteAccess}
/>
))}
<ShowMoreButton
show={hasNextMembers}
onShowMore={props.onLoadNextMembers}
aria-label={t("components.share.members.load_more_aria")}
/>
</div>
)}
Expand Down Expand Up @@ -485,18 +563,20 @@ export const ShareModal = <UserType, InvitationType, AccessType>({
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 (
<div className="c__share-modal__show-more-button">
<Button
variant="tertiary"
size="small"
icon={<span className="material-icons">arrow_downward</span>}
icon={<span className="material-icons" aria-hidden="true">arrow_downward</span>}
onClick={onShowMore}
aria-label={ariaLabel}
>
{t("components.share.members.load_more")}
</Button>
Expand Down
2 changes: 1 addition & 1 deletion src/components/share/modal/items/SearchUserItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const SearchUserItem = <UserType,>({
right={
<div className="c__search-user-item-right">
<span>{t("components.share.item.add")}</span>
<span className="material-icons">add</span>
<span className="material-icons" aria-hidden="true">add</span>
</div>
}
/>
Expand Down
10 changes: 10 additions & 0 deletions src/components/share/modal/items/ShareInvitationItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserType, InvitationType> = {
invitation: InvitationData<UserType, InvitationType>;
Expand All @@ -27,6 +28,11 @@ export const ShareInvitationItem = <UserType, InvitationType>({
roleTopMessage,
}: ShareInvitationItemProps<UserType, InvitationType>) => {
const roleDropdown = useDropdownMenu();
const { t } = useCunningham();
const displayName = invitation.email;
const currentRoleLabel = roles.find(
(r) => r.value === invitation.role,
)?.label;

return (
<div className="c__share-member-item">
Expand All @@ -49,6 +55,10 @@ export const ShareInvitationItem = <UserType, InvitationType>({
? () => deleteInvitation(invitation)
: undefined
}
aria-label={t("components.share.access.role_label", {
name: displayName,
role: currentRoleLabel ?? "",
})}
/>
</div>
}
Expand Down
8 changes: 4 additions & 4 deletions src/components/share/modal/items/ShareLinkSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export const ShareLinkSettings = ({
}}
>
{selectedLinkReachChoice?.label}
<span className="material-icons">
<span className="material-icons" aria-hidden="true">
{linkReachDropdown.isOpen ? "arrow_drop_up" : "arrow_drop_down"}
</span>
</Button>
Expand Down Expand Up @@ -184,7 +184,7 @@ export const ShareLinkSettings = ({
variant="tertiary"
data-testid="share-link-role-dropdown-button"
icon={
<span className="material-icons">
<span className="material-icons" aria-hidden="true">
{linkRoleDropdown.isOpen
? "arrow_drop_up"
: "arrow_drop_down"}
Expand Down Expand Up @@ -216,9 +216,9 @@ export const ShareLinkSettings = ({
className="c__share-modal__link-settings"
data-testid="share-link-settings"
>
<span className="c__share-modal__link-settings__title">
<h3 className="c__share-modal__link-settings__title">
{t("components.share.linkSettings.title")}
</span>
</h3>
<div className="c__share-modal__link-settings__content">
{renderLinkReach()}
<div className="c__share-modal__link-settings__content__description desktop">
Expand Down
10 changes: 10 additions & 0 deletions src/components/share/modal/items/ShareMemberItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
useDropdownMenu,
} from ":/components/dropdown-menu";
import { AccessRoleDropdown } from "../../access/AccessRoleDropdown";
import { useCunningham } from "@gouvfr-lasuite/cunningham-react";

export type ShareMemberItemProps<UserType, AccessType> = {
accessData: AccessData<UserType, AccessType>;
Expand All @@ -28,8 +29,13 @@ export const ShareMemberItem = <UserType, AccessType>({
roleTopMessage,
}: ShareMemberItemProps<UserType, AccessType>) => {
const roleDropdown = useDropdownMenu();
const { t } = useCunningham();
const canDelete =
accessData.is_explicit !== false && accessData.can_delete !== false;
const displayName = accessData.user.full_name || accessData.user.email;
const currentRoleLabel = roles.find(
(r) => r.value === accessData[accessRoleKey],
)?.label;
return (
<div className="c__share-member-item">
<QuickSearchItemTemplate
Expand All @@ -56,6 +62,10 @@ export const ShareMemberItem = <UserType, AccessType>({
onDelete={
deleteAccess ? () => deleteAccess(accessData) : undefined
}
aria-label={t("components.share.access.role_label", {
name: displayName,
role: currentRoleLabel ?? "",
})}
/>
</div>
}
Expand Down
14 changes: 14 additions & 0 deletions src/components/share/modal/share-modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@

$md: map.get($themes, "default", "globals", "breakpoints", "md");

.c__share-modal__sr-status {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

.c__share-modal__members {
display: flex;
flex-direction: column;
Expand All @@ -28,6 +40,7 @@ $md: map.get($themes, "default", "globals", "breakpoints", "md");
.c__share-modal__invitations-title {
font-size: var(--c--globals--font--sizes--sm);
font-weight: 700;
margin-top: 0;
margin-bottom: var(--c--globals--spacings--4xs);
color: var(--c--contextuals--content--semantic--neutral--primary);
}
Expand Down Expand Up @@ -57,6 +70,7 @@ $md: map.get($themes, "default", "globals", "breakpoints", "md");
&__title {
font-size: var(--c--globals--font--sizes--sm);
font-weight: 700;
margin-top: 0;
margin-bottom: var(--c--globals--spacings--4xs);
color: var(--c--contextuals--content--semantic--neutral--primary);
}
Expand Down
Loading
Loading