UNPKG

@zendeskgarden/container-splitter

Version:

Containers relating to Splitter in the Garden Design System

245 lines (240 loc) 9.07 kB
/** * 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 = (bodyPadding, pointerPosition, separatorHeightOrWidth = 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 = ({ idPrefix, environment, isFixed, min, max, orientation = 'vertical', keyboardStep = KEYBOARD_STEP, defaultValueNow = min, valueNow, onChange = () => undefined, separatorRef, isLeading, rtl }) => { 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(({ role = 'separator', onMouseDown, onTouchStart, onKeyDown, onClick, ...other }) => { 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.23', tabIndex: 0, ...other }; }, [doc, isFixed, isLeading, keyboardStep, lastPosition, max, min, move, orientation, primaryPaneId, rtl, separatorPosition, separatorElement, setRangedSeparatorPosition, setSeparatorPosition]); const getPrimaryPaneProps = useCallback((other = {}) => ({ 'data-garden-container-id': 'containers.splitter.primaryPane', 'data-garden-container-version': '2.0.23', id: primaryPaneId, ...other }), [primaryPaneId]); return useMemo(() => ({ getSeparatorProps, getPrimaryPaneProps, valueNow: separatorPosition }), [getSeparatorProps, getPrimaryPaneProps, separatorPosition]); }; const SplitterContainer = ({ children, render = children, ...options }) => 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 };