-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrate breadcrumbs to Stimulus (RevealController
/ w-breadcrumb
)
#10793
Migrate breadcrumbs to Stimulus (RevealController
/ w-breadcrumb
)
#10793
Conversation
Manage this branch in SquashTest this branch here: https://lb-feature10118-migrate-breadc-dd2fd.squash.io |
6d1bf0a
to
ef8814c
Compare
Just cleaned up a few things upon re-reading the PR; some duplicate HTML in the unit tests, a better solution for the initial Finally, the 'ready' event now fires after a setTimeout, this should allow any other non-Stimulus JS on initial load to listen to these events a bit easier. Less chance of a race condition between Stimulus & non-Stimulus code (e.g. minimap listeners / tooltip listeners). |
ef8814c
to
76b1550
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this PR! In terms of functionality, it seems to work as expected (I didn't see any change in user-facing behaviour). I can't wait to use it to work on #8767!
However, I find it hard to review because the refactoring is done in a single commit that simultaneously handles other cases that aren't actually observed in Wagtail's breadcrumb templates.
I appreciate that you're trying to pave the way for migrating the panels to Stimulus, which I think would make sense to use the same controller if possible. That said, it's much easier to review if we try to build the minimum Stimulus-equivalent to breadcrumb.js
first, and then try to handle other cases in subsequent commits/PRs. It helps the reviewer be much more confident in understanding what exactly the changes are trying to accomplish, which makes it more likely to be merged faster 😃
if (isClassNameClosed && (!ariaExpanded || ariaExpanded === 'false')) { | ||
this.closedValue = true; | ||
} else if (ariaExpanded === 'false') { | ||
this.closedValue = true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if the following is equivalent and easier to read?
if (isClassNameClosed && (!ariaExpanded || ariaExpanded === 'false')) { | |
this.closedValue = true; | |
} else if (ariaExpanded === 'false') { | |
this.closedValue = true; | |
} | |
if (ariaExpanded === 'false') { | |
this.closedValue = true; | |
} else if (isClassNameClosed && !ariaExpanded) { | |
this.closedValue = true; | |
} |
or maybe even
if (isClassNameClosed && (!ariaExpanded || ariaExpanded === 'false')) { | |
this.closedValue = true; | |
} else if (ariaExpanded === 'false') { | |
this.closedValue = true; | |
} | |
// if aria-expanded is set to false, | |
// or the closed class is used but aria-expanded is unset, | |
// ensure it's closed | |
if (ariaExpanded === 'false' || (isClassNameClosed && !ariaExpanded)) { | |
this.closedValue = true; | |
} |
That said, I'm not sure what kind of use case we're trying to handle here and why we need to be smart about it. Is it in case the initial values of aria-expanded
, the closedValue
, and isClassNameClosed
are inconsistent? If so, that sounds like a developer error due to misconfiguration in the templates to me and not something we need to explicitly handle case-by-case. Like, in here, what happens if ariaExpanded
is true
but closedValue
is also true?
Can we just rely on the initial value of closedValue
as the single source of truth and enforce it by calling closedValueChanged
or some way? I'd much prefer us have one thing that acts as the initial value of the expand/collapsed state, and the other attributes (aria-expanded
, class
) would follow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is mostly to set up for the panels use case where you can add the collapse
class and it will load in the DOM as collapsed.
https://github.com/wagtail/wagtail/blob/main/client/src/includes/panels.ts#L67
It's confusing I know but it will be required for the Panels usage alongside dynamic usage in StreamFields.
I can remove this for now and we can just discuss the implementation when ready for the Panels work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has been removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh yes, sorry, I completely forgot about the fact that we support the collapse
class for panels. But yep, while it's necessary for panels, we can focus on that later.
Maybe we can look into alternatives such as providing the appropriate Stimulus attribute from the Python side if the collapse
class is used. Or a different approach entirely e.g. a collapsed
flag on the Panel
instance itself, with the Stimulus attributes handled in the base class. A breaking change, but I think would be a welcome one. In any case, it's easier to focus on exploring those approaches later than trying to fit it in here. Thanks for the update!
const iconClass = | ||
icon && | ||
(!hasCloseIconClass || !hasOpenIconClass) && | ||
[...icon.classList].find((className) => className.startsWith('icon-')); | ||
|
||
if (iconClass) { | ||
if (!hasCloseIconClass) { | ||
if (iconClass) { | ||
this.element.setAttribute( | ||
`data-${this.identifier}-close-icon-class`, | ||
iconClass, | ||
); | ||
} | ||
} else if (!hasOpenIconClass) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, I don't know why we need to be "smart" about this. I think that instead of trying to figure out and resolve values/classes that are unset in the HTML, it's better if we agree on what the expected attributes for this controller are, and make it so that the HTML templates are consistently configured. At least for now, it's much easier to understand that way, as the code doesn't have to deal with cases like "what if attribute X isn't set?".
There are cases where optional attributes make sense and it's pretty clear to give sensible defaults, but at this point most of these cases still feel very hypothetical and make the code harder to review.
In other words: if you're configuring the HTML in a way that we don't currently use within Wagtail, it's going to result in an error (like the missing content
target) or undefined behaviour, and that's OK. If we do find use cases where it's possible to reuse the code with a slightly different attribute configuration, then we start handling such variations and making the controller code more robust.
If we do it too early, it's premature abstraction. We don't yet know what the other use cases would look like, so the abstraction/handling we put in place may or may not be necessary in the future, and if it does, it may not retain its shape as-is. Until then, it's hard to read the code without imagining what exactly the case is here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, this can be removed and added back later when needed for the panels or maybe we just change the approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has been removed.
); | ||
}); | ||
|
||
it('should allow for a class to set the initial state of closed', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yep, I just read the test after reviewing the implementation. This and the following two cases are what I meant, I think we should just consistently use the closedValue
's attribute as the single source of truth for the initial state, ignoring (and resolving) the others (class and aria-expanded) based on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree this is a bit convoluted, removing for now and IF we need it built this way for the panels implementation, we can revisit then.
Thanks for the feedback here.
* Adds the ability to make the controlled element be used as an | ||
* opening/closing (aka collapsing) element. | ||
* Supports, and relies on, correct usage of aria-* attributes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this description I think it's more fitting if we name it something like ExpandController
or CollapsibleController
. The former seems short and expressive enough, but not sure if that would cause any confusion with aria-expand
. I don't think so, and it makes sense (at least to me) that this would control aria-expand
just because it's clearly controlling an element that can be expanded/collapsed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I started this way when first looking into this but ran into a few issues.
- Common typos - Collapsible vs Collapsable - https://thecontentauthority.com/blog/collapsable-vs-collapsible
- Longer name makes all attributes longer
- It makes it tricky to NOT use the 'state' of expanded or collapsed and the 'actions' of expand or collapse. Using hide/show is not the most suitable here and is better for things that fully hide/show (e.g. tooltips, dialog use hide/show).
Additionally, based on feedback on the Panel issue #10168 (comment) we found that there is already a nice precedent in the community for naming this https://www.stimulus-components.com/docs/stimulus-reveal-controller/
Our existing implementations have the following
- https://github.com/wagtail/wagtail/blob/main/client/src/includes/breadcrumbs.js (open/closed)
- https://github.com/wagtail/wagtail/blob/main/client/src/entrypoints/admin/preview-panel.js (show/hide)
- https://github.com/wagtail/wagtail/blob/main/client/src/includes/panels.ts (expand/collapse)
If it's critical I am more than happy to re-write this using the Expand/Collapse naming but I did go down this way already and it felt more verbose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good points, yeah with ExpandController
I kind of wondered if it's going to be confusing with actions/states, not just aria-expanded
. Thanks for the link to the precedent, I'm happy to keep the naming 👍
client/src/controllers/index.ts
Outdated
@@ -34,6 +35,8 @@ export const coreControllerDefinitions: Definition[] = [ | |||
{ controllerConstructor: DropdownController, identifier: 'w-dropdown' }, | |||
{ controllerConstructor: MessagesController, identifier: 'w-messages' }, | |||
{ controllerConstructor: ProgressController, identifier: 'w-progress' }, | |||
{ controllerConstructor: RevealController, identifier: 'w-breadcrumb' }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason why the singular breadcrumb
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
haha, it's always been like that since the initial version @thibaudcolas built data-breadcrumb-next
not data-breadcrumbs-next
.
I am not sure, but have kept with the convention that exists, especially since the root class is w-breadcrumb
not w-breadcrumbs
.
If you think it's worth changing, I would 100% prefer the plural form here but would want to get the class changed also.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't really mind either way, but I also would prefer the plural form. If it were only data-breadcrumb-next
that we're dealing with, I think it's worth renaming, but as we also have w-breadcrumb
in the CSS, might be better to stick with the existing singular form. If Thibaud weighs in, I'm happy to go with whatever he says 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that our JS events were in the plural form so I think it is reasonable to update this across the board to be plural.
I have done this, will force push and I have included a note in the release notes about the classes change from w-breadcrumb
to w-breadcrumbs
.
76b1550
to
b0b66fd
Compare
Brilliant, thank you for testing.
Agreed, I have removed the ones that are clearly isolated from the core open/close behaviour and class setting/resetting. I can remove the @laymonage - really appreciate the thorough review, let me know if you really disagree with the |
Thanks for the updates LB! I'm off tomorrow but I'll re-review when I'm back on Monday. Really appreciate the fast response 😃 |
b0b66fd
to
02b6766
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, I think this is easier to understand now.
I would've much preferred it if we hadn't handled the "fall back to aria-controls
if the toggle target is not specified" just yet, or at least add it in a separate commit.
It seems the code would be much easier to understand if we had one commit that did the initial RevealController
implementation just by sticking to how the breadcrumbs.html
template uses the controller, i.e. the icon classes, toggle target, and content target are all well-defined in the template.
Then, a separate commit can be added that does all the "smart" tricks like falling back to aria-controls
, allowing the toggle target to be outside of the controller's scope, etc. Or, we can even leave that in a separate PR, with the accompanying concrete use case (e.g. panels migration).
At this point those changes are completely irrelevant to the breadcrumbs code, so I found them too distracting when reviewing. That said, it's already done and I've already reviewed it though, so I suppose it's fine to leave them in.
Anyway, for the breadcrumbs, I've tested in the following places:
- Page explorer
- Page edit
- Page chooser
- Snippets
And I didn't notice change in behaviour, other than the hidden="until-found"
I mentioned, so hopefully we're good after that.
/** | ||
* Use experimental `until-found` value, so users can search inside the content. | ||
* Browsers without support for `until-found` will not have this value set | ||
*/ | ||
if ('onbeforematch' in document.body) { | ||
content.setAttribute('hidden', 'until-found'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL!
But this doesn't seem to work as expected?
Screen.Recording.2023-09-04.at.17.10.54.mov
Or do you need to listen to the beforematch
event and toggle the expand?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The beforematch listener needs to be in the html actions. Good find, I'll revisit this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually sorry, beforematch won't make sense for breadcrumbs as they are set to hidden
by default.
I will remove this functionality completely, thanks for raising.
initialize() { | ||
const toggleTarget = this.toggleTarget; | ||
|
||
if (!this.hasContentTarget) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I understand, this aria-controls
handling is still unused in our usage of the breadcrumbs, and only here to support potential future RevealController
usages. Is that correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably should use it in the breadcrumbs but I didn't want to add too much to the html. We would need to add an ID to each Breadcrumbs item and then also add the set of the IDs to the toggle.
But yes, it's not used by the breadcrumbs, but to add that in another PR seemed quite noisy. So I opted to keep it in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, I have cleaned this up further, sorry for the runaround, new update coming.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, adding an ID to each breacrumb item is not practical without a larger change to the breadcrumbs include/shared template usage as we would probably need a base ID to build on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!
We probably should use it in the breadcrumbs but I didn't want to add too much to the html. We would need to add an ID to each Breadcrumbs item and then also add the set of the IDs to the toggle.
Unfortunately, adding an ID to each breacrumb item is not practical without a larger change to the breadcrumbs include/shared template usage as we would probably need a base ID to build on.
That is exactly why I much preferred not having the code to handle aria-controls
just yet. The breadcrumbs HTML code in this PR works without changing much of the HTML (just sprinkling some Stimulus attributes is enough), so I didn't see why we need to add the aria-controls
assumption for the toggles in the controller if it isn't used.
And I wasn't expecting you to add the aria-controls
to the HTML so that the code is used, but rather: we stick to Stimulus attributes for now, and see if we can reintroduce the aria-controls
handling later when there's a more appropriate use case, e.g. the panels where they already have aria-controls
and id
s and it's much simpler to use those there instead of adding Stimulus attributes.
Sorry if I wasn't clear, glad you didn't proceed with it 😅
02b6766
to
260575e
Compare
- Adds a new generic `RevealController` that handles the ability for content to be opened/closed while updating the relevant parts of the DOM such as aria-expanded on the toggle. - Register this controller as a generic `w-reveal` and also the `w-breadcrumb` identifiers so that we can differentiate usage via different DOM attributes and events. - Closes wagtail#10118
- This way we align with the controller name
260575e
to
bad0faa
Compare
Thanks @laymonage - I hope this is in a better shape for the initial implementation. There is still some code that relates to aria-controls in place but this is stuff that intertwines with other code and hopefully it's accepted as is. Should be good for another review now, all the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! This is much, much easier to understand as the controller code is now closer to the most minimum implementation required to migrate the breadcrumbs to Stimulus.
It could be even more simplified if we just assume that there's only one toggle and that's only linked as a Stimulus target (via toggleTarget
) instead of using toggleTargets
and also gathering the elements that use aria-controls
to the contentTargets
. I believe that would be sufficient for the breadcrumbs without any other changes to the HTML.
Or, at least using toggleTargets
directly without scanning for aria-controls
, so we can keep all the handling of "multiple toggles" in other methods without necessarily handling the aria-controls
case just yet. Later, we can add the logic in get toggles()
that concats toggleTargets
with the aria-controls
for the contentTargets
, when we actually do have the use case for it.
However, I understand that it could be useful later on e.g. with the panels, and it's simple enough to understand so I'm happy to keep that in.
Again, thank you very much! I'll leave this unmerged in case you want to make additional tweaks.
Thank you @laymonage - appreciate the feedback. I know it's a bit of a stretch to include some code for 'future', thanks for being OK with some of this being included. |
- This is not used and was added as part of wagtail#10793 - Avoid this controller managing this behaviour and instead if needed we can use w-init
- This is not used and was added as part of wagtail#10793 - Avoid this controller managing this behaviour and instead if needed we can use w-init
- This is not used and was added as part of #10793 - Avoid this controller managing this behaviour and instead if needed we can use w-init
Overview
RevealController
that handles the ability for content to be opened/closed while updating the relevant parts of the DOM such as aria-expanded on the toggle.w-reveal
and also thew-breadcrumbs
(plural) identifiers so that we can differentiate usage via different DOM attributes and events.w-breadcrumbs
notw-breadcrumb
.panels
to a Stimulus controllerw-panel
#10168w-breadcrumbs
#10118Testing
Checklist