aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/design-system
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2024-10-10 17:14:10 +0200
committersonartech <sonartech@sonarsource.com>2024-10-11 20:02:44 +0000
commit0f4af88bf13a17345b484fb0881acf199f91fa48 (patch)
treec561c457d7d4f5d6b7e1d3dfcdf35af8ebd41151 /server/sonar-web/design-system
parentc289b869b8914d3833cf19fd129de11d53782d93 (diff)
downloadsonarqube-0f4af88bf13a17345b484fb0881acf199f91fa48.tar.gz
sonarqube-0f4af88bf13a17345b484fb0881acf199f91fa48.zip
SONAR-22298 Fix clipboard a11y
Diffstat (limited to 'server/sonar-web/design-system')
-rw-r--r--server/sonar-web/design-system/src/components/DropdownMenu.tsx28
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap565
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/clipboard-test.tsx8
-rw-r--r--server/sonar-web/design-system/src/components/clipboard.tsx207
4 files changed, 405 insertions, 403 deletions
diff --git a/server/sonar-web/design-system/src/components/DropdownMenu.tsx b/server/sonar-web/design-system/src/components/DropdownMenu.tsx
index e3be15c7a1a..1251298bfcb 100644
--- a/server/sonar-web/design-system/src/components/DropdownMenu.tsx
+++ b/server/sonar-web/design-system/src/components/DropdownMenu.tsx
@@ -29,7 +29,7 @@ import { InputSizeKeys, ThemedProps } from '../types/theme';
import { BaseLink, LinkProps } from './Link';
import NavLink from './NavLink';
import { Tooltip } from './Tooltip';
-import { ClipboardBase } from './clipboard';
+import { useCopyClipboardEffect } from './clipboard';
import { Checkbox } from './input/Checkbox';
interface Props extends React.HtmlHTMLAttributes<HTMLMenuElement> {
@@ -223,23 +223,17 @@ interface ItemCopyProps {
export function ItemCopy(props: ItemCopyProps) {
const { children, className, copyValue, tooltipOverlay } = props;
+
+ const [copySuccess, handleCopy] = useCopyClipboardEffect(copyValue);
+
return (
- <ClipboardBase>
- {({ setCopyButton, copySuccess }) => (
- <Tooltip content={tooltipOverlay} visible={copySuccess}>
- <li role="none">
- <ItemButtonStyled
- className={className}
- data-clipboard-text={copyValue}
- ref={setCopyButton}
- role="menuitem"
- >
- {children}
- </ItemButtonStyled>
- </li>
- </Tooltip>
- )}
- </ClipboardBase>
+ <Tooltip content={tooltipOverlay} visible={copySuccess}>
+ <li role="none">
+ <ItemButtonStyled className={className} onClick={handleCopy} role="menuitem">
+ {children}
+ </ItemButtonStyled>
+ </li>
+ </Tooltip>
);
}
diff --git a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap
index 1d7d505e179..ac2ea24e199 100644
--- a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap
+++ b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap
@@ -16,16 +16,8 @@ exports[`should highlight code content correctly 1`] = `
border-radius: 0.25rem;
}
-.emotion-4 {
+.emotion-3 {
box-sizing: border-box;
- -webkit-text-decoration: none;
- text-decoration: none;
- outline: none;
- border: var(--border);
- color: var(--color);
- background-color: var(--background);
- -webkit-transition: background-color 0.2s ease;
- transition: background-color 0.2s ease;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
@@ -34,19 +26,28 @@ exports[`should highlight code content correctly 1`] = `
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
- height: 2.25rem;
- font: var(--echoes-typography-text-default-semi-bold);
- padding-left: 1rem;
- padding-right: 1rem;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- border-radius: 0.5rem;
+ padding: var(--echoes-dimension-space-0) var(--button-padding);
+ height: var(--button-height);
+ min-height: var(--button-height);
+ overflow: hidden;
+ font: var(--echoes-typography-others-label);
+ color: var(--button-color);
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ background-color: var(--button-background);
+ border: var(--button-border);
+ border-radius: var(--echoes-border-radius-400);
+ outline: none;
cursor: pointer;
- --background: rgb(255,255,255);
- --backgroundHover: rgb(239,242,249);
- --color: rgb(62,67,87);
- --focus: rgba(197,205,223,0.2);
- --border: 1px solid rgb(197,205,223);
+ --button-color: var(--echoes-color-text-default);
+ --button-border: var(--echoes-color-border-bold) solid var(--echoes-border-width-default);
+ --button-background: var(--echoes-color-background-default);
+ --button-background-hover: var(--echoes-color-background-default-hover);
+ --button-background-active: var(--echoes-color-background-default-active);
+ --button-background-focus: var(--echoes-color-background-default);
+ --button-background-disabled: var(--echoes-color-background-disabled);
+ --button-padding: var(--echoes-dimension-space-150);
+ --button-height: var(--echoes-sizes-buttons-large);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@@ -57,90 +58,115 @@ exports[`should highlight code content correctly 1`] = `
position: absolute;
}
-.emotion-4:hover,
-.emotion-4:active {
- color: var(--color);
- background-color: var(--backgroundHover);
+.emotion-3:focus,
+.emotion-3:focus-visible {
+ background-color: var(--button-background-focus);
+ outline: var(--echoes-color-focus-default) solid var(--echoes-focus-border-width-default);
+ outline-offset: var(--echoes-focus-border-offset-default);
}
-.emotion-4:focus,
-.emotion-4:active,
-.emotion-4:focus-visible {
- color: var(--color);
+.emotion-3:hover {
+ background-color: var(--button-background-hover);
}
-.emotion-4:focus-visible {
- outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
- outline-offset: var(--echoes-focus-border-offset-default);
+.emotion-3:active {
+ background-color: var(--button-background-active);
}
-.emotion-4:disabled,
-.emotion-4:disabled:hover {
+.emotion-3:disabled,
+.emotion-3:disabled:has(:hover, :active, :focus, :focus-visible) {
color: var(--echoes-color-text-disabled);
- background-color: rgb(239,242,249);
- border: 1px solid rgb(197,205,223);
+ background-color: var(--button-background-disabled);
+ border: none;
cursor: not-allowed;
+ pointer-events: none;
}
-.emotion-4>svg {
- margin-right: 0.25rem;
+.code-snippet-highlighted-oneline .emotion-3 {
+ bottom: 0.5rem;
}
-.emotion-4 [disabled] {
- pointer-events: none;
+.emotion-5 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ gap: var(--echoes-dimension-space-75);
+ overflow: hidden;
}
-.code-snippet-highlighted-oneline .emotion-4 {
- bottom: 0.5rem;
+.emotion-7 {
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ display: inline-block;
+ font-size: calc(1em + 4px);
+ font-style: normal;
+ font-weight: normal;
+ height: calc(2em - 16px);
+ line-height: calc(2em - 16px);
+ text-align: center;
+ vertical-align: bottom;
+ width: calc(2em - 16px);
+ font-family: 'Material Symbols Rounded';
+}
+
+.emotion-9 {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.emotion-6 code {
+.emotion-11 code {
font: var(--echoes-typography-code-default);
background: rgb(252,252,253);
color: rgb(51,53,60);
}
-.emotion-6 code.hljs {
+.emotion-11 code.hljs {
padding: unset;
}
-.emotion-6 .hljs-meta,
-.emotion-6 .hljs-variable {
+.emotion-11 .hljs-meta,
+.emotion-11 .hljs-variable {
color: rgb(51,53,60);
}
-.emotion-6 .hljs-doctag,
-.emotion-6 .hljs-title,
-.emotion-6 .hljs-title.class_,
-.emotion-6 .hljs-title.function_ {
+.emotion-11 .hljs-doctag,
+.emotion-11 .hljs-title,
+.emotion-11 .hljs-title.class_,
+.emotion-11 .hljs-title.function_ {
color: rgb(34,84,192);
}
-.emotion-6 .hljs-comment {
+.emotion-11 .hljs-comment {
font: var(--echoes-typography-code-comment);
color: rgb(109,111,119);
}
-.emotion-6 .hljs-keyword,
-.emotion-6 .hljs-tag,
-.emotion-6 .hljs-type {
+.emotion-11 .hljs-keyword,
+.emotion-11 .hljs-tag,
+.emotion-11 .hljs-type {
color: rgb(152,29,150);
}
-.emotion-6 .hljs-literal,
-.emotion-6 .hljs-number {
+.emotion-11 .hljs-literal,
+.emotion-11 .hljs-number {
color: rgb(126,83,5);
}
-.emotion-6 .hljs-string {
+.emotion-11 .hljs-string {
color: rgb(32,105,31);
}
-.emotion-6 .hljs-meta .hljs-keyword {
+.emotion-11 .hljs-meta .hljs-keyword {
color: rgb(47,103,48);
}
-.emotion-6 .sonar-underline {
+.emotion-11 .sonar-underline {
-webkit-text-decoration: underline rgb(253,162,155);
text-decoration: underline rgb(253,162,155);
-webkit-text-decoration: underline rgb(253,162,155) wavy;
@@ -149,17 +175,17 @@ exports[`should highlight code content correctly 1`] = `
text-decoration-skip-ink: none;
}
-.emotion-6.code-wrap {
+.emotion-11.code-wrap {
white-space: pre-wrap;
word-break: break-all;
}
-.emotion-6.code-wrap.wrap-words {
+.emotion-11.code-wrap.wrap-words {
word-break: normal;
overflow-wrap: break-word;
}
-.emotion-6 mark {
+.emotion-11 mark {
font-weight: 400;
padding: 0.25rem;
border-radius: 0.25rem;
@@ -172,32 +198,28 @@ exports[`should highlight code content correctly 1`] = `
class="sw-code fs-mask emotion-0 emotion-1"
>
<button
- aria-describedby="tooltip-3"
- class="sw-select-none emotion-2 emotion-3 emotion-4 emotion-5"
- data-clipboard-text="<prop>foobar<prop>"
+ class="sw-select-none emotion-2 emotion-3 emotion-4"
+ data-state="closed"
type="button"
>
- <svg
- aria-hidden="true"
- class="octicon octicon-copy"
- fill="currentColor"
- focusable="false"
- height="16"
- style="display: inline-block; user-select: none; vertical-align: middle; overflow: visible;"
- viewBox="0 0 16 16"
- width="16"
+ <span
+ class="emotion-5 emotion-6"
>
- <path
- d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"
- />
- <path
- d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"
- />
- </svg>
- Copy
+ <span
+ aria-hidden="true"
+ class="emotion-7 emotion-8"
+ >
+ 
+ </span>
+ <span
+ class="emotion-9 emotion-10"
+ >
+ Copy
+ </span>
+ </span>
</button>
<span
- class="hljs sw-overflow-auto sw-pr-24 sw-flex emotion-6 emotion-7"
+ class="hljs sw-overflow-auto sw-pr-24 sw-flex emotion-11 emotion-12"
>
<pre>
&lt;prop&gt;foobar&lt;prop&gt;
@@ -223,16 +245,8 @@ exports[`should show full size when multiline with no editing 1`] = `
border-radius: 0.25rem;
}
-.emotion-4 {
+.emotion-3 {
box-sizing: border-box;
- -webkit-text-decoration: none;
- text-decoration: none;
- outline: none;
- border: var(--border);
- color: var(--color);
- background-color: var(--background);
- -webkit-transition: background-color 0.2s ease;
- transition: background-color 0.2s ease;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
@@ -241,19 +255,28 @@ exports[`should show full size when multiline with no editing 1`] = `
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
- height: 2.25rem;
- font: var(--echoes-typography-text-default-semi-bold);
- padding-left: 1rem;
- padding-right: 1rem;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- border-radius: 0.5rem;
+ padding: var(--echoes-dimension-space-0) var(--button-padding);
+ height: var(--button-height);
+ min-height: var(--button-height);
+ overflow: hidden;
+ font: var(--echoes-typography-others-label);
+ color: var(--button-color);
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ background-color: var(--button-background);
+ border: var(--button-border);
+ border-radius: var(--echoes-border-radius-400);
+ outline: none;
cursor: pointer;
- --background: rgb(255,255,255);
- --backgroundHover: rgb(239,242,249);
- --color: rgb(62,67,87);
- --focus: rgba(197,205,223,0.2);
- --border: 1px solid rgb(197,205,223);
+ --button-color: var(--echoes-color-text-default);
+ --button-border: var(--echoes-color-border-bold) solid var(--echoes-border-width-default);
+ --button-background: var(--echoes-color-background-default);
+ --button-background-hover: var(--echoes-color-background-default-hover);
+ --button-background-active: var(--echoes-color-background-default-active);
+ --button-background-focus: var(--echoes-color-background-default);
+ --button-background-disabled: var(--echoes-color-background-disabled);
+ --button-padding: var(--echoes-dimension-space-150);
+ --button-height: var(--echoes-sizes-buttons-large);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@@ -264,90 +287,115 @@ exports[`should show full size when multiline with no editing 1`] = `
position: absolute;
}
-.emotion-4:hover,
-.emotion-4:active {
- color: var(--color);
- background-color: var(--backgroundHover);
+.emotion-3:focus,
+.emotion-3:focus-visible {
+ background-color: var(--button-background-focus);
+ outline: var(--echoes-color-focus-default) solid var(--echoes-focus-border-width-default);
+ outline-offset: var(--echoes-focus-border-offset-default);
}
-.emotion-4:focus,
-.emotion-4:active,
-.emotion-4:focus-visible {
- color: var(--color);
+.emotion-3:hover {
+ background-color: var(--button-background-hover);
}
-.emotion-4:focus-visible {
- outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
- outline-offset: var(--echoes-focus-border-offset-default);
+.emotion-3:active {
+ background-color: var(--button-background-active);
}
-.emotion-4:disabled,
-.emotion-4:disabled:hover {
+.emotion-3:disabled,
+.emotion-3:disabled:has(:hover, :active, :focus, :focus-visible) {
color: var(--echoes-color-text-disabled);
- background-color: rgb(239,242,249);
- border: 1px solid rgb(197,205,223);
+ background-color: var(--button-background-disabled);
+ border: none;
cursor: not-allowed;
+ pointer-events: none;
}
-.emotion-4>svg {
- margin-right: 0.25rem;
+.code-snippet-highlighted-oneline .emotion-3 {
+ bottom: 0.5rem;
}
-.emotion-4 [disabled] {
- pointer-events: none;
+.emotion-5 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ gap: var(--echoes-dimension-space-75);
+ overflow: hidden;
}
-.code-snippet-highlighted-oneline .emotion-4 {
- bottom: 0.5rem;
+.emotion-7 {
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ display: inline-block;
+ font-size: calc(1em + 4px);
+ font-style: normal;
+ font-weight: normal;
+ height: calc(2em - 16px);
+ line-height: calc(2em - 16px);
+ text-align: center;
+ vertical-align: bottom;
+ width: calc(2em - 16px);
+ font-family: 'Material Symbols Rounded';
}
-.emotion-6 code {
+.emotion-9 {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.emotion-11 code {
font: var(--echoes-typography-code-default);
background: rgb(252,252,253);
color: rgb(51,53,60);
}
-.emotion-6 code.hljs {
+.emotion-11 code.hljs {
padding: unset;
}
-.emotion-6 .hljs-meta,
-.emotion-6 .hljs-variable {
+.emotion-11 .hljs-meta,
+.emotion-11 .hljs-variable {
color: rgb(51,53,60);
}
-.emotion-6 .hljs-doctag,
-.emotion-6 .hljs-title,
-.emotion-6 .hljs-title.class_,
-.emotion-6 .hljs-title.function_ {
+.emotion-11 .hljs-doctag,
+.emotion-11 .hljs-title,
+.emotion-11 .hljs-title.class_,
+.emotion-11 .hljs-title.function_ {
color: rgb(34,84,192);
}
-.emotion-6 .hljs-comment {
+.emotion-11 .hljs-comment {
font: var(--echoes-typography-code-comment);
color: rgb(109,111,119);
}
-.emotion-6 .hljs-keyword,
-.emotion-6 .hljs-tag,
-.emotion-6 .hljs-type {
+.emotion-11 .hljs-keyword,
+.emotion-11 .hljs-tag,
+.emotion-11 .hljs-type {
color: rgb(152,29,150);
}
-.emotion-6 .hljs-literal,
-.emotion-6 .hljs-number {
+.emotion-11 .hljs-literal,
+.emotion-11 .hljs-number {
color: rgb(126,83,5);
}
-.emotion-6 .hljs-string {
+.emotion-11 .hljs-string {
color: rgb(32,105,31);
}
-.emotion-6 .hljs-meta .hljs-keyword {
+.emotion-11 .hljs-meta .hljs-keyword {
color: rgb(47,103,48);
}
-.emotion-6 .sonar-underline {
+.emotion-11 .sonar-underline {
-webkit-text-decoration: underline rgb(253,162,155);
text-decoration: underline rgb(253,162,155);
-webkit-text-decoration: underline rgb(253,162,155) wavy;
@@ -356,17 +404,17 @@ exports[`should show full size when multiline with no editing 1`] = `
text-decoration-skip-ink: none;
}
-.emotion-6.code-wrap {
+.emotion-11.code-wrap {
white-space: pre-wrap;
word-break: break-all;
}
-.emotion-6.code-wrap.wrap-words {
+.emotion-11.code-wrap.wrap-words {
word-break: normal;
overflow-wrap: break-word;
}
-.emotion-6 mark {
+.emotion-11 mark {
font-weight: 400;
padding: 0.25rem;
border-radius: 0.25rem;
@@ -379,33 +427,28 @@ exports[`should show full size when multiline with no editing 1`] = `
class="sw-code fs-mask emotion-0 emotion-1"
>
<button
- aria-describedby="tooltip-1"
- class="sw-select-none emotion-2 emotion-3 emotion-4 emotion-5"
- data-clipboard-text="foo
-bar"
+ class="sw-select-none emotion-2 emotion-3 emotion-4"
+ data-state="closed"
type="button"
>
- <svg
- aria-hidden="true"
- class="octicon octicon-copy"
- fill="currentColor"
- focusable="false"
- height="16"
- style="display: inline-block; user-select: none; vertical-align: middle; overflow: visible;"
- viewBox="0 0 16 16"
- width="16"
+ <span
+ class="emotion-5 emotion-6"
>
- <path
- d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"
- />
- <path
- d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"
- />
- </svg>
- Copy
+ <span
+ aria-hidden="true"
+ class="emotion-7 emotion-8"
+ >
+ 
+ </span>
+ <span
+ class="emotion-9 emotion-10"
+ >
+ Copy
+ </span>
+ </span>
</button>
<span
- class="hljs sw-overflow-auto sw-pr-24 sw-flex emotion-6 emotion-7"
+ class="hljs sw-overflow-auto sw-pr-24 sw-flex emotion-11 emotion-12"
>
<pre>
foo
@@ -432,16 +475,8 @@ exports[`should show reduced size when single line with no editing 1`] = `
border-radius: 0.25rem;
}
-.emotion-4 {
+.emotion-3 {
box-sizing: border-box;
- -webkit-text-decoration: none;
- text-decoration: none;
- outline: none;
- border: var(--border);
- color: var(--color);
- background-color: var(--background);
- -webkit-transition: background-color 0.2s ease;
- transition: background-color 0.2s ease;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
@@ -450,19 +485,28 @@ exports[`should show reduced size when single line with no editing 1`] = `
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
- height: 2.25rem;
- font: var(--echoes-typography-text-default-semi-bold);
- padding-left: 1rem;
- padding-right: 1rem;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- border-radius: 0.5rem;
+ padding: var(--echoes-dimension-space-0) var(--button-padding);
+ height: var(--button-height);
+ min-height: var(--button-height);
+ overflow: hidden;
+ font: var(--echoes-typography-others-label);
+ color: var(--button-color);
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ background-color: var(--button-background);
+ border: var(--button-border);
+ border-radius: var(--echoes-border-radius-400);
+ outline: none;
cursor: pointer;
- --background: rgb(255,255,255);
- --backgroundHover: rgb(239,242,249);
- --color: rgb(62,67,87);
- --focus: rgba(197,205,223,0.2);
- --border: 1px solid rgb(197,205,223);
+ --button-color: var(--echoes-color-text-default);
+ --button-border: var(--echoes-color-border-bold) solid var(--echoes-border-width-default);
+ --button-background: var(--echoes-color-background-default);
+ --button-background-hover: var(--echoes-color-background-default-hover);
+ --button-background-active: var(--echoes-color-background-default-active);
+ --button-background-focus: var(--echoes-color-background-default);
+ --button-background-disabled: var(--echoes-color-background-disabled);
+ --button-padding: var(--echoes-dimension-space-150);
+ --button-height: var(--echoes-sizes-buttons-large);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@@ -475,90 +519,115 @@ exports[`should show reduced size when single line with no editing 1`] = `
top: 1rem;
}
-.emotion-4:hover,
-.emotion-4:active {
- color: var(--color);
- background-color: var(--backgroundHover);
+.emotion-3:focus,
+.emotion-3:focus-visible {
+ background-color: var(--button-background-focus);
+ outline: var(--echoes-color-focus-default) solid var(--echoes-focus-border-width-default);
+ outline-offset: var(--echoes-focus-border-offset-default);
}
-.emotion-4:focus,
-.emotion-4:active,
-.emotion-4:focus-visible {
- color: var(--color);
+.emotion-3:hover {
+ background-color: var(--button-background-hover);
}
-.emotion-4:focus-visible {
- outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
- outline-offset: var(--echoes-focus-border-offset-default);
+.emotion-3:active {
+ background-color: var(--button-background-active);
}
-.emotion-4:disabled,
-.emotion-4:disabled:hover {
+.emotion-3:disabled,
+.emotion-3:disabled:has(:hover, :active, :focus, :focus-visible) {
color: var(--echoes-color-text-disabled);
- background-color: rgb(239,242,249);
- border: 1px solid rgb(197,205,223);
+ background-color: var(--button-background-disabled);
+ border: none;
cursor: not-allowed;
+ pointer-events: none;
}
-.emotion-4>svg {
- margin-right: 0.25rem;
+.code-snippet-highlighted-oneline .emotion-3 {
+ bottom: 0.5rem;
}
-.emotion-4 [disabled] {
- pointer-events: none;
+.emotion-5 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ gap: var(--echoes-dimension-space-75);
+ overflow: hidden;
}
-.code-snippet-highlighted-oneline .emotion-4 {
- bottom: 0.5rem;
+.emotion-7 {
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ display: inline-block;
+ font-size: calc(1em + 4px);
+ font-style: normal;
+ font-weight: normal;
+ height: calc(2em - 16px);
+ line-height: calc(2em - 16px);
+ text-align: center;
+ vertical-align: bottom;
+ width: calc(2em - 16px);
+ font-family: 'Material Symbols Rounded';
+}
+
+.emotion-9 {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.emotion-6 code {
+.emotion-11 code {
font: var(--echoes-typography-code-default);
background: rgb(252,252,253);
color: rgb(51,53,60);
}
-.emotion-6 code.hljs {
+.emotion-11 code.hljs {
padding: unset;
}
-.emotion-6 .hljs-meta,
-.emotion-6 .hljs-variable {
+.emotion-11 .hljs-meta,
+.emotion-11 .hljs-variable {
color: rgb(51,53,60);
}
-.emotion-6 .hljs-doctag,
-.emotion-6 .hljs-title,
-.emotion-6 .hljs-title.class_,
-.emotion-6 .hljs-title.function_ {
+.emotion-11 .hljs-doctag,
+.emotion-11 .hljs-title,
+.emotion-11 .hljs-title.class_,
+.emotion-11 .hljs-title.function_ {
color: rgb(34,84,192);
}
-.emotion-6 .hljs-comment {
+.emotion-11 .hljs-comment {
font: var(--echoes-typography-code-comment);
color: rgb(109,111,119);
}
-.emotion-6 .hljs-keyword,
-.emotion-6 .hljs-tag,
-.emotion-6 .hljs-type {
+.emotion-11 .hljs-keyword,
+.emotion-11 .hljs-tag,
+.emotion-11 .hljs-type {
color: rgb(152,29,150);
}
-.emotion-6 .hljs-literal,
-.emotion-6 .hljs-number {
+.emotion-11 .hljs-literal,
+.emotion-11 .hljs-number {
color: rgb(126,83,5);
}
-.emotion-6 .hljs-string {
+.emotion-11 .hljs-string {
color: rgb(32,105,31);
}
-.emotion-6 .hljs-meta .hljs-keyword {
+.emotion-11 .hljs-meta .hljs-keyword {
color: rgb(47,103,48);
}
-.emotion-6 .sonar-underline {
+.emotion-11 .sonar-underline {
-webkit-text-decoration: underline rgb(253,162,155);
text-decoration: underline rgb(253,162,155);
-webkit-text-decoration: underline rgb(253,162,155) wavy;
@@ -567,17 +636,17 @@ exports[`should show reduced size when single line with no editing 1`] = `
text-decoration-skip-ink: none;
}
-.emotion-6.code-wrap {
+.emotion-11.code-wrap {
white-space: pre-wrap;
word-break: break-all;
}
-.emotion-6.code-wrap.wrap-words {
+.emotion-11.code-wrap.wrap-words {
word-break: normal;
overflow-wrap: break-word;
}
-.emotion-6 mark {
+.emotion-11 mark {
font-weight: 400;
padding: 0.25rem;
border-radius: 0.25rem;
@@ -590,32 +659,28 @@ exports[`should show reduced size when single line with no editing 1`] = `
class="sw-code sw-py-6 code-snippet-highlighted-oneline fs-mask emotion-0 emotion-1"
>
<button
- aria-describedby="tooltip-2"
- class="sw-select-none emotion-2 emotion-3 emotion-4 emotion-5"
- data-clipboard-text="foobar"
+ class="sw-select-none emotion-2 emotion-3 emotion-4"
+ data-state="closed"
type="button"
>
- <svg
- aria-hidden="true"
- class="octicon octicon-copy"
- fill="currentColor"
- focusable="false"
- height="16"
- style="display: inline-block; user-select: none; vertical-align: middle; overflow: visible;"
- viewBox="0 0 16 16"
- width="16"
+ <span
+ class="emotion-5 emotion-6"
>
- <path
- d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"
- />
- <path
- d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"
- />
- </svg>
- Copy
+ <span
+ aria-hidden="true"
+ class="emotion-7 emotion-8"
+ >
+ 
+ </span>
+ <span
+ class="emotion-9 emotion-10"
+ >
+ Copy
+ </span>
+ </span>
</button>
<span
- class="hljs sw-overflow-auto sw-pr-24 sw-flex emotion-6 emotion-7"
+ class="hljs sw-overflow-auto sw-pr-24 sw-flex emotion-11 emotion-12"
>
<code>
foobar
diff --git a/server/sonar-web/design-system/src/components/__tests__/clipboard-test.tsx b/server/sonar-web/design-system/src/components/__tests__/clipboard-test.tsx
index 027b990fff6..e073810552e 100644
--- a/server/sonar-web/design-system/src/components/__tests__/clipboard-test.tsx
+++ b/server/sonar-web/design-system/src/components/__tests__/clipboard-test.tsx
@@ -43,9 +43,9 @@ describe('ClipboardButton', () => {
await user.click(screen.getByRole('button', { name: 'Copy' }));
- expect(await screen.findByText('Copied')).toBeVisible();
+ expect(await screen.findByRole('tooltip', { name: 'Copied' })).toBeInTheDocument();
- await waitForElementToBeRemoved(() => screen.queryByText('Copied'));
+ await waitForElementToBeRemoved(() => screen.queryByRole('tooltip', { name: 'Copied' }));
jest.runAllTimers();
});
@@ -74,9 +74,9 @@ describe('ClipboardIconButton', () => {
await user.click(screen.getByRole('button', { name: 'Copy to clipboard' }));
- expect(await screen.findByText('Copied')).toBeVisible();
+ expect(await screen.findByRole('tooltip', { name: 'Copied' })).toBeInTheDocument();
- await waitForElementToBeRemoved(() => screen.queryByText('Copied'));
+ await waitForElementToBeRemoved(() => screen.queryByRole('tooltip', { name: 'Copied' }));
jest.runAllTimers();
});
});
diff --git a/server/sonar-web/design-system/src/components/clipboard.tsx b/server/sonar-web/design-system/src/components/clipboard.tsx
index 1745e74f2fe..530880b86c6 100644
--- a/server/sonar-web/design-system/src/components/clipboard.tsx
+++ b/server/sonar-web/design-system/src/components/clipboard.tsx
@@ -17,91 +17,21 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import {
+ Button,
+ ButtonIcon,
+ ButtonSize,
+ ButtonVariety,
+ IconCopy,
+ Tooltip,
+ TooltipProvider,
+} from '@sonarsource/echoes-react';
import classNames from 'classnames';
-import Clipboard from 'clipboard';
-import React from 'react';
-import { INTERACTIVE_TOOLTIP_DELAY } from '../helpers/constants';
-import { ButtonSecondary } from '../sonar-aligned/components/buttons';
-import { DiscreetInteractiveIcon, InteractiveIcon, InteractiveIconSize } from './InteractiveIcon';
-import { Tooltip } from './Tooltip';
-import { CopyIcon } from './icons/CopyIcon';
-import { IconProps } from './icons/Icon';
+import { copy } from 'clipboard';
+import React, { ComponentProps, useCallback, useState } from 'react';
const COPY_SUCCESS_NOTIFICATION_LIFESPAN = 1000;
-export interface State {
- copySuccess: boolean;
-}
-
-interface RenderProps {
- copySuccess: boolean;
- setCopyButton: (node: HTMLElement | null) => void;
-}
-
-interface BaseProps {
- children: (props: RenderProps) => React.ReactNode;
-}
-
-export class ClipboardBase extends React.PureComponent<BaseProps, State> {
- private clipboard?: Clipboard;
- private copyButton?: HTMLElement | null;
- mounted = false;
- state: State = { copySuccess: false };
-
- componentDidMount() {
- this.mounted = true;
- if (this.copyButton) {
- this.clipboard = new Clipboard(this.copyButton, {
- container: this.copyButton.parentElement ?? undefined,
- });
- this.clipboard.on('success', this.handleSuccessCopy);
- }
- }
-
- componentDidUpdate(props: BaseProps) {
- if (this.props.children !== props.children) {
- if (this.clipboard) {
- this.clipboard.destroy();
- }
- if (this.copyButton) {
- this.clipboard = new Clipboard(this.copyButton, {
- container: this.copyButton.parentElement ?? undefined,
- });
- this.clipboard.on('success', this.handleSuccessCopy);
- }
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- if (this.clipboard) {
- this.clipboard.destroy();
- }
- }
-
- setCopyButton = (node: HTMLElement | null) => {
- this.copyButton = node;
- };
-
- handleSuccessCopy = () => {
- if (this.mounted) {
- this.setState({ copySuccess: true });
- setTimeout(() => {
- if (this.mounted) {
- this.setState({ copySuccess: false });
- }
- }, COPY_SUCCESS_NOTIFICATION_LIFESPAN);
- }
- };
-
- render() {
- return this.props.children({
- setCopyButton: this.setCopyButton,
- copySuccess: this.state.copySuccess,
- });
- }
-}
-
interface ButtonProps {
children?: React.ReactNode;
className?: string;
@@ -111,41 +41,42 @@ interface ButtonProps {
icon?: React.ReactNode;
}
-export function ClipboardButton({
- icon = <CopyIcon />,
- className,
- children,
- copyValue,
- copiedLabel = 'Copied',
- copyLabel = 'Copy',
-}: ButtonProps) {
+export function ClipboardButton(props: ButtonProps) {
+ const {
+ icon = <IconCopy />,
+ className,
+ children,
+ copyValue,
+ copiedLabel = 'Copied',
+ copyLabel = 'Copy',
+ } = props;
+ const [copySuccess, handleCopy] = useCopyClipboardEffect(copyValue);
+
return (
- <ClipboardBase>
- {({ setCopyButton, copySuccess }) => (
- <Tooltip content={copiedLabel} visible={copySuccess}>
- <ButtonSecondary
- className={classNames('sw-select-none', className)}
- data-clipboard-text={copyValue}
- icon={icon}
- ref={setCopyButton}
- >
- {children ?? copyLabel}
- </ButtonSecondary>
- </Tooltip>
- )}
- </ClipboardBase>
+ <TooltipProvider>
+ {/* TODO ^ Remove TooltipProvider after design-system is reintegrated into sonar-web */}
+ <Tooltip content={copiedLabel} isOpen={copySuccess}>
+ <Button
+ className={classNames('sw-select-none', className)}
+ onClick={handleCopy}
+ prefix={icon}
+ >
+ {children ?? copyLabel}
+ </Button>
+ </Tooltip>
+ </TooltipProvider>
);
}
interface IconButtonProps {
- Icon?: React.ComponentType<React.PropsWithChildren<IconProps>>;
+ Icon?: ComponentProps<typeof ButtonIcon>['Icon'];
'aria-label'?: string;
className?: string;
copiedLabel?: string;
copyLabel?: string;
copyValue: string;
discreet?: boolean;
- size?: InteractiveIconSize;
+ size?: ButtonSize;
}
export function ClipboardIconButton(props: IconButtonProps) {
@@ -153,37 +84,49 @@ export function ClipboardIconButton(props: IconButtonProps) {
className,
copyValue,
discreet,
- size = 'small',
- Icon = CopyIcon,
+ size = ButtonSize.Medium,
+ Icon = IconCopy,
copiedLabel = 'Copied',
copyLabel = 'Copy to clipboard',
} = props;
- const InteractiveIconComponent = discreet ? DiscreetInteractiveIcon : InteractiveIcon;
+
+ const [copySuccess, handleCopy] = useCopyClipboardEffect(copyValue);
return (
- <ClipboardBase>
- {({ setCopyButton, copySuccess }) => {
- return (
- <Tooltip
- content={
- <div className="sw-w-abs-150 sw-text-center">
- {copySuccess ? copiedLabel : copyLabel}
- </div>
- }
- mouseEnterDelay={INTERACTIVE_TOOLTIP_DELAY}
- {...(copySuccess ? { visible: copySuccess } : undefined)}
- >
- <InteractiveIconComponent
- Icon={Icon}
- aria-label={props['aria-label'] ?? copyLabel}
- className={className}
- data-clipboard-text={copyValue}
- ref={setCopyButton}
- size={size}
- />
- </Tooltip>
- );
- }}
- </ClipboardBase>
+ <TooltipProvider>
+ {/* TODO ^ Remove TooltipProvider after design-system is reintegrated into sonar-web */}
+ <ButtonIcon
+ Icon={Icon}
+ ariaLabel={props['aria-label'] ?? copyLabel}
+ className={className}
+ onClick={handleCopy}
+ size={size}
+ tooltipContent={copySuccess ? copiedLabel : copyLabel}
+ tooltipOptions={copySuccess ? { isOpen: copySuccess } : undefined}
+ variety={discreet ? ButtonVariety.DefaultGhost : ButtonVariety.Default}
+ />
+ </TooltipProvider>
);
}
+
+export function useCopyClipboardEffect(copyValue: string) {
+ const [copySuccess, setCopySuccess] = useState(false);
+
+ const handleCopy = useCallback(
+ ({ currentTarget }: React.MouseEvent<HTMLButtonElement>) => {
+ const isSuccess = copy(copyValue) === copyValue;
+ setCopySuccess(isSuccess);
+
+ if (isSuccess) {
+ setTimeout(() => {
+ setCopySuccess(false);
+ }, COPY_SUCCESS_NOTIFICATION_LIFESPAN);
+ }
+
+ currentTarget.focus();
+ },
+ [copyValue],
+ );
+
+ return [copySuccess, handleCopy] as const;
+}