diff --git a/packages/core/admin/admin/src/content-manager/components/RelationInput/RelationInput.js b/packages/core/admin/admin/src/content-manager/components/RelationInput/RelationInput.js index 0172cc5a06..7b1b5a7a6d 100644 --- a/packages/core/admin/admin/src/content-manager/components/RelationInput/RelationInput.js +++ b/packages/core/admin/admin/src/content-manager/components/RelationInput/RelationInput.js @@ -21,6 +21,7 @@ import { RelationItem } from './components/RelationItem'; import { RelationList } from './components/RelationList'; import { Option } from './components/Option'; import { RELATION_ITEM_HEIGHT } from './constants'; +import { usePrev } from '../../hooks'; const LinkEllipsis = styled(Link)` white-space: nowrap; @@ -204,6 +205,31 @@ const RelationInput = ({ onSearch(); }; + const previewRelationsLength = usePrev(relations.length); + /** + * @type {React.MutableRefObject<'onChange' | 'loadMore'>} + */ + const updatedRelationsWith = useRef(); + + const handleLoadMore = () => { + updatedRelationsWith.current = 'loadMore'; + onRelationLoadMore(); + }; + + useEffect(() => { + if ( + updatedRelationsWith.current === 'onChange' && + relations.length !== previewRelationsLength + ) { + listRef.current.scrollToItem(relations.length, 'end'); + } else if ( + updatedRelationsWith.current === 'loadMore' && + relations.length !== previewRelationsLength + ) { + listRef.current.scrollToItem(0, 'start'); + } + }, [previewRelationsLength, relations]); + return ( { setValue(null); onRelationConnect(relation); - - // scroll to the end of the list - if (relations.length > 0) { - setTimeout(() => { - listRef.current.scrollToItem(relations.length, 'end'); - }); - } + updatedRelationsWith.current = 'onChange'; }} onInputChange={(value) => { setValue(value); @@ -262,7 +282,7 @@ const RelationInput = ({ shouldDisplayLoadMoreButton && ( onRelationLoadMore()} + onClick={handleLoadMore} loading={paginatedRelations.isLoading || paginatedRelations.isFetchingNextPage} startIcon={} > diff --git a/packages/core/admin/admin/src/content-manager/components/RelationInput/tests/RelationInput.test.js b/packages/core/admin/admin/src/content-manager/components/RelationInput/tests/RelationInput.test.js index 072468c4b0..199e108ee5 100644 --- a/packages/core/admin/admin/src/content-manager/components/RelationInput/tests/RelationInput.test.js +++ b/packages/core/admin/admin/src/content-manager/components/RelationInput/tests/RelationInput.test.js @@ -1,7 +1,7 @@ import React from 'react'; import { IntlProvider } from 'react-intl'; import { MemoryRouter } from 'react-router-dom'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { ThemeProvider, lightTheme } from '@strapi/design-system'; import { RelationInput } from '../index'; @@ -45,40 +45,41 @@ const FIXTURES_SEARCH = { isSuccess: true, }; -const setup = (props) => - render( - - - - jest.fn()} - onRelationDisconnect={() => jest.fn()} - onRelationLoadMore={() => jest.fn()} - onSearch={() => jest.fn()} - onSearchNextPage={() => jest.fn()} - placeholder="Select..." - publicationStateTranslations={{ - draft: 'Draft', - published: 'Published', - }} - relations={FIXTURES_RELATIONS} - searchResults={FIXTURES_SEARCH} - size={8} - {...props} - /> - - - - ); +const Component = (props) => ( + + + + jest.fn()} + onRelationDisconnect={() => jest.fn()} + onRelationLoadMore={() => jest.fn()} + onSearch={() => jest.fn()} + onSearchNextPage={() => jest.fn()} + placeholder="Select..." + publicationStateTranslations={{ + draft: 'Draft', + published: 'Published', + }} + relations={FIXTURES_RELATIONS} + searchResults={FIXTURES_SEARCH} + size={8} + {...props} + /> + + + +); + +const setup = (props) => render(); describe('Content-Manager || RelationInput', () => { test('should render and match snapshot', () => { @@ -146,6 +147,65 @@ describe('Content-Manager || RelationInput', () => { expect(spy).toHaveBeenCalled(); }); + test('should scroll to the bottom when a new relation has been added & scroll to the top when load more is clicked', async () => { + const data = [ + ...FIXTURES_RELATIONS.data, + { id: 4, mainField: 'Relation 4', publicationState: 'draft' }, + { id: 5, mainField: 'Relation 5', publicationState: 'draft' }, + ]; + + const newRelation = { id: 6, mainField: 'Relation 6', publicationState: 'draft' }; + + const { rerender } = setup({ + relations: { + ...FIXTURES_RELATIONS, + data, + }, + searchResults: { + ...FIXTURES_SEARCH, + data: [newRelation], + }, + }); + + const el = screen.getByRole('list'); + + expect(el.parentNode.scrollTop).toBe(0); + + fireEvent.mouseDown(screen.getByText(/select\.\.\./i)); + + await waitFor(() => expect(screen.getByText('Relation 6')).toBeInTheDocument()); + + fireEvent.click(screen.getByText('Relation 6')); + + rerender( + + ); + + await waitFor(() => expect(el.parentNode.scrollTop).toBeGreaterThan(0)); + + fireEvent.click(screen.getByText('Load more')); + + rerender( + + ); + + await waitFor(() => expect(el.parentNode.scrollTop).toBe(0)); + }); + // TODO: check if it is possible to fire scroll event here // test.only('should call onSearchNextPage', () => { // const spy = jest.fn(); diff --git a/packages/core/admin/admin/src/content-manager/components/RelationInputDataManager/RelationInputDataManager.js b/packages/core/admin/admin/src/content-manager/components/RelationInputDataManager/RelationInputDataManager.js index a8b0883abc..91004b657a 100644 --- a/packages/core/admin/admin/src/content-manager/components/RelationInputDataManager/RelationInputDataManager.js +++ b/packages/core/admin/admin/src/content-manager/components/RelationInputDataManager/RelationInputDataManager.js @@ -42,7 +42,7 @@ export const RelationInputDataManager = ({ const relationsFromModifiedData = get(modifiedData, name) ?? []; - const currentLastPage = Math.ceil(relationsFromModifiedData.length / RELATIONS_TO_DISPLAY); + const currentLastPage = Math.ceil(get(initialData, name, []).length / RELATIONS_TO_DISPLAY); const { relations, search, searchFor } = useRelation(`${slug}-${name}-${initialData?.id ?? ''}`, { name, diff --git a/packages/core/admin/admin/src/content-manager/hooks/__test__/usePrev.test.js b/packages/core/admin/admin/src/content-manager/hooks/__test__/usePrev.test.js new file mode 100644 index 0000000000..2f1a38bdf1 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/hooks/__test__/usePrev.test.js @@ -0,0 +1,26 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { usePrev } from '../usePrev'; + +describe('usePrev', () => { + const setup = () => renderHook(({ state }) => usePrev(state), { initialProps: { state: 0 } }); + + it('should return undefined on initial render', () => { + const { result } = setup(); + + expect(result.current).toBeUndefined(); + }); + + it('should always return previous state after each update', () => { + const { result, rerender } = setup(); + + rerender({ state: 2 }); + expect(result.current).toBe(0); + + rerender({ state: 4 }); + expect(result.current).toBe(2); + + rerender({ state: 6 }); + expect(result.current).toBe(4); + }); +}); diff --git a/packages/core/admin/admin/src/content-manager/hooks/index.js b/packages/core/admin/admin/src/content-manager/hooks/index.js index 7f6072b948..38f451b46a 100644 --- a/packages/core/admin/admin/src/content-manager/hooks/index.js +++ b/packages/core/admin/admin/src/content-manager/hooks/index.js @@ -5,3 +5,4 @@ export { default as useLayoutDnd } from './useLayoutDnd'; export { default as usePluginsQueryParams } from './usePluginsQueryParams'; export { default as useSyncRbac } from './useSyncRbac'; export { default as useWysiwyg } from './useWysiwyg'; +export { usePrev } from './usePrev'; diff --git a/packages/core/admin/admin/src/content-manager/hooks/usePrev.js b/packages/core/admin/admin/src/content-manager/hooks/usePrev.js new file mode 100644 index 0000000000..ac15552d8e --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/hooks/usePrev.js @@ -0,0 +1,14 @@ +import { useEffect, useRef } from 'react'; + +/** + * @type {(value: T) => T | undefined} + */ +export const usePrev = (value) => { + const ref = useRef(); + + useEffect(() => { + ref.current = value; + }, [value]); + + return ref.current; +};