mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-23 18:42:58 +03:00
feat(core): allow keyboard navigation in tags inline editor (#7378)
fix AF-966 - Allow using arrowup/down to navigate the tag list candidates; press enter to add the currently focused tag option; - Allow using arrowleft/right to navigate the inline tag list (selected) and use backspace to delete focused tag.
This commit is contained in:
parent
c62d79ab14
commit
2a6ea3c9c6
@ -90,8 +90,10 @@ export const tagSelectorItem = style({
|
|||||||
gap: 8,
|
gap: 8,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
':hover': {
|
selectors: {
|
||||||
backgroundColor: cssVar('hoverColor'),
|
'&[data-focused=true]': {
|
||||||
|
backgroundColor: cssVar('hoverColor'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,11 +2,13 @@ import type { MenuProps } from '@affine/component';
|
|||||||
import { IconButton, Input, Menu, Scrollable } from '@affine/component';
|
import { IconButton, Input, Menu, Scrollable } from '@affine/component';
|
||||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||||
import { WorkspaceLegacyProperties } from '@affine/core/modules/properties';
|
import { WorkspaceLegacyProperties } from '@affine/core/modules/properties';
|
||||||
|
import type { Tag } from '@affine/core/modules/tag';
|
||||||
import { DeleteTagConfirmModal, TagService } from '@affine/core/modules/tag';
|
import { DeleteTagConfirmModal, TagService } from '@affine/core/modules/tag';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { DeleteIcon, MoreHorizontalIcon, TagsIcon } from '@blocksuite/icons/rc';
|
import { DeleteIcon, MoreHorizontalIcon, TagsIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { clamp } from 'lodash-es';
|
||||||
import type { HTMLAttributes, PropsWithChildren } from 'react';
|
import type { HTMLAttributes, PropsWithChildren } from 'react';
|
||||||
import { useCallback, useMemo, useReducer, useRef, useState } from 'react';
|
import { useCallback, useMemo, useReducer, useRef, useState } from 'react';
|
||||||
|
|
||||||
@ -19,6 +21,7 @@ import * as styles from './tags-inline-editor.css';
|
|||||||
interface TagsEditorProps {
|
interface TagsEditorProps {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
|
focusedIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InlineTagsListProps
|
interface InlineTagsListProps
|
||||||
@ -31,6 +34,7 @@ const InlineTagsList = ({
|
|||||||
pageId,
|
pageId,
|
||||||
readonly,
|
readonly,
|
||||||
children,
|
children,
|
||||||
|
focusedIndex,
|
||||||
onRemove,
|
onRemove,
|
||||||
}: PropsWithChildren<InlineTagsListProps>) => {
|
}: PropsWithChildren<InlineTagsListProps>) => {
|
||||||
const tagList = useService(TagService).tagList;
|
const tagList = useService(TagService).tagList;
|
||||||
@ -54,6 +58,7 @@ const InlineTagsList = ({
|
|||||||
<TagItem
|
<TagItem
|
||||||
key={tagId}
|
key={tagId}
|
||||||
idx={idx}
|
idx={idx}
|
||||||
|
focused={focusedIndex === idx}
|
||||||
onRemoved={onRemoved}
|
onRemoved={onRemoved}
|
||||||
mode="inline"
|
mode="inline"
|
||||||
tag={tag}
|
tag={tag}
|
||||||
@ -171,16 +176,51 @@ export const EditTagMenu = ({
|
|||||||
return <Menu {...menuProps}>{children}</Menu>;
|
return <Menu {...menuProps}>{children}</Menu>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TagOption = Tag | { readonly create: true; readonly value: string };
|
||||||
|
const isCreateNewTag = (
|
||||||
|
tagOption: TagOption
|
||||||
|
): tagOption is { readonly create: true; readonly value: string } => {
|
||||||
|
return 'create' in tagOption;
|
||||||
|
};
|
||||||
|
|
||||||
export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
|
export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const tagList = useService(TagService).tagList;
|
const tagList = useService(TagService).tagList;
|
||||||
const tags = useLiveData(tagList.tags$);
|
const tags = useLiveData(tagList.tags$);
|
||||||
const tagIds = useLiveData(tagList.tagIdsByPageId$(pageId));
|
const tagIds = useLiveData(tagList.tagIdsByPageId$(pageId));
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const filteredTags = useLiveData(
|
||||||
|
inputValue ? tagList.filterTagsByName$(inputValue) : tagList.tags$
|
||||||
|
);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
|
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const exactMatch = filteredTags.find(tag => tag.value$.value === inputValue);
|
||||||
|
const showCreateTag = !exactMatch && inputValue.trim();
|
||||||
|
|
||||||
|
// tag option candidates to show in the tag dropdown
|
||||||
|
const tagOptions: TagOption[] = useMemo(() => {
|
||||||
|
if (showCreateTag) {
|
||||||
|
return [{ create: true, value: inputValue } as const, ...filteredTags];
|
||||||
|
} else {
|
||||||
|
return filteredTags;
|
||||||
|
}
|
||||||
|
}, [filteredTags, inputValue, showCreateTag]);
|
||||||
|
|
||||||
|
const [focusedIndex, setFocusedIndex] = useState<number>(-1);
|
||||||
|
const [focusedInlineIndex, setFocusedInlineIndex] = useState<number>(
|
||||||
|
tagIds.length
|
||||||
|
);
|
||||||
|
|
||||||
|
// -1: no focus
|
||||||
|
const safeFocusedIndex = clamp(focusedIndex, -1, tagOptions.length - 1);
|
||||||
|
// inline tags focus index can go beyond the length of tagIds
|
||||||
|
// using -1 and tagIds.length to make keyboard navigation easier
|
||||||
|
const safeInlineFocusedIndex = clamp(focusedInlineIndex, -1, tagIds.length);
|
||||||
|
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleCloseModal = useCallback(
|
const handleCloseModal = useCallback(
|
||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
setOpen(open);
|
setOpen(open);
|
||||||
@ -197,13 +237,6 @@ export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
|
|||||||
[setOpen, setSelectedTagIds]
|
[setOpen, setSelectedTagIds]
|
||||||
);
|
);
|
||||||
|
|
||||||
const match = useLiveData(tagList.tagByTagValue$(inputValue));
|
|
||||||
const exactMatch = useLiveData(tagList.excactTagByValue$(inputValue));
|
|
||||||
|
|
||||||
const filteredTags = useLiveData(
|
|
||||||
inputValue ? tagList.filterTagsByName$(inputValue) : tagList.tags$
|
|
||||||
);
|
|
||||||
|
|
||||||
const onInputChange = useCallback(
|
const onInputChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setInputValue(e.target.value);
|
setInputValue(e.target.value);
|
||||||
@ -211,13 +244,19 @@ export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onAddTag = useCallback(
|
const onToggleTag = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
|
const tagEntity = tagList.tags$.value.find(o => o.id === id);
|
||||||
|
if (!tagEntity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!tagIds.includes(id)) {
|
if (!tagIds.includes(id)) {
|
||||||
tags.find(o => o.id === id)?.tag(pageId);
|
tagEntity.tag(pageId);
|
||||||
|
} else {
|
||||||
|
tagEntity.untag(pageId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[pageId, tagIds, tags]
|
[pageId, tagIds, tagList.tags$.value]
|
||||||
);
|
);
|
||||||
|
|
||||||
const focusInput = useCallback(() => {
|
const focusInput = useCallback(() => {
|
||||||
@ -234,40 +273,76 @@ export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
|
|||||||
|
|
||||||
const onCreateTag = useCallback(
|
const onCreateTag = useCallback(
|
||||||
(name: string) => {
|
(name: string) => {
|
||||||
if (!name.trim()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rotateNextColor();
|
rotateNextColor();
|
||||||
const newTag = tagList.createTag(name.trim(), nextColor);
|
const newTag = tagList.createTag(name.trim(), nextColor);
|
||||||
newTag.tag(pageId);
|
return newTag.id;
|
||||||
},
|
},
|
||||||
[nextColor, pageId, tagList]
|
[nextColor, tagList]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSelectTag = useCallback(
|
const onSelectTagOption = useCallback(
|
||||||
(id: string) => {
|
(tagOption: TagOption) => {
|
||||||
onAddTag(id);
|
const id = isCreateNewTag(tagOption)
|
||||||
|
? onCreateTag(tagOption.value)
|
||||||
|
: tagOption.id;
|
||||||
|
onToggleTag(id);
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
focusInput();
|
focusInput();
|
||||||
|
setFocusedIndex(-1);
|
||||||
|
setFocusedInlineIndex(tagIds.length + 1);
|
||||||
},
|
},
|
||||||
[focusInput, onAddTag]
|
[onCreateTag, onToggleTag, focusInput, tagIds.length]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onInputKeyDown = useCallback(
|
const onInputKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
if (exactMatch) {
|
if (safeFocusedIndex >= 0) {
|
||||||
onAddTag(exactMatch.id);
|
onSelectTagOption(tagOptions[safeFocusedIndex]);
|
||||||
} else {
|
|
||||||
onCreateTag(inputValue);
|
|
||||||
}
|
}
|
||||||
setInputValue('');
|
|
||||||
} else if (e.key === 'Backspace' && inputValue === '' && tagIds.length) {
|
} else if (e.key === 'Backspace' && inputValue === '' && tagIds.length) {
|
||||||
const lastTagId = tagIds[tagIds.length - 1];
|
const tagToRemove =
|
||||||
tags.find(tag => tag.id === lastTagId)?.untag(pageId);
|
safeInlineFocusedIndex < 0 || safeInlineFocusedIndex >= tagIds.length
|
||||||
|
? tagIds.length - 1
|
||||||
|
: safeInlineFocusedIndex;
|
||||||
|
tags.find(item => item.id === tagIds.at(tagToRemove))?.untag(pageId);
|
||||||
|
} else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
const newFocusedIndex = clamp(
|
||||||
|
safeFocusedIndex + (e.key === 'ArrowUp' ? -1 : 1),
|
||||||
|
0,
|
||||||
|
tagOptions.length - 1
|
||||||
|
);
|
||||||
|
scrollContainerRef.current
|
||||||
|
?.querySelector(
|
||||||
|
`.${styles.tagSelectorItem}:nth-child(${newFocusedIndex + 1})`
|
||||||
|
)
|
||||||
|
?.scrollIntoView({ block: 'nearest' });
|
||||||
|
setFocusedIndex(newFocusedIndex);
|
||||||
|
// reset inline focus
|
||||||
|
setFocusedInlineIndex(tagIds.length + 1);
|
||||||
|
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
||||||
|
const newItemToFocus =
|
||||||
|
e.key === 'ArrowLeft'
|
||||||
|
? safeInlineFocusedIndex - 1
|
||||||
|
: safeInlineFocusedIndex + 1;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
setFocusedInlineIndex(newItemToFocus);
|
||||||
|
// reset tag list focus
|
||||||
|
setFocusedIndex(-1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[inputValue, tagIds, exactMatch, onAddTag, onCreateTag, tags, pageId]
|
[
|
||||||
|
inputValue,
|
||||||
|
tagIds,
|
||||||
|
safeFocusedIndex,
|
||||||
|
onSelectTagOption,
|
||||||
|
tagOptions,
|
||||||
|
safeInlineFocusedIndex,
|
||||||
|
tags,
|
||||||
|
pageId,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -276,6 +351,7 @@ export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
|
|||||||
<InlineTagsList
|
<InlineTagsList
|
||||||
pageId={pageId}
|
pageId={pageId}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
|
focusedIndex={safeInlineFocusedIndex}
|
||||||
onRemove={focusInput}
|
onRemove={focusInput}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@ -295,45 +371,46 @@ export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<Scrollable.Root>
|
<Scrollable.Root>
|
||||||
<Scrollable.Viewport
|
<Scrollable.Viewport
|
||||||
|
ref={scrollContainerRef}
|
||||||
className={styles.tagSelectorTagsScrollContainer}
|
className={styles.tagSelectorTagsScrollContainer}
|
||||||
>
|
>
|
||||||
{filteredTags.map(tag => {
|
{tagOptions.map((tag, idx) => {
|
||||||
return (
|
const commonProps = {
|
||||||
<div
|
focused: safeFocusedIndex === idx,
|
||||||
key={tag.id}
|
onClick: () => onSelectTagOption(tag),
|
||||||
className={styles.tagSelectorItem}
|
onMouseEnter: () => setFocusedIndex(idx),
|
||||||
data-testid="tag-selector-item"
|
['data-testid']: 'tag-selector-item',
|
||||||
data-tag-id={tag.id}
|
['data-focused']: safeFocusedIndex === idx,
|
||||||
data-tag-value={tag.value$}
|
className: styles.tagSelectorItem,
|
||||||
onClick={() => {
|
};
|
||||||
onSelectTag(tag.id);
|
if (isCreateNewTag(tag)) {
|
||||||
}}
|
return (
|
||||||
>
|
<div key={tag.value + '.' + idx} {...commonProps}>
|
||||||
<TagItem maxWidth="100%" tag={tag} mode="inline" />
|
{t['Create']()}{' '}
|
||||||
<div className={styles.spacer} />
|
<TempTagItem value={inputValue} color={nextColor} />
|
||||||
<EditTagMenu tagId={tag.id} onTagDelete={onTagDelete}>
|
</div>
|
||||||
<IconButton
|
);
|
||||||
className={styles.tagEditIcon}
|
} else {
|
||||||
type="plain"
|
return (
|
||||||
icon={<MoreHorizontalIcon />}
|
<div
|
||||||
/>
|
key={tag.id}
|
||||||
</EditTagMenu>
|
{...commonProps}
|
||||||
</div>
|
data-tag-id={tag.id}
|
||||||
);
|
data-tag-value={tag.value$.value}
|
||||||
|
>
|
||||||
|
<TagItem maxWidth="100%" tag={tag} mode="inline" />
|
||||||
|
<div className={styles.spacer} />
|
||||||
|
<EditTagMenu tagId={tag.id} onTagDelete={onTagDelete}>
|
||||||
|
<IconButton
|
||||||
|
className={styles.tagEditIcon}
|
||||||
|
type="plain"
|
||||||
|
icon={<MoreHorizontalIcon />}
|
||||||
|
/>
|
||||||
|
</EditTagMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
})}
|
})}
|
||||||
{match || !inputValue ? null : (
|
|
||||||
<div
|
|
||||||
data-testid="tag-selector-item"
|
|
||||||
className={styles.tagSelectorItem}
|
|
||||||
onClick={() => {
|
|
||||||
setInputValue('');
|
|
||||||
onCreateTag(inputValue);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t['Create']()}{' '}
|
|
||||||
<TempTagItem value={inputValue} color={nextColor} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Scrollable.Viewport>
|
</Scrollable.Viewport>
|
||||||
<Scrollable.Scrollbar style={{ transform: 'translateX(6px)' }} />
|
<Scrollable.Scrollbar style={{ transform: 'translateX(6px)' }} />
|
||||||
</Scrollable.Root>
|
</Scrollable.Root>
|
||||||
|
@ -78,6 +78,12 @@ export const tagInnerWrapper = style({
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
padding: '0 8px',
|
padding: '0 8px',
|
||||||
color: cssVar('textPrimaryColor'),
|
color: cssVar('textPrimaryColor'),
|
||||||
|
borderColor: cssVar('borderColor'),
|
||||||
|
selectors: {
|
||||||
|
'&[data-focused=true]': {
|
||||||
|
borderColor: cssVar('primaryColor'),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
export const tagInline = style([
|
export const tagInline = style([
|
||||||
tagInnerWrapper,
|
tagInnerWrapper,
|
||||||
@ -85,7 +91,8 @@ export const tagInline = style([
|
|||||||
fontSize: 'inherit',
|
fontSize: 'inherit',
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
columnGap: '4px',
|
columnGap: '4px',
|
||||||
border: `1px solid ${cssVar('borderColor')}`,
|
borderWidth: '1px',
|
||||||
|
borderStyle: 'solid',
|
||||||
background: cssVar('backgroundPrimaryColor'),
|
background: cssVar('backgroundPrimaryColor'),
|
||||||
maxWidth: '128px',
|
maxWidth: '128px',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
@ -22,6 +22,7 @@ interface TagItemProps {
|
|||||||
idx?: number;
|
idx?: number;
|
||||||
maxWidth?: number | string;
|
maxWidth?: number | string;
|
||||||
mode: 'inline' | 'list-item';
|
mode: 'inline' | 'list-item';
|
||||||
|
focused?: boolean;
|
||||||
onRemoved?: () => void;
|
onRemoved?: () => void;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
@ -54,6 +55,7 @@ export const TagItem = ({
|
|||||||
tag,
|
tag,
|
||||||
idx,
|
idx,
|
||||||
mode,
|
mode,
|
||||||
|
focused,
|
||||||
onRemoved,
|
onRemoved,
|
||||||
style,
|
style,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
@ -79,6 +81,7 @@ export const TagItem = ({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{ maxWidth: maxWidth }}
|
style={{ maxWidth: maxWidth }}
|
||||||
|
data-focused={focused}
|
||||||
className={mode === 'inline' ? styles.tagInline : styles.tagListItem}
|
className={mode === 'inline' ? styles.tagInline : styles.tagListItem}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -63,12 +63,6 @@ export class TagList extends Entity {
|
|||||||
return trimmedValue.includes(trimmedQuery);
|
return trimmedValue.includes(trimmedQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
private findfn(value: string, query?: string) {
|
|
||||||
const trimmedTagValue = query?.trim().toLowerCase();
|
|
||||||
const trimmedInputValue = value.trim().toLowerCase();
|
|
||||||
return trimmedTagValue === trimmedInputValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterTagsByName$(name: string) {
|
filterTagsByName$(name: string) {
|
||||||
return LiveData.computed(get => {
|
return LiveData.computed(get => {
|
||||||
return get(this.tags$).filter(tag =>
|
return get(this.tags$).filter(tag =>
|
||||||
@ -76,16 +70,4 @@ export class TagList extends Entity {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
tagByTagValue$(value: string) {
|
|
||||||
return LiveData.computed(get => {
|
|
||||||
return get(this.tags$).find(tag => this.filterFn(get(tag.value$), value));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
excactTagByValue$(value: string) {
|
|
||||||
return LiveData.computed(get => {
|
|
||||||
return get(this.tags$).find(tag => this.findfn(get(tag.value$), value));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,24 @@ test('allow create tag', async ({ page }) => {
|
|||||||
await expectTagsVisible(page, ['Test2']);
|
await expectTagsVisible(page, ['Test2']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('allow using keyboard to navigate tags', async ({ page }) => {
|
||||||
|
await openTagsEditor(page);
|
||||||
|
await searchAndCreateTag(page, 'Test1');
|
||||||
|
await searchAndCreateTag(page, 'Test2');
|
||||||
|
|
||||||
|
await page.keyboard.press('ArrowLeft');
|
||||||
|
await page.keyboard.press('Backspace');
|
||||||
|
await closeTagsEditor(page);
|
||||||
|
await expectTagsVisible(page, ['Test1']);
|
||||||
|
|
||||||
|
await openTagsEditor(page);
|
||||||
|
await page.keyboard.press('ArrowDown');
|
||||||
|
await page.keyboard.press('ArrowDown');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await closeTagsEditor(page);
|
||||||
|
await expectTagsVisible(page, ['Test1', 'Test2']);
|
||||||
|
});
|
||||||
|
|
||||||
test('allow create tag on journals page', async ({ page }) => {
|
test('allow create tag on journals page', async ({ page }) => {
|
||||||
await openJournalsPage(page);
|
await openJournalsPage(page);
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
|
Loading…
Reference in New Issue
Block a user