@appbuckets/react-ui
Version:
Just Another React UI Framework
376 lines (373 loc) • 9.99 kB
JavaScript
import { __rest, __read, __spreadArray, __assign } from 'tslib';
import * as React from 'react';
import clsx from 'clsx';
import {
createShorthandFactory,
useAutoControlledValue,
handleRef,
Ref,
childrenUtils,
} from '@appbuckets/react-ui-core';
import { useSharedClassName } from '../utils/customHook.js';
import '../BucketTheme/BucketTheme.js';
import { useWithDefaultProps } from '../BucketTheme/BucketContext.js';
/* --------
* Internal Function
* -------- */
function nextFrame(callback) {
requestAnimationFrame(function () {
requestAnimationFrame(callback);
});
}
/* --------
* Component Render
* -------- */
var Collapsable = function (receivedProps) {
var props = useWithDefaultProps('collapsable', receivedProps);
var _a = useSharedClassName(props),
className = _a.className,
_b = _a.rest,
children = _b.children,
collapsedHeight = _b.collapsedHeight,
content = _b.content,
userDefinedDefaultOpen = _b.defaultOpen,
disabled = _b.disabled,
onChange = _b.onChange,
onClose = _b.onClose,
onOpen = _b.onOpen,
userDefinedOpen = _b.open,
skipAnimation = _b.skipAnimation,
trigger = _b.trigger,
userDefinedTriggerRef = _b.triggerRef,
rest = __rest(_b, [
'children',
'collapsedHeight',
'content',
'defaultOpen',
'disabled',
'onChange',
'onClose',
'onOpen',
'open',
'skipAnimation',
'trigger',
'triggerRef',
]);
// ----
// Memoized Props and Internal Hooks
// ----
var contentRef = React.useRef(null);
var triggerRef = React.useRef();
var _c = __read(
React.useReducer(function (val) {
return val + 1;
}, 0),
2
),
forceUpdate = _c[1];
var _d = __read(React.useState(0), 2),
callbackTick = _d[0],
setCallbackTick = _d[1];
var _e = __read(
useAutoControlledValue(false, {
defaultProp: userDefinedDefaultOpen,
prop: userDefinedOpen,
}),
2
),
isOpen = _e[0],
trySetOpen = _e[1];
var collapsedVisibility = React.useMemo(
function () {
return collapsedHeight === 0 ? 'hidden' : 'unset';
},
[collapsedHeight]
);
// ----
// Internal State
// ----
var state = React.useRef({
collapse: isOpen ? 'expanded' : 'collapsed',
style: {
height: isOpen ? '' : collapsedHeight,
visibility: isOpen ? 'unset' : collapsedVisibility,
},
}).current;
// ----
// Handlers and Callbacks
// ----
var onCallback = React.useCallback(function (callback, params) {
if (typeof callback === 'function') {
if (Array.isArray(params)) {
callback.apply(void 0, __spreadArray([], __read(params), false));
} else {
callback();
}
}
}, []);
var getElementHeight = React.useCallback(function () {
var _a;
return ''.concat(
((_a = contentRef.current) === null || _a === void 0
? void 0
: _a.scrollHeight) || 0,
'px'
);
}, []);
var setCollapsed = React.useCallback(
function () {
/** If no ref, return */
if (!contentRef.current) {
return;
}
/** Update the State */
state.collapse = 'collapsed';
state.style = {
height: collapsedHeight,
visibility: collapsedVisibility,
};
forceUpdate();
setTimeout(function () {
setCallbackTick(Date.now());
}, 0);
},
[collapsedHeight, collapsedVisibility, state]
);
var setCollapsing = React.useCallback(
function () {
/** If no ref, return */
if (!contentRef.current) {
return;
}
/** If must avoid animation, skip */
if (skipAnimation) {
return setCollapsed();
}
/** Update the State */
state.collapse = 'collapsing';
state.style = {
height: getElementHeight(),
visibility: 'unset',
};
forceUpdate();
nextFrame(function () {
if (!contentRef.current) {
return;
}
if (state.collapse !== 'collapsing') {
return;
}
state.style = {
height: collapsedHeight,
visibility: 'unset',
};
setCallbackTick(Date.now());
});
},
[collapsedHeight, getElementHeight, setCollapsed, skipAnimation, state]
);
var setExpanded = React.useCallback(
function () {
/** If no ref, return */
if (!contentRef.current) {
return;
}
/** Update the State */
state.collapse = 'expanded';
state.style = {
height: '',
visibility: 'unset',
};
forceUpdate();
setTimeout(function () {
setCallbackTick(Date.now());
}, 0);
},
[state]
);
var setExpanding = React.useCallback(
function () {
/** If no ref, return */
if (!contentRef.current) {
return;
}
/** If must avoid animation, skip */
if (skipAnimation) {
return setExpanded();
}
/** Update state */
state.collapse = 'expanding';
nextFrame(function () {
/** If no ref, return */
if (!contentRef.current) {
return;
}
if (state.collapse !== 'expanding') {
return;
}
state.style = {
height: getElementHeight(),
visibility: 'unset',
};
setCallbackTick(Date.now());
});
},
[getElementHeight, setExpanded, skipAnimation, state]
);
var handleTransitionEnd = React.useCallback(
function (_a) {
var target = _a.target,
propertyName = _a.propertyName;
/** Skip other transition */
if (target !== contentRef.current || propertyName !== 'height') {
return;
}
var height = target.style.height;
/** Properly continue transition */
switch (state.collapse) {
case 'expanding':
if (!(height === '' || height === ''.concat(collapsedHeight, 'px'))) {
setExpanded();
}
break;
case 'collapsing':
if (!(height === '' || height !== ''.concat(collapsedHeight, 'px'))) {
setCollapsed();
}
break;
}
},
[collapsedHeight, setExpanded, setCollapsed, state.collapse]
);
var handleTriggerRef = React.useCallback(
function (component) {
triggerRef.current = component;
if (userDefinedTriggerRef !== undefined) {
handleRef(userDefinedTriggerRef, component);
}
},
[userDefinedTriggerRef]
);
var handleCollapsableToggle = React.useCallback(
function () {
/** Abort if Disabled */
if (disabled) {
return;
}
if (state.collapse === 'collapsed' || state.collapse === 'collapsing') {
/** Invoke user defined callback */
if (typeof onOpen === 'function') {
onOpen(state);
}
/** Try setting State */
trySetOpen(true);
} else if (
state.collapse === 'expanded' ||
state.collapse === 'expanding'
) {
/** Invoke user defined callback */
if (typeof onClose === 'function') {
onClose(state);
}
/** Try setting State */
trySetOpen(false);
}
},
[disabled, onClose, onOpen, state, trySetOpen]
);
// ----
// LifeCycle Events
// ----
/** Simulate getDerivedStateFromProps */
var didOpen = state.collapse === 'expanding' || state.collapse === 'expanded';
React.useEffect(
function () {
if (callbackTick) {
onCallback(onChange, [state]);
}
},
// This effect is used to auto invoke on state change
// only once the callbackTick will be update.
// Callback Tick must be the only dependencies
// eslint-disable-next-line react-hooks/exhaustive-deps
[callbackTick]
);
React.useEffect(
function () {
if (isOpen && !didOpen && !disabled) {
setExpanding();
} else if (!isOpen && didOpen && !disabled) {
setCollapsing();
}
},
// This effect is used to set the animation start
// while opening or closing the collapsable element
// In this case, the only dependencies of the Effect
// must be the open state and the internal didOpen state
// eslint-disable-next-line react-hooks/exhaustive-deps
[didOpen, isOpen]
);
// ----
// Class List Building
// ----
var classes = clsx(
{
disabled: disabled,
opening: state.collapse === 'expanding',
opened: state.collapse === 'expanded' || state.collapse === 'expanding',
closing: state.collapse === 'collapsing',
closed: state.collapse === 'collapsed' || state.collapse === 'collapsing',
},
'collapsable',
className
);
// ----
// Trigger Element
// ----
var triggerElement = React.useMemo(
function () {
if (!trigger) {
return null;
}
return React.createElement(
Ref,
{ innerRef: handleTriggerRef },
React.cloneElement(trigger, {
onClick: handleCollapsableToggle,
})
);
},
[handleCollapsableToggle, handleTriggerRef, trigger]
);
// ----
// Component Render
// ----
var collapsableContent = childrenUtils.isNil(children) ? content : children;
var contentStyle = __assign(
{ overflow: state.collapse === 'expanded' ? '' : 'hidden' },
state.style
);
return React.createElement(
'div',
__assign({}, rest, { className: classes }),
triggerElement &&
React.createElement('div', { className: 'trigger' }, triggerElement),
React.createElement(
'div',
{
ref: contentRef,
className: 'content',
style: contentStyle,
onTransitionEnd: handleTransitionEnd,
},
collapsableContent
)
);
};
Collapsable.displayName = 'Collapsable';
Collapsable.create = createShorthandFactory(Collapsable, function (content) {
return {
content: content,
};
});
export { Collapsable as default };