@nyaruka/temba-components 0.108.7 → 0.109.0

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 (118) hide show
  1. package/CHANGELOG.md +19 -2
  2. package/dist/static/svg/index.svg +1 -1
  3. package/dist/temba-components.js +602 -455
  4. package/dist/temba-components.js.map +1 -1
  5. package/out-tsc/src/charcount/CharCount.js +4 -5
  6. package/out-tsc/src/charcount/CharCount.js.map +1 -1
  7. package/out-tsc/src/completion/Completion.js +27 -16
  8. package/out-tsc/src/completion/Completion.js.map +1 -1
  9. package/out-tsc/src/compose/Compose.js +259 -95
  10. package/out-tsc/src/compose/Compose.js.map +1 -1
  11. package/out-tsc/src/contacts/ContactChat.js +18 -16
  12. package/out-tsc/src/contacts/ContactChat.js.map +1 -1
  13. package/out-tsc/src/contacts/ContactTickets.js +1 -1
  14. package/out-tsc/src/contacts/ContactTickets.js.map +1 -1
  15. package/out-tsc/src/interfaces.js.map +1 -1
  16. package/out-tsc/src/list/ShortcutList.js +125 -0
  17. package/out-tsc/src/list/ShortcutList.js.map +1 -0
  18. package/out-tsc/src/list/TembaList.js +8 -5
  19. package/out-tsc/src/list/TembaList.js.map +1 -1
  20. package/out-tsc/src/options/Options.js +46 -35
  21. package/out-tsc/src/options/Options.js.map +1 -1
  22. package/out-tsc/src/select/Select.js +1 -1
  23. package/out-tsc/src/select/Select.js.map +1 -1
  24. package/out-tsc/src/store/Store.js +18 -3
  25. package/out-tsc/src/store/Store.js.map +1 -1
  26. package/out-tsc/src/tabpane/Tab.js +2 -0
  27. package/out-tsc/src/tabpane/Tab.js.map +1 -1
  28. package/out-tsc/src/tabpane/TabPane.js +27 -5
  29. package/out-tsc/src/tabpane/TabPane.js.map +1 -1
  30. package/out-tsc/src/textinput/TextInput.js +7 -2
  31. package/out-tsc/src/textinput/TextInput.js.map +1 -1
  32. package/out-tsc/src/utils/index.js.map +1 -1
  33. package/out-tsc/src/vectoricon/index.js +2 -2
  34. package/out-tsc/src/vectoricon/index.js.map +1 -1
  35. package/out-tsc/temba-modules.js +2 -0
  36. package/out-tsc/temba-modules.js.map +1 -1
  37. package/out-tsc/test/temba-compose.test.js +26 -18
  38. package/out-tsc/test/temba-compose.test.js.map +1 -1
  39. package/out-tsc/test/temba-contact-chat.test.js +27 -18
  40. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  41. package/package.json +1 -1
  42. package/screenshots/truth/compose/attachments-and-send-button.png +0 -0
  43. package/screenshots/truth/compose/attachments-no-send-button.png +0 -0
  44. package/screenshots/truth/compose/attachments-with-all-files-and-click-send.png +0 -0
  45. package/screenshots/truth/compose/attachments-with-all-files.png +0 -0
  46. package/screenshots/truth/compose/attachments-with-failure-files.png +0 -0
  47. package/screenshots/truth/compose/attachments-with-success-files-and-click-send.png +0 -0
  48. package/screenshots/truth/compose/attachments-with-success-files.png +0 -0
  49. package/screenshots/truth/compose/chatbox-attachments-counter-and-send-button.png +0 -0
  50. package/screenshots/truth/compose/chatbox-attachments-counter-no-send-button.png +0 -0
  51. package/screenshots/truth/compose/chatbox-attachments-no-counter-and-send-button.png +0 -0
  52. package/screenshots/truth/compose/chatbox-attachments-no-counter-no-send-button.png +0 -0
  53. package/screenshots/truth/compose/chatbox-counter-and-send-button.png +0 -0
  54. package/screenshots/truth/compose/chatbox-counter-no-send-button.png +0 -0
  55. package/screenshots/truth/compose/chatbox-no-counter-and-send-button.png +0 -0
  56. package/screenshots/truth/compose/chatbox-no-counter-no-send-button.png +0 -0
  57. package/screenshots/truth/compose/chatbox-no-text-attachments-with-all-files-and-click-send.png +0 -0
  58. package/screenshots/truth/compose/chatbox-no-text-attachments-with-all-files.png +0 -0
  59. package/screenshots/truth/compose/chatbox-no-text-attachments-with-failure-files.png +0 -0
  60. package/screenshots/truth/compose/chatbox-no-text-attachments-with-success-files-and-click-send.png +0 -0
  61. package/screenshots/truth/compose/chatbox-no-text-attachments-with-success-files.png +0 -0
  62. package/screenshots/truth/compose/chatbox-with-text-and-click-send.png +0 -0
  63. package/screenshots/truth/compose/chatbox-with-text-and-hit-enter.png +0 -0
  64. package/screenshots/truth/compose/chatbox-with-text-and-spaces.png +0 -0
  65. package/screenshots/truth/compose/chatbox-with-text-and-url.png +0 -0
  66. package/screenshots/truth/compose/chatbox-with-text-attachments-no-files-and-click-send.png +0 -0
  67. package/screenshots/truth/compose/chatbox-with-text-attachments-no-files-and-hit-enter.png +0 -0
  68. package/screenshots/truth/compose/chatbox-with-text-attachments-no-files.png +0 -0
  69. package/screenshots/truth/compose/chatbox-with-text-attachments-with-all-files-and-click-send.png +0 -0
  70. package/screenshots/truth/compose/chatbox-with-text-attachments-with-all-files-and-hit-enter.png +0 -0
  71. package/screenshots/truth/compose/chatbox-with-text-attachments-with-all-files.png +0 -0
  72. package/screenshots/truth/compose/chatbox-with-text-attachments-with-failure-files.png +0 -0
  73. package/screenshots/truth/compose/chatbox-with-text-attachments-with-success-files-and-click-send.png +0 -0
  74. package/screenshots/truth/compose/chatbox-with-text-attachments-with-success-files-and-hit-enter.png +0 -0
  75. package/screenshots/truth/compose/chatbox-with-text-attachments-with-success-files.png +0 -0
  76. package/screenshots/truth/compose/chatbox-with-text-no-spaces.png +0 -0
  77. package/screenshots/truth/compose/chatbox-with-text.png +0 -0
  78. package/screenshots/truth/contacts/compose-attachments-no-text-failure.png +0 -0
  79. package/screenshots/truth/contacts/compose-attachments-no-text-success.png +0 -0
  80. package/screenshots/truth/contacts/compose-text-and-attachments-failure-attachments.png +0 -0
  81. package/screenshots/truth/contacts/compose-text-and-attachments-failure-generic.png +0 -0
  82. package/screenshots/truth/contacts/compose-text-and-attachments-failure-text-and-attachments.png +0 -0
  83. package/screenshots/truth/contacts/compose-text-and-attachments-failure-text.png +0 -0
  84. package/screenshots/truth/contacts/compose-text-and-attachments-success.png +0 -0
  85. package/screenshots/truth/contacts/compose-text-no-attachments-failure.png +0 -0
  86. package/screenshots/truth/contacts/compose-text-no-attachments-success.png +0 -0
  87. package/screenshots/truth/contacts/contact-active-default.png +0 -0
  88. package/screenshots/truth/contacts/contact-active-show-chatbox.png +0 -0
  89. package/screenshots/truth/counter/summary.png +0 -0
  90. package/screenshots/truth/counter/text.png +0 -0
  91. package/screenshots/truth/counter/unicode-variables.png +0 -0
  92. package/screenshots/truth/counter/unicode.png +0 -0
  93. package/screenshots/truth/counter/variable.png +0 -0
  94. package/src/charcount/CharCount.ts +4 -5
  95. package/src/completion/Completion.ts +33 -19
  96. package/src/compose/Compose.ts +289 -96
  97. package/src/contacts/ContactChat.ts +18 -16
  98. package/src/contacts/ContactTickets.ts +1 -1
  99. package/src/interfaces.ts +7 -0
  100. package/src/list/ShortcutList.ts +137 -0
  101. package/src/list/TembaList.ts +9 -6
  102. package/src/options/Options.ts +53 -44
  103. package/src/select/Select.ts +1 -1
  104. package/src/store/Store.ts +23 -4
  105. package/src/tabpane/Tab.ts +2 -0
  106. package/src/tabpane/TabPane.ts +28 -5
  107. package/src/textinput/TextInput.ts +9 -3
  108. package/src/utils/index.ts +8 -2
  109. package/src/vectoricon/index.ts +2 -2
  110. package/static/svg/index.svg +1 -1
  111. package/static/svg/work/traced/zap-fast.svg +1 -0
  112. package/static/svg/work/used/zap-fast.svg +3 -0
  113. package/temba-modules.ts +2 -0
  114. package/test/temba-compose.test.ts +28 -35
  115. package/test/temba-contact-chat.test.ts +28 -37
  116. package/test-assets/store/shortcuts.json +14 -0
  117. package/static/svg/work/traced/message-dots-circle.svg +0 -1
  118. package/static/svg/work/used/message-dots-circle.svg +0 -3
