@vanilla-bean/components 1.1.0 → 2.0.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 (75) hide show
  1. package/Component/Component.js +226 -40
  2. package/Component/Component.scenarios.js +2 -2
  3. package/Component/Component.test.js +427 -40
  4. package/Component/README.md +480 -0
  5. package/Component/observeElementConnection.js +1 -1
  6. package/Elem/README.md +373 -0
  7. package/README.md +2 -2
  8. package/components/BottomSheet/BottomSheet.js +8 -16
  9. package/components/BottomSheet/README.md +1 -1
  10. package/components/Button/Button.js +12 -18
  11. package/components/Button/Button.lld.md +1 -1
  12. package/components/Calendar/Calendar.js +139 -59
  13. package/components/Calendar/CalendarEvent.js +9 -4
  14. package/components/Calendar/Toolbar.js +28 -20
  15. package/components/Calendar/index.js +1 -0
  16. package/components/Code/Code.js +26 -30
  17. package/components/Code/Code.lld.md +1 -1
  18. package/components/ColorPicker/ColorPicker.js +58 -58
  19. package/components/Dialog/Dialog.js +79 -73
  20. package/components/Dialog/Dialog.lld.md +1 -1
  21. package/components/Dialog/README.md +10 -10
  22. package/components/Form/Form.js +21 -17
  23. package/components/Form/Form.lld.md +2 -2
  24. package/components/Form/README.md +3 -3
  25. package/components/Icon/Icon.js +19 -11
  26. package/components/Input/Input.js +85 -67
  27. package/components/Input/README.md +7 -9
  28. package/components/Keyboard/Key.js +7 -10
  29. package/components/Keyboard/Keyboard.js +38 -46
  30. package/components/Keyboard/Keyboard.lld.md +1 -1
  31. package/components/Label/Label.js +52 -50
  32. package/components/Link/Link.js +14 -20
  33. package/components/List/List.js +28 -30
  34. package/components/Menu/Menu.js +19 -10
  35. package/components/Menu/Menu.lld.md +2 -2
  36. package/components/Notify/Notify.js +29 -19
  37. package/components/Notify/Notify.lld.md +2 -2
  38. package/components/Page/Page.lld.md +1 -1
  39. package/components/Popover/Popover.js +49 -44
  40. package/components/RadioButton/RadioButton.js +33 -29
  41. package/components/RadioButton/RadioButton.lld.md +1 -1
  42. package/components/Router/README.md +12 -12
  43. package/components/Router/Router.js +19 -21
  44. package/components/Select/README.md +5 -5
  45. package/components/Select/Select.js +25 -31
  46. package/components/Table/README.md +4 -4
  47. package/components/Table/Table.js +55 -36
  48. package/components/Table/Table.lld.md +1 -1
  49. package/components/TagList/Tag.js +16 -14
  50. package/components/TagList/TagList.js +15 -8
  51. package/components/TagList/TagList.lld.md +1 -1
  52. package/components/Tooltip/Tooltip.js +12 -25
  53. package/components/TooltipWrapper/TooltipWrapper.js +55 -49
  54. package/components/TooltipWrapper/TooltipWrapper.lld.md +2 -2
  55. package/components/Whiteboard/Whiteboard.js +45 -43
  56. package/components/Whiteboard/Whiteboard.lld.md +1 -1
  57. package/devTools/build.js +43 -0
  58. package/devTools/buildTypes.js +322 -0
  59. package/devTools/createComponent.js +155 -0
  60. package/devTools/extractJSDoc.js +395 -0
  61. package/devTools/processTemplate.js +500 -0
  62. package/devTools/updateComponentIndex.js +16 -0
  63. package/devTools/updateDemoViewIndex.js +90 -0
  64. package/eslint.config.cjs +5 -2
  65. package/index.d.ts +120 -62
  66. package/package.json +23 -22
  67. package/spellcheck.config.cjs +3 -0
  68. package/styled/README.md +329 -0
  69. package/theme/.test.js +2 -2
  70. package/theme/README.md +8 -8
  71. package/theme/colors.js +16 -13
  72. package/theme/colors.test.js +28 -0
  73. package/utils/browser.js +1 -1
  74. package/utils/element.js +2 -2
  75. package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
