Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean up copy/pasting of tabular content in EuiDataGrid and EuiBasic/InMemoryTable #8019

Merged
merged 14 commits into from
Sep 23, 2024

Conversation

cee-chen
Copy link
Member

@cee-chen cee-chen commented Sep 12, 2024

Summary

closes elastic/kibana#177952

This PR adds a tabular content copy service which listens for copy events, detects hidden characters that both EuiDataGrid and EuiBasicTable now embed, and strips those characters and any undesired newlines/tabs and replaces them with only newlines/tabs that correspond with rows and cells.

Warning

This will not work as expected for virtualized data grids that scroll (especially if the last cell is not visible and scrolled off the page). There is not a lot EUI can do about this as the cells are literally not rendered in the DOM - at most we could provide an onCopy API for consumers to hook into and provide their own cleaned data, but that would have to be a future feature request/enhancement.

QA

General checklist

  • Browser QA
    - [ ] Checked in both light and dark modes
    - [ ] Checked in mobile
    • Checked in Chrome, Safari, Edge, and Firefox
    • Checked for accessibility including keyboard-only and screenreader modes
  • Docs site QA - N/A
  • Code quality checklist
  • Release checklist
    • A changelog entry exists and is marked appropriately.
      - [ ] If applicable, added the breaking change issue label (and filled out the breaking change checklist)
  • Designer checklist - N/A

@cee-chen cee-chen changed the title Clean up copy/pasting or tabular content in EuiDataGrid and EuiBasic/InMemoryTable Clean up copy/pasting of tabular content in EuiDataGrid and EuiBasic/InMemoryTable Sep 12, 2024
and move existing `copy_to_clipboard` there
- requires waterfalling `visibleColCount` number down to header cell wrapper for isLastColumn logic
- and as such not appending the correct copy markers
- requires adding an `append` prop to various cell components, for rendering this hidden text outside of the cell content wrapper
- my god this was a StackOverflow journey
- we should be more specific with selectors anyway
works just fine in actual browsers, but typescript really wants document for whatever reason

see https://stackoverflow.com/questions/74809554/cant-get-paste-event-to-work-in-typescript :|
@cee-chen cee-chen marked this pull request as ready for review September 17, 2024 06:01
@cee-chen cee-chen requested a review from a team as a code owner September 17, 2024 06:01
Comment on lines 15 to 22
// Special visually hidden unicode characters that we use to manually clean content
// and force our own newlines/horizontal tabs
export const CHARS = {
NEWLINE: '↵',
TAB: '↦',
TABULAR_CONTENT_BOUND: '⁣', // U+2063 - invisible separator
NO_COPY_BOUND: '⁢', // U+2062 - invisible times
};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd really appreciate people's thoughts on the (semi-random) characters I've chosen here and if we think they're unlikely enough to be used in consumer content to feel safe using them here!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if the invisible times might be used in a grid somehow as its usage seems to be in maths context.

The invisible times codepoint is used in mathematical type-setting to indicate the multiplication of two terms without a visible multiplication operator, e.g. when type-setting 2x (the multiplication of the number 2 and the variable x), the invisible times codepoint can be inserted in-between: 2 <U+2062> x.

And the invisible seperator ("invisible comma") is apparently commonly used. Question would be if that's a case for a datagrid though.

Do we need to use invisible characters here?
If not, I found this "Angle with Down Zig-zag Arrow" character of the "Misc technical" block which seems to have no meaning and hence hopefully is not commonly used? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good with changing it to whatever! 👍 No strong feelings either way. I was also thinking maybe these scissors characters?

✁ UPPER BLADE SCISSORS
✃ LOWER BLADE SCISSORS

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'm fine with kind of whatever too, in the end the important thing is it's not commonly used.
We could try the scissors and worst case we update if issues are reported.
... I just had another thought: How about combining 2 random characters? That'll reduce the likelyhood of them being used for sure?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use two random characters for NO_COPY_BOUND fairly easily, but I'm a little more hesitant for TABULAR_CONTENT_BOUND, as that complicates the .split() logic I'm using & would possibly require a regex at that point. Do you have any suggestions for characters? 👀

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mgadewoll I misread your comment - I thought you meant using two different characters for the start & end bounds! Using two characters does seem super smart - I've made that tweak here (9a22861) while trying to keep the symbols still fairly relevant to the intent. Let me know if it looks good to you!

