Repository navigation
Conversation
A page had no lang attribute on its <html> element unless the developer
wrote one in the markup or bound the tag to a component. WCAG 2.2
success criterion 3.1.1 (level A) requires the language of every page to
be determinable by software; screen readers pick their pronunciation
from it. A lang written in the markup goes wrong as soon as the
application is localized, and the workaround - a
TransparentWebMarkupContainer bound to <html> with an AttributeModifier -
had to be known and repeated by every application.
WebPage#onHtmlTag(ComponentTag) is called with a mutable copy of the
<html> tag whenever the page renders. The default sets lang to
getLocale().toLanguageTag(), and dir="rtl" when Session.isRtlLanguage
says the language is written right to left. It changes nothing when the
markup has lang or xml:lang already, because the page then declares its
own language, or when the locale has no language (Locale.ROOT). A dir
in the markup is kept. Pages override the method to add attributes that
belong to the document as a whole, such as a theme class or a
data-theme.
How it works: the markup of a page keeps its <html> tags (each one, so
markup choosing between them with conditional comments is covered) as
tags with the reserved id "_html_" and the new
ComponentTag.RENDERED_BY_PAGE flag instead of merging them into raw
markup. No component is created. WebPage#renderNext writes the flagged
tag after the hook; MarkupContainer writes it unchanged for a Page that
is not a WebPage. Component queueing ignores the tag and
TagUtils.findTagMarkup keeps looking inside it, as it does for <head>;
without both, every component inside <html> would no longer be found.
The attribute cannot be added while parsing: MarkupCache shares the
parsed markup of a file between locales, so the first locale to load a
page would win.
Design decisions:
- dir="rtl" is rendered by default for right-to-left languages, using
the detection Session has had since WICKET-6943. It mirrors the page,
so an application that translated its strings into Arabic or Hebrew
but kept a left-to-right stylesheet sees its layout flip. Opting out
is cheap, which is why it is on by default: dir="ltr" in the markup,
or tag.remove("dir") after super.onHtmlTag in the base page. dir="ltr"
is never rendered; it is the browser's default.
- No setting switches either attribute off; overriding onHtmlTag in the
base page is one method.
- IOnComponentTagListeners are not called for the <html> tag. Existing
listeners would start receiving the page and an <html> tag, and one
that writes a test id or a path onto every tag would change the
output of applications that asked for none of this.
- There is no contributor mechanism through which components deeper in
the page add <html> attributes, like IHeaderContributor does for
<head>. The <html> tag is only rendered with the whole page: Ajax
replaces components by markup id, the DOM refuses outerHTML on the
document element, and adding the page to an AjaxRequestTarget
redirects to a full render. A contribution made during an Ajax
request would silently do nothing. State that a page reads in
onHtmlTag (a theme in the session) serves full renders; an Ajax
request changes the attribute with JavaScript, as the Javadoc shows.
A contributor or an application-level listener can be layered on
later without breaking the hook.
- Only <html> gets a hook. It carries what belongs to the document as a
whole and could only be reached through a transparent container;
<head> already has a component, and <body> takes a wicket:id without
the problems <html> has.
What applications see: every page without lang in its markup renders
<html lang="...">, and <html lang="..." dir="rtl"> in a right-to-left
locale, after this change. An <html wicket:id="..."> bound to
a component is left to that component, so the old workaround keeps
working. The <html> tag is now written from its parsed attributes, the
way component tags are: attribute values get double quotes, a valueless
attribute such as xmlns:wicket is written as xmlns:wicket="", and a tag
spread over several lines is written on one. Tests that compare rendered
pages with expected files have to update those files; this commit does
so for wicket-core-tests, where WicketTestCase renders with Locale.US.
Migration guide wording:
Pages render lang and dir on <html>. WebPage now renders
lang="<language tag of the page locale>" on the <html> tag of every
page whose markup does not have lang or xml:lang, and dir="rtl" when
that language is written right to left and the markup has no dir.
Expected-output files of tests that render pages change accordingly.
If your stylesheets do not support right-to-left layout, put
dir="ltr" on <html> in your markup, or override
WebPage#onHtmlTag(ComponentTag) in your base page and call
tag.remove("dir") after super. To switch both attributes off,
override it without calling super. If you bound <html> to a
TransparentWebMarkupContainer only to set lang, you can remove it.
GitHub issue #1848
Generated-by: Claude Code (Claude Opus 5.5)
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1870 +/- ##
============================================
+ Coverage 62.72% 62.75% +0.02%
- Complexity 11512 11534 +22
============================================
Files 1254 1255 +1
Lines 48665 48698 +33
Branches 6845 6855 +10
============================================
+ Hits 30526 30559 +33
Misses 15463 15463
Partials 2676 2676 🚀 New features to boost your workflow:
|
Codecov reported five changed lines without full coverage in #1870. A test now renders a Page that is not a WebPage, which writes its <html> tag unchanged through MarkupContainer#renderNext, and one renders page markup whose <html> tag has a namespace, which HtmlTagHandler leaves alone. The null check on the locale in WebPage#onHtmlTag goes: getLocale() falls back to the session locale and is never null. GitHub issue #1848 Generated-by: Claude Code (Claude Opus 5.5)
The first version of this change wrote dir only for right-to-left languages and left it off otherwise, because left to right is the browser's default and the attribute would be on every page without changing how it renders. In review, solomax argued for writing it in both directions. MDN recommends setting dir explicitly on the root element even for documents in a single script, and stylesheets that select on [dir="ltr"] only match when the attribute is there. WebPage#onHtmlTag now writes dir="ltr" or dir="rtl" next to lang. A dir in the markup is still kept, and markup that declares its own lang still gets neither. The expected-output files gain dir="ltr" after lang. migration-to-12.md describes the change for applications. The file is also created by #1865; whichever pull request is merged second keeps both sections. GitHub issue #1848 Generated-by: Claude Code (Claude Opus 5.5)
Both master and this branch create migration-to-12.md; the merge keeps both sections.
| */ | ||
| protected void onHtmlTag(final ComponentTag tag) | ||
| { | ||
| if (tag.getAttribute("lang") != null || tag.getAttribute("xml:lang") != null) |
There was a problem hiding this comment.
HTML attribute names are case-insensitive, but getAttribute is a plain map lookup. <html LANG="en" DIR="ltr"> doesn't count as having lang, and renders as <html LANG="en" DIR="ltr" lang="en-US" dir="ltr">. Browsers take the first one, but validators and accessibility checkers such as axe flag the duplicates. The same applies to XML:LANG and to the dir check below.
ValueMap.getKey(String) already does a case-insensitive lookup:
ValueMap attributes = tag.getAttributes();
if (attributes.getKey("lang") != null || attributes.getKey("xml:lang") != null)
{
return;
}
...
if (attributes.getKey("dir") == null)A case in HtmlTagTest with upper-case attributes would cover it.
There was a problem hiding this comment.
Good catch, thanks. fab7cc7 checks lang, xml:lang and dir with getKey now, as you suggested. HtmlTagTest has three new cases with upper-case attributes: LANG/DIR, XML:LANG and DIR alone. Before the fix they produced exactly the duplicates you described.
HTML attribute names are case-insensitive, but WebPage#onHtmlTag looked them up with getAttribute, a case-sensitive map lookup. Markup with <html LANG="en" DIR="ltr"> counted as having neither, and rendered as <html LANG="en" DIR="ltr" lang="en-US" dir="ltr">: browsers use the first of each, but validators and accessibility checkers such as axe flag the duplicates. The same went for XML:LANG. The checks now use IValueMap#getKey, which ignores case, as papegaaij pointed out in review. GitHub issue #1848 Generated-by: Claude Code (Claude Opus 5.5)
Pages now render
langanddiron their<html>tag from the page locale:dir="rtl"when that language is written right to left,dir="ltr"otherwise. Screen readers pick their pronunciation fromlang(WCAG 2.2 SC 3.1.1, level A). Until now an application had to bind<html>to aTransparentWebMarkupContainerto get it.WebPage#onHtmlTag(ComponentTag)receives a mutable copy of the<html>tag on every full render:lang: set togetLocale().toLanguageTag(), unless the markup haslangorxml:lang, or the locale has no language (Locale.ROOT).dir: set tortlwhenSession.isRtlLanguage(WICKET-6943) recognises the language and toltrotherwise, unless the markup has its owndiror declares its ownlang. Writingltrexplicitly follows solomax's review and MDN's recommendation; 4f19938 explains why.dir="ltr"on<html>in the markup, or calltag.remove("dir")aftersuper.onHtmlTag(tag)in the base page.data-theme.The commit messages carry the design decisions: why there is no setting, no
IOnComponentTagListenercall, no contributor mechanism and no hook for<head>or<body>. It also explains why<html>attributes can't change on an Ajax request. The draft migration-guide wording is there too.Where to look
Core:
WebPage:renderNextandonHtmlTag.HtmlTagHandler(new, page markup only): keeps each<html>tag as a tag with id_html_and the newComponentTag.RENDERED_BY_PAGEflag. No component is created.MarkupContainerandTagUtils: queueing ignores the flagged tag, andfindTagMarkupkeeps looking inside it, as it does for<head>. Without this, components inside<html>are no longer found or dequeued.HtmlTagTest: 21 tests covering locale changes between renders, markuplang/xml:lang/dirin any case,Locale.ROOT, RTL and the Latin-script exception, the opt-out, a bound<html wicket:id>, markup inheritance, queueing and an Ajax update.Mechanical:
<html>line (langanddiradded).<html>.Small test fixes:
AjaxBehaviorEnabledTestbuilt its ownWicketTesterand rendered with the JVM default locale, so it failed on non-US machines. It now usesnewWicketTester, which pinsLocale.US.OpenCloseTagExpandertest markup now quotes anxmlns:wicketvalue, because the parser mangles unquoted values containing/or:(Unquoted attribute values containing / or : are cut off by the markup parser #1864).Migration guide:
migration-to-12.md, now on master, gets a section on the change. The branch was merged with master to resolve the conflict on that file; the merge keeps both sections.Risk
langin their markup getlanganddir. Tests that compare rendered pages with expected files need updating.<html>tag is now written from its parsed attributes, like a component tag. Values get double quotes, a valuelessxmlns:wicketbecomesxmlns:wicket="", and a tag spread over several lines is written on one.Verification
mvn clean verify -Pjs-testpassed in two parts: the first run stopped at one extensions expected file, and the run resumed from wicket-extensions passed after fixing it.dir, wicket-core-tests (2571) and wicket-extensions (141) pass locally.nl_NL.Closes #1848
Generated-by: Claude Code (Claude Opus 5.5)