box-annotations 5.2.1-beta.2 → 5.2.1-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/annotations.css +1 -1
  2. package/dist/annotations.js +1 -1
  3. package/i18n/en-US.js +2 -0
  4. package/i18n/en-US.properties +4 -0
  5. package/i18n/json/src/components/BoundingBoxHighlight/messages.json +1 -0
  6. package/package.json +1 -1
  7. package/src/@types/events.ts +4 -0
  8. package/src/boundingBoxHighlight/BoundingBoxHighlightContainer.tsx +33 -0
  9. package/src/boundingBoxHighlight/BoundingBoxHighlightManager.tsx +19 -0
  10. package/src/boundingBoxHighlight/index.ts +1 -0
  11. package/src/common/BaseAnnotator.ts +113 -12
  12. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightList.scss +8 -0
  13. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightList.tsx +67 -0
  14. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightNav.scss +73 -0
  15. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightNav.tsx +49 -0
  16. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightRect.scss +22 -0
  17. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightRect.tsx +77 -0
  18. package/src/components/BoundingBoxHighlight/index.ts +4 -0
  19. package/src/components/BoundingBoxHighlight/messages.ts +14 -0
  20. package/src/document/DocumentAnnotator.ts +48 -4
  21. package/src/icons/ChevronLeft.tsx +24 -0
  22. package/src/icons/ChevronRight.tsx +24 -0
  23. package/src/image/ImageAnnotator.ts +47 -1
  24. package/src/store/annotations/reducer.ts +6 -1
  25. package/src/store/boundingBoxHighlights/actions.ts +6 -0
  26. package/src/store/boundingBoxHighlights/index.ts +4 -0
  27. package/src/store/boundingBoxHighlights/reducer.ts +33 -0
  28. package/src/store/boundingBoxHighlights/selectors.ts +12 -0
  29. package/src/store/boundingBoxHighlights/types.ts +13 -0
  30. package/src/store/createRootReducer.ts +2 -0
  31. package/src/store/eventing/boundingBoxHighlightNav.ts +13 -0
  32. package/src/store/eventing/middleware.ts +3 -0
  33. package/src/store/index.ts +1 -0
  34. package/src/store/options/actions.ts +2 -0
  35. package/src/store/options/reducer.ts +6 -1
  36. package/src/store/options/selectors.ts +2 -0
  37. package/src/store/options/types.ts +4 -0
  38. package/src/store/types.ts +2 -0
  39. package/src/utils/scroll.ts +8 -3
