@vanilla-bean/components 2.0.4 → 2.0.5

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.
@@ -611,19 +611,20 @@ class Component extends Elem {
611
611
 
612
612
  /**
613
613
  * Destroys this component and all children. Runs all cleanup functions,
614
- * clears registries, and removes from DOM.
614
+ * clears registries, and removes from DOM. Removal from the document is a disconnect: components listening for
615
+ * one (onDisconnected) hear it here, once, with no event detail.
615
616
  */
616
617
  destroy() {
617
- this.elemObserver?.disconnect();
618
-
619
- // Disconnect descendant elemObservers before running cleanup
620
- const disconnectDescendantObservers = children => {
621
- children.forEach(child => {
622
- child.elemObserver?.disconnect();
623
- disconnectDescendantObservers(child.children);
624
- });
625
- };
626
- disconnectDescendantObservers(this.children);
618
+ // Innermost first: a component's own disconnect runs its descendants' cleanup, which takes their listeners away
619
+ const watching = [
620
+ ...Array.from(this.elem?.querySelectorAll('*') ?? [], node => node._component).reverse(),
621
+ this,
622
+ ].filter(component => component?.elemObserver);
623
+ const wasConnected = Boolean(this.elem?.isConnected);
624
+
625
+ // Their observers are let go first, so the removal below reports nothing and this is the only disconnect
626
+ for (const component of watching) component.elemObserver.disconnect();
627
+ if (wasConnected) for (const component of watching) component.emit('disconnected');
627
628
 
628
629
  this.processCleanup(this.cleanup, true);
629
630
  if (this._destroyCleanup) this.processCleanup(this._destroyCleanup);
@@ -1047,6 +1047,47 @@ describe('Component', () => {
1047
1047
  });
1048
1048
  });
1049
1049
 
1050
+ describe('destroy', () => {
1051
+ const settle = () => new Promise(resolve => setTimeout(resolve, 0));
1052
+
1053
+ test('a component in the document hears its own removal once', async () => {
1054
+ const onDisconnected = mock();
1055
+ const comp = new Component({ appendTo: document.body, onDisconnected });
1056
+
1057
+ comp.destroy();
1058
+ await settle();
1059
+
1060
+ expect(onDisconnected).toHaveBeenCalledTimes(1);
1061
+ });
1062
+
1063
+ test('components inside it hear it too, nested in plain elements or not', async () => {
1064
+ const parent = new Component({ appendTo: document.body });
1065
+ const wrapper = document.createElement('div');
1066
+ const direct = mock();
1067
+ const nested = mock();
1068
+
1069
+ new Component({ appendTo: parent, onDisconnected: direct });
1070
+ parent.elem.append(wrapper);
1071
+ new Component({ appendTo: wrapper, onDisconnected: nested });
1072
+
1073
+ parent.destroy();
1074
+ await settle();
1075
+
1076
+ expect(direct).toHaveBeenCalledTimes(1);
1077
+ expect(nested).toHaveBeenCalledTimes(1);
1078
+ });
1079
+
1080
+ test('one never in the document hears nothing', async () => {
1081
+ const onDisconnected = mock();
1082
+ const comp = new Component({ autoRender: false, onDisconnected });
1083
+
1084
+ comp.destroy();
1085
+ await settle();
1086
+
1087
+ expect(onDisconnected).not.toHaveBeenCalled();
1088
+ });
1089
+ });
1090
+
1050
1091
  describe('_setOption routing', () => {
1051
1092
  test('aria-* routes to setAttribute', () => {
1052
1093
  component = new Component({ 'aria-label': 'test label' });
@@ -440,7 +440,7 @@ component.addCleanup(id, fn); // Register cleanup function (chains with existing
440
440
  component.replaceCleanup(id, fn); // Replace cleanup, running the previous one immediately
441
441
  component.replaceDestroyCleanup(id, fn); // Destroy-only cleanup: survives disconnect, runs on destroy()
442
442
  component.processCleanup(); // Execute all cleanup functions
443
- component.destroy(); // Disconnect observer, run all cleanup, remove from DOM
443
+ component.destroy(); // Remove from DOM: onDisconnected fires (innermost first), then all cleanup runs
444
444
  ```
445
445
 
446
446
  #### Event Methods
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanilla-bean/components",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
4
4
  "type": "module",
5
5
  "module": "./index.js",
6
6
  "types": "./index.d.ts",
@@ -6,6 +6,8 @@ module.exports = {
6
6
  templates: true,
7
7
  lang: 'en_US',
8
8
  skipWords: [
9
+ 'samp',
10
+ 'tt',
9
11
  'bezier',
10
12
  'ecma',
11
13
  'compat',
@@ -2,18 +2,28 @@
2
2
 
3
3
  import { Component } from '../Component';
4
4
  import theme from '../theme';
5
+ import * as utils from '../utils';
5
6
  import { styled, configured } from './styled';
6
7
 
7
8
  const mockShimCSS = mock(() => {});
8
9
  mock.module('./shimCSS', () => ({ shimCSS: mockShimCSS }));
9
10
 
10
- const mockNanoid = mock(() => 'test-id-1234567890');
11
- mock.module('../utils', () => ({ classSafeNanoid: mockNanoid }));
11
+ // bun's module mocks outlive this file, so the rest of utils passes through and the real id generator comes back
12
+ // once these tests are done
13
+ const realNanoid = utils.classSafeNanoid;
14
+ const testNanoid = () => 'test-id-1234567890';
15
+ const mockNanoid = mock(testNanoid);
16
+ mock.module('../utils', () => ({ ...utils, classSafeNanoid: mockNanoid }));
12
17
 
13
18
  describe('styled system', () => {
14
19
  beforeEach(() => {
15
20
  mockShimCSS.mockClear();
16
21
  mockNanoid.mockClear();
22
+ mockNanoid.mockImplementation(testNanoid);
23
+ });
24
+
25
+ afterAll(() => {
26
+ mockNanoid.mockImplementation(realNanoid);
17
27
  });
18
28
 
19
29
  describe('configured()', () => {
package/theme/page.js CHANGED
@@ -10,6 +10,8 @@ export default theme => `
10
10
  html {
11
11
  height: 100%;
12
12
  color-scheme: ${colors.black.isDark() ? 'dark' : 'light'};
13
+ font-size: 16px;
14
+ ${fonts.kodeMono}
13
15
  }
14
16
 
15
17
  body {
@@ -34,13 +36,17 @@ export default theme => `
34
36
  * {
35
37
  touch-action: manipulation;
36
38
  -webkit-text-size-adjust: none;
39
+ }
37
40
 
38
- font-size: 16px;
39
- ${fonts.kodeMono}
41
+ /* The page's font is inherited, so content that sets its own (a terminal, an embedded editor) keeps it. These are
42
+ the elements the browser gives a font or size of their own. */
43
+ :where(h1, h2, h3, h4, h5, h6, small, big, code, kbd, pre, samp, tt, var, button, input, select, textarea, optgroup, option, legend) {
44
+ font-family: inherit;
45
+ font-size: inherit;
46
+ }
40
47
 
41
- sub, sup {
42
- font-size: smaller;
43
- }
48
+ :where(sub, sup) {
49
+ font-size: smaller;
44
50
  }
45
51
 
46
52
  #app {