@via-profit/ui-kit 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,10 +44,9 @@ const findScrollableAncestor = (element, includeHidden = false) => {
44
44
  return window;
45
45
  };
46
46
  exports.findScrollableAncestor = findScrollableAncestor;
47
+ const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? react_1.default.useLayoutEffect : react_1.default.useEffect;
47
48
  const usePopper = (props) => {
48
49
  const { anchorElement, anchorPos = 'auto', positionStrategy = 'fixed', autoFlip = false, offset = 0, viewportMargin = 10, isOpen = false, alternativePlacements, } = props;
49
- const isMountedRef = react_1.default.useRef(false);
50
- const isPositioningRef = react_1.default.useRef(false);
51
50
  const [actualPlacement, setActualPlacement] = react_1.default.useState(anchorPos);
52
51
  const [style, setStyle] = react_1.default.useState(null);
53
52
  const [isVisible, setIsVisible] = react_1.default.useState(false);
@@ -59,12 +58,15 @@ const usePopper = (props) => {
59
58
  }
60
59
  return (0, exports.findScrollableAncestor)(anchorElement);
61
60
  }, [anchorElement]);
62
- // Synchronizing actualPlacement with anchorPos
61
+ // Reset actualPlacement only when the preferred placement itself changes.
62
+ // Comparing with actualPlacement here would immediately revert every flip.
63
+ const prevAnchorPosRef = react_1.default.useRef(anchorPos);
63
64
  react_1.default.useEffect(() => {
64
- if (actualPlacement !== anchorPos) {
65
+ if (prevAnchorPosRef.current !== anchorPos) {
66
+ prevAnchorPosRef.current = anchorPos;
65
67
  setActualPlacement(anchorPos);
66
68
  }
67
- }, [actualPlacement, anchorPos]);
69
+ }, [anchorPos]);
68
70
  const getPositionStyle = react_1.default.useCallback((props) => {
69
71
  var _a;
70
72
  const { place, anchorRect, popperRect, forViewportCheck = false } = props;
@@ -383,25 +385,56 @@ const usePopper = (props) => {
383
385
  }
384
386
  const anchorRect = anchorElement.getBoundingClientRect();
385
387
  const popperRect = popperRef.current.getBoundingClientRect();
386
- const { style, placement, found } = getPreferredPlacements(anchorPos, anchorRect, popperRect);
387
- if (found) {
388
- setActualPlacement(placement);
389
- }
388
+ const { style, placement } = getPreferredPlacements(anchorPos, anchorRect, popperRect);
389
+ setActualPlacement(placement);
390
390
  setStyle(style);
391
391
  }, [anchorElement, anchorPos, getPreferredPlacements]);
392
+ const frameRef = react_1.default.useRef(null);
393
+ const scheduleUpdate = react_1.default.useCallback(() => {
394
+ if (frameRef.current !== null) {
395
+ return;
396
+ }
397
+ frameRef.current = window.requestAnimationFrame(() => {
398
+ frameRef.current = null;
399
+ calculatePosition();
400
+ });
401
+ }, [calculatePosition]);
402
+ // Cancel the pending frame on unmount
403
+ react_1.default.useEffect(() => () => {
404
+ if (frameRef.current !== null) {
405
+ window.cancelAnimationFrame(frameRef.current);
406
+ frameRef.current = null;
407
+ }
408
+ }, []);
392
409
  /**
393
- * Open / Close popper logic
410
+ * Close popper logic
394
411
  */
395
412
  react_1.default.useEffect(() => {
396
413
  if (!isOpen) {
397
414
  setIsVisible(false);
398
415
  setStyle(null);
399
416
  }
400
- else if (anchorElement && isOpen && popperRef.current) {
417
+ }, [isOpen]);
418
+ /**
419
+ * Open popper logic.
420
+ * Runs after every render because the popper node can appear later than `isOpen`
421
+ * becomes true (e.g. the portal is rendered only after the client mount)
422
+ */
423
+ useIsomorphicLayoutEffect(() => {
424
+ if (isOpen && !isVisible && anchorElement && popperRef.current) {
401
425
  calculatePosition();
402
426
  setIsVisible(true);
403
427
  }
404
- }, [anchorElement, getPreferredPlacements, isOpen, calculatePosition]);
428
+ });
429
+ /**
430
+ * Recalculate position when the positioning params are changed
431
+ */
432
+ react_1.default.useEffect(() => {
433
+ if (isOpen && isVisible) {
434
+ calculatePosition();
435
+ }
436
+ // eslint-disable-next-line react-hooks/exhaustive-deps
437
+ }, [calculatePosition]);
405
438
  /**
406
439
  * The anchor element changes
407
440
  */
