Skip to content

Commit ba4875a

Browse files
Material Design Teamdrchen
authored andcommitted
[FloatingActionButton][A11y] Add tooltip label to FAB and eFAB
Updates FloatingActionButton to automatically set the tooltip text to match the content description. For ExtendedFloatingActionButton, the tooltip text is now dynamic based on the component's state: - When shrunk, the tooltip is set to the button's text or content description. - When extended, the tooltip is cleared as the text is already visible. Also ensures that setClickable(false) clears the tooltip, preventing the View from consuming touch events on API 26+. PiperOrigin-RevId: 875689637
1 parent 6923ba4 commit ba4875a

4 files changed

Lines changed: 285 additions & 0 deletions

File tree

lib/java/com/google/android/material/floatingactionbutton/ExtendedFloatingActionButton.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import android.content.res.TypedArray;
3535
import android.graphics.Color;
3636
import android.graphics.Rect;
37+
import android.os.Build;
3738
import android.text.TextUtils;
3839
import android.util.AttributeSet;
3940
import android.util.Property;
@@ -487,6 +488,69 @@ protected void onAttachedToWindow() {
487488
if (isExtended && TextUtils.isEmpty(getText()) && getIcon() != null) {
488489
isExtended = false;
489490
shrinkStrategy.performNow();
491+
} else {
492+
updateTooltip();
493+
}
494+
}
495+
496+
/**
497+
* Sets the content description for this view.
498+
*
499+
* <p>On API 26 (Android O) and above, this method also sets the tooltip text to the content
500+
* description if appropriate.
501+
*/
502+
@Override
503+
public void setContentDescription(@Nullable CharSequence contentDescription) {
504+
super.setContentDescription(contentDescription);
505+
updateTooltip();
506+
}
507+
508+
/**
509+
* Sets the text to be displayed.
510+
*
511+
* <p>On API 26 (Android O) and above, this method also sets the tooltip text if appropriate.
512+
*/
513+
@Override
514+
public void setText(CharSequence text, BufferType type) {
515+
super.setText(text, type);
516+
updateTooltip();
517+
}
518+
519+
/**
520+
* Sets the clickable state of this view.
521+
*
522+
* <p>On API 26 (Android O) and above, if the view is not clickable, this method also clears the
523+
* tooltip text to prevent the view from consuming touch events.
524+
*/
525+
@Override
526+
public void setClickable(boolean clickable) {
527+
super.setClickable(clickable);
528+
updateTooltip();
529+
}
530+
531+
/**
532+
* Updates the tooltip text based on the button's extended state, text, and clickability. If the
533+
* view is not clickable, tooltip text will be cleared to prevent the view from consuming touch
534+
* events on API 26+.
535+
*
536+
* <p>The tooltip is not set on lower APIs to avoid overwriting any custom {@link
537+
* View.OnLongClickListener}.
538+
*/
539+
private void updateTooltip() {
540+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
541+
return;
542+
}
543+
544+
CharSequence newTooltipText;
545+
if (isExtended || !isClickable()) {
546+
newTooltipText = null;
547+
} else {
548+
CharSequence text = getText();
549+
newTooltipText = TextUtils.isEmpty(text) ? getContentDescription() : text;
550+
}
551+
552+
if (!TextUtils.equals(getTooltipText(), newTooltipText)) {
553+
setTooltipText(newTooltipText);
490554
}
491555
}
492556

@@ -1380,6 +1444,7 @@ public void performNow() {
13801444
size.getPaddingEnd(),
13811445
getPaddingBottom());
13821446
requestLayout();
1447+
updateTooltip();
13831448
}
13841449

13851450
@Override
@@ -1449,6 +1514,7 @@ public void onAnimationStart(Animator animator) {
14491514
isExtended = extending;
14501515
isTransforming = true;
14511516
setHorizontallyScrolling(true);
1517+
updateTooltip();
14521518
}
14531519

14541520
@Override

lib/java/com/google/android/material/floatingactionbutton/FloatingActionButton.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,43 @@ public void setVisibility(int visibility) {
574574
super.setVisibility(visibility);
575575
}
576576

