chromium/ios/chrome/browser/shared/ui/table_view/cells/table_view_text_edit_item.mm

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_edit_item.h"

#import "base/notreached.h"
#import "ios/chrome/browser/shared/ui/elements/extended_touch_target_button.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_edit_item_delegate.h"
#import "ios/chrome/browser/shared/ui/table_view/legacy_chrome_table_view_styler.h"
#import "ios/chrome/browser/shared/ui/util/rtl_geometry.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/table_view/table_view_cells_constants.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"

namespace {

// Minimum gap between the label and the text field.
const CGFloat kLabelAndFieldGap = 5;
// Height/width of the edit icon.
const CGFloat kEditIconLength = 18;
// Height/width of the error icon.
const CGFloat kErrorIconLength = 20;
// Size of the symbols.
const CGFloat kSymbolSize = 15;

}  // namespace

@interface TableViewTextEditItem ()

// Whether the field has valid text.
@property(nonatomic, assign) BOOL hasValidText;

@end

@implementation TableViewTextEditItem

- (instancetype)initWithType:(NSInteger)type {
  self = [super initWithType:type];
  if (self) {
    self.cellClass = [TableViewTextEditCell class];
    _returnKeyType = UIReturnKeyNext;
    _keyboardType = UIKeyboardTypeDefault;
    _autoCapitalizationType = UITextAutocapitalizationTypeWords;
    _hasValidText = YES;
  }
  return self;
}

#pragma mark TableViewItem

- (void)configureCell:(TableViewTextEditCell*)cell
           withStyler:(ChromeTableViewStyler*)styler {
  [super configureCell:cell withStyler:styler];

  NSString* textLabelFormat = self.required ? @"%@*" : @"%@";
  cell.textLabel.text =
      [NSString stringWithFormat:textLabelFormat, self.fieldNameLabelText];
  if (self.textFieldPlaceholder) {
    cell.textField.attributedPlaceholder = [[NSAttributedString alloc]
        initWithString:self.textFieldPlaceholder
            attributes:@{
              NSForegroundColorAttributeName :
                  [UIColor colorNamed:kTextSecondaryColor]
            }];
  }
  cell.textField.text = self.textFieldValue;
  cell.textField.secureTextEntry = self.textFieldSecureTextEntry;
  if (self.customTextfieldAccessibilityIdentifier.length) {
    cell.textField.accessibilityIdentifier =
        self.customTextfieldAccessibilityIdentifier;
  } else if (self.fieldNameLabelText.length) {
    cell.textField.accessibilityIdentifier =
        [NSString stringWithFormat:@"%@_textField", self.fieldNameLabelText];
  }

  if (self.textFieldBackgroundColor) {
    cell.textLabel.backgroundColor = self.textFieldBackgroundColor;
    cell.textField.backgroundColor = self.textFieldBackgroundColor;
  } else if (styler.cellBackgroundColor) {
    cell.textLabel.backgroundColor = styler.cellBackgroundColor;
    cell.textField.backgroundColor = styler.cellBackgroundColor;
  } else {
    cell.textLabel.backgroundColor = styler.tableViewBackgroundColor;
    cell.textField.backgroundColor = styler.tableViewBackgroundColor;
  }

  cell.textField.enabled = self.textFieldEnabled;

  if (self.hideIcon) {
    cell.textField.textColor = self.textFieldTextColor
                                   ? self.textFieldTextColor
                                   : [UIColor colorNamed:kTextPrimaryColor];

    [cell setIcon:TableViewTextEditItemIconTypeNone];
  } else {
    if (self.hasValidText) {
      cell.textField.textColor = [UIColor colorNamed:kTextPrimaryColor];
    } else {
      cell.textField.textColor = [UIColor colorNamed:kRedColor];
    }

    if (!self.hasValidText) {
      cell.iconView.accessibilityIdentifier =
          [NSString stringWithFormat:@"%@_errorIcon", self.fieldNameLabelText];
      [cell setIcon:TableViewTextEditItemIconTypeError];
    } else if (cell.textField.editing && cell.textField.text.length > 0) {
      cell.iconView.accessibilityIdentifier =
          [NSString stringWithFormat:@"%@_noIcon", self.fieldNameLabelText];
      [cell setIcon:TableViewTextEditItemIconTypeNone];
    } else {
      cell.iconView.accessibilityIdentifier =
          [NSString stringWithFormat:@"%@_editIcon", self.fieldNameLabelText];
      [cell setIcon:TableViewTextEditItemIconTypeEdit];
    }
  }

  [cell.textField addTarget:self
                     action:@selector(textFieldChanged:)
           forControlEvents:UIControlEventEditingChanged];
  [cell.textField addTarget:self
                     action:@selector(textFieldBeginEditing:)
           forControlEvents:UIControlEventEditingDidBegin];
  [cell.textField addTarget:self
                     action:@selector(textFieldEndEditing:)
           forControlEvents:UIControlEventEditingDidEnd];
  cell.textField.returnKeyType = self.returnKeyType;
  cell.textField.keyboardType = self.keyboardType;
  cell.textField.autocapitalizationType = self.autoCapitalizationType;

  [cell setIdentifyingIcon:self.identifyingIcon];
  cell.identifyingIconButton.enabled = self.identifyingIconEnabled;
  if ([self.identifyingIconAccessibilityLabel length]) {
    cell.identifyingIconButton.accessibilityLabel =
        self.identifyingIconAccessibilityLabel;
  }

  // If the TextField or IconButton are enabled, the cell needs to make its
  // inner TextField or button accessible to voice over. In order to achieve
  // this the cell can't be an A11y element.
  cell.isAccessibilityElement =
      !(self.textFieldEnabled || self.identifyingIconEnabled);
}

