Explorar o código

Page shells: <main> starts before the cover, so the page heading is inside it (#5922)

The cover and its <h1> were printed before <main id="bx-content-main">, outside every
landmark: axe reported `region` (1 on /about, 20 on a profile page, also the profile
submenu) and the skip link jumped past the page heading.

Every shell (default _sub_header/_sub_footer, pt_application, and Artificer, Ocean,
Decorous, Protean, Lucid) now opens <main role="main" id="bx-content-main" tabindex="-1">
before the cover and closes it after #bx-content-wrapper. The old inner <main> becomes
<div class="bx-content-main ..."> with its spacing classes, so the layout is unchanged.
The htmx partials (page_54/56/58) open no <main>; they are swapped inside it.

In the theme shells #bx-content-wrapper was the flex item that grew to keep the footer at
the bottom; <main> takes that over in general.css, and Messenger's full-height column
rules follow.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Andrey Yasko hai 1 día
pai
achega
6c42f04a5d

+ 4 - 4
modules/boonex/artificer/data/template/system/pt_application.html

@@ -63,7 +63,7 @@
 
         <!-- BEG: Main Content -->
         <div id="main-content" class="bx-main-column wide relative w-full h-full overflow-y-auto xl:ml-64">
-            <div>
+            <main role="main" id="bx-content-main" tabindex="-1">
                 <bx_injection:injection_between_cover_logo />
                 __cover__
                 <bx_injection:injection_between_cover_top_menu />
@@ -90,17 +90,17 @@
                     __informer__
                     <div class="bx-page-wrapper mx-auto px-3 md:px-4 lg:px-6 box-border">
                         <div id="bx-content-container" class="bx-content-container">
-                            <main role="main"  id="bx-content-main" class="bx-content-main my-2 lg:my-3" tabindex="-1">
+                            <div class="bx-content-main my-2 lg:my-3">
                                 <bx_injection:injection_content_before />
                                 __page_main_code__
                                 <bx_injection:injection_content_after />
                                 <bx_injection:banner_bottom />
-                            </main> <!-- #bx-content-main -->
+                            </div> <!-- .bx-content-main -->
                             <bx_injection:injection_between_content_footer />
                         </div> <!-- #bx-content-container -->
                     </div> <!-- .bx-page-wrapper -->
                 </div> <!-- #bx-content-wrapper -->
-            </div>
+            </main> <!-- #bx-content-main -->
             <footer class="p-4 m-4 bg-white rounded-lg shadow md:flex md:items-center md:justify-between md:p-6 xl:p-8 dark:bg-gray-800">
                 <bx_injection:injection_footer_before />
                 <div id="bx-menu-bottom">

+ 2 - 1
modules/boonex/decorous/data/template/system/_sub_footer.html

@@ -5,7 +5,7 @@
 						
 						            <bx_injection:banner_bottom />
 						
-						        </main> <!-- #bx-content-main -->
+						        </div> <!-- .bx-content-main -->
 						
 						        <bx_injection:injection_between_content_footer />
 						
@@ -14,6 +14,7 @@
 						</div>
 				
 					</div> <!-- #bx-content-wrapper -->
+					</main> <!-- #bx-content-main -->
 	
                             <div id="bx-footer-wrapper" class="bx-footer-wrapper bx-def-image-bg-footer bx-def-color-bg-footer bx-def-border-footer bx-shadow-footer">    
                                 <div class="bx-def-centered bx-def-padding-sec-leftright bx-def-box-sizing">

+ 2 - 1
modules/boonex/decorous/data/template/system/_sub_header.html

@@ -24,6 +24,7 @@
                 <bx_menu:sys_site />
 	        <div class="content-wrapper-wrp">
                     <div class="content-wrapper bx-def-box-sizing">
+                        <main role="main" id="bx-content-main" tabindex="-1">
                         <bx_injection:injection_between_cover_logo />
                         __cover__
                         <bx_injection:injection_between_cover_top_menu />
@@ -56,4 +57,4 @@
                             <div class="bx-page-wrapper bx-def-centered bx-def-padding-sec-leftright bx-def-page-width bx-def-box-sizing">
                                 <div id="bx-content-container" class="bx-def-padding-sec-left bx-def-padding-sec-right">
                                     <bx_injection:injection_content_before />
-                                    <main role="main" id="bx-content-main" class=" my-4 lg:my-6">
+                                    <div class="bx-content-main my-4 lg:my-6">

+ 4 - 2
modules/boonex/decorous/data/template/system/pt_application.html

@@ -54,6 +54,7 @@
                     <!-- default sys_site is here -->
                     <div class="content-wrapper-wrp">
                         <div class="content-wrapper bx-def-box-sizing">
+                            <main role="main" id="bx-content-main" tabindex="-1">
                             <bx_injection:injection_between_cover_logo />
                             __cover__
                             <bx_injection:injection_between_cover_top_menu />
@@ -82,17 +83,18 @@
                                 __informer__
                                 <div class="bx-page-wrapper bx-def-centered bx-def-padding-sec-leftright bx-def-page-width bx-def-box-sizing">
                                     <div id="bx-content-container" class="bx-def-padding-sec-left bx-def-padding-sec-right">
-                                        <main role="main" id="bx-content-main" class="bx-def-margin-sec-topbottom">
+                                        <div class="bx-content-main bx-def-margin-sec-topbottom">
                                             <bx_injection:injection_content_before />
                                             __page_main_code__
                                             <bx_injection:injection_content_after />
                                             <div class="bx-clear"></div>
                                             <bx_injection:banner_bottom />
-                                        </main> <!-- #bx-content-main -->
+                                        </div> <!-- .bx-content-main -->
                                         <bx_injection:injection_between_content_footer />
                                     </div> <!-- #bx-content-container -->
                                 </div>
                             </div> <!-- #bx-content-wrapper -->
+                            </main> <!-- #bx-content-main -->
                         </div> <!-- .content-wrapper -->
                     </div> <!-- .content-wrapper-wrp -->
                 </div> <!-- .cd-main-content -->

+ 2 - 1
modules/boonex/lucid/data/template/system/_sub_footer.html

@@ -5,7 +5,7 @@
 						
 						            <bx_injection:banner_bottom />
 						
-						        </main> <!-- #bx-content-main -->
+						        </div> <!-- .bx-content-main -->
 						
 						        <bx_injection:injection_between_content_footer />
 						
@@ -14,6 +14,7 @@
 						</div>
 				
 					</div> <!-- #bx-content-wrapper -->
+					</main> <!-- #bx-content-main -->
 	
                             <div id="bx-footer-wrapper" class="bx-footer-wrapper bx-def-image-bg-footer bx-def-color-bg-footer bx-def-border-footer bx-shadow-footer">    
                                 <div class="bx-def-centered bx-def-padding-sec-leftright bx-def-box-sizing">

+ 2 - 1
modules/boonex/lucid/data/template/system/_sub_header.html

@@ -26,6 +26,7 @@
             <div class="cd-main-content bx-clearfix">
             <div class="content-wrapper-wrp">
                 <div class="content-wrapper bx-def-box-sizing">
+                    <main role="main" id="bx-content-main" tabindex="-1">
                     <bx_injection:injection_between_cover_logo />
                     __cover__
                     <bx_injection:injection_between_cover_top_menu />
@@ -58,4 +59,4 @@
                         <div class="bx-page-wrapper bx-def-centered bx-def-padding-sec-leftright bx-def-page-width bx-def-box-sizing">
                             <div id="bx-content-container" class="bx-def-padding-sec-left bx-def-padding-sec-right">
                                 <bx_injection:injection_content_before />
-                                <main role="main" id="bx-content-main" class=" my-4 lg:my-6">
+                                <div class="bx-content-main my-4 lg:my-6">

+ 4 - 2
modules/boonex/lucid/data/template/system/pt_application.html

@@ -53,6 +53,7 @@
                 <div class="cd-main-content bx-clearfix">
                     <div class="content-wrapper-wrp">
                         <div class="content-wrapper bx-def-box-sizing">
+                            <main role="main" id="bx-content-main" tabindex="-1">
                             <bx_injection:injection_between_cover_logo />
                             __cover__
                             <bx_injection:injection_between_cover_top_menu />
@@ -81,17 +82,18 @@
                                 __informer__
                                 <div class="bx-page-wrapper bx-def-centered bx-def-padding-sec-leftright bx-def-page-width bx-def-box-sizing">
                                     <div id="bx-content-container" class="bx-def-padding-sec-left bx-def-padding-sec-right">
-                                        <main role="main" id="bx-content-main" class="bx-def-margin-sec-topbottom">
+                                        <div class="bx-content-main bx-def-margin-sec-topbottom">
                                             <bx_injection:injection_content_before />
                                             __page_main_code__
                                             <bx_injection:injection_content_after />
                                             <div class="bx-clear"></div>
                                             <bx_injection:banner_bottom />
-                                        </main> <!-- #bx-content-main -->
+                                        </div> <!-- .bx-content-main -->
                                         <bx_injection:injection_between_content_footer />
                                     </div> <!-- #bx-content-container -->
                                 </div>
                             </div> <!-- #bx-content-wrapper -->
+                            </main> <!-- #bx-content-main -->
                         </div> <!-- .content-wrapper -->
                     </div> <!-- .content-wrapper-wrp -->
                 </div> <!-- .cd-main-content -->

+ 14 - 1
modules/boonex/messenger/template/css/messenger.css

@@ -45,7 +45,8 @@
 	min-height: auto;
 }
 
