From 04a9186649c7e4b9089d66ff2d251585d8c915ec Mon Sep 17 00:00:00 2001 From: Tanner Summers Date: Mon, 31 Oct 2022 15:17:15 -0500 Subject: [PATCH] chore(react): updated tile and stories (#12312) * chore(react): updated tile and stories * chore(react): updated tile story and closes 12315 Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../react/src/components/Tile/Tile-test.js | 472 ++++---- packages/react/src/components/Tile/Tile.js | 1011 ++++++++--------- .../Tile/{next => }/Tile.stories.js | 90 +- packages/react/src/components/Tile/index.js | 56 +- .../src/components/Tile/next/Tile-test.js | 262 ----- .../react/src/components/Tile/next/Tile.js | 641 ----------- .../Tile/{next => }/tile-story.scss | 0 7 files changed, 714 insertions(+), 1818 deletions(-) rename packages/react/src/components/Tile/{next => }/Tile.stories.js (77%) delete mode 100644 packages/react/src/components/Tile/next/Tile-test.js delete mode 100644 packages/react/src/components/Tile/next/Tile.js rename packages/react/src/components/Tile/{next => }/tile-story.scss (100%) diff --git a/packages/react/src/components/Tile/Tile-test.js b/packages/react/src/components/Tile/Tile-test.js index 5ce554b48cba..61a8a405bff3 100644 --- a/packages/react/src/components/Tile/Tile-test.js +++ b/packages/react/src/components/Tile/Tile-test.js @@ -13,185 +13,161 @@ import { ExpandableTile, TileAboveTheFoldContent, TileBelowTheFoldContent, -} from '../Tile'; -import { shallow, mount } from 'enzyme'; +} from './Tile'; + +import Link from '../Link'; +import { render, cleanup, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import { mount } from 'enzyme'; const prefix = 'cds'; -describe('Tile', () => { - describe('Renders default tile as expected', () => { - const wrapper = shallow( - -
Test
+describe('Default', () => { + afterEach(cleanup); + + it('adds extra classes that are passed via className', () => { + render( + + Default tile +
+
+ Link
); - it('renders children as expected', () => { - expect(wrapper.find('.child').length).toBe(1); - }); - - it('renders extra classes passed in via className', () => { - expect(wrapper.hasClass('extra-class')).toEqual(true); - }); + expect(screen.getByText('Default tile').classList.contains('🚀')).toBe( + true + ); }); +}); - describe('Renders clickable tile as expected', () => { - const wrapper = mount( - -
Test
+describe('ClickableTile', () => { + afterEach(cleanup); + + it('renders with a link', () => { + render( + + Clickable Tile ); - - beforeEach(() => { - wrapper.state().clicked = false; - }); - - it('renders children as expected', () => { - expect(wrapper.find('.child').length).toBe(1); - }); - - it('renders extra classes passed in via className', () => { - expect(wrapper.hasClass('extra-class')).toEqual(true); - }); - - it('toggles the clickable class on click', () => { - expect( - wrapper.find('Link').hasClass(`${prefix}--tile--is-clicked`) - ).toEqual(false); - wrapper.simulate('click', { persist: () => {} }); - expect( - wrapper.find('Link').hasClass(`${prefix}--tile--is-clicked`) - ).toEqual(true); - }); - - it('toggles the clickable state on click', () => { - expect(wrapper.state().clicked).toEqual(false); - wrapper.simulate('click', { persist: () => {} }); - expect(wrapper.state().clicked).toEqual(true); - }); - - it('toggles the clicked state when using enter or space', () => { - expect(wrapper.state().clicked).toEqual(false); - wrapper.simulate('keydown', { which: 32, persist: () => {} }); - expect(wrapper.state().clicked).toEqual(true); - wrapper.simulate('keydown', { which: 13, persist: () => {} }); - expect(wrapper.state().clicked).toEqual(false); - }); - - it('supports setting initial clicked state from props', () => { - expect(shallow().state().clicked).toEqual(true); - }); - - it('supports setting clicked state from props', () => { - wrapper.setProps({ clicked: true }); - wrapper.setState({ clicked: true }); - wrapper.setProps({ clicked: false }); - expect(wrapper.state().clicked).toEqual(false); - }); - - it('avoids changing clicked state upon setting props, unless actual value change is detected', () => { - wrapper.setProps({ clicked: true }); - wrapper.setState({ clicked: false }); - wrapper.setProps({ clicked: true }); - expect(wrapper.state().clicked).toEqual(false); - }); + expect(screen.getByRole('link')).toBeInTheDocument(); }); +}); - describe('Renders selectable tile as expected', () => { - let wrapper; - let label; +describe('Multi Select', () => { + afterEach(cleanup); + + it('does not invoke the click handler if SelectableTile is disabled', () => { + const onClick = jest.fn(); + render( +
+ + + 🚦 + + +
+ ); + const tile = screen.getByText('🚦'); + userEvent.click(tile); + expect(onClick).not.toHaveBeenCalled(); + }); - beforeEach(() => { - wrapper = mount( - -
Test
+ it('should cycle elements in document tab order', () => { + render( +
+ + tile 1 - ); - label = wrapper.find('label'); - }); + + tile 2 + + + tile 3 + +
+ ); + const [id1, id2, id3] = screen.getAllByTestId('element'); + expect(document.body).toHaveFocus(); - it('renders children as expected', () => { - expect(wrapper.find('.child').length).toBe(1); - }); + userEvent.tab(); - it('renders extra classes passed in via className', () => { - expect(wrapper.hasClass('extra-class')).toEqual(true); - }); + expect(id1).toHaveFocus(); - it('toggles the selectable state on click', () => { - expect(wrapper.hasClass(`${prefix}--tile--is-selected`)).toEqual(false); - label.simulate('click'); - expect(wrapper.props().onClick).toHaveBeenCalledTimes(1); - expect(wrapper.render().hasClass(`${prefix}--tile--is-selected`)).toEqual( - true - ); - }); + userEvent.tab(); - it('toggles the selectable state when using enter or space', () => { - expect(wrapper.hasClass(`${prefix}--tile--is-selected`)).toEqual(false); - label.simulate('keydown', { which: 32 }); - expect(wrapper.render().hasClass(`${prefix}--tile--is-selected`)).toEqual( - true - ); - label.simulate('keydown', { which: 13 }); - expect(wrapper.render().hasClass(`${prefix}--tile--is-selected`)).toEqual( - false - ); - }); + expect(id2).toHaveFocus(); - it('the input should be checked when state is selected', () => { - label.simulate('click'); - expect(wrapper.find('input').props().checked).toEqual(true); - }); + userEvent.tab(); - it('supports setting initial selected state from props', () => { - expect( - shallow() - .render() - .hasClass(`${prefix}--tile--is-selected`) - ).toEqual(true); - }); + expect(id3).toHaveFocus(); - it('supports setting selected state from props', () => { - wrapper.setProps({ selected: true }); - expect(wrapper.render().hasClass(`${prefix}--tile--is-selected`)).toEqual( - true - ); - }); + userEvent.tab(); - it('avoids changing selected state upon setting props, unless actual value change is detected', () => { - wrapper.setProps({ selected: true }); - label.simulate('click'); - wrapper.setProps({ selected: true }); - expect(wrapper.hasClass(`${prefix}--tile--is-selected`)).toEqual(false); - }); + // cycle goes back to the body element + expect(document.body).toHaveFocus(); - it('should call onChange when the checkbox value changes', () => { - const onChange = jest.fn(); - const wrapper = mount( - - test - - ); + userEvent.tab(); - const content = wrapper.find('#test-id'); + expect(id1).toHaveFocus(); + }); +}); - // Tile becomes selected - content.simulate('click'); - expect(onChange).toHaveBeenCalledTimes(1); +describe('ExpandableTile', () => { + const wrapper = mount( + + +
Test
+
+ +
Test
+
+
+ ); + + it('renders children as expected', () => { + expect(wrapper.props().children.length).toBe(2); + }); - // Tile becomes un-selected - content.simulate('click'); - expect(onChange).toHaveBeenCalledTimes(2); - }); + it('has the expected classes', () => { + expect(wrapper.children().hasClass(`${prefix}--tile--expandable`)).toEqual( + true + ); + }); - it('supports disabled state', () => { - wrapper.setProps({ disabled: true }); - expect(wrapper.find('input').props().disabled).toEqual(true); - }); + it('renders extra classes passed in via className', () => { + expect(wrapper.hasClass('extra-class')).toEqual(true); }); - describe('Renders expandable tile as expected', () => { + it('toggles the expandable class on click', () => { + expect(wrapper.children().hasClass(`${prefix}--tile--is-expanded`)).toEqual( + false + ); + wrapper.simulate('click'); + expect(wrapper.children().hasClass(`${prefix}--tile--is-expanded`)).toEqual( + true + ); + }); + + it('displays the default tooltip for the button', () => { const wrapper = mount( @@ -199,152 +175,88 @@ describe('Tile', () => {
Test
- - Test Link -
); + const defaultExpandedIconText = 'Interact to collapse Tile'; + const defaultCollapsedIconText = 'Interact to expand Tile'; - beforeEach(() => { - wrapper.state().expanded = false; - }); - - it('renders children as expected', () => { - expect(wrapper.props().children.length).toBe(2); - }); - - it('has the expected classes', () => { - expect( - wrapper.children().hasClass(`${prefix}--tile--expandable`) - ).toEqual(true); - }); - - it('renders extra classes passed in via className', () => { - expect(wrapper.hasClass('extra-class')).toEqual(true); - }); - - it('toggles the expandable class on click', () => { - expect( - wrapper.children().hasClass(`${prefix}--tile--is-expanded`) - ).toEqual(false); - wrapper.simulate('click'); - expect( - wrapper.children().hasClass(`${prefix}--tile--is-expanded`) - ).toEqual(true); - }); - - it('toggles the expandable state on click', () => { - expect(wrapper.state().expanded).toEqual(false); - wrapper.simulate('click'); - expect(wrapper.state().expanded).toEqual(true); - }); - - it('ignores allows click events to be ignored using onBeforeClick', () => { - wrapper.setProps({ - onBeforeClick: (evt) => evt.target.tagName.toLowerCase() !== 'a', // ignore link clicks - }); - expect(wrapper.state().expanded).toEqual(false); - wrapper.simulate('click'); - expect(wrapper.state().expanded).toEqual(true); - wrapper.find('#test-link').simulate('click'); - expect(wrapper.state().expanded).toEqual(true); - wrapper.simulate('click'); - expect(wrapper.state().expanded).toEqual(false); - }); + // Force the expanded tile to be collapsed. + wrapper.setProps({ expanded: false }); + const collapsedDescription = wrapper.find('button').prop('title'); + expect(collapsedDescription).toEqual(defaultCollapsedIconText); - it('displays the default tooltip for the button depending on state', () => { - const defaultExpandedIconText = 'Interact to collapse Tile'; - const defaultCollapsedIconText = 'Interact to expand Tile'; + // click on the item to expand it. + wrapper.simulate('click'); - // Force the expanded tile to be collapsed. - wrapper.setState({ expanded: false }); - const collapsedDescription = wrapper.find('button').prop('title'); - expect(collapsedDescription).toEqual(defaultCollapsedIconText); - - // click on the item to expand it. - wrapper.simulate('click'); - - // Validate the description change - const expandedDescription = wrapper.find('button').prop('title'); - expect(expandedDescription).toEqual(defaultExpandedIconText); - }); - - it('displays the custom tooltips for the button depending on state', () => { - const tileExpandedIconText = 'Click To Collapse'; - const tileCollapsedIconText = 'Click To Expand'; - - // Force the custom icon text - wrapper.setProps({ tileExpandedIconText, tileCollapsedIconText }); - - // Force the expanded tile to be collapsed. - wrapper.setState({ expanded: false }); - const collapsedDescription = wrapper.find('button').prop('title'); + // Validate the description change + const expandedDescription = wrapper.find('button').prop('title'); + expect(expandedDescription).toEqual(defaultExpandedIconText); + }); - expect(collapsedDescription).toEqual(tileCollapsedIconText); + it('displays the custom tooltips for the button depending on state', () => { + const wrapper = mount( + + +
Test
+
+ +
Test
+
+
+ ); - // click on the item to expand it. - wrapper.simulate('click'); + const tileExpandedIconText = 'Click To Collapse'; + const tileCollapsedIconText = 'Click To Expand'; - // Validate the description change - const expandedDescription = wrapper.find('button').prop('title'); - expect(expandedDescription).toEqual(tileExpandedIconText); + // Force the custom icon text and the expanded tile to be collapsed. + wrapper.setProps({ + tileExpandedIconText, + tileCollapsedIconText, + expanded: false, }); - it('supports setting initial expanded state from props', () => { - const { expanded } = mount( - - -
Test
-
- -
Test
-
-
- ).state(); - expect(expanded).toEqual(true); - }); + const collapsedDescription = wrapper.find('button').prop('title'); - it('supports setting expanded state from props', () => { - wrapper.setProps({ expanded: true }); - wrapper.setState({ expanded: true }); - wrapper.setProps({ expanded: false }); - expect(wrapper.state().expanded).toEqual(false); - }); + expect(collapsedDescription).toEqual(tileCollapsedIconText); - it('avoids changing expanded state upon setting props, unless actual value change is detected', () => { - wrapper.setProps({ expanded: true }); - wrapper.setState({ expanded: false }); - wrapper.setProps({ expanded: true }); - expect(wrapper.state().expanded).toEqual(false); - }); + // click on the item to expand it. + wrapper.simulate('click'); - it('supports setting max height from props', () => { - wrapper.setProps({ tileMaxHeight: 2 }); - wrapper.setState({ tileMaxHeight: 2 }); - wrapper.setProps({ tileMaxHeight: 1 }); - expect(wrapper.state().tileMaxHeight).toEqual(1); - }); + // Validate the description change + const expandedDescription = wrapper.find('button').prop('title'); + expect(expandedDescription).toEqual(tileExpandedIconText); + }); - it('avoids changing max height upon setting props, unless actual value change is detected', () => { - wrapper.setProps({ tileMaxHeight: 2 }); - wrapper.setState({ tileMaxHeight: 1 }); - wrapper.setProps({ tileMaxHeight: 2 }); - expect(wrapper.state().tileMaxHeight).toEqual(1); - }); + it('supports setting initial expanded state from props', () => { + const wrapper = mount( + + +
Test
+
+ +
Test
+
+
+ ); + expect(wrapper.children().hasClass(`${prefix}--tile--is-expanded`)).toEqual( + true + ); + }); - it('supports setting padding from props', () => { - wrapper.setProps({ tilePadding: 2 }); - wrapper.setState({ tilePadding: 2 }); - wrapper.setProps({ tilePadding: 1 }); - expect(wrapper.state().tilePadding).toEqual(1); - }); + it('supports setting expanded state from props', () => { + wrapper.setProps({ expanded: true }); + expect(wrapper.children().hasClass(`${prefix}--tile--is-expanded`)).toEqual( + true + ); - it('avoids changing padding upon setting props, unless actual value change is detected', () => { - wrapper.setProps({ tilePadding: 2 }); - wrapper.setState({ tilePadding: 1 }); - wrapper.setProps({ tilePadding: 2 }); - expect(wrapper.state().tilePadding).toEqual(1); - }); + wrapper.setProps({ expanded: false }); + expect(wrapper.children().hasClass(`${prefix}--tile--is-expanded`)).toEqual( + false + ); }); }); + +// Todo: Testing for a disabled ClickableTile +// Todo: Testing for ExpandableTile +// Todo: Testing for RadioTile diff --git a/packages/react/src/components/Tile/Tile.js b/packages/react/src/components/Tile/Tile.js index a63c71127a4c..cafc8f8c81db 100644 --- a/packages/react/src/components/Tile/Tile.js +++ b/packages/react/src/components/Tile/Tile.js @@ -1,245 +1,191 @@ -/** - * Copyright IBM Corp. 2016, 2018 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import React, { Component, useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; -import Link from '../Link'; import { Checkbox, CheckboxCheckedFilled, ChevronDown, } from '@carbon/icons-react'; +import Link from '../Link'; import { keys, matches } from '../../internal/keyboard'; import deprecate from '../../prop-types/deprecate'; import { composeEventHandlers } from '../../tools/events'; -import { PrefixContext, usePrefix } from '../../internal/usePrefix'; - -export class Tile extends Component { - static propTypes = { - /** - * The child nodes. - */ - children: PropTypes.node, - - /** - * The CSS class names. - */ - className: PropTypes.string, - - /** - * `true` to use the light version. For use on $ui-01 backgrounds only. - * Don't use this to make tile background color same as container background color. - */ - light: PropTypes.bool, - }; +import { usePrefix } from '../../internal/usePrefix'; +import useIsomorphicEffect from '../../internal/useIsomorphicEffect'; +import { getInteractiveContent } from '../../internal/useNoInteractiveChildren'; + +export const Tile = React.forwardRef(function Tile( + { children, className, light = false, ...rest }, + ref +) { + const prefix = usePrefix(); - static contextType = PrefixContext; + const tileClasses = cx( + `${prefix}--tile`, + { + [`${prefix}--tile--light`]: light, + }, + className + ); + return ( +
+ {children} +
+ ); +}); - static defaultProps = { - light: false, - }; +Tile.displayName = 'Tile'; +Tile.propTypes = { + /** + * The child nodes. + */ + children: PropTypes.node, - render() { - const prefix = this.context; - const { children, className, light, ...rest } = this.props; - const tileClasses = cx( - `${prefix}--tile`, - { - [`${prefix}--tile--light`]: light, - }, - className - ); - return ( -
- {children} -
- ); - } -} + /** + * The CSS class names. + */ + className: PropTypes.string, -export class ClickableTile extends Component { - state = {}; - - static propTypes = { - /** - * The child nodes. - */ - children: PropTypes.node, - - /** - * The CSS class names. - */ - className: PropTypes.string, - - /** - * Deprecated in v11. Use 'onClick' instead. - */ - handleClick: deprecate( - PropTypes.func, - 'The handleClick prop for ClickableTile has been deprecated in favor of onClick. It will be removed in the next major release.' - ), - - /** - * Specify the function to run when the ClickableTile is interacted with via a keyboard - */ - handleKeyDown: deprecate( - PropTypes.func, - 'The handleKeyDown prop for ClickableTile has been deprecated in favor of onKeyDown. It will be removed in the next major release.' - ), - - /** - * The href for the link. - */ - href: PropTypes.string, - - /** - * `true` to use the light version. For use on $ui-01 backgrounds only. - * Don't use this to make tile background color same as container background color. - */ - light: PropTypes.bool, - - /** - * Specify the function to run when the ClickableTile is clicked - */ - onClick: PropTypes.func, - - /** - * Specify the function to run when the ClickableTile is interacted with via a keyboard - */ - onKeyDown: PropTypes.func, - - /** - * The rel property for the link. - */ - rel: PropTypes.string, - }; + /** + * `true` to use the light version. For use on $ui-01 backgrounds only. + * Don't use this to make tile background color same as container background color. + */ + light: deprecate( + PropTypes.bool, + 'The `light` prop for `Tile` is no longer needed and has been deprecated. It will be removed in the next major release. Use the Layer component instead.' + ), +}; - static contextType = PrefixContext; +export const ClickableTile = React.forwardRef(function ClickableTile( + { + children, + className, + clicked = false, + href, + light, + onClick = () => {}, + onKeyDown = () => {}, + ...rest + }, + ref +) { + const prefix = usePrefix(); + const classes = cx( + `${prefix}--tile`, + `${prefix}--tile--clickable`, + { + [`${prefix}--tile--is-clicked`]: clicked, + [`${prefix}--tile--light`]: light, + }, + className + ); - static defaultProps = { - clicked: false, - onClick: () => {}, - onKeyDown: () => {}, - light: false, - }; + const [isSelected, setIsSelected] = useState(clicked); - handleClick = (evt) => { + function handleOnClick(evt) { evt.persist(); - this.setState( - { - clicked: !this.state.clicked, - }, - () => { - // TODO: Remove handleClick prop when handleClick is deprecated - this.props.handleClick?.(evt) || this.props.onClick?.(evt); - } - ); - }; + setIsSelected(!isSelected); + onClick(evt); + } - handleKeyDown = (evt) => { + function handleOnKeyDown(evt) { evt.persist(); if (matches(evt, [keys.Enter, keys.Space])) { - this.setState( - { - clicked: !this.state.clicked, - }, - () => { - // TODO: Remove handleKeyDown prop when handleKeyDown is deprecated - this.props.handleKeyDown?.(evt) || this.props.onKeyDown(evt); - } - ); - } else { - // TODO: Remove handleKeyDown prop when handleKeyDown is deprecated - this.props.handleKeyDown?.(evt) || this.props.onKeyDown(evt); + evt.preventDefault(); + setIsSelected(!isSelected); + onKeyDown(evt); } - }; - - // eslint-disable-next-line react/prop-types - static getDerivedStateFromProps({ clicked }, state) { - const { prevClicked } = state; - return prevClicked === clicked - ? null - : { - clicked, - prevClicked: clicked, - }; + onKeyDown(evt); } - render() { - const prefix = this.context; - const { - children, - href, - className, - handleClick, // eslint-disable-line - handleKeyDown, // eslint-disable-line - onClick, // eslint-disable-line - onKeyDown, // eslint-disable-line - clicked, // eslint-disable-line - light, - ...rest - } = this.props; - - const classes = cx( - `${prefix}--tile`, - `${prefix}--tile--clickable`, - { - [`${prefix}--tile--is-clicked`]: this.state.clicked, - [`${prefix}--tile--light`]: light, - }, - className - ); + return ( + + {children} + + ); +}); - return ( - - {children} - - ); - } -} +ClickableTile.displayName = 'ClickableTile'; +ClickableTile.propTypes = { + /** + * The child nodes. + */ + children: PropTypes.node, + + /** + * The CSS class names. + */ + className: PropTypes.string, + + /** + * Boolean for whether a tile has been clicked. + */ + clicked: PropTypes.bool, + + /** + * The href for the link. + */ + href: PropTypes.string, -export function SelectableTile(props) { - const { + /** + * `true` to use the light version. For use on $ui-01 backgrounds only. + * Don't use this to make tile background color same as container background color. + */ + light: deprecate( + PropTypes.bool, + 'The `light` prop for `ClickableTile` is no longer needed and has been deprecated. It will be removed in the next major release. Use the Layer component instead.' + ), + + /** + * Specify the function to run when the ClickableTile is clicked + */ + onClick: PropTypes.func, + + /** + * Specify the function to run when the ClickableTile is interacted with via a keyboard + */ + onKeyDown: PropTypes.func, + + /** + * The rel property for the link. + */ + rel: PropTypes.string, +}; + +export const SelectableTile = React.forwardRef(function SelectableTile( + { children, - id, - tabIndex, - value, - name, - title, - // eslint-disable-next-line no-unused-vars - iconDescription, className, - handleClick, - handleKeyDown, - onClick, - onChange, - onKeyDown, - light, disabled, - selected, + id, + light, + name, + onClick = () => {}, + onChange = () => {}, + onKeyDown = () => {}, + selected = false, + tabIndex = 0, + title = 'title', + value = 'value', ...rest - } = props; - + }, + ref +) { const prefix = usePrefix(); - // TODO: replace with onClick when handleClick prop is deprecated - const clickHandler = handleClick || onClick; + const clickHandler = onClick; - // TODO: replace with onKeyDown when handleKeyDown prop is deprecated - const keyDownHandler = handleKeyDown || onKeyDown; + const keyDownHandler = onKeyDown; const [isSelected, setIsSelected] = useState(selected); - const input = useRef(null); + const [prevSelected, setPrevSelected] = useState(selected); + const classes = cx( `${prefix}--tile`, `${prefix}--tile--selectable`, @@ -250,9 +196,6 @@ export function SelectableTile(props) { }, className ); - const inputClasses = cx(`${prefix}--tile-input`, { - [`${prefix}--tile-input--checked`]: isSelected, - }); // TODO: rename to handleClick when handleClick prop is deprecated function handleOnClick(evt) { @@ -279,55 +222,40 @@ export function SelectableTile(props) { onChange(event); } - useEffect(() => { + if (selected !== prevSelected) { setIsSelected(selected); - }, [selected]); + setPrevSelected(selected); + } return ( - <> - - {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */} -