Skip to content
This repository has been archived by the owner on Dec 30, 2022. It is now read-only.

feat(voiceSearch): add voice search widget #2316

Merged
merged 81 commits into from
Jun 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
881c414
feat(voiceSearch): WIP add VoiceSearch component
Apr 8, 2019
9e066d0
feat(voiceSearch): WIP add voice search helper
Apr 9, 2019
f24ee74
feat(voiceSearch): WIP adding connector
Apr 12, 2019
d23b3d5
Update packages/react-instantsearch-dom/src/components/VoiceSearch.tsx
Haroenv Apr 15, 2019
d7eb9ed
Update packages/react-instantsearch-dom/src/components/VoiceSearch.tsx
Haroenv Apr 15, 2019
1ccc9e2
Update packages/react-instantsearch-dom/src/components/VoiceSearch.tsx
Haroenv Apr 15, 2019
aebd6e2
Update packages/react-instantsearch-dom/src/components/VoiceSearch.tsx
Haroenv Apr 15, 2019
aff57a8
Update packages/react-instantsearch-dom/src/components/VoiceSearch.tsx
Haroenv Apr 15, 2019
1bd9f5c
Apply suggestions from code review
Haroenv Apr 15, 2019
9bd9c68
feat(voiceSearch): rename variables
Apr 15, 2019
ec2f150
feat(voiceSearch): rename helper to voiceSearch
Apr 15, 2019
b3b6093
type(voiceSearch): fix lint error
Apr 15, 2019
7e8d3c3
test(voiceSearch): add tests for component
Apr 15, 2019
b2bd80d
chore(voiceSearch): fix lint error
Apr 15, 2019
b3118ec
chore(voiceSearch): disable tslint for unused parameter
Apr 15, 2019
e1f73fa
test(voiceSearch): add stories
Apr 15, 2019
761a01b
fix(voiceSearch): fix wrong props
Apr 15, 2019
3c9700e
chore(voiceSearch): fix lint error
Apr 16, 2019
ed3e6cc
test(voiceSearch): add story for custom button
Apr 16, 2019
30e1292
chore(voiceSearch): change to curly brace
Apr 16, 2019
78a77ba
Apply suggestions from code review
francoischalifour Apr 16, 2019
200dcbf
Merge branch 'master' into feat/voice-search
Apr 16, 2019
73c7729
Merge branch 'feat/voice-search' of github.com:algolia/react-instants…
Apr 16, 2019
91260ad
chore(voiceSearch): remove comments since the config is now global
Apr 16, 2019
b53ed87
chore(voiceSearch): add default value to searchAsYouSpeak and put typ…
Apr 16, 2019
4481bd4
chore(voiceSearch): clean up svg
Apr 16, 2019
5a2b9ae
fix(voiceSearch): fix class name for status
Apr 16, 2019
b4067ca
chore(voiceSearch): remove unnecessary comment
Apr 16, 2019
c28a6bc
test(voiceSearch): remove unused test case
Apr 16, 2019
a4b3aac
test(voiceSearch): change way to match snapshot
Apr 16, 2019
bb193c5
chore(voiceSearch): remove 'else'
Apr 17, 2019
b04cf89
chore(voiceSearch): remove unnecessary $
Apr 17, 2019
e7c1119
test(voiceSearch): add stories
Apr 17, 2019
31cf985
fix(voiceSearch): provide one voiceSearchHelper instancer per component
Apr 17, 2019
38d89ed
refactor(voiceSearch): move voiceSearchHelper-related logic from the …
Apr 18, 2019
626e446
test(voiceSearch): fix tests for the component
Apr 19, 2019
ec305c5
test(voiceSearch): add tests for VoiceSearchHelper (copied from IS.js)
Apr 19, 2019
548bc0d
Merge branch 'master' into feat/voice-search
Apr 23, 2019
f623c5a
chore(voiceSearch): increase limit of bundlesize
Apr 23, 2019
a1808db
test(voiceSearch): improve tests
Apr 23, 2019
870dad6
chore(voiceSearch): increase bundlesize limit
Apr 29, 2019
bec7ba6
Merge branch 'master' into feat/voice-search
Apr 29, 2019
69e5582
chore(voiceSearch): increase limit of bundlesize
Apr 29, 2019
16deb29
Merge branch 'feat/voice-search' of github.com:algolia/react-instants…
Apr 29, 2019
918ef36
Update packages/react-instantsearch-dom/src/components/VoiceSearch.tsx
francoischalifour May 6, 2019
7ca40f2
chore(voiceSearch): update voiceSearchHelper from IS.js
May 6, 2019
bf5d46b
chore(voiceSearch): change code using Fragment
May 14, 2019
64ff987
type(voiceSearch): Update packages/react-instantsearch-core/src/types…
eunjae-lee May 14, 2019
94119f1
Merge branch 'master' into feat/voice-search
May 14, 2019
100e0c0
chore(voiceSearch): use defaultProps
May 14, 2019
a5ec2a1
Merge branch 'feat/voice-search' of github.com:algolia/react-instants…
May 14, 2019
5fa8fa0
fix(types): improve types for voiceSearch
May 15, 2019
4d17a68
test(voiceSearch): improve test
May 15, 2019
dbca103
fix(voiceSearch): rename buttonComponent to buttonTextComponent
May 15, 2019
82d5e6f
chore(voiceSearch): change the path to import directly from source
May 15, 2019
49a1aaa
type(voiceSearch): add accessibility modifier to defaultProps
May 15, 2019
53db919
test(voiceSearch): extract styles from stories to util.css
May 16, 2019
de79d8d
test(voiceSearch): clean up mocks after each test
May 16, 2019
8f8e971
feat(voiceSearch): add dispose to voiceSearchHelper
May 16, 2019
0d23082
chore(voiceSearch): rename voiceSearchHelper to createVoiceSearchHelper
May 16, 2019
92cf94f
feat(voiceSearch): dispose voiceSearchHelper on unmount
May 16, 2019
48d6dc4
chore(voiceSearch): extract svg element to a component to remove dupl…
May 16, 2019
9977f73
type(voiceSearch): add accessibility modifier
May 16, 2019
1df924b
chore: update bundlesize
May 16, 2019
d2ae4aa
Merge branch 'master' into feat/voice-search
May 16, 2019
643cf34
chore: adjust bundlesize
May 16, 2019
fc0c772
Merge branch 'feat/voice-search' of github.com:algolia/react-instants…
May 16, 2019
5f2ef47
Merge branch 'master' into feat/voice-search
May 20, 2019
c1a3a31
test(voiceSearch): fix wrong cleanup
May 27, 2019
b93918a
fix(voiceSearch): use defaultProps for searchAsYouSpeak
May 27, 2019
11bbf8b
fix(types): introduce Status, ErrorCode types
May 28, 2019
085d58f
fix(voiceSearch): initialize voiceSearchHelper at componentDidMount
May 28, 2019
e5bb1ad
fix(voiceSearch): stopping voice recognition also removes event liste…
May 28, 2019
75a0ee7
Merge branch 'master' into feat/voice-search
May 28, 2019
6509e7c
chore(voiceSearch): rename index.js to index.ts and modify rollup config
May 29, 2019
9656c5a
chore(voiceSearch): fix wrong prop name on story
May 29, 2019
e6515d2
Merge branch 'master' into feat/voice-search
May 31, 2019
a0d9b3f
Merge branch 'master' into feat/voice-search
May 31, 2019
4867016
feat(voiceSearch): export createVoiceSearchHelper for users to create…
May 31, 2019
7f9b84d
Merge branch 'master' into feat/voice-search
Jun 3, 2019
d0b843d
Merge branch 'master' into feat/voice-search
Jun 4, 2019
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,15 @@
},
{
"path": "packages/react-instantsearch/dist/umd/Dom.min.js",
"maxSize": "62.00 kB"
"maxSize": "64.20 kB"
},
{
"path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js",
"maxSize": "41.50 kB"
},
{
"path": "packages/react-instantsearch-dom/dist/umd/ReactInstantSearchDOM.min.js",
"maxSize": "63.00 kB"
"maxSize": "64.00 kB"
},
{
"path": "packages/react-instantsearch-dom-maps/dist/umd/ReactInstantSearchDOMMaps.min.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/react-instantsearch-core/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const plugins = [
];

