@vitus-labs/elements
Version:
Most basic react reusable components
1,284 lines (1,247 loc) • 47.3 kB
JavaScript
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