@furystack/shades-common-components 12.2.0 → 12.4.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 +56 -0
- package/esm/components/form.d.ts +5 -2
- package/esm/components/form.d.ts.map +1 -1
- package/esm/components/form.js +28 -6
- package/esm/components/form.js.map +1 -1
- package/esm/components/form.spec.js +207 -0
- package/esm/components/form.spec.js.map +1 -1
- package/esm/components/index.d.ts +1 -0
- package/esm/components/index.d.ts.map +1 -1
- package/esm/components/index.js +1 -0
- package/esm/components/index.js.map +1 -1
- package/esm/components/markdown/index.d.ts +5 -0
- package/esm/components/markdown/index.d.ts.map +1 -0
- package/esm/components/markdown/index.js +5 -0
- package/esm/components/markdown/index.js.map +1 -0
- package/esm/components/markdown/markdown-display.d.ts +19 -0
- package/esm/components/markdown/markdown-display.d.ts.map +1 -0
- package/esm/components/markdown/markdown-display.js +149 -0
- package/esm/components/markdown/markdown-display.js.map +1 -0
- package/esm/components/markdown/markdown-display.spec.d.ts +2 -0
- package/esm/components/markdown/markdown-display.spec.d.ts.map +1 -0
- package/esm/components/markdown/markdown-display.spec.js +191 -0
- package/esm/components/markdown/markdown-display.spec.js.map +1 -0
- package/esm/components/markdown/markdown-editor.d.ts +25 -0
- package/esm/components/markdown/markdown-editor.d.ts.map +1 -0
- package/esm/components/markdown/markdown-editor.js +113 -0
- package/esm/components/markdown/markdown-editor.js.map +1 -0
- package/esm/components/markdown/markdown-editor.spec.d.ts +2 -0
- package/esm/components/markdown/markdown-editor.spec.d.ts.map +1 -0
- package/esm/components/markdown/markdown-editor.spec.js +111 -0
- package/esm/components/markdown/markdown-editor.spec.js.map +1 -0
- package/esm/components/markdown/markdown-input.d.ts +29 -0
- package/esm/components/markdown/markdown-input.d.ts.map +1 -0
- package/esm/components/markdown/markdown-input.js +100 -0
- package/esm/components/markdown/markdown-input.js.map +1 -0
- package/esm/components/markdown/markdown-input.spec.d.ts +2 -0
- package/esm/components/markdown/markdown-input.spec.d.ts.map +1 -0
- package/esm/components/markdown/markdown-input.spec.js +215 -0
- package/esm/components/markdown/markdown-input.spec.js.map +1 -0
- package/esm/components/markdown/markdown-parser.d.ts +82 -0
- package/esm/components/markdown/markdown-parser.d.ts.map +1 -0
- package/esm/components/markdown/markdown-parser.js +274 -0
- package/esm/components/markdown/markdown-parser.js.map +1 -0
- package/esm/components/markdown/markdown-parser.spec.d.ts +2 -0
- package/esm/components/markdown/markdown-parser.spec.d.ts.map +1 -0
- package/esm/components/markdown/markdown-parser.spec.js +229 -0
- package/esm/components/markdown/markdown-parser.spec.js.map +1 -0
- package/esm/components/styles.d.ts +1 -0
- package/esm/components/styles.d.ts.map +1 -1
- package/esm/components/styles.js.map +1 -1
- package/esm/components/typography.d.ts.map +1 -1
- package/esm/components/typography.js +26 -14
- package/esm/components/typography.js.map +1 -1
- package/esm/services/css-variable-theme.d.ts +3 -0
- package/esm/services/css-variable-theme.d.ts.map +1 -1
- package/esm/services/css-variable-theme.js +3 -0
- package/esm/services/css-variable-theme.js.map +1 -1
- package/esm/services/css-variable-theme.spec.js +3 -0
- package/esm/services/css-variable-theme.spec.js.map +1 -1
- package/esm/services/default-dark-palette.d.ts +8 -0
- package/esm/services/default-dark-palette.d.ts.map +1 -0
- package/esm/services/default-dark-palette.js +56 -0
- package/esm/services/default-dark-palette.js.map +1 -0
- package/esm/services/default-dark-theme.d.ts +3 -0
- package/esm/services/default-dark-theme.d.ts.map +1 -1
- package/esm/services/default-dark-theme.js +7 -4
- package/esm/services/default-dark-theme.js.map +1 -1
- package/esm/services/default-light-theme.d.ts +3 -0
- package/esm/services/default-light-theme.d.ts.map +1 -1
- package/esm/services/default-light-theme.js +3 -0
- package/esm/services/default-light-theme.js.map +1 -1
- package/esm/services/index.d.ts +1 -0
- package/esm/services/index.d.ts.map +1 -1
- package/esm/services/index.js +1 -0
- package/esm/services/index.js.map +1 -1
- package/esm/services/theme-provider-service.d.ts +10 -1
- package/esm/services/theme-provider-service.d.ts.map +1 -1
- package/esm/services/theme-provider-service.js.map +1 -1
- package/package.json +2 -2
- package/src/components/form.spec.tsx +309 -0
- package/src/components/form.tsx +31 -8
- package/src/components/index.ts +1 -0
- package/src/components/markdown/index.ts +4 -0
- package/src/components/markdown/markdown-display.spec.tsx +243 -0
- package/src/components/markdown/markdown-display.tsx +202 -0
- package/src/components/markdown/markdown-editor.spec.tsx +142 -0
- package/src/components/markdown/markdown-editor.tsx +167 -0
- package/src/components/markdown/markdown-input.spec.tsx +274 -0
- package/src/components/markdown/markdown-input.tsx +143 -0
- package/src/components/markdown/markdown-parser.spec.ts +258 -0
- package/src/components/markdown/markdown-parser.ts +333 -0
- package/src/components/styles.tsx +1 -0
- package/src/components/typography.tsx +28 -15
- package/src/services/css-variable-theme.spec.ts +3 -0
- package/src/services/css-variable-theme.ts +3 -0
- package/src/services/default-dark-palette.ts +57 -0
- package/src/services/default-dark-theme.ts +7 -4
- package/src/services/default-light-theme.ts +3 -0
- package/src/services/index.ts +1 -0
- package/src/services/theme-provider-service.ts +7 -1
|
@@ -35,6 +35,18 @@ describe('FormService', () => {
|
|
|
35
35
|
expect(service.inputs.size).toBe(0)
|
|
36
36
|
})
|
|
37
37
|
})
|
|
38
|
+
|
|
39
|
+
it('should initialize isSubmitting as false', () => {
|
|
40
|
+
using(new FormService(), (service) => {
|
|
41
|
+
expect(service.isSubmitting.getValue()).toBe(false)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should initialize submitError as undefined', () => {
|
|
46
|
+
using(new FormService(), (service) => {
|
|
47
|
+
expect(service.submitError.getValue()).toBeUndefined()
|
|
48
|
+
})
|
|
49
|
+
})
|
|
38
50
|
})
|
|
39
51
|
|
|
40
52
|
describe('setFieldState', () => {
|
|
@@ -84,12 +96,18 @@ describe('FormService', () => {
|
|
|
84
96
|
const validatedFormDataDisposeSpy = vi.spyOn(service.validatedFormData, Symbol.dispose)
|
|
85
97
|
const rawFormDataDisposeSpy = vi.spyOn(service.rawFormData, Symbol.dispose)
|
|
86
98
|
const validationResultDisposeSpy = vi.spyOn(service.validationResult, Symbol.dispose)
|
|
99
|
+
const fieldErrorsDisposeSpy = vi.spyOn(service.fieldErrors, Symbol.dispose)
|
|
100
|
+
const isSubmittingDisposeSpy = vi.spyOn(service.isSubmitting, Symbol.dispose)
|
|
101
|
+
const submitErrorDisposeSpy = vi.spyOn(service.submitError, Symbol.dispose)
|
|
87
102
|
|
|
88
103
|
service[Symbol.dispose]()
|
|
89
104
|
|
|
90
105
|
expect(validatedFormDataDisposeSpy).toHaveBeenCalled()
|
|
91
106
|
expect(rawFormDataDisposeSpy).toHaveBeenCalled()
|
|
92
107
|
expect(validationResultDisposeSpy).toHaveBeenCalled()
|
|
108
|
+
expect(fieldErrorsDisposeSpy).toHaveBeenCalled()
|
|
109
|
+
expect(isSubmittingDisposeSpy).toHaveBeenCalled()
|
|
110
|
+
expect(submitErrorDisposeSpy).toHaveBeenCalled()
|
|
93
111
|
})
|
|
94
112
|
})
|
|
95
113
|
})
|
|
@@ -488,4 +506,295 @@ describe('Form component', () => {
|
|
|
488
506
|
await sleepAsync(50)
|
|
489
507
|
})
|
|
490
508
|
})
|
|
509
|
+
|
|
510
|
+
it('should set isSubmitting during async onSubmit and reset after', async () => {
|
|
511
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
512
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
513
|
+
|
|
514
|
+
let resolveSubmit: () => void
|
|
515
|
+
const submitPromise = new Promise<void>((resolve) => {
|
|
516
|
+
resolveSubmit = resolve
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
type FormData = { name: string }
|
|
520
|
+
|
|
521
|
+
initializeShadeRoot({
|
|
522
|
+
injector,
|
|
523
|
+
rootElement,
|
|
524
|
+
jsxElement: (
|
|
525
|
+
<Form<FormData>
|
|
526
|
+
onSubmit={() => submitPromise}
|
|
527
|
+
validate={(data): data is FormData => {
|
|
528
|
+
const d = data as Record<string, unknown>
|
|
529
|
+
return typeof d.name === 'string'
|
|
530
|
+
}}
|
|
531
|
+
>
|
|
532
|
+
<input name="name" type="text" />
|
|
533
|
+
<button type="submit">Submit</button>
|
|
534
|
+
</Form>
|
|
535
|
+
),
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
await sleepAsync(50)
|
|
539
|
+
|
|
540
|
+
const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
|
|
541
|
+
const input = form.querySelector('input[name="name"]') as HTMLInputElement
|
|
542
|
+
input.value = 'Test'
|
|
543
|
+
|
|
544
|
+
const formInjector = (form as unknown as { injector: Injector }).injector
|
|
545
|
+
const formService = formInjector.getInstance(FormService)
|
|
546
|
+
|
|
547
|
+
expect(formService.isSubmitting.getValue()).toBe(false)
|
|
548
|
+
|
|
549
|
+
const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
|
|
550
|
+
form.dispatchEvent(submitEvent)
|
|
551
|
+
|
|
552
|
+
await sleepAsync(50)
|
|
553
|
+
expect(formService.isSubmitting.getValue()).toBe(true)
|
|
554
|
+
|
|
555
|
+
resolveSubmit!()
|
|
556
|
+
await sleepAsync(50)
|
|
557
|
+
expect(formService.isSubmitting.getValue()).toBe(false)
|
|
558
|
+
})
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
it('should reset isSubmitting to false and set submitError when onSubmit throws', async () => {
|
|
562
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
563
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
564
|
+
|
|
565
|
+
type FormData = { name: string }
|
|
566
|
+
|
|
567
|
+
const submitError = new Error('Submit failed')
|
|
568
|
+
|
|
569
|
+
initializeShadeRoot({
|
|
570
|
+
injector,
|
|
571
|
+
rootElement,
|
|
572
|
+
jsxElement: (
|
|
573
|
+
<Form<FormData>
|
|
574
|
+
onSubmit={async () => {
|
|
575
|
+
throw submitError
|
|
576
|
+
}}
|
|
577
|
+
validate={(data): data is FormData => {
|
|
578
|
+
const d = data as Record<string, unknown>
|
|
579
|
+
return typeof d.name === 'string'
|
|
580
|
+
}}
|
|
581
|
+
>
|
|
582
|
+
<input name="name" type="text" />
|
|
583
|
+
<button type="submit">Submit</button>
|
|
584
|
+
</Form>
|
|
585
|
+
),
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
await sleepAsync(50)
|
|
589
|
+
|
|
590
|
+
const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
|
|
591
|
+
const input = form.querySelector('input[name="name"]') as HTMLInputElement
|
|
592
|
+
input.value = 'Test'
|
|
593
|
+
|
|
594
|
+
const formInjector = (form as unknown as { injector: Injector }).injector
|
|
595
|
+
const formService = formInjector.getInstance(FormService)
|
|
596
|
+
|
|
597
|
+
const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
|
|
598
|
+
form.dispatchEvent(submitEvent)
|
|
599
|
+
|
|
600
|
+
await sleepAsync(50)
|
|
601
|
+
expect(formService.isSubmitting.getValue()).toBe(false)
|
|
602
|
+
expect(formService.submitError.getValue()).toBe(submitError)
|
|
603
|
+
})
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
it('should clear submitError before a new submission', async () => {
|
|
607
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
608
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
609
|
+
|
|
610
|
+
let shouldThrow = true
|
|
611
|
+
let resolveSubmit: () => void
|
|
612
|
+
|
|
613
|
+
type FormData = { name: string }
|
|
614
|
+
|
|
615
|
+
initializeShadeRoot({
|
|
616
|
+
injector,
|
|
617
|
+
rootElement,
|
|
618
|
+
jsxElement: (
|
|
619
|
+
<Form<FormData>
|
|
620
|
+
onSubmit={async () => {
|
|
621
|
+
if (shouldThrow) {
|
|
622
|
+
throw new Error('First submit fails')
|
|
623
|
+
}
|
|
624
|
+
return new Promise<void>((resolve) => {
|
|
625
|
+
resolveSubmit = resolve
|
|
626
|
+
})
|
|
627
|
+
}}
|
|
628
|
+
validate={(data): data is FormData => {
|
|
629
|
+
const d = data as Record<string, unknown>
|
|
630
|
+
return typeof d.name === 'string'
|
|
631
|
+
}}
|
|
632
|
+
>
|
|
633
|
+
<input name="name" type="text" />
|
|
634
|
+
<button type="submit">Submit</button>
|
|
635
|
+
</Form>
|
|
636
|
+
),
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
await sleepAsync(50)
|
|
640
|
+
|
|
641
|
+
const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
|
|
642
|
+
const input = form.querySelector('input[name="name"]') as HTMLInputElement
|
|
643
|
+
input.value = 'Test'
|
|
644
|
+
|
|
645
|
+
const formInjector = (form as unknown as { injector: Injector }).injector
|
|
646
|
+
const formService = formInjector.getInstance(FormService)
|
|
647
|
+
|
|
648
|
+
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
|
|
649
|
+
await sleepAsync(50)
|
|
650
|
+
expect(formService.submitError.getValue()).toBeInstanceOf(Error)
|
|
651
|
+
|
|
652
|
+
shouldThrow = false
|
|
653
|
+
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
|
|
654
|
+
await sleepAsync(50)
|
|
655
|
+
expect(formService.submitError.getValue()).toBeUndefined()
|
|
656
|
+
expect(formService.isSubmitting.getValue()).toBe(true)
|
|
657
|
+
|
|
658
|
+
resolveSubmit!()
|
|
659
|
+
await sleepAsync(50)
|
|
660
|
+
expect(formService.isSubmitting.getValue()).toBe(false)
|
|
661
|
+
})
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
it('should set inert on form element when disableOnSubmit is true during async submit', async () => {
|
|
665
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
666
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
667
|
+
|
|
668
|
+
let resolveSubmit: () => void
|
|
669
|
+
const submitPromise = new Promise<void>((resolve) => {
|
|
670
|
+
resolveSubmit = resolve
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
type FormData = { name: string }
|
|
674
|
+
|
|
675
|
+
initializeShadeRoot({
|
|
676
|
+
injector,
|
|
677
|
+
rootElement,
|
|
678
|
+
jsxElement: (
|
|
679
|
+
<Form<FormData>
|
|
680
|
+
onSubmit={() => submitPromise}
|
|
681
|
+
disableOnSubmit
|
|
682
|
+
validate={(data): data is FormData => {
|
|
683
|
+
const d = data as Record<string, unknown>
|
|
684
|
+
return typeof d.name === 'string'
|
|
685
|
+
}}
|
|
686
|
+
>
|
|
687
|
+
<input name="name" type="text" />
|
|
688
|
+
<button type="submit">Submit</button>
|
|
689
|
+
</Form>
|
|
690
|
+
),
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
await sleepAsync(50)
|
|
694
|
+
|
|
695
|
+
const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
|
|
696
|
+
const input = form.querySelector('input[name="name"]') as HTMLInputElement
|
|
697
|
+
input.value = 'Test'
|
|
698
|
+
|
|
699
|
+
expect(form.inert).toBeFalsy()
|
|
700
|
+
|
|
701
|
+
const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
|
|
702
|
+
form.dispatchEvent(submitEvent)
|
|
703
|
+
|
|
704
|
+
await sleepAsync(50)
|
|
705
|
+
expect(form.inert).toBe(true)
|
|
706
|
+
|
|
707
|
+
resolveSubmit!()
|
|
708
|
+
await sleepAsync(50)
|
|
709
|
+
expect(form.inert).toBe(false)
|
|
710
|
+
})
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
it('should not set inert when disableOnSubmit is not provided', async () => {
|
|
714
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
715
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
716
|
+
|
|
717
|
+
let resolveSubmit: () => void
|
|
718
|
+
const submitPromise = new Promise<void>((resolve) => {
|
|
719
|
+
resolveSubmit = resolve
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
type FormData = { name: string }
|
|
723
|
+
|
|
724
|
+
initializeShadeRoot({
|
|
725
|
+
injector,
|
|
726
|
+
rootElement,
|
|
727
|
+
jsxElement: (
|
|
728
|
+
<Form<FormData>
|
|
729
|
+
onSubmit={() => submitPromise}
|
|
730
|
+
validate={(data): data is FormData => {
|
|
731
|
+
const d = data as Record<string, unknown>
|
|
732
|
+
return typeof d.name === 'string'
|
|
733
|
+
}}
|
|
734
|
+
>
|
|
735
|
+
<input name="name" type="text" />
|
|
736
|
+
<button type="submit">Submit</button>
|
|
737
|
+
</Form>
|
|
738
|
+
),
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
await sleepAsync(50)
|
|
742
|
+
|
|
743
|
+
const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
|
|
744
|
+
const input = form.querySelector('input[name="name"]') as HTMLInputElement
|
|
745
|
+
input.value = 'Test'
|
|
746
|
+
|
|
747
|
+
const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
|
|
748
|
+
form.dispatchEvent(submitEvent)
|
|
749
|
+
|
|
750
|
+
await sleepAsync(50)
|
|
751
|
+
expect(form.inert).toBeFalsy()
|
|
752
|
+
|
|
753
|
+
resolveSubmit!()
|
|
754
|
+
await sleepAsync(50)
|
|
755
|
+
})
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
it('should remove inert even if onSubmit throws when disableOnSubmit is true', async () => {
|
|
759
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
760
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
761
|
+
|
|
762
|
+
type FormData = { name: string }
|
|
763
|
+
|
|
764
|
+
initializeShadeRoot({
|
|
765
|
+
injector,
|
|
766
|
+
rootElement,
|
|
767
|
+
jsxElement: (
|
|
768
|
+
<Form<FormData>
|
|
769
|
+
onSubmit={async () => {
|
|
770
|
+
throw new Error('Submit failed')
|
|
771
|
+
}}
|
|
772
|
+
disableOnSubmit
|
|
773
|
+
validate={(data): data is FormData => {
|
|
774
|
+
const d = data as Record<string, unknown>
|
|
775
|
+
return typeof d.name === 'string'
|
|
776
|
+
}}
|
|
777
|
+
>
|
|
778
|
+
<input name="name" type="text" />
|
|
779
|
+
<button type="submit">Submit</button>
|
|
780
|
+
</Form>
|
|
781
|
+
),
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
await sleepAsync(50)
|
|
785
|
+
|
|
786
|
+
const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
|
|
787
|
+
const input = form.querySelector('input[name="name"]') as HTMLInputElement
|
|
788
|
+
input.value = 'Test'
|
|
789
|
+
|
|
790
|
+
const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
|
|
791
|
+
form.dispatchEvent(submitEvent)
|
|
792
|
+
|
|
793
|
+
await sleepAsync(50)
|
|
794
|
+
expect(form.inert).toBe(false)
|
|
795
|
+
const formInjector = (form as unknown as { injector: Injector }).injector
|
|
796
|
+
const formService = formInjector.getInstance(FormService)
|
|
797
|
+
expect(formService.isSubmitting.getValue()).toBe(false)
|
|
798
|
+
})
|
|
799
|
+
})
|
|
491
800
|
})
|
package/src/components/form.tsx
CHANGED
|
@@ -29,6 +29,10 @@ export class FormService<T> {
|
|
|
29
29
|
|
|
30
30
|
public inputs = new Set<HTMLInputElement>()
|
|
31
31
|
|
|
32
|
+
public isSubmitting = new ObservableValue<boolean>(false)
|
|
33
|
+
|
|
34
|
+
public submitError = new ObservableValue<unknown>(undefined)
|
|
35
|
+
|
|
32
36
|
public setFieldState = (key: keyof T, validationResult: InputValidationResult, validity: ValidityState) => {
|
|
33
37
|
this.fieldErrors.setValue({ ...this.fieldErrors.getValue(), [key]: { validationResult, validity } })
|
|
34
38
|
}
|
|
@@ -37,13 +41,17 @@ export class FormService<T> {
|
|
|
37
41
|
this.validatedFormData[Symbol.dispose]()
|
|
38
42
|
this.rawFormData[Symbol.dispose]()
|
|
39
43
|
this.validationResult[Symbol.dispose]()
|
|
44
|
+
this.fieldErrors[Symbol.dispose]()
|
|
45
|
+
this.isSubmitting[Symbol.dispose]()
|
|
46
|
+
this.submitError[Symbol.dispose]()
|
|
40
47
|
}
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
type FormProps<T> = {
|
|
44
|
-
onSubmit: (formData: T) => void
|
|
51
|
+
onSubmit: (formData: T) => void | Promise<void>
|
|
45
52
|
onReset?: () => void
|
|
46
|
-
validate: (formData:
|
|
53
|
+
validate: (formData: unknown) => formData is T
|
|
54
|
+
disableOnSubmit?: boolean
|
|
47
55
|
} & PartialElement<Omit<HTMLFormElement, 'onsubmit' | 'onchange' | 'onreset'>>
|
|
48
56
|
|
|
49
57
|
export const Form: <T>(props: FormProps<T>, children: ChildrenList) => JSX.Element = Shade({
|
|
@@ -61,13 +69,14 @@ export const Form: <T>(props: FormProps<T>, children: ChildrenList) => JSX.Eleme
|
|
|
61
69
|
// `injector` setter defined on the Shade base class.
|
|
62
70
|
useHostProps({ injector: formInjector })
|
|
63
71
|
|
|
64
|
-
const changeHandler = (ev: Event, shouldSubmit?: boolean) => {
|
|
72
|
+
const changeHandler = async (ev: Event, shouldSubmit?: boolean) => {
|
|
65
73
|
formService.inputs.forEach((i) => {
|
|
66
74
|
const e = document.createEvent('FocusEvent')
|
|
67
75
|
e.initEvent('blur', true, true)
|
|
68
76
|
i.dispatchEvent(e)
|
|
69
77
|
})
|
|
70
|
-
const
|
|
78
|
+
const formElement = ev.currentTarget as HTMLFormElement
|
|
79
|
+
const formData = Object.fromEntries(new FormData(formElement).entries())
|
|
71
80
|
formService.rawFormData.setValue(formData)
|
|
72
81
|
const currentFieldErrors = formService.fieldErrors.getValue()
|
|
73
82
|
|
|
@@ -80,7 +89,21 @@ export const Form: <T>(props: FormProps<T>, children: ChildrenList) => JSX.Eleme
|
|
|
80
89
|
formService.validationResult.setValue({ isValid: true })
|
|
81
90
|
formService.validatedFormData.setValue(formData)
|
|
82
91
|
if (shouldSubmit) {
|
|
83
|
-
|
|
92
|
+
formService.isSubmitting.setValue(true)
|
|
93
|
+
formService.submitError.setValue(undefined)
|
|
94
|
+
if (props.disableOnSubmit) {
|
|
95
|
+
formElement.inert = true
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
await props.onSubmit(formData)
|
|
99
|
+
} catch (error) {
|
|
100
|
+
formService.submitError.setValue(error)
|
|
101
|
+
} finally {
|
|
102
|
+
formService.isSubmitting.setValue(false)
|
|
103
|
+
if (props.disableOnSubmit) {
|
|
104
|
+
formElement.inert = false
|
|
105
|
+
}
|
|
106
|
+
}
|
|
84
107
|
}
|
|
85
108
|
} else {
|
|
86
109
|
formService.validationResult.setValue({ isValid: false, reason: 'validation-failed' })
|
|
@@ -89,14 +112,14 @@ export const Form: <T>(props: FormProps<T>, children: ChildrenList) => JSX.Eleme
|
|
|
89
112
|
|
|
90
113
|
useHostProps({
|
|
91
114
|
oninvalid: (ev: Event) => {
|
|
92
|
-
changeHandler(ev)
|
|
115
|
+
void changeHandler(ev)
|
|
93
116
|
},
|
|
94
117
|
onsubmit: (ev: SubmitEvent) => {
|
|
95
118
|
ev.preventDefault()
|
|
96
|
-
changeHandler(ev, true)
|
|
119
|
+
void changeHandler(ev, true)
|
|
97
120
|
},
|
|
98
121
|
onchange: (ev: Event) => {
|
|
99
|
-
changeHandler(ev)
|
|
122
|
+
void changeHandler(ev)
|
|
100
123
|
},
|
|
101
124
|
onreset: () => {
|
|
102
125
|
formService.rawFormData.setValue(null)
|
package/src/components/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ export * from './inputs/index.js'
|
|
|
29
29
|
export * from './linear-progress.js'
|
|
30
30
|
export * from './list/index.js'
|
|
31
31
|
export * from './loader.js'
|
|
32
|
+
export * from './markdown/index.js'
|
|
32
33
|
export * from './menu/index.js'
|
|
33
34
|
export * from './modal.js'
|
|
34
35
|
export * from './noty-list.js'
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { Injector } from '@furystack/inject'
|
|
2
|
+
import { createComponent, initializeShadeRoot } from '@furystack/shades'
|
|
3
|
+
import { sleepAsync, usingAsync } from '@furystack/utils'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { MarkdownDisplay } from './markdown-display.js'
|
|
6
|
+
|
|
7
|
+
describe('MarkdownDisplay', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
document.body.innerHTML = '<div id="root"></div>'
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
document.body.innerHTML = ''
|
|
14
|
+
vi.restoreAllMocks()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should render with shadow DOM', async () => {
|
|
18
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
19
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
20
|
+
|
|
21
|
+
initializeShadeRoot({
|
|
22
|
+
injector,
|
|
23
|
+
rootElement,
|
|
24
|
+
jsxElement: <MarkdownDisplay content="Hello" />,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
await sleepAsync(50)
|
|
28
|
+
|
|
29
|
+
const el = document.querySelector('shade-markdown-display')
|
|
30
|
+
expect(el).not.toBeNull()
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should render a heading', async () => {
|
|
35
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
36
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
37
|
+
|
|
38
|
+
initializeShadeRoot({
|
|
39
|
+
injector,
|
|
40
|
+
rootElement,
|
|
41
|
+
jsxElement: <MarkdownDisplay content="# Hello World" />,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
await sleepAsync(50)
|
|
45
|
+
|
|
46
|
+
const typography = document.querySelector('shade-markdown-display shade-typography')
|
|
47
|
+
expect(typography).not.toBeNull()
|
|
48
|
+
expect(typography?.getAttribute('data-variant')).toBe('h1')
|
|
49
|
+
expect(typography?.textContent).toContain('Hello World')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should render a paragraph', async () => {
|
|
54
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
55
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
56
|
+
|
|
57
|
+
initializeShadeRoot({
|
|
58
|
+
injector,
|
|
59
|
+
rootElement,
|
|
60
|
+
jsxElement: <MarkdownDisplay content="Just a paragraph." />,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
await sleepAsync(50)
|
|
64
|
+
|
|
65
|
+
const typography = document.querySelector('shade-markdown-display shade-typography[data-variant="body1"]')
|
|
66
|
+
expect(typography).not.toBeNull()
|
|
67
|
+
expect(typography?.textContent).toContain('Just a paragraph.')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should render a code block', async () => {
|
|
72
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
73
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
74
|
+
|
|
75
|
+
initializeShadeRoot({
|
|
76
|
+
injector,
|
|
77
|
+
rootElement,
|
|
78
|
+
jsxElement: <MarkdownDisplay content={'```js\nconsole.log("hi")\n```'} />,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
await sleepAsync(50)
|
|
82
|
+
|
|
83
|
+
const codeBlock = document.querySelector('shade-markdown-display .md-code-block')
|
|
84
|
+
expect(codeBlock).not.toBeNull()
|
|
85
|
+
expect(codeBlock?.textContent).toContain('console.log("hi")')
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should render a list', async () => {
|
|
90
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
91
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
92
|
+
|
|
93
|
+
initializeShadeRoot({
|
|
94
|
+
injector,
|
|
95
|
+
rootElement,
|
|
96
|
+
jsxElement: <MarkdownDisplay content={'- Item A\n- Item B'} />,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
await sleepAsync(50)
|
|
100
|
+
|
|
101
|
+
const list = document.querySelector('shade-markdown-display ul')
|
|
102
|
+
expect(list).not.toBeNull()
|
|
103
|
+
const items = document.querySelectorAll('shade-markdown-display .md-list-item')
|
|
104
|
+
expect(items.length).toBe(2)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should render checkboxes as disabled when readOnly (default)', async () => {
|
|
109
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
110
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
111
|
+
|
|
112
|
+
initializeShadeRoot({
|
|
113
|
+
injector,
|
|
114
|
+
rootElement,
|
|
115
|
+
jsxElement: <MarkdownDisplay content="- [ ] Task" />,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await sleepAsync(50)
|
|
119
|
+
|
|
120
|
+
const checkbox = document.querySelector('shade-markdown-display shade-checkbox')
|
|
121
|
+
expect(checkbox).not.toBeNull()
|
|
122
|
+
expect(checkbox?.hasAttribute('data-disabled')).toBe(true)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should render checkboxes as enabled when readOnly is false', async () => {
|
|
127
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
128
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
129
|
+
const onChange = vi.fn()
|
|
130
|
+
|
|
131
|
+
initializeShadeRoot({
|
|
132
|
+
injector,
|
|
133
|
+
rootElement,
|
|
134
|
+
jsxElement: <MarkdownDisplay content="- [ ] Task" readOnly={false} onChange={onChange} />,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
await sleepAsync(50)
|
|
138
|
+
|
|
139
|
+
const checkbox = document.querySelector('shade-markdown-display shade-checkbox')
|
|
140
|
+
expect(checkbox).not.toBeNull()
|
|
141
|
+
expect(checkbox?.hasAttribute('data-disabled')).toBe(false)
|
|
142
|
+
|
|
143
|
+
const input = checkbox?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
|
144
|
+
expect(input).not.toBeNull()
|
|
145
|
+
input.click()
|
|
146
|
+
|
|
147
|
+
await sleepAsync(50)
|
|
148
|
+
|
|
149
|
+
expect(onChange).toHaveBeenCalledOnce()
|
|
150
|
+
expect(onChange).toHaveBeenCalledWith('- [x] Task')
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should render a blockquote', async () => {
|
|
155
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
156
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
157
|
+
|
|
158
|
+
initializeShadeRoot({
|
|
159
|
+
injector,
|
|
160
|
+
rootElement,
|
|
161
|
+
jsxElement: <MarkdownDisplay content="> Quote text" />,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
await sleepAsync(50)
|
|
165
|
+
|
|
166
|
+
const bq = document.querySelector('shade-markdown-display .md-blockquote')
|
|
167
|
+
expect(bq).not.toBeNull()
|
|
168
|
+
expect(bq?.textContent).toContain('Quote text')
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should render a horizontal rule', async () => {
|
|
173
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
174
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
175
|
+
|
|
176
|
+
initializeShadeRoot({
|
|
177
|
+
injector,
|
|
178
|
+
rootElement,
|
|
179
|
+
jsxElement: <MarkdownDisplay content="---" />,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
await sleepAsync(50)
|
|
183
|
+
|
|
184
|
+
const hr = document.querySelector('shade-markdown-display .md-hr')
|
|
185
|
+
expect(hr).not.toBeNull()
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('should render links', async () => {
|
|
190
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
191
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
192
|
+
|
|
193
|
+
initializeShadeRoot({
|
|
194
|
+
injector,
|
|
195
|
+
rootElement,
|
|
196
|
+
jsxElement: <MarkdownDisplay content="[Click here](https://example.com)" />,
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
await sleepAsync(50)
|
|
200
|
+
|
|
201
|
+
const link = document.querySelector('shade-markdown-display .md-link') as HTMLAnchorElement
|
|
202
|
+
expect(link).not.toBeNull()
|
|
203
|
+
expect(link?.href).toContain('example.com')
|
|
204
|
+
expect(link?.textContent).toContain('Click here')
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('should render images', async () => {
|
|
209
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
210
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
211
|
+
|
|
212
|
+
initializeShadeRoot({
|
|
213
|
+
injector,
|
|
214
|
+
rootElement,
|
|
215
|
+
jsxElement: <MarkdownDisplay content="" />,
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
await sleepAsync(50)
|
|
219
|
+
|
|
220
|
+
const img = document.querySelector('shade-markdown-display .md-image') as HTMLImageElement
|
|
221
|
+
expect(img).not.toBeNull()
|
|
222
|
+
expect(img?.alt).toBe('alt text')
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('should render empty for empty content', async () => {
|
|
227
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
228
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
229
|
+
|
|
230
|
+
initializeShadeRoot({
|
|
231
|
+
injector,
|
|
232
|
+
rootElement,
|
|
233
|
+
jsxElement: <MarkdownDisplay content="" />,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
await sleepAsync(50)
|
|
237
|
+
|
|
238
|
+
const root = document.querySelector('shade-markdown-display .md-root')
|
|
239
|
+
expect(root).not.toBeNull()
|
|
240
|
+
expect(root?.children.length).toBe(0)
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
})
|