From 0644b571572a17b1bd5bfb1418b87c4648e76c64 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Sun, 19 Jan 2025 00:04:07 +0530 Subject: [PATCH 01/21] Add css variables for dimensions, and use svh units --- static/style.css | 48 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/static/style.css b/static/style.css index 2ab8b836e..02d7001e2 100644 --- a/static/style.css +++ b/static/style.css @@ -1,4 +1,18 @@ @layer typedoc { + :root { + --dim-toolbar-contents-height: 2.5rem; + --dim-toolbar-border-bottom-width: 1px; + --dim-header-height: calc( + var(--dim-toolbar-border-bottom-width) + + var(--dim-toolbar-contents-height) + ); + + /* 0rem For mobile; unit is required for calculation in `calc` */ + --dim-container-main-margin-y: 0rem; + + --dim-footer-height: 3.5rem; + } + :root { /* Light */ --light-color-background: #f2f4f8; @@ -421,16 +435,19 @@ border-top: 1px solid var(--color-accent); padding-top: 1rem; padding-bottom: 1rem; - max-height: 3.5rem; + max-height: var(--dim-footer-height); } footer > p { margin: 0 1em; } .container-main { - margin: 0 auto; + margin: var(--dim-container-main-margin-y) auto; /* toolbar, footer, margin */ - min-height: calc(100vh - 41px - 56px - 4rem); + min-height: calc( + 100svh - var(--dim-header-height) - var(--dim-footer-height) - 2 * + var(--dim-container-main-margin-y) + ); } @keyframes fade-in { @@ -1257,7 +1274,8 @@ width: 100%; color: var(--color-text); background: var(--color-background-secondary); - border-bottom: 1px var(--color-accent) solid; + border-bottom: var(--dim-toolbar-border-bottom-width) + var(--color-accent) solid; transition: transform 0.3s ease-in-out; } .tsd-page-toolbar a { @@ -1273,7 +1291,7 @@ .tsd-page-toolbar .tsd-toolbar-contents { display: flex; justify-content: space-between; - height: 2.5rem; + height: var(--dim-toolbar-contents-height); margin: 0 auto; } .tsd-page-toolbar .table-cell { @@ -1550,7 +1568,7 @@ display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); grid-template-areas: "sidebar content"; - margin: 2rem auto; + --dim-container-main-margin-y: 2rem; } .col-sidebar { @@ -1563,10 +1581,15 @@ } @media (min-width: 770px) and (max-width: 1399px) { .col-sidebar { - max-height: calc(100vh - 2rem - 42px); + max-height: calc( + 100vh - var(--dim-header-height) - var(--dim-footer-height) - 2 * + var(--dim-container-main-margin-y) + ); overflow: auto; position: sticky; - top: 42px; + top: calc( + var(--dim-header-height) + var(--dim-container-main-margin-y) + ); padding-top: 1rem; } .site-menu { @@ -1602,10 +1625,15 @@ .page-menu, .site-menu { - max-height: calc(100vh - 2rem - 42px); + max-height: calc( + 100vh - var(--dim-header-height) - var(--dim-footer-height) - 2 * + var(--dim-container-main-margin-y) + ); overflow: auto; position: sticky; - top: 42px; + top: calc( + var(--dim-header-height) + var(--dim-container-main-margin-y) + ); } } } From 32f46d9409c5e263fb2f16cb94f679803c234255 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Sun, 19 Jan 2025 00:24:47 +0530 Subject: [PATCH 02/21] Fix excess/absent margins and padding. - Add top margin to `.tsd-breadcrumb` in mobile screen - Remove top margin from `.tsd-navigation.settings` - Remove top padding from `.col-sidebar` on single sidebar - Remove margin for `.site-menu` on larger screens --- static/style.css | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/static/style.css b/static/style.css index 02d7001e2..dc2c6a400 100644 --- a/static/style.css +++ b/static/style.css @@ -646,6 +646,7 @@ .tsd-breadcrumb { margin: 0; + margin-top: 1rem; padding: 0; color: var(--color-text-aside); } @@ -893,7 +894,8 @@ } .tsd-navigation.settings { - margin: 1rem 0; + margin: 0; + margin-bottom: 1rem; } .tsd-navigation > a, .tsd-navigation .tsd-accordion-summary { @@ -1571,6 +1573,10 @@ --dim-container-main-margin-y: 2rem; } + .tsd-breadcrumb { + margin-top: 0; + } + .col-sidebar { grid-area: sidebar; } @@ -1590,7 +1596,6 @@ top: calc( var(--dim-header-height) + var(--dim-container-main-margin-y) ); - padding-top: 1rem; } .site-menu { margin-top: 1rem; @@ -1620,7 +1625,7 @@ } .site-menu { - margin-top: 1rem; + margin-top: 0rem; } .page-menu, From aaff25e51cb53eee25e567e21a0ea1432a80d776 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Sun, 19 Jan 2025 12:51:24 +0530 Subject: [PATCH 03/21] Fix tab order of toolbar and remove unused selectors. **Breaks search** - Move selectors (`#tsd-toolbar-links`, tag selectors) to a relevant place. - Remove classes (`.table-cell`, `.no-caption`, `.tsd-toolbar-icon`), and merge their styles to other common classes. --- .../themes/default/partials/toolbar.tsx | 53 +++---- static/style.css | 142 +++++++----------- 2 files changed, 75 insertions(+), 120 deletions(-) diff --git a/src/lib/output/themes/default/partials/toolbar.tsx b/src/lib/output/themes/default/partials/toolbar.tsx index cd18fe65b..98afa5911 100644 --- a/src/lib/output/themes/default/partials/toolbar.tsx +++ b/src/lib/output/themes/default/partials/toolbar.tsx @@ -7,42 +7,29 @@ import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext.js" export const toolbar = (context: DefaultThemeRenderContext, props: PageEvent) => (
-
); diff --git a/static/style.css b/static/style.css index dc2c6a400..3d503f2e4 100644 --- a/static/style.css +++ b/static/style.css @@ -572,6 +572,35 @@ border-left: 4px solid gray; } + button { + border: none; + appearance: none; + background-color: transparent; + } + + img { + max-width: 100%; + } + + * { + scrollbar-width: thin; + scrollbar-color: var(--color-accent) var(--color-icon-background); + } + + *::-webkit-scrollbar { + width: 0.75rem; + } + + *::-webkit-scrollbar-track { + background: var(--color-icon-background); + } + + *::-webkit-scrollbar-thumb { + background-color: var(--color-accent); + border-radius: 999rem; + border: 0.25rem solid var(--color-icon-background); + } + .tsd-typography { line-height: 1.333em; } @@ -1178,22 +1207,6 @@ display: block; } - #tsd-toolbar-links { - position: absolute; - top: 0; - right: 2rem; - height: 100%; - display: flex; - align-items: center; - justify-content: flex-end; - } - #tsd-toolbar-links a { - margin-left: 1.5rem; - } - #tsd-toolbar-links a:hover { - text-decoration: underline; - } - .tsd-signature { margin: 0 0 1rem 0; padding: 1rem 0.5rem; @@ -1282,73 +1295,46 @@ } .tsd-page-toolbar a { color: var(--color-text); - text-decoration: none; } - .tsd-page-toolbar a.title { - font-weight: bold; - } - .tsd-page-toolbar a.title:hover { - text-decoration: underline; - } - .tsd-page-toolbar .tsd-toolbar-contents { + .tsd-toolbar-contents { display: flex; - justify-content: space-between; + align-items: center; height: var(--dim-toolbar-contents-height); margin: 0 auto; } - .tsd-page-toolbar .table-cell { - position: relative; - white-space: nowrap; - line-height: 40px; - } - .tsd-page-toolbar .table-cell:first-child { - width: 100%; + .tsd-toolbar-contents > .title { + font-weight: bold; + margin-right: auto; } - .tsd-page-toolbar .tsd-toolbar-icon { - box-sizing: border-box; - line-height: 0; - padding: 12px 0; + #tsd-toolbar-links { + display: flex; + align-items: center; + gap: 1.5rem; + margin-right: 1rem; } .tsd-widget { + box-sizing: border-box; display: inline-block; - overflow: hidden; opacity: 0.8; - height: 40px; + height: 2.5rem; + width: 2.5rem; transition: opacity 0.1s, background-color 0.2s; - vertical-align: bottom; + text-align: center; cursor: pointer; } .tsd-widget:hover { opacity: 0.9; } - .tsd-widget.active { + .tsd-widget:active { opacity: 1; background-color: var(--color-accent); } - .tsd-widget.no-caption { - width: 40px; - } - .tsd-widget.no-caption:before { - margin: 0; - } - - .tsd-widget.options, - .tsd-widget.menu { + #tsd-toolbar-menu-trigger { display: none; } - input[type="checkbox"] + .tsd-widget:before { - background-position: -120px 0; - } - input[type="checkbox"]:checked + .tsd-widget:before { - background-position: -160px 0; - } - - img { - max-width: 100%; - } .tsd-member-summary-name { display: inline-flex; @@ -1457,41 +1443,26 @@ color: var(--color-text); } - * { - scrollbar-width: thin; - scrollbar-color: var(--color-accent) var(--color-icon-background); - } - - *::-webkit-scrollbar { - width: 0.75rem; - } - - *::-webkit-scrollbar-track { - background: var(--color-icon-background); - } - - *::-webkit-scrollbar-thumb { - background-color: var(--color-accent); - border-radius: 999rem; - border: 0.25rem solid var(--color-icon-background); - } - /* mobile */ @media (max-width: 769px) { - .tsd-widget.options, - .tsd-widget.menu { + #tsd-toolbar-menu-trigger { display: inline-block; + /* temporary fix to vertically align, for compatibilty */ + line-height: 2.5; + } + #tsd-toolbar-links { + display: none; } .container-main { display: flex; } - html .col-content { + .col-content { float: none; max-width: 100%; width: 100%; } - html .col-sidebar { + .col-sidebar { position: fixed !important; overflow-y: auto; -webkit-overflow-scrolling: touch; @@ -1506,10 +1477,10 @@ background-color: var(--color-background); transform: translate(100%, 0); } - html .col-sidebar > *:last-child { + .col-sidebar > *:last-child { padding-bottom: 20px; } - html .overlay { + .overlay { content: ""; display: block; position: fixed; @@ -1556,9 +1527,6 @@ .has-menu .tsd-navigation { max-height: 100%; } - #tsd-toolbar-links { - display: none; - } .tsd-navigation .tsd-nav-link { display: flex; } From 99ae7be75151942d7830f620d01442f57d6c2122 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Sun, 19 Jan 2025 13:09:52 +0530 Subject: [PATCH 04/21] Add more search related 118n strings, and inject more strings in js script - **theme_search_index_not_available** - **theme_search_no_results** - theme_search_placeholder - **theme_search_no_recent_searches** --- src/lib/internationalization/locales/en.cts | 5 ++++- src/lib/output/plugins/AssetsPlugin.ts | 4 ++++ src/lib/output/themes/default/assets/typedoc/Application.ts | 4 ++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/lib/internationalization/locales/en.cts b/src/lib/internationalization/locales/en.cts index 7e0cef9fd..328e4ef29 100644 --- a/src/lib/internationalization/locales/en.cts +++ b/src/lib/internationalization/locales/en.cts @@ -509,7 +509,6 @@ export = { theme_generated_using_typedoc: "Generated using TypeDoc", // If this includes "TypeDoc", theme will insert a link at that location. // Search theme_preparing_search_index: "Preparing search index...", - theme_search_index_not_available: "The search index is not available", // Left nav bar theme_loading: "Loading...", // Right nav bar @@ -537,4 +536,8 @@ export = { "This member is normally hidden due to your filter settings.", theme_hierarchy_expand: "Expand", theme_hierarchy_collapse: "Collapse", + theme_search_index_not_available: "The search index is not available", + theme_search_no_results: "No results found", + theme_search_placeholder: "Search the docs", + theme_search_no_recent_searches: "No recent searches", } as const; diff --git a/src/lib/output/plugins/AssetsPlugin.ts b/src/lib/output/plugins/AssetsPlugin.ts index d656853d5..ce3c881b3 100644 --- a/src/lib/output/plugins/AssetsPlugin.ts +++ b/src/lib/output/plugins/AssetsPlugin.ts @@ -41,6 +41,10 @@ export class AssetsPlugin extends RendererComponent { hierarchy_expand: i18n.theme_hierarchy_expand(), hierarchy_collapse: i18n.theme_hierarchy_collapse(), folder: i18n.theme_folder(), + theme_search_index_not_available: + this.application.i18n.theme_search_index_not_available(), + theme_search_no_results: + this.application.i18n.theme_search_no_results(), }; for (const key of getEnumKeys(ReflectionKind)) { diff --git a/src/lib/output/themes/default/assets/typedoc/Application.ts b/src/lib/output/themes/default/assets/typedoc/Application.ts index fa26210d4..3434aa0ac 100644 --- a/src/lib/output/themes/default/assets/typedoc/Application.ts +++ b/src/lib/output/themes/default/assets/typedoc/Application.ts @@ -12,6 +12,8 @@ declare global { // Kind strings for icons folder: string; [k: `kind_${number}`]: string; + theme_search_index_not_available: string; + theme_search_no_results: string; }; } } @@ -50,6 +52,8 @@ window.translations ||= { kind_2097152: "Type Alias", kind_4194304: "Reference", kind_8388608: "Document", + theme_search_index_not_available: "The search index is not available", + theme_search_no_results: "No results found", }; /** From 876b81e80e44ce89825475317dbe0ce3adf3ed6c Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Sun, 19 Jan 2025 23:20:04 +0530 Subject: [PATCH 05/21] Implement search component as combobox, with minimal functionality - Use HTML dialog element as popup - Use ARIA-spec compliant "combobox with list autocomplete" pattern for search - Add entry animations - Comment out affected dead code for keyboard event listeners --- .../assets/typedoc/components/Search.ts | 262 ++++++++++-------- .../default/assets/typedoc/utils/modal.ts | 29 ++ .../themes/default/partials/toolbar.tsx | 17 ++ static/style.css | 139 +++++----- 4 files changed, 263 insertions(+), 184 deletions(-) create mode 100644 src/lib/output/themes/default/assets/typedoc/utils/modal.ts diff --git a/src/lib/output/themes/default/assets/typedoc/components/Search.ts b/src/lib/output/themes/default/assets/typedoc/components/Search.ts index b5bd756db..011b58d26 100644 --- a/src/lib/output/themes/default/assets/typedoc/components/Search.ts +++ b/src/lib/output/themes/default/assets/typedoc/components/Search.ts @@ -1,6 +1,7 @@ import { debounce } from "../utils/debounce.js"; import { Index } from "lunr"; import { decompressJson } from "../utils/decompress.js"; +import { hideScrollbar, resetScrollbar } from "../utils/modal.js"; /** * Keep this in sync with the interface in src/lib/output/plugins/JavascriptIndexPlugin.ts @@ -33,90 +34,135 @@ interface SearchState { index?: Index; } -async function updateIndex(state: SearchState, searchEl: HTMLElement) { +/** Counter to get unique IDs for options */ +let optionsIdCounter = 0; + +let resultCount = 0; + +/** + * Populates search data into `state`, if available. + * Removes deault loading message + */ +async function updateIndex(state: SearchState, results: HTMLElement) { if (!window.searchData) return; - const data: IData = await decompressJson(window.searchData); + try { + const data: IData = await decompressJson(window.searchData); - state.data = data; - state.index = Index.load(data.index); + state.data = data; + state.index = Index.load(data.index); - searchEl.classList.remove("loading"); - searchEl.classList.add("ready"); + results.querySelector("li.state")?.remove(); + } catch (e) { + console.error(e); + const message = window.translations.theme_search_index_not_available; + const stateEl = createStateEl(message); + results.replaceChildren(stateEl); + } } export function initSearch() { - const searchEl = document.getElementById("tsd-search"); - if (!searchEl) return; + const searchTrigger = document.getElementById( + "tsd-search-trigger", + ) as HTMLButtonElement | null; - const state: SearchState = { - base: document.documentElement.dataset.base! + "/", - }; + const searchEl = document.getElementById( + "tsd-search", + ) as HTMLDialogElement | null; + + const field = document.getElementById( + "tsd-search-input", + ) as HTMLInputElement | null; + + const results = document.getElementById("tsd-search-results"); const searchScript = document.getElementById( "tsd-search-script", ) as HTMLScriptElement | null; - searchEl.classList.add("loading"); - if (searchScript) { - searchScript.addEventListener("error", () => { - searchEl.classList.remove("loading"); - searchEl.classList.add("failure"); - }); - searchScript.addEventListener("load", () => { - updateIndex(state, searchEl); - }); - updateIndex(state, searchEl); - } - - const field = document.querySelector("#tsd-search input"); - const results = document.querySelector("#tsd-search .results"); - if (!field || !results) { - throw new Error( - "The input field or the result list wrapper was not found", - ); + if (!(searchTrigger && searchEl && field && results && searchScript)) { + throw new Error("Search controls missing"); } - results.addEventListener("mouseup", () => { - hideSearch(searchEl); - }); + const state: SearchState = { + base: document.documentElement.dataset.base! + "/", + }; - field.addEventListener("focus", () => searchEl.classList.add("has-focus")); + searchScript.addEventListener("error", () => { + const message = window.translations.theme_search_index_not_available; + const stateEl = createStateEl(message); + results.replaceChildren(stateEl); + }); + searchScript.addEventListener("load", () => { + updateIndex(state, results); + }); + updateIndex(state, results); - bindEvents(searchEl, results, field, state); + bindEvents(searchTrigger, searchEl, results, field, state); } function bindEvents( - searchEl: HTMLElement, + trigger: HTMLButtonElement, + searchEl: HTMLDialogElement, results: HTMLElement, field: HTMLInputElement, state: SearchState, ) { + trigger.addEventListener("click", () => openModal(searchEl)); + + searchEl.addEventListener("close", resetScrollbar); + searchEl.addEventListener("cancel", resetScrollbar); + field.addEventListener( "input", debounce(() => { - updateResults(searchEl, results, field, state); + updateResults(results, field, state); }, 200), ); - // Narrator is a pain. It completely eats the up/down arrow key events, so we can't - // rely on detecting the input blurring to hide the focus. We have to instead check - // for a focus event on an item outside of the search field/results. field.addEventListener("keydown", (e) => { - if (e.key == "Enter") { - gotoCurrentResult(results, searchEl); - } else if (e.key == "ArrowUp") { - setCurrentResult(results, field, -1); - e.preventDefault(); - } else if (e.key === "ArrowDown") { - setCurrentResult(results, field, 1); - e.preventDefault(); + if (resultCount === 0 || e.ctrlKey || e.metaKey || e.altKey) { + return; + } + + // Get the visually focused element, if any + const currentId = field.getAttribute("aria-activedescendant"); + const current = document.getElementById(currentId || ""); + + // Remove visual focus on cursor position change + if (current) { + switch (e.key) { + case "Home": + case "End": + case "ArrowLeft": + case "ArrowRight": + removeVisualFocus(field); + } + } + + if (e.shiftKey) return; + + switch (e.key) { + case "Enter": + current?.querySelector("a")?.click(); + break; + case "ArrowUp": + setCurrentResult(results, field, current, -1); + break; + case "ArrowDown": + setCurrentResult(results, field, current, 1); + break; } }); + const _removeVisualFocus = () => removeVisualFocus(field); + field.addEventListener("change", _removeVisualFocus); + field.addEventListener("blur", _removeVisualFocus); + /** * Start searching by pressing slash. */ + /* document.body.addEventListener("keypress", (e) => { if (e.altKey || e.ctrlKey || e.metaKey) return; if (!field.matches(":focus") && e.key === "/") { @@ -135,14 +181,16 @@ function bindEvents( hideSearch(searchEl); } }); + */ } -function hideSearch(searchEl: HTMLElement) { - searchEl.classList.remove("has-focus"); +function openModal(searchEl: HTMLDialogElement) { + if (searchEl.open) return; + hideScrollbar(); + searchEl.showModal(); } function updateResults( - searchEl: HTMLElement, results: HTMLElement, query: HTMLInputElement, state: SearchState, @@ -151,7 +199,8 @@ function updateResults( // because loading or error message can be removed. if (!state.index || !state.data) return; - results.textContent = ""; + results.innerHTML = ""; + optionsIdCounter += 1; const searchText = query.value.trim(); @@ -173,6 +222,14 @@ function updateResults( res = []; } + resultCount = res.length; + + if (res.length === 0) { + const item = createStateEl(window.translations.theme_search_no_results); + results.appendChild(item); + return; + } + for (let i = 0; i < res.length; i++) { const item = res[i]; const row = state.data.rows[Number(item.ref)]; @@ -187,20 +244,10 @@ function updateResults( item.score *= boost; } - if (res.length === 0) { - let item = document.createElement("li"); - item.classList.add("no-results"); - - let anchor = document.createElement("span"); - anchor.textContent = "No results found"; - - item.appendChild(anchor); - results.appendChild(item); - } - res.sort((a, b) => b.score - a.score); - for (let i = 0, c = Math.min(10, res.length); i < c; i++) { + const c = Math.min(10, res.length); + for (let i = 0; i < c; i++) { const row = state.data.rows[Number(res[i].ref)]; const icon = ``; @@ -215,18 +262,18 @@ function updateResults( } const item = document.createElement("li"); + item.id = `tsd-search:${optionsIdCounter}-${i}`; + item.role = "option"; + item.ariaSelected = "false"; item.classList.value = row.classes ?? ""; const anchor = document.createElement("a"); + // Make links unfocusable inside option + anchor.tabIndex = -1; anchor.href = state.base + row.url; - anchor.innerHTML = icon + name; + anchor.innerHTML = icon + `${name}`; item.append(anchor); - anchor.addEventListener("focus", () => { - results.querySelector(".current")?.classList.remove("current"); - item.classList.add("current"); - }); - results.appendChild(item); } } @@ -237,57 +284,35 @@ function updateResults( function setCurrentResult( results: HTMLElement, field: HTMLInputElement, - dir: number, + current: Element | null, + dir: 1 | -1, ) { - let current = results.querySelector(".current"); - if (!current) { - current = results.querySelector( - dir == 1 ? "li:first-child" : "li:last-child", - ); - if (current) { - current.classList.add("current"); - } + let next: Element | null; + // If there's no active descendant, select the first or last + if (dir === 1) { + next = current?.nextElementSibling || results.firstElementChild; } else { - let rel: Element | undefined = current; - // Tricky: We have to check that rel has an offsetParent so that users can't mark a hidden result as - // current with the arrow keys. - if (dir === 1) { - do { - rel = rel.nextElementSibling ?? undefined; - } while (rel instanceof HTMLElement && rel.offsetParent == null); - } else { - do { - rel = rel.previousElementSibling ?? undefined; - } while (rel instanceof HTMLElement && rel.offsetParent == null); - } + next = current?.previousElementSibling || results.lastElementChild; + } - if (rel) { - current.classList.remove("current"); - rel.classList.add("current"); - } else if (dir === -1) { - current.classList.remove("current"); - field.focus(); - } + // bad markup + if (!next || next.role !== "option") { + console.error("Option missing"); + return; } + + next.ariaSelected = "true"; + next.scrollIntoView({ behavior: "smooth", block: "nearest" }); + field.setAttribute("aria-activedescendant", next.id); + current?.setAttribute("aria-selected", "false"); } -/** - * Navigate to the highlighted result. - */ -function gotoCurrentResult(results: HTMLElement, searchEl: HTMLElement) { - let current = results.querySelector(".current"); +function removeVisualFocus(field: HTMLInputElement) { + const currentId = field.getAttribute("aria-activedescendant"); + const current = document.getElementById(currentId || ""); - if (!current) { - current = results.querySelector("li:first-child"); - } - - if (current) { - const link = current.querySelector("a"); - if (link) { - window.location.href = link.href; - } - hideSearch(searchEl); - } + current?.setAttribute("aria-selected", "false"); + field.setAttribute("aria-activedescendant", ""); } function boldMatches(text: string, search: string) { @@ -332,3 +357,14 @@ function escapeHtml(text: string) { (match) => SPECIAL_HTML[match as keyof typeof SPECIAL_HTML], ); } + +/** + * Returns a `li` element, with `state` class, + * @param message Message to set as **innerHTML** + */ +function createStateEl(message: string) { + const stateEl = document.createElement("li"); + stateEl.className = "state"; + stateEl.innerHTML = message; + return stateEl; +} diff --git a/src/lib/output/themes/default/assets/typedoc/utils/modal.ts b/src/lib/output/themes/default/assets/typedoc/utils/modal.ts new file mode 100644 index 000000000..23977d4dd --- /dev/null +++ b/src/lib/output/themes/default/assets/typedoc/utils/modal.ts @@ -0,0 +1,29 @@ +/** + * @module + * + * Browsers allow scrolling of page with native dialog, which is a UX issue. + * + * @see + * - https://github.com/whatwg/html/issues/7732 + * - https://github.com/whatwg/html/issues/7732#issuecomment-2437820350 + */ + +/** Fills the gap that scrollbar occupies. Call when the modal is opened */ +export function hideScrollbar() { + // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes + // Should be computed *before* body overflow is set to hidden + const width = Math.abs( + window.innerWidth - document.documentElement.clientWidth, + ); + + document.body.style.overflow = "hidden"; + + // Give padding to element to balance the hidden scrollbar width + document.body.style.paddingRight = `${width}px`; +} + +/** Resets style changes made by {@link hideScrollbar} */ +export function resetScrollbar() { + document.body.style.removeProperty("overflow"); + document.body.style.removeProperty("padding-right"); +} diff --git a/src/lib/output/themes/default/partials/toolbar.tsx b/src/lib/output/themes/default/partials/toolbar.tsx index 98afa5911..7312890e9 100644 --- a/src/lib/output/themes/default/partials/toolbar.tsx +++ b/src/lib/output/themes/default/partials/toolbar.tsx @@ -20,6 +20,23 @@ export const toolbar = (context: DefaultThemeRenderContext, props: PageEvent {context.icons.search()} + + + +
    +
  • {context.i18n.theme_preparing_search_index()}
  • +