#pragma mark Actions

- (void)textFieldChanged:(UITextField*)textField {
  self.textFieldValue = textField.text;
  [self.delegate tableViewItemDidChange:self];
}

- (void)textFieldBeginEditing:(UITextField*)textField {
  [self.delegate tableViewItemDidBeginEditing:self];
}

- (void)textFieldEndEditing:(UITextField*)textField {
  [self.delegate tableViewItemDidEndEditing:self];
}

#pragma mark - Public

- (void)setHasValidText:(BOOL)hasValidText {
  if (_hasValidText == hasValidText) {
    return;
  }
  _hasValidText = hasValidText;
}

@end

#pragma mark - TableViewTextEditCell

@interface TableViewTextEditCell ()

@property(nonatomic, strong) NSLayoutConstraint* iconHeightConstraint;
@property(nonatomic, strong) NSLayoutConstraint* iconWidthConstraint;
@property(nonatomic, strong) NSLayoutConstraint* textFieldTrailingConstraint;
@property(nonatomic, strong) NSLayoutConstraint* textLabelTrailingConstraint;
@property(nonatomic, strong) NSLayoutConstraint* editIconHeightConstraint;
@property(nonatomic, strong) NSLayoutConstraint* iconTrailingConstraint;

// When they are activated, the label and the text field are on one line.
// They conflict with the `accessibilityConstraints`.
@property(nonatomic, strong) NSArray<NSLayoutConstraint*>* standardConstraints;
// When they are activated, the label is on one line, the text field is on
// another line. They conflict with the `standardConstraints`.
@property(nonatomic, strong)
    NSArray<NSLayoutConstraint*>* accessibilityConstraints;

@end

@implementation TableViewTextEditCell

@synthesize textLabel = _textLabel;

