refactor: replace 'preact' with 'react' (#1048)
This commit is contained in:
@@ -1,21 +1,26 @@
|
||||
import { JSX, FunctionComponent, ComponentChildren, VNode, RefCallback, ComponentChild, toChildArray } from 'preact'
|
||||
import { useCallback, useEffect, useRef } from 'preact/hooks'
|
||||
import { JSXInternal } from 'preact/src/jsx'
|
||||
import { MenuItem, MenuItemListElement } from './MenuItem'
|
||||
import {
|
||||
CSSProperties,
|
||||
FunctionComponent,
|
||||
KeyboardEventHandler,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { KeyboardKey } from '@/Services/IOService'
|
||||
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
|
||||
|
||||
type MenuProps = {
|
||||
className?: string
|
||||
style?: string | JSX.CSSProperties | undefined
|
||||
style?: CSSProperties | undefined
|
||||
a11yLabel: string
|
||||
children: ComponentChildren
|
||||
children: ReactNode
|
||||
closeMenu?: () => void
|
||||
isOpen: boolean
|
||||
initialFocus?: number
|
||||
}
|
||||
|
||||
export const Menu: FunctionComponent<MenuProps> = ({
|
||||
const Menu: FunctionComponent<MenuProps> = ({
|
||||
children,
|
||||
className = '',
|
||||
style,
|
||||
@@ -24,16 +29,10 @@ export const Menu: FunctionComponent<MenuProps> = ({
|
||||
isOpen,
|
||||
initialFocus,
|
||||
}: MenuProps) => {
|
||||
const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([])
|
||||
|
||||
const menuElementRef = useRef<HTMLMenuElement>(null)
|
||||
|
||||
const handleKeyDown: JSXInternal.KeyboardEventHandler<HTMLMenuElement> = useCallback(
|
||||
const handleKeyDown: KeyboardEventHandler<HTMLMenuElement> = useCallback(
|
||||
(event) => {
|
||||
if (!menuItemRefs.current) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === KeyboardKey.Escape) {
|
||||
closeMenu?.()
|
||||
return
|
||||
@@ -45,58 +44,13 @@ export const Menu: FunctionComponent<MenuProps> = ({
|
||||
useListKeyboardNavigation(menuElementRef, initialFocus)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && menuItemRefs.current.length > 0) {
|
||||
if (isOpen) {
|
||||
setTimeout(() => {
|
||||
menuElementRef.current?.focus()
|
||||
})
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const pushRefToArray: RefCallback<HTMLLIElement> = useCallback((instance) => {
|
||||
if (instance && instance.children) {
|
||||
Array.from(instance.children).forEach((child) => {
|
||||
if (
|
||||
child.getAttribute('role')?.includes('menuitem') &&
|
||||
!menuItemRefs.current.includes(child as HTMLButtonElement)
|
||||
) {
|
||||
menuItemRefs.current.push(child as HTMLButtonElement)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const mapMenuItems = useCallback(
|
||||
(child: ComponentChild, index: number, array: ComponentChild[]): ComponentChild => {
|
||||
if (!child || (Array.isArray(child) && child.length < 1)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(child)) {
|
||||
return child.map(mapMenuItems)
|
||||
}
|
||||
|
||||
const _child = child as VNode<unknown>
|
||||
const isFirstMenuItem = index === array.findIndex((child) => (child as VNode<unknown>).type === MenuItem)
|
||||
|
||||
const hasMultipleItems = Array.isArray(_child.props.children)
|
||||
? Array.from(_child.props.children as ComponentChild[]).some(
|
||||
(child) => (child as VNode<unknown>).type === MenuItem,
|
||||
)
|
||||
: false
|
||||
|
||||
const items = hasMultipleItems ? [...(_child.props.children as ComponentChild[])] : [_child]
|
||||
|
||||
return items.map((child) => {
|
||||
return (
|
||||
<MenuItemListElement isFirstMenuItem={isFirstMenuItem} ref={pushRefToArray}>
|
||||
{child}
|
||||
</MenuItemListElement>
|
||||
)
|
||||
})
|
||||
},
|
||||
[pushRefToArray],
|
||||
)
|
||||
|
||||
return (
|
||||
<menu
|
||||
className={`m-0 p-0 list-style-none focus:shadow-none ${className}`}
|
||||
@@ -105,7 +59,9 @@ export const Menu: FunctionComponent<MenuProps> = ({
|
||||
style={style}
|
||||
aria-label={a11yLabel}
|
||||
>
|
||||
{toChildArray(children).map(mapMenuItems)}
|
||||
{children}
|
||||
</menu>
|
||||
)
|
||||
}
|
||||
|
||||
export default Menu
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { ComponentChildren, FunctionComponent, VNode } from 'preact'
|
||||
import { forwardRef, Ref } from 'preact/compat'
|
||||
import { JSXInternal } from 'preact/src/jsx'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { Switch, SwitchProps } from '@/Components/Switch/Switch'
|
||||
import { forwardRef, MouseEventHandler, ReactNode, Ref } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import { SwitchProps } from '@/Components/Switch/SwitchProps'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants'
|
||||
|
||||
export enum MenuItemType {
|
||||
IconButton,
|
||||
RadioButton,
|
||||
SwitchButton,
|
||||
}
|
||||
import { MenuItemType } from './MenuItemType'
|
||||
|
||||
type MenuItemProps = {
|
||||
type: MenuItemType
|
||||
children: ComponentChildren
|
||||
onClick?: JSXInternal.MouseEventHandler<HTMLButtonElement>
|
||||
children: ReactNode
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>
|
||||
onChange?: SwitchProps['onChange']
|
||||
onBlur?: (event: { relatedTarget: EventTarget | null }) => void
|
||||
className?: string
|
||||
@@ -25,7 +19,7 @@ type MenuItemProps = {
|
||||
tabIndex?: number
|
||||
}
|
||||
|
||||
export const MenuItem: FunctionComponent<MenuItemProps> = forwardRef(
|
||||
const MenuItem = forwardRef(
|
||||
(
|
||||
{
|
||||
children,
|
||||
@@ -42,63 +36,42 @@ export const MenuItem: FunctionComponent<MenuItemProps> = forwardRef(
|
||||
ref: Ref<HTMLButtonElement>,
|
||||
) => {
|
||||
return type === MenuItemType.SwitchButton && typeof onChange === 'function' ? (
|
||||
<button
|
||||
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none justify-between"
|
||||
onClick={() => {
|
||||
onChange(!checked)
|
||||
}}
|
||||
onBlur={onBlur}
|
||||
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
role="menuitemcheckbox"
|
||||
aria-checked={checked}
|
||||
>
|
||||
<span className="flex flex-grow items-center">{children}</span>
|
||||
<Switch className="px-0" checked={checked} />
|
||||
</button>
|
||||
<li className="list-style-none" role="none">
|
||||
<button
|
||||
ref={ref}
|
||||
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none justify-between"
|
||||
onClick={() => {
|
||||
onChange(!checked)
|
||||
}}
|
||||
onBlur={onBlur}
|
||||
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
role="menuitemcheckbox"
|
||||
aria-checked={checked}
|
||||
>
|
||||
<span className="flex flex-grow items-center">{children}</span>
|
||||
<Switch className="px-0" checked={checked} />
|
||||
</button>
|
||||
</li>
|
||||
) : (
|
||||
<button
|
||||
ref={ref}
|
||||
role={type === MenuItemType.RadioButton ? 'menuitemradio' : 'menuitem'}
|
||||
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
className={`sn-dropdown-item focus:bg-info-backdrop focus:shadow-none ${className}`}
|
||||
onClick={onClick}
|
||||
onBlur={onBlur}
|
||||
{...(type === MenuItemType.RadioButton ? { 'aria-checked': checked } : {})}
|
||||
>
|
||||
{type === MenuItemType.IconButton && icon ? <Icon type={icon} className={iconClassName} /> : null}
|
||||
{type === MenuItemType.RadioButton && typeof checked === 'boolean' ? (
|
||||
<div className={`pseudo-radio-btn ${checked ? 'pseudo-radio-btn--checked' : ''} flex-shrink-0`}></div>
|
||||
) : null}
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const MenuItemSeparator: FunctionComponent = () => <div role="separator" className="h-1px my-2 bg-border"></div>
|
||||
|
||||
type ListElementProps = {
|
||||
isFirstMenuItem: boolean
|
||||
children: ComponentChildren
|
||||
}
|
||||
|
||||
export const MenuItemListElement: FunctionComponent<ListElementProps> = forwardRef(
|
||||
({ children, isFirstMenuItem }: ListElementProps, ref: Ref<HTMLLIElement>) => {
|
||||
const child = children as VNode<unknown>
|
||||
return (
|
||||
<li className="list-style-none" role="none" ref={ref}>
|
||||
{{
|
||||
...child,
|
||||
props: {
|
||||
...(child.props ? { ...child.props } : {}),
|
||||
...(child.type === MenuItem
|
||||
? {
|
||||
tabIndex: isFirstMenuItem ? 0 : -1,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}}
|
||||
<li className="list-style-none" role="none">
|
||||
<button
|
||||
ref={ref}
|
||||
role={type === MenuItemType.RadioButton ? 'menuitemradio' : 'menuitem'}
|
||||
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
className={`sn-dropdown-item focus:bg-info-backdrop focus:shadow-none ${className}`}
|
||||
onClick={onClick}
|
||||
onBlur={onBlur}
|
||||
{...(type === MenuItemType.RadioButton ? { 'aria-checked': checked } : {})}
|
||||
>
|
||||
{type === MenuItemType.IconButton && icon ? <Icon type={icon} className={iconClassName} /> : null}
|
||||
{type === MenuItemType.RadioButton && typeof checked === 'boolean' ? (
|
||||
<div className={`pseudo-radio-btn ${checked ? 'pseudo-radio-btn--checked' : ''} flex-shrink-0`}></div>
|
||||
) : null}
|
||||
{children}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default MenuItem
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
|
||||
const MenuItemSeparator: FunctionComponent = () => (
|
||||
<li className="list-style-none" role="none">
|
||||
<div role="separator" className="h-1px my-2 bg-border" />
|
||||
</li>
|
||||
)
|
||||
|
||||
export default MenuItemSeparator
|
||||
5
app/assets/javascripts/Components/Menu/MenuItemType.ts
Normal file
5
app/assets/javascripts/Components/Menu/MenuItemType.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum MenuItemType {
|
||||
IconButton,
|
||||
RadioButton,
|
||||
SwitchButton,
|
||||
}
|
||||
Reference in New Issue
Block a user