Patch at #56 is RTBC.
Follow ups to address related issues :
* #2848307: Inline errors not working on form table elements
* #2848319: Find a way to make it easier to hide Inline Form Error messages on child elements
* #2780209: Core relies on undocumented feature of FormStateInterface::setErrorByName()
This issue cleans up UX regression created by the inline error being repeated for every child element in the modile uninstall form. It uncovered related issues elsewhere in core, which can be dealt with in follow ups.
Before:
After:
Original IS
Go to /admin/modules/uninstall
Submit the form without checking any checkboxes.
This is what you get:
Neither the message (""1 error has been found: Uninstall Block module") nor the inline errors are very useful.
Before #1493324: Inline form errors for accessibility and UX it used to just show a message "No modules selected." A better message would be something like "You must select at least one module to uninstall".
But a message complaining about the first module that happens to be on this list, with inline errors for every single checkbox, is a big step backward in usability and accessibility for this page.
A lot of work went into #1493324: Inline form errors for accessibility and UX, so I'd rather not revert that, but the damage it did really needs to be cleaned up before 8.0 is released.
Comment | File | Size | Author |
---|---|---|---|
#60 | after-inline-errors.png | 78.01 KB | left |
#60 | before-inline-errors.png | 87.42 KB | left |
#59 | Uninstall _ drupal 8.3.x_2017-01-28_22-40-54.png | 16.74 KB | ok_lyndsey |
#56 | 2509268-56-IFE_uninstall_page.patch | 1.53 KB | dmsmidt |
#55 | uninstall_ife.png | 54.48 KB | dmsmidt |
Comments
Comment #1
willzyx CreditAttribution: willzyx commentedThe attached patch should solves the issue. It adds the error to a non existing element rather then to a real element (as it is done in various forms in core)
This is the result
Comment #2
cilefen CreditAttribution: cilefen commentedThis issue is normal priority.
Comment #3
TR CreditAttribution: TR commentedActually, this easily qualifies as Major because it is a unintended problem caused by a recent change and represents a major regression in usability and accessibility to a form that has been working properly since at least Drupal 5. We have other open (Major) issues geared towards improving the usability and accessibility of the module install/uninstall pages, so if it's a Major issue to improve the page then it's certainly Major when the page gets dramatically broken like I show above.
In fact, it wouldn't be wrong to make this a Critical priority release blocker, according to the guidelines, as it's really unthinkable that we could break this important form this late in the release cycle and not fix it before 8.0. But let's leave it at Major for now.
Comment #4
alexpottLooks like we should test this to make sure we don't regress again.
Comment #5
cilefen CreditAttribution: cilefen commented@TR: fair enough! - good info on why, thanks
Comment #6
TR CreditAttribution: TR commentedThe documentation for setErrorByName($name, $message) says that $name holds the name of the form element, but it doesn't specify what should happen if $name is empty or $name holds an invalid form element name.
ModulesUninstallForm currently sets $name ='uninstall', which is the name of the top-level container element in the form. It seems reasonable to expect that will set an error for the entire form, not an error on every single element. Indeed, it has always worked in the past to set an error for the entire form, until #1493324: Inline form errors for accessibility and UX was committed.
In core, it seems that $name = '' is used as a shortcut to set an error for the entire form, and NOT for just one specific form element as the function was designed to do. (I found 13 instances of this in core.) To me, this feels like we're taking advantage of an undocumented/unintentional side-effect of the setErrorByName() function. If we don't have a name, we shouldn't be using setErrorByName(), IMO.
The patch works by changing ModulesUninstallForm to call setErrorByName() with $name = ''. That works, the patch makes the error show up the way it used to. But it concerns me because 1) the old way *should* still be working and 2) the new way relies on the undocumented behavior of passing '' as the form element name.
So I think part of the fix here has to be to make it definite what behavior we should expect when calling setErrorByName() with an empty or invalid $name - if setErrorByName() is supposed to set an error on the entire form, then it should *always* do that *every* time (which it's clearly not doing right now ...). Or perhaps we need a new setFormError($message) function to explicitly set an error for the entire form. To be clear, this is not just a documentation issue; if '' is supposed to produce a known result, then setErrorByName() should explicitly check $name == '' rather than fall-through to some non-specified behavior.
Comment #7
joshi.rohit100Comment #8
joshi.rohit100Here is the patch with testcase. Not sure with the location for this (with test case name ): ).
Comment #9
joshi.rohit100Here is the test only patch.
Comment #10
joshi.rohit100Comment #11
joshi.rohit100Also same is the case module install page. It doesn't show up any message if we just hit submit without selecting anything.
Comment #14
joshi.rohit100extra space Oopz :)
Comment #15
bojanz CreditAttribution: bojanz at Centarro commentedSetting an error on a parent element should not repeat the same error on all child elements.
I've just encountered this bug on a field widget, it will keep turning up, so we can't just add a workaround.
Comment #16
bojanz CreditAttribution: bojanz at Centarro commentedRetitling. The problem is coming from FormErrorHandler::setElementErrorsFromFormState() which calls $form_state->getError(), which returns errors for parent elements as well.
The #errors key was previously used to indicate "do I or one of my parents have an error", to add an error class when needed, but now it's also used to display the error message, hence the problem.
This can be reproduced on any element that has #tree => TRUE.
Comment #17
tim.plunkettOkay, worked on this a bit.
Requires a UI change, to make $form['uninstall'] a real form element so that the error can be attached to it.
Likely needs a better label than "Modules that can be uninstalled".
No interdiff because it's a new approach.
Comment #19
tim.plunkettAll of those fails are from constraints, like \Drupal\aggregator\Plugin\Validation\Constraint\FeedUrlConstraint.
It's called from \Drupal\Core\Field\WidgetBase::flagErrors(), using \Drupal\Core\Field\WidgetBase::errorElement() to determine the element to set the error on.
It is setting it with a #parents of ['url'] when it should be ['url', 0, 'value'].
However, in these cases $delta is '' instead of 0, because
$violation->getPropertyPath()
is empty.Comment #20
tim.plunkettSeems we're blocked on #2027059: Improve the documentation of WidgetBase::errorElement() for mapping violation property paths to form elements :(
Comment #21
TR CreditAttribution: TR commentedJust FYI, I've also seen this on /admin/config/development/testing, where each and every test has an inline error (hidden, because all the fieldsets are closed on this page by default) and the message region shows an error message about the first test in the list (\Drupal\filter\Tests\FilterFormatAccessTest). This happens sometimes when returning to the testing page after a failed test.
Comment #24
tim.plunkettStraight reroll.
Comment #25
yched CreditAttribution: yched commentedComing from #2027059: Improve the documentation of WidgetBase::errorElement() for mapping violation property paths to form elements - discussed with @tim.plunkett in Barcelona.
I agree that the current behavior is a massive drag on things like checkboxes :-/
We do however have a lot of field widgets that rely on the current behavior when it comes to the task of "mapping a field constraint violation down to an actual form element" (i.e WidgetInterface::errorElement($violation)).
The default implementation in WidgetBase, which is good enough for most widgets, is "I'll flag the widget wrapper element as a whole, and the error will be displayed on every children (in most cases there's only one)". More complex widgets can, if they need, decide to do more fine-grained flagging if they have several child input elements.
That "catch all" default behavior :
- is the only default implementation that makes sense generically,
- is also what you want for violations that really are on the field itself (like a NotNull constraint for required fields, or the FeedUrl constraint for the feed 'url' field that seems to be causing the fails mentioned in #19) rather than on some specific property within the field.
So I'm a bit worried at how much we'd break by changing the "error displaying" behavior ?
Comment #28
tim.plunkettNo longer major as the code was moved out of the forms system and into the experimental module.
Comment #29
xjmComment #31
SKAUGHTplease refer to: https://www.drupal.org/node/2729287#comment-11209457
THESE ARE NOT TRUE CHILD ELEMENTS, they are 'checkbox' not 'checkboxes'
Comment #32
SKAUGHTthis is a clean patch aside from others.
this removes the initial validate check completely then lets the confirm form set the user message and redirected back to fresh form.
to note:
this visual error sequence is due to #tree('d) form elements, if the tree (was say) be an textfield, an email field, a select. you probably still wouldn't check against 'all items' in the tree but each uniquely. i think it's as much the case that these type of edge-case validation needs would be better handled by the developer of That From, not the From System trying to automatically handle everything in the tree.
Comment #33
SKAUGHTComment #35
SKAUGHTsome kind of test-bot oddness. changing status.
Comment #36
SKAUGHTi have just noticed [#21]
if this method is considered acceptable, i will happily look at testing module to address that form situation.
@tim.plunkett
Comment #37
dmsmidtComment #38
SKAUGHT@dmsmidt
please review comment [#31]. I do believe that this issue title/description to be miss leading in that it is expectable that if you #tree a bunch of FAPI items then call the error to that group, it should trip any appropriate errors for those children.
IE: if this was a Field Collection (acting like a #tree) and you called an error to the group and you have an image field, a select.. a bool checkbox. they would (and should) all have individual errors that would be rendered.
comment: [#32]. this patch lets the confirm_form complete and deliver a single error as would be expected at this is a confirm_form that got a validation tacked on that basically isn't needed
Comment #39
mgiffordI can't repeat the problem described in the summary at /admin/modules/uninstall
With or without the patch it doesn't give me that error.
Is there a better example where checkboxes produce errors as child elements? I understand that the one in the summary is a series of
type="checkbox"
Comment #40
TR CreditAttribution: TR commentedmgifford: You enabled the inline form errors module first?
Comment #41
mgiffordMy bad.. Sorry @TR. Without the patch it still looks like it does in the issue summary. With the patch it now looks much better and looks like:
where checkboxes produce errors as child elements?
This is screenshot only validates child
checkbox
elements:<input data-drupal-selector="edit-uninstall-automated-cron" id="edit-uninstall-automated-cron" name="uninstall[automated_cron]" value="1" class="form-checkbox error" aria-invalid="true" type="checkbox">
Comment #42
pfrenssenThis issue seems to swing between finding a structural solution and only fixing the particular instance of the problem in the uninstall form. I think the current scope of the issue needs to be clarified.
Here is a run down of the different points that were raised in the comments:
'#tree' => TRUE
. @TR gives another real life example in core in #2509268-21: Inline errors repeated on child elements in module uninstall form: it happens when submitting an empty form at/admin/config/development/testing
.'#tree' => TRUE
case is probably solvable by flagging the correct form element / field widget.So it seems that a case-by-case fix is preferred. We need to update the issue title and summary.
Comment #43
pfrenssenComment #44
pfrenssenThe patch from #32 doesn't seem like the right solution. It works around the problem by removing the validate handler. Then the confirmation form will handle it, set the error, and redirect back to the uninstall form which will display it. I would prefer keeping the validate handler.
I think the patch from #1 is on the right path. It relies on a setting an empty name which is not a documented feature but according to @TR in #2509268-6: Inline errors repeated on child elements in module uninstall form it is used at least in 13 other places in core already.
I would propose to continue on that path since the solution is simple and feels right: we do not want to flag a specific element because this error does not apply to any element in the form.
We should do something about this undocumented feature though, either document it, or provide an official way to set a generic error on the form without flagging a specific element. I made a followup for this: #2780209: Core relies on undocumented feature of FormStateInterface::setErrorByName().
Comment #45
pfrenssenI just saw that the case of the duplicate inline errors in the Simpletest form is due to the generic
Table
form element flagging the root table element when no items are selected inDrupal\Core\Render\Element::validateTable()
.There can be multiple tables in a single form, so how are we going to deal with this?
Comment #46
pfrenssenHow about allowing to flag elements that should not display their errors inline? This fixes both the uninstall form and the generic Table element which is used by the simpletest form.
Comment #47
dmsmidt@pfrenssen thank you for making such a nice summary in #42.
Concerning the proposed approach in #46, two thoughts:
Maybe we can just mark the checkboxes as erroneous without a message? And then use the new method proposed in #2780209: Core relies on undocumented feature of FormStateInterface::setErrorByName() to show one message for the complete form?
Comment #48
pfrenssenThe option to mark the entire form works fine for the uninstall form which is a simple form that is fully under our control. Unfortunately it wouldn't work for the
Table
element. That may be used in any kind of form in contrib or custom code, and it might be critical that the exact element has been marked with an error.I think with this
#error_no_inline_message
controlling which errors show up inline becomes really easy.What I don't like about it is that both the parent container and the child elements need to get this flag. But I think it is maybe unavoidable.
Comment #50
mgiffordWanting to highlight @xjm's point about timelines for this:
https://www.drupal.org/node/2504847#comment-11750733
@pfrenssen & @dmsmidt how do we move this forward?
Comment #51
huijse CreditAttribution: huijse commentedWorks.
I have tested the above and can confirm from my own findings that this patch works.
Thanks for the patch!
Comment #52
dmsmidt@sean conner, thanks for testing.
@prenssen, ok let's go for it, in complex forms it can be a good tool to have.
We should create a test for that new functionality and see if we can work around the problem of the need of adding
#error_no_inline_message
twice.Maybe we should create some extra logic on form elements to prevent showing child errors, if all children with errors have #error_no_inline_message. Thinking of a combination with #2754977: Enhance formErrorHandler to include children errors on RenderElements.
Comment #53
alexpottIsn't it quite likely that most times a checkbox type is used you're going to want to use
#error_no_inline_message
.Comment #54
dmsmidtFirst of all, the patch in #46 disables inline errors for all Table elements. Potentially we can have multiple tables and the table is not always at the top of the page. So Tables should support Inline Form Errors, otherwise accessibility is as bad as it was without IFE enabled.
Also, since not everybody is convinced, I tried to rethink the problem at hand and find a minimum impact solution.
To prevent errors from showing up, we already have the property '#error_no_message'. This already gives us the possibility to hide messages on $elements.
These two things combined allows setting errors on a complete table, allows setting errors on particular fields in a table (row). And also prevents adding the proposed '#error_no_inline_message' property multiple times.
I'm working on a patch to see if this idea works.
Note: I also had the idea to add a property, or an argument to the setError() and setErrorByName() methods, to prevent bubbling up of error messages.
Comment #55
dmsmidtAlright this is what you get with the approach mentioned in my previous comment for the uninstall page.
Now there are real inline form errors. Patch attached (no interdiff, new approach).
I think this can work in general for tables.
But, since there is only one element that can have an error in the uninstall page case, I think in this specific case IFE is overkill.
So we can get back to the patch of #14. This is a simple change, and doesn't need API changes.
In a follow up, we can work on IFE for tables with the approach shown above.
Comment #56
dmsmidtRe-roll of #14.
Comment #57
mgiffordComment #59
ok_lyndsey CreditAttribution: ok_lyndsey as a volunteer commented@left and I tested this one. As per patch #56
Behaviour of patch visual test - No modules selected heading as per patch intent
Alert using NDVA "no modules selected" - as per intent. Error message and link from link from screen shot above does not display as per comment.
Comment #60
left CreditAttribution: left commentedI confirm that I can replicate the issue when Inline Errors module is enabled and I click the 'uninstall' button without any modules selected. And that patch #56 works as intended. Tested with ChromeVox and the vocal error message seems correct.
Before
After
Comment #61
ok_lyndsey CreditAttribution: ok_lyndsey as a volunteer commented@left gets bonus credit for figuring out how to embed screenshots in a comment... :D
Comment #62
mgifford@dmsmidt I think you have addressed all of @alexpott concerns in #53. If so we should mark this RTBC.
Thanks @left & @ok_lyndsey!
Comment #63
kattekrab CreditAttribution: kattekrab as a volunteer commentedManually tested during sprint weekend. Looking good. A11y maintainer says looks good. Let's do it! RTBC!
Comment #64
kattekrab CreditAttribution: kattekrab as a volunteer commentedComment #65
alexpottSo reading the entire issue the question that occurs is how do we stop similar things from occurring in contrib. The current approach fixes the problem in core and I've looked for other places where this might occur but I can't spot any. I think a followup is in order that should explore a generic fix so that sites that use inline_form_errors don't hit this when developing custom modules or using contrib. The followup should exist before commit.
Comment #66
dmsmidt@alexpott
Actually we identified a lot in this issue, and there are multiple follow ups.
I created two new ones. Ready for commit!
Followups:
#2848319: Find a way to make it easier to hide Inline Form Error messages on child elements
#2848307: Inline errors not working on form table elements
#2780209: Core relies on undocumented feature of FormStateInterface::setErrorByName()
Comment #67
kattekrab CreditAttribution: kattekrab as a volunteer commentedComment #68
kattekrab CreditAttribution: kattekrab as a volunteer commentedComment #69
kattekrab CreditAttribution: kattekrab as a volunteer commentedComment #70
alexpottCommitted and pushed 43f6069 to 8.4.x and 4a430c8 to 8.3.x and 7e5875c to 8.2.x. Thanks!
Merged all the way back to 8.2.x since the fix is BC compatible and there is no point the uninstall form looking awful when IFE is installed on 8.2.x.