[LON-CAPA-cvs] cvs: loncom /interface loncommon.pm lonfeedback.pm

raeburn raeburn at source.lon-capa.org
Sun Jun 21 13:18:12 EDT 2026


raeburn		Sun Jun 21 17:18:12 2026 EDT

  Modified files:              
    /loncom/interface	lonfeedback.pm loncommon.pm 
  Log:
  - WCAG 2 compliance
    - Replace use of <table> for visual representation of threaded discussion
      with semantic <ul> <li> etc.
    - Include page region and heading structure in modal "Display Options" 
      window used to set "Other Views" of discussion posts.
    - Replace use of <table> for layout with <div>.
    - Group form elements in fieldset with legend for screenreaders.
    - Satisfy minimum spacing between touch targets.
    - Contrast ratio of at least 4.5:1 for normal text.
  
  
-------------- next part --------------
Index: loncom/interface/lonfeedback.pm
diff -u loncom/interface/lonfeedback.pm:1.398 loncom/interface/lonfeedback.pm:1.399
--- loncom/interface/lonfeedback.pm:1.398	Sat Jan 31 02:43:24 2026
+++ loncom/interface/lonfeedback.pm	Sun Jun 21 17:18:12 2026
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # Feedback
 #
-# $Id: lonfeedback.pm,v 1.398 2026/01/31 02:43:24 raeburn Exp $
+# $Id: lonfeedback.pm,v 1.399 2026/06/21 17:18:12 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -457,7 +457,6 @@
                 $discussion .= $lt{'aner'}.'<br />';
             }
 	} else {
-            my $colspan=$maxdepth+1;
             $discussion.= &Apache::lonhtmlcommon::scripttag(qq|
    function verifydelete (caller,symb,idx,newflag,previous,groupparm) {
        var symbparm = symb+':::'+idx
@@ -479,15 +478,14 @@
    }
             |);
 	    $discussion.='<form name="readchoices" method="post" action="/adm/feedback?chgreads='.$ressymb.'" >'.
-                         "\n".'<table width="100%" class="LC_discussion">';
-            $discussion .= &action_links_bar($colspan,$ressymb,$visible,
-                                             $newpostsflag,$group,
+                         "\n".'<div class="LC_grid LC_discussion" role="grid" style="width: 100%; margin: 8px 0 0 0;">';
+            $discussion .= &action_links_bar($ressymb,$visible,$newpostsflag,$group,
                                              $prevread,$markondisp,$seehidden);
             my $escsymb=&escape($ressymb);
             my $numhidden = keys(%notshown);
             if ($numhidden > 0) {
-                my $colspan = $maxdepth+1;
-                $discussion.="\n".'<tr><td bgcolor="#CCCCCC" colspan="'.$colspan.'">';
+                $discussion.="\n".'<div class="LC_grid_row" role="row" style="width: 100%;">'.
+                                  '<div class="LC_grid_cell" role=gridcell" style="background-color: #CCCCCC; margin: 0;">';
                 my $href = '/adm/feedback?allposts=1&symb='.$escsymb;
                 if ($newpostsflag) {
                     $href .= '&previous='.$prevread;
@@ -500,7 +498,7 @@
                     $discussion .= &mt('[_1]Show all posts[_2] to display [quant,_3,post] previously viewed',
                                        '<a href="'.$href.'">','</a>',$numhidden);
                 }
-                $discussion .= '<br/></td></tr>';
+                $discussion .= '<br/></div></div>';
             }
         }
 
@@ -531,6 +529,20 @@
         }
         my $currdepth = 0;
         my $firstidx = $alldiscussion{$showposts[0]};
