import { Rule , Diagnostic } from "@siteimprove/alfa-act" ; import { Keyword } from "@siteimprove/alfa-css" ; import { Device } from "@siteimprove/alfa-device" ; import { Element } from "@siteimprove/alfa-dom" ; import { Predicate } from "@siteimprove/alfa-predicate" ; import { Err , Ok } from "@siteimprove/alfa-result" ; import { Context } from "@siteimprove/alfa-selector" ; import { Style } from "@siteimprove/alfa-style" ; import { Criterion } from "@siteimprove/alfa-wcag" ; import { Page } from "@siteimprove/alfa-web" ; import { expectation } from "../common/expectation" ; import { hasBorder , hasBoxShadow , hasOutline , hasTextDecoration , isTabbable , } from "../common/predicate" ; import { Question } from "../common/question" ; import { Scope } from "../tags" ; const { isElement } = Element ; const { isKeyword } = Keyword ; const { or , test , xor } = Predicate ; export default Rule . Atomic . of < Page , Element , Question . Metadata > ( { uri : "https://alfa.siteimprove.com/rules/sia-r65" , requirements : [ Criterion . of ( "2.4.7" ) ] , tags : [ Scope . Component ] , evaluate ( { device , document } ) { return { applicability ( ) { const tabbables = document . tabOrder ( ) . filter ( isTabbable ( device ) ) ; if ( tabbables . take ( 2 ) . size < 2 ) { return [ ] ; } return tabbables ; } , expectations ( target ) { const askFocusIndicator = Question . of ( "has-focus-indicator" , target ) ; return { 1 : askFocusIndicator . answerIf ( hasFocusIndicator ( device ) ( target ) , true ) . map ( ( hasFocusIndicator ) => expectation ( hasFocusIndicator , ( ) => Outcomes . HasFocusIndicator , ( ) => Outcomes . HasNoFocusIndicator ) ) , } ; } , } ; } , } ) ; export namespace Outcomes { export const HasFocusIndicator = Ok . of ( Diagnostic . of ( `The element has a visible focus indicator` ) ) ; export const HasNoFocusIndicator = Err . of ( Diagnostic . of ( `The element does not have a visible focus indicator` ) ) ; } function hasFocusIndicator ( device ) { return ( element ) => { const withFocus = Context . focus ( element ) ; return element . inclusiveDescendants ( { flattened : true , } ) . concat ( element . ancestors ( { flattened : true , } ) ) . filter ( isElement ) . some ( or ( xor ( hasOutline ( device ) , hasOutline ( device , withFocus ) ) , xor ( hasTextDecoration ( device ) , hasTextDecoration ( device , withFocus ) ) , xor ( hasBoxShadow ( device ) , hasBoxShadow ( device , withFocus ) ) , hasDifferentColors ( device , withFocus ) , hasDifferentBackgroundColors ( device , withFocus ) , hasDifferentBorder ( device , withFocus ) ) ) ; } ; } function hasDifferentColors ( device , context1 = Context . empty ( ) , context2 = Context . empty ( ) ) { return function hasDifferentColors ( element ) : boolean { const color1 = Style . from ( element , device , context1 ) . computed ( "color" ) ; const color2 = Style . from ( element , device , context2 ) . computed ( "color" ) ; if ( isKeyword ( color1 ) || isKeyword ( color2 ) ) { return false ; } return ! color1 . equals ( color2 ) ; } ; } function hasDifferentBackgroundColors ( device , context1 = Context . empty ( ) , context2 = Context . empty ( ) ) { return function hasDifferentBackgroundColors ( element ) : boolean { const color1 = Style . from ( element , device , context1 ) . computed ( "background-color" ) ; const color2 = Style . from ( element , device , context2 ) . computed ( "background-color" ) ; if ( isKeyword ( color1 ) || isKeyword ( color2 ) ) { return false ; } return ! color1 . equals ( color2 ) ; } ; } function hasDifferentBorder ( device , context1 = Context . empty ( ) , context2 = Context . empty ( ) ) { return function hasDifferentBorder ( element ) : boolean { const style1 = Style . from ( element , device , context1 ) ; const style2 = Style . from ( element , device , context2 ) ; const hasBorder1 = test ( hasBorder ( device , context1 ) , element ) ; const hasBorder2 = test ( hasBorder ( device , context2 ) , element ) ; if ( hasBorder1 !== hasBorder2 ) { return true ; } if ( ! hasBorder1 && ! hasBorder2 ) { return false ; } for ( const side of [ "top" , "right" , "bottom" , "left" ] as const ) { for ( const effect of [ "color" , "style" , "width" ] as const ) { const longhand = `border-${ side }-${ effect }` as const ; const border1 = style1 . computed ( longhand ) ; const border2 = style2 . computed ( longhand ) ; if ( ! ( ( effect === "color" && ( isKeyword ( border1 ) || isKeyword ( border2 ) ) ) || border1 . equals ( border2 ) ) ) { return true ; } } } return false ; } ; }