alt_text, 'render_page' )
);
add_submenu_page(
self::MENU_SLUG,
__( 'Accessibility statement', 'accessvane' ),
__( 'Statement', 'accessvane' ),
self::CAPABILITY,
self::MENU_SLUG . '-statement',
array( accessvane()->statement, 'render_page' )
);
add_submenu_page(
self::MENU_SLUG,
__( 'Manual checks', 'accessvane' ),
__( 'Manual checks', 'accessvane' ),
self::CAPABILITY,
self::MENU_SLUG . '-manual',
array( $this, 'render_manual' )
);
}
/**
* Whether the current screen belongs to this plugin.
*
* @param string $hook Current admin page hook.
* @return bool
*/
private function is_plugin_screen( $hook ) {
return false !== strpos( (string) $hook, self::MENU_SLUG );
}
/**
* Enqueue admin assets on our screens only.
*
* @param string $hook Current admin page hook.
* @return void
*/
public function assets( $hook ) {
if ( ! $this->is_plugin_screen( $hook ) ) {
return;
}
wp_enqueue_style(
'accessvane-admin',
ACCESSVANE_URL . 'assets/css/admin.css',
array(),
ACCESSVANE_VERSION
);
// axe-core is only needed on the scanning screen, and it is a large
// file — do not make every other screen pay for it.
if ( false !== strpos( $hook, self::MENU_SLUG . '-scan' ) ) {
wp_enqueue_script(
'accessvane-axe',
ACCESSVANE_URL . 'assets/js/axe.js',
array(),
ACCESSVANE_AXE_VERSION,
true
);
// Our own checks, injected into the scanned frame alongside axe.
wp_enqueue_script(
'accessvane-custom-checks',
ACCESSVANE_URL . 'assets/js/custom-checks.js',
array(),
ACCESSVANE_VERSION,
true
);
wp_enqueue_script(
'accessvane-admin',
ACCESSVANE_URL . 'assets/js/admin.js',
array( 'accessvane-axe', 'accessvane-custom-checks', 'wp-api-fetch' ),
ACCESSVANE_VERSION,
true
);
wp_localize_script(
'accessvane-admin',
'accessVane',
array(
'restUrl' => esc_url_raw( rest_url( AccessVane_REST::NAMESPACE_V1 ) ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'homeUrl' => esc_url_raw( home_url() ),
'strings' => array(
'scanning' => __( 'Opening the page...', 'accessvane' ),
'analysing' => __( 'Checking it...', 'accessvane' ),
'saving' => __( 'Saving the result...', 'accessvane' ),
'done' => __( 'Done', 'accessvane' ),
'failedLoad' => __( 'That page could not be loaded for scanning. If a security plugin blocks framing on your own site, allow same-origin framing and try again.', 'accessvane' ),
'failedRun' => __( 'The page loaded but could not be checked. This usually means a plugin is blocking scripts inside the frame.', 'accessvane' ),
'failedSave' => __( 'The result could not be saved. Please reload and try again.', 'accessvane' ),
'timeout' => __( 'That page took too long to load. Try again, or scan a simpler page.', 'accessvane' ),
'confirmDelete' => __( 'Remove this page from the results?', 'accessvane' ),
'selectPage' => __( 'Select at least one page first.', 'accessvane' ),
),
)
);
}
}
/**
* Shared page header.
*
* @param string $title Page title.
* @param string $intro Supporting sentence.
* @return void
*/
private function header( $title, $intro ) {
printf(
'
%s
%s
',
esc_html( $title ),
esc_html( $intro )
);
}
/**
* The honesty note. Shown on every screen that reports a result.
*
* @return void
*/
private function coverage_note() {
?>
= 90 ) {
$state = 'good';
} elseif ( $score >= 70 ) {
$state = 'warn';
} else {
$state = 'bad';
}
printf(
'
%2$d/100',
esc_attr( $state ),
$score // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- cast to int above.
);
}
/**
* Overview screen.
*
* @return void
*/
public function render_dashboard() {
$summary = AccessVane_Storage::summary();
$issues = AccessVane_Catalog::issues();
$this->header(
__( 'Accessibility overview', 'accessvane' ),
__( 'A summary of every page you have scanned, and what to do next.', 'accessvane' )
);
if ( 0 === $summary['pages'] ) {
?>
score_badge( $summary['average_score'] ); ?>
__( 'Critical', 'accessvane' ),
'serious' => __( 'Serious', 'accessvane' ),
'moderate' => __( 'Moderate', 'accessvane' ),
'minor' => __( 'Minor', 'accessvane' ),
);
foreach ( $levels as $key => $label ) {
printf(
'
%2$d%3$s
',
esc_attr( $key ),
(int) $summary[ $key ],
esc_html( $label )
);
}
?>
coverage_note(); ?>
|
|
|
|
|
|
|
score_badge( $result['score'] ); ?> |
|
|
|
sibling->render(); ?>
header(
__( 'Scan pages', 'accessvane' ),
__( 'Pick the pages to check. Each one opens invisibly in this browser tab and is tested there, so nothing is sent to any server and the scan costs nothing to run.', 'accessvane' )
);
$pages = get_posts(
array(
'post_type' => array( 'page', 'post' ),
'post_status' => 'publish',
'numberposts' => 100,
'orderby' => 'menu_order title',
'order' => 'ASC',
'suppress_filters' => false,
)
);
?>
coverage_note(); ?>
ID ) {
return true;
}
$needles = array( 'contact', 'about', 'shop', 'cart', 'checkout', 'product', 'service', 'book', 'appointment' );
$slug = strtolower( $page->post_name );
foreach ( $needles as $needle ) {
if ( false !== strpos( $slug, $needle ) ) {
return true;
}
}
return false;
}
/**
* Repairs screen.
*
* @return void
*/
public function render_fixes() {
$settings = AccessVane_Settings::all();
$this->header(
__( 'Repairs', 'accessvane' ),
__( 'Each repair changes the pages your visitors receive, not a layer on top of them. Everything here is off until you switch it on, and switching it off restores exactly what your theme produced before.', 'accessvane' )
);
if ( isset( $_GET['accessvane_saved'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice flag.
printf(
'',
esc_html__( 'Repairs saved. Open your site in a new tab to see the result.', 'accessvane' )
);
}
$groups = $this->fix_definitions();
?>
__( 'Keyboard access', 'accessvane' ),
'intro' => __( 'The repairs that matter most to anyone who cannot use a mouse.', 'accessvane' ),
'fixes' => array(
'fix_skip_link' => array(
'label' => __( 'Add a "skip to main content" link', 'accessvane' ),
'what' => __( 'Adds a link at the very top of every page that jumps past the header and menu. It stays invisible until someone tabs to it.', 'accessvane' ),
'risk' => __( 'Safe. If the target below does not exist in your theme, the plugin finds your main content area and points the link there instead, so it always lands somewhere real.', 'accessvane' ),
),
'fix_focus_visible' => array(
'label' => __( 'Show a visible outline on the focused element', 'accessvane' ),
'what' => __( 'Draws a clear ring around whatever the keyboard is currently on. Many themes remove this for looks, which leaves keyboard users unable to see where they are.', 'accessvane' ),
'risk' => __( 'Safe. It only appears for keyboard users, never on mouse clicks.', 'accessvane' ),
),
'fix_scrollable_focus' => array(
'label' => __( 'Make scrolling areas reachable by keyboard', 'accessvane' ),
'what' => __( 'Wide tables and code blocks that scroll sideways cannot be scrolled without a mouse. This makes them a keyboard stop.', 'accessvane' ),
'risk' => __( 'Low. It adds extra tab stops on pages with wide tables, which is the intended trade.', 'accessvane' ),
),
),
'options' => array(
array(
'key' => 'skip_link_target',
'type' => 'text',
'label' => __( 'Skip link target', 'accessvane' ),
'placeholder' => '#content',
'help' => __( 'The id of your main content area. Leave it as #content unless you know yours differs; if it is not found, the plugin detects the right one automatically.', 'accessvane' ),
),
array(
'key' => 'skip_link_text',
'type' => 'text',
'label' => __( 'Skip link wording', 'accessvane' ),
'placeholder' => __( 'Skip to main content', 'accessvane' ),
'help' => __( 'Leave blank to use the default.', 'accessvane' ),
),
array(
'key' => 'focus_ring_color',
'type' => 'color',
'label' => __( 'Focus outline colour', 'accessvane' ),
'placeholder' => '#0b63ce',
'help' => __( 'Pick something that stands out against your backgrounds.', 'accessvane' ),
),
array(
'key' => 'focus_ring_width',
'type' => 'number',
'label' => __( 'Focus outline thickness (pixels)', 'accessvane' ),
'placeholder' => '3',
'help' => __( 'Between 1 and 6. Three is a good default.', 'accessvane' ),
),
),
),
array(
'title' => __( 'Reading and seeing', 'accessvane' ),
'intro' => __( 'Repairs for visitors with low vision or colour blindness.', 'accessvane' ),
'fixes' => array(
'fix_viewport_zoom' => array(
'label' => __( 'Allow pinch-zoom on phones', 'accessvane' ),
'what' => __( 'Some themes and speed plugins block zooming. This unblocks it, which is one of the most common and most frustrating failures on mobile.', 'accessvane' ),
'risk' => __( 'Safe. Nothing on your page changes until a visitor chooses to zoom.', 'accessvane' ),
),
'fix_link_underline' => array(
'label' => __( 'Underline links inside your content', 'accessvane' ),
'what' => __( 'Links in paragraphs that are only marked by colour are invisible to colour-blind readers. This underlines them — content links only, not menus or buttons.', 'accessvane' ),
'risk' => __( 'Visible change. Your body-copy links will look different. Turn it on, look at a post, and decide.', 'accessvane' ),
),
),
),
array(
'title' => __( 'Screen reader information', 'accessvane' ),
'intro' => __( 'Repairs that give assistive software the names and structure it needs.', 'accessvane' ),
'fixes' => array(
'fix_image_alt' => array(
'label' => __( 'Supply missing image descriptions', 'accessvane' ),
'what' => __( 'Where an image has no alt text, uses its caption or title instead, and marks it decorative when there is nothing meaningful to say. File names are never used — they describe nothing.', 'accessvane' ),
'risk' => __( 'Safe, but it is a stopgap. Real descriptions written by you are always better — the Image alt text screen makes that quick.', 'accessvane' ),
),
'fix_form_labels' => array(
'label' => __( 'Name form fields that have no label', 'accessvane' ),
'what' => __( 'Gives orphaned fields a name from their placeholder or field name, so a screen reader announces something useful instead of "edit text".', 'accessvane' ),
'risk' => __( 'Low, and also a stopgap. The real fix is a visible label in your form plugin, which sighted visitors need too.', 'accessvane' ),
),
'fix_iframe_titles' => array(
'label' => __( 'Title embedded videos and maps', 'accessvane' ),
'what' => __( 'Names each embed so a screen reader says what it contains instead of just "frame".', 'accessvane' ),
'risk' => __( 'Safe. Nothing visible changes.', 'accessvane' ),
),
'fix_landmark_labels' => array(
'label' => __( 'Tell your menus apart', 'accessvane' ),
'what' => __( 'When a page has several navigation areas, names each one so they are not all announced as simply "navigation".', 'accessvane' ),
'risk' => __( 'Safe. Nothing visible changes.', 'accessvane' ),
),
'fix_lang_attribute' => array(
'label' => __( 'Declare the page language', 'accessvane' ),
'what' => __( 'Makes sure every page states its language, so a screen reader uses the right pronunciation. Without it, English can be read with a French accent.', 'accessvane' ),
'risk' => __( 'Safe. Only applies if your theme has left it out.', 'accessvane' ),
),
),
),
);
}
/**
* Save the repairs form.
*
* @return void
*/
public function handle_save_fixes() {
if ( ! current_user_can( self::CAPABILITY ) ) {
wp_die( esc_html__( 'You are not allowed to change these settings.', 'accessvane' ), 403 );
}
check_admin_referer( 'accessvane_save_fixes' );
// Values are sanitised field by field inside AccessVane_Settings::save().
$input = wp_unslash( $_POST ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
// Preserve settings that this form does not present.
$existing = AccessVane_Settings::all();
$input = array_merge(
array(
'statement_page_id' => $existing['statement_page_id'],
),
$input
);
AccessVane_Settings::save( $input );
wp_safe_redirect(
add_query_arg(
array(
'page' => self::MENU_SLUG . '-fixes',
'accessvane_saved' => '1',
),
admin_url( 'admin.php' )
)
);
exit;
}
/**
* Manual checks screen.
*
* @return void
*/
public function render_manual() {
$this->header(
__( 'Manual checks', 'accessvane' ),
__( 'The problems no scanner can find. Work through these and you will have covered what automated testing cannot reach.', 'accessvane' )
);
?>
-