Skip to content
Permalink
Browse files

[Buttons] Match backgroundColor to titleColor API (#5919)

MDCButton's `backgroundColor:forState:` API will now handle the special
case of `(UIControlStateHighlighted | UIControlStateDisabled)` the same
way that UIButton's `titleColor:forState:` API does. With the exception
of fallback behavior in state handling, their behavior should be
identical.

Context and Background
----------------------

When `UIControlStateDisabled` and `UIControlStateHighlighted` are
included in the same argument for the `state` parameter in
`setTitleColor:forState:`, the `UIControlStateDisabled` bit is ignored.
This means that when setting a `titleColor` for the `(.selected |
.highlighted | .disabled)` state, UIButton will actually store it as the
`(.highlighted | .selected)` state. There is an additional exception
that if there is no current value for the "adjusted" UIControlState,
then no assignment is made.

**Example 1: Fallback to `.highlighted` state**

The following example verifies that UIButton will ignore the
`UIControlStateDisabled` bit in the UIControlState parameter when
combined with `UIControlStateHighlighted`. The result is a "fallback" to
the value for `UIControlStateHighlighted`.

```objc
UIButton *buttonSetHighlighted = [[UIButton alloc] init];
[buttonSetHighlighted setTitleColor:UIColor.orangeColor forState:UIControlStateHighlighted];
XCTAssertEqualObjects([buttonSetHighlighted titleColorForState:(UIControlStateDisabled | UIControlStateHighlighted)],
                      [buttonSetHighlighted titleColorForState:UIControlStateHighlighted]);
XCTAssertEqualObjects([buttonSetHighlighted titleColorForState:(UIControlStateSelected | UIControlStateDisabled | UIControlStateHighlighted)],
                      [buttonSetHighlighted titleColorForState:UIControlStateNormal]);
```

**Example 2: Failure to store value for `(.highlighted | .disabled)`**

The following example verifies that UIButton will discard the assigned
value for `(UIControlStateDisabled | UIControlStateHighlighted)` when no
previous value for `UIControlStateHighlighted` has been set.

```objc
UIButton *buttonSetHighlightedDisabled = [[UIButton alloc] init];
[buttonSetHighlightedDisabled setTitleColor:UIColor.orangeColor forState:(UIControlStateDisabled | UIControlStateHighlighted)];
XCTAssertEqualObjects([buttonSetHighlightedDisabled titleColorForState:(UIControlStateDisabled | UIControlStateHighlighted)],
                      [buttonSetHighlightedDisabled titleColorForState:UIControlStateNormal]);
XCTAssertEqualObjects([buttonSetHighlightedDisabled titleColorForState:(UIControlStateSelected | UIControlStateDisabled | UIControlStateHighlighted)],
                      [buttonSetHighlightedDisabled titleColorForState:UIControlStateNormal]);
```

**Example 3: Assigning value for `(.disabled | .highlighted)` overwrites
previous value for `.highlighted`**

The following example verifies that UIButton will overwrite any previous
value for the `UIControlStateHighlighted` state when a value for
`(UIControlStateDisabled | UIControlStateHighlighted)` is later
assigned.

```objc
UIButton *buttonSetHighlightedDisabled = [[UIButton alloc] init];
[buttonSetHighlightedDisabled setTitleColor:UIColor.purpleColor forState:UIControlStateHighlighted];
[buttonSetHighlightedDisabled setTitleColor:UIColor.orangeColor forState:(UIControlStateDisabled | UIControlStateHighlighted)];
XCTAssertEqualObjects([buttonSetHighlightedDisabled titleColorForState:(UIControlStateDisabled | UIControlStateHighlighted)],
                      UIColor.orangeColor);
XCTAssertEqualObjects([buttonSetHighlightedDisabled titleColorForState:UIControlStateHighlighted],
                      UIColor.orangeColor);
```

Part of #3411
  • Loading branch information
romoore committed Dec 7, 2018
1 parent 5b4c5e5 commit 71b24fe3b95c5989f7cb7c7de4e8c661f996fb16
Showing with 70 additions and 2 deletions.
  1. +17 −2 components/Buttons/src/MDCButton.m
  2. +53 −0 components/Buttons/tests/unit/ButtonsTests.m
@@ -541,12 +541,27 @@ - (UIColor *)backgroundColor {
}

- (UIColor *)backgroundColorForState:(UIControlState)state {
// If the `.highlighted` flag is set, turn off the `.disabled` flag
if ((state & UIControlStateHighlighted) == UIControlStateHighlighted) {
state = state & ~UIControlStateDisabled;
}
return _backgroundColors[@(state)];
}

- (void)setBackgroundColor:(UIColor *)backgroundColor forState:(UIControlState)state {
_backgroundColors[@(state)] = backgroundColor;
[self updateAlphaAndBackgroundColorAnimated:NO];
UIControlState storageState = state;
// If the `.highlighted` flag is set, turn off the `.disabled` flag
if ((state & UIControlStateHighlighted) == UIControlStateHighlighted) {
storageState = state & ~UIControlStateDisabled;
}

// Only update the backing dictionary if:
// 1. The `state` argument is the same as the "storage" state, OR
// 2. There is already a value in the "storage" state.
if (storageState == state || _backgroundColors[@(storageState)] != nil) {
_backgroundColors[@(storageState)] = backgroundColor;
[self updateAlphaAndBackgroundColorAnimated:NO];
}
}

#pragma mark - Image Tint Color
@@ -345,6 +345,59 @@ - (void)testBackgroundColorForStateUpdatesBackgroundColorWithFallback {
}
}

