@chat21/chat21-web-widget 5.1.34-rc1 → 5.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/.github/workflows/docker-community-push-latest.yml +13 -23
  2. package/.github/workflows/docker-image-tag-community-tag-push.yml +12 -22
  3. package/CHANGELOG.md +8 -129
  4. package/Dockerfile +5 -4
  5. package/angular.json +3 -21
  6. package/docs/changelog/this-branch.md +0 -36
  7. package/env.sample +2 -3
  8. package/nginx.conf +2 -22
  9. package/package.json +3 -10
  10. package/src/app/app.component.html +2 -2
  11. package/src/app/app.component.scss +14 -25
  12. package/src/app/app.component.spec.ts +6 -21
  13. package/src/app/app.component.ts +9 -10
  14. package/src/app/app.module.ts +0 -13
  15. package/src/app/component/conversation-detail/conversation/conversation.component.html +11 -25
  16. package/src/app/component/conversation-detail/conversation/conversation.component.scss +2 -40
  17. package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +75 -644
  18. package/src/app/component/conversation-detail/conversation/conversation.component.ts +14 -100
  19. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +13 -25
  20. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +5 -123
  21. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +0 -1
  22. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +10 -23
  23. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +1 -19
  24. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +149 -242
  25. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +5 -8
  26. package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +3 -53
  27. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +96 -200
  28. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +6 -211
  29. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +78 -452
  30. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +76 -291
  31. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +53 -113
  32. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +4 -12
  33. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +29 -274
  34. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +9 -23
  35. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +8 -80
  36. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +23 -29
  37. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +16 -185
  38. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +14 -34
  39. package/src/app/component/error-alert/error-alert.component.spec.ts +5 -65
  40. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +7 -16
  41. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +0 -21
  42. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +7 -89
  43. package/src/app/component/form/form-builder/form-builder.component.html +1 -1
  44. package/src/app/component/form/form-builder/form-builder.component.spec.ts +21 -163
  45. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +4 -8
  46. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +5 -10
  47. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +16 -90
  48. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +0 -26
  49. package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +11 -45
  50. package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +6 -24
  51. package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +5 -14
  52. package/src/app/component/form/inputs/form-text/form-text.component.html +12 -14
  53. package/src/app/component/form/inputs/form-text/form-text.component.scss +1 -11
  54. package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +17 -113
  55. package/src/app/component/form/inputs/form-text/form-text.component.ts +3 -35
  56. package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +11 -13
  57. package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +5 -6
  58. package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +13 -149
  59. package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +0 -26
  60. package/src/app/component/form/prechat-form/prechat-form.component.html +11 -14
  61. package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +10 -102
  62. package/src/app/component/form/prechat-form/prechat-form.component.ts +1 -8
  63. package/src/app/component/home/home.component.html +31 -38
  64. package/src/app/component/home/home.component.scss +2 -4
  65. package/src/app/component/home/home.component.spec.ts +11 -226
  66. package/src/app/component/home-conversations/home-conversations.component.html +26 -30
  67. package/src/app/component/home-conversations/home-conversations.component.scss +0 -3
  68. package/src/app/component/home-conversations/home-conversations.component.spec.ts +36 -212
  69. package/src/app/component/last-message/last-message.component.html +9 -15
  70. package/src/app/component/last-message/last-message.component.scss +2 -16
  71. package/src/app/component/last-message/last-message.component.spec.ts +23 -204
  72. package/src/app/component/last-message/last-message.component.ts +1 -4
  73. package/src/app/component/launcher-button/launcher-button.component.html +13 -8
  74. package/src/app/component/launcher-button/launcher-button.component.spec.ts +8 -104
  75. package/src/app/component/list-all-conversations/list-all-conversations.component.html +17 -12
  76. package/src/app/component/list-all-conversations/list-all-conversations.component.scss +0 -2
  77. package/src/app/component/list-conversations/list-conversations.component.html +22 -22
  78. package/src/app/component/menu-options/menu-options.component.html +20 -30
  79. package/src/app/component/menu-options/menu-options.component.spec.ts +9 -125
  80. package/src/app/component/message/audio/audio.component.html +15 -13
  81. package/src/app/component/message/audio/audio.component.spec.ts +5 -140
  82. package/src/app/component/message/audio/audio.component.ts +5 -1
  83. package/src/app/component/message/avatar/avatar.component.html +2 -2
  84. package/src/app/component/message/avatar/avatar.component.spec.ts +7 -99
  85. package/src/app/component/message/bubble-message/bubble-message.component.html +51 -38
  86. package/src/app/component/message/bubble-message/bubble-message.component.scss +1 -54
  87. package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +57 -154
  88. package/src/app/component/message/bubble-message/bubble-message.component.ts +11 -89
  89. package/src/app/component/message/buttons/action-button/action-button.component.html +4 -3
  90. package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +5 -49
  91. package/src/app/component/message/buttons/link-button/link-button.component.scss +8 -5
  92. package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +5 -50
  93. package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +5 -44
  94. package/src/app/component/message/carousel/carousel.component.html +16 -29
  95. package/src/app/component/message/carousel/carousel.component.scss +8 -20
  96. package/src/app/component/message/carousel/carousel.component.spec.ts +3 -80
  97. package/src/app/component/message/carousel/carousel.component.ts +0 -16
  98. package/src/app/component/message/frame/frame.component.html +4 -9
  99. package/src/app/component/message/frame/frame.component.spec.ts +15 -34
  100. package/src/app/component/message/frame/frame.component.ts +2 -7
  101. package/src/app/component/message/html/html.component.html +1 -1
  102. package/src/app/component/message/html/html.component.scss +1 -1
  103. package/src/app/component/message/html/html.component.spec.ts +7 -24
  104. package/src/app/component/message/image/image.component.html +10 -12
  105. package/src/app/component/message/image/image.component.scss +0 -16
  106. package/src/app/component/message/image/image.component.spec.ts +15 -101
  107. package/src/app/component/message/image/image.component.ts +51 -90
  108. package/src/app/component/message/info-message/info-message.component.spec.ts +14 -26
  109. package/src/app/component/message/like-unlike/like-unlike.component.html +9 -7
  110. package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +3 -31
  111. package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +17 -38
  112. package/src/app/component/message/text/text.component.html +3 -3
  113. package/src/app/component/message/text/text.component.scss +86 -80
  114. package/src/app/component/message/text/text.component.spec.ts +13 -106
  115. package/src/app/component/message-attachment/message-attachment.component.spec.ts +13 -134
  116. package/src/app/component/selection-department/selection-department.component.html +23 -21
  117. package/src/app/component/selection-department/selection-department.component.spec.ts +14 -159
  118. package/src/app/component/selection-department/selection-department.component.ts +1 -8
  119. package/src/app/component/send-button/send-button.component.html +13 -5
  120. package/src/app/component/send-button/send-button.component.spec.ts +2 -2
  121. package/src/app/component/star-rating-widget/star-rating-widget.component.html +81 -51
  122. package/src/app/directives/tooltip.directive.spec.ts +4 -8
  123. package/src/app/modals/confirm-close/confirm-close.component.html +8 -20
  124. package/src/app/modals/confirm-close/confirm-close.component.scss +0 -3
  125. package/src/app/modals/confirm-close/confirm-close.component.spec.ts +4 -13
  126. package/src/app/modals/confirm-close/confirm-close.component.ts +1 -8
  127. package/src/app/pipe/html-entites-encode.pipe.spec.ts +2 -35
  128. package/src/app/pipe/marked.pipe.spec.ts +2 -38
  129. package/src/app/pipe/marked.pipe.ts +41 -51
  130. package/src/app/providers/app-config.service.ts +2 -4
  131. package/src/app/providers/brand.service.spec.ts +2 -23
  132. package/src/app/providers/brand.service.ts +1 -1
  133. package/src/app/providers/global-settings.service.spec.ts +14 -1009
  134. package/src/app/providers/global-settings.service.ts +2 -82
  135. package/src/app/providers/translator.service.ts +6 -26
  136. package/src/app/sass/_variables.scss +0 -3
  137. package/src/app/sass/animations.scss +1 -19
  138. package/src/app/utils/globals.ts +1 -21
  139. package/src/app/utils/utils-resources.ts +1 -1
  140. package/src/assets/i18n/en.json +99 -106
  141. package/src/assets/i18n/es.json +100 -107
  142. package/src/assets/i18n/fr.json +100 -107
  143. package/src/assets/i18n/it.json +98 -107
  144. package/src/assets/twp/index-dev.html +0 -18
  145. package/src/chat21-core/models/message.ts +1 -2
  146. package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +2 -3
  147. package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +0 -12
  148. package/src/chat21-core/providers/scripts/script.service.spec.ts +2 -12
  149. package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
  150. package/src/chat21-core/utils/utils-message.ts +0 -7
  151. package/src/chat21-core/utils/utils.ts +2 -5
  152. package/src/widget-config-template.json +1 -4
  153. package/src/widget-config.json +1 -4
  154. package/tsconfig.json +0 -5
  155. package/.angular-mcp-cache/package.json +0 -1
  156. package/.cursor/angular18-accessibility-auditor-skill.md +0 -442
  157. package/.cursor/mcp.json +0 -15
  158. package/.github/workflows/build.yml +0 -22
  159. package/.github/workflows/playwright.yml +0 -27
  160. package/mocks/voice-websocket-mock/server.cjs +0 -245
  161. package/playwright.config.ts +0 -41
  162. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +0 -46
  163. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +0 -83
  164. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +0 -192
  165. package/src/app/component/form/prechat-form-test-mock.ts +0 -35
  166. package/src/app/component/message/audio-sync/audio-sync.component.html +0 -18
  167. package/src/app/component/message/audio-sync/audio-sync.component.scss +0 -65
  168. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +0 -103
  169. package/src/app/component/message/audio-sync/audio-sync.component.ts +0 -643
  170. package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +0 -117
  171. package/src/app/providers/tts-audio-playback-coordinator.service.ts +0 -109
  172. package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +0 -12
  173. package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +0 -171
  174. package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +0 -39
  175. package/src/app/providers/voice/audio.types.ts +0 -40
  176. package/src/app/providers/voice/vad.service.spec.ts +0 -28
  177. package/src/app/providers/voice/vad.service.ts +0 -70
  178. package/src/app/providers/voice/voice-streaming.service.spec.ts +0 -23
  179. package/src/app/providers/voice/voice-streaming.service.ts +0 -702
  180. package/src/app/providers/voice/voice-streaming.types.ts +0 -112
  181. package/src/app/providers/voice/voice.service.spec.ts +0 -227
  182. package/src/app/providers/voice/voice.service.ts +0 -973
  183. package/src/app/shims/onnxruntime-web-wasm.ts +0 -4
  184. package/src/assets/onnx/ort-wasm-simd-threaded.mjs +0 -59
  185. package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
  186. package/src/assets/sounds/keyboard.mp3 +0 -0
  187. package/src/assets/twp/tiledesk_widget_files/widget-css-override-example.css +0 -14
  188. package/src/assets/vad/silero_vad_legacy.onnx +0 -0
  189. package/src/assets/vad/vad.worklet.bundle.min.js +0 -1
  190. package/src/chat21-core/providers/chat-manager.spec.ts +0 -72
  191. package/tests/widget-form-rich.spec.ts +0 -67
  192. package/tests/widget-index-dev-settings.spec.ts +0 -52
  193. package/tests/widget-twp-iframe.spec.ts +0 -39
