Skip to content

Commit

Permalink
✨ feat(search-bar): 添加快捷键操作支持
Browse files Browse the repository at this point in the history
  • Loading branch information
arvinxx committed Feb 13, 2021
1 parent 5e1e570 commit e5a1532
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,36 @@ import { Checkbox, Space } from 'antd';
import cls from 'classnames';

import { SearchService } from '../../../useSearchService';
import { KeyboardService } from '../../useKeyboardService';

import styles from './style.less';

interface Option {
key: SearchBar.SearchType;
title: string;
}

const options: Option[] = [
{ key: 'repo', title: '知识库' },
{ key: 'doc', title: '文档' },
{ key: 'topic', title: '主题' },
{ key: 'artboard', title: '画板' },
{ key: 'group', title: '团队' },
// { key: 'user', title: '用户' },
// { key: 'attachment', title: '附件' },
];

const Options: FC = () => {
const { type, setType, related, setRelated } = useContext(SearchService);
const {
type,
setType,
related,
setRelated,
options,
optionActiveIndex,
} = useContext(SearchService);
const { focusKey, focusOnOptions } = useContext(KeyboardService);

return (
<div className={styles.container}>
<Space>
{options.map((option) => (
{options.map((option, index) => (
<div
key={option.key}
className={cls({
[styles.option]: true,
[styles.optionActive]: type === option.key,
[styles.active]: type === option.key,
[styles.selected]:
focusKey === 'options' && optionActiveIndex === index,
})}
onClick={() => {
setType(option.key);
focusOnOptions();
}}
>
{option.title}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,27 @@
transition: all 150ms ease-in-out;
}

.optionActive {
.active {
color: @yuque-brand-color-dark;
background: @yuque-brand-color-light;
}

.selected {
color: white;
background: @yuque-brand-color-light-darken;
}

[theme='dark'] {
.option {
background: @dark-mode-background-dark;
}
.optionActive {
.active {
color: @dark-mode-yuque-brand-color-dark;
background: @dark-mode-yuque-brand-color-light;
}

.selected {
color: white;
background: @dark-mode-yuque-brand-color-light-lighter;
}
}
18 changes: 9 additions & 9 deletions src/contentScripts/searchBar/app/SearchInput/index.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
import type { FC } from 'react';
import React, { useContext } from 'react';
import { Input } from 'antd';
import { KeyboardService, useKeyboardService } from './useKeyboardService';

import Options from './components/Options';
import { SearchService } from '../useSearchService';
import { SearchBarService } from '../useSearchBarService';

import styles from './style.less';

const SearchInput: FC = () => {
const { onSearchEvent } = useContext(SearchService);
const { hide } = useContext(SearchBarService);

const keyboardService = useKeyboardService();
const { inputRef, focusOnInput } = keyboardService;
return (
<div>
<KeyboardService.Provider value={keyboardService}>
<Input
autoFocus
ref={inputRef}
className={styles.input}
placeholder={'请输入待搜索内容...'}
size={'large'}
onChange={onSearchEvent}
onKeyDown={(e) => {
if (e.key === 'Escape') {
hide();
}
}}
onFocus={focusOnInput}
// onKeyDown={onKeyDown}
/>
<Options />
</div>
</KeyboardService.Provider>
);
};

Expand Down
110 changes: 110 additions & 0 deletions src/contentScripts/searchBar/app/SearchInput/useKeyboardService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { useContext, useEffect, useRef, useState } from 'react';
import type { Input } from 'antd';

import { getServiceToken } from '@/utils';
import { SearchService } from '../useSearchService';
import { SearchBarService } from '../useSearchBarService';

type FocusType = 'input' | 'options' | 'result';

/**
* Keyboard 需要的状态
*/
export const useKeyboardService = () => {
const { setType, optionKeys, optionActiveIndex } = useContext(SearchService);
const { hide } = useContext(SearchBarService);

const [focusKey, setFocusKey] = useState<FocusType>('input');
const inputRef = useRef<Input>(null);

/**
* 按 Tabs 键切换选中 type
* @param back
*/
const switchOptionIndex = (back?: boolean) => {
let newIndex: number;

if (back) {
newIndex =
optionActiveIndex === 0 ? optionKeys.length - 1 : optionActiveIndex - 1;
} else {
newIndex =
optionActiveIndex === optionKeys.length - 1 ? 0 : optionActiveIndex + 1;
}

setType(optionKeys[newIndex]);
};

const focusOnInput = () => {
inputRef.current?.focus();
setFocusKey('input');
};

const focusOnOptions = () => {
inputRef.current?.blur();
setFocusKey('options');
};

// 将焦点切换到 Options
const onKeyDown = (event: KeyboardEvent) => {
// 焦点在 options 的情况
if (focusKey === 'options') {
console.log(event.key);
switch (event.key) {
case 'Tab':
event.preventDefault();
switchOptionIndex(event.shiftKey);
break;

case 'ArrowRight':
switchOptionIndex();
break;
case 'ArrowLeft':
switchOptionIndex(true);
break;
case 'ArrowUp':
case 'Escape':
focusOnInput();
break;
default:
}
}

// 焦点在 input 的情况
if (focusKey === 'input') {
switch (event.key) {
case 'Tab':
event.preventDefault();
focusOnOptions();
switchOptionIndex(event.shiftKey);
break;
case 'Escape':
hide();
break;
case 'ArrowDown':
event.preventDefault();
focusOnOptions();
break;
default:
}
}
};

useEffect(() => {
window.onkeydown = onKeyDown;

return () => {
window.onkeydown = null;
};
}, [focusKey, optionActiveIndex]);

return {
onKeyDown,
focusKey,
inputRef,
focusOnInput,
focusOnOptions,
};
};

export const KeyboardService = getServiceToken(useKeyboardService);
10 changes: 9 additions & 1 deletion src/contentScripts/searchBar/app/SearchResult/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ const SearchResult: FC = () => {
const { result, loading } = useContext(SearchService);

return (
<Skeleton loading={loading} active className={styles.skeleton}>
<Skeleton
loading={loading}
title={false}
paragraph={{
rows: 4,
}}
active
className={styles.skeleton}
>
{result?.map((item) => {
const { title, info, id, url, target, type } = item;

Expand Down
1 change: 1 addition & 0 deletions src/contentScripts/searchBar/app/SearchResult/style.less
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@import '~theme/index';
.skeleton {
margin-top: 16px;
padding: 8px 12px 24px;
}
.repo {
Expand Down
4 changes: 2 additions & 2 deletions src/contentScripts/searchBar/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import SearchResult from './SearchResult';
import AnimatedHeight from './AnimatedHeight';

import styles from './style.less';
import { isDev } from '@/utils';

const SearchBar: FC = () => {
const { visible, searchBarRef } = useContext(SearchBarService);
Expand Down Expand Up @@ -60,7 +61,6 @@ const SearchBar: FC = () => {
<Button
type={'primary'}
onClick={() => {
console.log(chrome.runtime);
chrome.runtime.sendMessage({ action: 'openOptionsPage' });
}}
>
Expand All @@ -76,7 +76,7 @@ const SearchBar: FC = () => {

export default () => (
<YuqueTokenService.Provider value={useYuqueTokenService()}>
<SearchBarService.Provider value={useSearchBarService()}>
<SearchBarService.Provider value={useSearchBarService(isDev)}>
<SearchService.Provider value={useSearchService()}>
<SearchBar />
</SearchService.Provider>
Expand Down
4 changes: 0 additions & 4 deletions src/contentScripts/searchBar/app/useSearchBarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@ export default function useSearchBarService(initState?: boolean) {
[visible],
);

useHotkeys('Esc', () => {
hide();
});

return {
visible,
show,
Expand Down
50 changes: 40 additions & 10 deletions src/contentScripts/searchBar/app/useSearchService.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
import { getServiceToken, request } from '@/utils';
import { ChangeEvent } from 'react';
import { useState } from 'react';
import type { ChangeEvent } from 'react';
import { useMemo, useState } from 'react';
import { useDebounce } from 'ahooks';

import { useEventCallback } from 'rxjs-hooks';
import { map, debounceTime, switchMap, combineLatest } from 'rxjs/operators';
import { from, of } from 'rxjs';
import { from } from 'rxjs';
import {
map,
debounceTime,
switchMap,
combineLatest,
filter,
tap,
} from 'rxjs/operators';

import { getServiceToken, request } from '@/utils';

/**
* SearchInput 需要的状态
*/
export const useSearchService = () => {
const options: SearchBar.Option[] = useMemo(
() => [
{ key: 'repo', title: '知识库' },
{ key: 'doc', title: '文档' },
{ key: 'topic', title: '主题' },
{ key: 'artboard', title: '画板' },
{ key: 'group', title: '团队' },
],
[],
);

const request$ = (params: SearchBar.SearchParams) =>
from(
request.get<SearchBar.SearchResponse>('/search', {
Expand All @@ -25,8 +46,10 @@ export const useSearchService = () => {
const [type, setType] = useState<SearchBar.SearchType>('repo');
// 与我相关
const [related, setRelated] = useState(true);
const [loading, setLoading] = useState(false);

const defaultState = { data: [], total: 0 };

/**
* 搜索方法
*/
Expand All @@ -38,22 +61,26 @@ export const useSearchService = () => {
(event$, _, input$) =>
event$.pipe(
// 1. 获取 value
map(event => event.target.value),
map((event) => event.target.value.trim()),
// 2. 防抖
debounceTime(600),
// 过滤掉没有值的情况
filter((value) => value.length !== 0),
// 提供输入
combineLatest(input$),
// 3. 发起请求
switchMap(([value, input]) => {
// 没有值返回 null
if (value.length === 0) return of({ data: [], meta: { total: 0 } });
setLoading(true);

// 直接返回结果
const [relate, searchType] = input;
return request$({ q: value, type: searchType, related: relate });
}),
tap(() => {
setLoading(false);
}),
// 4. 解构得值
map(response => {
map((response) => {
if (!response) return defaultState;

// 之后在这一步做值解构
Expand All @@ -66,9 +93,12 @@ export const useSearchService = () => {
);

return {
options,
optionKeys: options.map((o) => o.key),
optionActiveIndex: options.findIndex((o) => o.key === type),
total,
result: data,
loading: false,
loading: useDebounce(loading, { wait: 500 }),
onSearchEvent,
type,
setType,
Expand Down
Loading

0 comments on commit e5a1532

Please sign in to comment.