mtrl 0.0.2 → 0.1.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 (52) hide show
  1. package/package.json +2 -2
  2. package/src/components/button/styles.scss +198 -161
  3. package/src/components/checkbox/checkbox.js +4 -3
  4. package/src/components/checkbox/styles.scss +105 -55
  5. package/src/components/container/styles.scss +65 -58
  6. package/src/components/list/styles.scss +240 -11
  7. package/src/components/menu/features/items-manager.js +5 -1
  8. package/src/components/menu/styles.scss +37 -30
  9. package/src/components/navigation/constants.js +19 -54
  10. package/src/components/navigation/styles.scss +406 -6
  11. package/src/components/snackbar/styles.scss +46 -17
  12. package/src/components/switch/styles.scss +104 -40
  13. package/src/components/switch/switch.js +1 -1
  14. package/src/components/textfield/styles.scss +351 -5
  15. package/src/core/build/_ripple.scss +79 -0
  16. package/src/core/compose/features/disabled.js +27 -7
  17. package/src/core/compose/features/input.js +9 -1
  18. package/src/core/compose/features/textinput.js +16 -20
  19. package/src/core/dom/create.js +0 -1
  20. package/src/styles/abstract/_mixins.scss +9 -7
  21. package/src/styles/abstract/_theme.scss +157 -0
  22. package/src/styles/abstract/_variables.scss +72 -6
  23. package/src/styles/base/_reset.scss +86 -0
  24. package/src/styles/base/_typography.scss +155 -0
  25. package/src/styles/main.scss +104 -57
  26. package/src/styles/themes/_base-theme.scss +2 -27
  27. package/src/styles/themes/_baseline.scss +64 -39
  28. package/src/styles/utilities/_color.scss +154 -0
  29. package/src/styles/utilities/_flexbox.scss +194 -0
  30. package/src/styles/utilities/_spacing.scss +139 -0
  31. package/src/styles/utilities/_typography.scss +178 -0
  32. package/src/styles/utilities/_visibility.scss +142 -0
  33. package/test/components/button.test.js +46 -34
  34. package/test/components/checkbox.test.js +238 -0
  35. package/test/components/list.test.js +105 -0
  36. package/test/components/menu.test.js +385 -0
  37. package/test/components/navigation.test.js +227 -0
  38. package/test/components/snackbar.test.js +234 -0
  39. package/test/components/switch.test.js +186 -0
  40. package/test/components/textfield.test.js +314 -0
  41. package/test/core/ripple.test.js +21 -120
  42. package/test/setup.js +152 -239
  43. package/src/components/list/styles/_list-item.scss +0 -142
  44. package/src/components/list/styles/_list.scss +0 -89
  45. package/src/components/list/styles/_variables.scss +0 -13
  46. package/src/components/navigation/styles/_bar.scss +0 -51
  47. package/src/components/navigation/styles/_base.scss +0 -129
  48. package/src/components/navigation/styles/_drawer.scss +0 -169
  49. package/src/components/navigation/styles/_rail.scss +0 -65
  50. package/src/components/textfield/styles/base.scss +0 -107
  51. package/src/components/textfield/styles/filled.scss +0 -58
  52. package/src/components/textfield/styles/outlined.scss +0 -66
