Skip to content

Associate the ThreePartDate inputs with the original label#336

Open
BryceStevenWilley wants to merge 2 commits into
mainfrom
accessible_three_part_date
Open

Associate the ThreePartDate inputs with the original label#336
BryceStevenWilley wants to merge 2 commits into
mainfrom
accessible_three_part_date

Conversation

@BryceStevenWilley
Copy link
Copy Markdown
Contributor

@BryceStevenWilley BryceStevenWilley commented Mar 26, 2026

Uses fieldset and legend, replacing docassemble's given div. Keeps the label as the visual element, as legend has display issues on Safari 1.

Tested on Chrome and Safari on Mac, and in Firefox (which uses Safari's engine) on iPhone. Doesn't really improve the situation on iPhone, but that is expected, as group support on iPhone is poor 1.

Fixes #164.

Footnotes

  1. https://adrianroselli.com/2022/07/use-legend-and-fieldset.html 2

Copy link
Copy Markdown
Collaborator

@plocket plocket left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great accessibility improvements!

From a second, visual, check of ALKiln's code these changes should have no effect on that functionality. That said, it's a complex system and testing it is even more complex at the moment because of the OS reasons we've discussed. Let me know if you think of an easy way to wrangle that. If all else fails, though, I can always patch ALKiln.

I made some suggestions for things I see as improvements, but I don't see any of them as critical, so I'll approve and you can do as you will!

Comment thread docassemble/ALToolbox/ThreePartsDate.py Outdated
Comment thread docassemble/ALToolbox/ThreePartsDate.py Outdated
Comment on lines +54 to +63
// Replace the div with a fieldset for accessibility.
$('div.da-field-container-datatype-ThreePartsDate').each(function() {{
var $container = $(this);
var $fieldset = $('<fieldset class="da-container da-form-group row da-field-container da-field-container-datatype-ThreePartsDate">');
for (let c of $container.children()) {{
$fieldset.append($(c).detach());
}}
$container.after($fieldset);
$container.remove();
}});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, this may have a better chance of staying robust to da DOM changes and give it more of a chance to handle how da or our users are currently using the div1:

Suggested change
// Replace the div with a fieldset for accessibility.
$('div.da-field-container-datatype-ThreePartsDate').each(function() {{
var $container = $(this);
var $fieldset = $('<fieldset class="da-container da-form-group row da-field-container da-field-container-datatype-ThreePartsDate">');
for (let c of $container.children()) {{
$fieldset.append($(c).detach());
}}
$container.after($fieldset);
$container.remove();
}});
// Replace the div with a fieldset for accessibility.
$('div.da-field-container-datatype-ThreePartsDate').each(function() {{
const $fieldset = $(`<fieldset>`);
$(this).wrap($fieldset)
}});

Unless there's some reason we can't adjust our code to this configuration.

I've tested the code directly in the browser and functionality and appearance seemed to stay the same with this particular configuration.

Footnotes

  1. The original div could be acted on by listeners that use event delegation. Those would be event listeners on an ancestor of the div that then use the div tag to find the form element. Others could be using the div as part of their css to style these elements. It's unlikely, but possible. Even this change doesn't solve the issue completely. E.g. css like this: form > div.da-form-group or something similar, or form *, but I don't see folks using those kinds of selectors as much.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this, and it has some issues with screen readers. Specifically, legend needs to be an immediate descendant of the fieldset, it can't be in a div or anything. https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/fieldset.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, missed checking that functionality completely. I can play around and see if I can flip the relationship. If not, I think we should go ahead without the nesting.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added code below that makes the legend a child of this fieldset. I think there's an improvement to make with my code though - adding a class to the fieldset to make it more accessible to css.

Suggested change
// Replace the div with a fieldset for accessibility.
$('div.da-field-container-datatype-ThreePartsDate').each(function() {{
var $container = $(this);
var $fieldset = $('<fieldset class="da-container da-form-group row da-field-container da-field-container-datatype-ThreePartsDate">');
for (let c of $container.children()) {{
$fieldset.append($(c).detach());
}}
$container.after($fieldset);
$container.remove();
}});
// Replace the div with a fieldset for accessibility.
$(`div.da-field-container-datatype-ThreePartsDate`).each(function() {{
const $fieldset = $(`<fieldset class="al-fieldset-datatype-ThreePartsDate">`);
$(this).wrap($fieldset)
}});

With regular quotes:

Suggested change
// Replace the div with a fieldset for accessibility.
$('div.da-field-container-datatype-ThreePartsDate').each(function() {{
var $container = $(this);
var $fieldset = $('<fieldset class="da-container da-form-group row da-field-container da-field-container-datatype-ThreePartsDate">');
for (let c of $container.children()) {{
$fieldset.append($(c).detach());
}}
$container.after($fieldset);
$container.remove();
}});
// Replace the div with a fieldset for accessibility.
$('div.da-field-container-datatype-ThreePartsDate').each(function() {{
const $fieldset = $('<fieldset class="al-fieldset-datatype-ThreePartsDate">');
$(this).wrap($fieldset)
}});

Comment on lines +89 to +90
var $label = $(this);
var $legend = $('<legend class="col-md-4 col-form-label da-form-label datext-right">' + $label.text() + '</legend>');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with the <fieldset>, this may make the code more robust to da DOM changes. Also, instead of using our own custom css for hiding the label, we could use da's pre-existing class for that and also make the code more robust to da changes. If you want to skip da's class, see below.

Suggested change
var $label = $(this);
var $legend = $('<legend class="col-md-4 col-form-label da-form-label datext-right">' + $label.text() + '</legend>');
const $label = $(this);
const $legend = $(`<legend>`).addClass($label.attr(`class`));
$legend.addClass(`visually-hidden`);

It ends up with this css:

.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption), .visually-hidden:not(caption) {
  position: absolute !important;
}
.visually-hidden, .visually-hidden-focusable:not(:focus):not(:focus-within) {
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  margin: -1px !important;
  overflow: hidden !important;
  clip: rect(0,0,0,0) !important;
  white-space: nowrap !important;
  border: 0 !important;
}

