Add popovers and tooltips.

+ Add popovers to show cards around an element: always, on hover, on focus, on click, with customized behavior.
+ Add tooltips as specific hover popovers.
This commit is contained in:
Madeorsk 2024-06-09 12:13:42 +02:00
parent 1fbc8f87c1
commit d2d4c9cab4
Signed by: Madeorsk
SSH key fingerprint: SHA256:J9G0ofIOLKf7kyS2IfrMqtMaPdfsk1W02+oGueZzDDU
10 changed files with 252 additions and 4 deletions

View file

@ -6,6 +6,8 @@ import {FloppyDisk, TrashSimple, XCircle} from "@phosphor-icons/react";
import {Card} from "../src/Components/Card";
import {PasswordInput} from "../src/Components/Forms/PasswordInput";
import {RequiredField} from "../src/Components/Forms/RequiredField";
import {Popover} from "../src/Components/Popovers/Popover";
import {Tooltip} from "../src/Components/Popovers/Tooltip";
export function DemoApp()
{
@ -82,7 +84,7 @@ export function DemoApp()
<a href={"#"}>Link test</a>
</div>
<p>
<strong>Lorem ipsum</strong> dolor <code>sit amet</code>, <em>consectetur</em> adipiscing elit. Donec accumsan
<strong>Lorem ipsum</strong> dolor <code>sit amet</code>, <em>consectetur</em> adipiscing elit <a href={"https://aleph.land"} target={"_blank"}>aleph</a>. Donec accumsan
pulvinar felis, vitae eleifend augue lacinia tempus. Integer nec iaculis ante. Duis a quam urna. Nullam
tincidunt rutrum felis, a efficitur enim facilisis sit amet. Quisque dictum semper sagittis. Maecenas in orci
hendrerit, tempor nunc non, tempus mi. Praesent blandit varius rutrum. Nullam quis mauris eros. Vestibulum
@ -161,7 +163,15 @@ export function DemoApp()
<Card>
<h3>Card title</h3>
<p>Donec lobortis quam sapien, et efficitur dolor laoreet ut. Ut pretium, lacus at bibendum rutrum, nibh nibh scelerisque nisi, nec semper dolor turpis sed tortor. Nulla massa sapien, accumsan id vestibulum et, elementum eget metus. Morbi quis bibendum purus. Nunc at fermentum tortor. Quisque viverra diam in sem auctor blandit. Vestibulum dignissim bibendum nunc, non tristique quam sollicitudin eu. Ut feugiat lectus tellus, tempus viverra sapien aliquet vel. Morbi ac est mauris. Praesent facilisis ut tellus at cursus. Aenean placerat nulla non mi vulputate hendrerit. Praesent fermentum dui eu gravida pharetra. Quisque rhoncus, magna non congue egestas, leo lorem malesuada felis, eu imperdiet orci magna ultrices est.</p>
<p>
Donec lobortis quam sapien, et efficitur dolor laoreet ut. Ut pretium, lacus at bibendum rutrum, nibh nibh
scelerisque nisi, nec semper dolor turpis sed tortor. Nulla massa sapien, accumsan id vestibulum et, elementum
eget metus. Morbi quis bibendum purus. Nunc at fermentum tortor. Quisque viverra diam in sem auctor blandit.
Vestibulum dignissim bibendum nunc, non tristique quam sollicitudin eu. Ut feugiat lectus tellus, tempus
viverra sapien aliquet vel. Morbi ac est mauris. Praesent facilisis ut tellus at cursus. Aenean placerat nulla
non mi vulputate hendrerit. Praesent fermentum dui eu gravida pharetra. Quisque rhoncus, magna non congue
egestas, leo lorem malesuada felis, eu imperdiet orci magna ultrices est.
</p>
<button type={"button"}>Button position ?</button>
<button type={"button"}>Button position ?</button>
@ -170,6 +180,45 @@ export function DemoApp()
<Card>
<p>Another small card</p>
</Card>
<h2>Popovers</h2>
<Popover mode={"hover"} content={"Do you see me?"}>
<button type={"button"}>Hover me!</button>
</Popover>
<Popover mode={"focus"} content={<>I am <strong>focused</strong></>}>
<button>Focus me!</button>
</Popover>
<Popover mode={"click"} content={(
<div>
You can add complex (clickable) content in me.
<button type={"button"}>OK</button>
</div>
)}>
<button>Click me!</button>
</Popover>
<Popover content={"I am always shown."} popperOptions={{ placement: "top" }}>
<button>Why always me?</button>
</Popover>
<div>
<Popover mode={"managed"} content={(show, hide) => (<button onClick={hide}>I can hide the popover!</button>)}>
{(show, hide) => (
<button type={"button"} onClick={show}>Customized behavior</button>
)}
</Popover>
</div>
<h2>Tooltips</h2>
<Card>
This is a very simple <Tooltip content={"I am a beautiful simple tooltip."}><a>tooltip</a></Tooltip>.
</Card>
<h2>Loaders</h2>
</main>
);
}

