font-size: var(--mediumFontSize);
}
-.navbar-search-item-link {
- display: flex !important;
-}
-
.navbar-search-item-match {
flex-grow: 5;
overflow: hidden;
}
.navbar-search-item-right {
- flex-grow: 1;
- padding-left: 10px;
text-align: right;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.navbar-search-item-icons {
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { debounce, keyBy, uniqBy } from 'lodash';
+import { debounce, uniqBy } from 'lodash';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { getSuggestions } from '../../../api/components';
loadingMore?: string;
more: More;
open: boolean;
- projects: Dict<{ name: string }>;
query: string;
results: Results;
selected?: string;
loading: false,
more: {},
open: false,
- projects: {},
query: '',
results: {},
shortQuery: false
componentDidMount() {
this.mounted = true;
+ document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keydown', this.handleSKeyDown);
}
componentWillUnmount() {
this.mounted = false;
document.removeEventListener('keydown', this.handleSKeyDown);
+ document.removeEventListener('keydown', this.handleKeyDown);
}
focusInput = () => {
this.setState({
more: {},
open: false,
- projects: {},
query: '',
results: {},
selected: undefined,
more[group.q] = group.more;
});
const list = this.getPlainComponentsList(results, more);
- this.setState(state => ({
+ this.setState({
loading: false,
more,
- projects: { ...state.projects, ...keyBy(response.projects, 'key') },
results,
selected: list.length > 0 ? list[0] : undefined,
shortQuery: query.length > MIN_SEARCH_QUERY_LENGTH && response.warning === 'short_input'
- }));
+ });
}
}, this.stopLoading);
} else {
loading: false,
loadingMore: undefined,
more: { ...state.more, [qualifier]: 0 },
- projects: { ...state.projects, ...keyBy(response.projects, 'key') },
results: {
...state.results,
[qualifier]: uniqBy([...state.results[qualifier], ...moreResults], 'key')
}
};
- handleKeyDown = (event: React.KeyboardEvent) => {
- switch (event.nativeEvent.key) {
+ handleKeyDown = (event: KeyboardEvent) => {
+ if (!this.state.open) {
+ return;
+ }
+
+ switch (event.key) {
case KeyboardKeys.Enter:
event.preventDefault();
- event.nativeEvent.stopImmediatePropagation();
+ event.stopPropagation();
this.openSelected();
break;
case KeyboardKeys.UpArrow:
event.preventDefault();
- event.nativeEvent.stopImmediatePropagation();
+ event.stopPropagation();
this.selectPrevious();
break;
case KeyboardKeys.Escape:
event.preventDefault();
- event.nativeEvent.stopImmediatePropagation();
+ event.stopPropagation();
this.closeSearch();
break;
case KeyboardKeys.DownArrow:
event.preventDefault();
- event.nativeEvent.stopImmediatePropagation();
+ event.stopPropagation();
this.selectNext();
break;
}
key={component.key}
onClose={this.closeSearch}
onSelect={this.handleSelect}
- projects={this.state.projects}
selected={this.state.selected === component.key}
/>
);
minLength={2}
onChange={this.handleQueryChange}
onFocus={this.handleFocus}
- onKeyDown={this.handleKeyDown}
placeholder={translate('search.placeholder')}
value={this.state.query}
/>
*/
import * as React from 'react';
import { Link } from 'react-router-dom';
-import Tooltip from '../../../components/controls/Tooltip';
import ClockIcon from '../../../components/icons/ClockIcon';
import FavoriteIcon from '../../../components/icons/FavoriteIcon';
import QualifierIcon from '../../../components/icons/QualifierIcon';
import { getComponentOverviewUrl } from '../../../helpers/urls';
-import { Dict } from '../../../types/types';
import { ComponentResult } from './utils';
interface Props {
innerRef: (componentKey: string, node: HTMLElement | null) => void;
onClose: () => void;
onSelect: (componentKey: string) => void;
- projects: Dict<{ name: string }>;
selected: boolean;
}
-
-interface State {
- tooltipVisible: boolean;
-}
-
-const TOOLTIP_DELAY = 1000;
-const MILLISECONDS_PER_SECOND = 1000;
-
-export default class SearchResult extends React.PureComponent<Props, State> {
- interval?: number;
- state: State = { tooltipVisible: false };
-
- componentDidMount() {
- if (this.props.selected) {
- this.scheduleTooltip();
- }
- }
-
- componentDidUpdate(prevProps: Props) {
- if (!prevProps.selected && this.props.selected) {
- this.scheduleTooltip();
- } else if (prevProps.selected && !this.props.selected) {
- this.unscheduleTooltip();
- this.setState({ tooltipVisible: false });
- }
- }
-
- componentWillUnmount() {
- this.unscheduleTooltip();
- }
-
- scheduleTooltip = () => {
- this.interval = window.setTimeout(() => {
- this.setState({ tooltipVisible: true });
- }, TOOLTIP_DELAY);
- };
-
- unscheduleTooltip = () => {
- if (this.interval) {
- window.clearInterval(this.interval);
- }
- };
-
- handleMouseEnter = () => {
+export default class SearchResult extends React.PureComponent<Props> {
+ doSelect = () => {
this.props.onSelect(this.props.component.key);
};
- renderProject = (component: ComponentResult) => {
- if (component.project == null) {
- return null;
- }
-
- const project = this.props.projects[component.project];
- return project ? (
- <div className="navbar-search-item-right text-muted-2">{project.name}</div>
- ) : null;
- };
-
render() {
const { component } = this.props;
ref={node => this.props.innerRef(component.key, node)}
role="option"
aria-selected={this.props.selected}>
- <Tooltip
- mouseEnterDelay={TOOLTIP_DELAY / MILLISECONDS_PER_SECOND}
- overlay={component.key}
- placement="left"
- visible={this.state.tooltipVisible}>
- <Link data-key={component.key} onClick={this.props.onClose} to={to}>
- <span className="navbar-search-item-link" onMouseEnter={this.handleMouseEnter}>
+ <Link data-key={component.key} onClick={this.props.onClose} onFocus={this.doSelect} to={to}>
+ <div className="navbar-search-item-link little-padded-top" onMouseEnter={this.doSelect}>
+ <div className="display-flex-center">
<span className="navbar-search-item-icons little-spacer-right">
{component.isFavorite && <FavoriteIcon favorite={true} size={12} />}
{!component.isFavorite && component.isRecentlyBrowsed && <ClockIcon size={12} />}
) : (
<span className="navbar-search-item-match">{component.name}</span>
)}
+ </div>
- {this.renderProject(component)}
- </span>
- </Link>
- </Tooltip>
+ <div className="navbar-search-item-right text-muted-2">{component.key}</div>
+ </div>
+ </Link>
</li>
);
}
import * as React from 'react';
import { KeyboardKeys } from '../../../../helpers/keycodes';
import { mockRouter } from '../../../../helpers/testMocks';
-import { elementKeydown, keydown } from '../../../../helpers/testUtils';
+import { keydown } from '../../../../helpers/testUtils';
import { queryToSearch } from '../../../../helpers/urls';
import { ComponentQualifier } from '../../../../types/component';
import { Search } from '../Search';
selected: selectedKey
});
- elementKeydown(form.find('SearchBox'), KeyboardKeys.Enter);
+ keydown({ key: KeyboardKeys.Enter });
expect(router.push).toBeCalledWith({
pathname: '/dashboard',
search: queryToSearch({ id: selectedKey })
selected: selectedKey
});
- elementKeydown(form.find('SearchBox'), KeyboardKeys.Enter);
+ keydown({ key: KeyboardKeys.Enter });
expect(router.push).toBeCalledWith({
pathname: '/portfolio',
search: queryToSearch({ id: selectedKey })
selected: selectedKey
});
- elementKeydown(form.find('SearchBox'), KeyboardKeys.Enter);
+ keydown({ key: KeyboardKeys.Enter });
expect(router.push).toBeCalledWith({
pathname: '/portfolio',
search: queryToSearch({ id: selectedKey })
expect(form.state().open).toBe(false);
keydown({ key: KeyboardKeys.KeyS });
expect(form.state().open).toBe(true);
- elementKeydown(form.find('SearchBox'), KeyboardKeys.Escape);
+ keydown({ key: KeyboardKeys.Escape });
expect(form.state().open).toBe(false);
});
+it('should ignore keyboard navigation when closed', () => {
+ const wrapper = shallowRender();
+
+ keydown({ key: KeyboardKeys.DownArrow });
+
+ expect(wrapper.state().selected).toBeUndefined();
+ expect(wrapper.state().open).toBe(false);
+
+ keydown({ key: KeyboardKeys.UpArrow });
+
+ expect(wrapper.state().selected).toBeUndefined();
+ expect(wrapper.state().open).toBe(false);
+
+ keydown({ key: KeyboardKeys.Enter });
+
+ expect(wrapper.state().selected).toBeUndefined();
+ expect(wrapper.state().open).toBe(false);
+});
+
function shallowRender(props: Partial<Search['props']> = {}) {
- return shallow<Search>(
- // @ts-ignore
- <Search currentUser={{ isLoggedIn: false }} {...props} />
- );
+ return shallow<Search>(<Search router={mockRouter()} {...props} />);
}
function component(key: string, qualifier = ComponentQualifier.Project) {
}
function next(form: ShallowWrapper<Search['props'], Search['state']>, expected: string) {
- elementKeydown(form.find('SearchBox'), KeyboardKeys.DownArrow);
+ keydown({ key: KeyboardKeys.DownArrow });
expect(form.state().selected).toBe(expected);
}
function prev(form: ShallowWrapper<Search['props'], Search['state']>, expected: string) {
- elementKeydown(form.find('SearchBox'), KeyboardKeys.UpArrow);
+ keydown({ key: KeyboardKeys.UpArrow });
expect(form.state().selected).toBe(expected);
}
import { ComponentQualifier } from '../../../../types/component';
import SearchResult from '../SearchResult';
-beforeAll(() => {
- jest.useFakeTimers();
-});
-
-afterAll(() => {
- jest.runOnlyPendingTimers();
- jest.useRealTimers();
-});
-
it('renders selected', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
key: 'foo',
name: 'foo',
match: 'f<mark>o</mark>o',
- qualifier: 'TRK'
+ qualifier: ComponentQualifier.Project
};
const wrapper = shallowRender({ component });
expect(wrapper).toMatchSnapshot();
isFavorite: true,
key: 'foo',
name: 'foo',
- qualifier: 'TRK'
+ qualifier: ComponentQualifier.Project
};
const wrapper = shallowRender({ component });
expect(wrapper).toMatchSnapshot();
isRecentlyBrowsed: true,
key: 'foo',
name: 'foo',
- qualifier: 'TRK'
- };
- const wrapper = shallowRender({ component });
- expect(wrapper).toMatchSnapshot();
-});
-
-it('renders projects', () => {
- const component = {
- isRecentlyBrowsed: true,
- key: 'qwe',
- name: 'qwe',
- qualifier: ComponentQualifier.Project,
- project: 'foo'
+ qualifier: ComponentQualifier.Project
};
const wrapper = shallowRender({ component });
expect(wrapper).toMatchSnapshot();
});
-it('shows tooltip after delay', () => {
- const wrapper = shallowRender();
- expect(wrapper.find('Tooltip').prop('visible')).toBe(false);
-
- wrapper.setProps({ selected: true });
- expect(wrapper.find('Tooltip').prop('visible')).toBe(false);
-
- jest.runAllTimers();
- wrapper.update();
- expect(wrapper.find('Tooltip').prop('visible')).toBe(true);
-
- wrapper.setProps({ selected: false });
- expect(wrapper.find('Tooltip').prop('visible')).toBe(false);
-});
-
function shallowRender(props: Partial<SearchResult['props']> = {}) {
return shallow(
<SearchResult
- component={{ key: 'foo', name: 'foo', qualifier: 'TRK' }}
+ component={{ key: 'foo', name: 'foo', qualifier: ComponentQualifier.Project }}
innerRef={jest.fn()}
onClose={jest.fn()}
onSelect={jest.fn()}
- projects={{ foo: { name: 'foo' } }}
selected={false}
{...props}
/>
key="foo"
role="option"
>
- <Tooltip
- mouseEnterDelay={1}
- overlay="foo"
- placement="left"
- visible={false}
- >
- <Link
- data-key="foo"
- onClick={[MockFunction]}
- to={
- Object {
- "pathname": "/dashboard",
- "search": "?id=foo",
- }
+ <Link
+ data-key="foo"
+ onClick={[MockFunction]}
+ onFocus={[Function]}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "search": "?id=foo",
}
+ }
+ >
+ <div
+ className="navbar-search-item-link little-padded-top"
+ onMouseEnter={[Function]}
>
- <span
- className="navbar-search-item-link"
- onMouseEnter={[Function]}
+ <div
+ className="display-flex-center"
>
<span
className="navbar-search-item-icons little-spacer-right"
>
foo
</span>
- </span>
- </Link>
- </Tooltip>
+ </div>
+ <div
+ className="navbar-search-item-right text-muted-2"
+ >
+ foo
+ </div>
+ </div>
+ </Link>
</li>
`;
key="foo"
role="option"
>
- <Tooltip
- mouseEnterDelay={1}
- overlay="foo"
- placement="left"
- visible={false}
- >
- <Link
- data-key="foo"
- onClick={[MockFunction]}
- to={
- Object {
- "pathname": "/dashboard",
- "search": "?id=foo",
- }
+ <Link
+ data-key="foo"
+ onClick={[MockFunction]}
+ onFocus={[Function]}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "search": "?id=foo",
}
+ }
+ >
+ <div
+ className="navbar-search-item-link little-padded-top"
+ onMouseEnter={[Function]}
>
- <span
- className="navbar-search-item-link"
- onMouseEnter={[Function]}
+ <div
+ className="display-flex-center"
>
<span
className="navbar-search-item-icons little-spacer-right"
}
}
/>
- </span>
- </Link>
- </Tooltip>
-</li>
-`;
-
-exports[`renders projects 1`] = `
-<li
- aria-selected={false}
- key="qwe"
- role="option"
->
- <Tooltip
- mouseEnterDelay={1}
- overlay="qwe"
- placement="left"
- visible={false}
- >
- <Link
- data-key="qwe"
- onClick={[MockFunction]}
- to={
- Object {
- "pathname": "/dashboard",
- "search": "?id=qwe",
- }
- }
- >
- <span
- className="navbar-search-item-link"
- onMouseEnter={[Function]}
+ </div>
+ <div
+ className="navbar-search-item-right text-muted-2"
>
- <span
- className="navbar-search-item-icons little-spacer-right"
- >
- <ClockIcon
- size={12}
- />
- <QualifierIcon
- className="little-spacer-right"
- qualifier="TRK"
- />
- </span>
- <span
- className="navbar-search-item-match"
- >
- qwe
- </span>
- <div
- className="navbar-search-item-right text-muted-2"
- >
- foo
- </div>
- </span>
- </Link>
- </Tooltip>
+ foo
+ </div>
+ </div>
+ </Link>
</li>
`;
key="foo"
role="option"
>
- <Tooltip
- mouseEnterDelay={1}
- overlay="foo"
- placement="left"
- visible={false}
- >
- <Link
- data-key="foo"
- onClick={[MockFunction]}
- to={
- Object {
- "pathname": "/dashboard",
- "search": "?id=foo",
- }
+ <Link
+ data-key="foo"
+ onClick={[MockFunction]}
+ onFocus={[Function]}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "search": "?id=foo",
}
+ }
+ >
+ <div
+ className="navbar-search-item-link little-padded-top"
+ onMouseEnter={[Function]}
>
- <span
- className="navbar-search-item-link"
- onMouseEnter={[Function]}
+ <div
+ className="display-flex-center"
>
<span
className="navbar-search-item-icons little-spacer-right"
>
foo
</span>
- </span>
- </Link>
- </Tooltip>
+ </div>
+ <div
+ className="navbar-search-item-right text-muted-2"
+ >
+ foo
+ </div>
+ </div>
+ </Link>
</li>
`;
key="foo"
role="option"
>
- <Tooltip
- mouseEnterDelay={1}
- overlay="foo"
- placement="left"
- visible={false}
- >
- <Link
- data-key="foo"
- onClick={[MockFunction]}
- to={
- Object {
- "pathname": "/dashboard",
- "search": "?id=foo",
- }
+ <Link
+ data-key="foo"
+ onClick={[MockFunction]}
+ onFocus={[Function]}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "search": "?id=foo",
}
+ }
+ >
+ <div
+ className="navbar-search-item-link little-padded-top"
+ onMouseEnter={[Function]}
>
- <span
- className="navbar-search-item-link"
- onMouseEnter={[Function]}
+ <div
+ className="display-flex-center"
>
<span
className="navbar-search-item-icons little-spacer-right"
>
foo
</span>
- </span>
- </Link>
- </Tooltip>
+ </div>
+ <div
+ className="navbar-search-item-right text-muted-2"
+ >
+ foo
+ </div>
+ </div>
+ </Link>
</li>
`;
key="foo"
role="option"
>
- <Tooltip
- mouseEnterDelay={1}
- overlay="foo"
- placement="left"
- visible={false}
- >
- <Link
- data-key="foo"
- onClick={[MockFunction]}
- to={
- Object {
- "pathname": "/dashboard",
- "search": "?id=foo",
- }
+ <Link
+ data-key="foo"
+ onClick={[MockFunction]}
+ onFocus={[Function]}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "search": "?id=foo",
}
+ }
+ >
+ <div
+ className="navbar-search-item-link little-padded-top"
+ onMouseEnter={[Function]}
>
- <span
- className="navbar-search-item-link"
- onMouseEnter={[Function]}
+ <div
+ className="display-flex-center"
>
<span
className="navbar-search-item-icons little-spacer-right"
>
foo
</span>
- </span>
- </Link>
- </Tooltip>
+ </div>
+ <div
+ className="navbar-search-item-right text-muted-2"
+ >
+ foo
+ </div>
+ </div>
+ </Link>
</li>
`;
key: string;
match?: string;
name: string;
- project?: string;
qualifier: string;
}
document.dispatchEvent(event);
}
-export function elementKeydown(element: ShallowWrapper, key: KeyboardKeys): void {
- const event = {
- currentTarget: { element },
- nativeEvent: {
- key,
- stopImmediatePropagation: () => {
- /* noop */
- }
- },
- preventDefault() {
- /*noop*/
- }
- };
-
- if (typeof element.type() === 'string') {
- // `type()` is string for native dom elements
- element.simulate('keydown', event);
- } else {
- element.prop<Function>('onKeyDown')(event);
- }
-}
-
export function resizeWindowTo(width?: number, height?: number) {
// `document.documentElement.clientHeight/clientWidth` are getters by default,
// so we need to redefine them. Pass `configurable: true` to allow to redefine