mtrl 0.2.6 → 0.2.8
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/demo/build.ts +349 -0
- package/demo/index.html +110 -0
- package/demo/main.js +448 -0
- package/demo/styles.css +239 -0
- package/index.ts +18 -0
- package/package.json +14 -3
- package/server.ts +86 -0
- package/src/components/badge/api.ts +70 -63
- package/src/components/badge/badge.ts +16 -2
- package/src/components/badge/config.ts +66 -13
- package/src/components/badge/features.ts +51 -42
- package/src/components/badge/index.ts +27 -2
- package/src/components/badge/types.ts +62 -30
- package/src/components/bottom-app-bar/bottom-app-bar.ts +154 -0
- package/src/components/bottom-app-bar/config.ts +29 -0
- package/src/components/bottom-app-bar/index.ts +17 -0
- package/src/components/bottom-app-bar/types.ts +114 -0
- package/src/components/button/api.ts +5 -0
- package/src/components/button/button.ts +0 -1
- package/src/components/button/config.ts +6 -2
- package/src/components/button/index.ts +10 -2
- package/src/components/button/types.ts +20 -2
- package/src/components/card/card.ts +13 -25
- package/src/components/card/config.ts +83 -30
- package/src/components/card/content.ts +8 -10
- package/src/components/card/features.ts +4 -3
- package/src/components/card/index.ts +29 -2
- package/src/components/card/types.ts +33 -22
- 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 +265 -0
- package/src/components/datepicker/config.ts +141 -0
- package/src/components/datepicker/datepicker.ts +341 -0
- package/src/components/datepicker/index.ts +12 -0
- package/src/components/datepicker/render.ts +450 -0
- package/src/components/datepicker/types.ts +397 -0
- package/src/components/datepicker/utils.ts +289 -0
- package/src/components/dialog/api.ts +55 -21
- package/src/components/dialog/config.ts +12 -9
- package/src/components/dialog/dialog.ts +6 -3
- package/src/components/dialog/features.ts +345 -151
- package/src/components/dialog/index.ts +38 -8
- package/src/components/dialog/types.ts +40 -14
- package/src/components/divider/config.ts +81 -0
- package/src/components/divider/divider.ts +37 -0
- package/src/components/divider/features.ts +207 -0
- package/src/components/divider/index.ts +9 -0
- package/src/components/divider/types.ts +55 -0
- package/src/components/extended-fab/api.ts +141 -0
- package/src/components/extended-fab/config.ts +112 -0
- package/src/components/extended-fab/extended-fab.ts +125 -0
- package/src/components/extended-fab/index.ts +9 -0
- package/src/components/extended-fab/types.ts +304 -0
- package/src/components/fab/api.ts +97 -0
- package/src/components/fab/config.ts +93 -0
- package/src/components/fab/fab.ts +67 -0
- package/src/components/fab/index.ts +9 -0
- package/src/components/fab/types.ts +251 -0
- 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 +78 -50
- package/src/components/navigation/config.ts +22 -10
- package/src/components/navigation/features/items.ts +284 -0
- package/src/components/navigation/index.ts +0 -6
- package/src/components/navigation/nav-item.ts +70 -33
- package/src/components/navigation/navigation.ts +53 -3
- package/src/components/navigation/types.ts +117 -70
- 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/api.ts +203 -0
- package/src/components/search/config.ts +86 -0
- package/src/components/search/features/index.ts +4 -0
- package/src/components/search/features/search.ts +717 -0
- package/src/components/search/features/states.ts +169 -0
- package/src/components/search/features/structure.ts +197 -0
- package/src/components/search/index.ts +7 -0
- package/src/components/search/search.ts +52 -0
- package/src/components/search/types.ts +175 -0
- package/src/components/segmented-button/config.ts +80 -0
- package/src/components/segmented-button/index.ts +4 -0
- package/src/components/segmented-button/segment.ts +154 -0
- package/src/components/segmented-button/segmented-button.ts +249 -0
- package/src/components/segmented-button/types.ts +254 -0
- package/src/components/slider/accessibility.md +5 -5
- package/src/components/slider/api.ts +41 -120
- package/src/components/slider/config.ts +51 -47
- package/src/components/slider/features/handlers.ts +495 -0
- package/src/components/slider/features/index.ts +1 -2
- package/src/components/slider/features/slider.ts +66 -84
- package/src/components/slider/features/states.ts +195 -0
- package/src/components/slider/features/structure.ts +136 -206
- package/src/components/slider/features/ui.ts +145 -206
- package/src/components/slider/index.ts +2 -11
- package/src/components/slider/slider.ts +9 -12
- package/src/components/slider/types.ts +67 -26
- 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/features.ts +4 -2
- package/src/components/tabs/index.ts +0 -15
- package/src/components/tabs/indicator.ts +73 -13
- package/src/components/tabs/tab-api.ts +12 -4
- package/src/components/tabs/tab.ts +18 -6
- package/src/components/tabs/types.ts +23 -5
- package/src/components/textfield/config.ts +2 -3
- package/src/components/textfield/index.ts +0 -1
- package/src/components/textfield/types.ts +17 -3
- package/src/components/timepicker/README.md +277 -0
- package/src/components/timepicker/api.ts +632 -0
- package/src/components/timepicker/clockdial.ts +482 -0
- package/src/components/timepicker/config.ts +228 -0
- package/src/components/timepicker/index.ts +3 -0
- package/src/components/timepicker/render.ts +613 -0
- package/src/components/timepicker/timepicker.ts +117 -0
- package/src/components/timepicker/types.ts +336 -0
- package/src/components/timepicker/utils.ts +241 -0
- 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/components/top-app-bar/config.ts +83 -0
- package/src/components/top-app-bar/index.ts +11 -0
- package/src/components/top-app-bar/top-app-bar.ts +316 -0
- package/src/components/top-app-bar/types.ts +140 -0
- package/src/core/build/_ripple.scss +6 -6
- package/src/core/build/ripple.ts +72 -95
- package/src/core/compose/features/icon.ts +3 -1
- package/src/core/compose/features/ripple.ts +4 -1
- package/src/core/compose/features/textlabel.ts +23 -2
- package/src/core/dom/create.ts +5 -0
- package/src/index.ts +9 -0
- package/src/styles/abstract/_theme.scss +9 -1
- package/src/styles/components/_badge.scss +182 -0
- package/src/styles/components/_bottom-app-bar.scss +103 -0
- package/src/{components/button/_styles.scss → styles/components/_button.scss} +0 -10
- package/src/{components/checkbox/_styles.scss → styles/components/_checkbox.scss} +0 -2
- package/src/styles/components/_datepicker.scss +358 -0
- package/src/styles/components/_dialog.scss +259 -0
- package/src/styles/components/_divider.scss +57 -0
- package/src/styles/components/_extended-fab.scss +267 -0
- package/src/styles/components/_fab.scss +225 -0
- package/src/{components/navigation/_styles.scss → styles/components/_navigation.scss} +1 -0
- package/src/styles/components/_search.scss +306 -0
- package/src/styles/components/_segmented-button.scss +117 -0
- package/src/{components/slider/_styles.scss → styles/components/_slider.scss} +83 -24
- package/src/{components/switch/_styles.scss → styles/components/_switch.scss} +0 -2
- package/src/{components/tabs/_styles.scss → styles/components/_tabs.scss} +95 -33
- package/src/{components/textfield/_styles.scss → styles/components/_textfield.scss} +70 -67
- package/src/styles/components/_timepicker.scss +451 -0
- package/src/styles/components/_top-app-bar.scss +225 -0
- package/src/styles/main.scss +98 -49
- package/src/styles/themes/_autumn.scss +21 -0
- package/src/styles/themes/_base-theme.scss +61 -0
- package/src/styles/themes/_baseline.scss +58 -0
- package/src/styles/themes/_bluekhaki.scss +125 -0
- package/src/styles/themes/_brownbeige.scss +125 -0
- package/src/styles/themes/_browngreen.scss +125 -0
- package/src/styles/themes/_forest.scss +6 -0
- package/src/styles/themes/_greenbeige.scss +125 -0
- package/src/styles/themes/_material.scss +125 -0
- package/src/styles/themes/_ocean.scss +6 -0
- package/src/styles/themes/_sageivory.scss +125 -0
- package/src/styles/themes/_spring.scss +6 -0
- package/src/styles/themes/_summer.scss +5 -0
- package/src/styles/themes/_sunset.scss +5 -0
- package/src/styles/themes/_tealcaramel.scss +125 -0
- package/src/styles/themes/_winter.scss +6 -0
- package/src/components/badge/_styles.scss +0 -174
- package/src/components/badge/constants.ts +0 -30
- package/src/components/button/constants.ts +0 -11
- package/src/components/card/constants.ts +0 -84
- package/src/components/dialog/_styles.scss +0 -213
- package/src/components/dialog/constants.ts +0 -32
- package/src/components/menu/constants.ts +0 -154
- package/src/components/navigation/constants.ts +0 -200
- package/src/components/navigation/features/items.js +0 -192
- package/src/components/progress/constants.ts +0 -29
- package/src/components/slider/features/appearance.ts +0 -94
- package/src/components/slider/features/disabled.ts +0 -68
- package/src/components/slider/features/events.ts +0 -164
- package/src/components/slider/features/interactions.ts +0 -396
- package/src/components/slider/features/keyboard.ts +0 -233
- package/src/components/switch/constants.ts +0 -80
- package/src/components/tabs/constants.ts +0 -89
- package/src/core/collection/adapters/mongodb.js +0 -232
- /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/chip/_styles.scss → styles/components/_chip.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/progress/_styles.scss → styles/components/_progress.scss} +0 -0
- /package/src/{components/radios/_styles.scss → styles/components/_radios.scss} +0 -0
- /package/src/{components/sheet/_styles.scss → styles/components/_sheet.scss} +0 -0
- /package/src/{components/snackbar/_styles.scss → styles/components/_snackbar.scss} +0 -0
- /package/src/{components/tooltip/_styles.scss → styles/components/_tooltip.scss} +0 -0
- /package/src/styles/utilities/{_color.scss → _colors.scss} +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// src/components/navigation/features/items.ts
|
|
2
|
+
import { createNavItem, getAllNestedItems } from '../nav-item';
|
|
3
|
+
import { NavItemConfig, NavItemData, BaseComponent, NavClass } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Interface for a component with items management capabilities
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
interface ItemsComponent extends BaseComponent {
|
|
10
|
+
items: Map<string, NavItemData>;
|
|
11
|
+
addItem: (config: NavItemConfig) => ItemsComponent;
|
|
12
|
+
removeItem: (id: string) => ItemsComponent;
|
|
13
|
+
getItem: (id: string) => NavItemData | undefined;
|
|
14
|
+
getAllItems: () => NavItemData[];
|
|
15
|
+
getActive: () => NavItemData | null;
|
|
16
|
+
getItemPath: (id: string) => string[];
|
|
17
|
+
setActive: (id: string) => ItemsComponent;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Interface for navigation configuration
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
interface NavigationConfig {
|
|
25
|
+
prefix?: string;
|
|
26
|
+
items?: NavItemConfig[];
|
|
27
|
+
[key: string]: any;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Adds navigation items management to a component
|
|
32
|
+
* @param {NavigationConfig} config - Navigation configuration
|
|
33
|
+
* @returns {Function} Component enhancer function
|
|
34
|
+
*/
|
|
35
|
+
export const withNavItems = (config: NavigationConfig) => (component: BaseComponent): ItemsComponent => {
|
|
36
|
+
const items = new Map<string, NavItemData>();
|
|
37
|
+
let activeItem: NavItemData | null = null;
|
|
38
|
+
const prefix = config.prefix || 'mtrl';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Recursively stores items in the items Map
|
|
42
|
+
* @param {NavItemConfig} itemConfig - Item configuration
|
|
43
|
+
* @param {HTMLElement} item - Created item element
|
|
44
|
+
*/
|
|
45
|
+
const storeItem = (itemConfig: NavItemConfig, item: HTMLElement): void => {
|
|
46
|
+
items.set(itemConfig.id, { element: item, config: itemConfig });
|
|
47
|
+
|
|
48
|
+
if (itemConfig.items?.length) {
|
|
49
|
+
itemConfig.items.forEach(nestedConfig => {
|
|
50
|
+
const container = item.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}`);
|
|
51
|
+
if (container) {
|
|
52
|
+
const nestedContainer = container.querySelector(`.${prefix}-${NavClass.NESTED_CONTAINER}`);
|
|
53
|
+
if (nestedContainer) {
|
|
54
|
+
const nestedItem = nestedContainer.querySelector(`[data-id="${nestedConfig.id}"]`) as HTMLElement;
|
|
55
|
+
if (nestedItem) {
|
|
56
|
+
storeItem(nestedConfig, nestedItem);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Updates the active state for an item
|
|
66
|
+
* @param {HTMLElement} item - Item element to activate
|
|
67
|
+
* @param {NavItemData} itemData - Item data
|
|
68
|
+
* @param {boolean} active - Whether to make active or inactive
|
|
69
|
+
*/
|
|
70
|
+
const updateActiveState = (item: HTMLElement, itemData: NavItemData, active: boolean): void => {
|
|
71
|
+
// Determine the correct active attribute based on role
|
|
72
|
+
const role = item.getAttribute('role');
|
|
73
|
+
|
|
74
|
+
if (active) {
|
|
75
|
+
item.classList.add(`${prefix}-${NavClass.ITEM}--active`);
|
|
76
|
+
|
|
77
|
+
// Set appropriate attribute based on role
|
|
78
|
+
if (role === 'tab') {
|
|
79
|
+
item.setAttribute('aria-selected', 'true');
|
|
80
|
+
item.setAttribute('tabindex', '0');
|
|
81
|
+
} else if (!item.getAttribute('aria-haspopup')) {
|
|
82
|
+
// Use aria-current for navigation items that aren't expandable
|
|
83
|
+
item.setAttribute('aria-current', 'page');
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
item.classList.remove(`${prefix}-${NavClass.ITEM}--active`);
|
|
87
|
+
|
|
88
|
+
// Remove appropriate attribute based on role
|
|
89
|
+
if (role === 'tab') {
|
|
90
|
+
item.setAttribute('aria-selected', 'false');
|
|
91
|
+
item.setAttribute('tabindex', '-1');
|
|
92
|
+
} else if (item.hasAttribute('aria-current')) {
|
|
93
|
+
item.removeAttribute('aria-current');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Create initial items
|
|
99
|
+
if (config.items) {
|
|
100
|
+
config.items.forEach(itemConfig => {
|
|
101
|
+
const item = createNavItem(itemConfig, component.element, prefix);
|
|
102
|
+
storeItem(itemConfig, item);
|
|
103
|
+
|
|
104
|
+
if (itemConfig.active) {
|
|
105
|
+
activeItem = { element: item, config: itemConfig };
|
|
106
|
+
updateActiveState(item, activeItem, true);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Handle item clicks
|
|
112
|
+
component.element.addEventListener('click', (event: Event) => {
|
|
113
|
+
const item = (event.target as HTMLElement).closest(`.${prefix}-${NavClass.ITEM}`) as HTMLElement;
|
|
114
|
+
if (!item || (item as any).disabled || item.getAttribute('aria-haspopup') === 'menu') return;
|
|
115
|
+
|
|
116
|
+
const id = item.dataset.id;
|
|
117
|
+
if (!id) return;
|
|
118
|
+
|
|
119
|
+
const itemData = items.get(id);
|
|
120
|
+
if (!itemData) return;
|
|
121
|
+
|
|
122
|
+
// Skip if this is an expandable item
|
|
123
|
+
if (item.getAttribute('aria-expanded') !== null) return;
|
|
124
|
+
|
|
125
|
+
// Store previous item before updating
|
|
126
|
+
const previousItem = activeItem;
|
|
127
|
+
|
|
128
|
+
// Update active state
|
|
129
|
+
if (activeItem) {
|
|
130
|
+
updateActiveState(activeItem.element, activeItem, false);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
updateActiveState(item, itemData, true);
|
|
134
|
+
activeItem = itemData;
|
|
135
|
+
|
|
136
|
+
// Emit change event with item data
|
|
137
|
+
if (component.emit) {
|
|
138
|
+
component.emit('change', {
|
|
139
|
+
id,
|
|
140
|
+
item: itemData,
|
|
141
|
+
previousItem,
|
|
142
|
+
path: getItemPath(id)
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Gets the path to an item (parent IDs)
|
|
149
|
+
* @param {string} id - Item ID to get path for
|
|
150
|
+
* @returns {Array<string>} Array of parent item IDs
|
|
151
|
+
*/
|
|
152
|
+
const getItemPath = (id: string): string[] => {
|
|
153
|
+
const path: string[] = [];
|
|
154
|
+
let currentItem = items.get(id);
|
|
155
|
+
|
|
156
|
+
if (!currentItem) return path;
|
|
157
|
+
|
|
158
|
+
let parentContainer = currentItem.element.closest(`.${prefix}-${NavClass.NESTED_CONTAINER}`);
|
|
159
|
+
while (parentContainer) {
|
|
160
|
+
const parentItemContainer = parentContainer.parentElement;
|
|
161
|
+
if (!parentItemContainer) break;
|
|
162
|
+
|
|
163
|
+
const parentItem = parentItemContainer.querySelector(`.${prefix}-${NavClass.ITEM}`);
|
|
164
|
+
if (!parentItem) break;
|
|
165
|
+
|
|
166
|
+
const parentId = parentItem.getAttribute('data-id');
|
|
167
|
+
if (!parentId) break;
|
|
168
|
+
|
|
169
|
+
path.unshift(parentId);
|
|
170
|
+
|
|
171
|
+
// Move up to next level
|
|
172
|
+
parentContainer = parentItemContainer.closest(`.${prefix}-${NavClass.NESTED_CONTAINER}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return path;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Clean up when component is destroyed
|
|
179
|
+
if (component.lifecycle) {
|
|
180
|
+
const originalDestroy = component.lifecycle.destroy;
|
|
181
|
+
component.lifecycle.destroy = () => {
|
|
182
|
+
items.clear();
|
|
183
|
+
if (originalDestroy) {
|
|
184
|
+
originalDestroy();
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
...component,
|
|
191
|
+
items,
|
|
192
|
+
|
|
193
|
+
addItem(itemConfig: NavItemConfig) {
|
|
194
|
+
if (items.has(itemConfig.id)) return this;
|
|
195
|
+
|
|
196
|
+
const item = createNavItem(itemConfig, component.element, prefix);
|
|
197
|
+
storeItem(itemConfig, item);
|
|
198
|
+
|
|
199
|
+
if (itemConfig.active) {
|
|
200
|
+
this.setActive(itemConfig.id);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (component.emit) {
|
|
204
|
+
component.emit('itemAdded', {
|
|
205
|
+
id: itemConfig.id,
|
|
206
|
+
item: { element: item, config: itemConfig }
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return this;
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
removeItem(id: string) {
|
|
213
|
+
const item = items.get(id);
|
|
214
|
+
if (!item) return this;
|
|
215
|
+
|
|
216
|
+
// Remove all nested items first
|
|
217
|
+
const nestedItems = getAllNestedItems(item.element, prefix);
|
|
218
|
+
nestedItems.forEach(nestedItem => {
|
|
219
|
+
const nestedId = nestedItem.dataset.id;
|
|
220
|
+
if (nestedId) items.delete(nestedId);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (activeItem?.config.id === id) {
|
|
224
|
+
activeItem = null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Remove the entire item container
|
|
228
|
+
const container = item.element.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}`);
|
|
229
|
+
if (container) {
|
|
230
|
+
container.remove();
|
|
231
|
+
}
|
|
232
|
+
items.delete(id);
|
|
233
|
+
|
|
234
|
+
if (component.emit) {
|
|
235
|
+
component.emit('itemRemoved', { id, item });
|
|
236
|
+
}
|
|
237
|
+
return this;
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
getItem: (id: string) => items.get(id),
|
|
241
|
+
getAllItems: () => Array.from(items.values()),
|
|
242
|
+
getActive: () => activeItem,
|
|
243
|
+
getItemPath: (id: string) => getItemPath(id),
|
|
244
|
+
|
|
245
|
+
setActive(id: string) {
|
|
246
|
+
const item = items.get(id);
|
|
247
|
+
if (!item || item.config.disabled) return this;
|
|
248
|
+
|
|
249
|
+
if (activeItem) {
|
|
250
|
+
updateActiveState(activeItem.element, activeItem, false);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
updateActiveState(item.element, item, true);
|
|
254
|
+
activeItem = item;
|
|
255
|
+
|
|
256
|
+
// Ensure all parent items are expanded
|
|
257
|
+
const path = getItemPath(id);
|
|
258
|
+
path.forEach(parentId => {
|
|
259
|
+
const parentItem = items.get(parentId);
|
|
260
|
+
if (parentItem) {
|
|
261
|
+
const parentButton = parentItem.element;
|
|
262
|
+
const container = parentButton.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}`);
|
|
263
|
+
if (container) {
|
|
264
|
+
const nestedContainer = container.querySelector(`.${prefix}-${NavClass.NESTED_CONTAINER}`);
|
|
265
|
+
if (nestedContainer) {
|
|
266
|
+
parentButton.setAttribute('aria-expanded', 'true');
|
|
267
|
+
nestedContainer.hidden = false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (component.emit) {
|
|
274
|
+
component.emit('activeChanged', {
|
|
275
|
+
id,
|
|
276
|
+
item,
|
|
277
|
+
previousItem: activeItem,
|
|
278
|
+
path: getItemPath(id)
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return this;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/components/navigation/nav-item.ts
|
|
2
|
-
import { NavItemConfig } from './types';
|
|
2
|
+
import { NavItemConfig, NavClass } from './types';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Creates an expand/collapse icon element
|
|
@@ -8,7 +8,7 @@ import { NavItemConfig } from './types';
|
|
|
8
8
|
*/
|
|
9
9
|
export const createExpandIcon = (prefix: string): HTMLElement => {
|
|
10
10
|
const icon = document.createElement('span');
|
|
11
|
-
icon.className = `${prefix}
|
|
11
|
+
icon.className = `${prefix}-${NavClass.EXPAND_ICON}`;
|
|
12
12
|
icon.innerHTML = `
|
|
13
13
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
|
14
14
|
<polyline points="9 18 15 12 9 6"></polyline>
|
|
@@ -30,8 +30,10 @@ export const createNestedContainer = (
|
|
|
30
30
|
createItem: (config: NavItemConfig, container: HTMLElement, prefix: string) => HTMLElement
|
|
31
31
|
): HTMLElement => {
|
|
32
32
|
const container = document.createElement('div');
|
|
33
|
-
container.className = `${prefix}
|
|
34
|
-
|
|
33
|
+
container.className = `${prefix}-${NavClass.NESTED_CONTAINER}`;
|
|
34
|
+
|
|
35
|
+
// Use appropriate role for nested menu
|
|
36
|
+
container.setAttribute('role', 'menu');
|
|
35
37
|
container.hidden = true;
|
|
36
38
|
|
|
37
39
|
items.forEach(itemConfig => {
|
|
@@ -54,67 +56,102 @@ export const createNavItem = (
|
|
|
54
56
|
prefix: string
|
|
55
57
|
): HTMLElement => {
|
|
56
58
|
const itemContainer = document.createElement('div');
|
|
57
|
-
itemContainer.className = `${prefix}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
itemContainer.className = `${prefix}-${NavClass.ITEM_CONTAINER}`;
|
|
60
|
+
|
|
61
|
+
// Determine if parent container uses tabs or menu role pattern
|
|
62
|
+
const isMenuContext = container.getAttribute('role') === 'menu';
|
|
63
|
+
const isTabContext = container.getAttribute('role') === 'tablist';
|
|
64
|
+
const isDrawerVariant = container.closest(`.${prefix}-nav--drawer, .${prefix}-nav--drawer-modal, .${prefix}-nav--drawer-standard`) !== null;
|
|
65
|
+
|
|
66
|
+
// Create the item element
|
|
67
|
+
const itemElement = document.createElement('button');
|
|
68
|
+
itemElement.className = `${prefix}-${NavClass.ITEM}`;
|
|
69
|
+
itemElement.type = 'button'; // Ensure it's a button type for proper behavior
|
|
70
|
+
|
|
71
|
+
// Set appropriate role based on context and items
|
|
72
|
+
if (config.items?.length) {
|
|
73
|
+
if (isDrawerVariant) {
|
|
74
|
+
// For expandable drawer items with nested items
|
|
75
|
+
itemElement.setAttribute('role', 'button');
|
|
76
|
+
itemElement.setAttribute('aria-expanded', config.expanded ? 'true' : 'false');
|
|
77
|
+
itemElement.setAttribute('aria-haspopup', 'menu');
|
|
78
|
+
} else {
|
|
79
|
+
// For non-drawer variants with nested items
|
|
80
|
+
itemElement.setAttribute('role', 'button');
|
|
81
|
+
}
|
|
82
|
+
} else if (isMenuContext) {
|
|
83
|
+
// For menu items
|
|
84
|
+
itemElement.setAttribute('role', 'menuitem');
|
|
85
|
+
} else if (isTabContext) {
|
|
86
|
+
// For tab items
|
|
87
|
+
itemElement.setAttribute('role', 'tab');
|
|
88
|
+
itemElement.setAttribute('aria-selected', config.active ? 'true' : 'false');
|
|
89
|
+
itemElement.setAttribute('tabindex', config.active ? '0' : '-1');
|
|
90
|
+
}
|
|
91
|
+
// For plain navigation buttons, we don't need to set the role since buttons have inherent semantics
|
|
63
92
|
|
|
64
93
|
if (config.id) {
|
|
65
|
-
|
|
94
|
+
itemElement.dataset.id = config.id;
|
|
66
95
|
}
|
|
67
96
|
|
|
68
97
|
if (config.disabled) {
|
|
69
|
-
|
|
70
|
-
|
|
98
|
+
itemElement.disabled = true;
|
|
99
|
+
itemElement.setAttribute('aria-disabled', 'true');
|
|
71
100
|
}
|
|
72
101
|
|
|
73
102
|
// Add icon if provided
|
|
74
103
|
if (config.icon) {
|
|
75
104
|
const icon = document.createElement('span');
|
|
76
|
-
icon.className = `${prefix}
|
|
105
|
+
icon.className = `${prefix}-${NavClass.ICON}`;
|
|
77
106
|
icon.innerHTML = config.icon;
|
|
78
|
-
|
|
107
|
+
itemElement.appendChild(icon);
|
|
79
108
|
}
|
|
80
109
|
|
|
81
110
|
// Add label if provided
|
|
82
111
|
if (config.label) {
|
|
83
112
|
const label = document.createElement('span');
|
|
84
|
-
label.className = `${prefix}
|
|
113
|
+
label.className = `${prefix}-${NavClass.LABEL}`;
|
|
85
114
|
label.textContent = config.label;
|
|
86
|
-
|
|
87
|
-
|
|
115
|
+
itemElement.appendChild(label);
|
|
116
|
+
itemElement.setAttribute('aria-label', config.label);
|
|
88
117
|
}
|
|
89
118
|
|
|
90
119
|
// Add badge if provided
|
|
91
120
|
if (config.badge) {
|
|
92
121
|
const badge = document.createElement('span');
|
|
93
|
-
badge.className = `${prefix}
|
|
122
|
+
badge.className = `${prefix}-${NavClass.BADGE}`;
|
|
94
123
|
badge.textContent = config.badge;
|
|
124
|
+
// Use appropriate aria labeling
|
|
95
125
|
badge.setAttribute('aria-label', `${config.badge} notifications`);
|
|
96
|
-
|
|
126
|
+
itemElement.appendChild(badge);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Mark active state with appropriate semantics
|
|
130
|
+
if (config.active && !config.items?.length) {
|
|
131
|
+
itemElement.classList.add(`${prefix}-${NavClass.ITEM}--active`);
|
|
132
|
+
|
|
133
|
+
// Use aria-current for standard navigation
|
|
134
|
+
if (!isTabContext) {
|
|
135
|
+
itemElement.setAttribute('aria-current', 'page');
|
|
136
|
+
}
|
|
97
137
|
}
|
|
98
138
|
|
|
99
|
-
itemContainer.appendChild(
|
|
139
|
+
itemContainer.appendChild(itemElement);
|
|
100
140
|
|
|
101
141
|
// Handle nested items - only for drawer variant
|
|
102
|
-
if (config.items?.length &&
|
|
142
|
+
if (config.items?.length && isDrawerVariant) {
|
|
103
143
|
const expandIcon = createExpandIcon(prefix);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
item.setAttribute('aria-expanded', config.expanded ? 'true' : 'false');
|
|
107
|
-
item.setAttribute('aria-haspopup', 'true');
|
|
144
|
+
itemElement.appendChild(expandIcon);
|
|
108
145
|
|
|
109
146
|
const nestedContainer = createNestedContainer(config.items, prefix, createNavItem);
|
|
110
147
|
nestedContainer.hidden = !config.expanded;
|
|
111
148
|
itemContainer.appendChild(nestedContainer);
|
|
112
149
|
|
|
113
150
|
// Handle expand/collapse
|
|
114
|
-
|
|
151
|
+
itemElement.addEventListener('click', (event) => {
|
|
115
152
|
event.stopPropagation();
|
|
116
|
-
const isExpanded =
|
|
117
|
-
|
|
153
|
+
const isExpanded = itemElement.getAttribute('aria-expanded') === 'true';
|
|
154
|
+
itemElement.setAttribute('aria-expanded', (!isExpanded).toString());
|
|
118
155
|
nestedContainer.hidden = isExpanded;
|
|
119
156
|
|
|
120
157
|
// Toggle expand icon rotation
|
|
@@ -125,7 +162,7 @@ export const createNavItem = (
|
|
|
125
162
|
}
|
|
126
163
|
|
|
127
164
|
container.appendChild(itemContainer);
|
|
128
|
-
return
|
|
165
|
+
return itemElement;
|
|
129
166
|
};
|
|
130
167
|
|
|
131
168
|
/**
|
|
@@ -135,13 +172,13 @@ export const createNavItem = (
|
|
|
135
172
|
* @returns {Array<HTMLElement>} Array of all nested items
|
|
136
173
|
*/
|
|
137
174
|
export const getAllNestedItems = (item: HTMLElement, prefix: string): HTMLElement[] => {
|
|
138
|
-
const container = item.closest(`.${prefix}
|
|
175
|
+
const container = item.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}`);
|
|
139
176
|
if (!container) return [];
|
|
140
177
|
|
|
141
|
-
const nestedContainer = container.querySelector(`.${prefix}
|
|
178
|
+
const nestedContainer = container.querySelector(`.${prefix}-${NavClass.NESTED_CONTAINER}`);
|
|
142
179
|
if (!nestedContainer) return [];
|
|
143
180
|
|
|
144
|
-
const items = Array.from(nestedContainer.querySelectorAll(`.${prefix}
|
|
181
|
+
const items = Array.from(nestedContainer.querySelectorAll(`.${prefix}-${NavClass.ITEM}`)) as HTMLElement[];
|
|
145
182
|
return items.reduce((acc: HTMLElement[], nestedItem: HTMLElement) => {
|
|
146
183
|
return [...acc, nestedItem, ...getAllNestedItems(nestedItem, prefix)];
|
|
147
184
|
}, []);
|
|
@@ -6,17 +6,57 @@ import {
|
|
|
6
6
|
withDisabled,
|
|
7
7
|
withLifecycle,
|
|
8
8
|
withVariant,
|
|
9
|
-
withPosition
|
|
9
|
+
withPosition
|
|
10
10
|
} from '../../core/compose/features';
|
|
11
11
|
import { withAPI } from './api';
|
|
12
12
|
import { withNavItems } from './features/items';
|
|
13
|
-
import { NavigationConfig, NavigationComponent } from './types';
|
|
13
|
+
import { NavigationConfig, NavigationComponent, NavVariant } from './types';
|
|
14
14
|
import {
|
|
15
15
|
createBaseConfig,
|
|
16
16
|
getElementConfig,
|
|
17
17
|
getApiConfig
|
|
18
18
|
} from './config';
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Sets up proper ARIA roles based on navigation variant
|
|
22
|
+
* @param {NavigationComponent} nav - Navigation component
|
|
23
|
+
* @param {NavigationConfig} config - Navigation configuration
|
|
24
|
+
*/
|
|
25
|
+
const setupAccessibility = (nav: NavigationComponent, config: NavigationConfig): void => {
|
|
26
|
+
const { element } = nav;
|
|
27
|
+
const variant = config.variant || 'rail';
|
|
28
|
+
const prefix = config.prefix || 'mtrl';
|
|
29
|
+
|
|
30
|
+
// Set appropriate aria-label
|
|
31
|
+
element.setAttribute('aria-label', config.ariaLabel || 'Main Navigation');
|
|
32
|
+
|
|
33
|
+
// For bar navigation (bottom or top nav)
|
|
34
|
+
if (variant === 'bar') {
|
|
35
|
+
// If bar navigation is acting as tabs
|
|
36
|
+
const hasNestedItems = config.items?.some(item => item.items?.length) || false;
|
|
37
|
+
|
|
38
|
+
if (!hasNestedItems) {
|
|
39
|
+
element.setAttribute('role', 'tablist');
|
|
40
|
+
element.setAttribute('aria-orientation', 'horizontal');
|
|
41
|
+
} else {
|
|
42
|
+
element.setAttribute('role', 'menubar');
|
|
43
|
+
element.setAttribute('aria-orientation', 'horizontal');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// For rail and drawer navigation
|
|
47
|
+
else {
|
|
48
|
+
// Use standard navigation landmark
|
|
49
|
+
element.setAttribute('role', 'navigation');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Set hidden state for modal drawers if needed
|
|
53
|
+
if ((variant === 'modal' ||
|
|
54
|
+
(variant === 'drawer' && config.behavior === 'dismissible')) &&
|
|
55
|
+
!config.expanded) {
|
|
56
|
+
element.classList.add(`${prefix}-nav--hidden`);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
20
60
|
/**
|
|
21
61
|
* Creates a new Navigation component
|
|
22
62
|
* @param {NavigationConfig} config - Navigation configuration
|
|
@@ -40,7 +80,17 @@ const createNavigation = (config: NavigationConfig = {}): NavigationComponent =>
|
|
|
40
80
|
comp => withAPI(getApiConfig(comp))(comp)
|
|
41
81
|
)(baseConfig);
|
|
42
82
|
|
|
43
|
-
|
|
83
|
+
const nav = navigation as NavigationComponent;
|
|
84
|
+
|
|
85
|
+
// Set up proper ARIA roles and relationships
|
|
86
|
+
setupAccessibility(nav, baseConfig);
|
|
87
|
+
|
|
88
|
+
// Implement any initialization logic
|
|
89
|
+
if (baseConfig.disabled) {
|
|
90
|
+
nav.disable();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return nav;
|
|
44
94
|
} catch (error) {
|
|
45
95
|
console.error('Navigation creation error:', error instanceof Error ? error.message : String(error));
|
|
46
96
|
throw new Error(`Failed to create navigation: ${error instanceof Error ? error.message : String(error)}`);
|