$element ) { if ( $plan->on( 'theme_compat' ) ) { $box = self::landmark( $element, $box, $plan ); } if ( isset( $links[ $ord ] ) ) { if ( $plan->on( 'icon_labels' ) ) { self::name_link( $element, $links[ $ord ], $doc, $plan ); } continue; } $class = self::classes( $element ); if ( 'A' === $element['tag'] && $plan->on( 'icon_labels' ) && Html_Analyzer::is_live_link( $element ) && ! Html_Analyzer::has_accessible_name( $element ) && ! $plan->planned( $ord, 'aria-label' ) ) { if ( Compat::has_class( $class, 'et_social_open_all' ) ) { $plan->label( $element, __( 'More share options', 'fixlight' ) ); } elseif ( Compat::has_class( $class, 'et_social_icon_cancel' ) ) { $plan->label( $element, __( 'Close', 'fixlight' ) ); } continue; } $control = self::control( $element ); if ( '' === $control || ! $plan->on( 'theme_compat' ) ) { continue; } switch ( $control ) { case 'hide_sidebar': // It shows the buttons again on the next press, so the name covers both. $plan->keyboard_button( $element, __( 'Show or hide share buttons', 'fixlight' ), 'relative' ); break; case 'mobile_button': $plan->keyboard_button( $element, __( 'Share this page', 'fixlight' ) ); break; case 'close': $plan->keyboard_button( $element, __( 'Close', 'fixlight' ) ); break; case 'heading': $plan->wrap_in_button( $element ); break; case 'media': $plan->keyboard_button( $element, self::media_label( $element, $plan ) ); break; } } } /** * Problems for the server check. * * @param array $doc Analysis. * @return array */ public static function problems( array $doc ) { $found = array(); foreach ( $doc['elements'] as $element ) { if ( '' !== self::control( $element ) && ! isset( $element['attrs'][ Compat::KB_MARK ] ) ) { $found[] = '<' . strtolower( $element['tag'] ) . ' class="' . self::classes( $element ) . '">'; } } return $found ? array( 'mouse-only-control' => $found ) : array(); } /** * CSS. * * @return string */ public static function css() { // Monarch clips its buttons (overflow:hidden) and removes their outline, so the focus ring goes inside the edge. return '.et_monarch .et_social_networks a:focus-visible,.et_monarch .et_social_sidebar_networks a:focus-visible,.et_monarch .et_social_networks .fixlight-kb:focus-visible{outline:3px solid #1d2327!important;outline-offset:-3px!important;box-shadow:inset 0 0 0 5px #fff!important}' // The small icons clip what is inside them, so their ring goes on the icon itself. . '.et_monarch .et_social_hide_sidebar:has(>.fixlight-kb:focus-visible),.et_monarch .et_social_pinterest_window .et_social_close:has(>.fixlight-kb:focus-visible){outline:3px solid #1d2327!important;outline-offset:2px!important;box-shadow:0 0 0 2px #fff!important}' // The phone share bar is dark and sits on the screen edge. . '.et_monarch .et_social_mobile_button:has(>.fixlight-kb:focus-visible){outline:3px solid #fff!important;outline-offset:-3px!important}' . '.et_monarch .et_social_mobile .et_social_close:has(>.fixlight-kb:focus-visible),.et_monarch .et_social_heading>.fixlight-kb-wrap:focus-visible{outline:3px solid #fff!important;outline-offset:2px!important}' // Inline, so the heading's arrow stays beside its text; the button slides off screen while the bar is open. . '.et_monarch .et_social_heading>.fixlight-kb-wrap{display:inline;width:auto}' . '.et_monarch .et_social_mobile_button:not(.et_social_active_button)>.fixlight-kb{visibility:hidden}' // Show what has focus: these only appear while the mouse is over them. . '.et_monarch .et_social_sidebar_networks:focus-within .et_social_hide_sidebar,.et_monarch .et_social_media_wrapper:focus-within .et_social_media{opacity:1}'; } /** * Names Monarch's boxes outside the page content: the floating bars become * a landmark, and the pop-up, fly-in and Pinterest window dialogs named by * their title. No aria-modal: nothing keeps focus inside them. * * @param array $element Recorded element. * @param int|null $box Box waiting for its title. * @param Compat_Plan $plan Plan. * @return int|null Box still waiting for its title. */ private static function landmark( array $element, $box, Compat_Plan $plan ) { $class = self::classes( $element ); if ( 'DIV' === $element['tag'] && '' !== $class && ! isset( $element['attrs']['role'] ) ) { if ( Compat::has_class( $class, 'et_social_sidebar_networks' ) || Compat::has_class( $class, 'et_social_mobile' ) ) { $plan->set( $element['ord'], 'role', 'complementary' ); $plan->set( $element['ord'], 'aria-label', __( 'Share this page', 'fixlight' ) ); $plan->count( 'theme_compat' ); return $box; } if ( Compat::has_class( $class, 'et_social_popup' ) || Compat::has_class( $class, 'et_social_flyin' ) || Compat::has_class( $class, 'et_social_pin_images_outer' ) ) { return $element['ord']; } } if ( null !== $box && 'H3' === $element['tag'] && ( Compat::has_class( $element['parent']['class'], 'et_social_header' ) || Compat::has_class( $element['parent']['class'], 'et_social_modal_header' ) ) ) { if ( '' !== $element['text'] ) { $plan->set( $box, 'role', 'dialog' ); $plan->set( $box, 'aria-label', $element['text'] ); $plan->count( 'theme_compat' ); } return null; } return $box; } /** * Monarch's share and follow links, each with the count element inside it * and whether Monarch's script adds the count after the page loads. * * @param array $doc Analysis. * @return array Link ordinal => details. */ private static function links( array $doc ) { $links = array(); $current = null; foreach ( $doc['elements'] as $ord => $element ) { $class = self::classes( $element ); if ( 'A' === $element['tag'] ) { $current = null; $network = isset( $element['attrs']['data-social_name'] ) && is_string( $element['attrs']['data-social_name'] ) ? $element['attrs']['data-social_name'] : ''; if ( '' !== $network && ( Compat::has_class( $class, 'et_social_share' ) || Compat::has_class( $class, 'et_social_share_pinterest' ) || Compat::has_class( $class, 'et_social_follow' ) ) ) { $current = $ord; $links[ $ord ] = array( 'count' => null, 'name' => false, 'js_count' => Compat::has_class( $class, 'et_social_display_count' ), ); } } elseif ( null !== $current ) { if ( 'SPAN' === $element['tag'] && Compat::has_class( $class, 'et_social_overlay' ) ) { $current = null; // The last thing inside every Monarch link. } elseif ( 'DIV' === $element['tag'] && Compat::has_class( $class, 'et_social_share' ) ) { $current = null; } elseif ( 'DIV' === $element['tag'] && Compat::has_class( $class, 'et_social_networkname' ) ) { $links[ $current ]['name'] = true; } elseif ( 'DIV' === $element['tag'] && Compat::has_class( $class, 'et_social_network_label' ) && Compat::has_class( $class, 'et_social_display_follow_counts' ) ) { $links[ $current ]['js_count'] = true; } elseif ( 'DIV' === $element['tag'] && Compat::has_class( $class, 'et_social_count' ) && null === $links[ $current ]['count'] ) { $links[ $current ]['count'] = $ord; } } } return $links; } /** * Names a share or follow link. Monarch prints no name; the generic fix * names the link from its address, but as aria-label, which no longer * matches what people see once Monarch's script adds the share count. So * the name goes in hidden text before the count instead, worded from the * network Monarch says the link is for. * * @param array $element Recorded link. * @param array $info Details from links(). * @param array $doc Analysis. * @param Compat_Plan $plan Plan. */ private static function name_link( array $element, array $info, array $doc, Compat_Plan $plan ) { $attrs = $element['attrs']; $ord = $element['ord']; if ( ! Html_Analyzer::is_live_link( $element ) || $info['name'] ) { return; // A network name shown on the page already names the link. } foreach ( array( 'aria-label', 'aria-labelledby', 'title' ) as $attribute ) { if ( isset( $attrs[ $attribute ] ) && is_string( $attrs[ $attribute ] ) && '' !== trim( $attrs[ $attribute ] ) ) { return; // Named by the site. } } $text = $element['text']; if ( '' !== $text && ( null === $info['count'] || ! preg_match( '/^>?\s*\d/', $text ) ) ) { return; // Text other than Monarch's count ("12", "1.2k"). } // What the generic fix named the link, '' when it left the link alone. $generic = ''; if ( '' === $text || Html_Analyzer::is_weak_name( $text ) ) { $generic = '' !== $element['hidden_text'] && '' === $text ? $element['hidden_text'] : Fixer::icon_label( isset( $attrs['href'] ) ? (string) $attrs['href'] : '', $attrs, $element['hint'], $plan->context ); } $label = Compat::has_class( self::classes( $element ), 'et_social_follow' ) ? '' : self::share_label( (string) $attrs['data-social_name'] ); if ( '' === $label ) { $label = $generic; } if ( '' === $label ) { return; } if ( '' === $text ) { $counted = $plan->planned( $ord, 'aria-label' ); if ( $info['js_count'] ) { // The count arrives inside the link later and joins the hidden name. if ( $counted ) { $plan->set( $ord, 'aria-label', false ); } $plan->prepend( $ord, '' . esc_html( $label ) . ' ' ); } else { $plan->set( $ord, 'aria-label', $label ); } if ( ! $counted ) { $plan->count( 'icon_labels' ); } return; } // A count printed with the page. if ( '' === $generic ) { $plan->label( $element, $label ); return; } if ( $label === $generic ) { return; } // The generic fix already put its own hidden name before the count, and // that cannot be taken back: name the link from this pack's text and // the count instead, so the generic text is not read. $count = $doc['elements'][ $info['count'] ]['attrs']; $id = 'fixlight-monarch-' . $ord; $own_id = isset( $count['id'] ) && is_string( $count['id'] ) && '' !== $count['id']; $count_id = $own_id ? $count['id'] : $id . '-count'; if ( isset( $doc['ids'][ $id ] ) || ( $own_id ? 1 !== $doc['ids'][ $count_id ] : isset( $doc['ids'][ $count_id ] ) ) ) { return; } $plan->prepend( $ord, '' . esc_html( $label ) . '' ); if ( ! $own_id ) { $plan->set( $info['count'], 'id', $count_id ); } $plan->set( $ord, 'aria-labelledby', $id . ' ' . $count_id ); } /** * Name for a share link, from Monarch's network key ('' when unknown). * * @param string $network Value of data-social_name. * @return string */ private static function share_label( $network ) { switch ( $network ) { case 'like': return __( 'Like this post', 'fixlight' ); case 'printfriendly': return __( 'Print', 'fixlight' ); case 'pinterest': return __( 'Save to Pinterest', 'fixlight' ); } $mail = array( 'gmail' => 'Gmail', 'yahoomail' => 'Yahoo Mail', 'aol' => 'AOL Mail', ); if ( isset( $mail[ $network ] ) ) { /* translators: %s: email service, such as Gmail. */ return sprintf( __( 'Share by %s', 'fixlight' ), $mail[ $network ] ); } $networks = array( 'facebook' => 'Facebook', 'twitter' => 'X (Twitter)', 'linkedin' => 'LinkedIn', 'reddit' => 'Reddit', 'tumblr' => 'Tumblr', 'vkontakte' => 'VK', 'buffer' => 'Buffer', 'evernote' => 'Evernote', 'blogger' => 'Blogger', 'livejournal' => 'LiveJournal', 'hackernews' => 'Hacker News', 'digg' => 'Digg', 'stumbleupon' => 'StumbleUpon', 'delicious' => 'Delicious', 'newsvine' => 'Newsvine', 'myspace' => 'Myspace', 'googleplus' => 'Google+', 'amazon' => 'Amazon', ); if ( ! isset( $networks[ $network ] ) ) { return ''; } /* translators: %s: social network name. */ return sprintf( __( 'Share on %s', 'fixlight' ), $networks[ $network ] ); } /** * Name for an "on media" share icon over an image. * * @param array $element Recorded div. * @param Compat_Plan $plan Plan. * @return string */ private static function media_label( array $element, Compat_Plan $plan ) { $attrs = $element['attrs']; $label = isset( $attrs['data-social_name'] ) && is_string( $attrs['data-social_name'] ) ? self::share_label( $attrs['data-social_name'] ) : ''; if ( '' === $label && isset( $attrs['data-social_link'] ) && is_string( $attrs['data-social_link'] ) ) { $label = Fixer::icon_label( $attrs['data-social_link'], array(), '', $plan->context ); } return '' !== $label ? $label : __( 'Share', 'fixlight' ); } /** * Which of Monarch's mouse-only controls an element is, '' for none. * * @param array $element Recorded element. * @return string */ private static function control( array $element ) { $class = self::classes( $element ); if ( '' === $class || $element['hidden'] ) { return ''; } if ( 'SPAN' === $element['tag'] ) { if ( Compat::has_class( $class, 'et_social_hide_sidebar' ) ) { return 'hide_sidebar'; } return Compat::has_class( $class, 'et_social_close' ) ? 'close' : ''; } if ( 'DIV' !== $element['tag'] ) { return ''; } if ( Compat::has_class( $class, 'et_social_mobile_button' ) ) { return 'mobile_button'; } if ( Compat::has_class( $class, 'et_social_heading' ) && Compat::has_class( $element['parent']['class'], 'et_social_mobile' ) ) { return 'heading'; } if ( Compat::has_class( $class, 'et_social_share' ) && isset( $element['attrs']['data-social_type'] ) && 'media' === $element['attrs']['data-social_type'] ) { return 'media'; } return ''; } /** * Class attribute of a recorded element. * * @param array $element Recorded element. * @return string */ private static function classes( array $element ) { return isset( $element['attrs']['class'] ) && is_string( $element['attrs']['class'] ) ? $element['attrs']['class'] : ''; } }