Skip to content

Desktop: Resolves #14763: add settings search to config screen (new spec) - #14820

Merged
laurent22 merged 29 commits into
laurent22:devfrom
slimuCS:feature/14763-settings-search-desktop-v2
Apr 30, 2026
Merged

Desktop: Resolves #14763: add settings search to config screen (new spec)#14820
laurent22 merged 29 commits into
laurent22:devfrom
slimuCS:feature/14763-settings-search-desktop-v2

Conversation

@slimuCS

@slimuCS slimuCS commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #14763

Problem

Desktop settings search did not meet the updated 0318 behavior requirements. Users needed a filtering workflow (not only navigation) that can show all matches in the right panel, keep all sections visible in the sidebar, and clearly indicate matched vs non-matched states.

Solution

This PR implements the desktop settings search model in line with the detailed spec:

  • Add a sidebar-top search field that filters settings by section title, label, and description.
  • Show all matching settings in the right panel by default, grouped by section.
  • Keep all sidebar sections visible during search, while graying out and disabling non-matching sections.
  • Support section-level filtering from the sidebar with a clear path back to all results.
  • Highlight matched terms in section titles, setting labels, and descriptions.
  • Include plugin settings in the same search and filtering flow.

Test Plan

Automated

yarn workspace @joplin/app-desktop tsc --noEmit
yarn linter-precommit
yarn workspace @joplin/app-desktop test gui/ConfigScreen/searchHighlight.test.tsx gui/ConfigScreen/searchHighlight.integration.test.tsx

Coverage focus in this PR:

  1. Query matching (label / description / section title).
  2. Grouped all-matches results and section filtering behavior.
  3. Sidebar disabled state for non-matching sections.
  4. Search highlight rendering in sidebar and right panel.

Video

Demo_0407.mp4

AI Assistance Disclosure

  • AI was used to help draft and refine this PR description (wording, structure, and clarity).
  • AI was also used to summarize implementation status from working logs.
  • I reviewed all AI-assisted content, verified it against the actual branch changes, and I understand the submitted code and text.

Note

This PR supersedes #14781. The updated spec changes the interaction model enough that I restarted the implementation in a new branch to keep the diff and review history clearer.

@coderabbitai

coderabbitai Bot commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a settings search feature to the desktop Config screen: sidebar search input, search result grouping/filtering, highlighted matches, shared search utilities, SCSS styles, type tightening for settings, and related tests and integration tweaks.

Changes

Cohort / File(s) Summary
Search core & helpers
packages/app-desktop/gui/ConfigScreen/configSearch.ts, packages/lib/components/shared/config/config-search-text.ts, packages/lib/components/shared/config/config-search.test.ts
New search logic: query normalization, match predicates, result grouping, and mapping of groups back to matched sections. Exports new interfaces and shared helper functions used by both desktop and mobile.
Highlighting & tests
packages/app-desktop/gui/ConfigScreen/searchHighlight.tsx, packages/app-desktop/gui/ConfigScreen/searchHighlight.test.tsx
New text-highlighting utility for search matches and accompanying unit tests covering case-insensitivity, regex characters, empty inputs and rendered output assertions.
Config screen integration
packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx, packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx
Added search state (query, section filter), search-aware rendering and sidebar selection handling; mobile now uses shared shouldShowBySearch. Propagates highlight renderer into setting components.
Sidebar & SearchInput UI
packages/app-desktop/gui/ConfigScreen/Sidebar.tsx, packages/app-desktop/gui/lib/SearchInput/SearchInput.tsx, packages/app-desktop/gui/ConfigScreen/Sidebar/style.scss, packages/app-desktop/gui/lib/SearchInput/style.scss
Replaced styled-components with plain elements and class-based styles, added search input UI and handlers, updated keyboard navigation to skip non-matching sections, and applied label highlighting. SearchInput props narrowed/extended.
Setting controls updates
packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.tsx, packages/app-desktop/gui/ConfigScreen/controls/SettingLabel.tsx, packages/app-desktop/gui/ConfigScreen/controls/SettingDescription.tsx, packages/app-desktop/gui/ConfigScreen/controls/SettingHeader.tsx, packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.tsx
Threaded optional renderSearchText callback through setting components to allow highlighted rendering; changed SettingDescription.text to `string
Config styles
packages/app-desktop/gui/ConfigScreen/style.scss, packages/app-desktop/style.scss
Added/updated SCSS imports and styles for search results panel, sidebar and highlighting variables.
Type tightening for settings
packages/lib/components/shared/config/config-shared.ts
Replaced any with a new SettingsMap type and updated multiple function signatures to use it, plus refined section typing.
Tests & tooling
packages/app-desktop/integration-tests/wcag.spec.ts, packages/app-desktop/integration-tests/models/NoteList.ts, packages/app-desktop/playwright.config.ts
Improved WCAG violation diagnostics logging, added note-list selection wait in integration helper, and increased Playwright expect timeout on CI.
Ignore rules
.eslintignore, .gitignore
Added ignore entries for generated/compiled search-related desktop and shared config files.

Sequence Diagrams

sequenceDiagram
    actor User
    participant Sidebar as Sidebar
    participant ConfigScreen as ConfigScreen
    participant SearchEngine as Search Engine
    participant Renderer as Renderer

    User->>Sidebar: Type query / click search controls
    Sidebar->>ConfigScreen: onSearchQueryChange(query)
    ConfigScreen->>SearchEngine: searchResultGroups(query, sections, appType)
    SearchEngine-->>ConfigScreen: SearchResultGroup[]
    ConfigScreen->>ConfigScreen: Compute matchedSections & searchSectionFilter
    ConfigScreen->>Renderer: Render search results panel (with renderSearchText)
    Renderer->>Renderer: Highlight labels/descriptions via highlightSearchText
    Renderer-->>User: Display highlighted, optionally filtered results
Loading
sequenceDiagram
    participant ConfigScreen
    participant SettingComponent
    participant SettingLabel
    participant SettingDescription

    ConfigScreen->>SettingComponent: pass renderSearchText callback
    SettingComponent->>SettingLabel: render label via renderSearchText
    SettingComponent->>SettingDescription: render description via renderSearchText
    SettingLabel->>SettingLabel: return highlighted JSX
    SettingDescription->>SettingDescription: return highlighted JSX
Loading

Possibly related PRs

Suggested labels

enhancement, desktop, search

Suggested reviewers

  • personalizedrefrigerator
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding settings search to the desktop config screen, resolving a specific issue.
Description check ✅ Passed The description is directly related to the changeset, detailing the problem, solution, implementation specifics, test plan, and disclosure of AI assistance.
Linked Issues check ✅ Passed The PR implementation addresses all coding requirements from #14763: sidebar search field, multi-field search matching, grouped results display, disabled non-matching sections, section filtering, term highlighting, and plugin settings inclusion.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the settings search feature or supporting infrastructure (type tightening, test coverage, styling, accessibility fixes, and integration test improvements).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added enhancement Feature requests and code enhancements desktop All desktop platforms search labels Mar 18, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
packages/lib/components/shared/config/config-shared.ts (1)

69-74: Replace any types with a properly-typed SettingsMap alias to comply with TypeScript guidelines.

The settings: any parameter appears in both the SearchResultGroupsState interface and the matchedSearchSections function signature. Whilst the eslint-disable comments reference legacy code, this is an opportunity to introduce proper typing where the code is being actively modified.

♻️ Suggested typing cleanup
+type SettingsMap = Record<string, unknown>;
+
 interface SearchResultGroupsState {
 	device: AppType;
-	// eslint-disable-next-line `@typescript-eslint/no-explicit-any` -- Old code before rule was applied
-	settings: any;
+	settings: SettingsMap;
 	query: string;
 }
@@
 export const matchedSearchSections = (
 	device: AppType,
-	// eslint-disable-next-line `@typescript-eslint/no-explicit-any` -- Old code before rule was applied
-	settings: any,
+	settings: SettingsMap,
 	groups: SearchResultGroup[],
 ): MatchedSearchSection[] => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/components/shared/config/config-shared.ts` around lines 69 - 74,
