Merge branch 'dev' into cw-1440-new-buy-sell-flow

# Conflicts:
#	lib/src/widgets/cake_image_widget.dart
This commit is contained in:
Robert Malikowski
2026-08-20 18:31:40 +02:00
13 changed files with 239 additions and 117 deletions
+13 -1
View File
@@ -1,3 +1,4 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:flutter/material.dart';
@@ -13,6 +14,7 @@ class QrImage extends StatelessWidget {
this.errorCorrectionLevel = qr.QrErrorCorrectLevel.H,
this.embeddedImagePath,
this.badgeImageOnEmbeddedImagePath,
this.semanticsLabel,
});
final double? size;
@@ -24,6 +26,11 @@ class QrImage extends StatelessWidget {
final String? embeddedImagePath;
final String? badgeImageOnEmbeddedImagePath;
/// Accessible name announced for the code itself. Defaults to a generic
/// localized "QR code" so the code is always discoverable; pass a more
/// specific label when the host screen knows what the code encodes.
final String? semanticsLabel;
@override
Widget build(BuildContext context) {
final imagePath = embeddedImagePath ?? 'assets/images/qr-cake.png';
@@ -68,8 +75,13 @@ class QrImage extends StatelessWidget {
foregroundColor: foregroundColor,
backgroundColor: backgroundColor,
padding: const EdgeInsets.all(12.0),
// The package default is a hardcoded English "qr code"; use the
// localized string so the code is announced in the user's language.
semanticsLabel: semanticsLabel ?? S.of(context).scan_qr_code,
),
centerImageToUse,
// The embedded logo and its badge are purely decorative — the node
// above already announces the code — so keep them out of the tree.
ExcludeSemantics(child: centerImageToUse),
],
);
}
+47 -13
View File
@@ -18,6 +18,7 @@ class CakeImageWidget extends StatelessWidget {
this.alignment,
this.allowDrawingOutsideViewBox,
this.filterQuality,
this.semanticsLabel,
});
final String? imageUrl;
@@ -33,6 +34,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) {
@@ -53,6 +64,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!,
@@ -62,6 +75,8 @@ class CakeImageWidget extends StatelessWidget {
width: width,
errorBuilder: (_, __, ___) => SizedBox(height: height, width: width),
colorFilter: effectiveColorFilter,
semanticsLabel: semanticsLabel,
excludeFromSemantics: _isDecorative,
fit: fit ?? BoxFit.contain,
);
});
@@ -73,6 +88,8 @@ class CakeImageWidget extends StatelessWidget {
fit: fit,
color: color,
filterQuality: filterQuality ?? FilterQuality.medium,
semanticLabel: semanticsLabel,
excludeFromSemantics: _isDecorative,
errorBuilder: (_, __, ___) => _buildErrorWidget(context),
);
}
@@ -86,13 +103,9 @@ class CakeImageWidget extends StatelessWidget {
alignment: alignment ?? Alignment.center,
allowDrawingOutsideViewBox: allowDrawingOutsideViewBox ?? false,
fit: fit ?? BoxFit.contain,
placeholderBuilder: (_) {
return loadingWidget ??
SizedBox(
height: height,
width: width,
child: Center(child: CupertinoActivityIndicator()));
},
semanticsLabel: semanticsLabel,
excludeFromSemantics: _isDecorative,
placeholderBuilder: (_) => _buildLoadingWidget(),
errorBuilder: (_, __, ___) => _buildErrorWidget(context),
)
: Image.network(
@@ -102,13 +115,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 ??
SizedBox(
height: height,
width: width,
child: Center(child: CupertinoActivityIndicator()));
return _buildLoadingWidget();
},
errorBuilder: (_, __, ___) => _buildErrorWidget(context),
);
@@ -117,8 +128,16 @@ 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:SizedBox(
height: height,
width: width,
child: Center(child: CupertinoActivityIndicator())));
Widget _buildErrorWidget(BuildContext context) {
return Container(
final Widget placeholder = Container(
height: height,
width: width,
decoration: BoxDecoration(
@@ -134,5 +153,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),
);
}
}
+7 -1
View File
@@ -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);
}
}