|
16 | 16 | package com.vaadin.flow.component.breadcrumbs; |
17 | 17 |
|
18 | 18 | import java.io.Serializable; |
| 19 | +import java.util.Collection; |
| 20 | +import java.util.List; |
19 | 21 | import java.util.Objects; |
20 | 22 |
|
| 23 | +import org.jspecify.annotations.Nullable; |
| 24 | + |
21 | 25 | import com.fasterxml.jackson.annotation.JsonInclude; |
22 | 26 | import com.vaadin.experimental.FeatureFlags; |
23 | 27 | import com.vaadin.flow.component.AttachEvent; |
|
31 | 35 | import com.vaadin.flow.component.dependency.JsModule; |
32 | 36 | import com.vaadin.flow.component.dependency.NpmPackage; |
33 | 37 | import com.vaadin.flow.component.shared.HasThemeVariant; |
| 38 | +import com.vaadin.flow.function.SerializableFunction; |
34 | 39 | import com.vaadin.flow.internal.JacksonUtils; |
| 40 | +import com.vaadin.flow.internal.nodefeature.SignalBindingFeature; |
| 41 | +import com.vaadin.flow.signals.Signal; |
35 | 42 |
|
36 | 43 | /** |
37 | 44 | * Breadcrumbs is a component for displaying a navigation trail that shows the |
@@ -66,6 +73,8 @@ public enum Mode { |
66 | 73 |
|
67 | 74 | private Mode mode; |
68 | 75 |
|
| 76 | + private boolean internalChildUpdate; |
| 77 | + |
69 | 78 | private BreadcrumbsI18n i18n; |
70 | 79 |
|
71 | 80 | /** |
@@ -106,15 +115,130 @@ public Mode getMode() { |
106 | 115 | * @param newMode |
107 | 116 | * the mode that determines how the trail is populated, not |
108 | 117 | * {@code null} |
| 118 | + * @throws IllegalStateException |
| 119 | + * if a children binding set via {@code bindChildren} is active; |
| 120 | + * such a binding takes over the trail and cannot be handed back |
| 121 | + * to component-controlled population |
109 | 122 | */ |
110 | 123 | public void setMode(Mode newMode) { |
111 | 124 | if (newMode == mode) { |
112 | 125 | return; |
113 | 126 | } |
| 127 | + if (hasChildrenBinding()) { |
| 128 | + throw new IllegalStateException( |
| 129 | + "Cannot change the mode while a children binding is active."); |
| 130 | + } |
114 | 131 | this.mode = newMode; |
115 | 132 | // Listener register/unregister and the initial router rebuild are |
116 | 133 | // deferred to a later task; for now just clear the existing children. |
117 | | - removeAll(); |
| 134 | + updateChildrenInternal(List.of()); |
| 135 | + } |
| 136 | + |
| 137 | + @Override |
| 138 | + public void add(BreadcrumbsItem... components) { |
| 139 | + checkManualMutationAllowed(); |
| 140 | + HasComponentsOfType.super.add(components); |
| 141 | + } |
| 142 | + |
| 143 | + @Override |
| 144 | + public void add(Collection<BreadcrumbsItem> components) { |
| 145 | + checkManualMutationAllowed(); |
| 146 | + HasComponentsOfType.super.add(components); |
| 147 | + } |
| 148 | + |
| 149 | + @Override |
| 150 | + public void remove(BreadcrumbsItem... components) { |
| 151 | + checkManualMutationAllowed(); |
| 152 | + HasComponentsOfType.super.remove(components); |
| 153 | + } |
| 154 | + |
| 155 | + @Override |
| 156 | + public void remove(Collection<BreadcrumbsItem> components) { |
| 157 | + checkManualMutationAllowed(); |
| 158 | + HasComponentsOfType.super.remove(components); |
| 159 | + } |
| 160 | + |
| 161 | + @Override |
| 162 | + public void removeAll() { |
| 163 | + checkManualMutationAllowed(); |
| 164 | + HasComponentsOfType.super.removeAll(); |
| 165 | + } |
| 166 | + |
| 167 | + @Override |
| 168 | + public void addComponentAsFirst(BreadcrumbsItem component) { |
| 169 | + checkManualMutationAllowed(); |
| 170 | + HasComponentsOfType.super.addComponentAsFirst(component); |
| 171 | + } |
| 172 | + |
| 173 | + @Override |
| 174 | + public void addComponentAtIndex(int index, BreadcrumbsItem component) { |
| 175 | + checkManualMutationAllowed(); |
| 176 | + HasComponentsOfType.super.addComponentAtIndex(index, component); |
| 177 | + } |
| 178 | + |
| 179 | + @Override |
| 180 | + public void replace(BreadcrumbsItem oldComponent, |
| 181 | + BreadcrumbsItem newComponent) { |
| 182 | + checkManualMutationAllowed(); |
| 183 | + HasComponentsOfType.super.replace(oldComponent, newComponent); |
| 184 | + } |
| 185 | + |
| 186 | + @Override |
| 187 | + public <V extends @Nullable Object, S extends Signal<V>> void bindChildren( |
| 188 | + Signal<List<S>> list, |
| 189 | + SerializableFunction<S, BreadcrumbsItem> childFactory) { |
| 190 | + checkManualMutationAllowed(); |
| 191 | + HasComponentsOfType.super.bindChildren(list, childFactory); |
| 192 | + } |
| 193 | + |
| 194 | + /** |
| 195 | + * Throws if manual child mutation is not allowed in the current mode. |
| 196 | + * <p> |
| 197 | + * Mutating children is rejected while in {@link Mode#ROUTER}, unless the |
| 198 | + * internal {@link #internalChildUpdate} flag is set (i.e. the change |
| 199 | + * originates from an internal child update such as a mode switch or a |
| 200 | + * router-driven rebuild via {@link #updateChildrenInternal(List)}). |
| 201 | + */ |
| 202 | + private void checkManualMutationAllowed() { |
| 203 | + if (mode == Mode.ROUTER && !internalChildUpdate) { |
| 204 | + throw new IllegalStateException( |
| 205 | + "Cannot modify breadcrumb items manually in Mode.ROUTER. " |
| 206 | + + "Switch to Mode.MANUAL to manage items directly."); |
| 207 | + } |
| 208 | + } |
| 209 | + |
| 210 | + /** |
| 211 | + * Replaces the current children with the given trail, bypassing the |
| 212 | + * {@link Mode#ROUTER} mutation guard for the duration of the update. Used |
| 213 | + * both for the router-driven rebuild and for clearing children on a mode |
| 214 | + * switch. |
| 215 | + * |
| 216 | + * @param trail |
| 217 | + * the breadcrumb items to set as the new children |
| 218 | + */ |
| 219 | + void updateChildrenInternal(List<BreadcrumbsItem> trail) { |
| 220 | + internalChildUpdate = true; |
| 221 | + try { |
| 222 | + removeAll(); |
| 223 | + add(trail.toArray(BreadcrumbsItem[]::new)); |
| 224 | + } finally { |
| 225 | + internalChildUpdate = false; |
| 226 | + } |
| 227 | + } |
| 228 | + |
| 229 | + /** |
| 230 | + * Checks whether a children binding (set via {@code bindChildren}) is |
| 231 | + * currently active on this component's element. Mirrors the internal check |
| 232 | + * Flow core performs, reading the binding state from the element node. |
| 233 | + * |
| 234 | + * @return {@code true} if a children binding is active |
| 235 | + */ |
| 236 | + private boolean hasChildrenBinding() { |
| 237 | + return getElement().getNode() |
| 238 | + .getFeatureIfInitialized(SignalBindingFeature.class) |
| 239 | + .map(feature -> feature |
| 240 | + .hasBinding(SignalBindingFeature.CHILDREN)) |
| 241 | + .orElse(false); |
118 | 242 | } |
119 | 243 |
|
120 | 244 | @Override |
|
0 commit comments