Make text in the ValueInterpreter selectable

Now, a user can select text in the MemoryViewer's
ValueInterpreter by either double-clicking or
manually highlighting the text-range.

before: https://imgur.com/a/HGpUkL9
after: https://imgur.com/a/nR0j6iX

Fixed: 1324962
Change-Id: I8db585c9aa5e04f69aaae657eb384e5fdae529e9
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3695369
Commit-Queue: Michal Pitr <michalpitr@google.com>
Reviewed-by: Kim-Anh Tran <kimanh@chromium.org>
diff --git a/front_end/ui/components/linear_memory_inspector/ValueInterpreterDisplay.ts b/front_end/ui/components/linear_memory_inspector/ValueInterpreterDisplay.ts
index 068285e..58b0667 100644
--- a/front_end/ui/components/linear_memory_inspector/ValueInterpreterDisplay.ts
+++ b/front_end/ui/components/linear_memory_inspector/ValueInterpreterDisplay.ts
@@ -153,10 +153,10 @@
     // Disabled until https://crbug.com/1079231 is fixed.
     // clang-format off
     return html`
-      <span class="value-type-cell-no-mode value-type-cell">${i18n.i18n.lockedString(type)}</span>
+      <span class="value-type-cell-no-mode value-type-cell selectable-text">${i18n.i18n.lockedString(type)}</span>
       <div class="value-type-cell">
         <div class="value-type-value-with-link" data-value="true">
-        <span>${unsignedValue}</span>
+        <span class="selectable-text">${unsignedValue}</span>
           ${
             html`
               <button class="jump-to-button" data-jump="true" title=${buttonTitle} ?disabled=${jumpDisabled}
@@ -179,7 +179,7 @@
     // Disabled until https://crbug.com/1079231 is fixed.
     // clang-format off
     return html`
-      <span class="value-type-cell">${i18n.i18n.lockedString(type)}</span>
+      <span class="value-type-cell selectable-text">${i18n.i18n.lockedString(type)}</span>
       <div>
         <select title=${i18nString(UIStrings.changeValueTypeMode)}
           data-mode-settings="true"
@@ -206,7 +206,7 @@
     const showSignedAndUnsigned =
         signedValue !== unsignedValue && mode !== ValueTypeMode.Hexadecimal && mode !== ValueTypeMode.Octal;
 
-    const unsignedRendered = html`<span class="value-type-cell"  title=${
+    const unsignedRendered = html`<span class="value-type-cell selectable-text"  title=${
         i18nString(UIStrings.unsignedValue)} data-value="true">${unsignedValue}</span>`;
     if (!showSignedAndUnsigned) {
       return unsignedRendered;
@@ -214,8 +214,8 @@
 
     // Some values are too long to show in one line, we're putting them into the next line.
     const showInMultipleLines = type === ValueType.Int32 || type === ValueType.Int64;
-    const signedRendered =
-        html`<span data-value="true" title=${i18nString(UIStrings.signedValue)}>${signedValue}</span>`;
+    const signedRendered = html`<span class="selectable-text" data-value="true" title=${
+        i18nString(UIStrings.signedValue)}>${signedValue}</span>`;
 
     if (showInMultipleLines) {
       return html`
diff --git a/front_end/ui/components/linear_memory_inspector/valueInterpreterDisplay.css b/front_end/ui/components/linear_memory_inspector/valueInterpreterDisplay.css
index a11ba1e..29b2db6 100644
--- a/front_end/ui/components/linear_memory_inspector/valueInterpreterDisplay.css
+++ b/front_end/ui/components/linear_memory_inspector/valueInterpreterDisplay.css
@@ -61,3 +61,11 @@
   background-color: var(--color-details-hairline);
   margin: 0 4px;
 }
+
+.selectable-text {
+  user-select: text;
+}
+
+.selectable-text::selection {
+  background-color: var(--legacy-item-selection-bg-color);
+}
diff --git a/test/unittests/front_end/ui/components/linear_memory_inspector/ValueInterpreterDisplay_test.ts b/test/unittests/front_end/ui/components/linear_memory_inspector/ValueInterpreterDisplay_test.ts
index 2f59ee0..c61dfa8 100644
--- a/test/unittests/front_end/ui/components/linear_memory_inspector/ValueInterpreterDisplay_test.ts
+++ b/test/unittests/front_end/ui/components/linear_memory_inspector/ValueInterpreterDisplay_test.ts
@@ -382,4 +382,64 @@
     assert.isTrue(buttons[0].disabled);
     assert.isTrue(buttons[1].disabled);
   });
+
+  it('selects text in data-value elements if user selects it', () => {
+    // To test the failing case, set .value-type user-select to `none`.
+    // This is necessary as we render the component in isolation, so it doesn't
+    // inherit this property from its parent.
+
+    const component = new LinearMemoryInspector.ValueInterpreterDisplay.ValueInterpreterDisplay();
+    const array = [1, 132, 172, 71];
+    component.data = {
+      buffer: new Uint8Array(array).buffer,
+      endianness: LinearMemoryInspector.ValueInterpreterDisplayUtils.Endianness.Little,
+      valueTypes: new Set([
+        LinearMemoryInspector.ValueInterpreterDisplayUtils.ValueType.Int8,
+        LinearMemoryInspector.ValueInterpreterDisplayUtils.ValueType.Int16,
+        LinearMemoryInspector.ValueInterpreterDisplayUtils.ValueType.Float32,
+        LinearMemoryInspector.ValueInterpreterDisplayUtils.ValueType.Pointer32,
+      ]),
+      memoryLength: array.length,
+    };
+    renderElementIntoDOM(component);
+
+    const dataValues = getElementsWithinComponent(component, '.selectable-text', HTMLSpanElement);
+    assert.lengthOf(dataValues, 9);
+
+    const expectedValues = [
+      'Integer 8-bit',
+      '1',
+      'Integer 16-bit',
+      '33793',
+      '-31743',
+      'Float 32-bit',
+      '88328.01',
+      'Pointer 32-bit',
+      '0x47AC8401',
+    ];
+
+    // Workaround for selecting text (instead of double-clicking it).
+    // We can use a range to specify an element. Range can be converted into
+    // a selection. We then check if the selected text meets our expectations.
+
+    // Continuous part of a document, independent of any visual representation.
+    const range = document.createRange();
+    // Represents user's highlighted text.
+    const selection = document.getSelection();
+
+    for (let i = 0; i < dataValues.length; ++i) {
+      if (selection === null) {
+        assert.fail('Selection is null');
+      }
+      // Set range around the element.
+      range.selectNodeContents(dataValues[i]);
+      // Remove ranges associated with selection.
+      selection?.removeAllRanges();
+      // Select element using range.
+      selection?.addRange(range);
+
+      const text = window.getSelection()?.toString();
+      assert.strictEqual(text, expectedValues[i]);
+    }
+  });
 });