diff --git a/projects/packages/my-jetpack/_inc/components/info-tooltip/index.tsx b/projects/packages/my-jetpack/_inc/components/info-tooltip/index.tsx index b4e6a62eae873..c0773c3ba6d2b 100644 --- a/projects/packages/my-jetpack/_inc/components/info-tooltip/index.tsx +++ b/projects/packages/my-jetpack/_inc/components/info-tooltip/index.tsx @@ -3,18 +3,18 @@ import { Popover } from '@wordpress/components'; import { useViewportMatch } from '@wordpress/compose'; import { useState, useCallback, useRef } from 'react'; import useAnalytics from '../../hooks/use-analytics'; -import type { FC, ReactNode } from 'react'; +import type { PopoverProps } from './types'; +import type { FC } from 'react'; import './style.scss'; -type Props = { - children: ReactNode; +interface Props extends PopoverProps { className?: string; icon?: string; iconSize?: number; tracksEventName?: string; tracksEventProps?: Record< Lowercase< string >, unknown >; -}; +} export const InfoTooltip: FC< Props > = ( { children, diff --git a/projects/packages/my-jetpack/_inc/components/info-tooltip/types.ts b/projects/packages/my-jetpack/_inc/components/info-tooltip/types.ts new file mode 100644 index 0000000000000..8fa06ae06e05f --- /dev/null +++ b/projects/packages/my-jetpack/_inc/components/info-tooltip/types.ts @@ -0,0 +1,210 @@ +/* eslint-disable jsdoc/check-indentation */ +import type { ReactNode, MutableRefObject, SyntheticEvent } from 'react'; + +type PositionYAxis = 'top' | 'middle' | 'bottom'; +type PositionXAxis = 'left' | 'center' | 'right'; +type PositionCorner = 'top' | 'right' | 'bottom' | 'left'; + +type DomRectWithOwnerDocument = DOMRect & { + ownerDocument?: Document; +}; + +type PopoverPlacement = + | 'top' + | 'top-start' + | 'top-end' + | 'right' + | 'right-start' + | 'right-end' + | 'bottom' + | 'bottom-start' + | 'bottom-end' + | 'left' + | 'left-start' + | 'left-end' + | 'overlay'; + +export type PopoverAnchorRefReference = MutableRefObject< Element | null | undefined >; +export type PopoverAnchorRefTopBottom = { top: Element; bottom: Element }; + +export type VirtualElement = Pick< Element, 'getBoundingClientRect' > & { + ownerDocument?: Document; +}; + +export type PopoverProps = { + /** + * The name of the Slot in which the popover should be rendered. It should + * be also passed to the corresponding `PopoverSlot` component. + * + * @default 'Popover' + */ + __unstableSlotName?: string; + /** + * The element that should be used by the popover as its anchor. It can either + * be an `Element` or, alternatively, a `VirtualElement` — ie. an object with + * the `getBoundingClientRect()` and the `ownerDocument` properties defined. + * + * **The anchor element should be stored in local state** rather than a + * plain React ref to ensure reactive updating when it changes. + */ + anchor?: Element | VirtualElement | null; + /** + * Whether the popover should animate when opening. + * + * @default true + */ + animate?: boolean; + /** + * The `children` elements rendered as the popover's content. + */ + children: ReactNode; + /** + * Show the popover fullscreen on mobile viewports. + */ + expandOnMobile?: boolean; + /** + * Specifies whether the popover should flip across its axis if there isn't + * space for it in the normal placement. + * When the using a 'top' placement, the popover will switch to a 'bottom' + * placement. When using a 'left' placement, the popover will switch to a + * `right' placement. + * The popover will retain its alignment of 'start' or 'end' when flipping. + * + * @default true + */ + flip?: boolean; + /** + * Determines whether tabbing is constrained to within the popover, + * preventing keyboard focus from leaving the popover content without + * explicit focus elswhere, or whether the popover remains part of the wider + * tab order. If no value is passed, it will be derived from `focusOnMount`. + * + * @default `focusOnMount` !== false + */ + constrainTabbing?: boolean; + /** + * By default, the _first tabbable element_ in the popover will receive focus + * when it mounts. This is the same as setting this prop to `"firstElement"`. + * Specifying a `false` value disables the focus handling entirely (this + * should only be done when an appropriately accessible substitute behavior + * exists). + * + * @default 'firstElement' + */ + focusOnMount?: 'firstElement' | boolean; + /** + * A callback invoked when the focus leaves the opened popover. This should + * only be provided in advanced use-cases when a popover should close under + * specific circumstances (for example, if the new `document.activeElement` + * is content of or otherwise controlling popover visibility). + * + * When not provided, the `onClose` callback will be called instead. + */ + onFocusOutside?: ( event: SyntheticEvent ) => void; + /** + * Used to customize the header text shown when the popover is toggled to + * fullscreen on mobile viewports (see the `expandOnMobile` prop). + */ + headerTitle?: string; + /** + * Used to show/hide the arrow that points at the popover's anchor. + * + * @default true + */ + noArrow?: boolean; + /** + * The distance (in px) between the anchor and the popover. + */ + offset?: number; + /** + * A callback invoked when the popover should be closed. + */ + onClose?: () => void; + /** + * Used to specify the popover's position with respect to its anchor. + * + * @default 'bottom-start' + */ + placement?: PopoverPlacement; + /** + * Legacy way to specify the popover's position with respect to its anchor. + * _Note: this prop is deprecated. Use the `placement` prop instead._ + */ + position?: + | `${ PositionYAxis }` + | `${ PositionYAxis } ${ PositionXAxis }` + | `${ PositionYAxis } ${ PositionXAxis } ${ PositionCorner }`; + /** + * Adjusts the size of the popover to prevent its contents from going out of + * view when meeting the viewport edges. + * + * @default true + */ + resize?: boolean; + /** + * Enables the `Popover` to shift in order to stay in view when meeting the + * viewport edges. + * + * @default false + */ + shift?: boolean; + /** + * Specifies the popover's style. + * + * Leave undefined for the default style. Other values are: + * - 'unstyled': The popover is essentially without any visible style, it + * has no background, border, outline or drop shadow, but + * the popover contents are still displayed. + * - 'toolbar': A style that has no elevation, but a high contrast with + * other elements. This is matches the style of the + * `Toolbar` component. + * + * @default undefined + */ + variant?: 'unstyled' | 'toolbar'; + /** + * Whether to render the popover inline or within the slot. + * + * @default false + */ + inline?: boolean; + // Deprecated props + /** + * Prevent the popover from flipping and resizing when meeting the viewport + * edges. _Note: this prop is deprecated. Instead, provide use the individual + * `flip` and `resize` props._ + * + * @deprecated + */ + __unstableForcePosition?: boolean; + /** + * An object extending a `DOMRect` with an additional optional `ownerDocument` + * property, used to specify a fixed popover position. + * + * @deprecated + */ + anchorRect?: DomRectWithOwnerDocument; + /** + * Used to specify a fixed popover position. It can be an `Element`, a React + * reference to an `element`, an object with a `top` and a `bottom` properties + * (both pointing to elements), or a `range`. + * + * @deprecated + */ + anchorRef?: Element | PopoverAnchorRefReference | PopoverAnchorRefTopBottom | Range; + /** + * A function returning the same value as the one expected by the `anchorRect` + * prop, used to specify a dynamic popover position. + * + * @deprecated + */ + getAnchorRect?: ( fallbackReferenceElement: Element | null ) => DomRectWithOwnerDocument; + /** + * Used to enable a different visual style for the popover. + * _Note: this prop is deprecated. Use the `variant` prop with the + * 'toolbar' value instead._ + * + * @deprecated + */ + isAlternate?: boolean; +}; diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/auto-firewall-status.tsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/auto-firewall-status.tsx index 79a5c4238a446..e403c034a2b94 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/auto-firewall-status.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/auto-firewall-status.tsx @@ -1,3 +1,4 @@ +import { useViewportMatch } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; import clsx from 'clsx'; import useProduct from '../../../data/products/use-product'; @@ -41,6 +42,7 @@ export const AutoFirewallStatus = () => { */ function WafStatus( { status }: { status: 'active' | 'inactive' | 'off' } ) { const slug = 'protect'; + const isMobileViewport: boolean = useViewportMatch( 'medium', '<' ); const { detail } = useProduct( slug ); const { hasPaidPlanForProduct = false } = detail || {}; const tooltipContent = useProtectTooltipCopy(); @@ -78,6 +80,7 @@ function WafStatus( { status }: { status: 'active' | 'inactive' | 'off' } ) { feature: 'jetpack-protect', has_paid_plan: hasPaidPlanForProduct, } } + placement={ isMobileViewport ? 'top' : 'right' } > <>

{ autoFirewallTooltip.title }

diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/scan-threats-status.tsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/scan-threats-status.tsx index af25fe770b8e0..414ed3db040b9 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/scan-threats-status.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/scan-threats-status.tsx @@ -138,10 +138,10 @@ function ThreatStatus( { focusOnMount={ 'container' } onClose={ hideTooltip } > - <> +

{ scanThreatsTooltip.title }

{ scanThreatsTooltip.text }

- +
) } @@ -157,8 +157,22 @@ function ThreatStatus( { return ( <> -
+
{ __( 'Threats', 'jetpack-my-jetpack' ) } + + <> +

{ scanThreatsTooltip.title }

+

{ scanThreatsTooltip.text }

+ +
{ numThreats }
diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts index 6f95e251ea099..4561bf81eacf7 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts @@ -39,6 +39,7 @@ export function useProtectTooltipCopy(): TooltipContent { plugins: fromScanPlugins, themes: fromScanThemes, num_threats: numThreats = 0, + threats = [], } = scanData || {}; const { jetpack_waf_automatic_rules: isAutoFirewallEnabled, @@ -49,6 +50,12 @@ export function useProtectTooltipCopy(): TooltipContent { const pluginsCount = fromScanPlugins.length || Object.keys( plugins ).length; const themesCount = fromScanThemes.length || Object.keys( themes ).length; + const criticalThreatCount: number = useMemo( () => { + return threats.length + ? threats.reduce( ( accum, threat ) => ( threat.severity >= 5 ? ( accum += 1 ) : accum ), 0 ) + : 0; + }, [ threats ] ); + const settingsLink = useMemo( () => { if ( isProtectPluginActive ) { return 'admin.php?page=jetpack-protect#/firewall'; @@ -173,23 +180,32 @@ export function useProtectTooltipCopy(): TooltipContent { hasProtectPaidPlan && numThreats ? { title: __( 'Auto-fix threats', 'jetpack-my-jetpack' ), - text: sprintf( - /* translators: %s is the singular or plural of number of detected critical threats on the site. */ - __( - 'The last scan identified %s. But don’t worry, use the “Auto-fix” button in the product to automatically fix most threats.', - 'jetpack-my-jetpack' - ), - sprintf( - /* translators: %d is the number of detected scan threats on the site. */ - _n( - '%d critical threat.', - '%d critical threats.', - numThreats, - 'jetpack-my-jetpack' - ), - numThreats - ) - ), + text: criticalThreatCount + ? sprintf( + /* translators: %1$s is the number of threats and %2$s is the numner of critical threats on the site. */ + __( + 'The last scan identified %1$s (%2$d\u00A0critical). But don’t worry, use the “Auto-fix” button in the product to automatically fix most threats.', + 'jetpack-my-jetpack' + ), + sprintf( + /* translators: %d is the number of detected scan threats on the site. */ + _n( '%d threat', '%d threats', numThreats, 'jetpack-my-jetpack' ), + numThreats + ), + criticalThreatCount + ) + : sprintf( + /* translators: %s is the singular or plural of number of detected critical threats on the site. */ + __( + 'The last scan identified %s. But don’t worry, use the “Auto-fix” button in the product to automatically fix most threats.', + 'jetpack-my-jetpack' + ), + sprintf( + /* translators: %d is the number of detected scan threats on the site. */ + _n( '%d threat', '%d threats', numThreats, 'jetpack-my-jetpack' ), + numThreats + ) + ), } : { title: __( 'Elevate your malware protection', 'jetpack-my-jetpack' ), diff --git a/projects/packages/my-jetpack/global.d.ts b/projects/packages/my-jetpack/global.d.ts index 5ae0a3a54fcf6..134f55d8e4ef0 100644 --- a/projects/packages/my-jetpack/global.d.ts +++ b/projects/packages/my-jetpack/global.d.ts @@ -268,6 +268,7 @@ interface Window { plugins: ScanItem[]; status: string; themes: ScanItem[]; + threats?: ThreatItem[]; }; wafConfig: { automatic_rules_available: boolean;