Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Search bar: Make it into its own component in order to fix layout bugs #2545

Merged
merged 8 commits into from
Jan 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
68 changes: 32 additions & 36 deletions lib/note-content-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ import {
withCheckboxCharacters,
withCheckboxSyntax,
} from './utils/task-transform';
import IconButton from './icon-button';
import ChevronRightIcon from './icons/chevron-right';

import * as S from './state';
import * as T from './types';
Expand Down Expand Up @@ -88,6 +86,7 @@ type StateProps = {
note: T.Note;
notes: Map<T.EntityId, T.Note>;
searchQuery: string;
selectedSearchMatchIndex: number | null;
spellCheckEnabled: boolean;
theme: T.Theme;
};
Expand All @@ -103,6 +102,8 @@ type DispatchProps = {
end: number,
direction: 'LTR' | 'RTL'
) => any;
storeNumberOfMatchesInNote: (matches: number) => any;
storeSearchSelection: (index: number) => any;
};

type Props = OwnProps & StateProps & DispatchProps;
Expand All @@ -113,7 +114,6 @@ type OwnState = {
noteId: T.EntityId | null;
overTodo: boolean;
searchQuery: string;
selectedSearchMatchIndex: number | null;
};

class NoteContentEditor extends Component<Props> {
Expand All @@ -130,7 +130,6 @@ class NoteContentEditor extends Component<Props> {
noteId: null,
overTodo: false,
searchQuery: '',
selectedSearchMatchIndex: null,
};

static getDerivedStateFromProps(props: Props, state: OwnState) {
Expand All @@ -146,16 +145,18 @@ class NoteContentEditor extends Component<Props> {

const editor = noteChanged ? (goFast ? 'fast' : 'full') : state.editor;

// reset search selection when either the note or the search changes
// to avoid, for example, opening a note and having the fourth match selected (or "4 of 1")
const searchChanged = props.searchQuery !== state.searchQuery;
const selectedSearchMatchIndex =
noteChanged || searchChanged ? null : state.selectedSearchMatchIndex;
if (noteChanged || searchChanged) {
props.storeSearchSelection(0);
}

return {
content,
editor,
noteId: props.noteId,
searchQuery: props.searchQuery,
selectedSearchMatchIndex,
};
}

Expand Down Expand Up @@ -342,12 +343,22 @@ class NoteContentEditor extends Component<Props> {
this.state.editor === 'full' &&
prevProps.searchQuery !== this.props.searchQuery
) {
this.editor?.layout();
this.setDecorators();
}

if (
this.editor &&
this.state.editor === 'full' &&
prevProps.selectedSearchMatchIndex !== this.props.selectedSearchMatchIndex
) {
this.setSearchSelection(this.props.selectedSearchMatchIndex);
}
}

setDecorators = () => {
this.matchesInNote = this.searchMatches() ?? [];
this.props.storeNumberOfMatchesInNote(this.matchesInNote.length);
const titleDecoration = this.getTitleDecoration() ?? [];

this.decorations = this.editor.deltaDecorations(this.decorations, [
Expand Down Expand Up @@ -1058,16 +1069,18 @@ class NoteContentEditor extends Component<Props> {
};

setNextSearchSelection = () => {
const { selectedSearchMatchIndex: index } = this.state;
const { selectedSearchMatchIndex: index } = this.props;
const total = this.matchesInNote.length;
const newIndex = (total + (index ?? -1) + 1) % total;
this.props.storeSearchSelection(newIndex);
this.setSearchSelection(newIndex);
};

setPrevSearchSelection = () => {
const { selectedSearchMatchIndex: index } = this.state;
const { selectedSearchMatchIndex: index } = this.props;
const total = this.matchesInNote.length;
const newIndex = (total + (index ?? total) - 1) % total;
this.props.storeSearchSelection(newIndex);
this.setSearchSelection(newIndex);
};

Expand All @@ -1076,15 +1089,14 @@ class NoteContentEditor extends Component<Props> {
return;
}
const range = this.matchesInNote[index].range;
this.setState({ selectedSearchMatchIndex: index });
this.editor.setSelection(range);
this.editor.revealLineInCenter(range.startLineNumber);
this.focusEditor();
};

render() {
const { lineLength, noteId, searchQuery, theme } = this.props;
const { content, editor, overTodo, selectedSearchMatchIndex } = this.state;
const { content, editor, overTodo } = this.state;
const searchMatches = searchQuery ? this.searchMatches() : [];

const editorPadding = getEditorPadding(
Expand Down Expand Up @@ -1153,31 +1165,6 @@ class NoteContentEditor extends Component<Props> {
value={content}
/>
)}
{searchQuery.length > 0 && searchMatches && (
<div className="search-results">
<div>
{selectedSearchMatchIndex === null
? `${searchMatches.length} Results`
: `${selectedSearchMatchIndex + 1} of ${searchMatches.length}`}
</div>
<span className="search-results-next">
<IconButton
disabled={searchMatches.length <= 1}
icon={<ChevronRightIcon />}
onClick={this.setNextSearchSelection}
title="Next"
/>
</span>
<span className="search-results-prev">
<IconButton
disabled={searchMatches.length <= 1}
icon={<ChevronRightIcon />}
onClick={this.setPrevSearchSelection}
title="Prev"
/>
</span>
</div>
)}
</div>
);
}
Expand All @@ -1196,6 +1183,7 @@ const mapStateToProps: S.MapState<StateProps> = (state) => ({
note: state.data.notes.get(state.ui.openedNote),
notes: state.data.notes,
searchQuery: state.ui.searchQuery,
selectedSearchMatchIndex: state.ui.selectedSearchMatchIndex,
spellCheckEnabled: state.settings.spellCheckEnabled,
theme: selectors.getTheme(state),
});
Expand All @@ -1212,6 +1200,14 @@ const mapDispatchToProps: S.MapDispatch<DispatchProps> = {
end,
direction,
}),
storeNumberOfMatchesInNote: (matches) => ({
type: 'STORE_NUMBER_OF_MATCHES_IN_NOTE',
matches,
}),
storeSearchSelection: (index) => ({
type: 'STORE_SEARCH_SELECTION',
index,
}),
};