577+
/**
578+
* Sets the content description for this view.
579+
*
580+
* <p>On API 26 (Android O) and above, this method also sets the tooltip text to the content
581+
* description if appropriate.
582+
*/
583+
@Override
584+
public void setContentDescription(@Nullable CharSequence contentDescription) {
585+
super.setContentDescription(contentDescription);
586+
updateTooltip();
587+
}
588+
589+
/**
590+
* Sets the clickable state of this view.
591+
*
592+
* <p>On API 26 (Android O) and above, if the view is not clickable, this method also clears the
593+
* tooltip text to prevent the view from consuming touch events.
594+
*/
595+
@Override
596+
public void setClickable(boolean clickable) {
597+
super.setClickable(clickable);
598+
updateTooltip();
599+
}
600+
601+
/**
602+
* Updates the tooltip text based on the button's clickability. If the view is not clickable,
603+
* tooltip text will be cleared to prevent the view from consuming touch events.
604+
*
605+
* <p>The tooltip is not set on lower APIs to avoid overwriting any custom {@link
606+
* View.OnLongClickListener}.
607+
*/
608+
private void updateTooltip() {
609+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
610+
setTooltipText(isClickable() ? getContentDescription() : null);
611+
}
612+
}
613+
577614
/**
578615
* Sets the max image size for this button.
579616
*

lib/javatests/com/google/android/material/floatingactionbutton/ExtendedFloatingActionButtonTest.java

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424
import static org.mockito.Mockito.verify;
2525
import static org.robolectric.Shadows.shadowOf;
2626

27+
import android.os.Build.VERSION_CODES;
2728
import android.os.Bundle;
2829
import android.os.Looper;
2930
import androidx.appcompat.app.AppCompatActivity;
3031
import android.view.View;
3132
import android.view.View.MeasureSpec;
3233
import android.view.ViewGroup.LayoutParams;
34+
import androidx.annotation.RequiresApi;
3335
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton.OnChangedCallback;
3436
import org.junit.Before;
3537
import org.junit.Test;
@@ -113,6 +115,132 @@ public void setExtended_correctSize_whenExtendedTrue() {
113115
assertThat(fabForTest.getMeasuredWidth()).isEqualTo(originalWidth);
114116
}
115117

118+
@Test
119+
@RequiresApi(VERSION_CODES.O)
120+
@Config(sdk = VERSION_CODES.O)
121+
public void shrink_setsTooltipText() {
122+
fabForTest.shrink();
123+
shadowOf(Looper.getMainLooper()).idle();
124+
125+
assertThat(fabForTest.getTooltipText().toString()).isEqualTo(fabForTest.getText().toString());
126+
}
127+
128+
@Test
129+
@RequiresApi(VERSION_CODES.O)
130+
@Config(sdk = VERSION_CODES.O)
131+
public void shrink_setsTooltipTextToContentDescription_whenTextEmpty() {
132+
fabForTest.shrink();
133+
shadowOf(Looper.getMainLooper()).idle();
134+
135+
fabForTest.setText("");
136+
fabForTest.setContentDescription("Content description");
137+
138+
assertThat(fabForTest.getTooltipText().toString())
139+
.isEqualTo(fabForTest.getContentDescription().toString());
140+
}
141+
142+
@Test
143+
@RequiresApi(VERSION_CODES.O)
144+
@Config(sdk = VERSION_CODES.O)
145+
public void extend_clearsTooltipText() {
146+
fabForTest.shrink();
147+
shadowOf(Looper.getMainLooper()).idle();
148+
fabForTest.extend();
149+
shadowOf(Looper.getMainLooper()).idle();
150+
151+
assertThat(fabForTest.getTooltipText()).isNull();
152+
}
153+
154+
@RequiresApi(VERSION_CODES.O)
155+
@Config(sdk = VERSION_CODES.O)
156+
@Test
157+
public void setExtended_false_setsTooltipText() {
158+
fabForTest.setExtended(false);
159+
shadowOf(Looper.getMainLooper()).idle();
160+
161+
assertThat(fabForTest.getTooltipText().toString()).isEqualTo(fabForTest.getText().toString());
162+
}
163+
164+
@RequiresApi(VERSION_CODES.O)
165+
@Config(sdk = VERSION_CODES.O)
166+
@Test
167+
public void setExtended_true_clearsTooltipText() {
168+
fabForTest.setExtended(false);
169+
shadowOf(Looper.getMainLooper()).idle();
170+
171+
fabForTest.setExtended(true);
172+
shadowOf(Looper.getMainLooper()).idle();
173+
174+
assertThat(fabForTest.getTooltipText()).isNull();
175+
}
176+
177+
@RequiresApi(VERSION_CODES.O)
178+
@Config(sdk = VERSION_CODES.O)
179+
@Test
180+
public void onAttachedToWindow_extended_clearsTooltip() {
181+
fabForTest.setText("Text");
182+
fabForTest.setExtended(true);
183+
activity.setContentView(fabForTest);
184+
shadowOf(Looper.getMainLooper()).idle();
185+
186+
assertThat(fabForTest.getTooltipText()).isNull();
187+
}
188+
189+
@RequiresApi(VERSION_CODES.O)
190+
@Config(sdk = VERSION_CODES.O)
191+
@Test
192+
public void setText_updatesTooltip_whenShrunk() {
193+
fabForTest.shrink();
194+
shadowOf(Looper.getMainLooper()).idle();
195+
196+
String newText = "New Test Text";
197+
fabForTest.setText(newText);
198+
199+
assertThat(fabForTest.getTooltipText().toString()).isEqualTo(newText);
200+
}
201+
202+
@RequiresApi(VERSION_CODES.O)
203+
@Config(sdk = VERSION_CODES.O)
204+
@Test
205+
public void setContentDescription_updatesTooltip_whenShrunkAndTextEmpty() {
206+
fabForTest.shrink();
207+
shadowOf(Looper.getMainLooper()).idle();
208+
fabForTest.setText("");
209+
210+
String newDescription = "New Description";
211+
fabForTest.setContentDescription(newDescription);
212+
shadowOf(Looper.getMainLooper()).idle();
213+
214+
assertThat(fabForTest.getTooltipText().toString()).isEqualTo(newDescription);
215+
}
216+
217+
@RequiresApi(VERSION_CODES.O)
218+
@Config(sdk = VERSION_CODES.O)
219+
@Test
220+
public void setClickable_false_clearsTooltipText() {
221+
fabForTest.shrink();
222+
shadowOf(Looper.getMainLooper()).idle();
223+
fabForTest.setClickable(false);
224+
shadowOf(Looper.getMainLooper()).idle();
225+
226+
assertThat(fabForTest.getTooltipText()).isNull();
227+
}
228+
229+
@RequiresApi(VERSION_CODES.O)
230+
@Config(sdk = VERSION_CODES.O)
231+
@Test
232+
public void setClickable_true_setsTooltipText() {
233+
fabForTest.shrink();
234+
shadowOf(Looper.getMainLooper()).idle();
235+
fabForTest.setClickable(false);
236+
shadowOf(Looper.getMainLooper()).idle();
237+
238+
fabForTest.setClickable(true);
239+
shadowOf(Looper.getMainLooper()).idle();
240+
241+
assertThat(fabForTest.getTooltipText().toString()).isEqualTo(fabForTest.getText().toString());
242+
}
243+
116244
private ExtendedFloatingActionButton createFabForTest() {
117245
ExtendedFloatingActionButton fab = new ExtendedFloatingActionButton(activity);
118246
fab.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));

lib/javatests/com/google/android/material/floatingactionbutton/FabTest.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@
2222
import static com.google.android.material.internal.ViewUtils.dpToPx;
2323
import static org.junit.Assert.assertEquals;
2424
import static org.junit.Assert.assertNotEquals;
25+
import static org.junit.Assert.assertNull;
2526
import static org.junit.Assert.assertTrue;
2627

2728
import android.content.Context;
29+
import android.os.Build.VERSION_CODES;
2830
import android.os.Bundle;
2931
import androidx.appcompat.app.AppCompatActivity;
3032
import android.view.View.MeasureSpec;
33+
import androidx.annotation.RequiresApi;
3134
import org.junit.Before;
3235
import org.junit.Test;
3336
import org.junit.runner.RunWith;
@@ -80,6 +83,57 @@ public void ensureMinTouchTargetFalse_isLessThan48dp() {
8083
assertTrue(fab.getMeasuredHeight() < minSize);
8184
}
8285

86+
@RequiresApi(VERSION_CODES.O)
87+
@Config(sdk = VERSION_CODES.O)
88+
@Test
89+
public void setContentDescription_setsTooltipText() {
90+
FloatingActionButton fab = new FloatingActionButton(activity);
91+
String description = "test description";
92+
93+
fab.setClickable(true);
94+
fab.setContentDescription(description);
95+
96+
assertEquals(description, fab.getTooltipText().toString());
97+
}
98+
99+
@RequiresApi(VERSION_CODES.O)
100+
@Config(sdk = VERSION_CODES.O)
101+
@Test
102+
public void setClickable_false_clearsTooltipText() {
103+
FloatingActionButton fab = new FloatingActionButton(activity);
104+
fab.setContentDescription("test description");
105+
106+
fab.setClickable(false);
107+
108+
assertNull(fab.getTooltipText());
109+
}
110+
111+
@RequiresApi(VERSION_CODES.O)
112+
@Config(sdk = VERSION_CODES.O)
113+
@Test
114+
public void setClickable_true_setsTooltipText() {
115+
FloatingActionButton fab = new FloatingActionButton(activity);
116+
String description = "test description";
117+
fab.setContentDescription(description);
118+
fab.setClickable(false);
119+
120+
fab.setClickable(true);
121+
122+
assertEquals(description, fab.getTooltipText().toString());
123+
}
124+
125+
@RequiresApi(VERSION_CODES.O)
126+
@Config(sdk = VERSION_CODES.O)
127+
@Test
128+
public void setContentDescription_notClickable_doesNotSetTooltipText() {
129+
FloatingActionButton fab = new FloatingActionButton(activity);
130+
fab.setClickable(false);
131+
132+
fab.setContentDescription("test description");
133+
134+
assertNull(fab.getTooltipText());
135+
}
136+
83137
private FloatingActionButton createFabForTest(boolean ensureMinTouchTarget) {
84138
FloatingActionButton fab = new FloatingActionButton(activity);
85139
float dimen = dpToPx(activity, MIN_SIZE_FOR_ALLY_DP);

0 commit comments

Comments
 (0)