diff --git a/.wporg-assets/screenshot-1.png b/.wporg-assets/screenshot-1.png
index 59294901..afdbfd46 100644
Binary files a/.wporg-assets/screenshot-1.png and b/.wporg-assets/screenshot-1.png differ
diff --git a/.wporg-assets/screenshot-2.png b/.wporg-assets/screenshot-2.png
index fba4cb90..a6c74ff5 100644
Binary files a/.wporg-assets/screenshot-2.png and b/.wporg-assets/screenshot-2.png differ
diff --git a/.wporg-assets/screenshot-3.png b/.wporg-assets/screenshot-3.png
index f020fc58..f6b2eca2 100644
Binary files a/.wporg-assets/screenshot-3.png and b/.wporg-assets/screenshot-3.png differ
diff --git a/.wporg-assets/screenshot-4.png b/.wporg-assets/screenshot-4.png
index 088d3354..4d9180c9 100644
Binary files a/.wporg-assets/screenshot-4.png and b/.wporg-assets/screenshot-4.png differ
diff --git a/.wporg-assets/screenshot-5.png b/.wporg-assets/screenshot-5.png
index bc44bf9f..429c2782 100644
Binary files a/.wporg-assets/screenshot-5.png and b/.wporg-assets/screenshot-5.png differ
diff --git a/.wporg-assets/screenshot-6.png b/.wporg-assets/screenshot-6.png
new file mode 100644
index 00000000..51749ad1
Binary files /dev/null and b/.wporg-assets/screenshot-6.png differ
diff --git a/.wporg-assets/screenshot-7.png b/.wporg-assets/screenshot-7.png
new file mode 100644
index 00000000..1bfd5b18
Binary files /dev/null and b/.wporg-assets/screenshot-7.png differ
diff --git a/README.md b/README.md
index dc82ad68..b6e75ea0 100644
--- a/README.md
+++ b/README.md
@@ -5,13 +5,15 @@
**Requires at least:** 6.6 \
**Tested up to:** 6.8 \
**Requires PHP:** 7.4 \
-**Stable tag:** 3.6.0 \
+**Stable tag:** 3.8.0 \
**License:** GPLv2 or later
Ally: Make your site more inclusive by scanning for accessibility violations, fixing them easily, and adding a usability widget and accessibility statement.
## Description
+https://www.youtube.com/watch?v=-2ig5D348vo
+
Ally (formerly One Click Accessibility) is a free, powerful, and user-friendly plugin that helps WordPress creators build more accessible websites with ease.
It simplifies accessibility with three essential tools:
@@ -194,29 +196,53 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro
## Screenshots
-### 1. Design: Customize the button's icon, size, and color and widget branding.
+### 1. Usability widget: Large, well organized controls let visitors adjust text, contrast, animations, and more for a more comfortable browsing experience.
+
+
+
+### 2. Accessibility button: Add a button that opens the accessibility menu in one click, and tailor its icon, color, label, and placement to match your brand.
+
+
+
+### 3. Accessibility statement: Quickly generate and publish a custom statement that signals your commitment, improves transparency, and offers a clear way to report issues.
-
+
-### 2. Position Settings: Set widget placement or hide it on desktop and mobile.
+### 4. Accessibility Assistant: Scan any page to instantly detect over 180 common accessibility issues and get clear, guided steps for remediation inside your site editor.
-
+
-### 3. Capabilities: Toggle accessibility features on or off as needed.
+### 5. Scan results: View issues highlighted in context and grouped by type such as alt text, ARIA, page structure, and more. Expand any item for step-by-step guidance and optional AI-powered suggestions.
-
+
-### 4. Accessibility Statement: Add or create a custom accessibility statement.
+### 6. Color contrast: Fine tune text and background colors with live checks that validate contrast ratios.
-
+
-### 5. Widget on Site: This is how the accessibility widget appears on a live website.
+### 7. Scanner dashboard: Track your site’s accessibility scans, monitor open issues, and follow progress over time.
-
+
## Changelog
+### 3.8.0 – 2025-09-29
+
+* New: Headings Remediation Management - Review and correct heading hierarchy with a guided flow
+* Tweak: Improved AI remediation results with added context for clearer responses
+* Fix: Disabled remediation action not working
+* Fix: Quota number displayed incorrectly for Standard plan
+
+### 3.7.0 - 2025-09-02
+
+* New: Redesigned Accessibility Widget – Clearer structure, Wider accessible buttons, and Improved mobile view
+* New: Intro banner for users who connected Ally
+* New: Reviews & CSAT flow to gather user feedback
+* Tweak: Added cache clearing option in Assistant panel and WordPress Admin Bar
+* Tweak: Enhanced color contrast evaluation to fix issues with gradient/video backgrounds
+* Fix: found issue count wrong in edge cases
+
### 3.6.0 - 2025-08-02
* New: Smart color contrast remediation flow in the accessibility assistant
@@ -226,7 +252,6 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro
* Fix: Added WPML compatibility
* Fix: WooCommerce AJAX conflict
-
### 3.5.2 - 2025-07-28
* Tweak: Improved performance by enqueuing Assistant only when logged in
diff --git a/assets/dev/js/services/mixpanel/mixpanel-events.js b/assets/dev/js/services/mixpanel/mixpanel-events.js
index 689ee84b..de113e44 100644
--- a/assets/dev/js/services/mixpanel/mixpanel-events.js
+++ b/assets/dev/js/services/mixpanel/mixpanel-events.js
@@ -69,4 +69,8 @@ export const mixpanelEvents = {
feedbackSubmitted: 'review_feedback_submitted',
publicRedirectClicked: 'review_public_redirect_clicked',
},
+
+ // Heading Structure
+ headingClicked: 'heading_clicked',
+ headingSelected: 'heading_selected',
};
diff --git a/assets/dev/js/utils/inject-template-vars.js b/assets/dev/js/utils/inject-template-vars.js
new file mode 100644
index 00000000..3a67ddcc
--- /dev/null
+++ b/assets/dev/js/utils/inject-template-vars.js
@@ -0,0 +1,21 @@
+import { createElement } from '@wordpress/element';
+
+export const injectTemplateVars = (message, components) => {
+ const regex = /\{\{(\w+)\}\}([^]*?)\{\{\/\1\}\}/g;
+ const splitMessage = message.split(regex);
+
+ // eslint-disable-next-line array-callback-return
+ return splitMessage.map((part, index) => {
+ if (index % 3 === 0) {
+ return part;
+ }
+
+ if (index % 3 === 1) {
+ return createElement(
+ components[part],
+ { key: index },
+ splitMessage[index + 1],
+ );
+ }
+ });
+};
diff --git a/modules/connect/classes/data.php b/modules/connect/classes/data.php
index ad1b758f..02cb08e9 100644
--- a/modules/connect/classes/data.php
+++ b/modules/connect/classes/data.php
@@ -191,7 +191,8 @@ public static function get_refresh_token() {
public static function get_home_url() {
$raw = self::get_connect_mode_data( self::HOME_URL, false );
$is_base64 = base64_encode( base64_decode( $raw, true ) ) === $raw;
- return $is_base64 ? base64_decode( $raw ) : $raw;
+ $url = $is_base64 ? base64_decode( $raw ) : $raw;
+ return apply_filters( 'ally_connect_home_url', $url );
}
/**
diff --git a/modules/core/components/skip-link.php b/modules/core/components/skip-link.php
index 783c755a..2babb0f3 100644
--- a/modules/core/components/skip-link.php
+++ b/modules/core/components/skip-link.php
@@ -71,6 +71,7 @@ public function __construct() {
if ( $this->settings && ! empty( $this->settings['enabled'] ) ) {
remove_action( 'wp_footer', 'the_block_template_skip_link' );
+ add_filter( 'hello_elementor_enable_skip_link', '__return_false' );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_skip_link_styles' ] );
add_action( 'wp_body_open', [ $this, 'render_skip_link' ] );
diff --git a/modules/remediation/classes/utils.php b/modules/remediation/classes/utils.php
index ecf5053e..bcd27edb 100644
--- a/modules/remediation/classes/utils.php
+++ b/modules/remediation/classes/utils.php
@@ -122,7 +122,11 @@ public static function get_current_object_type_name() : string {
}
if ( $wp_query->is_post_type_archive() ) {
- return $wp_query->query_vars['post_type'] ?? 'unknown';
+ $post_type = $wp_query->query_vars['post_type'];
+ if ( is_array( $post_type ) ) {
+ $post_type = implode( '_', $post_type );
+ }
+ return $post_type ?? 'unknown';
}
if ( $wp_query->is_singular() ) {
diff --git a/modules/remediation/components/remediation-runner.php b/modules/remediation/components/remediation-runner.php
index 9fa99f42..c858f656 100644
--- a/modules/remediation/components/remediation-runner.php
+++ b/modules/remediation/components/remediation-runner.php
@@ -148,7 +148,7 @@ private function should_run_remediation(): bool {
]);
$this->page_html = $this->page->get_page_html();
- $this->page_remediations = Remediation_Entry::get_page_remediations( $current_url );
+ $this->page_remediations = Remediation_Entry::get_page_active_remediations( $current_url );
$status = $this->page->__get( Page_Table::STATUS );
if ( empty( $this->page_remediations ) || Page_Table::STATUSES['ACTIVE'] !== $status ) {
diff --git a/modules/remediation/database/remediation-entry.php b/modules/remediation/database/remediation-entry.php
index 37c927fc..7102c998 100644
--- a/modules/remediation/database/remediation-entry.php
+++ b/modules/remediation/database/remediation-entry.php
@@ -79,6 +79,30 @@ public static function get_page_remediations( string $url, bool $total = false )
return Remediation_Table::select( $select, $where );
}
+ /**
+ * get_page_active_remediations
+ *
+ * @param string $url
+ * @return array
+ */
+ public static function get_page_active_remediations( string $url ) : array {
+ $where = [
+ [
+ 'column' => Remediation_Table::URL,
+ 'value' => $url,
+ 'operator' => '=',
+ 'relation_after' => 'AND',
+ ],
+ [
+ 'column' => Remediation_Table::ACTIVE,
+ 'value' => 1,
+ 'operator' => '=',
+ ],
+ ];
+
+ return Remediation_Table::select( '*', $where );
+ }
+
public static function get_all_remediations( int $period ) : array {
$date_threshold = gmdate( 'Y-m-d', strtotime( "-{$period} days" ) ) . ' 00:00:00';
diff --git a/modules/remediation/module.php b/modules/remediation/module.php
index 4de0999a..1c03c97a 100644
--- a/modules/remediation/module.php
+++ b/modules/remediation/module.php
@@ -28,6 +28,8 @@ public static function routes_list(): array {
'Items',
'Item',
'Trigger_Save',
+ 'Heading_Level',
+ 'Dismiss_Heading_Issue',
'Clear_Cache',
];
}
diff --git a/modules/remediation/rest/dismiss-heading-issue.php b/modules/remediation/rest/dismiss-heading-issue.php
new file mode 100644
index 00000000..d161a210
--- /dev/null
+++ b/modules/remediation/rest/dismiss-heading-issue.php
@@ -0,0 +1,69 @@
+verify_capability();
+
+ if ( $error ) {
+ return $error;
+ }
+
+ $post_id = (int) sanitize_text_field( $request->get_param( 'pageId' ) );
+ $xpath = sanitize_text_field( $request->get_param( 'xpath' ) );
+
+ if ( ! $post_id || ! $xpath ) {
+ return $this->respond_error_json( [
+ 'message' => 'Missing required parameters',
+ 'code' => 'missing_parameters',
+ ] );
+ }
+
+ $data = get_post_meta( $post_id, self::META_KEY, true ) ?? [];
+
+ if ( ! $data ) {
+ $data = [];
+ }
+
+ $data[] = $xpath;
+
+ update_post_meta( $post_id, self::META_KEY, array_unique( $data ) );
+
+ return $this->respond_success_json( [
+ 'message' => 'Heading issue dismissed',
+ ] );
+ } catch ( Throwable $t ) {
+ return $this->respond_error_json( [
+ 'message' => $t->getMessage(),
+ 'code' => 'internal_server_error',
+ ] );
+ }
+ }
+}
diff --git a/modules/remediation/rest/heading-level.php b/modules/remediation/rest/heading-level.php
new file mode 100644
index 00000000..a1d0e13e
--- /dev/null
+++ b/modules/remediation/rest/heading-level.php
@@ -0,0 +1,123 @@
+verify_capability();
+
+ if ( $error ) {
+ return $error;
+ }
+
+ $url = esc_url( $request->get_param( 'url' ) );
+ $level = sanitize_text_field( $request->get_param( 'level' ) );
+ $xpath = sanitize_text_field( $request->get_param( 'xpath' ) );
+ $rule = sanitize_text_field( $request->get_param( 'rule' ) ) ?? '';
+
+ // Remove existing remediations for this xpath before creating new ones
+ $this->remove_existing_remediations( $url, $xpath );
+
+ if ( 'p' === $level ) {
+ $this
+ ->create_remediation_entry( $url, $rule, [
+ 'xpath' => $xpath,
+ 'attribute_name' => 'role',
+ 'attribute_value' => 'presentation',
+ ] )
+ ->save();
+ } else {
+ $this
+ ->create_remediation_entry( $url, $rule, [
+ 'xpath' => $xpath,
+ 'attribute_name' => 'role',
+ 'attribute_value' => 'heading',
+ ] )
+ ->save();
+
+ $this
+ ->create_remediation_entry( $url, $rule, [
+ 'xpath' => $xpath,
+ 'attribute_name' => 'aria-level',
+ 'attribute_value' => trim( $level, 'h' ),
+ ] )
+ ->save();
+ }
+
+ Page_Entry::clear_cache( $url );
+
+ return $this->respond_success_json( [
+ 'message' => 'Remediation added',
+ ] );
+ } catch ( Throwable $t ) {
+ return $this->respond_error_json( [
+ 'message' => $t->getMessage(),
+ 'code' => 'internal_server_error',
+ ] );
+ }
+ }
+
+ private function remove_existing_remediations( string $url, string $xpath ): void {
+ $existing_entries = Remediation_Entry::get_page_remediations( $url );
+
+ foreach ( $existing_entries as $entry ) {
+ if ( isset( $entry->group ) && 'headingStructure' === $entry->group ) {
+ $content = json_decode( $entry->content, true );
+
+ if ( isset( $content['xpath'] ) &&
+ $content['xpath'] === $xpath &&
+ isset( $content['type'] ) &&
+ 'ATTRIBUTE' === $content['type'] &&
+ isset( $content['attribute_name'] ) &&
+ in_array( $content['attribute_name'], [ 'role', 'aria-level' ], true )
+ ) {
+ Remediation_Table::delete( [ 'id' => $entry->id ] );
+ }
+ }
+ }
+ }
+
+ private function create_remediation_entry( string $url, string $rule, array $data ): Remediation_Entry {
+ return new Remediation_Entry( [
+ 'data' => [
+ Remediation_Table::URL => $url,
+ Remediation_Table::CATEGORY => 'A',
+ Remediation_Table::RULE => $rule,
+ Remediation_Table::GROUP => 'headingStructure',
+ Remediation_Table::CONTENT => wp_json_encode( array_merge( [
+ 'category' => 'A',
+ 'type' => 'ATTRIBUTE',
+ 'action' => 'update',
+ ], $data ) ),
+ Remediation_Table::ACTIVE => true,
+ ],
+ ] );
+ }
+}
diff --git a/modules/scanner/assets/js/api/APIScanner.js b/modules/scanner/assets/js/api/APIScanner.js
index 294b43fb..2b0130b0 100644
--- a/modules/scanner/assets/js/api/APIScanner.js
+++ b/modules/scanner/assets/js/api/APIScanner.js
@@ -120,6 +120,22 @@ export class APIScanner extends API {
});
}
+ static async setHeadingLevel(data) {
+ return APIScanner.request({
+ method: 'POST',
+ path: `${v1Prefix}/remediation/heading-level`,
+ data,
+ });
+ }
+
+ static async dismissHeadingIssue(data) {
+ return APIScanner.request({
+ method: 'POST',
+ path: `${v1Prefix}/remediation/dismiss-heading-issue`,
+ data,
+ });
+ }
+
static async clearCache(data) {
return APIScanner.request({
method: 'DELETE',
diff --git a/modules/scanner/assets/js/app.js b/modules/scanner/assets/js/app.js
index 9e6c3eda..758eef4f 100644
--- a/modules/scanner/assets/js/app.js
+++ b/modules/scanner/assets/js/app.js
@@ -1,9 +1,10 @@
import ErrorBoundary from '@elementor/ui/ErrorBoundary';
+import { FocusTrap } from 'focus-trap-react';
import { Notifications } from '@ea11y/components';
import { useNotificationSettings } from '@ea11y-apps/global/hooks/use-notifications';
import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services';
import { ErrorMessage } from '@ea11y-apps/scanner/components/error-message';
-import { Header } from '@ea11y-apps/scanner/components/header';
+import Header from '@ea11y-apps/scanner/components/header';
import { Loader } from '@ea11y-apps/scanner/components/list-loader';
import { NotConnectedMessage } from '@ea11y-apps/scanner/components/not-connected-message';
import { QuotaMessage } from '@ea11y-apps/scanner/components/quota-message';
@@ -18,9 +19,10 @@ import {
RemediationLayout,
} from '@ea11y-apps/scanner/layouts';
import { ColorContrastLayout } from '@ea11y-apps/scanner/layouts/color-contrast-layout';
+import { HeadingStructureLayout } from '@ea11y-apps/scanner/layouts/heading-structure-layout';
import { AppContainer } from '@ea11y-apps/scanner/styles/app.styles';
import { removeExistingFocus } from '@ea11y-apps/scanner/utils/focus-on-element';
-import { useEffect } from '@wordpress/element';
+import { useEffect, useRef } from '@wordpress/element';
const App = () => {
const { notificationMessage, notificationType } = useNotificationSettings();
@@ -34,6 +36,7 @@ const App = () => {
quotaExceeded,
loading,
} = useScannerWizardContext();
+ const containerRef = useRef(null);
const showResolvedMessage = Boolean(
(resolved > 0 && violation === resolved) || violation === 0,
@@ -85,21 +88,31 @@ const App = () => {
return ;
case BLOCKS.colorContrast:
return ;
+ case BLOCKS.headingStructure:
+ return ;
default:
return isManage ? : ;
}
};
return (
-
- }>
-
+
+
+ }>
+
- {showResolvedMessage && !isManage ? : getBlock()}
+ {showResolvedMessage && !isManage ? : getBlock()}
-
-
-
+
+
+
+
);
};
diff --git a/modules/scanner/assets/js/components/block-button/index.js b/modules/scanner/assets/js/components/block-button/index.js
index 2b24ce66..f6725056 100644
--- a/modules/scanner/assets/js/components/block-button/index.js
+++ b/modules/scanner/assets/js/components/block-button/index.js
@@ -1,6 +1,6 @@
import CircleCheckFilledIcon from '@elementor/icons/CircleCheckFilledIcon';
-import { Chip } from '@elementor/ui';
import Box from '@elementor/ui/Box';
+import Chip from '@elementor/ui/Chip';
import Typography from '@elementor/ui/Typography';
import PropTypes from 'prop-types';
import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services';
diff --git a/modules/scanner/assets/js/components/block-button/manage-button.js b/modules/scanner/assets/js/components/block-button/manage-button.js
index 0fa5d259..a36de740 100644
--- a/modules/scanner/assets/js/components/block-button/manage-button.js
+++ b/modules/scanner/assets/js/components/block-button/manage-button.js
@@ -22,7 +22,7 @@ export const ManageButton = ({ title, count, block }) => {
mixpanelService.sendEvent(mixpanelEvents.categoryClicked, {
page_url: window.ea11yScannerData?.pageData?.url,
issue_count: count,
- category_name: title,
+ category_name: block,
source: 'remediation',
});
};
diff --git a/modules/scanner/assets/js/components/block-button/resolve-chip.js b/modules/scanner/assets/js/components/block-button/resolve-chip.js
index 99dc3e2c..22769804 100644
--- a/modules/scanner/assets/js/components/block-button/resolve-chip.js
+++ b/modules/scanner/assets/js/components/block-button/resolve-chip.js
@@ -1,4 +1,4 @@
-import { Chip } from '@elementor/ui';
+import Chip from '@elementor/ui/Chip';
import PropTypes from 'prop-types';
import { BLOCKS } from '@ea11y-apps/scanner/constants';
import { __ } from '@wordpress/i18n';
diff --git a/modules/scanner/assets/js/components/header/dropdown-menu.js b/modules/scanner/assets/js/components/header/dropdown-menu.js
index 117505f9..2628f507 100644
--- a/modules/scanner/assets/js/components/header/dropdown-menu.js
+++ b/modules/scanner/assets/js/components/header/dropdown-menu.js
@@ -4,6 +4,7 @@ import DotsHorizontalIcon from '@elementor/icons/DotsHorizontalIcon';
import ExternalLinkIcon from '@elementor/icons/ExternalLinkIcon';
import RefreshIcon from '@elementor/icons/RefreshIcon';
import SettingsIcon from '@elementor/icons/SettingsIcon';
+import ThemeBuilderIcon from '@elementor/icons/ThemeBuilderIcon';
import Box from '@elementor/ui/Box';
import IconButton from '@elementor/ui/IconButton';
import Menu from '@elementor/ui/Menu';
@@ -18,12 +19,19 @@ import { APIScanner } from '@ea11y-apps/scanner/api/APIScanner';
import { BLOCKS } from '@ea11y-apps/scanner/constants';
import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context';
import { DisabledMenuItemText } from '@ea11y-apps/scanner/styles/app.styles';
+import { areNoHeadingsDefined } from '@ea11y-apps/scanner/utils/page-headings';
import { useRef, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
export const DropdownMenu = () => {
- const { remediations, isManage, setOpenedBlock, setIsManage, runNewScan } =
- useScannerWizardContext();
+ const {
+ remediations,
+ isManage,
+ openedBlock,
+ setOpenedBlock,
+ setIsManage,
+ runNewScan,
+ } = useScannerWizardContext();
const { error } = useToastNotification();
const [isOpened, setIsOpened] = useState(false);
const [loading, setLoading] = useState(false);
@@ -68,6 +76,12 @@ export const DropdownMenu = () => {
sendOnClickEvent('Manage fixes');
};
+ const goToHeadingManager = () => {
+ handleClose();
+ setIsManage(true);
+ setOpenedBlock(BLOCKS.headingStructure);
+ };
+
return (
{
) : (
)}
+ {!areNoHeadingsDefined() && (
+
+ )}
+