Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,18 @@ abstract class AuthenticatorInputLocalizations {
/// In en, this message translates to:
/// **'Please enter the code from your registered Authenticator app'**
String get totpCodePrompt;

/// Tooltip and accessibility label for the button that reveals the hidden password in a password field.
///
/// In en, this message translates to:
/// **'Show password'**
String get showPassword;

/// Tooltip and accessibility label for the button that hides the visible password in a password field.
///
/// In en, this message translates to:
/// **'Hide password'**
String get hidePassword;
}

class _AuthenticatorInputLocalizationsDelegate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,10 @@ class AuthenticatorInputLocalizationsEn
@override
String get totpCodePrompt =>
'Please enter the code from your registered Authenticator app';

@override
String get showPassword => 'Show password';

@override
String get hidePassword => 'Hide password';
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ enum InputResolverKeyType {
passwordRequirements,
format,
mismatch,
showPasswordTooltip,
hidePasswordTooltip,
}

class InputResolverKey {
Expand Down Expand Up @@ -388,6 +390,16 @@ class InputResolverKey {
field: InputField.usernameType,
);

static const showPasswordTooltip = InputResolverKey._(
InputResolverKeyType.showPasswordTooltip,
field: InputField.password,
);

static const hidePasswordTooltip = InputResolverKey._(
InputResolverKeyType.hidePasswordTooltip,
field: InputField.password,
);

String resolve(BuildContext context, InputResolver inputResolver) =>
inputResolver.resolve(context, this);
}
Expand Down Expand Up @@ -552,6 +564,16 @@ class InputResolver extends Resolver<InputResolverKey> {
return AuthenticatorLocalizations.inputsOf(context).optional(title);
}

/// Returns the tooltip text for showing a hidden password.
String showPasswordTooltip(BuildContext context) {
return AuthenticatorLocalizations.inputsOf(context).showPassword;
}

/// Returns the tooltip text for hiding a visible password.
String hidePasswordTooltip(BuildContext context) {
return AuthenticatorLocalizations.inputsOf(context).hidePassword;
}