@@ -1,127 +1,31 @@
1
- import { Component } from '@angular/core';
2
1
  import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
3
- import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
4
- import { By } from '@angular/platform-browser';
2
+ import { FormGroupDirective } from '@angular/forms';
5
3
 
6
- import { FormArray } from 'src/chat21-core/models/formArray';
7
- import { clonePrechatFormJsonMock } from '../../prechat-form-test-mock';
8
4
  import { FormTextComponent } from './form-text.component';
9
5
 
10
- @Component({
11
- template: `
12
- <form [formGroup]="form">
13
- <chat-form-text
14
- [element]="element"
15
- [controlName]="controlName"
16
- [translationErrorLabelMap]="translationErrorLabelMap"
17
- [stylesMap]="stylesMap"
18
- [hasSubmitted]="hasSubmitted"
19
- ></chat-form-text>
20
- </form>`,
21
- })
22
- class FormTextHostComponent {
23
- form = new FormBuilder().group({
24
- userFullname: ['', Validators.required],
25
- userEmail: ['', [Validators.required, Validators.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)]],
26
- });
27
- controlName = 'userFullname';
28
- element = {
29
- name: 'userFullname',
30
- type: 'text',
31
- mandatory: true,
32
- text: 'User fullname',
33
- tabIndex: 1411,
34
- } as FormArray;
35
- stylesMap = new Map<string, string>([
36
- ['themeColor', '#2a6ac1'],
37
- ['foregroundColor', '#ffffff'],
38
- ]);
39
- translationErrorLabelMap = new Map<string, string>([
40
- ['LABEL_ERROR_FIELD_REQUIRED', 'Required field'],
41
- ]);
42
- hasSubmitted = false;
43
- }
44
-
45
6
  describe('FormTextComponent', () => {
46
- let hostFixture: ComponentFixture<FormTextHostComponent>;
47
- let host: FormTextHostComponent;
48
-
7
+ let component: FormTextComponent;
8
+ let fixture: ComponentFixture<FormTextComponent>;
9
+ let stylesMap = new Map<string, string>();
10
+ let translationErrorLabelMap = new Map<string, string>();
49
11
  beforeEach(waitForAsync(() => {
50
12
  TestBed.configureTestingModule({
51
- declarations: [FormTextHostComponent, FormTextComponent],
52
- imports: [ReactiveFormsModule],
53
- }).compileComponents();
13
+ declarations: [ FormTextComponent ],
14
+ providers: [ FormGroupDirective ]
15
+ })
16
+ .compileComponents();
54
17
  }));
