fix(core): pseudo-class handlers failing to unsubscribe listeners (#10680)

This commit is contained in:
Dimitris-Rafail Katsampas
2025-01-29 21:26:59 +02:00
committed by GitHub
parent 4902e27818
commit e6beb1d816
6 changed files with 51 additions and 33 deletions

View File

@ -103,6 +103,7 @@ export class Button extends ButtonBase {
this.on(GestureTypes[GestureTypes.touch], onButtonStateChange);
} else {
this.off(GestureTypes[GestureTypes.touch], onButtonStateChange);
this._removeVisualState('highlighted');
}
}

View File

@ -29,6 +29,12 @@ export class Button extends ButtonBase {
public disposeNativeView(): void {
this._tapHandler = null;
if (this._stateChangedHandler) {
this._stateChangedHandler.stop();
this._stateChangedHandler = null;
}
super.disposeNativeView();
}
@ -37,28 +43,32 @@ export class Button extends ButtonBase {
return this.nativeViewProtected;
}
public onUnloaded() {
super.onUnloaded();
if (this._stateChangedHandler) {
this._stateChangedHandler.stop();
}
}
@PseudoClassHandler('normal', 'highlighted', 'pressed', 'active')
_updateButtonStateChangeHandler(subscribe: boolean) {
if (subscribe) {
if (!this._stateChangedHandler) {
const viewRef = new WeakRef<Button>(this);
this._stateChangedHandler = new ControlStateChangeListener(this.nativeViewProtected, observableVisualStates, (state: string, add: boolean) => {
if (add) {
this._addVisualState(state);
} else {
this._removeVisualState(state);
const view = viewRef?.deref?.();
if (view) {
if (add) {
view._addVisualState(state);
} else {
view._removeVisualState(state);
}
}
});
}
this._stateChangedHandler.start();
} else {
this._stateChangedHandler.stop();
// Remove any possible pseudo-class leftovers
for (const state of observableVisualStates) {
this._removeVisualState(state);
}
}
}

View File

@ -2,9 +2,9 @@
@NativeClass
class ObserverClass extends NSObject {
public callback: WeakRef<ControlStateChangeListenerCallback>;
public callback: ControlStateChangeListenerCallback;
public static initWithCallback(callback: WeakRef<ControlStateChangeListenerCallback>): ObserverClass {
public static initWithCallback(callback: ControlStateChangeListenerCallback): ObserverClass {
const observer = <ObserverClass>ObserverClass.alloc().init();
observer.callback = callback;
@ -12,8 +12,7 @@ class ObserverClass extends NSObject {
}
public observeValueForKeyPathOfObjectChangeContext(path: string, object: UIControl) {
const callback = this.callback?.deref();
const callback = this.callback;
if (callback) {
callback(path, object[path]);
}
@ -30,7 +29,7 @@ export class ControlStateChangeListener implements ControlStateChangeListenerDef
constructor(control: UIControl, states: string[], callback: ControlStateChangeListenerCallback) {
this._control = control;
this._states = states;
this._observer = ObserverClass.initWithCallback(new WeakRef(callback));
this._observer = ObserverClass.initWithCallback(callback);
}
public start() {

View File

@ -53,19 +53,21 @@ export function viewMatchesModuleContext(view: ViewDefinition, context: ModuleCo
export function PseudoClassHandler(...pseudoClasses: string[]): MethodDecorator {
const stateEventNames = pseudoClasses.map((s) => ':' + s);
const listeners = Symbol('listeners');
return <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => {
function update(change: number) {
const prev = this[listeners] || 0;
const next = prev + change;
if (prev <= 0 && next > 0) {
this[propertyKey](true);
} else if (prev > 0 && next <= 0) {
this[propertyKey](false);
return <T>(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) => {
// This will help keep track of pseudo-class subscription changes
const subscribeKey = Symbol(propertyKey + '_flag');
function onSubscribe(subscribe: boolean) {
if (subscribe != !!this[subscribeKey]) {
this[subscribeKey] = subscribe;
this[propertyKey](subscribe);
}
}
stateEventNames.forEach((s) => (target[s] = update));
for (const eventName of stateEventNames) {
target[eventName] = onSubscribe;
}
};
}

View File

@ -34,19 +34,25 @@ export abstract class EditableTextBase extends TextBase implements EditableTextB
public autocorrect: boolean;
public hint: string;
public maxLength: number;
public placeholderColor: Color;
public valueFormatter: (value: string) => string;
public abstract dismissSoftInput();
public abstract _setInputType(inputType: number): void;
public abstract setSelection(start: number, stop?: number);
placeholderColor: Color;
@PseudoClassHandler('focus', 'blur')
_updateTextBaseFocusStateHandler(subscribe) {
const method = subscribe ? 'on' : 'off';
_updateTextBaseFocusStateHandler(subscribe: boolean) {
if (subscribe) {
this.on('focus', focusChangeHandler);
this.on('blur', focusChangeHandler);
} else {
this.off('focus', focusChangeHandler);
this.off('blur', focusChangeHandler);
this[method]('focus', focusChangeHandler);
this[method]('blur', focusChangeHandler);
this._removeVisualState('focus');
this._removeVisualState('blur');
}
}
}

View File

@ -792,7 +792,7 @@ export class CssState {
const eventName = ':' + pseudoClass;
view.addEventListener(':' + pseudoClass, this._onDynamicStateChangeHandler);
if (view[eventName]) {
view[eventName](+1);
view[eventName](true);
}
});
}
@ -812,7 +812,7 @@ export class CssState {
const eventName = ':' + pseudoClass;
view.removeEventListener(eventName, this._onDynamicStateChangeHandler);
if (view[eventName]) {
view[eventName](-1);
view[eventName](false);
}
});
}