Repository: marigold-ui/marigold (ts)

Issue:
make <Table> content selectable for copy and paste
## Description

in the `<Table>` you currently can't copy  the content. It would be better if the table can do this. 

## Context

you often need to copy some content from a table (money amounts etc)

## Consequences

problem: in react-aria tables are also not for copy content. 


Pull request:
feat: Make text in non-interactive tables selectable
When a table has `selectionMode = "none"`(default) we can safely let users select text.

closes #2365

Patch:
diff --git a/.changeset/plenty-goats-rescue.md b/.changeset/plenty-goats-rescue.md
new file mode 100644
index 000000000..7fb15334f
--- /dev/null
+++ b/.changeset/plenty-goats-rescue.md
@@ -0,0 +1,8 @@
+---
+"@marigold/components": patch
+"@marigold/theme-b2b": patch
+"@marigold/theme-core": patch
+"@marigold/theme-unicorn": patch
+---
+
+feat: Make text in non-interactive tables selectable
diff --git a/packages/components/src/Table/Context.tsx b/packages/components/src/Table/Context.tsx
index ae30f539d..9d2a60296 100644
--- a/packages/components/src/Table/Context.tsx
+++ b/packages/components/src/Table/Context.tsx
@@ -4,6 +4,7 @@ import { ComponentStyleParts } from '@marigold/system';
 
 export interface TableContextProps {
   state: TableState<object>;
+  interactive: boolean;
   styles: ComponentStyleParts<['table', 'header', 'row', 'cell']>;
 }
 
diff --git a/packages/components/src/Table/Table.stories.tsx b/packages/components/src/Table/Table.stories.tsx
index dc3b221a5..23817bb40 100644
--- a/packages/components/src/Table/Table.stories.tsx
+++ b/packages/components/src/Table/Table.stories.tsx
@@ -382,3 +382,40 @@ export const Expanded = () => (
     </Table.Body>
   </Table>
 );
