From 2dea917aec32eded1a1c15468cb2ca0eb9a5dcd3 Mon Sep 17 00:00:00 2001 From: Artur Santiago Date: Wed, 22 Jan 2025 16:47:24 -0300 Subject: [PATCH 01/12] feat: create tooltip component --- packages/components/src/index.ts | 2 + .../src/molecules/Tooltip/Tooltip.tsx | 136 +++++++++++ .../components/src/molecules/Tooltip/index.ts | 2 + .../components/molecules/Tooltip/styles.scss | 222 ++++++++++++++++++ 4 files changed, 362 insertions(+) create mode 100644 packages/components/src/molecules/Tooltip/Tooltip.tsx create mode 100644 packages/components/src/molecules/Tooltip/index.ts create mode 100644 packages/ui/src/components/molecules/Tooltip/styles.scss diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index c6051c2318..70c4f5e0cc 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -148,6 +148,8 @@ export { default as RatingField } from './molecules/RatingField' export type { RatingFieldProps } from './molecules/RatingField' export { default as RegionBar } from './molecules/RegionBar' export type { RegionBarProps } from './molecules/RegionBar' +export { default as Tooltip } from './molecules/Tooltip' +export type { TooltipProps } from './molecules/Tooltip' export { default as SearchProvider } from './molecules/SearchProvider' export type { SearchProviderContextValue } from './molecules/SearchProvider' diff --git a/packages/components/src/molecules/Tooltip/Tooltip.tsx b/packages/components/src/molecules/Tooltip/Tooltip.tsx new file mode 100644 index 0000000000..8e734aa455 --- /dev/null +++ b/packages/components/src/molecules/Tooltip/Tooltip.tsx @@ -0,0 +1,136 @@ +import React, { + type ReactNode, + type MouseEventHandler, + useState, + forwardRef, + HTMLAttributes, +} from 'react' +import Icon from '../../atoms/Icon' +import IconButton from '../IconButton' +/** + * Possible sides for the tooltip. + */ +export type Side = 'top' | 'right' | 'bottom' | 'left' + +/** + * Possible alignments for the tooltip. + */ +export type Alignment = 'start' | 'end' + +/** + * Example: "top", "top-start", "top-end", etc. + */ +export type AlignedPlacement = `${Side}-${Alignment}` + +/** + * Type that combines pure side (e.g., "top") or side + alignment (e.g., "top-start"). + */ +export type Placement = Side | AlignedPlacement + +export interface TooltipProps + extends Omit, 'content'> { + /** + * Text/content of the tooltip. + */ + content: ReactNode + + /** + * Defines the side or side-alignment (e.g., "top", "right-end") of the tooltip. + */ + placement?: Placement + + /** + * If the tooltip can be closed by a button. + */ + dismissable?: boolean + + /** + * (Optional) Called when the dismiss button is clicked. + */ + onDismiss?: MouseEventHandler + + /** + * Element that activates the tooltip on hover/focus. + */ + children: ReactNode + + /** + * For testing (Cypress, Jest, etc.). + */ + testId?: string + + /** + * Maximum width of the tooltip. + */ + maxWidth?: number +} + +const Tooltip = forwardRef(function Tooltip( + { + content, + placement = 'top', + dismissable = false, + onDismiss, + children, + testId = 'fs-tooltip', + maxWidth = 300, + ...otherProps + }, + ref +) { + const [open, setOpen] = useState(false) + const [dismissed, setDismissed] = useState(false) + + const handleDismiss: MouseEventHandler = (ev) => { + onDismiss?.(ev) + setOpen(false) + setDismissed(true) + } + + const toggleOpen = () => { + if (dismissed) { + setDismissed(false) + } + setOpen(true) + } + + return ( +
setOpen(false)} + onFocus={toggleOpen} + onBlur={() => setOpen(false)} + data-testid={testId} + > + {children} + + {open && !dismissed && ( +
+
{content}
+ {dismissable && ( + } + aria-label="Dismiss tooltip" + data-fs-tooltip-dismiss-button + onClick={handleDismiss} + /> + )} + + )} +
+ ) +}) + +export default Tooltip diff --git a/packages/components/src/molecules/Tooltip/index.ts b/packages/components/src/molecules/Tooltip/index.ts new file mode 100644 index 0000000000..9825087096 --- /dev/null +++ b/packages/components/src/molecules/Tooltip/index.ts @@ -0,0 +1,2 @@ +export { default } from './Tooltip' +export { TooltipProps } from './Tooltip' diff --git a/packages/ui/src/components/molecules/Tooltip/styles.scss b/packages/ui/src/components/molecules/Tooltip/styles.scss new file mode 100644 index 0000000000..9ed457cd6b --- /dev/null +++ b/packages/ui/src/components/molecules/Tooltip/styles.scss @@ -0,0 +1,222 @@ +[data-fs-tooltip-wrapper] { + position: relative; + + [data-fs-tooltip] { + // -------------------------------------------------------- + // Design Tokens for Tooltip + // -------------------------------------------------------- + + // Default properties + --fs-tooltip-z-index: var(--fs-z-index-high); + --fs-tooltip-background: var(--fs-color-neutral-6); + --fs-tooltip-text-color: var(--fs-color-text-inverse); + --fs-tooltip-border-radius: var(--fs-border-radius); + --fs-tooltip-padding: var(--fs-spacing-2); + --fs-tooltip-gap: var(--fs-spacing-1); + --fs-tooltip-transition-property: opacity; + --fs-tooltip-transition-timing: var(--fs-transition-timing); + --fs-tooltip-transition-function: var(--fs-transition-function); + + // Indicator + --fs-tooltip-indicator-size: var(--fs-spacing-1); + --fs-tooltip-indicator-distance-edge: var(--fs-spacing-3); + --fs-tooltip-indicator-distance-base: var(--fs-spacing-1); + --fs-tooltip-indicator-translate: calc( + var(--fs-tooltip-indicator-size) + + var(--fs-tooltip-indicator-distance-base) + ); + + // -------------------------------------------------------- + // Structural Styles + // -------------------------------------------------------- + + position: absolute; + z-index: var(--fs-tooltip-z-index); + + background-color: var(--fs-tooltip-background); + color: var(--fs-tooltip-text-color); + border-radius: var(--fs-tooltip-border-radius); + padding: var(--fs-tooltip-padding); + opacity: 1; + transition: var(--fs-tooltip-transition-property) + var(--fs-tooltip-transition-timing) var(--fs-tooltip-transition-function); + + display: flex; + gap: var(--fs-tooltip-gap); + align-items: flex-start; + } + + [data-fs-tooltip-content] { + font-size: var(--fs-text-size-0); + font-weight: var(--fs-text-weight-medium); + line-height: 16px; + width: max-content; + } + + [data-fs-tooltip-dismiss-button] { + flex-shrink: 0; + width: var(--fs-control-tap-size-smallest); + height: var(--fs-control-tap-size-smallest); + min-height: var(--fs-control-tap-size-smallest); + padding: 0; + } + + [data-fs-tooltip-indicator] { + position: absolute; + width: 0; + height: 0; + border: var(--fs-tooltip-indicator-size) solid transparent; + } + + // -------------------------------------------------------- + // Variants Styles + // -------------------------------------------------------- + + /* TOP */ + [data-fs-tooltip-position^='top'] { + bottom: 100%; + transform: translateY(calc(-1 * var(--fs-tooltip-indicator-translate))); + } + [data-fs-tooltip-position^='top'] [data-fs-tooltip-indicator] { + top: 100%; + border-top-color: var(--fs-tooltip-background); + } + + /* TOP-CENTER */ + [data-fs-tooltip-position='top'] { + left: 50%; + transform: translateX(-50%) + translateY(calc(-1 * var(--fs-tooltip-indicator-translate))); + } + [data-fs-tooltip-position='top'] [data-fs-tooltip-indicator] { + left: 50%; + transform: translateX(-50%); + } + + /* TOP-START */ + [data-fs-tooltip-position='top-start'] { + left: 0; + } + [data-fs-tooltip-position='top-start'] [data-fs-tooltip-indicator] { + left: var(--fs-spacing-3); + } + + /* TOP-END */ + [data-fs-tooltip-position='top-end'] { + right: 0; + } + [data-fs-tooltip-position='top-end'] [data-fs-tooltip-indicator] { + right: var(--fs-spacing-3); + } + + /* RIGHT */ + [data-fs-tooltip-position^='right'] { + left: 100%; + transform: translateX(var(--fs-tooltip-indicator-translate)); + } + [data-fs-tooltip-position^='right'] [data-fs-tooltip-indicator] { + right: 100%; + border-right-color: var(--fs-tooltip-background); + } + + /* RIGHT-CENTER*/ + [data-fs-tooltip-position='right'] { + top: 50%; + transform: translateY(-50%) + translateX(var(--fs-tooltip-indicator-translate)); + } + [data-fs-tooltip-position='right'] [data-fs-tooltip-indicator] { + top: 50%; + transform: translateY(-50%); + } + + /* RIGHT-START */ + [data-fs-tooltip-position='right-start'] { + top: 0; + } + [data-fs-tooltip-position='right-start'] [data-fs-tooltip-indicator] { + top: var(--fs-spacing-3); + } + + /* RIGHT-END */ + [data-fs-tooltip-position='right-end'] { + bottom: 0; + } + [data-fs-tooltip-position='right-end'] [data-fs-tooltip-indicator] { + bottom: var(--fs-spacing-3); + } + + /* BOTTOM */ + [data-fs-tooltip-position^='bottom'] { + top: 100%; + transform: translateY(var(--fs-tooltip-indicator-translate)); + } + [data-fs-tooltip-position^='bottom'] [data-fs-tooltip-indicator] { + bottom: 100%; + border-bottom-color: var(--fs-tooltip-background); + } + + /* BOTTOM-CENTER*/ + [data-fs-tooltip-position='bottom'] { + left: 50%; + transform: translateX(-50%) + translateY(var(--fs-tooltip-indicator-translate)); + } + [data-fs-tooltip-position='bottom'] [data-fs-tooltip-indicator] { + left: 50%; + transform: translateX(-50%); + } + + /* BOTTOM-START */ + [data-fs-tooltip-position='bottom-start'] { + left: 0; + } + [data-fs-tooltip-position='bottom-start'] [data-fs-tooltip-indicator] { + left: var(--fs-spacing-3); + } + + /* BOTTOM-END */ + [data-fs-tooltip-position='bottom-end'] { + right: 0; + } + [data-fs-tooltip-position='bottom-end'] [data-fs-tooltip-indicator] { + right: var(--fs-spacing-3); + } + + /* LEFT */ + [data-fs-tooltip-position^='left'] { + right: 100%; + transform: translateX(calc(-1 * var(--fs-tooltip-indicator-translate))); + } + [data-fs-tooltip-position^='left'] [data-fs-tooltip-indicator] { + left: 100%; + border-left-color: var(--fs-tooltip-background); + } + + /* LEFT-CENTER*/ + [data-fs-tooltip-position='left'] { + top: 50%; + transform: translateY(-50%) + translateX(calc(-1 * var(--fs-tooltip-indicator-translate))); + } + [data-fs-tooltip-position='left'] [data-fs-tooltip-indicator] { + top: 50%; + transform: translateY(-50%); + } + + /* LEFT-START */ + [data-fs-tooltip-position='left-start'] { + top: 0; + } + [data-fs-tooltip-position='left-start'] [data-fs-tooltip-indicator] { + top: var(--fs-spacing-3); + } + + /* LEFT-END */ + [data-fs-tooltip-position='left-end'] { + bottom: 0; + } + [data-fs-tooltip-position='left-end'] [data-fs-tooltip-indicator] { + bottom: var(--fs-spacing-3); + } +} From 204be107999b1b0d594b8c1e92e3fce82366ac82 Mon Sep 17 00:00:00 2001 From: Artur Santiago Date: Tue, 28 Jan 2025 14:40:36 -0300 Subject: [PATCH 02/12] feat(Tooltip): set display inline flex for tooltip wrapper --- packages/ui/src/components/molecules/Tooltip/styles.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/components/molecules/Tooltip/styles.scss b/packages/ui/src/components/molecules/Tooltip/styles.scss index 9ed457cd6b..5473dff340 100644 --- a/packages/ui/src/components/molecules/Tooltip/styles.scss +++ b/packages/ui/src/components/molecules/Tooltip/styles.scss @@ -1,5 +1,6 @@ [data-fs-tooltip-wrapper] { position: relative; + display: inline-flex; [data-fs-tooltip] { // -------------------------------------------------------- From 33cb40efa8e6235f8ffe4ef18d317af5afc83f91 Mon Sep 17 00:00:00 2001 From: Artur Santiago Date: Tue, 4 Feb 2025 15:38:08 -0300 Subject: [PATCH 03/12] fix: lint fix --- packages/components/src/molecules/Tooltip/Tooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/molecules/Tooltip/Tooltip.tsx b/packages/components/src/molecules/Tooltip/Tooltip.tsx index 8e734aa455..85ead7051f 100644 --- a/packages/components/src/molecules/Tooltip/Tooltip.tsx +++ b/packages/components/src/molecules/Tooltip/Tooltip.tsx @@ -3,7 +3,7 @@ import React, { type MouseEventHandler, useState, forwardRef, - HTMLAttributes, + type HTMLAttributes, } from 'react' import Icon from '../../atoms/Icon' import IconButton from '../IconButton' From 0433e9f90da4402b84a9ee7ee8bb4631ee281d2b Mon Sep 17 00:00:00 2001 From: Artur Santiago Date: Thu, 6 Feb 2025 17:55:19 -0300 Subject: [PATCH 04/12] feat: add tooltip to ui components styles --- packages/ui/src/styles/components.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/styles/components.scss b/packages/ui/src/styles/components.scss index 610b7c7f1d..83b13a55ac 100644 --- a/packages/ui/src/styles/components.scss +++ b/packages/ui/src/styles/components.scss @@ -57,6 +57,7 @@ @import "../components/molecules/Toast/styles"; @import "../components/molecules/Toggle/styles"; @import "../components/molecules/ToggleField/styles"; +@import "../components/molecules/Tooltip/styles"; // Organisms @import "../components/organisms/BannerText/styles"; From dd353aed7fee9b17d66c15fad6b7205a9b3babed Mon Sep 17 00:00:00 2001 From: Artur Santiago Date: Fri, 14 Feb 2025 14:00:37 -0300 Subject: [PATCH 05/12] style(Tooltip): add iddentation --- .../components/molecules/Tooltip/styles.scss | 144 ++++++++++-------- 1 file changed, 80 insertions(+), 64 deletions(-) diff --git a/packages/ui/src/components/molecules/Tooltip/styles.scss b/packages/ui/src/components/molecules/Tooltip/styles.scss index 5473dff340..26d0d5b182 100644 --- a/packages/ui/src/components/molecules/Tooltip/styles.scss +++ b/packages/ui/src/components/molecules/Tooltip/styles.scss @@ -8,24 +8,21 @@ // -------------------------------------------------------- // Default properties - --fs-tooltip-z-index: var(--fs-z-index-high); - --fs-tooltip-background: var(--fs-color-neutral-6); - --fs-tooltip-text-color: var(--fs-color-text-inverse); - --fs-tooltip-border-radius: var(--fs-border-radius); - --fs-tooltip-padding: var(--fs-spacing-2); - --fs-tooltip-gap: var(--fs-spacing-1); - --fs-tooltip-transition-property: opacity; - --fs-tooltip-transition-timing: var(--fs-transition-timing); - --fs-tooltip-transition-function: var(--fs-transition-function); + --fs-tooltip-z-index : var(--fs-z-index-high); + --fs-tooltip-background : var(--fs-color-neutral-6); + --fs-tooltip-text-color : var(--fs-color-text-inverse); + --fs-tooltip-border-radius : var(--fs-border-radius); + --fs-tooltip-padding : var(--fs-spacing-2); + --fs-tooltip-gap : var(--fs-spacing-1); + --fs-tooltip-transition-property : opacity; + --fs-tooltip-transition-timing : var(--fs-transition-timing); + --fs-tooltip-transition-function : var(--fs-transition-function); // Indicator - --fs-tooltip-indicator-size: var(--fs-spacing-1); - --fs-tooltip-indicator-distance-edge: var(--fs-spacing-3); - --fs-tooltip-indicator-distance-base: var(--fs-spacing-1); - --fs-tooltip-indicator-translate: calc( - var(--fs-tooltip-indicator-size) + - var(--fs-tooltip-indicator-distance-base) - ); + --fs-tooltip-indicator-size : var(--fs-spacing-1); + --fs-tooltip-indicator-distance-edge : var(--fs-spacing-3); + --fs-tooltip-indicator-distance-base : var(--fs-spacing-1); + --fs-tooltip-indicator-translate : calc(var(--fs-tooltip-indicator-size) + var(--fs-tooltip-indicator-distance-base)); // -------------------------------------------------------- // Structural Styles @@ -33,25 +30,24 @@ position: absolute; z-index: var(--fs-tooltip-z-index); - - background-color: var(--fs-tooltip-background); + display: flex; + gap: var(--fs-tooltip-gap); + align-items: flex-start; + padding: var(--fs-tooltip-padding); color: var(--fs-tooltip-text-color); + background-color: var(--fs-tooltip-background); border-radius: var(--fs-tooltip-border-radius); - padding: var(--fs-tooltip-padding); opacity: 1; - transition: var(--fs-tooltip-transition-property) + transition: + var(--fs-tooltip-transition-property) var(--fs-tooltip-transition-timing) var(--fs-tooltip-transition-function); - - display: flex; - gap: var(--fs-tooltip-gap); - align-items: flex-start; } [data-fs-tooltip-content] { + width: max-content; font-size: var(--fs-text-size-0); font-weight: var(--fs-text-weight-medium); line-height: 16px; - width: max-content; } [data-fs-tooltip-dismiss-button] { @@ -74,150 +70,170 @@ // -------------------------------------------------------- /* TOP */ - [data-fs-tooltip-position^='top'] { + [data-fs-tooltip-position^="top"] { bottom: 100%; transform: translateY(calc(-1 * var(--fs-tooltip-indicator-translate))); } - [data-fs-tooltip-position^='top'] [data-fs-tooltip-indicator] { + + [data-fs-tooltip-position^="top"] [data-fs-tooltip-indicator] { top: 100%; border-top-color: var(--fs-tooltip-background); } /* TOP-CENTER */ - [data-fs-tooltip-position='top'] { + [data-fs-tooltip-position="top"] { left: 50%; - transform: translateX(-50%) + transform: + translateX(-50%) translateY(calc(-1 * var(--fs-tooltip-indicator-translate))); } - [data-fs-tooltip-position='top'] [data-fs-tooltip-indicator] { + + [data-fs-tooltip-position="top"] [data-fs-tooltip-indicator] { left: 50%; transform: translateX(-50%); } /* TOP-START */ - [data-fs-tooltip-position='top-start'] { + [data-fs-tooltip-position="top-start"] { left: 0; } - [data-fs-tooltip-position='top-start'] [data-fs-tooltip-indicator] { + + [data-fs-tooltip-position="top-start"] [data-fs-tooltip-indicator] { left: var(--fs-spacing-3); } /* TOP-END */ - [data-fs-tooltip-position='top-end'] { + [data-fs-tooltip-position="top-end"] { right: 0; } - [data-fs-tooltip-position='top-end'] [data-fs-tooltip-indicator] { + + [data-fs-tooltip-position="top-end"] [data-fs-tooltip-indicator] { right: var(--fs-spacing-3); } /* RIGHT */ - [data-fs-tooltip-position^='right'] { + [data-fs-tooltip-position^="right"] { left: 100%; transform: translateX(var(--fs-tooltip-indicator-translate)); } - [data-fs-tooltip-position^='right'] [data-fs-tooltip-indicator] { + + [data-fs-tooltip-position^="right"] [data-fs-tooltip-indicator] { right: 100%; border-right-color: var(--fs-tooltip-background); } - /* RIGHT-CENTER*/ - [data-fs-tooltip-position='right'] { + /* RIGHT-CENTER */ + [data-fs-tooltip-position="right"] { top: 50%; - transform: translateY(-50%) + transform: + translateY(-50%) translateX(var(--fs-tooltip-indicator-translate)); } - [data-fs-tooltip-position='right'] [data-fs-tooltip-indicator] { + + [data-fs-tooltip-position="right"] [data-fs-tooltip-indicator] { top: 50%; transform: translateY(-50%); } /* RIGHT-START */ - [data-fs-tooltip-position='right-start'] { + [data-fs-tooltip-position="right-start"] { top: 0; } - [data-fs-tooltip-position='right-start'] [data-fs-tooltip-indicator] { + + [data-fs-tooltip-position="right-start"] [data-fs-tooltip-indicator] { top: var(--fs-spacing-3); } /* RIGHT-END */ - [data-fs-tooltip-position='right-end'] { + [data-fs-tooltip-position="right-end"] { bottom: 0; } - [data-fs-tooltip-position='right-end'] [data-fs-tooltip-indicator] { + + [data-fs-tooltip-position="right-end"] [data-fs-tooltip-indicator] { bottom: var(--fs-spacing-3); } /* BOTTOM */ - [data-fs-tooltip-position^='bottom'] { + [data-fs-tooltip-position^="bottom"] { top: 100%; transform: translateY(var(--fs-tooltip-indicator-translate)); } - [data-fs-tooltip-position^='bottom'] [data-fs-tooltip-indicator] { + + [data-fs-tooltip-position^="bottom"] [data-fs-tooltip-indicator] { bottom: 100%; border-bottom-color: var(--fs-tooltip-background); } - /* BOTTOM-CENTER*/ - [data-fs-tooltip-position='bottom'] { + /* BOTTOM-CENTER */ + [data-fs-tooltip-position="bottom"] { left: 50%; - transform: translateX(-50%) + transform: + translateX(-50%) translateY(var(--fs-tooltip-indicator-translate)); } - [data-fs-tooltip-position='bottom'] [data-fs-tooltip-indicator] { + + [data-fs-tooltip-position="bottom"] [data-fs-tooltip-indicator] { left: 50%; transform: translateX(-50%); } /* BOTTOM-START */ - [data-fs-tooltip-position='bottom-start'] { + [data-fs-tooltip-position="bottom-start"] { left: 0; } - [data-fs-tooltip-position='bottom-start'] [data-fs-tooltip-indicator] { + + [data-fs-tooltip-position="bottom-start"] [data-fs-tooltip-indicator] { left: var(--fs-spacing-3); } /* BOTTOM-END */ - [data-fs-tooltip-position='bottom-end'] { + [data-fs-tooltip-position="bottom-end"] { right: 0; } - [data-fs-tooltip-position='bottom-end'] [data-fs-tooltip-indicator] { + + [data-fs-tooltip-position="bottom-end"] [data-fs-tooltip-indicator] { right: var(--fs-spacing-3); } /* LEFT */ - [data-fs-tooltip-position^='left'] { + [data-fs-tooltip-position^="left"] { right: 100%; transform: translateX(calc(-1 * var(--fs-tooltip-indicator-translate))); } - [data-fs-tooltip-position^='left'] [data-fs-tooltip-indicator] { + + [data-fs-tooltip-position^="left"] [data-fs-tooltip-indicator] { left: 100%; border-left-color: var(--fs-tooltip-background); } - /* LEFT-CENTER*/ - [data-fs-tooltip-position='left'] { + /* LEFT-CENTER */ + [data-fs-tooltip-position="left"] { top: 50%; - transform: translateY(-50%) + transform: + translateY(-50%) translateX(calc(-1 * var(--fs-tooltip-indicator-translate))); } - [data-fs-tooltip-position='left'] [data-fs-tooltip-indicator] { + + [data-fs-tooltip-position="left"] [data-fs-tooltip-indicator] { top: 50%; transform: translateY(-50%); } /* LEFT-START */ - [data-fs-tooltip-position='left-start'] { + [data-fs-tooltip-position="left-start"] { top: 0; } - [data-fs-tooltip-position='left-start'] [data-fs-tooltip-indicator] { + + [data-fs-tooltip-position="left-start"] [data-fs-tooltip-indicator] { top: var(--fs-spacing-3); } /* LEFT-END */ - [data-fs-tooltip-position='left-end'] { + [data-fs-tooltip-position="left-end"] { bottom: 0; } - [data-fs-tooltip-position='left-end'] [data-fs-tooltip-indicator] { + + [data-fs-tooltip-position="left-end"] [data-fs-tooltip-indicator] { bottom: var(--fs-spacing-3); } } From 117ee04b6cf258a63f9f25c826f29dad14fb2583 Mon Sep 17 00:00:00 2001 From: Artur Santiago Date: Mon, 17 Feb 2025 12:03:02 -0300 Subject: [PATCH 06/12] docs(Tooltip): testId prop comment --- packages/components/src/molecules/Tooltip/Tooltip.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/components/src/molecules/Tooltip/Tooltip.tsx b/packages/components/src/molecules/Tooltip/Tooltip.tsx index 85ead7051f..d451658270 100644 --- a/packages/components/src/molecules/Tooltip/Tooltip.tsx +++ b/packages/components/src/molecules/Tooltip/Tooltip.tsx @@ -33,32 +33,26 @@ export interface TooltipProps * Text/content of the tooltip. */ content: ReactNode - /** * Defines the side or side-alignment (e.g., "top", "right-end") of the tooltip. */ placement?: Placement - /** * If the tooltip can be closed by a button. */ dismissable?: boolean - /** * (Optional) Called when the dismiss button is clicked. */ onDismiss?: MouseEventHandler - /** * Element that activates the tooltip on hover/focus. */ children: ReactNode - /** - * For testing (Cypress, Jest, etc.). + * ID to find this component in testing tools (e.g.: cypress, testing library, and jest). */ testId?: string - /** * Maximum width of the tooltip. */ From 901d438d6a350dfef483832e3436dcd2376e83e2 Mon Sep 17 00:00:00 2001 From: Artur Santiago Date: Mon, 17 Feb 2025 12:05:37 -0300 Subject: [PATCH 07/12] refactor(Tooltip): rename dismissable to dismissible --- packages/components/src/molecules/Tooltip/Tooltip.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/components/src/molecules/Tooltip/Tooltip.tsx b/packages/components/src/molecules/Tooltip/Tooltip.tsx index d451658270..702b25eb55 100644 --- a/packages/components/src/molecules/Tooltip/Tooltip.tsx +++ b/packages/components/src/molecules/Tooltip/Tooltip.tsx @@ -40,7 +40,7 @@ export interface TooltipProps /** * If the tooltip can be closed by a button. */ - dismissable?: boolean + dismissible?: boolean /** * (Optional) Called when the dismiss button is clicked. */ @@ -63,7 +63,7 @@ const Tooltip = forwardRef(function Tooltip( { content, placement = 'top', - dismissable = false, + dismissible = false, onDismiss, children, testId = 'fs-tooltip', @@ -104,12 +104,12 @@ const Tooltip = forwardRef(function Tooltip( ref={ref} data-fs-tooltip data-fs-tooltip-position={placement} - data-fs-tooltip-dismissable={dismissable} + data-fs-tooltip-dismissible={dismissible} style={{ maxWidth }} {...otherProps} >
{content}
- {dismissable && ( + {dismissible && ( Date: Mon, 17 Feb 2025 12:07:31 -0300 Subject: [PATCH 08/12] docs(Tooltip): adjust props comments --- packages/components/src/molecules/Tooltip/Tooltip.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/components/src/molecules/Tooltip/Tooltip.tsx b/packages/components/src/molecules/Tooltip/Tooltip.tsx index 702b25eb55..2ad24b60e4 100644 --- a/packages/components/src/molecules/Tooltip/Tooltip.tsx +++ b/packages/components/src/molecules/Tooltip/Tooltip.tsx @@ -8,12 +8,12 @@ import React, { import Icon from '../../atoms/Icon' import IconButton from '../IconButton' /** - * Possible sides for the tooltip. + * Specifies tooltip position. */ export type Side = 'top' | 'right' | 'bottom' | 'left' /** - * Possible alignments for the tooltip. + * Specifies tooltip alignment. */ export type Alignment = 'start' | 'end' @@ -23,7 +23,7 @@ export type Alignment = 'start' | 'end' export type AlignedPlacement = `${Side}-${Alignment}` /** - * Type that combines pure side (e.g., "top") or side + alignment (e.g., "top-start"). + * Combines pure side (e.g., "top") or side + alignment (e.g., "top-start"). */ export type Placement = Side | AlignedPlacement @@ -42,7 +42,7 @@ export interface TooltipProps */ dismissible?: boolean /** - * (Optional) Called when the dismiss button is clicked. + * Called when the dismiss button is clicked. */ onDismiss?: MouseEventHandler /** From 22635fd7d68a1d733a8dbcad9f4f1343f0bf47be Mon Sep 17 00:00:00 2001 From: Artur Santiago Date: Mon, 17 Feb 2025 17:12:56 -0300 Subject: [PATCH 09/12] feat(Tooltip): add accessibility to dismissible variant --- .../src/molecules/Tooltip/Tooltip.tsx | 62 +++++++++++++----- .../components/molecules/Tooltip/styles.scss | 65 ++++++++++--------- 2 files changed, 80 insertions(+), 47 deletions(-) diff --git a/packages/components/src/molecules/Tooltip/Tooltip.tsx b/packages/components/src/molecules/Tooltip/Tooltip.tsx index 2ad24b60e4..89325883d2 100644 --- a/packages/components/src/molecules/Tooltip/Tooltip.tsx +++ b/packages/components/src/molecules/Tooltip/Tooltip.tsx @@ -1,9 +1,10 @@ import React, { type ReactNode, - type MouseEventHandler, useState, forwardRef, type HTMLAttributes, + useRef, + useEffect, } from 'react' import Icon from '../../atoms/Icon' import IconButton from '../IconButton' @@ -15,17 +16,12 @@ export type Side = 'top' | 'right' | 'bottom' | 'left' /** * Specifies tooltip alignment. */ -export type Alignment = 'start' | 'end' +export type Alignment = 'start' | 'center' | 'end' /** - * Example: "top", "top-start", "top-end", etc. + * Combines side + alignment (e.g., "top-start"). */ -export type AlignedPlacement = `${Side}-${Alignment}` - -/** - * Combines pure side (e.g., "top") or side + alignment (e.g., "top-start"). - */ -export type Placement = Side | AlignedPlacement +export type Placement = `${Side}-${Alignment}` export interface TooltipProps extends Omit, 'content'> { @@ -34,7 +30,7 @@ export interface TooltipProps */ content: ReactNode /** - * Defines the side or side-alignment (e.g., "top", "right-end") of the tooltip. + * Defines the side or side-alignment (e.g., "top-center", "right-end") of the tooltip. */ placement?: Placement /** @@ -44,7 +40,11 @@ export interface TooltipProps /** * Called when the dismiss button is clicked. */ - onDismiss?: MouseEventHandler + onDismiss?: ( + ev: + | React.KeyboardEvent + | React.MouseEvent + ) => void /** * Element that activates the tooltip on hover/focus. */ @@ -57,25 +57,36 @@ export interface TooltipProps * Maximum width of the tooltip. */ maxWidth?: number + /** + * ID for the tooltip content to be used with aria-describedby. + */ + describedById?: string } const Tooltip = forwardRef(function Tooltip( { content, - placement = 'top', + placement = 'top-center', dismissible = false, onDismiss, children, testId = 'fs-tooltip', maxWidth = 300, + describedById = 'tooltip-content', ...otherProps }, ref ) { const [open, setOpen] = useState(false) const [dismissed, setDismissed] = useState(false) + const dismissButtonRef = useRef(null) + const triggerRef = useRef(null) - const handleDismiss: MouseEventHandler = (ev) => { + const handleDismiss = ( + ev: + | React.KeyboardEvent + | React.MouseEvent + ) => { onDismiss?.(ev) setOpen(false) setDismissed(true) @@ -88,6 +99,18 @@ const Tooltip = forwardRef(function Tooltip( setOpen(true) } + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + handleDismiss(event) + } + } + + useEffect(() => { + if (open && dismissible) { + dismissButtonRef.current?.focus() + } + }, [open, dismissible]) + return (
(function Tooltip( onFocus={toggleOpen} onBlur={() => setOpen(false)} data-testid={testId} + aria-describedby={describedById} + onKeyDown={handleKeyDown} + tabIndex={0} + ref={triggerRef} > {children} @@ -103,12 +130,16 @@ const Tooltip = forwardRef(function Tooltip(
-
{content}
+
+ {content} +
{dismissible && ( (function Tooltip( aria-label="Dismiss tooltip" data-fs-tooltip-dismiss-button onClick={handleDismiss} + ref={dismissButtonRef} /> )}