Skip to content

Commit

Permalink
STCOM-1288: Put focus on the last modified non-empty field after clos…
Browse files Browse the repository at this point in the history
…ing and opening <AdvancedSearch>, otherwise on the last query field.
  • Loading branch information
Dmytro-Melnyshyn committed May 2, 2024
1 parent ee7cea4 commit 4080129
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Validate ref in `Paneset` before dereferencing it. Refs STCOM-1235.
* Resolve bug with form control validation styles not rendering. Adjusted order of nested selectors. Refs STCOM-1284.
* `<MultiSelection/>`'s overlay will use the overlay container as its boundary when the `renderToOverlay` prop is applied, as opposed to the scrollParent of the control. Refs STCOM-1282.
* Put focus on the last modified non-empty field after closing and opening `<AdvancedSearch>`, otherwise on the last query field. Refs STCOM-1288.

## [12.1.0](https://github.com/folio-org/stripes-components/tree/v12.1.0) (2024-03-12)
[Full Changelog](https://github.com/folio-org/stripes-components/compare/v12.0.0...v12.1.0)
Expand Down
2 changes: 2 additions & 0 deletions lib/AdvancedSearch/AdvancedSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const AdvancedSearch = ({
rowState,
searchOptionsWithQuery,
showEmptyFirstRowMessage,
activeQueryFieldIndex,
} = useAdvancedSearch({
defaultSearchOptionValue,
firstRowInitialSearch,
Expand All @@ -74,6 +75,7 @@ const AdvancedSearch = ({
key={`$advanced-search-row-${index}`}
index={index}
rowState={rowState[index]}
isActive={index === activeQueryFieldIndex}
searchOptions={hasQueryOption ? searchOptionsWithQuery : searchOptions}
onChange={onChange}
errorMessage={index === 0 && showEmptyFirstRowMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const propTypes = {
errorMessage: PropTypes.string,
hasMatchSelection: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired,
isActive: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
rowState: PropTypes.shape({
[FIELD_NAMES.BOOL]: PropTypes.string.isRequired,
Expand All @@ -40,6 +41,7 @@ const TEXTAREA_HEIGHT = 1;

const AdvancedSearchRow = ({
index,
isActive,
rowState,
searchOptions,
onChange,
Expand Down Expand Up @@ -86,12 +88,13 @@ const AdvancedSearchRow = ({
</Col>
<Col xs>
<TextArea
isCursorAtEnd
rows={TEXTAREA_HEIGHT}
data-test-advanced-search-query
aria-label={intl.formatMessage({ id: 'stripes-components.advancedSearch.field.label' })}
onChange={(e) => onChange(index, FIELD_NAMES.QUERY, e.target.value)}
value={rowState[FIELD_NAMES.QUERY]}
autoFocus={index === 0}
autoFocus={isActive}
data-testid="advanced-search-query"
/>
<span className={styles.emptyRowErrorMessage}>{errorMessage}</span>
Expand Down
159 changes: 158 additions & 1 deletion lib/AdvancedSearch/hooks/tests/useAdvancedSearch-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
} from 'mocha';
import { expect } from 'chai';

import getHookExecutionResult from "../../../../tests/helpers/getHookExecutionResult";
import getHookExecutionResult from '../../../../tests/helpers/getHookExecutionResult';
import { waitFor } from '../../../../tests/helpers/waitFor';
import useAdvancedSearch from '../useAdvancedSearch';

describe('useAdvancedSearch', () => {
Expand Down Expand Up @@ -45,4 +46,160 @@ describe('useAdvancedSearch', () => {
expect(hookResult.filledRows[0].searchOption).to.equal('keyword');
});
});

describe('when reset is hit and user opens the modal', () => {
it('should focus the query field of the first row', async () => {
const firstRowInitialSearch = {
query: '',
option: '',
};

const args = {
defaultSearchOptionValue: 'keyword',
firstRowInitialSearch,
open: true,
};

const hookResult = await getHookExecutionResult(useAdvancedSearch, args);

hookResult.onChange(1, 'query', 'test');
await waitFor(() => hookResult.activeQueryFieldIndex === 1);
hookResult.resetRows();
await waitFor(() => hookResult.activeQueryFieldIndex === 0);

expect(hookResult.activeQueryFieldIndex).to.equal(0);
});
});

describe('when query is changed and next time user opens the modal', () => {
it('should focus the changed query field', async () => {
const firstRowInitialSearch = {
query: '',
option: '',
};

const args = {
defaultSearchOptionValue: 'keyword',
firstRowInitialSearch,
open: true,
};

const hookResult = await getHookExecutionResult(useAdvancedSearch, args);

hookResult.onChange(0, 'query', 'test1');
hookResult.onChange(1, 'query', 'test2');
await waitFor(() => hookResult.activeQueryFieldIndex === 1);

expect(hookResult.activeQueryFieldIndex).to.equal(1);
});
});

describe('when query is removed and next time user opens the modal', () => {
it('should focus to the last field with a non-empty query', async () => {
const firstRowInitialSearch = {
query: '',
option: '',
};

const args = {
defaultSearchOptionValue: 'keyword',
firstRowInitialSearch,
open: true,
};

const hookResult = await getHookExecutionResult(useAdvancedSearch, args);

hookResult.onChange(0, 'query', 'test1');
hookResult.onChange(1, 'query', 'test2');
hookResult.onChange(2, 'query', 'test3');
await waitFor(() => hookResult.activeQueryFieldIndex === 2);
hookResult.onChange(0, 'query', '');
await waitFor(() => hookResult.activeQueryFieldIndex === 1);

expect(hookResult.activeQueryFieldIndex).to.equal(1);
});
});

describe('when user opens the modal the first time', () => {
it('should focus to the last field with a non-empty query', async () => {
const firstRowInitialSearch = {
query: 'keyword containsAll test1 and contributor containsAll test2',
option: 'advancedSearch',
};

const queryToRow = () => [
{
bool: '',
searchOption: 'keyword',
match: 'containsAll',
query: 'test1',
},
{
bool: 'and',
searchOption: 'contributor',
match: 'containsAll',
query: 'test2',
}
];

const args = {
defaultSearchOptionValue: 'keyword',
firstRowInitialSearch,
queryToRow,
open: true,
};

const hookResult = await getHookExecutionResult(useAdvancedSearch, args);

await waitFor(() => hookResult.activeQueryFieldIndex === 1);

expect(hookResult.activeQueryFieldIndex).to.equal(1);
});
});

describe('when user changes query of one row and a search option of another one', () => {
it('should focus to the last field with a non-empty query', async () => {
const firstRowInitialSearch = {
query: 'keyword containsAll test1 and contributor containsAll test2 and contributor containsAll test3',
option: 'advancedSearch',
};

const queryToRow = () => [
{
bool: '',
searchOption: 'keyword',
match: 'containsAll',
query: 'test1',
},
{
bool: 'and',
searchOption: 'contributor',
match: 'containsAll',
query: 'test2',
},
{
bool: 'and',
searchOption: 'contributor',
match: 'containsAll',
query: 'test3',
},
];

const args = {
defaultSearchOptionValue: 'keyword',
firstRowInitialSearch,
queryToRow,
open: true,
};

const hookResult = await getHookExecutionResult(useAdvancedSearch, args);

hookResult.onChange(1, 'query', 'test3');
hookResult.onChange(0, 'searchOption', 'contributor');

await waitFor(() => hookResult.activeQueryFieldIndex === 2);

expect(hookResult.activeQueryFieldIndex).to.equal(2);
});
});
});
17 changes: 17 additions & 0 deletions lib/AdvancedSearch/hooks/useAdvancedSearch/useAdvancedSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,14 @@ const useAdvancedSearch = ({
return splitQueryRows(initialRows, queryToRow);
}, [firstRowInitialSearch, defaultSearchOptionValue, queryToRow]);

const getActiveQueryFieldIndex = (rowState) => {
const lastQueryIndex = rowState.findLastIndex(row => row.query);
return lastQueryIndex === -1 ? 0 : lastQueryIndex;
}

const intl = useIntl();
const [rowState, setRowState] = useState(initialRowState);
const [activeQueryFieldIndex, setActiveQueryFieldIndex] = useState(getActiveQueryFieldIndex(initialRowState));
const [showEmptyFirstRowMessage, setShowEmptyFirstRowMessage] = useState(false);
const [prevFirstRowInitialSearch, setPrevFirstRowInitialSearch] = useState(firstRowInitialSearch);
const filledRows = useMemo(() => {
Expand All @@ -86,6 +92,7 @@ const useAdvancedSearch = ({
), [intl, searchOptions]);

const resetRows = () => {
setActiveQueryFieldIndex(0);
setRowState(createInitialRowState(firstRowInitialSearch, defaultSearchOptionValue));
};

Expand All @@ -95,6 +102,14 @@ const useAdvancedSearch = ({

newRowState[rowIndex][key] = value;

const activeIndex = key === FIELD_NAMES.QUERY && value
? rowIndex
// rows with an empty query between rows with a non-empty query will be removed the next time the modal
// is opened, so we need to filter them out to have the last field with a non-empty query focused.
: getActiveQueryFieldIndex(newRowState.filter(row => row[FIELD_NAMES.QUERY]));

setActiveQueryFieldIndex(activeIndex);

return newRowState;
});
};
Expand All @@ -120,6 +135,7 @@ const useAdvancedSearch = ({
if (!isEqual(firstRowInitialSearch, prevFirstRowInitialSearch)) {
const splitRows = splitQueryRows(initialRowState, queryToRow);
setPrevFirstRowInitialSearch(firstRowInitialSearch);
setActiveQueryFieldIndex(getActiveQueryFieldIndex(splitRows));
setRowState(splitRows);
}

Expand All @@ -133,6 +149,7 @@ const useAdvancedSearch = ({
showEmptyFirstRowMessage,
filledRows,
query: queryBuilder(filledRows, rowFormatter),
activeQueryFieldIndex,
};
};

Expand Down
8 changes: 8 additions & 0 deletions lib/AdvancedSearch/tests/AdvancedSearch-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ describe('AdvancedSearch', () => {
expect(el.querySelector('[data-test-advanced-search-option]').value).to.equal('surname');
});
});

it('should shift focus to the end of the query', async () => {
await advancedSearch.find(RowInteractor({ index: 0 })).perform(el => {
const queryField = el.querySelector('[data-test-advanced-search-query]');
expect(queryField.selectionStart).to.equal('Test query'.length);
expect(document.activeElement === queryField).to.be.true;
});
});
});

describe('when resetting advanced search', () => {
Expand Down
10 changes: 10 additions & 0 deletions tests/helpers/waitFor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const waitFor = (condition) => {
return new Promise(resolve => {
const intervalId = setInterval(() => {
if (condition()) {
clearInterval(intervalId);
resolve();
}
}, 1);
});
};

0 comments on commit 4080129

Please sign in to comment.