Skip to content

Commit f445cef

Browse files
author
Mingze
authored
feat(drawing): Support drawing colors (#653)
* feat(drawing): Support drawing colors * feat(drawing): Address comments
1 parent 553b566 commit f445cef

16 files changed

+125
-29
lines changed

src/@types/events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ enum Event {
88
ANNOTATION_REMOVE = 'annotations_remove',
99
ANNOTATIONS_INITIALIZED = 'annotations_initialized',
1010
ANNOTATIONS_MODE_CHANGE = 'annotations_mode_change',
11+
COLOR_SET = 'annotations_color_set',
1112
VISIBLE_SET = 'annotations_visible_set',
1213
}
1314

src/common/BaseAnnotator.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export type Options = {
3434
};
3535
fileOptions?: FileOptions;
3636
hasTouch?: boolean;
37+
initialColor?: string;
3738
initialMode?: store.Mode;
3839
intl: IntlOptions;
3940
locale?: string;
@@ -60,7 +61,17 @@ export default class BaseAnnotator extends EventEmitter {
6061

6162
store: store.AppStore;
6263

63-
constructor({ apiHost, container, features = {}, file, fileOptions, initialMode, intl, token }: Options) {
64+
constructor({
65+
apiHost,
66+
container,
67+
features = {},
68+
file,
69+
fileOptions,
70+
initialColor,
71+
initialMode,
72+
intl,
73+
token,
74+
}: Options) {
6475
super();
6576

6677
const fileOptionsValue = fileOptions?.[file.id];
@@ -72,7 +83,7 @@ export default class BaseAnnotator extends EventEmitter {
7283
annotations: {
7384
activeId: fileOptionsValue?.annotations?.activeId ?? null,
7485
},
75-
common: { mode: initialMode },
86+
common: { color: initialColor, mode: initialMode },
7687
options: {
7788
features,
7889
fileId: file.id,
@@ -93,6 +104,7 @@ export default class BaseAnnotator extends EventEmitter {
93104
this.addListener(LegacyEvent.SCALE, this.handleScale);
94105
this.addListener(Event.ACTIVE_SET, this.handleSetActive);
95106
this.addListener(Event.ANNOTATION_REMOVE, this.handleRemove);
107+
this.addListener(Event.COLOR_SET, this.handleColorSet);
96108
this.addListener(Event.VISIBLE_SET, this.handleSetVisible);
97109

98110
// Load any required data at startup
@@ -155,6 +167,10 @@ export default class BaseAnnotator extends EventEmitter {
155167
this.store.dispatch(store.setActiveAnnotationIdAction(annotationId));
156168
}
157169

170+
public setColor(color: string): void {
171+
this.store.dispatch(store.setColorAction(color));
172+
}
173+
158174
public setVisibility(visibility: boolean): void {
159175
if (!this.containerEl) {
160176
return;
@@ -195,6 +211,10 @@ export default class BaseAnnotator extends EventEmitter {
195211
this.setVisibility(visibility);
196212
};
197213

214+
protected handleColorSet = (color: string): void => {
215+
this.setColor(color);
216+
};
217+
198218
protected hydrate(): void {
199219
// Redux dispatch method signature doesn't seem to like async actions
200220
this.store.dispatch<any>(store.fetchAnnotationsAction()); // eslint-disable-line @typescript-eslint/no-explicit-any

src/common/__tests__/BaseAnnotator-test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,11 @@ describe('BaseAnnotator', () => {
152152
expect(annotator.features).toEqual(features);
153153
});
154154

155-
test('should set initial mode', () => {
156-
initAnnotator({ initialMode: Mode.REGION });
155+
test('should set initial mode and color', () => {
156+
initAnnotator({ initialColor: '#000', initialMode: Mode.REGION });
157157

158158
expect(store.createStore).toHaveBeenLastCalledWith(
159-
expect.objectContaining({ common: { mode: Mode.REGION } }),
159+
expect.objectContaining({ common: { color: '#000', mode: Mode.REGION } }),
160160
expect.any(Object),
161161
);
162162
});
@@ -229,6 +229,7 @@ describe('BaseAnnotator', () => {
229229
jest.spyOn(annotator, 'init');
230230
jest.spyOn(annotator, 'removeAnnotation');
231231
jest.spyOn(annotator, 'setActiveId');
232+
jest.spyOn(annotator, 'setColor');
232233
jest.spyOn(annotator, 'setVisibility');
233234
});
234235

@@ -244,6 +245,9 @@ describe('BaseAnnotator', () => {
244245

245246
annotator.emit(Event.VISIBLE_SET, true);
246247
expect(annotator.setVisibility).toHaveBeenCalledWith(true);
248+
249+
annotator.emit(Event.COLOR_SET, '#000');
250+
expect(annotator.setColor).toHaveBeenCalledWith('#000');
247251
});
248252
});
249253

src/drawing/DrawingAnnotations.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type Props = {
1616
addDrawingPathGroup: (pathGroup: PathGroup) => void;
1717
annotations: AnnotationDrawing[];
1818
canShowPopupToolbar: boolean;
19+
color: string;
1920
drawnPathGroups: Array<PathGroup>;
2021
isCreating: boolean;
2122
location: number;
@@ -36,6 +37,7 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
3637
addDrawingPathGroup,
3738
annotations,
3839
canShowPopupToolbar,
40+
color,
3941
drawnPathGroups,
4042
isCreating,
4143
location,
@@ -119,7 +121,12 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
119121
)}
120122

121123
{isCreating && (
122-
<DrawingCreator className="ba-DrawingAnnotations-creator" onStart={handleStart} onStop={handleStop} />
124+
<DrawingCreator
125+
className="ba-DrawingAnnotations-creator"
126+
color={color}
127+
onStart={handleStart}
128+
onStop={handleStop}
129+
/>
123130
)}
124131

125132
{isCreating && hasPathGroups && drawingSVGGroup && canShowPopupToolbar && (

src/drawing/DrawingAnnotationsContainer.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
getActiveAnnotationId,
1717
getAnnotationMode,
1818
getAnnotationsForLocation,
19+
getColor,
1920
getCreatorStatus,
2021
Mode,
2122
setActiveAnnotationIdAction,
@@ -29,6 +30,7 @@ import { setupDrawingAction } from './actions';
2930
export type Props = {
3031
activeAnnotationId: string | null;
3132
annotations: AnnotationDrawing[];
33+
color: string;
3234
canShowPopupToolbar: boolean;
3335
drawnPathGroups: Array<PathGroup>;
3436
isCreating: boolean;
@@ -41,6 +43,7 @@ export const mapStateToProps = (state: AppState, { location }: { location: numbe
4143
return {
4244
activeAnnotationId: getActiveAnnotationId(state),
4345
annotations: getAnnotationsForLocation(state, location).filter(isDrawing),
46+
color: getColor(state),
4447
canShowPopupToolbar: creatorStatus === CreatorStatus.started,
4548
drawnPathGroups: getDrawingDrawnPathGroupsForLocation(state, location),
4649
isCreating: getAnnotationMode(state) === Mode.DRAWING && creatorStatus !== CreatorStatus.pending,

src/drawing/DrawingCreator.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,35 @@ import DrawingPathGroup from './DrawingPathGroup';
66
import DrawingSVG, { DrawingSVGRef } from './DrawingSVG';
77
import PointerCapture, { PointerCaptureRef, Status as DrawingStatus } from '../components/PointerCapture';
88
import { getPathCommands } from './drawingUtil';
9-
import { PathGroup, Position, Stroke } from '../@types';
9+
import { PathGroup, Position } from '../@types';
1010
import './DrawingCreator.scss';
1111

1212
export type Props = {
1313
className?: string;
14+
color?: string;
1415
onStart: () => void;
1516
onStop: (pathGroup: PathGroup) => void;
16-
stroke?: Stroke;
17+
size?: number;
1718
};
1819

19-
export const defaultStroke = {
20-
color: bdlBoxBlue,
21-
size: 4,
22-
};
20+
export const defaultStrokeColor = bdlBoxBlue;
21+
export const defaultStrokeSize = 4;
2322

24-
export default function DrawingCreator({ className, onStart, onStop, stroke = defaultStroke }: Props): JSX.Element {
23+
export default function DrawingCreator({
24+
className,
25+
color = defaultStrokeColor,
26+
onStart,
27+
onStop,
28+
size = defaultStrokeSize,
29+
}: Props): JSX.Element {
2530
const [drawingStatus, setDrawingStatus] = React.useState<DrawingStatus>(DrawingStatus.init);
2631
const capturedPointsRef = React.useRef<Array<Position>>([]);
2732
const creatorElRef = React.useRef<PointerCaptureRef>(null);
2833
const drawingDirtyRef = React.useRef<boolean>(false);
2934
const drawingPathRef = React.useRef<DrawingPathRef>(null);
3035
const drawingSVGRef = React.useRef<DrawingSVGRef>(null);
3136
const renderHandleRef = React.useRef<number | null>(null);
37+
const stroke = { color, size };
3238

3339
const getPoints = React.useCallback((): Array<Position> => {
3440
const { current: creatorEl } = creatorElRef;

src/drawing/__tests__/DrawingAnnotations-test.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ describe('DrawingAnnotations', () => {
1818
addDrawingPathGroup: jest.fn(),
1919
annotations: [],
2020
canShowPopupToolbar: false,
21+
color: '#000',
2122
drawnPathGroups: [],
2223
isCreating: false,
2324
location: 0,
@@ -211,6 +212,12 @@ describe('DrawingAnnotations', () => {
211212
expect(wrapper.find(PopupDrawingToolbar).hasClass('ba-is-drawing')).toBe(true);
212213
});
213214

215+
test('should apply selected color', () => {
216+
const wrapper = getWrapper({ color: '#111', isCreating: true });
217+
218+
expect(wrapper.find(DrawingCreator).prop('color')).toBe('#111');
219+
});
220+
214221
test.each`
215222
drawn | stashed | canComment | canRedo | canUndo
216223
${pathGroups} | ${[]} | ${true} | ${false} | ${true}

src/drawing/__tests__/DrawingAnnotationsContainer-test.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@ import { createStore, CreatorStatus, Mode } from '../../store';
99
jest.mock('../../common/withProviders');
1010

1111
describe('DrawingAnnotationsContainer', () => {
12+
const initialState = {
13+
common: { color: '#000' },
14+
};
15+
1216
const getDefaults = (): {
1317
intl: IntlShape;
1418
location: number;
1519
store: Store;
1620
} => ({
1721
intl: {} as IntlShape,
1822
location: 1,
19-
store: createStore(),
23+
store: createStore(initialState),
2024
});
2125
const getWrapper = (props = {}): ReactWrapper<Props> =>
2226
mount(<DrawingAnnotationsContainer {...getDefaults()} {...props} />);
@@ -31,6 +35,7 @@ describe('DrawingAnnotationsContainer', () => {
3135
addDrawingPathGroup: expect.any(Function),
3236
annotations: [],
3337
canShowPopupToolbar: false,
38+
color: '#000',
3439
drawnPathGroups: [],
3540
isCreating: false,
3641
location: 1,

src/drawing/__tests__/DrawingCreator-test.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React from 'react';
22
import { act } from 'react-dom/test-utils';
33
import { mount, ReactWrapper } from 'enzyme';
4-
import DrawingCreator, { defaultStroke, Props } from '../DrawingCreator';
4+
import DrawingCreator, { defaultStrokeColor, defaultStrokeSize, Props } from '../DrawingCreator';
55
import DrawingPath from '../DrawingPath';
6+
import DrawingPathGroup from '../DrawingPathGroup';
67
import DrawingSVG from '../DrawingSVG';
78
import PointerCapture, { Props as PointerCaptureProps } from '../../components/PointerCapture';
89

@@ -81,6 +82,14 @@ describe('DrawingCreator', () => {
8182
expect(wrapper.find(PointerCapture).exists()).toBe(true);
8283
expect(wrapper.find(DrawingSVG).exists()).toBe(true);
8384
});
85+
86+
test('should render DrawingPathGroup with right stroke color', () => {
87+
const wrapper = getWrapper({ color: '#111' });
88+
89+
simulateDrawStart(wrapper.find(PointerCapture));
90+
91+
expect(wrapper.find(DrawingPathGroup).prop('stroke')).toMatchObject({ color: '#111' });
92+
});
8493
});
8594

8695
describe('onStart()', () => {
@@ -127,7 +136,10 @@ describe('DrawingCreator', () => {
127136
],
128137
},
129138
],
130-
stroke: defaultStroke,
139+
stroke: {
140+
color: defaultStrokeColor,
141+
size: defaultStrokeSize,
142+
},
131143
});
132144
});
133145

@@ -160,15 +172,18 @@ describe('DrawingCreator', () => {
160172
],
161173
},
162174
],
163-
stroke: defaultStroke,
175+
stroke: {
176+
color: defaultStrokeColor,
177+
size: defaultStrokeSize,
178+
},
164179
});
165180
},
166181
);
167182

168183
test('should bound the drawn points by the boundary of the PointerCapture element based on the provided stroke size', () => {
169184
const customStroke = { color: '#000', size: 10 };
170185
const onStop = jest.fn();
171-
const wrapper = getWrapper({ onStop, stroke: customStroke });
186+
const wrapper = getWrapper({ onStop, ...customStroke });
172187

173188
simulateDrawStart(wrapper.find(PointerCapture));
174189

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Mode } from '../types';
22

33
export default {
4+
color: '#000',
45
mode: Mode.NONE,
56
};

0 commit comments

Comments
 (0)