- (instancetype)initWithStyle:(UITableViewCellStyle)style
              reuseIdentifier:(NSString*)reuseIdentifier {
  self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
  if (self) {
    self.isAccessibilityElement = YES;
    UIView* contentView = self.contentView;

    _textLabel = [[UILabel alloc] init];
    _textLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [_textLabel setContentHuggingPriority:UILayoutPriorityDefaultHigh
                                  forAxis:UILayoutConstraintAxisHorizontal];
    _textLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
    _textLabel.adjustsFontForContentSizeCategory = YES;
    [contentView addSubview:_textLabel];

    _textField = [[UITextField alloc] init];
    _textField.translatesAutoresizingMaskIntoConstraints = NO;
    _textField.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
    _textField.adjustsFontForContentSizeCategory = YES;
    [_textField
        setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
                                        forAxis:
                                            UILayoutConstraintAxisHorizontal];
    [contentView addSubview:_textField];

    _textField.autocorrectionType = UITextAutocorrectionTypeNo;
    _textField.clearButtonMode = UITextFieldViewModeWhileEditing;
    _textField.contentVerticalAlignment =
        UIControlContentVerticalAlignmentCenter;

    // Trailing icon button.
    _identifyingIconButton =
        [ExtendedTouchTargetButton buttonWithType:UIButtonTypeCustom];
    _identifyingIconButton.translatesAutoresizingMaskIntoConstraints = NO;
    [contentView addSubview:_identifyingIconButton];

    // Edit icon.
    _iconView = [[UIImageView alloc] initWithImage:[self editImage]];
    _iconView.tintColor = [UIColor colorNamed:kGrey400Color];
    _iconView.translatesAutoresizingMaskIntoConstraints = NO;
    [contentView addSubview:_iconView];

    // Set up the icons size constraints. They are activated here and updated in
    // layoutSubviews.
    _iconHeightConstraint =
        [_identifyingIconButton.heightAnchor constraintEqualToConstant:0];
    _iconWidthConstraint =
        [_identifyingIconButton.widthAnchor constraintEqualToConstant:0];
    _editIconHeightConstraint =
        [_iconView.heightAnchor constraintEqualToConstant:0];

    _textFieldTrailingConstraint = [_textField.trailingAnchor
        constraintEqualToAnchor:_iconView.leadingAnchor];
    _textLabelTrailingConstraint = [_textLabel.trailingAnchor
        constraintEqualToAnchor:_iconView.leadingAnchor];
    _iconTrailingConstraint = [_iconView.trailingAnchor
        constraintEqualToAnchor:_identifyingIconButton.leadingAnchor];

    _standardConstraints = @[
      [_textLabel.centerYAnchor
          constraintEqualToAnchor:self.contentView.centerYAnchor],
      [_textField.centerYAnchor
          constraintEqualToAnchor:_textLabel.centerYAnchor],
      [_textField.leadingAnchor
          constraintEqualToAnchor:_textLabel.trailingAnchor
                         constant:kLabelAndFieldGap],
    ];

    _accessibilityConstraints = @[
      [_textField.topAnchor constraintEqualToAnchor:_textLabel.bottomAnchor
                                           constant:kTableViewVerticalSpacing],
      [_textField.leadingAnchor
          constraintEqualToAnchor:contentView.leadingAnchor
                         constant:kTableViewHorizontalSpacing],
      _textLabelTrailingConstraint,
    ];

    // Set up the constraints.
    [NSLayoutConstraint activateConstraints:@[
      [_textLabel.leadingAnchor
          constraintEqualToAnchor:contentView.leadingAnchor
                         constant:kTableViewHorizontalSpacing],
      _textFieldTrailingConstraint,
      [_identifyingIconButton.trailingAnchor
          constraintEqualToAnchor:contentView.trailingAnchor
                         constant:-kTableViewHorizontalSpacing],
      [_identifyingIconButton.centerYAnchor
          constraintEqualToAnchor:contentView.centerYAnchor],
      [_iconView.centerYAnchor
          constraintEqualToAnchor:contentView.centerYAnchor],
      _iconHeightConstraint,
      _iconWidthConstraint,
      _iconTrailingConstraint,
      _editIconHeightConstraint,
      [_iconView.widthAnchor constraintEqualToAnchor:_iconView.heightAnchor],
    ]];
    AddOptionalVerticalPadding(contentView, _textLabel,
                               kTableViewOneLabelCellVerticalSpacing);
    AddOptionalVerticalPadding(contentView, _textField,
                               kTableViewOneLabelCellVerticalSpacing);

    [self updateForAccessibilityContentSizeCategory:
              UIContentSizeCategoryIsAccessibilityCategory(
                  self.traitCollection.preferredContentSizeCategory)];
  }
  return self;
}

#pragma mark Public

- (void)setIcon:(TableViewTextEditItemIconType)iconType {
  self.textFieldTrailingConstraint.constant = -kLabelAndFieldGap;
  self.textLabelTrailingConstraint.constant = -kLabelAndFieldGap;

  switch (iconType) {
    case TableViewTextEditItemIconTypeNone:
      self.iconView.hidden = YES;
      [self.iconView setImage:nil];
      self.textFieldTrailingConstraint.constant = 0;
      self.textLabelTrailingConstraint.constant = 0;

      _editIconHeightConstraint.constant = 0;
      break;
    case TableViewTextEditItemIconTypeEdit:
      self.iconView.hidden = NO;
      [self.iconView setImage:[self editImage]];
      self.iconView.tintColor = [UIColor colorNamed:kGrey400Color];

      _editIconHeightConstraint.constant = kEditIconLength;
      break;
    case TableViewTextEditItemIconTypeError:
      self.iconView.hidden = NO;
      [self.iconView setImage:[self errorImage]];
      self.iconView.tintColor = [UIColor colorNamed:kRedColor];

      _editIconHeightConstraint.constant = kErrorIconLength;
      break;
    default:
      NOTREACHED_IN_MIGRATION();
      break;
  }
}

