diff --git a/.all-contributorsrc b/.all-contributorsrc index 69787efcd674..5714f71cb033 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1041,6 +1041,15 @@ "code" ] }, + { + "login": "jsehull", + "name": "Jesse Hull", + "avatar_url": "https://avatars.githubusercontent.com/u/9935383?v=4", + "profile": "https://jsehull.com/", + "contributions": [ + "code" + ] + }, { "login": "awarrier99", "name": "Ashvin Warrier", diff --git a/README.md b/README.md index 61e17f1261e7..0d047e3f49e5 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,7 @@ check out our [Contributing Guide](/.github/CONTRIBUTING.md) and our
Marcin Lewandowski

💻
remolueoend

💻 +
Jesse Hull

💻
Ashvin Warrier

💻 diff --git a/docs/guides/adding-component-types.md b/docs/guides/adding-component-types.md index 3ad3696d5c8b..8f93ad5a9d83 100644 --- a/docs/guides/adding-component-types.md +++ b/docs/guides/adding-component-types.md @@ -37,7 +37,7 @@ By adding TypeScript types to components we anticipate a number of benefits: - Developer productivity will increase due to Component API's being self-documenting and providing tight integration with code editor intellisense. -- The qualtiy of products developed will increase due to more stable, correct, +- The quality of products developed will increase due to more stable, correct, and thorough component API typings provided first-party through `@carbon/react` itself. - Maintenance of the types themselves will be simplified by not having to go @@ -159,5 +159,5 @@ the repository. There is an issue tracking curring status of this effort, ### Where should I put the ts interface in the file? - Above the component definition (likely the top of the file) - - The component implementation should be sandwiched inbetween the ts interface - and the proptypes definition + - The component implementation should be sandwiched in-between the ts + interface and the proptypes definition diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 8197fadb82cd..f56ee3089ede 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -114,9 +114,6 @@ Map { "ariaLabel": Object { "type": "string", }, - "caption": Object { - "type": "string", - }, "children": Object { "type": "node", }, diff --git a/packages/react/src/components/ContextMenu/useContextMenu.js b/packages/react/src/components/ContextMenu/useContextMenu.js index 972d1257819f..91936a8d49d1 100644 --- a/packages/react/src/components/ContextMenu/useContextMenu.js +++ b/packages/react/src/components/ContextMenu/useContextMenu.js @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; /** - * @param {Element|Document|Window} [trigger=document] The element which should trigger the Menu on right-click + * @param {Element|Document|Window|object} [trigger=document] The element or ref which should trigger the Menu on right-click * @returns {object} Props object to pass onto Menu component */ function useContextMenu(trigger = document) { @@ -22,15 +22,17 @@ function useContextMenu(trigger = document) { } useEffect(() => { + const el = trigger?.current ?? trigger; + if ( - (trigger && trigger instanceof Element) || - trigger instanceof Document || - trigger instanceof Window + (el && el instanceof Element) || + el instanceof Document || + el instanceof Window ) { - trigger.addEventListener('contextmenu', openContextMenu); + el.addEventListener('contextmenu', openContextMenu); return () => { - trigger.removeEventListener('contextmenu', openContextMenu); + el.removeEventListener('contextmenu', openContextMenu); }; } }, [trigger]); diff --git a/packages/react/src/components/DataTable/__tests__/TableExpandHeader-test.js b/packages/react/src/components/DataTable/__tests__/TableExpandHeader-test.js index af83857a558d..04edba9c430f 100644 --- a/packages/react/src/components/DataTable/__tests__/TableExpandHeader-test.js +++ b/packages/react/src/components/DataTable/__tests__/TableExpandHeader-test.js @@ -6,7 +6,6 @@ */ import React, { useState, useEffect } from 'react'; -import { mount } from 'enzyme'; import DataTable, { Table, TableHead, @@ -21,202 +20,286 @@ import DataTable, { } from '../'; import { Pagination } from '../../../'; import { rows, headers } from '../stories/shared'; -import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; - -describe('DataTable.TableExpandHeader', () => { - it('should render', () => { - const wrapper = mount( - - - - - - -
- ); - expect(wrapper).toMatchSnapshot(); - }); -}); +import { render, screen } from '@testing-library/react'; describe('TableExpandHeader', () => { - it('should call onExpand', () => { - const onExpand = jest.fn(); - render( - - {({ - headers, - getTableProps, - getHeaderProps, - getExpandHeaderProps, - getTableContainerProps, - }) => ( - - - - - - {headers.map((header, i) => ( - - {header.header} - - ))} - - -
-
- )} -
- ); - - userEvent.click(screen.getByRole('button')); - - expect(onExpand).toHaveBeenCalled(); - }); + describe('renders as expected - Component API', () => { + it('should render', () => { + const { container } = render( + + + + + + +
+ ); + expect(container).toMatchSnapshot(); + }); - it('should update toggle button', () => { - const PaginationExample = () => { - const [rows, setRows] = useState([]); - const headers = [ - { - key: 'value', - header: 'Value', - }, - { - key: 'timestamp', - header: 'Submitted At', - }, - ]; - - const allRows = [ - { - id: 'f66f9f0e293e622b046ab4826f99d071a377418fd69bf1685c8d23c371f517cc', - value: 'First', - timestamp: '2022-06-06T12:57:27', - }, - { - id: 'd0d95500fccef68dd1e7cb36f381984d340e9a81657b00e578ef175b195d4983', - value: 'Sewcond', - timestamp: '2022-06-06T12:57:27', - }, - { - id: 'fad0a998e49fb8a9f5681f34f3288bea55559853d971c541607d22fd25773ed8', - value: 'third', - timestamp: '2022-06-06T12:57:27', - }, - { - id: 'c8ad923e8b0ff106d104e95d695f84e695525364c0acdc74786e4c59a457c637', - value: 'Fourth', - timestamp: '2022-06-06T12:57:20', - }, - { - id: '0f7b8a2912a59a737a6e7e1d3c4807d64ad0c8f54d383d9a118851f2c8f98ab6', - value: 'Fifth', - timestamp: '2022-06-06T12:57:21', - }, - { - id: 'fad0a998e49fb8a9f5681f34f3288bea07659834a971c541607d22fd25773ed8', - value: 'Sixth', - timestamp: '2022-06-06T12:57:27', - }, - ]; - - const paginate = ({ page, pageSize }) => { - const start = (page - 1) * pageSize; - const end = page * pageSize; - return allRows.slice(start, end); - }; + it('should respect ariaLabel prop', () => { + render( + + + + + + +
+ ); - useEffect(() => { - setRows(paginate({ page: 1, pageSize: 2 })); - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - return ( - <> - - {({ - rows, - headers, - getTableProps, - getHeaderProps, - getRowProps, - getExpandHeaderProps, - }) => ( + expect(screen.getByLabelText('Expand')).toBeInTheDocument(); + }); + + it('should support a custom `className` prop on the outermost element', () => { + render( + + + + + + +
+ ); + + expect(screen.getByRole('columnheader')).toHaveClass('test-class'); + }); + + it('should respect enableToggle prop', () => { + render( + + + + + + +
+ ); + + expect(screen.getByRole('button')).toHaveClass( + 'cds--table-expand__button' + ); + }); + + it('should respect expandIconDescription prop', () => { + render( + + + + + + +
+ ); + + expect( + screen.getByLabelText('Test icon description') + ).toBeInTheDocument(); + }); + + it('should respect id prop', () => { + render( + + + + + + +
+ ); + + expect(screen.getByRole('columnheader').id).toEqual('test-id'); + }); + }); + + describe('behaves as expected', () => { + it('should call onExpand', () => { + const onExpand = jest.fn(); + render( + + {({ + headers, + getTableProps, + getHeaderProps, + getExpandHeaderProps, + getTableContainerProps, + }) => ( + - {headers.map((header) => ( - + {headers.map((header, i) => ( + {header.header} ))} - - {rows.map((row, index) => ( - - - {row.cells.map((cell) => ( - {cell.value} - ))} - - - Some content for {row.id} - - - ))} -
- )} -
- { - setRows(paginate({ page, pageSize })); - }} - page={1} - pageSize={2} - pageSizes={[2]} - size="md" - totalItems={allRows.length} - /> - + + )} +
); - }; - render(); + userEvent.click(screen.getByRole('button')); + + expect(onExpand).toHaveBeenCalled(); + }); + + it('should update toggle button', () => { + const PaginationExample = () => { + const [rows, setRows] = useState([]); + const headers = [ + { + key: 'value', + header: 'Value', + }, + { + key: 'timestamp', + header: 'Submitted At', + }, + ]; + + const allRows = [ + { + id: 'f66f9f0e293e622b046ab4826f99d071a377418fd69bf1685c8d23c371f517cc', + value: 'First', + timestamp: '2022-06-06T12:57:27', + }, + { + id: 'd0d95500fccef68dd1e7cb36f381984d340e9a81657b00e578ef175b195d4983', + value: 'Sewcond', + timestamp: '2022-06-06T12:57:27', + }, + { + id: 'fad0a998e49fb8a9f5681f34f3288bea55559853d971c541607d22fd25773ed8', + value: 'third', + timestamp: '2022-06-06T12:57:27', + }, + { + id: 'c8ad923e8b0ff106d104e95d695f84e695525364c0acdc74786e4c59a457c637', + value: 'Fourth', + timestamp: '2022-06-06T12:57:20', + }, + { + id: '0f7b8a2912a59a737a6e7e1d3c4807d64ad0c8f54d383d9a118851f2c8f98ab6', + value: 'Fifth', + timestamp: '2022-06-06T12:57:21', + }, + { + id: 'fad0a998e49fb8a9f5681f34f3288bea07659834a971c541607d22fd25773ed8', + value: 'Sixth', + timestamp: '2022-06-06T12:57:27', + }, + ]; - userEvent.click(screen.getByLabelText('Expand all rows')); + const paginate = ({ page, pageSize }) => { + const start = (page - 1) * pageSize; + const end = page * pageSize; + return allRows.slice(start, end); + }; - expect(screen.getAllByRole('button')[0]).toHaveAttribute( - 'aria-label', - 'Collapse all rows' - ); + useEffect(() => { + setRows(paginate({ page: 1, pageSize: 2 })); + }, []); // eslint-disable-line react-hooks/exhaustive-deps - userEvent.click(screen.getByLabelText('Next page')); - expect(screen.getAllByRole('button')[0]).toHaveAttribute( - 'aria-label', - 'Expand all rows' - ); + return ( + <> + + {({ + rows, + headers, + getTableProps, + getHeaderProps, + getRowProps, + getExpandHeaderProps, + }) => ( + + + + + {headers.map((header) => ( + + {header.header} + + ))} + + + + {rows.map((row, index) => ( + + + {row.cells.map((cell) => ( + {cell.value} + ))} + + + Some content for {row.id} + + + ))} + +
+ )} +
+ { + setRows(paginate({ page, pageSize })); + }} + page={1} + pageSize={2} + pageSizes={[2]} + size="md" + totalItems={allRows.length} + /> + + ); + }; + + render(); + + userEvent.click(screen.getByLabelText('Expand all rows')); + + expect(screen.getAllByRole('button')[0]).toHaveAttribute( + 'aria-label', + 'Collapse all rows' + ); + + userEvent.click(screen.getByLabelText('Next page')); + expect(screen.getAllByRole('button')[0]).toHaveAttribute( + 'aria-label', + 'Expand all rows' + ); + }); }); }); diff --git a/packages/react/src/components/DataTable/__tests__/TableHeader-test.js b/packages/react/src/components/DataTable/__tests__/TableHeader-test.js index 36d2845e4e94..dae64cc87441 100644 --- a/packages/react/src/components/DataTable/__tests__/TableHeader-test.js +++ b/packages/react/src/components/DataTable/__tests__/TableHeader-test.js @@ -1,78 +1,189 @@ /** - * Copyright IBM Corp. 2016, 2023 + * Copyright IBM Corp. 2022, 2023 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ import React from 'react'; -import { mount } from 'enzyme'; -import { Table, TableHead, TableHeader, TableRow } from '../'; - -describe('DataTable.TableHeader', () => { - let mockProps; - - beforeEach(() => { - mockProps = { - isSortHeader: false, - onClick: jest.fn(), - sortDirection: 'NONE', - }; - }); +import { Table, TableHead, TableRow, TableHeader } from '../'; +import userEvent from '@testing-library/user-event'; +import { render, screen } from '@testing-library/react'; - it('should render', () => { - const simpleHeader = mount( - - - - Header - - -
- ); - expect(simpleHeader).toMatchSnapshot(); - - const sortHeader = mount( - - - - - Header - - - -
- ); - expect(sortHeader).toMatchSnapshot(); - }); +describe('TableHeader', () => { + describe('renders as expected - Component API', () => { + it('should render', () => { + const { container } = render( + + + + Header + + +
+ ); + expect(container).toMatchSnapshot(); + }); + + it('should spread extra props onto outermost element', () => { + render( + + + + + + +
+ ); + + expect(screen.getByTestId('test-id')).toHaveAttribute('test', 'test'); + }); + + it('should render children as expected', () => { + render( + + + + + add appropriate children + + + +
+ ); + + expect(screen.getByText('add appropriate children')).toBeInTheDocument(); + }); + + it('should support a custom `className` prop on the outermost element', () => { + render( + + + + + + +
+ ); + + expect(screen.getByTestId('test-id')).toHaveClass('custom-class'); + }); + + it('should respect colSpan prop', () => { + render( + + + + + + +
+ ); + + expect(screen.getByTestId('test-id')).toHaveAttribute('colSpan', '4'); + }); + + it('should respect id prop', () => { + render( + + + + + + +
+ ); - it('should have an active class if it is the sort header and the sort state is not NONE', () => { - const wrapper = mount( - - - - - Header - - - -
- ); - expect(wrapper).toMatchSnapshot(); + expect(screen.getByTestId('test-id')).toHaveAttribute('id', 'id'); + }); + + it('should respect isSortHeader prop', () => { + render( + + + + + + +
+ ); + + expect(screen.getByTestId('test-id')).toHaveClass( + 'cds--table-sort--descending' + ); + }); + + it('should respect isSortable prop', () => { + render( + + + + + + +
+ ); + + expect(screen.getByTestId('test-id')).toHaveClass('cds--table-sort'); + }); + + it('should respect scope prop', () => { + render( + + + + + + +
+ ); + + expect(screen.getByTestId('test-id')).toHaveAttribute('scope', 'row'); + }); + + it('should respect translateWithId prop', () => { + const translateWithId = () => { + return 'id translation'; + }; + + render( + + + + + + +
+ ); + + expect(screen.getByText('id translation')).toBeInTheDocument(); + }); }); - it('should have an active and ascending class if sorting by ASC', () => { - const wrapper = mount( - - - - - Header - - - -
- ); - expect(wrapper).toMatchSnapshot(); + describe('behaves as expected', () => { + it('should call onClick when expected', () => { + const onClick = jest.fn(); + render( + + + + + + +
+ ); + + userEvent.click(screen.getByRole('button'), 'test'); + expect(onClick).toHaveBeenCalled(); + }); }); }); diff --git a/packages/react/src/components/DataTable/__tests__/TableToolbar-test.js b/packages/react/src/components/DataTable/__tests__/TableToolbar-test.js index 5364f0b70b12..e2d3902dc9e3 100644 --- a/packages/react/src/components/DataTable/__tests__/TableToolbar-test.js +++ b/packages/react/src/components/DataTable/__tests__/TableToolbar-test.js @@ -6,12 +6,43 @@ */ import React from 'react'; -import { mount } from 'enzyme'; -import { TableToolbar } from '../'; +import TableToolbar from '../TableToolbar'; +import { render, screen } from '@testing-library/react'; -describe('DataTable.TableToolbar', () => { - it('should render', () => { - const wrapper = mount(); - expect(wrapper).toMatchSnapshot(); +describe('TableToolbar', () => { + describe('renders as expected - Component API', () => { + it('should render', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + + it('should spread extra props onto outermost element', () => { + const { container } = render(); + + expect(container.firstChild).toHaveAttribute('data-testid', 'test-id'); + }); + + it('should respect aria-label prop', () => { + render(); + + expect(screen.getByLabelText('Aria label')).toBeInTheDocument(); + }); + + it('should render children as expected', () => { + render( + +
child
+
+ ); + + expect(screen.getByText('child')).toBeInTheDocument(); + }); + + it('should respect size prop', () => { + const { container } = render(); + + expect(container.firstChild).toHaveClass('cds--table-toolbar--sm'); + }); }); }); diff --git a/packages/react/src/components/DataTable/__tests__/TableToolbarMenu-test.js b/packages/react/src/components/DataTable/__tests__/TableToolbarMenu-test.js index feac04753036..653ae8e91242 100644 --- a/packages/react/src/components/DataTable/__tests__/TableToolbarMenu-test.js +++ b/packages/react/src/components/DataTable/__tests__/TableToolbarMenu-test.js @@ -6,35 +6,52 @@ */ import React from 'react'; -import { mount } from 'enzyme'; +import TableToolbarMenu from '../TableToolbarMenu'; import { Download } from '@carbon/icons-react'; -import { TableToolbarMenu } from '..'; - -describe('DataTable.TableToolbarMenu', () => { - it('should render', () => { - const wrapper = mount( - - test - - ); - expect(wrapper).toMatchSnapshot(); - }); -}); +import { render, screen } from '@testing-library/react'; + +describe('TableToolbarMenu', () => { + describe('renders as expected - Component API', () => { + it('should render', () => { + const { container } = render( + + test + + ); + + expect(container).toMatchSnapshot(); + }); + + it('should support a custom `className` prop on the outermost element', () => { + render( + + test + + ); + expect(screen.getByRole('button')).toHaveClass('custom-class'); + }); + + it('should respect iconDescription prop', () => { + render( + + test + + ); + + expect(screen.getByText('Icon description')).toBeInTheDocument(); + }); + + it('should respect renderIcon prop', () => { + render( + + test + + ); -describe('Custom icon in DataTable.TableToolbarMenu', () => { - it('should render', () => { - const iconAction = mount( - - test - - ); - const originalIcon = mount().find('svg'); - const icon = iconAction.find('svg'); - expect(icon.getDOMNode().querySelectorAll(':not(svg):not(title)')).toEqual( - originalIcon.getDOMNode().querySelectorAll(':not(svg):not(title)') - ); + expect(screen.getByRole('img')).toHaveAttribute('aria-label', 'Download'); + }); }); }); diff --git a/packages/react/src/components/DataTable/__tests__/__snapshots__/TableExpandHeader-test.js.snap b/packages/react/src/components/DataTable/__tests__/__snapshots__/TableExpandHeader-test.js.snap index f1ed5d23a68b..2d15fbc0083c 100644 --- a/packages/react/src/components/DataTable/__tests__/__snapshots__/TableExpandHeader-test.js.snap +++ b/packages/react/src/components/DataTable/__tests__/__snapshots__/TableExpandHeader-test.js.snap @@ -1,39 +1,23 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`DataTable.TableExpandHeader should render 1`] = ` - +exports[`TableExpandHeader renders as expected - Component API should render 1`] = ` +
- - - - - - - - - + + + +
- -
+
- + `; diff --git a/packages/react/src/components/DataTable/__tests__/__snapshots__/TableHeader-test.js.snap b/packages/react/src/components/DataTable/__tests__/__snapshots__/TableHeader-test.js.snap index 27236e172f21..fb2393c64646 100644 --- a/packages/react/src/components/DataTable/__tests__/__snapshots__/TableHeader-test.js.snap +++ b/packages/react/src/components/DataTable/__tests__/__snapshots__/TableHeader-test.js.snap @@ -1,177 +1,28 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`DataTable.TableHeader should have an active and ascending class if sorting by ASC 1`] = ` - +exports[`TableHeader renders as expected - Component API should render 1`] = ` +
- - - - - - - - - - - + + + + +
-
- Header -
-
+
+ Header +
+
- -`; - -exports[`DataTable.TableHeader should have an active class if it is the sort header and the sort state is not NONE 1`] = ` - -
-
- - - - - - - - - - - -
-
- Header -
-
- - -`; - -exports[`DataTable.TableHeader should render 1`] = ` - -
-
- - - - - - - - - - - -
-
- Header -
-
- - -`; - -exports[`DataTable.TableHeader should render 2`] = ` - -
-
- - - - - - - - - - - -
-
- Header -
-
- - + `; diff --git a/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbar-test.js.snap b/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbar-test.js.snap index 21f97627e2d0..5d5a930fe1e3 100644 --- a/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbar-test.js.snap +++ b/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbar-test.js.snap @@ -1,13 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`DataTable.TableToolbar should render 1`] = ` - +exports[`TableToolbar renders as expected - Component API should render 1`] = ` +
- +
`; diff --git a/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbarMenu-test.js.snap b/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbarMenu-test.js.snap index dad893d3a3cb..b8b4c701697e 100644 --- a/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbarMenu-test.js.snap +++ b/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbarMenu-test.js.snap @@ -1,196 +1,54 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`DataTable.TableToolbarMenu should render 1`] = ` - - + - - - - - + + + + + `; diff --git a/packages/react/src/components/Menu/Menu.js b/packages/react/src/components/Menu/Menu.js index fc5ea6ed5f47..85880be18677 100644 --- a/packages/react/src/components/Menu/Menu.js +++ b/packages/react/src/components/Menu/Menu.js @@ -203,6 +203,10 @@ const Menu = React.forwardRef(function Menu( useEffect(() => { if (open) { handleOpen(); + } else { + // reset position when menu is closed in order for the --shown + // modifier to be applied correctly + setPosition(-1, -1); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); diff --git a/packages/react/src/components/Notification/Notification.js b/packages/react/src/components/Notification/Notification.js index 49b4fd4ccd88..6cd2cc981d8f 100644 --- a/packages/react/src/components/Notification/Notification.js +++ b/packages/react/src/components/Notification/Notification.js @@ -657,11 +657,6 @@ ActionableNotification.propTypes = { */ ariaLabel: PropTypes.string, - /** - * Specify the caption - */ - caption: PropTypes.string, - /** * Specify the content */