diff --git a/src/renderer/src/app.css b/src/renderer/src/app.css index 7c719f8..dceaf46 100644 --- a/src/renderer/src/app.css +++ b/src/renderer/src/app.css @@ -8945,11 +8945,6 @@ video.lightbox-media { background: var(--surface-container); } -/* "Invite friends to event": over the events pane. */ -.share-event-backdrop { - z-index: var(--z-sheet); -} - .share-event { width: min(480px, 94vw); } diff --git a/src/renderer/src/components/DiscordEvents.tsx b/src/renderer/src/components/DiscordEvents.tsx index 6a5e06f..3036a6c 100644 --- a/src/renderer/src/components/DiscordEvents.tsx +++ b/src/renderer/src/components/DiscordEvents.tsx @@ -1,7 +1,6 @@ import { useMediaUrl } from '../lib/route' -import { useEscapeLayer } from '../lib/layers' +import { Modal } from './Modal' import { useCallback, useEffect, useMemo, useState } from 'react' -import { createPortal } from 'react-dom' import { Icon, IconButton } from './Icon' import { Avatar } from './Avatar' import { ContextMenu, type MenuEntry } from './ContextMenu' @@ -90,8 +89,6 @@ export function EventsPane(): JSX.Element | null { if (count !== undefined) load() }, [count, load]) - useEscapeLayer(() => store.closeEvents(), !!pane && !menu && !sharing) - if (!pane) return null const act = (event: DiscordEvent, method: string, params: Record, failed: string): void => { @@ -127,9 +124,8 @@ export function EventsPane(): JSX.Element | null { ] const n = events?.length ?? 0 - return createPortal( -
store.closeEvents()}> -
e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Events"> + return ( + store.closeEvents()} header={
@@ -142,6 +138,7 @@ export function EventsPane(): JSX.Element | null { store.closeEvents()} />
+ }>
{events === null &&

Loading…

} @@ -204,11 +201,9 @@ export function EventsPane(): JSX.Element | null {
))}
-
{menu && setMenu(null)} />} {sharing && setSharing(null)} />} - , - document.body + ) } @@ -270,8 +265,6 @@ export function EventCreatePanel(): JSX.Element | null { .catch(() => setChannels([])) }, [target]) - useEscapeLayer(() => store.closeEventCreate(), !!target) - if (!target) return null const offered = channels.filter((c) => (kind === 'stage' ? c.stage : !c.stage)) @@ -336,15 +329,15 @@ export function EventCreatePanel(): JSX.Element | null { ) - return createPortal( -
store.closeEventCreate()}> -
e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Create an event"> + return ( + store.closeEventCreate()} header={
Create an event store.closeEventCreate()} />
+ }>
@@ -429,9 +422,7 @@ export function EventCreatePanel(): JSX.Element | null { {sending ? 'Creating…' : 'Create Event'}
-
-
, - document.body + ) } @@ -476,7 +467,6 @@ function ShareEvent({ .catch(() => setTargets([])) }, [accountId, event.guildId, event.id]) - useEscapeLayer(onClose) const q = query.trim().toLowerCase() const shown = (targets ?? []).filter( @@ -532,8 +522,7 @@ function ShareEvent({ } return ( -
-
e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Invite friends to event"> +
Invite friends to event
@@ -546,6 +535,7 @@ function ShareEvent({
+ }>
setQuery(e.target.value)} /> @@ -576,7 +566,6 @@ function ShareEvent({ : ''}
-
-
+ ) } diff --git a/src/renderer/src/components/Modal.tsx b/src/renderer/src/components/Modal.tsx index 16fbfa0..c5c5ffd 100644 --- a/src/renderer/src/components/Modal.tsx +++ b/src/renderer/src/components/Modal.tsx @@ -18,6 +18,7 @@ export function Modal({ iconColor, onClose, className, + header, children }: { title: string @@ -25,6 +26,12 @@ export function Modal({ iconColor?: string onClose: () => void className?: string + /** + * A heading of the dialog's own, in place of the title row: for a dialog whose + * head is a person's face, or a search box. It brings its own way out; the + * title is still what a screen reader calls the dialog. + */ + header?: React.ReactNode children: React.ReactNode }): JSX.Element { useEscapeLayer(onClose) @@ -37,11 +44,13 @@ export function Modal({ aria-label={title} onClick={(e) => e.stopPropagation()} > -
- {icon && } - {title} - -
+ {header ?? ( +
+ {icon && } + {title} + +
+ )} {children} , diff --git a/src/renderer/src/components/ProfileCard.tsx b/src/renderer/src/components/ProfileCard.tsx index 4056db6..ac21533 100644 --- a/src/renderer/src/components/ProfileCard.tsx +++ b/src/renderer/src/components/ProfileCard.tsx @@ -1,5 +1,4 @@ -import { useEscapeLayer } from '../lib/layers' -import { createPortal } from 'react-dom' +import { Modal } from './Modal' import { Avatar } from './Avatar' import { Icon, IconButton } from './Icon' import { useChat, useStore } from '../state/hooks' @@ -23,8 +22,6 @@ export function ProfileCard(): JSX.Element | null { const profile = useChat((s) => s.profile) const store = useStore() - useEscapeLayer(() => store.closeProfile(), !!profile) - if (!profile) return null const rows: [string, string][] = [] @@ -36,9 +33,8 @@ export function ProfileCard(): JSX.Element | null { if (profile.idleSeconds !== undefined) rows.push(['Idle', describeIdle(profile.idleSeconds)]) for (const extra of profile.extra ?? []) rows.push([extra.label, extra.value]) - return createPortal( -
store.closeProfile()}> -
e.stopPropagation()}> + return ( + store.closeProfile()} header={
@@ -50,6 +46,7 @@ export function ProfileCard(): JSX.Element | null {
store.closeProfile()} />
+ }> {/* Their standing, said plainly. A role list is the service's own words for it - "Operator in #channel", "Admin", a subscriber @@ -129,9 +126,7 @@ export function ProfileCard(): JSX.Element | null { {profile.service} keeps nothing else about them.

)} -
-
, - document.body + ) } diff --git a/src/renderer/src/components/RoomSearch.tsx b/src/renderer/src/components/RoomSearch.tsx index 8da8dbb..94a02e5 100644 --- a/src/renderer/src/components/RoomSearch.tsx +++ b/src/renderer/src/components/RoomSearch.tsx @@ -1,7 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react' -import { useEscapeLayer } from '../lib/layers' import { ReasonPrompt } from './ReasonPrompt' -import { createPortal } from 'react-dom' +import { Modal } from './Modal' import { Avatar } from './Avatar' import { Icon, IconButton } from './Icon' import { usePref, useStore } from '../state/hooks' @@ -124,7 +123,6 @@ export function RoomSearch({ account, onClose }: { account: Account; onClose: () // for what they are actually looking at now. const generation = useRef(0) - useEscapeLayer(onClose) const servers = extraServers @@ -287,15 +285,8 @@ export function RoomSearch({ account, onClose }: { account: Account; onClose: () const rooms = (answer?.rooms ?? []).filter((room) => !disabled.has(room.via)) const morePages = Object.keys(answer?.next ?? {}).length > 0 - return createPortal( -
-
e.stopPropagation()} - > + return ( +
+ }> {/* Which servers, by name, on their own row so a dozen of them fit - and each one a switch, because "search everywhere" and "search @@ -545,8 +537,6 @@ export function RoomSearch({ account, onClose }: { account: Account; onClose: ()

)}
- - , - document.body + ) }