Replace the loose any usage by introducing a SettingsMap alias and applying it
to the SearchResultGroupsState.settings property and the matchedSearchSections
function parameter: define SettingsMap (e.g., a record keyed by string or
AppType-specific keys to the proper settings shape) and change the type of
settings in the SearchResultGroupsState interface and the matchedSearchSections
signature from any to SettingsMap so callers and consumers
(SearchResultGroupsState, matchedSearchSections) get proper typing and avoid the
eslint-disable comment.
packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx (1)

524-528: Avoid duplicating highlight style construction.

markStyle duplicates the style logic already defined in renderSearchHighlightedText, which risks drift. Reuse the helper for section-title highlighting (or add the required duplication note if you intentionally keep both blocks).

♻️ Suggested simplification
-			const markStyle: React.CSSProperties = {
-				backgroundColor: theme.searchMarkerBackgroundColor,
-				color: theme.searchMarkerColor,
-				padding: 0,
-			};
-
 			const searchContent = filteredMatchedSections.map(({ section }) => {
 				const sectionComp = section.isScreen ? (
 					<div style={{ ...theme.textStyle, marginBottom: 20 }}>
 						{_('This section opens in its own screen and is matched by section title.')}
 					</div>
 				) : this.sectionToComponent(section.name, section, settings, true);
@@
-							{highlightSearchText(Setting.sectionNameToLabel(section.name), this.state.searchQuery, markStyle)}
+							{this.renderSearchHighlightedText(Setting.sectionNameToLabel(section.name))}
 						</h2>
 						{sectionComp}
 					</div>
 				);
 			});

As per coding guidelines, "When duplicating a substantial block of code, add a comment above it noting the duplication and referencing the original location".

Also applies to: 547-547

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx` around lines 524 -
528, The markStyle definition duplicates the highlight styling in
renderSearchHighlightedText; either reuse that helper there or add an explicit
duplication comment pointing to renderSearchHighlightedText to prevent
drift—modify the section-title rendering to call or derive styles from
renderSearchHighlightedText (or add a comment above the markStyle block
referencing renderSearchHighlightedText as the authoritative source) so both
places share the same style logic and stay in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx`:
- Around line 189-193: When searchMode is true the click handler only sets
selectedSectionName and searchSectionFilter, leaving screenName stale and
causing wrong content after exiting search; update the click path (where
selectedSectionName/searchSectionFilter are set) to also call
switchSection(sectionName) or explicitly set screenName to sectionName so screen
state stays in sync (ensure you reuse the existing switchSection(...) method
rather than duplicating logic and keep selectedSectionName, searchSectionFilter,
and screenName consistent).

In `@packages/app-desktop/gui/ConfigScreen/Sidebar.tsx`:
- Around line 115-116: The component currently determines search mode using the
raw props.searchQuery (e.g., isSearching = !!props.searchQuery), which treats
whitespace-only input as an active search; update all such checks (including the
occurrences around the isSearching computation and the other spots noted) to
trim the query before evaluating — e.g., use props.searchQuery?.trim().length >
0 (or equivalent) when computing isSearching and any conditional that toggles
the clear/search UI so that whitespace-only strings do not activate search mode.
- Around line 166-183: The component currently sets aria-selected and tabIndex
from selected even when isDisabled is true; compute an effectiveSelected (e.g.
const effectiveSelected = selected && !isDisabled) and use that for
aria-selected and tabIndex (aria-selected={effectiveSelected} and
tabIndex={effectiveSelected ? 0 : -1}) so disabled sections are not
keyboard-focusable or marked selected; update any usage of selected for these
attributes on StyledListItem (and keep href/aria-disabled logic unchanged).
- Around line 227-239: The search input (SearchInput inside
StyledSearchContainer) is currently a child of the element with role='tablist'
(StyledRoot) which violates ARIA tabs semantics; move the entire
StyledSearchContainer out of the StyledRoot so that StyledRoot contains only the
tab elements (buttons) and the SearchInput becomes a sibling element (before or
after StyledRoot) while preserving existing styling and props
(props.searchQuery, props.onSearchQueryChange, props.onSearchButtonClick, etc.);
update the JSX around StyledRoot and StyledSearchContainer to remove SearchInput
from inside StyledRoot and ensure no role or tabindex changes are inadvertently
applied to the SearchInput or its container.

---

Nitpick comments:
In `@packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx`:
- Around line 524-528: The markStyle definition duplicates the highlight styling
in renderSearchHighlightedText; either reuse that helper there or add an
explicit duplication comment pointing to renderSearchHighlightedText to prevent
drift—modify the section-title rendering to call or derive styles from
renderSearchHighlightedText (or add a comment above the markStyle block
referencing renderSearchHighlightedText as the authoritative source) so both
places share the same style logic and stay in sync.

In `@packages/lib/components/shared/config/config-shared.ts`:
- Around line 69-74: Replace the loose any usage by introducing a SettingsMap
alias and applying it to the SearchResultGroupsState.settings property and the
matchedSearchSections function parameter: define SettingsMap (e.g., a record
keyed by string or AppType-specific keys to the proper settings shape) and
change the type of settings in the SearchResultGroupsState interface and the
matchedSearchSections signature from any to SettingsMap so callers and consumers
(SearchResultGroupsState, matchedSearchSections) get proper typing and avoid the
eslint-disable comment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 141694da-6399-4863-b6bb-2d4a77d1474b

