From 89effb25097d9229ca621b776bd227ba7afcb663 Mon Sep 17 00:00:00 2001 From: Josie Date: Sat, 25 Jul 2026 22:50:02 +0100 Subject: [PATCH] Persona picker style tweaks --- .changeset/persona_picker_style_tweaks.md | 5 + src/app/components/ResponsiveMenu.tsx | 3 + .../room/persona-picker/PersonaPicker.tsx | 403 +++++++++--------- 3 files changed, 198 insertions(+), 213 deletions(-) create mode 100644 .changeset/persona_picker_style_tweaks.md diff --git a/.changeset/persona_picker_style_tweaks.md b/.changeset/persona_picker_style_tweaks.md new file mode 100644 index 000000000..c501086d5 --- /dev/null +++ b/.changeset/persona_picker_style_tweaks.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +# Persona Picker style tweaks diff --git a/src/app/components/ResponsiveMenu.tsx b/src/app/components/ResponsiveMenu.tsx index 5a6b1b07d..c9f6030ab 100644 --- a/src/app/components/ResponsiveMenu.tsx +++ b/src/app/components/ResponsiveMenu.tsx @@ -19,6 +19,7 @@ type ResponsiveMenuProps = { position?: ComponentPosition; align?: ComponentAlign; offset?: number; + alignOffset?: number; /** Set true for menus whose trigger should regain focus when they close. */ returnFocusOnDeactivate?: boolean; /** `both` also maps Left/Right, for menus laid out horizontally. */ @@ -38,6 +39,7 @@ export function ResponsiveMenu({ position = 'Bottom', align = 'End', offset, + alignOffset, returnFocusOnDeactivate = false, arrowNavigation = 'vertical', }: ResponsiveMenuProps) { @@ -93,6 +95,7 @@ export function ResponsiveMenu({ position={position} align={align} offset={offset} + alignOffset={alignOffset} content={ // Gated so a call site that builds its menu inline does that work on open, // not on every render of the trigger. diff --git a/src/app/features/room/persona-picker/PersonaPicker.tsx b/src/app/features/room/persona-picker/PersonaPicker.tsx index 0f92c2d40..55e6d3e3c 100644 --- a/src/app/features/room/persona-picker/PersonaPicker.tsx +++ b/src/app/features/room/persona-picker/PersonaPicker.tsx @@ -4,6 +4,7 @@ import { menuIcon, User as UserIcon, } from '$components/icons/phosphor'; +import { ResponsiveMenu } from '$components/ResponsiveMenu'; import { UserAvatar } from '$components/user-avatar/UserAvatar.tsx'; import { useMediaAuthentication } from '$hooks/useMediaAuthentication.ts'; import { @@ -14,10 +15,8 @@ import { getCurrentlyUsedPerMessageProfileForAccount, setCurrentlyUsedPerMessageProfileIdForAccount, } from '$hooks/usePerMessageProfile'; -import { stopPropagation } from '$utils/keyboard'; import { mxcUrlToHttp } from '$utils/matrix.ts'; import { isMobileOrTablet } from '$utils/platform'; -import FocusTrap from 'focus-trap-react'; import { nameInitials } from '$utils/common'; import { Avatar, @@ -27,7 +26,6 @@ import { Input, Menu, MenuItem, - PopOut, type RectCords, Scroll, Text, @@ -40,6 +38,10 @@ import * as css from './PersonaPicker.css.ts'; import { InfoCard } from '$components/info-card/InfoCard.tsx'; import { InfoIcon } from '@phosphor-icons/react'; +const pillStyles = { + cursor: 'pointer', +} as const; + export enum PersonaPickerTab { Global = 'Global', PerRoom = 'PerRoom', @@ -78,8 +80,6 @@ export function PersonaPicker({ const searchInputRef = useRef(null); const scrollRef = useRef(null); - const [showPersonaPicker, setShowPersonaPicker] = useState(false); - const [filteredProfiles, setFilteredProfiles] = useState( undefined ); @@ -142,224 +142,201 @@ export function PersonaPicker({ ); return ( - <> - { - // HACK: getAllPerMessageProfiles returns [] on Sable first load. - // BUG: On the third render the list returns empty in testing. - if (profiles?.length === 0) { - fetchProfiles(mx); - } - }, - onDeactivate: () => { - setAddPersonaMenuAnchor(undefined); - setShowPersonaPicker(false); - clearFilterInput(); - }, - clickOutsideDeactivates: true, - escapeDeactivates: stopPropagation, - }} + { + setAddPersonaMenuAnchor(undefined); + clearFilterInput(); + }} + menu={ + + - - + onTabChange(PersonaPickerTab.Global)} > - - onTabChange(PersonaPickerTab.Global)} - > - - Global - - - onTabChange(PersonaPickerTab.PerRoom)} - > - - Per-room - - - + + Global + + + onTabChange(PersonaPickerTab.PerRoom)} + > + + Per-room + + + - <> - + <> + - + {filteredProfiles?.map((profile) => ( + - {filteredProfiles?.map((profile) => ( - { - const isGlobal = tab === PersonaPickerTab.Global; - const selectedPersona = isGlobal - ? selectedGlobalPersona - : selectedRoomPersona; - const disabling = profile.id === selectedPersona?.id; + radii="300" + className={css.PersonaPickerMenuItem} + aria-selected={isPickerMenuItemSelected(profile)} + onClick={async () => { + const isGlobal = tab === PersonaPickerTab.Global; + const selectedPersona = isGlobal + ? selectedGlobalPersona + : selectedRoomPersona; + const disabling = profile.id === selectedPersona?.id; - if (!disabling) { - if (isGlobal) { - setSelectedGlobalPersona(profile); - await setCurrentlyUsedPerMessageProfileIdForAccount(mx, profile.id); - } else { - setSelectedRoomPersona(profile); - await setCurrentlyUsedPerMessageProfileIdForRoom( - mx, - roomId, - profile.id - ); - } - } else { - if (isGlobal) { - setSelectedGlobalPersona(null); - await setCurrentlyUsedPerMessageProfileIdForAccount( - mx, - undefined, - undefined, - true - ); - } else { - setSelectedRoomPersona(null); - await setCurrentlyUsedPerMessageProfileIdForRoom( - mx, - roomId, - undefined, - undefined, - true - ); - } - } - }} - before={ - - ( - - {nameInitials(profile.name)} - - )} - alt={`Avatar for profile ${profile.id}`} - /> - + if (!disabling) { + if (isGlobal) { + setSelectedGlobalPersona(profile); + await setCurrentlyUsedPerMessageProfileIdForAccount(mx, profile.id); + } else { + setSelectedRoomPersona(profile); + await setCurrentlyUsedPerMessageProfileIdForRoom(mx, roomId, profile.id); } + } else { + if (isGlobal) { + setSelectedGlobalPersona(null); + await setCurrentlyUsedPerMessageProfileIdForAccount( + mx, + undefined, + undefined, + true + ); + } else { + setSelectedRoomPersona(null); + await setCurrentlyUsedPerMessageProfileIdForRoom( + mx, + roomId, + undefined, + undefined, + true + ); + } + } + }} + before={ + - - {profile.name} - - - ))} - - - Message will use your per-room persona. - - ) : selectedGlobalPersona ? ( - <> - Message will use your global persona. - - ) : ( - <>No persona chosen. - ) + ( + + {nameInitials(profile.name)} + + )} + alt={`Avatar for profile ${profile.id}`} + /> + } - /> - - - - - } - /> - { - { - setShowPersonaPicker(true); - setAddPersonaMenuAnchor(evt.currentTarget.getBoundingClientRect()); - }} - onPointerDown={suppressEditorRefocus} - variant="SurfaceVariant" - size="300" - style={{ backgroundColor: 'transparent' }} - title="Switch persona" - aria-label="Switch persona" - > - {(selectedRoomPersona ?? selectedGlobalPersona) ? ( - - ( - - {nameInitials(defactoPersona()!.name)} - - )} - alt={`Avatar for profile ${defactoPersona()!.id}`} + > + + {profile.name} + + + ))} + + + Message will use your per-room persona. + + ) : selectedGlobalPersona ? ( + <> + Message will use your global persona. + + ) : ( + <>No persona chosen. + ) + } /> - - ) : ( - composerIcon(UserIcon, { weight: showPersonaPicker ? 'fill' : 'regular' }) - )} - + + + } - + > + { + // getAllPerMessageProfiles can return an empty list during initial startup. + if (profiles?.length === 0) { + fetchProfiles(mx); + } + setAddPersonaMenuAnchor(evt.currentTarget.getBoundingClientRect()); + }} + onPointerDown={suppressEditorRefocus} + variant="SurfaceVariant" + size="300" + style={{ backgroundColor: 'transparent' }} + title="Switch persona" + aria-label="Switch persona" + > + {(selectedRoomPersona ?? selectedGlobalPersona) ? ( + + ( + + {nameInitials(defactoPersona()!.name)} + + )} + alt={`Avatar for profile ${defactoPersona()!.id}`} + /> + + ) : ( + composerIcon(UserIcon, { weight: AddPersonaMenuAnchor ? 'fill' : 'regular' }) + )} + + ); }