UNPKG

@vitus-labs/elements

Version:
1,284 lines (1,247 loc) 47.3 kB
import { makeItResponsive, alignContent, extendCss, value } from '@vitus-labs/unistyle'; export { Provider } from '@vitus-labs/unistyle'; import React, { forwardRef, memo, useMemo, createRef, useCallback, Children, useState, useEffect, createContext, useContext, useRef } from 'react'; import { config, render, get, isEmpty, pick, omit, context as context$1, throttle } from '@vitus-labs/core'; import { isFragment } from 'react-is'; import { createPortal } from 'react-dom'; const PKG_NAME = '@vitus-labs/elements'; const IS_DEVELOPMENT = process.env.NODE_ENV !== 'production'; /* eslint-disable import/prefer-default-export */ // https://stackoverflow.com/questions/35464067/flexbox-not-working-on-button-or-fieldset-elements const INLINE_ELEMENTS_FLEX_FIX = { button: true, fieldset: true, legend: true, }; /* eslint-disable import/prefer-default-export */ const isWebFixNeeded = (tag) => { if (tag && tag in INLINE_ELEMENTS_FLEX_FIX) return true; return false; }; const { styled: styled$2, css: css$2, component: component$3 } = config; const childFixCSS = ` display: flex; flex: 1; width: 100%; height: 100%; `; const parentFixCSS = ` flex-direction: column; `; const parentFixBlockCSS = ` width: 100%; `; const fullHeightCSS = ` height: 100%; `; const blockCSS = ` align-self: stretch; `; const childFixPosition = (isBlock) => `display: ${isBlock ? 'flex' : 'inline-flex'};`; const styles$2 = ({ theme: t, css, }) => css ` ${t.alignY === 'block' && fullHeightCSS}; ${alignContent({ direction: t.direction, alignX: t.alignX, alignY: t.alignY, })}; ${t.block && blockCSS}; ${!t.childFix && childFixPosition(t.block)}; ${t.parentFix && t.block && parentFixBlockCSS}; ${t.parentFix && parentFixCSS}; ${t.extraStyles && extendCss(t.extraStyles)}; `; const platformCSS$1 = `box-sizing: border-box;` ; var Styled$1 = styled$2(component$3) ` position: relative; ${platformCSS$1}; ${({ $childFix }) => $childFix && childFixCSS}; ${makeItResponsive({ key: '$element', styles: styles$2, css: css$2, normalize: true, })}; `; // eslint-disable-next-line react/display-name const Component$9 = forwardRef(({ children, tag, block, extendCss, direction, alignX, alignY, equalCols, isInline, ...props }, ref) => { const debugProps = IS_DEVELOPMENT ? { 'data-vl-element': 'Element', } : {}; const COMMON_PROPS = { ...props, ...debugProps, ref, as: tag, }; const needsFix = !props.dangerouslySetInnerHTML && isWebFixNeeded(tag) ; if (!needsFix || false) { return (React.createElement(Styled$1, { ...COMMON_PROPS, "$element": { block, direction, alignX, alignY, equalCols, extraStyles: extendCss, } }, children)); } // eslint-disable-next-line no-nested-ternary const asTag = (isInline ? 'span' : 'div') ; return (React.createElement(Styled$1, { ...COMMON_PROPS, "$element": { parentFix: true, block, extraStyles: extendCss, } }, React.createElement(Styled$1, { as: asTag, "$childFix": true, "$element": { childFix: true, direction, alignX, alignY, equalCols, } }, children))); }); const { styled: styled$1, css: css$1, component: component$2 } = config; const equalColsCSS = ` flex: 1; `; const typeContentCSS = ` flex: 1; `; // -------------------------------------------------------- // calculate spacing between before / content / after // -------------------------------------------------------- const gapDimensions = { inline: { before: 'margin-right', after: 'margin-left', }, reverseInline: { before: 'margin-right', after: 'margin-left', }, rows: { before: 'margin-bottom', after: 'margin-top', }, reverseRows: { before: 'margin-bottom', after: 'margin-top', }, }; const calculateGap = ({ direction, type, value, }) => { if (!direction || !type) return undefined; const finalStyles = `${gapDimensions[direction][type]}: ${value};`; return finalStyles; }; // -------------------------------------------------------- // calculations of styles to be rendered // -------------------------------------------------------- const styles$1 = ({ css, theme: t, rootSize, }) => css ` ${alignContent({ direction: t.direction, alignX: t.alignX, alignY: t.alignY, })}; ${t.equalCols && equalColsCSS}; ${t.gap && t.contentType && calculateGap({ direction: t.parentDirection, type: t.contentType, value: value(t.gap, rootSize), })}; ${t.extraStyles && extendCss(t.extraStyles)}; `; const platformCSS = `box-sizing: border-box;` ; const StyledComponent = styled$1(component$2) ` ${platformCSS}; display: flex; align-self: stretch; flex-wrap: wrap; ${({ $contentType }) => $contentType === 'content' && typeContentCSS}; ${makeItResponsive({ key: '$element', styles: styles$1, css: css$1, normalize: true, })}; `; const Component$8 = ({ contentType, tag, parentDirection, direction, alignX, alignY, equalCols, gap, extendCss, ...props }) => { const debugProps = IS_DEVELOPMENT ? { 'data-vl-element': contentType, } : {}; const stylingProps = { contentType, parentDirection, direction, alignX, alignY, equalCols, gap, extraStyles: extendCss, }; return (React.createElement(StyledComponent, { as: tag, "$contentType": contentType, "$element": stylingProps, ...debugProps, ...props })); }; var component$1 = memo(Component$8); const INLINE_ELEMENTS = { span: true, a: true, button: true, input: true, label: true, select: true, textarea: true, br: true, img: true, strong: true, small: true, code: true, b: true, big: true, i: true, tt: true, abbr: true, acronym: true, cite: true, dfn: true, em: true, kbd: true, samp: true, var: true, bdo: true, map: true, object: true, q: true, script: true, sub: true, sup: true, }; const EMPTY_ELEMENTS = { area: true, base: true, br: true, col: true, embed: true, hr: true, img: true, input: true, keygen: true, link: true, textarea: true, // 'meta': true, // 'param': true, source: true, track: true, wbr: true, }; const isInlineElement = (tag) => { if (tag && tag in INLINE_ELEMENTS) return true; return false; }; const getShouldBeEmpty = (tag) => { if (tag && tag in EMPTY_ELEMENTS) return true; return false; }; const defaultDirection = 'inline'; const defaultContentDirection = 'rows'; const defaultAlignX = 'left'; const defaultAlignY = 'center'; const Component$7 = forwardRef(({ innerRef, tag, label, content, children, beforeContent, afterContent, block, equalCols, gap, direction, alignX = defaultAlignX, alignY = defaultAlignY, css, contentCss, beforeContentCss, afterContentCss, contentDirection = defaultContentDirection, contentAlignX = defaultAlignX, contentAlignY = defaultAlignY, beforeContentDirection = defaultDirection, beforeContentAlignX = defaultAlignX, beforeContentAlignY = defaultAlignY, afterContentDirection = defaultDirection, afterContentAlignX = defaultAlignX, afterContentAlignY = defaultAlignY, ...props }, ref) => { // -------------------------------------------------------- // check if should render only single element // -------------------------------------------------------- const shouldBeEmpty = !!props.dangerouslySetInnerHTML || getShouldBeEmpty(tag) ; // -------------------------------------------------------- // if not single element, calculate values // -------------------------------------------------------- const isSimpleElement = !beforeContent && !afterContent; const CHILDREN = children ?? content ?? label; const isInline = isInlineElement(tag) ; const SUB_TAG = isInline ? 'span' : undefined; // -------------------------------------------------------- // direction & alignX & alignY calculations // -------------------------------------------------------- const { wrapperDirection, wrapperAlignX, wrapperAlignY } = useMemo(() => { let wrapperDirection = direction; let wrapperAlignX = alignX; let wrapperAlignY = alignY; if (isSimpleElement) { if (contentDirection) wrapperDirection = contentDirection; if (contentAlignX) wrapperAlignX = contentAlignX; if (contentAlignY) wrapperAlignY = contentAlignY; } else if (direction) { wrapperDirection = direction; } else { wrapperDirection = defaultDirection; } return { wrapperDirection, wrapperAlignX, wrapperAlignY }; }, [ isSimpleElement, contentDirection, contentAlignX, contentAlignY, alignX, alignY, direction, ]); // -------------------------------------------------------- // common wrapper props // -------------------------------------------------------- const WRAPPER_PROPS = { ref: ref ?? innerRef, extendCss: css, tag, block, direction: wrapperDirection, alignX: wrapperAlignX, alignY: wrapperAlignY, as: undefined, // reset styled-components `as` prop }; // -------------------------------------------------------- // return simple/empty element like input or image etc. // -------------------------------------------------------- if (shouldBeEmpty) { return React.createElement(Component$9, { ...props, ...WRAPPER_PROPS }); } const contentRenderOutput = render(CHILDREN); return (React.createElement(Component$9, { ...props, ...WRAPPER_PROPS, isInline: isInline }, beforeContent && (React.createElement(component$1, { tag: SUB_TAG, contentType: "before", parentDirection: wrapperDirection, extendCss: beforeContentCss, direction: beforeContentDirection, alignX: beforeContentAlignX, alignY: beforeContentAlignY, equalCols: equalCols, gap: gap }, render(beforeContent))), isSimpleElement ? (contentRenderOutput) : (React.createElement(component$1, { tag: SUB_TAG, contentType: "content", parentDirection: wrapperDirection, extendCss: contentCss, direction: contentDirection, alignX: contentAlignX, alignY: contentAlignY, equalCols: equalCols }, contentRenderOutput)), afterContent && (React.createElement(component$1, { tag: SUB_TAG, contentType: "after", parentDirection: wrapperDirection, extendCss: afterContentCss, direction: afterContentDirection, alignX: afterContentAlignX, alignY: afterContentAlignY, equalCols: equalCols, gap: gap }, render(afterContent))))); }); const name$5 = `${PKG_NAME}/Element`; Component$7.displayName = name$5; Component$7.pkgName = PKG_NAME; Component$7.VITUS_LABS__COMPONENT = name$5; const isNumber = (a, b) => Number.isInteger(a) && Number.isInteger(b); const types = { height: 'offsetHeight', width: 'offsetWidth', }; const calculate = ({ beforeContent, afterContent }) => (type) => { const beforeContentSize = get(beforeContent, types[type]); const afterContentSize = get(afterContent, types[type]); if (isNumber(beforeContentSize, afterContentSize)) { if (beforeContentSize > afterContentSize) { beforeContent.style[type] = `${beforeContentSize}px`; afterContent.style[type] = `${beforeContentSize}px`; } else { beforeContent.style[type] = `${afterContentSize}px`; afterContent.style[type] = `${afterContentSize}px`; } } }; const withEqualBeforeAfter = (WrappedComponent) => { const displayName = WrappedComponent.displayName ?? WrappedComponent.name ?? 'Component'; const Enhanced = (props) => { const { equalBeforeAfter, direction, afterContent, beforeContent, ...rest } = props; const elementRef = createRef(); const calculateSize = () => { const beforeContent = get(elementRef, 'current.children[0]'); const afterContent = get(elementRef, 'current.children[2]'); if (beforeContent && afterContent) { const updateElement = calculate({ beforeContent, afterContent }); if (direction === 'rows') updateElement('height'); else updateElement('width'); } }; if (equalBeforeAfter) calculateSize(); return (React.createElement(WrappedComponent, { ...rest, afterContent: afterContent, beforeContent: beforeContent, // @ts-ignore ref: elementRef })); }; Enhanced.displayName = `withEqualSizeBeforeAfter(${displayName})`; return Enhanced; }; const RESERVED_PROPS = [ 'children', 'component', 'wrapComponent', 'data', 'itemKey', 'valueName', 'itemProps', 'wrapProps', ]; const attachItemProps = ({ i, length, }) => { const position = i + 1; return { index: i, first: position === 1, last: position === length, odd: position % 2 === 1, even: position % 2 === 0, position, }; }; const Component$6 = (props) => { const { itemKey, valueName, children, component, data, wrapComponent: Wrapper, wrapProps, itemProps, } = props; const injectItemProps = useMemo(() => (typeof itemProps === 'function' ? itemProps : () => itemProps), [itemProps]); const injectWrapItemProps = useMemo(() => (typeof wrapProps === 'function' ? wrapProps : () => wrapProps), [wrapProps]); const getKey = useCallback((item, index) => { if (typeof itemKey === 'function') return itemKey(item, index); return index; }, [itemKey]); const renderChild = (child, total = 1, i = 0) => { if (!itemProps && !Wrapper) return child; const extendedProps = attachItemProps({ i, length: total, }); const finalItemProps = itemProps ? injectItemProps({}, extendedProps) : {}; // if no props extension is required, just return children if (Wrapper) { const finalWrapProps = wrapProps ? injectWrapItemProps({}, extendedProps) : {}; return (React.createElement(Wrapper, { key: i, ...finalWrapProps }, render(child, finalItemProps))); } return render(child, { key: i, ...finalItemProps, }); }; // -------------------------------------------------------- // render children // -------------------------------------------------------- const renderChildren = () => { if (!children) return null; // if children is Array if (Array.isArray(children)) { return Children.map(children, (item, i) => renderChild(item, children.length, i)); } // if children is Fragment if (isFragment(children)) { const fragmentChildren = children?.props?.children; const childrenLength = fragmentChildren.length; return fragmentChildren.map((item, i) => renderChild(item, childrenLength, i)); } // if single child return renderChild(children); }; // -------------------------------------------------------- // render array of strings or numbers // -------------------------------------------------------- const renderSimpleArray = (data) => { const { length } = data; // if the data array is empty if (length === 0) return null; return data.map((item, i) => { const key = getKey(item, i); const keyName = valueName ?? 'children'; const extendedProps = attachItemProps({ i, length, }); const finalItemProps = { ...(itemProps ? injectItemProps({ [keyName]: item }, extendedProps) : {}), [keyName]: item, }; if (Wrapper) { const finalWrapProps = wrapProps ? injectWrapItemProps({ [keyName]: item }, extendedProps) : {}; return (React.createElement(Wrapper, { key: key, ...finalWrapProps }, render(component, finalItemProps))); } return render(component, { key, ...finalItemProps }); }); }; // -------------------------------------------------------- // render array of objects // -------------------------------------------------------- const renderComplexArray = (data) => { const renderData = data.filter((item) => !isEmpty(item)); // remove empty objects const { length } = renderData; // if it's empty if (renderData.length === 0) return null; const getKey = (item, index) => { if (!itemKey) return item.key ?? item.id ?? item.itemId ?? index; if (typeof itemKey === 'function') return itemKey(item, index); if (typeof itemKey === 'string') return item[itemKey]; return index; }; return renderData.map((item, i) => { const { component: itemComponent, ...restItem } = item; const renderItem = itemComponent ?? component; const key = getKey(restItem, i); const extendedProps = attachItemProps({ i, length, }); const finalItemProps = { ...(itemProps ? injectItemProps(item, extendedProps) : {}), ...restItem, }; if (Wrapper && !itemComponent) { const finalWrapProps = wrapProps ? injectWrapItemProps(item, extendedProps) : {}; return (React.createElement(Wrapper, { key: key, ...finalWrapProps }, render(renderItem, finalItemProps))); } return render(renderItem, { key, ...finalItemProps }); }); }; // -------------------------------------------------------- // render list items // -------------------------------------------------------- const renderItems = () => { // -------------------------------------------------------- // children have priority over props component + data // -------------------------------------------------------- if (children) return renderChildren(); // -------------------------------------------------------- // render props component + data // -------------------------------------------------------- if (component && Array.isArray(data)) { const clearData = data.filter((item) => item !== null && item !== undefined); const isSimpleArray = clearData.every((item) => typeof item === 'string' || typeof item === 'number'); if (isSimpleArray) return renderSimpleArray(clearData); const isComplexArray = clearData.every((item) => typeof item === 'object'); if (isComplexArray) return renderComplexArray(clearData); return null; } // -------------------------------------------------------- // if there are no children or valid react component and data as an array, // return null to prevent error // -------------------------------------------------------- return null; }; return renderItems(); }; Component$6.isIterator = true; Component$6.RESERVED_PROPS = RESERVED_PROPS; const Component$5 = forwardRef(({ rootElement = false, ...props }, ref) => { const renderedList = React.createElement(Component$6, { ...pick(props, Component$6.RESERVED_PROPS) }); if (!rootElement) return renderedList; return (React.createElement(Component$7, { ref: ref, ...omit(props, Component$6.RESERVED_PROPS) }, renderedList)); }); const name$4 = `${PKG_NAME}/List`; Component$5.displayName = name$4; Component$5.pkgName = PKG_NAME; Component$5.VITUS_LABS__COMPONENT = name$4; // @ts-nocheck const RESERVED_KEYS = [ 'type', 'activeItems', 'itemProps', 'activeItemRequired', ]; const component = (WrappedComponent) => { const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; const Enhanced = (props) => { const { type = 'single', activeItemRequired, activeItems, itemProps = {}, ...rest } = props; const initActiveItems = () => { if (type === 'single') { if (Array.isArray(activeItems)) { // eslint-disable-next-line no-console console.warn('Iterator is type of single. activeItems cannot be an array.'); } else { return activeItems; } } else if (type === 'multi') { const activeItemsHelper = Array.isArray(activeItems) ? activeItems : [activeItems]; return new Map(activeItemsHelper.map((id) => [id, true])); } return undefined; }; const [innerActiveItems, setActiveItems] = useState(initActiveItems()); const countActiveItems = (data) => { let result = 0; data.forEach((value) => { if (value) result += 1; }); return result; }; const updateItemState = (key) => { if (type === 'single') { setActiveItems((prevState) => { if (activeItemRequired) return key; if (prevState === key) return undefined; return key; }); } else if (type === 'multi') { setActiveItems((prevState) => { // TODO: add conditional type to fix this const activeItems = new Map(prevState); if (activeItemRequired && activeItems.get(key) && countActiveItems(activeItems) === 1) { return activeItems; } activeItems.set(key, !activeItems.get(key)); return activeItems; }); } else { setActiveItems(undefined); } }; const handleItemActive = (key) => { updateItemState(key); }; const updateAllItemsState = (status) => { { setActiveItems(new Map()); } }; const setItemActive = (key) => { updateItemState(key); }; const unsetItemActive = (key) => { updateItemState(key); }; const toggleItemActive = (key) => { updateItemState(key); }; // const setAllItemsActive = () => { // updateAllItemsState(true) // } const unsetAllItemsActive = () => { updateAllItemsState(); }; const isItemActive = (key) => { if (!innerActiveItems) return false; if (type === 'single') return innerActiveItems === key; if (type === 'multi' && innerActiveItems instanceof Map) { return !!innerActiveItems.get(key); } return false; }; const attachMultipleProps = { unsetAllItemsActive, }; const attachItemProps = (props) => { const { key } = props; const defaultItemProps = typeof itemProps === 'object' ? itemProps : itemProps(props); const result = { ...defaultItemProps, active: isItemActive(key), handleItemActive: () => handleItemActive(key), setItemActive, unsetItemActive, toggleItemActive, ...(type === 'multi' ? attachMultipleProps : {}), }; return result; }; useEffect(() => { if (type === 'single' && Array.isArray(activeItems)) { if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line no-console console.error('When type=`single` activeItems must be a single value, not an array'); } } }, [type, activeItems]); return React.createElement(WrappedComponent, { ...rest, itemProps: attachItemProps }); }; Enhanced.RESERVED_KEYS = RESERVED_KEYS; Enhanced.displayName = `@vitus-labs/elements/List/withActiveState(${displayName})`; return Enhanced; }; const Component$4 = ({ DOMLocation, tag = 'div', children, }) => { const [element, setElement] = useState(); useEffect(() => { if (!tag) return undefined; const position = DOMLocation ?? document.body; const element = document.createElement(tag); setElement(element); position.appendChild(element); return () => { position.removeChild(element); }; }, [tag, DOMLocation]); if (!tag || !element) return null; return createPortal(children, element); }; // ---------------------------------------------- // DEFINE STATICS // ---------------------------------------------- const name$3 = `${PKG_NAME}/Portal`; Component$4.displayName = name$3; Component$4.pkgName = PKG_NAME; Component$4.VITUS_LABS__COMPONENT = name$3; const context = createContext({}); const { Provider } = context; const useOverlayContext = () => useContext(context); const Component$3 = ({ children, blocked, setBlocked, setUnblocked, }) => { const ctx = useMemo(() => ({ blocked, setBlocked, setUnblocked, }), [blocked, setBlocked, setUnblocked]); return React.createElement(Provider, { value: ctx }, children); }; /* eslint-disable no-console */ const useOverlay = ({ isOpen = false, openOn = 'click', // click | hover closeOn = 'click', // click | 'clickOnTrigger' | 'clickOutsideContent' | hover | manual type = 'dropdown', // dropdown | tooltip | popover | modal position = 'fixed', // absolute | fixed | relative | static align = 'bottom', // main align prop top | left | bottom | right alignX = 'left', // left | center | right alignY = 'bottom', // top | center | bottom offsetX = 0, offsetY = 0, throttleDelay = 200, parentContainer, closeOnEsc = true, disabled, onOpen, onClose, } = {}) => { const { rootSize } = useContext(context$1); const ctx = useOverlayContext(); const [isContentLoaded, setContentLoaded] = useState(false); const [innerAlignX, setInnerAlignX] = useState(alignX); const [innerAlignY, setInnerAlignY] = useState(alignY); const [blocked, handleBlocked] = useState(false); const [active, handleActive] = useState(isOpen); const triggerRef = useRef(null); const contentRef = useRef(null); const setBlocked = useCallback(() => handleBlocked(true), []); const setUnblocked = useCallback(() => handleBlocked(false), []); const showContent = useCallback(() => { handleActive(true); }, []); const hideContent = useCallback(() => { handleActive(false); }, []); const calculateContentPosition = useCallback(() => { const overlayPosition = {}; if (!active || !isContentLoaded) return overlayPosition; if (type === 'modal' && !contentRef.current) { if (IS_DEVELOPMENT) { console.warn('Cannot access `ref` of `content` component.'); } return overlayPosition; } if (['dropdown', 'tooltip', 'popover'].includes(type)) { // return empty object when refs are not available if (!triggerRef.current || !contentRef.current) { if (IS_DEVELOPMENT) { console.warn('Cannot access `ref` of trigger or content component.'); } return overlayPosition; } const c = contentRef.current.getBoundingClientRect(); const t = triggerRef.current.getBoundingClientRect(); // align is top or bottom if (['top', 'bottom'].includes(align)) { // axe Y position // (assigned as top position) const top = t.top - offsetY - c.height; const bottom = t.bottom + offsetY; // axe X position // content position to trigger position // (assigned as left position) const left = t.left + offsetX; const right = t.right - offsetX - c.width; // calculate possible position const isTop = top >= 0; // represents window.height = 0 const isBottom = bottom + c.height <= window.innerHeight; const isLeft = left + c.width <= window.innerWidth; const isRight = right >= 0; // represents window.width = 0 if (align === 'top') { setInnerAlignY(isTop ? 'top' : 'bottom'); overlayPosition.top = isTop ? top : bottom; } else if (align === 'bottom') { setInnerAlignY(isBottom ? 'bottom' : 'top'); overlayPosition.top = isBottom ? bottom : top; } // left if (alignX === 'left') { setInnerAlignX(isLeft ? 'left' : 'right'); overlayPosition.left = isLeft ? left : right; } // center else if (alignX === 'center') { const center = t.left + (t.right - t.left) / 2 - c.width / 2; const isCenteredLeft = center >= 0; const isCenteredRight = center + c.width <= window.innerWidth; if (isCenteredLeft && isCenteredRight) { setInnerAlignX('center'); overlayPosition.left = center; } else if (isCenteredLeft) { setInnerAlignX('left'); overlayPosition.left = left; } else if (isCenteredRight) { setInnerAlignX('right'); overlayPosition.left = right; } } // right else if (alignX === 'right') { setInnerAlignX(isRight ? 'right' : 'left'); overlayPosition.left = isRight ? right : left; } } // align is left or right else if (['left', 'right'].includes(align)) { // axe X position // (assigned as left position) const left = t.left - offsetX - c.width; const right = t.right + offsetX; // axe Y position // content position to trigger position // (assigned as top position) const top = t.top + offsetY; const bottom = t.bottom - offsetY - c.height; const isLeft = left >= 0; const isRight = right + c.width <= window.innerWidth; const isTop = top + c.height <= window.innerHeight; const isBottom = bottom >= 0; if (align === 'left') { setInnerAlignX(isLeft ? 'left' : 'right'); overlayPosition.left = isLeft ? left : right; } else if (align === 'right') { setInnerAlignX(isRight ? 'right' : 'left'); overlayPosition.left = isRight ? right : left; } // top if (alignY === 'top') { setInnerAlignY(isTop ? 'top' : 'bottom'); overlayPosition.top = isTop ? top : bottom; } // center else if (alignY === 'center') { const center = t.top + (t.bottom - t.top) / 2 - c.height / 2; const isCenteredTop = center >= 0; const isCenteredBottom = center + c.height <= window.innerHeight; if (isCenteredTop && isCenteredBottom) { setInnerAlignY('center'); overlayPosition.top = center; } else if (isCenteredTop) { setInnerAlignY('top'); overlayPosition.top = top; } else if (isCenteredBottom) { setInnerAlignY('bottom'); overlayPosition.top = bottom; } } // bottom else if (alignY === 'bottom') { setInnerAlignY(isBottom ? 'bottom' : 'top'); overlayPosition.top = isBottom ? bottom : top; } } } // modal type else if (type === 'modal') { // return empty object when ref is not available // triggerRef is not needed in this case if (!contentRef.current) { if (IS_DEVELOPMENT) { console.warn('Cannot access `ref` of trigger or content component.'); } return overlayPosition; } const c = contentRef.current.getBoundingClientRect(); switch (alignX) { case 'right': overlayPosition.right = offsetX; break; case 'left': overlayPosition.left = offsetX; break; case 'center': overlayPosition.left = window.innerWidth / 2 - c.width / 2; break; default: overlayPosition.right = offsetX; } switch (alignY) { case 'top': overlayPosition.top = offsetY; break; case 'center': overlayPosition.top = window.innerHeight / 2 - c.height / 2; break; case 'bottom': overlayPosition.bottom = offsetY; break; default: overlayPosition.top = offsetY; } } return overlayPosition; }, [ isContentLoaded, active, align, alignX, alignY, offsetX, offsetY, type, triggerRef, contentRef, ]); const assignContentPosition = useCallback((values = {}) => { if (!contentRef.current) return; const isValue = (value) => { if (typeof value === 'number') return true; if (Number.isFinite(value)) return true; return !!value; }; const setValue = (param) => value(param, rootSize); // ADD POSITION STYLES TO CONTENT // eslint-disable-next-line no-param-reassign if (isValue(position)) contentRef.current.style.position = position; // eslint-disable-next-line no-param-reassign if (isValue(values.top)) contentRef.current.style.top = setValue(values.top); // eslint-disable-next-line no-param-reassign if (isValue(values.bottom)) contentRef.current.style.bottom = setValue(values.bottom); // eslint-disable-next-line no-param-reassign if (isValue(values.left)) contentRef.current.style.left = setValue(values.left); // eslint-disable-next-line no-param-reassign if (isValue(values.right)) contentRef.current.style.right = setValue(values.right); }, [position, rootSize, contentRef]); const setContentPosition = useCallback(() => { const currentPosition = calculateContentPosition(); assignContentPosition(currentPosition); }, [assignContentPosition, calculateContentPosition]); const isNodeOrChild = (ref /* | typeof contentRef */) => (e) => { if (e?.target && ref.current) { return (ref.current.contains(e.target) || e.target === ref.current); } return false; }; const handleVisibilityByEventType = useCallback((e) => { if (blocked || disabled) return; const isTrigger = isNodeOrChild(triggerRef); const isContent = isNodeOrChild(contentRef); // showing content observing if (!active) { if ((openOn === 'hover' && e.type === 'mousemove') || (openOn === 'click' && e.type === 'click')) { if (isTrigger(e)) { showContent(); } } } // hiding content observing if (active) { if (closeOn === 'hover' && e.type === 'mousemove' && !isTrigger(e) && !isContent(e)) { hideContent(); } if (closeOn === 'hover' && e.type === 'scroll') { hideContent(); } if (closeOn === 'click' && e.type === 'click') { hideContent(); } if (closeOn === 'clickOnTrigger' && e.type === 'click') { if (isTrigger(e)) { hideContent(); } } if (closeOn === 'clickOutsideContent' && e.type === 'click') { if (!isContent(e)) { hideContent(); } } } }, [ active, blocked, disabled, openOn, closeOn, hideContent, showContent, triggerRef, contentRef, ]); const handleContentPosition = useCallback(throttle(setContentPosition, throttleDelay), // same deps as `setContentPosition` [assignContentPosition, calculateContentPosition]); const handleClick = handleVisibilityByEventType; const handleVisibility = useCallback(throttle(handleVisibilityByEventType, throttleDelay), // same deps as `handleVisibilityByEventType` [ active, blocked, disabled, openOn, closeOn, hideContent, showContent, triggerRef, contentRef, ]); // -------------------------------------------------------------------------- // useEffects // -------------------------------------------------------------------------- useEffect(() => { setInnerAlignX(alignX); setInnerAlignY(alignY); if (disabled) { hideContent(); } }, [disabled, alignX, alignY, hideContent]); useEffect(() => { if (!active || !isContentLoaded) return undefined; setContentPosition(); setContentPosition(); return undefined; }, [active, isContentLoaded, setContentPosition]); // if an Overlay has an Overlay child, this will prevent closing parent child // and calculates correct position when an Overlay is opened useEffect(() => { if (active) { if (onOpen) onOpen(); if (ctx.setBlocked) ctx.setBlocked(); } else { setContentLoaded(false); } return () => { if (onClose) onClose(); if (ctx.setUnblocked) ctx.setUnblocked(); }; }, [active, showContent, ctx]); // handle closing only when content is active useEffect(() => { if (!closeOnEsc || !active || blocked) return undefined; const handleEscKey = (e) => { if (e.key === 'Escape') { hideContent(); } }; window.addEventListener('keydown', handleEscKey); return () => { window.removeEventListener('keydown', handleEscKey); }; }, [active, blocked, closeOnEsc, hideContent]); // handles repositioning of content on document events useEffect(() => { if (!active) return undefined; const shouldSetOverflow = type === 'modal'; const onScroll = (e) => { handleContentPosition(); handleVisibility(e); }; if (shouldSetOverflow) document.body.style.overflow = 'hidden'; window.addEventListener('resize', handleContentPosition); window.addEventListener('scroll', onScroll); return () => { if (shouldSetOverflow) document.body.style.overflow = ''; window.removeEventListener('resize', handleContentPosition); window.removeEventListener('scroll', onScroll); }; }, [active, type, handleVisibility, handleContentPosition]); // handles repositioning of content on a custom element if defined useEffect(() => { if (!active || !parentContainer) return undefined; // eslint-disable-next-line no-param-reassign if (closeOn !== 'hover') parentContainer.style.overflow = 'hidden'; const onScroll = (e) => { handleContentPosition(); handleVisibility(e); }; parentContainer.addEventListener('scroll', onScroll); return () => { // eslint-disable-next-line no-param-reassign parentContainer.style.overflow = ''; parentContainer.removeEventListener('scroll', onScroll); }; }, [ active, parentContainer, closeOn, handleContentPosition, handleVisibility, ]); // enable overlay manipulation only when the state is NOT blocked // nor in disabled state useEffect(() => { if (blocked || disabled) return undefined; const enabledMouseMove = openOn === 'hover' || closeOn === 'hover'; const enabledClick = openOn === 'click' || ['click', 'clickOnTrigger', 'clickOutsideContent'].includes(closeOn); if (enabledClick) { window.addEventListener('click', handleClick); } if (enabledMouseMove) { window.addEventListener('mousemove', handleVisibility); } return () => { window.removeEventListener('click', handleClick); window.removeEventListener('mousemove', handleVisibility); }; }, [ openOn, closeOn, blocked, disabled, active, handleClick, handleVisibility, ]); // hack-ish way to load content correctly on the first load // as `contentRef` is loaded dynamically const contentRefCallback = useCallback((node) => { if (node) { contentRef.current = node; setContentLoaded(true); } }, []); return { triggerRef, contentRef: contentRefCallback, active, align, alignX: innerAlignX, alignY: innerAlignY, showContent, hideContent, blocked, setBlocked, setUnblocked, Provider: Component$3, }; }; const IS_BROWSER = typeof window !== 'undefined'; const Component$2 = ({ children, trigger, DOMLocation, triggerRefName = 'ref', contentRefName = 'ref', ...props }) => { const { active, triggerRef, contentRef, showContent, hideContent, align, alignX, alignY, Provider, ...ctx } = useOverlay(props); const { openOn, closeOn } = props; const passHandlers = useMemo(() => openOn === 'manual' || closeOn === 'manual' || closeOn === 'clickOutsideContent', [openOn, closeOn]); return (React.createElement(React.Fragment, null, render(trigger, { [triggerRefName]: triggerRef, active, ...(passHandlers ? { showContent, hideContent } : {}), }), IS_BROWSER && active && (React.createElement(Component$4, { DOMLocation: DOMLocation }, React.createElement(Provider, { ...ctx }, render(children, { [contentRefName]: contentRef, active, align, alignX, alignY, ...(passHandlers ? { showContent, hideContent } : {}), })))))); }; const name$2 = `${PKG_NAME}/Overlay`; Component$2.displayName = name$2; Component$2.pkgName = PKG_NAME; Component$2.VITUS_LABS__COMPONENT = name$2; const { styled, css, textComponent } = config; const styles = ({ css, theme: t }) => css ` ${t.extraStyles && extendCss(t.extraStyles)}; `; var Styled = styled(textComponent) ` color: inherit; font-weight: inherit; line-height: 1; ${makeItResponsive({ key: '$text', styles, css, normalize: false, })}; `; const Component$1 = forwardRef(({ paragraph, label, children, tag, css, ...props }, ref) => { const renderContent = (as = undefined) => (React.createElement(Styled, { ref: ref, as: as, "$text": { extraStyles: css }, ...props }, children ?? label)); let finalTag; { if (paragraph) finalTag = 'p'; else { finalTag = tag; } } return renderContent(finalTag); }); // ---------------------------------------------- // DEFINE STATICS // ---------------------------------------------- const name$1 = `${PKG_NAME}/Text`; Component$1.displayName = name$1; Component$1.pkgName = PKG_NAME; Component$1.VITUS_LABS__COMPONENT = name$1; Component$1.isText = true; const Component = ({ children, className, style }) => { const mergedClasses = useMemo(() => (Array.isArray(className) ? className.join(' ') : className), [className]); const finalProps = {}; if (style) finalProps.style = style; if (mergedClasses) finalProps.className = mergedClasses; return render(children, finalProps); }; const name = `${PKG_NAME}/Util`; Component.displayName = name; Component.pkgName = PKG_NAME; Component.VITUS_LABS__COMPONENT = name; export { Component$7 as Element, Component$5 as List, Component$2 as Overlay, Component$3 as OverlayProvider, Component$4 as Portal, Component$1 as Text, Component as Util, useOverlay, component as withActiveState, withEqualBeforeAfter as withEqualSizeBeforeAfter }; //# sourceMappingURL=index.js.map