14
14
* limitations under the License.
15
15
*/
16
16
17
- import { closestCrossShadow , enclosingShadowRootOrDocument , getElementComputedStyle , isElementStyleVisibilityVisible , isVisibleTextNode , parentElementOrShadowHost } from './domUtils' ;
17
+ import { closestCrossShadow , elementSafeTagName , enclosingShadowRootOrDocument , getElementComputedStyle , isElementStyleVisibilityVisible , isVisibleTextNode , parentElementOrShadowHost } from './domUtils' ;
18
18
19
19
function hasExplicitAccessibleName ( e : Element ) {
20
20
return e . hasAttribute ( 'aria-label' ) || e . hasAttribute ( 'aria-labelledby' ) ;
@@ -70,7 +70,7 @@ function isFocusable(element: Element) {
70
70
}
71
71
72
72
function isNativelyFocusable ( element : Element ) {
73
- const tagName = element . tagName . toUpperCase ( ) ;
73
+ const tagName = elementSafeTagName ( element ) ;
74
74
if ( [ 'BUTTON' , 'DETAILS' , 'SELECT' , 'TEXTAREA' ] . includes ( tagName ) )
75
75
return true ;
76
76
if ( tagName === 'A' || tagName === 'AREA' )
@@ -124,7 +124,7 @@ const kImplicitRoleByTagName: { [tagName: string]: (e: Element) => string | null
124
124
if ( [ 'email' , 'tel' , 'text' , 'url' , '' ] . includes ( type ) ) {
125
125
// https://html.spec.whatwg.org/multipage/input.html#concept-input-list
126
126
const list = getIdRefs ( e , e . getAttribute ( 'list' ) ) [ 0 ] ;
127
- return ( list && list . tagName === 'DATALIST' ) ? 'combobox' : 'textbox' ;
127
+ return ( list && elementSafeTagName ( list ) === 'DATALIST' ) ? 'combobox' : 'textbox' ;
128
128
}
129
129
if ( type === 'hidden' )
130
130
return '' ;
@@ -201,17 +201,16 @@ const kPresentationInheritanceParents: { [tagName: string]: string[] } = {
201
201
} ;
202
202
203
203
function getImplicitAriaRole ( element : Element ) : string | null {
204
- // Elements from the svg namespace do not have uppercase tagName.
205
- const implicitRole = kImplicitRoleByTagName [ element . tagName . toUpperCase ( ) ] ?.( element ) || '' ;
204
+ const implicitRole = kImplicitRoleByTagName [ elementSafeTagName ( element ) ] ?.( element ) || '' ;
206
205
if ( ! implicitRole )
207
206
return null ;
208
207
// Inherit presentation role when required.
209
208
// https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none
210
209
let ancestor : Element | null = element ;
211
210
while ( ancestor ) {
212
211
const parent = parentElementOrShadowHost ( ancestor ) ;
213
- const parents = kPresentationInheritanceParents [ ancestor . tagName ] ;
214
- if ( ! parents || ! parent || ! parents . includes ( parent . tagName ) )
212
+ const parents = kPresentationInheritanceParents [ elementSafeTagName ( ancestor ) ] ;
213
+ if ( ! parents || ! parent || ! parents . includes ( elementSafeTagName ( parent ) ) )
215
214
break ;
216
215
const parentExplicitRole = getExplicitAriaRole ( parent ) ;
217
216
if ( ( parentExplicitRole === 'none' || parentExplicitRole === 'presentation' ) && ! hasPresentationConflictResolution ( parent , parentExplicitRole ) )
@@ -267,7 +266,7 @@ function getAriaBoolean(attr: string | null) {
267
266
// `Any descendants of elements that have the characteristic "Children Presentational: True"`
268
267
// https://www.w3.org/TR/wai-aria-1.2/#aria-hidden
269
268
export function isElementHiddenForAria ( element : Element ) : boolean {
270
- if ( [ 'STYLE' , 'SCRIPT' , 'NOSCRIPT' , 'TEMPLATE' ] . includes ( element . tagName ) )
269
+ if ( [ 'STYLE' , 'SCRIPT' , 'NOSCRIPT' , 'TEMPLATE' ] . includes ( elementSafeTagName ( element ) ) )
271
270
return true ;
272
271
const style = getElementComputedStyle ( element ) ;
273
272
const isSlot = element . nodeName === 'SLOT' ;
@@ -527,6 +526,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
527
526
}
528
527
529
528
const role = getAriaRole ( element ) || '' ;
529
+ const tagName = elementSafeTagName ( element ) ;
530
530
531
531
// step 2c:
532
532
// if the current node is a control embedded within the label (e.g. any element directly referenced by aria-labelledby) for another widget...
@@ -542,22 +542,22 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
542
542
if ( ! isOwnLabel && ! isOwnLabelledBy ) {
543
543
if ( role === 'textbox' ) {
544
544
options . visitedElements . add ( element ) ;
545
- if ( element . tagName === 'INPUT' || element . tagName === 'TEXTAREA' )
545
+ if ( tagName === 'INPUT' || tagName === 'TEXTAREA' )
546
546
return ( element as HTMLInputElement | HTMLTextAreaElement ) . value ;
547
547
return element . textContent || '' ;
548
548
}
549
549
if ( [ 'combobox' , 'listbox' ] . includes ( role ) ) {
550
550
options . visitedElements . add ( element ) ;
551
551
let selectedOptions : Element [ ] ;
552
- if ( element . tagName === 'SELECT' ) {
552
+ if ( tagName === 'SELECT' ) {
553
553
selectedOptions = [ ...( element as HTMLSelectElement ) . selectedOptions ] ;
554
554
if ( ! selectedOptions . length && ( element as HTMLSelectElement ) . options . length )
555
555
selectedOptions . push ( ( element as HTMLSelectElement ) . options [ 0 ] ) ;
556
556
} else {
557
557
const listbox = role === 'combobox' ? queryInAriaOwned ( element , '*' ) . find ( e => getAriaRole ( e ) === 'listbox' ) : element ;
558
558
selectedOptions = listbox ? queryInAriaOwned ( listbox , '[aria-selected="true"]' ) . filter ( e => getAriaRole ( e ) === 'option' ) : [ ] ;
559
559
}
560
- if ( ! selectedOptions . length && element . tagName === 'INPUT' ) {
560
+ if ( ! selectedOptions . length && tagName === 'INPUT' ) {
561
561
// SPEC DIFFERENCE:
562
562
// This fallback is not explicitly mentioned in the spec, but all browsers and
563
563
// wpt test name_heading-combobox-focusable-alternative-manual.html do this.
@@ -596,7 +596,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
596
596
// Spec says to ignore this when aria-labelledby is defined.
597
597
// WebKit follows the spec, while Chromium and Firefox do not.
598
598
// We align with Chromium and Firefox here.
599
- if ( element . tagName === 'INPUT' && [ 'button' , 'submit' , 'reset' ] . includes ( ( element as HTMLInputElement ) . type ) ) {
599
+ if ( tagName === 'INPUT' && [ 'button' , 'submit' , 'reset' ] . includes ( ( element as HTMLInputElement ) . type ) ) {
600
600
options . visitedElements . add ( element ) ;
601
601
const value = ( element as HTMLInputElement ) . value || '' ;
602
602
if ( trimFlatString ( value ) )
@@ -613,7 +613,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
613
613
//
614
614
// SPEC DIFFERENCE.
615
615
// Spec says to ignore this when aria-labelledby is defined, but all browsers take it into account.
616
- if ( element . tagName === 'INPUT' && ( element as HTMLInputElement ) . type === 'image' ) {
616
+ if ( tagName === 'INPUT' && ( element as HTMLInputElement ) . type === 'image' ) {
617
617
options . visitedElements . add ( element ) ;
618
618
const labels = ( element as HTMLInputElement ) . labels || [ ] ;
619
619
if ( labels . length && ! options . embeddedInLabelledBy )
@@ -630,7 +630,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
630
630
}
631
631
632
632
// https://w3c.github.io/html-aam/#button-element-accessible-name-computation
633
- if ( ! labelledBy && element . tagName === 'BUTTON' ) {
633
+ if ( ! labelledBy && tagName === 'BUTTON' ) {
634
634
options . visitedElements . add ( element ) ;
635
635
const labels = ( element as HTMLButtonElement ) . labels || [ ] ;
636
636
if ( labels . length )
@@ -639,7 +639,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
639
639
}
640
640
641
641
// https://w3c.github.io/html-aam/#output-element-accessible-name-computation
642
- if ( ! labelledBy && element . tagName === 'OUTPUT' ) {
642
+ if ( ! labelledBy && tagName === 'OUTPUT' ) {
643
643
options . visitedElements . add ( element ) ;
644
644
const labels = ( element as HTMLOutputElement ) . labels || [ ] ;
645
645
if ( labels . length )
@@ -652,13 +652,13 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
652
652
// For "other form elements", we count select and any other input.
653
653
//
654
654
// Note: WebKit does not follow the spec and uses placeholder when aria-labelledby is present.
655
- if ( ! labelledBy && ( element . tagName === 'TEXTAREA' || element . tagName === 'SELECT' || element . tagName === 'INPUT' ) ) {
655
+ if ( ! labelledBy && ( tagName === 'TEXTAREA' || tagName === 'SELECT' || tagName === 'INPUT' ) ) {
656
656
options . visitedElements . add ( element ) ;
657
657
const labels = ( element as ( HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement ) ) . labels || [ ] ;
658
658
if ( labels . length )
659
659
return getAccessibleNameFromAssociatedLabels ( labels , options ) ;
660
660
661
- const usePlaceholder = ( element . tagName === 'INPUT' && [ 'text' , 'password' , 'search' , 'tel' , 'email' , 'url' ] . includes ( ( element as HTMLInputElement ) . type ) ) || element . tagName === 'TEXTAREA' ;
661
+ const usePlaceholder = ( tagName === 'INPUT' && [ 'text' , 'password' , 'search' , 'tel' , 'email' , 'url' ] . includes ( ( element as HTMLInputElement ) . type ) ) || tagName === 'TEXTAREA' ;
662
662
const placeholder = element . getAttribute ( 'placeholder' ) || '' ;
663
663
const title = element . getAttribute ( 'title' ) || '' ;
664
664
if ( ! usePlaceholder || title )
@@ -667,10 +667,10 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
667
667
}
668
668
669
669
// https://w3c.github.io/html-aam/#fieldset-and-legend-elements
670
- if ( ! labelledBy && element . tagName === 'FIELDSET' ) {
670
+ if ( ! labelledBy && tagName === 'FIELDSET' ) {
671
671
options . visitedElements . add ( element ) ;
672
672
for ( let child = element . firstElementChild ; child ; child = child . nextElementSibling ) {
673
- if ( child . tagName === 'LEGEND' ) {
673
+ if ( elementSafeTagName ( child ) === 'LEGEND' ) {
674
674
return getTextAlternativeInternal ( child , {
675
675
...childOptions ,
676
676
embeddedInNativeTextAlternative : { element : child , hidden : isElementHiddenForAria ( child ) } ,
@@ -682,10 +682,10 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
682
682
}
683
683
684
684
// https://w3c.github.io/html-aam/#figure-and-figcaption-elements
685
- if ( ! labelledBy && element . tagName === 'FIGURE' ) {
685
+ if ( ! labelledBy && tagName === 'FIGURE' ) {
686
686
options . visitedElements . add ( element ) ;
687
687
for ( let child = element . firstElementChild ; child ; child = child . nextElementSibling ) {
688
- if ( child . tagName === 'FIGCAPTION' ) {
688
+ if ( elementSafeTagName ( child ) === 'FIGCAPTION' ) {
689
689
return getTextAlternativeInternal ( child , {
690
690
...childOptions ,
691
691
embeddedInNativeTextAlternative : { element : child , hidden : isElementHiddenForAria ( child ) } ,
@@ -700,7 +700,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
700
700
//
701
701
// SPEC DIFFERENCE.
702
702
// Spec says to ignore this when aria-labelledby is defined, but all browsers take it into account.
703
- if ( element . tagName === 'IMG' ) {
703
+ if ( tagName === 'IMG' ) {
704
704
options . visitedElements . add ( element ) ;
705
705
const alt = element . getAttribute ( 'alt' ) || '' ;
706
706
if ( trimFlatString ( alt ) )
@@ -710,10 +710,10 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
710
710
}
711
711
712
712
// https://w3c.github.io/html-aam/#table-element
713
- if ( element . tagName === 'TABLE' ) {
713
+ if ( tagName === 'TABLE' ) {
714
714
options . visitedElements . add ( element ) ;
715
715
for ( let child = element . firstElementChild ; child ; child = child . nextElementSibling ) {
716
- if ( child . tagName === 'CAPTION' ) {
716
+ if ( elementSafeTagName ( child ) === 'CAPTION' ) {
717
717
return getTextAlternativeInternal ( child , {
718
718
...childOptions ,
719
719
embeddedInNativeTextAlternative : { element : child , hidden : isElementHiddenForAria ( child ) } ,
@@ -731,7 +731,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
731
731
}
732
732
733
733
// https://w3c.github.io/html-aam/#area-element
734
- if ( element . tagName === 'AREA' ) {
734
+ if ( tagName === 'AREA' ) {
735
735
options . visitedElements . add ( element ) ;
736
736
const alt = element . getAttribute ( 'alt' ) || '' ;
737
737
if ( trimFlatString ( alt ) )
@@ -741,18 +741,18 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
741
741
}
742
742
743
743
// https://www.w3.org/TR/svg-aam-1.0/#mapping_additional_nd
744
- if ( element . tagName . toUpperCase ( ) === 'SVG' || ( element as SVGElement ) . ownerSVGElement ) {
744
+ if ( tagName === 'SVG' || ( element as SVGElement ) . ownerSVGElement ) {
745
745
options . visitedElements . add ( element ) ;
746
746
for ( let child = element . firstElementChild ; child ; child = child . nextElementSibling ) {
747
- if ( child . tagName . toUpperCase ( ) === 'TITLE' && ( child as SVGElement ) . ownerSVGElement ) {
747
+ if ( elementSafeTagName ( child ) === 'TITLE' && ( child as SVGElement ) . ownerSVGElement ) {
748
748
return getTextAlternativeInternal ( child , {
749
749
...childOptions ,
750
750
embeddedInLabelledBy : { element : child , hidden : isElementHiddenForAria ( child ) } ,
751
751
} ) ;
752
752
}
753
753
}
754
754
}
755
- if ( ( element as SVGElement ) . ownerSVGElement && element . tagName . toUpperCase ( ) === 'A' ) {
755
+ if ( ( element as SVGElement ) . ownerSVGElement && tagName === 'A' ) {
756
756
const title = element . getAttribute ( 'xlink:title' ) || '' ;
757
757
if ( trimFlatString ( title ) ) {
758
758
options . visitedElements . add ( element ) ;
@@ -762,7 +762,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
762
762
}
763
763
764
764
// See https://w3c.github.io/html-aam/#summary-element-accessible-name-computation for "summary"-specific check.
765
- const shouldNameFromContentForSummary = element . tagName === 'SUMMARY' && ! [ 'presentation' , 'none' ] . includes ( role ) ;
765
+ const shouldNameFromContentForSummary = tagName === 'SUMMARY' && ! [ 'presentation' , 'none' ] . includes ( role ) ;
766
766
767
767
// step 2f + step 2h.
768
768
if ( allowsNameFromContent ( role , options . embeddedInTargetElement === 'descendant' ) ||
@@ -815,7 +815,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
815
815
}
816
816
817
817
// step 2i.
818
- if ( ! [ 'presentation' , 'none' ] . includes ( role ) || element . tagName === 'IFRAME' ) {
818
+ if ( ! [ 'presentation' , 'none' ] . includes ( role ) || tagName === 'IFRAME' ) {
819
819
options . visitedElements . add ( element ) ;
820
820
const title = element . getAttribute ( 'title' ) || '' ;
821
821
if ( trimFlatString ( title ) )
@@ -830,7 +830,7 @@ export const kAriaSelectedRoles = ['gridcell', 'option', 'row', 'tab', 'rowheade
830
830
export function getAriaSelected ( element : Element ) : boolean {
831
831
// https://www.w3.org/TR/wai-aria-1.2/#aria-selected
832
832
// https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
833
- if ( element . tagName === 'OPTION' )
833
+ if ( elementSafeTagName ( element ) === 'OPTION' )
834
834
return ( element as HTMLOptionElement ) . selected ;
835
835
if ( kAriaSelectedRoles . includes ( getAriaRole ( element ) || '' ) )
836
836
return getAriaBoolean ( element . getAttribute ( 'aria-selected' ) ) === true ;
@@ -843,11 +843,12 @@ export function getAriaChecked(element: Element): boolean | 'mixed' {
843
843
return result === 'error' ? false : result ;
844
844
}
845
845
export function getChecked ( element : Element , allowMixed : boolean ) : boolean | 'mixed' | 'error' {
846
+ const tagName = elementSafeTagName ( element ) ;
846
847
// https://www.w3.org/TR/wai-aria-1.2/#aria-checked
847
848
// https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
848
- if ( allowMixed && element . tagName === 'INPUT' && ( element as HTMLInputElement ) . indeterminate )
849
+ if ( allowMixed && tagName === 'INPUT' && ( element as HTMLInputElement ) . indeterminate )
849
850
return 'mixed' ;
850
- if ( element . tagName === 'INPUT' && [ 'checkbox' , 'radio' ] . includes ( ( element as HTMLInputElement ) . type ) )
851
+ if ( tagName === 'INPUT' && [ 'checkbox' , 'radio' ] . includes ( ( element as HTMLInputElement ) . type ) )
851
852
return ( element as HTMLInputElement ) . checked ;
852
853
if ( kAriaCheckedRoles . includes ( getAriaRole ( element ) || '' ) ) {
853
854
const checked = element . getAttribute ( 'aria-checked' ) ;
@@ -877,7 +878,7 @@ export const kAriaExpandedRoles = ['application', 'button', 'checkbox', 'combobo
877
878
export function getAriaExpanded ( element : Element ) : boolean | 'none' {
878
879
// https://www.w3.org/TR/wai-aria-1.2/#aria-expanded
879
880
// https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
880
- if ( element . tagName === 'DETAILS' )
881
+ if ( elementSafeTagName ( element ) === 'DETAILS' )
881
882
return ( element as HTMLDetailsElement ) . open ;
882
883
if ( kAriaExpandedRoles . includes ( getAriaRole ( element ) || '' ) ) {
883
884
const expanded = element . getAttribute ( 'aria-expanded' ) ;
@@ -894,7 +895,7 @@ export const kAriaLevelRoles = ['heading', 'listitem', 'row', 'treeitem'];
894
895
export function getAriaLevel ( element : Element ) : number {
895
896
// https://www.w3.org/TR/wai-aria-1.2/#aria-level
896
897
// https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
897
- const native = { 'H1' : 1 , 'H2' : 2 , 'H3' : 3 , 'H4' : 4 , 'H5' : 5 , 'H6' : 6 } [ element . tagName ] ;
898
+ const native = { 'H1' : 1 , 'H2' : 2 , 'H3' : 3 , 'H4' : 4 , 'H5' : 5 , 'H6' : 6 } [ elementSafeTagName ( element ) ] ;
898
899
if ( native )
899
900
return native ;
900
901
if ( kAriaLevelRoles . includes ( getAriaRole ( element ) || '' ) ) {
@@ -922,7 +923,7 @@ function isNativelyDisabled(element: Element) {
922
923
function belongsToDisabledFieldSet ( element : Element | null ) : boolean {
923
924
if ( ! element )
924
925
return false ;
925
- if ( element . tagName === 'FIELDSET' && element . hasAttribute ( 'disabled' ) )
926
+ if ( elementSafeTagName ( element ) === 'FIELDSET' && element . hasAttribute ( 'disabled' ) )
926
927
return true ;
927
928
// fieldset does not work across shadow boundaries.
928
929
return belongsToDisabledFieldSet ( element . parentElement ) ;
0 commit comments