Dialogs: the profile card, room search and the three events panes are built from Modal (own heading supported); one scrim, one Escape stack (#315)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>, failed: string): void => {
|
||||
@@ -127,9 +124,8 @@ export function EventsPane(): JSX.Element | null {
|
||||
]
|
||||
|
||||
const n = events?.length ?? 0
|
||||
return createPortal(
|
||||
<div className="modal-scrim" onClick={() => store.closeEvents()}>
|
||||
<div className="events-pane" onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Events">
|
||||
return (
|
||||
<Modal title="Events" className="events-pane" onClose={() => store.closeEvents()} header={
|
||||
<div className="events-pane-head">
|
||||
<Icon name="calendar_month" size={22} />
|
||||
<span className="events-pane-count">
|
||||
@@ -142,6 +138,7 @@ export function EventsPane(): JSX.Element | null {
|
||||
<span className="events-pane-spacer" />
|
||||
<IconButton name="close" title="Close" onClick={() => store.closeEvents()} />
|
||||
</div>
|
||||
}>
|
||||
|
||||
<div className="events-pane-list">
|
||||
{events === null && <p className="small muted">Loading…</p>}
|
||||
@@ -204,11 +201,9 @@ export function EventsPane(): JSX.Element | null {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{menu && <ContextMenu x={menu.x} y={menu.y} entries={menuFor(menu.event)} onClose={() => setMenu(null)} />}
|
||||
{sharing && <ShareEvent accountId={pane.accountId} event={sharing} onClose={() => setSharing(null)} />}
|
||||
</div>,
|
||||
document.body
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
</label>
|
||||
)
|
||||
|
||||
return createPortal(
|
||||
<div className="modal-scrim" onClick={() => store.closeEventCreate()}>
|
||||
<div className="events-pane event-create" onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Create an event">
|
||||
return (
|
||||
<Modal title="Create an event" className="events-pane event-create" onClose={() => store.closeEventCreate()} header={
|
||||
<div className="events-pane-head">
|
||||
<Icon name="calendar_add_on" size={22} />
|
||||
<span className="events-pane-count">Create an event</span>
|
||||
<span className="events-pane-spacer" />
|
||||
<IconButton name="close" title="Close" onClick={() => store.closeEventCreate()} />
|
||||
</div>
|
||||
}>
|
||||
|
||||
<div className="event-create-body">
|
||||
<div className="event-create-section">
|
||||
@@ -429,9 +422,7 @@ export function EventCreatePanel(): JSX.Element | null {
|
||||
{sending ? 'Creating…' : 'Create Event'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="modal-scrim share-event-backdrop" onClick={onClose}>
|
||||
<div className="events-pane share-event" onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Invite friends to event">
|
||||
<Modal title="Invite friends to event" className="events-pane share-event" onClose={onClose} header={
|
||||
<div className="share-event-head">
|
||||
<div>
|
||||
<div className="events-pane-count">Invite friends to event</div>
|
||||
@@ -546,6 +535,7 @@ function ShareEvent({
|
||||
<span className="events-pane-spacer" />
|
||||
<IconButton name="close" title="Close" onClick={onClose} />
|
||||
</div>
|
||||
}>
|
||||
<div className="emoji-search share-event-search">
|
||||
<Icon name="search" size={16} />
|
||||
<input autoFocus placeholder="Search for friends" value={query} onChange={(e) => setQuery(e.target.value)} />
|
||||
@@ -576,7 +566,6 @@ function ShareEvent({
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
>
|
||||
<div className="dialog-head">
|
||||
{icon && <Icon name={icon} size={18} color={iconColor} />}
|
||||
<span className="dialog-title ellipsis">{title}</span>
|
||||
<IconButton name="close" size={16} title="Close" onClick={onClose} />
|
||||
</div>
|
||||
{header ?? (
|
||||
<div className="dialog-head">
|
||||
{icon && <Icon name={icon} size={18} color={iconColor} />}
|
||||
<span className="dialog-title ellipsis">{title}</span>
|
||||
<IconButton name="close" size={16} title="Close" onClick={onClose} />
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>,
|
||||
|
||||
@@ -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(
|
||||
<div className="modal-scrim" onClick={() => store.closeProfile()}>
|
||||
<div className="profile-card" role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}>
|
||||
return (
|
||||
<Modal title={profile.name} className="profile-card" onClose={() => store.closeProfile()} header={
|
||||
<div className="profile-head">
|
||||
<Avatar name={profile.name} url={profile.avatarUrl} size={40} status={profile.status} accountId={profile.accountId} />
|
||||
<div className="profile-title">
|
||||
@@ -50,6 +46,7 @@ export function ProfileCard(): JSX.Element | null {
|
||||
</div>
|
||||
<IconButton name="close" title="Close" onClick={() => store.closeProfile()} />
|
||||
</div>
|
||||
}>
|
||||
|
||||
{/* 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 {
|
||||
<Icon name="info" size={14} /> {profile.service} keeps nothing else about them.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
<div className="modal-scrim" onClick={onClose}>
|
||||
<div
|
||||
className="room-search"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Find a room"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
return (
|
||||
<Modal title="Find a room" className="room-search" onClose={onClose} header={
|
||||
<div className="room-search-head">
|
||||
<Icon name="search" size={18} />
|
||||
<input
|
||||
@@ -323,6 +314,7 @@ export function RoomSearch({ account, onClose }: { account: Account; onClose: ()
|
||||
/>
|
||||
<IconButton name="close" title="Close" onClick={onClose} />
|
||||
</div>
|
||||
}>
|
||||
|
||||
{/* 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: ()
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user