refactor: migrate disclosure & combobox from reach-ui (#2316)
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -30,7 +30,6 @@
|
|||||||
"@babel/preset-typescript": "^7.18.6",
|
"@babel/preset-typescript": "^7.18.6",
|
||||||
"@lexical/react": "0.9.2",
|
"@lexical/react": "0.9.2",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
||||||
"@reach/disclosure": "^0.18.0",
|
|
||||||
"@simplewebauthn/browser": "^7.1.0",
|
"@simplewebauthn/browser": "^7.1.0",
|
||||||
"@standardnotes/authenticator": "^2.3.9",
|
"@standardnotes/authenticator": "^2.3.9",
|
||||||
"@standardnotes/autobiography-theme": "^1.2.7",
|
"@standardnotes/autobiography-theme": "^1.2.7",
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
|
||||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
|
||||||
import { doesItemMatchSearchQuery } from '@/Utils/Items/Search/doesItemMatchSearchQuery'
|
import { doesItemMatchSearchQuery } from '@/Utils/Items/Search/doesItemMatchSearchQuery'
|
||||||
import { Disclosure, DisclosurePanel } from '@reach/disclosure'
|
import { Combobox, ComboboxItem, ComboboxPopover, useComboboxStore, VisuallyHidden } from '@ariakit/react'
|
||||||
import { classNames, ContentType, DecryptedItem, naturalSort } from '@standardnotes/snjs'
|
import { ContentType, DecryptedItem, naturalSort } from '@standardnotes/snjs'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { ChangeEventHandler, FocusEventHandler, useCallback, useEffect, useRef, useState } from 'react'
|
import { useDeferredValue, useEffect, useState } from 'react'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
import { useApplication } from '../ApplicationProvider'
|
||||||
import LinkedItemMeta from '../LinkedItems/LinkedItemMeta'
|
import LinkedItemMeta from '../LinkedItems/LinkedItemMeta'
|
||||||
import Menu from '../Menu/Menu'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
contentTypes: ContentType[]
|
contentTypes: ContentType[]
|
||||||
@@ -18,41 +15,11 @@ type Props = {
|
|||||||
const ItemSelectionDropdown = ({ contentTypes, placeholder, onSelection }: Props) => {
|
const ItemSelectionDropdown = ({ contentTypes, placeholder, onSelection }: Props) => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const combobox = useComboboxStore()
|
||||||
const [dropdownVisible, setDropdownVisible] = useState(false)
|
const value = combobox.useState('value')
|
||||||
|
const searchQuery = useDeferredValue(value)
|
||||||
const [dropdownMaxHeight, setDropdownMaxHeight] = useState<number | 'auto'>('auto')
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
|
||||||
const searchResultsMenuRef = useRef<HTMLMenuElement>(null)
|
|
||||||
const [items, setItems] = useState<DecryptedItem[]>([])
|
const [items, setItems] = useState<DecryptedItem[]>([])
|
||||||
|
|
||||||
const showDropdown = () => {
|
|
||||||
const { clientHeight } = document.documentElement
|
|
||||||
const inputRect = inputRef.current?.getBoundingClientRect()
|
|
||||||
if (inputRect) {
|
|
||||||
setDropdownMaxHeight(clientHeight - inputRect.bottom - 32 * 2)
|
|
||||||
setDropdownVisible(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [closeOnBlur] = useCloseOnBlur(containerRef, (visible: boolean) => {
|
|
||||||
setDropdownVisible(visible)
|
|
||||||
setSearchQuery('')
|
|
||||||
})
|
|
||||||
|
|
||||||
const onBlur: FocusEventHandler = (event) => {
|
|
||||||
closeOnBlur(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSearchQueryChange: ChangeEventHandler<HTMLInputElement> = (event) => {
|
|
||||||
setSearchQuery(event.currentTarget.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFocus = () => {
|
|
||||||
showDropdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const searchableItems = naturalSort(application.items.getItems(contentTypes), 'title')
|
const searchableItems = naturalSort(application.items.getItems(contentTypes), 'title')
|
||||||
const filteredItems = searchableItems.filter((item) => {
|
const filteredItems = searchableItems.filter((item) => {
|
||||||
@@ -61,70 +28,38 @@ const ItemSelectionDropdown = ({ contentTypes, placeholder, onSelection }: Props
|
|||||||
setItems(filteredItems)
|
setItems(filteredItems)
|
||||||
}, [searchQuery, application, contentTypes])
|
}, [searchQuery, application, contentTypes])
|
||||||
|
|
||||||
const onSelectItem = useCallback(
|
|
||||||
(item: DecryptedItem) => {
|
|
||||||
onSelection(item)
|
|
||||||
setSearchQuery('')
|
|
||||||
setDropdownVisible(false)
|
|
||||||
},
|
|
||||||
[onSelection],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" ref={containerRef}>
|
<div>
|
||||||
<Disclosure open={dropdownVisible} onChange={showDropdown}>
|
<label>
|
||||||
<input
|
<VisuallyHidden>Select an item</VisuallyHidden>
|
||||||
ref={inputRef}
|
<Combobox
|
||||||
className={classNames(
|
store={combobox}
|
||||||
'mr-10 w-70',
|
|
||||||
'bg-transparent text-sm text-text focus:border-b-2 focus:border-solid focus:border-info lg:text-xs',
|
|
||||||
'no-border h-7 focus:shadow-none focus:outline-none',
|
|
||||||
)}
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={onSearchQueryChange}
|
|
||||||
type="text"
|
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onFocus={handleFocus}
|
className="h-7 w-70 bg-transparent text-sm text-text focus:border-b-2 focus:border-info focus:shadow-none focus:outline-none lg:text-xs"
|
||||||
onBlur={onBlur}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
/>
|
||||||
|
</label>
|
||||||
{dropdownVisible && (
|
<ComboboxPopover
|
||||||
<DisclosurePanel
|
store={combobox}
|
||||||
className={classNames(
|
className="z-dropdown-menu max-h-[var(--popover-available-height)] w-[var(--popover-anchor-width)] overflow-y-auto rounded bg-default py-2 shadow-main"
|
||||||
'mr-10 w-70',
|
>
|
||||||
'absolute z-dropdown-menu flex flex-col overflow-y-auto rounded bg-default py-2 shadow-main',
|
{items.length > 0 ? (
|
||||||
)}
|
items.map((item) => (
|
||||||
style={{
|
<ComboboxItem
|
||||||
maxHeight: dropdownMaxHeight,
|
key={item.uuid}
|
||||||
}}
|
className="flex w-full cursor-pointer items-center justify-between gap-4 overflow-hidden py-2 px-3 hover:bg-contrast hover:text-foreground [&[data-active-item]]:bg-info-backdrop"
|
||||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
hideOnClick
|
||||||
onBlur={closeOnBlur}
|
onClick={() => {
|
||||||
>
|
combobox.setValue('')
|
||||||
<Menu
|
onSelection(item)
|
||||||
isOpen={dropdownVisible}
|
}}
|
||||||
a11yLabel="Tag search results"
|
|
||||||
ref={searchResultsMenuRef}
|
|
||||||
shouldAutoFocus={false}
|
|
||||||
>
|
>
|
||||||
{items.map((item) => {
|
<LinkedItemMeta item={item} searchQuery={searchQuery} />
|
||||||
return (
|
</ComboboxItem>
|
||||||
<button
|
))
|
||||||
key={item.uuid}
|
) : (
|
||||||
className={classNames(
|
<div className="px-2">No results found</div>
|
||||||
'flex w-full items-center justify-between gap-4 overflow-hidden py-2 px-3 hover:bg-contrast',
|
|
||||||
'hover:text-foreground focus:bg-info-backdrop',
|
|
||||||
)}
|
|
||||||
onClick={() => onSelectItem(item)}
|
|
||||||
>
|
|
||||||
<LinkedItemMeta item={item} searchQuery={searchQuery} />
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Menu>
|
|
||||||
</DisclosurePanel>
|
|
||||||
)}
|
)}
|
||||||
</Disclosure>
|
</ComboboxPopover>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,18 @@
|
|||||||
import {
|
import { FormEventHandler, KeyboardEventHandler, useDeferredValue, useEffect, useRef } from 'react'
|
||||||
ChangeEventHandler,
|
|
||||||
FocusEventHandler,
|
|
||||||
FormEventHandler,
|
|
||||||
KeyboardEventHandler,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
import { Disclosure, DisclosurePanel } from '@reach/disclosure'
|
|
||||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { classNames } from '@standardnotes/utils'
|
import { classNames } from '@standardnotes/utils'
|
||||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
|
||||||
import LinkedItemSearchResults from './LinkedItemSearchResults'
|
|
||||||
import { LinkingController } from '@/Controllers/LinkingController'
|
import { LinkingController } from '@/Controllers/LinkingController'
|
||||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
|
||||||
import { ElementIds } from '@/Constants/ElementIDs'
|
import { ElementIds } from '@/Constants/ElementIDs'
|
||||||
import Menu from '../Menu/Menu'
|
|
||||||
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
|
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
import { useApplication } from '../ApplicationProvider'
|
||||||
import { DecryptedItem } from '@standardnotes/snjs'
|
import { DecryptedItem, SNNote } from '@standardnotes/snjs'
|
||||||
|
import { Combobox, ComboboxItem, ComboboxPopover, useComboboxStore, VisuallyHidden } from '@ariakit/react'
|
||||||
|
import LinkedItemMeta from './LinkedItemMeta'
|
||||||
|
import { LinkedItemSearchResultsAddTagOption } from './LinkedItemSearchResultsAddTagOption'
|
||||||
|
import { Slot } from '@radix-ui/react-slot'
|
||||||
|
import Icon from '../Icon/Icon'
|
||||||
|
import { PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
|
||||||
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
linkingController: LinkingController
|
linkingController: LinkingController
|
||||||
@@ -45,33 +37,13 @@ const ItemLinkAutocompleteInput = ({
|
|||||||
|
|
||||||
const tagsLinkedToItem = getLinkedTagsForItem(item) || []
|
const tagsLinkedToItem = getLinkedTagsForItem(item) || []
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const combobox = useComboboxStore()
|
||||||
|
const value = combobox.useState('value')
|
||||||
|
const searchQuery = useDeferredValue(value)
|
||||||
|
|
||||||
const { unlinkedItems, shouldShowCreateTag } = getLinkingSearchResults(searchQuery, application, item)
|
const { unlinkedItems, shouldShowCreateTag } = getLinkingSearchResults(searchQuery, application, item)
|
||||||
|
|
||||||
const [dropdownVisible, setDropdownVisible] = useState(false)
|
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const [dropdownMaxHeight, setDropdownMaxHeight] = useState<number | 'auto'>('auto')
|
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
|
||||||
const searchResultsMenuRef = useRef<HTMLMenuElement>(null)
|
|
||||||
|
|
||||||
const [closeOnBlur] = useCloseOnBlur(containerRef, (visible: boolean) => {
|
|
||||||
setDropdownVisible(visible)
|
|
||||||
setSearchQuery('')
|
|
||||||
})
|
|
||||||
|
|
||||||
const showDropdown = () => {
|
|
||||||
const { clientHeight } = document.documentElement
|
|
||||||
const inputRect = inputRef.current?.getBoundingClientRect()
|
|
||||||
if (inputRect) {
|
|
||||||
setDropdownMaxHeight(clientHeight - inputRect.bottom - 32 * 2)
|
|
||||||
setDropdownVisible(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSearchQueryChange: ChangeEventHandler<HTMLInputElement> = (event) => {
|
|
||||||
setSearchQuery(event.currentTarget.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFormSubmit: FormEventHandler = async (event) => {
|
const onFormSubmit: FormEventHandler = async (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -84,11 +56,6 @@ const ItemLinkAutocompleteInput = ({
|
|||||||
if (focusedId !== ElementIds.ItemLinkAutocompleteInput) {
|
if (focusedId !== ElementIds.ItemLinkAutocompleteInput) {
|
||||||
setFocusedId(ElementIds.ItemLinkAutocompleteInput)
|
setFocusedId(ElementIds.ItemLinkAutocompleteInput)
|
||||||
}
|
}
|
||||||
showDropdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onBlur: FocusEventHandler = (event) => {
|
|
||||||
closeOnBlur(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onKeyDown: KeyboardEventHandler = (event) => {
|
const onKeyDown: KeyboardEventHandler = (event) => {
|
||||||
@@ -98,12 +65,6 @@ const ItemLinkAutocompleteInput = ({
|
|||||||
focusPreviousItem()
|
focusPreviousItem()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case KeyboardKey.Down:
|
|
||||||
if (searchQuery.length > 0) {
|
|
||||||
event.preventDefault()
|
|
||||||
searchResultsMenuRef.current?.focus()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,70 +74,63 @@ const ItemLinkAutocompleteInput = ({
|
|||||||
}
|
}
|
||||||
}, [focusedId])
|
}, [focusedId])
|
||||||
|
|
||||||
const areSearchResultsVisible = dropdownVisible && (unlinkedItems.length > 0 || shouldShowCreateTag)
|
|
||||||
|
|
||||||
const handleMenuKeyDown: KeyboardEventHandler<HTMLMenuElement> = useCallback((event) => {
|
|
||||||
if (event.key === KeyboardKey.Escape) {
|
|
||||||
inputRef.current?.focus()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef}>
|
<div>
|
||||||
<form onSubmit={onFormSubmit}>
|
<form onSubmit={onFormSubmit}>
|
||||||
<Disclosure open={dropdownVisible} onChange={showDropdown}>
|
<label>
|
||||||
<input
|
<VisuallyHidden>Link tags, notes or files</VisuallyHidden>
|
||||||
ref={inputRef}
|
<Combobox
|
||||||
|
store={combobox}
|
||||||
|
placeholder="Link tags, notes, files..."
|
||||||
className={classNames(
|
className={classNames(
|
||||||
`${tagsLinkedToItem.length > 0 ? 'w-80' : 'mr-10 w-70'}`,
|
`${tagsLinkedToItem.length > 0 ? 'w-80' : 'mr-10 w-70'}`,
|
||||||
'bg-transparent text-sm text-text focus:border-b-2 focus:border-solid focus:border-info lg:text-xs',
|
'h-7 w-70 bg-transparent text-sm text-text focus:border-b-2 focus:border-info focus:shadow-none focus:outline-none lg:text-xs',
|
||||||
'no-border h-7 focus:shadow-none focus:outline-none',
|
|
||||||
)}
|
)}
|
||||||
value={searchQuery}
|
title={hoverLabel}
|
||||||
onChange={onSearchQueryChange}
|
id={ElementIds.ItemLinkAutocompleteInput}
|
||||||
type="text"
|
ref={inputRef}
|
||||||
placeholder="Link tags, notes, files..."
|
|
||||||
onBlur={onBlur}
|
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
id={ElementIds.ItemLinkAutocompleteInput}
|
|
||||||
autoComplete="off"
|
|
||||||
title={hoverLabel}
|
|
||||||
aria-label={hoverLabel}
|
|
||||||
/>
|
/>
|
||||||
{areSearchResultsVisible && (
|
</label>
|
||||||
<DisclosurePanel
|
<ComboboxPopover
|
||||||
className={classNames(
|
store={combobox}
|
||||||
tagsLinkedToItem.length > 0 ? 'w-80' : 'mr-10 w-70',
|
className={classNames(
|
||||||
'absolute z-dropdown-menu flex flex-col overflow-y-auto rounded bg-default py-2 shadow-main',
|
'z-dropdown-menu max-h-[var(--popover-available-height)] w-[var(--popover-anchor-width)] overflow-y-auto rounded bg-default py-2 shadow-main',
|
||||||
)}
|
unlinkedItems.length === 0 && !shouldShowCreateTag && 'hidden',
|
||||||
style={{
|
|
||||||
maxHeight: dropdownMaxHeight,
|
|
||||||
}}
|
|
||||||
onBlur={closeOnBlur}
|
|
||||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
|
||||||
>
|
|
||||||
<Menu
|
|
||||||
isOpen={areSearchResultsVisible}
|
|
||||||
a11yLabel="Unlinked items search results"
|
|
||||||
onKeyDown={handleMenuKeyDown}
|
|
||||||
ref={searchResultsMenuRef}
|
|
||||||
shouldAutoFocus={false}
|
|
||||||
>
|
|
||||||
<LinkedItemSearchResults
|
|
||||||
createAndAddNewTag={createAndAddNewTag}
|
|
||||||
linkItems={linkItems}
|
|
||||||
item={item}
|
|
||||||
results={unlinkedItems}
|
|
||||||
searchQuery={searchQuery}
|
|
||||||
shouldShowCreateTag={shouldShowCreateTag}
|
|
||||||
onClickCallback={() => setSearchQuery('')}
|
|
||||||
isEntitledToNoteLinking={isEntitledToNoteLinking}
|
|
||||||
/>
|
|
||||||
</Menu>
|
|
||||||
</DisclosurePanel>
|
|
||||||
)}
|
)}
|
||||||
</Disclosure>
|
>
|
||||||
|
{unlinkedItems.map((result) => {
|
||||||
|
const cannotLinkItem = !isEntitledToNoteLinking && result instanceof SNNote
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ComboboxItem
|
||||||
|
key={result.uuid}
|
||||||
|
className="flex w-full cursor-pointer items-center justify-between gap-4 overflow-hidden py-2 px-3 hover:bg-contrast hover:text-foreground [&[data-active-item]]:bg-info-backdrop"
|
||||||
|
hideOnClick
|
||||||
|
onClick={() => {
|
||||||
|
linkItems(item, result).catch(console.error)
|
||||||
|
combobox.setValue('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LinkedItemMeta item={result} searchQuery={searchQuery} />
|
||||||
|
{cannotLinkItem && <Icon type={PremiumFeatureIconName} className="ml-auto flex-shrink-0 text-info" />}
|
||||||
|
</ComboboxItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{shouldShowCreateTag && (
|
||||||
|
<ComboboxItem
|
||||||
|
hideOnClick
|
||||||
|
as={Slot}
|
||||||
|
onClick={() => {
|
||||||
|
void createAndAddNewTag(searchQuery)
|
||||||
|
combobox.setValue('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LinkedItemSearchResultsAddTagOption searchQuery={searchQuery} />
|
||||||
|
</ComboboxItem>
|
||||||
|
)}
|
||||||
|
</ComboboxPopover>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,42 +1,49 @@
|
|||||||
import { classNames } from '@standardnotes/utils'
|
import { classNames } from '@standardnotes/utils'
|
||||||
|
import { ComponentPropsWithoutRef, ForwardedRef, forwardRef } from 'react'
|
||||||
import Icon from '../Icon/Icon'
|
import Icon from '../Icon/Icon'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
searchQuery: string
|
searchQuery: string
|
||||||
onClickCallback: (searchQuery: string) => void
|
onClickCallback?: (searchQuery: string) => void
|
||||||
isFocused?: boolean
|
isFocused?: boolean
|
||||||
}
|
} & ComponentPropsWithoutRef<'button'>
|
||||||
|
|
||||||
export const LinkedItemSearchResultsAddTagOption = ({ searchQuery, onClickCallback, isFocused }: Props) => {
|
export const LinkedItemSearchResultsAddTagOption = forwardRef(
|
||||||
return (
|
({ searchQuery, onClickCallback, isFocused, ...props }: Props, ref: ForwardedRef<HTMLButtonElement>) => {
|
||||||
<button
|
return (
|
||||||
className={classNames(
|
<button
|
||||||
'group flex w-full items-center gap-2 overflow-hidden py-2 px-3 hover:bg-contrast hover:text-foreground',
|
|
||||||
'focus:bg-info-backdrop',
|
|
||||||
isFocused ? 'bg-contrast bg-info-backdrop text-foreground' : '',
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
onClickCallback(searchQuery)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="flex-shrink-0 align-middle text-sm lg:text-xs">Create & add tag</span>{' '}
|
|
||||||
<span
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'inline-flex min-w-0 items-center gap-1 rounded py-1 pl-1 pr-2 align-middle text-xs ',
|
'group flex w-full items-center gap-2 overflow-hidden py-2 px-3 hover:bg-contrast hover:text-foreground',
|
||||||
'group-hover:bg-info group-hover:text-info-contrast',
|
'focus:bg-info-backdrop [&[data-active-item]]:bg-info-backdrop',
|
||||||
isFocused ? 'bg-info text-info-contrast' : 'bg-contrast text-text',
|
isFocused ? 'bg-info-backdrop text-foreground' : '',
|
||||||
)}
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
if (onClickCallback) {
|
||||||
|
onClickCallback(searchQuery)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<Icon
|
<span className="flex-shrink-0 align-middle text-sm lg:text-xs">Create & add tag</span>{' '}
|
||||||
type="hashtag"
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex-shrink-0 group-hover:text-info-contrast',
|
'inline-flex min-w-0 items-center gap-1 rounded py-1 pl-1 pr-2 align-middle text-xs ',
|
||||||
isFocused ? 'text-info-contrast' : 'text-info',
|
'group-hover:bg-info group-hover:text-info-contrast',
|
||||||
|
isFocused ? 'bg-info text-info-contrast' : 'bg-contrast text-text',
|
||||||
)}
|
)}
|
||||||
size="small"
|
>
|
||||||
/>
|
<Icon
|
||||||
<span className="min-w-0 overflow-hidden text-ellipsis">{searchQuery}</span>
|
type="hashtag"
|
||||||
</span>
|
className={classNames(
|
||||||
</button>
|
'flex-shrink-0 group-hover:text-info-contrast',
|
||||||
)
|
isFocused ? 'text-info-contrast' : 'text-info',
|
||||||
}
|
)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<span className="min-w-0 overflow-hidden text-ellipsis">{searchQuery}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,65 +1,21 @@
|
|||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
import { Hovercard, HovercardAnchor, useHovercardStore } from '@ariakit/react'
|
||||||
import { FunctionComponent, useState, useRef, useEffect, MouseEventHandler } from 'react'
|
|
||||||
import { IconType } from '@standardnotes/snjs'
|
|
||||||
|
|
||||||
type Props = {
|
const AuthAppInfoTooltip = () => {
|
||||||
className?: string
|
const infoHovercard = useHovercardStore({
|
||||||
icon: IconType
|
showTimeout: 100,
|
||||||
onMouseEnter?: MouseEventHandler<HTMLButtonElement>
|
})
|
||||||
onMouseLeave?: MouseEventHandler<HTMLButtonElement>
|
|
||||||
}
|
|
||||||
|
|
||||||
const DisclosureIconButton: FunctionComponent<Props> = ({ className = '', icon, onMouseEnter, onMouseLeave }) => (
|
|
||||||
<DisclosureButton
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
className={`no-border cursor-pointer bg-transparent p-0 hover:brightness-125 ${className ?? ''}`}
|
|
||||||
>
|
|
||||||
<Icon type={icon} />
|
|
||||||
</DisclosureButton>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AuthAppInfoPopup is an info icon that shows a tooltip when clicked
|
|
||||||
* Tooltip is dismissible by clicking outside
|
|
||||||
*
|
|
||||||
* Note: it can be generalized but more use cases are required
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const AuthAppInfoTooltip: FunctionComponent = () => {
|
|
||||||
const [isClicked, setClicked] = useState(false)
|
|
||||||
const [isHover, setHover] = useState(false)
|
|
||||||
const ref = useRef(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const dismiss = () => setClicked(false)
|
|
||||||
document.addEventListener('mousedown', dismiss)
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', dismiss)
|
|
||||||
}
|
|
||||||
}, [ref])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure open={isClicked || isHover} onChange={() => setClicked(!isClicked)}>
|
<>
|
||||||
<div className="relative">
|
<HovercardAnchor store={infoHovercard}>
|
||||||
<DisclosureIconButton
|
<Icon type="info" />
|
||||||
icon="info"
|
</HovercardAnchor>
|
||||||
className="mt-1"
|
<Hovercard store={infoHovercard} className=" max-w-76 rounded border border-border bg-default py-2 px-3 text-sm">
|
||||||
onMouseEnter={() => setHover(true)}
|
Some apps, like Google Authenticator, do not back up and restore your secret keys if you lose your device or get
|
||||||
onMouseLeave={() => setHover(false)}
|
a new one.
|
||||||
/>
|
</Hovercard>
|
||||||
<DisclosurePanel>
|
</>
|
||||||
<div
|
|
||||||
className={`bg-inverted-default text-inverted-default shadow-overlay w-103 -left-51
|
|
||||||
absolute -top-10 rounded py-1.5 px-2 text-center`}
|
|
||||||
>
|
|
||||||
Some apps, like Google Authenticator, do not back up and restore your secret keys if you lose your device or
|
|
||||||
get a new one.
|
|
||||||
</div>
|
|
||||||
</DisclosurePanel>
|
|
||||||
</div>
|
|
||||||
</Disclosure>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ const PreferencesMenuView: FunctionComponent<Props> = ({ menu }) => {
|
|||||||
}}
|
}}
|
||||||
classNameOverride={{
|
classNameOverride={{
|
||||||
wrapper: 'relative',
|
wrapper: 'relative',
|
||||||
popover: 'bottom-full w-full max-h-max',
|
|
||||||
button: 'focus:outline-none focus:shadow-none focus:ring-none',
|
button: 'focus:outline-none focus:shadow-none focus:ring-none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ const AccordionItem: FunctionComponent<Props> = ({ title, className = '', childr
|
|||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div
|
<div
|
||||||
className={'relative flex cursor-pointer hover:underline'}
|
className="relative flex cursor-pointer items-center justify-between hover:underline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsExpanded(!isExpanded)
|
setIsExpanded(!isExpanded)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Title>{title}</Title>
|
<Title>{title}</Title>
|
||||||
<ArrowDownCheckmarkIcon
|
<ArrowDownCheckmarkIcon
|
||||||
className={'sn-accordion-arrow-icon absolute right-0'}
|
className="sn-accordion-arrow-icon"
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
data-is-expanded={isExpanded}
|
data-is-expanded={isExpanded}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import TabPanel from '../Tabs/TabPanel'
|
|||||||
import { useTabState } from '../Tabs/useTabState'
|
import { useTabState } from '../Tabs/useTabState'
|
||||||
import TabsContainer from '../Tabs/TabsContainer'
|
import TabsContainer from '../Tabs/TabsContainer'
|
||||||
import CopyableCodeBlock from '../Shared/CopyableCodeBlock'
|
import CopyableCodeBlock from '../Shared/CopyableCodeBlock'
|
||||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
|
||||||
import { classNames } from '@standardnotes/utils'
|
import { classNames } from '@standardnotes/utils'
|
||||||
import Modal, { ModalAction } from '../Modal/Modal'
|
import Modal, { ModalAction } from '../Modal/Modal'
|
||||||
|
import { Disclosure, DisclosureContent, useDisclosureStore } from '@ariakit/react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
controller: AddSmartViewModalController
|
controller: AddSmartViewModalController
|
||||||
@@ -77,7 +77,8 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
|
|||||||
const [shouldShowIconPicker, setShouldShowIconPicker] = useState(false)
|
const [shouldShowIconPicker, setShouldShowIconPicker] = useState(false)
|
||||||
const iconPickerButtonRef = useRef<HTMLButtonElement>(null)
|
const iconPickerButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
const [shouldShowJsonExamples, setShouldShowJsonExamples] = useState(false)
|
const jsonExamplesDisclosure = useDisclosureStore()
|
||||||
|
const showingJsonExamples = jsonExamplesDisclosure.useState('open')
|
||||||
|
|
||||||
const toggleIconPicker = () => {
|
const toggleIconPicker = () => {
|
||||||
setShouldShowIconPicker((shouldShow) => !shouldShow)
|
setShouldShowIconPicker((shouldShow) => !shouldShow)
|
||||||
@@ -223,22 +224,26 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
|
|||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabsContainer>
|
</TabsContainer>
|
||||||
{tabState.activeTab === 'custom' && (
|
{tabState.activeTab === 'custom' && (
|
||||||
<Disclosure open={shouldShowJsonExamples} onChange={() => setShouldShowJsonExamples((show) => !show)}>
|
<div className="flex flex-col gap-1.5 rounded-md border-2 border-info-backdrop bg-info-backdrop py-3 px-4">
|
||||||
<div className="flex flex-col gap-1.5 rounded-md border-2 border-info-backdrop bg-info-backdrop py-3 px-4">
|
<Disclosure
|
||||||
<DisclosureButton className="flex items-center justify-between focus:shadow-none focus:outline-none">
|
store={jsonExamplesDisclosure}
|
||||||
<div className="text-sm font-semibold">Examples</div>
|
className="flex items-center justify-between focus:shadow-none focus:outline-none"
|
||||||
<Icon type={shouldShowJsonExamples ? 'chevron-up' : 'chevron-down'} />
|
>
|
||||||
</DisclosureButton>
|
<div className="text-sm font-semibold">Examples</div>
|
||||||
<DisclosurePanel className={classNames(shouldShowJsonExamples && 'flex', 'flex-col gap-2.5')}>
|
<Icon type={showingJsonExamples ? 'chevron-up' : 'chevron-down'} />
|
||||||
<div className="text-sm font-medium">1. List notes that are conflicted copies of another note:</div>
|
</Disclosure>
|
||||||
<CopyableCodeBlock code={ConflictedNotesExampleCode} />
|
<DisclosureContent
|
||||||
<div className="text-sm font-medium">
|
store={jsonExamplesDisclosure}
|
||||||
2. List notes that have the tag `todo` but not the tag `completed`:
|
className={classNames(showingJsonExamples && 'flex', 'flex-col gap-2.5')}
|
||||||
</div>
|
>
|
||||||
<CopyableCodeBlock code={ComplexCompoundExampleCode} />
|
<div className="text-sm font-medium">1. List notes that are conflicted copies of another note:</div>
|
||||||
</DisclosurePanel>
|
<CopyableCodeBlock code={ConflictedNotesExampleCode} />
|
||||||
</div>
|
<div className="text-sm font-medium">
|
||||||
</Disclosure>
|
2. List notes that have the tag `todo` but not the tag `completed`:
|
||||||
|
</div>
|
||||||
|
<CopyableCodeBlock code={ComplexCompoundExampleCode} />
|
||||||
|
</DisclosureContent>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
46
yarn.lock
46
yarn.lock
@@ -4230,51 +4230,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@reach/auto-id@npm:0.18.0":
|
|
||||||
version: 0.18.0
|
|
||||||
resolution: "@reach/auto-id@npm:0.18.0"
|
|
||||||
dependencies:
|
|
||||||
"@reach/utils": 0.18.0
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.8.0 || 17.x
|
|
||||||
react-dom: ^16.8.0 || 17.x
|
|
||||||
checksum: 75a37a0a09c382dfc358d37f3212cffdea2b4c80c2f555b7fee857af59a46e959324234be8dbb51c476fba0874ed93694254079192b478ce3e9abba5ce63fdec
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@reach/disclosure@npm:^0.18.0":
|
|
||||||
version: 0.18.0
|
|
||||||
resolution: "@reach/disclosure@npm:0.18.0"
|
|
||||||
dependencies:
|
|
||||||
"@reach/auto-id": 0.18.0
|
|
||||||
"@reach/polymorphic": 0.18.0
|
|
||||||
"@reach/utils": 0.18.0
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.8.0 || 17.x
|
|
||||||
react-dom: ^16.8.0 || 17.x
|
|
||||||
checksum: 77866743202a87e1c608fb41a56180b6676894a71b9853a6ca45408c82962f85fb7c3405e10c296518fcba95b0e7a4848ca681a6d8beb6d4574d548e606316d9
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@reach/polymorphic@npm:0.18.0":
|
|
||||||
version: 0.18.0
|
|
||||||
resolution: "@reach/polymorphic@npm:0.18.0"
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.8.0 || 17.x
|
|
||||||
checksum: 0d62260a55c71e0dc95f38867f24b3d699e0bb7ea2273c6fa5a1fa3d804269e2deb63a458f1f1eb7b74c32bc0add3ab31a1c2668c1dee6b2837187876d3c4cc2
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@reach/utils@npm:0.18.0":
|
|
||||||
version: 0.18.0
|
|
||||||
resolution: "@reach/utils@npm:0.18.0"
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.8.0 || 17.x
|
|
||||||
react-dom: ^16.8.0 || 17.x
|
|
||||||
checksum: eeda20a74c1db71e95680c622371c6efc46e2c767b28902bc43b580654436a34ec7f8b5f63c51a7c4cc8fd48c94066e3215496ba5a053c96e65c7a9bdcdc2687
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@react-native-async-storage/async-storage@npm:1.17.11":
|
"@react-native-async-storage/async-storage@npm:1.17.11":
|
||||||
version: 1.17.11
|
version: 1.17.11
|
||||||
resolution: "@react-native-async-storage/async-storage@npm:1.17.11"
|
resolution: "@react-native-async-storage/async-storage@npm:1.17.11"
|
||||||
@@ -5558,7 +5513,6 @@ __metadata:
|
|||||||
"@lexical/react": 0.9.2
|
"@lexical/react": 0.9.2
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": ^0.5.10
|
"@pmmmwh/react-refresh-webpack-plugin": ^0.5.10
|
||||||
"@radix-ui/react-slot": ^1.0.1
|
"@radix-ui/react-slot": ^1.0.1
|
||||||
"@reach/disclosure": ^0.18.0
|
|
||||||
"@simplewebauthn/browser": ^7.1.0
|
"@simplewebauthn/browser": ^7.1.0
|
||||||
"@standardnotes/authenticator": ^2.3.9
|
"@standardnotes/authenticator": ^2.3.9
|
||||||
"@standardnotes/autobiography-theme": ^1.2.7
|
"@standardnotes/autobiography-theme": ^1.2.7
|
||||||
|
|||||||
Reference in New Issue
Block a user