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 > -
+
{isLoadingSuggestions && - } - label={__('Fetching suggestions', 'eightshift-frontend-libs')} - additionalClasses='es-text-align-left es-gap-1.5! es-w-full es-h-10' - standalone - /> +
+ } + label={__('Fetching suggestions', 'eightshift-frontend-libs')} + additionalClasses='es-text-align-left es-gap-1.5! es-w-full es-h-10' + standalone + /> +
} {!isLoadingSuggestions && shownSuggestions.length < 1 && - {icons.searchEmpty}} - label={sprintf(__('No results found for %s', 'eightshift-frontend-libs'), inputValue)} - additionalClasses='es-text-align-left es-gap-1.5! es-w-full es-h-10' - standalone - /> +
+ +
} - {!isLoadingSuggestions && shownSuggestions?.length > 0 && shownSuggestions.map((suggestion, i) => { - const { label: title, value: url, metadata: { subtype } } = suggestion; - - let typeIcon = icons.file; - - if (subtype.toLowerCase() === 'url') { - typeIcon = icons.externalLink; - } else if (subtype.toLowerCase() === 'attachment') { - typeIcon = icons.file; - } else if (subtype.toLowerCase() === 'category') { - typeIcon = icons.layoutAlt; - } else if (subtype.toLowerCase() === 'internal') { - typeIcon = icons.anchor; - } else if (subtype.toLowerCase() === 'eightshift-forms') { - typeIcon = icons.formAlt; - } - - if (linkInputCptIconOverrides) { - const overrideIcon = linkInputCptIconOverrides?.[subtype]; - - if (overrideIcon && overrideIcon in icons) { - typeIcon = icons?.[overrideIcon]; - } - } + {!isLoadingSuggestions && shownSuggestions?.length > 0 && +
+ {shownSuggestions.map((suggestion, i) => { + const { label: title, value: url, metadata: { subtype } } = suggestion; + + let typeIcon = icons.file; + + if (subtype.toLowerCase() === 'url') { + typeIcon = icons.externalLink; + } else if (subtype.toLowerCase() === 'attachment') { + typeIcon = icons.file; + } else if (subtype.toLowerCase() === 'category') { + typeIcon = icons.layoutAlt; + } else if (subtype.toLowerCase() === 'internal') { + typeIcon = icons.anchor; + } else if (subtype.toLowerCase() === 'eightshift-forms') { + typeIcon = icons.formAlt; + } + + if (linkInputCptIconOverrides) { + const overrideIcon = linkInputCptIconOverrides?.[subtype]; + + if (overrideIcon && overrideIcon in icons) { + typeIcon = icons?.[overrideIcon]; + } + } + + if (suggestionTypeIconOverride) { + const overrideIcon = suggestionTypeIconOverride(subtype); + + if (overrideIcon) { + typeIcon = overrideIcon; + } + } + + return ( + + ); + })} +
+ } - if (suggestionTypeIconOverride) { - const overrideIcon = suggestionTypeIconOverride(subtype); - if (overrideIcon) { - typeIcon = overrideIcon; - } - } +
- return ( - - ); - })} +
+ +
- {!isLoadingSuggestions && - <> -
+ <> +
- - - } + } + Esc} + label={__('Close suggestions panel', 'eightshift-frontend-libs')} + standalone + /> +
+
} From cddea040ed4460c795f9c58e011088027fc04c10 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:59 +0100 Subject: [PATCH 2/2] update changelog and versions --- CHANGELOG.md | 6 ++++++ package.json | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ee14e9f4..6bb0bef9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. This projects adheres to [Semantic Versioning](https://semver.org/) and [Keep a CHANGELOG](https://keepachangelog.com/). +## [9.4.2] - 2024-02-21 + +### Updated +- `LinkInput` should handle input debouncing better, and the delay is now configurable via the `inputDebounceDelay` prop + ## [9.4.1] - 2024-02-19 ### Updated @@ -964,6 +969,7 @@ Follow this migration script in order for you project to work correctly with the [Unreleased]: https://github.com/infinum/eightshift-frontend-libs/compare/master...HEAD +[9.4.2]: https://github.com/infinum/eightshift-frontend-libs/compare/9.4.1...9.4.2 [9.4.1]: https://github.com/infinum/eightshift-frontend-libs/compare/9.4.0...9.4.1 [9.4.0]: https://github.com/infinum/eightshift-frontend-libs/compare/9.3.1...9.4.0 [9.3.1]: https://github.com/infinum/eightshift-frontend-libs/compare/9.3.0...9.3.1 diff --git a/package.json b/package.json index 7170d8ec2..039737e64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eightshift/frontend-libs", - "version": "9.4.1", + "version": "9.4.2", "description": "A collection of useful frontend utility modules. powered by Eightshift", "author": { "name": "Eightshift team", @@ -50,6 +50,7 @@ "@dnd-kit/utilities": "^3.2.2", "@infinumjs/eslint-config-react-js": "^3.5.0", "@stylistic/stylelint-plugin": "^2.1.0", + "@uidotdev/usehooks": "^2.4.1", "@wordpress/api-fetch": "^6.48.0", "@wordpress/block-editor": "^12.19.1", "@wordpress/dependency-extraction-webpack-plugin": "^5.2.0",