import { border, shadow } from '@repay/cactus-theme' import pick from 'lodash/pick' import PropTypes from 'prop-types' import React from 'react' import styled from 'styled-components' import { layout, LayoutProps, padding, PaddingProps } from 'styled-system' import { usePositioning } from '../helpers/positionPopover' import { useMergedRefs } from '../helpers/react' import usePopup, { PopupType, PositionPopup, TogglePopup } from '../helpers/usePopup' import { Sidebar } from '../Layout/Sidebar' import { OrderHint, OrderHintKey, useAction, useActionBarItems } from './ActionProvider' interface ItemProps extends React.ButtonHTMLAttributes { /** Required when used within an ActionProvider */ id?: string icon: React.ReactElement orderHint?: OrderHint 'aria-label': string } type StyleProps = LayoutProps & PaddingProps // @ts-ignore const stylePropNames: string[] = layout.propNames.concat(padding.propNames) type RenderFn = (t: TogglePopup, expanded: boolean) => React.ReactElement | null interface PanelProps extends StyleProps, Omit, 'children'> { icon: React.ReactElement orderHint?: OrderHint popupType?: PopupType positionPopup?: PositionPopup children?: React.ReactNode | RenderFn 'aria-label': string } export const ActionBarItem = React.forwardRef( ({ icon, orderHint, ...props }, ref) => { const child = ( {icon} ) return useAction(child, orderHint, props.id) } ) export const ActionBarPanel = React.forwardRef( ({ orderHint, ...props }, ref) => { // Functionality encapsulated in an "inner" component so `useAction` // won't interfere with the effect hooks inside of `usePopup`. const child = return useAction(child, orderHint, props.id) } ) const Panel = React.forwardRef( ( { icon, popupType = 'dialog', positionPopup, children, id, onBlur, onKeyDown, ...props }, ref ) => { const { expanded, toggle, wrapperProps, buttonProps, popupProps } = usePopup(popupType, { id, onWrapperBlur: onBlur, onWrapperKeyDown: onKeyDown, }) const buttonRef = React.useRef(null) const styleProps = pick(props, stylePropNames) as StyleProps // Mostly expecting the label props, but I'm not going to attempt a comprehensive whitelist. const ariaProps: React.AriaAttributes = {} for (const p of Object.keys(props)) { if (p.startsWith('aria-')) { // @ts-ignore ariaProps[p] = props[p] } else if (!styleProps.hasOwnProperty(p)) { // @ts-ignore wrapperProps[p] = props[p] } } const childNode = typeof children === 'function' ? (children as RenderFn)(toggle, expanded) : children return ( {icon} {childNode} ) } ) interface PanelPopupProps extends StyleProps { position?: PositionPopup anchorRef?: React.RefObject children?: React.ReactNode } // A variation on "as props": for `usePositioning` to work, the component must support refs. type RequireRef

= P extends { ref?: React.Ref } ? R extends HTMLElement ? P : never : never type RefProps = T extends keyof JSX.IntrinsicElements ? PanelPopupProps & Omit : T extends React.ComponentType ? PanelPopupProps & RequireRef> : never interface PanelPopupType { (p: { as?: T } & RefProps): React.ReactElement | null defaultProps?: Partial displayName?: string } const positionPanel = (popup: HTMLElement): void => { const parent = popup.offsetParent if (parent) { const rect = parent.getBoundingClientRect() if (parent.matches('.cactus-fixed-bottom')) { popup.style.top = 'unset' popup.style.left = '0' popup.style.bottom = `${rect.height}px` popup.style.right = '0' popup.style.maxHeight = `${rect.top}px` popup.style.boxShadow = 'none' popup.style.width = 'auto' } else if (parent.matches('.cactus-fixed-left, .cactus-grid-left')) { popup.style.top = '0' popup.style.left = `${rect.width}px` popup.style.bottom = '0' popup.style.right = 'unset' popup.style.maxHeight = '' popup.style.boxShadow = '' // This indicates a `width` prop has been used to override the default width. popup.style.width = !popup.hasAttribute('width') ? 'max-content' : '' } } } const PanelPopup: PanelPopupType = React.forwardRef( ({ position = positionPanel, anchorRef, ...props }, ref_) => { const ref = useMergedRefs(ref_) // @ts-ignore const visible = !(props['aria-hidden'] || props.hidden) usePositioning({ position, visible, ref, anchorRef }) return } ) as any PanelPopup.displayName = 'ActionBar.PanelPopup' PanelPopup.defaultProps = { position: positionPanel } // The box shadow is #2, but shifted to be only on the right side. const StyledPopup = styled.div.withConfig({ shouldForwardProp: (p) => (p as string) === 'width' || !stylePropNames.includes(p), })` ${(p) => p.theme.colorStyles.standard}; box-sizing: border-box; position: absolute; z-index: 100; outline: none; ${shadow('12px 0 24px -12px', (p) => `border-right: ${border(p, 'lightContrast')}`)} display: block; &[aria-hidden='true'] { display: none; } height: auto; width: auto; overflow: auto; ${layout} padding: 8px; ${padding} ` interface ActionBarType extends React.FC> { Item: typeof ActionBarItem Panel: typeof ActionBarPanel Button: typeof Sidebar.Button PanelPopup: typeof PanelPopup PanelWrapper: ReturnType } export const ActionBar: ActionBarType = ({ children, ...props }) => { const actionBarItems = useActionBarItems() return ( {actionBarItems} {children} ) } ActionBar.Item = ActionBarItem ActionBar.Panel = ActionBarPanel ActionBar.Button = Sidebar.Button ActionBar.PanelPopup = PanelPopup ActionBar.PanelWrapper = styled.div` outline: none; border: none; margin: 0; padding: 0; display: block; background-color: transparent; ` export default ActionBar ActionBarItem.propTypes = { icon: PropTypes.element.isRequired, orderHint: PropTypes.oneOfType([ PropTypes.number, PropTypes.oneOf(['top', 'high', 'center', 'low', 'bottom']), ]), 'aria-label': PropTypes.string.isRequired, } ActionBarPanel.propTypes = { icon: PropTypes.element.isRequired, orderHint: ActionBarItem.propTypes.orderHint, popupType: PropTypes.oneOf(['menu', 'listbox', 'tree', 'grid', 'dialog']), positionPopup: PropTypes.func, children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), } ActionBarPanel.defaultProps = { popupType: 'dialog' }