curls
Version:
💪 Responsive, expressive UI primitives for React written with Style Hooks and Emotion
393 lines (362 loc) • 10.7 kB
JavaScript
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,
}
}