mtrl 0.2.7 → 0.2.9
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 +2 -0
- package/package.json +14 -3
- package/src/components/badge/api.ts +23 -14
- package/src/components/badge/badge.ts +2 -2
- package/src/components/badge/config.ts +10 -11
- package/src/components/badge/features.ts +15 -10
- package/src/components/badge/index.ts +27 -2
- package/src/components/badge/types.ts +28 -8
- package/src/components/bottom-app-bar/bottom-app-bar.ts +2 -44
- package/src/components/bottom-app-bar/config.ts +1 -45
- package/src/components/bottom-app-bar/index.ts +7 -1
- package/src/components/bottom-app-bar/types.ts +7 -1
- package/src/components/button/button.ts +0 -1
- package/src/components/button/config.ts +1 -2
- package/src/components/button/index.ts +10 -2
- package/src/components/button/types.ts +14 -2
- package/src/components/card/config.ts +17 -9
- package/src/components/card/content.ts +8 -10
- package/src/components/card/features.ts +4 -6
- package/src/components/card/index.ts +29 -2
- package/src/components/card/types.ts +6 -23
- package/src/components/checkbox/config.ts +3 -4
- package/src/components/checkbox/index.ts +1 -2
- package/src/components/checkbox/types.ts +12 -3
- package/src/components/chip/api.ts +170 -221
- package/src/components/chip/chip.ts +34 -302
- package/src/components/chip/config.ts +1 -2
- package/src/components/chip/index.ts +10 -2
- package/src/components/chip/types.ts +224 -35
- package/src/components/datepicker/api.ts +18 -25
- package/src/components/datepicker/config.ts +9 -12
- package/src/components/datepicker/datepicker.ts +7 -12
- package/src/components/datepicker/index.ts +10 -7
- package/src/components/datepicker/render.ts +16 -18
- package/src/components/datepicker/types.ts +164 -35
- package/src/components/datepicker/utils.ts +1 -2
- package/src/components/dialog/api.ts +7 -8
- package/src/components/dialog/config.ts +3 -4
- package/src/components/dialog/features.ts +56 -22
- package/src/components/dialog/index.ts +38 -8
- package/src/components/dialog/types.ts +33 -10
- package/src/components/divider/index.ts +5 -1
- package/src/components/extended-fab/config.ts +6 -2
- package/src/components/extended-fab/index.ts +7 -2
- package/src/components/extended-fab/types.ts +21 -4
- package/src/components/fab/config.ts +3 -4
- package/src/components/fab/fab.ts +1 -1
- package/src/components/fab/index.ts +7 -2
- package/src/components/fab/types.ts +21 -4
- package/src/components/list/config.ts +4 -5
- package/src/components/list/features.ts +6 -7
- package/src/components/list/index.ts +7 -9
- package/src/components/list/list-item.ts +12 -13
- package/src/components/list/types.ts +50 -5
- package/src/components/list/utils.ts +30 -3
- package/src/components/menu/features/items-manager.ts +9 -9
- package/src/components/menu/features/positioning.ts +7 -7
- package/src/components/menu/features/visibility.ts +7 -7
- package/src/components/menu/index.ts +7 -9
- package/src/components/menu/menu-item.ts +6 -6
- package/src/components/menu/menu.ts +22 -0
- package/src/components/menu/types.ts +29 -10
- package/src/components/menu/utils.ts +67 -0
- package/src/components/navigation/api.ts +131 -96
- package/src/components/navigation/config.ts +22 -10
- package/src/components/navigation/features/controller.ts +273 -0
- package/src/components/navigation/features/items.ts +160 -87
- package/src/components/navigation/index.ts +0 -6
- package/src/components/navigation/nav-item.ts +12 -24
- package/src/components/navigation/navigation.ts +21 -8
- package/src/components/navigation/system-types.ts +124 -0
- package/src/components/navigation/system.ts +776 -0
- package/src/components/navigation/types.ts +228 -203
- package/src/components/progress/api.ts +2 -3
- package/src/components/progress/config.ts +2 -3
- package/src/components/progress/index.ts +0 -1
- package/src/components/progress/progress.ts +1 -2
- package/src/components/progress/types.ts +186 -33
- package/src/components/radios/config.ts +1 -1
- package/src/components/radios/index.ts +0 -1
- package/src/components/radios/types.ts +0 -7
- package/src/components/search/config.ts +1 -2
- package/src/components/search/features/search.ts +14 -15
- package/src/components/search/features/states.ts +5 -1
- package/src/components/search/features/structure.ts +3 -4
- package/src/components/search/index.ts +0 -3
- package/src/components/search/types.ts +18 -6
- package/src/components/segmented-button/config.ts +20 -7
- package/src/components/segmented-button/segment.ts +6 -7
- package/src/components/segmented-button/segmented-button.ts +4 -5
- package/src/components/segmented-button/types.ts +37 -2
- package/src/components/slider/config.ts +20 -2
- package/src/components/slider/features/controller.ts +761 -0
- package/src/components/slider/features/handlers.ts +18 -15
- package/src/components/slider/features/index.ts +3 -2
- package/src/components/slider/features/range.ts +104 -0
- package/src/components/slider/slider.ts +34 -14
- package/src/components/slider/structure.ts +152 -0
- package/src/components/slider/types.ts +34 -8
- package/src/components/snackbar/config.ts +2 -3
- package/src/components/snackbar/constants.ts +0 -32
- package/src/components/snackbar/index.ts +0 -1
- package/src/components/snackbar/position.ts +9 -1
- package/src/components/snackbar/types.ts +122 -46
- package/src/components/switch/config.ts +2 -3
- package/src/components/switch/index.ts +0 -1
- package/src/components/switch/types.ts +3 -2
- package/src/components/tabs/config.ts +3 -4
- package/src/components/tabs/index.ts +0 -15
- package/src/components/tabs/tab-api.ts +12 -4
- package/src/components/tabs/tab.ts +18 -6
- package/src/components/tabs/types.ts +13 -3
- package/src/components/textfield/api.ts +53 -0
- package/src/components/textfield/config.ts +2 -3
- package/src/components/textfield/features.ts +322 -0
- package/src/components/textfield/index.ts +0 -1
- package/src/components/textfield/textfield.ts +8 -0
- package/src/components/textfield/types.ts +29 -6
- package/src/components/timepicker/api.ts +1 -1
- package/src/components/timepicker/clockdial.ts +2 -5
- package/src/components/timepicker/config.ts +102 -4
- package/src/components/timepicker/index.ts +1 -6
- package/src/components/timepicker/render.ts +1 -1
- package/src/components/timepicker/timepicker.ts +1 -1
- package/src/components/tooltip/api.ts +1 -1
- package/src/components/tooltip/config.ts +27 -6
- package/src/components/tooltip/index.ts +0 -1
- package/src/components/tooltip/types.ts +13 -3
- package/src/core/compose/features/textinput.ts +15 -2
- package/src/core/compose/features/textlabel.ts +0 -3
- package/src/core/composition/features/dom.ts +33 -0
- package/src/core/composition/features/icon.ts +131 -0
- package/src/core/composition/features/index.ts +11 -0
- package/src/core/composition/features/label.ts +156 -0
- package/src/core/composition/features/structure.ts +22 -0
- package/src/core/composition/index.ts +26 -0
- package/src/core/index.ts +1 -1
- package/src/core/structure.ts +288 -0
- package/src/index.ts +1 -0
- package/src/styles/components/_navigation-mobile.scss +244 -0
- package/src/styles/components/_navigation-system.scss +151 -0
- package/src/{components/tabs/_styles.scss → styles/components/_tabs.scss} +1 -1
- package/src/{components/textfield/_styles.scss → styles/components/_textfield.scss} +314 -72
- package/src/styles/main.scss +98 -49
- package/src/components/badge/constants.ts +0 -40
- package/src/components/button/constants.ts +0 -11
- package/src/components/card/constants.ts +0 -84
- package/src/components/datepicker/constants.ts +0 -98
- package/src/components/dialog/constants.ts +0 -32
- package/src/components/extended-fab/constants.ts +0 -36
- package/src/components/fab/constants.ts +0 -41
- package/src/components/menu/constants.ts +0 -154
- package/src/components/navigation/constants.ts +0 -200
- package/src/components/progress/constants.ts +0 -29
- package/src/components/search/constants.ts +0 -21
- package/src/components/segmented-button/constants.ts +0 -42
- 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/switch/constants.ts +0 -80
- package/src/components/tabs/constants.ts +0 -89
- package/src/components/textfield/constants.ts +0 -100
- package/src/components/timepicker/constants.ts +0 -138
- /package/src/{components/badge/_styles.scss → styles/components/_badge.scss} +0 -0
- /package/src/{components/bottom-app-bar/_styles.scss → styles/components/_bottom-app-bar.scss} +0 -0
- /package/src/{components/button/_styles.scss → styles/components/_button.scss} +0 -0
- /package/src/{components/card/_styles.scss → styles/components/_card.scss} +0 -0
- /package/src/{components/carousel/_styles.scss → styles/components/_carousel.scss} +0 -0
- /package/src/{components/checkbox/_styles.scss → styles/components/_checkbox.scss} +0 -0
- /package/src/{components/chip/_styles.scss → styles/components/_chip.scss} +0 -0
- /package/src/{components/datepicker/_styles.scss → styles/components/_datepicker.scss} +0 -0
- /package/src/{components/dialog/_styles.scss → styles/components/_dialog.scss} +0 -0
- /package/src/{components/divider/_styles.scss → styles/components/_divider.scss} +0 -0
- /package/src/{components/extended-fab/_styles.scss → styles/components/_extended-fab.scss} +0 -0
- /package/src/{components/fab/_styles.scss → styles/components/_fab.scss} +0 -0
- /package/src/{components/list/_styles.scss → styles/components/_list.scss} +0 -0
- /package/src/{components/menu/_styles.scss → styles/components/_menu.scss} +0 -0
- /package/src/{components/navigation/_styles.scss → styles/components/_navigation.scss} +0 -0
- /package/src/{components/progress/_styles.scss → styles/components/_progress.scss} +0 -0
- /package/src/{components/radios/_styles.scss → styles/components/_radios.scss} +0 -0
- /package/src/{components/search/_styles.scss → styles/components/_search.scss} +0 -0
- /package/src/{components/segmented-button/_styles.scss → styles/components/_segmented-button.scss} +0 -0
- /package/src/{components/sheet/_styles.scss → styles/components/_sheet.scss} +0 -0
- /package/src/{components/slider/_styles.scss → styles/components/_slider.scss} +0 -0
- /package/src/{components/snackbar/_styles.scss → styles/components/_snackbar.scss} +0 -0
- /package/src/{components/switch/_styles.scss → styles/components/_switch.scss} +0 -0
- /package/src/{components/timepicker/_styles.scss → styles/components/_timepicker.scss} +0 -0
- /package/src/{components/tooltip/_styles.scss → styles/components/_tooltip.scss} +0 -0
- /package/src/{components/top-app-bar/_styles.scss → styles/components/_top-app-bar.scss} +0 -0
- /package/src/styles/utilities/{_color.scss → _colors.scss} +0 -0
|
@@ -5,15 +5,14 @@ import {
|
|
|
5
5
|
BaseComponentConfig
|
|
6
6
|
} from '../../core/config/component-config';
|
|
7
7
|
import { NavigationConfig, BaseComponent, ApiOptions } from './types';
|
|
8
|
-
import { NAV_VARIANTS, NAV_POSITIONS, NAV_BEHAVIORS } from './constants';
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* Default configuration for the Navigation component
|
|
12
11
|
*/
|
|
13
12
|
export const defaultConfig: NavigationConfig = {
|
|
14
|
-
variant:
|
|
15
|
-
position:
|
|
16
|
-
behavior:
|
|
13
|
+
variant: 'rail',
|
|
14
|
+
position: 'left',
|
|
15
|
+
behavior: 'fixed',
|
|
17
16
|
items: [],
|
|
18
17
|
showLabels: true,
|
|
19
18
|
scrimEnabled: false
|
|
@@ -32,16 +31,29 @@ export const createBaseConfig = (config: NavigationConfig = {}): NavigationConfi
|
|
|
32
31
|
* @param {NavigationConfig} config - Navigation configuration
|
|
33
32
|
* @returns {Object} Element configuration object for withElement
|
|
34
33
|
*/
|
|
35
|
-
export const getElementConfig = (config: NavigationConfig) =>
|
|
36
|
-
|
|
34
|
+
export const getElementConfig = (config: NavigationConfig) => {
|
|
35
|
+
// Build class list - start with the variant class
|
|
36
|
+
const variantClass = config.variant ? `${config.prefix}-nav--${config.variant}` : '';
|
|
37
|
+
|
|
38
|
+
// Add position class if specified
|
|
39
|
+
const positionClass = config.position ? `${config.prefix}-nav--${config.position}` : '';
|
|
40
|
+
|
|
41
|
+
// Add user-provided classes
|
|
42
|
+
const userClass = config.class || '';
|
|
43
|
+
|
|
44
|
+
// Combine all classes
|
|
45
|
+
const classNames = [variantClass, positionClass, userClass].filter(Boolean);
|
|
46
|
+
|
|
47
|
+
return createElementConfig(config, {
|
|
37
48
|
tag: 'nav',
|
|
38
49
|
componentName: 'nav',
|
|
39
50
|
attrs: {
|
|
40
51
|
role: 'navigation',
|
|
41
52
|
'aria-label': config.ariaLabel || 'Main Navigation'
|
|
42
53
|
},
|
|
43
|
-
className:
|
|
54
|
+
className: classNames
|
|
44
55
|
});
|
|
56
|
+
};
|
|
45
57
|
|
|
46
58
|
/**
|
|
47
59
|
* Creates API configuration for the Navigation component
|
|
@@ -50,11 +62,11 @@ export const getElementConfig = (config: NavigationConfig) =>
|
|
|
50
62
|
*/
|
|
51
63
|
export const getApiConfig = (comp: BaseComponent): ApiOptions => ({
|
|
52
64
|
disabled: {
|
|
53
|
-
enable: comp.disabled?.enable,
|
|
54
|
-
disable: comp.disabled?.disable
|
|
65
|
+
enable: comp.disabled?.enable || (() => {}),
|
|
66
|
+
disable: comp.disabled?.disable || (() => {})
|
|
55
67
|
},
|
|
56
68
|
lifecycle: {
|
|
57
|
-
destroy: comp.lifecycle?.destroy
|
|
69
|
+
destroy: comp.lifecycle?.destroy || (() => {})
|
|
58
70
|
}
|
|
59
71
|
});
|
|
60
72
|
|
|
@@ -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;
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
// src/components/navigation/features/items.ts
|
|
2
2
|
import { createNavItem, getAllNestedItems } from '../nav-item';
|
|
3
|
-
import { NavItemConfig, NavItemData } from '../types';
|
|
3
|
+
import { NavItemConfig, NavItemData, BaseComponent, NavClass } from '../types';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
destroy: () => void;
|
|
11
|
-
};
|
|
12
|
-
[key: string]: any;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface ItemsComponent extends Component {
|
|
5
|
+
/**
|
|
6
|
+
* Interface for a component with items management capabilities
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
interface ItemsComponent extends BaseComponent {
|
|
16
10
|
items: Map<string, NavItemData>;
|
|
17
11
|
addItem: (config: NavItemConfig) => ItemsComponent;
|
|
18
12
|
removeItem: (id: string) => ItemsComponent;
|
|
@@ -23,15 +17,44 @@ interface ItemsComponent extends Component {
|
|
|
23
17
|
setActive: (id: string) => ItemsComponent;
|
|
24
18
|
}
|
|
25
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Interface for navigation configuration
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
26
24
|
interface NavigationConfig {
|
|
27
25
|
prefix?: string;
|
|
28
26
|
items?: NavItemConfig[];
|
|
27
|
+
debug?: boolean;
|
|
29
28
|
[key: string]: any;
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Helper to get element ID or component type
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
function getElementId(element: HTMLElement | null, prefix: string): string | null {
|
|
36
|
+
if (!element) return null;
|
|
37
|
+
|
|
38
|
+
// Try to get data-id
|
|
39
|
+
const id = element.getAttribute('data-id');
|
|
40
|
+
if (id) return id;
|
|
41
|
+
|
|
42
|
+
// Try to identify by component class
|
|
43
|
+
if (element.classList.contains(`${prefix}-nav--rail`)) return 'rail';
|
|
44
|
+
if (element.classList.contains(`${prefix}-nav--drawer`)) return 'drawer';
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Adds navigation items management to a component
|
|
51
|
+
* @param {NavigationConfig} config - Navigation configuration
|
|
52
|
+
* @returns {Function} Component enhancer function
|
|
53
|
+
*/
|
|
54
|
+
export const withNavItems = (config: NavigationConfig) => (component: BaseComponent): ItemsComponent => {
|
|
33
55
|
const items = new Map<string, NavItemData>();
|
|
34
56
|
let activeItem: NavItemData | null = null;
|
|
57
|
+
const prefix = config.prefix || 'mtrl';
|
|
35
58
|
|
|
36
59
|
/**
|
|
37
60
|
* Recursively stores items in the items Map
|
|
@@ -43,9 +66,9 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
|
|
|
43
66
|
|
|
44
67
|
if (itemConfig.items?.length) {
|
|
45
68
|
itemConfig.items.forEach(nestedConfig => {
|
|
46
|
-
const container = item.closest(`.${
|
|
69
|
+
const container = item.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}`);
|
|
47
70
|
if (container) {
|
|
48
|
-
const nestedContainer = container.querySelector(`.${
|
|
71
|
+
const nestedContainer = container.querySelector(`.${prefix}-${NavClass.NESTED_CONTAINER}`);
|
|
49
72
|
if (nestedContainer) {
|
|
50
73
|
const nestedItem = nestedContainer.querySelector(`[data-id="${nestedConfig.id}"]`) as HTMLElement;
|
|
51
74
|
if (nestedItem) {
|
|
@@ -68,7 +91,7 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
|
|
|
68
91
|
const role = item.getAttribute('role');
|
|
69
92
|
|
|
70
93
|
if (active) {
|
|
71
|
-
item.classList.add(`${
|
|
94
|
+
item.classList.add(`${prefix}-${NavClass.ITEM}--active`);
|
|
72
95
|
|
|
73
96
|
// Set appropriate attribute based on role
|
|
74
97
|
if (role === 'tab') {
|
|
@@ -79,7 +102,7 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
|
|
|
79
102
|
item.setAttribute('aria-current', 'page');
|
|
80
103
|
}
|
|
81
104
|
} else {
|
|
82
|
-
item.classList.remove(`${
|
|
105
|
+
item.classList.remove(`${prefix}-${NavClass.ITEM}--active`);
|
|
83
106
|
|
|
84
107
|
// Remove appropriate attribute based on role
|
|
85
108
|
if (role === 'tab') {
|
|
@@ -94,7 +117,7 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
|
|
|
94
117
|
// Create initial items
|
|
95
118
|
if (config.items) {
|
|
96
119
|
config.items.forEach(itemConfig => {
|
|
97
|
-
const item = createNavItem(itemConfig, component.element,
|
|
120
|
+
const item = createNavItem(itemConfig, component.element, prefix);
|
|
98
121
|
storeItem(itemConfig, item);
|
|
99
122
|
|
|
100
123
|
if (itemConfig.active) {
|
|
@@ -104,72 +127,52 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
|
|
|
104
127
|
});
|
|
105
128
|
}
|
|
106
129
|
|
|
107
|
-
//
|
|
108
|
-
component.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
updateActiveState(item, itemData, true);
|
|
130
|
-
activeItem = itemData;
|
|
130
|
+
// Set up enhanced event handling for mouse events
|
|
131
|
+
if (component.emit) {
|
|
132
|
+
// Mouse over event handler
|
|
133
|
+
component.element.addEventListener('mouseover', (event: MouseEvent) => {
|
|
134
|
+
// Find the closest item with data-id
|
|
135
|
+
const target = event.target as HTMLElement;
|
|
136
|
+
const item = target.closest(`[data-id]`) as HTMLElement;
|
|
137
|
+
if (item) {
|
|
138
|
+
const id = item.dataset.id;
|
|
139
|
+
if (id) {
|
|
140
|
+
component.emit('mouseover', {
|
|
141
|
+
id,
|
|
142
|
+
clientX: event.clientX,
|
|
143
|
+
clientY: event.clientY,
|
|
144
|
+
target: item,
|
|
145
|
+
item: items.get(id)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}, { passive: true });
|
|
131
150
|
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
component.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
151
|
+
// Mouse enter event
|
|
152
|
+
component.element.addEventListener('mouseenter', (event: MouseEvent) => {
|
|
153
|
+
const componentId = component.element.dataset.id || component.componentName || 'nav';
|
|
154
|
+
|
|
155
|
+
component.emit('mouseenter', {
|
|
156
|
+
clientX: event.clientX,
|
|
157
|
+
clientY: event.clientY,
|
|
158
|
+
id: componentId
|
|
139
159
|
});
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Gets the path to an item (parent IDs)
|
|
145
|
-
* @param {string} id - Item ID to get path for
|
|
146
|
-
* @returns {Array<string>} Array of parent item IDs
|
|
147
|
-
*/
|
|
148
|
-
const getItemPath = (id: string): string[] => {
|
|
149
|
-
const path: string[] = [];
|
|
150
|
-
let currentItem = items.get(id);
|
|
151
|
-
|
|
152
|
-
if (!currentItem) return path;
|
|
153
|
-
|
|
154
|
-
let parentContainer = currentItem.element.closest(`.${config.prefix}-nav-nested-container`);
|
|
155
|
-
while (parentContainer) {
|
|
156
|
-
const parentItemContainer = parentContainer.parentElement;
|
|
157
|
-
if (!parentItemContainer) break;
|
|
158
|
-
|
|
159
|
-
const parentItem = parentItemContainer.querySelector(`.${config.prefix}-nav-item`);
|
|
160
|
-
if (!parentItem) break;
|
|
161
|
-
|
|
162
|
-
const parentId = parentItem.getAttribute('data-id');
|
|
163
|
-
if (!parentId) break;
|
|
160
|
+
}, { passive: true });
|
|
164
161
|
|
|
165
|
-
|
|
162
|
+
// Mouse leave event
|
|
163
|
+
component.element.addEventListener('mouseleave', (event: MouseEvent) => {
|
|
164
|
+
const relatedTarget = event.relatedTarget as HTMLElement;
|
|
165
|
+
const relatedTargetId = getElementId(relatedTarget, prefix);
|
|
166
|
+
const componentId = component.element.dataset.id || component.componentName || 'nav';
|
|
166
167
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
168
|
+
component.emit('mouseleave', {
|
|
169
|
+
clientX: event.clientX,
|
|
170
|
+
clientY: event.clientY,
|
|
171
|
+
relatedTargetId,
|
|
172
|
+
id: componentId
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
173
176
|
|
|
174
177
|
// Clean up when component is destroyed
|
|
175
178
|
if (component.lifecycle) {
|
|
@@ -182,6 +185,76 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
|
|
|
182
185
|
};
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Gets the path to an item (parent IDs and the item's own ID)
|
|
190
|
+
* @param {string} id - Item ID to get path for
|
|
191
|
+
* @returns {Array<string>} Array of parent item IDs including the item's own ID
|
|
192
|
+
*/
|
|
193
|
+
const getItemPath = (id: string): string[] => {
|
|
194
|
+
// Always include the item's own ID in the path
|
|
195
|
+
const path: string[] = [id];
|
|
196
|
+
const currentItem = items.get(id);
|
|
197
|
+
|
|
198
|
+
if (!currentItem) {
|
|
199
|
+
return path; // Still return [id] even if item not found in map
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// First, find the item container that contains this element
|
|
203
|
+
const itemContainer = currentItem.element.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}`);
|
|
204
|
+
if (!itemContainer) {
|
|
205
|
+
return path;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Then find the parent element of the item container
|
|
209
|
+
const parentElement = itemContainer.parentElement;
|
|
210
|
+
|
|
211
|
+
// If parent element is not a nested container, this is a root-level item
|
|
212
|
+
// Just return the item's own ID
|
|
213
|
+
if (!parentElement || !parentElement.classList.contains(`${prefix}-${NavClass.NESTED_CONTAINER}`)) {
|
|
214
|
+
return path;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// We're dealing with a nested item - find its ancestors
|
|
218
|
+
let currentNestedContainer = parentElement;
|
|
219
|
+
|
|
220
|
+
while (currentNestedContainer) {
|
|
221
|
+
// Find the parent item container
|
|
222
|
+
const parentItemContainer = currentNestedContainer.parentElement;
|
|
223
|
+
|
|
224
|
+
if (!parentItemContainer || !parentItemContainer.classList.contains(`${prefix}-${NavClass.ITEM_CONTAINER}`)) {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Find the parent item button/element
|
|
229
|
+
const parentItem = parentItemContainer.querySelector(`.${prefix}-${NavClass.ITEM}[data-id]`);
|
|
230
|
+
|
|
231
|
+
if (!parentItem) {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const parentId = parentItem.getAttribute('data-id');
|
|
236
|
+
|
|
237
|
+
if (!parentId) {
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Add to the beginning of path (since we're going up the tree)
|
|
242
|
+
// This puts ancestors before the item's own ID
|
|
243
|
+
path.unshift(parentId);
|
|
244
|
+
|
|
245
|
+
// Move up to the next level - find the container of this parent's container
|
|
246
|
+
const grandparentElement = parentItemContainer.parentElement;
|
|
247
|
+
|
|
248
|
+
if (!grandparentElement || !grandparentElement.classList.contains(`${prefix}-${NavClass.NESTED_CONTAINER}`)) {
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
currentNestedContainer = grandparentElement;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return path;
|
|
256
|
+
};
|
|
257
|
+
|
|
185
258
|
return {
|
|
186
259
|
...component,
|
|
187
260
|
items,
|
|
@@ -189,7 +262,7 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
|
|
|
189
262
|
addItem(itemConfig: NavItemConfig) {
|
|
190
263
|
if (items.has(itemConfig.id)) return this;
|
|
191
264
|
|
|
192
|
-
const item = createNavItem(itemConfig, component.element,
|
|
265
|
+
const item = createNavItem(itemConfig, component.element, prefix);
|
|
193
266
|
storeItem(itemConfig, item);
|
|
194
267
|
|
|
195
268
|
if (itemConfig.active) {
|
|
@@ -208,9 +281,9 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
|
|
|
208
281
|
removeItem(id: string) {
|
|
209
282
|
const item = items.get(id);
|
|
210
283
|
if (!item) return this;
|
|
211
|
-
|
|
284
|
+
|
|
212
285
|
// Remove all nested items first
|
|
213
|
-
const nestedItems = getAllNestedItems(item.element,
|
|
286
|
+
const nestedItems = getAllNestedItems(item.element, prefix);
|
|
214
287
|
nestedItems.forEach(nestedItem => {
|
|
215
288
|
const nestedId = nestedItem.dataset.id;
|
|
216
289
|
if (nestedId) items.delete(nestedId);
|
|
@@ -221,7 +294,7 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
|
|
|
221
294
|
}
|
|
222
295
|
|
|
223
296
|
// Remove the entire item container
|
|
224
|
-
const container = item.element.closest(`.${
|
|
297
|
+
const container = item.element.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}`);
|
|
225
298
|
if (container) {
|
|
226
299
|
container.remove();
|
|
227
300
|
}
|
|
@@ -241,7 +314,7 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
|
|
|
241
314
|
setActive(id: string) {
|
|
242
315
|
const item = items.get(id);
|
|
243
316
|
if (!item || item.config.disabled) return this;
|
|
244
|
-
|
|
317
|
+
|
|
245
318
|
if (activeItem) {
|
|
246
319
|
updateActiveState(activeItem.element, activeItem, false);
|
|
247
320
|
}
|
|
@@ -255,9 +328,9 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
|
|
|
255
328
|
const parentItem = items.get(parentId);
|
|
256
329
|
if (parentItem) {
|
|
257
330
|
const parentButton = parentItem.element;
|
|
258
|
-
const container = parentButton.closest(`.${
|
|
331
|
+
const container = parentButton.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}`);
|
|
259
332
|
if (container) {
|
|
260
|
-
const nestedContainer = container.querySelector(`.${
|
|
333
|
+
const nestedContainer = container.querySelector(`.${prefix}-${NavClass.NESTED_CONTAINER}`);
|
|
261
334
|
if (nestedContainer) {
|
|
262
335
|
parentButton.setAttribute('aria-expanded', 'true');
|
|
263
336
|
nestedContainer.hidden = false;
|