diff --git a/.changeset/heavy-geckos-grab.md b/.changeset/heavy-geckos-grab.md new file mode 100644 index 00000000000..48c1e68406c --- /dev/null +++ b/.changeset/heavy-geckos-grab.md @@ -0,0 +1,11 @@ +--- +'@talend/react-faceted-search': minor +'@talend/design-system': minor +'@talend/react-bootstrap': minor +'@talend/react-components': minor +'@talend/react-containers': minor +'@talend/react-forms': minor +'@talend/react-a11y': minor +--- + +Remove usage of lib keyCode diff --git a/.changeset/strong-keys-drum.md b/.changeset/strong-keys-drum.md new file mode 100644 index 00000000000..1c58b1bee7e --- /dev/null +++ b/.changeset/strong-keys-drum.md @@ -0,0 +1,5 @@ +--- +'@talend/scripts-config-jest': major +--- + +chore: bump testing-library to 6.x diff --git a/fork/react-bootstrap/package.json b/fork/react-bootstrap/package.json index 81442fade0d..c7d48702996 100644 --- a/fork/react-bootstrap/package.json +++ b/fork/react-bootstrap/package.json @@ -19,9 +19,6 @@ "storybook": "start-storybook -p 6006", "build-storybook": "build-storybook" }, - "prettier": { - "singleQuote": true - }, "files": [ "CHANGELOG.md", "lib", @@ -46,9 +43,9 @@ "@talend/scripts-core": "^16.3.0", "@talend/scripts-config-babel": "^13.2.0", "@talend/scripts-config-react-webpack": "^16.3.1", - "@testing-library/jest-dom": "^5.17.0", - "@testing-library/react": "^12.1.5", - "@testing-library/user-event": "^13.5.0", + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.5.1", "chai": "^4.3.10", "chalk": "^2.4.2", "create-react-class": "^15.7.0", @@ -63,7 +60,6 @@ "classnames": "^2.3.2", "dom-helpers": "^3.4.0", "invariant": "^2.2.4", - "keycode": "^2.2.1", "prop-types": "^15.8.1", "prop-types-extra": "^1.1.1", "react-overlays": "^0.9.3", diff --git a/fork/react-bootstrap/src/Alert.test.js b/fork/react-bootstrap/src/Alert.test.js index 30acd97cc4a..33b96a18fa6 100644 --- a/fork/react-bootstrap/src/Alert.test.js +++ b/fork/react-bootstrap/src/Alert.test.js @@ -1,92 +1,95 @@ -import React from 'react'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Alert from './Alert'; describe('', () => { - it('Should output a alert with message', () => { - // when - render( - - Message - - ); - - // then - expect(screen.getByRole('alert')).toBeInTheDocument(); - expect(screen.getByText('Message')).toBeInTheDocument(); - }); - - it('Should have bsType by default', () => { - // when - render( - - Message - - ); - - // then - expect(screen.getByRole('alert')).toHaveClass('alert-info'); - }); - - it('Should have dismissable style with onDismiss', () => { - // when - render( - - Message - - ); - - // then - expect(screen.getByRole('alert')).toHaveClass('alert-dismissable'); - }); - - it('Should call onDismiss callback on dismiss click', () => { - // given - const onDismiss = jest.fn(); - render( - - Message - - ); - expect(onDismiss).not.toBeCalled(); - - // when - userEvent.click(screen.getByRole('button', { name: 'close' })); - - // then - expect(onDismiss).toBeCalled(); - }); - - it('Should have a default bsStyle class', () => { - // when - render(Message); - - // then - expect(screen.getByRole('alert')).toHaveClass('alert-info'); - }); - - it('Should have use bsStyle class', () => { - // when - render(Message); - - // then - expect(screen.getByRole('alert')).toHaveClass('alert-danger'); - }); - - describe('Web Accessibility', () => { - it('Should call onDismiss callback when the sr-only dismiss link is activated', () => { - // given - const onDismiss = jest.fn(); - render(Message); - expect(onDismiss).not.toBeCalled(); - - // when - userEvent.click(screen.getByRole('button', { name: 'Close alert' })); - - // then - expect(onDismiss).toBeCalled(); - }); - }); + it('Should output a alert with message', () => { + // when + render( + + Message + , + ); + + // then + expect(screen.getByRole('alert')).toBeInTheDocument(); + expect(screen.getByText('Message')).toBeInTheDocument(); + }); + + it('Should have bsType by default', () => { + // when + render( + + Message + , + ); + + // then + expect(screen.getByRole('alert')).toHaveClass('alert-info'); + }); + + it('Should have dismissable style with onDismiss', () => { + // when + render( + + Message + , + ); + + // then + expect(screen.getByRole('alert')).toHaveClass('alert-dismissable'); + }); + + it('Should call onDismiss callback on dismiss click', async () => { + const user = userEvent.setup(); + + // given + const onDismiss = jest.fn(); + render( + + Message + , + ); + expect(onDismiss).not.toHaveBeenCalled(); + + // when + await user.click(screen.getByRole('button', { name: 'close' })); + + // then + expect(onDismiss).toHaveBeenCalled(); + }); + + it('Should have a default bsStyle class', () => { + // when + render(Message); + + // then + expect(screen.getByRole('alert')).toHaveClass('alert-info'); + }); + + it('Should have use bsStyle class', () => { + // when + render(Message); + + // then + expect(screen.getByRole('alert')).toHaveClass('alert-danger'); + }); + + describe('Web Accessibility', () => { + it('Should call onDismiss callback when the sr-only dismiss link is activated', async () => { + const user = userEvent.setup(); + + // given + const onDismiss = jest.fn(); + render(Message); + expect(onDismiss).not.toHaveBeenCalled(); + + // when + await user.click(screen.getByRole('button', { name: 'Close alert' })); + + // then + expect(onDismiss).toHaveBeenCalled(); + }); + }); }); diff --git a/fork/react-bootstrap/src/BreadcrumbItem.test.js b/fork/react-bootstrap/src/BreadcrumbItem.test.js index dd668ba9cc9..9766108e9a0 100644 --- a/fork/react-bootstrap/src/BreadcrumbItem.test.js +++ b/fork/react-bootstrap/src/BreadcrumbItem.test.js @@ -1,144 +1,135 @@ -import React from 'react'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Breadcrumb from './Breadcrumb'; describe('', () => { - it('Should render `a` as inner element when is not active', () => { - // when - render(Crumb); - - // then - const link = screen.getByRole('button'); - expect(link).toBeInTheDocument(); - expect(link.tagName).toBe('A'); - expect(link).not.toHaveClass('active'); - }); - - it('Should render `span.active` with `active` attribute set.', () => { - // when - render(Active Crumb); - - // then - const item = screen.getByRole('listitem'); - expect(item).toBeInTheDocument(); - expect(item).toHaveClass('active'); - }); - - it('Should render `span.active` when active and has href', () => { - // when - render( - - Active Crumb - - ); - - // then - const item = screen.getByRole('listitem'); - expect(item).toBeInTheDocument(); - expect(item).toHaveClass('active'); - expect(screen.queryByRole('button')).not.toBeInTheDocument(); - }); - - it('Should add custom classes onto `li` wrapper element', () => { - // when - render( - - Active Crumb - - ); - - // then - const item = screen.getByRole('listitem'); - expect(item).toHaveClass('custom-one'); - expect(item).toHaveClass('custom-two'); - }); - - it('Should spread additional props onto inner element', () => { - // given - const handleClick = jest.fn(); - render( - - Crumb - - ); - - // when - userEvent.click(screen.getByRole('button')); - - // then - expect(handleClick).toHaveBeenCalled(); - }); - - it('Should apply id onto the anchor', () => { - // when - render( - - Crumb - - ); - - // then - expect(screen.getByRole('button')).toHaveAttribute('id', 'test-link-id'); - }); - - it('Should apply `href` property onto `a` inner element', () => { - // when - render( - - Crumb - - ); - - // then - expect(screen.getByRole('link')).toHaveAttribute( - 'href', - 'http://getbootstrap.com/components/#breadcrumbs' - ); - }); - - it('Should apply `title` property onto `a` inner element', () => { - // when - render( - - Crumb - - ); - - // then - expect(screen.getByRole('link')).toHaveAttribute('title', 'test-title'); - }); - - it('Should not apply properties for inner `anchor` onto `li` wrapper element', () => { - // when - render( - - Crumb - - ); - - // then - const listitem = screen.getByRole('listitem'); - expect(listitem).not.toHaveAttribute('title'); - expect(listitem).not.toHaveAttribute('href'); - }); - - it('Should set `target` attribute on `anchor`', () => { - // when - render( - - Crumb - - ); - - // then - expect(screen.getByRole('link')).toHaveAttribute('target', '_blank'); - }); + it('Should render `a` as inner element when is not active', () => { + // when + render(Crumb); + + // then + const link = screen.getByRole('button'); + expect(link).toBeInTheDocument(); + expect(link.tagName).toBe('A'); + expect(link).not.toHaveClass('active'); + }); + + it('Should render `span.active` with `active` attribute set.', () => { + // when + render(Active Crumb); + + // then + const item = screen.getByRole('listitem'); + expect(item).toBeInTheDocument(); + expect(item).toHaveClass('active'); + }); + + it('Should render `span.active` when active and has href', () => { + // when + render( + + Active Crumb + , + ); + + // then + const item = screen.getByRole('listitem'); + expect(item).toBeInTheDocument(); + expect(item).toHaveClass('active'); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); + + it('Should add custom classes onto `li` wrapper element', () => { + // when + render(Active Crumb); + + // then + const item = screen.getByRole('listitem'); + expect(item).toHaveClass('custom-one'); + expect(item).toHaveClass('custom-two'); + }); + + it('Should spread additional props onto inner element', async () => { + const user = userEvent.setup(); + + // given + const handleClick = jest.fn(); + render( + + Crumb + , + ); + + // when + await user.click(screen.getByRole('button')); + + // then + expect(handleClick).toHaveBeenCalled(); + }); + + it('Should apply id onto the anchor', () => { + // when + render( + + Crumb + , + ); + + // then + expect(screen.getByRole('button')).toHaveAttribute('id', 'test-link-id'); + }); + + it('Should apply `href` property onto `a` inner element', () => { + // when + render( + + Crumb + , + ); + + // then + expect(screen.getByRole('link')).toHaveAttribute( + 'href', + 'http://getbootstrap.com/components/#breadcrumbs', + ); + }); + + it('Should apply `title` property onto `a` inner element', () => { + // when + render( + + Crumb + , + ); + + // then + expect(screen.getByRole('link')).toHaveAttribute('title', 'test-title'); + }); + + it('Should not apply properties for inner `anchor` onto `li` wrapper element', () => { + // when + render( + + Crumb + , + ); + + // then + const listitem = screen.getByRole('listitem'); + expect(listitem).not.toHaveAttribute('title'); + expect(listitem).not.toHaveAttribute('href'); + }); + + it('Should set `target` attribute on `anchor`', () => { + // when + render( + + Crumb + , + ); + + // then + expect(screen.getByRole('link')).toHaveAttribute('target', '_blank'); + }); }); diff --git a/fork/react-bootstrap/src/Button.test.js b/fork/react-bootstrap/src/Button.test.js index b5d67347451..df9107f17da 100644 --- a/fork/react-bootstrap/src/Button.test.js +++ b/fork/react-bootstrap/src/Button.test.js @@ -1,119 +1,120 @@ -import React from 'react'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Button from './Button'; describe('); - - // then - expect(screen.getByRole('button')).toBeInTheDocument(); - }); - - it('Should have type=button by default', () => { - // when - render(); - - // then - expect(screen.getByRole('button')).toHaveAttribute('type', 'button'); - }); - - it('Should show the type if passed one', () => { - // when - render(); - - // then - expect(screen.getByRole('button')).toHaveAttribute('type', 'submit'); - }); - - it('Should output an anchor if called with a href', () => { - // when - const href = '/url'; - render(); - - // then - expect(screen.getByRole('link')).toHaveAttribute('href', href); - }); - - it('Should call onClick callback', () => { - // given - const onClick = jest.fn(); - render(); - - // when - userEvent.click(screen.getByRole('button')); - - // then - expect(onClick).toHaveBeenCalled(); - }); - - it('Should be disabled', () => { - // when - render(); - - // then - expect(screen.getByRole('button')).toBeDisabled(); - }); - - it('Should be disabled link', () => { - // when - render( - - ); - - // then - const link = screen.getByRole('button'); - expect(link.tagName).toBe('A'); - expect(link).toHaveClass('disabled'); - }); - - it('Should have block class', () => { - // when - render(); - - // then - expect(screen.getByRole('button')).toHaveClass('btn-block'); - }); - - it('Should apply bsStyle class', () => { - // when - render(); - - // then - expect(screen.getByRole('button')).toHaveClass('btn-danger'); - }); - - it('Should honour additional classes passed in, adding not overriding', () => { - // when - render( - - ); - - // then - expect(screen.getByRole('button')).toHaveClass('btn-danger'); - expect(screen.getByRole('button')).toHaveClass('bob'); - }); - - it('Should default to bsStyle="default"', () => { - // when - render(); - - // then - expect(screen.getByRole('button')).toHaveClass('btn-default'); - }); - - it('Should be active', () => { - // when - render(); - - // then - expect(screen.getByRole('button')).toHaveClass('active'); - }); + it('Should output a button', () => { + // when + render(); + + // then + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + it('Should have type=button by default', () => { + // when + render(); + + // then + expect(screen.getByRole('button')).toHaveAttribute('type', 'button'); + }); + + it('Should show the type if passed one', () => { + // when + render(); + + // then + expect(screen.getByRole('button')).toHaveAttribute('type', 'submit'); + }); + + it('Should output an anchor if called with a href', () => { + // when + const href = '/url'; + render(); + + // then + expect(screen.getByRole('link')).toHaveAttribute('href', href); + }); + + it('Should call onClick callback', async () => { + const user = userEvent.setup(); + + // given + const onClick = jest.fn(); + render(); + + // when + await user.click(screen.getByRole('button')); + + // then + expect(onClick).toHaveBeenCalled(); + }); + + it('Should be disabled', () => { + // when + render(); + + // then + expect(screen.getByRole('button')).toBeDisabled(); + }); + + it('Should be disabled link', () => { + // when + render( + , + ); + + // then + const link = screen.getByRole('button'); + expect(link.tagName).toBe('A'); + expect(link).toHaveClass('disabled'); + }); + + it('Should have block class', () => { + // when + render(); + + // then + expect(screen.getByRole('button')).toHaveClass('btn-block'); + }); + + it('Should apply bsStyle class', () => { + // when + render(); + + // then + expect(screen.getByRole('button')).toHaveClass('btn-danger'); + }); + + it('Should honour additional classes passed in, adding not overriding', () => { + // when + render( + , + ); + + // then + expect(screen.getByRole('button')).toHaveClass('btn-danger'); + expect(screen.getByRole('button')).toHaveClass('bob'); + }); + + it('Should default to bsStyle="default"', () => { + // when + render(); + + // then + expect(screen.getByRole('button')).toHaveClass('btn-default'); + }); + + it('Should be active', () => { + // when + render(); + + // then + expect(screen.getByRole('button')).toHaveClass('active'); + }); }); diff --git a/fork/react-bootstrap/src/Carousel.test.js b/fork/react-bootstrap/src/Carousel.test.js index 02252274973..25564e7a529 100644 --- a/fork/react-bootstrap/src/Carousel.test.js +++ b/fork/react-bootstrap/src/Carousel.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Carousel from './Carousel'; @@ -48,7 +48,7 @@ describe('', () => { {null} {false} Item 2 content - + , ); // then @@ -64,41 +64,47 @@ describe('', () => { expect(list.querySelectorAll('li')).toHaveLength(2); }); - it('Should call onSelect when indicator selected', () => { + it('Should call onSelect when indicator selected', async () => { + const user = userEvent.setup(); + // given const onSelect = jest.fn(); render( {items} - + , ); // when - userEvent.click(screen.getAllByRole('listitem')[0]); + await user.click(screen.getAllByRole('listitem')[0]); // then - expect(onSelect).toBeCalledWith(0); + expect(onSelect).toHaveBeenCalledWith(0); }); - it('Should call onSelect with direction', () => { + it('Should call onSelect with direction', async () => { + const user = userEvent.setup(); + // given const onSelect = jest.fn((index, event) => {}); // force the event with direction by requiring event in callback render( {items} - + , ); // when - userEvent.click(screen.getAllByRole('listitem')[0]); + await user.click(screen.getAllByRole('listitem')[0]); // then - expect(onSelect).toBeCalled(); + expect(onSelect).toHaveBeenCalled(); expect(onSelect.mock.calls[0][0]).toBe(0); expect(onSelect.mock.calls[0][1].direction).toBe('prev'); }); - it('Should call onSelect with direction when there is no event', () => { + it('Should call onSelect with direction when there is no event', async () => { + const user = userEvent.setup(); + // function onSelect(index, event) { // expect(index).to.equal(0); // expect(event.direction).to.equal('next'); @@ -112,54 +118,56 @@ describe('', () => { render( {items} - + , ); // when - userEvent.click(screen.getByRole('button', { name: 'Next' })); + await user.click(screen.getByRole('button', { name: 'Next' })); // then - expect(onSelect).toBeCalled(); + expect(onSelect).toHaveBeenCalled(); expect(onSelect.mock.calls[0][0]).toBe(0); expect(onSelect.mock.calls[0][1].direction).toBe('next'); }); - it('Should show back button control on the first image if wrap is true', () => { + it('Should show back button control on the first image if wrap is true', async () => { + const user = userEvent.setup(); + // given - jest.useFakeTimers(); render( {items} - + , ); expect(screen.getByText('Item 1 content')).toHaveClass('active'); // when - userEvent.click(screen.getByRole('button', { name: 'Previous' })); - jest.runAllTimers(); + await user.click(screen.getByRole('button', { name: 'Previous' })); // then - expect(screen.getByText('Item 2 content')).toHaveClass('active'); - jest.useRealTimers(); + await waitFor(() => + expect(screen.getByText('Item 2 content')).toHaveClass('active'), + ); }); - it('Should show next button control on the last image if wrap is true', () => { + it('Should show next button control on the last image if wrap is true', async () => { + const user = userEvent.setup(); + // given - jest.useFakeTimers(); render( {items} - + , ); expect(screen.getByText('Item 2 content')).toHaveClass('active'); // when - userEvent.click(screen.getByRole('button', { name: 'Next' })); - jest.runAllTimers(); + await user.click(screen.getByRole('button', { name: 'Next' })); // then - expect(screen.getByText('Item 1 content')).toHaveClass('active'); - jest.useRealTimers(); + await waitFor(() => + expect(screen.getByText('Item 1 content')).toHaveClass('active'), + ); }); it('Should not show the prev button on the first image if wrap is false', () => { @@ -167,12 +175,12 @@ describe('', () => { render( {items} - + , ); // then expect( - screen.queryByRole('button', { name: 'Previous' }) + screen.queryByRole('button', { name: 'Previous' }), ).not.toBeInTheDocument(); }); @@ -181,12 +189,12 @@ describe('', () => { render( {items} - + , ); // then expect( - screen.queryByRole('button', { name: 'Next' }) + screen.queryByRole('button', { name: 'Next' }), ).not.toBeInTheDocument(); }); @@ -203,15 +211,15 @@ describe('', () => { Item 1 content Item 2 content Item 3 content - + , ); // then expect( - screen.getByRole('button', { name: 'Previous' }).firstChild + screen.getByRole('button', { name: 'Previous' }).firstChild, ).toHaveClass('ficon-left'); expect(screen.getByRole('button', { name: 'Next' }).firstChild).toHaveClass( - 'ficon-right' + 'ficon-right', ); }); @@ -228,35 +236,37 @@ describe('', () => { Item 1 content Item 2 content Item 3 content - + , ); // then expect( - screen.getByRole('button', { name: 'Previous awesomeness' }) + screen.getByRole('button', { name: 'Previous awesomeness' }), ).toBeInTheDocument(); expect( - screen.getByRole('button', { name: 'Next awesomeness' }) + screen.getByRole('button', { name: 'Next awesomeness' }), ).toBeInTheDocument(); }); - it('Should transition properly when slide animation is disabled', () => { + it('Should transition properly when slide animation is disabled', async () => { + const user = userEvent.setup(); + // given render( {items} - + , ); expect(screen.getByText('Item 1 content')).toHaveClass('active'); // when - userEvent.click(screen.getByRole('button', { name: 'Next' })); + await user.click(screen.getByRole('button', { name: 'Next' })); // then expect(screen.getByText('Item 2 content')).toHaveClass('active'); // when - userEvent.click(screen.getByRole('button', { name: 'Previous' })); + await user.click(screen.getByRole('button', { name: 'Previous' })); // then expect(screen.getByText('Item 1 content')).toHaveClass('active'); @@ -267,7 +277,7 @@ describe('', () => { // default active is the 2nd item, which will be removed on // subsequent render const { rerender } = render( - {items} + {items}, ); expect(screen.getByText('Item 1 content')).not.toHaveClass('active'); diff --git a/fork/react-bootstrap/src/CloseButton.test.js b/fork/react-bootstrap/src/CloseButton.test.js index a6dd026600a..c5fa0ca59ac 100644 --- a/fork/react-bootstrap/src/CloseButton.test.js +++ b/fork/react-bootstrap/src/CloseButton.test.js @@ -32,14 +32,16 @@ describe('', () => { expect(screen.getByRole('button')).toHaveClass('close'); }); - it('Should call onClick callback', () => { + it('Should call onClick callback', async () => { + const user = userEvent.setup(); + // given const onClick = jest.fn(); render(); expect(onClick).not.toHaveBeenCalled(); // when - userEvent.click(screen.getByRole('button')); + await user.click(screen.getByRole('button')); // then expect(onClick).toHaveBeenCalled(); diff --git a/fork/react-bootstrap/src/ControlLabel.test.js b/fork/react-bootstrap/src/ControlLabel.test.js index 3c7589d4d49..18766e7f7b5 100644 --- a/fork/react-bootstrap/src/ControlLabel.test.js +++ b/fork/react-bootstrap/src/ControlLabel.test.js @@ -5,64 +5,64 @@ import ControlLabel from './ControlLabel'; import FormGroup from './FormGroup'; describe('', () => { - const originalConsoleError = console.error; + const originalConsoleError = console.error; - beforeEach(() => { - console.error = jest.fn(); - }); + beforeEach(() => { + console.error = jest.fn(); + }); - afterEach(() => { - console.error = originalConsoleError; - }); + afterEach(() => { + console.error = originalConsoleError; + }); - it('should render correctly', () => { - // when - render( - - Label - - ); + it('should render correctly', () => { + // when + render( + + Label + , + ); - // then - const label = screen.getByText('Label'); - expect(label.tagName).toBe('LABEL'); - expect(label).toHaveClass('control-label'); - expect(label).toHaveClass('my-control-label'); - expect(label).toHaveAttribute('for', 'foo'); - }); + // then + const label = screen.getByText('Label'); + expect(label.tagName).toBe('LABEL'); + expect(label).toHaveClass('control-label'); + expect(label).toHaveClass('my-control-label'); + expect(label).toHaveAttribute('for', 'foo'); + }); - it('should respect srOnly', () => { - // when - render(Label); + it('should respect srOnly', () => { + // when + render(Label); - // then - expect(screen.getByText('Label')).toHaveClass('sr-only'); - }); + // then + expect(screen.getByText('Label')).toHaveClass('sr-only'); + }); - it('should use controlId for htmlFor', () => { - // when - render( - - Label - - ); + it('should use controlId for htmlFor', () => { + // when + render( + + Label + , + ); - // then - expect(screen.getByText('Label')).toHaveAttribute('for', 'foo'); - }); + // then + expect(screen.getByText('Label')).toHaveAttribute('for', 'foo'); + }); - it('should prefer explicit htmlFor', () => { - // when - render( - - Label - - ); + it('should prefer explicit htmlFor', () => { + // when + render( + + Label + , + ); - // then - expect(screen.getByText('Label')).toHaveAttribute('for', 'bar'); - expect(console.error).toBeCalledWith( - 'Warning: `controlId` is ignored on `` when `htmlFor` is specified.' - ); - }); + // then + expect(screen.getByText('Label')).toHaveAttribute('for', 'bar'); + expect(console.error).toHaveBeenCalledWith( + 'Warning: `controlId` is ignored on `` when `htmlFor` is specified.', + ); + }); }); diff --git a/fork/react-bootstrap/src/Dropdown.js b/fork/react-bootstrap/src/Dropdown.js index a992679e589..137ed555e31 100644 --- a/fork/react-bootstrap/src/Dropdown.js +++ b/fork/react-bootstrap/src/Dropdown.js @@ -1,7 +1,6 @@ import classNames from 'classnames'; import activeElement from 'dom-helpers/activeElement'; import contains from 'dom-helpers/query/contains'; -import keycode from 'keycode'; import React, { cloneElement } from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; @@ -34,7 +33,7 @@ const propTypes = { * @required */ id: isRequiredForA11y( - PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + PropTypes.oneOfType([PropTypes.string, PropTypes.number]), ), componentClass: elementType, @@ -45,7 +44,7 @@ const propTypes = { */ children: all( requiredRoles(TOGGLE_ROLE, MENU_ROLE), - exclusiveRoles(MENU_ROLE) + exclusiveRoles(MENU_ROLE), ), /** @@ -143,7 +142,7 @@ class Dropdown extends React.Component { if (!open && prevOpen) { this._focusInDropdown = contains( ReactDOM.findDOMNode(this.menu), - activeElement(document) + activeElement(document), ); // if focus hasn't already moved from the menu let's return it // to the toggle @@ -198,8 +197,9 @@ class Dropdown extends React.Component { return; } - switch (event.keyCode) { - case keycode.codes.down: + switch (event.key) { + case 'Down': + case 'ArrowDown': if (!this.props.open) { this.toggleOpen(event, { source: 'keydown' }); } else if (this.menu.focusNext) { @@ -207,8 +207,9 @@ class Dropdown extends React.Component { } event.preventDefault(); break; - case keycode.codes.esc: - case keycode.codes.tab: + case 'Esc': + case 'Escape': + case 'Tab': this.handleClose(event, { source: 'keydown' }); break; default: @@ -237,7 +238,7 @@ class Dropdown extends React.Component { false, 'String refs are not supported on `` components. ' + 'To apply a ref to the component use the callback signature:\n\n ' + - 'https://facebook.github.io/react/docs/more-about-refs.html#the-ref-callback-attribute' + 'https://facebook.github.io/react/docs/more-about-refs.html#the-ref-callback-attribute', ); } else { ref = createChainedFunction(child.ref, ref); @@ -252,7 +253,7 @@ class Dropdown extends React.Component { onSelect: createChainedFunction( child.props.onSelect, onSelect, - (key, event) => this.handleClose(event, { source: 'select' }) + (key, event) => this.handleClose(event, { source: 'select' }), ), rootCloseEvent, }); @@ -268,7 +269,7 @@ class Dropdown extends React.Component { false, 'String refs are not supported on `` components. ' + 'To apply a ref to the component use the callback signature:\n\n ' + - 'https://facebook.github.io/react/docs/more-about-refs.html#the-ref-callback-attribute' + 'https://facebook.github.io/react/docs/more-about-refs.html#the-ref-callback-attribute', ); } else { ref = createChainedFunction(child.ref, ref); @@ -281,7 +282,7 @@ class Dropdown extends React.Component { onClick: createChainedFunction(child.props.onClick, this.handleClick), onKeyDown: createChainedFunction( child.props.onKeyDown, - this.handleKeyDown + this.handleKeyDown, ), }); } diff --git a/fork/react-bootstrap/src/Dropdown.test.js b/fork/react-bootstrap/src/Dropdown.test.js index f4f804b3b72..ca3d832ec63 100644 --- a/fork/react-bootstrap/src/Dropdown.test.js +++ b/fork/react-bootstrap/src/Dropdown.test.js @@ -4,7 +4,6 @@ import { useState } from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import keycode from 'keycode'; import ReactDOM from 'react-dom'; import Dropdown from './Dropdown'; @@ -12,721 +11,700 @@ import Grid from './Grid'; import MenuItem from './MenuItem'; function CustomMenu({ children, ...props }) { - return ( -
- {children} -
- ); + return ( +
+ {children} +
+ ); } describe('', () => { - const dropdownChildren = [ - Child Title, - - Item 1 - Item 2 - Item 3 - Item 4 - , - ]; - - const simpleDropdown = ( - - {dropdownChildren} - - ); - - it('renders div with dropdown class', () => { - // when - render({dropdownChildren}); - - // then - const group = screen.getByRole('menu').parentElement; - expect(group.tagName).toBe('DIV'); - expect(group).toHaveClass('dropdown'); - expect(group).not.toHaveClass('dropup'); - }); - - it('renders div with dropup class', () => { - // when - render( - - {dropdownChildren} - - ); - - // then - const group = screen.getByRole('menu').parentElement; - expect(group.tagName).toBe('DIV'); - expect(group).not.toHaveClass('dropdown'); - expect(group).toHaveClass('dropup'); - }); - - it('renders toggle with Dropdown.Toggle', () => { - // when - render(simpleDropdown); - - // then - const toggle = screen.getByRole('button', { name: 'Child Title' }); - expect(toggle.tagName).toBe('BUTTON'); - expect(toggle).toHaveClass('btn btn-default dropdown-toggle'); - expect(toggle).toHaveAttribute('type', 'button'); - expect(toggle).toHaveAttribute('aria-expanded', 'false'); - }); - - it('renders dropdown toggle button caret', () => { - // when - render(simpleDropdown); - - // then - const btn = screen.getByRole('button', { name: 'Child Title' }); - expect(btn.querySelector('span.caret')).toBeTruthy(); - }); - - it('does not render toggle button caret', () => { - // when - render(Child Text); - - // then - const caret = screen.getByRole('button', { name: 'Child Text' }); - expect(caret.querySelector('.caret')).toBeFalsy(); - }); - - it('renders custom menu', () => { - // when - render( - - Child Text - - - Item 1 - - - ); - - // then - expect(screen.getByRole('menu')).toBeInTheDocument(); - expect(screen.getByRole('menu')).toHaveClass('custom-menu'); - }); - - it('forwards pullRight to menu', () => { - // when - render( - - {dropdownChildren} - - ); - - // then - expect(screen.getByRole('menu')).toHaveClass('dropdown-menu-right'); - }); - - // NOTE: The onClick event handler is invoked for both the Enter and Space - // keys as well since the component is a button. I cannot figure out how to - // get ReactTestUtils to simulate such though. - it('toggles open/closed when clicked', () => { - // given - render(simpleDropdown); - expect(screen.getByTestId('test-id')).not.toHaveClass('open'); - expect(screen.getByRole('button')).toHaveAttribute( - 'aria-expanded', - 'false' - ); - - // when - userEvent.click(screen.getByRole('button')); - - // then - expect(screen.getByTestId('test-id')).toHaveClass('open'); - expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'); - - // when - userEvent.click(screen.getByRole('button')); - - // then - expect(screen.getByTestId('test-id')).not.toHaveClass('open'); - expect(screen.getByRole('button')).toHaveAttribute( - 'aria-expanded', - 'false' - ); - }); - - it('closes when clicked outside', () => { - // given - render(simpleDropdown); - expect(screen.getByTestId('test-id')).not.toHaveClass('open'); - expect(screen.getByRole('button')).toHaveAttribute( - 'aria-expanded', - 'false' - ); - - // when - userEvent.click(screen.getByRole('button')); - - // then - expect(screen.getByTestId('test-id')).toHaveClass('open'); - expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'); - - // when - userEvent.click(document.body); - - // then - expect(screen.getByTestId('test-id')).not.toHaveClass('open'); - expect(screen.getByRole('button')).toHaveAttribute( - 'aria-expanded', - 'false' - ); - }); - - it('closes when mousedown outside if rootCloseEvent set', () => { - // given - render( - - {dropdownChildren} - - ); - expect(screen.getByTestId('test-id')).not.toHaveClass('open'); - expect(screen.getByRole('button')).toHaveAttribute( - 'aria-expanded', - 'false' - ); - - // when - userEvent.click(screen.getByRole('button')); - - // then - expect(screen.getByTestId('test-id')).toHaveClass('open'); - expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'); - - // when - fireEvent.mouseDown(document.body); - - // then - expect(screen.getByTestId('test-id')).not.toHaveClass('open'); - expect(screen.getByRole('button')).toHaveAttribute( - 'aria-expanded', - 'false' - ); - }); - - it('opens if dropdown contains no focusable menu item', () => { - // given - render( - - Toggle - -
  • Some custom nonfocusable content
  • -
    -
    - ); - - // when - userEvent.click(screen.getByRole('button')); - - // then - expect(screen.getByTestId('dropdown')).toHaveClass('open'); - }); - - it('when focused and closed toggles open when the key "down" is pressed', () => { - // given - render(simpleDropdown); - - // when - fireEvent.keyDown(screen.getByRole('button'), { - key: 'ArrowDown', - code: 'ArrowDown', - keyCode: keycode('down'), - charCode: keycode('down'), - }); - - // then - expect(screen.getByTestId('test-id')).toHaveClass('open'); - expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'); - }); - - it('button has aria-haspopup attribute (As per W3C WAI-ARIA Spec)', () => { - // when - render(simpleDropdown); - - // then - expect(screen.getByRole('button')).toHaveAttribute('aria-haspopup', 'true'); - }); - - it('does not pass onSelect to DOM node', () => { - // given - const onSelect = jest.fn(); - render( - - {dropdownChildren} - - ); - expect(onSelect).not.toBeCalled(); - - // when - userEvent.click(screen.getByRole('button')); - userEvent.click(screen.getByRole('menuitem', { name: 'Item 4' })); - - // then - expect(onSelect).toBeCalled(); - }); - - it('closes when child MenuItem is selected', () => { - // given - render( - - {dropdownChildren} - - ); - expect(screen.getByTestId('test-id')).not.toHaveClass('open'); - expect(screen.getByRole('button')).toHaveAttribute( - 'aria-expanded', - 'false' - ); - - // when - userEvent.click(screen.getByRole('button')); - - // then - expect(screen.getByTestId('test-id')).toHaveClass('open'); - expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'); - - // when - userEvent.click(screen.getByRole('menuitem', { name: 'Item 4' })); - - // then - expect(screen.getByTestId('test-id')).not.toHaveClass('open'); - expect(screen.getByRole('button')).toHaveAttribute( - 'aria-expanded', - 'false' - ); - }); - - it('does not close when onToggle is controlled', () => { - // given - const handleSelect = jest.fn(); - render( - - {dropdownChildren} - - ); - - // when - userEvent.click(screen.getByRole('button')); - expect(screen.getByTestId('test-id')).toHaveClass('open'); - userEvent.click(screen.getByRole('menuitem', { name: 'Item 4' })); - - // then - expect(screen.getByTestId('test-id')).toHaveClass('open'); - }); - - it('is open with explicit prop', () => { - // given - function OpenProp() { - const [open, setOpen] = useState(false); - - return ( -
    - - {}} - title="Prop open control" - data-testid="test-id" - id="lol" - > - {dropdownChildren} - -
    - ); - } - - render(); - expect(screen.getByTestId('test-id')).not.toHaveClass('open'); - - // when - userEvent.click(screen.getByRole('button', { name: 'Outer button' })); - - // then - expect(screen.getByTestId('test-id')).toHaveClass('open'); - - // when - userEvent.click(screen.getByRole('button', { name: 'Outer button' })); - - // then - expect(screen.getByTestId('test-id')).not.toHaveClass('open'); - }); - - it('has aria-labelledby same id as ezz toggle button', () => { - // when - render(simpleDropdown); - - // then - const id = screen.getByRole('button').getAttribute('id'); - expect(screen.getByRole('menu')).toHaveAttribute('aria-labelledby', id); - }); - - describe('PropType validation', () => { - describe('children', () => { - const originalConsoleError = console.error; - - beforeEach(() => { - console.error = jest.fn(); - }); - - afterEach(() => { - console.error = originalConsoleError; - }); - - xit('menu is exclusive', () => { - // when - render( - - - - - - ); - - // then - expect(console.error.mock.calls[0]).toContain( - '(children) Dropdown - Duplicate children detected of bsRole: menu. Only one child each allowed with the following bsRoles: menu' - ); - }); - - xit('menu is required', () => { - // Dropdowns can't render without a menu. - render( - - - - ); - - // then - expect(console.error.mock.calls[0][0]).toContain( - 'Warning: Failed prop type: (children) Dropdown - Missing a required child with bsRole: menu. Dropdown must have at least one child of each of the following bsRoles: toggle, menu' - ); - }); - - xit('toggles are not exclusive', () => { - // when - render( - - - - - - ); - - // then - expect(console.error).not.toBeCalled(); - }); - - xit('toggle is required', () => { - // when - render( - - - - ); - - // then - expect(console.error.mock.calls[0]).toContain( - '(children) Dropdown - Missing a required child with bsRole: toggle. Dropdown must have at least one child of each of the following bsRoles: toggle, menu' - ); - }); - }); - }); - - describe('ref', () => { - const originalConsoleError = console.error; - - beforeEach(() => { - console.error = jest.fn(); - }); - - afterEach(() => { - console.error = originalConsoleError; - }); - - it('chains refs', () => { - // given - function RefDropdown() { - const [hasBaseRef, setHasBaseRef] = useState(false); - const [hasToggleRef, setHasToggleRef] = useState(false); - const [hasMenuRef, setHasMenuRef] = useState(false); - - const setBaseRef = () => { - setHasBaseRef(true); - }; - const setToggleRef = () => { - setHasToggleRef(true); - }; - const setMenuRef = () => { - setHasMenuRef(true); - }; - - return ( - <> - - - - - {hasBaseRef &&
    } - {hasToggleRef &&
    } - {hasMenuRef &&
    } - - ); - } - - // when - render(); - - // then - expect(screen.getByTestId('baseRefSet')).toBeInTheDocument(); - expect(screen.getByTestId('toggleRefSet')).toBeInTheDocument(); - expect(screen.getByTestId('menuRefSet')).toBeInTheDocument(); - }); - - xit('warns when a string ref is specified', () => { - // given - function RefDropdown() { - return ( - - - - - ); - } - - // when - render(); - - // then - expect(console.error.mock.calls[0][0]).toContain( - 'String refs are not supported' - ); - }); - }); - - describe('focusable state', () => { - let focusableContainer; - - beforeEach(() => { - focusableContainer = document.createElement('div'); - document.body.appendChild(focusableContainer); - }); - - afterEach(() => { - ReactDOM.unmountComponentAtNode(focusableContainer); - document.body.removeChild(focusableContainer); - }); - - it('when focused and closed sets focus on first menu item when the key "down" is pressed', () => { - // given - render(simpleDropdown, { container: focusableContainer }); - - // when - fireEvent.focus(screen.getByRole('button')); - fireEvent.keyDown(screen.getByRole('button'), { - key: 'ArrowDown', - keyCode: keycode('down'), - }); - - // then - expect(screen.getByRole('menuitem', { name: 'Item 1' })).toHaveFocus(); - }); - - it('when focused and open does not toggle closed when the key "down" is pressed', () => { - // given - render(simpleDropdown); - - // when - userEvent.click(screen.getByRole('button')); - fireEvent.keyDown(screen.getByRole('button'), { - key: 'ArrowDown', - keyCode: keycode('down'), - }); - - // then - expect(screen.getByRole('button')).toHaveAttribute( - 'aria-expanded', - 'true' - ); - expect(screen.getByTestId('test-id')).toHaveClass('open'); - }); - - // This test is more complicated then it appears to need. This is - // because there was an intermittent failure of the test when not structured this way - // The failure occurred when all tests in the suite were run together, but not a subset of the tests. - // - // I am fairly confident that the failure is due to a test specific conflict and not an actual bug. - it('when open and the key "esc" is pressed the menu is closed and focus is returned to the button', () => { - // given - render( - - {dropdownChildren} - , - { container: focusableContainer } - ); - const firstItem = screen.getByRole('menuitem', { name: 'Item 1' }); - expect(firstItem).toHaveFocus(); - - // when - fireEvent.keyDown(firstItem, { - key: 'Escape', - keyCode: keycode('esc'), - }); - - // then - expect(screen.getByRole('button')).toHaveFocus(); - expect(screen.getByTestId('test-id')).not.toHaveClass('open'); - }); - - it('when open and the key "tab" is pressed the menu is closed and focus is progress to the next focusable element', () => { - // given - render( - - {simpleDropdown} - - , - { attachTo: focusableContainer } - ); - - // when - userEvent.click(screen.getByRole('button')); - expect(screen.getByRole('button')).toHaveAttribute( - 'aria-expanded', - 'true' - ); - fireEvent.keyDown(screen.getByRole('button'), { - key: 'Tab', - keyCode: keycode('tab'), - }); - - // then - expect(screen.getByRole('button')).toHaveAttribute( - 'aria-expanded', - 'false' - ); - }); - }); - - describe('DOM event and source passed to onToggle', () => { - let focusableContainer; - - beforeEach(() => { - focusableContainer = document.createElement('div'); - document.body.appendChild(focusableContainer); - }); - - afterEach(() => { - ReactDOM.unmountComponentAtNode(focusableContainer); - document.body.removeChild(focusableContainer); - }); - - it('passes open, event, and source correctly when opened with click', () => { - // given - const onToggle = jest.fn(); - render( - - {dropdownChildren} - - ); - expect(onToggle).not.toHaveBeenCalled(); - - // when - userEvent.click(screen.getByRole('button')); - - // then - expect(onToggle).toHaveBeenCalledWith(true, expect.any(Object), { - source: 'click', - }); - }); - - it('passes open, event, and source correctly when closed with click', () => { - // given - const onToggle = jest.fn(); - render( - - {dropdownChildren} - - ); - expect(onToggle).not.toHaveBeenCalled(); - - // when - userEvent.click(screen.getByRole('button')); - expect(onToggle).toHaveBeenCalledTimes(1); - userEvent.click(screen.getByRole('button')); - - // then - expect(onToggle.mock.calls.length).toBeGreaterThanOrEqual(2); - expect(onToggle).toHaveBeenCalledWith(false, expect.any(Object), { - source: 'click', - }); - }); - - it('passes open, event, and source correctly when child selected', () => { - // given - const onToggle = jest.fn(); - render( - - Child Title - - Item 1 - - - ); - - // when - userEvent.click(screen.getByRole('button')); - expect(onToggle).toBeCalledTimes(1); - userEvent.click(screen.getByRole('menuitem', { name: 'Item 1' })); - - // then - expect(onToggle).toBeCalledTimes(2); - expect(onToggle).toHaveBeenLastCalledWith(false, expect.any(Object), { - source: 'select', - }); - }); - - it('passes open, event, and source correctly when opened with keydown', () => { - // given - const onToggle = jest.fn(); - render( - - {dropdownChildren} - - ); - - // when - fireEvent.keyDown(screen.getByRole('button'), { - key: 'ArrowDown', - keyCode: keycode('down'), - }); - - // then - expect(onToggle).toHaveBeenCalledTimes(1); - expect(onToggle).toHaveBeenCalledWith(true, expect.any(Object), { - source: 'keydown', - }); - }); - }); - - it('should derive bsClass from parent', () => { - // when - render( - - Child Title - - Item 1 - - - ); - - // then - expect(screen.getByRole('button')).toHaveClass('my-dropdown-toggle'); - expect(screen.getByRole('menu')).toHaveClass('my-dropdown-menu'); - }); + const dropdownChildren = [ + Child Title, + + Item 1 + Item 2 + Item 3 + Item 4 + , + ]; + + const simpleDropdown = ( + + {dropdownChildren} + + ); + + it('renders div with dropdown class', () => { + // when + render({dropdownChildren}); + + // then + const group = screen.getByRole('menu').parentElement; + expect(group.tagName).toBe('DIV'); + expect(group).toHaveClass('dropdown'); + expect(group).not.toHaveClass('dropup'); + }); + + it('renders div with dropup class', () => { + // when + render( + + {dropdownChildren} + , + ); + + // then + const group = screen.getByRole('menu').parentElement; + expect(group.tagName).toBe('DIV'); + expect(group).not.toHaveClass('dropdown'); + expect(group).toHaveClass('dropup'); + }); + + it('renders toggle with Dropdown.Toggle', () => { + // when + render(simpleDropdown); + + // then + const toggle = screen.getByRole('button', { name: 'Child Title' }); + expect(toggle.tagName).toBe('BUTTON'); + expect(toggle).toHaveClass('btn btn-default dropdown-toggle'); + expect(toggle).toHaveAttribute('type', 'button'); + expect(toggle).toHaveAttribute('aria-expanded', 'false'); + }); + + it('renders dropdown toggle button caret', () => { + // when + render(simpleDropdown); + + // then + const btn = screen.getByRole('button', { name: 'Child Title' }); + expect(btn.querySelector('span.caret')).toBeTruthy(); + }); + + it('does not render toggle button caret', () => { + // when + render(Child Text); + + // then + const caret = screen.getByRole('button', { name: 'Child Text' }); + expect(caret.querySelector('.caret')).toBeFalsy(); + }); + + it('renders custom menu', () => { + // when + render( + + Child Text + + + Item 1 + + , + ); + + // then + expect(screen.getByRole('menu')).toBeInTheDocument(); + expect(screen.getByRole('menu')).toHaveClass('custom-menu'); + }); + + it('forwards pullRight to menu', () => { + // when + render( + + {dropdownChildren} + , + ); + + // then + expect(screen.getByRole('menu')).toHaveClass('dropdown-menu-right'); + }); + + // NOTE: The onClick event handler is invoked for both the Enter and Space + // keys as well since the component is a button. I cannot figure out how to + // get ReactTestUtils to simulate such though. + it('toggles open/closed when clicked', async () => { + const user = userEvent.setup(); + + // given + render(simpleDropdown); + expect(screen.getByTestId('test-id')).not.toHaveClass('open'); + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false'); + + // when + await user.click(screen.getByRole('button')); + + // then + expect(screen.getByTestId('test-id')).toHaveClass('open'); + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'); + + // when + await user.click(screen.getByRole('button')); + + // then + expect(screen.getByTestId('test-id')).not.toHaveClass('open'); + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false'); + }); + + it('closes when clicked outside', async () => { + const user = userEvent.setup(); + + // given + render(simpleDropdown); + expect(screen.getByTestId('test-id')).not.toHaveClass('open'); + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false'); + + // when + await user.click(screen.getByRole('button')); + + // then + expect(screen.getByTestId('test-id')).toHaveClass('open'); + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'); + + // when + await user.click(document.body); + + // then + expect(screen.getByTestId('test-id')).not.toHaveClass('open'); + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false'); + }); + + it('closes when mousedown outside if rootCloseEvent set', async () => { + const user = userEvent.setup(); + + // given + render( + + {dropdownChildren} + , + ); + expect(screen.getByTestId('test-id')).not.toHaveClass('open'); + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false'); + + // when + await user.click(screen.getByRole('button')); + + // then + expect(screen.getByTestId('test-id')).toHaveClass('open'); + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'); + + // when + fireEvent.mouseDown(document.body); + + // then + expect(screen.getByTestId('test-id')).not.toHaveClass('open'); + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false'); + }); + + it('opens if dropdown contains no focusable menu item', async () => { + const user = userEvent.setup(); + + // given + render( + + Toggle + +
  • Some custom nonfocusable content
  • +
    +
    , + ); + + // when + await user.click(screen.getByRole('button')); + + // then + expect(screen.getByTestId('dropdown')).toHaveClass('open'); + }); + + it('when focused and closed toggles open when the key "down" is pressed', async () => { + const user = userEvent.setup(); + + // given + render(simpleDropdown); + + // when + screen.getByRole('button').focus(); + await user.keyboard('[ArrowDown]'); + + // then + expect(screen.getByTestId('test-id')).toHaveClass('open'); + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'); + }); + + it('button has aria-haspopup attribute (As per W3C WAI-ARIA Spec)', () => { + // when + render(simpleDropdown); + + // then + expect(screen.getByRole('button')).toHaveAttribute('aria-haspopup', 'true'); + }); + + it('does not pass onSelect to DOM node', async () => { + const user = userEvent.setup(); + + // given + const onSelect = jest.fn(); + render( + + {dropdownChildren} + , + ); + expect(onSelect).not.toHaveBeenCalled(); + + // when + await user.click(screen.getByRole('button')); + await user.click(screen.getByRole('menuitem', { name: 'Item 4' })); + + // then + expect(onSelect).toHaveBeenCalled(); + }); + + it('closes when child MenuItem is selected', async () => { + const user = userEvent.setup(); + + // given + render( + + {dropdownChildren} + , + ); + expect(screen.getByTestId('test-id')).not.toHaveClass('open'); + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false'); + + // when + await user.click(screen.getByRole('button')); + + // then + expect(screen.getByTestId('test-id')).toHaveClass('open'); + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'); + + // when + await user.click(screen.getByRole('menuitem', { name: 'Item 4' })); + + // then + expect(screen.getByTestId('test-id')).not.toHaveClass('open'); + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false'); + }); + + it('does not close when onToggle is controlled', async () => { + const user = userEvent.setup(); + + // given + const handleSelect = jest.fn(); + render( + + {dropdownChildren} + , + ); + + // when + await user.click(screen.getByRole('button')); + expect(screen.getByTestId('test-id')).toHaveClass('open'); + await user.click(screen.getByRole('menuitem', { name: 'Item 4' })); + + // then + expect(screen.getByTestId('test-id')).toHaveClass('open'); + }); + + it('is open with explicit prop', async () => { + const user = userEvent.setup(); + + // given + function OpenProp() { + const [open, setOpen] = useState(false); + + return ( +
    + + {}} + title="Prop open control" + data-testid="test-id" + id="lol" + > + {dropdownChildren} + +
    + ); + } + + render(); + expect(screen.getByTestId('test-id')).not.toHaveClass('open'); + + // when + await user.click(screen.getByRole('button', { name: 'Outer button' })); + + // then + expect(screen.getByTestId('test-id')).toHaveClass('open'); + + // when + await user.click(screen.getByRole('button', { name: 'Outer button' })); + + // then + expect(screen.getByTestId('test-id')).not.toHaveClass('open'); + }); + + it('has aria-labelledby same id as ezz toggle button', () => { + // when + render(simpleDropdown); + + // then + const id = screen.getByRole('button').getAttribute('id'); + expect(screen.getByRole('menu')).toHaveAttribute('aria-labelledby', id); + }); + + describe('PropType validation', () => { + describe('children', () => { + const originalConsoleError = console.error; + + beforeEach(() => { + console.error = jest.fn(); + }); + + afterEach(() => { + console.error = originalConsoleError; + }); + + xit('menu is exclusive', () => { + // when + render( + + + + + , + ); + + // then + expect(console.error.mock.calls[0]).toContain( + '(children) Dropdown - Duplicate children detected of bsRole: menu. Only one child each allowed with the following bsRoles: menu', + ); + }); + + xit('menu is required', () => { + // Dropdowns can't render without a menu. + render( + + + , + ); + + // then + expect(console.error.mock.calls[0][0]).toContain( + 'Warning: Failed prop type: (children) Dropdown - Missing a required child with bsRole: menu. Dropdown must have at least one child of each of the following bsRoles: toggle, menu', + ); + }); + + xit('toggles are not exclusive', () => { + // when + render( + + + + + , + ); + + // then + expect(console.error).not.toHaveBeenCalled(); + }); + + xit('toggle is required', () => { + // when + render( + + + , + ); + + // then + expect(console.error.mock.calls[0]).toContain( + '(children) Dropdown - Missing a required child with bsRole: toggle. Dropdown must have at least one child of each of the following bsRoles: toggle, menu', + ); + }); + }); + }); + + describe('ref', () => { + const originalConsoleError = console.error; + + beforeEach(() => { + console.error = jest.fn(); + }); + + afterEach(() => { + console.error = originalConsoleError; + }); + + it('chains refs', () => { + // given + function RefDropdown() { + const [hasBaseRef, setHasBaseRef] = useState(false); + const [hasToggleRef, setHasToggleRef] = useState(false); + const [hasMenuRef, setHasMenuRef] = useState(false); + + const setBaseRef = () => { + setHasBaseRef(true); + }; + const setToggleRef = () => { + setHasToggleRef(true); + }; + const setMenuRef = () => { + setHasMenuRef(true); + }; + + return ( + <> + + + + + {hasBaseRef &&
    } + {hasToggleRef &&
    } + {hasMenuRef &&
    } + + ); + } + + // when + render(); + + // then + expect(screen.getByTestId('baseRefSet')).toBeInTheDocument(); + expect(screen.getByTestId('toggleRefSet')).toBeInTheDocument(); + expect(screen.getByTestId('menuRefSet')).toBeInTheDocument(); + }); + + xit('warns when a string ref is specified', () => { + // given + function RefDropdown() { + return ( + + + + + ); + } + + // when + render(); + + // then + expect(console.error.mock.calls[0][0]).toContain('String refs are not supported'); + }); + }); + + describe('focusable state', () => { + let focusableContainer; + + beforeEach(() => { + focusableContainer = document.createElement('div'); + document.body.appendChild(focusableContainer); + }); + + afterEach(() => { + ReactDOM.unmountComponentAtNode(focusableContainer); + document.body.removeChild(focusableContainer); + }); + + it('when focused and closed sets focus on first menu item when the key "down" is pressed', async () => { + const user = userEvent.setup(); + + // given + render(simpleDropdown, { container: focusableContainer }); + + // when + screen.getByRole('button').focus(); + await user.keyboard('[ArrowDown]'); + + // then + expect(screen.getByRole('menuitem', { name: 'Item 1' })).toHaveFocus(); + }); + + it('when focused and open does not toggle closed when the key "down" is pressed', async () => { + const user = userEvent.setup(); + + // given + render(simpleDropdown); + + // when + await user.click(screen.getByRole('button')); + await user.keyboard('[ArrowDown]'); + + // then + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'); + expect(screen.getByTestId('test-id')).toHaveClass('open'); + }); + + // This test is more complicated then it appears to need. This is + // because there was an intermittent failure of the test when not structured this way + // The failure occurred when all tests in the suite were run together, but not a subset of the tests. + // + // I am fairly confident that the failure is due to a test specific conflict and not an actual bug. + it.only('when open and the key "esc" is pressed the menu is closed and focus is returned to the button', async () => { + const user = userEvent.setup(); + + // given + render( + + {dropdownChildren} + , + { container: focusableContainer }, + ); + const firstItem = screen.getByRole('menuitem', { name: 'Item 1' }); + expect(firstItem).toHaveFocus(); + + // when + await user.keyboard('[Escape]'); + + // then + expect(screen.getByRole('button')).toHaveFocus(); + expect(screen.getByTestId('test-id')).not.toHaveClass('open'); + }); + + it('when open and the key "tab" is pressed the menu is closed and focus is progress to the next focusable element', async () => { + const user = userEvent.setup(); + + // given + render( + + {simpleDropdown} + + , + { attachTo: focusableContainer }, + ); + + // when + await user.click(screen.getByRole('button')); + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'); + await user.keyboard('[Tab]'); + + // then + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false'); + }); + }); + + describe('DOM event and source passed to onToggle', () => { + let focusableContainer; + + beforeEach(() => { + focusableContainer = document.createElement('div'); + document.body.appendChild(focusableContainer); + }); + + afterEach(() => { + ReactDOM.unmountComponentAtNode(focusableContainer); + document.body.removeChild(focusableContainer); + }); + + it('passes open, event, and source correctly when opened with click', async () => { + const user = userEvent.setup(); + + // given + const onToggle = jest.fn(); + render( + + {dropdownChildren} + , + ); + expect(onToggle).not.toHaveBeenCalled(); + + // when + await user.click(screen.getByRole('button')); + + // then + expect(onToggle).toHaveBeenCalledWith(true, expect.any(Object), { + source: 'click', + }); + }); + + it('passes open, event, and source correctly when closed with click', async () => { + const user = userEvent.setup(); + + // given + const onToggle = jest.fn(); + render( + + {dropdownChildren} + , + ); + expect(onToggle).not.toHaveBeenCalled(); + + // when + await user.click(screen.getByRole('button')); + expect(onToggle).toHaveBeenCalledTimes(1); + await user.click(screen.getByRole('button')); + + // then + expect(onToggle.mock.calls.length).toBeGreaterThanOrEqual(2); + expect(onToggle).toHaveBeenCalledWith(false, expect.any(Object), { + source: 'click', + }); + }); + + it('passes open, event, and source correctly when child selected', async () => { + const user = userEvent.setup(); + + // given + const onToggle = jest.fn(); + render( + + Child Title + + Item 1 + + , + ); + + // when + await user.click(screen.getByRole('button')); + expect(onToggle).toHaveBeenCalledTimes(1); + await user.click(screen.getByRole('menuitem', { name: 'Item 1' })); + + // then + expect(onToggle).toHaveBeenCalledTimes(2); + expect(onToggle).toHaveBeenLastCalledWith(false, expect.any(Object), { + source: 'select', + }); + }); + + it('passes open, event, and source correctly when opened with keydown', async () => { + const user = userEvent.setup(); + + // given + const onToggle = jest.fn(); + render( + + {dropdownChildren} + , + ); + + // when + screen.getByRole('button').focus(); + await user.keyboard('[ArrowDown]'); + + // then + expect(onToggle).toHaveBeenCalledTimes(1); + expect(onToggle).toHaveBeenCalledWith(true, expect.any(Object), { + source: 'keydown', + }); + }); + }); + + it('should derive bsClass from parent', () => { + // when + render( + + Child Title + + Item 1 + + , + ); + + // then + expect(screen.getByRole('button')).toHaveClass('my-dropdown-toggle'); + expect(screen.getByRole('menu')).toHaveClass('my-dropdown-menu'); + }); }); diff --git a/fork/react-bootstrap/src/DropdownMenu.js b/fork/react-bootstrap/src/DropdownMenu.js index 249346926e3..347e1462a15 100644 --- a/fork/react-bootstrap/src/DropdownMenu.js +++ b/fork/react-bootstrap/src/DropdownMenu.js @@ -1,5 +1,4 @@ import classNames from 'classnames'; -import keycode from 'keycode'; import React from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; @@ -9,7 +8,7 @@ import { bsClass, getClassSet, prefix, - splitBsPropsAndOmit + splitBsPropsAndOmit, } from './utils/bootstrapUtils'; import createChainedFunction from './utils/createChainedFunction'; import ValidComponentChildren from './utils/ValidComponentChildren'; @@ -20,12 +19,12 @@ const propTypes = { onClose: PropTypes.func, labelledBy: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), onSelect: PropTypes.func, - rootCloseEvent: PropTypes.oneOf(['click', 'mousedown']) + rootCloseEvent: PropTypes.oneOf(['click', 'mousedown']), }; const defaultProps = { bsRole: 'menu', - pullRight: false + pullRight: false, }; class DropdownMenu extends React.Component { @@ -73,17 +72,20 @@ class DropdownMenu extends React.Component { } handleKeyDown(event) { - switch (event.keyCode) { - case keycode.codes.down: + switch (event.key) { + case 'Down': + case 'ArrowDown': this.focusNext(); event.preventDefault(); break; - case keycode.codes.up: + case 'Up': + case 'ArrowUp': this.focusPrevious(); event.preventDefault(); break; - case keycode.codes.esc: - case keycode.codes.tab: + case 'Esc': + case 'Escape': + case 'Tab': this.props.onClose(event, { source: 'keydown' }); break; default: @@ -110,7 +112,7 @@ class DropdownMenu extends React.Component { const classes = { ...getClassSet(bsProps), - [prefix(bsProps, 'right')]: pullRight + [prefix(bsProps, 'right')]: pullRight, }; return ( @@ -125,14 +127,14 @@ class DropdownMenu extends React.Component { className={classNames(className, classes)} aria-labelledby={labelledBy} > - {ValidComponentChildren.map(children, child => + {ValidComponentChildren.map(children, (child) => React.cloneElement(child, { onKeyDown: createChainedFunction( child.props.onKeyDown, - this.handleKeyDown + this.handleKeyDown, ), - onSelect: createChainedFunction(child.props.onSelect, onSelect) - }) + onSelect: createChainedFunction(child.props.onSelect, onSelect), + }), )} diff --git a/fork/react-bootstrap/src/DropdownMenu.test.js b/fork/react-bootstrap/src/DropdownMenu.test.js index 5a5eb908e09..ef69a260936 100644 --- a/fork/react-bootstrap/src/DropdownMenu.test.js +++ b/fork/react-bootstrap/src/DropdownMenu.test.js @@ -1,4 +1,3 @@ -// import keycode from 'keycode'; // import ReactDOM from 'react-dom'; // import ReactTestUtils from 'react-dom/test-utils'; import { screen, render } from '@testing-library/react'; @@ -9,232 +8,232 @@ import MenuItem from './MenuItem'; // import { getOne } from './helpers'; describe('', () => { - const simpleMenu = ( - - Item 1 - Item 2 - Item 3 - Item 4 - - ); - - it('renders ul with dropdown-menu class', () => { - render(simpleMenu); - const node = screen.getByRole('menu'); - expect(node.tagName).toBe('UL'); - expect(node).toHaveClass('dropdown-menu'); - }); - - // xit('has role="menu"', () => { - // const instance = ReactTestUtils.renderIntoDocument(simpleMenu); - // const node = ReactDOM.findDOMNode(instance); - - // node.getAttribute('role').should.equal('menu'); - // }); - - // xit('has aria-labelledby=', () => { - // const instance1 = ReactTestUtils.renderIntoDocument( - // - // ); - // const instance2 = ReactTestUtils.renderIntoDocument( - // - // ); - // const node1 = ReactDOM.findDOMNode(instance1); - // const node2 = ReactDOM.findDOMNode(instance2); - - // node1.getAttribute('aria-labelledby').should.equal('herpa'); - // node2.getAttribute('aria-labelledby').should.equal('derpa'); - // }); - - // xit('forwards onSelect handler to MenuItems', (done) => { - // const selectedEvents = []; - // const onSelect = (eventKey) => { - // selectedEvents.push(eventKey); - - // if (selectedEvents.length === 4) { - // selectedEvents.should.eql(['1', '2', '3', '4']); - // done(); - // } - // }; - // const instance = ReactTestUtils.renderIntoDocument( - // - // Item 1 - // Item 2 - // Item 3 - // Item 4 - // - // ); - - // const menuItems = ReactTestUtils.scryRenderedDOMComponentsWithTag( - // instance, - // 'A' - // ); - - // menuItems.forEach((item) => { - // ReactTestUtils.Simulate.click(item); - // }); - // }); - - // xit('does not pass onSelect to DOM node', () => { - // shallow( {}} />) - // .find('ul') - // .props() - // .should.not.have.property('onSelect'); - // }); - - // xit('applies pull right', () => { - // const instance = ReactTestUtils.renderIntoDocument( - // - // Item - // - // ); - // const node = ReactDOM.findDOMNode(instance); - - // node.className.should.match(/\bdropdown-menu-right\b/); - // }); - - // xit('handles empty children', () => { - // ReactTestUtils.renderIntoDocument( - // - // Item - // {false && Item 2} - // - // ); - // }); - - // describe('focusable state', () => { - // let focusableContainer; - - // beforeEach(() => { - // focusableContainer = document.createElement('div'); - // document.body.appendChild(focusableContainer); - // }); - - // afterEach(() => { - // ReactDOM.unmountComponentAtNode(focusableContainer); - // document.body.removeChild(focusableContainer); - // }); - - // xit('clicking anything outside the menu will request close', () => { - // const requestClose = sinon.stub(); - // const instance = ReactDOM.render( - //
    - // - // - // Item - // - //
    , - // focusableContainer - // ); - - // const button = getOne(instance.getElementsByTagName('button')); - // button.click(); - - // requestClose.should.have.been.calledOnce; - // requestClose.getCall(0).args.length.should.equal(2); - // }); - - // describe('Keyboard Navigation', () => { - // xit('sets focus on next menu item when the key "down" is pressed', () => { - // const instance = ReactDOM.render(simpleMenu, focusableContainer); - - // const items = ReactTestUtils.scryRenderedDOMComponentsWithTag( - // instance, - // 'A' - // ); - // items.length.should.equal(4); - // items[0].focus(); - - // for (let i = 1; i < items.length; i++) { - // ReactTestUtils.Simulate.keyDown(document.activeElement, { - // keyCode: keycode('down'), - // }); - // document.activeElement.should.equal(items[i]); - // } - // }); - - // xit('with last item is focused when the key "down" is pressed first item gains focus', () => { - // const instance = ReactDOM.render(simpleMenu, focusableContainer); - - // const items = ReactTestUtils.scryRenderedDOMComponentsWithTag( - // instance, - // 'A' - // ); - // items.length.should.equal(4); - // items[3].focus(); - - // ReactTestUtils.Simulate.keyDown(document.activeElement, { - // keyCode: keycode('down'), - // }); - // document.activeElement.should.equal(items[0]); - // }); - - // xit('sets focus on previous menu item when the key "up" is pressed', () => { - // const instance = ReactDOM.render(simpleMenu, focusableContainer); - - // const items = ReactTestUtils.scryRenderedDOMComponentsWithTag( - // instance, - // 'A' - // ); - // items.length.should.equal(4); - // items[3].focus(); - - // for (let i = 2; i >= 0; i--) { - // ReactTestUtils.Simulate.keyDown(document.activeElement, { - // keyCode: keycode('up'), - // }); - // document.activeElement.should.equal(items[i]); - // } - // }); - - // xit('with first item focused when the key "up" is pressed last item gains focus', () => { - // const instance = ReactDOM.render(simpleMenu, focusableContainer); - - // const items = ReactTestUtils.scryRenderedDOMComponentsWithTag( - // instance, - // 'A' - // ); - // items.length.should.equal(4); - // items[0].focus(); - - // ReactTestUtils.Simulate.keyDown(document.activeElement, { - // keyCode: keycode('up'), - // }); - // document.activeElement.should.equal(items[3]); - // }); - - // ['esc', 'tab'].forEach((key) => { - // xit(`when the key "${key}" is pressed the requestClose prop is invoked with the originating event`, () => { - // const requestClose = sinon.spy(); - // const instance = ReactDOM.render( - // - // Item - // , - // focusableContainer - // ); - - // const item = ReactTestUtils.findRenderedDOMComponentWithTag( - // instance, - // 'A' - // ); - - // ReactTestUtils.Simulate.keyDown(item, { keyCode: keycode(key) }); - - // requestClose.should.have.been.calledOnce; - // requestClose.getCall(0).args[0].keyCode.should.equal(keycode(key)); - // }); - // }); - // }); - // }); - - // xit('Should pass props to dropdown', () => { - // let instance = ReactTestUtils.renderIntoDocument( - // - // MenuItem 1 content - // - // ); - - // let node = ReactDOM.findDOMNode(instance); - // assert.ok(node.className.match(/\bnew-fancy-class\b/)); - // }); + const simpleMenu = ( + + Item 1 + Item 2 + Item 3 + Item 4 + + ); + + it('renders ul with dropdown-menu class', () => { + render(simpleMenu); + const node = screen.getByRole('menu'); + expect(node.tagName).toBe('UL'); + expect(node).toHaveClass('dropdown-menu'); + }); + + // xit('has role="menu"', () => { + // const instance = ReactTestUtils.renderIntoDocument(simpleMenu); + // const node = ReactDOM.findDOMNode(instance); + + // node.getAttribute('role').should.equal('menu'); + // }); + + // xit('has aria-labelledby=', () => { + // const instance1 = ReactTestUtils.renderIntoDocument( + // + // ); + // const instance2 = ReactTestUtils.renderIntoDocument( + // + // ); + // const node1 = ReactDOM.findDOMNode(instance1); + // const node2 = ReactDOM.findDOMNode(instance2); + + // node1.getAttribute('aria-labelledby').should.equal('herpa'); + // node2.getAttribute('aria-labelledby').should.equal('derpa'); + // }); + + // xit('forwards onSelect handler to MenuItems', (done) => { + // const selectedEvents = []; + // const onSelect = (eventKey) => { + // selectedEvents.push(eventKey); + + // if (selectedEvents.length === 4) { + // selectedEvents.should.eql(['1', '2', '3', '4']); + // done(); + // } + // }; + // const instance = ReactTestUtils.renderIntoDocument( + // + // Item 1 + // Item 2 + // Item 3 + // Item 4 + // + // ); + + // const menuItems = ReactTestUtils.scryRenderedDOMComponentsWithTag( + // instance, + // 'A' + // ); + + // menuItems.forEach((item) => { + // ReactTestUtils.Simulate.click(item); + // }); + // }); + + // xit('does not pass onSelect to DOM node', () => { + // shallow( {}} />) + // .find('ul') + // .props() + // .should.not.have.property('onSelect'); + // }); + + // xit('applies pull right', () => { + // const instance = ReactTestUtils.renderIntoDocument( + // + // Item + // + // ); + // const node = ReactDOM.findDOMNode(instance); + + // node.className.should.match(/\bdropdown-menu-right\b/); + // }); + + // xit('handles empty children', () => { + // ReactTestUtils.renderIntoDocument( + // + // Item + // {false && Item 2} + // + // ); + // }); + + // describe('focusable state', () => { + // let focusableContainer; + + // beforeEach(() => { + // focusableContainer = document.createElement('div'); + // document.body.appendChild(focusableContainer); + // }); + + // afterEach(() => { + // ReactDOM.unmountComponentAtNode(focusableContainer); + // document.body.removeChild(focusableContainer); + // }); + + // xit('clicking anything outside the menu will request close', () => { + // const requestClose = sinon.stub(); + // const instance = ReactDOM.render( + //
    + // + // + // Item + // + //
    , + // focusableContainer + // ); + + // const button = getOne(instance.getElementsByTagName('button')); + // button.click(); + + // requestClose.should.have.been.calledOnce; + // requestClose.getCall(0).args.length.should.equal(2); + // }); + + // describe('Keyboard Navigation', () => { + // xit('sets focus on next menu item when the key "down" is pressed', () => { + // const instance = ReactDOM.render(simpleMenu, focusableContainer); + + // const items = ReactTestUtils.scryRenderedDOMComponentsWithTag( + // instance, + // 'A' + // ); + // items.length.should.equal(4); + // items[0].focus(); + + // for (let i = 1; i < items.length; i++) { + // ReactTestUtils.Simulate.keyDown(document.activeElement, { + // keyCode: keycode('down'), + // }); + // document.activeElement.should.equal(items[i]); + // } + // }); + + // xit('with last item is focused when the key "down" is pressed first item gains focus', () => { + // const instance = ReactDOM.render(simpleMenu, focusableContainer); + + // const items = ReactTestUtils.scryRenderedDOMComponentsWithTag( + // instance, + // 'A' + // ); + // items.length.should.equal(4); + // items[3].focus(); + + // ReactTestUtils.Simulate.keyDown(document.activeElement, { + // keyCode: keycode('down'), + // }); + // document.activeElement.should.equal(items[0]); + // }); + + // xit('sets focus on previous menu item when the key "up" is pressed', () => { + // const instance = ReactDOM.render(simpleMenu, focusableContainer); + + // const items = ReactTestUtils.scryRenderedDOMComponentsWithTag( + // instance, + // 'A' + // ); + // items.length.should.equal(4); + // items[3].focus(); + + // for (let i = 2; i >= 0; i--) { + // ReactTestUtils.Simulate.keyDown(document.activeElement, { + // keyCode: keycode('up'), + // }); + // document.activeElement.should.equal(items[i]); + // } + // }); + + // xit('with first item focused when the key "up" is pressed last item gains focus', () => { + // const instance = ReactDOM.render(simpleMenu, focusableContainer); + + // const items = ReactTestUtils.scryRenderedDOMComponentsWithTag( + // instance, + // 'A' + // ); + // items.length.should.equal(4); + // items[0].focus(); + + // ReactTestUtils.Simulate.keyDown(document.activeElement, { + // keyCode: keycode('up'), + // }); + // document.activeElement.should.equal(items[3]); + // }); + + // ['esc', 'tab'].forEach((key) => { + // xit(`when the key "${key}" is pressed the requestClose prop is invoked with the originating event`, () => { + // const requestClose = sinon.spy(); + // const instance = ReactDOM.render( + // + // Item + // , + // focusableContainer + // ); + + // const item = ReactTestUtils.findRenderedDOMComponentWithTag( + // instance, + // 'A' + // ); + + // ReactTestUtils.Simulate.keyDown(item, { keyCode: keycode(key) }); + + // requestClose.should.have.been.calledOnce; + // requestClose.getCall(0).args[0].keyCode.should.equal(keycode(key)); + // }); + // }); + // }); + // }); + + // xit('Should pass props to dropdown', () => { + // let instance = ReactTestUtils.renderIntoDocument( + // + // MenuItem 1 content + // + // ); + + // let node = ReactDOM.findDOMNode(instance); + // assert.ok(node.className.match(/\bnew-fancy-class\b/)); + // }); }); diff --git a/fork/react-bootstrap/src/Nav.js b/fork/react-bootstrap/src/Nav.js index 9aa6123e63d..350a818a084 100644 --- a/fork/react-bootstrap/src/Nav.js +++ b/fork/react-bootstrap/src/Nav.js @@ -1,18 +1,11 @@ import classNames from 'classnames'; -import keycode from 'keycode'; import React, { cloneElement } from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import all from 'prop-types-extra/lib/all'; import warning from 'warning'; -import { - bsClass, - bsStyles, - getClassSet, - prefix, - splitBsProps -} from './utils/bootstrapUtils'; +import { bsClass, bsStyles, getClassSet, prefix, splitBsProps } from './utils/bootstrapUtils'; import createChainedFunction from './utils/createChainedFunction'; import ValidComponentChildren from './utils/ValidComponentChildren'; @@ -24,333 +17,324 @@ import ValidComponentChildren from './utils/ValidComponentChildren'; // Consider renaming or replacing them. const propTypes = { - /** - * Marks the NavItem with a matching `eventKey` as active. Has a - * higher precedence over `activeHref`. - */ - activeKey: PropTypes.any, - - /** - * Marks the child NavItem with a matching `href` prop as active. - */ - activeHref: PropTypes.string, - - /** - * NavItems are be positioned vertically. - */ - stacked: PropTypes.bool, - - justified: all( - PropTypes.bool, - ({ justified, navbar }) => - justified && navbar - ? Error('justified navbar `Nav`s are not supported') - : null - ), - - /** - * A callback fired when a NavItem is selected. - * - * ```js - * function ( - * Any eventKey, - * SyntheticEvent event? - * ) - * ``` - */ - onSelect: PropTypes.func, - - /** - * ARIA role for the Nav, in the context of a TabContainer, the default will - * be set to "tablist", but can be overridden by the Nav when set explicitly. - * - * When the role is set to "tablist" NavItem focus is managed according to - * the ARIA authoring practices for tabs: - * https://www.w3.org/TR/2013/WD-wai-aria-practices-20130307/#tabpanel - */ - role: PropTypes.string, - - /** - * Apply styling an alignment for use in a Navbar. This prop will be set - * automatically when the Nav is used inside a Navbar. - */ - navbar: PropTypes.bool, - - /** - * Float the Nav to the right. When `navbar` is `true` the appropriate - * contextual classes are added as well. - */ - pullRight: PropTypes.bool, - - /** - * Float the Nav to the left. When `navbar` is `true` the appropriate - * contextual classes are added as well. - */ - pullLeft: PropTypes.bool + /** + * Marks the NavItem with a matching `eventKey` as active. Has a + * higher precedence over `activeHref`. + */ + activeKey: PropTypes.any, + + /** + * Marks the child NavItem with a matching `href` prop as active. + */ + activeHref: PropTypes.string, + + /** + * Chidlren. + */ + children: PropTypes.node, + + /** + * Css classname. + */ + className: PropTypes.string, + + /** + * NavItems are be positioned vertically. + */ + stacked: PropTypes.bool, + + justified: all(PropTypes.bool, ({ justified, navbar }) => + justified && navbar ? Error('justified navbar `Nav`s are not supported') : null, + ), + + /** + * A callback fired when a NavItem is selected. + * + * ```js + * function ( + * Any eventKey, + * SyntheticEvent event? + * ) + * ``` + */ + onSelect: PropTypes.func, + + /** + * ARIA role for the Nav, in the context of a TabContainer, the default will + * be set to "tablist", but can be overridden by the Nav when set explicitly. + * + * When the role is set to "tablist" NavItem focus is managed according to + * the ARIA authoring practices for tabs: + * https://www.w3.org/TR/2013/WD-wai-aria-practices-20130307/#tabpanel + */ + role: PropTypes.string, + + /** + * Apply styling an alignment for use in a Navbar. This prop will be set + * automatically when the Nav is used inside a Navbar. + */ + navbar: PropTypes.bool, + + /** + * Float the Nav to the right. When `navbar` is `true` the appropriate + * contextual classes are added as well. + */ + pullRight: PropTypes.bool, + + /** + * Float the Nav to the left. When `navbar` is `true` the appropriate + * contextual classes are added as well. + */ + pullLeft: PropTypes.bool, }; const defaultProps = { - justified: false, - pullRight: false, - pullLeft: false, - stacked: false + justified: false, + pullRight: false, + pullLeft: false, + stacked: false, }; const contextTypes = { - $bs_navbar: PropTypes.shape({ - bsClass: PropTypes.string, - onSelect: PropTypes.func - }), - - $bs_tabContainer: PropTypes.shape({ - activeKey: PropTypes.any, - onSelect: PropTypes.func.isRequired, - getTabId: PropTypes.func.isRequired, - getPaneId: PropTypes.func.isRequired - }) + $bs_navbar: PropTypes.shape({ + bsClass: PropTypes.string, + onSelect: PropTypes.func, + }), + + $bs_tabContainer: PropTypes.shape({ + activeKey: PropTypes.any, + onSelect: PropTypes.func.isRequired, + getTabId: PropTypes.func.isRequired, + getPaneId: PropTypes.func.isRequired, + }), }; class Nav extends React.Component { - componentDidUpdate() { - if (!this._needsRefocus) { - return; - } - - this._needsRefocus = false; - - const { children } = this.props; - const { activeKey, activeHref } = this.getActiveProps(); - - const activeChild = ValidComponentChildren.find(children, child => - this.isActive(child, activeKey, activeHref) - ); - - const childrenArray = ValidComponentChildren.toArray(children); - const activeChildIndex = childrenArray.indexOf(activeChild); - - const childNodes = ReactDOM.findDOMNode(this).children; - const activeNode = childNodes && childNodes[activeChildIndex]; - - if (!activeNode || !activeNode.firstChild) { - return; - } - - activeNode.firstChild.focus(); - } - - getActiveProps() { - const tabContainer = this.context.$bs_tabContainer; - - if (tabContainer) { - warning( - this.props.activeKey == null && !this.props.activeHref, - 'Specifying a `