@kubex/zinc 1.0.25 → 1.0.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/project.md +556 -0
- package/.github/workflows/js_build_and_deploy.yaml +9 -10
- package/dist/custom-elements.json +13575 -8856
- package/dist/vscode.html-custom-data.json +583 -48
- package/dist/web-types.json +1441 -181
- package/dist/zn.d.ts +1645 -660
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +1412 -893
- package/docs/_utilities/code-previews.cjs +10 -1
- package/docs/assets/scripts/docs.js +42 -18
- package/docs/data/data-table.json +229 -66
- package/docs/data/empty.json +6 -0
- package/docs/data/products-table.json +41 -193
- package/docs/pages/components/action-bar.md +121 -6
- package/docs/pages/components/alert.md +118 -15
- package/docs/pages/components/animated-button.md +299 -0
- package/docs/pages/components/audio-select.md +534 -0
- package/docs/pages/components/bulk-actions.md +79 -5
- package/docs/pages/components/button-group.md +235 -27
- package/docs/pages/components/button.md +139 -1
- package/docs/pages/components/chart.md +365 -9
- package/docs/pages/components/checkbox.md +387 -58
- package/docs/pages/components/chip.md +253 -33
- package/docs/pages/components/collapsible.md +393 -46
- package/docs/pages/components/cols.md +152 -30
- package/docs/pages/components/confirm.md +182 -16
- package/docs/pages/components/copy-button.md +169 -6
- package/docs/pages/components/data-select.md +194 -15
- package/docs/pages/components/data-table-filter.md +96 -5
- package/docs/pages/components/data-table-search.md +451 -0
- package/docs/pages/components/data-table-sort.md +66 -5
- package/docs/pages/components/data-table.md +727 -66
- package/docs/pages/components/datepicker.md +402 -7
- package/docs/pages/components/defined-label.md +120 -5
- package/docs/pages/components/dialog.md +339 -59
- package/docs/pages/components/dropdown.md +366 -11
- package/docs/pages/components/editor.md +604 -7
- package/docs/pages/components/empty-state.md +315 -16
- package/docs/pages/components/expanding-action.md +212 -28
- package/docs/pages/components/file.md +421 -12
- package/docs/pages/components/filter-container.md +158 -5
- package/docs/pages/components/filter-wrapper.md +167 -6
- package/docs/pages/components/form-group.md +383 -6
- package/docs/pages/components/hover-container.md +237 -6
- package/docs/pages/components/icon-picker.md +121 -0
- package/docs/pages/components/icon.md +454 -48
- package/docs/pages/components/inline-edit.md +787 -19
- package/docs/pages/components/input-group.md +207 -13
- package/docs/pages/components/input.md +194 -0
- package/docs/pages/components/item.md +18 -0
- package/docs/pages/components/key-container.md +115 -0
- package/docs/pages/components/key.md +101 -0
- package/docs/pages/components/menu.md +512 -6
- package/docs/pages/components/note.md +239 -36
- package/docs/pages/components/opt-group.md +107 -0
- package/docs/pages/components/pagination.md +332 -6
- package/docs/pages/components/pane.md +222 -6
- package/docs/pages/components/panel.md +213 -26
- package/docs/pages/components/popup.md +1354 -6
- package/docs/pages/components/priority-list.md +233 -0
- package/docs/pages/components/progress-bar.md +387 -6
- package/docs/pages/components/radio-group.md +242 -5
- package/docs/pages/components/radio.md +474 -6
- package/docs/pages/components/rating.md +331 -7
- package/docs/pages/components/reveal.md +524 -6
- package/docs/pages/components/scroll-container.md +552 -6
- package/docs/pages/components/select.md +762 -160
- package/docs/pages/components/skeleton.md +473 -30
- package/docs/pages/components/slideout.md +261 -5
- package/docs/pages/components/sp.md +99 -41
- package/docs/pages/components/split-button.md +347 -9
- package/docs/pages/components/split-pane.md +445 -6
- package/docs/pages/components/stat.md +549 -9
- package/docs/pages/components/status-indicator.md +230 -1
- package/docs/pages/components/stepper.md +563 -6
- package/docs/pages/components/tabs.md +439 -13
- package/docs/pages/components/textarea.md +415 -17
- package/docs/pages/components/tile.md +385 -26
- package/docs/pages/components/toggle.md +424 -6
- package/docs/pages/components/tooltip.md +367 -0
- package/docs/pages/components/translation-group.md +210 -0
- package/docs/pages/components/translations.md +319 -19
- package/docs/pages/components/vertical-stepper.md +401 -8
- package/docs/pages/components/well.md +300 -3
- package/docs/pages/getting-started/form-controls.md +162 -0
- package/docs/pages/index.md +116 -15
- package/package.json +5 -6
- package/scss/_global-spacing.scss +4 -0
- package/scss/_root.scss +2 -2
- package/scss/boot.scss +19 -0
- package/scss/shared/layout.scss +5 -0
- package/scss/themes/_light.scss +3 -3
- package/src/components/alert/alert.scss +1 -0
- package/src/components/animated-button/README.md +306 -0
- package/src/components/animated-button/animated-button.component.ts +229 -0
- package/src/components/animated-button/animated-button.scss +228 -0
- package/src/components/animated-button/animated-button.test.ts +143 -0
- package/src/components/animated-button/index.ts +12 -0
- package/src/components/button/button.component.ts +5 -0
- package/src/components/button-group/button-group.component.ts +25 -13
- package/src/components/button-group/button-group.scss +6 -0
- package/src/components/checkbox/checkbox.component.ts +4 -0
- package/src/components/checkbox/checkbox.scss +7 -4
- package/src/components/checkbox-group/checkbox-group.scss +7 -0
- package/src/components/chip/chip.component.ts +7 -2
- package/src/components/chip/chip.scss +16 -2
- package/src/components/collapsible/collapsible.component.ts +27 -20
- package/src/components/cols/cols.component.ts +14 -2
- package/src/components/cols/cols.scss +6 -12
- package/src/components/confirm/confirm.component.ts +7 -0
- package/src/components/copy-button/copy-button.component.ts +12 -11
- package/src/components/data-select/data-select.component.ts +215 -44
- package/src/components/data-select/data-select.scss +66 -6
- package/src/components/data-select/providers/currency-data-provider.ts +20 -6
- package/src/components/data-select/providers/provider.ts +1 -0
- package/src/components/data-select/providers/us-state-data-provider.ts +68 -0
- package/src/components/data-table/data-table.component.ts +81 -22
- package/src/components/data-table-search/data-table-search.component.ts +184 -0
- package/src/components/data-table-search/data-table-search.scss +17 -0
- package/src/components/data-table-search/data-table-search.test.ts +11 -0
- package/src/components/data-table-search/index.ts +12 -0
- package/src/components/datepicker/datepicker.component.ts +347 -13
- package/src/components/dialog/dialog.component.ts +1 -1
- package/src/components/empty-state/empty-state.scss +6 -1
- package/src/components/header/header.scss +3 -3
- package/src/components/icon/icon.component.ts +23 -1
- package/src/components/icon/icon.scss +23 -0
- package/src/components/icon-picker/brand-icons.ts +24 -0
- package/src/components/icon-picker/icon-picker.component.ts +407 -0
- package/src/components/icon-picker/icon-picker.scss +205 -0
- package/src/components/icon-picker/index.ts +12 -0
- package/src/components/icon-picker/line-icons.ts +612 -0
- package/src/components/icon-picker/material-icons.ts +14543 -0
- package/src/components/inline-edit/inline-edit.component.ts +190 -46
- package/src/components/inline-edit/inline-edit.scss +29 -0
- package/src/components/inline-edit/inline-edit.test.ts +314 -1
- package/src/components/input/input.component.ts +324 -5
- package/src/components/input/input.scss +87 -0
- package/src/components/input-group/input-group.component.ts +28 -7
- package/src/components/input-group/input-group.scss +48 -10
- package/src/components/item/item.component.ts +24 -3
- package/src/components/item/item.scss +18 -0
- package/src/components/key/index.ts +12 -0
- package/src/components/key/key.component.ts +47 -0
- package/src/components/key/key.scss +38 -0
- package/src/components/key-container/index.ts +12 -0
- package/src/components/key-container/key-container.component.ts +142 -0
- package/src/components/key-container/key-container.scss +33 -0
- package/src/components/menu-item/menu-item.component.ts +20 -0
- package/src/components/menu-item/menu-item.scss +69 -2
- package/src/components/navbar/navbar.component.ts +103 -11
- package/src/components/navbar/navbar.scss +6 -1
- package/src/components/opt-group/index.ts +12 -0
- package/src/components/opt-group/opt-group.component.ts +74 -0
- package/src/components/opt-group/opt-group.scss +37 -0
- package/src/components/opt-group/opt-group.test.ts +27 -0
- package/src/components/option/option.component.ts +1 -16
- package/src/components/option/option.scss +4 -0
- package/src/components/panel/panel.component.ts +10 -8
- package/src/components/panel/panel.scss +20 -0
- package/src/components/priority-list/index.ts +12 -0
- package/src/components/priority-list/priority-list.component.ts +611 -0
- package/src/components/priority-list/priority-list.scss +185 -0
- package/src/components/priority-list/priority-list.test.ts +139 -0
- package/src/components/progress-bar/progress-bar.component.ts +22 -16
- package/src/components/radio/radio.scss +10 -3
- package/src/components/select/select.component.ts +812 -72
- package/src/components/select/select.scss +108 -2
- package/src/components/select/select.test.ts +147 -2
- package/src/components/settings-container/settings-container.component.ts +44 -19
- package/src/components/settings-container/settings-container.scss +1 -1
- package/src/components/skeleton/skeleton.component.ts +6 -1
- package/src/components/slideout/slideout.scss +8 -3
- package/src/components/sp/sp.component.ts +3 -1
- package/src/components/sp/sp.scss +19 -0
- package/src/components/stepper/stepper.component.ts +16 -10
- package/src/components/textarea/textarea.scss +0 -4
- package/src/components/tile/tile.component.ts +12 -3
- package/src/components/tile/tile.scss +1 -1
- package/src/components/timer/timer.component.ts +15 -2
- package/src/components/toggle/toggle.component.ts +29 -26
- package/src/components/toggle/toggle.scss +13 -1
- package/src/components/translation-group/index.ts +12 -0
- package/src/components/translation-group/translation-group.component.ts +207 -0
- package/src/components/translation-group/translation-group.scss +8 -0
- package/src/components/translations/translations.component.ts +90 -44
- package/src/components/translations/translations.scss +4 -0
- package/src/components/vertical-stepper/vertical-stepper.component.ts +14 -9
- package/src/events/events.ts +12 -7
- package/src/events/zn-language-change.ts +7 -0
- package/src/events/zn-purchase.ts +11 -0
- package/src/events/zn-redirect.ts +7 -0
- package/src/events/zn-reorder.ts +7 -0
- package/src/events/zn-search-change.ts +11 -0
- package/src/events/zn-submit.ts +1 -1
- package/src/form-control.scss +4 -0
- package/src/internal/form-navigation.ts +510 -0
- package/src/internal/form.ts +6 -0
- package/src/zinc.ts +91 -83
package/src/events/zn-submit.ts
CHANGED
package/src/form-control.scss
CHANGED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import {formCollections} from "./form";
|
|
2
|
+
import {getTabbableElements} from "./tabbable";
|
|
3
|
+
import type {ZincFormControl} from "./zinc-element";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* WeakMap of navigation controllers to keep one controller per form
|
|
7
|
+
*/
|
|
8
|
+
const formNavigationControllers: WeakMap<HTMLFormElement, FormNavigationController> = new WeakMap();
|
|
9
|
+
|
|
10
|
+
export class FormNavigationController {
|
|
11
|
+
private readonly form: HTMLFormElement;
|
|
12
|
+
|
|
13
|
+
constructor(form: HTMLFormElement) {
|
|
14
|
+
this.form = form;
|
|
15
|
+
this.form.addEventListener('keydown', this.handleKeyDown, true);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private handleKeyDown = (event: KeyboardEvent) => {
|
|
19
|
+
if (event.key === 'Enter') {
|
|
20
|
+
const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
21
|
+
if (hasModifier) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (event.isComposing) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (event.defaultPrevented) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const target = event.target as HTMLElement;
|
|
34
|
+
if (this.isSelectOption(target)) {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
const selectElement = this.findParentSelect(target);
|
|
38
|
+
if (selectElement) {
|
|
39
|
+
this.focusNextControl(selectElement);
|
|
40
|
+
}
|
|
41
|
+
}, 0);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (this.shouldSkipEnterKey(target)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check if we're in a group (radio-group or checkbox-group)
|
|
50
|
+
const group = this.findParentGroup(target);
|
|
51
|
+
if (group) {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
const moved = this.focusNextInGroup(target, group);
|
|
54
|
+
// If we couldn't move within the group (we're at the last item)
|
|
55
|
+
if (!moved) {
|
|
56
|
+
this.focusNextControl(group);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
|
|
63
|
+
if (this.areRequiredFieldsFilled()) {
|
|
64
|
+
this.submitForm();
|
|
65
|
+
} else {
|
|
66
|
+
this.focusNextControl(target);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (event.key === 'ArrowDown') {
|
|
72
|
+
const target = event.target as HTMLElement;
|
|
73
|
+
const tagName = target.tagName;
|
|
74
|
+
const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
75
|
+
|
|
76
|
+
if (!hasModifier && !event.defaultPrevented) {
|
|
77
|
+
const group = this.findParentGroup(target);
|
|
78
|
+
if (group) {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
const moved = this.focusNextInGroup(target, group);
|
|
81
|
+
if (!moved) {
|
|
82
|
+
this.focusNextControl(group);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Individual checkboxes and toggles (not in a group)
|
|
88
|
+
if (tagName === 'ZN-CHECKBOX' || tagName === 'ZN-TOGGLE') {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
this.focusNextControl(target);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (event.key === 'ArrowUp') {
|
|
96
|
+
const target = event.target as HTMLElement;
|
|
97
|
+
const tagName = target.tagName;
|
|
98
|
+
const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
99
|
+
|
|
100
|
+
if (!hasModifier && !event.defaultPrevented) {
|
|
101
|
+
const group = this.findParentGroup(target);
|
|
102
|
+
if (group) {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
const moved = this.focusPreviousInGroup(target, group);
|
|
105
|
+
if (!moved) {
|
|
106
|
+
this.focusPreviousControl(group);
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (tagName === 'ZN-CHECKBOX' || tagName === 'ZN-TOGGLE') {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
this.focusPreviousControl(target);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
private shouldSkipEnterKey(element: HTMLElement): boolean {
|
|
120
|
+
const tagName = element.tagName.toLowerCase();
|
|
121
|
+
|
|
122
|
+
if (tagName === 'zn-textarea' || tagName === 'textarea') {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (tagName === 'zn-input' || tagName === 'input') {
|
|
127
|
+
const type = element.getAttribute('type');
|
|
128
|
+
if (type === 'search') {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return tagName === 'button' || tagName === 'zn-button';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private shouldExcludeFromNavigation(element: HTMLElement): boolean {
|
|
137
|
+
const tagName = element.tagName.toLowerCase();
|
|
138
|
+
return tagName === 'button' || tagName === 'zn-button';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private isSelectOption(element: HTMLElement): boolean {
|
|
142
|
+
const tagName = element.tagName.toLowerCase();
|
|
143
|
+
return tagName === 'zn-option';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private findParentSelect(element: HTMLElement): HTMLElement | null {
|
|
147
|
+
let current: HTMLElement | null = element;
|
|
148
|
+
while (current) {
|
|
149
|
+
if (current.tagName.toLowerCase() === 'zn-select') {
|
|
150
|
+
return current;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const rootNode = current.getRootNode();
|
|
154
|
+
if (rootNode instanceof ShadowRoot) {
|
|
155
|
+
current = rootNode.host as HTMLElement;
|
|
156
|
+
} else if (current.parentElement) {
|
|
157
|
+
current = current.parentElement;
|
|
158
|
+
} else {
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private findParentGroup(element: HTMLElement): HTMLElement | null {
|
|
166
|
+
let current: HTMLElement | null = element;
|
|
167
|
+
while (current) {
|
|
168
|
+
const tagName = current.tagName;
|
|
169
|
+
if (tagName === 'ZN-RADIO-GROUP' || tagName === 'ZN-CHECKBOX-GROUP') {
|
|
170
|
+
return current;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const rootNode = current.getRootNode();
|
|
174
|
+
if (rootNode instanceof ShadowRoot) {
|
|
175
|
+
current = rootNode.host as HTMLElement;
|
|
176
|
+
} else if (current.parentElement) {
|
|
177
|
+
current = current.parentElement;
|
|
178
|
+
} else {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private getGroupItems(group: HTMLElement): HTMLElement[] {
|
|
186
|
+
const itemTagName = group.tagName === 'ZN-RADIO-GROUP' ? 'ZN-RADIO' : 'ZN-CHECKBOX';
|
|
187
|
+
const items: HTMLElement[] = [];
|
|
188
|
+
const walker = document.createTreeWalker(
|
|
189
|
+
group,
|
|
190
|
+
NodeFilter.SHOW_ELEMENT,
|
|
191
|
+
{
|
|
192
|
+
acceptNode: (node: Node) => {
|
|
193
|
+
const element = node as HTMLElement;
|
|
194
|
+
if (element.tagName === itemTagName) {
|
|
195
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
196
|
+
}
|
|
197
|
+
return NodeFilter.FILTER_SKIP;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
let node: Node | null;
|
|
203
|
+
while ((node = walker.nextNode())) {
|
|
204
|
+
items.push(node as HTMLElement);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return items;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private focusNextInGroup(currentElement: HTMLElement, group: HTMLElement): boolean {
|
|
211
|
+
const items = this.getGroupItems(group);
|
|
212
|
+
if (items.length === 0) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let currentIndex = -1;
|
|
217
|
+
for (let i = 0; i < items.length; i++) {
|
|
218
|
+
if (items[i] === currentElement || items[i].contains(currentElement)) {
|
|
219
|
+
currentIndex = i;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (currentIndex >= 0 && currentIndex < items.length - 1) {
|
|
225
|
+
const nextItem = items[currentIndex + 1];
|
|
226
|
+
if (typeof nextItem.focus === 'function') {
|
|
227
|
+
nextItem.focus();
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private focusPreviousInGroup(currentElement: HTMLElement, group: HTMLElement): boolean {
|
|
236
|
+
const items = this.getGroupItems(group);
|
|
237
|
+
if (items.length === 0) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let currentIndex = -1;
|
|
242
|
+
for (let i = 0; i < items.length; i++) {
|
|
243
|
+
if (items[i] === currentElement || items[i].contains(currentElement)) {
|
|
244
|
+
currentIndex = i;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (currentIndex > 0) {
|
|
250
|
+
const previousItem = items[currentIndex - 1];
|
|
251
|
+
if (typeof previousItem.focus === 'function') {
|
|
252
|
+
previousItem.focus();
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private areRequiredFieldsFilled(): boolean {
|
|
261
|
+
const controls = formCollections.get(this.form);
|
|
262
|
+
|
|
263
|
+
if (!controls) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const control of controls) {
|
|
268
|
+
const formControl = control as ZincFormControl;
|
|
269
|
+
|
|
270
|
+
if (!formControl.name) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (formControl.disabled) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const value = formControl.value;
|
|
279
|
+
const hasValue = value !== undefined && value !== null && value !== '';
|
|
280
|
+
if (!hasValue) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (typeof formControl.checkValidity === 'function' && !formControl.checkValidity()) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private getNavigableControls(): HTMLElement[] {
|
|
293
|
+
const tabbableElements = getTabbableElements(this.form);
|
|
294
|
+
const navigableControls: HTMLElement[] = [];
|
|
295
|
+
const seen = new Set<HTMLElement>();
|
|
296
|
+
|
|
297
|
+
for (const el of tabbableElements) {
|
|
298
|
+
const tagName = el.tagName.toLowerCase();
|
|
299
|
+
|
|
300
|
+
const isZincControl = [
|
|
301
|
+
'zn-input',
|
|
302
|
+
'zn-select',
|
|
303
|
+
'zn-datepicker',
|
|
304
|
+
'zn-textarea',
|
|
305
|
+
'zn-file',
|
|
306
|
+
'zn-checkbox',
|
|
307
|
+
'zn-radio',
|
|
308
|
+
'zn-radio-group',
|
|
309
|
+
'zn-toggle'
|
|
310
|
+
].includes(tagName);
|
|
311
|
+
|
|
312
|
+
if (isZincControl) {
|
|
313
|
+
// Special handling for radios inside radio groups
|
|
314
|
+
if (tagName === 'zn-radio') {
|
|
315
|
+
// Check if this radio is inside a radio-group
|
|
316
|
+
const radioGroup = el.closest('zn-radio-group') as HTMLElement | null;
|
|
317
|
+
if (radioGroup) {
|
|
318
|
+
// Add the radio-group instead of the individual radio
|
|
319
|
+
if (!seen.has(radioGroup)) {
|
|
320
|
+
seen.add(radioGroup);
|
|
321
|
+
if (!this.shouldExcludeFromNavigation(radioGroup)) {
|
|
322
|
+
navigableControls.push(radioGroup);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (seen.has(el)) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
seen.add(el);
|
|
333
|
+
|
|
334
|
+
if (this.shouldExcludeFromNavigation(el)) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
navigableControls.push(el);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const hostElement = (el.getRootNode() as ShadowRoot).host as HTMLElement;
|
|
343
|
+
if (hostElement && hostElement !== this.form) {
|
|
344
|
+
const hostTagName = hostElement.tagName.toLowerCase();
|
|
345
|
+
const isHostZincControl = [
|
|
346
|
+
'zn-input',
|
|
347
|
+
'zn-select',
|
|
348
|
+
'zn-datepicker',
|
|
349
|
+
'zn-textarea',
|
|
350
|
+
'zn-file',
|
|
351
|
+
'zn-checkbox',
|
|
352
|
+
'zn-radio',
|
|
353
|
+
'zn-radio-group',
|
|
354
|
+
'zn-toggle'
|
|
355
|
+
].includes(hostTagName);
|
|
356
|
+
|
|
357
|
+
if (isHostZincControl) {
|
|
358
|
+
// Special handling for radios inside radio groups
|
|
359
|
+
if (hostTagName === 'zn-radio') {
|
|
360
|
+
// Check if this radio is inside a radio-group
|
|
361
|
+
const radioGroup = hostElement.closest('zn-radio-group') as HTMLElement | null;
|
|
362
|
+
if (radioGroup) {
|
|
363
|
+
// Add the radio-group instead of the individual radio
|
|
364
|
+
if (!seen.has(radioGroup)) {
|
|
365
|
+
seen.add(radioGroup);
|
|
366
|
+
if (!this.shouldExcludeFromNavigation(radioGroup)) {
|
|
367
|
+
navigableControls.push(radioGroup);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (seen.has(hostElement)) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
seen.add(hostElement);
|
|
378
|
+
|
|
379
|
+
if (this.shouldExcludeFromNavigation(hostElement)) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
navigableControls.push(hostElement);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const isNativeControl = ['input', 'select'].includes(tagName);
|
|
389
|
+
if (isNativeControl) {
|
|
390
|
+
if (seen.has(el)) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
seen.add(el);
|
|
394
|
+
|
|
395
|
+
const type = el.getAttribute('type');
|
|
396
|
+
if (type === 'search') {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
navigableControls.push(el);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return navigableControls;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private findCurrentControlIndex(currentElement: HTMLElement, navigableControls: HTMLElement[]): number {
|
|
408
|
+
for (let i = 0; i < navigableControls.length; i++) {
|
|
409
|
+
const control = navigableControls[i];
|
|
410
|
+
if (control === currentElement) {
|
|
411
|
+
return i;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const rootNode = currentElement.getRootNode();
|
|
415
|
+
if (rootNode instanceof ShadowRoot && rootNode.host === control) {
|
|
416
|
+
return i;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (control.contains(currentElement)) {
|
|
420
|
+
return i;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (currentElement.contains(control)) {
|
|
424
|
+
return i;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return -1;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private focusNextControl(currentElement: HTMLElement) {
|
|
432
|
+
const navigableControls = this.getNavigableControls();
|
|
433
|
+
const currentIndex = this.findCurrentControlIndex(currentElement, navigableControls);
|
|
434
|
+
|
|
435
|
+
if (currentIndex >= 0 && currentIndex < navigableControls.length - 1) {
|
|
436
|
+
const nextControl = navigableControls[currentIndex + 1];
|
|
437
|
+
|
|
438
|
+
if (typeof nextControl.focus === 'function') {
|
|
439
|
+
nextControl.focus();
|
|
440
|
+
|
|
441
|
+
const tagName = nextControl.tagName.toLowerCase();
|
|
442
|
+
if (tagName === 'zn-select') {
|
|
443
|
+
const selectElement = nextControl as HTMLElement & { show?: () => Promise<void> };
|
|
444
|
+
if (typeof selectElement.show === 'function') {
|
|
445
|
+
setTimeout(() => {
|
|
446
|
+
void selectElement.show?.();
|
|
447
|
+
}, 0);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private focusPreviousControl(currentElement: HTMLElement) {
|
|
455
|
+
const navigableControls = this.getNavigableControls();
|
|
456
|
+
const currentIndex = this.findCurrentControlIndex(currentElement, navigableControls);
|
|
457
|
+
|
|
458
|
+
if (currentIndex > 0) {
|
|
459
|
+
const previousControl = navigableControls[currentIndex - 1];
|
|
460
|
+
|
|
461
|
+
if (typeof previousControl.focus === 'function') {
|
|
462
|
+
previousControl.focus();
|
|
463
|
+
|
|
464
|
+
const tagName = previousControl.tagName.toLowerCase();
|
|
465
|
+
if (tagName === 'zn-select') {
|
|
466
|
+
const selectElement = previousControl as HTMLElement & { show?: () => Promise<void> };
|
|
467
|
+
if (typeof selectElement.show === 'function') {
|
|
468
|
+
setTimeout(() => {
|
|
469
|
+
void selectElement.show?.();
|
|
470
|
+
}, 0);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private submitForm() {
|
|
478
|
+
const submitButton = this.form.querySelector<HTMLButtonElement>('[type="submit"]');
|
|
479
|
+
|
|
480
|
+
if (submitButton) {
|
|
481
|
+
submitButton.click();
|
|
482
|
+
} else if (typeof this.form.requestSubmit === 'function') {
|
|
483
|
+
this.form.requestSubmit();
|
|
484
|
+
} else {
|
|
485
|
+
const submitEvent = new Event('submit', {
|
|
486
|
+
bubbles: true,
|
|
487
|
+
cancelable: true
|
|
488
|
+
});
|
|
489
|
+
this.form.dispatchEvent(submitEvent);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
destroy() {
|
|
494
|
+
this.form.removeEventListener('keydown', this.handleKeyDown, true);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Gets or creates a FormNavigationController for the given form
|
|
500
|
+
*/
|
|
501
|
+
export function getFormNavigationController(form: HTMLFormElement): FormNavigationController {
|
|
502
|
+
let controller = formNavigationControllers.get(form);
|
|
503
|
+
|
|
504
|
+
if (!controller) {
|
|
505
|
+
controller = new FormNavigationController(form);
|
|
506
|
+
formNavigationControllers.set(form, controller);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return controller;
|
|
510
|
+
}
|
package/src/internal/form.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {getFormNavigationController} from "./form-navigation";
|
|
1
2
|
import type {ReactiveController, ReactiveControllerHost} from "lit";
|
|
2
3
|
import type {ZincFormControl} from "./zinc-element";
|
|
3
4
|
import type Button from "../components/button";
|
|
@@ -156,6 +157,11 @@ export class FormControlController implements ReactiveController {
|
|
|
156
157
|
formCollections.set(this.form, new Set<ZincFormControl>([this.host]));
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
// Initialize form navigation controller if the form has the data-enter-navigation attribute
|
|
161
|
+
if (this.form.hasAttribute('data-enter-navigation')) {
|
|
162
|
+
getFormNavigationController(this.form);
|
|
163
|
+
}
|
|
164
|
+
|
|
159
165
|
this.form.addEventListener('formdata', this.handleFormData);
|
|
160
166
|
this.form.addEventListener('submit', this.handleFormSubmit);
|
|
161
167
|
this.form.addEventListener('reset', this.handleFormReset);
|