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:
parent
1fbc8f87c1
commit
d2d4c9cab4
10 changed files with 252 additions and 4 deletions
|
@ -6,6 +6,8 @@ import {FloppyDisk, TrashSimple, XCircle} from "@phosphor-icons/react";
|
||||||
import {Card} from "../src/Components/Card";
|
import {Card} from "../src/Components/Card";
|
||||||
import {PasswordInput} from "../src/Components/Forms/PasswordInput";
|
import {PasswordInput} from "../src/Components/Forms/PasswordInput";
|
||||||
import {RequiredField} from "../src/Components/Forms/RequiredField";
|
import {RequiredField} from "../src/Components/Forms/RequiredField";
|
||||||
|
import {Popover} from "../src/Components/Popovers/Popover";
|
||||||
|
import {Tooltip} from "../src/Components/Popovers/Tooltip";
|
||||||
|
|
||||||
export function DemoApp()
|
export function DemoApp()
|
||||||
{
|
{
|
||||||
|
@ -82,7 +84,7 @@ export function DemoApp()
|
||||||
<a href={"#"}>Link test</a>
|
<a href={"#"}>Link test</a>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<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
|
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
|
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
|
hendrerit, tempor nunc non, tempus mi. Praesent blandit varius rutrum. Nullam quis mauris eros. Vestibulum
|
||||||
|
@ -161,7 +163,15 @@ export function DemoApp()
|
||||||
<Card>
|
<Card>
|
||||||
<h3>Card title</h3>
|
<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>
|
||||||
<button type={"button"}>Button position ?</button>
|
<button type={"button"}>Button position ?</button>
|
||||||
|
@ -170,6 +180,45 @@ export function DemoApp()
|
||||||
<Card>
|
<Card>
|
||||||
<p>Another small card</p>
|
<p>Another small card</p>
|
||||||
</Card>
|
</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>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,9 @@
|
||||||
"@fontsource-variable/manrope": "^5.0.20",
|
"@fontsource-variable/manrope": "^5.0.20",
|
||||||
"@fontsource-variable/source-serif-4": "^5.0.19",
|
"@fontsource-variable/source-serif-4": "^5.0.19",
|
||||||
"@phosphor-icons/react": "^2.1.5",
|
"@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": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
|
|
96
src/Components/Popovers/Popover.tsx
Normal file
96
src/Components/Popovers/Popover.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
14
src/Components/Popovers/Tooltip.tsx
Normal file
14
src/Components/Popovers/Tooltip.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -5,5 +5,6 @@
|
||||||
@import "components/_headings";
|
@import "components/_headings";
|
||||||
@import "components/_link";
|
@import "components/_link";
|
||||||
@import "components/_list";
|
@import "components/_list";
|
||||||
|
@import "components/_popover";
|
||||||
@import "components/_steps";
|
@import "components/_steps";
|
||||||
@import "components/_table";
|
@import "components/_table";
|
||||||
|
|
|
@ -29,4 +29,9 @@
|
||||||
{
|
{
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.popover
|
||||||
|
{
|
||||||
|
width: 20em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
2
src/styles/components/_popover.less
Normal file
2
src/styles/components/_popover.less
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
@import "popovers/_popover";
|
||||||
|
@import "popovers/_tooltip";
|
27
src/styles/components/popovers/_popover.less
Normal file
27
src/styles/components/popovers/_popover.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
13
src/styles/components/popovers/_tooltip.less
Normal file
13
src/styles/components/popovers/_tooltip.less
Normal 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;
|
||||||
|
}
|
41
yarn.lock
41
yarn.lock
|
@ -599,6 +599,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@rollup/pluginutils@npm:^5.1.0":
|
||||||
version: 5.1.0
|
version: 5.1.0
|
||||||
resolution: "@rollup/pluginutils@npm:5.1.0"
|
resolution: "@rollup/pluginutils@npm:5.1.0"
|
||||||
|
@ -1766,12 +1773,14 @@ __metadata:
|
||||||
"@fontsource-variable/manrope": "npm:^5.0.20"
|
"@fontsource-variable/manrope": "npm:^5.0.20"
|
||||||
"@fontsource-variable/source-serif-4": "npm:^5.0.19"
|
"@fontsource-variable/source-serif-4": "npm:^5.0.19"
|
||||||
"@phosphor-icons/react": "npm:^2.1.5"
|
"@phosphor-icons/react": "npm:^2.1.5"
|
||||||
|
"@popperjs/core": "npm:^2.11.8"
|
||||||
"@types/react": "npm:^18.3.3"
|
"@types/react": "npm:^18.3.3"
|
||||||
"@types/react-dom": "npm:^18.3.0"
|
"@types/react-dom": "npm:^18.3.0"
|
||||||
"@vitejs/plugin-react": "npm:^4.3.0"
|
"@vitejs/plugin-react": "npm:^4.3.0"
|
||||||
less: "npm:^4.2.0"
|
less: "npm:^4.2.0"
|
||||||
react: "npm:^18.3.1"
|
react: "npm:^18.3.1"
|
||||||
react-dom: "npm:^18.3.1"
|
react-dom: "npm:^18.3.1"
|
||||||
|
react-popper: "npm:^2.3.0"
|
||||||
typescript: "npm:^5.4.5"
|
typescript: "npm:^5.4.5"
|
||||||
vite: "npm:^5.2.11"
|
vite: "npm:^5.2.11"
|
||||||
vite-plugin-dts: "npm:^3.9.1"
|
vite-plugin-dts: "npm:^3.9.1"
|
||||||
|
@ -1841,7 +1850,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"loose-envify@npm:^1.1.0":
|
"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0":
|
||||||
version: 1.4.0
|
version: 1.4.0
|
||||||
resolution: "loose-envify@npm:1.4.0"
|
resolution: "loose-envify@npm:1.4.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2245,6 +2254,27 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"react-refresh@npm:^0.14.2":
|
||||||
version: 0.14.2
|
version: 0.14.2
|
||||||
resolution: "react-refresh@npm:0.14.2"
|
resolution: "react-refresh@npm:0.14.2"
|
||||||
|
@ -2814,6 +2844,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"which@npm:^2.0.1":
|
||||||
version: 2.0.2
|
version: 2.0.2
|
||||||
resolution: "which@npm:2.0.2"
|
resolution: "which@npm:2.0.2"
|
||||||
|
|
Loading…
Reference in a new issue