@@ -0,0 +1,105 @@
1
+ import { describe, test, expect } from 'bun:test'
2
+ import createList from '../../src/components/list/list'
3
+ import { LIST_TYPES } from '../../src/components/list/constants'
4
+
5
+ describe('List Component', () => {
6
+ test('should create a default list element', () => {
7
+ const list = createList({
8
+ items: [{ id: 'item1', headline: 'Item 1' }]
9
+ })
10
+ expect(list.element).toBeDefined()
11
+ // Default type is "default" and role "list"
12
+ expect(list.element.getAttribute('data-type')).toBe(LIST_TYPES.DEFAULT)
13
+ expect(list.element.getAttribute('role')).toBe('list')
14
+ // Check at least one list item exists
15
+ const listItem = list.element.querySelector(`.${list.prefix}-list-item`)
16
+ expect(listItem).not.toBeNull()
17
+ })
18
+
19
+ test('should support single select behavior', () => {
20
+ const list = createList({
21
+ type: LIST_TYPES.SINGLE_SELECT,
22
+ items: [
23
+ { id: 'item1', headline: 'Item 1' },
24
+ { id: 'item2', headline: 'Item 2' }
25
+ ]
26
+ })
27
+
28
+ // Simulate clicking on the first item
29
+ const items = list.element.querySelectorAll(`.${list.prefix}-list-item`)
30
+ const firstItem = items[0]
31
+ firstItem.dispatchEvent(new Event('click'))
32
+ expect(firstItem.getAttribute('aria-selected')).toBe('true')
33
+
34
+ // Now click the second item; the first should be deselected
35
+ const secondItem = items[1]
36
+ secondItem.dispatchEvent(new Event('click'))
37
+ expect(firstItem.getAttribute('aria-selected')).toBe('false')
38
+ expect(secondItem.getAttribute('aria-selected')).toBe('true')
39
+ })
40
+
41
+ test('should support multi select behavior', () => {
42
+ const list = createList({
43
+ type: LIST_TYPES.MULTI_SELECT,
44
+ items: [
45
+ { id: 'item1', headline: 'Item 1' },
46
+ { id: 'item2', headline: 'Item 2' }
47
+ ]
48
+ })
49
+
50
+ const items = list.element.querySelectorAll(`.${list.prefix}-list-item`)
51
+ const firstItem = items[0]
52
+ const secondItem = items[1]
53
+
54
+ // Click to select first item
55
+ firstItem.dispatchEvent(new Event('click'))
56
+ expect(firstItem.getAttribute('aria-selected')).toBe('true')
57
+
58
+ // Click to select second item
59
+ secondItem.dispatchEvent(new Event('click'))
60
+ expect(secondItem.getAttribute('aria-selected')).toBe('true')
61
+ expect(list.getSelected().length).toBe(2)
62
+
63
+ // Click first item again to deselect it
64
+ firstItem.dispatchEvent(new Event('click'))
65
+ expect(firstItem.getAttribute('aria-selected')).toBe('false')
66
+ expect(list.getSelected().length).toBe(1)
67
+ })
68
+
69
+ test('should set selected items via setSelected', () => {
70
+ const list = createList({
71
+ type: LIST_TYPES.MULTI_SELECT,
72
+ items: [
73
+ { id: 'item1', headline: 'Item 1' },
74
+ { id: 'item2', headline: 'Item 2' },
75
+ { id: 'item3', headline: 'Item 3' }
76
+ ]
77
+ })
78
+
79
+ list.setSelected(['item2', 'item3'])
80
+ const items = Array.from(
81
+ list.element.querySelectorAll(`.${list.prefix}-list-item`)
82
+ )
83
+ const item2 = items.find(i => i.dataset.id === 'item2')
84
+ const item3 = items.find(i => i.dataset.id === 'item3')
85
+
86
+ expect(item2.getAttribute('aria-selected')).toBe('true')
87
+ expect(item3.getAttribute('aria-selected')).toBe('true')
88
+ expect(list.getSelected()).toEqual(expect.arrayContaining(['item2', 'item3']))
89
+ })
90
+
91
+ test('should add and remove items dynamically', () => {
92
+ const list = createList({
93
+ items: [{ id: 'item1', headline: 'Item 1' }]
94
+ })
95
+
96
+ const initialCount = list.element.querySelectorAll(`.${list.prefix}-list-item`).length
97
+ list.addItem({ id: 'item2', headline: 'Item 2' })
98
+ const newCount = list.element.querySelectorAll(`.${list.prefix}-list-item`).length
99
+ expect(newCount).toBe(initialCount + 1)
100
+
101
+ list.removeItem('item1')
102
+ const finalCount = list.element.querySelectorAll(`.${list.prefix}-list-item`).length
103
+ expect(finalCount).toBe(newCount - 1)
104
+ })
105
+ })
@@ -0,0 +1,385 @@
1
+ // test/components/menu.test.js
2
+ import { describe, test, expect, mock, beforeEach, afterEach } from 'bun:test'
3
+ import createMenu from '../../src/components/menu/menu'
4
+ import { MENU_ALIGN, MENU_VERTICAL_ALIGN, MENU_EVENTS, MENU_ITEM_TYPES } from '../../src/components/menu/constants'
5
+
6
+ // Mock DOM APIs that aren't available in the test environment
7
+ beforeEach(() => {
8
+ // Mock Element.prototype methods
9
+ Element.prototype.getBoundingClientRect = function () {
10
+ return {
11
+ width: 100,
12
+ height: 100,
13
+ top: 0,
14
+ left: 0,
15
+ right: 100,
16
+ bottom: 100
17
+ }
18
+ }
19
+
20
+ Element.prototype.closest = function (selector) {
21
+ return null // Simple mock that returns null by default
22
+ }
23
+
24
+ Element.prototype.matches = function (selector) {
25
+ return false // Simple mock that returns false by default
26
+ }
27
+
28
+ // Save original createElement to restore later
29
+ const originalCreateElement = document.createElement
30
+ document.createElement = function (tag) {
31
+ const element = originalCreateElement.call(document, tag)
32
+
33
+ // Add closest method for our tests
34
+ element.closest = function (selector) {
35
+ if (selector.includes('menu-item')) {
36
+ return this.classList && this.classList.contains('mtrl-menu-item') ? this : null
37
+ }
38
+ return null
39
+ }
40
+
41
+ // Add matches method for our tests
42
+ element.matches = function (selector) {
43
+ if (selector === ':hover') return false
44
+ return this.classList && this.classList.contains(selector.replace('.', ''))
45
+ }
46
+
47
+ // Mock the querySelectorAll method
48
+ element.querySelectorAll = function (selector) {
49
+ return [] // Return empty array by default
50
+ }
51
+
52
+ // Mock the querySelector method
53
+ element.querySelector = function (selector) {
54
+ return null // Return null by default
55
+ }
56
+
57
+ return element
58
+ }
59
+
60
+ // Mock window properties
61
+ global.window = {
62
+ ...global.window,
63
+ innerWidth: 1024,
64
+ innerHeight: 768
65
+ }
66
+
67
+ // Mock event listeners
68
+ if (!global.eventListeners) {
69
+ global.eventListeners = new Map()
70
+ }
71
+
72
+ const originalAddEventListener = Element.prototype.addEventListener
73
+ Element.prototype.addEventListener = function (event, handler) {
74
+ if (!global.eventListeners.has(this)) {
75
+ global.eventListeners.set(this, new Map())
76
+ }
77
+ if (!global.eventListeners.get(this).has(event)) {
78
+ global.eventListeners.get(this).set(event, new Set())
79
+ }
80
+ global.eventListeners.get(this).get(event).add(handler)
81
+
82
+ // Call original if it exists
83
+ if (originalAddEventListener) {
84
+ originalAddEventListener.call(this, event, handler)
85
+ }
86
+ }
87
+
88
+ const originalRemoveEventListener = Element.prototype.removeEventListener
89
+ Element.prototype.removeEventListener = function (event, handler) {
90
+ if (global.eventListeners.has(this) &&
91
+ global.eventListeners.get(this).has(event)) {
92
+ global.eventListeners.get(this).get(event).delete(handler)
93
+ }
94
+
95
+ // Call original if it exists
96
+ if (originalRemoveEventListener) {
97
+ originalRemoveEventListener.call(this, event, handler)
98
+ }
99
+ }
100
+
101
+ // Mock offsetHeight/offsetWidth
102
+ Object.defineProperty(Element.prototype, 'offsetHeight', {
103
+ configurable: true,
104
+ get: function () { return 100 }
105
+ })
106
+
107
+ Object.defineProperty(Element.prototype, 'offsetWidth', {
108
+ configurable: true,
109
+ get: function () { return 100 }
110
+ })
111
+ })
112
+
113
+ afterEach(() => {
114
+ // Clean up our mocks
115
+ delete Element.prototype.getBoundingClientRect
116
+ delete Element.prototype.closest
117
+ delete Element.prototype.matches
118
+
119
+ document.createElement = document.createElement.__originalFunction || document.createElement
120
+
121
+ // Clear event listeners
122
+ if (global.eventListeners) {
123
+ global.eventListeners.clear()
124
+ }
125
+ })
126
+
127
+ describe('Menu Component', () => {
128
+ // Sample menu items for testing
129
+ const testItems = [
130
+ {
131
+ name: 'copy',
132
+ text: 'Copy'
133
+ },
134
+ {
135
+ name: 'paste',
136
+ text: 'Paste'
137
+ },
138
+ {
139
+ type: 'divider'
140
+ },
141
+ {
142
+ name: 'delete',
143
+ text: 'Delete',
144
+ disabled: true
145
+ }
146
+ ]
147
+
148
+ // Sample nested menu items
149
+ const nestedTestItems = [
150
+ {
151
+ name: 'file',
152
+ text: 'File',
153
+ items: [
154
+ {
155
+ name: 'new',
156
+ text: 'New'
157
+ },
158
+ {
159
+ name: 'open',
160
+ text: 'Open'
161
+ }
162
+ ]
163
+ },
164
+ {
165
+ name: 'edit',
166
+ text: 'Edit',
167
+ items: [
168
+ {
169
+ name: 'copy',
170
+ text: 'Copy'
171
+ },
172
+ {
173
+ name: 'paste',
174
+ text: 'Paste'
175
+ }
176
+ ]
177
+ }
178
+ ]
179
+
180
+ test('should create a menu element', () => {
181
+ const menu = createMenu()
182
+
183
+ expect(menu.element).toBeDefined()
184
+ expect(menu.element.tagName).toBe('DIV')
185
+ expect(menu.element.className).toContain('mtrl-menu')
186
+ expect(menu.element.getAttribute('role')).toBe('menu')
187
+ })
188
+
189
+ test('should apply custom class', () => {
190
+ const customClass = 'custom-menu'
191
+ const menu = createMenu({
192
+ class: customClass
193
+ })
194
+
195
+ expect(menu.element.className).toContain(customClass)
196
+ })
197
+
198
+ test('should add initial items', () => {
199
+ const menu = createMenu({
200
+ items: testItems
201
+ })
202
+
203
+ // Check if items methods exist
204
+ expect(typeof menu.getItems).toBe('function')
205
+
206
+ // Get items and verify we have a Map
207
+ const items = menu.getItems()
208
+ expect(items instanceof Map).toBe(true)
209
+
210
+ // Verify item names in map
211
+ expect(items.has('copy')).toBe(true)
212
+ expect(items.has('paste')).toBe(true)
213
+ expect(items.has('delete')).toBe(true)
214
+ })
215
+
216
+ test('should have show/hide methods', () => {
217
+ const menu = createMenu()
218
+
219
+ // Check for API methods
220
+ expect(typeof menu.show).toBe('function')
221
+ expect(typeof menu.hide).toBe('function')
222
+ expect(typeof menu.isVisible).toBe('function')
223
+
224
+ // Test visibility state
225
+ expect(menu.isVisible()).toBe(false)
226
+
227
+ // Show menu
228
+ menu.show()
229
+ expect(menu.isVisible()).toBe(true)
230
+ expect(menu.element.classList.contains('mtrl-menu--visible')).toBe(true)
231
+
232
+ // Hide menu
233
+ menu.hide()
234
+
235
+ // Note: Due to animations, isVisible() might still return true immediately after hide()
236
+ // In a real environment, we'd wait for transitions to complete
237
+ })
238
+
239
+ test('should have positioning methods', () => {
240
+ const menu = createMenu()
241
+ const target = document.createElement('button')
242
+
243
+ // Check for API method
244
+ expect(typeof menu.position).toBe('function')
245
+
246
+ // Test with different alignments
247
+ const positionConfigs = [
248
+ { align: MENU_ALIGN.LEFT, vAlign: MENU_VERTICAL_ALIGN.TOP },
249
+ { align: MENU_ALIGN.RIGHT, vAlign: MENU_VERTICAL_ALIGN.BOTTOM },
250
+ { align: MENU_ALIGN.CENTER, vAlign: MENU_VERTICAL_ALIGN.MIDDLE }
251
+ ]
252
+
253
+ positionConfigs.forEach(config => {
254
+ try {
255
+ menu.position(target, config)
256
+ // If we reach here, no error was thrown
257
+ expect(true).toBe(true)
258
+ } catch (error) {
259
+ // If an error occurs, the test should fail
260
+ expect(error).toBeUndefined()
261
+ }
262
+ })
263
+ })
264
+
265
+ test('should add item dynamically', () => {
266
+ const menu = createMenu()
267
+
268
+ // Check for API method
269
+ expect(typeof menu.addItem).toBe('function')
270
+
271
+ // Test adding an item
272
+ const newItem = {
273
+ name: 'newItem',
274
+ text: 'New Item'
275
+ }
276
+
277
+ menu.addItem(newItem)
278
+
279
+ // Verify item was added
280
+ const items = menu.getItems()
281
+ expect(items.has('newItem')).toBe(true)
282
+ })
283
+
284
+ test('should remove item dynamically', () => {
285
+ const menu = createMenu({
286
+ items: testItems
287
+ })
288
+
289
+ // Check for API method
290
+ expect(typeof menu.removeItem).toBe('function')
291
+
292
+ // Test removing an item
293
+ menu.removeItem('copy')
294
+
295
+ // Verify item was removed
296
+ const items = menu.getItems()
297
+ expect(items.has('copy')).toBe(false)
298
+ })
299
+
300
+ test('should register event handlers', () => {
301
+ const menu = createMenu()
302
+
303
+ // Check for API methods
304
+ expect(typeof menu.on).toBe('function')
305
+ expect(typeof menu.off).toBe('function')
306
+
307
+ // Create a mock handler
308
+ const mockHandler = mock(() => {})
309
+
310
+ // Register handler
311
+ menu.on(MENU_EVENTS.SELECT, mockHandler)
312
+
313
+ // We can't easily test if the handler is called in this environment
314
+ // But we can check that the method works without error
315
+ expect(mockHandler.mock.calls.length).toBe(0)
316
+
317
+ // Unregister handler
318
+ menu.off(MENU_EVENTS.SELECT, mockHandler)
319
+ })
320
+
321
+ test('should create nested menus for items with children', () => {
322
+ // This test would be more complex in a real environment
323
+ // For now, just verify the basic menu creation works with nested items
324
+
325
+ const menu = createMenu({
326
+ items: nestedTestItems
327
+ })
328
+
329
+ // Verify parent items exist
330
+ const items = menu.getItems()
331
+ expect(items.has('file')).toBe(true)
332
+ expect(items.has('edit')).toBe(true)
333
+
334
+ // We can't easily test the submenu creation here
335
+ // But we can check that the parent items are created without error
336
+ })
337
+
338
+ test('should properly clean up resources on destroy', () => {
339
+ const menu = createMenu()
340
+
341
+ // Check for API method
342
+ expect(typeof menu.destroy).toBe('function')
343
+
344
+ const parentElement = document.createElement('div')
345
+ parentElement.appendChild(menu.element)
346
+
347
+ // Destroy the component
348
+ menu.destroy()
349
+
350
+ // Check if element was removed
351
+ expect(parentElement.children.length).toBe(0)
352
+ })
353
+
354
+ test('should support keyboard navigation', () => {
355
+ // Skip detailed keyboard navigation tests due to test environment limitations
356
+ // Just verify the API methods exist
357
+
358
+ const menu = createMenu()
359
+
360
+ // Show the menu to initialize keyboard handlers
361
+ menu.show()
362
+
363
+ // In a real environment, we would dispatch keydown events and check results
364
+ // But here we just verify the basic setup happens without errors
365
+
366
+ // Hide and clean up
367
+ menu.hide()
368
+ })
369
+
370
+ test('should handle outside clicks', () => {
371
+ // This would typically close the menu
372
+ // We can't fully test this behavior in the current environment
373
+
374
+ const menu = createMenu()
375
+ menu.show()
376
+
377
+ // In a real environment, we would:
378
+ // 1. Create a click event outside the menu
379
+ // 2. Dispatch it
380
+ // 3. Verify menu is hidden
381
+
382
+ // For now, just ensure our menu API method is called without error
383
+ menu.hide()
384
+ })
385
+ })