@elementor/editor-audits 4.3.3 → 4.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/index.d.mts +11 -1
  2. package/dist/index.d.ts +11 -1
  3. package/dist/index.js +1103 -1146
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1075 -1108
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +17 -15
  8. package/src/api/page-context-client.ts +8 -5
  9. package/src/audits/accessibility-statement.ts +39 -0
  10. package/src/audits/accessibility-widget.ts +41 -0
  11. package/src/audits/cookie-policy.ts +1 -0
  12. package/src/audits/google-consent-mode.ts +4 -7
  13. package/src/audits/images-alt-text.ts +6 -4
  14. package/src/audits/images-inefficient-format.ts +66 -0
  15. package/src/audits/images-too-large.ts +23 -10
  16. package/src/audits/nested-boxed-containers.ts +5 -0
  17. package/src/components/audit-feedback.tsx +4 -1
  18. package/src/components/audit-panel.tsx +13 -13
  19. package/src/components/audit-toggle-icon.tsx +22 -0
  20. package/src/components/fix-violation-with-angie.tsx +18 -7
  21. package/src/components/pages/category-page.tsx +27 -3
  22. package/src/components/pages/overview-page.tsx +24 -29
  23. package/src/components/report-shell.tsx +10 -91
  24. package/src/components/rescan-button.tsx +38 -0
  25. package/src/components/severity-icons.tsx +5 -1
  26. package/src/components/severity-summary-cards.tsx +52 -0
  27. package/src/components/violation-cta-button.tsx +11 -2
  28. package/src/components/violation-icons.tsx +2 -2
  29. package/src/components/violation-row.tsx +120 -62
  30. package/src/hooks/use-audit-report.ts +5 -4
  31. package/src/hooks/use-audit-toggle-props.ts +2 -2
  32. package/src/hooks/use-single-expanded-card.ts +11 -0
  33. package/src/init.ts +2 -0
  34. package/src/register-audits.ts +6 -2
  35. package/src/runner.ts +15 -7
  36. package/src/store/selectors.ts +4 -0
  37. package/src/store/slice.ts +10 -1
  38. package/src/sync-report-staleness.ts +14 -0
  39. package/src/types.ts +11 -1
  40. package/src/utils/all-image-sources.ts +17 -0
  41. package/src/utils/alt-text-image-sources.ts +24 -0
  42. package/src/utils/atomic-background-image-sources.ts +56 -0
  43. package/src/utils/atomic-image-sources.ts +66 -0
  44. package/src/utils/audit-status-summary.ts +0 -57
  45. package/src/utils/background-image-sources.ts +32 -0
  46. package/src/utils/image-alt.ts +3 -1
  47. package/src/utils/image-like-sources.ts +5 -14
  48. package/src/utils/image-size-key.ts +8 -0
  49. package/src/utils/page-attachments.ts +7 -6
  50. package/src/utils/scan-accessibility-widgets.ts +7 -0
  51. package/src/audits/too-many-widgets.ts +0 -53
  52. package/src/components/audit-statuses-section.tsx +0 -65
  53. package/src/components/count-summary-circle.tsx +0 -66
  54. package/src/components/pages/all-audits-page.tsx +0 -65
  55. package/src/components/pages/issues-page.tsx +0 -65
  56. package/src/components/score-bar.tsx +0 -56
  57. package/src/components/score-circle.tsx +0 -60
  58. package/src/components/severity-filter-chips.tsx +0 -57
  59. package/src/utils/score-thresholds.ts +0 -23
@@ -1,14 +1,14 @@
1
1
  import * as React from 'react';
2
- import { Box, Chip, Typography } from '@elementor/ui';
2
+ import { Box, Typography } from '@elementor/ui';
3
3
  import { __ } from '@wordpress/i18n';
4
4
 
5
5
  import { ALL_CATEGORIES, CATEGORY_LABELS } from '../../constants';
6
6
  import { type AuditCategory, type PageAuditReport } from '../../types';
7
7
  import { getPopulatedCategories } from '../../utils/audit-status-summary';
8
- import { getScoreTier } from '../../utils/score-thresholds';
8
+ import { countSeverities } from '../../utils/severity-counts';
9
+ import IssuesCategoryRow from '../issues-category-row';
9
10
  import Promotions from '../promotions';
