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`] = `
- - - - - - - - - - - - - - - - - Name - - - - - - name1 - - - - - name2 - - - - - name3 - - - - -
-
-`; - -exports[`EuiBasicTable cellProps renders rows with custom props from an object 1`] = ` -
-
- - - - - - - +
+ - - + + + + + - - - name1 - - - + Name + +
+ + name1 + +
+ + +
- - name2 - - - + Name + +
+ + name2 + +
+ +
+ - - name3 - - - - +
+ Name +
+
+ + name3 + +
+ + + +
- +
- - - - - - - Name - - - + + Name + + + description + + +
`; -exports[`EuiBasicTable empty is rendered 1`] = ` +exports[`EuiBasicTable renders (kitchen sink) with pagination, selection, sorting, actions, and footer 1`] = `
- - - - - - - - - - - - - - - - - Name - - - - - - - - - - -
-
-`; - -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`] = ` -
-
- - - - - - - +
+ +
+
+
+ + + + - - - - - - Name - - - ID - - - Age - - - - - + + + + - - - - - - - - - Name - - - ID - - - Age - - - - - - - - - - - name1 - - - 1 - - - 20 - - - - - - - - - - name2 - - - 2 - - - 21 - - - - - - - - - - name3 - - - 3 - - - 22 - - - - - - - - Name - - - - ID - - - - sum: - 63 -
- total items: - 5 -
-
-
- - - - - - -`; - -exports[`EuiBasicTable itemIdToExpandedRowMap renders an expanded row 1`] = ` -
-
- - - - - - - - -
- - - - Name - - - - - - name1 - - - - -
- Expanded row -
-
-
- - - name2 - - - - - name3 - - -
- - - -`; - -exports[`EuiBasicTable rowProps renders rows with custom props from a callback 1`] = ` -
-
- - - - - - - - -
- - - - Name - - - - - - name1 - - - - - name2 - - - - - name3 - - - - - - -`; - -exports[`EuiBasicTable rowProps renders rows with custom props from an object 1`] = ` -
-
- - - - - - - - -
- - - - Name - - - - - - name1 - - - - - name2 - - - - - name3 - - - - - - -`; - -exports[`EuiBasicTable with initial selection 1`] = ` -
-
-
-
-
-
- -
- -
-
-
-
-
-
- - - -
- name1 - - +
+ +
+
+
+
- 1 - - - 20 - - - - - name2 - - - 2 - - - 21 - - - - - name3 - - - 3 - - - 22 - - - - - - -`; - -exports[`EuiBasicTable footers render with pagination, selection, sorting, and footer 1`] = ` -
-
- - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - -
- - + + + Name + + + your name + + + + + + - + + - - + + + + + + + + - - -
-
-
-
- -
-
-
+ ID + + + your id + +
- Name + Age - description + your age + + + + + + Actions
Name +
+ NAME1 +
+
+
+ ID +
- name1 + 1
-
+
+ - +
+
+
+ + + + + + +
+
+
+
+ @@ -1579,19 +488,95 @@ exports[`EuiBasicTable with initial selection 1`] = ` > Name
+
+ NAME2 +
+
+
+ ID +
- name2 + 2 + +
+
+
+ Age +
+
+ + 21 + +
+
+
+ + + + +
Name +
+ NAME3 +
+
+
+ ID +
- name3 + 3
- - -`; - -exports[`EuiBasicTable with multiple record actions with custom availability 1`] = ` -
-
- - - - - - - - - - - - - - - - - Name - - - Actions - - - - - - name1 - - - - - - - - name2 - - - - - - - - name3 - - + Age +
+
+ + 22 + +
+ + - - - - - + + + + + + +
+ + + + + + - name3 - - + + + + - - - - - - - -`; - -exports[`EuiBasicTable with pagination - 2nd page 1`] = ` -
-
- - - - - - - - - - + +
+ + - - - - - - - Name - - - - - + + + Total items: + 5 + + +
+ + - name1 - - - - + + + + - name2 - - - - +
+ +
+ + + + - - - - -`; - -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