From 0f4af88bf13a17345b484fb0881acf199f91fa48 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Thu, 10 Oct 2024 17:14:10 +0200 Subject: SONAR-22298 Fix clipboard a11y --- .../design-system/src/components/DropdownMenu.tsx | 28 +- .../__snapshots__/CodeSnippet-test.tsx.snap | 565 ++++++++++++--------- .../src/components/__tests__/clipboard-test.tsx | 8 +- .../design-system/src/components/clipboard.tsx | 207 +++----- 4 files changed, 405 insertions(+), 403 deletions(-) (limited to 'server/sonar-web/design-system') 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 { @@ -223,23 +223,17 @@ interface ItemCopyProps { export function ItemCopy(props: ItemCopyProps) { const { children, className, copyValue, tooltipOverlay } = props; + + const [copySuccess, handleCopy] = useCopyClipboardEffect(copyValue); + return ( - - {({ setCopyButton, copySuccess }) => ( - -
  • - - {children} - -
  • -
    - )} -
    + +
  • + + {children} + +
  • +
    ); } 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" >
             <prop>foobar<prop>
    @@ -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"
       >
         
         
           
             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"
       >
         
         
           
             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 {
    -  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 = ,
    -  className,
    -  children,
    -  copyValue,
    -  copiedLabel = 'Copied',
    -  copyLabel = 'Copy',
    -}: ButtonProps) {
    +export function ClipboardButton(props: ButtonProps) {
    +  const {
    +    icon = ,
    +    className,
    +    children,
    +    copyValue,
    +    copiedLabel = 'Copied',
    +    copyLabel = 'Copy',
    +  } = props;
    +  const [copySuccess, handleCopy] = useCopyClipboardEffect(copyValue);
    +
       return (
    -    
    -      {({ setCopyButton, copySuccess }) => (
    -        
    -          
    -            {children ?? copyLabel}
    -          
    -        
    -      )}
    -    
    +    
    +      {/* TODO ^ Remove TooltipProvider after design-system is reintegrated into sonar-web */}
    +      
    +        
    +      
    +    
       );
     }
     
     interface IconButtonProps {
    -  Icon?: React.ComponentType>;
    +  Icon?: ComponentProps['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 (
    -    
    -      {({ setCopyButton, copySuccess }) => {
    -        return (
    -          
    -                {copySuccess ? copiedLabel : copyLabel}
    -              
    -            }
    -            mouseEnterDelay={INTERACTIVE_TOOLTIP_DELAY}
    -            {...(copySuccess ? { visible: copySuccess } : undefined)}
    -          >
    -            
    -          
    -        );
    -      }}
    -    
    +    
    +      {/* TODO ^ Remove TooltipProvider after design-system is reintegrated into sonar-web */}
    +      
    +    
       );
     }
    +
    +export function useCopyClipboardEffect(copyValue: string) {
    +  const [copySuccess, setCopySuccess] = useState(false);
    +
    +  const handleCopy = useCallback(
    +    ({ currentTarget }: React.MouseEvent) => {
    +      const isSuccess = copy(copyValue) === copyValue;
    +      setCopySuccess(isSuccess);
    +
    +      if (isSuccess) {
    +        setTimeout(() => {
    +          setCopySuccess(false);
    +        }, COPY_SUCCESS_NOTIFICATION_LIFESPAN);
    +      }
    +
    +      currentTarget.focus();
    +    },
    +    [copyValue],
    +  );
    +
    +  return [copySuccess, handleCopy] as const;
    +}
    -- 
    cgit v1.2.3