It has some of the same pitfalls as the css in this PR. Also, it doesn't include active.

Without da's class:

Suggested change
var $label = $(this);
var $legend = $('<legend class="col-md-4 col-form-label da-form-label datext-right">' + $label.text() + '</legend>');
const $label = $(this);
const $legend = $(`<legend>`).addClass($label.attr(`class`));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of these suggestions don't include the label text in the legend, which is the point of adding it?

I can try changing the CSS to docassemble's and see if it works the same. Looks like it might, but would like to check it myself.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completely left out the text, sorry about that:

Suggested change
var $label = $(this);
var $legend = $('<legend class="col-md-4 col-form-label da-form-label datext-right">' + $label.text() + '</legend>');
const $label = $(this);
const $legend = $(`<legend>` + $label.text() + `</legend>`).addClass($label.attr(`class`));
$legend.addClass(`visually-hidden`);
Suggested change
var $label = $(this);
var $legend = $('<legend class="col-md-4 col-form-label da-form-label datext-right">' + $label.text() + '</legend>');
const $label = $(this);
const $legend = $(`<legend>` + $label.text() + `</legend>`).addClass($label.attr(`class`));

Copy link
Copy Markdown
Collaborator

@plocket plocket Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Edit: putting a hold on this comment for the moment, I need to look into this code a bit more.]

[Edit: After a second look, I stand by what I said the first time:]
To solve the problem with the legend being a child of the fieldset, could we just add the legend directly as the first child of the outer fieldset? It would still be hidden, it just wouldn't be a sibling of the label.

Comment thread docassemble/ALToolbox/ThreePartsDate.py
Comment on lines +38 to +46
legend.da-form-label:not(:focus):not(:active) {
position: absolute;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
width: 1px;
height: 1px;
white-space: nowrap;
} No newline at end of file
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've probably already looked into accessibility issues and found guidance on using not(:focus). In this case, I believe it will end up showing both the legend and the label at the same time. I don't think that's the typical use case. Not sure if it's necessary/wise to handle it. If we want to handle it, something like this might suffice (testing needed):

