Skip to content

Commit 29cea73

Browse files
committed
chore(flutter): detect label for clicks from the text
1 parent 8f5944c commit 29cea73

File tree

2 files changed

+59
-29
lines changed

2 files changed

+59
-29
lines changed

flutter/packages/measure_flutter/lib/src/gestures/layout_snapshot_capture.dart

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ class LayoutSnapshotCaptureResult {
2020
final LayoutSnapshot snapshot;
2121
final Element? detectedElement;
2222
final String? detectedElementType;
23+
final String? detectedElementLabel;
2324

2425
LayoutSnapshotCaptureResult({
2526
required this.snapshot,
2627
this.detectedElement,
2728
this.detectedElementType,
29+
this.detectedElementLabel,
2830
});
2931
}
3032

@@ -39,6 +41,7 @@ class _CaptureState {
3941
// Results accumulated during traversal
4042
Element? detectedElement;
4143
String? detectedElementType;
44+
String? detectedElementLabel;
4245

4346
_CaptureState({
4447
required this.rootElement,
@@ -81,8 +84,7 @@ class LayoutSnapshotCapture {
8184
if (rootElement == null) return null;
8285

8386
// Step 2: Find starting point (scaffold or root)
84-
final scaffoldElement =
85-
developer.Timeline.timeSync('findTopmostScaffold', () {
87+
final scaffoldElement = developer.Timeline.timeSync('findTopmostScaffold', () {
8688
return _findTopmostScaffold(rootElement);
8789
});
8890
final startElement = scaffoldElement ?? rootElement;
@@ -118,16 +120,15 @@ class LayoutSnapshotCapture {
118120
snapshot: tree,
119121
detectedElement: state.detectedElement,
120122
detectedElementType: state.detectedElementType,
123+
detectedElementLabel: state.detectedElementLabel,
121124
);
122125
});
123126
}
124127

125128
/// Checks if an element has a valid RenderBox with size
126129
static bool _hasValidRenderBox(Element element) {
127130
final renderObject = element.renderObject;
128-
return renderObject != null &&
129-
renderObject is RenderBox &&
130-
renderObject.hasSize;
131+
return renderObject != null && renderObject is RenderBox && renderObject.hasSize;
131132
}
132133

133134
/// Checks if an element should be skipped during traversal
@@ -138,8 +139,7 @@ class LayoutSnapshotCapture {
138139

139140
/// Gets the node type for a widget based on the capture state
140141
static String? _getNodeType(Widget widget, _CaptureState state) {
141-
if (state.providedWidgetsTypes != null &&
142-
state.providedWidgetsTypes!.isNotEmpty) {
142+
if (state.providedWidgetsTypes != null && state.providedWidgetsTypes!.isNotEmpty) {
143143
return _getUserProvidedWidget(widget, state.providedWidgetsTypes);
144144
}
145145
return _getFrameworkWidget(widget);
@@ -177,9 +177,7 @@ class LayoutSnapshotCapture {
177177
/// Updates the state if a matching element is found
178178
static void _tryDetectElement(Element element, _CaptureState state) {
179179
// Skip if already detected or detection not enabled
180-
if (state.detectedElement != null ||
181-
state.detectionPosition == null ||
182-
state.detectionMode == null) {
180+
if (state.detectedElement != null || state.detectionPosition == null || state.detectionMode == null) {
183181
return;
184182
}
185183

@@ -197,10 +195,14 @@ class LayoutSnapshotCapture {
197195
}
198196

199197
// If it matches and is at the position, record it
200-
if (detectedType != null &&
201-
_hitTest(element, state.detectionPosition!, state.rootElement)) {
198+
if (detectedType != null && _hitTest(element, state.detectionPosition!, state.rootElement)) {
202199
state.detectedElement = element;
203200
state.detectedElementType = detectedType;
201+
202+
// Extract label for click detection mode
203+
if (state.detectionMode == DetectionMode.click) {
204+
state.detectedElementLabel = _findFirstTextInSubtree(element);
205+
}
204206
}
205207
}
206208

@@ -217,9 +219,7 @@ class LayoutSnapshotCapture {
217219
// Determine the node type
218220
final nodeType = _getNodeType(widget, state);
219221
final isDetected = (state.detectedElement == element);
220-
final effectiveNodeType = isDetected && nodeType == null
221-
? state.detectedElementType
222-
: nodeType;
222+
final effectiveNodeType = isDetected && nodeType == null ? state.detectedElementType : nodeType;
223223

224224
// Determine highlighting (for click detection)
225225
final isHighlighted = state.detectionMode == DetectionMode.click && isDetected;
@@ -356,8 +356,7 @@ class LayoutSnapshotCapture {
356356
return;
357357
}
358358

359-
if (widget.runtimeType == Scaffold ||
360-
widget.runtimeType == CupertinoPageScaffold) {
359+
if (widget.runtimeType == Scaffold || widget.runtimeType == CupertinoPageScaffold) {
361360
topmostScaffold = element;
362361
}
363362

@@ -372,8 +371,7 @@ class LayoutSnapshotCapture {
372371
if (rootElement == null) return false;
373372

374373
final renderObject = element.renderObject;
375-
if (renderObject == null ||
376-
(renderObject is RenderBox && !renderObject.hasSize)) {
374+
if (renderObject == null || (renderObject is RenderBox && !renderObject.hasSize)) {
377375
return false;
378376
}
379377

@@ -387,8 +385,7 @@ class LayoutSnapshotCapture {
387385

388386
// Check bounds
389387
final transform = renderObject.getTransformTo(rootElement.renderObject);
390-
final paintBounds =
391-
MatrixUtils.transformRect(transform, renderObject.paintBounds);
388+
final paintBounds = MatrixUtils.transformRect(transform, renderObject.paintBounds);
392389
return paintBounds.contains(position);
393390
}
394391

@@ -428,8 +425,7 @@ class LayoutSnapshotCapture {
428425
return type;
429426
}
430427

431-
static String? _getUserProvidedWidget(
432-
Widget widget, Map<Type, String>? providedWidgets) {
428+
static String? _getUserProvidedWidget(Widget widget, Map<Type, String>? providedWidgets) {
433429
final type = widget.runtimeType;
434430
if (providedWidgets?.keys.contains(type) == true) {
435431
return providedWidgets?[type];
@@ -457,11 +453,7 @@ class LayoutSnapshotCapture {
457453
ExpansionTile _ => 'ExpansionTile',
458454
Card _ => 'Card',
459455
InkWell w when w.onTap != null => 'InkWell',
460-
GestureDetector w
461-
when w.onTap != null ||
462-
w.onDoubleTap != null ||
463-
w.onLongPress != null =>
464-
'GestureDetector',
456+
GestureDetector w when w.onTap != null || w.onDoubleTap != null || w.onLongPress != null => 'GestureDetector',
465457
InkResponse w when w.onTap != null => 'InkResponse',
466458
InputChip w when w.onPressed != null => 'InputChip',
467459
ActionChip w when w.onPressed != null => 'ActionChip',
@@ -494,4 +486,42 @@ class LayoutSnapshotCapture {
494486
_ => null,
495487
};
496488
}
489+
490+
/// Extracts text or semantic label from a widget
491+
/// Returns null if the widget has no text content
492+
/// [maxLength] limits the returned text length (default: 32)
493+
static String? _extractTextFromWidget(Widget widget, {int maxLength = 32}) {
494+
final text = switch (widget) {
495+
Text w => w.data ?? w.textSpan?.toPlainText(),
496+
RichText w => w.text.toPlainText(),
497+
Icon w => w.semanticLabel,
498+
_ => null,
499+
};
500+
return text != null && text.length > maxLength ? text.substring(0, maxLength) : text;
501+
}
502+
503+
/// Finds the first text or semantic label in the subtree of an element
504+
/// Returns null if no text is found
505+
static String? _findFirstTextInSubtree(Element element) {
506+
// Try to extract text from this element first
507+
final text = _extractTextFromWidget(element.widget);
508+
if (text != null && text.isNotEmpty) {
509+
return text;
510+
}
511+
512+
// Traverse children to find text
513+
String? foundText;
514+
element.visitChildElements((child) {
515+
// Skip if we already found text
516+
if (foundText != null) return;
517+
518+
// Skip offstage widgets
519+
if (_shouldSkipElement(child)) return;
520+
521+
// Recursively search in child
522+
foundText = _findFirstTextInSubtree(child);
523+
});
524+
525+
return foundText;
526+
}
497527
}

flutter/packages/measure_flutter/lib/src/gestures/msr_gesture_detector.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ class MsrGestureDetectorState extends State<MsrGestureDetector> {
162162
target: result.detectedElementType!,
163163
x: (position.dx * devicePixelRatio).roundToDouble(),
164164
y: (position.dy * devicePixelRatio).roundToDouble(),
165-
targetId: truncateLabel(label, maxLength: _maxLabelLength),
165+
targetId: result.detectedElementLabel,
166166
touchDownTime: null,
167167
touchUpTime: null,
168168
),

0 commit comments

Comments
 (0)