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

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 (201) hide show
  1. package/.angular-mcp-cache/package.json +1 -0
  2. package/.cursor/angular18-accessibility-auditor-skill.md +442 -0
  3. package/.cursor/mcp.json +15 -0
  4. package/.github/workflows/docker-community-push-latest.yml +23 -13
  5. package/.github/workflows/docker-image-tag-community-tag-push.yml +22 -12
  6. package/.github/workflows/playwright.yml +27 -0
  7. package/CHANGELOG.md +130 -6
  8. package/Dockerfile +4 -5
  9. package/angular.json +24 -4
  10. package/docs/changelog/this-branch.md +36 -0
  11. package/env.sample +3 -2
  12. package/mocks/voice-websocket-mock/server.cjs +245 -0
  13. package/nginx.conf +22 -2
  14. package/package.json +10 -3
  15. package/playwright.config.ts +41 -0
  16. package/src/app/app.component.html +2 -2
  17. package/src/app/app.component.scss +25 -14
  18. package/src/app/app.component.spec.ts +21 -6
  19. package/src/app/app.component.ts +10 -9
  20. package/src/app/app.module.ts +15 -0
  21. package/src/app/component/conversation-detail/conversation/conversation.component.html +25 -11
  22. package/src/app/component/conversation-detail/conversation/conversation.component.scss +40 -2
  23. package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +644 -75
  24. package/src/app/component/conversation-detail/conversation/conversation.component.ts +100 -14
  25. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +25 -13
  26. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +123 -5
  27. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +1 -0
  28. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +23 -10
  29. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +33 -2
  30. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +242 -149
  31. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +8 -6
  32. package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +53 -3
  33. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +200 -96
  34. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +211 -6
  35. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +452 -78
  36. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +291 -76
  37. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +113 -53
  38. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +12 -4
  39. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +274 -29
  40. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +23 -9
  41. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +80 -8
  42. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +29 -23
  43. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +185 -16
  44. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +34 -14
  45. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +46 -0
  46. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +83 -0
  47. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +192 -0
  48. package/src/app/component/error-alert/error-alert.component.spec.ts +65 -5
  49. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +16 -7
  50. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +21 -0
  51. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +89 -7
  52. package/src/app/component/form/form-builder/form-builder.component.html +1 -1
  53. package/src/app/component/form/form-builder/form-builder.component.spec.ts +163 -21
  54. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +8 -4
  55. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +10 -5
  56. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +90 -16
  57. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +26 -0
  58. package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +45 -11
  59. package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +24 -6
  60. package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +14 -5
  61. package/src/app/component/form/inputs/form-text/form-text.component.html +14 -12
  62. package/src/app/component/form/inputs/form-text/form-text.component.scss +11 -1
  63. package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +113 -17
  64. package/src/app/component/form/inputs/form-text/form-text.component.ts +35 -3
  65. package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +13 -11
  66. package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +6 -5
  67. package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +149 -13
  68. package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +26 -0
  69. package/src/app/component/form/prechat-form/prechat-form.component.html +14 -11
  70. package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +102 -10
  71. package/src/app/component/form/prechat-form/prechat-form.component.ts +8 -1
  72. package/src/app/component/form/prechat-form-test-mock.ts +35 -0
  73. package/src/app/component/home/home.component.html +38 -31
  74. package/src/app/component/home/home.component.scss +4 -2
  75. package/src/app/component/home/home.component.spec.ts +226 -11
  76. package/src/app/component/home-conversations/home-conversations.component.html +30 -26
  77. package/src/app/component/home-conversations/home-conversations.component.scss +3 -0
  78. package/src/app/component/home-conversations/home-conversations.component.spec.ts +212 -36
  79. package/src/app/component/last-message/last-message.component.html +15 -9
  80. package/src/app/component/last-message/last-message.component.scss +16 -2
  81. package/src/app/component/last-message/last-message.component.spec.ts +204 -23
  82. package/src/app/component/last-message/last-message.component.ts +4 -1
  83. package/src/app/component/launcher-button/launcher-button.component.html +8 -13
  84. package/src/app/component/launcher-button/launcher-button.component.spec.ts +104 -8
  85. package/src/app/component/list-all-conversations/list-all-conversations.component.html +12 -17
  86. package/src/app/component/list-all-conversations/list-all-conversations.component.scss +2 -0
  87. package/src/app/component/list-conversations/list-conversations.component.html +22 -22
  88. package/src/app/component/menu-options/menu-options.component.html +30 -20
  89. package/src/app/component/menu-options/menu-options.component.spec.ts +125 -9
  90. package/src/app/component/message/audio/audio.component.html +13 -15
  91. package/src/app/component/message/audio/audio.component.spec.ts +140 -5
  92. package/src/app/component/message/audio/audio.component.ts +1 -5
  93. package/src/app/component/message/audio-sync/audio-sync.component.html +18 -0
  94. package/src/app/component/message/audio-sync/audio-sync.component.scss +65 -0
  95. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +103 -0
  96. package/src/app/component/message/audio-sync/audio-sync.component.ts +643 -0
  97. package/src/app/component/message/avatar/avatar.component.html +2 -2
  98. package/src/app/component/message/avatar/avatar.component.spec.ts +99 -7
  99. package/src/app/component/message/bubble-message/bubble-message.component.html +43 -51
  100. package/src/app/component/message/bubble-message/bubble-message.component.scss +59 -1
  101. package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +154 -57
  102. package/src/app/component/message/bubble-message/bubble-message.component.ts +152 -109
  103. package/src/app/component/message/buttons/action-button/action-button.component.html +3 -4
  104. package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +49 -5
  105. package/src/app/component/message/buttons/link-button/link-button.component.scss +5 -8
  106. package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +50 -5
  107. package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +44 -5
  108. package/src/app/component/message/carousel/carousel.component.html +29 -16
  109. package/src/app/component/message/carousel/carousel.component.scss +20 -8
  110. package/src/app/component/message/carousel/carousel.component.spec.ts +80 -3
  111. package/src/app/component/message/carousel/carousel.component.ts +16 -0
  112. package/src/app/component/message/frame/frame.component.html +9 -4
  113. package/src/app/component/message/frame/frame.component.spec.ts +34 -15
  114. package/src/app/component/message/frame/frame.component.ts +7 -2
  115. package/src/app/component/message/html/html.component.html +1 -1
  116. package/src/app/component/message/html/html.component.scss +1 -1
  117. package/src/app/component/message/html/html.component.spec.ts +24 -7
  118. package/src/app/component/message/image/image.component.html +12 -10
  119. package/src/app/component/message/image/image.component.scss +16 -0
  120. package/src/app/component/message/image/image.component.spec.ts +101 -15
  121. package/src/app/component/message/image/image.component.ts +90 -51
  122. package/src/app/component/message/info-message/info-message.component.spec.ts +26 -14
  123. package/src/app/component/message/json-sources/json-sources.component.html +38 -0
  124. package/src/app/component/message/json-sources/json-sources.component.scss +201 -0
  125. package/src/app/component/message/json-sources/json-sources.component.ts +89 -0
  126. package/src/app/component/message/like-unlike/like-unlike.component.html +7 -9
  127. package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +31 -3
  128. package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +38 -17
  129. package/src/app/component/message/text/text.component.html +3 -3
  130. package/src/app/component/message/text/text.component.scss +80 -86
  131. package/src/app/component/message/text/text.component.spec.ts +106 -13
  132. package/src/app/component/message-attachment/message-attachment.component.spec.ts +134 -13
  133. package/src/app/component/selection-department/selection-department.component.html +21 -23
  134. package/src/app/component/selection-department/selection-department.component.spec.ts +159 -14
  135. package/src/app/component/selection-department/selection-department.component.ts +8 -1
  136. package/src/app/component/send-button/send-button.component.html +5 -13
  137. package/src/app/component/send-button/send-button.component.spec.ts +2 -2
  138. package/src/app/component/star-rating-widget/star-rating-widget.component.html +51 -81
  139. package/src/app/directives/tooltip.directive.spec.ts +8 -4
  140. package/src/app/modals/confirm-close/confirm-close.component.html +20 -8
  141. package/src/app/modals/confirm-close/confirm-close.component.scss +3 -0
  142. package/src/app/modals/confirm-close/confirm-close.component.spec.ts +13 -4
  143. package/src/app/modals/confirm-close/confirm-close.component.ts +8 -1
  144. package/src/app/pipe/html-entites-encode.pipe.spec.ts +35 -2
  145. package/src/app/pipe/marked.pipe.spec.ts +38 -2
  146. package/src/app/pipe/marked.pipe.ts +51 -41
  147. package/src/app/providers/app-config.service.ts +4 -2
  148. package/src/app/providers/brand.service.spec.ts +23 -2
  149. package/src/app/providers/brand.service.ts +1 -1
  150. package/src/app/providers/global-settings.service.spec.ts +1009 -14
  151. package/src/app/providers/global-settings.service.ts +82 -2
  152. package/src/app/providers/json-sources-parser.service.ts +175 -0
  153. package/src/app/providers/translator.service.ts +26 -6
  154. package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +117 -0
  155. package/src/app/providers/tts-audio-playback-coordinator.service.ts +109 -0
  156. package/src/app/providers/url-preview.service.ts +82 -0
  157. package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +12 -0
  158. package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +171 -0
  159. package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +39 -0
  160. package/src/app/providers/voice/audio.types.ts +40 -0
  161. package/src/app/providers/voice/vad.service.spec.ts +28 -0
  162. package/src/app/providers/voice/vad.service.ts +70 -0
  163. package/src/app/providers/voice/voice-streaming.service.spec.ts +23 -0
  164. package/src/app/providers/voice/voice-streaming.service.ts +702 -0
  165. package/src/app/providers/voice/voice-streaming.types.ts +112 -0
  166. package/src/app/providers/voice/voice.service.spec.ts +227 -0
  167. package/src/app/providers/voice/voice.service.ts +973 -0
  168. package/src/app/sass/_variables.scss +3 -0
  169. package/src/app/sass/animations.scss +19 -1
  170. package/src/app/shims/onnxruntime-web-wasm.ts +4 -0
  171. package/src/app/utils/globals.ts +21 -1
  172. package/src/app/utils/json-sources-utils.ts +27 -0
  173. package/src/app/utils/url-utils.ts +98 -0
  174. package/src/app/utils/utils-resources.ts +1 -1
  175. package/src/assets/i18n/en.json +106 -99
  176. package/src/assets/i18n/es.json +107 -100
  177. package/src/assets/i18n/fr.json +107 -100
  178. package/src/assets/i18n/it.json +107 -98
  179. package/src/assets/onnx/ort-wasm-simd-threaded.mjs +59 -0
  180. package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
  181. package/src/assets/sounds/keyboard.mp3 +0 -0
  182. package/src/assets/twp/chatbot-panel.html +3 -1
  183. package/src/assets/twp/index-dev.html +18 -0
  184. package/src/assets/twp/tiledesk_widget_files/widget-css-override-example.css +14 -0
  185. package/src/assets/vad/silero_vad_legacy.onnx +0 -0
  186. package/src/assets/vad/vad.worklet.bundle.min.js +1 -0
  187. package/src/chat21-core/models/message.ts +2 -1
  188. package/src/chat21-core/providers/chat-manager.spec.ts +72 -0
  189. package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +3 -2
  190. package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +12 -0
  191. package/src/chat21-core/providers/scripts/script.service.spec.ts +12 -2
  192. package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
  193. package/src/chat21-core/utils/constants.ts +4 -0
  194. package/src/chat21-core/utils/utils-message.ts +45 -6
  195. package/src/chat21-core/utils/utils.ts +5 -2
  196. package/src/widget-config-template.json +4 -1
  197. package/src/widget-config.json +4 -1
  198. package/tests/widget-form-rich.spec.ts +67 -0
  199. package/tests/widget-index-dev-settings.spec.ts +52 -0
  200. package/tests/widget-twp-iframe.spec.ts +39 -0
  201. package/tsconfig.json +5 -0
