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 { 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 (
<Field error={error} name={name} hint={description} id={id}>
<Relation
@ -231,13 +257,7 @@ const RelationInput = ({
onChange={(relation) => {
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 && (
<TextButton
disabled={paginatedRelations.isLoading || paginatedRelations.isFetchingNextPage}
onClick={() => onRelationLoadMore()}
onClick={handleLoadMore}
loading={paginatedRelations.isLoading || paginatedRelations.isFetchingNextPage}
startIcon={<Refresh />}
>

View File

@ -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(
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<IntlProvider locale="en">
<RelationInput
description="this is a description"
id="1"
name="some-relation-1"
label="Some Relation"
labelLoadMore="Load more"
loadingMessage="Relations are loading"
labelDisconnectRelation="Remove"
numberOfRelationsToDisplay={5}
noRelationsMessage="No relations available"
onRelationConnect={() => 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}
/>
</IntlProvider>
</ThemeProvider>
</MemoryRouter>
);
const Component = (props) => (
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<IntlProvider locale="en">
<RelationInput
description="this is a description"
id="1"
name="some-relation-1"
label="Some Relation"
labelLoadMore="Load more"
loadingMessage="Relations are loading"
labelDisconnectRelation="Remove"
numberOfRelationsToDisplay={5}
noRelationsMessage="No relations available"
onRelationConnect={() => 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}
/>
</IntlProvider>
</ThemeProvider>
</MemoryRouter>
);
const setup = (props) => render(<Component {...props} />);
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(
<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
// test.only('should call onSearchNextPage', () => {
// const spy = jest.fn();

View File

@ -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,

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 useSyncRbac } from './useSyncRbac';
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;
};