From 7fd4094424126794efa97b5e43591b2a19e5422c Mon Sep 17 00:00:00 2001 From: David Collazo Date: Fri, 15 Dec 2023 10:59:59 -0800 Subject: [PATCH] Change MDCChipField to use UITextField instead of MDCTextField. PiperOrigin-RevId: 591304366 --- ...hipsFieldDeleteEnabledViewController.swift | 7 +- .../ChipsInputExampleViewController.m | 35 +-- components/Chips/src/MDCChipField.h | 17 +- components/Chips/src/MDCChipField.m | 222 ++++++++++++------ .../Chips/tests/unit/ChipsDelegateTests.m | 18 +- 5 files changed, 180 insertions(+), 119 deletions(-) diff --git a/components/Chips/examples/ChipsFieldDeleteEnabledViewController.swift b/components/Chips/examples/ChipsFieldDeleteEnabledViewController.swift index aef333a3970..fe94780ad9e 100644 --- a/components/Chips/examples/ChipsFieldDeleteEnabledViewController.swift +++ b/components/Chips/examples/ChipsFieldDeleteEnabledViewController.swift @@ -37,9 +37,12 @@ class ChipsFieldDeleteEnabledViewController: UIViewController, MDCChipFieldDeleg view.backgroundColor = containerScheming.colorScheme.backgroundColor chipField.frame = .zero chipField.delegate = self - chipField.textField.placeholderLabel.text = "This is a chip field." + let placeholderAttributes: [NSAttributedString.Key: Any] = [ + .foregroundColor: UIColor.placeholderText + ] + chipField.placeholderAttributes = placeholderAttributes + chipField.placeholder = "This is a chip field." chipField.backgroundColor = containerScheming.colorScheme.surfaceColor - chipField.showChipsDeleteButton = true view.addSubview(chipField) } diff --git a/components/Chips/examples/ChipsInputExampleViewController.m b/components/Chips/examples/ChipsInputExampleViewController.m index 44b3859ce76..f54fd147902 100644 --- a/components/Chips/examples/ChipsInputExampleViewController.m +++ b/components/Chips/examples/ChipsInputExampleViewController.m @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -#import "MaterialChips.h" -#import "MaterialChips+Theming.h" -#import "MaterialTextFields.h" -#import "MaterialColorScheme.h" -#import "MaterialContainerScheme.h" -#import "MaterialTypographyScheme.h" +#import "MDCChipField.h" +#import "MDCChipFieldDelegate.h" +#import "MDCChipView.h" +#import "MDCChipView+MaterialTheming.h" +#import "MDCContainerScheme.h" +#import "MDCTypographyScheme.h" @interface ChipsInputExampleViewController : UIViewController @property(nonatomic, strong) MDCContainerScheme *containerScheme; @@ -42,26 +42,17 @@ - (void)viewDidLoad { typographyScheme.useCurrentContentSizeCategoryWhenApplied = YES; self.containerScheme.typographyScheme = typographyScheme; - if (self.containerScheme.colorScheme) { - self.view.backgroundColor = self.containerScheme.colorScheme.backgroundColor; - } else { - MDCSemanticColorScheme *colorScheme = - [[MDCSemanticColorScheme alloc] initWithDefaults:MDCColorSchemeDefaultsMaterial201804]; - self.view.backgroundColor = colorScheme.backgroundColor; - } + self.view.backgroundColor = UIColor.systemBackgroundColor; self.chipField = [[MDCChipField alloc] initWithFrame:CGRectZero]; self.chipField.delegate = self; self.chipField.textField.accessibilityIdentifier = @"chip_field_text_field"; - self.chipField.textField.placeholderLabel.text = @"This is a chip field."; - self.chipField.textField.mdc_adjustsFontForContentSizeCategory = YES; - if (self.containerScheme.colorScheme) { - self.chipField.backgroundColor = self.containerScheme.colorScheme.surfaceColor; - } else { - MDCSemanticColorScheme *colorScheme = - [[MDCSemanticColorScheme alloc] initWithDefaults:MDCColorSchemeDefaultsMaterial201804]; - self.chipField.backgroundColor = colorScheme.surfaceColor; - } + NSDictionary *placeholderAttributes = + @{NSForegroundColorAttributeName : UIColor.placeholderTextColor}; + self.chipField.placeholderAttributes = placeholderAttributes; + self.chipField.placeholder = @"This is a chip field."; + self.chipField.textField.adjustsFontForContentSizeCategory = YES; + self.chipField.backgroundColor = UIColor.systemBackgroundColor; [self.view addSubview:self.chipField]; // When Dynamic Type changes we need to invalidate the collection view layout in order to let the diff --git a/components/Chips/src/MDCChipField.h b/components/Chips/src/MDCChipField.h index 9e7d745eb1d..2a21c869e3a 100644 --- a/components/Chips/src/MDCChipField.h +++ b/components/Chips/src/MDCChipField.h @@ -16,7 +16,6 @@ #import #import "MDCChipView.h" -#import "MaterialTextFields.h" /** Note: There is a UIKit bug affecting iOS 8.0-8.2 where UITextFields do not properly call the @@ -60,7 +59,7 @@ typedef NS_OPTIONS(NSUInteger, MDCChipFieldDelimiter) { /** This class provides an "input chips" experience on iOS, where chip creation is - coordinated with a user's text input. It manages an @c MDCTextField and a series of @c + coordinated with a user's text input. It manages a @c UITextField and a series of @c MDCChipViews. When the user hits the return key, new chips are added. When the client hits the delete button and the text field has no text, the last chip is deleted. @@ -80,7 +79,7 @@ typedef NS_OPTIONS(NSUInteger, MDCChipFieldDelimiter) { If you set a custom font, make sure to also set the custom font on textField.placeholderLabel and on your MDCChipView instances. */ -@property(nonatomic, nonnull, readonly) MDCTextField *textField; +@property(nonatomic, nonnull, readonly) UITextField *textField; /** The fixed height of all chip views. @@ -142,6 +141,18 @@ typedef NS_OPTIONS(NSUInteger, MDCChipFieldDelimiter) { */ @property(nonatomic, nullable, strong) UIImage *deleteButtonImage; +/** + The string to be used as the attributed placeholder for the UITextField associated + with a chip field. + */ +@property(nonatomic, nullable, copy) NSString *placeholder; + +/** + The attributes applied to the attributed placeholder in the UITextField associated with a + chip field. + */ +@property(nonatomic, nullable, copy) NSDictionary *placeholderAttributes; + /** Adds a chip to the chip field. diff --git a/components/Chips/src/MDCChipField.m b/components/Chips/src/MDCChipField.m index 5895e4f49c2..94e05091a8a 100644 --- a/components/Chips/src/MDCChipField.m +++ b/components/Chips/src/MDCChipField.m @@ -13,21 +13,20 @@ // limitations under the License. #import "MDCChipField.h" +#import #import "MDCChipView.h" #import - #import "MDCChipFieldDelegate.h" #import "MDCChipViewDeleteButton.h" -#import "MDCTextField.h" -#import "MaterialTextFields.h" NSString *const MDCEmptyTextString = @""; NSString *const MDCChipDelimiterSpace = @" "; +NSString *const MDCChipFieldDidSetTextNotification = @"MDCChipFieldDidSetTextNotification"; +static const CGFloat MDCChipFieldDefaultFontSize = 14; static const CGFloat MDCChipFieldHorizontalInset = 15; static const CGFloat MDCChipFieldVerticalInset = 8; -static const CGFloat MDCChipFieldIndent = 4; static const CGFloat MDCChipFieldHorizontalMargin = 8; static const CGFloat MDCChipFieldVerticalMargin = 8; static const UIKeyboardType MDCChipFieldDefaultKeyboardType = UIKeyboardTypeEmailAddress; @@ -39,11 +38,11 @@ @protocol MDCChipFieldTextFieldDelegate -- (void)textFieldShouldRespondToDeleteBackward:(UITextField *)textField; +- (void)textFieldDidDelete:(UITextField *)textField; @end -@interface MDCChipFieldTextField : MDCTextField +@interface MDCChipFieldTextField : UITextField @property(nonatomic, weak) id deletionDelegate; @@ -51,71 +50,73 @@ @interface MDCChipFieldTextField : MDCTextField @implementation MDCChipFieldTextField +const UIEdgeInsets MDCChipFieldTextFieldRTLEdgeInsets = {16, 0, 16, 4}; + +const UIEdgeInsets MDCChipFieldTextFieldLTREdgeInsets = {16, 4, 16, 0}; + +- (BOOL)isRTL { + return self.effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft; +} + +- (UIEdgeInsets)textEdgeInsets { + return [self isRTL] ? MDCChipFieldTextFieldRTLEdgeInsets : MDCChipFieldTextFieldLTREdgeInsets; +} + - (CGRect)textRectForBounds:(CGRect)bounds { - CGRect textRect = [super textRectForBounds:bounds]; - if (self.effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) { - textRect = MDFRectFlippedHorizontally(textRect, CGRectGetWidth(self.bounds)); - textRect.origin.x += 5; - } - return textRect; + return [super textRectForBounds:UIEdgeInsetsInsetRect(bounds, [self textEdgeInsets])]; +} + +- (CGRect)editingRectForBounds:(CGRect)bounds { + return [super textRectForBounds:UIEdgeInsetsInsetRect(bounds, [self textEdgeInsets])]; +} + +- (CGRect)placeholderRectForBounds:(CGRect)bounds { + return [super textRectForBounds:UIEdgeInsetsInsetRect(bounds, [self textEdgeInsets])]; } #pragma mark UIKeyInput - (void)deleteBackward { - if (self.text.length == 0) { - [self.deletionDelegate textFieldShouldRespondToDeleteBackward:self]; + if ([self.delegate respondsToSelector:@selector(textFieldDidDelete:)] && self.text.length == 0) { + [self.deletionDelegate textFieldDidDelete:self]; } [super deleteBackward]; } -#if MDC_CHIPFIELD_PRIVATE_API_BUG_FIX && \ - !(defined(__IPHONE_8_3) && (__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_3)) - -// WARNING: This is a private method, see the warning in MDCChipField.h. -// This is only compiled if you explicitly defined MDC_CHIPFIELD_PRIVATE_API_BUG_FIX yourself, and -// you are targeting an iOS version less than 8.3. -- (BOOL)keyboardInputShouldDelete:(UITextField *)textField { - BOOL shouldDelete = YES; - if ([UITextField instancesRespondToSelector:_cmd]) { - // clang-format off - BOOL (*keyboardInputShouldDelete)(id, SEL, UITextField *) = - (BOOL(*)(id, SEL, UITextField *))[UITextField instanceMethodForSelector:_cmd]; - // clang-format on - if (keyboardInputShouldDelete) { - shouldDelete = keyboardInputShouldDelete(self, _cmd, textField); - NSOperatingSystemVersion minimumVersion = {8, 0, 0}; - NSOperatingSystemVersion maximumVersion = {8, 3, 0}; - NSProcessInfo *processInfo = [NSProcessInfo processInfo]; - BOOL isIos8 = [processInfo isOperatingSystemAtLeastVersion:minimumVersion]; - BOOL isLessThanIos8_3 = ![processInfo isOperatingSystemAtLeastVersion:maximumVersion]; - if (![textField.text length] && isIos8 && isLessThanIos8_3) { - [self deleteBackward]; - } - } - } - return shouldDelete; +- (CGRect)accessibilityFrame { + CGRect frame = [super accessibilityFrame]; + UIEdgeInsets textEdgeInsets = [self textEdgeInsets]; + return CGRectMake(frame.origin.x + textEdgeInsets.left, frame.origin.y, + frame.size.width - textEdgeInsets.left, frame.size.height); } -#endif +- (void)setText:(NSString *)text { + [super setText:text]; -#pragma mark - UIAccessibility + if (!self.isFirstResponder) { + [[NSNotificationCenter defaultCenter] postNotificationName:MDCChipFieldDidSetTextNotification + object:self]; + } +} -- (CGRect)accessibilityFrame { - CGRect frame = [super accessibilityFrame]; - return CGRectMake(frame.origin.x + self.textInsets.left, frame.origin.y, - frame.size.width - self.textInsets.left, frame.size.height); +- (void)setAttributedText:(NSAttributedString *)attributedText { + [super setAttributedText:attributedText]; + + if (!self.isFirstResponder) { + [[NSNotificationCenter defaultCenter] postNotificationName:MDCChipFieldDidSetTextNotification + object:self]; + } } @end -@interface MDCChipField () +@interface MDCChipField () @end @implementation MDCChipField { NSMutableArray *_chips; + NSAttributedString *_attributedPlaceholder; + NSAttributedString *_emptyAttributedString; } - (instancetype)initWithFrame:(CGRect)frame { @@ -125,12 +126,17 @@ - (instancetype)initWithFrame:(CGRect)frame { _chips = [NSMutableArray array]; + _emptyAttributedString = [[NSAttributedString alloc] initWithString:@"" + attributes:_placeholderAttributes]; + MDCChipFieldTextField *chipFieldTextField = [[MDCChipFieldTextField alloc] initWithFrame:self.bounds]; - chipFieldTextField.underline.hidden = YES; + chipFieldTextField.adjustsFontForContentSizeCategory = YES; + UIFont *defaultFont = [UIFont systemFontOfSize:MDCChipFieldDefaultFontSize]; + UIFont *scaledDefaultFont = [UIFontMetrics.defaultMetrics scaledFontForFont:defaultFont]; + chipFieldTextField.font = scaledDefaultFont; chipFieldTextField.delegate = self; chipFieldTextField.deletionDelegate = self; - chipFieldTextField.positioningDelegate = self; chipFieldTextField.accessibilityTraits = UIAccessibilityTraitNone; chipFieldTextField.autocorrectionType = UITextAutocorrectionTypeNo; chipFieldTextField.autocapitalizationType = UITextAutocapitalizationTypeNone; @@ -143,9 +149,10 @@ - (instancetype)initWithFrame:(CGRect)frame { // Also listen for notifications posted when the text field is not the first responder. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldDidChange) - name:MDCTextFieldTextDidSetTextNotification + name:MDCChipFieldDidSetTextNotification object:chipFieldTextField]; [self addSubview:chipFieldTextField]; + [self updateTextFieldPlaceholderText]; _textField = chipFieldTextField; } return self; @@ -155,6 +162,7 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self commonMDCChipFieldInit]; + [self updateTextFieldPlaceholderText]; } return self; } @@ -198,7 +206,6 @@ - (void)layoutSubviews { BOOL heightChanged = CGRectGetMinY(textFieldFrame) != CGRectGetMinY(self.textField.frame); self.textField.frame = textFieldFrame; - [self updateTextFieldPlaceholderText]; [self invalidateIntrinsicContentSize]; if (heightChanged && [self.delegate respondsToSelector:@selector(chipFieldHeightDidChange:)]) { @@ -207,13 +214,15 @@ - (void)layoutSubviews { } - (void)updateTextFieldPlaceholderText { - // Place holder label should be hidden if showPlaceholderWithChips is NO and there are chips. - // MDCTextField sets the placeholderLabel opacity to 0 if the text field has no text. - self.textField.placeholderLabel.hidden = (!self.showPlaceholderWithChips && self.chips.count > 0); -} - -+ (UIFont *)textFieldFont { - return [UIFont systemFontOfSize:[UIFont systemFontSize]]; + // There should be no placeholder if showPlaceholderWithChips is NO and there are chips. + if (!self.showPlaceholderWithChips && self.chips.count > 0) { + self.textField.attributedPlaceholder = _emptyAttributedString; + } else if (_attributedPlaceholder) { + self.textField.attributedPlaceholder = _attributedPlaceholder; + } else { + self.textField.placeholder = _placeholder; + } + [self setNeedsLayout]; } - (CGSize)intrinsicContentSize { @@ -223,6 +232,53 @@ - (CGSize)intrinsicContentSize { return [self sizeThatFits:CGSizeMake(minWidth, CGFLOAT_MAX)]; } +- (void)setPlaceholder:(NSString *)string { + if (string == nil || string.length == 0) { + _placeholder = nil; + _attributedPlaceholder = nil; + } else { + _placeholder = [string copy]; + // If `placeholderAttributes` is nil, create and set one with a default color. + if (!_placeholderAttributes) { + _placeholderAttributes = @{NSForegroundColorAttributeName : UIColor.placeholderTextColor}; + } + + NSAttributedString *attributedPlaceholder = + [[NSAttributedString alloc] initWithString:string attributes:_placeholderAttributes]; + _attributedPlaceholder = attributedPlaceholder; + } + + [self updateTextFieldPlaceholderText]; +} + +- (void)setPlaceholderAttributes:(NSDictionary *)placeholderAttributes { + // If `placeholderAttributes` parameter is not nil, make a mutable copy of it. + if (placeholderAttributes) { + NSMutableDictionary *mutableAttributes = + [placeholderAttributes mutableCopy]; + // If no font name is passed in, use the current text field's font name. + // Other key:value pairs are overwritten by the new placeholder attributes. + if (!mutableAttributes[NSFontAttributeName]) { + mutableAttributes[NSFontAttributeName] = _textField.font; + } + _placeholderAttributes = mutableAttributes; + } else { + _placeholderAttributes = @{NSForegroundColorAttributeName : UIColor.placeholderTextColor}; + } + + if (_placeholder) { + NSAttributedString *attributedPlaceholder = + [[NSAttributedString alloc] initWithString:_placeholder attributes:_placeholderAttributes]; + + _attributedPlaceholder = attributedPlaceholder; + } + + _emptyAttributedString = [[NSAttributedString alloc] initWithString:@"" + attributes:_placeholderAttributes]; + + [self updateTextFieldPlaceholderText]; +} + - (CGSize)sizeThatFits:(CGSize)size { NSArray *chipFrames = [self chipFramesForSize:size]; CGRect lastChipFrame = [chipFrames.lastObject CGRectValue]; @@ -286,6 +342,7 @@ - (void)addChip:(MDCChipView *)chip { [self.delegate chipField:self didAddChip:chip]; } + [self updateTextFieldPlaceholderText]; [self.textField setNeedsLayout]; [self setNeedsLayout]; } @@ -296,6 +353,7 @@ - (void)removeChip:(MDCChipView *)chip { if ([self.delegate respondsToSelector:@selector(chipField:didRemoveChip:)]) { [self.delegate chipField:self didRemoveChip:chip]; } + [self updateTextFieldPlaceholderText]; [self.textField setNeedsLayout]; [self setNeedsLayout]; } @@ -416,12 +474,17 @@ - (void)chipTapped:(id)sender { #pragma mark - MDCChipFieldTextFieldDelegate -- (void)textFieldShouldRespondToDeleteBackward:(UITextField *)textField { - if ([self isAnyChipSelected]) { - [self removeSelectedChips]; - [self deselectAllChips]; - } else { - [self selectLastChip]; +- (void)textFieldDidDelete:(UITextField *)textField { + // If backspacing on an empty text field without a chip selected, select the last chip. + // If backspacing on an empty text field with a selected chip, delete the selected chip. + if (textField.text.length == 0) { + if ([self isAnyChipSelected]) { + [self removeSelectedChips]; + [self deselectAllChips]; + [self updateTextFieldPlaceholderText]; + } else { + [self selectLastChip]; + } } } @@ -502,6 +565,10 @@ - (void)textFieldDidChange { if ([self.delegate respondsToSelector:@selector(chipField:didChangeInput:)]) { [self.delegate chipField:self didChangeInput:[self.textField.text copy]]; } + + if (_textField.text.length == 0) { + [self updateTextFieldPlaceholderText]; + } } #pragma mark - Private @@ -627,7 +694,7 @@ - (CGFloat)textInputDesiredWidth { return placeholderDesiredWidth; } - UIFont *font = self.textField.placeholderLabel.font; + UIFont *font = self.textField.font; CGRect desiredRect = [self.textField.text boundingRectWithSize:CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric) options:NSStringDrawingUsesLineFragmentOrigin @@ -650,7 +717,12 @@ - (CGFloat)placeholderDesiredWidth { return self.minTextFieldWidth; } - UIFont *placeholderFont = self.textField.placeholderLabel.font; + UIFont *placeholderFont = _placeholderAttributes[NSFontAttributeName]; + + if (!placeholderFont) { + placeholderFont = self.textField.font; + } + CGRect placeholderDesiredRect = [placeholder boundingRectWithSize:CGRectStandardize(self.bounds).size options:NSStringDrawingUsesLineFragmentOrigin @@ -663,14 +735,6 @@ - (CGFloat)placeholderDesiredWidth { return MAX(placeholderDesiredWidth, self.minTextFieldWidth); } -#pragma mark - MDCTextInputPositioningDelegate - -- (UIEdgeInsets)textInsets:(UIEdgeInsets)defaultInsets - withSizeThatFitsWidthHint:(CGFloat)widthHint { - defaultInsets.left = MDCChipFieldIndent; - return defaultInsets; -} - #pragma mark - UIAccessibilityContainer - (BOOL)isAccessibilityElement { @@ -705,4 +769,10 @@ - (void)focusTextFieldForAccessibility { UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self.textField); } +- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { + [super traitCollectionDidChange:previousTraitCollection]; + [self.textField setNeedsLayout]; + [self setNeedsLayout]; +} + @end diff --git a/components/Chips/tests/unit/ChipsDelegateTests.m b/components/Chips/tests/unit/ChipsDelegateTests.m index 4e481043a5b..69fcc563424 100644 --- a/components/Chips/tests/unit/ChipsDelegateTests.m +++ b/components/Chips/tests/unit/ChipsDelegateTests.m @@ -15,9 +15,8 @@ #import #import -#import "MaterialChips.h" -#import "MaterialTextFields.h" -#import "MDCTextField+Testing.h" +#import "MDCChipField.h" +#import "MDCChipFieldDelegate.h" @interface ChipsDelegateTests : XCTestCase @@ -60,19 +59,6 @@ - (void)testSettingTextInvokesDidChangeInputOnDelegate { XCTAssertEqualObjects(self.delegateTextInput, @"Hello World"); } -- (void)testTouchUpOnClearButtonInvokesDidChangeInputOnDelegate { - // Given - self.chip.textField.text = @"Hello World"; - - // When - [self.chip.textField clearButtonDidTouch]; - - // Then - // Check length == 0 instead of looking for nil to handle both nil and @"". - // Cast to (unsigned long) to handle 32-bit and 64-bit tests. - XCTAssertEqual((unsigned long)self.delegateTextInput.length, 0UL); -} - - (void)testDelegateShouldNotBeginEditing { // Given self.delegateShouldBeginEditing = NO;