@zendeskgarden/container-splitter
Version:
Containers relating to Splitter in the Garden Design System
261 lines (256 loc) • 9.42 kB
JavaScript
/**
* Copyright Zendesk, Inc.
*
* Use of this source code is governed under the Apache License, Version 2.0
* found at http://www.apache.org/licenses/LICENSE-2.0.
*/
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { useId, composeEventHandlers, KEYS } from '@zendeskgarden/container-utilities';
import PropTypes from 'prop-types';
const KEYBOARD_STEP = 48;
const normalizePointerToSeparator = function (bodyPadding, pointerPosition, separatorHeightOrWidth, viewportWidthOrHeight) {
if (separatorHeightOrWidth === void 0) {
separatorHeightOrWidth = 0;
}
if (viewportWidthOrHeight === void 0) {
viewportWidthOrHeight = 0;
}
if (viewportWidthOrHeight === 0) {
return pointerPosition - bodyPadding - Math.floor(separatorHeightOrWidth / 2);
}
return viewportWidthOrHeight - pointerPosition - bodyPadding - Math.floor(separatorHeightOrWidth / 2);
};
const xor = (a, b) => {
if (a && b) {
return false;
}
return a || b;
};
const useSplitter = _ref => {
let {
idPrefix,
environment,
isFixed,
min,
max,
orientation = 'vertical',
keyboardStep = KEYBOARD_STEP,
defaultValueNow = min,
valueNow,
onChange = () => undefined,
separatorRef,
isLeading,
rtl
} = _ref;
const prefix = useId(idPrefix);
const primaryPaneId = `${prefix}--primary-pane`;
const isControlled = valueNow !== undefined && valueNow !== null;
const [state, setState] = useState(defaultValueNow);
const [separatorElement, setSeparatorElement] = useState(separatorRef.current);
const offsetRef = useRef({
left: 0,
right: 0,
top: 0,
bottom: 0
});
const separatorPosition = isControlled ? valueNow : state;
const [lastPosition, setLastPosition] = useState(separatorPosition);
const doc = environment || document;
useEffect(() => {
if (separatorRef.current !== separatorElement) {
setSeparatorElement(separatorRef.current);
}
});
const setSeparatorPosition = isControlled ? onChange : setState;
const setRangedSeparatorPosition = useCallback(nextDimension => {
if (nextDimension >= max) {
setSeparatorPosition(max);
} else if (nextDimension <= min) {
setSeparatorPosition(min);
} else {
setSeparatorPosition(nextDimension);
}
}, [max, min, setSeparatorPosition]);
const move = useCallback((pageX, pageY) => {
if (separatorElement) {
const clientWidth = xor(rtl, isLeading) ? doc.body.clientWidth : undefined;
const clientHeight = isLeading ? doc.body.clientHeight : undefined;
if (orientation === 'horizontal') {
const offset = isLeading ? offsetRef.current.bottom : offsetRef.current.top;
setRangedSeparatorPosition(
normalizePointerToSeparator(offset, pageY, separatorElement.offsetHeight, clientHeight));
} else {
const offset = xor(rtl, isLeading) ? offsetRef.current.right : offsetRef.current.left;
setRangedSeparatorPosition(
normalizePointerToSeparator(offset, pageX, separatorElement.offsetWidth, clientWidth));
}
}
}, [doc, isLeading, orientation, rtl, separatorElement, setRangedSeparatorPosition]);
const getSeparatorProps = useCallback(_ref2 => {
let {
role = 'separator',
onMouseDown,
onTouchStart,
onKeyDown,
onClick,
...other
} = _ref2;
const onMouseMove = event => {
move(event.pageX, event.pageY);
};
const onTouchMove = event => {
const {
pageY,
pageX
} = event.targetTouches[0];
move(pageX, pageY);
};
const onMouseUp = () => {
doc.removeEventListener('mouseup', onMouseUp);
doc.removeEventListener('mousemove', onMouseMove);
};
const onTouchEnd = () => {
doc.removeEventListener('touchend', onTouchEnd);
doc.removeEventListener('touchmove', onTouchMove);
};
const updateOffsets = () => {
if (separatorElement) {
const rect = separatorElement.getBoundingClientRect();
const clientWidth = doc.body.clientWidth;
const clientHeight = doc.body.clientHeight;
const win = doc.documentElement || doc.body.parentNode || doc.body;
offsetRef.current.left = rect.left - separatorPosition + win.scrollLeft;
offsetRef.current.right = clientWidth - rect.right - separatorPosition - win.scrollLeft;
offsetRef.current.top = rect.top - separatorPosition + win.scrollTop;
offsetRef.current.bottom = clientHeight - rect.bottom - separatorPosition - win.scrollTop;
}
};
const handleMouseDown = () => {
if (!isFixed) {
updateOffsets();
doc.addEventListener('mouseup', onMouseUp);
doc.addEventListener('mousemove', onMouseMove);
}
};
const handleTouchStart = () => {
if (!isFixed) {
updateOffsets();
doc.addEventListener('touchend', onTouchEnd);
doc.addEventListener('touchmove', onTouchMove);
}
};
const handleKeyDown = event => {
if (event.key === KEYS.ENTER) {
if (separatorPosition === min) {
setSeparatorPosition(lastPosition === min ? max : lastPosition);
} else {
setLastPosition(separatorPosition);
setSeparatorPosition(min);
}
} else if (event.key === KEYS.HOME) {
separatorPosition !== min && setLastPosition(separatorPosition);
setSeparatorPosition(min);
} else if (event.key === KEYS.END) {
setSeparatorPosition(max);
} else if (!isFixed) {
if (event.key === KEYS.RIGHT && orientation === 'vertical') {
let position;
if (rtl) {
position = separatorPosition + (isLeading ? keyboardStep : -keyboardStep);
} else {
position = separatorPosition + (isLeading ? -keyboardStep : keyboardStep);
}
setRangedSeparatorPosition(position);
event.preventDefault();
} else if (event.key === KEYS.LEFT && orientation === 'vertical') {
let position;
if (rtl) {
position = separatorPosition + (isLeading ? -keyboardStep : keyboardStep);
} else {
position = separatorPosition + (isLeading ? keyboardStep : -keyboardStep);
}
setRangedSeparatorPosition(position);
event.preventDefault();
} else if (event.key === KEYS.UP && orientation === 'horizontal') {
setRangedSeparatorPosition(separatorPosition + (isLeading ? keyboardStep : -keyboardStep));
event.preventDefault();
} else if (event.key === KEYS.DOWN && orientation === 'horizontal') {
setRangedSeparatorPosition(separatorPosition + (isLeading ? -keyboardStep : keyboardStep));
event.preventDefault();
}
}
};
const handleClick = event => {
if (isFixed || event.detail === 2 ) {
handleKeyDown({
key: KEYS.ENTER
});
}
};
const ariaValueNow = (separatorPosition - min) / (max - min) * 100;
const ariaValueMin = isFinite(ariaValueNow) ? 0 : min;
const ariaValueMax = isFinite(ariaValueNow) ? 100 : max;
return {
role: role === null ? undefined : role,
onMouseDown: composeEventHandlers(onMouseDown, handleMouseDown),
onTouchStart: composeEventHandlers(onTouchStart, handleTouchStart),
onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
onClick: composeEventHandlers(onClick, handleClick),
'aria-controls': primaryPaneId,
'aria-valuenow': isFinite(ariaValueNow) ? ariaValueNow : separatorPosition,
'aria-valuemin': ariaValueMin,
'aria-valuemax': ariaValueMax,
'aria-orientation': orientation,
'data-garden-container-id': 'containers.splitter.separator',
'data-garden-container-version': '2.0.20',
tabIndex: 0,
...other
};
}, [doc, isFixed, isLeading, keyboardStep, lastPosition, max, min, move, orientation, primaryPaneId, rtl, separatorPosition, separatorElement, setRangedSeparatorPosition, setSeparatorPosition]);
const getPrimaryPaneProps = useCallback(function (other) {
if (other === void 0) {
other = {};
}
return {
'data-garden-container-id': 'containers.splitter.primaryPane',
'data-garden-container-version': '2.0.20',
id: primaryPaneId,
...other
};
}, [primaryPaneId]);
return useMemo(() => ({
getSeparatorProps,
getPrimaryPaneProps,
valueNow: separatorPosition
}), [getSeparatorProps, getPrimaryPaneProps, separatorPosition]);
};
const SplitterContainer = _ref => {
let {
children,
render = children,
...options
} = _ref;
return React.createElement(React.Fragment, null, render(useSplitter(options)));
};
SplitterContainer.propTypes = {
children: PropTypes.func,
render: PropTypes.func,
idPrefix: PropTypes.string,
environment: PropTypes.any,
isFixed: PropTypes.bool,
min: PropTypes.number.isRequired,
max: PropTypes.number.isRequired,
orientation: PropTypes.oneOf(['horizontal', 'vertical']),
keyboardStep: PropTypes.number,
defaultValueNow: PropTypes.number,
valueNow: PropTypes.number,
onChange: PropTypes.func,
separatorRef: PropTypes.any.isRequired,
isLeading: PropTypes.bool,
rtl: PropTypes.bool
};
SplitterContainer.defaultProps = {
keyboardStep: KEYBOARD_STEP,
orientation: 'vertical'
};
export { SplitterContainer, useSplitter };