From a98409a95ff6a571d0c0c12f2d0196444758718a Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 1 Oct 2021 00:29:32 +0530 Subject: [PATCH] feat: Dropdown component feat: Add editor icons feat: Implement default editor selection --- .../javascripts/components/Dropdown.tsx | 77 +++++++++++++--- app/assets/javascripts/components/Icon.tsx | 14 +++ .../panes/general-segments/Defaults.tsx | 90 ++++++++++++++++++- app/assets/stylesheets/_reach-sub.scss | 3 +- app/assets/stylesheets/_sn.scss | 35 +++++++- 5 files changed, 204 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/components/Dropdown.tsx b/app/assets/javascripts/components/Dropdown.tsx index d9667c310..54b3b64b5 100644 --- a/app/assets/javascripts/components/Dropdown.tsx +++ b/app/assets/javascripts/components/Dropdown.tsx @@ -1,7 +1,14 @@ -import { Listbox, ListboxOption } from '@reach/listbox'; +import { + ListboxArrow, + ListboxButton, + ListboxInput, + ListboxList, + ListboxOption, + ListboxPopover, +} from '@reach/listbox'; import VisuallyHidden from '@reach/visually-hidden'; import { FunctionComponent } from 'preact'; -import { IconType } from './Icon'; +import { IconType, Icon } from './Icon'; import '@reach/listbox/styles.css'; import { useState } from 'preact/hooks'; @@ -11,35 +18,85 @@ export type DropdownItem = { value: string; }; -type Props = { +type DropdownProps = { id: string; srLabel: string; items: DropdownItem[]; defaultValue: string; + onChange: (value: string) => void; }; -export const Dropdown: FunctionComponent = ({ +type ListboxButtonProps = { + value: string | null; + label: string; + isExpanded: boolean; +}; + +const customDropdownButton: FunctionComponent = ({ + label, + isExpanded, +}) => ( + <> +
{label}
+ + + + +); + +export const Dropdown: FunctionComponent = ({ id, srLabel, items, defaultValue, + onChange, }) => { const [value, setValue] = useState(defaultValue); const labelId = `${id}-label`; + const handleChange = (value: string) => { + setValue(value); + onChange(value); + }; + return ( <> {srLabel} - setValue(value)} + onChange={handleChange} aria-labelledby={labelId} > - {items.map((item) => ( - {item.label} - ))} - + + +
+ + {items.map((item) => ( + + {item.icon ? ( +
+ +
+ ) : null} +
{item.label}
+
+ ))} +
+
+
+ ); }; diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx index e72804ae2..9e2ca1b1a 100644 --- a/app/assets/javascripts/components/Icon.tsx +++ b/app/assets/javascripts/components/Icon.tsx @@ -1,4 +1,5 @@ import PencilOffIcon from '../../icons/ic-pencil-off.svg'; +import PlainTextIcon from '../../icons/ic-text-paragraph.svg'; import RichTextIcon from '../../icons/ic-text-rich.svg'; import TrashIcon from '../../icons/ic-trash.svg'; import PinIcon from '../../icons/ic-pin.svg'; @@ -13,6 +14,12 @@ import PasswordIcon from '../../icons/ic-textbox-password.svg'; import TrashSweepIcon from '../../icons/ic-trash-sweep.svg'; import MoreIcon from '../../icons/ic-more.svg'; import TuneIcon from '../../icons/ic-tune.svg'; +import MenuArrowDownIcon from '../../icons/ic-menu-arrow-down.svg'; +import AuthenticatorIcon from '../../icons/ic-authenticator.svg'; +import SpreadsheetsIcon from '../../icons/ic-spreadsheets.svg'; +import TasksIcon from '../../icons/ic-tasks.svg'; +import MarkdownIcon from '../../icons/ic-markdown.svg'; +import CodeIcon from '../../icons/ic-code.svg'; import AccessibilityIcon from '../../icons/ic-accessibility.svg'; import HelpIcon from '../../icons/ic-help.svg'; @@ -35,7 +42,13 @@ import { FunctionalComponent } from 'preact'; const ICONS = { 'pencil-off': PencilOffIcon, + 'plain-text': PlainTextIcon, 'rich-text': RichTextIcon, + code: CodeIcon, + markdown: MarkdownIcon, + authenticator: AuthenticatorIcon, + spreadsheets: SpreadsheetsIcon, + tasks: TasksIcon, trash: TrashIcon, pin: PinIcon, unpin: UnpinIcon, @@ -64,6 +77,7 @@ const ICONS = { check: CheckIcon, 'check-bold': CheckBoldIcon, 'account-circle': AccountCircleIcon, + 'menu-arrow-down': MenuArrowDownIcon, }; export type IconType = keyof typeof ICONS; diff --git a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx index 7f492b854..5ebb8725f 100644 --- a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx +++ b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx @@ -1,4 +1,5 @@ import { Dropdown, DropdownItem } from '@/components/Dropdown'; +import { IconType } from '@/components/Icon'; import { PreferencesGroup, PreferencesSegment, @@ -7,7 +8,11 @@ import { Title, } from '@/preferences/components'; import { WebApplication } from '@/ui_models/application'; -import { ComponentArea } from '@standardnotes/snjs'; +import { + ComponentArea, + ComponentMutator, + SNComponent, +} from '@standardnotes/snjs'; import { FunctionComponent } from 'preact'; import { useEffect, useState } from 'preact/hooks'; @@ -15,21 +20,85 @@ type Props = { application: WebApplication; }; +const getEditorIconType = (name: string): IconType | null => { + switch (name) { + case 'Bold Editor': + case 'Plus Editor': + return 'rich-text'; + case 'TokenVault': + return 'authenticator'; + case 'Secure Spreadsheets': + return 'spreadsheets'; + case 'Task Editor': + return 'tasks'; + case 'Code Editor': + return 'code'; + } + if (name.includes('Markdown')) { + return 'markdown'; + } + return null; +}; + +const makeEditorDefault = ( + application: WebApplication, + component: SNComponent, + currentDefault: SNComponent +) => { + if (currentDefault) { + application.changeItem(currentDefault.uuid, (m) => { + const mutator = m as ComponentMutator; + mutator.defaultEditor = false; + }); + } + application.changeAndSaveItem(component.uuid, (m) => { + const mutator = m as ComponentMutator; + mutator.defaultEditor = true; + }); +}; + +const removeEditorDefault = ( + application: WebApplication, + component: SNComponent +) => { + application.changeAndSaveItem(component.uuid, (m) => { + const mutator = m as ComponentMutator; + mutator.defaultEditor = false; + }); +}; + +const getDefaultEditor = (application: WebApplication) => { + return application.componentManager + .componentsForArea(ComponentArea.Editor) + .filter((e) => e.isDefaultEditor())[0]; +}; + export const Defaults: FunctionComponent = ({ application }) => { const [editorItems, setEditorItems] = useState([]); + const [defaultEditorValue] = useState( + () => + getDefaultEditor(application)?.package_info?.identifier || 'plain-editor' + ); useEffect(() => { const editors = application.componentManager .componentsForArea(ComponentArea.Editor) + .sort((a, b) => { + return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; + }) .map((editor) => { + const iconType = getEditorIconType(editor.name); + return { label: editor.name, value: editor.package_info.identifier, + ...(iconType ? { icon: iconType } : null), }; }); setEditorItems([ { + icon: 'plain-text', label: 'Plain Editor', value: 'plain-editor', }, @@ -37,6 +106,22 @@ export const Defaults: FunctionComponent = ({ application }) => { ]); }, [application]); + const setDefaultEditor = (value: string) => { + const editors = application.componentManager.componentsForArea( + ComponentArea.Editor + ); + const currentDefault = getDefaultEditor(application); + + if (value !== 'plain-editor') { + const editorComponent = editors.filter( + (e) => e.package_info.identifier === value + )[0]; + makeEditorDefault(application, editorComponent, currentDefault); + } else { + removeEditorDefault(application, currentDefault); + } + }; + return ( @@ -49,7 +134,8 @@ export const Defaults: FunctionComponent = ({ application }) => { id="def-editor-dropdown" srLabel="Select the default editor" items={editorItems} - defaultValue="plain-editor" + defaultValue={defaultEditorValue} + onChange={setDefaultEditor} /> diff --git a/app/assets/stylesheets/_reach-sub.scss b/app/assets/stylesheets/_reach-sub.scss index fcb0da138..3d209c899 100644 --- a/app/assets/stylesheets/_reach-sub.scss +++ b/app/assets/stylesheets/_reach-sub.scss @@ -9,14 +9,13 @@ } [data-reach-dialog-overlay]::before { background-color: var(--sn-stylekit-contrast-background-color); - content: ""; + content: ''; position: fixed; top: 0px; right: 0px; bottom: 0px; left: 0px; opacity: 0.75; - } [data-reach-dialog-content] { diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 6ecdf4abe..39bdc30ca 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -53,6 +53,29 @@ } } +.sn-dropdown-popover { + z-index: 3001; +} + +.sn-dropdown-button { + @extend .rounded; + @extend .px-3\.5; + @extend .py-1\.75; + @extend .fit-content; + @extend .bg-default; + @extend .text-input; + @extend .color-text; + @extend .border-neutral; + @extend .border-solid; + @extend .border-gray-300; + @extend .border-1; + @extend .min-w-42; +} + +.sn-dropdown-arrow { + @extend .flex; +} + /** Lesser specificity will give priority to reach's styles */ [data-reach-custom-checkbox-container].sn-switch { @extend .duration-150; @@ -114,6 +137,16 @@ &.sn-dropdown-item--no-icon { @extend .py-2; } + + &[data-current-nav] { + @extend .bg-contrast; + @extend .hover\:color-text; + } + + &[data-current-selected] { + background-color: var(--sn-stylekit-info-backdrop-color); + @extend .color-info; + } } .sn-tag { @@ -278,4 +311,4 @@ .select-none { user-select: none; -} \ No newline at end of file +}