55
18
 
56
19
  beforeEach(() => {
57
- hostFixture = TestBed.createComponent(FormTextHostComponent);
58
- host = hostFixture.componentInstance;
59
- hostFixture.detectChanges();
60
- });
61
-
62
- it('should create with label and stable id from mock-like field', () => {
63
- expect(hostFixture.nativeElement.querySelector('chat-form-text')).toBeTruthy();
64
- const input = hostFixture.nativeElement.querySelector(
65
- 'input#c21-prechat-userFullname',
66
- ) as HTMLInputElement;
67
- expect(input.type).toBe('text');
68
- expect(input.getAttribute('aria-required')).toBe('true');
69
- });
70
-
71
- it('should use email input type when control name suggests email', () => {
72
- host.controlName = 'userEmail';
73
- host.element = clonePrechatFormJsonMock()[1] as FormArray;
74
- host.element.text = 'Email';
75
- hostFixture.detectChanges();
76
- const input = hostFixture.nativeElement.querySelector(
77
- 'input#c21-prechat-userEmail',
78
- ) as HTMLInputElement;
79
- expect(input.type).toBe('email');
80
- });
81
-
82
- it('should show required error region when submitted and empty', () => {
83
- host.hasSubmitted = true;
84
- hostFixture.detectChanges();
85
- const input = hostFixture.nativeElement.querySelector(
86
- 'input#c21-prechat-userFullname',
87
- ) as HTMLInputElement;
88
- expect(input.getAttribute('aria-invalid')).toBe('true');
89
- expect(input.getAttribute('aria-describedby')).toBe('c21-prechat-userFullname-errors');
90
- const alert = hostFixture.nativeElement.querySelector(
91
- '#c21-prechat-userFullname-errors',
92
- ) as HTMLElement;
93
- expect(alert.textContent).toContain('Required field');
94
- });
95
-
96
- it('onEnterPressed should emit keyboard event', () => {
97
- const inner = hostFixture.debugElement.query(By.directive(FormTextComponent))
98
- .componentInstance as FormTextComponent;
99
- spyOn(inner.onKeyEnterPressed, 'emit');
100
- const ev = new KeyboardEvent('keydown', { key: 'Enter' });
101
- inner.onEnterPressed(ev);
102
- expect(inner.onKeyEnterPressed.emit).toHaveBeenCalledWith(ev);
103
- });
104
-
105
- it('ariaInvalid should stay false until form is submitted', () => {
106
- const inner = hostFixture.debugElement.query(By.directive(FormTextComponent))
107
- .componentInstance as FormTextComponent;
108
- expect(inner.ariaInvalid).toBe('false');
109
- host.hasSubmitted = true;
110
- hostFixture.detectChanges();
111
- expect(inner.ariaInvalid).toBe('true');
20
+ fixture = TestBed.createComponent(FormTextComponent);
21
+ component = fixture.componentInstance;
22
+ component.stylesMap = stylesMap.set('themeColor', "#2a6ac1")
23
+ .set('foregroundColor', "#ffffff")
24
+ component.translationErrorLabelMap = translationErrorLabelMap.set('LABEL_ERROR_FIELD_REQUIRED', "LABEL_ERROR_FIELD_REQUIRED")
25
+ fixture.detectChanges();
112
26
  });
