Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add skip-link to FSE themes #28946

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/how-to-guides/themes/theme-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,38 @@ Currently block variations exist for "header" and "footer" values of the area te
}
```

#### `skipLinks`

Within this field, themes can change the behavior of the automatic skip-link generation, or opt-out.

```json
{
"skipLinks": {
"auto": true,
"css": true,
"links": [
{
"target": "#skip-link-target",
"label": "Skip to Content"
},
{
"target": "#main-navigation",
"label": "Skip to Navigation",
"useFallbacks": false
}
]
}
}
```

Arguments:
* `auto`: (bool) Whether skip-links will be auto-generated or not. Defaults to `true`. To opt-out of the automatic skip-links, set to `false`.
* `css:` (bool) Whether CSS for the skip-link should be automatically added or not. Defaults to `true`. To opt-out and manually handle CSS for skip-links, set to `false`.
* `links`: (array) Can be used to define a custom skip-link (or multiple skip-links if needed). Each "link" can have the following:
* `target`: (string|array) The selector of the element that should be used as the target.
* `label`: (string) The skip-link's label. If omitted defaults to `Skip to content`.
* `useFallbacks`: (bool) Defaults to `true`. If a `target` is not defined, or the `target` element is not located, then the skip-link target will fallback to the 1st instance of these elements: `#skip-link-target, main, .wp-block-post-title, .wp-block-query-loop, .wp-block-post-content, .entry-content, h1, h2`. If set to false, then the fallbacks won't be added. Use in case of multiple skip-links.

## Frequently Asked Questions

### The naming schema of CSS Custom Properties
Expand Down
62 changes: 61 additions & 1 deletion lib/class-wp-theme-json.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ class WP_Theme_JSON {
'custom' => null,
'layout' => null,
),
'skipLinks' => array(
'links' => null,
'auto' => null,
'css' => null,
),
);

/**
Expand Down Expand Up @@ -363,7 +368,6 @@ public function __construct( $theme_json = array() ) {
unset( $this->theme_json[ $subtree ] );
}
}

}

/**
Expand Down Expand Up @@ -980,6 +984,62 @@ public function get_template_parts() {
return $template_parts;
}

/**
* Return an array of skip-links.
*
* @return array
*/
public function get_skip_links() {

$fallback_elements = array(
'#skip-link-target',
'main',
'.wp-block-post-title',
'.wp-block-query-loop',
'.wp-block-post-content',
'.entry-content',
'h1',
'h2',
);

// If we don't have "skipLinks" defined return the defaults.
if ( ! isset( $this->theme_json['skipLinks'] ) ) {
return array(
array(
'target' => $fallback_elements,
'label' => __( 'Skip to content', 'gutenberg' ),
'useFallbacks' => false,
),
);
}

if ( isset( $this->theme_json['skipLinks']['auto'] ) && false === $this->theme_json['skipLinks']['auto'] ) {
return;
}

$links = array();
foreach ( $this->theme_json['skipLinks']['links'] as $link ) {
$selectors = isset( $link['target'] ) ? (array) $link['target'] : array();
if ( ! isset( $link['useFallbacks'] ) || $link['useFallbacks'] ) {
$selectors = array_unique( array_merge( $selectors, $fallback_elements ) );
}
$links[] = array(
'target' => array_values( $selectors ), // Use array_values to ensure there are no skipped keys in the array.
'label' => isset( $link['label'] ) ? $link['label'] : __( 'Skip to content', 'gutenberg' ),
);
}
return $links;
}

/**
* Whether the skip-link styles should be automatically added or not.
*
* @return string
*/
public function should_add_skip_link_styles() {
return ! isset( $this->theme_json['skipLinks'] ) || ! empty( $this->theme_json['skipLinks']['css'] );
}

