@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
@@ -11,19 +11,7 @@ const DialogButton = styled(
11
11
  `,
12
12
  );
13
13
 
14
- const size_enum = Object.freeze(['small', 'standard', 'large']);
15
- const variant_enum = Object.freeze(['info', 'success', 'warning', 'error']);
16
-
17
- const defaultOptions = {
18
- tag: 'dialog',
19
- size: 'small',
20
- openOnRender: 16,
21
- modal: true,
22
- get appendTo() {
23
- return document.body;
24
- },
25
- registeredEvents: new Set(['close']),
26
- };
14
+ const defaultOpenDelay = 16;
27
15
 
28
16
  /**
29
17
  * Modal and non-modal dialog component with customizable appearance and behavior.
@@ -149,57 +137,70 @@ class Dialog extends styled(
149
137
  }
150
138
  `,
151
139
  ) {
152
- variant_enum = variant_enum;
153
- size_enum = size_enum;
154
-
155
- static handlers = {
156
- openOnRender(value) {
157
- const delay = typeof value === 'number' ? value : defaultOptions.openOnRender;
158
- const openDelay = value ? setTimeout(() => this.open(), delay) : null;
159
- this.replaceCleanup('openOnRender', () => openDelay && clearTimeout(openDelay));
140
+ static schema = {
141
+ tag: { default: 'dialog' },
142
+ appendTo: {
143
+ get default() {
144
+ return document.body;
145
+ },
160
146
  },
161
- // Claim the key so standard routing doesn't try to call elem.showModal() or set elem.modal.
162
- // The value is read directly from this.options.modal in open().
163
- modal() {},
164
- size(value) {
165
- if (value && !size_enum.includes(value)) {
166
- throw new Error(
167
- `"${value}" is not a valid size. The size must be one of the following values: ${size_enum.join(', ')}`,
168
- );
169
- }
170
-
171
- this.removeClass(/\bsize-\S+\b/g);
172
-
173
- if (value) this.addClass(`size-${value}`);
147
+ openOnRender: {
148
+ default: defaultOpenDelay,
149
+ set(value) {
150
+ const delay = typeof value === 'number' ? value : defaultOpenDelay;
151
+ const openDelay = value ? setTimeout(() => this.open(), delay) : null;
152
+ this.replaceCleanup('openOnRender', () => openDelay && clearTimeout(openDelay));
153
+ },
174
154
  },
175
- variant(value) {
176
- if (value && !variant_enum.includes(value)) {
177
- throw new Error(
178
- `"${value}" is not a valid variant. The variant must be one of the following values: ${variant_enum.join(', ')}`,
179
- );
180
- }
181
-
182
- this.removeClass(/\bvariant-\S+\b/g);
155
+ // Read directly from this.options.modal in open()
156
+ modal: { default: true },
157
+ size: {
158
+ default: 'small',
159
+ enum: ['small', 'standard', 'large'],
160
+ set(value) {
161
+ this.removeClass(/\bsize-\S+\b/g);
162
+
163
+ if (value) this.addClass(`size-${value}`);
164
+ },
165
+ },
166
+ variant: {
167
+ enum: ['info', 'success', 'warning', 'error'],
168
+ set(value) {
169
+ this.removeClass(/\bvariant-\S+\b/g);
183
170
 
184
- if (value) this.addClass(`variant-${value}`);
171
+ if (value) this.addClass(`variant-${value}`);
172
+ },
173
+ },
174
+ body: {
175
+ set(value) {
176
+ this.body?.content(value);
177
+ },
178
+ },
179
+ header: {
180
+ set(value) {
181
+ this._header?.content(value);
182
+ },
185
183
  },
186
- body(value) {
187
- this.body?.content(value);
184
+ buttons: {
185
+ set() {
186
+ this._refreshFooter();
187
+ },
188
188
  },
189
- header(value) {
190
- this._header?.content(value);
189
+ footer: {
190
+ set() {
191
+ this._refreshFooter();
192
+ },
191
193
  },
194
+ // Read from this.options at button-press time
195
+ closeDialog: {},
196
+ onButtonPress: {},
192
197
  };
193
198
 
194
- defaultOptions = { ...super.defaultOptions, ...defaultOptions };
195
-
196
- constructor(options = {}, ...children) {
197
- super({ ...defaultOptions, ...options }, ...children);
199
+ static events = ['close'];
198
200
 
201
+ build() {
199
202
  this.options['aria-labelledby'] = this.uniqueId;
200
- }
201
203
 
202
- build() {
203
204
  this._header = new Elem({
204
205
  tag: 'div',
205
206
  id: this.uniqueId,
@@ -210,29 +211,34 @@ class Dialog extends styled(
210
211
 
211
212
  this._body = new Elem({ addClass: ['content'], appendTo: this });
212
213
 
213
- this._footer = new Elem({
214
- addClass: ['footer'],
215
- append:
216
- this.options.footer ||
217
- this.options.buttons?.map(
218
- button =>
219
- new DialogButton({
220
- onPointerPress: event =>
221
- this.options.onButtonPress?.({
222
- event,
223
- button,
224
- dialog: this,
225
- closeDialog: this.options.closeDialog || (() => this.close()),
226
- }),
227
- ...(typeof button === 'object' ? button : { textContent: button }),
228
- }),
229
- ),
230
- appendTo: this,
231
- });
214
+ this._footer = new Elem({ addClass: ['footer'], append: this._footerContent(), appendTo: this });
215
+ }
216
+
217
+ _refreshFooter() {
218
+ if (this.rendered) this._footer?.content(this._footerContent() || []);
219
+ }
220
+
221
+ _footerContent() {
222
+ return (
223
+ this.options.footer ||
224
+ this.options.buttons?.map(
225
+ button =>
226
+ new DialogButton({
227
+ onPointerPress: event =>
228
+ this.options.onButtonPress?.({
229
+ event,
230
+ button,
231
+ dialog: this,
232
+ closeDialog: this.options.closeDialog || (() => this.close()),
233
+ }),
234
+ ...(typeof button === 'object' ? button : { textContent: button }),
235
+ }),
236
+ )
237
+ );
232
238
  }
233
239
 
234
240
  /**
235
- * Content container — primary injection point for subclass and consumer content.
241
+ * Content container - primary injection point for subclass and consumer content.
236
242
  * @returns {Component} The dialog body component
237
243
  */
238
244
  get body() {
@@ -14,7 +14,7 @@ Native `<dialog>` element wrapper. The decision to use the native element rather
14
14
  - an unrecognized size or variant throws at the point of assignment, not later when layout breaks
15
15
  - does an unrecognized size value throw?
16
16
 
17
- ## Header, body, and footer are stable references — options update them in place
17
+ ## Header, body, and footer are stable references - options update them in place
18
18
 
19
19
  - the structural elements are created once in `build()` and persist across option updates; assigning a new `header` value changes the header's content, not the dialog's structure
20
20
  - does updating the header option change the header's content without replacing the dialog structure?
@@ -22,25 +22,25 @@ const dialog = new Dialog({
22
22
 
23
23
  | Option | Type | Default | Description |
24
24
  | --- | --- | --- | --- |
25
- | `header` | `string` | — | Text rendered in the `<h2>` header |
26
- | `body` | `string\|Component\|Array` | — | Content for the scrollable body section. Reactive: updating `options.body` replaces content |
27
- | `buttons` | `Array<string\|object>` | — | Shorthand footer buttons. Each entry is a string label or `{ textContent, variant?, onPointerPress?, ...buttonOptions }` |
28
- | `footer` | `Array<Component>` | — | Fully custom footer components. Takes precedence over `buttons` |
29
- | `onButtonPress` | `Function` | — | Called when any `buttons` entry is pressed. Receives `{ button, closeDialog, event }` |
25
+ | `header` | `string` | - | Text rendered in the `<h2>` header |
26
+ | `body` | `string\|Component\|Array` | - | Content for the scrollable body section. Reactive: updating `options.body` replaces content |
27
+ | `buttons` | `Array<string\|object>` | - | Shorthand footer buttons. Each entry is a string label or `{ textContent, variant?, onPointerPress?, ...buttonOptions }` |
28
+ | `footer` | `Array<Component>` | - | Fully custom footer components. Takes precedence over `buttons` |
29
+ | `onButtonPress` | `Function` | - | Called when any `buttons` entry is pressed. Receives `{ button, closeDialog, event }` |
30
30
  | `openOnRender` | `number\|boolean` | `16` | Auto-open delay in ms after render. Set to `false` to disable auto-open |
31
31
  | `modal` | `boolean` | `true` | Open as a modal (with backdrop) vs. non-modal |
32
32
  | `size` | `string` | `'small'` | Dialog dimensions: `'small'` (420×210px), `'standard'` (840×420px), `'large'` (90vw×90vh) |
33
- | `variant` | `string` | — | Color theme: `'info'`, `'success'`, `'warning'`, `'error'` |
34
- | `closeDialog` | `Function` | — | Override the default close behavior used inside `onButtonPress` |
33
+ | `variant` | `string` | - | Color theme: `'info'`, `'success'`, `'warning'`, `'error'` |
34
+ | `closeDialog` | `Function` | - | Override the default close behavior used inside `onButtonPress` |
35
35
  | `appendTo` | `Element` | `document.body` | Where to append the dialog in the DOM |
36
36
 
37
37
  ### `onButtonPress` callback signature
38
38
 
39
39
  ```js
40
40
  onButtonPress: ({ button, closeDialog, event }) => { ... }
41
- // button — the original entry from the buttons array (string or object)
42
- // closeDialog — function that closes the dialog (calls dialog.close() by default)
43
- // event — the pointer event that triggered the press
41
+ // button - the original entry from the buttons array (string or object)
42
+ // closeDialog - function that closes the dialog (calls dialog.close() by default)
43
+ // event - the pointer event that triggered the press
44
44
  ```
45
45
 
46
46
  ## Methods
@@ -14,18 +14,18 @@ import { Label } from '../Label';
14
14
  * **Field definition:**
15
15
  * ```js
16
16
  * {
17
- * key: 'name', // required — data key
18
- * label: 'Full Name', // optional — defaults to formatted key
19
- * InputComponent: Select, // optional — defaults to Input
20
- * onChange: event => {}, // optional — called after data update
21
- * parse: value => value.trim(), // optional — transform before storing
22
- * condition: data => data.type === 'x', // optional — hide field when false
23
- * validate: value => value ? null : 'Required', // optional — field-level validator
17
+ * key: 'name', // required - data key
18
+ * label: 'Full Name', // optional - defaults to formatted key
19
+ * InputComponent: Select, // optional - defaults to Input
20
+ * onChange: event => {}, // optional - called after data update
21
+ * parse: value => value.trim(), // optional - transform before storing
22
+ * condition: data => data.type === 'x', // optional - hide field when false
23
+ * validate: value => value ? null : 'Required', // optional - field-level validator
24
24
  * // ...any other InputComponent options
25
25
  * }
26
26
  * ```
27
27
  *
28
- * **Group definition** — renders inputs side-by-side in a layout container:
28
+ * **Group definition** - renders inputs side-by-side in a layout container:
29
29
  * ```js
30
30
  * {
31
31
  * type: 'group',
@@ -65,15 +65,19 @@ export default class Form extends Component {
65
65
  * Rebuild the form when inputs change without recreating the data store.
66
66
  * Existing data is preserved; new keys are added with their initial values.
67
67
  */
68
- static handlers = {
69
- inputs(value) {
70
- if (!this.rendered || !value || !this.options.data) return;
71
- this.empty();
72
- this._resetState();
73
- this._processInputs(value, this);
74
- this._wireConditions();
75
- this._setupAnnouncer();
68
+ static schema = {
69
+ inputs: {
70
+ set(value) {
71
+ if (!this.rendered || !value || !this.options.data) return;
72
+ this.empty();
73
+ this._resetState();
74
+ this._processInputs(value, this);
75
+ this._wireConditions();
76
+ this._setupAnnouncer();
77
+ },
76
78
  },
79
+ // Data store managed by build()
80
+ data: {},
77
81
  };
78
82
 
79
83
  /** @private */
@@ -91,7 +95,7 @@ export default class Form extends Component {
91
95
  if (!this._conditionals.length) return;
92
96
  const evaluate = () => this._evaluateConditions();
93
97
  this.options.data.addEventListener('set', evaluate);
94
- // replaceCleanup — safe to call multiple times (inputs handler re-wires on rebuild)
98
+ // replaceCleanup - safe to call multiple times (inputs handler re-wires on rebuild)
95
99
  this.replaceCleanup('conditions', () => this.options.data.removeEventListener('set', evaluate));
96
100
  }
97
101
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  Declarative form from an array of input configurations. The key design decision is that the form owns data binding. Each input's changes update a shared reactive `data` context, and `hasErrors()` validates the whole form in one call.
6
6
 
7
- ## Label text falls back to the field key — no separate label required for obvious fields
7
+ ## Label text falls back to the field key - no separate label required for obvious fields
8
8
 
9
9
  - if an input config omits `label`, the key name becomes the label
10
10
  - does an input with no label option display the key name as its label?
@@ -15,7 +15,7 @@ Declarative form from an array of input configurations. The key design decision
15
15
  - does hasErrors return false when all inputs are valid?
16
16
  - does hasErrors return true when an input's validation fails?
17
17
 
18
- ## Form data is live — changes are visible before any submit action
18
+ ## Form data is live - changes are visible before any submit action
19
19
 
20
20
  - each input's value feeds the shared `data` context as it changes
21
21
  - does typing in an input field update the form's data context before any submit?
@@ -20,7 +20,7 @@ const form = new Form({
20
20
 
21
21
  | Option | Type | Default | Description |
22
22
  | -------- | --------------- | ------- | ----------------------------------------------------------------------- |
23
- | `inputs` | `Array<object>` | — | Input configuration array; see shape below |
23
+ | `inputs` | `Array<object>` | - | Input configuration array; see shape below |
24
24
  | `data` | `object` | `{}` | Initial form data. Replaced with an `Oxject` instance on each `build()` |
25
25
 
26
26
  ### `inputs` entry shape
@@ -32,8 +32,8 @@ const form = new Form({
32
32
  | `InputComponent` | `Component class` | `Input` | Component class to instantiate. Can be `Select`, `ColorPicker`, or any component that accepts `value` and `onChange` |
33
33
  | `onChange` | `Function` | `() => {}` | Called after the data context is updated with the new value |
34
34
  | `parse` | `Function(value, input)` | `v => v` | Transforms the raw event value before storing it in `options.data` |
35
- | `validations` | `Array` | — | Passed directly to the `InputComponent` (see Input validation format) |
36
- | `...inputOptions` | `any` | — | Any additional options forwarded to the `InputComponent` constructor |
35
+ | `validations` | `Array` | - | Passed directly to the `InputComponent` (see Input validation format) |
36
+ | `...inputOptions` | `any` | - | Any additional options forwarded to the `InputComponent` constructor |
37
37
 
38
38
  ### `data` Oxject lifecycle
39
39
 
@@ -14,20 +14,28 @@ import { Component } from '../../Component';
14
14
  * @returns {Icon} Icon component instance
15
15
  */
16
16
  export default class Icon extends Component {
17
- static handlers = {
18
- icon() {
19
- this._refreshIcon();
17
+ static schema = {
18
+ icon: {
19
+ set() {
20
+ this._refreshIcon();
21
+ },
20
22
  },
21
- animation() {
22
- this._refreshIcon();
23
+ animation: {
24
+ set() {
25
+ this._refreshIcon();
26
+ },
23
27
  },
24
- content(value, next) {
25
- next(value);
26
- this._refreshIcon();
28
+ content: {
29
+ set(value, next) {
30
+ next(value);
31
+ this._refreshIcon();
32
+ },
27
33
  },
28
- textContent(value, next) {
29
- next(value);
30
- this._refreshIcon();
34
+ textContent: {
35
+ set(value, next) {
36
+ next(value);
37
+ this._refreshIcon();
38
+ },
31
39
  },
32
40
  };
33
41
 
@@ -26,13 +26,11 @@ const type_enum = Object.freeze([
26
26
  'url',
27
27
  'week',
28
28
  ]);
29
- const defaultOptions = {
30
- tag: 'input',
31
- value: '',
29
+ // Only meaningful on input/textarea elements - child classes rendering other tags (e.g. ColorPicker's div) must not receive them
30
+ const textInputDefaults = {
32
31
  autocomplete: 'off',
33
32
  autocapitalize: 'off',
34
33
  autocorrect: 'off',
35
- height: 'auto',
36
34
  placeholder: '',
37
35
  };
38
36
 
@@ -59,19 +57,91 @@ const dataTypeToInputType = { number: 'number', boolean: 'checkbox', string: 'te
59
57
  * @returns {Input} Input component instance
60
58
  */
61
59
  export default class Input extends Component {
62
- defaultOptions = { ...super.defaultOptions, ...defaultOptions };
63
- type_enum = type_enum;
60
+ static schema = {
61
+ tag: { default: 'input' },
62
+ type: { enum: type_enum },
63
+ height: {
64
+ default: 'auto',
65
+ set(value) {
66
+ if (this.tag !== 'textarea') return;
67
+
68
+ if (value === 'auto') {
69
+ this.__updateAutoHeight = () => {
70
+ this.elem.style.height = this.options.syntaxHighlighting
71
+ ? `calc(${((this.elem.value?.match(/\n/g) || '').length + 1) * 1.19}em + 10px)`
72
+ : `calc(${((this.elem.value?.match(/\n/g) || '').length + 1) * 1.25}em + 16px)`;
73
+ };
74
+
75
+ this.__debouncedUpdateAutoHeight =
76
+ this.__debouncedUpdateAutoHeight ||
77
+ (() => {
78
+ clearTimeout(this.__autoHeightTimeout);
79
+ this.__autoHeightTimeout = setTimeout(this.__updateAutoHeight, 16);
80
+ });
81
+
82
+ this.__updateAutoHeight();
83
+
84
+ this.elem.addEventListener('input', this.__debouncedUpdateAutoHeight);
85
+
86
+ this.replaceCleanup('autoHeight', () => {
87
+ clearTimeout(this.__autoHeightTimeout);
88
+ this.elem.removeEventListener?.('input', this.__debouncedUpdateAutoHeight);
89
+ });
90
+ } else {
91
+ this.elem.removeEventListener('input', this.__debouncedUpdateAutoHeight);
92
+ clearTimeout(this.__autoHeightTimeout);
64
93
 
65
- constructor(options = {}, ...children) {
66
- const rawValue = options.value?.__isDerived ? options.value.toJSON() : options.value;
67
- super(
68
- {
69
- ...((options.tag || 'input') === 'input' ? { type: dataTypeToInputType[typeof rawValue] || 'text' } : {}),
70
- ...defaultOptions,
71
- ...options,
94
+ this.elem.style.height = typeof value === 'number' ? `${value + 1}em` : value;
95
+ }
96
+ },
97
+ },
98
+ value: {
99
+ default: '',
100
+ set(value) {
101
+ if (this.options.type === 'checkbox') this.elem.checked = value;
102
+ else this.elem.value = value;
103
+
104
+ if (this.rendered && this.options.validations?.length) retry(() => this.validate(), { delay: 500, max: 1 });
105
+ },
106
+ },
107
+ syntaxHighlighting: {
108
+ set(value) {
109
+ this[value ? 'addClass' : 'removeClass']('syntax-highlighting');
72
110
  },
73
- ...children,
74
- );
111
+ },
112
+ language: {
113
+ set(value) {
114
+ if (!this.options.syntaxHighlighting) return;
115
+ this.removeClass(/\blanguage-\S+\b/g);
116
+ this.addClass(`language-${value}`);
117
+ },
118
+ },
119
+ autocorrect: {
120
+ set(value) {
121
+ this.elem.autocorrect = value;
122
+ },
123
+ },
124
+ // Read by validate() and the value handler
125
+ validations: {
126
+ get default() {
127
+ return [];
128
+ },
129
+ },
130
+ };
131
+
132
+ static prepareOptions(options) {
133
+ const rawValue = options.value?.__isDerived ? options.value.toJSON() : options.value;
134
+ const tag = options.tag || 'input';
135
+
136
+ return {
137
+ ...(tag === 'input' ? { type: dataTypeToInputType[typeof rawValue] || 'text' } : {}),
138
+ ...(tag === 'input' || tag === 'textarea' ? textInputDefaults : {}),
139
+ ...options,
140
+ };
141
+ }
142
+
143
+ constructor(options = {}, ...children) {
144
+ super(options, ...children);
75
145
 
76
146
  this.initialValue = this.options.value;
77
147
  }
@@ -93,58 +163,6 @@ export default class Input extends Component {
93
163
  }
94
164
  }
95
165
 
96
- static handlers = {
97
- height(value) {
98
- if (this.tag !== 'textarea') return;
99
-
100
- if (value === 'auto') {
101
- this.__updateAutoHeight = () => {
102
- this.elem.style.height = this.options.syntaxHighlighting
103
- ? `calc(${((this.elem.value?.match(/\n/g) || '').length + 1) * 1.19}em + 10px)`
104
- : `calc(${((this.elem.value?.match(/\n/g) || '').length + 1) * 1.25}em + 16px)`;
105
- };
106
-
107
- this.__debouncedUpdateAutoHeight =
108
- this.__debouncedUpdateAutoHeight ||
109
- (() => {
110
- clearTimeout(this.__autoHeightTimeout);
111
- this.__autoHeightTimeout = setTimeout(this.__updateAutoHeight, 16);
112
- });
113
-
114
- this.__updateAutoHeight();
115
-
116
- this.elem.addEventListener('input', this.__debouncedUpdateAutoHeight);
117
-
118
- this.replaceCleanup('autoHeight', () => {
119
- clearTimeout(this.__autoHeightTimeout);
120
- this.elem.removeEventListener?.('input', this.__debouncedUpdateAutoHeight);
121
- });
122
- } else {
123
- this.elem.removeEventListener('input', this.__debouncedUpdateAutoHeight);
124
- clearTimeout(this.__autoHeightTimeout);
125
-
126
- this.elem.style.height = typeof value === 'number' ? `${value + 1}em` : value;
127
- }
128
- },
129
- value(value) {
130
- if (this.options.type === 'checkbox') this.elem.checked = value;
131
- else this.elem.value = value;
132
-
133
- if (this.rendered && this.options.validations?.length) retry(() => this.validate(), { delay: 500, max: 1 });
134
- },
135
- syntaxHighlighting(value) {
136
- this[value ? 'addClass' : 'removeClass']('syntax-highlighting');
137
- },
138
- language(value) {
139
- if (!this.options.syntaxHighlighting) return;
140
- this.removeClass(/\blanguage-\S+\b/g);
141
- this.addClass(`language-${value}`);
142
- },
143
- autocorrect(value) {
144
- this.elem.autocorrect = value;
145
- },
146
- };
147
-
148
166
  /**
149
167
  * Checks if the input value has changed from its initial value
150
168
  * @returns {boolean} True if value is no longer the initial value
@@ -27,12 +27,12 @@ const input = new Input({
27
27
  | `autocapitalize` | `string` | `'off'` | Native autocapitalize attribute |
28
28
  | `autocorrect` | `string` | `'off'` | Native autocorrect attribute |
29
29
  | `height` | `string\|number` | `'auto'` | Height for textarea. `'auto'` dynamically sizes to content; a number sets `em` units |
30
- | `syntaxHighlighting` | `boolean` | — | Adds `syntaxHighlighting` class and Tab key indentation handling (textarea only) |
31
- | `language` | `string` | — | Adds `language-<value>` class for Prism-compatible highlighting (requires `syntaxHighlighting`) |
32
- | `validations` | `Array` | — | Validation rules; see Validation section below |
33
- | `onInput` | `Function` | — | Called on every keystroke with an enhanced event containing `.value` |
34
- | `onChange` | `Function` | — | Called on change with an enhanced event containing `.value` |
35
- | `onKeyUp` | `Function` | — | Called on keyup with an enhanced event containing `.value` |
30
+ | `syntaxHighlighting` | `boolean` | - | Adds `syntaxHighlighting` class and Tab key indentation handling (textarea only) |
31
+ | `language` | `string` | - | Adds `language-<value>` class for Prism-compatible highlighting (requires `syntaxHighlighting`) |
32
+ | `validations` | `Array` | - | Validation rules; see Validation section below |
33
+ | `onInput` | `Function` | - | Called on every keystroke with an enhanced event containing `.value` |
34
+ | `onChange` | `Function` | - | Called on change with an enhanced event containing `.value` |
35
+ | `onKeyUp` | `Function` | - | Called on keyup with an enhanced event containing `.value` |
36
36
 
37
37
  ### Supported `type` values
38
38
 
@@ -62,15 +62,13 @@ input.validate({ validations?, value? }): Array | undefined
62
62
  // Runs validation rules. Returns array of error messages if invalid, undefined if valid.
63
63
  // Calling with no args uses options.validations and options.value.
64
64
 
65
- input.isDirty: boolean // getter — true if current value differs from the initial value
65
+ input.isDirty: boolean // getter - true if current value differs from the initial value
66
66
  ```
67
67
 
68
68
  ## Events
69
69
 
70
70
  Input components emit enhanced native events. All handlers (`onInput`, `onChange`, `onKeyUp`, etc.) receive a standard DOM event augmented with a `.value` property containing the current input value.
71
71
 
72
- The `close` event is registered by default via `registeredEvents`.
73
-
74
72
  ## Example
75
73
 
76
74
  Textarea with auto-height and syntax highlighting:
@@ -21,17 +21,14 @@ const StyledButton = styled(
21
21
  );
22
22
 
23
23
  class Key extends StyledButton {
24
- constructor(options = {}, ...children) {
25
- const { key, text, class: keyClass, ...optionsWithoutConfig } = options;
24
+ static prepareOptions(options) {
25
+ const { key, text, class: keyClass, ...rest } = options;
26
26
 
27
- super(
28
- {
29
- textContent: text ?? key,
30
- ...optionsWithoutConfig,
31
- addClass: [key, keyClass].concat(options.addClass),
32
- },
33
- ...children,
34
- );
27
+ return {
28
+ textContent: text ?? key,
29
+ ...rest,
30
+ addClass: [key, keyClass].concat(options.addClass),
31
+ };
35
32
  }
36
33
  }
37
34