From be2315827c80d0cc4ba0e2b58ff3b43302ed1cda Mon Sep 17 00:00:00 2001
From: Cee Chen <549407+cee-chen@users.noreply.github.com>
Date: Mon, 23 Jan 2023 15:34:18 -0800
Subject: [PATCH] [Emotion] Convert EuiBasicTable (#6539)
* [tech debt] convert `useEuiTheme` tests to RTL `renderHook`
- which is generally a nicer API than the one I yolo'd
* [tech debt] Add more missing unit tests for `useEuiTheme`
* [tech debt] write basic unit test for `withEuiTheme`
* Add new `RenderWithEuiTheme` render prop util
* Convert `tbody` loading styles to Emotion
- I opted not to create a top-level component for this due to the very limited styles being applied, and due to HOC/theme access shenanigans
* Fix error/empty states not rendering loading styles
- by only rendering one `
`, not multiple
* Write basic `loading` test
+ switch `render` to RTL
* [extra] Massive clean up of EuiBasicTable unit tests
- switch to RTL totally (shallow was not handling the new render prop well)
- DRY out various repeated props
- stop use snapshots for every single test - use specific assertions instead. For visual rendering for various prop combos, we should use Storybook
- leave snapshots in for two specific render tests - barebones & kitchen sink props
* Delete scss files
* Add `shouldRenderCustomStyles` test
* changelog
* Add affordance for reduced motion media query
- this matches how EuiProgress behaves
+ clean up animation shorthand
* Add CSS workaround/fix for visual Safari bug
- apparently `position: relative` on the parent and not on the `tbody` was a cross-browser fix :(
---
.../__snapshots__/basic_table.test.tsx.snap | 4982 +++--------------
.../in_memory_table.test.tsx.snap | 12 +-
src/components/basic_table/_basic_table.scss | 41 -
src/components/basic_table/_index.scss | 1 -
.../basic_table/basic_table.styles.ts | 52 +-
.../basic_table/basic_table.test.tsx | 876 ++-
src/components/basic_table/basic_table.tsx | 104 +-
src/components/index.scss | 1 -
src/services/theme/hooks.test.ts | 18 -
src/services/theme/hooks.test.tsx | 83 +
src/services/theme/hooks.tsx | 19 +
src/services/theme/index.ts | 2 +-
src/services/theme/provider.tsx | 2 +-
upcoming_changelogs/6539.md | 4 +
14 files changed, 1275 insertions(+), 4922 deletions(-)
delete mode 100644 src/components/basic_table/_basic_table.scss
delete mode 100644 src/components/basic_table/_index.scss
delete mode 100644 src/services/theme/hooks.test.ts
create mode 100644 src/services/theme/hooks.test.tsx
create mode 100644 upcoming_changelogs/6539.md
diff --git a/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap b/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap
index a5a2a0f910d..176625e1870 100644
--- a/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap
+++ b/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap
@@ -1,1507 +1,340 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`EuiBasicTable cellProps renders cells with custom props from a callback 1`] = `
+exports[`EuiBasicTable renders (bare-bones) 1`] = `
-`;
-
-exports[`EuiBasicTable cellProps renders rows with custom props from an object 1`] = `
-
-
-
-
-
-
-
-
-
+
+
-
-
-
+
+
+
+
+
+
-
-
- name1
-
-
-
+
`;
-exports[`EuiBasicTable empty is rendered 1`] = `
+exports[`EuiBasicTable renders (kitchen sink) with pagination, selection, sorting, actions, and footer 1`] = `
-`;
-
-exports[`EuiBasicTable empty renders a node as a custom message 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Name
-
-
-
-
-
-
- no items, click
-
- here
-
- to make some
-
-
-
-
-
-
-
-`;
-
-exports[`EuiBasicTable empty renders a string as a custom message 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Select all rows
+
+
+
+
- Name
-
-
-
-
-
- where my items at?
-
-
-
-
-
-
-`;
-
-exports[`EuiBasicTable footers do not render without a column footer definition 1`] = `
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ Sorting
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- Name
-
-
- ID
-
-
- Age
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-`;
-
-exports[`EuiBasicTable with pagination - show all 1`] = `
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
- Name
-
-
-
-
-
- name1
-
-
-
-
+
+
+
+ Rows per page
+ :
+ 3
+
+
+
+
+
+
+
+
-
-
-
-
-`;
-
-exports[`EuiBasicTable with pagination 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
- Name
-
-
-
-
-
- name1
-
-
-
-
- name2
-
-
-
-
- name3
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`EuiBasicTable with pagination and error 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Name
-
-
-
-
-
-
-
- no can do
-
-
-
-
-
-
-`;
-
-exports[`EuiBasicTable with pagination and selection 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Name
-
-
-
-
-
-
-
-
-
-
- name1
-
-
-
-
-
-
-
-
-
- name2
-
-
-
-
-
-
-
-
-
- name3
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`EuiBasicTable with pagination, hiding the per page options 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Name
-
-
-
-
-
- name1
-
-
-
-
- name2
-
-
-
-
- name3
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`EuiBasicTable with pagination, selection and sorting 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Name
-
-
-
-
-
-
-
-
-
-
- name1
-
-
-
-
-
-
-
-
-
- name2
-
-
-
-
-
-
-
-
-
- name3
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`EuiBasicTable with pagination, selection, sorting and a single record action 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Name
-
-
- Actions
-
-
-
-
-
-
-
-
-
-
- name1
-
-
-
-
-
-
-
-
-
-
-
-
- name2
-
-
-
-
-
-
-
-
-
-
-
-
- name3
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`EuiBasicTable with pagination, selection, sorting and column dataType 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Count
-
-
-
-
-
-
-
-
-
-
- 1
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
-
-
-
- 3
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`EuiBasicTable with pagination, selection, sorting and column renderer 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Name
-
-
-
-
-
-
-
-
-
-
- NAME1
-
-
-
-
-
-
-
-
-
- NAME2
-
-
-
-
-
-
-
-
-
- NAME3
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`EuiBasicTable with pagination, selection, sorting and multiple record actions 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Name
-
-
- Actions
-
-
-
-
-
-
-
-
-
-
- name1
-
-
-
-
-
-
-
-
-
-
-
-
- name2
-
-
-
-
-
-
-
-
-
-
-
-
- name3
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`EuiBasicTable with pagination, selection, sorting, column renderer and column dataType 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Count
-
-
-
-
-
-
-
-
-
-
- x
-
-
-
-
-
-
-
-
-
- xx
-
-
-
-
-
-
-
-
-
- xxx
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`EuiBasicTable with sortable columns and sorting disabled 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Name
-
-
-
-
-
- name1
-
-
-
-
- name2
-
-
-
-
- name3
-
-
-
-
-
-
-`;
-
-exports[`EuiBasicTable with sorting 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Name
-
-
-
-
-
- name1
-
-
-
-
- name2
-
-
-
-
- name3
-
-
-
-
-
-
-`;
-
-exports[`EuiBasicTable with sorting enabled and enable all columns for sorting 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Name
-
-
-
-
-
- name1
-
-
-
-
- name2
-
-
-
-
- name3
-
-
-
-
+
+
+
+
`;
diff --git a/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap b/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap
index efec1cda816..35c5a2c1014 100644
--- a/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap
+++ b/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap
@@ -22,7 +22,7 @@ exports[`EuiInMemoryTable behavior pagination 1`] = `
@@ -55,7 +55,9 @@ exports[`EuiInMemoryTable behavior pagination 1`] = `
-
+
@@ -651,7 +653,7 @@ exports[`EuiInMemoryTable with pagination and "show all" page size 1`] = `
@@ -684,7 +686,9 @@ exports[`EuiInMemoryTable with pagination and "show all" page size 1`] = `
-
+
diff --git a/src/components/basic_table/_basic_table.scss b/src/components/basic_table/_basic_table.scss
deleted file mode 100644
index 8e8203ccb47..00000000000
--- a/src/components/basic_table/_basic_table.scss
+++ /dev/null
@@ -1,41 +0,0 @@
-.euiBasicTable {
- &-loading {
- position: relative;
-
- tbody {
- overflow: hidden;
- }
-
- tbody::before {
- position: absolute;
- content: '';
- width: 100%;
- height: $euiBorderWidthThick;
- background-color: $euiColorPrimary;
- animation: euiBasicTableLoading 1000ms linear;
- animation-iteration-count: infinite;
- }
- }
-}
-
-@keyframes euiBasicTableLoading {
- from {
- left: 0;
- width: 0;
- }
-
- 20% {
- left: 0;
- width: 40%;
- }
-
- 80% {
- left: 60%;
- width: 40%;
- }
-
- 100% {
- left: 100%;
- width: 0;
- }
-}
diff --git a/src/components/basic_table/_index.scss b/src/components/basic_table/_index.scss
deleted file mode 100644
index ca3dba62103..00000000000
--- a/src/components/basic_table/_index.scss
+++ /dev/null
@@ -1 +0,0 @@
-@import 'basic_table';
diff --git a/src/components/basic_table/basic_table.styles.ts b/src/components/basic_table/basic_table.styles.ts
index 271aea32657..a11071b592a 100644
--- a/src/components/basic_table/basic_table.styles.ts
+++ b/src/components/basic_table/basic_table.styles.ts
@@ -6,7 +6,57 @@
* Side Public License, v 1.
*/
-import { css } from '@emotion/react';
+import { css, keyframes } from '@emotion/react';
+
+import { logicalCSS, euiCantAnimate } from '../../global_styling';
+import { UseEuiTheme } from '../../services';
+
+const tableLoadingLine = keyframes`
+ from {
+ ${logicalCSS('left', 0)}
+ ${logicalCSS('width', 0)}
+ }
+
+ 20% {
+ ${logicalCSS('left', 0)}
+ ${logicalCSS('width', '40%')}
+ }
+
+ 80% {
+ ${logicalCSS('left', '60%')}
+ ${logicalCSS('width', '40%')}
+ }
+
+ 100% {
+ ${logicalCSS('left', '100%')}
+ ${logicalCSS('width', 0)}
+ }
+`;
+
+export const euiBasicTableBodyLoading = ({ euiTheme }: UseEuiTheme) => css`
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ position: absolute;
+ content: '';
+ ${logicalCSS('width', '100%')}
+ ${logicalCSS('height', euiTheme.border.width.thick)}
+ background-color: ${euiTheme.colors.primary};
+ animation: ${tableLoadingLine} 1s linear infinite;
+
+ ${euiCantAnimate} {
+ animation-duration: 2s;
+ }
+ }
+`;
+
+// Fix to make the loading indicator position correctly in Safari
+// For whatever annoying reason, Safari doesn't respect `position: relative;`
+// on `tbody` without `position: relative` on the parent `table`
+export const safariLoadingWorkaround = () => css`
+ position: relative;
+`;
// Unsets the extra height caused by tooltip/popover wrappers around table action buttons
// Without this, the row height jumps whenever actions are disabled
diff --git a/src/components/basic_table/basic_table.test.tsx b/src/components/basic_table/basic_table.test.tsx
index 3e78dbc59c6..5ab4d7b5e57 100644
--- a/src/components/basic_table/basic_table.test.tsx
+++ b/src/components/basic_table/basic_table.test.tsx
@@ -7,8 +7,9 @@
*/
import React from 'react';
-import { shallow, render } from 'enzyme';
+import { render } from '../../test/rtl';
import { requiredProps } from '../../test';
+import { shouldRenderCustomStyles } from '../../test/internal';
import {
EuiBasicTable,
@@ -47,16 +48,9 @@ interface BasicItem {
id: string;
name: string;
}
-
interface AgeItem extends BasicItem {
age: number;
}
-
-interface CountItem {
- id: string;
- count: number;
-}
-
const basicColumns: Array> = [
{
field: 'name',
@@ -64,74 +58,89 @@ const basicColumns: Array> = [
description: 'description',
},
];
+const basicItems = [
+ { id: '1', name: 'name1' },
+ { id: '2', name: 'name2' },
+ { id: '3', name: 'name3' },
+];
describe('EuiBasicTable', () => {
+ shouldRenderCustomStyles(
+
+ );
+
+ it('renders (bare-bones)', () => {
+ const props = {
+ ...requiredProps,
+ items: basicItems,
+ columns: basicColumns,
+ };
+ const { container } = render( );
+
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
describe('empty', () => {
test('is rendered', () => {
const props = {
- ...requiredProps,
items: [],
columns: basicColumns,
};
- const component = shallow( );
+ const { getByText } = render( );
- expect(component).toMatchSnapshot();
+ expect(getByText('No items found')).toBeTruthy();
});
test('renders a string as a custom message', () => {
const props: EuiBasicTableProps = {
items: [],
- columns: [
- {
- field: 'name',
- name: 'Name',
- description: 'description',
- },
- ],
+ columns: basicColumns,
noItemsMessage: 'where my items at?',
};
- const component = shallow( );
+ const { getByText } = render( );
- expect(component).toMatchSnapshot();
+ expect(getByText('where my items at?')).toBeTruthy();
});
test('renders a node as a custom message', () => {
const props: EuiBasicTableProps = {
items: [],
- columns: [
- {
- field: 'name',
- name: 'Name',
- description: 'description',
- },
- ],
+ columns: basicColumns,
noItemsMessage: (
no items, click here to make some
),
};
- const component = shallow( );
+ const { getByRole } = render( );
- expect(component).toMatchSnapshot();
+ expect(getByRole('link')).toBeTruthy();
});
});
+ test('loading', () => {
+ const props = {
+ items: basicItems,
+ columns: basicColumns,
+ loading: true,
+ };
+ const { container } = render( );
+
+ expect(container.querySelector('.euiBasicTable-loading')).toBeTruthy(); // Used by several Kibana tests as an assertion
+ expect(container.querySelector('tbody')?.className).toContain(
+ 'euiBasicTableBodyLoading'
+ );
+ // Hopefully one day we can delete this when Safari gets its act together
+ expect(container.querySelector('table')?.className).toContain(
+ 'safariLoadingWorkaround'
+ );
+ });
+
describe('rowProps', () => {
test('renders rows with custom props from a callback', () => {
const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
- ],
- columns: [
- {
- field: 'name',
- name: 'Name',
- description: 'description',
- },
- ],
+ items: basicItems,
+ columns: basicColumns,
rowProps: (item) => {
const { id } = item;
return {
@@ -141,52 +150,36 @@ describe('EuiBasicTable', () => {
};
},
};
- const component = shallow( {...props} />);
+ const { getByTestSubject } = render(
+ {...props} />
+ );
- expect(component).toMatchSnapshot();
+ expect(getByTestSubject('row-1')).toBeTruthy();
+ expect(getByTestSubject('row-2')).toBeTruthy();
+ expect(getByTestSubject('row-3')).toBeTruthy();
});
test('renders rows with custom props from an object', () => {
const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
- ],
- columns: [
- {
- field: 'name',
- name: 'Name',
- description: 'description',
- },
- ],
+ items: basicItems,
+ columns: basicColumns,
rowProps: {
'data-test-subj': 'row',
className: 'customClass',
onClick: () => {},
},
};
- const component = shallow( );
+ const { getAllByTestSubject } = render( );
- expect(component).toMatchSnapshot();
+ expect(getAllByTestSubject('row')).toHaveLength(3);
});
});
describe('cellProps', () => {
test('renders cells with custom props from a callback', () => {
const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
- ],
- columns: [
- {
- field: 'name',
- name: 'Name',
- description: 'description',
- },
- ],
+ items: basicItems,
+ columns: basicColumns,
cellProps: (item, column) => {
const { id } = item;
const { field } = column as EuiTableFieldDataColumnType;
@@ -197,76 +190,48 @@ describe('EuiBasicTable', () => {
};
},
};
- const component = shallow( );
+ const { getByTestSubject } = render( );
- expect(component).toMatchSnapshot();
+ expect(getByTestSubject('cell-1-name')).toBeTruthy();
+ expect(getByTestSubject('cell-2-name')).toBeTruthy();
+ expect(getByTestSubject('cell-3-name')).toBeTruthy();
});
- test('renders rows with custom props from an object', () => {
+ test('renders cells with custom props from an object', () => {
const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
- ],
- columns: [
- {
- field: 'name',
- name: 'Name',
- description: 'description',
- },
- ],
+ items: basicItems,
+ columns: basicColumns,
cellProps: {
'data-test-subj': 'cell',
className: 'customClass',
onClick: () => {},
},
};
- const component = shallow( );
+ const { getAllByTestSubject } = render( );
- expect(component).toMatchSnapshot();
+ expect(getAllByTestSubject('cell')).toHaveLength(3);
});
});
test('itemIdToExpandedRowMap renders an expanded row', () => {
const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
- ],
+ items: basicItems,
+ columns: basicColumns,
itemId: 'id',
- columns: [
- {
- field: 'name',
- name: 'Name',
- description: 'description',
- },
- ],
itemIdToExpandedRowMap: {
'1': Expanded row
,
},
- onChange: () => {},
+ isExpandable: true,
};
- const component = shallow( );
+ const { getByText } = render( );
- expect(component).toMatchSnapshot();
+ expect(getByText('Expanded row')).toBeTruthy();
});
test('with pagination', () => {
const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
- ],
- columns: [
- {
- field: 'name',
- name: 'Name',
- description: 'description',
- },
- ],
+ items: basicItems,
+ columns: basicColumns,
pagination: {
pageIndex: 0,
pageSize: 3,
@@ -274,24 +239,18 @@ describe('EuiBasicTable', () => {
},
onChange: () => {},
};
- const component = shallow( );
+ const { container, getByRole } = render( );
- expect(component).toMatchSnapshot();
+ expect(getByRole('list')).toBeTruthy();
+ expect(
+ container.querySelector('[aria-current="true"]')?.textContent
+ ).toEqual('1');
});
test('with pagination - 2nd page', () => {
const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- ],
- columns: [
- {
- field: 'name',
- name: 'Name',
- description: 'description',
- },
- ],
+ items: basicItems,
+ columns: basicColumns,
pagination: {
pageIndex: 1,
pageSize: 3,
@@ -299,24 +258,17 @@ describe('EuiBasicTable', () => {
},
onChange: () => {},
};
- const component = shallow( );
+ const { container } = render( );
- expect(component).toMatchSnapshot();
+ expect(
+ container.querySelector('[aria-current="true"]')?.textContent
+ ).toEqual('2');
});
test('with pagination - show all', () => {
const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- ],
- columns: [
- {
- field: 'name',
- name: 'Name',
- description: 'description',
- },
- ],
+ items: basicItems,
+ columns: basicColumns,
pagination: {
pageIndex: 0,
pageSize: 0,
@@ -325,52 +277,40 @@ describe('EuiBasicTable', () => {
},
onChange: () => {},
};
- const component = shallow( );
+ const { getByTestSubject, getByText } = render(
+
+ );
- expect(component).toMatchSnapshot();
+ expect(getByTestSubject('tablePaginationPopoverButton')).toBeTruthy();
+ expect(getByText('Showing all rows')).toBeTruthy();
});
- test('with pagination and error', () => {
+ it('does not show pagination bar if there is an error', () => {
const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
- ],
- columns: [
- {
- field: 'name',
- name: 'Name',
- description: 'description',
- },
- ],
+ items: basicItems,
+ columns: basicColumns,
pagination: {
pageIndex: 0,
pageSize: 3,
+ pageSizeOptions: [1, 5, 0],
totalItemCount: 5,
},
onChange: () => {},
error: 'no can do',
};
- const component = shallow( );
+ const { getByText, queryByTestSubject, queryByRole } = render(
+
+ );
- expect(component).toMatchSnapshot();
+ expect(getByText('no can do')).toBeTruthy();
+ expect(queryByTestSubject('tablePaginationPopoverButton')).toBeFalsy();
+ expect(queryByRole('list')).toBeFalsy();
});
test('with pagination, hiding the per page options', () => {
const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
- ],
- columns: [
- {
- field: 'name',
- name: 'Name',
- description: 'description',
- },
- ],
+ items: basicItems,
+ columns: basicColumns,
pagination: {
pageIndex: 0,
pageSize: 3,
@@ -379,23 +319,18 @@ describe('EuiBasicTable', () => {
},
onChange: () => {},
};
- const component = shallow( );
+ const { queryByTestSubject } = render( );
- expect(component).toMatchSnapshot();
+ expect(queryByTestSubject('tablePaginationPopoverButton')).toBeFalsy();
});
test('with sorting', () => {
const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
- ],
+ items: basicItems,
columns: [
{
field: 'name',
name: 'Name',
- description: 'description',
sortable: true,
},
],
@@ -404,18 +339,19 @@ describe('EuiBasicTable', () => {
},
onChange: () => {},
};
- const component = shallow( );
+ const { container, getByTestSubject } = render(
+
+ );
- expect(component).toMatchSnapshot();
+ expect(getByTestSubject('tableHeaderSortButton')).toBeTruthy();
+ expect(
+ container.querySelector('[aria-sort="ascending"]')?.textContent
+ ).toEqual('Name');
});
test('with sortable columns and sorting disabled', () => {
const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
- ],
+ items: basicItems,
columns: [
{
field: 'name',
@@ -426,25 +362,18 @@ describe('EuiBasicTable', () => {
],
onChange: () => {},
};
- const component = shallow( );
+ const { container, queryByTestSubject } = render(
+
+ );
- expect(component).toMatchSnapshot();
+ expect(queryByTestSubject('tableHeaderSortButton')).toBeFalsy();
+ expect(container.querySelector('[aria-sort]')).toBeFalsy();
});
test('with sorting enabled and enable all columns for sorting', () => {
const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
- ],
- columns: [
- {
- field: 'name',
- name: 'Name',
- description: 'description',
- },
- ],
+ items: basicItems,
+ columns: basicColumns,
sorting: {
sort: {
field: 'name',
@@ -454,355 +383,295 @@ describe('EuiBasicTable', () => {
},
onChange: () => {},
};
- const component = shallow( );
+ const { container, getByTestSubject } = render(
+
+ );
- expect(component).toMatchSnapshot();
+ expect(getByTestSubject('tableHeaderSortButton')).toBeTruthy();
+ expect(container.querySelector('[aria-sort]')).toBeTruthy();
});
test('with initial selection', () => {
const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
- ],
+ items: basicItems,
+ columns: basicColumns,
itemId: 'id',
- columns: [
- {
- field: 'name',
- name: 'Name',
- description: 'description',
- },
- ],
selection: {
- onSelectionChange: () => undefined,
- initialSelected: [{ id: '1', name: 'name1' }],
+ onSelectionChange: () => {},
+ initialSelected: [basicItems[0]],
},
};
- const component = render( );
+ const { getByTestSubject } = render( );
- expect(component).toMatchSnapshot();
+ expect(
+ (getByTestSubject('checkboxSelectRow-1') as HTMLInputElement).checked
+ ).toBeTruthy();
+ expect(
+ (getByTestSubject('checkboxSelectRow-2') as HTMLInputElement).checked
+ ).toBeFalsy();
});
- test('with pagination and selection', () => {
- const props: EuiBasicTableProps = {
+ test('footers', () => {
+ const props: EuiBasicTableProps = {
items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
+ { id: '1', name: 'name1', age: 20 },
+ { id: '2', name: 'name2', age: 21 },
+ { id: '3', name: 'name3', age: 22 },
],
itemId: 'id',
columns: [
{
field: 'name',
name: 'Name',
- description: 'description',
+ description: 'your name',
+ // No footer
+ },
+ {
+ field: 'id',
+ name: 'ID',
+ description: 'your id',
+ footer: 'Total users: 3',
+ },
+ {
+ field: 'age',
+ name: 'Age',
+ description: 'your age',
+ footer: ({ items }) => (
+ <>
+
+ Total ages: {items.reduce((acc, cur) => acc + cur.age, 0)}
+
+
+ Total items: {items.length}
+ >
+ ),
},
],
- pagination: {
- pageIndex: 0,
- pageSize: 3,
- totalItemCount: 5,
- },
- selection: {
- onSelectionChange: () => undefined,
- },
onChange: () => {},
};
- const component = shallow( );
+ const { getByText } = render( );
- expect(component).toMatchSnapshot();
+ expect(getByText('Total users: 3')).toBeTruthy();
+ expect(getByText('Total ages: 63')).toBeTruthy();
+ expect(getByText('Total items: 3')).toBeTruthy();
});
- test('with pagination, selection and sorting', () => {
+ test('column renderer', () => {
const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
- ],
- itemId: 'id',
+ items: basicItems,
columns: [
{
field: 'name',
name: 'Name',
description: 'description',
- sortable: true,
+ render: (name: string) => name.toUpperCase(),
},
],
- pagination: {
- pageIndex: 0,
- pageSize: 3,
- totalItemCount: 5,
- },
- selection: {
- onSelectionChange: () => undefined,
- },
- sorting: {
- sort: { field: 'name', direction: SortDirection.ASC },
- },
- onChange: () => {},
};
- const component = shallow( );
+ const { getByText } = render( );
- expect(component).toMatchSnapshot();
+ expect(getByText('NAME1')).toBeTruthy();
+ expect(getByText('NAME2')).toBeTruthy();
+ expect(getByText('NAME3')).toBeTruthy();
});
- describe('footers', () => {
- test('do not render without a column footer definition', () => {
- const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1', age: 20 },
- { id: '2', name: 'name2', age: 21 },
- { id: '3', name: 'name3', age: 22 },
- ],
- itemId: 'id',
+ describe('column dataType', () => {
+ interface DataTypeItem {
+ id: string;
+ count: number;
+ online: boolean;
+ date: Date;
+ }
+ const dataTypeItems = [
+ { id: '1', count: 1, online: true, date: new Date('1/1/1970') },
+ { id: '2', count: 2, online: false, date: new Date('2/2/1971') },
+ ];
+
+ test('number, boolean, and date types', () => {
+ const props: EuiBasicTableProps = {
+ items: dataTypeItems,
columns: [
{
- field: 'name',
- name: 'Name',
- description: 'your name',
+ field: 'age',
+ name: 'Count',
+ dataType: 'number',
},
{
- field: 'id',
- name: 'ID',
- description: 'your id',
+ field: 'online',
+ name: 'Status',
+ dataType: 'boolean',
},
{
- field: 'age',
- name: 'Age',
- description: 'your age',
+ field: 'date',
+ name: 'Date',
+ dataType: 'date',
},
],
- onChange: () => {},
};
- const component = shallow( );
+ const { container, getByText } = render( );
- expect(component).toMatchSnapshot();
+ // Numbers should be right aligned
+ expect(
+ container.querySelectorAll('.euiTableCellContent--alignRight')
+ ).toHaveLength(3);
+
+ // Booleans should output as Yes or No
+ expect(getByText('Yes')).toBeTruthy();
+ expect(getByText('No')).toBeTruthy();
+
+ // Dates should auto format
+ expect(getByText('1 Jan 1970 00:00')).toBeTruthy();
+ expect(getByText('2 Feb 1971 00:00')).toBeTruthy();
});
- test('render with pagination, selection, sorting, and footer', () => {
- const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1', age: 20 },
- { id: '2', name: 'name2', age: 21 },
- { id: '3', name: 'name3', age: 22 },
- ],
- itemId: 'id',
+ test('column renderer takes precedence over column data type', () => {
+ const props: EuiBasicTableProps = {
+ items: dataTypeItems,
columns: [
{
- field: 'name',
- name: 'Name',
- description: 'your name',
- sortable: true,
- footer: Name ,
+ field: 'online',
+ name: 'Status',
+ dataType: 'boolean',
+ render: (online: boolean) => (online ? 'Online' : 'Offline'),
},
{
- field: 'id',
- name: 'ID',
- description: 'your id',
- footer: 'ID',
- },
- {
- field: 'age',
- name: 'Age',
- description: 'your age',
- footer: ({ items, pagination }) => (
-
- sum:
- {items.reduce((acc, cur) => acc + cur.age, 0)}
-
- total items:
- {pagination!.totalItemCount}
-
- ),
+ field: 'date',
+ name: 'Date',
+ dataType: 'date',
+ render: (date: Date) => date.getFullYear(),
},
],
- pagination: {
- pageIndex: 0,
- pageSize: 3,
- totalItemCount: 5,
- },
- selection: {
- onSelectionChange: () => undefined,
- },
- sorting: {
- sort: { field: 'name', direction: SortDirection.ASC },
- },
- onChange: () => {},
};
- const component = shallow( );
+ const { queryByText } = render( );
- expect(component).toMatchSnapshot();
- });
- });
+ expect(queryByText('Yes')).toBeFalsy();
+ expect(queryByText('Online')).toBeTruthy();
- test('with pagination, selection, sorting and column renderer', () => {
- const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
- ],
- itemId: 'id',
- columns: [
- {
- field: 'name',
- name: 'Name',
- description: 'description',
- sortable: true,
- render: (name: string) => name.toUpperCase(),
- },
- ],
- pagination: {
- pageIndex: 0,
- pageSize: 3,
- totalItemCount: 5,
- },
- selection: {
- onSelectionChange: () => undefined,
- },
- sorting: {
- sort: { field: 'name', direction: SortDirection.ASC },
- },
- onChange: () => {},
- };
- const component = shallow( );
+ expect(queryByText('No')).toBeFalsy();
+ expect(queryByText('Offline')).toBeTruthy();
- expect(component).toMatchSnapshot();
+ expect(queryByText('1970')).toBeTruthy();
+ expect(queryByText('1971')).toBeTruthy();
+ });
});
- test('with pagination, selection, sorting and column dataType', () => {
- const props: EuiBasicTableProps = {
- items: [
- { id: '1', count: 1 },
- { id: '2', count: 2 },
- { id: '3', count: 3 },
- ],
- itemId: 'id',
- columns: [
- {
- field: 'count',
- name: 'Count',
- description: 'description of count',
- sortable: true,
- dataType: 'number',
- },
- ],
- pagination: {
- pageIndex: 0,
- pageSize: 3,
- totalItemCount: 5,
- },
- selection: {
- onSelectionChange: () => undefined,
- },
- sorting: {
- sort: { field: 'count', direction: SortDirection.ASC },
- },
- onChange: () => {},
- };
- const component = shallow( );
-
- expect(component).toMatchSnapshot();
- });
+ describe('actions', () => {
+ test('single action', () => {
+ const props: EuiBasicTableProps = {
+ items: basicItems,
+ columns: [
+ ...basicColumns,
+ {
+ name: 'Actions',
+ actions: [
+ {
+ type: 'button',
+ name: 'Edit',
+ description: 'edit',
+ onClick: () => {},
+ },
+ ],
+ },
+ ],
+ };
+ const { getAllByText } = render( );
- // here we want to verify that the column renderer takes precedence over the column data type
- test('with pagination, selection, sorting, column renderer and column dataType', () => {
- const props: EuiBasicTableProps = {
- items: [
- { id: '1', count: 1 },
- { id: '2', count: 2 },
- { id: '3', count: 3 },
- ],
- itemId: 'id',
- columns: [
- {
- field: 'count',
- name: 'Count',
- description: 'description of count',
- sortable: true,
- dataType: 'number',
- render: (count: number) => 'x'.repeat(count),
- },
- ],
- pagination: {
- pageIndex: 0,
- pageSize: 3,
- totalItemCount: 5,
- },
- selection: {
- onSelectionChange: () => undefined,
- },
- sorting: {
- sort: { field: 'count', direction: SortDirection.ASC },
- },
- onChange: () => {},
- };
- const component = shallow( );
+ expect(getAllByText('Edit')).toHaveLength(basicItems.length);
+ });
- expect(component).toMatchSnapshot();
+ test('multiple actions with custom availability', () => {
+ const props: EuiBasicTableProps = {
+ items: [...basicItems, { id: '4', name: 'name4' }],
+ columns: [
+ ...basicColumns,
+ {
+ name: 'Actions',
+ actions: [
+ {
+ type: 'icon',
+ name: 'Edit',
+ isPrimary: true,
+ icon: 'pencil',
+ available: ({ id }) => !(Number(id) % 2),
+ description: 'edit',
+ onClick: () => {},
+ },
+ {
+ type: 'icon',
+ name: 'Share',
+ icon: 'share',
+ isPrimary: true,
+ available: ({ id }) => id !== '3',
+ description: 'share',
+ onClick: () => {},
+ },
+ // Below actions are not primary and should be hidden behind collapse button
+ {
+ type: 'icon',
+ name: 'Copy',
+ icon: 'copy',
+ description: 'copy',
+ onClick: () => {},
+ },
+ {
+ type: 'icon',
+ name: 'Delete',
+ icon: 'trash',
+ description: 'delete',
+ onClick: () => {},
+ },
+ {
+ type: 'icon',
+ name: 'elastic.co',
+ icon: 'link',
+ description: 'Go to link',
+ onClick: () => {},
+ },
+ ],
+ },
+ ],
+ };
+ const { getAllByText, getAllByTestSubject } = render(
+
+ );
+
+ expect(getAllByText('Edit')).toHaveLength(2);
+ expect(getAllByText('Share')).toHaveLength(3);
+ expect(getAllByTestSubject('euiCollapsedItemActionsButton')).toHaveLength(
+ 4
+ );
+ });
});
- test('with pagination, selection, sorting and a single record action', () => {
- const props: EuiBasicTableProps = {
+ it('renders (kitchen sink) with pagination, selection, sorting, actions, and footer', () => {
+ const props: EuiBasicTableProps = {
items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
+ { id: '1', name: 'name1', age: 20 },
+ { id: '2', name: 'name2', age: 21 },
+ { id: '3', name: 'name3', age: 22 },
],
itemId: 'id',
columns: [
{
field: 'name',
name: 'Name',
- description: 'description',
+ description: 'your name',
sortable: true,
+ render: (name: string) => name.toUpperCase(),
},
{
- name: 'Actions',
- actions: [
- {
- type: 'button',
- name: 'Edit',
- description: 'edit',
- onClick: () => undefined,
- },
- ],
+ field: 'id',
+ name: 'ID',
+ description: 'your id',
+ footer: ({ pagination }) => (
+ Total items: {pagination!.totalItemCount}
+ ),
},
- ],
- pagination: {
- pageIndex: 0,
- pageSize: 3,
- totalItemCount: 5,
- },
- selection: {
- onSelectionChange: () => undefined,
- },
- sorting: {
- sort: { field: 'name', direction: SortDirection.ASC },
- },
- onChange: () => {},
- };
- const component = shallow( );
-
- expect(component).toMatchSnapshot();
- });
-
- test('with pagination, selection, sorting and multiple record actions', () => {
- const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
- ],
- itemId: 'id',
- columns: [
{
- field: 'name',
- name: 'Name',
- description: 'description',
- sortable: true,
+ field: 'age',
+ name: 'Age',
+ description: 'your age',
+ dataType: 'number',
},
{
name: 'Actions',
@@ -811,13 +680,13 @@ describe('EuiBasicTable', () => {
type: 'button',
name: 'Edit',
description: 'edit',
- onClick: () => undefined,
+ onClick: () => {},
},
{
type: 'button',
name: 'Delete',
description: 'delete',
- onClick: () => undefined,
+ onClick: () => {},
},
],
},
@@ -828,76 +697,15 @@ describe('EuiBasicTable', () => {
totalItemCount: 5,
},
selection: {
- onSelectionChange: () => undefined,
+ onSelectionChange: () => {},
},
sorting: {
sort: { field: 'name', direction: SortDirection.ASC },
},
onChange: () => {},
};
- const component = shallow( );
-
- expect(component).toMatchSnapshot();
- });
-
- test('with multiple record actions with custom availability', () => {
- const props: EuiBasicTableProps = {
- items: [
- { id: '1', name: 'name1' },
- { id: '2', name: 'name2' },
- { id: '3', name: 'name3' },
- { id: '4', name: 'name3' },
- ],
- itemId: 'id',
- columns: [
- {
- field: 'name',
- name: 'Name',
- description: 'description',
- },
- {
- name: 'Actions',
- actions: [
- {
- type: 'icon',
- name: 'Edit',
- isPrimary: true,
- icon: 'pencil',
- available: ({ id }) => !(Number(id) % 2),
- description: 'edit',
- onClick: () => undefined,
- },
- {
- type: 'icon',
- name: 'Copy',
- isPrimary: true,
- icon: 'copy',
- description: 'copy',
- onClick: () => undefined,
- },
- {
- type: 'icon',
- name: 'Delete',
- isPrimary: true,
- icon: 'trash',
- description: 'delete',
- onClick: () => undefined,
- },
- {
- type: 'icon',
- name: 'Share',
- icon: 'trash',
- available: ({ id }) => id !== '3',
- description: 'share',
- onClick: () => undefined,
- },
- ],
- },
- ],
- onChange: () => {},
- };
- const component = shallow( );
+ const { container } = render( );
- expect(component).toMatchSnapshot();
+ expect(container.firstChild).toMatchSnapshot();
});
});
diff --git a/src/components/basic_table/basic_table.tsx b/src/components/basic_table/basic_table.tsx
index f13f63bac11..9ba13b002dd 100644
--- a/src/components/basic_table/basic_table.tsx
+++ b/src/components/basic_table/basic_table.tsx
@@ -19,6 +19,7 @@ import {
LEFT_ALIGNMENT,
RIGHT_ALIGNMENT,
SortDirection,
+ RenderWithEuiTheme,
} from '../../services';
import { CommonProps } from '../common';
import { isFunction } from '../../services/predicate';
@@ -66,7 +67,11 @@ import {
} from './table_types';
import { EuiTableSortMobileProps } from '../table/mobile/table_sort_mobile';
-import { euiBasicTableActionsWrapper } from './basic_table.styles';
+import {
+ euiBasicTableBodyLoading,
+ safariLoadingWorkaround,
+ euiBasicTableActionsWrapper,
+} from './basic_table.styles';
type DataTypeProfiles = Record<
EuiTableDataType,
@@ -559,9 +564,7 @@ export class EuiBasicTable extends Component<
const classes = classNames(
'euiBasicTable',
- {
- 'euiBasicTable-loading': loading,
- },
+ { 'euiBasicTable-loading': loading },
className
);
@@ -577,7 +580,7 @@ export class EuiBasicTable extends Component<
}
renderTable() {
- const { compressed, responsive, tableLayout } = this.props;
+ const { compressed, responsive, tableLayout, loading } = this.props;
const mobileHeader = responsive ? (
@@ -603,6 +606,7 @@ export class EuiBasicTable extends Component<
tableLayout={tableLayout}
responsive={responsive}
compressed={compressed}
+ css={loading && safariLoadingWorkaround}
>
{caption}
{head}
@@ -942,58 +946,68 @@ export class EuiBasicTable extends Component<
}
renderTableBody() {
- if (this.props.error) {
- return this.renderErrorBody(this.props.error);
- }
- const { items } = this.props;
- if (items.length === 0) {
- return this.renderEmptyBody();
+ const { error, loading, items } = this.props;
+
+ let content: ReactNode;
+
+ if (error) {
+ content = this.renderErrorMessage(error);
+ } else if (items.length === 0) {
+ content = this.renderEmptyMessage();
+ } else {
+ content = items.map((item: T, index: number) => {
+ // if there's pagination the item's index must be adjusted to the where it is in the whole dataset
+ const tableItemIndex =
+ hasPagination(this.props) && this.props.pagination.pageSize > 0
+ ? this.props.pagination.pageIndex * this.props.pagination.pageSize +
+ index
+ : index;
+ return this.renderItemRow(item, tableItemIndex);
+ });
}
- const rows = items.map((item: T, index: number) => {
- // if there's pagination the item's index must be adjusted to the where it is in the whole dataset
- const tableItemIndex =
- hasPagination(this.props) && this.props.pagination.pageSize > 0
- ? this.props.pagination.pageIndex * this.props.pagination.pageSize +
- index
- : index;
- return this.renderItemRow(item, tableItemIndex);
- });
- return {rows} ;
+ return (
+
+ {(theme) => (
+
+ {content}
+
+ )}
+
+ );
}
- renderErrorBody(error: string) {
+ renderErrorMessage(error: string) {
const colSpan = this.props.columns.length + (this.props.selection ? 1 : 0);
return (
-
-
-
- {error}
-
-
-
+
+
+ {error}
+
+
);
}
- renderEmptyBody() {
+ renderEmptyMessage() {
const { columns, selection, noItemsMessage } = this.props;
const colSpan = columns.length + (selection ? 1 : 0);
return (
-
-
-
- {noItemsMessage}
-
-
-
+
+
+ {noItemsMessage}
+
+
);
}
diff --git a/src/components/index.scss b/src/components/index.scss
index 12c8908501b..f510b37a68d 100644
--- a/src/components/index.scss
+++ b/src/components/index.scss
@@ -1,7 +1,6 @@
// Components
@import 'accordion/index';
-@import 'basic_table/index';
@import 'button/index';
@import 'collapsible_nav/index';
@import 'color_picker/index';
diff --git a/src/services/theme/hooks.test.ts b/src/services/theme/hooks.test.ts
deleted file mode 100644
index 25209afac13..00000000000
--- a/src/services/theme/hooks.test.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * 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 { testCustomHook } from '../../test/internal';
-import { useEuiTheme } from './hooks';
-
-describe('useEuiTheme', () => {
- it('consecutive calls return a stable object', () => {
- const hookResult = testCustomHook(useEuiTheme);
- hookResult.updateHookArgs({});
- expect(hookResult.return).toBe(hookResult.getUpdatedState());
- });
-});
diff --git a/src/services/theme/hooks.test.tsx b/src/services/theme/hooks.test.tsx
new file mode 100644
index 00000000000..ca3a44c785d
--- /dev/null
+++ b/src/services/theme/hooks.test.tsx
@@ -0,0 +1,83 @@
+/*
+ * 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 { renderHook } from '@testing-library/react-hooks';
+import { render } from '@testing-library/react';
+
+import { setEuiDevProviderWarning } from './provider';
+import {
+ useEuiTheme,
+ UseEuiTheme,
+ withEuiTheme,
+ RenderWithEuiTheme,
+} from './hooks';
+
+describe('useEuiTheme', () => {
+ it('returns a context with theme variables, color mode, and modifications', () => {
+ const { result } = renderHook(useEuiTheme);
+ expect(result.current).toEqual({
+ euiTheme: expect.any(Object),
+ colorMode: 'LIGHT',
+ modifications: {},
+ });
+ });
+
+ it('logs, warns, or errors if a provider warning level has been set', () => {
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
+ setEuiDevProviderWarning('warn');
+
+ renderHook(useEuiTheme);
+ expect(warnSpy).toHaveBeenCalledWith(
+ expect.stringContaining('`EuiProvider` is missing')
+ );
+
+ setEuiDevProviderWarning(undefined);
+ warnSpy.mockRestore();
+ });
+
+ it('consecutive calls return a stable object', () => {
+ const { result, rerender } = renderHook(useEuiTheme);
+ expect(result.all.length).toEqual(1);
+ rerender({});
+ expect(result.all.length).toEqual(2);
+
+ expect(result.all[0]).toBe(result.all[1]);
+ });
+});
+
+describe('withEuiTheme', () => {
+ class ClassComponent extends React.Component<{ theme: UseEuiTheme }> {
+ render() {
+ const { theme } = this.props;
+ // output
+ return Object.keys(theme).join();
+ }
+ }
+ const Component = withEuiTheme(ClassComponent);
+
+ it('provides underlying class components with a `theme` prop', () => {
+ const { container } = render( );
+ expect(container.firstChild!.textContent).toEqual(
+ 'euiTheme,colorMode,modifications'
+ );
+ });
+});
+
+describe('RenderWithEuiTheme', () => {
+ it('passes the `theme` arg to children as a render prop', () => {
+ const { container } = render(
+
+ {(theme) => <>{Object.keys(theme).join()}>}
+
+ );
+ expect(container.firstChild!.textContent).toEqual(
+ 'euiTheme,colorMode,modifications'
+ );
+ });
+});
diff --git a/src/services/theme/hooks.tsx b/src/services/theme/hooks.tsx
index 8194c6735dd..f23885726ed 100644
--- a/src/services/theme/hooks.tsx
+++ b/src/services/theme/hooks.tsx
@@ -24,6 +24,9 @@ import {
const providerMessage = `\`EuiProvider\` is missing which can result in negative effects.
Wrap your component in \`EuiProvider\`: https://ela.st/euiprovider.`;
+/**
+ * Hook for function components
+ */
export interface UseEuiTheme {
euiTheme: EuiThemeComputed;
colorMode: EuiThemeColorModeStandard;
@@ -64,6 +67,9 @@ export const useEuiTheme = (): UseEuiTheme => {
return assembledTheme;
};
+/**
+ * HOC for class components
+ */
export interface WithEuiThemeProps {
theme: UseEuiTheme
;
}
@@ -88,3 +94,16 @@ export const withEuiTheme = (
return WithEuiTheme;
};
+
+/**
+ * Render prop alternative for complex class components
+ * Most useful for scenarios where a HOC may interfere with typing
+ */
+export const RenderWithEuiTheme = ({
+ children,
+}: {
+ children: (theme: UseEuiTheme) => React.ReactElement;
+}) => {
+ const theme = useEuiTheme();
+ return children(theme);
+};
diff --git a/src/services/theme/index.ts b/src/services/theme/index.ts
index f2e2a366911..4c7bf67a645 100644
--- a/src/services/theme/index.ts
+++ b/src/services/theme/index.ts
@@ -13,7 +13,7 @@ export {
EuiColorModeContext,
} from './context';
export type { UseEuiTheme, WithEuiThemeProps } from './hooks';
-export { useEuiTheme, withEuiTheme } from './hooks';
+export { useEuiTheme, withEuiTheme, RenderWithEuiTheme } from './hooks';
export type { EuiThemeProviderProps } from './provider';
export {
EuiThemeProvider,
diff --git a/src/services/theme/provider.tsx b/src/services/theme/provider.tsx
index 036eb8c2f6a..42d383e97a5 100644
--- a/src/services/theme/provider.tsx
+++ b/src/services/theme/provider.tsx
@@ -31,7 +31,7 @@ import {
type LEVELS = 'log' | 'warn' | 'error';
let providerWarning: LEVELS | undefined = undefined;
-export const setEuiDevProviderWarning = (level: LEVELS) =>
+export const setEuiDevProviderWarning = (level: LEVELS | undefined) =>
(providerWarning = level);
export const getEuiDevProviderWarning = () => providerWarning;
diff --git a/upcoming_changelogs/6539.md b/upcoming_changelogs/6539.md
new file mode 100644
index 00000000000..766c36155c3
--- /dev/null
+++ b/upcoming_changelogs/6539.md
@@ -0,0 +1,4 @@
+**CSS-in-JS conversions**
+
+- Converted `EuiBasicTable` to Emotion
+- Added a new `RenderWithEuiTheme` render prop utility