-.bx-main  > div{
+.bx-main  > div,
+.bx-main  > main{
 	flex: 0 0 auto;
 }
 #bx-content-with-submenu-wrapper,
@@ -57,6 +58,18 @@
    display: flex;
    flex-direction: column;
 }
+/* In the theme shells <main> starts before the cover and holds #bx-content-wrapper, so it takes the wrapper's place in the column. */
+#bx-content-main:has(> #bx-content-wrapper) {
+   flex: 1 1 0;
+   overflow:hidden;
+   max-height:100%;
+   height: 100%;
+   display: flex;
+   flex-direction: column;
+}
+#bx-content-main:has(> #bx-content-wrapper) > :not(#bx-content-wrapper) {
+   flex: 0 0 auto;
+}
 
 .cd-main-content .content-wrapper
 {

+ 2 - 1
modules/boonex/ocean/data/template/system/_sub_footer.html

@@ -5,7 +5,7 @@
 		
 		            <bx_injection:banner_bottom />
 		
-		        </main> <!-- #bx-content-main -->
+		        </div> <!-- .bx-content-main -->
 		
 		        <bx_injection:injection_between_content_footer />
 		
@@ -14,6 +14,7 @@
 		</div>
 	
 	</div> <!-- #bx-content-wrapper -->
+	</main> <!-- #bx-content-main -->
 	
 	<div id="bx-footer-wrapper">
             <div class="bx-def-centered bx-def-padding-topbottom bx-def-padding-sec-leftright bx-def-page-width bx-def-box-sizing">

+ 2 - 1
modules/boonex/ocean/data/template/system/_sub_header.html

@@ -22,6 +22,7 @@
 	
 	</div>
 
+	<main role="main" id="bx-content-main" tabindex="-1">
 	<bx_injection:injection_between_cover_logo />
 
         __cover__
@@ -61,4 +62,4 @@
 
                     <bx_injection:injection_content_before />
 
-                    <main role="main" id="bx-content-main" class="my-4 lg:my-6">
+                    <div class="bx-content-main my-4 lg:my-6">

+ 4 - 2
modules/boonex/ocean/data/template/system/pt_application.html

@@ -27,6 +27,7 @@
             </div>
         </div>
         <div class="bx-main-column wide">
+            <main role="main" id="bx-content-main" tabindex="-1">
             <bx_injection:injection_between_cover_logo />
             __cover__
             <bx_injection:injection_between_cover_top_menu />
@@ -55,17 +56,18 @@
                 __informer__
                 <div class="bx-page-wrapper bx-def-centered bx-def-padding-sec-leftright bx-def-page-width bx-def-box-sizing">
                     <div id="bx-content-container" class="bx-def-padding-sec-leftright">
-                        <main role="main" id="bx-content-main" class="bx-def-margin-sec-topbottom">
+                        <div class="bx-content-main bx-def-margin-sec-topbottom">
                             <bx_injection:injection_content_before />
                             __page_main_code__
                             <bx_injection:injection_content_after />
                             <div class="bx-clear"></div>
                             <bx_injection:banner_bottom />
-                        </main> <!-- #bx-content-main -->
+                        </div> <!-- .bx-content-main -->
                         <bx_injection:injection_between_content_footer />
                     </div> <!-- #bx-content-container -->
                 </div>
             </div> <!-- #bx-content-wrapper -->
+            </main> <!-- #bx-content-main -->
         </div> <!-- .bx-main-column -->
     </div> <!-- .bx-main-content -->
     <div id="bx-footer-wrapper">

+ 2 - 1
modules/boonex/protean/data/template/system/_sub_footer.html

@@ -5,7 +5,7 @@
 		
 		            <bx_injection:banner_bottom />
 		
-		        </main> <!-- #bx-content-main -->
+		        </div> <!-- .bx-content-main -->
 		
 		        <bx_injection:injection_between_content_footer />
 		
@@ -14,6 +14,7 @@
 		</div>
 
 	</div> <!-- #bx-content-wrapper -->
+	</main> <!-- #bx-content-main -->
 
 	<div id="bx-footer-wrapper" class="bx-def-image-bg-footer bx-def-color-bg-footer bx-def-border-footer bx-shadow-footer">	
             <div class="bx-def-centered bx-def-padding-sec-leftright bx-def-page-width bx-def-box-sizing">

+ 2 - 1
modules/boonex/protean/data/template/system/_sub_header.html

@@ -19,6 +19,7 @@
         </div>
     </div>
 
+    <main role="main" id="bx-content-main" tabindex="-1">
     <bx_injection:injection_between_cover_logo />
     __cover__
     <bx_injection:injection_between_cover_top_menu />
@@ -51,4 +52,4 @@
         <div class="bx-page-wrapper bx-def-centered bx-def-padding-sec-leftright bx-def-page-width bx-def-box-sizing">
             <div id="bx-content-container" class="bx-content-container bx-def-padding-sec-leftright">
                 <bx_injection:injection_content_before />
-                <main role="main" id="bx-content-main" class=" my-4 lg:my-6">
+                <div class="bx-content-main my-4 lg:my-6">

+ 4 - 2
modules/boonex/protean/data/template/system/pt_application.html

@@ -49,6 +49,7 @@
 
         <!-- BEG: Main Content -->
         <div id="main-content" class="bx-main-column wide relative w-full h-full overflow-y-auto xl:ml-64">
+            <main role="main" id="bx-content-main" tabindex="-1">
             <bx_injection:injection_between_cover_logo />
             __cover__
             <bx_injection:injection_between_cover_top_menu />
@@ -77,17 +78,18 @@
                 __informer__
                 <div class="bx-page-wrapper bx-def-padding-sec-leftright bx-def-box-sizing">
                     <div id="bx-content-container" class="bx-content-container bx-def-padding-sec-leftright">
-                        <main role="main" id="bx-content-main" class="bx-content-main bx-def-margin-top">
+                        <div class="bx-content-main bx-def-margin-top">
                             <bx_injection:injection_content_before />
                             __page_main_code__
                             <bx_injection:injection_content_after />
                             <div class="bx-clear"></div>
                             <bx_injection:banner_bottom />
-                        </main> <!-- #bx-content-main -->
+                        </div> <!-- .bx-content-main -->
                         <bx_injection:injection_between_content_footer />
                     </div> <!-- #bx-content-container -->
                 </div>
             </div> <!-- #bx-content-wrapper -->
+            </main> <!-- #bx-content-main -->
         </div> <!-- .bx-main-column -->
     </div> <!-- .bx-main-content -->
     <div id="bx-footer-wrapper" class="bx-def-image-bg-footer bx-def-color-bg-footer bx-def-border-footer bx-shadow-footer">	

+ 1 - 1
template/_page_content_beg.html

@@ -2,4 +2,4 @@ __informer__
 <div class="bx-page-wrapper bx-def-page-width mx-auto px-3 md:px-4 lg:px-6 box-border">
     <div id="bx-content-container" class="bx-content-container">
         <bx_injection:injection_content_before />
-        <main role="main" id="bx-content-main" class="bx-content-main my-4 lg:my-6" tabindex="-1">
+        <div class="bx-content-main my-4 lg:my-6">

+ 1 - 1
template/_page_content_end.html

@@ -1,6 +1,6 @@
             <bx_injection:injection_content_after />
             <bx_injection:banner_bottom />
-        </main> <!-- #bx-content-main -->
+        </div> <!-- .bx-content-main -->
         <bx_injection:injection_between_content_footer />
     </div> <!-- #bx-content-container -->
 </div>

+ 1 - 0
template/_sub_footer.html

@@ -2,6 +2,7 @@
             </div> <!-- #bx-content-wrapper -->
         </div> <!-- #bx-content-with-submenu-wrapper -->
     </div> <!-- #bx-content-with-cover-wrapper -->
+    </main> <!-- #bx-content-main -->
 
     <bx_include_auto:footer.html />
 

+ 1 - 0
template/_sub_header.html

@@ -2,6 +2,7 @@
 <div id="bx-content-preload" class="hidden"></div>
 <div id="bx-content-with-toolbar-wrapper" class="bx-main <bx_injection:injection_main_class />">
     <bx_include_auto:_page_toolbar.html />
+    <main role="main" id="bx-content-main" tabindex="-1">
     <div id="bx-content-with-cover-wrapper">
         <bx_include_auto:_page_cover.html />
         <div id="bx-content-with-submenu-wrapper">

+ 5 - 0
template/css/general.css

@@ -184,6 +184,11 @@
 	-webkit-flex-grow: 3; 
     flex-grow: 3;
 }
