mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-09 08:09:32 +08:00
chore: add stronger types to several files (#28347)
Issue number: Internal --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> As part of FW-2832, the team would like to swap out usages of the any type for stronger types. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. -->c529bc23f1- `scrollToTop` doesn't return anything, so I added the `void` return typea96971ad28- `animation.effect` is a type of [AnimationEffect](https://developer.mozilla.org/en-US/docs/Web/API/Animation/effect). One of the more common types of effects is a `KeyframeEffect`. However, TypeScript doesn't know which specific type of AnimationEffect we are using, so I cast `animation.effect` as KeyframeEffect where appropriate. - I also added `!` to places where we know the effect and other properties are always defined (since they run after the web animation has been constructed) - Added stronger types to the internal to/from/fromTo functions (the public facing type improvements are in https://github.com/ionic-team/ionic-framework/pull/28334)fdaf550059- `getRootNode` can return multiple types of objects, so I cast it to the specific types that we work with in `isFocused`.46a6efa510- Added the "Animation" type and resolved related errors once we had stronger typesa7cb9a5685- Made heavier use of the `T` generic - Once we know `node` is an Element (`nodeType === 1`) we manually cast the element as `T`6a9d1f095d- The focus visible utility is an internal utility, but it was lacking an interface, so I added one.90b64c2de5- Removed unneeded HTMLElement casting - Added `!` since we can assume the selected elements are defined with the refresher - Added documentation as to why casting `referencEl.style` as `any` is something we need to keep.3a084caf83- Avoided the Event naming collision by using globalThis ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> Note: This PR contains only type changes. Changes the required updates to the implementation of Ionic are pulled out into separate PRs and target a minor release branch to minimize risk. --------- Co-authored-by: Amanda Johnston <90629384+amandaejohnston@users.noreply.github.com>
This commit is contained in:
2
core/src/components.d.ts
vendored
2
core/src/components.d.ts
vendored
@ -2254,7 +2254,7 @@ export namespace Components {
|
||||
*/
|
||||
"name": string;
|
||||
"setButtonTabindex": (value: number) => Promise<void>;
|
||||
"setFocus": (ev: any) => Promise<void>;
|
||||
"setFocus": (ev: globalThis.Event) => Promise<void>;
|
||||
/**
|
||||
* the value of the radio.
|
||||
*/
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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<void> {
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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')
|
||||
);
|
||||
|
||||
@ -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<typeof setTimeout> | 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);
|
||||
};
|
||||
|
||||
|
||||
@ -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<any> => {
|
||||
export const scrollToTop = (el: HTMLElement, durationMs: number): Promise<void> => {
|
||||
if (isIonContent(el)) {
|
||||
const content = el as HTMLIonContentElement;
|
||||
return content.scrollToTop(durationMs);
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 = (
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
// TODO(FW-2832): types
|
||||
|
||||
export const watchForOptions = <T extends HTMLElement>(
|
||||
containerEl: HTMLElement,
|
||||
tagName: string,
|
||||
@ -20,21 +18,35 @@ export const watchForOptions = <T extends HTMLElement>(
|
||||
};
|
||||
|
||||
const getSelectedOption = <T extends HTMLElement>(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<T>(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 = <T extends HTMLElement & { value?: any | null }>(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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user