Skip to content

Commit 2536a4c

Browse files
Merge pull request #397 from uvarov-frontend/fix/tab_key_for_inputMode
Fix inputMode true and TAB key and arrow navigation
2 parents b4673e8 + 2700d8f commit 2536a4c

File tree

5 files changed

+105
-0
lines changed

5 files changed

+105
-0
lines changed

package/src/scripts/handles/handleInput.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import createToInput from '@scripts/creators/createToInput';
22
import { show } from '@scripts/methods';
33
import canOpenOnFocus from '@scripts/utils/canOpenOnFocus';
44
import setContext from '@scripts/utils/setContext';
5+
import { clearSkipOpenOnFocus, shouldSkipOpenOnFocus } from '@scripts/utils/skipOpenOnFocus';
56
import type { Calendar } from '@src/index';
67

78
const handleInput = (self: Calendar) => {
@@ -20,6 +21,10 @@ const handleInput = (self: Calendar) => {
2021
const shouldHandleFocus = typeof self.openOnFocus === 'function' || self.openOnFocus === true;
2122

2223
const handleOpenOnFocus = () => {
24+
if (shouldSkipOpenOnFocus(self)) {
25+
clearSkipOpenOnFocus(self);
26+
return;
27+
}
2328
if (!canOpenOnFocus(self)) return;
2429
handleOpenCalendar();
2530
};
@@ -28,12 +33,46 @@ const handleInput = (self: Calendar) => {
2833
(self.context.inputElement as HTMLInputElement).addEventListener('focus', handleOpenOnFocus);
2934
}
3035

36+
const focusIntoCalendar = (event: KeyboardEvent) => {
37+
if (!self.context.isShowInInputMode) return false;
38+
if (document.activeElement !== self.context.inputElement) return false;
39+
40+
const isFocusable = (el: HTMLElement) => el.tabIndex >= 0 && !el.hasAttribute('disabled') && el.getAttribute('aria-disabled') !== 'true';
41+
42+
const walker = document.createTreeWalker(self.context.mainElement, NodeFilter.SHOW_ELEMENT, {
43+
acceptNode: (node) => {
44+
const el = node as HTMLElement;
45+
if (!isFocusable(el)) return NodeFilter.FILTER_SKIP;
46+
return NodeFilter.FILTER_ACCEPT;
47+
},
48+
});
49+
50+
const focusTarget = (walker.nextNode() as HTMLElement | null) ?? (isFocusable(self.context.mainElement) ? self.context.mainElement : null);
51+
52+
if (!focusTarget || focusTarget.tabIndex < 0) return false;
53+
54+
event.preventDefault();
55+
focusTarget.focus();
56+
return true;
57+
};
58+
59+
const handleKeyIntoCalendar = (event: KeyboardEvent) => {
60+
const isTab = event.key === 'Tab' && !event.shiftKey;
61+
const isArrow = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key);
62+
if (!isTab && !isArrow) return;
63+
focusIntoCalendar(event);
64+
};
65+
66+
(self.context.inputElement as HTMLInputElement).addEventListener('keydown', handleKeyIntoCalendar);
67+
3168
return () => {
3269
(self.context.inputElement as HTMLInputElement).removeEventListener('click', handleOpenCalendar);
3370

3471
if (shouldHandleFocus) {
3572
(self.context.inputElement as HTMLInputElement).removeEventListener('focus', handleOpenOnFocus);
3673
}
74+
75+
(self.context.inputElement as HTMLInputElement).removeEventListener('keydown', handleKeyIntoCalendar);
3776
};
3877
};
3978

package/src/scripts/methods/hide.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
import setContext from '@scripts/utils/setContext';
2+
import { setSkipOpenOnFocus } from '@scripts/utils/skipOpenOnFocus';
3+
import { disableTabbing } from '@scripts/utils/toggleTabbing';
24
import type { Calendar } from '@src/index';
35

