@vanilla-bean/components 1.1.1 → 2.0.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.
Files changed (52) hide show
  1. package/Component/Component.js +222 -36
  2. package/Component/Component.test.js +425 -38
  3. package/Component/README.md +480 -0
  4. package/Elem/README.md +373 -0
  5. package/README.md +1 -1
  6. package/components/BottomSheet/BottomSheet.js +8 -16
  7. package/components/Button/Button.js +12 -18
  8. package/components/Calendar/Calendar.js +139 -59
  9. package/components/Calendar/CalendarEvent.js +9 -4
  10. package/components/Calendar/Toolbar.js +28 -20
  11. package/components/Calendar/index.js +1 -0
  12. package/components/Code/Code.js +26 -30
  13. package/components/ColorPicker/ColorPicker.js +57 -57
  14. package/components/Dialog/Dialog.js +78 -72
  15. package/components/Form/Form.js +12 -8
  16. package/components/Icon/Icon.js +19 -11
  17. package/components/Input/Input.js +85 -67
  18. package/components/Input/README.md +0 -2
  19. package/components/Keyboard/Key.js +7 -10
  20. package/components/Keyboard/Keyboard.js +38 -46
  21. package/components/Label/Label.js +52 -50
  22. package/components/Link/Link.js +14 -20
  23. package/components/List/List.js +28 -30
  24. package/components/Menu/Menu.js +19 -10
  25. package/components/Menu/Menu.lld.md +2 -2
  26. package/components/Notify/Notify.js +29 -19
  27. package/components/Popover/Popover.js +49 -44
  28. package/components/RadioButton/RadioButton.js +33 -29
  29. package/components/Router/Router.js +19 -21
  30. package/components/Select/Select.js +24 -30
  31. package/components/Table/Table.js +55 -36
  32. package/components/TagList/Tag.js +16 -14
  33. package/components/TagList/TagList.js +15 -8
  34. package/components/Tooltip/Tooltip.js +12 -25
  35. package/components/TooltipWrapper/TooltipWrapper.js +55 -49
  36. package/components/Whiteboard/Whiteboard.js +45 -43
  37. package/devTools/build.js +43 -0
  38. package/devTools/buildTypes.js +322 -0
  39. package/devTools/createComponent.js +155 -0
  40. package/devTools/extractJSDoc.js +395 -0
  41. package/devTools/processTemplate.js +500 -0
  42. package/devTools/updateComponentIndex.js +16 -0
  43. package/devTools/updateDemoViewIndex.js +90 -0
  44. package/eslint.config.cjs +6 -3
  45. package/index.d.ts +117 -59
  46. package/package.json +67 -22
  47. package/spellcheck.config.cjs +4 -0
  48. package/styled/README.md +329 -0
  49. package/theme/colors.js +15 -12
  50. package/theme/colors.test.js +28 -0
  51. package/utils/element.js +2 -2
  52. package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
@@ -532,90 +532,477 @@ describe('Component', () => {
532
532
  });
533
533
  });
534
534
 
