@tuturuuu/ui 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/package.json +4 -3
- package/src/components/ui/button.tsx +12 -8
- package/src/components/ui/color-picker.test.tsx +23 -0
- package/src/components/ui/color-picker.tsx +1 -1
- package/src/components/ui/custom/notification-popover-client.tsx +6 -19
- package/src/components/ui/custom/notification-popover-trigger.test.tsx +37 -0
- package/src/components/ui/custom/notification-popover-trigger.tsx +44 -0
- package/src/components/ui/text-editor/__tests__/editor-classes.test.ts +36 -0
- package/src/components/ui/text-editor/__tests__/inline-task-conversion.test.tsx +30 -0
- package/src/components/ui/text-editor/__tests__/keyboard.test.ts +173 -3
- package/src/components/ui/text-editor/__tests__/toggle-block-extension.test.ts +251 -0
- package/src/components/ui/text-editor/editor-classes.ts +100 -0
- package/src/components/ui/text-editor/editor.tsx +11 -153
- package/src/components/ui/text-editor/extensions.ts +19 -0
- package/src/components/ui/text-editor/keyboard.ts +47 -0
- package/src/components/ui/text-editor/toggle-block-extension.ts +153 -0
- package/src/components/ui/text-editor/tool-bar.tsx +24 -73
- package/src/components/ui/text-editor/toolbar-config.ts +67 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.28.0](https://github.com/tutur3u/platform/compare/ui-v0.27.0...ui-v0.28.0) (2026-08-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **tasks:** add toggle blocks and repair editor indentation ([c0805c8](https://github.com/tutur3u/platform/commit/c0805c8cc0e26c172eb6f46582e267b084289501))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **contacts:** restore group tag labels ([9fbcb25](https://github.com/tutur3u/platform/commit/9fbcb25bd44fcedcfad4a463599940e9497ebc06))
|
|
14
|
+
* **notifications:** prevent pre-hydration bell clicks ([6adba83](https://github.com/tutur3u/platform/commit/6adba83f3db63817cc64e332c33a6a2939e0bf83))
|
|
15
|
+
* **ui:** support button slots with TypeScript 7 ([3bdc636](https://github.com/tutur3u/platform/commit/3bdc6368daf0be48719a479c63a6325d414233d6))
|
|
16
|
+
|
|
3
17
|
## [0.27.0](https://github.com/tutur3u/platform/compare/ui-v0.26.1...ui-v0.27.0) (2026-08-14)
|
|
4
18
|
|
|
5
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"@tiptap/extension-collaboration": "3.30.0",
|
|
65
65
|
"@tiptap/extension-collaboration-caret": "3.30.0",
|
|
66
66
|
"@tiptap/extension-color": "3.30.0",
|
|
67
|
+
"@tiptap/extension-details": "3.30.0",
|
|
67
68
|
"@tiptap/extension-drag-handle": "3.30.0",
|
|
68
69
|
"@tiptap/extension-drag-handle-react": "3.30.0",
|
|
69
70
|
"@tiptap/extension-highlight": "3.30.0",
|
|
@@ -90,9 +91,9 @@
|
|
|
90
91
|
"@tuturuuu/apis": "0.11.2",
|
|
91
92
|
"@tuturuuu/hooks": "0.1.1",
|
|
92
93
|
"@tuturuuu/icons": "0.1.0",
|
|
93
|
-
"@tuturuuu/internal-api": "0.
|
|
94
|
+
"@tuturuuu/internal-api": "0.31.0",
|
|
94
95
|
"@tuturuuu/supabase": "0.5.0",
|
|
95
|
-
"@tuturuuu/utils": "0.25.
|
|
96
|
+
"@tuturuuu/utils": "0.25.1",
|
|
96
97
|
"@types/debug": "^4.1.13",
|
|
97
98
|
"browser-image-compression": "^2.0.2",
|
|
98
99
|
"class-variance-authority": "^0.7.1",
|
|
@@ -46,15 +46,19 @@ function Button({
|
|
|
46
46
|
VariantProps<typeof buttonVariants> & {
|
|
47
47
|
asChild?: boolean;
|
|
48
48
|
}) {
|
|
49
|
-
const
|
|
49
|
+
const classNames = cn(buttonVariants({ variant, size, className }));
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
if (asChild) {
|
|
52
|
+
return (
|
|
53
|
+
<Slot
|
|
54
|
+
data-slot="button"
|
|
55
|
+
className={classNames}
|
|
56
|
+
{...(props as React.ComponentProps<typeof Slot>)}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return <button data-slot="button" className={classNames} {...props} />;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
type ButtonProps = React.ComponentProps<typeof Button>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { ColorPicker } from './color-picker';
|
|
4
|
+
|
|
5
|
+
vi.mock('../../hooks/use-dom-resolved-theme', () => ({
|
|
6
|
+
useDomResolvedTheme: () => 'light',
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe('ColorPicker', () => {
|
|
10
|
+
it('renders the supplied read-only label', () => {
|
|
11
|
+
render(<ColorPicker text="Visible workspace tag" value="#123456" />);
|
|
12
|
+
|
|
13
|
+
expect(
|
|
14
|
+
screen.getByRole('button', { name: 'Visible workspace tag' })
|
|
15
|
+
).toBeVisible();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('keeps the icon-only control when no label is supplied', () => {
|
|
19
|
+
const { container } = render(<ColorPicker value="#123456" />);
|
|
20
|
+
|
|
21
|
+
expect(container.querySelector('button svg')).toBeTruthy();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -65,7 +65,7 @@ const ColorPicker = forwardRef<
|
|
|
65
65
|
variant="outline"
|
|
66
66
|
disabled={disabled}
|
|
67
67
|
>
|
|
68
|
-
<TextIcon className="h-4 w-4" />
|
|
68
|
+
{text || <TextIcon className="h-4 w-4" />}
|
|
69
69
|
</Button>
|
|
70
70
|
</PopoverTrigger>
|
|
71
71
|
<PopoverContent className="flex w-full flex-col items-center justify-center gap-4">
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
dispatchRequestOpenTask,
|
|
42
42
|
waitForTaskOpenResult,
|
|
43
43
|
} from '@tuturuuu/ui/lib/task-open-events';
|
|
44
|
-
import { Popover, PopoverContent
|
|
44
|
+
import { Popover, PopoverContent } from '@tuturuuu/ui/popover';
|
|
45
45
|
import { toast } from '@tuturuuu/ui/sonner';
|
|
46
46
|
import { cn } from '@tuturuuu/utils/format';
|
|
47
47
|
import dayjs from 'dayjs';
|
|
@@ -49,6 +49,7 @@ import relativeTime from 'dayjs/plugin/relativeTime';
|
|
|
49
49
|
import Link from 'next/link';
|
|
50
50
|
import { useParams, usePathname, useRouter } from 'next/navigation';
|
|
51
51
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
52
|
+
import { NotificationPopoverTriggerButton } from './notification-popover-trigger';
|
|
52
53
|
|
|
53
54
|
dayjs.extend(relativeTime);
|
|
54
55
|
|
|
@@ -190,24 +191,10 @@ export default function NotificationPopoverClient({
|
|
|
190
191
|
|
|
191
192
|
return (
|
|
192
193
|
<Popover open={open} onOpenChange={setOpen}>
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
aria-label={notificationsText}
|
|
198
|
-
title={notificationsText}
|
|
199
|
-
className="group relative flex size-10 flex-none transition-all"
|
|
200
|
-
>
|
|
201
|
-
<Bell className="h-6 w-6" />
|
|
202
|
-
{unreadCount > 0 && (
|
|
203
|
-
<div className="absolute top-1 right-2 flex h-1.5 w-1.5 flex-none items-center justify-center rounded-full bg-dynamic-red p-1 text-center font-semibold text-xs transition-all group-hover:-top-2 group-hover:-right-1 group-hover:h-5 group-hover:w-auto group-hover:px-1.5 group-hover:text-background">
|
|
204
|
-
<div className="relative opacity-0 group-hover:opacity-100">
|
|
205
|
-
{unreadCount > 99 ? '99+' : unreadCount}
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
)}
|
|
209
|
-
</Button>
|
|
210
|
-
</PopoverTrigger>
|
|
194
|
+
<NotificationPopoverTriggerButton
|
|
195
|
+
notificationsText={notificationsText}
|
|
196
|
+
unreadCount={unreadCount}
|
|
197
|
+
/>
|
|
211
198
|
<PopoverContent
|
|
212
199
|
className="w-100 overflow-hidden rounded-xl border p-0 shadow-xl"
|
|
213
200
|
align="end"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { Popover } from '@tuturuuu/ui/popover';
|
|
4
|
+
import { renderToString } from 'react-dom/server';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { NotificationPopoverTriggerButton } from './notification-popover-trigger';
|
|
7
|
+
|
|
8
|
+
function TriggerHarness({ unreadCount = 0 }: { unreadCount?: number }) {
|
|
9
|
+
return (
|
|
10
|
+
<Popover>
|
|
11
|
+
<NotificationPopoverTriggerButton
|
|
12
|
+
notificationsText="Notifications"
|
|
13
|
+
unreadCount={unreadCount}
|
|
14
|
+
/>
|
|
15
|
+
</Popover>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('NotificationPopoverTriggerButton', () => {
|
|
20
|
+
it('disables the server-rendered trigger until event handlers are attached', () => {
|
|
21
|
+
const markup = renderToString(<TriggerHarness unreadCount={1} />);
|
|
22
|
+
|
|
23
|
+
expect(markup).toContain('disabled=""');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('enables the trigger after hydration and opens the popover on the first click', async () => {
|
|
27
|
+
render(<TriggerHarness unreadCount={1} />);
|
|
28
|
+
|
|
29
|
+
const trigger = screen.getByRole('button', { name: 'Notifications' });
|
|
30
|
+
await waitFor(() => expect(trigger).toBeEnabled());
|
|
31
|
+
fireEvent.click(trigger);
|
|
32
|
+
|
|
33
|
+
await waitFor(() =>
|
|
34
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true')
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Bell } from '@tuturuuu/icons';
|
|
4
|
+
import { Button } from '@tuturuuu/ui/button';
|
|
5
|
+
import { PopoverTrigger } from '@tuturuuu/ui/popover';
|
|
6
|
+
import { useEffect, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
interface NotificationPopoverTriggerProps {
|
|
9
|
+
notificationsText: string;
|
|
10
|
+
unreadCount: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function NotificationPopoverTriggerButton({
|
|
14
|
+
notificationsText,
|
|
15
|
+
unreadCount,
|
|
16
|
+
}: NotificationPopoverTriggerProps) {
|
|
17
|
+
const [isHydrated, setIsHydrated] = useState(false);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setIsHydrated(true);
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<PopoverTrigger asChild>
|
|
25
|
+
<Button
|
|
26
|
+
variant="ghost"
|
|
27
|
+
size="icon"
|
|
28
|
+
aria-label={notificationsText}
|
|
29
|
+
title={notificationsText}
|
|
30
|
+
disabled={!isHydrated}
|
|
31
|
+
className="group relative flex size-10 flex-none transition-all"
|
|
32
|
+
>
|
|
33
|
+
<Bell className="h-6 w-6" />
|
|
34
|
+
{unreadCount > 0 && (
|
|
35
|
+
<div className="absolute top-1 right-2 flex h-1.5 w-1.5 flex-none items-center justify-center rounded-full bg-dynamic-red p-1 text-center font-semibold text-xs transition-all group-hover:-top-2 group-hover:-right-1 group-hover:h-5 group-hover:w-auto group-hover:px-1.5 group-hover:text-background">
|
|
36
|
+
<div className="relative opacity-0 group-hover:opacity-100">
|
|
37
|
+
{unreadCount > 99 ? '99+' : unreadCount}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
41
|
+
</Button>
|
|
42
|
+
</PopoverTrigger>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { getRichTextEditorClasses } from '../editor-classes';
|
|
3
|
+
|
|
4
|
+
describe('rich text editor classes', () => {
|
|
5
|
+
it('keeps regular and checklist nesting visibly indented', () => {
|
|
6
|
+
const classes = getRichTextEditorClasses({ readOnly: false });
|
|
7
|
+
|
|
8
|
+
expect(classes).toContain('[&_li_ul]:ml-6');
|
|
9
|
+
expect(classes).toContain('[&_li_ol]:ml-6');
|
|
10
|
+
expect(classes).toContain(
|
|
11
|
+
'[&_ul[data-type="taskList"]_ul[data-type="taskList"]]:ml-6'
|
|
12
|
+
);
|
|
13
|
+
expect(classes).not.toContain(
|
|
14
|
+
'[&_ul[data-type="taskList"]_ul[data-type="taskList"]]:ml-0'
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('styles toggle summaries by heading level and indents their body', () => {
|
|
19
|
+
const classes = getRichTextEditorClasses({ readOnly: false });
|
|
20
|
+
|
|
21
|
+
expect(classes).toContain('[&_summary[data-heading-level="1"]]:text-4xl');
|
|
22
|
+
expect(classes).toContain('[&_summary[data-heading-level="2"]]:text-3xl');
|
|
23
|
+
expect(classes).toContain('[&_summary[data-heading-level="3"]]:text-2xl');
|
|
24
|
+
expect(classes).toContain('[&_div[data-type="detailsContent"]]:border-l');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('retains read-only interaction guards and custom classes', () => {
|
|
28
|
+
const classes = getRichTextEditorClasses({
|
|
29
|
+
className: 'task-description-editor',
|
|
30
|
+
readOnly: true,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(classes).toContain('task-description-editor');
|
|
34
|
+
expect(classes).toContain('[&_.task-list-checkbox]:!pointer-events-none');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -19,6 +19,7 @@ function createEditorStub(): Editor {
|
|
|
19
19
|
toggleSubscript: vi.fn(() => chain),
|
|
20
20
|
toggleSuperscript: vi.fn(() => chain),
|
|
21
21
|
toggleTaskListSmart: vi.fn(() => chain),
|
|
22
|
+
toggleDetailsBlock: vi.fn(() => chain),
|
|
22
23
|
run: vi.fn(() => true),
|
|
23
24
|
};
|
|
24
25
|
|
|
@@ -43,4 +44,33 @@ describe('inline task conversion toolbar', () => {
|
|
|
43
44
|
|
|
44
45
|
expect(onConvertToTask).toHaveBeenCalledTimes(1);
|
|
45
46
|
});
|
|
47
|
+
|
|
48
|
+
it('exposes toggle blocks and delegates the command through the editor chain', () => {
|
|
49
|
+
const editor = createEditorStub();
|
|
50
|
+
render(<FixedToolbar editor={editor} />);
|
|
51
|
+
|
|
52
|
+
fireEvent.click(
|
|
53
|
+
screen.getByRole('button', { name: 'Toggle List or Heading' })
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const chain = vi.mocked(editor.chain).mock.results[0]?.value as {
|
|
57
|
+
toggleDetailsBlock: ReturnType<typeof vi.fn>;
|
|
58
|
+
run: ReturnType<typeof vi.fn>;
|
|
59
|
+
};
|
|
60
|
+
expect(chain.toggleDetailsBlock).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(chain.run).toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('uses the localized toggle block label when provided', () => {
|
|
65
|
+
render(
|
|
66
|
+
<FixedToolbar
|
|
67
|
+
editor={createEditorStub()}
|
|
68
|
+
toggleBlockLabel="Liste ou titre repliable"
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(
|
|
73
|
+
screen.getByRole('button', { name: 'Liste ou titre repliable' })
|
|
74
|
+
).toBeInTheDocument();
|
|
75
|
+
});
|
|
46
76
|
});
|
|
@@ -2,7 +2,7 @@ import { Schema } from '@tiptap/pm/model';
|
|
|
2
2
|
import { EditorState, TextSelection } from '@tiptap/pm/state';
|
|
3
3
|
import type { EditorView } from '@tiptap/pm/view';
|
|
4
4
|
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { handlePlainEnterFallback } from '../keyboard';
|
|
5
|
+
import { handleListIndentation, handlePlainEnterFallback } from '../keyboard';
|
|
6
6
|
|
|
7
7
|
const schema = new Schema({
|
|
8
8
|
nodes: {
|
|
@@ -15,6 +15,10 @@ const schema = new Schema({
|
|
|
15
15
|
group: 'block',
|
|
16
16
|
content: 'listItem+',
|
|
17
17
|
},
|
|
18
|
+
orderedList: {
|
|
19
|
+
group: 'block',
|
|
20
|
+
content: 'listItem+',
|
|
21
|
+
},
|
|
18
22
|
listItem: {
|
|
19
23
|
content: 'paragraph block*',
|
|
20
24
|
defining: true,
|
|
@@ -37,11 +41,19 @@ const schema = new Schema({
|
|
|
37
41
|
marks: {},
|
|
38
42
|
});
|
|
39
43
|
|
|
40
|
-
function stateWithSelection(
|
|
44
|
+
function stateWithSelection(
|
|
45
|
+
doc: ReturnType<typeof schema.node>,
|
|
46
|
+
offset = 1,
|
|
47
|
+
targetText?: string
|
|
48
|
+
) {
|
|
41
49
|
let textStart: number | null = null;
|
|
42
50
|
|
|
43
51
|
doc.descendants((node, pos) => {
|
|
44
|
-
if (
|
|
52
|
+
if (
|
|
53
|
+
textStart === null &&
|
|
54
|
+
node.isText &&
|
|
55
|
+
(!targetText || node.text === targetText)
|
|
56
|
+
) {
|
|
45
57
|
textStart = pos;
|
|
46
58
|
return false;
|
|
47
59
|
}
|
|
@@ -69,6 +81,19 @@ function enterEvent(overrides: Partial<KeyboardEvent> = {}) {
|
|
|
69
81
|
} as unknown as KeyboardEvent;
|
|
70
82
|
}
|
|
71
83
|
|
|
84
|
+
function tabEvent(overrides: Partial<KeyboardEvent> = {}) {
|
|
85
|
+
return {
|
|
86
|
+
altKey: false,
|
|
87
|
+
ctrlKey: false,
|
|
88
|
+
key: 'Tab',
|
|
89
|
+
metaKey: false,
|
|
90
|
+
preventDefault: vi.fn(),
|
|
91
|
+
shiftKey: false,
|
|
92
|
+
stopPropagation: vi.fn(),
|
|
93
|
+
...overrides,
|
|
94
|
+
} as unknown as KeyboardEvent;
|
|
95
|
+
}
|
|
96
|
+
|
|
72
97
|
function viewForState(state: EditorState) {
|
|
73
98
|
let currentState = state;
|
|
74
99
|
const view = {
|
|
@@ -173,4 +198,149 @@ describe('text editor keyboard handling', () => {
|
|
|
173
198
|
expect(handlePlainEnterFallback(view, event)).toBe(false);
|
|
174
199
|
expect(event.preventDefault).not.toHaveBeenCalled();
|
|
175
200
|
});
|
|
201
|
+
|
|
202
|
+
it.each(['bulletList', 'orderedList'] as const)(
|
|
203
|
+
'indents and outdents a second %s item with Tab and Shift+Tab',
|
|
204
|
+
(listType) => {
|
|
205
|
+
const state = stateWithSelection(
|
|
206
|
+
schema.node('doc', null, [
|
|
207
|
+
schema.node(listType, null, [
|
|
208
|
+
schema.node('listItem', null, [
|
|
209
|
+
schema.node('paragraph', null, schema.text('first')),
|
|
210
|
+
]),
|
|
211
|
+
schema.node('listItem', null, [
|
|
212
|
+
schema.node('paragraph', null, schema.text('second')),
|
|
213
|
+
]),
|
|
214
|
+
]),
|
|
215
|
+
]),
|
|
216
|
+
1,
|
|
217
|
+
'second'
|
|
218
|
+
);
|
|
219
|
+
const { view, getState } = viewForState(state);
|
|
220
|
+
const indentEvent = tabEvent();
|
|
221
|
+
|
|
222
|
+
expect(handleListIndentation(view, indentEvent)).toBe(true);
|
|
223
|
+
expect(indentEvent.preventDefault).toHaveBeenCalledTimes(1);
|
|
224
|
+
expect(indentEvent.stopPropagation).toHaveBeenCalledTimes(1);
|
|
225
|
+
expect(getState().doc.toJSON()).toMatchObject({
|
|
226
|
+
content: [
|
|
227
|
+
{
|
|
228
|
+
type: listType,
|
|
229
|
+
content: [
|
|
230
|
+
{
|
|
231
|
+
type: 'listItem',
|
|
232
|
+
content: [
|
|
233
|
+
{ type: 'paragraph' },
|
|
234
|
+
{
|
|
235
|
+
type: listType,
|
|
236
|
+
content: [{ type: 'listItem' }],
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const outdentEvent = tabEvent({ shiftKey: true });
|
|
246
|
+
expect(handleListIndentation(view, outdentEvent)).toBe(true);
|
|
247
|
+
expect(getState().doc.child(0).childCount).toBe(2);
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
it('indents and outdents checklist items', () => {
|
|
252
|
+
const state = stateWithSelection(
|
|
253
|
+
schema.node('doc', null, [
|
|
254
|
+
schema.node('taskList', null, [
|
|
255
|
+
schema.node('taskItem', null, [
|
|
256
|
+
schema.node('paragraph', null, schema.text('first')),
|
|
257
|
+
]),
|
|
258
|
+
schema.node('taskItem', null, [
|
|
259
|
+
schema.node('paragraph', null, schema.text('second')),
|
|
260
|
+
]),
|
|
261
|
+
]),
|
|
262
|
+
]),
|
|
263
|
+
1,
|
|
264
|
+
'second'
|
|
265
|
+
);
|
|
266
|
+
const { view, getState } = viewForState(state);
|
|
267
|
+
|
|
268
|
+
expect(handleListIndentation(view, tabEvent())).toBe(true);
|
|
269
|
+
expect(getState().doc.toJSON()).toMatchObject({
|
|
270
|
+
content: [
|
|
271
|
+
{
|
|
272
|
+
type: 'taskList',
|
|
273
|
+
content: [
|
|
274
|
+
{
|
|
275
|
+
type: 'taskItem',
|
|
276
|
+
content: [
|
|
277
|
+
{ type: 'paragraph' },
|
|
278
|
+
{
|
|
279
|
+
type: 'taskList',
|
|
280
|
+
content: [{ type: 'taskItem' }],
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(handleListIndentation(view, tabEvent({ shiftKey: true }))).toBe(
|
|
290
|
+
true
|
|
291
|
+
);
|
|
292
|
+
expect(getState().doc.child(0).childCount).toBe(2);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('does not consume Tab outside a list or when the first item cannot indent', () => {
|
|
296
|
+
const paragraph = viewForState(
|
|
297
|
+
stateWithSelection(
|
|
298
|
+
schema.node('doc', null, [
|
|
299
|
+
schema.node('paragraph', null, schema.text('plain')),
|
|
300
|
+
])
|
|
301
|
+
)
|
|
302
|
+
);
|
|
303
|
+
const paragraphEvent = tabEvent();
|
|
304
|
+
|
|
305
|
+
expect(handleListIndentation(paragraph.view, paragraphEvent)).toBe(false);
|
|
306
|
+
expect(paragraphEvent.preventDefault).not.toHaveBeenCalled();
|
|
307
|
+
|
|
308
|
+
const firstItem = viewForState(
|
|
309
|
+
stateWithSelection(
|
|
310
|
+
schema.node('doc', null, [
|
|
311
|
+
schema.node('bulletList', null, [
|
|
312
|
+
schema.node('listItem', null, [
|
|
313
|
+
schema.node('paragraph', null, schema.text('only')),
|
|
314
|
+
]),
|
|
315
|
+
]),
|
|
316
|
+
])
|
|
317
|
+
)
|
|
318
|
+
);
|
|
319
|
+
const firstItemEvent = tabEvent();
|
|
320
|
+
|
|
321
|
+
expect(handleListIndentation(firstItem.view, firstItemEvent)).toBe(false);
|
|
322
|
+
expect(firstItemEvent.preventDefault).not.toHaveBeenCalled();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('does not hijack modified Tab shortcuts', () => {
|
|
326
|
+
const state = stateWithSelection(
|
|
327
|
+
schema.node('doc', null, [
|
|
328
|
+
schema.node('bulletList', null, [
|
|
329
|
+
schema.node('listItem', null, [
|
|
330
|
+
schema.node('paragraph', null, schema.text('first')),
|
|
331
|
+
]),
|
|
332
|
+
schema.node('listItem', null, [
|
|
333
|
+
schema.node('paragraph', null, schema.text('second')),
|
|
334
|
+
]),
|
|
335
|
+
]),
|
|
336
|
+
]),
|
|
337
|
+
1,
|
|
338
|
+
'second'
|
|
339
|
+
);
|
|
340
|
+
const { view } = viewForState(state);
|
|
341
|
+
|
|
342
|
+
expect(handleListIndentation(view, tabEvent({ metaKey: true }))).toBe(
|
|
343
|
+
false
|
|
344
|
+
);
|
|
345
|
+
});
|
|
176
346
|
});
|