Skip to content

Commit a0e264a

Browse files
vursenclaude
andauthored
feat: add HasAriaDescription interface (#25117)
## Description Adds a `HasAriaDescription` mixin interface for setting the accessible description of a component, following the `HasAriaLabel` pattern: - `setAriaDescription(String)` / `getAriaDescription()` for the `aria-description` attribute - `setAriaDescribedBy(String)` / `getAriaDescribedBy()` for the `aria-describedby` attribute - `setAriaDescribedBy(Component)` for referencing a description component directly, generating an id for it lazily when needed (same mechanism as `setAriaLabelledBy(Component)`) Also adds the `aria-description` and `aria-describedby` attribute name constants to `ElementConstants`. Part of vaadin/web-components#11975 ## Type of change - [x] Feature --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent ab58985 commit a0e264a

3 files changed

Lines changed: 258 additions & 1 deletion

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.component;
17+
18+
import java.util.Optional;
19+
20+
import com.vaadin.flow.dom.ElementConstants;
21+
22+
/**
23+
* A generic interface for components that have an accessible description.
24+
* <p>
25+
* The accessible description provides supplementary information about the
26+
* component to assistive technologies, such as screen readers.
27+
* <p>
28+
* It can be set as plain text with the {@code aria-description} attribute, or
29+
* by referencing existing elements on the page with the
30+
* {@code aria-describedby} attribute. Prefer the latter when the description is
31+
* already visible to all users.
32+
*
33+
* @author Vaadin Ltd
34+
* @since 25.3
35+
* @see <a href=
36+
* "https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-description">
37+
* MDN: aria-description</a>
38+
* @see <a href=
39+
* "https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-describedby">
40+
* MDN: aria-describedby</a>
41+
*/
42+
public interface HasAriaDescription extends HasElement {
43+
44+
/**
45+
* Sets the {@code aria-description} attribute of the component to the given
46+
* text.
47+
* <p>
48+
* If both {@code aria-description} and {@code aria-describedby} are
49+
* present, {@code aria-describedby} takes precedence.
50+
*
51+
* @param ariaDescription
52+
* the description text, or {@code null} to remove the attribute
53+
* @see <a href=
54+
* "https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-description">
55+
* MDN: aria-description</a>
56+
*/
57+
default void setAriaDescription(String ariaDescription) {
58+
if (ariaDescription != null) {
59+
getElement().setAttribute(
60+
ElementConstants.ARIA_DESCRIPTION_ATTRIBUTE_NAME,
61+
ariaDescription);
62+
} else {
63+
getElement().removeAttribute(
64+
ElementConstants.ARIA_DESCRIPTION_ATTRIBUTE_NAME);
65+
}
66+
}
67+
68+
/**
69+
* Gets the {@code aria-description} attribute of the component.
70+
*
71+
* @return an optional aria-description, or an empty optional if none has
72+
* been set
73+
*/
74+
default Optional<String> getAriaDescription() {
75+
return Optional.ofNullable(getElement().getAttribute(
76+
ElementConstants.ARIA_DESCRIPTION_ATTRIBUTE_NAME));
77+
}
78+
79+
/**
80+
* Sets the {@code aria-describedby} attribute of the component to one or
81+
* more element IDs, separated by spaces.
82+
* <p>
83+
* Each ID must match the {@code id} attribute of another element that
84+
* describes the component. The description elements must be in the same DOM
85+
* scope as the component, otherwise screen readers will fail to announce
86+
* the description content properly.
87+
*
88+
* @param ariaDescribedBy
89+
* a space-separated list of element IDs, or {@code null} to
90+
* remove the attribute
91+
* @see <a href=
92+
* "https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-describedby">
93+
* MDN: aria-describedby</a>
94+
*/
95+
default void setAriaDescribedBy(String ariaDescribedBy) {
96+
if (ariaDescribedBy != null) {
97+
getElement().setAttribute(
98+
ElementConstants.ARIA_DESCRIBEDBY_ATTRIBUTE_NAME,
99+
ariaDescribedBy);
100+
} else {
101+
getElement().removeAttribute(
102+
ElementConstants.ARIA_DESCRIBEDBY_ATTRIBUTE_NAME);
103+
}
104+
}
105+
106+
/**
107+
* Sets the {@code aria-describedby} attribute of the component to reference
108+
* the given description component.
109+
* <p>
110+
* The description component does not need an ID: if it has none by the time
111+
* the value is sent to the client, one is generated automatically.
112+
* <p>
113+
* The description component must be in the same DOM scope as this
114+
* component, otherwise screen readers will fail to announce the description
115+
* content properly.
116+
*
117+
* @param descriptionComponent
118+
* the component to use as the description, not {@code null}
119+
* @throws IllegalArgumentException
120+
* if {@code descriptionComponent} is {@code null}; use
121+
* {@link #setAriaDescribedBy(String)} with {@code null} to
122+
* remove the attribute instead
123+
* @see #setAriaDescribedBy(String)
124+
*/
125+
default void setAriaDescribedBy(Component descriptionComponent) {
126+
if (descriptionComponent == null) {
127+
throw new IllegalArgumentException(
128+
"The provided component cannot be null");
129+
}
130+
ComponentUtil.resolveOrGenerateIdLater(getElement(),
131+
descriptionComponent, "ariadescribedby-",
132+
this::setAriaDescribedBy);
133+
}
134+
135+
/**
136+
* Gets the {@code aria-describedby} attribute of the component.
137+
*
138+
* @return an optional aria-describedby, or an empty optional if none has
139+
* been set
140+
*/
141+
default Optional<String> getAriaDescribedBy() {
142+
return Optional.ofNullable(getElement().getAttribute(
143+
ElementConstants.ARIA_DESCRIBEDBY_ATTRIBUTE_NAME));
144+
}
145+
}

‎flow-server/src/main/java/com/vaadin/flow/dom/ElementConstants.java‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,10 +446,22 @@ public class ElementConstants {
446446
public static final String ARIA_LABEL_ATTRIBUTE_NAME = "aria-label";
447447
/**
448448
* The aria-labelledby attribute.
449-
*
449+
*
450450
* @since 24.1
451451
*/
452452
public static final String ARIA_LABELLEDBY_ATTRIBUTE_NAME = "aria-labelledby";
453+
/**
454+
* The aria-describedby attribute.
455+
*
456+
* @since 25.3
457+
*/
458+
public static final String ARIA_DESCRIBEDBY_ATTRIBUTE_NAME = "aria-describedby";
459+
/**
460+
* The aria-description attribute.
461+
*
462+
* @since 25.3
463+
*/
464+
public static final String ARIA_DESCRIPTION_ATTRIBUTE_NAME = "aria-description";
453465

454466
private ElementConstants() {
455467
// Constants only
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.component;
17+
18+
import org.junit.jupiter.api.Assertions;
19+
import org.junit.jupiter.api.Test;
20+
21+
class HasAriaDescriptionTest {
22+
23+
@Tag(Tag.MAIN) // main is used, because div is not a valid target by default
24+
private static class TestComponent extends Component
25+
implements HasAriaDescription {
26+
27+
}
28+
29+
@Test
30+
void setAriaDescription() {
31+
TestComponent component = new TestComponent();
32+
Assertions.assertFalse(component.getAriaDescription().isPresent());
33+
34+
component.setAriaDescription("description text");
35+
Assertions.assertEquals("description text",
36+
component.getAriaDescription().get());
37+
38+
component.setAriaDescription(null);
39+
Assertions.assertFalse(component.getAriaDescription().isPresent());
40+
}
41+
42+
@Test
43+
void setAriaDescribedBy() {
44+
TestComponent component = new TestComponent();
45+
Assertions.assertFalse(component.getAriaDescribedBy().isPresent());
46+
47+
component.setAriaDescribedBy("description-id");
48+
Assertions.assertEquals("description-id",
49+
component.getAriaDescribedBy().get());
50+
51+
component.setAriaDescribedBy((String) null);
52+
Assertions.assertFalse(component.getAriaDescribedBy().isPresent());
53+
}
54+
55+
@Test
56+
void setAriaDescribedByComponent_withoutId() {
57+
UI ui = new UI();
58+
TestComponent component = new TestComponent();
59+
TestComponent descriptionComponent = new TestComponent();
60+
ui.add(component, descriptionComponent);
61+
62+
component.setAriaDescribedBy(descriptionComponent);
63+
ui.getInternals().getStateTree().runExecutionsBeforeClientResponse();
64+
65+
String generatedId = descriptionComponent.getId().get();
66+
Assertions.assertTrue(generatedId.startsWith("ariadescribedby-"));
67+
Assertions.assertEquals(generatedId,
68+
component.getAriaDescribedBy().get());
69+
}
70+
71+
@Test
72+
void setAriaDescribedByComponent_withIdSetBefore() {
73+
UI ui = new UI();
74+
TestComponent component = new TestComponent();
75+
TestComponent descriptionComponent = new TestComponent();
76+
ui.add(component, descriptionComponent);
77+
78+
descriptionComponent.setId("description-id");
79+
component.setAriaDescribedBy(descriptionComponent);
80+
ui.getInternals().getStateTree().runExecutionsBeforeClientResponse();
81+
82+
Assertions.assertEquals("description-id",
83+
component.getAriaDescribedBy().get());
84+
}
85+
86+
@Test
87+
void setAriaDescribedByComponent_withIdSetAfter() {
88+
UI ui = new UI();
89+
TestComponent component = new TestComponent();
90+
TestComponent descriptionComponent = new TestComponent();
91+
ui.add(component, descriptionComponent);
92+
93+
component.setAriaDescribedBy(descriptionComponent);
94+
descriptionComponent.setId("description-id");
95+
ui.getInternals().getStateTree().runExecutionsBeforeClientResponse();
96+
97+
Assertions.assertEquals("description-id",
98+
component.getAriaDescribedBy().get());
99+
}
100+
}

0 commit comments

Comments
 (0)