46
const hide = (self: Calendar) => {
57
if (!self.context.isShowInInputMode || !self.context.currentType) return;
68

79
self.context.mainElement.dataset.vcCalendarHidden = '';
810
setContext(self, 'isShowInInputMode', false);
11+
if (self.inputMode) disableTabbing(self.context.mainElement);
912

1013
if (self.context.cleanupHandlers[0]) {
1114
self.context.cleanupHandlers.forEach((cleanup) => cleanup());
1215
setContext(self, 'cleanupHandlers', []);
1316
}
1417

18+
if (self.inputMode && self.context.inputElement && self.context.mainElement.contains(document.activeElement)) {
19+
const shouldHandleFocus = typeof self.openOnFocus === 'function' || self.openOnFocus === true;
20+
if (shouldHandleFocus) setSkipOpenOnFocus(self);
21+
self.context.inputElement.focus();
22+
}
23+
1524
if (self.onHide) self.onHide(self);
1625
};
1726

package/src/scripts/methods/show.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import hide from '@scripts/methods/hide';
22
import setPosition from '@scripts/utils/positions/setPosition';
33
import setContext from '@scripts/utils/setContext';
4+
import { restoreTabbing } from '@scripts/utils/toggleTabbing';
45
import type { Calendar } from '@src/index';
56

67
const show = (self: Calendar) => {
@@ -13,6 +14,7 @@ const show = (self: Calendar) => {
1314

1415
setContext(self, 'cleanupHandlers', []);
1516
setContext(self, 'isShowInInputMode', true);
17+
if (self.inputMode) restoreTabbing(self.context.mainElement);
1618
setPosition(self.context.inputElement, self.context.mainElement, self.positionToInput);
1719
self.context.mainElement.removeAttribute('data-vc-calendar-hidden');
1820

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { Calendar } from '@src/index';
2+
3+
const skipOpenOnFocus = new WeakSet<Calendar>();
4+
5+
export const shouldSkipOpenOnFocus = (self: Calendar) => skipOpenOnFocus.has(self);
6+
7+
export const setSkipOpenOnFocus = (self: Calendar) => {
8+
skipOpenOnFocus.add(self);
9+
};
10+
11+
export const clearSkipOpenOnFocus = (self: Calendar) => {
12+
skipOpenOnFocus.delete(self);
13+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const PREV_TABINDEX_ATTR = 'data-vc-prev-tabindex';
2+
3+
const isFocusable = (el: HTMLElement) => el.tabIndex >= 0 && !el.hasAttribute('disabled') && el.getAttribute('aria-disabled') !== 'true';
4+
5+
const storePrevTabIndex = (el: HTMLElement) => {
6+
if (el.hasAttribute(PREV_TABINDEX_ATTR)) return;
7+
const prev = el.getAttribute('tabindex');
8+
el.setAttribute(PREV_TABINDEX_ATTR, prev ?? '');
9+
};
10+
11+
const restorePrevTabIndex = (el: HTMLElement) => {
12+
if (!el.hasAttribute(PREV_TABINDEX_ATTR)) return;
13+
const prev = el.getAttribute(PREV_TABINDEX_ATTR);
14+
if (prev === '' || prev === null) {
15+
el.removeAttribute('tabindex');
16+
} else {
17+
el.setAttribute('tabindex', prev);
18+
}
19+
el.removeAttribute(PREV_TABINDEX_ATTR);
20+
};
21+
22+
export const disableTabbing = (root: HTMLElement) => {
23+
if (isFocusable(root)) {
24+
storePrevTabIndex(root);
25+
root.tabIndex = -1;
26+
}
27+
28+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
29+
acceptNode: (node) => (isFocusable(node as HTMLElement) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP),
30+
});
31+
32+
while (walker.nextNode()) {
33+
const el = walker.currentNode as HTMLElement;
34+
storePrevTabIndex(el);
35+
el.tabIndex = -1;
36+
}
37+
};
38+
39+
export const restoreTabbing = (root: HTMLElement) => {
40+
restorePrevTabIndex(root);
41+
root.querySelectorAll<HTMLElement>(`[${PREV_TABINDEX_ATTR}]`).forEach(restorePrevTabIndex);
42+
};

0 commit comments

Comments
 (0)