@origostudio/origo-design 0.0.49 → 0.0.50

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 (80) hide show
  1. package/CHANGELOG.md +4 -4
  2. package/dist/packages/angular-renderer/CHANGELOG.md +10 -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/multi-select/multi-select.component.d.ts +110 -0
  11. package/dist/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.js +323 -0
  12. package/dist/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.js.map +1 -0
  13. package/dist/packages/angular-renderer/src/components/primitives/select/select.component.d.ts +80 -19
  14. package/dist/packages/angular-renderer/src/components/primitives/select/select.component.js +201 -24
  15. package/dist/packages/angular-renderer/src/components/primitives/select/select.component.js.map +1 -1
  16. package/dist/packages/angular-renderer/src/components/primitives/select/selection-utils.d.ts +9 -0
  17. package/dist/packages/angular-renderer/src/components/primitives/select/selection-utils.js +69 -0
  18. package/dist/packages/angular-renderer/src/components/primitives/select/selection-utils.js.map +1 -0
  19. package/dist/packages/angular-renderer/src/index.d.ts +3 -0
  20. package/dist/packages/angular-renderer/src/index.js +3 -0
  21. package/dist/packages/angular-renderer/src/index.js.map +1 -1
  22. package/dist/packages/angular-renderer/src/lib/primitives.provider.js +6 -0
  23. package/dist/packages/angular-renderer/src/lib/primitives.provider.js.map +1 -1
  24. package/dist/packages/cli/CHANGELOG.md +4 -0
  25. package/dist/packages/cli/package.json +2 -2
  26. package/dist/packages/cli/package.json.bak +2 -2
  27. package/dist/packages/core/CHANGELOG.md +4 -0
  28. package/dist/packages/core/package.json +1 -1
  29. package/dist/packages/core/package.json.bak +1 -1
  30. package/dist/packages/design-tokens/CHANGELOG.md +4 -0
  31. package/dist/packages/design-tokens/package.json +1 -1
  32. package/dist/packages/playground/browser/{chunk-DgLZ-dIi.js → chunk--xdAQ9AM.js} +1 -1
  33. package/dist/packages/playground/browser/{chunk-CpaI4VGh.js → chunk-BDvJAz2Z.js} +1 -1
  34. package/dist/packages/playground/browser/{chunk-Cb2ALrKC.js → chunk-BfTgubd-.js} +1 -1
  35. package/dist/packages/playground/browser/{chunk-BlCRDoSd.js → chunk-C9TW72xp.js} +1 -1
  36. package/dist/packages/playground/browser/{chunk-CCE_vAje.js → chunk-CCN2KoWz.js} +1 -1
  37. package/dist/packages/playground/browser/{chunk-r4-UbXQJ.js → chunk-COiPNsUk.js} +1 -1
  38. package/dist/packages/playground/browser/{chunk-DoYDPF2-.js → chunk-CR9pdAu4.js} +1 -1
  39. package/dist/packages/playground/browser/{chunk-B_swdRuc.js → chunk-CW1H0x80.js} +1 -1
  40. package/dist/packages/playground/browser/{chunk-XTbjIUbx.js → chunk-CcRAf-SI.js} +1 -1
  41. package/dist/packages/playground/browser/{chunk-BbeNQQ9Z.js → chunk-CeWsvybE.js} +1 -1
  42. package/dist/packages/playground/browser/{chunk-C8l8Pst_.js → chunk-DB3lTZW4.js} +1 -1
  43. package/dist/packages/playground/browser/{chunk-lFsGUZ3A.js → chunk-DBaAs95p.js} +1 -1
  44. package/dist/packages/playground/browser/{chunk-B8mwHWgW.js → chunk-UVpCaxmD.js} +1 -1
  45. package/dist/packages/playground/browser/{chunk-DkxgAarv.js → chunk-rUmdxUDA.js} +1 -1
  46. package/dist/packages/playground/browser/{chunk-G1sQXMvG.js → chunk-wSEtHeaq.js} +1 -1
  47. package/dist/packages/playground/browser/index.html +1 -1
  48. package/dist/packages/playground/browser/main-QKXZMSV7.js +737 -0
  49. package/package.json +1 -1
  50. package/packages/angular-renderer/CHANGELOG.md +10 -0
  51. package/packages/angular-renderer/package.json +3 -3
  52. package/packages/angular-renderer/src/components/primitives/autocomplete/autocomplete.component.html +83 -0
  53. package/packages/angular-renderer/src/components/primitives/autocomplete/autocomplete.component.scss +161 -0
  54. package/packages/angular-renderer/src/components/primitives/autocomplete/autocomplete.component.ts +338 -0
  55. package/packages/angular-renderer/src/components/primitives/combobox/combobox.component.html +90 -0
  56. package/packages/angular-renderer/src/components/primitives/combobox/combobox.component.scss +189 -0
  57. package/packages/angular-renderer/src/components/primitives/combobox/combobox.component.ts +281 -0
  58. package/packages/angular-renderer/src/components/primitives/icon-button/icon-button.component.html +1 -1
  59. package/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.html +150 -0
  60. package/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.scss +240 -0
  61. package/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.ts +362 -0
  62. package/packages/angular-renderer/src/components/primitives/select/select.component.html +92 -14
  63. package/packages/angular-renderer/src/components/primitives/select/select.component.scss +188 -13
  64. package/packages/angular-renderer/src/components/primitives/select/select.component.ts +242 -34
  65. package/packages/angular-renderer/src/components/primitives/select/selection-utils.ts +92 -0
  66. package/packages/angular-renderer/src/index.ts +3 -0
  67. package/packages/angular-renderer/src/lib/primitives.provider.ts +6 -0
  68. package/packages/cli/CHANGELOG.md +4 -0
  69. package/packages/cli/package.json +2 -2
  70. package/packages/core/CHANGELOG.md +4 -0
  71. package/packages/core/package.json +1 -1
  72. package/packages/design-tokens/CHANGELOG.md +4 -0
  73. package/packages/design-tokens/package.json +1 -1
  74. package/packages/devtools/CHANGELOG.md +10 -0
  75. package/packages/devtools/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  76. package/packages/devtools/package.json +3 -3
  77. package/packages/playground/CHANGELOG.md +4 -0
  78. package/packages/playground/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  79. package/packages/playground/package.json +3 -3
  80. package/dist/packages/playground/browser/main-D2PNGI4C.js +0 -734
