From 384c42f5a4237c29e1ac19e88b6e598cbf199a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matuzal=C3=A9m=20Teles?= Date: Wed, 14 May 2025 19:52:25 -0500 Subject: [PATCH 1/9] feat(@clayui/core): adds the loading indicator when loading items in the collection --- packages/clay-core/package.json | 2 +- .../clay-core/src/collection/Collection.tsx | 3 +- packages/clay-core/src/collection/types.ts | 5 + .../src/collection/useCollection.tsx | 135 ++++++++--- .../clay-core/src/collection/useVirtual.ts | 14 +- yarn.lock | 220 ++++++++---------- 6 files changed, 209 insertions(+), 170 deletions(-) diff --git a/packages/clay-core/package.json b/packages/clay-core/package.json index deb6899662..58aca85e2f 100644 --- a/packages/clay-core/package.json +++ b/packages/clay-core/package.json @@ -36,7 +36,7 @@ "@clayui/provider": "^3.128.0", "@clayui/shared": "^3.136.0", "@clayui/table": "^3.111.0", - "@tanstack/react-virtual": "3.0.0-beta.54", + "@tanstack/react-virtual": "^3.13.8", "aria-hidden": "^1.2.2", "classnames": "^2.2.6", "fuzzy": "^0.1.3", diff --git a/packages/clay-core/src/collection/Collection.tsx b/packages/clay-core/src/collection/Collection.tsx index 94be1cc917..6edf638e16 100644 --- a/packages/clay-core/src/collection/Collection.tsx +++ b/packages/clay-core/src/collection/Collection.tsx @@ -175,8 +175,7 @@ export function Collection< const target = event.target as HTMLElement; if ( - target.scrollTop + target.clientHeight >= - target.scrollHeight - 40 && + target.scrollTop + target.clientHeight >= target.scrollHeight && !isLoading ) { onLoadMore!(); diff --git a/packages/clay-core/src/collection/types.ts b/packages/clay-core/src/collection/types.ts index 85f0122acc..0df77be444 100644 --- a/packages/clay-core/src/collection/types.ts +++ b/packages/clay-core/src/collection/types.ts @@ -109,6 +109,11 @@ export type Props = { */ notFound?: JSX.Element; + /** + * Renders an element when it is loading more items. + */ + load?: JSX.Element; + /** * Defines which key should be used as the item identifier. */ diff --git a/packages/clay-core/src/collection/useCollection.tsx b/packages/clay-core/src/collection/useCollection.tsx index d56c39fbd0..a7d5c7d73d 100644 --- a/packages/clay-core/src/collection/useCollection.tsx +++ b/packages/clay-core/src/collection/useCollection.tsx @@ -61,6 +61,7 @@ export function useCollection< itemContainer: ItemContainer, itemIdKey = 'id', items, + load, notFound, parentKey, passthroughKey = true, @@ -96,7 +97,7 @@ export function useCollection< return false; }, - [filter] + [filter, filterKey] ); const performItemRender = useCallback( @@ -154,7 +155,7 @@ export function useCollection< ...(props ? props : {}), }); }, - [ItemContainer, performFilter] + [ItemContainer, performFilter, passthroughKey, suppressTextValueWarning] ); const createItemsLayout = useCallback( @@ -253,7 +254,16 @@ export function useCollection< }); } }, - [performFilter, publicApi, itemIdKey] + [ + performFilter, + publicApi, + itemIdKey, + exclude, + parentKey, + suppressTextValueWarning, + collectionId, + layout, + ] ); const performCollectionRender = useCallback( @@ -261,6 +271,28 @@ export function useCollection< if (children instanceof Function && items) { if (virtualizer) { return virtualizer.getVirtualItems().map((virtual) => { + const isLoader = virtual.index > items.length - 1; + + if (isLoader) { + return React.cloneElement( + load as React.ReactElement, + { + 'data-index': virtual.index, + key: `${virtual.index}-loader`, + ref: (node: HTMLElement) => { + virtualizer.measureElement(node); + }, + style: { + left: 0, + position: 'absolute', + top: 0, + transform: `translateY(${virtual.start}px)`, + width: '100%', + }, + } + ); + } + const item = items[virtual.index] as T; const publicItem = @@ -281,6 +313,10 @@ export function useCollection< ) as ChildElement); const props = { + className: + virtual.index === items.length - 1 && !!load + ? 'mb-2' + : undefined, 'data-index': virtual.index, ref: (node: HTMLElement) => { virtualizer.measureElement(node); @@ -368,21 +404,30 @@ export function useCollection< }); }, [ + load, performItemRender, publicApi, - virtualizer?.getVirtualItems().length, + exclude, + parentKey, visibleKeys, itemIdKey, + virtualizer, ] ); - const getItem = useCallback((key: React.Key) => { - return layout.current.get(key)!; - }, []); + const getItem = useCallback( + (key: React.Key) => { + return layout.current.get(key)!; + }, + [layout] + ); - const hasItem = useCallback((key: React.Key) => { - return layout.current.has(key)!; - }, []); + const hasItem = useCallback( + (key: React.Key) => { + return layout.current.has(key)!; + }, + [layout] + ); const getFirstItem = useCallback(() => { const key = layout.current.keys().next().value; @@ -391,7 +436,7 @@ export function useCollection< key, ...layout.current.get(key)!, }; - }, []); + }, [layout]); const getLastItem = useCallback(() => { const key = Array.from(layout.current.keys()).pop()!; @@ -400,13 +445,13 @@ export function useCollection< key, ...layout.current.get(key)!, }; - }, []); + }, [layout]); const getItems = useCallback(() => { return Array.from(layout.current.keys()); - }, []); + }, [layout]); - const getSize = useCallback(() => layout.current.size, [collectionId]); + const getSize = useCallback(() => layout.current.size, [layout]); const cleanUp = useCallback(() => { layout.current.forEach((value, key) => { @@ -429,7 +474,7 @@ export function useCollection< // before rendering the element. The data can be consumed later even // if the element is not rendered. createItemsLayout({children, items}); - }, [children, createItemsLayout, items]); + }, [children, createItemsLayout, items, cleanUp, parentLayout]); // It builds the dynamic or static collection, done in two steps: Data and // Rendering, both go through the elements to get the data of each item. @@ -444,7 +489,7 @@ export function useCollection< } return list; - }, [children, performCollectionRender, items]); + }, [children, performCollectionRender, items, notFound]); // Effect only called when the component is unmounted removing the layout // items that are rendered by the collection instance, effect only when @@ -468,31 +513,45 @@ export function useCollection< if (forceUpdate) { forceUpdate(null); } - }, [children, createItemsLayout, performCollectionRender, items]); + }, [forceUpdate, children, items]); + + const contextValue = useMemo( + () => ({ + forceUpdate: forceDeepRootUpdate ? setForceUpdate : undefined, + keys: layoutKeysRef, + layout, + }), + [forceDeepRootUpdate, setForceUpdate, layoutKeysRef, layout] + ); + + const collectionOutput = useMemo( + () => + connectNested ? ( + + {rendered} + + ) : ( + <>{rendered} + ), + [connectNested, contextValue, rendered] + ); + + const collectionAPI = useMemo( + () => ({ + getFirstItem, + getItem, + getItems, + getLastItem, + getSize, + hasItem, + }), + [getFirstItem, getItem, getItems, getLastItem, getSize, hasItem] + ); return { UNSAFE_virtualizer: virtualizer, - collection: connectNested ? ( - - {rendered} - - ) : ( - <>{rendered} - ), - getFirstItem, - getItem, - getItems, - getLastItem, - getSize, - hasItem, + collection: collectionOutput, + ...collectionAPI, size: virtualizer ? virtualizer.getTotalSize() : undefined, virtualize: !!virtualizer, }; diff --git a/packages/clay-core/src/collection/useVirtual.ts b/packages/clay-core/src/collection/useVirtual.ts index f941724c73..35880c3e1c 100644 --- a/packages/clay-core/src/collection/useVirtual.ts +++ b/packages/clay-core/src/collection/useVirtual.ts @@ -23,11 +23,21 @@ type Props = { * collection. */ parentRef: React.RefObject; + + /** + * Flag if a request is in progress. + */ + isLoading?: boolean; }; -export function useVirtual({estimateSize, items = [], parentRef}: Props) { +export function useVirtual({ + estimateSize, + items = [], + parentRef, + isLoading, +}: Props) { const virtualizer = useVirtualizer({ - count: items.length, + count: isLoading ? items.length + 1 : items.length, estimateSize: () => estimateSize, getScrollElement: () => parentRef.current, overscan: 7, diff --git a/yarn.lock b/yarn.lock index 8773ef8354..b89e746189 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2384,19 +2384,17 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@pmmmwh/react-refresh-webpack-plugin@0.5.1", "@pmmmwh/react-refresh-webpack-plugin@^0.5.3": - version "0.5.1" - resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.1.tgz#7e98d6f22c360e1dd00909f5fa9d0f6ecc263292" - integrity sha512-ccap6o7+y5L8cnvkZ9h8UXCGyy2DqtwCD+/N3Yru6lxMvcdkPKtdx13qd7sAC9s5qZktOmWf9lfUjsGOvSdYhg== +"@pmmmwh/react-refresh-webpack-plugin@^0.5.3": + version "0.5.16" + resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.16.tgz#36795b3d5a967032a769977780f2dcc01f4e9c4a" + integrity sha512-kLQc9xz6QIqd2oIYyXRUiAp79kGpFBm3fEM9ahfG1HI0WI5gdZ2OVHWdmZYnwODt7ISck+QuQ6sBPrtvUBML7Q== dependencies: - ansi-html-community "^0.0.8" - common-path-prefix "^3.0.0" - core-js-pure "^3.8.1" + ansi-html "^0.0.9" + core-js-pure "^3.23.3" error-stack-parser "^2.0.6" - find-up "^5.0.0" html-entities "^2.1.0" - loader-utils "^2.0.0" - schema-utils "^3.0.0" + loader-utils "^2.0.4" + schema-utils "^4.2.0" source-map "^0.7.3" "@react-dnd/asap@^4.0.0": @@ -3603,17 +3601,17 @@ dependencies: defer-to-connect "^1.0.1" -"@tanstack/react-virtual@3.0.0-beta.54": - version "3.0.0-beta.54" - resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.0.0-beta.54.tgz#755979455adf13f2584937204a3f38703e446037" - integrity sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ== +"@tanstack/react-virtual@^3.13.8": + version "3.13.8" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.13.8.tgz#ce7c438ddc379a090a1b01f5436d5b2a1ebd750f" + integrity sha512-meS2AanUg50f3FBSNoAdBSRAh8uS0ue01qm7zrw65KGJtiXB9QXfybqZwkh4uFpRv2iX/eu5tjcH5wqUpwYLPg== dependencies: - "@tanstack/virtual-core" "3.0.0-beta.54" + "@tanstack/virtual-core" "3.13.8" -"@tanstack/virtual-core@3.0.0-beta.54": - version "3.0.0-beta.54" - resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.54.tgz#12259d007911ad9fce1388385c54a9141f4ecdc4" - integrity sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g== +"@tanstack/virtual-core@3.13.8": + version "3.13.8" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.13.8.tgz#6346e688521c1f086f508ccbebaad0b472a2aefb" + integrity sha512-BT6w89Hqy7YKaWewYzmecXQzcJh6HTBbKYJIIkMaNU49DZ06LoTV3z32DWWEdUsgW6n1xTmwTLs4GtWrZC261w== "@testing-library/dom@^8.0.0", "@testing-library/dom@^8.9.0": version "8.10.1" @@ -4062,6 +4060,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== +"@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/lodash@^4.14.167": version "4.14.184" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.184.tgz#23f96cd2a21a28e106dc24d825d4aa966de7a9fe" @@ -4915,11 +4918,25 @@ ajv-errors@^1.0.0: resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -4930,6 +4947,16 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4, ajv json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^8.0.0, ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + ajv@^8.0.1, ajv@^8.6.3: version "8.6.3" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.3.tgz#11a66527761dc3e9a3845ea775d2d3c0414e8764" @@ -4986,11 +5013,16 @@ ansi-gray@^0.1.1: dependencies: ansi-wrap "0.1.0" -ansi-html-community@0.0.8, ansi-html-community@^0.0.8: +ansi-html-community@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== +ansi-html@^0.0.9: + version "0.0.9" + resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.9.tgz#6512d02342ae2cc68131952644a129cb734cd3f0" + integrity sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg== + ansi-regex@^0.2.0, ansi-regex@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9" @@ -5820,7 +5852,7 @@ bl@^4.0.3, bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" -bluebird@^3.0.5, bluebird@^3.3.5, bluebird@^3.5.5: +bluebird@^3.3.5, bluebird@^3.5.5: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -6977,7 +7009,7 @@ command-exists@^1.2.6: resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== -commander@2, commander@^2.11.0, commander@^2.19.0, commander@^2.20.0, commander@^2.8.1, commander@^2.9.0: +commander@2, commander@^2.11.0, commander@^2.19.0, commander@^2.20.0, commander@^2.8.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -7022,11 +7054,6 @@ common-ancestor-path@^1.0.1: resolved "https://registry.yarnpkg.com/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz#4f7d2d1394d91b7abdf51871c62f71eadb0182a7" integrity sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w== -common-path-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" - integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== - commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -7098,14 +7125,6 @@ config-chain@1.1.12: ini "^1.3.4" proto-list "~1.2.1" -config-chain@~1.1.5: - version "1.1.13" - resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" - integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== - dependencies: - ini "^1.3.4" - proto-list "~1.2.1" - configstore@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" @@ -7281,10 +7300,10 @@ core-js-compat@^3.16.0, core-js-compat@^3.16.2, core-js-compat@^3.8.1: browserslist "^4.17.5" semver "7.0.0" -core-js-pure@^3.8.1: - version "3.41.0" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.41.0.tgz#349fecad168d60807a31e83c99d73d786fe80811" - integrity sha512-71Gzp96T9YPk63aUvE5Q5qP+DryB4ZloUZPSOebGM88VNw8VNfvdA7z6kGA8iGOTEzAomsRidp4jXSmUIJsL+Q== +core-js-pure@^3.23.3: + version "3.42.0" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.42.0.tgz#e86c45a7f3bdcb608823e872f73d1ad9ddf0531d" + integrity sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ== core-js@^2.4.0, core-js@^2.6.10, core-js@^2.6.5: version "2.6.12" @@ -8795,7 +8814,7 @@ duplexer3@^0.1.4: resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= -duplexer@^0.1.1, duplexer@^0.1.2, duplexer@~0.1.1: +duplexer@^0.1.1, duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== @@ -8831,17 +8850,6 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -editorconfig@^0.13.2: - version "0.13.3" - resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.13.3.tgz#e5219e587951d60958fd94ea9a9a008cdeff1b34" - integrity sha512-WkjsUNVCu+ITKDj73QDvi0trvpdDWdkDyHybDGSXPfekLCqwmpD7CP7iPbvBgosNuLcI96XTDwNa75JyFl7tEQ== - dependencies: - bluebird "^3.0.5" - commander "^2.9.0" - lru-cache "^3.2.0" - semver "^5.1.0" - sigmund "^1.0.1" - ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -9431,19 +9439,6 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= -event-stream@3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" - integrity sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g== - dependencies: - duplexer "~0.1.1" - from "~0" - map-stream "~0.1.0" - pause-stream "0.0.11" - split "0.3" - stream-combiner "~0.0.4" - through "~2.3.1" - event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" @@ -9757,6 +9752,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-uri@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" + integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + fastparse@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" @@ -10176,11 +10176,6 @@ from2@^2.1.0: inherits "^2.0.1" readable-stream "^2.0.0" -from@~0: - version "0.1.7" - resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" - integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== - fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -10829,11 +10824,16 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@4.2.10, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: +graceful-fs@4.2.10, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +graceful-fs@^4.2.11: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + grapheme-breaker@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/grapheme-breaker/-/grapheme-breaker-0.3.2.tgz#5b9e6b78c3832452d2ba2bb1cb830f96276410ac" @@ -13088,16 +13088,6 @@ jora@^1.0.0-beta.5: resolved "https://registry.yarnpkg.com/jora/-/jora-1.0.0-beta.5.tgz#55b2c4d86078af1bc74da401e88b67be42b0bddd" integrity sha512-hPJKQyF0eiCqQOwfgIuQa+8wIn+WcEcjjyeOchuiXEUnt6zbV0tHKsUqRRwJY47ZtBiGcJQNr/BGuYW1Sfwbvg== -js-beautify@1.7.5: - version "1.7.5" - resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.7.5.tgz#69d9651ef60dbb649f65527b53674950138a7919" - integrity sha512-9OhfAqGOrD7hoQBLJMTA+BKuKmoEtTJXzZ7WDF/9gvjtey1koVLuZqIY6c51aPDjbNdNtIXAkiWKVhziawE9Og== - dependencies: - config-chain "~1.1.5" - editorconfig "^0.13.2" - mkdirp "~0.5.0" - nopt "~3.0.1" - js-string-escape@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" @@ -13648,6 +13638,15 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" +loader-utils@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -14068,13 +14067,6 @@ lru-cache@^2.5.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" integrity sha1-bUUk6LlV+V1PW1iFHOId1y+06VI= -lru-cache@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-3.2.0.tgz#71789b3b7f5399bec8565dda38aa30d2a097efee" - integrity sha512-91gyOKTc2k66UG6kHiH4h3S2eltcPwE1STVfMYC/NG+nZwf8IIuiamfmpGZjpbbxzSyEJaLC0tNSmhjlQUTJow== - dependencies: - pseudomap "^1.0.1" - lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -14208,11 +14200,6 @@ map-or-similar@^1.5.0: resolved "https://registry.yarnpkg.com/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08" integrity sha1-beJlMXSt+12e3DPGnT6Sobdvrwg= -map-stream@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" - integrity sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g== - map-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" @@ -14760,7 +14747,7 @@ mkdirp-infer-owner@^2.0.0: infer-owner "^1.0.4" mkdirp "^1.0.3" -mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.0, mkdirp@~0.5.1: +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -15074,7 +15061,7 @@ node-status-codes@^1.0.0: resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f" integrity sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8= -nopt@^3.0.0, nopt@~3.0.1: +nopt@^3.0.0: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" integrity sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg== @@ -16223,13 +16210,6 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pause-stream@0.0.11: - version "0.0.11" - resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" - integrity sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A== - dependencies: - through "~2.3" - pbkdf2@^3.0.3: version "3.1.2" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" @@ -17298,11 +17278,6 @@ prr@~1.0.1: resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= -pseudomap@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== - psl@^1.1.28, psl@^1.1.33: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -18694,6 +18669,16 @@ schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" +schema-utils@^4.2.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.2.tgz#0c10878bf4a73fd2b1dfd14b9462b26788c806ae" + integrity sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + scss-comment-parser@^0.8.4: version "0.8.4" resolved "https://registry.yarnpkg.com/scss-comment-parser/-/scss-comment-parser-0.8.4.tgz#8e82c3fcf7fdbbb7f172f8955e2aa88b685f86d8" @@ -18720,7 +18705,7 @@ semver-regex@^1.0.0: resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-1.0.0.tgz#92a4969065f9c70c694753d55248fc68f8f652c9" integrity sha1-kqSWkGX5xwxpR1PVUkj8aPj2Usk= -"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.1.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: +"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== @@ -18920,11 +18905,6 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -sigmund@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" - integrity sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g== - signal-exit@3.0.7, signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -19180,13 +19160,6 @@ split2@^3.0.0: dependencies: readable-stream "^3.0.0" -split@0.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" - integrity sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA== - dependencies: - through "2" - split@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" @@ -19352,13 +19325,6 @@ stream-combiner2@^1.1.1: duplexer2 "~0.1.0" readable-stream "^2.0.2" -stream-combiner@~0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" - integrity sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw== - dependencies: - duplexer "~0.1.1" - stream-each@^1.1.0: version "1.2.3" resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" @@ -20095,7 +20061,7 @@ through2@^4.0.0: dependencies: readable-stream "3" -through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@~2.3, through@~2.3.1: +through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== From aae061c5a140dd7beb31750131a02e37e6d39506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matuzal=C3=A9m=20Teles?= Date: Wed, 14 May 2025 19:54:01 -0500 Subject: [PATCH 2/9] feat(@clayui/autocomplete): add loading indicator in infinite scroller and example in storybook --- .../clay-autocomplete/src/Autocomplete.tsx | 6 ++ .../stories/Autocomplete.stories.tsx | 63 +++++++++++++++++++ packages/clay-data-provider/src/index.ts | 2 +- 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/packages/clay-autocomplete/src/Autocomplete.tsx b/packages/clay-autocomplete/src/Autocomplete.tsx index ba57e33e50..a241e5f295 100644 --- a/packages/clay-autocomplete/src/Autocomplete.tsx +++ b/packages/clay-autocomplete/src/Autocomplete.tsx @@ -347,6 +347,7 @@ function AutocompleteInner | string | number>( const virtualizer = useVirtual({ estimateSize: 37, + isLoading, items: filteredItems, parentRef: menuRef, }); @@ -396,6 +397,11 @@ function AutocompleteInner | string | number>( [value] ), items: filteredItems, + load: ( + + + + ), notFound: ( { ); }; +export const InfiniteScroller = () => { + const [value, setValue] = useState(''); + + const [networkStatus, setNetworkStatus] = useState( + NetworkStatus.Unused + ); + const {loadMore, resource} = useResource({ + fetch: async (link, options) => { + const result = await fetch(link, options); + const json = await result.json(); + + return { + cursor: json.info.next, + items: json.results, + }; + }, + fetchPolicy: FetchPolicy.CacheFirst, + link: 'https://rickandmortyapi.com/api/character/', + onNetworkStatusChange: setNetworkStatus, + variables: {name: value}, + }); + + return ( +
+
+
+
+ + ) ?? []} + loadingState={networkStatus} + messages={{ + listCount: '{0} option available.', + listCountPlural: '{0} options available.', + loading: 'Loading...', + notFound: 'No results found', + }} + onChange={setValue} + onItemsChange={() => {}} + onLoadMore={loadMore} + placeholder="Enter a name" + value={value} + > + {(item) => ( + + {item.name} + + )} + +
+
+
+
+ ); +}; + export const Keyboard = () => { const inputRef = useRef(null); const [value, setValue] = useState(''); diff --git a/packages/clay-data-provider/src/index.ts b/packages/clay-data-provider/src/index.ts index c2b63d352e..7451e462d8 100644 --- a/packages/clay-data-provider/src/index.ts +++ b/packages/clay-data-provider/src/index.ts @@ -6,6 +6,6 @@ import {DataProvider} from './DataProvider'; import {FetchPolicy, NetworkStatus, Sorting, useResource} from './useResource'; -export type {FetchPolicy, NetworkStatus, Sorting}; +export {FetchPolicy, NetworkStatus, Sorting}; export {DataProvider, useResource}; export default DataProvider; From b4f495a082a6887f7d43476aeab48f2fa261f182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matuzal=C3=A9m=20Teles?= Date: Fri, 16 May 2025 13:15:59 -0500 Subject: [PATCH 3/9] feat(@clayui/autocomplete): add infinite scroller with keyboard in autocomplete --- packages/clay-autocomplete/src/Autocomplete.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/clay-autocomplete/src/Autocomplete.tsx b/packages/clay-autocomplete/src/Autocomplete.tsx index a241e5f295..9d50581f42 100644 --- a/packages/clay-autocomplete/src/Autocomplete.tsx +++ b/packages/clay-autocomplete/src/Autocomplete.tsx @@ -449,6 +449,14 @@ function AutocompleteInner | string | number>( } }, [active]); + useEffect(() => { + const lastKey = collection.getLastItem(); + + if (onLoadMore && !isLoading && activeDescendant === lastKey?.key) { + onLoadMore(); + } + }, [activeDescendant]); + const optionCount = collection.getItems().length; const lastSize = useRef(optionCount); From cd15aca849b00d842b4312d07d4e6daffb6051d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matuzal=C3=A9m=20Teles?= Date: Fri, 16 May 2025 13:58:55 -0500 Subject: [PATCH 4/9] chore(@clayui/autocomplete): fix imports --- packages/clay-autocomplete/stories/Autocomplete.stories.tsx | 6 +----- packages/clay-data-provider/src/index.ts | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/clay-autocomplete/stories/Autocomplete.stories.tsx b/packages/clay-autocomplete/stories/Autocomplete.stories.tsx index 0ca56c2e61..addd418540 100644 --- a/packages/clay-autocomplete/stories/Autocomplete.stories.tsx +++ b/packages/clay-autocomplete/stories/Autocomplete.stories.tsx @@ -4,11 +4,7 @@ */ import {Text, TextHighlight} from '@clayui/core'; -import {useResource} from '@clayui/data-provider'; -import { - FetchPolicy, - NetworkStatus, -} from '@clayui/data-provider/src/useResource'; +import {FetchPolicy, NetworkStatus, useResource} from '@clayui/data-provider'; import DropDown from '@clayui/drop-down'; import Layout from '@clayui/layout'; import {FocusScope, useDebounce} from '@clayui/shared'; diff --git a/packages/clay-data-provider/src/index.ts b/packages/clay-data-provider/src/index.ts index 7451e462d8..b2c7c516a6 100644 --- a/packages/clay-data-provider/src/index.ts +++ b/packages/clay-data-provider/src/index.ts @@ -6,6 +6,5 @@ import {DataProvider} from './DataProvider'; import {FetchPolicy, NetworkStatus, Sorting, useResource} from './useResource'; -export {FetchPolicy, NetworkStatus, Sorting}; -export {DataProvider, useResource}; +export {FetchPolicy, NetworkStatus, Sorting, DataProvider, useResource}; export default DataProvider; From cdd6a1bbe13f119e246eca9d103f12a9ed6d9fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matuzal=C3=A9m=20Teles?= Date: Fri, 16 May 2025 16:03:08 -0500 Subject: [PATCH 5/9] feat(@clayui/core): adds the Live Announcer as a singleton --- packages/clay-core/src/index.ts | 3 + .../src/live-announcer/LiveAnnouncer.tsx | 60 ++++++--------- .../clay-core/src/live-announcer/index.ts | 2 + .../clay-core/src/live-announcer/store.tsx | 74 +++++++++++++++++++ 4 files changed, 103 insertions(+), 36 deletions(-) create mode 100644 packages/clay-core/src/live-announcer/store.tsx diff --git a/packages/clay-core/src/index.ts b/packages/clay-core/src/index.ts index d11ed6e98d..39d0e48a43 100644 --- a/packages/clay-core/src/index.ts +++ b/packages/clay-core/src/index.ts @@ -26,6 +26,9 @@ export {FocusTrap} from './focus-trap'; export {Nav} from './nav'; export {Body, Cell, Head, Row, Table} from './table'; export {LanguagePicker} from './language-picker'; +export {LiveAnnouncer, Announcer} from './live-announcer'; + +export type {Log} from './live-announcer'; // Experimental components export * as __EXPERIMENTAL_MENU from './drop-down'; diff --git a/packages/clay-core/src/live-announcer/LiveAnnouncer.tsx b/packages/clay-core/src/live-announcer/LiveAnnouncer.tsx index 4c15648def..a9ea4c9b50 100644 --- a/packages/clay-core/src/live-announcer/LiveAnnouncer.tsx +++ b/packages/clay-core/src/live-announcer/LiveAnnouncer.tsx @@ -6,66 +6,54 @@ import React, { forwardRef, useCallback, + useEffect, useImperativeHandle, useState, } from 'react'; import {createPortal} from 'react-dom'; +import warning from 'warning'; -import {useIsMounted} from '../hooks/useIsMounted'; import {VisuallyHidden} from './VisuallyHidden'; +import {Announcer} from './store'; + +import type {Log} from './store'; export type AnnouncerAPI = { announce: (message: string, assertiveness?: 'assertive' | 'polite') => void; }; -type Log = { - id: number; - message: string; - assertiveness: 'assertive' | 'polite'; -}; - -const LIVEREGION_TIMEOUT_DELAY = 7000; - -let counter = 0; - -/** - * TODO: LiveAnnouncer should be a singleton. - */ export const LiveAnnouncer = forwardRef(function LiveAnnouncer( _, ref ) { const [logs, setLogs] = useState>([]); - const isMounted = useIsMounted(); + const onLogChange = useCallback((logs: Array) => setLogs(logs), []); + + useEffect(() => Announcer.subscribe(onLogChange), []); + + /** + * Use the Announcer store directly instead of the component API. + * @example + * import {Announcer} from '@clayui/core'; + * Announcer.announce('message', 'assertive'); + * @deprecated + */ const announce = useCallback( ( message: string, assertiveness: 'assertive' | 'polite' = 'assertive' ) => { - setLogs((logs) => { - counter++; - - return [ - ...logs, - { - assertiveness, - id: counter, - message, - }, - ]; - }); + warning( + false, + `Use the Announcer store directly instead of the component API. - setTimeout(() => { - if (isMounted()) { - setLogs((logs) => { - const newLogs = [...logs]; - newLogs.shift(); +@example +import {Announcer} from '@clayui/core'; +Announcer.announce('message', 'assertive');` + ); - return newLogs; - }); - } - }, LIVEREGION_TIMEOUT_DELAY); + Announcer.announce(message, assertiveness); }, [] ); diff --git a/packages/clay-core/src/live-announcer/index.ts b/packages/clay-core/src/live-announcer/index.ts index e51d3453fb..3b001550c8 100644 --- a/packages/clay-core/src/live-announcer/index.ts +++ b/packages/clay-core/src/live-announcer/index.ts @@ -5,5 +5,7 @@ export {LiveAnnouncer} from './LiveAnnouncer'; export {VisuallyHidden} from './VisuallyHidden'; +export {Announcer} from './store'; +export type {Log} from './store'; export type {AnnouncerAPI} from './LiveAnnouncer'; diff --git a/packages/clay-core/src/live-announcer/store.tsx b/packages/clay-core/src/live-announcer/store.tsx new file mode 100644 index 0000000000..6ef267a06b --- /dev/null +++ b/packages/clay-core/src/live-announcer/store.tsx @@ -0,0 +1,74 @@ +/** + * SPDX-FileCopyrightText: © 2025 Liferay, Inc. + * SPDX-License-Identifier: BSD-3-Clause + */ + +export type Log = { + id: number; + message: string; + assertiveness: 'assertive' | 'polite'; +}; + +type Subscriber = (logs: Array) => void; + +const LIVEREGION_TIMEOUT_DELAY = 7000; + +class LiveAnnouncerStore { + protected logs: Array = []; + protected counter: number = 0; + protected subscribers: Set = new Set(); + protected delay: number = LIVEREGION_TIMEOUT_DELAY; + + protected setLogs(logs: Array) { + this.logs = logs; + + if (this.subscribers.size === 0) { + console.warn( + 'There are no components listening to the live announcer.' + ); + } else { + this.subscribers.forEach((subscriber) => subscriber(this.logs)); + } + } + + public subscribe(subscriber: Subscriber) { + if (this.subscribers.has(subscriber)) { + console.warn('Subscriber already exists.'); + + return; + } + + this.subscribers.add(subscriber); + + return () => { + this.subscribers.delete(subscriber); + }; + } + + public announce( + message: string, + assertiveness: 'assertive' | 'polite' = 'assertive' + ) { + this.counter++; + this.setLogs([ + ...this.logs, + { + assertiveness, + id: this.counter, + message, + }, + ]); + + setTimeout(() => { + const logs = [...this.logs]; + logs.shift(); + this.setLogs(logs); + }, this.delay); + } + + public getMessages(type: 'assertive' | 'polite') { + return this.logs.filter((log) => log.assertiveness === type); + } +} + +export const Announcer = new LiveAnnouncerStore(); From 6dea2c2aabbc3e1c06e3b0994c9512a1099f51e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matuzal=C3=A9m=20Teles?= Date: Fri, 16 May 2025 16:04:03 -0500 Subject: [PATCH 6/9] chore(@clayui/autocomplete): Adds a warning in storybook examples for deprecated autocomplete examples --- .../stories/Autocomplete.stories.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/clay-autocomplete/stories/Autocomplete.stories.tsx b/packages/clay-autocomplete/stories/Autocomplete.stories.tsx index addd418540..6b7e0be3a4 100644 --- a/packages/clay-autocomplete/stories/Autocomplete.stories.tsx +++ b/packages/clay-autocomplete/stories/Autocomplete.stories.tsx @@ -364,6 +364,17 @@ export const Keyboard = () => {
+
+ This Autocomplete implementation uses the deprecated + implementation, we recommend using the{' '} + + new pattern + + . +
@@ -421,6 +432,17 @@ export const AsyncData = () => {
+
+ This Autocomplete implementation uses the deprecated + implementation, we recommend using the{' '} + + new pattern + + . +
From 0bdbf508561b53a2b632017bf5e4cd7c83289bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matuzal=C3=A9m=20Teles?= Date: Fri, 16 May 2025 22:39:49 -0500 Subject: [PATCH 7/9] feat(@clayui/autocomplete): adds live announcer for infinite scrolling --- .../clay-autocomplete/src/Autocomplete.tsx | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/packages/clay-autocomplete/src/Autocomplete.tsx b/packages/clay-autocomplete/src/Autocomplete.tsx index 9d50581f42..9a64e64ed2 100644 --- a/packages/clay-autocomplete/src/Autocomplete.tsx +++ b/packages/clay-autocomplete/src/Autocomplete.tsx @@ -3,10 +3,7 @@ * SPDX-License-Identifier: BSD-3-Clause */ -import { - __NOT_PUBLIC_COLLECTION, - __NOT_PUBLIC_LIVE_ANNOUNCER, -} from '@clayui/core'; +import {Announcer, LiveAnnouncer, __NOT_PUBLIC_COLLECTION} from '@clayui/core'; import DropDown from '@clayui/drop-down'; import {ClayInput as Input} from '@clayui/form'; import LoadingIndicator from '@clayui/loading-indicator'; @@ -27,10 +24,9 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {AutocompleteContext} from './Context'; -import type {AnnouncerAPI, ICollectionProps} from '@clayui/core'; +import type {ICollectionProps} from '@clayui/core'; const {Collection, useCollection, useVirtual} = __NOT_PUBLIC_COLLECTION; -const {LiveAnnouncer} = __NOT_PUBLIC_LIVE_ANNOUNCER; type ItemProps = { children: React.ReactElement; @@ -110,8 +106,11 @@ export interface IProps * Messages for autocomplete. */ messages?: { + infiniteScrollingListCountPlural?: string; listCount?: string; listCountPlural?: string; + infiniteScrollingLoaded: string; + infiniteScrollingLoading: string; loading: string; notFound: string; }; @@ -189,6 +188,9 @@ function hasItem | string | number>( const ESCAPE_REGEXP = /[.*+?^${}()|[\]\\]/g; const defaultMessages = { + infiniteScrollingListCountPlural: '{0} items loaded.', + infiniteScrollingLoaded: '{0} more items loaded.', + infiniteScrollingLoading: 'Loading more items.', listCount: '{0} option available.', listCountPlural: '{0} options available.', loading: 'Loading...', @@ -268,8 +270,6 @@ function AutocompleteInner | string | number>( const ariaControlsId = useId(); - const announcerAPI = useRef(null); - const isFirst = useIsFirstRender(); const filterFn = useCallback( @@ -460,12 +460,33 @@ function AutocompleteInner | string | number>( const optionCount = collection.getItems().length; const lastSize = useRef(optionCount); + const itemsSizeRef = useRef(items?.length); + + useEffect(() => { + itemsSizeRef.current = items?.length; + }, [items?.length]); + + // TODO: Move to Collection in the future and identify a better standard for + // all components. + useEffect(() => { + if (active && isLoading) { + Announcer.announce(messages!.infiniteScrollingLoading); + + return () => { + Announcer.announce( + sub(messages!.infiniteScrollingLoaded, [ + itemsSizeRef.current! - items!.length!, + ]) + ); + }; + } + }, [active, messages!.infiniteScrollingLoading, isLoading]); + useEffect(() => { // Only announces the number of options available when the menu is open // if there is no item with focus, with the exception of Voice Over // which does not include the message. if ( - announcerAPI.current && active && (!activeDescendant || isAppleDevice() || @@ -473,10 +494,12 @@ function AutocompleteInner | string | number>( ) { const optionCount = collection.getItems().length; - announcerAPI.current.announce( + Announcer.announce( sub( optionCount === 1 ? messages!.listCount! + : onLoadMore + ? messages!.infiniteScrollingListCountPlural! : messages!.listCountPlural!, [optionCount] ) @@ -504,7 +527,7 @@ function AutocompleteInner | string | number>( return ( <> - + Date: Sat, 17 May 2025 00:07:40 -0500 Subject: [PATCH 8/9] chore(@clayui/multi-select): adds types for new messages --- packages/clay-autocomplete/src/Autocomplete.tsx | 6 +++--- packages/clay-multi-select/src/MultiSelect.tsx | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/clay-autocomplete/src/Autocomplete.tsx b/packages/clay-autocomplete/src/Autocomplete.tsx index 9a64e64ed2..69012580ed 100644 --- a/packages/clay-autocomplete/src/Autocomplete.tsx +++ b/packages/clay-autocomplete/src/Autocomplete.tsx @@ -107,10 +107,10 @@ export interface IProps */ messages?: { infiniteScrollingListCountPlural?: string; + infiniteScrollingLoaded?: string; + infiniteScrollingLoading?: string; listCount?: string; listCountPlural?: string; - infiniteScrollingLoaded: string; - infiniteScrollingLoading: string; loading: string; notFound: string; }; @@ -474,7 +474,7 @@ function AutocompleteInner | string | number>( return () => { Announcer.announce( - sub(messages!.infiniteScrollingLoaded, [ + sub(messages!.infiniteScrollingLoaded!, [ itemsSizeRef.current! - items!.length!, ]) ); diff --git a/packages/clay-multi-select/src/MultiSelect.tsx b/packages/clay-multi-select/src/MultiSelect.tsx index babaf8ee24..20ede96fcf 100644 --- a/packages/clay-multi-select/src/MultiSelect.tsx +++ b/packages/clay-multi-select/src/MultiSelect.tsx @@ -167,6 +167,9 @@ export interface IProps = Item> * Messages for autocomplete. */ messages?: { + infiniteScrollingListCountPlural?: string; + infiniteScrollingLoaded?: string; + infiniteScrollingLoading?: string; listCount?: string; listCountPlural?: string; loading: string; @@ -270,6 +273,9 @@ export const MultiSelect = React.forwardRef(function MultiSelectInner< menuRenderer: MenuRenderer, messages = { hotkeys: 'Press backspace to delete the current row.', + infiniteScrollingListCountPlural: '{0} items loaded.', + infiniteScrollingLoaded: '{0} more items loaded.', + infiniteScrollingLoading: 'Loading more items.', labelAdded: 'Label {0} added to the list', labelRemoved: 'Label {0} removed to the list', listCount: '{0} option available.', From 0dea413737c369e2aa4137e6e407685eabf7a024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matuzal=C3=A9m=20Teles?= Date: Tue, 20 May 2025 02:00:08 -0500 Subject: [PATCH 9/9] chore(@clayui/autocomplete): fix lint error --- packages/clay-autocomplete/src/Autocomplete.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clay-autocomplete/src/Autocomplete.tsx b/packages/clay-autocomplete/src/Autocomplete.tsx index 69012580ed..e6560a5fd8 100644 --- a/packages/clay-autocomplete/src/Autocomplete.tsx +++ b/packages/clay-autocomplete/src/Autocomplete.tsx @@ -470,7 +470,7 @@ function AutocompleteInner | string | number>( // all components. useEffect(() => { if (active && isLoading) { - Announcer.announce(messages!.infiniteScrollingLoading); + Announcer.announce(messages!.infiniteScrollingLoading!); return () => { Announcer.announce(