{
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}
+
+ );
+};