@origostudio/origo-design 0.0.49 → 0.0.51

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 (98) hide show
  1. package/CHANGELOG.md +6 -4
  2. package/dist/packages/angular-renderer/CHANGELOG.md +22 -0
  3. package/dist/packages/angular-renderer/package.json +3 -3
  4. package/dist/packages/angular-renderer/src/components/primitives/autocomplete/autocomplete.component.d.ts +99 -0
  5. package/dist/packages/angular-renderer/src/components/primitives/autocomplete/autocomplete.component.js +306 -0
  6. package/dist/packages/angular-renderer/src/components/primitives/autocomplete/autocomplete.component.js.map +1 -0
  7. package/dist/packages/angular-renderer/src/components/primitives/combobox/combobox.component.d.ts +69 -0
  8. package/dist/packages/angular-renderer/src/components/primitives/combobox/combobox.component.js +276 -0
  9. package/dist/packages/angular-renderer/src/components/primitives/combobox/combobox.component.js.map +1 -0
  10. package/dist/packages/angular-renderer/src/components/primitives/date-picker/date-picker.component.d.ts +57 -0
  11. package/dist/packages/angular-renderer/src/components/primitives/date-picker/date-picker.component.js +257 -0
  12. package/dist/packages/angular-renderer/src/components/primitives/date-picker/date-picker.component.js.map +1 -0
  13. package/dist/packages/angular-renderer/src/components/primitives/date-range-picker/date-range-picker.component.d.ts +67 -0
  14. package/dist/packages/angular-renderer/src/components/primitives/date-range-picker/date-range-picker.component.js +299 -0
  15. package/dist/packages/angular-renderer/src/components/primitives/date-range-picker/date-range-picker.component.js.map +1 -0
  16. package/dist/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.d.ts +110 -0
  17. package/dist/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.js +323 -0
  18. package/dist/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.js.map +1 -0
  19. package/dist/packages/angular-renderer/src/components/primitives/select/select.component.d.ts +80 -19
  20. package/dist/packages/angular-renderer/src/components/primitives/select/select.component.js +201 -24
  21. package/dist/packages/angular-renderer/src/components/primitives/select/select.component.js.map +1 -1
  22. package/dist/packages/angular-renderer/src/components/primitives/select/selection-utils.d.ts +9 -0
  23. package/dist/packages/angular-renderer/src/components/primitives/select/selection-utils.js +69 -0
  24. package/dist/packages/angular-renderer/src/components/primitives/select/selection-utils.js.map +1 -0
  25. package/dist/packages/angular-renderer/src/components/primitives/time-picker/time-picker.component.d.ts +48 -0
  26. package/dist/packages/angular-renderer/src/components/primitives/time-picker/time-picker.component.js +214 -0
  27. package/dist/packages/angular-renderer/src/components/primitives/time-picker/time-picker.component.js.map +1 -0
  28. package/dist/packages/angular-renderer/src/index.d.ts +6 -0
  29. package/dist/packages/angular-renderer/src/index.js +6 -0
  30. package/dist/packages/angular-renderer/src/index.js.map +1 -1
  31. package/dist/packages/angular-renderer/src/lib/primitives.provider.js +12 -0
  32. package/dist/packages/angular-renderer/src/lib/primitives.provider.js.map +1 -1
  33. package/dist/packages/cli/CHANGELOG.md +8 -0
  34. package/dist/packages/cli/package.json +2 -2
  35. package/dist/packages/cli/package.json.bak +2 -2
  36. package/dist/packages/core/CHANGELOG.md +8 -0
  37. package/dist/packages/core/package.json +1 -1
  38. package/dist/packages/core/package.json.bak +1 -1
  39. package/dist/packages/design-tokens/CHANGELOG.md +8 -0
  40. package/dist/packages/design-tokens/package.json +1 -1
  41. package/dist/packages/playground/browser/{chunk-B_swdRuc.js → chunk-2fyh0Go5.js} +1 -1
  42. package/dist/packages/playground/browser/{chunk-C8l8Pst_.js → chunk-6qDdu3iP.js} +1 -1
  43. package/dist/packages/playground/browser/{chunk-G1sQXMvG.js → chunk-BQHgLM1h.js} +1 -1
  44. package/dist/packages/playground/browser/{chunk-Cb2ALrKC.js → chunk-BQrkBjEv.js} +1 -1
  45. package/dist/packages/playground/browser/{chunk-BbeNQQ9Z.js → chunk-BSIGVQT8.js} +1 -1
  46. package/dist/packages/playground/browser/{chunk-BlCRDoSd.js → chunk-CD79k8Eb.js} +1 -1
  47. package/dist/packages/playground/browser/{chunk-CCE_vAje.js → chunk-D6mKAQPx.js} +1 -1
  48. package/dist/packages/playground/browser/{chunk-DoYDPF2-.js → chunk-DBZa4hap.js} +1 -1
  49. package/dist/packages/playground/browser/{chunk-CpaI4VGh.js → chunk-DGb2Oa6d.js} +1 -1
  50. package/dist/packages/playground/browser/{chunk-B8mwHWgW.js → chunk-DPEeny3K.js} +1 -1
  51. package/dist/packages/playground/browser/{chunk-DgLZ-dIi.js → chunk-DeS5TtZS.js} +1 -1
  52. package/dist/packages/playground/browser/{chunk-r4-UbXQJ.js → chunk-DnW3HJ2k.js} +1 -1
  53. package/dist/packages/playground/browser/{chunk-lFsGUZ3A.js → chunk-V8VJI0nO.js} +1 -1
  54. package/dist/packages/playground/browser/{chunk-XTbjIUbx.js → chunk-mGxxpk2V.js} +1 -1
  55. package/dist/packages/playground/browser/{chunk-DkxgAarv.js → chunk-vt9o3pSK.js} +1 -1
  56. package/dist/packages/playground/browser/index.html +1 -1
  57. package/dist/packages/playground/browser/main-6RRVB3JV.js +740 -0
  58. package/package.json +1 -1
  59. package/packages/angular-renderer/CHANGELOG.md +22 -0
  60. package/packages/angular-renderer/package.json +3 -3
  61. package/packages/angular-renderer/src/components/primitives/autocomplete/autocomplete.component.html +83 -0
  62. package/packages/angular-renderer/src/components/primitives/autocomplete/autocomplete.component.scss +161 -0
  63. package/packages/angular-renderer/src/components/primitives/autocomplete/autocomplete.component.ts +338 -0
  64. package/packages/angular-renderer/src/components/primitives/combobox/combobox.component.html +90 -0
  65. package/packages/angular-renderer/src/components/primitives/combobox/combobox.component.scss +189 -0
  66. package/packages/angular-renderer/src/components/primitives/combobox/combobox.component.ts +281 -0
  67. package/packages/angular-renderer/src/components/primitives/date-picker/date-picker.component.html +73 -0
  68. package/packages/angular-renderer/src/components/primitives/date-picker/date-picker.component.scss +77 -0
  69. package/packages/angular-renderer/src/components/primitives/date-picker/date-picker.component.ts +294 -0
  70. package/packages/angular-renderer/src/components/primitives/date-range-picker/date-range-picker.component.html +83 -0
  71. package/packages/angular-renderer/src/components/primitives/date-range-picker/date-range-picker.component.scss +92 -0
  72. package/packages/angular-renderer/src/components/primitives/date-range-picker/date-range-picker.component.ts +357 -0
  73. package/packages/angular-renderer/src/components/primitives/icon-button/icon-button.component.html +1 -1
  74. package/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.html +150 -0
  75. package/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.scss +240 -0
  76. package/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.ts +362 -0
  77. package/packages/angular-renderer/src/components/primitives/select/select.component.html +92 -14
  78. package/packages/angular-renderer/src/components/primitives/select/select.component.scss +188 -13
  79. package/packages/angular-renderer/src/components/primitives/select/select.component.ts +242 -34
  80. package/packages/angular-renderer/src/components/primitives/select/selection-utils.ts +92 -0
  81. package/packages/angular-renderer/src/components/primitives/time-picker/time-picker.component.html +65 -0
  82. package/packages/angular-renderer/src/components/primitives/time-picker/time-picker.component.scss +65 -0
  83. package/packages/angular-renderer/src/components/primitives/time-picker/time-picker.component.ts +229 -0
  84. package/packages/angular-renderer/src/index.ts +6 -0
  85. package/packages/angular-renderer/src/lib/primitives.provider.ts +12 -0
  86. package/packages/cli/CHANGELOG.md +8 -0
  87. package/packages/cli/package.json +2 -2
  88. package/packages/core/CHANGELOG.md +8 -0
  89. package/packages/core/package.json +1 -1
  90. package/packages/design-tokens/CHANGELOG.md +8 -0
  91. package/packages/design-tokens/package.json +1 -1
  92. package/packages/devtools/CHANGELOG.md +14 -0
  93. package/packages/devtools/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  94. package/packages/devtools/package.json +3 -3
  95. package/packages/playground/CHANGELOG.md +8 -0
  96. package/packages/playground/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  97. package/packages/playground/package.json +3 -3
  98. package/dist/packages/playground/browser/main-D2PNGI4C.js +0 -734
