Skip to content

Commit ab1dc6d

Browse files
committed
[FIX] useSpeechRecognition hook
1 parent c4ec519 commit ab1dc6d

9 files changed

Lines changed: 92 additions & 36 deletions

File tree

apps/react-tools-demo/src/components/hooks/useSpeechSynthesis/UseSpeechSynthesis.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const UseSpeechSynthesis = () => {
1010
console.log(ev)
1111
},
1212
});
13-
const [form, setForm] = useState<{ text: string, voice: string, lang: string, rate: string, pitch: string }>({ text: "", voice: "", lang: "", rate: "1", pitch: "1" });
13+
const [form, setForm] = useState<{ text: string, volume: string, voice: string, lang: string, rate: string, pitch: string }>({ text: "", volume: "0.1", voice: "", lang: "", rate: "1", pitch: "1" });
1414

1515
if (!state.isSupported) {
1616
return <div style={{ display: "flex", justifyContent: "center" }}>
@@ -40,13 +40,17 @@ export const UseSpeechSynthesis = () => {
4040
<select value={form.lang} onChange={e => setForm(f => ({ ...f, lang: e.target.value }))} disabled={state.status === "speaking"}>
4141
<>
4242
{
43-
["it-IT", "en-US", "en-GB", "de-DE", "es-ES", "fr-FR"].map(el => (
43+
["en-US", "it-IT", "en-GB", "de-DE", "es-ES", "fr-FR"].map(el => (
4444
<option key={el} value={el}>{`${el}`}</option>
4545
))
4646
}
4747
</>
4848
</select>
4949
</div>
50+
<div>
51+
<label htmlFor="volume">Volume</label>
52+
<input type="range" id="volume" name="volume" min="0" max="1" step="0.1" value={form.volume} onChange={e => setForm(f => ({...f, volume: e.target.value}))} disabled={state.status === "speaking"}/>
53+
</div>
5054
<div>
5155
<label htmlFor="range">Rate</label>
5256
<input type="range" id="range" name="range" min="0.1" max="10" step="0.1" value={form.rate} onChange={e => setForm(f => ({...f, rate: e.target.value}))} disabled={state.status === "speaking"}/>
@@ -63,13 +67,31 @@ export const UseSpeechSynthesis = () => {
6367
text: form.text,
6468
lang: form.lang as LanguageBCP47Tags,
6569
voice: (state.voices || []).filter(v => v.name === form.voice)[0],
70+
volume: Number(form.volume),
6671
rate: Number(form.rate),
6772
pitch: Number(form.pitch),
6873
})
6974
}}
75+
disabled={state.status === "paused"}
7076
>
7177
Speak
7278
</button>
79+
<button
80+
type="button"
81+
onClick={() => {
82+
speak({
83+
text: form.text,
84+
lang: form.lang as LanguageBCP47Tags,
85+
voice: (state.voices || []).filter(v => v.name === form.voice)[0],
86+
volume: Number(form.volume),
87+
rate: Number(form.rate),
88+
pitch: Number(form.pitch),
89+
startImmediatly: true
90+
})
91+
}}
92+
>
93+
Speak immediatly
94+
</button>
7395
<button
7496
type="button"
7597
onClick={() => {

apps/react-tools-demo/src/markdown/useResponsive.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,17 @@ export const UseResponsive = () => {
2424
## API
2525

2626
```tsx
27-
useResponsive<T extends useResponsiveKeys>(config?: useResponsiveBreakpoints<T>): { [s in (keyof typeof defaultConfig)]: boolean } | { [s in useResponsiveKeys<T>]: boolean }
27+
useResponsive<T extends UseResponsiveKeys>(config?: UseResponsiveBreakpoints<T>): { [s in (keyof typeof defaultConfig)]: boolean } | { [s in UseResponsiveKeys<T>]: boolean }
2828
```
2929
3030
> ### Params
3131
>
32-
> - __config?__: _useResponsiveBreakpoints_
32+
> - __config?__: _UseResponsiveBreakpoints_
3333
custom breakpoint object.
3434
>
3535
3636
> ### Returns
3737
>
3838
> __breakpoint key__: returns the __size key__ of the __config__, parameter if passed otherwise __default config__, corresponding to the size of the window.
39-
> - _keyof useResponsiveBreakpoints_
39+
> - _keyof UseResponsiveBreakpoints_
4040
>

apps/react-tools-demo/src/markdown/useSpeechSynthesis.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const UseSpeechSynthesis = () => {
1010
console.log(ev)
1111
},
1212
});
13-
const [form, setForm] = useState<{ text: string, voice: string, lang: string, rate: string, pitch: string }>({ text: "", voice: "", lang: "", rate: "1", pitch: "1" });
13+
const [form, setForm] = useState<{ text: string, volume: string, voice: string, lang: string, rate: string, pitch: string }>({ text: "", volume: "0.1", voice: "", lang: "", rate: "1", pitch: "1" });
1414

1515
if (!state.isSupported) {
1616
return <div style={{ display: "flex", justifyContent: "center" }}>
@@ -40,13 +40,17 @@ export const UseSpeechSynthesis = () => {
4040
<select value={form.lang} onChange={e => setForm(f => ({ ...f, lang: e.target.value }))} disabled={state.status === "speaking"}>
4141
<>
4242
{
43-
["it-IT", "en-US", "en-GB", "de-DE", "es-ES", "fr-FR"].map(el => (
43+
["en-US", "it-IT", "en-GB", "de-DE", "es-ES", "fr-FR"].map(el => (
4444
<option key={el} value={el}>{`${el}`}</option>
4545
))
4646
}
4747
</>
4848
</select>
4949
</div>
50+
<div>
51+
<label htmlFor="volume">Volume</label>
52+
<input type="range" id="volume" name="volume" min="0" max="1" step="0.1" value={form.volume} onChange={e => setForm(f => ({...f, volume: e.target.value}))} disabled={state.status === "speaking"}/>
53+
</div>
5054
<div>
5155
<label htmlFor="range">Rate</label>
5256
<input type="range" id="range" name="range" min="0.1" max="10" step="0.1" value={form.rate} onChange={e => setForm(f => ({...f, rate: e.target.value}))} disabled={state.status === "speaking"}/>
@@ -63,13 +67,31 @@ export const UseSpeechSynthesis = () => {
6367
text: form.text,
6468
lang: form.lang as LanguageBCP47Tags,
6569
voice: (state.voices || []).filter(v => v.name === form.voice)[0],
70+
volume: Number(form.volume),
6671
rate: Number(form.rate),
6772
pitch: Number(form.pitch),
6873
})
6974
}}
75+
disabled={state.status === "paused"}
7076
>
7177
Speak
7278
</button>
79+
<button
80+
type="button"
81+
onClick={() => {
82+
speak({
83+
text: form.text,
84+
lang: form.lang as LanguageBCP47Tags,
85+
voice: (state.voices || []).filter(v => v.name === form.voice)[0],
86+
volume: Number(form.volume),
87+
rate: Number(form.rate),
88+
pitch: Number(form.pitch),
89+
startImmediatly: true
90+
})
91+
}}
92+
>
93+
Speak immediatly
94+
</button>
7395
<button
7496
type="button"
7597
onClick={() => {
@@ -123,7 +145,7 @@ function that will be executed when _end_ event is fired.
123145
> - __opts.onCancel?__: _SpeechSynthesisonCancel_
124146
function that will be executed when _cancel_ event is fired.
125147
> - __opts.lang?__: _LanguageBCP47Tags_
126-
[MDN Reference](https://developer.mozilla.org/docs/Web/API/SpeechSynthesisUtterance/lang.
148+
[MDN Reference](https://developer.mozilla.org/docs/Web/API/SpeechSynthesisUtterance/lang).
127149
> - __opts.pitch?__: _SpeechSynthesisUtterance["pitch"]_
128150
[MDN Reference](https://developer.mozilla.org/docs/Web/API/SpeechSynthesisUtterance/pitch).
129151
> - __opts.rate?__: _SpeechSynthesisUtterance["rate"]_
@@ -139,7 +161,8 @@ function that will be executed when _cancel_ event is fired.
139161
> __return__: _ReturnType<UseSpeechSynthesis>_
140162
> - __state__: object with these properties:
141163
> - _isSupported_: Returns a boolean value indicating SpeechSynthesis availability.
142-
> - _status_: Returns the current status of SpeechSynthesis.
164+
> - _status_: Returns the current status of SpeechSynthesis between: _ready_ _speaking_ _paused_ _error_ _end_ and _unavailable_.
165+
> - _hasPending_: Returns a boolean indicating the presence of texts to speech.
143166
> - _voices_: Returns the list of available voices.
144167
> - __speak__: Function to start speaking.
145168
> - __pause__: Function to keep in pause speaking.

packages/react-tools/src/hooks/useResponsive.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { useCallback, useMemo, useRef } from "react";
22
import { useSyncExternalStore } from ".";
3-
import { useResponsiveBreakpoints, useResponsiveKeys } from "../models/useResponsive.model";
3+
import { UseResponsiveBreakpoints, UseResponsiveKeys } from "../models";
44

55
const listeners = new Set<() => void>();
66

77
const handler = () => listeners.forEach(l => l());
88

9-
const defaultConfig: useResponsiveBreakpoints<"xs" | "sm" | "md" | "lg" | "xl"> = {
9+
const defaultConfig: UseResponsiveBreakpoints<"xs" | "sm" | "md" | "lg" | "xl"> = {
1010
xs: {
1111
value: 576,
1212
condition: "<"
@@ -29,7 +29,7 @@ const defaultConfig: useResponsiveBreakpoints<"xs" | "sm" | "md" | "lg" | "xl">
2929
}
3030
};
3131

32-
function calcResponsive<T extends useResponsiveKeys>(config?: useResponsiveBreakpoints<T>): { [s in keyof typeof defaultConfig]: boolean } | { [s in useResponsiveKeys<T>]: boolean } {
32+
function calcResponsive<T extends UseResponsiveKeys>(config?: UseResponsiveBreakpoints<T>): { [s in keyof typeof defaultConfig]: boolean } | { [s in UseResponsiveKeys<T>]: boolean } {
3333
const width = window.innerWidth;
3434
const conf = {};
3535
const target = config ?? defaultConfig;
@@ -41,7 +41,7 @@ function calcResponsive<T extends useResponsiveKeys>(config?: useResponsiveBreak
4141
Reflect.set(conf, key, eval(`${width}${condition}${value}`) as boolean);
4242
}
4343
}
44-
return conf as typeof config extends undefined ? { [k in keyof typeof defaultConfig]: boolean } : { [k in useResponsiveKeys<T>]: boolean };
44+
return conf as typeof config extends undefined ? { [k in keyof typeof defaultConfig]: boolean } : { [k in UseResponsiveKeys<T>]: boolean };
4545
}
4646

4747
/**
@@ -52,12 +52,12 @@ function calcResponsive<T extends useResponsiveKeys>(config?: useResponsiveBreak
5252
* - md: { value: 768, condition: ">=" }
5353
* - lg: { value: 992, condition: ">=" }
5454
* - xl: { value: 1200, condition: ">=" }
55-
* @param {useResponsiveBreakpoints} [config] - custom breakpoint object.
56-
* @returns {keyof useResponsiveBreakpoints} breakpoint key - returns the __size key__ of the __config__, parameter if passed otherwise __default config__, corresponding to the size of the window.
55+
* @param {UseResponsiveBreakpoints} [config] - custom breakpoint object.
56+
* @returns {keyof UseResponsiveBreakpoints} breakpoint key - returns the __size key__ of the __config__, parameter if passed otherwise __default config__, corresponding to the size of the window.
5757
*/
5858
function useResponsive(config?: undefined): { [s in (keyof typeof defaultConfig)]: boolean };
59-
function useResponsive<T extends useResponsiveKeys>(config?: useResponsiveBreakpoints<T>): { [s in useResponsiveKeys<T>]: boolean };
60-
function useResponsive<T extends useResponsiveKeys>(config?: useResponsiveBreakpoints<T>): { [s in (keyof typeof defaultConfig)]: boolean } | { [s in useResponsiveKeys<T>]: boolean } {
59+
function useResponsive<T extends UseResponsiveKeys>(config?: UseResponsiveBreakpoints<T>): { [s in UseResponsiveKeys<T>]: boolean };
60+
function useResponsive<T extends UseResponsiveKeys>(config?: UseResponsiveBreakpoints<T>): { [s in (keyof typeof defaultConfig)]: boolean } | { [s in UseResponsiveKeys<T>]: boolean } {
6161
const configCache = useRef(() => {
6262
if (config === undefined) {
6363
return calcResponsive(defaultConfig);

packages/react-tools/src/hooks/useSpeechSynthesis.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@ import { UseSpeechSynthesis, SpeechSynthesisSpeakParam, UseSpeechSynthesisProps
1414
* @param {SpeechSynthesisUtterance["onerror"]} [opts.onError] - function that will be executed when _error_ event is fired.
1515
* @param {SpeechSynthesisUtterance["onend"]} [opts.onEnd] - function that will be executed when _end_ event is fired.
1616
* @param {SpeechSynthesisonCancel} [opts.onCancel] - function that will be executed when _cancel_ event is fired.
17-
* @param {LanguageBCP47Tags} [opts.lang] - [MDN Reference](https://developer.mozilla.org/docs/Web/API/SpeechSynthesisUtterance/lang.
17+
* @param {LanguageBCP47Tags} [opts.lang] - [MDN Reference](https://developer.mozilla.org/docs/Web/API/SpeechSynthesisUtterance/lang).
1818
* @param {SpeechSynthesisUtterance["pitch"]} [opts.pitch] - [MDN Reference](https://developer.mozilla.org/docs/Web/API/SpeechSynthesisUtterance/pitch).
1919
* @param {SpeechSynthesisUtterance["rate"]} [opts.rate] - [MDN Reference](https://developer.mozilla.org/docs/Web/API/SpeechSynthesisUtterance/rate).
2020
* @param {SpeechSynthesisUtterance["voice"]} [opts.voice] - [MDN Reference](https://developer.mozilla.org/docs/Web/API/SpeechSynthesisUtterance/voice).
2121
* @param {SpeechSynthesisUtterance["volume"]} [opts.volume] - [MDN Reference](https://developer.mozilla.org/docs/Web/API/SpeechSynthesisUtterance/volume).
2222
* @returns {ReturnType<UseSpeechSynthesis>} return - Object with these properties:
2323
* - __state__: object with these properties:
2424
* - _isSupported_: Returns a boolean value indicating SpeechSynthesis availability.
25-
* - _status_: Returns the current status of SpeechSynthesis.
25+
* - _status_: Returns the current status of SpeechSynthesis between: _ready_ _speaking_ _paused_ _error_ _end_ and _unavailable_.
26+
* - _hasPending_: Returns a boolean indicating the presence of texts to speech.
2627
* - _voices_: Returns the list of available voices.
2728
* - __speak__: Function to start speaking.
2829
* - __pause__: Function to keep in pause speaking.
@@ -35,32 +36,36 @@ export const useSpeechSynthesis = (opts?: UseSpeechSynthesisProps): ReturnType<U
3536
const synth = useRef(window && (window as any).speechSynthesis as SpeechSynthesis);
3637

3738
const notifRef = useRef<() => void>();
38-
const error = useRef(false);
39+
const status = useRef<ReturnType<UseSpeechSynthesis>["state"]["status"]>(isSupported ? "ready" : "unavailable");
3940

4041
const onStart = useRef<(handler?: UseSpeechSynthesisProps["onStart"]) => SpeechSynthesisUtterance["onstart"]>((handler) => {
4142
return (evt: SpeechSynthesisEvent) => {
42-
error.current = false;
43+
status.current = "speaking";
4344
notifRef.current && notifRef.current();
4445
handler && handler.call(evt.utterance, evt);
4546
}
4647
});
4748

4849
const onPause = useRef<(handler?: UseSpeechSynthesisProps["onPause"]) => SpeechSynthesisUtterance["onpause"]>((handler) => {
4950
return (evt: SpeechSynthesisEvent) => {
51+
status.current = "paused";
5052
notifRef.current && notifRef.current();
5153
handler && handler.call(evt.utterance, evt);
5254
}
5355
});
5456

5557
const onResume = useRef<(handler?: UseSpeechSynthesisProps["onResume"]) => SpeechSynthesisUtterance["onresume"]>((handler) => {
5658
return (evt: SpeechSynthesisEvent) => {
59+
status.current = "speaking";
5760
notifRef.current && notifRef.current();
5861
handler && handler.call(evt.utterance, evt);
5962
}
6063
});
6164

6265
const onEnd = useRef<(handler?: UseSpeechSynthesisProps["onEnd"]) => SpeechSynthesisUtterance["onend"]>((handler) => {
6366
return (evt: SpeechSynthesisEvent) => {
67+
status.current === "paused" && synth.current.cancel();
68+
status.current = "end";
6469
notifRef.current && notifRef.current();
6570
handler && handler.call(evt.utterance, evt);
6671
}
@@ -69,10 +74,11 @@ export const useSpeechSynthesis = (opts?: UseSpeechSynthesisProps): ReturnType<U
6974
const onError = useRef<(handler?: UseSpeechSynthesisProps["onError"]) => SpeechSynthesisUtterance["onerror"]>((handler) => {
7075
return (evt: SpeechSynthesisErrorEvent) => {
7176
if (opts?.onCancel && ["canceled", "interrupted"].includes(evt.error)) {
77+
status.current = "ready";
7278
const { charIndex, charLength, elapsedTime, utterance} = evt;
7379
opts.onCancel.call(evt.utterance, { charIndex, charLength, elapsedTime, utterance, name: "oncancel" })
7480
} else {
75-
error.current = true;
81+
status.current = "error";
7682
handler && handler.call(evt.utterance, evt);
7783
}
7884
notifRef.current && notifRef.current();
@@ -105,18 +111,18 @@ export const useSpeechSynthesis = (opts?: UseSpeechSynthesisProps): ReturnType<U
105111
!!onBoundaryHandler && (utterance.onboundary = onBoundaryHandler);
106112
!!onMarkHandler && (utterance.onmark = onMarkHandler);
107113

108-
if (synth.current.paused || rest.startImmediatly) {
114+
if (status.current === "paused" || rest.startImmediatly) {
109115
synth.current.cancel();
110116
}
111117

112-
!synth.current.speaking && opts?.onSpeak && opts?.onSpeak();
118+
["ready", "end", "error"].includes(status.current) && opts?.onSpeak && opts?.onSpeak();
113119

114120
synth.current.speak(utterance);
115121
});
116122

117-
const pause = useRef(() => isSupported.current && synth.current.speaking && synth.current.pause());
123+
const pause = useRef(() => isSupported.current && status.current === "speaking" && synth.current.pause());
118124

119-
const resume = useRef(() => isSupported.current && synth.current.paused && synth.current.resume());
125+
const resume = useRef(() => isSupported.current && status.current === "paused" && synth.current.resume());
120126

121127
const cancel = useRef(() => isSupported.current && synth.current.cancel());
122128

@@ -133,19 +139,22 @@ export const useSpeechSynthesis = (opts?: UseSpeechSynthesisProps): ReturnType<U
133139
useMemo(() => {
134140
let cached:ReturnType<UseSpeechSynthesis>["state"] = {
135141
isSupported: isSupported.current,
136-
status: isSupported.current ? synth.current.speaking ? "speaking" : synth.current.paused ? "paused" : error.current ? "error" : synth.current.pending ? "pending" : "end" : "unavailable",
142+
status: status.current,
143+
hasPending: isSupported.current ? synth.current.pending : false,
137144
voices: isSupported.current ? synth.current.getVoices() : null,
138145
}
139146

140147
return () => {
141148
const current = {
142-
status: isSupported.current ? synth.current.speaking ? "speaking" : synth.current.paused ? "paused" : error.current ? "error" : synth.current.pending ? "pending" : "end" : "unavailable",
149+
status: status.current,
150+
hasPending: isSupported.current ? synth.current.pending : false,
143151
voices: isSupported.current ? synth.current.getVoices() : null,
144152
}
145-
if (current.status !== cached.status || current.voices?.length !== cached.voices?.length) {
153+
if (current.status !== cached.status || current.hasPending !== cached.hasPending || current.voices?.length !== cached.voices?.length) {
146154
cached = {
147155
isSupported: isSupported.current,
148156
status: current.status as typeof cached["status"],
157+
hasPending: current.hasPending,
149158
voices: current.voices
150159
}
151160
}

packages/react-tools/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ export type {
66
UseScriptStatus,
77
UseScript,
88
TextSelection,
9-
useResponsiveKeys,
10-
useResponsiveBreakpoints,
9+
UseResponsiveKeys,
10+
UseResponsiveBreakpoints,
1111
ConnectionState,
1212
BatteryStatus,
1313
GeoLocationObject,

packages/react-tools/src/models/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export type { DependencyListTyped, CompareFn, ArrayMinLength1, LanguageBCP47Tags } from "./common.model";
22
export type { UseScriptProps, UseScript, UseScriptStatus } from './useScript.model';
33
export type { TextSelection } from './useTextSelection.model';
4-
export type { useResponsiveKeys, useResponsiveBreakpoints } from './useResponsive.model';
4+
export type { UseResponsiveKeys, UseResponsiveBreakpoints } from './useResponsive.model';
55
export type { ConnectionState } from './useNetwork.model';
66
export type { BatteryStatus } from './useBattery.model';
77
export type { GeoLocationObject } from './useGeolocation.model';
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
type useResponsiveKeysType = "xxxs" | "xxs" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl" | "xxxl";
1+
type UseResponsiveKeysType = "xxxs" | "xxs" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl" | "xxxl";
22

3-
export type useResponsiveKeys<T extends useResponsiveKeysType = useResponsiveKeysType> = T extends useResponsiveKeysType ? Extract<useResponsiveKeysType, T> : never;
3+
export type UseResponsiveKeys<T extends UseResponsiveKeysType = UseResponsiveKeysType> = T extends UseResponsiveKeysType ? Extract<UseResponsiveKeysType, T> : never;
44

5-
export type useResponsiveBreakpoints<T extends useResponsiveKeys = useResponsiveKeys> = {
5+
export type UseResponsiveBreakpoints<T extends UseResponsiveKeys = UseResponsiveKeys> = {
66
[k in T]: number | { value: number, condition: "<" | "<=" | ">" | ">=" }
77
}

packages/react-tools/src/models/useSpeechSynthesis.model.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ export interface UseSpeechSynthesis {
8181
/**Returns a boolean value indicating SpeechSynthesis availability.*/
8282
isSupported: boolean;
8383
/**Returns the current status of SpeechSynthesis.*/
84-
status: "speaking" | "paused" | "pending" | "error" | "end" | "unavailable";
84+
status: "ready" | "speaking" | "paused" | "error" | "end" | "unavailable";
85+
/**Returns a boolean indicating the presence of texts to speech.*/
86+
hasPending: boolean;
8587
/**Returns the list of available voices.*/
8688
voices: SpeechSynthesisVoice[] | null;
8789
},

0 commit comments

Comments
 (0)