+        if (@showposts) {
+            $discussion.= '<div class="LC_grid_row" role="row" style="margin: 0; width: 100%;">'.
+                          '<div class="LC_grid_cell" role="gridcell" style="margin: 0; width: 100%;">';
+        }
+        my ($doindent,$prevdepth);
+        unless ($outputtarget eq 'tex') {
+            if (($sortposts eq 'thread') ||
+                (($sortposts eq '') && (!$env{'environment.unthreadeddiscussion'}))) {
+                $doindent = 1;
+            }
+            unless ($doindent) {
+                $discussion.= '<ul style="margin: 0; padding: 0; list-style-type: none;">';
+            }
+        }
         foreach my $post (@showposts) {
             unless (($sortposts eq 'thread') || 
                     (($sortposts eq '') && (!$env{'environment.unthreadeddiscussion'})) || 
@@ -538,16 +550,31 @@
                 $alldiscussion{$post} = $post;
             }
             unless ( ($notshown{$alldiscussion{$post}} eq '1') || ($shown{$alldiscussion{$post}} == 0) ) {
-                if ($outputtarget ne 'tex' && $outputtarget ne 'export') {
-		    $discussion.="\n<tr>";
-		}
 	        my $thisdepth=$depth[$alldiscussion{$post}];
                 if ($outputtarget ne 'tex' && $outputtarget ne 'export') {
-		    for (1..$thisdepth) {
-			$discussion.='<td>    </td>';
-		    }
-		}
-	        my $colspan=$maxdepth-$thisdepth+1;
+                    if ($doindent) {
+                        my $diffdepth = $thisdepth - $prevdepth;
+                        if ($prevdepth eq '') {
+                            $discussion.= '<ul style="margin: 0; padding: 0; list-style-type: none;">';
+                        } else {
+                            my $diffdepth = $thisdepth - $prevdepth;
+                            if ($diffdepth > 0) {
+                                $discussion.="\n".'<ul style="margin: 0; padding-left: 2em; list-style-type: none;">';
+                            } else {
+                                $discussion.='</li>'."\n";
+                                if ($diffdepth < 0) {
+                                    for (my $i=0; $i>$diffdepth; $i--) {
+                                        $discussion.="\n</ul></li>";
+                                    }
+                                }
+                            }
+                        }
+                        $discussion.="\n<li>";
+                        $prevdepth = $thisdepth;
+                    } else {
+                        $discussion.="\n<li>";
+                    }
+                }
                 if ($outputtarget eq 'tex') {
 		    #cleanup block
 		    $discussionitems[$alldiscussion{$post}]=~s/<table([^>]*)>/<table TeXwidth="90 mm">/;
@@ -593,21 +620,32 @@
                     }
                     $copyresult.=&replicate_attachments($imsitems{$alldiscussion{$post}}{'allattachments'},$tempexport);
                 } else {
-                    $discussion.='<td class="'.$bgcols[$newitem{$alldiscussion{$post}}].
-                       '" colspan="'.$colspan.'">'. $discussionitems[$alldiscussion{$post}].
-                       '</td></tr>';
+                    $discussion.= '<div class="'.$bgcols[$newitem{$alldiscussion{$post}}].'">'
+                                 .$discussionitems[$alldiscussion{$post}].'</div>';
+                    unless ($doindent) {
+                        $discussion.= '</li>'."\n";
+                    }
                 }
 	    }
         }
+        if (@showposts) {
+            unless ($outputtarget eq 'tex') {
+                if ($doindent) {
+                    for (0..$prevdepth) {
+                        $discussion.= '</li></ul>';
+                    }
+                } else {
+                    $discussion.= '</ul>';
+                }
+            }
+            $discussion.= '</div></div>';
+        }
 	unless ($outputtarget eq 'tex' || $outputtarget eq 'export') {
-            my $colspan=$maxdepth+1;
             $discussion .= <<END;
-            <tr>
-             <td colspan="$colspan">
-              <table width="100%">
-               <tr>
-                <td class="LC_disc_action_left">
-                    <font size="-1"><b>$lt{'cuse'}</b>: 
+            <div class="LC_grid_row" role="row">
+            <div class="LC_grid_cell" role="gridcell">
+            <div class="LC_split_text" style="width: 100%; margin: 0; line-height: 170%;">
+            <span style="font-size: smaller;"><b>$lt{'cuse'}</b>: 
 END
             if ($newpostsflag) {
                 $discussion .= 
@@ -624,8 +662,7 @@
                 }
             }
             $discussion .= <<END;
