inviton-powerduck 0.0.216 → 0.0.218

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.
@@ -7,6 +7,7 @@ import type { AppMenuItem } from '../app/menu';
7
7
  import type { CardHeaderDropdownArgs } from '../card/card-header-with-options';
8
8
  import type { DropdownListOption } from '../dropdown';
9
9
  import type { ImageDropdownDataItem } from '../dropdown/image-dropdown';
10
+ import type { SmartDropdownCategoryItem, SmartDropdownSearchResultItem } from '../dropdown/smart-dropdown';
10
11
  import type { TimegridCalendarAddClickedArgs, TimegridCalendarEvent } from '../fullcalendar/timegrid-calendar';
11
12
  import { Temporal } from '@js-temporal/polyfill';
12
13
  import { Prop, toNative } from 'vue-facing-decorator';
@@ -30,6 +31,7 @@ import DropdownButtonItem from '../dropdown-button/dropdown-button-item';
30
31
  import DropdownButtonSeparator from '../dropdown-button/dropdown-button-separator';
31
32
  import CountryDropdown from '../dropdown/country-dropdown';
32
33
  import ImageDropdown from '../dropdown/image-dropdown';
34
+ import SmartDropdown from '../dropdown/smart-dropdown';
33
35
  import Fieldset from '../form/fieldset';
34
36
  import FlexContainer from '../form/flex-container';
35
37
  import FooterButtons from '../form/footer-buttons';
@@ -155,6 +157,81 @@ class TestAllComponentsPageComponent extends PowerduckViewModelBase {
155
157
  });
156
158
  }
157
159
 
160
+ // 1. Static Categories
161
+ categories: SmartDropdownCategoryItem[] = [
162
+ { id: 101, text: 'Skipass' },
163
+ { id: 102, text: 'Výlet lanovkou' },
164
+ { id: 103, text: 'Aquapass' },
165
+ { id: 104, text: 'Ubytovanie' },
166
+ { id: 105, text: 'Parkovanie' },
167
+ { id: 106, text: 'Eventy' },
168
+ { id: 107, text: 'Zazitky' },
169
+ { id: 108, text: 'Ine srandy' },
170
+ { id: 109, text: 'Vouchery' },
171
+ { id: 110, text: 'Pokemony' },
172
+ { id: 111, text: 'Kino' },
173
+ { id: 112, text: 'Restauracie' },
174
+ { id: 113, text: 'Este insie srandy' },
175
+ { id: 114, text: 'Sranda banda' },
176
+ ];
177
+
178
+ // 2. Selected State
179
+ selectedItems: (SmartDropdownCategoryItem | SmartDropdownSearchResultItem)[] = [];
180
+
181
+ // 3. Search Data Logic (Simulated Backend)
182
+ async fetchSmartDropdownSearchData(query: string): Promise<SmartDropdownSearchResultItem[]> {
183
+ console.log('Fetching for:', query);
184
+
185
+ // Simulate network delay
186
+ await new Promise(resolve => setTimeout(resolve, 500)); // 500ms delay
187
+
188
+ const mockDb: SmartDropdownSearchResultItem[] = [
189
+ { id: 1, text: 'Hotel Grand Jasná', subtitle: 'Ubytovanie • Jasná', imageUrl: 'https://picsum.photos/200' },
190
+ { id: 2, text: 'Celosezónny Skipass', subtitle: 'Produkt • Nízke Tatry', imageUrl: 'https://picsum.photos/200' },
191
+ { id: 3, text: 'Hotel Rotunda', subtitle: 'Ubytovanie • Chopok', imageUrl: 'https://picsum.photos/200' },
192
+ { id: 4, text: 'Fresh Track', subtitle: 'Zážitok • Lomnica' },
193
+ { id: 5, text: 'Wellness Hotel', subtitle: 'Ubytovanie • Bešeňová' },
194
+ ];
195
+
196
+ return mockDb.filter(item =>
197
+ item.text.toLowerCase().includes(query.toLowerCase()));
198
+ }
199
+
200
+ // 4. Handle Change
201
+ onSmartDropdownSelectionChange(newSelection: any[]) {
202
+ console.log('Selection updated:', newSelection);
203
+ this.selectedItems = newSelection;
204
+ }
205
+
206
+ // 2. Custom Sections (History & Inspiration)
207
+ get customSections() {
208
+ return [
209
+ {
210
+ id: 'history',
211
+ title: 'Nedávne vyhľadávania',
212
+ items: [
213
+ { id: 'h1', text: 'Jasná', type: 'history' },
214
+ { id: 'h2', text: 'Vysoké Tatry', type: 'history' },
215
+ ],
216
+ },
217
+ {
218
+ id: 'inspiration',
219
+ title: 'Váhate, kam cestovať najbližšie?',
220
+ items: [
221
+ { id: 'insp1', text: 'Zadajte Kamkoľvek', type: 'special', icon: 'pin' },
222
+ { id: 'insp2', text: 'V mojej lokalite', type: 'special', icon: 'target' },
223
+ ],
224
+ },
225
+ ];
226
+ }
227
+
228
+ // 4. Custom Handler for History Delete
229
+ removeSmartDropdownHistory(e: Event, id: string | number) {
230
+ e.stopPropagation();
231
+ console.log('Removing history', id);
232
+ // logic to remove from customSections...
233
+ }
234
+
158
235
  render(h) {
159
236
  return (
160
237
  <div>
@@ -333,11 +410,11 @@ class TestAllComponentsPageComponent extends PowerduckViewModelBase {
333
410
  <p>Some modal content1</p>
334
411
  </ModalSection>
335
412
  {this.boolValue == true
336
- && (
337
- <ModalSection icon="icon icon-settings" navCaption="Settings2">
338
- <p>Some modal content2</p>
339
- </ModalSection>
340
- )}
413
+ && (
414
+ <ModalSection icon="icon icon-settings" navCaption="Settings2">
415
+ <p>Some modal content2</p>
416
+ </ModalSection>
417
+ )}
341
418
 
342
419
  <ModalSection icon="icon icon-settings" navCaption="Settings4">
343
420
  <p>Some modal content4444</p>
@@ -411,16 +488,12 @@ class TestAllComponentsPageComponent extends PowerduckViewModelBase {
411
488
  h,
412
489
  item,
413
490
  originator,
414
- ) => {
415
- return (<CustomDropdownItem text={item.text} />);
416
- }}
491
+ ) => <CustomDropdownItem text={item.text} />}
417
492
  customRenderSelectionResult={(
418
493
  h,
419
494
  item,
420
495
  originator,
421
- ) => {
422
- return (<CustomDropdownItem text={item.text} />);
423
- }}
496
+ ) => <CustomDropdownItem text={item.text} />}
424
497
  changed={(v) => {
425
498
  this.selectedOptions = v.id;
426
499
  }}
