diff --git a/src/components/color_picker/utils.ts b/src/components/color_picker/utils.ts index cc326471e59..4f5f26dfa63 100644 --- a/src/components/color_picker/utils.ts +++ b/src/components/color_picker/utils.ts @@ -1,4 +1,5 @@ import { MouseEvent as ReactMouseEvent, TouchEvent, useEffect } from 'react'; +import { throttle } from '../../services' export const getEventPosition = ( location: { x: number; y: number }, @@ -24,16 +25,6 @@ export const getEventPosition = ( return { left: leftPos, top: topPos, width, height }; }; -export const throttle = (fn: (...args: any[]) => void, wait = 50) => { - let time = Date.now(); - return (...args: any[]) => { - if (time + wait - Date.now() < 0) { - fn(...args); - time = Date.now(); - } - }; -}; - export function isMouseEvent( event: ReactMouseEvent | TouchEvent ): event is ReactMouseEvent { diff --git a/src/components/list_group/index.ts b/src/components/list_group/index.ts index 69dca4816b4..a82a234ef34 100644 --- a/src/components/list_group/index.ts +++ b/src/components/list_group/index.ts @@ -1,2 +1,2 @@ -export { EuiListGroup } from './list_group'; -export { EuiListGroupItem } from './list_group_item'; +export { EuiListGroup, EuiListGroupProps } from './list_group'; +export { EuiListGroupItem, EuiListGroupItemProps } from './list_group_item'; diff --git a/src/components/list_group/list_group.tsx b/src/components/list_group/list_group.tsx index 9f4e3c7de44..b6ae859410c 100644 --- a/src/components/list_group/list_group.tsx +++ b/src/components/list_group/list_group.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import { EuiListGroupItem, EuiListGroupItemProps } from './list_group_item'; import { CommonProps } from '../common'; -type EuiListGroupProps = CommonProps & +export type EuiListGroupProps = CommonProps & HTMLAttributes & { /** * Add a border to the list container diff --git a/src/components/nav_drawer/index.js b/src/components/nav_drawer/index.ts similarity index 100% rename from src/components/nav_drawer/index.js rename to src/components/nav_drawer/index.ts diff --git a/src/components/nav_drawer/nav_drawer.js b/src/components/nav_drawer/nav_drawer.tsx similarity index 76% rename from src/components/nav_drawer/nav_drawer.js rename to src/components/nav_drawer/nav_drawer.tsx index 6c5401d6f92..25ff97476ec 100644 --- a/src/components/nav_drawer/nav_drawer.js +++ b/src/components/nav_drawer/nav_drawer.tsx @@ -1,29 +1,87 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, HTMLAttributes, isValidElement, ReactNode } from 'react'; import classNames from 'classnames'; -import { EuiListGroup, EuiListGroupItem } from '../list_group'; +import { + EuiListGroup, + EuiListGroupItem, + EuiListGroupProps, + EuiListGroupItemProps, +} from '../list_group'; import { EuiNavDrawerFlyout } from './nav_drawer_flyout'; -import { EuiNavDrawerGroup, ATTR_SELECTOR } from './nav_drawer_group'; +import { EuiNavDrawerGroup, ATTR_SELECTOR, EuiNavDrawerGroupProps } from './nav_drawer_group'; import { EuiOutsideClickDetector } from '../outside_click_detector'; import { EuiI18n } from '../i18n'; import { EuiFlexItem, EuiFlexGroup } from '../flex'; -import { throttle } from '../color_picker/utils'; +import { throttle } from '../../services'; +import { CommonProps } from '../common'; const MENU_ELEMENT_ID = 'navDrawerMenu'; -export class EuiNavDrawer extends Component { - constructor(props) { +export type FlyoutLink = + EuiListGroupItemProps + & { + "data-name"?: ReactNode, + flyoutMenu?: { + title: string, + listItems: EuiListGroupProps["listItems"] + } + }; + +export interface EuiNavDrawerProps + extends CommonProps, HTMLAttributes { + /** + * Keep drawer locked open by default + */ + isLocked?: boolean, + /** + * Returns the current state of isLocked + */ + onIsLockedUpdate?: (isLocked: boolean) => void, + /** + * Adds fixed toggle button to bottom of menu area + */ + showExpandButton?: boolean, + /** + * Display tooltips on side nav items + */ + showToolTips?: boolean, + isCollapsed: boolean, + } + +interface EuiNavDrawerState { + isLocked: boolean, + isCollapsed: boolean, + flyoutIsCollapsed: boolean, + outsideClickDisabled: boolean, + isManagingFocus: boolean, + toolTipsEnabled: boolean, + focusReturnRef?: ReactNode, + navFlyoutTitle: string | null, + navFlyoutContent: EuiListGroupProps["listItems"], +} + +export class EuiNavDrawer extends Component { + static get defaultProps() { + return { + showExpandButton: true, + showToolTips: true, + }; + }; + + private expandButtonRef?: HTMLInputElement; + + constructor(props: EuiNavDrawerProps) { super(props); - this.expandButtonRef; this.state = { - isLocked: props.isLocked, + isLocked: !!props.isLocked, isCollapsed: !props.isLocked, flyoutIsCollapsed: true, outsideClickDisabled: true, isManagingFocus: false, toolTipsEnabled: true, - focusReturnRef: null, + focusReturnRef: undefined, + navFlyoutTitle: null, + navFlyoutContent: undefined, }; } @@ -37,7 +95,7 @@ export class EuiNavDrawer extends Component { window.removeEventListener('resize', this.functionToCallOnWindowResize); } - returnOnIsLockedUpdate = isLockedState => { + returnOnIsLockedUpdate = (isLockedState: boolean) => { if (this.props.onIsLockedUpdate) { this.props.onIsLockedUpdate(isLockedState); } @@ -124,15 +182,15 @@ export class EuiNavDrawer extends Component { // Scrolls the menu and flyout back to top when the nav drawer collapses setTimeout(() => { - document.getElementById('navDrawerMenu').scrollTop = 0; + document.getElementById(MENU_ELEMENT_ID)!.scrollTop = 0; }, 50); // In case it was locked before, remove the window resize listener window.removeEventListener('resize', this.functionToCallOnWindowResize); }; - expandFlyout = (links, title, item) => { - const content = links; + expandFlyout = (items: Array, title: string, item: FlyoutLink) => { + const content = items; if (this.state.navFlyoutTitle === title) { this.collapseFlyout(); @@ -168,7 +226,7 @@ export class EuiNavDrawer extends Component { { flyoutIsCollapsed: true, navFlyoutTitle: null, - navFlyoutContent: null, + navFlyoutContent: undefined, toolTipsEnabled: this.state.isLocked ? false : true, focusReturnRef: null, }, @@ -177,7 +235,7 @@ export class EuiNavDrawer extends Component { // does not allow for deep `ref` element management at present const element = document.querySelector( `#${MENU_ELEMENT_ID} [${ATTR_SELECTOR}='${focusReturn}']` - ); + ) as HTMLElement; if (!element) return; requestAnimationFrame(() => { element.setAttribute('aria-expanded', 'false'); @@ -195,14 +253,14 @@ export class EuiNavDrawer extends Component { this.collapseFlyout(false); }; - handleDrawerMenuClick = e => { + handleDrawerMenuClick = (e: React.MouseEvent) => { // walk up e.target until either: // 1. a[href] - close the menu // 2. document.body - do nothing - let element = e.target; + let element: HTMLElement | null = e.target as HTMLElement; while ( - element !== undefined && + element != null && element !== document.body && (element.tagName !== 'A' || element.getAttribute('href') === undefined) ) { @@ -215,23 +273,24 @@ export class EuiNavDrawer extends Component { } }; - modifyChildren = children => { + modifyChildren = (children: ReactNode): ReactNode => { // Loop through the EuiNavDrawer children (EuiListGroup, EuiHorizontalRules, etc) // Filter out falsy items const filteredChildren = React.Children.toArray(children); - return React.Children.map(filteredChildren, child => { + return React.Children.map(filteredChildren, (child: NonNullable) => { // Allow for Fragments by recursive modification - if (child.type === React.Fragment) { - return this.modifyChildren(child.props.children); - } else if (child.type === EuiNavDrawerGroup) { - // Check if child is an EuiNavDrawerGroup and if it does have a flyout, add the expand function - return React.cloneElement(child, { - flyoutMenuButtonClick: this.expandFlyout, - showToolTips: this.state.toolTipsEnabled && this.props.showToolTips, - }); - } else { - return child; + if (isValidElement(child)) { + if (child.type === React.Fragment) { + return this.modifyChildren(child.props.children); + } else if (child.type === EuiNavDrawerGroup) { + // Check if child is an EuiNavDrawerGroup and if it does have a flyout, add the expand function + return React.cloneElement(child, { + flyoutMenuButtonClick: this.expandFlyout, + showToolTips: this.state.toolTipsEnabled && this.props.showToolTips, + }); + } } + return child; }); }; @@ -284,7 +343,7 @@ export class EuiNavDrawer extends Component { sideNavLockAriaLabel, sideNavLockExpanded, sideNavLockCollapsed, - ]) => ( + ]: string[]) => ( (this.expandButtonRef = node)} label={this.state.isCollapsed ? sideNavExpand : sideNavCollapse} @@ -324,7 +383,7 @@ export class EuiNavDrawer extends Component { const flyoutContent = ( { + /** + * Toggle the nav drawer between collapsed and expanded + */ + isCollapsed?: boolean, + /** + * Display a title atop the flyout + */ + title?: string, + listItems: EuiListGroupProps["listItems"], + wrapText: EuiListGroupProps["wrapText"], + /** + * Passthrough function to be called when the flyout is closing + * See ./nav_drawer.js + */ + onClose?: (shouldReturnFocus: boolean) => void, + } + +export const EuiNavDrawerFlyout: FunctionComponent = ({ className, title, isCollapsed, @@ -31,7 +50,7 @@ export const EuiNavDrawerFlyout = ({ className ); - const handleKeyDown = e => { + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.keyCode === keyCodes.ESCAPE) { handleClose(); } else if (e.keyCode === keyCodes.TAB) { @@ -51,7 +70,9 @@ export const EuiNavDrawerFlyout = ({ const handleClose = (shouldReturnFocus = true) => { setTabbables(null); - onClose(shouldReturnFocus); + if (onClose) { + onClose(shouldReturnFocus); + } }; return ( @@ -78,29 +99,3 @@ export const EuiNavDrawerFlyout = ({ ); }; - -EuiNavDrawerFlyout.propTypes = { - className: PropTypes.string, - listItems: EuiListGroup.propTypes.listItems, - wrapText: EuiListGroup.propTypes.wrapText, - - /** - * Display a title atop the flyout - */ - title: PropTypes.string, - - /** - * Toggle the nav drawer between collapsed and expanded - */ - isCollapsed: PropTypes.bool, - - /** - * Passthrough function to be called when the flyout is closing - * See ./nav_drawer.js - */ - onClose: PropTypes.func, -}; - -EuiNavDrawerFlyout.defaultProps = { - isCollapsed: true, -}; diff --git a/src/components/nav_drawer/nav_drawer_group.js b/src/components/nav_drawer/nav_drawer_group.tsx similarity index 57% rename from src/components/nav_drawer/nav_drawer_group.js rename to src/components/nav_drawer/nav_drawer_group.tsx index b7d383151d7..f4daca38daa 100644 --- a/src/components/nav_drawer/nav_drawer_group.js +++ b/src/components/nav_drawer/nav_drawer_group.tsx @@ -1,13 +1,33 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { HTMLAttributes, FunctionComponent, MouseEventHandler, SyntheticEvent } from 'react'; import classNames from 'classnames'; -import { EuiListGroup } from '../list_group/list_group'; import { toInitials } from '../../services'; +import { CommonProps } from '../common'; +import { EuiListGroup } from '../list_group'; +import { FlyoutLink } from './nav_drawer'; +import { EuiListGroupProps } from '../list_group'; export const ATTR_SELECTOR = 'data-name'; -export const EuiNavDrawerGroup = ({ +export interface EuiNavDrawerGroupProps + extends CommonProps, HTMLAttributes { + className: string, + listItems: Array, + /** + * While not normally required, it is required to pass a function for handling + * of the flyout menu button click + */ + flyoutMenuButtonClick?: (items: Array, title: string, item: FlyoutLink) => MouseEventHandler, + /** + * Passthrough function to be called when the flyout is closing + * See ./nav_drawer.js + */ + onClose: () => void, + ariaLabelledby?: string; + wrapText: EuiListGroupProps["wrapText"], + } + +export const EuiNavDrawerGroup: FunctionComponent = ({ className, listItems, flyoutMenuButtonClick, @@ -23,16 +43,17 @@ export const EuiNavDrawerGroup = ({ ? undefined : listItems.map(item => { // If the flyout menu exists, pass back the list of times and the title with the onClick handler of the item - const { flyoutMenu, onClick, ...itemProps } = item; - if (flyoutMenu && flyoutMenuButtonClick) { + const { flyoutMenu, onClick } = item; + const { ...itemProps } = item; + if (flyoutMenu && flyoutMenuButtonClick && flyoutMenu.listItems) { const items = [...flyoutMenu.listItems]; const title = `${flyoutMenu.title}`; itemProps.onClick = () => flyoutMenuButtonClick(items, title, item); itemProps['aria-expanded'] = false; } else { - itemProps.onClick = (...args) => { + itemProps.onClick = (event: React.MouseEvent) => { if (onClick) { - onClick(...args); + onClick(event); } onClose(); }; @@ -45,14 +66,14 @@ export const EuiNavDrawerGroup = ({ ); itemProps.size = item.size || 's'; itemProps[ATTR_SELECTOR] = item.label; - itemProps['aria-label'] = item['aria-label'] || item.label; + itemProps['aria-label'] = item['aria-label'] || item.label as string; // Add an avatar in place of non-existent icons const itemProvidesIcon = !!item.iconType || !!item.icon; if (!itemProvidesIcon) { itemProps.icon = ( - {toInitials(item.label)} + {toInitials(item.label as string)} ); } @@ -65,25 +86,3 @@ export const EuiNavDrawerGroup = ({ ); }; - -EuiNavDrawerGroup.propTypes = { - listItems: PropTypes.arrayOf( - PropTypes.shape({ - ...EuiListGroup.propTypes.listItems[0], - flyoutMenu: PropTypes.shape({ - title: PropTypes.string.isRequired, - listItems: EuiListGroup.propTypes.listItems.isRequired, - }), - }) - ), - /** - * While not normally required, it is required to pass a function for handling - * of the flyout menu button click - */ - flyoutMenuButtonClick: PropTypes.func, - /** - * Passthrough function to be called when the flyout is closing - * See ./nav_drawer.js - */ - onClose: PropTypes.func, -}; diff --git a/src/components/title/title.tsx b/src/components/title/title.tsx index ac061f2131c..62a234a6de4 100644 --- a/src/components/title/title.tsx +++ b/src/components/title/title.tsx @@ -26,6 +26,7 @@ export type EuiTitleProps = CommonProps & { size?: EuiTitleSize; textTransform?: EuiTitleTextTransform; id?: string; + tabIndex?: string; }; export const EuiTitle: FunctionComponent = ({ diff --git a/src/services/index.ts b/src/services/index.ts index 6dcddf73154..2e6158a3f5f 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -75,6 +75,10 @@ export { Comparators, } from './sort'; +export { + throttle +} from './throttle'; + export { calculatePopoverPosition, findPopoverPosition } from './popover'; export { diff --git a/src/services/throttle.ts b/src/services/throttle.ts new file mode 100644 index 00000000000..2b165ece0ce --- /dev/null +++ b/src/services/throttle.ts @@ -0,0 +1,9 @@ +export const throttle = (fn: (...args: any[]) => void, wait = 50) => { + let time = Date.now(); + return (...args: any[]) => { + if (time + wait - Date.now() < 0) { + fn(...args); + time = Date.now(); + } + }; +};