Suggested change
legend.da-form-label:not(:focus):not(:active) {
position: absolute;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
width: 1px;
height: 1px;
white-space: nowrap;
}
legend.da-form-label:not(:focus):not(:active) {
position: absolute;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
width: 1px;
height: 1px;
white-space: nowrap;
}
.da-form-group:has(legend.da-form-label:not(:focus):not(:active)) label.da-form-label {
display: none;
}

Also, see my note in the js about the da class.


Nit & curiosity:

What's the purpose of referencing active here. From what I can understand, that's for interactive elements that a user presses on, press enter on while in focus, or otherwise "activates".

The :active pseudo-class is commonly used on and elements. Other common targets of this pseudo-class include elements that are contained in an activated element, and form elements that are being activated through their associated .
- https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:active

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, I believe it will end up showing both the legend and the label at the same time.

A lot of what I ended up doing here was informed by this post from an accessibility developer I follow: https://adrianroselli.com/2022/07/use-legend-and-fieldset.html. They included a disclaimer: "If the is never at risk of receiving focus (through some rogue script or whatever), then dump the :not(:focus):not(:active) from the selector." and I wasn't 100% sure that it was never at risk of receiving focus, so I kept that in. I would guess it's a slightly better degraded interface than keeping it hidden when focused. Some goes for the active part.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That article is a great find!

I would guess it's a slightly better degraded interface than keeping it hidden when focused. Some goes for the active part.

Maybe! 🤷 It's a shame we can't reuse da's stuff. Regardless, I'm not dying on this sword, feel free to keep as is.

For the sake of completeness, here's some churning that may just be thoughts for People of the Future:

If I ever have time to find something to explain the reasoning for active and if it seems sane, I think it would be a decent candidate for an upstream change. I didn't see an example of :not(:focus):not(:active) or of potentially handling visual discrepancies, so I'm not sure how much the author experimented with a case where there was risk of the item gaining focus. I think the hiding I suggested might be a decent way to handle the visual discrepancy in that situation in our particular case, but more research and experimentation would be needed.

Uses `fieldset` and `legend`, replacing docassemble's given `div`.
Keeps the `label` as the visual element, as `legend` has display issues on Safari [^1].

Tested on Chrome and Safari on Mac, and in Firefox (which uses Safari's engine) on iPhone.
Doesn't really improve the situation on iPhone, but that is expected, as group support on
iPhone is poor [^1].

Fixes #164.

[^1] https://adrianroselli.com/2022/07/use-legend-and-fieldset.html
@BryceStevenWilley BryceStevenWilley force-pushed the accessible_three_part_date branch from 6d2be5a to 5eecbb8 Compare April 17, 2026 21:33
Comment on lines +89 to +91
var $label = $(this);
var $legend = $('<legend class="col-md-4 col-form-label da-form-label datext-right">' + $label.text() + '</legend>');
$label.after($legend);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this will do what the above suggests. Namely, ensure the legend is the child of the label's fieldset.

Suggested change
var $label = $(this);
var $legend = $('<legend class="col-md-4 col-form-label da-form-label datext-right">' + $label.text() + '</legend>');
$label.after($legend);
const $label = $(this);
const $legend = $(`<legend>`+ $label.text() + `</legend>`).addClass($label.attr(`class`));
const $fieldset = $label.closest(`fieldset`);
$fieldset.prepend($legend);

It's feeling a little convoluted at the moment. The whole situation is fairly convoluted, so I'm not sure if it's actually more convoluted or if I've been staring at it too long.

With regular quotation marks:

Suggested change
var $label = $(this);
var $legend = $('<legend class="col-md-4 col-form-label da-form-label datext-right">' + $label.text() + '</legend>');
$label.after($legend);
const $label = $(this);
const $legend = $('<legend>'+ $label.text() + '</legend>').addClass($label.attr('class'));
const $fieldset = $label.closest('fieldset');
$fieldset.prepend($legend);

I'm not sure how far down the rabbit hole we want to go to ensure backwards compatibility 🤷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make dates accessible

2 participants