From b736656be11ee881b41de394147544eb064d20a4 Mon Sep 17 00:00:00 2001
From: Aviran Levi <143411291+aviranLevi1@users.noreply.github.com>
Date: Tue, 11 Nov 2025 14:02:58 +0200
Subject: [PATCH 1/2] Release/v3.9.0 (#428)
---
.../dev/js/components/confirm-dialog/index.js | 2 +-
.../dev/js/components/notifications/index.js | 2 +-
.../dev}/js/icons/crown-filled.js | 0
.../js/services/mixpanel/mixpanel-events.js | 5 +-
assets/dev/js/utils/color-contrast-helpers.js | 94 ++
composer.json | 3 +-
modules/remediation/actions/attribute.php | 7 +-
modules/remediation/actions/replace.php | 11 +-
modules/remediation/actions/styles.php | 138 ++-
.../assets/js/actions/attribute.js | 6 +-
modules/remediation/assets/js/actions/base.js | 64 +-
.../remediation/assets/js/actions/replace.js | 8 +-
.../remediation/assets/js/actions/styles.js | 127 ++-
modules/remediation/assets/js/module.js | 5 +-
.../remediation/classes/remediation-base.php | 90 +-
.../components/remediation-runner.php | 26 +-
.../global-remediation-relationship-entry.php | 108 ++
.../global-remediation-relationship-table.php | 74 ++
.../database/remediation-entry.php | 162 ++-
.../database/remediation-table.php | 11 +-
modules/remediation/module.php | 11 +-
modules/remediation/rest/global-item.php | 150 +++
.../remediation/rest/global-items-group.php | 100 ++
modules/remediation/rest/global-items.php | 126 +++
modules/remediation/rest/heading-level.php | 3 +-
modules/remediation/rest/item.php | 7 +-
.../assets/src/components/dismiss-button.js | 46 +-
.../assets/src/components/feedback-form.js | 16 +-
.../assets/src/components/rating-form.js | 33 +-
.../assets/src/components/review-form.js | 59 +-
.../reviews/assets/src/hooks/use-settings.js | 120 ++-
.../assets/src/layouts/user-feedback-form.js | 116 +-
modules/reviews/module.php | 33 +
modules/scanner/assets/js/api/APIScanner.js | 72 ++
modules/scanner/assets/js/app.js | 84 +-
.../js/components/alt-text-form/index.js | 77 +-
.../js/components/block-button/button-menu.js | 126 +++
.../block-button/global-button-menu.js | 217 ++++
.../js/components/block-button/index.js | 6 +-
.../components/block-button/manage-button.js | 47 +-
.../components/color-contrast-form/index.js | 85 +-
.../components/empty-manage-message/index.js | 41 +
.../js/components/form-navigation/index.js | 69 +-
.../{header-container.js => app-header.js} | 35 +-
.../js/components/header/dropdown-menu.js | 83 +-
.../assets/js/components/header/index.js | 68 +-
.../js/components/header/stats/index.js | 19 -
.../header/stats/management-stats.js | 41 +-
.../header/subheader/breadcrumbs.js | 28 +-
.../subheader/heading-structure/index.js | 17 +-
.../js/components/header/subheader/index.js | 11 +-
.../title/{main-title.js => app-title.js} | 6 +-
.../js/components/header/title/index.js | 21 -
.../header/title/manage-fixes-title.js | 17 -
.../header/title/manage-headings-title.js | 27 -
.../assets/js/components/main-list/index.js | 2 +-
.../js/components/manage-alt-text/index.js | 84 ++
.../manage-color-contrast/color-data.js | 42 +
.../components/manage-color-contrast/index.js | 86 ++
.../global/delete-global-item.js | 55 +
.../global/manage-global-actions.js | 97 ++
.../components/manage-footer-actions/index.js | 12 +
.../page/manage-page-actions.js | 97 ++
.../page/set-global-remediation-modal.js | 45 +
.../manage-footer-actions/page/set-global.js | 177 ++++
.../js/components/manage-item-header/index.js | 73 ++
.../assets/js/components/manage-list/index.js | 31 -
.../disable-button.js | 29 -
.../manage-remediation-list/index.js | 43 +
.../global/delete-global-button.js | 59 ++
.../global/global-remediation-control-menu.js | 126 +++
.../manage-global-remediation-control.js | 38 +
.../manage-remediation-main-controls/index.js | 2 +
.../page}/delete-button.js | 41 +-
.../page/disable-button.js | 22 +
.../page}/enable-button.js | 14 +-
.../page/manage-remediation-control.js} | 22 +-
.../js/components/manual-fix-form/index.js | 60 +-
.../manual-fix-form/resolve-with-ai.js | 223 ++--
.../js/components/quota-message/index.js | 2 +-
.../delete-global-remediation-modal.js | 64 ++
.../delete-page-remediation-modal.js} | 28 +-
.../disable-global-remediation-modal.js | 54 +
.../enable-global-remediation-modal.js | 54 +
.../remediation-confirmation-modal/index.js | 4 +
.../js/components/remediation-form/index.js | 65 +-
.../remediation-form/remediation-snippet.js | 190 ++--
.../js/components/resolved-message/index.js | 16 +-
.../upgrade-info-tip/upgrade-content.js | 14 +-
modules/scanner/assets/js/constants/index.js | 4 +-
.../js/context/scanner-wizard-context.js | 401 +------
.../scanner/assets/js/context/tabs-context.js | 49 +
.../useScannerWizardActions.js | 254 +++++
.../useScannerWizardEffects.js | 95 ++
.../scanner-context/useScannerWizardState.js | 90 ++
.../assets/js/hooks/use-alt-text-form.js | 159 ++-
.../js/hooks/use-color-contrast-form.js | 267 ++---
.../js/hooks/use-global-manage-actions.js | 234 ++++
.../assets/js/hooks/use-manage-actions.js | 23 +-
.../assets/js/hooks/use-manual-fix-form.js | 99 +-
.../assets/js/hooks/use-scanner-settings.js | 29 +-
.../scanner/assets/js/images/empty-image.js | 67 ++
modules/scanner/assets/js/images/index.js | 1 +
modules/scanner/assets/js/index.js | 29 +-
modules/scanner/assets/js/layouts/index.js | 14 +-
.../assets/js/layouts/manage-main-layout.js | 47 -
.../management/manage-alt-text-layout.js | 75 ++
.../manage-color-contrast-layout.js | 98 ++
.../layouts/management/manage-main-layout.js | 120 +++
.../manage-manual-layout.js} | 37 +-
.../layouts/{ => scanner}/alt-text-layout.js | 4 +-
.../{ => scanner}/color-contrast-layout.js | 2 +-
.../{ => scanner}/heading-structure-layout.js | 0
.../js/layouts/{ => scanner}/main-layout.js | 0
.../js/layouts/{ => scanner}/manual-layout.js | 15 +-
.../assets/js/services/scanner-wizard.js | 4 +-
.../assets/js/static/global-infotip-image.png | Bin 0 -> 94429 bytes
.../scanner/assets/js/styles/app.styles.js | 69 +-
.../assets/js/styles/manual-fixes.styles.js | 20 +-
.../scanner/assets/js/types/scanner-item.js | 4 +
.../assets/js/utils/build-path-to-parent.js | 26 +
.../assets/js/utils/calc-color-ratio.js | 2 +
.../scanner/assets/js/utils/convert-colors.js | 30 -
.../assets/js/utils/focus-on-element.js | 17 -
.../js/utils/get-element-css-selector.js | 4 +-
.../assets/js/utils/get-initial-tab.js | 7 +
.../js/utils/get-outer-html-by-xpath.js | 44 +
.../assets/js/utils/validate-headings.js | 4 +-
modules/scanner/rest/scanner-results.php | 4 +-
modules/settings/assets/js/app.js | 4 +-
.../js/components/analytics/charts-list.js | 3 +-
.../components/analytics/charts/line-chart.js | 2 +-
.../capabilities-item/pro-item-infotip.js | 2 +-
.../js/components/connect-modal/check-icon.js | 2 +-
.../assets/js/components/copy-link/index.js | 2 +-
.../assets/js/components/edit-link/index.js | 2 +-
.../settings/assets/js/components/index.js | 1 -
.../media-uploader/media-uploader.js | 2 +-
.../js/components/my-account-menu/index.js | 12 +-
.../components/my-account-menu/popup-menu.js | 8 +-
.../assets/js/components/quota-bar/index.js | 5 +-
.../components/quota-bar/quota-bar-group.js | 3 +-
.../components/quota-bar/quota-indicator.js | 6 +-
.../js/components/sidebar-menu/menu-item.js | 2 +-
.../assets/js/components/sidebar-menu/menu.js | 12 +-
.../tooltips/accessibility-statement.js | 2 +-
.../sidebar-menu/tooltips/analytics.js | 2 +-
.../js/components/sitemap-settings/index.js | 2 +-
.../skip-to-content-settings/index.js | 2 +-
.../js/components/upgrade-modal/index.js | 2 +-
.../assets/js/layouts/position-settings.js | 3 +-
.../settings/assets/js/layouts/quota-bar.js | 2 +-
modules/settings/assets/js/layouts/sidebar.js | 2 +-
.../assets/js/layouts/top-bar-menu.js | 4 +-
.../js/pages/accessibility-statement.js | 2 +-
.../assets/js/pages/assistant/stats/index.js | 2 +-
.../settings/banners/bf-sale-2025-banner.php | 172 +++
.../banners/images/bf-2025-banner.png | Bin 0 -> 55061 bytes
modules/settings/module.php | 2 +
package-lock.json | 997 +++++++++---------
package.json | 5 +-
pojo-accessibility.php | 4 +-
readme.txt | 10 +-
163 files changed, 6376 insertions(+), 2286 deletions(-)
rename {modules/settings/assets => assets/dev}/js/icons/crown-filled.js (100%)
create mode 100644 assets/dev/js/utils/color-contrast-helpers.js
create mode 100644 modules/remediation/database/global-remediation-relationship-entry.php
create mode 100644 modules/remediation/database/global-remediation-relationship-table.php
create mode 100644 modules/remediation/rest/global-item.php
create mode 100644 modules/remediation/rest/global-items-group.php
create mode 100644 modules/remediation/rest/global-items.php
create mode 100644 modules/scanner/assets/js/components/block-button/button-menu.js
create mode 100644 modules/scanner/assets/js/components/block-button/global-button-menu.js
create mode 100644 modules/scanner/assets/js/components/empty-manage-message/index.js
rename modules/scanner/assets/js/components/header/{header-container.js => app-header.js} (70%)
delete mode 100644 modules/scanner/assets/js/components/header/stats/index.js
rename modules/scanner/assets/js/components/header/title/{main-title.js => app-title.js} (78%)
delete mode 100644 modules/scanner/assets/js/components/header/title/index.js
delete mode 100644 modules/scanner/assets/js/components/header/title/manage-fixes-title.js
delete mode 100644 modules/scanner/assets/js/components/header/title/manage-headings-title.js
create mode 100644 modules/scanner/assets/js/components/manage-alt-text/index.js
create mode 100644 modules/scanner/assets/js/components/manage-color-contrast/color-data.js
create mode 100644 modules/scanner/assets/js/components/manage-color-contrast/index.js
create mode 100644 modules/scanner/assets/js/components/manage-footer-actions/global/delete-global-item.js
create mode 100644 modules/scanner/assets/js/components/manage-footer-actions/global/manage-global-actions.js
create mode 100644 modules/scanner/assets/js/components/manage-footer-actions/index.js
create mode 100644 modules/scanner/assets/js/components/manage-footer-actions/page/manage-page-actions.js
create mode 100644 modules/scanner/assets/js/components/manage-footer-actions/page/set-global-remediation-modal.js
create mode 100644 modules/scanner/assets/js/components/manage-footer-actions/page/set-global.js
create mode 100644 modules/scanner/assets/js/components/manage-item-header/index.js
delete mode 100644 modules/scanner/assets/js/components/manage-list/index.js
delete mode 100644 modules/scanner/assets/js/components/manage-remediation-buttons/disable-button.js
create mode 100644 modules/scanner/assets/js/components/manage-remediation-list/index.js
create mode 100644 modules/scanner/assets/js/components/manage-remediation-main-controls/global/delete-global-button.js
create mode 100644 modules/scanner/assets/js/components/manage-remediation-main-controls/global/global-remediation-control-menu.js
create mode 100644 modules/scanner/assets/js/components/manage-remediation-main-controls/global/manage-global-remediation-control.js
create mode 100644 modules/scanner/assets/js/components/manage-remediation-main-controls/index.js
rename modules/scanner/assets/js/components/{manage-remediation-buttons => manage-remediation-main-controls/page}/delete-button.js (52%)
create mode 100644 modules/scanner/assets/js/components/manage-remediation-main-controls/page/disable-button.js
rename modules/scanner/assets/js/components/{manage-remediation-buttons => manage-remediation-main-controls/page}/enable-button.js (58%)
rename modules/scanner/assets/js/components/{manage-remediation-buttons/index.js => manage-remediation-main-controls/page/manage-remediation-control.js} (54%)
create mode 100644 modules/scanner/assets/js/components/remediation-confirmation-modal/delete-global-remediation-modal.js
rename modules/scanner/assets/js/components/{delete-remediation-modal/index.js => remediation-confirmation-modal/delete-page-remediation-modal.js} (61%)
create mode 100644 modules/scanner/assets/js/components/remediation-confirmation-modal/disable-global-remediation-modal.js
create mode 100644 modules/scanner/assets/js/components/remediation-confirmation-modal/enable-global-remediation-modal.js
create mode 100644 modules/scanner/assets/js/components/remediation-confirmation-modal/index.js
create mode 100644 modules/scanner/assets/js/context/tabs-context.js
create mode 100644 modules/scanner/assets/js/hooks/scanner-context/useScannerWizardActions.js
create mode 100644 modules/scanner/assets/js/hooks/scanner-context/useScannerWizardEffects.js
create mode 100644 modules/scanner/assets/js/hooks/scanner-context/useScannerWizardState.js
create mode 100644 modules/scanner/assets/js/hooks/use-global-manage-actions.js
create mode 100644 modules/scanner/assets/js/images/empty-image.js
delete mode 100644 modules/scanner/assets/js/layouts/manage-main-layout.js
create mode 100644 modules/scanner/assets/js/layouts/management/manage-alt-text-layout.js
create mode 100644 modules/scanner/assets/js/layouts/management/manage-color-contrast-layout.js
create mode 100644 modules/scanner/assets/js/layouts/management/manage-main-layout.js
rename modules/scanner/assets/js/layouts/{remediation-layout.js => management/manage-manual-layout.js} (60%)
rename modules/scanner/assets/js/layouts/{ => scanner}/alt-text-layout.js (94%)
rename modules/scanner/assets/js/layouts/{ => scanner}/color-contrast-layout.js (97%)
rename modules/scanner/assets/js/layouts/{ => scanner}/heading-structure-layout.js (100%)
rename modules/scanner/assets/js/layouts/{ => scanner}/main-layout.js (100%)
rename modules/scanner/assets/js/layouts/{ => scanner}/manual-layout.js (78%)
create mode 100644 modules/scanner/assets/js/static/global-infotip-image.png
create mode 100644 modules/scanner/assets/js/utils/build-path-to-parent.js
create mode 100644 modules/scanner/assets/js/utils/get-initial-tab.js
create mode 100644 modules/scanner/assets/js/utils/get-outer-html-by-xpath.js
create mode 100644 modules/settings/banners/bf-sale-2025-banner.php
create mode 100644 modules/settings/banners/images/bf-2025-banner.png
diff --git a/assets/dev/js/components/confirm-dialog/index.js b/assets/dev/js/components/confirm-dialog/index.js
index fd294ad0..d0cadc9d 100644
--- a/assets/dev/js/components/confirm-dialog/index.js
+++ b/assets/dev/js/components/confirm-dialog/index.js
@@ -1,4 +1,4 @@
-import { AlertTriangleFilledIcon } from '@elementor/icons';
+import AlertTriangleFilledIcon from '@elementor/icons/AlertTriangleFilledIcon';
import Button from '@elementor/ui/Button';
import Dialog from '@elementor/ui/Dialog';
import DialogActions from '@elementor/ui/DialogActions';
diff --git a/assets/dev/js/components/notifications/index.js b/assets/dev/js/components/notifications/index.js
index 66cc545b..2237dfb4 100644
--- a/assets/dev/js/components/notifications/index.js
+++ b/assets/dev/js/components/notifications/index.js
@@ -1,6 +1,6 @@
import Alert from '@elementor/ui/Alert';
import Snackbar from '@elementor/ui/Snackbar';
-import { useNotificationSettings } from '@ea11y/hooks';
+import { useNotificationSettings } from '@ea11y-apps/global/hooks/use-notifications';
const Notifications = ({ type, message }) => {
const {
diff --git a/modules/settings/assets/js/icons/crown-filled.js b/assets/dev/js/icons/crown-filled.js
similarity index 100%
rename from modules/settings/assets/js/icons/crown-filled.js
rename to assets/dev/js/icons/crown-filled.js
diff --git a/assets/dev/js/services/mixpanel/mixpanel-events.js b/assets/dev/js/services/mixpanel/mixpanel-events.js
index de113e44..9e5b25fa 100644
--- a/assets/dev/js/services/mixpanel/mixpanel-events.js
+++ b/assets/dev/js/services/mixpanel/mixpanel-events.js
@@ -38,7 +38,7 @@ export const mixpanelEvents = {
fixWithAiButtonClicked: 'fix_with_ai_button_clicked',
markAsDecorativeSelected: 'mark_as_decorative_selected',
resolveButtonClicked: 'resolve_button_clicked',
- navigationImageClicked: 'navigation_image_clicked',
+ navigationChanged: 'navigation_changed',
aiSuggestionAccepted: 'ai_suggestion_accepted',
markAsResolveClicked: 'mark_as_resolve_clicked',
issueSkipped: 'issue_skipped',
@@ -50,6 +50,9 @@ export const mixpanelEvents = {
contrastResetClicked: 'contrast_reset_clicked',
backgroundAdaptorTriggered: 'background_adaptor_triggered',
backgroundAdaptorChanged: 'background_adaptor_changed',
+ tabSelected: 'tab_selected',
+ markAsGlobalToggled: 'mark_as_global_toggled',
+ applyGlobalFixConfirmationClicked: 'apply_global_fix_confirmation_clicked',
// Accessibility assistant dashboard
assistantDashboardHistoryLogsButtonClicked: 'history_logs_button_clicked',
diff --git a/assets/dev/js/utils/color-contrast-helpers.js b/assets/dev/js/utils/color-contrast-helpers.js
new file mode 100644
index 00000000..1edbaefb
--- /dev/null
+++ b/assets/dev/js/utils/color-contrast-helpers.js
@@ -0,0 +1,94 @@
+import postcss from 'postcss';
+
+export const isValidCSS = (cssText) => {
+ try {
+ // Basic checks for common malicious patterns
+ if (!cssText || typeof cssText !== 'string') {
+ return false;
+ }
+ postcss.parse(cssText);
+ return true;
+ } catch (e) {
+ return false;
+ }
+};
+
+export const rgbOrRgbaToHex = (color) => {
+ const match = color.match(
+ /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/i,
+ );
+ if (!match) {
+ return null;
+ } // Not an RGB or RGBA string
+
+ const r = parseInt(match[1]).toString(16).padStart(2, '0');
+ const g = parseInt(match[2]).toString(16).padStart(2, '0');
+ const b = parseInt(match[3]).toString(16).padStart(2, '0');
+
+ // If alpha present and less than 1, include it
+ if (match[4] !== undefined && parseFloat(match[4]) < 1) {
+ const a = Math.round(parseFloat(match[4]) * 255)
+ .toString(16)
+ .padStart(2, '0');
+ return `#${r}${g}${b}${a}`.toUpperCase(); // 8-digit hex with alpha
+ }
+
+ return `#${r}${g}${b}`.toUpperCase(); // 6-digit hex
+};
+
+export const getDataFromCss = (cssRules) => {
+ const result = { color: null, background: null };
+
+ const ruleMatches = cssRules.matchAll(/([^{]+)\s*\{([^}]+)\}/g);
+
+ for (const [, selector, declarations] of ruleMatches) {
+ let element;
+ try {
+ element = document.querySelector(selector.trim());
+ } catch {
+ continue; // Skip invalid selectors
+ }
+
+ const colorMatch = declarations.match(
+ /(?get_element_by_xpath( $this->data['xpath'] );
+ $element_node = $this->data['global']
+ ? $this->get_element_by_xpath_with_snippet_fallback( $this->data['xpath'], $this->data['find'] )
+ : $this->get_element_by_xpath( $this->data['xpath'] );
+
if ( ! $element_node ) {
+ $this->use_frontend = true;
return null;
}
+
switch ( $this->data['action'] ) {
case 'update':
case 'add':
diff --git a/modules/remediation/actions/replace.php b/modules/remediation/actions/replace.php
index d785e313..28e4df69 100644
--- a/modules/remediation/actions/replace.php
+++ b/modules/remediation/actions/replace.php
@@ -17,15 +17,20 @@ class Replace extends Remediation_Base {
public static string $type = 'replace';
public function run() : ?DOMDocument {
- $element_node = $this->get_element_by_xpath( $this->data['xpath'] );
+ $element_node = $this->data['global']
+ ? $this->get_element_by_xpath_with_snippet_fallback( $this->data['xpath'], $this->data['find'] )
+ : $this->get_element_by_xpath( $this->data['xpath'] );
+
if ( ! $element_node instanceof \DOMElement ) {
+ $this->use_frontend = true;
return null; // nothing to do
}
$outer_html = $this->dom->saveHTML( $element_node );
if ( stripos( $outer_html, $this->data['find'] ) === false ) {
- return $this->dom;
+ $this->use_frontend = true;
+ return null;
}
$updated_html = str_ireplace( $this->data['find'], $this->data['replace'], $outer_html );
@@ -36,7 +41,7 @@ public function run() : ?DOMDocument {
$tmp_dom = new DOMDocument( '1.0', 'UTF-8' );
$tmp_dom->loadHTML(
- mb_convert_encoding( $updated_html, 'HTML-ENTITIES', 'UTF-8' ),
+ mb_encode_numericentity( $updated_html, [ 0x80, 0x10FFFF, 0, 0x1FFFFF ], 'UTF-8' ),
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NOERROR | LIBXML_NOWARNING
);
diff --git a/modules/remediation/actions/styles.php b/modules/remediation/actions/styles.php
index c709f0e8..fdba7c61 100644
--- a/modules/remediation/actions/styles.php
+++ b/modules/remediation/actions/styles.php
@@ -3,6 +3,7 @@
namespace EA11y\Modules\Remediation\Actions;
use DOMDocument;
+use DOMElement;
use EA11y\Modules\Remediation\Classes\Remediation_Base;
if ( ! defined( 'ABSPATH' ) ) {
@@ -14,14 +15,145 @@
*/
class Styles extends Remediation_Base {
public static string $type = 'styles';
+ public static string $style_id = 'ea11y-remediation-styles';
- public function __construct( DOMDocument $dom, $data ) {
- parent::__construct( $dom, $data );
- $this->use_frontend = true;
+ /**
+ * Build a CSS selector string for a given DOMElement.
+ *
+ * @param DOMElement|null $element
+ * @return string|null
+ */
+ public function get_element_css_selector( ?DOMElement $element ): ?string {
+ if ( ! $element ) {
+ return null;
+ }
+
+ $parts = [];
+
+ while ( $element && XML_ELEMENT_NODE === $element->nodeType ) {
+ $selector = strtolower( $element->tagName );
+
+ // If element has ID, stop here
+ if ( $element->hasAttribute( 'id' ) ) {
+ $selector .= '#' . $element->getAttribute( 'id' );
+ array_unshift( $parts, $selector );
+ break;
+ }
+
+ // Add classes unless body
+ if ( $element->hasAttribute( 'class' ) && strtolower( $element->tagName ) !== 'body' ) {
+ $classes = preg_split( '/\s+/', trim( $element->getAttribute( 'class' ) ) );
+ if ( ! empty( $classes ) ) {
+ $selector .= '.' . implode( '.', $classes );
+ }
+ }
+
+ // Add nth-of-type if needed
+ $parent = $element->parentNode;
+ if ( $parent instanceof DOMElement ) {
+ $tag_name = $element->tagName;
+ $siblings = [];
+ foreach ( $parent->childNodes as $child ) {
+ if ( $child instanceof DOMElement && $child->tagName === $tag_name ) {
+ $siblings[] = $child;
+ }
+ }
+
+ if ( count( $siblings ) > 1 ) {
+ foreach ( $siblings as $i => $sibling ) {
+ if ( $sibling->isSameNode( $element ) ) {
+ $selector .= ':nth-of-type(' . ( $i + 1 ) . ')';
+ break;
+ }
+ }
+ }
+ }
+
+ array_unshift( $parts, $selector );
+ $element = $element->parentNode instanceof DOMElement ? $element->parentNode : null;
+ }
+
+ return implode( ' > ', $parts );
+ }
+
+ /**
+ * Replace CSS selectors for color and background-color rules.
+ *
+ * @param string $css The original CSS string
+ * @param string|null $color_selector New selector for color rule
+ * @param string|null $bg_selector New selector for background-color rule
+ * @return string The modified CSS
+ */
+ public function replace_css_selectors(
+ string $css,
+ ?string $color_selector = null,
+ ?string $bg_selector = null
+ ): string {
+ // Match full CSS blocks like "selector { ... }"
+ preg_match_all( '/([^{]+)\{([^}]+)\}/', $css, $matches, PREG_SET_ORDER );
+
+ $result = '';
+
+ foreach ( $matches as $match ) {
+ $rules = trim( $match[2] );
+
+ // Find color value
+ if ( $color_selector && preg_match( '/(?data['rule'];
+ if ( $this->data['global'] ) {
+ $el_color = $this->get_element_by_xpath_with_snippet_fallback( $this->data['xpath'], $this->data['find'] );
+ $el_bg = $this->get_element_by_xpath_with_snippet_fallback( $this->data['parentXPath'], $this->data['parentFind'] );
+ $color_css_selector = $this->get_element_css_selector( $el_color );
+ $bg_css_selector = $this->get_element_css_selector( $el_bg );
+
+ if ( ! $color_css_selector && ! $bg_css_selector ) {
+ $this->use_frontend = true;
+ return null;
+ }
+
+ $rule = $this->replace_css_selectors( $rule, $color_css_selector, $bg_css_selector );
+ }
+
+ // Find or create
element
+ $head = $this->dom->getElementsByTagName( 'head' )->item( 0 );
+ if ( ! $head ) {
+ $head = $this->dom->createElement( 'head' );
+ $html_element = $this->dom->getElementsByTagName( 'html' )->item( 0 );
+ if ( $html_element ) {
+ $html_element->insertBefore( $head, $this->dom->getElementsByTagName( 'body' )->item( 0 ) );
+ } else {
+ $this->dom->appendChild( $head );
+ }
+ }
+
+ // Create ';
+
+ return $links;
+ }
+
public function __construct() {
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
add_action( 'admin_init', [ $this, 'register_base_data' ] );
add_action( 'rest_api_init', [ $this, 'register_settings' ] );
+ add_filter( 'plugin_row_meta', array( $this, 'add_plugin_row_meta' ), 10, 2 );
$this->register_routes();
}
diff --git a/modules/scanner/assets/js/api/APIScanner.js b/modules/scanner/assets/js/api/APIScanner.js
index 2b0130b0..8b84b6c3 100644
--- a/modules/scanner/assets/js/api/APIScanner.js
+++ b/modules/scanner/assets/js/api/APIScanner.js
@@ -104,6 +104,38 @@ export class APIScanner extends API {
});
}
+ static async setRemediationAsGlobal(data) {
+ return APIScanner.request({
+ method: 'POST',
+ path: `${v1Prefix}/remediation/global-item`,
+ data,
+ });
+ }
+
+ static async updateGlobalRemediationForPage(data) {
+ return APIScanner.request({
+ method: 'PUT',
+ path: `${v1Prefix}/remediation/global-item`,
+ data,
+ });
+ }
+
+ static async updateGlobalRemediationForAllPages(data) {
+ return APIScanner.request({
+ method: 'PATCH',
+ path: `${v1Prefix}/remediation/global-item`,
+ data,
+ });
+ }
+
+ static async deleteGlobalRemediation(data) {
+ return APIScanner.request({
+ method: 'DELETE',
+ path: `${v1Prefix}/remediation/global-item`,
+ data,
+ });
+ }
+
static async updateRemediationStatusForPage(data) {
return APIScanner.request({
method: 'PATCH',
@@ -112,6 +144,38 @@ export class APIScanner extends API {
});
}
+ static async updateGlobalRemediationGroupForPage(data) {
+ return APIScanner.request({
+ method: 'PUT',
+ path: `${v1Prefix}/remediation/global-items-group`,
+ data,
+ });
+ }
+
+ static async updateAllGlobalRemediationForAllPages(data) {
+ return APIScanner.request({
+ method: 'PATCH',
+ path: `${v1Prefix}/remediation/global-items`,
+ data,
+ });
+ }
+
+ static async updateAllGlobalRemediationForPage(data) {
+ return APIScanner.request({
+ method: 'PUT',
+ path: `${v1Prefix}/remediation/global-items`,
+ data,
+ });
+ }
+
+ static async updateGlobalRemediationGroupForAllPages(data) {
+ return APIScanner.request({
+ method: 'PATCH',
+ path: `${v1Prefix}/remediation/global-items-group`,
+ data,
+ });
+ }
+
static async deleteRemediationForPage(data) {
return APIScanner.request({
method: 'DELETE',
@@ -120,6 +184,14 @@ export class APIScanner extends API {
});
}
+ static async deleteGlobalRemediations(data) {
+ return APIScanner.request({
+ method: 'DELETE',
+ path: `${v1Prefix}/remediation/global-items`,
+ data,
+ });
+ }
+
static async setHeadingLevel(data) {
return APIScanner.request({
method: 'POST',
diff --git a/modules/scanner/assets/js/app.js b/modules/scanner/assets/js/app.js
index 758eef4f..67c02e3f 100644
--- a/modules/scanner/assets/js/app.js
+++ b/modules/scanner/assets/js/app.js
@@ -1,28 +1,39 @@
+import Box from '@elementor/ui/Box';
import ErrorBoundary from '@elementor/ui/ErrorBoundary';
+import Tab from '@elementor/ui/Tab';
import { FocusTrap } from 'focus-trap-react';
-import { Notifications } from '@ea11y/components';
+import Notifications from '@ea11y-apps/global/components/notifications';
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 AppHeader from '@ea11y-apps/scanner/components/header/app-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';
import { ResolvedMessage } from '@ea11y-apps/scanner/components/resolved-message';
import { BLOCKS, PAGE_QUOTA_LIMIT } from '@ea11y-apps/scanner/constants';
import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context';
+import { useTabsContext } from '@ea11y-apps/scanner/context/tabs-context';
import {
AltTextLayout,
+ ColorContrastLayout,
+ HeadingStructureLayout,
MainLayout,
+ ManageAltTextLayout,
+ ManageColorContrastLayout,
ManageMainLayout,
+ ManageManualLayout,
ManualLayout,
- 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 {
+ AppContainer,
+ AppsTabPanel,
+ AppsTabs,
+} from '@ea11y-apps/scanner/styles/app.styles';
import { removeExistingFocus } from '@ea11y-apps/scanner/utils/focus-on-element';
import { useEffect, useRef } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
const App = () => {
const { notificationMessage, notificationType } = useNotificationSettings();
@@ -31,15 +42,18 @@ const App = () => {
violation,
resolved,
openedBlock,
- isManage,
isError,
+ isManage,
quotaExceeded,
loading,
} = useScannerWizardContext();
+
+ const { tabsProps, getTabProps, getTabPanelProps } = useTabsContext();
+
const containerRef = useRef(null);
const showResolvedMessage = Boolean(
- (resolved > 0 && violation === resolved) || violation === 0,
+ !isManage && ((resolved > 0 && violation === resolved) || violation === 0),
);
useEffect(() => {
@@ -65,7 +79,7 @@ const App = () => {
}
}, [PAGE_QUOTA_LIMIT, quotaExceeded]);
- const getBlock = () => {
+ const getMsgStateBlock = () => {
if (!window.ea11yScannerData?.isConnected) {
return ;
}
@@ -79,11 +93,16 @@ const App = () => {
return ;
}
+ return null;
+ };
+
+ const getIssuesBlock = () => {
+ if (showResolvedMessage) {
+ return ;
+ }
switch (openedBlock) {
case BLOCKS.main:
return ;
- case BLOCKS.management:
- return ;
case BLOCKS.altText:
return ;
case BLOCKS.colorContrast:
@@ -91,7 +110,22 @@ const App = () => {
case BLOCKS.headingStructure:
return ;
default:
- return isManage ? : ;
+ return ;
+ }
+ };
+
+ const getManageBlock = () => {
+ switch (openedBlock) {
+ case BLOCKS.management:
+ return ;
+ case BLOCKS.altText:
+ return ;
+ case BLOCKS.colorContrast:
+ return ;
+ case BLOCKS.headingStructure:
+ return ;
+ default:
+ return ;
}
};
@@ -102,9 +136,35 @@ const App = () => {
>
}>
+
+
+
+
+
+
+
+
- {showResolvedMessage && !isManage ? : getBlock()}
+
+ {getMsgStateBlock() || getIssuesBlock()}
+
+
+ {getMsgStateBlock() || getManageBlock()}
+
{
+export const AltTextForm = ({ item, current, setCurrent, setIsEdit }) => {
+ const { isManage } = useScannerWizardContext();
const { error } = useToastNotification();
const {
+ isGlobal,
+ setIsGlobal,
data,
loadingAiText,
isSubmitDisabled,
@@ -36,12 +40,21 @@ export const AltTextForm = ({ items, current, setCurrent }) => {
handleChange,
handleCheck,
handleSubmit,
+ handleUpdate,
generateAltText,
} = useAltTextForm({
current,
- item: items[current],
+ item,
});
+ const onGlobalChange = (value) => {
+ setIsGlobal(value);
+ };
+
+ const onCancel = () => {
+ setIsEdit(false);
+ };
+
const onSubmit = async () => {
try {
await handleSubmit();
@@ -51,6 +64,11 @@ export const AltTextForm = ({ items, current, setCurrent }) => {
}
};
+ const onUpdate = async () => {
+ await handleUpdate();
+ void (setIsEdit ? setIsEdit(false) : setCurrent(current + 1));
+ };
+
const onUpgradeHover = () => {
mixpanelService.sendEvent(mixpanelEvents.upgradeSuggestionViewed, {
current_plan: window.ea11yScannerData?.planData?.plan?.name,
@@ -59,11 +77,15 @@ export const AltTextForm = ({ items, current, setCurrent }) => {
});
};
+ const applyBtnText = isManage
+ ? __('Apply changes', 'pojo-accessibility')
+ : __('Apply fix', 'pojo-accessibility');
+
return (
-
+
{
InputProps={{
endAdornment: (
- {IS_AI_ENABLED && AI_QUOTA_LIMIT ? (
+ {IS_PRO_PLAN && AI_QUOTA_LIMIT ? (
{
)}
-
+
+ {!isManage && (
+
+ )}
+
+
+ {isManage && (
+
+ )}
+
+
+
);
};
AltTextForm.propTypes = {
- items: PropTypes.arrayOf(scannerItem).isRequired,
+ item: scannerItem,
current: PropTypes.number.isRequired,
setCurrent: PropTypes.func.isRequired,
+ setIsEdit: PropTypes.func,
};
diff --git a/modules/scanner/assets/js/components/block-button/button-menu.js b/modules/scanner/assets/js/components/block-button/button-menu.js
new file mode 100644
index 00000000..5fdc64d7
--- /dev/null
+++ b/modules/scanner/assets/js/components/block-button/button-menu.js
@@ -0,0 +1,126 @@
+import DotsVerticalIcon from '@elementor/icons/DotsVerticalIcon';
+import ReloadIcon from '@elementor/icons/ReloadIcon';
+import TrashIcon from '@elementor/icons/TrashIcon';
+import Box from '@elementor/ui/Box';
+import IconButton from '@elementor/ui/IconButton';
+
+import Menu from '@elementor/ui/Menu';
+import MenuItem from '@elementor/ui/MenuItem';
+import MenuItemIcon from '@elementor/ui/MenuItemIcon';
+import MenuItemText from '@elementor/ui/MenuItemText';
+import PropTypes from 'prop-types';
+import { DeletePageRemediationModal } from '@ea11y-apps/scanner/components/remediation-confirmation-modal';
+import { BLOCK_TITLES } from '@ea11y-apps/scanner/constants';
+import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context';
+import { useManageActions } from '@ea11y-apps/scanner/hooks/use-manage-actions';
+import { StyledBanIcon } from '@ea11y-apps/scanner/styles/app.styles';
+import { useRef, useState } from '@wordpress/element';
+import { __, sprintf } from '@wordpress/i18n';
+
+export const ButtonMenu = ({ group }) => {
+ const { sortedRemediation } = useScannerWizardContext();
+ const { deleteAllRemediationForPage, updateAllRemediationForPage } =
+ useManageActions();
+ const anchorEl = useRef(null);
+
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+ const [isOpened, setIsOpened] = useState(false);
+
+ const count = sortedRemediation[group]?.length;
+
+ const isAllDisabled =
+ count ===
+ sortedRemediation[group]?.filter(
+ (remediation) => !Number(remediation.active),
+ )?.length;
+
+ const handleOpen = () => {
+ setIsOpened(true);
+ };
+ const handleClose = () => setIsOpened(false);
+
+ const toggleDeleteModal = () => setShowDeleteModal(!showDeleteModal);
+
+ const onDeleteRemediation = async () => {
+ setShowDeleteModal(false);
+ await deleteAllRemediationForPage(group);
+ };
+
+ return (
+
+
+
+
+
+ );
+};
+
+ButtonMenu.propTypes = {
+ group: PropTypes.string.isRequired,
+};
diff --git a/modules/scanner/assets/js/components/block-button/global-button-menu.js b/modules/scanner/assets/js/components/block-button/global-button-menu.js
new file mode 100644
index 00000000..a04e2f98
--- /dev/null
+++ b/modules/scanner/assets/js/components/block-button/global-button-menu.js
@@ -0,0 +1,217 @@
+import DotsVerticalIcon from '@elementor/icons/DotsVerticalIcon';
+import ReloadIcon from '@elementor/icons/ReloadIcon';
+import TrashIcon from '@elementor/icons/TrashIcon';
+import Box from '@elementor/ui/Box';
+import IconButton from '@elementor/ui/IconButton';
+
+import Menu from '@elementor/ui/Menu';
+import MenuItem from '@elementor/ui/MenuItem';
+import MenuItemIcon from '@elementor/ui/MenuItemIcon';
+import MenuItemText from '@elementor/ui/MenuItemText';
+import PropTypes from 'prop-types';
+import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services';
+import {
+ DeleteGlobalRemediationModal,
+ DisableGlobalRemediationModal,
+ EnableGlobalRemediationModal,
+} from '@ea11y-apps/scanner/components/remediation-confirmation-modal';
+import { BLOCK_TITLES } from '@ea11y-apps/scanner/constants';
+import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context';
+import { useGlobalManageActions } from '@ea11y-apps/scanner/hooks/use-global-manage-actions';
+import { StyledBanIcon } from '@ea11y-apps/scanner/styles/app.styles';
+import { useRef, useState } from '@wordpress/element';
+import { __, sprintf } from '@wordpress/i18n';
+
+export const GlobalButtonMenu = ({ group }) => {
+ const { sortedGlobalRemediation } = useScannerWizardContext();
+ const {
+ updateGlobalRemediationGroupForPage,
+ updateGlobalRemediationGroupForAllPages,
+ deleteGlobalRemediations,
+ } = useGlobalManageActions();
+ const anchorEl = useRef(null);
+
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+ const [showDisableModal, setShowDisableModal] = useState(false);
+ const [showEnableModal, setShowEnableModal] = useState(false);
+
+ const [isOpened, setIsOpened] = useState(false);
+
+ const count = sortedGlobalRemediation[group]?.length;
+
+ const isAllExcluded =
+ count ===
+ sortedGlobalRemediation[group]?.filter(
+ (remediation) => !Number(remediation.active_for_page),
+ )?.length;
+
+ const isAllDisabled =
+ count ===
+ sortedGlobalRemediation[group]?.filter(
+ (remediation) => !Number(remediation.active),
+ )?.length;
+
+ const handleClose = () => setIsOpened(false);
+ const handleOpen = () => {
+ setIsOpened(true);
+ };
+
+ const onShowDeleteModal = () => {
+ setShowDeleteModal(true);
+ mixpanelService.sendEvent(mixpanelEvents.popupButtonClicked, {
+ data: {
+ popupType: 'global_delete_confirmation',
+ buttonName: 'Remove across scans',
+ },
+ });
+ };
+ const onHideDeleteModal = () => setShowDeleteModal(false);
+ const toggleDisableModal = () => setShowDisableModal(!showDisableModal);
+ const toggleEnableModal = () => setShowDisableModal(!showDisableModal);
+
+ const onUpdateStatus = () => {
+ void (isAllDisabled ? setShowEnableModal(true) : setShowDisableModal(true));
+ mixpanelService.sendEvent(mixpanelEvents.popupButtonClicked, {
+ data: {
+ popupType: isAllDisabled
+ ? 'global_enable_confirmation'
+ : 'global_disable_confirmation',
+ buttonName: isAllDisabled
+ ? __('Enable across scans', 'pojo-accessibility')
+ : __('Disable across scans', 'pojo-accessibility'),
+ },
+ });
+ };
+
+ const onUpdateForPage = () => {
+ void updateGlobalRemediationGroupForPage(isAllExcluded, group);
+ };
+
+ const onUpdateForAllPages = () => {
+ void updateGlobalRemediationGroupForAllPages(isAllDisabled, group);
+ };
+
+ const onDeleteRemediation = () => {
+ setShowDeleteModal(false);
+ void deleteGlobalRemediations(group);
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+GlobalButtonMenu.propTypes = {
+ group: PropTypes.string.isRequired,
+};
diff --git a/modules/scanner/assets/js/components/block-button/index.js b/modules/scanner/assets/js/components/block-button/index.js
index f6725056..b006890c 100644
--- a/modules/scanner/assets/js/components/block-button/index.js
+++ b/modules/scanner/assets/js/components/block-button/index.js
@@ -13,6 +13,8 @@ import {
export const BlockButton = ({ title, count, block }) => {
const { setOpenedBlock, isResolved } = useScannerWizardContext();
+ const resolved = count === 0 || isResolved(block);
+
const handleClick = () => {
setOpenedBlock(block);
mixpanelService.sendEvent(mixpanelEvents.categoryClicked, {
@@ -23,8 +25,6 @@ export const BlockButton = ({ title, count, block }) => {
});
};
- const resolved = count === 0 || isResolved(block);
-
return (
{
color={resolved ? 'success' : 'default'}
>
-
+
{title}
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 a36de740..33531fba 100644
--- a/modules/scanner/assets/js/components/block-button/manage-button.js
+++ b/modules/scanner/assets/js/components/block-button/manage-button.js
@@ -1,24 +1,28 @@
+import WorldIcon from '@elementor/icons/WorldIcon';
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';
-import { DeleteButton } from '@ea11y-apps/scanner/components/manage-remediation-buttons/delete-button';
-import { DisableButton } from '@ea11y-apps/scanner/components/manage-remediation-buttons/disable-button';
-import { EnableButton } from '@ea11y-apps/scanner/components/manage-remediation-buttons/enable-button';
-import { BLOCKS } from '@ea11y-apps/scanner/constants';
+import { ButtonMenu } from '@ea11y-apps/scanner/components/block-button/button-menu';
+import { GlobalButtonMenu } from '@ea11y-apps/scanner/components/block-button/global-button-menu';
import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context';
import {
ActionButton,
- ManageButtonGroup,
ManageButtonWrap,
} from '@ea11y-apps/scanner/styles/app.styles';
import { __, sprintf } from '@wordpress/i18n';
-export const ManageButton = ({ title, count, block }) => {
- const { sortedRemediation, setOpenedBlock } = useScannerWizardContext();
+export const ManageButton = ({ title, count, block, global = false }) => {
+ const {
+ sortedRemediation,
+ sortedGlobalRemediation,
+ setOpenedBlock,
+ setIsManageGlobal,
+ } = useScannerWizardContext();
const handleClick = () => {
setOpenedBlock(block);
+ setIsManageGlobal(global);
mixpanelService.sendEvent(mixpanelEvents.categoryClicked, {
page_url: window.ea11yScannerData?.pageData?.url,
issue_count: count,
@@ -27,29 +31,24 @@ export const ManageButton = ({ title, count, block }) => {
});
};
- const total = sortedRemediation[block].length;
- const disabled = block === BLOCKS.colorContrast || block === BLOCKS.altText;
- const isAllDisabled =
- sortedRemediation[block]?.length ===
- sortedRemediation[block]?.filter(
- (remediation) => !Number(remediation.active),
- )?.length;
+ const remediations = global ? sortedGlobalRemediation : sortedRemediation;
+ const total = remediations[block].length;
return (
-
+
-
+
{title}
: null}
label={sprintf(
// Translators: %1$s - active, %2$s - total
__('%1$s/%2$s active', 'pojo-accessibility'),
@@ -61,14 +60,11 @@ export const ManageButton = ({ title, count, block }) => {
size="tiny"
disabled={count === 0}
/>
-
- {isAllDisabled ? (
-
- ) : (
-
- )}
-
-
+ {global ? (
+
+ ) : (
+
+ )}
);
};
@@ -77,4 +73,5 @@ ManageButton.propTypes = {
title: PropTypes.string.isRequired,
count: PropTypes.number.isRequired,
block: PropTypes.string.isRequired,
+ global: PropTypes.bool,
};
diff --git a/modules/scanner/assets/js/components/color-contrast-form/index.js b/modules/scanner/assets/js/components/color-contrast-form/index.js
index 6176623e..09efdca8 100644
--- a/modules/scanner/assets/js/components/color-contrast-form/index.js
+++ b/modules/scanner/assets/js/components/color-contrast-form/index.js
@@ -1,36 +1,44 @@
import Alert from '@elementor/ui/Alert';
import AlertTitle from '@elementor/ui/AlertTitle';
+import Box from '@elementor/ui/Box';
import Button from '@elementor/ui/Button';
import Divider from '@elementor/ui/Divider';
import Typography from '@elementor/ui/Typography';
import PropTypes from 'prop-types';
import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services';
+import { rgbOrRgbaToHex } from '@ea11y-apps/global/utils/color-contrast-helpers';
import { ColorSet } from '@ea11y-apps/scanner/components/color-contrast-form/color-set';
import { ColorSetDisabled } from '@ea11y-apps/scanner/components/color-contrast-form/color-set-disabled';
import { ParentSelector } from '@ea11y-apps/scanner/components/color-contrast-form/parent-selector';
+import { SetGlobal } from '@ea11y-apps/scanner/components/manage-footer-actions/page/set-global';
import { BLOCKS } from '@ea11y-apps/scanner/constants';
+import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context';
import { useColorContrastForm } from '@ea11y-apps/scanner/hooks/use-color-contrast-form';
import { StyledBox } from '@ea11y-apps/scanner/styles/app.styles';
import { scannerItem } from '@ea11y-apps/scanner/types/scanner-item';
import { checkContrastAA } from '@ea11y-apps/scanner/utils/calc-color-ratio';
-import { rgbOrRgbaToHex } from '@ea11y-apps/scanner/utils/convert-colors';
import { useEffect, useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
-export const ColorContrastForm = ({ items, current, setCurrent }) => {
- const item = items[current];
+export const ColorContrastForm = ({ item, current, setCurrent, setIsEdit }) => {
+ const { isManage } = useScannerWizardContext();
const {
+ isGlobal,
+ setIsGlobal,
color,
background,
parents,
+ isDisabled,
resolved,
backgroundChanged,
+ parentChanged,
loading,
changeColor,
changeBackground,
setParentSmaller,
setParentLarger,
onSubmit,
+ onUpdate,
} = useColorContrastForm({
item,
current,
@@ -47,10 +55,15 @@ export const ColorContrastForm = ({ items, current, setCurrent }) => {
}
}, [item]);
- const isBackgroundEnabled = item.messageArgs[3] && item.messageArgs[4];
+ const isBackgroundEnabled =
+ item.messageArgs[3] && item.messageArgs[4] && !item.isPotential;
const colorData = checkContrastAA(item.node);
+ const onCancel = () => {
+ setIsEdit(false);
+ };
+
const handleSubmit = async () => {
await onSubmit();
mixpanelService.sendEvent(mixpanelEvents.applyFixButtonClicked, {
@@ -60,9 +73,32 @@ export const ColorContrastForm = ({ items, current, setCurrent }) => {
background_level: parents.length,
category_name: BLOCKS.colorContrast,
page_url: window.ea11yScannerData?.pageData?.url,
+ is_global: isGlobal ? 'yes' : 'no',
});
};
+ const onGlobalChange = (value) => {
+ setIsGlobal(value);
+ };
+
+ const handleUpdate = async () => {
+ await onUpdate();
+ void (setIsEdit ? setIsEdit(false) : setCurrent(current + 1));
+ };
+
+ const isSubmitDisabled =
+ !colorData.passesAA ||
+ isDisabled ||
+ loading ||
+ (item.isEdit &&
+ color === item.messageArgs[3] &&
+ background === item.messageArgs[4] &&
+ !parentChanged);
+
+ const applyBtnText = isManage
+ ? __('Apply changes', 'pojo-accessibility')
+ : __('Apply fix', 'pojo-accessibility');
+
return (
@@ -113,22 +149,41 @@ export const ColorContrastForm = ({ items, current, setCurrent }) => {
{colorData.ratio}
-
+
+ {!isManage && (
+
+ )}
+
+ {isManage && (
+
+ )}
+
+
+
);
};
ColorContrastForm.propTypes = {
- items: PropTypes.arrayOf(scannerItem).isRequired,
+ item: scannerItem,
current: PropTypes.number.isRequired,
setCurrent: PropTypes.func.isRequired,
+ setIsEdit: PropTypes.func,
};
diff --git a/modules/scanner/assets/js/components/empty-manage-message/index.js b/modules/scanner/assets/js/components/empty-manage-message/index.js
new file mode 100644
index 00000000..032e59d5
--- /dev/null
+++ b/modules/scanner/assets/js/components/empty-manage-message/index.js
@@ -0,0 +1,41 @@
+import Button from '@elementor/ui/Button';
+import Typography from '@elementor/ui/Typography';
+
+import { BLOCKS } from '@ea11y-apps/scanner/constants';
+import { useTabsContext } from '@ea11y-apps/scanner/context/tabs-context';
+import { EmptyImage } from '@ea11y-apps/scanner/images';
+import { StateContainer } from '@ea11y-apps/scanner/styles/app.styles';
+import { __ } from '@wordpress/i18n';
+
+export const EmptyManageMessage = () => {
+ const { tabsProps } = useTabsContext();
+
+ const handleChangeTab = (event) => {
+ tabsProps.onChange(event, BLOCKS.main);
+ };
+
+ return (
+
+
+
+
+ {__('No fixes yet', 'pojo-accessibility')}
+
+
+
+ {__(
+ 'Once you start fixing issues for this page, they’ll appear here.',
+ 'pojo-accessibility',
+ )}
+
+
+
+ );
+};
diff --git a/modules/scanner/assets/js/components/form-navigation/index.js b/modules/scanner/assets/js/components/form-navigation/index.js
index 579b053e..ff77a70c 100644
--- a/modules/scanner/assets/js/components/form-navigation/index.js
+++ b/modules/scanner/assets/js/components/form-navigation/index.js
@@ -1,65 +1,40 @@
-import ChevronLeftIcon from '@elementor/icons/ChevronLeftIcon';
-import ChevronRightIcon from '@elementor/icons/ChevronRightIcon';
import Box from '@elementor/ui/Box';
import Divider from '@elementor/ui/Divider';
-import IconButton from '@elementor/ui/IconButton';
-import Typography from '@elementor/ui/Typography';
+import Pagination from '@elementor/ui/Pagination';
import { styled } from '@elementor/ui/styles';
import PropTypes from 'prop-types';
import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services';
-import { isRTL } from '@ea11y-apps/scanner/constants';
import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context';
-import { __, sprintf } from '@wordpress/i18n';
export const FormNavigation = ({ total, current, setCurrent }) => {
const { openedBlock } = useScannerWizardContext();
- const previous = current - 1;
- const next = current + 1;
- const navigate = (index, direction) => () => {
- setCurrent(index);
- mixpanelService.sendEvent(mixpanelEvents.navigationImageClicked, {
- direction,
+ const onChange = (event, index) => {
+ setCurrent(index - 1);
+ mixpanelService.sendEvent(mixpanelEvents.navigationChanged, {
+ page: index,
category: openedBlock,
});
};
return (
-
-
-
-
-
-
-
-
- {sprintf(
- // Translators: %1$s - current, %2$s - total
- __('%1$s of %2$s issues', 'pojo-accessibility'),
- current + 1,
- total,
- )}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
};
-const Navigation = styled(Box)`
- position: absolute;
+const StyledNavigationContainer = styled(Box)`
+ position: fixed;
bottom: 0;
- left: 0;
- width: 100%;
+ right: ${({ theme }) => theme.spacing(2)};
+ width: 393px;
+ background: ${({ theme }) => theme.palette.background.paper};
`;
const StyledBox = styled(Box)`
display: flex;
@@ -74,10 +49,6 @@ const StyledNavigation = styled(Box)`
gap: ${({ theme }) => theme.spacing(2)};
`;
-const StyledIconButton = styled(IconButton)`
- ${isRTL ? 'transform: rotate(180deg)' : ''}
-`;
-
FormNavigation.propTypes = {
total: PropTypes.number.isRequired,
current: PropTypes.number.isRequired,
diff --git a/modules/scanner/assets/js/components/header/header-container.js b/modules/scanner/assets/js/components/header/app-header.js
similarity index 70%
rename from modules/scanner/assets/js/components/header/header-container.js
rename to modules/scanner/assets/js/components/header/app-header.js
index 33f255ed..8e125269 100644
--- a/modules/scanner/assets/js/components/header/header-container.js
+++ b/modules/scanner/assets/js/components/header/app-header.js
@@ -1,27 +1,19 @@
-import ChevronLeftIcon from '@elementor/icons/ChevronLeftIcon';
import XIcon from '@elementor/icons/XIcon';
import Box from '@elementor/ui/Box';
import IconButton from '@elementor/ui/IconButton';
import Paper from '@elementor/ui/Paper';
import { styled } from '@elementor/ui/styles';
-import PropTypes from 'prop-types';
import { APIScanner } from '@ea11y-apps/scanner/api/APIScanner';
import { DropdownMenu } from '@ea11y-apps/scanner/components/header/dropdown-menu';
-import HeaderTitle from '@ea11y-apps/scanner/components/header/title';
-import { BLOCKS, ROOT_ID } from '@ea11y-apps/scanner/constants';
+import AppTitle from '@ea11y-apps/scanner/components/header/title/app-title';
+import { ROOT_ID } from '@ea11y-apps/scanner/constants';
import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context';
import { StyledHeaderContent } from '@ea11y-apps/scanner/styles/app.styles';
import { closeWidget } from '@ea11y-apps/scanner/utils/close-widget';
import { __ } from '@wordpress/i18n';
-const HeaderContainer = ({ children }) => {
- const { isChanged, setOpenedBlock, isManage, setIsManage } =
- useScannerWizardContext();
-
- const goBack = () => {
- setIsManage(false);
- setOpenedBlock(BLOCKS.main);
- };
+const AppHeader = () => {
+ const { isChanged } = useScannerWizardContext();
const onClose = () => {
const widget = document.getElementById(ROOT_ID);
@@ -46,17 +38,8 @@ const HeaderContainer = ({ children }) => {
alignItems="center"
>
- {isManage && (
-
-
-
- )}
-
-
+
@@ -73,8 +56,6 @@ const HeaderContainer = ({ children }) => {
-
- {children}
);
};
@@ -96,8 +77,4 @@ const StyledTitleWrapper = styled(Box)`
}
`;
-HeaderContainer.propTypes = {
- children: PropTypes.node,
-};
-
-export default HeaderContainer;
+export default AppHeader;
diff --git a/modules/scanner/assets/js/components/header/dropdown-menu.js b/modules/scanner/assets/js/components/header/dropdown-menu.js
index 2628f507..1bf39bca 100644
--- a/modules/scanner/assets/js/components/header/dropdown-menu.js
+++ b/modules/scanner/assets/js/components/header/dropdown-menu.js
@@ -1,9 +1,9 @@
import CalendarDollarIcon from '@elementor/icons/CalendarDollarIcon';
+import ChecklistIcon from '@elementor/icons/ChecklistIcon';
import ClearIcon from '@elementor/icons/ClearIcon';
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';
@@ -11,27 +11,22 @@ import Menu from '@elementor/ui/Menu';
import MenuItem from '@elementor/ui/MenuItem';
import MenuItemIcon from '@elementor/ui/MenuItemIcon';
import MenuItemText from '@elementor/ui/MenuItemText';
-import Tooltip from '@elementor/ui/Tooltip';
import { ELEMENTOR_URL } from '@ea11y-apps/global/constants';
import { useToastNotification } from '@ea11y-apps/global/hooks';
import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services';
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 useScannerSettings from '@ea11y-apps/scanner/hooks/use-scanner-settings';
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,
- openedBlock,
- setOpenedBlock,
- setIsManage,
- runNewScan,
- } = useScannerWizardContext();
+ const { remediations, isManage, openedBlock, setOpenedBlock, runNewScan } =
+ useScannerWizardContext();
+ const { dashboardUrl } = useScannerSettings();
const { error } = useToastNotification();
const [isOpened, setIsOpened] = useState(false);
const [loading, setLoading] = useState(false);
@@ -69,16 +64,8 @@ export const DropdownMenu = () => {
}
};
- const goToManagement = () => {
- handleClose();
- setIsManage(true);
- setOpenedBlock(BLOCKS.management);
- sendOnClickEvent('Manage fixes');
- };
-
const goToHeadingManager = () => {
handleClose();
- setIsManage(true);
setOpenedBlock(BLOCKS.headingStructure);
};
@@ -120,6 +107,21 @@ export const DropdownMenu = () => {
{__('Rescan', 'pojo-accessibility')}
+
+
+
{remediations.length > 0 ? (
)}
- {!remediations.length ? (
-
-
-
- ) : (
-
- )}
-
{!areNoHeadingsDefined() && (