@@ -479,6 +552,58 @@ class TestAllComponentsPageComponent extends PowerduckViewModelBase {
479
552
  }}
480
553
  />
481
554
 
555
+ <SmartDropdown
556
+ label="Mudry dropdown"
557
+ wrap={false}
558
+ categories={this.categories}
559
+ searchData={this.fetchSmartDropdownSearchData}
560
+ customSections={this.customSections}
561
+ value={this.selectedItems}
562
+ buttonLayout="inline"
563
+ selectionDisplay="chips"
564
+
565
+ searchMode="input"
566
+ changed={this.onSmartDropdownSelectionChange} // Or use @selectionChanged in template
567
+ multiselect={true} // Toggle to false for single select
568
+ placeholder="Hľadajte hotel, službu..."
569
+ // customSectionRender={(item, selected) => {
570
+ // // Example: History Item
571
+ // if (item.type === 'history') {
572
+ // return (
573
+ // <div style="display:flex; justify-content:space-between; padding: 8px 16px;">
574
+ // <span>
575
+ // ↻
576
+ // <b>{item.text}</b>
577
+ // </span>
578
+ // <span onClick={e => this.removeHistory(e, item.id)}>x</span>
579
+ // </div>
580
+ // );
581
+ // }
582
+
583
+ // // Example: Default Checkbox style via simple HTML
584
+ // return (
585
+ // <div style="display:flex; align-items:center; padding: 8px 16px;">
586
+ // <div class={`checkbox-visual ${selected ? 'checked' : ''}`} style="margin-right:10px; width:16px; height:16px; border:1px solid #ccc;"></div>
587
+ // {item.text}
588
+ // </div>
589
+ // );
590
+ // }}
591
+ customTriggerScope="mobile"
592
+ customTriggerRender={() => (
593
+ <div style="display:flex; flex-direction:column; width:100%; gap:8px;">
594
+ {/* Mimicking the stacked mobile inputs */}
595
+ <div style="border:1px solid #eee; padding:8px; border-radius:8px; display:flex; justify-content:space-between;">
596
+ <span style="color:#999; font-size:12px">Kam</span>
597
+ <strong>Jasná</strong>
598
+ </div>
599
+ <div style="border:1px solid #eee; padding:8px; border-radius:8px; display:flex; justify-content:space-between;">
600
+ <span style="color:#999; font-size:12px">Čo hľadáte</span>
601
+ <strong>Skipass</strong>
602
+ </div>
603
+ </div>
604
+ )}
605
+ />
606
+
482
607
  <DropdownButton layout={ButtonLayout.Default} size={ButtonSize.Regular} text="Akcie">
483
608
  <DropdownButtonItem icon="icon icon-settings" text="Edit" clicked={() => alert('clicked me')} />
484
609
  <DropdownButtonSeparator />
