mtrl 0.2.0 → 0.2.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtrl",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "A functional JavaScript component library with composable architecture based on Material Design 3",
5
5
  "keywords": [
6
6
  "component",
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "author": "floor",
25
25
  "license": "MIT License",
26
- "dependencies": {
26
+ "devDependencies": {
27
27
  "typedoc": "^0.27.9"
28
28
  }
29
29
  }
@@ -1,4 +1,4 @@
1
- // src/components/checkbox/constants.js
1
+ // src/components/checkbox/constants.ts
2
2
 
3
3
  /**
4
4
  * Visual variants for checkbox
@@ -6,7 +6,7 @@
6
6
  export const CHECKBOX_VARIANTS = {
7
7
  FILLED: 'filled',
8
8
  OUTLINED: 'outlined'
9
- }
9
+ } as const;
10
10
 
11
11
  /**
12
12
  * Label position options
@@ -14,58 +14,7 @@ export const CHECKBOX_VARIANTS = {
14
14
  export const CHECKBOX_LABEL_POSITION = {
15
15
  START: 'start',
16
16
  END: 'end'
17
- }
18
-
19
- /**
20
- * Validation schema for checkbox configuration
21
- */
22
- export const CHECKBOX_SCHEMA = {
23
- type: 'object',
24
- properties: {
25
- name: {
26
- type: 'string',
27
- optional: true
28
- },
29
- checked: {
30
- type: 'boolean',
31
- optional: true
32
- },
33
- indeterminate: {
34
- type: 'boolean',
35
- optional: true
36
- },
37
- required: {
38
- type: 'boolean',
39
- optional: true
40
- },
41
- disabled: {
42
- type: 'boolean',
43
- optional: true
44
- },
45
- value: {
46
- type: 'string',
47
- optional: true
48
- },
49
- label: {
50
- type: 'string',
51
- optional: true
52
- },
53
- labelPosition: {
54
- type: 'string',
55
- enum: Object.values(CHECKBOX_LABEL_POSITION),
56
- optional: true
57
- },
58
- variant: {
59
- type: 'string',
60
- enum: Object.values(CHECKBOX_VARIANTS),
61
- optional: true
62
- },
63
- class: {
64
- type: 'string',
65
- optional: true
66
- }
67
- }
68
- }
17
+ } as const;
69
18
 
70
19
  /**
71
20
  * Checkbox state classes
@@ -75,7 +24,7 @@ export const CHECKBOX_STATES = {
75
24
  INDETERMINATE: 'indeterminate',
76
25
  DISABLED: 'disabled',
77
26
  FOCUSED: 'focused'
78
- }
27
+ } as const;
79
28
 
80
29
  /**
81
30
  * Checkbox element classes
@@ -85,4 +34,4 @@ export const CHECKBOX_CLASSES = {
85
34
  INPUT: 'checkbox-input',
86
35
  ICON: 'checkbox-icon',
87
36
  LABEL: 'checkbox-label'
88
- }
37
+ } as const;
@@ -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
  };
@@ -72,6 +72,13 @@ export interface CreateElementOptions {
72
72
  [key: string]: any;
73
73
  }
74
74
 
75
+ /**
76
+ * Event handler storage to facilitate cleanup
77
+ */
78
+ export interface EventHandlerStorage {
79
+ [eventName: string]: EventListener;
80
+ }
81
+
75
82
  /**
76
83
  * Creates a DOM element with the specified options
77
84
  *
@@ -117,21 +124,57 @@ export const createElement = (options: CreateElementOptions = {}): HTMLElement =
117
124
  // Handle all other attributes
118
125
  const allAttrs = { ...attrs, ...rest };
119
126
  Object.entries(allAttrs).forEach(([key, value]) => {
120
- if (value != null) element.setAttribute(key, value);
127
+ if (value != null) element.setAttribute(key, String(value));
121
128
  });
122
129
 
123
- // Handle event forwarding if context has emit method
124
- if (context?.emit && forwardEvents) {
125
- Object.entries(forwardEvents).forEach(([nativeEvent, eventConfig]) => {
126
- const shouldForward = typeof eventConfig === 'function'
127
- ? eventConfig
128
- : () => true;
130
+ // Initialize event handler storage if not present
131
+ if (!element.__eventHandlers) {
132
+ element.__eventHandlers = {};
133
+ }
129
134
 
130
- element.addEventListener(nativeEvent, (event) => {
131
- if (shouldForward({ ...context, element }, event)) {
132
- context.emit(nativeEvent, { event });
135
+ // Handle event forwarding if context has emit method or is a component with on method
136
+ if (forwardEvents && (context?.emit || context?.on)) {
137
+ Object.entries(forwardEvents).forEach(([nativeEvent, eventConfig]) => {
138
+ // Create a wrapper handler function to evaluate condition and forward event
139
+ const handler = (event: Event) => {
140
+ // Determine if the event should be forwarded
141
+ let shouldForward = true;
142
+
143
+ if (typeof eventConfig === 'function') {
144
+ try {
145
+ // If it's a function, call with component context and event
146
+ shouldForward = eventConfig({ ...context, element }, event);
147
+ } catch (error) {
148
+ console.warn(`Error in event condition for ${nativeEvent}:`, error);
149
+ shouldForward = false;
150
+ }
151
+ } else {
152
+ // If it's a boolean, use directly
153
+ shouldForward = Boolean(eventConfig);
133
154
  }
134
- });
155
+
156
+ // Forward the event if condition passes
157
+ if (shouldForward) {
158
+ if (context.emit) {
159
+ context.emit(nativeEvent, { event, element, originalEvent: event });
160
+ } else if (context.on) {
161
+ // This is a component with on method but no emit method
162
+ // Dispatch a custom event that can be listened to
163
+ const customEvent = new CustomEvent(nativeEvent, {
164
+ detail: { event, element, originalEvent: event },
165
+ bubbles: true,
166
+ cancelable: true
167
+ });
168
+ element.dispatchEvent(customEvent);
169
+ }
170
+ }
171
+ };
172
+
173
+ // Store the handler for future removal
174
+ element.__eventHandlers[nativeEvent] = handler;
175
+
176
+ // Add the actual event listener
177
+ element.addEventListener(nativeEvent, handler);
135
178
  });
136
179
  }
137
180
 
@@ -147,6 +190,19 @@ export const createElement = (options: CreateElementOptions = {}): HTMLElement =
147
190
  return element;
148
191
  };
149
192
 
193
+ /**
194
+ * Removes event handlers from an element
195
+ * @param element - Element to cleanup
196
+ */
197
+ export const removeEventHandlers = (element: HTMLElement): void => {
198
+ if (element.__eventHandlers) {
199
+ Object.entries(element.__eventHandlers).forEach(([eventName, handler]) => {
200
+ element.removeEventListener(eventName, handler);
201
+ });
202
+ delete element.__eventHandlers;
203
+ }
204
+ };
205
+
150
206
  /**
151
207
  * Higher-order function to add attributes to an element
152
208
  * @param {Record<string, any>} attrs - Attributes to add
@@ -185,4 +241,11 @@ export const withContent = (content: Node | string) =>
185
241
  element.textContent = content;
186
242
  }
187
243
  return element;
188
- };
244
+ };
245
+
246
+ // Extend HTMLElement interface to add eventHandlers property
247
+ declare global {
248
+ interface HTMLElement {
249
+ __eventHandlers?: EventHandlerStorage;
250
+ }
251
+ }