mtrl 0.2.0 → 0.2.2

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.
@@ -1,6 +1,7 @@
1
- // src/components/chip/chip.ts
2
- import { pipe } from '../../core/compose';
3
- import { createBase, withElement } from '../../core/compose/component';
1
+ // src/components/chip/chip.js
2
+ import { PREFIX } from '../../core/config'
3
+ import { pipe } from '../../core/compose'
4
+ import { createBase, withElement } from '../../core/compose/component'
4
5
  import {
5
6
  withEvents,
6
7
  withText,
@@ -10,72 +11,383 @@ import {
10
11
  withRipple,
11
12
  withDisabled,
12
13
  withLifecycle
13
- } from '../../core/compose/features';
14
- import { withAPI } from './api';
15
- import { ChipConfig, ChipComponent, BaseComponent } from './types';
16
- import { createBaseConfig, getElementConfig, getApiConfig } from './config';
14
+ } from '../../core/compose/features'
15
+ import { withAPI } from './api'
16
+ import { CHIP_VARIANTS, CHIP_SIZES } from './constants'
17
17
 
18
18
  /**
19
19
  * Creates a new Chip component
20
- * @param {ChipConfig} config - Chip configuration object
21
- * @returns {ChipComponent} Chip component instance
20
+ * @param {Object} config - Chip configuration
21
+ * @param {string} [config.variant='filled'] - Chip variant
22
+ * @param {string} [config.size='medium'] - Chip size
23
+ * @param {boolean} [config.selected=false] - Whether the chip is initially selected
24
+ * @param {boolean} [config.disabled=false] - Whether the chip is initially disabled
25
+ * @param {string} [config.text] - Chip text content
26
+ * @param {string} [config.leadingIcon] - Leading icon HTML content
27
+ * @param {string} [config.trailingIcon] - Trailing icon HTML content
28
+ * @param {string} [config.class] - Additional CSS classes
29
+ * @param {string} [config.value] - Chip value
30
+ * @param {boolean} [config.ripple=true] - Whether to enable ripple effect
31
+ * @param {Function} [config.onTrailingIconClick] - Callback when trailing icon is clicked
32
+ * @param {Function} [config.onSelect] - Callback when chip is selected
33
+ * @param {Function} [config.onChange] - Callback when chip selection changes
34
+ * @returns {Object} Chip component instance
22
35
  */
