From d5f13406352ca2b8173b772813f71fc23075cde1 Mon Sep 17 00:00:00 2001 From: supergrecko Date: Tue, 21 Jan 2020 18:06:14 +0100 Subject: [PATCH] Translate EuiTabs to TS (#2717) * Translate EuiTabs to TS * EuiTabs: Export Props types and insert temporary type for FocusEvent * EuiTabs: Remove defaultProps and use types derived from object * EuiTabs: remove propTypes from EuiTabs * EuiTabs: move PropTypes docblocks into Props types * EuiTabs: run ESLint * EuiTabs: update jest snapshots * EuiTabs: Revert name change and update exports * Add changelog entry * Manually remove parentheses added by Eslint --- CHANGELOG.md | 1 + src/components/index.d.ts | 1 - .../{tabs.test.js.snap => tabs.test.tsx.snap} | 0 src/components/tabs/index.d.ts | 43 ------- src/components/tabs/index.js | 3 - src/components/tabs/index.ts | 7 ++ ...t.js.snap => tabbed_content.test.tsx.snap} | 6 +- src/components/tabs/tabbed_content/index.js | 1 - src/components/tabs/tabbed_content/index.ts | 5 + ...ontent.test.js => tabbed_content.test.tsx} | 7 +- .../{tabbed_content.js => tabbed_content.tsx} | 113 +++++++++++------- src/components/tabs/tabs.js | 63 ---------- .../tabs/{tabs.test.js => tabs.test.tsx} | 0 src/components/tabs/tabs.tsx | 61 ++++++++++ 14 files changed, 147 insertions(+), 164 deletions(-) rename src/components/tabs/__snapshots__/{tabs.test.js.snap => tabs.test.tsx.snap} (100%) delete mode 100644 src/components/tabs/index.d.ts delete mode 100644 src/components/tabs/index.js create mode 100644 src/components/tabs/index.ts rename src/components/tabs/tabbed_content/__snapshots__/{tabbed_content.test.js.snap => tabbed_content.test.tsx.snap} (99%) delete mode 100644 src/components/tabs/tabbed_content/index.js create mode 100644 src/components/tabs/tabbed_content/index.ts rename src/components/tabs/tabbed_content/{tabbed_content.test.js => tabbed_content.test.tsx} (94%) rename src/components/tabs/tabbed_content/{tabbed_content.js => tabbed_content.tsx} (62%) delete mode 100644 src/components/tabs/tabs.js rename src/components/tabs/{tabs.test.js => tabs.test.tsx} (100%) create mode 100644 src/components/tabs/tabs.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 718fc17ac39..cd947f1c8e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## [`master`](https://github.com/elastic/eui/tree/master) +- Converted `EuiTabs` to TypeScript ([#2717](https://github.com/elastic/eui/pull/2717)) - Converted `EuiFormRow` to TypeScript ([#2712](https://github.com/elastic/eui/pull/2712)) - Updated `logoAPM`, `logoSecurity` and `logoEnterpriseSearch`. Added `logoWorkplaceSearch` and `logoObservability` ([#2769](https://github.com/elastic/eui/pull/2769)) - Converted `EuiFilterButton` to TypeScript ([#2761](https://github.com/elastic/eui/pull/2761)) diff --git a/src/components/index.d.ts b/src/components/index.d.ts index 271af437ab9..8932ffe960a 100644 --- a/src/components/index.d.ts +++ b/src/components/index.d.ts @@ -3,7 +3,6 @@ /// /// /// -/// declare module '@elastic/eui' { // @ts-ignore diff --git a/src/components/tabs/__snapshots__/tabs.test.js.snap b/src/components/tabs/__snapshots__/tabs.test.tsx.snap similarity index 100% rename from src/components/tabs/__snapshots__/tabs.test.js.snap rename to src/components/tabs/__snapshots__/tabs.test.tsx.snap diff --git a/src/components/tabs/index.d.ts b/src/components/tabs/index.d.ts deleted file mode 100644 index d6c16d6085c..00000000000 --- a/src/components/tabs/index.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ReactNode, FunctionComponent, HTMLAttributes } from 'react'; -import { CommonProps } from '../common'; - -import { Props as EuiTabProps } from './tab'; - -declare module '@elastic/eui' { - type TAB_SIZES = 's' | 'm'; - - type TAB_DISPLAYS = 'default' | 'condensed'; - - interface EuiTabsProps { - size?: TAB_SIZES; - display?: TAB_DISPLAYS; - expand?: boolean; - } - - export interface EuiTabbedContentTab { - id: string; - name: string; - content: ReactNode; - } - - type TABBED_CONTENT_AUTOFOCUS = 'initial' | 'selected'; - - interface EuiTabbedContentProps { - tabs: EuiTabbedContentTab[]; - onTabClick?: (tab: EuiTabbedContentTab) => void; - selectedTab?: EuiTabbedContentTab; - initialSelectedTab?: EuiTabbedContentTab; - size?: TAB_SIZES; - display?: TAB_DISPLAYS; - expand?: boolean; - autoFocus?: TABBED_CONTENT_AUTOFOCUS; - } - - export const EuiTabs: FunctionComponent< - EuiTabsProps & CommonProps & HTMLAttributes - >; - export const EuiTabbedContent: FunctionComponent< - EuiTabbedContentProps & CommonProps & HTMLAttributes - >; - export const EuiTab: FunctionComponent; -} diff --git a/src/components/tabs/index.js b/src/components/tabs/index.js deleted file mode 100644 index bab8934d915..00000000000 --- a/src/components/tabs/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export { EuiTab } from './tab'; -export { EuiTabs } from './tabs'; -export { EuiTabbedContent } from './tabbed_content'; diff --git a/src/components/tabs/index.ts b/src/components/tabs/index.ts new file mode 100644 index 00000000000..f5cfc06bd0b --- /dev/null +++ b/src/components/tabs/index.ts @@ -0,0 +1,7 @@ +export { EuiTab } from './tab'; +export { EuiTabs, EuiTabsProps } from './tabs'; +export { + EuiTabbedContent, + EuiTabbedContentTab, + EuiTabbedContentProps, +} from './tabbed_content'; diff --git a/src/components/tabs/tabbed_content/__snapshots__/tabbed_content.test.js.snap b/src/components/tabs/tabbed_content/__snapshots__/tabbed_content.test.tsx.snap similarity index 99% rename from src/components/tabs/tabbed_content/__snapshots__/tabbed_content.test.js.snap rename to src/components/tabs/tabbed_content/__snapshots__/tabbed_content.test.tsx.snap index 42adc168596..1565246e9ea 100644 --- a/src/components/tabs/tabbed_content/__snapshots__/tabbed_content.test.js.snap +++ b/src/components/tabs/tabbed_content/__snapshots__/tabbed_content.test.tsx.snap @@ -74,11 +74,7 @@ exports[`EuiTabbedContent behavior when uncontrolled, the selected tab should up
- +
{ describe('props', () => { describe('onTabClick', () => { test('is called when a tab is clicked', () => { - const onTabClickHandler = sinon.stub(); + const onTabClickHandler = jest.fn(); const component = mount( ); findTestSubject(component, 'kibanaTab').simulate('click'); - sinon.assert.calledOnce(onTabClickHandler); - sinon.assert.calledWith(onTabClickHandler, kibanaTab); + expect(onTabClickHandler).toBeCalledTimes(1); + expect(onTabClickHandler).toBeCalledWith(kibanaTab); }); }); diff --git a/src/components/tabs/tabbed_content/tabbed_content.js b/src/components/tabs/tabbed_content/tabbed_content.tsx similarity index 62% rename from src/components/tabs/tabbed_content/tabbed_content.js rename to src/components/tabs/tabbed_content/tabbed_content.tsx index 715e355227f..dc3351874e0 100644 --- a/src/components/tabs/tabbed_content/tabbed_content.js +++ b/src/components/tabs/tabbed_content/tabbed_content.tsx @@ -1,64 +1,80 @@ -import React, { Component, createRef } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, createRef, HTMLAttributes, ReactNode } from 'react'; import { htmlIdGenerator } from '../../../services'; -import { EuiTabs, DISPLAYS, SIZES } from '../tabs'; +import { EuiTabs, EuiTabsDisplaySizes, EuiTabsSizes } from '../tabs'; import { EuiTab } from '../tab'; +import { CommonProps } from '../../common'; const makeId = htmlIdGenerator(); -export const AUTOFOCUS = ['initial', 'selected']; +/** + * Marked as const so type is `['initial', 'selected']` instead of `string[]` + */ +export const AUTOFOCUS = ['initial', 'selected'] as const; -export class EuiTabbedContent extends Component { - static propTypes = { - className: PropTypes.string, +export interface EuiTabbedContentTab { + id: string; + name: string; + content: ReactNode; +} + +interface EuiTabbedContentState { + selectedTabId: string | undefined; + inFocus: boolean; +} + +export type EuiTabbedContentProps = CommonProps & + HTMLAttributes & { + /** + * When tabbing into the tabs, set the focus on `initial` for the first tab, + * or `selected` for the currently selected tab. Best use case is for inside of + * overlay content like popovers or flyouts. + */ + autoFocus?: 'initial' | 'selected'; /** * Choose `default` or alternative `condensed` display styles */ - display: PropTypes.oneOf(DISPLAYS), + display?: EuiTabsDisplaySizes; /** * Evenly stretches each tab to fill the horizontal space */ - expand: PropTypes.bool, + expand?: boolean; /** * Use this prop to set the initially selected tab while letting the tabbed content component * control selection state internally */ - initialSelectedTab: PropTypes.object, - onTabClick: PropTypes.func, + initialSelectedTab?: EuiTabbedContentTab; + onTabClick?: (selectedTab: EuiTabbedContentTab) => void; /** * Use this prop if you want to control selection state within the owner component */ - selectedTab: PropTypes.object, - /** - * When tabbing into the tabs, set the focus on `initial` for the first tab, - * or `selected` for the currently selected tab. Best use case is for inside of - * overlay content like popovers or flyouts. - */ - autoFocus: PropTypes.oneOf(AUTOFOCUS), - size: PropTypes.oneOf(SIZES), + selectedTab?: EuiTabbedContentTab; + size?: EuiTabsSizes; /** * Each tab needs id and content properties, so we can associate it with its panel for accessibility. * The name property is also required to display to the user. */ - tabs: PropTypes.arrayOf( - PropTypes.shape({ - content: PropTypes.node.isRequired, - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - }) - ).isRequired, + tabs: EuiTabbedContentTab[]; + }; + +export class EuiTabbedContent extends Component< + EuiTabbedContentProps, + EuiTabbedContentState +> { + static defaultProps = { + autoFocus: 'initial', }; - constructor(props) { + private readonly rootId = makeId(); + + private readonly divRef = createRef(); + + constructor(props: EuiTabbedContentProps) { super(props); const { initialSelectedTab, selectedTab, tabs } = props; - this.rootId = makeId(); - this.divRef = createRef(); - // Only track selection state if it's not controlled externally. let selectedTabId; if (!selectedTab) { @@ -76,13 +92,21 @@ export class EuiTabbedContent extends Component { // IE11 doesn't support the `relatedTarget` event property for blur events // but does add it for focusout. React doesn't support `onFocusOut` so here we are. if (this.divRef.current) { - this.divRef.current.addEventListener('focusout', this.removeFocus); + // Current short-term solution for event listener (see https://github.com/elastic/eui/pull/2717) + this.divRef.current.addEventListener( + 'focusout' as 'blur', + this.removeFocus + ); } } componentWillUnmount() { if (this.divRef.current) { - this.divRef.current.removeEventListener('focusout', this.removeFocus); + // Current short-term solution for event listener (see https://github.com/elastic/eui/pull/2717) + this.divRef.current.removeEventListener( + 'focusout' as 'blur', + this.removeFocus + ); } } @@ -91,24 +115,27 @@ export class EuiTabbedContent extends Component { // Must wait for setState to finish before calling `.focus()` // as the focus call triggers a blur on the first tab this.setState({ inFocus: true }, () => { - const targetTab = this.divRef.current.querySelector( + const targetTab: HTMLDivElement | null = this.divRef.current!.querySelector( `#${this.state.selectedTabId}` ); - targetTab.focus(); + targetTab!.focus(); }); } }; - removeFocus = blurEvent => { + // todo: figure out type for blurEvent + removeFocus = (blurEvent: FocusEvent) => { // only set inFocus to false if the wrapping div doesn't contain the now-focusing element - if (blurEvent.currentTarget.contains(blurEvent.relatedTarget) === false) { + const currentTarget = blurEvent.currentTarget! as HTMLElement; + const relatedTarget = blurEvent.relatedTarget! as HTMLElement; + if (currentTarget.contains(relatedTarget) === false) { this.setState({ inFocus: false, }); } }; - onTabClick = selectedTab => { + onTabClick = (selectedTab: EuiTabbedContentTab) => { const { onTabClick, selectedTab: externalSelectedTab } = this.props; if (onTabClick) { @@ -138,9 +165,11 @@ export class EuiTabbedContent extends Component { // Allow the consumer to control tab selection. const selectedTab = externalSelectedTab || - tabs.find(tab => tab.id === this.state.selectedTabId); + tabs.find( + (tab: EuiTabbedContentTab) => tab.id === this.state.selectedTabId + ); - const { content: selectedTabContent, id: selectedTabId } = selectedTab; + const { content: selectedTabContent, id: selectedTabId } = selectedTab!; return (
- {tabs.map(tab => { + {tabs.map((tab: EuiTabbedContentTab) => { const { id, name, @@ -179,7 +208,3 @@ export class EuiTabbedContent extends Component { ); } } - -EuiTabbedContent.defaultProps = { - autoFocus: 'initial', -}; diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js deleted file mode 100644 index 57badef49b9..00000000000 --- a/src/components/tabs/tabs.js +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -const displayToClassNameMap = { - condensed: 'euiTabs--condensed', - default: null, -}; - -export const DISPLAYS = Object.keys(displayToClassNameMap); - -const sizeToClassNameMap = { - s: 'euiTabs--small', - m: null, -}; - -export const SIZES = Object.keys(sizeToClassNameMap); - -export const EuiTabs = ({ - children, - className, - display, - expand, - size, - ...rest -}) => { - const classes = classNames( - 'euiTabs', - displayToClassNameMap[display], - sizeToClassNameMap[size], - { - 'euiTabs--expand': expand, - }, - className - ); - - return ( -
- {children} -
- ); -}; - -EuiTabs.propTypes = { - children: PropTypes.node, - className: PropTypes.string, - /** - * Choose `default` or alternative `condensed` display styles - */ - display: PropTypes.oneOf(DISPLAYS), - /** - * Evenly stretches each tab to fill the - * horizontal space - */ - expand: PropTypes.bool, - size: PropTypes.oneOf(SIZES), -}; - -EuiTabs.defaultProps = { - display: 'default', - expand: false, - size: 'm', -}; diff --git a/src/components/tabs/tabs.test.js b/src/components/tabs/tabs.test.tsx similarity index 100% rename from src/components/tabs/tabs.test.js rename to src/components/tabs/tabs.test.tsx diff --git a/src/components/tabs/tabs.tsx b/src/components/tabs/tabs.tsx new file mode 100644 index 00000000000..3c27d57fad5 --- /dev/null +++ b/src/components/tabs/tabs.tsx @@ -0,0 +1,61 @@ +import React, { HTMLAttributes, PropsWithChildren } from 'react'; +import classNames from 'classnames'; +import { CommonProps, keysOf } from '../common'; + +const displayToClassNameMap = { + condensed: 'euiTabs--condensed', + default: null, +}; + +export const DISPLAYS = keysOf(displayToClassNameMap); + +export type EuiTabsDisplaySizes = keyof typeof displayToClassNameMap; + +const sizeToClassNameMap = { + s: 'euiTabs--small', + m: null, +}; + +export const SIZES = keysOf(sizeToClassNameMap); + +export type EuiTabsSizes = keyof typeof sizeToClassNameMap; + +export type EuiTabsProps = CommonProps & + HTMLAttributes & { + /** + * Choose `default` or alternative `condensed` display styles + */ + display?: EuiTabsDisplaySizes; + /** + * Evenly stretches each tab to fill the + * horizontal space + */ + expand?: boolean; + size?: EuiTabsSizes; + }; + +export const EuiTabs = ({ + children, + className, + + display = 'default', + expand = false, + size = 'm', + ...rest +}: PropsWithChildren) => { + const classes = classNames( + 'euiTabs', + displayToClassNameMap[display], + sizeToClassNameMap[size], + { + 'euiTabs--expand': expand, + }, + className + ); + + return ( +
+ {children} +
+ ); +};