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

Commit 84efffc

Browse files
author
Eunjae Lee
authored
feat(voiceSearch): add voice search component (#668)
* 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
1 parent 2a21b52 commit 84efffc

File tree

8 files changed

+486
-12
lines changed

8 files changed

+486
-12
lines changed

.storybook/styles.css

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,21 +108,21 @@ body {
108108
min-height: unset !important;
109109
}
110110

111-
input {
111+
input:not([class^='ais-']) {
112112
border-style: solid !important;
113113
border-width: 1px;
114114
border-color: #c4c8d8;
115115
border-radius: 5px !important;
116116
}
117117

118-
button {
118+
button:not([class^='ais-']) {
119119
border-style: solid !important;
120120
border-width: 1px;
121121
padding: 0.2em;
122122
border-color: black;
123123
border-radius: 5px !important;
124124
}
125125

126-
button:disabled {
126+
button:not([class^='ais-']):disabled {
127127
border-color: #c4c8d8;
128128
}

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
},
4747
"dependencies": {
4848
"algoliasearch-helper": "^2.26.1",
49-
"instantsearch.js": "^3.4.0"
49+
"instantsearch.js": "^3.5.1"
5050
},
5151
"peerDependencies": {
5252
"algoliasearch": "^3.30.0",
@@ -101,15 +101,15 @@
101101
"bundlesize": [
102102
{
103103
"path": "./dist/vue-instantsearch.js",
104-
"maxSize": "67.25 kB"
104+
"maxSize": "68.60 kB"
105105
},
106106
{
107107
"path": "./dist/vue-instantsearch.esm.js",
108-
"maxSize": "14 kB"
108+
"maxSize": "14.25 kB"
109109
},
110110
{
111111
"path": "./dist/vue-instantsearch.common.js",
112-
"maxSize": "14 kB"
112+
"maxSize": "14.50 kB"
113113
}
114114
],
115115
"jest": {

src/components/VoiceSearch.vue

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<template>
2+
<div
3+
v-if="state"
4+
:class="suit()"
5+
>
6+
<slot v-bind="rootSlotProps">
7+
<button
8+
type="button"
9+
:class="suit('button')"
10+
:title="state.isBrowserSupported ? buttonTitle : disabledButtonTitle"
11+
:disabled="!state.isBrowserSupported"
12+
@click="handleClick"
13+
>
14+
<slot
15+
name="buttonText"
16+
v-bind="innerSlotProps"
17+
>
18+
<svg
19+
v-bind="buttonSvgAttrs"
20+
v-if="errorNotAllowed"
21+
>
22+
<line
23+
x1="1"
24+
y1="1"
25+
x2="23"
26+
y2="23"
27+
/>
28+
<path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6" />
29+
<path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23" />
30+
<line
31+
x1="12"
32+
y1="19"
33+
x2="12"
34+
y2="23"
35+
/>
36+
<line
37+
x1="8"
38+
y1="23"
39+
x2="16"
40+
y2="23"
41+
/>
42+
</svg>
43+
<svg
44+
v-bind="buttonSvgAttrs"
45+
v-else
46+
>
47+
<path
48+
d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"
49+
:fill="state.isListening ? 'currentColor' : 'none'"
50+
/>
51+
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
52+
<line
53+
x1="12"
54+
y1="19"
55+
x2="12"
56+
y2="23"
57+
/>
58+
<line
59+
x1="8"
60+
y1="23"
61+
x2="16"
62+
y2="23"
63+
/>
64+
</svg>
65+
</slot>
66+
</button>
67+
<div :class="suit('status')">
68+
<slot
69+
name="status"
70+
v-bind="innerSlotProps"
71+
>
72+
<p>{{ state.voiceListeningState.transcript }}</p>
73+
</slot>
74+
</div>
75+
</slot>
76+
</div>
77+
</template>
78+
79+
<script>
80+
import { connectVoiceSearch } from 'instantsearch.js/es/connectors';
81+
import { createSuitMixin } from '../mixins/suit';
82+
import { createWidgetMixin } from '../mixins/widget';
83+
84+
export default {
85+
name: 'AisVoiceSearch',
86+
mixins: [
87+
createWidgetMixin({ connector: connectVoiceSearch }),
88+
createSuitMixin({ name: 'VoiceSearch' }),
89+
],
90+
props: {
91+
searchAsYouSpeak: {
92+
type: Boolean,
93+
required: false,
94+
default: false,
95+
},
96+
buttonTitle: {
97+
type: String,
98+
required: false,
99+
default: 'Search by voice',
100+
},
101+
disabledButtonTitle: {
102+
type: String,
103+
required: false,
104+
default: 'Search by voice (not supported on this browser)',
105+
},
106+
},
107+
data() {
108+
return {
109+
buttonSvgAttrs: {
110+
xmlns: 'http://www.w3.org/2000/svg',
111+
width: '16',
112+
height: '16',
113+
viewBox: '0 0 24 24',
114+
fill: 'none',
115+
stroke: 'currentColor',
116+
strokeWidth: '2',
117+
strokeLinecap: 'round',
118+
strokeLinejoin: 'round',
119+
},
120+
};
121+
},
122+
computed: {
123+
widgetParams() {
124+
return {
125+
searchAsYouSpeak: this.searchAsYouSpeak,
126+
};
127+
},
128+
errorNotAllowed() {
129+
return (
130+
this.state.voiceListeningState.status === 'error' &&
131+
this.state.voiceListeningState.errorCode === 'not-allowed'
132+
);
133+
},
134+
rootSlotProps() {
135+
return {
136+
isBrowserSupported: this.state.isBrowserSupported,
137+
isListening: this.state.isListening,
138+
toggleListening: this.state.toggleListening,
139+
voiceListeningState: this.state.voiceListeningState,
140+
};
141+
},
142+
innerSlotProps() {
143+
return {
144+
status: this.state.voiceListeningState.status,
145+
errorCode: this.state.voiceListeningState.errorCode,
146+
isListening: this.state.isListening,
147+
transcript: this.state.voiceListeningState.transcript,
148+
isSpeechFinal: this.state.voiceListeningState.isSpeechFinal,
149+
isBrowserSupported: this.state.isBrowserSupported,
150+
};
151+
},
152+
},
153+
methods: {
154+
handleClick(event) {
155+
event.currentTarget.blur();
156+
this.state.toggleListening();
157+
},
158+
},
159+
};
160+
</script>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import VoiceSearch from '../VoiceSearch.vue';
2+
import { mount } from '@vue/test-utils';
3+
import { __setState } from '../../mixins/widget';
4+
jest.mock('../../mixins/widget');
5+
6+
const defaultState = {
7+
voiceListeningState: {
8+
status: 'initial',
9+
transcript: undefined,
10+
isSpeechFinal: undefined,
11+
errorCode: undefined,
12+
},
13+
isBrowserSupported: true,
14+
isListening: false,
15+
toggleListening: jest.fn(),
16+
};
17+
18+
const buttonTextScopedSlot = `
19+
<span slot-scope="{ isListening }">
20+
{{isListening ? "Stop": "Start"}}
21+
</span>
22+
`;
23+
24+
describe('button', () => {
25+
it('calls toggleListening when the button is clicked', () => {
26+
__setState(defaultState);
27+
const wrapper = mount(VoiceSearch);
28+
wrapper.find('button').trigger('click');
29+
expect(wrapper.vm.state.toggleListening).toHaveBeenCalledTimes(1);
30+
});
31+
});
32+
33+
describe('Rendering', () => {
34+
it('renders default template correctly', () => {
35+
__setState(defaultState);
36+
const wrapper = mount(VoiceSearch);
37+
expect(wrapper.html()).toMatchSnapshot();
38+
});
39+
40+
it('display the button as disabled on unsupported browser', () => {
41+
__setState({
42+
...defaultState,
43+
isBrowserSupported: false,
44+
});
45+
const wrapper = mount(VoiceSearch);
46+
expect(wrapper.find('button').attributes().disabled).toBe('disabled');
47+
});
48+
49+
it('with custom template for buttonText (1)', () => {
50+
__setState({
51+
...defaultState,
52+
isListening: true,
53+
});
54+
const wrapper = mount(VoiceSearch, {
55+
scopedSlots: {
56+
buttonText: buttonTextScopedSlot,
57+
},
58+
});
59+
expect(wrapper.find('button').text()).toBe('Stop');
60+
});
61+
62+
it('with custom template for buttonText (2)', () => {
63+
__setState({
64+
...defaultState,
65+
isListening: false,
66+
});
67+
const wrapper = mount(VoiceSearch, {
68+
scopedSlots: {
69+
buttonText: buttonTextScopedSlot,
70+
},
71+
});
72+
expect(wrapper.find('button').text()).toBe('Start');
73+
});
74+
75+
it('with custom template for status', () => {
76+
__setState({
77+
voiceListeningState: {
78+
status: 'recognizing',
79+
transcript: 'Hello',
80+
isSpeechFinal: false,
81+
errorCode: undefined,
82+
},
83+
isBrowserSupported: true,
84+
isListening: true,
85+
toggleListening: jest.fn(),
86+
});
87+
const wrapper = mount(VoiceSearch, {
88+
scopedSlots: {
89+
status: `
90+
<div slot-scope="{ status, errorCode, isListening, transcript, isSpeechFinal, isBrowserSupported }">
91+
<p>status: {{status}}</p>
92+
<p>errorCode: {{errorCode}}</p>
93+
<p>isListening: {{isListening}}</p>
94+
<p>transcript: {{transcript}}</p>
95+
<p>isSpeechFinal: {{isSpeechFinal}}</p>
96+
<p>isBrowserSupported: {{isBrowserSupported}}</p>
97+
</div>
98+
`,
99+
},
100+
});
101+
expect(wrapper.find('.ais-VoiceSearch-status').html()).toMatchSnapshot();
102+
});
103+
});
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Rendering renders default template correctly 1`] = `
4+
5+
<div class="ais-VoiceSearch">
6+
<button type="button"
7+
title="Search by voice"
8+
class="ais-VoiceSearch-button"
9+
>
10+
<svg xmlns="http://www.w3.org/2000/svg"
11+
width="16"
12+
height="16"
13+
viewbox="0 0 24 24"
14+
fill="none"
15+
stroke="currentColor"
16+
strokewidth="2"
17+
strokelinecap="round"
18+
strokelinejoin="round"
19+
>
20+
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"
21+
fill="none"
22+
>
23+
</path>
24+
<path d="M19 10v2a7 7 0 0 1-14 0v-2">
25+
</path>
26+
<line x1="12"
27+
y1="19"
28+
x2="12"
29+
y2="23"
30+
>
31+
</line>
32+
<line x1="8"
33+
y1="23"
34+
x2="16"
35+
y2="23"
36+
>
37+
</line>
38+
</svg>
39+
</button>
40+
<div class="ais-VoiceSearch-status">
41+
<p>
42+
</p>
43+
</div>
44+
</div>
45+
46+
`;
47+
48+
exports[`Rendering with custom template for status 1`] = `
49+
50+
<div class="ais-VoiceSearch-status">
51+
<div>
52+
<p>
53+
status: recognizing
54+
</p>
55+
<p>
56+
errorCode:
57+
</p>
58+
<p>
59+
isListening: true
60+
</p>
61+
<p>
62+
transcript: Hello
63+
</p>
64+
<p>
65+
isSpeechFinal: false
66+
</p>
67+
<p>
68+
isBrowserSupported: true
69+
</p>
70+
</div>
71+
</div>
72+
73+
`;

src/widgets.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ export { default as AisStats } from './components/Stats.vue';
3737
export {
3838
default as AisToggleRefinement,
3939
} from './components/ToggleRefinement.vue';
40+
export { default as AisVoiceSearch } from './components/VoiceSearch.vue';

0 commit comments

Comments
 (0)