@estiva-app/ui 0.65.3 → 0.67.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.
Files changed (43) hide show
  1. package/dist/BlockNodes.d.ts +33 -0
  2. package/dist/BlockNodes.d.ts.map +1 -1
  3. package/dist/ComposerTriggers.d.ts +11 -2
  4. package/dist/ComposerTriggers.d.ts.map +1 -1
  5. package/dist/EditorTableControls.d.ts +38 -0
  6. package/dist/EditorTableControls.d.ts.map +1 -0
  7. package/dist/NavItem.d.ts +11 -1
  8. package/dist/NavItem.d.ts.map +1 -1
  9. package/dist/RichText.d.ts +13 -1
  10. package/dist/RichText.d.ts.map +1 -1
  11. package/dist/editor.d.ts +2 -1
  12. package/dist/editor.d.ts.map +1 -1
  13. package/dist/editor.js +363 -18
  14. package/dist/editor.js.map +4 -4
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +71 -23
  18. package/dist/index.js.map +3 -3
  19. package/package.json +2 -1
  20. package/registry.json +209 -4
  21. package/src/BlockNodes.mdx +32 -1
  22. package/src/BlockNodes.stories.tsx +29 -1
  23. package/src/BlockNodes.test.tsx +141 -2
  24. package/src/BlockNodes.tsx +227 -4
  25. package/src/ComposerTriggers.test.tsx +23 -0
  26. package/src/ComposerTriggers.tsx +61 -5
  27. package/src/EditorTableControls.mdx +78 -0
  28. package/src/EditorTableControls.stories.tsx +63 -0
  29. package/src/EditorTableControls.test.tsx +288 -0
  30. package/src/EditorTableControls.tsx +255 -0
  31. package/src/NavItem.mdx +10 -0
  32. package/src/NavItem.stories.tsx +23 -1
  33. package/src/NavItem.test.tsx +36 -0
  34. package/src/NavItem.tsx +40 -16
  35. package/src/RichText.mdx +12 -0
  36. package/src/RichText.stories.tsx +50 -0
  37. package/src/RichText.test.tsx +56 -0
  38. package/src/RichText.tsx +83 -10
  39. package/src/editor.ts +4 -0
  40. package/src/index.ts +1 -1
  41. package/src/registry/registry.test.ts +7 -7
  42. package/src/registry/siblings.test.ts +2 -2
  43. package/stories/Choosing.mdx +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estiva-app/ui",
3
- "version": "0.65.3",
3
+ "version": "0.67.0",
4
4
  "description": "Estiva's design tokens (the contract) and a small set of primitives (a convenience) for every Estiva app.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -136,6 +136,7 @@
136
136
  "@tiptap/core": "^3.31.3",
137
137
  "@tiptap/extension-code-block-lowlight": "^3.31.3",
138
138
  "@tiptap/extension-mention": "^3.31.3",
139
+ "@tiptap/extension-table": "^3.31.3",
139
140
  "@tiptap/pm": "^3.31.3",
140
141
  "@tiptap/react": "^3.31.3",
141
142
  "@tiptap/starter-kit": "^3.31.3",
package/registry.json CHANGED
@@ -4,9 +4,9 @@
4
4
  "kind": "package",
5
5
  "repo": "estiva-ui",
6
6
  "package": "@estiva-app/ui",
7
- "packageVersion": "0.65.3",
8
- "exports": 140,
9
- "typeExports": 145,
7
+ "packageVersion": "0.67.0",
8
+ "exports": 149,
9
+ "typeExports": 146,
10
10
  "files": null
11
11
  },
12
12
  "storybook": {
@@ -15,6 +15,44 @@
15
15
  "devUrl": "http://localhost:6008"
16
16
  },
