From 1d389fe594a749a3c97a947bdb11d0762ec441f0 Mon Sep 17 00:00:00 2001 From: mkrause Date: Tue, 19 Nov 2024 15:00:21 +0100 Subject: [PATCH 1/2] Update Tooltip component to fix arrow position and text overflow. --- .vscode/custom.css-data.json | 15 +- .vscode/settings.json | 2 +- .../forms/controls/Checkbox/Checkbox.tsx | 6 +- .../overlays/Tooltip/Tooltip.module.scss | 177 +++++++++++++++--- .../overlays/Tooltip/Tooltip.stories.tsx | 12 +- src/components/overlays/Tooltip/Tooltip.tsx | 48 +++-- .../Tooltip/TooltipProvider.stories.tsx | 2 +- .../overlays/Tooltip/TooltipProvider.tsx | 44 ++++- src/styling/variables.scss | 14 +- 9 files changed, 261 insertions(+), 59 deletions(-) diff --git a/.vscode/custom.css-data.json b/.vscode/custom.css-data.json index 1eabd8b..f0ccbd5 100644 --- a/.vscode/custom.css-data.json +++ b/.vscode/custom.css-data.json @@ -2,14 +2,23 @@ "version": 1.1, "properties": [ { "name": "interpolate-size" }, - { "name": "container" }, // https://github.com/microsoft/vscode-css-languageservice/issues/329 + { + "name": "container", + "references": [{ "name": "", "url": "https://github.com/microsoft/vscode-css-languageservice/issues/329" }] + }, { "name": "container-name" }, { "name": "container-type" }, { "name": "position-try-fallbacks" } ], "atDirectives": [ - { "name": "@scope" }, // https://github.com/microsoft/vscode-css-languageservice/issues/406 - { "name": "@starting-style" }, // https://github.com/microsoft/vscode-css-languageservice/issues/403 + { + "name": "@scope", + "references": [{ "name": "", "url": "https://github.com/microsoft/vscode-css-languageservice/issues/406" }] + }, + { + "name": "@starting-style", + "references": [{ "name": "", "url": "https://github.com/microsoft/vscode-css-languageservice/issues/403" }] + }, { "name": "@position-try" } ], "pseudoClasses": [], diff --git a/.vscode/settings.json b/.vscode/settings.json index 895524a..2f5dfff 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { // https://github.com/Microsoft/vscode-css-languageservice/blob/main/docs/customData.md // https://stackoverflow.com/questions/42520229/vs-code-and-intellisense-for-css-grid-and-css-modules - "css.customData": [".vscode/custom.css-data.json"], + "css.customData": ["./custom.css-data.json"], // Editor (code) "editor.insertSpaces": true, // Insert spaces when pressing Tab diff --git a/src/components/forms/controls/Checkbox/Checkbox.tsx b/src/components/forms/controls/Checkbox/Checkbox.tsx index fb77e29..26dd74f 100644 --- a/src/components/forms/controls/Checkbox/Checkbox.tsx +++ b/src/components/forms/controls/Checkbox/Checkbox.tsx @@ -18,7 +18,7 @@ export type CheckboxProps = ComponentProps<'input'> & { indeterminate?: undefined | boolean, }; /** - * A simple Checkbox control, just the <input type="checkbox"> and nothing else.. + * A simple Checkbox control, just the <input type="checkbox"> and nothing else. */ export const Checkbox = (props: CheckboxProps) => { const { @@ -26,9 +26,9 @@ export const Checkbox = (props: CheckboxProps) => { indeterminate = false, ...propsRest } = props; - + const checkboxRef = React.useRef>(null); - + React.useEffect(() => { if (checkboxRef?.current) { if (indeterminate) { diff --git a/src/components/overlays/Tooltip/Tooltip.module.scss b/src/components/overlays/Tooltip/Tooltip.module.scss index e8b7dd1..40d0546 100644 --- a/src/components/overlays/Tooltip/Tooltip.module.scss +++ b/src/components/overlays/Tooltip/Tooltip.module.scss @@ -4,7 +4,7 @@ @use '../../../styling/defs.scss' as bk; -/* https://css-tricks.com/books/greatest-css-tricks/scroll-shadows */ +/* https://css-tricks.com/books/greatest-css-tricks/scroll-shadows * / @define-mixin scroll-shadows { --bgRGB: 73, 89, 99; --bg: rgb(var(--bgRGB)); @@ -18,15 +18,118 @@ background-repeat: no-repeat; background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px; background-attachment: local, local, scroll, scroll; +}*/ + +@property --bk-tooltip-background-color { syntax: ''; inherits: true; initial-value: transparent; } +@property --bk-tooltip-border-color { syntax: ''; inherits: true; initial-value: transparent; } + +/* https://css-generators.com/tooltip-speech-bubble (#21) */ +@mixin bk-tooltip-arrow { + --arrow-pos: 50%; /* Arrow position (0% = left 100% = right) */ + + --a: 90deg; // Triangle angle (how "sharp" is the arrow) + --h: 1em; // Triangle height + --p: var(--arrow-pos); // Triangle position (0% = left, 100% = right) + --r: var(--bk-tooltip-border-radius); // Border radius + --b: 1px; // Border width + --c1: var(--bk-tooltip-border-color); // Border color + --c2: var(--bk-tooltip-background-color); // Background color + + position: relative; + background: var(--c1); + + &::before { + content: ""; + position: absolute; + z-index: -1; + inset: 0; + padding: var(--b); + border-radius: inherit; + background: var(--c2) content-box; + } +} +@mixin bk-tooltip-arrow-top { + border-radius: min(var(--r),var(--p) - var(--h)*tan(var(--a)/2)) min(var(--r),100% - var(--p) - var(--h)*tan(var(--a)/2)) var(--r) var(--r)/var(--r); + clip-path: polygon(0 0,0 100%,100% 100%,100% 0, + min(100%,var(--p) + var(--h)*tan(var(--a)/2)) 0, + var(--p) calc(-1*var(--h)), + max(0% ,var(--p) - var(--h)*tan(var(--a)/2)) 0); + border-image: conic-gradient(var(--c1) 0 0) fill 0/ + 0 max(0%,100% - var(--p) - var(--h)*tan(var(--a)/2)) var(--r) max(0%,var(--p) - var(--h)*tan(var(--a)/2))/var(--h) 0 0 0; + + &::before { + clip-path: polygon(0 0,0 100%,100% 100%,100% 0, + min(100% - var(--b),var(--p) + var(--h)*tan(var(--a)/2) - var(--b)*tan(45deg - var(--a)/4)) var(--b), + var(--p) calc(var(--b)/sin(var(--a)/2) - var(--h)), + max( var(--b),var(--p) - var(--h)*tan(var(--a)/2) + var(--b)*tan(45deg - var(--a)/4)) var(--b)); + border-image: conic-gradient(var(--c2) 0 0) fill 0/ + 0 max(var(--b),100% - var(--p) - var(--h)*tan(var(--a)/2)) var(--r) max(var(--b),var(--p) - var(--h)*tan(var(--a)/2))/var(--h) 0 0 0; + } +} +@mixin bk-tooltip-arrow-right { + border-radius: var(--r)/var(--r) min(var(--r),var(--p) - var(--h)*tan(var(--a)/2)) min(var(--r),100% - var(--p) - var(--h)*tan(var(--a)/2)) var(--r); + clip-path: polygon(100% 0,0 0,0 100%,100% 100%, + 100% min(100%,var(--p) + var(--h)*tan(var(--a)/2)), + calc(100% + var(--h)) var(--p), + 100% max(0% ,var(--p) - var(--h)*tan(var(--a)/2))); + border-image: conic-gradient(var(--c1) 0 0) fill 0/ + max(0%,var(--p) - var(--h)*tan(var(--a)/2)) 0 max(0%,100% - var(--p) - var(--h)*tan(var(--a)/2)) var(--r)/0 var(--h) 0 0; + + &::before { + clip-path: polygon(100% 0,0 0,0 100%,100% 100%, + calc(100% - var(--b)) min(100% - var(--b),var(--p) + var(--h)*tan(var(--a)/2) - var(--b)*tan(45deg - var(--a)/4)), + calc(100% + var(--h) - var(--b)/sin(var(--a)/2)) var(--p), + calc(100% - var(--b)) max( var(--b),var(--p) - var(--h)*tan(var(--a)/2) + var(--b)*tan(45deg - var(--a)/4))); + border-image: conic-gradient(var(--c2) 0 0) fill 0/ + max(var(--b),var(--p) - var(--h)*tan(var(--a)/2)) 0 max(var(--b),100% - var(--p) - var(--h)*tan(var(--a)/2)) var(--r)/0 var(--h) 0 0; + } +} +@mixin bk-tooltip-arrow-bottom { + border-radius: var(--r) var(--r) min(var(--r),100% - var(--p) - var(--h)*tan(var(--a)/2)) min(var(--r),var(--p) - var(--h)*tan(var(--a)/2))/var(--r); + clip-path: polygon(0 100%,0 0,100% 0,100% 100%, + min(100%,var(--p) + var(--h)*tan(var(--a)/2)) 100%, + var(--p) calc(100% + var(--h)), + max(0% ,var(--p) - var(--h)*tan(var(--a)/2)) 100%); + border-image: conic-gradient(var(--c1) 0 0) fill 0 + / var(--r) max(0%,100% - var(--p) - var(--h)*tan(var(--a)/2)) 0 max(0%,var(--p) - var(--h)*tan(var(--a)/2))/0 0 var(--h) 0; + + &::before { + clip-path: polygon(0 100%,0 0,100% 0,100% 100%, + min(100% - var(--b),var(--p) + var(--h)*tan(var(--a)/2) - var(--b)*tan(45deg - var(--a)/4)) calc(100% - var(--b)), + var(--p) calc(100% + var(--h) - var(--b)/sin(var(--a)/2)), + max( var(--b),var(--p) - var(--h)*tan(var(--a)/2) + var(--b)*tan(45deg - var(--a)/4)) calc(100% - var(--b))); + border-image: conic-gradient(var(--c2) 0 0) fill 0 + / var(--r) max(var(--b),100% - var(--p) - var(--h)*tan(var(--a)/2)) 0 max(var(--b),var(--p) - var(--h)*tan(var(--a)/2))/0 0 var(--h) 0; + } +} +@mixin bk-tooltip-arrow-left { + border-radius: var(--r)/min(var(--r),var(--p) - var(--h)*tan(var(--a)/2)) var(--r) var(--r) min(var(--r),100% - var(--p) - var(--h)*tan(var(--a)/2)); + clip-path: polygon(0 0,100% 0,100% 100%,0 100%, + 0 min(100%,var(--p) + var(--h)*tan(var(--a)/2)), + calc(-1*var(--h)) var(--p), + 0 max(0% ,var(--p) - var(--h)*tan(var(--a)/2))); + border-image: conic-gradient(var(--c1) 0 0) fill 0/ + max(0%,var(--p) - var(--h)*tan(var(--a)/2)) var(--r) max(0%,100% - var(--p) - var(--h)*tan(var(--a)/2)) 0/0 0 0 var(--h); + + &::before { + clip-path: polygon(0 0,100% 0,100% 100%,0 100%, + var(--b) min(100% - var(--b),var(--p) + var(--h)*tan(var(--a)/2) - var(--b)*tan(45deg - var(--a)/4)), + calc(var(--b)/sin(var(--a)/2) - var(--h)) var(--p), + var(--b) max( var(--b),var(--p) - var(--h)*tan(var(--a)/2) + var(--b)*tan(45deg - var(--a)/4))); + border-image: conic-gradient(var(--c2) 0 0) fill 0/ + max(var(--b),var(--p) - var(--h)*tan(var(--a)/2)) var(--r) max(var(--b),100% - var(--p) - var(--h)*tan(var(--a)/2)) 0/0 0 0 var(--h); + } } @layer baklava.components { .bk-tooltip { @include bk.component-base(bk-tooltip); - cursor: default; + --bk-tooltip-background-color: #{bk.$theme-tooltip-background-default}; + --bk-tooltip-border-color: #{bk.$theme-tooltip-border-default}; + --bk-tooltip-border-radius: #{bk.$radius-s}; - // overflow-y: auto; + cursor: default; max-width: 30rem; max-height: 8lh; /* Show about 8 lines of text before scrolling */ @@ -38,15 +141,9 @@ 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: left; - color: bk.$theme-tooltip-text-default; - @include bk.font(bk.$font-family-body); - font-size: 12px; - + background: var(--bk-tooltip-background-color); + border: 1px solid var(--bk-tooltip-border-color); + &.bk-tooltip--small { width: 140px; } @@ -56,10 +153,22 @@ &.bk-tooltip--large { width: 345px; } - + + + /* Tooltip arrow */ + --arrow-size: 7px; - - &.bk-tooltip--arrow:before { + + --h: 6px; /* Height of the triangle. Note: must match the `offset` in `useFloating()`. */ + --b: calc(var(--h) * 2); /* Base of the triangle */ + @include bk-tooltip-arrow; + &.bk-tooltip--arrow-top { @include bk-tooltip-arrow-top; } + &.bk-tooltip--arrow-right { @include bk-tooltip-arrow-right; } + &.bk-tooltip--arrow-bottom { @include bk-tooltip-arrow-bottom; } + &.bk-tooltip--arrow-left { @include bk-tooltip-arrow-left; } + + /* + &.bk-tooltip--arrow::before { content: ''; border-bottom: 1px solid bk.$theme-tooltip-border-default; border-right: 1px solid bk.$theme-tooltip-border-default; @@ -68,44 +177,64 @@ width: calc(2 * var(--arrow-size)); height: calc(2 * var(--arrow-size)); } - &:is(.bk-tooltip--arrow-bottom, .bk-tooltip--arrow-top):before { + &:is(.bk-tooltip--arrow-bottom, .bk-tooltip--arrow-top)::before { left: calc(50% - var(--arrow-size)); } - &.bk-tooltip--arrow-bottom:before { + &.bk-tooltip--arrow-bottom::before { bottom: calc(-1 * (calc(var(--arrow-size) + 1px))); transform: rotate(45deg); } - &.bk-tooltip--arrow-top:before { + &.bk-tooltip--arrow-top::before { top: calc(-1 * (calc(var(--arrow-size) + 1px))); transform: rotate(-135deg); } - &:is(.bk-tooltip--arrow-left, .bk-tooltip--arrow-right):before { + &:is(.bk-tooltip--arrow-left, .bk-tooltip--arrow-right)::before { top: calc(50% - var(--arrow-size)); } - &.bk-tooltip--arrow-left:before { + &.bk-tooltip--arrow-left::before { left: calc(-1 * (calc(var(--arrow-size) + 1px))); transform: rotate(135deg); } - &.bk-tooltip--arrow-right:before { + &.bk-tooltip--arrow-right::before { right: calc(-1 * (calc(var(--arrow-size) + 1px))); transform: rotate(-45deg); } - + */ + + + /* Content */ + + display: flex; + flex-direction: column; + align-items: stretch; + @include bk.text-layout; + text-align: left; + color: bk.$theme-tooltip-text-default; + @include bk.font(bk.$font-family-body); + font-size: bk.$font-size-s; + + .bk-tooltip__content { + overflow-y: auto; + } + .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; } } + + /* Anchor positioning */ + @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/Tooltip.stories.tsx b/src/components/overlays/Tooltip/Tooltip.stories.tsx index 1318e70..6908a72 100644 --- a/src/components/overlays/Tooltip/Tooltip.stories.tsx +++ b/src/components/overlays/Tooltip/Tooltip.stories.tsx @@ -4,11 +4,12 @@ import type { Meta, StoryObj } from '@storybook/react'; +import { classNames as cx } from '../../../util/componentUtil.ts'; import * as React from 'react'; import { OverflowTester } from '../../../util/storybook/OverflowTester.tsx'; import { Button } from '../../actions/Button/Button.tsx'; -import { Tooltip } from './Tooltip.tsx'; +import { TooltipClassNames, Tooltip } from './Tooltip.tsx'; type TooltipArgs = React.ComponentProps; @@ -31,6 +32,15 @@ export default { export const TooltipStandard: Story = { name: 'Tooltip', + args: { + style: {}, + }, +}; + +export const TooltipWithArrow: Story = { + args: { + arrow: 'bottom', + }, }; export const TooltipSmall: Story = { diff --git a/src/components/overlays/Tooltip/Tooltip.tsx b/src/components/overlays/Tooltip/Tooltip.tsx index ee7d155..c6530b6 100644 --- a/src/components/overlays/Tooltip/Tooltip.tsx +++ b/src/components/overlays/Tooltip/Tooltip.tsx @@ -2,7 +2,8 @@ |* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { classNames as cx, type ComponentProps } from '../../../util/componentUtil.ts'; +import { assertUnreachable } from '../../../util/types.ts'; +import { ClassNameArgument, classNames as cx, type ComponentProps } from '../../../util/componentUtil.ts'; import * as React from 'react'; import { Icon, IconProps } from '../../graphics/Icon/Icon.tsx'; @@ -14,29 +15,54 @@ export { cl as TooltipClassNames }; export type TooltipSize = 'small' | 'medium' | 'large'; +export type TooltipArrowPosition = 'top' | 'right' | 'bottom' | 'left'; + 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, + + /* If specified, will render an arrow at the given position. */ + arrow?: undefined | TooltipArrowPosition, }>; /** * A tooltip. Used by `TooltipProvider` to display a tooltip popover. */ -export const Tooltip = ({ unstyled = false, size = undefined, ...propsRest }: TooltipProps) => { +export const Tooltip = ({ children, unstyled = false, arrow, size = undefined, ...propsRest }: TooltipProps) => { + const arrowClassNames = ((): ClassNameArgument => { + if (!arrow) { return; } + + switch (arrow) { + case 'top': return cx(cl['bk-tooltip--arrow'], cl['bk-tooltip--arrow-top']); + case 'right': return cx(cl['bk-tooltip--arrow'], cl['bk-tooltip--arrow-right']); + case 'bottom': return cx(cl['bk-tooltip--arrow'], cl['bk-tooltip--arrow-bottom']); + case 'left': return cx(cl['bk-tooltip--arrow'], cl['bk-tooltip--arrow-left']); + default: return assertUnreachable(arrow); + } + })(); + return (
+ className={cx( + { + bk: true, + [cl['bk-tooltip']]: !unstyled, + [cl['bk-tooltip--small']]: size === 'small', + [cl['bk-tooltip--medium']]: size === 'medium', + [cl['bk-tooltip--large']]: size === 'large', + }, + arrowClassNames, + propsRest.className, + )} + > +
+ {children} +
+
); }; diff --git a/src/components/overlays/Tooltip/TooltipProvider.stories.tsx b/src/components/overlays/Tooltip/TooltipProvider.stories.tsx index d1012d0..70c3570 100644 --- a/src/components/overlays/Tooltip/TooltipProvider.stories.tsx +++ b/src/components/overlays/Tooltip/TooltipProvider.stories.tsx @@ -26,7 +26,7 @@ export default { argTypes: { }, args: { - tooltip: 'This is a tooltip', + tooltip: <>This is a tooltip, children: (props) =>