+/* In the theme shells <main> starts before the cover and holds #bx-content-wrapper,
+   so it is the flex item now and has to grow for the footer to stay at the bottom. */
+#bx-content-main:has(> #bx-content-wrapper) {
+    flex-grow: 3;
+}
 
 /* --------------- footer */
 

+ 4 - 2
template/pt_application.html

@@ -35,6 +35,7 @@
             </div>
         </div>
         <div class="bx-main-column wide">
+            <main role="main" id="bx-content-main" tabindex="-1">
             <bx_injection:injection_between_cover_logo />
             __cover__
             <bx_injection:injection_between_cover_top_menu />
@@ -61,17 +62,18 @@
                 __informer__
                 <div class="bx-page-wrapper bx-def-padding-sec-leftright bx-def-box-sizing">
                     <div id="bx-content-container" class="bx-content-container bx-def-padding-sec-leftright">
-                        <main role="main" id="bx-content-main" class="bx-content-main bx-def-margin-top" tabindex="-1">
+                        <div class="bx-content-main bx-def-margin-top">
                             <bx_injection:injection_content_before />
                             __page_main_code__
                             <bx_injection:injection_content_after />
                             <div class="bx-clear"></div>
                             <bx_injection:banner_bottom />
-                        </main> <!-- #bx-content-main -->
+                        </div> <!-- .bx-content-main -->
                         <bx_injection:injection_between_content_footer />
                     </div> <!-- #bx-content-container -->
                 </div>
             </div> <!-- #bx-content-wrapper -->