@@ -1,27 +1,163 @@
1
- import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
2
- import { FormGroup, FormGroupDirective } from '@angular/forms';
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';
3
5
 
6
+ import { FormArray } from 'src/chat21-core/models/formArray';
7
+ import { clonePrechatFormJsonMock } from '../../prechat-form-test-mock';
4
8
  import { FormTextareaComponent } from './form-textarea.component';
5
9
 
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
+
6
45
  describe('FormTextareaComponent', () => {
7
- let component: FormTextareaComponent;
8
- let fixture: ComponentFixture<FormTextareaComponent>;
46
+ let hostFixture: ComponentFixture<FormTextareaHostComponent>;
47
+ let host: FormTextareaHostComponent;
48
+ let textareaEl: HTMLTextAreaElement;
9
49
 
10
50
  beforeEach(waitForAsync(() => {
11
51
  TestBed.configureTestingModule({
12
- declarations: [ FormTextareaComponent ],
13
- providers: [ FormGroupDirective ]
14
- })
15
- .compileComponents();
52
+ declarations: [FormTextareaHostComponent, FormTextareaComponent],
53
+ imports: [ReactiveFormsModule],
54
+ }).compileComponents();
16
55
  }));
17
56
 
18
57
  beforeEach(() => {
19
- fixture = TestBed.createComponent(FormTextareaComponent);
20
- component = fixture.componentInstance;
21
- fixture.detectChanges();
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');
22
155
  });