+
li[role="option"] { background-color: var(--color-background); - line-height: initial; - padding: 4px; + line-height: 1.5; + box-sizing: border-box; + border-radius: 4px; } - #tsd-search .results li:nth-child(even) { + #tsd-search-results > li[role="option"]:nth-child(even) { background-color: var(--color-background-secondary); } - #tsd-search .results li.state { - display: none; - } - #tsd-search .results li.current:not(.no-results), - #tsd-search .results li:hover:not(.no-results) { + #tsd-search-results > li[role="option"]:is(:hover, [aria-selected="true"]) { background-color: var(--color-accent); } - #tsd-search .results a { + /* It's important that this takes full size of parent `li`, to capture a click on `li` */ + #tsd-search-results > li[role="option"] > a { display: flex; align-items: center; - padding: 0.25rem; + padding: 0.5rem 0.25rem; box-sizing: border-box; + width: 100%; } - #tsd-search .results a:before { - top: 10px; + #tsd-search-results > li[role="option"] > a > .text { + flex: 1 1 auto; + min-width: 0; + overflow-wrap: anywhere; } - #tsd-search .results span.parent { + #tsd-search-results > li[role="option"] > a .parent { color: var(--color-text-aside); - font-weight: normal; - } - #tsd-search.has-focus { - background-color: var(--color-accent); - } - #tsd-search.has-focus .field input { - top: 0; - opacity: 1; } - #tsd-search.has-focus .title, - #tsd-search.has-focus #tsd-toolbar-links a { - z-index: 0; - opacity: 0; - } - #tsd-search.has-focus .results { - visibility: visible; - } - #tsd-search.loading .results li.state.loading { - display: block; - } - #tsd-search.failure .results li.state.failure { - display: block; + #tsd-search-results > li.state { + flex: 1; + display: grid; + place-content: center; } .tsd-signature { From ae8388b5af78994f01fc2032699a24a6f269ed72 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Sun, 19 Jan 2025 23:22:49 +0530 Subject: [PATCH 06/21] Add global keyboard listeners for opening modal --- .../assets/typedoc/components/Search.ts | 68 +++++++++++++------ 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/src/lib/output/themes/default/assets/typedoc/components/Search.ts b/src/lib/output/themes/default/assets/typedoc/components/Search.ts index 011b58d26..3847bea49 100644 --- a/src/lib/output/themes/default/assets/typedoc/components/Search.ts +++ b/src/lib/output/themes/default/assets/typedoc/components/Search.ts @@ -155,33 +155,23 @@ function bindEvents( } }); - const _removeVisualFocus = () => removeVisualFocus(field); - field.addEventListener("change", _removeVisualFocus); - field.addEventListener("blur", _removeVisualFocus); + field.addEventListener("change", () => removeVisualFocus(field)); + field.addEventListener("blur", () => removeVisualFocus(field)); /** - * Start searching by pressing slash. + * Start searching by pressing slash, or Ctrl+K */ - /* - document.body.addEventListener("keypress", (e) => { - if (e.altKey || e.ctrlKey || e.metaKey) return; - if (!field.matches(":focus") && e.key === "/") { - e.preventDefault(); - field.focus(); - } - }); + document.body.addEventListener("keydown", (e) => { + if (e.altKey || e.metaKey || e.shiftKey) return; - document.body.addEventListener("keyup", (e) => { - if ( - searchEl.classList.contains("has-focus") && - (e.key === "Escape" || - (!results.matches(":focus-within") && !field.matches(":focus"))) - ) { - field.blur(); - hideSearch(searchEl); + const ctrlK = e.ctrlKey && e.key === "k"; + const slash = !e.ctrlKey && !isKeyboardActive() && e.key === "/"; + + if (ctrlK || slash) { + e.preventDefault(); + openModal(searchEl); } }); - */ } function openModal(searchEl: HTMLDialogElement) { @@ -368,3 +358,39 @@ function createStateEl(message: string) { stateEl.innerHTML = message; return stateEl; } + +/** + * that don't take printable character input from keyboard, + * to avoid catching "/" when active. + * + * based on [MDN: input types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types) + */ +const inputWithoutKeyboard = [ + "button", + "checkbox", + "file", + "hidden", + "image", + "radio", + "range", + "reset", + "submit", +]; + +/** Checks whether keyboard is active, i.e. an input is focused */ +function isKeyboardActive() { + const activeElement = document.activeElement as HTMLElement | null; + if (!activeElement) return false; + + if ( + activeElement.isContentEditable || + activeElement.tagName === "TEXTAREA" || + activeElement.tagName === "SEARCH" + ) + return true; + + return ( + activeElement.tagName === "INPUT" && + !inputWithoutKeyboard.includes((activeElement as HTMLInputElement).type) + ); +} From 35f5be60089306296778838685faf5ef485db565 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Sun, 19 Jan 2025 23:23:42 +0530 Subject: [PATCH 07/21] rename `setCurrentResult` to `setNextResult` --- .../themes/default/assets/typedoc/components/Search.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/output/themes/default/assets/typedoc/components/Search.ts b/src/lib/output/themes/default/assets/typedoc/components/Search.ts index 3847bea49..96137a982 100644 --- a/src/lib/output/themes/default/assets/typedoc/components/Search.ts +++ b/src/lib/output/themes/default/assets/typedoc/components/Search.ts @@ -147,10 +147,10 @@ function bindEvents( current?.querySelector("a")?.click(); break; case "ArrowUp": - setCurrentResult(results, field, current, -1); + setNextResult(results, field, current, -1); break; case "ArrowDown": - setCurrentResult(results, field, current, 1); + setNextResult(results, field, current, 1); break; } }); @@ -271,7 +271,7 @@ function updateResults( /** * Move the highlight within the result set. */ -function setCurrentResult( +function setNextResult( results: HTMLElement, field: HTMLInputElement, current: Element | null, From 10ede76ba2097c19e3e0c60f574c241be80bc2a9 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Sun, 19 Jan 2025 23:33:09 +0530 Subject: [PATCH 08/21] Use semantic element `mark` for highlighting search matches --- .../default/assets/typedoc/components/Search.ts | 12 ++++++------ static/style.css | 5 +++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/lib/output/themes/default/assets/typedoc/components/Search.ts b/src/lib/output/themes/default/assets/typedoc/components/Search.ts index 96137a982..2e5d3b1a1 100644 --- a/src/lib/output/themes/default/assets/typedoc/components/Search.ts +++ b/src/lib/output/themes/default/assets/typedoc/components/Search.ts @@ -241,14 +241,14 @@ function updateResults( const row = state.data.rows[Number(res[i].ref)]; const icon = ``; - // Bold the matched part of the query in the search results - let name = boldMatches(row.name, searchText); + // Highlight the matched part of the query in the search results + let name = highlightMatches(row.name, searchText); if (globalThis.DEBUG_SEARCH_WEIGHTS) { name += ` (score: ${res[i].score.toFixed(2)})`; } if (row.parent) { name = ` - ${boldMatches(row.parent, searchText)}.${name}`; + ${highlightMatches(row.parent, searchText)}.${name}`; } const item = document.createElement("li"); @@ -305,7 +305,7 @@ function removeVisualFocus(field: HTMLInputElement) { field.setAttribute("aria-activedescendant", ""); } -function boldMatches(text: string, search: string) { +function highlightMatches(text: string, search: string) { if (search === "") { return text; } @@ -319,9 +319,9 @@ function boldMatches(text: string, search: string) { while (index != -1) { parts.push( escapeHtml(text.substring(lastIndex, index)), - `${escapeHtml( + `${escapeHtml( text.substring(index, index + lowerSearch.length), - )}`, + )}`, ); lastIndex = index + lowerSearch.length; diff --git a/static/style.css b/static/style.css index a8e9235da..2b1d80c71 100644 --- a/static/style.css +++ b/static/style.css @@ -1198,6 +1198,11 @@ #tsd-search-results > li[role="option"] > a .parent { color: var(--color-text-aside); } + #tsd-search-results > li[role="option"] > a mark { + color: inherit; + background-color: inherit; + font-weight: bold; + } #tsd-search-results > li.state { flex: 1; display: grid; From 06e5b4d8e99e1d100686310c1891502b67227e68 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Mon, 20 Jan 2025 01:07:33 +0530 Subject: [PATCH 09/21] Add exit animation for modals, using custom overlay --- .../assets/typedoc/components/Search.ts | 13 +-- .../default/assets/typedoc/utils/modal.ts | 98 ++++++++++++++++++- static/style.css | 21 +++- 3 files changed, 115 insertions(+), 17 deletions(-) diff --git a/src/lib/output/themes/default/assets/typedoc/components/Search.ts b/src/lib/output/themes/default/assets/typedoc/components/Search.ts index 2e5d3b1a1..90abf46d0 100644 --- a/src/lib/output/themes/default/assets/typedoc/components/Search.ts +++ b/src/lib/output/themes/default/assets/typedoc/components/Search.ts @@ -1,7 +1,7 @@ import { debounce } from "../utils/debounce.js"; import { Index } from "lunr"; import { decompressJson } from "../utils/decompress.js"; -import { hideScrollbar, resetScrollbar } from "../utils/modal.js"; +import { openModal, setUpModal } from "../utils/modal.js"; /** * Keep this in sync with the interface in src/lib/output/plugins/JavascriptIndexPlugin.ts @@ -108,10 +108,9 @@ function bindEvents( field: HTMLInputElement, state: SearchState, ) { - trigger.addEventListener("click", () => openModal(searchEl)); + setUpModal(searchEl, "fade-out", { closeOnClick: true }); - searchEl.addEventListener("close", resetScrollbar); - searchEl.addEventListener("cancel", resetScrollbar); + trigger.addEventListener("click", () => openModal(searchEl)); field.addEventListener( "input", @@ -174,12 +173,6 @@ function bindEvents( }); } -function openModal(searchEl: HTMLDialogElement) { - if (searchEl.open) return; - hideScrollbar(); - searchEl.showModal(); -} - function updateResults( results: HTMLElement, query: HTMLInputElement, diff --git a/src/lib/output/themes/default/assets/typedoc/utils/modal.ts b/src/lib/output/themes/default/assets/typedoc/utils/modal.ts index 23977d4dd..577dacef3 100644 --- a/src/lib/output/themes/default/assets/typedoc/utils/modal.ts +++ b/src/lib/output/themes/default/assets/typedoc/utils/modal.ts @@ -3,13 +3,29 @@ * * Browsers allow scrolling of page with native dialog, which is a UX issue. * + * `@starting-style` and `overlay` aren't well supported in FF, and only available in latest versions of chromium, + * hence, a custom overlay workaround is requierd. + * + * Workaround: + * + * - Append a custom overlay element (a div) to `document.body`, + * this does **NOT** handle nested modals, + * as the overlay div cannot be in the top layer, which wouldn't overshadow the parent modal. + * + * - Add exit animation on dialog and overlay, without actually closing them + * - Listen for `animationend` event, and close the modal immediately + * * @see - * - https://github.com/whatwg/html/issues/7732 - * - https://github.com/whatwg/html/issues/7732#issuecomment-2437820350 + * - The "[right](https://frontendmasters.com/blog/animating-dialog/)" way to animate modals + * - [Workaround](https://github.com/whatwg/html/issues/7732#issuecomment-2437820350) to prevent background scrolling */ +// Constants +const CLOSING_CLASS = "closing"; +const OVERLAY_ID = "tsd-overlay"; + /** Fills the gap that scrollbar occupies. Call when the modal is opened */ -export function hideScrollbar() { +function hideScrollbar() { // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes // Should be computed *before* body overflow is set to hidden const width = Math.abs( @@ -23,7 +39,81 @@ export function hideScrollbar() { } /** Resets style changes made by {@link hideScrollbar} */ -export function resetScrollbar() { +function resetScrollbar() { document.body.style.removeProperty("overflow"); document.body.style.removeProperty("padding-right"); } + +/** Could be popover too */ +type Modal = HTMLDialogElement; + +/** + * Must be called to setup a modal element properly for entry and exit side-effects. + * + * Adds event listeners to the modal element, for the closing animation. + * + * Adds workaround to fix scrolling issues caused by default browser behavior. + * + * @param closingAnimation Name of `@keyframes` for closing animation + * @param options Configure modal behavior + * @param options.closeOnEsc Defaults to true + * @param options.closeOnClick Closes modal when clicked on overlay, defaults to false. + */ +export function setUpModal( + modal: Modal, + closingAnimation: string, + options?: { + closeOnEsc?: boolean; + closeOnClick?: boolean; + }, +) { + // Event listener for closing animation + modal.addEventListener("animationend", (e) => { + if (e.animationName !== closingAnimation) return; + modal.classList.remove(CLOSING_CLASS); + document.getElementById(OVERLAY_ID)?.remove(); + modal.close(); + resetScrollbar(); + }); + + // Override modal cancel behavior, hopefully all browsers have same behavior + // > When a `` is dismissed with the `Esc` key, both the `cancel` and `close` events are fired. + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/cancel_event + modal.addEventListener("cancel", (e) => { + e.preventDefault(); + closeModal(modal); + }); + + if (options?.closeOnClick) { + document.addEventListener( + "click", + (e) => { + if (modal.open && !modal.contains(e.target as HTMLElement)) { + closeModal(modal); + } + }, + true, // Disable invoking this handler in bubbling phase + ); + } +} + +export function openModal(modal: Modal) { + if (modal.open) return; + + const overlay = document.createElement("div"); + overlay.id = OVERLAY_ID; + document.body.appendChild(overlay); + + modal.showModal(); + hideScrollbar(); +} + +/** Initiates modal closing, by adding a `closing` class that starts the closing animation */ +export function closeModal(modal: Modal) { + if (!modal.open) return; + const overlay = document.getElementById(OVERLAY_ID); + if (overlay) { + overlay.classList.add(CLOSING_CLASS); + } + modal.classList.add(CLOSING_CLASS); +} diff --git a/static/style.css b/static/style.css index 2b1d80c71..cf1914fd4 100644 --- a/static/style.css +++ b/static/style.css @@ -609,8 +609,21 @@ padding: 0; background-color: var(--color-background); } - ::backdrop { + dialog::backdrop { + display: none; + } + #tsd-overlay { background-color: rgba(0, 0, 0, 0.5); + position: fixed; + z-index: 9999; + top: 0; + left: 0; + right: 0; + bottom: 0; + animation: fade-in var(--modal-animation-duration) forwards; + } + #tsd-overlay.closing { + animation-name: fade-out; } .tsd-typography { @@ -1122,10 +1135,12 @@ margin-bottom: 1rem; } - #tsd-search[open], - #tsd-search[open]::backdrop { + #tsd-search[open] { animation: fade-in var(--modal-animation-duration) ease-out forwards; } + #tsd-search[open].closing { + animation-name: fade-out; + } /* Avoid setting `display` on closed dialog */ #tsd-search[open] { From af6155657dfdf9ec2827d4ad213d6f68e5879285 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Mon, 20 Jan 2025 01:13:05 +0530 Subject: [PATCH 10/21] Remove unused keyframes. Merge `body` selectors --- static/style.css | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/static/style.css b/static/style.css index cf1914fd4..4a08addc2 100644 --- a/static/style.css +++ b/static/style.css @@ -253,10 +253,6 @@ color-scheme: var(--color-scheme); } - body { - margin: 0; - } - :root[data-theme="light"] { --color-background: var(--light-color-background); --color-background-secondary: var(--light-color-background-secondary); @@ -469,29 +465,6 @@ opacity: 0; } } - @keyframes fade-in-delayed { - 0% { - opacity: 0; - } - 33% { - opacity: 0; - } - 100% { - opacity: 1; - } - } - @keyframes fade-out-delayed { - 0% { - opacity: 1; - visibility: visible; - } - 66% { - opacity: 0; - } - 100% { - opacity: 0; - } - } @keyframes pop-in-from-right { from { transform: translate(100%, 0); @@ -515,6 +488,7 @@ Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; font-size: 16px; color: var(--color-text); + margin: 0; } a { From 573cd44ffe4ac3d6595bbaad9a915a9cc8f32e99 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Mon, 20 Jan 2025 01:20:12 +0530 Subject: [PATCH 11/21] Adjust max-height with virtual keyboard, in mobiles --- src/lib/output/themes/default/assets/bootstrap.ts | 5 +++++ static/style.css | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lib/output/themes/default/assets/bootstrap.ts b/src/lib/output/themes/default/assets/bootstrap.ts index 468499945..f5b45f811 100644 --- a/src/lib/output/themes/default/assets/bootstrap.ts +++ b/src/lib/output/themes/default/assets/bootstrap.ts @@ -26,3 +26,8 @@ Object.defineProperty(window, "app", { value: app }); initSearch(); initNav(); initHierarchy(); + +if ("virtualKeyboard" in navigator) { + // @ts-ignore + navigator.virtualKeyboard.overlaysContent = true; +} diff --git a/static/style.css b/static/style.css index 4a08addc2..3f2037d02 100644 --- a/static/style.css +++ b/static/style.css @@ -1124,10 +1124,11 @@ width: 32rem; max-width: 90vw; min-height: 8rem; - max-height: 50vh; + max-height: calc(100vh - env(keyboard-inset-height, 0px) - 25vh); /* Anchor dialog to top */ - margin-top: 25vh; + margin-top: 10vh; border-radius: 6px; + will-change: max-height; } #tsd-search-input { box-sizing: border-box; From 523e097619d46d1e57fbb4c2e8e71769ad163257 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Mon, 20 Jan 2025 01:44:42 +0530 Subject: [PATCH 12/21] fix placeholder text color on focus search input --- static/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/static/style.css b/static/style.css index 3f2037d02..9134985d4 100644 --- a/static/style.css +++ b/static/style.css @@ -1150,6 +1150,10 @@ background: var(--color-accent); border-color: transparent; } + #tsd-search-input:focus-visible::placeholder { + color: var(--color-text); + opacity: 0.8; + } #tsd-search-results { margin: 0; margin-top: 0.5rem; From f27f1501783ecea3d2d033210eb8043a615f440d Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Mon, 20 Jan 2025 12:22:49 +0530 Subject: [PATCH 13/21] Remove min-height from search dialog and set it to `.state` --- static/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/style.css b/static/style.css index 9134985d4..5aa970630 100644 --- a/static/style.css +++ b/static/style.css @@ -1123,7 +1123,6 @@ padding: 1rem; width: 32rem; max-width: 90vw; - min-height: 8rem; max-height: calc(100vh - env(keyboard-inset-height, 0px) - 25vh); /* Anchor dialog to top */ margin-top: 10vh; @@ -1201,6 +1200,7 @@ flex: 1; display: grid; place-content: center; + min-height: 6rem; } .tsd-signature { From 8986d0699d523ac672948553b530ab42931c9ed5 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Mon, 20 Jan 2025 12:30:12 +0530 Subject: [PATCH 14/21] fix single element edge case in `setNextResult` --- .../output/themes/default/assets/typedoc/components/Search.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/output/themes/default/assets/typedoc/components/Search.ts b/src/lib/output/themes/default/assets/typedoc/components/Search.ts index 90abf46d0..b9d560336 100644 --- a/src/lib/output/themes/default/assets/typedoc/components/Search.ts +++ b/src/lib/output/themes/default/assets/typedoc/components/Search.ts @@ -278,6 +278,9 @@ function setNextResult( next = current?.previousElementSibling || results.lastElementChild; } + // When only one child is present. + if (next === current) return; + // bad markup if (!next || next.role !== "option") { console.error("Option missing"); From d3fd2e10830079ef03e2bbba783d3ac4de807169 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Mon, 20 Jan 2025 12:50:27 +0530 Subject: [PATCH 15/21] Show recent searches when the search query is empty - store recent searches (that were clicked) in localStorage, with `tsd-search-recent` key - Omit `matchData` property in `res`'s type to make it compatible with recent searches stored in localStorage. - This implementation prevents broken links, but refs might change, changing the suggestions --- .../assets/typedoc/components/Search.ts | 130 ++++++++++++++---- 1 file changed, 106 insertions(+), 24 deletions(-) diff --git a/src/lib/output/themes/default/assets/typedoc/components/Search.ts b/src/lib/output/themes/default/assets/typedoc/components/Search.ts index b9d560336..4a1f6cddc 100644 --- a/src/lib/output/themes/default/assets/typedoc/components/Search.ts +++ b/src/lib/output/themes/default/assets/typedoc/components/Search.ts @@ -2,6 +2,7 @@ import { debounce } from "../utils/debounce.js"; import { Index } from "lunr"; import { decompressJson } from "../utils/decompress.js"; import { openModal, setUpModal } from "../utils/modal.js"; +import { storage } from "../utils/storage.js"; /** * Keep this in sync with the interface in src/lib/output/plugins/JavascriptIndexPlugin.ts @@ -39,6 +40,17 @@ let optionsIdCounter = 0; let resultCount = 0; +/** Omit `matchData` for compatibility with recent searches */ +type PartialSearch = Omit; + +/** + * Recent search result (which were clicked). + * Recent searches have their score value set to `0`. + * + * Max length: 3 + */ +let recentSearches: PartialSearch[] = []; + /** * Populates search data into `state`, if available. * Removes deault loading message @@ -53,6 +65,8 @@ async function updateIndex(state: SearchState, results: HTMLElement) { state.index = Index.load(data.index); results.querySelector("li.state")?.remove(); + recentSearches = parseRecentSearches(); + updateResults(results, "", state); } catch (e) { console.error(e); const message = window.translations.theme_search_index_not_available; @@ -115,7 +129,7 @@ function bindEvents( field.addEventListener( "input", debounce(() => { - updateResults(results, field, state); + updateResults(results, field.value.trim(), state); }, 200), ); @@ -171,11 +185,33 @@ function bindEvents( openModal(searchEl); } }); + + // Track the option being selected + results.addEventListener("click", (e) => { + const target = (e.target as HTMLElement).closest( + 'li[role="option"]', + ) as HTMLLinkElement | null; + if (!target) return; + + const ref = target.dataset.ref || ""; + const previousIndex = recentSearches.findIndex((el) => el.ref === ref); + if (previousIndex === -1) { + recentSearches.unshift({ ref, score: 0 }); + } else if (previousIndex !== 0) { + const prev = recentSearches.splice(previousIndex, 1)[0]; + recentSearches.unshift(prev); + } + + if (recentSearches.length > 3) { + recentSearches.pop(); + } + setRecentSearches(recentSearches); + }); } function updateResults( results: HTMLElement, - query: HTMLInputElement, + searchText: string, state: SearchState, ) { // Don't clear results if loading state is not ready, @@ -185,10 +221,8 @@ function updateResults( results.innerHTML = ""; optionsIdCounter += 1; - const searchText = query.value.trim(); - // Perform a wildcard search - let res: Index.Result[]; + let res: PartialSearch[]; if (searchText) { // Create a wildcard out of space-separated words in the query, // ignoring any extra spaces @@ -200,38 +234,36 @@ function updateResults( .join(" "); res = state.index.search(searchWithWildcards); } else { - // Set empty `res` to prevent getting random results with wildcard search + // Set res as the recent searches // when the `searchText` is empty. - res = []; + res = recentSearches; } resultCount = res.length; - if (res.length === 0) { - const item = createStateEl(window.translations.theme_search_no_results); - results.appendChild(item); - return; - } - - for (let i = 0; i < res.length; i++) { - const item = res[i]; - const row = state.data.rows[Number(item.ref)]; - let boost = 1; + if (searchText) { + for (let i = 0; i < res.length; i++) { + const item = res[i]; + const row = state.data.rows[Number(item.ref)]; + let boost = 1; + + // boost by exact match on name + if (row.name.toLowerCase().startsWith(searchText.toLowerCase())) { + boost *= + 1 + 1 / (1 + Math.abs(row.name.length - searchText.length)); + } - // boost by exact match on name - if (row.name.toLowerCase().startsWith(searchText.toLowerCase())) { - boost *= - 1 + 1 / (1 + Math.abs(row.name.length - searchText.length)); + item.score *= boost; } - item.score *= boost; + res.sort((a, b) => b.score - a.score); } - res.sort((a, b) => b.score - a.score); - const c = Math.min(10, res.length); for (let i = 0; i < c; i++) { const row = state.data.rows[Number(res[i].ref)]; + // Ref could be invalid when dealing with localStorage values + if (!row) continue; const icon = ``; // Highlight the matched part of the query in the search results @@ -248,6 +280,7 @@ function updateResults( item.id = `tsd-search:${optionsIdCounter}-${i}`; item.role = "option"; item.ariaSelected = "false"; + item.dataset.ref = res[i].ref; item.classList.value = row.classes ?? ""; const anchor = document.createElement("a"); @@ -259,6 +292,15 @@ function updateResults( results.appendChild(item); } + + if (results.childElementCount === 0) { + const message = searchText + ? window.translations.theme_search_no_results + : window.translations.theme_search_no_recent_searches; + const item = createStateEl(message); + results.appendChild(item); + return; + } } /** @@ -390,3 +432,43 @@ function isKeyboardActive() { !inputWithoutKeyboard.includes((activeElement as HTMLInputElement).type) ); } + +/** + * Gets recent searches from localStorage, parses, and returns + * + * This implementation in "unforgiving", + * i.e. the parsed value must match the criteria: string array of length <= 3 + * + * Returns empty array in case of failure + * @returns + */ +function parseRecentSearches() { + try { + const recent = storage.getItem("tsd-search-recent")!; + const parsed = JSON.parse(recent); + if (Array.isArray(parsed) && parsed.length <= 3) { + return parsed.map((ref) => { + if (isNaN(Number(ref))) throw new Error("Invalid ref"); + + return { + ref, + score: 0, + } satisfies PartialSearch; + }); + } + return []; + } catch { + return []; + } +} + +/** + * Save recentSearches to localStorage. + * Performs no checks on the argument. + */ +function setRecentSearches(recent: PartialSearch[]) { + storage.setItem( + "tsd-search-recent", + JSON.stringify(recent.map(({ ref }) => ref)), + ); +} From d25d3a08691aaee1ed4fff0e028472986dcab8ae Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Sat, 25 Jan 2025 12:13:14 +0530 Subject: [PATCH 16/21] Completely revert "recent searches" feature. This reverts commit 1beacfa1ee211b30e006f556e3225baedfdffbe6, and the new i18n strings added in 3548d14a1236edcdd12ee4527dbdfb3ec888a466. --- src/lib/internationalization/locales/en.cts | 1 - .../assets/typedoc/components/Search.ts | 130 ++++-------------- 2 files changed, 24 insertions(+), 107 deletions(-) diff --git a/src/lib/internationalization/locales/en.cts b/src/lib/internationalization/locales/en.cts index 328e4ef29..7fef752e1 100644 --- a/src/lib/internationalization/locales/en.cts +++ b/src/lib/internationalization/locales/en.cts @@ -539,5 +539,4 @@ export = { theme_search_index_not_available: "The search index is not available", theme_search_no_results: "No results found", theme_search_placeholder: "Search the docs", - theme_search_no_recent_searches: "No recent searches", } as const; diff --git a/src/lib/output/themes/default/assets/typedoc/components/Search.ts b/src/lib/output/themes/default/assets/typedoc/components/Search.ts index 4a1f6cddc..b9d560336 100644 --- a/src/lib/output/themes/default/assets/typedoc/components/Search.ts +++ b/src/lib/output/themes/default/assets/typedoc/components/Search.ts @@ -2,7 +2,6 @@ import { debounce } from "../utils/debounce.js"; import { Index } from "lunr"; import { decompressJson } from "../utils/decompress.js"; import { openModal, setUpModal } from "../utils/modal.js"; -import { storage } from "../utils/storage.js"; /** * Keep this in sync with the interface in src/lib/output/plugins/JavascriptIndexPlugin.ts @@ -40,17 +39,6 @@ let optionsIdCounter = 0; let resultCount = 0; -/** Omit `matchData` for compatibility with recent searches */ -type PartialSearch = Omit; - -/** - * Recent search result (which were clicked). - * Recent searches have their score value set to `0`. - * - * Max length: 3 - */ -let recentSearches: PartialSearch[] = []; - /** * Populates search data into `state`, if available. * Removes deault loading message @@ -65,8 +53,6 @@ async function updateIndex(state: SearchState, results: HTMLElement) { state.index = Index.load(data.index); results.querySelector("li.state")?.remove(); - recentSearches = parseRecentSearches(); - updateResults(results, "", state); } catch (e) { console.error(e); const message = window.translations.theme_search_index_not_available; @@ -129,7 +115,7 @@ function bindEvents( field.addEventListener( "input", debounce(() => { - updateResults(results, field.value.trim(), state); + updateResults(results, field, state); }, 200), ); @@ -185,33 +171,11 @@ function bindEvents( openModal(searchEl); } }); - - // Track the option being selected - results.addEventListener("click", (e) => { - const target = (e.target as HTMLElement).closest( - 'li[role="option"]', - ) as HTMLLinkElement | null; - if (!target) return; - - const ref = target.dataset.ref || ""; - const previousIndex = recentSearches.findIndex((el) => el.ref === ref); - if (previousIndex === -1) { - recentSearches.unshift({ ref, score: 0 }); - } else if (previousIndex !== 0) { - const prev = recentSearches.splice(previousIndex, 1)[0]; - recentSearches.unshift(prev); - } - - if (recentSearches.length > 3) { - recentSearches.pop(); - } - setRecentSearches(recentSearches); - }); } function updateResults( results: HTMLElement, - searchText: string, + query: HTMLInputElement, state: SearchState, ) { // Don't clear results if loading state is not ready, @@ -221,8 +185,10 @@ function updateResults( results.innerHTML = ""; optionsIdCounter += 1; + const searchText = query.value.trim(); + // Perform a wildcard search - let res: PartialSearch[]; + let res: Index.Result[]; if (searchText) { // Create a wildcard out of space-separated words in the query, // ignoring any extra spaces @@ -234,36 +200,38 @@ function updateResults( .join(" "); res = state.index.search(searchWithWildcards); } else { - // Set res as the recent searches + // Set empty `res` to prevent getting random results with wildcard search // when the `searchText` is empty. - res = recentSearches; + res = []; } resultCount = res.length; - if (searchText) { - for (let i = 0; i < res.length; i++) { - const item = res[i]; - const row = state.data.rows[Number(item.ref)]; - let boost = 1; - - // boost by exact match on name - if (row.name.toLowerCase().startsWith(searchText.toLowerCase())) { - boost *= - 1 + 1 / (1 + Math.abs(row.name.length - searchText.length)); - } + if (res.length === 0) { + const item = createStateEl(window.translations.theme_search_no_results); + results.appendChild(item); + return; + } - item.score *= boost; + for (let i = 0; i < res.length; i++) { + const item = res[i]; + const row = state.data.rows[Number(item.ref)]; + let boost = 1; + + // boost by exact match on name + if (row.name.toLowerCase().startsWith(searchText.toLowerCase())) { + boost *= + 1 + 1 / (1 + Math.abs(row.name.length - searchText.length)); } - res.sort((a, b) => b.score - a.score); + item.score *= boost; } + res.sort((a, b) => b.score - a.score); + const c = Math.min(10, res.length); for (let i = 0; i < c; i++) { const row = state.data.rows[Number(res[i].ref)]; - // Ref could be invalid when dealing with localStorage values - if (!row) continue; const icon = ``; // Highlight the matched part of the query in the search results @@ -280,7 +248,6 @@ function updateResults( item.id = `tsd-search:${optionsIdCounter}-${i}`; item.role = "option"; item.ariaSelected = "false"; - item.dataset.ref = res[i].ref; item.classList.value = row.classes ?? ""; const anchor = document.createElement("a"); @@ -292,15 +259,6 @@ function updateResults( results.appendChild(item); } - - if (results.childElementCount === 0) { - const message = searchText - ? window.translations.theme_search_no_results - : window.translations.theme_search_no_recent_searches; - const item = createStateEl(message); - results.appendChild(item); - return; - } } /** @@ -432,43 +390,3 @@ function isKeyboardActive() { !inputWithoutKeyboard.includes((activeElement as HTMLInputElement).type) ); } - -/** - * Gets recent searches from localStorage, parses, and returns - * - * This implementation in "unforgiving", - * i.e. the parsed value must match the criteria: string array of length <= 3 - * - * Returns empty array in case of failure - * @returns - */ -function parseRecentSearches() { - try { - const recent = storage.getItem("tsd-search-recent")!; - const parsed = JSON.parse(recent); - if (Array.isArray(parsed) && parsed.length <= 3) { - return parsed.map((ref) => { - if (isNaN(Number(ref))) throw new Error("Invalid ref"); - - return { - ref, - score: 0, - } satisfies PartialSearch; - }); - } - return []; - } catch { - return []; - } -} - -/** - * Save recentSearches to localStorage. - * Performs no checks on the argument. - */ -function setRecentSearches(recent: PartialSearch[]) { - storage.setItem( - "tsd-search-recent", - JSON.stringify(recent.map(({ ref }) => ref)), - ); -} From 0a64f24b2077acd9f5606dd8e35558251e93e424 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Sat, 25 Jan 2025 14:21:48 +0530 Subject: [PATCH 17/21] Fix typo and refactor search.ts code - fix typo (requierd -> required) - Remove unnecessary try-catch - Add falsy check for document.getElementById argument to avoid console warning in firefox --- .../assets/typedoc/components/Search.ts | 19 ++++++------------- .../default/assets/typedoc/utils/modal.ts | 2 +- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/lib/output/themes/default/assets/typedoc/components/Search.ts b/src/lib/output/themes/default/assets/typedoc/components/Search.ts index b9d560336..4946e8985 100644 --- a/src/lib/output/themes/default/assets/typedoc/components/Search.ts +++ b/src/lib/output/themes/default/assets/typedoc/components/Search.ts @@ -46,19 +46,12 @@ let resultCount = 0; async function updateIndex(state: SearchState, results: HTMLElement) { if (!window.searchData) return; - try { - const data: IData = await decompressJson(window.searchData); + const data: IData = await decompressJson(window.searchData); - state.data = data; - state.index = Index.load(data.index); + state.data = data; + state.index = Index.load(data.index); - results.querySelector("li.state")?.remove(); - } catch (e) { - console.error(e); - const message = window.translations.theme_search_index_not_available; - const stateEl = createStateEl(message); - results.replaceChildren(stateEl); - } + results.querySelector("li.state")?.remove(); } export function initSearch() { @@ -126,7 +119,7 @@ function bindEvents( // Get the visually focused element, if any const currentId = field.getAttribute("aria-activedescendant"); - const current = document.getElementById(currentId || ""); + const current = currentId ? document.getElementById(currentId) : null; // Remove visual focus on cursor position change if (current) { @@ -295,7 +288,7 @@ function setNextResult( function removeVisualFocus(field: HTMLInputElement) { const currentId = field.getAttribute("aria-activedescendant"); - const current = document.getElementById(currentId || ""); + const current = currentId ? document.getElementById(currentId) : null; current?.setAttribute("aria-selected", "false"); field.setAttribute("aria-activedescendant", ""); diff --git a/src/lib/output/themes/default/assets/typedoc/utils/modal.ts b/src/lib/output/themes/default/assets/typedoc/utils/modal.ts index 577dacef3..92dbbde63 100644 --- a/src/lib/output/themes/default/assets/typedoc/utils/modal.ts +++ b/src/lib/output/themes/default/assets/typedoc/utils/modal.ts @@ -4,7 +4,7 @@ * Browsers allow scrolling of page with native dialog, which is a UX issue. * * `@starting-style` and `overlay` aren't well supported in FF, and only available in latest versions of chromium, - * hence, a custom overlay workaround is requierd. + * hence, a custom overlay workaround is required. * * Workaround: * From 1a68fbf88ebf7191f5b667cfe00ae654b332d6f3 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Sun, 26 Jan 2025 20:53:05 +0530 Subject: [PATCH 18/21] Fix invalid search message implementation All children of a listbox must be options, showing error message in an li tag without role=option was invalid. An alternative approach is to use another element with aria-live=polite attribute, to announce the results to the user. However, consecutive queries with no results wouldn't be announced (because the message would be the same), so add search query as part of the message to make it dynamic - Add a new element with aria-live and aria-atomic attributes for a helpful message - Remove unnecessary attribute selector for search results. - Add styles height/margin styles for non-empty elements only. - Add maxLength to input --- src/lib/internationalization/locales/en.cts | 2 +- src/lib/output/plugins/AssetsPlugin.ts | 4 +- .../default/assets/typedoc/Application.ts | 4 +- .../assets/typedoc/components/Search.ts | 66 +++++++++++-------- .../themes/default/partials/toolbar.tsx | 8 ++- static/style.css | 24 ++++--- 6 files changed, 62 insertions(+), 46 deletions(-) diff --git a/src/lib/internationalization/locales/en.cts b/src/lib/internationalization/locales/en.cts index 7fef752e1..20269f19b 100644 --- a/src/lib/internationalization/locales/en.cts +++ b/src/lib/internationalization/locales/en.cts @@ -537,6 +537,6 @@ export = { theme_hierarchy_expand: "Expand", theme_hierarchy_collapse: "Collapse", theme_search_index_not_available: "The search index is not available", - theme_search_no_results: "No results found", + theme_search_no_results_found_for: "No results found for", // for theme_search_placeholder: "Search the docs", } as const; diff --git a/src/lib/output/plugins/AssetsPlugin.ts b/src/lib/output/plugins/AssetsPlugin.ts index ce3c881b3..71b29544a 100644 --- a/src/lib/output/plugins/AssetsPlugin.ts +++ b/src/lib/output/plugins/AssetsPlugin.ts @@ -43,8 +43,8 @@ export class AssetsPlugin extends RendererComponent { folder: i18n.theme_folder(), theme_search_index_not_available: this.application.i18n.theme_search_index_not_available(), - theme_search_no_results: - this.application.i18n.theme_search_no_results(), + theme_search_no_results_found_for: + this.application.i18n.theme_search_no_results_found_for(), }; for (const key of getEnumKeys(ReflectionKind)) { diff --git a/src/lib/output/themes/default/assets/typedoc/Application.ts b/src/lib/output/themes/default/assets/typedoc/Application.ts index 3434aa0ac..7cf4d738f 100644 --- a/src/lib/output/themes/default/assets/typedoc/Application.ts +++ b/src/lib/output/themes/default/assets/typedoc/Application.ts @@ -13,7 +13,7 @@ declare global { folder: string; [k: `kind_${number}`]: string; theme_search_index_not_available: string; - theme_search_no_results: string; + theme_search_no_results_found_for: string; }; } } @@ -53,7 +53,7 @@ window.translations ||= { kind_4194304: "Reference", kind_8388608: "Document", theme_search_index_not_available: "The search index is not available", - theme_search_no_results: "No results found", + theme_search_no_results_found_for: "No results found for", }; /** diff --git a/src/lib/output/themes/default/assets/typedoc/components/Search.ts b/src/lib/output/themes/default/assets/typedoc/components/Search.ts index 4946e8985..4c94d44a5 100644 --- a/src/lib/output/themes/default/assets/typedoc/components/Search.ts +++ b/src/lib/output/themes/default/assets/typedoc/components/Search.ts @@ -37,13 +37,11 @@ interface SearchState { /** Counter to get unique IDs for options */ let optionsIdCounter = 0; -let resultCount = 0; - /** * Populates search data into `state`, if available. * Removes deault loading message */ -async function updateIndex(state: SearchState, results: HTMLElement) { +async function updateIndex(state: SearchState, status: HTMLElement) { if (!window.searchData) return; const data: IData = await decompressJson(window.searchData); @@ -51,11 +49,11 @@ async function updateIndex(state: SearchState, results: HTMLElement) { state.data = data; state.index = Index.load(data.index); - results.querySelector("li.state")?.remove(); + status.innerHTML = ""; } export function initSearch() { - const searchTrigger = document.getElementById( + const trigger = document.getElementById( "tsd-search-trigger", ) as HTMLButtonElement | null; @@ -73,7 +71,9 @@ export function initSearch() { "tsd-search-script", ) as HTMLScriptElement | null; - if (!(searchTrigger && searchEl && field && results && searchScript)) { + const status = document.getElementById("tsd-search-status"); + + if (!(trigger && searchEl && field && results && searchScript && status)) { throw new Error("Search controls missing"); } @@ -83,24 +83,28 @@ export function initSearch() { searchScript.addEventListener("error", () => { const message = window.translations.theme_search_index_not_available; - const stateEl = createStateEl(message); - results.replaceChildren(stateEl); + updateStatusEl(status, message); }); searchScript.addEventListener("load", () => { - updateIndex(state, results); + updateIndex(state, status); }); - updateIndex(state, results); + updateIndex(state, status); - bindEvents(searchTrigger, searchEl, results, field, state); + bindEvents({ trigger, searchEl, results, field, status }, state); } function bindEvents( - trigger: HTMLButtonElement, - searchEl: HTMLDialogElement, - results: HTMLElement, - field: HTMLInputElement, + elements: { + trigger: HTMLButtonElement; + searchEl: HTMLDialogElement; + results: HTMLElement; + field: HTMLInputElement; + status: HTMLElement; + }, state: SearchState, ) { + const { field, results, searchEl, status, trigger } = elements; + setUpModal(searchEl, "fade-out", { closeOnClick: true }); trigger.addEventListener("click", () => openModal(searchEl)); @@ -108,12 +112,17 @@ function bindEvents( field.addEventListener( "input", debounce(() => { - updateResults(results, field, state); + updateResults(results, field, status, state); }, 200), ); field.addEventListener("keydown", (e) => { - if (resultCount === 0 || e.ctrlKey || e.metaKey || e.altKey) { + if ( + results.childElementCount === 0 || + e.ctrlKey || + e.metaKey || + e.altKey + ) { return; } @@ -169,6 +178,7 @@ function bindEvents( function updateResults( results: HTMLElement, query: HTMLInputElement, + status: HTMLElement, state: SearchState, ) { // Don't clear results if loading state is not ready, @@ -176,6 +186,7 @@ function updateResults( if (!state.index || !state.data) return; results.innerHTML = ""; + status.innerHTML = ""; optionsIdCounter += 1; const searchText = query.value.trim(); @@ -198,11 +209,11 @@ function updateResults( res = []; } - resultCount = res.length; - - if (res.length === 0) { - const item = createStateEl(window.translations.theme_search_no_results); - results.appendChild(item); + if (res.length === 0 && searchText) { + const message = + window.translations.theme_search_no_results_found_for + + ` "${searchText}"`; + updateStatusEl(status, message); return; } @@ -338,14 +349,11 @@ function escapeHtml(text: string) { } /** - * Returns a `li` element, with `state` class, - * @param message Message to set as **innerHTML** + * Updates the status element, with aria-live attriute, which should be announced to the user. + * @param message Message to set as **innerHTML** in a wrapper element, if not empty. */ -function createStateEl(message: string) { - const stateEl = document.createElement("li"); - stateEl.className = "state"; - stateEl.innerHTML = message; - return stateEl; +function updateStatusEl(status: HTMLElement, message: string) { + status.innerHTML = message ? `
${message}
` : ""; } /** diff --git a/src/lib/output/themes/default/partials/toolbar.tsx b/src/lib/output/themes/default/partials/toolbar.tsx index 7312890e9..bd1157171 100644 --- a/src/lib/output/themes/default/partials/toolbar.tsx +++ b/src/lib/output/themes/default/partials/toolbar.tsx @@ -31,11 +31,13 @@ export const toolbar = (context: DefaultThemeRenderContext, props: PageEvent -
    -
  • {context.i18n.theme_preparing_search_index()}
  • -
+
    +
    +
    {context.i18n.theme_preparing_search_index()}
    +
    li[role="option"] { + #tsd-search-results:not(:empty) { + margin-top: 0.5rem; + } + #tsd-search-results > li { background-color: var(--color-background); line-height: 1.5; box-sizing: border-box; border-radius: 4px; } - #tsd-search-results > li[role="option"]:nth-child(even) { + #tsd-search-results > li:nth-child(even) { background-color: var(--color-background-secondary); } - #tsd-search-results > li[role="option"]:is(:hover, [aria-selected="true"]) { + #tsd-search-results > li:is(:hover, [aria-selected="true"]) { background-color: var(--color-accent); } /* It's important that this takes full size of parent `li`, to capture a click on `li` */ - #tsd-search-results > li[role="option"] > a { + #tsd-search-results > li > a { display: flex; align-items: center; padding: 0.5rem 0.25rem; box-sizing: border-box; width: 100%; } - #tsd-search-results > li[role="option"] > a > .text { + #tsd-search-results > li > a > .text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; } - #tsd-search-results > li[role="option"] > a .parent { + #tsd-search-results > li > a .parent { color: var(--color-text-aside); } - #tsd-search-results > li[role="option"] > a mark { + #tsd-search-results > li > a mark { color: inherit; background-color: inherit; font-weight: bold; } - #tsd-search-results > li.state { + #tsd-search-status { flex: 1; display: grid; place-content: center; + text-align: center; + overflow-wrap: anywhere; + } + #tsd-search-status:not(:empty) { min-height: 6rem; } From f425f95ce2631dee3bd65b2fba9cb14848a0e722 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Mon, 27 Jan 2025 04:14:04 +0530 Subject: [PATCH 19/21] Add placeholder for no_results i18n string --- src/lib/internationalization/locales/en.cts | 2 +- src/lib/output/plugins/AssetsPlugin.ts | 6 ++++-- src/lib/output/themes/default/assets/typedoc/Application.ts | 4 ++-- .../themes/default/assets/typedoc/components/Search.ts | 6 ++++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/lib/internationalization/locales/en.cts b/src/lib/internationalization/locales/en.cts index 20269f19b..8bcd776f2 100644 --- a/src/lib/internationalization/locales/en.cts +++ b/src/lib/internationalization/locales/en.cts @@ -537,6 +537,6 @@ export = { theme_hierarchy_expand: "Expand", theme_hierarchy_collapse: "Collapse", theme_search_index_not_available: "The search index is not available", - theme_search_no_results_found_for: "No results found for", // for + theme_search_no_results_found_for_0: "No results found for {0}", theme_search_placeholder: "Search the docs", } as const; diff --git a/src/lib/output/plugins/AssetsPlugin.ts b/src/lib/output/plugins/AssetsPlugin.ts index 71b29544a..e1e24edb3 100644 --- a/src/lib/output/plugins/AssetsPlugin.ts +++ b/src/lib/output/plugins/AssetsPlugin.ts @@ -43,8 +43,10 @@ export class AssetsPlugin extends RendererComponent { folder: i18n.theme_folder(), theme_search_index_not_available: this.application.i18n.theme_search_index_not_available(), - theme_search_no_results_found_for: - this.application.i18n.theme_search_no_results_found_for(), + theme_search_no_results_found_for_0: + this.application.i18n.theme_search_no_results_found_for_0( + "{0}", + ), }; for (const key of getEnumKeys(ReflectionKind)) { diff --git a/src/lib/output/themes/default/assets/typedoc/Application.ts b/src/lib/output/themes/default/assets/typedoc/Application.ts index 7cf4d738f..d31e6bf75 100644 --- a/src/lib/output/themes/default/assets/typedoc/Application.ts +++ b/src/lib/output/themes/default/assets/typedoc/Application.ts @@ -13,7 +13,7 @@ declare global { folder: string; [k: `kind_${number}`]: string; theme_search_index_not_available: string; - theme_search_no_results_found_for: string; + theme_search_no_results_found_for_0: string; }; } } @@ -53,7 +53,7 @@ window.translations ||= { kind_4194304: "Reference", kind_8388608: "Document", theme_search_index_not_available: "The search index is not available", - theme_search_no_results_found_for: "No results found for", + theme_search_no_results_found_for_0: "No results found for {0}", }; /** diff --git a/src/lib/output/themes/default/assets/typedoc/components/Search.ts b/src/lib/output/themes/default/assets/typedoc/components/Search.ts index 4c94d44a5..166fbc805 100644 --- a/src/lib/output/themes/default/assets/typedoc/components/Search.ts +++ b/src/lib/output/themes/default/assets/typedoc/components/Search.ts @@ -211,8 +211,10 @@ function updateResults( if (res.length === 0 && searchText) { const message = - window.translations.theme_search_no_results_found_for + - ` "${searchText}"`; + window.translations.theme_search_no_results_found_for_0.replace( + "{0}", + ` "${escapeHtml(searchText)}" `, + ); updateStatusEl(status, message); return; } From 67d03bb32d43ef199de9a04d168c7a4c927e2ce9 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Mon, 27 Jan 2025 05:17:01 +0530 Subject: [PATCH 20/21] remove global button styles and add it to tsd-widget --- static/style.css | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/static/style.css b/static/style.css index 371e1f0b6..55dbc8f4f 100644 --- a/static/style.css +++ b/static/style.css @@ -548,12 +548,6 @@ border-left: 4px solid gray; } - button { - border: none; - appearance: none; - background-color: transparent; - } - img { max-width: 100%; } @@ -1323,9 +1317,11 @@ width: 2.5rem; transition: opacity 0.1s, - background-color 0.2s; + background-color 0.1s; text-align: center; cursor: pointer; + border: none; + background-color: transparent; } .tsd-widget:hover { opacity: 0.9; From f4ebea676f707e3a2624c57a9d1b4aa503f51df2 Mon Sep 17 00:00:00 2001 From: phoneticallySAARTHaK Date: Mon, 27 Jan 2025 05:40:14 +0530 Subject: [PATCH 21/21] Improve text contrast, satisfying WCAG level AA - Add background-active and contrast-text css properties - update text-aside, menu-item-active color in light theme - break light and dark colors in separate selectors to make them easier to work with "contrast-text" slightly improves contrast. though, not enough to pass WCAG level AAA --- static/style.css | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/static/style.css b/static/style.css index 55dbc8f4f..eea850faf 100644 --- a/static/style.css +++ b/static/style.css @@ -19,12 +19,15 @@ /* Light */ --light-color-background: #f2f4f8; --light-color-background-secondary: #eff0f1; - --light-color-warning-text: #222; + /* Not to be confused with [:active](https://developer.mozilla.org/en-US/docs/Web/CSS/:active) */ + --light-color-background-active: #d6d8da; --light-color-background-warning: #e6e600; + --light-color-warning-text: #222; --light-color-accent: #c5c7c9; - --light-color-active-menu-item: var(--light-color-accent); + --light-color-active-menu-item: var(--light-color-background-active); --light-color-text: #222; - --light-color-text-aside: #6e6e6e; + --light-color-contrast-text: #000; + --light-color-text-aside: #5e5e5e; --light-color-icon-background: var(--light-color-background); --light-color-icon-text: var(--light-color-text); @@ -72,15 +75,20 @@ --light-external-icon: url("data:image/svg+xml;utf8,"); --light-color-scheme: light; + } + :root { /* Dark */ --dark-color-background: #2b2e33; --dark-color-background-secondary: #1e2024; + /* Not to be confused with [:active](https://developer.mozilla.org/en-US/docs/Web/CSS/:active) */ + --dark-color-background-active: #5d5d6a; --dark-color-background-warning: #bebe00; --dark-color-warning-text: #222; --dark-color-accent: #9096a2; - --dark-color-active-menu-item: #5d5d6a; + --dark-color-active-menu-item: var(--dark-color-background-active); --dark-color-text: #f5f5f5; + --dark-color-contrast-text: #ffffff; --dark-color-text-aside: #dddddd; --dark-color-icon-background: var(--dark-color-background-secondary); @@ -135,11 +143,13 @@ --color-background-secondary: var( --light-color-background-secondary ); + --color-background-active: var(--light-color-background-active); --color-background-warning: var(--light-color-background-warning); --color-warning-text: var(--light-color-warning-text); --color-accent: var(--light-color-accent); --color-active-menu-item: var(--light-color-active-menu-item); --color-text: var(--light-color-text); + --color-contrast-text: var(--light-color-contrast-text); --color-text-aside: var(--light-color-text-aside); --color-icon-background: var(--light-color-icon-background); @@ -195,11 +205,13 @@ --color-background-secondary: var( --dark-color-background-secondary ); + --color-background-active: var(--dark-color-background-active); --color-background-warning: var(--dark-color-background-warning); --color-warning-text: var(--dark-color-warning-text); --color-accent: var(--dark-color-accent); --color-active-menu-item: var(--dark-color-active-menu-item); --color-text: var(--dark-color-text); + --color-contrast-text: var(--dark-color-contrast-text); --color-text-aside: var(--dark-color-text-aside); --color-icon-background: var(--dark-color-icon-background); @@ -256,12 +268,14 @@ :root[data-theme="light"] { --color-background: var(--light-color-background); --color-background-secondary: var(--light-color-background-secondary); + --color-background-active: var(--light-color-background-active); --color-background-warning: var(--light-color-background-warning); --color-warning-text: var(--light-color-warning-text); --color-icon-background: var(--light-color-icon-background); --color-accent: var(--light-color-accent); --color-active-menu-item: var(--light-color-active-menu-item); --color-text: var(--light-color-text); + --color-contrast-text: var(--light-color-contrast-text); --color-text-aside: var(--light-color-text-aside); --color-icon-text: var(--light-color-icon-text); @@ -311,12 +325,14 @@ :root[data-theme="dark"] { --color-background: var(--dark-color-background); --color-background-secondary: var(--dark-color-background-secondary); + --color-background-active: var(--dark-color-background-active); --color-background-warning: var(--dark-color-background-warning); --color-warning-text: var(--dark-color-warning-text); --color-icon-background: var(--dark-color-icon-background); --color-accent: var(--dark-color-accent); --color-active-menu-item: var(--dark-color-active-menu-item); --color-text: var(--dark-color-text); + --color-contrast-text: var(--dark-color-contrast-text); --color-text-aside: var(--dark-color-text-aside); --color-icon-text: var(--dark-color-icon-text); @@ -939,6 +955,7 @@ .tsd-navigation a.current, .tsd-page-navigation a.current { background: var(--color-active-menu-item); + color: var(--color-contrast-text); } .tsd-navigation a:hover, .tsd-page-navigation a:hover { @@ -1140,11 +1157,12 @@ background-color 0.2s; } #tsd-search-input:focus-visible { - background: var(--color-accent); + background-color: var(--color-background-active); border-color: transparent; + color: var(--color-contrast-text); } - #tsd-search-input:focus-visible::placeholder { - color: var(--color-text); + #tsd-search-input::placeholder { + color: inherit; opacity: 0.8; } #tsd-search-results { @@ -1169,7 +1187,8 @@ background-color: var(--color-background-secondary); } #tsd-search-results > li:is(:hover, [aria-selected="true"]) { - background-color: var(--color-accent); + background-color: var(--color-background-active); + color: var(--color-contrast-text); } /* It's important that this takes full size of parent `li`, to capture a click on `li` */ #tsd-search-results > li > a {