mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-26 10:33:44 +03:00
feat: arrow navigation in search results
This commit is contained in:
parent
e5458c0564
commit
fe3245a7a6
4
custom.d.ts
vendored
4
custom.d.ts
vendored
|
@ -20,3 +20,7 @@ declare var __REDOC_REVISION__: string;
|
||||||
declare type Dict<T> = {
|
declare type Dict<T> = {
|
||||||
[key: string]: T;
|
[key: string]: T;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface Element {
|
||||||
|
scrollIntoViewIfNeeded(centerIfNeeded?: boolean): void;
|
||||||
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ export interface SearchBoxProps {
|
||||||
export interface SearchBoxState {
|
export interface SearchBoxState {
|
||||||
results: any;
|
results: any;
|
||||||
term: string;
|
term: string;
|
||||||
|
activeItemIdx: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SearchResult {
|
interface SearchResult {
|
||||||
|
@ -29,11 +30,14 @@ interface SearchResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxState> {
|
export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxState> {
|
||||||
|
activeItemRef: MenuItem | null = null;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
results: [],
|
results: [],
|
||||||
term: '',
|
term: '',
|
||||||
|
activeItemIdx: -1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,14 +53,40 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
|
||||||
this.setState({
|
this.setState({
|
||||||
results: [],
|
results: [],
|
||||||
term: '',
|
term: '',
|
||||||
|
activeItemIdx: -1,
|
||||||
});
|
});
|
||||||
this.props.marker.unmark();
|
this.props.marker.unmark();
|
||||||
};
|
};
|
||||||
|
|
||||||
clearIfEsq = event => {
|
handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (event && event.keyCode === 27) {
|
if (event.keyCode === 27) {
|
||||||
|
// ESQ
|
||||||
this.clear();
|
this.clear();
|
||||||
}
|
}
|
||||||
|
if (event.keyCode === 40) {
|
||||||
|
// Arrow down
|
||||||
|
this.setState({
|
||||||
|
activeItemIdx: Math.min(this.state.activeItemIdx + 1, this.state.results.length - 1),
|
||||||
|
});
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
if (event.keyCode === 38) {
|
||||||
|
// Arrow up
|
||||||
|
this.setState({
|
||||||
|
activeItemIdx: Math.max(0, this.state.activeItemIdx - 1),
|
||||||
|
});
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
// enter
|
||||||
|
const activeResult = this.state.results[this.state.activeItemIdx];
|
||||||
|
if (activeResult) {
|
||||||
|
const item = this.props.getItemById(activeResult.id);
|
||||||
|
if (item) {
|
||||||
|
this.props.onActivate(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setResults(results: SearchDocument[], term: string) {
|
setResults(results: SearchDocument[], term: string) {
|
||||||
|
@ -84,6 +114,7 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { activeItemIdx } = this.state;
|
||||||
const results: SearchResult[] = this.state.results.map(res => ({
|
const results: SearchResult[] = this.state.results.map(res => ({
|
||||||
item: this.props.getItemById(res.id),
|
item: this.props.getItemById(res.id),
|
||||||
score: res.score,
|
score: res.score,
|
||||||
|
@ -97,16 +128,20 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
<SearchInput
|
<SearchInput
|
||||||
value={this.state.term}
|
value={this.state.term}
|
||||||
onKeyDown={this.clearIfEsq}
|
onKeyDown={this.handleKeyDown}
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
type="text"
|
type="text"
|
||||||
onChange={this.search}
|
onChange={this.search}
|
||||||
/>
|
/>
|
||||||
{results.length > 0 && (
|
{results.length > 0 && (
|
||||||
<SearchResultsBox>
|
<SearchResultsBox>
|
||||||
{results.map(res => (
|
{results.map((res, idx) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
item={res.item}
|
item={Object.create(res.item, {
|
||||||
|
active: {
|
||||||
|
value: idx === activeItemIdx,
|
||||||
|
},
|
||||||
|
})}
|
||||||
onActivate={this.props.onActivate}
|
onActivate={this.props.onActivate}
|
||||||
withoutChildren={true}
|
withoutChildren={true}
|
||||||
key={res.item.id}
|
key={res.item.id}
|
||||||
|
|
|
@ -71,6 +71,10 @@ export const SearchResultsBox = styled.div.attrs({
|
||||||
> svg {
|
> svg {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: #e1e1e1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
@ -14,15 +14,33 @@ interface MenuItemProps {
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class MenuItem extends React.Component<MenuItemProps> {
|
export class MenuItem extends React.Component<MenuItemProps> {
|
||||||
|
ref: Element | null;
|
||||||
|
|
||||||
activate = (evt: React.MouseEvent<HTMLElement>) => {
|
activate = (evt: React.MouseEvent<HTMLElement>) => {
|
||||||
this.props.onActivate!(this.props.item);
|
this.props.onActivate!(this.props.item);
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
if (this.props.item.active) {
|
||||||
|
this.scrollIntoView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollIntoView() {
|
||||||
|
if (this.ref) {
|
||||||
|
this.ref.scrollIntoViewIfNeeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveRef = ref => {
|
||||||
|
this.ref = ref;
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { item, withoutChildren } = this.props;
|
const { item, withoutChildren } = this.props;
|
||||||
return (
|
return (
|
||||||
<MenuItemLi onClick={this.activate} depth={item.depth}>
|
<MenuItemLi onClick={this.activate} depth={item.depth} innerRef={this.saveRef}>
|
||||||
{item.type === 'operation' ? (
|
{item.type === 'operation' ? (
|
||||||
<OperationMenuItemContent item={item as OperationModel} />
|
<OperationMenuItemContent item={item as OperationModel} />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -242,9 +242,11 @@ export class MenuStore {
|
||||||
*/
|
*/
|
||||||
@action.bound
|
@action.bound
|
||||||
activateAndScroll(item: IMenuItem | undefined, updateHash?: boolean, rewriteHistory?: boolean) {
|
activateAndScroll(item: IMenuItem | undefined, updateHash?: boolean, rewriteHistory?: boolean) {
|
||||||
this.activate(item, updateHash, rewriteHistory);
|
// item here can be a copy from search results so find corresponding item from menu
|
||||||
|
const menuItem = (item && this.getItemById(item.id)) || item;
|
||||||
|
this.activate(menuItem, updateHash, rewriteHistory);
|
||||||
this.scrollToActive();
|
this.scrollToActive();
|
||||||
if (!item || !item.items.length) {
|
if (!menuItem || !menuItem.items.length) {
|
||||||
this.closeSidebar();
|
this.closeSidebar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,3 +23,53 @@ export function html2Str(html: string): string {
|
||||||
})
|
})
|
||||||
.join(' ');
|
.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scrollIntoViewIfNeeded polyfill
|
||||||
|
|
||||||
|
if (!(Element as any).prototype.scrollIntoViewIfNeeded) {
|
||||||
|
(Element as any).prototype.scrollIntoViewIfNeeded = function(centerIfNeeded) {
|
||||||
|
centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded;
|
||||||
|
|
||||||
|
const parent = this.parentNode;
|
||||||
|
const parentComputedStyle = window.getComputedStyle(parent, undefined);
|
||||||
|
const parentBorderTopWidth = parseInt(
|
||||||
|
parentComputedStyle.getPropertyValue('border-top-width'),
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
const parentBorderLeftWidth = parseInt(
|
||||||
|
parentComputedStyle.getPropertyValue('border-left-width'),
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
const overTop = this.offsetTop - parent.offsetTop < parent.scrollTop;
|
||||||
|
const overBottom =
|
||||||
|
this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth >
|
||||||
|
parent.scrollTop + parent.clientHeight;
|
||||||
|
const overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft;
|
||||||
|
const overRight =
|
||||||
|
this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth >
|
||||||
|
parent.scrollLeft + parent.clientWidth;
|
||||||
|
const alignWithTop = overTop && !overBottom;
|
||||||
|
|
||||||
|
if ((overTop || overBottom) && centerIfNeeded) {
|
||||||
|
parent.scrollTop =
|
||||||
|
this.offsetTop -
|
||||||
|
parent.offsetTop -
|
||||||
|
parent.clientHeight / 2 -
|
||||||
|
parentBorderTopWidth +
|
||||||
|
this.clientHeight / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((overLeft || overRight) && centerIfNeeded) {
|
||||||
|
parent.scrollLeft =
|
||||||
|
this.offsetLeft -
|
||||||
|
parent.offsetLeft -
|
||||||
|
parent.clientWidth / 2 -
|
||||||
|
parentBorderLeftWidth +
|
||||||
|
this.clientWidth / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
|
||||||
|
this.scrollIntoView(alignWithTop);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user