/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import React, { ReactNode, ReactElement, useEffect, useRef, useCallback, CSSProperties, FunctionComponent, HTMLAttributes, ComponentType, } from 'react'; import classNames from 'classnames'; import { CommonProps } from '../common'; import { keys } from '../../services'; import { useResizeObserver } from '../observer/resize_observer'; import { OuiResizableContainerContextProvider } from './context'; import { OuiResizableButtonProps, ouiResizableButtonWithControls, } from './resizable_button'; import { OuiResizablePanelProps, ouiResizablePanelWithControls, getModeType, ToggleCollapseCallback, } from './resizable_panel'; import { useContainerCallbacks, getPosition } from './helpers'; import { OuiResizableButtonMouseEvent, OuiResizableButtonKeyDownEvent, OuiResizableContainerState, OuiResizableContainerActions, } from './types'; const containerDirections = { vertical: 'vertical', horizontal: 'horizontal', }; export interface OuiResizableContainerProps extends HTMLAttributes, CommonProps { /** * Specify the container direction */ direction?: keyof typeof containerDirections; /** * Pure function which accepts Panel and Resizer components in arguments * and returns a component tree */ children: ( Panel: ComponentType, Resizer: ComponentType, actions: Partial ) => ReactNode; /** * Pure function which accepts an object where keys are IDs of panels, which sizes were changed, * and values are actual sizes in percents */ onPanelWidthChange?: ({}: { [key: string]: number }) => any; onToggleCollapsed?: ToggleCollapseCallback; style?: CSSProperties; } const initialState: OuiResizableContainerState = { isDragging: false, currentResizerPos: -1, prevPanelId: null, nextPanelId: null, containerSize: 1, panels: {}, resizers: {}, }; export const OuiResizableContainer: FunctionComponent = ({ direction = 'horizontal', children, className, onPanelWidthChange, onToggleCollapsed, ...rest }) => { const containerRef = useRef(null); const isHorizontal = direction === containerDirections.horizontal; const classes = classNames( 'ouiResizableContainer', { 'ouiResizableContainer--vertical': !isHorizontal, 'ouiResizableContainer--horizontal': isHorizontal, }, className ); const [actions, reducerState] = useContainerCallbacks({ initialState: { ...initialState, isHorizontal }, containerRef, onPanelWidthChange, }); const containerSize = useResizeObserver( containerRef.current, isHorizontal ? 'width' : 'height' ); const initialize = useCallback(() => { actions.initContainer(isHorizontal); }, [actions, isHorizontal]); useEffect(() => { if (containerSize.width > 0 && containerSize.height > 0) { initialize(); } }, [initialize, containerSize]); const onMouseDown = useCallback( (event: OuiResizableButtonMouseEvent) => { const currentTarget = event.currentTarget; const prevPanel = currentTarget.previousElementSibling; const nextPanel = currentTarget.nextElementSibling; if (!prevPanel || !nextPanel) return; const prevPanelId = prevPanel!.id; const nextPanelId = nextPanel!.id; const position = getPosition(event, isHorizontal); actions.dragStart({ position, prevPanelId, nextPanelId }); }, [actions, isHorizontal] ); const onMouseMove = useCallback( (event: React.MouseEvent | React.TouchEvent) => { if ( !reducerState.prevPanelId || !reducerState.nextPanelId || !reducerState.isDragging ) return; const position = getPosition(event, isHorizontal); actions.dragMove({ position, prevPanelId: reducerState.prevPanelId, nextPanelId: reducerState.nextPanelId, }); }, [ actions, isHorizontal, reducerState.prevPanelId, reducerState.nextPanelId, reducerState.isDragging, ] ); const onKeyDown = useCallback( (event: OuiResizableButtonKeyDownEvent) => { const { key, currentTarget } = event; const shouldResizeHorizontalPanel = isHorizontal && (key === keys.ARROW_LEFT || key === keys.ARROW_RIGHT); const shouldResizeVerticalPanel = !isHorizontal && (key === keys.ARROW_UP || key === keys.ARROW_DOWN); const prevPanelId = currentTarget.previousElementSibling!.id; const nextPanelId = currentTarget.nextElementSibling!.id; let direction; if (key === keys.ARROW_DOWN || key === keys.ARROW_RIGHT) { direction = 'forward'; } if (key === keys.ARROW_UP || key === keys.ARROW_LEFT) { direction = 'backward'; } if ( direction === 'forward' || (direction === 'backward' && (shouldResizeHorizontalPanel || shouldResizeVerticalPanel) && prevPanelId && nextPanelId) ) { event.preventDefault(); actions.keyMove({ direction, prevPanelId, nextPanelId }); } }, [actions, isHorizontal] ); const onMouseUp = useCallback(() => { actions.reset(); }, [actions]); // eslint-disable-next-line react-hooks/exhaustive-deps const OuiResizableButton = useCallback( ouiResizableButtonWithControls({ onKeyDown, onMouseDown, onTouchStart: onMouseDown, onFocus: actions.resizerFocus, onBlur: actions.resizerBlur, isHorizontal, registration: { register: actions.registerResizer, deregister: actions.deregisterResizer, }, }), [actions, isHorizontal] ); // eslint-disable-next-line react-hooks/exhaustive-deps const OuiResizablePanel = useCallback( ouiResizablePanelWithControls({ isHorizontal, registration: { register: actions.registerPanel, deregister: actions.deregisterPanel, }, onToggleCollapsed, onToggleCollapsedInternal: actions.togglePanel, }), [actions, isHorizontal] ); const render = () => { const DEFAULT = 'custom'; const content = children(OuiResizablePanel, OuiResizableButton, { togglePanel: actions.togglePanel, }); const modes = React.isValidElement(content) ? content.props.children.map( (el: ReactElement) => getModeType(el.props.mode) || DEFAULT ) : null; if ( modes && (['collapsible', 'main'].every((i) => modes.includes(i)) || modes.every((i?: string) => i === DEFAULT)) ) { return content; } else { throw new Error( 'Both `collapsible` and `main` mode panels are required.' ); } }; return (
{render()}
); };