From a008af10565648f37ba7aef912b4061b17c7f560 Mon Sep 17 00:00:00 2001 From: Arlindo Pereira Date: Tue, 15 Oct 2024 19:04:59 +0200 Subject: [PATCH 1/4] Tooltip with proper layout for positions top and bottom --- .../overlays/Tooltip/Tooltip.module.scss | 72 ++++++++----------- .../Tooltip/TooltipProvider.stories.tsx | 13 +++- 2 files changed, 40 insertions(+), 45 deletions(-) diff --git a/src/components/overlays/Tooltip/Tooltip.module.scss b/src/components/overlays/Tooltip/Tooltip.module.scss index 8b847a9..0dc34c8 100644 --- a/src/components/overlays/Tooltip/Tooltip.module.scss +++ b/src/components/overlays/Tooltip/Tooltip.module.scss @@ -5,55 +5,28 @@ @use '../../../styling/defs.scss' as bk; /* https://css-tricks.com/books/greatest-css-tricks/scroll-shadows */ -/* @define-mixin scroll-shadows { --bgRGB: 73, 89, 99; --bg: rgb(var(--bgRGB)); --bgTrans: rgba(var(--bgRGB), 0); --shadow: rgba(41, 50, 56, 0.5); background: - linear-gradient(var(--bg) 30%, var(--bgTrans)) center top, /* Shadow Cover TOP * / - linear-gradient(var(--bgTrans), var(--bg) 70%) center bottom, /* Shadow Cover BOTTOM * / - radial-gradient(farthest-side at 50% 0, var(--shadow), rgba(0, 0, 0, 0)) center top, /* Shadow TOP * / - radial-gradient(farthest-side at 50% 100%, var(--shadow), rgba(0, 0, 0, 0)) center bottom; /* Shadow BOTTOM * / + linear-gradient(var(--bg) 30%, var(--bgTrans)) center top, // Shadow Cover TOP + linear-gradient(var(--bgTrans), var(--bg) 70%) center bottom, // Shadow Cover BOTTOM + radial-gradient(farthest-side at 50% 0, var(--shadow), rgba(0, 0, 0, 0)) center top, // Shadow TOP + radial-gradient(farthest-side at 50% 100%, var(--shadow), rgba(0, 0, 0, 0)) center bottom; // Shadow BOTTOM background-repeat: no-repeat; background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px; background-attachment: local, local, scroll, scroll; } -*/ - -/* https://css-generators.com/tooltip-speech-bubble */ -@mixin bk-tooltip-arrow-top { - --arrow-x: 50%; /* Arrow position (0% = left 100% = right) */ - - clip-path: polygon(0 0, 0 100%, 100% 100%, 100% 0, - min(100%, var(--arrow-x) + var(--b) / 2) 0, - var(--arrow-x) calc(-1 * var(--h)), - max(0%, var(--arrow-x) - var(--b) / 2) 0); - border-image: fill 0 / 1 / var(--h) - conic-gradient(var(--bk-tooltip-background-color) 0 0); -} -@mixin bk-tooltip-arrow-bottom { - --arrow-x: 50%; /* Arrow position (0% = left 100% = right) */ - - clip-path: polygon(0 100%, 0 0, 100% 0, 100% 100%, - min(100%, var(--arrow-x) + var(--b) / 2) 100%, - var(--arrow-x) calc(100% + var(--h)), - max(0%, var(--arrow-x) - var(--b) / 2) 100%); - border-image: fill 0 / 1 / var(--h) - conic-gradient(var(--bk-tooltip-background-color) 0 0); -} @layer baklava.components { .bk-tooltip { @include bk.component-base(bk-tooltip); - --bk-tooltip-background-color: bk.$theme-tooltip-background-default; - --bk-tooltip-text-color: bk.$theme-tooltip-text-default; - cursor: default; - overflow-y: auto; + // overflow-y: auto; max-width: 30rem; max-height: 8lh; /* Show about 8 lines of text before scrolling */ @@ -63,28 +36,39 @@ */ padding: 7px 12px; - border-radius: 2px; - background: var(--bk-tooltip-background-color); - + border-radius: bk.$radius-s; + background: bk.$theme-tooltip-background-default; + border: 1px solid bk.$theme-tooltip-border-default; + @include bk.text-layout; text-align: center; - color: var(--bk-tooltip-text-color); + color: bk.$theme-tooltip-text-default; @include bk.font(bk.$font-family-body); font-size: 12px; + + --arrow-size: 7px; - &:is(.bk-tooltip--arrow-top, .bk-tooltip--arrow-bottom) { - --h: 6px; /* Height of the triangle. Note: must match the `offset` in `useFloating()`. */ - --b: calc(var(--h) * 2); /* Base of the triangle */ + &:is(.bk-tooltip--arrow-top, .bk-tooltip--arrow-bottom):before { + content: ''; + border-bottom: 1px solid bk.$theme-tooltip-border-default; + border-right: 1px solid bk.$theme-tooltip-border-default; + background-color: bk.$theme-tooltip-background-default; + position: absolute; + width: calc(2 * var(--arrow-size)); + height: calc(2 * var(--arrow-size)); } - &.bk-tooltip--arrow-top { - @include bk-tooltip-arrow-top; + &.bk-tooltip--arrow-bottom:before { + left: calc(50% - var(--arrow-size)); + bottom: calc(-1 * (calc(var(--arrow-size) + 1px))); + transform: rotate(45deg); } - &.bk-tooltip--arrow-bottom { - @include bk-tooltip-arrow-bottom; + &.bk-tooltip--arrow-top:before { + left: calc(50% - var(--arrow-size)); + top: calc(-1 * (calc(var(--arrow-size) + 1px))); + transform: rotate(-135deg); } } - @position-try --bk-tooltip-position-top { margin-top: var(--bk-layout-header-height); /* Compensate for layout header */ margin-bottom: 6px; diff --git a/src/components/overlays/Tooltip/TooltipProvider.stories.tsx b/src/components/overlays/Tooltip/TooltipProvider.stories.tsx index a8257f8..6d895ee 100644 --- a/src/components/overlays/Tooltip/TooltipProvider.stories.tsx +++ b/src/components/overlays/Tooltip/TooltipProvider.stories.tsx @@ -3,7 +3,6 @@ |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import type { Meta, StoryObj } from '@storybook/react'; -import { userEvent, within } from '@storybook/test'; import * as React from 'react'; import { Draggable } from '../../../util/drag.ts'; @@ -34,6 +33,18 @@ export default { export const Standard: Story = {}; +export const PlacementTop: Story = { + args: { + placement: 'top', + }, +}; + +export const PlacementBottom: Story = { + args: { + placement: 'bottom', + }, +}; + /** * When a tooltip hits the viewport during scroll, it will automatically reposition to be visible. */ From 230503f997df97b924f223607ce521afd061e15e Mon Sep 17 00:00:00 2001 From: Arlindo Pereira Date: Wed, 16 Oct 2024 10:16:31 +0200 Subject: [PATCH 2/4] Adds placements left and right --- .../overlays/Tooltip/Tooltip.module.scss | 30 +++++++++++++++---- .../Tooltip/TooltipProvider.stories.tsx | 12 ++++++++ .../overlays/Tooltip/TooltipProvider.tsx | 3 ++ 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/components/overlays/Tooltip/Tooltip.module.scss b/src/components/overlays/Tooltip/Tooltip.module.scss index 0dc34c8..36b3fd3 100644 --- a/src/components/overlays/Tooltip/Tooltip.module.scss +++ b/src/components/overlays/Tooltip/Tooltip.module.scss @@ -47,8 +47,8 @@ font-size: 12px; --arrow-size: 7px; - - &:is(.bk-tooltip--arrow-top, .bk-tooltip--arrow-bottom):before { + + &.bk-tooltip--arrow:before { content: ''; border-bottom: 1px solid bk.$theme-tooltip-border-default; border-right: 1px solid bk.$theme-tooltip-border-default; @@ -57,16 +57,28 @@ width: calc(2 * var(--arrow-size)); height: calc(2 * var(--arrow-size)); } - &.bk-tooltip--arrow-bottom:before { + &:is(.bk-tooltip--arrow-bottom, .bk-tooltip--arrow-top):before { left: calc(50% - var(--arrow-size)); + } + &.bk-tooltip--arrow-bottom:before { bottom: calc(-1 * (calc(var(--arrow-size) + 1px))); transform: rotate(45deg); } &.bk-tooltip--arrow-top:before { - left: calc(50% - var(--arrow-size)); top: calc(-1 * (calc(var(--arrow-size) + 1px))); transform: rotate(-135deg); } + &:is(.bk-tooltip--arrow-left, .bk-tooltip--arrow-right):before { + top: calc(50% - var(--arrow-size)); + } + &.bk-tooltip--arrow-left:before { + left: calc(-1 * (calc(var(--arrow-size) + 1px))); + transform: rotate(135deg); + } + &.bk-tooltip--arrow-right:before { + right: calc(-1 * (calc(var(--arrow-size) + 1px))); + transform: rotate(-45deg); + } } @position-try --bk-tooltip-position-top { @@ -82,13 +94,21 @@ justify-self: anchor-center; top: anchor(bottom); } + @position-try --bk-tooltip-position-left { + justify-self: anchor-center; + right: anchor(left); + } + @position-try --bk-tooltip-position-right { + justify-self: anchor-center; + left: anchor(right); + } .bk-tooltip[popover] { inset: auto; /* Note: future versions of Chrome should have this by default */ position: fixed; /*position-anchor: --anchor-1;*/ /* Needs to be set dynamically */ - position-try-fallbacks: --bk-tooltip-position-top, --bk-tooltip-position-bottom; + position-try-fallbacks: --bk-tooltip-position-top, --bk-tooltip-position-bottom, --bk-tooltip-position-left, --bk-tooltip-position-right; filter: drop-shadow(4px 4px 4px rgba(50 50 50 / 30%)); diff --git a/src/components/overlays/Tooltip/TooltipProvider.stories.tsx b/src/components/overlays/Tooltip/TooltipProvider.stories.tsx index 6d895ee..0ac9738 100644 --- a/src/components/overlays/Tooltip/TooltipProvider.stories.tsx +++ b/src/components/overlays/Tooltip/TooltipProvider.stories.tsx @@ -45,6 +45,18 @@ export const PlacementBottom: Story = { }, }; +export const PlacementLeft: Story = { + args: { + placement: 'left', + }, +}; + +export const PlacementRight: Story = { + args: { + placement: 'right', + }, +}; + /** * When a tooltip hits the viewport during scroll, it will automatically reposition to be visible. */ diff --git a/src/components/overlays/Tooltip/TooltipProvider.tsx b/src/components/overlays/Tooltip/TooltipProvider.tsx index ca01afc..71bd5cf 100644 --- a/src/components/overlays/Tooltip/TooltipProvider.tsx +++ b/src/components/overlays/Tooltip/TooltipProvider.tsx @@ -96,8 +96,11 @@ export const TooltipProvider = (props: TooltipProviderProps) => { ref={mergeRefs(refs.setFloating as any, tooltipProps.ref)} className={cx( floatingProps.className as ClassNameArgument, + { [TooltipClassNames['bk-tooltip--arrow']]: !!arrow?.side }, { [TooltipClassNames['bk-tooltip--arrow-top']]: arrow?.side === 'top' }, { [TooltipClassNames['bk-tooltip--arrow-bottom']]: arrow?.side === 'bottom' }, + { [TooltipClassNames['bk-tooltip--arrow-left']]: arrow?.side === 'left' }, + { [TooltipClassNames['bk-tooltip--arrow-right']]: arrow?.side === 'right' }, tooltipProps.className, )} style={{ From 5414c28ac9c0159adbb46e6ac6fd40bf0548036f Mon Sep 17 00:00:00 2001 From: Arlindo Pereira Date: Wed, 16 Oct 2024 16:10:05 +0200 Subject: [PATCH 3/4] Tooltip size --- .../overlays/Tooltip/Tooltip.module.scss | 10 ++++++++ .../overlays/Tooltip/Tooltip.stories.tsx | 18 +++++++++++++ src/components/overlays/Tooltip/Tooltip.tsx | 9 ++++++- .../Tooltip/TooltipProvider.stories.tsx | 25 +++++++++++++++++-- .../overlays/Tooltip/TooltipProvider.tsx | 2 ++ 5 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/components/overlays/Tooltip/Tooltip.module.scss b/src/components/overlays/Tooltip/Tooltip.module.scss index 36b3fd3..3d0b567 100644 --- a/src/components/overlays/Tooltip/Tooltip.module.scss +++ b/src/components/overlays/Tooltip/Tooltip.module.scss @@ -46,6 +46,16 @@ @include bk.font(bk.$font-family-body); font-size: 12px; + &.bk-tooltip--size-small { + width: 140px; + } + &.bk-tooltip--size-medium { + width: 225px; + } + &.bk-tooltip--size-large { + width: 345px; + } + --arrow-size: 7px; &.bk-tooltip--arrow:before { diff --git a/src/components/overlays/Tooltip/Tooltip.stories.tsx b/src/components/overlays/Tooltip/Tooltip.stories.tsx index 8c679bc..91429b6 100644 --- a/src/components/overlays/Tooltip/Tooltip.stories.tsx +++ b/src/components/overlays/Tooltip/Tooltip.stories.tsx @@ -33,6 +33,24 @@ export const TooltipStandard: Story = { name: 'Tooltip', }; +export const TooltipSmall: Story = { + args: { + size: 'small', + }, +}; + +export const TooltipMedium: Story = { + args: { + size: 'medium', + }, +}; + +export const TooltipLarge: Story = { + args: { + size: 'large', + }, +}; + export const TooltipWordBreak: StoryObj = { name: 'Tooltip (word break)', render: () => ( diff --git a/src/components/overlays/Tooltip/Tooltip.tsx b/src/components/overlays/Tooltip/Tooltip.tsx index 84b9313..8c85c73 100644 --- a/src/components/overlays/Tooltip/Tooltip.tsx +++ b/src/components/overlays/Tooltip/Tooltip.tsx @@ -10,14 +10,18 @@ import cl from './Tooltip.module.scss'; export { cl as TooltipClassNames }; +export type TooltipSize = 'small' | 'medium' | 'large'; + export type TooltipProps = React.PropsWithChildren & { /** Whether this component should be unstyled. */ unstyled?: undefined | boolean, + /** Whether you want the component to have a fixed width. If unset, it will have dynamic size. */ + size?: undefined | TooltipSize, }>; /** * A tooltip. Used by `TooltipProvider` to display a tooltip popover. */ -export const Tooltip = ({ unstyled = false, ...propsRest }: TooltipProps) => { +export const Tooltip = ({ unstyled = false, size = undefined, ...propsRest }: TooltipProps) => { return (
{ bk: true, [cl['bk-tooltip']]: !unstyled, 'bk-body-text': !unstyled, + [cl['bk-tooltip--size-small']]: size === 'small', + [cl['bk-tooltip--size-medium']]: size === 'medium', + [cl['bk-tooltip--size-large']]: size === 'large', }, propsRest.className)} /> ); diff --git a/src/components/overlays/Tooltip/TooltipProvider.stories.tsx b/src/components/overlays/Tooltip/TooltipProvider.stories.tsx index 0ac9738..aaa6d6e 100644 --- a/src/components/overlays/Tooltip/TooltipProvider.stories.tsx +++ b/src/components/overlays/Tooltip/TooltipProvider.stories.tsx @@ -31,8 +31,6 @@ export default { } satisfies Meta; -export const Standard: Story = {}; - export const PlacementTop: Story = { args: { placement: 'top', @@ -57,6 +55,29 @@ export const PlacementRight: Story = { }, }; +export const TooltipSmall: Story = { + args: { + size: 'small', + tooltip: 'A small tooltip will have a fixed size, breaking line automatically if the content is too wide.', + }, +}; + +export const TooltipMedium: Story = { + args: { + placement: 'right', + size: 'medium', + tooltip: 'A medium tooltip will have a fixed size, breaking line automatically if the content is too wide.', + }, +}; + +export const TooltipLarge: Story = { + args: { + placement: 'left', + size: 'large', + tooltip: <>A large tooltip will have a fixed size,
even if the content is small., + }, +}; + /** * When a tooltip hits the viewport during scroll, it will automatically reposition to be visible. */ diff --git a/src/components/overlays/Tooltip/TooltipProvider.tsx b/src/components/overlays/Tooltip/TooltipProvider.tsx index 71bd5cf..adceb30 100644 --- a/src/components/overlays/Tooltip/TooltipProvider.tsx +++ b/src/components/overlays/Tooltip/TooltipProvider.tsx @@ -47,6 +47,7 @@ export const TooltipProvider = (props: TooltipProviderProps) => { children, tooltip, placement, + size, enablePreciseTracking = false, boundary, onTooltipActivated, @@ -109,6 +110,7 @@ export const TooltipProvider = (props: TooltipProviderProps) => { '--arrow-x': arrow?.arrowX, '--arrow-y': arrow?.arrowY, } as React.CSSProperties} + size={size} > {tooltip} {/* From 3b3d1b7ac7282bed4bc2897c3b61035cca005301 Mon Sep 17 00:00:00 2001 From: Arlindo Pereira Date: Thu, 17 Oct 2024 18:30:45 +0200 Subject: [PATCH 4/4] Tooltip inner content styling with helper elements --- .../overlays/Tooltip/Tooltip.module.scss | 19 ++++++++-- src/components/overlays/Tooltip/Tooltip.tsx | 36 +++++++++++++++++++ .../Tooltip/TooltipProvider.stories.tsx | 24 ++++++++++--- .../overlays/Tooltip/TooltipProvider.tsx | 5 +-- 4 files changed, 76 insertions(+), 8 deletions(-) diff --git a/src/components/overlays/Tooltip/Tooltip.module.scss b/src/components/overlays/Tooltip/Tooltip.module.scss index 3d0b567..e88b5b6 100644 --- a/src/components/overlays/Tooltip/Tooltip.module.scss +++ b/src/components/overlays/Tooltip/Tooltip.module.scss @@ -35,13 +35,14 @@ max-height: calc(100svh - var(--bk-sizing-2)); */ - padding: 7px 12px; + padding: bk.$spacing-4; + padding-bottom: bk.$spacing-7; border-radius: bk.$radius-s; background: bk.$theme-tooltip-background-default; border: 1px solid bk.$theme-tooltip-border-default; @include bk.text-layout; - text-align: center; + text-align: left; color: bk.$theme-tooltip-text-default; @include bk.font(bk.$font-family-body); font-size: 12px; @@ -89,6 +90,20 @@ right: calc(-1 * (calc(var(--arrow-size) + 1px))); transform: rotate(-45deg); } + + .bk-tooltip--title { + font-size: bk.$font-size-l; + font-weight: bk.$font-weight-semibold; + } + + .bk-tooltip--icon { + font-size: 18px; + margin-right: 10px; + } + + .bk-tooltip--alert { + color: bk.$theme-tooltip-text-error; + } } @position-try --bk-tooltip-position-top { diff --git a/src/components/overlays/Tooltip/Tooltip.tsx b/src/components/overlays/Tooltip/Tooltip.tsx index 8c85c73..6da409e 100644 --- a/src/components/overlays/Tooltip/Tooltip.tsx +++ b/src/components/overlays/Tooltip/Tooltip.tsx @@ -5,6 +5,8 @@ import { classNames as cx, type ComponentProps } from '../../../util/componentUtil.ts'; import * as React from 'react'; +import { Icon, IconProps } from '../../graphics/Icon/Icon.tsx'; + import cl from './Tooltip.module.scss'; @@ -37,3 +39,37 @@ export const Tooltip = ({ unstyled = false, size = undefined, ...propsRest }: To /> ); }; + +export type TooltipTitleProps = React.PropsWithChildren>; + +/** + * Tooltip title. Can be optionally used as tooltip children. + */ +export const TooltipTitle = ({ children }: TooltipTitleProps) => ( +

{children}

+); + +export type TooltipItemProps = React.PropsWithChildren & { + /** Whether the item is an alert */ + alert?: undefined | boolean; +}>; + +/** + * Tooltip item. Can be optionally used as tooltip children. + */ +export const TooltipItem = ({ alert = false, children }: TooltipItemProps) => ( +

+ {children} +

+); + +/** + * Tooltip icon. Can be optionally used as tooltip children. + */ +export const TooltipIcon = (props: IconProps) => ( + +); diff --git a/src/components/overlays/Tooltip/TooltipProvider.stories.tsx b/src/components/overlays/Tooltip/TooltipProvider.stories.tsx index aaa6d6e..1a0ad34 100644 --- a/src/components/overlays/Tooltip/TooltipProvider.stories.tsx +++ b/src/components/overlays/Tooltip/TooltipProvider.stories.tsx @@ -7,9 +7,11 @@ import type { Meta, StoryObj } from '@storybook/react'; import * as React from 'react'; import { Draggable } from '../../../util/drag.ts'; -import { OverflowTester } from '../../../util/storybook/OverflowTester.tsx'; -import { Button } from '../../actions/Button/Button.tsx'; import { TooltipProvider } from './TooltipProvider.tsx'; +import { TooltipIcon, TooltipItem, TooltipTitle } from './Tooltip.tsx'; +import { Button } from '../../actions/Button/Button.tsx'; +import { Icon } from '../../graphics/Icon/Icon.tsx'; +import { OverflowTester } from '../../../util/storybook/OverflowTester.tsx'; type TooltipProviderArgs = React.ComponentProps; @@ -58,7 +60,11 @@ export const PlacementRight: Story = { export const TooltipSmall: Story = { args: { size: 'small', - tooltip: 'A small tooltip will have a fixed size, breaking line automatically if the content is too wide.', + tooltip: <> + Title + Lorem ipsum + Lorem ipsum + , }, }; @@ -66,7 +72,17 @@ export const TooltipMedium: Story = { args: { placement: 'right', size: 'medium', - tooltip: 'A medium tooltip will have a fixed size, breaking line automatically if the content is too wide.', + tooltip: <> + Title + + + Lorem ipsum + + + + Lorem ipsum + + , }, }; diff --git a/src/components/overlays/Tooltip/TooltipProvider.tsx b/src/components/overlays/Tooltip/TooltipProvider.tsx index adceb30..bf37fc2 100644 --- a/src/components/overlays/Tooltip/TooltipProvider.tsx +++ b/src/components/overlays/Tooltip/TooltipProvider.tsx @@ -6,7 +6,7 @@ import { classNames as cx, type ClassNameArgument } from '../../../util/componen import { mergeRefs } from '../../../util/reactUtil.ts'; import * as React from 'react'; -import { type Placement, usePopover, usePopoverArrow } from '../../util/Popover/Popover.tsx'; +import { usePopover, usePopoverArrow } from '../../util/Popover/Popover.tsx'; import { type TooltipProps, TooltipClassNames, Tooltip } from './Tooltip.tsx'; @@ -25,7 +25,8 @@ export type TooltipProviderProps = Omit & { tooltip?: null | React.ReactNode, /** Where to show the tooltip relative to the anchor. */ - placement?: undefined | Placement, + // here we are not using Placement as exposed from Popover because Tooltip only supports a subset of Popover's default placements. + placement?: undefined | 'top' | 'bottom' | 'left' | 'right', /** Enable more precise tracking of the anchor, at the cost of performance. */ enablePreciseTracking?: undefined | boolean,