Real-World Scenarios: A Shared Icon Picker's New Required Field Blocked Saves on Three Unrelated Components
A compliance-driven field added to one shared dialog subtree quietly became a required field on three components that reused it via node-level sling:resourceSuperType — tracing the save-blocking bug back to an invisible dialog dependency, and the CI check that finds every consumer before a change ships.
Background reading: Policy-Driven Field Visibility and Node-Level Dialog Subtree Reuse covers the mechanics this post assumes.
Problem Statement
A compliance requirement meant every icon needed an accessible label for screen readers. The icon picker team added one field — accessibleLabel, marked required — to their component's shared dialog subtree at .../iconpicker/v1/iconpicker/cq:dialog/content/items/icon, tested it on their own component, and shipped it.
Within a day, three unrelated teams filed the same bug independently: authors editing a hero carousel's icon, a CTA button's icon, and a promo banner's icon could no longer save — Coral UI's client-side validation blocked the save dialog with a required-field error for a field none of those teams had ever added, on a tab their own component's code didn't reference anywhere. None of the three teams had touched the icon picker component recently, which made the bug genuinely confusing to trace: from their side, nothing in their own dialog XML had changed.
The actual cause: each of those three components reused the icon picker's dialog subtree via a node-level sling:resourceSuperType pointing directly at that path. Because that's a live merge, not a copied snapshot, the new required field appeared in all three the moment it was added to the source — with nothing in any of the three teams' own code or content history showing why.
Approach and Why
Two things needed to happen, matching the two-part pattern used for the earlier incidents in this series:
- Immediate fix: change the new field from required to optional at the source, since a screen-reader label being highly encouraged is a reasonable ask, but blocking saves on three components that had no warning is not an acceptable way to enforce it. A follow-up, coordinated rollout (with each consuming team notified) is the right way to eventually make it required everywhere it matters.
- Prevent recurrence: the real gap was that nobody working on the icon picker's dialog had any way to know, at review time, that three other components structurally depended on that exact subtree. A CI check that finds every consumer of a reused dialog subtree, given the subtree's path, turns an invisible dependency into a visible one — surfaced automatically whenever that subtree changes, not only when someone thinks to grep for it.
Making the new field optional rather than reverting it outright preserves the actual compliance goal (the field exists, authors are prompted for it) while removing the specific defect (a blocking requirement with no warning).
POC
class SharedDialogSubtreeConsumerTest {
private static final String ICON_PICKER_SUBTREE =
"myapp/components/core/iconpicker/v1/iconpicker/cq:dialog/content/items/icon";
@Test
void changesToASharedSubtreeAreCheckedAgainstEveryKnownConsumer() throws Exception {
List<Path> allDialogFiles = Files.walk(Path.of("src/main/content/jcr_root/apps"))
.filter(path -> path.toString().endsWith("_cq_dialog/.content.xml"))
.collect(Collectors.toList());
List<String> consumers = new ArrayList<>();
for (Path dialogFile : allDialogFiles) {
String content = Files.readString(dialogFile);
if (content.contains(ICON_PICKER_SUBTREE)) {
consumers.add(dialogFile.toString());
}
}
// This isn't a pass/fail assertion on its own - it's a required manual
// sign-off gate: any PR touching the icon picker's dialog subtree must
// list every path here in its description, confirming each consumer
// was checked against the change before merge.
assertThat(consumers)
.as("every component reusing the icon picker subtree - review before merging a change to it")
.isNotEmpty();
System.out.println("Consumers of " + ICON_PICKER_SUBTREE + ":");
consumers.forEach(System.out::println);
}
@Test
void newFieldsAddedToASharedSubtreeMustNotBeRequiredByDefault() throws Exception {
Document iconPickerDialog = parse(
"src/main/content/jcr_root/apps/myapp/components/core/iconpicker/v1/iconpicker/_cq_dialog/.content.xml");
List<Element> requiredFieldsInSharedSubtree = findRequiredFields(iconPickerDialog, "icon");
assertThat(requiredFieldsInSharedSubtree)
.as("fields in a subtree reused by multiple unrelated components should not be required - "
+ "a required field here silently blocks saves everywhere the subtree is reused")
.isEmpty();
}
}
The second test is the one that would have caught this specific incident directly — it fails the moment accessibleLabel is marked required="{Boolean}true" inside the shared icon node, regardless of which component's dialog happens to be checked.
Implementation
Dialog fix — field kept, requirement relaxed:
<!-- BEFORE: silently became required on every reusing component -->
<accessibleLabel
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Accessible Label"
name="./accessibleLabel"
required="{Boolean}true"/>
<!-- AFTER: encouraged, not blocking, until every consumer is notified -->
<accessibleLabel
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Accessible Label"
fieldDescription="Recommended for screen reader support."
name="./accessibleLabel"/>
Consumer-discovery helper, the piece that turns "nobody knew" into "everybody who needs to know gets flagged automatically":
final class SharedSubtreeConsumerScanner {
private SharedSubtreeConsumerScanner() {
}
static List<Path> findConsumers(Path appsRoot, String subtreePath) throws IOException {
try (Stream<Path> paths = Files.walk(appsRoot)) {
return paths
.filter(path -> path.toString().endsWith("_cq_dialog/.content.xml"))
.filter(path -> referencesSubtree(path, subtreePath))
.collect(Collectors.toList());
}
}
private static boolean referencesSubtree(Path dialogFile, String subtreePath) {
try {
return Files.readString(dialogFile).contains(subtreePath);
} catch (IOException e) {
return false;
}
}
}
A small script wired into the icon picker component's own build, run whenever its _cq_dialog changes, printing every consumer path directly into the build log so a reviewer sees the blast radius without having to think to check for it.
Rollout Steps
- Relaxed
accessibleLabelfrom required to optional immediately, unblocking all three affected teams within the same day. - Ran the consumer scanner against the full codebase, found exactly the three known affected components plus one more (a footer social-icon component) that hadn't yet been reported but had the same dependency.
- Notified all four consuming teams directly, gave them a two-sprint window to add the accessible label proactively, then scheduled the field to become required again after that window — this time as an announced, coordinated change instead of a silent one.
- Wired the consumer scanner into the icon picker component's CI pipeline so any future change to that dialog subtree prints its consumer list automatically in the build output, and added the "shared subtree fields must not be required" contract test to the icon picker's own test suite.
Why This Approach Held Up
Making the consumer list visible in CI output turns "who depends on this" from tribal knowledge into something anyone touching the file can see without asking around — which is the actual missing piece, since the merge mechanism itself was working exactly as designed. The "no required fields in a shared subtree" contract test encodes the specific lesson from this incident as a standing rule rather than a one-time fix, so the next well-intentioned compliance field doesn't reintroduce the same blast radius by accident.
Enjoyed this chapter?
Get an email when I publish the next chapter. No spam — just new technical deep-dives.
Comments
Share feedback or questions about this blog post.
No comments yet. Be the first to share your thoughts.