fix: media popup (#225)

This commit is contained in:
Daniel Lautzenheiser 2022-12-13 10:31:35 -05:00 committed by GitHub
parent c1ae2a1bcc
commit 6602f37495
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 963 additions and 85 deletions

View File

@ -56,7 +56,7 @@ collections:
required: false
- label: Body
name: body
widget: markdown
widget: mdx
hint: Main content goes here.
- name: faq
label: FAQ

View File

@ -14,4 +14,5 @@ module.exports = {
},
setupFiles: ['./test/setupEnv.js'],
testRegex: '\\.spec\\.tsx?$',
snapshotSerializers: ['@emotion/jest/serializer'],
};

View File

@ -176,8 +176,11 @@
"@babel/preset-react": "7.18.6",
"@babel/preset-typescript": "7.18.6",
"@emotion/eslint-plugin": "11.10.0",
"@emotion/jest": "11.10.5",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.10",
"@simbathesailor/use-what-changed": "2.0.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@types/common-tags": "1.8.1",
"@types/create-react-class": "15.6.3",
"@types/fs-extra": "9.0.13",
@ -230,6 +233,7 @@
"gitlab": "14.2.2",
"http-server": "14.1.1",
"jest": "29.3.1",
"jest-environment-jsdom": "29.3.1",
"js-yaml": "4.1.0",
"mockserver-client": "5.14.0",
"mockserver-node": "5.14.0",

View File

@ -0,0 +1 @@
export default {};

View File

@ -0,0 +1,4 @@
/* eslint-disable import/prefer-default-export */
const mockDisplatch = jest.fn();
export const useAppDispatch = jest.fn().mockReturnValue(mockDisplatch);
export const useAppSelector = jest.fn();

View File

@ -0,0 +1,2 @@
export const someNode = jest.fn();
export const toggleNodeType = jest.fn();

View File

@ -40,4 +40,28 @@ export {
ELEMENT_UL,
};
export const createPlateEditor = jest.fn();
export const createPluginFactory = jest.fn();
export const createPlugins = jest.fn();
export const createTEditor = jest.fn();
export const getTEditor = jest.fn();
export const useEditorRef = jest.fn();
export const useEditorState = jest.fn();
export const usePlateActions = jest.fn();
export const usePlateEditorRef = jest.fn();
export const usePlateEditorState = jest.fn();
export const usePlateSelectors = jest.fn();
export const usePlateStates = jest.fn();
export const findNodePath = jest.fn();
export const getNode = jest.fn();
export const getParentNode = jest.fn();
export const getSelectionBoundingClientRect = jest.fn();
export const getSelectionText = jest.fn();
export const isElement = jest.fn();
export const isElementEmpty = jest.fn();
export const isSelectionExpanded = jest.fn();
export const isText = jest.fn();
export const someNode = jest.fn();
export const usePlateSelection = jest.fn();
export default {};

View File

@ -1 +0,0 @@
export default jest.fn();

View File

@ -0,0 +1,2 @@
/* eslint-disable import/prefer-default-export */
export const useFocused = jest.fn();

View File

@ -0,0 +1,3 @@
export default function cleanStack(parts: string[]) {
return parts.join('/');
}

View File

@ -0,0 +1,4 @@
export const isNode = jest.fn();
export const isMap = jest.fn();
export default {};

View File