@@ -0,0 +1,422 @@
1
+ .gopass-filter-wrapper {
2
+ /* ... [Previous variables and base styles] ... */
3
+ --bs-blue: #0d6efd;
4
+ --bs-btn-bg: #0d6efd;
5
+ --bs-btn-color: #fff;
6
+ --bs-btn-hover: #0b5ed7;
7
+ --bs-btn-disabled-bg: #0d6efd;
8
+ --bs-btn-disabled-opacity: 0.65;
9
+ --bs-border-radius: 0.375rem;
10
+
11
+ font-family:
12
+ system-ui,
13
+ -apple-system,
14
+ 'Segoe UI',
15
+ Roboto,
16
+ sans-serif;
17
+ position: relative;
18
+ width: 100%;
19
+
20
+ * {
21
+ box-sizing: border-box;
22
+ }
23
+
24
+ /* --- TRIGGER --- */
25
+ .filter-trigger {
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: space-between;
29
+ width: 100%;
30
+ min-height: 38px;
31
+ padding: 0.375rem 0.75rem;
32
+ font-size: 1rem;
33
+ color: #212529;
34
+ background: #fff;
35
+ border: 1px solid #ced4da;
36
+ border-radius: var(--bs-border-radius);
37
+ cursor: pointer;
38
+ transition: all 0.15s;
39
+
40
+ &:hover {
41
+ border-color: #b3b3b3;
42
+ }
43
+
44
+ &.is-open {
45
+ border-color: #86b7fe;
46
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
47
+ }
48
+ }
49
+
50
+ .result-image {
51
+ width: 32px;
52
+ height: 32px;
53
+ border-radius: 50%;
54
+ /* Circle image */
55
+ object-fit: cover;
56
+ margin-right: 12px;
57
+ background-color: #f0f0f0;
58
+ flex-shrink: 0;
59
+ }
60
+
61
+ /* INPUT MODE TRIGGER STYLES */
62
+ .trigger-input {
63
+ width: 100%;
64
+ border: none;
65
+ outline: none;
66
+ background: transparent;
67
+ font-size: 1rem;
68
+ color: #212529;
69
+ padding: 0;
70
+ margin: 0;
71
+
72
+ &::placeholder {
73
+ color: #6c757d;
74
+ }
75
+ }
76
+
77
+ .trigger-input-inline {
78
+ border: none;
79
+ outline: none;
80
+ background: transparent;
81
+ font-size: 1rem;
82
+ min-width: 50px;
83
+ flex-grow: 1;
84
+ }
85
+
86
+ /* ... [Chips, Placeholder, Caret styles - No Change] ... */
87
+ .trigger-content {
88
+ flex-grow: 1;
89
+ display: flex;
90
+ flex-wrap: wrap;
91
+ gap: 4px;
92
+ overflow: hidden;
93
+ }
94
+
95
+ .placeholder-text {
96
+ color: #6c757d;
97
+ }
98
+
99
+ .caret {
100
+ margin-left: 8px;
101
+ border-top: 4px solid #666;
102
+ border-right: 4px solid transparent;
103
+ border-left: 4px solid transparent;
104
+ }
105
+
106
+ .chip {
107
+ background: #e9ecef;
108
+ color: #495057;
109
+ padding: 2px 8px;
110
+ border-radius: 12px;
111
+ font-size: 0.85rem;
112
+ display: inline-flex;
113
+ align-items: center;
114
+ font-weight: 500;
115
+ }
116
+
117
+ .chip-remove {
118
+ margin-left: 6px;
119
+ cursor: pointer;
120
+ font-weight: bold;
121
+ color: #888;
122
+ width: 16px;
123
+ height: 16px;
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ border-radius: 50%;
128
+
129
+ &:hover {
130
+ background: #ccc;
131
+ color: #000;
132
+ }
133
+ }
134
+
135
+ /* --- DROPDOWN --- */
136
+ .filter-dropdown {
137
+ position: absolute;
138
+ top: calc(100% + 4px);
139
+ left: 0;
140
+ width: 100%;
141
+ min-width: 300px;
142
+ background: white;
143
+ border: 1px solid rgba(0, 0, 0, 0.15);
144
+ border-radius: var(--bs-border-radius);
145
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
146
+ z-index: 1050;
147
+ display: flex;
148
+ flex-direction: column;
149
+ max-height: 450px;
150
+ animation: fadeIn 0.1s ease-out;
151
+ }
152
+
153
+ @keyframes fadeIn {
154
+ from {
155
+ opacity: 0;
156
+ transform: translateY(-5px);
157
+ }
158
+
159
+ to {
160
+ opacity: 1;
161
+ transform: translateY(0);
162
+ }
163
+ }
164
+
165
+ .search-header {
166
+ padding: 10px;
167
+ border-bottom: 1px solid #e9ecef;
168
+ background-color: #fff;
169
+ flex-shrink: 0;
170
+ display: flex;
171
+ gap: 8px;
172
+ align-items: center;
173
+ }
174
+
175
+ .search-input {
176
+ flex-grow: 1;
177
+ width: auto;
178
+ padding: 0.375rem 0.75rem 0.375rem 34px;
179
+ border: 1px solid #ced4da;
180
+ border-radius: 4px;
181
+ outline: none;
182
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23999' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z'/%3E%3C/svg%3E");
183
+ background-repeat: no-repeat;
184
+ background-position: 10px center;
185
+
186
+ &:focus {
187
+ border-color: #86b7fe;
188
+ }
189
+ }
190
+
191
+ /* ... [Button, List, Footer, Item styles - No Change] ... */
192
+ .btn-confirm {
193
+ display: inline-block;
194
+ font-weight: 500;
195
+ text-align: center;
196
+ vertical-align: middle;
197
+ user-select: none;
198
+ border: 1px solid transparent;
199
+ padding: 0.375rem 0.75rem;
200
+ font-size: 1rem;
201
+ line-height: 1.5;
202
+ border-radius: var(--bs-border-radius);
203
+ color: var(--bs-btn-color);
204
+ background-color: var(--bs-btn-bg);
205
+ cursor: pointer;
206
+ transition: all 0.15s;
207
+ white-space: nowrap;
208
+
209
+ &:hover:not(:disabled) {
210
+ background-color: var(--bs-btn-hover);
211
+ }
212
+
213
+ &:active:not(:disabled) {
214
+ background-color: #0a58ca;
215
+ }
216
+
217
+ &:disabled {
218
+ opacity: var(--bs-btn-disabled-opacity);
219
+ cursor: not-allowed;
220
+ }
221
+ }
222
+
223
+ .dropdown-footer {
224
+ padding: 10px;
225
+ border-top: 1px solid #e9ecef;
226
+ background-color: #f8f9fa;
227
+ flex-shrink: 0;
228
+
229
+ .btn-confirm {
230
+ width: 100%;
231
+ }
232
+ }
233
+
234
+ .list-container {
235
+ overflow-y: auto;
236
+ flex-grow: 1;
237
+ min-height: 150px;
238
+ }
239
+
240
+ .list-group-label {
241
+ padding: 8px 16px;
242
+ font-size: 0.75rem;
243
+ font-weight: 700;
244
+ text-transform: uppercase;
245
+ color: #adb5bd;
246
+ background: #fff;
247
+ border-bottom: 1px solid #f0f0f0;
248
+ position: sticky;
249
+ top: 0;
250
+ z-index: 5;
251
+ }
252
+
253
+ .sticky-selected {
254
+ background: #f8f9fa;
255
+ color: var(--bs-blue);
256
+ border-bottom: 2px solid #e9ecef;
257
+ }
258
+
259
+ .list-item-custom-container {
260
+ cursor: pointer;
261
+ border-bottom: 1px solid #f8f9fa;
262
+ transition: background-color 0.15s;
263
+
264
+ &:hover {
265
+ background-color: #e9ecef;
266
+ }
267
+
268
+ &.selected {
269
+ background-color: #e7f1ff;
270
+ }
271
+ }
272
+
273
+ .list-item {
274
+ padding: 8px 16px;
275
+ cursor: pointer;
276
+ display: flex;
277
+ align-items: center;
278
+ border-bottom: 1px solid #f8f9fa;
279
+
280
+ &:hover {
281
+ background: #e9ecef;
282
+ }
283
+
284
+ &.selected {
285
+ background: #e7f1ff;
286
+ }
287
+ }
288
+
289
+ .checkbox-visual {
290
+ width: 16px;
291
+ height: 16px;
292
+ border: 1px solid #adb5bd;
293
+ border-radius: 3px;
294
+ margin-right: 12px;
295
+ display: flex;
296
+ align-items: center;
297
+ justify-content: center;
298
+ background: #fff;
299
+ flex-shrink: 0;
300
+
301
+ &.checked {
302
+ background: var(--bs-blue);
303
+ border-color: var(--bs-blue);
304
+
305
+ &::after {
306
+ content: '';
307
+ width: 4px;
308
+ height: 8px;
309
+ border: solid white;
310
+ border-width: 0 2px 2px 0;
311
+ transform: rotate(45deg);
312
+ margin-top: -2px;
313
+ }
314
+ }
315
+ }
316
+
317
+ .no-checkbox .checkbox-visual {
318
+ display: none;
319
+ }
320
+
321
+ .item-content {
322
+ flex: 1;
323
+ }
324
+
325
+ .item-text {
326
+ display: block;
327
+ font-size: 0.95rem;
328
+ color: #212529;
329
+ }
330
+
331
+ .item-subtitle {
332
+ display: block;
333
+ font-size: 0.8rem;
334
+ color: #6c757d;
335
+ }
336
+
337
+ .loading-state,
338
+ .empty-state {
339
+ padding: 1.5rem;
340
+ text-align: center;
341
+ color: #6c757d;
342
+ font-size: 0.9rem;
343
+ }
344
+
345
+ .view-mobile-only {
346
+ width: 100%;
347
+ }
348
+
349
+ .view-desktop-only {
350
+ width: 100%;
351
+ }
352
+
353
+ /* Desktop View ( > 768px ) */
354
+ @media (min-width: 769px) {
355
+ .view-mobile-only {
356
+ display: none !important;
357
+ }
358
+
359
+ .view-desktop-only {
360
+ display: block !important;
361
+ }
362
+ }
363
+
364
+ /* Mobile View ( <= 768px ) */
365
+ @media (max-width: 768px) {
366
+ .view-mobile-only {
367
+ display: block !important;
368
+ }
369
+
370
+ .view-desktop-only {
371
+ display: none !important;
372
+ }
373
+ }
374
+
375
+ /* --- MODE LOGIC (Desktop) --- */
376
+
377
+ /* When in 'input' mode on Desktop: Hide internal search header and footer */
378
+ @media (min-width: 769px) {
379
+ &.mode-input {
380
+ .internal-search-header {
381
+ display: none !important;
382
+ }
383
+
384
+ .dropdown-footer {
385
+ display: none !important;
386
+ }
387
+ }
388
+ }
389
+
390
+ /* --- MOBILE RESPONSIVE --- */
391
+ @media (max-width: 768px) {
392
+ .filter-dropdown {
393
+ position: fixed;
394
+ top: 0;
395
+ left: 0;
396
+ width: 100vw;
397
+ height: 100vh;
398
+ border-radius: 0;
399
+ border: none;
400
+ z-index: 9999;
401
+ max-height: none;
402
+ }
403
+
404
+ .list-container {
405
+ height: auto;
406
+ }
407
+
408
+ /* On Mobile, always show the internal search/footer even if in 'input' mode */
409
+ /* because the external input is behind the modal */
410
+ &.mode-input {
411
+ .internal-search-header {
412
+ display: flex !important;
413
+ }
414
+
415
+ .dropdown-footer {
416
+ display: block !important;
417
+ }
418
+ }
419
+
420
+ /* Optional: Hide external input caretaker on mobile if it looks weird */
421
+ }
422
+ }
@@ -0,0 +1,739 @@
1
+ /* eslint-disable ts/no-this-alias */
2
+ /* eslint-disable no-useless-call */
3
+
4
+ import type { VNode } from 'vue';
5
+
6
+ import type { DropdownButtonItemArgs } from '../dropdown-button/dropdown-button-item';
7
+ import type { FormItemWrapperArgs, MarginType } from '../form/form-item-wrapper';
8
+ import { Component, Prop, Watch } from 'vue-facing-decorator';
9
+ import PowerduckState from '../../app/powerduck-state';
10
+ import TsxComponent from '../../app/vuetsx';
11
+ import FormItemWrapper from '../form/form-item-wrapper';
12
+ import './css/smart-dropdown.scss';
13
+
14
+ // --- Resources ---
15
+ export class SmartDropdownResources {
16
+ static placeholderDefault = 'Vyberte...';
17
+ static doneButtonText = 'Hotovo';
18
+ static loadingTextShort = '...';
19
+ static loadingTextLong = 'Načítavam...';
20
+ static noResultsSearch = 'Nenašli sa žiadne výsledky.';
21
+ static noResultsDefault = 'Žiadne možnosti.';
22
+ static searchResultTitle = 'Výsledky vyhľadávania';
23
+ static selectedTitle = 'Vybrané';
24
+ static categoriesTitle = 'Kategórie';
25
+ static summarySuffix = ' vybrané';
26
+ static closeLabel = PowerduckState.getResourceValue('close');
27
+ }
28
+
29
+ // --- Interfaces ---
30
+ export interface SmartDropdownCategoryItem {
31
+ id: number | string;
32
+ text: string;
33
+ [key: string]: any;
34
+ }
35
+
36
+ export interface SmartDropdownSearchResultItem {
37
+ id: number | string;
38
+ text: string;
39
+ subtitle?: string;
40
+ imageUrl?: string;
41
+ [key: string]: any;
42
+ }
43
+
44
+ export type SmartDropdownItem = SmartDropdownCategoryItem | SmartDropdownSearchResultItem;
45
+
46
+ export interface SmartDropdownSection {
47
+ id: string;
48
+ title?: string;
49
+ items: SmartDropdownItem[];
50
+ }
51
+
52
+ interface SmartDropdownArgs extends FormItemWrapperArgs {
53
+ categories: SmartDropdownCategoryItem[];
54
+ customSections: SmartDropdownSection[];
55
+ searchData: (text: string) => Promise<SmartDropdownSearchResultItem[]>;
56
+ value: SmartDropdownItem[];
57
+ multiselect: boolean;
58
+ placeholder: string;
59
+ selectionDisplay: 'chips' | 'text';
60
+ buttonLayout: 'footer' | 'inline';
61
+ searchMode: 'dropdown' | 'input';
62
+ customTriggerScope?: 'mobile' | 'desktop' | 'all';
63
+ customSectionRender?: (item: SmartDropdownItem, selected: boolean) => VNode;
64
+ customTriggerRender?: () => VNode;
65
+ changed: (e: SmartDropdownItem[]) => void;
66
+ }
67
+
68
+ @Component
69
+ export default class SmartDropdown extends TsxComponent<SmartDropdownArgs> implements SmartDropdownArgs {
70
+ @Prop() label!: string;
71
+ @Prop() labelButtons!: DropdownButtonItemArgs[];
72
+ @Prop() subtitle!: string;
73
+ @Prop() cssClass!: string;
74
+ @Prop() mandatory!: boolean;
75
+ @Prop() disabled!: boolean;
76
+ @Prop() wrap!: boolean;
77
+ @Prop() hint: string;
78
+ @Prop() appendIcon: string;
79
+ @Prop() prependIcon: string;
80
+ @Prop() maxWidth?: number;
81
+ @Prop() marginType?: MarginType;
82
+ @Prop() appendClicked: () => void;
83
+ @Prop() prependClicked: () => void;
84
+ @Prop() prependIconClicked: () => void;
85
+ @Prop() appendIconClicked: () => void;
86
+ @Prop() keyDown: (e: KeyboardEvent) => void;
87
+ @Prop() keyUp: (e: KeyboardEvent) => void;
88
+ @Prop() enterPressed: (e: KeyboardEvent) => void;
89
+ @Prop() showClearValueButton!: boolean;
90
+
91
+ // --- Props from SmartDropdown---
92
+ @Prop({ type: Array, required: true }) readonly categories!: SmartDropdownCategoryItem[];
93
+ @Prop({ type: Array, default: () => [] }) readonly customSections!: SmartDropdownSection[];
94
+ @Prop({ type: Function, required: true }) readonly searchData!: (text: string) => Promise<SmartDropdownSearchResultItem[]>;
95
+ @Prop({ type: Array, default: () => [] }) readonly value!: SmartDropdownItem[];
96
+ @Prop({ type: Boolean, default: false }) readonly multiselect!: boolean;
97
+ @Prop({ type: String, default: () => SmartDropdownResources.placeholderDefault }) readonly placeholder!: string;
98
+ @Prop({ type: String, default: 'text' }) readonly selectionDisplay!: 'chips' | 'text';
99
+ @Prop({ type: String, default: 'footer' }) readonly buttonLayout!: 'footer' | 'inline';
100
+ @Prop({ type: String, default: 'dropdown' }) readonly searchMode!: 'dropdown' | 'input';
101
+ @Prop({ type: String, default: 'all' }) readonly customTriggerScope!: 'mobile' | 'desktop' | 'all';
102
+ @Prop({ type: Function }) readonly customSectionRender?: (item: SmartDropdownItem, selected: boolean) => VNode;
103
+ @Prop({ type: Function }) readonly customTriggerRender?: () => VNode;
104
+ @Prop() readonly changed: (e: SmartDropdownItem[]) => void;
105
+
106
+ // --- State ---
107
+ isOpen = false;
108
+ searchQuery = '';
109
+ searchResults: SmartDropdownSearchResultItem[] = [];
110
+ isLoading = false;
111
+ debounceTimer: number | null = null;
112
+ focusedIndex = -1;
113
+ triggerInputValue = '';
114
+
115
+ // Accessibility ID
116
+ uid = `smart-dd-${Math.random().toString(36).slice(2, 9)}`;
117
+
118
+ // --- Watchers ---
119
+ @Watch('value', { deep: true, immediate: true })
120
+ onSelectionChange() {
121
+ if (!this.isOpen || this.searchQuery === '') {
122
+ this.triggerInputValue = this.displayText;
123
+ }
124
+ }
125
+
126
+ // --- Computed ---
127
+ get listboxId(): string {
128
+ return `${this.uid}-listbox`;
129
+ }
130
+
131
+ get activeDescendantId(): string | undefined {
132
+ return this.focusedIndex >= 0 ? `${this.uid}-option-${this.focusedIndex}` : undefined;
133
+ }
134
+
135
+ get flattenedDisplayItems(): SmartDropdownItem[] {
136
+ // Helper to get flat list for keyboard navigation
137
+ if (this.isSearchActive) {
138
+ return this.searchResults;
139
+ }
140
+
141
+ const items: SmartDropdownItem[] = [];
142
+ if (this.pinnedSelectedItems.length > 0) {
143
+ items.push(...this.pinnedSelectedItems);
144
+ }
145
+
146
+ this.customSections.forEach(s => items.push(...s.items));
147
+ if (this.standardDisplayItems.length > 0) {
148
+ items.push(...this.standardDisplayItems);
149
+ }
150
+
151
+ return items;
152
+ }
153
+
154
+ get standardDisplayItems(): SmartDropdownItem[] {
155
+ if (this.multiselect && this.selectionDisplay === 'text' && this.value.length > 0) {
156
+ const selectedIds = new Set(this.value.map(i => i.id));
157
+ return this.categories.filter(c => !selectedIds.has(c.id));
158
+ }
159
+
160
+ return this.categories;
161
+ }
162
+
163
+ get pinnedSelectedItems(): SmartDropdownItem[] {
164
+ if (this.multiselect && this.selectionDisplay === 'text' && !this.isSearchActive) {
165
+ return this.value;
166
+ }
167
+
168
+ return [];
169
+ }
170
+
171
+ get isSearchActive(): boolean {
172
+ return this.searchQuery.trim().length > 0;
173
+ }
174
+
175
+ get displayText(): string {
176
+ if (this.value.length === 0) {
177
+ return '';
178
+ }
179
+
180
+ if (!this.multiselect) {
181
+ return this.value[0].text;
182
+ }
183
+
184
+ if (this.value.length === 1) {
185
+ return this.value[0].text;
186
+ }
187
+
188
+ return `${this.value.length}${SmartDropdownResources.summarySuffix}`;
189
+ }
190
+
191
+ beforeDestroy() {
192
+ this.removeEventHandlers();
193
+ }
194
+
195
+ // --- Methods ---
196
+
197
+ toggleDropdown(force?: boolean) {
198
+ const nextState = typeof force === 'boolean' ? force : !this.isOpen;
199
+ if (nextState) {
200
+ this.isOpen = true;
201
+ this.focusedIndex = -1; // Reset focus logic
202
+
203
+ this.$nextTick(() => {
204
+ const isMobile = window.innerWidth <= 768;
205
+ const dropdownInput = this.$el.querySelector('.dropdown-search-input') as HTMLInputElement;
206
+ const triggerInput = this.$el.querySelector('.trigger-input') as HTMLInputElement;
207
+
208
+ // Focus Logic
209
+ const shouldUseExternalFocus = this.searchMode === 'input' && !isMobile && this.customTriggerScope !== 'desktop';
210
+
211
+ if (shouldUseExternalFocus) {
212
+ triggerInput?.select();
213
+ } else {
214
+ dropdownInput?.focus();
215
+ }
216
+
217
+ // Initial Scroll Logic
218
+ if (this.pinnedSelectedItems.length > 0) {
219
+ const listContainer = this.$el.querySelector('.list-container') as HTMLElement;
220
+ const categoryHeader = this.$el.querySelector('.section-categories') as HTMLElement;
221
+ if (listContainer && categoryHeader) {
222
+ listContainer.scrollTop = categoryHeader.offsetTop;
223
+ }
224
+ }
225
+ });
226
+
227
+ this.removeEventHandlers();
228
+
229
+ const self = this;
230
+ this.handleClickOutside = (e: Event) => {
231
+ if (!self.$el.contains(e.target as Node)) {
232
+ self.closeDropdown.call(self);
233
+ }
234
+ };
235
+
236
+ this.handleGlobalKeydown = (e: KeyboardEvent) => {
237
+ if (!this.isOpen) {
238
+ // Allow opening with Enter or Down Arrow/Space if focused on trigger
239
+ if ((e.key === 'Enter' || e.key === 'ArrowDown' || e.key === ' ') && (e.target as HTMLElement).classList.contains('filter-trigger')) {
240
+ self.toggleDropdown.call(self, true);
241
+ e.preventDefault();
242
+ }
243
+
244
+ return;
245
+ }
246
+
247
+ const totalItems = self.flattenedDisplayItems.length;
248
+ switch (e.key) {
249
+ case 'Escape':
250
+ self.closeDropdown.call(self);
251
+ e.preventDefault();
252
+ break;
253
+ case 'Enter':
254
+ e.preventDefault();
255
+ if (self.focusedIndex >= 0) {
256
+ self.handleItemClick.call(self, self.flattenedDisplayItems[self.focusedIndex]);
257
+ } else {
258
+ self.confirmAndClose();
259
+ }
260
+
261
+ break;
262
+ case 'ArrowDown':
263
+ e.preventDefault();
264
+ self.focusedIndex = (self.focusedIndex + 1) % totalItems;
265
+ self.scrollItemIntoView.call(self, self.focusedIndex);
266
+ break;
267
+ case 'ArrowUp':
268
+ e.preventDefault();
269
+ self.focusedIndex = (self.focusedIndex - 1 + totalItems) % totalItems;
270
+ self.scrollItemIntoView.call(self, self.focusedIndex);
271
+ break;
272
+ case 'Home':
273
+ e.preventDefault();
274
+ self.focusedIndex = 0;
275
+ self.scrollItemIntoView.call(self, self.focusedIndex);
276
+ break;
277
+ case 'End':
278
+ e.preventDefault();
279
+ self.focusedIndex = totalItems - 1;
280
+ self.scrollItemIntoView.call(self, self.focusedIndex);
281
+ break;
282
+ case 'Tab':
283
+ self.closeDropdown.call(self); // Tab out closes dropdown
284
+ break;
285
+ }
286
+ };
287
+
288
+ document.addEventListener('click', this.handleClickOutside);
289
+ document.addEventListener('keydown', this.handleGlobalKeydown);
290
+ } else {
291
+ this.closeDropdown();
292
+ }
293
+ }
294
+
295
+ removeEventHandlers() {
296
+ if (this.handleClickOutside) {
297
+ document.removeEventListener('click', this.handleClickOutside);
298
+ }
299
+
300
+ if (this.handleGlobalKeydown) {
301
+ document.removeEventListener('keydown', this.handleGlobalKeydown);
302
+ }
303
+ }
304
+
305
+ closeDropdown() {
306
+ this.isOpen = false;
307
+ this.focusedIndex = -1;
308
+ this.triggerInputValue = this.displayText;
309
+ this.searchQuery = '';
310
+ }
311
+
312
+ confirmAndClose() {
313
+ if (this.isLoading) {
314
+ return;
315
+ }
316
+
317
+ this.searchQuery = '';
318
+ this.searchResults = [];
319
+ this.closeDropdown();
320
+ }
321
+
322
+ handleClickOutside = (e: Event) => {
323
+ if (!this.$el.contains(e.target as Node)) {
324
+ this.closeDropdown();
325
+ }
326
+ };
327
+
328
+ handleGlobalKeydown = (e: KeyboardEvent) => {
329
+ if (!this.isOpen) {
330
+ // Allow opening with Enter or Down Arrow/Space if focused on trigger
331
+ if ((e.key === 'Enter' || e.key === 'ArrowDown' || e.key === ' ') && (e.target as HTMLElement).classList.contains('filter-trigger')) {
332
+ this.toggleDropdown(true);
333
+ e.preventDefault();
334
+ }
335
+
336
+ return;
337
+ }
338
+
339
+ const totalItems = this.flattenedDisplayItems.length;
340
+
341
+ switch (e.key) {
342
+ case 'Escape':
343
+ this.closeDropdown();
344
+ e.preventDefault();
345
+ break;
346
+ case 'Enter':
347
+ e.preventDefault();
348
+ if (this.focusedIndex >= 0) {
349
+ this.handleItemClick(this.flattenedDisplayItems[this.focusedIndex]);
350
+ } else {
351
+ this.confirmAndClose();
352
+ }
353
+
354
+ break;
355
+ case 'ArrowDown':
356
+ e.preventDefault();
357
+ this.focusedIndex = (this.focusedIndex + 1) % totalItems;
358
+ this.scrollItemIntoView(this.focusedIndex);
359
+ break;
360
+ case 'ArrowUp':
361
+ e.preventDefault();
362
+ this.focusedIndex = (this.focusedIndex - 1 + totalItems) % totalItems;
363
+ this.scrollItemIntoView(this.focusedIndex);
364
+ break;
365
+ case 'Home':
366
+ e.preventDefault();
367
+ this.focusedIndex = 0;
368
+ this.scrollItemIntoView(this.focusedIndex);
369
+ break;
370
+ case 'End':
371
+ e.preventDefault();
372
+ this.focusedIndex = totalItems - 1;
373
+ this.scrollItemIntoView(this.focusedIndex);
374
+ break;
375
+ case 'Tab':
376
+ this.closeDropdown(); // Tab out closes dropdown
377
+ break;
378
+ }
379
+ };
380
+
381
+ scrollItemIntoView(index: number) {
382
+ // Use nextTick to ensure DOM is updated if virtualized (not here, but good practice)
383
+ this.$nextTick(() => {
384
+ const itemId = `${this.uid}-option-${index}`;
385
+ const el = document.getElementById(itemId);
386
+ el?.scrollIntoView({ block: 'nearest' });
387
+ });
388
+ }
389
+
390
+ handleInput(e: Event) {
391
+ const val = (e.target as HTMLInputElement).value;
392
+ this.triggerInputValue = val;
393
+ this.searchQuery = val;
394
+
395
+ if (this.debounceTimer) {
396
+ clearTimeout(this.debounceTimer);
397
+ }
398
+
399
+ if (val.trim().length === 0) {
400
+ this.isLoading = false;
401
+ this.searchResults = [];
402
+ return;
403
+ }
404
+
405
+ this.isLoading = true;
406
+ this.debounceTimer = window.setTimeout(async () => {
407
+ try {
408
+ this.searchResults = await this.searchData(val);
409
+ } catch (err) {
410
+ this.searchResults = [];
411
+ } finally {
412
+ this.isLoading = false;
413
+ }
414
+ }, 800);
415
+ }
416
+
417
+ handleItemClick(item: SmartDropdownItem) {
418
+ const isSelected = this.isSelected(item);
419
+ let newSelection: SmartDropdownItem[] = [];
420
+
421
+ if (this.multiselect) {
422
+ if (isSelected) {
423
+ newSelection = this.value.filter(i => i.id !== item.id);
424
+ } else {
425
+ newSelection = [
426
+ ...this.value,
427
+ item,
428
+ ];
429
+ }
430
+ } else {
431
+ newSelection = [item];
432
+ this.closeDropdown();
433
+ }
434
+
435
+ this.changed(newSelection);
436
+ this.$nextTick(() => {
437
+ this.triggerInputValue = this.displayText;
438
+ });
439
+ }
440
+
441
+ removeSelection(e: Event, item: SmartDropdownItem) {
442
+ e.stopPropagation();
443
+ const newSelection = this.value.filter(i => i.id !== item.id);
444
+ this.changed(newSelection);
445
+ }
446
+
447
+ isSelected(item: SmartDropdownItem): boolean {
448
+ return this.value.some(i => i.id === item.id);
449
+ }
450
+
451
+ // --- Renders ---
452
+
453
+ renderStandardTrigger() {
454
+ if (this.multiselect && this.selectionDisplay === 'chips' && this.value.length > 0) {
455
+ return (
456
+ <div class="trigger-content">
457
+ {this.value.map(item => (
458
+ <span class="chip" onClick={e => e.stopPropagation()} key={item.id}>
459
+ {item.text}
460
+ <span
461
+ class="chip-remove"
462
+ onClick={e => this.removeSelection(e, item)}
463
+ role="button"
464
+ aria-label={`Remove ${item.text}`}
465
+ tabindex={0}
466
+ >
467
+ &times;
468
+ </span>
469
+ </span>
470
+ ))}
471
+ {this.searchMode === 'input' && (
472
+ <input
473
+ class="trigger-input-inline"
474
+ value={this.triggerInputValue === this.displayText ? '' : this.triggerInputValue}
475
+ onInput={this.handleInput}
476
+ aria-label={this.placeholder}
477
+ />
478
+ )}
479
+ </div>
480
+ );
481
+ }
482
+
483
+ if (this.searchMode === 'input') {
484
+ return (
485
+ <input
486
+ class="trigger-input"
487
+ type="text"
488
+ placeholder={this.placeholder}
489
+ value={this.triggerInputValue}
490
+ onInput={this.handleInput}
491
+ onClick={e => (e.target as HTMLInputElement).select()}
492
+ aria-autocomplete="list"
493
+ aria-controls={this.listboxId}
494
+ aria-activedescendant={this.isOpen ? this.activeDescendantId : undefined}
495
+ aria-expanded={this.isOpen}
496
+ />
497
+ );
498
+ }
499
+
500
+ return (
501
+ <div class="trigger-content">
502
+ <span class={this.value.length === 0 ? 'placeholder-text' : ''}>
503
+ {this.displayText || this.placeholder}
504
+ </span>
505
+ </div>
506
+ );
507
+ }
508
+
509
+ renderTrigger() {
510
+ if (!this.customTriggerRender) {
511
+ return this.renderStandardTrigger();
512
+ }
513
+
514
+ const customEl = (
515
+ <div class="trigger-custom-wrapper">
516
+ {this.customTriggerRender()}
517
+ </div>
518
+ );
519
+
520
+ const standardEl = this.renderStandardTrigger();
521
+
522
+ switch (this.customTriggerScope) {
523
+ case 'all': return customEl;
524
+ case 'mobile': return [
525
+ <div class="view-mobile-only">{customEl}</div>,
526
+ <div class="view-desktop-only">{standardEl}</div>,
527
+ ];
528
+ case 'desktop': return [
529
+ <div class="view-desktop-only">{customEl}</div>,
530
+ <div class="view-mobile-only">{standardEl}</div>,
531
+ ];
532
+ default: return standardEl;
533
+ }
534
+ }
535
+
536
+ // Main List Renderer (Manages Groups)
537
+ renderList() {
538
+ if (this.isLoading) {
539
+ return <div class="loading-state" role="status">{SmartDropdownResources.loadingTextLong}</div>;
540
+ }
541
+
542
+ // --- Helper to track global index for keyboard nav ---
543
+ let globalIndex = 0;
544
+ const getIndex = () => globalIndex++;
545
+
546
+ // A. Search Results
547
+ if (this.isSearchActive) {
548
+ if (this.searchResults.length === 0) {
549
+ return <div class="empty-state" role="status">{SmartDropdownResources.noResultsSearch}</div>;
550
+ }
551
+
552
+ return (
553
+ <div class="list-group" role="group" aria-label={SmartDropdownResources.searchResultTitle}>
554
+ <div class="list-group-label" aria-hidden="true">{SmartDropdownResources.searchResultTitle}</div>
555
+ {this.searchResults.map(item => this.renderListItem(item, getIndex()))}
556
+ </div>
557
+ );
558
+ }
559
+
560
+ const hasPinned = this.pinnedSelectedItems.length > 0;
561
+ const hasCustom = this.customSections.length > 0;
562
+ const hasStandard = this.standardDisplayItems.length > 0;
563
+
564
+ if (!hasPinned && !hasCustom && !hasStandard) {
565
+ return <div class="empty-state" role="status">{SmartDropdownResources.noResultsDefault}</div>;
566
+ }
567
+
568
+ return (
569
+ <div class="list-wrapper">
570
+ {/* 1. Pinned Selected */}
571
+ {hasPinned && (
572
+ <div class="list-group section-pinned" role="group" aria-label={SmartDropdownResources.selectedTitle}>
573
+ <div class="list-group-label sticky-selected" aria-hidden="true">
574
+ {SmartDropdownResources.selectedTitle}
575
+ {' '}
576
+ (
577
+ {this.value.length}
578
+ )
579
+ </div>
580
+ {this.pinnedSelectedItems.map(item => this.renderListItem(item, getIndex()))}
581
+ </div>
582
+ )}
583
+
584
+ {/* 2. Custom Sections */}
585
+ {this.customSections.map(section => (
586
+ <div class="list-group section-custom" key={section.id} role="group" aria-label={section.title || 'Section'}>
587
+ {section.title && <div class="list-group-label" aria-hidden="true">{section.title}</div>}
588
+ {section.items.map(item => this.renderListItem(item, getIndex()))}
589
+ </div>
590
+ ))}
591
+
592
+ {/* 3. Standard Categories */}
593
+ {hasStandard && (
594
+ <div class="list-group section-categories" role="group" aria-label={SmartDropdownResources.categoriesTitle}>
595
+ <div class="list-group-label" aria-hidden="true">{SmartDropdownResources.categoriesTitle}</div>
596
+ {this.standardDisplayItems.map(item => this.renderListItem(item, getIndex()))}
597
+ </div>
598
+ )}
599
+ </div>
600
+ );
601
+ }
602
+
603
+ renderListItem(item: SmartDropdownItem, index: number) {
604
+ const isSelected = this.isSelected(item);
605
+ const isFocused = this.focusedIndex === index;
606
+ const uniqueId = `${this.uid}-option-${index}`;
607
+
608
+ // Wrapper properties for accessibility
609
+ const wrapperProps = {
610
+ 'id': uniqueId,
611
+ 'class': {
612
+ 'list-item-custom-container': !!this.customSectionRender,
613
+ 'list-item': !this.customSectionRender,
614
+ 'selected': isSelected,
615
+ 'focused': isFocused, // Visual focus state
616
+ 'no-checkbox': !this.multiselect,
617
+ },
618
+ 'role': 'option',
619
+ 'aria-selected': isSelected,
620
+ 'onClick': () => this.handleItemClick(item),
621
+ 'key': item.id,
622
+ };
623
+
624
+ // 1. Custom Renderer
625
+ if (this.customSectionRender) {
626
+ return (
627
+ <div {...wrapperProps}>
628
+ {this.customSectionRender(item, isSelected)}
629
+ </div>
630
+ );
631
+ }
632
+
633
+ // 2. Default Renderer
634
+ const searchResult = item as SmartDropdownSearchResultItem;
635
+ const hasSubtitle = !!searchResult.subtitle;
636
+ const hasImage = !!searchResult.imageUrl;
637
+ const showCheckbox = this.multiselect && !this.isSearchActive && !hasImage;
638
+
639
+ return (
640
+ <div {...wrapperProps}>
641
+ {hasImage
642
+ ? (
643
+ <img src={searchResult.imageUrl} class="result-image" alt="" />
644
+ )
645
+ : (
646
+ showCheckbox && <div class={`checkbox-visual ${isSelected ? 'checked' : ''}`} aria-hidden="true"></div>
647
+ )}
648
+
649
+ <div class="item-content">
650
+ <span class="item-text">{item.text}</span>
651
+ {hasSubtitle && (
652
+ <span class="item-subtitle">{searchResult.subtitle}</span>
653
+ )}
654
+ </div>
655
+ </div>
656
+ );
657
+ }
658
+
659
+ render() {
660
+ const showCaret = this.searchMode !== 'input' && !this.customTriggerRender;
661
+
662
+ return (
663
+ <FormItemWrapper label={this.label} cssClass={this.cssClass} mandatory={this.mandatory} wrap={this.wrap} appendIcon={this.appendIcon} prependIcon={this.prependIcon} hint={this.hint} marginType={this.marginType} appendClicked={this.appendClicked} prependClicked={this.prependClicked} prependIconClicked={this.prependIconClicked} appendIconClicked={this.appendIconClicked} maxWidth={this.maxWidth} validationState={this.validationState} labelButtons={this.labelButtons} subtitle={this.subtitle} showClearValueButton={this.showClearValueButton}>
664
+ <div
665
+ class={`gopass-filter-wrapper mode-${this.searchMode}`}
666
+ // Only the outer wrapper acts as combobox if not in direct input mode
667
+ role={this.searchMode !== 'input' ? 'combobox' : undefined}
668
+ aria-expanded={this.isOpen}
669
+ aria-haspopup="listbox"
670
+ aria-controls={this.listboxId}
671
+ aria-label={this.placeholder}
672
+ >
673
+
674
+ {/* TRIGGER */}
675
+ <div
676
+ class={{ 'filter-trigger': true, 'is-open': this.isOpen }}
677
+ onClick={() => this.toggleDropdown(true)}
678
+ tabindex={this.searchMode === 'input' && !this.customTriggerRender ? undefined : 0}
679
+ >
680
+ {this.renderTrigger()}
681
+ {showCaret && <span class="caret" aria-hidden="true"></span>}
682
+ </div>
683
+
684
+ {/* DROPDOWN */}
685
+ {this.isOpen && (
686
+ <div class="filter-dropdown" onClick={e => e.stopPropagation()}>
687
+
688
+ {/* INTERNAL HEADER */}
689
+ <div class="search-header internal-search-header">
690
+ <input
691
+ type="text"
692
+ class="search-input dropdown-search-input"
693
+ placeholder={this.placeholder}
694
+ value={this.triggerInputValue}
695
+ onInput={this.handleInput}
696
+ // Accessibility for internal input
697
+ aria-autocomplete="list"
698
+ aria-controls={this.listboxId}
699
+ aria-activedescendant={this.activeDescendantId}
700
+ />
701
+ {this.buttonLayout === 'inline' && (
702
+ <button
703
+ class="btn-confirm"
704
+ disabled={this.isLoading}
705
+ onClick={() => this.confirmAndClose()}
706
+ >
707
+ {this.isLoading ? SmartDropdownResources.loadingTextShort : SmartDropdownResources.doneButtonText}
708
+ </button>
709
+ )}
710
+ </div>
711
+
712
+ {/* LIST */}
713
+ <div
714
+ class="list-container"
715
+ id={this.listboxId}
716
+ role="listbox"
717
+ aria-multiselectable={this.multiselect}
718
+ >
719
+ {this.renderList()}
720
+ </div>
721
+
722
+ {/* FOOTER */}
723
+ {this.buttonLayout === 'footer' && (
724
+ <div class="dropdown-footer">
725
+ <button
726
+ class="btn-confirm"
727
+ onClick={() => this.confirmAndClose()}
728
+ >
729
+ {SmartDropdownResources.doneButtonText}
730
+ </button>
731
+ </div>
732
+ )}
733
+ </div>
734
+ )}
735
+ </div>
736
+ </FormItemWrapper>
737
+ );
738
+ }
739
+ }
@@ -60,6 +60,12 @@
60
60
  max-width: 650px;
