diff --git a/lib/Value/AnswerChecker.pm b/lib/Value/AnswerChecker.pm index bc2b15719..34d1ff372 100644 --- a/lib/Value/AnswerChecker.pm +++ b/lib/Value/AnswerChecker.pm @@ -441,21 +441,17 @@ sub ans_matrix { my $named_ans_rule = pgRef('NAMED_ANS_RULE'); my $HTML = ""; pgCall('RECORD_IMPLICIT_ANS_NAME', $name = pgCall('NEW_ANS_NAME')) unless $name; - my $ename = "${answerPrefix}_${name}"; + my $ename = "${answerPrefix}_${name}"; + my $base_label = pgCall('generate_aria_label', $ename, $options{aria_label}); $self->{ans_name} = $ename; $self->{ans_rows} = $rows; $self->{ans_cols} = $cols; # warn "ans_matrix: ename=$ename answer_group_name=$options{answer_group_name}"; my @array = (); - foreach my $i (0 .. $rows - 1) { + for my $i (0 .. $rows - 1) { my @row = (); - foreach my $j (0 .. $cols - 1) { - my $label; - if ($options{aria_label}) { - $label = $options{aria_label} . pgCall('maketext', 'row [_1] col [_2] ', $i + 1, $j + 1); - } else { - $label = pgCall('generate_aria_label', ANS_NAME($ename, $i, $j)); - } + for my $j (0 .. $cols - 1) { + my $label = $base_label . pgCall('maketext', 'row [_1] col [_2] ', $i + 1, $j + 1); my $answer_group_name = $options{answer_group_name} // $name; if ($i == 0 && $j == 0) { if ($extend) { diff --git a/macros/core/PGML.pl b/macros/core/PGML.pl index 9fe50219d..4d9abc927 100644 --- a/macros/core/PGML.pl +++ b/macros/core/PGML.pl @@ -404,7 +404,7 @@ sub Image { sub Answer { my $self = shift; my $token = shift; - my $def = { options => [ "answer", "width", "name", "cmp_options" ] }; + my $def = { options => [ 'answer', 'width', 'name', 'cmp_options', 'aria_label' ] }; $def->{hasStar} = 1 if $token =~ m/\*$/; $self->Item("answer", $token, $def); } @@ -1285,6 +1285,11 @@ sub string { return join('', @strings); } +sub aria_label { + my ($self, $label) = @_; + return $label ? (aria_label => "$label ") : (); +} + sub nl { my $self = shift; my $nl = $self->{nl}; @@ -1373,10 +1378,10 @@ sub Answer { if (defined($ans)) { if (ref($ans) eq 'CODE' || (ref($ans) eq 'AnswerEvaluator' && !Value::isValue($ans->{rh_ans}{correct_value}))) { if (defined($item->{name})) { - $rule = main::NAMED_ANS_RULE($item->{name}, $item->{width}); + $rule = main::NAMED_ANS_RULE($item->{name}, $item->{width}, $self->aria_label($item->{aria_label})); main::NAMED_ANS($item->{name} => $ans); } else { - $rule = main::ans_rule($item->{width}); + $rule = main::ans_rule($item->{width}, $self->aria_label($item->{aria_label})); main::ANS($ans); } } else { @@ -1398,7 +1403,7 @@ sub Answer { $ans = main::String(""); ### use something else? } } - my @options = ($item->{width}); + my @options = ($item->{width}, $self->aria_label($item->{aria_label})); my $method = ($item->{hasStar} ? "ans_array" : "ans_rule"); if ($item->{name}) { unshift(@options, $item->{name}); @@ -1426,9 +1431,9 @@ sub Answer { } } else { if (defined($item->{name})) { - $rule = main::NAMED_ANS_RULE($item->{name}, $item->{width}); + $rule = main::NAMED_ANS_RULE($item->{name}, $item->{width}, $self->aria_label($item->{aria_label})); } else { - $rule = main::ans_rule($item->{width}); + $rule = main::ans_rule($item->{width}, $self->aria_label($item->{aria_label})); } } return $rule; diff --git a/macros/core/PGbasicmacros.pl b/macros/core/PGbasicmacros.pl index 2b204e7f4..2198d250c 100644 --- a/macros/core/PGbasicmacros.pl +++ b/macros/core/PGbasicmacros.pl @@ -309,7 +309,7 @@ sub NAMED_ANS_RULE { size => $col, name => $name, id => $name, - aria_label => $options{aria_label} // generate_aria_label($name), + aria_label => generate_aria_label($name, $options{aria_label}), dir => 'auto', autocomplete => 'off', autocapitalize => 'off', @@ -385,7 +385,7 @@ sub NAMED_ANS_RULE_EXTENSION { size => $col, name => $name, id => $name, - aria_label => $options{aria_label} // generate_aria_label($name), + aria_label => generate_aria_label($name, $options{aria_label}), dir => 'auto', autocomplete => 'off', autocapitalize => 'off', @@ -402,7 +402,7 @@ sub ANS_RULE { my ($number, $col) = @_; my $name = NEW_ANS_NAME(); RECORD_IMPLICIT_ANS_NAME($name); - return NAMED_ANS_RULE($name, $col); + return NAMED_ANS_RULE($name, $col, @_); } sub NAMED_ANS_BOX { @@ -413,7 +413,6 @@ sub NAMED_ANS_BOX { my $height = .07 * $row; my $answer_value = $inputs_ref->{$name} // ''; $name = RECORD_ANS_NAME($name, $answer_value); - my $label = $options{aria_label} // generate_aria_label($name); return MODES( TeX => qq!\\vskip $height in \\hrulefill\\quad !, @@ -426,7 +425,7 @@ sub NAMED_ANS_BOX { id => $name, rows => $row, cols => $col, - aria_label => $label, + aria_label => generate_aria_label($name, $options{aria_label}), encode_pg_and_html($answer_value) ) ) @@ -454,10 +453,11 @@ sub NAMED_ANS_RADIO { 'label', tag( 'input', - type => 'radio', - name => $name, - id => $name, - value => $value, + type => 'radio', + name => $name, + id => $name, + value => $value, + aria_label => generate_aria_label($name, $options{aria_label}), $checked ? (checked => undef) : (), %{ $options{attributes} } ) @@ -485,10 +485,11 @@ sub NAMED_ANS_RADIO_EXTENSION { 'label', tag( 'input', - type => 'radio', - name => $name, - id => $options{id} // "${name}_$value", - value => $value, + type => 'radio', + name => $name, + id => $options{id} // "${name}_$value", + value => $value, + aria_label => generate_aria_label($name, $options{aria_label}), $checked ? (checked => undef) : (), %{ $options{attributes} } ) @@ -508,51 +509,56 @@ sub NAMED_ANS_RADIO_BUTTONS { while (@buttons) { $value = shift @buttons; $tag = shift @buttons; - push(@out, NAMED_ANS_RADIO_EXTENSION($name, $value, $tag, aria_label => $label . "option $count ")); + push(@out, + NAMED_ANS_RADIO_EXTENSION($name, $value, $tag, aria_label => $label . maketext('option [_1] ', $count)) + ); $count++; } return wantarray ? @out : join(" ", @out); } ############################################## -# generate_aria_label( $name ) -# takes the name of an ANS_RULE and generates an appropriate -# aria label for screen readers +# generate_aria_label($name, $inLabel) +# Takes the name of an ANS_RULE and generates an appropriate aria label for screen readers. ############################################## sub generate_aria_label { - my $name = shift; - my $label = ''; + my ($name, $inLabel) = @_; + my $outLabel = ''; - # if we dont have an AnSwEr type name then we do the best we can + # Return the input label unless it contains '%s'. + if ($inLabel) { + if ($inLabel =~ /^\S$/) { + $inLabel = ''; + } else { + $inLabel = "$inLabel " unless $inLabel =~ / $/; + return $inLabel unless $inLabel =~ /%s/; + } + } + + # If we don't have an AnSwEr type name then we do the best we can. if ($name !~ /AnSwEr\d+/) { - return maketext('answer [_1] ', $name); + $outLabel = maketext('answer [_1] ', $name); + return $inLabel ? $inLabel =~ s/%s/$outLabel/r : $outLabel; } - # check for quiz prefix + # Check for quiz prefix. if ($name =~ /^Q\d+/ || $name =~ /^MaTrIx_Q\d+/) { $name =~ s/Q0*(\d+)_//; - $label .= maketext('problem [_1] ', $1); + $outLabel .= maketext('problem [_1] ', $1); } - # get answer number + # Get answer number. $name =~ /AnSwEr0*(\d+)/; - $label .= maketext('answer [_1] ', $1); + $outLabel .= maketext('answer [_1] ', $1); - # check for Multianswer + # Check for Multianswer. if ($name =~ /MuLtIaNsWeR_/) { - $name =~ s/MuLtIaNsWeR_//; $name =~ /AnSwEr(\d+)_(\d+)/; - $label .= maketext('part [_1] ', $2 + 1); + $outLabel .= maketext('part [_1] ', $2 + 1); } - # check for Matrix - if ($name =~ /^MaTrIx_/) { - $name =~ /_(\d+)_(\d+)$/; - $label .= maketext('row [_1] column [_2] ', $1 + 1, $2 + 1); - } - - return $label; + return $inLabel ? $inLabel =~ s/%s/$outLabel/r : $outLabel; } ############################################## @@ -609,7 +615,7 @@ sub NAMED_ANS_CHECKBOX { type => 'checkbox', name => $name, id => $name, - aria_label => $options{aria_label} // (generate_aria_label($name) . maketext('option [_1] ', 1)), + aria_label => generate_aria_label($name, $options{aria_label}), value => $value, $checked ? (checked => undef) : (), %{ $options{attributes} } @@ -640,8 +646,8 @@ sub NAMED_ANS_CHECKBOX_OPTION { 'input', type => 'checkbox', name => $name, - id => $options{id} // "${name}_$value", - aria_label => $options{aria_label} // generate_aria_label($name), + id => $options{id} // "${name}_$value", + aria_label => generate_aria_label($name, $options{aria_label}), value => $value, $checked ? (checked => undef) : (), %{ $options{attributes} } @@ -675,7 +681,7 @@ sub ans_rule { my $len = shift; my $name = NEW_ANS_NAME(); RECORD_IMPLICIT_ANS_NAME($name); - return NAMED_ANS_RULE($name, $len || 20); + return NAMED_ANS_RULE($name, $len || 20, @_); } sub ans_radio_buttons { @@ -832,8 +838,6 @@ sub NAMED_ANS_ARRAY_EXTENSION { $answer_value = '' unless defined($answer_value); } - my $label = $options{aria_label} // generate_aria_label($name); - # the name of the answer evaluator controlling this collection of responses. my $answer_group_name; @@ -869,7 +873,7 @@ sub NAMED_ANS_ARRAY_EXTENSION { name => $name, id => $name, class => 'codeshard', - aria_label => $label, + aria_label => generate_aria_label($name, $options{aria_label}), autocomplete => 'off', autocapitalize => 'off', spellcheck => 'false', diff --git a/macros/core/PGessaymacros.pl b/macros/core/PGessaymacros.pl index 2dcecea5b..7d8090b73 100644 --- a/macros/core/PGessaymacros.pl +++ b/macros/core/PGessaymacros.pl @@ -119,7 +119,7 @@ sub NAMED_ESSAY_BOX { %html_options, name => $name, id => $name, - aria_label => generate_aria_label($name), + aria_label => generate_aria_label($name, $html_options{aria_label}), rows => $row, cols => $col, class => 'latexentryfield', diff --git a/macros/parsers/parserCheckboxList.pl b/macros/parsers/parserCheckboxList.pl index 9a6c86c4e..6af06d4fd 100644 --- a/macros/parsers/parserCheckboxList.pl +++ b/macros/parsers/parserCheckboxList.pl @@ -541,7 +541,7 @@ sub CHECKS { my @checks; main::RECORD_IMPLICIT_ANS_NAME($name = main::NEW_ANS_NAME()) unless $name; - my $label = (delete $options{aria_label}) // main::generate_aria_label($name); + my $label = main::generate_aria_label($name, delete $options{aria_label}); for my $i (0 .. $#{ $self->{orderedChoices} }) { my $value = $self->{values}[$i]; diff --git a/macros/parsers/parserMultiAnswer.pl b/macros/parsers/parserMultiAnswer.pl index ab8c1ed3a..a32fad0b0 100644 --- a/macros/parsers/parserMultiAnswer.pl +++ b/macros/parsers/parserMultiAnswer.pl @@ -422,26 +422,26 @@ sub NEW_NAME { # by the settings of the MultiAnswer. # sub ans_rule { - my $self = shift; - my $size = shift || 20; + my ($self, $size, %options) = @_; + $size ||= 20; my $data = $self->{data}[ $self->{part} ]; my $name = $self->ANS_NAME($self->{part}++); if ($self->{singleResult} && $self->{part} == 1) { - my $label = main::generate_aria_label($answerPrefix . $name . "_0"); main::RECORD_IMPLICIT_ANS_NAME($name) unless $self->{namedRules}; - return $data->named_ans_rule($name, $size, @_, aria_label => $label); + return $data->named_ans_rule($name, $size, %options, + aria_label => main::generate_aria_label($answerPrefix . $name . '_0', $options{aria_label})); } if ($self->{singleResult} && $self->{part} > 1) { my $extension_ans_rule = $data->named_ans_rule_extension( $name, $size, answer_group_name => $self->{answerNames}{0}, - @_ + %options ); # warn "extension rule created: $extension_ans_rule for ", ref($data); return $extension_ans_rule; } else { main::RECORD_IMPLICIT_ANS_NAME($name) unless $self->{namedRules}; - return $data->named_ans_rule($name, $size, @_); + return $data->named_ans_rule($name, $size, %options); } } @@ -451,30 +451,29 @@ sub ans_rule { # Reset the correct_ans once the array is made # sub ans_array { - my $self = shift; - my $size = shift || 5; + my ($self, $size, %options) = @_; + $size ||= 5; my $HTML; my $data = $self->{data}[ $self->{part} ]; my $name = $self->ANS_NAME($self->{part}++); if ($self->{singleResult} && $self->{part} == 1) { - my $label = main::generate_aria_label($answerPrefix . $name . "_0"); main::RECORD_IMPLICIT_ANS_NAME($name) unless $self->{namedRules}; return $data->named_ans_array( $name, $size, answer_group_name => $self->{answerNames}{0}, - @_, aria_label => $label + %options, aria_label => main::generate_aria_label($answerPrefix . $name . '_0', $options{aria_label}) ); } if ($self->{singleResult} && $self->{part} > 1) { $HTML = $data->named_ans_array_extension( $self->NEW_NAME($name), $size, answer_group_name => $self->{answerNames}{0}, - @_ + %options ); # warn "array extension rule created: $HTML for ", ref($data); } else { main::RECORD_IMPLICIT_ANS_NAME($name) unless $self->{namedRules}; - $HTML = $data->named_ans_array($name, $size, @_); + $HTML = $data->named_ans_array($name, $size, %options); } $self->{cmp}[ $self->{part} - 1 ] = $data->cmp(@ans_defaults); return $HTML; diff --git a/macros/parsers/parserPopUp.pl b/macros/parsers/parserPopUp.pl index 5c69869ab..7ecd14a2a 100644 --- a/macros/parsers/parserPopUp.pl +++ b/macros/parsers/parserPopUp.pl @@ -405,7 +405,7 @@ sub MENU { my $menu = ""; main::RECORD_IMPLICIT_ANS_NAME($name = main::NEW_ANS_NAME()) unless $name; my $answer_value = (defined($main::inputs_ref->{$name}) ? $main::inputs_ref->{$name} : ''); - my $aria_label = $options{aria_label} // main::generate_aria_label($name); + my $aria_label = main::generate_aria_label($name, $options{aria_label}); if ($main::displayMode =~ m/^HTML/) { if ($self->{useHTMLSelect}) { @@ -457,8 +457,8 @@ sub MENU { data_feedback_insert_method => 'append_content', join( '', - main::tag('input', type => 'hidden', name => $name, value => $answer_value), - main::tag('span', class => 'visually-hidden', $aria_label), + main::tag('input', type => 'hidden', name => $name, value => $answer_value), + main::tag('span', class => 'visually-hidden', aria_label => $aria_label), main::tag( 'button', class => 'btn dropdown-toggle text-nowrap ', diff --git a/macros/parsers/parserRadioButtons.pl b/macros/parsers/parserRadioButtons.pl index 83551ad62..3f40a692c 100644 --- a/macros/parsers/parserRadioButtons.pl +++ b/macros/parsers/parserRadioButtons.pl @@ -608,17 +608,16 @@ sub Index { # Create the radio-buttons text # sub BUTTONS { - my $self = shift; - my $extend = shift; - my $name = shift; - my $size = shift; + my ($self, $extend, $name, $size, %options) = @_; my @choices = @{ $self->{orderedChoices} }; my @radio = (); main::RECORD_IMPLICIT_ANS_NAME($name = main::NEW_ANS_NAME()) unless $name; - foreach my $i (0 .. $#choices) { - my $value = $self->{values}[$i]; - my $tag = $choices[$i]; + my $label = main::generate_aria_label($name, delete $options{aria_label}); + for my $i (0 .. $#choices) { + my $value = $self->{values}[$i]; + my $tag = $choices[$i]; + my $aria_label = $label . main::maketext('option [_1] ', $i + 1); $value = "%" . $value if $i == $self->{checkedI}; $tag = $self->labelFormat($self->{labels}[$i]) . $tag if $self->{displayLabels}; if ($i > 0) { @@ -626,7 +625,8 @@ sub BUTTONS { @radio, main::NAMED_ANS_RADIO_EXTENSION( $name, $value, $tag, - id => "${name}_$i", + id => "${name}_$i", + aria_label => $aria_label, $self->{uncheckable} ? ( attributes => { @@ -635,7 +635,7 @@ sub BUTTONS { } ) : (), - @_ + %options ) ); } else { @@ -643,6 +643,7 @@ sub BUTTONS { @radio, main::NAMED_ANS_RADIO( $name, $value, $tag, $extend, + aria_label => $aria_label, $self->{uncheckable} ? ( attributes => { @@ -651,7 +652,7 @@ sub BUTTONS { } ) : (), - @_ + %options ) ); } diff --git a/macros/parsers/parserRadioMultiAnswer.pl b/macros/parsers/parserRadioMultiAnswer.pl index ae9d4b296..1b1b73940 100644 --- a/macros/parsers/parserRadioMultiAnswer.pl +++ b/macros/parsers/parserRadioMultiAnswer.pl @@ -227,6 +227,15 @@ =head1 OPTIONS Specifies whether labels should be displayed after the radio button and before its text. This makes the association between the choices and the label used as an answer more explicit. +=item ariaLabels (Default: ariaLabels => []) + +An array reference which is a list of accessibility labels to add to the answer blanks. The +labels match the order of the answers. If the corresponding label is undefined, the default +accessibility label is of the form "answer X part Y subpart Z" (Y is the label if C +is 1, other wise the number of the radio button, and subpart Z is only shown if there are more +than one subparts). A single C<%s> can be used to include the default accessibility label as part +of the custom accessibility label, e.g. "%s custom label". + =item checked (Default: checked => undef) The index (starting at zero) of the radio button to be checked initially. By default this is @@ -290,6 +299,7 @@ sub new { values => [], namedRules => 0, cmpOpts => undef, + ariaLabels => [], checkTypes => 1, allowBlankAnswers => 0, tex_separator => ';\,', @@ -632,36 +642,41 @@ sub label { } sub generate_aria_label { - my ($name, $radioIndex, $partIndex) = @_; - my $label = ''; + my ($self, $name, $radioIndex, $partIndex, $inLabel) = @_; + my $outLabel = ''; + + # Return the input label unless it contains '%s'. + if ($inLabel) { + if ($inLabel =~ /^\S$/) { + $inLabel = ''; + } else { + $inLabel = "$inLabel " unless $inLabel =~ / $/; + return $inLabel unless $inLabel =~ /%s/; + } + } $name =~ s/$answerPrefix//; # Check for the quiz prefix. if ($name =~ /^Q\d+/ || $name =~ /^MaTrIx_Q\d+/) { $name =~ s/Q0*(\d+)_//; - $label .= main::maketext('problem [_1] ', $1); + $outLabel .= main::maketext('problem [_1] ', $1); } # Get the answer number. + my $partLabel = $self->{displayLabels} ? $self->label($radioIndex - 1) : $radioIndex; $name =~ /AnSwEr0*(\d+)/; - $label .= main::maketext('answer [_1] ', $1); + $outLabel .= + $partIndex + ? main::maketext('answer [_1] part [_2] subpart [_3] ', $1, $partLabel, $partIndex) + : main::maketext('answer [_1] part [_2] ', $1, $partLabel); - $label .= main::maketext('part [_1] ', $radioIndex); - $label .= main::maketext('subpart [_1] ', $partIndex); - - # Check for a Matrix answer. - if ($name =~ /^MaTrIx_/) { - $name =~ /_(\d+)_(\d+)$/; - $label .= main::maketext('row [_1] column [_2] ', $1 + 1, $2 + 1); - } - - return $label; + return $inLabel ? $inLabel =~ s/%s/$outLabel/r : $outLabel; } # Produce the answer rule. sub ans_rule { - my ($self, $size, @options) = @_; + my ($self, $size, %options) = @_; $size ||= 20; my @data = @{ $self->{data} }; @@ -676,8 +691,14 @@ sub ans_rule { my @part_rules; my @part_names; my @positions = $data[$i][0] =~ /(%s\*?)/g; - for (1 .. $#{ $data[$i] }) { - my $name = $self->ANS_NAME($part++); + my $subparts = $#{ $data[$i] }; + for (1 .. $subparts) { + my $name = $self->ANS_NAME($part++); + my $aria_label = $self->generate_aria_label( + $name, $i + 1, + $subparts == 1 ? 0 : $_, + $self->{ariaLabels}[ $part - 2 ] || $options{aria_label} + ); if ($positions[ $_ - 1 ] eq '%s*') { push( @part_rules, @@ -688,8 +709,8 @@ sub ans_rule { ? (defined $size->[$i][ $_ - 1 ] ? $size->[$i][ $_ - 1 ] : 20) : $size, answer_group_name => $radio_name, - aria_label => generate_aria_label($name, $i + 1, $_), - @options + %options, + aria_label => $aria_label, ) ) ); @@ -704,8 +725,8 @@ sub ans_rule { ? (defined $size->[$i][ $_ - 1 ] ? $size->[$i][ $_ - 1 ] : 20) : $size, answer_group_name => $radio_name, - aria_label => generate_aria_label($name, $i + 1, $_), - @options + %options, + aria_label => $aria_label, ) ) ); diff --git a/macros/parsers/parserWordCompletion.pl b/macros/parsers/parserWordCompletion.pl index 53c88fdb4..4ccac0df5 100644 --- a/macros/parsers/parserWordCompletion.pl +++ b/macros/parsers/parserWordCompletion.pl @@ -86,7 +86,7 @@ sub menu { name => $name, id => $name, list => "$name-list", - aria_label => $options{aria_label} // main::generate_aria_label($name), + aria_label => main::generate_aria_label($name, $options{aria_label}), dir => 'auto', autocomplete => 'off', autocapitalize => 'off',