@@ -0,0 +1,240 @@
1
+ :host {
2
+ display: inline-block;
3
+ position: relative;
4
+ box-sizing: border-box;
5
+ font-family: var(--origo-typography-input-font-family);
6
+ font-size: var(--origo-typography-input-font-size);
7
+ font-weight: var(--origo-typography-input-font-weight);
8
+ line-height: var(--origo-typography-input-line-height);
9
+ }
10
+
11
+ :host(.origo-multi-select--fluid) {
12
+ display: block;
13
+ width: 100%;
14
+ }
15
+
16
+ .origo-multi-select__wrapper {
17
+ display: flex;
18
+ flex-direction: column;
19
+ position: relative;
20
+ width: 100%;
21
+ }
22
+
23
+ .origo-multi-select__control {
24
+ display: flex;
25
+ width: 100%;
26
+ }
27
+
28
+ .origo-multi-select__trigger {
29
+ box-sizing: border-box;
30
+ width: 100%;
31
+ min-height: 2.5rem;
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: space-between;
35
+ background-color: var(--origo-color-surface-background);
36
+ color: var(--origo-color-text-primary);
37
+ padding: var(--origo-spacing-container-padding);
38
+ border: var(--origo-border-width-default) solid var(--origo-color-border-default);
39
+ border-radius: var(--origo-radius-sm);
40
+ box-shadow: var(--origo-shadow-sm);
41
+ font-family: inherit;
42
+ font-size: inherit;
43
+ text-align: start;
44
+ cursor: pointer;
45
+ transition: all var(--origo-duration-fast) ease-in-out;
46
+
47
+ &:hover:not(:disabled) {
48
+ border-color: var(--origo-color-border-hover);
49
+ }
50
+
51
+ &:active:not(:disabled) {
52
+ border-color: var(--origo-color-border-active);
53
+ }
54
+
55
+ &:focus,
56
+ &:focus-visible {
57
+ outline: var(--origo-border-width-thick) solid var(--origo-color-focus);
58
+ outline-offset: var(--origo-border-offset-negative);
59
+ border-color: var(--origo-color-focus);
60
+ }
61
+
62
+ &:disabled {
63
+ cursor: not-allowed;
64
+ opacity: var(--origo-opacity-disabled);
65
+ background-color: var(--origo-color-surface-disabled);
66
+ color: var(--origo-color-text-disabled);
67
+ }
68
+ }
69
+
70
+ :host(.origo-multi-select--filled) .origo-multi-select__trigger {
71
+ background-color: var(--origo-color-surface-subtle);
72
+ border: none;
73
+ }
74
+
75
+ :host(.origo-multi-select--invalid) .origo-multi-select__trigger {
76
+ border-color: var(--origo-color-text-danger);
77
+ }
78
+
79
+ .origo-multi-select__display {
80
+ flex: 1;
81
+ overflow: hidden;
82
+ }
83
+
84
+ .origo-multi-select__label {
85
+ display: block;
86
+ overflow: hidden;
87
+ text-overflow: ellipsis;
88
+ white-space: nowrap;
89
+
90
+ &--placeholder {
91
+ color: var(--origo-color-text-muted);
92
+ }
93
+ }
94
+
95
+ .origo-multi-select__chips {
96
+ display: flex;
97
+ flex-wrap: wrap;
98
+ gap: var(--origo-spacing-xs);
99
+ }
100
+
101
+ .origo-multi-select__chip {
102
+ display: inline-flex;
103
+ align-items: center;
104
+ gap: var(--origo-spacing-xs);
105
+ padding: var(--origo-spacing-xs) var(--origo-spacing-sm);
106
+ background-color: var(--origo-color-surface-subtle);
107
+ color: var(--origo-color-text-primary);
108
+ border-radius: var(--origo-radius-sm);
109
+ font-size: var(--origo-typography-caption-font-size);
110
+ }
111
+
112
+ .origo-multi-select__chip-remove {
113
+ cursor: pointer;
114
+ color: var(--origo-color-text-muted);
115
+
116
+ &:hover {
117
+ color: var(--origo-color-text-primary);
118
+ }
119
+ }
120
+
121
+ .origo-multi-select__icons {
122
+ display: inline-flex;
123
+ align-items: center;
124
+ gap: var(--origo-spacing-xs);
125
+ margin-inline-start: var(--origo-spacing-sm);
126
+ }
127
+
128
+ .origo-multi-select__clear {
129
+ cursor: pointer;
130
+ color: var(--origo-color-text-muted);
131
+ font-size: var(--origo-typography-caption-font-size);
132
+
133
+ &:hover {
134
+ color: var(--origo-color-text-primary);
135
+ }
136
+ }
137
+
138
+ .origo-multi-select__arrow {
139
+ color: var(--origo-color-text-muted);
140
+ font-size: var(--origo-typography-caption-font-size);
141
+ }
142
+
143
+ .origo-multi-select__dropdown {
144
+ position: absolute;
145
+ top: 100%;
146
+ left: 0;
147
+ right: 0;
148
+ z-index: 1000;
149
+ margin-top: var(--origo-spacing-xs);
150
+ box-sizing: border-box;
151
+ background-color: var(--origo-color-surface-background);
152
+ border: var(--origo-border-width-default) solid var(--origo-color-border-default);
153
+ border-radius: var(--origo-radius-sm);
154
+ box-shadow: var(--origo-shadow-md);
155
+ max-height: 16rem;
156
+ overflow-y: auto;
157
+ }
158
+
159
+ .origo-multi-select__filter-input {
160
+ box-sizing: border-box;
161
+ width: 100%;
162
+ padding: var(--origo-spacing-xs);
163
+ background-color: var(--origo-color-surface-background);
164
+ color: var(--origo-color-text-primary);
165
+ border: var(--origo-border-width-default) solid var(--origo-color-border-default);
166
+ border-radius: var(--origo-radius-sm);
167
+ font-family: inherit;
168
+ font-size: inherit;
169
+
170
+ &:focus {
171
+ outline: var(--origo-border-width-thick) solid var(--origo-color-focus);
172
+ outline-offset: var(--origo-border-offset-negative);
173
+ }
174
+ }
175
+
176
+ .origo-multi-select__toggle-all {
177
+ display: flex;
178
+ align-items: center;
179
+ gap: var(--origo-spacing-xs);
180
+ padding: var(--origo-spacing-xs) var(--origo-spacing-container-padding);
181
+ border-bottom: var(--origo-border-width-default) solid var(--origo-color-border-default);
182
+ font-weight: bold;
183
+ font-size: var(--origo-typography-caption-font-size);
184
+ color: var(--origo-color-text-primary);
185
+ cursor: pointer;
186
+ background-color: var(--origo-color-surface-subtle);
187
+ }
188
+
189
+ .origo-multi-select__options {
190
+ list-style: none;
191
+ margin: 0;
192
+ padding: 0;
193
+ }
194
+
195
+ .origo-multi-select__option {
196
+ display: flex;
197
+ align-items: center;
198
+ gap: var(--origo-spacing-xs);
199
+ padding: var(--origo-spacing-container-padding);
200
+ color: var(--origo-color-text-primary);
201
+ cursor: pointer;
202
+ transition: background-color var(--origo-duration-fast) ease-in-out;
203
+
204
+ &:hover,
205
+ &--active {
206
+ background-color: var(--origo-color-surface-subtle);
207
+ }
208
+
209
+ &--selected {
210
+ background-color: var(--origo-color-surface-subtle);
211
+ }
212
+
213
+ &--disabled {
214
+ opacity: var(--origo-opacity-disabled);
215
+ cursor: not-allowed;
216
+ }
217
+ }
218
+
219
+ .origo-multi-select__checkbox {
220
+ margin: 0;
221
+ accent-color: var(--origo-color-focus);
222
+ }
223
+
224
+ .origo-multi-select__no-options {
225
+ padding: var(--origo-spacing-container-padding);
226
+ color: var(--origo-color-text-muted);
227
+ text-align: center;
228
+ }
229
+
230
+ .origo-multi-select__error {
231
+ color: var(--origo-color-text-danger);
232
+ font-size: var(--origo-typography-caption-font-size);
233
+ margin-top: var(--origo-spacing-xs);
234
+ }
235
+
236
+ .origo-multi-select__help {
237
+ color: var(--origo-color-text-muted);
238
+ font-size: var(--origo-typography-caption-font-size);
239
+ margin-top: var(--origo-spacing-xs);
240
+ }
@@ -0,0 +1,362 @@
1
+ import {
2
+ Component,
3
+ input,
4
+ model,
5
+ ChangeDetectionStrategy,
6
+ computed,
7
+ ViewEncapsulation,
8
+ inject,
9
+ effect,
10
+ untracked,
11
+ signal,
12
+ PLATFORM_ID,
13
+ HostListener,
14
+ ElementRef,
15
+ } from '@angular/core';
16
+ import { isPlatformBrowser } from '@angular/common';
17
+ import { InteractionContract } from '@origostudio/core';
18
+ import { OrigoAdapter } from '../../../adapters/web/adapter';
19
+ import { WebExperienceAdapterService } from '../../../adapters/web/experience-adapter.service';
20
+ import { NormalizedOption, normalizeOptions, filterOptions } from '../select/selection-utils';
21
+
22
+ export interface MultiSelectProps {
23
+ id?: string;
24
+ value?: Array<unknown>;
25
+ options: Array<unknown>;
26
+ optionLabel?: string;
27
+ optionValue?: string;
28
+ placeholder?: string;
29
+ filter?: boolean;
30
+ filterBy?: string;
31
+ filterPlaceholder?: string;
32
+ showToggleAll?: boolean;
33
+ maxSelectedLabels?: number;
34
+ selectedItemsLabel?: string;
35
+ display?: 'comma' | 'chip';
36
+ clearable?: boolean;
37
+ virtualScroll?: boolean;
38
+ itemSize?: number;
39
+ loading?: boolean;
40
+ disabled?: boolean;
41
+ readonly?: boolean;
42
+ required?: boolean;
43
+ invalid?: boolean;
44
+ errorText?: string;
45
+ helpText?: string;
46
+ variant?: 'outlined' | 'filled';
47
+ fluid?: boolean;
48
+ 'aria-label'?: string;
49
+ 'aria-describedby'?: string;
50
+ permissions?: Record<string, string>;
51
+ rules?: Record<string, unknown>;
52
+ metadata?: Record<string, unknown>;
53
+ }
54
+
55
+ @Component({
56
+ selector: 'origo-multi-select',
57
+ standalone: true,
58
+ templateUrl: './multi-select.component.html',
59
+ styleUrls: ['./multi-select.component.scss'],
60
+ changeDetection: ChangeDetectionStrategy.OnPush,
61
+ encapsulation: ViewEncapsulation.ShadowDom,
62
+ host: {
63
+ '[class.origo-multi-select]': 'true',
64
+ '[attr.data-testid]': 'contract().id ?? ""',
65
+ '[class.origo-multi-select--fluid]': 'computedFluid()',
66
+ '[class.origo-multi-select--outlined]': 'computedVariant() === "outlined"',
67
+ '[class.origo-multi-select--filled]': 'computedVariant() === "filled"',
68
+ '[class.origo-multi-select--invalid]': 'computedInvalid()',
69
+ '[class.origo-multi-select--disabled]': 'computedDisabled()',
70
+ '[class.origo-multi-select--readonly]': 'computedReadonly()',
71
+ },
72
+ })
73
+ export class MultiSelectComponent implements OrigoAdapter<MultiSelectProps> {
74
+ static readonly contractSchema = {
75
+ permissions: 'object',
76
+ rules: 'object',
77
+ metadata: 'object',
78
+ options: 'array',
79
+ value: 'array',
80
+ optionLabel: 'string',
81
+ optionValue: 'string',
82
+ placeholder: 'string',
83
+ filter: 'boolean',
84
+ filterBy: 'string',
85
+ filterPlaceholder: 'string',
86
+ showToggleAll: 'boolean',
87
+ maxSelectedLabels: 'number',
88
+ selectedItemsLabel: 'string',
89
+ display: 'string',
90
+ clearable: 'boolean',
91
+ virtualScroll: 'boolean',
92
+ itemSize: 'number',
93
+ loading: 'boolean',
94
+ disabled: 'boolean',
95
+ readonly: 'boolean',
96
+ required: 'boolean',
97
+ invalid: 'boolean',
98
+ errorText: 'string',
99
+ helpText: 'string',
100
+ variant: 'string',
101
+ fluid: 'boolean',
102
+ 'aria-label': 'string',
103
+ 'aria-describedby': 'string',
104
+ };
105
+ static readonly strictContract = false;
106
+
107
+ contract = input.required<InteractionContract<MultiSelectProps>>();
108
+ value = model<Array<unknown>>([]);
109
+
110
+ isOpen = signal<boolean>(false);
111
+ filterQuery = signal<string>('');
112
+ activeIndex = signal<number>(-1);
113
+
114
+ computedNormalizedOptions = computed(() => {
115
+ const props = this.contract().props;
116
+ return normalizeOptions(props?.options, props?.optionLabel, props?.optionValue);
117
+ });
118
+
119
+ filteredOptions = computed(() => {
120
+ const opts = this.computedNormalizedOptions();
121
+ if (!this.computedFilter() && !this.filterQuery()) {
122
+ return opts;
123
+ }
124
+ return filterOptions(opts, this.filterQuery(), 'contains');
125
+ });
126
+
127
+ computedSelectedOptions = computed(() => {
128
+ const current = new Set(this.value());
129
+ return this.computedNormalizedOptions().filter(opt => current.has(opt.value));
130
+ });
131
+
132
+ computedPlaceholder = computed(() => this.contract().props?.placeholder ?? 'Select options');
133
+ computedDisabled = computed(() => !!this.contract().props?.disabled);
134
+ computedReadonly = computed(() => !!this.contract().props?.readonly);
135
+ computedRequired = computed(() => !!this.contract().props?.required);
136
+ computedInvalid = computed(() => !!this.contract().props?.invalid);
137
+ computedFilter = computed(() => !!this.contract().props?.filter);
138
+ computedFilterPlaceholder = computed(
139
+ () => this.contract().props?.filterPlaceholder ?? 'Search...'
140
+ );
141
+ computedShowToggleAll = computed(() => this.contract().props?.showToggleAll ?? true);
142
+ computedClearable = computed(() => !!this.contract().props?.clearable);
143
+ computedDisplay = computed(() => this.contract().props?.display ?? 'comma');
144
+ computedMaxSelectedLabels = computed(() => this.contract().props?.maxSelectedLabels ?? 3);
145
+ computedSelectedItemsLabel = computed(
146
+ () => this.contract().props?.selectedItemsLabel ?? '{0} items selected'
147
+ );
148
+ computedVariant = computed(() => this.contract().props?.variant ?? 'outlined');
149
+ computedFluid = computed(() => !!this.contract().props?.fluid);
150
+ computedErrorText = computed(() => this.contract().props?.errorText ?? '');
151
+ computedHelpText = computed(() => this.contract().props?.helpText ?? '');
152
+
153
+ computedAriaLabel = computed(() => {
154
+ const label = this.contract().props?.['aria-label'];
155
+ if (label !== undefined && label !== null && String(label).trim() !== '') {
156
+ return String(label);
157
+ }
158
+ return this.computedPlaceholder();
159
+ });
160
+
161
+ computedAriaDescribedBy = computed(() => {
162
+ const parts: string[] = [];
163
+ const desc = this.contract().props?.['aria-describedby'];
164
+ if (desc) parts.push(String(desc));
165
+ if (this.computedErrorText()) parts.push(`${this.contract().id}-error`);
166
+ if (this.computedHelpText()) parts.push(`${this.contract().id}-help`);
167
+ return parts.length > 0 ? parts.join(' ') : undefined;
168
+ });
169
+
170
+ displaySummary = computed(() => {
171
+ const selected = this.computedSelectedOptions();
172
+ const count = selected.length;
173
+ if (count === 0) return '';
174
+ const max = this.computedMaxSelectedLabels();
175
+ if (count <= max) {
176
+ return selected.map(s => s.label).join(', ');
177
+ }
178
+ const template = this.computedSelectedItemsLabel();
179
+ return template.replace('{0}', String(count));
180
+ });
181
+
182
+ isAllSelected = computed(() => {
183
+ const filtered = this.filteredOptions().filter(o => !o.disabled);
184
+ if (filtered.length === 0) return false;
185
+ const current = new Set(this.value());
186
+ return filtered.every(o => current.has(o.value));
187
+ });
188
+
189
+ private experienceAdapter = inject(WebExperienceAdapterService);
190
+ private platformId = inject(PLATFORM_ID);
191
+ private elementRef = inject(ElementRef);
192
+
193
+ constructor() {
194
+ effect(() => {
195
+ const contractVal = this.contract().props?.value;
196
+ untracked(() => {
197
+ if (Array.isArray(contractVal)) {
198
+ const current = this.value();
199
+ if (
200
+ current.length !== contractVal.length ||
201
+ !current.every((v, i) => v === contractVal[i])
202
+ ) {
203
+ this.value.set([...contractVal]);
204
+ }
205
+ }
206
+ });
207
+ });
208
+ }
209
+
210
+ toggleOpen() {
211
+ if (this.computedDisabled() || this.computedReadonly()) return;
212
+ const next = !this.isOpen();
213
+ this.isOpen.set(next);
214
+ if (next) {
215
+ this.filterQuery.set('');
216
+ this.activeIndex.set(0);
217
+ }
218
+ }
219
+
220
+ isSelected(opt: NormalizedOption): boolean {
221
+ return this.value().includes(opt.value);
222
+ }
223
+
224
+ toggleOption(optionValue: unknown) {
225
+ if (this.computedDisabled() || this.computedReadonly()) return;
226
+ const current = [...this.value()];
227
+ const idx = current.indexOf(optionValue);
228
+ if (idx >= 0) {
229
+ current.splice(idx, 1);
230
+ } else {
231
+ current.push(optionValue);
232
+ }
233
+ this.value.set(current);
234
+ this.experienceAdapter.updateState(this.contract().id, 'value', current);
235
+ }
236
+
237
+ toggleAll() {
238
+ if (this.computedDisabled() || this.computedReadonly()) return;
239
+ const filtered = this.filteredOptions().filter(o => !o.disabled);
240
+ let nextValue: Array<unknown>;
241
+
242
+ if (this.isAllSelected()) {
243
+ const filteredSet = new Set(filtered.map(o => o.value));
244
+ nextValue = this.value().filter(v => !filteredSet.has(v));
245
+ } else {
246
+ const set = new Set(this.value());
247
+ for (const opt of filtered) {
248
+ set.add(opt.value);
249
+ }
250
+ nextValue = Array.from(set);
251
+ }
252
+
253
+ this.value.set(nextValue);
254
+ this.experienceAdapter.updateState(this.contract().id, 'value', nextValue);
255
+ }
256
+
257
+ removeOption(optionValue: unknown, event?: Event) {
258
+ event?.stopPropagation();
259
+ if (this.computedDisabled() || this.computedReadonly()) return;
260
+ const next = this.value().filter(v => v !== optionValue);
261
+ this.value.set(next);
262
+ this.experienceAdapter.updateState(this.contract().id, 'value', next);
263
+ }
264
+
265
+ clear(event?: Event) {
266
+ if (this.computedDisabled() || this.computedReadonly()) return;
267
+ event?.stopPropagation();
268
+ this.value.set([]);
269
+ this.experienceAdapter.updateState(this.contract().id, 'value', []);
270
+ }
271
+
272
+ onFilterInput(event: Event) {
273
+ const target = event.target as HTMLInputElement;
274
+ this.filterQuery.set(target.value);
275
+ this.activeIndex.set(0);
276
+ }
277
+
278
+ onKeydown(event: KeyboardEvent) {
279
+ if (this.computedDisabled() || this.computedReadonly()) return;
280
+
281
+ const opts = this.filteredOptions();
282
+ const count = opts.length;
283
+
284
+ switch (event.key) {
285
+ case 'ArrowDown': {
286
+ event.preventDefault();
287
+ if (!this.isOpen()) {
288
+ this.isOpen.set(true);
289
+ this.activeIndex.set(0);
290
+ } else if (count > 0) {
291
+ const next = (this.activeIndex() + 1) % count;
292
+ this.activeIndex.set(next);
293
+ }
294
+ break;
295
+ }
296
+ case 'ArrowUp': {
297
+ event.preventDefault();
298
+ if (!this.isOpen()) {
299
+ this.isOpen.set(true);
300
+ this.activeIndex.set(count - 1);
301
+ } else if (count > 0) {
302
+ const prev = (this.activeIndex() - 1 + count) % count;
303
+ this.activeIndex.set(prev);
304
+ }
305
+ break;
306
+ }
307
+ case 'Enter':
308
+ case ' ': {
309
+ const target = event.target as HTMLElement;
310
+ if (event.key === ' ' && target && target.tagName === 'INPUT') {
311
+ return;
312
+ }
313
+ event.preventDefault();
314
+ if (this.isOpen()) {
315
+ const idx = this.activeIndex();
316
+ if (idx >= 0 && idx < count && !opts[idx].disabled) {
317
+ this.toggleOption(opts[idx].value);
318
+ }
319
+ } else {
320
+ this.isOpen.set(true);
321
+ }
322
+ break;
323
+ }
324
+ case 'Escape': {
325
+ if (this.isOpen()) {
326
+ event.preventDefault();
327
+ this.isOpen.set(false);
328
+ }
329
+ break;
330
+ }
331
+ case 'Tab': {
332
+ if (this.isOpen()) {
333
+ this.isOpen.set(false);
334
+ }
335
+ break;
336
+ }
337
+ case 'Home': {
338
+ if (this.isOpen() && count > 0) {
339
+ event.preventDefault();
340
+ this.activeIndex.set(0);
341
+ }
342
+ break;
343
+ }
344
+ case 'End': {
345
+ if (this.isOpen() && count > 0) {
346
+ event.preventDefault();
347
+ this.activeIndex.set(count - 1);
348
+ }
349
+ break;
350
+ }
351
+ }
352
+ }
353
+
354
+ @HostListener('document:click', ['$event'])
355
+ onDocumentClick(event: MouseEvent) {
356
+ if (!isPlatformBrowser(this.platformId)) return;
357
+ const clickedInside = this.elementRef.nativeElement.contains(event.target as Node);
358
+ if (!clickedInside && this.isOpen()) {
359
+ this.isOpen.set(false);
360
+ }
361
+ }
362
+ }
@@ -1,15 +1,93 @@
1
- <select
2
- [id]="contract().id"
3
- [disabled]="computedDisabled()"
4
- [required]="computedRequired()"
5
- [attr.aria-label]="computedAriaLabel()"
6
- [attr.aria-describedby]="computedAriaDescribedBy()"
7
- (change)="onChange($event)"
8
- >
9
- @if (computedPlaceholder()) {
10
- <option value="" disabled selected hidden>{{ computedPlaceholder() }}</option>
1
+ <div class="origo-select__wrapper">
2
+ <div class="origo-select__control">
3
+ <button
4
+ type="button"
5
+ class="origo-select__trigger"
6
+ [id]="contract().id + '-trigger'"
7
+ role="combobox"
8
+ [attr.aria-expanded]="isOpen()"
9
+ [attr.aria-haspopup]="'listbox'"
10
+ [attr.aria-controls]="isOpen() ? contract().id + '-listbox' : null"
11
+ [attr.aria-activedescendant]="
12
+ isOpen() && activeIndex() >= 0 ? contract().id + '-option-' + activeIndex() : null
13
+ "
14
+ [attr.aria-invalid]="computedInvalid() ? 'true' : null"
15
+ [attr.aria-required]="computedRequired() ? 'true' : null"
16
+ [attr.aria-label]="computedAriaLabel()"
17
+ [attr.aria-describedby]="computedAriaDescribedBy()"
18
+ [disabled]="computedDisabled()"
19
+ (click)="toggleOpen()"
20
+ (keydown)="onKeydown($event)"
21
+ >
22
+ <span class="origo-select__label" [class.origo-select__label--placeholder]="!selectedLabel()">
23
+ {{ selectedLabel() || computedPlaceholder() }}
24
+ </span>
25
+ <span class="origo-select__icons">
26
+ @if (computedClearable() && value() && !computedDisabled() && !computedReadonly()) {
27
+ <span
28
+ class="origo-select__clear"
29
+ role="button"
30
+ tabindex="0"
31
+ aria-label="Clear selection"
32
+ (click)="clear($event)"
33
+ (keydown.enter)="clear($event)"
34
+ >✕</span
35
+ >
36
+ }
37
+ <span class="origo-select__arrow" aria-hidden="true">▼</span>
38
+ </span>
39
+ </button>
40
+ </div>
41
+
42
+ @if (isOpen()) {
43
+ <div
44
+ class="origo-select__dropdown"
45
+ [id]="contract().id + '-listbox'"
46
+ role="listbox"
47
+ [attr.aria-label]="computedAriaLabel()"
48
+ >
49
+ @if (computedFilter()) {
50
+ <div class="origo-select__filter-container">
51
+ <input
52
+ type="text"
53
+ class="origo-select__filter-input"
54
+ [placeholder]="computedFilterPlaceholder()"
55
+ [value]="filterQuery()"
56
+ (input)="onFilterInput($event)"
57
+ (keydown)="onKeydown($event)"
58
+ aria-label="Filter options"
59
+ />
60
+ </div>
61
+ }
62
+ <ul class="origo-select__options">
63
+ @for (opt of filteredOptions(); track opt.value; let idx = $index) {
64
+ @if (opt.group && (idx === 0 || filteredOptions()[idx - 1].group !== opt.group)) {
65
+ <li class="origo-select__group-label" role="presentation">{{ opt.group }}</li>
66
+ }
67
+ <li
68
+ class="origo-select__option"
69
+ [class.origo-select__option--selected]="opt.value === value()"
70
+ [class.origo-select__option--active]="activeIndex() === idx"
71
+ [class.origo-select__option--disabled]="opt.disabled"
72
+ [id]="contract().id + '-option-' + idx"
73
+ role="option"
74
+ [attr.aria-selected]="opt.value === value()"
75
+ [attr.aria-disabled]="opt.disabled ? 'true' : null"
76
+ (click)="!opt.disabled && selectOption(opt.value)"
77
+ >
78
+ {{ opt.label }}
79
+ </li>
80
+ } @empty {
81
+ <li class="origo-select__no-options" role="presentation">No options found</li>
82
+ }
83
+ </ul>
84
+ </div>
11
85
  }
12
- @for (option of computedOptions(); track option.value) {
13
- <option [value]="option.value" [selected]="value() === option.value">{{ option.label }}</option>
14
- }
15
- </select>
86
+ </div>
87
+
88
+ @if (computedErrorText()) {
89
+ <div class="origo-select__error" [id]="contract().id + '-error'">{{ computedErrorText() }}</div>
90
+ }
91
+ @if (computedHelpText()) {
92
+ <div class="origo-select__help" [id]="contract().id + '-help'">{{ computedHelpText() }}</div>
93
+ }