UNPKG

@amaui/ui-react

Version:
495 lines (465 loc) 22.8 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } import React from 'react'; import { is, isEnvironment, element as element_, clamp, wait } from '@amaui/utils'; import { useAmauiTheme } from '@amaui/style-react'; import PortalElement from '../Portal'; const valuesDefault = { x: 0, y: 0, switch: false, init: true }; const Append = props_ => { const theme = useAmauiTheme(); const props = React.useMemo(() => _objectSpread(_objectSpread(_objectSpread({}, theme?.ui?.elements?.all?.props?.default), theme?.ui?.elements?.amauiAppend?.props?.default), props_), [props_]); const Portal = React.useMemo(() => theme?.elements?.Portal || PortalElement, [theme]); const { open, portal = false, accelerated = true, anchor, anchorElement: anchorElement_, offset = [0, 0], padding = [0, 0], paddingUnfollow = props.padding || [0, 0], inset: inset_, position: position_ = 'bottom', alignment: alignment_ = 'end', switch: switch_ = true, overflow = true, unfollow = false, style: style_, update, element, parent: parentElement, additional, children } = props; const [init, setInit] = React.useState(false); const [values, setValues] = React.useState(valuesDefault); const refs = { root: React.useRef(undefined), element: React.useRef(undefined), values: React.useRef(values), alignment: React.useRef(undefined), position: React.useRef(undefined), portal: React.useRef(undefined), props: React.useRef(undefined), anchor: React.useRef(undefined), additional: React.useRef(undefined) }; refs.alignment.current = alignment_; if (theme.direction === 'rtl' && ['top', 'bottom'].includes(position_)) { if (alignment_ === 'start') refs.alignment.current = 'end';else if (alignment_ === 'end') refs.alignment.current = 'start'; } refs.position.current = position_; refs.portal.current = portal; refs.anchor.current = anchor; refs.additional.current = additional; const anchorElement = anchorElement_?.current ? anchorElement_?.current : anchorElement_; if (anchorElement) refs.root.current = anchorElement; refs.props.current = props; const onScroll = React.useCallback(event => { // Only if it's parent's scroll event // if (event.target.contains(refs.root.current) && anchor === undefined) make(); make(); }, [anchor]); const observerMethod = React.useCallback(mutations => { for (const mutation of mutations) { if (mutation.target === refs.root.current ? // Root attributes or childList ['attributes', 'childList'].includes(mutation.type) && [null, undefined, 'style'].includes(mutation.attributeName) : // or subtree's childList ['attributes', 'childList'].includes(mutation.type) && [null, undefined, 'style'].includes(mutation.attributeName)) { if (refs.anchor.current === undefined) make(); } } }, []); const observerResizeMethod = React.useCallback(() => { if (refs.anchor.current === undefined) make(); }, []); React.useEffect(() => { const rootWindow = isEnvironment('browser') ? refs.root.current?.ownerDocument?.defaultView || window : undefined; make(); // Scroll rootWindow.addEventListener('scroll', onScroll, true); // Init setInit(true); return () => { rootWindow.removeEventListener('scroll', onScroll); }; }, []); // Anchor React.useEffect(() => { if (init) { if (open) make();else { if (refs.props.current.clearOnClose) setValues(valuesDefault); } } }, [open]); // Anchor React.useEffect(() => { if (init) { if (anchor?.x && anchor?.y) make(); } }, [anchor]); // Anchor React.useEffect(() => { make(); }, [anchorElement]); // Anchor element React.useEffect(() => { // Resize const observer = new MutationObserver(observerMethod); try { if (refs.root.current) { observer.observe(refs.root.current, { attributes: true, childList: true, subtree: true }); } } catch (error) {} return () => { if (refs.root.current) { observer.disconnect(); } }; }, [anchor, refs.root.current]); // Element resize React.useEffect(() => { // Resize const observer = new MutationObserver(observerMethod); try { if (refs.element.current) { observer.observe(refs.element.current, { attributes: true, childList: true, subtree: true }); } } catch (error) {} return () => { if (refs.element.current) { observer.disconnect(); } }; }, [anchor, refs.element.current]); // Update React.useEffect(() => { if (init) make(); }, [update]); // Update React.useEffect(() => { if (init) { if (is('function', refs.props.current.onUpdate)) refs.props.current.onUpdate(values); } }, [values]); const getBoundingRect = React.useCallback(elementHTML => new Promise(async (resolve, reject) => { if (!elementHTML?.getBoundingClientRect) return; let tries = 5; while (tries) { const valueRect = elementHTML.getBoundingClientRect(); if (valueRect?.height && valueRect?.width) return resolve(valueRect); tries--; await wait(40); } }), []); const getValues = async () => { if (!((refs.root.current || refs.anchor.current) && refs.element.current)) return; const wrapperRect = await getBoundingRect((refs.root.current || refs.element.current)?.parentElement); if (!wrapperRect) return; const resolve = () => { if (!anchor) return; if (!portal) { anchor.x = anchor.x - wrapperRect.x; anchor.y = anchor.y - wrapperRect.y; } return anchor; }; // Anchor relative to parent values const anchor_ = resolve(); const rect = { root: anchor_ || (await getBoundingRect(refs.root.current)), element: await getBoundingRect(refs.element.current) }; const rectOffset = { root: { x: refs.root.current ? refs.root.current.offsetLeft : anchor_?.x, y: refs.root.current ? refs.root.current.offsetTop : anchor_?.y, width: refs.root.current ? refs.root.current.offsetLeft + refs.root.current.offsetWidth : anchor_?.x + anchor_?.width, height: refs.root.current ? refs.root.current.offsetTop + refs.root.current.offsetHeight : anchor_?.y + anchor_?.height }, element: { x: refs.element.current.offsetLeft, y: refs.element.current.offsetTop, width: refs.element.current.offsetLeft + refs.element.current.offsetWidth, height: refs.element.current.offsetTop + refs.element.current.offsetHeight } }; return { rect, rectOffset }; }; const make = async function () { let value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { position: refs.position.current, alignment: refs.alignment.current, inset: inset_, switch: false }; let valueMeasurements_ = arguments.length > 1 ? arguments[1] : undefined; const valueMeasurements = valueMeasurements_ !== undefined ? valueMeasurements_ : await getValues(); if (!valueMeasurements || valueMeasurements.rect.element.width === 0 && valueMeasurements.rect.element.height === 0) return; const rootDocument = isEnvironment('browser') ? refs.root.current?.ownerDocument || window.document : undefined; const wrapperRect = (overflow || switch_) && (refs.root.current || refs.element.current).parentElement.getBoundingClientRect(); const scrollableParents = element_(refs.root.current).parents().filter(item => { if (!(item instanceof Element)) return; const overflow_ = window.getComputedStyle(item).overflow; return (overflow_.includes('auto') || overflow_.includes('hidden')) && (!refs.portal.current || item.clientHeight !== item.scrollHeight || item.clientHeight > window.innerHeight || item.clientWidth > window.innerWidth); }); // If no parents, ie. anchor // add rootDocument.body as an only value if (!scrollableParents.length) scrollableParents.push(rootDocument.body); const { position, alignment, inset, switch: switched } = value; const { rect } = valueMeasurements; let { rectOffset } = valueMeasurements; // We need both root and element refs // to make our values for it const values_ = { x: 0, y: 0 }; const rootX = portal ? rect.root.x : rectOffset.root.x; const rootY = portal ? rect.root.y : rectOffset.root.y; const rootBottom = portal ? rect.root.bottom : rectOffset.root.y + rect.root.height; const rootRight = portal ? rect.root.right : rectOffset.root.x + rect.root.width; const parent_ = (parentElement !== undefined ? parentElement : portal ? rootDocument.body : refs.root.current?.parentElement)?.getBoundingClientRect(); // Top, Bottom if (['top', 'bottom'].includes(position)) { if (alignment === 'start') values_.x = rootX; if (!alignment || alignment === 'center') values_.x = rootX + (rect.root.width - rect.element.width) / 2; if (alignment === 'end') values_.x = rootX + rect.root.width - rect.element.width; if (position === 'top') { values_.y = rootBottom - (parent_.height || 0) - offset[1] - rect.root.height; if (inset) values_.y = rootBottom - (parent_.height || 0) - rect.root.height + rect.element.height + offset[1]; } else { values_.y = rootY + offset[1] + rect.root.height; if (inset) values_.y = rootY + rect.root.height - rect.element.height - offset[1]; } } // Left if (['left', 'right'].includes(position)) { if (alignment === 'start') values_.y = rootY; if (!alignment || alignment === 'center') values_.y = rootY + (rect.root.height - rect.element.height) / 2; if (alignment === 'end') values_.y = rootY + rect.root.height - rect.element.height; if (position === 'left') { values_.x = rootRight - (parent_.width || 0) - offset[0] - rect.root.width; if (inset) values_.x = rootRight - (parent_.width || 0) - rect.root.width + rect.element.width + offset[0]; } else { values_.x = rootX + offset[0] + rect.root.width; if (inset) values_.x = rootX + rect.root.width - rect.element.width - offset[0]; } } // Absolute position if (portal) { values_.y += rootDocument.documentElement.scrollTop; values_.x += rootDocument.documentElement.scrollLeft; } // Overflow if (overflow) { // If x or y is out of bounds of the parent // or window push them to 0 value // only if that value doesn't unfollow them from the element // or unfollow them if unfollow is true if (portal) rectOffset = rect; const top = portal ? rootDocument.documentElement.scrollTop : 0; const left = portal ? rootDocument.documentElement.scrollLeft : 0; const rootY_ = !portal ? wrapperRect.y + rectOffset.root.y : rect.root.y; const valueY = !portal ? wrapperRect.y + values_.y : values_.y; const rootX_ = !portal ? wrapperRect.x + rectOffset.root.x : rect.root.x; const valueX = !portal ? wrapperRect.x + values_.x : values_.x; const wrapperRectY = !portal ? wrapperRect.y : 0; const wrapperRectX = !portal ? wrapperRect.x : 0; if (['left', 'right'].includes(position)) { // All parents that are scrollable const valuesY = [values_.y]; let result = values_.y; scrollableParents.forEach(parent => { const scrollParentRect = parent.getBoundingClientRect(); const scrollParentY = scrollParentRect.y - Math.abs(rect.root.y); const valueScrollParentY = valueY - scrollParentRect.y; // top if (valueY - top <= 0 + padding[1] || valueScrollParentY - top <= 0 + padding[1]) { if (rootY_ + rect.root.height > 0 || unfollow) { const mathValues = [values_.y, top, scrollParentY, scrollParentRect.y - wrapperRectY + top, 0]; if (!portal) mathValues.push(rectOffset.root.y - rootY_); values_.y = Math.max(...mathValues); // padding const padding_ = values_.y > rectOffset.root.y + rect.root.height + top && unfollow ? paddingUnfollow : padding; const scrollRoot = scrollParentRect.y - wrapperRectY >= 0; values_.y += clamp(Math.abs(values_.y - (scrollRoot ? scrollParentRect.y : 0) + wrapperRectY - padding_[1]), 0, padding_[1]); if (!unfollow) values_.y = clamp(values_.y, Number.MIN_SAFE_INTEGER, rectOffset.root.y + rect.root.height + top); } else values_.y = rectOffset.root.y + rect.root.height + top; valuesY.push(values_.y); result = Math.max(...valuesY); } // bottom if (valueY + rect.element.height - top >= window.innerHeight - padding[1] || values_.y + rect.element.height - top >= scrollParentRect.y + scrollParentRect.height - wrapperRectY - padding[1]) { if (rect.root.y < window.innerHeight || rectOffset.root.y < scrollParentRect.y + scrollParentRect.height - wrapperRectY || unfollow) { const mathValues = [values_.y, window.innerHeight - wrapperRectY - rect.element.height + top, scrollParentRect.y + scrollParentRect.height - wrapperRectY - rect.element.height + top]; values_.y = Math.abs(Math.min(...mathValues)); // padding const padding_ = values_.y < rectOffset.root.y - rect.element.height + top && unfollow ? paddingUnfollow : padding; const scrollRoot = scrollParentRect.y - wrapperRectY >= 0; values_.y -= clamp(Math.abs(values_.y - ((scrollRoot ? scrollParentRect.y + scrollParentRect.height : window.innerHeight) - wrapperRectY - rect.element.height - padding_[1])), 0, padding_[1]); if (!unfollow) values_.y = clamp(values_.y, rectOffset.root.y - rect.element.height + top, Number.MAX_SAFE_INTEGER); } else values_.y = rectOffset.root.y - rect.element.height + top; valuesY.push(values_.y); result = Math.min(...valuesY); } // Reset values_.y = valuesY[0]; }); values_.y = result; } if (['top', 'bottom'].includes(position)) { // All parents that are scrollable const valuesX = [values_.x]; let result = values_.x; scrollableParents.forEach(parent => { const scrollParentRect = parent.getBoundingClientRect(); const scrollParentX = scrollParentRect.x - Math.abs(rect.root.x); const valueScrollParentX = valueX - scrollParentRect.x; // left if (valueX - left <= 0 + padding[0] || valueScrollParentX - left <= 0 + padding[0]) { if (rootX_ + rect.root.width > 0 || unfollow) { const mathValues = [values_.x, left, scrollParentX, scrollParentRect.x - wrapperRectX + left, 0]; if (!portal) mathValues.push(rectOffset.root.x - rootX_); values_.x = Math.max(...mathValues); // padding const padding_ = values_.x > rectOffset.root.x + rect.root.width + left && unfollow ? paddingUnfollow : padding; const scrollRoot = scrollParentRect.x - wrapperRectX >= 0; values_.x += clamp(Math.abs(values_.x - (scrollRoot ? scrollParentRect.x : 0) + wrapperRectX - padding_[0]), 0, padding_[0]); if (!unfollow) values_.x = clamp(values_.x, Number.MIN_SAFE_INTEGER, rectOffset.root.x + rect.root.width + left); } else values_.x = rectOffset.root.x + rect.root.width + left; valuesX.push(values_.x); result = Math.max(...valuesX); } // right if (valueX + rect.element.width - left >= window.innerWidth - padding[0] || values_.x + rect.element.width - left >= scrollParentRect.x + scrollParentRect.width - wrapperRectX - padding[0]) { if (rect.root.x < window.innerWidth || rectOffset.root.x < scrollParentRect.x + scrollParentRect.width - wrapperRectX || unfollow) { const mathValues = [values_.x, window.innerWidth - wrapperRectX - rect.element.width + left, scrollParentRect.x + scrollParentRect.width - wrapperRectX - rect.element.width + left]; values_.x = Math.abs(Math.min(...mathValues)); // padding const padding_ = values_.x < rectOffset.root.x - rect.element.width + left && unfollow ? paddingUnfollow : padding; const scrollRoot = scrollParentRect.x - wrapperRectX >= 0; values_.x -= clamp(Math.abs(values_.x - ((scrollRoot ? scrollParentRect.x + scrollParentRect.width : window.innerWidth) - wrapperRectX - rect.element.width - padding_[1])), 0, padding_[1]); if (!unfollow) values_.x = clamp(values_.x, rectOffset.root.x - rect.element.width + left, Number.MAX_SAFE_INTEGER); } else values_.x = rectOffset.root.x - rect.element.width + left; valuesX.push(values_.x); result = Math.min(...valuesX); } // Reset values_.x = valuesX[0]; }); values_.x = result; } } // Switch if (switch_ && !value.switch) { let newPosition = position; const rectValue = { element: {} }; if (position_ === 'top') rectValue.element.y = rect.root.y - offset[1] - rect.element.height; if (position_ === 'bottom') rectValue.element.y = rect.root.y + rect.root.height + offset[1]; if (position_ === 'left') rectValue.element.x = rect.root.x - offset[0] - rect.element.width; if (position_ === 'right') rectValue.element.x = rect.root.x + rect.root.width + offset[0]; const update_ = scrollableParents.some(parent => { const rectParent = parent.getBoundingClientRect(); if (position_ === 'top') return !(rectValue.element.y - (['top', 'bottom'].includes(position_) ? padding[0] : 0) >= 0 && rectValue.element.y - (['top', 'bottom'].includes(position_) ? padding[0] : 0) >= rectParent.y); if (position_ === 'bottom') return !(rectValue.element.y + rect.element.height + (['top', 'bottom'].includes(position_) ? padding[0] : 0) <= window.innerHeight && rectValue.element.y + rect.element.height + (['top', 'bottom'].includes(position_) ? padding[0] : 0) <= rectParent.y + rectParent.height); if (position_ === 'left') return !(rectValue.element.x - (['left', 'right'].includes(position_) ? padding[1] : 0) >= 0 && rectValue.element.x - (['left', 'right'].includes(position_) ? padding[1] : 0) >= rectParent.x); if (position_ === 'right') return !(rectValue.element.x + rect.element.width + (['left', 'right'].includes(position_) ? padding[1] : 0) <= window.innerWidth && rectValue.element.x + rect.element.width + (['left', 'right'].includes(position_) ? padding[1] : 0) <= rectParent.x + rectParent.width); }); if (update_) { if (position_ === 'top') newPosition = 'bottom'; if (position_ === 'left') newPosition = 'right'; if (position_ === 'right') newPosition = 'left'; if (position_ === 'bottom') newPosition = 'top'; return make({ position: newPosition, alignment: alignment_, inset: inset_, switch: true }); } } refs.values.current = _objectSpread({ position: value.position, alignment: value.alignment, switch: switched, init: false }, values_); if (is('function', refs.additional.current)) { refs.values.current = _objectSpread(_objectSpread({}, refs.values.current), additional(rect, rectOffset)); } // Update setValues(refs.values.current); }; let style = {}; style.position = 'absolute'; if (values.position === 'top') style.inset = 'auto auto 0px 0px';else if (values.position === 'left') style.inset = '0px 0px auto auto';else style.inset = '0px auto auto 0px'; if (accelerated) { const ppiHigh = isEnvironment('browser') && window.devicePixelRatio > 1; if (ppiHigh) style.transform = `translate3d(${values.x}px, ${values.y}px, 0px)`;else style.transform = `translate(${values.x}px, ${values.y}px)`; } else { style.top = values.y; style.left = values.x; } style = _objectSpread(_objectSpread(_objectSpread(_objectSpread({}, element?.props?.style), style), style_), values?.style); const PortalComponent = portal ? Portal : React.Fragment; const PortalComponentProps = {}; const rootDocumentElement = isEnvironment('browser') ? refs.root.current?.ownerDocument || window.document : undefined; if (portal && isEnvironment('browser')) { PortalComponentProps.element = rootDocumentElement.body; // relative rootDocumentElement.body.style.position = rootDocumentElement.body.style.position || 'relative'; rootDocumentElement.body.style.margin = rootDocumentElement.body.style.margin || '0'; } return /*#__PURE__*/React.createElement(React.Fragment, null, children && /*#__PURE__*/React.cloneElement(children, { ref: item => { if (children.ref) { if (is('function', children.ref)) children.ref(item);else children.ref.current = item; } refs.root.current = item; } }), open && (children || anchorElement || anchor) && /*#__PURE__*/React.createElement(PortalComponent, PortalComponentProps, is('function', element) ? element({ ref: refs.element, values, style }) : /*#__PURE__*/React.cloneElement(element, { ref: item => { if (element?.ref) { if (is('function', element.ref)) element.ref(item);else element.ref.current = item; } refs.element.current = item; }, style }))); }; Append.displayName = 'amaui-Append'; export default Append;