Skip to content

Commit

Permalink
Merge pull request #8 from fortanix/feature/tooltip
Browse files Browse the repository at this point in the history
Tooltip component
  • Loading branch information
nighto authored Oct 29, 2024
2 parents b21a611 + 66e5682 commit 2ca638d
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 55 deletions.
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--small {
width: 140px;
}
&.bk-tooltip--medium {
width: 225px;
}
&.bk-tooltip--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--small']]: size === 'small',
[cl['bk-tooltip--medium']]: size === 'medium',
[cl['bk-tooltip--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>
);

export type TooltipItemProps = React.PropsWithChildren<ComponentProps<'p'> & {
/** 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

0 comments on commit 2ca638d

Please sign in to comment.