diff --git a/src/frontend/src/features/rooms/livekit/components/Admin.tsx b/src/frontend/src/features/rooms/livekit/components/Admin.tsx index 7697346a..3ebaea9e 100644 --- a/src/frontend/src/features/rooms/livekit/components/Admin.tsx +++ b/src/frontend/src/features/rooms/livekit/components/Admin.tsx @@ -13,11 +13,13 @@ import { usePublishSourcesManager } from '@/features/rooms/livekit/hooks/usePubl import { useSidePanel } from '../hooks/useSidePanel' import { useRestoreFocus } from '@/hooks/useRestoreFocus' import { useSidePanelRef } from '../hooks/useSidePanelRef' +import { useSidePanelTriggers } from '../hooks/useSidePanelTriggers' export const Admin = () => { const { t } = useTranslation('rooms', { keyPrefix: 'admin' }) const { isAdminOpen } = useSidePanel() const panelRef = useSidePanelRef() + const { getTrigger } = useSidePanelTriggers() const { roomId } = useParams() @@ -46,11 +48,7 @@ export const Admin = () => { // Restore focus to the element that opened the Admin panel useRestoreFocus(isAdminOpen, { resolveTrigger: (activeEl) => { - // Find the Admin toggle button - it doesn't have a data-attr, so we'll search by aria-label - const adminButton = Array.from( - document.querySelectorAll('button') - ).find((btn) => btn.getAttribute('aria-label')?.includes('admin')) - return adminButton || activeEl + return getTrigger('admin') ?? activeEl }, // Focus the first focusable element when the panel opens (first Field switch) onOpened: () => { @@ -58,9 +56,8 @@ export const Admin = () => { const panel = panelRef.current if (panel) { // Find the first switch in the moderation section - const firstSwitch = panel.querySelector( - '[role="switch"]:first-of-type' - ) + const firstSwitch = + panel.querySelector('[role="switch"]') if (firstSwitch) { firstSwitch.focus({ preventScroll: true }) } diff --git a/src/frontend/src/features/rooms/livekit/components/AdminToggle.tsx b/src/frontend/src/features/rooms/livekit/components/AdminToggle.tsx index 047fc5f3..8b25bd79 100644 --- a/src/frontend/src/features/rooms/livekit/components/AdminToggle.tsx +++ b/src/frontend/src/features/rooms/livekit/components/AdminToggle.tsx @@ -1,3 +1,4 @@ +import { useCallback } from 'react' import { ToggleButton } from '@/primitives' import { RiAdminLine } from '@remixicon/react' import { useTranslation } from 'react-i18next' @@ -5,6 +6,7 @@ import { css } from '@/styled-system/css' import { ToggleButtonProps } from '@/primitives/ToggleButton' import { useIsAdminOrOwner } from '../hooks/useIsAdminOrOwner' import { useSidePanel } from '../hooks/useSidePanel' +import { useSidePanelTriggers } from '../hooks/useSidePanelTriggers' export const AdminToggle = ({ variant = 'primaryTextDark', @@ -14,10 +16,17 @@ export const AdminToggle = ({ const { t } = useTranslation('rooms', { keyPrefix: 'controls.admin' }) const { isAdminOpen, toggleAdmin } = useSidePanel() + const { setTrigger } = useSidePanelTriggers() const tooltipLabel = isAdminOpen ? 'open' : 'closed' + const setAdminTriggerRef = useCallback( + (el: HTMLElement | null) => { + setTrigger('admin', el) + }, + [setTrigger] + ) const hasAdminAccess = useIsAdminOrOwner() - if (!hasAdminAccess) return + if (!hasAdminAccess) return null return (
{ toggleAdmin() onPress?.(e) diff --git a/src/frontend/src/features/rooms/livekit/components/Info.tsx b/src/frontend/src/features/rooms/livekit/components/Info.tsx index 7b63ce8d..08d3c1ff 100644 --- a/src/frontend/src/features/rooms/livekit/components/Info.tsx +++ b/src/frontend/src/features/rooms/livekit/components/Info.tsx @@ -12,11 +12,13 @@ import { useCopyRoomToClipboard } from '../hooks/useCopyRoomToClipboard' import { useSidePanel } from '../hooks/useSidePanel' import { useRestoreFocus } from '@/hooks/useRestoreFocus' import { useSidePanelRef } from '../hooks/useSidePanelRef' +import { useSidePanelTriggers } from '../hooks/useSidePanelTriggers' export const Info = () => { const { t } = useTranslation('rooms', { keyPrefix: 'info' }) const { isInfoOpen } = useSidePanel() const panelRef = useSidePanelRef() + const { getTrigger } = useSidePanelTriggers() const data = useRoomData() const roomUrl = getRouteUrl('room', data?.slug) @@ -31,17 +33,14 @@ export const Info = () => { // Restore focus to the element that opened the Info panel useRestoreFocus(isInfoOpen, { - resolveTrigger: () => { - // Find the Info toggle button - return document.querySelector('[data-attr*="controls-info"]') - }, + resolveTrigger: (activeEl) => getTrigger('info') ?? activeEl, // Focus the first focusable element when the panel opens onOpened: () => { requestAnimationFrame(() => { const panel = panelRef.current if (panel) { const firstButton = panel.querySelector( - '[data-attr="copy-info-sidepannel"]' + '[data-attr="copy-info-sidepanel"]' ) if (firstButton) { firstButton.focus({ preventScroll: true }) @@ -100,7 +99,7 @@ export const Info = () => { variant={isCopied ? 'success' : 'tertiaryText'} aria-label={t('roomInformation.button.ariaLabel')} onPress={copyRoomToClipboard} - data-attr="copy-info-sidepannel" + data-attr="copy-info-sidepanel" style={{ marginLeft: '-8px', }} diff --git a/src/frontend/src/features/rooms/livekit/components/SidePanel.tsx b/src/frontend/src/features/rooms/livekit/components/SidePanel.tsx index 5f40871b..f52c1fa4 100644 --- a/src/frontend/src/features/rooms/livekit/components/SidePanel.tsx +++ b/src/frontend/src/features/rooms/livekit/components/SidePanel.tsx @@ -13,7 +13,6 @@ import { Effects } from './effects/Effects' import { Admin } from './Admin' import { Tools } from './Tools' import { Info } from './Info' -import { SidePanelProvider } from '../contexts/SidePanelContext' import { useSidePanelRef } from '../hooks/useSidePanelRef' import { HStack } from '@/styled-system/jsx' @@ -173,6 +172,8 @@ const SidePanelContent = () => { onBack={() => (layoutStore.activeSubPanelId = null)} panelRef={panelRef} > + {/* keepAlive preserves focus restoration + state (e.g. scroll/input) across panels; + revisit if memory becomes a concern */} @@ -196,9 +197,5 @@ const SidePanelContent = () => { } export const SidePanel = () => { - return ( - - - - ) + return } diff --git a/src/frontend/src/features/rooms/livekit/components/Tools.tsx b/src/frontend/src/features/rooms/livekit/components/Tools.tsx index c048de08..522e6b7e 100644 --- a/src/frontend/src/features/rooms/livekit/components/Tools.tsx +++ b/src/frontend/src/features/rooms/livekit/components/Tools.tsx @@ -6,6 +6,7 @@ import { ReactNode } from 'react' import { SubPanelId, useSidePanel } from '../hooks/useSidePanel' import { useRestoreFocus } from '@/hooks/useRestoreFocus' import { useSidePanelRef } from '../hooks/useSidePanelRef' +import { useSidePanelTriggers } from '../hooks/useSidePanelTriggers' import { useIsRecordingModeEnabled, RecordingMode, @@ -100,6 +101,7 @@ export const Tools = () => { useSidePanel() const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' }) const panelRef = useSidePanelRef() + const { getTrigger } = useSidePanelTriggers() // Restore focus to the element that opened the Tools panel // following the same pattern as Chat. @@ -108,10 +110,10 @@ export const Tools = () => { // find the "more options" button ("Plus d'options") that opened the menu resolveTrigger: (activeEl) => { if (activeEl?.tagName === 'DIV') { - return document.querySelector('#room-options-trigger') + return getTrigger('options') ?? activeEl } // For direct button clicks (e.g. "Plus d'outils"), use the active element as is - return activeEl + return getTrigger('tools') ?? activeEl }, // Focus the first focusable element when the panel opens onOpened: () => { diff --git a/src/frontend/src/features/rooms/livekit/components/controls/InfoToggle.tsx b/src/frontend/src/features/rooms/livekit/components/controls/InfoToggle.tsx index 2f61921b..8578665d 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/InfoToggle.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/InfoToggle.tsx @@ -1,9 +1,11 @@ +import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { RiInformationLine } from '@remixicon/react' import { css } from '@/styled-system/css' import { ToggleButton } from '@/primitives' import { useSidePanel } from '../../hooks/useSidePanel' import { ToggleButtonProps } from '@/primitives/ToggleButton' +import { useSidePanelTriggers } from '../../hooks/useSidePanelTriggers' export const InfoToggle = ({ onPress, @@ -12,7 +14,14 @@ export const InfoToggle = ({ const { t } = useTranslation('rooms', { keyPrefix: 'controls.info' }) const { isInfoOpen, toggleInfo } = useSidePanel() + const { setTrigger } = useSidePanelTriggers() const tooltipLabel = isInfoOpen ? 'open' : 'closed' + const setInfoTriggerRef = useCallback( + (el: HTMLElement | null) => { + setTrigger('info', el) + }, + [setTrigger] + ) return (
{ toggleInfo() onPress?.(e) diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsButton.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsButton.tsx index 715dcd30..c5c56a68 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsButton.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsButton.tsx @@ -1,10 +1,19 @@ +import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { RiMoreFill } from '@remixicon/react' import { Button, Menu } from '@/primitives' import { OptionsMenuItems } from './OptionsMenuItems' +import { useSidePanelTriggers } from '../../../hooks/useSidePanelTriggers' export const OptionsButton = () => { const { t } = useTranslation('rooms') + const { setTrigger } = useSidePanelTriggers() + const setOptionsTriggerRef = useCallback( + (el: HTMLElement | null) => { + setTrigger('options', el) + }, + [setTrigger] + ) return ( @@ -14,6 +23,7 @@ export const OptionsButton = () => { variant="primaryDark" aria-label={t('options.buttonLabel')} tooltip={t('options.buttonLabel')} + ref={setOptionsTriggerRef} > diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList.tsx index a0c3d13a..b41a024f 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList.tsx @@ -70,6 +70,7 @@ export function ParticipantsCollapsableList({ setIsOpen(!isOpen)} style={{ borderRadius: !isOpen ? '7px' : undefined, diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx index 1ab637d3..b8e302d9 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx @@ -15,12 +15,14 @@ import { MuteEveryoneButton } from './MuteEveryoneButton' import { useSidePanel } from '../../../hooks/useSidePanel' import { useRestoreFocus } from '@/hooks/useRestoreFocus' import { useSidePanelRef } from '../../../hooks/useSidePanelRef' +import { useSidePanelTriggers } from '../../../hooks/useSidePanelTriggers' // TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short. export const ParticipantsList = () => { const { t } = useTranslation('rooms', { keyPrefix: 'participants' }) const { isParticipantsOpen } = useSidePanel() const panelRef = useSidePanelRef() + const { getTrigger } = useSidePanelTriggers() // Preferred using the 'useParticipants' hook rather than the separate remote and local hooks, // because the 'useLocalParticipant' hook does not update the participant's information when their @@ -56,12 +58,7 @@ export const ParticipantsList = () => { // Restore focus to the element that opened the Participants panel useRestoreFocus(isParticipantsOpen, { resolveTrigger: (activeEl) => { - // Find the Participants toggle button - return ( - document.querySelector( - '[data-attr*="controls-participants"]' - ) || activeEl - ) + return getTrigger('participants') ?? activeEl }, // Focus the first focusable element when the panel opens onOpened: () => { @@ -71,10 +68,8 @@ export const ParticipantsList = () => { const panel = panelRef.current if (panel) { // Find the first ToggleHeader (collapsable list header) in the participants panel - // Look for buttons with aria-label containing "liste" (list headers) - // Exclude close/back buttons const firstListHeader = panel.querySelector( - 'button[aria-label*="liste"]:not([aria-label*="Masquer les participants"])' + 'button[data-focus-target="list-header"]' ) firstListHeader?.focus({ preventScroll: true }) } diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsToggle.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsToggle.tsx index e9e7eb1f..dbb17055 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsToggle.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsToggle.tsx @@ -1,3 +1,4 @@ +import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { RiGroupLine, RiInfinityLine } from '@remixicon/react' import { ToggleButton } from '@/primitives' @@ -6,6 +7,7 @@ import { css } from '@/styled-system/css' import { useParticipants } from '@livekit/components-react' import { useSidePanel } from '../../../hooks/useSidePanel' import { ToggleButtonProps } from '@/primitives/ToggleButton' +import { useSidePanelTriggers } from '../../../hooks/useSidePanelTriggers' export const ParticipantsToggle = ({ onPress, @@ -24,8 +26,15 @@ export const ParticipantsToggle = ({ numParticipants && numParticipants > 0 ? numParticipants : 1 const { isParticipantsOpen, toggleParticipants } = useSidePanel() + const { setTrigger } = useSidePanelTriggers() const tooltipLabel = isParticipantsOpen ? 'open' : 'closed' + const setParticipantsTriggerRef = useCallback( + (el: HTMLElement | null) => { + setTrigger('participants', el) + }, + [setTrigger] + ) return (
{ toggleParticipants() onPress?.(e) diff --git a/src/frontend/src/features/rooms/livekit/components/controls/ToolsToggle.tsx b/src/frontend/src/features/rooms/livekit/components/controls/ToolsToggle.tsx index 782377c1..d676713d 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/ToolsToggle.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/ToolsToggle.tsx @@ -1,9 +1,11 @@ +import { useCallback } from 'react' import { ToggleButton } from '@/primitives' import { RiShapesLine } from '@remixicon/react' import { useTranslation } from 'react-i18next' import { useSidePanel } from '../../hooks/useSidePanel' import { css } from '@/styled-system/css' import { ToggleButtonProps } from '@/primitives/ToggleButton' +import { useSidePanelTriggers } from '../../hooks/useSidePanelTriggers' export const ToolsToggle = ({ variant = 'primaryTextDark', @@ -13,7 +15,14 @@ export const ToolsToggle = ({ const { t } = useTranslation('rooms', { keyPrefix: 'controls.tools' }) const { isToolsOpen, toggleTools } = useSidePanel() + const { setTrigger } = useSidePanelTriggers() const tooltipLabel = isToolsOpen ? 'open' : 'closed' + const setToolsTriggerRef = useCallback( + (el: HTMLElement | null) => { + setTrigger('tools', el) + }, + [setTrigger] + ) return (
{ toggleTools() onPress?.(e) diff --git a/src/frontend/src/features/rooms/livekit/components/effects/Effects.tsx b/src/frontend/src/features/rooms/livekit/components/effects/Effects.tsx index cfa9eaea..4c6f0ac0 100644 --- a/src/frontend/src/features/rooms/livekit/components/effects/Effects.tsx +++ b/src/frontend/src/features/rooms/livekit/components/effects/Effects.tsx @@ -8,6 +8,7 @@ import { TrackSource } from '@livekit/protocol' import { useSidePanel } from '../../hooks/useSidePanel' import { useRestoreFocus } from '@/hooks/useRestoreFocus' import { useSidePanelRef } from '../../hooks/useSidePanelRef' +import { useSidePanelTriggers } from '../../hooks/useSidePanelTriggers' export const Effects = () => { const { cameraTrack } = useLocalParticipant() @@ -15,13 +16,14 @@ export const Effects = () => { const { saveProcessorSerialized } = usePersistentUserChoices() const { isEffectsOpen } = useSidePanel() const panelRef = useSidePanelRef() + const { getTrigger } = useSidePanelTriggers() const canPublishCamera = useCanPublishTrack(TrackSource.CAMERA) useRestoreFocus(isEffectsOpen, { resolveTrigger: (activeEl) => { if (activeEl?.tagName === 'DIV') { - return document.querySelector('#room-options-trigger') + return getTrigger('options') ?? activeEl } // For direct button clicks, use the active element as is return activeEl diff --git a/src/frontend/src/features/rooms/livekit/contexts/SidePanelContext.tsx b/src/frontend/src/features/rooms/livekit/contexts/SidePanelContext.tsx index 1d39f778..3c991d17 100644 --- a/src/frontend/src/features/rooms/livekit/contexts/SidePanelContext.tsx +++ b/src/frontend/src/features/rooms/livekit/contexts/SidePanelContext.tsx @@ -1,11 +1,27 @@ import { useRef, ReactNode } from 'react' -import { SidePanelContext } from './sidePanelContextValue' +import { SidePanelContext, SidePanelTriggerKey } from './sidePanelContextValue' export const SidePanelProvider = ({ children }: { children: ReactNode }) => { const panelRef = useRef(null) + const triggersRef = useRef>({ + participants: null, + tools: null, + info: null, + admin: null, + options: null, + effects: null, + }) + + const setTrigger = (key: SidePanelTriggerKey, el: HTMLElement | null) => { + triggersRef.current[key] = el + } + + const getTrigger = (key: SidePanelTriggerKey) => { + return triggersRef.current[key] ?? null + } return ( - + {children} ) diff --git a/src/frontend/src/features/rooms/livekit/contexts/sidePanelContextValue.ts b/src/frontend/src/features/rooms/livekit/contexts/sidePanelContextValue.ts index 15b87ec0..65b9cf6a 100644 --- a/src/frontend/src/features/rooms/livekit/contexts/sidePanelContextValue.ts +++ b/src/frontend/src/features/rooms/livekit/contexts/sidePanelContextValue.ts @@ -1,7 +1,17 @@ import { createContext } from 'react' +export type SidePanelTriggerKey = + | 'participants' + | 'tools' + | 'info' + | 'admin' + | 'options' + | 'effects' + export type SidePanelContextValue = { panelRef: React.RefObject + setTrigger: (key: SidePanelTriggerKey, el: HTMLElement | null) => void + getTrigger: (key: SidePanelTriggerKey) => HTMLElement | null } export const SidePanelContext = createContext( diff --git a/src/frontend/src/features/rooms/livekit/hooks/useSidePanelTriggers.ts b/src/frontend/src/features/rooms/livekit/hooks/useSidePanelTriggers.ts new file mode 100644 index 00000000..9a2b9562 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/hooks/useSidePanelTriggers.ts @@ -0,0 +1,15 @@ +import { useContext } from 'react' +import { SidePanelContext } from '../contexts/sidePanelContextValue' + +export const useSidePanelTriggers = () => { + const context = useContext(SidePanelContext) + if (!context) { + throw new Error( + 'useSidePanelTriggers must be used within SidePanelProvider' + ) + } + return { + setTrigger: context.setTrigger, + getTrigger: context.getTrigger, + } +} diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx index 2841dfbc..3e24a48f 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx @@ -27,6 +27,7 @@ import { FocusLayout } from '../components/FocusLayout' import { ParticipantTile } from '../components/ParticipantTile' import { SidePanel } from '../components/SidePanel' import { useSidePanel } from '../hooks/useSidePanel' +import { SidePanelProvider } from '../contexts/SidePanelContext' import { RecordingProvider } from '@/features/recording' import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal' import { useConnectionObserver } from '../hooks/useConnectionObserver' @@ -259,75 +260,77 @@ export function VideoConference({ ...props }: VideoConferenceProps) { value={layoutContext} // onPinChange={handleFocusStateChange} > - setIsShareErrorVisible(false)} - /> - -
- -
- {!focusTrack ? ( -
- - - -
- ) : ( -
- - + + setIsShareErrorVisible(false)} + /> + +
+ +
+ {!focusTrack ? ( +
+ - - {focusTrack && } - -
- )} -
-
- - -
- { - console.error(e) - if ( - e.source == Track.Source.ScreenShare && - e.error.toString() == - 'NotAllowedError: Permission denied by system' - ) { - setIsShareErrorVisible(true) - } - }} - /> - + +
+ ) : ( +
+ + + + + {focusTrack && } + +
+ )} +
+
+ + +
+ { + console.error(e) + if ( + e.source == Track.Source.ScreenShare && + e.error.toString() == + 'NotAllowedError: Permission denied by system' + ) { + setIsShareErrorVisible(true) + } + }} + /> + + )} diff --git a/src/frontend/src/hooks/useRestoreFocus.ts b/src/frontend/src/hooks/useRestoreFocus.ts index da5cd5cb..14992d10 100644 --- a/src/frontend/src/hooks/useRestoreFocus.ts +++ b/src/frontend/src/hooks/useRestoreFocus.ts @@ -58,20 +58,20 @@ export function useRestoreFocus( trigger.focus({ preventScroll }) // Only show focus ring if last interaction was keyboard (like native :focus-visible) if (lastInteractionRef.current === 'keyboard') { - trigger.setAttribute('data-focus-visible', '') - // Remove focus ring when focus moves to another element - const handleFocusChange = (e: FocusEvent) => { - if (e.target !== trigger && document.contains(trigger)) { - trigger.removeAttribute('data-focus-visible') + trigger.setAttribute('data-restore-focus-visible', '') + // Remove focus ring only when the trigger loses focus + const handleBlur = () => { + if (document.contains(trigger)) { + trigger.removeAttribute('data-restore-focus-visible') } } - document.addEventListener('focusin', handleFocusChange, { - once: true, - }) + trigger.addEventListener('blur', handleBlur, { once: true }) // Store cleanup for unmount case + cleanupRef.current?.() cleanupRef.current = () => { + trigger.removeEventListener('blur', handleBlur) if (document.contains(trigger)) { - trigger.removeAttribute('data-focus-visible') + trigger.removeAttribute('data-restore-focus-visible') } } } diff --git a/src/frontend/src/styles/index.css b/src/frontend/src/styles/index.css index 906159af..2e2835d9 100644 --- a/src/frontend/src/styles/index.css +++ b/src/frontend/src/styles/index.css @@ -23,11 +23,20 @@ body, } [data-rac][data-focus-visible]:not(label, .react-aria-Select), +[data-rac][data-restore-focus-visible]:not(label, .react-aria-Select), :is(a, button, input[type='text'], select, textarea):not( [data-rac] ):focus-visible, /* Show focus ring when data-focus-visible is set programmatically (e.g., when restoring focus) */ -[data-focus-visible]:is(a, button, input[type='text'], select, textarea):focus { +[data-focus-visible]:is(a, button, input[type='text'], select, textarea):focus, +/* Show focus ring when restoring focus on react-aria buttons without overriding its focus state */ +[data-restore-focus-visible]:is( + a, + button, + input[type='text'], + select, + textarea + ):focus { outline: 2px solid var(--colors-focus-ring); outline-offset: 1px; }