mtrl 0.0.1 → 0.0.3
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/TESTING.md +104 -0
- package/package.json +6 -3
- package/src/components/button/config.js +3 -1
- package/src/components/checkbox/checkbox.js +1 -1
- package/src/components/checkbox/styles.scss +5 -5
- package/src/components/list/constants.js +5 -0
- package/src/components/list/list-item.js +4 -12
- package/src/components/list/list.js +19 -11
- package/src/components/menu/features/items-manager.js +5 -1
- package/src/components/navigation/constants.js +19 -54
- package/src/components/switch/styles.scss +18 -1
- package/src/components/switch/switch.js +1 -1
- package/src/core/build/ripple.js +76 -9
- package/src/core/compose/features/disabled.js +55 -16
- package/src/core/compose/features/input.js +9 -1
- package/src/core/compose/features/textinput.js +16 -20
- package/src/core/state/disabled.js +41 -4
- package/test/components/button.test.js +170 -0
- package/test/components/checkbox.test.js +238 -0
- package/test/components/list.test.js +105 -0
- package/test/components/menu.test.js +385 -0
- package/test/components/navigation.test.js +227 -0
- package/test/components/snackbar.test.js +234 -0
- package/test/components/switch.test.js +186 -0
- package/test/components/textfield.test.js +314 -0
- package/test/core/emitter.test.js +141 -0
- package/test/core/ripple.test.js +66 -0
- package/test/setup.js +371 -0
package/TESTING.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Testing MTRL Components
|
|
2
|
+
|
|
3
|
+
This document provides guidelines for writing and running tests for the MTRL library. We use Bun's built-in test runner for fast, efficient testing.
|
|
4
|
+
|
|
5
|
+
## Running Tests
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Run all tests
|
|
9
|
+
bun test
|
|
10
|
+
|
|
11
|
+
# Run tests in watch mode (re-runs tests when files change)
|
|
12
|
+
bun test --watch
|
|
13
|
+
|
|
14
|
+
# Run tests with coverage report
|
|
15
|
+
bun test --coverage
|
|
16
|
+
|
|
17
|
+
# Run tests with UI
|
|
18
|
+
bun test --watch --ui
|
|
19
|
+
|
|
20
|
+
# Run specific test file or pattern
|
|
21
|
+
bun test test/components/button.test.js
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Test Structure
|
|
25
|
+
|
|
26
|
+
Tests are organized to mirror the source code structure:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
test/
|
|
30
|
+
├── setup.js # Global test setup and DOM mocking
|
|
31
|
+
├── components/ # Component tests
|
|
32
|
+
│ ├── button.test.js
|
|
33
|
+
│ ├── textfield.test.js
|
|
34
|
+
│ └── ...
|
|
35
|
+
└── core/ # Core functionality tests
|
|
36
|
+
├── build/
|
|
37
|
+
│ └── ripple.test.js
|
|
38
|
+
├── dom/
|
|
39
|
+
│ └── ...
|
|
40
|
+
└── state/
|
|
41
|
+
└── emitter.test.js
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Writing Tests
|
|
45
|
+
|
|
46
|
+
When writing tests for MTRL components, follow these guidelines:
|
|
47
|
+
|
|
48
|
+
### 1. Test Component Creation
|
|
49
|
+
|
|
50
|
+
Always verify that components are created correctly with default options:
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
test('should create a component element', () => {
|
|
54
|
+
const component = createComponent();
|
|
55
|
+
expect(component.element).toBeDefined();
|
|
56
|
+
expect(component.element.tagName).toBe('DIV');
|
|
57
|
+
expect(component.element.className).toContain('mtrl-component');
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 2. Test Configuration Options
|
|
62
|
+
|
|
63
|
+
Test that configuration options properly affect the component:
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
test('should apply variant class', () => {
|
|
67
|
+
const variant = 'filled';
|
|
68
|
+
const component = createComponent({
|
|
69
|
+
variant
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(component.element.className).toContain(`mtrl-component--${variant}`);
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 3. Test Events
|
|
77
|
+
|
|
78
|
+
Verify that events are properly emitted and handled:
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
test('should handle click events', () => {
|
|
82
|
+
const component = createComponent();
|
|
83
|
+
const handleClick = mock(() => {});
|
|
84
|
+
|
|
85
|
+
component.on('click', handleClick);
|
|
86
|
+
|
|
87
|
+
// Simulate event
|
|
88
|
+
const event = new Event('click');
|
|
89
|
+
component.element.dispatchEvent(event);
|
|
90
|
+
|
|
91
|
+
expect(handleClick).toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 4. Test State Changes
|
|
96
|
+
|
|
97
|
+
Test component state changes through its API:
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
test('should support disabled state', () => {
|
|
101
|
+
const component = createComponent();
|
|
102
|
+
|
|
103
|
+
// Initially not disabled
|
|
104
|
+
expect(component.element.has
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mtrl",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "A functional JavaScript component library with composable architecture based on Material Design 3",
|
|
5
5
|
"keywords": ["component", "library", "ui", "user interface", "functional", "composable"],
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "
|
|
8
|
+
"test": "bun test",
|
|
9
|
+
"test:watch": "bun test --watch",
|
|
10
|
+
"test:coverage": "bun test --coverage",
|
|
11
|
+
"test:ui": "bun test --watch --ui"
|
|
9
12
|
},
|
|
10
13
|
"repository": {
|
|
11
14
|
"type": "git",
|
|
@@ -13,5 +16,5 @@
|
|
|
13
16
|
},
|
|
14
17
|
|
|
15
18
|
"author": "floor",
|
|
16
|
-
"license": "
|
|
19
|
+
"license": "MIT License"
|
|
17
20
|
}
|
|
@@ -82,7 +82,7 @@ const createCheckbox = (config = {}) => {
|
|
|
82
82
|
withCheckIcon(baseConfig),
|
|
83
83
|
withTextLabel(baseConfig),
|
|
84
84
|
enhancedWithCheckable,
|
|
85
|
-
withDisabled(),
|
|
85
|
+
withDisabled(baseConfig), // Pass the baseConfig to withDisabled
|
|
86
86
|
withLifecycle(),
|
|
87
87
|
comp => withAPI({
|
|
88
88
|
disabled: comp.disabled,
|
|
@@ -157,13 +157,13 @@
|
|
|
157
157
|
&::before {
|
|
158
158
|
content: '';
|
|
159
159
|
position: absolute;
|
|
160
|
-
top: -
|
|
161
|
-
left: -
|
|
162
|
-
right: -
|
|
163
|
-
bottom: -
|
|
160
|
+
top: -12px;
|
|
161
|
+
left: -12px;
|
|
162
|
+
right: -12px;
|
|
163
|
+
bottom: -12px;
|
|
164
164
|
background-color: var(--mtrl-sys-color-on-surface);
|
|
165
165
|
opacity: 0.08;
|
|
166
|
-
border-radius:
|
|
166
|
+
border-radius: 50%;
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
}
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
// src/components/list/list-item.js
|
|
2
|
-
|
|
3
2
|
import { PREFIX } from '../../core/config'
|
|
4
3
|
import { pipe } from '../../core/compose'
|
|
5
4
|
import { createBase, withElement } from '../../core/compose/component'
|
|
6
5
|
import { withEvents, withDisabled } from '../../core/compose/features'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Supported list item layouts
|
|
10
|
-
*/
|
|
11
|
-
export const LIST_ITEM_LAYOUTS = {
|
|
12
|
-
HORIZONTAL: 'horizontal', // Default horizontal layout
|
|
13
|
-
VERTICAL: 'vertical' // Stacked layout with vertical alignment
|
|
14
|
-
}
|
|
6
|
+
import { LIST_ITEM_LAYOUTS } from './constants'
|
|
15
7
|
|
|
16
8
|
/**
|
|
17
9
|
* Creates a DOM element with optional class and content
|
|
@@ -56,8 +48,7 @@ const createListItem = (config = {}) => {
|
|
|
56
48
|
}
|
|
57
49
|
|
|
58
50
|
const createContent = (component) => {
|
|
59
|
-
const { element } = component
|
|
60
|
-
const { prefix } = baseConfig
|
|
51
|
+
const { element, prefix } = component
|
|
61
52
|
const isVertical = config.layout === LIST_ITEM_LAYOUTS.VERTICAL
|
|
62
53
|
|
|
63
54
|
// Create content container
|
|
@@ -137,7 +128,8 @@ const createListItem = (config = {}) => {
|
|
|
137
128
|
tag: 'div',
|
|
138
129
|
role: config.role || 'listitem',
|
|
139
130
|
componentName: 'list-item',
|
|
140
|
-
|
|
131
|
+
// Ensure that every list item includes the prefix-based class
|
|
132
|
+
className: `${baseConfig.prefix}-list-item ${config.layout === LIST_ITEM_LAYOUTS.VERTICAL ? 'vertical' : ''} ${config.class || ''}`.trim()
|
|
141
133
|
}),
|
|
142
134
|
withDisabled(),
|
|
143
135
|
createContent
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// src/components/list/list.js
|
|
2
|
-
|
|
3
1
|
import { PREFIX } from '../../core/config'
|
|
4
2
|
import { pipe } from '../../core/compose'
|
|
5
3
|
import { createBase, withElement } from '../../core/compose/component'
|
|
@@ -56,29 +54,29 @@ const createList = (config = {}) => {
|
|
|
56
54
|
const focusedItem = document.activeElement
|
|
57
55
|
if (!focusedItem?.classList.contains(`${prefix}-list-item`)) return
|
|
58
56
|
|
|
59
|
-
const
|
|
60
|
-
const currentIndex =
|
|
57
|
+
const allItems = Array.from(element.querySelectorAll(`.${prefix}-list-item`))
|
|
58
|
+
const currentIndex = allItems.indexOf(focusedItem)
|
|
61
59
|
|
|
62
60
|
switch (event.key) {
|
|
63
61
|
case 'ArrowDown':
|
|
64
62
|
case 'ArrowRight':
|
|
65
63
|
event.preventDefault()
|
|
66
|
-
const nextItem =
|
|
64
|
+
const nextItem = allItems[currentIndex + 1]
|
|
67
65
|
if (nextItem) nextItem.focus()
|
|
68
66
|
break
|
|
69
67
|
case 'ArrowUp':
|
|
70
68
|
case 'ArrowLeft':
|
|
71
69
|
event.preventDefault()
|
|
72
|
-
const prevItem =
|
|
70
|
+
const prevItem = allItems[currentIndex - 1]
|
|
73
71
|
if (prevItem) prevItem.focus()
|
|
74
72
|
break
|
|
75
73
|
case 'Home':
|
|
76
74
|
event.preventDefault()
|
|
77
|
-
|
|
75
|
+
allItems[0]?.focus()
|
|
78
76
|
break
|
|
79
77
|
case 'End':
|
|
80
78
|
event.preventDefault()
|
|
81
|
-
|
|
79
|
+
allItems[allItems.length - 1]?.focus()
|
|
82
80
|
break
|
|
83
81
|
case ' ':
|
|
84
82
|
case 'Enter':
|
|
@@ -187,7 +185,7 @@ const createList = (config = {}) => {
|
|
|
187
185
|
|
|
188
186
|
element.addEventListener('keydown', handleKeyDown)
|
|
189
187
|
|
|
190
|
-
// Clean up
|
|
188
|
+
// Clean up lifecycle if defined
|
|
191
189
|
if (component.lifecycle) {
|
|
192
190
|
const originalDestroy = component.lifecycle.destroy
|
|
193
191
|
component.lifecycle.destroy = () => {
|
|
@@ -248,12 +246,14 @@ const createList = (config = {}) => {
|
|
|
248
246
|
}
|
|
249
247
|
}
|
|
250
248
|
|
|
251
|
-
|
|
249
|
+
const list = pipe(
|
|
252
250
|
createBase,
|
|
253
251
|
withEvents(),
|
|
254
252
|
withElement({
|
|
255
253
|
tag: 'div',
|
|
256
|
-
|
|
254
|
+
// Use role "list" for default (non-selectable) lists,
|
|
255
|
+
// and "listbox" for interactive, selectable types.
|
|
256
|
+
role: (!config.type || config.type === LIST_TYPES.DEFAULT) ? 'list' : 'listbox',
|
|
257
257
|
'aria-multiselectable': config.type === LIST_TYPES.MULTI_SELECT ? 'true' : undefined,
|
|
258
258
|
componentName: LIST_CLASSES.ROOT,
|
|
259
259
|
className: config.class
|
|
@@ -262,6 +262,14 @@ const createList = (config = {}) => {
|
|
|
262
262
|
withLifecycle(),
|
|
263
263
|
createContent
|
|
264
264
|
)(baseConfig)
|
|
265
|
+
|
|
266
|
+
// Ensure that for default lists, the role is correctly "list"
|
|
267
|
+
if (!config.type || config.type === LIST_TYPES.DEFAULT) {
|
|
268
|
+
list.element.setAttribute('role', 'list')
|
|
269
|
+
}
|
|
270
|
+
// Expose the prefix on the returned component for testing purposes.
|
|
271
|
+
list.prefix = baseConfig.prefix
|
|
272
|
+
return list
|
|
265
273
|
}
|
|
266
274
|
|
|
267
275
|
export default createList
|
|
@@ -331,6 +331,10 @@ export const withItemsManager = (config) => (component) => {
|
|
|
331
331
|
removeItem (name) {
|
|
332
332
|
if (!name) return this
|
|
333
333
|
|
|
334
|
+
// First, ensure we remove the item from our internal map
|
|
335
|
+
itemsMap.delete(name)
|
|
336
|
+
|
|
337
|
+
// Now try to remove the item from the DOM
|
|
334
338
|
const item = list.querySelector(`[data-name="${name}"]`)
|
|
335
339
|
if (item) {
|
|
336
340
|
// Remove event listeners
|
|
@@ -344,8 +348,8 @@ export const withItemsManager = (config) => (component) => {
|
|
|
344
348
|
submenus.delete(name)
|
|
345
349
|
}
|
|
346
350
|
|
|
351
|
+
// Remove the item from the DOM
|
|
347
352
|
item.remove()
|
|
348
|
-
itemsMap.delete(name)
|
|
349
353
|
}
|
|
350
354
|
|
|
351
355
|
return this
|
|
@@ -121,8 +121,17 @@ export const NAV_SCHEMA = {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Navigation item states
|
|
126
|
+
*/
|
|
127
|
+
export const NAV_ITEM_STATES = {
|
|
128
|
+
EXPANDED: 'expanded',
|
|
129
|
+
COLLAPSED: 'collapsed'
|
|
130
|
+
}
|
|
131
|
+
|
|
124
132
|
/**
|
|
125
133
|
* Navigation item schema
|
|
134
|
+
* Enhanced with support for nested items
|
|
126
135
|
*/
|
|
127
136
|
export const NAV_ITEM_SCHEMA = {
|
|
128
137
|
type: 'object',
|
|
@@ -154,82 +163,38 @@ export const NAV_ITEM_SCHEMA = {
|
|
|
154
163
|
groupId: {
|
|
155
164
|
type: 'string',
|
|
156
165
|
optional: true
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Navigation group schema
|
|
163
|
-
*/
|
|
164
|
-
export const NAV_GROUP_SCHEMA = {
|
|
165
|
-
type: 'object',
|
|
166
|
-
properties: {
|
|
167
|
-
id: {
|
|
168
|
-
type: 'string',
|
|
169
|
-
required: true
|
|
170
166
|
},
|
|
171
|
-
|
|
172
|
-
type: '
|
|
173
|
-
|
|
167
|
+
items: {
|
|
168
|
+
type: 'array',
|
|
169
|
+
optional: true,
|
|
170
|
+
description: 'Nested navigation items'
|
|
174
171
|
},
|
|
175
172
|
expanded: {
|
|
176
173
|
type: 'boolean',
|
|
177
174
|
optional: true,
|
|
178
|
-
default:
|
|
175
|
+
default: false
|
|
179
176
|
}
|
|
180
177
|
}
|
|
181
178
|
}
|
|
182
179
|
|
|
183
180
|
/**
|
|
184
|
-
* Navigation
|
|
181
|
+
* Navigation group schema
|
|
185
182
|
*/
|
|
186
|
-
export const
|
|
187
|
-
EXPANDED: 'expanded',
|
|
188
|
-
COLLAPSED: 'collapsed'
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
// Update NAV_ITEM_SCHEMA to support nested items
|
|
193
|
-
export const NAV_ITEM_SCHEMA = {
|
|
183
|
+
export const NAV_GROUP_SCHEMA = {
|
|
194
184
|
type: 'object',
|
|
195
185
|
properties: {
|
|
196
186
|
id: {
|
|
197
187
|
type: 'string',
|
|
198
188
|
required: true
|
|
199
189
|
},
|
|
200
|
-
|
|
201
|
-
type: 'string',
|
|
202
|
-
required: true
|
|
203
|
-
},
|
|
204
|
-
label: {
|
|
190
|
+
title: {
|
|
205
191
|
type: 'string',
|
|
206
192
|
required: true
|
|
207
193
|
},
|
|
208
|
-
badge: {
|
|
209
|
-
type: 'string',
|
|
210
|
-
optional: true
|
|
211
|
-
},
|
|
212
|
-
disabled: {
|
|
213
|
-
type: 'boolean',
|
|
214
|
-
optional: true
|
|
215
|
-
},
|
|
216
|
-
subtitle: {
|
|
217
|
-
type: 'string',
|
|
218
|
-
optional: true
|
|
219
|
-
},
|
|
220
|
-
groupId: {
|
|
221
|
-
type: 'string',
|
|
222
|
-
optional: true
|
|
223
|
-
},
|
|
224
|
-
items: {
|
|
225
|
-
type: 'array',
|
|
226
|
-
optional: true,
|
|
227
|
-
description: 'Nested navigation items'
|
|
228
|
-
},
|
|
229
194
|
expanded: {
|
|
230
195
|
type: 'boolean',
|
|
231
196
|
optional: true,
|
|
232
|
-
default:
|
|
197
|
+
default: true
|
|
233
198
|
}
|
|
234
199
|
}
|
|
235
|
-
}
|
|
200
|
+
}
|
|
@@ -136,7 +136,24 @@
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
&--disabled {
|
|
139
|
-
opacity: .
|
|
139
|
+
opacity: 0.38;
|
|
140
|
+
|
|
141
|
+
// Specific styles for disabled + checked
|
|
142
|
+
&.#{c.$prefix}-switch--checked {
|
|
143
|
+
.#{c.$prefix}-switch-track {
|
|
144
|
+
background-color: var(--mtrl-sys-color-outline);
|
|
145
|
+
border-color: var(--mtrl-sys-color-outline);
|
|
146
|
+
opacity: 0.38;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.#{c.$prefix}-switch-thumb {
|
|
150
|
+
background-color: var(--mtrl-sys-color-on-primary);
|
|
151
|
+
opacity: 1;
|
|
152
|
+
&-icon {
|
|
153
|
+
color: var(--mtrl-sys-color-outline)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
140
157
|
}
|
|
141
158
|
|
|
142
159
|
// Hover effects
|
|
@@ -58,7 +58,7 @@ const createSwitch = (config = {}) => {
|
|
|
58
58
|
withTextLabel(baseConfig),
|
|
59
59
|
withLabelPosition(baseConfig),
|
|
60
60
|
withCheckable(baseConfig),
|
|
61
|
-
withDisabled(),
|
|
61
|
+
withDisabled(baseConfig), // Pass the config to ensure disabled state is properly initialized
|
|
62
62
|
withLifecycle(),
|
|
63
63
|
comp => withAPI({
|
|
64
64
|
disabled: comp.disabled,
|
package/src/core/build/ripple.js
CHANGED
|
@@ -13,7 +13,13 @@ const DEFAULT_CONFIG = RIPPLE_CONFIG
|
|
|
13
13
|
* @returns {Object} Ripple controller instance
|
|
14
14
|
*/
|
|
15
15
|
export const createRipple = (config = {}) => {
|
|
16
|
-
|
|
16
|
+
// Make sure we fully merge the config options
|
|
17
|
+
const options = {
|
|
18
|
+
...DEFAULT_CONFIG,
|
|
19
|
+
...config,
|
|
20
|
+
// Handle nested objects like opacity array
|
|
21
|
+
opacity: config.opacity || DEFAULT_CONFIG.opacity
|
|
22
|
+
}
|
|
17
23
|
|
|
18
24
|
const getEndCoordinates = (bounds) => {
|
|
19
25
|
const size = Math.max(bounds.width, bounds.height)
|
|
@@ -36,7 +42,29 @@ export const createRipple = (config = {}) => {
|
|
|
36
42
|
return ripple
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
// Store document event listeners for cleanup
|
|
46
|
+
let documentListeners = []
|
|
47
|
+
|
|
48
|
+
// Safe document event handling
|
|
49
|
+
const addDocumentListener = (event, handler) => {
|
|
50
|
+
if (typeof document.addEventListener === 'function') {
|
|
51
|
+
document.addEventListener(event, handler)
|
|
52
|
+
documentListeners.push({ event, handler })
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const removeDocumentListener = (event, handler) => {
|
|
57
|
+
if (typeof document.removeEventListener === 'function') {
|
|
58
|
+
document.removeEventListener(event, handler)
|
|
59
|
+
documentListeners = documentListeners.filter(
|
|
60
|
+
listener => !(listener.event === event && listener.handler === handler)
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
39
65
|
const animate = (event, container) => {
|
|
66
|
+
if (!container) return
|
|
67
|
+
|
|
40
68
|
const bounds = container.getBoundingClientRect()
|
|
41
69
|
const ripple = createRippleElement()
|
|
42
70
|
|
|
@@ -49,7 +77,10 @@ export const createRipple = (config = {}) => {
|
|
|
49
77
|
})
|
|
50
78
|
|
|
51
79
|
container.appendChild(ripple)
|
|
52
|
-
|
|
80
|
+
|
|
81
|
+
// Force reflow
|
|
82
|
+
// eslint-disable-next-line no-unused-expressions
|
|
83
|
+
ripple.offsetHeight
|
|
53
84
|
|
|
54
85
|
// Animate to end position
|
|
55
86
|
const end = getEndCoordinates(bounds)
|
|
@@ -61,13 +92,20 @@ export const createRipple = (config = {}) => {
|
|
|
61
92
|
|
|
62
93
|
const cleanup = () => {
|
|
63
94
|
ripple.style.opacity = '0'
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
95
|
+
|
|
96
|
+
// Use setTimeout to remove element after animation
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
if (ripple.parentNode) {
|
|
99
|
+
ripple.parentNode.removeChild(ripple)
|
|
100
|
+
}
|
|
101
|
+
}, options.duration)
|
|
102
|
+
|
|
103
|
+
removeDocumentListener('mouseup', cleanup)
|
|
104
|
+
removeDocumentListener('mouseleave', cleanup)
|
|
67
105
|
}
|
|
68
106
|
|
|
69
|
-
|
|
70
|
-
|
|
107
|
+
addDocumentListener('mouseup', cleanup)
|
|
108
|
+
addDocumentListener('mouseleave', cleanup)
|
|
71
109
|
}
|
|
72
110
|
|
|
73
111
|
return {
|
|
@@ -81,12 +119,41 @@ export const createRipple = (config = {}) => {
|
|
|
81
119
|
}
|
|
82
120
|
element.style.overflow = 'hidden'
|
|
83
121
|
|
|
84
|
-
|
|
122
|
+
// Store the mousedown handler to be able to remove it later
|
|
123
|
+
const mousedownHandler = (e) => animate(e, element)
|
|
124
|
+
|
|
125
|
+
// Store handler reference on the element
|
|
126
|
+
if (!element.__rippleHandlers) {
|
|
127
|
+
element.__rippleHandlers = []
|
|
128
|
+
}
|
|
129
|
+
element.__rippleHandlers.push(mousedownHandler)
|
|
130
|
+
|
|
131
|
+
element.addEventListener('mousedown', mousedownHandler)
|
|
85
132
|
},
|
|
86
133
|
|
|
87
134
|
unmount: (element) => {
|
|
88
135
|
if (!element) return
|
|
89
|
-
|
|
136
|
+
|
|
137
|
+
// Clear document event listeners
|
|
138
|
+
documentListeners.forEach(({ event, handler }) => {
|
|
139
|
+
removeDocumentListener(event, handler)
|
|
140
|
+
})
|
|
141
|
+
documentListeners = []
|
|
142
|
+
|
|
143
|
+
// Remove event listeners
|
|
144
|
+
if (element.__rippleHandlers) {
|
|
145
|
+
element.__rippleHandlers.forEach(handler => {
|
|
146
|
+
element.removeEventListener('mousedown', handler)
|
|
147
|
+
})
|
|
148
|
+
element.__rippleHandlers = []
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Remove all ripple elements
|
|
152
|
+
const ripples = element.querySelectorAll('.ripple')
|
|
153
|
+
ripples.forEach(ripple => {
|
|
154
|
+
// Call remove directly to match the test expectation
|
|
155
|
+
ripple.remove()
|
|
156
|
+
})
|
|
90
157
|
}
|
|
91
158
|
}
|
|
92
159
|
}
|
|
@@ -1,25 +1,64 @@
|
|
|
1
1
|
// src/core/compose/features/disabled.js
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Adds disabled state management to a component
|
|
5
|
+
* @param {Object} config - Disabled configuration
|
|
6
|
+
* @returns {Function} Component enhancer
|
|
7
|
+
*/
|
|
8
|
+
export const withDisabled = (config = {}) => (component) => {
|
|
9
|
+
// Get the disabled class based on component name
|
|
10
|
+
const disabledClass = `${component.getClass(component.componentName)}--disabled`
|
|
5
11
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
// Directly implement disabled functionality
|
|
13
|
+
const disabled = {
|
|
14
|
+
enable () {
|
|
15
|
+
component.element.classList.remove(disabledClass)
|
|
16
|
+
if (component.input) {
|
|
17
|
+
component.input.disabled = false
|
|
18
|
+
component.input.removeAttribute('disabled')
|
|
19
|
+
} else {
|
|
11
20
|
component.element.disabled = false
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
component.element.removeAttribute('disabled')
|
|
22
|
+
}
|
|
23
|
+
return this
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
disable () {
|
|
27
|
+
component.element.classList.add(disabledClass)
|
|
28
|
+
if (component.input) {
|
|
29
|
+
component.input.disabled = true
|
|
30
|
+
component.input.setAttribute('disabled', 'true')
|
|
31
|
+
} else {
|
|
18
32
|
component.element.disabled = true
|
|
19
|
-
|
|
20
|
-
component.element.classList.add(className)
|
|
21
|
-
return this
|
|
33
|
+
component.element.setAttribute('disabled', 'true')
|
|
22
34
|
}
|
|
35
|
+
return this
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
toggle () {
|
|
39
|
+
if (this.isDisabled()) {
|
|
40
|
+
this.enable()
|
|
41
|
+
} else {
|
|
42
|
+
this.disable()
|
|
43
|
+
}
|
|
44
|
+
return this
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
isDisabled () {
|
|
48
|
+
return component.input ? component.input.disabled : component.element.disabled
|
|
23
49
|
}
|
|
24
50
|
}
|
|
51
|
+
|
|
52
|
+
// Initialize disabled state if configured
|
|
53
|
+
if (config.disabled) {
|
|
54
|
+
// Use requestAnimationFrame to ensure DOM is ready
|
|
55
|
+
requestAnimationFrame(() => {
|
|
56
|
+
disabled.disable()
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
...component,
|
|
62
|
+
disabled
|
|
63
|
+
}
|
|
25
64
|
}
|