', '' ); } if ( 'et_search_icon' === $id ) { return array( __( 'Search', 'fixlight' ), '', '' ); } if ( Compat::has_class( $class, 'et_close_search_field' ) ) { return array( __( 'Close search', 'fixlight' ), '', '' ); } if ( Compat::has_class( $class, 'et_pb_scroll_top' ) ) { return array( __( 'Back to top', 'fixlight' ), '', '' ); } if ( Compat::has_class( $class, 'show-menu' ) ) { return array( __( 'Menu', 'fixlight' ), '', 'relative' ); } if ( 'back_to_top' === $id ) { return array( __( 'Back to top', 'fixlight' ), '', '' ); } return null; } /** * Whether an element is a toggle or accordion title. * * @param array $element Recorded element. * @return bool */ private static function is_toggle_title( array $element ) { return in_array( $element['tag'], array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) && isset( $element['attrs']['class'] ) && Compat::has_class( (string) $element['attrs']['class'], 'et_pb_toggle_title' ); } /** * Plans fixes. * * @param array $doc Analysis. * @param Compat_Plan $plan Plan. */ public static function plan( array $doc, Compat_Plan $plan ) { $keyboard = $plan->on( 'theme_compat' ); $names = $plan->on( 'icon_labels' ); $forms = $plan->on( 'form_labels' ); $theme = self::is_theme(); $extra = self::is_extra(); $elements = $doc['elements']; // Labels the themes hide (with CSS, or with a script that turns them into field values). $labels = array(); if ( $forms ) { foreach ( $elements as $element ) { if ( 'LABEL' !== $element['tag'] || ! isset( $element['attrs']['for'] ) || ! is_string( $element['attrs']['for'] ) || '' === $element['attrs']['for'] ) { continue; } $class = isset( $element['attrs']['class'] ) && is_string( $element['attrs']['class'] ) ? $element['attrs']['class'] : ''; $comment = 'P' === $element['parent']['tag'] && 1 === preg_match( '/(^|\s)comment-form-(comment|author|email|url)(\s|$)/', $element['parent']['class'] ); if ( $comment || Compat::has_class( $class, 'et_pb_contact_form_label' ) || Compat::has_class( $class, 'screen-reader-text' ) || Compat::has_class( $class, 'wp-block-search__label' ) ) { $text = self::label_text( $element['text'] ); if ( '' !== $text ) { $labels[ $element['attrs']['for'] ] = $text; } } } } $field_label = ''; foreach ( $elements as $ord => $element ) { if ( $element['hidden'] ) { continue; } $attrs = $element['attrs']; $class = isset( $attrs['class'] ) && is_string( $attrs['class'] ) ? $attrs['class'] : ''; $control = $keyboard ? self::mouse_control( $element ) : null; if ( $control ) { $plan->keyboard_button( $element, $control[0], $control[2] ); continue; } switch ( $element['tag'] ) { case 'H1': case 'H2': case 'H3': case 'H4': case 'H5': case 'H6': // Text-only titles: the toggle's content must be the very next element. $next = isset( $elements[ $ord + 1 ] ) ? $elements[ $ord + 1 ] : null; if ( $keyboard && self::is_toggle_title( $element ) && '' !== $element['text'] && $next && 'DIV' === $next['tag'] && isset( $next['attrs']['class'] ) && Compat::has_class( (string) $next['attrs']['class'], 'et_pb_toggle_content' ) ) { $plan->wrap_in_button( $element ); } break; case 'P': case 'DIV': if ( Compat::has_class( $class, 'et_pb_contact_field' ) ) { $field_label = ''; } break; case 'LABEL': if ( Compat::has_class( $class, 'et_pb_contact_form_label' ) ) { $field_label = self::label_text( $element['text'] ); } break; case 'SPAN': if ( $forms && '' !== $field_label && Compat::has_class( $class, 'et_pb_contact_field_options_wrapper' ) && ! isset( $attrs['role'] ) && ! self::named( $element, $plan ) ) { // Checkboxes and radios have their own labels; the hidden field label names the group. $plan->set( $ord, 'role', 'group' ); $plan->set( $ord, 'aria-label', $field_label ); $plan->count( 'form_labels' ); $field_label = ''; } elseif ( $names && $extra && Compat::has_class( $class, 'rating-stars' ) && ! isset( $attrs['role'] ) && ! self::named( $element, $plan ) ) { $rating = self::rating( isset( $attrs['title'] ) ? (string) $attrs['title'] : '' ); if ( '' !== $rating ) { $plan->set( $ord, 'role', 'img' ); /* translators: %s: star rating, such as 4.5. */ $plan->set( $ord, 'aria-label', sprintf( __( 'Rated %s out of 5', 'fixlight' ), $rating ) ); $plan->count( 'icon_labels' ); } } break; case 'A': case 'BUTTON': if ( $names && ( 'BUTTON' === $element['tag'] || Html_Analyzer::is_live_link( $element ) ) && ! self::named( $element, $plan ) ) { $label = self::link_label( $element, $class, $theme, $extra ); if ( '' !== $label ) { $plan->set( $ord, 'aria-label', $label ); $plan->count( 'icon_labels' ); } } break; case 'INPUT': case 'SELECT': case 'TEXTAREA': if ( ! $forms || $element['in_label'] || ! Html_Analyzer::control_needs_name( $element ) || self::named( $element, $plan ) || ( isset( $attrs['title'] ) && is_string( $attrs['title'] ) && '' !== trim( $attrs['title'] ) ) ) { break; } $id = isset( $attrs['id'] ) && is_string( $attrs['id'] ) ? $attrs['id'] : ''; $label = '' !== $id && isset( $labels[ $id ] ) ? $labels[ $id ] : ''; if ( '' === $label && 'INPUT' === $element['tag'] && Compat::has_class( $class, 'et_pb_contact_captcha' ) ) { $first = isset( $attrs['data-first_digit'] ) ? (string) $attrs['data-first_digit'] : ''; $second = isset( $attrs['data-second_digit'] ) ? (string) $attrs['data-second_digit'] : ''; if ( ctype_digit( $first ) && ctype_digit( $second ) ) { /* translators: 1: first number, 2: second number of the sum a form asks visitors to solve. */ $label = sprintf( __( 'Solve %1$s + %2$s', 'fixlight' ), $first, $second ); } } elseif ( '' === $label && $theme && 'TEXTAREA' === $element['tag'] && 'comment' === $id && ! Html_Analyzer::control_has_name( $element, $doc ) ) { $label = __( 'Comment', 'fixlight' ); } if ( '' !== $label ) { $plan->set( $ord, 'aria-label', $label ); $plan->count( 'form_labels' ); } break; } } } /** * A name for a Divi or Extra link or button whose own name is wrong, or ''. * * @param array $element Recorded A or BUTTON. * @param string $classes Its class attribute. * @param bool $theme Whether Divi or Extra is the theme. * @param bool $extra Whether Extra is the theme. * @return string */ private static function link_label( array $element, $classes, $theme, $extra ) { $text = $element['text']; $parent = $element['parent']; if ( 'A' === $element['tag'] ) { if ( $extra && isset( $element['attrs']['id'] ) && 'et-trending-button' === $element['attrs']['id'] && '' === $text ) { return __( 'Menu', 'fixlight' ); // It opens the secondary menu. } if ( Compat::has_class( $classes, 'scroll-down-container' ) && ( '' === $text || Html_Analyzer::is_weak_name( $text ) ) ) { return __( 'Scroll down', 'fixlight' ); // Its only text is an icon-font glyph. } if ( $theme && Compat::has_class( $classes, 'comments-link' ) && ctype_digit( $text ) ) { $count = (int) $text; /* translators: %s: number of comments. */ return sprintf( _n( '%s comment', '%s comments', $count, 'fixlight' ), number_format_i18n( $count ) ); } } // The visible text again, without the icon glyph the theme adds with CSS ("Submit 5", "Services 3"). // Not for "Read more" and the like: an aria-label would hide that they still need a better name. if ( '' === $text || Fixer::is_ambiguous( $text ) ) { return ''; } if ( Compat::has_class( $classes, 'et_pb_button' ) ) { return $text; } if ( 'A' === $element['tag'] && 'LI' === $parent['tag'] && Compat::has_class( $parent['class'], 'menu-item-has-children' ) && ( $theme || false !== strpos( $parent['class'], 'et_pb_menu_page_id-' ) ) ) { return $text; } return ''; } /** * Whether an element already has, or is about to get, an ARIA name. * * @param array $element Recorded element. * @param Compat_Plan $plan Plan. * @return bool */ private static function named( array $element, Compat_Plan $plan ) { foreach ( array( 'aria-label', 'aria-labelledby' ) as $attribute ) { if ( isset( $element['attrs'][ $attribute ] ) && is_string( $element['attrs'][ $attribute ] ) && '' !== trim( $element['attrs'][ $attribute ] ) ) { return true; } } return $plan->planned( $element['ord'], 'aria-label' ) || $plan->planned( $element['ord'], 'aria-labelledby' ); } /** * A hidden label's text as a field name: without the required-field * asterisk, and in sentence case when written in capitals (Extra's "COMMENT"). * * @param string $text Label text. * @return string */ private static function label_text( $text ) { $text = trim( (string) preg_replace( '/\s*\*+$/', '', $text ) ); if ( function_exists( 'mb_strtolower' ) && mb_strlen( $text ) > 3 && Html_Analyzer::text_matches( '/\p{Lu}/u', $text ) && ! Html_Analyzer::text_matches( '/\p{Ll}/u', $text ) ) { $lower = mb_strtolower( $text, 'UTF-8' ); $text = mb_strtoupper( mb_substr( $lower, 0, 1, 'UTF-8' ), 'UTF-8' ) . mb_substr( $lower, 1, null, 'UTF-8' ); } return $text; } /** * Rating from Extra's star title ("Rating: 4.50"), as text, or '' when unrated. * * @param string $title Title attribute. * @return string */ private static function rating( $title ) { if ( ! preg_match( '/(\d+(?:[.,]\d+)?)/', $title, $match ) ) { return ''; } $value = round( (float) str_replace( ',', '.', $match[1] ), 1 ); if ( $value <= 0 || $value > 5 ) { return ''; } return number_format_i18n( $value, floor( $value ) === $value ? 0 : 1 ); } /** * Adjusts Fixlight's skip link. Divi's smooth-scroll script cancels "#" links * unless they carry its opt-out class; Extra's cancels every link that starts * with "#", so there the link names the page itself before the fragment. * * @param array $link Link: href and class. * @param array $context Fixer context. * @return array */ public static function skip_link( array $link, array $context ) { $link['class'] .= ' et_smooth_scroll_disabled'; if ( self::is_extra() && 0 === strpos( $link['href'], '#' ) ) { $uri = self::request_uri(); if ( '' !== $uri ) { $path = (string) wp_parse_url( $uri, PHP_URL_PATH ); $query = (string) wp_parse_url( $uri, PHP_URL_QUERY ); // A path starting with "//" would read as another host. if ( '' !== $path && '/' === $path[0] && 0 !== strpos( $path, '//' ) && false === strpos( $path, '\\' ) ) { $link['href'] = $path . ( '' !== $query ? '?' . $query : '' ) . $link['href']; } } } return $link; } /** * The URI of the page being served, or '' outside a front-end page request * (background and REST checks, WP-CLI), where the link keeps its plain "#" * form so the server check still recognizes it. * * @return string */ private static function request_uri() { if ( null !== self::$request_uri ) { return self::$request_uri; } if ( ( defined( 'WP_CLI' ) && WP_CLI ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || wp_doing_ajax() || wp_doing_cron() || is_admin() || ! isset( $_SERVER['REQUEST_URI'] ) ) { return ''; } return esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ); } /** * Problems for the server check: controls that still only work with a mouse. * * @param array $doc Analysis. * @return array */ public static function problems( array $doc ) { $samples = array(); foreach ( $doc['elements'] as $element ) { if ( $element['hidden'] || isset( $element['attrs'][ Compat::KB_MARK ] ) ) { continue; } $control = self::mouse_control( $element ); if ( $control ) { $samples[] = $control[1]; } elseif ( self::is_toggle_title( $element ) ) { $samples[] = '<' . strtolower( $element['tag'] ) . ' class="et_pb_toggle_title">'; } } return $samples ? array( 'mouse-only-control' => $samples ) : array(); } /** * CSS. * * @return string */ public static function css() { $hidden = 'position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;clip-path:inset(50%)!important;white-space:nowrap!important;border:0!important'; // The shared reset for Fixlight's buttons is in Compat::css(). // Slider arrows: their text is display:none, so the links had no name. Shown on keyboard focus, as on hover. $css = '.et-pb-arrow-prev span,.et-pb-arrow-next span{display:block!important;' . $hidden . '}' . '.et-pb-slider-arrows a:focus-visible{opacity:1!important}' . '.et_pb_slider:has(:focus-visible) .et-pb-arrow-prev{left:22px;opacity:1}' . '.et_pb_slider:has(:focus-visible) .et-pb-arrow-next{right:22px;opacity:1}' // Contact form checkboxes and radios: display:none kept them out of reach; the styled box shows focus. . '.et_pb_contact_field input[type=checkbox],.et_pb_contact_field input[type=radio]{display:block!important;position:absolute!important;width:1px!important;height:1px!important;margin:0!important;padding:0!important;opacity:0!important;pointer-events:none}' . '.et_pb_contact_field input[type=checkbox]:focus-visible+label i,.et_pb_contact_field input[type=radio]:focus-visible+label i{outline:3px solid #1d2327;outline-offset:2px;box-shadow:0 0 0 2px #fff}'; if ( self::is_extra() ) { $css .= '.et_pb_widget .screen-reader-text{display:block!important;' . $hidden . '}' . 'header.header li:focus-within>ul{opacity:1;visibility:visible}' . '#et-navigation .mega-menu:focus-within>ul ul{visibility:visible}' . '.et-slider:has(:focus-visible) .et-pb-slider-arrows a,.featured-posts-slider-module:has(:focus-visible) .et-pb-slider-arrows a,.posts-carousel-module:has(:focus-visible) .et-pb-slider-arrows a{opacity:.7}' . '.et-slider:has(:focus-visible) .et-pb-arrow-prev,.featured-posts-slider-module:has(:focus-visible) .et-pb-arrow-prev,.posts-carousel-module:has(:focus-visible) .et-pb-arrow-prev{left:20px}' . '.et-slider:has(:focus-visible) .et-pb-arrow-next,.featured-posts-slider-module:has(:focus-visible) .et-pb-arrow-next,.posts-carousel-module:has(:focus-visible) .et-pb-arrow-next{right:20px}'; } else { // Submenus open on keyboard focus, as on hover. $css .= '.nav li:focus-within>ul,#et-secondary-nav li:focus-within>ul{opacity:1;visibility:visible}' . '.et-menu-nav li.mega-menu:focus-within>ul{opacity:1!important;visibility:visible!important}' // Closed header search: its field and close button stay out of the tab order (after the fade-out). . '.et_search_outer .et_search_form_container:not(.et_pb_search_visible){visibility:hidden;transition:visibility 0s 1s}' // The search icon is drawn by a pseudo-element; the button takes its box. . '#et_top_search #et_search_icon>.fixlight-kb{top:-3px!important;bottom:auto!important;height:17px!important}' . '.et_pb_scroll_top.et-hidden>.fixlight-kb{visibility:hidden!important}'; } return $css; } }