mtrl 0.2.9 → 0.3.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/CLAUDE.md +33 -0
- package/package.json +3 -1
- package/src/components/button/button.ts +34 -5
- package/src/components/navigation/index.ts +4 -1
- package/src/components/navigation/system/core.ts +302 -0
- package/src/components/navigation/system/events.ts +240 -0
- package/src/components/navigation/system/index.ts +184 -0
- package/src/components/navigation/system/mobile.ts +278 -0
- package/src/components/navigation/system/state.ts +77 -0
- package/src/components/navigation/system/types.ts +364 -0
- package/src/components/navigation/types.ts +33 -0
- package/src/components/slider/config.ts +2 -2
- package/src/components/slider/features/controller.ts +1 -25
- package/src/components/slider/features/handlers.ts +0 -1
- package/src/components/slider/features/range.ts +7 -7
- package/src/components/slider/{structure.ts → schema.ts} +2 -13
- package/src/components/slider/slider.ts +3 -2
- package/src/components/snackbar/index.ts +7 -1
- package/src/components/snackbar/types.ts +25 -0
- package/src/components/switch/api.ts +16 -0
- package/src/components/switch/config.ts +1 -18
- package/src/components/switch/features.ts +198 -0
- package/src/components/switch/index.ts +6 -1
- package/src/components/switch/switch.ts +3 -3
- package/src/components/switch/types.ts +27 -2
- package/src/components/textfield/index.ts +7 -1
- package/src/components/textfield/types.ts +36 -0
- package/src/core/composition/features/dom.ts +26 -14
- package/src/core/composition/features/icon.ts +18 -18
- package/src/core/composition/features/index.ts +3 -2
- package/src/core/composition/features/label.ts +16 -17
- package/src/core/composition/features/layout.ts +47 -0
- package/src/core/composition/index.ts +4 -4
- package/src/core/layout/README.md +350 -0
- package/src/core/layout/array.ts +181 -0
- package/src/core/layout/create.ts +55 -0
- package/src/core/layout/index.ts +26 -0
- package/src/core/layout/object.ts +124 -0
- package/src/core/layout/processor.ts +58 -0
- package/src/core/layout/result.ts +85 -0
- package/src/core/layout/types.ts +125 -0
- package/src/core/layout/utils.ts +136 -0
- package/src/styles/abstract/_variables.scss +28 -0
- package/src/styles/components/_switch.scss +133 -69
- package/src/styles/components/_textfield.scss +9 -16
- package/test/components/badge.test.ts +545 -0
- package/test/components/bottom-app-bar.test.ts +303 -0
- package/test/components/button.test.ts +233 -0
- package/test/components/card.test.ts +560 -0
- package/test/components/carousel.test.ts +951 -0
- package/test/components/checkbox.test.ts +462 -0
- package/test/components/chip.test.ts +692 -0
- package/test/components/datepicker.test.ts +1124 -0
- package/test/components/dialog.test.ts +990 -0
- package/test/components/divider.test.ts +412 -0
- package/test/components/extended-fab.test.ts +672 -0
- package/test/components/fab.test.ts +561 -0
- package/test/components/list.test.ts +365 -0
- package/test/components/menu.test.ts +718 -0
- package/test/components/navigation.test.ts +186 -0
- package/test/components/progress.test.ts +567 -0
- package/test/components/radios.test.ts +699 -0
- package/test/components/search.test.ts +1135 -0
- package/test/components/segmented-button.test.ts +732 -0
- package/test/components/sheet.test.ts +641 -0
- package/test/components/slider.test.ts +1220 -0
- package/test/components/snackbar.test.ts +461 -0
- package/test/components/switch.test.ts +452 -0
- package/test/components/tabs.test.ts +1369 -0
- package/test/components/textfield.test.ts +400 -0
- package/test/components/timepicker.test.ts +592 -0
- package/test/components/tooltip.test.ts +630 -0
- package/test/components/top-app-bar.test.ts +566 -0
- package/test/core/dom.attributes.test.ts +148 -0
- package/test/core/dom.classes.test.ts +152 -0
- package/test/core/dom.events.test.ts +243 -0
- package/test/core/emitter.test.ts +141 -0
- package/test/core/ripple.test.ts +99 -0
- package/test/core/state.store.test.ts +189 -0
- package/test/core/utils.normalize.test.ts +61 -0
- package/test/core/utils.object.test.ts +120 -0
- package/test/setup.ts +451 -0
- package/tsconfig.json +2 -2
- package/src/components/navigation/system-types.ts +0 -124
- package/src/components/navigation/system.ts +0 -776
- package/src/components/snackbar/constants.ts +0 -26
- package/src/core/composition/features/structure.ts +0 -22
- package/src/core/layout/index.js +0 -95
- package/src/core/structure.ts +0 -288
- package/test/components/button.test.js +0 -170
- package/test/components/checkbox.test.js +0 -238
- package/test/components/list.test.js +0 -105
- package/test/components/menu.test.js +0 -385
- package/test/components/navigation.test.js +0 -227
- package/test/components/snackbar.test.js +0 -234
- package/test/components/switch.test.js +0 -186
- package/test/components/textfield.test.js +0 -314
- package/test/core/emitter.test.js +0 -141
- package/test/core/ripple.test.js +0 -66
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Build & Development Commands
|
|
6
|
+
- Build: `bun run build`
|
|
7
|
+
- Dev server: `bun run dev`
|
|
8
|
+
- Tests: `bun test`
|
|
9
|
+
- Single test: `bun test test/components/button.test.ts`
|
|
10
|
+
- Watch tests: `bun test --watch`
|
|
11
|
+
- Test with UI: `bun test --watch --ui`
|
|
12
|
+
- Test coverage: `bun test --coverage`
|
|
13
|
+
- Docs: `bun run docs` (uses TypeDoc)
|
|
14
|
+
|
|
15
|
+
## Code Conventions
|
|
16
|
+
- Factory pattern: Use `createComponent` naming for component creators
|
|
17
|
+
- Functional composition with pipe pattern
|
|
18
|
+
- Follow BEM-style CSS naming: `mtrl-component__element--modifier`
|
|
19
|
+
- Use TypeDoc-compatible comments for documentation
|
|
20
|
+
- Use TypeScript with strict typing where possible
|
|
21
|
+
- Error handling: Try/catch blocks for component creation
|
|
22
|
+
- Import order: core modules first, then utility functions, local imports last
|
|
23
|
+
- Components follow a standard structure in their directories (api.ts, config.ts, etc.)
|
|
24
|
+
- Use ES6+ features with full browser compatibility
|
|
25
|
+
|
|
26
|
+
## Test Conventions
|
|
27
|
+
- Tests live in `test/components/` with `.test.ts` extension
|
|
28
|
+
- Use JSDOM for DOM manipulation in tests
|
|
29
|
+
- Create mock implementations to avoid circular dependencies
|
|
30
|
+
- Test structure follows `describe/test` pattern
|
|
31
|
+
- Test component creation, options, events, and state changes
|
|
32
|
+
- Use `import type` to avoid circular dependencies in TypeScript tests
|
|
33
|
+
- Test directory structure mirrors src directory structure
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mtrl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A functional TypeScript/JavaScript component library with composable architecture based on Material Design 3",
|
|
5
5
|
"author": "floor",
|
|
6
6
|
"license": "MIT License",
|
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
"url": "https://github.com/floor/mtrl.git"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
+
"@types/jsdom": "^21.1.7",
|
|
46
|
+
"jsdom": "^26.0.0",
|
|
45
47
|
"sass": "^1.85.1",
|
|
46
48
|
"typedoc": "^0.27.9"
|
|
47
49
|
}
|
|
@@ -16,13 +16,43 @@ import { ButtonConfig } from './types';
|
|
|
16
16
|
import { createBaseConfig, getElementConfig, getApiConfig } from './config';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Creates a new Button component
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* Creates a new Button component with the specified configuration.
|
|
20
|
+
*
|
|
21
|
+
* The Button component is created using a functional composition pattern,
|
|
22
|
+
* applying various features through the pipe function. This approach allows
|
|
23
|
+
* for flexible and modular component construction.
|
|
24
|
+
*
|
|
25
|
+
* @param {ButtonConfig} config - Configuration options for the button
|
|
26
|
+
* This can include text content, icon options, variant styling, disabled state,
|
|
27
|
+
* and other button properties. See {@link ButtonConfig} for available options.
|
|
28
|
+
*
|
|
29
|
+
* @returns {ButtonComponent} A fully configured button component instance with
|
|
30
|
+
* all requested features applied. The returned component has methods for
|
|
31
|
+
* manipulation, event handling, and lifecycle management.
|
|
32
|
+
*
|
|
33
|
+
* @throws {Error} Throws an error if button creation fails for any reason
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // Create a simple text button
|
|
37
|
+
* const textButton = createButton({ text: 'Click me' });
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Create a primary button with an icon
|
|
41
|
+
* const primaryButton = createButton({
|
|
42
|
+
* text: 'Submit',
|
|
43
|
+
* variant: 'primary',
|
|
44
|
+
* icon: 'send'
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // Create a disabled button
|
|
49
|
+
* const disabledButton = createButton({
|
|
50
|
+
* text: 'Not available',
|
|
51
|
+
* disabled: true
|
|
52
|
+
* });
|
|
22
53
|
*/
|
|
23
54
|
const createButton = (config: ButtonConfig = {}) => {
|
|
24
55
|
const baseConfig = createBaseConfig(config);
|
|
25
|
-
|
|
26
56
|
try {
|
|
27
57
|
const button = pipe(
|
|
28
58
|
createBase,
|
|
@@ -36,7 +66,6 @@ const createButton = (config: ButtonConfig = {}) => {
|
|
|
36
66
|
withLifecycle(),
|
|
37
67
|
comp => withAPI(getApiConfig(comp))(comp)
|
|
38
68
|
)(baseConfig);
|
|
39
|
-
|
|
40
69
|
return button;
|
|
41
70
|
} catch (error) {
|
|
42
71
|
console.error('Button creation error:', error);
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// src/components/navigation/system/core.ts
|
|
2
|
+
|
|
3
|
+
import { NavigationSystemState, NavigationSystemConfig, NavigationItem } from './types';
|
|
4
|
+
import { isMobileDevice } from '../../../core/utils/mobile';
|
|
5
|
+
import createNavigation from '../navigation';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Update drawer content for a specific section WITHOUT changing visibility
|
|
9
|
+
*
|
|
10
|
+
* @param state - System state
|
|
11
|
+
* @param sectionId - Section ID to display
|
|
12
|
+
* @param showDrawer - Function to show the drawer
|
|
13
|
+
* @param hideDrawer - Function to hide the drawer
|
|
14
|
+
*/
|
|
15
|
+
export const updateDrawerContent = (
|
|
16
|
+
state: NavigationSystemState,
|
|
17
|
+
sectionId: string,
|
|
18
|
+
showDrawer: () => void,
|
|
19
|
+
hideDrawer: () => void
|
|
20
|
+
): void => {
|
|
21
|
+
if (!state.drawer || !sectionId || !state.items[sectionId]) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Get section items
|
|
26
|
+
const sectionData = state.items[sectionId];
|
|
27
|
+
const items = sectionData.items || [];
|
|
28
|
+
|
|
29
|
+
// If no items, hide drawer and exit
|
|
30
|
+
if (items.length === 0) {
|
|
31
|
+
hideDrawer();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Clear existing drawer items first using the API
|
|
36
|
+
const currentItems = state.drawer.getAllItems();
|
|
37
|
+
if (currentItems?.length > 0) {
|
|
38
|
+
currentItems.forEach((item: any) => {
|
|
39
|
+
state.drawer.removeItem(item.config.id);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Add new items to drawer through the API
|
|
44
|
+
items.forEach((item: NavigationItem) => {
|
|
45
|
+
state.drawer.addItem(item);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Show the drawer
|
|
49
|
+
showDrawer();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Creates the rail navigation component
|
|
54
|
+
*
|
|
55
|
+
* @param state - System state
|
|
56
|
+
* @param config - System configuration
|
|
57
|
+
* @returns Rail navigation component
|
|
58
|
+
*/
|
|
59
|
+
export const createRailNavigation = (
|
|
60
|
+
state: NavigationSystemState,
|
|
61
|
+
config: any
|
|
62
|
+
): any => {
|
|
63
|
+
// Build rail items from sections
|
|
64
|
+
const railItems = Object.keys(state.items || {}).map(sectionId => ({
|
|
65
|
+
id: sectionId,
|
|
66
|
+
label: state.items[sectionId]?.label || sectionId,
|
|
67
|
+
icon: state.items[sectionId]?.icon || '',
|
|
68
|
+
active: sectionId === state.activeSection
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
// Create the rail component
|
|
72
|
+
const rail = createNavigation({
|
|
73
|
+
variant: 'rail',
|
|
74
|
+
position: 'left',
|
|
75
|
+
showLabels: config.showLabelsOnRail,
|
|
76
|
+
items: railItems,
|
|
77
|
+
...config.railOptions
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
document.body.appendChild(rail.element);
|
|
81
|
+
return rail;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Creates the drawer navigation component
|
|
86
|
+
*
|
|
87
|
+
* @param state - System state
|
|
88
|
+
* @param config - System configuration
|
|
89
|
+
* @returns Drawer navigation component
|
|
90
|
+
*/
|
|
91
|
+
export const createDrawerNavigation = (
|
|
92
|
+
state: NavigationSystemState,
|
|
93
|
+
config: any
|
|
94
|
+
): any => {
|
|
95
|
+
// Create the drawer component (initially empty)
|
|
96
|
+
const drawer = createNavigation({
|
|
97
|
+
variant: 'drawer',
|
|
98
|
+
position: 'left',
|
|
99
|
+
items: [], // Start empty
|
|
100
|
+
...config.drawerOptions
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
document.body.appendChild(drawer.element);
|
|
104
|
+
|
|
105
|
+
// Mark drawer with identifier
|
|
106
|
+
drawer.element.dataset.id = 'drawer';
|
|
107
|
+
|
|
108
|
+
// IMPORTANT: Make drawer initially hidden unless explicitly expanded
|
|
109
|
+
if (!config.expanded) {
|
|
110
|
+
drawer.element.classList.add('mtrl-nav--hidden');
|
|
111
|
+
drawer.element.setAttribute('aria-hidden', 'true');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return drawer;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Shows the drawer with mobile-specific behaviors
|
|
119
|
+
*
|
|
120
|
+
* @param state - System state
|
|
121
|
+
* @param mobileConfig - Mobile configuration
|
|
122
|
+
*/
|
|
123
|
+
export const showDrawer = (
|
|
124
|
+
state: NavigationSystemState,
|
|
125
|
+
mobileConfig: any
|
|
126
|
+
): void => {
|
|
127
|
+
if (!state.drawer) return;
|
|
128
|
+
|
|
129
|
+
state.drawer.element.classList.remove('mtrl-nav--hidden');
|
|
130
|
+
state.drawer.element.setAttribute('aria-hidden', 'false');
|
|
131
|
+
|
|
132
|
+
// Apply mobile-specific behaviors
|
|
133
|
+
if (state.isMobile) {
|
|
134
|
+
if (state.overlayElement) {
|
|
135
|
+
state.overlayElement.classList.add('active');
|
|
136
|
+
state.overlayElement.setAttribute('aria-hidden', 'false');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Lock body scroll if enabled
|
|
140
|
+
if (mobileConfig.lockBodyScroll) {
|
|
141
|
+
document.body.classList.add(mobileConfig.bodyLockClass);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Ensure close button is visible
|
|
145
|
+
if (state.closeButtonElement) {
|
|
146
|
+
state.closeButtonElement.style.display = 'flex';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Hides the drawer with mobile-specific behaviors
|
|
153
|
+
*
|
|
154
|
+
* @param state - System state
|
|
155
|
+
* @param mobileConfig - Mobile configuration
|
|
156
|
+
*/
|
|
157
|
+
export const hideDrawer = (
|
|
158
|
+
state: NavigationSystemState,
|
|
159
|
+
mobileConfig: any
|
|
160
|
+
): void => {
|
|
161
|
+
if (!state.drawer) return;
|
|
162
|
+
|
|
163
|
+
state.drawer.element.classList.add('mtrl-nav--hidden');
|
|
164
|
+
state.drawer.element.setAttribute('aria-hidden', 'true');
|
|
165
|
+
|
|
166
|
+
// Remove mobile-specific effects
|
|
167
|
+
if (state.overlayElement) {
|
|
168
|
+
state.overlayElement.classList.remove('active');
|
|
169
|
+
state.overlayElement.setAttribute('aria-hidden', 'true');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Unlock body scroll
|
|
173
|
+
if (mobileConfig.lockBodyScroll) {
|
|
174
|
+
document.body.classList.remove(mobileConfig.bodyLockClass);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Checks if drawer is visible
|
|
180
|
+
*
|
|
181
|
+
* @param state - System state
|
|
182
|
+
* @returns true if drawer is visible
|
|
183
|
+
*/
|
|
184
|
+
export const isDrawerVisible = (
|
|
185
|
+
state: NavigationSystemState
|
|
186
|
+
): boolean => {
|
|
187
|
+
if (!state.drawer) return false;
|
|
188
|
+
return !state.drawer.element.classList.contains('mtrl-nav--hidden');
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Checks and updates the mobile state
|
|
193
|
+
*
|
|
194
|
+
* @param state - System state
|
|
195
|
+
* @param mobileConfig - Mobile configuration
|
|
196
|
+
* @param setupMobileMode - Function to set up mobile mode
|
|
197
|
+
* @param teardownMobileMode - Function to tear down mobile mode
|
|
198
|
+
* @param systemApi - Reference to the public system API
|
|
199
|
+
*/
|
|
200
|
+
export const checkMobileState = (
|
|
201
|
+
state: NavigationSystemState,
|
|
202
|
+
mobileConfig: any,
|
|
203
|
+
setupMobileMode: () => void,
|
|
204
|
+
teardownMobileMode: () => void,
|
|
205
|
+
systemApi: any
|
|
206
|
+
): void => {
|
|
207
|
+
const prevState = state.isMobile;
|
|
208
|
+
state.isMobile = window.innerWidth <= mobileConfig.breakpoint || isMobileDevice();
|
|
209
|
+
|
|
210
|
+
// If state changed, adjust UI
|
|
211
|
+
if (prevState !== state.isMobile) {
|
|
212
|
+
if (state.isMobile) {
|
|
213
|
+
// Switched to mobile mode
|
|
214
|
+
setupMobileMode();
|
|
215
|
+
} else {
|
|
216
|
+
// Switched to desktop mode
|
|
217
|
+
teardownMobileMode();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Emit a view change event
|
|
221
|
+
if (systemApi.onViewChange) {
|
|
222
|
+
systemApi.onViewChange({
|
|
223
|
+
mobile: state.isMobile,
|
|
224
|
+
previousMobile: prevState,
|
|
225
|
+
width: window.innerWidth
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Clean up resources when the system is destroyed
|
|
233
|
+
*
|
|
234
|
+
* @param state - System state
|
|
235
|
+
*/
|
|
236
|
+
export const cleanupResources = (
|
|
237
|
+
state: NavigationSystemState
|
|
238
|
+
): void => {
|
|
239
|
+
// Clean up overlay
|
|
240
|
+
if (state.overlayElement && state.overlayElement.parentNode) {
|
|
241
|
+
state.overlayElement.parentNode.removeChild(state.overlayElement);
|
|
242
|
+
state.overlayElement = null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Destroy components
|
|
246
|
+
if (state.rail) {
|
|
247
|
+
state.rail.destroy();
|
|
248
|
+
state.rail = null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (state.drawer) {
|
|
252
|
+
state.drawer.destroy();
|
|
253
|
+
state.drawer = null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Reset state
|
|
257
|
+
state.activeSection = null;
|
|
258
|
+
state.activeSubsection = null;
|
|
259
|
+
state.mouseInDrawer = false;
|
|
260
|
+
state.mouseInRail = false;
|
|
261
|
+
state.processingChange = false;
|
|
262
|
+
state.isMobile = false;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Navigate to a specific section and subsection
|
|
267
|
+
*
|
|
268
|
+
* @param state - System state
|
|
269
|
+
* @param section - Section ID
|
|
270
|
+
* @param subsection - Subsection ID (optional)
|
|
271
|
+
* @param silent - Whether to suppress change events
|
|
272
|
+
*/
|
|
273
|
+
export const navigateTo = (
|
|
274
|
+
state: NavigationSystemState,
|
|
275
|
+
section: string,
|
|
276
|
+
subsection?: string,
|
|
277
|
+
silent?: boolean
|
|
278
|
+
): void => {
|
|
279
|
+
// Skip if section doesn't exist
|
|
280
|
+
if (!section || !state.items[section]) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Check if we're already on this section and subsection
|
|
285
|
+
if (state.activeSection === section && state.activeSubsection === subsection) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Update active section
|
|
290
|
+
state.activeSection = section;
|
|
291
|
+
|
|
292
|
+
// Update rail if it exists
|
|
293
|
+
if (state.rail) {
|
|
294
|
+
state.rail.setActive(section, silent);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Update active subsection if specified
|
|
298
|
+
if (subsection && state.drawer) {
|
|
299
|
+
state.activeSubsection = subsection;
|
|
300
|
+
state.drawer.setActive(subsection, silent);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// src/components/navigation/system/events.ts
|
|
2
|
+
|
|
3
|
+
import { NavigationSystemState } from './types';
|
|
4
|
+
import { NavigationItem, NavigationSection } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Registers rail navigation event handlers
|
|
8
|
+
*
|
|
9
|
+
* @param state - System state
|
|
10
|
+
* @param config - System configuration
|
|
11
|
+
* @param updateDrawerContent - Function to update drawer content
|
|
12
|
+
* @param showDrawer - Function to show the drawer
|
|
13
|
+
* @param hideDrawer - Function to hide the drawer
|
|
14
|
+
* @param systemApi - Reference to the public system API
|
|
15
|
+
*/
|
|
16
|
+
export const registerRailEvents = (
|
|
17
|
+
state: NavigationSystemState,
|
|
18
|
+
config: any,
|
|
19
|
+
updateDrawerContent: (sectionId: string) => void,
|
|
20
|
+
showDrawer: () => void,
|
|
21
|
+
hideDrawer: () => void,
|
|
22
|
+
systemApi: any
|
|
23
|
+
): void => {
|
|
24
|
+
const rail = state.rail;
|
|
25
|
+
if (!rail) return;
|
|
26
|
+
|
|
27
|
+
// Register for change events - will listen for when rail items are clicked
|
|
28
|
+
rail.on('change', (event: any) => {
|
|
29
|
+
// Extract ID from event data
|
|
30
|
+
const id = event?.id;
|
|
31
|
+
|
|
32
|
+
if (!id || state.processingChange) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check if this is a user action
|
|
37
|
+
const isUserAction = event?.source === 'userAction';
|
|
38
|
+
|
|
39
|
+
// Set processing flag to prevent loops
|
|
40
|
+
state.processingChange = true;
|
|
41
|
+
|
|
42
|
+
// Update active section
|
|
43
|
+
state.activeSection = id;
|
|
44
|
+
|
|
45
|
+
// Handle internally first - update drawer content
|
|
46
|
+
updateDrawerContent(id);
|
|
47
|
+
|
|
48
|
+
// Then notify external handlers
|
|
49
|
+
if (systemApi.onSectionChange && isUserAction) {
|
|
50
|
+
systemApi.onSectionChange(id, { source: isUserAction ? 'userClick' : 'programmatic' });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Clear the processing flag after a delay
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
state.processingChange = false;
|
|
56
|
+
}, 50);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
rail.on('mouseover', (event: any) => {
|
|
60
|
+
const id = event?.id;
|
|
61
|
+
|
|
62
|
+
// Set rail mouse state
|
|
63
|
+
state.mouseInRail = true;
|
|
64
|
+
|
|
65
|
+
// Clear any existing hover timer
|
|
66
|
+
clearTimeout(state.hoverTimer as number);
|
|
67
|
+
state.hoverTimer = null;
|
|
68
|
+
|
|
69
|
+
// Only schedule drawer operations if there's an ID
|
|
70
|
+
if (id) {
|
|
71
|
+
// Check if this section has items
|
|
72
|
+
if (state.items[id]?.items?.length > 0) {
|
|
73
|
+
// Has items - schedule drawer opening
|
|
74
|
+
state.hoverTimer = window.setTimeout(() => {
|
|
75
|
+
updateDrawerContent(id);
|
|
76
|
+
}, config.hoverDelay) as unknown as number;
|
|
77
|
+
} else {
|
|
78
|
+
// No items - hide drawer after a delay to prevent flickering
|
|
79
|
+
state.closeTimer = window.setTimeout(() => {
|
|
80
|
+
// Only hide if we're still in the rail but not in the drawer
|
|
81
|
+
if (state.mouseInRail && !state.mouseInDrawer) {
|
|
82
|
+
hideDrawer();
|
|
83
|
+
}
|
|
84
|
+
}, config.hoverDelay) as unknown as number;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
rail.on('mouseenter', () => {
|
|
90
|
+
state.mouseInRail = true;
|
|
91
|
+
|
|
92
|
+
// Clear any pending drawer close timer when entering rail
|
|
93
|
+
clearTimeout(state.closeTimer as number);
|
|
94
|
+
state.closeTimer = null;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
rail.on('mouseleave', () => {
|
|
98
|
+
state.mouseInRail = false;
|
|
99
|
+
|
|
100
|
+
// Clear any existing hover timer
|
|
101
|
+
clearTimeout(state.hoverTimer as number);
|
|
102
|
+
state.hoverTimer = null;
|
|
103
|
+
|
|
104
|
+
// Only set timer to hide drawer if we're not in drawer either
|
|
105
|
+
if (!state.mouseInDrawer) {
|
|
106
|
+
state.closeTimer = window.setTimeout(() => {
|
|
107
|
+
// Double-check we're still not in rail or drawer before hiding
|
|
108
|
+
if (!state.mouseInRail && !state.mouseInDrawer) {
|
|
109
|
+
hideDrawer();
|
|
110
|
+
}
|
|
111
|
+
}, config.closeDelay) as unknown as number;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Registers drawer navigation event handlers
|
|
118
|
+
*
|
|
119
|
+
* @param state - System state
|
|
120
|
+
* @param config - System configuration
|
|
121
|
+
* @param hideDrawer - Function to hide the drawer
|
|
122
|
+
* @param systemApi - Reference to the public system API
|
|
123
|
+
*/
|
|
124
|
+
export const registerDrawerEvents = (
|
|
125
|
+
state: NavigationSystemState,
|
|
126
|
+
config: any,
|
|
127
|
+
hideDrawer: () => void,
|
|
128
|
+
systemApi: any
|
|
129
|
+
): void => {
|
|
130
|
+
const drawer = state.drawer;
|
|
131
|
+
if (!drawer) return;
|
|
132
|
+
|
|
133
|
+
// Use the component's native event system
|
|
134
|
+
if (typeof drawer.on === 'function') {
|
|
135
|
+
// Handle item selection
|
|
136
|
+
drawer.on('change', (event: any) => {
|
|
137
|
+
const id = event.id;
|
|
138
|
+
|
|
139
|
+
state.activeSubsection = id;
|
|
140
|
+
|
|
141
|
+
// If configuration specifies to hide drawer on click, do so
|
|
142
|
+
if (config.hideDrawerOnClick) {
|
|
143
|
+
hideDrawer();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Emit item selection event
|
|
147
|
+
if (systemApi.onItemSelect) {
|
|
148
|
+
systemApi.onItemSelect(event);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Handle mouseenter/mouseleave for drawer
|
|
153
|
+
drawer.on('mouseenter', () => {
|
|
154
|
+
state.mouseInDrawer = true;
|
|
155
|
+
|
|
156
|
+
// Clear any hover and close timers
|
|
157
|
+
clearTimeout(state.hoverTimer as number);
|
|
158
|
+
clearTimeout(state.closeTimer as number);
|
|
159
|
+
state.hoverTimer = null;
|
|
160
|
+
state.closeTimer = null;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
drawer.on('mouseleave', () => {
|
|
164
|
+
state.mouseInDrawer = false;
|
|
165
|
+
|
|
166
|
+
// Only set timer to hide drawer if we're not in rail
|
|
167
|
+
if (!state.mouseInRail) {
|
|
168
|
+
state.closeTimer = window.setTimeout(() => {
|
|
169
|
+
// Double-check we're still not in drawer or rail before hiding
|
|
170
|
+
if (!state.mouseInDrawer && !state.mouseInRail) {
|
|
171
|
+
hideDrawer();
|
|
172
|
+
}
|
|
173
|
+
}, config.closeDelay) as unknown as number;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Sets up window resize and orientation change handling
|
|
181
|
+
*
|
|
182
|
+
* @param state - System state
|
|
183
|
+
* @param checkMobileState - Function to check and update mobile state
|
|
184
|
+
*/
|
|
185
|
+
export const setupResponsiveHandling = (
|
|
186
|
+
state: NavigationSystemState,
|
|
187
|
+
checkMobileState: () => void
|
|
188
|
+
): void => {
|
|
189
|
+
// Setup responsive behavior
|
|
190
|
+
if (window.ResizeObserver) {
|
|
191
|
+
// Use ResizeObserver for better performance
|
|
192
|
+
state.resizeObserver = new ResizeObserver(() => {
|
|
193
|
+
checkMobileState();
|
|
194
|
+
});
|
|
195
|
+
state.resizeObserver.observe(document.body);
|
|
196
|
+
} else {
|
|
197
|
+
// Fallback to window resize event
|
|
198
|
+
window.addEventListener('resize', checkMobileState);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Listen for orientation changes on mobile
|
|
202
|
+
window.addEventListener('orientationchange', () => {
|
|
203
|
+
// Small delay to ensure dimensions have updated
|
|
204
|
+
setTimeout(checkMobileState, 100);
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Cleans up all event handlers and resources
|
|
210
|
+
*
|
|
211
|
+
* @param state - System state
|
|
212
|
+
* @param checkMobileState - Function reference to remove event handlers
|
|
213
|
+
*/
|
|
214
|
+
export const cleanupEvents = (
|
|
215
|
+
state: NavigationSystemState,
|
|
216
|
+
checkMobileState: () => void
|
|
217
|
+
): void => {
|
|
218
|
+
// Clean up resize observer
|
|
219
|
+
if (state.resizeObserver) {
|
|
220
|
+
state.resizeObserver.disconnect();
|
|
221
|
+
state.resizeObserver = null;
|
|
222
|
+
} else {
|
|
223
|
+
window.removeEventListener('resize', checkMobileState);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Remove orientation change listener
|
|
227
|
+
window.removeEventListener('orientationchange', checkMobileState);
|
|
228
|
+
|
|
229
|
+
// Remove outside click handler
|
|
230
|
+
if (state.outsideClickHandler) {
|
|
231
|
+
const eventType = ('ontouchend' in window) ? 'touchend' : 'click';
|
|
232
|
+
document.removeEventListener(eventType, state.outsideClickHandler);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Clear timers
|
|
236
|
+
clearTimeout(state.hoverTimer as number);
|
|
237
|
+
clearTimeout(state.closeTimer as number);
|
|
238
|
+
state.hoverTimer = null;
|
|
239
|
+
state.closeTimer = null;
|
|
240
|
+
};
|