UNPKG

curls

Version:

💪 Responsive, expressive UI primitives for React written with Style Hooks and Emotion

393 lines (362 loc) • 10.7 kB
import {jsx as ___EmotionJSX} from '@emotion/core' function _objectWithoutProperties(source, excluded) { if (source == null) return {} var target = _objectWithoutPropertiesLoose(source, excluded) var key, i if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source) for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i] if (excluded.indexOf(key) >= 0) continue if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue target[key] = source[key] } } return target } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {} var target = {} var sourceKeys = Object.keys(source) var key, i for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i] if (excluded.indexOf(key) >= 0) continue target[key] = source[key] } return target } function _extends() { _extends = Object.assign || function(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key] } } } return target } return _extends.apply(this, arguments) } import React, { useRef, useEffect, useContext, useState, useCallback, useMemo, } from 'react' import {css} from '@emotion/core' import {createElement, useStyles} from '@style-hooks/core' import useWindowSize from '@react-hook/window-size/throttled' import useLayoutEffect from '@react-hook/passive-layout-effect' import useMergedRef from '@react-hook/merged-ref' import useSwitch from '@react-hook/switch' import useWindowScroll from '@react-hook/window-scroll' import emptyArr from 'empty/array' import {useBox} from '../Box' import {useFade} from '../Fade' import useBreakpointValueParser from '../useBreakpointValueParser' import {portalize, objectWithoutProps, pushCss} from '../utils' import {setPlacementStyle} from './utils' import emptyObj from 'empty/object' export const PopoverContext = React.createContext({}), {Consumer: PopoverConsumer} = PopoverContext, usePopoverContext = () => useContext(PopoverContext) const defaultStyles = { name: '1o2bxov', styles: 'display:flex;position:fixed;z-index:1001;', }, withoutPop = { ref: 0, triggerRef: 0, style: 0, } const defaultTransition = ({ isOpen, /*, placement*/ }) => useFade({ visible: isOpen, from: 0, to: 1, }) function _ref(match) { return match.matches } export const usePopoverBox = props => { const popover = usePopoverContext() props = useStyles( 'popover', emptyObj, pushCss(props, [defaultStyles, popover.css]) ) props.id = props.id || popover.id props.tabIndex = props.tabIndex || '0' const {css} = (props.transition || defaultTransition)({ isOpen: popover.isOpen, placement: popover.placement, }) delete props.transition props.css = props.css ? [defaultStyles, css].concat(props.css) : [defaultStyles, css] props.style = props.style ? _extends({}, popover.style, props.style) : popover.style return props }, PopoverBox = React.forwardRef((props_, ref) => { const _useBox = useBox(usePopoverBox(props_)), {placement = 'bottom', portal, children} = _useBox, props = _objectWithoutProperties(_useBox, [ 'placement', 'portal', 'children', ]) const matches = useBreakpointValueParser(placement) const popover = usePopoverContext() // handles repositioning the popover // Yes this is correct, it's useEffect, not useLayoutEffect // Just move on. useEffect(() => { if (typeof placement === 'function') { popover.reposition(placement) } else if (matches) { popover.reposition(matches.filter(_ref).pop().value) } }, [placement, matches]) // handles closing the popover when the ESC key is pressed function _ref2() { return popover.ref.current.focus() } function _ref3(event) { return parseInt(event.keyCode) === 27 && popover.close() } useLayoutEffect(() => { if (popover.isOpen) { setTimeout(_ref2, 100) const callback = _ref3 popover.ref.current.addEventListener('keyup', callback) return () => popover.ref.current.removeEventListener('keyup', callback) } }, [popover.isOpen]) props.ref = useMergedRef(popover.ref, ref) props.children = typeof children === 'function' ? children(objectWithoutProps(popover, withoutPop)) : children return portalize(createElement('div', props), portal) }) let ID = 0 const PopoverContainer = React.memo( ({ open, close, toggle, isOpen, containPolicy, windowSize, scrollY, children, }) => { const triggerRef = useRef(null), popoverRef = useRef(null), id = useRef('curls.popover.' + ID++).current, [{style, requestedPlacement, placement}, setState] = useState({ style: {}, placement: null, requestedPlacement: null, }), reposition = useCallback( nextPlacement => { setState( setPlacementStyle( nextPlacement, triggerRef.current, popoverRef.current, containPolicy ) ) }, [containPolicy] ), childContext = useMemo( () => ({ isOpen, open, close, toggle, id, style, ref: popoverRef, placement, reposition, triggerRef, }), [isOpen, open, close, toggle, placement, reposition, style] ) useEffect(() => { isOpen && reposition(requestedPlacement) }, [isOpen, reposition, scrollY, windowSize[0], windowSize[1]]) return ___EmotionJSX(PopoverContext.Provider, { value: childContext, children: typeof children === 'function' ? children(childContext) : children, }) }, ( prev, next // bails out if the popover is closed and was closed ) => // and the children didn't change (next.isOpen === false && prev.isOpen === false && prev.children === next.children) || // bails out if all else is equal (prev.children === next.children && prev.isOpen === next.isOpen && prev.windowSize[0] === next.windowSize[0] && prev.windowSize[1] === next.windowSize[1] && prev.scrollY === next.scrollY && prev.containPolicy === next.containPolicy) ) export const PopoverMe = props => { const {children, on, tabIndex} = props const matches = useBreakpointValueParser(on), {isOpen, open, close, toggle, id} = usePopoverContext(), elementRef = useRef(null), ref = useMergedRef(usePopoverContext().triggerRef, elementRef), seen = useRef(false) // returns the focus to the trigger when the popover box closes if focus is // not an event that triggers opening the popover useLayoutEffect(() => { if (isOpen === false) { if (seen.current === true) { let isTriggeredByFocus = false for (let match of matches) if (match.matches === true && match.value === 'focus') { isTriggeredByFocus = true break } if (!isTriggeredByFocus) elementRef.current.focus() } seen.current = true } }, [isOpen]) // handles trigger events function _ref4(e) { e.stopPropagation() toggle() } function _ref5(args) { return elementRef.current.removeEventListener(...args) } useLayoutEffect(() => { if (elementRef.current && Array.isArray(matches)) { const listeners = [] const addListener = (...args) => { listeners.push(args) elementRef.current.addEventListener(...args) } for (let match of matches) { if (match.matches === true) { switch (match.value) { case 'hover': addListener('mouseenter', open) addListener('mouseleave', close) break case 'focus': addListener('focus', open) // addListener('blur', close) break case 'click': addListener('click', _ref4) break } } } return () => { listeners.forEach(_ref5) } } }, [elementRef.current, matches, open, close, toggle]) return React.cloneElement(children, { tabIndex: children.props.tabIndex || (typeof tabIndex === 'string' ? tabIndex : undefined), 'aria-controls': props['aria-controls'] || id, 'aria-haspopup': 'true', 'aria-expanded': String(isOpen), ref, }) } const ScrollPositioner = props => React.createElement( PopoverContainer, _extends( { scrollY: useWindowScroll( props.repositionOnScroll === true ? 30 : props.repositionOnScroll ), }, props ) ) const ResizePositioner = props => { props = _extends({}, props) props.windowSize = useWindowSize(1280, 720, { fps: props.repositionOnResize === true ? 30 : props.repositionOnResize, }) return React.createElement( props.repositionOnScroll ? ScrollPositioner : PopoverContainer, props ) } export const Popover = ({ open, initialOpen, repositionOnResize = 0, repositionOnScroll = 0, containPolicy = 'flip', children, }) => { let [isOpen, toggle] = useSwitch(initialOpen) isOpen = open === void 0 || open === null ? isOpen : open return React.createElement( repositionOnResize ? ResizePositioner : repositionOnScroll ? ScrollPositioner : PopoverContainer, { children, open: toggle.on, close: toggle.off, toggle, isOpen, containPolicy, windowSize: emptyArr, repositionOnResize, repositionOnScroll, } ) } if (process.env.NODE_ENV !== 'production') { const PropTypes = require('prop-types') Popover.displayName = 'Popover' PopoverBox.displayName = 'PopoverBox' Popover.propTypes = { open: PropTypes.bool, initialOpen: PropTypes.bool, repositionOnResize: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]), repositionOnScroll: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]), containPolicy: PropTypes.oneOfType([ PropTypes.func, PropTypes.string, PropTypes.bool, ]), } PopoverBox.propTypes = { placement: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), transition: PropTypes.func, } }