-                     <b><a href="$chglink">$lt{'chgt'}</a></b></font>
-                </td>
+                     <b><a href="$chglink">$lt{'chgt'}</a></b></span>
 END
             if ($sortposts) {
                 my %sort_types = ();
@@ -633,7 +670,7 @@
                 my %status_types = ();
                 &sort_filter_names(\%sort_types,\%role_types,\%status_types,$crstype);
 
-                $discussion .= '<td class="LC_disc_action_right"><font size="-1"><b>'.&mt('Sorted by').'</b>: '.$sort_types{$sortposts}.'<br />';
+                $discussion .= '<span style="font-size: smaller;"><b>'.&mt('Sorted by').'</b>: '.$sort_types{$sortposts}.'<br />';
                 if (defined($env{'form.totposters'})) {
                     $discussion .= &mt('Posts by').':';
                     if ($totposters > 0) {
@@ -669,27 +706,20 @@
                         $discussion .= '<b>'.&mt('Filters').'</b>: '.$filterchoice;
                     }
                 }
-                $discussion .= '</font></td>';
-
+                $discussion .= '</span>';
             }
             if ($dischash{$toggkey}) {
                 my $storebutton = &mt('Save read/unread changes');
-                $discussion.='<td align="right">'.
+                $discussion.= '<span>'.
               '<input type="hidden" name="discsymb" value="'.$ressymb.'" />'."\n".
               '<input type="button" name="readoptions" value="'.$storebutton.'"'.
               ' onclick="this.form.submit();" />'."\n".
-              '</td>';
+              '</span>';
             }
