Skip to content

Commit 6bbe2ff

Browse files
authored
feat: add bounding box highlight UI components and types
- Add BoundingBoxHighlightRect, BoundingBoxHighlightNav, BoundingBoxHighlightList - Add BoundingBox type and component styles - Add ChevronLeft and ChevronRight icons - Add messages for aria labels - Add comprehensive unit tests for all components
1 parent 79f074a commit 6bbe2ff

14 files changed

+914
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.ba-BoundingBoxHighlightList {
2+
position: absolute;
3+
top: 0;
4+
left: 0;
5+
width: 100%;
6+
height: 100%;
7+
pointer-events: none;
8+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import * as React from 'react';
2+
import BoundingBoxHighlightRect from './BoundingBoxHighlightRect';
3+
import { BoundingBox } from './types';
4+
import './BoundingBoxHighlightList.scss';
5+
6+
type Props = {
7+
/**
8+
* All bounding box highlights in the document.
9+
*/
10+
allBoundingBoxes: BoundingBox[];
11+
/**
12+
* Bounding boxes on the current page.
13+
*/
14+
boundingBoxes: BoundingBox[];
15+
/**
16+
* Callback invoked to navigate to the previous or next bounding box highlight.
17+
*/
18+
onNavigate?: (id: string) => void;
19+
/**
20+
* Callback invoked when the user clicks on a bounding box highlight.
21+
*/
22+
onSelect?: (id: string) => void;
23+
/**
24+
* The ID of the selected bounding box highlight.
25+
*/
26+
selectedId: string | null;
27+
};
28+
29+
const BoundingBoxHighlightList = ({
30+
allBoundingBoxes,
31+
boundingBoxes,
32+
onNavigate,
33+
onSelect,
34+
selectedId,
35+
}: Props): React.ReactElement | null => {
36+
const total = allBoundingBoxes.length;
37+
38+
if (total === 0) {
39+
return null;
40+
}
41+
42+
const selectedIndex = allBoundingBoxes.findIndex(h => h.id === selectedId);
43+
const prevId = selectedIndex > 0 ? allBoundingBoxes[selectedIndex - 1].id : undefined;
44+
const nextId = selectedIndex < total - 1 ? allBoundingBoxes[selectedIndex + 1].id : undefined;
45+
46+
return (
47+
<div className="ba-BoundingBoxHighlightList" data-testid="ba-BoundingBoxHighlightList">
48+
{boundingBoxes.map(bbox => {
49+
return (
50+
<BoundingBoxHighlightRect
51+
key={bbox.id}
52+
boundingBox={bbox}
53+
currentIndex={selectedIndex}
54+
isSelected={selectedId === bbox.id}
55+
nextId={nextId}
56+
onNavigate={onNavigate}
57+
onSelect={onSelect}
58+
prevId={prevId}
59+
total={total}
60+
/>
61+
);
62+
})}
63+
</div>
64+
);
65+
};
66+
67+
export default React.memo(BoundingBoxHighlightList);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
$ba-BoundingBoxHighlightNav-bg: #fff;
2+
$ba-BoundingBoxHighlightNav-shadow: 0 6px 20px rgba(0, 0, 0, .1);
3+
$ba-BoundingBoxHighlightNav-arrow: #909090;
4+
$ba-BoundingBoxHighlightNav-border: #e8e8e8;
5+
$ba-BoundingBoxHighlightNav-counter: #222;
6+
7+
.ba-BoundingBoxHighlightNav {
8+
position: absolute;
9+
bottom: 100%;
10+
left: 50%;
11+
z-index: 1;
12+
display: flex;
13+
gap: 4px;
14+
align-items: center;
15+
margin-bottom: 6px;
16+
padding: 3px;
17+
white-space: nowrap;
18+
background: $ba-BoundingBoxHighlightNav-bg;
19+
border: 1px solid $ba-BoundingBoxHighlightNav-border;
20+
border-radius: 20px;
21+
box-shadow: $ba-BoundingBoxHighlightNav-shadow;
22+
transform: translateX(-50%);
23+
cursor: default;
24+
pointer-events: auto;
25+
}
26+
27+
.ba-BoundingBoxHighlightNav-btn {
28+
display: flex;
29+
align-items: center;
30+
justify-content: center;
31+
width: 32px;
32+
height: 32px;
33+
padding: 0;
34+
line-height: 1;
35+
background: transparent;
36+
border: none;
37+
border-radius: 50%;
38+
cursor: pointer;
39+
transition: background-color 150ms ease;
40+
41+
svg {
42+
path {
43+
fill: $ba-BoundingBoxHighlightNav-arrow;
44+
}
45+
}
46+
47+
&:hover:not(:disabled) {
48+
background: rgba(0, 0, 0, .06);
49+
}
50+
51+
&:active:not(:disabled) {
52+
background: rgba(0, 0, 0, .12);
53+
}
54+
55+
&:disabled {
56+
cursor: default;
57+
58+
svg {
59+
path {
60+
fill: $ba-BoundingBoxHighlightNav-border;
61+
}
62+
}
63+
}
64+
}
65+
66+
.ba-BoundingBoxHighlightNav-counter {
67+
padding: 0 4px;
68+
color: $ba-BoundingBoxHighlightNav-counter;
69+
font-weight: normal;
70+
font-size: 15px;
71+
font-family: Lato, Arial, sans-serif;
72+
user-select: none;
73+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as React from 'react';
2+
import { useIntl } from 'react-intl';
3+
import ChevronLeft from '../../icons/ChevronLeft';
4+
import ChevronRight from '../../icons/ChevronRight';
5+
import messages from './messages';
6+
import './BoundingBoxHighlightNav.scss';
7+
8+
type Props = {
9+
currentIndex: number;
10+
total: number;
11+
onPrev: (event: React.MouseEvent) => void;
12+
onNext: (event: React.MouseEvent) => void;
13+
};
14+
15+
const BoundingBoxHighlightNav = ({ currentIndex, total, onPrev, onNext }: Props): JSX.Element => {
16+
const intl = useIntl();
17+
const isPrevDisabled = currentIndex === 0;
18+
const isNextDisabled = currentIndex === total - 1;
19+
20+
return (
21+
<div className="ba-BoundingBoxHighlightNav" data-testid="ba-BoundingBoxHighlightNav">
22+
<button
23+
aria-label={intl.formatMessage(messages.ariaLabelViewPrevReference)}
24+
className="ba-BoundingBoxHighlightNav-btn"
25+
data-testid="ba-BoundingBoxHighlightNav-prev"
26+
disabled={isPrevDisabled}
27+
onClick={onPrev}
28+
type="button"
29+
>
30+
<ChevronLeft width={20} height={20} />
31+
</button>
32+
<span className="ba-BoundingBoxHighlightNav-counter">
33+
{currentIndex + 1} / {total}
34+
</span>
35+
<button
36+
aria-label={intl.formatMessage(messages.ariaLabelViewNextReference)}
37+
className="ba-BoundingBoxHighlightNav-btn"
38+
data-testid="ba-BoundingBoxHighlightNav-next"
39+
disabled={isNextDisabled}
40+
onClick={onNext}
41+
type="button"
42+
>
43+
<ChevronRight width={20} height={20} />
44+
</button>
45+
</div>
46+
);
47+
};
48+
49+
export default BoundingBoxHighlightNav;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
$ba-BoundingBoxHighlight-color: #0061d5;
2+
$ba-BoundingBoxHighlight-bgColor--default: rgba($ba-BoundingBoxHighlight-color, .05);
3+
$ba-BoundingBoxHighlight-bgColor--hover: rgba($ba-BoundingBoxHighlight-color, .16);
4+
$ba-BoundingBoxHighlight-bgColor--selected: rgba($ba-BoundingBoxHighlight-color, .1);
5+
6+
.ba-BoundingBoxHighlightRect {
7+
position: absolute;
8+
box-sizing: border-box;
9+
background-color: $ba-BoundingBoxHighlight-bgColor--default;
10+
border: 2px solid rgba($ba-BoundingBoxHighlight-color, .2);
11+
border-radius: 8px;
12+
outline: none;
13+
cursor: pointer;
14+
transition: background-color 200ms ease, border-color 200ms ease, box-shadow 200ms ease;
15+
pointer-events: auto;
16+
17+
&.is-selected {
18+
background-color: $ba-BoundingBoxHighlight-bgColor--selected;
19+
border-color: $ba-BoundingBoxHighlight-color;
20+
box-shadow: 0 0 0 2px rgba($ba-BoundingBoxHighlight-color, .3), 0 4px 12px rgba(0, 0, 0, .15);
21+
}
22+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import * as React from 'react';
2+
import classNames from 'classnames';
3+
import BoundingBoxHighlightNav from './BoundingBoxHighlightNav';
4+
import { BoundingBox } from './types';
5+
import './BoundingBoxHighlightRect.scss';
6+
7+
type Props = {
8+
boundingBox: BoundingBox;
9+
currentIndex: number;
10+
isSelected?: boolean;
11+
onNavigate?: (id: string) => void;
12+
onSelect?: (id: string) => void;
13+
total: number;
14+
prevId?: string;
15+
nextId?: string;
16+
};
17+
18+
const BoundingBoxHighlightRect = ({
19+
boundingBox,
20+
currentIndex,
21+
isSelected,
22+
onNavigate,
23+
onSelect,
24+
total,
25+
prevId,
26+
nextId,
27+
}: Props): JSX.Element => {
28+
const { id, x, y, width, height } = boundingBox;
29+
30+
const style: React.CSSProperties = {
31+
display: 'block',
32+
left: `${x}%`,
33+
top: `${y}%`,
34+
width: `${width}%`,
35+
height: `${height}%`,
36+
};
37+
38+
const handleClick = (event: React.MouseEvent): void => {
39+
event.stopPropagation();
40+
onSelect?.(id);
41+
};
42+
43+
const handlePrev = (event: React.MouseEvent): void => {
44+
event.stopPropagation();
45+
if (prevId) {
46+
onNavigate?.(prevId);
47+
}
48+
};
49+
50+
const handleNext = (event: React.MouseEvent): void => {
51+
event.stopPropagation();
52+
if (nextId) {
53+
onNavigate?.(nextId);
54+
}
55+
};
56+
57+
return (
58+
<div
59+
className={classNames('ba-BoundingBoxHighlightRect', { 'is-selected': isSelected })}
60+
data-testid={`ba-BoundingBoxHighlightRect-${id}`}
61+
onClick={handleClick}
62+
role="presentation"
63+
style={style}
64+
>
65+
{isSelected && total > 1 && (
66+
<BoundingBoxHighlightNav
67+
currentIndex={currentIndex}
68+
onNext={handleNext}
69+
onPrev={handlePrev}
70+
total={total}
71+
/>
72+
)}
73+
</div>
74+
);
75+
};
76+
77+
export default BoundingBoxHighlightRect;

0 commit comments

Comments
 (0)