+```
+
+### Various updates
+
+All the same functionality for Tabs is available in v11 and more! Below are the
+minor tweaks in naming or implementation.
+
+- the `type` prop is deprecated. Both "container" and "default" tabs still exist
+ but now can be called by adding the prop `contained` to the `TabList`. See the
+ above "Contained Tabs" for an example.
+- Default tabs are now referred to as line tabs in our documentation here and on
+ our website.
+- `hidden` prop is no longer needed with the new composable Tabs. You have full
+ control over tab content and when it's hidden through the `TabPanel` and
+ `TabPanels` components.
+- `selected` prop is now named `selectedIndex`.
+- `tabContentClassName` is no longer needed. `TabPanel` (equivalent to tab
+ content) takes in a className prop on its outermost node.
+- For `Tab`, `label` is no longer needed. `children` of `Tab` are now the label.
+- Due to its composability, `renderAnchor`, `renderButton`, `renderContent` are
+ no longer needed on `Tab`.
+- `selected` on `Tab` is deprecated in favor or `selectedIndex`, now placed on
+ `Tabs` instead.
+- Because `renderButton` is no longer needed, the associated `tabIndex` prop has
+ also been deprecated.
+
## Feedback
Help us improve this component by providing feedback, asking questions on Slack,
diff --git a/packages/react/src/components/Tabs/next/Tabs.stories.js b/packages/react/src/components/Tabs/Tabs.stories.js
similarity index 72%
rename from packages/react/src/components/Tabs/next/Tabs.stories.js
rename to packages/react/src/components/Tabs/Tabs.stories.js
index b2496eb32731..8c3a1be7fd93 100644
--- a/packages/react/src/components/Tabs/next/Tabs.stories.js
+++ b/packages/react/src/components/Tabs/Tabs.stories.js
@@ -7,9 +7,9 @@
import React from 'react';
import { Tabs, TabList, Tab, TabPanels, TabPanel, IconTab } from './Tabs';
-import TextInput from '../../TextInput';
-import Checkbox from '../../Checkbox';
-import Button from '../../Button';
+import TextInput from '../TextInput';
+import Checkbox from '../Checkbox';
+import Button from '../Button';
import mdx from './Tabs.mdx';
import TabsSkeleton from './Tabs.Skeleton';
@@ -29,11 +29,18 @@ export default {
page: mdx,
},
},
+ argTypes: {
+ light: {
+ table: {
+ disable: true,
+ },
+ },
+ },
};
-export const Default = (args) => (
+export const Default = () => (
);
-export const Icon20Only = (args) => (
+export const Icon20Only = () => (
);
-export const Contained = (args) => (
+export const Contained = () => (
);
-export const Skeleton = (args) => {
+export const Skeleton = () => {
return (
-
+
);
};
+
+export const Playground = (args) => (
+
+
+ Tab Label 1
+ Tab Label 2
+ Tab Label 3
+ Tab Label 4
+
+
+ Tab Panel 1
+ Tab Panel 2
+ Tab Panel 3
+ Tab Panel 4
+
+
+);
+
+Playground.argTypes = {
+ automatic: {
+ control: { type: 'select' },
+ options: ['automatic', 'manual'],
+ },
+ contained: {
+ control: {
+ type: 'boolean',
+ },
+ defaultValue: false,
+ },
+ iconSize: {
+ control: { type: 'select' },
+ options: ['default', 'lg'],
+ },
+ leftOverflowButtonProps: {
+ control: {
+ type: 'object',
+ },
+ },
+ rightOverflowButtonProps: {
+ control: {
+ type: 'object',
+ },
+ },
+ scrollDebounceWait: {
+ control: {
+ type: 'number',
+ },
+ defaultValue: 200,
+ },
+ scrollIntoView: {
+ control: {
+ type: 'boolean',
+ },
+ },
+};
diff --git a/packages/react/src/components/Tabs/index.js b/packages/react/src/components/Tabs/index.js
index c769621ce194..59c9a6dadf3e 100644
--- a/packages/react/src/components/Tabs/index.js
+++ b/packages/react/src/components/Tabs/index.js
@@ -5,26 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
-import * as FeatureFlags from '@carbon/feature-flags';
-import {
- Tabs as TabsNext,
- TabPanel,
- TabPanels,
- TabList,
- IconTab,
- Tab,
-} from './next/Tabs';
-import { default as TabsClassic } from './Tabs';
-import { default as TabsSkeletonClassic } from './Tabs.Skeleton';
-import { default as TabsSkeletonNext } from './next/Tabs.Skeleton';
-
-const Tabs = FeatureFlags.enabled('enable-v11-release')
- ? TabsNext
- : TabsClassic;
-
-const TabsSkeleton = FeatureFlags.enabled('enable-v11-release')
- ? TabsSkeletonNext
- : TabsSkeletonClassic;
+import { Tabs, TabPanel, TabPanels, TabList, IconTab, Tab } from './Tabs';
+import { default as TabsSkeleton } from './Tabs.Skeleton';
export { TabsSkeleton, TabPanels, TabPanel, TabList, IconTab, Tab };
diff --git a/packages/react/src/components/Tabs/next/Tabs.Skeleton.js b/packages/react/src/components/Tabs/next/Tabs.Skeleton.js
deleted file mode 100644
index 86a56b0065a4..000000000000
--- a/packages/react/src/components/Tabs/next/Tabs.Skeleton.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * 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 PropTypes from 'prop-types';
-import React from 'react';
-import cx from 'classnames';
-import { usePrefix } from '../../../internal/usePrefix';
-
-function Tab() {
- const prefix = usePrefix();
- return (
-
-
-
-
-
- );
-}
-
-function TabsSkeleton({ className, contained, ...rest }) {
- const prefix = usePrefix();
- const tabClasses = cx(className, `${prefix}--tabs`, `${prefix}--skeleton`, {
- [`${prefix}--tabs--contained`]: contained,
- });
- return (
-
-
- {Tab()}
- {Tab()}
- {Tab()}
- {Tab()}
- {Tab()}
-
-
- );
-}
-
-TabsSkeleton.propTypes = {
- /**
- * Specify an optional className to add.
- */
- className: PropTypes.string,
-
- /**
- * Provide the type of Tab
- */
- contained: PropTypes.bool,
-};
-
-export default TabsSkeleton;
diff --git a/packages/react/src/components/Tabs/next/Tabs.js b/packages/react/src/components/Tabs/next/Tabs.js
deleted file mode 100644
index d5abd9353044..000000000000
--- a/packages/react/src/components/Tabs/next/Tabs.js
+++ /dev/null
@@ -1,734 +0,0 @@
-/**
- * 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 { ChevronLeft, ChevronRight } from '@carbon/icons-react';
-import cx from 'classnames';
-import debounce from 'lodash.debounce';
-import PropTypes from 'prop-types';
-import React, { useCallback, useState, useRef, useEffect } from 'react';
-import { Tooltip } from '../../Tooltip/next';
-import { useControllableState } from '../../../internal/useControllableState';
-import { useEffectOnce } from '../../../internal/useEffectOnce';
-import { useId } from '../../../internal/useId';
-import useIsomorphicEffect from '../../../internal/useIsomorphicEffect';
-import { useMergedRefs } from '../../../internal/useMergedRefs';
-import { getInteractiveContent } from '../../../internal/useNoInteractiveChildren';
-import { usePrefix } from '../../../internal/usePrefix';
-import { keys, match, matches } from '../../../internal/keyboard';
-import { usePressable } from './usePressable';
-
-// Used to manage the overall state of the Tabs
-const TabsContext = React.createContext();
-
-// Used to keep track of position in a tablist
-const TabContext = React.createContext();
-
-// Used to keep track of position in a list of tab panels
-const TabPanelContext = React.createContext();
-function Tabs({
- children,
- defaultSelectedIndex = 0,
- onChange,
- selectedIndex: controlledSelectedIndex,
-}) {
- const baseId = useId('ccs');
- // The active index is used to track the element which has focus in our tablist
- const [activeIndex, setActiveIndex] = useState(defaultSelectedIndex);
- // The selected index is used for the tab/panel pairing which is "visible"
- const [selectedIndex, setSelectedIndex] = useControllableState({
- value: controlledSelectedIndex,
- defaultValue: defaultSelectedIndex,
- onChange: (value) => {
- if (onChange) {
- onChange({ selectedIndex: value });
- }
- },
- });
-
- const value = {
- baseId,
- activeIndex,
- defaultSelectedIndex,
- setActiveIndex,
- selectedIndex,
- setSelectedIndex,
- };
-
- return
{children};
-}
-
-Tabs.propTypes = {
- /**
- * Provide child elements to be rendered inside of the `Tabs`.
- * These elements should render either `TabsList` or `TabsPanels`
- */
- children: PropTypes.node,
-
- /**
- * Specify which content tab should be initially selected when the component
- * is first rendered
- */
- defaultSelectedIndex: PropTypes.number,
-
- /**
- * Provide an optional function which is called whenever the state of the
- * `Tabs` changes
- */
- onChange: PropTypes.func,
-
- /**
- * Control which content panel is currently selected. This puts the component
- * in a controlled mode and should be used along with `onChange`
- */
- selectedIndex: PropTypes.number,
-};
-
-/**
- * Get the next index for a given keyboard event given a count of the total
- * items and the current index
- * @param {Event} event
- * @param {number} total
- * @param {number} index
- * @returns {number}
- */
-function getNextIndex(event, total, index) {
- if (match(event, keys.ArrowRight)) {
- return (index + 1) % total;
- } else if (match(event, keys.ArrowLeft)) {
- return (total + index - 1) % total;
- } else if (match(event, keys.Home)) {
- return 0;
- } else if (match(event, keys.End)) {
- return total - 1;
- }
-}
-
-function TabList({
- activation = 'automatic',
- 'aria-label': label,
- children,
- className: customClassName,
- contained = false,
- iconSize,
- leftOverflowButtonProps,
- light,
- rightOverflowButtonProps,
- scrollDebounceWait = 200,
- scrollIntoView,
- ...rest
-}) {
- const { activeIndex, selectedIndex, setSelectedIndex, setActiveIndex } =
- React.useContext(TabsContext);
- const prefix = usePrefix();
- const ref = useRef(null);
- const previousButton = useRef(null);
- const nextButton = useRef(null);
- const [isScrollable, setIsScrollable] = useState(false);
- const [scrollLeft, setScrollLeft] = useState(null);
- const className = cx(`${prefix}--tabs`, customClassName, {
- [`${prefix}--tabs--contained`]: contained,
- [`${prefix}--tabs--light`]: light,
- [`${prefix}--tabs__icon--default`]: iconSize === 'default',
- [`${prefix}--tabs__icon--lg`]: iconSize === 'lg',
- });
-
- // Previous Button
- // VISIBLE IF:
- // SCROLLABLE
- // AND SCROLL_LEFT > 0
- const buttonWidth = 44;
- const isPreviousButtonVisible = ref.current
- ? isScrollable && scrollLeft > 0
- : false;
- // Next Button
- // VISIBLE IF:
- // SCROLLABLE
- // AND SCROLL_LEFT + CLIENT_WIDTH < SCROLL_WIDTH
- const isNextButtonVisible = ref.current
- ? scrollLeft + buttonWidth + ref.current.clientWidth <
- ref.current.scrollWidth
- : false;
- const previousButtonClasses = cx(
- `${prefix}--tab--overflow-nav-button`,
- `${prefix}--tab--overflow-nav-button--previous`,
- {
- [`${prefix}--tab--overflow-nav-button--hidden`]: !isPreviousButtonVisible,
- }
- );
- const nextButtonClasses = cx(
- `${prefix}--tab--overflow-nav-button`,
- `${prefix}--tab--overflow-nav-button--next`,
- {
- [`${prefix}--tab--overflow-nav-button--hidden`]: !isNextButtonVisible,
- }
- );
-
- const tabs = useRef([]);
- const debouncedOnScroll = useCallback(() => {
- return debounce((event) => {
- setScrollLeft(event.target.scrollLeft);
- }, scrollDebounceWait);
- }, [scrollDebounceWait]);
-
- function onKeyDown(event) {
- if (
- matches(event, [keys.ArrowRight, keys.ArrowLeft, keys.Home, keys.End])
- ) {
- event.preventDefault();
-
- const activeTabs = tabs.current.filter((tab) => {
- return !tab.disabled;
- });
-
- const currentIndex = activeTabs.indexOf(
- tabs.current[activation === 'automatic' ? selectedIndex : activeIndex]
- );
- const nextIndex = tabs.current.indexOf(
- activeTabs[getNextIndex(event, activeTabs.length, currentIndex)]
- );
-
- if (activation === 'automatic') {
- setSelectedIndex(nextIndex);
- } else if (activation === 'manual') {
- setActiveIndex(nextIndex);
- }
-
- tabs.current[nextIndex].focus();
- }
- }
-
- useEffectOnce(() => {
- const tab = tabs.current[selectedIndex];
- if (scrollIntoView && tab) {
- tab.scrollIntoView({
- block: 'nearest',
- inline: 'nearest',
- });
- }
- });
-
- useEffectOnce(() => {
- if (tabs.current[selectedIndex].disabled) {
- const activeTabs = tabs.current.filter((tab) => {
- return !tab.disabled;
- });
-
- if (activeTabs.length > 0) {
- const tab = activeTabs[0];
- setSelectedIndex(tabs.current.indexOf(tab));
- }
- }
- });
-
- useIsomorphicEffect(() => {
- if (ref.current) {
- setIsScrollable(ref.current.scrollWidth > ref.current.clientWidth);
- }
-
- function handler() {
- if (ref.current) {
- setIsScrollable(ref.current.scrollWidth > ref.current.clientWidth);
- }
- }
-
- const debouncedHandler = debounce(handler, 200);
- window.addEventListener('resize', debouncedHandler);
- return () => {
- debouncedHandler.cancel();
- window.removeEventListener('resize', debouncedHandler);
- };
- }, []);
-
- // updates scroll location for all scroll behavior.
- useIsomorphicEffect(() => {
- if (scrollLeft !== null) {
- ref.current.scrollLeft = scrollLeft;
- }
- }, [scrollLeft]);
-
- useIsomorphicEffect(() => {
- if (!isScrollable) {
- return;
- }
-
- const tab =
- activation === 'manual'
- ? tabs.current[activeIndex]
- : tabs.current[selectedIndex];
- if (tab) {
- // The width of the "scroll buttons"
-
- // The start and end position of the selected tab
- const { width: tabWidth } = tab.getBoundingClientRect();
- const start = tab.offsetLeft;
- const end = tab.offsetLeft + tabWidth;
-
- // The start and end of the visible area for the tabs
- const visibleStart = ref.current.scrollLeft + buttonWidth;
- const visibleEnd =
- ref.current.scrollLeft + ref.current.clientWidth - buttonWidth;
-
- // The beginning of the tab is clipped and not visible
- if (start < visibleStart) {
- setScrollLeft(start - buttonWidth);
- }
-
- // The end of teh tab is clipped and not visible
- if (end > visibleEnd) {
- setScrollLeft(end + buttonWidth - ref.current.clientWidth);
- }
- }
- }, [activation, activeIndex, selectedIndex, isScrollable]);
-
- usePressable(previousButton, {
- onPress({ longPress }) {
- if (!longPress) {
- setScrollLeft(
- Math.max(
- scrollLeft - (ref.current.scrollWidth / tabs.current.length) * 1.5,
- 0
- )
- );
- }
- },
- onLongPress() {
- return createLongPressBehavior(ref, 'backward', setScrollLeft);
- },
- });
-
- usePressable(nextButton, {
- onPress({ longPress }) {
- if (!longPress) {
- setScrollLeft(
- Math.min(
- scrollLeft + (ref.current.scrollWidth / tabs.current.length) * 1.5,
- ref.current.scrollWidth - ref.current.clientWidth
- )
- );
- }
- },
- onLongPress() {
- return createLongPressBehavior(ref, 'forward', setScrollLeft);
- },
- });
-
- return (
-
-
- {/* eslint-disable-next-line jsx-a11y/interactive-supports-focus */}
-
- {React.Children.map(children, (child, index) => {
- return (
-
- {React.cloneElement(child, {
- ref: (node) => {
- tabs.current[index] = node;
- },
- })}
-
- );
- })}
-
-
-
- );
-}
-
-TabList.propTypes = {
- /**
- * Specify whether the content tab should be activated automatically or
- * manually
- */
- activation: PropTypes.oneOf(['automatic', 'manual']),
-
- /**
- * Provide an accessible label to be read when a user interacts with this
- * component
- */
- 'aria-label': PropTypes.string.isRequired,
-
- /**
- * Provide child elements to be rendered inside of `ContentTabs`.
- * These elements should render a `ContentTab`
- */
- children: PropTypes.node,
-
- /**
- * Specify an optional className to be added to the container node
- */
- className: PropTypes.string,
-
- /**
- * Specify whether component is contained type
- */
- contained: PropTypes.bool,
-
- /**
- * If using `IconTab`, specify the size of the icon being used.
- */
- iconSize: PropTypes.oneOf(['default', 'lg']),
-
- /**
- * Provide the props that describe the left overflow button
- */
- leftOverflowButtonProps: PropTypes.object,
-
- /**
- * Specify whether or not to use the light component variant
- */
- light: PropTypes.bool,
-
- /**
- * Provide the props that describe the right overflow button
- */
- rightOverflowButtonProps: PropTypes.object,
-
- /**
- * Optionally provide a delay (in milliseconds) passed to the lodash
- * debounce of the onScroll handler. This will impact the responsiveness
- * of scroll arrow buttons rendering when scrolling to the first or last tab.
- */
- scrollDebounceWait: PropTypes.number,
-
- /**
- * Choose whether or not to automatically scroll to newly selected tabs
- * on component rerender
- */
- scrollIntoView: PropTypes.bool,
-};
-
-/**
- * Helper function to setup the behavior when a button is "long pressed". This
- * function will take a ref to the tablist, a direction, and a setter for
- * scrollLeft and will update the scroll position within a
- * requestAnimationFrame.
- *
- * It returns a cleanup function to be run when the long press is
- * deactivated
- *
- * @param {RefObject} ref
- * @param {'forward' | 'backward'} direction
- * @param {Function} setScrollLeft
- * @returns {Function}
- */
-function createLongPressBehavior(ref, direction, setScrollLeft) {
- // We manually override the scroll behavior to be "auto". If it is set as
- // smooth, this animation does not update correctly
- let defaultScrollBehavior = ref.current.style['scroll-behavior'];
- ref.current.style['scroll-behavior'] = 'auto';
-
- const scrollDelta = direction === 'forward' ? 5 : -5;
- let frameId = null;
-
- function tick() {
- ref.current.scrollLeft = ref.current.scrollLeft + scrollDelta;
- frameId = requestAnimationFrame(tick);
- }
-
- frameId = requestAnimationFrame(tick);
-
- return () => {
- // Restore the previous scroll behavior
- ref.current.style['scroll-behavior'] = defaultScrollBehavior;
-
- // Make sure that our `scrollLeft` value is in sync with the existing
- // `ref` after our requestAnimationFrame loop above
- setScrollLeft(ref.current.scrollLeft);
-
- if (frameId) {
- cancelAnimationFrame(frameId);
- }
- };
-}
-
-const Tab = React.forwardRef(function Tab(
- {
- as: BaseComponent = 'button',
- children,
- className: customClassName,
- disabled,
- onClick,
- onKeyDown,
- ...rest
- },
- ref
-) {
- const prefix = usePrefix();
- const { selectedIndex, setSelectedIndex, baseId } =
- React.useContext(TabsContext);
- const index = React.useContext(TabContext);
- const id = `${baseId}-tab-${index}`;
- const panelId = `${baseId}-tabpanel-${index}`;
- const className = cx(
- `${prefix}--tabs__nav-item`,
- `${prefix}--tabs__nav-link`,
- customClassName,
- {
- [`${prefix}--tabs__nav-item--selected`]: selectedIndex === index,
- [`${prefix}--tabs__nav-item--disabled`]: disabled,
- }
- );
-
- return (
-
{
- if (disabled) {
- return;
- }
- setSelectedIndex(index);
- if (onClick) {
- onClick(evt);
- }
- }}
- onKeyDown={onKeyDown}
- tabIndex={selectedIndex === index ? '0' : '-1'}
- type="button">
- {children}
-
- );
-});
-
-Tab.propTypes = {
- /**
- * Provide a custom element to render instead of the default button
- */
- as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]),
-
- /**
- * Provide child elements to be rendered inside of `Tab`.
- */
- children: PropTypes.node,
-
- /**
- * Specify an optional className to be added to your Tab
- */
- className: PropTypes.string,
-
- /**
- * Whether your Tab is disabled.
- */
- disabled: PropTypes.bool,
-
- /**
- * Provide a handler that is invoked when a user clicks on the control
- */
- onClick: PropTypes.func,
-
- /**
- * Provide a handler that is invoked on the key down event for the control
- */
- onKeyDown: PropTypes.func,
-
- /*
- * An optional parameter to allow overriding the anchor rendering.
- * Useful for using Tab along with react-router or other client
- * side router libraries.
- **/
- renderButton: PropTypes.func,
-};
-
-const IconTab = React.forwardRef(function IconTab(
- {
- children,
- className: customClassName,
- defaultOpen = false,
- enterDelayMs,
- leaveDelayMs,
- label,
- ...rest
- },
- ref
-) {
- const prefix = usePrefix();
-
- const classNames = cx(`${prefix}--tabs__nav-item--icon`, customClassName);
- return (
-
-
- {children}
-
-
- );
-});
-
-IconTab.propTypes = {
- /**
- * Provide an icon to be rendered inside of `IconTab` as the visual label for Tab.
- */
- children: PropTypes.node,
-
- /**
- * Specify an optional className to be added to your Tab
- */
- className: PropTypes.string,
-
- /**
- * Specify whether the tooltip for the icon should be open when it first renders
- */
- defaultOpen: PropTypes.bool,
-
- /**
- * Specify the duration in milliseconds to delay before displaying the tooltip for the icon.
- */
- enterDelayMs: PropTypes.number,
-
- /**
- * Provide the label to be rendered inside of the Tooltip. The label will use
- * `aria-labelledby` and will fully describe the child node that is provided.
- * This means that if you have text in the child node it will not be
- * announced to the screen reader.
- */
- label: PropTypes.node.isRequired,
-
- /**
- * Specify the duration in milliseconds to delay before hiding the tooltip
- */
- leaveDelayMs: PropTypes.number,
-};
-
-const TabPanel = React.forwardRef(function TabPanel(
- { children, className: customClassName, ...rest },
- forwardRef
-) {
- const prefix = usePrefix();
- const panel = useRef(null);
- const ref = useMergedRefs([forwardRef, panel]);
-
- const [tabIndex, setTabIndex] = useState('0');
- const [interactiveContent, setInteractiveContent] = useState(false);
- const { selectedIndex, baseId } = React.useContext(TabsContext);
- const index = React.useContext(TabPanelContext);
- const id = `${baseId}-tabpanel-${index}`;
- const tabId = `${baseId}-tab-${index}`;
- const className = cx(`${prefix}--tab-content`, customClassName, {
- [`${prefix}--tab-content--interactive`]: interactiveContent,
- });
-
- useEffectOnce(() => {
- if (!panel.current) {
- return;
- }
-
- const content = getInteractiveContent(panel.current);
- if (content) {
- setInteractiveContent(true);
- setTabIndex('-1');
- }
- });
-
- // tabindex should only be 0 if no interactive content in children
- useEffect(() => {
- if (!panel.current) {
- return;
- }
-
- const { current: node } = panel;
-
- function callback() {
- const content = getInteractiveContent(node);
- if (content) {
- setInteractiveContent(true);
- setTabIndex('-1');
- } else {
- setInteractiveContent(false);
- setTabIndex('0');
- }
- }
-
- const observer = new MutationObserver(callback);
-
- observer.observe(node, {
- childList: true,
- subtree: true,
- });
-
- return () => {
- observer.disconnect(node);
- };
- }, []);
-
- return (
-
- {children}
-
- );
-});
-
-TabPanel.propTypes = {
- /**
- * Provide child elements to be rendered inside of `TabPanel`.
- */
- children: PropTypes.node,
-
- /**
- * Specify an optional className to be added to TabPanel.
- */
- className: PropTypes.string,
-};
-
-function TabPanels({ children }) {
- return React.Children.map(children, (child, index) => {
- return (
-
{child}
- );
- });
-}
-
-TabPanels.propTypes = {
- /**
- * Provide child elements to be rendered inside of `TabPanels`.
- */
- children: PropTypes.node,
-};
-
-export { Tabs, Tab, IconTab, TabPanel, TabPanels, TabList };
diff --git a/packages/react/src/components/Tabs/next/Tabs.mdx b/packages/react/src/components/Tabs/next/Tabs.mdx
deleted file mode 100644
index eb55e496372b..000000000000
--- a/packages/react/src/components/Tabs/next/Tabs.mdx
+++ /dev/null
@@ -1,151 +0,0 @@
-import { Props, Preview, Story } from '@storybook/addon-docs';
-import { Tabs, TabList, Tab, TabPanels, TabPanel } from './Tabs';
-
-# Tabs
-
-[Source code](https://github.com/carbon-design-system/carbon/tree/main/packages/react/src/components/Tabs)
- |
-[Usage guidelines](https://www.carbondesignsystem.com/components/tabs/usage)
- |
-[Accessibility](https://www.carbondesignsystem.com/components/tabs/accessibility)
-
-## Table of Contents
-
-- [Overview](#overview)
- - [Line Tabs](#line-tabs)
- - [Container Tabs](#container-tabs)
-- [Component API](#component-api)
- - [Tab `renderContent`](#tab-rendercontent)
-- [Feedback](#feedback)
-
-## Overview
-
-Use tabs to allow users to navigate easily between views within the same
-context. Tabs are now more composable, meaning that you have more flexibility in
-what is in rendered inside of `Tab` and `TabPanel`.
-
-### Line Tabs
-
-
-
-
-
-### Contained Tabs
-
-
-
-
-
-### Icon Tabs
-
-
-
-
-
-
-
-
-
-## Component API
-
-
-
-### Tab - render content on click
-
-You will occasionally run into a situation where you only want Tab content to be
-loaded when the Tab is clicked. In v11, to do this, you can this by setting
-`activation` to `manual`:
-
-```jsx
-
-
- Tab Label 1
- Tab Label 2
- Tab Label 3
-
-
- Tab Panel 1
- Tab Panel 2
- Tab Panel 3
-
-
-```
-
-## V11
-
-### Tabs composition
-
-Tabs got a big revamp in v11! Tabs are now more composable than ever before,
-meaning that you have the flexibity and control on your end to make them look
-and act how you want. The biggest difference is that the Tab label and the Tab
-content are now separate components.
-
-Example of Tabs in v10:
-
-```js
-
-
- Content for first tab goes here.
-
-
- Content for second tab goes here.
-
-
- Content for third tab goes here.
-
-
- Content for fourth tab goes here.
-
-
-```
-
-Those same Tabs, now in v11:
-
-```js
-
-
- Tab Label 1
- Tab Label 2
- Tab Label 3
- Tab Label 4 shows truncation
-
-
- Content for first tab goes here.
- Content for second tab goes here.
- Content for third tab goes here.
- Content for fourth tab goes here.
-
-
-```
-
-### Various updates
-
-All the same functionality for Tabs is available in v11 and more! Below are the
-minor tweaks in naming or implementation.
-
-- the `type` prop is deprecated. Both "container" and "default" tabs still exist
- but now can be called by adding the prop `contained` to the `TabList`. See the
- above "Contained Tabs" for an example.
-- Default tabs are now referred to as line tabs in our documentation here and on
- our website.
-- `hidden` prop is no longer needed with the new composable Tabs. You have full
- control over tab content and when it's hidden through the `TabPanel` and
- `TabPanels` components.
-- `selected` prop is now named `selectedIndex`.
-- `tabContentClassName` is no longer needed. `TabPanel` (equivalent to tab
- content) takes in a className prop on its outermost node.
-- For `Tab`, `label` is no longer needed. `children` of `Tab` are now the label.
-- Due to its composability, `renderAnchor`, `renderButton`, `renderContent` are
- no longer needed on `Tab`.
-- `selected` on `Tab` is deprecated in favor or `selectedIndex`, now placed on
- `Tabs` instead.
-- Because `renderButton` is no longer needed, the associated `tabIndex` prop has
- also been deprecated.
-
-## Feedback
-
-Help us improve this component by providing feedback, asking questions on Slack,
-or updating this file on
-[GitHub](https://github.com/carbon-design-system/carbon/edit/main/packages/react/src/components/Tabs/Tabs.mdx).
diff --git a/packages/react/src/components/Tabs/next/usePressable.js b/packages/react/src/components/Tabs/usePressable.js
similarity index 100%
rename from packages/react/src/components/Tabs/next/usePressable.js
rename to packages/react/src/components/Tabs/usePressable.js
diff --git a/packages/react/src/components/TextArea/TextArea-story.js b/packages/react/src/components/TextArea/TextArea-story.js
deleted file mode 100644
index 47c6a057a6ff..000000000000
--- a/packages/react/src/components/TextArea/TextArea-story.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * 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 from 'react';
-import { action } from '@storybook/addon-actions';
-
-import { withKnobs, boolean, number, text } from '@storybook/addon-knobs';
-import TextArea from '../TextArea';
-import TextAreaSkeleton from '../TextArea/TextArea.Skeleton';
-import mdx from './TextArea.mdx';
-import { FeatureFlags } from '../FeatureFlags';
-
-const TextAreaProps = () => ({
- className: 'some-class',
- disabled: boolean('Disabled (disabled)', false),
- light: boolean('Light variant (light)', false),
- hideLabel: boolean('No label (hideLabel)', false),
- labelText: text('Label text (labelText)', 'Text Area label'),
- invalid: boolean('Show form validation UI (invalid)', false),
- invalidText: text(
- 'Content of form validation UI (invalidText)',
- 'A valid value is required'
- ),
- helperText: text('Helper text (helperText)', 'Optional helper text.'),
- enableCounter: boolean(
- 'Enable character counter/limit (enableCounter)',
- false
- ),
- maxCount: number('Character limit (maxCount)', undefined),
- id: 'test2',
- cols: number('Columns (columns)', 50),
- rows: number('Rows (rows)', 4),
- onChange: action('onChange'),
- onClick: action('onClick'),
-});
-
-export default {
- title: 'Components/TextArea',
- component: TextArea,
- decorators: [withKnobs],
- subcomponents: {
- TextAreaSkeleton,
- },
- parameters: {
- docs: {
- page: mdx,
- },
- },
-};
-
-export const Default = () =>
;
-
-export const Skeleton = () =>
;
-
-export const ClassNameChangeTest = () => (
- <>
-
-
-
-
-
- >
-);
diff --git a/packages/react/src/components/TextArea/TextArea.stories.js b/packages/react/src/components/TextArea/TextArea.stories.js
new file mode 100644
index 000000000000..5a89e26b4738
--- /dev/null
+++ b/packages/react/src/components/TextArea/TextArea.stories.js
@@ -0,0 +1,150 @@
+/**
+ * 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 from 'react';
+import { default as TextArea, TextAreaSkeleton } from './';
+import { Layer } from '../Layer';
+
+export default {
+ title: 'Components/TextArea',
+ component: TextArea,
+ subcomponents: {
+ TextAreaSkeleton,
+ },
+};
+
+export const Default = () => (
+
+);
+
+export const WithLayer = () => {
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export const Skeleton = () =>
;
+
+export const Playground = (args) =>
;
+
+Playground.argTypes = {
+ className: {
+ control: {
+ type: 'text',
+ },
+ },
+ cols: {
+ control: {
+ type: 'number',
+ },
+ defaultValue: 50,
+ },
+ defaultValue: {
+ control: {
+ type: 'text',
+ },
+ },
+ value: {
+ control: {
+ type: 'text',
+ },
+ },
+ disabled: {
+ control: {
+ type: 'boolean',
+ },
+ defaultValue: false,
+ },
+ enableCounter: {
+ control: {
+ type: 'boolean',
+ },
+ defaultValue: false,
+ },
+ helperText: {
+ control: {
+ type: 'text',
+ },
+ },
+ hideLabel: {
+ control: {
+ type: 'boolean',
+ },
+ defaultValue: false,
+ },
+ id: {
+ control: {
+ type: 'text',
+ },
+ },
+ invalid: {
+ control: {
+ type: 'boolean',
+ },
+ defaultValue: false,
+ },
+ invalidText: {
+ control: {
+ type: 'text',
+ },
+ },
+ labelText: {
+ control: {
+ type: 'text',
+ },
+ },
+ light: {
+ control: {
+ type: 'boolean',
+ },
+ defaultValue: false,
+ },
+ maxCount: {
+ control: {
+ type: 'number',
+ },
+ },
+};
+
+Playground.args = {
+ enableCounter: true,
+ helperText: 'TextArea helper text',
+ labelText: 'TextArea label',
+ maxCount: 500,
+};
diff --git a/packages/react/src/components/TextArea/next/TextArea.stories.js b/packages/react/src/components/TextArea/next/TextArea.stories.js
deleted file mode 100644
index a8b2b0d34c5a..000000000000
--- a/packages/react/src/components/TextArea/next/TextArea.stories.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * 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 from 'react';
-import { default as TextArea, TextAreaSkeleton } from '../';
-import { Layer } from '../../Layer';
-
-export default {
- title: 'Components/TextArea',
- component: TextArea,
- subcomponents: {
- TextAreaSkeleton,
- },
- argTypes: {
- light: {
- table: {
- disable: true,
- },
- },
- },
-};
-
-export const Default = () => (
-
-);
-
-export const WithLayer = () => {
- return (
- <>
-
-
-
-
-
-
-
- >
- );
-};
-
-export const Skeleton = () =>
;
-
-export const Playground = (args) => (
-
-);
diff --git a/packages/react/src/components/TextInput/TextInput.js b/packages/react/src/components/TextInput/TextInput.js
index e57bf1a082b3..dab5ce186ffd 100644
--- a/packages/react/src/components/TextInput/TextInput.js
+++ b/packages/react/src/components/TextInput/TextInput.js
@@ -142,7 +142,6 @@ const TextInput = React.forwardRef(function TextInput(
[`${prefix}--text-input__invalid-icon`]:
normalizedProps.invalid || normalizedProps.warn,
[`${prefix}--text-input__invalid-icon--warning`]: normalizedProps.warn,
- [`${prefix}--text-input__readonly-icon`]: readOnly,
});
const counterClasses = classNames(`${prefix}--label`, {
diff --git a/packages/react/src/components/TextInput/__tests__/TextInput-test.js b/packages/react/src/components/TextInput/__tests__/TextInput-test.js
index c9d66d93a72f..cf9095cf46e7 100644
--- a/packages/react/src/components/TextInput/__tests__/TextInput-test.js
+++ b/packages/react/src/components/TextInput/__tests__/TextInput-test.js
@@ -295,7 +295,7 @@ describe('TextInput', () => {
it('should respect readOnly prop', () => {
const onChange = jest.fn();
const onClick = jest.fn();
- const { container } = render(
+ render(
{
userEvent.type(screen.getByRole('textbox'), 'x');
expect(screen.getByRole('textbox')).not.toHaveValue('x');
expect(onChange).toHaveBeenCalledTimes(0);
-
- // Should display the "read-only" icon
- const icon = container.querySelector(
- `svg.${prefix}--text-input__readonly-icon`
- );
- expect(icon).toBeInTheDocument();
});
it('should not render counter with only enableCounter prop passed in', () => {
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 */}
-