8000 feat(core): support for simultaneous pseudo states (#10656) · NativeScript/NativeScript@f970455 · GitHub
[go: up one dir, main page]

Skip to content

Commit f970455

Browse files
authored
feat(core): support for simultaneous pseudo states (#10656)
1 parent a883a79 commit f970455

File tree

16 files changed

+210
-103
lines changed

16 files changed

+210
-103
lines changed

apps/automated/src/ui/button/button-tests.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ export var test_StateHighlighted_also_fires_pressedState = function () {
274274

275275
helper.waitUntilLayoutReady(view);
276276

277-
view._goToVisualState('highlighted');
277+
view._addVisualState('highlighted');
278278

279279
var actualResult = buttonTestsNative.getNativeBackgroundColor(view);
280280
TKUnit.assert(actualResult.hex === expectedNormalizedColor, 'Actual: ' + actualResult.hex + '; Expected: ' + expectedNormalizedColor);
@@ -291,7 +291,7 @@ export var test_StateHighlighted_also_fires_activeState = function () {
291291

292292
helper.waitUntilLayoutReady(view);
293293

294-
view._goToVisualState('highlighted');
294+
view._addVisualState('highlighted');
295295

296296
var actualResult = buttonTestsNative.getNativeBackgroundColor(view);
297297
TKUnit.assert(actualResult.hex === expectedNormalizedColor, 'Actual: ' + actualResult.hex + '; Expected: ' + expectedNormalizedColor);

apps/automated/src/ui/styling/style-tests.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -602,9 +602,9 @@ export function test_restore_original_values_when_state_is_changed() {
602602
page.css = 'button { color: blue; } ' + 'button:pressed { color: red; } ';
603603

604604
helper.assertViewColor(btn, '#0000FF');
605-
btn._goToVisualState('pressed');
605+
btn._addVisualState('pressed');
606606
helper.assertViewColor(btn, '#FF0000');
607-
btn._goToVisualState('normal');
607+
btn._removeVisualState('pressed');
608608
helper.assertViewColor(btn, '#0000FF');
609609
}
610610

@@ -655,9 +655,9 @@ export const test_composite_selector_type_class_state = function () {
655655

656656
// The button with no class should not react to state changes.
657657
TKUnit.assertNull(btnWithNoClass.style.color, 'Color should not have a value.');
658-
btnWithNoClass._goToVisualState('pressed');
658+
btnWithNoClass._addVisualState('pressed');
659659
TKUnit.assertNull(btnWithNoClass.style.color, 'Color should not have a value.');
660-
btnWithNoClass._goToVisualState('normal');
660+
btnWithNoClass._removeVisualState('pressed');
661661
TKUnit.assertNull(btnWithNoClass.style.color, 'Color should not have a value.');
662662

663663
TKUnit.assertNull(lblWithClass.style.color, 'Color should not have a value');
@@ -864,11 +864,11 @@ function testSelectorsPrioritiesTemplate(css: string) {
864864
function testButtonPressedStateIsRed(btn: Button) {
865865
TKUnit.assert(btn.style.color === undefined, 'Color should not have a value.');
866866

867-
btn._goToVisualState('pressed');
867+
btn._addVisualState('pressed');
868868

869869
helper.assertViewColor(btn, '#FF0000');
870870

871-
btn._goToVisualState('normal');
871+
btn._removeVisualState('pressed');
872872

873873
TKUnit.assert(btn.style.color === undefined, 'Color should not have a value after returned to normal state.');
874874
}

apps/automated/src/ui/styling/visual-state-tests.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,54 @@ export var test_goToVisualState_NoState_ShouldGoToNormal = function () {
9494

9595
helper.do_PageTest_WithButton(test);
9696
};
97+
98+
export var test_addVisualState = function () {
99+
var test = function (views: Array<view.View>) {
100+
(<page.Page>views[0]).css = 'button:hovered { color: red; background-color: orange } button:pressed { color: white }';
101+
102+
var btn = views[1];
103+
104+
assertInState(btn, btn.defaultVisualState, ['hovered', 'pressed', btn.defaultVisualState]);
105+
106+
btn._addVisualState('hovered');
107+
108+
assertInState(btn, 'hovered', ['hovered', 'pressed', btn.defaultVisualState]);
109+
110+
TKUnit.assert(types.isDefined(btn.style.color) && btn.style.color.name === 'red');
111+
TKUnit.assert(types.isDefined(btn.style.backgroundColor) && btn.style.backgroundColor.name === 'orange');
112+
113+
btn._addVisualState('pressed');
114+
115+
assertInState(btn, 'hovered', ['hovered', btn.defaultVisualState]);
116+
assertInState(btn, 'pressed', ['pressed', btn.defaultVisualState]);
117+
118+
TKUnit.assert(types.isDefined(btn.style.color) && btn.style.color.name === 'white');
119+
TKUnit.assert(types.isDefined(btn.style.backgroundColor) && btn.style.backgroundColor.name === 'orange');
120+
};
121+
122+
helper.do_PageTest_WithButton(test);
123+
};
124+
125+
export var test_removeVisualState = function () {
126+
var test = function (views: Array<view.View>) {
127+
(<page.Page>views[0]).css = 'button { background-color: yellow; color: green } button:pressed { background-color: red; color: white }';
128+
129+
var btn = views[1];
130+
131+
btn._addVisualState('pressed');
132+
133+
assertInState(btn, 'pressed', ['pressed', 'hovered', btn.defaultVisualState]);
134+
135+
TKUnit.assert(types.isDefined(btn.style.color) && btn.style.color.name === 'white');
136+
TKUnit.assert(types.isDefined(btn.style.backgroundColor) && btn.style.backgroundColor.name === 'red');
137+
138+
btn._removeVisualState('pressed');
139+
140+
assertInState(btn, btn.defaultVisualState, ['hovered', 'pressed', btn.defaultVisualState]);
141+
142+
TKUnit.assert(types.isDefined(btn.style.color) && btn.style.color.name === 'green');
143+
TKUnit.assert(types.isDefined(btn.style.backgroundColor) && btn.style.backgroundColor.name === 'yellow');
144+
};
145+
146+
helper.do_PageTest_WithButton(test);
147+
};

packages/core/ui/button/index.android.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,24 @@ function initializeClickListener(): void {
4444
ClickListener = ClickListenerImpl;
4545
}
4646

47+
function onButtonStateChange(args: TouchGestureEventData) {
48+
const button = args.object as Button;
49+
50+
switch (args.action) {
51+
case TouchAction.up:
52+
case TouchAction.cancel:
53+
button._removeVisualState('highlighted');
54+
break;
55+
case TouchAction.down:
56+
button._addVisualState('highlighted');
57+
break;
58+
}
59+
}
60+
4761
export class Button extends ButtonBase {
4862
nativeViewProtected: android.widget.Button;
4963

5064
private _stateListAnimator: any;
51-
private _highlightedHandler: (args: TouchGestureEventData) => void;
5265

5366
@profile
5467
public createNativeView() {
@@ -87,22 +100,9 @@ export class Button extends ButtonBase {
87100
@PseudoClassHandler('normal', 'highlighted', 'pressed', 'active')
88101
_updateButtonStateChangeHandler(subscribe: boolean) {
89102
if (subscribe) {
90-
this._highlightedHandler =
91-
this._highlightedHandler ||
92-
((args: TouchGestureEventData) => {
93-
switch (args.action) {
94-
case TouchAction.up:
95-
case TouchAction.cancel:
96-
this._goToVisualState(this.defaultVisualState);
97-
break;
98-
case TouchAction.down:
99-
this._goToVisualState('highlighted');
100-
break;
101-
}
102-
});
103-
this.on(GestureTypes[GestureTypes.touch], this._highlightedHandler);
103+
this.on(GestureTypes[GestureTypes.touch], onButtonStateChange);
104104
} else {
105-
this.off(GestureTypes[GestureTypes.touch], this._highlightedHandler);
105+
this.off(GestureTypes[GestureTypes.touch], onButtonStateChange);
106106
}
107107
}
108108

packages/core/ui/button/index.ios.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { Color } from '../../color';
99

1010
export * from './button-common';
1111

12+
const observableVisualStates = ['highlighted']; // States like :disabled are handled elsewhere
13+
1214
export class Button extends ButtonBase {
1315
public nativeViewProtected: UIButton;
1416

@@ -46,8 +48,12 @@ export class Button extends ButtonBase {
4648
_updateButtonStateChangeHandler(subscribe: boolean) {
4749
if (subscribe) {
4850
if (!this._stateChangedHandler) {
49-
this._stateChangedHandler = new ControlStateChangeListener(this.nativeViewProtected, (s: string) => {
50-
this._goToVisualState(s);
51+
this._stateChangedHandler = new ControlStateChangeListener(this.nativeViewProtected, observableVisualStates, (state: string, add: boolean) => {
52+
if (add) {
53+
this._addVisualState(state);
54+
} else {
55+
this._removeVisualState(state);
56+
}
5157
});
5258
}
5359
this._stateChangedHandler.start();

packages/core/ui/core/control-state-change/index.android.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/* tslint:disable:no-unused-variable */
22
/* tslint:disable:no-empty */
3-
import { ControlStateChangeListener as ControlStateChangeListenerDefinition } from '.';
3+
import { ControlStateChangeListenerCallback, ControlStateChangeListener as ControlStateChangeListenerDefinition } from '.';
44

55
export class ControlStateChangeListener implements ControlStateChangeListenerDefinition {
6-
constructor(control: any /* UIControl */, callback: (state: string) => void) {
6+
constructor(control: any /* UIControl */, states: string[], callback: ControlStateChangeListenerCallback) {
77
console.log('ControlStateChangeListener is intended for IOS usage only.');
88
}
99
public start() {}

packages/core/ui/core/control-state-change/index.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
/**
1+
export type ControlStateChangeListenerCallback = (state: string, add: boolean) => void;
2+
3+
/**
24
* An utility class used for supporting styling infrastructure.
35
* WARNING: This class is intended for IOS only.
46
*/
@@ -8,7 +10,7 @@ export class ControlStateChangeListener {
810
* @param control An instance of the UIControl which state will be watched.
911
* @param callback A callback called when a visual state of the UIControl is changed.
1012
*/
11-
constructor(control: any /* UIControl */, callback: (state: string) => void);
13+
constructor(control: any /* UIControl */, states: string[], callback: ControlStateChangeListenerCallback);
1214

1315
start();
1416
stop();
Lines changed: 28 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,55 @@
1-
/* tslint:disable:no-unused-variable */
2-
import { ControlStateChangeListener as ControlStateChangeListenerDefinition } from '.';
1+
import { ControlStateChangeListenerCallback, ControlStateChangeListener as ControlStateChangeListenerDefinition } from '.';
32

43
@NativeClass
54
class ObserverClass extends NSObject {
6-
// NOTE: Refactor this - use Typescript property instead of strings....
7-
observeValueForKeyPathOfObjectChangeContext(path: string) {
8-
if (path === 'selected') {
9-
this['_owner']._onSelectedChanged();
10-
} else if (path === 'enabled') {
11-
this['_owner']._onEnabledChanged();
12-
} else if (path === 'highlighted') {
13-
this['_owner']._onHighlightedChanged();
5+
public callback: WeakRef<ControlStateChangeListenerCallback>;
6+
7+
public static initWithCallback(callback: WeakRef<ControlStateChangeListenerCallback>): ObserverClass {
8+
const observer = <ObserverClass>ObserverClass.alloc().init();
9+
observer.callback = callback;
10+
11+
return observer;
12+
}
13+
14+
public observeValueForKeyPathOfObjectChangeContext(path: string, object: UIControl) {
15+
const callback = this.callback?.deref();
16+
17+
if (callback) {
18+
callback(path, object[path]);
1419
}
1520
}
1621
}
1722

1823
export class ControlStateChangeListener implements ControlStateChangeListenerDefinition {
1924
private _observer: NSObject;
2025
private _control: UIControl;
21-
private _observing = false;
26+
private _observing: boolean = false;
2227

23-
private _callback: (state: string) => void;
28+
private readonly _states: string[];
2429

25-
constructor(control: UIControl, callback: (state: string) => void) {
26-
this._observer = ObserverClass.alloc().init();
27-
this._observer['_owner'] = this;
30+
constructor(control: UIControl, states: string[], callback: ControlStateChangeListenerCallback) {
2831
this._control = control;
29-
this._callback = callback;
32+
this._states = states;
33+
this._observer = ObserverClass.initWithCallback(new WeakRef(callback));
3034
}
3135

3236
public start() {
3337
if (!this._observing) {
34-
this._control.addObserverForKeyPathOptionsContext(this._observer, 'highlighted', NSKeyValueObservingOptions.New, null);
3538
this._observing = true;
36-
this._updateState();
39+
40+
for (const state of this._states) {
41+
this._control.addObserverForKeyPathOptionsContext(this._observer, state, NSKeyValueObservingOptions.New, null);
42+
}
3743
}
3844
}
3945

4046
public stop() {
4147
if (this._observing) {
42-
this._observing = false;
43-
this._control.removeObserverForKeyPath(this._observer, 'highlighted');
44-
}
45-
}
46-
47-
//@ts-ignore
48-
private _onEnabledChanged() {
49-
this._updateState();
50-
}
51-
52-
//@ts-ignore
53-
private _onSelectedChanged() {
54-
this._updateState();
55-
}
56-
57-
//@ts-ignore
58-
private _onHighlightedChanged() {
59-
this._updateState();
60-
}
48+
for (const state of this._states) {
49+
this._control.removeObserverForKeyPath(this._observer, state);
50+
}
6151

62-
private _updateState() {
63-
let state = 'normal';
64-
if (this._control.highlighted) {
65-
state = 'highlighted';
52+
this._observing = false;
6653
}
67-
this._callback(state);
6854
}
6955
}

0 commit comments

Comments
 (0)
0