17
17
  "entries": [
18
+ {
19
+ "name": "appendTableColumn",
20
+ "repo": "estiva-ui",
21
+ "kind": "helper",
22
+ "importPath": "@estiva-app/ui/editor",
23
+ "sourceFile": "src/EditorTableControls.tsx",
24
+ "purpose": "A column after the last one: the ➕ right of the table.",
25
+ "purposeFrom": "comment",
26
+ "props": [],
27
+ "variants": [],
28
+ "ownsBehaviours": [],
29
+ "status": "stable",
30
+ "migrationStage": null,
31
+ "docsId": "primitives-editortablecontrols--docs",
32
+ "storyId": "primitives-editortablecontrols--default",
33
+ "docPage": null,
34
+ "app": null,
35
+ "looks": []
36
+ },
37
+ {
38
+ "name": "appendTableRow",
39
+ "repo": "estiva-ui",
40
+ "kind": "helper",
41
+ "importPath": "@estiva-app/ui/editor",
42
+ "sourceFile": "src/EditorTableControls.tsx",
43
+ "purpose": "A row after the last one: the ➕ under the table.",
44
+ "purposeFrom": "comment",
45
+ "props": [],
46
+ "variants": [],
47
+ "ownsBehaviours": [],
48
+ "status": "stable",
49
+ "migrationStage": null,
50
+ "docsId": "primitives-editortablecontrols--docs",
51
+ "storyId": "primitives-editortablecontrols--default",
52
+ "docPage": null,
53
+ "app": null,
54
+ "looks": []
55
+ },
18
56
  {
19
57
  "name": "applyFormat",
20
58
  "repo": "estiva-ui",
@@ -734,6 +772,44 @@
734
772
  "signal:bg-transparent signal:border signal:border-border-default signal:text-text-primary signal:shadow-none signal:hover:bg-success-muted signal:hover:border-success-outline signal:hover:text-success-default signal:transition-colors"
735
773
  ]
736
774
  },
775
+ {
776
+ "name": "Callout",
777
+ "repo": "estiva-ui",
778
+ "kind": "component",
779
+ "importPath": "@estiva-app/ui/editor",
780
+ "sourceFile": "src/BlockNodes.tsx",
781
+ "purpose": "A callout — SPEC §13.3's `tone` on a quote (RIC-19): a quote drawn to stand out, in one of GitHub's alert kinds (`note`, `tip`, `important`, `warning`, `caution`).",
782
+ "purposeFrom": "comment",
783
+ "props": [],
784
+ "variants": [],
785
+ "ownsBehaviours": [],
786
+ "status": "stable",
787
+ "migrationStage": null,
788
+ "docsId": "primitives-blocknodes--docs",
789
+ "storyId": "primitives-blocknodes--default",
790
+ "docPage": null,
791
+ "app": null,
792
+ "looks": []
793
+ },
794
+ {
795
+ "name": "calloutLook",
796
+ "repo": "estiva-ui",
797
+ "kind": "helper",
798
+ "importPath": "@estiva-app/ui",
799
+ "sourceFile": "src/RichText.tsx",
800
+ "purpose": "The tone a callout is drawn in: its own, or `note` for one this part does not know (§13.3).",
801
+ "purposeFrom": "comment",
802
+ "props": [],
803
+ "variants": [],
804
+ "ownsBehaviours": [],
805
+ "status": "stable",
806
+ "migrationStage": null,
807
+ "docsId": "components-richtext--docs",
808
+ "storyId": "components-richtext--every-block",
809
+ "docPage": null,
810
+ "app": null,
811
+ "looks": []
812
+ },
737
813
  {
738
814
  "name": "CaptionedReference",
739
815
  "repo": "estiva-ui",
@@ -967,6 +1043,25 @@
967
1043
  "rounded-lg transition-colors bg-bg-selected cursor-pointer hover:bg-bg-hover"
968
1044
  ]
969
1045
  },
1046
+ {
1047
+ "name": "Checklist",
1048
+ "repo": "estiva-ui",
1049
+ "kind": "component",
1050
+ "importPath": "@estiva-app/ui/editor",
1051
+ "sourceFile": "src/BlockNodes.tsx",
1052
+ "purpose": "A checklist item — SPEC §13.3's `checked` on a list item (RIC-19): `true` ticked, `false` not, absent a plain item.",
1053
+ "purposeFrom": "comment",
1054
+ "props": [],
1055
+ "variants": [],
1056
+ "ownsBehaviours": [],
1057
+ "status": "stable",
1058
+ "migrationStage": null,
1059
+ "docsId": "primitives-blocknodes--docs",
1060
+ "storyId": "primitives-blocknodes--default",
1061
+ "docPage": null,
1062
+ "app": null,
1063
+ "looks": []
1064
+ },
970
1065
  {
971
1066
  "name": "Chip",
972
1067
  "repo": "estiva-ui",
@@ -1906,6 +2001,44 @@
1906
2001
  "app": null,
1907
2002
  "looks": []
1908
2003
  },