@ -1,5 +1,7 @@
import yamlFormatter from '../YamlFormatter';
jest.unmock('yaml');
const json = {
keyF: 'valueF',
keyA: 'valueA',

View File

@ -16,17 +16,18 @@ import {
someNode,
usePlateSelection,
} from '@udecode/plate';
import { useFocused } from 'slate-react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useFocused } from 'slate-react';
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
import { isEmpty } from '@staticcms/core/lib/util/string.util';
import { useMdPlateEditorState, VOID_ELEMENTS } from '@staticcms/markdown';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import { VOID_ELEMENTS } from '../../serialization/slate/ast-types';
import BasicElementToolbarButtons from '../buttons/BasicElementToolbarButtons';
import BasicMarkToolbarButtons from '../buttons/BasicMarkToolbarButtons';
import MediaToolbarButtons from '../buttons/MediaToolbarButtons';
import TableToolbarButtons from '../buttons/TableToolbarButtons';
import ShortcodeToolbarButton from '../buttons/ShortcodeToolbarButton';
import TableToolbarButtons from '../buttons/TableToolbarButtons';
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
import type { ClientRectObject } from '@udecode/plate';
@ -76,6 +77,13 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
const [hasFocus, setHasFocus] = useState(false);
const debouncedHasFocus = useDebounce(hasFocus, 150);
const [childFocusState, setChildFocusState] = useState<Record<string, boolean>>({});
const childHasFocus = useMemo(
() => Object.keys(childFocusState).reduce((acc, value) => acc || childFocusState[value], false),
[childFocusState],
);
const debouncedChildHasFocus = useDebounce(hasFocus, 150);
const handleFocus = useCallback(() => {
setHasFocus(true);
}, []);
@ -84,6 +92,26 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
setHasFocus(false);
}, []);
const handleChildFocus = useCallback(
(key: string) => () => {
setChildFocusState(oldState => ({
...oldState,
[key]: true,
}));
},
[],
);
const handleChildBlur = useCallback(
(key: string) => () => {
setChildFocusState(oldState => ({
...oldState,
[key]: false,
}));
},
[],
);
const anchorEl = useRef<HTMLDivElement>();
const [selectionBoundingClientRect, setSelectionBoundingClientRect] =
useState<ClientRectObject | null>(null);
@ -102,8 +130,8 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
const node = getNode(editor, editor.selection?.anchor.path ?? []);
useEffect(() => {
if (!editor) {
return undefined;
if (!editor || !hasEditorFocus) {
return;
}
setTimeout(() => {
@ -121,7 +149,14 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
const debouncedEditorFocus = useDebounce(hasEditorFocus, 150);
const groups: ReactNode[] = useMemo(() => {
if (!mediaOpen && !debouncedEditorFocus && !hasFocus && !debouncedHasFocus) {
if (
!mediaOpen &&
!debouncedEditorFocus &&
!hasFocus &&
!debouncedHasFocus &&
!debouncedChildHasFocus &&
!childHasFocus
) {
return [];
}
@ -146,7 +181,9 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
field={field}
entry={entry}
onMediaToggle={setMediaOpen}
hideUploads
hideImages
handleChildFocus={handleChildFocus}
handleChildBlur={handleChildBlur}
/>,
].filter(Boolean);
}
@ -183,9 +220,11 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
field={field}
entry={entry}
onMediaToggle={setMediaOpen}
handleChildFocus={handleChildFocus}
handleChildBlur={handleChildBlur}
/>,
!useMdx ? <ShortcodeToolbarButton key="shortcode-button" /> : null,
];
].filter(Boolean);
}
}
@ -240,7 +279,7 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
}}
/>
<Popper
open={debouncedOpen}
open={Boolean(debouncedOpen && anchorEl.current)}
placement="top"
anchorEl={anchorEl.current ?? null}
sx={{ zIndex: 100 }}

View File

