Skip to content

Commit

Permalink
feat(voice): allow custom voice helper (#4363)
Browse files Browse the repository at this point in the history
* WIP: feat(voice): allow custom voice helper

If the `createVoiceHelper` function can be overridden, other third-party voice solutions can be used, without them needing to redo all the rendering & templating.

cc @dylanbfox

* test: make sure creator gets called

* change signature & exposed types

* update tests

* tsc

* chore: rename internal functions

* accept arbitrary state

* Revert "accept arbitrary state"

This reverts commit 4f40e97.

Co-authored-by: eunjae-lee <[email protected]>
Co-authored-by: Dustin Coates <[email protected]>
  • Loading branch information
3 people authored May 11, 2020
1 parent 547f6aa commit 4a00fa6
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 71 deletions.
8 changes: 2 additions & 6 deletions src/components/VoiceSearch/VoiceSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import { h } from 'preact';
import Template from '../Template/Template';

import { VoiceSearchTemplates } from '../../widgets/voice-search/voice-search';

import {
VoiceListeningState,
ToggleListening,
} from '../../lib/voiceSearchHelper';
import { VoiceListeningState } from '../../lib/voiceSearchHelper/types';

export type VoiceSearchComponentCSSClasses = {
root: string;
Expand All @@ -20,7 +16,7 @@ export type VoiceSearchProps = {
cssClasses: VoiceSearchComponentCSSClasses;
isBrowserSupported: boolean;
isListening: boolean;
toggleListening: ToggleListening;
toggleListening: () => void;
voiceListeningState: VoiceListeningState;
templates: VoiceSearchTemplates;
};
Expand Down
29 changes: 27 additions & 2 deletions src/connectors/voice-search/__tests__/connectVoiceSearch-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import connectVoiceSearch from '../connectVoiceSearch';

jest.mock('../../../lib/voiceSearchHelper', () => {
return ({ onStateChange, onQueryChange }) => {
let isListening = false;
return {
getState: () => {},
isBrowserSupported: () => true,
isListening: () => false,
toggleListening: () => {},
isListening: () => isListening,
startListening: () => {
isListening = !isListening;
},
dispose: jest.fn(),
// ⬇️ for test
changeState: () => onStateChange(),
Expand Down Expand Up @@ -65,6 +68,28 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/voice-searc
})
);
});

it('creates custom voice helper', () => {
const voiceHelper = {
isBrowserSupported: () => true,
dispose: () => {},
getState: () => ({
isSpeechFinal: true,
status: 'askingPermission',
transcript: '',
}),
isListening: () => true,
toggleListening: () => {},
};

const { widget } = getInitializedWidget({
widgetParams: {
createVoiceSearchHelper: () => voiceHelper,
},
});

expect(widget._voiceSearchHelper).toBe(voiceHelper);
});
});

