diff --git a/.all-contributorsrc b/.all-contributorsrc index 4e65102ce0f5..8561df1f98d2 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -905,6 +905,16 @@ "code" ] }, + { + "login": "61130061", + "name": "Llam4u", + "avatar_url": "https://avatars.githubusercontent.com/u/54393468?v=4", + "profile": "https://61130061.github.io/llam4u-terminal/", + "contributions": [ + "code", + "bug" + ] + }, { "login": "torresga", "name": "G. Torres", diff --git a/README.md b/README.md index cd5fdef7ad2c..80aa2b8258ec 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,7 @@ check out our [Contributing Guide](/.github/CONTRIBUTING.md) and our
Hannele Valtanen

πŸ’» +
Llam4u

πŸ’» πŸ›
G. Torres

πŸ’»
Fiona

πŸ’»
kindoflew

πŸ’» diff --git a/e2e/icons-react/__snapshots__/PublicAPI-test.js.snap b/e2e/icons-react/__snapshots__/PublicAPI-test.js.snap index fad5ee404eb3..df2fc1e5a690 100644 --- a/e2e/icons-react/__snapshots__/PublicAPI-test.js.snap +++ b/e2e/icons-react/__snapshots__/PublicAPI-test.js.snap @@ -502,6 +502,7 @@ Array [ "DewPointFilled", "Diagram", "DiagramReference", + "DiamondFill", "DirectLink", "DirectionBearRight_01", "DirectionBearRight_01Filled", diff --git a/e2e/icons-vue/__snapshots__/PublicAPI-test.js.snap b/e2e/icons-vue/__snapshots__/PublicAPI-test.js.snap index 08ba460ce045..2f3cf78059b8 100644 --- a/e2e/icons-vue/__snapshots__/PublicAPI-test.js.snap +++ b/e2e/icons-vue/__snapshots__/PublicAPI-test.js.snap @@ -1993,6 +1993,7 @@ Array [ "DiagramReference20", "DiagramReference24", "DiagramReference32", + "DiamondFillGlyph", "DirectLink16", "DirectLink20", "DirectLink24", @@ -6924,7 +6925,6 @@ Array [ "UndefinedFilled20", "UndefinedFilled24", "UndefinedFilled32", - "UndefinedGlyph", "Undo16", "Undo20", "Undo24", diff --git a/e2e/icons/__snapshots__/PublicAPI-test.js.snap b/e2e/icons/__snapshots__/PublicAPI-test.js.snap index c14d62d9ad2e..11e054fb4e31 100644 --- a/e2e/icons/__snapshots__/PublicAPI-test.js.snap +++ b/e2e/icons/__snapshots__/PublicAPI-test.js.snap @@ -1992,6 +1992,7 @@ Array [ "DiagramReference20", "DiagramReference24", "DiagramReference32", + "DiamondFillGlyph", "DirectLink16", "DirectLink20", "DirectLink24", @@ -6923,7 +6924,6 @@ Array [ "UndefinedFilled20", "UndefinedFilled24", "UndefinedFilled32", - "UndefinedGlyph", "Undo16", "Undo20", "Undo24", diff --git a/packages/icons/categories.yml b/packages/icons/categories.yml index 92fc398b2c48..9992517fb939 100644 --- a/packages/icons/categories.yml +++ b/packages/icons/categories.yml @@ -1316,6 +1316,8 @@ categories: - ZIP--reference - name: Status members: + - caution + - caution-inverted - checkmark - checkmark--filled - checkmark--filled--error @@ -1323,19 +1325,17 @@ categories: - checkmark--outline - checkmark--outline--error - checkmark--outline--warning + - circle-fill + - circle-stroke - condition--point - condition--wait-point + - critical + - critical-severity + - diamond-fill - error - error--filled - error--outline - - caution - - caution-inverted - - circle-fill - - circle-stroke - - critical-severity - low-severity - - critical - - square-fill - help - help--filled - in-progress @@ -1355,6 +1355,7 @@ categories: - pending - pending--filled - queued + - square-fill - undefined - undefined--filled - unknown diff --git a/packages/icons/icons.yml b/packages/icons/icons.yml index 85a35eaf0bc6..2a4750b3fa92 100644 --- a/packages/icons/icons.yml +++ b/packages/icons/icons.yml @@ -5855,6 +5855,14 @@ - source sizes: - 32 +- name: diamond-fill + friendly_name: Diamond fill + aliases: + - diamond + - fill + - status + sizes: + - glyph - name: dicom--6000 friendly_name: DICOM 6000 sizes: @@ -18463,7 +18471,6 @@ - undefined sizes: - 32 - - glyph - name: undefined--filled friendly_name: Undefined filled sizes: diff --git a/packages/icons/src/svg/undefined.svg b/packages/icons/src/svg/diamond-fill.svg similarity index 100% rename from packages/icons/src/svg/undefined.svg rename to packages/icons/src/svg/diamond-fill.svg diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 318439b116fd..4f65b27dfd83 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -1363,6 +1363,17 @@ Map { "isRequired": true, "type": "oneOfType", }, + "size": Object { + "args": Array [ + Array [ + "sm", + "md", + "lg", + "xl", + ], + ], + "type": "oneOf", + }, }, }, "ContainedListItem" => Object { diff --git a/packages/react/src/components/ContainedList/ContainedList.js b/packages/react/src/components/ContainedList/ContainedList.js index 85440bdf5807..d4cc2b2ddfef 100644 --- a/packages/react/src/components/ContainedList/ContainedList.js +++ b/packages/react/src/components/ContainedList/ContainedList.js @@ -19,6 +19,7 @@ function ContainedList({ className, kind = variants[0], label, + size = 'lg', }) { const labelId = `${useId('contained-list')}-header`; const prefix = usePrefix(); @@ -26,6 +27,7 @@ function ContainedList({ const classes = classNames( `${prefix}--contained-list`, `${prefix}--contained-list--${kind}`, + `${prefix}--contained-list--${size}`, className ); @@ -69,6 +71,11 @@ ContainedList.propTypes = { * A label describing the contained list. */ label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, + + /** + * Specify the size of the contained list. + */ + size: PropTypes.oneOf(['sm', 'md', 'lg', 'xl']), }; export default ContainedList; diff --git a/packages/react/src/components/ContainedList/ContainedList.stories.js b/packages/react/src/components/ContainedList/ContainedList.stories.js index 0835b9d476be..abf0236b603e 100644 --- a/packages/react/src/components/ContainedList/ContainedList.stories.js +++ b/packages/react/src/components/ContainedList/ContainedList.stories.js @@ -90,16 +90,14 @@ export const WithInteractiveItems = () => { const onClick = action('onClick (ContainedListItem)'); return ( - - - List item - - List item - - List item - List item - - + + List item + + List item + + List item + List item + ); }; @@ -114,19 +112,17 @@ export const WithActions = () => { ); return ( - - }> - List item - - List item - - List item - List item - - + }> + List item + + List item + + List item + List item + ); }; @@ -142,89 +138,81 @@ export const WithInteractiveItemsAndActions = () => { ); return ( - - }> - - List item - - - List item - - - List item - - - List item - - - + }> + + List item + + + List item + + + List item + + + List item + + ); }; export const WithListTitleDecorators = () => { return ( - - - List title - 4 - - } - kind="on-page"> - List item - List item - List item - List item - - + + List title + 4 + + } + kind="on-page"> + List item + List item + List item + List item + ); }; export const WithIcons = () => { return ( - - - List item - List item - List item - List item - - + + List item + List item + List item + List item + ); }; export const WithLayer = () => { return ( - - - - List item - List item - - - + + + List item + List item + + + + + List item + List item + + List item List item - - - List item - List item - - - - - + + + ); }; @@ -250,4 +238,7 @@ Playground.argTypes = { kind: { defaultValue: 'on-page', }, + size: { + defaultValue: 'lg', + }, }; diff --git a/packages/react/src/components/FormLabel/FormLabel.stories.js b/packages/react/src/components/FormLabel/FormLabel.stories.js index 62bfdfeae962..9d4b847013c9 100644 --- a/packages/react/src/components/FormLabel/FormLabel.stories.js +++ b/packages/react/src/components/FormLabel/FormLabel.stories.js @@ -8,7 +8,7 @@ import React from 'react'; import FormLabel from './FormLabel'; -import { Tooltip } from '../Tooltip/next/Tooltip'; +import { Tooltip } from '../Tooltip'; import { Information } from '@carbon/icons-react'; import { ActionableNotification } from '../Notification'; import { Toggletip, ToggletipButton, ToggletipContent } from '../Toggletip'; diff --git a/packages/react/src/components/IconButton/index.js b/packages/react/src/components/IconButton/index.js index 112a7329b985..4d649109ad37 100644 --- a/packages/react/src/components/IconButton/index.js +++ b/packages/react/src/components/IconButton/index.js @@ -8,7 +8,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import Button from '../Button'; -import { Tooltip } from '../Tooltip/next'; +import { Tooltip } from '../Tooltip'; import { usePrefix } from '../../internal/usePrefix'; import cx from 'classnames'; diff --git a/packages/react/src/components/Pagination/Pagination-test.js b/packages/react/src/components/Pagination/Pagination-test.js index bc00dbffe288..af0c2e2acdda 100644 --- a/packages/react/src/components/Pagination/Pagination-test.js +++ b/packages/react/src/components/Pagination/Pagination-test.js @@ -187,6 +187,16 @@ describe('Pagination', () => { expect(screen.getByText(`pΓ‘gina ${page}`)).toBeInTheDocument(); }); + it('should not include page count when pagesUnknown', () => { + const page = 1; + render( + + ); + + expect(screen.getByText(`page`)).toBeInTheDocument(); + expect(screen.queryByText(`page ${page}`)).not.toBeInTheDocument(); + }); + it('should respect size prop', () => { const { container } = render(); diff --git a/packages/react/src/components/Pagination/Pagination.js b/packages/react/src/components/Pagination/Pagination.js index 9facda43f19a..f48b9a5d9e5f 100644 --- a/packages/react/src/components/Pagination/Pagination.js +++ b/packages/react/src/components/Pagination/Pagination.js @@ -67,7 +67,7 @@ const Pagination = React.forwardRef(function Pagination( pageSize: controlledPageSize, pageSizeInputDisabled, pageSizes: controlledPageSizes, - pageText = (page) => `page ${page}`, + pageText = (page, pagesUnknown) => `page ${pagesUnknown ? '' : page}`, pagesUnknown = false, size = 'md', totalItems, @@ -251,6 +251,14 @@ const Pagination = React.forwardRef(function Pagination(
+ {pagesUnknown ? ( + + {pagesUnknown + ? pageText(page, pagesUnknown) + : pageRangeText(page, totalPages)} + + ) : null} - - {pagesUnknown ? pageText(page) : pageRangeText(page, totalPages)} - + {pagesUnknown ? null : ( + + {pagesUnknown + ? pageText(page, pagesUnknown) + : pageRangeText(page, totalPages)} + + )}
{ const definition = 'Uniform Resource Locator; the address of a resource (such as a document or website) on the Internet.'; @@ -102,10 +101,23 @@ Playground.argTypes = { }, defaultValue: 'Example definition', }, + id: { + table: { disable: true }, + }, openOnHover: { control: { type: 'boolean', }, defaultValue: false, }, + tooltipText: { + table: { + disable: true, + }, + }, + triggerClassName: { + table: { + disable: true, + }, + }, }; diff --git a/packages/react/src/components/Tooltip/Tooltip-story.js b/packages/react/src/components/Tooltip/Tooltip-story.js deleted file mode 100644 index 414d2fba6cfa..000000000000 --- a/packages/react/src/components/Tooltip/Tooltip-story.js +++ /dev/null @@ -1,314 +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, { useState } from 'react'; -import { - withKnobs, - select, - text, - number, - boolean, -} from '@storybook/addon-knobs'; -import Tooltip from '../Tooltip'; -import { Tooltip as OGTooltip } from './Tooltip'; -import Button from '../Button'; -import { OverflowMenuVertical } from '@carbon/icons-react'; -import mdx from './Tooltip.mdx'; - -const prefix = 'cds'; -const directions = { - 'Bottom (bottom)': 'bottom', - 'Left (left)': 'left', - 'Top (top)': 'top', - 'Right (right)': 'right', -}; -const alignments = { - 'Start (start)': 'start', - 'Center (center)': 'center', - 'End (end)': 'end', -}; - -const props = { - withIcon: () => ({ - align: select('Tooltip alignment (align)', alignments, 'center'), - direction: select('Tooltip direction (direction)', directions, 'bottom'), - triggerText: text('Trigger text (triggerText)', 'Tooltip label'), - tabIndex: number('Tab index (tabIndex in )', 0), - selectorPrimaryFocus: text( - 'Primary focus element selector (selectorPrimaryFocus)', - '' - ), - }), - autoOrientation: () => ({ - align: select('Tooltip alignment (align)', alignments, 'center'), - direction: select('Tooltip direction (direction)', directions, 'bottom'), - triggerText: text('Trigger text (triggerText)', 'Test'), - tabIndex: number('Tab index (tabIndex in )', 0), - selectorPrimaryFocus: text( - 'Primary focus element selector (selectorPrimaryFocus)', - '' - ), - autoOrientation: boolean('Auto orientation', true), - }), - withoutIcon: () => ({ - showIcon: false, - align: select('Tooltip alignment (align)', alignments, 'center'), - direction: select('Tooltip direction (direction)', directions, 'bottom'), - triggerText: text('Trigger text (triggerText)', 'Tooltip label'), - tabIndex: number('Tab index (tabIndex in )', 0), - selectorPrimaryFocus: text( - 'Primary focus element selector (selectorPrimaryFocus)', - '' - ), - }), - customIcon: () => ({ - showIcon: true, - align: select('Tooltip alignment (align)', alignments, 'center'), - direction: select('Tooltip direction (direction)', directions, 'bottom'), - triggerText: text('Trigger text (triggerText)', 'Tooltip label'), - tabIndex: number('Tab index (tabIndex in )', 0), - selectorPrimaryFocus: text( - 'Primary focus element selector (selectorPrimaryFocus)', - '' - ), - // eslint-disable-next-line react/display-name - renderIcon: () => ( - - - - - - ), - }), - customIconOnly: () => ({ - showIcon: true, - align: select('Tooltip alignment (align)', alignments, 'center'), - direction: select('Tooltip direction (direction)', directions, 'bottom'), - iconDescription: 'Helpful Information', - tabIndex: number('Tab index (tabIndex in )', 0), - selectorPrimaryFocus: text( - 'Primary focus element selector (selectorPrimaryFocus)', - '' - ), - renderIcon: OverflowMenuVertical, - }), -}; - -const containerStyles = { - height: 'calc(100vh - 6rem)', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', -}; - -Tooltip.displayName = 'Tooltip'; - -function UncontrolledTooltipExample() { - const [value, setValue] = useState(true); - return ( - <> - - -
- My text wrapped with tooltip
} - open={value}> -

- This is some tooltip text. This box shows the maximum amount of text - that should appear inside. If more room is needed please use a modal - instead. -

-
- - Learn More - - -
-
-
- - ); -} - -export default { - title: 'Components/Tooltip', - component: OGTooltip, - decorators: [withKnobs], - - parameters: { - docs: { - page: mdx, - }, - }, -}; - -export const DefaultBottom = () => ( -
- -

- This is some tooltip text. This box shows the maximum amount of text - that should appear inside. If more room is needed please use a modal - instead. -

-
-
-); - -DefaultBottom.storyName = 'default (bottom)'; - -export const AutoOrientation = () => ( -
- {/* Top Left */} -
- -

- This is some tooltip text. This box shows the maximum amount of text - that should appear inside. If more room is needed please use a modal - instead. -

-
- - Learn More - - -
-
-
- {/* Top Right */} -
- -

- This is some tooltip text. This box shows the maximum amount of text - that should appear inside. If more room is needed please use a modal - instead. -

-
- - Learn More - - -
-
-
- {/* Bottom Left */} -
- -

- This is some tooltip text. This box shows the maximum amount of text - that should appear inside. If more room is needed please use a modal - instead. -

-
- - Learn More - - -
-
-
- {/* Bottom Right */} -
- -

- This is some tooltip text. This box shows the maximum amount of text - that should appear inside. If more room is needed please use a modal - instead. -

-
- - Learn More - - -
-
-
-
-); - -AutoOrientation.storyName = 'auto orientation'; - -export const NoIcon = () => ( -
- -

- This is some tooltip text. This box shows the maximum amount of text - that should appear inside. If more room is needed please use a modal - instead. -

-
- - Learn More - - -
-
-
-); - -NoIcon.storyName = 'no icon'; - -export const RenderCustomIcon = () => ( -
- -

- This is some tooltip text. This box shows the maximum amount of text - that should appear inside. If more room is needed please use a modal - instead. -

-
- - Learn More - - -
-
-
-); - -RenderCustomIcon.storyName = 'render custom icon'; - -export const OnlyCustomIcon = () => ( -
- -

- This is some tooltip text. This box shows the maximum amount of text - that should appear inside. If more room is needed please use a modal - instead. -

-
- - Learn More - - -
-
-
-); - -OnlyCustomIcon.storyName = 'only custom icon'; - -export const UncontrolledTooltip = () => ; - -UncontrolledTooltip.storyName = 'uncontrolled tooltip'; diff --git a/packages/react/src/components/Tooltip/Tooltip-test.js b/packages/react/src/components/Tooltip/Tooltip-test.js deleted file mode 100644 index ac546a4cd01b..000000000000 --- a/packages/react/src/components/Tooltip/Tooltip-test.js +++ /dev/null @@ -1,331 +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, { Component, useState } from 'react'; -import debounce from 'lodash.debounce'; // eslint-disable-line no-unused-vars -import FloatingMenu from '../../internal/FloatingMenu'; -import Tooltip from '../Tooltip'; -import Link from '../Link'; -import Button from '../Button'; -import { mount } from 'enzyme'; -import { screen, render } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { Information, Add, OverflowMenuVertical } from '@carbon/icons-react'; -import '@testing-library/jest-dom'; - -const prefix = 'cds'; - -jest.mock('lodash.debounce', () => (fn) => { - fn.cancel = jest.fn(); - return fn; -}); - -describe('Tooltip', () => { - // An icon component class - class CustomIcon extends Component { - render() { - return
; - } - } - - describe('Renders as expected with defaults', () => { - const wrapper = mount( - -

Tooltip label

-

Lorem ipsum dolor sit amet

-
- ); - - const trigger = wrapper.find(`.${prefix}--tooltip__trigger`); - - describe('tooltip trigger', () => { - it('renders a tooltip container', () => { - expect(trigger.length).toEqual(1); - }); - - it('renders the info icon', () => { - const icon = trigger.find(Information); - expect(icon.length).toBe(1); - }); - }); - }); - - describe('Renders as expected with specified properties', () => { - const wrapper = mount( - -

Tooltip label

-

Lorem ipsum dolor sit amet

-
- ); - const label = wrapper.find(`.${prefix}--tooltip__label`); - const floatingMenu = wrapper.find(FloatingMenu); - - describe('tooltip container', () => { - it("sets the tooltip's position", () => { - expect(floatingMenu.prop('menuDirection')).toEqual('bottom'); - }); - it("sets the tooltip's offset", () => { - expect(floatingMenu.prop('menuOffset')).toEqual({ left: 10, top: 15 }); - }); - it('does not render info icon', () => { - const icon = label.find(Information); - expect(icon.exists()).toBe(false); - }); - it('sets the tooltip class', () => { - expect( - floatingMenu - .find('[data-floating-menu-direction]') - .first() - .prop('className') - ).toBe( - `${prefix}--tooltip ${prefix}--tooltip--shown ${prefix}--tooltip--bottom ${prefix}--tooltip--align-center tooltip--class` - ); - }); - it('sets the trigger class', () => { - expect(label.prop('className')).toBe( - `${prefix}--tooltip__label tooltip--trigger-class` - ); - }); - }); - }); - - describe('Renders as expected when an Icon component wrapped with forwardRef is provided', () => { - const wrapper = mount(); - - it('does render Icon', () => { - const icon = wrapper.find(Add); - expect(icon.exists()).toBe(true); - }); - }); - - describe('Renders as expected when custom icon component with forwardRef is provided', () => { - const wrapper = mount( - ( - - ))} - /> - ); - - it('does render provided custom icon component instance', () => { - const icon = wrapper.find(CustomIcon); - expect(icon.exists()).toBe(true); - }); - }); - - describe('Renders as expected when custom icon component with inner forwardRef is provided', () => { - const wrapper = mount(); - - it('does render provided custom icon component instance', () => { - const icon = wrapper.find(OverflowMenuVertical); - expect(icon.exists()).toBe(true); - }); - }); - - describe('events', () => { - it('A different key press does not change state', () => { - const wrapper = mount(); - const icon = wrapper.find(Information); - icon.simulate('keyDown', { which: 'x' }); - // Enzyme doesn't seem to allow state() in a forwardRef-wrapped class component - expect(wrapper.find('Tooltip').instance().state.open).toBeFalsy(); - }); - - it('A different key press does not change state when custom icon is set', () => { - const wrapper = mount( - ( -
- ))} - triggerText="Tooltip" - /> - ); - const icon = wrapper.find('.custom-icon'); - icon.simulate('keyDown', { which: 'x' }); - // Enzyme doesn't seem to allow state() in a forwardRef-wrapped class component - expect(wrapper.find('Tooltip').instance().state.open).toBeFalsy(); - }); - - it('should be in a closed state after handleOutsideClick() is invoked', () => { - const rootWrapper = mount(); - // Enzyme doesn't seem to allow state() in a forwardRef-wrapped class component - expect(rootWrapper.find('Tooltip').instance().state.open).toBeFalsy(); - // Enzyme doesn't seem to allow setState() in a forwardRef-wrapped class component - rootWrapper.find('Tooltip').instance().setState({ open: true }); - rootWrapper.update(); - rootWrapper.find('Tooltip').instance().handleClickOutside(); - // Enzyme doesn't seem to allow state() in a forwardRef-wrapped class component - expect(rootWrapper.find('Tooltip').instance().state.open).toEqual(false); - }); - - it('prop.open change should update open state', () => { - const rootWrapper = mount(); - // Enzyme doesn't seem to allow state() in a forwardRef-wrapped class component - expect(rootWrapper.find('Tooltip').instance().state.open).toEqual(false); - rootWrapper.setProps({ - open: true, - triggerText: 'Tooltip', - }); - // Enzyme doesn't seem to allow state() in a forwardRef-wrapped class component - expect(rootWrapper.find('Tooltip').instance().state.open).toEqual(true); - }); - - it('should avoid change the open state upon setting props, unless there the value actually changes', () => { - const rootWrapper = mount(); - rootWrapper.setProps({ open: true }); - // Enzyme doesn't seem to allow setState() in a forwardRef-wrapped class component - rootWrapper.find('Tooltip').instance().setState({ open: false }); - rootWrapper.update(); - rootWrapper.setProps({ open: true }); - // Enzyme doesn't seem to allow state() in a forwardRef-wrapped class component - expect(rootWrapper.find('Tooltip').instance().state.open).toEqual(false); - }); - - it('escape key keyDown should not bubble outside the tooltip', () => { - const onKeyDown = jest.fn(); - render( - <> - {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} -
- -
- - ); - - userEvent.click(screen.getAllByRole('button')[0]); - userEvent.keyboard('{esc}'); - - expect(onKeyDown).not.toHaveBeenCalled(); - }); - - it('should close the tooltip when escape key is pressed', () => { - render( - -

tooltip body

-
- ); - - expect(screen.queryByText('trigger text')).toBeInTheDocument(); - expect(screen.queryByText('tooltip body')).not.toBeInTheDocument(); - - const triggerButton = screen.getByRole('button'); - userEvent.click(triggerButton); - // I am unsure why, but the trigger must be clicked a second time for the tooltip body to appear - userEvent.click(triggerButton); - - expect(screen.queryByText('tooltip body')).toBeInTheDocument(); - - userEvent.keyboard('{esc}'); - - expect(screen.queryByText('tooltip body')).not.toBeInTheDocument(); - }); - - it('should not call onChange on focus of an interactive element in body when controlled', () => { - const onChange = jest.fn(); - function ControlledWithStateOnChange() { - const [tipOpen, setTipOpen] = useState(false); - const handleChange = (ev, { open }) => { - onChange(ev, { open }); - setTipOpen(open); - }; - - return ( - -

- This is some tooltip text. This box shows the maximum amount of - text that should be displayed inside. If more room is needed, use - a modal instead. -

-
- Learn more - -
-
- ); - } - - render(); - - expect( - screen.queryByText('ControlledWithStateOnChange label') - ).toBeInTheDocument(); - expect( - screen.queryByRole('button', { name: 'Create' }) - ).not.toBeInTheDocument(); - - // The trigger to open the tooltip - userEvent.click(screen.getByRole('button')); - - expect(onChange).toHaveBeenCalledTimes(2); - expect(onChange).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - type: 'focus', - }), - expect.objectContaining({ - open: false, - }) - ); - expect(onChange).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - type: 'click', - }), - expect.objectContaining({ - open: true, - }) - ); - - expect( - screen.queryByRole('button', { name: 'Create' }) - ).toBeInTheDocument(); - userEvent.click(screen.getByRole('button', { name: 'Create' })); - expect( - screen.queryByRole('button', { name: 'Create' }) - ).not.toBeInTheDocument(); - expect(onChange).toHaveBeenCalledTimes(2); - expect(onChange).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - type: 'focus', - target: expect.objectContaining({ - className: `${prefix}--tooltip__trigger`, - }), - }), - expect.objectContaining({ - open: false, - }) - ); - expect(onChange).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - type: 'click', - target: expect.objectContaining({ - className: `${prefix}--tooltip__trigger`, - }), - }), - expect.objectContaining({ - open: true, - }) - ); - }); - }); -}); diff --git a/packages/react/src/components/Tooltip/Tooltip.js b/packages/react/src/components/Tooltip/Tooltip.js index 15a3bd17ab22..304920b9392e 100644 --- a/packages/react/src/components/Tooltip/Tooltip.js +++ b/packages/react/src/components/Tooltip/Tooltip.js @@ -5,744 +5,171 @@ * LICENSE file in the root directory of this source tree. */ -import React, { Component } from 'react'; +import cx from 'classnames'; import PropTypes from 'prop-types'; -import { isForwardRef } from 'react-is'; -import debounce from 'lodash.debounce'; -import classNames from 'classnames'; -import { Information } from '@carbon/icons-react'; -import FloatingMenu, { - DIRECTION_LEFT, - DIRECTION_TOP, - DIRECTION_RIGHT, - DIRECTION_BOTTOM, -} from '../../internal/FloatingMenu'; -import ClickListener from '../../internal/ClickListener'; -import mergeRefs from '../../tools/mergeRefs'; -import { keys, matches as keyDownMatch } from '../../internal/keyboard'; -import isRequiredOneOf from '../../prop-types/isRequiredOneOf'; -import requiredIfValueExists from '../../prop-types/requiredIfValueExists'; -import { useControlledStateWithValue } from '../../internal/FeatureFlags'; -import { PrefixContext } from '../../internal/usePrefix'; +import React, { useRef, useEffect } from 'react'; +import { Popover, PopoverContent } from '../Popover'; +import { keys, match } from '../../internal/keyboard'; +import { useDelayedState } from '../../internal/useDelayedState'; +import { useId } from '../../internal/useId'; +import { + useNoInteractiveChildren, + getInteractiveContent, +} from '../../internal/useNoInteractiveChildren'; +import { usePrefix } from '../../internal/usePrefix'; + +function Tooltip({ + align = 'top', + className: customClassName, + children, + label, + description, + enterDelayMs = 100, + leaveDelayMs = 300, + defaultOpen = false, + ...rest +}) { + const containerRef = useRef(null); + const tooltipRef = useRef(null); + const [open, setOpen] = useDelayedState(defaultOpen); + const id = useId('tooltip'); + const prefix = usePrefix(); + const child = React.Children.only(children); + + const triggerProps = { + onFocus: () => setOpen(true), + onBlur: () => setOpen(false), + // This should be placed on the trigger in case the element is disabled + onMouseEnter, + }; -/** - * @param {Element} menuBody The menu body with the menu arrow. - * @param {string} menuDirection Where the floating menu menu should be placed relative to the trigger button. - * @returns {FloatingMenu~offset} The adjustment of the floating menu position, upon the position of the menu arrow. - * @private - */ -const getMenuOffset = (menuBody, menuDirection) => { - const arrowStyle = menuBody.ownerDocument.defaultView.getComputedStyle( - menuBody, - ':before' - ); - const arrowPositionProp = { - [DIRECTION_LEFT]: 'right', - [DIRECTION_TOP]: 'bottom', - [DIRECTION_RIGHT]: 'left', - [DIRECTION_BOTTOM]: 'top', - }[menuDirection]; - const menuPositionAdjustmentProp = { - [DIRECTION_LEFT]: 'left', - [DIRECTION_TOP]: 'top', - [DIRECTION_RIGHT]: 'left', - [DIRECTION_BOTTOM]: 'top', - }[menuDirection]; - const values = [arrowPositionProp, 'border-bottom-width'].reduce( - (o, name) => ({ - ...o, - [name]: Number( - (/^([\d-]+)px$/.exec(arrowStyle.getPropertyValue(name)) || [])[1] - ), - }), - {} - ); - values[arrowPositionProp] = values[arrowPositionProp] || -6; // IE, etc. - if (Object.keys(values).every((name) => !isNaN(values[name]))) { - const { - [arrowPositionProp]: arrowPosition, - 'border-bottom-width': borderBottomWidth, - } = values; - return { - left: 0, - top: 0, - [menuPositionAdjustmentProp]: - Math.sqrt(Math.pow(borderBottomWidth, 2) * 2) - arrowPosition, - }; + if (label) { + triggerProps['aria-labelledby'] = id; + } else { + triggerProps['aria-describedby'] = id; } -}; -class Tooltip extends Component { - constructor(props) { - super(props); - this.isControlled = props.open !== undefined; - if (useControlledStateWithValue && this.isControlled) { - // Skips the logic of setting initial state if this component is controlled - return; + function onKeyDown(event) { + if (open && match(event, keys.Escape)) { + event.stopPropagation(); + setOpen(false); } - const open = useControlledStateWithValue ? props.defaultOpen : props.open; - this.state = { - open, - storedDirection: props.direction, - storedAlign: props.align, - }; } - static propTypes = { - /** - * Specify the alignment (to the trigger button) of the tooltip. - * Can be one of: start, center, or end. - */ - align: PropTypes.oneOf(['start', 'center', 'end']), - - /** - * Whether or not to re-orientate the tooltip if it goes outside, - * of the bounds of the parent. - */ - autoOrientation: PropTypes.bool, - /** - * Contents to put into the tooltip. - */ - children: PropTypes.node, - - /** - * The CSS class names of the tooltip. - */ - className: PropTypes.string, - - /** - * Optional starting value for uncontrolled state - */ - defaultOpen: PropTypes.bool, - - /** - * Where to put the tooltip, relative to the trigger UI. - */ - direction: PropTypes.oneOf(['bottom', 'top', 'left', 'right']), - - /** - * Enable or disable focus trap behavior - */ - focusTrap: PropTypes.bool, - - /** - * The name of the default tooltip icon. - */ - iconName: PropTypes.string, - - /** - * The adjustment of the tooltip position. - */ - menuOffset: PropTypes.oneOfType([ - PropTypes.shape({ - top: PropTypes.number, - left: PropTypes.number, - }), - PropTypes.func, - ]), - - /** - * * the signature of the event handler will be: - * * `onChange(event, { open })` where: - * * `event` is the (React) raw event - * * `open` is the new value - */ - onChange: !useControlledStateWithValue - ? PropTypes.func - : requiredIfValueExists(PropTypes.func), - - /** - * Open/closed state. - */ - open: PropTypes.bool, - - /** - * The callback function to optionally render the icon element. - * It should be a component with React.forwardRef(). - */ - renderIcon: function (props, propName, componentName) { - if (props[propName] == undefined) { - return; - } - const RefForwardingComponent = props[propName]; - if (!isForwardRef()) { - return new Error(`Invalid value of prop '${propName}' supplied to '${componentName}', - it should be created/wrapped with React.forwardRef() to have a ref and access the proper - DOM node of the element to calculate its position in the viewport.`); - } - }, - - /** - * Specify a CSS selector that matches the DOM element that should - * be focused when the Tooltip opens - */ - selectorPrimaryFocus: PropTypes.string, - - /** - * `true` to show the default tooltip icon. - */ - showIcon: PropTypes.bool, + function onMouseEnter() { + setOpen(true, enterDelayMs); + } - /** - * Optional prop to specify the tabIndex of the Tooltip - */ - tabIndex: PropTypes.number, + function onMouseLeave() { + setOpen(false, leaveDelayMs); + } - /** - * The ID of the tooltip body content. - */ - tooltipBodyId: PropTypes.string, + useNoInteractiveChildren( + tooltipRef, + 'The Tooltip component must have no interactive content rendered by the' + + '`label` or `description` prop' + ); - /** - * The ID of the tooltip content. - */ - tooltipId: PropTypes.string, + useEffect(() => { + const interactiveContent = getInteractiveContent(containerRef.current); + if (!interactiveContent) { + setOpen(false); + } + }); + + return ( + + {React.cloneElement(child, triggerProps)} + + + ); +} - /** - * The CSS class names of the trigger UI. - */ - triggerClassName: PropTypes.string, +Tooltip.propTypes = { + /** + * Specify how the trigger should align with the tooltip + */ + align: PropTypes.oneOf([ + 'top', + 'top-left', + 'top-right', - /** - * The ID of the trigger button. - */ - triggerId: PropTypes.string, + 'bottom', + 'bottom-left', + 'bottom-right', - ...isRequiredOneOf({ - /** - * The content to put into the trigger UI, except the (default) tooltip icon. - */ - triggerText: PropTypes.node, - /** - * The description of the default tooltip icon, to be put in its SVG 'aria-label' and 'alt' . - */ - iconDescription: PropTypes.string, - }), - }; + 'left', + 'left-bottom', + 'left-top', - static defaultProps = { - align: 'center', - direction: DIRECTION_BOTTOM, - focusTrap: true, - renderIcon: Information, - showIcon: true, - triggerText: null, - menuOffset: getMenuOffset, - selectorPrimaryFocus: '[data-tooltip-primary-focus]', - }; + 'right', + 'right-bottom', + 'right-top', + ]), /** - * The element of the tooltip body. - * @type {Element} - * @private + * Pass in the child to which the tooltip will be applied */ - _tooltipEl = null; + children: PropTypes.node, /** - * The element ref of the tooltip's trigger button. - * @type {React.RefObject} - * @private + * Specify an optional className to be applied to the container node */ - _triggerRef = React.createRef(); + className: PropTypes.string, /** - * Unique tooltip ID that is user-provided or auto-generated - * Referenced in aria-labelledby attribute - * @type {string} - * @private + * Specify whether the tooltip should be open when it first renders */ - _tooltipId = - this.props.id || - this.props.tooltipId || - `__carbon-tooltip_${Math.random().toString(36).substr(2)}`; + defaultOpen: PropTypes.bool, /** - * Internal flag for tracking whether or not focusing on the tooltip trigger - * should automatically display the tooltip body + * Provide the description to be rendered inside of the Tooltip. The + * description will use `aria-describedby` and will describe the child node + * in addition to the text rendered inside of the child. This means that if you + * have text in the child node, that it will be announced alongside the + * description to the screen reader. + * + * Note: if label and description are both provided, label will be used and + * description will not be used */ - _tooltipDismissed = false; - - componentDidMount() { - if (!this._debouncedHandleFocus) { - this._debouncedHandleFocus = debounce(this._handleFocus, 200); - } - } - - componentDidUpdate(prevProps, prevState) { - if (prevProps.direction != this.props.direction) { - this.setState({ storedDirection: this.props.direction }); - } - if (prevProps.align != this.props.align) { - this.setState({ storedAlign: this.props.align }); - } - if (prevState.open && !this.state.open) { - // Reset orientation when closing - this.setState({ - storedDirection: this.props.direction, - storedAlign: this.props.align, - }); - } - } - - updateOrientation = (params) => { - if (this.props.autoOrientation) { - const newOrientation = this.getBestDirection(params); - const { direction, align } = newOrientation; - - if (direction !== this.state.storedDirection) { - this.setState({ open: false }, () => { - this.setState({ open: true, storedDirection: direction }); - }); - } - - if (align === 'original') { - this.setState({ storedAlign: this.props.align }); - } else { - this.setState({ storedAlign: align }); - } - } - }; - - getBestDirection = ({ - menuSize, - refPosition = {}, - offset = {}, - direction = DIRECTION_BOTTOM, - scrollX: pageXOffset = 0, - scrollY: pageYOffset = 0, - container, - }) => { - const { - left: refLeft = 0, - top: refTop = 0, - right: refRight = 0, - bottom: refBottom = 0, - } = refPosition; - const scrollX = container.position !== 'static' ? 0 : pageXOffset; - const scrollY = container.position !== 'static' ? 0 : pageYOffset; - const relativeDiff = { - top: container.position !== 'static' ? container.rect.top : 0, - left: container.position !== 'static' ? container.rect.left : 0, - }; - const { width, height } = menuSize; - const { top = 0, left = 0 } = offset; - const refCenterHorizontal = (refLeft + refRight) / 2; - const refCenterVertical = (refTop + refBottom) / 2; - - // Calculate whether a new direction is needed to stay in parent. - // It will switch the current direction to the opposite i.e. - // If the direction="top" and the top boundary is overflowed - // then it switches the direction to "bottom". - const newDirection = () => { - switch (direction) { - case DIRECTION_LEFT: - return refLeft - width + scrollX - left - relativeDiff.left < 0 - ? DIRECTION_RIGHT - : direction; - case DIRECTION_TOP: - return refTop - height + scrollY - top - relativeDiff.top < 0 - ? DIRECTION_BOTTOM - : direction; - case DIRECTION_RIGHT: - return refRight + scrollX + left - relativeDiff.left + width > - container.rect.width - ? DIRECTION_LEFT - : direction; - case DIRECTION_BOTTOM: - return refBottom + scrollY + top - relativeDiff.top + height > - container.rect.height - ? DIRECTION_TOP - : direction; - default: - // If there is a new direction then ignore the logic above - return direction; - } - }; - - // Calculate whether a new alignment is needed to stay in parent - // If the direction is left or right this involves checking the - // overflow in the vertical direction. If the direction is top or - // bottom, this involves checking overflow in the horizontal direction. - // "original" is used to signify no change. - const newAlignment = () => { - switch (direction) { - case DIRECTION_LEFT: - case DIRECTION_RIGHT: - if ( - refCenterVertical - - height / 2 + - scrollY + - top - - 9 - - relativeDiff.top < - 0 - ) { - // If goes above the top boundary - return 'start'; - } else if ( - refCenterVertical - - height / 2 + - scrollY + - top - - 9 - - relativeDiff.top + - height > - container.rect.height - ) { - // If goes below the bottom boundary - return 'end'; - } else { - // No need to change alignment - return 'original'; - } - case DIRECTION_TOP: - case DIRECTION_BOTTOM: - if ( - refCenterHorizontal - - width / 2 + - scrollX + - left - - relativeDiff.left < - 0 - ) { - // If goes below the left boundary - return 'start'; - } else if ( - refCenterHorizontal - - width / 2 + - scrollX + - left - - relativeDiff.left + - width > - container.rect.width - ) { - // If it goes over the right boundary - return 'end'; - } else { - // No need to change alignment - return 'original'; - } - default: - // No need to change alignment - return 'original'; - } - }; - - return { - direction: newDirection(), - align: newAlignment(), - }; - }; - - componentWillUnmount() { - if (this._debouncedHandleFocus) { - this._debouncedHandleFocus.cancel(); - this._debouncedHandleFocus = null; - } - } - - static getDerivedStateFromProps({ open }, state) { - /** - * so that tooltip can be controlled programmatically through this `open` prop - */ - const { prevOpen } = state; - return prevOpen === open - ? null - : { - open, - prevOpen: open, - }; - } - - _handleUserInputOpenClose = (event, { open }) => { - if (this.isControlled && this.props.onChange) { - // Callback to the parent to let them decide what to do - this.props.onChange(event, { open }); - return; - } - // capture tooltip body element before it is removed from the DOM - const tooltipBody = this._tooltipEl; - this.setState({ open }, () => { - if (this.props.onChange) { - this.props.onChange(event, { open }); - } - if (!open && tooltipBody && tooltipBody.id === this._tooltipId) { - this._tooltipDismissed = true; - const currentActiveNode = event?.relatedTarget; - if ( - !currentActiveNode && - document.activeElement === document.body && - event?.type !== 'click' - ) { - this._triggerRef?.current.focus(); - } - } - }); - }; + description: PropTypes.node, /** - * Handles `focus`/`blur` event. - * @param {string} state `over` to show the tooltip, `out` to hide the tooltip. - * @param {Element} [evt] For handing `mouseout` event, indicates where the mouse pointer is gone. + * Specify the duration in milliseconds to delay before displaying the tooltip */ - _handleFocus = (state, evt) => { - const { currentTarget, relatedTarget } = evt; - if (currentTarget !== relatedTarget) { - this._tooltipDismissed = false; - } - if (state === 'over' && !this.isControlled) { - if (!this._tooltipDismissed) { - this._handleUserInputOpenClose(evt, { open: true }); - } - this._tooltipDismissed = false; - } else if (state !== 'out') { - // Note: SVGElement in IE11 does not have `.contains()` - const { current: triggerEl } = this._triggerRef; - const shouldPreventClose = - relatedTarget && - ((triggerEl && triggerEl?.contains(relatedTarget)) || - (this._tooltipEl && this._tooltipEl.contains(relatedTarget))); - if (!shouldPreventClose) { - this._handleUserInputOpenClose(evt, { open: false }); - } - } - }; + enterDelayMs: PropTypes.number, /** - * The debounced version of the `focus`/`blur` event handler. - * @type {Function} - * @private + * 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, that it will not be + * announced to the screen reader. + * + * Note: if label and description are both provided, description will not be + * used */ - _debouncedHandleFocus = null; + label: PropTypes.node, /** - * @returns {Element} The DOM element where the floating menu is placed in. + * Specify the duration in milliseconds to delay before hiding the tooltip */ - _getTarget = () => { - const { current: triggerEl } = this._triggerRef; - return ( - (triggerEl && triggerEl.closest('[data-floating-menu-container]')) || - document.body - ); - }; - - handleMouse = (evt) => { - evt.persist(); - const state = { - focus: 'over', - blur: 'out', - click: 'click', - }[evt.type]; - const hadContextMenu = this._hasContextMenu; - if (evt.type === 'click' || evt.type === 'contextmenu') { - this._hasContextMenu = evt.type === 'contextmenu'; - } - - if (this._hasContextMenu) { - this._handleUserInputOpenClose(evt, { open: false }); - return; - } - - if (state === 'click') { - evt.preventDefault(); - const shouldOpen = this.isControlled - ? !this.props.open - : !this.state.open; - this._handleUserInputOpenClose(evt, { open: shouldOpen }); - } else if (state && (state !== 'out' || !hadContextMenu)) { - this?._debouncedHandleFocus(state, evt); - } - }; - - handleClickOutside = (evt) => { - const shouldPreventClose = - evt && - evt.target && - this._tooltipEl && - this._tooltipEl.contains(evt.target); - if (!shouldPreventClose && this.state.open) { - this._handleUserInputOpenClose(evt, { open: false }); - } - }; - - handleKeyPress = (event) => { - if (keyDownMatch(event, [keys.Escape, keys.Tab])) { - event.stopPropagation(); - this._handleUserInputOpenClose(event, { open: false }); - } - - if (keyDownMatch(event, [keys.Enter, keys.Space])) { - event.stopPropagation(); - event.preventDefault(); - const shouldOpen = this.isControlled - ? !this.props.open - : !this.state.open; - this._handleUserInputOpenClose(event, { open: shouldOpen }); - } - }; - - handleEscKeyPress = (event) => { - const { open } = this.isControlled ? this.props : this.state; - if (open && keyDownMatch(event, [keys.Escape])) { - event.stopPropagation(); - return this._handleUserInputOpenClose(event, { open: false }); - } - }; - - render() { - const { - triggerId = (this.triggerId = - this.triggerId || - `__carbon-tooltip-trigger_${Math.random().toString(36).substr(2)}`), - tooltipBodyId, - children, - className, - triggerClassName, - focusTrap, - triggerText, - showIcon, - iconName, - iconDescription, - renderIcon: IconCustomElement, - menuOffset, - tabIndex = 0, - innerRef: ref, - selectorPrimaryFocus, // eslint-disable-line - tooltipId, //eslint-disable-line - autoOrientation, //eslint-disable-line - ...other - } = this.props; - - const { open } = this.isControlled ? this.props : this.state; - const { storedDirection, storedAlign } = this.state; - - return ( - - {(prefix) => { - const tooltipClasses = classNames( - `${prefix}--tooltip`, - { - [`${prefix}--tooltip--shown`]: open, - [`${prefix}--tooltip--${storedDirection}`]: storedDirection, - [`${prefix}--tooltip--align-${storedAlign}`]: storedAlign, - }, - className - ); - - const triggerClasses = classNames( - `${prefix}--tooltip__label`, - triggerClassName - ); - - const refProp = mergeRefs(this._triggerRef, ref); - const iconProperties = { - name: iconName, - role: null, - description: null, - }; - - const properties = { - role: 'button', - tabIndex: tabIndex, - onClick: this.handleMouse, - onContextMenu: this.handleMouse, - onKeyDown: this.handleKeyPress, - onMouseOver: this.handleMouse, - onMouseOut: this.handleMouse, - onFocus: this.handleMouse, - onBlur: this.handleMouse, - 'aria-controls': !open ? undefined : this._tooltipId, - 'aria-expanded': open, - 'aria-describedby': open ? this._tooltipId : null, - // if the user provides property `triggerText`, - // then the button should use aria-labelledby to point to its id, - // if the user doesn't provide property `triggerText`, - // then an aria-label will be provided via the `iconDescription` property. - ...(triggerText - ? { - 'aria-labelledby': triggerId, - } - : { - 'aria-label': iconDescription, - }), - }; - - return ( - <> - - {showIcon ? ( -
- {triggerText} -
- -
-
- ) : ( -
- {triggerText} -
- )} -
- {open && ( - { - this._tooltipEl = node; - }} - updateOrientation={this.updateOrientation}> - {/* This rule is disabled because the onKeyDown event handler is only - * being used to capture and prevent the event from bubbling: */} - {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} -
- -
- {children} -
-
-
- )} - - ); - }} -
- ); - } -} + leaveDelayMs: PropTypes.number, +}; export { Tooltip }; -export default (() => { - const forwardRef = (props, ref) => ; - forwardRef.displayName = 'Tooltip'; - return React.forwardRef(forwardRef); -})(); diff --git a/packages/react/src/components/Tooltip/Tooltip.mdx b/packages/react/src/components/Tooltip/Tooltip.mdx index 0749eae36ce4..6626ac612b82 100644 --- a/packages/react/src/components/Tooltip/Tooltip.mdx +++ b/packages/react/src/components/Tooltip/Tooltip.mdx @@ -1,4 +1,4 @@ -import { Props } from '@storybook/addon-docs'; +import { Story, ArgsTable, Canvas } from '@storybook/addon-docs'; # Tooltip @@ -8,25 +8,113 @@ import { Props } from '@storybook/addon-docs';  |  [Accessibility](https://www.carbondesignsystem.com/components/tooltip/accessibility) + + + ## Table of Contents +- [Overview](#overview) + - [Toggletips vs Tooltips](#toggletips-vs-tooltips) + - [Customizing the content of a tooltip](#customizing-the-content-of-a-tooltip) + - [Tooltip alignment](#tooltip-alignment) + - [Customizing the duration of a tooltip](#customizing-the-duration-of-a-tooltip) +- [Component API](#component-api) +- [Feedback](#feedback) + + + ## Overview -### `data-floating-menu-container` +You can use the `Tooltip` component as a wrapper to display a popup for an +interactive element that you provide. If you are trying to place interactive +content inside of the `Tooltip` itself, consider using `Toggletip` instead. You +can provide the interactive element as a child to `Tooltip`, for example: -`Tooltip` uses React Portals to render the tooltip into the DOM. To determine -where in the DOM the menu will be placed, it looks for a parent element with the -`data-floating-menu-container` attribute. If no parent with this attribute is -found, the menu will be placed on `document.body`. +```jsx + + + +``` -## Component API +`Tooltip` accepts a single child for its `children` prop. It is important that +the component you provide renders an interactive element. In addition, you can +specify the contents of the popup through the `label` or `description` prop. + + + + + +### Toggletips vs Tooltips + +Toggletips and tooltips are similar visually and both contain a popover and +interactive trigger element. The two components differ in the way they are +invoked and dismissed and if the user must interact with the contents. A tooltip +is exposed on hover or focus when you need to expose brief, supplemental +information that is not interactive. A toggletip is used on click or enter when +you need to expose interactive elements, such as button, that a user needs to +interact with. + +### Customizing the content of a tooltip + +The `Tooltip` component supports tooltip customization through the `label` and +`description` props. Under the hood, `label` will map to `aria-labelledby` and +`description` will map to `aria-describedby`. + +It's important to consider the type of content that you provide through the +tooltip in order to know whether to use `label` or `description`. If the +information shown is the only content that describes your component, you should +use a `label`. If the information is secondary or adds additional information, +`description` would be a good pick. + +While the `label` and `description` props accept any arbitrary React element, +it's important that the value you pass to either prop has no interactive +content. This will cause a violation in the component as the semantics of this +content will not be accessible to users of screen reader software. + +### Tooltip alignment -Please note that in addition to the props below, `Tooltip` also has two -additional props: `triggerText` and `iconDescription`. If the `triggerText` prop -is _not_ provided, the `iconDescription` prop is required to populate the -`aria-label` property for a11y reasons. +The `align` prop allows you to specify where your content should be placed +relative to the tooltip. For example, if you provide `align="top"` to the +`Tooltip` component then the tooltip will render above your component. +Similarly, if you provide `align="bottom"` then the tooltip will render below +your component. + + + + + +You can also configure the placement of the caret, if it is enabled, by choosing +between `left` and `right` or `bottom` and `top`, depending on where your +tooltip is being rendered. + +If you are using `align="top"`, then you can choose between `align="top-left"` +and `align="top-right"`. These options will place the caret closer to the left +or right edges of the tooltip. + +If you are using `align="left"` or `align="right"`, you can use `top` or +`bottom` to control this alignment. + +### Customizing the duration of a tooltip + +You can customize the delay between when a tooltip is invoked and its contents +are shown. You can also customize the delay between when a tooltip is dismissed +and its contents are hidden. + + + + + +The `enterDelayMs` prop allows you to provide a time, in milliseconds, that the +component will wait before showing the tooltip. The `exitDelayMs` prop allows +you to provide a time, in milliseconds, that the component will wait before +hiding the tooltip. + +By default, these values are set to 100ms for `enterDelayms` and 300ms for +`exitDelayMs`. + +## Component API - + ## Feedback diff --git a/packages/react/src/components/Tooltip/next/Tooltip.stories.js b/packages/react/src/components/Tooltip/Tooltip.stories.js similarity index 100% rename from packages/react/src/components/Tooltip/next/Tooltip.stories.js rename to packages/react/src/components/Tooltip/Tooltip.stories.js diff --git a/packages/react/src/components/Tooltip/next/__tests__/DefinitionTooltip-test.js b/packages/react/src/components/Tooltip/__tests__/DefinitionTooltip-test.js similarity index 97% rename from packages/react/src/components/Tooltip/next/__tests__/DefinitionTooltip-test.js rename to packages/react/src/components/Tooltip/__tests__/DefinitionTooltip-test.js index 474ca48e2876..b93766f75e29 100644 --- a/packages/react/src/components/Tooltip/next/__tests__/DefinitionTooltip-test.js +++ b/packages/react/src/components/Tooltip/__tests__/DefinitionTooltip-test.js @@ -8,7 +8,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { DefinitionTooltip } from '../../next/DefinitionTooltip'; +import { DefinitionTooltip } from '../DefinitionTooltip'; describe('DefintiionTooltip', () => { it('should display onClick a defintion provided via prop', () => { diff --git a/packages/react/src/components/Tooltip/next/__tests__/Tooltip-test.js b/packages/react/src/components/Tooltip/__tests__/Tooltip-test.js similarity index 97% rename from packages/react/src/components/Tooltip/next/__tests__/Tooltip-test.js rename to packages/react/src/components/Tooltip/__tests__/Tooltip-test.js index b0930979448b..cb7963038258 100644 --- a/packages/react/src/components/Tooltip/next/__tests__/Tooltip-test.js +++ b/packages/react/src/components/Tooltip/__tests__/Tooltip-test.js @@ -7,7 +7,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; -import { Tooltip } from '../../next'; +import { Tooltip } from '../'; describe('Tooltip', () => { it('should support a custom className with the `className` prop', () => { diff --git a/packages/react/src/components/Tooltip/index.js b/packages/react/src/components/Tooltip/index.js index 1738e93f2eaf..e4fd5f092da9 100644 --- a/packages/react/src/components/Tooltip/index.js +++ b/packages/react/src/components/Tooltip/index.js @@ -5,4 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -export default from './Tooltip'; +import { DefinitionTooltip } from './DefinitionTooltip'; +import { Tooltip } from './Tooltip'; + +export { DefinitionTooltip, Tooltip }; diff --git a/packages/react/src/components/Tooltip/next/Tooltip.js b/packages/react/src/components/Tooltip/next/Tooltip.js deleted file mode 100644 index 6d5956a142ff..000000000000 --- a/packages/react/src/components/Tooltip/next/Tooltip.js +++ /dev/null @@ -1,175 +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 cx from 'classnames'; -import PropTypes from 'prop-types'; -import React, { useRef, useEffect } from 'react'; -import { Popover, PopoverContent } from '../../Popover'; -import { keys, match } from '../../../internal/keyboard'; -import { useDelayedState } from '../../../internal/useDelayedState'; -import { useId } from '../../../internal/useId'; -import { - useNoInteractiveChildren, - getInteractiveContent, -} from '../../../internal/useNoInteractiveChildren'; -import { usePrefix } from '../../../internal/usePrefix'; - -function Tooltip({ - align = 'top', - className: customClassName, - children, - label, - description, - enterDelayMs = 100, - leaveDelayMs = 300, - defaultOpen = false, - ...rest -}) { - const containerRef = useRef(null); - const tooltipRef = useRef(null); - const [open, setOpen] = useDelayedState(defaultOpen); - const id = useId('tooltip'); - const prefix = usePrefix(); - const child = React.Children.only(children); - - const triggerProps = { - onFocus: () => setOpen(true), - onBlur: () => setOpen(false), - // This should be placed on the trigger in case the element is disabled - onMouseEnter, - }; - - if (label) { - triggerProps['aria-labelledby'] = id; - } else { - triggerProps['aria-describedby'] = id; - } - - function onKeyDown(event) { - if (open && match(event, keys.Escape)) { - event.stopPropagation(); - setOpen(false); - } - } - - function onMouseEnter() { - setOpen(true, enterDelayMs); - } - - function onMouseLeave() { - setOpen(false, leaveDelayMs); - } - - useNoInteractiveChildren( - tooltipRef, - 'The Tooltip component must have no interactive content rendered by the' + - '`label` or `description` prop' - ); - - useEffect(() => { - const interactiveContent = getInteractiveContent(containerRef.current); - if (!interactiveContent) { - setOpen(false); - } - }); - - return ( - - {React.cloneElement(child, triggerProps)} - - - ); -} - -Tooltip.propTypes = { - /** - * Specify how the trigger should align with the tooltip - */ - align: PropTypes.oneOf([ - 'top', - 'top-left', - 'top-right', - - 'bottom', - 'bottom-left', - 'bottom-right', - - 'left', - 'left-bottom', - 'left-top', - - 'right', - 'right-bottom', - 'right-top', - ]), - - /** - * Pass in the child to which the tooltip will be applied - */ - children: PropTypes.node, - - /** - * Specify an optional className to be applied to the container node - */ - className: PropTypes.string, - - /** - * Specify whether the tooltip should be open when it first renders - */ - defaultOpen: PropTypes.bool, - - /** - * Provide the description to be rendered inside of the Tooltip. The - * description will use `aria-describedby` and will describe the child node - * in addition to the text rendered inside of the child. This means that if you - * have text in the child node, that it will be announced alongside the - * description to the screen reader. - * - * Note: if label and description are both provided, label will be used and - * description will not be used - */ - description: PropTypes.node, - - /** - * Specify the duration in milliseconds to delay before displaying the tooltip - */ - 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, that it will not be - * announced to the screen reader. - * - * Note: if label and description are both provided, description will not be - * used - */ - label: PropTypes.node, - - /** - * Specify the duration in milliseconds to delay before hiding the tooltip - */ - leaveDelayMs: PropTypes.number, -}; - -export { Tooltip }; diff --git a/packages/react/src/components/Tooltip/next/Tooltip.mdx b/packages/react/src/components/Tooltip/next/Tooltip.mdx deleted file mode 100644 index 2ede2af6519d..000000000000 --- a/packages/react/src/components/Tooltip/next/Tooltip.mdx +++ /dev/null @@ -1,123 +0,0 @@ -import { Story, ArgsTable, Canvas } from '@storybook/addon-docs'; - -# Tooltip - -[Source code](https://github.com/carbon-design-system/carbon/tree/main/packages/react/src/components/Tooltip/next) - |  -[Usage guidelines](https://www.carbondesignsystem.com/components/tooltip/usage) - |  -[Accessibility](https://www.carbondesignsystem.com/components/tooltip/accessibility) - - - - -## Table of Contents - -- [Overview](#overview) - - [Toggletips vs Tooltips](#toggletips-vs-tooltips) - - [Customizing the content of a tooltip](#customizing-the-content-of-a-tooltip) - - [Tooltip alignment](#tooltip-alignment) - - [Customizing the duration of a tooltip](#customizing-the-duration-of-a-tooltip) -- [Component API](#component-api) -- [Feedback](#feedback) - - - -## Overview - -You can use the `Tooltip` component as a wrapper to display a popup for an -interactive element that you provide. If you are trying to place interactive -content inside of the `Tooltip` itself, consider using `Toggletip` instead. You -can provide the interactive element as a child to `Tooltip`, for example: - -```jsx - - - -``` - -`Tooltip` accepts a single child for its `children` prop. It is important that -the component you provide renders an interactive element. In addition, you can -specify the contents of the popup through the `label` or `description` prop. - - - - - -### Toggletips vs Tooltips - -Toggletips and tooltips are similar visually and both contain a popover and -interactive trigger element. The two components differ in the way they are -invoked and dismissed and if the user must interact with the contents. A tooltip -is exposed on hover or focus when you need to expose brief, supplemental -information that is not interactive. A toggletip is used on click or enter when -you need to expose interactive elements, such as button, that a user needs to -interact with. - -### Customizing the content of a tooltip - -The `Tooltip` component supports tooltip customization through the `label` and -`description` props. Under the hood, `label` will map to `aria-labelledby` and -`description` will map to `aria-describedby`. - -It's important to consider the type of content that you provide through the -tooltip in order to know whether to use `label` or `description`. If the -information shown is the only content that describes your component, you should -use a `label`. If the information is secondary or adds additional information, -`description` would be a good pick. - -While the `label` and `description` props accept any arbitrary React element, -it's important that the value you pass to either prop has no interactive -content. This will cause a violation in the component as the semantics of this -content will not be accessible to users of screen reader software. - -### Tooltip alignment - -The `align` prop allows you to specify where your content should be placed -relative to the tooltip. For example, if you provide `align="top"` to the -`Tooltip` component then the tooltip will render above your component. -Similarly, if you provide `align="bottom"` then the tooltip will render below -your component. - - - - - -You can also configure the placement of the caret, if it is enabled, by choosing -between `left` and `right` or `bottom` and `top`, depending on where your -tooltip is being rendered. - -If you are using `align="top"`, then you can choose between `align="top-left"` -and `align="top-right"`. These options will place the caret closer to the left -or right edges of the tooltip. - -If you are using `align="left"` or `align="right"`, you can use `top` or -`bottom` to control this alignment. - -### Customizing the duration of a tooltip - -You can customize the delay between when a tooltip is invoked and its contents -are shown. You can also customize the delay between when a tooltip is dismissed -and its contents are hidden. - - - - - -The `enterDelayMs` prop allows you to provide a time, in milliseconds, that the -component will wait before showing the tooltip. The `exitDelayMs` prop allows -you to provide a time, in milliseconds, that the component will wait before -hiding the tooltip. - -By default, these values are set to 100ms for `enterDelayms` and 300ms for -`exitDelayMs`. - -## Component API - - - -## 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/Tooltip/next/Tooltip.mdx). diff --git a/packages/react/src/components/Tooltip/next/index.js b/packages/react/src/components/Tooltip/next/index.js deleted file mode 100644 index e4fd5f092da9..000000000000 --- a/packages/react/src/components/Tooltip/next/index.js +++ /dev/null @@ -1,11 +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 { DefinitionTooltip } from './DefinitionTooltip'; -import { Tooltip } from './Tooltip'; - -export { DefinitionTooltip, Tooltip }; diff --git a/packages/react/src/components/Tooltip/next/story.scss b/packages/react/src/components/Tooltip/story.scss similarity index 100% rename from packages/react/src/components/Tooltip/next/story.scss rename to packages/react/src/components/Tooltip/story.scss diff --git a/packages/react/src/index.js b/packages/react/src/index.js index 57f33606e848..9d6fab5f50f6 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -264,12 +264,12 @@ export { export { Popover, PopoverContent } from './components/Popover'; export { default as ProgressBar } from './components/ProgressBar'; export { HStack, Stack, VStack } from './components/Stack'; -export { Tooltip } from './components/Tooltip/next'; +export { Tooltip } from './components/Tooltip'; export { Text as unstable_Text, TextDirection as unstable_TextDirection, } from './components/Text'; -export { DefinitionTooltip } from './components/Tooltip/next/DefinitionTooltip'; +export { DefinitionTooltip } from './components/Tooltip/DefinitionTooltip'; export { GlobalTheme, Theme, useTheme } from './components/Theme'; export { usePrefix } from './internal/usePrefix'; export { useIdPrefix } from './internal/useIdPrefix'; diff --git a/packages/styles/scss/components/contained-list/_contained-list.scss b/packages/styles/scss/components/contained-list/_contained-list.scss index 8d8958bf86cf..225dffdc4d1b 100644 --- a/packages/styles/scss/components/contained-list/_contained-list.scss +++ b/packages/styles/scss/components/contained-list/_contained-list.scss @@ -5,6 +5,8 @@ // LICENSE file in the root directory of this source tree. // +@use 'sass:list'; + @use '../../config' as *; @use '../../motion' as *; @use '../../spacing' as *; @@ -31,6 +33,35 @@ width: 100%; } + // Sizes + + $sizes: ( + // size: (height, item-block-padding) + 'sm': (rem(32px), $spacing-03), + 'md': (rem(40px), $spacing-04), + 'lg': (rem(48px), $spacing-05), + 'xl': (rem(64px), $spacing-06) + ); + + @each $size, $definition in $sizes { + $height: list.nth($definition, 1); + $item-block-padding: list.nth($definition, 2); + + .#{$prefix}--contained-list--on-page.#{$prefix}--contained-list--#{$size} + .#{$prefix}--contained-list__header { + height: $height; + } + + .#{$prefix}--contained-list--#{$size} + .#{$prefix}--contained-list-item__content, + .#{$prefix}--contained-list--#{$size} + .#{$prefix}--contained-list-item--clickable + .#{$prefix}--contained-list-item__content { + min-height: $height; + padding: calc(#{$item-block-padding} - #{rem(2px)}) $spacing-05; + } + } + // "On Page" variant .#{$prefix}--contained-list--on-page + .#{$prefix}--contained-list--on-page { @@ -40,7 +71,6 @@ .#{$prefix}--contained-list--on-page .#{$prefix}--contained-list__header { @include type-style('heading-compact-01'); - height: $spacing-09; border-bottom: 1px solid $border-subtle; background-color: $background; color: $text-primary; @@ -92,7 +122,6 @@ .#{$prefix}--contained-list-item__content { @include type-style('body-01'); - padding: calc(#{$spacing-05} - #{rem(2px)}) $spacing-05; color: $text-primary; } diff --git a/packages/styles/scss/components/pagination/_pagination.scss b/packages/styles/scss/components/pagination/_pagination.scss index 43f13430ac50..5f35b2c0023b 100644 --- a/packages/styles/scss/components/pagination/_pagination.scss +++ b/packages/styles/scss/components/pagination/_pagination.scss @@ -134,9 +134,7 @@ border-right: 1px solid $border-subtle; } - .#{$prefix}--pagination - .#{$prefix}--select__page-number - .#{$prefix}--select-input { + .#{$prefix}--pagination__right { border-left: 1px solid $border-subtle; } @@ -166,6 +164,16 @@ margin-left: rem(1px); } + .#{$prefix}--pagination__right + .#{$prefix}--pagination__text.#{$prefix}--pagination__page-text { + margin-right: rem(1px); + margin-left: 1rem; + } + + .#{$prefix}--pagination__right .#{$prefix}--pagination__text:empty { + margin: 0; + } + .#{$prefix}--pagination__left { padding: 0 $spacing-05 0 0;