diff --git a/.changeset/fair-swans-shake.md b/.changeset/fair-swans-shake.md new file mode 100644 index 000000000..e71bd1628 --- /dev/null +++ b/.changeset/fair-swans-shake.md @@ -0,0 +1,11 @@ +--- +'@strapi/design-system': major +--- + +chore!: streamline IconButton API + +- remove `icon` prop +- remove `ariaLabel` prop +- add `withTooltip` prop (default false) + +`children` & `label` are now required props. diff --git a/.changeset/wild-colts-return.md b/.changeset/wild-colts-return.md new file mode 100644 index 000000000..c4a190c99 --- /dev/null +++ b/.changeset/wild-colts-return.md @@ -0,0 +1,7 @@ +--- +'@strapi/design-system': major +--- + +feat!: refactor Tooltip to use radix-ui + +The Tooltip API has changed significently whilst retaining it's functionality, we recommend your review the documentation to understand the changes and how to migrate your code. diff --git a/docs/stories/00-getting started/migration guides/migration-v1-v2.mdx b/docs/stories/00-getting started/migration guides/migration-v1-v2.mdx index 9bd3224f4..4bbdb13b2 100644 --- a/docs/stories/00-getting started/migration guides/migration-v1-v2.mdx +++ b/docs/stories/00-getting started/migration guides/migration-v1-v2.mdx @@ -203,6 +203,7 @@ of our APIs will change, but functionality should still remain the same. We reco for any of these components to understand better how to migrate your code. - `Accordion` +- `Tooltip` ### Removed `ThemeProvider` @@ -213,6 +214,17 @@ for any of these components to understand better how to migrate your code. + import { DesignSystemProvider } from '@strapi/design-system'; ``` +### IconButton API changes + +The `IconButton` API has been streamlined, namely `icon` and `ariaLabel` have been removed. Users should instead use +`label` and `children`, by default we always show a tooltip to preserve old functionality but this will deprecated in a +future release. If you don't want a tooltip you should set `withTooltip` to `false`: + +```diff +- ++ +``` + --- ## Icons diff --git a/docs/stories/04-components/Accordion.stories.tsx b/docs/stories/04-components/Accordion.stories.tsx index 71e4ca75c..493561371 100644 --- a/docs/stories/04-components/Accordion.stories.tsx +++ b/docs/stories/04-components/Accordion.stories.tsx @@ -307,10 +307,10 @@ export const WithActions = { {title} - + - + diff --git a/docs/stories/04-components/Tooltip.mdx b/docs/stories/04-components/Tooltip.mdx new file mode 100644 index 000000000..f7a1a2d44 --- /dev/null +++ b/docs/stories/04-components/Tooltip.mdx @@ -0,0 +1,43 @@ +import { Meta, Canvas, ArgTypes, Controls } from '@storybook/blocks'; +import { Tooltip } from '@strapi/design-system'; + +import * as TooltipStories from './Tooltip.stories'; + + + +# Tooltip + +- [Overview](#overview) +- [Usage](#usage) +- [Props](#props) +- [Positioning](#positioning) + +## Overview + +Tooltips are a specific type of popover that displays information related to an element when said element recieves focus +or the hover. + + + +## Usage + +```js +import { Tooltip } from '@strapi/design-system'; +``` + +They shouldn't be used to display critical or urgent information, but rather to provide additional context or +information. Information should be short & concise. + + + +## Props + + + +## Positioning + +Use a combination of `align` and `side` to dictate the position of a tooltip, the tooltip will still move itself if it's +going to colide with the viewport. The default position is `top` and `center`. + + + diff --git a/docs/stories/04-components/Tooltip.stories.tsx b/docs/stories/04-components/Tooltip.stories.tsx new file mode 100644 index 000000000..9c106c7ec --- /dev/null +++ b/docs/stories/04-components/Tooltip.stories.tsx @@ -0,0 +1,77 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { Tooltip, IconButton } from '@strapi/design-system'; +import { Trash } from '@strapi/icons'; +import { outdent } from 'outdent'; + +const meta: Meta = { + title: 'Components/Tooltip', + component: Tooltip, + args: { + label: 'Delete all items', + }, + render: (args) => { + return ( + + + + + + ); + }, + parameters: { + docs: { + source: { + code: outdent` + + + + `, + }, + }, + /* this will never show the component without interaction, so we never want it snapshot. */ + chromatic: { disableSnapshot: true }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Base = { + name: 'base', +} satisfies Story; + +export const Positioned = { + name: 'positioned', + argTypes: { + align: { + control: 'select', + options: ['start', 'center', 'end'], + }, + side: { + control: 'select', + options: ['left', 'right', 'top', 'bottom'], + }, + }, + args: { + align: 'center', + open: true, + side: 'right', + }, + parameters: { + docs: { + source: { + code: outdent` + + + + `, + }, + }, + chromatic: { disableSnapshot: false }, + }, +} satisfies Story; diff --git a/docs/stories/Card.stories.tsx b/docs/stories/Card.stories.tsx index 82497bd10..b250d334a 100644 --- a/docs/stories/Card.stories.tsx +++ b/docs/stories/Card.stories.tsx @@ -37,7 +37,9 @@ export const Base = { - } /> + + + 05:39 diff --git a/docs/stories/Carousel.stories.tsx b/docs/stories/Carousel.stories.tsx index 8075452f5..a13b986c8 100644 --- a/docs/stories/Carousel.stories.tsx +++ b/docs/stories/Carousel.stories.tsx @@ -37,10 +37,18 @@ export const Base = { hint="Description line" actions={ - console.log('edit')} label="Edit" id="edit" icon={} /> - console.log('Create')} label="Create" icon={} /> - console.log('Delete')} label="Delete" icon={} /> - console.log('Publish')} label="Publish" icon={} /> + console.log('edit')} label="Edit" id="edit"> + + + console.log('Create')} label="Create"> + + + console.log('Delete')} label="Delete"> + + + console.log('Publish')} label="Publish"> + + } style={{ @@ -74,10 +82,18 @@ export const OneSlideOnly = { hint="Description line" actions={ - console.log('edit')} label="Edit" id="edit" icon={} /> - console.log('Create')} label="Create" icon={} /> - console.log('Delete')} label="Delete" icon={} /> - console.log('Publish')} label="Publish" icon={} /> + console.log('edit')} label="Edit" id="edit"> + + + console.log('Create')} label="Create"> + + + console.log('Delete')} label="Delete"> + + + console.log('Publish')} label="Publish"> + + } style={{ @@ -105,10 +121,18 @@ export const BrokenAsset = { hint="Description line" actions={ - console.log('edit')} label="Edit" id="edit" icon={} /> - console.log('Create')} label="Create" icon={} /> - console.log('Delete')} label="Delete" icon={} /> - console.log('Publish')} label="Publish" icon={} /> + console.log('edit')} label="Edit" id="edit"> + + + console.log('Create')} label="Create"> + + + console.log('Delete')} label="Delete"> + + + console.log('Publish')} label="Publish"> + + } style={{ diff --git a/docs/stories/IconButton.stories.tsx b/docs/stories/IconButton.stories.tsx index 55261eeb8..6030c5930 100644 --- a/docs/stories/IconButton.stories.tsx +++ b/docs/stories/IconButton.stories.tsx @@ -23,10 +23,18 @@ export const Base = { {currentAction} - setCurrentAction('edit')} label="Edit" icon={} /> - setCurrentAction('Create')} label="Create" icon={} /> - setCurrentAction('Delete')} label="Delete" icon={} /> - setCurrentAction('Publish')} label="Publish" icon={} /> + setCurrentAction('edit')} label="Edit"> + + + setCurrentAction('Create')} label="Create"> + + + setCurrentAction('Delete')} label="Delete"> + + + setCurrentAction('Publish')} label="Publish"> + + ); @@ -45,10 +53,18 @@ export const Disabled = { {currentAction} - setCurrentAction('edit')} label="Edit" icon={} /> - setCurrentAction('Create')} label="Create" icon={} /> - setCurrentAction('Delete')} label="Delete" icon={} /> - setCurrentAction('Publish')} label="Publish" icon={} /> + setCurrentAction('edit')} label="Edit"> + + + setCurrentAction('Create')} label="Create"> + + + setCurrentAction('Delete')} label="Delete"> + + + setCurrentAction('Publish')} label="Publish"> + + ); @@ -61,7 +77,9 @@ export const WithoutTooltip = { render: () => ( - console.log('edit')} aria-label="Edit" icon={} /> + console.log('edit')} withTooltip={false} label="Edit"> + + ), @@ -74,10 +92,18 @@ export const Group = { - console.log('edit')} label="Edit" icon={} /> - console.log('Create')} label="Create" icon={} /> - console.log('Delete')} label="Delete" icon={} /> - console.log('Publish')} label="Publish" icon={} /> + console.log('edit')} label="Edit"> + + + console.log('Create')} label="Create"> + + + console.log('Delete')} label="Delete"> + + + console.log('Publish')} label="Publish"> + + @@ -89,9 +115,15 @@ export const Group = { export const Sizes = { render: () => ( - } size="S" /> - } size="M" /> - } size="L" /> + + + + + + + + + ), @@ -102,7 +134,9 @@ export const Variants = { render: () => ( {(['tertiary', 'secondary'] as const).map((variant) => ( - } variant={variant} key={variant} label={variant} /> + + + ))} ), @@ -120,16 +154,16 @@ export const Children = { {currentAction} - setCurrentAction('Edit')} aria-label="Edit"> + setCurrentAction('Edit')} withTooltip={false} label="Edit"> - setCurrentAction('Create')} aria-label="Create"> + setCurrentAction('Create')} withTooltip={false} label="Create"> - setCurrentAction('Delete')} aria-label="Delete"> + setCurrentAction('Delete')} withTooltip={false} label="Delete"> - setCurrentAction('Publish')} aria-label="Publish"> + setCurrentAction('Publish')} withTooltip={false} label="Publish"> diff --git a/docs/stories/RawTable.stories.tsx b/docs/stories/RawTable.stories.tsx index 530799c29..b5f6ccfe3 100644 --- a/docs/stories/RawTable.stories.tsx +++ b/docs/stories/RawTable.stories.tsx @@ -206,19 +206,13 @@ export const Aria = { ) : cellIndex === row.length - 1 ? ( - console.log('edit')} - label="Edit" - borderWidth={0} - icon={} - /> + console.log('edit')} label="Edit" borderWidth={0}> + + - console.log('delete')} - label="Delete" - borderWidth={0} - icon={} - /> + console.log('delete')} label="Delete" borderWidth={0}> + + diff --git a/docs/stories/Table.stories.tsx b/docs/stories/Table.stories.tsx index e5e36fe38..1bfa9c1cc 100644 --- a/docs/stories/Table.stories.tsx +++ b/docs/stories/Table.stories.tsx @@ -105,14 +105,13 @@ export const Base = { G - console.log('edit')} label="Edit" borderWidth={0} icon={} /> + console.log('edit')} label="Edit" borderWidth={0}> + + - console.log('delete')} - label="Delete" - borderWidth={0} - icon={} - /> + console.log('delete')} label="Delete" borderWidth={0}> + + @@ -199,14 +198,13 @@ export const BaseWithoutFooter = { - console.log('edit')} label="Edit" borderWidth={0} icon={} /> + console.log('edit')} label="Edit" borderWidth={0}> + + - console.log('delete')} - label="Delete" - borderWidth={0} - icon={} - /> + console.log('delete')} label="Delete" borderWidth={0}> + + @@ -254,7 +252,13 @@ export const WithThActions = { - } borderWidth={0} />}> + + + + } + > ID @@ -297,14 +301,13 @@ export const WithThActions = { - console.log('edit')} label="Edit" borderWidth={0} icon={} /> + console.log('edit')} label="Edit" borderWidth={0}> + + - console.log('delete')} - label="Delete" - borderWidth={0} - icon={} - /> + console.log('delete')} label="Delete" borderWidth={0}> + + diff --git a/docs/stories/Tooltip.mdx b/docs/stories/Tooltip.mdx deleted file mode 100644 index b9006b540..000000000 --- a/docs/stories/Tooltip.mdx +++ /dev/null @@ -1,52 +0,0 @@ -import { Meta, Canvas, ArgTypes } from '@storybook/blocks'; -import { Tooltip } from '@strapi/design-system'; - -import * as TooltipStories from './Tooltip.stories'; - - - -# Tooltip - -Tooltips are floating labels that display additional information upon hover of an element. - -**Best practices** - -- The tooltip should remain as long as the mouse is not moved. -- The tooltip should provide additional useful details. -- The tooltip should not be used to provide critical or urgent information. -- Tooltip shouldn't be longer or wider than 400px. -- Tooltips should be used sparingly. - [View source](https://github.com/strapi/design-system/tree/main/packages/design-system/src/Tooltip) - -## Imports - -```js -import { Tooltip } from '@strapi/design-system'; -``` - -## Usage - -Tooltips are used to show the full version of truncated text or is a way to display further information about an -element. The tooltip will appear on top of adjacent text. - - - -### Tooltip with smaller text or longer description - -Tootips work with longer texts as well. - - - -### Alternative positioning - -You can change the position of the tooltip by changing the `position` parameter. The default position is `top`. - - - -## Props - -The Tooltip component wraps all its children in the -(Box)\[https://design-system-git-main-strapijs.vercel.app/?path=/docs/design-system-technical-components-box--base] -component, so you can pass all Box props to change its style. - - diff --git a/docs/stories/Tooltip.stories.tsx b/docs/stories/Tooltip.stories.tsx deleted file mode 100644 index a8de725c1..000000000 --- a/docs/stories/Tooltip.stories.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; -import { Tooltip, Grid as DSGrid, GridItem, Typography } from '@strapi/design-system'; - -const meta: Meta = { - title: 'Design System/Components/Tooltip', - component: Tooltip, -}; - -export default meta; - -type Story = StoryObj; - -export const Base = { - render: () => ( -
- - An infinite amount of content to place correctly the tooltip. Lorem ipsum dolor sit amet, consectetur adipiscing - elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud - exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in - voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, - sunt in culpa qui officia deserunt mollit anim id est laborum. - - - Show tooltip - - - An infinite amount of content to place correctly the tooltip. Lorem ipsum dolor sit amet, consectetur adipiscing - elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud - exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in - voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, - sunt in culpa qui officia deserunt mollit anim id est laborum. - -
- ), - - name: 'base', -} satisfies Story; - -export const WithTextInside = { - render: () => ( -
- - Show tooltip - -
- ), - - name: 'with text inside', -} satisfies Story; - -export const Grid = { - render: () => ( - - - - - - - - - - - - - - - - - - - - - - - ), - - name: 'grid', -} satisfies Story; diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 94976791f..7afbcc942 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-dismissable-layer": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-tooltip": "1.0.7", "@radix-ui/react-use-controllable-state": "^1.0.1", "@strapi/ui-primitives": "^2.0.0-beta.3", "@uiw/react-codemirror": "^4.21.25", diff --git a/packages/design-system/src/components/CarouselInput/CarouselImage.tsx b/packages/design-system/src/components/CarouselInput/CarouselImage.tsx index dd2e02fe1..4d6f396cc 100644 --- a/packages/design-system/src/components/CarouselInput/CarouselImage.tsx +++ b/packages/design-system/src/components/CarouselInput/CarouselImage.tsx @@ -24,7 +24,7 @@ export const CarouselImage = (props: CarouselImageProps) => { if (isError) { return ( - + ); diff --git a/packages/design-system/src/components/IconButton/IconButton.tsx b/packages/design-system/src/components/IconButton/IconButton.tsx index f7bd874fd..083d99690 100644 --- a/packages/design-system/src/components/IconButton/IconButton.tsx +++ b/packages/design-system/src/components/IconButton/IconButton.tsx @@ -3,8 +3,8 @@ import * as React from 'react'; import { styled } from 'styled-components'; import { PolymorphicRef, PropsToTransientProps } from '../../types'; +import { AccessibleIcon } from '../../utilities/AccessibleIcon'; import { forwardRef } from '../../utilities/forwardRef'; -import { VisuallyHidden } from '../../utilities/VisuallyHidden'; import { BaseButton, BaseButtonComponent, BaseButtonProps } from '../BaseButton'; import { Flex, FlexComponent } from '../Flex'; import { Tooltip } from '../Tooltip'; @@ -20,55 +20,38 @@ const VARIANTS = [VARIANT_DEFAULT, VARIANT_SECONDARY] as const; type IconButtonSize = (typeof SIZES)[number]; type IconButtonVariant = (typeof VARIANTS)[number]; -type SharedIconButtonProps = BaseButtonProps & { +type IconButtonProps = BaseButtonProps & { + children: React.ReactNode; + /** + * This isn't visually rendererd, but required for accessibility. + */ + label: string; onClick?: React.MouseEventHandler; + /** + * @default 'S' + */ size?: IconButtonSize; + /** + * @default 'tertiary' + */ variant?: IconButtonVariant; + /** + * @default true + */ + withTooltip?: boolean; }; -type LabelOnlyProps = SharedIconButtonProps & { - label: string; - ['aria-label']?: never; -}; - -type AriaLabelOnlyProps = SharedIconButtonProps & { - label?: never; - ['aria-label']: string; -}; - -interface IconOnlyProps { - icon: React.ReactNode; - children?: never; -} - -interface ChildrenOnlyProps { - icon?: never; - children: React.ReactNode; -} - -type ChildrenWithLabel = LabelOnlyProps & ChildrenOnlyProps; -type ChildrenWithAriaLabel = AriaLabelOnlyProps & ChildrenOnlyProps; -type IconWithLabel = LabelOnlyProps & IconOnlyProps; -type IconWithAriaLabel = AriaLabelOnlyProps & IconOnlyProps; - -type IconButtonProps = - | ChildrenWithLabel - | ChildrenWithAriaLabel - | IconWithLabel - | IconWithAriaLabel; - const IconButton = forwardRef( ( { label, background, children, - icon, disabled = false, onClick, size = SIZES[0], - 'aria-label': ariaLabel, variant = VARIANTS[0], + withTooltip = true, ...restProps }: IconButtonProps, ref: PolymorphicRef, @@ -90,16 +73,11 @@ const IconButton = forwardRef( onClick={handleClick} $variant={variant} > - {label ?? ariaLabel} - - {React.cloneElement((icon || children) as React.ReactElement, { - 'aria-hidden': true, - focusable: false, // See: https://allyjs.io/tutorials/focusing-in-svg.html#making-svg-elements-focusable - })} + {children} ); - return label ? {component} : component; + return withTooltip ? {component} : component; }, ); @@ -180,15 +158,4 @@ const IconButtonGroup = styled(Flex)` `; export { IconButton, IconButtonGroup }; -export type { - IconButtonProps, - IconButtonComponent, - IconButtonSize, - IconButtonVariant, - ChildrenOnlyProps, - ChildrenWithAriaLabel, - ChildrenWithLabel, - IconOnlyProps, - IconWithAriaLabel, - IconWithLabel, -}; +export type { IconButtonProps, IconButtonComponent, IconButtonSize, IconButtonVariant }; diff --git a/packages/design-system/src/components/ModalLayout/ModalHeader.tsx b/packages/design-system/src/components/ModalLayout/ModalHeader.tsx index a4e038eb5..586daf810 100644 --- a/packages/design-system/src/components/ModalLayout/ModalHeader.tsx +++ b/packages/design-system/src/components/ModalLayout/ModalHeader.tsx @@ -24,7 +24,9 @@ export const ModalHeader = ({ children, closeLabel = 'Close the modal' }: ModalH {children} - } /> + + + ); diff --git a/packages/design-system/src/components/ModalLayout/__tests__/__snapshots__/ModalLayout.test.tsx.snap b/packages/design-system/src/components/ModalLayout/__tests__/__snapshots__/ModalLayout.test.tsx.snap index 1daf4b1f8..6a8bbed37 100644 --- a/packages/design-system/src/components/ModalLayout/__tests__/__snapshots__/ModalLayout.test.tsx.snap +++ b/packages/design-system/src/components/ModalLayout/__tests__/__snapshots__/ModalLayout.test.tsx.snap @@ -310,11 +310,6 @@ exports[`ModalLayout should render component and match snapshot 1`] = ` class="c8 c2 c9 c10" type="button" > - - Close the modal - + + Close the modal + diff --git a/packages/design-system/src/components/SubNav/SubNavHeader.tsx b/packages/design-system/src/components/SubNav/SubNavHeader.tsx index 7ec2645e2..4838a06b0 100644 --- a/packages/design-system/src/components/SubNav/SubNavHeader.tsx +++ b/packages/design-system/src/components/SubNav/SubNavHeader.tsx @@ -108,7 +108,9 @@ export const SubNavHeader = ({ {label} {searchable && ( - } /> + + + )} diff --git a/packages/design-system/src/components/Tooltip/Tooltip.tsx b/packages/design-system/src/components/Tooltip/Tooltip.tsx index 1e320f86e..84fb9998c 100644 --- a/packages/design-system/src/components/Tooltip/Tooltip.tsx +++ b/packages/design-system/src/components/Tooltip/Tooltip.tsx @@ -1,69 +1,93 @@ import * as React from 'react'; -import { styled } from 'styled-components'; +import * as Tooltip from '@radix-ui/react-tooltip'; +import { keyframes, styled } from 'styled-components'; -import { useId } from '../../hooks/useId'; -import { VisuallyHidden } from '../../utilities/VisuallyHidden'; -import { Box, BoxComponent, BoxProps } from '../Box'; -import { Portal } from '../Portal'; import { Typography } from '../Typography'; -import { useTooltipHandlers } from './hooks/useTooltipHandlers'; -import { useTooltipLayout } from './hooks/useTooltipLayout'; -import { TooltipPosition } from './utils/positionTooltip'; +type TooltipElement = HTMLDivElement; -interface TooltipProps extends Omit, 'position'> { +interface TooltipProps extends Tooltip.TooltipContentProps { + children?: React.ReactNode; + defaultOpen?: boolean; + /** + * The duration from when the pointer enters the trigger until the tooltip gets opened. This will + * override the prop with the same name passed to Provider. + * @default 500 + */ + delayDuration?: number; + /** + * @deprecated Use `label` instead. + */ description?: string; - delay?: number; - id?: string; + /** + * When `true`, trying to hover the content will result in the tooltip closing as the pointer leaves the trigger. + * @default false + */ + disableHoverableContent?: boolean; label?: React.ReactNode; - position?: TooltipPosition; + onOpenChange?: (open: boolean) => void; + open?: boolean; } -const Tooltip = ({ children, label, description, delay = 500, position = 'top', id, ...props }: TooltipProps) => { - const tooltipId = useId(id); - const descriptionId = useId(); - const { visible, ...tooltipHandlers } = useTooltipHandlers(delay); - const { tooltipWrapperRef, toggleSourceRef } = useTooltipLayout(visible, position); +const TooltipImpl = React.forwardRef( + ( + { + children, + description, + label, + defaultOpen, + open, + onOpenChange, + delayDuration = 500, + disableHoverableContent, + ...restProps + }, + forwardedRef, + ) => { + return ( + + {children} + + + + {label || description} + + + + + ); + }, +); - const childrenClone = React.cloneElement(children as React.ReactElement, { - tabIndex: 0, - 'aria-labelledby': label ? tooltipId : undefined, - 'aria-describedby': description ? tooltipId : undefined, - ...tooltipHandlers, - }); - - return ( - <> - - - {visible && {description}} - - {label || description} - - - +const fadeIn = keyframes` + from { + opacity: 0; + } + to { + opacity: 1; + } +`; - {childrenClone} - - ); -}; +const TooltipContent = styled(Tooltip.Content)` + background-color: ${(props) => props.theme.colors.neutral900}; + color: ${(props) => props.theme.colors.neutral0}; + padding-inline: ${(props) => props.theme.spaces[2]}; + padding-block: ${(props) => props.theme.spaces[2]}; + border-radius: ${(props) => props.theme.borderRadius}; + will-change: opacity; + transform-origin: var(--radix-tooltip-content-transform-origin); -const TooltipWrapper = styled(Box)<{ $visible: boolean }>` - /* z-index exist because of its position inside Modals */ - z-index: 4; - display: ${({ $visible }) => ($visible ? 'revert' : 'none')}; + @media (prefers-reduced-motion: no-preference) { + animation: ${fadeIn} 200ms ${(props) => props.theme.easings.authenticMotion}; + } `; -export { Tooltip }; -export type { TooltipProps }; +export { TooltipImpl as Tooltip }; +export type { TooltipProps, TooltipElement }; diff --git a/packages/design-system/src/components/Tooltip/__tests__/Tooltip.test.tsx b/packages/design-system/src/components/Tooltip/__tests__/Tooltip.test.tsx deleted file mode 100644 index d8526b14b..000000000 --- a/packages/design-system/src/components/Tooltip/__tests__/Tooltip.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { render, fireEvent, screen } from '@test/utils'; - -import { Tooltip } from '../Tooltip'; - -describe('Tooltip', () => { - it('snapshots document.body when the tooltip is not visible but exists in the DOM', () => { - render( - - - , - ); - - expect(document.body).toMatchSnapshot(); - }); - - it('snapshots document.body when the button is focused (aria-describedby exists)', () => { - render( - - - , - ); - - fireEvent.focus(screen.getByText('Show tooltip')); - - expect(document.body).toMatchSnapshot(); - }); - - it('snapshots document.body with a label', () => { - render( - - - , - ); - - fireEvent.focus(screen.getByText('+')); - - expect(document.body).toMatchSnapshot(); - }); -}); diff --git a/packages/design-system/src/components/Tooltip/__tests__/__snapshots__/Tooltip.test.tsx.snap b/packages/design-system/src/components/Tooltip/__tests__/__snapshots__/Tooltip.test.tsx.snap deleted file mode 100644 index 1b77ab334..000000000 --- a/packages/design-system/src/components/Tooltip/__tests__/__snapshots__/Tooltip.test.tsx.snap +++ /dev/null @@ -1,265 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Tooltip snapshots document.body when the button is focused (aria-describedby exists) 1`] = ` -.c0 { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; -} - -.c1 { - background: #212134; - padding-block: 8px; - padding-inline: 8px; - border-radius: 4px; - position: absolute; -} - -.c3 { - font-size: 1.2rem; - line-height: 1.33; - font-weight: 600; - color: #ffffff; -} - -.c2 { - z-index: 4; - display: revert; -} - - -
- - - - -

-

-

-
- -
- -`; - -exports[`Tooltip snapshots document.body when the tooltip is not visible but exists in the DOM 1`] = ` -.c0 { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; -} - -.c1 { - background: #212134; - padding-block: 8px; - padding-inline: 8px; - border-radius: 4px; - position: absolute; -} - -.c3 { - font-size: 1.2rem; - line-height: 1.33; - font-weight: 600; - color: #ffffff; -} - -.c2 { - z-index: 4; - display: none; -} - - -
- - - - -

-

-

-
- -
- -`; - -exports[`Tooltip snapshots document.body with a label 1`] = ` -.c0 { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; -} - -.c1 { - background: #212134; - padding-block: 8px; - padding-inline: 8px; - border-radius: 4px; - position: absolute; -} - -.c3 { - font-size: 1.2rem; - line-height: 1.33; - font-weight: 600; - color: #ffffff; -} - -.c2 { - z-index: 4; - display: revert; -} - - -
- - - - -

-

-

-
- -
- -`; diff --git a/packages/design-system/src/components/Tooltip/hooks/useTooltipHandlers.ts b/packages/design-system/src/components/Tooltip/hooks/useTooltipHandlers.ts deleted file mode 100644 index 658d51ac1..000000000 --- a/packages/design-system/src/components/Tooltip/hooks/useTooltipHandlers.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; - -export const useTooltipHandlers = (delay: number) => { - const [visible, setVisible] = React.useState(false); - const timerRef = React.useRef(null); - - const clearTimer = () => { - if (typeof timerRef.current === 'number') { - clearTimeout(timerRef.current); - timerRef.current = null; - } - }; - - React.useEffect(() => { - return () => { - clearTimer(); - }; - }, []); - - const onFocus = () => { - setVisible(true); - }; - - const onBlur = () => { - setVisible(false); - }; - - const onMouseEnter = () => { - timerRef.current = setTimeout(() => { - setVisible(true); - }, delay); - }; - - const onMouseLeave = () => { - clearTimer(); - setVisible(false); - }; - - return { visible, onFocus, onBlur, onMouseEnter, onMouseLeave }; -}; diff --git a/packages/design-system/src/components/Tooltip/hooks/useTooltipLayout.ts b/packages/design-system/src/components/Tooltip/hooks/useTooltipLayout.ts deleted file mode 100644 index 1066d91d5..000000000 --- a/packages/design-system/src/components/Tooltip/hooks/useTooltipLayout.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react'; - -import { positionTooltip, TooltipPosition } from '../utils/positionTooltip'; - -export const useTooltipLayout = (visible: boolean, position: TooltipPosition) => { - const tooltipWrapperRef = React.useRef(null); - const toggleSourceRef = React.useRef(null); - - React.useLayoutEffect(() => { - if (visible) { - const tooltip = tooltipWrapperRef.current; - const toggleSource = toggleSourceRef.current; - - if (tooltip && toggleSource) { - const tooltipPosition = positionTooltip(tooltip, toggleSource, position); - - tooltip.style.left = `${tooltipPosition.left}px`; - tooltip.style.top = `${tooltipPosition.top}px`; - } - } - }, [position, visible]); - - return { tooltipWrapperRef, toggleSourceRef }; -}; diff --git a/packages/design-system/src/components/Tooltip/utils/__tests__/positionTooltip.spec.ts b/packages/design-system/src/components/Tooltip/utils/__tests__/positionTooltip.spec.ts deleted file mode 100644 index f2a4f97e4..000000000 --- a/packages/design-system/src/components/Tooltip/utils/__tests__/positionTooltip.spec.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { positionTooltip } from '../positionTooltip'; - -describe('positionTooltip', () => { - let tooltipNode; - let toggleSourceNode; - - beforeEach(() => { - tooltipNode = { - getBoundingClientRect: () => ({ width: 160, height: 40 }), - }; - toggleSourceNode = { - getBoundingClientRect: () => ({ left: 500, top: 300, width: 100, height: 30 }), - }; - - window.pageYOffset = 800; - }); - - it('positions the tooltip at the top of the toggle source', () => { - const position = positionTooltip(tooltipNode, toggleSourceNode); - - expect(position).toEqual({ left: 470, top: 1052 }); - }); - - it('positions the tooltip at the bottom of the toggle source', () => { - const position = positionTooltip(tooltipNode, toggleSourceNode, 'bottom'); - - expect(position).toEqual({ left: 470, top: 1138 }); - }); - - it('positions the tooltip on the left of the toggle source', () => { - const position = positionTooltip(tooltipNode, toggleSourceNode, 'left'); - - expect(position).toEqual({ left: 332, top: 1095 }); - }); - - it('positions the tooltip on the right of the toggle source', () => { - const position = positionTooltip(tooltipNode, toggleSourceNode, 'right'); - - expect(position).toEqual({ left: 608, top: 1095 }); - }); - - it('positions the tooltip on the right of the toggle source when toggle source pos is top-left', () => { - toggleSourceNode = { - getBoundingClientRect: () => ({ left: 15, top: 15, width: 50, height: 20 }), - }; - window.pageYOffset = 0; - - const position = positionTooltip(tooltipNode, toggleSourceNode); - - expect(position).toEqual({ left: 73, top: 3 }); - }); - - it('positions the tooltip on the right of the toggle source when toggle source pos is bottom-left', () => { - toggleSourceNode = { - getBoundingClientRect: () => ({ left: 15, top: 760, width: 50, height: 20 }), - }; - - window.pageYOffset = 0; - window.innerHeight = 800; - - const position = positionTooltip(tooltipNode, toggleSourceNode); - - expect(position).toEqual({ left: 73, top: 748 }); - }); - - it('positions the tooltip below the toggle source when toggle source pos is top-center', () => { - toggleSourceNode = { - getBoundingClientRect: () => ({ left: 400, top: 15, width: 50, height: 20 }), - }; - window.pageYOffset = 0; - window.innerWidth = 800; - - const position = positionTooltip(tooltipNode, toggleSourceNode); - - expect(position).toEqual({ left: 345, top: 43 }); - }); - - it('positions the tooltip above the toggle source when toggle source pos is bottom-center', () => { - toggleSourceNode = { - getBoundingClientRect: () => ({ left: 400, top: 760, width: 50, height: 20 }), - }; - window.pageYOffset = 0; - window.innerWidth = 800; - - const position = positionTooltip(tooltipNode, toggleSourceNode); - - expect(position).toEqual({ left: 345, top: 712 }); - }); - - it('positions the tooltip on the left of the toggle source when toggle source pos is top-right', () => { - toggleSourceNode = { - getBoundingClientRect: () => ({ left: 750, right: 800, top: 15, width: 50, height: 20 }), - }; - window.innerWidth = 800; - window.pageYOffset = 0; - - const position = positionTooltip(tooltipNode, toggleSourceNode); - - expect(position).toEqual({ left: 582, top: 5 }); - }); - - it('positions the tooltip on the left of the toggle source when toggle source pos is bottom-right', () => { - toggleSourceNode = { - getBoundingClientRect: () => ({ left: 750, right: 800, top: 760, width: 50, height: 20 }), - }; - window.innerWidth = 800; - window.innerHeight = 800; - window.pageYOffset = 0; - - const position = positionTooltip(tooltipNode, toggleSourceNode); - - expect(position).toEqual({ left: 582, top: 750 }); - }); -}); diff --git a/packages/design-system/src/components/Tooltip/utils/positionTooltip.ts b/packages/design-system/src/components/Tooltip/utils/positionTooltip.ts deleted file mode 100644 index 8b258e6f4..000000000 --- a/packages/design-system/src/components/Tooltip/utils/positionTooltip.ts +++ /dev/null @@ -1,89 +0,0 @@ -export type TooltipPosition = 'top' | 'bottom' | 'left' | 'right'; - -const SPACE_BETWEEN = 8; - -const positionBottom = (tooltipRect: DOMRect, toggleSourceRect: DOMRect) => { - const widthDifference = (tooltipRect.width - toggleSourceRect.width) / 2; - const left = toggleSourceRect.left - widthDifference; - const top = toggleSourceRect.top + toggleSourceRect.height + SPACE_BETWEEN + window.pageYOffset; - - return { - left, - top, - }; -}; - -const positionRight = (tooltipRect: DOMRect, toggleSourceRect: DOMRect) => { - const heightDifference = (tooltipRect.height - toggleSourceRect.height) / 2; - const left = toggleSourceRect.left + toggleSourceRect.width + SPACE_BETWEEN; - const top = toggleSourceRect.top - heightDifference + window.pageYOffset; - - return { left, top }; -}; - -const positionLeft = (tooltipRect: DOMRect, toggleSourceRect: DOMRect) => { - const heightDifference = (tooltipRect.height - toggleSourceRect.height) / 2; - const left = toggleSourceRect.left - tooltipRect.width - SPACE_BETWEEN; - const top = toggleSourceRect.top - heightDifference + window.pageYOffset; - - return { left, top }; -}; - -const positionTop = (tooltipRect: DOMRect, toggleSourceRect: DOMRect) => { - const widthDifference = (tooltipRect.width - toggleSourceRect.width) / 2; - let left = toggleSourceRect.left - widthDifference; - let top = toggleSourceRect.top - tooltipRect.height - SPACE_BETWEEN + window.pageYOffset; - - // CONDITIONS TO HANDLE TOOLTIP OVERFLOW OUT OF VIEWPORT - - // calculate the space between rightside viewport to rightside source element - // knowing this space will help calculate if tooltip will overflow right side - const rightSpaceDifference = window.innerWidth - toggleSourceRect.right; - - // calculate rightside pos of tooltip to compare later to window.innerWidth - const overflowRight = toggleSourceRect.left + tooltipRect.width - rightSpaceDifference; - - if (overflowRight > window.innerWidth) { - // if tooltip overflow right side of viewport - // place tooltip left side from source element - left = toggleSourceRect.left - tooltipRect.width - SPACE_BETWEEN; - top = toggleSourceRect.top + window.scrollY - toggleSourceRect.height / 2; - } else if (left < 0) { - // if overflow left - // place tooltip right side from source element - left = toggleSourceRect.width + toggleSourceRect.left + SPACE_BETWEEN; - top = toggleSourceRect.top + window.scrollY - tooltipRect.height / 2 + SPACE_BETWEEN; - } else if (top < 0 && left > 0) { - // if overflow top but not left - // place tooltip below source element - top = toggleSourceRect.top + toggleSourceRect.height + SPACE_BETWEEN; - } - - return { - left, - top, - }; -}; - -export const positionTooltip = ( - tooltipNode: HTMLDivElement, - toggleSourceNode: HTMLSpanElement, - position?: TooltipPosition, -) => { - const tooltipRect = tooltipNode.getBoundingClientRect(); - const toggleSourceRect = toggleSourceNode.getBoundingClientRect(); - - if (position === 'bottom') { - return positionBottom(tooltipRect, toggleSourceRect); - } - - if (position === 'right') { - return positionRight(tooltipRect, toggleSourceRect); - } - - if (position === 'left') { - return positionLeft(tooltipRect, toggleSourceRect); - } - - return positionTop(tooltipRect, toggleSourceRect); -}; diff --git a/packages/design-system/src/utilities/DesignSystemProvider.tsx b/packages/design-system/src/utilities/DesignSystemProvider.tsx index bab00ed48..bccb0b8c0 100644 --- a/packages/design-system/src/utilities/DesignSystemProvider.tsx +++ b/packages/design-system/src/utilities/DesignSystemProvider.tsx @@ -1,3 +1,4 @@ +import { Provider as TooltipProvider, TooltipProviderProps } from '@radix-ui/react-tooltip'; import { DefaultTheme, ThemeProvider } from 'styled-components'; import { LiveRegions } from '../components/LiveRegions'; @@ -30,17 +31,19 @@ const [Provider, useDesignSystem] = createContext('Str interface DesignSystemProviderProps extends Partial { children?: React.ReactNode; theme?: DefaultTheme; + tooltipConfig?: Omit; } const DesignSystemProvider = ({ + children, locale = getDefaultLocale(), theme = lightTheme, - children, + tooltipConfig, }: DesignSystemProviderProps) => { return ( - {children} + {children} diff --git a/turbo.json b/turbo.json index f20ae5bbf..98db104bc 100644 --- a/turbo.json +++ b/turbo.json @@ -15,7 +15,6 @@ "persistent": true }, "develop": { - "dependsOn": ["^build"], "cache": false, "persistent": true }, diff --git a/yarn.lock b/yarn.lock index eab687acc..ed90c4f1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4113,6 +4113,37 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-tooltip@npm:1.0.7": + version: 1.0.7 + resolution: "@radix-ui/react-tooltip@npm:1.0.7" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-dismissable-layer": 1.0.5 + "@radix-ui/react-id": 1.0.1 + "@radix-ui/react-popper": 1.1.3 + "@radix-ui/react-portal": 1.0.4 + "@radix-ui/react-presence": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-slot": 1.0.2 + "@radix-ui/react-use-controllable-state": 1.0.1 + "@radix-ui/react-visually-hidden": 1.0.3 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 894d448c69a3e4d7626759f9f6c7997018fe8ef9cde098393bd83e10743d493dfd284eef041e46accc45486d5a5cd5f76d97f56afbdace7aed6e0cb14007bf15 + languageName: node + linkType: hard + "@radix-ui/react-use-callback-ref@npm:1.0.1, @radix-ui/react-use-callback-ref@npm:^1.0.1": version: 1.0.1 resolution: "@radix-ui/react-use-callback-ref@npm:1.0.1" @@ -5301,6 +5332,7 @@ __metadata: "@radix-ui/react-dismissable-layer": ^1.0.5 "@radix-ui/react-dropdown-menu": ^2.0.6 "@radix-ui/react-focus-scope": 1.0.4 + "@radix-ui/react-tooltip": 1.0.7 "@radix-ui/react-use-controllable-state": ^1.0.1 "@strapi/icons": ^2.0.0-beta.3 "@strapi/pack-up": ^5.0.0