it('calls renderFn during init and render', () => {
Expand Down
25 changes: 19 additions & 6 deletions src/connectors/voice-search/connectVoiceSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import {
noop,
} from '../../lib/utils';
import { Renderer, RendererOptions, WidgetFactory } from '../../types';
import createVoiceSearchHelper, {
import builtInCreateVoiceSearchHelper from '../../lib/voiceSearchHelper';
import {
CreateVoiceSearchHelper,
VoiceListeningState,
ToggleListening,
} from '../../lib/voiceSearchHelper';
} from '../../lib/voiceSearchHelper/types';

const withUsage = createDocumentationMessageGenerator({
name: 'voice-search',
Expand All @@ -21,12 +22,13 @@ export type VoiceSearchConnectorParams = {
additionalQueryParameters?: (params: {
query: string;
}) => PlainSearchParameters | void;
createVoiceSearchHelper?: CreateVoiceSearchHelper;
};

export type VoiceSearchRendererOptions<TVoiceSearchWidgetParams> = {
isBrowserSupported: boolean;
isListening: boolean;
toggleListening: ToggleListening;
toggleListening: () => void;
voiceListeningState: VoiceListeningState;
} & RendererOptions<TVoiceSearchWidgetParams>;

Expand Down Expand Up @@ -58,15 +60,25 @@ const connectVoiceSearch: VoiceSearchConnector = (
voiceSearchHelper: {
isBrowserSupported,
isListening,
toggleListening,
startListening,
stopListening,
getState,
},
}): void => {
renderFn(
{
isBrowserSupported: isBrowserSupported(),
isListening: isListening(),
toggleListening,
toggleListening() {
if (!isBrowserSupported()) {
return;
}
if (isListening()) {
stopListening();
} else {
startListening();
}
},
voiceListeningState: getState(),
widgetParams,
instantSearchInstance,
Expand All @@ -79,6 +91,7 @@ const connectVoiceSearch: VoiceSearchConnector = (
searchAsYouSpeak = false,
language,
additionalQueryParameters,
createVoiceSearchHelper = builtInCreateVoiceSearchHelper,
} = widgetParams;

return {
Expand Down
12 changes: 6 additions & 6 deletions src/lib/voiceSearchHelper/__tests__/index-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe('VoiceSearchHelper', () => {
onStateChange,
});

voiceSearchHelper.toggleListening();
voiceSearchHelper.startListening();
expect(onStateChange).toHaveBeenCalledTimes(1);
expect(voiceSearchHelper.getState().status).toEqual('askingPermission');
simulateListener.start();
Expand Down Expand Up @@ -132,7 +132,7 @@ describe('VoiceSearchHelper', () => {
onStateChange,
});

voiceSearchHelper.toggleListening();
voiceSearchHelper.startListening();
expect(onStateChange).toHaveBeenCalledTimes(1);
expect(voiceSearchHelper.getState().status).toEqual('askingPermission');
simulateListener.start();
Expand Down Expand Up @@ -169,7 +169,7 @@ describe('VoiceSearchHelper', () => {
onQueryChange,
onStateChange,
});
voiceSearchHelper.toggleListening();
voiceSearchHelper.startListening();
expect(voiceSearchHelper.getState().status).toEqual('askingPermission');
simulateListener.error({
error: 'not-allowed',
Expand All @@ -187,7 +187,7 @@ describe('VoiceSearchHelper', () => {
onQueryChange: () => {},
onStateChange: () => {},
});
voiceSearchHelper.toggleListening();
voiceSearchHelper.startListening();
voiceSearchHelper.dispose();
expect(stop).toHaveBeenCalledTimes(1);
});
Expand All @@ -202,8 +202,8 @@ describe('VoiceSearchHelper', () => {
onStateChange,
});

voiceSearchHelper.toggleListening();
voiceSearchHelper.toggleListening();
voiceSearchHelper.startListening();
voiceSearchHelper.stopListening();
expect(voiceSearchHelper.getState().status).toBe('finished');
});
});
58 changes: 10 additions & 48 deletions src/lib/voiceSearchHelper/index.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,11 @@
export type VoiceSearchHelperParams = {
searchAsYouSpeak: boolean;
language?: string;
onQueryChange: (query: string) => void;
onStateChange: () => void;
};

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

export type VoiceListeningState = {
status: Status;
transcript: string;
isSpeechFinal: boolean;
errorCode?: SpeechRecognitionErrorCode;
};
import { CreateVoiceSearchHelper, Status, VoiceListeningState } from './types';

export type VoiceSearchHelper = {
getState: () => VoiceListeningState;
isBrowserSupported: () => boolean;
isListening: () => boolean;
toggleListening: () => void;
dispose: () => void;
};

export type ToggleListening = () => void;

export default function createVoiceSearchHelper({
const createVoiceSearchHelper: CreateVoiceSearchHelper = function createVoiceSearchHelper({
searchAsYouSpeak,
language,
onQueryChange,
onStateChange,
}: VoiceSearchHelperParams): VoiceSearchHelper {
}) {
const SpeechRecognitionAPI: new () => SpeechRecognition =
(window as any).webkitSpeechRecognition ||
(window as any).SpeechRecognition;
Expand Down Expand Up @@ -100,7 +70,7 @@ export default function createVoiceSearchHelper({
}
};

const start = (): void => {
const startListening = (): void => {
recognition = new SpeechRecognitionAPI();
if (!recognition) {
return;
Expand Down Expand Up @@ -131,30 +101,22 @@ export default function createVoiceSearchHelper({
recognition = undefined;
};

const stop = (): void => {
const stopListening = (): 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;
}
if (isListening()) {
stop();
} else {
start();
}
};

return {
getState,
isBrowserSupported,
isListening,
toggleListening,
startListening,
stopListening,
dispose,
};
}
};

export default createVoiceSearchHelper;
34 changes: 34 additions & 0 deletions src/lib/voiceSearchHelper/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export type Status =
| 'initial'
| 'askingPermission'
| 'waiting'
| 'recognizing'
| 'finished'
| 'error';

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

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

export type VoiceSearchHelper = {
getState: () => VoiceListeningState;
isBrowserSupported: () => boolean;
isListening: () => boolean;
startListening: () => void;
stopListening: () => void;
dispose: () => void;
};

export type CreateVoiceSearchHelper = (
params: VoiceSearchHelperParams
) => VoiceSearchHelper;
30 changes: 28 additions & 2 deletions src/widgets/voice-search/__tests__/voice-search-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
import { createSingleSearchResponse } from '../../../../test/mock/createAPIResponse';
import { castToJestMock } from '../../../../test/utils/castToJestMock';
import { Widget } from '../../../types';
import voiceSearch from '../voice-search';
import voiceSearch, { VoiceSearchWidgetParams } from '../voice-search';
import { VoiceSearchHelper } from '../../../lib/voiceSearchHelper/types';

const render = castToJestMock(preactRender);

Expand All @@ -31,7 +32,9 @@ type DefaultSetupWrapper = {
widgetRender: (helper: Helper) => void;
};

function defaultSetup(opts = {}): DefaultSetupWrapper {
function defaultSetup(
opts: Omit<VoiceSearchWidgetParams, 'container'> = {}
): DefaultSetupWrapper {
const container = document.createElement('div');
const widget = voiceSearch({ container, ...opts });

Expand Down Expand Up @@ -92,6 +95,29 @@ describe('voiceSearch()', () => {
See documentation: https://www.algolia.com/doc/api-reference/widgets/voice-search/js/"
`);
});

it('creates custom voice helper', () => {
const voiceHelper: VoiceSearchHelper = {
isBrowserSupported: () => true,
dispose: () => {},
getState: () => ({
isSpeechFinal: true,
status: 'askingPermission',
transcript: '',
}),
isListening: () => true,
startListening: () => {},
stopListening: () => {},
};

const { widgetInit, widget } = defaultSetup({
createVoiceSearchHelper: () => voiceHelper,
});

widgetInit(helper);

expect((widget as any)._voiceSearchHelper).toBe(voiceHelper);
});
});

describe('Rendering', () => {
Expand Down
6 changes: 5 additions & 1 deletion src/widgets/voice-search/voice-search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import VoiceSearch, {
} from '../../components/VoiceSearch/VoiceSearch';
import defaultTemplates from './defaultTemplates';
import { WidgetFactory, Template } from '../../types';
import { CreateVoiceSearchHelper } from '../../lib/voiceSearchHelper/types';

const withUsage = createDocumentationMessageGenerator({ name: 'voice-search' });
const suit = component('VoiceSearch');
Expand All @@ -40,7 +41,7 @@ export type VoiceSearchTemplates = {
status: Template<VoiceSearchTemplateProps>;
};

type VoiceSearchWidgetParams = {
export type VoiceSearchWidgetParams = {
container: string | HTMLElement;
cssClasses?: Partial<VoiceSearchCSSClasses>;
templates?: Partial<VoiceSearchTemplates>;
Expand All @@ -49,6 +50,7 @@ type VoiceSearchWidgetParams = {
additionalQueryParameters?: (params: {
query: string;
}) => PlainSearchParameters | void;
createVoiceSearchHelper?: CreateVoiceSearchHelper;
};

type VoiceSearchRendererWidgetParams = {
Expand Down Expand Up @@ -89,6 +91,7 @@ const voiceSearch: VoiceSearch = (
searchAsYouSpeak = false,
language,
additionalQueryParameters,
createVoiceSearchHelper,
} = {} as VoiceSearchWidgetParams
) => {
if (!container) {
Expand All @@ -114,6 +117,7 @@ const voiceSearch: VoiceSearch = (
searchAsYouSpeak,
language,
additionalQueryParameters,
createVoiceSearchHelper,
});
};

Expand Down

0 comments on commit 4a00fa6

Please sign in to comment.