+            </main> <!-- #bx-content-main -->
         </div> <!-- .bx-main-column -->
     </div> <!-- .bx-main-content -->
     <div id="bx-footer-wrapper">

+ 82 - 0
tests/units/template/PageShellLandmarksTest.php

@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * Every page shell opens <main> before the cover, so the cover and its <h1> sit inside the main landmark
+ * and the skip link (#bx-content-main) lands on the page heading. The htmx partials swapped into it
+ * (page_54/56/58) open no <main> of their own.
+ */
+class PageShellLandmarksTest extends BxDolTestCase
+{
+    private static function read(string $sPath): string
+    {
+        $s = file_get_contents(__DIR__ . '/../../../' . $sPath);
+        if ($s === false)
+            self::fail($sPath . ' is not readable.');
+
+        return str_replace("\r\n", "\n", $s);
+    }
+
+    /**
+     * Shell => [files it is made of, in output order; cover marker; marker of the page content; marker that must come right before </main>].
+     */
+    public static function shells(): array
+    {
+        $a = [
+            'default' => [['template/_sub_header.html', 'template/default.html', 'template/_sub_footer.html'], '<bx_include_auto:_page_cover.html />', '__page_main_code__', '</div> <!-- #bx-content-with-cover-wrapper -->'],
+            'application' => [['template/pt_application.html'], '__cover__', '__page_main_code__', '</div> <!-- #bx-content-wrapper -->'],
+            'artificer application' => [['modules/boonex/artificer/data/template/system/pt_application.html'], '__cover__', '__page_main_code__', '</div> <!-- #bx-content-wrapper -->'],
+        ];
+
+        foreach (['ocean', 'decorous', 'protean', 'lucid'] as $sTemplate) {
+            $sDir = 'modules/boonex/' . $sTemplate . '/data/template/system/';
+            $a[$sTemplate] = [[$sDir . '_sub_header.html', 'template/default.html', $sDir . '_sub_footer.html'], '__cover__', '__page_main_code__', '</div> <!-- #bx-content-wrapper -->'];
+            $a[$sTemplate . ' application'] = [[$sDir . 'pt_application.html'], '__cover__', '__page_main_code__', '</div> <!-- #bx-content-wrapper -->'];
+        }
+
+        return $a;
+    }
+
+    #[\PHPUnit\Framework\Attributes\DataProvider('shells')]
+    public function testCoverIsInsideMain(array $aFiles, string $sCover, string $sContent, string $sBeforeClose): void
+    {
+        $s = implode("\n", array_map([self::class, 'read'], $aFiles));
+
+        $this->assertSame(1, preg_match_all('/<main\b/', $s), 'One <main> per page.');
+        $this->assertSame(1, substr_count($s, '</main>'));
+        $this->assertSame(1, substr_count($s, 'id="bx-content-main"'), 'The skip link target is the <main> itself.');
+        $this->assertMatchesRegularExpression('/<main role="main" id="bx-content-main" tabindex="-1">/', $s);
+
+        $iOpen = strpos($s, '<main');
+        $iCover = strpos($s, $sCover);
+        $iContent = strpos($s, $sContent);
+        $iClose = strpos($s, '</main>');
+        $this->assertNotFalse($iCover);
+        $this->assertNotFalse($iContent);
+        $this->assertTrue($iOpen < $iCover && $iCover < $iContent && $iContent < $iClose, '<main> opens before the cover and closes after the content.');
+        $this->assertMatchesRegularExpression('/' . preg_quote($sBeforeClose, '/') . '\n\s*<\/main>/', $s, '<main> closes right after the content wrapper.');
+
+        $this->assertDoesNotMatchRegularExpression('/<footer\b/', substr($s, $iOpen, $iClose - $iOpen), 'The footer stays outside <main>.');
+    }
+
+    public static function partials(): array
+    {
+        return array_map(fn($s) => [$s], ['template/_page_content_beg.html', 'template/_page_content_end.html', 'template/page_54.html', 'template/page_56.html', 'template/page_58.html']);
+    }
+
+    #[\PHPUnit\Framework\Attributes\DataProvider('partials')]
+    public function testHtmxPartialsOpenNoMain(string $sPath): void
+    {
+        $s = self::read($sPath);
+
+        $this->assertDoesNotMatchRegularExpression('/<\/?main\b/', $s, 'The partial is swapped inside the shell\'s <main>.');
+        $this->assertStringNotContainsString('id="bx-content-main"', $s);
+    }
+
+    public function testMainKeepsTheFooterAtTheBottomInThemeShells(): void
+    {
+        $s = self::read('template/css/general.css');
+
+        $this->assertMatchesRegularExpression('/#bx-content-wrapper \{[^}]*flex-grow: 3;/', $s);
+        $this->assertMatchesRegularExpression('/#bx-content-main:has\(> #bx-content-wrapper\) \{[^}]*flex-grow: 3;/', $s, '<main> took #bx-content-wrapper\'s place as the flex item.');
+    }
+}