Problem/Motivation
jQuery UI 1.14.1 (https://github.com/jquery/jquery-ui/releases/tag/1.14.1) introduces an option (<code>uiDialogTitleHeadingLevel - https://github.com/jquery/jquery-ui/issues/2271) for wrapping the title in dialog modals in a heading element, and choosing a specific heading level. Currently titles in Drupal are still just wrapped in span elements, which is semantically wrong. The problem was exacerbated, with the release of Drupal 10.3.0 and 11.0.0, dialog modals are having now an aria-modal attribute (#3454960: Update to jquery UI 1.14.0 beta 2 - https://github.com/jquery/jquery-ui/issues/2246), which excludes every element in the background of the dialog modal from the accessibility object model (AOM) - that way a modal might have no h1 or even no heading at all.
Steps to reproduce
Proposed resolution
✅ Wrap dialog modal titles in an h1 - for a proper semantic markup either an h1 or an h2 is preferable over a span. Patrick Hlauke provides a good summary of the problem space in the following comment: https://github.com/w3c/wcag/discussions/2722 where he states:
as modal dialogs (if done correctly) are almost like a whole self-contained new little document,
h1would be appropriate as they're not within the hierarchy of the rest of the page anymore.
but even if using anh2,h3, whatever, it's arguably fine as long as any further headings (if any) inside the modal correctly come below whatever heading level you chose. skipping levels is not a hard failure of 1.3.1 Info & Relationships, for instance.
and in a discussion on the a11y slack https://web-a11y.slack.com/archives/C042TSFGN/p1708471859401949 Roberto Perez adds
While a modal dialog is opened, only the content of the dialog is accessible. The overall heading structure of the page is irrelevant at that point in my opinion.
and James Scholes finally makes a good case for going with an h1 for dialog modals:
Modal titles should be
, for the reasons stated by @Roberto Perez. There is no reason to use an h2, and all it does is take a heading level away from the modal's content. Granted, if a modal's content needs all six heading levels, it probably shouldn't be a modal, but still...
James Scholes is an a11y professional and blind screenreader user.
✅ Wrap non-dialog modal titles in an h2 - see #30
...any section heading element would a clear improvement over a span element. Since the non-modal dialog isn’t wrapped into an article or section element, it wouldn’t be advisable to go with an h1 just for consistency reasons; any page the non modal dialog is invoked on might probably already have an h1 element. Going with an h2 instead would the most neutral semantic choice, and accept in consequence that there “might” be a potential skip of a heading level. So although hierarchical gapless heading structures reflect the best practice, skipping heading levels does not represent a WCAG failure.
✅ Add tests for the aria-modal attribute - #3454960: Update to jquery UI 1.14.0 beta 2 added the usage of the aria-modal attribute for dialog modals available with jQuery 1.14.0… Back then only the option was added but never a test. Add a test that the aria-modal attribute is added on dialog modals, and another test that there is no aria-modal attribute on non-modal dialogs.
✅ Add tests that the titles in dialog modals are wrapped in h1 elements and titles in non-dialog modals are wrapped in h2 elements.
Remaining tasks
Figure out why the tests on the test-only changes job are being skipped and the job passes with a green checkmark (https://git.drupalcode.org/issue/drupal-3485202/-/jobs/3890198)- Review
User interface changes
Introduced terminology
API changes
Data model changes
Release notes snippet
Issue fork drupal-3485202
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
- 3485202-change-the-dialog
changes, plain diff MR !10631
Comments
Comment #2
rkollerComment #3
rkollerforgot the ui in jquery ui , updated title and issue summary
Comment #4
mgiffordI'd love to see this brought into Core. This adds some missing semantics to our work.
Comment #5
rkollerI'Ve updated the title and rescoped the issue proposed resolution since jquery ui 1.14.1 got updated #3493146: Update all JavaScript dependencies which cause no changes. My initial proposed resolution by adding
uiDialogTitleHeadingLevel: 1after line 40 is wrong (tested it manually altering 11.x locally) . i was under the impression it would be a similar one liner like for adding the aria-modal functionality added for 11.0.0, but it seems not. reason is the_createWrappermethod was already available in the dialog.jquery-ui.js but the_createTitlebarmethod is not available there yet.Comment #6
rkollerComment #7
rkollerInitially filed it as a task because of the need of updating jquery but the remaining step about changing the wrapping element of the title i would consider a bugfix.
Comment #9
rkollerOpened an initial MR. It is setting the option
uiDialogTitleHeadingLevelthat was newly introduced with jQuery UI 1.4.1 to1ondialog.showModalmethod. That way it is ensured that all dialogs modals have their title properly wrapped in anh1instead of aspan, the previous default. Since Drupal 11.0.0 and the update to jQuery UI 1.14.0 dialog modals got thearia-modalattribute added which ensures that all elements in the background of the dialog modal are removed from the accessibility object model, so the main or sometimes sole heading within a dialog modal is the title, and therefore it is advised to go with the h1 element for the title wrapper. In addition to that i've also set the margin for.ui-dialog .ui-dialog-titlebar .ui-dialog-titleto zero, because the change to the h1 added an additional margin to the layout and by setting it to zero it readjusts it to the previous state of the design.*@rocketeerbkw worked with me on the upstream change (https://github.com/jquery/jquery-ui/issues/2271) as well as on this issue here
Comment #10
rkollerI was able to fix the stylelint issue but the phpunit ones i feel unable to solve and they look from my none developer perspective unrelated? the errors show in the context of package manager and i've only added an option to a javascript file? if someone else could take a look please who is more familiar with this kind of stuff. thank you.
Comment #11
oily commentedThe pipeline is running all green now. I had to re-run the failing tests.
However, test coverage is now required if this is to make it into core.
Comment #12
oily commentedSo the FunctionalJavascript/ Functional/ Nightwatch test seems to be defined as follows, from #9:
That is, the test needs to prove that at least one dialog modal does not have its title wrapped in a span but rather in an h1 element.
That should be possible by adding a couple of assertions to an existing test that tests dialog modals.
Comment #13
rkolleruh that is odd that the tests got green without any changes to the MR.
And about the to be added tests. i've tried to chop together a starting point. I am not a developer and haven't done any work on tests so far (only managed to create the upstream changes for jquery ui in relation to dialog modals including the necessary tests for both PRs). The first PR added the
aria-modalattribute the dialog. when that was added to Drupal 11.0.0/10.3.0 there was no test added. therefore i think it would be good aside testing that the title is wrapped in a h1 also to test that the dialog modal also got anaria-modalattribute added. looking at the existing tests, there iscore/tests/Drupal/FunctionalJavascriptTests/Dialog/DialogPositionTest.php, and i thought it might be the easiest to simply add the two necessary assertions here. even though it is against the single responsibility principle and the test is mainly about the positioning of the dialog modal. but if a dedicated test making sure the aria-modal attribute is set and the title wrapped in a h1, that test would contain a good share of steps DialogPositionTest.php already contains - so basically all those steps would be redundant and unnecessary. so i simply added the assertions to DialogPositionTest.php for now and added the reasoning and what is happening to a comment. as i said it is only a starting point. if people think it would be better to move those assertions to a dedicated test and or if the assertions need work please correct me and update the MR.Comment #14
oily commented@rkoller, That's great! I agree we should test for the aria-modal attribute too. Yes, there is a tension between DRY and single responsibility principle here. But in test code I have seen DRY win over single reponsibility before. It happens.
Comment #15
rkollerha i think we can stay "relatively" dry. the tests failed... the pipeline that contains the test where the two new asserts got added passed https://git.drupalcode.org/issue/drupal-3485202/-/jobs/3825700 :D buuuut another pipeline failed https://git.drupalcode.org/issue/drupal-3485202/-/jobs/3825702 and it looks like there is another test that tests not the positioning but the dialog in general (DialogTest) ... and the two asserts that fail there check for
span.ui-dialog-title:contains('AJAX Dialog & contents')... so i guess i remove the tests from DialogPositionTest, and update the selector in DialogTest and add the assert for the aria-modal there instead as well including the comment . and about the failing nightwatch test https://git.drupalcode.org/issue/drupal-3485202/-/jobs/3825720 i will see later, seems unrelated.Comment #16
oily commented@rkoller I have added feedback to the test code. The problem with the test coverage is that if you run the 'test-only' test which can only be run manually it runs the test code only and it runs it against core without the fix code. It should therefore fail. But just now it is passing.
BTW, if a test fails and looks unrelated I re-run it. More often than not it passes on second run. It happens frequently.
Comment #17
oily commentedIt looks like to make the DialogTest.php tests pass you just need to change e.g.:
$dialog_title = $link1_dialog_div->find('css', "span.ui-dialog-title:contains('AJAX Dialog & contents')");to
$dialog_title = $link1_dialog_div->find('css', "h1.ui-dialog-title:contains('AJAX Dialog & contents')");Or has 'ui-dialog-title' been changed/ removed?
Comment #18
oily commentedApplied code fix to DialogTest.php to make it pass.
Comment #19
rkollerthanks! but on a closer look it is surprising that the tests pass with your recent changes. cuz you've applied the change from span to h1 for every occasion of span. but some of the test cases are also for dialogs that are no modals (for example line 119 and 121 in the context of link 7 - non-modal, no target), cuz h1 is only set for dialog that are actual modals at this point in dialog.js
but maybe a good reminder to change the wrapping element for dialogs that are no modals as well? i would lean towards h2, might be a relatively save call in this case, but i've asked @mgifford and will report back when i get feedback.
and about the assertion about aria-modal and your comment:
does that mean that
$this->assertEquals('true', $dialog->getAttribute('aria-modal'), 'Dialog modal has aria-modal attribute');would be fine? at least the same pattern is used on for example in FromGroupingElementsTest.php in line 136, therefore i thought it would be save to use, otherwise it would have to be corrected in more places in core tests? and i am still uncertain where to place that assertEquals in the DialogTest.phpComment #20
oily commented@rkoller DialogTest.php works now. I fixed each pair of asserts locally using PHPUNIT. With the existing span.ui-dialog... selector the asserts were failing and with h1.ui-dialog... they work. When I fixed the first pair in the file the test progressed to the next pair and failed on them. I think there were 6 or 7 asserts in total needing span changed to h1.
Either the tests in DialogTest.php are defective which will require raising an issue or they are doing their job, in which case there is no need to do anything more inside the file (unless someone changes code that breaks the tests again). I think you are mistaken. h1 is applied to all modals which is why the tests in DialogTest.php are passing. It may be surprising.
As far as my code comment and #17 go, the two assertions are defective. They are passing when they should be failing. To get a handle on the reason it is worth trying to assert eg that 'span etc' does not exist. That will fail without the MR fix and pass with it. It will help to identify why the current test coverage is passing when it should not.
Comment #21
rkollerbut if you install the ajax-test module the onethe tests are using for the given tests and you take a look at
link 1 (modal)then this modals title is an h1 with an aria-modal="true" attribute while if you take a look atlink 7it opens a dialog that has no aria-modal attribute and the title is wrapped in a span. therefore the assertion in 119 and 121 would have to fail imho currently since they check for h1 and that h1 is not in place but a span instead still?! is it possible that the reason for the tests not failing is because modals and none modals are open both?! cuz the different dialogs are opened in the dialogTest.php but only the first opened dialog is also closed in line 70 with$close_button->press();, the rest is kept open?Comment #22
oily commented@rkoller That is interesting. It could be that the tests in DialogTest.php have exposed their limitations and so they need to be changed, too.
It seems here that you are trying to beat the test. The tests are never wrong. The way we read them can be wrong. Sometimes the best thing is to try out asserts that have to fail. Try different asserts each will give more information not just about what the asserts do but on what is actually happening inside the tests.
Comment #23
oily commented@rkoller The assertion at 119 searches the page for an element identified by the selector 'h1.ui-dialog-title' and that contains the text, 'AJAX Dialog & contents'. I am not sure how 'link 7' is relevant? In any case it finds the element and so the assertion passes. I am not sure that any explanation is necessary.
Comment #24
rkolleri was completely wrong and got confused by the several context switches in a row. in the given example with 119 and 121 the point of reference was NOT link 7 which is a none modal but button 1 which is a modal (line 113) and which i've missed. so the tests are correct. managed to get the local tests running and therefore i was able to validate and cross check. so your changes are all correct and doesnt look like a shortcoming or bug of the existing tests.
and i'Ve pushed another commit that contains two asserts that add checks for the aria-modal attribute. locally those work for me.
Comment #25
oily commentedI see one failing test the 4/8 one, i think? The test inside it that fails looks unrelated. If re-trigger 4/8 it should pass and you will have all green pipeline. Then you should run the 'test-only' test. It has be to run manually. That test is critical because it automatically finds any test code in the MR and isolates it; then runs it against the 11.x branch (the one our MR will finally be merged into). What we want is to see that test fail.
Please go ahead, I assume you are new to testing so giving you the opportunity of completing the steps..
Comment #26
rkollerhm i keep getting errors. the first two times i've restarted the 4/8 functional test you've mentioned, it failed each time with another mismatch for a string. totally unrelated to the changes in this MR. i've reran all tests now, again functional test 4 out of 8 fails but this time with a 403... plus an error in a test for package manager: https://git.drupalcode.org/issue/drupal-3485202/-/jobs/3827987 :/ confusing and "sort of" arbitrary.
ahhhhh and now i see the test only changes test. understood. thank you for the pointer, i would have missed that!
Comment #27
oily commented@rkoller Looks good. Pipeline all green. Now you should run the 'test-only changes' test. If it passes, we need to change status to 'Needs work' otherwise leave it at 'Needs review'.
Comment #28
rkolleri ran the test-only test now, but unfortunately it succeeded, and it sounded like it would have to fail instead?!
Comment #29
oily commentedYes, correct. If you can run the tests locally i recommend you start off by creating an assertion that has to fail. It proves that everything works. Even if it is 4 is not 2 or false is not true. From there you should figure it out. Try a commit to the pipeline if it helps convincing you that the pipeline is working right. Might be worth leaving it for a day. You will see what is wrong when you return to it!
Comment #30
rkollerUsability review
We discussed this issue at #3495184: Drupal Usability Meeting 2025-01-03. That issue will have a link to a recording of the meeting.
For the record, the attendees at the usability meeting were @benjifisher, @rkoller, and @simohell.
We’ve discussed the open question whether a section heading element should be used to wrap the title of a non-modal dialog same as the title of modal dialogs, and if so, which heading level should be chosen? There was the clear consensus that same as for the modal dialogs, any section heading element would a clear improvement over a span element. Since the non-modal dialog isn’t wrapped into an
articleorsectionelement, it wouldn’t be advisable to go with anh1just for consistency reasons; any page the non modal dialog is invoked on might probably already have anh1element. Going with anh2instead would the most neutral semantic choice, and accept in consequence that there “might” be a potential skip of a heading level. So although hierarchical gapless heading structures reflect the best practice, skipping heading levels does not represent a WCAG failure.(https://www.tpgi.com/heading-off-confusion-when-do-headings-fail-wcag/).If you want more feedback from the usability team, a good way to reach out is in the #ux channel in Slack.
p.s. @mgifford agreed and leaned towards the proposed resolution going with an h2 for non modal dialogs in a thread on the Drupal Slack https://drupal.slack.com/archives/C1AFW2ZPD/p1735760864177209 in the run-up to the meeting, where i’ve asked for more opinions in the #ux and #accessibility channel.
Comment #31
rkoller@oily i've already added the changes we've discussed yesterday outlined in #30 to the MR last night (basically added the h2 to non modal dialogs and included an assertion that one of the non modal dialog examples has their title wrapped in an h2).
I got the automated tests green again but the test-only changes confuse me and remain green/passing. i followed your recommendation and tested locally. i simply copied the content of DialogTest.php into the clipboard, then checked out 11.x, then opened DialogTest.php again and pasted/replaced the test code with the one from the MR, and finally reran that test on 11.x with the test code from the MR.... there things fail as expected:
but on https://git.drupalcode.org/issue/drupal-3485202/-/jobs/3890198 it looks like tests are being skipped(?!?) and that is the reason why the job is shown as passed?
Comment #32
oily commented@rkoller Thank you for the details of the meeting and the test-only test. This may be an issue with the pipeline. It happened recently on another issue i was working on. Local testing proved that test coverage was effective. You could raise this in the #contribute slack channel. Else the reviewer can test locally when the review is carried out.
Comment #33
rkolleri've already posted in #testing last night https://drupal.slack.com/archives/C223PR743/p1735958223766589 and asked there, but no answer yet. will give it another day or two and then repost in contribute (or comment in one or another thread that i found in the context of test-only tests not failing). but aside that detail i leave the issue at needs review so people can take a look at the recent changes. think the only thing left to do aside another round of review might and figuring out the test-only problem is updating the issue summary so it reflects the recent changes. will do so the next days.
Comment #34
oily commented@rkoller Thank you for the work!
Comment #35
oily commentedComment #36
rkollerUpdated the issue summary
Comment #37
rkollerComment #38
rkollerComment #39
rkollerComment #40
rkollerI've removed the fail-on-skip as well as the debug flag from test-only.sh and also ran a rebase after #3494332: Functional Javascript tests are silently skipped on Gitlab CI due to selenium standalone configuration went in. fingers crossed that the test-only tests aren't skipped anymore and properly fail now.
Comment #41
rkoller@oily since you were following along #3494332: Functional Javascript tests are silently skipped on Gitlab CI due to selenium standalone configuration as well. i ran the test-only test now after the rebase and the job failed: https://git.drupalcode.org/issue/drupal-3485202/-/jobs/4151589 . but to me it looks like the testing "infrastructure" aka selenium failed but not the actual test?! do i read that correctly? i "thought" the changes from #3494332: Functional Javascript tests are silently skipped on Gitlab CI due to selenium standalone configuration were contained in that rebase?
Comment #42
oily commented#40 I just viewed the last pipeline. The test-only test is failing as it should. This is close to RTBTC, now. You seem to have resolved all threads.
Comment #43
oily commented@rkoller Have you linked this in one of the Slack channels? You can reach out to Javascript aficionados to get one more review before RTBTC.
Comment #44
rkolleryeah but if you take a look at the output as noted in #41 you'll notice the two failures were:
that is an infrastructure specific error not an error based on what is actually tested against with this MR?
Comment #45
rkoller@oily the fail was a false fail. after some more investigation and some advice from @catch it turns out the tests-only pipeline service definition has to be changed from with-chrome to with-selenium-chrome... the last two commits tested that on this issue. made the change on this MR, the test-only test then correctly failed with
(see https://git.drupalcode.org/issue/drupal-3485202/-/jobs/4155477) . but per the recommendation of @catch i've created a new issue that makes that one line change. #3502629: Change the test-only pipeline service from with-chrome to with-selenium-chrome. when this issue is in then we can rebase this issue and can come to a proper finish.
Comment #46
oily commented@rkoller Sounds good. We could change status of this to 'postponed'?
Comment #47
rkoller@oily since you are kindly following along. i would have one question. if you see the comments here in this thread, between #44 and #45 i've committed and tested the changes that got into #3502629: Change the test-only pipeline service from with-chrome to with-selenium-chrome and then committed and went back to the previous state with
*with-chrome... now #3502629: Change the test-only pipeline service from with-chrome to with-selenium-chrome got fixed, so i went ahead and rebased this MR to get the changes in. problem is. if you take a look at https://git.drupalcode.org/project/drupal/-/merge_requests/10631/diffs#n... , there are changes for the pipeline.yml. it somehow the changes introduced in the other issue and that got added by rebase are getting invalidated and the service changed to*with-chrome. looking at https://git.drupalcode.org/project/drupal/-/merge_requests/10631/commits... is it possible that on a rebase all the commits on a MR are committed again after the rebased core? and that is the reason the change gets reverted again back to*with-chromewith ba444f84??? and my question is how to fix that?Comment #48
the_g_bomb commentedIf you think about what we are dealing with here.
There is core it has been forked at a point in time to make the fork repo and branch for this issue.
Since that point core has progressed, this repo and branch are still stuck in time, back when the repo was cloned.
To bring this up to date we need to get core 11.x up to date, we need to push that to the 11,.x version in this repo and then probably also need to merge the changes in this branch too.
If you look at the forked repo you may notice it says
`Forked from project / drupal
223 commits behind the upstream repository.` I think that refers to the default branch.
If you have permission there is usually a button you can click to update the repo, or sync the repo, which is probably the easiest, assuming you have the 11.x branch in your forked, that is probably easiest.
You can also manually do it with git.
Which I can imagine might look something like:
That should make your forked repo up to date with the current core as it is now, all the latest code will be in 11.x and your branch, so only the changes you are introducing should be shown in the MR.
Comment #49
rkollerI've simply pushed another commit now reverting the changes of the commit on the MR from the 27th of january invalidating the changes in the pipeline.yml (4th february) that got in with the rebase... tests are green now and the test only fails properly now as well: https://git.drupalcode.org/issue/drupal-3485202/-/jobs/4243209 ... so the only thing left to do is still another pair of eyes reviewing the MR.
Comment #50
mgiffordLooks good to me. This would be a significant improvement in semantically presenting this content.
Comment #51
smustgrave commentedTreating #50 as sign off for accessibility
Seems to have gone through several rounds of review and looking at the MR I see the threads have been resolved and I'm not noticing anything off.
Verified by going to a simple view, example /admin/structure/views/view/content and just opening dialog there.
Can confirm the styling appears unchanged also.
LGTM
Comment #54
nod_Committed 23f9b78 and pushed to 11.x. Thanks!