Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tooltip component #8

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 77 additions & 48 deletions src/components/overlays/Tooltip/Tooltip.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -62,29 +35,77 @@
max-height: calc(100svh - var(--bk-sizing-2));
*/

padding: 7px 12px;
border-radius: 2px;
background: var(--bk-tooltip-background-color);

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;
color: var(--bk-tooltip-text-color);
text-align: left;
color: bk.$theme-tooltip-text-default;
@include bk.font(bk.$font-family-body);
font-size: 12px;

&: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 */

&.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 {
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;
&:is(.bk-tooltip--arrow-bottom, .bk-tooltip--arrow-top):before {
left: calc(50% - var(--arrow-size));
}
&.bk-tooltip--arrow-bottom {
@include bk-tooltip-arrow-bottom;
&.bk-tooltip--arrow-bottom:before {
bottom: calc(-1 * (calc(var(--arrow-size) + 1px)));
transform: rotate(45deg);
}
&.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 {
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);
}

.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 {
margin-top: var(--bk-layout-header-height); /* Compensate for layout header */
margin-bottom: 6px;
Expand All @@ -98,13 +119,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%));

Expand Down
18 changes: 18 additions & 0 deletions src/components/overlays/Tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof Tooltip> = {
name: 'Tooltip (word break)',
render: () => (
Expand Down
45 changes: 44 additions & 1 deletion src/components/overlays/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@
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';


export { cl as TooltipClassNames };

export type TooltipSize = 'small' | 'medium' | 'large';

export type TooltipProps = React.PropsWithChildren<ComponentProps<'div'> & {
/** 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 (
<div
role="tooltip" // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tooltip_role
Expand All @@ -26,7 +32,44 @@ export const Tooltip = ({ unstyled = false, ...propsRest }: TooltipProps) => {
bk: true,
[cl['bk-tooltip']]: !unstyled,
'bk-body-text': !unstyled,
[cl['bk-tooltip--size-small']]: size === 'small',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bk-tooltip--size-small can be bk-tooltip--small if we follow the same way as different components like bk-spinner--small or bk-link--small format.

[cl['bk-tooltip--size-medium']]: size === 'medium',
[cl['bk-tooltip--size-large']]: size === 'large',
}, propsRest.className)}
/>
);
};

export type TooltipTitleProps = React.PropsWithChildren<ComponentProps<'h1'>>;

/**
* Tooltip title. Can be optionally used as tooltip children.
*/
export const TooltipTitle = ({ children }: TooltipTitleProps) => (
<h1 className={cl['bk-tooltip--title']}>{children}</h1>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think class name bk-tooltip--title should be bk-tooltip__title in BEM (https://getbem.com/naming/).
The same goes for bk-tooltip--alert and bk-tooltip--icon in the code below.

);

export type TooltipItemProps = React.PropsWithChildren<ComponentProps<'h1'> & {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ComponentProps<'h1'> should be ComponentProps<'p'> as TooltipItem has p element?

/** 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) => (
<p
className={cx({
[cl['bk-tooltip--alert']]: alert,
})}
>
{children}
</p>
);

/**
* Tooltip icon. Can be optionally used as tooltip children.
*/
export const TooltipIcon = (props: IconProps) => (
<Icon className={cl['bk-tooltip--icon']} {...props} />
);
68 changes: 64 additions & 4 deletions src/components/overlays/Tooltip/TooltipProvider.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
|* 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';

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<typeof TooltipProvider>;
Expand All @@ -32,7 +33,66 @@ export default {
} satisfies Meta<TooltipProviderArgs>;


export const Standard: Story = {};
export const PlacementTop: Story = {
args: {
placement: 'top',
},
};

export const PlacementBottom: Story = {
args: {
placement: 'bottom',
},
};

export const PlacementLeft: Story = {
args: {
placement: 'left',
},
};

export const PlacementRight: Story = {
args: {
placement: 'right',
},
};

export const TooltipSmall: Story = {
args: {
size: 'small',
tooltip: <>
<TooltipTitle>Title</TooltipTitle>
<TooltipItem>Lorem ipsum</TooltipItem>
<TooltipItem>Lorem ipsum</TooltipItem>
</>,
},
};

export const TooltipMedium: Story = {
args: {
placement: 'right',
size: 'medium',
tooltip: <>
<TooltipTitle>Title</TooltipTitle>
<TooltipItem alert={true}>
<TooltipIcon icon="alert" />
Lorem ipsum
</TooltipItem>
<TooltipItem>
<TooltipIcon icon="copy" />
Lorem ipsum
</TooltipItem>
</>,
},
};

export const TooltipLarge: Story = {
args: {
placement: 'left',
size: 'large',
tooltip: <>A large tooltip will have a fixed size,<br />even if the content is small.</>,
},
};

/**
* When a tooltip hits the viewport during scroll, it will automatically reposition to be visible.
Expand Down
Loading