Skip to content

Commit

Permalink
14.6.8 - pricing card title tooltips, support for multiple tooltip or…
Browse files Browse the repository at this point in the history
…ientations
  • Loading branch information
jamiehenson committed Oct 10, 2024
1 parent 028abfd commit ae871fc
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 25 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ably/ui",
"version": "14.6.7",
"version": "14.6.8",
"description": "Home of the Ably design system library ([design.ably.com](https://design.ably.com)). It provides a showcase, development/test environment and a publishing pipeline for different distributables.",
"repository": {
"type": "git",
Expand Down
23 changes: 17 additions & 6 deletions src/core/Pricing/PricingCards.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { Fragment, useEffect, useRef, useState } from "react";
import throttle from "lodash.throttle";
import type { PricingDataFeature } from "./types";
import { determineThemeColor } from "../styles/colors/utils";
import { ColorClass, Theme } from "../styles/colors/types";
import Icon from "../Icon";
import FeaturedLink from "../FeaturedLink";
import { IconName } from "../Icon/types";
import throttle from "lodash.throttle";
import Tooltip from "../Tooltip";

export type PricingCardsProps = {
data: PricingDataFeature[];
Expand Down Expand Up @@ -105,11 +106,21 @@ const PricingCards = ({
className={`relative z-10 flex flex-col gap-24 ${delimiter ? "@[520px]:flex-1 @[920px]:flex-none" : ""}`}
>
<div>
<p
className={`mb-12 ${title.className ?? ""} ${t(title.color ?? "text-neutral-000")}`}
>
{title.content}
</p>
<div className="flex items-center mb-12">
<p
className={`${title.className ?? ""} ${t(title.color ?? "text-neutral-000")}`}
>
{title.content}
</p>
{title.tooltip ? (
<Tooltip
theme={theme}
interactive={typeof title.tooltip !== "string"}
>
{title.tooltip}
</Tooltip>
) : null}
</div>
<p
className={`ui-text-p1 ${description.className ?? ""} ${t(description.color ?? "text-neutral-000")} min-h-20`}
style={{ height: descriptionHeight }}
Expand Down
19 changes: 19 additions & 0 deletions src/core/Pricing/data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,16 @@ export const consumptionData: PricingDataFeature[] = [
content: "Channels",
className: "ui-text-h3",
color: "text-neutral-000",
tooltip: (
<p>
Ably aggregates all its data into named units of distribution,
referred to as “channels”. These are used to transmit from one device
to another.{" "}
<a href="/docs/channels" className="ui-link">
Find out more.
</a>
</p>
),
},
description: {
content:
Expand Down Expand Up @@ -255,6 +265,15 @@ export const consumptionData: PricingDataFeature[] = [
content: "Connections",
className: "ui-text-h3",
color: "text-neutral-000",
tooltip: (
<p>
Clients establish and maintain a connection to the Ably service using
the most efficient transport available, typically WebSockets.{" "}
<a href="/docs/connect" className="ui-link">
Find out more.
</a>
</p>
),
},
description: {
content:
Expand Down
1 change: 1 addition & 0 deletions src/core/Pricing/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ type PricingDataHeader = {
content: string;
className?: string;
color?: ColorClass;
tooltip?: string | ReactNode;
};

type PricingDataFeatureCta = {
Expand Down
66 changes: 48 additions & 18 deletions src/core/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const Tooltip = ({
}: PropsWithChildren<TooltipProps>) => {
const [open, setOpen] = useState(false);
const [fadeOut, setFadeOut] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [position, setPosition] = useState({ x: 0, y: 0, orientation: "top" });
const offset = 8;
const reference = useRef<HTMLButtonElement>(null);
const floating = useRef<HTMLDivElement>(null);
Expand All @@ -47,6 +47,7 @@ const Tooltip = ({
const referenceRect = reference.current?.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let orientation = "top";

if (floatingRect && referenceRect) {
let x =
Expand All @@ -60,27 +61,30 @@ const Tooltip = ({
// Adjust if tooltip goes off the right edge
if (x + floatingRect.width > viewportWidth + window.scrollX) {
x = viewportWidth + window.scrollX - floatingRect.width - offset;
orientation = "left";
}

// Adjust if tooltip goes off the left edge
if (x < window.scrollX) {
x = window.scrollX + offset;
orientation = "right";
}

// Adjust if tooltip goes off the top edge
if (y < window.scrollY) {
y = referenceRect.bottom + offset + window.scrollY;
orientation = "bottom";
}

// Adjust if tooltip goes off the bottom edge
if (y + floatingRect.height > viewportHeight + window.scrollY) {
y = referenceRect.top - floatingRect.height - offset + window.scrollY;
}

setPosition({ x, y });
setPosition({ x, y, orientation });
}
} else {
setPosition({ x: 0, y: 0 });
setPosition({ x: 0, y: 0, orientation: "top" });
}

return () => {
Expand All @@ -98,38 +102,62 @@ const Tooltip = ({
}, 250);
};

const cursorHeadsNorth = (
const cursorTowardsTooltip = (
event: MouseEvent,
ref: RefObject<HTMLButtonElement>,
) => {
if (ref.current) {
const { clientX, clientY } = event;
const { x, y, width } = ref.current.getBoundingClientRect();
return clientX >= x && clientX <= x + width && clientY < y;
if (!ref.current) {
return false;
}

return false;
const { clientX, clientY } = event;
const { x, y, width, height } = ref.current.getBoundingClientRect();
const { orientation } = position;

switch (orientation) {
case "top":
return clientX >= x && clientX <= x + width && clientY < y;
case "left":
return clientY >= y && clientY <= y + height && clientX < x;
case "right":
return clientY >= y && clientY <= y + height && clientX > x + width;
case "bottom":
return clientX >= x && clientX <= x + width && clientY > y + height;
default:
return false;
}
};

const fadeOutIfNotWithinTrigger = (event: MouseEvent) => {
if (!reference.current) return;

const { clientX, clientY } = event;
const { x, y, width, height } = reference.current.getBoundingClientRect();
const withinBounds =
clientX >= x &&
clientX <= x + width &&
clientY >= y &&
clientY <= y + height;

if (!withinBounds) {
initiateFadeOut();
}
};

return (
<div
{...rest}
className={`relative inline-block align-top h-16 ${
rest?.className ?? ""
}`}
>
<div {...rest} className={`inline-flex ml-8 ${rest?.className ?? ""}`}>
<button
onMouseEnter={() => setOpen(true)}
onMouseLeave={(event) => {
if (!interactive || !cursorHeadsNorth(event, reference)) {
if (!interactive || !cursorTowardsTooltip(event, reference)) {
initiateFadeOut();
}
}}
type="button"
ref={reference}
aria-describedby="tooltip"
{...triggerProps}
className={`ml-8 p-0 relative top-1 focus:outline-none ${
className={`p-0 relative focus:outline-none h-[1rem] ${
triggerProps?.className ?? ""
}`}
>
Expand All @@ -147,7 +175,9 @@ const Tooltip = ({
<div
role="tooltip"
ref={floating}
onMouseLeave={initiateFadeOut}
onMouseLeave={(event) =>
setTimeout(() => fadeOutIfNotWithinTrigger(event), 250)
}
style={{
top: position.y,
left: position.x,
Expand Down

0 comments on commit ae871fc

Please sign in to comment.