/**
* Returns the stylesheet that results of processing
* the theme.json structure this object represents.
Expand Down
115 changes: 115 additions & 0 deletions lib/full-site-editing/templates.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,118 @@ function set_unique_slug_on_create_template( $post_id ) {
}
}
add_action( 'save_post_wp_template', 'set_unique_slug_on_create_template' );

/**
* Inject the skip-link.
*/
function gutenberg_inject_skip_link() {
global $template_html, $skip_links_via_script;

$skip_links_via_script = array();

// Add the skip-link styles if needed.
if ( WP_Theme_JSON_Resolver::get_theme_data()->should_add_skip_link_styles() ) {
echo '<style id="skip-link-styles">';
echo '.skip-link.screen-reader-text{border:0;clip:rect(1px,1px,1px,1px);clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute !important;width:1px;word-wrap:normal !important;}';
gziolo marked this conversation as resolved.
Show resolved Hide resolved
echo '.skip-link.screen-reader-text:focus{background-color:#eee;clip:auto !important;clip-path:none;color:#444;display:block;font-size:1em;height:auto;left:5px;line-height:normal;padding:15px 23px 14px;text-decoration:none;top:5px;width:auto;z-index:100000;}';
echo '</style>';
}

// Get the array of skip-links.
$links = WP_Theme_JSON_Resolver::get_theme_data()->get_skip_links();

// Sanity check.
if ( empty( $links ) || ! $template_html ) {
return;
}

// Loop links.
foreach ( $links as $link ) {
$element_found = false;
// Try to find the selector in $template_html and print the skip-link.
foreach ( $link['target'] as $selector ) {
if ( 0 === strpos( $selector, '#' ) ) {
$selector_no_hash = str_replace( '#', '', $selector );
if ( strpos( $template_html, 'id="' . $selector_no_hash . '"' ) || strpos( $template_html, "id='$selector_no_hash'" ) ) {
echo '<a id="wp--skip-link--' . esc_attr( $selector_no_hash ) . '" class="skip-link screen-reader-text" href="' . esc_attr( $selector ) . '">' . esc_html( $link['label'] ) . '</a>';
$element_found = true;
break;
}
}
}

if ( ! $element_found ) {
// Add the skip-link to the $skip_links_via_script global.
$skip_links_via_script[] = $link;
}
}

if ( ! empty( $skip_links_via_script ) ) {
// An element with the appropriate target ID was not located.
// Add a script to auto-generate the target and link based on the content we have.
add_action( 'wp_footer', 'gutenberg_the_skip_link_script' );
}
}
add_action( 'wp_body_open', 'gutenberg_inject_skip_link' );

/**
* Print the skip-link script.
*
* @return void
*/
function gutenberg_the_skip_link_script() {

// Get the links.
global $skip_links_via_script;

// Sanity check.
if ( empty( $skip_links_via_script ) ) {
return;
}
?>
<script>
( function() {
var links = <?php echo wp_json_encode( array_reverse( $skip_links_via_script ) ); // phpcs:ignore WordPress.Security.EscapeOutput ?>,
parentEl = document.querySelector( '.wp-site-blocks' ) || document.body,
generateLink = function( selectors, label, parent ) {
var contentEl, contentElID, skipLink, i;

// Find the content element.
for ( i = 0; i < selectors.length; i++ ) {
if ( ! contentEl ) {
contentEl = document.querySelector( selectors[ i ] );
}
}

// Early exit if no content element was found.
if ( ! contentEl ) {
return;
}

// Get the ID of the content element.
contentElID = contentEl.id;
if ( ! contentElID ) {
contentElID = 'auto-skip-link-target';
contentEl.id = contentElID;
}

// Create the skip link.
skipLink = document.createElement( 'a' );
skipLink.classList.add( 'skip-link', 'screen-reader-text' );
skipLink.href = '#' + contentElID;
skipLink.innerHTML = label;
skipLink.id = 'wp--skip-link--' + contentElID;

// Inject the skip link.
parent.insertAdjacentElement( 'afterbegin', skipLink );
},
i;

for ( i = 0; i < links.length; i++ ) {
generateLink( links[ i ]['target'], links[ i ]['label'], parentEl );
}

}() );
</script>
<?php
}