Commit 51ef55a1 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Remove `SearchStatusBar` and `FocusedModeHeader`

These are replaced/consolidated by `FilterStatus`
parent 7172a059
import { createElement } from 'preact';
import useStore from '../store/use-store';
/**
* Render a control to interact with any focused "mode" in the sidebar.
* Currently only a user-focus mode is supported but this could be broadened
* and abstracted if needed. Allow user to toggle in and out of the focus "mode."
*/
export default function FocusedModeHeader() {
const toggleFocusMode = useStore(store => store.toggleFocusMode);
const selectors = useStore(store => ({
focusModeActive: store.focusModeActive(),
focusModeConfigured: store.focusModeConfigured(),
focusModeUserPrettyName: store.focusModeUserPrettyName(),
}));
// Nothing to do here for now if we're not focused on a user
if (!selectors.focusModeConfigured) {
return null;
}
const filterStatus = (
<div className="focused-mode-header__filter-status">
{selectors.focusModeActive ? (
<span>
Showing <strong>{selectors.focusModeUserPrettyName}</strong> only
</span>
) : (
<span>
Showing <strong>all</strong>
</span>
)}
</div>
);
const buttonText = (() => {
if (selectors.focusModeActive) {
return 'Show all';
} else {
return `Show only ${selectors.focusModeUserPrettyName}`;
}
})();
return (
<div className="focused-mode-header">
{filterStatus}
<button
onClick={() => toggleFocusMode()}
className="focused-mode-header__btn"
>
{buttonText}
</button>
</div>
);
}
FocusedModeHeader.propTypes = {};
import { createElement } from 'preact';
import { useMemo } from 'preact/hooks';
import useStore from '../store/use-store';
import uiConstants from '../ui-constants';
import useRootThread from './hooks/use-root-thread';
import Button from './button';
/**
* Of the annotations in the thread `annThread`, how many
* are currently `visible` in the browser (sidebar)?
*/
const countVisibleAnns = annThread => {
return annThread.children.reduce(
function (count, child) {
return count + countVisibleAnns(child);
},
annThread.visible ? 1 : 0
);
};
/**
* UI for displaying information about the currently-applied filtering of
* annotations, and, in some cases, a mechanism for clearing the filter(s).
* */
function SearchStatusBar() {
const thread = useRootThread();
const actions = useStore(store => ({
clearSelection: store.clearSelection,
}));
const counts = useStore(store => ({
annotations: store.annotationCount(),
notes: store.noteCount(),
}));
const {
filterQuery,
focusModeActive,
focusModeUserPrettyName,
hasSelectedAnnotations,
selectedTab,
} = useStore(store => ({
filterQuery: store.getState().selection.filterQuery,
focusModeActive: store.focusModeActive(),
focusModeUserPrettyName: store.focusModeUserPrettyName(),
hasSelectedAnnotations: store.hasSelectedAnnotations(),
selectedTab: store.getState().selection.selectedTab,
}));
// The search status bar UI represents multiple "modes" of filtering
const modes = {
/**
* @type {Boolean}
* A search (filter) query, visible to the user in the search bar, is
* currently applied
*/
filtered: !!filterQuery,
/**
* @type {Boolean}
* The client has a currently-applied focus on a single user. Superseded by
* `filtered` mode.
*/
focused: focusModeActive && !filterQuery,
/**
* @type {Boolean}
* 0 - n annotations are currently "selected", by, e.g. clicking on highlighted
* text in the host page, direct-linking to an annotation, etc. Superseded by
* `filtered` mode.
*/
selected: (() => {
return hasSelectedAnnotations && !filterQuery;
})(),
};
const visibleCount = useMemo(() => {
return countVisibleAnns(thread);
}, [thread]);
// Each "mode" has corresponding descriptive text about the number of
// matching/applicable annotations and, sometimes, a way to clear the
// filter
const modeText = {
filtered: (() => {
switch (visibleCount) {
case 0:
return `No results for "${filterQuery}"`;
case 1:
return '1 search result';
default:
return `${visibleCount} search results`;
}
})(),
focused: (() => {
switch (visibleCount) {
case 0:
return `No annotations for ${focusModeUserPrettyName}`;
case 1:
return 'Showing 1 annotation';
default:
return `Showing ${visibleCount} annotations`;
}
})(),
selected: (() => {
// Generate the proper text to show on the clear-selection button.
// For non-user-focused modes, we can display the number of annotations
// that will be visible if the selection is cleared (`counts.annotations`)
// but this number is inaccurate/misleading when also focused on a user.
let selectedText = '';
switch (selectedTab) {
case uiConstants.TAB_ORPHANS:
selectedText = 'Show all annotations and notes';
break;
case uiConstants.TAB_NOTES:
selectedText = 'Show all notes';
if (counts.notes > 1 && !modes.focused) {
selectedText += ` (${counts.notes})`;
} else if (modes.focused) {
selectedText += ` by ${focusModeUserPrettyName}`;
}
break;
case uiConstants.TAB_ANNOTATIONS:
selectedText = 'Show all annotations';
if (counts.annotations > 1 && !modes.focused) {
selectedText += ` (${counts.annotations})`;
} else if (modes.focused) {
selectedText = `Show all annotations by ${focusModeUserPrettyName}`;
}
break;
}
return selectedText;
})(),
};
return (
<div>
{modes.filtered && (
<div className="search-status-bar">
<Button
icon="cancel"
buttonText="Clear search"
onClick={actions.clearSelection}
className="search-status-bar__button"
/>
<span className="search-status-bar__filtered-text">
{modeText.filtered}
</span>
</div>
)}
{modes.focused && (
<div className="search-status-bar">
<span className="search-status-bar__focused-text">
<strong>{modeText.focused}</strong>
</span>
</div>
)}
{modes.selected && (
<div className="search-status-bar">
<Button
buttonText={modeText.selected}
onClick={actions.clearSelection}
className="search-status-bar__button"
/>
</div>
)}
</div>
);
}
// Necessary for test-mocking purposes (`mockImportedComponents`)
SearchStatusBar.propTypes = {};
export default SearchStatusBar;
import { mount } from 'enzyme';
import { createElement } from 'preact';
import FocusedModeHeader from '../focused-mode-header';
import { $imports } from '../focused-mode-header';
import { checkAccessibility } from '../../../test-util/accessibility';
import mockImportedComponents from '../../../test-util/mock-imported-components';
describe('FocusedModeHeader', function () {
let fakeStore;
function createComponent() {
return mount(<FocusedModeHeader />);
}
beforeEach(function () {
fakeStore = {
selection: {
focusMode: {
enabled: true,
focused: true,
},
},
focusModeActive: sinon.stub().returns(true),
focusModeConfigured: sinon.stub().returns(true),
focusModeUserPrettyName: sinon.stub().returns('Fake User'),
toggleFocusMode: sinon.stub(),
};
$imports.$mock(mockImportedComponents());
$imports.$mock({
'../store/use-store': callback => callback(fakeStore),
});
});
afterEach(() => {
$imports.$restore();
});
context('not in user-focused mode', () => {
it('should not render anything if not in user-focused mode', () => {
fakeStore.focusModeConfigured.returns(false);
const wrapper = createComponent();
assert.isFalse(wrapper.exists('.focused-mode-header'));
});
});
context('user-focused mode', () => {
context('focus is applied (focused/on)', () => {
it("should render status text indicating only that user's annotations are visible", () => {
const wrapper = createComponent();
assert.match(wrapper.text(), /Showing.+Fake User.+only/);
});
it('should render a button allowing the user to view all annotations', () => {
const wrapper = createComponent();
const button = wrapper.find('button');
assert.include(button.text(), 'Show all');
});
});
context('focus is not applied (unfocused/off)', () => {
beforeEach(() => {
fakeStore.focusModeActive = sinon.stub().returns(false);
});
it("should render status text indicating that all user's annotations are visible", () => {
const wrapper = createComponent();
assert.match(wrapper.text(), /Showing.+all/);
});
it("should render a button allowing the user to view only focus user's annotations", () => {
const wrapper = createComponent();
const button = wrapper.find('button');
assert.include(button.text(), 'Show only Fake User');
});
});
describe('toggle button', () => {
it('should toggle focus mode to false if clicked when focused', () => {
fakeStore.focusModeActive = sinon.stub().returns(true);
const wrapper = createComponent();
wrapper.find('button').simulate('click');
assert.calledOnce(fakeStore.toggleFocusMode);
});
});
});
it(
'should pass a11y checks',
checkAccessibility({
content: () => createComponent(),
})
);
});
import { mount } from 'enzyme';
import { createElement } from 'preact';
import SearchStatusBar from '../search-status-bar';
import { $imports } from '../search-status-bar';
import { checkAccessibility } from '../../../test-util/accessibility';
import mockImportedComponents from '../../../test-util/mock-imported-components';
describe('SearchStatusBar', () => {
let fakeUseRootThread;
let fakeStore;
function createComponent(props) {
return mount(<SearchStatusBar {...props} />);
}
beforeEach(() => {
fakeUseRootThread = sinon.stub().returns({ children: [] });
fakeStore = {
getState: sinon.stub().returns({
selection: {},
}),
annotationCount: sinon.stub().returns(1),
focusModeActive: sinon.stub().returns(false),
focusModeUserPrettyName: sinon.stub().returns('Fake User'),
hasSelectedAnnotations: sinon.stub(),
noteCount: sinon.stub().returns(0),
};
$imports.$mock(mockImportedComponents());
$imports.$mock({
'./hooks/use-root-thread': fakeUseRootThread,
'../store/use-store': callback => callback(fakeStore),
});
});
afterEach(() => {
$imports.$restore();
});
context('user search query is applied', () => {
beforeEach(() => {
fakeStore.getState.returns({
selection: {
filterQuery: 'tag:foo',
selectedTab: 'annotation',
},
});
fakeStore.annotationCount.returns(3);
});
[
{
description:
'shows correct text if 2 annotations match the search filter',
children: [
{
id: '1',
visible: true,
children: [{ id: '3', visible: true, children: [] }],
},
{
id: '2',
visible: false,
children: [],
},
],
expectedText: '2 search results',
},
{
description:
'shows correct text if 1 annotation matches the search filter',
children: [
{
id: '1',
visible: true,
children: [{ id: '3', visible: false, children: [] }],
},
{
id: '2',
visible: false,
children: [],
},
],
expectedText: '1 search result',
},
{
description:
'shows correct text if no annotation matches the search filter',
children: [
{
id: '1',
visible: false,
children: [{ id: '3', visible: false, children: [] }],
},
{
id: '2',
visible: false,
children: [],
},
],
expectedText: 'No results for "tag:foo"',
},
].forEach(test => {
it(test.description, () => {
fakeUseRootThread.returns({
children: test.children,
});
const wrapper = createComponent({});
const button = wrapper.find('Button');
assert.equal(button.props().buttonText, 'Clear search');
const searchResultsText = wrapper.find('span').text();
assert.equal(searchResultsText, test.expectedText);
});
});
});
context('user-focused mode applied', () => {
beforeEach(() => {
fakeStore.focusModeActive = sinon.stub().returns(true);
});
it('should not display a clear/show-all-annotations button when user-focused', () => {
const wrapper = createComponent({});
const buttons = wrapper.find('button');
assert.equal(buttons.length, 0);
});
[
{
description:
'shows pluralized annotation count when multiple annotations match for user',
children: [
{ id: '1', visible: true, children: [] },
{ id: '2', visible: true, children: [] },
],
expected: 'Showing 2 annotations',
},
{
description:
'shows single annotation count when one annotation matches for user',
children: [{ id: '1', visible: true, children: [] }],
expected: 'Showing 1 annotation',
},
{
description:
'shows "no annotations" wording when no annotations match for user',
children: [],
expected: 'No annotations for Fake User',
},
].forEach(test => {
it(test.description, () => {
fakeUseRootThread.returns({
children: test.children,
});
const wrapper = createComponent({});
const resultText = wrapper
.find('.search-status-bar__focused-text')
.text();
assert.equal(resultText, test.expected);
});
});
it('should not display user-focused mode text if filtered mode is (also) applied', () => {
fakeUseRootThread.returns({
children: [
{ id: '1', visible: true, children: [] },
{ id: '2', visible: true, children: [] },
],
});
fakeStore.getState.returns({
selection: {
filterQuery: 'tag:foo',
selectedTab: 'annotation',
},
});
const wrapper = createComponent({});
const focusedTextEl = wrapper.find('.search-status-bar__focused-text');
const filteredTextEl = wrapper.find('.search-status-bar__filtered-text');
assert.isFalse(focusedTextEl.exists());
assert.isTrue(filteredTextEl.exists());
});
});
context('selected-annotation(s) mode applied', () => {
context('selected mode only', () => {
[
{
description:
'should display show-all-annotation button with annotation count',
tab: 'annotation',
annotationCount: 5,
noteCount: 3,
buttonText: 'Show all annotations (5)',
},
{
description:
'should display show-all-annotation button with annotation and notes count',
tab: 'orphan',
annotationCount: 5,
noteCount: 3,
buttonText: 'Show all annotations and notes',
},
{
description: 'should display show-all-notes button with note count',
tab: 'note',
annotationCount: 3,
noteCount: 3,
buttonText: 'Show all notes (3)',
},
{
description:
'should display show-all-annotation button with no count',
tab: 'annotation',
annotationCount: 1,
noteCount: 3,
buttonText: 'Show all annotations',
},
{
description: 'should display show-all-notes button with no count',
tab: 'note',
annotationCount: 1,
noteCount: 1,
buttonText: 'Show all notes',
},
].forEach(test => {
it(test.description, () => {
fakeStore.getState.returns({
selection: {
filterQuery: null,
selectedTab: test.tab,
},
});
fakeStore.annotationCount.returns(test.annotationCount);
fakeStore.noteCount.returns(test.noteCount);
fakeStore.hasSelectedAnnotations.returns(true);
const wrapper = createComponent({});
const button = wrapper.find('Button');
assert.isTrue(button.exists());
assert.equal(button.props().buttonText, test.buttonText);
});
});
});
context('combined with applied user-focused mode', () => {
[
{ tab: 'annotation', buttonText: 'Show all annotations by Fake User' },
{ tab: 'orphan', buttonText: 'Show all annotations and notes' },
{ tab: 'note', buttonText: 'Show all notes by Fake User' },
].forEach(test => {
it(`displays correct text for tab '${test.tab}', without count`, () => {
fakeStore.focusModeActive = sinon.stub().returns(true);
fakeStore.getState.returns({
selection: {
filterQuery: null,
selectedTab: test.tab,
},
});
fakeStore.annotationCount.returns(5);
fakeStore.hasSelectedAnnotations.returns(true);
fakeStore.noteCount.returns(3);
const wrapper = createComponent({});
const button = wrapper.find('Button');
assert.isTrue(button.exists());
assert.equal(button.props().buttonText, test.buttonText);
});
});
});
context('combined with applied query filter', () => {
// Applied-query mode wins out here; no selection UI rendered
it('does not show selected-mode elements', () => {
fakeStore.focusModeActive = sinon.stub().returns(true);
fakeStore.getState.returns({
selection: {
filterQuery: 'tag:foo',
selectedTab: 'annotation',
},
});
fakeStore.annotationCount.returns(5);
fakeStore.noteCount.returns(3);
const wrapper = createComponent({});
const button = wrapper.find('Button');
assert.isTrue(button.exists());
assert.equal(button.props().buttonText, 'Clear search');
});
});
});
it(
'should pass a11y checks',
checkAccessibility({
content: () => createComponent({}),
})
);
});
@use "../../mixins/focus";
@use "../../mixins/forms";
@use "../../mixins/layout";
@use "../../mixins/molecules";
@use "../../mixins/utils";
@use "../../variables" as var;
// A dark grey button used for the primary action
// in a form
.focused-mode-header {
@include molecules.card-frame;
@include layout.row($align: center);
position: relative;
background-color: var.$color-background;
border-radius: 2px;
font-weight: 300;
// TODO cleanup
padding: var.$layout-space--xsmall;
margin-bottom: var.$layout-space;
&__btn {
// TODO use Button component
@include forms.primary-action-btn;
@include layout.row($align: center);
margin-left: auto;
height: 30px;
padding-left: 10px;
padding-right: 10px;
}
&__filter-status {
font-weight: 400;
}
}
@use '../../mixins/buttons';
@use '../../mixins/layout';
.search-status-bar {
@include layout.row($align: center);
margin-bottom: 10px;
}
.search-status-bar__button {
@include buttons.button--primary;
padding-right: 0.75em;
margin-right: 10px;
}
...@@ -26,7 +26,6 @@ ...@@ -26,7 +26,6 @@
@use './components/button'; @use './components/button';
@use './components/excerpt'; @use './components/excerpt';
@use './components/filter-status'; @use './components/filter-status';
@use './components/focused-mode-header';
@use './components/group-list'; @use './components/group-list';
@use './components/group-list-item'; @use './components/group-list-item';
@use './components/help-panel'; @use './components/help-panel';
...@@ -38,7 +37,6 @@ ...@@ -38,7 +37,6 @@
@use './components/menu-item'; @use './components/menu-item';
@use './components/menu-section'; @use './components/menu-section';
@use './components/moderation-banner'; @use './components/moderation-banner';
@use './components/search-status-bar';
@use './components/selection-tabs'; @use './components/selection-tabs';
@use './components/share-annotations-panel'; @use './components/share-annotations-panel';
@use './components/search-input'; @use './components/search-input';
......
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