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

Commit

Permalink
feat(voiceSearch): add voice search component (#668)
Browse files Browse the repository at this point in the history
* chore(voiceSearch): scope styles to non-ais elements

* feat(voiceSearch): WIP adding voiceSearch component

* test(voiceSearch): add stories

* test(voiceSearch): add tests for the component

* chore(voiceSearch): formatted

* chore(voiceSearch): fix lint errors

* doc: revert a mistake

* chore(voiceSearch): remove unnecessary computed properties

* feat(voiceSearch): provide all data to the default root slot

* feat(voiceSearch): change slot properties to kebab case

* feat(voiceSearch): add a computed property for root slot props

* chore(voiceSearch): replace temp files with the ones from latest IS.js

* fix(voiceSearch): fix an import path

* chore(voiceSearch): update bundlesize

* Apply suggestions from code review

Co-Authored-By: Haroen Viaene <fingebimus@me.com>

* chore: update IS.js@3.5.1

* chore: update bundlesize
  • Loading branch information
Eunjae Lee authored May 21, 2019
1 parent 2a21b52 commit 84efffc
Show file tree
Hide file tree
Showing 8 changed files with 486 additions and 12 deletions.
6 changes: 3 additions & 3 deletions .storybook/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,21 @@ body {
min-height: unset !important;
}

input {
input:not([class^='ais-']) {
border-style: solid !important;
border-width: 1px;
border-color: #c4c8d8;
border-radius: 5px !important;
}

button {
button:not([class^='ais-']) {
border-style: solid !important;
border-width: 1px;
padding: 0.2em;
border-color: black;
border-radius: 5px !important;
}

button:disabled {
button:not([class^='ais-']):disabled {
border-color: #c4c8d8;
}
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
},
"dependencies": {
"algoliasearch-helper": "^2.26.1",
"instantsearch.js": "^3.4.0"
"instantsearch.js": "^3.5.1"
},
"peerDependencies": {
"algoliasearch": "^3.30.0",
Expand Down Expand Up @@ -101,15 +101,15 @@
"bundlesize": [
{
"path": "./dist/vue-instantsearch.js",
"maxSize": "67.25 kB"
"maxSize": "68.60 kB"
},
{
"path": "./dist/vue-instantsearch.esm.js",
"maxSize": "14 kB"
"maxSize": "14.25 kB"
},
{
"path": "./dist/vue-instantsearch.common.js",
"maxSize": "14 kB"
"maxSize": "14.50 kB"
}
],
"jest": {
Expand Down
160 changes: 160 additions & 0 deletions src/components/VoiceSearch.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<template>
<div
v-if="state"
:class="suit()"
>
<slot v-bind="rootSlotProps">
<button
type="button"
:class="suit('button')"
:title="state.isBrowserSupported ? buttonTitle : disabledButtonTitle"
:disabled="!state.isBrowserSupported"
@click="handleClick"
>
<slot
name="buttonText"
v-bind="innerSlotProps"
>
<svg
v-bind="buttonSvgAttrs"
v-if="errorNotAllowed"
>
<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"
/>
</svg>
<svg
v-bind="buttonSvgAttrs"
v-else
>
<path
d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"
:fill="state.isListening ? 'currentColor' : 'none'"
/>
<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"
/>
</svg>
</slot>
</button>
<div :class="suit('status')">
<slot
name="status"
v-bind="innerSlotProps"
>
<p>{{ state.voiceListeningState.transcript }}</p>
</slot>
</div>
</slot>
</div>
</template>

<script>
import { connectVoiceSearch } from 'instantsearch.js/es/connectors';
import { createSuitMixin } from '../mixins/suit';
import { createWidgetMixin } from '../mixins/widget';
export default {
name: 'AisVoiceSearch',
mixins: [
createWidgetMixin({ connector: connectVoiceSearch }),
createSuitMixin({ name: 'VoiceSearch' }),
],
props: {
searchAsYouSpeak: {
type: Boolean,
required: false,
default: false,
},
buttonTitle: {
type: String,
required: false,
default: 'Search by voice',
},
disabledButtonTitle: {
type: String,
required: false,
default: 'Search by voice (not supported on this browser)',
},
},
data() {
return {
buttonSvgAttrs: {
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',
},
};
},
computed: {
widgetParams() {
return {
searchAsYouSpeak: this.searchAsYouSpeak,
};
},
errorNotAllowed() {
return (
this.state.voiceListeningState.status === 'error' &&
this.state.voiceListeningState.errorCode === 'not-allowed'
);
},
rootSlotProps() {
return {
isBrowserSupported: this.state.isBrowserSupported,
isListening: this.state.isListening,
toggleListening: this.state.toggleListening,
voiceListeningState: this.state.voiceListeningState,
};
},
innerSlotProps() {
return {
status: this.state.voiceListeningState.status,
errorCode: this.state.voiceListeningState.errorCode,
isListening: this.state.isListening,
transcript: this.state.voiceListeningState.transcript,
isSpeechFinal: this.state.voiceListeningState.isSpeechFinal,
isBrowserSupported: this.state.isBrowserSupported,
};
},
},
methods: {
handleClick(event) {
event.currentTarget.blur();
this.state.toggleListening();
},
},
};
</script>
103 changes: 103 additions & 0 deletions src/components/__tests__/VoiceSearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import VoiceSearch from '../VoiceSearch.vue';
import { mount } from '@vue/test-utils';
import { __setState } from '../../mixins/widget';
jest.mock('../../mixins/widget');

const defaultState = {
voiceListeningState: {
status: 'initial',
transcript: undefined,
isSpeechFinal: undefined,
errorCode: undefined,
},
isBrowserSupported: true,
isListening: false,
toggleListening: jest.fn(),
};

const buttonTextScopedSlot = `
<span slot-scope="{ isListening }">
{{isListening ? "Stop": "Start"}}
</span>
`;

describe('button', () => {
it('calls toggleListening when the button is clicked', () => {
__setState(defaultState);
const wrapper = mount(VoiceSearch);
wrapper.find('button').trigger('click');
expect(wrapper.vm.state.toggleListening).toHaveBeenCalledTimes(1);
});
});

describe('Rendering', () => {
it('renders default template correctly', () => {
__setState(defaultState);
const wrapper = mount(VoiceSearch);
expect(wrapper.html()).toMatchSnapshot();
});

it('display the button as disabled on unsupported browser', () => {
__setState({
...defaultState,
isBrowserSupported: false,
});
const wrapper = mount(VoiceSearch);
expect(wrapper.find('button').attributes().disabled).toBe('disabled');
});

it('with custom template for buttonText (1)', () => {
__setState({
...defaultState,
isListening: true,
});
const wrapper = mount(VoiceSearch, {
scopedSlots: {
buttonText: buttonTextScopedSlot,
},
});
expect(wrapper.find('button').text()).toBe('Stop');
});

it('with custom template for buttonText (2)', () => {
__setState({
...defaultState,
isListening: false,
});
const wrapper = mount(VoiceSearch, {
scopedSlots: {
buttonText: buttonTextScopedSlot,
},
});
expect(wrapper.find('button').text()).toBe('Start');
});

it('with custom template for status', () => {
__setState({
voiceListeningState: {
status: 'recognizing',
transcript: 'Hello',
isSpeechFinal: false,
errorCode: undefined,
},
isBrowserSupported: true,
isListening: true,
toggleListening: jest.fn(),
});
const wrapper = mount(VoiceSearch, {
scopedSlots: {
status: `
<div slot-scope="{ status, errorCode, isListening, transcript, isSpeechFinal, isBrowserSupported }">
<p>status: {{status}}</p>
<p>errorCode: {{errorCode}}</p>
<p>isListening: {{isListening}}</p>
<p>transcript: {{transcript}}</p>
<p>isSpeechFinal: {{isSpeechFinal}}</p>
<p>isBrowserSupported: {{isBrowserSupported}}</p>
</div>
`,
},
});
expect(wrapper.find('.ais-VoiceSearch-status').html()).toMatchSnapshot();
});
});
73 changes: 73 additions & 0 deletions src/components/__tests__/__snapshots__/VoiceSearch.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Rendering renders default template correctly 1`] = `
<div class="ais-VoiceSearch">
<button type="button"
title="Search by voice"
class="ais-VoiceSearch-button"
>
<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"
>
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"
fill="none"
>
</path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2">
</path>
<line x1="12"
y1="19"
x2="12"
y2="23"
>
</line>
<line x1="8"
y1="23"
x2="16"
y2="23"
>
</line>
</svg>
</button>
<div class="ais-VoiceSearch-status">
<p>
</p>
</div>
</div>
`;

exports[`Rendering with custom template for status 1`] = `
<div class="ais-VoiceSearch-status">
<div>
<p>
status: recognizing
</p>
<p>
errorCode:
</p>
<p>
isListening: true
</p>
<p>
transcript: Hello
</p>
<p>
isSpeechFinal: false
</p>
<p>
isBrowserSupported: true
</p>
</div>
</div>
`;
1 change: 1 addition & 0 deletions src/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ export { default as AisStats } from './components/Stats.vue';
export {
default as AisToggleRefinement,
} from './components/ToggleRefinement.vue';
export { default as AisVoiceSearch } from './components/VoiceSearch.vue';
Loading

0 comments on commit 84efffc

Please sign in to comment.