@@ -0,0 +1,90 @@
1
+ <div class="origo-combobox__wrapper">
2
+ <div class="origo-combobox__control">
3
+ <input
4
+ type="text"
5
+ class="origo-combobox__input"
6
+ [id]="contract().id + '-input'"
7
+ role="combobox"
8
+ aria-autocomplete="list"
9
+ [attr.aria-expanded]="isOpen()"
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
+ [placeholder]="computedPlaceholder()"
19
+ [disabled]="computedDisabled()"
20
+ [readonly]="computedReadonly()"
21
+ [value]="inputValue()"
22
+ (input)="onInput($event)"
23
+ (paste)="onPaste($event)"
24
+ (keydown)="onKeydown($event)"
25
+ />
26
+
27
+ <span class="origo-combobox__icons">
28
+ @if (computedClearable() && inputValue() && !computedDisabled() && !computedReadonly()) {
29
+ <span
30
+ class="origo-combobox__clear"
31
+ role="button"
32
+ tabindex="0"
33
+ aria-label="Clear value"
34
+ (click)="clear($event)"
35
+ (keydown.enter)="clear($event)"
36
+ >✕</span
37
+ >
38
+ }
39
+ <button
40
+ type="button"
41
+ class="origo-combobox__toggle"
42
+ tabindex="-1"
43
+ aria-label="Toggle options"
44
+ [disabled]="computedDisabled() || computedReadonly()"
45
+ (click)="toggleOpen()"
46
+ >
47
+ <span class="origo-combobox__arrow" aria-hidden="true">▼</span>
48
+ </button>
49
+ </span>
50
+ </div>
51
+
52
+ @if (isOpen()) {
53
+ <div
54
+ class="origo-combobox__dropdown"
55
+ [id]="contract().id + '-listbox'"
56
+ role="listbox"
57
+ [attr.aria-label]="computedAriaLabel()"
58
+ >
59
+ <ul class="origo-combobox__options">
60
+ @for (opt of filteredOptions(); track opt.value; let idx = $index) {
61
+ @if (opt.group && (idx === 0 || filteredOptions()[idx - 1].group !== opt.group)) {
62
+ <li class="origo-combobox__group-label" role="presentation">{{ opt.group }}</li>
63
+ }
64
+ <li
65
+ class="origo-combobox__option"
66
+ [class.origo-combobox__option--selected]="opt.value === value()"
67
+ [class.origo-combobox__option--active]="activeIndex() === idx"
68
+ [class.origo-combobox__option--disabled]="opt.disabled"
69
+ [id]="contract().id + '-option-' + idx"
70
+ role="option"
71
+ [attr.aria-selected]="opt.value === value()"
72
+ [attr.aria-disabled]="opt.disabled ? 'true' : null"
73
+ (click)="!opt.disabled && selectOption(opt.value)"
74
+ >
75
+ {{ opt.label }}
76
+ </li>
77
+ } @empty {
78
+ <li class="origo-combobox__no-options" role="presentation">No options found</li>
79
+ }
80
+ </ul>
81
+ </div>
82
+ }
83
+ </div>
84
+
85
+ @if (computedErrorText()) {
86
+ <div class="origo-combobox__error" [id]="contract().id + '-error'">{{ computedErrorText() }}</div>
87
+ }
88
+ @if (computedHelpText()) {
89
+ <div class="origo-combobox__help" [id]="contract().id + '-help'">{{ computedHelpText() }}</div>
90
+ }
@@ -0,0 +1,189 @@
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-combobox--fluid) {
12
+ display: block;
13
+ width: 100%;
14
+ }
15
+
16
+ .origo-combobox__wrapper {
17
+ display: flex;
18
+ flex-direction: column;
19
+ position: relative;
20
+ width: 100%;
21
+ }
22
+
23
+ .origo-combobox__control {
24
+ position: relative;
25
+ display: flex;
26
+ align-items: center;
27
+ width: 100%;
28
+ }
29
+
30
+ .origo-combobox__input {
31
+ box-sizing: border-box;
32
+ width: 100%;
33
+ background-color: var(--origo-color-surface-background);
34
+ color: var(--origo-color-text-primary);
35
+ padding: var(--origo-spacing-container-padding);
36
+ padding-inline-end: 3rem;
37
+ border: var(--origo-border-width-default) solid var(--origo-color-border-default);
38
+ border-radius: var(--origo-radius-sm);
39
+ box-shadow: var(--origo-shadow-sm);
40
+ font-family: inherit;
41
+ font-size: inherit;
42
+ transition: all var(--origo-duration-fast) ease-in-out;
43
+
44
+ &:hover:not(:disabled):not([readonly]) {
45
+ border-color: var(--origo-color-border-hover);
46
+ }
47
+
48
+ &:active:not(:disabled):not([readonly]) {
49
+ border-color: var(--origo-color-border-active);
50
+ }
51
+
52
+ &:focus,
53
+ &:focus-visible {
54
+ outline: var(--origo-border-width-thick) solid var(--origo-color-focus);
55
+ outline-offset: var(--origo-border-offset-negative);
56
+ border-color: var(--origo-color-focus);
57
+ }
58
+
59
+ &:disabled {
60
+ cursor: not-allowed;
61
+ opacity: var(--origo-opacity-disabled);
62
+ background-color: var(--origo-color-surface-disabled);
63
+ color: var(--origo-color-text-disabled);
64
+ }
65
+
66
+ &[readonly] {
67
+ background-color: var(--origo-color-surface-readonly);
68
+ }
69
+
70
+ &::placeholder {
71
+ color: var(--origo-color-text-muted);
72
+ }
73
+ }
74
+
75
+ :host(.origo-combobox--filled) .origo-combobox__input {
76
+ background-color: var(--origo-color-surface-subtle);
77
+ border: none;
78
+ }
79
+
80
+ :host(.origo-combobox--invalid) .origo-combobox__input {
81
+ border-color: var(--origo-color-text-danger);
82
+ }
83
+
84
+ .origo-combobox__icons {
85
+ position: absolute;
86
+ right: var(--origo-spacing-sm);
87
+ display: inline-flex;
88
+ align-items: center;
89
+ gap: var(--origo-spacing-xs);
90
+ }
91
+
92
+ .origo-combobox__clear {
93
+ cursor: pointer;
94
+ color: var(--origo-color-text-muted);
95
+ font-size: var(--origo-typography-caption-font-size);
96
+
97
+ &:hover {
98
+ color: var(--origo-color-text-primary);
99
+ }
100
+ }
101
+
102
+ .origo-combobox__toggle {
103
+ background: transparent;
104
+ border: none;
105
+ cursor: pointer;
106
+ padding: var(--origo-spacing-xs);
107
+ display: inline-flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+
111
+ &:disabled {
112
+ cursor: not-allowed;
113
+ }
114
+ }
115
+
116
+ .origo-combobox__arrow {
117
+ color: var(--origo-color-text-muted);
118
+ font-size: var(--origo-typography-caption-font-size);
119
+ }
120
+
121
+ .origo-combobox__dropdown {
122
+ position: absolute;
123
+ top: 100%;
124
+ left: 0;
125
+ right: 0;
126
+ z-index: 1000;
127
+ margin-top: var(--origo-spacing-xs);
128
+ box-sizing: border-box;
129
+ background-color: var(--origo-color-surface-background);
130
+ border: var(--origo-border-width-default) solid var(--origo-color-border-default);
131
+ border-radius: var(--origo-radius-sm);
132
+ box-shadow: var(--origo-shadow-md);
133
+ max-height: 16rem;
134
+ overflow-y: auto;
135
+ }
136
+
137
+ .origo-combobox__options {
138
+ list-style: none;
139
+ margin: 0;
140
+ padding: 0;
141
+ }
142
+
143
+ .origo-combobox__group-label {
144
+ padding: var(--origo-spacing-xs) var(--origo-spacing-container-padding);
145
+ font-weight: bold;
146
+ font-size: var(--origo-typography-caption-font-size);
147
+ color: var(--origo-color-text-muted);
148
+ background-color: var(--origo-color-surface-subtle);
149
+ }
150
+
151
+ .origo-combobox__option {
152
+ padding: var(--origo-spacing-container-padding);
153
+ color: var(--origo-color-text-primary);
154
+ cursor: pointer;
155
+ transition: background-color var(--origo-duration-fast) ease-in-out;
156
+
157
+ &:hover,
158
+ &--active {
159
+ background-color: var(--origo-color-surface-subtle);
160
+ }
161
+
162
+ &--selected {
163
+ background-color: var(--origo-color-surface-subtle);
164
+ font-weight: bold;
165
+ }
166
+
167
+ &--disabled {
168
+ opacity: var(--origo-opacity-disabled);
169
+ cursor: not-allowed;
170
+ }
171
+ }
172
+
173
+ .origo-combobox__no-options {
174
+ padding: var(--origo-spacing-container-padding);
175
+ color: var(--origo-color-text-muted);
176
+ text-align: center;
177
+ }
178
+
179
+ .origo-combobox__error {
180
+ color: var(--origo-color-text-danger);
181
+ font-size: var(--origo-typography-caption-font-size);
182
+ margin-top: var(--origo-spacing-xs);
183
+ }
184
+
185
+ .origo-combobox__help {
186
+ color: var(--origo-color-text-muted);
187
+ font-size: var(--origo-typography-caption-font-size);
188
+ margin-top: var(--origo-spacing-xs);
189
+ }
@@ -0,0 +1,281 @@
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 { SelectProps } from '../select/select.component';
21
+ import { normalizeOptions, filterOptions } from '../select/selection-utils';
22
+
23
+ export type ComboboxProps = SelectProps;
24
+
25
+ @Component({
26
+ selector: 'origo-combobox',
27
+ standalone: true,
28
+ templateUrl: './combobox.component.html',
29
+ styleUrls: ['./combobox.component.scss'],
30
+ changeDetection: ChangeDetectionStrategy.OnPush,
31
+ encapsulation: ViewEncapsulation.ShadowDom,
32
+ host: {
33
+ '[class.origo-combobox]': 'true',
34
+ '[attr.data-testid]': 'contract().id ?? ""',
35
+ '[class.origo-combobox--fluid]': 'computedFluid()',
36
+ '[class.origo-combobox--outlined]': 'computedVariant() === "outlined"',
37
+ '[class.origo-combobox--filled]': 'computedVariant() === "filled"',
38
+ '[class.origo-combobox--invalid]': 'computedInvalid()',
39
+ '[class.origo-combobox--disabled]': 'computedDisabled()',
40
+ '[class.origo-combobox--readonly]': 'computedReadonly()',
41
+ },
42
+ })
43
+ export class ComboboxComponent implements OrigoAdapter<ComboboxProps> {
44
+ static readonly contractSchema = {
45
+ permissions: 'object',
46
+ rules: 'object',
47
+ metadata: 'object',
48
+ options: 'array',
49
+ value: 'string',
50
+ optionLabel: 'string',
51
+ optionValue: 'string',
52
+ optionGroupLabel: 'string',
53
+ optionGroupChildren: 'string',
54
+ placeholder: 'string',
55
+ filter: 'boolean',
56
+ filterBy: 'string',
57
+ filterMatchMode: 'string',
58
+ filterPlaceholder: 'string',
59
+ editable: 'boolean',
60
+ clearable: 'boolean',
61
+ appendTo: 'string',
62
+ virtualScroll: 'boolean',
63
+ itemSize: 'number',
64
+ loading: 'boolean',
65
+ disabled: 'boolean',
66
+ readonly: 'boolean',
67
+ required: 'boolean',
68
+ invalid: 'boolean',
69
+ errorText: 'string',
70
+ helpText: 'string',
71
+ variant: 'string',
72
+ fluid: 'boolean',
73
+ 'aria-label': 'string',
74
+ 'aria-describedby': 'string',
75
+ };
76
+ static readonly strictContract = false;
77
+
78
+ contract = input.required<InteractionContract<ComboboxProps>>();
79
+ value = model<unknown>('');
80
+
81
+ inputValue = signal<string>('');
82
+ isOpen = signal<boolean>(false);
83
+ activeIndex = signal<number>(-1);
84
+
85
+ computedNormalizedOptions = computed(() => {
86
+ const props = this.contract().props;
87
+ return normalizeOptions(
88
+ props?.options,
89
+ props?.optionLabel,
90
+ props?.optionValue,
91
+ props?.optionGroupLabel,
92
+ props?.optionGroupChildren
93
+ );
94
+ });
95
+
96
+ filteredOptions = computed(() => {
97
+ const opts = this.computedNormalizedOptions();
98
+ const query = this.inputValue();
99
+ if (!query) return opts;
100
+ const mode = this.contract().props?.filterMatchMode ?? 'contains';
101
+ return filterOptions(opts, query, mode);
102
+ });
103
+
104
+ computedPlaceholder = computed(() => this.contract().props?.placeholder ?? '');
105
+ computedDisabled = computed(() => !!this.contract().props?.disabled);
106
+ computedReadonly = computed(() => !!this.contract().props?.readonly);
107
+ computedRequired = computed(() => !!this.contract().props?.required);
108
+ computedInvalid = computed(() => !!this.contract().props?.invalid);
109
+ computedClearable = computed(() => !!this.contract().props?.clearable);
110
+ computedVariant = computed(() => this.contract().props?.variant ?? 'outlined');
111
+ computedFluid = computed(() => !!this.contract().props?.fluid);
112
+ computedErrorText = computed(() => this.contract().props?.errorText ?? '');
113
+ computedHelpText = computed(() => this.contract().props?.helpText ?? '');
114
+
115
+ computedAriaLabel = computed(() => {
116
+ const label = this.contract().props?.['aria-label'];
117
+ if (label !== undefined && label !== null && String(label).trim() !== '') {
118
+ return String(label);
119
+ }
120
+ return this.computedPlaceholder() || 'Combobox';
121
+ });
122
+
123
+ computedAriaDescribedBy = computed(() => {
124
+ const parts: string[] = [];
125
+ const desc = this.contract().props?.['aria-describedby'];
126
+ if (desc) parts.push(String(desc));
127
+ if (this.computedErrorText()) parts.push(`${this.contract().id}-error`);
128
+ if (this.computedHelpText()) parts.push(`${this.contract().id}-help`);
129
+ return parts.length > 0 ? parts.join(' ') : undefined;
130
+ });
131
+
132
+ private experienceAdapter = inject(WebExperienceAdapterService);
133
+ private platformId = inject(PLATFORM_ID);
134
+ private elementRef = inject(ElementRef);
135
+
136
+ constructor() {
137
+ effect(() => {
138
+ const contractVal = this.contract().props?.value;
139
+ untracked(() => {
140
+ if (contractVal !== undefined && contractVal !== this.value()) {
141
+ this.value.set(contractVal);
142
+ const match = this.computedNormalizedOptions().find(o => o.value === contractVal);
143
+ this.inputValue.set(match ? match.label : String(contractVal ?? ''));
144
+ }
145
+ });
146
+ });
147
+ }
148
+
149
+ onInput(event: Event) {
150
+ if (this.computedDisabled() || this.computedReadonly()) return;
151
+ const target = event.target as HTMLInputElement;
152
+ const text = target.value;
153
+ this.inputValue.set(text);
154
+ this.value.set(text);
155
+ this.isOpen.set(true);
156
+ this.activeIndex.set(0);
157
+ this.experienceAdapter.updateState(this.contract().id, 'value', text);
158
+ }
159
+
160
+ onPaste(event: ClipboardEvent) {
161
+ if (this.computedDisabled() || this.computedReadonly()) return;
162
+ event.preventDefault();
163
+ const plain = event.clipboardData?.getData('text/plain') ?? '';
164
+ const target = event.target as HTMLInputElement;
165
+ if (!target) return;
166
+ const start = target.selectionStart ?? target.value.length;
167
+ const end = target.selectionEnd ?? target.value.length;
168
+ const newValue = target.value.slice(0, start) + plain + target.value.slice(end);
169
+ target.value = newValue;
170
+ this.inputValue.set(newValue);
171
+ this.value.set(newValue);
172
+ this.isOpen.set(true);
173
+ this.activeIndex.set(0);
174
+ this.experienceAdapter.updateState(this.contract().id, 'value', newValue);
175
+ }
176
+
177
+ toggleOpen() {
178
+ if (this.computedDisabled() || this.computedReadonly()) return;
179
+ const next = !this.isOpen();
180
+ this.isOpen.set(next);
181
+ if (next) {
182
+ this.activeIndex.set(0);
183
+ }
184
+ }
185
+
186
+ selectOption(optionValue: unknown) {
187
+ if (this.computedDisabled() || this.computedReadonly()) return;
188
+ const match = this.computedNormalizedOptions().find(o => o.value === optionValue);
189
+ this.value.set(optionValue);
190
+ this.inputValue.set(match ? match.label : String(optionValue ?? ''));
191
+ this.isOpen.set(false);
192
+ this.experienceAdapter.updateState(this.contract().id, 'value', optionValue);
193
+ }
194
+
195
+ clear(event?: Event) {
196
+ if (this.computedDisabled() || this.computedReadonly()) return;
197
+ event?.stopPropagation();
198
+ this.value.set('');
199
+ this.inputValue.set('');
200
+ this.isOpen.set(false);
201
+ this.experienceAdapter.updateState(this.contract().id, 'value', '');
202
+ }
203
+
204
+ onKeydown(event: KeyboardEvent) {
205
+ if (this.computedDisabled() || this.computedReadonly()) return;
206
+
207
+ const opts = this.filteredOptions();
208
+ const count = opts.length;
209
+
210
+ switch (event.key) {
211
+ case 'ArrowDown': {
212
+ event.preventDefault();
213
+ if (!this.isOpen() && count > 0) {
214
+ this.isOpen.set(true);
215
+ this.activeIndex.set(0);
216
+ } else if (count > 0) {
217
+ const next = (this.activeIndex() + 1) % count;
218
+ this.activeIndex.set(next);
219
+ }
220
+ break;
221
+ }
222
+ case 'ArrowUp': {
223
+ event.preventDefault();
224
+ if (!this.isOpen() && count > 0) {
225
+ this.isOpen.set(true);
226
+ this.activeIndex.set(count - 1);
227
+ } else if (count > 0) {
228
+ const prev = (this.activeIndex() - 1 + count) % count;
229
+ this.activeIndex.set(prev);
230
+ }
231
+ break;
232
+ }
233
+ case 'Enter': {
234
+ if (this.isOpen()) {
235
+ event.preventDefault();
236
+ const idx = this.activeIndex();
237
+ if (idx >= 0 && idx < count && !opts[idx].disabled) {
238
+ this.selectOption(opts[idx].value);
239
+ }
240
+ }
241
+ break;
242
+ }
243
+ case 'Escape': {
244
+ if (this.isOpen()) {
245
+ event.preventDefault();
246
+ this.isOpen.set(false);
247
+ }
248
+ break;
249
+ }
250
+ case 'Tab': {
251
+ if (this.isOpen()) {
252
+ this.isOpen.set(false);
253
+ }
254
+ break;
255
+ }
256
+ case 'Home': {
257
+ if (this.isOpen() && count > 0) {
258
+ event.preventDefault();
259
+ this.activeIndex.set(0);
260
+ }
261
+ break;
262
+ }
263
+ case 'End': {
264
+ if (this.isOpen() && count > 0) {
265
+ event.preventDefault();
266
+ this.activeIndex.set(count - 1);
267
+ }
268
+ break;
269
+ }
270
+ }
271
+ }
272
+
273
+ @HostListener('document:click', ['$event'])
274
+ onDocumentClick(event: MouseEvent) {
275
+ if (!isPlatformBrowser(this.platformId)) return;
276
+ const clickedInside = this.elementRef.nativeElement.contains(event.target as Node);
277
+ if (!clickedInside && this.isOpen()) {
278
+ this.isOpen.set(false);
279
+ }
280
+ }
281
+ }
@@ -3,7 +3,7 @@
3
3
  [disabled]="computedDisabled()"
4
4
  [attr.aria-label]="computedAriaLabel()"
5
5
  [attr.aria-describedby]="computedAriaDescribedBy()"
6
- [title]="contract().props?.tooltip || null"
6
+ [title]="contract().props.tooltip || null"
7
7
  (click)="onClick()"
8
8
  >
9
9
  @if (computedLoading()) {