Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
e4427ce9
Commit
e4427ce9
authored
Jan 24, 2024
by
Robert Knight
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Remove old search UI
Remove the `search_panel` feature flag tests and old search UI.
parent
aa8995dc
Changes
14
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
21 additions
and
1347 deletions
+21
-1347
HypothesisApp.tsx
src/sidebar/components/HypothesisApp.tsx
+1
-3
SidebarView.tsx
src/sidebar/components/SidebarView.tsx
+2
-12
TopBar.tsx
src/sidebar/components/TopBar.tsx
+1
-10
use-root-thread-test.js
src/sidebar/components/hooks/test/use-root-thread-test.js
+0
-38
use-root-thread.ts
src/sidebar/components/hooks/use-root-thread.ts
+1
-8
FilterStatus.tsx
src/sidebar/components/old-search/FilterStatus.tsx
+0
-274
SearchInput.tsx
src/sidebar/components/old-search/SearchInput.tsx
+0
-212
FilterStatus-test.js
src/sidebar/components/old-search/test/FilterStatus-test.js
+0
-395
SearchInput-test.js
src/sidebar/components/old-search/test/SearchInput-test.js
+0
-301
StreamSearchInput.tsx
src/sidebar/components/search/StreamSearchInput.tsx
+1
-5
StreamSearchInput-test.js
src/sidebar/components/search/test/StreamSearchInput-test.js
+2
-6
HypothesisApp-test.js
src/sidebar/components/test/HypothesisApp-test.js
+0
-13
SidebarView-test.js
src/sidebar/components/test/SidebarView-test.js
+3
-38
TopBar-test.js
src/sidebar/components/test/TopBar-test.js
+10
-32
No files found.
src/sidebar/components/HypothesisApp.tsx
View file @
e4427ce9
...
@@ -66,8 +66,6 @@ function HypothesisApp({
...
@@ -66,8 +66,6 @@ function HypothesisApp({
const
isThirdParty
=
isThirdPartyService
(
settings
);
const
isThirdParty
=
isThirdPartyService
(
settings
);
const
searchPanelEnabled
=
store
.
isFeatureEnabled
(
'search_panel'
);
const
login
=
async
()
=>
{
const
login
=
async
()
=>
{
if
(
serviceConfig
(
settings
))
{
if
(
serviceConfig
(
settings
))
{
// Let the host page handle the login request
// Let the host page handle the login request
...
@@ -166,7 +164,7 @@ function HypothesisApp({
...
@@ -166,7 +164,7 @@ function HypothesisApp({
<
div
className=
"container"
>
<
div
className=
"container"
>
<
ToastMessages
/>
<
ToastMessages
/>
<
HelpPanel
/>
<
HelpPanel
/>
{
searchPanelEnabled
&&
<
SearchPanel
/>
}
<
SearchPanel
/>
<
ShareDialog
shareTab=
{
!
isThirdParty
}
/>
<
ShareDialog
shareTab=
{
!
isThirdParty
}
/>
{
route
&&
(
{
route
&&
(
...
...
src/sidebar/components/SidebarView.tsx
View file @
e4427ce9
...
@@ -12,7 +12,6 @@ import SelectionTabs from './SelectionTabs';
...
@@ -12,7 +12,6 @@ import SelectionTabs from './SelectionTabs';
import
SidebarContentError
from
'./SidebarContentError'
;
import
SidebarContentError
from
'./SidebarContentError'
;
import
ThreadList
from
'./ThreadList'
;
import
ThreadList
from
'./ThreadList'
;
import
{
useRootThread
}
from
'./hooks/use-root-thread'
;
import
{
useRootThread
}
from
'./hooks/use-root-thread'
;
import
FilterStatus
from
'./old-search/FilterStatus'
;
import
FilterControls
from
'./search/FilterControls'
;
import
FilterControls
from
'./search/FilterControls'
;
export
type
SidebarViewProps
=
{
export
type
SidebarViewProps
=
{
...
@@ -40,8 +39,6 @@ function SidebarView({
...
@@ -40,8 +39,6 @@ function SidebarView({
// Store state values
// Store state values
const
store
=
useSidebarStore
();
const
store
=
useSidebarStore
();
const
focusedGroupId
=
store
.
focusedGroupId
();
const
focusedGroupId
=
store
.
focusedGroupId
();
const
hasSelection
=
store
.
hasSelectedAnnotations
();
const
hasAppliedFilter
=
store
.
hasAppliedFilter
()
||
hasSelection
;
const
isLoading
=
store
.
isLoading
();
const
isLoading
=
store
.
isLoading
();
const
isLoggedIn
=
store
.
isLoggedIn
();
const
isLoggedIn
=
store
.
isLoggedIn
();
...
@@ -67,19 +64,13 @@ function SidebarView({
...
@@ -67,19 +64,13 @@ function SidebarView({
const
hasContentError
=
const
hasContentError
=
hasDirectLinkedAnnotationError
||
hasDirectLinkedGroupError
;
hasDirectLinkedAnnotationError
||
hasDirectLinkedGroupError
;
const
searchPanelEnabled
=
store
.
isFeatureEnabled
(
'search_panel'
);
const
showTabs
=
!
hasContentError
;
const
showTabs
=
!
hasContentError
&&
(
searchPanelEnabled
||
!
hasAppliedFilter
);
// Whether to render the old filter status UI. If no filter is active, this
// will render nothing.
const
showFilterStatus
=
!
hasContentError
&&
!
searchPanelEnabled
;
// Whether to render the new filter UI. Note that when the search panel is
// Whether to render the new filter UI. Note that when the search panel is
// open, filter controls are integrated into it. The UI may render nothing
// open, filter controls are integrated into it. The UI may render nothing
// if no filters are configured or selection is active.
// if no filters are configured or selection is active.
const
isSearchPanelOpen
=
store
.
isSidebarPanelOpen
(
'searchAnnotations'
);
const
isSearchPanelOpen
=
store
.
isSidebarPanelOpen
(
'searchAnnotations'
);
const
showFilterControls
=
searchPanelEnabled
&&
!
isSearchPanelOpen
;
const
showFilterControls
=
!
hasContentError
&&
!
isSearchPanelOpen
;
// Show a CTA to log in if successfully viewing a direct-linked annotation
// Show a CTA to log in if successfully viewing a direct-linked annotation
// and not logged in
// and not logged in
...
@@ -143,7 +134,6 @@ function SidebarView({
...
@@ -143,7 +134,6 @@ function SidebarView({
return
(
return
(
<
div
>
<
div
>
<
h2
className=
"sr-only"
>
Annotations
</
h2
>
<
h2
className=
"sr-only"
>
Annotations
</
h2
>
{
showFilterStatus
&&
<
FilterStatus
/>
}
{
showFilterControls
&&
<
FilterControls
withCardContainer
/>
}
{
showFilterControls
&&
<
FilterControls
withCardContainer
/>
}
<
LoginPromptPanel
onLogin=
{
onLogin
}
onSignUp=
{
onSignUp
}
/>
<
LoginPromptPanel
onLogin=
{
onLogin
}
onSignUp=
{
onSignUp
}
/>
{
hasDirectLinkedAnnotationError
&&
(
{
hasDirectLinkedAnnotationError
&&
(
...
...
src/sidebar/components/TopBar.tsx
View file @
e4427ce9
...
@@ -13,7 +13,6 @@ import PendingUpdatesButton from './PendingUpdatesButton';
...
@@ -13,7 +13,6 @@ import PendingUpdatesButton from './PendingUpdatesButton';
import
PressableIconButton
from
'./PressableIconButton'
;
import
PressableIconButton
from
'./PressableIconButton'
;
import
SortMenu
from
'./SortMenu'
;
import
SortMenu
from
'./SortMenu'
;
import
UserMenu
from
'./UserMenu'
;
import
UserMenu
from
'./UserMenu'
;
import
SearchInput
from
'./old-search/SearchInput'
;
import
SearchIconButton
from
'./search/SearchIconButton'
;
import
SearchIconButton
from
'./search/SearchIconButton'
;
import
StreamSearchInput
from
'./search/StreamSearchInput'
;
import
StreamSearchInput
from
'./search/StreamSearchInput'
;
...
@@ -70,10 +69,8 @@ function TopBar({
...
@@ -70,10 +69,8 @@ function TopBar({
const
loginLinkStyle
=
applyTheme
([
'accentColor'
],
settings
);
const
loginLinkStyle
=
applyTheme
([
'accentColor'
],
settings
);
const
store
=
useSidebarStore
();
const
store
=
useSidebarStore
();
const
filterQuery
=
store
.
filterQuery
();
const
isLoggedIn
=
store
.
isLoggedIn
();
const
isLoggedIn
=
store
.
isLoggedIn
();
const
hasFetchedProfile
=
store
.
hasFetchedProfile
();
const
hasFetchedProfile
=
store
.
hasFetchedProfile
();
const
searchPanelEnabled
=
store
.
isFeatureEnabled
(
'search_panel'
);
const
toggleSharePanel
=
()
=>
{
const
toggleSharePanel
=
()
=>
{
store
.
toggleSidebarPanel
(
'shareGroupAnnotations'
);
store
.
toggleSidebarPanel
(
'shareGroupAnnotations'
);
...
@@ -118,13 +115,7 @@ function TopBar({
...
@@ -118,13 +115,7 @@ function TopBar({
{
isSidebar
&&
(
{
isSidebar
&&
(
<>
<>
<
PendingUpdatesButton
/>
<
PendingUpdatesButton
/>
{
!
searchPanelEnabled
&&
(
<
SearchIconButton
/>
<
SearchInput
query=
{
filterQuery
||
null
}
onSearch=
{
store
.
setFilterQuery
}
/>
)
}
{
searchPanelEnabled
&&
<
SearchIconButton
/>
}
<
SortMenu
/>
<
SortMenu
/>
<
TopBarToggleButton
<
TopBarToggleButton
icon=
{
ShareIcon
}
icon=
{
ShareIcon
}
...
...
src/sidebar/components/hooks/test/use-root-thread-test.js
View file @
e4427ce9
...
@@ -64,42 +64,4 @@ describe('sidebar/components/hooks/use-root-thread', () => {
...
@@ -64,42 +64,4 @@ describe('sidebar/components/hooks/use-root-thread', () => {
assert
.
equal
(
threadState
.
showTabs
,
showTabs
);
assert
.
equal
(
threadState
.
showTabs
,
showTabs
);
});
});
});
});
[
// When using search UI, always filter by tab.
{
newSearchUI
:
true
,
hasFilter
:
true
,
hasSelection
:
false
,
showTabs
:
true
},
// When using old search UI, only filter by tab if no selection or filter
// is active.
{
newSearchUI
:
false
,
hasFilter
:
true
,
hasSelection
:
false
,
showTabs
:
false
,
},
{
newSearchUI
:
false
,
hasFilter
:
false
,
hasSelection
:
true
,
showTabs
:
false
,
},
{
newSearchUI
:
false
,
hasFilter
:
false
,
hasSelection
:
false
,
showTabs
:
true
,
},
].
forEach
(({
newSearchUI
,
hasFilter
,
hasSelection
,
showTabs
})
=>
{
it
(
'if `search_panel` is disabled, does not filter by tab if there is a filter active'
,
()
=>
{
fakeStore
.
route
.
returns
(
'sidebar'
);
fakeStore
.
isFeatureEnabled
.
withArgs
(
'search_panel'
).
returns
(
newSearchUI
);
fakeStore
.
hasAppliedFilter
.
returns
(
hasFilter
);
fakeStore
.
hasSelectedAnnotations
.
returns
(
hasSelection
);
mount
(
<
DummyComponent
/>
);
const
threadState
=
fakeThreadAnnotations
.
getCall
(
0
).
args
[
0
];
assert
.
equal
(
threadState
.
showTabs
,
showTabs
);
});
});
});
});
src/sidebar/components/hooks/use-root-thread.ts
View file @
e4427ce9
...
@@ -18,14 +18,7 @@ export function useRootThread(): ThreadAnnotationsResult {
...
@@ -18,14 +18,7 @@ export function useRootThread(): ThreadAnnotationsResult {
const
route
=
store
.
route
();
const
route
=
store
.
route
();
const
selectionState
=
store
.
selectionState
();
const
selectionState
=
store
.
selectionState
();
const
filters
=
store
.
getFilterValues
();
const
filters
=
store
.
getFilterValues
();
const
showTabs
=
route
===
'sidebar'
;
// This logic mirrors code in `SidebarView`. It can be simplified once
// the "search_panel" feature is turned on everywhere.
const
searchPanelEnabled
=
store
.
isFeatureEnabled
(
'search_panel'
);
const
hasAppliedFilter
=
store
.
hasAppliedFilter
()
||
store
.
hasSelectedAnnotations
();
const
showTabs
=
route
===
'sidebar'
&&
(
searchPanelEnabled
||
!
hasAppliedFilter
);
const
threadState
=
useMemo
(():
ThreadState
=>
{
const
threadState
=
useMemo
(():
ThreadState
=>
{
const
selection
=
{
...
selectionState
,
filterQuery
:
query
,
filters
};
const
selection
=
{
...
selectionState
,
filterQuery
:
query
,
filters
};
...
...
src/sidebar/components/old-search/FilterStatus.tsx
deleted
100644 → 0
View file @
aa8995dc
import
{
Button
,
CancelIcon
,
Card
,
CardContent
,
Spinner
,
}
from
'@hypothesis/frontend-shared'
;
import
classnames
from
'classnames'
;
import
{
useMemo
}
from
'preact/hooks'
;
import
{
countVisible
}
from
'../../helpers/thread'
;
import
{
useSidebarStore
}
from
'../../store'
;
import
{
useRootThread
}
from
'../hooks/use-root-thread'
;
type
FilterStatusMessageProps
=
{
/**
* A count of items that are visible but do not match the filters (i.e. items
* that have been "forced visible" by the user)
*/
additionalCount
:
number
;
/** Singular unit of the items being shown, e.g. "result" or "annotation" */
entitySingular
:
string
;
/** Plural unit of the items being shown */
entityPlural
:
string
;
/** Currently-applied filter query string, if any */
filterQuery
:
string
|
null
;
/** Display name for the user currently focused, if any */
focusDisplayName
?:
string
|
null
;
/**
* The number of items that match the current filter(s). When searching by
* query or focusing on a user, this value includes annotations and replies.
* When there are selected annotations, this number includes only top-level
* annotations.
*/
resultCount
:
number
;
};
/**
* Render status text describing the currently-applied filters.
*/
function
FilterStatusMessage
({
additionalCount
,
entitySingular
,
entityPlural
,
filterQuery
,
focusDisplayName
,
resultCount
,
}:
FilterStatusMessageProps
)
{
return
(
<>
{
resultCount
>
0
&&
<
span
>
Showing
</
span
>
}
<
span
className=
"whitespace-nowrap font-bold"
>
{
resultCount
>
0
?
resultCount
:
'No'
}{
' '
}
{
resultCount
===
1
?
entitySingular
:
entityPlural
}
</
span
>
{
filterQuery
&&
(
<
span
>
{
' '
}
for
<
span
className=
"break-words"
>
{
`'${filterQuery}'`
}
</
span
>
</
span
>
)
}
{
focusDisplayName
&&
(
<
span
>
{
' '
}
by
{
' '
}
<
span
className=
"whitespace-nowrap font-bold"
>
{
focusDisplayName
}
</
span
>
</
span
>
)
}
{
additionalCount
>
0
&&
(
<
span
className=
"whitespace-nowrap italic text-color-text-light"
>
{
' '
}
(and
{
additionalCount
}
more)
</
span
>
)
}
</>
);
}
/**
* Show a description of currently-applied filters and a button to clear the
* filter(s).
*
* There are four filter modes. Exactly one is applicable at any time. In order
* of precedence:
*
* 1. selection
* One or more annotations is "selected", either by direct user input or
* "direct-linked" annotation(s)
*
* Message formatting:
* "[Showing] (No|<resultCount>) annotation[s] [\(and <additionalCount> more\)]"
* Button:
* "<cancel icon> Show all [\(<totalCount)\)]" - clears the selection
*
* 2. query
* A search query filter is applied
*
* Message formatting:
* "[Showing] (No|<resultCount>) result[s] for '<filterQuery>'
* by <focusDisplayName] [\(and <additionalCount> more\)]"
* Button:
* "<cancel icon> Clear search" - Clears the search query
*
* 3. focus
* User-focused mode is configured, but may or may not be active/applied.
*
* Message formatting:
* "[Showing] (No|<resultCount>) annotation[s] [by <focusDisplayName>]
* [\(and <additionalCount> more\)]"
* Button:
* - If there are no forced-visible threads:
* "Show (all|only <focusDisplayName>)" - Toggles the user filter activation
* - If there are any forced-visible threads:
* "Reset filters" - Clears selection/filters (does not affect user filter activation)
*
* 4. null
* No filters are applied.
*
* Message formatting:
* N/A (but container elements still render)
* Button:
* N/A
*
* This component must render its container elements if no filters are applied
* ("null" filter mode). This is because the element with `role="status"`
* needs to be continuously present in the DOM such that dynamic updates to its
* text content are available to assistive technology.
* See https://www.w3.org/WAI/WCAG21/Techniques/aria/ARIA22
*/
export
default
function
FilterStatus
()
{
const
store
=
useSidebarStore
();
const
{
rootThread
}
=
useRootThread
();
const
annotationCount
=
store
.
annotationCount
();
const
directLinkedId
=
store
.
directLinkedAnnotationId
();
const
filterQuery
=
store
.
filterQuery
();
const
focusState
=
store
.
focusState
();
const
forcedVisibleCount
=
store
.
forcedVisibleThreads
().
length
;
const
selectedCount
=
store
.
selectedAnnotations
().
length
;
const
filterMode
=
useMemo
(()
=>
{
if
(
selectedCount
>
0
)
{
return
'selection'
;
}
else
if
(
filterQuery
)
{
return
'query'
;
}
else
if
(
focusState
.
configured
)
{
return
'focus'
;
}
return
null
;
},
[
selectedCount
,
filterQuery
,
focusState
]);
// Number of items that match the current filters
const
resultCount
=
useMemo
(()
=>
{
return
filterMode
===
'selection'
?
selectedCount
:
countVisible
(
rootThread
)
-
forcedVisibleCount
;
},
[
filterMode
,
selectedCount
,
rootThread
,
forcedVisibleCount
]);
// Number of additional items that are visible but do not match current
// filters. This can happen when, e.g.:
// - A user manually expands a thread that does not match the current
// filtering
// - A user creates a new annotation when there are applied filters
const
additionalCount
=
useMemo
(()
=>
{
if
(
filterMode
===
'selection'
)
{
// Selection filtering deals in top-level annotations only.
// Compare visible top-level annotations against the count of selected
// (top-level) annotatinos.
const
visibleAnnotationCount
=
(
rootThread
.
children
||
[]).
filter
(
thread
=>
thread
.
annotation
&&
thread
.
visible
,
).
length
;
return
visibleAnnotationCount
-
selectedCount
;
}
else
{
return
forcedVisibleCount
;
}
},
[
filterMode
,
forcedVisibleCount
,
rootThread
.
children
,
selectedCount
]);
const
buttonText
=
useMemo
(()
=>
{
if
(
filterMode
===
'selection'
)
{
// Because of the confusion between counts of entities between selected
// annotations and filtered annotations, don't display the total number
// when in user-focus mode because the numbers won't appear to make sense.
// Don't display total count, either, when viewing a direct-linked annotation.
const
showCount
=
!
focusState
.
configured
&&
!
directLinkedId
;
return
showCount
?
`Show all (
${
annotationCount
}
)`
:
'Show all'
;
}
else
if
(
filterMode
===
'focus'
)
{
if
(
forcedVisibleCount
>
0
)
{
return
'Reset filters'
;
}
return
focusState
.
active
?
'Show all'
:
`Show only
${
focusState
.
displayName
}
`
;
}
return
'Clear search'
;
},
[
annotationCount
,
directLinkedId
,
focusState
,
filterMode
,
forcedVisibleCount
,
]);
return
(
<
div
// This container element needs to be present at all times but
// should only be visible when there are applied filters
className=
{
classnames
(
'mb-3'
,
{
'sr-only'
:
!
filterMode
})
}
data
-
testid=
"filter-status-container"
>
<
Card
>
<
CardContent
>
{
store
.
isLoading
()
?
(
<
Spinner
size=
"md"
/>
)
:
(
<
div
className=
"flex items-center justify-center space-x-1"
>
<
div
className=
{
classnames
(
// Setting `min-width: 0` here allows wrapping to work as
// expected for long `filterQuery` strings. See
// https://css-tricks.com/flexbox-truncated-text/
'grow min-w-[0]'
,
)
}
role=
"status"
>
{
filterMode
&&
(
<
FilterStatusMessage
additionalCount=
{
additionalCount
}
entitySingular=
{
filterMode
===
'query'
?
'result'
:
'annotation'
}
entityPlural=
{
filterMode
===
'query'
?
'results'
:
'annotations'
}
filterQuery=
{
filterQuery
}
focusDisplayName=
{
filterMode
!==
'selection'
&&
focusState
.
active
?
focusState
.
displayName
:
''
}
resultCount=
{
resultCount
}
/>
)
}
</
div
>
{
filterMode
&&
(
<
Button
onClick=
{
filterMode
===
'focus'
&&
!
forcedVisibleCount
?
()
=>
store
.
toggleFocusMode
()
:
()
=>
store
.
clearSelection
()
}
size=
"sm"
title=
{
buttonText
}
variant=
"primary"
data
-
testid=
"clear-button"
>
{
/** @TODO: Set `icon` prop in `Button` instead when https://github.com/hypothesis/frontend-shared/issues/675 is fixed*/
}
{
filterMode
!==
'focus'
&&
<
CancelIcon
/>
}
{
buttonText
}
</
Button
>
)
}
</
div
>
)
}
</
CardContent
>
</
Card
>
</
div
>
);
}
src/sidebar/components/old-search/SearchInput.tsx
deleted
100644 → 0
View file @
aa8995dc
import
{
IconButton
,
Input
,
SearchIcon
,
Spinner
,
}
from
'@hypothesis/frontend-shared'
;
import
classnames
from
'classnames'
;
import
type
{
RefObject
}
from
'preact'
;
import
{
useCallback
,
useRef
,
useState
}
from
'preact/hooks'
;
import
{
useShortcut
}
from
'../../../shared/shortcut'
;
import
{
isMacOS
}
from
'../../../shared/user-agent'
;
import
{
useSidebarStore
}
from
'../../store'
;
/**
* Respond to keydown events on the document (shortcut keys):
*
* - Focus the search input when the user presses '/', unless the user is
* currently typing in or focused on an input field.
* - Focus the search input when the user presses CMD-K (MacOS) or CTRL-K
* (everyone else)
* - Restore previous focus when the user presses 'Escape' while the search
* input is focused.
*/
function
useSearchKeyboardShortcuts
(
searchInputRef
:
RefObject
<
HTMLInputElement
>
,
)
{
const
prevFocusRef
=
useRef
<
HTMLOrSVGElement
|
null
>
(
null
);
const
focusSearch
=
useCallback
(
(
event
:
KeyboardEvent
)
=>
{
// When user is in an input field, respond to CMD-/CTRL-K keypresses,
// but ignore '/' keypresses
if
(
!
event
.
metaKey
&&
!
event
.
ctrlKey
&&
event
.
target
instanceof
HTMLElement
&&
[
'INPUT'
,
'TEXTAREA'
].
includes
(
event
.
target
.
tagName
)
)
{
return
;
}
prevFocusRef
.
current
=
document
.
activeElement
as
HTMLOrSVGElement
|
null
;
if
(
searchInputRef
.
current
)
{
searchInputRef
.
current
?.
focus
();
event
.
preventDefault
();
event
.
stopPropagation
();
}
},
[
searchInputRef
],
);
const
restoreFocus
=
useCallback
(()
=>
{
if
(
document
.
activeElement
===
searchInputRef
.
current
)
{
if
(
prevFocusRef
.
current
)
{
prevFocusRef
.
current
.
focus
();
prevFocusRef
.
current
=
null
;
}
searchInputRef
.
current
?.
blur
();
}
},
[
searchInputRef
]);
const
modifierKey
=
isMacOS
()
?
'meta'
:
'ctrl'
;
useShortcut
(
'/'
,
focusSearch
);
useShortcut
(
`
${
modifierKey
}
+k`
,
focusSearch
);
useShortcut
(
'escape'
,
restoreFocus
);
}
export
type
SearchInputProps
=
{
/**
* When true, the input field is always shown. If false, the input field is
* only shown if the query is non-empty.
*/
alwaysExpanded
?:
boolean
;
/** The currently-active filter query */
query
:
string
|
null
;
/** Callback for when the current filter query changes */
onSearch
:
(
value
:
string
)
=>
void
;
};
/**
* An input field in the top bar for entering a query that filters annotations
* (in the sidebar) or searches annotations (in the stream/single annotation
* view).
*
* This component also renders a eloading spinner to indicate when the client
* is fetching for data from the API or in a "loading" state for any other
* reason.
*/
export
default
function
SearchInput
({
alwaysExpanded
,
query
,
onSearch
,
}:
SearchInputProps
)
{
const
store
=
useSidebarStore
();
const
isLoading
=
store
.
isLoading
();
const
input
=
useRef
<
HTMLInputElement
|
null
>
(
null
);
useSearchKeyboardShortcuts
(
input
);
// The active filter query from the previous render.
const
[
prevQuery
,
setPrevQuery
]
=
useState
(
query
);
// The query that the user is currently typing, but may not yet have applied.
const
[
pendingQuery
,
setPendingQuery
]
=
useState
(
query
);
const
onSubmit
=
(
e
:
Event
)
=>
{
e
.
preventDefault
();
if
(
input
.
current
?.
value
||
prevQuery
)
{
// Don't set an initial empty query, but allow a later empty query to
// clear `prevQuery`
onSearch
(
input
.
current
?.
value
??
''
);
}
};
// When the active query changes outside of this component, update the input
// field to match. This happens when clearing the current filter for example.
if
(
query
!==
prevQuery
)
{
setPendingQuery
(
query
);
setPrevQuery
(
query
);
}
const
isExpanded
=
alwaysExpanded
||
query
;
return
(
<
form
action=
"#"
className=
{
classnames
(
// Relative positioning allows the search input to expand without
// pushing other things in the top bar to the right when there is
// a long group name (input will slide "over" end of group name in menu)
'relative'
,
'flex items-center'
,
// Having a nearly opaque white background makes the collision with
// group names to the left a little less jarring. Full white on hover
// to fully remove the distraction.
'bg-white/90 hover:bg-white transition-colors'
,
)
}
name=
"searchForm"
onSubmit=
{
onSubmit
}
>
<
Input
aria
-
label=
"Search"
classes=
{
classnames
(
// This element is ordered second in the flex layout (appears to the
// right of the search icon-button) but having it first in source
// ensures it is first in keyboard tab order
'order-1'
,
'text-base'
,
{
// Borders must be turned off when input is not expanded or focused
// to ensure it has 0 dimensions
'border-0'
:
!
isExpanded
,
// The goal is to have a one-pixel grey border when `isExpanded`.
// Setting it both on focus (when it will be ofuscated by the focus
// ring) and when expanded prevents any change in the input's size
// when moving between the two states.
'focus:border'
:
true
,
border
:
isExpanded
,
},
{
// Make the input dimensionless when not expanded (or focused)
// Make the 0-padding rule `!important` so that it doesn't get
// superseded by `Input` padding
'max-w-0 !p-0'
:
!
isExpanded
,
// However, if the input it focused, it is visually expanded, and
// needs that padding back
'focus:!p-1.5'
:
true
,
// Make the input have dimensions and padding when focused or
// expanded. The left-margin is to make room for the focus ring of
// the search icon-button when navigating by keyboard. Set a
// max-width to allow transition to work when exact width is unknown.
'focus:max-w-[150px] focus:ml-[2px]'
:
true
,
'max-w-[150px] p-1.5 ml-[2px]'
:
isExpanded
,
},
'transition-[max-width] duration-300 ease-out'
,
)
}
data
-
testid=
"search-input"
dir=
"auto"
type=
"text"
name=
"query"
placeholder=
{
(
isLoading
&&
'Loading…'
)
||
'Search…'
}
disabled=
{
isLoading
}
elementRef=
{
input
}
value=
{
pendingQuery
||
''
}
onInput=
{
(
e
:
Event
)
=>
setPendingQuery
((
e
.
target
as
HTMLInputElement
).
value
)
}
/>
{
!
isLoading
&&
(
<
div
className=
"order-0"
>
<
IconButton
icon=
{
SearchIcon
}
onClick=
{
()
=>
input
.
current
?.
focus
()
}
title=
"Search annotations"
// The containing form has a white background. The top bar is only
// 40px high. If we allow standard touch-minimum height here (44px),
// the visible white background exceeds the height of the top bar in
// touch contexts. Disable touch sizing via `size="custom"`, then
// add back the width rule and padding to keep horizontal spacing
// consistent.
size=
"custom"
classes=
"touch:min-w-touch-minimum p-1"
/>
</
div
>
)
}
{
isLoading
&&
<
Spinner
/>
}
</
form
>
);
}
src/sidebar/components/old-search/test/FilterStatus-test.js
deleted
100644 → 0
View file @
aa8995dc
import
{
mockImportedComponents
}
from
'@hypothesis/frontend-testing'
;
import
{
mount
}
from
'enzyme'
;
import
FilterStatus
,
{
$imports
}
from
'../FilterStatus'
;
function
getFocusState
()
{
return
{
active
:
false
,
configured
:
false
,
focusDisplayName
:
''
,
};
}
describe
(
'FilterStatus'
,
()
=>
{
let
fakeStore
;
let
fakeUseRootThread
;
let
fakeThreadUtil
;
const
createComponent
=
()
=>
{
return
mount
(
<
FilterStatus
/>
);
};
beforeEach
(()
=>
{
fakeThreadUtil
=
{
countVisible
:
sinon
.
stub
().
returns
(
0
),
};
fakeStore
=
{
annotationCount
:
sinon
.
stub
(),
clearSelection
:
sinon
.
stub
(),
directLinkedAnnotationId
:
sinon
.
stub
(),
filterQuery
:
sinon
.
stub
().
returns
(
null
),
focusState
:
sinon
.
stub
().
returns
(
getFocusState
()),
forcedVisibleThreads
:
sinon
.
stub
().
returns
([]),
isLoading
:
sinon
.
stub
().
returns
(
false
),
selectedAnnotations
:
sinon
.
stub
().
returns
([]),
toggleFocusMode
:
sinon
.
stub
(),
};
fakeUseRootThread
=
sinon
.
stub
().
returns
({
rootThread
:
{
children
:
[]
},
});
$imports
.
$mock
(
mockImportedComponents
());
$imports
.
$mock
({
'../hooks/use-root-thread'
:
{
useRootThread
:
fakeUseRootThread
},
'../../store'
:
{
useSidebarStore
:
()
=>
fakeStore
},
'../../helpers/thread'
:
fakeThreadUtil
,
});
});
afterEach
(()
=>
{
$imports
.
$restore
();
});
function
assertFilterText
(
wrapper
,
text
)
{
const
filterText
=
wrapper
.
find
(
'[role="status"]'
).
text
();
assert
.
equal
(
filterText
,
text
);
}
function
assertButton
(
wrapper
,
expected
)
{
const
button
=
wrapper
.
find
(
'Button[data-testid="clear-button"]'
);
const
buttonProps
=
button
.
props
();
assert
.
equal
(
buttonProps
.
title
,
expected
.
text
);
assert
.
equal
(
button
.
find
(
'CancelIcon'
).
exists
(),
!!
expected
.
icon
);
buttonProps
.
onClick
();
assert
.
calledOnce
(
expected
.
callback
);
}
function
assertClearButton
(
wrapper
)
{
const
button
=
wrapper
.
find
(
'Button[data-testid="clear-button"]'
);
assert
.
equal
(
button
.
text
(),
'Clear search'
);
assert
.
isTrue
(
button
.
find
(
'CancelIcon'
).
exists
());
button
.
props
().
onClick
();
assert
.
calledOnce
(
fakeStore
.
clearSelection
);
}
context
(
'Loading'
,
()
=>
{
it
(
'shows a loading spinner'
,
()
=>
{
fakeStore
.
filterQuery
.
returns
(
'foobar'
);
fakeStore
.
isLoading
.
returns
(
true
);
const
wrapper
=
createComponent
();
assert
.
isTrue
(
wrapper
.
find
(
'Spinner'
).
exists
());
});
});
context
(
'(State 1): no search filters active'
,
()
=>
{
it
(
'should render hidden but available to screen readers'
,
()
=>
{
const
wrapper
=
createComponent
();
const
containerEl
=
wrapper
.
find
(
'div[data-testid="filter-status-container"]'
)
.
getDOMNode
();
assert
.
include
(
containerEl
.
className
,
'sr-only'
);
assertFilterText
(
wrapper
,
''
);
});
});
context
(
'(State 2): filtered by query'
,
()
=>
{
beforeEach
(()
=>
{
fakeStore
.
filterQuery
.
returns
(
'foobar'
);
fakeThreadUtil
.
countVisible
.
returns
(
1
);
});
it
(
'should provide a "Clear search" button that clears the selection'
,
()
=>
{
assertClearButton
(
createComponent
());
});
it
(
'should show the count of matching results'
,
()
=>
{
assertFilterText
(
createComponent
(),
"Showing 1 result for 'foobar'"
);
});
it
(
'should show pluralized count of results when appropriate'
,
()
=>
{
fakeThreadUtil
.
countVisible
.
returns
(
5
);
assertFilterText
(
createComponent
(),
"Showing 5 results for 'foobar'"
);
});
it
(
'should show a no results message when no matches'
,
()
=>
{
fakeThreadUtil
.
countVisible
.
returns
(
0
);
assertFilterText
(
createComponent
(),
"No results for 'foobar'"
);
});
});
context
(
'(State 3): filtered by query with force-expanded threads'
,
()
=>
{
beforeEach
(()
=>
{
fakeStore
.
filterQuery
.
returns
(
'foobar'
);
fakeStore
.
forcedVisibleThreads
.
returns
([
1
,
2
,
3
]);
fakeThreadUtil
.
countVisible
.
returns
(
5
);
});
it
(
'should show a separate count for results versus forced visible'
,
()
=>
{
assertFilterText
(
createComponent
(),
"Showing 2 results for 'foobar' (and 3 more)"
,
);
});
it
(
'should provide a "Clear search" button that clears the selection'
,
()
=>
{
assertClearButton
(
createComponent
());
});
});
context
(
'(State 4): selected annotations'
,
()
=>
{
beforeEach
(()
=>
{
fakeStore
.
selectedAnnotations
.
returns
([
1
]);
});
it
(
'should show the count of annotations'
,
()
=>
{
assertFilterText
(
createComponent
(),
'Showing 1 annotation'
);
});
it
(
'should pluralize annotations when necessary'
,
()
=>
{
fakeStore
.
selectedAnnotations
.
returns
([
1
,
2
,
3
,
4
]);
assertFilterText
(
createComponent
(),
'Showing 4 annotations'
);
});
it
(
'should show the count of additionally-shown top-level annotations'
,
()
=>
{
// In selection mode, "forced visible" count is computed by subtracting
// the selectedCount from the count of all visible top-level threads
// (children/replies are ignored in this count)
fakeUseRootThread
.
returns
({
rootThread
:
{
id
:
'__default__'
,
children
:
[
{
id
:
'1'
,
annotation
:
{
$tag
:
'1'
},
visible
:
true
,
children
:
[]
},
{
id
:
'2'
,
annotation
:
{
$tag
:
'2'
},
visible
:
true
,
children
:
[
{
id
:
'2a'
,
annotation
:
{
$tag
:
'2a'
},
visible
:
true
,
children
:
[],
},
],
},
],
},
});
assertFilterText
(
createComponent
(),
'Showing 1 annotation (and 1 more)'
);
});
it
(
'should provide a "Show all" button that shows a count of all annotations'
,
()
=>
{
fakeStore
.
annotationCount
.
returns
(
5
);
assertButton
(
createComponent
(),
{
text
:
'Show all (5)'
,
icon
:
true
,
callback
:
fakeStore
.
clearSelection
,
});
});
it
(
'should not show count of annotations on "Show All" button if direct-linked annotation present'
,
()
=>
{
fakeStore
.
annotationCount
.
returns
(
5
);
fakeStore
.
directLinkedAnnotationId
.
returns
(
1
);
assertButton
(
createComponent
(),
{
text
:
'Show all'
,
icon
:
true
,
callback
:
fakeStore
.
clearSelection
,
});
});
});
context
(
'(State 5): user-focus mode active'
,
()
=>
{
beforeEach
(()
=>
{
fakeStore
.
focusState
.
returns
({
active
:
true
,
configured
:
true
,
displayName
:
'Ebenezer Studentolog'
,
});
fakeThreadUtil
.
countVisible
.
returns
(
1
);
});
it
(
'should show a count of annotations by the focused user'
,
()
=>
{
assertFilterText
(
createComponent
(),
'Showing 1 annotation by Ebenezer Studentolog'
,
);
});
it
(
'should pluralize annotations when needed'
,
()
=>
{
fakeThreadUtil
.
countVisible
.
returns
(
3
);
assertFilterText
(
createComponent
(),
'Showing 3 annotations by Ebenezer Studentolog'
,
);
});
it
(
'should show a no results message when user has no annotations'
,
()
=>
{
fakeThreadUtil
.
countVisible
.
returns
(
0
);
assertFilterText
(
createComponent
(),
'No annotations by Ebenezer Studentolog'
,
);
});
it
(
'should provide a "Show all" button that toggles user focus mode'
,
()
=>
{
assertButton
(
createComponent
(),
{
text
:
'Show all'
,
icon
:
false
,
callback
:
fakeStore
.
toggleFocusMode
,
});
});
});
context
(
'(State 6): user-focus mode active, filtered by query'
,
()
=>
{
beforeEach
(()
=>
{
fakeStore
.
focusState
.
returns
({
active
:
true
,
configured
:
true
,
displayName
:
'Ebenezer Studentolog'
,
});
fakeStore
.
filterQuery
.
returns
(
'biscuits'
);
fakeThreadUtil
.
countVisible
.
returns
(
1
);
});
it
(
'should show a count of annotations by the focused user'
,
()
=>
{
assertFilterText
(
createComponent
(),
"Showing 1 result for 'biscuits' by Ebenezer Studentolog"
,
);
});
it
(
'should pluralize annotations when needed'
,
()
=>
{
fakeThreadUtil
.
countVisible
.
returns
(
3
);
assertFilterText
(
createComponent
(),
"Showing 3 results for 'biscuits' by Ebenezer Studentolog"
,
);
});
it
(
'should show a no results message when user has no annotations'
,
()
=>
{
fakeThreadUtil
.
countVisible
.
returns
(
0
);
assertFilterText
(
createComponent
(),
"No results for 'biscuits' by Ebenezer Studentolog"
,
);
});
it
(
'should provide a "Clear search" button'
,
()
=>
{
assertClearButton
(
createComponent
());
});
});
context
(
'(State 7): user-focus mode active, filtered by query, force-expanded threads'
,
()
=>
{
beforeEach
(()
=>
{
fakeStore
.
focusState
.
returns
({
active
:
true
,
configured
:
true
,
displayName
:
'Ebenezer Studentolog'
,
});
fakeStore
.
filterQuery
.
returns
(
'biscuits'
);
fakeStore
.
forcedVisibleThreads
.
returns
([
1
,
2
]);
fakeThreadUtil
.
countVisible
.
returns
(
3
);
});
it
(
'should show a count of annotations by the focused user'
,
()
=>
{
assertFilterText
(
createComponent
(),
"Showing 1 result for 'biscuits' by Ebenezer Studentolog (and 2 more)"
,
);
});
it
(
'should provide a "Clear search" button'
,
()
=>
{
assertClearButton
(
createComponent
());
});
},
);
context
(
'(State 8): user-focus mode active, selected annotations'
,
()
=>
{
beforeEach
(()
=>
{
fakeStore
.
focusState
.
returns
({
active
:
true
,
configured
:
true
,
displayName
:
'Ebenezer Studentolog'
,
});
fakeStore
.
selectedAnnotations
.
returns
([
1
,
2
]);
});
it
(
'should ignore user and display selected annotations'
,
()
=>
{
assertFilterText
(
createComponent
(),
'Showing 2 annotations'
);
});
it
(
'should provide a "Show all" button'
,
()
=>
{
assertButton
(
createComponent
(),
{
text
:
'Show all'
,
icon
:
true
,
callback
:
fakeStore
.
clearSelection
,
});
});
});
context
(
'(State 9): user-focus mode active, force-expanded threads'
,
()
=>
{
beforeEach
(()
=>
{
fakeStore
.
focusState
.
returns
({
active
:
true
,
configured
:
true
,
displayName
:
'Ebenezer Studentolog'
,
});
fakeStore
.
forcedVisibleThreads
.
returns
([
1
,
2
,
3
]);
fakeThreadUtil
.
countVisible
.
returns
(
7
);
});
it
(
'should show count of user results separately from forced-visible threads'
,
()
=>
{
assertFilterText
(
createComponent
(),
'Showing 4 annotations by Ebenezer Studentolog (and 3 more)'
,
);
});
it
(
'should handle cases when there are no focused-user annotations'
,
()
=>
{
fakeStore
.
forcedVisibleThreads
.
returns
([
1
,
2
,
3
,
4
,
5
,
6
,
7
]);
assertFilterText
(
createComponent
(),
'No annotations by Ebenezer Studentolog (and 7 more)'
,
);
});
it
(
'should provide a "Reset filters" button'
,
()
=>
{
assertButton
(
createComponent
(),
{
text
:
'Reset filters'
,
icon
:
false
,
callback
:
fakeStore
.
clearSelection
,
});
});
});
context
(
'(State 10): user-focus mode configured but inactive'
,
()
=>
{
beforeEach
(()
=>
{
fakeStore
.
focusState
.
returns
({
active
:
false
,
configured
:
true
,
displayName
:
'Ebenezer Studentolog'
,
});
fakeThreadUtil
.
countVisible
.
returns
(
7
);
});
it
(
"should show a count of everyone's annotations"
,
()
=>
{
assertFilterText
(
createComponent
(),
'Showing 7 annotations'
);
});
it
(
'should provide a button to activate user-focused mode'
,
()
=>
{
assertButton
(
createComponent
(),
{
text
:
'Show only Ebenezer Studentolog'
,
icon
:
false
,
callback
:
fakeStore
.
toggleFocusMode
,
});
});
});
});
src/sidebar/components/old-search/test/SearchInput-test.js
deleted
100644 → 0
View file @
aa8995dc
import
{
checkAccessibility
,
mockImportedComponents
,
}
from
'@hypothesis/frontend-testing'
;
import
{
mount
}
from
'enzyme'
;
import
SearchInput
from
'../SearchInput'
;
import
{
$imports
}
from
'../SearchInput'
;
describe
(
'SearchInput'
,
()
=>
{
let
fakeIsMacOS
;
let
fakeStore
;
let
container
;
let
wrappers
;
const
createSearchInput
=
(
props
=
{})
=>
{
const
wrapper
=
mount
(
<
SearchInput
{...
props
}
/>, { attachTo: container }
)
;
wrappers
.
push
(
wrapper
);
return
wrapper
;
};
function
typeQuery
(
wrapper
,
query
)
{
const
input
=
wrapper
.
find
(
'input'
);
input
.
getDOMNode
().
value
=
query
;
input
.
simulate
(
'input'
);
}
beforeEach
(()
=>
{
wrappers
=
[];
container
=
document
.
createElement
(
'div'
);
document
.
body
.
appendChild
(
container
);
fakeIsMacOS
=
sinon
.
stub
().
returns
(
false
);
fakeStore
=
{
isLoading
:
sinon
.
stub
().
returns
(
false
)
};
$imports
.
$mock
(
mockImportedComponents
());
$imports
.
$mock
({
'../../store'
:
{
useSidebarStore
:
()
=>
fakeStore
},
'../../../shared/user-agent'
:
{
isMacOS
:
fakeIsMacOS
,
},
});
});
afterEach
(()
=>
{
wrappers
.
forEach
(
wrapper
=>
wrapper
.
unmount
());
container
.
remove
();
$imports
.
$restore
();
});
it
(
'displays the active query'
,
()
=>
{
const
wrapper
=
createSearchInput
({
query
:
'foo'
});
assert
.
equal
(
wrapper
.
find
(
'input'
).
prop
(
'value'
),
'foo'
);
});
it
(
'resets input field value to active query when active query changes'
,
()
=>
{
const
wrapper
=
createSearchInput
({
query
:
'foo'
});
// Simulate user editing the pending query, but not committing it.
typeQuery
(
wrapper
,
'pending-query'
);
// Check that the pending query is displayed.
assert
.
equal
(
wrapper
.
find
(
'input'
).
prop
(
'value'
),
'pending-query'
);
// Simulate active query being reset.
wrapper
.
setProps
({
query
:
''
});
assert
.
equal
(
wrapper
.
find
(
'input'
).
prop
(
'value'
),
''
);
});
it
(
'invokes `onSearch` with pending query when form is submitted'
,
()
=>
{
const
onSearch
=
sinon
.
stub
();
const
wrapper
=
createSearchInput
({
query
:
'foo'
,
onSearch
});
typeQuery
(
wrapper
,
'new-query'
);
wrapper
.
find
(
'form'
).
simulate
(
'submit'
);
assert
.
calledWith
(
onSearch
,
'new-query'
);
});
it
(
'does not set an initial empty query when form is submitted'
,
()
=>
{
// If the first query entered is empty, it will be ignored
const
onSearch
=
sinon
.
stub
();
const
wrapper
=
createSearchInput
({
onSearch
});
typeQuery
(
wrapper
,
''
);
wrapper
.
find
(
'form'
).
simulate
(
'submit'
);
assert
.
notCalled
(
onSearch
);
});
it
(
'sets subsequent empty queries if entered'
,
()
=>
{
// If there has already been at least one query set, subsequent
// empty queries will be honored
const
onSearch
=
sinon
.
stub
();
const
wrapper
=
createSearchInput
({
query
:
'foo'
,
onSearch
});
typeQuery
(
wrapper
,
''
);
wrapper
.
find
(
'form'
).
simulate
(
'submit'
);
assert
.
calledWith
(
onSearch
,
''
);
});
it
(
'renders loading indicator when app is in a "loading" state'
,
()
=>
{
fakeStore
.
isLoading
.
returns
(
true
);
const
wrapper
=
createSearchInput
();
assert
.
isTrue
(
wrapper
.
exists
(
'Spinner'
));
});
it
(
'doesn
\'
t render search button when app is in "loading" state'
,
()
=>
{
fakeStore
.
isLoading
.
returns
(
true
);
const
wrapper
=
createSearchInput
();
assert
.
isFalse
(
wrapper
.
exists
(
'button'
));
});
it
(
'doesn
\'
t render loading indicator when app is not in "loading" state'
,
()
=>
{
fakeStore
.
isLoading
.
returns
(
false
);
const
wrapper
=
createSearchInput
();
assert
.
isFalse
(
wrapper
.
exists
(
'Spinner'
));
});
it
(
'renders search button when app is not in "loading" state'
,
()
=>
{
fakeStore
.
isLoading
.
returns
(
false
);
const
wrapper
=
createSearchInput
();
assert
.
isTrue
(
wrapper
.
exists
(
'IconButton'
));
});
it
(
'focuses search input when button pressed'
,
()
=>
{
fakeStore
.
isLoading
.
returns
(
false
);
const
wrapper
=
createSearchInput
();
const
inputEl
=
wrapper
.
find
(
'input'
).
getDOMNode
();
wrapper
.
find
(
'IconButton'
).
props
().
onClick
();
assert
.
equal
(
document
.
activeElement
,
inputEl
);
});
describe
(
'shortcut key handling'
,
()
=>
{
it
(
'focuses search input when "/" is pressed outside of the component element'
,
()
=>
{
const
wrapper
=
createSearchInput
();
const
searchInputEl
=
wrapper
.
find
(
'input'
).
getDOMNode
();
document
.
body
.
dispatchEvent
(
new
KeyboardEvent
(
'keydown'
,
{
bubbles
:
true
,
cancelable
:
true
,
key
:
'/'
,
}),
);
assert
.
equal
(
document
.
activeElement
,
searchInputEl
);
});
it
(
'focuses search input for non-Mac OSes when "ctrl-K" is pressed outside of the component element'
,
()
=>
{
fakeIsMacOS
.
returns
(
false
);
const
wrapper
=
createSearchInput
();
const
searchInputEl
=
wrapper
.
find
(
'input'
).
getDOMNode
();
document
.
body
.
dispatchEvent
(
new
KeyboardEvent
(
'keydown'
,
{
bubbles
:
true
,
cancelable
:
true
,
key
:
'K'
,
ctrlKey
:
true
,
}),
);
assert
.
equal
(
document
.
activeElement
,
searchInputEl
);
});
it
(
'focuses search input for Mac OS when "Cmd-K" is pressed outside of the component element'
,
()
=>
{
fakeIsMacOS
.
returns
(
true
);
const
wrapper
=
createSearchInput
();
const
searchInputEl
=
wrapper
.
find
(
'input'
).
getDOMNode
();
document
.
body
.
dispatchEvent
(
new
KeyboardEvent
(
'keydown'
,
{
bubbles
:
true
,
cancelable
:
true
,
key
:
'K'
,
metaKey
:
true
,
}),
);
assert
.
equal
(
document
.
activeElement
,
searchInputEl
);
});
it
(
'restores previous focus when focused and "Escape" key pressed'
,
()
=>
{
const
button
=
document
.
createElement
(
'button'
);
button
.
id
=
'a-button'
;
container
.
append
(
button
);
const
wrapper
=
createSearchInput
();
const
inputEl
=
wrapper
.
find
(
'input'
).
getDOMNode
();
const
buttonEl
=
document
.
querySelector
(
'#a-button'
);
buttonEl
.
focus
();
buttonEl
.
dispatchEvent
(
new
KeyboardEvent
(
'keydown'
,
{
bubbles
:
true
,
cancelable
:
true
,
key
:
'/'
,
}),
);
assert
.
equal
(
document
.
activeElement
,
inputEl
,
'focus is moved from button to input'
,
);
inputEl
.
dispatchEvent
(
new
KeyboardEvent
(
'keydown'
,
{
bubbles
:
true
,
cancelable
:
true
,
key
:
'Escape'
,
}),
);
assert
.
equal
(
document
.
activeElement
,
buttonEl
,
'focus has been restored to the button'
,
);
});
[
'textarea'
,
'input'
].
forEach
(
elementName
=>
{
it
(
'does not steal focus when "/" pressed if user is in an input field'
,
()
=>
{
const
input
=
document
.
createElement
(
elementName
);
input
.
id
=
'an-input'
;
container
.
append
(
input
);
createSearchInput
();
const
inputEl
=
document
.
querySelector
(
'#an-input'
);
inputEl
.
focus
();
assert
.
equal
(
document
.
activeElement
,
inputEl
);
inputEl
.
dispatchEvent
(
new
KeyboardEvent
(
'keydown'
,
{
bubbles
:
true
,
cancelable
:
true
,
key
:
'/'
,
}),
);
assert
.
equal
(
document
.
activeElement
,
inputEl
,
'focus has not been moved to search input'
,
);
});
});
it
(
'focuses search input if user is in an input field and presses "Ctrl-k"'
,
()
=>
{
fakeIsMacOS
.
returns
(
false
);
const
input
=
document
.
createElement
(
'input'
);
input
.
id
=
'an-input'
;
container
.
append
(
input
);
const
wrapper
=
createSearchInput
();
const
inputEl
=
document
.
querySelector
(
'#an-input'
);
const
searchInputEl
=
wrapper
.
find
(
'[data-testid="search-input"]'
)
.
at
(
0
)
.
getDOMNode
();
inputEl
.
focus
();
assert
.
equal
(
document
.
activeElement
,
inputEl
);
inputEl
.
dispatchEvent
(
new
KeyboardEvent
(
'keydown'
,
{
bubbles
:
true
,
cancelable
:
true
,
key
:
'k'
,
ctrlKey
:
true
,
}),
);
assert
.
equal
(
document
.
activeElement
,
searchInputEl
,
'focus has been moved to search input'
,
);
});
});
it
(
'should pass a11y checks'
,
checkAccessibility
([
{
content
:
()
=>
createSearchInput
(),
},
{
name
:
'loading state'
,
content
:
()
=>
{
fakeStore
.
isLoading
.
returns
(
true
);
return
createSearchInput
();
},
},
]),
);
});
src/sidebar/components/search/StreamSearchInput.tsx
View file @
e4427ce9
import
{
withServices
}
from
'../../service-context'
;
import
{
withServices
}
from
'../../service-context'
;
import
type
{
RouterService
}
from
'../../services/router'
;
import
type
{
RouterService
}
from
'../../services/router'
;
import
{
useSidebarStore
}
from
'../../store'
;
import
{
useSidebarStore
}
from
'../../store'
;
import
SearchInput
from
'../old-search/SearchInput'
;
import
SearchField
from
'./SearchField'
;
import
SearchField
from
'./SearchField'
;
export
type
StreamSearchInputProps
=
{
export
type
StreamSearchInputProps
=
{
...
@@ -15,7 +14,6 @@ export type StreamSearchInputProps = {
...
@@ -15,7 +14,6 @@ export type StreamSearchInputProps = {
*/
*/
function
StreamSearchInput
({
router
}:
StreamSearchInputProps
)
{
function
StreamSearchInput
({
router
}:
StreamSearchInputProps
)
{
const
store
=
useSidebarStore
();
const
store
=
useSidebarStore
();
const
searchPanelEnabled
=
store
.
isFeatureEnabled
(
'search_panel'
);
const
{
q
}
=
store
.
routeParams
();
const
{
q
}
=
store
.
routeParams
();
const
setQuery
=
(
query
:
string
)
=>
{
const
setQuery
=
(
query
:
string
)
=>
{
// Re-route the user to `/stream` if they are on `/a/:id` and then set
// Re-route the user to `/stream` if they are on `/a/:id` and then set
...
@@ -23,14 +21,12 @@ function StreamSearchInput({ router }: StreamSearchInputProps) {
...
@@ -23,14 +21,12 @@ function StreamSearchInput({ router }: StreamSearchInputProps) {
router
.
navigate
(
'stream'
,
{
q
:
query
});
router
.
navigate
(
'stream'
,
{
q
:
query
});
};
};
return
searchPanelEnabled
?
(
return
(
<
SearchField
<
SearchField
query=
{
q
??
''
}
query=
{
q
??
''
}
onSearch=
{
setQuery
}
onSearch=
{
setQuery
}
onClearSearch=
{
()
=>
setQuery
(
''
)
}
onClearSearch=
{
()
=>
setQuery
(
''
)
}
/>
/>
)
:
(
<
SearchInput
query=
{
q
??
''
}
onSearch=
{
setQuery
}
alwaysExpanded
/>
);
);
}
}
...
...
src/sidebar/components/search/test/StreamSearchInput-test.js
View file @
e4427ce9
...
@@ -14,7 +14,6 @@ describe('StreamSearchInput', () => {
...
@@ -14,7 +14,6 @@ describe('StreamSearchInput', () => {
};
};
fakeStore
=
{
fakeStore
=
{
routeParams
:
sinon
.
stub
().
returns
({}),
routeParams
:
sinon
.
stub
().
returns
({}),
isFeatureEnabled
:
sinon
.
stub
().
returns
(
false
),
};
};
$imports
.
$mock
(
mockImportedComponents
());
$imports
.
$mock
(
mockImportedComponents
());
$imports
.
$mock
({
$imports
.
$mock
({
...
@@ -33,20 +32,18 @@ describe('StreamSearchInput', () => {
...
@@ -33,20 +32,18 @@ describe('StreamSearchInput', () => {
it
(
'displays current "q" search param'
,
()
=>
{
it
(
'displays current "q" search param'
,
()
=>
{
fakeStore
.
routeParams
.
returns
({
q
:
'the-query'
});
fakeStore
.
routeParams
.
returns
({
q
:
'the-query'
});
const
wrapper
=
createSearchInput
();
const
wrapper
=
createSearchInput
();
assert
.
equal
(
wrapper
.
find
(
'Search
Input
'
).
prop
(
'query'
),
'the-query'
);
assert
.
equal
(
wrapper
.
find
(
'Search
Field
'
).
prop
(
'query'
),
'the-query'
);
});
});
it
(
'sets path and query when user searches'
,
()
=>
{
it
(
'sets path and query when user searches'
,
()
=>
{
const
wrapper
=
createSearchInput
();
const
wrapper
=
createSearchInput
();
act
(()
=>
{
act
(()
=>
{
wrapper
.
find
(
'Search
Input
'
).
props
().
onSearch
(
'new-query'
);
wrapper
.
find
(
'Search
Field
'
).
props
().
onSearch
(
'new-query'
);
});
});
assert
.
calledWith
(
fakeRouter
.
navigate
,
'stream'
,
{
q
:
'new-query'
});
assert
.
calledWith
(
fakeRouter
.
navigate
,
'stream'
,
{
q
:
'new-query'
});
});
});
it
(
'renders new SearchField when search panel feature is enabled'
,
()
=>
{
it
(
'renders new SearchField when search panel feature is enabled'
,
()
=>
{
fakeStore
.
isFeatureEnabled
.
returns
(
true
);
const
wrapper
=
createSearchInput
();
const
wrapper
=
createSearchInput
();
assert
.
isFalse
(
wrapper
.
exists
(
'SearchInput'
));
assert
.
isFalse
(
wrapper
.
exists
(
'SearchInput'
));
...
@@ -54,7 +51,6 @@ describe('StreamSearchInput', () => {
...
@@ -54,7 +51,6 @@ describe('StreamSearchInput', () => {
});
});
it
(
'clears filter when clear button is clicked'
,
()
=>
{
it
(
'clears filter when clear button is clicked'
,
()
=>
{
fakeStore
.
isFeatureEnabled
.
returns
(
true
);
const
wrapper
=
createSearchInput
();
const
wrapper
=
createSearchInput
();
act
(()
=>
{
act
(()
=>
{
wrapper
.
find
(
'SearchField'
).
props
().
onClearSearch
();
wrapper
.
find
(
'SearchField'
).
props
().
onClearSearch
();
...
...
src/sidebar/components/test/HypothesisApp-test.js
View file @
e4427ce9
...
@@ -54,7 +54,6 @@ describe('HypothesisApp', () => {
...
@@ -54,7 +54,6 @@ describe('HypothesisApp', () => {
route
:
sinon
.
stub
().
returns
(
'sidebar'
),
route
:
sinon
.
stub
().
returns
(
'sidebar'
),
getLink
:
sinon
.
stub
(),
getLink
:
sinon
.
stub
(),
isFeatureEnabled
:
sinon
.
stub
().
returns
(
true
),
};
};
fakeAuth
=
{};
fakeAuth
=
{};
...
@@ -407,16 +406,4 @@ describe('HypothesisApp', () => {
...
@@ -407,16 +406,4 @@ describe('HypothesisApp', () => {
assert
.
isFalse
(
container
.
hasClass
(
'theme-clean'
));
assert
.
isFalse
(
container
.
hasClass
(
'theme-clean'
));
});
});
});
});
describe
(
'search panel'
,
()
=>
{
[
true
,
false
].
forEach
(
searchPanelEnabled
=>
{
it
(
'renders SearchPanel when feature is enabled'
,
()
=>
{
fakeStore
.
isFeatureEnabled
.
returns
(
searchPanelEnabled
);
const
wrapper
=
createComponent
();
assert
.
equal
(
wrapper
.
exists
(
'SearchPanel'
),
searchPanelEnabled
);
});
});
});
});
});
src/sidebar/components/test/SidebarView-test.js
View file @
e4427ce9
...
@@ -70,7 +70,6 @@ describe('SidebarView', () => {
...
@@ -70,7 +70,6 @@ describe('SidebarView', () => {
profile
:
sinon
.
stub
().
returns
({
userid
:
null
}),
profile
:
sinon
.
stub
().
returns
({
userid
:
null
}),
searchUris
:
sinon
.
stub
().
returns
([]),
searchUris
:
sinon
.
stub
().
returns
([]),
toggleFocusMode
:
sinon
.
stub
(),
toggleFocusMode
:
sinon
.
stub
(),
isFeatureEnabled
:
sinon
.
stub
().
returns
(
false
),
};
};
fakeTabsUtil
=
{
fakeTabsUtil
=
{
...
@@ -215,7 +214,7 @@ describe('SidebarView', () => {
...
@@ -215,7 +214,7 @@ describe('SidebarView', () => {
it
(
'does not render filter status'
,
()
=>
{
it
(
'does not render filter status'
,
()
=>
{
const
wrapper
=
createComponent
();
const
wrapper
=
createComponent
();
assert
.
isFalse
(
wrapper
.
find
(
'Filter
Statu
s'
).
exists
());
assert
.
isFalse
(
wrapper
.
find
(
'Filter
Control
s'
).
exists
());
});
});
});
});
});
});
...
@@ -244,19 +243,11 @@ describe('SidebarView', () => {
...
@@ -244,19 +243,11 @@ describe('SidebarView', () => {
it
(
'does not render filter status'
,
()
=>
{
it
(
'does not render filter status'
,
()
=>
{
const
wrapper
=
createComponent
();
const
wrapper
=
createComponent
();
assert
.
isFalse
(
wrapper
.
find
(
'Filter
Statu
s'
).
exists
());
assert
.
isFalse
(
wrapper
.
find
(
'Filter
Control
s'
).
exists
());
});
});
});
});
describe
(
'filter controls'
,
()
=>
{
describe
(
'filter controls'
,
()
=>
{
it
(
'renders old filter controls when `search_panel` feature is disabled'
,
()
=>
{
fakeStore
.
isFeatureEnabled
.
withArgs
(
'search_panel'
).
returns
(
false
);
const
wrapper
=
createComponent
();
assert
.
isTrue
(
wrapper
.
exists
(
'FilterStatus'
));
assert
.
isFalse
(
wrapper
.
exists
(
'FilterControls'
));
});
[
[
{
{
searchPanelOpen
:
false
,
searchPanelOpen
:
false
,
...
@@ -267,15 +258,13 @@ describe('SidebarView', () => {
...
@@ -267,15 +258,13 @@ describe('SidebarView', () => {
showControls
:
false
,
showControls
:
false
,
},
},
].
forEach
(({
searchPanelOpen
,
showControls
})
=>
{
].
forEach
(({
searchPanelOpen
,
showControls
})
=>
{
it
(
`renders new filter controls when "search_panel" is enabled and search panel is not open`
,
()
=>
{
it
(
`renders filter controls when search panel is not open`
,
()
=>
{
fakeStore
.
isFeatureEnabled
.
withArgs
(
'search_panel'
).
returns
(
true
);
fakeStore
.
isSidebarPanelOpen
fakeStore
.
isSidebarPanelOpen
.
withArgs
(
'searchAnnotations'
)
.
withArgs
(
'searchAnnotations'
)
.
returns
(
searchPanelOpen
);
.
returns
(
searchPanelOpen
);
const
wrapper
=
createComponent
();
const
wrapper
=
createComponent
();
assert
.
isFalse
(
wrapper
.
exists
(
'FilterStatus'
));
assert
.
equal
(
wrapper
.
exists
(
'FilterControls'
),
showControls
);
assert
.
equal
(
wrapper
.
exists
(
'FilterControls'
),
showControls
);
});
});
});
});
...
@@ -299,30 +288,6 @@ describe('SidebarView', () => {
...
@@ -299,30 +288,6 @@ describe('SidebarView', () => {
});
});
});
});
describe
(
'selection tabs'
,
()
=>
{
it
(
'renders tabs'
,
()
=>
{
const
wrapper
=
createComponent
();
assert
.
isTrue
(
wrapper
.
find
(
'SelectionTabs'
).
exists
());
});
it
(
'does not render tabs if there is an applied filter'
,
()
=>
{
fakeStore
.
hasAppliedFilter
.
returns
(
true
);
const
wrapper
=
createComponent
();
assert
.
isFalse
(
wrapper
.
find
(
'SelectionTabs'
).
exists
());
});
it
(
'does not render tabs if there are selected annotations'
,
()
=>
{
fakeStore
.
hasSelectedAnnotations
.
returns
(
true
);
const
wrapper
=
createComponent
();
assert
.
isFalse
(
wrapper
.
find
(
'SelectionTabs'
).
exists
());
});
});
it
(
it
(
'should pass a11y checks'
,
'should pass a11y checks'
,
checkAccessibility
({
checkAccessibility
({
...
...
src/sidebar/components/test/TopBar-test.js
View file @
e4427ce9
...
@@ -15,13 +15,10 @@ describe('TopBar', () => {
...
@@ -15,13 +15,10 @@ describe('TopBar', () => {
beforeEach
(()
=>
{
beforeEach
(()
=>
{
fakeStore
=
{
fakeStore
=
{
filterQuery
:
sinon
.
stub
().
returns
(
null
),
hasFetchedProfile
:
sinon
.
stub
().
returns
(
false
),
hasFetchedProfile
:
sinon
.
stub
().
returns
(
false
),
isLoggedIn
:
sinon
.
stub
().
returns
(
false
),
isLoggedIn
:
sinon
.
stub
().
returns
(
false
),
isSidebarPanelOpen
:
sinon
.
stub
().
returns
(
false
),
isSidebarPanelOpen
:
sinon
.
stub
().
returns
(
false
),
setFilterQuery
:
sinon
.
stub
(),
toggleSidebarPanel
:
sinon
.
stub
(),
toggleSidebarPanel
:
sinon
.
stub
(),
isFeatureEnabled
:
sinon
.
stub
().
returns
(
false
),
};
};
fakeFrameSync
=
{
fakeFrameSync
=
{
...
@@ -177,41 +174,22 @@ describe('TopBar', () => {
...
@@ -177,41 +174,22 @@ describe('TopBar', () => {
assert
.
isTrue
(
shareButton
.
prop
(
'expanded'
));
assert
.
isTrue
(
shareButton
.
prop
(
'expanded'
));
});
});
it
(
'displays search input in the sidebar'
,
()
=>
{
fakeStore
.
filterQuery
.
returns
(
'test-query'
);
const
wrapper
=
createTopBar
();
assert
.
equal
(
wrapper
.
find
(
'SearchInput'
).
prop
(
'query'
),
'test-query'
);
});
it
(
'updates current filter when changing search query in the sidebar'
,
()
=>
{
const
wrapper
=
createTopBar
();
wrapper
.
find
(
'SearchInput'
).
prop
(
'onSearch'
)(
'new-query'
);
assert
.
calledWith
(
fakeStore
.
setFilterQuery
,
'new-query'
);
});
it
(
'displays search input in the single annotation view / stream'
,
()
=>
{
it
(
'displays search input in the single annotation view / stream'
,
()
=>
{
const
wrapper
=
createTopBar
({
isSidebar
:
false
});
const
wrapper
=
createTopBar
({
isSidebar
:
false
});
const
searchInput
=
wrapper
.
find
(
'StreamSearchInput'
);
const
searchInput
=
wrapper
.
find
(
'StreamSearchInput'
);
assert
.
ok
(
searchInput
.
exists
());
assert
.
ok
(
searchInput
.
exists
());
});
});
context
(
'in the stream and single annotation pages'
,
()
=>
{
[
true
,
false
].
forEach
(
isSidebar
=>
{
it
(
'does not render the group list, sort menu or share menu'
,
()
=>
{
it
(
'renders certain controls only in the sidebar'
,
()
=>
{
const
wrapper
=
createTopBar
({
isSidebar
:
false
});
const
wrapper
=
createTopBar
({
isSidebar
});
assert
.
isFalse
(
wrapper
.
exists
(
'GroupList'
));
assert
.
equal
(
wrapper
.
exists
(
'GroupList'
),
isSidebar
);
assert
.
isFalse
(
wrapper
.
exists
(
'SortMenu'
));
assert
.
equal
(
wrapper
.
exists
(
'SortMenu'
),
isSidebar
);
assert
.
isFalse
(
wrapper
.
exists
(
'button[title="Share this page"]'
));
assert
.
equal
(
wrapper
.
exists
(
'SearchIconButton'
),
isSidebar
);
});
assert
.
equal
(
});
wrapper
.
exists
(
'button[data-testid="share-icon-button"]'
),
isSidebar
,
context
(
'when sidebar panel feature is enabled'
,
()
=>
{
);
it
(
'displays search input in the sidebar'
,
()
=>
{
fakeStore
.
isFeatureEnabled
.
returns
(
true
);
const
wrapper
=
createTopBar
();
assert
.
isFalse
(
wrapper
.
exists
(
'SearchInput'
));
assert
.
isTrue
(
wrapper
.
exists
(
'SearchIconButton'
));
});
});
});
});
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment