-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- replace evergreen-ui's TagInput with a custom TagInput - fix issue where 2-3 tags in search would cause the TagInput to overflow the header - add a 'ghost' style to tag input on edit page to get a slightly cleaner look
- Loading branch information
Showing
5 changed files
with
163 additions
and
74 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import { cn } from "@udecode/cn"; | ||
import { observable } from "mobx"; | ||
import { observer } from "mobx-react-lite"; | ||
import * as React from "react"; | ||
|
||
// todo(chris): Refactor this to accept a list of options / details dynanmically | ||
const availableTags = ["in:", "tag:", "title:", "text:", "before:"]; | ||
|
||
interface TagInputProps { | ||
tokens: string[]; | ||
onAdd: (token: string) => void; | ||
onRemove: (token: string) => void; | ||
/** Whether to show the dropdown on focus */ | ||
dropdownEnabled?: boolean; | ||
/** placeholder text */ | ||
placeholder?: string; | ||
/** When true, hide the borders / disable padding */ | ||
ghost?: boolean; | ||
} | ||
|
||
const TagInput = observer((props: TagInputProps) => { | ||
const inputRef = React.useRef<HTMLInputElement>(null); | ||
const containerRef = React.useRef<HTMLDivElement>(null); | ||
const [dropdown, _] = React.useState(observable({ open: false })); | ||
|
||
// Close the typeahead menu when clicking outside of the dropdown | ||
React.useEffect(() => { | ||
const handleClickOutside = (e: MouseEvent) => { | ||
if ( | ||
containerRef.current && | ||
!containerRef.current.contains(e.target as Node) | ||
) { | ||
dropdown.open = false; | ||
} | ||
}; | ||
document.addEventListener("mousedown", handleClickOutside); | ||
return () => document.removeEventListener("mousedown", handleClickOutside); | ||
}, []); | ||
|
||
return ( | ||
<div | ||
className={cn( | ||
"flex w-0 max-w-full flex-grow flex-col rounded-sm border bg-background text-xs drag-none", | ||
props.ghost && "border-none", | ||
)} | ||
ref={containerRef} | ||
onClick={() => inputRef.current?.focus()} | ||
> | ||
<div | ||
className={cn( | ||
"flex flex-grow items-center p-1.5", | ||
props.ghost && "p-0", | ||
)} | ||
> | ||
{props.tokens.map((token, idx) => ( | ||
<Tag key={idx} token={token} remove={props.onRemove} /> | ||
))} | ||
<input | ||
ref={inputRef} | ||
className="w-0 min-w-8 flex-shrink flex-grow outline-none" | ||
type="text" | ||
placeholder={props.tokens.length ? "" : props.placeholder} | ||
onKeyDown={(e) => { | ||
if (e.key === "Backspace" && e.currentTarget.value === "") { | ||
// remove the last search token, if any | ||
if (props.tokens.length) { | ||
props.onRemove(props.tokens[props.tokens.length - 1]); | ||
setTimeout(() => inputRef.current?.focus(), 0); // Refocus the input | ||
} | ||
} | ||
|
||
if (e.key === "Enter" && e.currentTarget.value.trim() !== "") { | ||
props.onAdd(e.currentTarget.value.trim()); // Add the token | ||
e.currentTarget.value = ""; // Clear input | ||
|
||
// Unfocus and close the dropdown; after entering a tag, the user | ||
// likely wants to view the search results | ||
// e.currentTarget.blur(); | ||
// dropdown.open = false; | ||
} | ||
|
||
// I'm angry, get me out of here! (close dropdown) | ||
if (e.key === "Escape") { | ||
e.currentTarget.value = ""; | ||
e.currentTarget.blur(); | ||
dropdown.open = false; | ||
} | ||
}} | ||
onFocus={() => (dropdown.open = true)} | ||
onInput={() => (dropdown.open = true)} // open menu anytime user types | ||
/> | ||
</div> | ||
<div className="relative"> | ||
{props.dropdownEnabled && dropdown.open && ( | ||
<div className="absolute left-0 top-1 z-10 mt-2 w-full border bg-white shadow-md"> | ||
{availableTags.slice(0, 5).map((tag, idx) => ( | ||
<div | ||
key={idx} | ||
className="flex cursor-pointer justify-between p-2 hover:bg-gray-200" | ||
onMouseDown={(e) => { | ||
e.preventDefault(); // Prevent blur | ||
inputRef.current!.value = tag; // Set input to tag | ||
}} | ||
> | ||
<span>{tag}</span> | ||
<span className="text-gray-400"> | ||
{tag === "in:" && "Filter to specific journal"} | ||
{tag === "tag:" && "Filter to specific tag"} | ||
{tag === "title:" && "Filter by title"} | ||
{tag === "text:" && "Search body text"} | ||
{tag === "before:" && | ||
"Filter to notes before date (YYYY-MM-DD)"} | ||
</span> | ||
</div> | ||
))} | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
}); | ||
|
||
export default TagInput; | ||
|
||
interface TagProps { | ||
token: string; | ||
remove: (token: string) => void; | ||
} | ||
|
||
const Tag = ({ token, remove }: TagProps) => { | ||
return ( | ||
<span className="mr-2 flex flex-shrink items-center overflow-hidden text-ellipsis whitespace-nowrap rounded-sm border border-slate-800 bg-violet-200 px-1 py-0.5 text-xs text-slate-600"> | ||
<span className="flex-shrink overflow-hidden text-ellipsis">{token}</span> | ||
<button | ||
className="text-grey-400 ml-1 flex-shrink-0" | ||
onClick={() => remove(token)} | ||
> | ||
× | ||
</button> | ||
</span> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,48 +1,18 @@ | ||
import { TagInput } from "evergreen-ui"; | ||
import { observer } from "mobx-react-lite"; | ||
import React from "react"; | ||
import { SearchStore } from "../SearchStore"; | ||
import TagInput from "../../../components/TagInput"; | ||
import { useSearchStore } from "../SearchStore"; | ||
|
||
interface Props { | ||
store: SearchStore; | ||
} | ||
|
||
/** | ||
* SearchDocuments is a component that provides a search input for searching documents. | ||
*/ | ||
const SearchDocuments = (props: Props) => { | ||
function onRemove(tag: string | React.ReactNode, idx: number) { | ||
if (typeof tag !== "string") return; | ||
props.store.removeToken(tag); | ||
} | ||
|
||
function onAdd(tokens: string[]) { | ||
if (tokens.length > 1) { | ||
// https://evergreen.segment.com/components/tag-input | ||
// Documents say this is single value, Type says array | ||
// Testing says array but with only one value... unsure how multiple | ||
// values end up in the array. | ||
console.warn( | ||
"TagInput.onAdd called with > 1 token? ", | ||
tokens, | ||
"ignoring extra tokens", | ||
); | ||
} | ||
|
||
const token = tokens[0]; | ||
props.store.addToken(token); | ||
} | ||
export const SearchInput = observer(() => { | ||
const searchStore = useSearchStore()!; | ||
|
||
return ( | ||
<TagInput | ||
className="drag-none" | ||
flexGrow={1} | ||
inputProps={{ placeholder: "Search journals" }} | ||
values={props.store.searchTokens} | ||
onAdd={onAdd} | ||
onRemove={onRemove} | ||
tokens={searchStore.searchTokens} | ||
onAdd={(token) => searchStore.addToken(token)} | ||
onRemove={(token) => searchStore.removeToken(token)} | ||
placeholder="Search notes" | ||
dropdownEnabled={true} | ||
/> | ||
); | ||
}; | ||
|
||
export default observer(SearchDocuments); | ||
}); |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters