fix(workspace): tighten link preview card, slash menu matching/scrolling, and Enter on last empty bullet
This commit is contained in:
1 parent
02ce554c02
commit
446f8e4649
5 files changed
+209
-48
No files matched your search
@@ -118,3 +118,140 @@ describe('conflict copies', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('slash command menu and outliner fixes', () => {
|
||||
it('does not open the slash menu for URLs like https://a.de/b/ but opens for Text /co and closes on Escape or Backspace', () => {
|
||||
const block = { id: 'b1', type: 'bullet', content: '', level: 0 } as DocumentBlock;
|
||||
const { container, queryByText } = renderBlock(block);
|
||||
const ta = container.querySelector('textarea') as HTMLTextAreaElement;
|
||||
ta.focus();
|
||||
|
||||
fireEvent.change(ta, { target: { value: 'https://a.de/b/' } });
|
||||
expect(queryByText('Befehl auswählen')).toBeNull();
|
||||
|
||||
fireEvent.change(ta, { target: { value: 'Text /co' } });
|
||||
expect(queryByText('Befehl auswählen')).toBeTruthy();
|
||||
expect(queryByText('/code')).toBeTruthy();
|
||||
|
||||
// Removing the slash closes the menu
|
||||
fireEvent.change(ta, { target: { value: 'Text ' } });
|
||||
expect(queryByText('Befehl auswählen')).toBeNull();
|
||||
|
||||
// Even when no command matches, Escape closes the menu
|
||||
fireEvent.change(ta, { target: { value: '/zzzznotfound' } });
|
||||
expect(queryByText('Kein Befehl gefunden')).toBeTruthy();
|
||||
fireEvent.keyDown(ta, { key: 'Escape' });
|
||||
expect(queryByText('Befehl auswählen')).toBeNull();
|
||||
});
|
||||
|
||||
it('scrolls the active slash command button into view on ArrowDown', () => {
|
||||
const scrollSpy = vi.fn();
|
||||
HTMLElement.prototype.scrollIntoView = scrollSpy;
|
||||
const block = { id: 'b1', type: 'bullet', content: '', level: 0 } as DocumentBlock;
|
||||
const { container } = renderBlock(block);
|
||||
const ta = container.querySelector('textarea') as HTMLTextAreaElement;
|
||||
ta.focus();
|
||||
|
||||
fireEvent.change(ta, { target: { value: '/' } });
|
||||
scrollSpy.mockClear();
|
||||
fireEvent.keyDown(ta, { key: 'ArrowDown' });
|
||||
expect(scrollSpy).toHaveBeenCalledWith({ block: 'nearest', inline: 'nearest' });
|
||||
});
|
||||
|
||||
it('creates a new block on Enter at the last empty top-level bullet, after outdenting if indented', () => {
|
||||
const onIndent = vi.fn();
|
||||
const onCreateBelow = vi.fn();
|
||||
const indented = { id: 'b1', type: 'bullet', content: '', level: 1 } as DocumentBlock;
|
||||
const { container, rerender } = render(
|
||||
<OutlinerBlock
|
||||
docId="d"
|
||||
block={indented}
|
||||
index={0}
|
||||
isLast
|
||||
maxAllowedLevel={2}
|
||||
hasChildren={false}
|
||||
isFocused
|
||||
canEdit
|
||||
onUpdateBlock={() => {}}
|
||||
onCreateBlockBelow={onCreateBelow}
|
||||
onDeleteBlock={() => {}}
|
||||
onIndentBlock={onIndent}
|
||||
onNavigateFocus={() => {}}
|
||||
registerTextareaRef={() => {}}
|
||||
/>
|
||||
);
|
||||
const ta = container.querySelector('textarea') as HTMLTextAreaElement;
|
||||
fireEvent.keyDown(ta, { key: 'Enter' });
|
||||
expect(onIndent).toHaveBeenCalledWith('b1', -1);
|
||||
expect(onCreateBelow).not.toHaveBeenCalled();
|
||||
|
||||
rerender(
|
||||
<OutlinerBlock
|
||||
docId="d"
|
||||
block={{ ...indented, level: 0 }}
|
||||
index={0}
|
||||
isLast
|
||||
maxAllowedLevel={1}
|
||||
hasChildren={false}
|
||||
isFocused
|
||||
canEdit
|
||||
onUpdateBlock={() => {}}
|
||||
onCreateBlockBelow={onCreateBelow}
|
||||
onDeleteBlock={() => {}}
|
||||
onIndentBlock={onIndent}
|
||||
onNavigateFocus={() => {}}
|
||||
registerTextareaRef={() => {}}
|
||||
/>
|
||||
);
|
||||
fireEvent.keyDown(ta, { key: 'Enter' });
|
||||
expect(onCreateBelow).toHaveBeenCalledWith('b1', '');
|
||||
});
|
||||
|
||||
it('inserts a bare URL for /link with a web address in an empty bullet, and [Text](url) otherwise', () => {
|
||||
const updates: DocumentBlock[] = [];
|
||||
const block = { id: 'b1', type: 'bullet', content: '', level: 0 } as DocumentBlock;
|
||||
const { container, getByText, getByRole, getByPlaceholderText, rerender } = renderBlock(block, {
|
||||
onUpdateBlock: (u: DocumentBlock) => updates.push(u),
|
||||
});
|
||||
const ta = container.querySelector('textarea') as HTMLTextAreaElement;
|
||||
ta.focus();
|
||||
|
||||
fireEvent.change(ta, { target: { value: '/link' } });
|
||||
fireEvent.keyDown(ta, { key: 'Enter' });
|
||||
|
||||
fireEvent.click(getByText('Web-Link'));
|
||||
fireEvent.change(getByPlaceholderText('https://example.com'), { target: { value: 'https://example.com/test' } });
|
||||
fireEvent.click(getByRole('button', { name: 'Link einfügen' }));
|
||||
|
||||
expect(updates[updates.length - 1]?.content).toBe('https://example.com/test');
|
||||
|
||||
// Non-empty bullet keeps [Text](url)
|
||||
rerender(
|
||||
<OutlinerBlock
|
||||
docId="d"
|
||||
block={{ ...block, content: 'Siehe' }}
|
||||
index={0}
|
||||
isLast
|
||||
maxAllowedLevel={1}
|
||||
hasChildren={false}
|
||||
isFocused={false}
|
||||
canEdit
|
||||
onUpdateBlock={(u: DocumentBlock) => updates.push(u)}
|
||||
onCreateBlockBelow={() => {}}
|
||||
onDeleteBlock={() => {}}
|
||||
onIndentBlock={() => {}}
|
||||
onNavigateFocus={() => {}}
|
||||
registerTextareaRef={() => {}}
|
||||
/>
|
||||
);
|
||||
ta.focus();
|
||||
fireEvent.change(ta, { target: { value: 'Siehe /link' } });
|
||||
fireEvent.keyDown(ta, { key: 'Enter' });
|
||||
fireEvent.click(getByText('Web-Link'));
|
||||
fireEvent.change(getByPlaceholderText('z. B. MindSift Dokumentation'), { target: { value: 'Doku' } });
|
||||
fireEvent.change(getByPlaceholderText('https://example.com'), { target: { value: 'https://example.com/doku' } });
|
||||
fireEvent.click(getByRole('button', { name: 'Link einfügen' }));
|
||||
|
||||
expect(updates[updates.length - 1]?.content).toBe('Siehe [Doku](https://example.com/doku)');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user