From 4584d919c83ced1387a1bdac7f3a52e452e63098 Mon Sep 17 00:00:00 2001
From: Cee Chen <549407+cee-chen@users.noreply.github.com>
Date: Wed, 7 Feb 2024 15:00:11 -0800
Subject: [PATCH] [Emotion] Convert EuiTreeView (#7513)
Co-authored-by: Trevor Pierce <1Copenut@users.noreply.github.com>
---
changelogs/upcoming/7513.md | 9 +
src-docs/src/views/tree_view/playground.js | 9 +-
src/components/index.scss | 1 -
.../__snapshots__/tree_view.test.tsx.snap | 175 +++++------
src/components/tree_view/_index.scss | 1 -
.../tree_view/_tree_view_item.styles.ts | 102 +++++++
src/components/tree_view/_tree_view_item.tsx | 119 ++++++++
src/components/tree_view/tree_view.a11y.tsx | 4 +-
src/components/tree_view/tree_view.scss | 117 --------
src/components/tree_view/tree_view.spec.tsx | 2 +-
.../tree_view/tree_view.stories.tsx | 105 +++++++
src/components/tree_view/tree_view.styles.ts | 33 +++
src/components/tree_view/tree_view.test.tsx | 58 ++--
src/components/tree_view/tree_view.tsx | 278 +++++++-----------
14 files changed, 592 insertions(+), 421 deletions(-)
create mode 100644 changelogs/upcoming/7513.md
delete mode 100644 src/components/tree_view/_index.scss
create mode 100644 src/components/tree_view/_tree_view_item.styles.ts
create mode 100644 src/components/tree_view/_tree_view_item.tsx
delete mode 100644 src/components/tree_view/tree_view.scss
create mode 100644 src/components/tree_view/tree_view.stories.tsx
create mode 100644 src/components/tree_view/tree_view.styles.ts
diff --git a/changelogs/upcoming/7513.md b/changelogs/upcoming/7513.md
new file mode 100644
index 00000000000..30a64d7248a
--- /dev/null
+++ b/changelogs/upcoming/7513.md
@@ -0,0 +1,9 @@
+**Bug fixes**
+
+- Fixed an `EuiTreeView` bug where `aria-expanded` was being applied to items without expandable children
+
+**CSS-in-JS conversions**
+
+- Converted `EuiTreeView` to Emotion. Updates as part of the conversion:
+ - Removed `.euiTreeView__wrapper` div node
+ - Enforced consistent `icon` size based on `display` size
diff --git a/src-docs/src/views/tree_view/playground.js b/src-docs/src/views/tree_view/playground.js
index b8d5ca18d68..66d6254b045 100644
--- a/src-docs/src/views/tree_view/playground.js
+++ b/src-docs/src/views/tree_view/playground.js
@@ -1,15 +1,18 @@
import { EuiTreeView, EuiIcon } from '../../../../src/components';
+import { EuiTreeViewClass } from '../../../../src/components/tree_view/tree_view';
import {
propUtilityForPlayground,
generateCustomProps,
} from '../../services/playground';
export const TreeViewConfig = () => {
- const docgenInfo = Array.isArray(EuiTreeView.__docgenInfo)
- ? EuiTreeView.__docgenInfo[0]
- : EuiTreeView.__docgenInfo;
+ const docgenInfo = Array.isArray(EuiTreeViewClass.__docgenInfo)
+ ? EuiTreeViewClass.__docgenInfo[0]
+ : EuiTreeViewClass.__docgenInfo;
const propsToUse = propUtilityForPlayground(docgenInfo.props);
+ delete propsToUse.theme;
+
propsToUse.display = {
...propsToUse.display,
defaultValue: 'default',
diff --git a/src/components/index.scss b/src/components/index.scss
index 251ea2bdb7d..5b934fcc505 100644
--- a/src/components/index.scss
+++ b/src/components/index.scss
@@ -6,7 +6,6 @@
@import 'datagrid/index';
@import 'form/index';
@import 'markdown_editor/index';
-@import 'tree_view/index';
@import 'side_nav/index';
@import 'selectable/index';
@import 'table/index';
diff --git a/src/components/tree_view/__snapshots__/tree_view.test.tsx.snap b/src/components/tree_view/__snapshots__/tree_view.test.tsx.snap
index ed9e607e858..0d1220c25eb 100644
--- a/src/components/tree_view/__snapshots__/tree_view.test.tsx.snap
+++ b/src/components/tree_view/__snapshots__/tree_view.test.tsx.snap
@@ -1,9 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EuiTreeView is rendered 1`] = `
-
+
Item Two
-
diff --git a/src/components/tree_view/_index.scss b/src/components/tree_view/_index.scss
deleted file mode 100644
index df3d78bc721..00000000000
--- a/src/components/tree_view/_index.scss
+++ /dev/null
@@ -1 +0,0 @@
-@import 'tree_view';
diff --git a/src/components/tree_view/_tree_view_item.styles.ts b/src/components/tree_view/_tree_view_item.styles.ts
new file mode 100644
index 00000000000..f33c875ce6d
--- /dev/null
+++ b/src/components/tree_view/_tree_view_item.styles.ts
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { css } from '@emotion/react';
+
+import { UseEuiTheme, transparentize } from '../../services';
+import { euiFocusRing, logicalCSS, mathWithUnits } from '../../global_styling';
+
+export const euiTreeViewItemStyles = (euiThemeContext: UseEuiTheme) => {
+ const { euiTheme } = euiThemeContext;
+
+ const defaultSize = euiTheme.size.xl;
+ const compressedSize = euiTheme.size.l;
+
+ return {
+ li: {
+ euiTreeView__node: css``,
+ default: css`
+ ${logicalCSS('max-height', defaultSize)}
+ line-height: ${defaultSize};
+ `,
+ compressed: css`
+ ${logicalCSS('max-height', compressedSize)}
+ line-height: ${compressedSize};
+ `,
+ expanded: css`
+ ${logicalCSS('max-height', '100vh')}
+ `,
+ },
+
+ button: {
+ euiTreeView__nodeInner: css`
+ ${logicalCSS('width', '100%')}
+ ${logicalCSS('padding-left', euiTheme.size.s)}
+ ${logicalCSS('padding-right', euiTheme.size.xxs)}
+ display: flex;
+ align-items: center;
+
+ &:focus {
+ ${euiFocusRing(euiThemeContext, 'inset')}
+ }
+
+ &:hover,
+ &:active,
+ &:focus {
+ background-color: ${transparentize(
+ euiTheme.colors.text,
+ euiTheme.focus.transparency
+ )};
+ }
+ `,
+ default: css`
+ ${logicalCSS('height', defaultSize)}
+ gap: ${euiTheme.size.s};
+ border-radius: ${euiTheme.border.radius.medium};
+ `,
+ compressed: css`
+ ${logicalCSS('height', compressedSize)}
+ gap: ${euiTheme.size.xs};
+ border-radius: ${euiTheme.border.radius.small};
+ `,
+ },
+
+ icon: {
+ euiTreeView__iconWrapper: css`
+ flex-shrink: 0;
+ line-height: 0; /* Vertically centers the icon */
+
+ /* Handle smaller icons in compressed mode */
+ & > * {
+ ${logicalCSS('max-width', '100%')}
+ }
+
+ & > .euiToken {
+ ${logicalCSS('max-height', '100%')}
+ ${logicalCSS('height', 'auto')}
+
+ svg {
+ ${logicalCSS('width', '100%')}
+ }
+ }
+ `,
+ default: css`
+ ${logicalCSS(
+ 'width',
+ mathWithUnits(defaultSize, (x) => x / 2)
+ )}
+ `,
+ compressed: css`
+ ${logicalCSS(
+ 'width',
+ mathWithUnits(compressedSize, (x) => x / 2)
+ )}
+ `,
+ },
+ };
+};
diff --git a/src/components/tree_view/_tree_view_item.tsx b/src/components/tree_view/_tree_view_item.tsx
new file mode 100644
index 00000000000..3330e188099
--- /dev/null
+++ b/src/components/tree_view/_tree_view_item.tsx
@@ -0,0 +1,119 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, {
+ FunctionComponent,
+ PropsWithChildren,
+ HTMLAttributes,
+ ReactNode,
+ Ref,
+ memo,
+} from 'react';
+import classNames from 'classnames';
+
+import { useEuiTheme } from '../../services';
+import { CommonProps } from '../common';
+import { EuiIcon } from '../icon';
+
+import { EuiTreeViewProps } from './tree_view';
+import { euiTreeViewItemStyles } from './_tree_view_item.styles';
+
+type EuiTreeViewItemProps = HTMLAttributes
&
+ CommonProps &
+ PropsWithChildren & {
+ id: string;
+ label: ReactNode;
+ icon?: ReactNode;
+ hasArrow?: boolean;
+ isActive?: boolean;
+ isExpanded?: boolean;
+ display?: EuiTreeViewProps['display'];
+ buttonRef?: Ref;
+ wrapperProps?: HTMLAttributes;
+ };
+
+export const EuiTreeViewItem: FunctionComponent = memo(
+ ({
+ id,
+ label,
+ className,
+ children,
+ display = 'default',
+ icon,
+ hasArrow,
+ isActive,
+ isExpanded,
+ buttonRef,
+ wrapperProps,
+ ...rest
+ }) => {
+ const euiTheme = useEuiTheme();
+ const styles = euiTreeViewItemStyles(euiTheme);
+
+ const wrapperClasses = classNames(
+ 'euiTreeView__node',
+ { 'euiTreeView__node--expanded': isExpanded },
+ wrapperProps?.className
+ );
+ const wrapperStyles = [
+ styles.li.euiTreeView__node,
+ styles.li[display],
+ isExpanded && styles.li.expanded,
+ ];
+
+ const buttonClasses = classNames('euiTreeView__nodeInner', className, {
+ 'euiTreeView__node--active': isActive,
+ });
+ const buttonStyles = [
+ styles.button.euiTreeView__nodeInner,
+ styles.button[display],
+ ];
+
+ const iconStyles = [
+ styles.icon.euiTreeView__iconWrapper,
+ styles.icon[display],
+ ];
+
+ return (
+
+
+ {hasArrow &&
+ (!!children ? (
+
+ ) : (
+
+ ))}
+ {icon && (
+
+ {icon}
+
+ )}
+
+ {label}
+
+
+ {children}
+
+ );
+ }
+);
+EuiTreeViewItem.displayName = 'EuiTreeViewItem';
diff --git a/src/components/tree_view/tree_view.a11y.tsx b/src/components/tree_view/tree_view.a11y.tsx
index 580204ed111..092eda6ddfb 100644
--- a/src/components/tree_view/tree_view.a11y.tsx
+++ b/src/components/tree_view/tree_view.a11y.tsx
@@ -77,13 +77,13 @@ describe('EuiTreeView', () => {
describe('Automated accessibility check', () => {
it('has zero violations on first render', () => {
cy.mount();
- cy.get('div.euiTreeView__wrapper').should('exist');
+ cy.get('ul.euiTreeView').should('exist');
cy.checkAxe();
});
it('has zero violations with a nested child expanded', () => {
cy.mount();
- cy.get('div.euiTreeView__wrapper').should('exist');
+ cy.get('ul.euiTreeView').should('exist');
cy.get('button#item_b').realClick();
cy.get('button#item_b').should('have.attr', 'aria-expanded', 'true');
cy.get('li.euiTreeView__node').contains('A Cloud').should('exist');
diff --git a/src/components/tree_view/tree_view.scss b/src/components/tree_view/tree_view.scss
deleted file mode 100644
index 36bc649a7b4..00000000000
--- a/src/components/tree_view/tree_view.scss
+++ /dev/null
@@ -1,117 +0,0 @@
-.euiTreeView__wrapper .euiTreeView {
- margin: 0;
- list-style-type: none;
-}
-
-.euiTreeView .euiTreeView {
- padding-left: $euiSizeL;
-}
-
-.euiTreeView__node {
- max-height: $euiSizeXL;
- line-height: $euiSizeXL;
-}
-
-.euiTreeView__node--expanded {
- max-height: 100vh;
-}
-
-.euiTreeView__nodeInner {
- @include euiTextTruncate;
-
- padding-left: $euiSizeS;
- display: flex;
- flex-direction: row;
- align-items: center;
- height: $euiSizeXL;
- border-radius: $euiBorderRadius;
- width: 100%;
- text-align-last: left;
-
- &:focus {
- @include euiFocusRing('small', 'inner');
- }
-
- &:hover,
- &:active,
- &:focus {
- @include euiFocusBackground($euiTextColor);
- }
-
- .euiTreeView__iconPlaceholder {
- width: $euiSizeXL;
- }
-}
-
-.euiTreeView__nodeLabel {
- @include euiTextTruncate;
-}
-
-.euiTreeView__iconWrapper {
- margin-top: -($euiSizeXS / 2);
- margin-right: $euiSizeS;
-
- // This helps tokens appear vertically centered
- .euiToken {
- margin-top: $euiSizeXS / 2;
- }
-}
-
-// TODO: Address nesting during Emotion conversion, if possible
-// stylelint-disable max-nesting-depth
-.euiTreeView--compressed {
- .euiTreeView__node {
- max-height: $euiSizeL;
- line-height: $euiSizeL;
-
- .euiTreeView__nodeInner {
- height: $euiSizeL;
- }
-
- .euiTreeView__iconWrapper {
- margin: 0 ($euiSizeS * .75) 0 0;
- }
-
- .euiTreeView__nodeLabel {
- margin-top: -1px;
- }
-
- .euiTreeView__iconPlaceholder {
- width: $euiSizeL;
- }
- }
-
- .euiTreeView__node--expanded {
- max-height: 100vh;
- }
-}
-
-.euiTreeView--withArrows {
- .euiTreeView__expansionArrow {
- margin-right: $euiSizeXS;
- }
-
- &.euiTreeView {
- .euiTreeView__nodeInner--withArrows {
- .euiTreeView__iconWrapper {
- margin-left: 0;
- }
- }
-
- .euiTreeView__iconWrapper {
- margin-left: $euiSize + $euiSizeXS;
- }
- }
-
- &.euiTreeView--compressed {
- .euiTreeView__nodeInner--withArrows {
- .euiTreeView__iconWrapper {
- margin-left: 0;
- }
- }
-
- .euiTreeView__iconWrapper {
- margin-left: $euiSize;
- }
- }
-}
diff --git a/src/components/tree_view/tree_view.spec.tsx b/src/components/tree_view/tree_view.spec.tsx
index c15ecacf1aa..1d51c66efda 100644
--- a/src/components/tree_view/tree_view.spec.tsx
+++ b/src/components/tree_view/tree_view.spec.tsx
@@ -77,7 +77,7 @@ describe('EuiTreeView', () => {
describe('Keyboard functionality', () => {
it('Expands and collapses children correctly', () => {
cy.realMount();
- cy.get('div.euiTreeView__wrapper').should('exist');
+ cy.get('ul.euiTreeView').should('exist');
cy.repeatRealPress('Tab', 3);
cy.focused().contains('Item B');
cy.realPress('Enter');
diff --git a/src/components/tree_view/tree_view.stories.tsx b/src/components/tree_view/tree_view.stories.tsx
new file mode 100644
index 00000000000..61ec5c3e4bb
--- /dev/null
+++ b/src/components/tree_view/tree_view.stories.tsx
@@ -0,0 +1,105 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { EuiIcon } from '../icon';
+import { EuiToken } from '../token';
+
+import { EuiTreeView, EuiTreeViewProps } from './tree_view';
+
+const meta: Meta = {
+ title: 'EuiTreeView',
+ component: EuiTreeView,
+ argTypes: {
+ 'aria-label': {
+ type: {
+ name: 'string',
+ required: true,
+ },
+ description:
+ 'Passing either an `aria-label` or an `aria-labelledby` is required for accessibility.',
+ },
+ 'aria-labelledby': {
+ type: { name: 'string', required: true },
+ description:
+ 'Passing either an `aria-label` or an `aria-labelledby` is required for accessibility.',
+ },
+ },
+ args: {
+ // Component defaults
+ display: 'default',
+ expandByDefault: false,
+ showExpansionArrows: false,
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Playground: Story = {
+ args: {
+ 'aria-label': 'Directory of items',
+ items: [
+ {
+ label: 'Item One',
+ id: 'item_one',
+ icon: ,
+ iconWhenExpanded: ,
+ isExpanded: true,
+ children: [
+ {
+ label: 'Item A',
+ id: 'item_a',
+ icon: ,
+ },
+ {
+ label: 'Item B',
+ id: 'item_b',
+ useEmptyIcon: true,
+ children: [
+ {
+ label: 'A Cloud',
+ id: 'item_cloud',
+ icon: ,
+ },
+ {
+ label: "I'm a Bug",
+ id: 'item_bug',
+ icon: ,
+ },
+ ],
+ },
+ {
+ label: 'Item C',
+ id: 'item_c',
+ children: [
+ {
+ label: 'Another Cloud',
+ id: 'item_cloud2',
+ icon: ,
+ },
+ {
+ label:
+ 'This one is a really long string that we will check truncates correctly',
+ id: 'item_bug2',
+ useEmptyIcon: true,
+ callback: () => '',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ label: 'Item Two',
+ id: 'item_two',
+ },
+ ],
+ },
+};
diff --git a/src/components/tree_view/tree_view.styles.ts b/src/components/tree_view/tree_view.styles.ts
new file mode 100644
index 00000000000..ba74d808a5a
--- /dev/null
+++ b/src/components/tree_view/tree_view.styles.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { css } from '@emotion/react';
+
+import { UseEuiTheme } from '../../services';
+import { euiFontSize, logicalCSS } from '../../global_styling';
+
+export const euiTreeViewStyles = (euiThemeContext: UseEuiTheme) => {
+ const { euiTheme } = euiThemeContext;
+
+ return {
+ euiTreeView: css`
+ margin: 0;
+ list-style-type: none;
+
+ & & {
+ ${logicalCSS('padding-left', euiTheme.size.l)}
+ }
+ `,
+ default: css`
+ font-size: ${euiFontSize(euiThemeContext, 'm').fontSize};
+ `,
+ compressed: css`
+ font-size: ${euiFontSize(euiThemeContext, 's').fontSize};
+ `,
+ };
+};
diff --git a/src/components/tree_view/tree_view.test.tsx b/src/components/tree_view/tree_view.test.tsx
index 00d94b8cdf9..150980023b6 100644
--- a/src/components/tree_view/tree_view.test.tsx
+++ b/src/components/tree_view/tree_view.test.tsx
@@ -7,11 +7,13 @@
*/
import React from 'react';
+import { fireEvent } from '@testing-library/react';
+import { render } from '../../test/rtl';
+import { shouldRenderCustomStyles } from '../../test/internal';
+import { requiredProps } from '../../test/required_props';
+
import { EuiIcon } from '../icon';
import { EuiToken } from '../token';
-import { shallow } from 'enzyme';
-import { requiredProps } from '../../test/required_props';
-import { render } from '../../test/rtl';
import { EuiTreeView } from './tree_view';
@@ -74,52 +76,52 @@ const items = [
];
describe('EuiTreeView', () => {
+ shouldRenderCustomStyles();
+
test('is rendered', () => {
const { container } = render(
);
- expect(container.firstChild).toMatchSnapshot();
+ expect(container).toMatchSnapshot();
});
- test('length of open items', () => {
- const component = shallow(
+ test('item expansion', () => {
+ const { container, getByText } = render(
);
- const instance = component.instance();
-
- expect(component.state('openItems')).toHaveLength(1);
+ const getExpandedItems = () =>
+ container.querySelectorAll('[aria-expanded="true"]');
+ expect(getExpandedItems()).toHaveLength(1);
- instance.handleNodeClick(items[1]);
- expect(component.state('openItems')).toHaveLength(2);
+ fireEvent.click(getByText('Item B'));
+ expect(getExpandedItems()).toHaveLength(2);
});
- test('activeItem changes', () => {
- const component = shallow(
+ test('active item', () => {
+ const { container, getByText } = render(
);
- const instance = component.instance();
-
- expect(component.state('activeItem')).toBe('');
+ const getActiveItem = () =>
+ container.querySelector('.euiTreeView__node--active');
+ expect(getActiveItem()).not.toBeInTheDocument();
- instance.handleNodeClick(items[1]);
- expect(component.state('activeItem')).toBe('item_two');
+ fireEvent.click(getByText('Item Two')!);
+ expect(getActiveItem()).toBeInTheDocument();
});
test('open node changes', () => {
- const component = shallow(
+ const { queryByText, getByText } = render(
);
- const instance = component.instance();
-
- expect(instance.isNodeOpen(items[1])).toBe(false);
-
- instance.handleNodeClick(items[1]);
- expect(instance.isNodeOpen(items[1])).toBe(true);
+ expect(queryByText('Item C')).toBeInTheDocument();
+ expect(queryByText('Another Bug')).not.toBeInTheDocument();
- expect(instance.isNodeOpen(items[0])).toBe(true);
+ fireEvent.click(getByText('Item C'));
+ expect(queryByText('Another Bug')).toBeInTheDocument();
- instance.handleNodeClick(items[0]);
- expect(instance.isNodeOpen(items[0])).toBe(false);
+ fireEvent.click(getByText('Item One'));
+ expect(queryByText('Item C')).not.toBeInTheDocument();
+ expect(queryByText('Another Bug')).not.toBeInTheDocument();
});
});
diff --git a/src/components/tree_view/tree_view.tsx b/src/components/tree_view/tree_view.tsx
index d866bd7eb11..36daca8febc 100644
--- a/src/components/tree_view/tree_view.tsx
+++ b/src/components/tree_view/tree_view.tsx
@@ -13,21 +13,21 @@ import React, {
ContextType,
} from 'react';
import classNames from 'classnames';
+
+import {
+ withEuiTheme,
+ WithEuiThemeProps,
+ keys,
+ htmlIdGenerator,
+} from '../../services';
import { CommonProps } from '../common';
import { EuiI18n } from '../i18n';
-import { EuiIcon } from '../icon';
import { EuiScreenReaderOnly } from '../accessibility';
-import { EuiText } from '../text';
-import { keys, htmlIdGenerator } from '../../services';
-import { EuiInnerText } from '../inner_text';
-const EuiTreeViewContext = createContext('');
+import { EuiTreeViewItem } from './_tree_view_item';
+import { euiTreeViewStyles } from './tree_view.styles';
-function hasAriaLabel(
- x: HTMLAttributes
-): x is { 'aria-label': string } {
- return x.hasOwnProperty('aria-label');
-}
+const EuiTreeViewContext = createContext('');
function getTreeId(
propId: string | undefined,
@@ -72,13 +72,6 @@ export interface Node {
export type EuiTreeViewDisplayOptions = 'default' | 'compressed';
-const displayToClassNameMap: {
- [option in EuiTreeViewDisplayOptions]: string | null;
-} = {
- default: null,
- compressed: 'euiTreeView--compressed',
-};
-
interface EuiTreeViewState {
openItems: string[];
activeItem: string;
@@ -92,16 +85,21 @@ export type CommonTreeProps = CommonProps &
* Never accepts children directly, only through the `items` prop
*/
children?: never;
- /** An array of EuiTreeViewNodes
+ /**
+ * An array of EuiTreeViewNodes
*/
items: Node[];
- /** Optionally use a variation with smaller text and icon sizes
+ /**
+ * Optionally use a variation with smaller text and icon sizes
+ * @default default
*/
display?: EuiTreeViewDisplayOptions;
- /** Set all items to open on initial load
+ /**
+ * Set all items to open on initial load
*/
expandByDefault?: boolean;
- /** Display expansion arrows next to all items
+ /**
+ * Display expansion arrows next to all items
* that contain children
*/
showExpansionArrows?: boolean;
@@ -113,7 +111,10 @@ export type EuiTreeViewProps = Omit<
> &
({ 'aria-label': string } | { 'aria-labelledby': string });
-export class EuiTreeView extends Component {
+export class EuiTreeViewClass extends Component<
+ EuiTreeViewProps & WithEuiThemeProps,
+ EuiTreeViewState
+> {
treeIdGenerator = htmlIdGenerator('euiTreeView');
static contextType = EuiTreeViewContext;
@@ -122,7 +123,7 @@ export class EuiTreeView extends Component {
isNested: boolean;
constructor(
- props: EuiTreeViewProps,
+ props: EuiTreeViewProps & WithEuiThemeProps,
// Without the optional ? typing, TS will throw errors on JSX component errors
// @see https://github.com/facebook/react/issues/13944#issuecomment-1183693239
context?: ContextType
@@ -272,167 +273,98 @@ export class EuiTreeView extends Component {
display = 'default',
expandByDefault,
showExpansionArrows,
+ theme,
...rest
} = this.props;
+ const styles = euiTreeViewStyles(theme);
+ const cssStyles = [styles.euiTreeView, styles[display]];
+
// Computed classNames
- const classes = classNames(
- 'euiTreeView',
- display ? displayToClassNameMap[display] : null,
- { 'euiTreeView--withArrows': showExpansionArrows },
- className
- );
+ const classes = classNames('euiTreeView', className);
const instructionsId = `${this.state.treeID}--instruction`;
return (
-
- {!this.isNested && (
-
- {(listNavigationInstructions: string) => (
-
- {listNavigationInstructions}
-
- )}
-
- )}
-
- {items.map((node, index) => {
- const buttonId = node.id;
- const wrappingId = this.treeIdGenerator(buttonId);
-
- return (
-
- {(ref, innerText) => (
-
- {(ariaLabel: string) => {
- const label:
- | { 'aria-label': string }
- | { 'aria-labelledby': string } = hasAriaLabel(rest)
- ? {
- 'aria-label': ariaLabel,
- }
- : {
- 'aria-labelledby': `${buttonId} ${rest['aria-labelledby']}`,
- };
-
- const nodeClasses = classNames(
- 'euiTreeView__node',
- display ? displayToClassNameMap[display] : null,
- {
- 'euiTreeView__node--expanded':
- this.isNodeOpen(node),
- }
- );
-
- const nodeButtonClasses = classNames(
- 'euiTreeView__nodeInner',
- showExpansionArrows && node.children
- ? 'euiTreeView__nodeInner--withArrows'
- : null,
- this.state.activeItem === node.id
- ? 'euiTreeView__node--active'
- : null,
- node.className ? node.className : null
- );
-
- return (
-
- -
- this.setButtonRef(ref, index)}
- data-test-subj={`euiTreeViewButton-${this.state.treeID}`}
- onKeyDown={(event: React.KeyboardEvent) =>
- this.onKeyDown(event, node)
- }
- onClick={() => this.handleNodeClick(node)}
- className={nodeButtonClasses}
- >
- {showExpansionArrows && node.children ? (
-
- ) : null}
- {node.icon && !node.useEmptyIcon ? (
-
- {this.isNodeOpen(node) &&
- node.iconWhenExpanded
- ? node.iconWhenExpanded
- : node.icon}
-
- ) : null}
- {node.useEmptyIcon && !node.icon ? (
-
- ) : null}
-
- {node.label}
-
-
-
- this.onChildrenKeydown(event, index)
- }
- >
- {node.children && this.isNodeOpen(node) ? (
-
- ) : null}
-
-
-
- );
- }}
-
- )}
-
- );
- })}
-
-
+ {(listNavigationInstructions: string) => (
+
+ {listNavigationInstructions}
+
+ )}
+
+ )}
+ {/* eslint-disable-next-line jsx-a11y/no-redundant-roles */}
+
);
}
}
+
+export const EuiTreeView = withEuiTheme(EuiTreeViewClass);