From 620661b61191bebf495615ab91b3d564d1b3056f Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Fri, 26 Jun 2026 17:00:33 -0400 Subject: [PATCH 01/42] first attempt at closing tooltip after click --- packages/gamut/src/Button/IconButton.tsx | 2 +- packages/gamut/src/Tip/ToolTip/index.tsx | 10 ++++++-- packages/gamut/src/Tip/shared/FloatingTip.tsx | 17 +++++++++++++ packages/gamut/src/Tip/shared/InlineTip.tsx | 25 ++++++++++++++++++- packages/gamut/src/Tip/shared/types.tsx | 1 + 5 files changed, 51 insertions(+), 4 deletions(-) diff --git a/packages/gamut/src/Button/IconButton.tsx b/packages/gamut/src/Button/IconButton.tsx index fc5301765ff..b85be7e04d1 100644 --- a/packages/gamut/src/Button/IconButton.tsx +++ b/packages/gamut/src/Button/IconButton.tsx @@ -40,7 +40,7 @@ export const IconButton = forwardRef( const iconSize = iconSizeMapping[buttonSize]; return ( - + = ({ diff --git a/packages/gamut/src/Tip/shared/FloatingTip.tsx b/packages/gamut/src/Tip/shared/FloatingTip.tsx index c3013661549..d65ba03cdc6 100644 --- a/packages/gamut/src/Tip/shared/FloatingTip.tsx +++ b/packages/gamut/src/Tip/shared/FloatingTip.tsx @@ -24,6 +24,7 @@ export const FloatingTip: React.FC = ({ alignment, avatar, children, + closeOnClick, escapeKeyPressHandler, inheritDims, info, @@ -122,6 +123,21 @@ export const FloatingTip: React.FC = ({ ? (e: FocusOrMouseEvent) => handleShowHideAction(e) : undefined; + const handleClick = useCallback(() => { + if (hoverDelayRef.current) { + clearTimeout(hoverDelayRef.current); + hoverDelayRef.current = undefined; + } + if (focusDelayRef.current) { + clearTimeout(focusDelayRef.current); + focusDelayRef.current = undefined; + } + setIsOpen(false); + setIsFocused(false); + }, []); + + const clickHandler = closeOnClick && isHoverType ? handleClick : undefined; + const contents = isPreviewType ? ( = ({ ref={ref} width={inheritDims ? 'inherit' : undefined} onBlur={toolOnlyEventFunc} + onClick={clickHandler} onFocus={toolOnlyEventFunc} onKeyDown={escapeKeyPressHandler} onMouseDown={(e) => e.preventDefault()} diff --git a/packages/gamut/src/Tip/shared/InlineTip.tsx b/packages/gamut/src/Tip/shared/InlineTip.tsx index 871cfaac1ac..7e45a61ec94 100644 --- a/packages/gamut/src/Tip/shared/InlineTip.tsx +++ b/packages/gamut/src/Tip/shared/InlineTip.tsx @@ -1,3 +1,5 @@ +import { useCallback, useState } from 'react'; + import { InfoTipContainer } from '../InfoTip/styles'; import { PreviewTipContents, PreviewTipShadow } from '../PreviewTip/elements'; import { ToolTipContainer } from '../ToolTip/elements'; @@ -15,6 +17,7 @@ export const InlineTip: React.FC = ({ alignment, avatar, children, + closeOnClick, escapeKeyPressHandler, id, inheritDims, @@ -31,6 +34,15 @@ export const InlineTip: React.FC = ({ zIndex, }) => { const isHoverType = type === 'tool' || type === 'preview'; + const [isSuppressed, setIsSuppressed] = useState(false); + + const handleClick = useCallback(() => { + if (closeOnClick) setIsSuppressed(true); + }, [closeOnClick]); + + const handleBlur = useCallback(() => setIsSuppressed(false), []); + + const handleMouseLeave = useCallback(() => setIsSuppressed(false), []); const InlineTipWrapper = isHoverType ? ToolTipWrapper : InfoTipWrapper; const InlineTipBodyWrapper = isHoverType @@ -41,11 +53,18 @@ export const InlineTip: React.FC = ({ const tipBodyAlignment = getAlignmentStyles({ alignment, avatar, type }); const isHorizontalCenter = tipBodyAlignment === 'horizontalCenter'; + const suppressedBodyStyle = + isHoverType && isSuppressed + ? { opacity: 0, visibility: 'hidden' as const, transition: 'none' } + : undefined; + const target = ( {children} @@ -57,6 +76,7 @@ export const InlineTip: React.FC = ({ alignment={alignment} zIndex={zIndex ?? 1} {...inlineWrapperProps} + style={suppressedBodyStyle} > = ({ ); return ( - + {alignment.includes('top') ? ( <> {tipBody} diff --git a/packages/gamut/src/Tip/shared/types.tsx b/packages/gamut/src/Tip/shared/types.tsx index 164ed1aaf37..689fd71debf 100644 --- a/packages/gamut/src/Tip/shared/types.tsx +++ b/packages/gamut/src/Tip/shared/types.tsx @@ -81,6 +81,7 @@ export type TipPlacementComponentProps = Omit< contentRef?: | React.RefObject | ((node: HTMLDivElement | null) => void); + closeOnClick?: boolean; type: 'info' | 'tool' | 'preview'; wrapperRef?: React.RefObject; zIndex?: number; From 9b59cc5ebc00480a850b185bd5d9f6b1ed6b3bba Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 29 Jun 2026 10:02:06 -0400 Subject: [PATCH 02/42] removed zIndex from tip props --- packages/gamut/src/Tip/ToolTip/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/gamut/src/Tip/ToolTip/index.tsx b/packages/gamut/src/Tip/ToolTip/index.tsx index ce8a2652599..f8662a43f8c 100644 --- a/packages/gamut/src/Tip/ToolTip/index.tsx +++ b/packages/gamut/src/Tip/ToolTip/index.tsx @@ -22,7 +22,6 @@ export type ToolTipProps = TipNewBaseProps & * Can be used for accessibility - the same id needs to be passed to the `aria-describedby` attribute of the element that the tooltip is describing. */ id?: string; - zIndex?: number; }; export const ToolTip: React.FC = ({ From 145c2c66135684a481e99b1fd567f9c796edda39 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 29 Jun 2026 11:27:14 -0400 Subject: [PATCH 03/42] InlineToolTip now a state instead of style, added testing for onClick --- .../src/Button/__tests__/IconButton.test.tsx | 30 +++++++++++++++++++ packages/gamut/src/Tip/shared/InlineTip.tsx | 10 ++----- packages/gamut/src/Tip/shared/elements.tsx | 17 +++++++++-- .../Buttons/IconButton/IconButton.stories.tsx | 8 +++++ 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/packages/gamut/src/Button/__tests__/IconButton.test.tsx b/packages/gamut/src/Button/__tests__/IconButton.test.tsx index 5f9adefd72c..bd849529c2e 100644 --- a/packages/gamut/src/Button/__tests__/IconButton.test.tsx +++ b/packages/gamut/src/Button/__tests__/IconButton.test.tsx @@ -70,4 +70,34 @@ describe('IconButton', () => { await waitFor(() => expect(view.queryAllByText(tip).length).toBe(2)); }); + + describe('closeOnClick', () => { + beforeEach(() => { + onClick.mockClear(); + }); + + it('does not prevent button onClick when the inline tip click handler fires', async () => { + const { view } = renderView(); + + await userEvent.click(view.getByRole('button', { name: label })); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('does not prevent button onClick when the floating tip click handler fires', async () => { + const { view } = renderFloatingView(); + + await userEvent.click(view.getByRole('button', { name: label })); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('does not prevent button onClick when closeOnClick is disabled', async () => { + const { view } = renderView({ tipProps: { closeOnClick: false } }); + + await userEvent.click(view.getByRole('button', { name: label })); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/gamut/src/Tip/shared/InlineTip.tsx b/packages/gamut/src/Tip/shared/InlineTip.tsx index 7e45a61ec94..6b77d629c68 100644 --- a/packages/gamut/src/Tip/shared/InlineTip.tsx +++ b/packages/gamut/src/Tip/shared/InlineTip.tsx @@ -49,15 +49,12 @@ export const InlineTip: React.FC = ({ ? ToolTipContainer : InfoTipContainer; const inlineWrapperProps = isHoverType ? {} : { hideTip: isTipHidden }; - const tipWrapperProps = isHoverType ? ({ inheritDims } as const) : {}; + const tipWrapperProps = isHoverType + ? { inheritDims, suppress: isSuppressed } + : {}; const tipBodyAlignment = getAlignmentStyles({ alignment, avatar, type }); const isHorizontalCenter = tipBodyAlignment === 'horizontalCenter'; - const suppressedBodyStyle = - isHoverType && isSuppressed - ? { opacity: 0, visibility: 'hidden' as const, transition: 'none' } - : undefined; - const target = ( = ({ alignment={alignment} zIndex={zIndex ?? 1} {...inlineWrapperProps} - style={suppressedBodyStyle} > div, &:focus-within > div': { + opacity: 0, + visibility: 'hidden', + transition: 'none', + }, + }, +}); + export const FloatingTipTextWrapper = styled(FlexBox)< StyleProps >( @@ -43,7 +53,9 @@ export const FloatingTipTextWrapper = styled(FlexBox)< floatingTipTextStates ); -export const ToolTipWrapper = styled.div>( +export const ToolTipWrapper = styled.div< + StyleProps & StyleProps +>( css({ '&:hover > div, &:focus-within > div': { opacity: 1, @@ -52,7 +64,8 @@ export const ToolTipWrapper = styled.div>( }, ...tipWrapperStyles, }), - inlineTipStates + inlineTipStates, + toolTipWrapperStates ); export const InfoTipWrapper = styled.div( diff --git a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx index 9c9f8b41612..3fbd32df8ff 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx +++ b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx @@ -48,3 +48,11 @@ type Story = StoryObj; export const Default: Story = { args: {}, }; + +export const CloseOnClick: Story = { + args: { + tip: 'Tooltip closes on click', + // eslint-disable-next-line no-console + onClick: () => console.log('button onClick fired'), + }, +}; From 7a201de5754d03f98a1dc7d8e788740170183c8b Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 29 Jun 2026 15:58:40 -0400 Subject: [PATCH 04/42] fix various bugs --- packages/gamut/src/Tip/shared/InlineTip.tsx | 21 +++++++++++++++----- packages/gamut/src/Tip/shared/elements.tsx | 22 +++++++++++---------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/gamut/src/Tip/shared/InlineTip.tsx b/packages/gamut/src/Tip/shared/InlineTip.tsx index 6b77d629c68..48304ba9e43 100644 --- a/packages/gamut/src/Tip/shared/InlineTip.tsx +++ b/packages/gamut/src/Tip/shared/InlineTip.tsx @@ -40,15 +40,25 @@ export const InlineTip: React.FC = ({ if (closeOnClick) setIsSuppressed(true); }, [closeOnClick]); - const handleBlur = useCallback(() => setIsSuppressed(false), []); + const handleUnsuppress = useCallback(() => setIsSuppressed(false), []); - const handleMouseLeave = useCallback(() => setIsSuppressed(false), []); + // Skip synthetic enter/leave fired when a child changes visibility (relatedTarget stays inside the wrapper). + const handleMouseEnterAndLeave = useCallback( + (e: React.MouseEvent) => { + const related = e.relatedTarget as Node | null; + if (related && e.currentTarget.contains(related)) return; + setIsSuppressed(false); + }, + [] + ); const InlineTipWrapper = isHoverType ? ToolTipWrapper : InfoTipWrapper; const InlineTipBodyWrapper = isHoverType ? ToolTipContainer : InfoTipContainer; - const inlineWrapperProps = isHoverType ? {} : { hideTip: isTipHidden }; + const inlineWrapperProps = isHoverType + ? { 'data-tooltip-body': '' } + : { hideTip: isTipHidden }; const tipWrapperProps = isHoverType ? { inheritDims, suppress: isSuppressed } : {}; @@ -60,7 +70,7 @@ export const InlineTip: React.FC = ({ height={inheritDims ? 'inherit' : undefined} ref={wrapperRef} width={inheritDims ? 'inherit' : undefined} - onBlur={isHoverType ? handleBlur : undefined} + onBlur={isHoverType ? handleUnsuppress : undefined} onClick={isHoverType ? handleClick : undefined} onKeyDown={escapeKeyPressHandler} > @@ -108,7 +118,8 @@ export const InlineTip: React.FC = ({ return ( {alignment.includes('top') ? ( <> diff --git a/packages/gamut/src/Tip/shared/elements.tsx b/packages/gamut/src/Tip/shared/elements.tsx index c42fcb67c5d..c5f92b2f210 100644 --- a/packages/gamut/src/Tip/shared/elements.tsx +++ b/packages/gamut/src/Tip/shared/elements.tsx @@ -35,11 +35,12 @@ const inlineTipStates = states({ const toolTipWrapperStates = states({ suppress: { - '&:hover > div, &:focus-within > div': { - opacity: 0, - visibility: 'hidden', - transition: 'none', - }, + '&:hover > [data-tooltip-body], &:has(:focus-visible) > [data-tooltip-body]': + { + opacity: 0, + visibility: 'hidden', + transition: 'none', + }, }, }); @@ -57,11 +58,12 @@ export const ToolTipWrapper = styled.div< StyleProps & StyleProps >( css({ - '&:hover > div, &:focus-within > div': { - opacity: 1, - transition: `opacity ${timing.fast} ${timing.base}`, - visibility: 'visible', - }, + '&:hover > [data-tooltip-body], &:has(:focus-visible) > [data-tooltip-body]': + { + opacity: 1, + transition: `opacity ${timing.fast} ${timing.base}`, + visibility: 'visible', + }, ...tipWrapperStyles, }), inlineTipStates, From 8de8bd9550563ec26ad638042f38c4a53df85901 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 29 Jun 2026 16:59:29 -0400 Subject: [PATCH 05/42] added stories --- .../Atoms/Buttons/IconButton/IconButton.mdx | 6 +++ .../Buttons/IconButton/IconButton.stories.tsx | 47 ++++++++++++++++--- .../lib/Molecules/Tips/ToolTip/ToolTip.mdx | 6 +++ .../Tips/ToolTip/ToolTip.stories.tsx | 43 +++++++++++++++++ 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.mdx b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.mdx index 6c21691cfcc..6f2afb1e2a5 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.mdx +++ b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.mdx @@ -31,6 +31,12 @@ Use for secondary or space-constrained actions with recognizable icons. Use regu For more detailed information on `IconButton` tooltips, take a look at the ToolTip story. +## Close on Click + +By default, `IconButton` tooltips close immediately on click and reappear only after the cursor leaves and re-enters. Pass `tipProps={{ closeOnClick: false }}` to opt out (e.g. copy → copied patterns). The first two buttons close on click; the last two stay open. + + + ## Playground diff --git a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx index 3fbd32df8ff..63b5cf62ba4 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx +++ b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx @@ -1,4 +1,5 @@ -import { IconButton } from '@codecademy/gamut'; +import { FlexBox, IconButton } from '@codecademy/gamut'; +import { SparkleIcon } from '@codecademy/gamut-icons'; import * as icons from '@codecademy/gamut-icons'; import type { Meta, StoryObj } from '@storybook/react'; import type { TypeWithDeepControls } from 'storybook-addon-deep-controls'; @@ -49,10 +50,44 @@ export const Default: Story = { args: {}, }; +// eslint-disable-next-line no-console +const logClick = () => console.log('button onClick fired'); + export const CloseOnClick: Story = { - args: { - tip: 'Tooltip closes on click', - // eslint-disable-next-line no-console - onClick: () => console.log('button onClick fired'), - }, + render: () => ( + + + + + + + ), }; diff --git a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.mdx b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.mdx index 14f834ff785..684eda388b7 100644 --- a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.mdx +++ b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.mdx @@ -69,6 +69,12 @@ When a Button is disabled with a tooltip, you must use the `aria-disabled` prop +### Close on click + +By default, `ToolTip` closes on click and reappears only after the cursor leaves and re-enters. Pass `closeOnClick={false}` to opt out (e.g. copy → copied patterns). The first two buttons close on click; the last two stay open. + + + ## Playground diff --git a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx index f08cd8c55b4..f1e388c111b 100644 --- a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx +++ b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx @@ -154,6 +154,49 @@ export const Disabled: Story = { ), }; +export const CloseOnClick: Story = { + render: () => ( + + + Floating + + + Inline + + + + Floating (stays open) + + + + + Inline (stays open) + + + + ), +}; + export const HorizontalAlignments: Story = { render: () => ( <> From 8e127a3af6c8cbe0f3978cf07fe4d36f52cc9cd2 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Tue, 30 Jun 2026 10:28:17 -0400 Subject: [PATCH 06/42] fix up errors --- packages/gamut/src/Button/IconButton.tsx | 2 +- packages/gamut/src/Tip/shared/InlineTip.tsx | 4 ++-- .../src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/gamut/src/Button/IconButton.tsx b/packages/gamut/src/Button/IconButton.tsx index b85be7e04d1..dd3f1a7d741 100644 --- a/packages/gamut/src/Button/IconButton.tsx +++ b/packages/gamut/src/Button/IconButton.tsx @@ -40,7 +40,7 @@ export const IconButton = forwardRef( const iconSize = iconSizeMapping[buttonSize]; return ( - + = ({ // Skip synthetic enter/leave fired when a child changes visibility (relatedTarget stays inside the wrapper). const handleMouseEnterAndLeave = useCallback( (e: React.MouseEvent) => { - const related = e.relatedTarget as Node | null; - if (related && e.currentTarget.contains(related)) return; + const related = e.relatedTarget; + if (related instanceof Node && e.currentTarget.contains(related)) return; setIsSuppressed(false); }, [] diff --git a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx index f1e388c111b..3d0fa365822 100644 --- a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx +++ b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx @@ -158,36 +158,36 @@ export const CloseOnClick: Story = { render: () => ( Floating Inline Floating (stays open) Inline (stays open) From 8f21e735c34bc1db70fb970090f41ba85705b1ba Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Tue, 30 Jun 2026 16:48:28 -0400 Subject: [PATCH 07/42] update MenuItem tooltip --- packages/gamut/src/Menu/MenuItem.tsx | 7 ++++++- packages/gamut/src/Menu/elements.tsx | 9 +++++++-- .../styleguide/src/lib/Molecules/Menu/Menu.stories.tsx | 10 +++++++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/gamut/src/Menu/MenuItem.tsx b/packages/gamut/src/Menu/MenuItem.tsx index c08251a7f0a..2de1daca73c 100644 --- a/packages/gamut/src/Menu/MenuItem.tsx +++ b/packages/gamut/src/Menu/MenuItem.tsx @@ -76,6 +76,7 @@ export const MenuItem = forwardRef< target, width = 1, 'aria-label': explicitAriaLabel, + closeOnClick = true, ...props }, ref @@ -160,7 +161,11 @@ export const MenuItem = forwardRef< return ( - + , 'children' | 'label'> & { tipId: string; + closeOnClick?: boolean; } -> = ({ children, label, tipId }) => { +> = ({ children, label, tipId, closeOnClick }) => { if (!label) { return <>{children}; } @@ -267,5 +268,9 @@ export const MenuToolTipWrapper: React.FC< ...defaultTipProps, }; - return {children}; + return ( + + {children} + + ); }; diff --git a/packages/styleguide/src/lib/Molecules/Menu/Menu.stories.tsx b/packages/styleguide/src/lib/Molecules/Menu/Menu.stories.tsx index cc5a1942b24..38e94d44933 100644 --- a/packages/styleguide/src/lib/Molecules/Menu/Menu.stories.tsx +++ b/packages/styleguide/src/lib/Molecules/Menu/Menu.stories.tsx @@ -262,14 +262,18 @@ export const IconMenu: Story = { <> {}} /> {}} /> {}} /> Date: Tue, 30 Jun 2026 17:00:10 -0400 Subject: [PATCH 08/42] update MenuItem type --- packages/gamut/src/Menu/MenuItem.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/gamut/src/Menu/MenuItem.tsx b/packages/gamut/src/Menu/MenuItem.tsx index 2de1daca73c..8a74017b3ea 100644 --- a/packages/gamut/src/Menu/MenuItem.tsx +++ b/packages/gamut/src/Menu/MenuItem.tsx @@ -49,6 +49,7 @@ interface MenuItemIconOnly extends HTMLProps, ForwardListItemProps { /** ToolTips will only render for interactive items, otherwise the label will be used as a generic aria-label */ label: ToolTipLabel; disabled?: boolean; + closeOnClick?: boolean; } interface MenuTextItem extends HTMLProps, ForwardListItemProps { @@ -56,6 +57,7 @@ interface MenuTextItem extends HTMLProps, ForwardListItemProps { children: React.ReactNode; label?: ToolTipLabel; disabled?: boolean; + closeOnClick?: never; } type MenuItemTypes = MenuItemIconOnly | MenuTextItem; From 087ec9d3a9d765994c6c0ac9c67751858ae5da32 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 1 Jul 2026 11:25:14 -0400 Subject: [PATCH 09/42] updated supressed => dismissed --- packages/gamut/src/Tip/shared/InlineTip.tsx | 12 ++++++------ packages/gamut/src/Tip/shared/elements.tsx | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/gamut/src/Tip/shared/InlineTip.tsx b/packages/gamut/src/Tip/shared/InlineTip.tsx index fe527dc5a2a..51107b70c59 100644 --- a/packages/gamut/src/Tip/shared/InlineTip.tsx +++ b/packages/gamut/src/Tip/shared/InlineTip.tsx @@ -34,20 +34,20 @@ export const InlineTip: React.FC = ({ zIndex, }) => { const isHoverType = type === 'tool' || type === 'preview'; - const [isSuppressed, setIsSuppressed] = useState(false); + const [isDismissed, setIsDismissed] = useState(false); const handleClick = useCallback(() => { - if (closeOnClick) setIsSuppressed(true); + if (closeOnClick) setIsDismissed(true); }, [closeOnClick]); - const handleUnsuppress = useCallback(() => setIsSuppressed(false), []); + const handleUndismissed = useCallback(() => setIsDismissed(false), []); // Skip synthetic enter/leave fired when a child changes visibility (relatedTarget stays inside the wrapper). const handleMouseEnterAndLeave = useCallback( (e: React.MouseEvent) => { const related = e.relatedTarget; if (related instanceof Node && e.currentTarget.contains(related)) return; - setIsSuppressed(false); + setIsDismissed(false); }, [] ); @@ -60,7 +60,7 @@ export const InlineTip: React.FC = ({ ? { 'data-tooltip-body': '' } : { hideTip: isTipHidden }; const tipWrapperProps = isHoverType - ? { inheritDims, suppress: isSuppressed } + ? { inheritDims, dismissed: isDismissed } : {}; const tipBodyAlignment = getAlignmentStyles({ alignment, avatar, type }); const isHorizontalCenter = tipBodyAlignment === 'horizontalCenter'; @@ -70,7 +70,7 @@ export const InlineTip: React.FC = ({ height={inheritDims ? 'inherit' : undefined} ref={wrapperRef} width={inheritDims ? 'inherit' : undefined} - onBlur={isHoverType ? handleUnsuppress : undefined} + onBlur={isHoverType ? handleUndismissed : undefined} onClick={isHoverType ? handleClick : undefined} onKeyDown={escapeKeyPressHandler} > diff --git a/packages/gamut/src/Tip/shared/elements.tsx b/packages/gamut/src/Tip/shared/elements.tsx index c5f92b2f210..8b5ab1ef6de 100644 --- a/packages/gamut/src/Tip/shared/elements.tsx +++ b/packages/gamut/src/Tip/shared/elements.tsx @@ -34,7 +34,7 @@ const inlineTipStates = states({ }); const toolTipWrapperStates = states({ - suppress: { + dismissed: { '&:hover > [data-tooltip-body], &:has(:focus-visible) > [data-tooltip-body]': { opacity: 0, From f4b3454fb65dd1c58c65c81c809798db424abb77 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 1 Jul 2026 12:00:44 -0400 Subject: [PATCH 10/42] undoing some type changes --- packages/gamut/src/Button/IconButton.tsx | 2 +- packages/gamut/src/Tip/ToolTip/index.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/gamut/src/Button/IconButton.tsx b/packages/gamut/src/Button/IconButton.tsx index dd3f1a7d741..1e200229215 100644 --- a/packages/gamut/src/Button/IconButton.tsx +++ b/packages/gamut/src/Button/IconButton.tsx @@ -40,7 +40,7 @@ export const IconButton = forwardRef( const iconSize = iconSizeMapping[buttonSize]; return ( - + Date: Wed, 1 Jul 2026 14:01:40 -0400 Subject: [PATCH 11/42] add version plan --- .nx/version-plans/version-plan-1782928816938.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .nx/version-plans/version-plan-1782928816938.md diff --git a/.nx/version-plans/version-plan-1782928816938.md b/.nx/version-plans/version-plan-1782928816938.md new file mode 100644 index 00000000000..e81bc503819 --- /dev/null +++ b/.nx/version-plans/version-plan-1782928816938.md @@ -0,0 +1,5 @@ +--- +gamut: patch +--- + +enable dismissable of ToolTip via onClick for interactive components like IconButton, MenuItem (icon-only), and FloatingToolTip + InlineToolTip From 1da928e77b7a5b395ca39105934453c24cda14c4 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 1 Jul 2026 14:24:43 -0400 Subject: [PATCH 12/42] update publish workflow --- .github/workflows/publish-alpha.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish-alpha.yml b/.github/workflows/publish-alpha.yml index b968dd17558..e4c5830d8a6 100644 --- a/.github/workflows/publish-alpha.yml +++ b/.github/workflows/publish-alpha.yml @@ -27,6 +27,8 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 30 steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prerelease-publish with: node-auth-token: ${{ secrets.NODE_AUTH_TOKEN }} From b91cd7615f8f82eb95f3e64aefed17c8e603a3db Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 1 Jul 2026 14:38:22 -0400 Subject: [PATCH 13/42] robo patch for now --- packages/gamut/src/Tag/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gamut/src/Tag/index.tsx b/packages/gamut/src/Tag/index.tsx index 686478ff325..8d514f40478 100644 --- a/packages/gamut/src/Tag/index.tsx +++ b/packages/gamut/src/Tag/index.tsx @@ -89,7 +89,7 @@ export const Tag: React.FC = ({ {isSelection && ( Date: Wed, 1 Jul 2026 16:11:29 -0400 Subject: [PATCH 14/42] slight clean-up and added story for persisting tooltip in IconButton --- packages/gamut/src/Button/IconButton.tsx | 2 +- .../src/lib/Atoms/Buttons/IconButton/IconButton.mdx | 8 +++++++- .../Atoms/Buttons/IconButton/IconButton.stories.tsx | 12 ++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/gamut/src/Button/IconButton.tsx b/packages/gamut/src/Button/IconButton.tsx index 1e200229215..fc5301765ff 100644 --- a/packages/gamut/src/Button/IconButton.tsx +++ b/packages/gamut/src/Button/IconButton.tsx @@ -40,7 +40,7 @@ export const IconButton = forwardRef( const iconSize = iconSizeMapping[buttonSize]; return ( - + +### Persisting the tooltip + +Pass `tipProps={{ closeOnClick: false }}` to keep the tooltip visible after a click. Use this for actions where the tooltip content changes in response to the click, such as a copy → copied pattern. + + + ## Playground diff --git a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx index 63b5cf62ba4..eaf1be816ae 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx +++ b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx @@ -53,6 +53,18 @@ export const Default: Story = { // eslint-disable-next-line no-console const logClick = () => console.log('button onClick fired'); +export const PersistTooltip: Story = { + args: { + tip: 'Tooltip stays open on click', + tipProps: { + placement: 'floating', + alignment: 'top-center', + closeOnClick: false, + }, + onClick: logClick, + }, +}; + export const CloseOnClick: Story = { render: () => ( From 7380e65d7604db6f7af1322af51af763f3704740 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Fri, 26 Jun 2026 17:00:33 -0400 Subject: [PATCH 15/42] first attempt at closing tooltip after click --- packages/gamut/src/Button/IconButton.tsx | 2 +- packages/gamut/src/Tip/ToolTip/index.tsx | 10 ++++++-- packages/gamut/src/Tip/shared/FloatingTip.tsx | 17 +++++++++++++ packages/gamut/src/Tip/shared/InlineTip.tsx | 25 ++++++++++++++++++- packages/gamut/src/Tip/shared/types.tsx | 1 + 5 files changed, 51 insertions(+), 4 deletions(-) diff --git a/packages/gamut/src/Button/IconButton.tsx b/packages/gamut/src/Button/IconButton.tsx index fc5301765ff..b85be7e04d1 100644 --- a/packages/gamut/src/Button/IconButton.tsx +++ b/packages/gamut/src/Button/IconButton.tsx @@ -40,7 +40,7 @@ export const IconButton = forwardRef( const iconSize = iconSizeMapping[buttonSize]; return ( - + = ({ diff --git a/packages/gamut/src/Tip/shared/FloatingTip.tsx b/packages/gamut/src/Tip/shared/FloatingTip.tsx index c3013661549..d65ba03cdc6 100644 --- a/packages/gamut/src/Tip/shared/FloatingTip.tsx +++ b/packages/gamut/src/Tip/shared/FloatingTip.tsx @@ -24,6 +24,7 @@ export const FloatingTip: React.FC = ({ alignment, avatar, children, + closeOnClick, escapeKeyPressHandler, inheritDims, info, @@ -122,6 +123,21 @@ export const FloatingTip: React.FC = ({ ? (e: FocusOrMouseEvent) => handleShowHideAction(e) : undefined; + const handleClick = useCallback(() => { + if (hoverDelayRef.current) { + clearTimeout(hoverDelayRef.current); + hoverDelayRef.current = undefined; + } + if (focusDelayRef.current) { + clearTimeout(focusDelayRef.current); + focusDelayRef.current = undefined; + } + setIsOpen(false); + setIsFocused(false); + }, []); + + const clickHandler = closeOnClick && isHoverType ? handleClick : undefined; + const contents = isPreviewType ? ( = ({ ref={ref} width={inheritDims ? 'inherit' : undefined} onBlur={toolOnlyEventFunc} + onClick={clickHandler} onFocus={toolOnlyEventFunc} onKeyDown={escapeKeyPressHandler} onMouseDown={(e) => e.preventDefault()} diff --git a/packages/gamut/src/Tip/shared/InlineTip.tsx b/packages/gamut/src/Tip/shared/InlineTip.tsx index 871cfaac1ac..7e45a61ec94 100644 --- a/packages/gamut/src/Tip/shared/InlineTip.tsx +++ b/packages/gamut/src/Tip/shared/InlineTip.tsx @@ -1,3 +1,5 @@ +import { useCallback, useState } from 'react'; + import { InfoTipContainer } from '../InfoTip/styles'; import { PreviewTipContents, PreviewTipShadow } from '../PreviewTip/elements'; import { ToolTipContainer } from '../ToolTip/elements'; @@ -15,6 +17,7 @@ export const InlineTip: React.FC = ({ alignment, avatar, children, + closeOnClick, escapeKeyPressHandler, id, inheritDims, @@ -31,6 +34,15 @@ export const InlineTip: React.FC = ({ zIndex, }) => { const isHoverType = type === 'tool' || type === 'preview'; + const [isSuppressed, setIsSuppressed] = useState(false); + + const handleClick = useCallback(() => { + if (closeOnClick) setIsSuppressed(true); + }, [closeOnClick]); + + const handleBlur = useCallback(() => setIsSuppressed(false), []); + + const handleMouseLeave = useCallback(() => setIsSuppressed(false), []); const InlineTipWrapper = isHoverType ? ToolTipWrapper : InfoTipWrapper; const InlineTipBodyWrapper = isHoverType @@ -41,11 +53,18 @@ export const InlineTip: React.FC = ({ const tipBodyAlignment = getAlignmentStyles({ alignment, avatar, type }); const isHorizontalCenter = tipBodyAlignment === 'horizontalCenter'; + const suppressedBodyStyle = + isHoverType && isSuppressed + ? { opacity: 0, visibility: 'hidden' as const, transition: 'none' } + : undefined; + const target = ( {children} @@ -57,6 +76,7 @@ export const InlineTip: React.FC = ({ alignment={alignment} zIndex={zIndex ?? 1} {...inlineWrapperProps} + style={suppressedBodyStyle} > = ({ ); return ( - + {alignment.includes('top') ? ( <> {tipBody} diff --git a/packages/gamut/src/Tip/shared/types.tsx b/packages/gamut/src/Tip/shared/types.tsx index 164ed1aaf37..689fd71debf 100644 --- a/packages/gamut/src/Tip/shared/types.tsx +++ b/packages/gamut/src/Tip/shared/types.tsx @@ -81,6 +81,7 @@ export type TipPlacementComponentProps = Omit< contentRef?: | React.RefObject | ((node: HTMLDivElement | null) => void); + closeOnClick?: boolean; type: 'info' | 'tool' | 'preview'; wrapperRef?: React.RefObject; zIndex?: number; From 4656e6efd6b2d1f6a2ed4b22e5b77f86be496ad1 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 29 Jun 2026 10:02:06 -0400 Subject: [PATCH 16/42] removed zIndex from tip props --- packages/gamut/src/Tip/ToolTip/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/gamut/src/Tip/ToolTip/index.tsx b/packages/gamut/src/Tip/ToolTip/index.tsx index ce8a2652599..f8662a43f8c 100644 --- a/packages/gamut/src/Tip/ToolTip/index.tsx +++ b/packages/gamut/src/Tip/ToolTip/index.tsx @@ -22,7 +22,6 @@ export type ToolTipProps = TipNewBaseProps & * Can be used for accessibility - the same id needs to be passed to the `aria-describedby` attribute of the element that the tooltip is describing. */ id?: string; - zIndex?: number; }; export const ToolTip: React.FC = ({ From 22d8ca6fd5e8f4770520283a9badaee640989b1c Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 29 Jun 2026 11:27:14 -0400 Subject: [PATCH 17/42] InlineToolTip now a state instead of style, added testing for onClick --- .../src/Button/__tests__/IconButton.test.tsx | 30 +++++++++++++++++++ packages/gamut/src/Tip/shared/InlineTip.tsx | 10 ++----- packages/gamut/src/Tip/shared/elements.tsx | 17 +++++++++-- .../Buttons/IconButton/IconButton.stories.tsx | 8 +++++ 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/packages/gamut/src/Button/__tests__/IconButton.test.tsx b/packages/gamut/src/Button/__tests__/IconButton.test.tsx index 5f9adefd72c..bd849529c2e 100644 --- a/packages/gamut/src/Button/__tests__/IconButton.test.tsx +++ b/packages/gamut/src/Button/__tests__/IconButton.test.tsx @@ -70,4 +70,34 @@ describe('IconButton', () => { await waitFor(() => expect(view.queryAllByText(tip).length).toBe(2)); }); + + describe('closeOnClick', () => { + beforeEach(() => { + onClick.mockClear(); + }); + + it('does not prevent button onClick when the inline tip click handler fires', async () => { + const { view } = renderView(); + + await userEvent.click(view.getByRole('button', { name: label })); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('does not prevent button onClick when the floating tip click handler fires', async () => { + const { view } = renderFloatingView(); + + await userEvent.click(view.getByRole('button', { name: label })); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('does not prevent button onClick when closeOnClick is disabled', async () => { + const { view } = renderView({ tipProps: { closeOnClick: false } }); + + await userEvent.click(view.getByRole('button', { name: label })); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/gamut/src/Tip/shared/InlineTip.tsx b/packages/gamut/src/Tip/shared/InlineTip.tsx index 7e45a61ec94..6b77d629c68 100644 --- a/packages/gamut/src/Tip/shared/InlineTip.tsx +++ b/packages/gamut/src/Tip/shared/InlineTip.tsx @@ -49,15 +49,12 @@ export const InlineTip: React.FC = ({ ? ToolTipContainer : InfoTipContainer; const inlineWrapperProps = isHoverType ? {} : { hideTip: isTipHidden }; - const tipWrapperProps = isHoverType ? ({ inheritDims } as const) : {}; + const tipWrapperProps = isHoverType + ? { inheritDims, suppress: isSuppressed } + : {}; const tipBodyAlignment = getAlignmentStyles({ alignment, avatar, type }); const isHorizontalCenter = tipBodyAlignment === 'horizontalCenter'; - const suppressedBodyStyle = - isHoverType && isSuppressed - ? { opacity: 0, visibility: 'hidden' as const, transition: 'none' } - : undefined; - const target = ( = ({ alignment={alignment} zIndex={zIndex ?? 1} {...inlineWrapperProps} - style={suppressedBodyStyle} > div, &:focus-within > div': { + opacity: 0, + visibility: 'hidden', + transition: 'none', + }, + }, +}); + export const FloatingTipTextWrapper = styled(FlexBox)< StyleProps >( @@ -43,7 +53,9 @@ export const FloatingTipTextWrapper = styled(FlexBox)< floatingTipTextStates ); -export const ToolTipWrapper = styled.div>( +export const ToolTipWrapper = styled.div< + StyleProps & StyleProps +>( css({ '&:hover > div, &:focus-within > div': { opacity: 1, @@ -52,7 +64,8 @@ export const ToolTipWrapper = styled.div>( }, ...tipWrapperStyles, }), - inlineTipStates + inlineTipStates, + toolTipWrapperStates ); export const InfoTipWrapper = styled.div( diff --git a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx index 9c9f8b41612..3fbd32df8ff 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx +++ b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx @@ -48,3 +48,11 @@ type Story = StoryObj; export const Default: Story = { args: {}, }; + +export const CloseOnClick: Story = { + args: { + tip: 'Tooltip closes on click', + // eslint-disable-next-line no-console + onClick: () => console.log('button onClick fired'), + }, +}; From 3f3ebc0087fd1c20936e7496cf33885da90c5249 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 29 Jun 2026 15:58:40 -0400 Subject: [PATCH 18/42] fix various bugs --- packages/gamut/src/Tip/shared/InlineTip.tsx | 21 +++++++++++++++----- packages/gamut/src/Tip/shared/elements.tsx | 22 +++++++++++---------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/gamut/src/Tip/shared/InlineTip.tsx b/packages/gamut/src/Tip/shared/InlineTip.tsx index 6b77d629c68..48304ba9e43 100644 --- a/packages/gamut/src/Tip/shared/InlineTip.tsx +++ b/packages/gamut/src/Tip/shared/InlineTip.tsx @@ -40,15 +40,25 @@ export const InlineTip: React.FC = ({ if (closeOnClick) setIsSuppressed(true); }, [closeOnClick]); - const handleBlur = useCallback(() => setIsSuppressed(false), []); + const handleUnsuppress = useCallback(() => setIsSuppressed(false), []); - const handleMouseLeave = useCallback(() => setIsSuppressed(false), []); + // Skip synthetic enter/leave fired when a child changes visibility (relatedTarget stays inside the wrapper). + const handleMouseEnterAndLeave = useCallback( + (e: React.MouseEvent) => { + const related = e.relatedTarget as Node | null; + if (related && e.currentTarget.contains(related)) return; + setIsSuppressed(false); + }, + [] + ); const InlineTipWrapper = isHoverType ? ToolTipWrapper : InfoTipWrapper; const InlineTipBodyWrapper = isHoverType ? ToolTipContainer : InfoTipContainer; - const inlineWrapperProps = isHoverType ? {} : { hideTip: isTipHidden }; + const inlineWrapperProps = isHoverType + ? { 'data-tooltip-body': '' } + : { hideTip: isTipHidden }; const tipWrapperProps = isHoverType ? { inheritDims, suppress: isSuppressed } : {}; @@ -60,7 +70,7 @@ export const InlineTip: React.FC = ({ height={inheritDims ? 'inherit' : undefined} ref={wrapperRef} width={inheritDims ? 'inherit' : undefined} - onBlur={isHoverType ? handleBlur : undefined} + onBlur={isHoverType ? handleUnsuppress : undefined} onClick={isHoverType ? handleClick : undefined} onKeyDown={escapeKeyPressHandler} > @@ -108,7 +118,8 @@ export const InlineTip: React.FC = ({ return ( {alignment.includes('top') ? ( <> diff --git a/packages/gamut/src/Tip/shared/elements.tsx b/packages/gamut/src/Tip/shared/elements.tsx index c42fcb67c5d..c5f92b2f210 100644 --- a/packages/gamut/src/Tip/shared/elements.tsx +++ b/packages/gamut/src/Tip/shared/elements.tsx @@ -35,11 +35,12 @@ const inlineTipStates = states({ const toolTipWrapperStates = states({ suppress: { - '&:hover > div, &:focus-within > div': { - opacity: 0, - visibility: 'hidden', - transition: 'none', - }, + '&:hover > [data-tooltip-body], &:has(:focus-visible) > [data-tooltip-body]': + { + opacity: 0, + visibility: 'hidden', + transition: 'none', + }, }, }); @@ -57,11 +58,12 @@ export const ToolTipWrapper = styled.div< StyleProps & StyleProps >( css({ - '&:hover > div, &:focus-within > div': { - opacity: 1, - transition: `opacity ${timing.fast} ${timing.base}`, - visibility: 'visible', - }, + '&:hover > [data-tooltip-body], &:has(:focus-visible) > [data-tooltip-body]': + { + opacity: 1, + transition: `opacity ${timing.fast} ${timing.base}`, + visibility: 'visible', + }, ...tipWrapperStyles, }), inlineTipStates, From 560c34a1e0a36d87c780ed215a0d10fcfb9d773e Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 29 Jun 2026 16:59:29 -0400 Subject: [PATCH 19/42] added stories --- .../Atoms/Buttons/IconButton/IconButton.mdx | 6 +++ .../Buttons/IconButton/IconButton.stories.tsx | 47 ++++++++++++++++--- .../lib/Molecules/Tips/ToolTip/ToolTip.mdx | 6 +++ .../Tips/ToolTip/ToolTip.stories.tsx | 43 +++++++++++++++++ 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.mdx b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.mdx index 6c21691cfcc..6f2afb1e2a5 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.mdx +++ b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.mdx @@ -31,6 +31,12 @@ Use for secondary or space-constrained actions with recognizable icons. Use regu For more detailed information on `IconButton` tooltips, take a look at the ToolTip story. +## Close on Click + +By default, `IconButton` tooltips close immediately on click and reappear only after the cursor leaves and re-enters. Pass `tipProps={{ closeOnClick: false }}` to opt out (e.g. copy → copied patterns). The first two buttons close on click; the last two stay open. + + + ## Playground diff --git a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx index 3fbd32df8ff..63b5cf62ba4 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx +++ b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx @@ -1,4 +1,5 @@ -import { IconButton } from '@codecademy/gamut'; +import { FlexBox, IconButton } from '@codecademy/gamut'; +import { SparkleIcon } from '@codecademy/gamut-icons'; import * as icons from '@codecademy/gamut-icons'; import type { Meta, StoryObj } from '@storybook/react'; import type { TypeWithDeepControls } from 'storybook-addon-deep-controls'; @@ -49,10 +50,44 @@ export const Default: Story = { args: {}, }; +// eslint-disable-next-line no-console +const logClick = () => console.log('button onClick fired'); + export const CloseOnClick: Story = { - args: { - tip: 'Tooltip closes on click', - // eslint-disable-next-line no-console - onClick: () => console.log('button onClick fired'), - }, + render: () => ( + + + + + + + ), }; diff --git a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.mdx b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.mdx index 14f834ff785..684eda388b7 100644 --- a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.mdx +++ b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.mdx @@ -69,6 +69,12 @@ When a Button is disabled with a tooltip, you must use the `aria-disabled` prop +### Close on click + +By default, `ToolTip` closes on click and reappears only after the cursor leaves and re-enters. Pass `closeOnClick={false}` to opt out (e.g. copy → copied patterns). The first two buttons close on click; the last two stay open. + + + ## Playground diff --git a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx index f08cd8c55b4..f1e388c111b 100644 --- a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx +++ b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx @@ -154,6 +154,49 @@ export const Disabled: Story = { ), }; +export const CloseOnClick: Story = { + render: () => ( + + + Floating + + + Inline + + + + Floating (stays open) + + + + + Inline (stays open) + + + + ), +}; + export const HorizontalAlignments: Story = { render: () => ( <> From 4d036d24f3e14f9b830375e6daf398732aec51ca Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Tue, 30 Jun 2026 10:28:17 -0400 Subject: [PATCH 20/42] fix up errors --- packages/gamut/src/Button/IconButton.tsx | 2 +- packages/gamut/src/Tip/shared/InlineTip.tsx | 4 ++-- .../src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/gamut/src/Button/IconButton.tsx b/packages/gamut/src/Button/IconButton.tsx index b85be7e04d1..dd3f1a7d741 100644 --- a/packages/gamut/src/Button/IconButton.tsx +++ b/packages/gamut/src/Button/IconButton.tsx @@ -40,7 +40,7 @@ export const IconButton = forwardRef( const iconSize = iconSizeMapping[buttonSize]; return ( - + = ({ // Skip synthetic enter/leave fired when a child changes visibility (relatedTarget stays inside the wrapper). const handleMouseEnterAndLeave = useCallback( (e: React.MouseEvent) => { - const related = e.relatedTarget as Node | null; - if (related && e.currentTarget.contains(related)) return; + const related = e.relatedTarget; + if (related instanceof Node && e.currentTarget.contains(related)) return; setIsSuppressed(false); }, [] diff --git a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx index f1e388c111b..3d0fa365822 100644 --- a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx +++ b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx @@ -158,36 +158,36 @@ export const CloseOnClick: Story = { render: () => ( Floating Inline Floating (stays open) Inline (stays open) From 1c5e743db15520dce57f95d37cb9b3c429b18ef3 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Tue, 30 Jun 2026 16:48:28 -0400 Subject: [PATCH 21/42] update MenuItem tooltip --- packages/gamut/src/Menu/MenuItem.tsx | 7 ++++++- packages/gamut/src/Menu/elements.tsx | 9 +++++++-- .../styleguide/src/lib/Molecules/Menu/Menu.stories.tsx | 10 +++++++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/gamut/src/Menu/MenuItem.tsx b/packages/gamut/src/Menu/MenuItem.tsx index c08251a7f0a..2de1daca73c 100644 --- a/packages/gamut/src/Menu/MenuItem.tsx +++ b/packages/gamut/src/Menu/MenuItem.tsx @@ -76,6 +76,7 @@ export const MenuItem = forwardRef< target, width = 1, 'aria-label': explicitAriaLabel, + closeOnClick = true, ...props }, ref @@ -160,7 +161,11 @@ export const MenuItem = forwardRef< return ( - + , 'children' | 'label'> & { tipId: string; + closeOnClick?: boolean; } -> = ({ children, label, tipId }) => { +> = ({ children, label, tipId, closeOnClick }) => { if (!label) { return <>{children}; } @@ -267,5 +268,9 @@ export const MenuToolTipWrapper: React.FC< ...defaultTipProps, }; - return {children}; + return ( + + {children} + + ); }; diff --git a/packages/styleguide/src/lib/Molecules/Menu/Menu.stories.tsx b/packages/styleguide/src/lib/Molecules/Menu/Menu.stories.tsx index cc5a1942b24..38e94d44933 100644 --- a/packages/styleguide/src/lib/Molecules/Menu/Menu.stories.tsx +++ b/packages/styleguide/src/lib/Molecules/Menu/Menu.stories.tsx @@ -262,14 +262,18 @@ export const IconMenu: Story = { <> {}} /> {}} /> {}} /> Date: Tue, 30 Jun 2026 17:00:10 -0400 Subject: [PATCH 22/42] update MenuItem type --- packages/gamut/src/Menu/MenuItem.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/gamut/src/Menu/MenuItem.tsx b/packages/gamut/src/Menu/MenuItem.tsx index 2de1daca73c..8a74017b3ea 100644 --- a/packages/gamut/src/Menu/MenuItem.tsx +++ b/packages/gamut/src/Menu/MenuItem.tsx @@ -49,6 +49,7 @@ interface MenuItemIconOnly extends HTMLProps, ForwardListItemProps { /** ToolTips will only render for interactive items, otherwise the label will be used as a generic aria-label */ label: ToolTipLabel; disabled?: boolean; + closeOnClick?: boolean; } interface MenuTextItem extends HTMLProps, ForwardListItemProps { @@ -56,6 +57,7 @@ interface MenuTextItem extends HTMLProps, ForwardListItemProps { children: React.ReactNode; label?: ToolTipLabel; disabled?: boolean; + closeOnClick?: never; } type MenuItemTypes = MenuItemIconOnly | MenuTextItem; From 02e1f2a1c0ffc75a30c231d7bf6810c50f42e112 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 1 Jul 2026 11:25:14 -0400 Subject: [PATCH 23/42] updated supressed => dismissed --- packages/gamut/src/Tip/shared/InlineTip.tsx | 12 ++++++------ packages/gamut/src/Tip/shared/elements.tsx | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/gamut/src/Tip/shared/InlineTip.tsx b/packages/gamut/src/Tip/shared/InlineTip.tsx index fe527dc5a2a..51107b70c59 100644 --- a/packages/gamut/src/Tip/shared/InlineTip.tsx +++ b/packages/gamut/src/Tip/shared/InlineTip.tsx @@ -34,20 +34,20 @@ export const InlineTip: React.FC = ({ zIndex, }) => { const isHoverType = type === 'tool' || type === 'preview'; - const [isSuppressed, setIsSuppressed] = useState(false); + const [isDismissed, setIsDismissed] = useState(false); const handleClick = useCallback(() => { - if (closeOnClick) setIsSuppressed(true); + if (closeOnClick) setIsDismissed(true); }, [closeOnClick]); - const handleUnsuppress = useCallback(() => setIsSuppressed(false), []); + const handleUndismissed = useCallback(() => setIsDismissed(false), []); // Skip synthetic enter/leave fired when a child changes visibility (relatedTarget stays inside the wrapper). const handleMouseEnterAndLeave = useCallback( (e: React.MouseEvent) => { const related = e.relatedTarget; if (related instanceof Node && e.currentTarget.contains(related)) return; - setIsSuppressed(false); + setIsDismissed(false); }, [] ); @@ -60,7 +60,7 @@ export const InlineTip: React.FC = ({ ? { 'data-tooltip-body': '' } : { hideTip: isTipHidden }; const tipWrapperProps = isHoverType - ? { inheritDims, suppress: isSuppressed } + ? { inheritDims, dismissed: isDismissed } : {}; const tipBodyAlignment = getAlignmentStyles({ alignment, avatar, type }); const isHorizontalCenter = tipBodyAlignment === 'horizontalCenter'; @@ -70,7 +70,7 @@ export const InlineTip: React.FC = ({ height={inheritDims ? 'inherit' : undefined} ref={wrapperRef} width={inheritDims ? 'inherit' : undefined} - onBlur={isHoverType ? handleUnsuppress : undefined} + onBlur={isHoverType ? handleUndismissed : undefined} onClick={isHoverType ? handleClick : undefined} onKeyDown={escapeKeyPressHandler} > diff --git a/packages/gamut/src/Tip/shared/elements.tsx b/packages/gamut/src/Tip/shared/elements.tsx index c5f92b2f210..8b5ab1ef6de 100644 --- a/packages/gamut/src/Tip/shared/elements.tsx +++ b/packages/gamut/src/Tip/shared/elements.tsx @@ -34,7 +34,7 @@ const inlineTipStates = states({ }); const toolTipWrapperStates = states({ - suppress: { + dismissed: { '&:hover > [data-tooltip-body], &:has(:focus-visible) > [data-tooltip-body]': { opacity: 0, From 3ea21c720b878c2bb188674dfecafff632e7b2dc Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 1 Jul 2026 12:00:44 -0400 Subject: [PATCH 24/42] undoing some type changes --- packages/gamut/src/Button/IconButton.tsx | 2 +- packages/gamut/src/Tip/ToolTip/index.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/gamut/src/Button/IconButton.tsx b/packages/gamut/src/Button/IconButton.tsx index dd3f1a7d741..1e200229215 100644 --- a/packages/gamut/src/Button/IconButton.tsx +++ b/packages/gamut/src/Button/IconButton.tsx @@ -40,7 +40,7 @@ export const IconButton = forwardRef( const iconSize = iconSizeMapping[buttonSize]; return ( - + Date: Wed, 1 Jul 2026 14:01:40 -0400 Subject: [PATCH 25/42] add version plan --- .nx/version-plans/version-plan-1782928816938.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .nx/version-plans/version-plan-1782928816938.md diff --git a/.nx/version-plans/version-plan-1782928816938.md b/.nx/version-plans/version-plan-1782928816938.md new file mode 100644 index 00000000000..e81bc503819 --- /dev/null +++ b/.nx/version-plans/version-plan-1782928816938.md @@ -0,0 +1,5 @@ +--- +gamut: patch +--- + +enable dismissable of ToolTip via onClick for interactive components like IconButton, MenuItem (icon-only), and FloatingToolTip + InlineToolTip From 2c47b8ebe81eb32894c2f70a337612d66f889a71 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 1 Jul 2026 16:18:39 -0400 Subject: [PATCH 26/42] clean up prop and add new story --- packages/gamut/src/Button/IconButton.tsx | 2 +- .../src/lib/Atoms/Buttons/IconButton/IconButton.mdx | 8 +++++++- .../Atoms/Buttons/IconButton/IconButton.stories.tsx | 12 ++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/gamut/src/Button/IconButton.tsx b/packages/gamut/src/Button/IconButton.tsx index 1e200229215..fc5301765ff 100644 --- a/packages/gamut/src/Button/IconButton.tsx +++ b/packages/gamut/src/Button/IconButton.tsx @@ -40,7 +40,7 @@ export const IconButton = forwardRef( const iconSize = iconSizeMapping[buttonSize]; return ( - + +### Persisting the tooltip + +Pass `tipProps={{ closeOnClick: false }}` to keep the tooltip visible after a click. Use this for actions where the tooltip content changes in response to the click, such as a copy → copied pattern. + + + ## Playground diff --git a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx index 63b5cf62ba4..7c417970986 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx +++ b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx @@ -91,3 +91,15 @@ export const CloseOnClick: Story = { ), }; + +export const PersistTooltip: Story = { + args: { + tip: 'Tooltip stays open on click', + tipProps: { + placement: 'floating', + alignment: 'top-center', + closeOnClick: false, + }, + onClick: logClick, + }, +}; From 90c8362c3f95a823157971a2a29e6b114073007a Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 1 Jul 2026 16:44:35 -0400 Subject: [PATCH 27/42] move back closeOnClick and update more stories --- packages/gamut/src/Button/IconButton.tsx | 2 +- .../src/lib/Atoms/Buttons/IconButton/IconButton.mdx | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/gamut/src/Button/IconButton.tsx b/packages/gamut/src/Button/IconButton.tsx index fc5301765ff..1e200229215 100644 --- a/packages/gamut/src/Button/IconButton.tsx +++ b/packages/gamut/src/Button/IconButton.tsx @@ -40,7 +40,7 @@ export const IconButton = forwardRef( const iconSize = iconSizeMapping[buttonSize]; return ( - + -### Persisting the tooltip - -Pass `tipProps={{ closeOnClick: false }}` to keep the tooltip visible after a click. Use this for actions where the tooltip content changes in response to the click, such as a copy → copied pattern. - - - ## Playground From 31e06c843d626b13b17aea84bf05f81c44fbd7a5 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Fri, 26 Jun 2026 17:00:33 -0400 Subject: [PATCH 28/42] first attempt at closing tooltip after click --- packages/gamut/src/Button/IconButton.tsx | 2 +- packages/gamut/src/Tip/ToolTip/index.tsx | 10 ++++++-- packages/gamut/src/Tip/shared/FloatingTip.tsx | 17 +++++++++++++ packages/gamut/src/Tip/shared/InlineTip.tsx | 25 ++++++++++++++++++- packages/gamut/src/Tip/shared/types.tsx | 1 + 5 files changed, 51 insertions(+), 4 deletions(-) diff --git a/packages/gamut/src/Button/IconButton.tsx b/packages/gamut/src/Button/IconButton.tsx index fc5301765ff..b85be7e04d1 100644 --- a/packages/gamut/src/Button/IconButton.tsx +++ b/packages/gamut/src/Button/IconButton.tsx @@ -40,7 +40,7 @@ export const IconButton = forwardRef( const iconSize = iconSizeMapping[buttonSize]; return ( - + = ({ diff --git a/packages/gamut/src/Tip/shared/FloatingTip.tsx b/packages/gamut/src/Tip/shared/FloatingTip.tsx index c3013661549..d65ba03cdc6 100644 --- a/packages/gamut/src/Tip/shared/FloatingTip.tsx +++ b/packages/gamut/src/Tip/shared/FloatingTip.tsx @@ -24,6 +24,7 @@ export const FloatingTip: React.FC = ({ alignment, avatar, children, + closeOnClick, escapeKeyPressHandler, inheritDims, info, @@ -122,6 +123,21 @@ export const FloatingTip: React.FC = ({ ? (e: FocusOrMouseEvent) => handleShowHideAction(e) : undefined; + const handleClick = useCallback(() => { + if (hoverDelayRef.current) { + clearTimeout(hoverDelayRef.current); + hoverDelayRef.current = undefined; + } + if (focusDelayRef.current) { + clearTimeout(focusDelayRef.current); + focusDelayRef.current = undefined; + } + setIsOpen(false); + setIsFocused(false); + }, []); + + const clickHandler = closeOnClick && isHoverType ? handleClick : undefined; + const contents = isPreviewType ? ( = ({ ref={ref} width={inheritDims ? 'inherit' : undefined} onBlur={toolOnlyEventFunc} + onClick={clickHandler} onFocus={toolOnlyEventFunc} onKeyDown={escapeKeyPressHandler} onMouseDown={(e) => e.preventDefault()} diff --git a/packages/gamut/src/Tip/shared/InlineTip.tsx b/packages/gamut/src/Tip/shared/InlineTip.tsx index 871cfaac1ac..7e45a61ec94 100644 --- a/packages/gamut/src/Tip/shared/InlineTip.tsx +++ b/packages/gamut/src/Tip/shared/InlineTip.tsx @@ -1,3 +1,5 @@ +import { useCallback, useState } from 'react'; + import { InfoTipContainer } from '../InfoTip/styles'; import { PreviewTipContents, PreviewTipShadow } from '../PreviewTip/elements'; import { ToolTipContainer } from '../ToolTip/elements'; @@ -15,6 +17,7 @@ export const InlineTip: React.FC = ({ alignment, avatar, children, + closeOnClick, escapeKeyPressHandler, id, inheritDims, @@ -31,6 +34,15 @@ export const InlineTip: React.FC = ({ zIndex, }) => { const isHoverType = type === 'tool' || type === 'preview'; + const [isSuppressed, setIsSuppressed] = useState(false); + + const handleClick = useCallback(() => { + if (closeOnClick) setIsSuppressed(true); + }, [closeOnClick]); + + const handleBlur = useCallback(() => setIsSuppressed(false), []); + + const handleMouseLeave = useCallback(() => setIsSuppressed(false), []); const InlineTipWrapper = isHoverType ? ToolTipWrapper : InfoTipWrapper; const InlineTipBodyWrapper = isHoverType @@ -41,11 +53,18 @@ export const InlineTip: React.FC = ({ const tipBodyAlignment = getAlignmentStyles({ alignment, avatar, type }); const isHorizontalCenter = tipBodyAlignment === 'horizontalCenter'; + const suppressedBodyStyle = + isHoverType && isSuppressed + ? { opacity: 0, visibility: 'hidden' as const, transition: 'none' } + : undefined; + const target = ( {children} @@ -57,6 +76,7 @@ export const InlineTip: React.FC = ({ alignment={alignment} zIndex={zIndex ?? 1} {...inlineWrapperProps} + style={suppressedBodyStyle} > = ({ ); return ( - + {alignment.includes('top') ? ( <> {tipBody} diff --git a/packages/gamut/src/Tip/shared/types.tsx b/packages/gamut/src/Tip/shared/types.tsx index 164ed1aaf37..689fd71debf 100644 --- a/packages/gamut/src/Tip/shared/types.tsx +++ b/packages/gamut/src/Tip/shared/types.tsx @@ -81,6 +81,7 @@ export type TipPlacementComponentProps = Omit< contentRef?: | React.RefObject | ((node: HTMLDivElement | null) => void); + closeOnClick?: boolean; type: 'info' | 'tool' | 'preview'; wrapperRef?: React.RefObject; zIndex?: number; From 6040b72c882241e8a434605b236eedcddb682be8 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 29 Jun 2026 10:02:06 -0400 Subject: [PATCH 29/42] removed zIndex from tip props --- packages/gamut/src/Tip/ToolTip/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/gamut/src/Tip/ToolTip/index.tsx b/packages/gamut/src/Tip/ToolTip/index.tsx index ce8a2652599..f8662a43f8c 100644 --- a/packages/gamut/src/Tip/ToolTip/index.tsx +++ b/packages/gamut/src/Tip/ToolTip/index.tsx @@ -22,7 +22,6 @@ export type ToolTipProps = TipNewBaseProps & * Can be used for accessibility - the same id needs to be passed to the `aria-describedby` attribute of the element that the tooltip is describing. */ id?: string; - zIndex?: number; }; export const ToolTip: React.FC = ({ From 3d9c0f6d078c29b0a3b62b99ca4b0a8a9c9c2994 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 29 Jun 2026 11:27:14 -0400 Subject: [PATCH 30/42] InlineToolTip now a state instead of style, added testing for onClick --- .../src/Button/__tests__/IconButton.test.tsx | 30 +++++++++++++++++++ packages/gamut/src/Tip/shared/InlineTip.tsx | 10 ++----- packages/gamut/src/Tip/shared/elements.tsx | 17 +++++++++-- .../Buttons/IconButton/IconButton.stories.tsx | 8 +++++ 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/packages/gamut/src/Button/__tests__/IconButton.test.tsx b/packages/gamut/src/Button/__tests__/IconButton.test.tsx index 5f9adefd72c..bd849529c2e 100644 --- a/packages/gamut/src/Button/__tests__/IconButton.test.tsx +++ b/packages/gamut/src/Button/__tests__/IconButton.test.tsx @@ -70,4 +70,34 @@ describe('IconButton', () => { await waitFor(() => expect(view.queryAllByText(tip).length).toBe(2)); }); + + describe('closeOnClick', () => { + beforeEach(() => { + onClick.mockClear(); + }); + + it('does not prevent button onClick when the inline tip click handler fires', async () => { + const { view } = renderView(); + + await userEvent.click(view.getByRole('button', { name: label })); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('does not prevent button onClick when the floating tip click handler fires', async () => { + const { view } = renderFloatingView(); + + await userEvent.click(view.getByRole('button', { name: label })); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('does not prevent button onClick when closeOnClick is disabled', async () => { + const { view } = renderView({ tipProps: { closeOnClick: false } }); + + await userEvent.click(view.getByRole('button', { name: label })); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/gamut/src/Tip/shared/InlineTip.tsx b/packages/gamut/src/Tip/shared/InlineTip.tsx index 7e45a61ec94..6b77d629c68 100644 --- a/packages/gamut/src/Tip/shared/InlineTip.tsx +++ b/packages/gamut/src/Tip/shared/InlineTip.tsx @@ -49,15 +49,12 @@ export const InlineTip: React.FC = ({ ? ToolTipContainer : InfoTipContainer; const inlineWrapperProps = isHoverType ? {} : { hideTip: isTipHidden }; - const tipWrapperProps = isHoverType ? ({ inheritDims } as const) : {}; + const tipWrapperProps = isHoverType + ? { inheritDims, suppress: isSuppressed } + : {}; const tipBodyAlignment = getAlignmentStyles({ alignment, avatar, type }); const isHorizontalCenter = tipBodyAlignment === 'horizontalCenter'; - const suppressedBodyStyle = - isHoverType && isSuppressed - ? { opacity: 0, visibility: 'hidden' as const, transition: 'none' } - : undefined; - const target = ( = ({ alignment={alignment} zIndex={zIndex ?? 1} {...inlineWrapperProps} - style={suppressedBodyStyle} > div, &:focus-within > div': { + opacity: 0, + visibility: 'hidden', + transition: 'none', + }, + }, +}); + export const FloatingTipTextWrapper = styled(FlexBox)< StyleProps >( @@ -43,7 +53,9 @@ export const FloatingTipTextWrapper = styled(FlexBox)< floatingTipTextStates ); -export const ToolTipWrapper = styled.div>( +export const ToolTipWrapper = styled.div< + StyleProps & StyleProps +>( css({ '&:hover > div, &:focus-within > div': { opacity: 1, @@ -52,7 +64,8 @@ export const ToolTipWrapper = styled.div>( }, ...tipWrapperStyles, }), - inlineTipStates + inlineTipStates, + toolTipWrapperStates ); export const InfoTipWrapper = styled.div( diff --git a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx index 9c9f8b41612..3fbd32df8ff 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx +++ b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx @@ -48,3 +48,11 @@ type Story = StoryObj; export const Default: Story = { args: {}, }; + +export const CloseOnClick: Story = { + args: { + tip: 'Tooltip closes on click', + // eslint-disable-next-line no-console + onClick: () => console.log('button onClick fired'), + }, +}; From 919ec6a70f3d634e5698b75f5c03d336f7e5eadc Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 29 Jun 2026 15:58:40 -0400 Subject: [PATCH 31/42] fix various bugs --- packages/gamut/src/Tip/shared/InlineTip.tsx | 21 +++++++++++++++----- packages/gamut/src/Tip/shared/elements.tsx | 22 +++++++++++---------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/gamut/src/Tip/shared/InlineTip.tsx b/packages/gamut/src/Tip/shared/InlineTip.tsx index 6b77d629c68..48304ba9e43 100644 --- a/packages/gamut/src/Tip/shared/InlineTip.tsx +++ b/packages/gamut/src/Tip/shared/InlineTip.tsx @@ -40,15 +40,25 @@ export const InlineTip: React.FC = ({ if (closeOnClick) setIsSuppressed(true); }, [closeOnClick]); - const handleBlur = useCallback(() => setIsSuppressed(false), []); + const handleUnsuppress = useCallback(() => setIsSuppressed(false), []); - const handleMouseLeave = useCallback(() => setIsSuppressed(false), []); + // Skip synthetic enter/leave fired when a child changes visibility (relatedTarget stays inside the wrapper). + const handleMouseEnterAndLeave = useCallback( + (e: React.MouseEvent) => { + const related = e.relatedTarget as Node | null; + if (related && e.currentTarget.contains(related)) return; + setIsSuppressed(false); + }, + [] + ); const InlineTipWrapper = isHoverType ? ToolTipWrapper : InfoTipWrapper; const InlineTipBodyWrapper = isHoverType ? ToolTipContainer : InfoTipContainer; - const inlineWrapperProps = isHoverType ? {} : { hideTip: isTipHidden }; + const inlineWrapperProps = isHoverType + ? { 'data-tooltip-body': '' } + : { hideTip: isTipHidden }; const tipWrapperProps = isHoverType ? { inheritDims, suppress: isSuppressed } : {}; @@ -60,7 +70,7 @@ export const InlineTip: React.FC = ({ height={inheritDims ? 'inherit' : undefined} ref={wrapperRef} width={inheritDims ? 'inherit' : undefined} - onBlur={isHoverType ? handleBlur : undefined} + onBlur={isHoverType ? handleUnsuppress : undefined} onClick={isHoverType ? handleClick : undefined} onKeyDown={escapeKeyPressHandler} > @@ -108,7 +118,8 @@ export const InlineTip: React.FC = ({ return ( {alignment.includes('top') ? ( <> diff --git a/packages/gamut/src/Tip/shared/elements.tsx b/packages/gamut/src/Tip/shared/elements.tsx index c42fcb67c5d..c5f92b2f210 100644 --- a/packages/gamut/src/Tip/shared/elements.tsx +++ b/packages/gamut/src/Tip/shared/elements.tsx @@ -35,11 +35,12 @@ const inlineTipStates = states({ const toolTipWrapperStates = states({ suppress: { - '&:hover > div, &:focus-within > div': { - opacity: 0, - visibility: 'hidden', - transition: 'none', - }, + '&:hover > [data-tooltip-body], &:has(:focus-visible) > [data-tooltip-body]': + { + opacity: 0, + visibility: 'hidden', + transition: 'none', + }, }, }); @@ -57,11 +58,12 @@ export const ToolTipWrapper = styled.div< StyleProps & StyleProps >( css({ - '&:hover > div, &:focus-within > div': { - opacity: 1, - transition: `opacity ${timing.fast} ${timing.base}`, - visibility: 'visible', - }, + '&:hover > [data-tooltip-body], &:has(:focus-visible) > [data-tooltip-body]': + { + opacity: 1, + transition: `opacity ${timing.fast} ${timing.base}`, + visibility: 'visible', + }, ...tipWrapperStyles, }), inlineTipStates, From d71e6a6a3cf608d13f8be82db222ca6364dba548 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 29 Jun 2026 16:59:29 -0400 Subject: [PATCH 32/42] added stories --- .../Atoms/Buttons/IconButton/IconButton.mdx | 6 +++ .../Buttons/IconButton/IconButton.stories.tsx | 47 ++++++++++++++++--- .../lib/Molecules/Tips/ToolTip/ToolTip.mdx | 6 +++ .../Tips/ToolTip/ToolTip.stories.tsx | 43 +++++++++++++++++ 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.mdx b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.mdx index 6c21691cfcc..6f2afb1e2a5 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.mdx +++ b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.mdx @@ -31,6 +31,12 @@ Use for secondary or space-constrained actions with recognizable icons. Use regu For more detailed information on `IconButton` tooltips, take a look at the ToolTip story. +## Close on Click + +By default, `IconButton` tooltips close immediately on click and reappear only after the cursor leaves and re-enters. Pass `tipProps={{ closeOnClick: false }}` to opt out (e.g. copy → copied patterns). The first two buttons close on click; the last two stay open. + + + ## Playground diff --git a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx index 3fbd32df8ff..63b5cf62ba4 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx +++ b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx @@ -1,4 +1,5 @@ -import { IconButton } from '@codecademy/gamut'; +import { FlexBox, IconButton } from '@codecademy/gamut'; +import { SparkleIcon } from '@codecademy/gamut-icons'; import * as icons from '@codecademy/gamut-icons'; import type { Meta, StoryObj } from '@storybook/react'; import type { TypeWithDeepControls } from 'storybook-addon-deep-controls'; @@ -49,10 +50,44 @@ export const Default: Story = { args: {}, }; +// eslint-disable-next-line no-console +const logClick = () => console.log('button onClick fired'); + export const CloseOnClick: Story = { - args: { - tip: 'Tooltip closes on click', - // eslint-disable-next-line no-console - onClick: () => console.log('button onClick fired'), - }, + render: () => ( + + + + + + + ), }; diff --git a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.mdx b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.mdx index 14f834ff785..684eda388b7 100644 --- a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.mdx +++ b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.mdx @@ -69,6 +69,12 @@ When a Button is disabled with a tooltip, you must use the `aria-disabled` prop +### Close on click + +By default, `ToolTip` closes on click and reappears only after the cursor leaves and re-enters. Pass `closeOnClick={false}` to opt out (e.g. copy → copied patterns). The first two buttons close on click; the last two stay open. + + + ## Playground diff --git a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx index f08cd8c55b4..f1e388c111b 100644 --- a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx +++ b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx @@ -154,6 +154,49 @@ export const Disabled: Story = { ), }; +export const CloseOnClick: Story = { + render: () => ( + + + Floating + + + Inline + + + + Floating (stays open) + + + + + Inline (stays open) + + + + ), +}; + export const HorizontalAlignments: Story = { render: () => ( <> From 67500ec800691fcd3f1c7f5b691c70fc566d7868 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Tue, 30 Jun 2026 10:28:17 -0400 Subject: [PATCH 33/42] fix up errors --- packages/gamut/src/Button/IconButton.tsx | 2 +- packages/gamut/src/Tip/shared/InlineTip.tsx | 4 ++-- .../src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/gamut/src/Button/IconButton.tsx b/packages/gamut/src/Button/IconButton.tsx index b85be7e04d1..dd3f1a7d741 100644 --- a/packages/gamut/src/Button/IconButton.tsx +++ b/packages/gamut/src/Button/IconButton.tsx @@ -40,7 +40,7 @@ export const IconButton = forwardRef( const iconSize = iconSizeMapping[buttonSize]; return ( - + = ({ // Skip synthetic enter/leave fired when a child changes visibility (relatedTarget stays inside the wrapper). const handleMouseEnterAndLeave = useCallback( (e: React.MouseEvent) => { - const related = e.relatedTarget as Node | null; - if (related && e.currentTarget.contains(related)) return; + const related = e.relatedTarget; + if (related instanceof Node && e.currentTarget.contains(related)) return; setIsSuppressed(false); }, [] diff --git a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx index f1e388c111b..3d0fa365822 100644 --- a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx +++ b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx @@ -158,36 +158,36 @@ export const CloseOnClick: Story = { render: () => ( Floating Inline Floating (stays open) Inline (stays open) From 5ba17d2cc201cb05f83b62149c9b4de75b3dddde Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Tue, 30 Jun 2026 16:48:28 -0400 Subject: [PATCH 34/42] update MenuItem tooltip --- packages/gamut/src/Menu/MenuItem.tsx | 7 ++++++- packages/gamut/src/Menu/elements.tsx | 9 +++++++-- .../styleguide/src/lib/Molecules/Menu/Menu.stories.tsx | 10 +++++++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/gamut/src/Menu/MenuItem.tsx b/packages/gamut/src/Menu/MenuItem.tsx index c08251a7f0a..2de1daca73c 100644 --- a/packages/gamut/src/Menu/MenuItem.tsx +++ b/packages/gamut/src/Menu/MenuItem.tsx @@ -76,6 +76,7 @@ export const MenuItem = forwardRef< target, width = 1, 'aria-label': explicitAriaLabel, + closeOnClick = true, ...props }, ref @@ -160,7 +161,11 @@ export const MenuItem = forwardRef< return ( - + , 'children' | 'label'> & { tipId: string; + closeOnClick?: boolean; } -> = ({ children, label, tipId }) => { +> = ({ children, label, tipId, closeOnClick }) => { if (!label) { return <>{children}; } @@ -267,5 +268,9 @@ export const MenuToolTipWrapper: React.FC< ...defaultTipProps, }; - return {children}; + return ( + + {children} + + ); }; diff --git a/packages/styleguide/src/lib/Molecules/Menu/Menu.stories.tsx b/packages/styleguide/src/lib/Molecules/Menu/Menu.stories.tsx index cc5a1942b24..38e94d44933 100644 --- a/packages/styleguide/src/lib/Molecules/Menu/Menu.stories.tsx +++ b/packages/styleguide/src/lib/Molecules/Menu/Menu.stories.tsx @@ -262,14 +262,18 @@ export const IconMenu: Story = { <> {}} /> {}} /> {}} /> Date: Tue, 30 Jun 2026 17:00:10 -0400 Subject: [PATCH 35/42] update MenuItem type --- packages/gamut/src/Menu/MenuItem.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/gamut/src/Menu/MenuItem.tsx b/packages/gamut/src/Menu/MenuItem.tsx index 2de1daca73c..8a74017b3ea 100644 --- a/packages/gamut/src/Menu/MenuItem.tsx +++ b/packages/gamut/src/Menu/MenuItem.tsx @@ -49,6 +49,7 @@ interface MenuItemIconOnly extends HTMLProps, ForwardListItemProps { /** ToolTips will only render for interactive items, otherwise the label will be used as a generic aria-label */ label: ToolTipLabel; disabled?: boolean; + closeOnClick?: boolean; } interface MenuTextItem extends HTMLProps, ForwardListItemProps { @@ -56,6 +57,7 @@ interface MenuTextItem extends HTMLProps, ForwardListItemProps { children: React.ReactNode; label?: ToolTipLabel; disabled?: boolean; + closeOnClick?: never; } type MenuItemTypes = MenuItemIconOnly | MenuTextItem; From f5ee22678b6544fd4fff43ee0657900332631fa2 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 1 Jul 2026 11:25:14 -0400 Subject: [PATCH 36/42] updated supressed => dismissed --- packages/gamut/src/Tip/shared/InlineTip.tsx | 12 ++++++------ packages/gamut/src/Tip/shared/elements.tsx | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/gamut/src/Tip/shared/InlineTip.tsx b/packages/gamut/src/Tip/shared/InlineTip.tsx index fe527dc5a2a..51107b70c59 100644 --- a/packages/gamut/src/Tip/shared/InlineTip.tsx +++ b/packages/gamut/src/Tip/shared/InlineTip.tsx @@ -34,20 +34,20 @@ export const InlineTip: React.FC = ({ zIndex, }) => { const isHoverType = type === 'tool' || type === 'preview'; - const [isSuppressed, setIsSuppressed] = useState(false); + const [isDismissed, setIsDismissed] = useState(false); const handleClick = useCallback(() => { - if (closeOnClick) setIsSuppressed(true); + if (closeOnClick) setIsDismissed(true); }, [closeOnClick]); - const handleUnsuppress = useCallback(() => setIsSuppressed(false), []); + const handleUndismissed = useCallback(() => setIsDismissed(false), []); // Skip synthetic enter/leave fired when a child changes visibility (relatedTarget stays inside the wrapper). const handleMouseEnterAndLeave = useCallback( (e: React.MouseEvent) => { const related = e.relatedTarget; if (related instanceof Node && e.currentTarget.contains(related)) return; - setIsSuppressed(false); + setIsDismissed(false); }, [] ); @@ -60,7 +60,7 @@ export const InlineTip: React.FC = ({ ? { 'data-tooltip-body': '' } : { hideTip: isTipHidden }; const tipWrapperProps = isHoverType - ? { inheritDims, suppress: isSuppressed } + ? { inheritDims, dismissed: isDismissed } : {}; const tipBodyAlignment = getAlignmentStyles({ alignment, avatar, type }); const isHorizontalCenter = tipBodyAlignment === 'horizontalCenter'; @@ -70,7 +70,7 @@ export const InlineTip: React.FC = ({ height={inheritDims ? 'inherit' : undefined} ref={wrapperRef} width={inheritDims ? 'inherit' : undefined} - onBlur={isHoverType ? handleUnsuppress : undefined} + onBlur={isHoverType ? handleUndismissed : undefined} onClick={isHoverType ? handleClick : undefined} onKeyDown={escapeKeyPressHandler} > diff --git a/packages/gamut/src/Tip/shared/elements.tsx b/packages/gamut/src/Tip/shared/elements.tsx index c5f92b2f210..8b5ab1ef6de 100644 --- a/packages/gamut/src/Tip/shared/elements.tsx +++ b/packages/gamut/src/Tip/shared/elements.tsx @@ -34,7 +34,7 @@ const inlineTipStates = states({ }); const toolTipWrapperStates = states({ - suppress: { + dismissed: { '&:hover > [data-tooltip-body], &:has(:focus-visible) > [data-tooltip-body]': { opacity: 0, From 1624b7d5580539deefc7f558a91f7095902acf58 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 1 Jul 2026 12:00:44 -0400 Subject: [PATCH 37/42] undoing some type changes --- packages/gamut/src/Button/IconButton.tsx | 2 +- packages/gamut/src/Tip/ToolTip/index.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/gamut/src/Button/IconButton.tsx b/packages/gamut/src/Button/IconButton.tsx index dd3f1a7d741..1e200229215 100644 --- a/packages/gamut/src/Button/IconButton.tsx +++ b/packages/gamut/src/Button/IconButton.tsx @@ -40,7 +40,7 @@ export const IconButton = forwardRef( const iconSize = iconSizeMapping[buttonSize]; return ( - + Date: Wed, 1 Jul 2026 14:01:40 -0400 Subject: [PATCH 38/42] add version plan --- .nx/version-plans/version-plan-1782928816938.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .nx/version-plans/version-plan-1782928816938.md diff --git a/.nx/version-plans/version-plan-1782928816938.md b/.nx/version-plans/version-plan-1782928816938.md new file mode 100644 index 00000000000..e81bc503819 --- /dev/null +++ b/.nx/version-plans/version-plan-1782928816938.md @@ -0,0 +1,5 @@ +--- +gamut: patch +--- + +enable dismissable of ToolTip via onClick for interactive components like IconButton, MenuItem (icon-only), and FloatingToolTip + InlineToolTip From 1ce3ee9839500af3a0b98663afa7b30c370a6f41 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 1 Jul 2026 16:18:39 -0400 Subject: [PATCH 39/42] clean up prop and add new story --- packages/gamut/src/Button/IconButton.tsx | 2 +- .../src/lib/Atoms/Buttons/IconButton/IconButton.mdx | 8 +++++++- .../Atoms/Buttons/IconButton/IconButton.stories.tsx | 12 ++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/gamut/src/Button/IconButton.tsx b/packages/gamut/src/Button/IconButton.tsx index 1e200229215..fc5301765ff 100644 --- a/packages/gamut/src/Button/IconButton.tsx +++ b/packages/gamut/src/Button/IconButton.tsx @@ -40,7 +40,7 @@ export const IconButton = forwardRef( const iconSize = iconSizeMapping[buttonSize]; return ( - + +### Persisting the tooltip + +Pass `tipProps={{ closeOnClick: false }}` to keep the tooltip visible after a click. Use this for actions where the tooltip content changes in response to the click, such as a copy → copied pattern. + + + ## Playground diff --git a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx index 63b5cf62ba4..7c417970986 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx +++ b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx @@ -91,3 +91,15 @@ export const CloseOnClick: Story = { ), }; + +export const PersistTooltip: Story = { + args: { + tip: 'Tooltip stays open on click', + tipProps: { + placement: 'floating', + alignment: 'top-center', + closeOnClick: false, + }, + onClick: logClick, + }, +}; From 56c0b4b1a04b472c66f6a14dc624372f461a81ae Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 1 Jul 2026 16:44:35 -0400 Subject: [PATCH 40/42] move back closeOnClick and update more stories --- packages/gamut/src/Button/IconButton.tsx | 2 +- .../src/lib/Atoms/Buttons/IconButton/IconButton.mdx | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/gamut/src/Button/IconButton.tsx b/packages/gamut/src/Button/IconButton.tsx index fc5301765ff..1e200229215 100644 --- a/packages/gamut/src/Button/IconButton.tsx +++ b/packages/gamut/src/Button/IconButton.tsx @@ -40,7 +40,7 @@ export const IconButton = forwardRef( const iconSize = iconSizeMapping[buttonSize]; return ( - + -### Persisting the tooltip - -Pass `tipProps={{ closeOnClick: false }}` to keep the tooltip visible after a click. Use this for actions where the tooltip content changes in response to the click, such as a copy → copied pattern. - - - ## Playground From 061b086e76a7ebeac2a89b552eb8485a96e180a5 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 1 Jul 2026 16:50:52 -0400 Subject: [PATCH 41/42] remove unused story --- .../Atoms/Buttons/IconButton/IconButton.stories.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx index 7c417970986..63b5cf62ba4 100644 --- a/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx +++ b/packages/styleguide/src/lib/Atoms/Buttons/IconButton/IconButton.stories.tsx @@ -91,15 +91,3 @@ export const CloseOnClick: Story = { ), }; - -export const PersistTooltip: Story = { - args: { - tip: 'Tooltip stays open on click', - tipProps: { - placement: 'floating', - alignment: 'top-center', - closeOnClick: false, - }, - onClick: logClick, - }, -}; From 214656696fa590acc0009122ac9d70b5c359f8e2 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Thu, 16 Jul 2026 09:49:02 -0400 Subject: [PATCH 42/42] add true default to tooltips closing on click --- packages/gamut/src/Tip/ToolTip/index.tsx | 2 ++ .../src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/gamut/src/Tip/ToolTip/index.tsx b/packages/gamut/src/Tip/ToolTip/index.tsx index 7a1a9e6b5e2..9b5ef322131 100644 --- a/packages/gamut/src/Tip/ToolTip/index.tsx +++ b/packages/gamut/src/Tip/ToolTip/index.tsx @@ -26,6 +26,7 @@ export type ToolTipProps = TipBaseProps & export const ToolTip: React.FC = ({ alignment = 'top-center', + closeOnClick = true, children, info, placement = tipDefaultProps.placement, @@ -44,6 +45,7 @@ export const ToolTip: React.FC = ({ const tipProps = { alignment, + closeOnClick, info, wrapperRef, ...rest, diff --git a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx index 3d0fa365822..cd4d3731298 100644 --- a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx +++ b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx @@ -158,7 +158,6 @@ export const CloseOnClick: Story = { render: () => ( Floating