mirror of
https://github.com/strapi/strapi.git
synced 2025-09-25 16:29:34 +00:00
Merge pull request #14801 from strapi/fix/relations/scroll-to-end
This commit is contained in:
commit
1daf8dd67a
@ -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 />}
|
||||
>
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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';
|
||||
|
@ -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;
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user