2004
+ {
2005
+ "name": "deleteTableColumn",
2006
+ "repo": "estiva-ui",
2007
+ "kind": "helper",
2008
+ "importPath": "@estiva-app/ui/editor",
2009
+ "sourceFile": "src/EditorTableControls.tsx",
2010
+ "purpose": "The same for a column: the last one deletes the table.",
2011
+ "purposeFrom": "comment",
2012
+ "props": [],
2013
+ "variants": [],
2014
+ "ownsBehaviours": [],
2015
+ "status": "stable",
2016
+ "migrationStage": null,
2017
+ "docsId": "primitives-editortablecontrols--docs",
2018
+ "storyId": "primitives-editortablecontrols--default",
2019
+ "docPage": null,
2020
+ "app": null,
2021
+ "looks": []
2022
+ },
2023
+ {
2024
+ "name": "deleteTableRow",
2025
+ "repo": "estiva-ui",
2026
+ "kind": "helper",
2027
+ "importPath": "@estiva-app/ui/editor",
2028
+ "sourceFile": "src/EditorTableControls.tsx",
2029
+ "purpose": "The cell's row, or the whole table when it is its only one: a table holds at least one row (prosemirror-tables refuses to delete it).",
2030
+ "purposeFrom": "comment",
2031
+ "props": [],
2032
+ "variants": [],
2033
+ "ownsBehaviours": [],
2034
+ "status": "stable",
2035
+ "migrationStage": null,
2036
+ "docsId": "primitives-editortablecontrols--docs",
2037
+ "storyId": "primitives-editortablecontrols--default",
2038
+ "docPage": null,
2039
+ "app": null,
2040
+ "looks": []
2041
+ },
1909
2042
  {
1910
2043
  "name": "DialogShell",
1911
2044
  "repo": "estiva-ui",
@@ -2235,6 +2368,47 @@
2235
2368
  "app": null,
2236
2369
  "looks": []
2237
2370
  },
2371
+ {
2372
+ "name": "EditorTableControls",
2373
+ "repo": "estiva-ui",
2374
+ "kind": "component",
2375
+ "importPath": "@estiva-app/ui/editor",
2376
+ "sourceFile": "src/EditorTableControls.tsx",
2377
+ "purpose": "A table's own controls in a Tiptap editor: point at a cell for a handle on its row and on its column, and a ➕ bar under the table and right of it.",
2378
+ "purposeFrom": "page",
2379
+ "props": [
2380
+ {
2381
+ "name": "editor",
2382
+ "takes": "Editor | null",
2383
+ "required": true,
2384
+ "note": "The editor, with TableKit (`@tiptap/extension-table`) in its extensions."
2385
+ },
2386
+ {
2387
+ "name": "children",
2388
+ "takes": "anything",
2389
+ "required": true,
2390
+ "note": "The `EditorContent`."
2391
+ },
2392
+ {
2393
+ "name": "className",
2394
+ "takes": "text",
2395
+ "required": false,
2396
+ "note": null
2397
+ }
2398
+ ],
2399
+ "variants": [],
2400
+ "ownsBehaviours": [],
2401
+ "status": "stable",
2402
+ "migrationStage": null,
2403
+ "docsId": "primitives-editortablecontrols--docs",
2404
+ "storyId": "primitives-editortablecontrols--default",
2405
+ "docPage": "src/EditorTableControls.mdx",
2406
+ "app": null,
2407
+ "looks": [
2408
+ "rounded-lg border border-border-default bg-bg-elevated shadow-lg shadow-none -translate-x-1/2 -translate-y-1/2",
2409
+ "rounded-lg border border-border-default bg-bg-elevated shadow-lg shadow-none"
2410
+ ]
2411
+ },
2238
2412
  {
2239
2413
  "name": "EmptyState",
2240
2414
  "repo": "estiva-ui",
@@ -3144,6 +3318,25 @@
3144
3318
  "app": null,
3145
3319
  "looks": []
3146
3320
  },
