packages/core/src/features/viz/lib/hover-card.tsx
This is the source snapshot used to build these API details. View this revision on GitHub.
1 import {
2 Group,
3 Rectangle,
4 definePrimitive,
5 resolveSignalValue,
6 resolveBox,
7 resolveLength,
8 type BoxStyle,
9 type PibblNode,
10 type FillStyle,
11 type StrokeStyle,
12 type SignalValue,
13 } from "@pibbl/core";
14 import {
15 pushLayoutBox,
16 setCurrentComponentAfterRender,
17 type SignalStyle,
18 } from "@pibbl/core/internal";
19 import type { HoverPoint } from "./hover-types.js";
20 import {
21 placeHoverCard,
22 type HoverBounds,
23 type HoverPlacement,
24 } from "./hover-placement.js";
25 export type { HoverBounds, HoverPlacement } from "./hover-placement.js";
26 /**
27 * Supported geometry and presentation properties for HoverCard.
28 *
29 * @see {@link BoxStyle}
30 * @see {@link FillStyle}
31 * @see {@link StrokeStyle}
32 * @see {@link HoverCardProps}
33 */
34 export interface HoverCardStyle {
35 /** Horizontal extent in the units of the containing geometry or surface. See {@link BoxStyle}. */
36 readonly width: Exclude<BoxStyle["width"], undefined | "auto">;
37 /** Vertical extent in the units of the containing geometry or surface. See {@link BoxStyle}. */
38 readonly height: Exclude<BoxStyle["height"], undefined | "auto">;
39 /** Insets between the border box and content box. See {@link BoxStyle}. */
40 readonly padding?: BoxStyle["padding"];
41 /** Paint used for the interior. See {@link FillStyle}. */
42 readonly fill?: FillStyle;
43 /** Paint used for the outline. See {@link StrokeStyle}. */
44 readonly stroke?: StrokeStyle;
45 /** Width of the painted outline. See {@link HoverCardStyle}. */
46 readonly strokeWidth?: number;
47 /**
48 * Uniform corner radius used where no per-corner override is supplied. See
49 * {@link HoverCardStyle}.
50 */
51 readonly cornerRadius?: number;
52 /** Opacity of the painted result. See {@link HoverCardStyle}. */
53 readonly opacity?: number;
54 /**
55 * Not accepted in this variant; use the alternative fields instead. See {@link HoverCardStyle}.
56 */
57 readonly filter?: never;
58 /**
59 * Not accepted in this variant; use the alternative fields instead. See {@link HoverCardStyle}.
60 */
61 readonly custom?: never;
62 }
63 /**
64 * Authored inputs for HoverCard, including the declared data and presentation options.
65 *
66 * @see {@link SignalValue}
67 * @see {@link HoverPoint}
68 * @see {@link HoverBounds}
69 * @see {@link HoverPlacement}
70 * @see {@link SignalStyle}
71 * @see {@link HoverCardStyle}
72 * @see {@link PibblNode}
73 * @see {@link HoverCard}
74 */
75 export interface HoverCardProps {
76 /**
77 * Logical point to which the content or result is anchored. See {@link SignalValue},
78 * {@link HoverPoint}.
79 */
80 readonly anchor: SignalValue<HoverPoint>;
81 /** Clearance around the anchor before placing the item. See {@link SignalValue}. */
82 readonly anchorRadius?: SignalValue<number>;
83 /** Spacing between adjacent items. See {@link SignalValue}. */
84 readonly gap?: SignalValue<number>;
85 /**
86 * Logical region constraining the content or query. See {@link SignalValue},
87 * {@link HoverBounds}.
88 */
89 readonly bounds?: SignalValue<HoverBounds>;
90 /**
91 * Preferred placement relative to the anchor or containing geometry. See {@link HoverPlacement}
92 * .
93 */
94 readonly placement?: HoverPlacement;
95 /**
96 * Declared presentation and layout properties. See {@link SignalValue}, {@link SignalStyle},
97 * {@link HoverCardStyle}.
98 */
99 readonly style: SignalValue<SignalStyle<HoverCardStyle>>;
100 /** Descendant content or the callback that supplies it. See {@link PibblNode}. */
101 readonly children?: PibblNode;
102 }
103 type CardInputs = Omit<
104 HoverCardProps,
105 "style" | "anchor" | "anchorRadius" | "gap" | "bounds"
106 > & {
107 anchor: HoverPoint;
108 anchorRadius: number;
109 gap: number;
110 bounds?: HoverBounds;
111 };
112 interface CardResolved {
113 readonly custom?: never;
114 readonly box: ReturnType<typeof resolveBox>;
115 readonly bounds: HoverBounds;
116 readonly fill: FillStyle;
117 readonly stroke: StrokeStyle;
118 readonly strokeWidth: number;
119 readonly cornerRadius: number;
120 readonly opacity: number;
121 }
122 function Content({
123 width,
124 height,
125 children,
126 }: {
127 width: number;
128 height: number;
129 children?: PibblNode;
130 }) {
131 // Establish both allocation and percentage basis for arbitrary composed nodes;
132 // this is a content box, not a layout parent that requires child dimensions.
133 const box = { x: 0, y: 0, width, height };
134 const outer = pushLayoutBox(box);
135 let inner: (() => void) | undefined;
136 try {
137 inner = pushLayoutBox(box);
138 setCurrentComponentAfterRender(() => {
139 inner!();
140 outer();
141 });
142 } catch (error) {
143 inner?.();
144 outer();
145 throw error;
146 }
147 return children;
148 }
149 const Card = definePrimitive<
150 CardInputs,
151 HoverCardStyle,
152 HoverCardStyle,
153 CardResolved
154 >(
155 function renderHoverCard(props, style, context) {
156 context.globalAlpha *= style.opacity;
157 const { borderBox: box, contentBox: content } = style.box;
158 const at = placeHoverCard({
159 anchor: props.anchor,
160 anchorRadius: props.anchorRadius,
161 gap: props.gap,
162 bounds: props.bounds ?? style.bounds,
163 width: box.width,
164 height: box.height,
165 strokeWidth: style.stroke ? style.strokeWidth : 0,
166 placement: props.placement ?? "auto",
167 });
168 return (
169 <Group style={{ translateX: at.x, translateY: at.y }}>
170 <Rectangle
171 pointerEvents="none"
172 style={{
173 width: box.width,
174 height: box.height,
175 fill: style.fill,
176 stroke: style.stroke,
177 strokeWidth: style.strokeWidth,
178 cornerRadius: style.cornerRadius,
179 }}
180 />
181 <Group style={{ translateX: content.x, translateY: content.y }}>
182 <Content width={content.width} height={content.height}>
183 {props.children}
184 </Content>
185 </Group>
186 </Group>
187 );
188 },
189 {
190 resolveStyle(style, context) {
191 const width = resolveLength(
192 style.width,
193 context.allocation.width,
194 "width",
195 ),
196 height = resolveLength(
197 style.height,
198 context.allocation.height,
199 "height",
200 );
201 if (width === undefined || height === undefined)
202 throw new TypeError("HoverCard requires declared width and height.");
203 const box = resolveBox(
204 { width, height, padding: style.padding ?? 10 },
205 { minWidth: 0, maxWidth: Infinity, minHeight: 0, maxHeight: Infinity },
206 { component: "HoverCard" },
207 );
208 const strokeWidth = style.strokeWidth ?? 1,
209 cornerRadius = style.cornerRadius ?? 7,
210 opacity = style.opacity ?? 1;
211 for (const value of [strokeWidth, cornerRadius, opacity])
212 if (!Number.isFinite(value) || value < 0)
213 throw new RangeError(
214 "HoverCard paint dimensions must be finite and nonnegative.",
215 );
216 if (opacity > 1)
217 throw new RangeError("HoverCard opacity must be in [0,1].");
218 return {
219 box,
220 bounds: {
221 x: 0,
222 y: 0,
223 width: context.allocation.width,
224 height: context.allocation.height,
225 },
226 fill: style.fill ?? "rgba(255,255,255,0.92)",
227 stroke: style.stroke ?? "rgba(98,117,139,0.35)",
228 strokeWidth,
229 cornerRadius,
230 opacity,
231 };
232 },
233 },
234 );
235 const fields = new Set([
236 "width",
237 "height",
238 "padding",
239 "fill",
240 "stroke",
241 "strokeWidth",
242 "cornerRadius",
243 "opacity",
244 ]);
245 /**
246 * Positions a declared card outside a protected anchor, allowing overflow if needed.
247 *
248 * @param props - Anchor, placement, style, and hover-card content. See {@link HoverCardProps}.
249 * @returns Pibbl content placed as a hover card. See {@link PibblNode}.
250 *
251 * @see {@link HoverCardProps}
252 * @see {@link PibblNode}
253 */
254 export function HoverCard(props: HoverCardProps): PibblNode {
255 const style = resolveSignalValue(props.style);
256 if (!style || typeof style !== "object")
257 throw new TypeError("HoverCard requires a style with width and height.");
258 for (const field of Object.keys(style))
259 if (!fields.has(field))
260 throw new TypeError(`HoverCard does not support style.${field}.`);
261 return (
262 <Card
263 anchor={resolveSignalValue(props.anchor)}
264 anchorRadius={resolveSignalValue(props.anchorRadius ?? 0)}
265 gap={resolveSignalValue(props.gap ?? 8)}
266 bounds={
267 props.bounds === undefined
268 ? undefined
269 : resolveSignalValue(props.bounds)
270 }
271 placement={props.placement}
272 style={style}
273 >
274 {props.children}
275 </Card>
276 );
277 }
278
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.