113
27
 
114
- it('ngOnChanges should map theme CSS variables onto host', () => {
115
- const innerDe = hostFixture.debugElement.query(By.directive(FormTextComponent));
116
- const inner = innerDe.componentInstance as FormTextComponent;
117
- inner.ngOnChanges({
118
- stylesMap: {
119
- currentValue: host.stylesMap,
120
- previousValue: null,
121
- firstChange: true,
122
- isFirstChange: () => true,
123
- },
124
- } as any);
125
- expect(innerDe.nativeElement.style.getPropertyValue('--themeColor')).toBe('#2a6ac1');
28
+ it('should create', () => {
29
+ expect(component).toBeTruthy();
126
30
  });
127
31
  });
@@ -1,6 +1,5 @@
1
- import { Component, ElementRef, EventEmitter, Input, OnInit, OnDestroy, Output, SimpleChange, ViewChild } from '@angular/core';
1
+ import { Component, ElementRef, EventEmitter, Input, OnInit, Output, SimpleChange, ViewChild } from '@angular/core';
2
2
  import { FormGroup, FormGroupDirective } from '@angular/forms';
3
- import { Subscription } from 'rxjs';
4
3
  import { FormArray } from '../../../../../chat21-core/models/formArray';
5
4
 
6
5
  @Component({
@@ -8,7 +7,7 @@ import { FormArray } from '../../../../../chat21-core/models/formArray';
8
7
  templateUrl: './form-text.component.html',
9
8
  styleUrls: ['./form-text.component.scss']
10
9
  })
