import { ColorLike } from '@o/color' import { isEqual } from '@o/fast-compare' import { on } from '@o/utils' import { Box, CurrentThemeContext, gloss, isGlossView, Theme } from 'gloss' import { debounce, isNumber, pick } from 'lodash' import * as React from 'react' import { Arrow } from './Arrow' import { BreadcrumbReset } from './Breadcrumbs' import { zIndex } from './constants' import { GlobalPopovers } from './GlobalPopovers' import { getTarget } from './helpers/getTarget' import { Portal } from './helpers/portal' import { Surface, SurfaceProps } from './Surface' import { SurfacePassPropsReset } from './SurfacePropsContext' import { View } from './View/View' const acceptsProps = (x, val) => x.type.acceptsProps && x.type.acceptsProps[val] export type PopoverProps = Omit & { /** Custom theme for just the popover content */ popoverTheme?: string /** If you set a group, it acts as an ID that makes sure only one popover within that ID is ever open */ group?: string /** Can pass function to get isOpen passed in */ children?: React.ReactNode | PopoverChildrenFn /** Element or function that returns element, or querySelector to element */ target?: React.ReactNode | (() => React.ReactNode) | string open?: boolean /** The amount of space around popover you can move mouse */ /** Before it triggers it to close */ forgiveness?: number /** Show a background over content */ overlay?: boolean left?: number top?: number /** The distance the popover is from the target */ distance?: number /** Open when target is clicked */ openOnClick?: boolean /** Open automatically when target is hovered */ openOnHover?: boolean /** Delay until openOnHover */ delay?: number /** Prevent popover itself from catching pointer events */ noHoverOnChildren?: boolean /** Size of shown arrow */ arrowSize?: number /** Close when you click outside it */ closeOnClickAway?: boolean /** Close when you click inside it */ closeOnClick?: boolean closeOnEsc?: boolean /** Callback when hover event happens, even if controlled */ onHover?: (isHovered: boolean) => any /** Which direction it shows towards */ /** Default determine direction automatically */ towards?: PopoverDirection /** Popover can aim to be centered or left aligned on the target */ alignPopover?: 'left' | 'center' padding?: number[] | number onClose?: Function openAnimation?: string closeAnimation?: string /** Lets you adjust position after target is positioned */ adjust?: number[] /** Hide arrow */ noArrow?: boolean /** Padding from edge of window */ edgePadding?: number /** Pretty much what it says, for use with closeOnClick */ keepOpenOnClickTarget?: boolean /** Callback after close */ onDidClose?: Function /** Callback after open */ onDidOpen?: Function onOpen?: Function height?: number width?: number background?: true | ColorLike passActive?: boolean popoverProps?: Object elevation?: number ignoreSegment?: boolean onChangeVisibility?: (visibility: boolean) => any noPortal?: boolean /** Helps you see forgiveness zone */ showForgiveness?: boolean /** Get ref to the Popover class */ popoverRef?: Function /** Get ref to the popover element */ nodeRef?: Function /** Portal styling */ portalStyle?: Object } const defaultProps = { edgePadding: 10, distance: 14, arrowSize: 14, forgiveness: 14, towards: 'auto', transition: 'all ease 100ms', openAnimation: 'slide 300ms', closeAnimation: 'bounce 300ms', adjust: [0, 0], delay: 16, group: 'global', zIndex: zIndex.Popover, } type PopoverPropsWithDefaults = PopoverProps & typeof defaultProps type DebouncedFn = Function & { cancel: () => void } type PopoverDirection = 'top' | 'bottom' | 'left' | 'right' | 'auto' type PositionStateX = { arrowLeft: number; left: number } type PositionStateY = { arrowTop: number; top: number; maxHeight: number } type Bounds = { top: number; left: number; width: number; height: number } const getIsManuallyPositioned = ({ top, left }: { top?: number; left?: number }) => { return isNumber(top) && isNumber(left) } const getPositionState = ( props: PopoverPropsWithDefaults, popoverBounds: Bounds, targetBounds?: Bounds, ) => { const direction = getDirection(props, popoverBounds, targetBounds) return { ...positionStateX(props, popoverBounds, direction, targetBounds), ...positionStateY(props, popoverBounds, direction, targetBounds), direction, } } const getDirection = ( props: PopoverPropsWithDefaults, popoverBounds: Bounds, targetBounds?: Bounds, ): PopoverDirection => { if (!targetBounds || props.towards !== 'auto') { return props.towards || 'top' } const popoverY = popoverBounds.height + props.forgiveness const targetY = targetBounds.top + targetBounds.height const towardsTop = targetY + popoverY > window.innerHeight return towardsTop ? 'top' : 'bottom' } const getEdgePadding = ( props: PopoverPropsWithDefaults, currentPosition: number, windowSize: number, popoverBounds: number, ) => { return Math.min( // upper limit windowSize - props.edgePadding - popoverBounds, // lower limit Math.max(props.edgePadding, currentPosition), ) } const positionStateY = ( props: PopoverPropsWithDefaults, popoverBounds: Bounds, direction: PopoverDirection, targetBounds?: Bounds, ): PositionStateY => { const { distance, adjust, noArrow, arrowSize } = props const VERTICAL = direction === 'top' || direction === 'bottom' // since its rotated 45deg, the real height is less 1/4 of set height const arrowHeight = noArrow ? 0 : arrowSize * 0.75 const targetCenter = targetBounds ? targetBounds.top + targetBounds.height / 2 : popoverBounds.height / 2 const targetTopReal = targetBounds ? targetBounds.top : popoverBounds.top let arrowTop = 0 let maxHeight = window.innerHeight let top = 0 const arrowAdjust = distance // bottom half if (VERTICAL) { // determine arrow location if (direction === 'top') { arrowTop = popoverBounds.height + arrowAdjust top = targetTopReal - popoverBounds.height - distance } else { arrowTop = -arrowSize top = targetTopReal + (targetBounds ? targetBounds.height : 0) + distance } // final top top = getEdgePadding(props, top, window.innerHeight + window.scrollY, popoverBounds.height) } else { // HORIZONTAL const popoverCenter = popoverBounds.height / 2 top = targetCenter - popoverCenter arrowTop = targetCenter - top + arrowHeight / 2 } // adjustments top += adjust[1] arrowTop -= adjust[1] // max height if (VERTICAL) { if (direction === 'top') { maxHeight = (targetBounds ? targetBounds.top : 0) - top + props.distance * 2 - arrowSize / 2 } else { // HORIZONTAL maxHeight = window.innerHeight - (targetBounds ? targetBounds.top + targetBounds.height : 0) } } return { arrowTop: Math.round(arrowTop), top: Math.round(top), maxHeight } } const positionStateX = ( props: PopoverPropsWithDefaults, popoverBounds: Bounds, direction: PopoverDirection, targetBounds?: Bounds, ): PositionStateX => { const { arrowSize } = props let targetCenter: number if (getIsManuallyPositioned(props)) { targetCenter = props.left + props.width / 2 popoverBounds = { width: props.width, height: props.height, top: 0, left: 0 } } else { targetCenter = targetBounds ? targetBounds.left + targetBounds.width / 2 : popoverBounds.width / 2 + popoverBounds.left } const popoverHalfWidth = popoverBounds.width / 2 let left = 0 let arrowLeft = 0 // if (props.alignPopover === 'left') { // popoverCenter = targetBounds ? targetBounds.left : popoverBounds.left // } switch (direction) { case 'top': case 'bottom': const naturalLeft = targetCenter - popoverHalfWidth left = getEdgePadding(props, naturalLeft, window.innerWidth, popoverBounds.width) const popoverCenter = left + popoverHalfWidth // when near edges, adjust for the misaligned centers if (targetCenter !== popoverCenter) { arrowLeft = targetCenter - popoverCenter } // center width of arrow arrowLeft -= arrowSize / 2 // arrowLeft bounds // const popoverMaxX = popoverBounds.width + popoverBounds.left // const max = Math.max(arrowSize + props.edgePadding, popoverMaxX - arrowSize) // const min = popoverBounds.left + arrowSize // arrowLeft = Math.max(min, Math.min(max, arrowLeft)) break case 'left': left = (targetBounds ? targetBounds.left : 0) - popoverBounds.width - props.distance break case 'right': left = (targetBounds ? targetBounds.left + targetBounds.width : 0) + props.distance arrowLeft = -popoverHalfWidth - arrowSize break } // adjustments left += props.adjust[0] arrowLeft -= props.adjust[0] // adjust arrow for alignment if (props.alignPopover === 'left') { // move it back to the left the amount we move popover right arrowLeft += targetCenter - (targetBounds ? targetBounds.left : 0) } return { arrowLeft: Math.round(arrowLeft), left: Math.round(left) } } export type PopoverChildrenFn = (showPopover: boolean) => React.ReactNode const INVERSE = { top: 'bottom', bottom: 'top', left: 'right', right: 'left', } const initialState = { maxHeight: null, targetBounds: null, popoverBounds: null, showPopover: false, targetHovered: 0, menuHovered: 0, top: 0, left: 0, arrowTop: 0, arrowLeft: 0, arrowInnerTop: 0, isPinnedOpen: 0, direction: 'auto' as PopoverDirection, delay: 16, props: {} as PopoverProps, closing: false, measureState: 'done' as 'done' | 'shouldMeasure', } type PopoverState = typeof initialState & { // TODO make these types real targetBounds: any popoverBounds: any maxHeight: any } const isHovered = (props: PopoverProps, state: PopoverState) => { const { targetHovered, menuHovered } = state if (props.noHoverOnChildren) { return targetHovered } return targetHovered || menuHovered } const shouldShowPopover = (props: PopoverProps, state: PopoverState) => { const { isPinnedOpen } = state const { openOnHover, open } = props if (open || isPinnedOpen) { return true } if (typeof open === 'undefined') { return !!(openOnHover && isHovered(props, state)) } return false } export class Popover extends React.Component { static defaultProps = defaultProps static contextType = CurrentThemeContext targetRef = React.createRef() popoverRef: HTMLElement state = initialState get target() { return this.targetRef.current } static getDerivedStateFromProps(props: PopoverPropsWithDefaults, state: PopoverState) { let nextState: Partial = {} const isManuallyPositioned = getIsManuallyPositioned(props) if (isManuallyPositioned) { nextState = { ...nextState, ...getPositionState(props, { width: props.width, height: props.height, top: props.top, left: props.left, }), props, } } const nextShow = shouldShowPopover(props, state) if (nextShow !== state.showPopover) { if (nextShow) { nextState.measureState = 'shouldMeasure' } else { nextState.showPopover = false } } if (!Object.keys(nextState).length) { return null } return nextState } setPopoverRef = (ref: HTMLElement) => { if (ref) { this.popoverRef = ref if (this.props.nodeRef) { this.props.nodeRef(ref) } } } componentDidMount() { const { openOnClick, closeOnClick, closeOnClickAway, closeOnEsc, open, target, popoverRef, } = this.props if (popoverRef) { popoverRef(this) } if (openOnClick || closeOnClick || closeOnClickAway) { this.listenForClickAway() } if (open) { this.open() } const isManuallyPositioned = getIsManuallyPositioned(this.props) // allow multiple ways to pass in targets if (typeof target === 'string') { // selector // @ts-ignore this.targetRef['current'] = getTarget(target) } else { if (!this.target && !isManuallyPositioned) { // potentially we can just get it directly console.error('Couldnt pass className down to target to do measurements...', this) return } } if (!isManuallyPositioned && !this.target) { console.error('Not manually positioned and no target found.') return } if (this.target) { this.listenForClick() this.listenForHover() on(this, this.target, 'click', this.handleTargetClick) if (closeOnEsc) { const parentNode = this.target.parentElement.parentElement const parentPortalNode = (parentNode.querySelector('.popover-portal') as HTMLDivElement) || parentNode.firstElementChild on(this, parentPortalNode, 'keyup', e => { if (e.keyCode === 27) { e.preventDefault() e.stopPropagation() this.forceClose() } }) } } this.updateMeasure() } unmounted = false componentWillUnmount() { GlobalPopovers.state.delete(this) this.unmounted = true } componentDidUpdate(_prevProps, prevState) { if (!this.showPopover) { GlobalPopovers.state.delete(this) } this.updateMeasure() if (this.props.onChangeVisibility) { if (prevState.showPopover !== this.state.showPopover) { this.props.onChangeVisibility(this.state.showPopover) } } if (this.props.onDidOpen) { if (this.showPopover) { this.props.onDidOpen() } } // unhovered if (this.props.onHover) { if ( (prevState.targetHovered || prevState.menuHovered) && (!this.state.targetHovered && !this.state.menuHovered) ) { this.props.onHover(false) } if ( !prevState.targetHovered && !prevState.menuHovered && (this.state.targetHovered || this.state.menuHovered) ) { this.props.onHover(true) } } } // make sure you do it through here setShowPopover = (state: Partial, cb?: any) => { this.closeOthersWithinGroup() this.addToOpenPopoversList() this.setState({ ...state, showPopover: true } as any, cb) } addToOpenPopoversList() { if (typeof this.props.open === 'undefined') { GlobalPopovers.state.add(this) } } updateMeasure() { if (this.state.measureState === 'shouldMeasure') { this.setPosition(this.setShowPopover) } } setPosition = afterMeasure => { const { props } = this if (this.unmounted) return if (getIsManuallyPositioned(props)) return if (!this.popoverRef || !this.target) { throw new Error('missing popvoer ref or target') } // get popover first child which is the inner div that doesn't deal with forgiveness padding const popoverBounds = this.popoverRef.firstElementChild.getBoundingClientRect() const targetNode = this.target.firstElementChild || this.target const targetBounds = targetNode.getBoundingClientRect() const extraY = window.scrollY const nextState = { targetBounds: { top: targetBounds.top + extraY, left: targetBounds.left, width: targetBounds.width, height: targetBounds.height, }, popoverBounds: { top: popoverBounds.top + extraY, left: popoverBounds.left, width: Math.min(window.innerWidth, popoverBounds.width), height: Math.min(window.innerHeight, popoverBounds.height), }, } // if changed, update const prevState = pick(this.state, Object.keys(nextState)) if (!isEqual(nextState, prevState)) { const nextPosition = getPositionState( props as PopoverPropsWithDefaults, nextState.popoverBounds, nextState.targetBounds, ) this.setState({ ...nextState, measureState: 'done', ...nextPosition, }) } else { this.setState( { measureState: 'done', }, afterMeasure, ) } } forceClose = async ({ animate = true } = {}) => { // clear any pending hovers that will eventually open a competing menu this.cancelIfWillOpen() this.stopListeningUntilNextMouseEnter() if (animate) { await this.startClosing() } this.setState({ closing: false, isPinnedOpen: 0, showPopover: false, measureState: 'done', }) } toggleOpen = () => { if (this.showPopover) { this.forceClose() } else { this.open() } } open = () => { this.setShowPopover(null, () => { if (this.props.onOpen) { this.props.onOpen() } }) } startClosing = () => { return new Promise(async resolve => { this.setState({ closing: true }, () => { if (this.props.onClose) { this.props.onClose() } setTimeout(resolve, 300) }) }) } close = async () => { await this.startClosing() this.setState({ closing: false, showPopover: false }) if (this.props.onDidClose) { this.props.onDidClose() } } targetClickOff: any = null listenForClick = () => { if (!this.props.openOnClick) { return } if (!(this.target instanceof HTMLElement)) { console.log('bad target', this.target, this.props) return } if (this.targetClickOff) { this.targetClickOff() } // click away to close this.targetClickOff = on(this, this.target, 'click', this.handleClickOpen) } handleClickOpen = e => { this.closeOthersWithinGroup() e.stopPropagation() if (this.state.isPinnedOpen) { if (this.state.targetHovered && this.props.openOnHover) { // avoid closing when clicking while hovering + openOnHover return } this.forceClose() } else { this.setState({ isPinnedOpen: Date.now() }) } } get wasJustClicked() { return Date.now() - this.state.isPinnedOpen < 10 } listenForClickAway() { on(this, window, 'click', e => { const { showPopover } = this const { keepOpenOnClickTarget, open, closeOnClick, closeOnClickAway } = this.props // forced open or hidden if (open || !showPopover) { return } const clickedTarget = this.wasJustClicked if (keepOpenOnClickTarget && clickedTarget) { return } // closeOnClickPopover if (closeOnClick && clickedTarget) { this.forceClose() e.stopPropagation() return } const clickedInPopover = e.path.findIndex( x => x instanceof HTMLDivElement && x.classList.contains('popover-portal'), ) > -1 if (closeOnClickAway && !clickedTarget && !clickedInPopover) { this.forceClose() e.stopPropagation() return } }) } private stopListeningUntilNextMouseEnter = async () => { if (this.state.isPinnedOpen) { return } const hovered = this.isHovered this.cancelIfWillOpen() await this.clearHovered() if (this.unmounted) { return } this.close() // after click, remove hover listeners until mouseleave if (hovered) { this.removeListeners() const off = on(this, this.target, 'mouseleave', () => { off() this.listenForHover() }) } } clearHovered() { // @ts-ignore return new Promise(resolve => this.setState({ menuHovered: 0, targetHovered: 0 }, resolve)) } get isHovered() { return isHovered(this.props, this.state) } handleOverlayClick(event) { event.stopPropagation() this.close() } listeners: any[] = [] listenForHover() { if (!this.props.openOnHover) { return } if (!(this.target instanceof HTMLElement)) { return } this.removeListeners() this.addHoverListeners('target', this.target) // noHoverOnChildren === no hover on the actual popover child element if (!this.props.noHoverOnChildren) { this.addHoverListeners('menu', this.popoverRef) } } removeListeners() { for (const listener of this.listeners) { if (typeof listener === 'function') { listener() } } this.listeners = [] } delayOpenIfHover: { [key: string]: DebouncedFn | null } = { target: null, menu: null, } private cancelIfWillOpen() { for (const key in this.delayOpenIfHover) { const fn = this.delayOpenIfHover[key] if (fn && fn.cancel) { fn.cancel() } } } addHoverListeners(name: 'target' | 'menu', node: HTMLElement) { if (!(node instanceof HTMLElement)) { console.log('no node!', name) return null } // because debouncing things is dangerous, lets track last event // and use it as source of truth for "is hovered or isnt" let lastEvent: 'enter' | 'leave' = 'leave' const listeners = [] const { delay, noHoverOnChildren } = this.props const isPopover = name === 'menu' const isTarget = name === 'target' const setHovered = () => { if (lastEvent === 'enter') this.hoverStateSet(name, true) } const setUnhovered = () => { if (lastEvent === 'leave') this.hoverStateSet(name, false) } const openIfOver = () => { if (this.isNodeHovered(node)) { setHovered() } } this.cancelIfWillOpen() this.delayOpenIfHover[name] = debounce(openIfOver, isTarget ? delay : 0) let retryOutTm: any = null const unhover = () => { if (!this.isNodeHovered(node)) { setUnhovered() this.cancelIfWillOpen() } } const closeIfOut = () => { clearTimeout(retryOutTm) // avoid if too soon if (isPopover && Date.now() - this.state.menuHovered < 200) { retryOutTm = setTimeout(closeIfOut, 100) on(this, retryOutTm) return null } unhover() // ensure check if we have a delay open if (delay && isTarget) { const tm = setTimeout(unhover, delay) on(this, tm) } } // this will avoid the delay open if its already open const onEnter = () => { if (isTarget && this.state.menuHovered) { openIfOver() } else { this.addToOpenPopoversList() // so we cancel pending hovers too this.delayOpenIfHover[name]() } } // 🐛 target should close slower than menu opens const onLeave = isTarget ? debounce(closeIfOut, 80) : closeIfOut // logic for enter/leave listeners.push( on(this, node, 'mouseenter', () => { lastEvent = 'enter' onEnter() // insanity, but mouseleave is horrible if (this.props.target) { on(this, setTimeout(onLeave, 150)) } }), ) // if noHoverOnChildren it reduces bugs to just not check hovered state const onMouseLeave = noHoverOnChildren ? setUnhovered : onLeave listeners.push( on(this, node, 'mouseleave', () => { lastEvent = 'leave' onMouseLeave() }), ) this.listeners = [...this.listeners, ...listeners] } // hover helpers hoverStateSet(name: string, next: boolean) { const { openOnHover } = this.props const setter = () => { const val = next ? Date.now() : 0 if (name === 'target') { this.setState({ targetHovered: val }) } else { this.setState({ menuHovered: val }) } } if (next) { if (openOnHover) { setter() } } else { if (openOnHover) { setter() } } return next } isNodeHovered = (node: HTMLElement) => { const childSelector = `${node.tagName.toLowerCase()}.${node.className .trim() .replace(/\s+/g, '.')}:hover` const parentNode = node.parentNode as HTMLDivElement if (parentNode) { const hoverChild = parentNode.querySelector(childSelector) if (hoverChild) { return hoverChild } } return node.querySelector(':hover') } overlayRef = ref => { if (ref) { on(this, ref, 'contextmenu', e => this.handleOverlayClick(e)) } } handleTargetClick = () => { // settimeout so it captures once the action completes setTimeout(() => { if (!this.unmounted) { this.stopListeningUntilNextMouseEnter() } }, 0) } controlledTarget = target => { if (!target) { return null } const targetProps = { active: undefined, className: `${target.props['className'] || ''} popover-target`, // both so we support regular dom and gloss-style ...(typeof target.type === 'function' || isGlossView(target.type) ? { nodeRef: this.targetRef, } : { ref: this.targetRef, }), } if (this.props.passActive) { targetProps.active = this.showPopover } const acceptsHovered = acceptsProps(target, 'hover') if (acceptsHovered) { targetProps[acceptsHovered === true ? 'hover' : acceptsHovered] = this.showPopover } return React.cloneElement(target, targetProps) } closeOthersWithinGroup() { GlobalPopovers.closeGroup(this.props.group, this) } get isMeasuring() { return this.state.measureState !== 'done' } get showPopover() { if (this.isMeasuring) { return false } if (typeof this.props.open === 'boolean') { return this.props.open } return this.state.showPopover } render() { const { adjust, arrowSize, background, children, closeAnimation, closeOnClick, closeOnEsc, delay, distance, edgePadding, elevation, forgiveness, height, keepOpenOnClickTarget, left: _left, noArrow, noHoverOnChildren, onClose, onDidOpen, open, openAnimation, openOnClick, openOnHover, overlay, passActive, popoverProps, showForgiveness, style, target, theme, top: _top, towards, width, transition, noPortal, popoverTheme, zIndex, transform, portalStyle, ...restProps } = this.props const { top, left, arrowTop, arrowLeft, // isOpen, closing, maxHeight, direction, } = this.state const { isMeasuring, showPopover } = this const backgroundProp = !background ? null : { background: background === true ? themeBg : background } const hasMeasuredOnce = !!this.state.popoverBounds let popoverContent = ( {!!overlay && ( this.handleOverlayClick(e)} overlay={overlay} /> )} {!noArrow && ( )} {typeof children === 'function' ? (children as PopoverChildrenFn)(showPopover) : children} ) if (popoverTheme) { popoverContent = {popoverContent} } if (noPortal) { return ( <> {React.isValidElement(target) && this.controlledTarget(target)} {popoverContent} ) } return ( <> {React.isValidElement(target) && this.controlledTarget(target)} {popoverContent} ) } } const themeBg = theme => theme.background const PopoverContainer = gloss<{ isOpen?: boolean; isTouchable?: boolean }>(View, { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 5000, pointerEvents: 'none', '& > *': { pointerEvents: 'none !important', }, conditional: { isOpen: { opacity: 1, }, isTouchable: { '& > *': { pointerEvents: 'all !important', }, }, }, }) const Overlay = gloss(Box, { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, background: 'transparent', pointerEvents: 'none', opacity: 0, transition: 'all ease-in 100ms', zIndex: -2, conditional: { isShown: { opacity: 1, pointerEvents: 'all', }, }, }).theme(({ overlay }) => ({ background: overlay === true ? 'rgba(0,0,0,0.2)' : overlay, })) const PopoverWrap = gloss<{ distance?: number forgiveness?: number isOpen?: boolean noPortal?: boolean willReposition?: boolean noHoverOnChildren?: any showForgiveness?: boolean }>(View, { position: 'absolute', pointerEvents: 'none', zIndex: -1, }).theme(p => { let pointerEvents = p.noHoverOnChildren ? 'none' : p.pointerEvents || 'none' if (p.isOpen && !p.noHoverOnChildren) { pointerEvents = p.noPortal ? 'inherit' : 'auto' } const transform = { y: (p.isOpen && !p.willReposition ? 0 : -5) + typeof p.top === 'string' ? +p.top : 0, } return { transition: p.willReposition ? 'none' : p.transition, opacity: p.isOpen && !p.willReposition ? 1 : 0, pointerEvents, transform, padding: p.forgiveness, margin: -p.forgiveness, background: p.showForgiveness ? [250, 250, 0, 0.2] : 'auto', } }) const PopoverInner = gloss(View, { flex: 1, position: 'relative', }) const ArrowContain = gloss({ position: 'absolute', left: '50%', })