// Behavioral test to verify that MDCButton's `backgroundColor:forState:` matches the behavior of
// UIButton's `titleColor:forState:`. Specifically, to ensure that the special handling of
// (UIControlStateDisabled | UIControlStateHighlighted) is identical.
//
// This test is valuable because clients who are familiar with the fallback behavior of
// `titleColor:forState:` may be surprised if the MDCButton APIs don't match. For example, setting
// the titleColor for (UIControlStateDisabled | UIControlStateHighlighted) will actually update the
// value assigned for UIControlStateHighlighted, but ONLY if it has already been assigned. Otherwise
// no update will take place.
- (void)testBackgroundColorForStateBehaviorMatchesTitleColorForStateWithoutFallbackForward {
// Given
MDCButton *testButton = [[MDCButton alloc] init];
UIButton *uiButton = [[UIButton alloc] init];

// When
UIControlState maxState = UIControlStateNormal | UIControlStateHighlighted |
UIControlStateDisabled | UIControlStateSelected;
for (UIControlState state = 0; state <= maxState; ++state) {
UIColor *color = [UIColor colorWithWhite:0 alpha:(CGFloat)(state / (CGFloat)maxState)];
[testButton setBackgroundColor:color forState:state];
[uiButton setTitleColor:color forState:state];
}

// Then
for (UIControlState state = 0; state <= maxState; ++state) {
XCTAssertEqualObjects([testButton backgroundColorForState:state],
[uiButton titleColorForState:state], @" for state (%lu)",
(unsigned long)state);
}
}

- (void)testBackgroundColorForStateBehaviorMatchesTitleColorForStateWithoutFallbackBackward {
// Given
MDCButton *testButton = [[MDCButton alloc] init];
UIButton *uiButton = [[UIButton alloc] init];

// When
UIControlState maxState = UIControlStateNormal | UIControlStateHighlighted |
UIControlStateDisabled | UIControlStateSelected;
for (NSInteger state = maxState; state >= 0; --state) {
UIColor *color = [UIColor colorWithWhite:0 alpha:(CGFloat)(state / (CGFloat)maxState)];
[testButton setBackgroundColor:color forState:(UIControlState)state];
[uiButton setTitleColor:color forState:(UIControlState)state];
}

// Then
for (UIControlState state = 0; state <= maxState; ++state) {
XCTAssertEqualObjects([testButton backgroundColorForState:state],
[uiButton titleColorForState:state], @" for state (%lu)",
(unsigned long)state);
}
}

#pragma mark - shadowColor:forState:

- (void)testRemovedShadowColorForState {

0 comments on commit 71b24fe

Please sign in to comment.
You can’t perform that action at this time.