+
+export const Static = () => (
+  <Table aria-label="Table without interaction" selectionMode="none">
+    <Table.Header>
+      <Table.Column>Name</Table.Column>
+      <Table.Column>Firstname</Table.Column>
+      <Table.Column>House</Table.Column>
+      <Table.Column>Year of birth</Table.Column>
+    </Table.Header>
+    <Table.Body>
+      <Table.Row key={1}>
+        <Table.Cell>Potter</Table.Cell>
+        <Table.Cell>Harry</Table.Cell>
+        <Table.Cell>Gryffindor</Table.Cell>
+        <Table.Cell>1980</Table.Cell>
+      </Table.Row>
+      <Table.Row key={2}>
+        <Table.Cell>Malfoy</Table.Cell>
+        <Table.Cell>Draco</Table.Cell>
+        <Table.Cell>Slytherin</Table.Cell>
+        <Table.Cell>1980</Table.Cell>
+      </Table.Row>
+      <Table.Row key={3}>
+        <Table.Cell>Diggory</Table.Cell>
+        <Table.Cell>Cedric</Table.Cell>
+        <Table.Cell>Hufflepuff</Table.Cell>
+        <Table.Cell>1977</Table.Cell>
+      </Table.Row>
+      <Table.Row key={4}>
+        <Table.Cell>Lovegood</Table.Cell>
+        <Table.Cell>Luna</Table.Cell>
+        <Table.Cell>Ravenclaw</Table.Cell>
+        <Table.Cell>1981</Table.Cell>
+      </Table.Row>
+    </Table.Body>
+  </Table>
+);
diff --git a/packages/components/src/Table/Table.tsx b/packages/components/src/Table/Table.tsx
index 7cf5becbe..83fb59481 100755
--- a/packages/components/src/Table/Table.tsx
+++ b/packages/components/src/Table/Table.tsx
@@ -53,13 +53,16 @@ export const Table: Table = ({
   variant,
   size,
   stretch,
+  selectionMode = 'none',
   ...props
 }: TableProps) => {
+  const interactive = selectionMode !== 'none';
   const tableRef = useRef(null);
   const state = useTableState({
     ...props,
+    selectionMode,
     showSelectionCheckboxes:
-      props.selectionMode === 'multiple' &&
+      selectionMode === 'multiple' &&
       // TODO: It this necessary?
       props.selectionBehavior !== 'replace',
   });
@@ -74,7 +77,7 @@ export const Table: Table = ({
   const { collection } = state;
 
   return (
-    <TableContext.Provider value={{ state, styles }}>
+    <TableContext.Provider value={{ state, interactive, styles }}>
       <Box
         __baseCSS={{
           overflow: ['scroll', 'unset'],
diff --git a/packages/components/src/Table/TableCell.tsx b/packages/components/src/Table/TableCell.tsx
index 6ec8b038c..f05475c74 100644
--- a/packages/components/src/Table/TableCell.tsx
+++ b/packages/components/src/Table/TableCell.tsx
@@ -14,7 +14,7 @@ export interface TableCellProps {
 
 export const TableCell = ({ cell }: TableCellProps) => {
   const ref = useRef(null);
-  const { state, styles } = useTableContext();
+  const { interactive, state, styles } = useTableContext();
   const disabled = state.disabledKeys.has(cell.parentKey!);
   const { gridCellProps } = useTableCell(
     {
@@ -24,6 +24,18 @@ export const TableCell = ({ cell }: TableCellProps) => {
     ref
   );
 
+  const cellProps = interactive
+    ? gridCellProps
+    : {
+        /**
+         * Override `react-aria` handler so users can select text.
+         * Solution from https://github.com/adobe/react-spectrum/issues/2585
+         */
+        ...gridCellProps,
+        onMouseDown: (e: MouseEvent) => e.stopPropagation(),
+        onPointerDown: (e: MouseEvent) => e.stopPropagation(),
+      };
+
   const { focusProps, isFocusVisible } = useFocusRing();
   const stateProps = useStateProps({ disabled, focusVisible: isFocusVisible });
 
@@ -32,7 +44,7 @@ export const TableCell = ({ cell }: TableCellProps) => {
       as="td"
       ref={ref}
       css={styles.cell}
-      {...mergeProps(gridCellProps, focusProps)}
+      {...mergeProps(cellProps, focusProps)}
       {...stateProps}
     >
       {cell.rendered}
diff --git a/packages/components/src/Table/TableColumnHeader.tsx b/packages/components/src/Table/TableColumnHeader.tsx
index d287edd49..22e2c5f0f 100644
--- a/packages/components/src/Table/TableColumnHeader.tsx
+++ b/packages/components/src/Table/TableColumnHeader.tsx
@@ -66,6 +66,7 @@ export const TableColumnHeader = ({ column }: TableColumnHeaderProps) => {
       as="th"
       colSpan={column.colspan}
       ref={ref}
+      __baseCSS={{ cursor: 'default' }}
       css={styles.header}
       {...mergeProps(columnHeaderProps, hoverProps, focusProps)}
       {...stateProps}
diff --git a/packages/components/src/Table/TableRow.tsx b/packages/components/src/Table/TableRow.tsx
index c081e61b0..3cf36f033 100644
--- a/packages/components/src/Table/TableRow.tsx
+++ b/packages/components/src/Table/TableRow.tsx
@@ -20,7 +20,7 @@ export interface TableRowProps {
 // ---------------
 export const TableRow = ({ children, row }: TableRowProps) => {
   const ref = useRef(null);
-  const { state, styles } = useTableContext();
+  const { interactive, state, styles } = useTableContext();
   const { rowProps, isPressed } = useTableRow(
     {
       node: row,
@@ -34,7 +34,9 @@ export const TableRow = ({ children, row }: TableRowProps) => {
 
   // Rows are focused if any cell inside it is focused
   const { focusProps, isFocusVisible } = useFocusRing({ within: true });
-  const { hoverProps, isHovered } = useHover({ isDisabled: disabled });
+  const { hoverProps, isHovered } = useHover({
+    isDisabled: disabled || !interactive,
+  });
 
   const stateProps = useStateProps({
     disabled,
@@ -48,6 +50,9 @@ export const TableRow = ({ children, row }: TableRowProps) => {
     <Box
       as="tr"
       ref={ref}
+      __baseCSS={{
+        cursor: !interactive ? 'text' : disabled ? 'default' : 'pointer',
+      }}
       css={styles.row}
       {...mergeProps(rowProps, focusProps, hoverProps)}
       {...stateProps}
diff --git a/themes/theme-b2b/src/components/Table.style.ts b/themes/theme-b2b/src/components/Table.style.ts
index 135b44392..06ee39e61 100644
--- a/themes/theme-b2b/src/components/Table.style.ts
+++ b/themes/theme-b2b/src/components/Table.style.ts
@@ -20,7 +20,7 @@ export const Table: Theme['components']['Table'] = {
       '&:selected': {
         bg: 'orange10',
       },
-      '&:hover': {
+      '&[data-hover]': {
         bg: 'gray30',
       },
       '&:focus-visible': {
@@ -30,7 +30,6 @@ export const Table: Theme['components']['Table'] = {
     cell: {
       py: 'small',
       px: 'small',
-      cursor: 'default',
       color: 'gray70',
       borderBottom: '1px solid',
       borderColor: 'gray50',
diff --git a/themes/theme-core/src/components/Table.style.ts b/themes/theme-core/src/components/Table.style.ts
index 690e6a749..7458a6475 100644
--- a/themes/theme-core/src/components/Table.style.ts
+++ b/themes/theme-core/src/components/Table.style.ts
@@ -17,7 +17,7 @@ export const Table: Theme['components']['Table'] = {
       '&:checked': {
         bg: 'orange10',
       },
-      '&:hover': {
+      '&[data-hover]': {
         bg: 'gray20',
       },
       '&:focus-visible': {
@@ -26,7 +26,6 @@ export const Table: Theme['components']['Table'] = {
     },
     cell: {
       p: 'xsmall',
-      cursor: 'default',
       borderBottom: '1px solid',
       borderColor: 'gray50',
       '&:focus': {
diff --git a/themes/theme-unicorn/src/components/Table.style.ts b/themes/theme-unicorn/src/components/Table.style.ts
index fbc844938..d1114409b 100644
--- a/themes/theme-unicorn/src/components/Table.style.ts
+++ b/themes/theme-unicorn/src/components/Table.style.ts
@@ -17,7 +17,7 @@ export const Table: Theme['components']['Table'] = {
       '&:checked': {
         bg: 'purple10',
       },
-      '&:hover': {
+      '&[data-hover]': {
         bg: 'gray30',
       },
       '&:focus-visible': {
@@ -26,7 +26,6 @@ export const Table: Theme['components']['Table'] = {
     },
     cell: {
       p: 'xsmall',
-      cursor: 'default',
       borderBottom: '1px solid',
       borderColor: 'gray50',
       '&:focus': {


Test patch:
diff --git a/packages/components/src/Table/Table.test.tsx b/packages/components/src/Table/Table.test.tsx
index 3ae608136..f5af80422 100644
--- a/packages/components/src/Table/Table.test.tsx
+++ b/packages/components/src/Table/Table.test.tsx
@@ -292,3 +292,55 @@ test('allows to strecht to fit container', () => {
   const table = screen.getAllByRole(/grid/);
   expect(table[0]).toHaveStyle(`width: 100%`);
 });
+
+test('supports non-interactive table', async () => {
+  render(
+    <Table
+      aria-label="Non-interactive table"
+      selectionMode="none"
+      disabledKeys={['Jane']}
+    >
+      <Table.Header>
+        <Table.Column>Name</Table.Column>
+      </Table.Header>
+      <Table.Body>
+        <Table.Row key="Alice">
+          <Table.Cell>Alice</Table.Cell>
+        </Table.Row>
+        <Table.Row key="Jane">
+          <Table.Cell>Jane</Table.Cell>
+        </Table.Row>
+      </Table.Body>
+    </Table>
+  );
+
+  const rows = screen.getAllByRole('row');
+  expect(rows[1]).toHaveStyle('cursor: text');
+  expect(rows[2]).toHaveStyle('cursor: text'); // Disabled, but still selectable text
+});
+
+test('cursor indicates interactivity', async () => {
+  render(
+    <Table
+      aria-label="Interactive table"
+      selectionMode="single"
+      disabledKeys={['Jane']}
+    >
+      <Table.Header>
+        <Table.Column>Name</Table.Column>
+      </Table.Header>
+      <Table.Body>
+        <Table.Row key="Alice">
+          <Table.Cell>Alice</Table.Cell>
+        </Table.Row>
+        <Table.Row key="Jane">
+          <Table.Cell>Jane</Table.Cell>
+        </Table.Row>
+      </Table.Body>
+    </Table>
+  );
+
+  const rows = screen.getAllByRole('row');
+  expect(rows[1]).toHaveStyle('cursor: pointer');
+  expect(rows[2]).toHaveStyle('cursor: default');
+});