Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Search as ARIA spec compliant* dialog and combobox pattern #2831

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c53e480
Add css variables for dimensions, and use svh units
phoneticallySAARTHaK Jan 18, 2025
79a9b56
Fix excess/absent margins and padding.
phoneticallySAARTHaK Jan 18, 2025
1df9967
Fix tab order of toolbar and remove unused selectors. **Breaks search**
phoneticallySAARTHaK Jan 19, 2025
3548d14
Add more search related 118n strings, and inject more strings in js s…
phoneticallySAARTHaK Jan 19, 2025
55c0576
Implement search component as combobox, with minimal functionality
phoneticallySAARTHaK Jan 19, 2025
28320e1
Add global keyboard listeners for opening modal
phoneticallySAARTHaK Jan 19, 2025
b0f75ef
rename `setCurrentResult` to `setNextResult`
phoneticallySAARTHaK Jan 19, 2025
f4d4fbd
Use semantic element `mark` for highlighting search matches
phoneticallySAARTHaK Jan 19, 2025
0b58bf5
Add exit animation for modals, using custom overlay
phoneticallySAARTHaK Jan 19, 2025
b2f79de
Remove unused keyframes. Merge `body` selectors
phoneticallySAARTHaK Jan 19, 2025
d687ef6
Adjust max-height with virtual keyboard, in mobiles
phoneticallySAARTHaK Jan 19, 2025
548df5c
fix placeholder text color on focus search input
phoneticallySAARTHaK Jan 19, 2025
a606615
Remove min-height from search dialog and set it to `.state`
phoneticallySAARTHaK Jan 20, 2025
ec6b774
fix single element edge case in `setNextResult`
phoneticallySAARTHaK Jan 20, 2025
1beacfa
Show recent searches when the search query is empty
phoneticallySAARTHaK Jan 20, 2025
4fd690e
Completely revert "recent searches" feature.
phoneticallySAARTHaK Jan 25, 2025
b49240a
Fix typo and refactor search.ts code
phoneticallySAARTHaK Jan 25, 2025
43d91d6
Fix invalid search message implementation
phoneticallySAARTHaK Jan 26, 2025
8a4b5e3
Add placeholder for no_results i18n string
phoneticallySAARTHaK Jan 26, 2025
cccf2d4
remove global button styles and add it to tsd-widget
phoneticallySAARTHaK Jan 26, 2025
28e49c6
Improve text contrast, satisfying WCAG level AA
phoneticallySAARTHaK Jan 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib/internationalization/locales/en.cts
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,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 <search query>
phoneticallySAARTHaK marked this conversation as resolved.
Show resolved Hide resolved
theme_search_placeholder: "Search the docs",
} as const;
4 changes: 2 additions & 2 deletions src/lib/output/plugins/AssetsPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export class AssetsPlugin extends RendererComponent {
this.application.i18n.theme_hierarchy_collapse(),
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(),
};
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/output/themes/default/assets/typedoc/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ declare global {
hierarchy_expand: string;
hierarchy_collapse: string;
theme_search_index_not_available: string;
theme_search_no_results: string;
theme_search_no_results_found_for: string;
};
}
}
Expand All @@ -23,7 +23,7 @@ window.translations ||= {
hierarchy_expand: "Expand",
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",
};

/**
Expand Down
66 changes: 37 additions & 29 deletions src/lib/output/themes/default/assets/typedoc/components/Search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,23 @@ 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);

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;

Expand All @@ -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");
}

Expand All @@ -83,37 +83,46 @@ 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));

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;
}

Expand Down Expand Up @@ -169,13 +178,15 @@ function bindEvents(
function updateResults(
results: HTMLElement,
query: HTMLInputElement,
status: HTMLElement,
state: SearchState,
) {
// Don't clear results if loading state is not ready,
// because loading or error message can be removed.
if (!state.index || !state.data) return;

results.innerHTML = "";
status.innerHTML = "";
optionsIdCounter += 1;

const searchText = query.value.trim();
Expand All @@ -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 +
` "<strong>${searchText}</strong>"`;
phoneticallySAARTHaK marked this conversation as resolved.
Show resolved Hide resolved
updateStatusEl(status, message);
return;
}

Expand Down Expand Up @@ -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 ? `<div>${message}</div>` : "";
}

/**
Expand Down
8 changes: 5 additions & 3 deletions src/lib/output/themes/default/partials/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ export const toolbar = (context: DefaultThemeRenderContext, props: PageEvent<Ref
autocapitalize="off"
autocomplete="off"
placeholder={context.i18n.theme_search_placeholder()}
maxLength={100}
/>

<ul role="listbox" id="tsd-search-results">
<li class="state">{context.i18n.theme_preparing_search_index()}</li>
</ul>
<ul role="listbox" id="tsd-search-results"></ul>
<div id="tsd-search-status" aria-live="polite" aria-atomic="true">
<div>{context.i18n.theme_preparing_search_index()}</div>
</div>
</dialog>

<a
Expand Down
24 changes: 15 additions & 9 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1154,51 +1154,57 @@
}
#tsd-search-results {
margin: 0;
margin-top: 0.5rem;
padding: 0;
list-style: none;
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow-y: auto;
}
#tsd-search-results > 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;
}

Expand Down
Loading