/* * 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, { cloneElement, FunctionComponent, ReactElement, ReactNode, useEffect, useState, } from 'react'; import classNames from 'classnames'; import { htmlIdGenerator, isWithinMinBreakpoint, throttle, } from '../../services'; import { OuiFlyout, OuiFlyoutProps } from '../flyout'; // Extend all the flyout props except `onClose` because we handle this internally export type OuiCollapsibleNavProps = Omit< OuiFlyoutProps, 'closeButtonAriaLabel' | 'type' | 'pushBreakpoint' > & { /** * ReactNode to render as this component's content */ children?: ReactNode; /** * Shows the navigation flyout */ isOpen?: boolean; /** * Keeps navigation flyout visible and push `` content via padding */ isDocked?: boolean; /** * Named breakpoint or pixel value for customizing the minimum window width to enable docking */ dockedBreakpoint?: OuiFlyoutProps['pushMinBreakpoint']; /** * Button for controlling visible state of the nav */ button?: ReactElement; /** * Keeps the display of toggle button when in docked state */ showButtonIfDocked?: boolean; }; export const OuiCollapsibleNav: FunctionComponent = ({ id, children, className, isDocked = false, isOpen = false, button, showButtonIfDocked = false, dockedBreakpoint = 'l', // Setting different OuiFlyout defaults as = 'nav' as OuiCollapsibleNavProps['as'], size = 320, side = 'left', role = null, ownFocus = true, outsideClickCloses = true, closeButtonPosition = 'outside', paddingSize = 'none', ...rest }) => { const [flyoutID] = useState(id || htmlIdGenerator()('ouiCollapsibleNav')); /** * Setting the initial state of pushed based on the `type` prop * and if the current window size is large enough (larger than `pushBreakpoint`) */ const [windowIsLargeEnoughToPush, setWindowIsLargeEnoughToPush] = useState( isWithinMinBreakpoint( typeof window === 'undefined' ? 0 : window.innerWidth, dockedBreakpoint ) ); const navIsDocked = isDocked && windowIsLargeEnoughToPush; /** * Watcher added to the window to maintain `isPushed` state depending on * the window size compared to the `pushBreakpoint` */ const functionToCallOnWindowResize = throttle(() => { if (isWithinMinBreakpoint(window.innerWidth, dockedBreakpoint)) { setWindowIsLargeEnoughToPush(true); } else { setWindowIsLargeEnoughToPush(false); } // reacts every 50ms to resize changes and always gets the final update }, 50); useEffect(() => { if (isDocked) { // Only add the event listener if we'll need to accommodate with padding window.addEventListener('resize', functionToCallOnWindowResize); } return () => { if (isDocked) { window.removeEventListener('resize', functionToCallOnWindowResize); } }; }, [isDocked, functionToCallOnWindowResize]); const classes = classNames('ouiCollapsibleNav', className); // Show a trigger button if one was passed but // not if navIsDocked and showButtonIfDocked is false const trigger = navIsDocked && !showButtonIfDocked ? undefined : button && cloneElement(button as ReactElement, { 'aria-controls': flyoutID, 'aria-expanded': isOpen, 'aria-pressed': isOpen, // When OuiOutsideClickDetector is enabled, we don't want both the toggle button and document touches/clicks to happen, they'll cancel eachother out onTouchEnd: (e: React.MouseEvent) => { e.nativeEvent.stopImmediatePropagation(); }, onMouseUpCapture: (e: React.MouseEvent) => { e.nativeEvent.stopImmediatePropagation(); }, }); const flyout = ( {children} ); return ( <> {trigger} {(isOpen || navIsDocked) && flyout} ); };