23
156
 
24
- it('should create', () => {
25
- expect(component).toBeTruthy();
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');
26
162
  });
27
163
  });
@@ -18,6 +18,32 @@ 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
+
21
47
  constructor(private rootFormGroup: FormGroupDirective,
22
48
  private elementRef: ElementRef) { }
23
49
 
@@ -1,19 +1,23 @@
1
- <!-- tabindex="1400"-->
2
1
  <div id="chat21-prechat-form"
3
- #afPrechatFormComponent
4
- tabindex="1400"
5
- aria-modal="true"
6
- onFocus="document.querySelector('[start-focus-chat21-prechat-form]').focus()">
2
+ #afPrechatFormComponent
3
+ role="dialog"
4
+ aria-modal="true"
5
+ cdkTrapFocus
6
+ [cdkTrapFocusAutoCapture]="true"
7
+ [attr.aria-label]="g.LABEL_COMPLETE_FORM">
7
8
 
8
9
  <!-- HEADER -->
9
- <div class="c21-header" [ngStyle]="{'color': g.themeForegroundColor, 'background-image': g.colorGradient180 }" >
10
+ <div class="c21-header" [ngStyle]="{'color': g.themeForegroundColor, 'background-image': g.colorGradient180 }">
10
11
  <div class="c21-header-container">
11
12
 
12
- <!-- ICON CLOSE CHAT -->
13
+ <!-- ICON CLOSE PANEL -->
13
14
  <div class="c21-header-button">
14
15
  <div class="c21-close-button c21-small">
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">
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">
17
21
  <path fill="none" d="M0 0h24v24H0V0z"/>
18
22
  <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"/>
19
23
  </svg>
@@ -38,7 +42,7 @@ onFocus="document.querySelector('[start-focus-chat21-prechat-form]').focus()">
38
42
  <div class="c21-body-container">
39
43
 
40
44
  <div class="c21-body-header">
41
- <div start-focus-chat21-prechat-form tabindex="1410" class="c21-form-message-field">{{g.LABEL_COMPLETE_FORM}}</div>
45
+ <h2 start-focus-chat21-prechat-form class="c21-form-message-field">{{g.LABEL_COMPLETE_FORM}}</h2>
42
46
  </div>
43
47
 
44
48
  <div class="c21-body-content">
@@ -88,7 +92,6 @@ onFocus="document.querySelector('[start-focus-chat21-prechat-form]').focus()">
88
92
  </div>
89
93
 
90
94
  </div>
91
- <span tabindex="1499" onFocus="document.querySelector('[start-focus-chat21-prechat-form]').focus()"></span>
92
95
 
93
96
 
94
97
  <!-- <label *ngIf="g.privacyField" class="privacy-field">
@@ -1,30 +1,122 @@
1
- import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2
- import { FormsModule, ReactiveFormsModule } from '@angular/forms';
1
+ import { A11yModule } from '@angular/cdk/a11y';
2
+ import { Component, EventEmitter, Input, Output } from '@angular/core';
3
+ import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing';
4
+ import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
3
5
 
4
- import { PrechatFormComponent } from './prechat-form.component';
5
- import { AppStorageService } from 'src/chat21-core/providers/abstract/app-storage.service';
6
6
  import { Globals } from 'src/app/utils/globals';
7
+ import { AppStorageService } from 'src/chat21-core/providers/abstract/app-storage.service';
8
+ import { CustomLogger } from 'src/chat21-core/providers/logger/customLogger';
9
+ import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
10
+
11
+ import { PrechatFormComponent } from './prechat-form.component';
12
+
13
+ @Component({
14
+ selector: 'chat-form-builder',
15
+ template: '',
16
+ })
17
+ class ChatFormBuilderStubComponent {
18
+ @Input() formArray: unknown;
19
+ @Input() isOpenPrechatForm: boolean;
20
+ @Input() stylesMap: Map<string, string> | undefined;
21
+ @Output() onSubmitForm = new EventEmitter<Record<string, string>>();
22
+ @Output() onErrorRenderForm = new EventEmitter<void>();
23
+ }
7
24
 
