Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.ba-BoundingBoxHighlightList {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as React from 'react';
import BoundingBoxHighlightRect from './BoundingBoxHighlightRect';
import { BoundingBox } from './types';
import './BoundingBoxHighlightList.scss';

type Props = {
/**
* All bounding box highlights in the document.
*/
allBoundingBoxes: BoundingBox[];
/**
* Bounding boxes on the current page.
*/
boundingBoxes: BoundingBox[];
/**
* Callback invoked to navigate to the previous or next bounding box highlight.
*/
onNavigate?: (id: string) => void;
/**
* Callback invoked when the user clicks on a bounding box highlight.
*/
onSelect?: (id: string) => void;
/**
* The ID of the selected bounding box highlight.
*/
selectedId: string | null;
};

const BoundingBoxHighlightList = ({
allBoundingBoxes,
boundingBoxes,
onNavigate,
onSelect,
selectedId,
}: Props): React.ReactElement | null => {
const total = allBoundingBoxes.length;

if (total === 0) {
return null;
}

const selectedIndex = allBoundingBoxes.findIndex(h => h.id === selectedId);
const prevId = selectedIndex > 0 ? allBoundingBoxes[selectedIndex - 1].id : undefined;
const nextId = selectedIndex < total - 1 ? allBoundingBoxes[selectedIndex + 1].id : undefined;

return (
<div className="ba-BoundingBoxHighlightList" data-testid="ba-BoundingBoxHighlightList">
{boundingBoxes.map(bbox => {
return (
<BoundingBoxHighlightRect
key={bbox.id}
boundingBox={bbox}
currentIndex={selectedIndex}
isSelected={selectedId === bbox.id}
nextId={nextId}
onNavigate={onNavigate}
onSelect={onSelect}
prevId={prevId}
total={total}
/>
);
})}
</div>
);
};

export default React.memo(BoundingBoxHighlightList);
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
$ba-BoundingBoxHighlightNav-bg: #fff;
$ba-BoundingBoxHighlightNav-shadow: 0 6px 20px rgba(0, 0, 0, .1);
$ba-BoundingBoxHighlightNav-arrow: #909090;
$ba-BoundingBoxHighlightNav-border: #e8e8e8;
$ba-BoundingBoxHighlightNav-counter: #222;

.ba-BoundingBoxHighlightNav {
position: absolute;
bottom: 100%;
left: 50%;
z-index: 1;
display: flex;
gap: 4px;
align-items: center;
margin-bottom: 6px;
padding: 3px;
white-space: nowrap;
background: $ba-BoundingBoxHighlightNav-bg;
border: 1px solid $ba-BoundingBoxHighlightNav-border;
border-radius: 20px;
box-shadow: $ba-BoundingBoxHighlightNav-shadow;
transform: translateX(-50%);
cursor: default;
pointer-events: auto;
}

.ba-BoundingBoxHighlightNav-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
line-height: 1;
background: transparent;
border: none;
border-radius: 50%;
cursor: pointer;
transition: background-color 150ms ease;

svg {
path {
fill: $ba-BoundingBoxHighlightNav-arrow;
}
}

&:hover:not(:disabled) {
background: rgba(0, 0, 0, .06);
}

&:active:not(:disabled) {
background: rgba(0, 0, 0, .12);
}

&:disabled {
cursor: default;

svg {
path {
fill: $ba-BoundingBoxHighlightNav-border;
}
}
}
}

.ba-BoundingBoxHighlightNav-counter {
padding: 0 4px;
color: $ba-BoundingBoxHighlightNav-counter;
font-weight: normal;
font-size: 15px;
font-family: Lato, Arial, sans-serif;
user-select: none;
}
49 changes: 49 additions & 0 deletions src/components/BoundingBoxHighlight/BoundingBoxHighlightNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as React from 'react';
import { useIntl } from 'react-intl';
import ChevronLeft from '../../icons/ChevronLeft';
import ChevronRight from '../../icons/ChevronRight';
import messages from './messages';
import './BoundingBoxHighlightNav.scss';

type Props = {
currentIndex: number;
total: number;
onPrev: (event: React.MouseEvent) => void;
onNext: (event: React.MouseEvent) => void;
};