📥 Commits

Reviewing files that changed from the base of the PR and between 2dcb537 and bab6b59.

📒 Files selected for processing (9)
  • .eslintignore
  • .gitignore
  • packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
  • packages/app-desktop/gui/ConfigScreen/Sidebar.tsx
  • packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.tsx
  • packages/app-desktop/gui/ConfigScreen/controls/SettingDescription.tsx
  • packages/app-desktop/gui/ConfigScreen/controls/SettingLabel.tsx
  • packages/app-desktop/gui/ConfigScreen/searchHighlight.tsx
  • packages/lib/components/shared/config/config-shared.ts

Comment thread packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
Comment thread packages/app-desktop/gui/ConfigScreen/Sidebar.tsx Outdated
Comment thread packages/app-desktop/gui/ConfigScreen/Sidebar.tsx Outdated
Comment thread packages/app-desktop/gui/ConfigScreen/Sidebar.tsx Outdated

@personalizedrefrigerator personalizedrefrigerator left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this! I've left a few comments.

Comment thread packages/app-desktop/gui/ConfigScreen/searchHighlight.tsx Outdated
Comment thread packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx Outdated
Comment thread packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx Outdated
Comment thread packages/app-desktop/gui/ConfigScreen/Sidebar.tsx Outdated
Comment thread packages/lib/components/shared/config/config-shared.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
packages/app-desktop/gui/ConfigScreen/Sidebar.tsx (3)

166-186: ⚠️ Potential issue | 🟠 Major

Disabled sections should not remain selected or focusable.

At Line 180 and Line 182, disabled tabs can still be marked selected and keep tabIndex={0}, which conflicts with “disabled for keyboard interaction”.

♿ Suggested accessibility fix
 		const selected = props.selection === section.name;
 		const isSearching = !!props.searchQuery;
 		const hasMatch = matchedSectionNames.has(section.name);
 		const isDisabled = isSearching && !hasMatch;
+		const isActiveTab = selected && !isDisabled;
@@
-				aria-selected={selected}
+				aria-selected={isActiveTab}
 				aria-disabled={isDisabled}
-				tabIndex={selected ? 0 : -1}
+				tabIndex={isActiveTab ? 0 : -1}
@@
-				selected={selected}
+				selected={isActiveTab}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/gui/ConfigScreen/Sidebar.tsx` around lines 166 - 186,
The tab for a disabled section (isDisabled) must not appear selected or be
keyboard-focusable: update the return props on StyledListItem (used in
Sidebar.tsx) so that when isDisabled is true you force selected to false
(override the computed selected), set tabIndex to -1, and ensure aria-selected
is false; keep aria-disabled true. Adjust the values derived from selected and
isDisabled (the selected variable and tabIndex assignment near
Setting.isSubSection and buttonRefs usage) so disabled tabs cannot be focused or
announced as selected.

227-239: ⚠️ Potential issue | 🟠 Major

Keep tablist ownership semantic: move search input outside the tablist.

SearchInput is currently rendered as a child of the role='tablist' container. This should be a sibling container so the tablist owns tabs only.

According to the WAI-ARIA Tabs Pattern, should an element with `role="tablist"` contain only `role="tab"` owned elements, or can it include a text input used for search?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/gui/ConfigScreen/Sidebar.tsx` around lines 227 - 239,
The SearchInput is currently placed inside the element using role='tablist'
(StyledRoot), violating the ARIA tabs pattern; move the StyledSearchContainer
(and its SearchInput) out of StyledRoot so the tablist contains only tab
elements (the buttons variable). Update the JSX so StyledSearchContainer is
rendered as a sibling to StyledRoot (e.g., place it immediately before
StyledRoot) and leave StyledRoot with role='tablist' containing only the
tabs/buttons; keep props and handlers (props.searchQuery,
props.onSearchQueryChange, props.onSearchButtonClick) unchanged.

115-116: ⚠️ Potential issue | 🟠 Major

Use trimmed query when toggling search mode.

Line 115, Line 167, and Line 234 treat whitespace-only input as active search, which can disable sections and show “search started” state incorrectly.

