Commit 3e387a12 authored by Robert Knight's avatar Robert Knight

Disable other filter controls when a selection is active

When a selection exists, it replaces other filters rather than being combined
with them. Disable the other filter controls in the UI in this case to make it
more obvious how the filters are being applied.
parent ccb0f77b
...@@ -38,6 +38,8 @@ type FilterToggleProps = { ...@@ -38,6 +38,8 @@ type FilterToggleProps = {
setActive: (active: boolean) => void; setActive: (active: boolean) => void;
testId?: string; testId?: string;
disabled?: boolean;
}; };
/** /**
...@@ -48,6 +50,7 @@ function FilterToggle({ ...@@ -48,6 +50,7 @@ function FilterToggle({
icon: IconComponent, icon: IconComponent,
description, description,
active, active,
disabled = false,
setActive, setActive,
testId, testId,
}: FilterToggleProps) { }: FilterToggleProps) {
...@@ -60,7 +63,9 @@ function FilterToggle({ ...@@ -60,7 +63,9 @@ function FilterToggle({
'font-medium rounded-lg py-1': true, 'font-medium rounded-lg py-1': true,
'text-grey-7 bg-grey-2': !active, 'text-grey-7 bg-grey-2': !active,
'text-grey-1 bg-grey-7': active, 'text-grey-1 bg-grey-7': active,
'opacity-50': disabled,
})} })}
disabled={disabled}
onClick={() => setActive(!active)} onClick={() => setActive(!active)}
pressed={active} pressed={active}
variant="custom" variant="custom"
...@@ -169,6 +174,8 @@ export default function FilterControls({ ...@@ -169,6 +174,8 @@ export default function FilterControls({
label={`By ${focusFilters.user.display}`} label={`By ${focusFilters.user.display}`}
description={`Show annotations by ${focusFilters.user.display}`} description={`Show annotations by ${focusFilters.user.display}`}
active={focusActive.has('user')} active={focusActive.has('user')}
// When a selection exists, it replaces other filters.
disabled={hasSelection}
setActive={() => store.toggleFocusMode({ key: 'user' })} setActive={() => store.toggleFocusMode({ key: 'user' })}
testId="user-focus-toggle" testId="user-focus-toggle"
/> />
...@@ -179,6 +186,7 @@ export default function FilterControls({ ...@@ -179,6 +186,7 @@ export default function FilterControls({
label={`Pages ${focusFilters.page.display}`} label={`Pages ${focusFilters.page.display}`}
description={`Show annotations on pages ${focusFilters.page.display}`} description={`Show annotations on pages ${focusFilters.page.display}`}
active={focusActive.has('page')} active={focusActive.has('page')}
disabled={hasSelection}
setActive={() => store.toggleFocusMode({ key: 'page' })} setActive={() => store.toggleFocusMode({ key: 'page' })}
testId="page-focus-toggle" testId="page-focus-toggle"
/> />
...@@ -189,6 +197,7 @@ export default function FilterControls({ ...@@ -189,6 +197,7 @@ export default function FilterControls({
label="Selected chapter" label="Selected chapter"
description="Show annotations on selected book chapter(s)" description="Show annotations on selected book chapter(s)"
active={focusActive.has('cfi')} active={focusActive.has('cfi')}
disabled={hasSelection}
setActive={() => store.toggleFocusMode({ key: 'cfi' })} setActive={() => store.toggleFocusMode({ key: 'cfi' })}
testId="cfi-focus-toggle" testId="cfi-focus-toggle"
/> />
......
...@@ -19,6 +19,9 @@ export type SearchFieldProps = { ...@@ -19,6 +19,9 @@ export type SearchFieldProps = {
onClearSearch: () => void; onClearSearch: () => void;
/** Disable input editing or submitting the search field. */
disabled?: boolean;
/** Callback for when the current filter query changes */ /** Callback for when the current filter query changes */
onSearch: (value: string) => void; onSearch: (value: string) => void;
...@@ -38,6 +41,7 @@ export type SearchFieldProps = { ...@@ -38,6 +41,7 @@ export type SearchFieldProps = {
*/ */
export default function SearchField({ export default function SearchField({
classes, classes,
disabled = false,
inputRef, inputRef,
onClearSearch, onClearSearch,
onKeyDown, onKeyDown,
...@@ -84,18 +88,24 @@ export default function SearchField({ ...@@ -84,18 +88,24 @@ export default function SearchField({
className={classnames('space-y-3', classes)} className={classnames('space-y-3', classes)}
> >
<InputGroup> <InputGroup>
<IconButton icon={SearchIcon} title="Search" type="submit" /> <IconButton
icon={SearchIcon}
title="Search"
type="submit"
disabled={disabled}
/>
<Input <Input
aria-label="Search annotations" aria-label="Search annotations"
classes={classnames( classes={classnames(
'text-base p-1.5', 'p-1.5',
'transition-[max-width] duration-300 ease-out', 'transition-[max-width] duration-300 ease-out',
'disabled:text-grey-6',
)} )}
data-testid="search-input" data-testid="search-input"
dir="auto" dir="auto"
name="query" name="query"
placeholder={(isLoading && 'Loading…') || 'Search annotations…'} placeholder={(isLoading && 'Loading…') || 'Search annotations…'}
disabled={isLoading} disabled={disabled || isLoading}
elementRef={input} elementRef={input}
value={pendingQuery || ''} value={pendingQuery || ''}
onInput={(e: Event) => onInput={(e: Event) =>
...@@ -109,6 +119,7 @@ export default function SearchField({ ...@@ -109,6 +119,7 @@ export default function SearchField({
data-testid="clear-button" data-testid="clear-button"
title="Clear search" title="Clear search"
onClick={onClearSearch} onClick={onClearSearch}
disabled={disabled}
/> />
)} )}
</InputGroup> </InputGroup>
......
...@@ -10,6 +10,7 @@ export default function SearchPanel() { ...@@ -10,6 +10,7 @@ export default function SearchPanel() {
const store = useSidebarStore(); const store = useSidebarStore();
const filterQuery = store.filterQuery(); const filterQuery = store.filterQuery();
const inputRef = useRef<HTMLInputElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null);
const hasSelection = store.hasSelectedAnnotations();
const clearSearch = () => { const clearSearch = () => {
store.closeSidebarPanel('searchAnnotations'); store.closeSidebarPanel('searchAnnotations');
...@@ -33,6 +34,9 @@ export default function SearchPanel() { ...@@ -33,6 +34,9 @@ export default function SearchPanel() {
<SearchField <SearchField
inputRef={inputRef} inputRef={inputRef}
classes="grow" classes="grow"
// Disable the input when there is a selection, as the selection
// replaces any other filters.
disabled={hasSelection}
query={filterQuery || null} query={filterQuery || null}
onClearSearch={clearSearch} onClearSearch={clearSearch}
onSearch={store.setFilterQuery} onSearch={store.setFilterQuery}
......
...@@ -36,6 +36,10 @@ describe('FilterControls', () => { ...@@ -36,6 +36,10 @@ describe('FilterControls', () => {
this.update(); this.update();
} }
disabled() {
return this.button.exists() && this.button.prop('disabled');
}
exists() { exists() {
return this.button.exists(); return this.button.exists();
} }
...@@ -144,4 +148,36 @@ describe('FilterControls', () => { ...@@ -144,4 +148,36 @@ describe('FilterControls', () => {
assert.isFalse(toggle.isActive()); assert.isFalse(toggle.isActive());
}); });
}); });
it('disables focus filter controls if there is a selection', () => {
// Enable all the focus toggles.
fakeStore.getFocusFilters.returns({
user: {
display: 'John Smith',
},
cfi: {
display: 'Chapter 1',
},
page: {
display: '10-30',
},
});
const toggles = ['cfi', 'page', 'user'];
fakeStore.getFocusActive.returns(new Set(toggles));
// Add a selection. The focus controls should be disabled.
fakeStore.selectedAnnotations.returns([{ id: '123' }]);
const wrapper = createComponent();
const toggleButtons = toggles.map(
name => new ToggleButtonWrapper(wrapper, `${name}-focus-toggle`),
);
assert.isTrue(toggleButtons.every(button => button.disabled()));
// Clear the selection, the focus toggles should be enabled.
fakeStore.selectedAnnotations.returns([]);
wrapper.setProps({});
toggleButtons.forEach(tb => tb.update());
assert.isFalse(toggleButtons.some(button => button.disabled()));
});
}); });
...@@ -148,6 +148,22 @@ describe('SearchField', () => { ...@@ -148,6 +148,22 @@ describe('SearchField', () => {
assert.calledOnce(onClearSearch); assert.calledOnce(onClearSearch);
}); });
[true, false].forEach(disabled => {
it('disables controls if `disabled` prop is true', () => {
// Set a non-empty query so that clear button is shown.
const query = 'some query';
const wrapper = createSearchField({ disabled, query });
assert.equal(wrapper.find('input').prop('disabled'), disabled);
// Both clear and search buttons should be disabled.
assert.deepEqual(
wrapper.find('button').map(btn => btn.prop('disabled')),
[disabled, disabled],
);
});
});
it( it(
'should pass a11y checks', 'should pass a11y checks',
checkAccessibility([ checkAccessibility([
......
...@@ -8,9 +8,10 @@ describe('SearchPanel', () => { ...@@ -8,9 +8,10 @@ describe('SearchPanel', () => {
beforeEach(() => { beforeEach(() => {
fakeStore = { fakeStore = {
setFilterQuery: sinon.stub(),
filterQuery: sinon.stub().returns(null),
closeSidebarPanel: sinon.stub(), closeSidebarPanel: sinon.stub(),
filterQuery: sinon.stub().returns(null),
hasSelectedAnnotations: sinon.stub().returns(false),
setFilterQuery: sinon.stub(),
}; };
$imports.$mock(mockImportedComponents()); $imports.$mock(mockImportedComponents());
...@@ -61,4 +62,12 @@ describe('SearchPanel', () => { ...@@ -61,4 +62,12 @@ describe('SearchPanel', () => {
assert.calledWith(fakeStore.setFilterQuery, 'foo'); assert.calledWith(fakeStore.setFilterQuery, 'foo');
}); });
[true, false].forEach(hasSelection => {
it('disables search field when there is a selection', () => {
fakeStore.hasSelectedAnnotations.returns(hasSelection);
const wrapper = createSearchPanel();
assert.equal(wrapper.find('SearchField').prop('disabled'), hasSelection);
});
});
}); });
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment