@furystack/shades-common-components 12.5.0 → 12.7.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 +94 -0
- package/esm/components/data-grid/data-grid.d.ts +7 -1
- package/esm/components/data-grid/data-grid.d.ts.map +1 -1
- package/esm/components/data-grid/data-grid.js +1 -1
- package/esm/components/data-grid/data-grid.js.map +1 -1
- package/esm/components/data-grid/footer.d.ts +1 -0
- package/esm/components/data-grid/footer.d.ts.map +1 -1
- package/esm/components/data-grid/footer.js +8 -15
- package/esm/components/data-grid/footer.js.map +1 -1
- package/esm/components/data-grid/footer.spec.js +85 -47
- package/esm/components/data-grid/footer.spec.js.map +1 -1
- package/esm/components/grid.d.ts +3 -0
- package/esm/components/grid.d.ts.map +1 -1
- package/esm/components/grid.js +3 -0
- package/esm/components/grid.js.map +1 -1
- package/esm/components/inputs/autocomplete.d.ts +3 -0
- package/esm/components/inputs/autocomplete.d.ts.map +1 -1
- package/esm/components/inputs/autocomplete.js +3 -0
- package/esm/components/inputs/autocomplete.js.map +1 -1
- package/esm/components/list/list.d.ts +10 -0
- package/esm/components/list/list.d.ts.map +1 -1
- package/esm/components/list/list.js +23 -2
- package/esm/components/list/list.js.map +1 -1
- package/esm/components/list/list.spec.js +101 -0
- package/esm/components/list/list.spec.js.map +1 -1
- package/esm/components/markdown/markdown-editor.d.ts +16 -2
- package/esm/components/markdown/markdown-editor.d.ts.map +1 -1
- package/esm/components/markdown/markdown-editor.js +42 -8
- package/esm/components/markdown/markdown-editor.js.map +1 -1
- package/esm/components/markdown/markdown-editor.spec.js +190 -0
- package/esm/components/markdown/markdown-editor.spec.js.map +1 -1
- package/esm/components/markdown/markdown-input.d.ts +16 -0
- package/esm/components/markdown/markdown-input.d.ts.map +1 -1
- package/esm/components/markdown/markdown-input.js +44 -3
- package/esm/components/markdown/markdown-input.js.map +1 -1
- package/esm/components/markdown/markdown-input.spec.js +140 -0
- package/esm/components/markdown/markdown-input.spec.js.map +1 -1
- package/esm/components/markdown/markdown-validation.d.ts +25 -0
- package/esm/components/markdown/markdown-validation.d.ts.map +1 -0
- package/esm/components/markdown/markdown-validation.js +15 -0
- package/esm/components/markdown/markdown-validation.js.map +1 -0
- package/esm/components/suggest/index.d.ts +10 -2
- package/esm/components/suggest/index.d.ts.map +1 -1
- package/esm/components/suggest/index.js +21 -1
- package/esm/components/suggest/index.js.map +1 -1
- package/esm/components/suggest/index.spec.js +50 -0
- package/esm/components/suggest/index.spec.js.map +1 -1
- package/esm/components/wizard/index.d.ts +8 -0
- package/esm/components/wizard/index.d.ts.map +1 -1
- package/esm/components/wizard/index.js +90 -0
- package/esm/components/wizard/index.js.map +1 -1
- package/esm/components/wizard/index.spec.js +79 -2
- package/esm/components/wizard/index.spec.js.map +1 -1
- package/package.json +3 -3
- package/src/components/data-grid/data-grid.tsx +13 -2
- package/src/components/data-grid/footer.spec.tsx +104 -50
- package/src/components/data-grid/footer.tsx +25 -31
- package/src/components/grid.tsx +3 -0
- package/src/components/inputs/autocomplete.tsx +3 -0
- package/src/components/list/list.spec.tsx +173 -0
- package/src/components/list/list.tsx +56 -19
- package/src/components/markdown/markdown-editor.spec.tsx +261 -0
- package/src/components/markdown/markdown-editor.tsx +63 -10
- package/src/components/markdown/markdown-input.spec.tsx +205 -0
- package/src/components/markdown/markdown-input.tsx +61 -2
- package/src/components/markdown/markdown-validation.ts +33 -0
- package/src/components/suggest/index.spec.tsx +83 -0
- package/src/components/suggest/index.tsx +36 -3
- package/src/components/wizard/index.spec.tsx +118 -1
- package/src/components/wizard/index.tsx +125 -0
|
@@ -139,4 +139,265 @@ describe('MarkdownEditor', () => {
|
|
|
139
139
|
expect(heading?.textContent).toContain('Test Content')
|
|
140
140
|
})
|
|
141
141
|
})
|
|
142
|
+
|
|
143
|
+
describe('form integration', () => {
|
|
144
|
+
it('should render a label when labelTitle is provided', async () => {
|
|
145
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
146
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
147
|
+
|
|
148
|
+
initializeShadeRoot({
|
|
149
|
+
injector,
|
|
150
|
+
rootElement,
|
|
151
|
+
jsxElement: <MarkdownEditor value="" labelTitle="Description" />,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
await flushUpdates()
|
|
155
|
+
|
|
156
|
+
const label = document.querySelector('shade-markdown-editor .md-editor-label')
|
|
157
|
+
expect(label).not.toBeNull()
|
|
158
|
+
expect(label?.textContent).toBe('Description')
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should not render a label when labelTitle is not provided', async () => {
|
|
163
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
164
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
165
|
+
|
|
166
|
+
initializeShadeRoot({
|
|
167
|
+
injector,
|
|
168
|
+
rootElement,
|
|
169
|
+
jsxElement: <MarkdownEditor value="" />,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
await flushUpdates()
|
|
173
|
+
|
|
174
|
+
const label = document.querySelector('shade-markdown-editor .md-editor-label')
|
|
175
|
+
expect(label).toBeNull()
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should set data-invalid when required and value is empty', async () => {
|
|
180
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
181
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
182
|
+
|
|
183
|
+
initializeShadeRoot({
|
|
184
|
+
injector,
|
|
185
|
+
rootElement,
|
|
186
|
+
jsxElement: <MarkdownEditor value="" required />,
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
await flushUpdates()
|
|
190
|
+
|
|
191
|
+
const editor = document.querySelector('shade-markdown-editor') as HTMLElement
|
|
192
|
+
expect(editor.hasAttribute('data-invalid')).toBe(true)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should not set data-invalid when required and value is provided', async () => {
|
|
197
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
198
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
199
|
+
|
|
200
|
+
initializeShadeRoot({
|
|
201
|
+
injector,
|
|
202
|
+
rootElement,
|
|
203
|
+
jsxElement: <MarkdownEditor value="some content" required />,
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
await flushUpdates()
|
|
207
|
+
|
|
208
|
+
const editor = document.querySelector('shade-markdown-editor') as HTMLElement
|
|
209
|
+
expect(editor.hasAttribute('data-invalid')).toBe(false)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('should show "Value is required" helper text when required and empty', async () => {
|
|
214
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
215
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
216
|
+
|
|
217
|
+
initializeShadeRoot({
|
|
218
|
+
injector,
|
|
219
|
+
rootElement,
|
|
220
|
+
jsxElement: <MarkdownEditor value="" required />,
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
await flushUpdates()
|
|
224
|
+
|
|
225
|
+
const helperText = document.querySelector('shade-markdown-editor .md-editor-helperText')
|
|
226
|
+
expect(helperText).not.toBeNull()
|
|
227
|
+
expect(helperText?.textContent).toBe('Value is required')
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should set data-invalid when getValidationResult returns invalid', async () => {
|
|
232
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
233
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
234
|
+
|
|
235
|
+
initializeShadeRoot({
|
|
236
|
+
injector,
|
|
237
|
+
rootElement,
|
|
238
|
+
jsxElement: (
|
|
239
|
+
<MarkdownEditor
|
|
240
|
+
value="short"
|
|
241
|
+
getValidationResult={({ value }) =>
|
|
242
|
+
value.length < 10 ? { isValid: false, message: 'Too short' } : { isValid: true }
|
|
243
|
+
}
|
|
244
|
+
/>
|
|
245
|
+
),
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
await flushUpdates()
|
|
249
|
+
|
|
250
|
+
const editor = document.querySelector('shade-markdown-editor') as HTMLElement
|
|
251
|
+
expect(editor.hasAttribute('data-invalid')).toBe(true)
|
|
252
|
+
expect(editor.textContent).toContain('Too short')
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('should not set data-invalid when getValidationResult returns valid', async () => {
|
|
257
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
258
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
259
|
+
|
|
260
|
+
initializeShadeRoot({
|
|
261
|
+
injector,
|
|
262
|
+
rootElement,
|
|
263
|
+
jsxElement: (
|
|
264
|
+
<MarkdownEditor
|
|
265
|
+
value="long enough content"
|
|
266
|
+
getValidationResult={({ value }) =>
|
|
267
|
+
value.length < 10 ? { isValid: false, message: 'Too short' } : { isValid: true }
|
|
268
|
+
}
|
|
269
|
+
/>
|
|
270
|
+
),
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
await flushUpdates()
|
|
274
|
+
|
|
275
|
+
const editor = document.querySelector('shade-markdown-editor') as HTMLElement
|
|
276
|
+
expect(editor.hasAttribute('data-invalid')).toBe(false)
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('should display helper text from getHelperText', async () => {
|
|
281
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
282
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
283
|
+
|
|
284
|
+
initializeShadeRoot({
|
|
285
|
+
injector,
|
|
286
|
+
rootElement,
|
|
287
|
+
jsxElement: <MarkdownEditor value="" getHelperText={() => 'Enter your description'} />,
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
await flushUpdates()
|
|
291
|
+
|
|
292
|
+
const helperText = document.querySelector('shade-markdown-editor .md-editor-helperText')
|
|
293
|
+
expect(helperText).not.toBeNull()
|
|
294
|
+
expect(helperText?.textContent).toBe('Enter your description')
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('should forward name prop to the inner textarea', async () => {
|
|
299
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
300
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
301
|
+
|
|
302
|
+
initializeShadeRoot({
|
|
303
|
+
injector,
|
|
304
|
+
rootElement,
|
|
305
|
+
jsxElement: <MarkdownEditor value="" name="description" />,
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
await flushUpdates()
|
|
309
|
+
|
|
310
|
+
const textarea = document.querySelector('shade-markdown-editor textarea') as HTMLTextAreaElement
|
|
311
|
+
expect(textarea.name).toBe('description')
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('should forward required prop to the inner textarea', async () => {
|
|
316
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
317
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
318
|
+
|
|
319
|
+
initializeShadeRoot({
|
|
320
|
+
injector,
|
|
321
|
+
rootElement,
|
|
322
|
+
jsxElement: <MarkdownEditor value="content" required />,
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
await flushUpdates()
|
|
326
|
+
|
|
327
|
+
const textarea = document.querySelector('shade-markdown-editor textarea') as HTMLTextAreaElement
|
|
328
|
+
expect(textarea.required).toBe(true)
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('should forward disabled prop to the inner textarea', async () => {
|
|
333
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
334
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
335
|
+
|
|
336
|
+
initializeShadeRoot({
|
|
337
|
+
injector,
|
|
338
|
+
rootElement,
|
|
339
|
+
jsxElement: <MarkdownEditor value="" disabled />,
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
await flushUpdates()
|
|
343
|
+
|
|
344
|
+
const textarea = document.querySelector('shade-markdown-editor textarea') as HTMLTextAreaElement
|
|
345
|
+
expect(textarea.disabled).toBe(true)
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('should forward placeholder prop to the inner textarea', async () => {
|
|
350
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
351
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
352
|
+
|
|
353
|
+
initializeShadeRoot({
|
|
354
|
+
injector,
|
|
355
|
+
rootElement,
|
|
356
|
+
jsxElement: <MarkdownEditor value="" placeholder="Type here..." />,
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
await flushUpdates()
|
|
360
|
+
|
|
361
|
+
const textarea = document.querySelector('shade-markdown-editor textarea') as HTMLTextAreaElement
|
|
362
|
+
expect(textarea.placeholder).toBe('Type here...')
|
|
363
|
+
})
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('should forward rows prop to the inner textarea', async () => {
|
|
367
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
368
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
369
|
+
|
|
370
|
+
initializeShadeRoot({
|
|
371
|
+
injector,
|
|
372
|
+
rootElement,
|
|
373
|
+
jsxElement: <MarkdownEditor value="" rows={5} />,
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
await flushUpdates()
|
|
377
|
+
|
|
378
|
+
const textarea = document.querySelector('shade-markdown-editor textarea') as HTMLTextAreaElement
|
|
379
|
+
expect(textarea.rows).toBe(5)
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it('should set hideChrome on the inner MarkdownInput', async () => {
|
|
384
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
385
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
386
|
+
|
|
387
|
+
initializeShadeRoot({
|
|
388
|
+
injector,
|
|
389
|
+
rootElement,
|
|
390
|
+
jsxElement: <MarkdownEditor value="" labelTitle="My Label" />,
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
await flushUpdates()
|
|
394
|
+
|
|
395
|
+
const editorLabel = document.querySelector('shade-markdown-editor .md-editor-label')
|
|
396
|
+
expect(editorLabel?.textContent).toBe('My Label')
|
|
397
|
+
|
|
398
|
+
const inputLabel = document.querySelector('shade-markdown-editor shade-markdown-input label > span')
|
|
399
|
+
expect(inputLabel).toBeNull()
|
|
400
|
+
})
|
|
401
|
+
})
|
|
402
|
+
})
|
|
142
403
|
})
|
|
@@ -2,7 +2,8 @@ import { Shade, createComponent } from '@furystack/shades'
|
|
|
2
2
|
import { cssVariableTheme } from '../../services/css-variable-theme.js'
|
|
3
3
|
import { Tabs } from '../tabs.js'
|
|
4
4
|
import { MarkdownDisplay } from './markdown-display.js'
|
|
5
|
-
import { MarkdownInput } from './markdown-input.js'
|
|
5
|
+
import { MarkdownInput, type MarkdownInputProps } from './markdown-input.js'
|
|
6
|
+
import { resolveValidationState } from './markdown-validation.js'
|
|
6
7
|
|
|
7
8
|
export type MarkdownEditorLayout = 'side-by-side' | 'tabs' | 'above-below'
|
|
8
9
|
|
|
@@ -19,7 +20,10 @@ export type MarkdownEditorProps = {
|
|
|
19
20
|
readOnly?: boolean
|
|
20
21
|
/** Inline styles applied to the host element */
|
|
21
22
|
style?: Partial<CSSStyleDeclaration>
|
|
22
|
-
}
|
|
23
|
+
} & Pick<
|
|
24
|
+
MarkdownInputProps,
|
|
25
|
+
'name' | 'required' | 'labelTitle' | 'disabled' | 'placeholder' | 'rows' | 'getValidationResult' | 'getHelperText'
|
|
26
|
+
>
|
|
23
27
|
|
|
24
28
|
type TabType = 'edit' | 'preview'
|
|
25
29
|
|
|
@@ -32,11 +36,40 @@ export const MarkdownEditor = Shade<MarkdownEditorProps>({
|
|
|
32
36
|
css: {
|
|
33
37
|
display: 'flex',
|
|
34
38
|
flexDirection: 'column',
|
|
35
|
-
border: `1px solid ${cssVariableTheme.action.subtleBorder}`,
|
|
36
|
-
borderRadius: cssVariableTheme.shape.borderRadius.md,
|
|
37
|
-
overflow: 'hidden',
|
|
38
39
|
minHeight: '0',
|
|
39
40
|
|
|
41
|
+
'& .md-editor-label': {
|
|
42
|
+
fontSize: cssVariableTheme.typography.fontSize.xs,
|
|
43
|
+
color: cssVariableTheme.text.secondary,
|
|
44
|
+
padding: `0 0 ${cssVariableTheme.spacing.sm} 0`,
|
|
45
|
+
transition: `color ${cssVariableTheme.transitions.duration.slow} ${cssVariableTheme.transitions.easing.default}`,
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
'&[data-invalid] .md-editor-label': {
|
|
49
|
+
color: cssVariableTheme.palette.error.main,
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
'& .md-editor-frame': {
|
|
53
|
+
display: 'flex',
|
|
54
|
+
flexDirection: 'column',
|
|
55
|
+
border: `1px solid ${cssVariableTheme.action.subtleBorder}`,
|
|
56
|
+
borderRadius: cssVariableTheme.shape.borderRadius.md,
|
|
57
|
+
overflow: 'hidden',
|
|
58
|
+
flex: '1',
|
|
59
|
+
minHeight: '0',
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
'&[data-invalid] .md-editor-frame': {
|
|
63
|
+
borderColor: cssVariableTheme.palette.error.main,
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
'& .md-editor-helperText': {
|
|
67
|
+
fontSize: cssVariableTheme.typography.fontSize.xs,
|
|
68
|
+
padding: `${cssVariableTheme.spacing.sm} 0 0 0`,
|
|
69
|
+
opacity: '0.85',
|
|
70
|
+
lineHeight: '1.4',
|
|
71
|
+
},
|
|
72
|
+
|
|
40
73
|
'& .md-editor-split': {
|
|
41
74
|
display: 'flex',
|
|
42
75
|
flex: '1',
|
|
@@ -119,8 +152,11 @@ export const MarkdownEditor = Shade<MarkdownEditorProps>({
|
|
|
119
152
|
render: ({ props, useState, useHostProps }) => {
|
|
120
153
|
const layout = props.layout ?? 'side-by-side'
|
|
121
154
|
|
|
155
|
+
const { isInvalid, helperNode } = resolveValidationState(props)
|
|
156
|
+
|
|
122
157
|
useHostProps({
|
|
123
158
|
...(props.style ? { style: props.style as Record<string, string> } : {}),
|
|
159
|
+
'data-invalid': isInvalid ? '' : undefined,
|
|
124
160
|
})
|
|
125
161
|
|
|
126
162
|
const [activeTab, setActiveTab] = useState<TabType>('activeTab', 'edit')
|
|
@@ -131,13 +167,22 @@ export const MarkdownEditor = Shade<MarkdownEditorProps>({
|
|
|
131
167
|
onValueChange={props.onValueChange}
|
|
132
168
|
maxImageSizeBytes={props.maxImageSizeBytes}
|
|
133
169
|
readOnly={props.readOnly}
|
|
170
|
+
name={props.name}
|
|
171
|
+
required={props.required}
|
|
172
|
+
disabled={props.disabled}
|
|
173
|
+
placeholder={props.placeholder}
|
|
174
|
+
rows={props.rows}
|
|
175
|
+
getValidationResult={props.getValidationResult}
|
|
176
|
+
hideChrome
|
|
134
177
|
/>
|
|
135
178
|
)
|
|
136
179
|
|
|
137
180
|
const previewPane = <MarkdownDisplay content={props.value} readOnly={false} onChange={props.onValueChange} />
|
|
138
181
|
|
|
182
|
+
let content: JSX.Element
|
|
183
|
+
|
|
139
184
|
if (layout === 'tabs') {
|
|
140
|
-
|
|
185
|
+
content = (
|
|
141
186
|
<Tabs
|
|
142
187
|
activeKey={activeTab}
|
|
143
188
|
onTabChange={(key) => setActiveTab(key as TabType)}
|
|
@@ -155,13 +200,21 @@ export const MarkdownEditor = Shade<MarkdownEditorProps>({
|
|
|
155
200
|
]}
|
|
156
201
|
/>
|
|
157
202
|
)
|
|
203
|
+
} else {
|
|
204
|
+
content = (
|
|
205
|
+
<div className="md-editor-split" data-layout={layout}>
|
|
206
|
+
<div className="md-editor-pane md-editor-pane-input">{inputPane}</div>
|
|
207
|
+
<div className="md-editor-pane md-editor-pane-preview">{previewPane}</div>
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
158
210
|
}
|
|
159
211
|
|
|
160
212
|
return (
|
|
161
|
-
|
|
162
|
-
<
|
|
163
|
-
<div className="md-editor-
|
|
164
|
-
|
|
213
|
+
<>
|
|
214
|
+
{props.labelTitle ? <span className="md-editor-label">{props.labelTitle}</span> : null}
|
|
215
|
+
<div className="md-editor-frame">{content}</div>
|
|
216
|
+
{helperNode ? <span className="md-editor-helperText">{helperNode}</span> : null}
|
|
217
|
+
</>
|
|
165
218
|
)
|
|
166
219
|
},
|
|
167
220
|
})
|
|
@@ -2,6 +2,7 @@ import { Injector } from '@furystack/inject'
|
|
|
2
2
|
import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades'
|
|
3
3
|
import { usingAsync } from '@furystack/utils'
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { Form } from '../form.js'
|
|
5
6
|
import { MarkdownInput } from './markdown-input.js'
|
|
6
7
|
|
|
7
8
|
describe('MarkdownInput', () => {
|
|
@@ -160,6 +161,210 @@ describe('MarkdownInput', () => {
|
|
|
160
161
|
})
|
|
161
162
|
})
|
|
162
163
|
|
|
164
|
+
it('should set name attribute on textarea', async () => {
|
|
165
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
166
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
167
|
+
|
|
168
|
+
initializeShadeRoot({
|
|
169
|
+
injector,
|
|
170
|
+
rootElement,
|
|
171
|
+
jsxElement: <MarkdownInput value="" name="description" />,
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
await flushUpdates()
|
|
175
|
+
|
|
176
|
+
const textarea = document.querySelector('shade-markdown-input textarea') as HTMLTextAreaElement
|
|
177
|
+
expect(textarea.name).toBe('description')
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('should set required attribute on textarea', async () => {
|
|
182
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
183
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
184
|
+
|
|
185
|
+
initializeShadeRoot({
|
|
186
|
+
injector,
|
|
187
|
+
rootElement,
|
|
188
|
+
jsxElement: <MarkdownInput value="" required />,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
await flushUpdates()
|
|
192
|
+
|
|
193
|
+
const textarea = document.querySelector('shade-markdown-input textarea') as HTMLTextAreaElement
|
|
194
|
+
expect(textarea.required).toBe(true)
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('should set data-invalid when required and value is empty', async () => {
|
|
199
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
200
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
201
|
+
|
|
202
|
+
initializeShadeRoot({
|
|
203
|
+
injector,
|
|
204
|
+
rootElement,
|
|
205
|
+
jsxElement: <MarkdownInput value="" required />,
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
await flushUpdates()
|
|
209
|
+
|
|
210
|
+
const wrapper = document.querySelector('shade-markdown-input') as HTMLElement
|
|
211
|
+
expect(wrapper.hasAttribute('data-invalid')).toBe(true)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should not set data-invalid when required and value is provided', async () => {
|
|
216
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
217
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
218
|
+
|
|
219
|
+
initializeShadeRoot({
|
|
220
|
+
injector,
|
|
221
|
+
rootElement,
|
|
222
|
+
jsxElement: <MarkdownInput value="some content" required />,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
await flushUpdates()
|
|
226
|
+
|
|
227
|
+
const wrapper = document.querySelector('shade-markdown-input') as HTMLElement
|
|
228
|
+
expect(wrapper.hasAttribute('data-invalid')).toBe(false)
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should show validation error message from getValidationResult', async () => {
|
|
233
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
234
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
235
|
+
|
|
236
|
+
initializeShadeRoot({
|
|
237
|
+
injector,
|
|
238
|
+
rootElement,
|
|
239
|
+
jsxElement: (
|
|
240
|
+
<MarkdownInput
|
|
241
|
+
value="short"
|
|
242
|
+
getValidationResult={({ value }) =>
|
|
243
|
+
value.length < 10 ? { isValid: false, message: 'Too short' } : { isValid: true }
|
|
244
|
+
}
|
|
245
|
+
/>
|
|
246
|
+
),
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
await flushUpdates()
|
|
250
|
+
|
|
251
|
+
const wrapper = document.querySelector('shade-markdown-input') as HTMLElement
|
|
252
|
+
expect(wrapper.hasAttribute('data-invalid')).toBe(true)
|
|
253
|
+
expect(wrapper.textContent).toContain('Too short')
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('should show helper text from getHelperText', async () => {
|
|
258
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
259
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
260
|
+
|
|
261
|
+
initializeShadeRoot({
|
|
262
|
+
injector,
|
|
263
|
+
rootElement,
|
|
264
|
+
jsxElement: <MarkdownInput value="" getHelperText={() => 'Write some markdown here'} />,
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
await flushUpdates()
|
|
268
|
+
|
|
269
|
+
const wrapper = document.querySelector('shade-markdown-input') as HTMLElement
|
|
270
|
+
expect(wrapper.textContent).toContain('Write some markdown here')
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
describe('hideChrome', () => {
|
|
275
|
+
it('should suppress the label when hideChrome is true', async () => {
|
|
276
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
277
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
278
|
+
|
|
279
|
+
initializeShadeRoot({
|
|
280
|
+
injector,
|
|
281
|
+
rootElement,
|
|
282
|
+
jsxElement: <MarkdownInput value="" labelTitle="My Label" hideChrome />,
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
await flushUpdates()
|
|
286
|
+
|
|
287
|
+
const wrapper = document.querySelector('shade-markdown-input') as HTMLElement
|
|
288
|
+
const spans = wrapper.querySelectorAll('label > span')
|
|
289
|
+
const labelSpan = Array.from(spans).find((s) => s.textContent === 'My Label')
|
|
290
|
+
expect(labelSpan).toBeUndefined()
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('should suppress the helper text when hideChrome is true', async () => {
|
|
295
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
296
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
297
|
+
|
|
298
|
+
initializeShadeRoot({
|
|
299
|
+
injector,
|
|
300
|
+
rootElement,
|
|
301
|
+
jsxElement: (
|
|
302
|
+
<MarkdownInput
|
|
303
|
+
value="short"
|
|
304
|
+
hideChrome
|
|
305
|
+
getValidationResult={({ value }) =>
|
|
306
|
+
value.length < 10 ? { isValid: false, message: 'Too short' } : { isValid: true }
|
|
307
|
+
}
|
|
308
|
+
/>
|
|
309
|
+
),
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
await flushUpdates()
|
|
313
|
+
|
|
314
|
+
const helperText = document.querySelector('shade-markdown-input .helperText')
|
|
315
|
+
expect(helperText).toBeNull()
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('should still set data-invalid when hideChrome is true', async () => {
|
|
320
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
321
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
322
|
+
|
|
323
|
+
initializeShadeRoot({
|
|
324
|
+
injector,
|
|
325
|
+
rootElement,
|
|
326
|
+
jsxElement: <MarkdownInput value="" required hideChrome />,
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
await flushUpdates()
|
|
330
|
+
|
|
331
|
+
const wrapper = document.querySelector('shade-markdown-input') as HTMLElement
|
|
332
|
+
expect(wrapper.hasAttribute('data-invalid')).toBe(true)
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
it('should render with validation inside a Form', async () => {
|
|
338
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
339
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
340
|
+
|
|
341
|
+
initializeShadeRoot({
|
|
342
|
+
injector,
|
|
343
|
+
rootElement,
|
|
344
|
+
jsxElement: (
|
|
345
|
+
<Form onSubmit={() => {}} validate={(_data): _data is { content: string } => true}>
|
|
346
|
+
<MarkdownInput
|
|
347
|
+
value="short"
|
|
348
|
+
name="content"
|
|
349
|
+
getValidationResult={({ value }) =>
|
|
350
|
+
value.length < 10 ? { isValid: false, message: 'Too short' } : { isValid: true }
|
|
351
|
+
}
|
|
352
|
+
/>
|
|
353
|
+
</Form>
|
|
354
|
+
),
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
await flushUpdates()
|
|
358
|
+
|
|
359
|
+
const wrapper = document.querySelector('shade-markdown-input') as HTMLElement
|
|
360
|
+
expect(wrapper.hasAttribute('data-invalid')).toBe(true)
|
|
361
|
+
expect(wrapper.textContent).toContain('Too short')
|
|
362
|
+
|
|
363
|
+
const textarea = wrapper.querySelector('textarea') as HTMLTextAreaElement
|
|
364
|
+
expect(textarea.name).toBe('content')
|
|
365
|
+
})
|
|
366
|
+
})
|
|
367
|
+
|
|
163
368
|
describe('image paste', () => {
|
|
164
369
|
const createPasteEvent = (items: Array<{ type: string; file: File | null }>) => {
|
|
165
370
|
const pasteEvent = new Event('paste', { bubbles: true, cancelable: true })
|