🧭 Suggested consistency fix
 export default function Sidebar(props: Props) {
 	const buttonRefs = useRef<HTMLElement[]>([]);
+	const isSearching = props.searchQuery.trim().length > 0;
@@
-		const isSearching = !!props.searchQuery;
@@
-		const isSearching = !!props.searchQuery;
@@
-					searchStarted={!!props.searchQuery}
+					searchStarted={isSearching}

Also applies to: 167-170, 229-235

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/gui/ConfigScreen/Sidebar.tsx` around lines 115 - 116,
The current checks use props.searchQuery directly which treats whitespace-only
input as an active search; compute a trimmed query once (e.g., const
trimmedQuery = (props.searchQuery || "").trim()) and replace uses of
props.searchQuery when determining isSearching and any toggles/conditional UI
(replace isSearching = !!props.searchQuery with isSearching =
trimmedQuery.length > 0 and use trimmedQuery for the "search started" state and
section-disable logic) so whitespace-only queries are treated as empty.
packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx (1)

189-197: ⚠️ Potential issue | 🟠 Major

Keep screenName in sync when selecting sections during search mode.

In the search-mode branch, only selectedSectionName/searchSectionFilter are updated. If the selected section is screen-backed, clearing search can leave screenName stale.

🛠️ Suggested state-sync fix
 	if (searchMode) {
+		const section = this.sectionByName(sectionName);
 		this.setState({
 			selectedSectionName: sectionName,
+			screenName: section.isScreen ? section.name : '',
 			searchSectionFilter: sectionName,
 		});
 		return;
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx` around lines 189 -
197, When handling a section selection while searchMode is true, also keep
screenName in sync: inside the search-mode branch where you call this.setState({
selectedSectionName, searchSectionFilter }), include logic to set screenName to
sectionName if the chosen section is screen-backed (otherwise clear it, e.g.
""), so that later clearing the search doesn't leave a stale screenName; use the
same detection used elsewhere (the same check used by switchSection or the
section metadata) and update screenName in the same setState call.
🧹 Nitpick comments (2)
packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx (1)

378-383: Use theme-defined highlight colours instead of hard-coded RGB values.

Both highlight style blocks hard-code colour values, which makes theme consistency harder to maintain.

As per coding guidelines: "For GUI style changes in Desktop and mobile clients, refer to packages/lib/theme.ts for all styling information".

Also applies to: 526-530

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx` around lines 378 -
383, The highlight styles in renderSearchHighlightedText (which calls
highlightSearchText with hard-coded RGB strings) must be replaced with
theme-defined colour tokens from packages/lib/theme.ts; import the appropriate
exports from that module and pass them instead of the literal 'rgb(...)' values
(also update the similar hard-coded block used elsewhere where
highlightSearchText is called), ensuring you use the exported theme variable
names (e.g., the module's highlight background/foreground constants) so the
component uses theme-defined colours tied to state.searchQuery and
highlightSearchText.
packages/app-desktop/gui/ConfigScreen/Sidebar.test.js (1)

70-113: Consolidate repeated search test setup with helpers or test.each.

There’s repeated searchResults construction and render setup across multiple tests. This is a good candidate for a shared helper or parameterised cases.

Based on learnings: "Applies to **/*.{test,spec}.{ts,tsx,js,jsx} : Avoid duplicating code in tests; use test.each or shared helpers instead of repeating similar test blocks".

Also applies to: 153-185

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/gui/ConfigScreen/Sidebar.test.js` around lines 70 - 113,
Extract the repeated search-setup into a small helper and/or use test.each to
parameterize cases: create a helper (e.g., renderWithSearch) that accepts
{searchQuery, searchResultGroups, onSelectionChange} and calls
render(React.createElement(Sidebar.default, {...defaultProps, ...opts})), then
replace duplicated setups in tests "should disable sections without search
matches", "should prevent clicking disabled sections", "should allow clicking
enabled sections during search", and "should restore enabled state when search
is cleared" to call renderWithSearch or a test.each table that passes different
searchResultGroups and expectations; reference the existing symbols
Sidebar.default, defaultProps, searchResultGroups, searchQuery,
onSelectionChange and update the tests to use the helper or parameterized inputs
to remove repeated construction and render calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app-desktop/gui/ConfigScreen/Sidebar.test.js`:
- Around line 115-126: The test "should skip disabled sections during keyboard
navigation" currently only checks that onSelectionChange was called; change it
to assert the actual target selection to ensure it navigated to the Appearance
tab. In the test in Sidebar.test.js, after firing ArrowDown on the generalTab,
assert that the mock onSelectionChange received the selection for 'appearance'
(or that the component selection prop/state equals 'appearance') rather than
only using toHaveBeenCalled(); reference the onSelectionChange mock and the
Sidebar component used in the render call to locate where to change the
expectation.

---

Duplicate comments:
In `@packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx`:
- Around line 189-197: When handling a section selection while searchMode is
true, also keep screenName in sync: inside the search-mode branch where you call
this.setState({ selectedSectionName, searchSectionFilter }), include logic to
set screenName to sectionName if the chosen section is screen-backed (otherwise
clear it, e.g. ""), so that later clearing the search doesn't leave a stale
screenName; use the same detection used elsewhere (the same check used by
switchSection or the section metadata) and update screenName in the same
setState call.

In `@packages/app-desktop/gui/ConfigScreen/Sidebar.tsx`:
- Around line 166-186: The tab for a disabled section (isDisabled) must not
appear selected or be keyboard-focusable: update the return props on
StyledListItem (used in Sidebar.tsx) so that when isDisabled is true you force
selected to false (override the computed selected), set tabIndex to -1, and
ensure aria-selected is false; keep aria-disabled true. Adjust the values
derived from selected and isDisabled (the selected variable and tabIndex
assignment near Setting.isSubSection and buttonRefs usage) so disabled tabs
cannot be focused or announced as selected.
- Around line 227-239: The SearchInput is currently placed inside the element
using role='tablist' (StyledRoot), violating the ARIA tabs pattern; move the
StyledSearchContainer (and its SearchInput) out of StyledRoot so the tablist
contains only tab elements (the buttons variable). Update the JSX so
StyledSearchContainer is rendered as a sibling to StyledRoot (e.g., place it
immediately before StyledRoot) and leave StyledRoot with role='tablist'
containing only the tabs/buttons; keep props and handlers (props.searchQuery,
props.onSearchQueryChange, props.onSearchButtonClick) unchanged.
- Around line 115-116: The current checks use props.searchQuery directly which
treats whitespace-only input as an active search; compute a trimmed query once
(e.g., const trimmedQuery = (props.searchQuery || "").trim()) and replace uses
of props.searchQuery when determining isSearching and any toggles/conditional UI
(replace isSearching = !!props.searchQuery with isSearching =
trimmedQuery.length > 0 and use trimmedQuery for the "search started" state and
section-disable logic) so whitespace-only queries are treated as empty.

---

Nitpick comments:
In `@packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx`:
- Around line 378-383: The highlight styles in renderSearchHighlightedText
(which calls highlightSearchText with hard-coded RGB strings) must be replaced
with theme-defined colour tokens from packages/lib/theme.ts; import the
appropriate exports from that module and pass them instead of the literal
'rgb(...)' values (also update the similar hard-coded block used elsewhere where
highlightSearchText is called), ensuring you use the exported theme variable
names (e.g., the module's highlight background/foreground constants) so the
component uses theme-defined colours tied to state.searchQuery and
highlightSearchText.

In `@packages/app-desktop/gui/ConfigScreen/Sidebar.test.js`:
- Around line 70-113: Extract the repeated search-setup into a small helper
and/or use test.each to parameterize cases: create a helper (e.g.,
renderWithSearch) that accepts {searchQuery, searchResultGroups,
onSelectionChange} and calls render(React.createElement(Sidebar.default,
{...defaultProps, ...opts})), then replace duplicated setups in tests "should
disable sections without search matches", "should prevent clicking disabled
sections", "should allow clicking enabled sections during search", and "should
restore enabled state when search is cleared" to call renderWithSearch or a
test.each table that passes different searchResultGroups and expectations;
reference the existing symbols Sidebar.default, defaultProps,
searchResultGroups, searchQuery, onSelectionChange and update the tests to use
the helper or parameterized inputs to remove repeated construction and render
calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e6762ee5-841d-45f2-a44f-3318fcf03c25

📥 Commits

Reviewing files that changed from the base of the PR and between bab6b59 and 7d0b190.

📒 Files selected for processing (8)
  • .eslintignore
  • .gitignore
  • packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
  • packages/app-desktop/gui/ConfigScreen/Sidebar.test.js
  • packages/app-desktop/gui/ConfigScreen/Sidebar.tsx
  • packages/app-desktop/gui/ConfigScreen/searchHighlight.integration.test.tsx
  • packages/app-desktop/gui/ConfigScreen/searchHighlight.test.tsx
  • packages/lib/components/shared/config/config-search.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/lib/components/shared/config/config-search.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • .gitignore
  • .eslintignore

Comment thread packages/app-desktop/gui/ConfigScreen/Sidebar.test.js Outdated
@slimuCS

slimuCS commented Mar 18, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for working on this! I've left a few comments.

Hi personalizedrefrigerator,

I’ll update the implementation accordingly later.

thanks for the detailed review!

@slimuCS

slimuCS commented Mar 18, 2026

Copy link
Copy Markdown
Contributor Author

I want to confirm the scope for search indexing in screen-based sections (Encryption / Web Clipper / Keyboard Shortcuts).

In the current implementation, these sections can be matched by section title, but their internal screen controls are not fully indexed through the same metadata path as regular settings. Should we treat this as acceptable for this issue, or should we also index screen-internal fields so they can be matched by query?

@personalizedrefrigerator

Copy link
Copy Markdown
Collaborator

Should we treat this as acceptable for this issue, or should we also index screen-internal fields so they can be matched by query?

For full-screen sections that lack searchable metadata: Unless indexing screen-internal text can be done without significantly increasing the size and complexity of this pull request, I would suggest only searching the section header for now.

@laurent22 laurent22 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the implementation. Please address our comments, and also CodeRabbit's comments

Comment thread packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx Outdated
Comment thread packages/app-desktop/gui/ConfigScreen/searchHighlight.integration.test.tsx Outdated
Comment thread packages/app-desktop/gui/ConfigScreen/searchHighlight.integration.test.tsx Outdated
Comment thread packages/app-desktop/gui/ConfigScreen/searchHighlight.integration.test.tsx Outdated
Comment thread packages/app-desktop/gui/ConfigScreen/Sidebar.test.js Outdated
Comment thread packages/app-desktop/gui/ConfigScreen/Sidebar.tsx Outdated
Comment thread packages/app-desktop/gui/ConfigScreen/Sidebar.tsx Outdated
Comment thread packages/lib/components/shared/config/config-search.test.ts Outdated
Comment thread packages/lib/components/shared/config/config-search.test.ts
Comment thread packages/lib/components/shared/config/config-shared.ts Outdated
@personalizedrefrigerator

personalizedrefrigerator commented Mar 19, 2026

Copy link
Copy Markdown
Collaborator

I've added a new test that should help auto-detect certain accessibility issues. I'm merging upstream changes into this pull request so that the test runs in CI.

@personalizedrefrigerator

Copy link
Copy Markdown
Collaborator

I've added a new test that should help auto-detect certain accessibility issues. I'm merging upstream changes into this pull request so that the test runs in CI.

The accessibility checker has flagged a few potential issues:

  • Contrast issues:
  • ARIA issues:
    • Elements with role="tablist" cannot contain buttons or inputs.
      • Based on the spec, tablist should only contain elements with the tab role.
Details
+ Array [
+   Object {
+     "description": "Ensure elements with an ARIA role that require child roles contain them",
+     "help": "Certain ARIA roles must contain particular children",
+     "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/aria-required-children?application=playwright",
+     "id": "aria-required-children",
+     "impact": "critical",
+     "nodes": Array [
+       Object {
+         "all": Array [],
+         "any": Array [
+           Object {
+             "data": Object {
+               "messageKey": "unallowed",
+               "values": "input[tabindex], button[aria-label]",
+             },
+             "id": "aria-required-children",
+             "impact": "critical",
+             "message": "Element has children which are not allowed: input[tabindex], button[aria-label]",
+             "relatedNodes": Array [
+               Object {
+                 "html": "<input placeholder=\"Search settings...\" spellcheck=\"false\" class=\"sc-jsJBEP sc-iHGNWf cGZLNz dPRrXf\" type=\"search\" value=\"\">",
+                 "target": Array [
+                   "input[placeholder=\"Search settings...\"]",
+                 ],
+               },
+               Object {
+                 "html": "<button aria-label=\"Search\" class=\"sc-koXPp jxDRVO\"><span class=\"sc-bmzYkS hQFofX icon-search\"></span></button>",
+                 "target": Array [
+                   ".sc-fXSgeo > .sc-eeDRCY.kILkiy > .sc-koXPp.jxDRVO[aria-label=\"Search\"]",
+                 ],
+               },
+             ],
+           },
+         ],
+         "failureSummary": "Fix any of the following:
+   Element has children which are not allowed: input[tabindex], button[aria-label]",
+         "html": "<div class=\"sc-esYiGF ffDzkK settings-sidebar _scrollbar2\" role=\"tablist\">",
+         "impact": "critical",
+         "none": Array [],
+         "target": Array [
+           ".sc-esYiGF",
+         ],
+       },
+     ],
+     "tags": Array [
+       "cat.aria",
+       "wcag2a",
+       "wcag131",
+       "EN-301-549",
+       "EN-9.1.3.1",
+       "RGAAv4",
+       "RGAA-9.3.1",
+     ],
+   },
+   Object {
+     "description": "Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds",
+     "help": "Elements must meet minimum color contrast ratio thresholds",
+     "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/color-contrast?application=playwright",
+     "id": "color-contrast",
+     "impact": "serious",
+     "nodes": Array [
+       Object {
+         "all": Array [],
+         "any": Array [
+           Object {
+             "data": Object {
+               "bgColor": "#989ba0",
+               "colorParse": undefined,
+               "contrastRatio": 4.29,
+               "expectedContrastRatio": "4.5:1",
+               "fgColor": "#32373f",
+               "fontSize": "9.0pt (12px)",
+               "fontWeight": "normal",
+               "messageKey": null,
+               "shadowColor": undefined,
+             },
+             "id": "color-contrast",
+             "impact": "serious",
+             "message": "Element has insufficient color contrast of 4.29 (foreground color: #32373f, background color: #989ba0, font size: 9.0pt (12px), font weight: normal). Expected contrast ratio of 4.5:1",
+             "relatedNodes": Array [
+               Object {
+                 "html": "<input placeholder=\"Search settings...\" spellcheck=\"false\" class=\"sc-jsJBEP sc-iHGNWf cGZLNz dPRrXf\" type=\"search\" value=\"\">",
+                 "target": Array [
+                   "input[placeholder=\"Search settings...\"]",
+                 ],
+               },
+               Object {
+                 "html": "<div class=\"sc-esYiGF ffDzkK settings-sidebar _scrollbar2\" role=\"tablist\">",
+                 "target": Array [
+                   ".sc-esYiGF",
+                 ],
+               },
+             ],
+           },
+         ],
+         "failureSummary": "Fix any of the following:
+   Element has insufficient color contrast of 4.29 (foreground color: #32373f, background color: #989ba0, font size: 9.0pt (12px), font weight: normal). Expected contrast ratio of 4.5:1",
+         "html": "<input placeholder=\"Search settings...\" spellcheck=\"false\" class=\"sc-jsJBEP sc-iHGNWf cGZLNz dPRrXf\" type=\"search\" value=\"\">",
+         "impact": "serious",
+         "none": Array [],
+         "target": Array [
+           "input[placeholder=\"Search settings...\"]",
+         ],
+       },
+       Object {
+         "all": Array [],
+         "any": Array [
+           Object {
+             "data": Object {
+               "bgColor": "#26292f",
+               "colorParse": undefined,
+               "contrastRatio": 3.78,
+               "expectedContrastRatio": "4.5:1",
+               "fgColor": "#7f8289",
+               "fontSize": "9.0pt (12px)",
+               "fontWeight": "normal",
+               "messageKey": null,
+               "shadowColor": undefined,
+             },
+             "id": "color-contrast",
+             "impact": "serious",
+             "message": "Element has insufficient color contrast of 3.78 (foreground color: #7f8289, background color: #26292f, font size: 9.0pt (12px), font weight: normal). Expected contrast ratio of 4.5:1",
+             "relatedNodes": Array [
+               Object {
+                 "html": "<div class=\"sc-fjvvzt jcdFkQ\">Plugins</div>",
+                 "target": Array [
+                   ".sc-fjvvzt",
+                 ],
+               },
+               Object {
+                 "html": "<div class=\"sc-esYiGF ffDzkK settings-sidebar _scrollbar2\" role=\"tablist\">",
+                 "target": Array [
+                   ".sc-esYiGF",
+                 ],
+               },
+             ],
+           },
+         ],
+         "failureSummary": "Fix any of the following:
+   Element has insufficient color contrast of 3.78 (foreground color: #7f8289, background color: #26292f, font size: 9.0pt (12px), font weight: normal). Expected contrast ratio of 4.5:1",
+         "html": "<div class=\"sc-fjvvzt jcdFkQ\">Plugins</div>",
+         "impact": "serious",
+         "none": Array [],
+         "target": Array [
+           ".sc-fjvvzt",
+         ],
+       },
+     ],
+     "tags": Array [
+       "cat.color",
+       "wcag2aa",
+       "wcag143",
+       "TTv5",
+       "TT13.c",
+       "EN-301-549",
+       "EN-9.1.4.3",
+       "ACT",
+       "RGAAv4",
+       "RGAA-3.2.1",
+     ],
+   },
+ ]

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
packages/app-desktop/gui/ConfigScreen/Sidebar.tsx (2)

221-231: ⚠️ Potential issue | 🟠 Major

Keep the search box outside the tablist.

StyledRoot is exposed as role='tablist', so nesting SearchInput under it breaks the tablist ownership rules and matches the ARIA violation already reported for this PR. The search container needs to be a sibling wrapper, not a child of the tablist.

♿ One way to restructure this
+export const StyledSidebar = styled.div`
+	display: flex;
+	flex-direction: column;
+	background-color: ${(props: StyleProps) => props.theme.backgroundColor2};
+`;
+
 export default function Sidebar(props: Props) {
 	...
 	return (
-		<StyledRoot className='settings-sidebar _scrollbar2' role='tablist'>
-			<StyledSearchContainer>
-				<SearchInput
-					inputRef={null}
-					value={props.searchQuery}
-					onChange={props.onSearchQueryChange}
-					onSearchButtonClick={props.onSearchButtonClick}
-					searchStarted={isSearching}
-					placeholder={_('Search settings...')}
-				/>
-			</StyledSearchContainer>
-			{buttons}
-		</StyledRoot>
+		<StyledSidebar className='settings-sidebar'>
+			<StyledSearchContainer>
+				<SearchInput
+					inputRef={null}
+					value={props.searchQuery}
+					onChange={props.onSearchQueryChange}
+					onSearchButtonClick={props.onSearchButtonClick}
+					searchStarted={isSearching}
+					placeholder={_('Search settings...')}
+				/>
+			</StyledSearchContainer>
+			<StyledRoot className='_scrollbar2' role='tablist'>
+				{buttons}
+			</StyledRoot>
+		</StyledSidebar>
 	);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/gui/ConfigScreen/Sidebar.tsx` around lines 221 - 231,
The search input is currently nested inside StyledRoot which is rendered with
role='tablist', violating ARIA tablist ownership; move the StyledSearchContainer
(the wrapper for SearchInput) out of the StyledRoot so it becomes a sibling
wrapper before or after StyledRoot rather than a child, ensuring StyledRoot only
contains tab elements. Update the JSX that renders StyledRoot,
StyledSearchContainer and SearchInput so that SearchInput is not a descendant of
the element with role='tablist' (reference: StyledRoot, StyledSearchContainer,
SearchInput, role='tablist').

161-185: ⚠️ Potential issue | 🟠 Major

Don’t leave a disabled section in the roving tab stop.

If the active section stops matching, this still renders it as selected with tabIndex={0}. That keeps a greyed-out tab focusable and conflicts with the new “disabled for keyboard interaction” behaviour. Please derive an effectiveSelected value from selected && !isDisabled and use that for the selected state, tab index, and selected styling.

♿ Suggested guard
 	function renderButton(section: SettingMetadataSection, index: number) {
 		const selected = props.selection === section.name;
 		const hasMatch = matchedSectionNames.has(section.name);
 		const isDisabled = isSearching && !hasMatch;
+		const effectiveSelected = selected && !isDisabled;
 
 		return (
 			<StyledListItem
 				key={section.name}
 				href={isDisabled ? undefined : '#'}
 				role='tab'
 				ref={(item: HTMLElement) => { buttonRefs.current[index] = item; }}
 
 				id={`setting-tab-${section.name}`}
 				aria-controls={`setting-section-${section.name}`}
-				aria-selected={selected}
+				aria-selected={effectiveSelected}
 				aria-disabled={isDisabled}
-				tabIndex={selected ? 0 : -1}
+				tabIndex={effectiveSelected ? 0 : -1}
 
 				isSubSection={Setting.isSubSection(section.name)}
-				selected={selected}
+				selected={effectiveSelected}
 				disabled={isDisabled}
 				onClick={() => {
 					if (isDisabled) return;
 					props.onSelectionChange({ section: section });
 				}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/gui/ConfigScreen/Sidebar.tsx` around lines 161 - 185,
The current logic keeps a disabled section focusable because selected is set
from props.selection even when isDisabled is true; derive an effectiveSelected =
selected && !isDisabled and use that instead of selected for the
accessible/interaction state: replace usages of selected in aria-selected,
tabIndex, the StyledListItem selected prop, and any roving-tabstop/ref focus
logic (e.g., where buttonRefs/current index or onKeyDown depend on selection) so
a section that is disabled (isSearching &&
!matchedSectionNames.has(section.name)) is not treated as focusable/selected;
keep props.selection and isDisabled calculations as-is but switch UI/ARIA
consumers to effectiveSelected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app-desktop/gui/ConfigScreen/Sidebar.tsx`:
- Line 72: The "Plugins" divider in Sidebar.tsx is being dimmed by applying
opacity: 0.38 to its container, which reduces text contrast; remove that opacity
on the divider/container (the element rendering the "Plugins" heading in the
Sidebar component) and instead apply a dedicated colour token for a visually
quieter state (e.g., use an existing neutral/disabled text token or add a new
token) to the divider/heading text so the element retains full opacity while
meeting contrast requirements.

---

Duplicate comments:
In `@packages/app-desktop/gui/ConfigScreen/Sidebar.tsx`:
- Around line 221-231: The search input is currently nested inside StyledRoot
which is rendered with role='tablist', violating ARIA tablist ownership; move
the StyledSearchContainer (the wrapper for SearchInput) out of the StyledRoot so
it becomes a sibling wrapper before or after StyledRoot rather than a child,
ensuring StyledRoot only contains tab elements. Update the JSX that renders
StyledRoot, StyledSearchContainer and SearchInput so that SearchInput is not a
descendant of the element with role='tablist' (reference: StyledRoot,
StyledSearchContainer, SearchInput, role='tablist').
- Around line 161-185: The current logic keeps a disabled section focusable
because selected is set from props.selection even when isDisabled is true;
derive an effectiveSelected = selected && !isDisabled and use that instead of
selected for the accessible/interaction state: replace usages of selected in
aria-selected, tabIndex, the StyledListItem selected prop, and any
roving-tabstop/ref focus logic (e.g., where buttonRefs/current index or
onKeyDown depend on selection) so a section that is disabled (isSearching &&
!matchedSectionNames.has(section.name)) is not treated as focusable/selected;
keep props.selection and isDisabled calculations as-is but switch UI/ARIA
consumers to effectiveSelected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9829fc8e-4dd9-4669-9b32-84e8d349ac3c

📥 Commits

Reviewing files that changed from the base of the PR and between ee0bae2 and 413813f.

📒 Files selected for processing (8)
  • .eslintignore
  • .gitignore
  • packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
  • packages/app-desktop/gui/ConfigScreen/Sidebar.tsx
  • packages/app-desktop/gui/ConfigScreen/searchHighlight.test.tsx
  • packages/app-desktop/gui/ConfigScreen/searchHighlight.tsx
  • packages/lib/components/shared/config/config-search.test.ts
  • packages/lib/components/shared/config/config-shared.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/app-desktop/gui/ConfigScreen/searchHighlight.tsx
  • packages/lib/components/shared/config/config-search.test.ts
  • packages/app-desktop/gui/ConfigScreen/searchHighlight.test.tsx
  • packages/lib/components/shared/config/config-shared.ts
  • .eslintignore
  • .gitignore

Comment thread packages/app-desktop/gui/ConfigScreen/Sidebar.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx (1)

525-547: Screen-only sections display a placeholder message in search results.

Consider whether linking to or automatically navigating to the screen section would provide a better UX, so users don't have to manually click the sidebar. This is optional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx` around lines 525 -
547, Screen-only sections currently render a static placeholder when matched in
search (handled in filteredMatchedSections mapping when section.isScreen);
replace that placeholder with an interactive element (link/button) that
navigates to or opens the screen section so users don't have to use the sidebar.
Implement a small handler (e.g., openScreenSection(sectionName)) on the
component and wire the interactive element's onClick to it; keep the existing
icon/title rendering via Setting.sectionNameToIcon and
renderSearchHighlightedText(Setting.sectionNameToLabel(section.name)), and fall
back to the current placeholder only if navigation cannot be performed. Ensure
the new handler uses your app's existing navigation/sidebar API to show the
screen for the given section name.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx`:
- Around line 525-547: Screen-only sections currently render a static
placeholder when matched in search (handled in filteredMatchedSections mapping
when section.isScreen); replace that placeholder with an interactive element
(link/button) that navigates to or opens the screen section so users don't have
to use the sidebar. Implement a small handler (e.g.,
openScreenSection(sectionName)) on the component and wire the interactive
element's onClick to it; keep the existing icon/title rendering via
Setting.sectionNameToIcon and
renderSearchHighlightedText(Setting.sectionNameToLabel(section.name)), and fall
back to the current placeholder only if navigation cannot be performed. Ensure
the new handler uses your app's existing navigation/sidebar API to show the
screen for the given section name.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3b5d1907-2e81-439f-a27c-487fd8f827b3

📥 Commits

Reviewing files that changed from the base of the PR and between 413813f and 7cff948.

📒 Files selected for processing (2)
  • packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
  • packages/app-desktop/gui/ConfigScreen/Sidebar.tsx

@slimuCS

slimuCS commented Mar 19, 2026

Copy link
Copy Markdown
Contributor Author

I've added a new test that should help auto-detect certain accessibility issues. I'm merging upstream changes into this pull request so that the test runs in CI.

The accessibility checker has flagged a few potential issues:

Hi personalizedrefrigerator, thanks so much for the detailed accessibility feedback!

I’ve applied the fixes by improving the search placeholder contrast for WCAG readability, removing opacity-based dimming on the Plugins divider to preserve active text contrast, and moving SearchInput outside the tablist while marking the Plugins divider as presentational so tablist semantics remain correct.

@coderabbitai coderabbitai Bot added enhancement Feature requests and code enhancements desktop All desktop platforms search labels Apr 9, 2026
@slimuCS

slimuCS commented Apr 9, 2026

Copy link
Copy Markdown
Contributor Author

By the way, I’ve also uploaded a new demo video showing the latest behavior. If there’s anything else you’d like me to adjust, please feel free to let me know

@coderabbitai coderabbitai Bot added plugins Anything related to Joplin's plugin system accessibility Related to accessibility and removed enhancement Feature requests and code enhancements desktop All desktop platforms search labels Apr 16, 2026
@laurent22

Copy link
Copy Markdown
Owner

Claude gives this as a review. I didn't check all of it but I see at least 1 and 3 appear to be correct:

Critical / Bugs

1. Crash on initial render — searchQuery not initialized in state

In ConfigScreen.tsx, the constructor never initializes searchQuery in state. On first render, normalizeQuery(this.state.searchQuery) is called, which runs undefined.trim() and throws a TypeError. The state needs searchQuery: '' and searchSectionFilter: null in the constructor (or from defaultScreenState).

2. Mobile: all settings disappear when search is opened with empty query

In the mobile ConfigScreen.tsx, when searching is true but searchQuery is empty, shouldShowBySearch() returns false for everything, causing all settings to be hidden. The condition should also check hasNormalizedQuery(this.state.searchQuery) before applying the search filter.

3. Possible regression — control_GlobalHotkeyInput removed

In SettingComponent.tsx, the import and registration of control_GlobalHotkeyInput appears to have been removed. This is likely an accidental regression from rebasing — the global hotkey setting will no longer render with its custom control.

4. Keyboard navigation can land on disabled sections

In Sidebar.tsx, when searching with no matched sections, the arrow-key skip loop cycles through all sections and breaks at the initial index (a non-matched section), then calls onSelectionChange with a disabled section. Should be a no-op when no sections match.


Medium — Accessibility / Correctness

5. aria-controls references non-existent panels in search mode

Sidebar buttons have aria-controls="setting-section-${section.name}", but in search mode the panel is a single setting-section-search-results div. The referenced IDs don't exist in the DOM.

6. Mobile/Desktop search behavior inconsistency

Mobile uses equalsNormalizedQuery (exact match) for section titles, while desktop uses includesNormalizedQuery (substring match). Typing "Gen" will match the "General" section on desktop but not on mobile. This should be intentional or aligned.

7. Inconsistent empty-query semantics

shouldShowBySearch returns false for empty queries while isMetadataMatched returns true. They serve different callers but this inconsistency is a maintenance risk.


Low — Style / Minor

8. .js extension in import

The new import from '@joplin/lib/components/shared/config/config-search-text.js' uses .js extension — verify this is consistent with the project convention (other imports in the same file don't use extensions).

9. Triple normalization of query

searchResultGroupsisMetadataMatchedincludesNormalizedQuery each call normalizeQuery(). Idempotent so not a bug, but wasteful and confusing. Normalize once and pass the normalized value.

10. SettingHeader changed <label> to <span>

Unrelated to the search feature — should be called out or split into a separate commit.

11. Test coverage gaps

  • No test for shouldShowBySearch with array relatedText
  • No test for toSearchText(undefined) (only null tested)
  • Two highlight tests overlap significantly — could consolidate

12. Missing searchSectionFilter in mobile ConfigScreenState interface

The shared defaultScreenState now includes searchSectionFilter, but the mobile TypeScript interface doesn't declare it — runtime works but the type is incomplete.


The most urgent items are #1 (crash on mount), #2 (blank screen on mobile), and #3 (regression removing the global hotkey control). The rest are worth addressing but less critical.

@coderabbitai coderabbitai Bot added enhancement Feature requests and code enhancements desktop All desktop platforms search and removed plugins Anything related to Joplin's plugin system accessibility Related to accessibility labels Apr 21, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx (1)

516-523: ⚠️ Potential issue | 🟠 Major

Search filters are still exposed as tabs for one shared results region.

This mounts a single role='region' for search results, but the sidebar still receives a selected section and renders enabled section buttons as tabs targeting that same shared panel. Either render search-mode sidebar entries as filter buttons, or mount real per-section tabpanels during search.

Also applies to: 581-588

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx` around lines 516 -
523, The search UI currently mounts a single shared results panel (the element
with id 'setting-section-search-results' pushed into tabComponents) while the
sidebar still renders section entries as tabs targeting that shared panel;
update ConfigScreen.tsx so that in search mode the sidebar renders those entries
as filter buttons or, alternatively, create distinct tabpanel elements per
section instead of the single role='region' panel. Locate the code that
constructs tabComponents (the push of the div with id
'setting-section-search-results') and the sidebar rendering logic that uses the
selected section, then either switch the sidebar entries to non-tab controls
when isSearchMode is true or change the tabComponents construction to produce
separate panels for each enabled section so each sidebar tab targets its own
tabpanel. Ensure ARIA roles are consistent: filter buttons should not be
role='tab' and per-section panels must retain role='region' (or role='tabpanel')
and unique ids.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx`:
- Around line 464-466: screenComp being truthy (from this.state.screenName and
screenFromName) hides the settings container even when the search UI is active
and replacing that screen; update the hide condition to also verify search is
not active/has no results. Replace the shouldHideSettingsContainer assignment
with a check that screenComp is truthy AND the search UI is closed (e.g.
!this.state.searchOpen) and there are no search results (e.g.
!this.state.searchResults || this.state.searchResults.length === 0), and make
the same change where the same logic appears (the other block around the lines
referenced) so editable settings in search results retain their save/apply
handlers.

---

Duplicate comments:
In `@packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx`:
- Around line 516-523: The search UI currently mounts a single shared results
panel (the element with id 'setting-section-search-results' pushed into
tabComponents) while the sidebar still renders section entries as tabs targeting
that shared panel; update ConfigScreen.tsx so that in search mode the sidebar
renders those entries as filter buttons or, alternatively, create distinct
tabpanel elements per section instead of the single role='region' panel. Locate
the code that constructs tabComponents (the push of the div with id
'setting-section-search-results') and the sidebar rendering logic that uses the
selected section, then either switch the sidebar entries to non-tab controls
when isSearchMode is true or change the tabComponents construction to produce
separate panels for each enabled section so each sidebar tab targets its own
tabpanel. Ensure ARIA roles are consistent: filter buttons should not be
role='tab' and per-section panels must retain role='region' (or role='tabpanel')
and unique ids.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0bd56e66-29d6-49fd-868c-e5921d74247b

📥 Commits

Reviewing files that changed from the base of the PR and between f96839d and 37a4cdb.

📒 Files selected for processing (5)
  • packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
  • packages/app-desktop/gui/ConfigScreen/Sidebar.tsx
  • packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx
  • packages/lib/components/shared/config/config-search-text.ts
  • packages/lib/components/shared/config/config-search.test.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/lib/components/shared/config/config-search.test.ts
  • packages/lib/components/shared/config/config-search-text.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx

Comment thread packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx Outdated
@slimuCS

slimuCS commented Apr 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review! I've pushed fixes for the confirmed bugs. Here's a rundown:

Fixed: (1), (2), (3), (4), (5), (6), (9), (12), and the two missing test cases from (11).

(7): The return true branch in isMetadataMatched is unreachable in normal flow — the caller already guards against empty queries. Left it as-is, but happy to clean it up if preferred.

(10): The <label><span> change was intentional — a standalone <label> without htmlFor is semantically incorrect. It came in with the a11y commit so I kept it bundled. Happy to split it out if preferred.

@laurent22 laurent22 added the v3.7 label Apr 23, 2026
@laurent22

Copy link
Copy Markdown
Owner

Thank you @slimuCS! We will make the feature part of next pre-release 3.7

@laurent22
laurent22 merged commit d744db5 into laurent22:dev Apr 30, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

desktop All desktop platforms enhancement Feature requests and code enhancements search v3.7

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for searching for settings in the desktop app

3 participants