Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
3.8 kB · 86 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687import { useEffect, useRef, type KeyboardEvent, type ReactNode } from 'react'import { X } from 'lucide-react'import { getIconWithAttrs } from '../../plugin/ui/icon'import { ModalShell } from './ModalShell'import './SectionedDialog.css'
export interface DialogSection { id: string label: string icon: string separatorBefore?: boolean}
interface SectionedDialogProps { isOpen: boolean onClose: () => void title: string idPrefix: string sections: readonly DialogSection[] activeSection: string onSelectSection: (id: string) => void children: ReactNode closeLabel?: string}
function SectionIcon({ name }: { name: string }) { const ref = useRef<HTMLSpanElement>(null) useEffect(() => { const svg = getIconWithAttrs(name, { width: 20, height: 20 }) ref.current?.replaceChildren(...(svg ? [svg] : [])) }, [name]) return <span ref={ref} className="sectioned-dialog__icon" aria-hidden="true" />}
/** App-owned section navigator; distinct from the imperative Obsidian Modal API. */export function SectionedDialog({ isOpen, onClose, title, idPrefix, sections, activeSection, onSelectSection, children, closeLabel,}: SectionedDialogProps) { const activeTabRef = useRef<HTMLButtonElement>(null) if (!isOpen) return null
const handleNavKeyDown = (event: KeyboardEvent<HTMLElement>) => { if (!(event.target instanceof HTMLButtonElement) || event.target.getAttribute('role') !== 'tab') return const focusedTab = event.target const index = sections.findIndex(section => `${idPrefix}-tab-${section.id}` === focusedTab.id) if (index === -1) return const next = event.key === 'ArrowDown' ? (index + 1) % sections.length : event.key === 'ArrowUp' ? (index - 1 + sections.length) % sections.length : event.key === 'Home' ? 0 : event.key === 'End' ? sections.length - 1 : -1 if (next === -1) return event.preventDefault() const target = sections[next] document.getElementById(`${idPrefix}-tab-${target.id}`)?.focus() onSelectSection(target.id) }
return <ModalShell isOpen={isOpen} onClose={onClose} titleId={`${idPrefix}-modal-title`} contentClassName="sectioned-dialog" initialFocusRef={activeTabRef}> <nav className={`sectioned-dialog__nav${closeLabel ? ' sectioned-dialog__nav--closable' : ''}`} role="tablist" aria-orientation="vertical" aria-label={`${title} sections`} onKeyDown={handleNavKeyDown}> <div id={`${idPrefix}-modal-title`} className="sectioned-dialog__nav-header"> {title} </div> <div className="sectioned-dialog__nav-sections">{sections.map(section => <div className="sectioned-dialog__nav-entry" key={section.id}> {section.separatorBefore && <hr className="sectioned-dialog__separator" />} <button type="button" role="tab" className="sectioned-dialog__nav-item" id={`${idPrefix}-tab-${section.id}`} ref={activeSection === section.id ? activeTabRef : undefined} tabIndex={activeSection === section.id ? 0 : -1} aria-selected={activeSection === section.id} aria-label={section.label} title={section.label} onClick={() => onSelectSection(section.id)} > <SectionIcon name={section.icon} /> <span className="sectioned-dialog__nav-label">{section.label}</span> </button> </div>)}</div> </nav> {closeLabel && <button type="button" className="sectioned-dialog__close" aria-label={closeLabel} onClick={onClose}><X size={20} aria-hidden="true" /></button>} <div className="sectioned-dialog__body vertical-tab-content" role="tabpanel" id={`${idPrefix}-panel-${activeSection}`} aria-labelledby={`${idPrefix}-tab-${activeSection}`}> {children} </div> </ModalShell>}