23
- const createChip = (config: ChipConfig = {}): ChipComponent => {
24
- const baseConfig = createBaseConfig(config);
36
+ const createChip = (config = {}) => {
37
+ const baseConfig = {
38
+ ...config,
39
+ variant: config.variant || CHIP_VARIANTS.FILLED,
40
+ size: config.size || CHIP_SIZES.MEDIUM,
41
+ componentName: 'chip',
42
+ prefix: PREFIX,
43
+ ripple: config.ripple !== false
44
+ }
25
45
 
26
46
  try {
47
+ // Create base component with core features
27
48
  const chip = pipe(
28
49
  createBase,
29
50
  withEvents(),
30
- withElement(getElementConfig(baseConfig)),
31
- withVariant(baseConfig),
32
- withSize(baseConfig),
33
- withText(baseConfig),
34
- withIcon({
35
- ...baseConfig,
36
- position: 'start',
37
- iconContent: config.leadingIcon || config.icon
51
+ withElement({
52
+ tag: 'div',
53
+ componentName: 'chip',
54
+ attrs: {
55
+ role: 'button',
56
+ tabindex: '0',
57
+ 'aria-disabled': config.disabled ? 'true' : 'false',
58
+ 'aria-selected': config.selected ? 'true' : 'false',
59
+ 'data-value': config.value || ''
60
+ },
61
+ className: config.class,
62
+ forwardEvents: {
63
+ click: (component) => component.element.getAttribute('aria-disabled') !== 'true',
64
+ focus: true,
65
+ blur: true
66
+ }
38
67
  }),
39
- withDisabled(baseConfig),
40
- withRipple(baseConfig),
41
- withLifecycle(),
42
- comp => withAPI(getApiConfig(comp))(comp)
43
- )(baseConfig);
68
+ withLifecycle()
69
+ )(baseConfig)
70
+
71
+ // Track selected state
72
+ let isSelectedState = !!config.selected;
73
+
74
+ // Manually add the variant class
75
+ if (config.variant) {
76
+ chip.element.classList.add(`${chip.getClass('chip')}--${config.variant}`)
77
+ }
78
+
79
+ // Manually add the size class
80
+ if (config.size) {
81
+ chip.element.classList.add(`${chip.getClass('chip')}--${config.size}`)
82
+ }
83
+
84
+ // Add ripple if enabled
85
+ if (config.ripple) {
86
+ withRipple(baseConfig)(chip)
87
+ }
88
+
89
+ // Add disabled state if needed
90
+ if (config.disabled) {
91
+ withDisabled(baseConfig)(chip)
92
+ }
93
+
94
+ // Add selected class if needed
95
+ if (config.selected) {
96
+ chip.element.classList.add(`${chip.getClass('chip')}--selected`)
97
+ }
98
+
99
+ // Create a container for the chip content to ensure proper ordering
100
+ const contentContainer = document.createElement('div')
101
+ contentContainer.className = `${chip.getClass('chip')}-content`
102
+ contentContainer.style.display = 'flex'
103
+ contentContainer.style.alignItems = 'center'
104
+ contentContainer.style.justifyContent = 'center'
105
+ contentContainer.style.width = '100%'
106
+ chip.element.appendChild(contentContainer)
107
+
108
+ // Add leading icon if provided
109
+ if (config.leadingIcon) {
110
+ const leadingIconElement = document.createElement('span')
111
+ leadingIconElement.className = `${chip.getClass('chip')}-leading-icon`
112
+ leadingIconElement.innerHTML = config.leadingIcon
113
+ contentContainer.appendChild(leadingIconElement)
114
+ }
115
+
116
+ // Add text element if provided
117
+ if (config.text) {
118
+ const textElement = document.createElement('span')
119
+ textElement.className = `${chip.getClass('chip')}-text`
120
+ textElement.textContent = config.text
121
+ contentContainer.appendChild(textElement)
122
+ }
44
123
 
45
124
  // Add trailing icon if provided
46
125
  if (config.trailingIcon) {
47
- const trailingIconElement = document.createElement('span');
48
- trailingIconElement.className = `${baseConfig.prefix}-chip-trailing-icon`;
49
- trailingIconElement.innerHTML = config.trailingIcon;
50
- chip.element.appendChild(trailingIconElement);
51
-
52
- // Add event listener for remove/close action if needed
126
+ const trailingIconElement = document.createElement('span')
127
+ trailingIconElement.className = `${chip.getClass('chip')}-trailing-icon`
128
+ trailingIconElement.innerHTML = config.trailingIcon
129
+
130
+ // Add click handler for trailing icon
53
131
  if (config.onTrailingIconClick) {
54
132
  trailingIconElement.addEventListener('click', (e) => {
55
- e.stopPropagation();
56
- config.onTrailingIconClick!(chip as ChipComponent);
57
- });
133
+ e.stopPropagation() // Prevent chip click event
134
+ config.onTrailingIconClick(enhancedChip)
135
+ })
58
136
  }
137
+
138
+ contentContainer.appendChild(trailingIconElement)
59
139
  }
60
140
 
61
- // Initialize selected state if needed
62
- if (config.selected) {
63
- (chip as ChipComponent).setSelected(true);
64
- }
141
+ // Create enhanced component with API
142
+ const enhancedChip = {
143
+ ...chip,
144
+
145
+ /**
146
+ * Checks if the chip is disabled
147
+ * @returns {boolean} True if the chip is disabled
148
+ */
149
+ isDisabled() {
150
+ return chip.element.getAttribute('aria-disabled') === 'true';
151
+ },
152
+
153
+ /**
154
+ * Checks if the chip is selected
155
+ * @returns {boolean} True if the chip is selected
156
+ */
157
+ isSelected() {
158
+ return isSelectedState;
159
+ },
160
+
161
+ /**
162
+ * Sets the chip's selected state
163
+ * @param {boolean} selected - Whether the chip should be selected
164
+ * @returns {Object} The chip instance for chaining
165
+ */
166
+ setSelected(selected) {
167
+ isSelectedState = !!selected;
168
+
169
+ if (selected) {
170
+ chip.element.classList.add(`${chip.getClass('chip')}--selected`);
171
+ chip.element.setAttribute('aria-selected', 'true');
172
+ } else {
173
+ chip.element.classList.remove(`${chip.getClass('chip')}--selected`);
174
+ chip.element.setAttribute('aria-selected', 'false');
175
+ }
176
+
177
+ return this;
178
+ },
179
+
180
+ /**
181
+ * Toggles the chip's selected state
182
+ * @returns {Object} The chip instance for chaining
183
+ */
184
+ toggleSelected() {
185
+ return this.setSelected(!isSelectedState);
186
+ },
187
+
188
+ /**
189
+ * Gets the chip's value
190
+ * @returns {string} The chip's value
191
+ */
192
+ getValue() {
193
+ return chip.element.getAttribute('data-value');
194
+ },
195
+
196
+ /**
197
+ * Sets the chip's value
198
+ * @param {string} value - Value to set
199
+ * @returns {Object} The chip instance for chaining
200
+ */
201
+ setValue(value) {
202
+ chip.element.setAttribute('data-value', value);
203
+ return this;
204
+ },
205
+
206
+ /**
207
+ * Enables the chip
208
+ * @returns {Object} The chip instance for chaining
209
+ */
210
+ enable() {
211
+ chip.element.classList.remove(`${chip.getClass('chip')}--disabled`);
212
+ chip.element.setAttribute('aria-disabled', 'false');
213
+ chip.element.setAttribute('tabindex', '0');
214
+ return this;
215
+ },
216
+
217
+ /**
218
+ * Disables the chip
219
+ * @returns {Object} The chip instance for chaining
220
+ */
221
+ disable() {
222
+ chip.element.classList.add(`${chip.getClass('chip')}--disabled`);
223
+ chip.element.setAttribute('aria-disabled', 'true');
224
+ chip.element.setAttribute('tabindex', '-1');
225
+ return this;
226
+ },
227
+
228
+ /**
229
+ * Sets the chip's text content
230
+ * @param {string} content - Text content
231
+ * @returns {Object} The chip instance for chaining
232
+ */
233
+ setText(content) {
234
+ const textElement = chip.element.querySelector(`.${chip.getClass('chip')}-text`);
235
+
236
+ if (textElement) {
237
+ textElement.textContent = content;
238
+ } else if (content) {
239
+ const newTextElement = document.createElement('span');
240
+ newTextElement.className = `${chip.getClass('chip')}-text`;
241
+ newTextElement.textContent = content;
242
+ contentContainer.appendChild(newTextElement);
243
+ }
244
+
245
+ return this;
246
+ },
247
+
248
+ /**
249
+ * Gets the chip's text content
250
+ * @returns {string} The chip's text content
251
+ */
252
+ getText() {
253
+ const textElement = chip.element.querySelector(`.${chip.getClass('chip')}-text`);
254
+ return textElement ? textElement.textContent : '';
255
+ },
256
+
257
+ /**
258
+ * Sets the chip's icon
259
+ * @param {string} icon - Icon HTML content
260
+ * @returns {Object} The chip instance for chaining
261
+ */
262
+ setIcon(icon) {
263
+ return this.setLeadingIcon(icon);
264
+ },
265
+
266
+ /**
267
+ * Gets the chip's icon
268
+ * @returns {string} The chip's icon HTML
269
+ */
270
+ getIcon() {
271
+ const iconElement = chip.element.querySelector(`.${chip.getClass('chip')}-leading-icon`);
272
+ return iconElement ? iconElement.innerHTML : '';
273
+ },
274
+
275
+ /**
276
+ * Sets the chip's leading icon
277
+ * @param {string} icon - Icon HTML content
278
+ * @returns {Object} The chip instance for chaining
279
+ */
280
+ setLeadingIcon(icon) {
281
+ const leadingIconSelector = `.${chip.getClass('chip')}-leading-icon`;
282
+ let leadingIconElement = chip.element.querySelector(leadingIconSelector);
283
+
284
+ if (!leadingIconElement && icon) {
285
+ leadingIconElement = document.createElement('span');
286
+ leadingIconElement.className = `${chip.getClass('chip')}-leading-icon`;
287
+
288
+ // Insert at the beginning of the content container
289
+ contentContainer.insertBefore(leadingIconElement, contentContainer.firstChild);
290
+ }
291
+
292
+ if (leadingIconElement) {
293
+ leadingIconElement.innerHTML = icon || '';
294
+
295
+ // Remove the element if icon is empty
296
+ if (!icon && leadingIconElement.parentNode) {
297
+ leadingIconElement.parentNode.removeChild(leadingIconElement);
298
+ }
299
+ }
300
+
301
+ return this;
302
+ },
303
+
304
+ /**
305
+ * Sets the chip's trailing icon
306
+ * @param {string} icon - Icon HTML content
307
+ * @param {Function} [onClick] - Click handler for the trailing icon
308
+ * @returns {Object} The chip instance for chaining
309
+ */
310
+ setTrailingIcon(icon, onClick) {
311
+ const trailingIconSelector = `.${chip.getClass('chip')}-trailing-icon`;
312
+ let trailingIconElement = chip.element.querySelector(trailingIconSelector);
313
+
314
+ if (!trailingIconElement && icon) {
315
+ trailingIconElement = document.createElement('span');
316
+ trailingIconElement.className = `${chip.getClass('chip')}-trailing-icon`;
317
+
318
+ // Add at the end of the content container
319
+ contentContainer.appendChild(trailingIconElement);
320
+
321
+ // Add click handler if provided
322
+ if (onClick) {
323
+ trailingIconElement.addEventListener('click', (e) => {
324
+ e.stopPropagation(); // Prevent chip click event
325
+ onClick(this);
326
+ });
327
+ }
328
+ }
329
+
330
+ if (trailingIconElement) {
331
+ trailingIconElement.innerHTML = icon || '';
332
+
333
+ // Remove the element if icon is empty
334
+ if (!icon && trailingIconElement.parentNode) {
335
+ trailingIconElement.parentNode.removeChild(trailingIconElement);
336
+ }
337
+ }
338
+
339
+ return this;
340
+ },
341
+
342
+ /**
343
+ * Destroys the chip component and cleans up resources
344
+ */
345
+ destroy() {
346
+ chip.lifecycle && chip.lifecycle.destroy && chip.lifecycle.destroy();
347
+ chip.element.remove();
348
+ },
349
+
350
+ // Forward event methods from the original chip
351
+ on: chip.on,
352
+ off: chip.off,
353
+
354
+ /**
355
+ * Add CSS classes to the chip element
356
+ * @param {...string} classes - CSS classes to add
357
+ * @returns {Object} The chip instance for chaining
358
+ */
359
+ addClass(...classes) {
360
+ chip.element.classList.add(...classes);
361
+ return this;
362
+ }
363
+ };
65
364
 
66
- // Handle selection callback
67
- if (config.onSelect) {
365
+ // Add click handler for selection toggle
366
+ if (config.variant === CHIP_VARIANTS.FILTER ||
367
+ config.variant === CHIP_VARIANTS.ASSIST ||
368
+ config.selectable) {
369
+
68
370
  chip.element.addEventListener('click', () => {
69
- if (chip.element.getAttribute('aria-disabled') !== 'true') {
70
- config.onSelect!(chip as ChipComponent);
371
+ if (enhancedChip.isDisabled()) return;
372
+
373
+ enhancedChip.toggleSelected();
374
+
375
+ // Call onChange callback if provided
376
+ if (config.onChange) {
377
+ config.onChange(enhancedChip);
378
+ }
379
+
380
+ // Call onSelect callback if provided
381
+ if (config.onSelect) {
382
+ config.onSelect(enhancedChip);
71
383
  }
72
384
  });
73
385
  }
74
386
 
75
- return chip as ChipComponent;
387
+ return enhancedChip;
76
388
  } catch (error) {
77
- console.error('Chip creation error:', error instanceof Error ? error.message : String(error));
78
- throw new Error(`Failed to create chip: ${error instanceof Error ? error.message : String(error)}`);
389
+ console.error('Chip creation error:', error);
390
+ throw new Error(`Failed to create chip: ${error.message}`);
79
391
  }
80
392
  };
81
393
 
@@ -1,4 +1,4 @@
1
- // src/components/chip/constants.ts
1
+ // src/components/chip/constants.js
2
2
 
3
3
  /**
4
4
  * Available variants for the Chip component
@@ -25,7 +25,7 @@ export const CHIP_VARIANTS = {
25
25
 
26
26
  /** Suggestion chip for presenting options */
27
27
  SUGGESTION: 'suggestion'
28
- } as const;
28
+ };
29
29
 