3321
+ {
3322
+ "name": "isChecklistTick",
3323
+ "repo": "estiva-ui",
3324
+ "kind": "helper",
3325
+ "importPath": "@estiva-app/ui/editor",
3326
+ "sourceFile": "src/BlockNodes.tsx",
3327
+ "purpose": "Whether a change is a click on a checklist item's box.",
3328
+ "purposeFrom": "comment",
3329
+ "props": [],
3330
+ "variants": [],
3331
+ "ownsBehaviours": [],
3332
+ "status": "stable",
3333
+ "migrationStage": null,
3334
+ "docsId": "primitives-blocknodes--docs",
3335
+ "storyId": "primitives-blocknodes--default",
3336
+ "docPage": null,
3337
+ "app": null,
3338
+ "looks": []
3339
+ },
3147
3340
  {
3148
3341
  "name": "isSuggestionActive",
3149
3342
  "repo": "estiva-ui",
@@ -4164,6 +4357,12 @@
4164
4357
  "required": false,
4165
4358
  "note": "The ⋮'s accessible name and tooltip. Default \"More options for {label}\"."
4166
4359
  },
4360
+ {
4361
+ "name": "action",
4362
+ "takes": "SectionAction",
4363
+ "required": false,
4364
+ "note": "One button beside the ⋮, shown with it: any 16px icon, its tooltip and its handler — a `SectionHeader` action's shape."
4365
+ },
4167
4366
  {
4168
4367
  "name": "hint",
4169
4368
  "takes": "anything",
@@ -5406,6 +5605,12 @@
5406
5605
  "takes": "a handler",
5407
5606
  "required": false,
5408
5607
  "note": "Draws a block this part has no design for (an attachment, a widget)."
5608
+ },
5609
+ {
5610
+ "name": "onTick",
5611
+ "takes": "a handler",
5612
+ "required": false,
5613
+ "note": "Ticks or unticks a checklist item, by its block id: its box is then a control, one click where the text is read."
5409
5614
  }
5410
5615
  ],
