@waveso/docs 0.3.0 → 0.4.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/CHANGELOG.md +28 -0
- package/dist/react/search-dialog.d.ts +34 -3
- package/dist/react/search-dialog.js +133 -38
- package/dist/styles.css +62 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @waveso/docs
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- The search dialog, corrected by using it. Five defects a reader meets and no test could see, and one rename that follows from the largest of them.
|
|
8
|
+
|
|
9
|
+
**`maxResults` is now `pageSize`, and it is a window rather than a ceiling.** It capped the list at 20 (8 in 0.2.0): on a _six-page_ site "docs" matches 18, so results were unreachable, and the live region announced the slice as though it were the total — not a smaller truth but a false one.
|
|
10
|
+
|
|
11
|
+
The cap was justified by a claim nobody had measured, and measuring it did not support the claim. On a 300-page corpus (2,100 records) a MiniSearch query costs **1.3–3.0 ms**, and rendering _every_ matching row costs **40 ms**, 128 ms at 4× CPU throttle. The search was never the cost; the DOM only becomes one in the thousands.
|
|
12
|
+
|
|
13
|
+
So the list pages. Twenty rows render, and another twenty each time the reader scrolls near the end — the DOM stays bounded and nothing is withheld. The keyboard widens the window too, or `aria-activedescendant` points at an option that is not in the DOM. `aria-setsize` carries the real total on every option, because a listbox rendering 20 of 2,100 that says "20 of 20" tells a reader they have reached the end when they have not.
|
|
14
|
+
|
|
15
|
+
**Migration:** rename the prop. `maxResults={20}` becomes `pageSize={20}`, and it now means "reveal this many at a time" rather than "never show more than this".
|
|
16
|
+
|
|
17
|
+
**The dialog sizes to its results.** It was 32rem tall in every state — measured 514px with no query, 514px with eight results, 514px with none, of which 392px was an empty results area, so a reader typed into a box floating at the top of a large blank rectangle. A flex container defaults to `align-items: stretch`, so the dialog stretched to the viewport and `max-height` capped it at a constant instead of being the ceiling it was written to be. It is content-height now, and starts scrolling at the same 32rem.
|
|
18
|
+
|
|
19
|
+
**Hovering a half-visible result no longer yanks the list.** Pointing at a row clipped by the top or bottom edge set the active option, which fired the scroll-into-view meant for the arrow keys: the row snapped flush, the whole list moved under the cursor, and the cursor was then over a different row. Measured as a 28px jump. Only the keyboard scrolls now, and a new result set explicitly returns to the top — which that effect had been doing by accident.
|
|
20
|
+
|
|
21
|
+
**Every result row says where it lands, in the same words.** The second line was a breadcrumb of page and heading names, except on a page's own record — whose heading _is_ its page title — which got no second line at all rather than repeat itself. On a six-page site that is six of twenty-nine records, so the list came out ragged and the barest rows said the least: a row reading only "Wave Docs" told a reader nothing about what it opened.
|
|
22
|
+
|
|
23
|
+
Every row now shows the page it lands on: the route, without its anchor. The anchor is slugged from the heading printed directly above it, so it spent the line restating line one. The link keeps it, so a hit still deep-links to its section.
|
|
24
|
+
|
|
25
|
+
**Breaking in rendered output:** `.wave-docs-search-result-breadcrumb`, `.wave-docs-search-result-crumb` and `.wave-docs-search-result-crumb-separator` are replaced by a single `.wave-docs-search-result-location`, because a breadcrumb it is not. A screen reader still hears the words — a route read aloud is punctuation — so the option's `aria-label` carries "Layout tokens, Styling" while the visible line carries the address.
|
|
26
|
+
|
|
27
|
+
**A query is at least two characters**, settable with `minQueryLength`. Measured on this package's own documentation: `a` matches 100% of the corpus, `i` 97%, `s` 93%. One character is not a query, it is a reader halfway through typing one, and answering it with everything teaches them that search returns noise. Below the floor nothing runs — no search, no index request — and the dialog says "Keep typing" rather than sitting there answering nothing.
|
|
28
|
+
|
|
29
|
+
Two rather than three, and the difference matters on a docs site: three would refuse `ts`, `js`, `id`, `h1` and `px`, each a real query here and each selective — 10%, 17%, 14%, 3%, 0%. The noise is at one character, so that is where the floor goes.
|
|
30
|
+
|
|
3
31
|
## 0.3.0
|
|
4
32
|
|
|
5
33
|
### Minor Changes
|
|
@@ -26,8 +26,39 @@ interface SearchDialogProps {
|
|
|
26
26
|
placeholder?: string | undefined;
|
|
27
27
|
/** Accessible name for the dialog. Defaults to `'Search documentation'`. */
|
|
28
28
|
dialogLabel?: string | undefined;
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* How many results to render at a time. Defaults to 20.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ NOT A CAP. Every match is reachable — the list renders this many, then
|
|
33
|
+
* another `pageSize` each time the reader scrolls near the end, so the DOM
|
|
34
|
+
* stays bounded without anything being withheld.
|
|
35
|
+
*
|
|
36
|
+
* This was `maxResults`, and it was a hard ceiling of 8. On a *six-page*
|
|
37
|
+
* site "docs" matches 18, so ten results simply could not be reached, and
|
|
38
|
+
* the live region announced "8 results" — not a smaller truth but a false
|
|
39
|
+
* one. The ceiling was justified by a claim nobody had measured, and the
|
|
40
|
+
* measurement did not support it: on a 300-page corpus (2,100 records) a
|
|
41
|
+
* query costs 1.3–3.0 ms and rendering *every* row costs 40 ms, 128 ms at
|
|
42
|
+
* 4x CPU throttle. Paging exists to keep that worst case from ever being
|
|
43
|
+
* reached, not because the search cannot find things.
|
|
44
|
+
*/
|
|
45
|
+
pageSize?: number | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Shortest query that runs. Defaults to 2.
|
|
48
|
+
*
|
|
49
|
+
* A single character is not a query — measured on this package's own docs,
|
|
50
|
+
* "a" matches 100% of the corpus, "i" 97%, "s" 93%. Answering those wastes a
|
|
51
|
+
* render and, worse, teaches a reader mid-word that search returns noise.
|
|
52
|
+
*
|
|
53
|
+
* ⚠️ TWO, NOT THREE, AND THE DIFFERENCE MATTERS ON A DOCS SITE. Three would
|
|
54
|
+
* refuse `ts`, `js`, `id`, `h1`, `px` — every one a real query here, and each
|
|
55
|
+
* one selective: 10%, 17%, 14%, 3%, 0%. The noise is at one character, so
|
|
56
|
+
* that is where the floor goes.
|
|
57
|
+
*
|
|
58
|
+
* A word like `is` still matches 83%; that is a stopword problem rather than
|
|
59
|
+
* a length one, and `miniSearchOptions.processTerm` is the tool for it.
|
|
60
|
+
*/
|
|
61
|
+
minQueryLength?: number | undefined;
|
|
31
62
|
/** Input debounce in milliseconds. Defaults to 120. */
|
|
32
63
|
debounceMs?: number | undefined;
|
|
33
64
|
/** Extra class names for the trigger button, e.g. a navbar's own layout. */
|
|
@@ -64,6 +95,6 @@ interface SearchDialogProps {
|
|
|
64
95
|
* portalled to `document.body`, so a navbar's stacking context cannot trap
|
|
65
96
|
* it behind the page.
|
|
66
97
|
*/
|
|
67
|
-
declare function SearchDialog({ indexUrl, navigate, Link, triggerLabel, placeholder, dialogLabel,
|
|
98
|
+
declare function SearchDialog({ indexUrl, navigate, Link, triggerLabel, placeholder, dialogLabel, pageSize, minQueryLength, debounceMs, className, miniSearchOptions }: SearchDialogProps): ReactNode;
|
|
68
99
|
//#endregion
|
|
69
100
|
export { SearchDialog, SearchDialogProps };
|
|
@@ -26,10 +26,31 @@ const FOCUSABLE_SELECTOR = [
|
|
|
26
26
|
* portalled to `document.body`, so a navbar's stacking context cannot trap
|
|
27
27
|
* it behind the page.
|
|
28
28
|
*/
|
|
29
|
-
function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", placeholder = "Search documentation", dialogLabel = "Search documentation",
|
|
29
|
+
function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", placeholder = "Search documentation", dialogLabel = "Search documentation", pageSize = 20, minQueryLength = 2, debounceMs = 120, className, miniSearchOptions }) {
|
|
30
30
|
const [isOpen, setIsOpen] = useState(false);
|
|
31
31
|
const [query, setQuery] = useState("");
|
|
32
32
|
const [hits, setHits] = useState([]);
|
|
33
|
+
/**
|
|
34
|
+
* How many of `hits` are rendered.
|
|
35
|
+
*
|
|
36
|
+
* `hits` holds every match; this is the window. It grows by `pageSize` when
|
|
37
|
+
* the reader scrolls near the end, and whenever the keyboard walks past it —
|
|
38
|
+
* so an option always exists for `aria-activedescendant` to point at.
|
|
39
|
+
*/
|
|
40
|
+
const [visibleCount, setVisibleCount] = useState(pageSize);
|
|
41
|
+
/**
|
|
42
|
+
* Whether the active option moved because of a key, rather than a pointer.
|
|
43
|
+
*
|
|
44
|
+
* ⚠️ THE SCROLL-INTO-VIEW BELOW MUST NOT RUN FOR A HOVER. Pointing at a row
|
|
45
|
+
* that is half-clipped by the top or bottom edge set the active index, which
|
|
46
|
+
* scrolled that row flush — moving the whole list under the cursor, which
|
|
47
|
+
* then landed on a different row. Measured: hovering the visible sliver of a
|
|
48
|
+
* clipped row jumped the list 28px.
|
|
49
|
+
*
|
|
50
|
+
* A ref rather than state: it records how the *last* change happened and must
|
|
51
|
+
* not itself cause a render.
|
|
52
|
+
*/
|
|
53
|
+
const movedByKeyboard = useRef(false);
|
|
33
54
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
34
55
|
const [status, setStatus] = useState("idle");
|
|
35
56
|
const [shortcutHint, setShortcutHint] = useState("");
|
|
@@ -135,9 +156,10 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
|
|
|
135
156
|
}, [isOpen]);
|
|
136
157
|
useEffect(() => {
|
|
137
158
|
const trimmed = query.trim();
|
|
138
|
-
if (trimmed
|
|
159
|
+
if (trimmed.length < minQueryLength) {
|
|
139
160
|
setHits([]);
|
|
140
161
|
setActiveIndex(0);
|
|
162
|
+
setVisibleCount(pageSize);
|
|
141
163
|
return;
|
|
142
164
|
}
|
|
143
165
|
let isCancelled = false;
|
|
@@ -145,8 +167,9 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
|
|
|
145
167
|
ensureIndex().then((index) => {
|
|
146
168
|
if (isCancelled) return;
|
|
147
169
|
setStatus("ready");
|
|
148
|
-
setHits(index.search(trimmed).
|
|
170
|
+
setHits(index.search(trimmed).map(toSearchHit).filter(isSearchHit));
|
|
149
171
|
setActiveIndex(0);
|
|
172
|
+
setVisibleCount(pageSize);
|
|
150
173
|
}, () => {
|
|
151
174
|
if (!isCancelled) setStatus("error");
|
|
152
175
|
});
|
|
@@ -158,11 +181,55 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
|
|
|
158
181
|
}, [
|
|
159
182
|
query,
|
|
160
183
|
ensureIndex,
|
|
161
|
-
|
|
162
|
-
debounceMs
|
|
184
|
+
pageSize,
|
|
185
|
+
debounceMs,
|
|
186
|
+
minQueryLength
|
|
163
187
|
]);
|
|
188
|
+
/**
|
|
189
|
+
* Reveal another page when the reader nears the end of the list.
|
|
190
|
+
*
|
|
191
|
+
* A scroll handler rather than an `IntersectionObserver` on a sentinel: the
|
|
192
|
+
* scrollport is one element this component already holds a ref to, the test
|
|
193
|
+
* is one subtraction, and an observer would cost bytes on the largest client
|
|
194
|
+
* entry this package ships for no behaviour the reader can tell apart.
|
|
195
|
+
*
|
|
196
|
+
* `passive`, because this never calls `preventDefault` and a non-passive
|
|
197
|
+
* scroll listener blocks the compositor on every wheel event.
|
|
198
|
+
*
|
|
199
|
+
* No `isOpen` dependency, though the list does not exist while the dialog is
|
|
200
|
+
* closed: the effect returns early on a null ref, and `hits.length` going
|
|
201
|
+
* from 0 to N re-runs it — which happens after the list has mounted, because
|
|
202
|
+
* closing resets the query. The listener attaches exactly when there is
|
|
203
|
+
* something to scroll.
|
|
204
|
+
*/
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
const list = listRef.current;
|
|
207
|
+
if (list === null || visibleCount >= hits.length) return;
|
|
208
|
+
const onScroll = () => {
|
|
209
|
+
if (list.scrollHeight - list.scrollTop - list.clientHeight < list.clientHeight) setVisibleCount((count) => Math.min(count + pageSize, hits.length));
|
|
210
|
+
};
|
|
211
|
+
list.addEventListener("scroll", onScroll, { passive: true });
|
|
212
|
+
return () => list.removeEventListener("scroll", onScroll);
|
|
213
|
+
}, [
|
|
214
|
+
visibleCount,
|
|
215
|
+
hits.length,
|
|
216
|
+
pageSize
|
|
217
|
+
]);
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (activeIndex >= visibleCount) setVisibleCount(Math.min(activeIndex + 1, hits.length));
|
|
220
|
+
}, [
|
|
221
|
+
activeIndex,
|
|
222
|
+
visibleCount,
|
|
223
|
+
hits.length
|
|
224
|
+
]);
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
const list = listRef.current;
|
|
227
|
+
if (list !== null && list.scrollTop !== 0) list.scrollTop = 0;
|
|
228
|
+
}, [hits]);
|
|
164
229
|
useEffect(() => {
|
|
165
230
|
if (hits.length === 0) return;
|
|
231
|
+
if (!movedByKeyboard.current) return;
|
|
232
|
+
movedByKeyboard.current = false;
|
|
166
233
|
(listRef.current?.querySelector(`#${CSS.escape(`${baseId}-option-${activeIndex}`)}`))?.scrollIntoView({ block: "nearest" });
|
|
167
234
|
}, [
|
|
168
235
|
activeIndex,
|
|
@@ -179,6 +246,7 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
|
|
|
179
246
|
if (hits.length === 0) return;
|
|
180
247
|
event.preventDefault();
|
|
181
248
|
const delta = event.key === "ArrowDown" ? 1 : -1;
|
|
249
|
+
movedByKeyboard.current = true;
|
|
182
250
|
setActiveIndex((index) => (index + delta + hits.length) % hits.length);
|
|
183
251
|
return;
|
|
184
252
|
}
|
|
@@ -250,11 +318,16 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
|
|
|
250
318
|
className: "wave-docs-search-results",
|
|
251
319
|
role: "listbox",
|
|
252
320
|
"aria-label": dialogLabel,
|
|
253
|
-
children: hits.map((hit, index) => /* @__PURE__ */ jsx(SearchResultOption, {
|
|
321
|
+
children: hits.slice(0, visibleCount).map((hit, index) => /* @__PURE__ */ jsx(SearchResultOption, {
|
|
254
322
|
hit,
|
|
255
323
|
id: optionId(index),
|
|
256
324
|
isActive: index === activeIndex,
|
|
257
|
-
|
|
325
|
+
setSize: hits.length,
|
|
326
|
+
posInSet: index + 1,
|
|
327
|
+
onActivate: () => {
|
|
328
|
+
movedByKeyboard.current = false;
|
|
329
|
+
setActiveIndex(index);
|
|
330
|
+
},
|
|
258
331
|
onSelect: selectHit,
|
|
259
332
|
...Link === void 0 ? {} : { Link }
|
|
260
333
|
}, hit.id))
|
|
@@ -262,40 +335,36 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
|
|
|
262
335
|
/* @__PURE__ */ jsx(SearchStatus, {
|
|
263
336
|
status,
|
|
264
337
|
query: query.trim(),
|
|
265
|
-
hitCount: hits.length
|
|
338
|
+
hitCount: hits.length,
|
|
339
|
+
minQueryLength
|
|
266
340
|
})
|
|
267
341
|
]
|
|
268
342
|
})
|
|
269
343
|
}), document.body) : null] });
|
|
270
344
|
}
|
|
271
345
|
/** One result row: a real link, so middle-click and "open in new tab" work. */
|
|
272
|
-
function SearchResultOption({ hit, id, isActive, onActivate, onSelect, Link }) {
|
|
346
|
+
function SearchResultOption({ hit, id, isActive, setSize, posInSet, onActivate, onSelect, Link }) {
|
|
273
347
|
function handleClick(event) {
|
|
274
348
|
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
275
349
|
event.preventDefault();
|
|
276
350
|
onSelect(hit);
|
|
277
351
|
}
|
|
278
|
-
const trail = toBreadcrumbs(hit);
|
|
279
352
|
const body = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
280
353
|
className: "wave-docs-search-result-heading",
|
|
281
354
|
children: hit.heading
|
|
282
|
-
}),
|
|
283
|
-
className: "wave-docs-search-result-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
children: [index === 0 ? null : /* @__PURE__ */ jsx("span", {
|
|
287
|
-
className: "wave-docs-search-result-crumb-separator",
|
|
288
|
-
"aria-hidden": "true",
|
|
289
|
-
children: "›"
|
|
290
|
-
}), crumb.text]
|
|
291
|
-
}, crumb.key))
|
|
355
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
356
|
+
className: "wave-docs-search-result-location",
|
|
357
|
+
"aria-hidden": "true",
|
|
358
|
+
children: toDisplayPath(hit.href)
|
|
292
359
|
})] });
|
|
293
360
|
return /* @__PURE__ */ jsx("div", {
|
|
294
361
|
id,
|
|
295
362
|
className: isActive ? "wave-docs-search-result wave-docs-search-result-active" : "wave-docs-search-result",
|
|
296
363
|
role: "option",
|
|
297
364
|
"aria-selected": isActive,
|
|
298
|
-
"aria-
|
|
365
|
+
"aria-setsize": setSize,
|
|
366
|
+
"aria-posinset": posInSet,
|
|
367
|
+
"aria-label": spokenName(hit),
|
|
299
368
|
tabIndex: -1,
|
|
300
369
|
onPointerMove: onActivate,
|
|
301
370
|
children: Link === void 0 ? /* @__PURE__ */ jsx("a", {
|
|
@@ -314,7 +383,7 @@ function SearchResultOption({ hit, id, isActive, onActivate, onSelect, Link }) {
|
|
|
314
383
|
});
|
|
315
384
|
}
|
|
316
385
|
/** Loading, failure and empty states, plus a live region for hit counts. */
|
|
317
|
-
function SearchStatus({ status, query, hitCount }) {
|
|
386
|
+
function SearchStatus({ status, query, hitCount, minQueryLength }) {
|
|
318
387
|
let message = null;
|
|
319
388
|
let modifier = "";
|
|
320
389
|
if (status === "error") {
|
|
@@ -323,6 +392,9 @@ function SearchStatus({ status, query, hitCount }) {
|
|
|
323
392
|
} else if (query === "") {
|
|
324
393
|
message = "Start typing to search the documentation.";
|
|
325
394
|
modifier = " wave-docs-search-status-hint";
|
|
395
|
+
} else if (query.length < minQueryLength) {
|
|
396
|
+
message = `Keep typing — ${minQueryLength} characters or more.`;
|
|
397
|
+
modifier = " wave-docs-search-status-hint";
|
|
326
398
|
} else if (status !== "ready") {
|
|
327
399
|
message = "Loading the search index…";
|
|
328
400
|
modifier = " wave-docs-search-status-loading";
|
|
@@ -380,24 +452,47 @@ function isSearchHit(hit) {
|
|
|
380
452
|
return hit !== void 0;
|
|
381
453
|
}
|
|
382
454
|
/**
|
|
383
|
-
*
|
|
384
|
-
* the page title so the index does not carry it twice.
|
|
455
|
+
* The route, without its anchor, for display only.
|
|
385
456
|
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
457
|
+
* ⚠️ THE ANCHOR IS NOISE HERE, AND ALMOST ALWAYS A REPEAT. A section's anchor
|
|
458
|
+
* is slugged from its heading, so `/docs/styling#layout-tokens` under a row
|
|
459
|
+
* whose first line already reads "Layout tokens" spends its width restating
|
|
460
|
+
* it — and on a real site it is the part that pushes the line past the
|
|
461
|
+
* ellipsis.
|
|
462
|
+
*
|
|
463
|
+
* What the line is for is "which page does this land on", and the path answers
|
|
464
|
+
* that on its own. Two rows from the same page showing the same path is not
|
|
465
|
+
* ambiguity: they *are* the same page, and their headings above say which part.
|
|
466
|
+
*
|
|
467
|
+
* Display only. `hit.href` keeps the anchor, so the link still deep-links to
|
|
468
|
+
* the section — that is the whole point of section-scoped records.
|
|
390
469
|
*/
|
|
391
|
-
function
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
470
|
+
function toDisplayPath(href) {
|
|
471
|
+
const hash = href.indexOf("#");
|
|
472
|
+
return hash === -1 ? href : href.slice(0, hash);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* What a result is called when it is read aloud.
|
|
476
|
+
*
|
|
477
|
+
* Words, not the route the row displays. `/docs/styling#layout-tokens` is
|
|
478
|
+
* punctuation to a screen reader — spelled out slash by slash — so the visible
|
|
479
|
+
* line and the announced name deliberately carry the same fact in two forms:
|
|
480
|
+
* the route for a sighted reader scanning for where a hit lands, and
|
|
481
|
+
* "Layout tokens, Styling" for a listener.
|
|
482
|
+
*
|
|
483
|
+
* `ancestors` deliberately excludes the page title, so the page comes first
|
|
484
|
+
* here and the enclosing headings follow, outermost first.
|
|
485
|
+
*
|
|
486
|
+
* A page's own record carries `heading === title` and no ancestors, so its name
|
|
487
|
+
* is the heading alone — "Styling, Styling" is not a path, it is a stutter.
|
|
488
|
+
*/
|
|
489
|
+
function spokenName(hit) {
|
|
490
|
+
if (hit.ancestors.length === 0 && hit.heading === hit.title) return hit.heading;
|
|
491
|
+
return [
|
|
492
|
+
hit.heading,
|
|
493
|
+
hit.title,
|
|
494
|
+
...hit.ancestors
|
|
495
|
+
].join(", ");
|
|
401
496
|
}
|
|
402
497
|
function trapFocus(root, event) {
|
|
403
498
|
if (root === null) return;
|
package/dist/styles.css
CHANGED
|
@@ -1844,6 +1844,19 @@
|
|
|
1844
1844
|
z-index: 50;
|
|
1845
1845
|
display: flex;
|
|
1846
1846
|
justify-content: center;
|
|
1847
|
+
/*
|
|
1848
|
+
* ⚠️ `flex-start`, OR THE DIALOG IS ALWAYS 32rem TALL. A flex container
|
|
1849
|
+
* defaults to `align-items: stretch`, so the dialog stretched to the full
|
|
1850
|
+
* viewport height and `max-height` then capped it — at a constant. Measured
|
|
1851
|
+
* before this line: 514px with no query, 514px with eight results, 514px
|
|
1852
|
+
* with none, of which 392px was an empty results area. The reader typed
|
|
1853
|
+
* into a box floating at the top of a large blank rectangle.
|
|
1854
|
+
*
|
|
1855
|
+
* Aligned to the start, the dialog is as tall as its content and the
|
|
1856
|
+
* `max-height` below goes back to being what it says it is — a ceiling
|
|
1857
|
+
* where the list starts scrolling, not a height.
|
|
1858
|
+
*/
|
|
1859
|
+
align-items: flex-start;
|
|
1847
1860
|
/* Not centred: a dialog that grows downward from a fixed top does not
|
|
1848
1861
|
* shift under the reader as results stream in. */
|
|
1849
1862
|
/* `dvh`, not `vh`: on a phone `vh` is the viewport with the URL bar
|
|
@@ -1930,12 +1943,29 @@
|
|
|
1930
1943
|
border-color: var(--wave-docs-border-strong);
|
|
1931
1944
|
}
|
|
1932
1945
|
|
|
1946
|
+
/*
|
|
1947
|
+
* `1 1 auto` and not `0 1 auto`: once the dialog hits its `max-height` this
|
|
1948
|
+
* is the part that must take the remaining space and scroll, rather than the
|
|
1949
|
+
* list overflowing a box sized to the input. With the dialog content-sized
|
|
1950
|
+
* there is no free space to grow into, so it only bites at the ceiling —
|
|
1951
|
+
* which is exactly when it should.
|
|
1952
|
+
*/
|
|
1933
1953
|
.wave-docs-search-results {
|
|
1934
1954
|
flex: 1 1 auto;
|
|
1935
1955
|
overflow-y: auto;
|
|
1936
1956
|
padding: 0.375rem;
|
|
1937
1957
|
}
|
|
1938
1958
|
|
|
1959
|
+
/*
|
|
1960
|
+
* An empty results container has no padding to contribute. Without this it
|
|
1961
|
+
* adds 12px of nothing under the input on every keystroke that matches
|
|
1962
|
+
* nothing — small, and precisely the sort of gap that reads as a broken
|
|
1963
|
+
* layout rather than as an empty state.
|
|
1964
|
+
*/
|
|
1965
|
+
.wave-docs-search-results:empty {
|
|
1966
|
+
display: none;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1939
1969
|
.wave-docs-search-result {
|
|
1940
1970
|
border-radius: var(--wave-docs-radius-sm);
|
|
1941
1971
|
}
|
|
@@ -1969,20 +1999,39 @@
|
|
|
1969
1999
|
line-height: 1.4;
|
|
1970
2000
|
}
|
|
1971
2001
|
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
*
|
|
1980
|
-
|
|
2002
|
+
/*
|
|
2003
|
+
* Where the hit lands, as a route.
|
|
2004
|
+
*
|
|
2005
|
+
* ⚠️ RENAMED FROM `…-breadcrumb` / `…-crumb`, WHICH IS BREAKING AND
|
|
2006
|
+
* DELIBERATE. This used to be a trail of human names — "Installation ›
|
|
2007
|
+
* Requirements" — and a page's own record, whose heading IS its page title,
|
|
2008
|
+
* got no line at all rather than repeat itself. That left a list where some
|
|
2009
|
+
* rows had two lines and some had one.
|
|
2010
|
+
*
|
|
2011
|
+
* The first repair gave those rows the route and kept the trail on the
|
|
2012
|
+
* others, which was worse: one slot carrying two different kinds of thing,
|
|
2013
|
+
* so "Styling" under one row was a page and "/docs/styling" under the next
|
|
2014
|
+
* was an address. Now every row shows the route, always, and the class is
|
|
2015
|
+
* named for what it holds — a breadcrumb it is not.
|
|
2016
|
+
*
|
|
2017
|
+
* The words live in the option's `aria-label`, because a route read aloud is
|
|
2018
|
+
* punctuation.
|
|
2019
|
+
*/
|
|
2020
|
+
.wave-docs-search-result-location {
|
|
2021
|
+
display: block;
|
|
2022
|
+
/* One line, ellipsised. A real site's routes reach
|
|
2023
|
+
* `/docs/api/reference/authentication#rotating-keys`, and a wrapped route
|
|
2024
|
+
* makes the row two different heights depending on its depth — the raggedness
|
|
2025
|
+
* this whole change exists to remove. */
|
|
2026
|
+
overflow: hidden;
|
|
1981
2027
|
white-space: nowrap;
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
2028
|
+
text-overflow: ellipsis;
|
|
2029
|
+
color: var(--wave-docs-fg-subtle);
|
|
2030
|
+
font-family: var(--wave-docs-font-mono);
|
|
2031
|
+
/* Smaller than the trail was: a monospace face reads larger at the same
|
|
2032
|
+
* size, and this line is a reference rather than something to read. */
|
|
2033
|
+
font-size: 0.6875rem;
|
|
2034
|
+
line-height: 1.5;
|
|
1986
2035
|
}
|
|
1987
2036
|
|
|
1988
2037
|
.wave-docs-search-status {
|
package/package.json
CHANGED