export default connect(mapStateToProps, mapDispatchToProps)(NoteContentEditor);
15 changes: 14 additions & 1 deletion lib/note-editor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import SearchResultsBar from '../search-results-bar';
import TagField from '../tag-field';
import NoteDetail from '../note-detail';
import NotePreview from '../components/note-preview';
Expand All @@ -16,6 +17,8 @@ type StateProps = {
isEditorActive: boolean;
isSearchActive: boolean;
isSmallScreen: boolean;
hasSearchMatchesInNote: boolean;
hasSearchQuery: boolean;
keyboardShortcuts: boolean;
noteId: T.EntityId;
note: T.Note;
Expand Down Expand Up @@ -111,7 +114,13 @@ export class NoteEditor extends Component<Props> {
};

render() {
const { editMode, note, noteId } = this.props;
const {
editMode,
hasSearchQuery,
hasSearchMatchesInNote,
note,
noteId,
} = this.props;

if (!note) {
return (
Expand Down Expand Up @@ -139,6 +148,7 @@ export class NoteEditor extends Component<Props> {
storeHasFocus={this.storeTagFieldHasFocus}
/>
)}
{hasSearchQuery && hasSearchMatchesInNote && <SearchResultsBar />}
</div>
);
}
Expand All @@ -152,6 +162,9 @@ const mapStateToProps: S.MapState<StateProps> = (state) => ({
noteId: state.ui.openedNote,
note: state.data.notes.get(state.ui.openedNote),
revision: state.ui.selectedRevision,
hasSearchQuery: state.ui.searchQuery !== '',
hasSearchMatchesInNote:
!!state.ui.numberOfMatchesInNote && state.ui.numberOfMatchesInNote > 0,
isSearchActive: !!state.ui.searchQuery.length,
isSmallScreen: selectors.isSmallScreen(state),
});
Expand Down
83 changes: 83 additions & 0 deletions lib/search-results-bar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* External dependencies
*/
import React, { FunctionComponent, useEffect, useRef } from 'react';
import { connect } from 'react-redux';

/**
* Internal dependencies
*/
import IconButton from '../icon-button';
import ChevronRightIcon from '../icons/chevron-right';

import * as S from '../state';

type StateProps = {
selectedSearchMatchIndex: number | null;
numberOfMatchesInNote: number;
};

type DispatchProps = {
setSearchSelection: (index: number) => any;
};

type Props = StateProps & DispatchProps;

const SearchResultsBar: FunctionComponent<Props> = ({
selectedSearchMatchIndex: index,
numberOfMatchesInNote: total,
setSearchSelection,
}) => {
const setPrev = (event: MouseEvent) => {
const newIndex = (total + (index ?? -1) - 1) % total;
setSearchSelection(newIndex);
};

const setNext = (event: MouseEvent) => {
const newIndex = (total + (index ?? -1) + 1) % total;
setSearchSelection(newIndex);
};

return (
<div className="search-results">
<div>
{index === null ? `${total} Results` : `${index + 1} of ${total}`}
</div>
<span className="search-results-next">
<IconButton
disabled={total <= 1}
icon={<ChevronRightIcon />}
onClick={setNext}
title="Next"
/>
</span>
<span className="search-results-prev">
<IconButton
disabled={total <= 1}
icon={<ChevronRightIcon />}
onClick={setPrev}
title="Prev"
/>
</span>
</div>
);
};

const mapStateToProps: S.MapState<StateProps> = ({
ui: { selectedSearchMatchIndex, numberOfMatchesInNote },
}) => ({
selectedSearchMatchIndex,
numberOfMatchesInNote,
});

const mapDispatchToProps: S.MapDispatch<DispatchProps> = (dispatch) => ({
setSearchSelection: (index: number) =>
dispatch({
type: 'STORE_SEARCH_SELECTION',
index,
}),
});
codebykat marked this conversation as resolved.
Show resolved Hide resolved

SearchResultsBar.displayName = 'SearchResultsBar';

export default connect(mapStateToProps, mapDispatchToProps)(SearchResultsBar);
35 changes: 35 additions & 0 deletions lib/search-results-bar/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.search-results {
height: 56px;
line-height: 56px;
z-index: 100;
border-top: 1px solid $studio-gray-5;
background-color: $studio-white;
text-align: center;
user-select: none;

div {
display: inline-block;
}

.search-results-next,
.search-results-prev {
float: right;
padding: 0 6px;
width: 42px;
height: 100%;

svg {
fill: $studio-simplenote-blue-50;
}
}
.search-results-next {
margin-right: 6px;
}
.search-results-prev svg {
transform: scaleX(-1);
}

@media only screen and (max-width: $single-column) {
left: 0;
}
}
10 changes: 10 additions & 0 deletions lib/state/action-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ export type StoreEditorSelection = Action<
'STORE_EDITOR_SELECTION',
{ noteId: T.EntityId; start: number; end: number; direction: 'RTL' | 'LTR' }
>;
export type StoreNumberOfMatchesInNote = Action<
'STORE_NUMBER_OF_MATCHES_IN_NOTE',
{ matches: number }
>;
export type StoreSearchSelection = Action<
'STORE_SEARCH_SELECTION',
{ index: number }
>;
export type SystemThemeUpdate = Action<
'SYSTEM_THEME_UPDATE',
{ prefers: 'light' | 'dark' }
Expand Down Expand Up @@ -376,6 +384,8 @@ export type ActionType =
| ShowAllNotes
| ShowDialog
| StoreEditorSelection
| StoreNumberOfMatchesInNote
| StoreSearchSelection
| SubmitPendingChange
| SystemThemeUpdate
| TagBucketRemove
Expand Down