Comment on lines 15 to 22
// Special visually hidden unicode characters that we use to manually clean content
// and force our own newlines/horizontal tabs
export const CHARS = {
NEWLINE: '↵',
TAB: '↦',
TABULAR_CONTENT_BOUND: '⁣', // U+2063 - invisible separator
NO_COPY_BOUND: '⁢', // U+2062 - invisible times
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if the invisible times might be used in a grid somehow as its usage seems to be in maths context.

The invisible times codepoint is used in mathematical type-setting to indicate the multiplication of two terms without a visible multiplication operator, e.g. when type-setting 2x (the multiplication of the number 2 and the variable x), the invisible times codepoint can be inserted in-between: 2 <U+2062> x.

And the invisible seperator ("invisible comma") is apparently commonly used. Question would be if that's a case for a datagrid though.

Do we need to use invisible characters here?
If not, I found this "Angle with Down Zig-zag Arrow" character of the "Misc technical" block which seems to have no meaning and hence hopefully is not commonly used? 🤔

@@ -80,6 +83,7 @@ const EuiDataGridHeaderRow = memo(
<EuiDataGridControlHeaderCell
key={controlColumn.id}
index={index + leadingControlColumns.length + columns.length}
visibleColCount={visibleColCount}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the leading/trailing columns have a visually hidden (SR only) column title.
This one will be copied though it's not visible. Is that expected or should visually hidden content not be copied? 🤔

Copy link
Member Author

@cee-chen cee-chen Sep 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screen reader text will generally be copied unless it's been marked with special characters to remove it from copied text (which is what I did for our enter key instructions). In this case I think it's entirely appropriate to copy.

@jughosta did request a configurable feature for control columns in Slack:

J: Would it be possible to exclude control columns and icons from the output too? Maybe we could allow some customization options.
C: In theory with this approach consumers could render their own 'exclude' bounds with the character I've created for that 🤔 but are you saying you'd prefer a prop like columns={[{ id: 'someColumn', excludeWhenCopying: true }]}?

I'm not totally sure I have time for that right now though, so I wanted to ship this PR as-is (since it's a large improvement) and consider adding that in a future follow-up PR. My 2c is that users deleting a single column from a spreadsheet is very few clicks in comparison to completely cleaning newlines from every cell.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree. It's not much effort to delete columns that are unwanted and it's already a great improvement and optionally removing columns could be a follow-up if required.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not say that it would be simple for users to remove columns from the output as the output might become misaligned.

For example, I skip the controls and start my selection from text "Green..."
Screenshot 2024-09-18 at 10 44 02

and after copying I get the following:
Screenshot 2024-09-18 at 10 43 51

Agree that the PR already brings a huge improvement although many users would still run into the similar problems if we don't address it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, agreed, this was also the first thing I was running into, so this might be a common problem, would it be possible to exclude control columns by default?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker. Thanks for improving the selection!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, not a blocker, thx for taking care of this so quickly 🙏 Btw it's interesting how Security is using it, for me this is a edge case

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed this is not a blocker and it's already a massive improvement! 🤘 The issue described here is true for native HTML tables too, so I wouldn't be too concerned. There's a risk trying to be too smart around control columns could result in unexpected behaviour for consumers anyway (or maybe not, hard to guess). The suggestion for consumers to selectively exclude what they don't want copied using the same characters seems safe and reasonable IMO, especially if that's already possible for control columns.

@cee-chen It looks like the utils from tabular_copy.tsx are exported publicly, is that right? If so it should already be easy from our side to exclude the control columns in Discover if we want to.

Copy link
Member Author

@cee-chen cee-chen Sep 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll be able to exclude screen reader only control column text, but the control columns themselves will always generate a hidden horizontal tab character - that is currently outside the scope of what's overrideable right now in this PR.

For the former use case, usage would look something like this:

import { EuiDataGrid, tabularCopyMarkers } from '@elastic/eui';

<EuiDataGrid
  leadingControlColumns={[{
    id: 'selection',
    width: 32, 
    headerCellRender: () => (
      <EuiScreenReaderOnly>
        <span>
          {tabularCopyMarkers.hiddenNoCopyBoundary}
          Example text that does not show on copy
          {tabularCopyMarkers.hiddenNoCopyBoundary}
        </span>
      </EuiScreenReaderOnly>
    ),
  }]}
/>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, thanks for the clarification. In that case additional control over the control columns would be useful to consider as a follow up, but the changes in this PR are still a big improvement regardless 👍

Cypress.automation('remote:debugger:protocol', {
command: 'Browser.grantPermissions',
params: {
permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤯

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went down so many google/stackoverflow rabbit holes to figure out how to get this working haha 🫠 honestly until the very end I wasn't really sure it was going to be, but very glad to have a regression test for this!

@cee-chen
Copy link
Member Author

@kertal Before we ship this, do you see any issues or concerns with this approach in production Kibana/Discover?

@kertal
Copy link
Member

kertal commented Sep 17, 2024

@kertal Before we ship this, do you see any issues or concerns with this approach in production Kibana/Discover?

@cee-chen just a quick first take on it, @jughosta, could you check tomorrow, since you implemented and closed #192395 for this approach. many thx 🙏. we could also create a shared a-la-carte with this PR (if current state of EUI/Kibana is allowing to do so)

also FYI @davismcphee because of his focus on Discover rendering performance, and optimizations around the grid in Discover

@kertal
Copy link
Member

kertal commented Sep 18, 2024

Shared Kibana/EUI PR instance to test btw: https://kertal-pr-192756-eui-v95-11-0.kbndev.co/app/r/s/KzSxC

…nsumer

+ use 2 characters for increased chances

+ add more `data` attributes to help identify usages
- in case consumers want to use it outside SR text - thanks Davis for the prompt!
Copy link
Contributor

@mgadewoll mgadewoll left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚢 🐈‍⬛ The additional changes look good to me! There seems to just be a small linting error still blocking the CI.
Sorry for not directly following up on the re-review 🙈

@@ -17,10 +17,11 @@ import React, { PropsWithChildren, useEffect } from 'react';
export const CHARS = {
NEWLINE: '↵',
TAB: '↦',
TABULAR_CONTENT_BOUND: '⁣', // U+2063 - invisible separator
NO_COPY_BOUND: '⁢', // U+2062 - invisible times
// Use multiple characters to reduce the chances of consumers also using these characters
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should definitely reduce the likelihood of it being a problem 👍

@cee-chen cee-chen enabled auto-merge (squash) September 23, 2024 17:21
@elasticmachine
Copy link
Collaborator

💚 Build Succeeded

History

@cee-chen cee-chen merged commit 31c8385 into elastic:main Sep 23, 2024
5 checks passed
@cee-chen cee-chen deleted the tabular-copying branch September 23, 2024 17:49
jbudz pushed a commit to elastic/kibana that referenced this pull request Sep 30, 2024
`v95.11.0`⏩`v95.12.0-backport.0`

> [!note]
> A few fun highlights from this release:
> - Content within `EuiDataGrid`, `EuiBasicTable`, and
`EuiInMemoryTable`, when manually selected/highlighted by users, should
now more cleanly copy and paste into various spreadsheet apps
> - `EuiProvider` will now detect the user's system-level dark or light
mode and default to that if no `colorMode` prop is passed manually

_[Questions? Please see our Kibana upgrade
FAQ.](https://github.com/elastic/eui/blob/main/wiki/eui-team-processes/upgrading-kibana.md#faq-for-kibana-teams)_

---

## [`v95.12.0`](https://github.com/elastic/eui/releases/v95.12.0)

- Enhanced `EuiDataGrid` and `EuiBasic/InMemoryTable` to clean content
newlines/tabs when users copy and paste from their tabular data
([#8019](elastic/eui#8019))
- Updated `EuiResizableButton` with a new `accountForScrollbars` prop
([#8021](elastic/eui#8021))
- Updated `EuiProvider` to inherit from the user's OS/system light/dark
mode setting if a `colorMode` prop has not been passed
([#8026](elastic/eui#8026))

**Bug fixes**

- Fixed `EuiDatePicker`'s `onClear` button to not appear when the input
is `disabled` ([#8020](elastic/eui#8020))
- Fixed several `EuiDataGrid` row height bugs:
([#8025](elastic/eui#8025))
- Fixed row heights not recalculating when
`rowHeightOptions.lineHeight`, `gridStyles.fontSize`, or
`gridStyles.cellPadding` changed
- Fixed incorrect height calculations for `rowHeightOptions.rowHeights`
with `lineCount`s
- Fixed control column content to align better with multi-line row
heights, as well as custom line-heights
##
[`v95.12.0-backport.0`](https://github.com/elastic/eui/releases/v95.12.0-backport.0)

**This is a backport release only intended for use by Kibana.**

**Bug fixes**

- Fixed `EuiProvider`'s system color mode detection causing errors
during server-side rendering
([#8040](elastic/eui#8040))
- Fixed an `EuiDataGrid` rendering bug that was causing bouncing
scrollbar issues ([#8041](elastic/eui#8041))
kibanamachine pushed a commit to kibanamachine/kibana that referenced this pull request Sep 30, 2024
`v95.11.0`⏩`v95.12.0-backport.0`

> [!note]
> A few fun highlights from this release:
> - Content within `EuiDataGrid`, `EuiBasicTable`, and
`EuiInMemoryTable`, when manually selected/highlighted by users, should
now more cleanly copy and paste into various spreadsheet apps
> - `EuiProvider` will now detect the user's system-level dark or light
mode and default to that if no `colorMode` prop is passed manually

_[Questions? Please see our Kibana upgrade
FAQ.](https://github.com/elastic/eui/blob/main/wiki/eui-team-processes/upgrading-kibana.md#faq-for-kibana-teams)_

---

## [`v95.12.0`](https://github.com/elastic/eui/releases/v95.12.0)

- Enhanced `EuiDataGrid` and `EuiBasic/InMemoryTable` to clean content
newlines/tabs when users copy and paste from their tabular data
([elastic#8019](elastic/eui#8019))
- Updated `EuiResizableButton` with a new `accountForScrollbars` prop
([elastic#8021](elastic/eui#8021))
- Updated `EuiProvider` to inherit from the user's OS/system light/dark
mode setting if a `colorMode` prop has not been passed
([elastic#8026](elastic/eui#8026))

**Bug fixes**

- Fixed `EuiDatePicker`'s `onClear` button to not appear when the input
is `disabled` ([elastic#8020](elastic/eui#8020))
- Fixed several `EuiDataGrid` row height bugs:
([elastic#8025](elastic/eui#8025))
- Fixed row heights not recalculating when
`rowHeightOptions.lineHeight`, `gridStyles.fontSize`, or
`gridStyles.cellPadding` changed
- Fixed incorrect height calculations for `rowHeightOptions.rowHeights`
with `lineCount`s
- Fixed control column content to align better with multi-line row
heights, as well as custom line-heights
##
[`v95.12.0-backport.0`](https://github.com/elastic/eui/releases/v95.12.0-backport.0)

**This is a backport release only intended for use by Kibana.**

**Bug fixes**

- Fixed `EuiProvider`'s system color mode detection causing errors
during server-side rendering
([elastic#8040](elastic/eui#8040))
- Fixed an `EuiDataGrid` rendering bug that was causing bouncing
scrollbar issues ([elastic#8041](elastic/eui#8041))

(cherry picked from commit 8a89c85)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Remove new lines when copy/pasting in Discover
6 participants