import { WebApplication } from '@/ui_models/application'; import { AppState } from '@/ui_models/app_state'; import { Disclosure, DisclosureButton, DisclosurePanel, } from '@reach/disclosure'; import { ContentType, SNTheme } from '@standardnotes/snjs'; import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import { JSXInternal } from 'preact/src/jsx'; import { Icon } from './Icon'; import { toDirective, useCloseOnBlur } from './utils'; const MENU_CLASSNAME = 'sn-menu-border sn-dropdown min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto'; type ThemeButtonProps = { theme: SNTheme; application: WebApplication; onBlur: (event: { relatedTarget: EventTarget | null }) => void; }; type MenuProps = { appState: AppState; application: WebApplication; }; const ThemeButton: FunctionComponent = ({ application, theme, onBlur, }) => { const toggleTheme = () => { if (theme.isLayerable() || !theme.active) { application.toggleComponent(theme); } }; return ( ); }; const QuickSettingsMenu: FunctionComponent = observer( ({ application, appState }) => { const { closeQuickSettingsMenu, shouldAnimateCloseMenu } = appState.quickSettingsMenu; const [themes, setThemes] = useState([]); const [themesMenuOpen, setThemesMenuOpen] = useState(false); const [themesMenuPosition, setThemesMenuPosition] = useState({}); const [defaultThemeOn, setDefaultThemeOn] = useState(false); const themesMenuRef = useRef(null); const themesButtonRef = useRef(null); const prefsButtonRef = useRef(null); const quickSettingsMenuRef = useRef(null); const defaultThemeButtonRef = useRef(null); const reloadThemes = useCallback(() => { application.streamItems(ContentType.Theme, () => { const themes = application.getDisplayableItems( ContentType.Theme ) as SNTheme[]; setThemes( themes.sort((a, b) => { const aIsLayerable = a.isLayerable(); const bIsLayerable = b.isLayerable(); if (aIsLayerable && !bIsLayerable) { return 1; } else if (!aIsLayerable && bIsLayerable) { return -1; } else { return a.package_info.name.toLowerCase() < b.package_info.name.toLowerCase() ? -1 : 1; } }) ); setDefaultThemeOn( !themes.find((theme) => theme.active && !theme.isLayerable()) ); }); }, [application]); useEffect(() => { reloadThemes(); }, [reloadThemes]); useEffect(() => { if (themesMenuOpen) { defaultThemeButtonRef.current!.focus(); } }, [themesMenuOpen]); useEffect(() => { prefsButtonRef.current!.focus(); }, []); const [closeOnBlur] = useCloseOnBlur(themesMenuRef as any, setThemesMenuOpen); const toggleThemesMenu = () => { if (!themesMenuOpen) { const themesButtonRect = themesButtonRef.current!.getBoundingClientRect(); setThemesMenuPosition({ left: themesButtonRect.right, bottom: document.documentElement.clientHeight - themesButtonRect.bottom, }); setThemesMenuOpen(true); } else { setThemesMenuOpen(false); } }; const openPreferences = () => { closeQuickSettingsMenu(); appState.preferences.openPreferences(); }; const handleBtnKeyDown: React.KeyboardEventHandler = ( event ) => { switch (event.key) { case 'Escape': setThemesMenuOpen(false); themesButtonRef.current!.focus(); break; case 'ArrowRight': if (!themesMenuOpen) { toggleThemesMenu(); } } }; const handleQuickSettingsKeyDown: JSXInternal.KeyboardEventHandler = (event) => { const items: NodeListOf = quickSettingsMenuRef.current!.querySelectorAll(':scope > button'); const currentFocusedIndex = Array.from(items).findIndex( (btn) => btn === document.activeElement ); if (!themesMenuOpen) { switch (event.key) { case 'Escape': closeQuickSettingsMenu(); break; case 'ArrowDown': if (items[currentFocusedIndex + 1]) { items[currentFocusedIndex + 1].focus(); } else { items[0].focus(); } break; case 'ArrowUp': if (items[currentFocusedIndex - 1]) { items[currentFocusedIndex - 1].focus(); } else { items[items.length - 1].focus(); } break; } } }; const handlePanelKeyDown: React.KeyboardEventHandler = ( event ) => { const themes = themesMenuRef.current!.querySelectorAll('button'); const currentFocusedIndex = Array.from(themes).findIndex( (themeBtn) => themeBtn === document.activeElement ); switch (event.key) { case 'Escape': case 'ArrowLeft': event.stopPropagation(); setThemesMenuOpen(false); themesButtonRef.current!.focus(); break; case 'ArrowDown': if (themes[currentFocusedIndex + 1]) { themes[currentFocusedIndex + 1].focus(); } else { themes[0].focus(); } break; case 'ArrowUp': if (themes[currentFocusedIndex - 1]) { themes[currentFocusedIndex - 1].focus(); } else { themes[themes.length - 1].focus(); } break; } }; const toggleDefaultTheme = () => { const activeTheme = themes.find( (theme) => theme.active && !theme.isLayerable() ); if (activeTheme) application.toggleComponent(activeTheme); }; return (
Quick Settings
Themes
Themes
{themes.map((theme) => ( ))}
); } ); export const QuickSettingsMenuDirective = toDirective(QuickSettingsMenu);