@egose/shadcn-theme-ng 0.5.3 → 0.6.1
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.
- package/README.md +131 -12
- package/autocomplete/fesm2022/autocomplete.mjs +14 -4
- package/autocomplete/types/autocomplete.d.ts +4 -1
- package/checkbox/README.md +6 -2
- package/checkbox/fesm2022/checkbox.mjs +11 -7
- package/checkbox/types/checkbox.d.ts +6 -4
- package/combobox/fesm2022/combobox.mjs +13 -7
- package/combobox/types/combobox.d.ts +8 -2
- package/data-table/README.md +174 -0
- package/data-table/fesm2022/data-table.mjs +713 -0
- package/data-table/package.json +25 -0
- package/data-table/types/data-table.d.ts +495 -0
- package/date-picker/README.md +41 -9
- package/date-picker/fesm2022/date-picker.mjs +265 -59
- package/date-picker/types/date-picker.d.ts +58 -27
- package/dropdown-menu/README.md +2 -0
- package/form-autocomplete/README.md +4 -2
- package/form-autocomplete/fesm2022/form-autocomplete.mjs +19 -6
- package/form-autocomplete/types/form-autocomplete.d.ts +9 -3
- package/form-combobox/README.md +4 -2
- package/form-combobox/fesm2022/form-combobox.mjs +23 -6
- package/form-combobox/types/form-combobox.d.ts +10 -3
- package/form-input-otp/README.md +7 -5
- package/form-input-otp/fesm2022/form-input-otp.mjs +20 -8
- package/form-input-otp/types/form-input-otp.d.ts +9 -3
- package/form-searchable-multiselect/README.md +15 -1
- package/form-searchable-multiselect/fesm2022/form-searchable-multiselect.mjs +15 -3
- package/form-searchable-multiselect/types/form-searchable-multiselect.d.ts +9 -1
- package/form-slider/README.md +3 -1
- package/form-slider/fesm2022/form-slider.mjs +19 -6
- package/form-slider/types/form-slider.d.ts +9 -3
- package/form-text-input/fesm2022/form-text-input.mjs +1 -1
- package/form-textarea/fesm2022/form-textarea.mjs +1 -1
- package/form-toggle/README.md +22 -18
- package/form-toggle/fesm2022/form-toggle.mjs +22 -9
- package/form-toggle/types/form-toggle.d.ts +6 -2
- package/input/README.md +7 -12
- package/input/fesm2022/input.mjs +7 -4
- package/input/types/input.d.ts +3 -1
- package/input-otp/README.md +33 -9
- package/input-otp/fesm2022/input-otp.mjs +127 -11
- package/input-otp/types/input-otp.d.ts +24 -4
- package/layout-simple/fesm2022/layout-simple.mjs +1 -1
- package/package.json +6 -1
- package/pagination/README.md +21 -12
- package/pagination/fesm2022/pagination.mjs +73 -65
- package/pagination/types/pagination.d.ts +21 -8
- package/phone-input/README.md +16 -0
- package/phone-input/fesm2022/phone-input.mjs +42 -4
- package/phone-input/types/phone-input.d.ts +5 -2
- package/searchable-multiselect/README.md +62 -40
- package/searchable-multiselect/fesm2022/searchable-multiselect.mjs +59 -32
- package/searchable-multiselect/types/searchable-multiselect.d.ts +23 -6
- package/slider/fesm2022/slider.mjs +9 -5
- package/slider/types/slider.d.ts +6 -2
- package/stepper/README.md +39 -19
- package/stepper/fesm2022/stepper.mjs +64 -12
- package/stepper/types/stepper.d.ts +12 -1
- package/table/README.md +2 -0
- package/table/fesm2022/table.mjs +12 -8
- package/table/types/table.d.ts +7 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Searchable Multiselect (`@egose/shadcn-theme-ng/searchable-multiselect`)
|
|
2
2
|
|
|
3
|
-
A shadcn/ui-style **multi-select with chips + popover picker** — selected values render as removable chips, and a
|
|
3
|
+
A shadcn/ui-style **multi-select with chips + searchable popover picker** — selected values render as named removable chips, and a labeled local search filters the checkbox option list. There is no exact single shadcn/ui counterpart; it composes the `Popover`, `Button`, and `Checkbox` patterns into one opinionated control.
|
|
4
4
|
|
|
5
5
|
It is a **standalone `ControlValueAccessor` component** (`EgSearchableMultiselect`), so it binds directly to Angular reactive forms (`formControlName`) and template-driven forms (`ngModel`) with a `string[]` value. Internally it reuses `HlmPopover`/`HlmCheckbox`/`HlmButton` — you do not import those yourself.
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng/searchable-multi
|
|
|
23
23
|
// import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng-tw/searchable-multiselect';
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Peer dependencies (see [package README](../../README.md) for
|
|
26
|
+
Peer dependencies include Angular and `@spartan-ng/brain` (`>=1.3.2 <2.0.0`); see the [package README](../../README.md) for the complete peer list and supported versions. The composed popover/checkbox/button implementations are included in the theme package, but their Spartan behavior is supplied by that peer.
|
|
27
27
|
|
|
28
28
|
## Imports
|
|
29
29
|
|
|
@@ -64,11 +64,13 @@ Renders (internally — you do not write this yourself):
|
|
|
64
64
|
<span><!-- {{ placeholder }} or chip {{ item.label }} + ✕ button --></span>
|
|
65
65
|
</div>
|
|
66
66
|
|
|
67
|
-
<!-- popover trigger + checkbox list -->
|
|
67
|
+
<!-- popover trigger + local search + checkbox list -->
|
|
68
68
|
<hlm-popover>
|
|
69
69
|
<button hlmPopoverTrigger hlmButton>N selected</button>
|
|
70
70
|
<hlm-popover-content>
|
|
71
|
+
<label>Search options <input type="search" /></label>
|
|
71
72
|
<label><!-- <hlm-checkbox> per option + label text --></label>
|
|
73
|
+
<div role="status" aria-live="polite"><!-- No matching options, when empty --></div>
|
|
72
74
|
</hlm-popover-content>
|
|
73
75
|
</hlm-popover>
|
|
74
76
|
</eg-searchable-multiselect>
|
|
@@ -80,25 +82,47 @@ Real selector: `eg-searchable-multiselect` (element). The `✕` chip buttons and
|
|
|
80
82
|
|
|
81
83
|
### EgSearchableMultiselect (component, `ControlValueAccessor`)
|
|
82
84
|
|
|
83
|
-
| Input | Type | Default | Description
|
|
84
|
-
| --------------------- | --------------------- | ------------------------ |
|
|
85
|
-
| `options` | `SelectOption[]` | `[]` | Full option list (`{ label, value }`)
|
|
86
|
-
| `value` | `string[]` | `[]` |
|
|
87
|
-
| `placeholder` | `string` | `'Start typing to add…'` | Text of the pill shown when nothing is selected
|
|
88
|
-
| `id` | `string` | `''` | `id` placed on the trigger button
|
|
89
|
-
| `disabled` | `boolean` | `false` | Disables chips + trigger + checkboxes
|
|
90
|
-
| `wrapperDisabled` | `boolean` | `false` | Second disable flag (e.g. set by wrapper form components); OR-ed with `disabled`
|
|
91
|
-
| `ariaLabel` | `string \| undefined` | `undefined` | `aria-label` for the trigger button
|
|
92
|
-
| `ariaDescribedby` | `string \| null` | `null` | `aria-describedby` for the trigger button
|
|
93
|
-
| `class` (`userClass`) | `ClassValue` | `''` | Extra classes on the host
|
|
94
|
-
|
|
95
|
-
| Output | Type | Description
|
|
96
|
-
| ------------- | ---------- |
|
|
97
|
-
| `valueChange` | `string[]` | Emitted with
|
|
85
|
+
| Input | Type | Default | Description |
|
|
86
|
+
| --------------------- | --------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------- |
|
|
87
|
+
| `options` | `SelectOption[]` | `[]` | Full option list (`{ label, value }`) |
|
|
88
|
+
| `value` | `string[]` | `[]` | Standalone selected IDs; pairs with `valueChange` for `[(value)]`. Ignored after the first CVA write. |
|
|
89
|
+
| `placeholder` | `string` | `'Start typing to add…'` | Text of the pill shown when nothing is selected |
|
|
90
|
+
| `id` | `string` | `''` | `id` placed on the trigger button |
|
|
91
|
+
| `disabled` | `boolean` | `false` | Disables chips + trigger + search + checkboxes |
|
|
92
|
+
| `wrapperDisabled` | `boolean` | `false` | Second disable flag (e.g. set by wrapper form components); OR-ed with `disabled` |
|
|
93
|
+
| `ariaLabel` | `string \| undefined` | `undefined` | `aria-label` for the trigger button |
|
|
94
|
+
| `ariaDescribedby` | `string \| null` | `null` | `aria-describedby` for the trigger button |
|
|
95
|
+
| `class` (`userClass`) | `ClassValue` | `''` | Extra classes on the host |
|
|
96
|
+
|
|
97
|
+
| Output | Type | Description |
|
|
98
|
+
| ------------- | ---------- | ---------------------------------------------------------------- |
|
|
99
|
+
| `valueChange` | `string[]` | Emitted with a new value array on each effective user add/remove |
|
|
98
100
|
|
|
99
101
|
`ControlValueAccessor` contract: `writeValue(values)`, `registerOnChange`, `registerOnTouched`, `setDisabledState` are implemented, so `formControl` / `formControlName` / `ngModel` all work. Effective disabled state = `disabled() \|\| wrapperDisabled() \|\| formDisabled()` (the last set by forms via `setDisabledState`).
|
|
100
102
|
|
|
101
|
-
|
|
103
|
+
`value` is a plain `input`, not a `model`: form writes flow through `writeValue`, and user edits flow out through `valueChange` + the CVA `onChange` callback.
|
|
104
|
+
|
|
105
|
+
### Local search
|
|
106
|
+
|
|
107
|
+
Additional copy inputs:
|
|
108
|
+
|
|
109
|
+
- `searchLabel: string` (default `'Search options'`): visible, associated native search label; supply nonempty localized text.
|
|
110
|
+
- `searchPlaceholder: string` (default `'Type to filter…'`): search input hint, separate from the empty-selection pill.
|
|
111
|
+
- `emptyMessage: string` (default `'No matching options'`): polite live-region copy for no matches or no supplied options.
|
|
112
|
+
- `removeLabel: (option: SelectOption) => string` (default ``option => `Remove ${option.label}` ``): pure formatter for each chip button's accessible name; unresolved labels are raw IDs.
|
|
113
|
+
|
|
114
|
+
Pass the complete `options` list. The native search field trims the query and performs case-insensitive substring matching on **labels**, retaining source order. An empty/whitespace query restores all choices. Search is local to the supplied list: it does not search IDs, fetch remote options, or virtualize results. New option arrays and label replacements recompute the current filter immediately.
|
|
115
|
+
|
|
116
|
+
Filtering hides checkbox rows only. All selections, including unresolved IDs, remain selected; chip labels still resolve from the complete options list. Search edits never emit `valueChange`/CVA changes or mark touched. The query persists across close/reopen and external value writes/resets; clear its text to restore all choices. The empty message occupies a persistent polite status region while the popover is open.
|
|
117
|
+
|
|
118
|
+
### Selection ownership and asynchronous options
|
|
119
|
+
|
|
120
|
+
- **Standalone:** `[value]` initializes selection, and each new input array replaces it (including `[]` to clear). User edits update the displayed selection immediately and emit `valueChange`; `[(value)]` keeps the parent synchronized. With one-way binding, local edits persist until a new input array arrives.
|
|
121
|
+
- **Angular forms:** the first `writeValue` takes ownership for the component's lifetime. Subsequent `[value]` changes are ignored; use `formControl`, `formControlName`, or `ngModel` as the source of truth. Every form write replaces selection; `null` and `[]` clear it, including resets.
|
|
122
|
+
- Selected IDs are stored independently of `options`, in value-array order. Unresolved IDs render as removable chips labeled with the raw ID and count toward the selected total. When options arrive or labels change, chips update automatically. When an option disappears, its chip falls back to the ID. Adding/removing another option never discards these unresolved IDs; explicitly remove their chips or write a replacement value to clear them.
|
|
123
|
+
- External input/form writes and option updates never emit `valueChange`, call CVA `onChange`, or mark touched. Effective user adds/removes emit once per channel and call `onTouched`. Opening/focusing the picker alone does not mark touched; duplicate adds and absent removals are no-ops.
|
|
124
|
+
- Disabled state combines the input, wrapper, and form flags. It blocks trigger/chip/search/checkbox edits, including when disabled with the panel open, while still accepting external value and option updates.
|
|
125
|
+
- Input arrays and option objects are never mutated. Output and CVA callbacks receive separate fresh arrays. Supply unique selected IDs and unique option values, and replace arrays/option objects rather than mutating them in place so signal updates are observed.
|
|
102
126
|
|
|
103
127
|
## Examples
|
|
104
128
|
|
|
@@ -224,47 +248,44 @@ export class DisabledMultiComponent {
|
|
|
224
248
|
|
|
225
249
|
Disabling via `formControl.disable()` works too — it flows through `setDisabledState`.
|
|
226
250
|
|
|
227
|
-
### 5.
|
|
251
|
+
### 5. Local search with configurable copy
|
|
228
252
|
|
|
229
|
-
|
|
253
|
+
Supply all options so hidden selections retain their labels:
|
|
230
254
|
|
|
231
255
|
```ts
|
|
232
|
-
import { Component,
|
|
233
|
-
import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng/searchable-multiselect';
|
|
256
|
+
import { Component, signal } from '@angular/core';
|
|
257
|
+
import { EgSearchableMultiselect, type SelectOption } from '@egose/shadcn-theme-ng/searchable-multiselect';
|
|
234
258
|
|
|
235
259
|
@Component({
|
|
236
260
|
selector: 'app-search-multi',
|
|
237
261
|
standalone: true,
|
|
238
262
|
imports: [EgSearchableMultiselect],
|
|
239
263
|
template: `
|
|
240
|
-
<
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
264
|
+
<eg-searchable-multiselect
|
|
265
|
+
[options]="frameworks"
|
|
266
|
+
[(value)]="selected"
|
|
267
|
+
ariaLabel="Framework assignments"
|
|
268
|
+
searchLabel="Find frameworks"
|
|
269
|
+
searchPlaceholder="Type a framework name…"
|
|
270
|
+
emptyMessage="No frameworks match your search"
|
|
271
|
+
[removeLabel]="removeFrameworkLabel"
|
|
246
272
|
/>
|
|
247
|
-
<eg-searchable-multiselect [options]="filtered()" [(value)]="selected" />
|
|
248
273
|
`,
|
|
249
274
|
})
|
|
250
275
|
export class SearchMultiComponent {
|
|
251
|
-
readonly query = signal('');
|
|
252
276
|
readonly selected = signal<string[]>([]);
|
|
253
|
-
|
|
277
|
+
readonly removeFrameworkLabel = (option: SelectOption) => `Unassign ${option.label}`;
|
|
278
|
+
readonly frameworks: SelectOption[] = [
|
|
254
279
|
{ label: 'Angular', value: 'angular' },
|
|
255
280
|
{ label: 'React', value: 'react' },
|
|
256
281
|
{ label: 'Vue', value: 'vue' },
|
|
257
282
|
{ label: 'Svelte', value: 'svelte' },
|
|
258
283
|
{ label: 'Solid', value: 'solid' },
|
|
259
284
|
];
|
|
260
|
-
readonly filtered = computed(() => {
|
|
261
|
-
const q = this.query().trim().toLowerCase();
|
|
262
|
-
return q ? this.all.filter((o) => o.label.toLowerCase().includes(q)) : this.all;
|
|
263
|
-
});
|
|
264
285
|
}
|
|
265
286
|
```
|
|
266
287
|
|
|
267
|
-
|
|
288
|
+
The form wrapper `EgFormSearchableMultiselect` forwards the same `searchLabel`, `searchPlaceholder`, `emptyMessage`, and `removeLabel` inputs. For localization, the remove formatter receives the current label and value, so it can include an ID to distinguish duplicate display names.
|
|
268
289
|
|
|
269
290
|
### 6. Async options + reacting to changes
|
|
270
291
|
|
|
@@ -308,9 +329,10 @@ export class AsyncMultiComponent {
|
|
|
308
329
|
## Accessibility notes
|
|
309
330
|
|
|
310
331
|
- The trigger is a real `<button>` — give it an accessible name via `ariaLabel` (or a visible `<label>` paired with `id`) and descriptions via `ariaDescribedby`.
|
|
311
|
-
-
|
|
312
|
-
-
|
|
313
|
-
-
|
|
332
|
+
- Open the trigger with Enter/Space. With the default popover focus configuration, focus moves to the labeled native search field. Type to filter, clear the text to restore choices, and Tab/Shift+Tab among controls. Enter in search does not submit a surrounding form. Escape closes the popover and restores trigger focus.
|
|
333
|
+
- Options use native buttons with checkbox roles inside `<label>` elements. Use Space/Enter to toggle a focused choice.
|
|
334
|
+
- Chip `✕` buttons are named `Remove <label>` by default (`Remove <ID>` when unresolved); customize with `removeLabel`. They are disabled along with the control.
|
|
335
|
+
- No matches (including an empty options list) displays `emptyMessage` in a polite, atomic status region. The search remains editable when there are no results. Browser DOM/focus tests verify these contracts; they do not constitute screen-reader testing.
|
|
314
336
|
|
|
315
337
|
## Theming / CSS variables
|
|
316
338
|
|
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input, output, signal, computed, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
2
|
+
import { input, output, signal, linkedSignal, computed, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
3
|
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
4
4
|
import { HlmPopover, HlmPopoverTrigger, HlmPopoverContent, HlmPopoverPortal } from '@egose/shadcn-theme-ng/popover';
|
|
5
5
|
import { HlmButton } from '@egose/shadcn-theme-ng/button';
|
|
6
6
|
import { HlmCheckbox } from '@egose/shadcn-theme-ng/checkbox';
|
|
7
7
|
import { hlm } from '@egose/shadcn-theme-ng/utils';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Chip multiselect with local, case-insensitive label search over the complete options input.
|
|
11
|
+
* Filtering never changes selected IDs or chip labels. Search edits do not emit or touch the form.
|
|
12
|
+
* Import from `@egose/shadcn-theme-ng/searchable-multiselect` (or the `-tw` variant).
|
|
13
|
+
*/
|
|
9
14
|
class EgSearchableMultiselect {
|
|
10
15
|
/** Full option list with labels/values */
|
|
11
16
|
options = input([], /* @ts-ignore */
|
|
12
17
|
...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
13
18
|
placeholder = input('Start typing to add…', /* @ts-ignore */
|
|
14
19
|
...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
20
|
+
/** Visible, associated label for the native search field; provide nonempty localized text. */
|
|
21
|
+
searchLabel = input('Search options', /* @ts-ignore */
|
|
22
|
+
...(ngDevMode ? [{ debugName: "searchLabel" }] : /* istanbul ignore next */ []));
|
|
23
|
+
/** Search field hint, independent of the empty-selection chip placeholder. */
|
|
24
|
+
searchPlaceholder = input('Type to filter…', /* @ts-ignore */
|
|
25
|
+
...(ngDevMode ? [{ debugName: "searchPlaceholder" }] : /* istanbul ignore next */ []));
|
|
26
|
+
/** Polite live-region message when no supplied option labels match, including an empty options list. */
|
|
27
|
+
emptyMessage = input('No matching options', /* @ts-ignore */
|
|
28
|
+
...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
|
|
29
|
+
/** Accessible remove name from the current chip label (raw ID when unresolved). Supply a pure formatter. */
|
|
30
|
+
removeLabel = input((option) => `Remove ${option.label}`, /* @ts-ignore */
|
|
31
|
+
...(ngDevMode ? [{ debugName: "removeLabel" }] : /* istanbul ignore next */ []));
|
|
15
32
|
id = input('', /* @ts-ignore */
|
|
16
33
|
...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
|
|
17
34
|
disabled = input(false, /* @ts-ignore */
|
|
@@ -23,21 +40,42 @@ class EgSearchableMultiselect {
|
|
|
23
40
|
ariaDescribedby = input(null, /* @ts-ignore */
|
|
24
41
|
...(ngDevMode ? [{ debugName: "ariaDescribedby" }] : /* istanbul ignore next */ []));
|
|
25
42
|
userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
|
|
26
|
-
/**
|
|
43
|
+
/** Standalone selected IDs. New input arrays replace local edits; ignored after the first CVA writeValue. */
|
|
27
44
|
value = input([], /* @ts-ignore */
|
|
28
45
|
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
46
|
+
/** User selection changes only; external input/CVA writes and option updates do not emit. */
|
|
29
47
|
valueChange = output();
|
|
30
|
-
|
|
48
|
+
// Undefined means standalone ownership; even a null CVA write takes form ownership with an empty array.
|
|
49
|
+
formValue = signal(undefined, /* @ts-ignore */
|
|
50
|
+
...(ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []));
|
|
51
|
+
selectedValues = linkedSignal(() => [...(this.formValue() ?? this.value())], /* @ts-ignore */
|
|
52
|
+
...(ngDevMode ? [{ debugName: "selectedValues" }] : /* istanbul ignore next */ []));
|
|
53
|
+
selectedItems = computed(() => {
|
|
54
|
+
const options = new Map(this.options().map((option) => [option.value, option]));
|
|
55
|
+
return this.selectedValues().map((value) => options.get(value) ?? { value, label: value });
|
|
56
|
+
}, /* @ts-ignore */
|
|
31
57
|
...(ngDevMode ? [{ debugName: "selectedItems" }] : /* istanbul ignore next */ []));
|
|
32
58
|
formDisabled = signal(false, /* @ts-ignore */
|
|
33
59
|
...(ngDevMode ? [{ debugName: "formDisabled" }] : /* istanbul ignore next */ []));
|
|
34
60
|
disabledState = computed(() => this.disabled() || this.wrapperDisabled() || this.formDisabled(), /* @ts-ignore */
|
|
35
61
|
...(ngDevMode ? [{ debugName: "disabledState" }] : /* istanbul ignore next */ []));
|
|
36
|
-
|
|
62
|
+
// Local UI state persists across popover closes and external writes; it never owns selection.
|
|
63
|
+
searchQuery = signal('', /* @ts-ignore */
|
|
64
|
+
...(ngDevMode ? [{ debugName: "searchQuery" }] : /* istanbul ignore next */ []));
|
|
65
|
+
filteredOptions = computed(() => {
|
|
66
|
+
const query = this.searchQuery().trim().toLowerCase();
|
|
67
|
+
return this.options().filter((option) => option.label.toLowerCase().includes(query));
|
|
68
|
+
}, /* @ts-ignore */
|
|
69
|
+
...(ngDevMode ? [{ debugName: "filteredOptions" }] : /* istanbul ignore next */ []));
|
|
37
70
|
_hostClass = computed(() => hlm(this.userClass()), /* @ts-ignore */
|
|
38
71
|
...(ngDevMode ? [{ debugName: "_hostClass" }] : /* istanbul ignore next */ []));
|
|
72
|
+
updateSearch(query) {
|
|
73
|
+
if (this.disabledState())
|
|
74
|
+
return;
|
|
75
|
+
this.searchQuery.set(query);
|
|
76
|
+
}
|
|
39
77
|
isSelected(value) {
|
|
40
|
-
return this.
|
|
78
|
+
return this.selectedValues().includes(value);
|
|
41
79
|
}
|
|
42
80
|
toggle(value, checked) {
|
|
43
81
|
if (this.disabledState())
|
|
@@ -50,40 +88,29 @@ class EgSearchableMultiselect {
|
|
|
50
88
|
}
|
|
51
89
|
}
|
|
52
90
|
addItem(value) {
|
|
53
|
-
if (!this.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return;
|
|
57
|
-
const updated = [...this.selectedItems(), opt];
|
|
58
|
-
this.updateValueFromSelected(updated);
|
|
59
|
-
}
|
|
91
|
+
if (this.disabledState() || this.isSelected(value) || !this.options().some((option) => option.value === value))
|
|
92
|
+
return;
|
|
93
|
+
this.updateValueFromSelected([...this.selectedValues(), value]);
|
|
60
94
|
}
|
|
61
95
|
removeItem(value) {
|
|
62
|
-
if (this.disabledState())
|
|
96
|
+
if (this.disabledState() || !this.isSelected(value))
|
|
63
97
|
return;
|
|
64
|
-
const updated = this.
|
|
98
|
+
const updated = this.selectedValues().filter((selected) => selected !== value);
|
|
65
99
|
this.updateValueFromSelected(updated);
|
|
66
100
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.selectedItems.set(items);
|
|
73
|
-
const values = items.map((i) => i.value);
|
|
74
|
-
this.valueChange.emit(values);
|
|
75
|
-
this.onChange(values);
|
|
101
|
+
updateValueFromSelected(values) {
|
|
102
|
+
this.selectedValues.set(values);
|
|
103
|
+
// Neither consumer channel owns the internal array or the other channel's payload.
|
|
104
|
+
this.valueChange.emit([...values]);
|
|
105
|
+
this.onChange([...values]);
|
|
76
106
|
this.onTouched();
|
|
77
107
|
}
|
|
78
|
-
constructor() {
|
|
79
|
-
// No effect needed; we read `value` input directly via writeValue for form-bound usage.
|
|
80
|
-
}
|
|
81
108
|
// --- ControlValueAccessor ---
|
|
82
109
|
onChange = () => { };
|
|
83
110
|
onTouched = () => { };
|
|
111
|
+
/** Takes CVA ownership, retaining all IDs independently of options. Null clears; never emits or touches. */
|
|
84
112
|
writeValue(values) {
|
|
85
|
-
this.
|
|
86
|
-
this.updateSelectedFromValues(values || []);
|
|
113
|
+
this.formValue.set([...(values ?? [])]);
|
|
87
114
|
}
|
|
88
115
|
registerOnChange(fn) {
|
|
89
116
|
this.onChange = fn;
|
|
@@ -95,13 +122,13 @@ class EgSearchableMultiselect {
|
|
|
95
122
|
this.formDisabled.set(isDisabled);
|
|
96
123
|
}
|
|
97
124
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgSearchableMultiselect, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
98
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgSearchableMultiselect, isStandalone: true, selector: "eg-searchable-multiselect", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, wrapperDisabled: { classPropertyName: "wrapperDisabled", publicName: "wrapperDisabled", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "ariaDescribedby", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { properties: { "class": "_hostClass()" } }, providers: [
|
|
125
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgSearchableMultiselect, isStandalone: true, selector: "eg-searchable-multiselect", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, searchLabel: { classPropertyName: "searchLabel", publicName: "searchLabel", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, removeLabel: { classPropertyName: "removeLabel", publicName: "removeLabel", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, wrapperDisabled: { classPropertyName: "wrapperDisabled", publicName: "wrapperDisabled", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "ariaDescribedby", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { properties: { "class": "_hostClass()" } }, providers: [
|
|
99
126
|
{
|
|
100
127
|
provide: NG_VALUE_ACCESSOR,
|
|
101
128
|
useExisting: forwardRef(() => EgSearchableMultiselect),
|
|
102
129
|
multi: true,
|
|
103
130
|
},
|
|
104
|
-
], ngImport: i0, template: "\n <div class=\"flex flex-col gap-2 w-full\">\n <!-- Selected chips -->\n <div class=\"flex flex-wrap gap-2\">\n @if (selectedItems().length === 0) {\n <span\n class=\"bg-gray-100 text-gray-400 rounded-full\n px-2 py-1 inline-flex items-center text-xs\"\n >\n {{ placeholder() }}\n </span>\n } @else {\n @for (item of selectedItems(); track item.value) {\n <span\n class=\"bg-gray-200 rounded-full px-2 py-1\n inline-flex items-center gap-2 text-xs\"\n >\n {{ item.label }}\n <button\n type=\"button\"\n [disabled]=\"disabledState()\"\n (click)=\"removeItem(item.value)\"\n class=\"bg-transparent border-0 text-gray-600\n hover:text-red-500 cursor-pointer\"\n >\n ✕\n </button>\n </span>\n }\n }\n </div>\n\n <!-- Trigger -->\n <hlm-popover>\n <button\n hlmPopoverTrigger\n hlmButton\n variant=\"secondary\"\n appearance=\"outline\"\n type=\"button\"\n [id]=\"id()\"\n [disabled]=\"disabledState()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-describedby]=\"ariaDescribedby()\"\n >\n {{ selectedItems().length }} selected\n </button>\n\n <hlm-popover-content class=\"w-64 p-2\" *hlmPopoverPortal=\"let ctx\">\n <div class=\"max-h-60 overflow-auto flex flex-col gap-1\">\n @for (option of
|
|
131
|
+
], ngImport: i0, template: "\n <div class=\"flex flex-col gap-2 w-full\">\n <!-- Selected chips -->\n <div class=\"flex flex-wrap gap-2\">\n @if (selectedItems().length === 0) {\n <span\n class=\"bg-gray-100 text-gray-400 rounded-full\n px-2 py-1 inline-flex items-center text-xs\"\n >\n {{ placeholder() }}\n </span>\n } @else {\n @for (item of selectedItems(); track item.value) {\n <span\n class=\"bg-gray-200 rounded-full px-2 py-1\n inline-flex items-center gap-2 text-xs\"\n >\n {{ item.label }}\n <button\n type=\"button\"\n [disabled]=\"disabledState()\"\n [attr.aria-label]=\"removeLabel()(item)\"\n (click)=\"removeItem(item.value)\"\n class=\"bg-transparent border-0 text-gray-600\n hover:text-red-500 cursor-pointer\"\n >\n ✕\n </button>\n </span>\n }\n }\n </div>\n\n <!-- Trigger -->\n <hlm-popover>\n <button\n hlmPopoverTrigger\n hlmButton\n variant=\"secondary\"\n appearance=\"outline\"\n type=\"button\"\n [id]=\"id()\"\n [disabled]=\"disabledState()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-describedby]=\"ariaDescribedby()\"\n >\n {{ selectedItems().length }} selected\n </button>\n\n <hlm-popover-content class=\"w-64 p-2\" *hlmPopoverPortal=\"let ctx\">\n <label class=\"flex flex-col gap-1 text-sm\">\n <span>{{ searchLabel() }}</span>\n <input\n #search\n type=\"search\"\n [value]=\"searchQuery()\"\n [placeholder]=\"searchPlaceholder()\"\n [disabled]=\"disabledState()\"\n (input)=\"updateSearch(search.value)\"\n (keydown.enter)=\"$event.preventDefault()\"\n class=\"w-full rounded-md border border-input bg-transparent px-2 py-1 outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50\"\n />\n </label>\n <div class=\"max-h-60 overflow-auto flex flex-col gap-1\">\n @for (option of filteredOptions(); track option.value) {\n <label\n class=\"flex items-center gap-2 cursor-pointer px-2 py-1 rounded-sm hover:bg-secondary\"\n >\n <hlm-checkbox\n [checked]=\"isSelected(option.value)\"\n [disabled]=\"disabledState()\"\n (changed)=\"toggle(option.value, $event)\"\n />\n <span class=\"text-sm\">{{ option.label }}</span>\n </label>\n }\n </div>\n <div role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" class=\"text-xs text-muted-foreground\">\n @if (filteredOptions().length === 0) {\n {{ emptyMessage() }}\n }\n </div>\n </hlm-popover-content>\n </hlm-popover>\n </div>\n ", isInline: true, dependencies: [{ kind: "directive", type: HlmPopover, selector: "[hlmPopover],hlm-popover" }, { kind: "directive", type: HlmPopoverTrigger, selector: "button[hlmPopoverTrigger],button[hlmPopoverTriggerFor]" }, { kind: "directive", type: HlmPopoverContent, selector: "[hlmPopoverContent],hlm-popover-content" }, { kind: "directive", type: HlmPopoverPortal, selector: "[hlmPopoverPortal]" }, { kind: "component", type: HlmButton, selector: "button[hlmButton], a[hlmButton]", inputs: ["variant", "size", "appearance", "loading", "icon", "iconPosition", "class", "spinnerUserClass", "disabled", "type"] }, { kind: "component", type: HlmCheckbox, selector: "hlm-checkbox", inputs: ["class", "id", "aria-label", "aria-labelledby", "aria-describedby", "checked", "name", "required", "disabled", "wrapperDisabled"], outputs: ["checkedChange", "changed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
105
132
|
}
|
|
106
133
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgSearchableMultiselect, decorators: [{
|
|
107
134
|
type: Component,
|
|
@@ -120,9 +147,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
|
|
|
120
147
|
host: {
|
|
121
148
|
'[class]': '_hostClass()',
|
|
122
149
|
},
|
|
123
|
-
template: "\n <div class=\"flex flex-col gap-2 w-full\">\n <!-- Selected chips -->\n <div class=\"flex flex-wrap gap-2\">\n @if (selectedItems().length === 0) {\n <span\n class=\"bg-gray-100 text-gray-400 rounded-full\n px-2 py-1 inline-flex items-center text-xs\"\n >\n {{ placeholder() }}\n </span>\n } @else {\n @for (item of selectedItems(); track item.value) {\n <span\n class=\"bg-gray-200 rounded-full px-2 py-1\n inline-flex items-center gap-2 text-xs\"\n >\n {{ item.label }}\n <button\n type=\"button\"\n [disabled]=\"disabledState()\"\n (click)=\"removeItem(item.value)\"\n class=\"bg-transparent border-0 text-gray-600\n hover:text-red-500 cursor-pointer\"\n >\n ✕\n </button>\n </span>\n }\n }\n </div>\n\n <!-- Trigger -->\n <hlm-popover>\n <button\n hlmPopoverTrigger\n hlmButton\n variant=\"secondary\"\n appearance=\"outline\"\n type=\"button\"\n [id]=\"id()\"\n [disabled]=\"disabledState()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-describedby]=\"ariaDescribedby()\"\n >\n {{ selectedItems().length }} selected\n </button>\n\n <hlm-popover-content class=\"w-64 p-2\" *hlmPopoverPortal=\"let ctx\">\n <div class=\"max-h-60 overflow-auto flex flex-col gap-1\">\n @for (option of
|
|
150
|
+
template: "\n <div class=\"flex flex-col gap-2 w-full\">\n <!-- Selected chips -->\n <div class=\"flex flex-wrap gap-2\">\n @if (selectedItems().length === 0) {\n <span\n class=\"bg-gray-100 text-gray-400 rounded-full\n px-2 py-1 inline-flex items-center text-xs\"\n >\n {{ placeholder() }}\n </span>\n } @else {\n @for (item of selectedItems(); track item.value) {\n <span\n class=\"bg-gray-200 rounded-full px-2 py-1\n inline-flex items-center gap-2 text-xs\"\n >\n {{ item.label }}\n <button\n type=\"button\"\n [disabled]=\"disabledState()\"\n [attr.aria-label]=\"removeLabel()(item)\"\n (click)=\"removeItem(item.value)\"\n class=\"bg-transparent border-0 text-gray-600\n hover:text-red-500 cursor-pointer\"\n >\n ✕\n </button>\n </span>\n }\n }\n </div>\n\n <!-- Trigger -->\n <hlm-popover>\n <button\n hlmPopoverTrigger\n hlmButton\n variant=\"secondary\"\n appearance=\"outline\"\n type=\"button\"\n [id]=\"id()\"\n [disabled]=\"disabledState()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-describedby]=\"ariaDescribedby()\"\n >\n {{ selectedItems().length }} selected\n </button>\n\n <hlm-popover-content class=\"w-64 p-2\" *hlmPopoverPortal=\"let ctx\">\n <label class=\"flex flex-col gap-1 text-sm\">\n <span>{{ searchLabel() }}</span>\n <input\n #search\n type=\"search\"\n [value]=\"searchQuery()\"\n [placeholder]=\"searchPlaceholder()\"\n [disabled]=\"disabledState()\"\n (input)=\"updateSearch(search.value)\"\n (keydown.enter)=\"$event.preventDefault()\"\n class=\"w-full rounded-md border border-input bg-transparent px-2 py-1 outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50\"\n />\n </label>\n <div class=\"max-h-60 overflow-auto flex flex-col gap-1\">\n @for (option of filteredOptions(); track option.value) {\n <label\n class=\"flex items-center gap-2 cursor-pointer px-2 py-1 rounded-sm hover:bg-secondary\"\n >\n <hlm-checkbox\n [checked]=\"isSelected(option.value)\"\n [disabled]=\"disabledState()\"\n (changed)=\"toggle(option.value, $event)\"\n />\n <span class=\"text-sm\">{{ option.label }}</span>\n </label>\n }\n </div>\n <div role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" class=\"text-xs text-muted-foreground\">\n @if (filteredOptions().length === 0) {\n {{ emptyMessage() }}\n }\n </div>\n </hlm-popover-content>\n </hlm-popover>\n </div>\n ",
|
|
124
151
|
}]
|
|
125
|
-
}],
|
|
152
|
+
}], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], searchLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchLabel", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], removeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "removeLabel", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], wrapperDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrapperDisabled", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedby", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
|
|
126
153
|
|
|
127
154
|
/*
|
|
128
155
|
* Public API Surface of searchable-multiselect
|
|
@@ -6,39 +6,56 @@ interface SelectOption {
|
|
|
6
6
|
label: string;
|
|
7
7
|
value: string;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Chip multiselect with local, case-insensitive label search over the complete options input.
|
|
11
|
+
* Filtering never changes selected IDs or chip labels. Search edits do not emit or touch the form.
|
|
12
|
+
* Import from `@egose/shadcn-theme-ng/searchable-multiselect` (or the `-tw` variant).
|
|
13
|
+
*/
|
|
9
14
|
declare class EgSearchableMultiselect implements ControlValueAccessor {
|
|
10
15
|
/** Full option list with labels/values */
|
|
11
16
|
options: _angular_core.InputSignal<SelectOption[]>;
|
|
12
17
|
placeholder: _angular_core.InputSignal<string>;
|
|
18
|
+
/** Visible, associated label for the native search field; provide nonempty localized text. */
|
|
19
|
+
searchLabel: _angular_core.InputSignal<string>;
|
|
20
|
+
/** Search field hint, independent of the empty-selection chip placeholder. */
|
|
21
|
+
searchPlaceholder: _angular_core.InputSignal<string>;
|
|
22
|
+
/** Polite live-region message when no supplied option labels match, including an empty options list. */
|
|
23
|
+
emptyMessage: _angular_core.InputSignal<string>;
|
|
24
|
+
/** Accessible remove name from the current chip label (raw ID when unresolved). Supply a pure formatter. */
|
|
25
|
+
removeLabel: _angular_core.InputSignal<(option: SelectOption) => string>;
|
|
13
26
|
id: _angular_core.InputSignal<string>;
|
|
14
27
|
disabled: _angular_core.InputSignal<boolean>;
|
|
15
28
|
wrapperDisabled: _angular_core.InputSignal<boolean>;
|
|
16
29
|
ariaLabel: _angular_core.InputSignal<string | undefined>;
|
|
17
30
|
ariaDescribedby: _angular_core.InputSignal<string | null>;
|
|
18
31
|
userClass: _angular_core.InputSignal<ClassValue>;
|
|
19
|
-
/**
|
|
32
|
+
/** Standalone selected IDs. New input arrays replace local edits; ignored after the first CVA writeValue. */
|
|
20
33
|
value: _angular_core.InputSignal<string[]>;
|
|
34
|
+
/** User selection changes only; external input/CVA writes and option updates do not emit. */
|
|
21
35
|
valueChange: _angular_core.OutputEmitterRef<string[]>;
|
|
22
|
-
|
|
36
|
+
private readonly formValue;
|
|
37
|
+
private readonly selectedValues;
|
|
38
|
+
protected readonly selectedItems: _angular_core.Signal<SelectOption[]>;
|
|
23
39
|
protected readonly formDisabled: _angular_core.WritableSignal<boolean>;
|
|
24
40
|
protected readonly disabledState: _angular_core.Signal<boolean>;
|
|
25
|
-
|
|
41
|
+
protected readonly searchQuery: _angular_core.WritableSignal<string>;
|
|
42
|
+
protected readonly filteredOptions: _angular_core.Signal<SelectOption[]>;
|
|
26
43
|
protected readonly _hostClass: _angular_core.Signal<string>;
|
|
44
|
+
protected updateSearch(query: string): void;
|
|
27
45
|
protected isSelected(value: string): boolean;
|
|
28
46
|
protected toggle(value: string, checked: boolean): void;
|
|
29
47
|
protected addItem(value: string): void;
|
|
30
48
|
protected removeItem(value: string): void;
|
|
31
|
-
private updateSelectedFromValues;
|
|
32
49
|
private updateValueFromSelected;
|
|
33
|
-
constructor();
|
|
34
50
|
private onChange;
|
|
35
51
|
private onTouched;
|
|
52
|
+
/** Takes CVA ownership, retaining all IDs independently of options. Null clears; never emits or touches. */
|
|
36
53
|
writeValue(values: string[] | null): void;
|
|
37
54
|
registerOnChange(fn: (value: string[]) => void): void;
|
|
38
55
|
registerOnTouched(fn: () => void): void;
|
|
39
56
|
setDisabledState(isDisabled: boolean): void;
|
|
40
57
|
static ɵfac: _angular_core.ɵɵFactoryDeclaration<EgSearchableMultiselect, never>;
|
|
41
|
-
static ɵcmp: _angular_core.ɵɵComponentDeclaration<EgSearchableMultiselect, "eg-searchable-multiselect", never, { "options": { "alias": "options"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "wrapperDisabled": { "alias": "wrapperDisabled"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "ariaDescribedby": { "alias": "ariaDescribedby"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "valueChange": "valueChange"; }, never, never, true, never>;
|
|
58
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<EgSearchableMultiselect, "eg-searchable-multiselect", never, { "options": { "alias": "options"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "searchLabel": { "alias": "searchLabel"; "required": false; "isSignal": true; }; "searchPlaceholder": { "alias": "searchPlaceholder"; "required": false; "isSignal": true; }; "emptyMessage": { "alias": "emptyMessage"; "required": false; "isSignal": true; }; "removeLabel": { "alias": "removeLabel"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "wrapperDisabled": { "alias": "wrapperDisabled"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "ariaDescribedby": { "alias": "ariaDescribedby"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "valueChange": "valueChange"; }, never, never, true, never>;
|
|
42
59
|
}
|
|
43
60
|
|
|
44
61
|
export { EgSearchableMultiselect };
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
|
|
2
|
+
import { inject, input, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
|
|
3
|
+
import { BrnFieldControl, BrnFieldControlDescribedBy } from '@spartan-ng/brain/field';
|
|
3
4
|
import * as i1 from '@spartan-ng/brain/slider';
|
|
4
5
|
import { injectBrnSlider, BrnSlider, BrnSliderImports } from '@spartan-ng/brain/slider';
|
|
5
6
|
import { classes } from '@egose/shadcn-theme-ng/utils';
|
|
6
7
|
|
|
7
8
|
class HlmSlider {
|
|
8
9
|
_slider = injectBrnSlider();
|
|
10
|
+
_fieldControl = inject(BrnFieldControl);
|
|
11
|
+
/** Description IDs applied to every thumb, merged with enclosing Spartan field descriptions. */
|
|
12
|
+
ariaDescribedBy = input(null, { ...(ngDevMode ? { debugName: "ariaDescribedBy" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
|
|
9
13
|
constructor() {
|
|
10
14
|
classes(() => [
|
|
11
15
|
"group flex w-full touch-none flex-col select-none data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-40 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-row data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
12
16
|
]);
|
|
13
17
|
}
|
|
14
18
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmSlider, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
15
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmSlider, isStandalone: true, selector: "hlm-slider, brn-slider [hlm]", hostDirectives: [{ directive: i1.BrnSlider, inputs: ["id", "id", "value", "value", "disabled", "disabled", "min", "min", "max", "max", "step", "step", "minStepsBetweenThumbs", "minStepsBetweenThumbs", "inverted", "inverted", "orientation", "orientation", "showTicks", "showTicks", "maxTicks", "maxTicks", "tickLabelInterval", "tickLabelInterval", "formatTick", "formatTick", "draggableRange", "draggableRange", "draggableRangeOnly", "draggableRangeOnly", "aria-label", "aria-label", "aria-labelledby", "aria-labelledby"], outputs: ["valueChange", "valueChange"] }], ngImport: i0, template: "\n <div\n class=\"relative flex w-full items-center group-data-[orientation=vertical]:w-auto group-data-[orientation=vertical]:flex-col\"\n >\n <div\n brnSliderTrack\n class=\"bg-muted rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5 relative grow overflow-hidden\"\n >\n <div\n class=\"bg-primary absolute select-none data-draggable-range:cursor-move data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\"\n brnSliderRange\n ></div>\n </div>\n\n @for (i of _slider.thumbIndexes(); track i) {\n <span\n class=\"border-primary ring-ring/50 size-4 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden absolute block shrink-0 select-none after:absolute after:-inset-2\"\n brnSliderThumb\n ></span>\n }\n </div>\n\n @if (_slider.showTicks()) {\n <div\n class=\"px-2 group-data-[orientation=vertical]:px-0 group-data-[orientation=vertical]:py-2 text-muted-foreground mt-3 flex w-full items-start justify-between gap-1 text-xs font-medium group-data-[orientation=horizontal]:group-data-inverted:flex-row-reverse group-data-[orientation=vertical]:ms-3 group-data-[orientation=vertical]:mt-0 group-data-[orientation=vertical]:w-auto group-data-[orientation=vertical]:flex-col-reverse group-data-[orientation=vertical]:group-data-inverted:flex-col\"\n >\n <div\n *brnSliderTick=\"let tick; let formattedTick = formattedTick\"\n class=\"group flex w-0 flex-col items-center justify-center gap-2 group-data-[orientation=vertical]:h-0 group-data-[orientation=vertical]:w-auto group-data-[orientation=vertical]:flex-row\"\n >\n <div\n class=\"bg-muted-foreground/70 h-1 w-px group-data-[orientation=vertical]:h-px group-data-[orientation=vertical]:w-1 group-data-[orientation=horizontal]:group-data-[skip]:h-0.5 group-data-[orientation=vertical]:group-data-[skip]:w-0.5\"\n ></div>\n <div class=\"text-center group-data-[skip]:opacity-0\">{{ formattedTick }}</div>\n </div>\n </div>\n }\n ", isInline: true, dependencies: [{ kind: "directive", type: i1.BrnSliderTrack, selector: "[brnSliderTrack]" }, { kind: "directive", type: i1.BrnSliderThumb, selector: "[brnSliderThumb]" }, { kind: "directive", type: i1.BrnSliderRange, selector: "[brnSliderRange]" }, { kind: "directive", type: i1.BrnSliderTick, selector: "[brnSliderTick]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
19
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmSlider, isStandalone: true, selector: "hlm-slider, brn-slider [hlm]", inputs: { ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null } }, hostDirectives: [{ directive: i1.BrnSlider, inputs: ["id", "id", "value", "value", "disabled", "disabled", "min", "min", "max", "max", "step", "step", "minStepsBetweenThumbs", "minStepsBetweenThumbs", "inverted", "inverted", "orientation", "orientation", "showTicks", "showTicks", "maxTicks", "maxTicks", "tickLabelInterval", "tickLabelInterval", "formatTick", "formatTick", "draggableRange", "draggableRange", "draggableRangeOnly", "draggableRangeOnly", "aria-label", "aria-label", "aria-labelledby", "aria-labelledby"], outputs: ["valueChange", "valueChange"] }], ngImport: i0, template: "\n <div\n class=\"relative flex w-full items-center group-data-[orientation=vertical]:w-auto group-data-[orientation=vertical]:flex-col\"\n >\n <div\n brnSliderTrack\n class=\"bg-muted rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5 relative grow overflow-hidden\"\n >\n <div\n class=\"bg-primary absolute select-none data-draggable-range:cursor-move data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\"\n brnSliderRange\n ></div>\n </div>\n\n @for (i of _slider.thumbIndexes(); track i) {\n <span\n class=\"border-primary ring-ring/50 size-4 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden absolute block shrink-0 select-none after:absolute after:-inset-2\"\n brnSliderThumb\n brnFieldControlDescribedBy\n [aria-describedby]=\"ariaDescribedBy()\"\n [attr.aria-invalid]=\"_fieldControl.invalid() ? 'true' : null\"\n ></span>\n }\n </div>\n\n @if (_slider.showTicks()) {\n <div\n class=\"px-2 group-data-[orientation=vertical]:px-0 group-data-[orientation=vertical]:py-2 text-muted-foreground mt-3 flex w-full items-start justify-between gap-1 text-xs font-medium group-data-[orientation=horizontal]:group-data-inverted:flex-row-reverse group-data-[orientation=vertical]:ms-3 group-data-[orientation=vertical]:mt-0 group-data-[orientation=vertical]:w-auto group-data-[orientation=vertical]:flex-col-reverse group-data-[orientation=vertical]:group-data-inverted:flex-col\"\n >\n <div\n *brnSliderTick=\"let tick; let formattedTick = formattedTick\"\n class=\"group flex w-0 flex-col items-center justify-center gap-2 group-data-[orientation=vertical]:h-0 group-data-[orientation=vertical]:w-auto group-data-[orientation=vertical]:flex-row\"\n >\n <div\n class=\"bg-muted-foreground/70 h-1 w-px group-data-[orientation=vertical]:h-px group-data-[orientation=vertical]:w-1 group-data-[orientation=horizontal]:group-data-[skip]:h-0.5 group-data-[orientation=vertical]:group-data-[skip]:w-0.5\"\n ></div>\n <div class=\"text-center group-data-[skip]:opacity-0\">{{ formattedTick }}</div>\n </div>\n </div>\n }\n ", isInline: true, dependencies: [{ kind: "directive", type: i1.BrnSliderTrack, selector: "[brnSliderTrack]" }, { kind: "directive", type: i1.BrnSliderThumb, selector: "[brnSliderThumb]" }, { kind: "directive", type: i1.BrnSliderRange, selector: "[brnSliderRange]" }, { kind: "directive", type: i1.BrnSliderTick, selector: "[brnSliderTick]" }, { kind: "directive", type: BrnFieldControlDescribedBy, selector: "[brnFieldControlDescribedBy]", inputs: ["aria-describedby"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
16
20
|
}
|
|
17
21
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmSlider, decorators: [{
|
|
18
22
|
type: Component,
|
|
19
23
|
args: [{
|
|
20
24
|
selector: 'hlm-slider, brn-slider [hlm]',
|
|
21
|
-
imports: [BrnSliderImports],
|
|
25
|
+
imports: [BrnSliderImports, BrnFieldControlDescribedBy],
|
|
22
26
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
23
27
|
hostDirectives: [
|
|
24
28
|
{
|
|
@@ -45,9 +49,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
|
|
|
45
49
|
outputs: ['valueChange'],
|
|
46
50
|
},
|
|
47
51
|
],
|
|
48
|
-
template: "\n <div\n class=\"relative flex w-full items-center group-data-[orientation=vertical]:w-auto group-data-[orientation=vertical]:flex-col\"\n >\n <div\n brnSliderTrack\n class=\"bg-muted rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5 relative grow overflow-hidden\"\n >\n <div\n class=\"bg-primary absolute select-none data-draggable-range:cursor-move data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\"\n brnSliderRange\n ></div>\n </div>\n\n @for (i of _slider.thumbIndexes(); track i) {\n <span\n class=\"border-primary ring-ring/50 size-4 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden absolute block shrink-0 select-none after:absolute after:-inset-2\"\n brnSliderThumb\n ></span>\n }\n </div>\n\n @if (_slider.showTicks()) {\n <div\n class=\"px-2 group-data-[orientation=vertical]:px-0 group-data-[orientation=vertical]:py-2 text-muted-foreground mt-3 flex w-full items-start justify-between gap-1 text-xs font-medium group-data-[orientation=horizontal]:group-data-inverted:flex-row-reverse group-data-[orientation=vertical]:ms-3 group-data-[orientation=vertical]:mt-0 group-data-[orientation=vertical]:w-auto group-data-[orientation=vertical]:flex-col-reverse group-data-[orientation=vertical]:group-data-inverted:flex-col\"\n >\n <div\n *brnSliderTick=\"let tick; let formattedTick = formattedTick\"\n class=\"group flex w-0 flex-col items-center justify-center gap-2 group-data-[orientation=vertical]:h-0 group-data-[orientation=vertical]:w-auto group-data-[orientation=vertical]:flex-row\"\n >\n <div\n class=\"bg-muted-foreground/70 h-1 w-px group-data-[orientation=vertical]:h-px group-data-[orientation=vertical]:w-1 group-data-[orientation=horizontal]:group-data-[skip]:h-0.5 group-data-[orientation=vertical]:group-data-[skip]:w-0.5\"\n ></div>\n <div class=\"text-center group-data-[skip]:opacity-0\">{{ formattedTick }}</div>\n </div>\n </div>\n }\n ",
|
|
52
|
+
template: "\n <div\n class=\"relative flex w-full items-center group-data-[orientation=vertical]:w-auto group-data-[orientation=vertical]:flex-col\"\n >\n <div\n brnSliderTrack\n class=\"bg-muted rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5 relative grow overflow-hidden\"\n >\n <div\n class=\"bg-primary absolute select-none data-draggable-range:cursor-move data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\"\n brnSliderRange\n ></div>\n </div>\n\n @for (i of _slider.thumbIndexes(); track i) {\n <span\n class=\"border-primary ring-ring/50 size-4 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden absolute block shrink-0 select-none after:absolute after:-inset-2\"\n brnSliderThumb\n brnFieldControlDescribedBy\n [aria-describedby]=\"ariaDescribedBy()\"\n [attr.aria-invalid]=\"_fieldControl.invalid() ? 'true' : null\"\n ></span>\n }\n </div>\n\n @if (_slider.showTicks()) {\n <div\n class=\"px-2 group-data-[orientation=vertical]:px-0 group-data-[orientation=vertical]:py-2 text-muted-foreground mt-3 flex w-full items-start justify-between gap-1 text-xs font-medium group-data-[orientation=horizontal]:group-data-inverted:flex-row-reverse group-data-[orientation=vertical]:ms-3 group-data-[orientation=vertical]:mt-0 group-data-[orientation=vertical]:w-auto group-data-[orientation=vertical]:flex-col-reverse group-data-[orientation=vertical]:group-data-inverted:flex-col\"\n >\n <div\n *brnSliderTick=\"let tick; let formattedTick = formattedTick\"\n class=\"group flex w-0 flex-col items-center justify-center gap-2 group-data-[orientation=vertical]:h-0 group-data-[orientation=vertical]:w-auto group-data-[orientation=vertical]:flex-row\"\n >\n <div\n class=\"bg-muted-foreground/70 h-1 w-px group-data-[orientation=vertical]:h-px group-data-[orientation=vertical]:w-1 group-data-[orientation=horizontal]:group-data-[skip]:h-0.5 group-data-[orientation=vertical]:group-data-[skip]:w-0.5\"\n ></div>\n <div class=\"text-center group-data-[skip]:opacity-0\">{{ formattedTick }}</div>\n </div>\n </div>\n }\n ",
|
|
49
53
|
}]
|
|
50
|
-
}], ctorParameters: () => [] });
|
|
54
|
+
}], ctorParameters: () => [], propDecorators: { ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }] } });
|
|
51
55
|
|
|
52
56
|
/*
|
|
53
57
|
* Public API Surface of slider
|
package/slider/types/slider.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { BrnFieldControl } from '@spartan-ng/brain/field';
|
|
1
3
|
import * as i1 from '@spartan-ng/brain/slider';
|
|
2
4
|
import { BrnSlider } from '@spartan-ng/brain/slider';
|
|
3
|
-
import * as i0 from '@angular/core';
|
|
4
5
|
|
|
5
6
|
declare class HlmSlider {
|
|
6
7
|
protected readonly _slider: BrnSlider;
|
|
8
|
+
protected readonly _fieldControl: BrnFieldControl;
|
|
9
|
+
/** Description IDs applied to every thumb, merged with enclosing Spartan field descriptions. */
|
|
10
|
+
readonly ariaDescribedBy: i0.InputSignal<string | null>;
|
|
7
11
|
constructor();
|
|
8
12
|
static ɵfac: i0.ɵɵFactoryDeclaration<HlmSlider, never>;
|
|
9
|
-
static ɵcmp: i0.ɵɵComponentDeclaration<HlmSlider, "hlm-slider, brn-slider [hlm]", never, {}, {}, never, never, true, [{ directive: typeof i1.BrnSlider; inputs: { "id": "id"; "value": "value"; "disabled": "disabled"; "min": "min"; "max": "max"; "step": "step"; "minStepsBetweenThumbs": "minStepsBetweenThumbs"; "inverted": "inverted"; "orientation": "orientation"; "showTicks": "showTicks"; "maxTicks": "maxTicks"; "tickLabelInterval": "tickLabelInterval"; "formatTick": "formatTick"; "draggableRange": "draggableRange"; "draggableRangeOnly": "draggableRangeOnly"; "aria-label": "aria-label"; "aria-labelledby": "aria-labelledby"; }; outputs: { "valueChange": "valueChange"; }; }]>;
|
|
13
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<HlmSlider, "hlm-slider, brn-slider [hlm]", never, { "ariaDescribedBy": { "alias": "aria-describedby"; "required": false; "isSignal": true; }; }, {}, never, never, true, [{ directive: typeof i1.BrnSlider; inputs: { "id": "id"; "value": "value"; "disabled": "disabled"; "min": "min"; "max": "max"; "step": "step"; "minStepsBetweenThumbs": "minStepsBetweenThumbs"; "inverted": "inverted"; "orientation": "orientation"; "showTicks": "showTicks"; "maxTicks": "maxTicks"; "tickLabelInterval": "tickLabelInterval"; "formatTick": "formatTick"; "draggableRange": "draggableRange"; "draggableRangeOnly": "draggableRangeOnly"; "aria-label": "aria-label"; "aria-labelledby": "aria-labelledby"; }; outputs: { "valueChange": "valueChange"; }; }]>;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
declare const HlmSliderImports: readonly [typeof HlmSlider];
|