From 19e930821e9eac608c701aa8b3f0cb5690dda3a7 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 6 Jan 2025 20:56:07 -0800 Subject: [PATCH] Use an ItemList for suggestions Note: This commit and the prior are related to https://github.com/laurent22/joplin/issues/10795 --- packages/app-desktop/gui/InlineCombobox.tsx | 60 +++++++++++-------- .../styles/combobox-suggestion-option.scss | 2 + .../gui/styles/combobox-wrapper.scss | 2 - 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/packages/app-desktop/gui/InlineCombobox.tsx b/packages/app-desktop/gui/InlineCombobox.tsx index ec716ecdfa1..7dd91eadcd7 100644 --- a/packages/app-desktop/gui/InlineCombobox.tsx +++ b/packages/app-desktop/gui/InlineCombobox.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { useState, useCallback, CSSProperties, useEffect, useRef, useId } from 'react'; import { _ } from '@joplin/lib/locale'; import { focus } from '@joplin/lib/utils/focusHandler'; +import ItemList from './ItemList'; interface Props { inputType?: string; @@ -24,10 +25,9 @@ const suggestionMatchesFilter = (suggestion: string, filter: string) => { const InlineCombobox: React.FC = ({ inputType, controls, inputStyle, value, suggestedValues, renderOption, onChange, inputId }) => { const [showList, setShowList] = useState(false); - const [visibleSuggestions, setVisibleSuggestions] = useState([]); const containerRef = useRef(null); const inputRef = useRef(null); - const listboxRef = useRef(null); + const listboxRef = useRef|null>(null); const [filteredSuggestions, setFilteredSuggestions] = useState(suggestedValues); @@ -38,8 +38,10 @@ const InlineCombobox: React.FC = ({ inputType, controls, inputStyle, valu const selectedIndex = filteredSuggestions.indexOf(value); useEffect(() => { - setVisibleSuggestions(filteredSuggestions); - }, [filteredSuggestions]); + if (selectedIndex >= 0 && showList) { + listboxRef.current?.makeItemIndexVisible(selectedIndex); + } + }, [selectedIndex, showList]); const focusInput = useCallback(() => { focus('ComboBox/focus input', inputRef.current); @@ -61,9 +63,10 @@ const InlineCombobox: React.FC = ({ inputType, controls, inputStyle, valu setShowList(true); }, []); - const onBlur = useCallback(() => { + const onBlur = useCallback((event: React.FocusEvent) => { const hasHoverOrFocus = !!containerRef.current.querySelector(':focus-within, :hover'); - if (!hasHoverOrFocus) { + const movesToContainedItem = containerRef.current.contains(event.relatedTarget); + if (!hasHoverOrFocus && !movesToContainedItem) { setShowList(false); } }, []); @@ -75,6 +78,7 @@ const InlineCombobox: React.FC = ({ inputType, controls, inputStyle, valu focusInput(); onChange(newValue); setFilteredSuggestions(suggestedValues); + setShowList(false); }, [onChange, suggestedValues, focusInput]); const onKeyDown: React.KeyboardEventHandler = useCallback(event => { @@ -103,8 +107,6 @@ const InlineCombobox: React.FC = ({ inputType, controls, inputStyle, valu } const newKey = filteredSuggestions[newSelectedIndex]; onChange(newKey); - const targetChild = listboxRef.current.children[newSelectedIndex]; - targetChild?.scrollIntoView({ block: 'nearest' }); } setShowList(true); } else if (event.code === 'Enter') { @@ -118,28 +120,32 @@ const InlineCombobox: React.FC = ({ inputType, controls, inputStyle, valu }, [filteredSuggestions, value, selectedIndex, onChange]); const valuesListId = useId(); - let selectedSuggestionId = undefined; - const suggestionElements = []; - for (const key of visibleSuggestions) { - const selected = key === value; - const id = `combobox-${valuesListId}-option-${key}`; - if (selected) { - selectedSuggestionId = id; + const itemId = (index: number) => { + if (index < 0) { + return undefined; + } else { + return `combobox-${valuesListId}-option-${index}`; } + }; + const onRenderItem = (key: string, index: number) => { + const selected = key === value; + const id = itemId(index); - suggestionElements.push( + return (
{renderOption(key)}
, + >{renderOption(key)} ); - } + }; return (
= ({ inputType, controls, inputStyle, valu aria-autocomplete='list' aria-controls={valuesListId} aria-expanded={showList} - aria-activedescendant={selectedSuggestionId} + aria-activedescendant={itemId(selectedIndex)} />
{ // Custom controls controls } -
= 0 ? selectedIndex : undefined} + + items={filteredSuggestions} + itemRenderer={onRenderItem} id={valuesListId} ref={listboxRef} - > - {suggestionElements} -
+ />
); diff --git a/packages/app-desktop/gui/styles/combobox-suggestion-option.scss b/packages/app-desktop/gui/styles/combobox-suggestion-option.scss index bed5c4a2384..4d13617f048 100644 --- a/packages/app-desktop/gui/styles/combobox-suggestion-option.scss +++ b/packages/app-desktop/gui/styles/combobox-suggestion-option.scss @@ -1,4 +1,6 @@ .combobox-suggestion-option { + height: 26px; + box-sizing: border-box; padding: 5px; border-bottom: 1px solid var(--joplin-border-color4); cursor: pointer; diff --git a/packages/app-desktop/gui/styles/combobox-wrapper.scss b/packages/app-desktop/gui/styles/combobox-wrapper.scss index b959ce17316..63b97569d82 100644 --- a/packages/app-desktop/gui/styles/combobox-wrapper.scss +++ b/packages/app-desktop/gui/styles/combobox-wrapper.scss @@ -2,10 +2,8 @@ .combobox-wrapper { > .suggestions { background-color: var(--joplin-background-color); - max-height: 200px; width: 50%; min-width: 20em; - overflow-y: auto; border: 1px solid var(--joplin-border-color4); border-radius: 5px; margin-top: 10px;