diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 72e908cb92..52da25cceb 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -2254,7 +2254,7 @@ export namespace Components { */ "name": string; "setButtonTabindex": (value: number) => Promise; - "setFocus": (ev: any) => Promise; + "setFocus": (ev: globalThis.Event) => Promise; /** * the value of the radio. */ diff --git a/core/src/components/app/app.tsx b/core/src/components/app/app.tsx index fd11019db2..b0fc4731b3 100644 --- a/core/src/components/app/app.tsx +++ b/core/src/components/app/app.tsx @@ -1,5 +1,6 @@ import type { ComponentInterface } from '@stencil/core'; import { Build, Component, Element, Host, Method, h } from '@stencil/core'; +import type { FocusVisibleUtility } from '@utils/focus-visible'; import { isPlatform } from '@utils/platform'; import { config } from '../../global/config'; @@ -10,7 +11,7 @@ import { getIonMode } from '../../global/ionic-global'; styleUrl: 'app.scss', }) export class App implements ComponentInterface { - private focusVisible?: any; // TODO(FW-2832): type + private focusVisible?: FocusVisibleUtility; @Element() el!: HTMLElement; diff --git a/core/src/components/menu/menu.tsx b/core/src/components/menu/menu.tsx index db40853b88..4925051837 100644 --- a/core/src/components/menu/menu.tsx +++ b/core/src/components/menu/menu.tsx @@ -33,7 +33,7 @@ const focusableQueryString = shadow: true, }) export class Menu implements ComponentInterface, MenuI { - private animation?: any; // TODO(FW-2832): type + private animation?: Animation; private lastOnEnd = 0; private gesture?: Gesture; private blocker = GESTURE_CONTROLLER.createBlocker({ disableScroll: true }); @@ -491,11 +491,11 @@ export class Menu implements ComponentInterface, MenuI { this.animation = undefined; } // Create new animation - this.animation = await menuController._createAnimation(this.type!, this); + const animation = (this.animation = await menuController._createAnimation(this.type!, this)); if (!config.getBoolean('animated', true)) { - this.animation.duration(0); + animation.duration(0); } - this.animation.fill('both'); + animation.fill('both'); } private async startAnimation(shouldOpen: boolean, animated: boolean): Promise { diff --git a/core/src/components/radio/radio.tsx b/core/src/components/radio/radio.tsx index e22a47b34f..24f03cdd86 100644 --- a/core/src/components/radio/radio.tsx +++ b/core/src/components/radio/radio.tsx @@ -136,8 +136,7 @@ export class Radio implements ComponentInterface { /** @internal */ @Method() - async setFocus(ev: any) { - // TODO(FW-2832): type (using Event triggers a build error due to conflict with Stencil Event import) + async setFocus(ev: globalThis.Event) { ev.stopPropagation(); ev.preventDefault(); diff --git a/core/src/components/refresher/refresher.utils.ts b/core/src/components/refresher/refresher.utils.ts index d40653727d..8ae86de258 100644 --- a/core/src/components/refresher/refresher.utils.ts +++ b/core/src/components/refresher/refresher.utils.ts @@ -25,12 +25,11 @@ export const createPullingAnimation = ( }; const createBaseAnimation = (pullingRefresherIcon: HTMLElement) => { - // TODO(FW-2832): add types/re-evaluate asserting so many things - const spinner = pullingRefresherIcon.querySelector('ion-spinner') as HTMLElement; - const circle = spinner!.shadowRoot!.querySelector('circle') as any; - const spinnerArrowContainer = pullingRefresherIcon.querySelector('.spinner-arrow-container') as HTMLElement; + const spinner = pullingRefresherIcon.querySelector('ion-spinner')!; + const circle = spinner!.shadowRoot!.querySelector('circle')!; + const spinnerArrowContainer = pullingRefresherIcon.querySelector('.spinner-arrow-container')!; const arrowContainer = pullingRefresherIcon!.querySelector('.arrow-container'); - const arrow = arrowContainer ? (arrowContainer!.querySelector('ion-icon') as HTMLElement) : null; + const arrow = arrowContainer ? arrowContainer!.querySelector('ion-icon') : null; const baseAnimation = createAnimation().duration(1000).easing('ease-out'); @@ -210,6 +209,14 @@ export const shouldUseNativeRefresher = async (referenceEl: HTMLIonRefresherElem return ( pullingSpinner !== null && refreshingSpinner !== null && + /** + * We use webkitOverflowScrolling for feature detection with rubber band scrolling + * on iOS. When doing referenceEl.style, webkitOverflowScrolling is undefined on non-iOS platforms. + * However, it will be the empty string on iOS. + * Note that we do not use getPropertyValue (and thus need to cast as any) because calling + * getPropertyValue('-webkit-overflow-scrolling') will return the empty string if it is not + * set on the element, even if the platform does not support that. + */ ((mode === 'ios' && isPlatform('mobile') && (referenceEl.style as any).webkitOverflowScrolling !== undefined) || mode === 'md') ); diff --git a/core/src/utils/animation/animation.ts b/core/src/utils/animation/animation.ts index c547bafd3c..3581390df0 100644 --- a/core/src/utils/animation/animation.ts +++ b/core/src/utils/animation/animation.ts @@ -32,6 +32,12 @@ interface AnimationOnFinishCallback { type AnimationOnStopCallback = AnimationOnFinishCallback; +/** + * The callback used for beforeAddRead, beforeAddWrite, + * afterAddRead, and afterAddWrite. + */ +type AnimationReadWriteCallback = () => void; + export const createAnimation = (animationId?: string): Animation => { let _delay: number | undefined; let _duration: number | undefined; @@ -51,7 +57,7 @@ export const createAnimation = (animationId?: string): Animation => { let numAnimationsRunning = 0; let shouldForceLinearEasing = false; let shouldForceSyncPlayback = false; - let cssAnimationsTimerFallback: any; + let cssAnimationsTimerFallback: ReturnType | undefined; let forceDirectionValue: AnimationDirection | undefined; let forceDurationValue: number | undefined; let forceDelayValue: number | undefined; @@ -69,11 +75,11 @@ export const createAnimation = (animationId?: string): Animation => { const elements: HTMLElement[] = []; const childAnimations: Animation[] = []; const stylesheets: HTMLElement[] = []; - const _beforeAddReadFunctions: any[] = []; - const _beforeAddWriteFunctions: any[] = []; - const _afterAddReadFunctions: any[] = []; - const _afterAddWriteFunctions: any[] = []; - const webAnimations: any[] = []; + const _beforeAddReadFunctions: AnimationReadWriteCallback[] = []; + const _beforeAddWriteFunctions: AnimationReadWriteCallback[] = []; + const _afterAddReadFunctions: AnimationReadWriteCallback[] = []; + const _afterAddWriteFunctions: AnimationReadWriteCallback[] = []; + const webAnimations: globalThis.Animation[] = []; const supportsAnimationEffect = typeof (AnimationEffect as any) === 'function' || (win !== undefined && typeof (win as any).AnimationEffect === 'function'); @@ -229,25 +235,25 @@ export const createAnimation = (animationId?: string): Animation => { stylesheets.length = 0; }; - const beforeAddRead = (readFn: () => void) => { + const beforeAddRead = (readFn: AnimationReadWriteCallback) => { _beforeAddReadFunctions.push(readFn); return ani; }; - const beforeAddWrite = (writeFn: () => void) => { + const beforeAddWrite = (writeFn: AnimationReadWriteCallback) => { _beforeAddWriteFunctions.push(writeFn); return ani; }; - const afterAddRead = (readFn: () => void) => { + const afterAddRead = (readFn: AnimationReadWriteCallback) => { _afterAddReadFunctions.push(readFn); return ani; }; - const afterAddWrite = (writeFn: () => void) => { + const afterAddWrite = (writeFn: AnimationReadWriteCallback) => { _afterAddWriteFunctions.push(writeFn); return ani; @@ -505,10 +511,25 @@ export const createAnimation = (animationId?: string): Animation => { const updateKeyframes = (keyframeValues: AnimationKeyFrames) => { if (supportsWebAnimations) { getWebAnimations().forEach((animation) => { - if (animation.effect.setKeyframes) { - animation.effect.setKeyframes(keyframeValues); + /** + * animation.effect's type is AnimationEffect. + * However, in this case we have a more specific + * type of AnimationEffect called KeyframeEffect which + * inherits from AnimationEffect. As a result, + * we cast animation.effect to KeyframeEffect. + */ + const keyframeEffect = animation.effect as KeyframeEffect; + + /** + * setKeyframes is not supported in all browser + * versions that Ionic supports, so we need to + * check for support before using it. + */ + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (keyframeEffect.setKeyframes) { + keyframeEffect.setKeyframes(keyframeValues); } else { - const newEffect = new KeyframeEffect(animation.effect.target, keyframeValues, animation.effect.getTiming()); + const newEffect = new KeyframeEffect(keyframeEffect.target, keyframeValues, keyframeEffect.getTiming()); animation.effect = newEffect; } }); @@ -686,7 +707,8 @@ export const createAnimation = (animationId?: string): Animation => { step = Math.min(Math.max(step, 0), 0.9999); if (supportsWebAnimations) { webAnimations.forEach((animation) => { - animation.currentTime = animation.effect.getComputedTiming().delay + getDuration() * step; + // When creating the animation the delay is guaranteed to be set to a number. + animation.currentTime = animation.effect!.getComputedTiming().delay! + getDuration() * step; animation.pause(); }); } else { @@ -703,7 +725,7 @@ export const createAnimation = (animationId?: string): Animation => { const updateWebAnimation = (step?: number) => { webAnimations.forEach((animation) => { - animation.effect.updateTiming({ + animation.effect!.updateTiming({ delay: getDelay(), duration: getDuration(), easing: getEasing(), @@ -1052,7 +1074,7 @@ export const createAnimation = (animationId?: string): Animation => { onStopOneTimeCallbacks.length = 0; }; - const from = (property: string, value: any) => { + const from = (property: string, value: string | number) => { const firstFrame = _keyframes[0] as AnimationKeyFrameEdge | undefined; if (firstFrame !== undefined && (firstFrame.offset === undefined || firstFrame.offset === 0)) { @@ -1064,7 +1086,7 @@ export const createAnimation = (animationId?: string): Animation => { return ani; }; - const to = (property: string, value: any) => { + const to = (property: string, value: string | number) => { const lastFrame = _keyframes[_keyframes.length - 1] as AnimationKeyFrameEdge | undefined; if (lastFrame !== undefined && (lastFrame.offset === undefined || lastFrame.offset === 1)) { @@ -1075,7 +1097,7 @@ export const createAnimation = (animationId?: string): Animation => { return ani; }; - const fromTo = (property: string, fromValue: any, toValue: any) => { + const fromTo = (property: string, fromValue: string | number, toValue: string | number) => { return from(property, fromValue).to(property, toValue); }; diff --git a/core/src/utils/content/index.ts b/core/src/utils/content/index.ts index da19ee1d0e..44a6f7bff1 100644 --- a/core/src/utils/content/index.ts +++ b/core/src/utils/content/index.ts @@ -62,8 +62,7 @@ export const findClosestIonContent = (el: Element) => { * Scrolls to the top of the element. If an `ion-content` is found, it will scroll * using the public API `scrollToTop` with a duration. */ -// TODO(FW-2832): type -export const scrollToTop = (el: HTMLElement, durationMs: number): Promise => { +export const scrollToTop = (el: HTMLElement, durationMs: number): Promise => { if (isIonContent(el)) { const content = el as HTMLIonContentElement; return content.scrollToTop(durationMs); diff --git a/core/src/utils/focus-visible.ts b/core/src/utils/focus-visible.ts index 97db5096e5..b5473c9e68 100644 --- a/core/src/utils/focus-visible.ts +++ b/core/src/utils/focus-visible.ts @@ -15,7 +15,12 @@ const FOCUS_KEYS = [ 'End', ]; -export const startFocusVisible = (rootEl?: HTMLElement) => { +export interface FocusVisibleUtility { + destroy: () => void; + setFocus: (elements: Element[]) => void; +} + +export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility => { let currentFocus: Element[] = []; let keyboardMode = true; diff --git a/core/src/utils/input-shims/hacks/common.ts b/core/src/utils/input-shims/hacks/common.ts index 6a4a89c193..2f42749c2e 100644 --- a/core/src/utils/input-shims/hacks/common.ts +++ b/core/src/utils/input-shims/hacks/common.ts @@ -18,9 +18,17 @@ export const relocateInput = ( } }; -// TODO(FW-2832): type export const isFocused = (input: HTMLInputElement | HTMLTextAreaElement): boolean => { - return input === (input as any).getRootNode().activeElement; + /** + * 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 = ( diff --git a/core/src/utils/watch-options.ts b/core/src/utils/watch-options.ts index 803aa90c4b..e322c060cb 100644 --- a/core/src/utils/watch-options.ts +++ b/core/src/utils/watch-options.ts @@ -1,5 +1,3 @@ -// TODO(FW-2832): types - export const watchForOptions = ( containerEl: HTMLElement, tagName: string, @@ -20,21 +18,35 @@ export const watchForOptions = ( }; const getSelectedOption = (mutationList: MutationRecord[], tagName: string): T | undefined => { - let newOption: HTMLElement | undefined; + let newOption: T | undefined; mutationList.forEach((mut) => { // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < mut.addedNodes.length; i++) { - newOption = findCheckedOption(mut.addedNodes[i], tagName) || newOption; + newOption = findCheckedOption(mut.addedNodes[i], tagName) || newOption; } }); - return newOption as any; + return newOption; }; -export const findCheckedOption = (el: any, tagName: string) => { - if (el.nodeType !== 1) { +/** + * The "value" key is only set on some components such as ion-select-option. + * As a result, we create a default union type of HTMLElement and the "value" key. + * However, implementers are required to provide the appropriate component type + * such as HTMLIonSelectOptionElement. + */ +export const findCheckedOption = (node: Node, tagName: string) => { + /** + * https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType + * The above check ensures "node" is an Element (nodeType 1). + */ + if (node.nodeType !== 1) { return undefined; } - const options: HTMLElement[] = el.tagName === tagName.toUpperCase() ? [el] : Array.from(el.querySelectorAll(tagName)); - return options.find((o: any) => o.value === el.value); + // HTMLElement inherits from Element, so we cast "el" as T. + const el = node as T; + + const options: T[] = el.tagName === tagName.toUpperCase() ? [el] : Array.from(el.querySelectorAll(tagName)); + + return options.find((o: T) => o.value === el.value); };