@@ -331,61 +331,79 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
331331 if (shouldSkipNode (data)) {
332332 return result;
333333 }
334+ final String text = data.label.isEmpty ? data.value : data.label;
335+ final Iterable <Element > elements = find.text (text).hitTestable ().evaluate ();
336+ for (final Element element in elements) {
337+ result += await _evaluateElement (node, element, tester, image, byteData);
338+ }
339+ return result;
340+ }
334341
342+ Future <Evaluation > _evaluateElement (
343+ SemanticsNode node,
344+ Element element,
345+ WidgetTester tester,
346+ ui.Image image,
347+ ByteData byteData,
348+ ) async {
335349 // Look up inherited text properties to determine text size and weight.
336350 late bool isBold;
337351 double ? fontSize;
338352
339- final String text = data.label.isEmpty ? data.value : data.label;
340- final List <Element > elements = find.text (text).hitTestable ().evaluate ().toList ();
341353 late final Rect paintBounds;
354+ late final Rect paintBoundsWithOffset;
342355
343- if (elements.length == 1 ) {
344- final Element element = elements.single;
345- final RenderObject ? renderBox = element.renderObject;
346- if (renderBox is ! RenderBox ) {
347- throw StateError ('Unexpected renderObject type: $renderBox ' );
348- }
356+ final RenderObject ? renderBox = element.renderObject;
357+ if (renderBox is ! RenderBox ) {
358+ throw StateError ('Unexpected renderObject type: $renderBox ' );
359+ }
349360
350- const Offset offset = Offset (4.0 , 4.0 );
351- paintBounds = Rect .fromPoints (
352- renderBox.localToGlobal (renderBox.paintBounds.topLeft - offset),
353- renderBox.localToGlobal (renderBox.paintBounds.bottomRight + offset),
354- );
355- final Widget widget = element.widget;
356- final DefaultTextStyle defaultTextStyle = DefaultTextStyle .of (element);
357- if (widget is Text ) {
358- final TextStyle ? style = widget.style;
359- final TextStyle effectiveTextStyle = style == null || style.inherit
360- ? defaultTextStyle.style.merge (widget.style)
361- : style;
362- isBold = effectiveTextStyle.fontWeight == FontWeight .bold;
363- fontSize = effectiveTextStyle.fontSize;
364- } else if (widget is EditableText ) {
365- isBold = widget.style.fontWeight == FontWeight .bold;
366- fontSize = widget.style.fontSize;
367- } else {
368- throw StateError ('Unexpected widget type: ${widget .runtimeType }' );
369- }
370- } else if (elements.length > 1 ) {
371- return Evaluation .fail (
372- 'Multiple nodes with the same label: ${data .label }\n ' ,
373- );
361+ const Offset offset = Offset (4.0 , 4.0 );
362+ paintBoundsWithOffset = Rect .fromPoints (
363+ renderBox.localToGlobal (renderBox.paintBounds.topLeft - offset),
364+ renderBox.localToGlobal (renderBox.paintBounds.bottomRight + offset),
365+ );
366+
367+ paintBounds = Rect .fromPoints (
368+ renderBox.localToGlobal (renderBox.paintBounds.topLeft),
369+ renderBox.localToGlobal (renderBox.paintBounds.bottomRight),
370+ );
371+
372+ final Offset ? nodeOffset = node.transform != null ? MatrixUtils .getAsTranslation (node.transform! ) : null ;
373+
374+ final Rect nodeBounds = node.rect.shift (nodeOffset ?? Offset .zero);
375+ final Rect intersection = nodeBounds.intersect (paintBounds);
376+ if (intersection.width <= 0 || intersection.height <= 0 ) {
377+ // Skip this element since it doesn't correspond to the given semantic
378+ // node.
379+ return const Evaluation .pass ();
380+ }
381+
382+ final Widget widget = element.widget;
383+ final DefaultTextStyle defaultTextStyle = DefaultTextStyle .of (element);
384+ if (widget is Text ) {
385+ final TextStyle ? style = widget.style;
386+ final TextStyle effectiveTextStyle = style == null || style.inherit
387+ ? defaultTextStyle.style.merge (widget.style)
388+ : style;
389+ isBold = effectiveTextStyle.fontWeight == FontWeight .bold;
390+ fontSize = effectiveTextStyle.fontSize;
391+ } else if (widget is EditableText ) {
392+ isBold = widget.style.fontWeight == FontWeight .bold;
393+ fontSize = widget.style.fontSize;
374394 } else {
375- // If we can't find the text node then assume the label does not
376- // correspond to actual text.
377- return result;
395+ throw StateError ('Unexpected widget type: ${widget .runtimeType }' );
378396 }
379397
380- if (isNodeOffScreen (paintBounds , tester.binding.window)) {
381- return result ;
398+ if (isNodeOffScreen (paintBoundsWithOffset , tester.binding.window)) {
399+ return const Evaluation . pass () ;
382400 }
383401
384- final Map <Color , int > colorHistogram = _colorsWithinRect (byteData, paintBounds , image.width, image.height);
402+ final Map <Color , int > colorHistogram = _colorsWithinRect (byteData, paintBoundsWithOffset , image.width, image.height);
385403
386404 // Node was too far off screen.
387405 if (colorHistogram.isEmpty) {
388- return result ;
406+ return const Evaluation . pass () ;
389407 }
390408
391409 final _ContrastReport report = _ContrastReport (colorHistogram);
@@ -394,19 +412,18 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
394412 final double targetContrastRatio = this .targetContrastRatio (fontSize, bold: isBold);
395413
396414 if (contrastRatio - targetContrastRatio >= _tolerance) {
397- return result + const Evaluation .pass ();
415+ return const Evaluation .pass ();
398416 }
399- return result +
400- Evaluation .fail (
401- '$node :\n '
402- 'Expected contrast ratio of at least $targetContrastRatio '
403- 'but found ${contrastRatio .toStringAsFixed (2 )} '
404- 'for a font size of $fontSize .\n '
405- 'The computed colors was:\n '
406- 'light - ${report .lightColor }, dark - ${report .darkColor }\n '
407- 'See also: '
408- 'https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html' ,
409- );
417+ return Evaluation .fail (
418+ '$node :\n '
419+ 'Expected contrast ratio of at least $targetContrastRatio '
420+ 'but found ${contrastRatio .toStringAsFixed (2 )} '
421+ 'for a font size of $fontSize .\n '
422+ 'The computed colors was:\n '
423+ 'light - ${report .lightColor }, dark - ${report .darkColor }\n '
424+ 'See also: '
425+ 'https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html' ,
426+ );
410427 }
411428
412429 /// Returns whether node should be skipped.
0 commit comments