View file

@ -15,7 +15,9 @@
"@fontsource-variable/manrope": "^5.0.20",
"@fontsource-variable/source-serif-4": "^5.0.19",
"@phosphor-icons/react": "^2.1.5",
"react": "^18.3.1"
"@popperjs/core": "^2.11.8",
"react": "^18.3.1",
"react-popper": "^2.3.0"
},
"devDependencies": {
"@types/react": "^18.3.3",

View file

@ -0,0 +1,96 @@
import React, {useMemo, useRef, useState} from "react";
import {usePopper} from "react-popper";
import * as PopperJS from "@popperjs/core";
import {Card} from "../Card";
/**
* Fully managed popover content function.
*/
export type Managed<T = React.ReactElement|React.ReactNode> = (show: () => void, hide: () => void) => T;
/**
* Allowed popover modes.
*/
export type PopoverMode = "always"|"click"|"hover"|"focus"|"managed";
/**
* A component to show something next to an element.
*/
export function Popover({children, content, className, mode, popperOptions}: {
children: React.ReactElement|Managed<React.ReactElement>;
content?: React.ReactNode|Managed<React.ReactNode>;
className?: string;
mode?: PopoverMode;
popperOptions?: Partial<PopperJS.Options>;
}): React.ReactElement
{
// By default, use "always" mode.
if (!mode) mode = "always";
// Followed show status.
const [shown, setShown] = useState(false);
// Create show / hide functions.
const show = useMemo(() => (!shown ? () => setShown(true) : () => {}), [shown]);
const hide = useMemo(() => (shown ? () => setShown(false) : () => {}), [shown]);
// If show mode is "always", always show the popover after render.
setTimeout(() => {
if (mode == "always") setShown(true);
}, 0);
// HTML elements references.
const referenceElement = useRef();
const popperElement = useRef();
// Change the child element to use the reference.
const referencedChild = useMemo(() => (
React.cloneElement(
// Render the children if a managed popover function is passed.
typeof children == "function" ? children(show, hide) : children,
Object.assign(
{
ref: referenceElement,
},
// Pass click event if click mode is enabled.
mode == "click" ? {
onClick: (event: React.MouseEvent): void => {
setShown(!shown);
},
} : {},
// Pass focus events if focus mode is enabled.
mode == "focus" ? {
onFocus: show,
onBlur: hide,
} : {},
// Pass the hover event if hover mode is enabled.
mode == "hover" ? {
onMouseEnter: show,
onMouseOut: hide,
} : {},
))
), [mode, shown, show, hide, children]);
// Update popover content.
const popoverContent = useMemo(() => (
// Render the children if a managed popover function is passed.
typeof content == "function" ? content(show, hide) : content
), [shown, show, hide, content]);
// Initialize popper.
const { styles, attributes } = usePopper(
referenceElement?.current, popperElement?.current, popperOptions
);
return (
<>
{referencedChild}
<div ref={popperElement} style={styles.popper} {...attributes.popper} className={"popover"} hidden={!shown}>
<Card className={`popover${className ? ` ${className}` : ""}`}>
{popoverContent}
</Card>
</div>
</>
);
}

View file

@ -0,0 +1,14 @@
import React from "react";
import {Popover} from "./Popover";
export function Tooltip({children, content}: {
children: React.ReactElement;
content: React.ReactNode;
}): React.ReactElement
{
return (
<Popover mode={"hover"} content={content} className={"tooltip"} popperOptions={{ placement: "top" }}>
{children}
</Popover>
);
}

View file

@ -5,5 +5,6 @@
@import "components/_headings";
@import "components/_link";
@import "components/_list";
@import "components/_popover";
@import "components/_steps";
@import "components/_table";

View file

@ -29,4 +29,9 @@
{
margin-bottom: 0;
}
&.popover
{
width: 20em;
}
}