@ -0,0 +1,169 @@
/**
* @jest-environment jsdom
*/
import { render, screen } from '@testing-library/react';
import {
findNodePath,
getNode,
getParentNode,
isElement,
isElementEmpty,
someNode,
usePlateEditorState,
} from '@udecode/plate';
import React, { useRef } from 'react';
import { useFocused } from 'slate-react';
import '@testing-library/jest-dom';
import { mockMarkdownCollection, mockMarkdownField } from '@staticcms/test/data/collection';
import BalloonToolbar from '../BalloonToolbar';
import type { Entry } from '@staticcms/core/interface';
import type { MdEditor } from '@staticcms/markdown/plate/plateTypes';
import type { FC } from 'react';
let entry: Entry;
interface BalloonToolbarWrapperProps {
useMdx?: boolean;
}
const BalloonToolbarWrapper: FC<BalloonToolbarWrapperProps> = ({ useMdx = false }) => {
const ref = useRef<HTMLDivElement | null>(null);
return (
<div ref={ref}>
<BalloonToolbar
key="balloon-toolbar"
useMdx={useMdx}
containerRef={ref.current}
collection={mockMarkdownCollection}
field={mockMarkdownField}
entry={entry}
/>
</div>
);
};
describe(BalloonToolbar.name, () => {
const mockUseEditor = usePlateEditorState as jest.Mock;
let mockEditor: MdEditor;
const mockGetNode = getNode as jest.Mock;
const mockIsElement = isElement as unknown as jest.Mock;
const mockIsElementEmpty = isElementEmpty as jest.Mock;
const mockSomeNode = someNode as jest.Mock;
const mockUseFocused = useFocused as jest.Mock;
const mockFindNodePath = findNodePath as jest.Mock;
const mockGetParentNode = getParentNode as jest.Mock;
beforeEach(() => {
entry = {
collection: 'posts',
slug: '2022-12-13-post-number-1',
path: '_posts/2022-12-13-post-number-1.md',
partial: false,
raw: '--- title: "This is post # 1" draft: false date: 2022-12-13T00:00:00.000Z --- # The post is number 1\n\nAnd some text',
label: '',
author: '',
mediaFiles: [],
isModification: null,
newRecord: false,
updatedOn: '',
data: {
title: 'This is post # 1',
draft: false,
date: '2022-12-13T00:00:00.000Z',
body: '# The post is number 1\n\nAnd some text',
},
};
mockEditor = {
selection: undefined,
} as unknown as MdEditor;
mockUseEditor.mockReturnValue(mockEditor);
});
it('renders empty div by default', () => {
render(<BalloonToolbarWrapper />);
expect(screen.queryAllByRole('button').length).toBe(0);
});
describe('empty node toolbar', () => {
interface EmptyNodeToolbarSetupOptions {
useMdx?: boolean;
}
const emptyNodeToolbarSetup = ({ useMdx }: EmptyNodeToolbarSetupOptions = {}) => {
mockEditor = {
selection: undefined,
children: [
{
type: 'p',
children: [{ text: '' }],
},
{
type: 'p',
children: [{ text: '' }],
},
],
} as unknown as MdEditor;
mockUseEditor.mockReturnValue(mockEditor);
mockGetNode.mockReturnValue({ text: '' });
mockIsElement.mockReturnValue(true);
mockIsElementEmpty.mockReturnValue(true);
mockSomeNode.mockReturnValue(false);
mockUseFocused.mockReturnValue(true);
mockFindNodePath.mockReturnValue([1, 0]);
mockGetParentNode.mockReturnValue([
{
type: 'p',
children: [{ text: '' }],
},
]);
const { rerender } = render(<BalloonToolbarWrapper />);
rerender(<BalloonToolbarWrapper useMdx={useMdx} />);
};
it('renders empty node toolbar for markdown', () => {
emptyNodeToolbarSetup();
expect(screen.queryByTestId('toolbar-button-bold')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-italic')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-code')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-strikethrough')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-blockquote')).toBeInTheDocument();
expect(screen.queryByTestId('font-type-select')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-add-table')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-insert-link')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-insert-image')).toBeInTheDocument();
// MDX Only do not show for markdown version
expect(screen.queryByTestId('toolbar-button-underline')).not.toBeInTheDocument();
});
it('renders empty node toolbar for mdx', () => {
emptyNodeToolbarSetup({ useMdx: true });
expect(screen.queryByTestId('toolbar-button-bold')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-italic')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-code')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-strikethrough')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-blockquote')).toBeInTheDocument();
expect(screen.queryByTestId('font-type-select')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-add-table')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-insert-link')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-insert-image')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-underline')).toBeInTheDocument();
});
});
});

