UNPKG

@gluestack-ui/menu

Version:

A universal headless menu component for React Native, Next.js & React

89 lines (88 loc) 3.43 kB
import React from 'react'; import { useControllableState } from '@gluestack-ui/hooks'; import { Overlay } from '@gluestack-ui/overlay'; import { PopoverProvider } from './PopoverContext'; import { FocusScope as FocusScopeAria } from '@react-native-aria/focus'; import { PopoverContent } from './PopoverContent'; import { MenuContext } from '../MenuContext'; export const Popover = ({ state, onOpen, trigger, children, defaultIsOpen = false, initialFocusRef, finalFocusRef, useRNModal, trapFocus = true, placement = 'bottom', shouldOverlapWithTrigger = false, crossOffset, offset, triggerRef, shouldFlip, focusScope = true, StyledBackdrop, }) => { const [isOpen, setIsOpen] = useControllableState({ value: state?.isOpen, defaultValue: defaultIsOpen, onChange: (value) => { value ? onOpen && onOpen() : state.close && state.close(); }, }); const { onClose } = React.useContext(MenuContext); const [bodyMounted, setBodyMounted] = React.useState(false); const [headerMounted, setHeaderMounted] = React.useState(false); let idCounter = 0; function uniqueId(prefix = '') { const id = ++idCounter; return prefix + id; } const id = uniqueId(); const popoverContentId = `${id}-content`; const headerId = `${popoverContentId}-header`; const bodyId = `${popoverContentId}-body`; const handleOpen = React.useCallback(() => { setIsOpen(true); }, [setIsOpen]); const handleClose = React.useCallback(() => { setIsOpen(false); }, [setIsOpen]); const updatedTrigger = (reference) => { if (trigger) { return trigger({ 'ref': reference, 'onPress': handleOpen, 'aria-expanded': isOpen ? true : false, 'aria-controls': isOpen ? popoverContentId : undefined, 'aria-haspopup': true, }, { open: isOpen }); } return null; }; const targetRefTemp = React.useRef(null); const targetRef = triggerRef || targetRefTemp; return (<> {updatedTrigger(targetRef)} <Overlay isOpen={isOpen} onRequestClose={handleClose} isKeyboardDismissable useRNModal={useRNModal}> <PopoverProvider value={{ onClose: handleClose, targetRef, strategy: 'absolute', handleClose: handleClose, initialFocusRef, finalFocusRef, popoverContentId, bodyId, headerId, headerMounted, bodyMounted, setBodyMounted, setHeaderMounted, isOpen, placement, shouldOverlapWithTrigger, crossOffset, offset, shouldFlip, }}> <StyledBackdrop onPress={onClose} tabIndex={-1} // for ios accessibilityElementsHidden importantForAccessibility="no-hide-descendants" aria-hidden={true}/> <FocusScopeComponent trapFocus={trapFocus} focusScope={focusScope}> <PopoverContent>{children}</PopoverContent> </FocusScopeComponent> </PopoverProvider> </Overlay> </>); }; const FocusScopeComponent = ({ trapFocus, focusScope, children }) => { if (focusScope) return (<FocusScopeAria contain={trapFocus} restoreFocus autoFocus> {children} </FocusScopeAria>); return children; };