/*
* 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}
>
);
};