Merge pull request #14801 from strapi/fix/relations/scroll-to-end

This commit is contained in:
Josh 2022-11-08 12:28:07 +00:00 committed by GitHub
commit 1daf8dd67a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 165 additions and 44 deletions

View File

@ -21,6 +21,7 @@ import { RelationItem } from './components/RelationItem';
import { RelationList } from './components/RelationList'; import { RelationList } from './components/RelationList';
import { Option } from './components/Option'; import { Option } from './components/Option';
import { RELATION_ITEM_HEIGHT } from './constants'; import { RELATION_ITEM_HEIGHT } from './constants';
import { usePrev } from '../../hooks';
const LinkEllipsis = styled(Link)` const LinkEllipsis = styled(Link)`
white-space: nowrap; white-space: nowrap;
@ -204,6 +205,31 @@ const RelationInput = ({
onSearch(); 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 ( return (
<Field error={error} name={name} hint={description} id={id}> <Field error={error} name={name} hint={description} id={id}>
<Relation <Relation
@ -231,13 +257,7 @@ const RelationInput = ({
onChange={(relation) => { onChange={(relation) => {
setValue(null); setValue(null);
onRelationConnect(relation); onRelationConnect(relation);
updatedRelationsWith.current = 'onChange';
// scroll to the end of the list
if (relations.length > 0) {
setTimeout(() => {
listRef.current.scrollToItem(relations.length, 'end');
});
}
}} }}
onInputChange={(value) => { onInputChange={(value) => {
setValue(value); setValue(value);
@ -262,7 +282,7 @@ const RelationInput = ({
shouldDisplayLoadMoreButton && ( shouldDisplayLoadMoreButton && (
<TextButton <TextButton
disabled={paginatedRelations.isLoading || paginatedRelations.isFetchingNextPage} disabled={paginatedRelations.isLoading || paginatedRelations.isFetchingNextPage}
onClick={() => onRelationLoadMore()} onClick={handleLoadMore}
loading={paginatedRelations.isLoading || paginatedRelations.isFetchingNextPage} loading={paginatedRelations.isLoading || paginatedRelations.isFetchingNextPage}
startIcon={<Refresh />} startIcon={<Refresh />}
> >

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { IntlProvider } from 'react-intl'; import { IntlProvider } from 'react-intl';
import { MemoryRouter } from 'react-router-dom'; 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 { ThemeProvider, lightTheme } from '@strapi/design-system';
import { RelationInput } from '../index'; import { RelationInput } from '../index';
@ -45,40 +45,41 @@ const FIXTURES_SEARCH = {
isSuccess: true, isSuccess: true,
}; };
const setup = (props) => const Component = (props) => (
render( <MemoryRouter>
<MemoryRouter> <ThemeProvider theme={lightTheme}>
<ThemeProvider theme={lightTheme}> <IntlProvider locale="en">
<IntlProvider locale="en"> <RelationInput
<RelationInput description="this is a description"
description="this is a description" id="1"
id="1" name="some-relation-1"
name="some-relation-1" label="Some Relation"
label="Some Relation" labelLoadMore="Load more"
labelLoadMore="Load more" loadingMessage="Relations are loading"
loadingMessage="Relations are loading" labelDisconnectRelation="Remove"
labelDisconnectRelation="Remove" numberOfRelationsToDisplay={5}
numberOfRelationsToDisplay={5} noRelationsMessage="No relations available"
noRelationsMessage="No relations available" onRelationConnect={() => jest.fn()}
onRelationConnect={() => jest.fn()} onRelationDisconnect={() => jest.fn()}
onRelationDisconnect={() => jest.fn()} onRelationLoadMore={() => jest.fn()}
onRelationLoadMore={() => jest.fn()} onSearch={() => jest.fn()}
onSearch={() => jest.fn()} onSearchNextPage={() => jest.fn()}
onSearchNextPage={() => jest.fn()} placeholder="Select..."
placeholder="Select..." publicationStateTranslations={{
publicationStateTranslations={{ draft: 'Draft',
draft: 'Draft', published: 'Published',
published: 'Published', }}
}} relations={FIXTURES_RELATIONS}
relations={FIXTURES_RELATIONS} searchResults={FIXTURES_SEARCH}
searchResults={FIXTURES_SEARCH} size={8}
size={8} {...props}
{...props} />
/> </IntlProvider>
</IntlProvider> </ThemeProvider>
</ThemeProvider> </MemoryRouter>
</MemoryRouter> );
);
const setup = (props) => render(<Component {...props} />);
describe('Content-Manager || RelationInput', () => { describe('Content-Manager || RelationInput', () => {
test('should render and match snapshot', () => { test('should render and match snapshot', () => {
@ -146,6 +147,65 @@ describe('Content-Manager || RelationInput', () => {
expect(spy).toHaveBeenCalled(); 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(
<Component
relations={{
...FIXTURES_RELATIONS,
data: [...data, newRelation],
}}
/>
);
await waitFor(() => expect(el.parentNode.scrollTop).toBeGreaterThan(0));
fireEvent.click(screen.getByText('Load more'));
rerender(
<Component
relations={{
...FIXTURES_RELATIONS,
data: [
{ id: 7, mainField: 'Relation 7', publicationState: false },
...data,
newRelation,
],
}}
/>
);
await waitFor(() => expect(el.parentNode.scrollTop).toBe(0));
});
// TODO: check if it is possible to fire scroll event here // TODO: check if it is possible to fire scroll event here
// test.only('should call onSearchNextPage', () => { // test.only('should call onSearchNextPage', () => {
// const spy = jest.fn(); // const spy = jest.fn();

View File

@ -42,7 +42,7 @@ export const RelationInputDataManager = ({
const relationsFromModifiedData = get(modifiedData, name) ?? []; 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 ?? ''}`, { const { relations, search, searchFor } = useRelation(`${slug}-${name}-${initialData?.id ?? ''}`, {
name, name,

View File

@ -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);
});
});

View File

@ -5,3 +5,4 @@ export { default as useLayoutDnd } from './useLayoutDnd';
export { default as usePluginsQueryParams } from './usePluginsQueryParams'; export { default as usePluginsQueryParams } from './usePluginsQueryParams';
export { default as useSyncRbac } from './useSyncRbac'; export { default as useSyncRbac } from './useSyncRbac';
export { default as useWysiwyg } from './useWysiwyg'; export { default as useWysiwyg } from './useWysiwyg';
export { usePrev } from './usePrev';

View File

@ -0,0 +1,14 @@
import { useEffect, useRef } from 'react';
/**
* @type {<T>(value: T) => T | undefined}
*/
export const usePrev = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};