@amaui/ui-react
Version:
UI for React
495 lines (465 loc) • 22.8 kB
JavaScript
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;