blob: 9831bbbb255a2054ebd3c30a54bb7fad313040db [file] [log] [blame] [edit]
// Copyright 2017-present the Material Components for iOS authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "MDCTextInputControllerFullWidth.h"
#import "MDCIntrinsicHeightTextView.h"
#import "MDCMultilineTextField.h"
#import "MDCTextField.h"
#import "MDCTextInput.h"
#import "MDCTextInputCharacterCounter.h"
#import "MDCTextInputController.h"
#import "MDCTextInputUnderlineView.h"
#import "MaterialPalettes.h"
#import "MaterialTypography.h"
static const CGFloat MDCTextInputControllerFullWidthHintTextOpacity = (CGFloat)0.54;
static const CGFloat MDCTextInputControllerFullWidthHorizontalInnerPadding = 8;
static const CGFloat MDCTextInputControllerFullWidthHorizontalPadding = 16;
static const CGFloat MDCTextInputControllerFullWidthVerticalPadding = 20;
static inline UIColor *MDCTextInputControllerFullWidthInlinePlaceholderTextColorDefault() {
return [UIColor colorWithWhite:0 alpha:MDCTextInputControllerFullWidthHintTextOpacity];
}
static inline UIColor *MDCTextInputControllerFullWidthErrorColorDefault() {
return [MDCPalette redPalette].accent400;
}
#pragma mark - Class Properties
static BOOL _mdc_adjustsFontForContentSizeCategoryDefault = NO;
static UIColor *_backgroundColorDefault;
static UIColor *_errorColorDefault;
static UIColor *_inlinePlaceholderColorDefault;
static UIColor *_trailingUnderlineLabelTextColorDefault;
static UIFont *_inlinePlaceholderFontDefault;
static UIFont *_textInputFontDefault;
static UIColor *_textInputClearButtonTintColorDefault;
static UIFont *_trailingUnderlineLabelFontDefault;
@interface MDCTextInputControllerFullWidth () {
BOOL _mdc_adjustsFontForContentSizeCategory;
MDCTextInputAllCharactersCounter *_characterCounter;
UIColor *_backgroundColor;
UIColor *_errorColor;
UIColor *_inlinePlaceholderColor;
UIColor *_trailingUnderlineLabelTextColor;
UIFont *_inlinePlaceholderFont;
UIFont *_textInputFont;
UIColor *_textInputClearButtonTintColor;
UIFont *_trailingUnderlineLabelFont;
}
@property(nonatomic, assign, readonly) BOOL isDisplayingCharacterCountError;
@property(nonatomic, strong) MDCTextInputAllCharactersCounter *internalCharacterCounter;
@property(nonatomic, strong) NSLayoutConstraint *characterCountY;
@property(nonatomic, strong) NSLayoutConstraint *characterCountTrailing;
@property(nonatomic, strong) NSLayoutConstraint *clearButtonY;
@property(nonatomic, strong) NSLayoutConstraint *clearButtonTrailingCharacterCountLeading;
@property(nonatomic, strong) NSLayoutConstraint *multilineCharacterCountHeight;
@property(nonatomic, strong) NSLayoutConstraint *multilinePlaceholderCenterY;
@property(nonatomic, strong) NSLayoutConstraint *multilineTextViewBottom;
@property(nonatomic, strong) NSLayoutConstraint *multilineTextViewTop;
@property(nonatomic, strong) NSLayoutConstraint *placeholderLeading;
@property(nonatomic, strong) NSLayoutConstraint *placeholderTrailingCharacterCountLeading;
@property(nonatomic, strong) NSLayoutConstraint *placeholderTrailingSuperviewTrailing;
@property(nonatomic, copy) NSString *errorAccessibilityValue;
@property(nonatomic, copy, readwrite) NSString *errorText;
@property(nonatomic, copy) NSString *helperAccessibilityLabel;
@property(nonatomic, copy) NSString *previousLeadingText;
@property(nonatomic, strong) UIColor *previousPlaceholderColor;
@end
@implementation MDCTextInputControllerFullWidth
@synthesize backgroundColor = _backgroundColor;
@synthesize characterCountMax = _characterCountMax;
@synthesize characterCountViewMode = _characterCountViewMode;
@synthesize textInput = _textInput;
// TODO: (larche): Support in-line auto complete.
- (instancetype)init {
self = [super init];
if (self) {
[self commonMDCTextInputControllerFullWidthInitialization];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
[self commonMDCTextInputControllerFullWidthInitialization];
}
return self;
}
- (instancetype)initWithTextInput:(UIView<MDCTextInput> *)textInput {
self = [self init];
if (self) {
_textInput = textInput;
[self setupInput];
}
return self;
}
- (instancetype)copyWithZone:(__unused NSZone *)zone {
MDCTextInputControllerFullWidth *copy = [[[self class] alloc] init];
copy.characterCounter = self.characterCounter; // Just a pointer value copy
copy.characterCountViewMode = self.characterCountViewMode;
copy.characterCountMax = self.characterCountMax;
copy.errorAccessibilityValue = [self.errorAccessibilityValue copy];
copy.errorColor = self.errorColor;
copy.errorText = [self.errorText copy];
copy.helperText = [self.helperText copy];
copy.inlinePlaceholderColor = self.inlinePlaceholderColor;
copy.inlinePlaceholderFont = self.inlinePlaceholderFont;
copy.previousLeadingText = [self.previousLeadingText copy];
copy.previousPlaceholderColor = self.previousPlaceholderColor;
copy.textInput = self.textInput; // Just a pointer value copy
copy.trailingUnderlineLabelFont = self.trailingUnderlineLabelFont;
copy.trailingUnderlineLabelTextColor = self.trailingUnderlineLabelTextColor;
copy.activeColor = self.activeColor;
copy.disabledColor = self.disabledColor;
copy.normalColor = self.normalColor;
return copy;
}
- (void)dealloc {
[self unsubscribeFromNotifications];
}
- (void)commonMDCTextInputControllerFullWidthInitialization {
_characterCountViewMode = UITextFieldViewModeAlways;
_internalCharacterCounter = [[MDCTextInputAllCharactersCounter alloc] init];
}
- (void)setupInput {
if (!_textInput) {
return;
}
// This controller will handle Dynamic Type and all fonts for the text input
_mdc_adjustsFontForContentSizeCategory =
_textInput.mdc_adjustsFontForContentSizeCategory ||
[self class].mdc_adjustsFontForContentSizeCategoryDefault;
_textInput.mdc_adjustsFontForContentSizeCategory = NO;
_textInput.positioningDelegate = self;
[self subscribeForNotifications];
_textInput.underline.color = [UIColor clearColor];
_textInput.clearButton.tintColor = self.textInputClearButtonTintColor;
[self updateLayout];
}
- (void)subscribeForNotifications {
if (!_textInput) {
return;
}
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
if ([_textInput isKindOfClass:[UITextField class]]) {
[defaultCenter addObserver:self
selector:@selector(textInputDidBeginEditing:)
name:UITextFieldTextDidBeginEditingNotification
object:_textInput];
[defaultCenter addObserver:self
selector:@selector(textInputDidChange:)
name:UITextFieldTextDidChangeNotification
object:_textInput];
[defaultCenter addObserver:self
selector:@selector(textInputDidEndEditing:)
name:UITextFieldTextDidEndEditingNotification
object:_textInput];
[defaultCenter addObserver:self
selector:@selector(textInputDidChange:)
name:MDCTextFieldTextDidSetTextNotification
object:_textInput];
}
if ([_textInput isKindOfClass:[MDCMultilineTextField class]]) {
MDCMultilineTextField *textField = (MDCMultilineTextField *)_textInput;
[defaultCenter addObserver:self
selector:@selector(textInputDidBeginEditing:)
name:UITextViewTextDidBeginEditingNotification
object:textField.textView];
[defaultCenter addObserver:self
selector:@selector(textInputDidChange:)
name:UITextViewTextDidChangeNotification
object:textField.textView];
[defaultCenter addObserver:self
selector:@selector(textInputDidEndEditing:)
name:UITextViewTextDidEndEditingNotification
object:textField.textView];
}
}
- (void)unsubscribeFromNotifications {
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter removeObserver:self];
}
#pragma mark - Character Max Implementation
- (NSUInteger)characterCount {
return [self.characterCounter characterCountForTextInput:self.textInput];
}
- (id<MDCTextInputCharacterCounter>)characterCounter {
if (!_characterCounter) {
_characterCounter = self.internalCharacterCounter;
}
return _characterCounter;
}
- (void)setCharacterCounter:(id<MDCTextInputCharacterCounter>)characterCounter {
if (_characterCounter != characterCounter) {
_characterCounter = characterCounter;
[self updateLayout];
}
}
- (void)setCharacterCountMax:(NSUInteger)characterCountMax {
if (_characterCountMax != characterCountMax) {
_characterCountMax = characterCountMax;
[self updateLayout];
}
}
#pragma mark - TextInput Customization
- (void)updateTextInput {
UIFont *font = self.textInputFont;
if (self.mdc_adjustsFontForContentSizeCategory) {
// TODO: (#4331) This needs to be converted to the new text scheme.
font = [font mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleBody1
scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
}
self.textInput.font = font;
}
#pragma mark - Leading Label Customization
- (void)updateLeadingUnderlineLabel {
self.textInput.leadingUnderlineLabel.text = nil;
self.textInput.leadingUnderlineLabel.textColor = self.leadingUnderlineLabelTextColor;
}
#pragma mark - Placeholder Customization
- (void)updatePlaceholder {
UIFont *placeHolderFont = self.inlinePlaceholderFont;
if (self.mdc_adjustsFontForContentSizeCategory) {
// TODO: (#4331) This needs to be converted to the new text scheme.
placeHolderFont =
[placeHolderFont mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleBody1
scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
}
self.textInput.placeholderLabel.font = placeHolderFont;
self.textInput.placeholderLabel.textColor = self.inlinePlaceholderColor;
self.textInput.placeholderLabel.backgroundColor = self.backgroundColor;
}
#pragma mark - Trailing Label Customization
- (void)updateTrailingUnderlineLabel {
if (!self.characterCountMax) {
self.textInput.trailingUnderlineLabel.text = nil;
} else {
self.textInput.trailingUnderlineLabel.text = [self characterCountText];
UIFont *font = self.trailingUnderlineLabelFont;
if (self.mdc_adjustsFontForContentSizeCategory) {
// TODO: (#4331) This needs to be converted to the new text scheme.
font = [font mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleCaption
scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
}
self.textInput.trailingUnderlineLabel.font = font;
}
UIColor *textColor = self.trailingUnderlineLabelTextColor;
if (self.isDisplayingCharacterCountError || self.isDisplayingErrorText) {
textColor = self.errorColor;
}
switch (self.characterCountViewMode) {
case UITextFieldViewModeAlways:
break;
case UITextFieldViewModeWhileEditing:
textColor = !self.textInput.isEditing ? [UIColor clearColor] : textColor;
break;
case UITextFieldViewModeUnlessEditing:
textColor = self.textInput.isEditing ? [UIColor clearColor] : textColor;
break;
case UITextFieldViewModeNever:
textColor = [UIColor clearColor];
break;
}
self.textInput.trailingUnderlineLabel.textColor = textColor;
self.textInput.trailingUnderlineLabel.backgroundColor = self.backgroundColor;
}
- (NSString *)characterCountText {
// TODO: (larche) Localize
return [NSString stringWithFormat:@"%lu / %lu", (unsigned long)[self characterCount],
(unsigned long)self.characterCountMax];
}
#pragma mark - Underline Customization
- (void)updateUnderline {
// Hide the underline.
self.textInput.underline.color = [UIColor clearColor];
}
#pragma mark - Underline Labels Fonts
+ (UIFont *)placeholderFont {
// TODO: (#4331) This needs to be converted to the new text scheme.
return [UIFont mdc_standardFontForMaterialTextStyle:MDCFontTextStyleBody1];
}
+ (UIFont *)underlineLabelsFont {
// TODO: (#4331) This needs to be converted to the new text scheme.
return [UIFont mdc_standardFontForMaterialTextStyle:MDCFontTextStyleCaption];
}
#pragma mark - Properties Implementation
// The underline is never shown in this style.
- (void)setActiveColor:(__unused UIColor *)activeColor {
[self updateUnderline];
}
- (UIColor *)activeColor {
return [UIColor clearColor];
}
+ (UIColor *)activeColorDefault {
return [UIColor clearColor];
}
+ (void)setActiveColorDefault:(__unused UIColor *)activeColorDefault {
// Not implemented. Underline is always clear.
}
- (UIColor *)backgroundColor {
if (!_backgroundColor) {
_backgroundColor = [self class].backgroundColorDefault;
}
return _backgroundColor;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor {
if (_backgroundColor != backgroundColor) {
_backgroundColor = backgroundColor ?: [self class].backgroundColorDefault;
}
}
+ (UIColor *)backgroundColorDefault {
if (!_backgroundColorDefault) {
_backgroundColorDefault = [UIColor clearColor];
}
return _backgroundColorDefault;
}
+ (void)setBackgroundColorDefault:(UIColor *)backgroundColorDefault {
_backgroundColorDefault = backgroundColorDefault ?: [UIColor clearColor];
}
- (void)setCharacterCountViewMode:(UITextFieldViewMode)characterCountViewMode {
if (_characterCountViewMode != characterCountViewMode) {
_characterCountViewMode = characterCountViewMode;
[self updateLayout];
}
}
- (void)setDisabledColor:(__unused UIColor *)disabledColor {
[self updateUnderline];
}
- (UIColor *)disabledColor {
return [UIColor clearColor];
}
+ (void)setDisabledColorDefault:(__unused UIColor *)disabledColorDefault {
// This controller does not have decorations that need to change for a disabled state.
}
+ (UIColor *)disabledColorDefault {
return [UIColor clearColor];
}
- (void)setErrorAccessibilityValue:(NSString *)errorAccessibilityValue {
_errorAccessibilityValue = [errorAccessibilityValue copy];
}
- (void)setHelperAccessibilityLabel:(NSString *)helperAccessibilityLabel {
_helperAccessibilityLabel = [helperAccessibilityLabel copy];
if ([self.textInput.leadingUnderlineLabel.text isEqualToString:self.helperText]) {
self.textInput.leadingUnderlineLabel.accessibilityLabel = _helperAccessibilityLabel;
}
}
- (UIColor *)errorColor {
if (!_errorColor) {
_errorColor = [self class].errorColorDefault;
}
return _errorColor;
}
- (void)setErrorColor:(UIColor *)errorColor {
if (![_errorColor isEqual:errorColor]) {
_errorColor = errorColor ? errorColor : [self class].errorColorDefault;
if (self.isDisplayingCharacterCountError || self.isDisplayingErrorText) {
[self updateLeadingUnderlineLabel];
[self updatePlaceholder];
[self updateTrailingUnderlineLabel];
[self updateUnderline];
}
}
}
+ (UIColor *)errorColorDefault {
if (!_errorColorDefault) {
_errorColorDefault = MDCTextInputControllerFullWidthErrorColorDefault();
}
return _errorColorDefault;
}
+ (void)setErrorColorDefault:(UIColor *)errorColorDefault {
_errorColorDefault =
errorColorDefault ? errorColorDefault : MDCTextInputControllerFullWidthErrorColorDefault();
}
- (void)setErrorText:(NSString *)errorText {
_errorText = [errorText copy];
}
- (void)setHelperText:(NSString *)helperText {
if (self.isDisplayingErrorText) {
self.previousLeadingText = helperText;
} else {
if (![self.textInput.leadingUnderlineLabel.text isEqualToString:helperText]) {
self.textInput.leadingUnderlineLabel.text = helperText;
[self updateLayout];
}
}
}
- (NSString *)helperText {
if (self.isDisplayingErrorText) {
return self.previousLeadingText;
} else {
return self.textInput.leadingUnderlineLabel.text;
}
}
- (void)setInlinePlaceholderColor:(UIColor *)inlinePlaceholderColor {
if (![_inlinePlaceholderColor isEqual:inlinePlaceholderColor]) {
_inlinePlaceholderColor = inlinePlaceholderColor;
[self updatePlaceholder];
}
}
- (UIColor *)inlinePlaceholderColor {
return _inlinePlaceholderColor ? _inlinePlaceholderColor
: [self class].inlinePlaceholderColorDefault;
}
+ (UIColor *)inlinePlaceholderColorDefault {
if (!_inlinePlaceholderColorDefault) {
_inlinePlaceholderColorDefault =
MDCTextInputControllerFullWidthInlinePlaceholderTextColorDefault();
}
return _inlinePlaceholderColorDefault;
}
+ (void)setInlinePlaceholderColorDefault:(UIColor *)inlinePlaceholderColorDefault {
_inlinePlaceholderColorDefault =
inlinePlaceholderColorDefault
? inlinePlaceholderColorDefault
: MDCTextInputControllerFullWidthInlinePlaceholderTextColorDefault();
}
- (UIFont *)inlinePlaceholderFont {
return _inlinePlaceholderFont ?: [self class].inlinePlaceholderFontDefault;
}
- (void)setInlinePlaceholderFont:(UIFont *)inlinePlaceholderFont {
if (![_inlinePlaceholderFont isEqual:inlinePlaceholderFont]) {
_inlinePlaceholderFont = inlinePlaceholderFont;
[self updateLayout];
}
}
+ (UIFont *)inlinePlaceholderFontDefault {
return _inlinePlaceholderFontDefault ?: [[self class] placeholderFont];
}
+ (void)setInlinePlaceholderFontDefault:(UIFont *)inlinePlaceholderFontDefault {
_inlinePlaceholderFontDefault = inlinePlaceholderFontDefault;
}
- (BOOL)isDisplayingCharacterCountError {
return self.characterCountMax && [self characterCount] > self.characterCountMax;
}
- (BOOL)isDisplayingErrorText {
return self.errorText != nil;
}
- (UIFont *)leadingUnderlineLabelFont {
// Not implemented. The leading underline label is never seen.
return [[self class] leadingUnderlineLabelFontDefault];
}
- (void)setLeadingUnderlineLabelFont:(__unused UIColor *)leadingUnderlineLabelFont {
// Not implemented. The leading underline label is never seen.
}
+ (UIFont *)leadingUnderlineLabelFontDefault {
// Implemented only for protocol conformance. The leading underline label is never seen.
return [[self class] underlineLabelsFont];
}
+ (void)setLeadingUnderlineLabelFontDefault:(__unused UIFont *)leadingUnderlineLabelFontDefault {
// Not implemented. The leading underline label is never seen.
}
// The leading underline label must always be clear to not obstruct the placeholder and input.
- (UIColor *)leadingUnderlineLabelTextColor {
return [UIColor clearColor];
}
- (void)setLeadingUnderlineLabelTextColor:(__unused UIColor *)leadingUnderlineLabelTextColor {
// Not implemented. Leading underline label is always clear.
}
+ (UIColor *)leadingUnderlineLabelTextColorDefault {
return [UIColor clearColor];
}
+ (void)setLeadingUnderlineLabelTextColorDefault:
(__unused UIColor *)leadingUnderlineLabelTextColorDefault {
// Not implemented. Leading underline label is always clear.
}
// The underline is never shown in this style.
- (void)setNormalColor:(__unused UIColor *)normalColor {
[self updateUnderline];
}
- (UIColor *)normalColor {
return [UIColor clearColor];
}
+ (void)setNormalColorDefault:(__unused UIColor *)normalColorDefault {
// Not implemented. Underline is always clear.
}
+ (UIColor *)normalColorDefault {
return [UIColor clearColor];
}
- (NSString *)placeholderText {
return _textInput.placeholder;
}
- (void)setPlaceholderText:(NSString *)placeholderText {
if ([_textInput.placeholder isEqualToString:placeholderText]) {
return;
}
_textInput.placeholder = [placeholderText copy];
}
- (void)setPreviousLeadingText:(NSString *)previousLeadingText {
_previousLeadingText = [previousLeadingText copy];
}
- (void)setPreviousPlaceholderColor:(UIColor *)previousPlaceholderColor {
_previousPlaceholderColor = previousPlaceholderColor;
}
- (UIRectCorner)roundedCorners {
return 0;
}
- (void)setRoundedCorners:(__unused UIRectCorner)roundedCorners {
// Not implemented. There are no corners to round.
}
+ (UIRectCorner)roundedCornersDefault {
return 0;
}
+ (void)setRoundedCornersDefault:(__unused UIRectCorner)roundedCornersDefault {
// Not implemented. There are no corners to round.
}
- (void)setTextInput:(UIView<MDCTextInput> *)textInput {
if (_textInput != textInput) {
[self unsubscribeFromNotifications];
_textInput = textInput;
[self setupInput];
}
}
- (UIFont *)textInputFont {
if (_textInputFont) {
return _textInputFont;
}
return [self class].textInputFontDefault ?: self.textInput.font;
}
- (void)setTextInputFont:(UIFont *)textInputFont {
if (![_textInputFont isEqual:textInputFont]) {
_textInputFont = textInputFont;
[self updateLayout];
}
}
+ (UIFont *)textInputFontDefault {
return _textInputFontDefault;
}
+ (void)setTextInputFontDefault:(UIFont *)textInputFontDefault {
_textInputFontDefault = textInputFontDefault;
}
- (UIColor *)textInputClearButtonTintColor {
if (_textInputClearButtonTintColor) {
return _textInputClearButtonTintColor;
}
return [self class].textInputClearButtonTintColorDefault ?: self.textInput.clearButton.tintColor;
}
- (void)setTextInputClearButtonTintColor:(UIColor *)textInputClearButtonTintColor {
_textInputClearButtonTintColor = textInputClearButtonTintColor;
_textInput.clearButton.tintColor = _textInputClearButtonTintColor;
}
+ (UIColor *)textInputClearButtonTintColorDefault {
return _textInputClearButtonTintColorDefault;
}
+ (void)setTextInputClearButtonTintColorDefault:(UIColor *)textInputClearButtonTintColorDefault {
_textInputClearButtonTintColorDefault = textInputClearButtonTintColorDefault;
}
- (UIFont *)trailingUnderlineLabelFont {
return _trailingUnderlineLabelFont ?: [self class].trailingUnderlineLabelFontDefault;
}
- (void)setTrailingUnderlineLabelFont:(UIFont *)trailingUnderlineLabelFont {
if (![_trailingUnderlineLabelFont isEqual:trailingUnderlineLabelFont]) {
_trailingUnderlineLabelFont = trailingUnderlineLabelFont;
[self updateLayout];
}
}
+ (UIFont *)trailingUnderlineLabelFontDefault {
return _trailingUnderlineLabelFontDefault ?: [[self class] underlineLabelsFont];
}
+ (void)setTrailingUnderlineLabelFontDefault:(UIFont *)trailingUnderlineLabelFontDefault {
_trailingUnderlineLabelFontDefault = trailingUnderlineLabelFontDefault;
}
- (UIColor *)trailingUnderlineLabelTextColor {
return _trailingUnderlineLabelTextColor ? _trailingUnderlineLabelTextColor
: [self class].trailingUnderlineLabelTextColorDefault;
}
- (void)setTrailingUnderlineLabelTextColor:(UIColor *)trailingUnderlineLabelTextColor {
if (_trailingUnderlineLabelTextColor != trailingUnderlineLabelTextColor) {
_trailingUnderlineLabelTextColor = trailingUnderlineLabelTextColor
? trailingUnderlineLabelTextColor
: [self class].trailingUnderlineLabelTextColorDefault;
[self updateTrailingUnderlineLabel];
}
}
+ (UIColor *)trailingUnderlineLabelTextColorDefault {
if (!_trailingUnderlineLabelTextColorDefault) {
_trailingUnderlineLabelTextColorDefault =
MDCTextInputControllerFullWidthInlinePlaceholderTextColorDefault();
}
return _trailingUnderlineLabelTextColorDefault;
}
+ (void)setTrailingUnderlineLabelTextColorDefault:
(UIColor *)trailingUnderlineLabelTextColorDefault {
_trailingUnderlineLabelTextColorDefault =
trailingUnderlineLabelTextColorDefault
? trailingUnderlineLabelTextColorDefault
: MDCTextInputControllerFullWidthInlinePlaceholderTextColorDefault();
}
- (CGFloat)underlineHeightActive {
return 0;
}
- (void)setUnderlineHeightActive:(__unused CGFloat)underlineHeightActive {
// Not implemented. Underline is never shown.
}
+ (CGFloat)underlineHeightActiveDefault {
return 0;
}
+ (void)setUnderlineHeightActiveDefault:(__unused CGFloat)underlineHeightActiveDefault {
// Not implemented. Underline is never shown.
}
- (CGFloat)underlineHeightNormal {
return 0;
}
- (void)setUnderlineHeightNormal:(__unused CGFloat)underlineHeightNormal {
// Not implemented. Underline is never shown.
}
+ (CGFloat)underlineHeightNormalDefault {
return 0;
}
+ (void)setUnderlineHeightNormalDefault:(__unused CGFloat)underlineHeightNormalDefault {
// Not implemented. Underline is never shown.
}
- (UITextFieldViewMode)underlineViewMode {
return UITextFieldViewModeNever;
}
- (void)setUnderlineViewMode:(__unused UITextFieldViewMode)underlineViewMode {
[self updateLayout];
}
+ (UITextFieldViewMode)underlineViewModeDefault {
return UITextFieldViewModeNever;
}
+ (void)setUnderlineViewModeDefault:(__unused UITextFieldViewMode)underlineViewModeDefault {
// Not implemented. Underline is never shown.
}
#pragma mark - Layout
- (void)updateLayout {
if (!_textInput) {
return;
}
[self updatePlaceholder];
[self updateLeadingUnderlineLabel];
[self updateTrailingUnderlineLabel];
[self updateTextInput];
[self updateUnderline];
[self updateConstraints];
}
- (void)updateConstraints {
if (!self.characterCountTrailing) {
self.characterCountTrailing = [NSLayoutConstraint
constraintWithItem:self.textInput.trailingUnderlineLabel
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeTrailing
multiplier:1
constant:-1 * MDCTextInputControllerFullWidthHorizontalPadding];
}
if (!self.clearButtonTrailingCharacterCountLeading) {
self.clearButtonTrailingCharacterCountLeading =
[NSLayoutConstraint constraintWithItem:self.textInput.clearButton
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self.textInput.trailingUnderlineLabel
attribute:NSLayoutAttributeLeading
multiplier:1
constant:0];
}
if (!self.placeholderLeading) {
self.placeholderLeading =
[NSLayoutConstraint constraintWithItem:self.textInput.placeholderLabel
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeLeading
multiplier:1
constant:MDCTextInputControllerFullWidthHorizontalPadding];
}
if (!self.placeholderTrailingCharacterCountLeading) {
self.placeholderTrailingCharacterCountLeading = [NSLayoutConstraint
constraintWithItem:self.textInput.placeholderLabel
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationLessThanOrEqual
toItem:self.textInput.trailingUnderlineLabel
attribute:NSLayoutAttributeLeading
multiplier:1
constant:-1 * MDCTextInputControllerFullWidthHorizontalInnerPadding];
}
if (!self.placeholderTrailingSuperviewTrailing) {
self.placeholderTrailingSuperviewTrailing = [NSLayoutConstraint
constraintWithItem:self.textInput.placeholderLabel
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationLessThanOrEqual
toItem:self.textInput
attribute:NSLayoutAttributeTrailing
multiplier:1
constant:-1 * MDCTextInputControllerFullWidthHorizontalPadding];
}
// Multi-line Only
if ([self.textInput isKindOfClass:[MDCMultilineTextField class]]) {
[self.textInput.leadingUnderlineLabel setContentHuggingPriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisVertical];
[self.textInput.leadingUnderlineLabel
setContentCompressionResistancePriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisVertical];
[self.textInput.trailingUnderlineLabel
setContentCompressionResistancePriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisVertical];
if (!self.characterCountY) {
self.characterCountY =
[NSLayoutConstraint constraintWithItem:self.textInput.trailingUnderlineLabel
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:((MDCMultilineTextField *)self.textInput).textView
attribute:NSLayoutAttributeBottom
multiplier:1
constant:0];
}
if (!self.clearButtonY) {
self.clearButtonY =
[NSLayoutConstraint constraintWithItem:self.textInput.clearButton
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.textInput.trailingUnderlineLabel
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0];
}
if (!self.multilineTextViewBottom) {
self.multilineTextViewBottom = [NSLayoutConstraint
constraintWithItem:((MDCMultilineTextField *)self.textInput).textView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeBottom
multiplier:1
constant:-1 * MDCTextInputControllerFullWidthVerticalPadding];
}
if (!self.multilineTextViewTop) {
self.multilineTextViewTop =
[NSLayoutConstraint constraintWithItem:((MDCMultilineTextField *)self.textInput).textView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeTop
multiplier:1
constant:MDCTextInputControllerFullWidthVerticalPadding];
}
if (!self.multilinePlaceholderCenterY) {
self.multilinePlaceholderCenterY =
[NSLayoutConstraint constraintWithItem:self.textInput.placeholderLabel
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:((MDCMultilineTextField *)self.textInput).textView
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0];
}
CGFloat scale = UIScreen.mainScreen.scale;
CGFloat characterCountHeightConstant =
ceil(((MDCMultilineTextField *)self.textInput).textView.font.lineHeight * scale) / scale;
if (!self.multilineCharacterCountHeight) {
self.multilineCharacterCountHeight =
[NSLayoutConstraint constraintWithItem:self.textInput.trailingUnderlineLabel
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1
constant:characterCountHeightConstant];
}
self.multilineCharacterCountHeight.constant = characterCountHeightConstant;
[NSLayoutConstraint activateConstraints:@[
self.multilineTextViewBottom, self.multilineTextViewTop, self.multilinePlaceholderCenterY,
self.multilineCharacterCountHeight
]];
// A height constraint is not necessary for multiline. Its height is calculated in
// intrinsicContentSize:
} else {
// Single-line Only
// .fullWidth
if (!self.characterCountY) {
self.characterCountY =
[NSLayoutConstraint constraintWithItem:self.textInput.trailingUnderlineLabel
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0];
}
if (!self.clearButtonY) {
self.clearButtonY = [NSLayoutConstraint constraintWithItem:self.textInput.clearButton
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0];
}
}
[NSLayoutConstraint activateConstraints:@[
self.characterCountY, self.characterCountTrailing,
self.clearButtonTrailingCharacterCountLeading, self.clearButtonY, self.placeholderLeading,
self.placeholderTrailingCharacterCountLeading, self.placeholderTrailingSuperviewTrailing
]];
[self.textInput.trailingUnderlineLabel setContentHuggingPriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisVertical];
}
#pragma mark - MDCTextFieldPositioningDelegate
// clang-format off
/**
textInsets: is the source of truth for vertical layout. It's used to figure out the proper
height and also where to place the placeholder / text field.
NOTE: It's applied before the textRect is flipped for RTL. So all calculations are done here à la
LTR.
The vertical layout is, simply:
MDCTextInputControllerFullWidthVerticalPadding // Top padding
rint(MAX(self.textInput.font.lineHeight, // Text field or placeholder
self.textInput.placeholderLabel.font.lineHeight))
MDCTextInputControllerFullWidthVerticalPadding // Bottom padding
*/
// clang-format on
- (UIEdgeInsets)textInsets:(__unused UIEdgeInsets)defaultInsets
withSizeThatFitsWidthHint:(CGFloat)widthHint {
// NOTE: UITextFields have a centerY based layout. But you can change EITHER the height or the Y.
// Not both. Don't know why. So, we have to leave the text rect as big as the bounds and move it
// to a Y that works. In other words, no bottom inset will make a difference here for UITextFields
UIEdgeInsets textInsets = UIEdgeInsetsZero;
textInsets.top = MDCTextInputControllerFullWidthVerticalPadding;
textInsets.bottom = MDCTextInputControllerFullWidthVerticalPadding;
textInsets.left = MDCTextInputControllerFullWidthHorizontalPadding;
textInsets.right = MDCTextInputControllerFullWidthHorizontalPadding;
// The trailing label gets in the way. If it has a frame, it's used. But if not, an
// estimate is made of the size the text will be.
if (CGRectGetWidth(self.textInput.trailingUnderlineLabel.frame) > 1) {
textInsets.right += ceil(CGRectGetWidth(self.textInput.trailingUnderlineLabel.frame));
} else if (self.characterCountMax) {
CGRect charCountRect = [[self characterCountText]
boundingRectWithSize:self.textInput.bounds.size
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{NSFontAttributeName : self.textInput.trailingUnderlineLabel.font}
context:nil];
textInsets.right += ceil(CGRectGetWidth(charCountRect));
}
return textInsets;
}
- (CGRect)editingRectForBounds:(__unused CGRect)bounds defaultRect:(CGRect)defaultRect {
if (![self.textInput isKindOfClass:[UITextField class]]) {
return CGRectZero;
}
MDCTextField *textField = (MDCTextField *)self.textInput;
CGRect editingRect = defaultRect;
// Full width text fields have their clear button in the horizontal margin, but because the
// internal implementation of textRect calls [super clearButtonRectForBounds:] in its
// implementation, our modifications are not picked up. Adjust accordingly.
// Full width text fields also have their character count on the text input line.
if (self.textInput.text.length > 0) {
switch (textField.clearButtonMode) {
case UITextFieldViewModeWhileEditing:
editingRect.size.width -= CGRectGetWidth(self.textInput.clearButton.bounds);
case UITextFieldViewModeUnlessEditing:
// The 'defaultRect' is based on the textInsets so we need to compensate for
// the button NOT being there.
editingRect.size.width += CGRectGetWidth(self.textInput.clearButton.bounds);
editingRect.size.width -= MDCTextInputControllerFullWidthHorizontalInnerPadding;
break;
default:
break;
}
}
return editingRect;
}
- (UIEdgeInsets)textInsets:(UIEdgeInsets)defaultInsets {
return [self textInsets:defaultInsets withSizeThatFitsWidthHint:0];
}
#pragma mark - UITextField & UITextView Notification Observation
- (void)textInputDidBeginEditing:(__unused NSNotification *)note {
[self updateLayout];
if (self.characterCountMax > 0) {
NSString *announcementString;
if (!announcementString.length) {
announcementString = [NSString
stringWithFormat:@"%lu character limit.", (unsigned long)self.characterCountMax];
}
// Simply sending a layout change notification does not seem to
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementString);
}
}
- (void)textInputDidChange:(__unused NSNotification *)note {
[self updateLayout];
// Accessibility
if (self.textInput.isEditing && self.characterCountMax > 0) {
NSString *announcementString;
if (!announcementString.length) {
announcementString =
[NSString stringWithFormat:@"%lu of %lu characters",
(unsigned long)[self.characterCounter
characterCountForTextInput:self.textInput],
(unsigned long)self.characterCountMax];
}
// Simply sending a layout change notification does not seem to
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementString);
}
}
- (void)textInputDidEndEditing:(__unused NSNotification *)note {
[self updateLayout];
}
#pragma mark - Public API
- (void)setErrorText:(NSString *)errorText
errorAccessibilityValue:(NSString *)errorAccessibilityValue {
// Turn on error:
//
// Here the 'magic' logic happens for error text.
// When the user sets error text, we save the current state of their underline, leading text,
// trailing text, and placeholder text for both content and color.
if (errorText && !self.isDisplayingErrorText) {
// If we are not in error, but will be, we need to save the existing state.
self.previousLeadingText = self.textInput.leadingUnderlineLabel.text
? self.textInput.leadingUnderlineLabel.text.copy
: @"";
self.textInput.leadingUnderlineLabel.text = errorText;
[self updatePlaceholder];
}
// Change error:
if (errorText && self.isDisplayingErrorText) {
self.textInput.leadingUnderlineLabel.text = errorText;
}
// Turn off error:
//
// If error text is unset (nil) we reset to previous values.
if (!errorText) {
// If there is a saved state, use it.
if (self.previousLeadingText) {
self.textInput.leadingUnderlineLabel.text = self.previousLeadingText;
}
// Clear out saved state.
self.previousLeadingText = nil;
}
self.errorText = errorText;
self.errorAccessibilityValue = errorAccessibilityValue;
[self updateLayout];
// Accessibility
// TODO: (larche) Localize
if (errorText) {
NSString *announcementString = errorAccessibilityValue;
if (!announcementString.length) {
announcementString =
errorText.length > 0 ? [NSString stringWithFormat:@"Error: %@", errorText] : @"Error.";
}
// Simply sending a layout change notification does not seem to
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementString);
NSString *valueString = @"";
if (self.textInput.text.length > 0) {
valueString = [self.textInput.text copy];
}
if (self.textInput.placeholder.length > 0) {
valueString = [NSString stringWithFormat:@"%@. %@", valueString, self.textInput.placeholder];
}
valueString = [valueString stringByAppendingString:@"."];
self.textInput.accessibilityValue = valueString;
NSString *leadingUnderlineLabelText = self.textInput.leadingUnderlineLabel.text;
self.textInput.leadingUnderlineLabel.accessibilityLabel =
[NSString stringWithFormat:@"Error: %@.",
leadingUnderlineLabelText ? leadingUnderlineLabelText : @""];
} else {
self.textInput.accessibilityValue = nil;
if ([self.textInput.leadingUnderlineLabel.text isEqualToString:self.helperText]) {
self.textInput.leadingUnderlineLabel.accessibilityLabel = self.helperAccessibilityLabel;
} else {
self.textInput.leadingUnderlineLabel.accessibilityLabel = nil;
}
}
}
- (void)setHelperText:(NSString *)helperText
helperAccessibilityLabel:(NSString *)helperAccessibilityLabel {
self.helperText = helperText;
self.helperAccessibilityLabel = helperAccessibilityLabel;
}
#pragma mark - Accessibility
- (BOOL)mdc_adjustsFontForContentSizeCategory {
return _mdc_adjustsFontForContentSizeCategory;
}
- (void)mdc_setAdjustsFontForContentSizeCategory:(BOOL)adjusts {
_mdc_adjustsFontForContentSizeCategory = adjusts;
[self updateLayout];
if (_mdc_adjustsFontForContentSizeCategory) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(contentSizeCategoryDidChange:)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
} else {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIContentSizeCategoryDidChangeNotification
object:nil];
}
}
+ (BOOL)mdc_adjustsFontForContentSizeCategoryDefault {
return _mdc_adjustsFontForContentSizeCategoryDefault;
}
+ (void)setMdc_adjustsFontForContentSizeCategoryDefault:
(BOOL)mdc_adjustsFontForContentSizeCategoryDefault {
_mdc_adjustsFontForContentSizeCategoryDefault = mdc_adjustsFontForContentSizeCategoryDefault;
}
- (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification {
[self updateLayout];
}
@end