Skip to content

Commit

Permalink
UIIN-2267: Browse Lists | Focus updates (#2362)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dmytro-Melnyshyn authored Nov 30, 2023
1 parent b6a3b3b commit 6f96fbe
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* Don't render Fast Add record modal in a `<Paneset>` to re-calculate other `<Pane>`'s widths after closing. Fixes UIIN-2690.
* "Saving instance failed" modal does not show error message. Fixes UIIN-2686.
* Make browse result items that are not anchors and have no records not clickable, and show 0 in number of titles. Fixes UIIN-2699.
* Browse Lists | Focus updates. Fixes UIIN-2267.

## [10.0.6](https://github.com/folio-org/ui-inventory/tree/v10.0.6) (2023-11-24)
[Full Changelog](https://github.com/folio-org/ui-inventory/compare/v10.0.5...v10.0.6)
Expand Down
3 changes: 3 additions & 0 deletions src/components/BrowseResultsPane/BrowseResultsPane.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const BrowseResultsPane = ({
isFetching,
isFiltersOpened,
pagination,
paneTitleRef,
toggleFiltersPane,
totalRecords,
}) => {
Expand Down Expand Up @@ -78,6 +79,7 @@ const BrowseResultsPane = ({
data-testid="browse-results-pane"
id="browse-inventory-results-pane"
padContent={false}
paneTitleRef={paneTitleRef}
defaultWidth="fill"
appIcon={<AppIcon app={namespace} />}
paneTitle={<FormattedMessage id="ui-inventory.title.browseCall" />}
Expand All @@ -102,6 +104,7 @@ BrowseResultsPane.propTypes = {
filters: PropTypes.object.isRequired,
isFetching: PropTypes.bool,
isFiltersOpened: PropTypes.bool.isRequired,
paneTitleRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
pagination: PropTypes.shape({
hasPrevPage: PropTypes.bool,
hasNextPage: PropTypes.bool,
Expand Down
10 changes: 10 additions & 0 deletions src/components/InstancesList/InstancesList.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ class InstancesList extends React.Component {
constructor(props) {
super(props);

this.paneTitleRef = React.createRef();

this.state = {
showNewFastAddModal: false,
inTransitItemsExportInProgress: false,
Expand All @@ -177,8 +179,11 @@ class InstancesList extends React.Component {
const {
history,
namespace,
getParams,
} = this.props;

const params = getParams();

this.unlisten = history.listen((location) => {
const hasReset = new URLSearchParams(location.search).get('reset');

Expand All @@ -191,6 +196,10 @@ class InstancesList extends React.Component {
}
});

if (params.selectedBrowseResult === 'true') {
this.paneTitleRef.current.focus();
}

this.processLastSearchTerms();

registerLogoutListener(this.clearStorage, namespace, 'instances-list-logout', history);
Expand Down Expand Up @@ -1218,6 +1227,7 @@ class InstancesList extends React.Component {
editRecordComponent={InstanceForm}
onChangeIndex={onChangeIndex}
onSelectRow={this.onSelectRow}
paneTitleRef={this.paneTitleRef}
newRecordInitialValues={(this.state && this.state.copiedInstance) ? this.state.copiedInstance : {
discoverySuppress: false,
staffSuppress: false,
Expand Down
2 changes: 2 additions & 0 deletions src/hooks/useInventoryBrowse/useInventoryBrowse.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const useInventoryBrowse = ({

const {
data = {},
isFetched,
isFetching,
isLoading,
} = useQuery(
Expand Down Expand Up @@ -127,6 +128,7 @@ const useInventoryBrowse = ({

return {
data: data.items,
isFetched,
isFetching,
isLoading,
pagination: {
Expand Down
23 changes: 22 additions & 1 deletion src/views/BrowseInventory/BrowseInventory.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ const BrowseInventory = () => {
const { isFiltersOpened, toggleFilters } = useFiltersToogle(`${namespace}/filters`);
const { deleteItemToView } = useItemToView('browse');
const [pageConfig, setPageConfig] = useState(getLastBrowseOffset());
const hasFocusedSearchOptionOnMount = useRef(false);
const inputRef = useRef();
const indexRef = useRef();
const paneTitleRef = useRef();

const { search } = location;

Expand Down Expand Up @@ -81,6 +84,7 @@ const BrowseInventory = () => {

const {
data,
isFetched,
isFetching,
pagination,
totalRecords,
Expand Down Expand Up @@ -128,7 +132,10 @@ const BrowseInventory = () => {
const onApplySearch = useCallback(() => {
const isSearchQueryValid = validateDataQuery(searchQuery);

if (isSearchQueryValid) applySearch();
if (isSearchQueryValid) {
applySearch();
paneTitleRef.current.focus();
}
}, [searchQuery, filters]);

const onChangeSearchIndex = useCallback((e) => {
Expand All @@ -141,6 +148,17 @@ const BrowseInventory = () => {
inputRef.current.focus();
}, [inputRef.current]);

useEffect(() => {
if (hasFocusedSearchOptionOnMount.current) {
return;
}

if (!search || (search && isFetched && !isFetching)) {
indexRef.current.focus();
hasFocusedSearchOptionOnMount.current = true;
}
}, [isFetched, search, isFetching]);

return (
<PersistedPaneset
appId={namespace}
Expand All @@ -156,6 +174,7 @@ const BrowseInventory = () => {
/>

<SingleSearchForm
autoFocus={false}
applySearch={onApplySearch}
changeSearch={changeSearch}
disabled={!searchIndex}
Expand All @@ -167,6 +186,7 @@ const BrowseInventory = () => {
selectedIndex={searchIndex}
searchableIndexesPlaceholder={searchableIndexesPlaceholder}
inputType="textarea"
indexRef={indexRef}
inputRef={inputRef}
/>

Expand All @@ -189,6 +209,7 @@ const BrowseInventory = () => {
isFetching={isFetching}
isFiltersOpened={isFiltersOpened}
pagination={pagination}
paneTitleRef={paneTitleRef}
toggleFiltersPane={toggleFilters}
totalRecords={totalRecords}
/>
Expand Down
107 changes: 95 additions & 12 deletions src/views/BrowseInventory/BrowseInventory.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import '../../../test/jest/__mock__';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import userEvent from '@folio/jest-config-stripes/testing-library/user-event';
import { act, screen, fireEvent } from '@folio/jest-config-stripes/testing-library/react';
import { act, screen, fireEvent, waitFor } from '@folio/jest-config-stripes/testing-library/react';

import { useLocationFilters } from '@folio/stripes-acq-components';
import stripesAcqComponents from '@folio/stripes-acq-components';

import {
renderWithIntl,
Expand All @@ -20,10 +20,13 @@ import {
useInventoryBrowse,
} from '../../hooks';

const { useLocationFilters } = stripesAcqComponents;

const mockGetLastSearch = jest.fn();
const mockGetLastBrowseOffset = jest.fn().mockImplementation(() => INIT_PAGE_CONFIG);
const mockStoreLastBrowse = jest.fn();
const mockStoreLastBrowseOffset = jest.fn();
const mockFocusPaneTitle = jest.fn();

jest.mock('../../storage');
jest.mock('@folio/stripes-acq-components', () => ({
Expand All @@ -32,7 +35,10 @@ jest.mock('@folio/stripes-acq-components', () => ({
}));
jest.mock('../../components', () => ({
BrowseInventoryFilters: jest.fn(() => <>BrowseInventoryFilters</>),
BrowseResultsPane: jest.fn(() => <>BrowseResultsPane</>),
BrowseResultsPane: jest.fn(({ paneTitleRef }) => {
paneTitleRef.current = { focus: mockFocusPaneTitle };
return <>BrowseResultsPane</>;
}),
SearchModeNavigation: jest.fn(() => <>SearchModeNavigation</>),
}));
jest.mock('../../hooks', () => ({
Expand Down Expand Up @@ -81,22 +87,19 @@ const getFiltersUtils = ({
describe('BrowseInventory', () => {
beforeEach(() => {
history = createMemoryHistory();
applySearch.mockClear();
applyFilters.mockClear();
changeSearch.mockClear();
resetFilters.mockClear();
changeSearchIndex.mockClear();
mockGetLastSearch.mockClear();
mockGetLastBrowseOffset.mockClear();
mockStoreLastBrowse.mockClear();
mockStoreLastBrowseOffset.mockClear();
jest.clearAllMocks();
useLocationFilters.mockClear().mockReturnValue(getFiltersUtils());
useLastSearchTerms.mockClear().mockReturnValue({
getLastSearch: mockGetLastSearch,
getLastBrowseOffset: mockGetLastBrowseOffset,
storeLastBrowse: mockStoreLastBrowse,
storeLastBrowseOffset: mockStoreLastBrowseOffset,
});
useInventoryBrowse.mockReturnValue({
data: [],
isFetched: false,
isFetching: false,
});
});

describe('when the component is mounted', () => {
Expand Down Expand Up @@ -181,6 +184,86 @@ describe('BrowseInventory', () => {
expect(applySearch).not.toHaveBeenCalled();
});

describe('when a new search query is submitted', () => {
it('should focus pane title', async () => {
const { getByRole } = renderBrowseInventory();

userEvent.type(getByRole('textbox'), 'newQuery');
userEvent.click(getByRole('button', { name: 'Search' }));

await waitFor(() => {
expect(mockFocusPaneTitle).toHaveBeenCalled();
});
});
});

describe('when page is mounted without predefined search', () => {
it('should focus search option once', async () => {
const mockSearchOptionFocus = jest.fn();

jest.spyOn(stripesAcqComponents, 'SingleSearchForm')
.mockImplementation(({ indexRef }) => {
indexRef.current = { focus: mockSearchOptionFocus };
return <>SingleSearchForm</>;
});

const { rerender } = renderBrowseInventory();

useInventoryBrowse.mockReturnValue({
isFetching: false,
});

renderBrowseInventory({}, rerender);

expect(mockSearchOptionFocus).toHaveBeenCalledTimes(1);

jest.spyOn(stripesAcqComponents, 'SingleSearchForm').mockRestore();
});
});

describe('when page is mounted and data of last search is loaded', () => {
it('should focus search option', async () => {
const mockSearchOptionFocus = jest.fn();

jest.spyOn(stripesAcqComponents, 'SingleSearchForm')
.mockImplementation(({ indexRef }) => {
indexRef.current = { focus: mockSearchOptionFocus };
return <>SingleSearchForm</>;
});

history.push({ search: '?qindex=browseSubjects&query=book' });

useInventoryBrowse.mockReturnValue({
isFetched: false,
isFetching: true,
});

const { rerender } = renderBrowseInventory();

expect(mockSearchOptionFocus).not.toHaveBeenCalled();

useInventoryBrowse.mockReturnValue({
isFetched: true,
isFetching: true,
});

renderBrowseInventory({}, rerender);

expect(mockSearchOptionFocus).not.toHaveBeenCalled();

useInventoryBrowse.mockReturnValue({
isFetched: true,
isFetching: false,
});

renderBrowseInventory({}, rerender);

expect(mockSearchOptionFocus).toHaveBeenCalledTimes(1);

jest.spyOn(stripesAcqComponents, 'SingleSearchForm').mockRestore();
});
});

describe('when the selected qindex is one of those that should comprise the callNumberType param', () => {
it('should be added to the useInventoryBrowse filters', () => {
const filters = {
Expand Down

0 comments on commit 6f96fbe

Please sign in to comment.