diff --git a/.changeset/orange-seahorses-talk.md b/.changeset/orange-seahorses-talk.md new file mode 100644 index 0000000000..aaf34ae08b --- /dev/null +++ b/.changeset/orange-seahorses-talk.md @@ -0,0 +1,5 @@ +--- +'@talend/design-system': minor +--- + +feat(TDC-7378): New enumeration component diff --git a/fork/react-bootstrap/src/Carousel.test.js b/fork/react-bootstrap/src/Carousel.test.js index 25564e7a52..a908f0de81 100644 --- a/fork/react-bootstrap/src/Carousel.test.js +++ b/fork/react-bootstrap/src/Carousel.test.js @@ -1,317 +1,300 @@ import React from 'react'; + import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Carousel from './Carousel'; describe('', () => { - const items = [ - Item 1 content, - Item 2 content, - ]; - - it('Should show the correct item', () => { - // when - render({items}); - - // then - const item1 = screen.getByText('Item 1 content'); - const item2 = screen.getByText('Item 2 content'); - expect(item1).toBeInTheDocument(); - expect(item1).not.toHaveClass('active'); - expect(item2).toBeInTheDocument(); - expect(item2).toHaveClass('active'); - }); - - it('Should show the correct item with defaultActiveIndex', () => { - // when - render({items}); - - // then - const item1 = screen.getByText('Item 1 content'); - const item2 = screen.getByText('Item 2 content'); - expect(item1).toBeInTheDocument(); - expect(item1).not.toHaveClass('active'); - expect(item2).toBeInTheDocument(); - expect(item2).toHaveClass('active'); - const list = screen.getByRole('list'); - expect(list).toBeInTheDocument(); - expect(list).toHaveClass('carousel-indicators'); - expect(list.querySelectorAll('li')).toHaveLength(2); - }); - - it('Should handle null children', () => { - // when - render( - - Item 1 content - {null} - {false} - Item 2 content - , - ); - - // then - const item1 = screen.getByText('Item 1 content'); - const item2 = screen.getByText('Item 2 content'); - expect(item1).toBeInTheDocument(); - expect(item1).not.toHaveClass('active'); - expect(item2).toBeInTheDocument(); - expect(item2).toHaveClass('active'); - const list = screen.getByRole('list'); - expect(list).toBeInTheDocument(); - expect(list).toHaveClass('carousel-indicators'); - expect(list.querySelectorAll('li')).toHaveLength(2); - }); - - it('Should call onSelect when indicator selected', async () => { - const user = userEvent.setup(); - - // given - const onSelect = jest.fn(); - render( - - {items} - , - ); - - // when - await user.click(screen.getAllByRole('listitem')[0]); - - // then - expect(onSelect).toHaveBeenCalledWith(0); - }); - - 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 - await user.click(screen.getAllByRole('listitem')[0]); - - // then - 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', async () => { - const user = userEvent.setup(); - - // function onSelect(index, event) { - // expect(index).to.equal(0); - // expect(event.direction).to.equal('next'); - // expect(event.target).to.not.exist; - - // done(); - // } - - // given - const onSelect = jest.fn((index, event) => {}); - render( - - {items} - , - ); - - // when - await user.click(screen.getByRole('button', { name: 'Next' })); - - // then - 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', async () => { - const user = userEvent.setup(); - - // given - render( - - {items} - , - ); - expect(screen.getByText('Item 1 content')).toHaveClass('active'); - - // when - await user.click(screen.getByRole('button', { name: 'Previous' })); - - // then - await waitFor(() => - expect(screen.getByText('Item 2 content')).toHaveClass('active'), - ); - }); - - it('Should show next button control on the last image if wrap is true', async () => { - const user = userEvent.setup(); - - // given - render( - - {items} - , - ); - expect(screen.getByText('Item 2 content')).toHaveClass('active'); - - // when - await user.click(screen.getByRole('button', { name: 'Next' })); - - // then - 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', () => { - // when - render( - - {items} - , - ); - - // then - expect( - screen.queryByRole('button', { name: 'Previous' }), - ).not.toBeInTheDocument(); - }); - - it('Should not show the next button on the last image if wrap is false', () => { - // when - render( - - {items} - , - ); - - // then - expect( - screen.queryByRole('button', { name: 'Next' }), - ).not.toBeInTheDocument(); - }); - - it('Should allow user to specify a previous and next icon', () => { - // when - render( - } - nextIcon={} - > - Item 1 content - Item 2 content - Item 3 content - , - ); - - // then - expect( - screen.getByRole('button', { name: 'Previous' }).firstChild, - ).toHaveClass('ficon-left'); - expect(screen.getByRole('button', { name: 'Next' }).firstChild).toHaveClass( - 'ficon-right', - ); - }); - - it('Should allow user to specify a previous and next SR label', () => { - // when - render( - - Item 1 content - Item 2 content - Item 3 content - , - ); - - // then - expect( - screen.getByRole('button', { name: 'Previous awesomeness' }), - ).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: 'Next awesomeness' }), - ).toBeInTheDocument(); - }); - - 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 - await user.click(screen.getByRole('button', { name: 'Next' })); - - // then - expect(screen.getByText('Item 2 content')).toHaveClass('active'); - - // when - await user.click(screen.getByRole('button', { name: 'Previous' })); - - // then - expect(screen.getByText('Item 1 content')).toHaveClass('active'); - }); - - it('Should render on update, default active item > new child length', () => { - // given - // default active is the 2nd item, which will be removed on - // subsequent render - const { rerender } = render( - {items}, - ); - - expect(screen.getByText('Item 1 content')).not.toHaveClass('active'); - expect(screen.getByText('Item 2 content')).toHaveClass('active'); - expect(screen.getAllByRole('listitem')).toHaveLength(2); // carousel-indicators - - const fewerItems = items.slice(); - fewerItems.pop(); - - // when - rerender({fewerItems}); - - // then - expect(screen.getByText('Item 1 content')).toHaveClass('active'); - expect(screen.getAllByRole('listitem')).toHaveLength(1); - }); - - it('Should render on update, active item > new child length', () => { - // given - // default active is the 2nd item, which will be removed on - // subsequent render - const { rerender } = render({items}); - expect(screen.getByText('Item 1 content')).not.toHaveClass('active'); - expect(screen.getByText('Item 2 content')).toHaveClass('active'); - expect(screen.getAllByRole('listitem')).toHaveLength(2); // carousel-indicators - - const fewerItems = items.slice(); - fewerItems.pop(); - - // when - rerender({fewerItems}); - - // then - expect(screen.getByText('Item 1 content')).toHaveClass('active'); - expect(screen.getAllByRole('listitem')).toHaveLength(1); - }); + const items = [ + Item 1 content, + Item 2 content, + ]; + + it('Should show the correct item', () => { + // when + render({items}); + + // then + const item1 = screen.getByText('Item 1 content'); + const item2 = screen.getByText('Item 2 content'); + expect(item1).toBeInTheDocument(); + expect(item1).not.toHaveClass('active'); + expect(item2).toBeInTheDocument(); + expect(item2).toHaveClass('active'); + }); + + it('Should show the correct item with defaultActiveIndex', () => { + // when + render({items}); + + // then + const item1 = screen.getByText('Item 1 content'); + const item2 = screen.getByText('Item 2 content'); + expect(item1).toBeInTheDocument(); + expect(item1).not.toHaveClass('active'); + expect(item2).toBeInTheDocument(); + expect(item2).toHaveClass('active'); + const list = screen.getByRole('list'); + expect(list).toBeInTheDocument(); + expect(list).toHaveClass('carousel-indicators'); + expect(list.querySelectorAll('li')).toHaveLength(2); + }); + + it('Should handle null children', () => { + // when + render( + + Item 1 content + {null} + {false} + Item 2 content + , + ); + + // then + const item1 = screen.getByText('Item 1 content'); + const item2 = screen.getByText('Item 2 content'); + expect(item1).toBeInTheDocument(); + expect(item1).not.toHaveClass('active'); + expect(item2).toBeInTheDocument(); + expect(item2).toHaveClass('active'); + const list = screen.getByRole('list'); + expect(list).toBeInTheDocument(); + expect(list).toHaveClass('carousel-indicators'); + expect(list.querySelectorAll('li')).toHaveLength(2); + }); + + it('Should call onSelect when indicator selected', async () => { + const user = userEvent.setup(); + + // given + const onSelect = jest.fn(); + render( + + {items} + , + ); + + // when + await user.click(screen.getAllByRole('listitem')[0]); + + // then + expect(onSelect).toHaveBeenCalledWith(0); + }); + + 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 + await user.click(screen.getAllByRole('listitem')[0]); + + // then + 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', async () => { + const user = userEvent.setup(); + + // function onSelect(index, event) { + // expect(index).to.equal(0); + // expect(event.direction).to.equal('next'); + // expect(event.target).to.not.exist; + + // done(); + // } + + // given + const onSelect = jest.fn((index, event) => {}); + render( + + {items} + , + ); + + // when + await user.click(screen.getByRole('button', { name: 'Next' })); + + // then + 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', async () => { + const user = userEvent.setup(); + + // given + render( + + {items} + , + ); + expect(screen.getByText('Item 1 content')).toHaveClass('active'); + + // when + await user.click(screen.getByRole('button', { name: 'Previous' })); + + // then + await waitFor(() => expect(screen.getByText('Item 2 content')).toHaveClass('active')); + }); + + it('Should show next button control on the last image if wrap is true', async () => { + const user = userEvent.setup(); + + // given + render( + + {items} + , + ); + expect(screen.getByText('Item 2 content')).toHaveClass('active'); + + // when + await user.click(screen.getByRole('button', { name: 'Next' })); + + // then + 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', () => { + // when + render( + + {items} + , + ); + + // then + expect(screen.queryByRole('button', { name: 'Previous' })).not.toBeInTheDocument(); + }); + + it('Should not show the next button on the last image if wrap is false', () => { + // when + render( + + {items} + , + ); + + // then + expect(screen.queryByRole('button', { name: 'Next' })).not.toBeInTheDocument(); + }); + + it('Should allow user to specify a previous and next icon', () => { + // when + render( + } + nextIcon={} + > + Item 1 content + Item 2 content + Item 3 content + , + ); + + // then + expect(screen.getByRole('button', { name: 'Previous' }).firstChild).toHaveClass('ficon-left'); + expect(screen.getByRole('button', { name: 'Next' }).firstChild).toHaveClass('ficon-right'); + }); + + it('Should allow user to specify a previous and next SR label', () => { + // when + render( + + Item 1 content + Item 2 content + Item 3 content + , + ); + + // then + expect(screen.getByRole('button', { name: 'Previous awesomeness' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Next awesomeness' })).toBeInTheDocument(); + }); + + 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 + await user.click(screen.getByRole('button', { name: 'Next' })); + + // then + expect(screen.getByText('Item 2 content')).toHaveClass('active'); + + // when + await user.click(screen.getByRole('button', { name: 'Previous' })); + + // then + expect(screen.getByText('Item 1 content')).toHaveClass('active'); + }); + + it('Should render on update, default active item > new child length', () => { + // given + // default active is the 2nd item, which will be removed on + // subsequent render + const { rerender } = render({items}); + + expect(screen.getByText('Item 1 content')).not.toHaveClass('active'); + expect(screen.getByText('Item 2 content')).toHaveClass('active'); + expect(screen.getAllByRole('listitem')).toHaveLength(2); // carousel-indicators + + const fewerItems = items.slice(); + fewerItems.pop(); + + // when + rerender({fewerItems}); + + // then + expect(screen.getByText('Item 1 content')).toHaveClass('active'); + expect(screen.getAllByRole('listitem')).toHaveLength(1); + }); + + it('Should render on update, active item > new child length', () => { + // given + // default active is the 2nd item, which will be removed on + // subsequent render + const { rerender } = render({items}); + expect(screen.getByText('Item 1 content')).not.toHaveClass('active'); + expect(screen.getByText('Item 2 content')).toHaveClass('active'); + expect(screen.getAllByRole('listitem')).toHaveLength(2); // carousel-indicators + + const fewerItems = items.slice(); + fewerItems.pop(); + + // when + rerender({fewerItems}); + + // then + expect(screen.getByText('Item 1 content')).toHaveClass('active'); + expect(screen.getAllByRole('listitem')).toHaveLength(1); + }); }); diff --git a/fork/react-bootstrap/src/CloseButton.test.js b/fork/react-bootstrap/src/CloseButton.test.js index c5fa0ca59a..fe1fdedbc5 100644 --- a/fork/react-bootstrap/src/CloseButton.test.js +++ b/fork/react-bootstrap/src/CloseButton.test.js @@ -1,77 +1,78 @@ import React from 'react'; + import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import CloseButton from './CloseButton'; describe('', () => { - it('Should output a button', () => { - // when - const onClick = jest.fn(); - render(); - - // then - expect(screen.getByRole('button')).toBeInTheDocument(); - }); - - it('Should have type=button by default', () => { - // when - const onClick = jest.fn(); - render(); - - // then - expect(screen.getByRole('button')).toHaveAttribute('type', 'button'); - }); - - it('Should have class=close by default', () => { - // when - const onClick = jest.fn(); - render(); - - // then - expect(screen.getByRole('button')).toHaveClass('close'); - }); - - it('Should call onClick callback', async () => { - const user = userEvent.setup(); - - // given - const onClick = jest.fn(); - render(); - expect(onClick).not.toHaveBeenCalled(); - - // when - await user.click(screen.getByRole('button')); - - // then - expect(onClick).toHaveBeenCalled(); - }); - - it('Should have a span with aria-hidden=true', () => { - // when - const onClick = jest.fn(); - render(); - - // then - expect(screen.getByText('×')).toHaveAttribute('aria-hidden', 'true'); - }); - - it('Should have a span with class=sr-only', () => { - // when - const onClick = jest.fn(); - render(); - - // then - expect(screen.getByText('Close')).toHaveClass('sr-only'); - }); - - it('Should have a span with the custom text of the label', () => { - // when - const onClick = jest.fn(); - const label = 'Close Item'; - render(); - - // then - expect(screen.getByText(label)).toBeInTheDocument(); - }); + it('Should output a button', () => { + // when + const onClick = jest.fn(); + render(); + + // then + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + it('Should have type=button by default', () => { + // when + const onClick = jest.fn(); + render(); + + // then + expect(screen.getByRole('button')).toHaveAttribute('type', 'button'); + }); + + it('Should have class=close by default', () => { + // when + const onClick = jest.fn(); + render(); + + // then + expect(screen.getByRole('button')).toHaveClass('close'); + }); + + it('Should call onClick callback', async () => { + const user = userEvent.setup(); + + // given + const onClick = jest.fn(); + render(); + expect(onClick).not.toHaveBeenCalled(); + + // when + await user.click(screen.getByRole('button')); + + // then + expect(onClick).toHaveBeenCalled(); + }); + + it('Should have a span with aria-hidden=true', () => { + // when + const onClick = jest.fn(); + render(); + + // then + expect(screen.getByText('×')).toHaveAttribute('aria-hidden', 'true'); + }); + + it('Should have a span with class=sr-only', () => { + // when + const onClick = jest.fn(); + render(); + + // then + expect(screen.getByText('Close')).toHaveClass('sr-only'); + }); + + it('Should have a span with the custom text of the label', () => { + // when + const onClick = jest.fn(); + const label = 'Close Item'; + render(); + + // then + expect(screen.getByText(label)).toBeInTheDocument(); + }); }); diff --git a/fork/react-bootstrap/src/ControlLabel.test.js b/fork/react-bootstrap/src/ControlLabel.test.js index 18766e7f7b..6dbde7b2a5 100644 --- a/fork/react-bootstrap/src/ControlLabel.test.js +++ b/fork/react-bootstrap/src/ControlLabel.test.js @@ -1,4 +1,5 @@ import React from 'react'; + import { render, screen } from '@testing-library/react'; import ControlLabel from './ControlLabel'; diff --git a/fork/react-bootstrap/src/Dropdown.js b/fork/react-bootstrap/src/Dropdown.js index 137ed555e3..2e892904e8 100644 --- a/fork/react-bootstrap/src/Dropdown.js +++ b/fork/react-bootstrap/src/Dropdown.js @@ -1,9 +1,10 @@ +import React, { cloneElement } from 'react'; +import ReactDOM from 'react-dom'; + import classNames from 'classnames'; import activeElement from 'dom-helpers/activeElement'; import contains from 'dom-helpers/query/contains'; -import React, { cloneElement } from 'react'; import PropTypes from 'prop-types'; -import ReactDOM from 'react-dom'; import all from 'prop-types-extra/lib/all'; import elementType from 'prop-types-extra/lib/elementType'; import isRequiredForA11y from 'prop-types-extra/lib/isRequiredForA11y'; @@ -13,7 +14,7 @@ import warning from 'warning'; import ButtonGroup from './ButtonGroup'; import DropdownMenu from './DropdownMenu'; import DropdownToggle from './DropdownToggle'; -import { bsClass as setBsClass, prefix } from './utils/bootstrapUtils'; +import { prefix, bsClass as setBsClass } from './utils/bootstrapUtils'; import createChainedFunction from './utils/createChainedFunction'; import { exclusiveRoles, requiredRoles } from './utils/PropTypes'; import ValidComponentChildren from './utils/ValidComponentChildren'; @@ -22,332 +23,316 @@ const TOGGLE_ROLE = DropdownToggle.defaultProps.bsRole; const MENU_ROLE = DropdownMenu.defaultProps.bsRole; const propTypes = { - /** - * The menu will open above the dropdown button, instead of below it. - */ - dropup: PropTypes.bool, - - /** - * An html id attribute, necessary for assistive technologies, such as screen readers. - * @type {string|number} - * @required - */ - id: isRequiredForA11y( - PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - ), - - componentClass: elementType, - - /** - * The children of a Dropdown may be a `` or a ``. - * @type {node} - */ - children: all( - requiredRoles(TOGGLE_ROLE, MENU_ROLE), - exclusiveRoles(MENU_ROLE), - ), - - /** - * Whether or not component is disabled. - */ - disabled: PropTypes.bool, - - /** - * Align the menu to the right side of the Dropdown toggle - */ - pullRight: PropTypes.bool, - - /** - * Whether or not the Dropdown is visible. - * - * @controllable onToggle - */ - open: PropTypes.bool, - - defaultOpen: PropTypes.bool, - - /** - * A callback fired when the Dropdown wishes to change visibility. Called with the requested - * `open` value, the DOM event, and the source that fired it: `'click'`,`'keydown'`,`'rootClose'`, or `'select'`. - * - * ```js - * function(Boolean isOpen, Object event, { String source }) {} - * ``` - * @controllable open - */ - onToggle: PropTypes.func, - - /** - * A callback fired when a menu item is selected. - * - * ```js - * (eventKey: any, event: Object) => any - * ``` - */ - onSelect: PropTypes.func, - - /** - * If `'menuitem'`, causes the dropdown to behave like a menu item rather than - * a menu button. - */ - role: PropTypes.string, - - /** - * Which event when fired outside the component will cause it to be closed - * - * *Note: For custom dropdown components, you will have to pass the - * `rootCloseEvent` to `` in your custom dropdown menu - * component ([similarly to how it is implemented in ``](https://github.com/react-bootstrap/react-bootstrap/blob/v0.31.5/src/DropdownMenu.js#L115-L119)).* - */ - rootCloseEvent: PropTypes.oneOf(['click', 'mousedown']), - - /** - * @private - */ - onMouseEnter: PropTypes.func, - /** - * @private - */ - onMouseLeave: PropTypes.func, + /** + * The menu will open above the dropdown button, instead of below it. + */ + dropup: PropTypes.bool, + + /** + * An html id attribute, necessary for assistive technologies, such as screen readers. + * @type {string|number} + * @required + */ + id: isRequiredForA11y(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + + componentClass: elementType, + + /** + * The children of a Dropdown may be a `` or a ``. + * @type {node} + */ + children: all(requiredRoles(TOGGLE_ROLE, MENU_ROLE), exclusiveRoles(MENU_ROLE)), + + /** + * Whether or not component is disabled. + */ + disabled: PropTypes.bool, + + /** + * Align the menu to the right side of the Dropdown toggle + */ + pullRight: PropTypes.bool, + + /** + * Whether or not the Dropdown is visible. + * + * @controllable onToggle + */ + open: PropTypes.bool, + + defaultOpen: PropTypes.bool, + + /** + * A callback fired when the Dropdown wishes to change visibility. Called with the requested + * `open` value, the DOM event, and the source that fired it: `'click'`,`'keydown'`,`'rootClose'`, or `'select'`. + * + * ```js + * function(Boolean isOpen, Object event, { String source }) {} + * ``` + * @controllable open + */ + onToggle: PropTypes.func, + + /** + * A callback fired when a menu item is selected. + * + * ```js + * (eventKey: any, event: Object) => any + * ``` + */ + onSelect: PropTypes.func, + + /** + * If `'menuitem'`, causes the dropdown to behave like a menu item rather than + * a menu button. + */ + role: PropTypes.string, + + /** + * Which event when fired outside the component will cause it to be closed + * + * *Note: For custom dropdown components, you will have to pass the + * `rootCloseEvent` to `` in your custom dropdown menu + * component ([similarly to how it is implemented in ``](https://github.com/react-bootstrap/react-bootstrap/blob/v0.31.5/src/DropdownMenu.js#L115-L119)).* + */ + rootCloseEvent: PropTypes.oneOf(['click', 'mousedown']), + + /** + * @private + */ + onMouseEnter: PropTypes.func, + /** + * @private + */ + onMouseLeave: PropTypes.func, }; const defaultProps = { - componentClass: ButtonGroup, + componentClass: ButtonGroup, }; class Dropdown extends React.Component { - constructor(props, context) { - super(props, context); - - this.handleClick = this.handleClick.bind(this); - this.handleKeyDown = this.handleKeyDown.bind(this); - this.handleClose = this.handleClose.bind(this); - - this._focusInDropdown = false; - this.lastOpenEventType = null; - } - - componentDidMount() { - this.focusNextOnOpen(); - } - - componentDidUpdate(prevProps) { - const { open } = this.props; - const prevOpen = prevProps.open; - - if (open && !prevOpen) { - this.focusNextOnOpen(); - } - - if (!open && prevOpen) { - this._focusInDropdown = contains( - ReactDOM.findDOMNode(this.menu), - activeElement(document), - ); - // if focus hasn't already moved from the menu let's return it - // to the toggle - if (this._focusInDropdown) { - this._focusInDropdown = false; - this.focus(); - } - } - } - - focus() { - const toggle = ReactDOM.findDOMNode(this.toggle); - - if (toggle && toggle.focus) { - toggle.focus(); - } - } - - focusNextOnOpen() { - const menu = this.menu; - - if (!menu || !menu.focusNext) { - return; - } - - if ( - this.lastOpenEventType === 'keydown' || - this.props.role === 'menuitem' - ) { - menu.focusNext(); - } - } - - handleClick(event) { - if (this.props.disabled) { - return; - } - - this.toggleOpen(event, { source: 'click' }); - } - - handleClose(event, eventDetails) { - if (!this.props.open) { - return; - } - - this.toggleOpen(event, eventDetails); - } - - handleKeyDown(event) { - if (this.props.disabled) { - return; - } - - switch (event.key) { - case 'Down': - case 'ArrowDown': - if (!this.props.open) { - this.toggleOpen(event, { source: 'keydown' }); - } else if (this.menu.focusNext) { - this.menu.focusNext(); - } - event.preventDefault(); - break; - case 'Esc': - case 'Escape': - case 'Tab': - this.handleClose(event, { source: 'keydown' }); - break; - default: - } - } - - toggleOpen(event, eventDetails) { - let open = !this.props.open; - - if (open) { - this.lastOpenEventType = eventDetails.source; - } - - if (this.props.onToggle) { - this.props.onToggle(open, event, eventDetails); - } - } - - renderMenu(child, { id, onSelect, rootCloseEvent, ...props }) { - let ref = (c) => { - this.menu = c; - }; - - if (typeof child.ref === 'string') { - warning( - 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', - ); - } else { - ref = createChainedFunction(child.ref, ref); - } - - return cloneElement(child, { - ...props, - ref, - labelledBy: id, - bsClass: prefix(props, 'menu'), - onClose: createChainedFunction(child.props.onClose, this.handleClose), - onSelect: createChainedFunction( - child.props.onSelect, - onSelect, - (key, event) => this.handleClose(event, { source: 'select' }), - ), - rootCloseEvent, - }); - } - - renderToggle(child, props) { - let ref = (c) => { - this.toggle = c; - }; - - if (typeof child.ref === 'string') { - warning( - 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', - ); - } else { - ref = createChainedFunction(child.ref, ref); - } - - return cloneElement(child, { - ...props, - ref, - bsClass: prefix(props, 'toggle'), - onClick: createChainedFunction(child.props.onClick, this.handleClick), - onKeyDown: createChainedFunction( - child.props.onKeyDown, - this.handleKeyDown, - ), - }); - } - - render() { - const { - componentClass: Component, - id, - dropup, - disabled, - pullRight, - open, - onSelect, - role, - bsClass, - className, - rootCloseEvent, - children, - ...props - } = this.props; - - delete props.onToggle; - - const classes = { - [bsClass]: true, - open, - disabled, - }; - - if (dropup) { - classes[bsClass] = false; - classes.dropup = true; - } - - // This intentionally forwards bsSize and bsStyle (if set) to the - // underlying component, to allow it to render size and style variants. - - return ( - - {ValidComponentChildren.map(children, (child) => { - switch (child.props.bsRole) { - case TOGGLE_ROLE: - return this.renderToggle(child, { - id, - disabled, - open, - role, - bsClass, - }); - case MENU_ROLE: - return this.renderMenu(child, { - id, - open, - pullRight, - bsClass, - onSelect, - rootCloseEvent, - }); - default: - return child; - } - })} - - ); - } + constructor(props, context) { + super(props, context); + + this.handleClick = this.handleClick.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleClose = this.handleClose.bind(this); + + this._focusInDropdown = false; + this.lastOpenEventType = null; + } + + componentDidMount() { + this.focusNextOnOpen(); + } + + componentDidUpdate(prevProps) { + const { open } = this.props; + const prevOpen = prevProps.open; + + if (open && !prevOpen) { + this.focusNextOnOpen(); + } + + if (!open && prevOpen) { + this._focusInDropdown = contains(ReactDOM.findDOMNode(this.menu), activeElement(document)); + // if focus hasn't already moved from the menu let's return it + // to the toggle + if (this._focusInDropdown) { + this._focusInDropdown = false; + this.focus(); + } + } + } + + focus() { + const toggle = ReactDOM.findDOMNode(this.toggle); + + if (toggle && toggle.focus) { + toggle.focus(); + } + } + + focusNextOnOpen() { + const menu = this.menu; + + if (!menu || !menu.focusNext) { + return; + } + + if (this.lastOpenEventType === 'keydown' || this.props.role === 'menuitem') { + menu.focusNext(); + } + } + + handleClick(event) { + if (this.props.disabled) { + return; + } + + this.toggleOpen(event, { source: 'click' }); + } + + handleClose(event, eventDetails) { + if (!this.props.open) { + return; + } + + this.toggleOpen(event, eventDetails); + } + + handleKeyDown(event) { + if (this.props.disabled) { + return; + } + + switch (event.key) { + case 'Down': + case 'ArrowDown': + if (!this.props.open) { + this.toggleOpen(event, { source: 'keydown' }); + } else if (this.menu.focusNext) { + this.menu.focusNext(); + } + event.preventDefault(); + break; + case 'Esc': + case 'Escape': + case 'Tab': + this.handleClose(event, { source: 'keydown' }); + break; + default: + } + } + + toggleOpen(event, eventDetails) { + let open = !this.props.open; + + if (open) { + this.lastOpenEventType = eventDetails.source; + } + + if (this.props.onToggle) { + this.props.onToggle(open, event, eventDetails); + } + } + + renderMenu(child, { id, onSelect, rootCloseEvent, ...props }) { + let ref = c => { + this.menu = c; + }; + + if (typeof child.ref === 'string') { + warning( + 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', + ); + } else { + ref = createChainedFunction(child.ref, ref); + } + + return cloneElement(child, { + ...props, + ref, + labelledBy: id, + bsClass: prefix(props, 'menu'), + onClose: createChainedFunction(child.props.onClose, this.handleClose), + onSelect: createChainedFunction(child.props.onSelect, onSelect, (key, event) => + this.handleClose(event, { source: 'select' }), + ), + rootCloseEvent, + }); + } + + renderToggle(child, props) { + let ref = c => { + this.toggle = c; + }; + + if (typeof child.ref === 'string') { + warning( + 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', + ); + } else { + ref = createChainedFunction(child.ref, ref); + } + + return cloneElement(child, { + ...props, + ref, + bsClass: prefix(props, 'toggle'), + onClick: createChainedFunction(child.props.onClick, this.handleClick), + onKeyDown: createChainedFunction(child.props.onKeyDown, this.handleKeyDown), + }); + } + + render() { + const { + componentClass: Component, + id, + dropup, + disabled, + pullRight, + open, + onSelect, + role, + bsClass, + className, + rootCloseEvent, + children, + ...props + } = this.props; + + delete props.onToggle; + + const classes = { + [bsClass]: true, + open, + disabled, + }; + + if (dropup) { + classes[bsClass] = false; + classes.dropup = true; + } + + // This intentionally forwards bsSize and bsStyle (if set) to the + // underlying component, to allow it to render size and style variants. + + return ( + + {ValidComponentChildren.map(children, child => { + switch (child.props.bsRole) { + case TOGGLE_ROLE: + return this.renderToggle(child, { + id, + disabled, + open, + role, + bsClass, + }); + case MENU_ROLE: + return this.renderMenu(child, { + id, + open, + pullRight, + bsClass, + onSelect, + rootCloseEvent, + }); + default: + return child; + } + })} + + ); + } } Dropdown.propTypes = propTypes; diff --git a/fork/react-bootstrap/src/Dropdown.test.js b/fork/react-bootstrap/src/Dropdown.test.js index ca3d832ec6..d592965d19 100644 --- a/fork/react-bootstrap/src/Dropdown.test.js +++ b/fork/react-bootstrap/src/Dropdown.test.js @@ -1,11 +1,12 @@ /* eslint-disable react/no-string-refs */ + /* eslint-disable react/prop-types */ import { useState } from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - import ReactDOM from 'react-dom'; +import { fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + import Dropdown from './Dropdown'; import Grid from './Grid'; import MenuItem from './MenuItem'; diff --git a/fork/react-bootstrap/src/DropdownMenu.js b/fork/react-bootstrap/src/DropdownMenu.js index 347e1462a1..8a7d631a60 100644 --- a/fork/react-bootstrap/src/DropdownMenu.js +++ b/fork/react-bootstrap/src/DropdownMenu.js @@ -1,145 +1,126 @@ -import classNames from 'classnames'; import React from 'react'; -import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import RootCloseWrapper from 'react-overlays/lib/RootCloseWrapper'; -import { - bsClass, - getClassSet, - prefix, - splitBsPropsAndOmit, -} from './utils/bootstrapUtils'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; + +import { bsClass, getClassSet, prefix, splitBsPropsAndOmit } from './utils/bootstrapUtils'; import createChainedFunction from './utils/createChainedFunction'; import ValidComponentChildren from './utils/ValidComponentChildren'; const propTypes = { - open: PropTypes.bool, - pullRight: PropTypes.bool, - onClose: PropTypes.func, - labelledBy: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - onSelect: PropTypes.func, - rootCloseEvent: PropTypes.oneOf(['click', 'mousedown']), + open: PropTypes.bool, + pullRight: PropTypes.bool, + onClose: PropTypes.func, + labelledBy: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + onSelect: PropTypes.func, + rootCloseEvent: PropTypes.oneOf(['click', 'mousedown']), }; const defaultProps = { - bsRole: 'menu', - pullRight: false, + bsRole: 'menu', + pullRight: false, }; class DropdownMenu extends React.Component { - constructor(props) { - super(props); - - this.handleRootClose = this.handleRootClose.bind(this); - this.handleKeyDown = this.handleKeyDown.bind(this); - } - - getFocusableMenuItems() { - const node = ReactDOM.findDOMNode(this); - if (!node) { - return []; - } - - return Array.from(node.querySelectorAll('[tabIndex="-1"]')); - } - - getItemsAndActiveIndex() { - const items = this.getFocusableMenuItems(); - const activeIndex = items.indexOf(document.activeElement); - - return { items, activeIndex }; - } - - focusNext() { - const { items, activeIndex } = this.getItemsAndActiveIndex(); - if (items.length === 0) { - return; - } - - const nextIndex = activeIndex === items.length - 1 ? 0 : activeIndex + 1; - items[nextIndex].focus(); - } - - focusPrevious() { - const { items, activeIndex } = this.getItemsAndActiveIndex(); - if (items.length === 0) { - return; - } - - const prevIndex = activeIndex === 0 ? items.length - 1 : activeIndex - 1; - items[prevIndex].focus(); - } - - handleKeyDown(event) { - switch (event.key) { - case 'Down': - case 'ArrowDown': - this.focusNext(); - event.preventDefault(); - break; - case 'Up': - case 'ArrowUp': - this.focusPrevious(); - event.preventDefault(); - break; - case 'Esc': - case 'Escape': - case 'Tab': - this.props.onClose(event, { source: 'keydown' }); - break; - default: - } - } - - handleRootClose(event) { - this.props.onClose(event, { source: 'rootClose' }); - } - - render() { - const { - open, - pullRight, - labelledBy, - onSelect, - className, - rootCloseEvent, - children, - ...props - } = this.props; - - const [bsProps, elementProps] = splitBsPropsAndOmit(props, ['onClose']); - - const classes = { - ...getClassSet(bsProps), - [prefix(bsProps, 'right')]: pullRight, - }; - - return ( - -
    - {ValidComponentChildren.map(children, (child) => - React.cloneElement(child, { - onKeyDown: createChainedFunction( - child.props.onKeyDown, - this.handleKeyDown, - ), - onSelect: createChainedFunction(child.props.onSelect, onSelect), - }), - )} -
-
- ); - } + constructor(props) { + super(props); + + this.handleRootClose = this.handleRootClose.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + } + + getFocusableMenuItems() { + const node = ReactDOM.findDOMNode(this); + if (!node) { + return []; + } + + return Array.from(node.querySelectorAll('[tabIndex="-1"]')); + } + + getItemsAndActiveIndex() { + const items = this.getFocusableMenuItems(); + const activeIndex = items.indexOf(document.activeElement); + + return { items, activeIndex }; + } + + focusNext() { + const { items, activeIndex } = this.getItemsAndActiveIndex(); + if (items.length === 0) { + return; + } + + const nextIndex = activeIndex === items.length - 1 ? 0 : activeIndex + 1; + items[nextIndex].focus(); + } + + focusPrevious() { + const { items, activeIndex } = this.getItemsAndActiveIndex(); + if (items.length === 0) { + return; + } + + const prevIndex = activeIndex === 0 ? items.length - 1 : activeIndex - 1; + items[prevIndex].focus(); + } + + handleKeyDown(event) { + switch (event.key) { + case 'Down': + case 'ArrowDown': + this.focusNext(); + event.preventDefault(); + break; + case 'Up': + case 'ArrowUp': + this.focusPrevious(); + event.preventDefault(); + break; + case 'Esc': + case 'Escape': + case 'Tab': + this.props.onClose(event, { source: 'keydown' }); + break; + default: + } + } + + handleRootClose(event) { + this.props.onClose(event, { source: 'rootClose' }); + } + + render() { + const { open, pullRight, labelledBy, onSelect, className, rootCloseEvent, children, ...props } = + this.props; + + const [bsProps, elementProps] = splitBsPropsAndOmit(props, ['onClose']); + + const classes = { + ...getClassSet(bsProps), + [prefix(bsProps, 'right')]: pullRight, + }; + + return ( + +
    + {ValidComponentChildren.map(children, child => + React.cloneElement(child, { + onKeyDown: createChainedFunction(child.props.onKeyDown, this.handleKeyDown), + onSelect: createChainedFunction(child.props.onSelect, onSelect), + }), + )} +
+
+ ); + } } DropdownMenu.propTypes = propTypes; diff --git a/fork/react-bootstrap/src/DropdownMenu.test.js b/fork/react-bootstrap/src/DropdownMenu.test.js index ef69a26093..4f0c012ef7 100644 --- a/fork/react-bootstrap/src/DropdownMenu.test.js +++ b/fork/react-bootstrap/src/DropdownMenu.test.js @@ -1,6 +1,6 @@ // import ReactDOM from 'react-dom'; // import ReactTestUtils from 'react-dom/test-utils'; -import { screen, render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import DropdownMenu from './DropdownMenu'; import MenuItem from './MenuItem'; diff --git a/fork/react-bootstrap/src/Nav.js b/fork/react-bootstrap/src/Nav.js index 350a818a08..b98dd600c1 100644 --- a/fork/react-bootstrap/src/Nav.js +++ b/fork/react-bootstrap/src/Nav.js @@ -1,7 +1,8 @@ -import classNames from 'classnames'; import React, { cloneElement } from 'react'; -import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; + +import classNames from 'classnames'; +import PropTypes from 'prop-types'; import all from 'prop-types-extra/lib/all'; import warning from 'warning'; diff --git a/fork/react-bootstrap/test/DropdownMenuSpec.js b/fork/react-bootstrap/test/DropdownMenuSpec.js index aa3dc04d61..55739c8f87 100644 --- a/fork/react-bootstrap/test/DropdownMenuSpec.js +++ b/fork/react-bootstrap/test/DropdownMenuSpec.js @@ -1,10 +1,10 @@ import ReactDOM from 'react-dom'; import ReactTestUtils from 'react-dom/test-utils'; + import { shallow } from 'enzyme'; import DropdownMenu from '../src/DropdownMenu'; import MenuItem from '../src/MenuItem'; - import { getOne } from './helpers'; describe('', () => { diff --git a/fork/react-bootstrap/test/DropdownSpec.js b/fork/react-bootstrap/test/DropdownSpec.js index 79fcba18dd..9b446215a3 100644 --- a/fork/react-bootstrap/test/DropdownSpec.js +++ b/fork/react-bootstrap/test/DropdownSpec.js @@ -1,13 +1,13 @@ import React from 'react'; import ReactDOM from 'react-dom'; import ReactTestUtils from 'react-dom/test-utils'; + import { mount, shallow } from 'enzyme'; import Dropdown from '../src/Dropdown'; import DropdownMenu from '../src/DropdownMenu'; import Grid from '../src/Grid'; import MenuItem from '../src/MenuItem'; - import { shouldWarn } from './helpers'; class CustomMenu extends React.Component { diff --git a/fork/react-bootstrap/test/MenuItemSpec.js b/fork/react-bootstrap/test/MenuItemSpec.js index 5be3275eb1..3c46b194e9 100644 --- a/fork/react-bootstrap/test/MenuItemSpec.js +++ b/fork/react-bootstrap/test/MenuItemSpec.js @@ -1,9 +1,9 @@ import ReactDOM from 'react-dom'; import ReactTestUtils from 'react-dom/test-utils'; + import { shallow } from 'enzyme'; import MenuItem from '../src/MenuItem'; - import { shouldWarn } from './helpers'; describe('', () => { diff --git a/fork/react-bootstrap/test/NavSpec.js b/fork/react-bootstrap/test/NavSpec.js index d55adf71fb..992ccf6f0b 100644 --- a/fork/react-bootstrap/test/NavSpec.js +++ b/fork/react-bootstrap/test/NavSpec.js @@ -1,9 +1,9 @@ import ReactTestUtils from 'react-dom/test-utils'; + import { mount } from 'enzyme'; import Nav from '../src/Nav'; import NavItem from '../src/NavItem'; - import { shouldWarn } from './helpers'; describe('