30
30
  /**
31
31
  * Available sizes for the Chip component
@@ -35,4 +35,4 @@ export const CHIP_SIZES = {
35
35
  SMALL: 'small',
36
36
  MEDIUM: 'medium',
37
37
  LARGE: 'large'
38
- } as const;
38
+ };
@@ -1,4 +1,4 @@
1
- // src/components/chip/index.ts
1
+ // src/components/chip/index.js
2
2
  export { default } from './chip'
3
- export { CHIP_VARIANTS, CHIP_SIZES } from './constants'
4
- export { ChipConfig, ChipComponent } from './types'
3
+ export { default as createChipSet } from './chip-set'
4
+ export { CHIP_VARIANTS, CHIP_SIZES } from './constants'
@@ -1,4 +1,4 @@
1
- // src/components/switch/constants.js
1
+ // src/components/switch/constants.ts
2
2
 
3
3
  /**
4
4
  * Label position options
@@ -6,7 +6,7 @@
6
6
  export const SWITCH_LABEL_POSITION = {
7
7
  START: 'start',
8
8
  END: 'end'
9
- }
9
+ } as const;
10
10
 
11
11
  /**
12
12
  * Validation schema for switch configuration
@@ -56,7 +56,7 @@ export const SWITCH_SCHEMA = {
56
56
  optional: true
57
57
  }
58
58
  }
59
- }
59
+ } as const;
60
60
 
61
61
  /**
62
62
  * Switch state classes
@@ -65,7 +65,7 @@ export const SWITCH_STATES = {
65
65
  CHECKED: 'checked',
66
66
  DISABLED: 'disabled',
67
67
  FOCUSED: 'focused'
68
- }
68
+ } as const;
69
69
 
70
70
  /**
71
71
  * Switch element classes
@@ -77,4 +77,4 @@ export const SWITCH_CLASSES = {
77
77
  THUMB: 'thumb',
78
78
  THUMB_ICON: 'thumb-icon',
79
79
  LABEL: 'switch-label'
80
- }
80
+ } as const;
@@ -4,7 +4,7 @@
4
4
  * @description Core utilities for component composition and creation with built-in mobile support
5
5
  */