8
25
  describe('PrechatFormComponent', () => {
9
26
  let component: PrechatFormComponent;
10
27
  let fixture: ComponentFixture<PrechatFormComponent>;
28
+ let g: Globals;
29
+ let appStorage: jasmine.SpyObj<AppStorageService>;
30
+ const ngxlogger = jasmine.createSpyObj('NGXLogger', ['log', 'trace', 'debug', 'warn', 'error']);
31
+ const customLogger = new CustomLogger(ngxlogger);
32
+
33
+ beforeEach(waitForAsync(() => {
34
+ appStorage = jasmine.createSpyObj('AppStorageService', [
35
+ 'initialize',
36
+ 'getItem',
37
+ 'setItem',
38
+ 'getItemWithoutProjectID',
39
+ 'setItemWithoutProjectID',
40
+ 'removeItem',
41
+ 'clear',
42
+ ]);
11
43
 
12
- beforeEach(async(() => {
13
44
  TestBed.configureTestingModule({
14
- declarations: [ PrechatFormComponent ],
15
- imports: [ FormsModule, ReactiveFormsModule ],
16
- providers: [ Globals, AppStorageService]
17
- })
18
- .compileComponents();
45
+ declarations: [PrechatFormComponent, ChatFormBuilderStubComponent],
46
+ imports: [A11yModule, ReactiveFormsModule],
47
+ providers: [
48
+ Globals,
49
+ FormBuilder,
50
+ { provide: AppStorageService, useValue: appStorage },
51
+ ],
52
+ }).compileComponents();
19
53
  }));
20
54
 
21
55
  beforeEach(() => {
56
+ LoggerInstance.setInstance(customLogger);
22
57
  fixture = TestBed.createComponent(PrechatFormComponent);
23
58
  component = fixture.componentInstance;
59
+ g = TestBed.inject(Globals);
60
+ g.LABEL_COMPLETE_FORM = 'Complete form';
61
+ g.attributes = { foo: 'bar' };
24
62
  fixture.detectChanges();
25
63
  });
26
64
 
27
65
  it('should create', () => {
28
66
  expect(component).toBeTruthy();
29
67
  });
68
+
69
+ it('onSubmitForm should persist attributes, merge preChatForm, and emit onCloseForm', () => {
70
+ spyOn(component.onCloseForm, 'emit');
71
+ const form = { userFullname: 'Jane Doe', userEmail: 'jane@example.com' };
72
+ component.onSubmitForm(form);
73
+ expect(g.attributes.userFullname).toBe('Jane Doe');
74
+ expect(g.attributes.userEmail).toBe('jane@example.com');
75
+ expect(g.attributes.preChatForm).toEqual(form);
76
+ expect(appStorage.setItem).toHaveBeenCalledWith('attributes', jasmine.any(String));
77
+ expect(component.onCloseForm.emit).toHaveBeenCalled();
78
+ });
79
+
80
+ it('onSubmitForm should not emit when g.attributes is missing', () => {
81
+ g.attributes = undefined as any;
82
+ spyOn(component.onCloseForm, 'emit');
83
+ component.onSubmitForm({ userFullname: 'X' });
84
+ expect(component.onCloseForm.emit).not.toHaveBeenCalled();
85
+ expect(appStorage.setItem).not.toHaveBeenCalled();
86
+ });
87
+
88
+ it('returnClosePage should emit onClosePage', () => {
89
+ spyOn(component.onClosePage, 'emit');
90
+ component.returnClosePage();
91
+ expect(component.onClosePage.emit).toHaveBeenCalled();
92
+ });
93
+
94
+ it('onEscape should stop propagation, prevent default, and close page', () => {
95
+ spyOn(component.onClosePage, 'emit');
96
+ const ev = new KeyboardEvent('keydown', { key: 'Escape' });
97
+ spyOn(ev, 'preventDefault');
98
+ spyOn(ev, 'stopPropagation');
99
+ component.onEscape(ev);
100
+ expect(ev.preventDefault).toHaveBeenCalled();
101
+ expect(ev.stopPropagation).toHaveBeenCalled();
102
+ expect(component.onClosePage.emit).toHaveBeenCalled();
103
+ });
104
+
105
+ it('onErrorRenderForm should restore default preChatFormJson via Globals', () => {
106
+ spyOn(g, 'setParameter');
107
+ component.onErrorRenderForm();
108
+ expect(g.setParameter).toHaveBeenCalledWith('preChatFormJson', jasmine.any(Array));
109
+ const arg = (g.setParameter as jasmine.Spy).calls.mostRecent().args[1] as any[];
110
+ expect(arg[0].name).toBe('userFullname');
111
+ expect(arg[1].name).toBe('userEmail');
112
+ });
113
+
114
+ it('should request focus on dialog root after view init (async)', fakeAsync(() => {
115
+ const fixtureInner = TestBed.createComponent(PrechatFormComponent);
116
+ const el = fixtureInner.nativeElement.querySelector('#chat21-prechat-form') as HTMLElement;
117
+ spyOn(el, 'focus');
118
+ fixtureInner.detectChanges();
119
+ tick(1000);
120
+ expect(el.focus).toHaveBeenCalled();
121
+ }));
30
122
  });
@@ -1,4 +1,4 @@
1
- import { AfterViewInit, Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
1
+ import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, Input, OnInit, Output, ViewChild } from '@angular/core';
2
2
  import { FormBuilder, FormGroup } from '@angular/forms';
3
3
  import { Globals } from 'src/app/utils/globals';
4
4
  import { AppStorageService } from 'src/chat21-core/providers/abstract/app-storage.service';
@@ -120,6 +120,13 @@ export class PrechatFormComponent implements OnInit, AfterViewInit {
120
120
  this.onClosePage.emit();
121
121
  }
122
122
 
123
+ @HostListener('keydown.escape', ['$event'])
124
+ onEscape(event: KeyboardEvent){
125
+ event.preventDefault();
126
+ event.stopPropagation();
127
+ this.returnClosePage();
128
+ }
129
+
123
130
  onSubmitForm(form: {}){
124
131
  this.logger.debug('[PRE-CHAT-FORM] onSubmitForm:', form)
125
132
  if(this.g.attributes){
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Shared mock for pre-chat dynamic form (unit / integration-style tests).
3
+ * Mirrors a typical `g.preChatFormJson` payload with i18n labels and email regex.
4
+ */
5
+ export const PRECHAT_FORM_JSON_MOCK = [
6
+ {
7
+ name: 'userFullname',
8
+ type: 'text',
9
+ mandatory: true,
10
+ label: {
11
+ en: 'User fullname',
12
+ it: 'Nome utente',
13
+ },
14
+ },
15
+ {
16
+ name: 'userEmail',
17
+ type: 'text',
18
+ mandatory: true,
19
+ regex:
20
+ "/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$/",
21
+ label: {
22
+ en: 'Email',
23
+ it: 'Indirizzo email',
24
+ },
25
+ errorLabel: {
26
+ en: 'Invalid email address',
27
+ it: 'Indirizzo email non valido',
28
+ },
29
+ },
30
+ ];
31
+
32
+ /** Deep clone so `setTranslations` / `buildFormGroup` can mutate safely in tests. */
33
+ export function clonePrechatFormJsonMock(): any[] {
34
+ return JSON.parse(JSON.stringify(PRECHAT_FORM_JSON_MOCK));
35
+ }
@@ -1,6 +1,7 @@
1
1
 
2
- <!-- tabindex="2000"-->
3
- <div #homeComponent id='chat21-home-component' tabindex="2002" onFocus="document.querySelector('[aflistconv]').focus()">
2
+ <div #homeComponent id='chat21-home-component'
3
+ role="region"
4
+ [attr.aria-label]="g?.welcomeTitle || g?.project?.widgetTitle || 'Chat'">
4
5
 
5
6
  <!-- HEADER -->
6
7
  <div class="c21-header" (click)="hideMenuOptions()" [ngStyle]="{'color': g.themeForegroundColor, 'background-image': g.colorGradient }" >
@@ -15,50 +16,59 @@
15
16
 
16
17
  <!-- ICON MAXIMIZE -->
17
18
  <div *ngIf="size === 'min' && !fullscreenMode && !g?.isMobile" class="c21-size-button">
18
- <button [attr.disabled]="(isButtonsDisabled)?true:null" tabindex="-1" class="c21-close-button-body" [ngStyle]="{'display': (hideHeaderConversationOptionsMenu)?'none':'flex'}" (click)="onChangeSize('max')" >
19
- <svg role="img" aria-labelledby="altIconTitle" [ngStyle]="{'fill': stylesMap?.get('foregroundColor') }" xmlns="http://www.w3.org/2000/svg" transform="matrix(-1,0,0,1,0,0)"
19
+ <button [attr.disabled]="(isButtonsDisabled)?true:null"
20
+ type="button"
21
+ class="c21-close-button-body"
22
+ [attr.aria-label]="translationMap?.get('MAXIMIZE')"
23
+ [ngStyle]="{'display': (hideHeaderConversationOptionsMenu)?'none':'flex'}"
24
+ (click)="onChangeSize('max')">
25
+ <svg aria-hidden="true" focusable="false" [ngStyle]="{'fill': stylesMap?.get('foregroundColor') }" xmlns="http://www.w3.org/2000/svg" transform="matrix(-1,0,0,1,0,0)"
20
26
  width="17" height="17" viewBox="0 0 17 17">
21
27
  <path d="M6.49001 8.30999L3.69 11.11V9.40999C3.69 8.93999 3.31 8.55999 2.84 8.55999C2.37 8.55999 1.99001 8.93999 1.99001 9.40999V14.01H6.59C7.06 14.01 7.44 13.63 7.44 13.16C7.44 12.69 7.06 12.31 6.59 12.31H4.89L7.69 9.50999L6.49001 8.30999ZM9.41 1.98999C8.94 1.98999 8.56001 2.36999 8.56001 2.83999C8.56001 3.30999 8.94 3.68999 9.41 3.68999H11.11L8.31001 6.48999L9.51 7.68999L12.31 4.88999V6.58999C12.31 7.05999 12.69 7.43999 13.16 7.43999C13.63 7.43999 14.01 7.05999 14.01 6.58999V1.98999H9.41Z"></path>
22
- <title id="altIconTitle">{{ translationMap?.get('MAXIMIZE') }}</title>
23
28
  </svg>
24
29
  </button>
25
30
  </div>
26
31
 
27
32
  <!-- ICON MINIMIZE -->
28
33
  <div *ngIf="size==='top' && !fullscreenMode && !g?.isMobile" class="c21-size-button">
29
- <button [attr.disabled]="(isButtonsDisabled)?true:null" tabindex="-1" class="c21-close-button-body" [ngStyle]="{'display': (hideHeaderConversationOptionsMenu)?'none':'flex'}" (click)="onChangeSize('min')" >
30
- <svg role="img" aria-labelledby="altIconTitle" class="icon-menu" [ngStyle]="{'fill': stylesMap?.get('foregroundColor')}" xmlns="http://www.w3.org/2000/svg" transform="matrix(-1,0,0,1,0,0)"
34
+ <button [attr.disabled]="(isButtonsDisabled)?true:null"
35
+ type="button"
36
+ class="c21-close-button-body"
37
+ [attr.aria-label]="translationMap?.get('MINIMIZE')"
38
+ [ngStyle]="{'display': (hideHeaderConversationOptionsMenu)?'none':'flex'}"
39
+ (click)="onChangeSize('min')">
40
+ <svg aria-hidden="true" focusable="false" class="icon-menu" [ngStyle]="{'fill': stylesMap?.get('foregroundColor')}" xmlns="http://www.w3.org/2000/svg" transform="matrix(-1,0,0,1,0,0)"
31
41
  width="17" height="17" viewBox="0 0 17 17">
32
42
  <path d="M13.59 5.31h-1.7l3.3-3.3-1.2-1.2-3.3 3.3v-1.7a.85.85 0 1 0-1.7 0v4.6h4.6a.85.85 0 1 0 0-1.7M1.57 9.84c0 .47.38.85.85.85h1.7l-3.3 3.3 1.2 1.2 3.3-3.3v1.7a.85.85 0 1 0 1.7 0v-4.6h-4.6a.85.85 0 0 0-.85.85"></path>
33
- <title id="altIconTitle">{{ translationMap?.get('MINIMIZE') }}</title>
34
43
  </svg>
35
44
  </button>
36
45
  </div>
37
46
 
38
47
  <!-- ICON TOP -->
39
48
  <div *ngIf="size==='max' && !fullscreenMode && !g?.isMobile" class="c21-size-button">
40
- <button [attr.disabled]="(isButtonsDisabled)?true:null" tabindex="-1" class="c21-close-button-body" [ngStyle]="{'display': (hideHeaderConversationOptionsMenu)?'none':'flex'}" (click)="onChangeSize('top')" >
41
- <svg role="img" aria-labelledby="altIconTitle" [ngStyle]="{'fill': stylesMap?.get('foregroundColor')}" xmlns="http://www.w3.org/2000/svg" transform="matrix(-1,0,0,1,0,0)"
49
+ <button [attr.disabled]="(isButtonsDisabled)?true:null"
50
+ type="button"
51
+ class="c21-close-button-body"
52
+ [attr.aria-label]="translationMap?.get('CENTER')"
53
+ [ngStyle]="{'display': (hideHeaderConversationOptionsMenu)?'none':'flex'}"
54
+ (click)="onChangeSize('top')">
55
+ <svg aria-hidden="true" focusable="false" [ngStyle]="{'fill': stylesMap?.get('foregroundColor')}" xmlns="http://www.w3.org/2000/svg" transform="matrix(-1,0,0,1,0,0)"
42
56
  width="17" height="17" viewBox="0 0 17 17">
43
57
  <path d="M3.7,7.6L2.5,6.4C2.2,6,2.2,5.5,2.5,5.2s0.9-0.4,1.2-0.1L7,8.4l-3.3,3.3c-0.4,0.4-0.8,0.3-1.1,0s-0.4-0.9-0.1-1.2l1.2-1.2 H1.5l0-1.7H3.7z"/>
44
58
  <path d="M13.3,7.6l1.2-1.2c0.4-0.4,0.3-0.8,0-1.1s-0.9-0.4-1.2-0.1L10,8.4l3.3,3.3c0.4,0.4,0.8,0.3,1.1,0s0.4-0.9,0.1-1.2l-1.2-1.2 h2.2l0-1.7H13.3z"/>
45
59
  <path d="M8.5,14.5L8.5,14.5c-0.5,0-0.9-0.4-0.9-0.8V3.3c0-0.5,0.4-0.8,0.8-0.8h0c0.5,0,0.8,0.4,0.8,0.8v10.4 C9.4,14.2,9,14.5,8.5,14.5z"/>
46
- <title id="altIconTitle">{{ translationMap?.get('CENTER') }}</title>
47
60
  </svg>
48
61
  </button>
49
62
  </div>
50
63
 
51
64
 
52
65
  <div class="c21-close-button">
53
- <button tabindex="2001" aria-label=" chiudi chat" class="c21-close-button-body" (click)="f21_close()">
54
- <!-- <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">
55
- <path fill="none" d="M0 0h24v24H0V0z"/>
56
- <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"/>
57
- <title id="altIconTitle">{{translationMapHeader?.get('BUTTON_CLOSE_TO_ICON')}}</title>
58
- </svg> -->
59
- <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">
66
+ <button type="button"
67
+ class="c21-close-button-body"
68
+ [attr.aria-label]="translationMapHeader?.get('BUTTON_CLOSE_TO_ICON')"
69
+ (click)="f21_close()">
70
+ <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">
60
71
  <path d="M19.35,9.21c.2-.2.2-.51,0-.71l-.71-.71c-.2-.2-.51-.2-.71,0l-5.94,5.94-5.94-5.94c-.2-.2-.51-.2-.71,0l-.71.71c-.2.2-.2.51,0,.71l7,7c.2.2.51.2.71,0l7-7Z"/>
61
- <title id="altIconTitle">{{translationMapHeader?.get('BUTTON_CLOSE_TO_ICON')}}</title>
62
72
  </svg>
63
73
  </button>
64
74
  </div>
@@ -73,8 +83,8 @@
73
83
 
74
84
  <!-- MESSAGE HEADER -->
75
85
  <div class="c21-text fade-in-bottom">
76
- <div class="c21-text-welcome">{{g?.welcomeTitle}}</div>
77
- <div *ngIf="g.welcomeMsg" class="c21-text-intro">{{g.welcomeMsg}}</div>
86
+ <h1 class="c21-text-welcome">{{g?.welcomeTitle}}</h1>
87
+ <p *ngIf="g.welcomeMsg" class="c21-text-intro">{{g.welcomeMsg}}</p>
78
88
  </div>
79
89
  </div>
80
90
  </div>
@@ -114,9 +124,9 @@
114
124
  <div class="c21-footer-content fade-in-bottom-footer" *ngIf="g?.whatsappNumber || g?.telegramUsername || g?.messangerPageTitle">
115
125
  <div class="label-item">{{translationMapFooter?.get('SWITCH_TO')}}</div>
116
126
  <div id="c21-whatsapp" *ngIf="g?.whatsappNumber && g?.whatsappNumber.trim() !== ''">
117
- <button tabindex="1040" aflistconv #aflistconv class="c21-button-primary" (click)="openConversationOnPlatform('whatsapp')">
127
+ <button type="button" aflistconv #aflistconv class="c21-button-primary" aria-label="WhatsApp" (click)="openConversationOnPlatform('whatsapp')">
118
128
  <span class="v-align-center">
119
- <svg aria-labelledby="altIconTitle" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="28" height="28" viewBox="0 0 1219.547 1225.016">
129
+ <svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="28" height="28" viewBox="0 0 1219.547 1225.016">
120
130
  <path fill="#E0E0E0" d="M1041.858 178.02C927.206 63.289 774.753.07 612.325 0 277.617 0 5.232 272.298 5.098 606.991c-.039 106.986 27.915 211.42 81.048 303.476L0 1225.016l321.898-84.406c88.689 48.368 188.547 73.855 290.166 73.896h.258.003c334.654 0 607.08-272.346 607.222-607.023.056-162.208-63.052-314.724-177.689-429.463zm-429.533 933.963h-.197c-90.578-.048-179.402-24.366-256.878-70.339l-18.438-10.93-191.021 50.083 51-186.176-12.013-19.087c-50.525-80.336-77.198-173.175-77.16-268.504.111-278.186 226.507-504.503 504.898-504.503 134.812.056 261.519 52.604 356.814 147.965 95.289 95.36 147.728 222.128 147.688 356.948-.118 278.195-226.522 504.543-504.693 504.543z"/>
121
131
  <linearGradient id="a" gradientUnits="userSpaceOnUse" x1="609.77" y1="1190.114" x2="609.77" y2="21.084">
122
132
  <stop offset="0" stop-color="#20b038"/>
@@ -125,7 +135,6 @@
125
135
  <image overflow="visible" opacity=".08" width="20" height="20" xlink:href="FCC0802E2AF8A915.png" transform="translate(270.984 291.372)"/>
126
136
  <path fill-rule="evenodd" clip-rule="evenodd" fill="#FFF" d="M462.273 349.294c-11.234-24.977-23.062-25.477-33.75-25.914-8.742-.375-18.75-.352-28.742-.352-10 0-26.25 3.758-39.992 18.766-13.75 15.008-52.5 51.289-52.5 125.078 0 73.797 53.75 145.102 61.242 155.117 7.5 10 103.758 166.266 256.203 226.383 126.695 49.961 152.477 40.023 179.977 37.523s88.734-36.273 101.234-71.297c12.5-35.016 12.5-65.031 8.75-71.305-3.75-6.25-13.75-10-28.75-17.5s-88.734-43.789-102.484-48.789-23.75-7.5-33.75 7.516c-10 15-38.727 48.773-47.477 58.773-8.75 10.023-17.5 11.273-32.5 3.773-15-7.523-63.305-23.344-120.609-74.438-44.586-39.75-74.688-88.844-83.438-103.859-8.75-15-.938-23.125 6.586-30.602 6.734-6.719 15-17.508 22.5-26.266 7.484-8.758 9.984-15.008 14.984-25.008 5-10.016 2.5-18.773-1.25-26.273s-32.898-81.67-46.234-111.326z"/>
127
137
  <path fill="#FFF" d="M1036.898 176.091C923.562 62.677 772.859.185 612.297.114 281.43.114 12.172 269.286 12.039 600.137 12 705.896 39.633 809.13 92.156 900.13L7 1211.067l318.203-83.438c87.672 47.812 186.383 73.008 286.836 73.047h.255.003c330.812 0 600.109-269.219 600.25-600.055.055-160.343-62.328-311.108-175.649-424.53zm-424.601 923.242h-.195c-89.539-.047-177.344-24.086-253.93-69.531l-18.227-10.805-188.828 49.508 50.414-184.039-11.875-18.867c-49.945-79.414-76.312-171.188-76.273-265.422.109-274.992 223.906-498.711 499.102-498.711 133.266.055 258.516 52 352.719 146.266 94.195 94.266 146.031 219.578 145.992 352.852-.118 274.999-223.923 498.749-498.899 498.749z"/>
128
- <title id="altIconTitle">Whatsapp</title>
129
138
  </svg>
130
139
  <!-- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="28" height="28" focusable="false" viewBox="0 0 448 512" xml:space="preserve">
131
140
  <path stroke="#5f6368" stroke-width="8px" fill="#5f6368" d="M380.9,97.1C339,55.1,283.2,32,223.9,32c-122.4,0-222,99.6-222,222c0,39.1,10.2,77.3,29.6,111L0,480l117.7-30.9 c32.4,17.7,68.9,27,106.1,27h0.1c122.3,0,224.1-99.6,224.1-222C448,194.8,422.8,139.1,380.9,97.1z M223.9,438.7 c-33.2,0-65.7-8.9-94-25.7l-6.7-4l-69.8,18.3L72,359.2l-4.4-7c-18.5-29.4-28.2-63.3-28.2-98.2c0-101.7,82.8-184.5,184.6-184.5 c49.3,0,95.6,19.2,130.4,54.1s56.2,81.2,56.1,130.5C410.5,355.9,325.6,438.7,223.9,438.7z M325.1,300.5c-5.5-2.8-32.8-16.2-37.9-18 c-5.1-1.9-8.8-2.8-12.5,2.8c-3.7,5.6-14.3,18-17.6,21.8c-3.2,3.7-6.5,4.2-12,1.4c-32.6-16.3-54-29.1-75.5-66 c-5.7-9.8,5.7-9.1,16.3-30.3c1.8-3.7,0.9-6.9-0.5-9.7s-12.5-30.1-17.1-41.2c-4.5-10.8-9.1-9.3-12.5-9.5c-3.2-0.2-6.9-0.2-10.6-0.2 c-3.7,0-9.7,1.4-14.8,6.9c-5.1,5.6-19.4,19-19.4,46.3s19.9,53.7,22.6,57.4c2.8,3.7,39.1,59.7,94.8,83.8c35.2,15.2,49,16.5,66.6,13.9 c10.7-1.6,32.8-13.4,37.4-26.4s4.6-24.1,3.2-26.4C334.3,304.6,330.6,303.2,325.1,300.5z"/>
@@ -135,9 +144,9 @@
135
144
  </button>
136
145
  </div>
137
146
  <div id="c21-telegram" *ngIf="g?.telegramUsername && g?.telegramUsername.trim() !== ''">
138
- <button tabindex="1040" aflistconv #aflistconv class="c21-button-primary" (click)="openConversationOnPlatform('telegram')">
147
+ <button type="button" aflistconv #aflistconv class="c21-button-primary" aria-label="Telegram" (click)="openConversationOnPlatform('telegram')">
139
148
  <span class="v-align-center">
140
- <svg aria-labelledby="altIconTitle" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="26" height="26" viewBox="0 0 368 368" style="enable-background:new 0 0 368 368;" xml:space="preserve">
149
+ <svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="26" height="26" viewBox="0 0 368 368" style="enable-background:new 0 0 368 368;" xml:space="preserve">
141
150
  <g id="XMLID_1_">
142
151
  <linearGradient id="XMLID_7_" gradientUnits="userSpaceOnUse" x1="-66.831" y1="-68.1757" x2="-65.3102" y2="-68.1757" gradientTransform="matrix(-60 139.992 -139.992 -60 -13316.416 5324.4634)">
143
152
  <stop offset="0" style="stop-color:#37AEE2"/>
@@ -152,7 +161,6 @@
152
161
  </linearGradient>
153
162
  <path id="XMLID_6_" fill="url(#XMLID_8_)" d="M153.6,221.1l73.5,54.3c8.4,4.6,14.4,2.2,16.5-7.8l29.9-141.1c3.1-12.3-4.7-17.9-12.7-14.2 L85.2,180.2c-12,4.8-11.9,11.5-2.2,14.5l45.1,14.1l104.4-65.9c4.9-3,9.5-1.4,5.7,1.9"/>
154
163
  </g>
155
- <title id="altIconTitle">Telegram</title>
156
164
  </svg>
157
165
  <!-- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="26" height="26" viewBox="0 0 448 512" xml:space="preserve">
158
166
  <path fill="#5f6368" d="M446.7,98.6l-67.6,318.8c-5.1,22.5-18.4,28.1-37.3,17.5l-103-75.9l-49.7,47.8c-5.5,5.5-10.1,10.1-20.7,10.1 l7.4-104.9l190.9-172.5c8.3-7.4-1.8-11.5-12.9-4.1L117.8,284L16.2,252.2c-22.1-6.9-22.5-22.1,4.6-32.7L418.2,66.4 C436.6,59.5,452.7,70.5,446.7,98.6z"/>
@@ -162,9 +170,9 @@
162
170
  </button>
163
171
  </div>
164
172
  <div id="c21-messanger" *ngIf="g?.messangerPageTitle && g?.messangerPageTitle.trim() !== ''">
165
- <button tabindex="1040" aflistconv #aflistconv class="c21-button-primary" (click)="openConversationOnPlatform('messanger')">
173
+ <button type="button" aflistconv #aflistconv class="c21-button-primary" aria-label="Facebook Messenger" (click)="openConversationOnPlatform('messanger')">
166
174
  <span class="v-align-center">
167
- <svg aria-labelledby="altIconTitle" xmlns="http://www.w3.org/2000/svg" width="26" height="26" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 800 800">
175
+ <svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" width="26" height="26" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 800 800">
168
176
  <radialGradient id="Gradient" cx="101.925" cy="809.0079" r="1.0896" gradientTransform="matrix(800 0 0 -799.9985 -81386 648000.75)" gradientUnits="userSpaceOnUse">
169
177
  <stop offset="0" style="stop-color:#0099FF"/>
170
178
  <stop offset="0.6098" style="stop-color:#A033FF"/>
@@ -173,7 +181,6 @@
173
181
  </radialGradient>
174
182
  <path fill="url(#Gradient)" d="M400,0C174.7,0,0,165.1,0,388c0,116.6,47.8,217.4,125.6,287c6.5,5.8,10.5,14,10.7,22.8l2.2,71.2 c0.7,22.7,24.1,37.5,44.9,28.3l79.4-35c6.7-3,14.3-3.5,21.4-1.6c36.5,10,75.3,15.4,115.8,15.4c225.3,0,400-165.1,400-388 S625.3,0,400,0z"/>
175
183
  <path fill="#FFFFFF" d="M159.8,501.5l117.5-186.4c18.7-29.7,58.7-37,86.8-16l93.5,70.1c8.6,6.4,20.4,6.4,28.9-0.1 l126.2-95.8c16.8-12.8,38.8,7.4,27.6,25.3L522.7,484.9c-18.7,29.7-58.7,37-86.8,16l-93.5-70.1c-8.6-6.4-20.4-6.4-28.9,0.1 l-126.2,95.8C170.5,539.5,148.5,519.4,159.8,501.5z"/>
176
- <title id="altIconTitle">Facebook Messanger</title>
177
184
  </svg>
178
185
  <!-- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="26" height="26" focusable="false" viewBox="0 0 512 512" xml:space="preserve">
179
186
  <path fill="#5f6368" d="M256.5,8C116.5,8,8,110.3,8,248.6c0,72.3,29.7,134.8,78.1,177.9c8.3,7.5,6.6,11.9,8.1,58.2 c0.4,11,9.6,19.6,20.6,19.2c2.5-0.1,5-0.6,7.3-1.7c52.9-23.3,53.6-25.1,62.6-22.7c153.3,42.2,319.4-55.9,319.4-231 C504,110.3,396.6,8,256.5,8z M405.8,193.1l-73,115.6c-11.1,17.4-34.1,22.6-51.6,11.5c-0.8-0.5-1.6-1-2.3-1.6l-58.1-43.5 c-5.3-4-12.7-4-18,0l-78.4,59.4c-10.5,7.9-24.2-4.6-17.1-15.7l73-115.6c11-17.4,34.1-22.6,51.6-11.6c0.8,0.5,1.6,1.1,2.4,1.6 l58.1,43.5c5.3,4,12.7,4,18,0l78.4-59.4C399.1,169.5,412.8,182,405.8,193.1L405.8,193.1z"/>
@@ -187,7 +194,7 @@
187
194
  <!-- FOOTER -->
188
195
  <div class="c21-footer fade-in-bottom-footer">
189
196
  <div id="c21-powered-by" (mouseover)="hover=true" (mouseleave)="hover=false">
190
- <div tabindex="-1" class="c21-powered-by" [innerHTML]="g.poweredBy" (click)="managePoweredBy($event)"></div>
197
+ <div class="c21-powered-by" [innerHTML]="g.poweredBy" (click)="managePoweredBy($event)"></div>
191
198
  <!-- <div class="build_version">{{g.BUILD_VERSION}}</div> -->
192
199
  </div>
193
200
  <chat-menu-options *ngIf="!hideSettings"