๐Ÿ“ฆ ionic-team / ionic-framework

๐Ÿ“„ common.ts ยท 111 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111const cloneMap = new WeakMap<HTMLElement, HTMLElement>();

export const relocateInput = (
  componentEl: HTMLElement,
  inputEl: HTMLInputElement | HTMLTextAreaElement,
  shouldRelocate: boolean,
  inputRelativeY = 0,
  disabledClonedInput = false
) => {
  if (cloneMap.has(componentEl) === shouldRelocate) {
    return;
  }

  if (shouldRelocate) {
    addClone(componentEl, inputEl, inputRelativeY, disabledClonedInput);
  } else {
    removeClone(componentEl, inputEl);
  }
};

export const isFocused = (input: HTMLInputElement | HTMLTextAreaElement): boolean => {
  /**
   * https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode
   * Calling getRootNode on an element in standard web page will return HTMLDocument.
   * Calling getRootNode on an element inside of the Shadow DOM will return the associated ShadowRoot.
   * Calling getRootNode on an element that is not attached to a document/shadow tree will return
   * the root of the DOM tree it belongs to.
   * isFocused is used for the hide-caret utility which only considers input/textarea elements
   * that are present in the DOM, so we don't set types for that final case since it does not apply.
   */
  return input === (input.getRootNode() as HTMLDocument | ShadowRoot).activeElement;
};

const addClone = (
  componentEl: HTMLElement,
  inputEl: HTMLInputElement | HTMLTextAreaElement,
  inputRelativeY: number,
  disabledClonedInput = false
) => {
  // this allows for the actual input to receive the focus from
  // the user's touch event, but before it receives focus, it
  // moves the actual input to a location that will not screw
  // up the app's layout, and does not allow the native browser
  // to attempt to scroll the input into place (messing up headers/footers)
  // the cloned input fills the area of where native input should be
  // while the native input fakes out the browser by relocating itself
  // before it receives the actual focus event
  // We hide the focused input (with the visible caret) invisible by making it scale(0),
  const parentEl = inputEl.parentNode!;

  // DOM WRITES
  const clonedEl = inputEl.cloneNode(false) as HTMLInputElement | HTMLTextAreaElement;
  clonedEl.classList.add('cloned-input');
  clonedEl.tabIndex = -1;

  /**
   * Making the cloned input disabled prevents
   * Chrome for Android from still scrolling
   * the entire page since this cloned input
   * will briefly be hidden by the keyboard
   * even though it is not focused.
   *
   * This is not needed on iOS. While this
   * does not cause functional issues on iOS,
   * the input still appears slightly dimmed even
   * if we set opacity: 1.
   */
  if (disabledClonedInput) {
    clonedEl.disabled = true;
  }

  /**
   * Position the clone at the same horizontal offset as the native input
   * to prevent the placeholder from overlapping start slot content (e.g., icons).
   */
  const doc = componentEl.ownerDocument!;
  const isRTL = doc.dir === 'rtl';

  if (isRTL) {
    const parentWidth = (parentEl as HTMLElement).offsetWidth;
    const startOffset = parentWidth - inputEl.offsetLeft - inputEl.offsetWidth;
    clonedEl.style.insetInlineStart = `${startOffset}px`;
  } else {
    clonedEl.style.insetInlineStart = `${inputEl.offsetLeft}px`;
  }

  parentEl.appendChild(clonedEl);
  cloneMap.set(componentEl, clonedEl);

  const tx = isRTL ? 9999 : -9999;
  componentEl.style.pointerEvents = 'none';
  inputEl.style.transform = `translate3d(${tx}px,${inputRelativeY}px,0) scale(0)`;
};

const removeClone = (componentEl: HTMLElement, inputEl: HTMLElement) => {
  const clone = cloneMap.get(componentEl);
  if (clone) {
    cloneMap.delete(componentEl);
    clone.remove();
  }
  componentEl.style.pointerEvents = '';
  inputEl.style.transform = '';
};

/**
 * Factoring in 50px gives us some room
 * in case the keyboard shows password/autofill bars
 * asynchronously.
 */
export const SCROLL_AMOUNT_PADDING = 50;