6
6
 
7
- import { createElement, CreateElementOptions } from '../dom/create';
7
+ import { createElement, CreateElementOptions, removeEventHandlers } from '../dom/create';
8
8
  import {
9
9
  normalizeEvent,
10
10
  hasTouchSupport,
@@ -138,7 +138,7 @@ export const createBase = (config: Record<string, any> = {}): BaseComponent => (
138
138
  * @returns {Function} Component enhancer
139
139
  */
140
140
  export const withElement = (options: WithElementOptions = {}) =>
141
- (base: BaseComponent): ElementComponent => {
141
+ <T extends BaseComponent>(component: T): T & ElementComponent => {
142
142
  /**
143
143
  * Handles the start of a touch interaction.
144
144
  */
@@ -146,8 +146,8 @@ export const withElement = (options: WithElementOptions = {}) =>
146
146
  base.updateTouchState(event, 'start');
147
147
  element.classList.add(`${base.getClass('touch-active')}`);
148
148
 
149
- if (options.forwardEvents?.touchstart && base.config?.emit) {
150
- base.config.emit('touchstart', normalizeEvent(event));
149
+ if (options.forwardEvents?.touchstart && 'emit' in component) {
150
+ (component as any).emit('touchstart', normalizeEvent(event));
151
151
  }
152
152
  };
153
153
 
@@ -162,12 +162,12 @@ export const withElement = (options: WithElementOptions = {}) =>
162
162
  base.updateTouchState(event, 'end');
163
163
 
164
164
  // Emit tap event for short touches
165
- if (touchDuration < TOUCH_CONFIG.TAP_THRESHOLD && base.config?.emit) {
166
- base.config.emit('tap', normalizeEvent(event));
165
+ if (touchDuration < TOUCH_CONFIG.TAP_THRESHOLD && 'emit' in component) {
166
+ (component as any).emit('tap', normalizeEvent(event));
167
167
  }
168
168
 
169
- if (options.forwardEvents?.touchend && base.config?.emit) {
170
- base.config.emit('touchend', normalizeEvent(event));
169
+ if (options.forwardEvents?.touchend && 'emit' in component) {
170
+ (component as any).emit('touchend', normalizeEvent(event));
171
171
  }
172
172
  };
173
173
 
@@ -182,19 +182,22 @@ export const withElement = (options: WithElementOptions = {}) =>
182
182
  const deltaY = normalized.clientY - base.touchState.startPosition.y;
183
183
 
184
184
  // Detect and emit swipe gestures
185
- if (Math.abs(deltaX) > TOUCH_CONFIG.SWIPE_THRESHOLD && base.config?.emit) {
186
- base.config.emit('swipe', {
185
+ if (Math.abs(deltaX) > TOUCH_CONFIG.SWIPE_THRESHOLD && 'emit' in component) {
186
+ (component as any).emit('swipe', {
187
187
  direction: deltaX > 0 ? 'right' : 'left',
188
188
  deltaX,
189
189
  deltaY
190
190
  });
191
191
  }
192
192
 
193
- if (options.forwardEvents?.touchmove && base.config?.emit) {
194
- base.config.emit('touchmove', { ...normalized, deltaX, deltaY });
193
+ if (options.forwardEvents?.touchmove && 'emit' in component) {
194
+ (component as any).emit('touchmove', { ...normalized, deltaX, deltaY });
195
195
  }
196
196
  };
197
197
 
198
+ // Get the base component for reference
199
+ const base = component;
200
+
198
201
  // Create element options from component options
199
202
  const elementOptions: CreateElementOptions = {
200
203
  tag: options.tag || 'div',
@@ -204,7 +207,8 @@ export const withElement = (options: WithElementOptions = {}) =>
204
207
  options.className
205
208
  ].filter(Boolean),
206
209
  attrs: options.attrs || {},
207
- context: base
210
+ forwardEvents: options.forwardEvents || {},
211
+ context: component // Pass component as context for events
208
212
  };
209
213
 
210
214
  // Create the element with appropriate classes
@@ -218,7 +222,7 @@ export const withElement = (options: WithElementOptions = {}) =>
218
222
  }
219
223
 
220
224
  return {
221
- ...base,
225
+ ...component,
222
226
  element,
223
227
 
224
228
  /**
@@ -241,6 +245,10 @@ export const withElement = (options: WithElementOptions = {}) =>
241
245
  element.removeEventListener('touchend', handleTouchEnd);
242
246
  element.removeEventListener('touchmove', handleTouchMove);
243
247
  }
248
+
249
+ // Clean up any registered event handlers using our new utility
250
+ removeEventHandlers(element);
251
+
244
252
  element.remove();
245
253
  }
246
254
  };