@@ -269,17 +269,16 @@ export class ContactChat extends ContactStoreElement {
269
269
  min-height: 0;
270
270
  }
271
271
 
272
- .chatbox {
272
+ .compose {
273
273
  background: #fff;
274
274
  display: flex;
275
275
  flex-direction: column;
276
- --textarea-min-height: 1em;
277
- --textarea-height: 1.2em;
276
+ --textarea-min-height: 8em;
277
+ --textarea-height: 0.5em;
278
278
  --widget-box-shadow-focused: none;
279
- }
280
-
281
- .chatbox.full {
282
- border-bottom-right-radius: 0 !important;
279
+ --compose-curvature: 0px;
280
+ border-top: 1px solid #e6e6e6;
281
+ overflow: hidden;
283
282
  }
284
283
 
285
284
  .closed-footer {
@@ -314,8 +313,11 @@ export class ContactChat extends ContactStoreElement {
314
313
  }
315
314
 
316
315
  .border {
317
- border-top: 1px solid #f1f1f1;
318
- margin: 0 1em;
316
+ }
317
+
318
+ temba-compose {
319
+ border-top-right-radius: 0;
320
+ border-top-left-radius: 0;
319
321
  }
320
322
  `;
321
323
  }
@@ -487,10 +489,10 @@ export class ContactChat extends ContactStoreElement {
487
489
  const contactHistory = this.currentContact
488
490
  ? this.getTembaContactHistory()
489
491
  : null;
490
- const chatbox = this.currentContact ? this.getTembaChatbox() : null;
492
+ const tembaCompose = this.currentContact ? this.getTembaCompose() : null;
491
493
 
492
494
  const contactHistoryAndChatbox = html`
493
- <div class="chat-wrapper">${contactHistory} ${chatbox}</div>
495
+ <div class="chat-wrapper">${contactHistory} ${tembaCompose}</div>
494
496
  `;
495
497
  return html`${contactHistoryAndChatbox}`;
496
498
  }
@@ -851,7 +853,7 @@ export class ContactChat extends ContactStoreElement {
851
853
  ></temba-chat>`;
852
854
  }
853
855
 
854
- private getTembaChatbox(): TemplateResult {
856
+ private getTembaCompose(): TemplateResult {
855
857
  if (this.currentTicket) {
856
858
  if (this.currentContact && this.currentContact.status !== 'active') {
857
859
  //no chatbox for archived, blocked, or stopped contacts
@@ -859,7 +861,7 @@ export class ContactChat extends ContactStoreElement {
859
861
  } else {
860
862
  if (!this.currentTicket.closed_on) {
861
863
  //chatbox for active contacts with an open ticket
862
- return this.getChatbox();
864
+ return this.getCompose();
863
865
  } else {
864
866
  return null;
865
867
  }
@@ -871,13 +873,13 @@ export class ContactChat extends ContactStoreElement {
871
873
  return null;
872
874
  } else {
873
875
  //chatbox for active contacts
874
- return this.getChatbox();
876
+ return this.getCompose();
875
877
  }
876
878
  }
877
879
 
878
- private getChatbox(): TemplateResult {
880
+ private getCompose(): TemplateResult {
879
881
  return html`<div class="border"></div>
880
- <div class="chatbox">
882
+ <div class="compose">
881
883
  <temba-compose
882
884
  chatbox
883
885
  attachments
@@ -231,7 +231,7 @@ export class ContactTickets extends EndpointMonitorElement {
231
231
  ): void {
232
232
  super.updated(changes);
233
233
 
234
- if (changes.has('data')) {
234
+ if (changes.has('data') && this.data) {
235
235
  this.fireCustomEvent(CustomEventType.DetailsChanged, {
236
236
  count: this.data.length
237
237
  });
package/src/interfaces.ts CHANGED
@@ -110,6 +110,13 @@ export interface ObjectReference {
110
110
  name: string;
111
111
  }
112
112
 
113
+ export interface Shortcut {
114
+ uuid: string;
115
+ name: string;
116
+ text: string;
117
+ modified_on: string;
118
+ }
119
+
113
120
  export interface ContactField {
114
121
  key: string;
115
122
  label: string;
@@ -0,0 +1,137 @@
1
+ import { css, html, PropertyValueMap, TemplateResult } from 'lit';
2
+ import { property } from 'lit/decorators.js';
3
+ import { Shortcut } from '../interfaces';
4
+ import { StoreMonitorElement } from '../store/StoreMonitorElement';
5
+ import { Options } from '../options/Options';
6
+
7
+ export class ShortcutList extends StoreMonitorElement {
8
+ static get styles() {
9
+ return css`
10
+ temba-options {
11
+ display: block;
12
+ width: 100%;
13
+ flex-grow: 1;
14
+ }
15
+
16
+ .options-empty {
17
+ height: 0;
18
+ overflow: hidden;
19
+ }
20
+
21
+ .no-match {
22
+ margin: 5px 10px;
23
+ padding: 10px;
24
+ }
25
+
26
+ .filter {
27
+ background: #f3f3f3;
28
+ padding: 0.1em 0.3em;
29
+ border-radius: var(--curvature);
30
+ }
31
+ `;
32
+ }
33
+
34
+ @property({ type: String })
35
+ filter: string;
36
+
37
+ @property({ type: Array })
38
+ filteredShortcuts = [];
39
+
40
+ @property({ type: Number })
41
+ cursorIndex = 0;
42
+
43
+ constructor() {
44
+ super();
45
+ }
46
+
47
+ protected firstUpdated(
48
+ changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
49
+ ): void {
50
+ super.firstUpdated(changes);
51
+ }
52
+
53
+ public storeUpdated(): void {
54
+ this.filteredShortcuts = this.store.getShortcuts();
55
+ }
56
+
57
+ public updated(
58
+ changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
59
+ ): void {
60
+ super.updated(changes);
61
+
62
+ if (changes.has('filter')) {
63
+ if (!this.filter) {
64
+ this.filteredShortcuts = this.store.getShortcuts();
65
+ } else {
66
+ this.filteredShortcuts = this.store
67
+ .getShortcuts()
68
+ .filter((shortcut: Shortcut) =>
69
+ shortcut.name.toLowerCase().includes(this.filter.toLowerCase())
70
+ );
71
+ }
72
+ this.cursorIndex = 0;
73
+ }
74
+ }
75
+
76
+ public renderShortcut(shortcut: Shortcut): TemplateResult {
77
+ return html`<div style="display:flex;align-items: center;min-width:0">
78
+ <div
79
+ style="
80
+ overflow: hidden;
81
+ text-overflow: ellipsis;
82
+ width:100px;
83
+ padding-right: 10px;
84
+ white-space: nowrap;"
85
+ >
86
+ ${shortcut.name}
87
+ </div>
88
+ <div
89
+ style="
90
+ font-size: 0.9em;
91
+ color: rgba(0, 0, 0, 0.4);
92
+ flex:1;
93
+ overflow: hidden;
94
+ text-overflow: ellipsis;
95
+
96
+ display: -webkit-box;
97
+ line-height: 16px;
98
+ max-height: 16px;
99
+ -webkit-line-clamp: 1;
100
+ -webkit-box-orient: vertical;
101
+ "
102
+ >
103
+ ${shortcut.text}
104
+ </div>
105
+ </div>`;
106
+ }
107
+
108
+ public getShortcut() {
109
+ const options = this.shadowRoot.querySelector('temba-options') as Options;
110
+ return options.getSelection();
111
+ }
112
+
113
+ public render(): TemplateResult {
114
+ return html`
115
+ ${this.filteredShortcuts.length === 0
116
+ ? html`<div class="no-match">
117
+ ${this.filter
118
+ ? html`No matches for <span class="filter">${this.filter}</span>.`
119
+ : html`No shortcuts yet, create some to quickly include them in
120
+ your messages.`}
121
+ </div>`
122
+ : null}
123
+
124
+ <temba-options
125
+ class="options-${this.filteredShortcuts.length === 0
126
+ ? 'empty'
127
+ : 'full'}"
128
+ block
129
+ cursorHover
130
+ visible
131
+ .renderOption=${this.renderShortcut}
132
+ .cursorIndex=${this.cursorIndex}
133
+ .options=${this.filteredShortcuts}
134
+ ></temba-options>
135
+ `;
136
+ }
137
+ }
@@ -410,9 +410,6 @@ export class TembaList extends RapidElement {
410
410
  }
411
411
  }
412
412
 
413
- // TODO: Not sure why this is needed
414
- // this.requestUpdate('cursorIndex');
415
-
416
413
  if (this.value) {
417
414
  this.setSelection(this.value);
418
415
  this.value = null;
@@ -456,7 +453,12 @@ export class TembaList extends RapidElement {
456
453
  }
457
454
 
458
455
  protected handleSelection(event: CustomEvent) {
459
- const { selected, index } = event.detail;
456
+ let index = event.detail.index;
457
+ const selected = event.detail.selected;
458
+
459
+ if (index === -1) {
460
+ index = 0;
461
+ }
460
462
 
461
463
  this.selected = selected;
462
464
  this.cursorIndex = index;
@@ -470,8 +472,9 @@ export class TembaList extends RapidElement {
470
472
  ${this.renderHeader()}
471
473
  <temba-options
472
474
  style="${this.getListStyle()}"
473
- ?visible=${true}
474
- ?block=${true}
475
+ visible
476
+ block
477
+ cursorSelection
475
478
  ?hideShadow=${this.hideShadow}
476
479
  ?collapsed=${this.collapsed}
477
480
  ?loading=${this.loading}
@@ -111,6 +111,7 @@ export class Options extends RapidElement {
111
111
  margin: 0.3em;
112
112
  cursor: pointer;
113
113
  color: var(--color-text-dark);
114
+ scroll-margin: 5px 0px;
114
115
  }
115
116
 
116
117
  .option * {
@@ -209,6 +210,14 @@ export class Options extends RapidElement {
209
210
  @property({ type: Boolean })
210
211
  block: boolean;
211
212
 
213
+ // if we allow the focus to follow the cursor
214
+ @property({ type: Boolean })
215
+ cursorHover: boolean;
216
+
217
+ // fire selection events when cursor changes
218
+ @property({ type: Boolean })
219
+ cursorSelection: boolean;
220
+
212
221
  @property({ type: Number })
213
222
  scrollPct = 75;
214
223
 
@@ -302,67 +311,42 @@ export class Options extends RapidElement {
302
311
  return focused;
303
312
  }
304
313
 
305
- public updated(changedProperties: Map<string, any>) {
306
- super.updated(changedProperties);
314
+ public updated(changed: Map<string, any>) {
315
+ super.updated(changed);
307
316
 
308
317
  // if our cursor changed, lets make sure our scrollbox is showing it
309
- if (!this.internalFocusDisabled && changedProperties.has('cursorIndex')) {
310
- const focusedOption = this.shadowRoot.querySelector(
311
- `div[data-option-index="${this.cursorIndex}"]`
312
- ) as HTMLDivElement;
313
-
314
- if (focusedOption) {
315
- const scrollBox = this.shadowRoot.querySelector('.options-container');
316
- const scrollBoxRect = scrollBox.getBoundingClientRect();
317
- const scrollBoxHeight = scrollBoxRect.height;
318
- const focusedEleHeight = focusedOption.getBoundingClientRect().height;
319
-
320
- if (
321
- focusedOption.offsetTop + focusedEleHeight >
322
- scrollBox.scrollTop + scrollBoxHeight - 5
323
- ) {
324
- const scrollTo =
325
- focusedOption.offsetTop - scrollBoxHeight + focusedEleHeight + 5;
326
- scrollBox.scrollTop = scrollTo;
327
- } else if (focusedOption.offsetTop < scrollBox.scrollTop) {
328
- const scrollTo = focusedOption.offsetTop - 5;
329
- scrollBox.scrollTop = scrollTo;
330
- }
331
- }
332
-
318
+ if (!this.internalFocusDisabled && changed.has('cursorIndex')) {
333
319
  this.fireCustomEvent(CustomEventType.CursorChanged, {
334
320
  index: this.cursorIndex
335
321
  });
336
322
  }
337
323
 
338
- if (changedProperties.has('visible') && changedProperties.has('options')) {
324
+ if (changed.has('visible') && changed.has('options')) {
339
325
  if (!this.visible && this.options.length == 0) {
340
- this.tempOptions = changedProperties.get('options');
326
+ this.tempOptions = changed.get('options');
341
327
  window.setTimeout(() => {
342
328
  this.tempOptions = [];
343
329
  }, 300);
344
330
  }
345
331
  }
346
332
 
347
- if (changedProperties.has('options')) {
333
+ if (changed.has('options')) {
348
334
  this.calculatePosition();
349
335
 
350
336
  // allow scrolls to trigger again
351
337
  this.triggerScroll = true;
352
338
 
353
- const prevOptions = changedProperties.get('options');
339
+ const prevOptions = changed.get('options');
354
340
  const previousCount = prevOptions ? prevOptions.length : 0;
355
341
  const newCount = this.options ? this.options.length : 0;
356
342
 
357
343
  if (
358
344
  this.cursorIndex === -1 ||
359
345
  newCount < previousCount ||
360
- (previousCount === 0 &&
361
- newCount > 0 &&
362
- !changedProperties.has('cursorIndex'))
346
+ (previousCount === 0 && newCount > 0 && !changed.has('cursorIndex'))
363
347
  ) {
364
348
  if (!this.internalFocusDisabled) {
365
- if (!this.block) {
349
+ if (!this.block || this.cursorIndex === -1) {
366
350
  this.cursorIndex = 0;
367
351
  } else {
368
352
  if (this.cursorIndex >= newCount) {
@@ -370,7 +354,7 @@ export class Options extends RapidElement {
370
354
  }
371
355
  }
372
356
 
373
- if (this.block) {
357
+ if (this.cursorSelection) {
374
358
  this.handleSelection(false);
375
359
  }
376
360
  }
@@ -379,12 +363,14 @@ export class Options extends RapidElement {
379
363
  // if on initial load we don't have enough options to load, trigger a scroll
380
364
  // threshold event in case the page size is smaller than our control height
381
365
  const scrollBox = this.shadowRoot.querySelector('.options');
382
- if (scrollBox.scrollHeight == scrollBox.clientHeight) {
383
- this.fireCustomEvent(CustomEventType.ScrollThreshold);
366
+ if (scrollBox) {
367
+ if (scrollBox.scrollHeight == scrollBox.clientHeight) {
368
+ this.fireCustomEvent(CustomEventType.ScrollThreshold);
369
+ }
384
370
  }
385
371
  }
386
372
 
387
- if (changedProperties.has('visible')) {
373
+ if (changed.has('visible')) {
388
374
  window.setTimeout(() => {
389
375
  this.calculatePosition();
390
376
  }, 100);
@@ -427,6 +413,10 @@ export class Options extends RapidElement {
427
413
  return html` ${option.detail} `;
428
414
  }
429
415
 
416
+ public getSelection() {
417
+ return this.options[this.cursorIndex];
418
+ }
419
+
430
420
  private handleSelection(tabbed = false, index = -1) {
431
421
  if (!this.internalFocusDisabled) {
432
422
  if (index === -1) {
@@ -468,25 +458,42 @@ export class Options extends RapidElement {
468
458
  scrollBox.scrollTop = 0;
469
459
  }
470
460
 
461
+ private ensureOptionVisible() {
462
+ const focusedOption = this.shadowRoot.querySelector(
463
+ `div[data-option-index="${this.cursorIndex}"]`
464
+ ) as HTMLDivElement;
465
+ if (focusedOption) {
466
+ focusedOption.scrollIntoView({
467
+ block: 'nearest',
468
+ inline: 'start'
469
+ });
470
+ }
471
+ }
471
472
  private handleKeyDown(evt: KeyboardEvent) {
472
473
  if (this.internalFocusDisabled || (this.block && !this.isFocused())) {
473
474
  return;
474
475
  }
475
476
 
477
+ if (this.offsetParent === null) {
478
+ return;
479
+ }
480
+
476
481
  if (this.options && this.options.length > 0) {
477
482
  if ((evt.ctrlKey && evt.key === 'n') || evt.key === 'ArrowDown') {
478
483
  this.moveCursor(1);
479
484
  evt.preventDefault();
480
485
  evt.stopPropagation();
481
- if (this.block) {
486
+ if (this.cursorSelection) {
482
487
  this.handleSelection(false);
483
488
  }
489
+ this.ensureOptionVisible();
484
490
  } else if ((evt.ctrlKey && evt.key === 'p') || evt.key === 'ArrowUp') {
485
491
  this.moveCursor(-1);
486
492
  evt.preventDefault();
487
- if (this.block) {
493
+ if (this.cursorSelection) {
488
494
  this.handleSelection(false);
489
495
  }
496
+ this.ensureOptionVisible();
490
497
  } else if (
491
498
  evt.key === 'Enter' ||
492
499
  evt.key === 'Tab' ||
@@ -495,9 +502,7 @@ export class Options extends RapidElement {
495
502
  evt.preventDefault();
496
503
  evt.stopPropagation();
497
504
  this.handleSelection(evt.key === 'Tab');
498
- }
499
-
500
- if (evt.key === 'Escape') {
505
+ } else if (evt.key === 'Escape') {
501
506
  this.fireCustomEvent(CustomEventType.Canceled);
502
507
  }
503
508
  }
@@ -578,7 +583,7 @@ export class Options extends RapidElement {
578
583
  }
579
584
 
580
585
  private handleMouseMove(evt: MouseEvent) {
581
- if (!this.block) {
586
+ if (!this.block || this.cursorHover) {
582
587
  if (Math.abs(evt.movementX) + Math.abs(evt.movementY) > 0) {
583
588
  const index = (evt.currentTarget as HTMLElement).getAttribute(
584
589
  'data-option-index'
@@ -610,6 +615,10 @@ export class Options extends RapidElement {
610
615
  }
611
616
 
612
617
  public render(): TemplateResult {
618
+ if (!this.resolvedRenderOption) {
619
+ return null;
620
+ }
621
+
613
622
  let vertical = this.block ? 0 : this.marginVertical;
614
623
  if (this.poppedTop) {
615
624
  vertical *= -1;
@@ -81,7 +81,7 @@ export class Select extends FormElement {
81
81
  }
82
82
 
83
83
  .wrapper-bg {
84
- background: #f3f3f3;
84
+ background: #fff;
85
85
  box-shadow: inset 0px 0px 4px rgb(0 0 0 / 10%);
86
86
  border-radius: var(--curvature-widget);
87
87
  }
@@ -18,7 +18,8 @@ import {
18
18
  KeyedAssets,
19
19
  CustomEventType,
20
20
  Workspace,
21
- User
21
+ User,
22
+ Shortcut
22
23
  } from '../interfaces';
23
24
  import { RapidElement } from '../RapidElement';
24
25
  import { lru } from 'tiny-lru';
@@ -85,6 +86,9 @@ export class Store extends RapidElement {
85
86
  @property({ type: String, attribute: 'users' })
86
87
  usersEndpoint: string;
87
88
 
89
+ @property({ type: String, attribute: 'shortcuts' })
90
+ shortcutsEndpoint: string;
91
+
88
92
  @property({ type: Object, attribute: false })
89
93
  private schema: CompletionSchema;
90
94
 
@@ -98,6 +102,7 @@ export class Store extends RapidElement {
98
102
 
99
103
  private fields: { [key: string]: ContactField } = {};
100
104
  private groups: { [uuid: string]: ContactGroup } = {};
105
+ private shortcuts: Shortcut[] = [];
101
106
  private languages: any = {};
102
107
  private users: User[];
103
108
  private workspace: Workspace;
@@ -229,6 +234,10 @@ export class Store extends RapidElement {
229
234
  );
230
235
  }
231
236
 
237
+ if (this.shortcutsEndpoint) {
238
+ fetches.push(this.refreshShortcuts());
239
+ }
240
+
232
241
  this.initialHttpComplete = Promise.all(fetches);
233
242
 
234
243
  this.initialHttpComplete.then(() => {
@@ -236,6 +245,10 @@ export class Store extends RapidElement {
236
245
  });
237
246
  }
238
247
 
248
+ public getShortcuts() {
249
+ return this.shortcuts || [];
250
+ }
251
+
239
252
  public getAssignableUsers() {
240
253
  return this.users.filter((user: User) =>
241
254
  ['administrator', 'editor', 'agent'].includes(user.role)
@@ -257,13 +270,19 @@ export class Store extends RapidElement {
257
270
  return 'en';
258
271
  }
259
272
 
260
- public refreshGlobals() {
261
- getAssets(this.globalsEndpoint).then((assets: Asset[]) => {
273
+ public async refreshGlobals() {
274
+ return getAssets(this.globalsEndpoint).then((assets: Asset[]) => {
262
275
  this.keyedAssets['globals'] = assets.map((asset: Asset) => asset.key);
263
276
  });
264
277
  }
265
278
 
266
- public refreshFields() {
279
+ public async refreshShortcuts() {
280
+ return getAssets(this.shortcutsEndpoint).then((shortcuts: Shortcut[]) => {
281
+ this.shortcuts = shortcuts;
282
+ });
283
+ }
284
+
285
+ public async refreshFields() {
267
286
  return getAssets(this.fieldsEndpoint).then((assets: Asset[]) => {
268
287
  this.keyedAssets['fields'] = [];
269
288
  this.featuredFields = [];
@@ -10,11 +10,13 @@ export class Tab extends RapidElement {
10
10
  display: none;
11
11
  flex-direction: column;
12
12
  min-height: 0;
13
+ pointer-events: none;
13
14
  }
14
15
 
15
16
  :host(.selected) {
16
17
  display: flex;
17
18
  flex-grow: 1;
19
+ pointer-events: auto;
18
20
  }
19
21
  `;
20
22
  }
@@ -82,7 +82,6 @@ export class TabPane extends RapidElement {
82
82
  }
83
83
 
84
84
  .focusedname .tab.selected {
85
- transform: none;
86
85
  }
87
86
 
88
87
  .focusedname .tab .name {
@@ -119,8 +118,6 @@ export class TabPane extends RapidElement {
119
118
  }
120
119
 
121
120
  .embedded .tab.selected {
122
- border: none;
123
- transform: none;
124
121
  }
125
122
 
126
123
  .tab.selected .dot {
@@ -153,7 +150,10 @@ export class TabPane extends RapidElement {
153
150
  flex-direction: column;
154
151
  flex-grow: 1;
155
152
  background: var(--focused-tab-color, #fff);
156
- border-radius: var(--curvature);
153
+ border-bottom-left-radius: var(--curvature);
154
+ border-bottom-right-radius: var(--curvature);
155
+ overflow: hidden;
156
+
157
157
  box-shadow: var(
158
158
  --tabs-shadow,
159
159
  rgba(0, 0, 0, 0.1) 0px 1px 3px 0px,
@@ -217,6 +217,9 @@ export class TabPane extends RapidElement {
217
217
  .embedded.pane {
218
218
  box-shadow: none;
219
219
  margin: 0;
220
+ border-left: none !important;
221
+ border-right: none !important;
222
+ border-bottom: none !important;
220
223
  }
221
224
 
222
225
  .embedded.tabs {
@@ -224,6 +227,13 @@ export class TabPane extends RapidElement {
224
227
  }
225
228
 
226
229
  .embedded .tab {
230
+ border-top: none !important;
231
+ border-bottom: none !important;
232
+ border-radius: 0px;
233
+ }
234
+
235
+ .embedded .tab.first {
236
+ border-left: none !important;
227
237
  }
228
238
 
229
239
  .embedded.tabs .tab.selected {
@@ -332,6 +342,14 @@ export class TabPane extends RapidElement {
332
342
  }
333
343
  }
334
344
 
345
+ public focusTab(name: string): Tab {
346
+ const index = this.tabs.findIndex((tab) => tab.name === name);
347
+ if (index >= 0) {
348
+ this.index = index;
349
+ return this.getTab(index);
350
+ }
351
+ }
352
+
335
353
  public setTabDetails(
336
354
  index: number,
337
355
  details: { count: number; hidden: boolean }
@@ -444,7 +462,11 @@ export class TabPane extends RapidElement {
444
462
  ${!this.bottom
445
463
  ? html`<div
446
464
  @temba-details-changed=${this.handleTabDetailsChanged}
447
- style="border: 1px solid ${activeTab?.borderColor};background:${activeTab?.selectionBackground}"
465
+ style="${activeTab?.borderColor
466
+ ? `border: 1px solid ${activeTab?.borderColor};`
467
+ : ''} ${activeTab?.selectionBackground
468
+ ? `background:${activeTab?.selectionBackground};`
469
+ : ``}"
448
470
  class="pane ${getClasses({
449
471
  first: this.index == 0,
450
472
  embedded: this.embedded,
@@ -452,6 +474,7 @@ export class TabPane extends RapidElement {
452
474
  })}"
453
475
  >
454
476
  <slot></slot>
477
+ <slot name="pane-bottom"></slot>
455
478
  </div>`
456
479
  : null}
457
480
  `;