Skip to content

Commit 70635db

Browse files
r-farkhutdinovRuslan Farkhutdinov
andauthored
SpeechToText: Add onEnd event & stop recognition on disabled option change (#31171)
Co-authored-by: Ruslan Farkhutdinov <ruslan.farkhutdinov@devexpress.com>
1 parent 8e8298f commit 70635db

File tree

9 files changed

+89
-12
lines changed

9 files changed

+89
-12
lines changed

packages/devextreme-angular/src/ui/speech-to-text/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
} from '@angular/core';
1818

1919

20-
import { CustomSpeechRecognizer, ContentReadyEvent, DisposingEvent, ErrorEvent, InitializedEvent, OptionChangedEvent, ResultEvent, StartClickEvent, StopClickEvent, SpeechRecognitionConfig } from 'devextreme/ui/speech_to_text';
20+
import { CustomSpeechRecognizer, ContentReadyEvent, DisposingEvent, EndEvent, ErrorEvent, InitializedEvent, OptionChangedEvent, ResultEvent, StartClickEvent, StopClickEvent, SpeechRecognitionConfig } from 'devextreme/ui/speech_to_text';
2121
import { ButtonStyle, ButtonType } from 'devextreme/common';
2222

2323
import DxSpeechToText from 'devextreme/ui/speech_to_text';
@@ -275,6 +275,14 @@ export class DxSpeechToTextComponent extends DxComponent implements OnDestroy {
275275
* [descr:undefined]
276276
277277
278+
*/
279+
@Output() onEnd: EventEmitter<EndEvent>;
280+
281+
/**
282+
283+
* [descr:undefined]
284+
285+
278286
*/
279287
@Output() onError: EventEmitter<ErrorEvent>;
280288

@@ -476,6 +484,7 @@ export class DxSpeechToTextComponent extends DxComponent implements OnDestroy {
476484
this._createEventEmitters([
477485
{ subscribe: 'contentReady', emit: 'onContentReady' },
478486
{ subscribe: 'disposing', emit: 'onDisposing' },
487+
{ subscribe: 'end', emit: 'onEnd' },
479488
{ subscribe: 'error', emit: 'onError' },
480489
{ subscribe: 'initialized', emit: 'onInitialized' },
481490
{ subscribe: 'optionChanged', emit: 'onOptionChanged' },

packages/devextreme-react/src/speech-to-text.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import dxSpeechToText, {
88
import { Component as BaseComponent, IHtmlOptions, ComponentRef, NestedComponentMeta } from "./core/component";
99
import NestedOption from "./core/nested-option";
1010

11-
import type { ContentReadyEvent, DisposingEvent, ErrorEvent, InitializedEvent, ResultEvent, StartClickEvent, StopClickEvent } from "devextreme/ui/speech_to_text";
11+
import type { ContentReadyEvent, DisposingEvent, EndEvent, ErrorEvent, InitializedEvent, ResultEvent, StartClickEvent, StopClickEvent } from "devextreme/ui/speech_to_text";
1212

1313
type ReplaceFieldTypes<TSource, TReplacement> = {
1414
[P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P];
@@ -17,6 +17,7 @@ type ReplaceFieldTypes<TSource, TReplacement> = {
1717
type ISpeechToTextOptionsNarrowedEvents = {
1818
onContentReady?: ((e: ContentReadyEvent) => void);
1919
onDisposing?: ((e: DisposingEvent) => void);
20+
onEnd?: ((e: EndEvent) => void) | undefined;
2021
onError?: ((e: ErrorEvent) => void) | undefined;
2122
onInitialized?: ((e: InitializedEvent) => void);
2223
onResult?: ((e: ResultEvent) => void) | undefined;
@@ -43,7 +44,7 @@ const SpeechToText = memo(
4344
}
4445
), []);
4546

46-
const independentEvents = useMemo(() => (["onContentReady","onDisposing","onError","onInitialized","onResult","onStartClick","onStopClick"]), []);
47+
const independentEvents = useMemo(() => (["onContentReady","onDisposing","onEnd","onError","onInitialized","onResult","onStartClick","onStopClick"]), []);
4748

4849
const expectedChildren = useMemo(() => ({
4950
customSpeechRecognizer: { optionName: "customSpeechRecognizer", isCollectionItem: false },

packages/devextreme-vue/src/speech-to-text.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
CustomSpeechRecognizer,
77
ContentReadyEvent,
88
DisposingEvent,
9+
EndEvent,
910
ErrorEvent,
1011
InitializedEvent,
1112
OptionChangedEvent,
@@ -32,6 +33,7 @@ type AccessibleOptions = Pick<Properties,
3233
"hoverStateEnabled" |
3334
"onContentReady" |
3435
"onDisposing" |
36+
"onEnd" |
3537
"onError" |
3638
"onInitialized" |
3739
"onOptionChanged" |
@@ -68,6 +70,7 @@ const componentConfig = {
6870
hoverStateEnabled: Boolean,
6971
onContentReady: Function as PropType<((e: ContentReadyEvent) => void)>,
7072
onDisposing: Function as PropType<((e: DisposingEvent) => void)>,
73+
onEnd: Function as PropType<((e: EndEvent) => void)>,
7174
onError: Function as PropType<((e: ErrorEvent) => void)>,
7275
onInitialized: Function as PropType<((e: InitializedEvent) => void)>,
7376
onOptionChanged: Function as PropType<((e: OptionChangedEvent) => void)>,
@@ -100,6 +103,7 @@ const componentConfig = {
100103
"update:hoverStateEnabled": null,
101104
"update:onContentReady": null,
102105
"update:onDisposing": null,
106+
"update:onEnd": null,
103107
"update:onError": null,
104108
"update:onInitialized": null,
105109
"update:onOptionChanged": null,

packages/devextreme/js/__internal/core/speech_recognition_adapter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ type SpeechRecognition = SpeechRecognitionConfig & {
1010
interface SpeechRecognitionEvents {
1111
onResult: (event: Event) => void;
1212
onError: (event: Event) => void;
13-
onEnd: () => void;
13+
onEnd: (event: Event) => void;
1414
}
1515

1616
export const NOT_SUPPORTED_ERROR = 'E1065';

packages/devextreme/js/__internal/ui/speech_to_text/speech_to_text.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ enum SpeechToTextState {
2727
DISABLED = 'disabled',
2828
}
2929

30-
type SpeechToTextActions = Pick<SpeechToTextProperties, 'onStartClick' | 'onStopClick' | 'onResult' | 'onError'>;
30+
type SpeechToTextActions = Pick<SpeechToTextProperties, 'onStartClick' | 'onStopClick' | 'onResult' | 'onError' | 'onEnd'>;
3131
type PointerLikeEvent = KeyboardEvent | MouseEvent | PointerEvent | TouchEvent;
3232

3333
const ACTIONS: (keyof SpeechToTextActions)[] = [
3434
'onStartClick',
3535
'onStopClick',
3636
'onResult',
3737
'onError',
38+
'onEnd',
3839
];
3940

4041
type Properties = SpeechToTextProperties & {
@@ -68,6 +69,8 @@ class SpeechToText extends Widget<Properties> {
6869
onStopClick: undefined,
6970
onResult: undefined,
7071
onError: undefined,
72+
onEnd: undefined,
73+
speechRecognitionConfig: undefined,
7174
};
7275
}
7376

@@ -204,7 +207,7 @@ class SpeechToText extends Widget<Properties> {
204207
: startText ?? '';
205208
}
206209

207-
private _emitNativeEvent<K extends keyof Pick<SpeechToTextActions, 'onResult' | 'onError'>>(name: K, event: Event): void {
210+
private _emitNativeEvent<K extends keyof Pick<SpeechToTextActions, 'onResult' | 'onError' | 'onEnd'>>(name: K, event: Event): void {
208211
this._actions[name]?.({ component: this, element: this.element(), event });
209212
}
210213

@@ -244,10 +247,12 @@ class SpeechToText extends Widget<Properties> {
244247
this._emitDxEvent('onStopClick', e.event);
245248
}
246249

247-
private _handleSpeechRecognitionEnd(): void {
250+
private _handleSpeechRecognitionEnd(event: Event): void {
248251
if (this._state !== SpeechToTextState.DISABLED && !this._isCustomSpeechRecognitionEnabled()) {
249252
this._setState(SpeechToTextState.INITIAL);
250253
}
254+
255+
this._emitNativeEvent('onEnd', event);
251256
}
252257

253258
private _handleSpeechRecognitionResult(event: Event): void {
@@ -258,6 +263,12 @@ class SpeechToText extends Widget<Properties> {
258263
this._emitNativeEvent('onError', event);
259264
}
260265

266+
private _stopRecognitionOnDisable(disabled?: boolean): void {
267+
if (disabled) {
268+
this._speechRecognitionAdapter?.stop();
269+
}
270+
}
271+
261272
private _setState(newState: SpeechToTextState): void {
262273
if (this._state === newState) {
263274
return;
@@ -315,6 +326,7 @@ class SpeechToText extends Widget<Properties> {
315326
case 'disabled':
316327
this._button?.option(name, value);
317328
this._setState(value ? SpeechToTextState.DISABLED : SpeechToTextState.INITIAL);
329+
this._stopRecognitionOnDisable(value);
318330
break;
319331

320332
case 'startIcon':

packages/devextreme/js/ui/speech_to_text.d.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ export type ResultEvent = EventInfo<dxSpeechToText> & { event: Event };
9898
*/
9999
export type ErrorEvent = EventInfo<dxSpeechToText> & { event: Event };
100100

101+
/**
102+
* @docid _ui_speech_to_text_EndEvent
103+
* @public
104+
* @type object
105+
* @inherits EventInfo
106+
*/
107+
export type EndEvent = EventInfo<dxSpeechToText> & { event: Event };
108+
101109
/**
102110
* @docid _ui_speech_to_text_ContentReadyEvent
103111
* @public
@@ -225,6 +233,15 @@ export interface Properties extends WidgetOptions<dxSpeechToText> {
225233
* @public
226234
*/
227235
onError?: ((e: ErrorEvent) => void) | undefined;
236+
237+
/**
238+
* @docid dxSpeechToTextOptions.onEnd
239+
* @default undefined
240+
* @type_function_param1 e:{ui/speech_to_text:EndEvent}
241+
* @action
242+
* @public
243+
*/
244+
onEnd?: ((e: EndEvent) => void) | undefined;
228245
}
229246

230247
/**
@@ -258,7 +275,7 @@ type FilterOutHidden<T> = Omit<T, 'onFocusIn' | 'onFocusOut'>;
258275
type EventsIntegrityCheckingHelper = CheckedEvents<
259276
FilterOutHidden<Properties>,
260277
Required<Events>,
261-
'onStartClick' | 'onStopClick' | 'onResult' | 'onError'
278+
'onStartClick' | 'onStopClick' | 'onResult' | 'onError' | 'onEnd'
262279
>;
263280

264281
/**

packages/devextreme/js/ui/speech_to_text_types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export {
55
StopClickEvent,
66
ResultEvent,
77
ErrorEvent,
8+
EndEvent,
89
ContentReadyEvent,
910
DisposingEvent,
1011
InitializedEvent,

packages/devextreme/testing/tests/DevExpress.ui.widgets/speechToText.tests.js

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ QUnit.module('Options', moduleConfig, () => {
753753

754754
QUnit.module('Component Lifecycle', moduleConfig, () => {
755755
QUnit.test('should properly initialize actions with noop fallback', function(assert) {
756-
const expectedActions = ['onStartClick', 'onStopClick', 'onResult', 'onError'];
756+
const expectedActions = ['onStartClick', 'onStopClick', 'onResult', 'onError', 'onEnd'];
757757

758758
expectedActions.forEach(action => {
759759
assert.notStrictEqual(this.instance._actions[action], undefined, `${action} action initialized`);
@@ -869,6 +869,7 @@ QUnit.module('SpeechRecognitionAdapter integration', moduleConfig, () => {
869869
const actionsSpies = {
870870
onResult: sinon.spy(),
871871
onError: sinon.spy(),
872+
onEnd: sinon.spy(),
872873
};
873874

874875
this.reinit(actionsSpies);
@@ -878,6 +879,7 @@ QUnit.module('SpeechRecognitionAdapter integration', moduleConfig, () => {
878879
const actions = [
879880
{ option: 'onResult', method: 'onresult', spy: actionsSpies.onResult, event: { type: 'result' } },
880881
{ option: 'onError', method: 'onerror', spy: actionsSpies.onError, event: { type: 'error' } },
882+
{ option: 'onEnd', method: 'onend', spy: actionsSpies.onEnd, event: { type: 'end' } },
881883
];
882884

883885
actions.forEach(({ option, method, spy, event }) => {
@@ -953,14 +955,16 @@ QUnit.module('SpeechRecognitionAdapter integration', moduleConfig, () => {
953955
assert.strictEqual(this.getSpeechRecognition().lang, 'fr-FR', 'config updated in recognition instance');
954956
});
955957

956-
QUnit.test('should update onResult/onError handlers at runtime', function(assert) {
958+
QUnit.test('should update onResult/onError/onEnd handlers at runtime', function(assert) {
957959
const initialActionsSpies = {
958960
onResult: sinon.spy(),
959961
onError: sinon.spy(),
962+
onEnd: sinon.spy(),
960963
};
961964
const updatedActionsSpies = {
962965
onResult: sinon.spy(),
963966
onError: sinon.spy(),
967+
onEnd: sinon.spy(),
964968
};
965969

966970
this.reinit(initialActionsSpies);
@@ -970,6 +974,8 @@ QUnit.module('SpeechRecognitionAdapter integration', moduleConfig, () => {
970974
const actions = [
971975
{ prop: 'onresult', option: 'onResult', initial: initialActionsSpies.onResult, updated: updatedActionsSpies.onResult, event: { type: 'result' } },
972976
{ prop: 'onerror', option: 'onError', initial: initialActionsSpies.onError, updated: updatedActionsSpies.onError, event: { type: 'error' } },
977+
{ prop: 'onend', option: 'onEnd', initial: initialActionsSpies.onEnd, updated: updatedActionsSpies.onEnd, event: { type: 'error' } },
978+
973979
];
974980

975981
actions.forEach(({ prop, option, initial, updated, event }) => {
@@ -983,14 +989,15 @@ QUnit.module('SpeechRecognitionAdapter integration', moduleConfig, () => {
983989
});
984990
});
985991

986-
QUnit.test('should allow subscribing to result/error via .on()', function(assert) {
987-
assert.expect(6);
992+
QUnit.test('should allow subscribing to result/error/end via .on()', function(assert) {
993+
assert.expect(9);
988994

989995
this.reinit();
990996

991997
const events = [
992998
{ eventName: 'result', trigger: 'onresult' },
993999
{ eventName: 'error', trigger: 'onerror' },
1000+
{ eventName: 'end', trigger: 'onend' },
9941001
];
9951002

9961003
const speechRecognition = this.getSpeechRecognition();
@@ -1006,6 +1013,20 @@ QUnit.module('SpeechRecognitionAdapter integration', moduleConfig, () => {
10061013
});
10071014
});
10081015

1016+
QUnit.test('should stop recognition when disabled set to true at runtime', function(assert) {
1017+
this.reinit();
1018+
const adapter = this.getAdapter();
1019+
const stopSpy = sinon.spy(adapter, 'stop');
1020+
1021+
this.getButton().trigger('dxclick');
1022+
assert.ok(this.$element.hasClass(SPEECH_TO_TEXT_LISTENING_CLASS), 'component in listening state');
1023+
1024+
this.instance.option('disabled', true);
1025+
1026+
assert.ok(stopSpy.calledOnce, 'stop called when disabled');
1027+
assert.ok(!this.$element.hasClass(SPEECH_TO_TEXT_LISTENING_CLASS), 'state reset after disabling');
1028+
});
1029+
10091030
QUnit.test('should dispose adapter and set it to null', function(assert) {
10101031
this.reinit();
10111032

packages/devextreme/ts/dx.all.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27762,6 +27762,13 @@ declare module DevExpress.ui {
2776227762
*/
2776327763
export type DisposingEvent =
2776427764
DevExpress.common.core.events.EventInfo<dxSpeechToText>;
27765+
/**
27766+
* [descr:_ui_speech_to_text_EndEvent]
27767+
*/
27768+
export type EndEvent =
27769+
DevExpress.common.core.events.EventInfo<dxSpeechToText> & {
27770+
event: Event;
27771+
};
2776527772
/**
2776627773
* [descr:_ui_speech_to_text_ErrorEvent]
2776727774
*/
@@ -27856,6 +27863,11 @@ declare module DevExpress.ui {
2785627863
* [descr:dxSpeechToTextOptions.onError]
2785727864
*/
2785827865
onError?: ((e: ErrorEvent) => void) | undefined;
27866+
27867+
/**
27868+
* [descr:dxSpeechToTextOptions.onEnd]
27869+
*/
27870+
onEnd?: ((e: EndEvent) => void) | undefined;
2785927871
}
2786027872
/**
2786127873
* [descr:_ui_speech_to_text_ResultEvent]

0 commit comments

Comments
 (0)