From a2e79ca82fed7e41ee4d9da347bdaeafc3b1e21d Mon Sep 17 00:00:00 2001 From: oda <934854676@qq.com> Date: Mon, 29 Apr 2024 10:19:06 +0800 Subject: [PATCH] add: a button to clear text in searchable-dropdown --- .../props-typing/i-interactable-id.tsx | 3 + src/ui/react-view/view-template/icon-view.tsx | 50 +++++++++---- .../view-template/searchable-dropdown.tsx | 70 ++++++++++++------- 3 files changed, 81 insertions(+), 42 deletions(-) create mode 100644 src/ui/react-view/props-typing/i-interactable-id.tsx diff --git a/src/ui/react-view/props-typing/i-interactable-id.tsx b/src/ui/react-view/props-typing/i-interactable-id.tsx new file mode 100644 index 0000000..e8e28a3 --- /dev/null +++ b/src/ui/react-view/props-typing/i-interactable-id.tsx @@ -0,0 +1,3 @@ +export interface I_InteractableId { + interactableId?: string; +} diff --git a/src/ui/react-view/view-template/icon-view.tsx b/src/ui/react-view/view-template/icon-view.tsx index 2674884..74683a8 100644 --- a/src/ui/react-view/view-template/icon-view.tsx +++ b/src/ui/react-view/view-template/icon-view.tsx @@ -2,6 +2,7 @@ import {IRenderable} from "../../common/i-renderable"; import React from "react"; import {getIcon} from "obsidian"; import {HtmlStringComponent} from "./html-string-component"; +import {I_InteractableId} from "../props-typing/i-interactable-id"; export const CssClass_Link = "cm-underline"; export const obsidianIconTopOffset = 4; @@ -41,33 +42,52 @@ export function InternalLinkView({content, onIconClicked, onContentClicked, styl /> } +interface I_IconClickable { + content?: IRenderable; + onIconClicked?: () => void; + onContentClicked?: () => void; + clickable?: boolean; + +} + /** * Can also be used as non-clickable */ -export function ClickableIconView({content, onIconClicked, onContentClicked, iconName, style, clickable}: { - content?: IRenderable, - onIconClicked?: () => void, - onContentClicked?: () => void, +export function ClickableIconView({ + content, + onIconClicked, + onContentClicked, + iconName, + style, + clickable, + interactableId + }: { iconName: string, - clickable?: boolean, -} & I_Stylable) { +} & I_IconClickable & I_Stylable & I_InteractableId) { return } content={content} - onIconClicked={onIconClicked} onContentClicked={onContentClicked} style={style}/> + onIconClicked={onIconClicked} onContentClicked={onContentClicked} style={style} + interactableId={interactableId}/> } -export function ClickableView({icon, content, style, clickable = true, onIconClicked, onContentClicked}: { - content?: IRenderable, - onIconClicked?: () => void, - onContentClicked?: () => void, +export function ClickableView({ + icon, + content, + style, + clickable = true, + onIconClicked, + onContentClicked, + interactableId + }: { icon: IRenderable, - clickable?: boolean, -} & I_Stylable) { +} & I_IconClickable & I_Stylable & I_InteractableId) { + onIconClicked = clickable ? onIconClicked : undefined; + onContentClicked = clickable ? onContentClicked : undefined; return - {clickable ? + {clickable ? {icon} : icon} - + {content} diff --git a/src/ui/react-view/view-template/searchable-dropdown.tsx b/src/ui/react-view/view-template/searchable-dropdown.tsx index ebbd2d5..f79dddb 100644 --- a/src/ui/react-view/view-template/searchable-dropdown.tsx +++ b/src/ui/react-view/view-template/searchable-dropdown.tsx @@ -8,6 +8,7 @@ import {isCharacterInput} from "../../../utils/react-user-input"; import {IRenderable} from "../../common/i-renderable"; import {toggleValueInArray} from "../workflow-filter"; import {dropdownSelectedColor} from "../style-def"; +import {ClickableIconView} from "./icon-view"; interface I_OptionItem { @@ -40,7 +41,7 @@ export const SearchableDropdown = (props: { RenderView?: (props: { item: OptionValueType }) => IRenderable; singleSelect?: boolean; currentOptionValues?: OptionValueType[]; // Current selected option values. If singleSelect, we don't need to pass this. - dropdownId: string; + dropdownId: string; // used to check if we clicked outside the dropdown. The direct interactable element should have a prefix of this id. } & StyleProps) => { const singleSelect = props.singleSelect ?? true; // Default to single select if (!singleSelect) { @@ -50,7 +51,7 @@ export const SearchableDropdown = (props: { console.error("Multiple Select Dropdown must pass currentOptionValues. Data are ", props.data) } } - const searchString = props.dropdownId; + const dropdownId = props.dropdownId; const [searchText, setSearchText] = useState("") const {dropDownDisplay, setDropDownDisplay, showDropdown} = usePopup(); const filtered = props.data.filter(k => k.name.toLowerCase().includes(searchText.toLowerCase())) @@ -73,7 +74,9 @@ export const SearchableDropdown = (props: { onKeyDown={handleBaseKeyboard} onBlur={(event) => { // Hide Dropdown if we lose focus - if (event.relatedTarget && event.relatedTarget.id.startsWith(searchString)) { + // target is the element that lost focus (input), relatedTarget is the element that gains focus + devLog(event.relatedTarget?.id) + if (event.relatedTarget?.id?.startsWith(dropdownId)) { // Let the project_choice button handle the click event // Otherwise when we lose focus and hide the dropdown, the button will not be triggered. } else { @@ -83,39 +86,51 @@ export const SearchableDropdown = (props: { } }} > - { - const text = event.target.value; - // when there is text, show the dropdown - if (text && text.length > 0) { - showDropdown() - } else { - hideDropdown() - } - handleSetSearchText(text) - }} - - onFocus={() => { - // show when click search input box - - devLog("Input Focused"); - showDropdown(); - }} - /> + + { + const text = event.target.value; + // when there is text, show the dropdown + if (text && text.length > 0) { + showDropdown() + } else { + hideDropdown() + } + handleSetSearchText(text) + }} + + onFocus={() => { + // show when click search input box + + devLog("Input Focused"); + showDropdown(); + }} + /> + { + setSearchText("") + // input's focus is already lost onclick, so we need to + // 1. show the already hidden dropdown. + // a. must. merely focusing on the input box will show the dropdown on the next event loop, which causes blinking dropdown. + // 2. focus on it again, so that user can input text immediately. + showDropdown() + inputRef.current?.focus() + }} iconName={"x-circle"}/> + {/*Add background so it won't be transparent. */} -
{filtered.map((option: OptionValueType) => { - const childId = `${searchString}_${option.name}`; + const childId = `${dropdownId}_${option.name}`; // @ts-ignore const has = !singleSelect ? props.currentOptionValues.includes(option) : false; return (
+ function handleBaseKeyboard(event: KeyboardEvent) {