@lilydesignsystem/angular-search-picker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -0
- package/fesm2022/lilydesignsystem-angular-search-picker.mjs +609 -0
- package/fesm2022/lilydesignsystem-angular-search-picker.mjs.map +1 -0
- package/package.json +48 -0
- package/types/lilydesignsystem-angular-search-picker.d.ts +183 -0
- package/types/lilydesignsystem-angular-search-picker.d.ts.map +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# SearchPicker (Angular helper)
|
|
2
|
+
|
|
3
|
+
A headless Angular 20 site-search control: a single-icon button (a
|
|
4
|
+
bundled magnifying-glass SVG) that opens a dropdown holding a search
|
|
5
|
+
field and, at its right, a submit button labelled `⏎`. Pressing Return
|
|
6
|
+
in the field, or the `⏎` button, navigates to `/?<query>` — a search for
|
|
7
|
+
`foo` goes to `/?foo`.
|
|
8
|
+
|
|
9
|
+
The single source of truth is [spec/index.md](./spec/index.md). This
|
|
10
|
+
file is the human-readable guide.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { SearchPicker, searchHref } from "@lilydesignsystem/angular-search-picker";
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { Component } from "@angular/core";
|
|
22
|
+
import { SearchPicker } from "@lilydesignsystem/angular-search-picker";
|
|
23
|
+
|
|
24
|
+
@Component({
|
|
25
|
+
selector: "app-header",
|
|
26
|
+
standalone: true,
|
|
27
|
+
imports: [SearchPicker],
|
|
28
|
+
template: `
|
|
29
|
+
<lily-search-picker
|
|
30
|
+
label="Search this site"
|
|
31
|
+
inputLabel="Search terms"
|
|
32
|
+
submitLabel="Search"
|
|
33
|
+
/>
|
|
34
|
+
`,
|
|
35
|
+
})
|
|
36
|
+
export class AppHeader {}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
That is the whole wiring: a search for `foo` performs a GET to `/?foo`.
|
|
40
|
+
|
|
41
|
+
## Where the search goes
|
|
42
|
+
|
|
43
|
+
The destination is `searchHref(query, action)`:
|
|
44
|
+
|
|
45
|
+
| You type | `action` | Destination |
|
|
46
|
+
| ------------ | ----------- | ------------- |
|
|
47
|
+
| `foo` | `"/"` | `/?foo` |
|
|
48
|
+
| ` foo bar ` | `"/"` | `/?foo%20bar` |
|
|
49
|
+
| `a&b` | `"/"` | `/?a%26b` |
|
|
50
|
+
| `foo` | `"/search"` | `/search?foo` |
|
|
51
|
+
|
|
52
|
+
The query is trimmed and URI-encoded, so spaces and `&` cannot split or
|
|
53
|
+
corrupt it. An empty query goes nowhere.
|
|
54
|
+
|
|
55
|
+
The bare query (`/?foo`, not `/?q=foo`) is why the component navigates
|
|
56
|
+
in script: a native GET form always sends `name=value` pairs, so it
|
|
57
|
+
cancels the native submission and navigates to the exact URL itself.
|
|
58
|
+
|
|
59
|
+
## Client-side routing
|
|
60
|
+
|
|
61
|
+
By default the component calls `location.assign(href)` — a real GET
|
|
62
|
+
request. In a single-page app, pass a function that hands the URL to
|
|
63
|
+
your router:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { Component, inject } from "@angular/core";
|
|
67
|
+
import { Router } from "@angular/router";
|
|
68
|
+
import { SearchPicker } from "@lilydesignsystem/angular-search-picker";
|
|
69
|
+
|
|
70
|
+
@Component({
|
|
71
|
+
selector: "app-header",
|
|
72
|
+
standalone: true,
|
|
73
|
+
imports: [SearchPicker],
|
|
74
|
+
template: `
|
|
75
|
+
<lily-search-picker
|
|
76
|
+
label="Search this site"
|
|
77
|
+
inputLabel="Search terms"
|
|
78
|
+
submitLabel="Search"
|
|
79
|
+
[navigate]="navigate"
|
|
80
|
+
(searched)="record($event.query)"
|
|
81
|
+
/>
|
|
82
|
+
`,
|
|
83
|
+
})
|
|
84
|
+
export class AppHeader {
|
|
85
|
+
private readonly router = inject(Router);
|
|
86
|
+
readonly navigate = (href: string) => this.router.navigateByUrl(href);
|
|
87
|
+
record(query: string): void {
|
|
88
|
+
console.log("searched for", query);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`(searched)` emits `{ query, href }` before navigating, for analytics or
|
|
94
|
+
to record the query. It is named `searched`, not `search`, because
|
|
95
|
+
`<input type="search">` fires a native bubbling `search` DOM event that
|
|
96
|
+
Angular would also deliver to a `(search)` binding.
|
|
97
|
+
|
|
98
|
+
## Inputs and outputs
|
|
99
|
+
|
|
100
|
+
Full table in [spec/index.md §4.1](./spec/index.md#41-inputs--outputs).
|
|
101
|
+
Required: `label`, `inputLabel`, `submitLabel` — no English defaults,
|
|
102
|
+
because every user-facing string is yours to localise. Optional:
|
|
103
|
+
`placeholder`, `value` (two-way, `[(value)]`), `action`, `navigate`,
|
|
104
|
+
`className`. Output: `searched`. A projected `<ng-template>` (optionally
|
|
105
|
+
marked `lilySearchPickerIcon`) replaces the icon.
|
|
106
|
+
|
|
107
|
+
## Accessibility
|
|
108
|
+
|
|
109
|
+
- The icon is `aria-hidden`; the button's name comes from `label`.
|
|
110
|
+
- The dropdown is a real `<form role="search">` — a search landmark
|
|
111
|
+
named by `label` — with a real `type="search"` field and
|
|
112
|
+
`type="submit"` button, so Return-to-submit and mobile search
|
|
113
|
+
keyboards just work.
|
|
114
|
+
- `⏎` is the visible label only: it is `aria-hidden`, and the submit
|
|
115
|
+
button's name is `submitLabel`.
|
|
116
|
+
- Opening focuses the field; `Escape` closes and returns focus to the
|
|
117
|
+
button; clicking outside or tabbing away closes.
|
|
118
|
+
- See [docs/accessibility.md](./docs/accessibility.md) for the tradeoffs.
|
|
119
|
+
|
|
120
|
+
## Styling
|
|
121
|
+
|
|
122
|
+
Class hooks: `.search-picker` (root), `.search-picker-button`,
|
|
123
|
+
`.search-picker-icon`, `.search-picker-panel`, `.search-picker-form`,
|
|
124
|
+
`.search-picker-input`, `.search-picker-submit`,
|
|
125
|
+
`.search-picker-submit-symbol`, `.search-picker-tooltip`.
|
|
126
|
+
|
|
127
|
+
The package ships no CSS beyond the icon markup. The root `themes/`
|
|
128
|
+
stylesheets position the panel and lay the field and `⏎` button out in
|
|
129
|
+
a row.
|
|
130
|
+
|
|
131
|
+
## Tests
|
|
132
|
+
|
|
133
|
+
`npx vitest run lily-design-system-angular-search-picker` from the
|
|
134
|
+
catalog root — 24 cases, one per §7 clause.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
Lily™ and Lily Design System™ are trademarks.
|
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
import { NgTemplateOutlet } from "@angular/common";
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
import { ChangeDetectionStrategy, Component, Directive, TemplateRef, computed, contentChild, effect, input, model, output, signal, viewChild } from "@angular/core";
|
|
4
|
+
import { IconButton } from "@lilydesignsystem/angular-headless";
|
|
5
|
+
const RETURN_SYMBOL = "⏎";
|
|
6
|
+
function searchHref(query, action = "/") {
|
|
7
|
+
return `${action}?${encodeURIComponent(query.trim())}`;
|
|
8
|
+
}
|
|
9
|
+
let uid = 0;
|
|
10
|
+
function nextSearchPickerId() {
|
|
11
|
+
uid += 1;
|
|
12
|
+
return `search-picker-${uid}`;
|
|
13
|
+
}
|
|
14
|
+
var SearchPickerIcon = class SearchPickerIcon {
|
|
15
|
+
static ngTemplateContextGuard(_dir, _ctx) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
static {
|
|
19
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
20
|
+
minVersion: "12.0.0",
|
|
21
|
+
version: "22.2.1",
|
|
22
|
+
ngImport: i0,
|
|
23
|
+
type: SearchPickerIcon,
|
|
24
|
+
deps: [],
|
|
25
|
+
target: i0.ɵɵFactoryTarget.Directive
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
static {
|
|
29
|
+
this.ɵdir = i0.ɵɵngDeclareDirective({
|
|
30
|
+
minVersion: "14.0.0",
|
|
31
|
+
version: "22.2.1",
|
|
32
|
+
type: SearchPickerIcon,
|
|
33
|
+
isStandalone: true,
|
|
34
|
+
selector: "ng-template[lilySearchPickerIcon]",
|
|
35
|
+
ngImport: i0
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
40
|
+
minVersion: "12.0.0",
|
|
41
|
+
version: "22.2.1",
|
|
42
|
+
ngImport: i0,
|
|
43
|
+
type: SearchPickerIcon,
|
|
44
|
+
decorators: [{
|
|
45
|
+
type: Directive,
|
|
46
|
+
args: [{
|
|
47
|
+
selector: "ng-template[lilySearchPickerIcon]",
|
|
48
|
+
standalone: true
|
|
49
|
+
}]
|
|
50
|
+
}]
|
|
51
|
+
});
|
|
52
|
+
var SearchPicker = class SearchPicker {
|
|
53
|
+
constructor() {
|
|
54
|
+
this.label = input.required(...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
|
|
55
|
+
this.inputLabel = input.required(...ngDevMode ? [{ debugName: "inputLabel" }] : /* istanbul ignore next */ []);
|
|
56
|
+
this.submitLabel = input.required(...ngDevMode ? [{ debugName: "submitLabel" }] : /* istanbul ignore next */ []);
|
|
57
|
+
this.placeholder = input(void 0, ...ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []);
|
|
58
|
+
this.value = model("", ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
|
|
59
|
+
this.action = input("/", ...ngDevMode ? [{ debugName: "action" }] : /* istanbul ignore next */ []);
|
|
60
|
+
this.navigate = input(void 0, ...ngDevMode ? [{ debugName: "navigate" }] : /* istanbul ignore next */ []);
|
|
61
|
+
this.className = input("", ...ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []);
|
|
62
|
+
this.searched = output();
|
|
63
|
+
this.iconTemplate = contentChild(TemplateRef, ...ngDevMode ? [{ debugName: "iconTemplate" }] : /* istanbul ignore next */ []);
|
|
64
|
+
this.rootRef = viewChild.required("rootEl", ...ngDevMode ? [{ debugName: "rootRef" }] : /* istanbul ignore next */ []);
|
|
65
|
+
this.buttonRef = viewChild.required("buttonEl", ...ngDevMode ? [{ debugName: "buttonRef" }] : /* istanbul ignore next */ []);
|
|
66
|
+
this.panelRef = viewChild.required("panelEl", ...ngDevMode ? [{ debugName: "panelRef" }] : /* istanbul ignore next */ []);
|
|
67
|
+
this.inputRef = viewChild.required("inputEl", ...ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []);
|
|
68
|
+
this.baseId = nextSearchPickerId();
|
|
69
|
+
this.panelId = `${this.baseId}-panel`;
|
|
70
|
+
this.returnSymbol = "⏎";
|
|
71
|
+
this.open = signal(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
|
|
72
|
+
this.tooltipId = `${this.baseId}-tooltip`;
|
|
73
|
+
this.hoverButton = signal(false, ...ngDevMode ? [{ debugName: "hoverButton" }] : /* istanbul ignore next */ []);
|
|
74
|
+
this.hoverTooltip = signal(false, ...ngDevMode ? [{ debugName: "hoverTooltip" }] : /* istanbul ignore next */ []);
|
|
75
|
+
this.focusButton = signal(false, ...ngDevMode ? [{ debugName: "focusButton" }] : /* istanbul ignore next */ []);
|
|
76
|
+
this.dismissed = signal(false, ...ngDevMode ? [{ debugName: "dismissed" }] : /* istanbul ignore next */ []);
|
|
77
|
+
this.tooltipVisible = computed(() => !this.open() && !this.dismissed() && (this.hoverButton() || this.hoverTooltip() || this.focusButton()), ...ngDevMode ? [{ debugName: "tooltipVisible" }] : /* istanbul ignore next */ []);
|
|
78
|
+
this.tooltipEscapeListener = effect((onCleanup) => {
|
|
79
|
+
if (!this.tooltipVisible() || typeof document === "undefined") return;
|
|
80
|
+
const onKey = (event) => {
|
|
81
|
+
if (event.key === "Escape") this.dismissed.set(true);
|
|
82
|
+
};
|
|
83
|
+
document.addEventListener("keydown", onKey);
|
|
84
|
+
onCleanup(() => document.removeEventListener("keydown", onKey));
|
|
85
|
+
}, ...ngDevMode ? [{ debugName: "tooltipEscapeListener" }] : /* istanbul ignore next */ []);
|
|
86
|
+
this.childContext = computed(() => {
|
|
87
|
+
const args = {
|
|
88
|
+
open: this.open(),
|
|
89
|
+
query: this.value()
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
$implicit: args,
|
|
93
|
+
...args
|
|
94
|
+
};
|
|
95
|
+
}, ...ngDevMode ? [{ debugName: "childContext" }] : /* istanbul ignore next */ []);
|
|
96
|
+
}
|
|
97
|
+
onTooltipButtonEnter() {
|
|
98
|
+
this.hoverButton.set(true);
|
|
99
|
+
this.dismissed.set(false);
|
|
100
|
+
}
|
|
101
|
+
onTooltipButtonFocusIn() {
|
|
102
|
+
try {
|
|
103
|
+
this.focusButton.set(this.buttonRef().element?.matches(":focus-visible") ?? false);
|
|
104
|
+
} catch {
|
|
105
|
+
this.focusButton.set(true);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
onTooltipButtonFocusOut() {
|
|
109
|
+
this.focusButton.set(false);
|
|
110
|
+
this.dismissed.set(false);
|
|
111
|
+
}
|
|
112
|
+
onTooltipKeydown(event) {
|
|
113
|
+
if (event.key === "Escape" && this.tooltipVisible()) this.dismissed.set(true);
|
|
114
|
+
}
|
|
115
|
+
openPanel() {
|
|
116
|
+
this.open.set(true);
|
|
117
|
+
this.panelRef().nativeElement.removeAttribute("hidden");
|
|
118
|
+
this.inputRef().nativeElement.focus({ preventScroll: true });
|
|
119
|
+
}
|
|
120
|
+
closePanel(refocus = true) {
|
|
121
|
+
if (!this.open()) return;
|
|
122
|
+
if (refocus) this.buttonRef().focus({ preventScroll: true });
|
|
123
|
+
this.open.set(false);
|
|
124
|
+
}
|
|
125
|
+
onButtonClick() {
|
|
126
|
+
if (this.open()) this.closePanel();
|
|
127
|
+
else this.openPanel();
|
|
128
|
+
}
|
|
129
|
+
onPanelKeydown(event) {
|
|
130
|
+
if (event.key === "Escape") {
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
this.closePanel();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
onRootFocusOut(event) {
|
|
136
|
+
const next = event.relatedTarget;
|
|
137
|
+
if (!next || this.rootRef().nativeElement.contains(next)) return;
|
|
138
|
+
this.closePanel(false);
|
|
139
|
+
}
|
|
140
|
+
onDocumentClick(event) {
|
|
141
|
+
if (!this.open()) return;
|
|
142
|
+
const t = event.target;
|
|
143
|
+
if (t && !this.rootRef().nativeElement.contains(t)) this.closePanel(false);
|
|
144
|
+
}
|
|
145
|
+
onSubmit(event) {
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
const query = this.value().trim();
|
|
148
|
+
if (!query) return;
|
|
149
|
+
const href = searchHref(query, this.action());
|
|
150
|
+
this.searched.emit({
|
|
151
|
+
query,
|
|
152
|
+
href
|
|
153
|
+
});
|
|
154
|
+
this.closePanel(false);
|
|
155
|
+
const navigate = this.navigate();
|
|
156
|
+
if (navigate) navigate(href);
|
|
157
|
+
else if (typeof location !== "undefined") location.assign(href);
|
|
158
|
+
}
|
|
159
|
+
static {
|
|
160
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
161
|
+
minVersion: "12.0.0",
|
|
162
|
+
version: "22.2.1",
|
|
163
|
+
ngImport: i0,
|
|
164
|
+
type: SearchPicker,
|
|
165
|
+
deps: [],
|
|
166
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
static {
|
|
170
|
+
this.ɵcmp = i0.ɵɵngDeclareComponent({
|
|
171
|
+
minVersion: "17.0.0",
|
|
172
|
+
version: "22.2.1",
|
|
173
|
+
type: SearchPicker,
|
|
174
|
+
isStandalone: true,
|
|
175
|
+
selector: "lily-search-picker",
|
|
176
|
+
inputs: {
|
|
177
|
+
label: {
|
|
178
|
+
classPropertyName: "label",
|
|
179
|
+
publicName: "label",
|
|
180
|
+
isSignal: true,
|
|
181
|
+
isRequired: true,
|
|
182
|
+
transformFunction: null
|
|
183
|
+
},
|
|
184
|
+
inputLabel: {
|
|
185
|
+
classPropertyName: "inputLabel",
|
|
186
|
+
publicName: "inputLabel",
|
|
187
|
+
isSignal: true,
|
|
188
|
+
isRequired: true,
|
|
189
|
+
transformFunction: null
|
|
190
|
+
},
|
|
191
|
+
submitLabel: {
|
|
192
|
+
classPropertyName: "submitLabel",
|
|
193
|
+
publicName: "submitLabel",
|
|
194
|
+
isSignal: true,
|
|
195
|
+
isRequired: true,
|
|
196
|
+
transformFunction: null
|
|
197
|
+
},
|
|
198
|
+
placeholder: {
|
|
199
|
+
classPropertyName: "placeholder",
|
|
200
|
+
publicName: "placeholder",
|
|
201
|
+
isSignal: true,
|
|
202
|
+
isRequired: false,
|
|
203
|
+
transformFunction: null
|
|
204
|
+
},
|
|
205
|
+
value: {
|
|
206
|
+
classPropertyName: "value",
|
|
207
|
+
publicName: "value",
|
|
208
|
+
isSignal: true,
|
|
209
|
+
isRequired: false,
|
|
210
|
+
transformFunction: null
|
|
211
|
+
},
|
|
212
|
+
action: {
|
|
213
|
+
classPropertyName: "action",
|
|
214
|
+
publicName: "action",
|
|
215
|
+
isSignal: true,
|
|
216
|
+
isRequired: false,
|
|
217
|
+
transformFunction: null
|
|
218
|
+
},
|
|
219
|
+
navigate: {
|
|
220
|
+
classPropertyName: "navigate",
|
|
221
|
+
publicName: "navigate",
|
|
222
|
+
isSignal: true,
|
|
223
|
+
isRequired: false,
|
|
224
|
+
transformFunction: null
|
|
225
|
+
},
|
|
226
|
+
className: {
|
|
227
|
+
classPropertyName: "className",
|
|
228
|
+
publicName: "className",
|
|
229
|
+
isSignal: true,
|
|
230
|
+
isRequired: false,
|
|
231
|
+
transformFunction: null
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
outputs: {
|
|
235
|
+
value: "valueChange",
|
|
236
|
+
searched: "searched"
|
|
237
|
+
},
|
|
238
|
+
host: { listeners: { "document:click": "onDocumentClick($event)" } },
|
|
239
|
+
queries: [{
|
|
240
|
+
propertyName: "iconTemplate",
|
|
241
|
+
first: true,
|
|
242
|
+
predicate: TemplateRef,
|
|
243
|
+
descendants: true,
|
|
244
|
+
isSignal: true
|
|
245
|
+
}],
|
|
246
|
+
viewQueries: [
|
|
247
|
+
{
|
|
248
|
+
propertyName: "rootRef",
|
|
249
|
+
first: true,
|
|
250
|
+
predicate: ["rootEl"],
|
|
251
|
+
descendants: true,
|
|
252
|
+
isSignal: true
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
propertyName: "buttonRef",
|
|
256
|
+
first: true,
|
|
257
|
+
predicate: ["buttonEl"],
|
|
258
|
+
descendants: true,
|
|
259
|
+
isSignal: true
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
propertyName: "panelRef",
|
|
263
|
+
first: true,
|
|
264
|
+
predicate: ["panelEl"],
|
|
265
|
+
descendants: true,
|
|
266
|
+
isSignal: true
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
propertyName: "inputRef",
|
|
270
|
+
first: true,
|
|
271
|
+
predicate: ["inputEl"],
|
|
272
|
+
descendants: true,
|
|
273
|
+
isSignal: true
|
|
274
|
+
}
|
|
275
|
+
],
|
|
276
|
+
ngImport: i0,
|
|
277
|
+
template: `
|
|
278
|
+
<div
|
|
279
|
+
#rootEl
|
|
280
|
+
class="search-picker {{ className() }}"
|
|
281
|
+
(focusout)="onRootFocusOut($event)"
|
|
282
|
+
>
|
|
283
|
+
<lily-icon-button
|
|
284
|
+
#buttonEl
|
|
285
|
+
[label]="label()"
|
|
286
|
+
baseClass="search-picker-button"
|
|
287
|
+
[ariaExpanded]="open()"
|
|
288
|
+
[ariaControls]="panelId"
|
|
289
|
+
(click)="onButtonClick(); hoverButton.set(false)"
|
|
290
|
+
(mouseenter)="onTooltipButtonEnter()"
|
|
291
|
+
(mouseleave)="hoverButton.set(false)"
|
|
292
|
+
(focusin)="onTooltipButtonFocusIn()"
|
|
293
|
+
(focusout)="onTooltipButtonFocusOut()"
|
|
294
|
+
(keydown)="onTooltipKeydown($event)"
|
|
295
|
+
>
|
|
296
|
+
@if (iconTemplate(); as tpl) {
|
|
297
|
+
<ng-container
|
|
298
|
+
[ngTemplateOutlet]="tpl"
|
|
299
|
+
[ngTemplateOutletContext]="childContext()"
|
|
300
|
+
/>
|
|
301
|
+
} @else {
|
|
302
|
+
<svg
|
|
303
|
+
class="search-picker-icon"
|
|
304
|
+
viewBox="0 0 16 16"
|
|
305
|
+
width="1.05rem"
|
|
306
|
+
height="1.05rem"
|
|
307
|
+
aria-hidden="true"
|
|
308
|
+
fill="none"
|
|
309
|
+
stroke="currentColor"
|
|
310
|
+
stroke-width="1.6"
|
|
311
|
+
stroke-linecap="round"
|
|
312
|
+
stroke-linejoin="round"
|
|
313
|
+
>
|
|
314
|
+
<circle cx="7" cy="7" r="4.5" />
|
|
315
|
+
<path d="M10.5 10.5 14 14" />
|
|
316
|
+
</svg>
|
|
317
|
+
}
|
|
318
|
+
</lily-icon-button>
|
|
319
|
+
|
|
320
|
+
<!-- Purely visual: the same text is already the button's aria-label,
|
|
321
|
+
so it is not wired with aria-describedby (that would announce
|
|
322
|
+
the name twice). -->
|
|
323
|
+
<div
|
|
324
|
+
class="search-picker-tooltip"
|
|
325
|
+
role="tooltip"
|
|
326
|
+
[id]="tooltipId"
|
|
327
|
+
[attr.hidden]="tooltipVisible() ? null : ''"
|
|
328
|
+
(mouseenter)="hoverTooltip.set(true)"
|
|
329
|
+
(mouseleave)="hoverTooltip.set(false)"
|
|
330
|
+
>{{ label() }}</div>
|
|
331
|
+
|
|
332
|
+
<!-- The keydown handler only listens for Escape bubbling up from the
|
|
333
|
+
field and the submit button inside; the panel itself takes no
|
|
334
|
+
focus. -->
|
|
335
|
+
<div
|
|
336
|
+
#panelEl
|
|
337
|
+
class="search-picker-panel"
|
|
338
|
+
[id]="panelId"
|
|
339
|
+
[attr.hidden]="open() ? null : ''"
|
|
340
|
+
(keydown)="onPanelKeydown($event)"
|
|
341
|
+
>
|
|
342
|
+
<form
|
|
343
|
+
class="search-picker-form"
|
|
344
|
+
role="search"
|
|
345
|
+
[attr.aria-label]="label()"
|
|
346
|
+
[attr.action]="action()"
|
|
347
|
+
method="get"
|
|
348
|
+
(submit)="onSubmit($event)"
|
|
349
|
+
>
|
|
350
|
+
<input
|
|
351
|
+
#inputEl
|
|
352
|
+
class="search-picker-input"
|
|
353
|
+
type="search"
|
|
354
|
+
[attr.aria-label]="inputLabel()"
|
|
355
|
+
[attr.placeholder]="placeholder() ?? null"
|
|
356
|
+
enterkeyhint="search"
|
|
357
|
+
[value]="value()"
|
|
358
|
+
(input)="value.set($any($event.target).value)"
|
|
359
|
+
/>
|
|
360
|
+
<button
|
|
361
|
+
type="submit"
|
|
362
|
+
class="search-picker-submit"
|
|
363
|
+
[attr.aria-label]="submitLabel()"
|
|
364
|
+
>
|
|
365
|
+
<span class="search-picker-submit-symbol" aria-hidden="true">{{
|
|
366
|
+
returnSymbol
|
|
367
|
+
}}</span>
|
|
368
|
+
</button>
|
|
369
|
+
</form>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
`,
|
|
373
|
+
isInline: true,
|
|
374
|
+
dependencies: [{
|
|
375
|
+
kind: "directive",
|
|
376
|
+
type: NgTemplateOutlet,
|
|
377
|
+
selector: "[ngTemplateOutlet]",
|
|
378
|
+
inputs: [
|
|
379
|
+
"ngTemplateOutletContext",
|
|
380
|
+
"ngTemplateOutlet",
|
|
381
|
+
"ngTemplateOutletInjector"
|
|
382
|
+
]
|
|
383
|
+
}, {
|
|
384
|
+
kind: "component",
|
|
385
|
+
type: IconButton,
|
|
386
|
+
selector: "lily-icon-button",
|
|
387
|
+
inputs: [
|
|
388
|
+
"label",
|
|
389
|
+
"className",
|
|
390
|
+
"baseClass",
|
|
391
|
+
"ariaHaspopup",
|
|
392
|
+
"ariaExpanded",
|
|
393
|
+
"ariaControls",
|
|
394
|
+
"tabIndex",
|
|
395
|
+
"disabled"
|
|
396
|
+
]
|
|
397
|
+
}],
|
|
398
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
403
|
+
minVersion: "12.0.0",
|
|
404
|
+
version: "22.2.1",
|
|
405
|
+
ngImport: i0,
|
|
406
|
+
type: SearchPicker,
|
|
407
|
+
decorators: [{
|
|
408
|
+
type: Component,
|
|
409
|
+
args: [{
|
|
410
|
+
selector: "lily-search-picker",
|
|
411
|
+
standalone: true,
|
|
412
|
+
imports: [NgTemplateOutlet, IconButton],
|
|
413
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
414
|
+
host: { "(document:click)": "onDocumentClick($event)" },
|
|
415
|
+
template: `
|
|
416
|
+
<div
|
|
417
|
+
#rootEl
|
|
418
|
+
class="search-picker {{ className() }}"
|
|
419
|
+
(focusout)="onRootFocusOut($event)"
|
|
420
|
+
>
|
|
421
|
+
<lily-icon-button
|
|
422
|
+
#buttonEl
|
|
423
|
+
[label]="label()"
|
|
424
|
+
baseClass="search-picker-button"
|
|
425
|
+
[ariaExpanded]="open()"
|
|
426
|
+
[ariaControls]="panelId"
|
|
427
|
+
(click)="onButtonClick(); hoverButton.set(false)"
|
|
428
|
+
(mouseenter)="onTooltipButtonEnter()"
|
|
429
|
+
(mouseleave)="hoverButton.set(false)"
|
|
430
|
+
(focusin)="onTooltipButtonFocusIn()"
|
|
431
|
+
(focusout)="onTooltipButtonFocusOut()"
|
|
432
|
+
(keydown)="onTooltipKeydown($event)"
|
|
433
|
+
>
|
|
434
|
+
@if (iconTemplate(); as tpl) {
|
|
435
|
+
<ng-container
|
|
436
|
+
[ngTemplateOutlet]="tpl"
|
|
437
|
+
[ngTemplateOutletContext]="childContext()"
|
|
438
|
+
/>
|
|
439
|
+
} @else {
|
|
440
|
+
<svg
|
|
441
|
+
class="search-picker-icon"
|
|
442
|
+
viewBox="0 0 16 16"
|
|
443
|
+
width="1.05rem"
|
|
444
|
+
height="1.05rem"
|
|
445
|
+
aria-hidden="true"
|
|
446
|
+
fill="none"
|
|
447
|
+
stroke="currentColor"
|
|
448
|
+
stroke-width="1.6"
|
|
449
|
+
stroke-linecap="round"
|
|
450
|
+
stroke-linejoin="round"
|
|
451
|
+
>
|
|
452
|
+
<circle cx="7" cy="7" r="4.5" />
|
|
453
|
+
<path d="M10.5 10.5 14 14" />
|
|
454
|
+
</svg>
|
|
455
|
+
}
|
|
456
|
+
</lily-icon-button>
|
|
457
|
+
|
|
458
|
+
<!-- Purely visual: the same text is already the button's aria-label,
|
|
459
|
+
so it is not wired with aria-describedby (that would announce
|
|
460
|
+
the name twice). -->
|
|
461
|
+
<div
|
|
462
|
+
class="search-picker-tooltip"
|
|
463
|
+
role="tooltip"
|
|
464
|
+
[id]="tooltipId"
|
|
465
|
+
[attr.hidden]="tooltipVisible() ? null : ''"
|
|
466
|
+
(mouseenter)="hoverTooltip.set(true)"
|
|
467
|
+
(mouseleave)="hoverTooltip.set(false)"
|
|
468
|
+
>{{ label() }}</div>
|
|
469
|
+
|
|
470
|
+
<!-- The keydown handler only listens for Escape bubbling up from the
|
|
471
|
+
field and the submit button inside; the panel itself takes no
|
|
472
|
+
focus. -->
|
|
473
|
+
<div
|
|
474
|
+
#panelEl
|
|
475
|
+
class="search-picker-panel"
|
|
476
|
+
[id]="panelId"
|
|
477
|
+
[attr.hidden]="open() ? null : ''"
|
|
478
|
+
(keydown)="onPanelKeydown($event)"
|
|
479
|
+
>
|
|
480
|
+
<form
|
|
481
|
+
class="search-picker-form"
|
|
482
|
+
role="search"
|
|
483
|
+
[attr.aria-label]="label()"
|
|
484
|
+
[attr.action]="action()"
|
|
485
|
+
method="get"
|
|
486
|
+
(submit)="onSubmit($event)"
|
|
487
|
+
>
|
|
488
|
+
<input
|
|
489
|
+
#inputEl
|
|
490
|
+
class="search-picker-input"
|
|
491
|
+
type="search"
|
|
492
|
+
[attr.aria-label]="inputLabel()"
|
|
493
|
+
[attr.placeholder]="placeholder() ?? null"
|
|
494
|
+
enterkeyhint="search"
|
|
495
|
+
[value]="value()"
|
|
496
|
+
(input)="value.set($any($event.target).value)"
|
|
497
|
+
/>
|
|
498
|
+
<button
|
|
499
|
+
type="submit"
|
|
500
|
+
class="search-picker-submit"
|
|
501
|
+
[attr.aria-label]="submitLabel()"
|
|
502
|
+
>
|
|
503
|
+
<span class="search-picker-submit-symbol" aria-hidden="true">{{
|
|
504
|
+
returnSymbol
|
|
505
|
+
}}</span>
|
|
506
|
+
</button>
|
|
507
|
+
</form>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
`
|
|
511
|
+
}]
|
|
512
|
+
}],
|
|
513
|
+
propDecorators: {
|
|
514
|
+
label: [{
|
|
515
|
+
type: i0.Input,
|
|
516
|
+
args: [{
|
|
517
|
+
isSignal: true,
|
|
518
|
+
alias: "label",
|
|
519
|
+
required: true
|
|
520
|
+
}]
|
|
521
|
+
}],
|
|
522
|
+
inputLabel: [{
|
|
523
|
+
type: i0.Input,
|
|
524
|
+
args: [{
|
|
525
|
+
isSignal: true,
|
|
526
|
+
alias: "inputLabel",
|
|
527
|
+
required: true
|
|
528
|
+
}]
|
|
529
|
+
}],
|
|
530
|
+
submitLabel: [{
|
|
531
|
+
type: i0.Input,
|
|
532
|
+
args: [{
|
|
533
|
+
isSignal: true,
|
|
534
|
+
alias: "submitLabel",
|
|
535
|
+
required: true
|
|
536
|
+
}]
|
|
537
|
+
}],
|
|
538
|
+
placeholder: [{
|
|
539
|
+
type: i0.Input,
|
|
540
|
+
args: [{
|
|
541
|
+
isSignal: true,
|
|
542
|
+
alias: "placeholder",
|
|
543
|
+
required: false
|
|
544
|
+
}]
|
|
545
|
+
}],
|
|
546
|
+
value: [{
|
|
547
|
+
type: i0.Input,
|
|
548
|
+
args: [{
|
|
549
|
+
isSignal: true,
|
|
550
|
+
alias: "value",
|
|
551
|
+
required: false
|
|
552
|
+
}]
|
|
553
|
+
}, {
|
|
554
|
+
type: i0.Output,
|
|
555
|
+
args: ["valueChange"]
|
|
556
|
+
}],
|
|
557
|
+
action: [{
|
|
558
|
+
type: i0.Input,
|
|
559
|
+
args: [{
|
|
560
|
+
isSignal: true,
|
|
561
|
+
alias: "action",
|
|
562
|
+
required: false
|
|
563
|
+
}]
|
|
564
|
+
}],
|
|
565
|
+
navigate: [{
|
|
566
|
+
type: i0.Input,
|
|
567
|
+
args: [{
|
|
568
|
+
isSignal: true,
|
|
569
|
+
alias: "navigate",
|
|
570
|
+
required: false
|
|
571
|
+
}]
|
|
572
|
+
}],
|
|
573
|
+
className: [{
|
|
574
|
+
type: i0.Input,
|
|
575
|
+
args: [{
|
|
576
|
+
isSignal: true,
|
|
577
|
+
alias: "className",
|
|
578
|
+
required: false
|
|
579
|
+
}]
|
|
580
|
+
}],
|
|
581
|
+
searched: [{
|
|
582
|
+
type: i0.Output,
|
|
583
|
+
args: ["searched"]
|
|
584
|
+
}],
|
|
585
|
+
iconTemplate: [{
|
|
586
|
+
type: i0.ContentChild,
|
|
587
|
+
args: [i0.forwardRef(() => TemplateRef), { isSignal: true }]
|
|
588
|
+
}],
|
|
589
|
+
rootRef: [{
|
|
590
|
+
type: i0.ViewChild,
|
|
591
|
+
args: ["rootEl", { isSignal: true }]
|
|
592
|
+
}],
|
|
593
|
+
buttonRef: [{
|
|
594
|
+
type: i0.ViewChild,
|
|
595
|
+
args: ["buttonEl", { isSignal: true }]
|
|
596
|
+
}],
|
|
597
|
+
panelRef: [{
|
|
598
|
+
type: i0.ViewChild,
|
|
599
|
+
args: ["panelEl", { isSignal: true }]
|
|
600
|
+
}],
|
|
601
|
+
inputRef: [{
|
|
602
|
+
type: i0.ViewChild,
|
|
603
|
+
args: ["inputEl", { isSignal: true }]
|
|
604
|
+
}]
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
export { RETURN_SYMBOL, SearchPicker, SearchPickerIcon, nextSearchPickerId, searchHref };
|
|
608
|
+
|
|
609
|
+
//# sourceMappingURL=lilydesignsystem-angular-search-picker.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lilydesignsystem-angular-search-picker.mjs","names":[],"sources":["../../search-picker.component.ts"],"sourcesContent":["import { NgTemplateOutlet } from \"@angular/common\";\nimport {\n ChangeDetectionStrategy,\n Component,\n Directive,\n ElementRef,\n TemplateRef,\n computed,\n contentChild,\n effect,\n input,\n model,\n output,\n signal,\n viewChild,\n} from \"@angular/core\";\n// Only the trigger button composes a headless primitive. The panel is a\n// real <form role=\"search\"> with a real search field and a real submit\n// button — a disclosure, not a listbox or a menu — so headless `Listbox`\n// is the wrong widget for it, not merely an unmigrated one.\nimport { IconButton } from \"@lilydesignsystem/angular-headless\";\n\n/**\n * The submit button's visible content: U+23CE RETURN SYMBOL, a bare\n * literal character (never an escape — see `bin/test`'s glyph check).\n * It is the button's visible label only; the accessible name comes from\n * the required `submitLabel` input, so assistive technology never has to\n * announce a symbol.\n */\nexport const RETURN_SYMBOL = \"⏎\";\n\n/** Context passed to a custom icon `<ng-template>` (the button icon). */\nexport type ChildArgs = {\n /** Is the search panel open? */\n open: boolean;\n /** The current text in the search field. */\n query: string;\n};\n\n/** Payload of the `searched` output: the trimmed query and its destination. */\nexport type SearchEvent = {\n /** The trimmed, non-empty query. */\n query: string;\n /** Where the search navigates: `searchHref(query, action)`. */\n href: string;\n};\n\n/**\n * The destination for a query: `action` + `?` + the URI-encoded, trimmed\n * query. `searchHref(\"foo\")` is `\"/?foo\"`; `searchHref(\"foo bar\")` is\n * `\"/?foo%20bar\"`.\n */\nexport function searchHref(query: string, action = \"/\"): string {\n return `${action}?${encodeURIComponent(query.trim())}`;\n}\n\nlet uid = 0;\n/** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */\nexport function nextSearchPickerId(): string {\n uid += 1;\n return `search-picker-${uid}`;\n}\n\n/**\n * Optional marker for the projected icon template. Gives consumers typed\n * `let-` variables:\n *\n * ```html\n * <lily-search-picker label=\"Search\" inputLabel=\"Search terms\" submitLabel=\"Search\">\n * <ng-template lilySearchPickerIcon let-args>{{ args.open ? \"×\" : \"🔍\" }}</ng-template>\n * </lily-search-picker>\n * ```\n *\n * The component queries any projected `<ng-template>`, so the marker is\n * for type-checking and readability, not for matching.\n */\n@Directive({\n selector: \"ng-template[lilySearchPickerIcon]\",\n standalone: true,\n})\nexport class SearchPickerIcon {\n static ngTemplateContextGuard(\n _dir: SearchPickerIcon,\n _ctx: unknown,\n ): _ctx is ChildArgs & { $implicit: ChildArgs } {\n return true;\n }\n}\n\n/**\n * SearchPicker — a headless site-search control.\n *\n * A single-icon button (a bundled magnifying-glass SVG) that opens a\n * dropdown holding a search field and, at its right, a `⏎` submit button.\n * Return in the field, or the `⏎` button, navigates to `/?<query>` — a\n * search for `foo` goes to `/?foo`.\n *\n * Like `share-picker` this owns an *action*, not a preference: it applies\n * nothing to the document and persists nothing. See `spec/index.md` for\n * the full contract.\n */\n@Component({\n selector: \"lily-search-picker\",\n standalone: true,\n imports: [NgTemplateOutlet, IconButton],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n \"(document:click)\": \"onDocumentClick($event)\",\n },\n template: `\n <div\n #rootEl\n class=\"search-picker {{ className() }}\"\n (focusout)=\"onRootFocusOut($event)\"\n >\n <lily-icon-button\n #buttonEl\n [label]=\"label()\"\n baseClass=\"search-picker-button\"\n [ariaExpanded]=\"open()\"\n [ariaControls]=\"panelId\"\n (click)=\"onButtonClick(); hoverButton.set(false)\"\n (mouseenter)=\"onTooltipButtonEnter()\"\n (mouseleave)=\"hoverButton.set(false)\"\n (focusin)=\"onTooltipButtonFocusIn()\"\n (focusout)=\"onTooltipButtonFocusOut()\"\n (keydown)=\"onTooltipKeydown($event)\"\n >\n @if (iconTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"childContext()\"\n />\n } @else {\n <svg\n class=\"search-picker-icon\"\n viewBox=\"0 0 16 16\"\n width=\"1.05rem\"\n height=\"1.05rem\"\n aria-hidden=\"true\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <circle cx=\"7\" cy=\"7\" r=\"4.5\" />\n <path d=\"M10.5 10.5 14 14\" />\n </svg>\n }\n </lily-icon-button>\n\n <!-- Purely visual: the same text is already the button's aria-label,\n so it is not wired with aria-describedby (that would announce\n the name twice). -->\n <div\n class=\"search-picker-tooltip\"\n role=\"tooltip\"\n [id]=\"tooltipId\"\n [attr.hidden]=\"tooltipVisible() ? null : ''\"\n (mouseenter)=\"hoverTooltip.set(true)\"\n (mouseleave)=\"hoverTooltip.set(false)\"\n >{{ label() }}</div>\n\n <!-- The keydown handler only listens for Escape bubbling up from the\n field and the submit button inside; the panel itself takes no\n focus. -->\n <div\n #panelEl\n class=\"search-picker-panel\"\n [id]=\"panelId\"\n [attr.hidden]=\"open() ? null : ''\"\n (keydown)=\"onPanelKeydown($event)\"\n >\n <form\n class=\"search-picker-form\"\n role=\"search\"\n [attr.aria-label]=\"label()\"\n [attr.action]=\"action()\"\n method=\"get\"\n (submit)=\"onSubmit($event)\"\n >\n <input\n #inputEl\n class=\"search-picker-input\"\n type=\"search\"\n [attr.aria-label]=\"inputLabel()\"\n [attr.placeholder]=\"placeholder() ?? null\"\n enterkeyhint=\"search\"\n [value]=\"value()\"\n (input)=\"value.set($any($event.target).value)\"\n />\n <button\n type=\"submit\"\n class=\"search-picker-submit\"\n [attr.aria-label]=\"submitLabel()\"\n >\n <span class=\"search-picker-submit-symbol\" aria-hidden=\"true\">{{\n returnSymbol\n }}</span>\n </button>\n </form>\n </div>\n </div>\n `,\n})\nexport class SearchPicker {\n /** Accessible name for the icon button and the search landmark. */\n readonly label = input.required<string>();\n /** Accessible name for the search text field. */\n readonly inputLabel = input.required<string>();\n /** Accessible name for the ⏎ submit button. */\n readonly submitLabel = input.required<string>();\n /** Placeholder text for the search field. No default. */\n readonly placeholder = input<string | undefined>(undefined);\n /** The search text. Two-way bindable: `[(value)]`. */\n readonly value = model<string>(\"\");\n /**\n * Path the query is appended to. The search for `foo` navigates to\n * `${action}?foo`; the default `\"/\"` gives `/?foo`.\n */\n readonly action = input<string>(\"/\");\n /**\n * Performs the navigation. Defaults to `location.assign(href)` — a real\n * GET request. Pass a client-side router's navigate function (e.g.\n * `(href) => router.navigateByUrl(href)`) to keep the navigation in-app.\n */\n readonly navigate = input<((href: string) => void) | undefined>(undefined);\n /** Extra CSS class on the root div. */\n readonly className = input<string>(\"\");\n\n /**\n * Fires with the trimmed query and the destination, before navigating.\n * Named `searched`, not `search`: `<input type=\"search\">` fires a\n * native, bubbling `search` DOM event in Chromium and WebKit, and\n * Angular binds `(search)` on a component host to BOTH the output and\n * any same-named DOM event — the handler would receive a raw `Event`\n * on every Return.\n */\n readonly searched = output<SearchEvent>();\n\n /** Projected icon template; replaces the default icon when supplied. */\n protected readonly iconTemplate = contentChild(TemplateRef);\n\n private readonly rootRef =\n viewChild.required<ElementRef<HTMLDivElement>>(\"rootEl\");\n // A template-ref-variable on a component tag resolves to the component\n // INSTANCE — exactly what's needed to call IconButton's own `focus()`.\n private readonly buttonRef = viewChild.required<IconButton>(\"buttonEl\");\n private readonly panelRef =\n viewChild.required<ElementRef<HTMLDivElement>>(\"panelEl\");\n private readonly inputRef =\n viewChild.required<ElementRef<HTMLInputElement>>(\"inputEl\");\n\n private readonly baseId = nextSearchPickerId();\n protected readonly panelId = `${this.baseId}-panel`;\n protected readonly returnSymbol = RETURN_SYMBOL;\n\n protected readonly open = signal(false);\n\n // Tooltip: shown while the pointer is over the button or the tooltip\n // itself (hoverable, WCAG 1.4.13) or while the button has keyboard\n // focus; Escape dismisses it without moving focus; never shown while\n // the popup is open, since the popup then explains the control.\n protected readonly tooltipId = `${this.baseId}-tooltip`;\n protected readonly hoverButton = signal(false);\n protected readonly hoverTooltip = signal(false);\n protected readonly focusButton = signal(false);\n protected readonly dismissed = signal(false);\n protected readonly tooltipVisible = computed(\n () =>\n !this.open() &&\n !this.dismissed() &&\n (this.hoverButton() || this.hoverTooltip() || this.focusButton()),\n );\n\n protected onTooltipButtonEnter(): void {\n this.hoverButton.set(true);\n this.dismissed.set(false);\n }\n\n protected onTooltipButtonFocusIn(): void {\n // Keyboard focus only: a mouse click also focuses the button in\n // Chromium, and the tooltip should not stick after a click.\n try {\n this.focusButton.set(\n this.buttonRef().element?.matches(\":focus-visible\") ?? false,\n );\n } catch {\n this.focusButton.set(true); // engine without :focus-visible — err towards showing\n }\n }\n\n protected onTooltipButtonFocusOut(): void {\n this.focusButton.set(false);\n this.dismissed.set(false);\n }\n\n protected onTooltipKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\" && this.tooltipVisible()) this.dismissed.set(true);\n }\n\n // WCAG 1.4.13 \"dismissable\": while the tooltip is visible, Escape works\n // wherever focus is (hover-only tooltips have focus elsewhere). The\n // listener exists only while visible; the effect cleanup removes it on\n // hide and on destroy. No preventDefault/stopPropagation, focus untouched.\n private readonly tooltipEscapeListener = effect((onCleanup) => {\n if (!this.tooltipVisible() || typeof document === \"undefined\") return;\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") this.dismissed.set(true);\n };\n document.addEventListener(\"keydown\", onKey);\n onCleanup(() => document.removeEventListener(\"keydown\", onKey));\n });\n\n protected readonly childContext = computed(() => {\n const args: ChildArgs = { open: this.open(), query: this.value() };\n return { $implicit: args, ...args };\n });\n\n // ---------------------------------------------------------------\n // Open / close\n // ---------------------------------------------------------------\n\n /** Open the panel and move focus into the search field. */\n openPanel(): void {\n this.open.set(true);\n // Unhide the panel now rather than waiting for change detection, so\n // the field is focusable in this same task — whether the host app is\n // zone-based or zoneless, the template binding settles on the same\n // value afterwards. preventScroll: the panel is positioned by\n // consumer CSS, and focusing a field rendered partly off-screen would\n // otherwise scroll the whole page.\n this.panelRef().nativeElement.removeAttribute(\"hidden\");\n this.inputRef().nativeElement.focus({ preventScroll: true });\n }\n\n /** Close the panel; `refocus` returns focus to the trigger. */\n closePanel(refocus = true): void {\n if (!this.open()) return;\n // Focus the trigger BEFORE hiding the panel, so focus never drops to\n // <body> in between.\n if (refocus) this.buttonRef().focus({ preventScroll: true });\n this.open.set(false);\n }\n\n protected onButtonClick(): void {\n if (this.open()) this.closePanel();\n else this.openPanel();\n }\n\n protected onPanelKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\") {\n event.preventDefault();\n this.closePanel();\n }\n }\n\n protected onRootFocusOut(event: FocusEvent): void {\n // Close only when focus moves to a known element outside the picker.\n // A focusout with no relatedTarget is not \"focus left\": Safari does\n // not focus a <button> on click, so pressing ⏎ (or the icon button)\n // blurs the field with relatedTarget = null. Closing there hid the\n // panel before the click landed, so ⏎ never searched and the icon\n // button re-opened instead of closing. Clicks outside the picker are\n // handled by the document click listener below.\n const next = event.relatedTarget as Node | null;\n if (!next || this.rootRef().nativeElement.contains(next)) return;\n this.closePanel(false);\n }\n\n protected onDocumentClick(event: Event): void {\n if (!this.open()) return;\n const t = event.target as Node | null;\n if (t && !this.rootRef().nativeElement.contains(t)) this.closePanel(false);\n }\n\n // ---------------------------------------------------------------\n // Searching\n // ---------------------------------------------------------------\n\n protected onSubmit(event: Event): void {\n // The form's native GET would send `/?name=value`; the contract is\n // the bare query (`/?foo`), so navigation is done here instead.\n event.preventDefault();\n const query = this.value().trim();\n if (!query) return;\n const href = searchHref(query, this.action());\n this.searched.emit({ query, href });\n this.closePanel(false);\n const navigate = this.navigate();\n if (navigate) navigate(href);\n else if (typeof location !== \"undefined\") location.assign(href);\n }\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lilydesignsystem/angular-search-picker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": "=26"
|
|
6
|
+
},
|
|
7
|
+
"description": "Lily Design System Angular 20 search picker: a magnifying-glass icon button opening a search field with a ⏎ submit button that navigates to /?<query>. Headless, SSR-safe, no CSS.",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@lilydesignsystem/angular-headless": "^0.2.0",
|
|
10
|
+
"tslib": "^2.3.0"
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"@angular/core": ">=20.0.0 <23.0.0",
|
|
14
|
+
"@angular/common": ">=20.0.0 <23.0.0"
|
|
15
|
+
},
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"keywords": [
|
|
18
|
+
"lily",
|
|
19
|
+
"design",
|
|
20
|
+
"system",
|
|
21
|
+
"angular",
|
|
22
|
+
"search",
|
|
23
|
+
"picker"
|
|
24
|
+
],
|
|
25
|
+
"author": "Joel Parker Henderson <joel@joelparkerhenderson.com>",
|
|
26
|
+
"license": "MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/LilyDesignSystem/lily-design-system-angular-helpers.git",
|
|
30
|
+
"directory": "lily-design-system-angular-search-picker"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://lilydesignsystem.com/",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/LilyDesignSystem/lily-design-system/issues"
|
|
35
|
+
},
|
|
36
|
+
"module": "fesm2022/lilydesignsystem-angular-search-picker.mjs",
|
|
37
|
+
"typings": "types/lilydesignsystem-angular-search-picker.d.ts",
|
|
38
|
+
"exports": {
|
|
39
|
+
"./package.json": {
|
|
40
|
+
"default": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
".": {
|
|
43
|
+
"types": "./types/lilydesignsystem-angular-search-picker.d.ts",
|
|
44
|
+
"default": "./fesm2022/lilydesignsystem-angular-search-picker.mjs"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"type": "module"
|
|
48
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { TemplateRef } from "@angular/core";
|
|
3
|
+
/**
|
|
4
|
+
* The submit button's visible content: U+23CE RETURN SYMBOL, a bare
|
|
5
|
+
* literal character (never an escape — see `bin/test`'s glyph check).
|
|
6
|
+
* It is the button's visible label only; the accessible name comes from
|
|
7
|
+
* the required `submitLabel` input, so assistive technology never has to
|
|
8
|
+
* announce a symbol.
|
|
9
|
+
*/
|
|
10
|
+
export declare const RETURN_SYMBOL = "⏎";
|
|
11
|
+
/** Context passed to a custom icon `<ng-template>` (the button icon). */
|
|
12
|
+
type ChildArgs = {
|
|
13
|
+
/** Is the search panel open? */
|
|
14
|
+
open: boolean;
|
|
15
|
+
/** The current text in the search field. */
|
|
16
|
+
query: string;
|
|
17
|
+
};
|
|
18
|
+
/** Payload of the `searched` output: the trimmed query and its destination. */
|
|
19
|
+
type SearchEvent = {
|
|
20
|
+
/** The trimmed, non-empty query. */
|
|
21
|
+
query: string;
|
|
22
|
+
/** Where the search navigates: `searchHref(query, action)`. */
|
|
23
|
+
href: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The destination for a query: `action` + `?` + the URI-encoded, trimmed
|
|
27
|
+
* query. `searchHref("foo")` is `"/?foo"`; `searchHref("foo bar")` is
|
|
28
|
+
* `"/?foo%20bar"`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function searchHref(query: string, action?: string): string;
|
|
31
|
+
/** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */
|
|
32
|
+
export declare function nextSearchPickerId(): string;
|
|
33
|
+
/**
|
|
34
|
+
* Optional marker for the projected icon template. Gives consumers typed
|
|
35
|
+
* `let-` variables:
|
|
36
|
+
*
|
|
37
|
+
* ```html
|
|
38
|
+
* <lily-search-picker label="Search" inputLabel="Search terms" submitLabel="Search">
|
|
39
|
+
* <ng-template lilySearchPickerIcon let-args>{{ args.open ? "×" : "🔍" }}</ng-template>
|
|
40
|
+
* </lily-search-picker>
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* The component queries any projected `<ng-template>`, so the marker is
|
|
44
|
+
* for type-checking and readability, not for matching.
|
|
45
|
+
*/
|
|
46
|
+
export declare class SearchPickerIcon {
|
|
47
|
+
static ngTemplateContextGuard(_dir: SearchPickerIcon, _ctx: unknown): _ctx is ChildArgs & {
|
|
48
|
+
$implicit: ChildArgs;
|
|
49
|
+
};
|
|
50
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<SearchPickerIcon, never>;
|
|
51
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<SearchPickerIcon, "ng-template[lilySearchPickerIcon]", never, {}, {}, never, never, true, never>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* SearchPicker — a headless site-search control.
|
|
55
|
+
*
|
|
56
|
+
* A single-icon button (a bundled magnifying-glass SVG) that opens a
|
|
57
|
+
* dropdown holding a search field and, at its right, a `⏎` submit button.
|
|
58
|
+
* Return in the field, or the `⏎` button, navigates to `/?<query>` — a
|
|
59
|
+
* search for `foo` goes to `/?foo`.
|
|
60
|
+
*
|
|
61
|
+
* Like `share-picker` this owns an *action*, not a preference: it applies
|
|
62
|
+
* nothing to the document and persists nothing. See `spec/index.md` for
|
|
63
|
+
* the full contract.
|
|
64
|
+
*/
|
|
65
|
+
export declare class SearchPicker {
|
|
66
|
+
/** Accessible name for the icon button and the search landmark. */
|
|
67
|
+
readonly label: import("@angular/core").InputSignal<string>;
|
|
68
|
+
/** Accessible name for the search text field. */
|
|
69
|
+
readonly inputLabel: import("@angular/core").InputSignal<string>;
|
|
70
|
+
/** Accessible name for the ⏎ submit button. */
|
|
71
|
+
readonly submitLabel: import("@angular/core").InputSignal<string>;
|
|
72
|
+
/** Placeholder text for the search field. No default. */
|
|
73
|
+
readonly placeholder: import("@angular/core").InputSignal<string | undefined>;
|
|
74
|
+
/** The search text. Two-way bindable: `[(value)]`. */
|
|
75
|
+
readonly value: import("@angular/core").ModelSignal<string>;
|
|
76
|
+
/**
|
|
77
|
+
* Path the query is appended to. The search for `foo` navigates to
|
|
78
|
+
* `${action}?foo`; the default `"/"` gives `/?foo`.
|
|
79
|
+
*/
|
|
80
|
+
readonly action: import("@angular/core").InputSignal<string>;
|
|
81
|
+
/**
|
|
82
|
+
* Performs the navigation. Defaults to `location.assign(href)` — a real
|
|
83
|
+
* GET request. Pass a client-side router's navigate function (e.g.
|
|
84
|
+
* `(href) => router.navigateByUrl(href)`) to keep the navigation in-app.
|
|
85
|
+
*/
|
|
86
|
+
readonly navigate: import("@angular/core").InputSignal<((href: string) => void) | undefined>;
|
|
87
|
+
/** Extra CSS class on the root div. */
|
|
88
|
+
readonly className: import("@angular/core").InputSignal<string>;
|
|
89
|
+
/**
|
|
90
|
+
* Fires with the trimmed query and the destination, before navigating.
|
|
91
|
+
* Named `searched`, not `search`: `<input type="search">` fires a
|
|
92
|
+
* native, bubbling `search` DOM event in Chromium and WebKit, and
|
|
93
|
+
* Angular binds `(search)` on a component host to BOTH the output and
|
|
94
|
+
* any same-named DOM event — the handler would receive a raw `Event`
|
|
95
|
+
* on every Return.
|
|
96
|
+
*/
|
|
97
|
+
readonly searched: import("@angular/core").OutputEmitterRef<SearchEvent>;
|
|
98
|
+
/** Projected icon template; replaces the default icon when supplied. */
|
|
99
|
+
protected readonly iconTemplate: import("@angular/core").Signal<TemplateRef<any> | undefined>;
|
|
100
|
+
private readonly rootRef;
|
|
101
|
+
private readonly buttonRef;
|
|
102
|
+
private readonly panelRef;
|
|
103
|
+
private readonly inputRef;
|
|
104
|
+
private readonly baseId;
|
|
105
|
+
protected readonly panelId: string;
|
|
106
|
+
protected readonly returnSymbol = "⏎";
|
|
107
|
+
protected readonly open: import("@angular/core").WritableSignal<boolean>;
|
|
108
|
+
protected readonly tooltipId: string;
|
|
109
|
+
protected readonly hoverButton: import("@angular/core").WritableSignal<boolean>;
|
|
110
|
+
protected readonly hoverTooltip: import("@angular/core").WritableSignal<boolean>;
|
|
111
|
+
protected readonly focusButton: import("@angular/core").WritableSignal<boolean>;
|
|
112
|
+
protected readonly dismissed: import("@angular/core").WritableSignal<boolean>;
|
|
113
|
+
protected readonly tooltipVisible: import("@angular/core").Signal<boolean>;
|
|
114
|
+
protected onTooltipButtonEnter(): void;
|
|
115
|
+
protected onTooltipButtonFocusIn(): void;
|
|
116
|
+
protected onTooltipButtonFocusOut(): void;
|
|
117
|
+
protected onTooltipKeydown(event: KeyboardEvent): void;
|
|
118
|
+
private readonly tooltipEscapeListener;
|
|
119
|
+
protected readonly childContext: import("@angular/core").Signal<{
|
|
120
|
+
/** Is the search panel open? */
|
|
121
|
+
open: boolean;
|
|
122
|
+
/** The current text in the search field. */
|
|
123
|
+
query: string;
|
|
124
|
+
$implicit: ChildArgs;
|
|
125
|
+
}>;
|
|
126
|
+
/** Open the panel and move focus into the search field. */
|
|
127
|
+
openPanel(): void;
|
|
128
|
+
/** Close the panel; `refocus` returns focus to the trigger. */
|
|
129
|
+
closePanel(refocus?: boolean): void;
|
|
130
|
+
protected onButtonClick(): void;
|
|
131
|
+
protected onPanelKeydown(event: KeyboardEvent): void;
|
|
132
|
+
protected onRootFocusOut(event: FocusEvent): void;
|
|
133
|
+
protected onDocumentClick(event: Event): void;
|
|
134
|
+
protected onSubmit(event: Event): void;
|
|
135
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<SearchPicker, never>;
|
|
136
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<SearchPicker, "lily-search-picker", never, {
|
|
137
|
+
"label": {
|
|
138
|
+
"alias": "label";
|
|
139
|
+
"required": true;
|
|
140
|
+
"isSignal": true;
|
|
141
|
+
};
|
|
142
|
+
"inputLabel": {
|
|
143
|
+
"alias": "inputLabel";
|
|
144
|
+
"required": true;
|
|
145
|
+
"isSignal": true;
|
|
146
|
+
};
|
|
147
|
+
"submitLabel": {
|
|
148
|
+
"alias": "submitLabel";
|
|
149
|
+
"required": true;
|
|
150
|
+
"isSignal": true;
|
|
151
|
+
};
|
|
152
|
+
"placeholder": {
|
|
153
|
+
"alias": "placeholder";
|
|
154
|
+
"required": false;
|
|
155
|
+
"isSignal": true;
|
|
156
|
+
};
|
|
157
|
+
"value": {
|
|
158
|
+
"alias": "value";
|
|
159
|
+
"required": false;
|
|
160
|
+
"isSignal": true;
|
|
161
|
+
};
|
|
162
|
+
"action": {
|
|
163
|
+
"alias": "action";
|
|
164
|
+
"required": false;
|
|
165
|
+
"isSignal": true;
|
|
166
|
+
};
|
|
167
|
+
"navigate": {
|
|
168
|
+
"alias": "navigate";
|
|
169
|
+
"required": false;
|
|
170
|
+
"isSignal": true;
|
|
171
|
+
};
|
|
172
|
+
"className": {
|
|
173
|
+
"alias": "className";
|
|
174
|
+
"required": false;
|
|
175
|
+
"isSignal": true;
|
|
176
|
+
};
|
|
177
|
+
}, {
|
|
178
|
+
"value": "valueChange";
|
|
179
|
+
"searched": "searched";
|
|
180
|
+
}, ["iconTemplate"], never, true, never>;
|
|
181
|
+
}
|
|
182
|
+
export type { ChildArgs, SearchEvent };
|
|
183
|
+
//# sourceMappingURL=lilydesignsystem-angular-search-picker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lilydesignsystem-angular-search-picker.d.ts","names":[],"sources":["../../search-picker.component.ts"],"mappings":";;;;;;;;;qBA6Ba;;KAGD;;EAEV;;EAEA;;;KAIU;;EAEV;;EAEA;;;;;;;wBAQc,WAAW,eAAe;;wBAM1B;;;;;;;;;;;;;;qBAsBH;SACJ,uBACL,MAAM,kBACN,gBACC,QAAQ;IAAc,WAAW;;SAJzB,MAAA,GAAA,qBAAA;SAAA,MAAA,GAAA,uBAAA;;;;;;;;;;;;;;qBA8HA;;WAEF,+BAAK;;WAEL,oCAAU;;WAEV,qCAAW;;WAEX,qCAAW;;WAEX,+BAAK;;;;;WAKL,gCAAM;;;;;;WAMN,kCAAQ,cAAA;;WAER,mCAAS;;;;;;;;;WAUT,kCAAQ,iBAAA;;qBAGE,sCAAY,OAAA;mBAEd;mBAIA;mBACA;mBAEA;mBAGA;qBACE;qBACA;qBAEA,8BAAI;qBAMJ;qBACA,qCAAW;qBACX,sCAAY;qBACZ,qCAAW;qBACX,mCAAS;qBACT,wCAAc;YAOvB;YAKA;YAYA;YAKA,iBAAiB,OAAO;mBAQjB;qBASE,sCAAY;;IAzRzB;;IAEC;;;;EAiSP;;EAaA,WAAW;YAQD;YAKA,eAAe,OAAO;YAOtB,eAAe,OAAO;YAatB,gBAAgB,OAAO;YAUvB,SAAS,OAAO;SA/Kf,MAAA,GAAA,qBAAA;SAAA,MAAA,GAAA,uBAAA;IAAY;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;;IAAA;IAAA"}
|