diff --git a/demo/DemoApp.tsx b/demo/DemoApp.tsx index ee128ea..f129f6c 100644 --- a/demo/DemoApp.tsx +++ b/demo/DemoApp.tsx @@ -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() Link test

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec accumsan + Lorem ipsum dolor sit amet, consectetur adipiscing elit aleph. 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 title

-

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.

+

+ 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. +

@@ -170,6 +180,45 @@ export function DemoApp()

Another small card

+ +

Popovers

+ + + + + + I am focused}> + + + + + You can add complex (clickable) content in me. + + + )}> + + + + + + + +
+ ()}> + {(show, hide) => ( + + )} + +
+ +

Tooltips

+ + + This is a very simple tooltip. + + +

Loaders

); } diff --git a/package.json b/package.json index de6824d..005ec65 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Components/Popovers/Popover.tsx b/src/Components/Popovers/Popover.tsx new file mode 100644 index 0000000..52a88de --- /dev/null +++ b/src/Components/Popovers/Popover.tsx @@ -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 = (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; + content?: React.ReactNode|Managed; + className?: string; + mode?: PopoverMode; + popperOptions?: Partial; +}): 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} + + + + ); +} diff --git a/src/Components/Popovers/Tooltip.tsx b/src/Components/Popovers/Tooltip.tsx new file mode 100644 index 0000000..07a8b31 --- /dev/null +++ b/src/Components/Popovers/Tooltip.tsx @@ -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 ( + + {children} + + ); +} diff --git a/src/styles/_components.less b/src/styles/_components.less index 3b213d4..6bef8a6 100644 --- a/src/styles/_components.less +++ b/src/styles/_components.less @@ -5,5 +5,6 @@ @import "components/_headings"; @import "components/_link"; @import "components/_list"; +@import "components/_popover"; @import "components/_steps"; @import "components/_table"; diff --git a/src/styles/components/_card.less b/src/styles/components/_card.less index 17aa28d..6b26038 100644 --- a/src/styles/components/_card.less +++ b/src/styles/components/_card.less @@ -29,4 +29,9 @@ { margin-bottom: 0; } + + &.popover + { + width: 20em; + } } diff --git a/src/styles/components/_popover.less b/src/styles/components/_popover.less new file mode 100644 index 0000000..12dc107 --- /dev/null +++ b/src/styles/components/_popover.less @@ -0,0 +1,2 @@ +@import "popovers/_popover"; +@import "popovers/_tooltip"; diff --git a/src/styles/components/popovers/_popover.less b/src/styles/components/popovers/_popover.less new file mode 100644 index 0000000..ff7354b --- /dev/null +++ b/src/styles/components/popovers/_popover.less @@ -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; + } +} diff --git a/src/styles/components/popovers/_tooltip.less b/src/styles/components/popovers/_tooltip.less new file mode 100644 index 0000000..68382a3 --- /dev/null +++ b/src/styles/components/popovers/_tooltip.less @@ -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; +} diff --git a/yarn.lock b/yarn.lock index 04ce3c9..f939a66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"