blob: 052d4db70a73ce04cfcca94d9f5f870d4b6a51b1 [file]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/login/ui/access_code_input.h"
#include <string>
#include <string_view>
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/system_textfield.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_id.h"
#include "ui/compositor/layer.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/gfx/range/range.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
namespace ash {
namespace {
// Identifier for focus traversal.
constexpr int kFixedLengthInputGroup = 1;
constexpr int kAccessCodeFlexLengthWidthDp = 192;
constexpr int kAccessCodeFlexUnderlineThicknessDp = 1;
constexpr int kAccessCodeFontSizeDeltaDp = 4;
constexpr int kObscuredGlyphSpacingDp = 6;
constexpr int kAccessCodeInputFieldWidthDp = 24;
constexpr int kAccessCodeBetweenInputFieldsGapDp = 8;
} // namespace
BEGIN_METADATA(AccessCodeInput)
END_METADATA
FlexCodeInput::FlexCodeInput(OnInputChange on_input_change,
OnEnter on_enter,
OnEscape on_escape,
bool obscure_pin)
: on_input_change_(std::move(on_input_change)),
on_enter_(std::move(on_enter)),
on_escape_(std::move(on_escape)) {
DCHECK(on_input_change_);
DCHECK(on_enter_);
DCHECK(on_escape_);
SetLayoutManager(std::make_unique<views::FillLayout>());
const ui::ColorId input_color_id =
static_cast<ui::ColorId>(cros_tokens::kCrosSysOnSurface);
code_field_ = AddChildView(
std::make_unique<SystemTextfield>(SystemTextfield::Type::kMedium));
code_field_->set_controller(this);
code_field_->SetFontList(views::Textfield::GetDefaultFontList().Derive(
kAccessCodeFontSizeDeltaDp, gfx::Font::FontStyle::NORMAL,
gfx::Font::Weight::NORMAL));
code_field_->SetBorder(views::CreateSolidSidedBorder(
gfx::Insets::TLBR(0, 0, kAccessCodeFlexUnderlineThicknessDp, 0),
input_color_id));
code_field_->SetBackgroundEnabled(false);
code_field_->SetFocusBehavior(FocusBehavior::ALWAYS);
code_field_->SetPreferredSize(
gfx::Size(kAccessCodeFlexLengthWidthDp, kAccessCodeInputFieldHeightDp));
SetInputColorId(input_color_id);
if (obscure_pin) {
code_field_->SetTextInputType(ui::TEXT_INPUT_TYPE_PASSWORD);
code_field_->SetObscuredGlyphSpacing(kObscuredGlyphSpacingDp);
} else {
code_field_->SetTextInputType(ui::TEXT_INPUT_TYPE_NUMBER);
}
}
FlexCodeInput::~FlexCodeInput() = default;
void FlexCodeInput::InsertDigit(int value) {
DCHECK_LE(0, value);
DCHECK_GE(9, value);
if (code_field_->GetEnabled()) {
code_field_->SetText(
base::StrCat({code_field_->GetText(), base::NumberToString16(value)}));
on_input_change_.Run(true);
}
}
void FlexCodeInput::Backspace() {
// Instead of just adjusting code_field_ text directly, fire a backspace key
// event as this handles the various edge cases (ie, selected text).
// views::Textfield::OnKeyPressed is private, so we call it via views::View.
auto* view = static_cast<views::View*>(code_field_);
view->OnKeyPressed(ui::KeyEvent(ui::EventType::kKeyPressed, ui::VKEY_BACK,
ui::DomCode::BACKSPACE, ui::EF_NONE));
view->OnKeyPressed(ui::KeyEvent(ui::EventType::kKeyReleased, ui::VKEY_BACK,
ui::DomCode::BACKSPACE, ui::EF_NONE));
// This triggers ContentsChanged(), which calls |on_input_change_|.
}
std::optional<std::string> FlexCodeInput::GetCode() const {
std::u16string_view code = code_field_->GetText();
if (!code.length()) {
return std::nullopt;
}
return base::UTF16ToUTF8(code);
}
void FlexCodeInput::SetInputColorId(ui::ColorId color_id) {
code_field_->SetTextColorId(color_id);
}
void FlexCodeInput::SetInputEnabled(bool input_enabled) {
code_field_->SetEnabled(input_enabled);
}
void FlexCodeInput::SetReadOnly(bool read_only) {
NOTIMPLEMENTED();
}
bool FlexCodeInput::IsReadOnly() const {
NOTIMPLEMENTED();
return false;
}
void FlexCodeInput::ClearInput() {
code_field_->SetText(std::u16string());
on_input_change_.Run(false);
}
void FlexCodeInput::RequestFocus() {
code_field_->RequestFocus();
}
void FlexCodeInput::SetAccessibleNameOnTextfield(const std::u16string& name) {
code_field_->GetViewAccessibility().SetName(name);
}
void FlexCodeInput::ContentsChanged(views::Textfield* sender,
const std::u16string& new_contents) {
const bool has_content = new_contents.length() > 0;
on_input_change_.Run(has_content);
}
bool FlexCodeInput::HandleKeyEvent(views::Textfield* sender,
const ui::KeyEvent& key_event) {
// Only handle keys.
if (key_event.type() != ui::EventType::kKeyPressed) {
return false;
}
// Default handling for events with Alt modifier like spoken feedback.
if (key_event.IsAltDown()) {
return false;
}
// FlexCodeInput class responds to a limited subset of key press events.
// All events not handled below are sent to |code_field_|.
const ui::KeyboardCode key_code = key_event.key_code();
// Allow using tab for keyboard navigation.
if (key_code == ui::VKEY_TAB || key_code == ui::VKEY_BACKTAB) {
return false;
}
if (key_code == ui::VKEY_RETURN) {
if (GetCode().has_value()) {
on_enter_.Run();
}
return true;
}
if (key_code == ui::VKEY_ESCAPE) {
on_escape_.Run();
return true;
}
return false;
}
BEGIN_METADATA(FlexCodeInput)
END_METADATA
AccessibleInputField::AccessibleInputField()
: SystemTextfield(SystemTextfield::Type::kMedium) {
// We want the PIN input field, an empty input field, to retain
// NameFrom::kAttributeExplicitlyEmpty.
GetViewAccessibility().SetName(
std::string(), ax::mojom::NameFrom::kAttributeExplicitlyEmpty);
}
bool AccessibleInputField::IsGroupFocusTraversable() const {
return false;
}
views::View* AccessibleInputField::GetSelectedViewForGroup(int group) {
return parent() ? parent()->GetSelectedViewForGroup(group) : nullptr;
}
void AccessibleInputField::OnGestureEvent(ui::GestureEvent* event) {
if (event->type() == ui::EventType::kGestureTap) {
RequestFocusWithPointer(event->details().primary_pointer_type());
return;
}
views::Textfield::OnGestureEvent(event);
}
BEGIN_METADATA(AccessibleInputField)
END_METADATA
FixedLengthCodeInput::FixedLengthCodeInput(int length,
OnInputChange on_input_change,
OnEnter on_enter,
OnEscape on_escape,
bool obscure_pin)
: on_input_change_(std::move(on_input_change)),
on_enter_(std::move(on_enter)),
on_escape_(std::move(on_escape)),
is_obscure_pin_(obscure_pin) {
DCHECK_LT(0, length);
DCHECK(on_input_change_);
auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
kAccessCodeBetweenInputFieldsGapDp));
SetGroup(kFixedLengthInputGroup);
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
const ui::ColorId text_color_id =
static_cast<ui::ColorId>(cros_tokens::kCrosSysOnSurface);
for (int i = 0; i < length; ++i) {
auto* field = new AccessibleInputField();
views::FocusRing::Get(field)->SetHasFocusPredicate(
base::BindRepeating([](const views::View* view) { return false; }));
field->SetBackgroundEnabled(false);
field->set_controller(this);
field->SetPreferredSize(
gfx::Size(kAccessCodeInputFieldWidthDp, kAccessCodeInputFieldHeightDp));
field->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_CENTER);
if (is_obscure_pin_) {
field->SetTextInputType(ui::TEXT_INPUT_TYPE_PASSWORD);
} else {
field->SetTextInputType(ui::TEXT_INPUT_TYPE_NUMBER);
}
field->SetTextColorId(text_color_id);
field->SetFontList(views::Textfield::GetDefaultFontList().Derive(
kAccessCodeFontSizeDeltaDp, gfx::Font::FontStyle::NORMAL,
gfx::Font::Weight::NORMAL));
field->SetBorder(views::CreateThemedSolidSidedBorder(
gfx::Insets::TLBR(0, 0, kAccessCodeInputFieldUnderlineThicknessDp, 0),
text_color_id));
field->SetGroup(kFixedLengthInputGroup);
// Ignores the a11y focus of |field| because the a11y needs to focus to the
// FixedLengthCodeInput object.
field->GetViewAccessibility().set_propagate_focus_to_ancestor(true);
input_fields_.push_back(field);
AddChildViewRaw(field);
layout->SetFlexForView(field, 1);
}
text_value_for_a11y_ = std::u16string(length, ' ');
GetViewAccessibility().SetRole(ax::mojom::Role::kTextField);
GetViewAccessibility().SetName(l10n_util::GetStringUTF8(
IDS_ASH_LOGIN_PARENT_ACCESS_GENERIC_DESCRIPTION));
GetViewAccessibility().SetIsProtected(is_obscure_pin_);
GetViewAccessibility().SetValue(text_value_for_a11y_);
GetViewAccessibility().SetIsEditable(true);
GetViewAccessibility().AddHTMLAttributes(std::make_pair("type", "tel"));
OnTextSelectionChanged();
}
FixedLengthCodeInput::~FixedLengthCodeInput() = default;
// Inserts |value| into the |active_field_| and moves focus to the next field
// if it exists.
void FixedLengthCodeInput::InsertDigit(int value) {
DCHECK_LE(0, value);
DCHECK_GE(9, value);
ActiveField()->SetText(base::NumberToString16(value));
bool was_last_field = IsLastFieldActive();
ResetTextValueForA11y();
FocusNextField();
NotifyAccessibilityEventDeprecated(ax::mojom::Event::kTextSelectionChanged,
true);
on_input_change_.Run(was_last_field, GetCode().has_value());
}
// Clears input from the |active_field_|. If |active_field| is empty moves
// focus to the previous field (if exists) and clears input there.
void FixedLengthCodeInput::Backspace() {
// Ignore backspace on the first field, if empty.
if (IsFirstFieldActive() && ActiveInput().empty()) {
return;
}
if (ActiveInput().empty()) {
FocusPreviousField();
}
ActiveField()->SetText(std::u16string());
ResetTextValueForA11y();
NotifyAccessibilityEventDeprecated(ax::mojom::Event::kTextSelectionChanged,
true);
on_input_change_.Run(IsLastFieldActive(), false /*complete*/);
}
// Returns access code as string if all fields contain input.
std::optional<std::string> FixedLengthCodeInput::GetCode() const {
std::string result;
size_t length;
for (ash::AccessibleInputField* field : input_fields_) {
length = field->GetText().length();
if (!length) {
return std::nullopt;
}
DCHECK_EQ(1u, length);
base::StrAppend(&result, {base::UTF16ToUTF8(field->GetText())});
}
return result;
}
void FixedLengthCodeInput::SetInputColorId(ui::ColorId color_id) {
const ui::ColorId error_color_id =
static_cast<ui::ColorId>(cros_tokens::kCrosSysError);
for (ash::AccessibleInputField* field : input_fields_) {
field->SetTextColorId(color_id);
// We don't update the underline color to red.
if (color_id != error_color_id) {
field->SetBorder(views::CreateThemedSolidSidedBorder(
gfx::Insets::TLBR(0, 0, kAccessCodeInputFieldUnderlineThicknessDp, 0),
color_id));
}
}
}
bool FixedLengthCodeInput::IsGroupFocusTraversable() const {
return false;
}
views::View* FixedLengthCodeInput::GetSelectedViewForGroup(int group) {
return ActiveField();
}
void FixedLengthCodeInput::RequestFocus() {
ActiveField()->RequestFocus();
OnTextSelectionChanged();
}
void FixedLengthCodeInput::ResetTextValueForA11y() {
std::u16string result;
for (size_t i = 0; i < input_fields_.size(); ++i) {
if (input_fields_[i]->GetText().empty()) {
result.push_back(' ');
} else {
result.push_back(is_obscure_pin_ ? u'•' : input_fields_[i]->GetText()[0]);
}
}
text_value_for_a11y_ = result;
GetViewAccessibility().SetValue(text_value_for_a11y_);
OnTextSelectionChanged();
}
gfx::Range FixedLengthCodeInput::GetSelectedRangeOfTextValueForA11y() {
int text_sel_start = active_input_index_;
bool empty = text_value_for_a11y_[text_sel_start] == ' ';
int text_sel_end = text_sel_start + (empty ? 0 : 1);
return gfx::Range(text_sel_start, text_sel_end);
}
void FixedLengthCodeInput::OnTextSelectionChanged() {
const gfx::Range& range = GetSelectedRangeOfTextValueForA11y();
GetViewAccessibility().SetTextSelStart(range.start());
GetViewAccessibility().SetTextSelEnd(range.end());
}
bool FixedLengthCodeInput::HandleKeyEvent(views::Textfield* sender,
const ui::KeyEvent& key_event) {
if (key_event.type() != ui::EventType::kKeyPressed) {
return false;
}
// Default handling for events with Alt modifier like spoken feedback.
if (key_event.IsAltDown()) {
return false;
}
// Default handling for events with Control modifier like sign out.
if (key_event.IsControlDown()) {
return false;
}
if (sender->GetReadOnly()) {
return false;
}
// FixedLengthCodeInput class responds to limited subset of key press
// events. All key pressed events not handled below are ignored.
const ui::KeyboardCode key_code = key_event.key_code();
if (key_code == ui::VKEY_TAB || key_code == ui::VKEY_BACKTAB) {
// Allow using tab for keyboard navigation.
return false;
} else if (key_code == ui::VKEY_PROCESSKEY) {
// Default handling for keyboard events that are not generated by physical
// key press. This can happen, for example, when virtual keyboard button
// is pressed.
return false;
} else if (key_code >= ui::VKEY_0 && key_code <= ui::VKEY_9) {
InsertDigit(key_code - ui::VKEY_0);
} else if (key_code >= ui::VKEY_NUMPAD0 && key_code <= ui::VKEY_NUMPAD9) {
InsertDigit(key_code - ui::VKEY_NUMPAD0);
} else if (key_code == ui::VKEY_LEFT && arrow_navigation_allowed_) {
FocusPreviousField();
NotifyAccessibilityEventDeprecated(ax::mojom::Event::kTextSelectionChanged,
true);
} else if (key_code == ui::VKEY_RIGHT && arrow_navigation_allowed_) {
// Do not allow to leave empty field when moving focus with arrow key.
if (!ActiveInput().empty()) {
FocusNextField();
NotifyAccessibilityEventDeprecated(
ax::mojom::Event::kTextSelectionChanged, true);
}
} else if (key_code == ui::VKEY_BACK) {
Backspace();
} else if (key_code == ui::VKEY_RETURN) {
if (GetCode().has_value()) {
on_enter_.Run();
}
} else if (key_code == ui::VKEY_ESCAPE) {
on_escape_.Run();
}
return true;
}
void FixedLengthCodeInput::ContentsChanged(views::Textfield* sender,
const std::u16string& new_contents) {
if (new_contents.empty()) {
return;
}
// Called when a character or text is inserted from the virtual keyboard.
if (new_contents.size() > 1) {
sender->SetText(std::u16string());
return;
}
unsigned new_digit = 0;
// If a non-numeric character is inserted from the virtual keyboard, clear it.
if (!base::StringToUint(new_contents, &new_digit) ||
!base::IsValueInRangeForNumericType<uint8_t>(new_digit)) {
sender->SetText(std::u16string());
return;
}
bool was_last_field = IsLastFieldActive();
ResetTextValueForA11y();
FocusNextField();
NotifyAccessibilityEventDeprecated(ax::mojom::Event::kTextSelectionChanged,
true);
on_input_change_.Run(was_last_field, GetCode().has_value());
}
bool FixedLengthCodeInput::HandleMouseEvent(views::Textfield* sender,
const ui::MouseEvent& mouse_event) {
if (!(mouse_event.IsOnlyLeftMouseButton() ||
mouse_event.IsOnlyRightMouseButton())) {
return false;
}
// Move focus to the field that was selected with mouse input.
for (size_t i = 0; i < input_fields_.size(); ++i) {
if (input_fields_[i] == sender) {
active_input_index_ = i;
RequestFocus();
NotifyAccessibilityEventDeprecated(
ax::mojom::Event::kTextSelectionChanged, true);
break;
}
}
return true;
}
bool FixedLengthCodeInput::HandleGestureEvent(
views::Textfield* sender,
const ui::GestureEvent& gesture_event) {
if (gesture_event.details().type() != ui::EventType::kGestureTap) {
return false;
}
// Move focus to the field that was selected with gesture.
for (size_t i = 0; i < input_fields_.size(); ++i) {
if (input_fields_[i] == sender) {
active_input_index_ = i;
RequestFocus();
NotifyAccessibilityEventDeprecated(
ax::mojom::Event::kTextSelectionChanged, true);
break;
}
}
return true;
}
void FixedLengthCodeInput::SetInputEnabled(bool input_enabled) {
for (ash::AccessibleInputField* field : input_fields_) {
field->SetEnabled(input_enabled);
}
}
void FixedLengthCodeInput::SetReadOnly(bool read_only) {
const ui::ColorId underline_color_id =
static_cast<ui::ColorId>(cros_tokens::kCrosSysOnSurface);
for (ash::AccessibleInputField* field : input_fields_) {
field->SetReadOnly(read_only);
field->SetBackground(nullptr);
field->SetBorder(views::CreateThemedSolidSidedBorder(
gfx::Insets::TLBR(0, 0, kAccessCodeInputFieldUnderlineThicknessDp, 0),
underline_color_id));
field->SetCursorEnabled(!read_only);
}
}
bool FixedLengthCodeInput::IsReadOnly() const {
if (!input_fields_.empty()) {
// As SetReadOnly above propagates flag to all fields, just
// check the first field here instead of implementing complex
// combining logic.
return static_cast<views::SelectionControllerDelegate*>(input_fields_[0])
->IsReadOnly();
}
return false;
}
void FixedLengthCodeInput::ClearInput() {
for (ash::AccessibleInputField* field : input_fields_) {
field->SetText(std::u16string());
}
active_input_index_ = 0;
text_value_for_a11y_.clear();
GetViewAccessibility().RemoveValue();
RequestFocus();
}
bool FixedLengthCodeInput::IsEmpty() const {
for (ash::AccessibleInputField* field : input_fields_) {
if (field->GetText().length()) {
return false;
}
}
return true;
}
void FixedLengthCodeInput::SetAllowArrowNavigation(bool allowed) {
arrow_navigation_allowed_ = allowed;
}
void FixedLengthCodeInput::FocusPreviousField() {
if (active_input_index_ == 0) {
return;
}
--active_input_index_;
RequestFocus();
}
void FixedLengthCodeInput::FocusNextField() {
if (IsLastFieldActive()) {
return;
}
++active_input_index_;
RequestFocus();
}
bool FixedLengthCodeInput::IsLastFieldActive() const {
return active_input_index_ == (static_cast<int>(input_fields_.size()) - 1);
}
bool FixedLengthCodeInput::IsFirstFieldActive() const {
return active_input_index_ == 0;
}
AccessibleInputField* FixedLengthCodeInput::ActiveField() const {
return input_fields_[active_input_index_];
}
std::u16string_view FixedLengthCodeInput::ActiveInput() const {
return ActiveField()->GetText();
}
BEGIN_METADATA(FixedLengthCodeInput)
END_METADATA
} // namespace ash