@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
@@ -69,38 +69,42 @@ const RadioButtonInput = styled(
69
69
  * @returns {RadioButton} RadioButton component instance
70
70
  */
71
71
  class RadioButton extends Component {
72
- static handlers = {
73
- options(value) {
74
- this.empty();
72
+ static schema = {
73
+ options: {
74
+ set(value) {
75
+ this.empty();
75
76
 
76
- if (!value) return;
77
+ if (!value) return;
77
78
 
78
- this.append(
79
- value.map(
80
- option =>
81
- new RadioButtonLabel({
82
- tag: 'label',
83
- append: [
84
- new RadioButtonInput({
85
- tag: 'input',
86
- type: 'radio',
87
- value: option?.value || option,
88
- name: this.uniqueId,
89
- checked: this.options.value === (option?.value || option),
90
- onChange: event => {
91
- if (event.target.checked) this.options.value = event.target.value;
92
- },
93
- }),
94
- option?.label || option,
95
- ],
96
- }),
97
- ),
98
- );
79
+ this.append(
80
+ value.map(
81
+ option =>
82
+ new RadioButtonLabel({
83
+ tag: 'label',
84
+ append: [
85
+ new RadioButtonInput({
86
+ tag: 'input',
87
+ type: 'radio',
88
+ value: option?.value || option,
89
+ name: this.uniqueId,
90
+ checked: this.options.value === (option?.value || option),
91
+ onChange: event => {
92
+ if (event.target.checked) this.options.value = event.target.value;
93
+ },
94
+ }),
95
+ option?.label || option,
96
+ ],
97
+ }),
98
+ ),
99
+ );
100
+ },
99
101
  },
100
- value(value) {
101
- this.elem.querySelectorAll('input[type="radio"]').forEach(input => {
102
- input.checked = input.value === String(value);
103
- });
102
+ value: {
103
+ set(value) {
104
+ this.elem.querySelectorAll('input[type="radio"]').forEach(input => {
105
+ input.checked = input.value === String(value);
106
+ });
107
+ },
104
108
  },
105
109
  };
106
110
  }
@@ -4,7 +4,7 @@
4
4
 
5
5
  Radio group from an array of options. The design decision: the HTML `name` coordination that makes radios mutually exclusive is handled automatically; callers pass values and get a working group without managing name attributes.
6
6
 
7
- ## All radios in the group share one name — mutual exclusivity is provided by the browser
7
+ ## All radios in the group share one name - mutual exclusivity is provided by the browser
8
8
 
9
9
  - the component generates a shared `name` attribute; the browser enforces that only one radio in the group can be checked at a time
10
10
  - does each radio input in the group share the same name attribute?
@@ -9,7 +9,7 @@ import { Router, View, Page } from '@vanilla-bean/components';
9
9
 
10
10
  class HomeView extends View {
11
11
  build() {
12
- // render view content here — no Page needed, the shell provides it
12
+ // render view content here - no Page needed, the shell provides it
13
13
  }
14
14
  }
15
15
 
@@ -43,20 +43,20 @@ new Router({
43
43
 
44
44
  | Option | Type | Default | Description |
45
45
  | --- | --- | --- | --- |
46
- | `views` | `object` | — | **Required.** Maps route patterns to component classes. Patterns may contain `:param` segments. |
46
+ | `views` | `object` | - | **Required.** Maps route patterns to component classes. Patterns may contain `:param` segments. |
47
47
  | `defaultPath` | `string` | First key of `views` | Route used when the hash is empty and for fallback on unmatched routes. |
48
- | `notFound` | `typeof Component` | — | Component class rendered when no route matches and `defaultPath` fallback also fails. Receives `{ route }` as an option. |
49
- | `onRenderView` | `Function` | — | Called with the matched route string whenever a new view is rendered. |
48
+ | `notFound` | `typeof Component` | - | Component class rendered when no route matches and `defaultPath` fallback also fails. Receives `{ route }` as an option. |
49
+ | `onRenderView` | `Function` | - | Called with the matched route string whenever a new view is rendered. |
50
50
  | `mode` | `'hash' \| 'history'` | `'hash'` | `'hash'` uses URL fragments (`#/path`). `'history'` uses `pushState` and real pathnames (`/path`). History mode requires the server to serve `index.html` for all routes. |
51
51
 
52
52
  ## Properties
53
53
 
54
54
  ```js
55
- router.path; // string — current URL hash, stripped of '#/', '/', and query strings
55
+ router.path; // string - current URL hash, stripped of '#/', '/', and query strings
56
56
  router.path = '/users/42'; // navigate; sets window.location.hash and re-renders
57
57
 
58
- router.route; // string — the matched route pattern for the current path (e.g. '/users/:id')
59
- router.currentRoute; // string — the last successfully rendered route pattern
58
+ router.route; // string - the matched route pattern for the current path (e.g. '/users/:id')
59
+ router.currentRoute; // string - the last successfully rendered route pattern
60
60
  ```
61
61
 
62
62
  ## Methods
@@ -77,9 +77,9 @@ Routes are matched using exact string equality first, then by replacing `:param`
77
77
 
78
78
  ```js
79
79
  views: {
80
- '/users/new': NewUserView, // exact match — always wins
81
- '/users/:id': UserView, // one param — tested before /:section/:id
82
- '/:section/:id': SectionView, // two params — tested last
80
+ '/users/new': NewUserView, // exact match - always wins
81
+ '/users/:id': UserView, // one param - tested before /:section/:id
82
+ '/:section/:id': SectionView, // two params - tested last
83
83
  }
84
84
  ```
85
85
 
@@ -121,7 +121,7 @@ class SettingsView extends View {
121
121
  }
122
122
  ```
123
123
 
124
- ## Example — full SPA shell
124
+ ## Example - full SPA shell
125
125
 
126
126
  ```js
127
127
  import { Router, View, Nav, NavItem, Page } from '@vanilla-bean/components';
@@ -138,7 +138,7 @@ class ArticleView extends View {
138
138
  }
139
139
  }
140
140
 
141
- // Page is the app shell — Router and Nav live inside it, not inside each view
141
+ // Page is the app shell - Router and Nav live inside it, not inside each view
142
142
  const page = new Page({ title: 'My App', appendTo: document.body });
143
143
 
144
144
  const router = new Router({
@@ -16,20 +16,26 @@ import { routeToRegex } from './utils';
16
16
  * @returns {Router} Router component instance
17
17
  */
18
18
  class Router extends Component {
19
- constructor(options = {}, ...children) {
20
- super(
21
- {
22
- defaultPath: Object.keys(options.views)[0],
23
- ...options,
24
- style: {
25
- display: 'flex',
26
- flex: 1,
27
- overflow: 'hidden',
28
- ...options.style,
29
- },
19
+ // Routing config read from this.options by the path getters and renderView()
20
+ static schema = {
21
+ views: {},
22
+ defaultPath: {},
23
+ notFound: {},
24
+ onRenderView: {},
25
+ mode: { default: 'hash', enum: ['hash', 'history'] },
26
+ };
27
+
28
+ static prepareOptions(options) {
29
+ return {
30
+ defaultPath: Object.keys(options.views || {})[0],
31
+ ...options,
32
+ style: {
33
+ display: 'flex',
34
+ flex: 1,
35
+ overflow: 'hidden',
36
+ ...options.style,
30
37
  },
31
- ...children,
32
- );
38
+ };
33
39
  }
34
40
 
35
41
  /**
@@ -102,14 +108,6 @@ class Router extends Component {
102
108
  return parameters;
103
109
  }
104
110
 
105
- static handlers = {
106
- views() {},
107
- defaultPath() {},
108
- notFound() {},
109
- onRenderView() {},
110
- mode() {},
111
- };
112
-
113
111
  build() {
114
112
  const reRenderView = () => this.renderView();
115
113
 
@@ -20,9 +20,9 @@ Select inherits all options from `Input`. Key additions:
20
20
 
21
21
  | Option | Type | Default | Description |
22
22
  | --- | --- | --- | --- |
23
- | `options` | `Array<string\|object>` | — | List of selectable options. See shape below. Reactive: reassigning `options.options` rebuilds the `<option>` elements |
24
- | `value` | `any` | — | Currently selected value. Matched against option `value` attributes |
25
- | `onChange` | `Function` | — | Called with an enhanced event containing `.value` (the selected value string) on change |
23
+ | `options` | `Array<string\|object>` | - | List of selectable options. See shape below. Reactive: reassigning `options.options` rebuilds the `<option>` elements |
24
+ | `value` | `any` | - | Currently selected value. Matched against option `value` attributes |
25
+ | `onChange` | `Function` | - | Called with an enhanced event containing `.value` (the selected value string) on change |
26
26
 
27
27
  All other `Input` options (`placeholder`, `validations`, `onInput`, etc.) are available but less commonly used with Select.
28
28
 
@@ -46,8 +46,8 @@ options: ['plain string', { label: 'Display Text', value: 42 }, { label: 'Unavai
46
46
  ## Methods
47
47
 
48
48
  ```js
49
- select.value // getter — returns value, label, or textContent of the selected <option>
50
- select.value = newVal // setter — sets elem.value directly
49
+ select.value // getter - returns value, label, or textContent of the selected <option>
50
+ select.value = newVal // setter - sets elem.value directly
51
51
 
52
52
  // Inherited from Input:
53
53
  select.validate({ validations?, value? }): Array | undefined
@@ -1,13 +1,6 @@
1
1
  import { Elem } from '../../Elem';
2
2
  import { Input } from '../Input';
3
3
 
4
- const defaultOptions = {
5
- tag: 'select',
6
- get priorityOptions() {
7
- return new Set(['textContent', 'content', 'appendTo', 'prependTo', 'options']);
8
- },
9
- };
10
-
11
4
  /**
12
5
  * Select dropdown component extending Input with dynamic option management.
13
6
  *
@@ -21,33 +14,34 @@ const defaultOptions = {
21
14
  * @returns {Select} Select component instance
22
15
  */
23
16
  class Select extends Input {
24
- defaultOptions = { ...super.defaultOptions, ...defaultOptions };
25
-
26
- constructor(options = {}, ...children) {
27
- super({ ...defaultOptions, ...options }, ...children);
28
- }
17
+ static schema = {
18
+ tag: { default: 'select' },
19
+ autocomplete: { default: 'off' },
20
+ options: {
21
+ set(value) {
22
+ this.empty();
29
23
 
30
- static handlers = {
31
- options(value) {
32
- this.empty();
24
+ if (!value) return;
33
25
 
34
- if (!value) return;
35
-
36
- for (const option of value) {
37
- // Optgroup: { label: 'Group', options: [...] }
38
- if (typeof option === 'object' && Array.isArray(option.options)) {
39
- const group = document.createElement('optgroup');
40
- if (option.label) group.label = option.label;
41
- for (const o of option.options) {
42
- group.append(new Elem({ tag: 'option', ...(typeof o === 'object' ? o : { label: o, value: o }) }).elem);
26
+ for (const option of value) {
27
+ // Optgroup: { label: 'Group', options: [...] }
28
+ if (typeof option === 'object' && Array.isArray(option.options)) {
29
+ const group = document.createElement('optgroup');
30
+ if (option.label) group.label = option.label;
31
+ for (const o of option.options) {
32
+ group.append(new Elem({ tag: 'option', ...(typeof o === 'object' ? o : { label: o, value: o }) }).elem);
33
+ }
34
+ this.elem.append(group);
35
+ } else {
36
+ this.append(
37
+ new Elem({ tag: 'option', ...(typeof option === 'object' ? option : { label: option, value: option }) }),
38
+ );
43
39
  }
44
- this.elem.append(group);
45
- } else {
46
- this.append(
47
- new Elem({ tag: 'option', ...(typeof option === 'object' ? option : { label: option, value: option }) }),
48
- );
49
40
  }
50
- }
41
+
42
+ // Re-apply the current value - option elements may not have existed when value was processed
43
+ if (this.options.value !== undefined) this.elem.value = this.options.value;
44
+ },
51
45
  },
52
46
  };
53
47
 
@@ -56,7 +50,7 @@ class Select extends Input {
56
50
  * @returns {*} Selected option value, label, or text content
57
51
  */
58
52
  get value() {
59
- // Use elem.options (HTMLOptionsCollection) — works across optgroups
53
+ // Use elem.options (HTMLOptionsCollection) - works across optgroups
60
54
  const selected = Array.from(this.elem.options).find(({ selected }) => selected);
61
55
 
62
56
  return selected?.value ?? selected?.label ?? selected?.textContent ?? this.elem.value;
@@ -20,11 +20,11 @@ const table = new Table({
20
20
 
21
21
  | Option | Type | Default | Description |
22
22
  | --- | --- | --- | --- |
23
- | `data` | `Array<object>` | — | Array of row data objects. Reactive: reassigning `options.data` rebuilds the tbody |
23
+ | `data` | `Array<object>` | - | Array of row data objects. Reactive: reassigning `options.data` rebuilds the tbody |
24
24
  | `columns` | `Array<string\|object>` | `[]` | Column definitions; see shape below |
25
- | `footer` | `Array<string\|object>` | — | Footer row cells, same shape as columns. Strings are capitalized automatically |
26
- | `sortProperty` | `string` | — | Key of the currently sorted column |
27
- | `sortDirection` | `string` | — | `'asc'` or `'desc'` |
25
+ | `footer` | `Array<string\|object>` | - | Footer row cells, same shape as columns. Strings are capitalized automatically |
26
+ | `sortProperty` | `string` | - | Key of the currently sorted column |
27
+ | `sortDirection` | `string` | - | `'asc'` or `'desc'` |
28
28
  | `onSort` | `Function` | built-in | Called with `(property, direction)` when a sortable column header is clicked. Default sorts `options.data` in place using `orderBy` |
29
29
 
30
30
  ### `columns` entry shape
@@ -4,8 +4,6 @@ import { Elem } from '../../Elem';
4
4
  import { Component } from '../../Component';
5
5
  import { Icon } from '../Icon';
6
6
 
7
- const defaultOptions = { tag: 'table' };
8
-
9
7
  /**
10
8
  * Data table component with sorting, column configuration, and footer support.
11
9
  *
@@ -23,37 +21,55 @@ const defaultOptions = { tag: 'table' };
23
21
  * @returns {Table} Table component instance
24
22
  */
25
23
  class Table extends Component {
26
- constructor(options = {}, ...children) {
27
- super(
28
- {
29
- ...defaultOptions,
30
- onSort: (property, direction) => {
31
- this.options.data = this.options.data.sort(orderBy({ property, direction }));
32
- },
33
- ...options,
34
- columns: (options.columns || []).map(column =>
35
- typeof column === 'string' ? { key: column, content: capitalize(column) } : column,
36
- ),
37
- footer: options.footer?.map(column => (typeof column === 'string' ? { content: capitalize(column) } : column)),
24
+ static schema = {
25
+ tag: { default: 'table' },
26
+ sortDirection: {
27
+ enum: ['asc', 'desc'],
28
+ set(value) {
29
+ if (!this.rendered) return;
30
+ if (this.options.onSort) this.options.onSort(this.options.sortProperty, value);
31
+ else
32
+ this.options.data = this.options.data.sort(
33
+ orderBy({ property: this.options.sortProperty, direction: value }),
34
+ );
38
35
  },
39
- ...children,
40
- );
41
- }
42
-
43
- static handlers = {
44
- sortDirection(value) {
45
- if (this.rendered) this.options.onSort(this.options.sortProperty, value);
46
36
  },
47
- data() {
48
- this._renderTable();
37
+ data: {
38
+ set() {
39
+ this._renderTable();
40
+ },
49
41
  },
50
- selection() {
51
- this._renderTable();
42
+ selection: {
43
+ set() {
44
+ this._renderTable();
45
+ },
52
46
  },
47
+ columns: {
48
+ set() {
49
+ if (this.rendered) this._renderTable();
50
+ },
51
+ },
52
+ footer: {
53
+ set() {
54
+ if (this.rendered) this._renderTable();
55
+ },
56
+ },
57
+ // Read from this.options at sort time; sortProperty changes are always followed
58
+ // by a sortDirection change, which drives the re-render
59
+ sortProperty: {},
60
+ onSort: {},
53
61
  };
54
62
 
55
63
  _renderTable() {
56
64
  const value = this.options.data;
65
+ // Columns and footer accept string shorthand - normalized at read time so
66
+ // construction and reactive assignment take the same path
67
+ const columns = (this.options.columns || []).map(column =>
68
+ typeof column === 'string' ? { key: column, content: capitalize(column) } : column,
69
+ );
70
+ const footer = this.options.footer?.map(column =>
71
+ typeof column === 'string' ? { content: capitalize(column) } : column,
72
+ );
57
73
 
58
74
  this._sortSubscribers?.forEach(sub => sub.destroy?.());
59
75
  this._sortSubscribers = null;
@@ -75,19 +91,22 @@ class Table extends Component {
75
91
  this.thead.append(
76
92
  new Component(
77
93
  { tag: 'tr' },
78
- this.options.columns.map(column => {
79
- const th = new Component({ tag: 'th', ...column, scope: 'col' });
94
+ columns.map(column => {
95
+ // key/sort/dataColumn are column config for the Table itself - keep them off the th elem
96
+ const { key, sort, ...columnOptions } = column;
97
+ delete columnOptions.dataColumn;
98
+ const th = new Component({ tag: 'th', ...columnOptions, scope: 'col' });
80
99
 
81
- if (column.sort) {
100
+ if (sort) {
82
101
  const iconSub = this.options.subscriber('sortDirection', () => {
83
- if (this.options.sortProperty !== column.key) return 'sort';
102
+ if (this.options.sortProperty !== key) return 'sort';
84
103
 
85
104
  return this.options.sortDirection === 'asc' ? 'sort-down' : 'sort-up';
86
105
  });
87
106
  const styleSub = this.options.subscriber('sortDirection', () => ({
88
107
  display: 'inline',
89
108
  marginLeft: '6px',
90
- ...(this.options.sortProperty !== column.key
109
+ ...(this.options.sortProperty !== key
91
110
  ? { color: theme.colors.dark(theme.colors.gray) }
92
111
  : { color: theme.colors.white }),
93
112
  }));
@@ -96,7 +115,7 @@ class Table extends Component {
96
115
 
97
116
  const updateAriaSort = () => {
98
117
  let ariaSort = 'none';
99
- if (this.options.sortProperty === column.key) {
118
+ if (this.options.sortProperty === key) {
100
119
  ariaSort = this.options.sortDirection === 'asc' ? 'ascending' : 'descending';
101
120
  }
102
121
  th.elem.setAttribute('aria-sort', ariaSort);
@@ -121,12 +140,12 @@ class Table extends Component {
121
140
  });
122
141
 
123
142
  th.onPointerPress(() => {
124
- if (this.options.sortProperty === column.key) {
143
+ if (this.options.sortProperty === key) {
125
144
  this.options.sortDirection = this.options.sortDirection === 'asc' ? 'desc' : 'asc';
126
145
  return;
127
146
  }
128
147
 
129
- this.options.sortProperty = column.key;
148
+ this.options.sortProperty = key;
130
149
  this.options.sortDirection = 'desc';
131
150
  });
132
151
  }
@@ -141,7 +160,7 @@ class Table extends Component {
141
160
  rowData =>
142
161
  new Component(
143
162
  { tag: 'tr' },
144
- this.options.columns.map(
163
+ columns.map(
145
164
  column =>
146
165
  new Component(
147
166
  {
@@ -157,11 +176,11 @@ class Table extends Component {
157
176
  ),
158
177
  );
159
178
 
160
- if (this.options.footer) {
179
+ if (footer) {
161
180
  this.tfoot.append(
162
181
  new Component(
163
182
  { tag: 'tr' },
164
- this.options.footer.map(footData => new Component({ tag: 'td', ...footData })),
183
+ footer.map(footData => new Component({ tag: 'td', ...footData })),
165
184
  ),
166
185
  );
167
186
  }
@@ -4,7 +4,7 @@
4
4
 
5
5
  Sortable data table where sort state is explicit options. The design decision: `sortProperty` and `sortDirection` are readable and writable options; the sort state is in the component's options object, not hidden inside event handlers, so it's readable and settable from outside.
6
6
 
7
- ## Sort state lives in options — externally readable and settable
7
+ ## Sort state lives in options - externally readable and settable
8
8
 
9
9
  - clicking a column header updates `sortProperty` and `sortDirection` as regular options; external code can read or set sort state without querying the DOM
10
10
  - does clicking a sortable column update sortProperty to that column's key?
@@ -52,14 +52,23 @@ const StyledComponent = styled(
52
52
  `,
53
53
  );
54
54
 
55
- const defaultOptions = { tag: 'li', readOnly: false };
56
-
57
55
  class Tag extends StyledComponent {
58
- defaultOptions = { ...super.defaultOptions, ...defaultOptions };
59
-
60
- constructor(options = {}, ...children) {
61
- super({ tabindex: '0', ...defaultOptions, ...options }, ...children);
62
- }
56
+ static schema = {
57
+ tag: { default: 'li' },
58
+ tabIndex: { default: 0 },
59
+ readOnly: {
60
+ default: false,
61
+ set(value) {
62
+ this.toggleClass('readOnly', !!value);
63
+ },
64
+ },
65
+ textContent: {
66
+ set(value) {
67
+ this.elem.setAttribute('data-value', value);
68
+ this._textSpan.elem.textContent = value;
69
+ },
70
+ },
71
+ };
63
72
 
64
73
  build() {
65
74
  this._textSpan = new Elem({ tag: 'span', appendTo: this });
@@ -72,13 +81,6 @@ class Tag extends StyledComponent {
72
81
  });
73
82
  }
74
83
  }
75
-
76
- static handlers = {
77
- textContent(value) {
78
- this.elem.setAttribute('data-value', value);
79
- this._textSpan.elem.textContent = value;
80
- },
81
- };
82
84
  }
83
85
 
84
86
  export default Tag;
@@ -32,8 +32,6 @@ const TagListInput = styled(
32
32
  `,
33
33
  );
34
34
 
35
- const defaultOptions = { readOnly: false };
36
-
37
35
  /**
38
36
  * Interactive tag list component with add/remove functionality and input support.
39
37
  *
@@ -47,11 +45,20 @@ const defaultOptions = { readOnly: false };
47
45
  * @returns {TagList} TagList component instance
48
46
  */
49
47
  class TagList extends StyledComponent {
50
- defaultOptions = { ...super.defaultOptions, ...defaultOptions };
51
-
52
- constructor(options = {}, ...children) {
53
- super({ ...options, tag: 'ul' }, ...children);
54
- }
48
+ static schema = {
49
+ tag: { default: 'ul' },
50
+ readOnly: {
51
+ default: false,
52
+ set(value) {
53
+ this.toggleClass('readOnly', !!value);
54
+ },
55
+ },
56
+ tags: {
57
+ get default() {
58
+ return [];
59
+ },
60
+ },
61
+ };
55
62
 
56
63
  _addTag(value) {
57
64
  value = (value || '').trim();
@@ -91,7 +98,7 @@ class TagList extends StyledComponent {
91
98
  }
92
99
 
93
100
  this.append([
94
- ...(this.options.tags || []).map(textContent => new Tag({ readOnly: this.options.readOnly, textContent })),
101
+ ...this.options.tags.map(textContent => new Tag({ readOnly: this.options.readOnly, textContent })),
95
102
  ...(this.addTag ? [this.addTag] : []),
96
103
  ]);
97
104
  }
@@ -10,7 +10,7 @@ Editable tag collection. The key decision: read-only mode removes the editing in
10
10
  - does a readOnly TagList contain no input element?
11
11
  - does a non-readOnly TagList contain both an input and an add button?
12
12
 
13
- ## Duplicate tags are silently rejected — adding them does nothing
13
+ ## Duplicate tags are silently rejected - adding them does nothing
14
14
 
15
15
  **method:** `duplicateRejected`
16
16
 
@@ -80,8 +80,6 @@ const StyledPopover = styled(
80
80
  `,
81
81
  );
82
82
 
83
- const defaultOptions = { position: 'topRight', autoOpen: false };
84
-
85
83
  const position_enum = Object.freeze([
86
84
  'center',
87
85
  'top',
@@ -108,32 +106,21 @@ const position_enum = Object.freeze([
108
106
  * @returns {Tooltip} Tooltip component instance
109
107
  */
110
108
  class Tooltip extends StyledPopover {
111
- position_enum = position_enum;
112
- defaultOptions = { ...super.defaultOptions, ...defaultOptions };
113
-
114
- constructor(options = {}, ...children) {
115
- super(
116
- {
117
- ...defaultOptions,
118
- ...options,
119
- addClass: ['tooltip'].concat(options.addClass),
109
+ static schema = {
110
+ autoOpen: { default: false },
111
+ position: {
112
+ default: 'topRight',
113
+ enum: position_enum,
114
+ set(value) {
115
+ this.removeClass(...position_enum);
116
+ this.addClass(value);
120
117
  },
121
- ...children,
122
- );
123
- }
124
-
125
- static handlers = {
126
- position(value) {
127
- if (!position_enum.includes(value)) {
128
- throw new Error(
129
- `"${value}" is not a valid position. The position must be one of the following values: ${position_enum.join(', ')}`,
130
- );
131
- }
132
-
133
- this.removeClass(...position_enum);
134
- this.addClass(value);
135
118
  },
136
119
  };
120
+
121
+ static prepareOptions(options) {
122
+ return { ...options, addClass: ['tooltip'].concat(options.addClass) };
123
+ }
137
124
  }
138
125
 
139
126
  export default Tooltip;