@@ -24,54 +24,46 @@ const KeyRow = styled(
24
24
  * @returns {Keyboard} Keyboard component instance
25
25
  */
26
26
  class Keyboard extends Component {
27
- constructor(options = {}, ...children) {
28
- super(
29
- {
30
- layout: 'simple',
31
- tactileResponse: true,
32
- keyDefinitions: {
33
- simple: { text: 'ABC' },
34
- number: { text: '123' },
35
- shift: { class: 'u1_5' },
36
- return: { class: 'u1_5' },
37
- backspace: { class: 'u1_5' },
38
- clear: { class: 'u1_5' },
39
- done: { class: 'u1_5' },
40
- exponent: { key: 'e', text: 'e', class: 'u1_5' },
41
- space: { key: ' ', text: ' ', class: 'u6' },
42
- },
43
- layouts: {
44
- simple: [
45
- ['simple', 'number'],
46
- ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
47
- ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
48
- ['shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'backspace'],
49
- [',', '.', 'space', '!', 'return'],
50
- ],
51
- number: [
52
- ['simple', 'number'],
53
- ['1', '2', '3', 'backspace'],
54
- ['4', '5', '6', 'clear'],
55
- ['7', '8', '9', 'done'],
56
- ['.', '0', '-', 'exponent'],
57
- ],
58
- },
59
- ...options,
27
+ static schema = {
28
+ layout: {
29
+ default: 'simple',
30
+ set(value) {
31
+ this.setLayout(value);
32
+ },
33
+ },
34
+ // Read directly from this.options in setLayout() and key press handling
35
+ layouts: {
36
+ default: {
37
+ simple: [
38
+ ['simple', 'number'],
39
+ ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
40
+ ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
41
+ ['shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'backspace'],
42
+ [',', '.', 'space', '!', 'return'],
43
+ ],
44
+ number: [
45
+ ['simple', 'number'],
46
+ ['1', '2', '3', 'backspace'],
47
+ ['4', '5', '6', 'clear'],
48
+ ['7', '8', '9', 'done'],
49
+ ['.', '0', '-', 'exponent'],
50
+ ],
51
+ },
52
+ },
53
+ keyDefinitions: {
54
+ default: {
55
+ simple: { text: 'ABC' },
56
+ number: { text: '123' },
57
+ shift: { class: 'u1_5' },
58
+ return: { class: 'u1_5' },
59
+ backspace: { class: 'u1_5' },
60
+ clear: { class: 'u1_5' },
61
+ done: { class: 'u1_5' },
62
+ exponent: { key: 'e', text: 'e', class: 'u1_5' },
63
+ space: { key: ' ', text: ' ', class: 'u6' },
60
64
  },
61
- ...children,
62
- );
63
-
64
- this.setLayout(this.options.layout);
65
- }
66
-
67
- static handlers = {
68
- // Claim these keys so standard routing doesn't try to assign them as elem properties.
69
- // Both are read directly from this.options in setLayout() and key resolution.
70
- layouts() {},
71
- keyDefinitions() {},
72
- layout(value) {
73
- this.setLayout(value);
74
65
  },
66
+ tactileResponse: { default: true },
75
67
  };
76
68
 
77
69
  // todo native keyboard events https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent
@@ -4,7 +4,7 @@
4
4
 
5
5
  On-screen keyboard where layout switching rebuilds the key DOM rather than showing/hiding rows. The decision: a clean rebuild on layout change is safer than managing per-key visibility across layout transitions; no key from one layout can bleed into another.
6
6
 
7
- ## Layout switch produces only keys from the new layout — no residual keys remain
7
+ ## Layout switch produces only keys from the new layout - no residual keys remain
8
8
 
9
9
  - changing `layout` removes all existing keys and builds the new set from scratch; a key that exists in layout A but not layout B is definitively absent after the switch
10
10
  - does switching layout remove keys from the previous layout?
@@ -120,10 +120,6 @@ const StyledLabel = styled(
120
120
  `,
121
121
  );
122
122
 
123
- const variant_enum = Object.freeze(['overlay', 'collapsible', 'inline', 'inline-after', 'simple']);
124
-
125
- const defaultOptions = { variant: 'simple' };
126
-
127
123
  /**
128
124
  * Label component with multiple display variants and input association.
129
125
  *
@@ -139,20 +135,61 @@ const defaultOptions = { variant: 'simple' };
139
135
  * @returns {Label} Label component instance
140
136
  */
141
137
  class Label extends StyledLabel {
142
- defaultOptions = { ...super.defaultOptions, ...defaultOptions };
143
- variant_enum = variant_enum;
138
+ static schema = {
139
+ variant: {
140
+ default: 'simple',
141
+ enum: ['overlay', 'collapsible', 'inline', 'inline-after', 'simple'],
142
+ set(value) {
143
+ this.removeClass(/\bvariant-\S+\b/g);
144
+ this.addClass(`variant-${value}`);
145
+
146
+ if (this._labelText) {
147
+ const shouldBeAfter = value === 'inline-after';
148
+ const isAfter = this._labelText.elem === this.elem.lastElementChild;
149
+ if (shouldBeAfter !== isAfter) this[shouldBeAfter ? 'append' : 'prepend'](this._labelText);
150
+ if (value === 'collapsible') {
151
+ this._labelText.elem.setAttribute('aria-expanded', this.hasClass('collapsed') ? 'false' : 'true');
152
+ }
153
+ }
154
+ },
155
+ },
156
+ label: {
157
+ set(value) {
158
+ if (typeof value === 'object') this._labelText?.setOptions(value);
159
+ else if (this._labelText) this._labelText.options.content = value;
160
+ },
161
+ },
162
+ collapsed: {
163
+ set(value) {
164
+ this[value ? 'addClass' : 'removeClass']('collapsed');
165
+ if (this.options.variant === 'collapsible' && this._labelText) {
166
+ this._labelText.elem.setAttribute('aria-expanded', value ? 'false' : 'true');
167
+ }
168
+ },
169
+ },
170
+ for: {
171
+ set(value) {
172
+ let forId = typeof value === 'string' ? value : value?.id || value?.elem?.id;
144
173
 
145
- constructor(options = {}, ...children) {
146
- if (typeof options === 'string') options = { label: options };
174
+ if (!forId && value?._component) {
175
+ forId = value.elem.id = value.uniqueId;
176
+ }
147
177
 
148
- super(
149
- {
150
- ...(children.length > 0 && { for: children[0] }),
151
- ...defaultOptions,
152
- ...options,
178
+ if (this._labelText) this._labelText.elem.htmlFor = forId ?? '';
153
179
  },
154
- ...children,
155
- );
180
+ },
181
+ };
182
+
183
+ static prepareOptions(options, children) {
184
+ return {
185
+ ...(children.length > 0 && { for: children[0] }),
186
+ ...options,
187
+ };
188
+ }
189
+
190
+ constructor(options = {}, ...children) {
191
+ // String shorthand must resolve before Component spreads options
192
+ super(typeof options === 'string' ? { label: options } : options, ...children);
156
193
  }
157
194
 
158
195
  build() {
@@ -174,41 +211,6 @@ class Label extends StyledLabel {
174
211
 
175
212
  this[this.options.variant === 'inline-after' ? 'append' : 'prepend'](this._labelText);
176
213
  }
177
-
178
- static handlers = {
179
- label(value) {
180
- if (typeof value === 'object') this._labelText?.setOptions(value);
181
- else if (this._labelText) this._labelText.options.content = value;
182
- },
183
- collapsed(value) {
184
- this[value ? 'addClass' : 'removeClass']('collapsed');
185
- if (this.options.variant === 'collapsible' && this._labelText) {
186
- this._labelText.elem.setAttribute('aria-expanded', value ? 'false' : 'true');
187
- }
188
- },
189
- variant(value) {
190
- this.removeClass(/\bvariant-\S+\b/g);
191
- this.addClass(`variant-${value}`);
192
-
193
- if (this._labelText) {
194
- const shouldBeAfter = value === 'inline-after';
195
- const isAfter = this._labelText.elem === this.elem.lastElementChild;
196
- if (shouldBeAfter !== isAfter) this[shouldBeAfter ? 'append' : 'prepend'](this._labelText);
197
- if (value === 'collapsible') {
198
- this._labelText.elem.setAttribute('aria-expanded', this.hasClass('collapsed') ? 'false' : 'true');
199
- }
200
- }
201
- },
202
- for(value) {
203
- let forId = typeof value === 'string' ? value : value?.id || value?.elem?.id;
204
-
205
- if (!forId && value?._component) {
206
- forId = value.elem.id = value.uniqueId;
207
- }
208
-
209
- if (this._labelText) this._labelText.elem.htmlFor = forId ?? '';
210
- },
211
- };
212
214
  }
213
215
 
214
216
  export default Label;
@@ -1,14 +1,5 @@
1
1
  import { TooltipWrapper } from '../TooltipWrapper';
2
2
 
3
- const defaultOptions = {
4
- tag: 'a',
5
- variant: 'link',
6
- get tooltip() {
7
- return { icon: 'link', style: { fontSize: '12px' } };
8
- },
9
- };
10
- const variant_enum = Object.freeze(['link', 'button']);
11
-
12
3
  /**
13
4
  * Link component with tooltip support and display variants.
14
5
  *
@@ -25,17 +16,20 @@ const variant_enum = Object.freeze(['link', 'button']);
25
16
  * @returns {Link} Link component instance
26
17
  */
27
18
  class Link extends TooltipWrapper {
28
- defaultOptions = { ...super.defaultOptions, ...defaultOptions };
29
- variant_enum = variant_enum;
30
-
31
- constructor(options = {}, ...children) {
32
- super({ ...defaultOptions, ...options }, ...children);
33
- }
34
-
35
- static handlers = {
36
- variant(value) {
37
- this.removeClass(/\bvariant-\S+\b/g);
38
- this.addClass(`variant-${value}`);
19
+ static schema = {
20
+ tag: { default: 'a' },
21
+ variant: {
22
+ default: 'link',
23
+ enum: ['link', 'button'],
24
+ set(value) {
25
+ this.removeClass(/\bvariant-\S+\b/g);
26
+ this.addClass(`variant-${value}`);
27
+ },
28
+ },
29
+ tooltip: {
30
+ get default() {
31
+ return { icon: 'link', style: { fontSize: '12px' } };
32
+ },
39
33
  },
40
34
  };
41
35
  }
@@ -42,41 +42,39 @@ const ListItem = styled(
42
42
  * @returns {List} List component instance
43
43
  */
44
44
  export default class List extends StyledComponent {
45
- constructor(options = {}, ...children) {
46
- super(
47
- {
48
- ...options,
49
- tag: 'ul',
45
+ static schema = {
46
+ tag: { default: 'ul' },
47
+ noStyle: {
48
+ set(value) {
49
+ this.toggleClass('no-style', !!value);
50
50
  },
51
- ...children,
52
- );
53
- }
54
-
55
- static handlers = {
56
- noStyle(value) {
57
- this.toggleClass('no-style', !!value);
58
51
  },
59
- items(value) {
60
- if (!value) {
61
- this.empty();
62
- return;
63
- }
52
+ items: {
53
+ set(value) {
54
+ if (!value) {
55
+ this.empty();
56
+ return;
57
+ }
64
58
 
65
- this.content(
66
- value.map(item => {
67
- const isContent = typeof item === 'string' || item?.elem || Array.isArray(item);
68
- const listItem = new ListItem({ tag: 'li', ...item?.listItemOptions });
59
+ this.content(
60
+ value.map(item => {
61
+ const isContent = typeof item === 'string' || item?.elem || Array.isArray(item);
62
+ const listItem = new ListItem({ tag: 'li', ...item?.listItemOptions });
63
+ // listItemOptions belongs to the li wrapper - strip it before passing item options along.
64
+ const itemOptions = isContent ? item : { ...item };
65
+ if (!isContent) delete itemOptions.listItemOptions;
69
66
 
70
- if (this.options.ListItemComponent) {
71
- listItem.content(new this.options.ListItemComponent(isContent ? { content: item } : item));
72
- } else if (item.ListItemComponent) {
73
- listItem.content(new item.ListItemComponent(item));
74
- } else if (isContent) listItem.content(item);
75
- else listItem.setOptions(item);
67
+ if (this.options.ListItemComponent) {
68
+ listItem.content(new this.options.ListItemComponent(isContent ? { content: item } : itemOptions));
69
+ } else if (item.ListItemComponent) {
70
+ listItem.content(new item.ListItemComponent(itemOptions));
71
+ } else if (isContent) listItem.content(item);
72
+ else listItem.setOptions(itemOptions);
76
73
 
77
- return listItem;
78
- }),
79
- );
74
+ return listItem;
75
+ }),
76
+ );
77
+ },
80
78
  },
81
79
  };
82
80
  }
@@ -37,23 +37,32 @@ const StyledList = styled(
37
37
  * borders, and click handling. Provides consistent menu appearance and behavior.
38
38
  * @param {object} [options={}] - Menu configuration options
39
39
  * @param {Array<*>} [options.items] - Array of menu items to render
40
- * @param {Function} [options.onSelect] - Handler called when menu item is selected
40
+ * @param {Function} [options.onSelect] - Handler for the "select" CustomEvent fired when a menu item is activated; the activation event is in event.detail
41
41
  * @param {Component} [options.ListItemComponent] - Custom component class for rendering menu items
42
42
  * @param {...(Component|HTMLElement|string)} children - Child elements to append
43
43
  * @returns {Menu} Menu component instance
44
44
  */
45
45
  export default class Menu extends StyledList {
46
- static handlers = {
47
- items(value, next) {
48
- next(value);
49
- if (!value) return;
50
- const items = Array.from(this.elem.children).filter(el => el.tagName === 'LI');
51
- items.forEach((li, i) => li.setAttribute('tabindex', i === 0 ? '0' : '-1'));
46
+ static schema = {
47
+ role: { default: 'menu' },
48
+ items: {
49
+ set(value, next) {
50
+ next(value);
51
+ if (!value) return;
52
+ const items = Array.from(this.elem.children).filter(el => el.tagName === 'LI');
53
+ items.forEach((li, i) => li.setAttribute('tabindex', i === 0 ? '0' : '-1'));
54
+ },
52
55
  },
53
56
  };
54
57
 
55
- constructor(options = {}, ...children) {
56
- super({ role: 'menu', ...options, onPointerPress: options.onSelect }, ...children);
58
+ static events = ['select'];
59
+
60
+ build() {
61
+ this.on({
62
+ targetEvent: 'pointerdown',
63
+ id: 'menuSelect',
64
+ callback: event => this.emit('select', event),
65
+ });
57
66
 
58
67
  this.on({
59
68
  targetEvent: 'keydown',
@@ -78,7 +87,7 @@ export default class Menu extends StyledList {
78
87
  next = items[items.length - 1];
79
88
  } else if ((e.key === 'Enter' || e.key === ' ') && currentIndex >= 0) {
80
89
  e.preventDefault();
81
- this.options.onSelect?.(e);
90
+ this.emit('select', e);
82
91
  return;
83
92
  }
84
93
 
@@ -2,11 +2,11 @@
2
2
 
3
3
  > ./Menu.js
4
4
 
5
- Styled list whose items trigger an `onSelect` callback. The design decision: Menu routes selection through the same unified pointer+keyboard activation path as Button; keyboard and pointer users get the same experience without separate handlers.
5
+ Styled list whose items emit a registered `select` CustomEvent, with the activation event in `detail`. The design decision: Menu routes selection through the same unified pointer+keyboard activation path as Button; keyboard and pointer users get the same experience without separate handlers. `onSelect` is the option-form listener for the event.
6
6
 
7
7
  ## Selecting a menu item works the same way regardless of input method
8
8
 
9
- - clicking, touching, Space/Enter, and screen reader activation all invoke `onSelect`; one handler, no separate keyboard path
9
+ - clicking, touching, Space/Enter, and screen reader activation all emit `select`; one handler, no separate keyboard path
10
10
  - does clicking a menu item invoke onSelect?
11
11
 
12
12
  ## Menu renders any item format that List supports
@@ -1,8 +1,6 @@
1
1
  import { styled } from '../../styled';
2
2
  import { Popover } from '../Popover';
3
3
 
4
- const type_enum = Object.freeze(['info', 'success', 'warning', 'error']);
5
-
6
4
  const StyledPopover = styled(
7
5
  Popover,
8
6
  ({ colors, fonts }) => `
@@ -68,29 +66,41 @@ const StyledPopover = styled(
68
66
  * @returns {Notify} Notify component instance
69
67
  */
70
68
  export default class Notify extends StyledPopover {
71
- type_enum = type_enum;
69
+ static schema = {
70
+ type: { default: 'success', enum: ['info', 'success', 'warning', 'error'] },
71
+ timeout: {
72
+ set(value) {
73
+ clearTimeout(this.timeout);
74
+ if (!value) return;
75
+ this.timeout = setTimeout(() => this.destroy(), value);
76
+ this.replaceCleanup('timeout', () => clearTimeout(this.timeout));
77
+ },
78
+ },
79
+ };
72
80
 
73
- constructor(options = {}) {
74
- const { timeout, type = 'success' } = options;
75
- const icon =
76
- options.icon ||
77
- { info: 'circle-info', success: 'check', warning: 'triangle-exclamation', error: 'skull-crossbones' }[type];
81
+ static prepareOptions(options) {
82
+ const { type } = options;
78
83
 
79
- super({
84
+ return {
80
85
  role: type === 'error' || type === 'warning' ? 'alert' : 'status',
81
86
  'aria-atomic': 'true',
82
- onPointerPress: e => {
83
- if (!e.target.closest('button, [role="button"]')) this.destroy();
84
- },
85
- state: 'manual',
86
87
  ...options,
87
88
  addClass: [type].concat(options.addClass),
88
- icon,
89
- });
89
+ icon:
90
+ options.icon ||
91
+ { info: 'circle-info', success: 'check', warning: 'triangle-exclamation', error: 'skull-crossbones' }[type],
92
+ };
93
+ }
90
94
 
91
- if (timeout) {
92
- this.timeout = setTimeout(() => this.destroy(), timeout);
93
- this.addCleanup('timeout', () => clearTimeout(this.timeout));
94
- }
95
+ build() {
96
+ super.build();
97
+
98
+ this.on({
99
+ targetEvent: 'pointerdown',
100
+ id: 'notifyDismiss',
101
+ callback: e => {
102
+ if (!e.target.closest('button, [role="button"]')) this.destroy();
103
+ },
104
+ });
95
105
  }
96
106
  }
@@ -9,12 +9,12 @@ Transient notification that self-destructs. The design choice is that click-to-d
9
9
  - callers pass `type: 'error'` and the component picks the matching icon; a custom `icon` option overrides this only when the standard mapping doesn't fit
10
10
  - does a success notification use a visually distinct icon from an error notification?
11
11
 
12
- ## Clicking anywhere on the notification dismisses it — the whole surface is the target
12
+ ## Clicking anywhere on the notification dismisses it - the whole surface is the target
13
13
 
14
14
  - notifications are temporary and should be easy to clear; there is no separate close button
15
15
  - does clicking the notification body remove it from the page?
16
16
 
17
- ## Manual dismiss cancels a pending timeout — no duplicate destroy fires
17
+ ## Manual dismiss cancels a pending timeout - no duplicate destroy fires
18
18
 
19
19
  - when a `timeout` is set and the user dismisses manually before it fires, the timer is cancelled so a second destroy call after the component is already gone cannot happen
20
20
  - does manually dismissing a notification before its timeout prevent a second dismiss?
@@ -4,7 +4,7 @@
4
4
 
5
5
  Top-level layout component that takes ownership of stylesheet loading. The design decision: Page loads FontAwesome and typography as part of mounting; application code does not import or configure these separately.
6
6
 
7
- ## Required stylesheets load once — remounting does not re-fetch
7
+ ## Required stylesheets load once - remounting does not re-fetch
8
8
 
9
9
  - Page checks whether each stylesheet is already present before injecting; remounting in a single-page app does not duplicate link elements or re-trigger network requests
10
10
  - does mounting a Page a second time leave the stylesheet count unchanged?
@@ -18,19 +18,6 @@ const StyledIcon = styled(
18
18
  `,
19
19
  );
20
20
 
21
- const defaultOptions = {
22
- uniqueId: true,
23
- state: 'manual',
24
- outsideClose: false,
25
- get viewport() {
26
- return document.documentElement;
27
- },
28
- get appendTo() {
29
- return document.body;
30
- },
31
- };
32
- const state_enum = Object.freeze(['auto', 'manual']);
33
-
34
21
  /**
35
22
  * Popover component using native HTML popover API with edge-aware positioning.
36
23
  *
@@ -50,45 +37,63 @@ const state_enum = Object.freeze(['auto', 'manual']);
50
37
  * @returns {Popover} Popover component instance
51
38
  */
52
39
  export default class Popover extends StyledIcon {
53
- defaultOptions = { ...super.defaultOptions, ...defaultOptions };
54
- state_enum = state_enum;
55
-
56
- constructor({ autoOpen = true, onConnected: userOnConnected, ...options } = {}, ...children) {
57
- super(
58
- {
59
- ...defaultOptions,
60
- onConnected: () => {
61
- if (autoOpen) {
62
- const timeoutId = setTimeout(() => this.show(), 200);
63
- this.replaceCleanup('autoOpen', () => clearTimeout(timeoutId));
64
- }
65
- userOnConnected?.();
66
- },
67
- ...options,
40
+ static schema = {
41
+ uniqueId: { default: true },
42
+ state: {
43
+ default: 'manual',
44
+ enum: ['auto', 'manual'],
45
+ set(value) {
46
+ this.elem.popover = value;
47
+ },
48
+ },
49
+ autoOpen: { default: true },
50
+ // Positioning and dismissal config read from this.options by show()/edgeAwarePlacement()
51
+ outsideClose: { default: false },
52
+ viewport: {
53
+ get default() {
54
+ return document.documentElement;
55
+ },
56
+ },
57
+ appendTo: {
58
+ get default() {
59
+ return document.body;
60
+ },
61
+ },
62
+ maxWidth: { default: 264 },
63
+ maxHeight: { default: 132 },
64
+ x: {
65
+ set() {
66
+ this._placeAtPoint();
67
+ },
68
+ },
69
+ y: {
70
+ set() {
71
+ this._placeAtPoint();
68
72
  },
69
- ...children,
70
- );
73
+ },
74
+ };
71
75
 
72
- if (this.options.x !== undefined && this.options.y !== undefined) this.edgeAwarePlacement(this.options);
76
+ build() {
77
+ this.on({
78
+ targetEvent: 'connected',
79
+ id: 'popoverAutoOpen',
80
+ callback: () => {
81
+ if (!this.options.autoOpen) return;
82
+ const timeoutId = setTimeout(() => this.show(), 200);
83
+ this.replaceCleanup('autoOpen', () => clearTimeout(timeoutId));
84
+ },
85
+ });
73
86
  }
74
87
 
75
- static handlers = {
76
- state(value) {
77
- this.elem.popover = value;
78
- },
79
- outsideClose() {},
80
- viewport() {},
81
- maxWidth() {},
82
- maxHeight() {},
83
- x() {},
84
- y() {},
85
- };
88
+ _placeAtPoint() {
89
+ if (this.options.x !== undefined && this.options.y !== undefined) this.edgeAwarePlacement(this.options);
90
+ }
86
91
 
87
92
  edgeAwarePlacement({
88
93
  x,
89
94
  y,
90
- maxHeight = this.options.maxHeight ?? 132,
91
- maxWidth = this.options.maxWidth ?? 264,
95
+ maxHeight = this.options.maxHeight,
96
+ maxWidth = this.options.maxWidth,
92
97
  padding = 24,
93
98
  viewport = this.options.viewport || this.options.appendTo,
94
99
  }) {