View file

@ -0,0 +1,2 @@
@import "popovers/_popover";
@import "popovers/_tooltip";

View file

@ -0,0 +1,27 @@
:not(.card).popover
{
transition: opacity 0.2s ease;
display: flex;
z-index: 1;
opacity: 1;
&[data-popper-placement="top"], &[data-popper-placement="bottom"]
{ justify-content: center; }
&[data-popper-placement="right"]
{ justify-content: left; }
&[data-popper-placement="left"]
{ justify-content: right; }
> .card.popover
{
margin: 0.4em;
}
&[hidden]
{
opacity: 0;
pointer-events: none;
}
}

View file

@ -0,0 +1,13 @@
.card.popover.tooltip
{
width: unset;
max-width: 20em;
padding: 0.5em;
border-color: var(--foreground-darker);
background: var(--foreground-lightest);
color: var(--background);
font-size: 0.9em;
text-align: center;
}

View file

@ -599,6 +599,13 @@ __metadata:
languageName: node
linkType: hard
"@popperjs/core@npm:^2.11.8":
version: 2.11.8
resolution: "@popperjs/core@npm:2.11.8"
checksum: 10c0/4681e682abc006d25eb380d0cf3efc7557043f53b6aea7a5057d0d1e7df849a00e281cd8ea79c902a35a414d7919621fc2ba293ecec05f413598e0b23d5a1e63
languageName: node
linkType: hard
"@rollup/pluginutils@npm:^5.1.0":
version: 5.1.0
resolution: "@rollup/pluginutils@npm:5.1.0"
@ -1766,12 +1773,14 @@ __metadata:
"@fontsource-variable/manrope": "npm:^5.0.20"
"@fontsource-variable/source-serif-4": "npm:^5.0.19"
"@phosphor-icons/react": "npm:^2.1.5"
"@popperjs/core": "npm:^2.11.8"
"@types/react": "npm:^18.3.3"
"@types/react-dom": "npm:^18.3.0"
"@vitejs/plugin-react": "npm:^4.3.0"
less: "npm:^4.2.0"
react: "npm:^18.3.1"
react-dom: "npm:^18.3.1"
react-popper: "npm:^2.3.0"
typescript: "npm:^5.4.5"
vite: "npm:^5.2.11"
vite-plugin-dts: "npm:^3.9.1"
@ -1841,7 +1850,7 @@ __metadata:
languageName: node
linkType: hard
"loose-envify@npm:^1.1.0":
"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0":
version: 1.4.0
resolution: "loose-envify@npm:1.4.0"
dependencies:
@ -2245,6 +2254,27 @@ __metadata:
languageName: node
linkType: hard
"react-fast-compare@npm:^3.0.1":
version: 3.2.2
resolution: "react-fast-compare@npm:3.2.2"
checksum: 10c0/0bbd2f3eb41ab2ff7380daaa55105db698d965c396df73e6874831dbafec8c4b5b08ba36ff09df01526caa3c61595247e3269558c284e37646241cba2b90a367
languageName: node
linkType: hard
"react-popper@npm:^2.3.0":
version: 2.3.0
resolution: "react-popper@npm:2.3.0"
dependencies:
react-fast-compare: "npm:^3.0.1"
warning: "npm:^4.0.2"
peerDependencies:
"@popperjs/core": ^2.0.0
react: ^16.8.0 || ^17 || ^18
react-dom: ^16.8.0 || ^17 || ^18
checksum: 10c0/23f93540537ca4c035425bb8d5e51b11131fbc921d7ac1d041d0ae557feac8c877f3a012d36b94df8787803f52ed81e6df9257ac9e58719875f7805518d6db3f
languageName: node
linkType: hard
"react-refresh@npm:^0.14.2":
version: 0.14.2
resolution: "react-refresh@npm:0.14.2"
@ -2814,6 +2844,15 @@ __metadata:
languageName: node
linkType: hard
"warning@npm:^4.0.2":
version: 4.0.3
resolution: "warning@npm:4.0.3"
dependencies:
loose-envify: "npm:^1.0.0"
checksum: 10c0/aebab445129f3e104c271f1637fa38e55eb25f968593e3825bd2f7a12bd58dc3738bb70dc8ec85826621d80b4acfed5a29ebc9da17397c6125864d72301b937e
languageName: node
linkType: hard
"which@npm:^2.0.1":
version: 2.0.2
resolution: "which@npm:2.0.2"