@override
String resolve(BuildContext context, InputResolverKey key) {
switch (key.type) {
Expand All @@ -571,6 +593,10 @@ class InputResolver extends Resolver<InputResolverKey> {
return AuthenticatorLocalizations.inputsOf(context).passwordsDoNotMatch;
case InputResolverKeyType.format:
return format(context, key.field);
case InputResolverKeyType.showPasswordTooltip:
return showPasswordTooltip(context);
case InputResolverKeyType.hidePasswordTooltip:
return hidePasswordTooltip(context);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,12 @@
"@selectEmail": {
"description": "Label for the radio button to select email as the user's chosen MFA method."
},
"showPassword": "Show password",
"@showPassword": {
"description": "Tooltip and accessibility label for the button that reveals the hidden password in a password field."
},
"hidePassword": "Hide password",
"@hidePassword": {
"description": "Tooltip and accessibility label for the button that hides the visible password in a password field."
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@ mixin AuthenticatorTextField<
suffixIcon: suffix,
errorMaxLines: errorMaxLines,
hintText: hintText,
// Workaround for Flutter issue where validation errors are not
// announced by screen readers on web unless helperText is set.
// See: https://github.com/flutter/flutter/issues/99715
helperText: ' ',
),
maxLength: maxLength,
maxLengthEnforcement: MaxLengthEnforcement.enforced,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,20 @@ MaterialBanner createMaterialBanner(
key: keyAuthenticatorBanner,
backgroundColor: colorsChoices.background,
leading: Icon(type.icon, color: colorsChoices.foreground),
content: Center(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Text(
message.trim(),
style: TextStyle(color: colorsChoices.foreground),
content: Semantics(
liveRegion: true,
child: Center(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Text(
message.trim(),
style: TextStyle(color: colorsChoices.foreground),
),
),
),
],
],
),
),
),
actions: [
Expand Down Expand Up @@ -70,18 +73,21 @@ SnackBar createSnackBar(
return SnackBar(
key: keyAuthenticatorBanner,
backgroundColor: colorsChoices.background,
content: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(type.icon, color: fallbackIconColor),
const SizedBox(width: 16),
Expanded(
child: Text(
message.trim(),
style: TextStyle(color: colorsChoices.foreground),
content: Semantics(
liveRegion: true,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(type.icon, color: fallbackIconColor),
const SizedBox(width: 16),
Expanded(
child: Text(
message.trim(),
style: TextStyle(color: colorsChoices.foreground),
),
),
),
],
],
),
),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,17 @@ class AuthenticatorFormState<T extends AuthenticatorForm>
return ValueListenableBuilder<bool>(
valueListenable: obscureTextToggleValue,
builder: (BuildContext context, bool toggleObscureText, Widget? _) {
final inputResolver = stringResolver.inputs;
return IconButton(
onPressed: () {
obscureTextToggleValue.value = !toggleObscureText;
},
icon: Icon(
toggleObscureText ? Icons.visibility : Icons.visibility_off,
),
tooltip: toggleObscureText
? inputResolver.showPasswordTooltip(context)
: inputResolver.hidePasswordTooltip(context),
);
},
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import 'package:amplify_authenticator/amplify_authenticator.dart';
import 'package:amplify_authenticator/src/enums/status_type.dart';
import 'package:amplify_authenticator/src/keys.dart';
import 'package:amplify_authenticator/src/widgets/authenticator_banner.dart';
import 'package:amplify_authenticator_test/amplify_authenticator_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

setUp(TestWidgetsFlutterBinding.ensureInitialized);

group('Password visibility toggle accessibility (WCAG 4.1.2)', () {
testWidgets(
'exposes a "Show password" accessible name while the password is hidden',
(tester) async {
await tester.pumpWidget(const MockAuthenticatorApp());
await tester.pumpAndSettle();

SignInPage(tester: tester).expectStep(AuthenticatorStep.signIn);

// Password starts obscured, so the toggle offers to show it
expect(find.byTooltip('Show password'), findsOneWidget);
expect(find.byTooltip('Hide password'), findsNothing);
},
);

testWidgets(
'flips the accessible name to "Hide password" once the password is shown',
(tester) async {
await tester.pumpWidget(const MockAuthenticatorApp());
await tester.pumpAndSettle();

SignInPage(tester: tester).expectStep(AuthenticatorStep.signIn);

// Reveal the password
await tester.tap(find.byTooltip('Show password'));
await tester.pumpAndSettle();

// Password is now visible, so the toggle offers to hide it
expect(find.byTooltip('Hide password'), findsOneWidget);
expect(find.byTooltip('Show password'), findsNothing);
},
);
});

group('Status banner screen-reader announcements', () {
testWidgets(
'MaterialBanner content is wrapped in a live-region Semantics node',
(tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Builder(
builder: (context) => createMaterialBanner(
context,
type: StatusType.error,
message: 'Something went wrong',
actionCallback: () {},
),
),
),
),
);
await tester.pumpAndSettle();

final banner = find.byKey(keyAuthenticatorBanner);
expect(banner, findsOneWidget);

final liveRegion = find.descendant(
of: banner,
matching: find.byWidgetPredicate(
(widget) =>
widget is Semantics && (widget.properties.liveRegion ?? false),
),
);
expect(liveRegion, findsOneWidget);
},
);

testWidgets('SnackBar content is wrapped in a live-region Semantics node', (
tester,
) async {
late BuildContext capturedContext;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Builder(
builder: (context) {
capturedContext = context;
return const SizedBox.shrink();
},
),
),
),
);

// A SnackBar only builds its content once shown via the messenger
ScaffoldMessenger.of(capturedContext).showSnackBar(
createSnackBar(
capturedContext,
type: StatusType.error,
message: 'Something went wrong',
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 750));

// Flutter's SnackBar adds its own live-region, so allow more than one
final liveRegionAroundMessage = find.ancestor(
of: find.text('Something went wrong'),
matching: find.byWidgetPredicate(
(widget) =>
widget is Semantics && (widget.properties.liveRegion ?? false),
),
);
expect(liveRegionAroundMessage, findsWidgets);
});
});
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading