Problem/Motivation
The settings tray currently uses the dialog system and is always rendered on the right side of the page. Some modules may have a need for the tray to appear at the top of the page.
Proposed resolution
Add a new value off_canvas_topfor the data-dialog-renderer attribute when creating a dialog link.
This will make the off-canvas dialog to render at the top of the page.
Remaining tasks
Commit.
User interface changes
Module will be able to create off-canvas dialog links where the dialog comes down from the top of the page instead of the side.
API changes
There will be a new value off_canvas_topavailable for the data-dialog-renderer attribute when creating a dialog link.
To create a off-canvas-top dialog link us the following
'off_canvas_top_link_1' => [
'#title' => 'Open top panel 1',
'#type' => 'link',
'#url' => Url::fromRoute('off_canvas_test.thing1'),
'#attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas_top',
],
],
Note this the same as the current off-canvas dialog except for the off_canvas_top value.
Data model changes
None
Manually testing
To manually test this patch
- Enabled the
off_canvas_testtest module. - Goto
/off-canvas-test-links - Click the various links to open the dialog either on the side or on the top.
| Comment | File | Size | Author |
|---|---|---|---|
| #86 | 2916781-86.patch | 33.29 KB | timmillwood |
| #86 | interdiff-2916781-86.txt | 1.68 KB | timmillwood |
| #81 | 2916781-81.patch | 33.32 KB | GrandmaGlassesRopeMan |
| #79 | interdiff-2916781-78-79.txt | 3.2 KB | GrandmaGlassesRopeMan |
| #79 | 2916781-79.patch | 33.32 KB | GrandmaGlassesRopeMan |
Comments
Comment #2
tedbowThis functionality should be doable because of #2844261: Allow dialog links to specify a renderer in addition to a dialog type
The way envision it working is
I think we should tackle this after #2784443: Move off-canvas functionality from Settings tray module into drupal.dialog.ajax library so that other modules can use it because then modules would not need the Settings Tray module installed to use the dialogs.
So after that issue we would need to
\Drupal\Core\Render\MainContent\OffCanvasRenderer::__constructto accept another argument$positiondefaulting to "side".main_content_renderer.off_canvas_topthat would duplicatemain_content_renderer.off_canvasaccept it would send another parameter "top".\Drupal\Core\Ajax\OpenOffCanvasDialogCommand::__construct\Drupal\Core\Ajax\OpenOffCanvasDialogCommand::__constructadd classdialog-off-canvas-topclass to the dialogoff-canvas.*.cssfiles to position based ondialog-off-canvas-topoff-canvas.es6.jsnot also ways displace from the side.Comment #3
tedbow#2784443: Move off-canvas functionality from Settings tray module into drupal.dialog.ajax library so that other modules can use it was committed 🎉! So removing from
settings_tray.modulecomponent.@timmillwood was there a UX discussion/issue about whether to use existing off-canvas as it is vs from the top. Why I ask is that one of the reasons we wanted to get the off-canvas dialog out of the Settings Tray and offer it as a stand alone dialog was so that other parts of core and contrib could use. The idea being that it is more unified experience if this kind of configuration always appears in the same place for the user.
Another thing I realized is that the CSS reset made in https://www.drupal.org/node/2826722 we specifically choose an ID selector because it is the most specific outside of inline css. If the off-canvas-top want to use this same reset then we would have to make 1 version of the off-canvase closed the other so that you couldn't have both the top and regular off-canvas open at the same(unless we found another way). Actually that seems like totally reasonable UX rule because both version would need resize the page.
Comment #4
timmillwoodInitial patch based on the steps in #2.
Comment #6
tedbowI like the reuse of the same class here!
We would want the default value for
$positionto be "side" since the current off-canvas is only on the right side for LTR languages.Comment #7
timmillwoodThis is really testing my JavaScript stills, learning new things is fun!
The updated patch should fix the failing tests, as well as adding a new test for off_canvas_top using off_canvas_test module.
It also contains some updates to off-canvas.es6.js to handle the position element. You can see in the gif below I've started by limiting "top" dialogs to 300 pixels, and adding the relevant padding (which looks a little off).

One question, where are the width and right / left element values set, it doesn't seem to be in off-canvas.es6.js?
Next on my list:
Comment #8
GrandmaGlassesRopeManShould have spaces around the properties.
Should have spaces around the properties.
Instead of calling `Drupal.offCanvas.getPostion()` twice, can we store the response and use it in the ternary?
Incorrect spacing around properties.
Same feedback as above.
Incorrect spacing around properties.
Incorrect spacing around properties.
If we're going to assign `height` to a variable, use object-shorthand.
Same feedback as above.
This can be written a bit more simply, also no jQuery :)
Comment #9
tedbowI don't think this should have to be done in JS at all. Instead of relying on the class. It would be better to in
\Drupal\Core\Ajax\OpenOffCanvasDialogCommand::__construct()just set$this->dialogOptions['drupalOffCanvasPostion'] = $position;For
dialogOptionsif we want to pass an option that is not a jQuery UI dialog option then we just prefix it withdrupal. That way we can pass options that will be ignore by jquery dialog but available in JS for us.Comment #10
tedbowI think you would only want to worry about
width !== mainCanvasPaddingif$position == 'side'Also the existing code
We attach handlers which up to now have used to handle resizing based on width, when the dialog itself is resized and when the body is resized.
It seems like the JS logic might be simpler if we attach the existing handlers only when
$postion == 'side'. If$position == 'top'then we could attach different handlers that deal with special case what needs to happen on top.Also in
\Drupal\Core\Ajax\OpenOffCanvasDialogCommand::__construct()This should probably happen only for side. I think for `top` we would want width to be 100%. Not sure that Jquery dialog supports that but we could use .css() to set this. then resizing the body would be easier I think
Comment #11
timmillwoodThanks @drpal and @tedbow for the reviews.
I've taken the suggestion to pass in
drupalOffCanvasPosition, and that is working nicely. The js file has then been reworked, not as extensively as @tedbow suggests in #10 by having different handlers depending on position, but it does work quite well.There are a few issues though.
- When opening a side tray after a top tray the offset is not reset correctly, causing the tray to load 379px from the top.
- The top tray initially loads on the right, then flashes to the top once
afterCreate()kicks in.- The sliding animation is a bit over the top and distracting.
Comment #13
timmillwoodFixing failing tests.
Comment #15
timmillwoodHere's a quick gif showing the issues outlines in #11:

Comment #16
GrandmaGlassesRopeManAlright, some information about what's going on.
When transitioning between a top rendered tray and a right side rendered tray, without closing the tray in between, the information returned by
Drupal.displace()still contains the offsets as if the top rendered tray where still open.When
resetSize()is called, the offsets are incorrect here because the previous dialog element is not actually closed, just it's position changed. Here,const topPosition = position === 'side' && offsets.top !== 0 ? `+${offsets.top}` : '';, this will append the incorrect offset to the top.There are a couple of options which I think might be possible to fix this,
- Ensure that the dialog is closed in between transitions. Settings tray does this by manually clicking on the close button.
- If the dialog is already open and an orientation change is happening, ignore (or adjust) the values coming from
Drupal.displace()Comment #17
tedbowThis method works.
Comment #18
timmillwoodAwesome thanks @tedbow!
- When opening a side tray after a top tray the offset is not reset correctly, causing the tray to load 379px from the top.- The top tray initially loads on the right, then flashes to the top once afterCreate() kicks in.
- The sliding animation is a bit over the top and distracting.
Only two more issues left to resolve, I'll try and work out what's going on today. The sliding animation should be an easy fix, I assume just dial down the CSS. As for the initial load on the right, I assume we need to some how set some more neutral initial settings, or
display: none;untilafterCreate()is called?Comment #19
timmillwoodResolved the flash of loading on the right by setting better initial height and width.
- When opening a side tray after a top tray the offset is not reset correctly, causing the tray to load 379px from the top.- The top tray initially loads on the right, then flashes to the top once afterCreate() kicks in.- The sliding animation is a bit over the top and distracting.
Comment #20
timmillwoodUpdated the animation CSS so it's faster for the top dialog, but still the same for the side dialogs. Here's a quick demo:

Ready for review / RTBC IMHO!
Comment #21
tedbowTested it. Looks much better with the new animation.
@timmillwood can you post the related issue(s) here as where this is actually needed?
We could use a dataprovider here too to test both positions.
Should make a data provider for this test to test top and side?
Comment #22
timmillwoodThis will be used for workspaces, there isn't really an issue, but we have:
Comment #23
timmillwoodUpdated the tests to add a data provider, also added a
off_canvas_siderenderer just because it feels right to have a specific one, even if we're not deprecatingoff_canvasyet.Comment #24
timmillwoodThis patch updates the top dialog to allow a configurable height. It defaults to
height: auto;, but a custom height can be set per link via adata-dialog-optionsattribute.Comment #26
timmillwoodI managed to replicate the test locally, but then after trying to debug it I can't replicate it again. So here's a patch just updating coding standards issues.
Comment #28
timmillwoodThe issue seemed to be that it wasn't waiting long enough for the dialog to appear, therefore I've updated the
$this->getSession()->wait(800)to$this->assertSession()->waitForId('#drupal-off-canvas');. However this has increased the run time of the test from 2 minutes, to 7 minutes.Comment #30
tedbowThere was something going on with
\Drupal\Tests\system\FunctionalJavascript\OffCanvasTest::testOffCanvasLinks()where it iterator over all the themes it failed after the first theme.This may be because some state that was set or javascript interaction in the previous iteration.
Anyways this loop just made the test much more complicated.
So I create a dataProvider function that runs the test once for every theme. I also reversed #28 because I think it is no longer needed.
We ran into this same problem with
\Drupal\Tests\settings_tray\FunctionalJavascript\SettingsTrayBlockFormTest::testBlocks()and remove the loop over the themes.see: #2902191: Determine cause and fix random fail in \Drupal\Tests\settings_tray\FunctionalJavascript\SettingsTrayBlockFormTest::testBlocks
In that case it was test logic error that only showed on second iteration but having that loop needlessly complicated the test as I think it does here(I probably added it originally, sorry).
We might to do a follow to remove all remaining instances of
and use this dataProvider
Comment #32
timmillwoodSeems to be the same issue as #28. When I ran locally it wasn't waiting long enough for the dialog to update. Hence the change in #29. Although I've no idea why it went from a 2min to a 7min test, surely it doesn't take that long to load the dialog. I tried to test with webdriver so I could see what it was doing, but couldn't get it working locally.
Comment #33
tedbowFrom the error I think this was only passing because the top panel had been opened previously.
Since we don't close dialog it passes before the old one closed.
Fixed by keeping track of last dialog class.
Comment #34
tedbowChanged this so all the links follow the same pattern.
Changed the link text so all links follow the same pattern.
Assigning a unique class to each link so that when you click a dialog link and a dialog is already open the test can distinguish between the old dialog and the new dialog.
Now that link text all follows the same pattern this can be simplified.
First wait for the old dialog to close, then wait for the new one to open.
Otherwise
waitForCanvasToOpenwill actually pass because the old dialog has the same class on the 2nd iteration.Both these checks are now in
waitForCanvasToOpen()Comment #35
timmillwoodAs I wrote a lot of the patch I don't think I can legally RTBC #33, but if anyone else does, it looks good to me!
Comment #36
amateescu commentedReviewed the code and manually tested with the
off_canvas_testmodule, and everything is looking great :)I think there are a couple of things that could be improved:
- the styling of the top dialog needs to remove the left border
- it would be nice if the animation triggered when the top dialog is opened would also include the toolbar. Right now, the "main window" transitions slowly to accommodate for the new thing at the top, but the toolbar moves "instantly", without any animation
But both of those things could be done in a followup, so let's get this in!
Comment #37
tedbowI don't really think it make sense to add this
off_canvas_siderenderer now.What is the advantage of doing this? I think it will just confuse developers.
Which one should they choose? What is the difference?
I know there is no difference but for a new developer this would be confusing. I would problem assume there would some difference.
I think we should just not introduce
off_canvas_sideservice until we are ready to deprecateoff_canvasservice.I think deprecating
off_canvasin 8.6 just seems like a pain to developer who just started using the off-canvas dialog in after 8.5.Comment #38
ada hernandez commented@tedbow I removed off_canvas_side and off_canvas_top services, please correct me if it was just off_canvas_side
Comment #40
timmillwood@Adita - it was just off_canvas_side to be removed, this issue is all about adding off_canvas_top
@tedbow - I think adding off_canvas_side give more clarity and specificity around what's going on, but I'm happy to argue the case in a follow up.
Comment #41
tedbow@timmillwood ok. we can figure it out in follow up.
One reason I think it is ok not have the default be side without explictly putting _side in the service is the term "off-canvas" seems to usually mean on the side.
https://www.w3schools.com/howto/howto_js_off-canvas.asp
https://foundation.zurb.com/sites/docs/v/5.5.3/components/offcanvas.html
https://www.webpagefx.com/blog/web-design/off-canvas-menu/
https://tympanus.net/codrops/2014/09/16/off-canvas-menu-effects/
Comment #42
andrewmacpherson commented@tedbow pinged us in the accessibility channel on Slack, in case this needed an accessibility review.
tl;dr - we're OK I think.
Apart from using different viewport edges, does the patch here change any other behaviour? If not, then I don't expect this to this introduce any new accessibility issues that we don't already know about.
Note we already have #2940677: Support prefers-reduced-motion in off-canvas dialog as a follow-up, and that will need to take this new feature into account. I think we should ensure that UI designs don't rely on animation, just as we don't want them to rely on colour vision. The settings tray is probably a high-risk animation for users affected by vestibular disorders, etc. - though admittedly I'm not exactly clear how to assess animation risks. I'm going by the fact that it involves a large portion of the viewport, especially on the top-edge demo. It isn't a blocker for this issue, but I think it should be a stabilty-blocker for experimental modules which employ off-canvas from now on. Especially if they're going to use trays with bigger dimensions. The good news is it looks easy enough to implement.
The most recent screenshot GIF is in #20 - is that still broadly up to date? One thing in that GIF strikes me as odd. It shows a tray on the right-hand side being dismissed when the user clicks a link on the main page, which makes a different tray slide in from the top. If the tray has an accessible role of "dialog", then this means it's behaving as a non-modal dialog. At one stage, the WAI-ARIA Authoring Practices suggested those ought to have a standard keyboard control for window-switching behaviour (like old HTML framesets, F6 I think). I went looking for the details but it isn't in the latest version of that W3C note. (I asked the editor about it on Twitter, in case it's still a thing.) It's not a blocker here, but it's on my back-burner thinking list now. Surprised I didn't wonder about it before.
Comment #43
tedbow@andrewmacpherson thanks for taking a look.
Not sure if I follow all this but so far "off-canvas" in only being using a non-modal way, that it does not stop interaction with the rest of the page. In core using
data-dialog-type=modalattribute would actually stop you from interacting with the page when the modal is open.Off-canvas uses
data-dialog-type=dialogwhich does not stop you from interacting with the rest of the page. So even before this patch you could open the off-canvas and then click another link that opens in the off-canvas which would close you previous off-canvas dialog.So this follows the same pattern. If you had 1 off-canvas dialog open, whether side or top and click another link that opened an off-canvas dialog either side or top your previous dialog would be closed.
Comment #44
andrewmacpherson commentedThanks for the extra info @tedbow. The part about
data-dialog-type=modal|dialogis handy to know. It's something to consider in #2893640: Modernize ARIA usage, in line with ARIA 1.1 and the ARIA Authoring Practices guide., in relation to the new<dialog>element from HTML 5.2 andaria-modal="true"from ARIA 1.1 which we might want to employ one day.I see, I think I saw that behaviour, but it don't think it was obvious that the off-canvas closed and re-opened (would it be animated, if they were both on the same side, and same width...?)
Comment #45
yoroy commentedIn general it's a rather weak argument to me that "some modules may have a need". With these kinds of additional options we also introduce more ways to make things inconsistent for people using these interfaces.
1. Does this only "make possible" or is using another position implemented already with this patch? I assume this is mostly in preparation for workspaces to use top positioning eventually.
2. General question: is it at all possible to show multiple trays at the same time? I hope not! :)
Thanks!
Comment #46
timmillwood1. Yes, this only makes it possible, the real implementation is in #2949991: Add workspace UI in top dialog
2. No, one tray at a time.
Comment #47
yoroy commentedThanks!
Comment #48
lauriiiHow is the off-canvas rendered in different responsive modes when it is on the top mode?
I didn't review the code yet, but will get to that next.
Comment #49
timmillwood@lauriii - The width is set to 100% so will scale, the default height set by jQueryUI is "auto", but this can be altered to a fixed height in the link's "data-dialog-options". When using a fixed height the dialog can scroll due to the current off canvas css allowing overflow.
Comment #50
timmillwoodOoops, didn't mean to RTBC it.
Comment #51
borisson_Nitpicks ahead, not setting back to Needs work because the changes are so small and they might not even be needed.
Shouldn't we be more explicit here? And pass in
side, andtopas possible positions? The fact that NULL falls back to side should be a unit or kernel-level test instead.I think this docblock needs a summary as well. It also doesn't need to be static.
Comment #52
timmillwood#51.1 - This comes back to #37 and that we don't have a
siderenderer, so added another condition when getting the wrapper format to accommodate this.#51.2 - Fixed.
Comment #53
lauriiiAdding this slightly changes the way off canvas is rendered. This causes the off-canvas to "reboot" when updating content within same off-canvas which makes the interactions less smooth.
s/with/width
I don't think we can do this since this would create dependency on toolbar module. Let's explore if there would be a way to detach this from the core toolbar implementation.
Comment #54
tedbowJust was doing some manually testing found that this breaks some functionality we weren't actually testing for:
If you don't do step 1 the link in step 3 will work.
Adding the test to the existing patch.
Also adding a patch with *only* this test change to show that somehow this patch breaks it.
Not sure why this is yet but a top panel dialog does not have to be involved for it not to work.
Comment #55
tedbowComment #57
tedbowFix patch with just new test
Comment #59
lauriiiI looked at the toolbar problem. It would be better if we make the dependency from Toolbar to off-canvas. It should quite easy with the existing architecture by listening to the events that off-canvas provides.
Also few problems I spotted:
Currently if you open off-canvas top on mobile/tablet, it is opened behind the toolbar:

We also don't support screen resizing properly because this is what happens if you open the off-canvas top on desktop and then reduce the screen size:

Comment #60
timmillwoodThis should fix the responsive issues. Although my JS skills don't stretch far enough to move the toolbar items to the toolbar module. Can anyone help?
Comment #62
timmillwoodI'm missing the test fix from #57.
Comment #63
lauriiiThis moves the toolbar related interactions to toolbar module.
Comment #65
timmillwoodThank you so much @lauriii for adding that change. I tested manually and it works perfectly.
I've tried to have a look at the failing test, and can't make sense of it. The failing test was introduced by @tedbow in #54 to test an issue we weren't testing for. When an off canvas dialog is opened (top or side), then another dialog with a link in it is opened, the link will not open in a dialog. As the steps in #54 explain perfectly.
Comment #66
tedbowThis addition added in #17 is actually the change that causes the current test failure that I described in #54.
I debugged this with @drpal and he figured out that we could fix this with adding
Drupal.ajax.bindAjaxLinks($element);inopenDialogofcore/misc/dialog/dialog.es6.js. But this now I figured out the problem is that we are closing dialog after theuse-ajaxevents have been bound this doesn't seems that best fix.I originally I added this line that is causing the failure from @drpal's suggestion in #16 but he also suggested another way in the same comment.
Maybe should try the 2nd approach he mentioned here.
Closing the dialog also the problem mention by @lauriii in #53
Comment #67
timmillwoodThanks @tedbow, I can confirm removing
$('.ui-dialog-off-canvas .ui-dialog-titlebar-close').trigger('click');fixes the failing tests, but the issue demo'd in #15 is back.I'm not really sure what I'm doing with Drupal.displace(), from some testing it doesn't seem like re-calculating the offsets will work, so ignoring them and calculating the 'top' value ourselves seems like it might be the best option. Apart from putting something in 'resetSize' I'm not really sure how to tackle this.
Comment #68
ckrinaI've been taking a look to some patterns and using the off-canvas top makes sense, specially for content that needs to have an horizontal distribution (which will eventually need a general solution on small screens). The left also makes sense and has a much simpler and known responsive solution. Thanks :)
But I'm afraid using this pattern for left&bottom situations could create some issues with OS elements like the Dock&similar in desktops or collide with Drupal elements on the left (like the Toolbar). Would it be possible to limit it somehow to only use the top&right positions?
Comment #69
tedbow@ckrina thanks for the feedback.
Update the title because this issue just adds the top rendering.
We need to update the summary to match.
Comment #70
manuel garcia commentedUpdated IS as requested on #69.
Comment #71
tedbowRemoving the code I added in #17 that caused the current bug. This will reintroduce the problem described in #16
Comment #72
tedbow@drpal helped me figure this out.
Needed to remove the
data-offset-topafterdisplace()is called so when a new dialog link is clicked without closing the previous one it will not pick up thedata-offset-topattribute from the previous dialog.Comment #73
lauriiiWhat if displace is initiated elsewhere? Wouldn't that lead into wrong offsets?
Comment #74
timmillwood@tedbow Thank you so much for looking into this again.
Comment #75
GrandmaGlassesRopeMan@lauriii
#73.1 - Yes. I talked with Ted about this exact problem yesterday. What should happen is that the previous orientation offset data attributes are removed. However currently we don't have a way to know which orientation the tray is transitioning from. We could make some manual attempt to guess previous orientation, or just close the tray in between transitions.
Comment #76
tedbowRe #74
If we close the dialog let's not do it in the same place as before, the
dialog:beforecreateevent. This has at least 1 known side effect as we saw in this issue but also would probably mess up other module trying to react to that event because it would probably act weird if there listeners came before ours.The only way I can think to do it right now would be to a click event listener to any off-canvas dialog link that would fire before the regular ajax listeners and close the off-canvas there.
Comment #77
GrandmaGlassesRopeMan@tedbow Could we store the current open orientation on
window.Drupaland just reference that?Comment #78
GrandmaGlassesRopeManComment #79
GrandmaGlassesRopeMan- fixes an issue with resize events
- sets a minimum height on the tray element if opened by the top.
Comment #80
tedbowI still get visibly different heights when open up the "top panel 1" link.
Found the problem.
top here should surrounded in quotes 'top'.
It was never matching.
Comment #81
GrandmaGlassesRopeMan- fixes incorrect match of
topComment #82
tedbow@drpal 🎉, thanks for the fix.
I tested it manually and everything looks great!
Comment #84
GrandmaGlassesRopeMan- do not clobber the state object, just set a
postitionprop instead.Comment #85
lauriiiThank you @drpal! I think we are getting really close on getting this RTBC, nitpicks only at this point.
Nit pick: should we also document the type?
We should probably use null instead of boolean to keep the type more consistent.
Thanks for adding the clarifying documentation! Nit pick: could we change this to use the single line comment syntax instead?
Nit pick: s/Reset/Resets
Comment #86
timmillwoodThank you so much @tedbow and @drpal!
Thanks @lauriii for reviewing, thought I'd have a stab at those 4 updates.
Comment #87
timmillwoodAdded a change record https://www.drupal.org/node/2974784
Please feel free to update, or suggest changes.
Comment #88
lauriiiThank you! Finally I think we are ready! 🚀
Comment #89
gábor hojtsyUpdate credits. Removed mentions of bugs from issue summary because as per recent comments those got fixed.
Comment #91
gábor hojtsyLooked good to me too other than a missing semicolon in toolbar.es6.js that I fixed on commit:
Thanks all! I'll publish the change record now.
Comment #92
amateescu commentedI discovered a small (unrelated) bug while working on implementing the workspace UI using the top position for the off-canvas dialog: #2976287: The off-canvas dialog should have a 1px transparent border
Comment #93
amateescu commentedPosted another followup to this issue: #2976385: Provide the ability to wrap the entire page with a border when opening off-canvas in the top position
It would be great to hear the opinion of the product managers and off-canvas maintainers in there :)