From d1b7c77856f92470aa51ad94cf6b5973ffd81b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Goran=20Alkovi=C4=87?= <77000136+goranalkovic-infinum@users.noreply.github.com> Date: Thu, 22 Feb 2024 10:02:19 +0100 Subject: [PATCH] Release 9.4.2 (#798) * update packages * tweaking linkinput * update changelog and versions --- CHANGELOG.md | 12 + package.json | 14 +- scripts/components/link-input/link-input.js | 277 ++++++++++++-------- 3 files changed, 186 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60bd0f62f..6bb0bef9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ 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 +- Dependency updates + ## [9.4.0] - 2024-02-14 ### Added @@ -959,6 +969,8 @@ 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 [9.3.0]: https://github.com/infinum/eightshift-frontend-libs/compare/9.2.1...9.3.0 diff --git a/package.json b/package.json index 5c8cded0d..039737e64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eightshift/frontend-libs", - "version": "9.4.0", + "version": "9.4.2", "description": "A collection of useful frontend utility modules. powered by Eightshift", "author": { "name": "Eightshift team", @@ -29,7 +29,6 @@ "lint": "npm run lintJs && npm run lintStyle", "storybookBuild": "storybook build -c .storybook -o docs/storybook", "storybook": "storybook dev -c .storybook", - "sassdocBuild": "node ./node_modules/sassdoc/bin/sassdoc styles/scss --dest docs/sassdocs && cp ./styles/sassdocs/main.css ./docs/sassdocs/assets/css/", "docsBuild": "rm -rf docs && npm run storybookBuild", "docsDeploy": "npm run docsBuild && gh-pages -d docs", "test": "jest --maxWorkers=2", @@ -50,7 +49,8 @@ "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", "@infinumjs/eslint-config-react-js": "^3.5.0", - "@stylistic/stylelint-plugin": "^2.0.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", @@ -91,23 +91,23 @@ "react-html-parser": "^2.0.2", "react-select": "^5.8.0", "regenerator-runtime": "^0.14.1", - "sass": "^1.70.0", + "sass": "^1.71.0", "sass-loader": "^14.1.0", - "storybook": "^7.6.15", + "storybook": "^7.6.16", "stream-browserify": "^3.0.0", "style-loader": "^3.3.4", "stylelint": "^16.2.1", "stylelint-config-standard": "^36.0.0", "stylelint-config-standard-scss": "^13.0.0", "terser-webpack-plugin": "^5.3.10", - "webpack": "^5.90.1", + "webpack": "^5.90.3", "webpack-cli": "^5.1.4", "webpack-manifest-plugin": "^5.0.0", "webpack-merge": "^5.10.0" }, "devDependencies": { "@babel/preset-env": "^7.23.9", - "@eightshift/storybook": "^1.6.0", + "@eightshift/storybook": "^1.6.1", "@jest/globals": "^29.7.0", "babel-jest": "^29.7.0", "chalk": "^5.3.0", 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 + /> +
+
}