refactor: popover a11y aria attributes

This commit is contained in:
Aman Harwara
2023-08-14 14:42:33 +05:30
parent e1c5d52dbe
commit 4865e3ba28
31 changed files with 110 additions and 88 deletions

View File

@@ -32,7 +32,7 @@ const WorkspaceSwitcherOption: FunctionComponent<Props> = ({ mainApplicationGrou
<Popover <Popover
title="Switch workspace" title="Switch workspace"
align="end" align="end"
anchorElement={buttonRef.current} anchorElement={buttonRef}
className="py-2" className="py-2"
open={isOpen} open={isOpen}
side="right" side="right"

View File

@@ -27,7 +27,7 @@ const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainApplication
<Popover <Popover
title="Switch workspace" title="Switch workspace"
align="center" align="center"
anchorElement={buttonRef.current} anchorElement={buttonRef}
className="py-2" className="py-2"
open={isOpen} open={isOpen}
overrideZIndex="z-modal" overrideZIndex="z-modal"

View File

@@ -77,7 +77,7 @@ const ChangeEditorButton: FunctionComponent<Props> = ({ noteViewController, onCl
title="Change note type" title="Change note type"
togglePopover={toggleMenu} togglePopover={toggleMenu}
disableClickOutside={isClickOutsideDisabled} disableClickOutside={isClickOutsideDisabled}
anchorElement={buttonRef.current} anchorElement={buttonRef}
open={isOpen} open={isOpen}
className="pt-2 md:pt-0" className="pt-2 md:pt-0"
> >

View File

@@ -23,7 +23,7 @@ const ChangeMultipleButton = ({ application, notesController }: Props) => {
title="Change note type" title="Change note type"
togglePopover={toggleMenu} togglePopover={toggleMenu}
disableClickOutside={disableClickOutside} disableClickOutside={disableClickOutside}
anchorElement={changeButtonRef.current} anchorElement={changeButtonRef}
open={isChangeMenuOpen} open={isChangeMenuOpen}
className="pt-2 md:pt-0" className="pt-2 md:pt-0"
> >

View File

@@ -67,47 +67,49 @@ const AddItemMenuButton = ({
<Icon type="add" size="custom" className="h-5 w-5" /> <Icon type="add" size="custom" className="h-5 w-5" />
</button> </button>
</StyledTooltip> </StyledTooltip>
<Popover {canShowMenu && (
title="Add item" <Popover
open={canShowMenu && isMenuOpen} title="Add item"
anchorElement={addItemButtonRef.current} open={isMenuOpen}
togglePopover={() => { anchorElement={addItemButtonRef}
setIsMenuOpen((isOpen) => !isOpen) togglePopover={() => {
}} setIsMenuOpen((isOpen) => !isOpen)
side="bottom" }}
align="center" side="bottom"
className="py-2" align="center"
> className="py-2"
<Menu a11yLabel={'test'} isOpen={isMenuOpen}> >
<MenuItem <Menu a11yLabel={'test'} isOpen={isMenuOpen}>
onClick={() => { <MenuItem
addNewItem() onClick={() => {
setIsMenuOpen(false) addNewItem()
}} setIsMenuOpen(false)
> }}
<Icon type="add" className="mr-2" /> >
{addButtonLabel} <Icon type="add" className="mr-2" />
</MenuItem> {addButtonLabel}
<MenuItem </MenuItem>
onClick={async () => { <MenuItem
setCaptureType('photo') onClick={async () => {
setIsMenuOpen(false) setCaptureType('photo')
}} setIsMenuOpen(false)
> }}
<Icon type="camera" className="mr-2" /> >
Take photo <Icon type="camera" className="mr-2" />
</MenuItem> Take photo
<MenuItem </MenuItem>
onClick={async () => { <MenuItem
setCaptureType('video') onClick={async () => {
setIsMenuOpen(false) setCaptureType('video')
}} setIsMenuOpen(false)
> }}
<Icon type="camera" className="mr-2" /> >
Record video <Icon type="camera" className="mr-2" />
</MenuItem> Record video
</Menu> </MenuItem>
</Popover> </Menu>
</Popover>
)}
<ModalOverlay isOpen={captureType === 'photo'} close={closeCaptureModal}> <ModalOverlay isOpen={captureType === 'photo'} close={closeCaptureModal}>
<PhotoCaptureModal filesController={filesController} close={closeCaptureModal} /> <PhotoCaptureModal filesController={filesController} close={closeCaptureModal} />
</ModalOverlay> </ModalOverlay>

View File

@@ -116,7 +116,7 @@ const ContentListHeader = ({
/> />
<Popover <Popover
open={showDisplayOptionsMenu} open={showDisplayOptionsMenu}
anchorElement={displayOptionsButtonRef.current} anchorElement={displayOptionsButtonRef}
togglePopover={toggleDisplayOptionsMenu} togglePopover={toggleDisplayOptionsMenu}
align="start" align="start"
className="py-2" className="py-2"

View File

@@ -67,7 +67,7 @@ const ContextMenuCell = ({ items }: { items: DecryptedItemInterface[] }) => {
<Popover <Popover
title="File options" title="File options"
open={contextMenuVisible} open={contextMenuVisible}
anchorElement={anchorElementRef.current} anchorElement={anchorElementRef}
togglePopover={() => { togglePopover={() => {
setContextMenuVisible(false) setContextMenuVisible(false)
}} }}
@@ -120,7 +120,7 @@ const ItemLinksCell = ({ item }: { item: DecryptedItemInterface }) => {
<Popover <Popover
title="Linked items" title="Linked items"
open={contextMenuVisible} open={contextMenuVisible}
anchorElement={anchorElementRef.current} anchorElement={anchorElementRef}
togglePopover={() => { togglePopover={() => {
setContextMenuVisible(false) setContextMenuVisible(false)
}} }}

View File

@@ -19,13 +19,7 @@ const FilesOptionsPanel = ({ itemListController }: Props) => {
return ( return (
<> <>
<RoundIconButton label="File options menu" onClick={toggleMenu} ref={buttonRef} icon="more" /> <RoundIconButton label="File options menu" onClick={toggleMenu} ref={buttonRef} icon="more" />
<Popover <Popover title="File options" togglePopover={toggleMenu} anchorElement={buttonRef} open={isOpen} className="py-2">
title="File options"
togglePopover={toggleMenu}
anchorElement={buttonRef.current}
open={isOpen}
className="py-2"
>
<Menu a11yLabel="File options panel" isOpen={isOpen}> <Menu a11yLabel="File options panel" isOpen={isOpen}>
<FileMenuOptions <FileMenuOptions
selectedFiles={itemListController.selectedFiles} selectedFiles={itemListController.selectedFiles}

View File

@@ -212,7 +212,7 @@ const FilePreviewModal = observer(({ application }: Props) => {
<Popover <Popover
title="File options" title="File options"
open={showOptionsMenu} open={showOptionsMenu}
anchorElement={menuButtonRef.current} anchorElement={menuButtonRef}
togglePopover={closeOptionsMenu} togglePopover={closeOptionsMenu}
side="bottom" side="bottom"
align="start" align="start"

View File

@@ -100,7 +100,7 @@ const FileViewWithoutProtection = ({ application, file }: FileViewProps) => {
title="Details" title="Details"
open={isFileInfoPanelOpen} open={isFileInfoPanelOpen}
togglePopover={toggleFileInfoPanel} togglePopover={toggleFileInfoPanel}
anchorElement={fileInfoButtonRef.current} anchorElement={fileInfoButtonRef}
side="bottom" side="bottom"
align="center" align="center"
> >

View File

@@ -36,7 +36,7 @@ const AccountMenuButton = ({ hasError, controller, mainApplicationGroup, onClick
</StyledTooltip> </StyledTooltip>
<Popover <Popover
title="Account" title="Account"
anchorElement={buttonRef.current} anchorElement={buttonRef}
open={isOpen} open={isOpen}
togglePopover={toggleMenu} togglePopover={toggleMenu}
side="top" side="top"

View File

@@ -51,7 +51,7 @@ const QuickSettingsButton = ({ application, isMobileNavigation = false }: Props)
<Popover <Popover
title="Quick settings" title="Quick settings"
togglePopover={toggleMenu} togglePopover={toggleMenu}
anchorElement={buttonRef.current} anchorElement={buttonRef}
open={isOpen} open={isOpen}
side="top" side="top"
align="start" align="start"

View File

@@ -56,7 +56,7 @@ const VaultSelectionButton = ({ isMobileNavigation = false }: { isMobileNavigati
<Popover <Popover
title="Vault options" title="Vault options"
togglePopover={toggleMenu} togglePopover={toggleMenu}
anchorElement={buttonRef.current} anchorElement={buttonRef}
open={isOpen} open={isOpen}
side="top" side="top"
align="start" align="start"

View File

@@ -32,7 +32,7 @@ const LinkedItemsButton = ({ linkingController, onClickPreprocessing }: Props) =
<Popover <Popover
title="Linked items" title="Linked items"
togglePopover={toggleMenu} togglePopover={toggleMenu}
anchorElement={buttonRef.current} anchorElement={buttonRef}
open={isLinkingPanelOpen} open={isLinkingPanelOpen}
className="pb-2" className="pb-2"
> >

View File

@@ -101,7 +101,7 @@ export const LinkedItemsSectionItem = ({
title="Options" title="Options"
open={isMenuOpen} open={isMenuOpen}
togglePopover={toggleMenu} togglePopover={toggleMenu}
anchorElement={menuButtonRef.current} anchorElement={menuButtonRef}
side="bottom" side="bottom"
align="center" align="center"
className="py-2" className="py-2"

View File

@@ -8,6 +8,7 @@ import {
MouseEventHandler, MouseEventHandler,
ReactNode, ReactNode,
useCallback, useCallback,
useRef,
useState, useState,
} from 'react' } from 'react'
import Icon from '../Icon/Icon' import Icon from '../Icon/Icon'
@@ -27,12 +28,12 @@ const Tooltip = ({ text }: { text: string }) => {
[visible], [visible],
) )
const [anchorElement, setAnchorElement] = useState<HTMLDivElement | null>(null) const anchorElement = useRef(null)
return ( return (
<div className="relative"> <div className="relative">
<div <div
ref={setAnchorElement} ref={anchorElement}
className={classNames('peer z-0 flex h-5 w-5 items-center justify-center rounded-full')} className={classNames('peer z-0 flex h-5 w-5 items-center justify-center rounded-full')}
onClick={onClickMobile} onClick={onClickMobile}
onMouseEnter={() => setVisible(true)} onMouseEnter={() => setVisible(true)}

View File

@@ -131,7 +131,7 @@ const Modal = ({
<Popover <Popover
title="Advanced" title="Advanced"
open={showAdvanced} open={showAdvanced}
anchorElement={advancedOptionRef.current} anchorElement={advancedOptionRef}
disableMobileFullscreenTakeover={true} disableMobileFullscreenTakeover={true}
togglePopover={() => setShowAdvanced((show) => !show)} togglePopover={() => setShowAdvanced((show) => !show)}
align="start" align="start"

View File

@@ -67,7 +67,7 @@ const AddTagOption: FunctionComponent<Props> = ({
<Popover <Popover
title="Add tag" title="Add tag"
togglePopover={toggleMenu} togglePopover={toggleMenu}
anchorElement={buttonRef.current} anchorElement={buttonRef}
open={isOpen} open={isOpen}
side="right" side="right"
align="start" align="start"

View File

@@ -52,7 +52,7 @@ const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ applic
<Popover <Popover
title="Change note type" title="Change note type"
align="start" align="start"
anchorElement={buttonRef.current} anchorElement={buttonRef}
className="pt-2 md:pt-0" className="pt-2 md:pt-0"
open={isOpen} open={isOpen}
side="right" side="right"

View File

@@ -50,7 +50,7 @@ const ListedActionsOption: FunctionComponent<Props> = ({ application, note, icon
<Popover <Popover
title="Listed" title="Listed"
togglePopover={toggleMenu} togglePopover={toggleMenu}
anchorElement={buttonRef.current} anchorElement={buttonRef}
open={isOpen} open={isOpen}
side="right" side="right"
align="end" align="end"

View File

@@ -35,7 +35,7 @@ const NotesOptionsPanel = ({ notesController, onClickPreprocessing }: Props) =>
title="Note options" title="Note options"
disableClickOutside={disableClickOutside} disableClickOutside={disableClickOutside}
togglePopover={toggleMenu} togglePopover={toggleMenu}
anchorElement={buttonRef.current} anchorElement={buttonRef}
open={isOpen} open={isOpen}
className="select-none pt-2" className="select-none pt-2"
> >

View File

@@ -52,6 +52,7 @@ const MobilePopoverContent = ({
<div <div
ref={mergeRefs([setPopoverElement, addCloseMethod])} ref={mergeRefs([setPopoverElement, addCloseMethod])}
className="fixed left-0 top-0 z-modal flex h-full w-full flex-col bg-default pb-safe-bottom pt-safe-top" className="fixed left-0 top-0 z-modal flex h-full w-full flex-col bg-default pb-safe-bottom pt-safe-top"
id={'popover/' + id}
data-popover={id} data-popover={id}
data-mobile-popover data-mobile-popover
> >

View File

@@ -1,7 +1,6 @@
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler' import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
import { UuidGenerator } from '@standardnotes/snjs' import { createContext, useCallback, useContext, useEffect, useId, useMemo, useState } from 'react'
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import MobilePopoverContent from './MobilePopoverContent' import MobilePopoverContent from './MobilePopoverContent'
import PositionedPopoverContent from './PositionedPopoverContent' import PositionedPopoverContent from './PositionedPopoverContent'
import { PopoverProps } from './Types' import { PopoverProps } from './Types'
@@ -62,11 +61,11 @@ const PositionedPopoverContentWithAnimation = (
} }
const Popover = (props: PopoverProps) => { const Popover = (props: PopoverProps) => {
const popoverId = useRef(UuidGenerator.GenerateUuid()) const popoverId = useId()
const addAndroidBackHandler = useAndroidBackHandler() const addAndroidBackHandler = useAndroidBackHandler()
useRegisterPopoverToParent(popoverId.current) useRegisterPopoverToParent(popoverId)
const [childPopovers, setChildPopovers] = useState<Set<string>>(new Set()) const [childPopovers, setChildPopovers] = useState<Set<string>>(new Set())
@@ -106,6 +105,27 @@ const Popover = (props: PopoverProps) => {
} }
}, [addAndroidBackHandler, props, props.open]) }, [addAndroidBackHandler, props, props.open])
useEffect(() => {
const anchorElement =
props.anchorElement && 'current' in props.anchorElement ? props.anchorElement.current : props.anchorElement
if (anchorElement) {
anchorElement.setAttribute('aria-haspopup', 'true')
if (props.open) {
anchorElement.setAttribute('aria-expanded', 'true')
} else {
anchorElement.removeAttribute('aria-expanded')
}
}
return () => {
if (anchorElement) {
anchorElement.removeAttribute('aria-haspopup')
anchorElement.removeAttribute('aria-expanded')
}
}
}, [props.anchorElement, props.open])
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm) const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
if (isMobileScreen && !props.disableMobileFullscreenTakeover) { if (isMobileScreen && !props.disableMobileFullscreenTakeover) {
@@ -117,7 +137,7 @@ const Popover = (props: PopoverProps) => {
}} }}
title={props.title} title={props.title}
className={props.className} className={props.className}
id={popoverId.current} id={popoverId}
> >
{props.children} {props.children}
</MobilePopoverContent> </MobilePopoverContent>
@@ -126,7 +146,7 @@ const Popover = (props: PopoverProps) => {
return ( return (
<PopoverContext.Provider value={contextValue}> <PopoverContext.Provider value={contextValue}>
<PositionedPopoverContentWithAnimation {...props} childPopovers={childPopovers} id={popoverId.current} /> <PositionedPopoverContentWithAnimation {...props} childPopovers={childPopovers} id={popoverId} />
</PopoverContext.Provider> </PopoverContext.Provider>
) )
} }

View File

@@ -35,7 +35,8 @@ const PositionedPopoverContent = ({
}: PopoverContentProps) => { }: PopoverContentProps) => {
const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null) const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
const popoverRect = useAutoElementRect(popoverElement) const popoverRect = useAutoElementRect(popoverElement)
const anchorElementRect = useAutoElementRect(anchorElement, { const resolvedAnchorElement = anchorElement && 'current' in anchorElement ? anchorElement.current : anchorElement
const anchorElementRect = useAutoElementRect(resolvedAnchorElement, {
updateOnWindowResize: true, updateOnWindowResize: true,
}) })
const anchorPointRect = DOMRect.fromRect({ const anchorPointRect = DOMRect.fromRect({
@@ -75,7 +76,7 @@ const PositionedPopoverContent = ({
usePopoverCloseOnClickOutside({ usePopoverCloseOnClickOutside({
popoverElement, popoverElement,
anchorElement, anchorElement: resolvedAnchorElement,
togglePopover, togglePopover,
childPopovers, childPopovers,
hideOnClickInModal, hideOnClickInModal,
@@ -122,13 +123,14 @@ const PositionedPopoverContent = ({
} as CSSProperties } as CSSProperties
} }
ref={mergeRefs([setPopoverElement, addCloseMethod])} ref={mergeRefs([setPopoverElement, addCloseMethod])}
id={'popover/' + id}
data-popover={id} data-popover={id}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === KeyboardKey.Escape) { if (event.key === KeyboardKey.Escape) {
event.stopPropagation() event.stopPropagation()
togglePopover?.() togglePopover?.()
if (anchorElement) { if (resolvedAnchorElement) {
anchorElement.focus() resolvedAnchorElement.focus()
} }
} }
}} }}

View File

@@ -1,4 +1,4 @@
import { ReactNode } from 'react' import { ReactNode, RefObject } from 'react'
export type PopoverState = 'closed' | 'positioning' | 'open' export type PopoverState = 'closed' | 'positioning' | 'open'
@@ -20,8 +20,10 @@ type Point = {
y: number y: number
} }
type AnchorElementOrRef = RefObject<HTMLElement | null> | HTMLElement | null
type PopoverAnchorElementProps = { type PopoverAnchorElementProps = {
anchorElement: HTMLElement | null anchorElement: AnchorElementOrRef
anchorPoint?: never anchorPoint?: never
} }
@@ -49,7 +51,7 @@ type CommonPopoverProps = {
} }
export type PopoverContentProps = CommonPopoverProps & { export type PopoverContentProps = CommonPopoverProps & {
anchorElement?: HTMLElement | null anchorElement?: AnchorElementOrRef
anchorPoint?: Point anchorPoint?: Point
childPopovers: Set<string> childPopovers: Set<string>
togglePopover?: () => void togglePopover?: () => void

View File

@@ -117,7 +117,7 @@ const EditSmartViewModal = ({ controller, platform }: Props) => {
<Popover <Popover
title="Choose icon" title="Choose icon"
open={shouldShowIconPicker} open={shouldShowIconPicker}
anchorElement={iconPickerButtonRef.current} anchorElement={iconPickerButtonRef}
togglePopover={toggleIconPicker} togglePopover={toggleIconPicker}
align="start" align="start"
overrideZIndex="z-modal" overrideZIndex="z-modal"

View File

@@ -244,7 +244,7 @@ const EditVaultModal: FunctionComponent<Props> = ({ onCloseDialog, existingVault
<Popover <Popover
title="Choose icon" title="Choose icon"
open={shouldShowIconPicker} open={shouldShowIconPicker}
anchorElement={iconPickerButtonRef.current} anchorElement={iconPickerButtonRef}
togglePopover={toggleIconPicker} togglePopover={toggleIconPicker}
align="start" align="start"
overrideZIndex="z-modal" overrideZIndex="z-modal"

View File

@@ -51,7 +51,7 @@ const HistoryModalDialogContent = ({ dismissModal, note }: RevisionHistoryModalC
<Popover <Popover
title="Advanced" title="Advanced"
open={showTabMenu} open={showTabMenu}
anchorElement={tabOptionRef.current} anchorElement={tabOptionRef}
disableMobileFullscreenTakeover={true} disableMobileFullscreenTakeover={true}
togglePopover={toggleTabMenu} togglePopover={toggleTabMenu}
align="start" align="start"

View File

@@ -168,7 +168,7 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
<Popover <Popover
title="Choose icon" title="Choose icon"
open={shouldShowIconPicker} open={shouldShowIconPicker}
anchorElement={iconPickerButtonRef.current} anchorElement={iconPickerButtonRef}
togglePopover={toggleIconPicker} togglePopover={toggleIconPicker}
align="start" align="start"
overrideZIndex="z-modal" overrideZIndex="z-modal"

View File

@@ -98,7 +98,7 @@ export const ItemSelectionPlugin: FunctionComponent<Props> = ({ currentNote }) =
<Popover <Popover
title="Select item" title="Select item"
align="start" align="start"
anchorElement={anchorElementRef.current} anchorElement={anchorElementRef}
open={true} open={true}
disableMobileFullscreenTakeover={true} disableMobileFullscreenTakeover={true}
side={isMobileScreen() ? 'top' : 'bottom'} side={isMobileScreen() ? 'top' : 'bottom'}

View File

@@ -140,7 +140,7 @@ const AddToVaultMenuOption = ({ iconClassName, items }: { iconClassName: string;
<Popover <Popover
title="Move to vault" title="Move to vault"
togglePopover={toggleSubMenu} togglePopover={toggleSubMenu}
anchorElement={buttonRef.current} anchorElement={buttonRef}
open={isSubMenuOpen} open={isSubMenuOpen}
side="right" side="right"
align="start" align="start"