-            $discussion .= (<<END);
-               </tr>
-              </table>
-             </td>
-            </tr>
-END
-            $discussion .= &action_links_bar($colspan,$ressymb,$visible,
-                                             $newpostsflag,$group,
+            $discussion .= '</div></div></div>';
+            $discussion .= &action_links_bar($ressymb,$visible,$newpostsflag,$group,
                                              $prevread,$markondisp,$seehidden);
-            $discussion .= "</table></form>\n";
+            $discussion .= "</div></form>\n";
         }
         if ($outputtarget eq 'export') {
             if ($manifestok) {
@@ -916,11 +946,12 @@
 }
 
 sub action_links_bar {
-    my ($colspan,$ressymb,$visible,$newpostsflag,$group,$prevread,$markondisp,
+    my ($ressymb,$visible,$newpostsflag,$group,$prevread,$markondisp,
         $seehidden) = @_;
-    my $discussion = '<tr><td colspan="'.$colspan.'">'.
-                     '<table width="100%"><tr>'.
-                     '<td class="LC_disc_action_left">';
+    my $discussion = '<div class="LC_grid_row" role="row" style="margin: 0;">'.
+                     '<div class="LC_grid_cell" role="gridcell" style="margin: 0;">'.
+                     '<div class="LC_split_text" style="width: 100%; margin: 0; line-height: 180%;">'.
+                     '<span>';
     my $escsymb=&escape($ressymb);
     if ($visible) {
         $discussion .= '<a href="/adm/feedback?cmd=threadedon&symb='.$escsymb;
@@ -959,23 +990,19 @@
         $discussion .= &group_args($group);
         $discussion .= '">'.&mt('Undelete all deleted entries').'</a>';
     }
-    $discussion.='</td>';
+    $discussion.='</span>';
     if ($newpostsflag) {
         if (!$markondisp) {
-            $discussion .='<td class="LC_disc_action_right"><a href="/adm/preferences?action=changediscussions';
+            $discussion .='<span><a href="/adm/preferences?action=changediscussions';
             $discussion .= &group_args($group);
             $discussion .= '">'.
                            &mt('My general preferences on what is marked as NEW').
                            '</a><br /><a href="/adm/feedback?markread=1&symb='.$escsymb;
             $discussion .= &group_args($group);
-            $discussion .= '">'.&mt('Mark NEW posts no longer new').'</a></td>';
-        } else {
-            $discussion .= '<td> </td>';
+            $discussion .= '">'.&mt('Mark NEW posts no longer new').'</a></span>';
         }
-    } else {
-        $discussion .= '<td> </td>';
     }
-    $discussion .= '</tr></table></td></tr>';
+    $discussion .= '</div></div></div>';
     return $discussion;
 }
 
@@ -1300,7 +1327,7 @@
                             }
                             $sender = '<b>'.$sender.'</b>';
 			    if ($contrib{$idx.':anonymous'}) {
-			        $sender.=' <font color="red"><b>['.$$anonhash{$key}.']</b></font> '.
+			        $sender.=' <span class="LC_info"><b>['.$$anonhash{$key}.']</b></span> '.
 				    $screenname;
 			    }
 			    if ($see_anonymous) {
@@ -1515,7 +1542,7 @@
                         unless ($$notshown{$idx} == 1) {
                             if ($prevread > 0 && $prevread <= $posttime) {
                                 $$newitem{$idx} = 1;
-                                $$discussionitems[$idx] .= '<font color="#FF0000"><b>'.&mt('NEW').'  </b></font>';
+                                $$discussionitems[$idx] .= '<span class="LC_info"><b>'.&mt('NEW').'  </b></span>';
                             } else {
                                 $$newitem{$idx} = 0;
                             }
@@ -1593,7 +1620,7 @@
                             if ($contrib{$idx.':history'}) {
                                 my @postversions = ();
                                 $$discussionitems[$idx] .= '  '.&mt('This post has been edited by the author.');
-                                if ($seehidden) {
+                                if (($seehidden) && ($outputtarget ne 'tex')) {
                                     $$discussionitems[$idx] .= '  '.
                                          &discussion_link($ressymb,&mt('Display all versions'),'allversions',$idx,$$newpostsflag,$prevread,&group_args($group));
                                 }
@@ -1607,6 +1634,7 @@
                                     my $version = $i+1;
                                     $$discussionitems[$idx] .= '<b>'.$version.'.</b> - '.&Apache::lonlocal::locallocaltime($postversions[$i]).'  ';
                                 }
+                                $$discussionitems[$idx].='<br/>';
                             }
 # end of unless ($$notshown ...)
                         }
@@ -2176,6 +2204,7 @@
         'curr' => 'Current setting ',
         'actn' => 'Action',
         'deff' => 'Default for all discussions',
+        'sdpf' => 'Set display preferences for this discussion',
         'prca' => 'Preferences can be set for this discussion that determine ....',
         'whpo' => 'Which posts are displayed when you display this discussion board or resource, and',
         'unwh' => 'Under what circumstances posts are identified as "NEW", and',
@@ -2307,13 +2336,15 @@
 
 
     my $start_page =
-	&Apache::loncommon::start_page('Discussion display options',$js);
+	&Apache::loncommon::start_page('Discussion display options',$js)."\n".
+        '<div class="LC_landmark" role="main" id="LC_main_content">'."\n";
     my $end_page =
-	&Apache::loncommon::end_page();
+	'</div>'.&Apache::loncommon::end_page();
     $r->print(<<END);
 $start_page
 <form name="modifydisp" method="post" action="/adm/feedback">
-$lt{'sdpf'}<br/> $lt{'prca'}  <ol><li>$lt{'whpo'}</li><li>$lt{'unwh'}</li><li>$lt{'wipa'}</li></ol>
+<h1 class="LC_heading_3">$lt{'sdpf'}</h1>
+$lt{'prca'}  <ol><li>$lt{'whpo'}</li><li>$lt{'unwh'}</li><li>$lt{'wipa'}</li></ol>
 <br />
 END
     $r->print(&Apache::loncommon::start_data_table());
@@ -2328,9 +2359,13 @@
     $r->print(<<END);
        <td>$lt{'disa'}</td>
        <td>$lt{$discdisp}</td>
-       <td><label><input type="checkbox" name="discdisp" onclick="discdispChk('0')" /> $lt{'chgt'} "$dispchangeA"</label>
+       <td>
+       <fieldset class="LC_borderless" style="line-height: 185%;">
+       <legend class="LC_visually_hidden">$lt{'disa'}</legend>
+           <label><input type="checkbox" name="discdisp" onclick="discdispChk('0')" /> $lt{'chgt'} "$dispchangeA"</label>
            <br />
            <label><input type="checkbox" name="discdisp" onclick="discdispChk('1')" /> $lt{'chgt'} "$dispchangeB"</label>
+       </fieldset>
        </td>
 END
     $r->print(&Apache::loncommon::end_data_table_row());
@@ -2448,11 +2483,11 @@
         'actn' => 'Action',
         'prca' => 'Set options that control the sort order of posts, and/or which posts are displayed.',
         'soor' => 'Sort order',
-        'spur' => 'Specific user roles',
+        'spur' => 'Specific roles',
         'sprs' => 'Specific role status',
         'spse' => 'Specific sections',
         'spgr' => 'Specific groups',
-        'psub' => 'Pick specific users (by name)',
+        'psub' => 'Specific users',
         'shal' => 'Show a list of current posters',
         'stor' => 'Save changes',
     );
@@ -2501,32 +2536,28 @@
 END
 
     my $start_page=
-	&Apache::loncommon::start_page('Discussion options',$js);
+	&Apache::loncommon::start_page('Discussion options',$js,{no_main_skip => 1});
     my $end_page=
 	&Apache::loncommon::end_page();
 
     $r->print(<<END);
 $start_page
+<div class="LC_landmark" role="contentinfo">
 <form name="modifyshown" method="post" action="/adm/feedback">
-<b>$lt{'diso'}</b><br/> $lt{'prca'}
+<h1 class="LC_heading_3">$lt{'diop'}</h1>$lt{'prca'}
 <br />
-<table border="0">
+<table border="0" style="border-collapse: separate; border-spacing: 10px; 0;">
  <tr>
-  <th>$lt{'soor'}</th>
-  <th> </th>
-  <th>$lt{'sprs'}</th>
-  <th> </th>
-  <th>$lt{'spur'}</th>
-  <th> </th>
-  <th>$lt{'spse'}</th>
-  <th> </th>
-  <th>$lt{'spgr'}</th>
-  <th> </th>
+  <th><label for="sortposts">$lt{'soor'}</th>
+  <th><label for="statusfilter">$lt{'sprs'}</th>
+  <th><label for="rolefilter">$lt{'spur'}</th>
+  <th><label for="sectionpick">$lt{'spse'}</th>
+  <th><label for="grouppick">$lt{'spgr'}</label></th>
   <th>$lt{'psub'}</th>
  </tr>
  <tr>
   <td align="center" valign="top">
-   <select name="sortposts">
+   <select name="sortposts" id="sortposts">
     <option value="ascdate" selected="selected">$sort_types{'ascdate'}</option>
     <option value="descdate">$sort_types{'descdate'}</option>
     <option value="thread">$sort_types{'thread'}</option>
@@ -2535,18 +2566,16 @@
     <option value="lastfirst">$sort_types{'lastfirst'}</option>
    </select>
   </td>
-  <td> </td>
   <td align="center" valign="top">
-   <select name="statusfilter">
+   <select name="statusfilter" id="statusfilter">
     <option value="all" selected="selected">$status_types{'all'}</option>
     <option value="Active">$status_types{'Active'}</option>
     <option value="Expired">$status_types{'Expired'}</option>
     <option value="Future">$status_types{'Future'}</option>
    </select>
   </td>
-  <td> </td>
   <td align="center" valign="top">
-   <select name="rolefilter" multiple="multiple" size="5">
+   <select name="rolefilter" id="rolefilter" multiple="multiple" size="5">
     <option value="all">$role_types{'all'}</option>
     <option value="st">$role_types{'st'}</option>
     <option value="$ccrole">$role_types{$ccrole}</option>
@@ -2557,19 +2586,16 @@
     <option value="cr">$role_types{'cr'}</option>
    </select>
   </td>
-  <td> </td>
   <td align="center" valign="top">
-   <select name="sectionpick" multiple="multiple" size="$numvisible">
+   <select name="sectionpick" id="sectionpick" multiple="multiple" size="$numvisible">
     $section_sel
    </select>
   </td>
-  <td> </td>
   <td align="center" valign="top">
-   <select name="grouppick" multiple="multiple" size="$numvisible">
+   <select name="grouppick" id="grouppick" multiple="multiple" size="$numvisible">
     $group_sel
    </select>
   </td>
-  <td> </td>
   <td valign="top"><label><input type="checkbox" name="posterlist" value="$symb" />$lt{'shal'}</label></td>
  </tr>
 </table>
@@ -2590,6 +2616,7 @@
 <br />
 <br />
 </form>
+</div>
 $end_page
 ");
 }
