mtrl 0.2.8 → 0.3.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.
- package/index.ts +4 -0
- package/package.json +1 -1
- package/src/components/button/button.ts +34 -5
- package/src/components/navigation/api.ts +131 -96
- package/src/components/navigation/features/controller.ts +273 -0
- package/src/components/navigation/features/items.ts +133 -64
- package/src/components/navigation/navigation.ts +17 -2
- 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/slider/config.ts +20 -2
- package/src/components/slider/features/controller.ts +737 -0
- package/src/components/slider/features/handlers.ts +18 -16
- package/src/components/slider/features/index.ts +3 -2
- package/src/components/slider/features/range.ts +104 -0
- package/src/components/slider/schema.ts +141 -0
- package/src/components/slider/slider.ts +34 -13
- 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 +1 -0
- package/src/components/switch/switch.ts +3 -3
- package/src/components/switch/types.ts +14 -2
- package/src/components/textfield/api.ts +53 -0
- package/src/components/textfield/features.ts +322 -0
- package/src/components/textfield/textfield.ts +8 -0
- package/src/components/textfield/types.ts +12 -3
- package/src/components/timepicker/clockdial.ts +1 -4
- package/src/core/compose/features/textinput.ts +15 -2
- package/src/core/composition/features/dom.ts +45 -0
- package/src/core/composition/features/icon.ts +131 -0
- package/src/core/composition/features/index.ts +12 -0
- package/src/core/composition/features/label.ts +155 -0
- package/src/core/composition/features/layout.ts +47 -0
- package/src/core/composition/index.ts +26 -0
- package/src/core/index.ts +1 -1
- 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/index.ts +1 -0
- package/src/styles/abstract/_variables.scss +28 -0
- package/src/styles/components/_navigation-mobile.scss +244 -0
- package/src/styles/components/_navigation-system.scss +151 -0
- package/src/styles/components/_switch.scss +133 -69
- package/src/styles/components/_textfield.scss +259 -27
- package/demo/build.ts +0 -349
- package/demo/index.html +0 -110
- package/demo/main.js +0 -448
- package/demo/styles.css +0 -239
- package/server.ts +0 -86
- package/src/components/slider/features/slider.ts +0 -318
- package/src/components/slider/features/structure.ts +0 -181
- package/src/components/slider/features/ui.ts +0 -388
- package/src/components/textfield/constants.ts +0 -100
- package/src/core/layout/index.js +0 -95
package/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// index.js
|
|
2
2
|
import {
|
|
3
3
|
createLayout,
|
|
4
|
+
createStructure,
|
|
4
5
|
createElement,
|
|
5
6
|
createBadge,
|
|
6
7
|
createBottomAppBar,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
createList,
|
|
19
20
|
createMenu,
|
|
20
21
|
createNavigation,
|
|
22
|
+
createNavigationSystem,
|
|
21
23
|
createProgress,
|
|
22
24
|
createRadios,
|
|
23
25
|
createSearch,
|
|
@@ -34,6 +36,7 @@ import {
|
|
|
34
36
|
|
|
35
37
|
export {
|
|
36
38
|
createLayout,
|
|
39
|
+
createStructure,
|
|
37
40
|
createElement,
|
|
38
41
|
createBadge,
|
|
39
42
|
createBottomAppBar,
|
|
@@ -51,6 +54,7 @@ export {
|
|
|
51
54
|
createList,
|
|
52
55
|
createMenu,
|
|
53
56
|
createNavigation,
|
|
57
|
+
createNavigationSystem,
|
|
54
58
|
createProgress,
|
|
55
59
|
createRadios,
|
|
56
60
|
createSearch,
|
package/package.json
CHANGED
|
@@ -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);
|
|
@@ -1,107 +1,142 @@
|
|
|
1
|
-
// src/components/
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
interface ApiOptions {
|
|
5
|
-
disabled: {
|
|
6
|
-
enable: () => void;
|
|
7
|
-
disable: () => void;
|
|
8
|
-
};
|
|
9
|
-
lifecycle: {
|
|
10
|
-
destroy: () => void;
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface ComponentWithElements {
|
|
15
|
-
element: HTMLElement;
|
|
16
|
-
text: {
|
|
17
|
-
setText: (content: string) => any;
|
|
18
|
-
getText: () => string;
|
|
19
|
-
getElement: () => HTMLElement | null;
|
|
20
|
-
};
|
|
21
|
-
icon: {
|
|
22
|
-
setIcon: (html: string) => any;
|
|
23
|
-
getIcon: () => string;
|
|
24
|
-
getElement: () => HTMLElement | null;
|
|
25
|
-
};
|
|
26
|
-
getClass: (name: string) => string;
|
|
27
|
-
}
|
|
1
|
+
// src/components/navigation/api.ts
|
|
2
|
+
import { NavigationComponent, NavItemConfig, NavItemData, BaseComponent, ApiOptions } from './types';
|
|
28
3
|
|
|
29
4
|
/**
|
|
30
|
-
* Enhances a
|
|
5
|
+
* Enhances a component with navigation-specific API methods
|
|
31
6
|
* @param {ApiOptions} options - API configuration options
|
|
32
7
|
* @returns {Function} Higher-order function that adds API methods to component
|
|
33
|
-
* @internal This is an internal utility for the Button component
|
|
34
8
|
*/
|
|
35
|
-
export const withAPI = (
|
|
36
|
-
(component:
|
|
37
|
-
...component as any,
|
|
38
|
-
element: component.element as HTMLButtonElement,
|
|
9
|
+
export const withAPI = (options: ApiOptions) =>
|
|
10
|
+
(component: BaseComponent): NavigationComponent => {
|
|
39
11
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
component.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
12
|
+
const navComponent = {
|
|
13
|
+
...component,
|
|
14
|
+
element: component.element,
|
|
15
|
+
items: component.items || new Map(),
|
|
16
|
+
|
|
17
|
+
// Basic item operations
|
|
18
|
+
addItem(config: NavItemConfig): NavigationComponent {
|
|
19
|
+
if (typeof component.addItem === 'function') {
|
|
20
|
+
component.addItem(config);
|
|
21
|
+
}
|
|
22
|
+
return this;
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
removeItem(id: string): NavigationComponent {
|
|
26
|
+
if (typeof component.removeItem === 'function') {
|
|
27
|
+
component.removeItem(id);
|
|
28
|
+
}
|
|
29
|
+
return this;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
getItem(id: string): NavItemData | undefined {
|
|
33
|
+
if (typeof component.getItem === 'function') {
|
|
34
|
+
return component.getItem(id);
|
|
35
|
+
}
|
|
36
|
+
return this.items.get(id);
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
getAllItems(): NavItemData[] {
|
|
40
|
+
if (typeof component.getAllItems === 'function') {
|
|
41
|
+
return component.getAllItems();
|
|
42
|
+
}
|
|
43
|
+
return Array.from(this.items.values());
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Path and active item management
|
|
47
|
+
getActive(): NavItemData | null {
|
|
48
|
+
if (typeof component.getActive === 'function') {
|
|
49
|
+
return component.getActive();
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
getItemPath(id: string): string[] {
|
|
55
|
+
if (typeof component.getItemPath === 'function') {
|
|
56
|
+
return component.getItemPath(id);
|
|
57
|
+
}
|
|
58
|
+
return [];
|
|
59
|
+
},
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
setActive(id: string, silent): NavigationComponent {
|
|
62
|
+
// Use the controller if available for consistent handling
|
|
63
|
+
if (typeof component.handleItemClick === 'function') {
|
|
64
|
+
component.handleItemClick(id);
|
|
65
|
+
} else if (typeof component.setActive === 'function') {
|
|
66
|
+
component.setActive(id);
|
|
67
|
+
} else {
|
|
68
|
+
// Fallback if setActive is not available
|
|
69
|
+
const item = this.items.get(id);
|
|
70
|
+
if (item && item.element) {
|
|
71
|
+
// Emit a change event to propagate the state change
|
|
72
|
+
if (component.emit) {
|
|
73
|
+
component.emit('change', {
|
|
74
|
+
id,
|
|
75
|
+
item,
|
|
76
|
+
source: 'api'
|
|
77
|
+
});
|
|
78
|
+
}
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
|
-
|
|
81
|
+
return this;
|
|
82
|
+
},
|
|
72
83
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
84
|
+
// Navigation state management
|
|
85
|
+
enable(): NavigationComponent {
|
|
86
|
+
if (options.disabled.enable) {
|
|
87
|
+
options.disabled.enable();
|
|
88
|
+
}
|
|
89
|
+
return this;
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
disable(): NavigationComponent {
|
|
93
|
+
if (options.disabled.disable) {
|
|
94
|
+
options.disabled.disable();
|
|
95
|
+
}
|
|
96
|
+
return this;
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
expand(): NavigationComponent {
|
|
100
|
+
this.element.classList.remove(`${this.element.className.split(' ')[0]}--hidden`);
|
|
101
|
+
this.element.setAttribute('aria-hidden', 'false');
|
|
102
|
+
|
|
103
|
+
if (component.emit) {
|
|
104
|
+
component.emit('expanded', { source: 'api' });
|
|
105
|
+
}
|
|
106
|
+
return this;
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
collapse(): NavigationComponent {
|
|
110
|
+
this.element.classList.add(`${this.element.className.split(' ')[0]}--hidden`);
|
|
111
|
+
this.element.setAttribute('aria-hidden', 'true');
|
|
112
|
+
|
|
113
|
+
if (component.emit) {
|
|
114
|
+
component.emit('collapsed', { source: 'api' });
|
|
115
|
+
}
|
|
116
|
+
return this;
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
isExpanded(): boolean {
|
|
120
|
+
return !this.element.classList.contains(`${this.element.className.split(' ')[0]}--hidden`);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
toggle(): NavigationComponent {
|
|
124
|
+
return this.isExpanded() ? this.collapse() : this.expand();
|
|
125
|
+
},
|
|
126
|
+
// on: component.on,
|
|
127
|
+
// off: component.off,
|
|
128
|
+
// emit: component.emit,
|
|
129
|
+
|
|
130
|
+
// Destruction
|
|
131
|
+
destroy(): void {
|
|
132
|
+
if (options.lifecycle.destroy) {
|
|
133
|
+
options.lifecycle.destroy();
|
|
134
|
+
}
|
|
105
135
|
}
|
|
106
|
-
}
|
|
107
|
-
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Return the enhanced component
|
|
139
|
+
return navComponent;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export default withAPI;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// src/components/navigation/features/controller.ts
|
|
2
|
+
import { BaseComponent, NavClass, NavItemData } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration interface for controller feature
|
|
6
|
+
*/
|
|
7
|
+
interface ControllerConfig {
|
|
8
|
+
/** Component prefix for class names */
|
|
9
|
+
prefix?: string;
|
|
10
|
+
|
|
11
|
+
/** Debug mode flag */
|
|
12
|
+
debug?: boolean;
|
|
13
|
+
|
|
14
|
+
/** Component name */
|
|
15
|
+
componentName?: string;
|
|
16
|
+
|
|
17
|
+
/** Additional configuration options */
|
|
18
|
+
[key: string]: any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Enhanced component with controller capabilities
|
|
23
|
+
*/
|
|
24
|
+
interface ControllerComponent extends BaseComponent {
|
|
25
|
+
/** Handler method for item click events */
|
|
26
|
+
handleItemClick: (id: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Adds event delegation controller to a navigation component
|
|
31
|
+
* This centralizes event handling for all navigation items
|
|
32
|
+
*
|
|
33
|
+
* @param {ControllerConfig} config - Controller configuration
|
|
34
|
+
* @returns {Function} Component enhancer function
|
|
35
|
+
*/
|
|
36
|
+
export const withController = (config: ControllerConfig) => (component: BaseComponent): ControllerComponent => {
|
|
37
|
+
const prefix = config.prefix || 'mtrl';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Updates the active state for an item
|
|
41
|
+
* @param {HTMLElement} item - Item element to activate
|
|
42
|
+
* @param {boolean} active - Whether to make active or inactive
|
|
43
|
+
*/
|
|
44
|
+
const updateItemState = (item: HTMLElement, active: boolean): void => {
|
|
45
|
+
// Safety check - ensure item exists
|
|
46
|
+
if (!item) return;
|
|
47
|
+
|
|
48
|
+
// Determine the correct active attribute based on role
|
|
49
|
+
const role = item.getAttribute('role');
|
|
50
|
+
|
|
51
|
+
if (active) {
|
|
52
|
+
item.classList.add(`${prefix}-${NavClass.ITEM}--active`);
|
|
53
|
+
|
|
54
|
+
// Set appropriate attribute based on role
|
|
55
|
+
if (role === 'tab') {
|
|
56
|
+
item.setAttribute('aria-selected', 'true');
|
|
57
|
+
item.setAttribute('tabindex', '0');
|
|
58
|
+
} else if (!item.getAttribute('aria-haspopup')) {
|
|
59
|
+
// Use aria-current for navigation items that aren't expandable
|
|
60
|
+
item.setAttribute('aria-current', 'page');
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
item.classList.remove(`${prefix}-${NavClass.ITEM}--active`);
|
|
64
|
+
|
|
65
|
+
// Remove appropriate attribute based on role
|
|
66
|
+
if (role === 'tab') {
|
|
67
|
+
item.setAttribute('aria-selected', 'false');
|
|
68
|
+
item.setAttribute('tabindex', '-1');
|
|
69
|
+
} else if (item.hasAttribute('aria-current')) {
|
|
70
|
+
item.removeAttribute('aria-current');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Handle expandable item toggle
|
|
77
|
+
* @param {HTMLElement} item - Expandable item element
|
|
78
|
+
* @returns {boolean} Whether the item was expandable and handled
|
|
79
|
+
*/
|
|
80
|
+
const handleExpandableItem = (item: HTMLElement): boolean => {
|
|
81
|
+
const isExpandable = item.getAttribute('aria-expanded') !== null;
|
|
82
|
+
if (!isExpandable) return false;
|
|
83
|
+
|
|
84
|
+
// Toggle expanded state
|
|
85
|
+
const isExpanded = item.getAttribute('aria-expanded') === 'true';
|
|
86
|
+
item.setAttribute('aria-expanded', (!isExpanded).toString());
|
|
87
|
+
|
|
88
|
+
// Find and toggle nested container - use flexible selectors
|
|
89
|
+
const container = item.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}, .mtrl-nav-item-container`);
|
|
90
|
+
if (container) {
|
|
91
|
+
const nestedContainer = container.querySelector(
|
|
92
|
+
`.${prefix}-${NavClass.NESTED_CONTAINER}, .mtrl-nav-nested-container`
|
|
93
|
+
);
|
|
94
|
+
if (nestedContainer) {
|
|
95
|
+
nestedContainer.hidden = isExpanded;
|
|
96
|
+
|
|
97
|
+
// Also toggle expand icon rotation if present
|
|
98
|
+
const expandIcon = item.querySelector(
|
|
99
|
+
`.${prefix}-${NavClass.EXPAND_ICON}, .mtrl-nav-expand-icon`
|
|
100
|
+
);
|
|
101
|
+
if (expandIcon && expandIcon.style) {
|
|
102
|
+
expandIcon.style.transform = isExpanded ? '' : 'rotate(90deg)';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// For expandable items, we still emit a change event
|
|
108
|
+
const id = item.dataset.id;
|
|
109
|
+
if (id && component.emit) {
|
|
110
|
+
component.emit('expandToggle', {
|
|
111
|
+
id,
|
|
112
|
+
expanded: !isExpanded
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return true;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Create the enhanced component with handleItemClick method
|
|
120
|
+
const enhancedComponent: ControllerComponent = {
|
|
121
|
+
...component,
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Handler method for item click events
|
|
125
|
+
* @param {string} id - ID of the clicked item
|
|
126
|
+
*/
|
|
127
|
+
handleItemClick(id: string) {
|
|
128
|
+
if (!component.items) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const itemData = component.items.get(id);
|
|
133
|
+
if (!itemData) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Find the currently active item by DOM query instead of relying on getActive
|
|
138
|
+
const activeElement = component.element.querySelector(`.${prefix}-${NavClass.ITEM}--active, .mtrl-nav-item--active`);
|
|
139
|
+
|
|
140
|
+
// Check if this item is already active - prevent infinite loops
|
|
141
|
+
// if (activeElement && activeElement === itemData.element) {
|
|
142
|
+
// return;
|
|
143
|
+
// }
|
|
144
|
+
|
|
145
|
+
// Deactivate previous active item if found
|
|
146
|
+
if (activeElement) {
|
|
147
|
+
updateItemState(activeElement as HTMLElement, false);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Make sure itemData.element exists before updating
|
|
151
|
+
if (itemData.element) {
|
|
152
|
+
updateItemState(itemData.element, true);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Emit change event
|
|
156
|
+
if (component.emit) {
|
|
157
|
+
// Get the path to the item (for nested items)
|
|
158
|
+
const path = component.getItemPath ? component.getItemPath(id) : [];
|
|
159
|
+
|
|
160
|
+
component.emit('change', {
|
|
161
|
+
id,
|
|
162
|
+
item: itemData,
|
|
163
|
+
previousItem: activeElement ? {
|
|
164
|
+
element: activeElement as HTMLElement,
|
|
165
|
+
config: { id: activeElement.dataset.id }
|
|
166
|
+
} : null,
|
|
167
|
+
path,
|
|
168
|
+
source: 'userAction'
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Set up click event delegation for all navigation items
|
|
175
|
+
component.element.addEventListener('click', (event: Event) => {
|
|
176
|
+
const target = event.target as HTMLElement;
|
|
177
|
+
|
|
178
|
+
// Use more flexible selectors that match actual DOM structure
|
|
179
|
+
const item = target.closest(`.${prefix}-${NavClass.ITEM}, .mtrl-nav-item`) as HTMLElement;
|
|
180
|
+
|
|
181
|
+
if (!item) {
|
|
182
|
+
// Fallback to elements with data-id attribute
|
|
183
|
+
const itemByDataId = target.closest('[data-id]') as HTMLElement;
|
|
184
|
+
if (!itemByDataId) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Use the found item or fallback
|
|
190
|
+
const navItem = item || target.closest('[data-id]') as HTMLElement;
|
|
191
|
+
|
|
192
|
+
if (navItem.hasAttribute('disabled') || navItem.getAttribute('aria-disabled') === 'true') {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Get the ID from the data attribute
|
|
197
|
+
const id = navItem.dataset.id;
|
|
198
|
+
if (!id) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Handle expandable items first
|
|
203
|
+
if (handleExpandableItem(navItem)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Let the enhanced component handle normal item activation
|
|
208
|
+
enhancedComponent.handleItemClick(id);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Add keyboard support for navigation
|
|
212
|
+
component.element.addEventListener('keydown', (event: KeyboardEvent) => {
|
|
213
|
+
// Only handle specific keys
|
|
214
|
+
if (!['Enter', ' ', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const isVertical = ['rail', 'drawer'].includes(component.variant || '');
|
|
219
|
+
const isHorizontal = ['bar'].includes(component.variant || '');
|
|
220
|
+
|
|
221
|
+
// Handle Enter/Space for activation
|
|
222
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
223
|
+
const item = document.activeElement as HTMLElement;
|
|
224
|
+
if (item && item.classList.contains(`${prefix}-${NavClass.ITEM}`)) {
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
|
|
227
|
+
const id = item.dataset.id;
|
|
228
|
+
if (id) {
|
|
229
|
+
item.click(); // Trigger a click event for the item
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Get all focusable navigation items - use flexible selector
|
|
236
|
+
const items = Array.from(
|
|
237
|
+
component.element.querySelectorAll(
|
|
238
|
+
`.${prefix}-${NavClass.ITEM}:not([disabled]):not([aria-disabled="true"]),
|
|
239
|
+
.mtrl-nav-item:not([disabled]):not([aria-disabled="true"])`
|
|
240
|
+
)
|
|
241
|
+
) as HTMLElement[];
|
|
242
|
+
|
|
243
|
+
if (items.length === 0) return;
|
|
244
|
+
|
|
245
|
+
// Find the currently focused item
|
|
246
|
+
const focusedItem = document.activeElement as HTMLElement;
|
|
247
|
+
const focusedIndex = items.indexOf(focusedItem);
|
|
248
|
+
|
|
249
|
+
// Handle navigation keys
|
|
250
|
+
let newIndex = -1;
|
|
251
|
+
|
|
252
|
+
if ((isVertical && (event.key === 'ArrowDown' || event.key === 'ArrowRight')) ||
|
|
253
|
+
(isHorizontal && event.key === 'ArrowRight')) {
|
|
254
|
+
newIndex = focusedIndex < 0 ? 0 : (focusedIndex + 1) % items.length;
|
|
255
|
+
} else if ((isVertical && (event.key === 'ArrowUp' || event.key === 'ArrowLeft')) ||
|
|
256
|
+
(isHorizontal && event.key === 'ArrowLeft')) {
|
|
257
|
+
newIndex = focusedIndex < 0 ? items.length - 1 : (focusedIndex - 1 + items.length) % items.length;
|
|
258
|
+
} else if (event.key === 'Home') {
|
|
259
|
+
newIndex = 0;
|
|
260
|
+
} else if (event.key === 'End') {
|
|
261
|
+
newIndex = items.length - 1;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (newIndex >= 0) {
|
|
265
|
+
event.preventDefault();
|
|
266
|
+
items[newIndex].focus();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return enhancedComponent;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export default withController;
|