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
7bb5f105
Unverified
Commit
7bb5f105
authored
Jun 18, 2019
by
Robert Knight
Committed by
GitHub
Jun 18, 2019
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1174 from hypothesis/react-search-input
Convert `<search-input>` component to Preact
parents
fbe1a173
06bab908
Changes
8
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
205 additions
and
156 deletions
+205
-156
search-input.js
src/sidebar/components/search-input.js
+83
-36
search-input-test.js
src/sidebar/components/test/search-input-test.js
+66
-46
top-bar-test.js
src/sidebar/components/test/top-bar-test.js
+5
-1
index.js
src/sidebar/index.js
+4
-1
search-input.html
src/sidebar/templates/search-input.html
+0
-14
search-input.scss
src/styles/sidebar/components/search-input.scss
+46
-0
simple-search.scss
src/styles/sidebar/components/simple-search.scss
+0
-57
sidebar.scss
src/styles/sidebar/sidebar.scss
+1
-1
No files found.
src/sidebar/components/search-input.js
View file @
7bb5f105
'use strict'
;
'use strict'
;
// @ngInject
const
classnames
=
require
(
'classnames'
);
function
SearchInputController
(
$element
,
store
)
{
const
{
createElement
}
=
require
(
'preact'
);
const
self
=
this
;
const
{
useRef
,
useState
}
=
require
(
'preact/hooks'
);
const
button
=
$element
.
find
(
'button'
);
const
propTypes
=
require
(
'prop-types'
);
const
input
=
$element
.
find
(
'input'
)[
0
];
const
form
=
$element
.
find
(
'form'
)[
0
];
const
useStore
=
require
(
'../store/use-store'
);
button
.
on
(
'click'
,
function
()
{
const
Spinner
=
require
(
'./spinner'
);
input
.
focus
();
});
/**
* An input field in the top bar for entering a query that filters annotations
form
.
onsubmit
=
function
(
e
)
{
* (in the sidebar) or searches annotations (in the stream/single annotation
e
.
preventDefault
();
* view).
self
.
onSearch
({
$query
:
input
.
value
});
*
};
* This component also renders a loading spinner to indicate when the client
* is fetching for data from the API or in a "loading" state for any other
* reason.
*/
function
SearchInput
({
alwaysExpanded
,
query
,
onSearch
})
{
const
isLoading
=
useStore
(
store
=>
store
.
isLoading
());
const
input
=
useRef
();
// The active filter query from the previous render.
const
[
prevQuery
,
setPrevQuery
]
=
useState
(
query
);
this
.
isLoading
=
()
=>
store
.
isLoading
();
// The query that the user is currently typing, but may not yet have applied.
const
[
pendingQuery
,
setPendingQuery
]
=
useState
(
query
);
this
.
inputClasses
=
function
()
{
const
onSubmit
=
e
=>
{
return
{
'is-expanded'
:
self
.
alwaysExpanded
||
self
.
query
};
e
.
preventDefault
();
// TODO - When the parent components are converted to React, the signature
// of the callback can be simplified to `onSearch(query)` rather than
// `onSearch({ $query: query })`.
onSearch
({
$query
:
input
.
current
.
value
});
};
};
this
.
$onChanges
=
function
(
changes
)
{
// When the active query changes outside of this component, update the input
if
(
changes
.
query
)
{
// field to match. This happens when clearing the current filter for example.
input
.
value
=
changes
.
query
.
currentValue
;
if
(
query
!==
prevQuery
)
{
setPendingQuery
(
query
);
setPrevQuery
(
query
);
}
}
};
return
(
<
form
className
=
"search-input__form"
name
=
"searchForm"
onSubmit
=
{
onSubmit
}
>
<
input
className
=
{
classnames
(
'search-input__input'
,
{
'is-expanded'
:
alwaysExpanded
||
query
,
})}
type
=
"text"
name
=
"query"
placeholder
=
{(
isLoading
&&
'Loading…'
)
||
'Search…'
}
disabled
=
{
isLoading
}
ref
=
{
input
}
value
=
{
pendingQuery
}
onInput
=
{
e
=>
setPendingQuery
(
e
.
target
.
value
)}
/
>
{
!
isLoading
&&
(
<
button
type
=
"button"
className
=
"search-input__icon top-bar__btn"
onClick
=
{()
=>
input
.
current
.
focus
()}
>
<
i
className
=
"h-icon-search"
><
/i
>
<
/button
>
)}
{
isLoading
&&
<
Spinner
className
=
"top-bar__btn"
title
=
"Loading…"
/>
}
<
/form
>
);
}
}
module
.
exports
=
{
SearchInput
.
propTypes
=
{
controller
:
SearchInputController
,
/**
controllerAs
:
'vm'
,
* If true, the input field is always shown. If false, the input field is
bindings
:
{
* only shown if the query is non-empty.
// Specifies whether the search input field should always be expanded,
*/
// regardless of whether the it is focused or has an active query.
alwaysExpanded
:
propTypes
.
bool
,
//
// If false, it is only expanded when focused or when 'query' is non-empty
/**
alwaysExpanded
:
'<'
,
* The currently active filter query.
query
:
'<'
,
*/
onSearch
:
'&'
,
query
:
propTypes
.
string
,
},
template
:
require
(
'../templates/search-input.html'
),
/**
* Callback to invoke when the current filter query changes.
*/
onSearch
:
propTypes
.
func
,
};
};
module
.
exports
=
SearchInput
;
src/sidebar/components/test/search-input-test.js
View file @
7bb5f105
'use strict'
;
'use strict'
;
const
angular
=
require
(
'angular'
);
const
{
createElement
}
=
require
(
'preact'
);
const
{
mount
}
=
require
(
'enzyme'
);
const
util
=
require
(
'../../directive/test/util
'
);
const
SearchInput
=
require
(
'../search-input
'
);
describe
(
'
searchInput'
,
function
()
{
describe
(
'
SearchInput'
,
()
=>
{
let
fakeStore
;
let
fakeStore
;
before
(
function
()
{
const
createSearchInput
=
(
props
=
{})
=>
angular
// `mount` rendering is used so we can get access to DOM nodes.
.
module
(
'app'
,
[])
mount
(
<
SearchInput
{...
props
}
/>
)
;
.
component
(
'searchInput'
,
require
(
'../search-input'
));
});
function
typeQuery
(
wrapper
,
query
)
{
const
input
=
wrapper
.
find
(
'input'
);
input
.
getDOMNode
().
value
=
query
;
input
.
simulate
(
'input'
);
}
beforeEach
(
function
()
{
beforeEach
(
()
=>
{
fakeStore
=
{
isLoading
:
sinon
.
stub
().
returns
(
false
)
};
fakeStore
=
{
isLoading
:
sinon
.
stub
().
returns
(
false
)
};
angular
.
mock
.
module
(
'app'
,
{
store
:
fakeStore
,
});
});
it
(
'displays the search query'
,
function
()
{
const
FakeSpinner
=
()
=>
null
;
const
el
=
util
.
createDirective
(
document
,
'searchInput'
,
{
FakeSpinner
.
displayName
=
'Spinner'
;
query
:
'foo'
,
SearchInput
.
$imports
.
$mock
({
'./spinner'
:
FakeSpinner
,
'../store/use-store'
:
callback
=>
callback
(
fakeStore
),
});
});
const
input
=
el
.
find
(
'input'
)[
0
];
assert
.
equal
(
input
.
value
,
'foo'
);
});
});
it
(
'invokes #onSearch() when the query changes'
,
function
()
{
afterEach
(()
=>
{
const
onSearch
=
sinon
.
stub
();
SearchInput
.
$imports
.
$restore
();
const
el
=
util
.
createDirective
(
document
,
'searchInput'
,
{
query
:
'foo'
,
onSearch
:
{
args
:
[
'$query'
],
callback
:
onSearch
,
},
});
});
const
input
=
el
.
find
(
'input'
)[
0
];
const
form
=
el
.
find
(
'form'
);
it
(
'displays the active query'
,
()
=>
{
input
.
value
=
'new-query'
;
const
wrapper
=
createSearchInput
({
query
:
'foo'
});
form
.
submit
();
assert
.
equal
(
wrapper
.
find
(
'input'
).
prop
(
'value'
),
'foo'
);
assert
.
calledWith
(
onSearch
,
'new-query'
);
});
});
describe
(
'loading indicator'
,
function
()
{
it
(
'resets input field value to active query when active query changes'
,
()
=>
{
it
(
'is hidden when there are no API requests in flight'
,
function
()
{
const
wrapper
=
createSearchInput
({
query
:
'foo'
});
const
el
=
util
.
createDirective
(
document
,
'search-input'
,
{});
const
spinner
=
el
[
0
].
querySelector
(
'spinner'
);
fakeStore
.
isLoading
.
returns
(
false
);
// Simulate user editing the pending query, but not committing it.
el
.
scope
.
$digest
();
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
(
util
.
isHidden
(
spinner
),
true
);
assert
.
equal
(
wrapper
.
find
(
'input'
).
prop
(
'value'
),
''
);
});
});
it
(
'is visible when there are API requests in flight'
,
function
()
{
it
(
'invokes `onSearch` with pending query when form is submitted'
,
()
=>
{
const
el
=
util
.
createDirective
(
document
,
'search-input'
,
{});
const
onSearch
=
sinon
.
stub
();
const
spinner
=
el
[
0
].
querySelector
(
'spinner'
);
const
wrapper
=
createSearchInput
({
query
:
'foo'
,
onSearch
});
typeQuery
(
wrapper
,
'new-query'
);
wrapper
.
find
(
'form'
).
simulate
(
'submit'
);
assert
.
calledWith
(
onSearch
,
{
$query
:
'new-query'
});
});
it
(
'renders loading indicator when app is in a "loading" state'
,
()
=>
{
fakeStore
.
isLoading
.
returns
(
true
);
fakeStore
.
isLoading
.
returns
(
true
);
el
.
scope
.
$digest
();
const
wrapper
=
createSearchInput
();
assert
.
isTrue
(
wrapper
.
exists
(
'Spinner'
));
});
assert
.
equal
(
util
.
isHidden
(
spinner
),
false
);
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
(
'button'
));
});
});
});
});
src/sidebar/components/test/top-bar-test.js
View file @
7bb5f105
...
@@ -17,7 +17,11 @@ describe('topBar', function() {
...
@@ -17,7 +17,11 @@ describe('topBar', function() {
bindings
:
require
(
'../login-control'
).
bindings
,
bindings
:
require
(
'../login-control'
).
bindings
,
})
})
.
component
(
'searchInput'
,
{
.
component
(
'searchInput'
,
{
bindings
:
require
(
'../search-input'
).
bindings
,
bindings
:
{
alwaysExpanded
:
'<'
,
query
:
'<'
,
onSearch
:
'&'
,
},
});
});
});
});
...
...
src/sidebar/index.js
View file @
7bb5f105
...
@@ -177,7 +177,10 @@ function startAngularApp(config) {
...
@@ -177,7 +177,10 @@ function startAngularApp(config) {
'publishAnnotationBtn'
,
'publishAnnotationBtn'
,
require
(
'./components/publish-annotation-btn'
)
require
(
'./components/publish-annotation-btn'
)
)
)
.
component
(
'searchInput'
,
require
(
'./components/search-input'
))
.
component
(
'searchInput'
,
wrapReactComponent
(
require
(
'./components/search-input'
))
)
.
component
(
'searchStatusBar'
,
require
(
'./components/search-status-bar'
))
.
component
(
'searchStatusBar'
,
require
(
'./components/search-status-bar'
))
.
component
(
'selectionTabs'
,
require
(
'./components/selection-tabs'
))
.
component
(
'selectionTabs'
,
require
(
'./components/selection-tabs'
))
.
component
(
'sidebarContent'
,
require
(
'./components/sidebar-content'
))
.
component
(
'sidebarContent'
,
require
(
'./components/sidebar-content'
))
...
...
src/sidebar/templates/search-input.html
deleted
100644 → 0
View file @
fbe1a173
<form
class=
"simple-search-form"
name=
"searchForm"
ng-class=
"!vm.query && 'simple-search-inactive'"
>
<input
class=
"simple-search-input"
type=
"text"
name=
"query"
placeholder=
"{{vm.isLoading() && 'Loading' || 'Search'}}…"
ng-disabled=
"vm.isLoading()"
ng-class=
"vm.inputClasses()"
/>
<button
type=
"button"
class=
"simple-search-icon top-bar__btn"
ng-hide=
"vm.isLoading()"
>
<i
class=
"h-icon-search"
></i>
</button>
<spinner
class=
"top-bar__btn"
ng-show=
"vm.isLoading()"
title=
"Loading…"
></spinner>
</form>
src/styles/sidebar/components/search-input.scss
0 → 100644
View file @
7bb5f105
.search-input__form
{
display
:
flex
;
flex-flow
:
row
nowrap
;
position
:
relative
;
color
:
$gray-dark
;
}
.search-input__icon
{
order
:
0
;
}
.search-input__input
{
@include
outline-on-keyboard-focus
;
flex-grow
:
1
;
order
:
1
;
color
:
$text-color
;
// Disable default browser styling for the input.
border
:
none
;
padding
:
0px
;
width
:
100%
;
// The search box expands when focused, via a change in the
// `max-width` property. In Safari, the <input> will not accept
// focus if `max-width` is set to 0px so we set it to
// a near-zero positive value instead.
// See https://github.com/hypothesis/h/issues/2654
max-width
:
0
.1px
;
transition
:
max-width
.3s
ease-out
,
padding-left
.3s
ease-out
;
&
:disabled
{
background
:
none
;
color
:
$gray-light
;
}
// Expand the search input when focused (triggered by clicking
// on the search icon) or when `is-expanded` is applied.
&
:focus
,
&
.is-expanded
{
max-width
:
150px
;
padding-left
:
6px
;
}
}
src/styles/sidebar/components/simple-search.scss
deleted
100644 → 0
View file @
fbe1a173
@import
"../../base.scss"
;
@import
"../../mixins/icons"
;
.simple-search-form
{
display
:
flex
;
flex-flow
:
row
nowrap
;
position
:
relative
;
color
:
$gray-dark
;
}
.simple-search-icon
{
order
:
0
;
}
:not
(
:focus
)
~
.simple-search-icon
{
color
:
$gray-light
;
}
@at-root
{
$expanded-max-width
:
150px
;
.simple-search-input
{
@include
outline-on-keyboard-focus
;
flex-grow
:
1
;
order
:
1
;
color
:
$text-color
;
// disable the default browser styling for the input
border
:
none
;
padding
:
0px
;
width
:
100%
;
// the search box expands when focused, via a change in the
// `max-width` property. In Safari, the <input> will not accept
// focus if `max-width` is set to 0px so we set it to
// a near-zero positive value instead.
// See GH #2654
max-width
:
0
.1px
;
transition
:
max-width
.3s
ease-out
,
padding-left
.3s
ease-out
;
&
:disabled
{
background
:
none
;
color
:
$gray-light
;
}
// expand the search input when focused (triggered by clicking
// on the search icon) or when `is-expanded` is applied
&
:focus
,
&
.is-expanded
{
max-width
:
$expanded-max-width
;
padding-left
:
6px
;
}
}
}
src/styles/sidebar/sidebar.scss
View file @
7bb5f105
...
@@ -41,8 +41,8 @@ $base-line-height: 20px;
...
@@ -41,8 +41,8 @@ $base-line-height: 20px;
@import
'./components/search-status-bar'
;
@import
'./components/search-status-bar'
;
@import
'./components/selection-tabs'
;
@import
'./components/selection-tabs'
;
@import
'./components/share-link'
;
@import
'./components/share-link'
;
@import
'./components/search-input'
;
@import
'./components/sidebar-tutorial'
;
@import
'./components/sidebar-tutorial'
;
@import
'./components/simple-search'
;
@import
'./components/svg-icon'
;
@import
'./components/svg-icon'
;
@import
'./components/spinner'
;
@import
'./components/spinner'
;
@import
'./components/tags-input'
;
@import
'./components/tags-input'
;
...
...
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