61
61
  }
62
62
 
63
+ @media (min-width: 768px) {
64
+ .modal-dialog.modal-stn {
65
+ max-width: 400px;
66
+ }
67
+ }
68
+
63
69
  @media (max-width: 767.99px) {
64
70
  .modal-bottom-sheet .modal-dialog {
65
71
  position: fixed !important;
@@ -42,6 +42,7 @@ export enum ModalSize {
42
42
  ExtraLarge = 3,
43
43
  FullWidth = 4,
44
44
  NormalToLarge = 5,
45
+ SmallToNormal = 6,
45
46
  }
46
47
 
47
48
  export enum ModalHeaderIcon {
@@ -104,6 +105,8 @@ class ModalComponent extends TsxComponent<ModalArgs> implements ModalArgs {
104
105
  return ' modal-fw';
105
106
  } else if (this.size == ModalSize.NormalToLarge) {
106
107
  return ' modal-ntl';
108
+ } else if (this.size == ModalSize.SmallToNormal) {
109
+ return ' modal-stn';
107
110
  } else {
108
111
  return '';
109
112
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "inviton-powerduck",
3
3
  "type": "module",
4
- "version": "0.0.216",
4
+ "version": "0.0.218",
5
5
  "files": [
6
6
  "app/",
7
7
  "common/",