mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-27 06:18:50 +01:00
a11y: decorative/meaningful image semantics, labeled icon buttons, audible addresses (#3465)
This commit is contained in:
@@ -17,6 +17,7 @@ class CakeImageWidget extends StatelessWidget {
|
||||
this.alignment,
|
||||
this.allowDrawingOutsideViewBox,
|
||||
this.filterQuality,
|
||||
this.semanticsLabel,
|
||||
});
|
||||
|
||||
final String? imageUrl;
|
||||
@@ -32,6 +33,16 @@ class CakeImageWidget extends StatelessWidget {
|
||||
final double borderRadius;
|
||||
final FilterQuality? filterQuality;
|
||||
|
||||
/// Accessible name for this image.
|
||||
///
|
||||
/// Leave `null` (the default) for decorative imagery — the image then
|
||||
/// contributes nothing at all to the semantics tree, so screen readers do not
|
||||
/// stop on an unnamed node. Pass a localized string only when the image is the
|
||||
/// sole carrier of information for the user.
|
||||
final String? semanticsLabel;
|
||||
|
||||
bool get _isDecorative => semanticsLabel == null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (imageUrl == null || imageUrl!.isEmpty) {
|
||||
@@ -52,6 +63,8 @@ class CakeImageWidget extends StatelessWidget {
|
||||
alignment: alignment ?? Alignment.center,
|
||||
allowDrawingOutsideViewBox: allowDrawingOutsideViewBox ?? false,
|
||||
colorFilter: effectiveColorFilter,
|
||||
semanticsLabel: semanticsLabel,
|
||||
excludeFromSemantics: _isDecorative,
|
||||
fit: fit ?? BoxFit.contain, errorBuilder: (context, e, trace) {
|
||||
return SvgPicture.asset(
|
||||
imageUrl!,
|
||||
@@ -61,6 +74,8 @@ class CakeImageWidget extends StatelessWidget {
|
||||
width: width,
|
||||
errorBuilder: (_, __, ___) => SizedBox(height: height, width: width),
|
||||
colorFilter: effectiveColorFilter,
|
||||
semanticsLabel: semanticsLabel,
|
||||
excludeFromSemantics: _isDecorative,
|
||||
fit: fit ?? BoxFit.contain,
|
||||
);
|
||||
});
|
||||
@@ -72,6 +87,8 @@ class CakeImageWidget extends StatelessWidget {
|
||||
fit: fit,
|
||||
color: color,
|
||||
filterQuality: filterQuality ?? FilterQuality.medium,
|
||||
semanticLabel: semanticsLabel,
|
||||
excludeFromSemantics: _isDecorative,
|
||||
errorBuilder: (_, __, ___) => _buildErrorWidget(context),
|
||||
);
|
||||
}
|
||||
@@ -85,9 +102,9 @@ class CakeImageWidget extends StatelessWidget {
|
||||
alignment: alignment ?? Alignment.center,
|
||||
allowDrawingOutsideViewBox: allowDrawingOutsideViewBox ?? false,
|
||||
fit: fit ?? BoxFit.contain,
|
||||
placeholderBuilder: (_) {
|
||||
return loadingWidget ?? const Center(child: CircularProgressIndicator());
|
||||
},
|
||||
semanticsLabel: semanticsLabel,
|
||||
excludeFromSemantics: _isDecorative,
|
||||
placeholderBuilder: (_) => _buildLoadingWidget(),
|
||||
errorBuilder: (_, __, ___) => _buildErrorWidget(context),
|
||||
)
|
||||
: Image.network(
|
||||
@@ -97,9 +114,11 @@ class CakeImageWidget extends StatelessWidget {
|
||||
fit: fit ?? BoxFit.cover,
|
||||
color: color,
|
||||
filterQuality: filterQuality ?? FilterQuality.medium,
|
||||
semanticLabel: semanticsLabel,
|
||||
excludeFromSemantics: _isDecorative,
|
||||
loadingBuilder: (_, Widget child, ImageChunkEvent? progress) {
|
||||
if (progress == null) return child;
|
||||
return loadingWidget ?? const Center(child: CircularProgressIndicator());
|
||||
return _buildLoadingWidget();
|
||||
},
|
||||
errorBuilder: (_, __, ___) => _buildErrorWidget(context),
|
||||
);
|
||||
@@ -108,8 +127,13 @@ class CakeImageWidget extends StatelessWidget {
|
||||
return imageWidget;
|
||||
}
|
||||
|
||||
/// A caller-supplied [loadingWidget] owns its own semantics; the built-in
|
||||
/// spinner is purely visual and must not become an unnamed focus stop.
|
||||
Widget _buildLoadingWidget() =>
|
||||
loadingWidget ?? const ExcludeSemantics(child: Center(child: CircularProgressIndicator()));
|
||||
|
||||
Widget _buildErrorWidget(BuildContext context) {
|
||||
return Container(
|
||||
final Widget placeholder = Container(
|
||||
height: height,
|
||||
width: width,
|
||||
decoration: BoxDecoration(
|
||||
@@ -125,5 +149,20 @@ class CakeImageWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Several call sites render meaningful content (e.g. the asset's initials) as
|
||||
// their [errorWidget], so leave its semantics to the caller.
|
||||
if (errorWidget != null) {
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
return _isDecorative
|
||||
? ExcludeSemantics(child: placeholder)
|
||||
: Semantics(
|
||||
container: true,
|
||||
image: true,
|
||||
label: semanticsLabel,
|
||||
child: ExcludeSemantics(child: placeholder),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ class RoundedCheckbox extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return value
|
||||
// Unchecked renders nothing at all, so without an explicit `checked` state
|
||||
// the selection is invisible to screen readers.
|
||||
final Widget indicator = value
|
||||
? Container(
|
||||
height: 20.0,
|
||||
width: 20.0,
|
||||
@@ -23,5 +25,9 @@ class RoundedCheckbox extends StatelessWidget {
|
||||
size: 14.0,
|
||||
))
|
||||
: Offstage();
|
||||
|
||||
// Deliberately not a semantics container: the state merges into the
|
||||
// enclosing row/option node instead of adding a second focus stop.
|
||||
return Semantics(checked: value, child: indicator);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user