@automattic/jetpack-components 0.58.0 → 0.59.0

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ### This is a list detailing changes for the Jetpack RNA Components package releases.
4
4
 
5
+ ## [0.59.0] - 2024-11-11
6
+ ### Added
7
+ - Add ThreatsDataViews component [#39754]
8
+ - Components: add ref for container component [#39850]
9
+ - IconTooltip: add support for showing tooltip on hover. [#39916]
10
+
11
+ ### Changed
12
+ - Add ToggleGroupControl to ThreatsDataViews for easily toggling between Active and Historical threats [#39901]
13
+ - Updated package dependencies. [#39999]
14
+ - Updated package dependencies. [#40000]
15
+ - Updated package dependencies. [#40060]
16
+
17
+ ## [0.58.1] - 2024-11-04
18
+ ### Added
19
+ - Enable test coverage. [#39961]
20
+
21
+ ### Fixed
22
+ - Fix tooltip behavior. [#39879]
23
+
5
24
  ## [0.58.0] - 2024-10-15
6
25
  ### Added
7
26
  - Add DiffViewer component [#39672]
@@ -1195,6 +1214,8 @@
1195
1214
  ### Changed
1196
1215
  - Update node version requirement to 14.16.1
1197
1216
 
1217
+ [0.59.0]: https://github.com/Automattic/jetpack-components/compare/0.58.1...0.59.0
1218
+ [0.58.1]: https://github.com/Automattic/jetpack-components/compare/0.58.0...0.58.1
1198
1219
  [0.58.0]: https://github.com/Automattic/jetpack-components/compare/0.57.0...0.58.0
1199
1220
  [0.57.0]: https://github.com/Automattic/jetpack-components/compare/0.56.3...0.57.0
1200
1221
  [0.56.3]: https://github.com/Automattic/jetpack-components/compare/0.56.2...0.56.3
@@ -0,0 +1,39 @@
1
+ import clsx from 'clsx';
2
+ import React from 'react';
3
+ import styles from './style.module.scss';
4
+
5
+ type BadgeProps = {
6
+ children?: React.ReactNode;
7
+ className?: string;
8
+ variant?: 'success' | 'warning' | 'danger';
9
+ [ key: string ]: unknown;
10
+ };
11
+
12
+ /**
13
+ * Badge component
14
+ *
15
+ * @param {object} props - The component properties.
16
+ * @param {string} props.variant - The badge variant (i.e. 'success', 'warning', 'danger').
17
+ * @param {JSX.Element} props.children - Badge text or content.
18
+ * @param {string} props.className - Additional class name to pass to the Badge component.
19
+ *
20
+ * @return {React.ReactElement} The `Badge` component.
21
+ */
22
+ const Badge: React.FC< BadgeProps > = ( { children, className, variant = 'info', ...props } ) => {
23
+ const classes = clsx(
24
+ styles.badge,
25
+ {
26
+ [ styles[ 'is-success' ] ]: variant === 'success',
27
+ [ styles[ 'is-warning' ] ]: variant === 'warning',
28
+ [ styles[ 'is-danger' ] ]: variant === 'danger',
29
+ },
30
+ className
31
+ );
32
+ return (
33
+ <span className={ classes } { ...props }>
34
+ { children }
35
+ </span>
36
+ );
37
+ };
38
+
39
+ export default Badge;
@@ -0,0 +1,25 @@
1
+ .badge {
2
+ display: inline-block;
3
+ border-radius: 4px;
4
+ background-color: var(--jp-gray-0);
5
+ color: var(--jp-gray-80);
6
+ padding: 4px 8px;
7
+ font-size: 13px;
8
+ font-weight: 400;
9
+ line-height: 16px;
10
+
11
+ &.is-success {
12
+ background-color: var(--jp-green-5);
13
+ color: var(--jp-green-50);
14
+ }
15
+
16
+ &.is-warning {
17
+ background-color: var(--jp-yellow-5);
18
+ color: var(--jp-yellow-60);
19
+ }
20
+
21
+ &.is-danger {
22
+ background-color: var(--jp-red-5);
23
+ color: var(--jp-red-70);
24
+ }
25
+ }
@@ -5,6 +5,17 @@ $white: #ffffff;
5
5
  .jb-score-tooltips-container {
6
6
  width: 100%;
7
7
  position: relative;
8
+ pointer-events: none;
9
+
10
+ }
11
+
12
+ .jb-score-tooltip-react-root {
13
+ position: absolute;
14
+ bottom: -20px;
15
+ translate: -50% calc( 100% - 20px );
16
+ z-index: 1000;
17
+ pointer-events: auto;
18
+ user-select: text;
8
19
  }
9
20
 
10
21
  .jb-score-tooltip {
@@ -17,6 +28,11 @@ $white: #ffffff;
17
28
  width: 20em;
18
29
  position: relative;
19
30
  box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.05);
31
+ display: none;
32
+
33
+ .visible &, &:hover {
34
+ display: block;
35
+ }
20
36
 
21
37
  hr {
22
38
  border-top: 1px solid $grey;
@@ -25,35 +25,26 @@ export function tooltipsPlugin( periods ) {
25
25
  if ( ! reactDom ) {
26
26
  reactDom = ReactDOM.createRoot( reactRoot );
27
27
  }
28
- reactRoot.style.position = 'absolute';
29
- reactRoot.style.bottom = -20 + 'px';
30
- reactRoot.style.translate = '-50% calc( 100% - 20px )';
31
- reactRoot.style.zIndex = '1000';
28
+ reactRoot.classList.add( 'jb-score-tooltip-react-root' );
32
29
 
33
30
  container.appendChild( reactRoot );
34
31
 
35
32
  u.over.appendChild( container );
36
33
 
37
- /**
38
- * Hides all tooltips.
39
- */
40
- function hideTips() {
41
- reactRoot.style.display = 'none';
42
- }
34
+ u.over.addEventListener( 'mouseenter', () => {
35
+ container.classList.add( 'visible' );
36
+ } );
43
37
 
44
- /**
45
- * Shows all tooltips.
46
- */
47
- function showTips() {
48
- reactRoot.style.display = null;
49
- }
38
+ u.over.addEventListener( 'mouseleave', () => {
39
+ container.classList.remove( 'visible' );
40
+ } );
50
41
 
51
- container.addEventListener( 'mouseleave', () => {
52
- hideTips();
42
+ reactRoot.addEventListener( 'mouseenter', () => {
43
+ reactRoot.classList.add( 'visible' );
53
44
  } );
54
45
 
55
- container.addEventListener( 'mouseenter', () => {
56
- showTips();
46
+ reactRoot.addEventListener( 'mouseleave', () => {
47
+ reactRoot.classList.remove( 'visible' );
57
48
  } );
58
49
  }
59
50
 
@@ -62,7 +53,7 @@ export function tooltipsPlugin( periods ) {
62
53
  * @param {uPlot} u - The uPlot instance.
63
54
  */
64
55
  function setSize( u: uPlot ) {
65
- container.style.height = u.over.clientHeight + 'px';
56
+ container.style.paddingTop = u.over.clientHeight + 'px';
66
57
  }
67
58
 
68
59
  /**
@@ -75,6 +66,10 @@ export function tooltipsPlugin( periods ) {
75
66
 
76
67
  const period = periods[ idx ];
77
68
 
69
+ if ( ! period ) {
70
+ return;
71
+ }
72
+
78
73
  // Timestamp of the cursor position
79
74
  const timestamp = u.data[ 0 ][ idx ];
80
75
 
@@ -38,12 +38,14 @@ const IconTooltip: React.FC< IconTooltipProps > = ( {
38
38
  children,
39
39
  popoverAnchorStyle = 'icon',
40
40
  forceShow = false,
41
+ hoverShow = false,
41
42
  wide = false,
42
43
  inline = true,
43
44
  shift = false,
44
45
  } ) => {
45
46
  const POPOVER_HELPER_WIDTH = 124;
46
47
  const [ isVisible, setIsVisible ] = useState( false );
48
+ const [ hoverTimeout, setHoverTimeout ] = useState( null );
47
49
  const hideTooltip = useCallback( () => setIsVisible( false ), [ setIsVisible ] );
48
50
  const toggleTooltip = useCallback(
49
51
  e => {
@@ -78,8 +80,33 @@ const IconTooltip: React.FC< IconTooltipProps > = ( {
78
80
 
79
81
  const isForcedToShow = isAnchorWrapper && forceShow;
80
82
 
83
+ const handleMouseEnter = useCallback( () => {
84
+ if ( hoverShow ) {
85
+ if ( hoverTimeout ) {
86
+ clearTimeout( hoverTimeout );
87
+ setHoverTimeout( null );
88
+ }
89
+ setIsVisible( true );
90
+ }
91
+ }, [ hoverShow, hoverTimeout ] );
92
+
93
+ const handleMouseLeave = useCallback( () => {
94
+ if ( hoverShow ) {
95
+ const id = setTimeout( () => {
96
+ setIsVisible( false );
97
+ setHoverTimeout( null );
98
+ }, 100 );
99
+ setHoverTimeout( id );
100
+ }
101
+ }, [ hoverShow ] );
102
+
81
103
  return (
82
- <div className={ wrapperClassNames } data-testid="icon-tooltip_wrapper">
104
+ <div
105
+ className={ wrapperClassNames }
106
+ data-testid="icon-tooltip_wrapper"
107
+ onMouseEnter={ handleMouseEnter }
108
+ onMouseLeave={ handleMouseLeave }
109
+ >
83
110
  { ! isAnchorWrapper && (
84
111
  <Button variant="link" onMouseDown={ toggleTooltip }>
85
112
  <Gridicon className={ iconClassName } icon={ iconCode } size={ iconSize } />
@@ -69,6 +69,11 @@ export type IconTooltipProps = {
69
69
  */
70
70
  forceShow?: boolean;
71
71
 
72
+ /**
73
+ * Enables the Popover to show on hover.
74
+ */
75
+ hoverShow?: boolean;
76
+
72
77
  /**
73
78
  * Uses a wider content area when enabled.
74
79
  */
@@ -1,5 +1,5 @@
1
1
  import clsx from 'clsx';
2
- import { createElement, useMemo } from 'react';
2
+ import { createElement, forwardRef, useMemo } from 'react';
3
3
  import { ContainerProps } from '../types';
4
4
  import styles from './style.module.scss';
5
5
  import type React from 'react';
@@ -7,17 +7,21 @@ import type React from 'react';
7
7
  /**
8
8
  * JP Container
9
9
  *
10
- * @param {ContainerProps} props - Component properties.
10
+ * @param {ContainerProps} props - Component properties.
11
+ * @param {React.MutableRefObject} ref - Ref to the component
11
12
  * @return {React.ReactElement} Container component.
12
13
  */
13
- const Container: React.FC< ContainerProps > = ( {
14
- children,
15
- fluid = false,
16
- tagName = 'div',
17
- className,
18
- horizontalGap = 1,
19
- horizontalSpacing = 1,
20
- } ) => {
14
+ const Container = (
15
+ {
16
+ children,
17
+ fluid = false,
18
+ tagName = 'div',
19
+ className,
20
+ horizontalGap = 1,
21
+ horizontalSpacing = 1,
22
+ }: ContainerProps,
23
+ ref: React.MutableRefObject< HTMLElement | null >
24
+ ): React.ReactElement => {
21
25
  const containerStyle = useMemo( () => {
22
26
  const padding = `calc( var(--horizontal-spacing) * ${ horizontalSpacing } )`;
23
27
  const rowGap = `calc( var(--horizontal-spacing) * ${ horizontalGap } )`;
@@ -38,9 +42,10 @@ const Container: React.FC< ContainerProps > = ( {
38
42
  {
39
43
  className: containerClassName,
40
44
  style: containerStyle,
45
+ ref,
41
46
  },
42
47
  children
43
48
  );
44
49
  };
45
50
 
46
- export default Container;
51
+ export default forwardRef< HTMLElement, ContainerProps >( Container );
@@ -1,5 +1,5 @@
1
1
  import clsx from 'clsx';
2
- import React, { useMemo, forwardRef } from 'react';
2
+ import React, { forwardRef, useMemo } from 'react';
3
3
  import { BOX_MODEL_VALUES, VARIANTS_MAPPING } from './constants';
4
4
  import styles from './style.module.scss';
5
5
  import type { H3Props, TextProps, TitleProps } from './types';
@@ -26,12 +26,11 @@ const Text = forwardRef< HTMLElement, TextProps >(
26
26
  }, '' );
27
27
  }, [ componentProps ] );
28
28
 
29
- componentProps.ref = ref;
30
-
31
29
  return (
32
30
  <Component
33
31
  className={ clsx( styles.reset, styles[ variant ], className, boxModelClasses ) }
34
32
  { ...componentProps }
33
+ ref={ ref }
35
34
  >
36
35
  { children }
37
36
  </Component>
@@ -39,7 +39,6 @@ export type TextProps = {
39
39
  children: React.ReactNode;
40
40
  /** Force an specific tag (span, div) or use a custom component that will receive className and children */
41
41
  component?: React.FC< { [ prop: string ]: unknown } > | React.ElementType;
42
- [ prop: string ]: unknown;
43
42
  };
44
43
 
45
44
  export type H3Props = TextProps & {
@@ -0,0 +1,142 @@
1
+ import { Button, Text, ActionPopover } from '@automattic/jetpack-components';
2
+ import { CONTACT_SUPPORT_URL, type Threat, fixerStatusIsStale } from '@automattic/jetpack-scan';
3
+ import { ExternalLink } from '@wordpress/components';
4
+ import { createInterpolateElement, useCallback, useMemo, useState } from '@wordpress/element';
5
+ import { __, sprintf } from '@wordpress/i18n';
6
+ import styles from './styles.module.scss';
7
+
8
+ /**
9
+ * Threat Fixer Button component.
10
+ *
11
+ * @param {object} props - Component props.
12
+ * @param {object} props.threat - The threat.
13
+ * @param {Function} props.onClick - The onClick function.
14
+ * @param {string} props.className - The className.
15
+ *
16
+ * @return {JSX.Element} The component.
17
+ */
18
+ export default function ThreatFixerButton( {
19
+ threat,
20
+ className,
21
+ onClick,
22
+ }: {
23
+ threat: Threat;
24
+ onClick: ( items: Threat[] ) => void;
25
+ className?: string;
26
+ } ): JSX.Element {
27
+ const [ isPopoverVisible, setIsPopoverVisible ] = useState( false );
28
+
29
+ const [ anchor, setAnchor ] = useState( null );
30
+
31
+ const children = useMemo( () => {
32
+ if ( ! threat.fixable ) {
33
+ return null;
34
+ }
35
+ if ( threat.fixer && 'error' in threat.fixer && threat.fixer.error ) {
36
+ return __( 'Error', 'jetpack' );
37
+ }
38
+ if ( threat.fixer && 'status' in threat.fixer && threat.fixer.status === 'in_progress' ) {
39
+ return __( 'Fixing…', 'jetpack' );
40
+ }
41
+ if ( threat.fixable.fixer === 'delete' ) {
42
+ return __( 'Delete', 'jetpack' );
43
+ }
44
+ if ( threat.fixable.fixer === 'update' ) {
45
+ return __( 'Update', 'jetpack' );
46
+ }
47
+ return __( 'Fix', 'jetpack' );
48
+ }, [ threat.fixable, threat.fixer ] );
49
+
50
+ const errorMessage = useMemo( () => {
51
+ if ( threat.fixer && fixerStatusIsStale( threat.fixer ) ) {
52
+ return __( 'The fixer is taking longer than expected.', 'jetpack' );
53
+ }
54
+
55
+ if ( threat.fixer && 'error' in threat.fixer && threat.fixer.error ) {
56
+ return __( 'An error occurred auto-fixing this threat.', 'jetpack' );
57
+ }
58
+
59
+ return null;
60
+ }, [ threat.fixer ] );
61
+
62
+ const handleClick = useCallback(
63
+ ( event: React.MouseEvent ) => {
64
+ event.stopPropagation();
65
+ if ( errorMessage && ! isPopoverVisible ) {
66
+ setIsPopoverVisible( true );
67
+ return;
68
+ }
69
+ onClick( [ threat ] );
70
+ },
71
+ [ onClick, errorMessage, isPopoverVisible, threat ]
72
+ );
73
+
74
+ const closePopover = useCallback( () => {
75
+ setIsPopoverVisible( false );
76
+ }, [] );
77
+
78
+ if ( ! threat.fixable ) {
79
+ return null;
80
+ }
81
+
82
+ return (
83
+ <div>
84
+ <Button
85
+ size="small"
86
+ weight="regular"
87
+ variant="secondary"
88
+ onClick={ handleClick }
89
+ children={ children }
90
+ className={ className }
91
+ disabled={
92
+ threat.fixer &&
93
+ 'status' in threat.fixer &&
94
+ threat.fixer.status === 'in_progress' &&
95
+ ! errorMessage
96
+ }
97
+ isLoading={
98
+ threat.fixer && 'status' in threat.fixer && threat.fixer.status === 'in_progress'
99
+ }
100
+ isDestructive={
101
+ ( threat.fixable && threat.fixable.fixer === 'delete' ) ||
102
+ ( threat.fixer && 'error' in threat.fixer && threat.fixer.error ) ||
103
+ ( threat.fixer && fixerStatusIsStale( threat.fixer ) )
104
+ }
105
+ style={ { minWidth: '72px' } }
106
+ ref={ setAnchor }
107
+ />
108
+ { isPopoverVisible && (
109
+ <ActionPopover
110
+ anchor={ anchor }
111
+ buttonContent={ __( 'Retry Fix', 'jetpack' ) }
112
+ hideCloseButton={ true }
113
+ noArrow={ false }
114
+ onClick={ handleClick }
115
+ onClose={ closePopover }
116
+ title={ __( 'Auto-fix error', 'jetpack' ) }
117
+ >
118
+ <Text>
119
+ { createInterpolateElement(
120
+ sprintf(
121
+ /* translators: placeholder is an error message. */
122
+ __(
123
+ '%s Please try again or <supportLink>contact support</supportLink>.',
124
+ 'jetpack'
125
+ ),
126
+ errorMessage
127
+ ),
128
+ {
129
+ supportLink: (
130
+ <ExternalLink
131
+ href={ CONTACT_SUPPORT_URL }
132
+ className={ styles[ 'support-link' ] }
133
+ />
134
+ ),
135
+ }
136
+ ) }
137
+ </Text>
138
+ </ActionPopover>
139
+ ) }
140
+ </div>
141
+ );
142
+ }
@@ -0,0 +1,9 @@
1
+ .support-link {
2
+ color: inherit;
3
+
4
+ &:focus,
5
+ &:hover {
6
+ color: inherit;
7
+ box-shadow: none;
8
+ }
9
+ }
@@ -1,34 +1,24 @@
1
1
  import { _x } from '@wordpress/i18n';
2
- import styles from './styles.module.scss';
2
+ import Badge from '../badge';
3
3
 
4
- const severityClassNames = severity => {
4
+ const ThreatSeverityBadge = ( { severity } ) => {
5
5
  if ( severity >= 5 ) {
6
- return 'is-critical';
7
- } else if ( severity >= 3 && severity < 5 ) {
8
- return 'is-high';
6
+ return (
7
+ <Badge variant="danger">
8
+ { _x( 'Critical', 'Severity label for issues rated 5 or higher.', 'jetpack' ) }
9
+ </Badge>
10
+ );
9
11
  }
10
- return 'is-low';
11
- };
12
12
 
13
- const severityText = severity => {
14
- if ( severity >= 5 ) {
15
- return _x( 'Critical', 'Severity label for issues rated 5 or higher.', 'jetpack' );
16
- } else if ( severity >= 3 && severity < 5 ) {
17
- return _x( 'High', 'Severity label for issues rated between 3 and 5.', 'jetpack' );
13
+ if ( severity >= 3 && severity < 5 ) {
14
+ return (
15
+ <Badge variant="warning">
16
+ { _x( 'High', 'Severity label for issues rated between 3 and 5.', 'jetpack' ) }
17
+ </Badge>
18
+ );
18
19
  }
19
- return _x( 'Low', 'Severity label for issues rated below 3.', 'jetpack' );
20
- };
21
20
 
22
- const ThreatSeverityBadge = ( { severity } ) => {
23
- return (
24
- <div
25
- className={ `${ styles[ 'threat-severity-badge' ] } ${
26
- styles[ severityClassNames( severity ) ]
27
- }` }
28
- >
29
- { severityText( severity ) }
30
- </div>
31
- );
21
+ return <Badge>{ _x( 'Low', 'Severity label for issues rated below 3.', 'jetpack' ) }</Badge>;
32
22
  };
33
23
 
34
24
  export default ThreatSeverityBadge;
@@ -0,0 +1,49 @@
1
+ import { __ } from '@wordpress/i18n';
2
+ import {
3
+ code as fileIcon,
4
+ color as themeIcon,
5
+ plugins as pluginIcon,
6
+ shield as shieldIcon,
7
+ wordpress as coreIcon,
8
+ } from '@wordpress/icons';
9
+
10
+ export const THREAT_STATUSES: { value: string; label: string; variant?: 'success' | 'warning' }[] =
11
+ [
12
+ { value: 'current', label: __( 'Active', 'jetpack' ), variant: 'warning' },
13
+ { value: 'fixed', label: __( 'Fixed', 'jetpack' ), variant: 'success' },
14
+ { value: 'ignored', label: __( 'Ignored', 'jetpack' ) },
15
+ ];
16
+
17
+ export const THREAT_TYPES = [
18
+ { value: 'plugin', label: __( 'Plugin', 'jetpack' ) },
19
+ { value: 'theme', label: __( 'Theme', 'jetpack' ) },
20
+ { value: 'core', label: __( 'WordPress', 'jetpack' ) },
21
+ { value: 'file', label: __( 'File', 'jetpack' ) },
22
+ ];
23
+
24
+ export const THREAT_ICONS = {
25
+ plugin: pluginIcon,
26
+ theme: themeIcon,
27
+ core: coreIcon,
28
+ file: fileIcon,
29
+ default: shieldIcon,
30
+ };
31
+
32
+ export const THREAT_FIELD_THREAT = 'threat';
33
+ export const THREAT_FIELD_TITLE = 'title';
34
+ export const THREAT_FIELD_DESCRIPTION = 'description';
35
+ export const THREAT_FIELD_ICON = 'icon';
36
+ export const THREAT_FIELD_STATUS = 'status';
37
+ export const THREAT_FIELD_TYPE = 'type';
38
+ export const THREAT_FIELD_EXTENSION = 'extension';
39
+ export const THREAT_FIELD_PLUGIN = 'plugin';
40
+ export const THREAT_FIELD_THEME = 'theme';
41
+ export const THREAT_FIELD_SEVERITY = 'severity';
42
+ export const THREAT_FIELD_SIGNATURE = 'signature';
43
+ export const THREAT_FIELD_FIRST_DETECTED = 'first-detected';
44
+ export const THREAT_FIELD_FIXED_ON = 'fixed-on';
45
+ export const THREAT_FIELD_AUTO_FIX = 'auto-fix';
46
+
47
+ export const THREAT_ACTION_FIX = 'fix';
48
+ export const THREAT_ACTION_IGNORE = 'ignore';
49
+ export const THREAT_ACTION_UNIGNORE = 'unignore';