fix: make sure input is visible when no tags are overflowed
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { SNTag } from '@standardnotes/snjs';
|
import { SNTag } from '@standardnotes/snjs';
|
||||||
import { FunctionalComponent, RefObject } from 'preact';
|
import { FunctionalComponent, RefObject } from 'preact';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { Disclosure, DisclosurePanel } from '@reach/disclosure';
|
import { Disclosure, DisclosurePanel } from '@reach/disclosure';
|
||||||
import { useCloseOnBlur } from './utils';
|
import { useCloseOnBlur } from './utils';
|
||||||
@@ -11,15 +11,15 @@ type Props = {
|
|||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
tagsRef: RefObject<HTMLButtonElement[]>;
|
tagsRef: RefObject<HTMLButtonElement[]>;
|
||||||
tabIndex: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
||||||
application,
|
application,
|
||||||
appState,
|
appState,
|
||||||
tagsRef,
|
tagsRef,
|
||||||
tabIndex,
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const { tags, tagsContainerMaxWidth, tagsOverflowed } = appState.activeNote;
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||||
const [dropdownMaxHeight, setDropdownMaxHeight] =
|
const [dropdownMaxHeight, setDropdownMaxHeight] =
|
||||||
@@ -84,6 +84,23 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
|||||||
await createAndAddNewTag();
|
await createAndAddNewTag();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reloadInputOverflowed = useCallback(() => {
|
||||||
|
let overflowed = false;
|
||||||
|
if (!tagsOverflowed && tagsRef.current && tagsRef.current.length > 0) {
|
||||||
|
const firstTagTop = tagsRef.current[0].offsetTop;
|
||||||
|
overflowed = inputRef.current.offsetTop > firstTagTop;
|
||||||
|
}
|
||||||
|
appState.activeNote.setInputOverflowed(overflowed);
|
||||||
|
}, [appState.activeNote, tagsOverflowed, tagsRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reloadInputOverflowed();
|
||||||
|
}, [
|
||||||
|
reloadInputOverflowed,
|
||||||
|
tagsContainerMaxWidth,
|
||||||
|
tags,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHintVisible(
|
setHintVisible(
|
||||||
searchQuery !== '' && !tagResults.some((tag) => tag.title === searchQuery)
|
searchQuery !== '' && !tagResults.some((tag) => tag.title === searchQuery)
|
||||||
@@ -100,7 +117,7 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
|||||||
onChange={onSearchQueryChange}
|
onChange={onSearchQueryChange}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Add tag"
|
placeholder="Add tag"
|
||||||
tabIndex={tabIndex}
|
tabIndex={tagsOverflowed ? -1 : 0}
|
||||||
onBlur={closeOnBlur}
|
onBlur={closeOnBlur}
|
||||||
onFocus={showDropdown}
|
onFocus={showDropdown}
|
||||||
onKeyUp={(event) => {
|
onKeyUp={(event) => {
|
||||||
@@ -129,7 +146,7 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
|||||||
className="sn-dropdown-item"
|
className="sn-dropdown-item"
|
||||||
onClick={() => onTagOptionClick(tag)}
|
onClick={() => onTagOptionClick(tag)}
|
||||||
onBlur={closeOnBlur}
|
onBlur={closeOnBlur}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tagsOverflowed ? -1 : 0}
|
||||||
>
|
>
|
||||||
<Icon type="hashtag" className="color-neutral mr-2 min-h-5 min-w-5" />
|
<Icon type="hashtag" className="color-neutral mr-2 min-h-5 min-w-5" />
|
||||||
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
|
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||||
@@ -167,7 +184,7 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
|||||||
className="sn-dropdown-item"
|
className="sn-dropdown-item"
|
||||||
onClick={onTagHintClick}
|
onClick={onTagHintClick}
|
||||||
onBlur={closeOnBlur}
|
onBlur={closeOnBlur}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tagsOverflowed ? -1 : 0}
|
||||||
>
|
>
|
||||||
<span>Create new tag:</span>
|
<span>Create new tag:</span>
|
||||||
<span className="bg-contrast rounded text-xs color-text py-1 pl-1 pr-2 flex items-center ml-2">
|
<span className="bg-contrast rounded text-xs color-text py-1 pl-1 pr-2 flex items-center ml-2">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type Props = {
|
|||||||
|
|
||||||
const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
||||||
const {
|
const {
|
||||||
|
inputOverflowed,
|
||||||
overflowedTagsCount,
|
overflowedTagsCount,
|
||||||
tags,
|
tags,
|
||||||
tagsContainerMaxWidth,
|
tagsContainerMaxWidth,
|
||||||
@@ -82,8 +83,10 @@ const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (tagsRef.current[lastVisibleTagIndex]) {
|
if (tagsRef.current[lastVisibleTagIndex]) {
|
||||||
const { offsetLeft: lastVisibleTagLeft, clientWidth: lastVisibleTagWidth } =
|
const {
|
||||||
tagsRef.current[lastVisibleTagIndex];
|
offsetLeft: lastVisibleTagLeft,
|
||||||
|
clientWidth: lastVisibleTagWidth,
|
||||||
|
} = tagsRef.current[lastVisibleTagIndex];
|
||||||
setOverflowCountPosition(lastVisibleTagLeft + lastVisibleTagWidth);
|
setOverflowCountPosition(lastVisibleTagLeft + lastVisibleTagWidth);
|
||||||
}
|
}
|
||||||
}, [lastVisibleTagIndex, tagsContainerExpanded]);
|
}, [lastVisibleTagIndex, tagsContainerExpanded]);
|
||||||
@@ -108,11 +111,7 @@ const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reloadTagsContainerLayout();
|
reloadTagsContainerLayout();
|
||||||
}, [
|
}, [reloadTagsContainerLayout, tags, tagsContainerMaxWidth]);
|
||||||
reloadTagsContainerLayout,
|
|
||||||
tags,
|
|
||||||
tagsContainerMaxWidth,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let tagResizeObserver: ResizeObserver;
|
let tagResizeObserver: ResizeObserver;
|
||||||
@@ -120,7 +119,9 @@ const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
|||||||
tagResizeObserver = new ResizeObserver(() => {
|
tagResizeObserver = new ResizeObserver(() => {
|
||||||
reloadTagsContainerLayout();
|
reloadTagsContainerLayout();
|
||||||
});
|
});
|
||||||
tagsRef.current.forEach((tagElement) => tagResizeObserver.observe(tagElement));
|
tagsRef.current.forEach((tagElement) =>
|
||||||
|
tagResizeObserver.observe(tagElement)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -132,15 +133,17 @@ const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex transition-height duration-150 h-9 relative"
|
className={`flex transition-height duration-150 relative ${
|
||||||
|
inputOverflowed ? 'h-18' : 'h-9'
|
||||||
|
}`}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
style={tagsContainerExpanded ? { height: expandedContainerHeight } : {}}
|
style={tagsContainerExpanded ? { height: expandedContainerHeight } : {}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={tagsContainerRef}
|
ref={tagsContainerRef}
|
||||||
className={`absolute bg-default h-9 flex flex-wrap pl-1 -ml-1 ${
|
className={`absolute bg-default flex flex-wrap pl-1 -ml-1 ${
|
||||||
tagsContainerExpanded ? '' : 'overflow-hidden'
|
inputOverflowed ? 'h-18' : 'h-9'
|
||||||
}`}
|
} ${tagsContainerExpanded || !tagsOverflowed ? '' : 'overflow-hidden'}`}
|
||||||
style={{
|
style={{
|
||||||
maxWidth: tagsContainerMaxWidth,
|
maxWidth: tagsContainerMaxWidth,
|
||||||
}}
|
}}
|
||||||
@@ -152,16 +155,17 @@ const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
|||||||
index={index}
|
index={index}
|
||||||
tag={tag}
|
tag={tag}
|
||||||
maxWidth={tagsContainerMaxWidth}
|
maxWidth={tagsContainerMaxWidth}
|
||||||
overflowed={!tagsContainerExpanded &&
|
overflowed={
|
||||||
|
!tagsContainerExpanded &&
|
||||||
!!lastVisibleTagIndex &&
|
!!lastVisibleTagIndex &&
|
||||||
index > lastVisibleTagIndex}
|
index > lastVisibleTagIndex
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<AutocompleteTagInput
|
<AutocompleteTagInput
|
||||||
application={application}
|
application={application}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
tagsRef={tagsRef}
|
tagsRef={tagsRef}
|
||||||
tabIndex={tagsOverflowed ? -1 : 0}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{tagsOverflowed && (
|
{tagsOverflowed && (
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ import { WebApplication } from '../application';
|
|||||||
import { AppState } from './app_state';
|
import { AppState } from './app_state';
|
||||||
|
|
||||||
export class ActiveNoteState {
|
export class ActiveNoteState {
|
||||||
|
inputOverflowed = false;
|
||||||
|
overflowedTagsCount = 0;
|
||||||
tags: SNTag[] = [];
|
tags: SNTag[] = [];
|
||||||
tagsContainerMaxWidth: number | 'auto' = 0;
|
tagsContainerMaxWidth: number | 'auto' = 0;
|
||||||
tagsContainerExpanded = false;
|
|
||||||
overflowedTagsCount = 0;
|
|
||||||
tagFocused = false;
|
tagFocused = false;
|
||||||
|
tagsContainerExpanded = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private application: WebApplication,
|
private application: WebApplication,
|
||||||
@@ -25,18 +26,20 @@ export class ActiveNoteState {
|
|||||||
appEventListeners: (() => void)[]
|
appEventListeners: (() => void)[]
|
||||||
) {
|
) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
tags: observable,
|
inputOverflowed: observable,
|
||||||
tagsContainerMaxWidth: observable,
|
|
||||||
tagsContainerExpanded: observable,
|
|
||||||
overflowedTagsCount: observable,
|
overflowedTagsCount: observable,
|
||||||
|
tags: observable,
|
||||||
tagFocused: observable,
|
tagFocused: observable,
|
||||||
|
tagsContainerExpanded: observable,
|
||||||
|
tagsContainerMaxWidth: observable,
|
||||||
|
|
||||||
tagsOverflowed: computed,
|
tagsOverflowed: computed,
|
||||||
|
|
||||||
setTagsContainerMaxWidth: action,
|
setInputOverflowed: action,
|
||||||
setTagsContainerExpanded: action,
|
|
||||||
setOverflowedTagsCount: action,
|
setOverflowedTagsCount: action,
|
||||||
setTagFocused: action,
|
setTagFocused: action,
|
||||||
|
setTagsContainerExpanded: action,
|
||||||
|
setTagsContainerMaxWidth: action,
|
||||||
reloadTags: action,
|
reloadTags: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,12 +61,8 @@ export class ActiveNoteState {
|
|||||||
return this.overflowedTagsCount > 0 && !this.tagsContainerExpanded;
|
return this.overflowedTagsCount > 0 && !this.tagsContainerExpanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTagsContainerMaxWidth(width: number): void {
|
setInputOverflowed(overflowed: boolean): void {
|
||||||
this.tagsContainerMaxWidth = width;
|
this.inputOverflowed = overflowed;
|
||||||
}
|
|
||||||
|
|
||||||
setTagsContainerExpanded(expanded: boolean): void {
|
|
||||||
this.tagsContainerExpanded = expanded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setOverflowedTagsCount(count: number): void {
|
setOverflowedTagsCount(count: number): void {
|
||||||
@@ -74,6 +73,14 @@ export class ActiveNoteState {
|
|||||||
this.tagFocused = focused;
|
this.tagFocused = focused;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTagsContainerExpanded(expanded: boolean): void {
|
||||||
|
this.tagsContainerExpanded = expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTagsContainerMaxWidth(width: number): void {
|
||||||
|
this.tagsContainerMaxWidth = width;
|
||||||
|
}
|
||||||
|
|
||||||
reloadTags(): void {
|
reloadTags(): void {
|
||||||
const { activeNote } = this;
|
const { activeNote } = this;
|
||||||
if (activeNote) {
|
if (activeNote) {
|
||||||
|
|||||||
@@ -222,6 +222,10 @@
|
|||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-18 {
|
||||||
|
height: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.max-h-120 {
|
.max-h-120 {
|
||||||
max-height: 30rem;
|
max-height: 30rem;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user