Skip to content

Commit

Permalink
Annotation label <foreignobject> render (#1383)
Browse files Browse the repository at this point in the history
* Annotation label <foreignobject> render

Add option to pass in an element to render as a label as a child.
Uses <foreignObject> to allow user to pass in an HTML element.

Related to this discussion: #1173

This has the benefit of letting the element handle text reflow, with
the drawback being that the user need to manage min and max width of
the container element they render.

This approach seems much better imo, favouring composability over
configuration which is much more in line with the philosophy of d3.

* Fix child render for Safari

Add witdth and height to <foreignObject>. Worked for Chrome but was not
displaying anything for Safari.

* Fix test cases

Not sure how idomatic all the `dive()`s are, but I've heard complaints
about them so I'm guess it's the way to go.

* Remove console log

* Address PR comments

Rename ForeignObjectLabel -> HtmlLabel

Split out HtmlLabel and LabelAnchorLine to separate components.

Export HtmlLabel as a separate component as it uses a small subset of the shared props.
  • Loading branch information
valtism authored Jan 21, 2022
1 parent 04a5c2a commit ae47109
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 27 deletions.
87 changes: 87 additions & 0 deletions packages/visx-annotation/src/components/HtmlLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { useContext, useMemo } from 'react';
import cx from 'classnames';
import useMeasure from 'react-use-measure';
import Group from '@visx/group/lib/Group';
import AnnotationContext from '../context/AnnotationContext';
import AnchorLine from './LabelAnchorLine';
import { LabelProps } from './Label';

const wrapperStyle = { display: 'inline-block' };

export type HtmlLabelProps = Pick<
LabelProps,
| 'anchorLineStroke'
| 'className'
| 'horizontalAnchor'
| 'resizeObserverPolyfill'
| 'showAnchorLine'
| 'verticalAnchor'
| 'x'
| 'y'
> & {
/** Pass in a custom element as the label to style as you like. Renders inside a <foreignObject>, be aware that most non-browser SVG renderers will not render HTML <foreignObject>s. See: https://github.com/airbnb/visx/issues/1173#issuecomment-1014380545. */
children?: React.ReactNode;
};
export default function HtmlLabel({
anchorLineStroke = '#222',
children,
className,
horizontalAnchor: propsHorizontalAnchor,
resizeObserverPolyfill,
showAnchorLine = true,
verticalAnchor: propsVerticalAnchor,
x: propsX,
y: propsY,
}: HtmlLabelProps) {
// we must measure the rendered title + subtitle to compute container height
const [labelRef, titleBounds] = useMeasure({
polyfill: resizeObserverPolyfill,
});
const { width, height } = titleBounds;

// if props are provided, they take precedence over context
const { x = 0, y = 0, dx = 0, dy = 0 } = useContext(AnnotationContext);

// offset container position based on horizontal + vertical anchor
const horizontalAnchor =
propsHorizontalAnchor || (Math.abs(dx) < Math.abs(dy) ? 'middle' : dx > 0 ? 'start' : 'end');
const verticalAnchor =
propsVerticalAnchor || (Math.abs(dx) > Math.abs(dy) ? 'middle' : dy > 0 ? 'start' : 'end');

const containerCoords = useMemo(() => {
let adjustedX: number = propsX == null ? x + dx : propsX;
let adjustedY: number = propsY == null ? y + dy : propsY;

if (horizontalAnchor === 'middle') adjustedX -= width / 2;
if (horizontalAnchor === 'end') adjustedX -= width;
if (verticalAnchor === 'middle') adjustedY -= height / 2;
if (verticalAnchor === 'end') adjustedY -= height;

return { x: adjustedX, y: adjustedY };
}, [propsX, x, dx, propsY, y, dy, horizontalAnchor, verticalAnchor, width, height]);

return (
<Group
top={containerCoords.y}
left={containerCoords.x}
pointerEvents="none"
className={cx('visx-annotationlabel', className)}
>
{showAnchorLine && (
<AnchorLine
anchorLineOrientation={Math.abs(dx) > Math.abs(dy) ? 'vertical' : 'horizontal'}
anchorLineStroke={anchorLineStroke}
verticalAnchor={verticalAnchor}
horizontalAnchor={horizontalAnchor}
width={width}
height={height}
/>
)}
<foreignObject width={width} height={height} overflow="visible">
<div ref={labelRef} style={wrapperStyle}>
{children}
</div>
</foreignObject>
</Group>
);
}
37 changes: 16 additions & 21 deletions packages/visx-annotation/src/components/Label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Text, { TextProps } from '@visx/text/lib/Text';
import { useText } from '@visx/text';
import useMeasure, { Options as UseMeasureOptions } from 'react-use-measure';
import AnnotationContext from '../context/AnnotationContext';
import AnchorLine from './LabelAnchorLine';

export type LabelProps = {
/** Stroke color of anchor line. */
Expand Down Expand Up @@ -93,9 +94,13 @@ export default function Label({
x: propsX,
y: propsY,
}: LabelProps) {
// we must measure the rendered title + subtitle to compute container height
const [titleRef, titleBounds] = useMeasure({ polyfill: resizeObserverPolyfill });
const [subtitleRef, subtitleBounds] = useMeasure({ polyfill: resizeObserverPolyfill });
// we must measure the rendered html content to compute container height
const [titleRef, titleBounds] = useMeasure({
polyfill: resizeObserverPolyfill,
});
const [subtitleRef, subtitleBounds] = useMeasure({
polyfill: resizeObserverPolyfill,
});

const padding = useMemo(() => getCompletePadding(backgroundPadding), [backgroundPadding]);

Expand Down Expand Up @@ -182,10 +187,6 @@ export default function Label({
[subtitleFontSize, subtitleFontWeight, subtitleFontFamily],
) as React.CSSProperties;

const anchorLineOrientation = Math.abs(dx) > Math.abs(dy) ? 'vertical' : 'horizontal';

const backgroundOutline = showAnchorLine ? { stroke: anchorLineStroke, strokeWidth: 2 } : null;

return !title && !subtitle ? null : (
<Group
top={containerCoords.y}
Expand All @@ -206,20 +207,14 @@ export default function Label({
/>
)}
{showAnchorLine && (
<>
{anchorLineOrientation === 'horizontal' && verticalAnchor === 'start' && (
<line {...backgroundOutline} x1={0} x2={width} y1={0} y2={0} />
)}
{anchorLineOrientation === 'horizontal' && verticalAnchor === 'end' && (
<line {...backgroundOutline} x1={0} x2={width} y1={height} y2={height} />
)}
{anchorLineOrientation === 'vertical' && horizontalAnchor === 'start' && (
<line {...backgroundOutline} x1={0} x2={0} y1={0} y2={height} />
)}
{anchorLineOrientation === 'vertical' && horizontalAnchor === 'end' && (
<line {...backgroundOutline} x1={width} x2={width} y1={0} y2={height} />
)}
</>
<AnchorLine
anchorLineOrientation={Math.abs(dx) > Math.abs(dy) ? 'vertical' : 'horizontal'}
anchorLineStroke={anchorLineStroke}
verticalAnchor={verticalAnchor}
horizontalAnchor={horizontalAnchor}
width={width}
height={height}
/>
)}
{title && (
<Text
Expand Down
39 changes: 39 additions & 0 deletions packages/visx-annotation/src/components/LabelAnchorLine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import { TextProps } from '@visx/text';

interface AnchorLineProps {
anchorLineOrientation: 'horizontal' | 'vertical';
verticalAnchor: TextProps['verticalAnchor'];
horizontalAnchor: TextProps['textAnchor'];
anchorLineStroke: string;
width: number;
height: number;
}

export default function AnchorLine({
anchorLineOrientation,
anchorLineStroke,
verticalAnchor,
horizontalAnchor,
width,
height,
}: AnchorLineProps) {
const backgroundOutline = { stroke: anchorLineStroke, strokeWidth: 2 };

return (
<>
{anchorLineOrientation === 'horizontal' && verticalAnchor === 'start' && (
<line {...backgroundOutline} x1={0} x2={width} y1={0} y2={0} />
)}
{anchorLineOrientation === 'horizontal' && verticalAnchor === 'end' && (
<line {...backgroundOutline} x1={0} x2={width} y1={height} y2={height} />
)}
{anchorLineOrientation === 'vertical' && horizontalAnchor === 'start' && (
<line {...backgroundOutline} x1={0} x2={0} y1={0} y2={height} />
)}
{anchorLineOrientation === 'vertical' && horizontalAnchor === 'end' && (
<line {...backgroundOutline} x1={width} x2={width} y1={0} y2={height} />
)}
</>
);
}
1 change: 1 addition & 0 deletions packages/visx-annotation/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { default as Connector } from './components/Connector';
export { default as Label } from './components/Label';
export { default as HtmlLabel } from './components/HtmlLabel';
export { default as CircleSubject } from './components/CircleSubject';
export { default as LineSubject } from './components/LineSubject';
export { default as Annotation } from './components/Annotation';
Expand Down
16 changes: 10 additions & 6 deletions packages/visx-annotation/test/Label.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe('<Label />', () => {
it('should render title Text', () => {
expect(
shallow(<Label title="title test" resizeObserverPolyfill={ResizeObserver} />)
.dive()
.children()
.find(Text)
.prop('children'),
Expand All @@ -25,6 +26,7 @@ describe('<Label />', () => {
resizeObserverPolyfill={ResizeObserver}
/>,
)
.dive()
.children()
.find(Text)
.at(1)
Expand All @@ -33,16 +35,18 @@ describe('<Label />', () => {
});
it('should render a background', () => {
expect(
shallow(
<Label title="title test" showBackground resizeObserverPolyfill={ResizeObserver} />,
).find('rect'),
shallow(<Label title="title test" showBackground resizeObserverPolyfill={ResizeObserver} />)
.dive()
.find('rect'),
).toHaveLength(1);
});
it('should render an anchor line', () => {
expect(
shallow(
<Label title="title test" showAnchorLine resizeObserverPolyfill={ResizeObserver} />,
).find('line'),
shallow(<Label title="title test" showAnchorLine resizeObserverPolyfill={ResizeObserver} />)
.dive()
.find('AnchorLine')
.dive()
.find('line'),
).toHaveLength(1);
});
});
2 changes: 2 additions & 0 deletions packages/visx-demo/src/pages/docs/annotation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import CircleSubject from '../../../../visx-annotation/src/components/CircleSubj
import LineSubject from '../../../../visx-annotation/src/components/LineSubject';
import Connector from '../../../../visx-annotation/src/components/Connector';
import Label from '../../../../visx-annotation/src/components/Label';
import HtmlLabel from '../../../../visx-annotation/src/components/HtmlLabel';
import LinePathAnnotationDeprecated from '../../../../visx-annotation/src/deprecated/LinePathAnnotation';
import DocPage from '../../components/DocPage';
import AnnotationTile from '../../components/Gallery/AnnotationTile';
Expand All @@ -17,6 +18,7 @@ const components = [
LineSubject,
Connector,
Label,
HtmlLabel,
LinePathAnnotationDeprecated,
];

Expand Down

0 comments on commit ae47109

Please sign in to comment.