package/i18n/en-US.js CHANGED
@@ -4,6 +4,8 @@ export default {
4
4
  "ba.annotationsLoadError": "We’re sorry, the annotations failed to load for this file.",
5
5
  "ba.annotationsPost": "Post",
6
6
  "ba.annotationsSave": "Save",
7
+ "ba.boundingBoxHighlight.viewNextReference": "View next reference",
8
+ "ba.boundingBoxHighlight.viewPrevReference": "View previous reference",
7
9
  "ba.popup.reply.comment": "Comment",
8
10
  "ba.popup.reply.field": "Type a comment",
9
11
  "ba.popups.cancel": "Cancel",
@@ -8,6 +8,10 @@ ba.annotationsLoadError = We’re sorry, the annotations failed to load for this
8
8
  ba.annotationsPost = Post
9
9
  # Label for the save button
10
10
  ba.annotationsSave = Save
11
+ # Aria label for the button to view the next reference
12
+ ba.boundingBoxHighlight.viewNextReference = View next reference
13
+ # Aria label for the button to view the previous reference
14
+ ba.boundingBoxHighlight.viewPrevReference = View previous reference
11
15
  # Aria label description for reply popup
12
16
  ba.popup.reply.comment = Comment
13
17
  # Aria label description for annotation comment field
@@ -0,0 +1 @@
1
+ [{"id":"ba.boundingBoxHighlight.viewNextReference","description":"Aria label for the button to view the next reference","defaultMessage":"View next reference"},{"id":"ba.boundingBoxHighlight.viewPrevReference","description":"Aria label for the button to view the previous reference","defaultMessage":"View previous reference"}]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "box-annotations",
3
- "version": "5.2.1-beta.2",
3
+ "version": "5.2.1-beta.5",
4
4
  "description": "Box Annotations",
5
5
  "author": "Box (https://www.box.com/)",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -9,7 +9,11 @@ enum Event {
9
9
  ANNOTATIONS_INITIALIZED = 'annotations_initialized',
10
10
  ANNOTATIONS_MODE_CHANGE = 'annotations_mode_change',
11
11
  COLOR_SET = 'annotations_color_set',
12
+ BOUNDING_BOX_HIGHLIGHTS_SET = 'bounding_box_highlights_set',
13
+ BOUNDING_BOX_HIGHLIGHT_NAVIGATE = 'bounding_box_highlight_navigate',
14
+ BOUNDING_BOX_HIGHLIGHT_SELECT = 'bounding_box_highlight_select',
12
15
  VISIBLE_SET = 'annotations_visible_set',
16
+ VIEW_MODE_SET = 'view_mode_set',
13
17
  }
14
18
 
15
19
  // Existing legacy events, don't rename
@@ -0,0 +1,33 @@
1
+ import { connect } from 'react-redux';
2
+ import { BoundingBoxHighlightList } from '../components/BoundingBoxHighlight';
3
+ import withProviders from '../common/withProviders';
4
+ import {
5
+ AppState,
6
+ BoundingBox,
7
+ getBoundingBoxHighlights,
8
+ getBoundingBoxHighlightsForPage,
9
+ getSelectedBoundingBoxHighlightId,
10
+ navigateBoundingBoxHighlightAction,
11
+ setSelectedBoundingBoxHighlightAction,
12
+ } from '../store';
13
+
14
+ export type Props = {
15
+ location: number;
16
+ };
17
+
18
+ const mapStateToProps = (state: AppState, { location }: Props): {
19
+ allBoundingBoxes: BoundingBox[];
20
+ boundingBoxes: BoundingBox[];
21
+ selectedId: string | null;
22
+ } => ({
23
+ allBoundingBoxes: getBoundingBoxHighlights(state),
24
+ boundingBoxes: getBoundingBoxHighlightsForPage(state, location),
25
+ selectedId: getSelectedBoundingBoxHighlightId(state),
26
+ });
27
+
28
+ const mapDispatchToProps = {
29
+ onNavigate: navigateBoundingBoxHighlightAction,
30
+ onSelect: setSelectedBoundingBoxHighlightAction,
31
+ };
32
+
33
+ export default connect(mapStateToProps, mapDispatchToProps)(withProviders(BoundingBoxHighlightList));
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+ import * as ReactDOM from 'react-dom/client';
3
+ import BaseManager, { Props } from '../common/BaseManager';
4
+ import BoundingBoxHighlightContainer from './BoundingBoxHighlightContainer';
5
+
6
+ export default class BoundingBoxHighlightManager extends BaseManager {
7
+ decorate(): void {
8
+ this.reactEl.classList.add('ba-Layer--boundingBoxHighlight');
9
+ this.reactEl.dataset.testid = 'ba-Layer--boundingBoxHighlight';
10
+ }
11
+
12
+ render(props: Props): void {
13
+ if (!this.root) {
14
+ this.root = ReactDOM.createRoot(this.reactEl);
15
+ }
16
+
17
+ this.root.render(<BoundingBoxHighlightContainer location={this.location} {...props} />);
18
+ }
19
+ }
@@ -0,0 +1 @@
1
+ export { default as BoundingBoxHighlightManager } from './BoundingBoxHighlightManager';
@@ -7,7 +7,10 @@ import EventEmitter from './EventEmitter';
7
7
  import i18n from '../utils/i18n';
8
8
  import messages from '../messages';
9
9
  import { Event, IntlOptions, LegacyEvent, Permissions } from '../@types';
10
+ import { BoundingBox, getBoundingBoxHighlights } from '../store/boundingBoxHighlights';
11
+ import { ViewMode } from '../store/options/types';
10
12
  import { Features } from '../BoxAnnotations';
13
+ import { scrollToLocation } from '../utils/scroll';
11
14
  import './BaseAnnotator.scss';
12
15
 
13
16
  export type Container = string | HTMLElement;
@@ -37,6 +40,8 @@ export type Options = {
37
40
  hasTouch?: boolean;
38
41
  initialColor?: string;
39
42
  initialMode?: store.Mode;
43
+ /** View mode: annotations and bounding boxes are mutually exclusive. Defaults to 'annotations'. */
44
+ initialViewMode?: ViewMode;
40
45
  intl: IntlOptions;
41
46
  locale?: string;
42
47
  token: string;
@@ -72,6 +77,7 @@ export default class BaseAnnotator extends EventEmitter {
72
77
  fileOptions,
73
78
  initialColor,
74
79
  initialMode,
80
+ initialViewMode = 'annotations',
75
81
  intl,
76
82
  token,
77
83
  }: Options) {
@@ -93,6 +99,7 @@ export default class BaseAnnotator extends EventEmitter {
93
99
  fileVersionId: fileOptionsVersionId ?? fileVersionId,
94
100
  isCurrentFileVersion: !fileOptionsVersionId || fileOptionsVersionId === currentFileVersionId,
95
101
  permissions: file.permissions,
102
+ viewMode: initialViewMode,
96
103
  },
97
104
  };
98
105
 
@@ -109,6 +116,10 @@ export default class BaseAnnotator extends EventEmitter {
109
116
  this.addListener(Event.ANNOTATION_REMOVE, this.handleRemove);
110
117
  this.addListener(Event.COLOR_SET, this.handleSetColor);
111
118
  this.addListener(Event.VISIBLE_SET, this.handleSetVisible);
119
+ this.addListener(Event.BOUNDING_BOX_HIGHLIGHTS_SET, this.handleSetBoundingBoxHighlights);
120
+ this.addListener(Event.BOUNDING_BOX_HIGHLIGHT_NAVIGATE, this.handleNavigateBoundingBoxHighlight);
121
+ this.addListener(Event.BOUNDING_BOX_HIGHLIGHT_SELECT, this.handleSelectBoundingBoxHighlight);
122
+ this.addListener(Event.VIEW_MODE_SET, this.handleSetViewMode);
112
123
 
113
124
  // Load any required data at startup
114
125
  this.hydrate();
@@ -134,14 +145,10 @@ export default class BaseAnnotator extends EventEmitter {
134
145
  this.removeListener(Event.ANNOTATION_REMOVE, this.handleRemove);
135
146
  this.removeListener(Event.COLOR_SET, this.handleSetColor);
136
147
  this.removeListener(Event.VISIBLE_SET, this.handleSetVisible);
137
- }
138
-
139
- public getActiveId(): string | null {
140
- return store.getActiveAnnotationId(this.store.getState());
141
- }
142
-
143
- public getAnnotationContainerEl(): HTMLElement | null {
144
- return this.containerEl ?? null;
148
+ this.removeListener(Event.BOUNDING_BOX_HIGHLIGHTS_SET, this.handleSetBoundingBoxHighlights);
149
+ this.removeListener(Event.BOUNDING_BOX_HIGHLIGHT_NAVIGATE, this.handleNavigateBoundingBoxHighlight);
150
+ this.removeListener(Event.BOUNDING_BOX_HIGHLIGHT_SELECT, this.handleSelectBoundingBoxHighlight);
151
+ this.removeListener(Event.VIEW_MODE_SET, this.handleSetViewMode);
145
152
  }
146
153
 
147
154
  public init(scale = 1, rotation = 0): void {
@@ -170,10 +177,6 @@ export default class BaseAnnotator extends EventEmitter {
170
177
  return getProp(this.features, feature, false);
171
178
  }
172
179
 
173
- public isPopupOpen(): boolean {
174
- return !!this.containerEl?.querySelector('.ba-Popup');
175
- }
176
-
177
180
  public removeAnnotation = (annotationId: string): void => {
178
181
  this.store.dispatch(store.removeAnnotationAction(annotationId));
179
182
  };
@@ -183,6 +186,88 @@ export default class BaseAnnotator extends EventEmitter {
183
186
  // Called by box-content-preview
184
187
  }
185
188
 
189
+ public scrollToBoundingBoxHighlight(highlightId: string | null): void {
190
+ if (!highlightId || !this.annotatedEl) {
191
+ return;
192
+ }
193
+
194
+ const highlights = getBoundingBoxHighlights(this.store.getState());
195
+ const highlight = highlights.find((h: BoundingBox) => h.id === highlightId);
196
+
197
+ if (!highlight) {
198
+ return;
199
+ }
200
+
201
+ const referenceEl = this.getScrollReferenceForHighlight(highlight);
202
+ if (!referenceEl) {
203
+ return;
204
+ }
205
+
206
+ const offsets = {
207
+ x: highlight.x + highlight.width / 2,
208
+ y: highlight.y + highlight.height / 2,
209
+ };
210
+
211
+ scrollToLocation(this.annotatedEl, referenceEl, {
212
+ offsets: this.getAdjustedScrollOffsets(offsets),
213
+ smooth: true,
214
+ });
215
+ }
216
+
217
+ /** Returns the element to scroll relative to for the given bounding box highlight. */
218
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
219
+ protected getScrollReferenceForHighlight(_highlight: BoundingBox): HTMLElement | null | undefined {
220
+ return undefined; // Must be implemented in child class
221
+ }
222
+
223
+ /** Adjusts scroll offsets before passing to scrollToLocation (e.g. to apply rotation). */
224
+ protected getAdjustedScrollOffsets(offsets: { x: number; y: number }): { x: number; y: number } {
225
+ return offsets;
226
+ }
227
+
228
+ /**
229
+ * Sets the bounding box highlights to display. This replaces all existing bounding boxes.
230
+ * Triggers a re-render.
231
+ *
232
+ * Note: Bounding boxes are only visible when viewMode is set to 'boundingBoxes'.
233
+ *
234
+ * @param {BoundingBox[]} boundingBoxes - Array of bounding box definitions with coordinates and page numbers
235
+ */
236
+ public setBoundingBoxHighlights(boundingBoxes: BoundingBox[]): void {
237
+ this.store.dispatch(store.setBoundingBoxHighlightsAction(boundingBoxes));
238
+ this.render();
239
+ }
240
+
241
+ /**
242
+ * Sets the view mode for the annotator. Annotations and bounding boxes are mutually exclusive.
243
+ * Triggers a re-render.
244
+ *
245
+ * Side effects:
246
+ * - Switching to 'boundingBoxes': Clears active annotation ID
247
+ * - Switching to 'annotations': Clears selected bounding box ID
248
+ *
249
+ * @param {ViewMode} viewMode - Either 'annotations' (default) or 'boundingBoxes'
250
+ */
251
+ public setViewMode(viewMode: ViewMode): void {
252
+ this.store.dispatch(store.setViewModeAction(viewMode));
253
+ this.render();
254
+ }
255
+
256
+ /**
257
+ * Selects a bounding box highlight and scrolls to it. Pass null to deselect.
258
+ *
259
+ * Note: For programmatic navigation (prev/next), use the navigateBoundingBoxHighlightAction
260
+ * which emits BOUNDING_BOX_HIGHLIGHT_NAVIGATE events.
261
+ *
262
+ * @param {string | null} highlightId - The ID of the bounding box to select, or null to clear selection
263
+ */
264
+ public selectBoundingBoxHighlight(highlightId: string | null): void {
265
+ this.store.dispatch(store.setSelectedBoundingBoxHighlightAction(highlightId));
266
+ if (highlightId) {
267
+ this.scrollToBoundingBoxHighlight(highlightId);
268
+ }
269
+ }
270
+
186
271
  public setActiveId(annotationId: string | null): void {
187
272
  this.store.dispatch(store.setActiveAnnotationIdAction(annotationId));
188
273
  }
@@ -235,6 +320,22 @@ export default class BaseAnnotator extends EventEmitter {
235
320
  this.setColor(color);
236
321
  };
237
322
 
323
+ protected handleSetBoundingBoxHighlights = (boundingBoxes: BoundingBox[]): void => {
324
+ this.setBoundingBoxHighlights(boundingBoxes);
325
+ };
326
+
327
+ protected handleNavigateBoundingBoxHighlight = (highlightId: string): void => {
328
+ this.scrollToBoundingBoxHighlight(highlightId);
329
+ };
330
+
331
+ protected handleSelectBoundingBoxHighlight = (highlightId: string | null): void => {
332
+ this.selectBoundingBoxHighlight(highlightId);
333
+ };
334
+
335
+ protected handleSetViewMode = ({ viewMode }: { viewMode: ViewMode }): void => {
336
+ this.setViewMode(viewMode);
337
+ };
338
+
238
339
  protected hydrate(): void {
239
340
  // Redux dispatch method signature doesn't seem to like async actions
240
341
  this.store.dispatch<any>(store.fetchAnnotationsAction()); // eslint-disable-line @typescript-eslint/no-explicit-any
@@ -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
+ }
@@ -0,0 +1,67 @@
1
+ import * as React from 'react';
2
+ import BoundingBoxHighlightRect from './BoundingBoxHighlightRect';
3
+ import { BoundingBox } from '../../store/boundingBoxHighlights/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);
@@ -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
+ }
@@ -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;
@@ -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
+ }
@@ -0,0 +1,77 @@
1
+ import * as React from 'react';
2
+ import classNames from 'classnames';
3
+ import BoundingBoxHighlightNav from './BoundingBoxHighlightNav';
4
+ import { BoundingBox } from '../../store/boundingBoxHighlights/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,0 +1,4 @@
1
+ export { default as BoundingBoxHighlightList } from './BoundingBoxHighlightList';
2
+ export { default as BoundingBoxHighlightNav } from './BoundingBoxHighlightNav';
3
+ export { default as BoundingBoxHighlightRect } from './BoundingBoxHighlightRect';
4
+ export type { BoundingBox } from '../../store/boundingBoxHighlights/types';
@@ -0,0 +1,14 @@
1
+ import { defineMessages } from 'react-intl';
2
+
3
+ export default defineMessages({
4
+ ariaLabelViewNextReference: {
5
+ id: 'ba.boundingBoxHighlight.viewNextReference',
6
+ description: 'Aria label for the button to view the next reference',
7
+ defaultMessage: 'View next reference',
8
+ },
9
+ ariaLabelViewPrevReference: {
10
+ id: 'ba.boundingBoxHighlight.viewPrevReference',
11
+ description: 'Aria label for the button to view the previous reference',
12
+ defaultMessage: 'View previous reference',
13
+ },
14
+ });