@@ -3688,6 +3715,8 @@
 
     my %lt = &Apache::lonlocal::texthash(
                'subj' => 'Subject',
+               'atta' => 'Attachments',
+               'rema' => 'Remove attachments',
                'chth' => 'Check the checkboxes for any you wish to remove.',
                'thef' => 'The following attachments have been uploaded for inclusion with this posting.',
                'adda' => 'Add a new attachment to this post',
@@ -3742,7 +3771,7 @@
 <h1 class="LC_heading_1">$lt{'clic'}</h1>
 END
     $r->print(&Apache::lonhtmlcommon::start_pick_box());
-    $r->print(&Apache::lonhtmlcommon::row_title(&mt('Subject')));
+    $r->print(&Apache::lonhtmlcommon::row_title($lt{'subj'}));
     $r->print('<b>'.$subject.'</b>');
     $r->print(&Apache::lonhtmlcommon::row_closure());
     $r->print(&Apache::lonhtmlcommon::row_title('<label for="addnewattach">'.$lt{'adda'}.'</label>'));
@@ -3752,12 +3781,14 @@
         .'onclick="this.form.submit()" />  '.$attachmaxtext);
     if(($idx)||(ref($currnewattach) eq 'ARRAY') && (@{$currnewattach} > 0)){
         $r->print(&Apache::lonhtmlcommon::row_closure());
-        $r->print(&Apache::lonhtmlcommon::row_title(&mt('Attachments')));
+        $r->print(&Apache::lonhtmlcommon::row_title($lt{'atta'}).
+                  '<fieldset class="LC_borderless"><legend class="LC_visually_hidden">'.
+                  $lt{'rema'}.'</legend>');
         if ($idx) {
             if ($attachmenturls) {
                 my @currold = keys(%currattach);
                 if (@currold > 0) {
-                    $r->print($lt{'thfo'}.'<br />'.$lt{'chth'}.'<br />'."\n");
+                    $r->print($lt{'thef'}.'<br />'.$lt{'chth'}.'<br />'."\n");
                     foreach my $id (@currold) {
                         my $attachurl = &HTML::Entities::decode($attachments{$id}{'filename'});
                         $attachurl =~ m#/([^/]+)$#;
@@ -3768,12 +3799,15 @@
             }
         }
         if ((ref($currnewattach) eq 'ARRAY') && (@{$currnewattach} > 0)) {
-            $r->print($lt{'chth'}.'<br />'."\n");
+            $r->print($lt{'chth'}."\n");
             foreach my $attach (@{$currnewattach}) {
                 $attach =~ m#/([^/]+)$#;
-                $r->print('<label><input type="checkbox" name="delnewattach" value="'.$attach.'" /> '.$1.'</label><br />'."\n");
+                $r->print('<div class="LC_touch_target">'.
+                          '<label><input type="checkbox" name="delnewattach" value="'.$attach.'" />'.
+                          ' '.$1.'</label></div>'."\n");
             }
         }
+        $r->print('</fieldset>');
     }
     $r->print(&Apache::lonhtmlcommon::row_closure(1));
     $r->print(&Apache::lonhtmlcommon::end_pick_box());
@@ -3913,7 +3947,7 @@
         &Apache::lonnet::allowuploaded('/adm/feedback',
                                $attachurl);
     } elsif (@attached > 1) {
-        $$message.='<ol>';
+        $$message.='<ol style="line-height: 170%;">';
         foreach my $attach (@attached) {
             my $id = $attach;
             my $attachurl = &HTML::Entities::decode($$attachments{$id}{'filename'});
Index: loncom/interface/loncommon.pm
diff -u loncom/interface/loncommon.pm:1.1529 loncom/interface/loncommon.pm:1.1530
--- loncom/interface/loncommon.pm:1.1529	Fri Jun 19 13:19:46 2026
+++ loncom/interface/loncommon.pm	Sun Jun 21 17:18:12 2026
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # a pile of common routines
 #
-# $Id: loncommon.pm,v 1.1529 2026/06/19 13:19:46 raeburn Exp $
+# $Id: loncommon.pm,v 1.1530 2026/06/21 17:18:12 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -7750,6 +7750,12 @@
   color: #525252;
 }
 
+.LC_split_text {
+  background: $sidebg;
+  display: flex;
+  justify-content: space-between;
+}
+
 .LC_discussion {
   background: $data_table_dark;
   border: 1px solid black;


More information about the LON-CAPA-cvs mailing list