535
- describe('static handlers', () => {
536
- test('handler is called for matching option key on render', () => {
537
- const called = [];
535
+ describe('static schema', () => {
536
+ test('schema defaults are merged automatically', () => {
537
+ class TestComp extends Component {
538
+ static schema = {
539
+ label: { default: 'default label' },
540
+ };
541
+ }
542
+
543
+ const comp = new TestComp();
544
+ expect(comp.options.label).toBe('default label');
545
+ comp.destroy();
546
+ });
538
547
 
548
+ test('instance options override schema defaults', () => {
539
549
  class TestComp extends Component {
540
- static handlers = {
541
- label(value) {
542
- called.push(value);
543
- },
550
+ static schema = {
551
+ label: { default: 'default label' },
544
552
  };
545
553
  }
546
554
 
547
- const comp = new TestComp({ label: 'hello' });
548
- expect(called).toContain('hello');
555
+ const comp = new TestComp({ label: 'custom' });
556
+ expect(comp.options.label).toBe('custom');
549
557
  comp.destroy();
550
558
  });
551
559
 
552
- test('handler is called for matching option key on reactive update', () => {
560
+ test('child class schema defaults override parent defaults per-key', () => {
561
+ class Parent extends Component {
562
+ static schema = {
563
+ label: { default: 'parent' },
564
+ size: { default: 'small' },
565
+ };
566
+ }
567
+
568
+ class Child extends Parent {
569
+ static schema = {
570
+ label: { default: 'child' },
571
+ };
572
+ }
573
+
574
+ const comp = new Child();
575
+ expect(comp.options.label).toBe('child');
576
+ expect(comp.options.size).toBe('small');
577
+ comp.destroy();
578
+ });
579
+
580
+ test('getter defaults evaluate fresh per instance', () => {
581
+ let evaluations = 0;
582
+
583
+ class TestComp extends Component {
584
+ static schema = {
585
+ stamp: {
586
+ get default() {
587
+ return ++evaluations;
588
+ },
589
+ },
590
+ };
591
+ }
592
+
593
+ const first = new TestComp();
594
+ const second = new TestComp();
595
+ expect(first.options.stamp).toBe(1);
596
+ expect(second.options.stamp).toBe(2);
597
+ first.destroy();
598
+ second.destroy();
599
+ });
600
+
601
+ test('set runs on render and reactive update', () => {
553
602
  const called = [];
554
603
 
555
604
  class TestComp extends Component {
556
- static handlers = {
557
- label(value) {
558
- called.push(value);
605
+ static schema = {
606
+ label: {
607
+ default: 'first',
608
+ set(value) {
609
+ called.push(value);
610
+ },
559
611
  },
560
612
  };
561
613
  }
562
614
 
563
- const comp = new TestComp({ label: 'first' });
615
+ const comp = new TestComp();
564
616
  comp.options.label = 'second';
565
- expect(called).toContain('second');
617
+ expect(called).toEqual(['first', 'second']);
618
+ comp.destroy();
619
+ });
620
+
621
+ test('declared keys with no DOM match are stored silently', () => {
622
+ class TestComp extends Component {
623
+ static schema = {
624
+ gameState: {},
625
+ };
626
+ }
627
+
628
+ const comp = new TestComp({ gameState: { score: 0 } });
629
+ comp.options.gameState = { score: 5 };
630
+ expect(comp.options.gameState).toEqual({ score: 5 });
631
+ expect(comp.elem.gameState).toBeUndefined();
632
+ comp.destroy();
633
+ });
634
+
635
+ test('declared callback options are stored without event registration or warning', () => {
636
+ const warnings = [];
637
+ const originalWarn = console.warn;
638
+ console.warn = (...args) => warnings.push(args.join(' '));
639
+
640
+ class TestComp extends Component {
641
+ static schema = {
642
+ onCustomAction: {},
643
+ };
644
+ }
645
+
646
+ const callback = () => {};
647
+ const comp = new TestComp({ onCustomAction: callback });
648
+ console.warn = originalWarn;
649
+
650
+ expect(comp.options.onCustomAction).toBe(callback);
651
+ expect(comp.onCustomAction).toBeUndefined();
652
+ expect(warnings.filter(warning => warning.includes('onCustomAction'))).toEqual([]);
566
653
  comp.destroy();
567
654
  });
568
655
 
569
- test('subclass handler shadows parent handler', () => {
570
- const parentCalls = [];
571
- const childCalls = [];
656
+ test('data: true forces store-only routing for DOM-colliding names', () => {
657
+ class TestComp extends Component {
658
+ static schema = {
659
+ title: { data: true },
660
+ };
661
+ }
662
+
663
+ const comp = new TestComp({ title: 'component data' });
664
+ expect(comp.options.title).toBe('component data');
665
+ expect(comp.elem.title).toBe('');
666
+ comp.destroy();
667
+ });
668
+
669
+ test('subclass set shadows parent set when next is not called', () => {
670
+ const order = [];
572
671
 
573
672
  class Parent extends Component {
574
- static handlers = {
575
- label(value) {
576
- parentCalls.push(value);
673
+ static schema = {
674
+ label: {
675
+ set(value) {
676
+ order.push(`parent:${value}`);
677
+ },
577
678
  },
578
679
  };
579
680
  }
580
681
 
581
682
  class Child extends Parent {
582
- static handlers = {
583
- label(value) {
584
- childCalls.push(value);
683
+ static schema = {
684
+ label: {
685
+ set(value) {
686
+ order.push(`child:${value}`);
687
+ },
585
688
  },
586
689
  };
587
690
  }
588
691
 
589
692
  const comp = new Child({ label: 'test' });
590
- expect(childCalls).toContain('test');
591
- expect(parentCalls).not.toContain('test');
693
+ expect(order).toEqual(['child:test']);
592
694
  comp.destroy();
593
695
  });
594
696
 
595
- test('handler can call parent handler explicitly', () => {
596
- const parentCalls = [];
597
- const childCalls = [];
697
+ test('subclass set chains into parent set via next', () => {
698
+ const order = [];
598
699
 
599
700
  class Parent extends Component {
600
- static handlers = {
601
- label(value) {
602
- parentCalls.push(value);
701
+ static schema = {
702
+ label: {
703
+ set(value) {
704
+ order.push(`parent:${value}`);
705
+ },
603
706
  },
604
707
  };
605
708
  }
606
709
 
607
710
  class Child extends Parent {
608
- static handlers = {
609
- label(value) {
610
- childCalls.push(value);
611
- Parent.handlers.label.call(this, value);
711
+ static schema = {
712
+ label: {
713
+ set(value, next) {
714
+ order.push(`child:${value}`);
715
+ next(value);
716
+ },
612
717
  },
613
718
  };
614
719
  }
615
720
 
616
721
  const comp = new Child({ label: 'test' });
617
- expect(childCalls).toContain('test');
618
- expect(parentCalls).toContain('test');
722
+ expect(order).toEqual(['child:test', 'parent:test']);
723
+ comp.destroy();
724
+ });
725
+
726
+ test('defaultOptions getter reflects merged schema defaults', () => {
727
+ class TestComp extends Component {
728
+ static schema = {
729
+ label: { default: 'from schema' },
730
+ };
731
+ }
732
+
733
+ const comp = new TestComp();
734
+ expect(comp.defaultOptions.label).toBe('from schema');
735
+ expect(comp.defaultOptions.tag).toBe('div');
736
+ comp.destroy();
737
+ });
738
+
739
+ test('attribute: true routes the key through setAttribute', () => {
740
+ class TestComp extends Component {
741
+ static schema = {
742
+ colspan: { attribute: true },
743
+ };
744
+ }
745
+
746
+ const comp = new TestComp({ colspan: '2' });
747
+ expect(comp.elem.getAttribute('colspan')).toBe('2');
748
+ comp.destroy();
749
+ });
750
+
751
+ test('priority: true keys process before other keys', () => {
752
+ const order = [];
753
+
754
+ class TestComp extends Component {
755
+ static schema = {
756
+ first: {
757
+ priority: true,
758
+ set() {
759
+ order.push('first');
760
+ },
761
+ },
762
+ second: {
763
+ set() {
764
+ order.push('second');
765
+ },
766
+ },
767
+ };
768
+ }
769
+
770
+ const comp = new TestComp({ second: 'b', first: 'a' });
771
+ expect(order).toEqual(['first', 'second']);
772
+ comp.destroy();
773
+ });
774
+ });
775
+
776
+ describe('schema field inheritance', () => {
777
+ test('subclass can opt out of a parent data flag with data: false', () => {
778
+ class Parent extends Component {
779
+ static schema = {
780
+ title: { data: true },
781
+ };
782
+ }
783
+
784
+ class Child extends Parent {
785
+ static schema = {
786
+ title: { data: false },
787
+ };
788
+ }
789
+
790
+ const parent = new Parent({ title: 'stored-only' });
791
+ const child = new Child({ title: 'routed' });
792
+ expect(parent.elem.title).toBe('');
793
+ expect(child.elem.title).toBe('routed');
794
+ parent.destroy();
795
+ child.destroy();
796
+ });
797
+
798
+ test('subclass set without a data field keeps the parent data flag', () => {
799
+ const seen = [];
800
+
801
+ class Parent extends Component {
802
+ static schema = {
803
+ title: { data: true },
804
+ };
805
+ }
806
+
807
+ class Child extends Parent {
808
+ static schema = {
809
+ title: {
810
+ set(value, next) {
811
+ seen.push(value);
812
+ next(value);
813
+ },
814
+ },
815
+ };
816
+ }
817
+
818
+ const child = new Child({ title: 'stored-only' });
819
+ expect(seen).toEqual(['stored-only']);
820
+ expect(child.elem.title).toBe('');
821
+ child.destroy();
822
+ });
823
+
824
+ test('subclass can lift a parent enum with enum: null', () => {
825
+ class Parent extends Component {
826
+ static schema = {
827
+ variant: { enum: ['plain'], data: true },
828
+ };
829
+ }
830
+
831
+ class Child extends Parent {
832
+ static schema = {
833
+ variant: { enum: null },
834
+ };
835
+ }
836
+
837
+ expect(() => new Parent({ variant: 'wild' })).toThrow();
838
+ const child = new Child({ variant: 'wild' });
839
+ expect(child.options.variant).toBe('wild');
840
+ child.destroy();
841
+ });
842
+ });
843
+
844
+ describe('schema enum', () => {
845
+ class EnumComp extends Component {
846
+ static schema = {
847
+ variant: { default: 'plain', enum: ['plain', 'fancy'] },
848
+ };
849
+ }
850
+
851
+ test('valid values pass on construction and reactive update', () => {
852
+ const comp = new EnumComp({ variant: 'fancy' });
853
+ comp.options.variant = 'plain';
854
+ expect(comp.options.variant).toBe('plain');
855
+ comp.destroy();
856
+ });
857
+
858
+ test('invalid values throw with the valid values listed', () => {
859
+ expect(() => new EnumComp({ variant: 'bogus' })).toThrow(
860
+ '"bogus" is not a valid variant. The variant must be one of the following values: plain, fancy',
861
+ );
862
+ });
863
+
864
+ test('invalid reactive assignment throws', () => {
865
+ const comp = new EnumComp();
866
+ expect(() => (comp.options.variant = 'bogus')).toThrow();
867
+ comp.destroy();
868
+ });
869
+
870
+ test('null and undefined bypass enum validation', () => {
871
+ class OptionalEnumComp extends Component {
872
+ static schema = {
873
+ variant: { enum: ['plain', 'fancy'] },
874
+ };
875
+ }
876
+
877
+ const comp = new OptionalEnumComp({ variant: undefined });
878
+ comp.options.variant = null;
879
+ comp.destroy();
880
+ });
881
+
882
+ test('optionEnum exposes the declared values, nearest class first', () => {
883
+ class Wider extends EnumComp {
884
+ static schema = {
885
+ variant: { enum: ['plain', 'fancy', 'wild'] },
886
+ };
887
+ }
888
+
889
+ const base = new EnumComp();
890
+ const wider = new Wider({ variant: 'wild' });
891
+ expect(base.optionEnum('variant')).toEqual(['plain', 'fancy']);
892
+ expect(wider.optionEnum('variant')).toEqual(['plain', 'fancy', 'wild']);
893
+ expect(base.optionEnum('missing')).toBeUndefined();
894
+ base.destroy();
895
+ wider.destroy();
896
+ });
897
+ });
898
+
899
+ describe('static events', () => {
900
+ test('declared events register via on() and fire via emit()', () => {
901
+ const received = [];
902
+
903
+ class TestComp extends Component {
904
+ static events = ['activate'];
905
+ }
906
+
907
+ const comp = new TestComp({ onActivate: event => received.push(event.detail) });
908
+ comp.emit('activate', 'payload');
909
+ expect(received).toEqual(['payload']);
910
+ comp.destroy();
911
+ });
912
+
913
+ test('subclass events union with parent events instead of replacing', () => {
914
+ class Parent extends Component {
915
+ static events = ['open'];
916
+ }
917
+
918
+ class Child extends Parent {
919
+ static events = ['close'];
920
+ }
921
+
922
+ const received = [];
923
+ const comp = new Child({
924
+ onOpen: () => received.push('open'),
925
+ onClose: () => received.push('close'),
926
+ });
927
+ comp.emit('open');
928
+ comp.emit('close');
929
+ expect(received).toEqual(['open', 'close']);
930
+ comp.destroy();
931
+ });
932
+ });
933
+
934
+ describe('static prepareOptions', () => {
935
+ test('transforms merged options before processing', () => {
936
+ class TestComp extends Component {
937
+ static schema = {
938
+ label: { default: 'raw' },
939
+ };
940
+
941
+ static prepareOptions(options) {
942
+ return { ...options, textContent: `prepared:${options.label}` };
943
+ }
944
+ }
945
+
946
+ const comp = new TestComp();
947
+ expect(comp.elem.textContent).toBe('prepared:raw');
948
+ comp.destroy();
949
+ });
950
+
951
+ test('sees schema defaults from child classes (merged view)', () => {
952
+ const seen = [];
953
+
954
+ class Parent extends Component {
955
+ static prepareOptions(options) {
956
+ seen.push(options.mode);
957
+ return options;
958
+ }
959
+ }
960
+
961
+ class Child extends Parent {
962
+ static schema = {
963
+ mode: { default: 'child-mode', data: true },
964
+ };
965
+ }
966
+
967
+ const comp = new Child();
968
+ expect(seen).toEqual(['child-mode']);
969
+ comp.destroy();
970
+ });
971
+
972
+ test('runs leaf-first so child transforms feed parent hooks', () => {
973
+ const order = [];
974
+
975
+ class Parent extends Component {
976
+ static prepareOptions(options) {
977
+ order.push(`parent:${options.stage}`);
978
+ return options;
979
+ }
980
+ }
981
+
982
+ class Child extends Parent {
983
+ static prepareOptions(options) {
984
+ order.push(`child:${options.stage}`);
985
+ return { ...options, stage: 'transformed' };
986
+ }
987
+ }
988
+
989
+ const comp = new Child({ stage: 'raw', 'data-stage': 'x' });
990
+ expect(order).toEqual(['child:raw', 'parent:transformed']);
991
+ comp.destroy();
992
+ });
993
+
994
+ test('receives constructor children', () => {
995
+ let receivedChildren;
996
+
997
+ class TestComp extends Component {
998
+ static prepareOptions(options, children) {
999
+ receivedChildren = children;
1000
+ return options;
1001
+ }
1002
+ }
1003
+
1004
+ const comp = new TestComp({}, 'child content');
1005
+ expect(receivedChildren).toEqual(['child content']);
619
1006
  comp.destroy();
620
1007
  });
621
1008
  });