const createConfiguration = ({ name, minify = false } = {}) => ({
input: 'src/index.js',
input: 'src/index.ts',
external: ['react'],
output: {
file: `dist/umd/ReactInstantSearch${name}${minify ? '.min' : ''}.js`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,6 @@ export {
default as connectToggleRefinement,
} from './connectors/connectToggleRefinement';
export { default as connectHitInsights } from './connectors/connectHitInsights';

// Types
export * from './types';
1 change: 1 addition & 0 deletions packages/react-instantsearch-core/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './translatable';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Translate = (key: string, ...params: any[]) => string;
157 changes: 157 additions & 0 deletions packages/react-instantsearch-dom/src/components/VoiceSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React, { Component } from 'react';
import { translatable, Translate } from 'react-instantsearch-core';
import { createClassNames } from '../core/utils';
import createVoiceSearchHelper, {
VoiceSearchHelper,
VoiceListeningState,
Status,
ErrorCode,
} from '../lib/voiceSearchHelper';
const cx = createClassNames('VoiceSearch');

type InnerComponentProps = {
status: Status;
errorCode?: ErrorCode;
isListening: boolean;
transcript: string;
isSpeechFinal: boolean;
isBrowserSupported: boolean;
};

type VoiceSearchProps = {
searchAsYouSpeak: boolean;
refine: (query: string) => void;
translate: Translate;
buttonTextComponent: React.FC<InnerComponentProps>;
statusComponent: React.FC<InnerComponentProps>;
};

const ButtonSvg = ({ children }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
{children}
</svg>
);

const DefaultButtonText: React.FC<InnerComponentProps> = ({
status,
errorCode,
isListening,
}) => {
return status === 'error' && errorCode === 'not-allowed' ? (
<ButtonSvg>
eunjae-lee marked this conversation as resolved.
Show resolved Hide resolved
<line x1="1" y1="1" x2="23" y2="23" />
<path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6" />
<path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23" />
<line x1="12" y1="19" x2="12" y2="23" />
<line x1="8" y1="23" x2="16" y2="23" />
</ButtonSvg>
) : (
<ButtonSvg>
<path
d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"
fill={isListening ? 'currentColor' : ''}
/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
<line x1="12" y1="19" x2="12" y2="23" />
<line x1="8" y1="23" x2="16" y2="23" />
</ButtonSvg>
);
};

const DefaultStatus: React.FC<InnerComponentProps> = ({ transcript }) => (
<p>{transcript}</p>
);

class VoiceSearch extends Component<VoiceSearchProps, VoiceListeningState> {
protected static defaultProps = {
searchAsYouSpeak: false,
buttonTextComponent: DefaultButtonText,
statusComponent: DefaultStatus,
};
private voiceSearchHelper?: VoiceSearchHelper;

public componentDidMount() {
const { searchAsYouSpeak, refine } = this.props;
this.voiceSearchHelper = createVoiceSearchHelper({
eunjae-lee marked this conversation as resolved.
Show resolved Hide resolved
searchAsYouSpeak,
onQueryChange: query => refine(query),
onStateChange: () => {
this.setState(this.voiceSearchHelper!.getState());
},
});
this.setState(this.voiceSearchHelper.getState());
}

public render() {
if (!this.voiceSearchHelper) {
return null;
}

const { status, transcript, isSpeechFinal, errorCode } = this.state;
const { isListening, isBrowserSupported } = this.voiceSearchHelper;
const {
translate,
buttonTextComponent: ButtonTextComponent,
statusComponent: StatusComponent,
} = this.props;
const innerProps: InnerComponentProps = {
status,
errorCode,
isListening: isListening(),
transcript,
isSpeechFinal,
isBrowserSupported: isBrowserSupported(),
};

return (
<div className={cx('')}>
<button
className={cx('button')}
type="button"
title={
isBrowserSupported()
? translate('buttonTitle')
: translate('disabledButtonTitle')
}
onClick={this.onClick}
disabled={!isBrowserSupported()}
>
<ButtonTextComponent {...innerProps} />
</button>
<div className={cx('status')}>
<StatusComponent {...innerProps} />
</div>
</div>
);
}

public componentWillUnmount() {
if (this.voiceSearchHelper) {
this.voiceSearchHelper.dispose();
}
}

private onClick = (event: React.MouseEvent<HTMLElement>) => {
if (!this.voiceSearchHelper) {
return;
}
event.currentTarget.blur();
const { toggleListening } = this.voiceSearchHelper;
toggleListening();
};
}

export default translatable({
buttonTitle: 'Search by voice',
disabledButtonTitle: 'Search by voice (not supported on this browser)',
})(VoiceSearch);
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React from 'react';
import Enzyme, { mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import VoiceSearch from '../VoiceSearch';

const mockGetState = jest.fn().mockImplementation(() => ({}));
const mockIsBrowserSupported = jest.fn().mockImplementation(() => true);
const mockIsListening = jest.fn();
const mockToggleListening = jest.fn();
samouss marked this conversation as resolved.
Show resolved Hide resolved
const mockDispose = jest.fn();

jest.mock('../../lib/voiceSearchHelper', () => {
return () => {
return {
getState: mockGetState,
isBrowserSupported: mockIsBrowserSupported,
isListening: mockIsListening,
toggleListening: mockToggleListening,
dispose: mockDispose,
};
};
});

Enzyme.configure({ adapter: new Adapter() });

describe('VoiceSearch', () => {
afterEach(() => {
mockGetState.mockImplementation(() => ({}));
mockIsBrowserSupported.mockImplementation(() => true);
eunjae-lee marked this conversation as resolved.
Show resolved Hide resolved
mockIsListening.mockClear();
mockToggleListening.mockClear();
});

describe('button', () => {
it('calls toggleListening when button is clicked', () => {
const wrapper = mount(<VoiceSearch />);
wrapper.find('button').simulate('click');
expect(mockToggleListening).toHaveBeenCalledTimes(1);
});
});

describe('Rendering', () => {
it('with default props', () => {
const wrapper = mount(<VoiceSearch />);
expect(wrapper).toMatchSnapshot();
});

it('with custom component for button with isListening: false', () => {
const customButtonText = ({ isListening }) =>
isListening ? 'Stop' : 'Start';

const wrapper = mount(
<VoiceSearch buttonTextComponent={customButtonText} />
);
expect(wrapper.find('button').text()).toBe('Start');
});

it('with custom component for button with isListening: true', () => {
const customButtonText = ({ isListening }) =>
isListening ? 'Stop' : 'Start';
mockIsListening.mockImplementation(() => true);

const wrapper = mount(
<VoiceSearch buttonTextComponent={customButtonText} />
);
expect(wrapper.find('button').text()).toBe('Stop');
});

it('renders a disabled button when the browser is not supported', () => {
mockIsBrowserSupported.mockImplementation(() => false);
const wrapper = mount(<VoiceSearch />);
expect(wrapper.find('button').prop('title')).toBe(
'Search by voice (not supported on this browser)'
);
expect(wrapper.find('button').prop('disabled')).toBe(true);
});

it('with custom template for status', () => {
const customStatus = ({
status,
errorCode,
isListening,
transcript,
isSpeechFinal,
isBrowserSupported,
}) => (
<div>
<p>status: {status}</p>
<p>errorCode: {errorCode}</p>
<p>isListening: {isListening ? 'true' : 'false'}</p>
<p>transcript: {transcript}</p>
<p>isSpeechFinal: {isSpeechFinal ? 'true' : 'false'}</p>
<p>isBrowserSupported: {isBrowserSupported ? 'true' : 'false'}</p>
</div>
);

mockIsListening.mockImplementation(() => true);
mockGetState.mockImplementation(() => ({
status: 'recognizing',
transcript: 'Hello',
isSpeechFinal: false,
errorCode: undefined,
}));

const wrapper = mount(<VoiceSearch statusComponent={customStatus} />);
expect(wrapper.find('.ais-VoiceSearch-status')).toMatchSnapshot();
});

it('calls voiceSearchHelper.dispose() on unmount', () => {
const wrapper = mount(<VoiceSearch />);
wrapper.find('button').simulate('click');
wrapper.unmount();
expect(mockDispose).toHaveBeenCalledTimes(1);
});
});
});
Loading