const BoundingBoxHighlightNav = ({ currentIndex, total, onPrev, onNext }: Props): JSX.Element => {
const intl = useIntl();
const isPrevDisabled = currentIndex === 0;
const isNextDisabled = currentIndex === total - 1;

return (
<div className="ba-BoundingBoxHighlightNav" data-testid="ba-BoundingBoxHighlightNav">
<button
aria-label={intl.formatMessage(messages.ariaLabelViewPrevReference)}
className="ba-BoundingBoxHighlightNav-btn"
data-testid="ba-BoundingBoxHighlightNav-prev"
disabled={isPrevDisabled}
onClick={onPrev}
type="button"
>
<ChevronLeft width={20} height={20} />
</button>
<span className="ba-BoundingBoxHighlightNav-counter">
{currentIndex + 1} / {total}
</span>
<button
aria-label={intl.formatMessage(messages.ariaLabelViewNextReference)}
className="ba-BoundingBoxHighlightNav-btn"
data-testid="ba-BoundingBoxHighlightNav-next"
disabled={isNextDisabled}
onClick={onNext}
type="button"
>
<ChevronRight width={20} height={20} />
</button>
</div>
);
};

export default BoundingBoxHighlightNav;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
$ba-BoundingBoxHighlight-color: #0061d5;
$ba-BoundingBoxHighlight-bgColor--default: rgba($ba-BoundingBoxHighlight-color, .05);
$ba-BoundingBoxHighlight-bgColor--hover: rgba($ba-BoundingBoxHighlight-color, .16);
$ba-BoundingBoxHighlight-bgColor--selected: rgba($ba-BoundingBoxHighlight-color, .1);

.ba-BoundingBoxHighlightRect {
position: absolute;
box-sizing: border-box;
background-color: $ba-BoundingBoxHighlight-bgColor--default;
border: 2px solid rgba($ba-BoundingBoxHighlight-color, .2);
border-radius: 8px;
outline: none;
cursor: pointer;
transition: background-color 200ms ease, border-color 200ms ease, box-shadow 200ms ease;
pointer-events: auto;

&.is-selected {
background-color: $ba-BoundingBoxHighlight-bgColor--selected;
border-color: $ba-BoundingBoxHighlight-color;
box-shadow: 0 0 0 2px rgba($ba-BoundingBoxHighlight-color, .3), 0 4px 12px rgba(0, 0, 0, .15);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as React from 'react';
import classNames from 'classnames';
import BoundingBoxHighlightNav from './BoundingBoxHighlightNav';
import { BoundingBox } from './types';
import './BoundingBoxHighlightRect.scss';

type Props = {
boundingBox: BoundingBox;
currentIndex: number;
isSelected?: boolean;
onNavigate?: (id: string) => void;
onSelect?: (id: string) => void;
total: number;
prevId?: string;
nextId?: string;
};

const BoundingBoxHighlightRect = ({
boundingBox,
currentIndex,
isSelected,
onNavigate,
onSelect,
total,
prevId,
nextId,
}: Props): JSX.Element => {
const { id, x, y, width, height } = boundingBox;

const style: React.CSSProperties = {
display: 'block',
left: `${x}%`,
top: `${y}%`,
width: `${width}%`,
height: `${height}%`,
};

const handleClick = (event: React.MouseEvent): void => {
event.stopPropagation();
onSelect?.(id);
};

const handlePrev = (event: React.MouseEvent): void => {
event.stopPropagation();
if (prevId) {
onNavigate?.(prevId);
}
};

const handleNext = (event: React.MouseEvent): void => {
event.stopPropagation();
if (nextId) {
onNavigate?.(nextId);
}
};

return (
<div
className={classNames('ba-BoundingBoxHighlightRect', { 'is-selected': isSelected })}
data-testid={`ba-BoundingBoxHighlightRect-${id}`}
onClick={handleClick}
role="presentation"
style={style}
>
{isSelected && total > 1 && (
<BoundingBoxHighlightNav
currentIndex={currentIndex}
onNext={handleNext}
onPrev={handlePrev}
total={total}
/>
)}
</div>
);
};

export default BoundingBoxHighlightRect;
Loading