From b7ee79d139a0ab4411e258d29c128394d603569b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Goran=20Alkovi=C4=87?= <77000136+goranalkovic-infinum@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:04:46 +0100 Subject: [PATCH 1/2] tweaking linkinput --- scripts/components/link-input/link-input.js | 277 ++++++++++++-------- 1 file changed, 167 insertions(+), 110 deletions(-) diff --git a/scripts/components/link-input/link-input.js b/scripts/components/link-input/link-input.js index b28d05ede..bc32b5a1e 100644 --- a/scripts/components/link-input/link-input.js +++ b/scripts/components/link-input/link-input.js @@ -1,5 +1,6 @@ -import React, { useState, useRef, useMemo, useCallback } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { __, sprintf } from '@wordpress/i18n'; +import { useDebounce } from '@uidotdev/usehooks'; import { Button, Tooltip, Popover, Spinner } from '@wordpress/components'; import { select } from '@wordpress/data'; import { @@ -11,7 +12,6 @@ import { truncateMiddle, getFetchWpApi, unescapeHTML, - debounce, truncate, STORE_NAME, } from '@eightshift/frontend-libs/scripts'; @@ -40,6 +40,7 @@ import { * @param {React.Component?} [props.additionalOptionTiles] - If provided, allows adding additional option tiles. * @param {callback} [props.suggestionTypeIconOverride] - Allows overriding the default icon for the suggestion type, e.g. when using CPTs. Callback should be in the format: `(type) => icon or React component`. * @param {callback} [props.fetchSuggestions] - Allows overriding the default function for fetching suggestions. Callback should be in the format: `(searchTerm) => Promise`. + * @param {int} [props.inputDebounceDelay=500] - Allows overriding the default debounce delay for the input. Default is 500ms. * * @since 9.4.0 */ @@ -74,6 +75,8 @@ export const LinkInput = ({ suggestionTypeIconOverride, fetchSuggestions, + + inputDebounceDelay = 300, }) => { const hasUrl = url?.trim()?.length > 0; const isAnchor = hasUrl && url?.includes('#'); @@ -83,47 +86,73 @@ export const LinkInput = ({ const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false); const [shownSuggestions, setShownSuggestions] = useState([]); const [suggestionsVisible, setSuggestionsVisible] = useState(false); + const [suggestionFocusMessageVisible, setSuggestionFocusMessageVisible] = useState(false); + + const debouncedInputValue = useDebounce(inputValue, inputDebounceDelay); const inputContainerRef = useRef(); const inputRef = useRef(); + const suggestionPopoverRef = useRef(); const { config: { linkInputCptIconOverrides } } = select(STORE_NAME).getSettings(); - const showSuggestionPanel = useCallback(async (searchTerm) => { - setSuggestionsVisible(true); - - setIsLoadingSuggestions(true); + useEffect(() => { + const newUrl = debouncedInputValue; - const fetchFunction = fetchSuggestions ?? getFetchWpApi('search', { - processId: ({ url }) => url, - processLabel: ({ title }) => unescapeHTML(title), - processMetadata: ({ type, subtype }) => ({ type, subtype }), - additionalParam: { - search: searchTerm, - type: 'post', - _locale: 'user', - per_page: 5, - }, - noCache: true, - searchColumns: 'post_title', - fields: 'id,title,type,subtype,url', - }); - - const items = await fetchFunction(); + // eslint-disable-next-line max-len + if (newUrl?.startsWith('#') || newUrl?.startsWith(':') || newUrl?.startsWith('mailto:') || newUrl?.startsWith('tel:') || newUrl?.startsWith('http://') || newUrl?.startsWith('https://')) { + setSuggestionsVisible(false); + onChange({ url: newUrl, isAnchor: newUrl?.includes('#'), newTab: opensInNewTab }); + return; + } else if (newUrl?.length < 3) { + setSuggestionsVisible(false); + return; + } - setIsLoadingSuggestions(false); - setShownSuggestions(items); - }, []); // eslint-disable-line react-hooks/exhaustive-deps + const fetchSuggestionData = async () => { + if (!suggestionsVisible) { + setSuggestionFocusMessageVisible(true); + } - const debouncedShowSuggestionPanel = useMemo(() => debounce(showSuggestionPanel, 250), [showSuggestionPanel]); + setSuggestionsVisible(true); + setIsLoadingSuggestions(true); + + const fetchFunction = fetchSuggestions ?? getFetchWpApi('search', { + processId: ({ url }) => url, + processLabel: ({ title }) => unescapeHTML(title), + processMetadata: ({ type, subtype }) => ({ type, subtype }), + additionalParam: { + search: debouncedInputValue, + type: 'post', + _locale: 'user', + per_page: 5, + }, + noCache: true, + searchColumns: 'post_title', + fields: 'id,title,type,subtype,url', + }); + + const items = await fetchFunction(); + + setIsLoadingSuggestions(false); + setShownSuggestions(items); + }; + + fetchSuggestionData(); + }, [debouncedInputValue]); // eslint-disable-line react-hooks/exhaustive-deps + + const closeSuggestionPanel = () => { + setSuggestionsVisible(false); + setSuggestionFocusMessageVisible(true); + inputRef?.current?.focus(); + }; - const handleCommitUrl = (url, blurInput = false) => { + const handleCommitUrl = (url, closeSuggestions = false) => { onChange({ url: url, isAnchor: url?.includes('#'), newTab: opensInNewTab }); setInputValue(url); - if (blurInput) { - setSuggestionsVisible(false); - inputRef?.current?.blur(); + if (closeSuggestions) { + closeSuggestionPanel(); } }; @@ -133,16 +162,6 @@ export const LinkInput = ({ } setInputValue(newUrl); - - // eslint-disable-next-line max-len - if (newUrl?.startsWith('#') || newUrl?.startsWith(':') || newUrl?.startsWith('mailto:') || newUrl?.startsWith('tel:') || newUrl?.startsWith('http://') || newUrl?.startsWith('https://')) { - setSuggestionsVisible(false); - onChange({ url: newUrl, isAnchor: newUrl?.includes('#'), newTab: opensInNewTab }); - } else if (newUrl?.length < 3) { - setSuggestionsVisible(false); - } else { - debouncedShowSuggestionPanel(newUrl); - } }; const AnchorTooltip = () => { @@ -190,7 +209,13 @@ export const LinkInput = ({ placeholder={__('Search or enter URL', 'eightshift-frontend-libs')} onKeyDown={(e) => { if (e.key === 'Enter') { - handleCommitUrl(e?.target?.value); + handleCommitUrl(e?.target?.value, true); + } + + if (suggestionsVisible && ['ArrowDown', 'Tab'].includes(e.key)) { + e.preventDefault(); + setSuggestionFocusMessageVisible(false); + suggestionPopoverRef?.current?.querySelector('.components-button')?.focus(); } }} onBlur={(e) => handleCommitUrl(e?.target?.value)} @@ -228,90 +253,122 @@ export const LinkInput = ({ resize={false} offset={4} position='bottom' - onClose={() => setSuggestionsVisible(false)} - onFocusOutside={() => setSuggestionsVisible(false)} - focusOnMount + onClose={() => closeSuggestionPanel()} + onFocusOutside={() => closeSuggestionPanel()} + ref={suggestionPopoverRef} + focusOnMount={suggestionFocusMessageVisible ? false : 'firstElement'} + constrainTabbing > -