11
- export class FormTextComponent implements OnInit, OnDestroy {
10
+ export class FormTextComponent implements OnInit {
12
11
 
13
12
  @Input() element: FormArray;
14
13
  @Input() controlName: string;
@@ -20,50 +19,19 @@ export class FormTextComponent implements OnInit, OnDestroy {
20
19
  @ViewChild('div_input') input: ElementRef;
21
20
  form: FormGroup<any>;
22
21
  inputType: string = 'text'
23
- private valueChangesSub?: Subscription;
24
-
25
- get fieldBaseId(): string {
26
- const raw = this.element?.name || this.controlName || 'field';
27
- return 'c21-prechat-' + String(raw).replace(/[^a-zA-Z0-9_-]/g, '_');
28
- }
29
-
30
- get errorsId(): string {
31
- return this.fieldBaseId + '-errors';
32
- }
33
-
34
- get ariaDescribedByErrors(): string | null {
35
- const name = this.element?.name;
36
- if (!this.hasSubmitted || !this.form?.controls?.[name]?.errors) {
37
- return null;
38
- }
39
- return this.errorsId;
40
- }
41
-
42
- get ariaInvalid(): 'true' | 'false' {
43
- const name = this.element?.name;
44
- if (!this.hasSubmitted || !this.form?.controls?.[name]) {
45
- return 'false';
46
- }
47
- return this.form.controls[name].invalid ? 'true' : 'false';
48
- }
49
-
50
22
  constructor(private rootFormGroup: FormGroupDirective,
51
23
  private elementRef: ElementRef) { }
52
24
 
53
25
  ngOnInit() {
54
26
  this.form = this.rootFormGroup.control as FormGroup<any>;
55
27
  if(this.form && this.form.controls && this.form.controls[this.controlName]){
56
- this.valueChangesSub = this.form.controls[this.controlName].valueChanges.subscribe((value) => {
28
+ this.form.controls[this.controlName].valueChanges.subscribe((value) => {
57
29
  this.hasSubmitted= false;
58
30
  this.setFormStyle();
59
31
  })
60
32
  }
61
33
  }
62
34
 
63
- ngOnDestroy() {
64
- this.valueChangesSub?.unsubscribe();
65
- }
66
-
67
35
  ngOnChanges(changes: SimpleChange){
68
36
  if(this.controlName && (this.controlName.toLowerCase().includes('email') || this.controlName.toLowerCase().includes('e-mail')) ){
69
37
  this.inputType = 'email';
@@ -1,27 +1,25 @@
1
1
  <form [formGroup]="form">
2
- <div class="form-group label-floating" [id]="'wrap-' + fieldBaseId" #div_input
2
+ <div class="form-group label-floating" id="div_input" #div_input
3
3
  [ngClass]="{'is-empty': !form?.controls[element.name]?.value}">
4
- <label class="control-label" [attr.for]="fieldBaseId">
4
+ <label class="control-label">
5
5
  {{element?.text}}
6
6
  </label>
7
- <textarea
8
- [id]="fieldBaseId"
9
- [formControlName]="element?.name"
7
+ <textarea
8
+ [formControlName]="element?.name"
10
9
  [rows]="rows"
11
10
  [tabIndex]="element?.tabIndex"
12
11
  type="text"
13
12
  class="form-control"
14
- [attr.aria-required]="element?.mandatory ? 'true' : null"
15
- [attr.aria-invalid]="ariaInvalid"
16
- [attr.aria-describedby]="ariaDescribedByErrors"
17
13
  (keydown)="onkeydown($event)"
18
- (keydown.enter)="onEnterPressed($event)"
19
- (focus)="onFocus()"
14
+ (keydown.enter)="onEnterPressed($event)"
15
+ (focus)="onFocus()"
20
16
  (focusout)="onFocusOut()">
21
17
  </textarea>
22
18
  </div>
23
- <div [id]="errorsId" *ngIf="hasSubmitted && form?.controls[element.name]?.errors" class="text-danger" role="alert">
24
- <span *ngIf="form?.controls[element.name].hasError('required')">{{translationErrorLabelMap.get('LABEL_ERROR_FIELD_REQUIRED')}}</span>
25
- <span *ngIf="form?.controls[element.name].hasError('pattern') && element?.errorLabel">{{element.errorLabel}}</span>
19
+ <div *ngIf="hasSubmitted && form?.controls[element.name].errors && form?.controls[element.name].hasError('required')" class="text-danger">
20
+ {{translationErrorLabelMap.get('LABEL_ERROR_FIELD_REQUIRED')}}
21
+ </div>
22
+ <div *ngIf="hasSubmitted && form?.controls[element.name].errors && form?.controls[element.name].hasError('pattern') && element?.errorLabel" class="text-danger">
23
+ {{element.errorLabel}}
26
24
  </div>
27
25
  </form>
@@ -42,7 +42,7 @@
42
42
  }
43
43
 
44
44
  /* Remove input border colour */
45
- textarea:focus:not(:focus-visible),
45
+ textarea:focus,
46
46
  textarea:active,
47
47
  textarea:hover,
48
48
  textarea:visited {
@@ -52,6 +52,9 @@
52
52
  background-position: center bottom,center calc(100% - 1px);
53
53
  background-color: rgba(0,0,0,0);
54
54
  transition: background 0s ease-out;
55
+ // box-shadow: none;
56
+ // background-color: #ffffff;
57
+ // outline: none;
55
58
  }
56
59
 
57
60
  /* Remove input background colour for Chrome autocomplete */
@@ -179,15 +182,11 @@ textarea {
179
182
  background-repeat: no-repeat;
180
183
  background-position: center bottom,center calc(100% - 1px);
181
184
  background-size: 100% 2px, 100% 1px;
185
+ // box-shadow: none;
182
186
  transition-duration: 0.3s;
183
187
  width: 100%;
184
188
  }
185
189
 
186
- .form-group.is-focused .form-control:focus-visible {
187
- outline: 2px solid var(--themeColor, #1a73e8) !important;
188
- outline-offset: 2px;
189
- }
190
-
191
190
  .form-group.is-focused.form-info .form-control {
192
191
  background-image: linear-gradient(#00bcd4, #00bcd4), linear-gradient(#D2D2D2, #D2D2D2);
193
192
  }
@@ -1,163 +1,27 @@
1
- import { Component } from '@angular/core';
2
- import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
3
- import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
4
- import { By } from '@angular/platform-browser';
1
+ import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { FormGroup, FormGroupDirective } from '@angular/forms';
5
3
 
6
- import { FormArray } from 'src/chat21-core/models/formArray';
7
- import { clonePrechatFormJsonMock } from '../../prechat-form-test-mock';
8
4
  import { FormTextareaComponent } from './form-textarea.component';
9
5
 
10
- @Component({
11
- template: `
12
- <form [formGroup]="form">
13
- <chat-form-textarea
14
- [element]="element"
15
- [controlName]="controlName"
16
- [rows]="rows"
17
- [translationErrorLabelMap]="translationErrorLabelMap"
18
- [stylesMap]="stylesMap"
19
- [hasSubmitted]="hasSubmitted">
20
- </chat-form-textarea>
21
- </form>`,
22
- })
23
- class FormTextareaHostComponent {
24
- form = new FormBuilder().group({
25
- notes: ['', [Validators.required, Validators.pattern(/^[A-Z]+$/)]],
26
- });
27
- controlName = 'notes';
28
- rows = 5;
29
- element = {
30
- name: 'notes',
31
- type: 'textarea',
32
- text: 'Note libere',
33
- mandatory: true,
34
- tabIndex: 2,
35
- errorLabel: 'Solo maiuscole',
36
- } as FormArray;
37
- stylesMap = new Map<string, string>([
38
- ['themeColor', '#0a0'],
39
- ['foregroundColor', '#efe'],
40
- ]);
41
- translationErrorLabelMap = new Map([['LABEL_ERROR_FIELD_REQUIRED', 'Campo obbligatorio']]);
42
- hasSubmitted = false;
43
- }
44
-
45
6
  describe('FormTextareaComponent', () => {
46
- let hostFixture: ComponentFixture<FormTextareaHostComponent>;
47
- let host: FormTextareaHostComponent;
48
- let textareaEl: HTMLTextAreaElement;
7
+ let component: FormTextareaComponent;
8
+ let fixture: ComponentFixture<FormTextareaComponent>;
49
9
 
50
10
  beforeEach(waitForAsync(() => {
51
11
  TestBed.configureTestingModule({
52
- declarations: [FormTextareaHostComponent, FormTextareaComponent],
53
- imports: [ReactiveFormsModule],
54
- }).compileComponents();
12
+ declarations: [ FormTextareaComponent ],
13
+ providers: [ FormGroupDirective ]
14
+ })
15
+ .compileComponents();
55
16
  }));
56
17
 
57
18
  beforeEach(() => {
58
- hostFixture = TestBed.createComponent(FormTextareaHostComponent);
59
- host = hostFixture.componentInstance;
60
- hostFixture.detectChanges();
61
- textareaEl = hostFixture.nativeElement.querySelector('textarea#c21-prechat-notes') as HTMLTextAreaElement;
62
- });
63
-
64
- it('should create with label bound to element text and stable id', () => {
65
- expect(hostFixture.nativeElement.querySelector('chat-form-textarea')).toBeTruthy();
66
- const label = hostFixture.nativeElement.querySelector('label[for="c21-prechat-notes"]') as HTMLLabelElement;
67
- expect(label.textContent?.trim()).toBe('Note libere');
68
- expect(textareaEl).toBeTruthy();
69
- expect(textareaEl.rows).toBe(5);
70
- expect(textareaEl.getAttribute('aria-required')).toBe('true');
71
- });
72
-
73
- it('fieldBaseId should sanitize unsafe characters in element name', () => {
74
- const inner = hostFixture.debugElement.query(By.directive(FormTextareaComponent)).componentInstance as FormTextareaComponent;
75
- inner.element = { name: 'a b<c>', text: 'x' } as FormArray;
76
- expect(inner.fieldBaseId).toBe('c21-prechat-a_b_c_');
77
- });
78
-
79
- it('should set aria-invalid and show required error when submitted and empty', () => {
80
- host.hasSubmitted = true;
81
- hostFixture.detectChanges();
82
- expect(textareaEl.getAttribute('aria-invalid')).toBe('true');
83
- expect(textareaEl.getAttribute('aria-describedby')).toBe('c21-prechat-notes-errors');
84
- const alert = hostFixture.nativeElement.querySelector('#c21-prechat-notes-errors') as HTMLElement;
85
- expect(alert.getAttribute('role')).toBe('alert');
86
- expect(alert.textContent).toContain('Campo obbligatorio');
87
- });
88
-
89
- it('should show pattern error label when value invalid for pattern', () => {
90
- host.form.patchValue({ notes: 'lowercase' });
91
- host.hasSubmitted = true;
92
- hostFixture.detectChanges();
93
- const alert = hostFixture.nativeElement.querySelector('#c21-prechat-notes-errors') as HTMLElement;
94
- expect(alert.textContent).toContain('Solo maiuscole');
95
- });
96
-
97
- it('onEnterPressed should emit and call preventDefault on handler', () => {
98
- const innerDe = hostFixture.debugElement.query(By.directive(FormTextareaComponent));
99
- const inner = innerDe.componentInstance as FormTextareaComponent;
100
- spyOn(inner.onKeyEnterPressed, 'emit');
101
- const ev = new KeyboardEvent('keydown', { key: 'Enter' });
102
- spyOn(ev, 'preventDefault');
103
- inner.onEnterPressed(ev);
104
- expect(ev.preventDefault).toHaveBeenCalled();
105
- expect(inner.onKeyEnterPressed.emit).toHaveBeenCalledWith(ev);
106
- });
107
-
108
- it('onkeydown with modifier + Enter should append newline to control value', () => {
109
- const inner = hostFixture.debugElement.query(By.directive(FormTextareaComponent)).componentInstance as FormTextareaComponent;
110
- host.form.patchValue({ notes: 'A' });
111
- const ev = {
112
- metaKey: true,
113
- keyCode: 13,
114
- which: 13,
115
- preventDefault: jasmine.createSpy('preventDefault'),
116
- } as any;
117
- inner.onkeydown(ev);
118
- expect(host.form.controls.notes.value).toContain('\r\n');
119
- expect(ev.preventDefault).toHaveBeenCalled();
120
- });
121
-
122
- it('setFormStyle should toggle form-danger / form-success on wrapper', () => {
123
- const inner = hostFixture.debugElement.query(By.directive(FormTextareaComponent)).componentInstance as FormTextareaComponent;
124
- host.form.patchValue({ notes: '' });
125
- inner.setFormStyle();
126
- hostFixture.detectChanges();
127
- const wrap = hostFixture.nativeElement.querySelector('#wrap-c21-prechat-notes') as HTMLElement;
128
- expect(wrap.classList.contains('form-danger')).toBe(true);
129
- host.form.patchValue({ notes: 'ABC' });
130
- inner.setFormStyle();
131
- expect(wrap.classList.contains('form-success')).toBe(true);
132
- });
133
-
134
- it('ngOnChanges should set CSS variables from stylesMap on host element', () => {
135
- const innerDe = hostFixture.debugElement.query(By.directive(FormTextareaComponent));
136
- const inner = innerDe.componentInstance as FormTextareaComponent;
137
- inner.ngOnChanges({
138
- stylesMap: {
139
- currentValue: host.stylesMap,
140
- previousValue: null,
141
- firstChange: true,
142
- isFirstChange: () => true,
143
- },
144
- });
145
- const hostEl: HTMLElement = innerDe.nativeElement;
146
- expect(hostEl.style.getPropertyValue('--themeColor')).toBe('#0a0');
147
- expect(hostEl.style.getPropertyValue('--foregroundColor')).toBe('#efe');
148
- });
149
-
150
- it('fieldBaseId for pre-chat mock fullname matches stable public id prefix', () => {
151
- const inner = hostFixture.debugElement.query(By.directive(FormTextareaComponent))
152
- .componentInstance as FormTextareaComponent;
153
- inner.element = { name: 'userFullname', text: 'Nome' } as FormArray;
154
- expect(inner.fieldBaseId).toBe('c21-prechat-userFullname');
19
+ fixture = TestBed.createComponent(FormTextareaComponent);
20
+ component = fixture.componentInstance;
21
+ fixture.detectChanges();
155
22
  });
156
23
 
157
- it('errorsId should append -errors to fieldBaseId (used with aria-describedby)', () => {
158
- const inner = hostFixture.debugElement.query(By.directive(FormTextareaComponent))
159
- .componentInstance as FormTextareaComponent;
160
- inner.element = clonePrechatFormJsonMock()[1] as FormArray;
161
- expect(inner.errorsId).toBe('c21-prechat-userEmail-errors');
24
+ it('should create', () => {
25
+ expect(component).toBeTruthy();
162
26
  });
163
27
  });
@@ -18,32 +18,6 @@ export class FormTextareaComponent implements OnInit {
18
18
  @Output() onKeyEnterPressed = new EventEmitter<any>();
19
19
  @ViewChild('div_input') input: ElementRef;
20
20
  form: FormGroup<any>;
21
-
22
- get fieldBaseId(): string {
23
- const raw = this.element?.name || this.controlName || 'field';
24
- return 'c21-prechat-' + String(raw).replace(/[^a-zA-Z0-9_-]/g, '_');
25
- }
26
-
27
- get errorsId(): string {
28
- return this.fieldBaseId + '-errors';
29
- }
30
-
31
- get ariaDescribedByErrors(): string | null {
32
- const name = this.element?.name;
33
- if (!this.hasSubmitted || !this.form?.controls?.[name]?.errors) {
34
- return null;
35
- }
36
- return this.errorsId;
37
- }
38
-
39
- get ariaInvalid(): 'true' | 'false' {
40
- const name = this.element?.name;
41
- if (!this.hasSubmitted || !this.form?.controls?.[name]) {
42
- return 'false';
43
- }
44
- return this.form.controls[name].invalid ? 'true' : 'false';
45
- }
46
-
47
21
  constructor(private rootFormGroup: FormGroupDirective,
48
22
  private elementRef: ElementRef) { }
49
23
 
@@ -1,23 +1,19 @@
1
+ <!-- tabindex="1400"-->
1
2
  <div id="chat21-prechat-form"
2
- #afPrechatFormComponent
3
- role="dialog"
4
- aria-modal="true"
5
- cdkTrapFocus
6
- [cdkTrapFocusAutoCapture]="true"
7
- [attr.aria-label]="g.LABEL_COMPLETE_FORM">
3
+ #afPrechatFormComponent
4
+ tabindex="1400"
5
+ aria-modal="true"
6
+ onFocus="document.querySelector('[start-focus-chat21-prechat-form]').focus()">
8
7
 
9
8
  <!-- HEADER -->
10
- <div class="c21-header" [ngStyle]="{'color': g.themeForegroundColor, 'background-image': g.colorGradient180 }">
9
+ <div class="c21-header" [ngStyle]="{'color': g.themeForegroundColor, 'background-image': g.colorGradient180 }" >
11
10
  <div class="c21-header-container">
12
11
 
13
- <!-- ICON CLOSE PANEL -->
12
+ <!-- ICON CLOSE CHAT -->
14
13
  <div class="c21-header-button">
15
14
  <div class="c21-close-button c21-small">
16
- <button type="button"
17
- [attr.aria-label]="g.BUTTON_CLOSE_TO_ICON || 'Close panel'"
18
- class="c21-close-button-body"
19
- (click)="returnClosePage()">
20
- <svg aria-hidden="true" focusable="false" [ngStyle]="{'fill': g.themeForegroundColor }" xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 24 24">
15
+ <button tabindex="1490" aria-label=" chiudi pannello" class="c21-close-button-body" (click)="returnClosePage()">
16
+ <svg role="img" aria-labelledby="altIconTitle" [ngStyle]="{'fill': g.themeForegroundColor }" xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 24 24">
21
17
  <path fill="none" d="M0 0h24v24H0V0z"/>
22
18
  <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>
23
19
  </svg>
@@ -42,7 +38,7 @@
42
38
  <div class="c21-body-container">
43
39
 
44
40
  <div class="c21-body-header">
45
- <h2 start-focus-chat21-prechat-form class="c21-form-message-field">{{g.LABEL_COMPLETE_FORM}}</h2>
41
+ <div start-focus-chat21-prechat-form tabindex="1410" class="c21-form-message-field">{{g.LABEL_COMPLETE_FORM}}</div>
46
42
  </div>
47
43
 
48
44
  <div class="c21-body-content">
@@ -92,6 +88,7 @@
92
88
  </div>
93
89
 
94
90
  </div>
91
+ <span tabindex="1499" onFocus="document.querySelector('[start-focus-chat21-prechat-form]').focus()"></span>
95
92
 
96
93
 
97
94
  <!-- <label *ngIf="g.privacyField" class="privacy-field">