Skip to content

Commit

Permalink
fix(voiceSearch): remove event listeners on stop (#3845)
Browse files Browse the repository at this point in the history
* fix(voiceSearch): add better types for VoiceSearchHelper

* fix(voiceSearch): remove event listeners when stopping voice recognition

* fix(voiceSearch): fix wrong status when stopped

* chore(voiceSearch): fix lint error

* test(voiceSearch): add test to ensure status becomes finished on stop

* fix(types): add return types

* chore(voiceSearch): replace ErrorCode with SpeechRecognitionErrorCode
  • Loading branch information
Eunjae Lee authored Jun 13, 2019
1 parent 2ede778 commit 688e36a
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 31 deletions.
2 changes: 1 addition & 1 deletion src/components/VoiceSearch/__tests__/VoiceSearch-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe('VoiceSearch', () => {
});

it('with custom template for status', () => {
const props = {
const props: VoiceSearchProps = {
...defaultProps,
isListening: true,
voiceListeningState: {
Expand Down
15 changes: 15 additions & 0 deletions src/lib/voiceSearchHelper/__tests__/index-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,19 @@ describe('VoiceSearchHelper', () => {
voiceSearchHelper.dispose();
expect(stop).toHaveBeenCalledTimes(1);
});

it('stops and the status becomes `finished`', () => {
window.SpeechRecognition = createFakeSpeechRecognition();
const onQueryChange = (): void => {};
const onStateChange = (): void => {};
const voiceSearchHelper = createVoiceSearchHelper({
searchAsYouSpeak: true,
onQueryChange,
onStateChange,
});

voiceSearchHelper.toggleListening();
voiceSearchHelper.toggleListening();
expect(voiceSearchHelper.getState().status).toBe('finished');
});
});
61 changes: 31 additions & 30 deletions src/lib/voiceSearchHelper/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
const STATUS_INITIAL = 'initial';
const STATUS_ASKING_PERMISSION = 'askingPermission';
const STATUS_WAITING = 'waiting';
const STATUS_RECOGNIZING = 'recognizing';
const STATUS_FINISHED = 'finished';
const STATUS_ERROR = 'error';

export type VoiceSearchHelperParams = {
searchAsYouSpeak: boolean;
onQueryChange: (query: string) => void;
onStateChange: () => void;
};

export type Status =
| 'initial'
| 'askingPermission'
| 'waiting'
| 'recognizing'
| 'finished'
| 'error';

export type VoiceListeningState = {
status: string;
status: Status;
transcript: string;
isSpeechFinal: boolean;
errorCode?: string;
errorCode?: SpeechRecognitionErrorCode;
};

export type VoiceSearchHelper = {
Expand All @@ -36,46 +37,46 @@ export default function createVoiceSearchHelper({
const SpeechRecognitionAPI: new () => SpeechRecognition =
(window as any).webkitSpeechRecognition ||
(window as any).SpeechRecognition;
const getDefaultState = (status: string): VoiceListeningState => ({
const getDefaultState = (status: Status): VoiceListeningState => ({
status,
transcript: '',
isSpeechFinal: false,
errorCode: undefined,
});
let state: VoiceListeningState = getDefaultState(STATUS_INITIAL);
let state: VoiceListeningState = getDefaultState('initial');
let recognition: SpeechRecognition | undefined;

const isBrowserSupported = (): boolean => Boolean(SpeechRecognitionAPI);

const isListening = (): boolean =>
state.status === STATUS_ASKING_PERMISSION ||
state.status === STATUS_WAITING ||
state.status === STATUS_RECOGNIZING;
state.status === 'askingPermission' ||
state.status === 'waiting' ||
state.status === 'recognizing';

const setState = (newState = {}): void => {
const setState = (newState: Partial<VoiceListeningState> = {}): void => {
state = { ...state, ...newState };
onStateChange();
};

const getState = (): VoiceListeningState => state;

const resetState = (status = STATUS_INITIAL): void => {
const resetState = (status: Status = 'initial'): void => {
setState(getDefaultState(status));
};

const onStart = (): void => {
setState({
status: STATUS_WAITING,
status: 'waiting',
});
};

const onError = (event: SpeechRecognitionError): void => {
setState({ status: STATUS_ERROR, errorCode: event.error });
setState({ status: 'error', errorCode: event.error });
};

const onResult = (event: SpeechRecognitionEvent): void => {
setState({
status: STATUS_RECOGNIZING,
status: 'recognizing',
transcript:
(event.results[0] &&
event.results[0][0] &&
Expand All @@ -92,25 +93,17 @@ export default function createVoiceSearchHelper({
if (!state.errorCode && state.transcript && !searchAsYouSpeak) {
onQueryChange(state.transcript);
}
if (state.status !== STATUS_ERROR) {
setState({ status: STATUS_FINISHED });
if (state.status !== 'error') {
setState({ status: 'finished' });
}
};

const stop = (): void => {
if (recognition) {
recognition.stop();
recognition = undefined;
}
resetState();
};

const start = (): void => {
recognition = new SpeechRecognitionAPI();
if (!recognition) {
return;
}
resetState(STATUS_ASKING_PERMISSION);
resetState('askingPermission');
recognition.interimResults = true;
recognition.addEventListener('start', onStart);
recognition.addEventListener('error', onError);
Expand All @@ -131,6 +124,14 @@ export default function createVoiceSearchHelper({
recognition = undefined;
};

const stop = (): void => {
dispose();
// Because `dispose` removes event listeners, `end` listener is not called.
// So we're setting the `status` as `finished` here.
// If we don't do it, it will be still `waiting` or `recognizing`.
resetState('finished');
};

const toggleListening = (): void => {
if (!isBrowserSupported()) {
return;
Expand Down

0 comments on commit 688e36a

Please sign in to comment.