@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.
Files changed (70) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/esm/components/data-grid/data-grid.d.ts +7 -1
  3. package/esm/components/data-grid/data-grid.d.ts.map +1 -1
  4. package/esm/components/data-grid/data-grid.js +1 -1
  5. package/esm/components/data-grid/data-grid.js.map +1 -1
  6. package/esm/components/data-grid/footer.d.ts +1 -0
  7. package/esm/components/data-grid/footer.d.ts.map +1 -1
  8. package/esm/components/data-grid/footer.js +8 -15
  9. package/esm/components/data-grid/footer.js.map +1 -1
  10. package/esm/components/data-grid/footer.spec.js +85 -47
  11. package/esm/components/data-grid/footer.spec.js.map +1 -1
  12. package/esm/components/grid.d.ts +3 -0
  13. package/esm/components/grid.d.ts.map +1 -1
  14. package/esm/components/grid.js +3 -0
  15. package/esm/components/grid.js.map +1 -1
  16. package/esm/components/inputs/autocomplete.d.ts +3 -0
  17. package/esm/components/inputs/autocomplete.d.ts.map +1 -1
  18. package/esm/components/inputs/autocomplete.js +3 -0
  19. package/esm/components/inputs/autocomplete.js.map +1 -1
  20. package/esm/components/list/list.d.ts +10 -0
  21. package/esm/components/list/list.d.ts.map +1 -1
  22. package/esm/components/list/list.js +23 -2
  23. package/esm/components/list/list.js.map +1 -1
  24. package/esm/components/list/list.spec.js +101 -0
  25. package/esm/components/list/list.spec.js.map +1 -1
  26. package/esm/components/markdown/markdown-editor.d.ts +16 -2
  27. package/esm/components/markdown/markdown-editor.d.ts.map +1 -1
  28. package/esm/components/markdown/markdown-editor.js +42 -8
  29. package/esm/components/markdown/markdown-editor.js.map +1 -1
  30. package/esm/components/markdown/markdown-editor.spec.js +190 -0
  31. package/esm/components/markdown/markdown-editor.spec.js.map +1 -1
  32. package/esm/components/markdown/markdown-input.d.ts +16 -0
  33. package/esm/components/markdown/markdown-input.d.ts.map +1 -1
  34. package/esm/components/markdown/markdown-input.js +44 -3
  35. package/esm/components/markdown/markdown-input.js.map +1 -1
  36. package/esm/components/markdown/markdown-input.spec.js +140 -0
  37. package/esm/components/markdown/markdown-input.spec.js.map +1 -1
  38. package/esm/components/markdown/markdown-validation.d.ts +25 -0
  39. package/esm/components/markdown/markdown-validation.d.ts.map +1 -0
  40. package/esm/components/markdown/markdown-validation.js +15 -0
  41. package/esm/components/markdown/markdown-validation.js.map +1 -0
  42. package/esm/components/suggest/index.d.ts +10 -2
  43. package/esm/components/suggest/index.d.ts.map +1 -1
  44. package/esm/components/suggest/index.js +21 -1
  45. package/esm/components/suggest/index.js.map +1 -1
  46. package/esm/components/suggest/index.spec.js +50 -0
  47. package/esm/components/suggest/index.spec.js.map +1 -1
  48. package/esm/components/wizard/index.d.ts +8 -0
  49. package/esm/components/wizard/index.d.ts.map +1 -1
  50. package/esm/components/wizard/index.js +90 -0
  51. package/esm/components/wizard/index.js.map +1 -1
  52. package/esm/components/wizard/index.spec.js +79 -2
  53. package/esm/components/wizard/index.spec.js.map +1 -1
  54. package/package.json +3 -3
  55. package/src/components/data-grid/data-grid.tsx +13 -2
  56. package/src/components/data-grid/footer.spec.tsx +104 -50
  57. package/src/components/data-grid/footer.tsx +25 -31
  58. package/src/components/grid.tsx +3 -0
  59. package/src/components/inputs/autocomplete.tsx +3 -0
  60. package/src/components/list/list.spec.tsx +173 -0
  61. package/src/components/list/list.tsx +56 -19
  62. package/src/components/markdown/markdown-editor.spec.tsx +261 -0
  63. package/src/components/markdown/markdown-editor.tsx +63 -10
  64. package/src/components/markdown/markdown-input.spec.tsx +205 -0
  65. package/src/components/markdown/markdown-input.tsx +61 -2
  66. package/src/components/markdown/markdown-validation.ts +33 -0
  67. package/src/components/suggest/index.spec.tsx +83 -0
  68. package/src/components/suggest/index.tsx +36 -3
  69. package/src/components/wizard/index.spec.tsx +118 -1
  70. 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
- return (
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
- <div className="md-editor-split" data-layout={layout}>
162
- <div className="md-editor-pane md-editor-pane-input">{inputPane}</div>
163
- <div className="md-editor-pane md-editor-pane-preview">{previewPane}</div>
164
- </div>
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 })