mtrl 0.0.0 → 0.0.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.
- package/CONTRIBUTING.md +179 -0
- package/README.md +2 -0
- package/TESTING.md +104 -0
- package/index.js +3 -2
- package/package.json +5 -2
- package/src/components/button/config.js +3 -1
- package/src/components/menu/api.js +117 -0
- package/src/components/menu/constants.js +42 -0
- package/src/components/menu/features/items-manager.js +371 -0
- package/src/components/menu/features/keyboard-navigation.js +129 -0
- package/src/components/menu/features/positioning.js +125 -0
- package/src/components/menu/features/visibility.js +179 -0
- package/src/components/menu/index.js +2 -0
- package/src/components/menu/menu-item.js +41 -0
- package/src/components/menu/menu.js +54 -0
- package/src/components/menu/styles.scss +150 -0
- package/src/core/build/ripple.js +76 -9
- package/src/core/compose/features/disabled.js +38 -19
- package/src/core/state/disabled.js +41 -4
- package/src/index.js +4 -3
- package/src/styles/abstract/_config.scss +1 -1
- package/src/styles/abstract/_mixins.scss +75 -0
- package/src/styles/abstract/_variables.scss +1 -0
- package/test/components/button.test.js +158 -0
- package/test/core/emitter.test.js +141 -0
- package/test/core/ripple.test.js +165 -0
- package/test/setup.js +458 -0
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
// src/styles/abstract/_mixins.scss
|
|
2
2
|
@use 'sass:map';
|
|
3
3
|
@use 'sass:list';
|
|
4
|
+
@use 'sass:math';
|
|
4
5
|
@use 'variables' as v;
|
|
5
6
|
@use 'functions' as f;
|
|
6
7
|
|
|
8
|
+
// Common icons map
|
|
9
|
+
$icons: (
|
|
10
|
+
'chevron_right': 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpolyline points="9 18 15 12 9 6"%3E%3C/polyline%3E%3C/svg%3E',
|
|
11
|
+
'chevron_down': 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpolyline points="6 9 12 15 18 9"%3E%3C/polyline%3E%3C/svg%3E',
|
|
12
|
+
'check': 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpolyline points="20 6 9 17 4 12"%3E%3C/polyline%3E%3C/svg%3E'
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
@mixin icon($name, $size: 24px) {
|
|
16
|
+
$icon-url: map.get($icons, $name);
|
|
17
|
+
@if $icon-url {
|
|
18
|
+
content: '';
|
|
19
|
+
display: inline-block;
|
|
20
|
+
width: $size;
|
|
21
|
+
height: $size;
|
|
22
|
+
background-image: url($icon-url);
|
|
23
|
+
background-size: contain;
|
|
24
|
+
background-position: center;
|
|
25
|
+
background-repeat: no-repeat;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
7
29
|
// Typography
|
|
8
30
|
@mixin typography($scale) {
|
|
9
31
|
$styles: f.get-typography($scale);
|
|
@@ -253,6 +275,59 @@
|
|
|
253
275
|
}
|
|
254
276
|
}
|
|
255
277
|
|
|
278
|
+
|
|
279
|
+
// Scrollbar mixin for consistent styling across components
|
|
280
|
+
@mixin scrollbar(
|
|
281
|
+
$width: 8px,
|
|
282
|
+
$track-color: rgba(0, 0, 0, 0.05),
|
|
283
|
+
$thumb-color: rgba(0, 0, 0, 0.2)
|
|
284
|
+
) {
|
|
285
|
+
&::-webkit-scrollbar {
|
|
286
|
+
width: $width;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
&::-webkit-scrollbar-track {
|
|
290
|
+
background: $track-color;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
&::-webkit-scrollbar-thumb {
|
|
294
|
+
background: $thumb-color;
|
|
295
|
+
border-radius: math.div($width, 2);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Firefox scrollbar (future compatibility)
|
|
299
|
+
scrollbar-width: thin;
|
|
300
|
+
scrollbar-color: $thumb-color $track-color;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
// Flexbox layout mixins
|
|
305
|
+
@mixin flex-row($align: center, $justify: flex-start, $gap: 0) {
|
|
306
|
+
display: flex;
|
|
307
|
+
flex-direction: row;
|
|
308
|
+
align-items: $align;
|
|
309
|
+
justify-content: $justify;
|
|
310
|
+
@if $gap > 0 {
|
|
311
|
+
gap: $gap;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@mixin flex-column($align: flex-start, $justify: flex-start, $gap: 0) {
|
|
316
|
+
display: flex;
|
|
317
|
+
flex-direction: column;
|
|
318
|
+
align-items: $align;
|
|
319
|
+
justify-content: $justify;
|
|
320
|
+
@if $gap > 0 {
|
|
321
|
+
gap: $gap;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
@mixin flex-center {
|
|
326
|
+
display: flex;
|
|
327
|
+
align-items: center;
|
|
328
|
+
justify-content: center;
|
|
329
|
+
}
|
|
330
|
+
|
|
256
331
|
// Print
|
|
257
332
|
@mixin print {
|
|
258
333
|
@media print {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// test/components/button.test.js
|
|
2
|
+
import { describe, test, expect, mock } from 'bun:test'
|
|
3
|
+
import createButton from '../../src/components/button/button'
|
|
4
|
+
|
|
5
|
+
describe('Button Component', () => {
|
|
6
|
+
test('should create a button element', () => {
|
|
7
|
+
const button = createButton()
|
|
8
|
+
expect(button.element).toBeDefined()
|
|
9
|
+
expect(button.element.tagName).toBe('BUTTON')
|
|
10
|
+
expect(button.element.className).toContain('mtrl-button')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('should add text content', () => {
|
|
14
|
+
const buttonText = 'Click Me'
|
|
15
|
+
const button = createButton({
|
|
16
|
+
text: buttonText
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const textElement = button.element.querySelector('.mtrl-button-text')
|
|
20
|
+
expect(textElement).toBeDefined()
|
|
21
|
+
expect(textElement.textContent).toBe(buttonText)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('should apply variant class', () => {
|
|
25
|
+
const variant = 'filled'
|
|
26
|
+
const button = createButton({
|
|
27
|
+
variant
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
expect(button.element.className).toContain(`mtrl-button--${variant}`)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('should handle click events', () => {
|
|
34
|
+
const button = createButton()
|
|
35
|
+
const handleClick = mock(() => {})
|
|
36
|
+
|
|
37
|
+
button.on('click', handleClick)
|
|
38
|
+
|
|
39
|
+
// Simulate click event
|
|
40
|
+
const event = new Event('click')
|
|
41
|
+
button.element.dispatchEvent(event)
|
|
42
|
+
|
|
43
|
+
expect(handleClick).toHaveBeenCalled()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('should support disabled state', () => {
|
|
47
|
+
const button = createButton()
|
|
48
|
+
|
|
49
|
+
// Initially not disabled
|
|
50
|
+
expect(button.element.hasAttribute('disabled')).toBe(false)
|
|
51
|
+
|
|
52
|
+
// Disable the button
|
|
53
|
+
button.disable()
|
|
54
|
+
expect(button.element.hasAttribute('disabled')).toBe(true)
|
|
55
|
+
|
|
56
|
+
// Check that the disabled property is also set
|
|
57
|
+
expect(button.element.disabled).toBe(true)
|
|
58
|
+
|
|
59
|
+
// Enable the button
|
|
60
|
+
button.enable()
|
|
61
|
+
expect(button.element.hasAttribute('disabled')).toBe(false)
|
|
62
|
+
expect(button.element.disabled).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('should add icon content', () => {
|
|
66
|
+
const iconSvg = '<svg><path d="M10 10"></path></svg>'
|
|
67
|
+
const button = createButton({
|
|
68
|
+
icon: iconSvg
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const iconElement = button.element.querySelector('.mtrl-button-icon')
|
|
72
|
+
expect(iconElement).toBeDefined()
|
|
73
|
+
expect(iconElement.innerHTML).toBe(iconSvg)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('should position icon correctly', () => {
|
|
77
|
+
const iconSvg = '<svg><path d="M10 10"></path></svg>'
|
|
78
|
+
|
|
79
|
+
// Test end position
|
|
80
|
+
const endButton = createButton({
|
|
81
|
+
text: 'End Icon',
|
|
82
|
+
icon: iconSvg,
|
|
83
|
+
iconPosition: 'end'
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const textElement = endButton.element.querySelector('.mtrl-button-text')
|
|
87
|
+
const iconElement = endButton.element.querySelector('.mtrl-button-icon')
|
|
88
|
+
|
|
89
|
+
// In the DOM, for end position, the text should come before the icon
|
|
90
|
+
const children = Array.from(endButton.element.childNodes)
|
|
91
|
+
const textIndex = children.indexOf(textElement)
|
|
92
|
+
const iconIndex = children.indexOf(iconElement)
|
|
93
|
+
|
|
94
|
+
expect(textIndex).toBeLessThan(iconIndex)
|
|
95
|
+
|
|
96
|
+
// Test start position
|
|
97
|
+
const startButton = createButton({
|
|
98
|
+
text: 'Start Icon',
|
|
99
|
+
icon: iconSvg,
|
|
100
|
+
iconPosition: 'start'
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const startTextElement = startButton.element.querySelector('.mtrl-button-text')
|
|
104
|
+
const startIconElement = startButton.element.querySelector('.mtrl-button-icon')
|
|
105
|
+
|
|
106
|
+
const startChildren = Array.from(startButton.element.childNodes)
|
|
107
|
+
const startTextIndex = startChildren.indexOf(startTextElement)
|
|
108
|
+
const startIconIndex = startChildren.indexOf(startIconElement)
|
|
109
|
+
|
|
110
|
+
expect(startIconIndex).toBeLessThan(startTextIndex)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('should support different sizes', () => {
|
|
114
|
+
const sizes = ['small', 'medium', 'large']
|
|
115
|
+
|
|
116
|
+
sizes.forEach(size => {
|
|
117
|
+
const button = createButton({
|
|
118
|
+
size
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
expect(button.element.className).toContain(`mtrl-button--${size}`)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('should allow updating text', () => {
|
|
126
|
+
const button = createButton({
|
|
127
|
+
text: 'Initial'
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const newText = 'Updated Text'
|
|
131
|
+
button.setText(newText)
|
|
132
|
+
|
|
133
|
+
const textElement = button.element.querySelector('.mtrl-button-text')
|
|
134
|
+
expect(textElement.textContent).toBe(newText)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('should allow updating icon', () => {
|
|
138
|
+
const button = createButton()
|
|
139
|
+
|
|
140
|
+
const iconSvg = '<svg><path d="M10 10"></path></svg>'
|
|
141
|
+
button.setIcon(iconSvg)
|
|
142
|
+
|
|
143
|
+
const iconElement = button.element.querySelector('.mtrl-button-icon')
|
|
144
|
+
expect(iconElement).toBeDefined()
|
|
145
|
+
expect(iconElement.innerHTML).toBe(iconSvg)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('should properly clean up resources', () => {
|
|
149
|
+
const button = createButton()
|
|
150
|
+
const parentElement = document.createElement('div')
|
|
151
|
+
parentElement.appendChild(button.element)
|
|
152
|
+
|
|
153
|
+
// Destroy should remove the element and clean up resources
|
|
154
|
+
button.destroy()
|
|
155
|
+
|
|
156
|
+
expect(parentElement.children.length).toBe(0)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// test/core/state/emitter.test.js
|
|
2
|
+
import { describe, test, expect, mock } from 'bun:test'
|
|
3
|
+
import { createEmitter } from '../../src/core/state/emitter'
|
|
4
|
+
|
|
5
|
+
describe('Event Emitter', () => {
|
|
6
|
+
test('should create an emitter with expected methods', () => {
|
|
7
|
+
const emitter = createEmitter()
|
|
8
|
+
expect(emitter).toBeDefined()
|
|
9
|
+
expect(emitter.on).toBeInstanceOf(Function)
|
|
10
|
+
expect(emitter.off).toBeInstanceOf(Function)
|
|
11
|
+
expect(emitter.emit).toBeInstanceOf(Function)
|
|
12
|
+
expect(emitter.clear).toBeInstanceOf(Function)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('should register event handlers with on()', () => {
|
|
16
|
+
const emitter = createEmitter()
|
|
17
|
+
const handler = mock(() => {})
|
|
18
|
+
|
|
19
|
+
const unsubscribe = emitter.on('test', handler)
|
|
20
|
+
|
|
21
|
+
expect(unsubscribe).toBeInstanceOf(Function)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('should invoke handlers when event is emitted', () => {
|
|
25
|
+
const emitter = createEmitter()
|
|
26
|
+
const handler1 = mock(() => {})
|
|
27
|
+
const handler2 = mock(() => {})
|
|
28
|
+
const eventData = { foo: 'bar' }
|
|
29
|
+
|
|
30
|
+
emitter.on('test', handler1)
|
|
31
|
+
emitter.on('test', handler2)
|
|
32
|
+
|
|
33
|
+
emitter.emit('test', eventData)
|
|
34
|
+
|
|
35
|
+
expect(handler1).toHaveBeenCalledTimes(1)
|
|
36
|
+
expect(handler1).toHaveBeenCalledWith(eventData)
|
|
37
|
+
|
|
38
|
+
expect(handler2).toHaveBeenCalledTimes(1)
|
|
39
|
+
expect(handler2).toHaveBeenCalledWith(eventData)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('should remove handlers with off()', () => {
|
|
43
|
+
const emitter = createEmitter()
|
|
44
|
+
const handler = mock(() => {})
|
|
45
|
+
|
|
46
|
+
emitter.on('test', handler)
|
|
47
|
+
emitter.off('test', handler)
|
|
48
|
+
|
|
49
|
+
emitter.emit('test')
|
|
50
|
+
|
|
51
|
+
expect(handler).not.toHaveBeenCalled()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('should remove specific handlers while keeping others', () => {
|
|
55
|
+
const emitter = createEmitter()
|
|
56
|
+
const handler1 = mock(() => {})
|
|
57
|
+
const handler2 = mock(() => {})
|
|
58
|
+
|
|
59
|
+
emitter.on('test', handler1)
|
|
60
|
+
emitter.on('test', handler2)
|
|
61
|
+
|
|
62
|
+
emitter.off('test', handler1)
|
|
63
|
+
|
|
64
|
+
emitter.emit('test')
|
|
65
|
+
|
|
66
|
+
expect(handler1).not.toHaveBeenCalled()
|
|
67
|
+
expect(handler2).toHaveBeenCalledTimes(1)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('should unsubscribe handlers using the returned function', () => {
|
|
71
|
+
const emitter = createEmitter()
|
|
72
|
+
const handler = mock(() => {})
|
|
73
|
+
|
|
74
|
+
const unsubscribe = emitter.on('test', handler)
|
|
75
|
+
unsubscribe()
|
|
76
|
+
|
|
77
|
+
emitter.emit('test')
|
|
78
|
+
|
|
79
|
+
expect(handler).not.toHaveBeenCalled()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('should support multiple event types', () => {
|
|
83
|
+
const emitter = createEmitter()
|
|
84
|
+
const handler1 = mock(() => {})
|
|
85
|
+
const handler2 = mock(() => {})
|
|
86
|
+
|
|
87
|
+
emitter.on('event1', handler1)
|
|
88
|
+
emitter.on('event2', handler2)
|
|
89
|
+
|
|
90
|
+
emitter.emit('event1')
|
|
91
|
+
|
|
92
|
+
expect(handler1).toHaveBeenCalledTimes(1)
|
|
93
|
+
expect(handler2).not.toHaveBeenCalled()
|
|
94
|
+
|
|
95
|
+
emitter.emit('event2')
|
|
96
|
+
|
|
97
|
+
expect(handler1).toHaveBeenCalledTimes(1)
|
|
98
|
+
expect(handler2).toHaveBeenCalledTimes(1)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('should clear all event handlers', () => {
|
|
102
|
+
const emitter = createEmitter()
|
|
103
|
+
const handler1 = mock(() => {})
|
|
104
|
+
const handler2 = mock(() => {})
|
|
105
|
+
|
|
106
|
+
emitter.on('event1', handler1)
|
|
107
|
+
emitter.on('event2', handler2)
|
|
108
|
+
|
|
109
|
+
emitter.clear()
|
|
110
|
+
|
|
111
|
+
emitter.emit('event1')
|
|
112
|
+
emitter.emit('event2')
|
|
113
|
+
|
|
114
|
+
expect(handler1).not.toHaveBeenCalled()
|
|
115
|
+
expect(handler2).not.toHaveBeenCalled()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('should pass multiple arguments to handlers', () => {
|
|
119
|
+
const emitter = createEmitter()
|
|
120
|
+
const handler = mock(() => {})
|
|
121
|
+
|
|
122
|
+
emitter.on('test', handler)
|
|
123
|
+
|
|
124
|
+
const arg1 = { id: 1 }
|
|
125
|
+
const arg2 = 'string'
|
|
126
|
+
const arg3 = [1, 2, 3]
|
|
127
|
+
|
|
128
|
+
emitter.emit('test', arg1, arg2, arg3)
|
|
129
|
+
|
|
130
|
+
expect(handler).toHaveBeenCalledWith(arg1, arg2, arg3)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('should do nothing when emitting event with no handlers', () => {
|
|
134
|
+
const emitter = createEmitter()
|
|
135
|
+
|
|
136
|
+
// This should not throw
|
|
137
|
+
expect(() => {
|
|
138
|
+
emitter.emit('nonexistent')
|
|
139
|
+
}).not.toThrow()
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// test/core/build/ripple.test.js
|
|
2
|
+
import { describe, test, expect, mock, spyOn } from 'bun:test'
|
|
3
|
+
import { createRipple } from '../../src/core/build/ripple'
|
|
4
|
+
import '../setup'
|
|
5
|
+
|
|
6
|
+
describe('Ripple Effect', () => {
|
|
7
|
+
test('should create a ripple controller', () => {
|
|
8
|
+
const ripple = createRipple()
|
|
9
|
+
expect(ripple).toBeDefined()
|
|
10
|
+
expect(ripple.mount).toBeInstanceOf(Function)
|
|
11
|
+
expect(ripple.unmount).toBeInstanceOf(Function)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('should mount ripple effect to an element', () => {
|
|
15
|
+
const ripple = createRipple()
|
|
16
|
+
const element = document.createElement('div')
|
|
17
|
+
|
|
18
|
+
// Element should start with static position
|
|
19
|
+
element.style.position = 'static'
|
|
20
|
+
|
|
21
|
+
ripple.mount(element)
|
|
22
|
+
|
|
23
|
+
// Position should now be relative for proper ripple positioning
|
|
24
|
+
expect(element.style.position).toBe('relative')
|
|
25
|
+
expect(element.style.overflow).toBe('hidden')
|
|
26
|
+
|
|
27
|
+
// Should have added mousedown event listener
|
|
28
|
+
expect(element.__handlers).toBeDefined()
|
|
29
|
+
expect(element.__handlers.mousedown).toBeDefined()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('should not fail when mounting to a null element', () => {
|
|
33
|
+
const ripple = createRipple()
|
|
34
|
+
expect(() => ripple.mount(null)).not.toThrow()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('should create ripple element on mousedown', () => {
|
|
38
|
+
const ripple = createRipple()
|
|
39
|
+
const element = document.createElement('div')
|
|
40
|
+
|
|
41
|
+
// Spy on appendChild to verify ripple element creation
|
|
42
|
+
const appendChildSpy = spyOn(element, 'appendChild')
|
|
43
|
+
|
|
44
|
+
ripple.mount(element)
|
|
45
|
+
|
|
46
|
+
// Simulate mousedown event
|
|
47
|
+
const mouseEvent = {
|
|
48
|
+
type: 'mousedown',
|
|
49
|
+
offsetX: 10,
|
|
50
|
+
offsetY: 20,
|
|
51
|
+
target: element
|
|
52
|
+
}
|
|
53
|
+
element.__handlers.mousedown[0](mouseEvent)
|
|
54
|
+
|
|
55
|
+
// Should have created and appended a ripple element
|
|
56
|
+
expect(appendChildSpy).toHaveBeenCalled()
|
|
57
|
+
expect(appendChildSpy.mock.calls[0][0].className).toBe('ripple')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('should add document cleanup event listeners', () => {
|
|
61
|
+
const ripple = createRipple()
|
|
62
|
+
const element = document.createElement('div')
|
|
63
|
+
|
|
64
|
+
// Mock document event listeners
|
|
65
|
+
const docAddEventListener = mock(() => {})
|
|
66
|
+
const originalDocAddEventListener = document.addEventListener
|
|
67
|
+
document.addEventListener = docAddEventListener
|
|
68
|
+
|
|
69
|
+
ripple.mount(element)
|
|
70
|
+
|
|
71
|
+
// Simulate mousedown event
|
|
72
|
+
const mouseEvent = {
|
|
73
|
+
type: 'mousedown',
|
|
74
|
+
offsetX: 10,
|
|
75
|
+
offsetY: 20,
|
|
76
|
+
target: element
|
|
77
|
+
}
|
|
78
|
+
element.__handlers.mousedown[0](mouseEvent)
|
|
79
|
+
|
|
80
|
+
// Should have added mouseup and mouseleave event listeners to document
|
|
81
|
+
expect(docAddEventListener).toHaveBeenCalledTimes(2)
|
|
82
|
+
expect(docAddEventListener.mock.calls[0][0]).toBe('mouseup')
|
|
83
|
+
expect(docAddEventListener.mock.calls[1][0]).toBe('mouseleave')
|
|
84
|
+
|
|
85
|
+
// Restore original
|
|
86
|
+
document.addEventListener = originalDocAddEventListener
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('should remove ripple elements on unmount', () => {
|
|
90
|
+
const ripple = createRipple()
|
|
91
|
+
const element = document.createElement('div')
|
|
92
|
+
|
|
93
|
+
// Add a few ripple elements
|
|
94
|
+
const ripple1 = document.createElement('div')
|
|
95
|
+
ripple1.className = 'ripple'
|
|
96
|
+
const ripple2 = document.createElement('div')
|
|
97
|
+
ripple2.className = 'ripple'
|
|
98
|
+
|
|
99
|
+
element.appendChild(ripple1)
|
|
100
|
+
element.appendChild(ripple2)
|
|
101
|
+
|
|
102
|
+
// Mock the querySelectorAll and forEach methods
|
|
103
|
+
element.querySelectorAll = (selector) => {
|
|
104
|
+
if (selector === '.ripple') {
|
|
105
|
+
return [ripple1, ripple2]
|
|
106
|
+
}
|
|
107
|
+
return []
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const removeSpy1 = spyOn(ripple1, 'remove')
|
|
111
|
+
const removeSpy2 = spyOn(ripple2, 'remove')
|
|
112
|
+
|
|
113
|
+
ripple.unmount(element)
|
|
114
|
+
|
|
115
|
+
// Should have removed both ripple elements
|
|
116
|
+
expect(removeSpy1).toHaveBeenCalled()
|
|
117
|
+
expect(removeSpy2).toHaveBeenCalled()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// test('should handle custom config options', () => {
|
|
121
|
+
// const customConfig = {
|
|
122
|
+
// duration: 500,
|
|
123
|
+
// timing: 'ease-out',
|
|
124
|
+
// opacity: ['0.8', '0.2']
|
|
125
|
+
// }
|
|
126
|
+
|
|
127
|
+
// const ripple = createRipple(customConfig)
|
|
128
|
+
// const element = document.createElement('div')
|
|
129
|
+
|
|
130
|
+
// // Spy on appendChild to capture the ripple element
|
|
131
|
+
// let capturedRipple
|
|
132
|
+
// const originalAppendChild = element.appendChild
|
|
133
|
+
// element.appendChild = (child) => {
|
|
134
|
+
// capturedRipple = child
|
|
135
|
+
// return originalAppendChild.call(element, child)
|
|
136
|
+
// }
|
|
137
|
+
|
|
138
|
+
// ripple.mount(element)
|
|
139
|
+
|
|
140
|
+
// // Simulate mousedown event
|
|
141
|
+
// const mouseEvent = {
|
|
142
|
+
// type: 'mousedown',
|
|
143
|
+
// offsetX: 10,
|
|
144
|
+
// offsetY: 20,
|
|
145
|
+
// target: element
|
|
146
|
+
// }
|
|
147
|
+
// element.__handlers.mousedown[0](mouseEvent)
|
|
148
|
+
|
|
149
|
+
// // Verify custom config was applied
|
|
150
|
+
// expect(capturedRipple.style.transition).toContain(`${customConfig.duration}ms`)
|
|
151
|
+
// expect(capturedRipple.style.transition).toContain(customConfig.timing)
|
|
152
|
+
// expect(capturedRipple.style.opacity).toBe(customConfig.opacity[0])
|
|
153
|
+
|
|
154
|
+
// // Force reflow simulation
|
|
155
|
+
// capturedRipple.offsetHeight
|
|
156
|
+
|
|
157
|
+
// // Check end opacity is applied after animation
|
|
158
|
+
// expect(capturedRipple.style.opacity).toBe(customConfig.opacity[1])
|
|
159
|
+
// })
|
|
160
|
+
|
|
161
|
+
test('should not fail when unmounting a null element', () => {
|
|
162
|
+
const ripple = createRipple()
|
|
163
|
+
expect(() => ripple.unmount(null)).not.toThrow()
|
|
164
|
+
})
|
|
165
|
+
})
|