@@ -409,12 +442,7 @@ const usePopper = (props) => {
409
442
  if (!isOpen || !anchorElement) {
410
443
  return;
411
444
  }
412
- const observer = new MutationObserver(() => {
413
- window.requestAnimationFrame(() => {
414
- // console.log('The anchor element was change');
415
- calculatePosition();
416
- });
417
- });
445
+ const observer = new MutationObserver(scheduleUpdate);
418
446
  observer.observe(anchorElement, {
419
447
  attributes: true,
420
448
  childList: true,
@@ -424,36 +452,47 @@ const usePopper = (props) => {
424
452
  return () => {
425
453
  observer.disconnect();
426
454
  };
427
- }, [isOpen, anchorElement, calculatePosition]);
455
+ }, [isOpen, anchorElement, scheduleUpdate]);
428
456
  /**
429
- * On scroll - recalculate position
457
+ * The size of the anchor or popper changes
430
458
  */
431
459
  react_1.default.useEffect(() => {
432
- if (!isOpen || !scrollableAncestor) {
460
+ if (!isOpen || !isVisible || typeof ResizeObserver === 'undefined') {
433
461
  return;
434
462
  }
435
- const handleScroll = () => {
436
- window.requestAnimationFrame(() => {
437
- calculatePosition();
438
- });
439
- };
440
- // parent scroll listener
441
- if (scrollableAncestor instanceof HTMLElement) {
442
- scrollableAncestor.addEventListener('scroll', handleScroll, { passive: true });
463
+ const observer = new ResizeObserver(scheduleUpdate);
464
+ if (anchorElement) {
465
+ observer.observe(anchorElement);
443
466
  }
444
- else if (scrollableAncestor === window) {
445
- window.addEventListener('scroll', handleScroll, { passive: true });
467
+ if (popperRef.current) {
468
+ observer.observe(popperRef.current);
446
469
  }
447
470
  // eslint-disable-next-line consistent-return
448
471
  return () => {
449
- if (scrollableAncestor instanceof HTMLElement) {
450
- scrollableAncestor.removeEventListener('scroll', handleScroll);
451
- }
452
- else if (scrollableAncestor === window) {
453
- window.removeEventListener('scroll', handleScroll);
472
+ observer.disconnect();
473
+ };
474
+ }, [isOpen, isVisible, anchorElement, scheduleUpdate]);
475
+ /**
476
+ * On scroll of any ancestor (captured on window) - recalculate position
477
+ */
478
+ react_1.default.useEffect(() => {
479
+ if (!isOpen) {
480
+ return;
481
+ }
482
+ const handleScroll = (event) => {
483
+ var _a;
484
+ // Scrolling inside the popper does not move it
485
+ if (event.target instanceof Node && ((_a = popperRef.current) === null || _a === void 0 ? void 0 : _a.contains(event.target))) {
486
+ return;
454
487
  }
488
+ scheduleUpdate();
489
+ };
490
+ window.addEventListener('scroll', handleScroll, { passive: true, capture: true });
491
+ // eslint-disable-next-line consistent-return
492
+ return () => {
493
+ window.removeEventListener('scroll', handleScroll, { capture: true });
455
494
  };
456
- }, [isOpen, scrollableAncestor, calculatePosition]);
495
+ }, [isOpen, scheduleUpdate]);
457
496
  /**
458
497
  * On window resize - recalculate position
459
498
  */
@@ -461,18 +500,12 @@ const usePopper = (props) => {
461
500
  if (!isOpen) {
462
501
  return;
463
502
  }
464
- const handleResize = () => {
465
- window.requestAnimationFrame(() => {
466
- calculatePosition();
467
- });
468
- };
469
- // parent scroll listener
470
- window.addEventListener('resize', handleResize);
503
+ window.addEventListener('resize', scheduleUpdate);
471
504
  // eslint-disable-next-line consistent-return
472
505
  return () => {
473
- window.removeEventListener('resize', handleResize);
506
+ window.removeEventListener('resize', scheduleUpdate);
474
507
  };
475
- }, [calculatePosition, isOpen]);
508
+ }, [scheduleUpdate, isOpen]);
476
509
  return {
477
510
  actualPlacement,
478
511
  style,
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { MenuProps, MenuRef } from '../Menu';
3
- import { OnChange } from 'Selectbox';
3
+ import type { OnChange } from './index';
4
4
  export interface SelectbosMenuProps<T, Multiple extends boolean | undefined = undefined> extends MenuProps<T, Multiple> {
5
5
  readonly onChange?: OnChange<T, Multiple>;
6
6
  }
@@ -60,31 +60,14 @@ const DEFAULT_OVERRIDES = {
60
60
  ButtonWrapper: SelectboxButtonWrapper_1.default,
61
61
  Container: SelectboxContainer_1.default,
62
62
  };
63
- const generateGuid = () => {
64
- const u = Date.now().toString(16) + Math.random().toString(16) + '0'.repeat(16);
65
- return [
66
- u.substring(0, 8),
67
- u.substring(8, 12),
68
- '4000-8' + u.substring(13, 16),
69
- u.substring(16, 28),
70
- ].join('-');
71
- };
72
63
  const Selectbox = React.forwardRef((props, ref) => {
73
- const { items, value, multiple, isOpen = false, isLoading = false, anchorPos = 'bottom-fill', fullWidth = false, children, onChange, selectedItemToString, getOptionSelected, onRequestOpen = () => undefined, onRequestClose = () => undefined, overrides, id, label, onFocus, requiredAsterisk, error, errorText, notSetLabel = 'Not set', autoFlip = true, positionStrategy } = props, nativeButtonProps = __rest(props, ["items", "value", "multiple", "isOpen", "isLoading", "anchorPos", "fullWidth", "children", "onChange", "selectedItemToString", "getOptionSelected", "onRequestOpen", "onRequestClose", "overrides", "id", "label", "onFocus", "requiredAsterisk", "error", "errorText", "notSetLabel", "autoFlip", "positionStrategy"]);
64
+ const { items, value, multiple, isOpen = false, isLoading = false, anchorPos = 'bottom-fill', fullWidth = false, children, onChange, selectedItemToString, getOptionSelected, onRequestOpen = () => undefined, onRequestClose = () => undefined, overrides, id, label, requiredAsterisk, error, errorText, notSetLabel = 'Not set', autoFlip = true, positionStrategy } = props, nativeButtonProps = __rest(props, ["items", "value", "multiple", "isOpen", "isLoading", "anchorPos", "fullWidth", "children", "onChange", "selectedItemToString", "getOptionSelected", "onRequestOpen", "onRequestClose", "overrides", "id", "label", "requiredAsterisk", "error", "errorText", "notSetLabel", "autoFlip", "positionStrategy"]);
74
65
  const menuRef = React.useRef(null);
75
66
  const [anchorElement, setAnchorElement] = React.useState(null);
76
67
  const overridesMap = React.useMemo(() => (Object.assign(Object.assign({}, DEFAULT_OVERRIDES), overrides)), [overrides]);
77
- const inputID = React.useMemo(() => {
78
- if (typeof id === 'string') {
79
- return id;
80
- }
81
- return generateGuid();
82
- }, [id]);
83
- React.useEffect(() => {
84
- if (isOpen && menuRef.current) {
85
- menuRef.current.scrollToFirstSelected();
86
- }
87
- }, [isOpen]);
68
+ // useId is stable between server and client renders
69
+ const generatedID = React.useId();
70
+ const inputID = typeof id === 'string' ? id : generatedID;
88
71
  const renderValueAsString = React.useCallback(() => {
89
72
  if ((!multiple && !value) || (multiple && value.length === 0)) {
90
73
  return notSetLabel;
@@ -128,11 +111,15 @@ const Selectbox = React.forwardRef((props, ref) => {
128
111
  }
129
112
  }, [onRequestClose, anchorElement]);
130
113
  React.useEffect(() => {
114
+ // A closed selectbox has nothing to close
115
+ if (!isOpen) {
116
+ return undefined;
117
+ }
131
118
  window.document.addEventListener(ClickOutside_1.mouseEventMap.onMouseDown, handleMouseDown);
132
119
  return () => {
133
120
  window.document.removeEventListener(ClickOutside_1.mouseEventMap.onMouseDown, handleMouseDown);
134
121
  };
135
- }, [handleMouseDown]);
122
+ }, [handleMouseDown, isOpen]);
136
123
  const buttonProps = React.useMemo(() => (Object.assign({ fullWidth,
137
124
  error,
138
125
  isOpen,
@@ -141,15 +128,12 @@ const Selectbox = React.forwardRef((props, ref) => {
141
128
  htmlFor: inputID,
142
129
  error,
143
130
  }), [inputID, error]);
144
- if (!(items === null || items === void 0 ? void 0 : items.length) && !isLoading) {
145
- return null;
146
- }
147
131
  return (React.createElement(overridesMap.Container, null,
148
132
  label != null && (React.createElement(overridesMap.Label, Object.assign({}, labelProps),
149
133
  label,
150
- requiredAsterisk != null && (React.createElement(overridesMap.Asterisk, null, typeof requiredAsterisk === 'boolean' ? '*' : requiredAsterisk)))),
134
+ requiredAsterisk != null && requiredAsterisk !== false && (React.createElement(overridesMap.Asterisk, null, typeof requiredAsterisk === 'boolean' ? '*' : requiredAsterisk)))),
151
135
  React.createElement(overridesMap.ButtonWrapper, { fullWidth: fullWidth, error: error, isOpen: isOpen },
152
- React.createElement(overridesMap.Button, Object.assign({}, buttonProps, { onClick: handleButtonClick, ref: setRefs }), renderValueAsString())),
136
+ React.createElement(overridesMap.Button, Object.assign({}, buttonProps, { id: inputID, onClick: handleButtonClick, ref: setRefs }), renderValueAsString())),
153
137
  React.createElement(overridesMap.ErrorText, { error: error }, errorText),
154
138
  React.createElement(SelectboxMenu_1.default, { multiple: multiple, items: items, value: value, isOpen: isOpen, anchorPos: anchorPos, positionStrategy: positionStrategy, autoFlip: autoFlip, alternativePlacements: ['bottom-fill', 'top-fill'], anchorElement: anchorElement, getOptionSelected: getOptionSelected, onChange: onChange, onRequestClose: onRequestClose, ref: menuRef }, children)));
155
139
  });
package/Swiper/Swiper.js CHANGED
@@ -106,7 +106,6 @@ exports.Swiper = React.forwardRef((props, ref) => {
106
106
  const isAnimating = React.useRef(false);
107
107
  const autoplayTimer = React.useRef();
108
108
  const mounted = React.useRef(true);
109
- const normalizeTimeout = React.useRef();
110
109
  // #region Real Index
111
110
  const realIndex = React.useMemo(() => {
112
111
  if (!infinite) {
@@ -167,7 +166,9 @@ exports.Swiper = React.forwardRef((props, ref) => {
167
166
  indexRef.current = index;
168
167
  }, [index]);
169
168
  const goToIndex = React.useCallback((i) => {
170
- if (isAnimating.current)
169
+ // In the infinite mode the index is normalized after the transition, so wait for it.
170
+ // In the regular mode the transition is simply retargeted
171
+ if (infinite && isAnimating.current)
171
172
  return;
172
173
  let targetIndex = i;
173
174
  if (infinite) {
@@ -278,11 +279,13 @@ exports.Swiper = React.forwardRef((props, ref) => {
278
279
  else {
279
280
  setIndex(nextIndex);
280
281
  }
281
- isAnimating.current = true;
282
+ // A click without movement does not start a transition, so transitionend never comes
283
+ // and the flag would block autoplay and goToIndex forever
284
+ isAnimating.current = nextIndex !== index || (snap && offset !== 0);
282
285
  dragging.current = false;
283
286
  setIsDragging(false);
284
287
  e.target.releasePointerCapture(e.pointerId);
285
- }, [threshold, dragThreshold, index, maxIndex, snap, infinite, total]);
288
+ }, [threshold, dragThreshold, index, maxIndex, snap, infinite, total, offset]);
286
289
  const onLostPointerCapture = React.useCallback((e) => {
287
290
  if (dragging.current) {
288
291
  onPointerUp(e);
@@ -347,8 +350,17 @@ exports.Swiper = React.forwardRef((props, ref) => {
347
350
  isAnimating.current = false;
348
351
  normalizeIndex(); // <-- вот здесь
349
352
  };
353
+ const handleTransitionCancel = (e) => {
354
+ if (e.propertyName !== 'transform')
355
+ return;
356
+ isAnimating.current = false;
357
+ };
350
358
  track.addEventListener('transitionend', handleTransitionEnd);
351
- return () => track.removeEventListener('transitionend', handleTransitionEnd);
359
+ track.addEventListener('transitioncancel', handleTransitionCancel);
360
+ return () => {
361
+ track.removeEventListener('transitionend', handleTransitionEnd);
362
+ track.removeEventListener('transitioncancel', handleTransitionCancel);
363
+ };
352
364
  }, [normalizeIndex]);
353
365
  // #region Mount
354
366
  React.useEffect(() => {
package/TextArea/index.js CHANGED
@@ -48,19 +48,9 @@ const TextAreaIconWrapper_1 = __importDefault(require("./TextAreaIconWrapper"));
48
48
  const TextArea = (props, ref) => {
49
49
  const { error, fullWidth, inputRef, label, id, errorText, className, style, endIcon, startIcon, requiredAsterisk, overrides, onChange, onFocus, onBlur } = props, nativeInputProps = __rest(props, ["error", "fullWidth", "inputRef", "label", "id", "errorText", "className", "style", "endIcon", "startIcon", "requiredAsterisk", "overrides", "onChange", "onFocus", "onBlur"]);
50
50
  const [focused, setFocused] = React.useState(false);
51
- const inputID = React.useMemo(() => {
52
- if (typeof id === 'string') {
53
- return id;
54
- }
55
- const u = Date.now().toString(16) + Math.random().toString(16) + '0'.repeat(16);
56
- const guid = [
57
- u.substring(0, 8),
58
- u.substring(8, 12),
59
- '4000-8' + u.substring(13, 16),
60
- u.substring(16, 28),
61
- ].join('-');
62
- return guid;
63
- }, [id]);
51
+ // useId is stable between server and client renders
52
+ const generatedID = React.useId();
53
+ const inputID = typeof id === 'string' ? id : generatedID;
64
54
  const inputChange = React.useCallback(event => {
65
55
  if (onChange) {
66
56
  onChange(event);
@@ -92,7 +82,9 @@ const TextArea = (props, ref) => {
92
82
  return (React.createElement(overridesMap.Container, { ref: ref, fullWidth: fullWidth, className: className, style: style, focused: focused },
93
83
  typeof label !== 'undefined' && label !== null && (React.createElement(overridesMap.Label, { htmlFor: inputID, error: error },
94
84
  label,
95
- typeof requiredAsterisk !== 'undefined' && requiredAsterisk !== null && (React.createElement(overridesMap.Asterisk, null, typeof requiredAsterisk === 'boolean' ? '*' : requiredAsterisk)))),
85
+ typeof requiredAsterisk !== 'undefined' &&
86
+ requiredAsterisk !== null &&
87
+ requiredAsterisk !== false && (React.createElement(overridesMap.Asterisk, null, typeof requiredAsterisk === 'boolean' ? '*' : requiredAsterisk)))),
96
88
  React.createElement(overridesMap.InputWrapper, { error: error, focused: focused, fullWidth: fullWidth },
97
89
  hasStartIcon && (React.createElement(overridesMap.IconWrapper, { position: "start" }, startIcon)),
98
90
  React.createElement(overridesMap.Input, Object.assign({}, nativeInputProps, { hasStartIcon: hasStartIcon, hasEndIcon: hasEndIcon, ref: inputRef, id: inputID, onChange: inputChange, onFocus: inputFocus, onBlur: inputBlur })),
@@ -67,26 +67,14 @@ const defailtOverrides = {
67
67
  const TextField = React.forwardRef((props, ref) => {
68
68
  const { error, fullWidth, inputRef, label, id, errorText, className, style, endIcon, startIcon, requiredAsterisk, overrides, onChange, onFocus, onBlur } = props, nativeInputProps = __rest(props, ["error", "fullWidth", "inputRef", "label", "id", "errorText", "className", "style", "endIcon", "startIcon", "requiredAsterisk", "overrides", "onChange", "onFocus", "onBlur"]);
69
69
  const [focused, setFocused] = React.useState(false);
70
- const inputID = React.useMemo(() => {
71
- if (typeof id === 'string') {
72
- return id;
73
- }
74
- const u = Date.now().toString(16) + Math.random().toString(16) + '0'.repeat(16);
75
- return [
76
- u.substring(0, 8),
77
- u.substring(8, 12),
78
- '4000-8' + u.substring(13, 16),
79
- u.substring(16, 28),
80
- ].join('-');
81
- }, [id]);
82
- const stableOnChange = React.useCallback(onChange ? onChange : () => undefined, [onChange]); // уже есть
83
- // const stableOnFocus = React.useCallback(onFocus, [onFocus]); // уже есть
84
- // const stableOnBlur = React.useCallback(onBlur, [onBlur]); // уже есть
70
+ // useId is stable between server and client renders
71
+ const generatedID = React.useId();
72
+ const inputID = typeof id === 'string' ? id : generatedID;
85
73
  const inputChange = React.useCallback(event => {
86
- if (stableOnChange) {
87
- stableOnChange(event);
74
+ if (typeof onChange === 'function') {
75
+ onChange(event);
88
76
  }
89
- }, [stableOnChange]);
77
+ }, [onChange]);
90
78
  const inputFocus = React.useCallback(event => {
91
79
  setFocused(true);
92
80
  if (typeof onFocus === 'function') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@via-profit/ui-kit",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "UI Kit",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,7 +25,7 @@
25
25
  * window.document.addEventListener('keydown', keydown);
26
26
  *
27
27
  * return () => {
28
- * window.document.addEventListener('keydown', keydown);
28
+ * window.document.removeEventListener('keydown', keydown);
29
29
  * }
30
30
  * }, []);
31
31
  *
@@ -51,6 +51,12 @@ declare class TabManager {
51
51
  registerContainer(container: HTMLElement): void;
52
52
  unregisterContainer(container: HTMLElement, revertBackFocus?: boolean): void;
53
53
  getElementsList(): HTMLElement[];
54
+ /**
55
+ * Index of the currently focused element in the list.
56
+ * Uses the real focus, so a focus set by mouse is taken into account
57
+ */
58
+ getCurrentIndex(list: readonly HTMLElement[]): number;
59
+ focusByIndex(list: readonly HTMLElement[], index: number): void;
54
60
  focusNext(): void;
55
61
  focusPrev(): void;
56
62
  }
@@ -39,7 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  * window.document.addEventListener('keydown', keydown);
40
40
  *
41
41
  * return () => {
42
- * window.document.addEventListener('keydown', keydown);
42
+ * window.document.removeEventListener('keydown', keydown);
43
43
  * }
44
44
  * }, []);
45
45
  *
@@ -78,7 +78,6 @@ class TabManager {
78
78
  __classPrivateFieldSet(this, _TabManager_stack, __classPrivateFieldGet(this, _TabManager_stack, "f").filter(stackElem => {
79
79
  const founded = stackElem.container === container;
80
80
  if (founded && revertBackFocus && stackElem.lastFocused instanceof HTMLElement) {
81
- console.debug('revert back focus to', stackElem.lastFocused);
82
81
  stackElem.lastFocused.focus();
83
82
  }
84
83
  return !founded;
@@ -86,60 +85,55 @@ class TabManager {
86
85
  }
87
86
  getElementsList() {
88
87
  const { stackElement } = this.getTopOfTheStack();
89
- const universe = stackElement.container.querySelectorAll('input, button, select, textarea, a[href]');
90
- const list = Array.prototype.filter.call(universe, function (item) {
91
- return item.tabIndex >= 0;
92
- });
93
- list.sort((a, b) => {
94
- if (a.tabIndex === 0) {
95
- return 1;
96
- }
97
- if (b.tabIndex === 0) {
98
- return -1;
99
- }
100
- if (a.tabIndex < b.tabIndex) {
101
- return -1;
102
- }
103
- if (a.tabIndex > b.tabIndex) {
104
- return 1;
105
- }
106
- return 0;
107
- });
88
+ if (!stackElement) {
89
+ return [];
90
+ }
91
+ const universe = stackElement.container.querySelectorAll('input, button, select, textarea, a[href], [tabindex]');
92
+ const list = Array.prototype.filter.call(universe, (item) => item.tabIndex >= 0 &&
93
+ !item.disabled &&
94
+ item !== stackElement.container);
95
+ // Positive tabIndex goes first (ascending), then tabIndex=0 in the DOM order.
96
+ // Array.prototype.sort is stable, so equal keys keep the DOM order
97
+ const sortKey = (el) => el.tabIndex === 0 ? Number.MAX_SAFE_INTEGER : el.tabIndex;
98
+ list.sort((a, b) => sortKey(a) - sortKey(b));
108
99
  return list;
109
100
  }
110
- focusNext() {
111
- const { stackElement, stackIndex } = this.getTopOfTheStack();
112
- const list = this.getElementsList();
113
- const nextIndex = stackElement.lastIndex + 1;
114
- if (list[nextIndex]) {
115
- list[nextIndex].focus();
116
- __classPrivateFieldGet(this, _TabManager_stack, "f")[stackIndex].lastIndex = nextIndex;
117
- return;
101
+ /**
102
+ * Index of the currently focused element in the list.
103
+ * Uses the real focus, so a focus set by mouse is taken into account
104
+ */
105
+ getCurrentIndex(list) {
106
+ const { stackElement } = this.getTopOfTheStack();
107
+ const activeIndex = list.findIndex(el => el === document.activeElement);
108
+ if (activeIndex !== -1) {
109
+ return activeIndex;
118
110
  }
119
- // Othrwise
120
- const nextEl = list[0];
121
- if (nextEl) {
122
- __classPrivateFieldGet(this, _TabManager_stack, "f")[stackIndex].lastIndex = 0;
123
- nextEl.focus();
111
+ return stackElement ? stackElement.lastIndex : -1;
112
+ }
113
+ focusByIndex(list, index) {
114
+ const { stackElement } = this.getTopOfTheStack();
115
+ const el = list[index];
116
+ if (!stackElement || !el) {
124
117
  return;
125
118
  }
119
+ stackElement.lastIndex = index;
120
+ el.focus();
126
121
  }
127
- focusPrev() {
128
- const { stackElement, stackIndex } = this.getTopOfTheStack();
122
+ focusNext() {
129
123
  const list = this.getElementsList();
130
- const nextIndex = stackElement.lastIndex - 1;
131
- if (list[nextIndex]) {
132
- list[nextIndex].focus();
133
- __classPrivateFieldGet(this, _TabManager_stack, "f")[stackIndex].lastIndex = nextIndex;
124
+ if (list.length === 0) {
134
125
  return;
135
126
  }
136
- // Othrwise
137
- const nextEl = list[list.length - 1];
138
- if (nextEl) {
139
- __classPrivateFieldGet(this, _TabManager_stack, "f")[stackIndex].lastIndex = list.length - 1;
140
- nextEl.focus();
127
+ const nextIndex = this.getCurrentIndex(list) + 1;
128
+ this.focusByIndex(list, nextIndex < list.length ? nextIndex : 0);
129
+ }
130
+ focusPrev() {
131
+ const list = this.getElementsList();
132
+ if (list.length === 0) {
141
133
  return;
142
134
  }
135
+ const prevIndex = this.getCurrentIndex(list) - 1;
136
+ this.focusByIndex(list, prevIndex >= 0 ? prevIndex : list.length - 1);
143
137
  }
144
138
  }
145
139
  _TabManager_stack = new WeakMap();