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); this.on(GestureTypes[GestureTypes.touch], onButtonStateChange);
} else { } else {
this.off(GestureTypes[GestureTypes.touch], onButtonStateChange); this.off(GestureTypes[GestureTypes.touch], onButtonStateChange);
this._removeVisualState('highlighted');
} }
} }

View File

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

View File

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

View File

@ -53,19 +53,21 @@ export function viewMatchesModuleContext(view: ViewDefinition, context: ModuleCo
export function PseudoClassHandler(...pseudoClasses: string[]): MethodDecorator { export function PseudoClassHandler(...pseudoClasses: string[]): MethodDecorator {
const stateEventNames = pseudoClasses.map((s) => ':' + s); const stateEventNames = pseudoClasses.map((s) => ':' + s);
const listeners = Symbol('listeners');
return <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => { return <T>(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) => {
function update(change: number) { // This will help keep track of pseudo-class subscription changes
const prev = this[listeners] || 0; const subscribeKey = Symbol(propertyKey + '_flag');
const next = prev + change;
if (prev <= 0 && next > 0) { function onSubscribe(subscribe: boolean) {
this[propertyKey](true); if (subscribe != !!this[subscribeKey]) {
} else if (prev > 0 && next <= 0) { this[subscribeKey] = subscribe;
this[propertyKey](false); 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 autocorrect: boolean;
public hint: string; public hint: string;
public maxLength: number; public maxLength: number;
public placeholderColor: Color;
public valueFormatter: (value: string) => string; public valueFormatter: (value: string) => string;
public abstract dismissSoftInput(); public abstract dismissSoftInput();
public abstract _setInputType(inputType: number): void; public abstract _setInputType(inputType: number): void;
public abstract setSelection(start: number, stop?: number); public abstract setSelection(start: number, stop?: number);
placeholderColor: Color;
@PseudoClassHandler('focus', 'blur') @PseudoClassHandler('focus', 'blur')
_updateTextBaseFocusStateHandler(subscribe) { _updateTextBaseFocusStateHandler(subscribe: boolean) {
const method = subscribe ? 'on' : 'off'; if (subscribe) {
this.on('focus', focusChangeHandler);
this.on('blur', focusChangeHandler);
} else {
this.off('focus', focusChangeHandler);
this.off('blur', focusChangeHandler);
this[method]('focus', focusChangeHandler); this._removeVisualState('focus');
this[method]('blur', focusChangeHandler); this._removeVisualState('blur');
}
} }
} }

View File

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