5411
5616
  "variants": [
@@ -5430,7 +5635,7 @@
5430
5635
  "[&_:not(pre)>code]:rounded-sm [&_:not(pre)>code]:bg-bg-code [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:border [&_:not(pre)>code]:border-border-default [&_code]:font-mono [&_code]:text-[0.85em] [&_code]:text-text-primary",
5431
5636
  "[&_pre>code]:text-[1em] [&_pre]:whitespace-pre-wrap [&_pre]:break-words [&_pre]:rounded-md [&_pre]:bg-bg-code [&_pre]:px-3 [&_pre]:py-2.5 [&_pre]:font-mono [&_pre]:text-[0.85em] [&_pre]:leading-relaxed [&_pre]:text-text-primary",
5432
5637
  "[&_:is(.hljs-keyword,.hljs-doctag,.hljs-type,.hljs-template-tag,.hljs-template-variable)]:text-syntax-keyword [&_.hljs-title]:text-syntax-function [&_:is(.hljs-number,.hljs-literal,.hljs-attr,.hljs-attribute,.hljs-meta,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id)]:text-syntax-constant [&_:is(.hljs-string,.hljs-regexp)]:text-syntax-string [&_:is(.hljs-built_in,.hljs-symbol)]:text-syntax-variable [&_.hljs-comment]:text-syntax-comment [&_:is(.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo)]:text-syntax-tag",
5433
- "[&_:is(th,td)>p]:m-0 [&_:is(th,td)]:border [&_:is(th,td)]:border-border-default [&_:is(th,td)]:px-2 [&_:is(th,td)]:py-1 [&_:is(th,td)]:text-left [&_:is(th,td)]:align-top [&_table]:w-full [&_table]:border-collapse [&_th]:bg-bg-code [&_th]:font-medium [&_th]:text-text-primary",
5638
+ "[&_:is(th,td)>p]:m-0 [&_:is(th,td)]:border [&_:is(th,td)]:border-border-default [&_:is(th,td)]:px-2 [&_:is(th,td)]:py-1 [&_:is(th,td)]:text-left [&_:is(th,td)]:align-top [&_table]:my-2 [&_table]:w-full [&_table]:border-collapse [&_th]:bg-bg-code [&_th]:font-medium [&_th]:text-text-primary",
5434
5639
  "[&_em]:italic [&_strong]:font-semibold [&_strong]:text-text-primary [&_u]:underline [&_u]:decoration-text-muted [&_u]:underline-offset-2",
5435
5640
  "text-body-2 leading-normal [&>:is(h2,h3)]:text-body-2 [&>:is(h2,h3)]:font-semibold [&>h1]:text-h4",
5436
5641
  "text-caption leading-snug [&>:is(h1,h2,h3)]:mb-0.5 [&>:is(h1,h2,h3)]:mt-1.5 [&>:is(h1,h2,h3)+:is(h1,h2,h3)]:mt-1 [&>:is(h1,h2,h3):last-child]:mb-0 [&>:is(h2,h3)]:text-caption [&>:is(h2,h3)]:font-semibold [&>h1]:text-body-2 [&>h1]:font-semibold"
@@ -6,7 +6,8 @@ import * as BlockNodesStories from './BlockNodes.stories'
6
6
  # BlockNodes
7
7
 
8
8
  The Tiptap nodes an editor of a block document needs beside StarterKit: an id
9
- on every block, a block it cannot edit kept whole, a reference, and a file.
9
+ on every block, a block it cannot edit kept whole, a reference, a file, a
10
+ checklist item and a callout.
10
11
 
11
12
  <Canvas of={BlockNodesStories.Default} />
12
13
 
@@ -34,8 +35,11 @@ import StarterKit from '@tiptap/starter-kit'
34
35
  import {
35
36
  AttachmentNode,
36
37
  BlockId,
38
+ Callout,
39
+ Checklist,
37
40
  ReferenceNode,
38
41
  UnknownBlock,
42
+ isChecklistTick,
39
43
  type AttachmentViewProps,
40
44
  type ReferenceViewProps,
41
45
  } from '@estiva-app/ui/editor'
@@ -50,11 +54,17 @@ const editor = useEditor({
50
54
  extensions: [
51
55
  StarterKit,
52
56
  BlockId,
57
+ Checklist,
58
+ Callout,
53
59
  UnknownBlock,
54
60
  ReferenceNode.configure({ view: Reference }),
55
61
  AttachmentNode.configure({ view: File }),
56
62
  ],
57
63
  content: opened,
64
+ // A tick is a click, so no blur follows it: save it now.
65
+ onUpdate: ({ transaction }) => {
66
+ if (isChecklistTick(transaction)) console.log('save now')
67
+ },
58
68
  })
59
69
 
60
70
  <EditorContent editor={editor} />
@@ -74,9 +84,30 @@ const editor = useEditor({
74
84
  - `UnknownBlock` draws a block the editor has no design for as its text, in a
75
85
  box. It cannot be changed here, only moved or deleted, and it is saved back
76
86
  exactly as it arrived.
87
+ - `Checklist` is a list item with `checked` (§13.3): a box before it that
88
+ ticks with one click. `[ ] ` or `[] ` at the start of a line makes one,
89
+ `[x] ` a ticked one; `/`'s Checklist does too, in a block editor
90
+ (`formatSection('block')`). A change that is a tick says so
91
+ (`isChecklistTick`): save it at once, as no blur follows a click on the box.
92
+ The box is named by its item's words, and ticks only while the editor is
93
+ editable — `setEditable` is followed, not read once. Without `Checklist`, a
94
+ save drops the tick and keeps the item, and `/` does not offer Checklist.
95
+ - `Callout` is a quote with a `tone` (§13.3), drawn on its tone's wash as
96
+ `RichText` draws it. `/` offers Note and Warning in a block editor that has
97
+ it; the other tones are read and kept as written.
77
98
 
78
99
  <Canvas of={BlockNodesStories.WithoutViews} />
79
100
 
101
+ <Canvas of={BlockNodesStories.ChecklistAndCallouts} />
102
+
80
103
  ## What it owns
81
104
 
82
105
  Nothing. It only draws. Clicks and keys are yours.
106
+
107
+ ## Keys
108
+
109
+ | Key | In a checklist item |
110
+ | --- | --- |
111
+ | Enter | Starts the next item, unticked. At the start of the item, its text moves down with its tick. On an empty item, leaves the list. |
112
+ | Backspace | At the start of the item, takes its box away; again, joins it to the item above. |
113
+ | Space after `[ ]`, `[]` or `[x]` | At the start of a line, makes a checklist item (ticked for `[x]`). |
@@ -4,7 +4,7 @@ import StarterKit from '@tiptap/starter-kit'
4
4
  import { AttachmentCard } from './AttachmentCard'
5
5
  import { InlineChip } from './InlineChip'
6
6
  import { richTextClassName } from './RichText'
7
- import { AttachmentNode, BlockId, ReferenceNode, UnknownBlock, type AttachmentViewProps, type ReferenceViewProps } from './BlockNodes'
7
+ import { AttachmentNode, BlockId, Callout, Checklist, ReferenceNode, UnknownBlock, type AttachmentViewProps, type ReferenceViewProps } from './BlockNodes'
8
8
 
9
9
  /**
10
10
  * The nodes an editor of a block document adds to StarterKit: an id on every
@@ -77,3 +77,31 @@ export const Default: Story = {
77
77
  export const WithoutViews: Story = {
78
78
  render: () => <WithAnEditor views={false} />,
79
79
  }
80
+
81
+ const item = (blockId: string, text: string, checked?: boolean) => ({
82
+ type: 'listItem',
83
+ attrs: { blockId, ...(checked === undefined ? {} : { checked }) },
84
+ content: [{ type: 'paragraph', content: [{ type: 'text', text }] }],
85
+ })
86
+
87
+ function WithChecklist() {
88
+ const editor = useEditor({
89
+ extensions: [StarterKit, BlockId, Checklist, Callout],
90
+ content: {
91
+ type: 'doc',
92
+ content: [
93
+ { type: 'bulletList', attrs: { blockId: 'l1' }, content: [item('i1', 'Ticked: click the box', true), item('i2', 'Enter starts the next one unticked', false), item('i3', 'A plain item')] },
94
+ { type: 'paragraph', attrs: { blockId: 'p1' }, content: [{ type: 'text', text: 'Type [ ] and a space at the start of a line for a checklist.' }] },
95
+ { type: 'blockquote', attrs: { blockId: 'q1', tone: 'note' }, content: [{ type: 'paragraph', content: [{ type: 'text', text: 'A note.' }] }] },
96
+ { type: 'blockquote', attrs: { blockId: 'q2', tone: 'warning' }, content: [{ type: 'paragraph', content: [{ type: 'text', text: 'A warning.' }] }] },
97
+ ],
98
+ },
99
+ editorProps: { attributes: { class: richTextClassName('default', 'outline-none'), 'aria-label': 'Text' } },
100
+ })
101
+ return <EditorContent editor={editor} className="max-w-[560px]" />
102
+ }
103
+
104
+ /** A checklist and two callouts, in the editor: the box ticks with one click, as where it is read (RIC-19). */
105
+ export const ChecklistAndCallouts: Story = {
106
+ render: () => <WithChecklist />,
107
+ }
@@ -8,11 +8,12 @@
8
8
  * reads — so each test is a load, an edit or none, and a read back.
9
9
  */
10
10
  import { afterEach, describe, expect, it } from 'vitest'
11
- import { render, screen, cleanup } from '@testing-library/react'
11
+ import { act, render, screen, cleanup, waitFor } from '@testing-library/react'
12
12
  import { Editor } from '@tiptap/core'
13
13
  import { EditorContent, useEditor } from '@tiptap/react'
14
14
  import StarterKit from '@tiptap/starter-kit'
15
- import { AttachmentNode, BlockId, ReferenceNode, UnknownBlock, type ReferenceViewProps } from './BlockNodes'
15
+ import { AttachmentNode, BlockId, Callout, Checklist, ReferenceNode, UnknownBlock, isChecklistTick, type ReferenceViewProps } from './BlockNodes'
16
+ import { applyFormat } from './ComposerTriggers'
16
17
 
17
18
  const EXTENSIONS = [StarterKit, BlockId, UnknownBlock, ReferenceNode, AttachmentNode]
18
19
 
@@ -573,3 +574,141 @@ describe('Tab on a list item — MAN-13', () => {
573
574
  expect(item.content![1].attrs?.blockId ?? null).toBeNull()
574
575
  })
575
576
  })
577
+
578
+ describe('a checklist item and a callout — RIC-19', () => {
579
+ const WITH = [StarterKit, BlockId, Checklist, Callout]
580
+ const press = (key: string) => editor!.view.someProp('handleKeyDown', (f) => f(editor!.view, new KeyboardEvent('keydown', { key })))
581
+ const item = (id: string, text: string, checked?: boolean) => ({
582
+ type: 'listItem',
583
+ attrs: { blockId: id, ...(checked === undefined ? {} : { checked }) },
584
+ content: [{ type: 'paragraph', content: text ? [{ type: 'text', text }] : [] }],
585
+ })
586
+ const CHECKLIST = {
587
+ type: 'doc',
588
+ content: [
589
+ { type: 'bulletList', attrs: { blockId: 'l1' }, content: [item('i1', 'ship', true), item('i2', 'plain')] },
590
+ { type: 'blockquote', attrs: { blockId: 'q1', tone: 'warning' }, content: [{ type: 'paragraph', content: [{ type: 'text', text: 'careful' }] }] },
591
+ ],
592
+ }
593
+ const items = () => ((editor!.getJSON() as Json).content![0].content ?? []).map((n) => [JSON.stringify(n.content).match(/"text":"([^"]*)"/)?.[1] ?? '', n.attrs?.checked ?? null])
594
+ /** The end of "ship": 1 opens the list, 1 the item, 1 its paragraph. */
595
+ const END_OF_SHIP = 3 + 4
596
+
597
+ it('keeps a tick and a tone through a load and a read, and a plain item plain', () => {
598
+ editor = new Editor({ extensions: WITH, content: CHECKLIST })
599
+ expect(items()).toEqual([['ship', true], ['plain', null]])
600
+ expect((editor.getJSON() as Json).content![1].attrs?.tone).toBe('warning')
601
+ expect(editor.getHTML()).toContain('data-callout="warning"')
602
+ })
603
+
604
+ it('starts the next item unticked on Enter, never with the tick it was split from', () => {
605
+ editor = new Editor({ extensions: WITH, content: CHECKLIST })
606
+ editor.commands.setTextSelection(END_OF_SHIP)
607
+ expect(press('Enter')).toBe(true)
608
+ expect(items()).toEqual([['ship', true], ['', false], ['plain', null]])
609
+ })
610
+
611
+ it('starts the next item unticked on Enter over a selection, the selection gone (review of #170)', () => {
612
+ editor = new Editor({ extensions: WITH, content: CHECKLIST })
613
+ editor.commands.setTextSelection({ from: 4, to: 5 })
614
+ expect(press('Enter')).toBe(true)
615
+ expect(items()).toEqual([['s', true], ['ip', false], ['plain', null]])
616
+ })
617
+
618
+ it('moves the text down with its tick on Enter at the start of an item', () => {
619
+ editor = new Editor({ extensions: WITH, content: CHECKLIST })
620
+ editor.commands.setTextSelection(3)
621
+ expect(press('Enter')).toBe(true)
622
+ expect(items()).toEqual([['', false], ['ship', true], ['plain', null]])
623
+ })
624
+
625
+ it('takes the box away first on Backspace at the start of an item', () => {
626
+ editor = new Editor({ extensions: WITH, content: CHECKLIST })
627
+ editor.commands.setTextSelection(3)
628
+ expect(press('Backspace')).toBe(true)
629
+ expect(items()).toEqual([['ship', null], ['plain', null]])
630
+ })
631
+
632
+ /** A space typed after `text`, as a key reaches the input rules. */
633
+ const typeSpaceAfter = (doc: object, at: number) => {
634
+ editor = new Editor({ extensions: WITH, content: doc })
635
+ editor.commands.setTextSelection(at)
636
+ const tr = () => editor!.state.tr.insertText(' ', at, at)
637
+ return editor.view.someProp('handleTextInput', (f) => f(editor!.view, at, at, ' ', tr))
638
+ }
639
+
640
+ it('makes a checklist item from [ ] at the start of a line', () => {
641
+ expect(typeSpaceAfter({ type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: '[ ]' }] }] }, 4)).toBe(true)
642
+ const list = (editor!.getJSON() as Json).content![0]
643
+ expect(list.type).toBe('bulletList')
644
+ expect(list.content![0].attrs?.checked).toBe(false)
645
+ expect(editor!.getText().trim()).toBe('')
646
+ })
647
+
648
+ it('turns a list item into a ticked one from [x]', () => {
649
+ const doc = { type: 'doc', content: [{ type: 'bulletList', content: [item('i1', '[x]')] }] }
650
+ expect(typeSpaceAfter(doc, 6)).toBe(true)
651
+ expect(items()).toEqual([['', true]])
652
+ })
653
+
654
+ it('makes a checklist and a callout from `/`', () => {
655
+ editor = new Editor({ extensions: WITH, content: { type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'todo' }] }, { type: 'paragraph', content: [{ type: 'text', text: 'heads up' }] }] } })
656
+ editor.commands.setTextSelection(2)
657
+ expect(applyFormat(editor, 'checklist')).toBe(true)
658
+ editor.commands.setTextSelection(editor.state.doc.content.size - 2)
659
+ expect(applyFormat(editor, 'note')).toBe(true)
660
+ const [list, quote] = (editor.getJSON() as Json).content!
661
+ expect(list.content![0].attrs?.checked).toBe(false)
662
+ expect(quote.attrs?.tone).toBe('note')
663
+ })
664
+
665
+ it('keeps a tick when `/` Checklist is picked on a ticked item (review of #170)', () => {
666
+ editor = new Editor({ extensions: WITH, content: CHECKLIST })
667
+ editor.commands.setTextSelection(4)
668
+ expect(applyFormat(editor, 'checklist')).toBe(true)
669
+ expect(items()[0]).toEqual(['ship', true])
670
+ })
671
+
672
+ it('leaves `[] ` as typed in an editor with no bullet list (review of #170)', () => {
673
+ editor = new Editor({ extensions: [StarterKit.configure({ bulletList: false }), BlockId, Checklist], content: { type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: '[]' }] }] } })
674
+ editor.commands.setTextSelection(3)
675
+ const tr = () => editor!.state.tr.insertText(' ', 3, 3)
676
+ expect(editor.view.someProp('handleTextInput', (f) => f(editor!.view, 3, 3, ' ', tr))).toBeFalsy()
677
+ expect(editor.getText()).toBe('[]')
678
+ })
679
+
680
+ it('ticks with one click on the box, and says the change was a tick', async () => {
681
+ let ticked = false
682
+ function Checklisted() {
683
+ const e = useEditor({ extensions: WITH, content: CHECKLIST, onUpdate: ({ transaction }) => { ticked ||= isChecklistTick(transaction) } })
684
+ editor = e ?? undefined
685
+ return <EditorContent editor={e} />
686
+ }
687
+ render(<Checklisted />)
688
+ // Named by its item's words (Katerina, review of #170).
689
+ const boxes = await screen.findAllByRole('checkbox', { name: 'ship' })
690
+ expect(boxes).toHaveLength(1)
691
+ expect(boxes[0].getAttribute('aria-checked')).toBe('true')
692
+ boxes[0].click()
693
+ expect(items()[0]).toEqual(['ship', false])
694
+ expect(ticked).toBe(true)
695
+ })
696
+
697
+ it('follows the editor going read-only and back, not only how it was when drawn', async () => {
698
+ function Checklisted() {
699
+ const e = useEditor({ extensions: WITH, content: CHECKLIST })
700
+ editor = e ?? undefined
701
+ return <EditorContent editor={e} />
702
+ }
703
+ const { container } = render(<Checklisted />)
704
+ await screen.findByRole('checkbox', { name: 'ship' })
705
+ const box = () => container.querySelector('[data-checklist-box]')!
706
+ act(() => editor!.setEditable(false))
707
+ // A picture now: the box around it says done, and nothing ticks.
708
+ expect([box().getAttribute('role'), box().getAttribute('aria-checked'), box().getAttribute('aria-readonly')]).toEqual(['checkbox', 'true', 'true'])
709
+ await waitFor(() => expect(box().querySelector('[role=checkbox]')).toBeNull())
710
+ act(() => editor!.setEditable(true))
711
+ expect(box().getAttribute('role')).toBeNull()
712
+ expect(await screen.findByRole('checkbox', { name: 'ship' })).toBeTruthy()
713
+ })
714
+ })