@fragments-sdk/ui 0.10.0 → 0.11.1
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/dist/assets/ui.css +329 -26
- package/dist/blocks/BlogEditor.block.d.ts +3 -0
- package/dist/blocks/BlogEditor.block.d.ts.map +1 -0
- package/dist/codeblock.cjs +37 -10
- package/dist/codeblock.cjs.map +1 -1
- package/dist/codeblock.js +15 -10
- package/dist/codeblock.js.map +1 -1
- package/dist/components/AppShell/AppShell.module.scss.cjs +14 -14
- package/dist/components/AppShell/AppShell.module.scss.js +14 -14
- package/dist/components/CodeBlock/index.d.ts.map +1 -1
- package/dist/components/Drawer/index.cjs +2 -1
- package/dist/components/Drawer/index.cjs.map +1 -1
- package/dist/components/Drawer/index.d.ts +3 -1
- package/dist/components/Drawer/index.d.ts.map +1 -1
- package/dist/components/Drawer/index.js +2 -1
- package/dist/components/Drawer/index.js.map +1 -1
- package/dist/components/Editor/Editor.module.scss.cjs +57 -0
- package/dist/components/Editor/Editor.module.scss.cjs.map +1 -0
- package/dist/components/Editor/Editor.module.scss.js +57 -0
- package/dist/components/Editor/Editor.module.scss.js.map +1 -0
- package/dist/components/Editor/index.cjs +548 -0
- package/dist/components/Editor/index.cjs.map +1 -0
- package/dist/components/Editor/index.d.ts +107 -0
- package/dist/components/Editor/index.d.ts.map +1 -0
- package/dist/components/Editor/index.js +531 -0
- package/dist/components/Editor/index.js.map +1 -0
- package/dist/components/Sidebar/index.cjs +14 -16
- package/dist/components/Sidebar/index.cjs.map +1 -1
- package/dist/components/Sidebar/index.d.ts +4 -6
- package/dist/components/Sidebar/index.d.ts.map +1 -1
- package/dist/components/Sidebar/index.js +14 -16
- package/dist/components/Sidebar/index.js.map +1 -1
- package/dist/index.cjs +22 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/keyboard-shortcuts.cjs +295 -0
- package/dist/utils/keyboard-shortcuts.cjs.map +1 -0
- package/dist/utils/keyboard-shortcuts.d.ts +293 -0
- package/dist/utils/keyboard-shortcuts.d.ts.map +1 -0
- package/dist/utils/keyboard-shortcuts.js +295 -0
- package/dist/utils/keyboard-shortcuts.js.map +1 -0
- package/fragments.json +1 -1
- package/package.json +28 -3
- package/src/blocks/BlogEditor.block.ts +34 -0
- package/src/components/AppShell/AppShell.module.scss +12 -13
- package/src/components/CodeBlock/index.tsx +15 -11
- package/src/components/Drawer/index.tsx +4 -1
- package/src/components/Editor/Editor.fragment.tsx +322 -0
- package/src/components/Editor/Editor.module.scss +333 -0
- package/src/components/Editor/Editor.test.tsx +174 -0
- package/src/components/Editor/index.tsx +815 -0
- package/src/components/Sidebar/index.tsx +16 -22
- package/src/index.ts +43 -0
- package/src/utils/keyboard-shortcuts.test.ts +357 -0
- package/src/utils/keyboard-shortcuts.ts +502 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
3
|
+
|
|
4
|
+
// ============================================
|
|
5
|
+
// Editor Root
|
|
6
|
+
// ============================================
|
|
7
|
+
|
|
8
|
+
.editor {
|
|
9
|
+
@include surface-elevated;
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
min-width: 400px;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
|
|
15
|
+
// Focus ring (Enhancement #7)
|
|
16
|
+
&:focus-within:not([data-disabled]) {
|
|
17
|
+
@include focus-ring;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
&[data-disabled] {
|
|
21
|
+
opacity: 0.5;
|
|
22
|
+
pointer-events: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
&[data-readonly] {
|
|
26
|
+
.contentTextarea {
|
|
27
|
+
cursor: default;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Size variants (Enhancement #2)
|
|
32
|
+
&[data-size='sm'] {
|
|
33
|
+
.content,
|
|
34
|
+
.contentTextarea,
|
|
35
|
+
:global(.tiptap) {
|
|
36
|
+
min-height: 120px;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&[data-size='md'] {
|
|
41
|
+
.content,
|
|
42
|
+
.contentTextarea,
|
|
43
|
+
:global(.tiptap) {
|
|
44
|
+
min-height: 200px;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&[data-size='lg'] {
|
|
49
|
+
.content,
|
|
50
|
+
.contentTextarea,
|
|
51
|
+
:global(.tiptap) {
|
|
52
|
+
min-height: 400px;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================
|
|
58
|
+
// Toolbar
|
|
59
|
+
// ============================================
|
|
60
|
+
|
|
61
|
+
.toolbar {
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
justify-content: space-between;
|
|
65
|
+
flex-wrap: wrap;
|
|
66
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
67
|
+
padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-sm, $fui-padding-item-sm);
|
|
68
|
+
border-bottom: 1px solid var(--fui-border, $fui-border);
|
|
69
|
+
background-color: var(--fui-bg-secondary, $fui-bg-secondary);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================
|
|
73
|
+
// Toolbar Group
|
|
74
|
+
// ============================================
|
|
75
|
+
|
|
76
|
+
.toolbarGroup {
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: var(--fui-space-0-5, $fui-space-0-5);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================
|
|
83
|
+
// Toolbar Button
|
|
84
|
+
// ============================================
|
|
85
|
+
|
|
86
|
+
.toolbarButton {
|
|
87
|
+
@include button-reset;
|
|
88
|
+
@include interactive-base;
|
|
89
|
+
@include touch-target;
|
|
90
|
+
|
|
91
|
+
display: inline-flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
justify-content: center;
|
|
94
|
+
width: 2rem;
|
|
95
|
+
height: 2rem;
|
|
96
|
+
border-radius: var(--fui-radius-md, $fui-radius-md);
|
|
97
|
+
background-color: transparent;
|
|
98
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
99
|
+
|
|
100
|
+
&:hover:not(:disabled) {
|
|
101
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
102
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&:active:not(:disabled) {
|
|
106
|
+
background-color: var(--fui-bg-active, $fui-bg-active);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
svg {
|
|
110
|
+
width: 1rem;
|
|
111
|
+
height: 1rem;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.toolbarButtonActive {
|
|
116
|
+
color: var(--fui-color-accent, $fui-color-accent);
|
|
117
|
+
background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
|
|
118
|
+
|
|
119
|
+
&:hover:not(:disabled) {
|
|
120
|
+
color: var(--fui-color-accent-hover, $fui-color-accent-hover);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============================================
|
|
125
|
+
// Separator
|
|
126
|
+
// ============================================
|
|
127
|
+
|
|
128
|
+
.separator {
|
|
129
|
+
width: 1px;
|
|
130
|
+
height: 1rem;
|
|
131
|
+
background-color: var(--fui-border, $fui-border);
|
|
132
|
+
margin: 0 var(--fui-space-1, $fui-space-1);
|
|
133
|
+
flex-shrink: 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============================================
|
|
137
|
+
// Status Indicator
|
|
138
|
+
// ============================================
|
|
139
|
+
|
|
140
|
+
.statusIndicator {
|
|
141
|
+
@include section-label-text;
|
|
142
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
143
|
+
white-space: nowrap;
|
|
144
|
+
padding: 0 var(--fui-space-2, $fui-space-2);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.statusError {
|
|
148
|
+
color: var(--fui-color-danger, $fui-color-danger);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ============================================
|
|
152
|
+
// Content
|
|
153
|
+
// ============================================
|
|
154
|
+
|
|
155
|
+
.content {
|
|
156
|
+
flex: 1;
|
|
157
|
+
min-height: 200px;
|
|
158
|
+
overflow-y: auto;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.contentRich {
|
|
162
|
+
// TipTap editor styling
|
|
163
|
+
:global(.tiptap) {
|
|
164
|
+
padding: var(--fui-padding-inline-md, $fui-padding-inline-md);
|
|
165
|
+
min-height: 200px;
|
|
166
|
+
outline: none;
|
|
167
|
+
@include text-base;
|
|
168
|
+
line-height: var(--fui-line-height-relaxed, $fui-line-height-relaxed);
|
|
169
|
+
font-size: var(--fui-font-size-base, $fui-font-size-base);
|
|
170
|
+
|
|
171
|
+
&:focus-visible {
|
|
172
|
+
outline: none;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Placeholder
|
|
176
|
+
p.is-editor-empty:first-child::before {
|
|
177
|
+
content: attr(data-placeholder);
|
|
178
|
+
float: left;
|
|
179
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
180
|
+
pointer-events: none;
|
|
181
|
+
height: 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Headings (Enhancement #4)
|
|
185
|
+
h1 {
|
|
186
|
+
font-size: var(--fui-font-size-2xl, $fui-font-size-2xl);
|
|
187
|
+
font-weight: var(--fui-font-weight-semibold, $fui-font-weight-semibold);
|
|
188
|
+
line-height: var(--fui-line-height-tight, $fui-line-height-tight);
|
|
189
|
+
margin: 0 0 var(--fui-space-3, $fui-space-3);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
h2 {
|
|
193
|
+
font-size: var(--fui-font-size-xl, $fui-font-size-xl);
|
|
194
|
+
font-weight: var(--fui-font-weight-semibold, $fui-font-weight-semibold);
|
|
195
|
+
line-height: var(--fui-line-height-tight, $fui-line-height-tight);
|
|
196
|
+
margin: 0 0 var(--fui-space-2, $fui-space-2);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
h3 {
|
|
200
|
+
font-size: var(--fui-font-size-lg, $fui-font-size-lg);
|
|
201
|
+
font-weight: var(--fui-font-weight-semibold, $fui-font-weight-semibold);
|
|
202
|
+
line-height: var(--fui-line-height-tight, $fui-line-height-tight);
|
|
203
|
+
margin: 0 0 var(--fui-space-2, $fui-space-2);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Blockquote (Enhancement #5)
|
|
207
|
+
blockquote {
|
|
208
|
+
border-left: 3px solid var(--fui-color-accent, $fui-color-accent);
|
|
209
|
+
padding-left: var(--fui-space-4, $fui-space-4);
|
|
210
|
+
margin: 0 0 var(--fui-space-2, $fui-space-2);
|
|
211
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
212
|
+
font-style: italic;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Inline code
|
|
216
|
+
code {
|
|
217
|
+
background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
|
|
218
|
+
border-radius: var(--fui-radius-sm, $fui-radius-sm);
|
|
219
|
+
padding: 0.15em 0.3em;
|
|
220
|
+
font-family: var(--fui-font-mono, $fui-font-mono);
|
|
221
|
+
font-size: 0.9em;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Links
|
|
225
|
+
a {
|
|
226
|
+
color: var(--fui-color-accent, $fui-color-accent);
|
|
227
|
+
text-decoration: underline;
|
|
228
|
+
cursor: pointer;
|
|
229
|
+
|
|
230
|
+
&:hover {
|
|
231
|
+
color: var(--fui-color-accent-hover, $fui-color-accent-hover);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Unordered lists
|
|
236
|
+
ul {
|
|
237
|
+
padding-left: var(--fui-space-5, $fui-space-5);
|
|
238
|
+
list-style-type: disc;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Ordered lists (Enhancement #3)
|
|
242
|
+
ol {
|
|
243
|
+
padding-left: var(--fui-space-5, $fui-space-5);
|
|
244
|
+
list-style-type: decimal;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Strikethrough
|
|
248
|
+
s {
|
|
249
|
+
text-decoration: line-through;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Paragraphs
|
|
253
|
+
p {
|
|
254
|
+
margin: 0 0 var(--fui-space-2, $fui-space-2);
|
|
255
|
+
|
|
256
|
+
&:last-child {
|
|
257
|
+
margin-bottom: 0;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.contentTextarea {
|
|
264
|
+
@include button-reset;
|
|
265
|
+
@include text-base;
|
|
266
|
+
|
|
267
|
+
display: block;
|
|
268
|
+
width: 100%;
|
|
269
|
+
height: 100%;
|
|
270
|
+
min-height: 200px;
|
|
271
|
+
padding: var(--fui-padding-inline-md, $fui-padding-inline-md);
|
|
272
|
+
background: transparent;
|
|
273
|
+
border: none;
|
|
274
|
+
resize: none;
|
|
275
|
+
overflow-y: auto;
|
|
276
|
+
line-height: var(--fui-line-height-relaxed, $fui-line-height-relaxed);
|
|
277
|
+
font-size: var(--fui-font-size-base, $fui-font-size-base);
|
|
278
|
+
|
|
279
|
+
&::placeholder {
|
|
280
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
&:focus-visible {
|
|
284
|
+
outline: none;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
&:disabled {
|
|
288
|
+
cursor: not-allowed;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ============================================
|
|
293
|
+
// Status Bar
|
|
294
|
+
// ============================================
|
|
295
|
+
|
|
296
|
+
.statusBar {
|
|
297
|
+
display: flex;
|
|
298
|
+
align-items: center;
|
|
299
|
+
justify-content: space-between;
|
|
300
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
301
|
+
padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-sm, $fui-padding-item-sm);
|
|
302
|
+
border-top: 1px solid var(--fui-border, $fui-border);
|
|
303
|
+
background-color: var(--fui-bg-secondary, $fui-bg-secondary);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.statusBarLeft {
|
|
307
|
+
display: flex;
|
|
308
|
+
align-items: center;
|
|
309
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.statusBarRight {
|
|
313
|
+
display: flex;
|
|
314
|
+
align-items: center;
|
|
315
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
316
|
+
margin-left: auto;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.statusBarItem {
|
|
320
|
+
@include helper-text;
|
|
321
|
+
white-space: nowrap;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// maxLength indicator states (Enhancement #1)
|
|
325
|
+
.statusBarItemWarning {
|
|
326
|
+
color: var(--fui-color-warning, $fui-color-warning);
|
|
327
|
+
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.statusBarItemError {
|
|
331
|
+
color: var(--fui-color-danger, $fui-color-danger);
|
|
332
|
+
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
333
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen, userEvent, expectNoA11yViolations } from '../../test/utils';
|
|
3
|
+
import { Editor } from './index';
|
|
4
|
+
import { Button } from '../Button';
|
|
5
|
+
|
|
6
|
+
function renderEditor(
|
|
7
|
+
props: {
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
defaultValue?: string;
|
|
12
|
+
onValueChange?: (v: string) => void;
|
|
13
|
+
formats?: ('bold' | 'italic' | 'strikethrough' | 'link' | 'code' | 'bulletList')[];
|
|
14
|
+
} = {},
|
|
15
|
+
) {
|
|
16
|
+
return render(
|
|
17
|
+
<Editor
|
|
18
|
+
placeholder={props.placeholder ?? 'Start typing your masterpiece here...'}
|
|
19
|
+
onValueChange={props.onValueChange}
|
|
20
|
+
disabled={props.disabled}
|
|
21
|
+
readOnly={props.readOnly}
|
|
22
|
+
defaultValue={props.defaultValue}
|
|
23
|
+
formats={props.formats}
|
|
24
|
+
>
|
|
25
|
+
<Editor.Toolbar>
|
|
26
|
+
<Editor.ToolbarGroup aria-label="Text formatting">
|
|
27
|
+
{(props.formats ?? ['bold', 'italic', 'code']).map((f) => (
|
|
28
|
+
<Editor.ToolbarButton key={f} format={f} />
|
|
29
|
+
))}
|
|
30
|
+
</Editor.ToolbarGroup>
|
|
31
|
+
<Editor.ToolbarGroup aria-label="Actions">
|
|
32
|
+
<Editor.StatusIndicator status="saved" />
|
|
33
|
+
<Button variant="accent" size="sm">
|
|
34
|
+
Publish
|
|
35
|
+
</Button>
|
|
36
|
+
</Editor.ToolbarGroup>
|
|
37
|
+
</Editor.Toolbar>
|
|
38
|
+
<Editor.Content />
|
|
39
|
+
<Editor.StatusBar showWordCount showCharCount />
|
|
40
|
+
</Editor>,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Find the editor content area. TipTap renders a contenteditable div,
|
|
46
|
+
* the fallback renders a textarea. Both are accessible as textbox role.
|
|
47
|
+
*/
|
|
48
|
+
function getEditorInput() {
|
|
49
|
+
// Try textarea first (markdown fallback), then contenteditable (TipTap)
|
|
50
|
+
const textarea = document.querySelector('textarea');
|
|
51
|
+
if (textarea) return textarea;
|
|
52
|
+
const contenteditable = document.querySelector('[contenteditable]');
|
|
53
|
+
if (contenteditable) return contenteditable as HTMLElement;
|
|
54
|
+
throw new Error('Could not find editor content area');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('Editor', () => {
|
|
58
|
+
it('renders editor content area with placeholder', () => {
|
|
59
|
+
renderEditor({ placeholder: 'Write here...' });
|
|
60
|
+
// TipTap stores placeholder on wrapper via data-placeholder;
|
|
61
|
+
// textarea uses native placeholder. Check either.
|
|
62
|
+
const wrapper = document.querySelector('[data-placeholder="Write here..."]');
|
|
63
|
+
const textarea = document.querySelector('textarea[placeholder="Write here..."]');
|
|
64
|
+
expect(wrapper || textarea).toBeTruthy();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('renders toolbar with format buttons', () => {
|
|
68
|
+
renderEditor({ formats: ['bold', 'italic', 'code'] });
|
|
69
|
+
expect(screen.getByRole('button', { name: /bold/i })).toBeInTheDocument();
|
|
70
|
+
expect(screen.getByRole('button', { name: /italic/i })).toBeInTheDocument();
|
|
71
|
+
expect(screen.getByRole('button', { name: /code/i })).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('renders toolbar with proper ARIA role', () => {
|
|
75
|
+
renderEditor();
|
|
76
|
+
expect(screen.getByRole('toolbar')).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('renders toolbar groups with role="group"', () => {
|
|
80
|
+
renderEditor();
|
|
81
|
+
const groups = screen.getAllByRole('group');
|
|
82
|
+
expect(groups.length).toBeGreaterThanOrEqual(2);
|
|
83
|
+
expect(groups[0]).toHaveAttribute('aria-label', 'Text formatting');
|
|
84
|
+
expect(groups[1]).toHaveAttribute('aria-label', 'Actions');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('renders status indicator with AUTO-SAVED text', () => {
|
|
88
|
+
renderEditor();
|
|
89
|
+
expect(screen.getByText('AUTO-SAVED')).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('renders status indicator with aria-live="polite"', () => {
|
|
93
|
+
renderEditor();
|
|
94
|
+
const indicator = screen.getByRole('status');
|
|
95
|
+
expect(indicator).toHaveAttribute('aria-live', 'polite');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('renders Publish button', () => {
|
|
99
|
+
renderEditor();
|
|
100
|
+
expect(screen.getByRole('button', { name: /publish/i })).toBeInTheDocument();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('renders status bar with word and character counts', () => {
|
|
104
|
+
renderEditor({ defaultValue: 'Hello world' });
|
|
105
|
+
expect(screen.getByText('2 Words')).toBeInTheDocument();
|
|
106
|
+
expect(screen.getByText('11 Characters')).toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('shows singular "Word" for count of 1', () => {
|
|
110
|
+
renderEditor({ defaultValue: 'Hello' });
|
|
111
|
+
expect(screen.getByText('1 Word')).toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('shows 0 Words when empty', () => {
|
|
115
|
+
renderEditor({ defaultValue: '' });
|
|
116
|
+
expect(screen.getByText('0 Words')).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('toolbar buttons have aria-pressed attribute', () => {
|
|
120
|
+
renderEditor({ formats: ['bold'] });
|
|
121
|
+
const boldBtn = screen.getByRole('button', { name: /bold/i });
|
|
122
|
+
expect(boldBtn).toHaveAttribute('aria-pressed', 'false');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('disables toolbar buttons when editor is disabled', () => {
|
|
126
|
+
renderEditor({ disabled: true, formats: ['bold', 'italic'] });
|
|
127
|
+
expect(screen.getByRole('button', { name: /bold/i })).toBeDisabled();
|
|
128
|
+
expect(screen.getByRole('button', { name: /italic/i })).toBeDisabled();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('sets data-disabled on root when disabled', () => {
|
|
132
|
+
const { container } = renderEditor({ disabled: true });
|
|
133
|
+
const root = container.firstElementChild;
|
|
134
|
+
expect(root).toHaveAttribute('data-disabled');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('sets contenteditable to false or disables textarea when disabled', () => {
|
|
138
|
+
renderEditor({ disabled: true });
|
|
139
|
+
const input = getEditorInput();
|
|
140
|
+
if (input instanceof HTMLTextAreaElement) {
|
|
141
|
+
expect(input).toBeDisabled();
|
|
142
|
+
} else {
|
|
143
|
+
expect(input).toHaveAttribute('contenteditable', 'false');
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('sets data-readonly on root when readOnly', () => {
|
|
148
|
+
const { container } = renderEditor({ readOnly: true });
|
|
149
|
+
const root = container.firstElementChild;
|
|
150
|
+
expect(root).toHaveAttribute('data-readonly');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('disables toolbar buttons when readOnly', () => {
|
|
154
|
+
renderEditor({ readOnly: true, formats: ['bold'] });
|
|
155
|
+
expect(screen.getByRole('button', { name: /bold/i })).toBeDisabled();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('renders with default toolbar and status bar when no children', () => {
|
|
159
|
+
render(
|
|
160
|
+
<Editor placeholder="Auto layout" formats={['bold', 'italic']} />,
|
|
161
|
+
);
|
|
162
|
+
expect(screen.getByRole('toolbar')).toBeInTheDocument();
|
|
163
|
+
// Check placeholder via wrapper or native textarea
|
|
164
|
+
const wrapper = document.querySelector('[data-placeholder="Auto layout"]');
|
|
165
|
+
const textarea = document.querySelector('textarea[placeholder="Auto layout"]');
|
|
166
|
+
expect(wrapper || textarea).toBeTruthy();
|
|
167
|
+
expect(screen.getByText('0 Words')).toBeInTheDocument();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('has no accessibility violations', async () => {
|
|
171
|
+
const { container } = renderEditor();
|
|
172
|
+
await expectNoA11yViolations(container);
|
|
173
|
+
});
|
|
174
|
+
});
|