refactor: migrate dropdowns from reach-ui (#2315)
This commit is contained in:
@@ -155,7 +155,6 @@ const PhotoCaptureModal = ({ filesController, close }: Props) => {
|
||||
<label className="text-sm font-medium text-neutral">
|
||||
Device:
|
||||
<Dropdown
|
||||
id={'photo-capture-device-dropdown'}
|
||||
label={'Photo Capture Device'}
|
||||
items={devicesAsDropdownItems}
|
||||
value={recorder.selectedDevice.deviceId}
|
||||
@@ -164,7 +163,6 @@ const PhotoCaptureModal = ({ filesController, close }: Props) => {
|
||||
}}
|
||||
classNameOverride={{
|
||||
wrapper: 'mt-1',
|
||||
popover: 'z-modal',
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -149,12 +149,8 @@ const NewNotePreferences: FunctionComponent<Props> = ({
|
||||
<div className="mt-3 text-mobile-menu-item md:text-menu-item">Note Type</div>
|
||||
<div className="mt-2">
|
||||
<Dropdown
|
||||
classNameOverride={{
|
||||
popover: 'z-modal',
|
||||
}}
|
||||
disabled={disabled}
|
||||
fullWidth={true}
|
||||
id="def-editor-dropdown"
|
||||
label="Select the default note type"
|
||||
items={editorItems}
|
||||
value={defaultEditorIdentifier}
|
||||
@@ -166,12 +162,8 @@ const NewNotePreferences: FunctionComponent<Props> = ({
|
||||
<div className="mt-3 text-mobile-menu-item md:text-menu-item">Title Format</div>
|
||||
<div className="mt-2">
|
||||
<Dropdown
|
||||
classNameOverride={{
|
||||
popover: 'z-modal',
|
||||
}}
|
||||
disabled={disabled}
|
||||
fullWidth={true}
|
||||
id="def-new-note-title-format"
|
||||
label="Select the default note type"
|
||||
items={NoteTitleFormatOptions}
|
||||
value={newNoteTitleFormat}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { ListboxArrow, ListboxInput, ListboxList, ListboxPopover } from '@reach/listbox'
|
||||
import '@reach/listbox/styles.css'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { DropdownItem } from './DropdownItem'
|
||||
import StyledListboxButton from './StyledListboxButton'
|
||||
import StyledListboxOption from './StyledListboxOption'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { VisuallyHidden } from '@ariakit/react'
|
||||
import {
|
||||
Select,
|
||||
SelectArrow,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectPopover,
|
||||
useSelectStore,
|
||||
VisuallyHidden,
|
||||
} from '@ariakit/react'
|
||||
|
||||
type DropdownProps = {
|
||||
id: string
|
||||
label: string
|
||||
items: DropdownItem[]
|
||||
value: string
|
||||
@@ -21,92 +24,77 @@ type DropdownProps = {
|
||||
popover?: string
|
||||
}
|
||||
fullWidth?: boolean
|
||||
portal?: boolean
|
||||
}
|
||||
|
||||
type ListboxButtonProps = DropdownItem & {
|
||||
isExpanded: boolean
|
||||
}
|
||||
const Dropdown = ({ label, value, onChange, items, disabled, fullWidth, classNameOverride = {} }: DropdownProps) => {
|
||||
const select = useSelectStore({
|
||||
defaultValue: value,
|
||||
})
|
||||
|
||||
const CustomDropdownButton: FunctionComponent<ListboxButtonProps> = ({
|
||||
label,
|
||||
isExpanded,
|
||||
icon,
|
||||
iconClassName = '',
|
||||
}) => (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
{icon ? (
|
||||
<div className="mr-2 flex">
|
||||
<Icon type={icon} className={iconClassName} size="small" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-base lg:text-sm">{label}</div>
|
||||
</div>
|
||||
<ListboxArrow className={`flex ${isExpanded ? 'rotate-180' : ''}`}>
|
||||
<Icon type="menu-arrow-down" className="text-passive-1" size="small" />
|
||||
</ListboxArrow>
|
||||
</>
|
||||
)
|
||||
const selectedValue = select.useState('value')
|
||||
const isExpanded = select.useState('open')
|
||||
|
||||
const Dropdown: FunctionComponent<DropdownProps> = ({
|
||||
id,
|
||||
label,
|
||||
items,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
classNameOverride = {},
|
||||
fullWidth,
|
||||
portal = true,
|
||||
}) => {
|
||||
const labelId = `${id}-label`
|
||||
const currentItem = items.find((item) => item.value === selectedValue)
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
const selectedItem = items.find((item) => item.value === value) as DropdownItem
|
||||
|
||||
onChange(value, selectedItem)
|
||||
}
|
||||
useEffect(() => {
|
||||
return select.subscribe(
|
||||
(state) => {
|
||||
if (state.value !== value) {
|
||||
onChange(state.value, items.find((item) => item.value === state.value) as DropdownItem)
|
||||
}
|
||||
},
|
||||
['value'],
|
||||
)
|
||||
}, [items, onChange, select, value])
|
||||
|
||||
return (
|
||||
<div className={classNameOverride.wrapper}>
|
||||
<VisuallyHidden id={labelId}>{label}</VisuallyHidden>
|
||||
<ListboxInput value={value} onChange={handleChange} aria-labelledby={labelId} disabled={disabled}>
|
||||
<StyledListboxButton
|
||||
className={classNames('w-full', !fullWidth && 'md:w-fit', classNameOverride.button)}
|
||||
children={({ value, label, isExpanded }) => {
|
||||
const current = items.find((item) => item.value === value)
|
||||
const icon = current ? current?.icon : null
|
||||
const iconClassName = current ? current?.iconClassName : null
|
||||
return CustomDropdownButton({
|
||||
value: value ? value : label.toLowerCase(),
|
||||
label,
|
||||
isExpanded,
|
||||
...(icon ? { icon } : null),
|
||||
...(iconClassName ? { iconClassName } : null),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ListboxPopover
|
||||
portal={portal}
|
||||
className={classNames('sn-dropdown sn-dropdown-popover', classNameOverride.popover)}
|
||||
>
|
||||
<div className="sn-component">
|
||||
<ListboxList>
|
||||
{items.map((item) => (
|
||||
<StyledListboxOption key={item.value} value={item.value} label={item.label} disabled={item.disabled}>
|
||||
{item.icon ? (
|
||||
<div className="mr-3 flex">
|
||||
<Icon type={item.icon} className={item.iconClassName ?? ''} size="small" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-base lg:text-sm">{item.label}</div>
|
||||
</StyledListboxOption>
|
||||
))}
|
||||
</ListboxList>
|
||||
</div>
|
||||
</ListboxPopover>
|
||||
</ListboxInput>
|
||||
<VisuallyHidden>
|
||||
<SelectLabel store={select}>{label}</SelectLabel>
|
||||
</VisuallyHidden>
|
||||
<Select
|
||||
className={classNames(
|
||||
'flex w-full min-w-55 items-center justify-between rounded border border-border bg-default py-1.5 px-3.5 text-sm text-foreground',
|
||||
disabled && 'opacity-50',
|
||||
classNameOverride.button,
|
||||
!fullWidth && 'md:w-fit',
|
||||
)}
|
||||
store={select}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{currentItem?.icon ? (
|
||||
<div className="mr-2 flex">
|
||||
<Icon type={currentItem.icon} className={currentItem.iconClassName ?? ''} size="small" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-base lg:text-sm">{currentItem?.label}</div>
|
||||
</div>
|
||||
<SelectArrow className={classNames('text-passive-1', isExpanded && 'rotate-180')} />
|
||||
</Select>
|
||||
<SelectPopover
|
||||
store={select}
|
||||
className={classNames(
|
||||
'max-h-[var(--popover-available-height)] w-[var(--popover-anchor-width)] overflow-y-auto rounded border border-border bg-default py-1',
|
||||
classNameOverride.popover,
|
||||
)}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<SelectItem
|
||||
className="flex cursor-pointer items-center bg-transparent py-1.5 px-3 text-sm text-text hover:bg-contrast hover:text-foreground [&[data-active-item]]:bg-info [&[data-active-item]]:text-info-contrast"
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon ? (
|
||||
<div className="mr-3 flex">
|
||||
<Icon type={item.icon} className={item.iconClassName ?? ''} size="small" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-base lg:text-sm">{item.label}</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectPopover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { ListboxButton } from '@reach/listbox'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const StyledListboxButton = styled(ListboxButton)`
|
||||
&[data-reach-listbox-button] {
|
||||
background-color: var(--sn-stylekit-background-color);
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid var(--sn-stylekit-border-color);
|
||||
color: var(--sn-stylekit-contrast-foreground-color);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
min-width: 13.75rem;
|
||||
padding-bottom: 0.375rem;
|
||||
padding-left: 0.875rem;
|
||||
padding-right: 0.875rem;
|
||||
padding-top: 0.375rem;
|
||||
}
|
||||
`
|
||||
|
||||
export default StyledListboxButton
|
||||
@@ -1,38 +0,0 @@
|
||||
import { ListboxOption } from '@reach/listbox'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const StyledListboxOption = styled(ListboxOption)`
|
||||
&[data-reach-listbox-option] {
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--sn-stylekit-contrast-foreground-color);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 0.875rem;
|
||||
padding-bottom: 0.375rem;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
padding-top: 0.375rem;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
&[data-current-selected] {
|
||||
color: var(--sn-stylekit-info-color);
|
||||
background-color: var(--sn-stylekit-info-backdrop-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--sn-stylekit-contrast-background-color);
|
||||
color: var(--sn-stylekit-foreground-color);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: var(--sn-stylekit-info-backdrop-color);
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default StyledListboxOption
|
||||
@@ -14,19 +14,10 @@ type Props = {
|
||||
platform: Platform
|
||||
useIconGrid?: boolean
|
||||
iconGridClassName?: string
|
||||
portalDropdown?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const IconPicker = ({
|
||||
selectedValue,
|
||||
onIconChange,
|
||||
platform,
|
||||
className,
|
||||
useIconGrid,
|
||||
portalDropdown,
|
||||
iconGridClassName,
|
||||
}: Props) => {
|
||||
const IconPicker = ({ selectedValue, onIconChange, platform, className, useIconGrid, iconGridClassName }: Props) => {
|
||||
const iconKeys = useMemo(() => Object.keys(IconNameToSvgMapping), [])
|
||||
|
||||
const iconOptions = useMemo(
|
||||
@@ -127,12 +118,10 @@ const IconPicker = ({
|
||||
) : (
|
||||
<Dropdown
|
||||
fullWidth={true}
|
||||
id="change-tag-icon-dropdown"
|
||||
label="Change the icon for a tag"
|
||||
items={iconOptions}
|
||||
value={selectedValue as string}
|
||||
onChange={handleIconChange}
|
||||
portal={portalDropdown}
|
||||
/>
|
||||
))}
|
||||
{currentType === 'emoji' && (
|
||||
|
||||
@@ -47,7 +47,6 @@ const SuperExportModal = ({ exportNotes, close }: Props) => {
|
||||
We detected your selection includes Super notes. How do you want to export them?
|
||||
</div>
|
||||
<Dropdown
|
||||
id="export-format-dropdown"
|
||||
label="Super notes export format"
|
||||
items={[
|
||||
{ label: 'Keep as Super', value: 'json' },
|
||||
@@ -61,7 +60,6 @@ const SuperExportModal = ({ exportNotes, close }: Props) => {
|
||||
value as PrefValue[PrefKey.SuperNoteExportFormat],
|
||||
)
|
||||
}}
|
||||
portal={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-passive-0">
|
||||
|
||||
@@ -23,8 +23,8 @@ export const usePopoverCloseOnClickOutside = ({
|
||||
const isAnchorElement = anchorElement ? anchorElement === event.target || anchorElement.contains(target) : false
|
||||
const closestPopoverId = target.closest('[data-popover]')?.getAttribute('data-popover')
|
||||
const isDescendantOfChildPopover = closestPopoverId && childPopovers.has(closestPopoverId)
|
||||
const isPopoverInModal = popoverElement?.closest('[aria-modal="true"]')
|
||||
const isDescendantOfModal = isPopoverInModal ? false : !!target.closest('[aria-modal="true"]')
|
||||
const isPopoverInModal = popoverElement?.closest('[data-dialog]')
|
||||
const isDescendantOfModal = isPopoverInModal ? false : !!target.closest('[data-dialog]')
|
||||
|
||||
if (!isDescendantOfMenu && !isAnchorElement && !isDescendantOfChildPopover && !isDescendantOfModal) {
|
||||
if (!disabled) {
|
||||
|
||||
@@ -127,15 +127,11 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
|
||||
<Text>Theme to be used for system light mode:</Text>
|
||||
<div className="mt-2">
|
||||
<Dropdown
|
||||
id="auto-light-theme-dropdown"
|
||||
label="Select the automatic light theme"
|
||||
items={themeItems}
|
||||
value={autoLightTheme}
|
||||
onChange={changeAutoLightTheme}
|
||||
disabled={!useDeviceSettings}
|
||||
classNameOverride={{
|
||||
popover: '!z-modal',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,15 +141,11 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
|
||||
<Text>Theme to be used for system dark mode:</Text>
|
||||
<div className="mt-2">
|
||||
<Dropdown
|
||||
id="auto-dark-theme-dropdown"
|
||||
label="Select the automatic dark theme"
|
||||
items={themeItems}
|
||||
value={autoDarkTheme}
|
||||
onChange={changeAutoDarkTheme}
|
||||
disabled={!useDeviceSettings}
|
||||
classNameOverride={{
|
||||
popover: '!z-modal',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,14 +94,10 @@ const EditorDefaults = ({ application }: Props) => {
|
||||
<Text>Sets the font size in plaintext and Super notes</Text>
|
||||
<div className="mt-2">
|
||||
<Dropdown
|
||||
id="def-font-size"
|
||||
label="Select the font size for plaintext notes"
|
||||
items={fontSizeDropdownOptions}
|
||||
value={fontSize}
|
||||
onChange={handleFontSizeChange}
|
||||
classNameOverride={{
|
||||
popover: '!z-modal',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,14 +107,10 @@ const EditorDefaults = ({ application }: Props) => {
|
||||
<Text>Sets the line height (leading) in plaintext and Super notes</Text>
|
||||
<div className="mt-2">
|
||||
<Dropdown
|
||||
id="def-line-height"
|
||||
label="Select the line height for plaintext notes"
|
||||
items={lineHeightDropdownOptions}
|
||||
value={lineHeight}
|
||||
onChange={handleLineHeightChange}
|
||||
classNameOverride={{
|
||||
popover: '!z-modal',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -125,14 +125,10 @@ const EmailBackups = ({ application }: Props) => {
|
||||
<Spinner className="h-4 w-4" />
|
||||
) : (
|
||||
<Dropdown
|
||||
id="def-editor-dropdown"
|
||||
label="Select email frequency"
|
||||
items={emailFrequencyOptions}
|
||||
value={emailFrequency}
|
||||
onChange={handleEmailFrequencyChange}
|
||||
classNameOverride={{
|
||||
popover: '!z-modal',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -131,7 +131,6 @@ const EditSmartViewModal = ({ controller, platform }: Props) => {
|
||||
}}
|
||||
platform={platform}
|
||||
useIconGrid={true}
|
||||
portalDropdown={false}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Dropdown from '../Dropdown/Dropdown'
|
||||
import { DropdownItem } from '../Dropdown/DropdownItem'
|
||||
import PreferencesMenuItem from './PreferencesComponents/MenuItem'
|
||||
@@ -14,19 +13,6 @@ type Props = {
|
||||
menu: PreferencesMenu
|
||||
}
|
||||
|
||||
const StyledDropdown = styled(Dropdown)`
|
||||
[data-reach-listbox-button] {
|
||||
background: var(--sn-stylekit-contrast-background-color);
|
||||
color: var(--sn-stylekit-info-color);
|
||||
font-weight: bold;
|
||||
padding: 0.55rem 0.875rem;
|
||||
|
||||
[data-reach-listbox-arrow] svg {
|
||||
fill: var(--sn-stylekit-info-color);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const PreferencesMenuView: FunctionComponent<Props> = ({ menu }) => {
|
||||
const application = useApplication()
|
||||
const { selectedPaneId, selectPane, menuItems } = menu
|
||||
@@ -83,8 +69,7 @@ const PreferencesMenuView: FunctionComponent<Props> = ({ menu }) => {
|
||||
))}
|
||||
</div>
|
||||
<div className="md:hidden">
|
||||
<StyledDropdown
|
||||
id="preferences-menu"
|
||||
<Dropdown
|
||||
items={dropdownMenuItems}
|
||||
label="Preferences Menu"
|
||||
value={selectedPaneId}
|
||||
@@ -94,8 +79,8 @@ const PreferencesMenuView: FunctionComponent<Props> = ({ menu }) => {
|
||||
classNameOverride={{
|
||||
wrapper: 'relative',
|
||||
popover: 'bottom-full w-full max-h-max',
|
||||
button: 'focus:outline-none focus:shadow-none focus:ring-none',
|
||||
}}
|
||||
portal={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -181,7 +181,6 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
|
||||
}}
|
||||
platform={platform}
|
||||
useIconGrid={true}
|
||||
portalDropdown={false}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
@@ -90,7 +90,6 @@ const CodeOptionsPlugin = () => {
|
||||
<>
|
||||
<div className="absolute top-2 right-6 rounded border border-border bg-default p-2">
|
||||
<Dropdown
|
||||
id="code-language-dropdown"
|
||||
label="Change code block language"
|
||||
items={CODE_LANGUAGE_OPTIONS.map(([value, label]) => ({
|
||||
label,
|
||||
|
||||
Reference in New Issue
Block a user