jekyll-client-search 0.4.0 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/README.developer.md +16 -1
- data/README.md +31 -9
- data/assets/client-search-dropdown.js +57 -27
- data/lib/jekyll/client_search/dropdown_tag.rb +7 -3
- data/lib/jekyll/client_search/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e0accecae865157aa09bda9bf9df5060bcad7a45eeb55555137f963fe1cca93
|
|
4
|
+
data.tar.gz: 8bb447d56a507fb5c7a632ae38433606317bccfda3e2cb01388c129617a49b8a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bcb6ee7ce75d7b75facd5c69db2f0c8479f4e70c0d5e6830b92f65cec936235596f3eb8e97db6621e9b6ec3559868b1f34d3b3ea1b8f5610464b880b88261104
|
|
7
|
+
data.tar.gz: 1c35127143ea928d0156ba202c0c4e6a485729de69afd69e3203f45d5625e0f00848a17e2f4cfe50f9085b8c61e76d16f81b36e05ffa8f27352cb655a42f5eaf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 — 2026-11-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `rake docs:demo` — captures the checked-in illustrative overview as
|
|
8
|
+
animated AVIF + WebP + static PNG (headless Chrome + ffmpeg + img2webp;
|
|
9
|
+
build-host tools), embedded in README via `<picture>`. Examples are
|
|
10
|
+
schematic, not generated from a live Jekyll build.
|
|
11
|
+
|
|
12
|
+
- Dev-tooling bumps: `markdownlint-cli2` 0.24.0, `eslint` ~10.14.0,
|
|
13
|
+
`globals` 17.14.0, `rubocop` 1.92.2, `ollama-ruby` 1.26.0,
|
|
14
|
+
`excon` 1.11.0, `sass-embedded` 1.108.1, `addressable` 2.9.2,
|
|
15
|
+
`csv` 3.3.7, `io-console` 0.9.5, `parser` 3.3.13.0,
|
|
16
|
+
`irb` 1.19.0, `tins` 1.62.0; `BUNDLED WITH` 4.0.9 → 4.0.24.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- The live dropdown now follows the ARIA combobox/listbox pattern:
|
|
21
|
+
focused input with `aria-activedescendant`, uniquely identified plain
|
|
22
|
+
text options, and no nested links inside listbox options.
|
|
23
|
+
- CI Node.js matrix is now `24` / `26` — matching the `engines:
|
|
24
|
+
>=24` floor. Node 22 dropped (maintenance-only).
|
|
25
|
+
- CI action pins bumped: `ruby/setup-ruby` v1.338.0,
|
|
26
|
+
`sonarqube-scan-action` v8.4.1, `semgrep` image 1.183.0.
|
|
27
|
+
- `spec/search_index_page_spec.rb` expanded — now covers the default
|
|
28
|
+
root-level output path (`search-index.json`), an empty document
|
|
29
|
+
list, and field/order preservation.
|
|
30
|
+
- `test/meta.test.js` cross-engine check split into one named `test()`
|
|
31
|
+
per exact-match query — a single failure no longer hides the
|
|
32
|
+
remaining queries.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Benchmark scripts access environment via `ENV.fetch("KEY", default)`
|
|
37
|
+
instead of `ENV["KEY"] || default` (SonarQube: environment variable
|
|
38
|
+
access without a default value).
|
|
39
|
+
- SonarQube coverage denominator no longer counts `benchmarks/**` —
|
|
40
|
+
the ~850 lines of manual benchmark tooling are not instrumented by
|
|
41
|
+
SimpleCov, which diluted the reported project coverage to ~50%.
|
|
42
|
+
|
|
43
|
+
### Security
|
|
44
|
+
|
|
45
|
+
- `overrides` pins `re2js` 2.8.8 — markdownlint 0.42.0 (via
|
|
46
|
+
markdownlint-cli2 0.24.0) pins the vulnerable 2.8.6 (ReDoS in
|
|
47
|
+
lookbehind matching, GHSA-gg6h-894w-4m9c).
|
|
48
|
+
- `nokogiri` 1.19.7 (dev-only, via `ollama-ruby`) — fixes
|
|
49
|
+
GHSA-3w22-6jfg-527w (use-after-free), GHSA-4p39-36wj-23h4
|
|
50
|
+
(mutation XSS), GHSA-v995-44v6-5fqm (JRuby SSRF).
|
|
51
|
+
|
|
3
52
|
## 0.4.0 — 2026-10-24
|
|
4
53
|
|
|
5
54
|
- Dev-tooling bump: `jsdom` 30.1.3 (patch).
|
data/README.developer.md
CHANGED
|
@@ -20,12 +20,27 @@ Both test suites pass with only the committed Wikipedia fixtures and the
|
|
|
20
20
|
committed baseline JSON artifacts. No network access or external services
|
|
21
21
|
are required for the default test run.
|
|
22
22
|
|
|
23
|
+
Specs that need a Jekyll site use `jekyll-test-harness` (`jekyll_build` +
|
|
24
|
+
`jekyll_files` DSL, installed in `spec/spec_helper.rb`) instead of hand-rolled
|
|
25
|
+
`Dir.mktmpdir`/`Jekyll::Site.new` scaffolding — see
|
|
26
|
+
`spec/support/site_helpers.rb` and `spec/generator_spec.rb` for the pattern.
|
|
27
|
+
Note that the gem's `Jekyll::ClientSearch::Generator` subclasses
|
|
28
|
+
`Jekyll::Generator`, so it runs automatically inside `jekyll_build`'s
|
|
29
|
+
`site.process`; specs should assert on the built site directly and stub
|
|
30
|
+
`build_embedding_adapter` (see `stub_embedding_adapter`) before building
|
|
31
|
+
rather than calling `generate` manually. Specs that rebuild the same source
|
|
32
|
+
directory multiple times (`spec/embedding_cache_spec.rb`,
|
|
33
|
+
`spec/system_spec.rb`, Ollama integration) still use their own helpers,
|
|
34
|
+
since the harness builds each site in a fresh temporary directory.
|
|
35
|
+
|
|
23
36
|
## Quality checks and git hooks
|
|
24
37
|
|
|
25
38
|
The project uses a focused quality stack: RuboCop (style), bundler-audit
|
|
26
39
|
(security), RSpec (Ruby tests), `npm test` (JavaScript tests), and Semgrep
|
|
27
40
|
(security scanning). This covers the actionable ground without the
|
|
28
41
|
maintenance burden of additional code-smell tools on a small focused gem.
|
|
42
|
+
`npm test` also runs ESLint, Markdownlint, and jsdom tests for the browser
|
|
43
|
+
runtime, including the dropdown combobox/listbox keyboard and ARIA contract.
|
|
29
44
|
|
|
30
45
|
External automated code review services:
|
|
31
46
|
|
|
@@ -112,7 +127,7 @@ CI. Skip either with `git commit --no-verify` or `git push --no-verify`.
|
|
|
112
127
|
### CI checks
|
|
113
128
|
|
|
114
129
|
The [CI workflow](.github/workflows/ci.yml) runs on every push and pull
|
|
115
|
-
request across Ruby 3.3/3.4 and Node
|
|
130
|
+
request across Ruby 3.3/3.4 and Node 24/26:
|
|
116
131
|
|
|
117
132
|
- `bundle exec rake ci` (rspec, rubocop, syntax checks, npm test, gem build)
|
|
118
133
|
- `bundle exec bundle-audit check --update` (Ruby dependency security)
|
data/README.md
CHANGED
|
@@ -23,6 +23,19 @@ via pre-computed embeddings from a local [Ollama](https://ollama.ai) server.
|
|
|
23
23
|
Related articles can be generated at build time from shared tags, categories,
|
|
24
24
|
and vector similarity.
|
|
25
25
|
|
|
26
|
+
<!-- markdownlint-disable MD033 -->
|
|
27
|
+
<picture>
|
|
28
|
+
<source
|
|
29
|
+
srcset="docs/assets/jekyll-client-search-demo.avif"
|
|
30
|
+
type="image/avif">
|
|
31
|
+
<source
|
|
32
|
+
srcset="docs/assets/jekyll-client-search-demo.webp"
|
|
33
|
+
type="image/webp">
|
|
34
|
+
<img src="docs/assets/jekyll-client-search-demo.png"
|
|
35
|
+
alt="jekyll-client-search demo">
|
|
36
|
+
</picture>
|
|
37
|
+
<!-- markdownlint-enable MD033 -->
|
|
38
|
+
|
|
26
39
|
## Table of contents
|
|
27
40
|
|
|
28
41
|
- [Lightning start](#lightning-start)
|
|
@@ -268,7 +281,8 @@ Tag modes:
|
|
|
268
281
|
`/search/`) with `?q=...` for full results on the search page
|
|
269
282
|
- Arrow Up/Down → highlight items, Enter on highlighted → navigate
|
|
270
283
|
- Click an item → navigate to that document's URL
|
|
271
|
-
- Escape
|
|
284
|
+
- Escape → close the popup and keep focus in the combobox; click outside
|
|
285
|
+
→ close the dropdown
|
|
272
286
|
- Index is lazy-loaded on first keystroke (no cost on pages where the
|
|
273
287
|
visitor doesn't search)
|
|
274
288
|
|
|
@@ -289,24 +303,32 @@ and the search index includes that field (e.g. from jekyll-documents),
|
|
|
289
303
|
an `<img>` icon is rendered before each result title. Set `icon_field:
|
|
290
304
|
null` to disable.
|
|
291
305
|
|
|
292
|
-
**CSS styling:** The dropdown emits no framework classes.
|
|
306
|
+
**CSS styling:** The dropdown emits no framework classes. Its input uses
|
|
307
|
+
the ARIA combobox pattern; the popup is a listbox whose options are plain
|
|
308
|
+
text containers (not nested links or controls). Style with:
|
|
293
309
|
|
|
294
310
|
```css
|
|
295
311
|
.client-search-dropdown { position: relative; }
|
|
296
|
-
.client-search-dropdown
|
|
312
|
+
.client-search-dropdown [role="listbox"] {
|
|
297
313
|
position: absolute; top: 100%; right: 0;
|
|
298
314
|
min-width: 300px; z-index: 30;
|
|
299
315
|
background: white; box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
316
|
+
margin: 0; padding: 0;
|
|
317
|
+
}
|
|
318
|
+
.client-search-dropdown [role="listbox"] ul[role="presentation"] {
|
|
300
319
|
list-style: none; margin: 0; padding: 0;
|
|
301
320
|
}
|
|
302
|
-
.client-search-dropdown
|
|
303
|
-
.
|
|
304
|
-
|
|
305
|
-
.client-search-dropdown
|
|
321
|
+
.client-search-dropdown [role="option"] {
|
|
322
|
+
cursor: pointer; padding: 0.5rem 1rem;
|
|
323
|
+
}
|
|
324
|
+
.client-search-dropdown [role="option"]:hover,
|
|
325
|
+
.client-search-dropdown [role="option"][aria-selected="true"] {
|
|
326
|
+
background: #f5f5f5;
|
|
327
|
+
}
|
|
306
328
|
|
|
307
329
|
/* Type-based styling via data attributes */
|
|
308
|
-
.client-search-dropdown
|
|
309
|
-
.client-search-dropdown
|
|
330
|
+
.client-search-dropdown [role="option"][data-source="documents"] { border-left: 3px solid #48c774; }
|
|
331
|
+
.client-search-dropdown [role="option"][data-file-type="pdf"]::before {
|
|
310
332
|
content: "PDF"; background: #ff0000; color: white;
|
|
311
333
|
padding: 0 0.3em; font-size: 0.7em; margin-right: 0.3em;
|
|
312
334
|
}
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* Features:
|
|
10
10
|
* - Lazy index loading (fetches search-index.json on first keystroke)
|
|
11
11
|
* - Two-stage search (AND first, fuzzy OR fallback) via engine adapter
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
12
|
+
* - ARIA combobox + listbox with plain-text options and optional icons
|
|
13
|
+
* - Active-descendant keyboard navigation (Arrow Up/Down, Enter, Escape)
|
|
14
14
|
* - Enter with no selection → redirect to redirect_url (?q=...)
|
|
15
15
|
* - Click outside or Escape → close dropdown
|
|
16
16
|
* - Multi-instance via [data-client-search-dropdown] attributes
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
var sharedIndex = null;
|
|
21
21
|
var sharedDocuments = null;
|
|
22
22
|
var indexLoadPromise = null;
|
|
23
|
+
var nextDropdownInstanceId = 0;
|
|
24
|
+
var initializedRoots = new WeakSet();
|
|
23
25
|
|
|
24
26
|
function safeUrl(value) {
|
|
25
27
|
if (value === "#") {
|
|
@@ -116,25 +118,27 @@
|
|
|
116
118
|
});
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
function createResultItem(match, documents, iconField) {
|
|
121
|
+
function createResultItem(match, documents, iconField, optionId) {
|
|
120
122
|
var entry = documents.get(match.ref);
|
|
121
123
|
if (!entry) {
|
|
122
124
|
return null;
|
|
123
125
|
}
|
|
124
126
|
|
|
125
|
-
var
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
var option = document.createElement("li");
|
|
128
|
+
option.id = optionId;
|
|
129
|
+
option.setAttribute("role", "option");
|
|
130
|
+
option.setAttribute("aria-selected", "false");
|
|
131
|
+
option.className = "client-search-dropdown-item";
|
|
132
|
+
option.dataset.url = safeUrl(entry.url);
|
|
129
133
|
|
|
130
134
|
if (entry.source) {
|
|
131
|
-
|
|
135
|
+
option.dataset.source = entry.source;
|
|
132
136
|
}
|
|
133
137
|
if (entry.categories && entry.categories.length) {
|
|
134
|
-
|
|
138
|
+
option.dataset.categories = entry.categories.join(" ");
|
|
135
139
|
}
|
|
136
140
|
if (entry.tags && entry.tags.length) {
|
|
137
|
-
|
|
141
|
+
option.dataset.tags = entry.tags.join(" ");
|
|
138
142
|
}
|
|
139
143
|
Object.keys(entry).forEach(function (key) {
|
|
140
144
|
var skip = ["id", "title", "url", "excerpt", "content",
|
|
@@ -145,7 +149,7 @@
|
|
|
145
149
|
}
|
|
146
150
|
var value = entry[key];
|
|
147
151
|
if (typeof value === "string" || typeof value === "number") {
|
|
148
|
-
|
|
152
|
+
option.dataset[toCamelCase(key)] = String(value);
|
|
149
153
|
}
|
|
150
154
|
});
|
|
151
155
|
|
|
@@ -153,22 +157,21 @@
|
|
|
153
157
|
var icon = document.createElement("img");
|
|
154
158
|
icon.className = "client-search-dropdown-icon";
|
|
155
159
|
icon.src = safeUrl(entry[iconField]);
|
|
156
|
-
icon.alt =
|
|
160
|
+
icon.alt = "";
|
|
161
|
+
icon.setAttribute("aria-hidden", "true");
|
|
157
162
|
icon.loading = "lazy";
|
|
158
163
|
icon.style.width = "1em";
|
|
159
164
|
icon.style.height = "1em";
|
|
160
165
|
icon.style.verticalAlign = "middle";
|
|
161
|
-
icon
|
|
162
|
-
li.appendChild(icon);
|
|
166
|
+
option.appendChild(icon);
|
|
163
167
|
}
|
|
164
168
|
|
|
165
|
-
var
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
li.appendChild(link);
|
|
169
|
+
var title = document.createElement("span");
|
|
170
|
+
title.className = "client-search-dropdown-link";
|
|
171
|
+
title.textContent = entry.title;
|
|
172
|
+
option.appendChild(title);
|
|
170
173
|
|
|
171
|
-
return
|
|
174
|
+
return option;
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
function DropdownInstance(root, config) {
|
|
@@ -176,6 +179,7 @@
|
|
|
176
179
|
this.form = root.querySelector("[data-cs-dropdown-form]");
|
|
177
180
|
this.input = root.querySelector("[data-cs-dropdown-input]");
|
|
178
181
|
this.results = root.querySelector("[data-cs-dropdown-results]");
|
|
182
|
+
this.listbox = this.results.closest('[role="listbox"]') || this.results;
|
|
179
183
|
this.config = config;
|
|
180
184
|
this.maxItems = Number.parseInt(this.results.dataset.maxItems, 10) || config.maxItems || 5;
|
|
181
185
|
this.selectedIndex = -1;
|
|
@@ -183,6 +187,9 @@
|
|
|
183
187
|
this.debounceTimer = null;
|
|
184
188
|
this.renderVersion = 0;
|
|
185
189
|
this.adapter = null;
|
|
190
|
+
this.optionIdPrefix = this.results.id
|
|
191
|
+
? this.results.id + "-option"
|
|
192
|
+
: "cs-dropdown-" + (++nextDropdownInstanceId) + "-option";
|
|
186
193
|
}
|
|
187
194
|
|
|
188
195
|
DropdownInstance.prototype.init = function () {
|
|
@@ -190,6 +197,11 @@
|
|
|
190
197
|
return;
|
|
191
198
|
}
|
|
192
199
|
|
|
200
|
+
this.results.setAttribute("aria-hidden", "true");
|
|
201
|
+
this.listbox.setAttribute("aria-hidden", "true");
|
|
202
|
+
this.input.setAttribute("aria-expanded", "false");
|
|
203
|
+
this.input.removeAttribute("aria-activedescendant");
|
|
204
|
+
|
|
193
205
|
var self = this;
|
|
194
206
|
|
|
195
207
|
this.input.addEventListener("input", function () {
|
|
@@ -212,9 +224,9 @@
|
|
|
212
224
|
});
|
|
213
225
|
|
|
214
226
|
this.results.addEventListener("click", function (event) {
|
|
215
|
-
var
|
|
216
|
-
if (
|
|
217
|
-
window.location.href =
|
|
227
|
+
var option = event.target.closest('[role="option"]');
|
|
228
|
+
if (option && option.dataset.url && option.dataset.url !== "#") {
|
|
229
|
+
window.location.href = option.dataset.url;
|
|
218
230
|
}
|
|
219
231
|
});
|
|
220
232
|
};
|
|
@@ -223,10 +235,10 @@
|
|
|
223
235
|
var self = this;
|
|
224
236
|
this.renderVersion += 1;
|
|
225
237
|
clearTimeout(this.debounceTimer);
|
|
238
|
+
this.hide();
|
|
226
239
|
|
|
227
240
|
var query = this.input.value.trim();
|
|
228
241
|
if (!query || query.length < this.config.minChars) {
|
|
229
|
-
this.hide();
|
|
230
242
|
return;
|
|
231
243
|
}
|
|
232
244
|
|
|
@@ -264,7 +276,6 @@
|
|
|
264
276
|
break;
|
|
265
277
|
case "Escape":
|
|
266
278
|
this.hide();
|
|
267
|
-
this.input.blur();
|
|
268
279
|
break;
|
|
269
280
|
case "Tab":
|
|
270
281
|
this.hide();
|
|
@@ -318,6 +329,7 @@
|
|
|
318
329
|
this.results.replaceChildren();
|
|
319
330
|
this.currentItems = [];
|
|
320
331
|
this.selectedIndex = -1;
|
|
332
|
+
this.input.removeAttribute("aria-activedescendant");
|
|
321
333
|
|
|
322
334
|
if (!matches || matches.length === 0) {
|
|
323
335
|
this.hide();
|
|
@@ -328,7 +340,12 @@
|
|
|
328
340
|
var count = Math.min(matches.length, this.maxItems);
|
|
329
341
|
|
|
330
342
|
for (var i = 0; i < count; i++) {
|
|
331
|
-
var item = createResultItem(
|
|
343
|
+
var item = createResultItem(
|
|
344
|
+
matches[i],
|
|
345
|
+
documents,
|
|
346
|
+
iconField,
|
|
347
|
+
this.optionIdPrefix + "-" + i
|
|
348
|
+
);
|
|
332
349
|
if (item) {
|
|
333
350
|
this.results.appendChild(item);
|
|
334
351
|
this.currentItems.push(item);
|
|
@@ -353,18 +370,26 @@
|
|
|
353
370
|
}
|
|
354
371
|
}
|
|
355
372
|
if (this.currentItems[index]) {
|
|
356
|
-
this.currentItems[index].
|
|
373
|
+
this.input.setAttribute("aria-activedescendant", this.currentItems[index].id);
|
|
374
|
+
if (typeof this.currentItems[index].scrollIntoView === "function") {
|
|
375
|
+
this.currentItems[index].scrollIntoView({ block: "nearest" });
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
this.input.removeAttribute("aria-activedescendant");
|
|
357
379
|
}
|
|
358
380
|
};
|
|
359
381
|
|
|
360
382
|
DropdownInstance.prototype.show = function () {
|
|
361
383
|
this.results.setAttribute("aria-hidden", "false");
|
|
384
|
+
this.listbox.setAttribute("aria-hidden", "false");
|
|
362
385
|
this.input.setAttribute("aria-expanded", "true");
|
|
363
386
|
};
|
|
364
387
|
|
|
365
388
|
DropdownInstance.prototype.hide = function () {
|
|
366
389
|
this.results.setAttribute("aria-hidden", "true");
|
|
390
|
+
this.listbox.setAttribute("aria-hidden", "true");
|
|
367
391
|
this.input.setAttribute("aria-expanded", "false");
|
|
392
|
+
this.input.removeAttribute("aria-activedescendant");
|
|
368
393
|
this.results.replaceChildren();
|
|
369
394
|
this.currentItems = [];
|
|
370
395
|
this.selectedIndex = -1;
|
|
@@ -389,6 +414,11 @@
|
|
|
389
414
|
|
|
390
415
|
var roots = document.querySelectorAll("[data-client-search-dropdown]");
|
|
391
416
|
roots.forEach(function (root) {
|
|
417
|
+
if (initializedRoots.has(root)) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
initializedRoots.add(root);
|
|
421
|
+
|
|
392
422
|
var instance = new DropdownInstance(root, mergedConfig);
|
|
393
423
|
instance.init();
|
|
394
424
|
});
|
|
@@ -87,13 +87,17 @@ module Jekyll
|
|
|
87
87
|
<div class="client-search-dropdown" data-client-search-dropdown>
|
|
88
88
|
<form role="search" autocomplete="off" data-cs-dropdown-form>
|
|
89
89
|
<label class="sr-only" for="#{@input_id}">Search</label>
|
|
90
|
-
<input id="#{@input_id}" type="search" name="q"
|
|
90
|
+
<input id="#{@input_id}" type="search" name="q" role="combobox"
|
|
91
|
+
aria-autocomplete="list" aria-haspopup="listbox"
|
|
91
92
|
placeholder="Search…" autocomplete="off"
|
|
92
93
|
aria-expanded="false" aria-controls="#{@results_id}"
|
|
93
94
|
data-cs-dropdown-input>
|
|
94
95
|
</form>
|
|
95
|
-
<
|
|
96
|
-
|
|
96
|
+
<div id="#{@results_id}" role="listbox" aria-label="Search suggestions"
|
|
97
|
+
aria-hidden="true">
|
|
98
|
+
<ul role="presentation" aria-hidden="true"
|
|
99
|
+
data-cs-dropdown-results data-max-items="#{max}"></ul>
|
|
100
|
+
</div>
|
|
97
101
|
</div>
|
|
98
102
|
HTML
|
|
99
103
|
end
|