10
- import ScoreBar from '../score-bar';
11
- import ScoreCircle from '../score-circle';
11
+ import SeveritySummaryCards from '../severity-summary-cards';
12
12
 
13
13
  type Props = {
14
14
  onCategoryClick: ( category: AuditCategory ) => void;
@@ -17,35 +17,30 @@ type Props = {
17
17
 
18
18
  export default function OverviewPage( { onCategoryClick, report }: Props ) {
19
19
  const populatedCategories = getPopulatedCategories( report.categories, ALL_CATEGORIES );
20
- const overallScore = getScoreTier( report.overall );
20
+
21
+ const categoryRows = populatedCategories
22
+ .map( ( category ) => ( { category, counts: countSeverities( report, category ) } ) )
23
+ .filter( ( { counts } ) => Object.values( counts ).some( ( count ) => count > 0 ) );
21
24
 
22
25
  return (
23
- <Box sx={ { display: 'flex', flexDirection: 'column', gap: 4, p: 2 } }>
24
- <Box sx={ { display: 'flex', alignItems: 'center', gap: 2 } }>
25
- <ScoreCircle color={ overallScore.color } score={ report.overall } />
26
- <Box sx={ { display: 'flex', flexDirection: 'column', gap: 0.5 } }>
27
- <Chip
28
- label={ overallScore.label }
29
- color={ overallScore.color }
30
- variant="standard"
31
- size="small"
32
- sx={ { fontWeight: 600, alignSelf: 'flex-start' } }
33
- />
34
- <Typography variant="body2" color="text.secondary">
35
- { __( 'Overall score', 'elementor' ) }
36
- </Typography>
26
+ <Box display="flex" flexDirection="column" gap={ 3 } p={ 2 }>
27
+ <SeveritySummaryCards report={ report } />
28
+ <Box display="flex" flexDirection="column" gap={ 1 }>
29
+ <Typography variant="subtitle1" component="h2">
30
+ { __( 'All issues', 'elementor' ) }
31
+ </Typography>
32
+ <Box display="flex" flexDirection="column" gap={ 1 }>
33
+ { categoryRows.map( ( { category, counts } ) => (
34
+ <IssuesCategoryRow
35
+ key={ category }
36
+ category={ category }
37
+ label={ CATEGORY_LABELS[ category ] }
38
+ counts={ counts }
39
+ onClick={ () => onCategoryClick( category ) }
40
+ />
41
+ ) ) }
37
42
  </Box>
38
43
  </Box>
39
- <Box sx={ { display: 'flex', flexDirection: 'column', gap: 2 } }>
40
- { populatedCategories.map( ( category ) => (
41
- <ScoreBar
42
- key={ category }
43
- label={ CATEGORY_LABELS[ category ] }
44
- score={ report.categories[ category ].score }
45
- onClick={ () => onCategoryClick( category ) }
46
- />
47
- ) ) }
48
- </Box>
49
44
  <Promotions report={ report } />
50
45
  </Box>
51
46
  );
@@ -1,114 +1,33 @@
1
1
  import * as React from 'react';
2
2
  import { useState } from 'react';
3
- import { Box, Divider, Tab, Tabs } from '@elementor/ui';
4
- import { __ } from '@wordpress/i18n';
3
+ import { Box } from '@elementor/ui';
5
4
 
6
5
  import { type AuditCategory, type PageAuditReport } from '../types';
7
- import { type AuditStatusGroup } from '../utils/audit-status-summary';
8
- import AllAuditsPage from './pages/all-audits-page';
9
6
  import CategoryPage from './pages/category-page';
10
- import IssuesPage from './pages/issues-page';
11
7
  import OverviewPage from './pages/overview-page';
12
8
 
13
- type TabId = 'overview' | 'issues';
14
-
15
- type ActivePage =
16
- | 'overview'
17
- | 'issues'
18
- | { backTo: TabId; category: AuditCategory }
19
- | { backTo: TabId; expand?: AuditStatusGroup; page: 'all-audits' };
9
+ type ActivePage = 'overview' | { category: AuditCategory };
20
10
 
21
11
  type Props = {
22
12
  report: PageAuditReport;
23
13
  };
24
14
 
25
- function activeTab( page: ActivePage ): TabId {
26
- if ( page === 'overview' ) {
27
- return 'overview';
28
- }
29
-
30
- if ( page === 'issues' ) {
31
- return 'issues';
32
- }
33
-
34
- return page.backTo;
35
- }
36
-
37
- function isAllAuditsPage( page: ActivePage ): page is { backTo: TabId; expand?: AuditStatusGroup; page: 'all-audits' } {
38
- return typeof page === 'object' && 'page' in page && page.page === 'all-audits';
39
- }
40
-
41
- function isCategoryPage( page: ActivePage ): page is { backTo: TabId; category: AuditCategory } {
15
+ function isCategoryPage( page: ActivePage ): page is { category: AuditCategory } {
42
16
  return typeof page === 'object' && 'category' in page;
43
17
  }
44
18
 
45
19
  export default function ReportShell( { report }: Props ) {
46
20
  const [ activePage, setActivePage ] = useState< ActivePage >( 'overview' );
47
21
 
48
- const currentTab = activeTab( activePage );
49
-
50
- const handleTabChange = ( _: React.SyntheticEvent, value: TabId ) => {
51
- setActivePage( value );
52
- };
53
-
54
- const openCategory = ( category: AuditCategory, backTo: TabId ) => {
55
- setActivePage( { category, backTo } );
56
- };
57
-
58
- const openAllAudits = ( expand?: AuditStatusGroup, backTo: TabId = 'issues' ) => {
59
- setActivePage( { page: 'all-audits', expand, backTo } );
60
- };
61
-
62
- const backFromSubPage = () => {
63
- if ( isAllAuditsPage( activePage ) || isCategoryPage( activePage ) ) {
64
- setActivePage( activePage.backTo );
65
- }
66
- };
22
+ const openCategory = ( category: AuditCategory ) => setActivePage( { category } );
23
+ const backToOverview = () => setActivePage( 'overview' );
67
24
 
68
25
  return (
69
- <Box sx={ { display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 } }>
70
- <Box sx={ { flexShrink: 0 } }>
71
- <Tabs
72
- aria-label={ __( 'Audit navigation', 'elementor' ) }
73
- value={ currentTab }
74
- onChange={ handleTabChange }
75
- textColor="secondary"
76
- indicatorColor="secondary"
77
- size="small"
78
- centered={ true }
79
- variant="fullWidth"
80
- >
81
- <Tab value="overview" label={ __( 'Overview', 'elementor' ) } />
82
- <Tab value="issues" label={ __( 'Issues', 'elementor' ) } />
83
- </Tabs>
84
- <Divider />
85
- </Box>
86
- <Box sx={ { flex: 1, minHeight: 0, overflowY: 'auto' } }>
87
- { activePage === 'overview' && (
88
- <OverviewPage
89
- report={ report }
90
- onCategoryClick={ ( category ) => openCategory( category, 'overview' ) }
91
- />
92
- ) }
93
- { activePage === 'issues' && (
94
- <IssuesPage
95
- report={ report }
96
- onCategoryClick={ ( category ) => openCategory( category, 'issues' ) }
97
- onAllAuditsClick={ () => openAllAudits() }
98
- onStatusClick={ ( status ) => openAllAudits( status ) }
99
- />
100
- ) }
101
- { isAllAuditsPage( activePage ) && (
102
- <AllAuditsPage
103
- report={ report }
104
- initialExpandedStatus={ activePage.expand }
105
- onBack={ backFromSubPage }
106
- />
107
- ) }
108
- { isCategoryPage( activePage ) && (
109
- <CategoryPage category={ activePage.category } report={ report } onBack={ backFromSubPage } />
110
- ) }
111
- </Box>
26
+ <Box sx={ { flex: 1, minHeight: 0, overflowY: 'auto' } }>
27
+ { activePage === 'overview' && <OverviewPage report={ report } onCategoryClick={ openCategory } /> }
28
+ { isCategoryPage( activePage ) && (
29
+ <CategoryPage category={ activePage.category } report={ report } onBack={ backToOverview } />
30
+ ) }
112
31
  </Box>
113
32
  );
114
33
  }
@@ -0,0 +1,38 @@
1
+ import * as React from 'react';
2
+ import { useFloatingPanelZIndex } from '@elementor/editor-floating-panels';
3
+ import { ReloadIcon } from '@elementor/icons';
4
+ import { Badge, Button, Tooltip } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ import { AUDIT_PANEL_ID } from '../constants';
8
+
9
+ type Props = {
10
+ hasScanned: boolean;
11
+ isStale: boolean;
12
+ disabled: boolean;
13
+ onClick: () => void;
14
+ };
15
+
16
+ export default function RescanButton( { hasScanned, isStale, disabled, onClick }: Props ) {
17
+ const panelZIndex = useFloatingPanelZIndex( AUDIT_PANEL_ID );
18
+
19
+ return (
20
+ <Tooltip
21
+ title={ __( 'Page content changed - rescan advised', 'elementor' ) }
22
+ disableHoverListener={ ! isStale }
23
+ PopperProps={ { sx: { zIndex: panelZIndex } } }
24
+ >
25
+ <Badge variant="dot" color="warning" invisible={ ! isStale } overlap="rectangular">
26
+ <Button
27
+ variant="contained"
28
+ size="small"
29
+ startIcon={ hasScanned ? <ReloadIcon fontSize="small" /> : undefined }
30
+ onClick={ onClick }
31
+ disabled={ disabled }
32
+ >
33
+ { hasScanned ? __( 'Rescan', 'elementor' ) : __( 'Run page audit', 'elementor' ) }
34
+ </Button>
35
+ </Badge>
36
+ </Tooltip>
37
+ );
38
+ }
@@ -20,7 +20,11 @@ type Props = {
20
20
  severity: AuditSeverity;
21
21
  };
22
22
 
23
+ const INFO_ICON_VERTICAL_NUDGE = 'translateY(-2px)';
24
+
23
25
  export default function SeverityIcon( { severity }: Props ) {
24
26
  const { Icon, color } = SEVERITY_CONFIG[ severity ];
25
- return <Icon fontSize="small" color={ color } />;
27
+ const sx = 'info' === severity ? { transform: INFO_ICON_VERTICAL_NUDGE } : undefined;
28
+
29
+ return <Icon fontSize="small" color={ color } sx={ sx } />;
26
30
  }
@@ -0,0 +1,52 @@
1
+ import * as React from 'react';
2
+ import { Box, Typography } from '@elementor/ui';
3
+
4
+ import { type AuditSeverity, type PageAuditReport } from '../types';
5
+ import { ALL_SEVERITIES, countSeverities, severityPluralLabel } from '../utils/severity-counts';
6
+ import SeverityIcon from './severity-icons';
7
+
8
+ type Props = {
9
+ report: PageAuditReport;
10
+ };
11
+
12
+ export default function SeveritySummaryCards( { report }: Props ) {
13
+ const counts = countSeverities( report );
14
+
15
+ return (
16
+ <Box display="flex" gap={ 1.5 }>
17
+ { ALL_SEVERITIES.map( ( severity ) => (
18
+ <SeverityCard key={ severity } severity={ severity } count={ counts[ severity ] } />
19
+ ) ) }
20
+ </Box>
21
+ );
22
+ }
23
+
24
+ type SeverityCardProps = {
25
+ severity: AuditSeverity;
26
+ count: number;
27
+ };
28
+
29
+ function SeverityCard( { severity, count }: SeverityCardProps ) {
30
+ return (
31
+ <Box
32
+ display="flex"
33
+ flex={ 1 }
34
+ flexDirection="column"
35
+ gap={ 1 }
36
+ bgcolor="action.hover"
37
+ borderRadius={ 1.5 }
38
+ px={ 1.5 }
39
+ py={ 1 }
40
+ >
41
+ <SeverityIcon severity={ severity } />
42
+ <Box display="flex" flexDirection="column">
43
+ <Typography variant="h5" fontWeight={ 700 } component="span">
44
+ { count }
45
+ </Typography>
46
+ <Typography variant="caption" color="text.secondary">
47
+ { severityPluralLabel( severity ) }
48
+ </Typography>
49
+ </Box>
50
+ </Box>
51
+ );
52
+ }
@@ -1,12 +1,15 @@
1
1
  import * as React from 'react';
2
+ import { WandIcon } from '@elementor/icons';
2
3
  import { Button } from '@elementor/ui';
3
4
 
4
5
  type Props = {
5
6
  ctaLabel: string;
6
7
  externalUrl: string;
8
+ variant?: 'outlined' | 'text';
9
+ withIcon?: boolean;
7
10
  };
8
11
 
9
- export default function ViolationCtaButton( { ctaLabel, externalUrl }: Props ) {
12
+ export default function ViolationCtaButton( { ctaLabel, externalUrl, variant = 'outlined', withIcon = false }: Props ) {
10
13
  const handleClick = ( event: React.MouseEvent< HTMLButtonElement > ) => {
11
14
  event.stopPropagation();
12
15
  event.preventDefault();
@@ -15,7 +18,13 @@ export default function ViolationCtaButton( { ctaLabel, externalUrl }: Props ) {
15
18
  };
16
19
 
17
20
  return (
18
- <Button variant="outlined" color="secondary" size="small" onClick={ handleClick }>
21
+ <Button
22
+ variant={ variant }
23
+ color="secondary"
24
+ size="small"
25
+ startIcon={ withIcon ? <WandIcon fontSize="tiny" aria-hidden={ true } /> : undefined }
26
+ onClick={ handleClick }
27
+ >
19
28
  { ctaLabel }
20
29
  </Button>
21
30
  );
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { FileSettingsIcon, SettingsIcon, ShieldCheckIcon } from '@elementor/icons';
2
+ import { FileSettingsIcon, SettingsIcon } from '@elementor/icons';
3
3
  import { Box } from '@elementor/ui';
4
4
 
5
5
  import { type AuditViolation } from '../types';
@@ -29,5 +29,5 @@ export default function ViolationIcon( { violation, widgetIcon }: Props ) {
29
29
  return <SettingsIcon fontSize="inherit" aria-hidden={ true } />;
30
30
  }
31
31
 
32
- return <ShieldCheckIcon fontSize="inherit" aria-hidden={ true } />;
32
+ return <SettingsIcon fontSize="inherit" aria-hidden={ true } />;
33
33
  }
@@ -1,10 +1,11 @@
1
1
  import * as React from 'react';
2
- import { useState } from 'react';
3
2
  import { getElementIcon, getElementTitle } from '@elementor/editor-elements';
4
- import { AlertCircleIcon, BulbIcon, CheckIcon, ChevronDownIcon, EyeIcon, HelpIcon } from '@elementor/icons';
5
- import { Alert, AlertTitle, Box, Collapse, IconButton, Tooltip, Typography } from '@elementor/ui';
3
+ import { useFloatingPanelZIndex } from '@elementor/editor-floating-panels';
4
+ import { CheckIcon, ChevronDownIcon, HelpIcon, InfoCircleIcon } from '@elementor/icons';
5
+ import { Alert, Box, Collapse, IconButton, Tooltip, Typography } from '@elementor/ui';
6
6
  import { __ } from '@wordpress/i18n';
7
7
 
8
+ import { AUDIT_PANEL_ID } from '../constants';
8
9
  import { focusViolation } from '../hooks/focus-violation';
9
10
  import { type AuditMeta, type AuditViolation } from '../types';
10
11
  import { buildAngiePrompt } from '../utils/build-angie-prompt';
@@ -17,6 +18,8 @@ import ViolationIcon from './violation-icons';
17
18
 
18
19
  type Props = {
19
20
  audit: AuditMeta;
21
+ expanded: boolean;
22
+ onToggleExpand: () => void;
20
23
  skipReason?: string;
21
24
  violations?: AuditViolation[];
22
25
  };
@@ -35,12 +38,12 @@ function StatusIndicator( { audit, violations }: Pick< Props, 'audit' | 'violati
35
38
  if ( violations ) {
36
39
  return (
37
40
  <>
41
+ <SeverityIcon severity={ audit.severity } />
38
42
  { isScoredAudit( audit ) && (
39
43
  <Typography variant="caption" color="text.secondary" fontWeight="bold">
40
44
  { violations.length }
41
45
  </Typography>
42
46
  ) }
43
- <SeverityIcon severity={ audit.severity } />
44
47
  </>
45
48
  );
46
49
  }
@@ -48,10 +51,73 @@ function StatusIndicator( { audit, violations }: Pick< Props, 'audit' | 'violati
48
51
  return <CheckIcon fontSize="small" color="success" />;
49
52
  }
50
53
 
51
- export default function ViolationRow( { audit, skipReason, violations }: Props ) {
52
- const [ expanded, setExpanded ] = useState( false );
54
+ function GuidanceAction( {
55
+ violation,
56
+ sharedAngiePrompt,
57
+ panelZIndex,
58
+ }: {
59
+ violation: AuditViolation;
60
+ sharedAngiePrompt?: string;
61
+ panelZIndex: number;
62
+ } ) {
63
+ const hasPrimaryCta = !! ( violation.ctaLabel && violation.externalUrl );
53
64
 
54
- const toggleExpanded = () => setExpanded( ( value ) => ! value );
65
+ if ( ! hasPrimaryCta && ! sharedAngiePrompt ) {
66
+ return null;
67
+ }
68
+
69
+ return (
70
+ <Box display="flex" alignItems="center" gap={ 1 } mt={ 1 }>
71
+ { hasPrimaryCta && (
72
+ <ViolationCtaButton
73
+ ctaLabel={ violation.ctaLabel as string }
74
+ externalUrl={ violation.externalUrl as string }
75
+ withIcon
76
+ />
77
+ ) }
78
+ { hasPrimaryCta && violation.secondaryCtaLabel && violation.secondaryCtaUrl && (
79
+ <ViolationCtaButton
80
+ ctaLabel={ violation.secondaryCtaLabel }
81
+ externalUrl={ violation.secondaryCtaUrl }
82
+ variant="text"
83
+ />
84
+ ) }
85
+ { sharedAngiePrompt && (
86
+ <FixViolationWithAngie prompt={ sharedAngiePrompt } panelZIndex={ panelZIndex } variant="button" />
87
+ ) }
88
+ </Box>
89
+ );
90
+ }
91
+
92
+ function getSharedAngiePrompt( violations?: AuditViolation[] ): string | undefined {
93
+ if ( ! violations || violations.length < 2 ) {
94
+ return undefined;
95
+ }
96
+
97
+ const [ first, ...rest ] = violations;
98
+
99
+ if ( ! first.angieFix || ! first.angiePrompt ) {
100
+ return undefined;
101
+ }
102
+
103
+ const allShareTheSameFix = rest.every(
104
+ ( violation ) => violation.angieFix && violation.angiePrompt === first.angiePrompt
105
+ );
106
+
107
+ return allShareTheSameFix ? first.angiePrompt : undefined;
108
+ }
109
+
110
+ export default function ViolationRow( { audit, expanded, onToggleExpand, skipReason, violations }: Props ) {
111
+ const primaryViolation = violations?.[ 0 ];
112
+ const isGuidanceFocusable = !! primaryViolation;
113
+ const panelZIndex = useFloatingPanelZIndex( AUDIT_PANEL_ID );
114
+ const sharedAngiePrompt = getSharedAngiePrompt( violations );
115
+
116
+ const handleGuidanceClick = () => {
117
+ if ( isGuidanceFocusable && primaryViolation ) {
118
+ focusViolation( primaryViolation );
119
+ }
120
+ };
55
121
 
56
122
  return (
57
123
  <Box sx={ { borderBottom: 1, borderColor: 'divider', paddingBlock: 0.5 } }>
@@ -65,9 +131,9 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
65
131
  gap: 0.5,
66
132
  minWidth: 0,
67
133
  } }
68
- onClick={ toggleExpanded }
134
+ onClick={ onToggleExpand }
69
135
  >
70
- <Typography variant="body2" sx={ { flex: 1 } }>
136
+ <Typography variant="subtitle2" color="text.secondary" sx={ { flex: 1 } }>
71
137
  { audit.title }
72
138
  </Typography>
73
139
  { ! skipReason && <StatusIndicator audit={ audit } violations={ violations } /> }
@@ -76,7 +142,7 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
76
142
  <IconButton
77
143
  size="small"
78
144
  aria-label={ expanded ? __( 'Collapse', 'elementor' ) : __( 'Expand', 'elementor' ) }
79
- onClick={ toggleExpanded }
145
+ onClick={ onToggleExpand }
80
146
  >
81
147
  <ChevronDownIcon
82
148
  fontSize="small"
@@ -88,44 +154,40 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
88
154
  </IconButton>
89
155
  </Box>
90
156
  <Collapse in={ expanded }>
91
- <Box sx={ { display: 'flex', flexDirection: 'column', gap: 1, paddingBlock: 1 } }>
92
- <Alert
93
- severity="secondary"
94
- sx={ { p: 1 } }
95
- icon={ <AlertCircleIcon fontSize="small" color="secondary" aria-hidden={ true } /> }
96
- >
97
- <AlertTitle>
98
- <Typography variant="caption" component="p" color="text.primary" fontWeight="bold">
99
- { __( "What's the issue", 'elementor' ) }
157
+ <Box paddingBlock={ 1 }>
158
+ <Tooltip title={ audit.fixHint } placement="top">
159
+ <Alert
160
+ severity="secondary"
161
+ sx={ { p: 1, cursor: isGuidanceFocusable ? 'pointer' : undefined } }
162
+ icon={ <InfoCircleIcon fontSize="small" aria-hidden={ true } /> }
163
+ role={ isGuidanceFocusable ? 'button' : undefined }
164
+ tabIndex={ isGuidanceFocusable ? 0 : undefined }
165
+ onClick={ isGuidanceFocusable ? handleGuidanceClick : undefined }
166
+ onKeyDown={ isGuidanceFocusable ? onKeyboardClick( handleGuidanceClick ) : undefined }
167
+ >
168
+ <Typography variant="caption" component="p" color="text.secondary">
169
+ { audit.description }
100
170
  </Typography>
101
- </AlertTitle>
102
- <Typography variant="caption" component="p" color="text.secondary">
103
- { audit.description }
104
- </Typography>
105
- </Alert>
106
- <Alert
107
- severity="info"
108
- sx={ { p: 1 } }
109
- icon={ <BulbIcon fontSize="small" color="info" aria-hidden={ true } /> }
110
- >
111
- <AlertTitle>
112
- <Typography variant="caption" component="p" color="text.primary" fontWeight="bold">
113
- { __( 'How to resolve', 'elementor' ) }
114
- </Typography>
115
- </AlertTitle>
116
- <Typography variant="caption" component="p" color="text.secondary">
117
- { audit.fixHint }
118
- </Typography>
119
- </Alert>
171
+ { primaryViolation && (
172
+ <GuidanceAction
173
+ violation={ primaryViolation }
174
+ sharedAngiePrompt={ sharedAngiePrompt }
175
+ panelZIndex={ panelZIndex }
176
+ />
177
+ ) }
178
+ </Alert>
179
+ </Tooltip>
120
180
  </Box>
121
181
  { violations && violations.length > 0 && (
122
- <Box role="list" sx={ { paddingBlockEnd: 1, paddingInlineStart: 2 } }>
182
+ <Box role="list" sx={ { paddingBlockEnd: 1 } }>
123
183
  { violations.map( ( violation, idx ) => {
124
184
  const widgetIcon = violation.elementId ? getElementIcon( violation.elementId ) : null;
125
185
  const elementTitle = violation.elementId ? getElementTitle( violation.elementId ) : null;
126
- const rowLabel = elementTitle
186
+ const displayLabel = elementTitle ?? violation.label;
187
+ const angiePromptContext = elementTitle
127
188
  ? `${ elementTitle } - ${ violation.label }`
128
189
  : violation.label;
190
+ const showsOwnFixAction = !! violation.angieFix && ! sharedAngiePrompt;
129
191
 
130
192
  return (
131
193
  <Box
@@ -139,41 +201,37 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
139
201
  alignItems: 'center',
140
202
  gap: 1,
141
203
  paddingBlock: 0.5,
142
- paddingInline: 2,
204
+ paddingInline: 1,
143
205
  borderRadius: 1,
144
206
  cursor: 'pointer',
145
- '& .violation-hover-icon': { opacity: 0, transition: 'opacity .15s' },
146
207
  '&:hover': { bgcolor: 'action.hover' },
147
- '&:hover .violation-hover-icon, &:focus-visible .violation-hover-icon': {
148
- opacity: 1,
149
- },
208
+ '&:hover .violation-row-fix-action, &:focus-visible .violation-row-fix-action':
209
+ {
210
+ opacity: 1,
211
+ },
150
212
  } }
151
213
  >
152
214
  <ViolationIcon violation={ violation } widgetIcon={ widgetIcon } />
153
215
  <Box sx={ { flex: 1 } }>
154
- <Typography variant="caption">{ rowLabel }</Typography>
216
+ <Typography variant="caption">{ displayLabel }</Typography>
155
217
  { violation.detail && (
156
218
  <Typography variant="caption" color="text.secondary">
157
219
  { violation.detail }
158
220
  </Typography>
159
221
  ) }
160
222
  </Box>
161
- { violation.angieFix && (
162
- <FixViolationWithAngie
163
- prompt={ violation.angiePrompt ?? buildAngiePrompt( rowLabel ) }
164
- />
165
- ) }
166
- { violation.ctaLabel && violation.externalUrl ? (
167
- <ViolationCtaButton
168
- ctaLabel={ violation.ctaLabel }
169
- externalUrl={ violation.externalUrl }
170
- />
171
- ) : (
172
- <EyeIcon
173
- className="violation-hover-icon"
174
- fontSize="tiny"
175
- aria-hidden={ true }
176
- />
223
+ { showsOwnFixAction && (
224
+ <Box
225
+ className="violation-row-fix-action"
226
+ sx={ { opacity: 0, transition: 'opacity .15s' } }
227
+ >
228
+ <FixViolationWithAngie
229
+ prompt={
230
+ violation.angiePrompt ?? buildAngiePrompt( angiePromptContext )
231
+ }
232
+ panelZIndex={ panelZIndex }
233
+ />
234
+ </Box>
177
235
  ) }
178
236
  </Box>
179
237
  );
@@ -1,9 +1,9 @@
1
1
  import { useEffect } from 'react';
2
- import { getCurrentDocumentId } from '@elementor/editor-elements';
2
+ import { getHostDocumentId } from '@elementor/editor-elements';
3
3
  import { __useDispatch as useDispatch, __useSelector as useSelector } from '@elementor/store';
4
4
 
5
5
  import { runPageAudit } from '../runner';
6
- import { type GlobalState, selectError, selectReport, selectStatus, slice } from '../store';
6
+ import { type GlobalState, selectError, selectIsStale, selectReport, selectStatus, slice } from '../store';
7
7
  import { getPersistedReport, persistReport } from '../utils/report-storage';
8
8
  import { SessionExpiredError } from '../utils/session-expiration';
9
9
 
@@ -11,8 +11,9 @@ export function useAuditReport() {
11
11
  const status = useSelector( ( state: GlobalState ) => selectStatus( state ) );
12
12
  const report = useSelector( ( state: GlobalState ) => selectReport( state ) );
13
13
  const error = useSelector( ( state: GlobalState ) => selectError( state ) );
14
+ const isStale = useSelector( ( state: GlobalState ) => selectIsStale( state ) );
14
15
  const dispatch = useDispatch();
15
- const documentId = getCurrentDocumentId() ?? 0;
16
+ const documentId = getHostDocumentId() ?? 0;
16
17
 
17
18
  useEffect( () => {
18
19
  if ( ! documentId || report?.documentId === documentId ) {
@@ -45,5 +46,5 @@ export function useAuditReport() {
45
46
  }
46
47
  };
47
48
 
48
- return { status, report, error, run };
49
+ return { status, report, error, isStale, run };
49
50
  }
@@ -1,7 +1,7 @@
1
1
  import { type ToggleActionProps } from '@elementor/editor-app-bar';
2
- import { ShieldCheckIcon } from '@elementor/icons';
3
2
  import { __ } from '@wordpress/i18n';
4
3
 
4
+ import AuditToggleIcon from '../components/audit-toggle-icon';
5
5
  import { auditPanel } from '../editor-panel';
6
6
 
7
7
  export function useAuditToggleProps(): ToggleActionProps {
@@ -10,7 +10,7 @@ export function useAuditToggleProps(): ToggleActionProps {
10
10
 
11
11
  return {
12
12
  title: __( 'Audit Page', 'elementor' ),
13
- icon: ShieldCheckIcon,
13
+ icon: AuditToggleIcon,
14
14
  selected: isOpen,
15
15
  onClick: () => toggle(),
16
16
  };