View File

@ -16,7 +16,7 @@ import { someNode, toggleNodeType } from '@udecode/plate-core';
import React, { useCallback, useMemo, useState } from 'react';
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
import { useMdPlateEditorState } from '@staticcms/markdown';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import type { SelectChangeEvent } from '@mui/material/Select';
import type { FC } from 'react';
@ -107,6 +107,7 @@ const FontTypeSelect: FC<FontTypeSelectProps> = ({ disabled = false }) => {
<StyledSelect
labelId="font-type-select-label"
id="font-type-select"
data-testid="font-type-select"
value={value?.type ?? ELEMENT_PARAGRAPH}
onChange={handleChange}
size="small"

View File

@ -10,12 +10,14 @@ import type { FC } from 'react';
export interface MediaToolbarButtonsProps {
containerRef: HTMLElement | null;
hideUploads?: boolean;
hideImages?: boolean;
collection: Collection<MarkdownField>;
field: MarkdownField;
entry: Entry;
inserting?: boolean;
onMediaToggle?: (open: boolean) => void;
handleChildFocus?: (key: string) => () => void;
handleChildBlur?: (key: string) => () => void;
}
const MediaToolbarButtons: FC<MediaToolbarButtonsProps> = ({
@ -23,8 +25,10 @@ const MediaToolbarButtons: FC<MediaToolbarButtonsProps> = ({
collection,
field,
entry,
hideUploads = false,
hideImages = false,
onMediaToggle,
handleChildFocus,
handleChildBlur,
}) => {
const [open, setOpen] = useState(false);
const [linkMediaOpen, setLinkMediaOpen] = useState(false);
@ -56,8 +60,10 @@ const MediaToolbarButtons: FC<MediaToolbarButtonsProps> = ({
entry={entry}
mediaOpen={linkMediaOpen}
onMediaToggle={setLinkMediaOpen}
onFocus={handleChildFocus?.('link')}
onBlur={handleChildBlur?.('link')}
/>
{!hideUploads ? (
{!hideImages ? (
<ImageToolbarButton
containerRef={containerRef}
tooltip="Insert Image"
@ -68,6 +74,8 @@ const MediaToolbarButtons: FC<MediaToolbarButtonsProps> = ({
entry={entry}
mediaOpen={imageMediaOpen}
onMediaToggle={setImageMediaOpen}
onFocus={handleChildFocus?.('image')}
onBlur={handleChildBlur?.('image')}
/>
) : null}
</>

View File

@ -6,7 +6,7 @@ import React, { useCallback, useMemo, useState } from 'react';
import { getShortcodes } from '../../../../../lib/registry';
import { toTitleCase } from '../../../../../lib/util/string.util';
import { ELEMENT_SHORTCODE, useMdPlateEditorState } from '../../plateTypes';
import { ELEMENT_SHORTCODE, useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import ToolbarButton from './common/ToolbarButton';
import type { FC, MouseEvent } from 'react';

View File

@ -1,7 +1,7 @@
import { isCollapsed, KEY_ALIGN, setAlign, someNode } from '@udecode/plate';
import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '@staticcms/markdown';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import ToolbarButton from './ToolbarButton';
import type { MdEditor } from '@staticcms/markdown';

View File

@ -1,7 +1,7 @@
import { someNode, toggleNodeType } from '@udecode/plate';
import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '@staticcms/markdown';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import ToolbarButton from './ToolbarButton';
import type { MdEditor } from '@staticcms/markdown';

View File

@ -10,7 +10,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Transforms } from 'slate';
import { ReactEditor } from 'slate-react';
import { useMdPlateEditorState } from '@staticcms/markdown';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import ColorPicker from '../../color-picker/ColorPicker';
import ToolbarDropdown from './dropdown/ToolbarDropdown';

View File

@ -2,7 +2,7 @@ import { insertImage } from '@udecode/plate';
import React, { useCallback } from 'react';
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
import { useMdPlateEditorState } from '@staticcms/markdown';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import MediaToolbarButton from './MediaToolbarButton';
import type { FC } from 'react';

View File

@ -2,7 +2,7 @@ import { ELEMENT_LINK, insertLink, someNode } from '@udecode/plate';
import React, { useCallback } from 'react';
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
import { useMdPlateEditorState } from '@staticcms/markdown';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import MediaToolbarButton from './MediaToolbarButton';
import type { FC } from 'react';
@ -12,10 +12,10 @@ const LinkToolbarButton: FC<Omit<MediaToolbarButtonProps, 'onChange'>> = props =
const editor = useMdPlateEditorState();
const handleInsert = useCallback(
(newUrl: string, newText: string | undefined) => {
if (isNotEmpty(newUrl) && isNotEmpty(newText)) {
if (isNotEmpty(newUrl)) {
insertLink(
editor,
{ url: newUrl, text: newText },
{ url: newUrl, text: isNotEmpty(newText) ? newText : newUrl },
{ at: editor.selection ?? editor.prevSelection! },
);
}

View File

@ -1,7 +1,7 @@
import { getListItemEntry, toggleList } from '@udecode/plate-list';
import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '@staticcms/markdown';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import ToolbarButton from './ToolbarButton';
import type { MdEditor } from '@staticcms/markdown';

View File

@ -1,7 +1,7 @@
import { isMarkActive, toggleMark } from '@udecode/plate';
import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '@staticcms/markdown';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import ToolbarButton from './ToolbarButton';
import type { MdEditor } from '@staticcms/markdown';

View File

@ -1,10 +1,9 @@
import { focusEditor } from '@udecode/plate-core';
import React, { useCallback, useEffect, useState } from 'react';
import { useFocused } from 'slate-react';
import { MediaPopover, useMdPlateEditorState } from '@staticcms/markdown';
import MediaPopover from '../../common/MediaPopover';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import ToolbarButton from './ToolbarButton';
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
import type { MarkdownField } from '@staticcms/core/interface';
import type { MdEditor, MediaPopoverProps } from '@staticcms/markdown';
@ -21,6 +20,8 @@ export interface MediaToolbarButtonProps
mediaOpen: boolean;
onMediaToggle: (open: boolean) => void;
onChange: (newUrl: string, newText: string | undefined) => void;
onFocus?: () => void;
onBlur?: () => void;
}
const MediaToolbarButton: FC<MediaToolbarButtonProps> = ({
@ -34,23 +35,18 @@ const MediaToolbarButton: FC<MediaToolbarButtonProps> = ({
mediaOpen,
onMediaToggle,
onChange,
onFocus,
onBlur,
...props
}) => {
const editor = useMdPlateEditorState();
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
if (forImage) {
console.log('anchorEl', anchorEl);
}
const [internalUrl, setInternalUrl] = useState('');
const [internalText, setInternalText] = useState('');
const handleClose = useCallback(
(newValue: string | undefined, shouldFocus: boolean) => {
if (forImage) {
console.log('handleClose', newValue, shouldFocus);
}
setAnchorEl(null);
setInternalUrl('');
setInternalText('');
@ -72,9 +68,6 @@ const MediaToolbarButton: FC<MediaToolbarButtonProps> = ({
return;
}
if (forImage) {
console.log('handleOnClick');
}
setAnchorEl(event.currentTarget);
},
[anchorEl, handleClose],
@ -94,13 +87,12 @@ const MediaToolbarButton: FC<MediaToolbarButtonProps> = ({
[handleClose],
);
const editorHasFocus = useFocused();
const debouncedEditorHasFocus = useDebounce(editorHasFocus, 150);
useEffect(() => {
if (!editorHasFocus && !debouncedEditorHasFocus) {
if (anchorEl && !mediaOpen) {
handleClose(undefined, false);
}
}, [debouncedEditorHasFocus, editorHasFocus, handleClose]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mediaOpen]);
return (
<>
@ -122,6 +114,8 @@ const MediaToolbarButton: FC<MediaToolbarButtonProps> = ({
onMediaToggle={onMediaToggle}
onMediaChange={handleMediaChange}
onClose={handlePopoverClose}
onFocus={onFocus}
onBlur={onBlur}
/>
</>
);

View File

@ -4,7 +4,7 @@ import Tooltip from '@mui/material/Tooltip';
import { focusEditor } from '@udecode/plate';
import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '@staticcms/markdown';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import type { MdEditor } from '@staticcms/markdown';
import type { FC, MouseEvent, ReactNode } from 'react';
@ -56,6 +56,7 @@ const ToolbarButton: FC<ToolbarButtonProps> = ({
aria-label={label ?? tooltip}
size="small"
color="inherit"
data-testid={`toolbar-button-${label ?? tooltip}`.replace(' ', '-').toLowerCase()}
sx={{
padding: '2px',
minWidth: 'unset',

View File

@ -20,7 +20,7 @@ const StyledPopperContent = styled('div')(
gap: 4px;
background: ${theme.palette.background.paper};
box-shadow: ${theme.shadows[8]};
margin-bottom: 10px;
margin: 10px 0;
padding: 6px;
border-radius: 4px;
align-items: center;
@ -69,6 +69,8 @@ export interface MediaPopoverProps<T extends FileOrImageField | MarkdownField> {
onMediaToggle?: (open: boolean) => void;
onMediaChange: (newValue: string) => void;
onRemove?: () => void;
onFocus?: () => void;
onBlur?: () => void;
}
const MediaPopover = <T extends FileOrImageField | MarkdownField>({
@ -89,6 +91,8 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
onMediaToggle,
onMediaChange,
onRemove,
onFocus,
onBlur,
}: MediaPopoverProps<T>) => {
const theme = useTheme();
const buttonRef = useRef<HTMLButtonElement | null>(null);
@ -98,7 +102,6 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
const [editing, setEditing] = useState(inserting);
const hasEditorFocus = useFocused();
const debouncedHasEditorFocus = useDebounce(hasEditorFocus, 150);
const [hasFocus, setHasFocus] = useState(false);
const debouncedHasFocus = useDebounce(hasFocus, 150);
@ -152,34 +155,62 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
}
}, [anchorEl, editing, inserting, urlDisabled]);
const [
{ prevAnchorEl, prevHasEditorFocus, prevHasFocus, prevDebouncedHasFocus },
setPrevFocusState,
] = useState<{
prevAnchorEl: HTMLElement | null;
prevHasEditorFocus: boolean;
prevHasFocus: boolean;
prevDebouncedHasFocus: boolean;
}>({
prevAnchorEl: anchorEl,
prevHasEditorFocus: hasEditorFocus,
prevHasFocus: hasFocus,
prevDebouncedHasFocus: debouncedHasFocus,
});
useEffect(() => {
if (
anchorEl &&
!debouncedHasEditorFocus &&
!hasEditorFocus &&
!hasFocus &&
!debouncedHasFocus &&
!mediaOpen
) {
if (mediaOpen) {
return;
}
if (anchorEl && !prevHasEditorFocus && hasEditorFocus) {
handleClose(false);
}
if (anchorEl && (prevHasFocus || prevDebouncedHasFocus) && !hasFocus && !debouncedHasFocus) {
handleClose(false);
}
setPrevFocusState({
prevAnchorEl: anchorEl,
prevHasEditorFocus: hasEditorFocus,
prevHasFocus: hasFocus,
prevDebouncedHasFocus: debouncedHasFocus,
});
}, [
anchorEl,
debouncedHasEditorFocus,
debouncedHasFocus,
handleClose,
hasEditorFocus,
hasFocus,
mediaOpen,
prevAnchorEl,
prevDebouncedHasFocus,
prevHasEditorFocus,
prevHasFocus,
]);
const handleFocus = useCallback(() => {
setHasFocus(true);
}, []);
onFocus?.();
}, [onFocus]);
const handleBlur = useCallback(() => {
setHasFocus(false);
}, []);
onBlur?.();
}, [onBlur]);
const handleMediaChange = useCallback(
(newValue: string) => {

View File

@ -1,5 +1,6 @@
export * from './balloon-toolbar';
export * from './buttons';
export * from './color-picker';
export * from './common';
export * from './nodes';
export * from './toolbar';

View File

@ -1,6 +1,5 @@
export * from './blockquote';
export * from './code-block';
export * from './common';
export * from './headings';
export * from './horizontal-rule';
export * from './image';

View File

@ -8,7 +8,7 @@ import {
} from '@udecode/plate';
import React, { useCallback, useState } from 'react';
import MediaPopover from '../common/MediaPopover';
import MediaPopover from '../../common/MediaPopover';
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
import type { MdLinkElement, MdValue } from '@staticcms/markdown';
@ -52,10 +52,11 @@ const withLinkElement = ({ containerRef, collection, field, entry }: WithLinkEle
const handleChange = useCallback(
(newUrl: string, newText: string) => {
const path = findNodePath(editor, element);
path && setNodes<MdLinkElement>(editor, { url: newUrl }, { at: path });
upsertLink(editor, { url: newUrl, text: newText });
},
[editor, path],
[editor, element],
);
const handleMediaChange = useCallback(

View File

@ -0,0 +1,54 @@
import type { Collection, MarkdownField } from '@staticcms/core';
export const mockMarkdownField: MarkdownField = {
label: 'Body',
name: 'body',
widget: 'markdown',
hint: 'Main content goes here.',
};
export const mockMarkdownCollection: Collection<MarkdownField> = {
name: 'posts',
label: 'Posts',
label_singular: 'Post',
description:
'The description is a great place for tone setting, high level information, and editing guidelines that are specific to a collection.\n',
folder: '_posts',
slug: '{{year}}-{{month}}-{{day}}-{{slug}}',
summary: '{{title}} -- {{year}}/{{month}}/{{day}}',
sortable_fields: {
fields: ['title', 'date'],
default: {
field: 'title',
},
},
create: true,
fields: [
{
label: 'Title',
name: 'title',
widget: 'string',
},
{
label: 'Draft',
name: 'draft',
widget: 'boolean',
default: false,
},
{
label: 'Publish Date',
name: 'date',
widget: 'datetime',
date_format: 'yyyy-MM-dd',
time_format: 'HH:mm',
format: "yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
},
{
label: 'Cover Image',
name: 'image',
widget: 'image',
required: false,
},
mockMarkdownField,
],
};

View File

@ -1,6 +1,3 @@
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;
if (typeof window === 'undefined') {
global.window = {
URL: {
@ -15,3 +12,5 @@ if (typeof window === 'undefined') {
},
};
}
global.URL.createObjectURL = jest.fn();

View File

@ -54,6 +54,6 @@
"@staticcms/core": ["./src"],
"@staticcms/core/*": ["./src/*"]
},
"types": ["@emotion/react/types/css-prop", "@types/jest"]
"types": ["@emotion/react/types/css-prop", "@types/jest", "@testing-library/jest-dom"]
}
}

View File

@ -1,5 +1,5 @@
{
"extends": "./tsconfig.base.json",
"include": ["src/**/*"],
"include": ["src/**/*", "test/**/*"],
"exclude": ["node_modules"]
}

File diff suppressed because it is too large Load Diff