- (void)setIdentifyingIcon:(UIImage*)icon {
  // Set Image as UIImageRenderingModeAlwaysTemplate to allow the Button tint
  // color to propagate.
  [self.identifyingIconButton
      setImage:[icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]
      forState:UIControlStateNormal];
  // Set the same image for the button's disable state so it's not grayed out
  // when disabled.
  [self.identifyingIconButton setImage:icon forState:UIControlStateDisabled];
  if (icon) {
    self.iconTrailingConstraint.constant = -kLabelAndFieldGap;

    // Set the size constraints of the icon view to the dimensions of the image.
    self.iconHeightConstraint.constant = icon.size.height;
    self.iconWidthConstraint.constant = icon.size.width;
  } else {
    self.iconTrailingConstraint.constant = 0;
    self.iconHeightConstraint.constant = 0;
    self.iconWidthConstraint.constant = 0;
  }
}

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];
  BOOL isCurrentCategoryAccessibility =
      UIContentSizeCategoryIsAccessibilityCategory(
          self.traitCollection.preferredContentSizeCategory);
  if (isCurrentCategoryAccessibility !=
      UIContentSizeCategoryIsAccessibilityCategory(
          previousTraitCollection.preferredContentSizeCategory)) {
    [self updateForAccessibilityContentSizeCategory:
              isCurrentCategoryAccessibility];
  }
}

#pragma mark - UITableViewCell

- (void)prepareForReuse {
  [super prepareForReuse];
  self.textLabel.text = nil;
  self.textField.text = nil;
  self.textField.attributedPlaceholder = nil;
  self.textField.returnKeyType = UIReturnKeyNext;
  self.textField.keyboardType = UIKeyboardTypeDefault;
  self.textField.autocapitalizationType = UITextAutocapitalizationTypeWords;
  self.textField.autocorrectionType = UITextAutocorrectionTypeNo;
  self.textField.clearButtonMode = UITextFieldViewModeWhileEditing;
  self.isAccessibilityElement = YES;
  self.textField.accessibilityIdentifier = nil;
  self.textField.enabled = NO;
  self.textField.delegate = nil;
  self.textField.secureTextEntry = NO;
  [self.textField removeTarget:nil
                        action:nil
              forControlEvents:UIControlEventAllEvents];
  [self setIdentifyingIcon:nil];
  self.identifyingIconButton.enabled = NO;
  [self.identifyingIconButton removeTarget:nil
                                    action:nil
                          forControlEvents:UIControlEventAllEvents];
}

#pragma mark Accessibility

- (NSString*)accessibilityLabel {
  // If `textFieldSecureTextEntry` is
  // YES, the voice over should not read the text value.
  NSString* textFieldText =
      self.textField.secureTextEntry ? @"" : self.textField.text;
  return
      [NSString stringWithFormat:@"%@, %@", self.textLabel.text, textFieldText];
}

#pragma mark Private

// Updates the cell such as it is layouted correctly with regard to the
// preferred content size category, if it is an
// `accessibilityContentSizeCategory` or not.
- (void)updateForAccessibilityContentSizeCategory:
    (BOOL)accessibilityContentSizeCategory {
  if (accessibilityContentSizeCategory) {
    [NSLayoutConstraint deactivateConstraints:_standardConstraints];
    [NSLayoutConstraint activateConstraints:_accessibilityConstraints];
    _textField.textAlignment =
        UseRTLLayout() ? NSTextAlignmentRight : NSTextAlignmentLeft;
  } else {
    [NSLayoutConstraint deactivateConstraints:_accessibilityConstraints];
    [NSLayoutConstraint activateConstraints:_standardConstraints];
    _textField.textAlignment =
        UseRTLLayout() ? NSTextAlignmentLeft : NSTextAlignmentRight;
  }
}

// Returns the edit icon image.
- (UIImage*)editImage {
  return DefaultSymbolWithPointSize(kEditActionSymbol, kSymbolSize);
}

// Returns the error icon image.
- (UIImage*)errorImage {
  return DefaultSymbolWithPointSize(kErrorCircleFillSymbol, kSymbolSize);
}

@end