diff --git a/semcore/data-table/__tests__/data-table-accordion.browser-test.tsx b/semcore/data-table/__tests__/data-table-accordion.browser-test.tsx index 0a607817b3..4bffe9ae12 100644 --- a/semcore/data-table/__tests__/data-table-accordion.browser-test.tsx +++ b/semcore/data-table/__tests__/data-table-accordion.browser-test.tsx @@ -253,7 +253,6 @@ test.describe(`${TAG.VISUAL}`, () => { await loadPage(page, 'stories/components/data-table/advanced/examples/accordion_with_checkbox.tsx', 'en', item); const cells = locators.row(page, 2).locator('[data-ui-name="Row.Cell"]'); - await page.locator('[data-ui-name="Checkbox"]').nth(1).click(); const accordion = page.locator('[role="gridcell"][aria-level="2"]'); @@ -265,6 +264,10 @@ test.describe(`${TAG.VISUAL}`, () => { 'background-color': stylesActiveHovered[1], }); + await checkStyles(accordion, { + 'background-color': stylesNotActive[2], + }); + await expect(page).toHaveScreenshot(); if (item.variant == 'card' || item.sideIndents == 'wide') { diff --git a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx b/semcore/data-table/__tests__/data-table-cells.browser-test.tsx index b7a274f6cb..826d5be993 100644 --- a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx +++ b/semcore/data-table/__tests__/data-table-cells.browser-test.tsx @@ -2,7 +2,7 @@ import { expect, test } from '@semcore/testing-utils/playwright'; import { loadPage } from '@semcore/testing-utils/shared/helpers'; import { TAG } from '@semcore/testing-utils/shared/tags'; -import { locators, checkStyles } from './utils'; +import { locators } from './utils'; /* ===================================================== @visual @@ -48,131 +48,8 @@ test.describe(`${TAG.VISUAL}`, () => { } await expect(page).toHaveScreenshot(); }); - - test('Verify empty data with selectable rows', { - tag: [TAG.PRIORITY_MEDIUM, - '@data-table'], - }, async ({ page }) => { - await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/checkbox-in-table-with-no-data.tsx', 'en'); - - await expect(page).toHaveScreenshot(); - }); - - test('Verify sideIndents=wide with selectable rows non compact and compact', { - tag: [TAG.PRIORITY_MEDIUM, - '@data-table'], - }, async ({ page }) => { - await loadPage(page, 'stories/components/data-table/docs/examples/checkbox-in-table.tsx', 'en', { - sideIndents: 'wide', - }); - - await test.step('Verify wide for non compact data-table', async () => { - await expect(page).toHaveScreenshot(); - }); - - await test.step('Verify wide for compact data-table', async () => { - await loadPage(page, 'stories/components/data-table/docs/examples/checkbox-in-table.tsx', 'en', { - sideIndents: 'wide', compact: true, - }); - await expect(page).toHaveScreenshot(); - }); - }); - - test('Verify color on hover when merged rows AND columns with multi-level header', { - tag: [TAG.PRIORITY_HIGH, - '@data-table'], - }, async ({ page, browserName }) => { - if (browserName == 'firefox') test.skip(); - - await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/merged-row-for-multi-level-header.tsx', 'en'); - - const consoleErrors: string[] = []; - - page.on('console', (msg) => { - if (msg.type() === 'error') { - consoleErrors.push(msg.text()); - } - }); - - page.on('pageerror', (error) => { - consoleErrors.push(error.message); - }); - - await test.step('Verify Color when child cell hovered', async () => { - await locators.getCell(page, 3, 1).hover(); - - await checkStyles(locators.getCell(page, 3, 1), { 'background-color': 'rgb(240, 240, 244)' }); - await checkStyles(locators.getCell(page, 2, 2), { 'background-color': 'rgb(240, 240, 244)' }); - await checkStyles(locators.getCell(page, 2, 1), { 'background-color': 'rgb(255, 255, 255)' }); - }); - - await test.step('Verify Color when parent cell hovered', async () => { - await locators.getCell(page, 2, 2).hover(); - - for (let row = 2; row <= 5; row++) { - await checkStyles(locators.getCell(page, row, 1), { 'background-color': 'rgb(240, 240, 244)' }); - } - }); - - await test.step('Verify no console errors', async () => { - expect(consoleErrors, `Console errors found:\n${consoleErrors.join('\n')}`).toHaveLength(0); - }); - }); - - test('Verify styles when checkbox in merged cells checked by keyboard', { - tag: [TAG.PRIORITY_HIGH, - TAG.KEYBOARD, - '@data-table', - '@tooltip'], - }, async ({ page, browserName }) => { - await loadPage(page, 'stories/components/data-table/advanced/examples/selectable_with_merged_rows.tsx', 'en'); - - await page.keyboard.press('Tab'); - - await page.keyboard.press('ArrowDown'); - await expect(locators.getCell(page, 2, 1).locator('input')).toBeFocused(); - - await page.keyboard.press('Space'); - - await page.getByRole('button', { name: 'Deselect all' }).waitFor({ state: 'visible' }); - await checkStyles(locators.getCell(page, 2, 1), { 'background-color': 'rgb(233, 247, 255)' }); - await checkStyles(locators.getCell(page, 2, 2), { 'background-color': 'rgb(233, 247, 255)' }); - await checkStyles(locators.getCell(page, 2, 3), { 'background-color': 'rgb(233, 247, 255)' }); - await checkStyles(locators.getCell(page, 2, 4), { 'background-color': 'rgb(233, 247, 255)' }); - await checkStyles(locators.getCell(page, 3, 2), { 'background-color': 'rgb(233, 247, 255)' }); - await checkStyles(locators.getCell(page, 3, 3), { 'background-color': 'rgb(233, 247, 255)' }); - await checkStyles(locators.getCell(page, 3, 4), { 'background-color': 'rgb(233, 247, 255)' }); - await expect(page).toHaveScreenshot(); - - if (browserName == 'firefox') return; - const cell22 = locators.getCell(page, 2, 1); - const box22 = await cell22.boundingBox(); - if (box22) { - await page.mouse.move(box22.x + box22.width / 2, box22.y + box22.height / 2); - } - - await checkStyles(locators.getCell(page, 2, 1), { 'background-color': 'rgb(196, 229, 254)' }); - await checkStyles(locators.getCell(page, 2, 2), { 'background-color': 'rgb(196, 229, 254)' }); - await checkStyles(locators.getCell(page, 2, 3), { 'background-color': 'rgb(196, 229, 254)' }); - await checkStyles(locators.getCell(page, 2, 4), { 'background-color': 'rgb(196, 229, 254)' }); - await checkStyles(locators.getCell(page, 3, 2), { 'background-color': 'rgb(196, 229, 254)' }); - await checkStyles(locators.getCell(page, 3, 3), { 'background-color': 'rgb(196, 229, 254)' }); - await checkStyles(locators.getCell(page, 3, 4), { 'background-color': 'rgb(196, 229, 254)' }); - - const cell23 = locators.getCell(page, 2, 3); - const box23 = await cell23.boundingBox(); - if (box23) { - await page.mouse.move(box23.x + box23.width / 2, box23.y + box23.height / 2); - } - await checkStyles(locators.getCell(page, 2, 1), { 'background-color': 'rgb(196, 229, 254)' }); - await checkStyles(locators.getCell(page, 2, 2), { 'background-color': 'rgb(196, 229, 254)' }); - await checkStyles(locators.getCell(page, 2, 3), { 'background-color': 'rgb(196, 229, 254)' }); - await checkStyles(locators.getCell(page, 2, 4), { 'background-color': 'rgb(196, 229, 254)' }); - await checkStyles(locators.getCell(page, 3, 2), { 'background-color': 'rgb(233, 247, 255)' }); - await checkStyles(locators.getCell(page, 3, 3), { 'background-color': 'rgb(233, 247, 255)' }); - await checkStyles(locators.getCell(page, 3, 4), { 'background-color': 'rgb(233, 247, 255)' }); - }); }); + /* ===================================================== @functional Keyboard and mouse interactions - no snapshots here. @@ -473,119 +350,6 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); }); - test('Verify select rows with Shift', { - tag: [ - TAG.KEYBOARD, - TAG.MOUSE, - - '@data-table', - ], - }, async ({ page }) => { - await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/checkbox.tsx', 'en'); - - const firstCell = locators.getCell(page, 3, 1); - const secondCell = locators.getCell(page, 7, 1); - - await firstCell.locator('label').click(); - await secondCell.locator('label').click({ modifiers: ['Shift'] }); - - for (let i = 3; i <= 7; i++) { - await expect(locators.getCell(page, i, 1).locator('input')).toBeChecked(); - } - - await locators.getCell(page, 5, 1).locator('label').click({ modifiers: ['Shift'] }); - - for (let i = 5; i <= 7; i++) { - await expect(locators.getCell(page, i, 1).locator('input')).not.toBeChecked(); - } - - await locators.getCell(page, 9, 1).locator('label').click({ modifiers: ['Shift'] }); - for (let i = 5; i <= 8; i++) { - await expect(locators.getCell(page, i, 1).locator('input')).not.toBeChecked(); - } - await expect(locators.getCell(page, 9, 1).locator('input')).toBeChecked(); - }); - - test('Verify select rows with Shift when checkbox in merged cells', { - tag: [ - TAG.KEYBOARD, - TAG.MOUSE, - '@data-table', - ], - }, async ({ page }) => { - await loadPage(page, 'stories/components/data-table/advanced/examples/selectable_with_merged_rows.tsx', 'en'); - - const firstCell = locators.getCell(page, 2, 1); - const secondCell = locators.getCell(page, 4, 1); - - await firstCell.locator('label').click(); - await secondCell.locator('label').click({ modifiers: ['Shift'] }); - - await expect(locators.getCell(page, 2, 1).locator('input')).toBeChecked(); - await expect(locators.getCell(page, 4, 1).locator('input')).toBeChecked(); - - await locators.getCell(page, 8, 1).locator('label').click({ modifiers: ['Shift'] }); - - await expect(locators.getCell(page, 2, 1).locator('input')).toBeChecked(); - await expect(locators.getCell(page, 4, 1).locator('input')).toBeChecked(); - await expect(locators.getCell(page, 6, 1).locator('input')).toBeChecked(); - await expect(locators.getCell(page, 8, 1).locator('input')).toBeChecked(); - }); - - test('Verify keyboard interaction when checkbox in merged cells', { - tag: [TAG.PRIORITY_HIGH, - TAG.KEYBOARD, - '@data-table', - '@tooltip'], - }, async ({ page }) => { - await loadPage(page, 'stories/components/data-table/advanced/examples/selectable_with_merged_rows.tsx', 'en'); - - await test.step('Verify Focus on checkbox', async () => { - await page.keyboard.press('Tab'); - await page.keyboard.press('ArrowDown'); - await expect(locators.getCell(page, 2, 1).locator('input')).toBeFocused(); - }); - - await test.step('Verify navigation to the 1st child', async () => { - await page.keyboard.press('Space'); - await page.keyboard.press('ArrowRight'); - await page.keyboard.press('ArrowRight'); - await expect(locators.getCell(page, 2, 3)).toBeFocused(); - }); - await test.step('Verify focus returns to checkbox', async () => { - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowLeft'); - await expect(locators.getCell(page, 2, 1).locator('input')).toBeFocused(); - }); - await test.step('Verify navigation from non merged to merged', async () => { - for (let i = 0; i < 4; i++) - await page.keyboard.press('ArrowDown'); - await expect(locators.getCell(page, 10, 1).locator('input')).toBeFocused(); - await page.keyboard.press('ArrowUp'); - await expect(locators.getCell(page, 8, 1).locator('input')).toBeFocused(); - }); - await test.step('Verify navigation from 2nd child outside the table and back', async () => { - await page.keyboard.press('ArrowRight'); - await page.keyboard.press('ArrowRight'); - await page.keyboard.press('ArrowRight'); - await page.keyboard.press('ArrowDown'); - await expect(locators.getCell(page, 9, 4)).toBeFocused(); - - await page.keyboard.press('Tab'); - await expect(locators.button(page, 'Next')).toBeFocused(); - await page.keyboard.press('Shift+Tab'); - await expect(locators.getCell(page, 9, 4)).toBeFocused(); - }); - await test.step('Verify navigation from last child outside the table and back', async () => { - await page.keyboard.press('ArrowRight'); - await expect(locators.getCell(page, 9, 5)).toBeFocused(); - await page.keyboard.press('Tab'); - await expect(locators.button(page, 'Next')).toBeFocused(); - await page.keyboard.press('Shift+Tab'); - await expect(locators.getCell(page, 9, 5)).toBeFocused(); - }); - }); - test('Verify multiple access to cells with spin', { tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, diff --git a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-empty-data-with-selectable-rows-1-chromium-linux.png b/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-empty-data-with-selectable-rows-1-chromium-linux.png deleted file mode 100644 index 43463d4a6c..0000000000 Binary files a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-empty-data-with-selectable-rows-1-chromium-linux.png and /dev/null differ diff --git a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-empty-data-with-selectable-rows-1-firefox-linux.png b/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-empty-data-with-selectable-rows-1-firefox-linux.png deleted file mode 100644 index 57fd467031..0000000000 Binary files a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-empty-data-with-selectable-rows-1-firefox-linux.png and /dev/null differ diff --git a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-empty-data-with-selectable-rows-1-webkit-linux.png b/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-empty-data-with-selectable-rows-1-webkit-linux.png deleted file mode 100644 index 288bcec9a2..0000000000 Binary files a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-empty-data-with-selectable-rows-1-webkit-linux.png and /dev/null differ diff --git a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-styles-when-checkbox-in-merged-cells-checked-by-keyboard-1-chromium-linux.png b/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-styles-when-checkbox-in-merged-cells-checked-by-keyboard-1-chromium-linux.png deleted file mode 100644 index de34fb895c..0000000000 Binary files a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-styles-when-checkbox-in-merged-cells-checked-by-keyboard-1-chromium-linux.png and /dev/null differ diff --git a/semcore/data-table/__tests__/data-table-states.browser-test.tsx b/semcore/data-table/__tests__/data-table-states.browser-test.tsx index 5479c2108a..8a8d8379a3 100644 --- a/semcore/data-table/__tests__/data-table-states.browser-test.tsx +++ b/semcore/data-table/__tests__/data-table-states.browser-test.tsx @@ -2,7 +2,7 @@ import { expect, test } from '@semcore/testing-utils/playwright'; import { loadPage } from '@semcore/testing-utils/shared/helpers'; import { TAG } from '@semcore/testing-utils/shared/tags'; -import { locators } from './utils'; +import { locators, checkStyles } from './utils'; /* ===================================================== @visual @@ -191,6 +191,7 @@ test.describe(`${TAG.VISUAL}`, () => { }); }); }); + test.describe('Limited mode', () => { test(`Verify limited state for table with accordion keyboard and mouse interactions`, { tag: [TAG.PRIORITY_HIGH, @@ -251,6 +252,231 @@ test.describe(`${TAG.VISUAL}`, () => { await expect(page).toHaveScreenshot(); }); }); + + test.describe('Selectable rows (old API)', () => { + test('Verify sideIndents=wide with selectable rows non compact and compact', { + tag: [TAG.PRIORITY_MEDIUM, + '@data-table'], + }, async ({ page }) => { + await loadPage(page, 'stories/components/data-table/docs/examples/checkbox-in-table.tsx', 'en', { + sideIndents: 'wide', + }); + + await test.step('Verify wide for non compact data-table', async () => { + await expect(page).toHaveScreenshot(); + }); + + await test.step('Verify wide for compact data-table', async () => { + await loadPage(page, 'stories/components/data-table/docs/examples/checkbox-in-table.tsx', 'en', { + sideIndents: 'wide', compact: true, + }); + await expect(page).toHaveScreenshot(); + }); + }); + + test('Verify color on hover when merged rows AND columns with multi-level header', { + tag: [TAG.PRIORITY_HIGH, + '@data-table'], + }, async ({ page, browserName }) => { + if (browserName == 'firefox') test.skip(); + + await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/merged-row-for-multi-level-header.tsx', 'en'); + + const consoleErrors: string[] = []; + + page.on('console', (msg) => { + if (msg.type() === 'error') { + consoleErrors.push(msg.text()); + } + }); + + page.on('pageerror', (error) => { + consoleErrors.push(error.message); + }); + + await test.step('Verify Color when child cell hovered', async () => { + await locators.getCell(page, 3, 1).hover(); + + await checkStyles(locators.getCell(page, 3, 1), { 'background-color': 'rgb(240, 240, 244)' }); + await checkStyles(locators.getCell(page, 2, 2), { 'background-color': 'rgb(240, 240, 244)' }); + await checkStyles(locators.getCell(page, 2, 1), { 'background-color': 'rgb(255, 255, 255)' }); + }); + + await test.step('Verify Color when parent cell hovered', async () => { + await locators.getCell(page, 2, 2).hover(); + + for (let row = 2; row <= 5; row++) { + await checkStyles(locators.getCell(page, row, 1), { 'background-color': 'rgb(240, 240, 244)' }); + } + }); + + await test.step('Verify no console errors', async () => { + expect(consoleErrors, `Console errors found:\n${consoleErrors.join('\n')}`).toHaveLength(0); + }); + }); + + test('Verify styles when checkbox in merged cells checked by keyboard', { + tag: [TAG.PRIORITY_HIGH, + TAG.KEYBOARD, + '@data-table', + '@tooltip'], + }, async ({ page, browserName }) => { + await loadPage(page, 'stories/components/data-table/advanced/examples/selectable_with_merged_rows.tsx', 'en'); + + await page.keyboard.press('Tab'); + + await page.keyboard.press('ArrowDown'); + await expect(locators.getCell(page, 2, 1).locator('input')).toBeFocused(); + + await page.keyboard.press('Space'); + + await page.getByRole('button', { name: 'Deselect all' }).waitFor({ state: 'visible' }); + await checkStyles(locators.getCell(page, 2, 1), { 'background-color': 'rgb(233, 247, 255)' }); + await checkStyles(locators.getCell(page, 2, 2), { 'background-color': 'rgb(233, 247, 255)' }); + await checkStyles(locators.getCell(page, 2, 3), { 'background-color': 'rgb(233, 247, 255)' }); + await checkStyles(locators.getCell(page, 2, 4), { 'background-color': 'rgb(233, 247, 255)' }); + await checkStyles(locators.getCell(page, 3, 2), { 'background-color': 'rgb(233, 247, 255)' }); + await checkStyles(locators.getCell(page, 3, 3), { 'background-color': 'rgb(233, 247, 255)' }); + await checkStyles(locators.getCell(page, 3, 4), { 'background-color': 'rgb(233, 247, 255)' }); + await expect(page).toHaveScreenshot(); + + if (browserName == 'firefox') return; + const cell22 = locators.getCell(page, 2, 1); + const box22 = await cell22.boundingBox(); + if (box22) { + await page.mouse.move(box22.x + box22.width / 2, box22.y + box22.height / 2); + } + + await checkStyles(locators.getCell(page, 2, 1), { 'background-color': 'rgb(196, 229, 254)' }); + await checkStyles(locators.getCell(page, 2, 2), { 'background-color': 'rgb(196, 229, 254)' }); + await checkStyles(locators.getCell(page, 2, 3), { 'background-color': 'rgb(196, 229, 254)' }); + await checkStyles(locators.getCell(page, 2, 4), { 'background-color': 'rgb(196, 229, 254)' }); + await checkStyles(locators.getCell(page, 3, 2), { 'background-color': 'rgb(196, 229, 254)' }); + await checkStyles(locators.getCell(page, 3, 3), { 'background-color': 'rgb(196, 229, 254)' }); + await checkStyles(locators.getCell(page, 3, 4), { 'background-color': 'rgb(196, 229, 254)' }); + + const cell23 = locators.getCell(page, 2, 3); + const box23 = await cell23.boundingBox(); + if (box23) { + await page.mouse.move(box23.x + box23.width / 2, box23.y + box23.height / 2); + } + await checkStyles(locators.getCell(page, 2, 1), { 'background-color': 'rgb(196, 229, 254)' }); + await checkStyles(locators.getCell(page, 2, 2), { 'background-color': 'rgb(196, 229, 254)' }); + await checkStyles(locators.getCell(page, 2, 3), { 'background-color': 'rgb(196, 229, 254)' }); + await checkStyles(locators.getCell(page, 2, 4), { 'background-color': 'rgb(196, 229, 254)' }); + await checkStyles(locators.getCell(page, 3, 2), { 'background-color': 'rgb(233, 247, 255)' }); + await checkStyles(locators.getCell(page, 3, 3), { 'background-color': 'rgb(233, 247, 255)' }); + await checkStyles(locators.getCell(page, 3, 4), { 'background-color': 'rgb(233, 247, 255)' }); + }); + }); + + test.describe('SelectableRows (reactive API)', () => { + test('Verify SelectableRows row highlight on selection', { + tag: [TAG.PRIORITY_HIGH, + '@data-table'], + }, async ({ page, browserName }) => { + await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/checkbox-reactive.tsx', 'en'); + if (browserName == 'firefox') return; + const firstRowCheckbox = locators.getCell(page, 2, 1).locator('label'); + await firstRowCheckbox.click(); + + await checkStyles(locators.getCell(page, 2, 1), { 'background-color': 'rgb(196, 229, 254)' }); + await checkStyles(locators.getCell(page, 2, 2), { 'background-color': 'rgb(196, 229, 254)' }); + await checkStyles(locators.getCell(page, 2, 3), { 'background-color': 'rgb(196, 229, 254)' }); + + await expect(page).toHaveScreenshot(); + }); + + test('Verify SelectableRows select all rows highlight', { + tag: [TAG.PRIORITY_HIGH, + '@data-table'], + }, async ({ page }) => { + await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/checkbox-reactive.tsx', 'en'); + + const selectAllCheckbox = page.locator('[data-ui-name="DataTable.Head"] [data-ui-name="Checkbox"]'); + await selectAllCheckbox.click(); + + const bodyRows = page.locator('[data-ui-name="DataTable.Body"] [data-ui-name="DataTable.Row"]'); + const rowCount = await bodyRows.count(); + + for (let i = 0; i < rowCount; i++) { + const row = bodyRows.nth(i); + const cells = row.locator('[data-ui-name="Row.Cell"]'); + const cellCount = await cells.count(); + for (let j = 0; j < cellCount; j++) { + await checkStyles(cells.nth(j), { 'background-color': 'rgb(196, 229, 254)' }); + } + } + }); + + test('Verify color on hover when merged rows with SelectableRows', { + tag: [TAG.PRIORITY_HIGH, + '@data-table'], + }, async ({ page, browserName }) => { + if (browserName === 'firefox') test.skip(); + + await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/checkbox-reactive-with-pagination.tsx', 'en', { mergedRows: true }); + + const consoleErrors: string[] = []; + + page.on('console', (msg) => { + if (msg.type() === 'error') { + consoleErrors.push(msg.text()); + } + }); + + page.on('pageerror', (error) => { + consoleErrors.push(error.message); + }); + + await test.step('Verify color when child cell hovered', async () => { + await locators.getCell(page, 3, 3).hover(); + + await checkStyles(locators.getCell(page, 2, 1), { 'background-color': 'rgb(240, 240, 244)' }); + await checkStyles(locators.getCell(page, 2, 2), { 'background-color': 'rgb(240, 240, 244)' }); + await checkStyles(locators.getCell(page, 2, 3), { 'background-color': 'rgb(255, 255, 255)' }); + await checkStyles(locators.getCell(page, 3, 3), { 'background-color': 'rgb(240, 240, 244)' }); + }); + + await test.step('Verify color when parent merged cell hovered', async () => { + await locators.getCell(page, 2, 2).hover(); + + for (let row = 2; row <= 3; row++) { + await checkStyles(locators.getCell(page, row, 1), { 'background-color': 'rgb(240, 240, 244)' }); + } + }); + + await test.step('Verify color when checkbox checked', async () => { + const firstRowCheckbox = locators.getCell(page, 2, 1).locator('label'); + await firstRowCheckbox.click(); + await locators.collapse(page).waitFor({ state: 'visible' }); + for (let row = 2; row <= 3; row++) { + await checkStyles(locators.getCell(page, row, 1), { 'background-color': 'rgb(196, 229, 254)' }); + } + }); + + await test.step('Verify color when parent hovered', async () => { + await locators.getCell(page, 2, 2).hover(); + + for (let row = 2; row <= 3; row++) { + await checkStyles(locators.getCell(page, row, 1), { 'background-color': 'rgb(196, 229, 254)' }); + } + }); + + await test.step('Verify color when child cell hovered', async () => { + await locators.getCell(page, 3, 3).hover(); + + await checkStyles(locators.getCell(page, 2, 1), { 'background-color': 'rgb(196, 229, 254)' }); + await checkStyles(locators.getCell(page, 2, 2), { 'background-color': 'rgb(196, 229, 254)' }); + await checkStyles(locators.getCell(page, 2, 3), { 'background-color': 'rgb(233, 247, 255)' }); + await checkStyles(locators.getCell(page, 3, 3), { 'background-color': 'rgb(196, 229, 254)' }); + }); + + await test.step('Verify no console errors', async () => { + expect(consoleErrors, `Console errors found:\n${consoleErrors.join('\n')}`).toHaveLength(0); + }); + }); + }); }); /* ===================================================== @@ -311,7 +537,16 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); }); - test.describe('Checkbox in table', () => { + test.describe('Selectable rows (old API)', () => { + test('Verify empty data with selectable rows', { + tag: [TAG.PRIORITY_MEDIUM, + '@data-table'], + }, async ({ page }) => { + await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/checkbox-in-table-with-no-data.tsx', 'en'); + + const selectAllCheckbox = page.locator('[data-ui-name="DataTable.Head"] [data-ui-name="Checkbox"]'); + await expect(selectAllCheckbox).not.toBeChecked(); + }); test('Verify table with checkbox attributes and mouse interaction', { tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, @@ -340,6 +575,8 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { await expect(collapse).toBeHidden(); await expect(selectedRowsCount).toBeHidden(); await expect(selectAllCheckbox).not.toBeChecked(); + for (let i = 0; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).not.toBeChecked(); }); await test.step('Verify each checkbox in cell has aria-labelledby ', async () => { @@ -389,7 +626,8 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { await expect(selectedRowsCount).toHaveText('10'); await expect(selectAllCheckbox).toBeChecked(); - await expect(selectAllCheckbox).toHaveClass(/checked/); + for (let i = 0; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).toBeChecked(); }); await test.step('Verify action bar when one item on next page unchecked', async () => { @@ -398,6 +636,8 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { await expect(rowCheckboxes.first()).not.toBeChecked(); await expect(selectedRowsCount).toHaveText('9'); await expect(selectAllCheckbox).toHaveClass(/indeterminate/); + for (let i = 1; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).toBeChecked(); }); await test.step('Verify action bar when next page opened', async () => { @@ -405,11 +645,16 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { await expect(selectAllCheckbox).not.toBeChecked(); await expect(selectedRowsCount).toHaveText('9'); + for (let i = 0; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).not.toBeChecked(); }); await test.step('Verify indeterminate state saved when prev button is opened', async () => { await prevButton.click(); await expect(selectAllCheckbox).toHaveClass(/indeterminate/); + await expect(rowCheckboxes.first()).not.toBeChecked(); + for (let i = 1; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).toBeChecked(); }); await test.step('Verify checked state on all pages changes to undhecked by click on Deselect all', async () => { @@ -466,13 +711,8 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { await expect(selectedRowsCount).toHaveText('5'); await expect(selectAllCheckbox).toBeChecked(); - const count = await firstColumnCells.count(); - for (let i = 0; i < count; i++) { - const firstColumnCell = firstColumnCells.nth(i); - const checkbox = firstColumnCell.locator( - 'input[type="checkbox"][data-ui-name="Checkbox.Value"]', - ); - } + for (let i = 0; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).toBeChecked(); }); await test.step('Verify panel state when next page opened', async () => { await page.keyboard.press('Tab'); @@ -481,6 +721,8 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { await expect(collapse).toBeVisible(); await expect(selectedRowsCount).toHaveText('5'); await expect(selectAllCheckbox).not.toBeChecked(); + for (let i = 0; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).not.toBeChecked(); }); await test.step('Verify panel when activating Select all on text page', async () => { await page.keyboard.press('Shift+Tab'); @@ -489,17 +731,21 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { await page.keyboard.press('Space'); await expect(selectedRowsCount).toHaveText('10'); await expect(selectAllCheckbox).toBeChecked(); - await expect(selectAllCheckbox).toHaveClass(/checked/); + for (let i = 0; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).toBeChecked(); }); await test.step('Verify counter on the panel decreased and indeterminate state when uncheck one checkbox', async () => { await page.keyboard.press('ArrowDown'); await page.keyboard.press('ArrowDown'); await page.keyboard.press('Space'); + await expect(rowCheckboxes.nth(0)).toBeChecked(); await expect(rowCheckboxes.nth(1)).not.toBeChecked(); await expect(selectedRowsCount).toHaveText('9'); await expect(selectAllCheckbox).toHaveClass(/indeterminate/); + for (let i = 2; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).toBeChecked(); }); await test.step('Verify panel when opening next page', async () => { await page.keyboard.press('Tab'); @@ -508,15 +754,20 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { await page.keyboard.press('Space'); await expect(selectAllCheckbox).not.toBeChecked(); await expect(selectedRowsCount).toHaveText('9'); + for (let i = 0; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).not.toBeChecked(); }); await test.step('Verify panel state saved on prev pages', async () => { await page.keyboard.press('Shift+Tab'); await page.keyboard.press('Space'); await expect(selectAllCheckbox).toHaveClass(/indeterminate/); + await expect(rowCheckboxes.nth(0)).toBeChecked(); + await expect(rowCheckboxes.nth(1)).not.toBeChecked(); + for (let i = 2; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).toBeChecked(); await page.keyboard.press('Shift+Tab'); await page.keyboard.press('Space'); - await expect(selectAllCheckbox).toBeChecked(); }); if (browserName === 'webkit') return; // because of pagination bus in safari @@ -537,6 +788,568 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { await expect(selectAllCheckbox).not.toBeChecked(); }); }); + + test('Verify select rows with Shift', { + tag: [ + TAG.KEYBOARD, + TAG.MOUSE, + + '@data-table', + ], + }, async ({ page }) => { + await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/checkbox.tsx', 'en'); + + const firstCell = locators.getCell(page, 3, 1); + const secondCell = locators.getCell(page, 7, 1); + + await firstCell.locator('label').click(); + await secondCell.locator('label').click({ modifiers: ['Shift'] }); + + for (let i = 3; i <= 7; i++) { + await expect(locators.getCell(page, i, 1).locator('input')).toBeChecked(); + } + + await locators.getCell(page, 5, 1).locator('label').click({ modifiers: ['Shift'] }); + + for (let i = 5; i <= 7; i++) { + await expect(locators.getCell(page, i, 1).locator('input')).not.toBeChecked(); + } + + await locators.getCell(page, 9, 1).locator('label').click({ modifiers: ['Shift'] }); + for (let i = 5; i <= 8; i++) { + await expect(locators.getCell(page, i, 1).locator('input')).not.toBeChecked(); + } + await expect(locators.getCell(page, 9, 1).locator('input')).toBeChecked(); + }); + + test('Verify select rows with Shift when checkbox in merged cells', { + tag: [ + TAG.KEYBOARD, + TAG.MOUSE, + '@data-table', + ], + }, async ({ page }) => { + await loadPage(page, 'stories/components/data-table/advanced/examples/selectable_with_merged_rows.tsx', 'en'); + + const firstCell = locators.getCell(page, 2, 1); + const secondCell = locators.getCell(page, 4, 1); + + await firstCell.locator('label').click(); + await secondCell.locator('label').click({ modifiers: ['Shift'] }); + + await expect(locators.getCell(page, 2, 1).locator('input')).toBeChecked(); + await expect(locators.getCell(page, 4, 1).locator('input')).toBeChecked(); + + await locators.getCell(page, 8, 1).locator('label').click({ modifiers: ['Shift'] }); + + await expect(locators.getCell(page, 2, 1).locator('input')).toBeChecked(); + await expect(locators.getCell(page, 4, 1).locator('input')).toBeChecked(); + await expect(locators.getCell(page, 6, 1).locator('input')).toBeChecked(); + await expect(locators.getCell(page, 8, 1).locator('input')).toBeChecked(); + }); + + test('Verify keyboard interaction when checkbox in merged cells', { + tag: [TAG.PRIORITY_HIGH, + TAG.KEYBOARD, + '@data-table', + '@tooltip'], + }, async ({ page }) => { + await loadPage(page, 'stories/components/data-table/advanced/examples/selectable_with_merged_rows.tsx', 'en'); + + await test.step('Verify Focus on checkbox', async () => { + await page.keyboard.press('Tab'); + await page.keyboard.press('ArrowDown'); + await expect(locators.getCell(page, 2, 1).locator('input')).toBeFocused(); + }); + + await test.step('Verify navigation to the 1st child', async () => { + await page.keyboard.press('Space'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + await expect(locators.getCell(page, 2, 3)).toBeFocused(); + }); + await test.step('Verify focus returns to checkbox', async () => { + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await expect(locators.getCell(page, 2, 1).locator('input')).toBeFocused(); + }); + await test.step('Verify navigation from non merged to merged', async () => { + for (let i = 0; i < 4; i++) + await page.keyboard.press('ArrowDown'); + await expect(locators.getCell(page, 10, 1).locator('input')).toBeFocused(); + await page.keyboard.press('ArrowUp'); + await expect(locators.getCell(page, 8, 1).locator('input')).toBeFocused(); + }); + await test.step('Verify navigation from 2nd child outside the table and back', async () => { + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowDown'); + await expect(locators.getCell(page, 9, 4)).toBeFocused(); + + await page.keyboard.press('Tab'); + await expect(locators.button(page, 'Next')).toBeFocused(); + await page.keyboard.press('Shift+Tab'); + await expect(locators.getCell(page, 9, 4)).toBeFocused(); + }); + await test.step('Verify navigation from last child outside the table and back', async () => { + await page.keyboard.press('ArrowRight'); + await expect(locators.getCell(page, 9, 5)).toBeFocused(); + await page.keyboard.press('Tab'); + await expect(locators.button(page, 'Next')).toBeFocused(); + await page.keyboard.press('Shift+Tab'); + await expect(locators.getCell(page, 9, 5)).toBeFocused(); + }); + }); + }); + + test.describe('SelectableRows (reactive API)', () => { + test('Verify SelectableRows empty data with selectable rows', { + tag: [TAG.PRIORITY_MEDIUM, + '@data-table'], + }, async ({ page }) => { + await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/checkbox-in-table-with-no-data.tsx', 'en', { reactive: true }); + + const selectAllCheckbox = page.locator('[data-ui-name="DataTable.Head"] [data-ui-name="Checkbox"]'); + await expect(selectAllCheckbox).not.toBeChecked(); + }); + + test('Verify SelectableRows checkbox attributes and mouse interaction', { + tag: [TAG.PRIORITY_HIGH, + TAG.MOUSE, + '@data-table'], + }, async ({ page }) => { + await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/checkbox-reactive.tsx', 'en'); + + const selectAllCheckbox = page.locator('[data-ui-name="DataTable.Head"] [data-ui-name="Checkbox"]'); + const headerCheckbox = locators.getHeadColumn(page, 1).locator('input'); + const rowCheckboxes = page.locator('[data-ui-name="DataTable.Body"] [data-ui-name="Checkbox"]'); + + await test.step('Verify header checkbox has aria-label', async () => { + await expect(headerCheckbox).toHaveAttribute('aria-label', 'All items'); + }); + + await test.step('Verify no selection initially', async () => { + await expect(selectAllCheckbox).not.toBeChecked(); + await expect(rowCheckboxes.first()).not.toBeChecked(); + }); + + await test.step('Verify single row selection by click', async () => { + await rowCheckboxes.first().click(); + await expect(rowCheckboxes.first()).toBeChecked(); + await expect(selectAllCheckbox).toHaveClass(/indeterminate/); + }); + + await test.step('Verify single row deselection by click', async () => { + await rowCheckboxes.first().click(); + await expect(rowCheckboxes.first()).not.toBeChecked(); + await expect(selectAllCheckbox).not.toBeChecked(); + }); + + await test.step('Verify select all by header checkbox', async () => { + await selectAllCheckbox.click(); + await expect(selectAllCheckbox).toBeChecked(); + + const count = await rowCheckboxes.count(); + for (let i = 0; i < count; i++) { + await expect(rowCheckboxes.nth(i)).toBeChecked(); + } + }); + + await test.step('Verify deselect all by header checkbox', async () => { + await selectAllCheckbox.click(); + await expect(selectAllCheckbox).not.toBeChecked(); + + const count = await rowCheckboxes.count(); + for (let i = 0; i < count; i++) { + await expect(rowCheckboxes.nth(i)).not.toBeChecked(); + } + }); + + await test.step('Verify indeterminate state', async () => { + await selectAllCheckbox.click(); + await rowCheckboxes.first().click(); + + await expect(rowCheckboxes.first()).not.toBeChecked(); + await expect(selectAllCheckbox).toHaveClass(/indeterminate/); + }); + }); + + test('Verify SelectableRows keyboard navigation and selection', { + tag: [TAG.PRIORITY_HIGH, + TAG.KEYBOARD, + '@data-table'], + }, async ({ page }) => { + await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/checkbox-reactive.tsx', 'en'); + + const selectAllCheckbox = page.locator('[data-ui-name="DataTable.Head"] [data-ui-name="Checkbox"]'); + const headerCheckbox = locators.getHeadColumn(page, 1).locator('input'); + const rowCheckboxes = page.locator('[data-ui-name="DataTable.Body"] [data-ui-name="Checkbox"]'); + + await test.step('Verify header checkbox focused by Tab', async () => { + await page.keyboard.press('Tab'); + await expect(headerCheckbox).toBeFocused(); + }); + + await test.step('Verify select all by Space on header', async () => { + await page.keyboard.press('Space'); + + await expect(selectAllCheckbox).toBeChecked(); + const count = await rowCheckboxes.count(); + for (let i = 0; i < count; i++) { + await expect(rowCheckboxes.nth(i)).toBeChecked(); + } + }); + + await test.step('Verify deselect all by Space on header', async () => { + await page.keyboard.press('Space'); + + await expect(selectAllCheckbox).not.toBeChecked(); + const count = await rowCheckboxes.count(); + for (let i = 0; i < count; i++) { + await expect(rowCheckboxes.nth(i)).not.toBeChecked(); + } + }); + + await test.step('Verify single row selection by keyboard', async () => { + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Space'); + + await expect(rowCheckboxes.first()).toBeChecked(); + await expect(selectAllCheckbox).toHaveClass(/indeterminate/); + }); + + await test.step('Verify single row deselection by keyboard', async () => { + await page.keyboard.press('Space'); + + await expect(rowCheckboxes.first()).not.toBeChecked(); + await expect(selectAllCheckbox).not.toBeChecked(); + }); + }); + + test('Verify SelectableRows with pagination mouse interaction', { + tag: [TAG.PRIORITY_HIGH, + TAG.MOUSE, + '@data-table'], + }, async ({ page }) => { + await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/checkbox-reactive-with-pagination.tsx', 'en'); + + const selectAllCheckbox = page.locator('[data-ui-name="DataTable.Head"] [data-ui-name="Checkbox"]'); + const collapse = locators.collapse(page); + const selectedRowsCount = collapse.locator('[data-ui-name="Text"]').nth(1); + const deselectAllButton = collapse.locator('button'); + const nextButton = page.locator('[data-ui-name="Pagination.NextPage"]'); + const prevButton = page.locator('[data-ui-name="Pagination.PrevPage"]'); + const rowCheckboxes = page.locator('[data-ui-name="DataTable.Body"] [data-ui-name="Checkbox"]'); + + await test.step('Verify no action bar when nothing selected', async () => { + await expect(collapse).toBeHidden(); + await expect(selectAllCheckbox).not.toBeChecked(); + }); + + await test.step('Verify action bar when header checkbox is checked', async () => { + await selectAllCheckbox.click(); + await page.getByRole('button', { name: 'Deselect all' }).waitFor({ state: 'visible' }); + + await expect(collapse).toBeVisible(); + await expect(selectedRowsCount).toHaveText('5'); + await expect(selectAllCheckbox).toBeChecked(); + }); + + await test.step('Verify action bar persists when navigating to next page', async () => { + await nextButton.click(); + + await expect(collapse).toBeVisible(); + await expect(selectedRowsCount).toHaveText('5'); + await expect(selectAllCheckbox).not.toBeChecked(); + const count = await rowCheckboxes.count(); + for (let i = 0; i < count; i++) + await expect(rowCheckboxes.nth(i)).not.toBeChecked(); + }); + + await test.step('Verify counter increases when selecting on next page', async () => { + await selectAllCheckbox.click(); + + await expect(selectedRowsCount).toHaveText('10'); + await expect(selectAllCheckbox).toBeChecked(); + const count = await rowCheckboxes.count(); + for (let i = 0; i < count; i++) + await expect(rowCheckboxes.nth(i)).toBeChecked(); + }); + + await test.step('Verify indeterminate when one row unchecked', async () => { + await rowCheckboxes.first().click(); + + await expect(rowCheckboxes.first()).not.toBeChecked(); + const count = await rowCheckboxes.count(); + for (let i = 1; i < count; i++) + await expect(rowCheckboxes.nth(i)).toBeChecked(); + await expect(selectedRowsCount).toHaveText('9'); + await expect(selectAllCheckbox).toHaveClass(/indeterminate/); + }); + + await test.step('Verify counter on next page', async () => { + await nextButton.click(); + + await expect(selectAllCheckbox).not.toBeChecked(); + const count = await rowCheckboxes.count(); + for (let i = 0; i < count; i++) + await expect(rowCheckboxes.nth(i)).not.toBeChecked(); + await expect(selectedRowsCount).toHaveText('9'); + }); + + await test.step('Verify indeterminate state saved when prev button clicked', async () => { + await prevButton.click(); + await expect(selectAllCheckbox).toHaveClass(/indeterminate/); + await expect(rowCheckboxes.first()).not.toBeChecked(); + const count = await rowCheckboxes.count(); + for (let i = 1; i < count; i++) + await expect(rowCheckboxes.nth(i)).toBeChecked(); + }); + + await test.step('Verify deselect all clears everything', async () => { + await deselectAllButton.click(); + + await expect(collapse).toBeHidden(); + await expect(selectAllCheckbox).not.toBeChecked(); + for (let i = 0; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).not.toBeChecked(); + + await nextButton.click(); + await expect(selectAllCheckbox).not.toBeChecked(); + for (let i = 0; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).not.toBeChecked(); + }); + }); + + test('Verify SelectableRows with pagination keyboard interaction', { + tag: [TAG.PRIORITY_HIGH, + TAG.KEYBOARD, + '@data-table'], + }, async ({ page, browserName }) => { + await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/checkbox-reactive-with-pagination.tsx', 'en'); + + const selectAllCheckbox = page.locator('[data-ui-name="DataTable.Head"] [data-ui-name="Checkbox"]'); + const collapse = locators.collapse(page); + const selectedRowsCount = collapse.locator('[data-ui-name="Text"]').nth(1); + const rowCheckboxes = page.locator('[data-ui-name="DataTable.Body"] [data-ui-name="Checkbox"]'); + + await test.step('Verify select all via keyboard', async () => { + await page.keyboard.press('Tab'); + await page.keyboard.press('Space'); + await page.getByRole('button', { name: 'Deselect all' }).waitFor({ state: 'visible' }); + + await expect(collapse).toBeVisible(); + await expect(selectedRowsCount).toHaveText('5'); + await expect(selectAllCheckbox).toBeChecked(); + for (let i = 0; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).toBeChecked(); + }); + + await test.step('Verify navigate to next page', async () => { + await page.keyboard.press('Tab'); + await page.keyboard.press('Enter'); + + await expect(collapse).toBeVisible(); + await expect(selectedRowsCount).toHaveText('5'); + await expect(selectAllCheckbox).not.toBeChecked(); + for (let i = 0; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).not.toBeChecked(); + }); + + await test.step('Verify select all on second page', async () => { + await page.keyboard.press('Shift+Tab'); + await expect(page.getByRole('button', { name: 'Prev' })).toBeFocused(); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Space'); + await expect(selectedRowsCount).toHaveText('10'); + await expect(selectAllCheckbox).toBeChecked(); + for (let i = 0; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).toBeChecked(); + }); + + await test.step('Verify uncheck single row', async () => { + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Space'); + await expect(rowCheckboxes.nth(0)).toBeChecked(); + await expect(rowCheckboxes.nth(1)).not.toBeChecked(); + for (let i = 2; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).toBeChecked(); + await expect(selectedRowsCount).toHaveText('9'); + await expect(selectAllCheckbox).toHaveClass(/indeterminate/); + }); + + if (browserName === 'webkit') return; + + await test.step('Verify deselect all via keyboard', async () => { + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Space'); + + await expect(collapse).toBeHidden(); + await expect(selectAllCheckbox).not.toBeChecked(); + for (let i = 0; i < await rowCheckboxes.count(); i++) + await expect(rowCheckboxes.nth(i)).not.toBeChecked(); + }); + }); + + test('Verify SelectableRows select rows with Shift', { + tag: [ + TAG.PRIORITY_HIGH, + TAG.KEYBOARD, + TAG.MOUSE, + '@data-table', + ], + }, async ({ page }) => { + await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/checkbox-reactive.tsx', 'en'); + + const firstCell = locators.getCell(page, 3, 1); + const secondCell = locators.getCell(page, 7, 1); + + await test.step('Verify Shift+click selects range', async () => { + await firstCell.locator('label').click(); + await secondCell.locator('label').click({ modifiers: ['Shift'] }); + + for (let i = 3; i <= 7; i++) { + await expect(locators.getCell(page, i, 1).locator('input')).toBeChecked(); + } + }); + + await test.step('Verify Shift+click deselects partial range', async () => { + await locators.getCell(page, 5, 1).locator('label').click({ modifiers: ['Shift'] }); + + for (let i = 5; i <= 7; i++) { + await expect(locators.getCell(page, i, 1).locator('input')).not.toBeChecked(); + } + }); + + await test.step('Verify Shift+click on unchecked range', async () => { + await locators.getCell(page, 9, 1).locator('label').click({ modifiers: ['Shift'] }); + for (let i = 5; i <= 8; i++) { + await expect(locators.getCell(page, i, 1).locator('input')).not.toBeChecked(); + } + await expect(locators.getCell(page, 9, 1).locator('input')).toBeChecked(); + }); + }); + + test('Verify SelectableRows Shift selection with merged rows', { + tag: [ + TAG.PRIORITY_HIGH, + TAG.KEYBOARD, + TAG.MOUSE, + '@data-table', + ], + }, async ({ page }) => { + await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/checkbox-reactive-with-pagination.tsx', 'en', { mergedRows: true }); + + const firstCell = locators.getCell(page, 2, 1); + const secondCell = locators.getCell(page, 4, 1); + + await test.step('Verify Shift+click selects range in merged rows', async () => { + await firstCell.locator('label').click(); + await secondCell.locator('label').click({ modifiers: ['Shift'] }); + + await expect(locators.getCell(page, 2, 1).locator('input')).toBeChecked(); + await expect(locators.getCell(page, 4, 1).locator('input')).toBeChecked(); + }); + + await test.step('Verify Shift+click extends selection in merged rows', async () => { + await locators.getCell(page, 8, 1).locator('label').click({ modifiers: ['Shift'] }); + + await expect(locators.getCell(page, 2, 1).locator('input')).toBeChecked(); + await expect(locators.getCell(page, 4, 1).locator('input')).toBeChecked(); + await expect(locators.getCell(page, 6, 1).locator('input')).toBeChecked(); + await expect(locators.getCell(page, 8, 1).locator('input')).toBeChecked(); + }); + }); + + test('Verify SelectableRows with merged rows mouse interaction', { + tag: [TAG.PRIORITY_HIGH, + TAG.MOUSE, + '@data-table'], + }, async ({ page }) => { + await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/checkbox-reactive-with-pagination.tsx', 'en', { mergedRows: true }); + + const selectAllCheckbox = page.locator('[data-ui-name="DataTable.Head"] [data-ui-name="Checkbox"]'); + const collapse = locators.collapse(page); + const selectedRowsCount = collapse.locator('[data-ui-name="Text"]').nth(1); + const rowCheckboxes = page.locator('[data-ui-name="DataTable.Body"] [data-ui-name="Checkbox"]'); + + await test.step('Verify single row selection in merged group', async () => { + await rowCheckboxes.first().click(); + await page.getByRole('button', { name: 'Deselect all' }).waitFor({ state: 'visible' }); + + await expect(rowCheckboxes.first()).toBeChecked(); + await expect(collapse).toBeVisible(); + await expect(selectedRowsCount).toHaveText('1'); + await expect(selectAllCheckbox).toHaveClass(/indeterminate/); + }); + + await test.step('Verify deselection', async () => { + await rowCheckboxes.first().click(); + await expect(rowCheckboxes.first()).not.toBeChecked(); + await expect(selectAllCheckbox).not.toBeChecked(); + }); + + await test.step('Verify select all with merged rows', async () => { + await selectAllCheckbox.click(); + await page.getByRole('button', { name: 'Deselect all' }).waitFor({ state: 'visible' }); + + await expect(selectAllCheckbox).toBeChecked(); + const count = await rowCheckboxes.count(); + for (let i = 0; i < count; i++) { + await expect(rowCheckboxes.nth(i)).toBeChecked(); + } + }); + + await test.step('Verify deselect all with merged rows', async () => { + await selectAllCheckbox.click(); + await expect(selectAllCheckbox).not.toBeChecked(); + const count = await rowCheckboxes.count(); + for (let i = 0; i < count; i++) { + await expect(rowCheckboxes.nth(i)).not.toBeChecked(); + } + }); + }); + + test('Verify SelectableRows with merged rows keyboard interaction', { + tag: [TAG.PRIORITY_HIGH, + TAG.KEYBOARD, + '@data-table'], + }, async ({ page }) => { + await loadPage(page, 'stories/components/data-table/tests/examples/cells-tests/checkbox-reactive-with-pagination.tsx', 'en', { mergedRows: true }); + + const selectAllCheckbox = page.locator('[data-ui-name="DataTable.Head"] [data-ui-name="Checkbox"]'); + const rowCheckboxes = page.locator('[data-ui-name="DataTable.Body"] [data-ui-name="Checkbox"]'); + + await test.step('Verify focus on header checkbox', async () => { + await page.keyboard.press('Tab'); + await expect(locators.getHeadColumn(page, 1).locator('input')).toBeFocused(); + }); + + await test.step('Verify select all via keyboard', async () => { + await page.keyboard.press('Space'); + await page.getByRole('button', { name: 'Deselect all' }).waitFor({ state: 'visible' }); + + await expect(selectAllCheckbox).toBeChecked(); + }); + + await test.step('Verify navigate to row checkbox and deselect', async () => { + await page.keyboard.press('ArrowDown'); + await expect(locators.getCell(page, 2, 1).locator('input')).toBeFocused(); + + await page.keyboard.press('Space'); + await expect(rowCheckboxes.first()).not.toBeChecked(); + await expect(selectAllCheckbox).toHaveClass(/indeterminate/); + }); + + await test.step('Verify re-select row', async () => { + await page.keyboard.press('Space'); + await expect(rowCheckboxes.first()).toBeChecked(); + await expect(selectAllCheckbox).toBeChecked(); + }); + }); }); test.describe('Limited state', () => { diff --git a/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Limited-mode-Verify-limited-state-for-checkbox-in-table-1-chromium-linux.png b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Limited-mode-Verify-limited-state-for-checkbox-in-table-1-chromium-linux.png index 665be5b7fb..c671a1294c 100644 Binary files a/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Limited-mode-Verify-limited-state-for-checkbox-in-table-1-chromium-linux.png and b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Limited-mode-Verify-limited-state-for-checkbox-in-table-1-chromium-linux.png differ diff --git a/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Limited-mode-Verify-limited-state-for-table-with-accordion-keyboard-and-mouse-interactions-2-chromium-linux.png b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Limited-mode-Verify-limited-state-for-table-with-accordion-keyboard-and-mouse-interactions-2-chromium-linux.png index 788a669a3b..f9d1b531ac 100644 Binary files a/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Limited-mode-Verify-limited-state-for-table-with-accordion-keyboard-and-mouse-interactions-2-chromium-linux.png and b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Limited-mode-Verify-limited-state-for-table-with-accordion-keyboard-and-mouse-interactions-2-chromium-linux.png differ diff --git a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-sideIndents-wide-with-selectable-rows-non-compact-and-compact-2-chromium-linux.png b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-sideI-37a5a--with-selectable-rows-non-compact-and-compact-2-chromium-linux.png similarity index 100% rename from semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-sideIndents-wide-with-selectable-rows-non-compact-and-compact-2-chromium-linux.png rename to semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-sideI-37a5a--with-selectable-rows-non-compact-and-compact-2-chromium-linux.png diff --git a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-sideIndents-wide-with-selectable-rows-non-compact-and-compact-2-firefox-linux.png b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-sideI-37a5a--with-selectable-rows-non-compact-and-compact-2-firefox-linux.png similarity index 100% rename from semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-sideIndents-wide-with-selectable-rows-non-compact-and-compact-2-firefox-linux.png rename to semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-sideI-37a5a--with-selectable-rows-non-compact-and-compact-2-firefox-linux.png diff --git a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-sideIndents-wide-with-selectable-rows-non-compact-and-compact-2-webkit-linux.png b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-sideI-37a5a--with-selectable-rows-non-compact-and-compact-2-webkit-linux.png similarity index 100% rename from semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-sideIndents-wide-with-selectable-rows-non-compact-and-compact-2-webkit-linux.png rename to semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-sideI-37a5a--with-selectable-rows-non-compact-and-compact-2-webkit-linux.png diff --git a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-sideIndents-wide-with-selectable-rows-non-compact-and-compact-1-chromium-linux.png b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-sideI-62955--with-selectable-rows-non-compact-and-compact-1-chromium-linux.png similarity index 100% rename from semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-sideIndents-wide-with-selectable-rows-non-compact-and-compact-1-chromium-linux.png rename to semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-sideI-62955--with-selectable-rows-non-compact-and-compact-1-chromium-linux.png diff --git a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-sideIndents-wide-with-selectable-rows-non-compact-and-compact-1-firefox-linux.png b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-sideI-62955--with-selectable-rows-non-compact-and-compact-1-firefox-linux.png similarity index 100% rename from semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-sideIndents-wide-with-selectable-rows-non-compact-and-compact-1-firefox-linux.png rename to semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-sideI-62955--with-selectable-rows-non-compact-and-compact-1-firefox-linux.png diff --git a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-sideIndents-wide-with-selectable-rows-non-compact-and-compact-1-webkit-linux.png b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-sideI-62955--with-selectable-rows-non-compact-and-compact-1-webkit-linux.png similarity index 100% rename from semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-sideIndents-wide-with-selectable-rows-non-compact-and-compact-1-webkit-linux.png rename to semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-sideI-62955--with-selectable-rows-non-compact-and-compact-1-webkit-linux.png diff --git a/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-styles-when-checkbox-in-merged-cells-checked-by-keyboard-1-chromium-linux.png b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-styles-when-checkbox-in-merged-cells-checked-by-keyboard-1-chromium-linux.png new file mode 100644 index 0000000000..1c1c4c341e Binary files /dev/null and b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-styles-when-checkbox-in-merged-cells-checked-by-keyboard-1-chromium-linux.png differ diff --git a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-styles-when-checkbox-in-merged-cells-checked-by-keyboard-1-firefox-linux.png b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-styles-when-checkbox-in-merged-cells-checked-by-keyboard-1-firefox-linux.png similarity index 100% rename from semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-styles-when-checkbox-in-merged-cells-checked-by-keyboard-1-firefox-linux.png rename to semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-styles-when-checkbox-in-merged-cells-checked-by-keyboard-1-firefox-linux.png diff --git a/semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-styles-when-checkbox-in-merged-cells-checked-by-keyboard-1-webkit-linux.png b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-styles-when-checkbox-in-merged-cells-checked-by-keyboard-1-webkit-linux.png similarity index 100% rename from semcore/data-table/__tests__/data-table-cells.browser-test.tsx-snapshots/-visual-Verify-styles-when-checkbox-in-merged-cells-checked-by-keyboard-1-webkit-linux.png rename to semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-Selectable-rows-old-API-Verify-styles-when-checkbox-in-merged-cells-checked-by-keyboard-1-webkit-linux.png diff --git a/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-SelectableRows-reactive-API-Verify-SelectableRows-row-highlight-on-selection-1-chromium-linux.png b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-SelectableRows-reactive-API-Verify-SelectableRows-row-highlight-on-selection-1-chromium-linux.png new file mode 100644 index 0000000000..a5bfbd02a9 Binary files /dev/null and b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-SelectableRows-reactive-API-Verify-SelectableRows-row-highlight-on-selection-1-chromium-linux.png differ diff --git a/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-SelectableRows-reactive-API-Verify-SelectableRows-row-highlight-on-selection-1-webkit-linux.png b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-SelectableRows-reactive-API-Verify-SelectableRows-row-highlight-on-selection-1-webkit-linux.png new file mode 100644 index 0000000000..503db94da6 Binary files /dev/null and b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-SelectableRows-reactive-API-Verify-SelectableRows-row-highlight-on-selection-1-webkit-linux.png differ diff --git a/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-SelectableRows-reactive-API-Verify-SelectableRows-visual---row-highlight-on-selection-1-firefox-linux.png b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-SelectableRows-reactive-API-Verify-SelectableRows-visual---row-highlight-on-selection-1-firefox-linux.png new file mode 100644 index 0000000000..51cd1b1dd6 Binary files /dev/null and b/semcore/data-table/__tests__/data-table-states.browser-test.tsx-snapshots/-visual-SelectableRows-reactive-API-Verify-SelectableRows-visual---row-highlight-on-selection-1-firefox-linux.png differ diff --git a/semcore/data-table/src/components/Body/Body.tsx b/semcore/data-table/src/components/Body/Body.tsx index 3bb19a92fb..7831b3e995 100644 --- a/semcore/data-table/src/components/Body/Body.tsx +++ b/semcore/data-table/src/components/Body/Body.tsx @@ -6,15 +6,16 @@ import Spin from '@semcore/spin'; import * as React from 'react'; import type { BodyPropsInner, DataTableBodyProps, DataTableBodyType } from './Body.types'; -import { MergedColumnsCell, MergedRowsCell } from './MergedCells'; +import { MergedColumnsCell } from './MergedCells'; import type { RowRoot } from './Row'; import { Row } from './Row'; -import type { DataTableRowProps, DataTableRowType, DTRow, RowPropsInner } from './Row.types'; +import type { DataTableRowType, DTRow, RowPropsInner } from './Row.types'; +import { RowGroup } from './RowGroup'; import style from './style.shadow.css'; import { GRID_ROW_INDEX, IS_EMPTY_DATA_ROW, - ROW_INDEX, SELECT_ALL, + ROW_INDEX, UNIQ_ROW_KEY, } from '../DataTable/DataTable'; import type { DataTableData } from '../DataTable/DataTable.types'; @@ -149,11 +150,6 @@ class BodyRoot extends Component extends Component extends Component - {row.map((item, i) => { - const rowProps: DataTableRowProps = { - row: item, - mergedRow: i > 0 ? true : false, - componentRef: this.handleComponentRef(item), - }; - - if ((isFirstCellAreMergedRows && selectedRows?.includes(groupUniqKey))) { - rowProps.theme = 'info'; - } - - return ( - - ); - })} - , + rows={row} + selectedRows={selectedRows} + columns={columns} + startIndex={this.startIndex} + rowIndex={index} + handleRef={this.handleRef} + handleComponentRef={this.handleComponentRef} + /> ); } diff --git a/semcore/data-table/src/components/Body/Body.types.ts b/semcore/data-table/src/components/Body/Body.types.ts index fdfee88cc8..9d51d4bf37 100644 --- a/semcore/data-table/src/components/Body/Body.types.ts +++ b/semcore/data-table/src/components/Body/Body.types.ts @@ -3,6 +3,7 @@ import type * as React from 'react'; import type { DataTableCellProps, Theme } from './Cell.types'; import type { DTRow, RowPropsInner } from './Row.types'; +import type { ISelectedRows } from '../../store/SelectableRows'; import type { ACCORDION } from '../DataTable/DataTable'; import type { DataRowItem, DTUse, VirtualScroll, DataTableProps, DataTableData } from '../DataTable/DataTable.types'; import type { DTColumn } from '../Head/Column.types'; @@ -73,7 +74,7 @@ export type BodyPropsInner = DataTableB renderCell?: (props: CellRenderProps) => React.ReactNode | Record; onBackFromAccordion: (colName: string) => void; stickyHeader?: boolean; - selectedRows?: UniqKeyType[]; + selectedRows?: UniqKeyType[] | ISelectedRows; onSelectRow?: ( isSelect: boolean, selectedRowIndex: number, diff --git a/semcore/data-table/src/components/Body/LimitOverlay.tsx b/semcore/data-table/src/components/Body/LimitOverlay.tsx index be9f4fd92c..eb4ab068e7 100644 --- a/semcore/data-table/src/components/Body/LimitOverlay.tsx +++ b/semcore/data-table/src/components/Body/LimitOverlay.tsx @@ -120,7 +120,7 @@ class LimitOverlayRoot extends Component e.stopPropagation()} + onClick={(e: React.SyntheticEvent) => e.stopPropagation()} > = { expandedForAnimation: boolean; @@ -50,7 +50,8 @@ export class RowRoot extends Component< } componentDidMount() { - this.asProps.componentRef?.(this); + const { componentRef } = this.asProps; + componentRef?.(this); this.setAccordion(); } @@ -108,20 +109,6 @@ export class RowRoot extends Component< ); } - handleSelectRow = (value: boolean, event?: React.SyntheticEvent) => { - const { row, rowIndex, onSelectRow } = this.asProps; - - onSelectRow?.(value, rowIndex, row, event); - }; - - handleClickCheckbox = (value: boolean) => (event?: React.SyntheticEvent) => { - event?.preventDefault(); - event?.stopPropagation(); - const { row, rowIndex, onSelectRow } = this.asProps; - - onSelectRow?.(value, rowIndex, row, event); - }; - handleBackFromAccordion = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { this.asProps.onBackFromAccordion(this.cellName); @@ -423,7 +410,6 @@ export class RowRoot extends Component< const SRow = Root; const SCollapseRow = Collapse; const SCell = Row.Cell; - const SCheckboxCell = Row.Cell; const { columns, row, @@ -453,6 +439,7 @@ export class RowRoot extends Component< scrollAreaRef, accordionAnimationRows, onCellClick, + onSelectRow, theme, } = this.asProps; @@ -522,38 +509,27 @@ export class RowRoot extends Component< } if (selectedRows && i === 0 && row[IS_EMPTY_DATA_ROW] !== true) { - const checked = selectedRows.includes(rowUniqKey); - const nextColumnName = columns[i + 1].name; - if (!(nextColumnName in row)) { + if (!(nextColumnName in row) || Array.isArray(row)) { return null; } return ( - - - - - + uid={uid} + selectedRows={selectedRows} + onSelectRow={onSelectRow} + /> ); } diff --git a/semcore/data-table/src/components/Body/Row.types.ts b/semcore/data-table/src/components/Body/Row.types.ts index 869dfbdf96..b99675558d 100644 --- a/semcore/data-table/src/components/Body/Row.types.ts +++ b/semcore/data-table/src/components/Body/Row.types.ts @@ -5,6 +5,7 @@ import type { CellRenderProps } from './Body.types'; import type { DataTableCellProps, Theme } from './Cell.types'; import type { MergedColumnsCell, MergedRowsCell } from './MergedCells'; import type { RowRoot } from './Row'; +import type { ISelectedRows } from '../../store/SelectableRows'; import type { ACCORDION, GRID_ROW_INDEX, @@ -68,7 +69,7 @@ export type RowPropsInner = JSX.Intrins gridTemplateAreas: string[]; gridTemplateColumns: string[]; - selectedRows?: UniqKeyType[]; + selectedRows?: UniqKeyType[] | ISelectedRows; onSelectRow?: ( isSelect: boolean, selectedRowIndex: number, diff --git a/semcore/data-table/src/components/Body/RowGroup.tsx b/semcore/data-table/src/components/Body/RowGroup.tsx new file mode 100644 index 0000000000..84ecea5573 --- /dev/null +++ b/semcore/data-table/src/components/Body/RowGroup.tsx @@ -0,0 +1,93 @@ +import { Box } from '@semcore/base-components'; +import { sstyled } from '@semcore/core'; +import * as React from 'react'; + +import style from './style.shadow.css'; +import type { ISelectedRows } from '../../store/SelectableRows'; +import { SelectableRows } from '../../store/SelectableRows'; +import { Body } from '../Body/Body'; +import { MergedRowsCell } from '../Body/MergedCells'; +import type { RowRoot } from '../Body/Row'; +import type { DTRow, DataTableRowProps } from '../Body/Row.types'; +import { SELECT_ALL, UNIQ_ROW_KEY } from '../DataTable/DataTable'; +import type { DataTableData } from '../DataTable/DataTable.types'; +import type { DTColumn } from '../Head/Column.types'; + +type RowGroupProps = { + rows: DTRow[]; + selectedRows?: UniqKeyType[] | ISelectedRows; + columns: DTColumn[]; + startIndex: number; + rowIndex: number; + handleRef: (index: number, row: DTRow) => (node: HTMLElement | null) => void; + handleComponentRef: (row: DTRow) => (component: RowRoot | null) => void; + +}; + +export class RowGroup extends React.PureComponent> { + // private unsubscribeToggle: undefined | (() => void) = undefined; + // + // componentDidMount() { + // const { selectedRows, rows } = this.props; + // + // if (selectedRows && !Array.isArray(selectedRows)) { + // this.unsubscribeToggle = selectedRows.subscribe(SelectableRows.TOGGLE_EVENT, (key: UniqKeyType) => { + // if (rows[0][UNIQ_ROW_KEY] === key) { + // this.forceUpdate(); + // } + // }); + // } + // } + // + // componentWillUnmount() { + // this.unsubscribeToggle?.(); + // } + + render() { + const SRowGroup = Box; + const { rows, selectedRows, columns, startIndex, rowIndex } = this.props; + + const groupUniqKey = rows[0][UNIQ_ROW_KEY]; + + let isFirstCellAreMergedRows = false; + // const theme: 'info' | undefined = undefined; + + if (selectedRows) { + const nextColumnName = columns[1].name; + const firstCell = rows[0][nextColumnName]; + + if (firstCell instanceof MergedRowsCell) { + rows[0][SELECT_ALL.toString()] = new MergedRowsCell('', firstCell.rowsCount); + + isFirstCellAreMergedRows = true; + } + } + + return sstyled(style)( + + {rows.map((item, i) => { + const rowProps: DataTableRowProps = { + row: item, + mergedRow: i > 0 ? true : false, + componentRef: this.props.handleComponentRef(item), + }; + + // if (isFirstCellAreMergedRows && (Array.isArray(selectedRows) ? selectedRows.includes(groupUniqKey) : selectedRows?.isChecked(groupUniqKey))) { + // rowProps.theme = 'info'; + // } + + return ( + + ); + })} + , + ); + } +} diff --git a/semcore/data-table/src/components/Body/style.shadow.css b/semcore/data-table/src/components/Body/style.shadow.css index 7583173075..0037a790fc 100644 --- a/semcore/data-table/src/components/Body/style.shadow.css +++ b/semcore/data-table/src/components/Body/style.shadow.css @@ -103,9 +103,9 @@ SCollapseRow > SCellWrapper > SCell:not([theme]), SRow[isAccordionRow] > SCellWr /* we need a media query here because of the postcssHoverMediaFeature plugin. it doesn't handle this type of selectors correctly */ @media (hover: hover) { - SRow:not([accordionType='row'][expanded]):hover:not([isNonInteractive]):not([aria-hidden]):not(:has(SLimitOverlayCellWrapper:hover)) > SCellWrapper > SCell:not([theme]):not([expanded][withAccordion]):not([aria-hidden]), - SRowGroup:has(SCell[data-grouped-by='rowgroup']:hover) > SRow:not([isNonInteractive]):not([aria-hidden]) > SCellWrapper > SCell:not([theme]):not([expanded][withAccordion]):not([aria-hidden]), - SRowGroup:has(SCell:hover) > SRow:not([isNonInteractive]):not([aria-hidden]) > SCellWrapper > SCell[data-grouped-by='rowgroup']:not([theme]):not([expanded][withAccordion]):not([aria-hidden]) { + :not(SRowGroup:has([data-row-selector="true"] input:checked)) > SRow:not([accordionType='row'][expanded]):hover:not([isNonInteractive]):not(:has([data-row-selector="true"] input:checked)):not([aria-hidden]):not(:has(SLimitOverlayCellWrapper:hover)) > SCellWrapper > SCell:not([theme]):not([expanded][withAccordion]):not([aria-hidden]), + SRowGroup:has(SCell[data-grouped-by='rowgroup']:hover):not(:has([data-row-selector="true"] input:checked)) > SRow:not([isNonInteractive]):not([aria-hidden]) > SCellWrapper > SCell:not([theme]):not([expanded][withAccordion]):not([aria-hidden]), + SRowGroup:has(SCell:hover):not(:has([data-row-selector="true"] input:checked)) > SRow:not([isNonInteractive]):not([aria-hidden]) > SCellWrapper > SCell[data-grouped-by='rowgroup']:not([theme]):not([expanded][withAccordion]):not([aria-hidden]) { background-color: var(--intergalactic-table-td-cell-hover, #f0f0f4); } @@ -121,9 +121,13 @@ SCollapseRow > SCellWrapper > SCell:not([theme]), SRow[isAccordionRow] > SCellWr /* INFO THEME */ SRow:hover:not([isNonInteractive]):not([aria-hidden]):not(:has(SLimitOverlayCellWrapper:hover)) > SCellWrapper > SCell[theme='info']:not([aria-hidden]), + SRow:hover:has([data-row-selector="true"] input:checked):not([isNonInteractive]):not([aria-hidden]):not(:has(SLimitOverlayCellWrapper:hover)) > SCellWrapper > SCell:not([aria-hidden]), SRow[theme='info']:hover:not([expanded][isNonInteractive]):not([aria-hidden]):not(:has(SLimitOverlayCellWrapper:hover)) > SCellWrapper > SCell:not([theme]):not([expanded][withAccordion]):not([aria-hidden]), SRowGroup:has(SCell[data-grouped-by='rowgroup']:hover) > SRow:not([isNonInteractive]):not([aria-hidden]) > SCellWrapper > SCell[theme='info']:not([aria-hidden]), + SRowGroup:has(SCell[data-grouped-by='rowgroup']:hover):has([data-row-selector="true"] input:checked) SRow:not([isNonInteractive]):not([aria-hidden]) > SCellWrapper > SCell:not([aria-hidden]), SRowGroup:has(SCell[data-grouped-by='rowgroup']:hover) > SRow[theme='info']:not([isNonInteractive]):not([aria-hidden]) > SCellWrapper > SCell:not([theme]):not([aria-hidden]), + SRowGroup:has(SCell:hover):has([data-row-selector="true"] input:checked) > SRow:not([isNonInteractive]):not([aria-hidden]) > SCellWrapper > SCell[data-grouped-by='rowgroup']:not([aria-hidden]), + SRowGroup:has([data-row-selector="true"] input:checked) > SRow:hover:not([isNonInteractive]):not([aria-hidden]) > SCellWrapper > SCell:not([aria-hidden]), SRowGroup:has(SCell:hover) > SRow:not([isNonInteractive]):not([aria-hidden]) > SCellWrapper > SCell[data-grouped-by='rowgroup'][theme='info']:not([aria-hidden]), SRowGroup:has(SCell:hover) > SRow[theme='info']:not([isNonInteractive]):not([aria-hidden]) > SCellWrapper > SCell[data-grouped-by='rowgroup']:not([theme]):not([aria-hidden]) { background-color: var(--intergalactic-table-td-cell-selected-hover, #c4e5fe); @@ -177,7 +181,7 @@ SRow[theme='muted'][active] > SCellWrapper > SCell:not([theme]) { /* INFO THEME */ -SRow[theme='info'] SCell:not([theme]) { +SRow[theme='info'] SCell:not([theme]), SRow:has([data-row-selector="true"] input:checked) SCell:not([theme]) { &:not([expanded][withAccordion]) { background-color: var(--intergalactic-table-td-cell-selected, #e9f7ff); } @@ -235,6 +239,11 @@ SRow[theme='danger'][active] > SCellWrapper > SCell:not([theme]) { background-color: var(--intergalactic-table-td-cell-critical-active, #ffd7df); } +SRowGroup:has([data-row-selector="true"] input:checked) > SRow > SCellWrapper > SCell, +SRow:has([data-row-selector="true"] input:checked) > SCellWrapper > SCell { + background-color: var(--intergalactic-table-td-cell-selected, #e9f7ff); +} + SCell { display: flex; height: 100%; diff --git a/semcore/data-table/src/components/DataTable/DataTable.tsx b/semcore/data-table/src/components/DataTable/DataTable.tsx index 365ec8eaad..b926f56620 100644 --- a/semcore/data-table/src/components/DataTable/DataTable.tsx +++ b/semcore/data-table/src/components/DataTable/DataTable.tsx @@ -23,6 +23,8 @@ import type { ColumnItemConfig, DataRowItem, } from './DataTable.types'; +import type { ISelectedRows } from '../../store/SelectableRows'; +import { SelectableRows } from '../../store/SelectableRows'; import scrollStyles from '../../style/scroll-shadows.shadow.css'; import { localizedMessages } from '../../translations/__intergalactic-dynamic-locales'; import { Body } from '../Body/Body'; @@ -127,7 +129,7 @@ class DataTableRoot< private headerNodesMap = new Map(); - private isPressedShift = false; + private selectedRowsContainer: ISelectedRows; private lastSelectedRowKey: UniqKeyType | undefined; constructor(props: DataTableProps) { @@ -140,6 +142,12 @@ class DataTableRoot< this.calculatedRows = this.getRows(); this.flatRows = this.calculatedRows.flat(); this.tmpData = props.data; + + if (Array.isArray(props.selectedRows) || !props.selectedRows) { + this.selectedRowsContainer = new SelectableRows(); + } else { + this.selectedRowsContainer = props.selectedRows; + } } state: State = { @@ -178,7 +186,7 @@ class DataTableRoot< this.calculateVerticalShadow(); } } - if (prevProps.selectedRows !== selectedRows && selectedRows !== undefined) { + if (prevProps.selectedRows !== selectedRows && selectedRows !== undefined && Array.isArray(selectedRows)) { const selectedRowsSet = new Set(selectedRows); const allChecked: UniqKeyType[] = []; @@ -198,6 +206,9 @@ class DataTableRoot< this.setSelectAllMessage(false); } } + if (prevProps.selectedRows !== selectedRows && selectedRows !== undefined && !Array.isArray(selectedRows)) { + this.selectedRowsContainer = selectedRows; + } } componentWillUnmount() { @@ -276,7 +287,6 @@ class DataTableRoot< getI18nText, uid, headerProps, - onSelectedRowsChange, selectedRows, sideIndents, variant, @@ -305,7 +315,7 @@ class DataTableRoot< totalRows: this.totalRows, selectedRows, flatRows: this.getFlatRows(), - onChangeSelectAll: onSelectedRowsChange, + onChangeSelectAll: Array.isArray(selectedRows) ? this.handleSelectAllRows : undefined, getFixedStyle: this.getFixedStyle, onCellClick: this.handleCellClick, shadowVertical, @@ -372,7 +382,7 @@ class DataTableRoot< renderEmptyData, sideIndents, selectedRows, - onSelectRow: this.handleSelectRow, + onSelectRow: Array.isArray(selectedRows) ? this.handleSelectRow : undefined, getFixedStyle: this.getFixedStyle, onCellClick: this.handleCellClick, rawData, @@ -427,19 +437,27 @@ class DataTableRoot< } }; + handleSelectAllRows = (selectedRows: UniqKeyType[], event?: React.SyntheticEvent) => { + if (!('onSelectedRowsChange' in this.asProps) || !this.asProps.onSelectedRowsChange || !Array.isArray(selectedRows)) return; + + this.asProps.onSelectedRowsChange(selectedRows, event); + }; + handleSelectRow = ( isSelected: boolean, selectedRowIndex: number, row: DTRow, event?: React.SyntheticEvent, ) => { - const { selectedRows, onSelectedRowsChange } = this.asProps; + const { selectedRows } = this.asProps; - if (!selectedRows || !onSelectedRowsChange) return; + if (!selectedRows || !('onSelectedRowsChange' in this.asProps) || !this.asProps.onSelectedRowsChange || !Array.isArray(selectedRows)) return; + + const { onSelectedRowsChange } = this.asProps; const selectedRowsSet = new Set(selectedRows); - if (this.isPressedShift && selectedRowsSet.size > 0 && this.lastSelectedRowKey && (isSelected ? selectedRowsSet.has(this.lastSelectedRowKey) : true)) { + if (this.selectedRowsContainer.isPressedShift && selectedRowsSet.size > 0 && this.lastSelectedRowKey && (isSelected ? selectedRowsSet.has(this.lastSelectedRowKey) : true)) { let select = false; const firstColumnKey = this.columns[0].name; const isMerged = this.flatRows.some((item) => item[firstColumnKey] instanceof MergedRowsCell); @@ -710,14 +728,14 @@ class DataTableRoot< break; } case 'Shift': { - this.isPressedShift = true; + this.selectedRowsContainer.isPressedShift = true; } } }; handleKeyUp = (e: React.KeyboardEvent) => { if (e.key === 'Shift') { - this.isPressedShift = false; + this.selectedRowsContainer.isPressedShift = false; } }; @@ -1243,7 +1261,7 @@ class DataTableRoot< private getRows(): Array[] | DTRow> { const columns = this.columns; // @ts-ignore - const { data, uid, uniqueRowKey } = this.props; + const { data, uid, uniqueRowKey, selectedRows } = this.props; if (this.tmpData === data) { return this.calculatedRows; @@ -1251,6 +1269,8 @@ class DataTableRoot< this.tmpData = data; + const availableRowKeys: UniqKeyType[] = []; + const rows: Array[] | DTRow> = []; const columnNames = columns.map((column: DTColumn) => column.name); @@ -1340,6 +1360,10 @@ class DataTableRoot< }); } + if (!excludeColumns) { // we should add only the main row in mergedRows or default rows + availableRowKeys.push(dtRow[UNIQ_ROW_KEY]); + } + return dtRow; }; @@ -1414,6 +1438,11 @@ class DataTableRoot< }); this.calculatedRows = rows; + + if (selectedRows && !Array.isArray(selectedRows)) { + selectedRows.setAvailableKeys(availableRowKeys); + } + return rows; } diff --git a/semcore/data-table/src/components/DataTable/DataTable.types.ts b/semcore/data-table/src/components/DataTable/DataTable.types.ts index d77e2c9b3d..acd32349f8 100644 --- a/semcore/data-table/src/components/DataTable/DataTable.types.ts +++ b/semcore/data-table/src/components/DataTable/DataTable.types.ts @@ -4,6 +4,7 @@ import type Tooltip from '@semcore/tooltip'; import type * as React from 'react'; import type { ACCORDION, ROW_GROUP, UNIQ_ROW_KEY } from './DataTable'; +import type { ISelectedRows } from '../../store/SelectableRows'; import type { DataTableBodyProps } from '../Body/Body.types'; import type { DTRow, RowPropsInner } from '../Body/Row.types'; import type { DataTableColumnProps } from '../Head/Column.types'; @@ -41,7 +42,7 @@ export type DataTableData = DataRowItem[]; export type DTUse = 'primary' | 'secondary'; -export type Sizes = Pick; +export type Sizes = Partial>; export type DataTableProps< Data extends DataTableData, @@ -115,22 +116,6 @@ export type DataTableProps< */ uniqueRowKey?: UniqKey; - /** - * List of selected rows (uniqIds from a data array) - */ - selectedRows?: UniqKeyType[]; - - /** Callback when row selection changes */ - onSelectedRowsChange?: ( - selectedRows: UniqKeyType[], - event?: React.SyntheticEvent, - opts?: { - selectedRowIndex: number; - isSelected: boolean; - row: DTRow; - } - ) => void; - /** * For custom empty data widget. */ @@ -196,7 +181,30 @@ export type DataTableProps< * Handling table container resizing. */ onResize?: ResizeObserverCallback; - }; + } & ({ + /** + * List of selected rows (uniqIds from a data array) + * @deprecated use ISelectedRows for this property instead of an array. + */ + selectedRows?: UniqKeyType[]; + + /** Callback when row selection changes */ + onSelectedRowsChange?: ( + selectedRows: UniqKeyType[], + event?: React.SyntheticEvent, + opts?: { + selectedRowIndex: number; + isSelected: boolean; + row: DTRow; + } + ) => void; + } | { + /** + * Entity of selected rows (uniq id from them) + * This is mutable! variable because of table performance. Don't change the link on it. + */ + selectedRows?: ISelectedRows; + }); export type ColumnItemConfig = Intergalactic.InternalTypings.EfficientOmit< Intergalactic.InternalTypings.ComponentProps< diff --git a/semcore/data-table/src/components/Head/Head.tsx b/semcore/data-table/src/components/Head/Head.tsx index e21119c332..82440a68b5 100644 --- a/semcore/data-table/src/components/Head/Head.tsx +++ b/semcore/data-table/src/components/Head/Head.tsx @@ -10,6 +10,7 @@ import { Group } from './Group'; import type { DataTableGroupProps } from './Group.type'; import type { DataTableHeadProps, HeadPropsInner } from './Head.types'; import style from './style.shadow.css'; +import { SelectableRows } from '../../store/SelectableRows'; import type { DTRow } from '../Body/Row.types'; import { DataTable, type ROW_GROUP, SELECT_ALL, UNIQ_ROW_KEY } from '../DataTable/DataTable'; import type { DataTableData } from '../DataTable/DataTable.types'; @@ -28,6 +29,22 @@ class HeadRoot< static displayName = 'Head'; static style = style; + private unsubscribeSelectAll: undefined | (() => void) = undefined; + + componentDidMount() { + const { selectedRows } = this.asProps; + + if (selectedRows && !Array.isArray(selectedRows)) { + this.unsubscribeSelectAll = selectedRows.subscribe(SelectableRows.SELECT_ALL_EVENT, () => { + this.forceUpdate(); + }); + } + } + + componentWillUnmount() { + this.unsubscribeSelectAll?.(); + } + sortableColumnDescribeId() { const { uid } = this.asProps; return `${uid}-column-sortable-describer`; @@ -129,20 +146,29 @@ class HeadRoot< } handleSelectAll = (value: boolean, event?: React.SyntheticEvent) => { - const { selectedRows = [] } = this.asProps; - const idsSet = new Set(selectedRows); + const { selectedRows } = this.asProps; - if (value) { - this.selectableRows.forEach((row) => { - idsSet.add(row[UNIQ_ROW_KEY]); - }); - } else { - this.selectableRows.forEach((row) => { - idsSet.delete(row[UNIQ_ROW_KEY]); - }); - } + if (Array.isArray(selectedRows)) { + const idsSet = new Set(selectedRows); + + if (value) { + this.selectableRows.forEach((row) => { + idsSet.add(row[UNIQ_ROW_KEY]); + }); + } else { + this.selectableRows.forEach((row) => { + idsSet.delete(row[UNIQ_ROW_KEY]); + }); + } - this.asProps.onChangeSelectAll?.(Array.from(idsSet), event); + this.asProps.onChangeSelectAll?.(Array.from(idsSet), event); + } else if (selectedRows) { + if (value) { + selectedRows.selectAll(); + } else { + selectedRows.clearAllAvailable(); + } + } }; handleClickSelectAll = (value: boolean) => (event?: React.SyntheticEvent) => { @@ -153,15 +179,23 @@ class HeadRoot< }; get areAllRowsSelected() { - const { selectedRows = [] } = this.asProps; + const { selectedRows } = this.asProps; - return selectedRows.length > 0 && this.selectableRows.every((row) => selectedRows?.includes(row[UNIQ_ROW_KEY])); + if (Array.isArray(selectedRows)) { + return selectedRows.length > 0 && this.selectableRows.every((row) => selectedRows?.includes(row[UNIQ_ROW_KEY])); + } else if (selectedRows) { + return selectedRows.isAllSelected(); + } } get isIndeterminate() { const { selectedRows } = this.asProps; - return this.selectableRows.some((row) => selectedRows?.includes(row[UNIQ_ROW_KEY])); + if (Array.isArray(selectedRows)) { + return this.selectableRows.some((row) => selectedRows?.includes(row[UNIQ_ROW_KEY])); + } else if (selectedRows) { + return selectedRows.isIndeterminate(); + } } get selectableRows(): DTRow[] { diff --git a/semcore/data-table/src/components/Head/Head.types.ts b/semcore/data-table/src/components/Head/Head.types.ts index 91e6e1adf7..3885f1fec0 100644 --- a/semcore/data-table/src/components/Head/Head.types.ts +++ b/semcore/data-table/src/components/Head/Head.types.ts @@ -1,4 +1,5 @@ import type { ColumnPropsInner, DTColumn } from './Column.types'; +import type { ISelectedRows } from '../../store/SelectableRows'; import type { BodyPropsInner } from '../Body/Body.types'; import type { DataTableCellProps } from '../Body/Cell.types'; import type { DTRow } from '../Body/Row.types'; @@ -54,7 +55,7 @@ export type HeadPropsInner< sideIndents?: 'wide'; totalRows: number; - selectedRows?: UniqKeyType[]; + selectedRows?: UniqKeyType[] | ISelectedRows; onChangeSelectAll?: (selectedRows: UniqKeyType[], event?: React.SyntheticEvent) => void; flatRows: DTRow[]; diff --git a/semcore/data-table/src/components/RowSelector/RowsSelector.tsx b/semcore/data-table/src/components/RowSelector/RowsSelector.tsx new file mode 100644 index 0000000000..63e4fb768e --- /dev/null +++ b/semcore/data-table/src/components/RowSelector/RowsSelector.tsx @@ -0,0 +1,121 @@ +import Checkbox from '@semcore/checkbox'; +import React from 'react'; + +import type { ISelectedRows } from '../../store/SelectableRows'; +import { SelectableRows } from '../../store/SelectableRows'; +import type { Theme } from '../Body/Cell.types'; +import { Row } from '../Body/Row'; +import type { DTRow } from '../Body/Row.types'; +import { UNIQ_ROW_KEY, SELECT_ALL } from '../DataTable/DataTable'; + +type RowSelectorProps = { + row: DTRow; + rowIndex: number; + gridRowIndex: number; + expanded: boolean; + withAccordion: boolean; + + uid: string; + theme?: Theme; + isCellHidden?: boolean; + isAccordionRow?: boolean; + selectedRows?: UniqKeyType[] | ISelectedRows; + onSelectRow?: ( + isSelect: boolean, + selectedRowIndex: number, + row: DTRow, + event?: React.SyntheticEvent, + ) => void; +}; + +type State = { + checked: boolean; +}; + +export class RowSelector extends React.PureComponent, State> { + state: State = { + checked: false, + }; + + private unsubscribeToggle: undefined | (() => void) = undefined; + + constructor(props: RowSelectorProps) { + super(props); + + const { row, selectedRows } = props; + if (selectedRows && !Array.isArray(selectedRows)) { + this.state.checked = selectedRows.has(row[UNIQ_ROW_KEY]); + } + } + + componentDidMount(): void { + const { row, selectedRows } = this.props; + if (selectedRows && !Array.isArray(selectedRows)) { + this.unsubscribeToggle = selectedRows.subscribe(SelectableRows.TOGGLE_EVENT, (key: UniqKeyType) => { + if (row[UNIQ_ROW_KEY] === key) { + this.setState({ checked: selectedRows.has(row[UNIQ_ROW_KEY]) }); + } + }); + } + } + + componentWillUnmount(): void { + this.unsubscribeToggle?.(); + } + + handleSelectRow = (value: boolean, event?: React.SyntheticEvent) => { + const { row, rowIndex, onSelectRow, selectedRows } = this.props; + + onSelectRow?.(value, rowIndex, row, event); + + if (selectedRows && !Array.isArray(selectedRows)) { + selectedRows.toggle(value, row); + } + }; + + handleClickCheckbox = (value: boolean) => (event?: React.SyntheticEvent) => { + event?.preventDefault(); + event?.stopPropagation(); + const { row, rowIndex, onSelectRow, selectedRows } = this.props; + + onSelectRow?.(value, rowIndex, row, event); + + if (selectedRows && !Array.isArray(selectedRows)) { + selectedRows.toggle(value, row); + } + }; + + render() { + const SCheckboxCell = Row.Cell; + + const { row, rowIndex, gridRowIndex, expanded, withAccordion, isAccordionRow, isCellHidden, theme, uid, selectedRows } = this.props; + const rowUniqKey = row[UNIQ_ROW_KEY]; + + const checked = Array.isArray(selectedRows) ? selectedRows.includes(rowUniqKey) : this.state.checked; + + return ( + + + + + + ); + } +} diff --git a/semcore/data-table/src/index.ts b/semcore/data-table/src/index.ts index 39ff2750a3..aba62e0f75 100644 --- a/semcore/data-table/src/index.ts +++ b/semcore/data-table/src/index.ts @@ -13,6 +13,7 @@ import type { ColumnGroupConfig, ColumnItemConfig, } from './components/DataTable/DataTable.types'; +import { SelectableRows } from './store/SelectableRows'; const wrapDataTable = ( wrapper: ( @@ -34,6 +35,7 @@ export { */ UNIQ_ROW_KEY, wrapDataTable, + SelectableRows, }; export type { DataTableSort, diff --git a/semcore/data-table/src/store/SelectableRows.ts b/semcore/data-table/src/store/SelectableRows.ts new file mode 100644 index 0000000000..202db5281d --- /dev/null +++ b/semcore/data-table/src/store/SelectableRows.ts @@ -0,0 +1,199 @@ +import EventEmitter from '@semcore/core/lib/utils/eventEmitter'; + +import type { DTRow } from '../components/Body/Row.types'; +import { UNIQ_ROW_KEY } from '../components/DataTable/DataTable'; + +export interface ISelectedRows { + /** Flag for multiple rows selection */ + isPressedShift: boolean; + /** Method for set keys from data. Call it in DataTable, on data changes */ + setAvailableKeys(keys: UniqKeyType[]): void; + + /** Get the list of keys */ + get(): UniqKeyType[]; + + /** Check if the row is selected */ + isChecked(key: UniqKeyType): boolean; + + /** Replace the list of keys. */ + replace(value: UniqKeyType[]): void; + + /** Check if the key exists in selected rows */ + has(value: UniqKeyType): boolean; + + /** Select all handler */ + selectAll(): void; + + /** Clear all handler */ + clearAll(): void; + + /** Clear all available values (rows on current page) handler */ + clearAllAvailable(): void; + + /** Toggle selection of row */ + toggle(selected: boolean, row: DTRow): void; + + /** Check if all rows selected */ + isAllSelected(): boolean; + + /** Check if at least one row selected */ + isIndeterminate(): boolean; + + /** Subscribe to changes */ + subscribe: EventEmitter['subscribe']; +} + +export class SelectableRows extends EventEmitter implements ISelectedRows { + private readonly values: Set; + + private availableKeys = new Set(); + + private lastSelectedRow: UniqRowKeyType | null = null; + + public static TOGGLE_EVENT = 'toggle_selected_row'; + public static SELECT_ALL_EVENT = 'select_all_selected_rows'; + + public isPressedShift: boolean = false; + + constructor(initValues: UniqRowKeyType[] = []) { + super(); + + this.values = new Set(initValues); + } + + public setAvailableKeys(value: UniqRowKeyType[]): void { + this.availableKeys = new Set(value); + } + + public get() { + return Array.from(this.values.keys()); + } + + public isChecked(key: UniqRowKeyType): boolean { + return this.values.has(key); + } + + public replace(value: UniqRowKeyType[]): void { + this.clearAll(); + + value.forEach((val) => { + this.values.add(val); + + this.emit(SelectableRows.TOGGLE_EVENT, val); + }); + } + + public has(value: UniqRowKeyType): boolean { + return this.values.has(value); + } + + public isAllSelected(): boolean { + let isAllSelected = true; + + if (this.availableKeys.size === 0 || this.values.size === 0) { + return false; + } + + for (const key of this.availableKeys.values()) { + if (!this.values.has(key)) { + isAllSelected = false; + break; + } + } + + return isAllSelected; + } + + public isIndeterminate(): boolean { + let isIndeterminate = false; + + for (const key of this.availableKeys.values()) { + if (this.values.has(key)) { + isIndeterminate = true; + break; + } + } + + return isIndeterminate; + } + + public selectAll(): void { + for (const key of this.availableKeys.values()) { + this.values.add(key); + this.emit(SelectableRows.TOGGLE_EVENT, key); + } + + this.emit(SelectableRows.SELECT_ALL_EVENT); + } + + public clearAll(): void { + const keys = Array.from(this.values.values()); + this.values.clear(); + for (const key of keys) { + this.emit(SelectableRows.TOGGLE_EVENT, key); + } + + this.emit(SelectableRows.SELECT_ALL_EVENT); + } + + public clearAllAvailable(): void { + for (const key of this.availableKeys.values()) { + this.values.delete(key); + this.emit(SelectableRows.TOGGLE_EVENT, key); + } + + this.emit(SelectableRows.SELECT_ALL_EVENT); + } + + public toggle(selected: boolean, row: DTRow): void { + if (this.isPressedShift && this.values.size > 0 && this.lastSelectedRow && (selected ? this.values.has(this.lastSelectedRow) : true)) { + let select = false; + + for (const item of this.availableKeys.values()) { + if (!select && (item === row[UNIQ_ROW_KEY] || item === this.lastSelectedRow)) { + select = true; + this.toggleOneRow(selected, item); + continue; + } + + if (select) { + this.toggleOneRow(selected, item); + } + + if (select && (item === row[UNIQ_ROW_KEY] || item === this.lastSelectedRow)) { + break; + } + } + } else { + this.toggleOneRow(selected, row[UNIQ_ROW_KEY]); + } + + this.lastSelectedRow = row[UNIQ_ROW_KEY]; + } + + private toggleOneRow(isSelected: boolean, key: UniqRowKeyType): void { + if (isSelected) { + if (this.values.size === 0) { + this.emit(SelectableRows.SELECT_ALL_EVENT); + } + + this.values.add(key); + + if (this.values.size === this.availableKeys.size) { + this.emit(SelectableRows.SELECT_ALL_EVENT); + } + } else { + if (this.values.size === this.availableKeys.size) { + this.emit(SelectableRows.SELECT_ALL_EVENT); + } + + this.values.delete(key); + + if (this.values.size === 0) { + this.emit(SelectableRows.SELECT_ALL_EVENT); + } + } + + this.emit(SelectableRows.TOGGLE_EVENT, key); + } +} diff --git a/stories/components/data-table/advanced/data-table.advanced.stories.tsx b/stories/components/data-table/advanced/data-table.advanced.stories.tsx index 17fdbdccd2..c855b47fad 100644 --- a/stories/components/data-table/advanced/data-table.advanced.stories.tsx +++ b/stories/components/data-table/advanced/data-table.advanced.stories.tsx @@ -1,5 +1,5 @@ import { DataTable } from '@semcore/ui/data-table'; -import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { Meta, StoryObj, ArgTypes } from '@storybook/react-vite'; import AccordionInMergedRowsExample, { accordionMergedProps } from './examples/accordion_in_merged_rows'; import type { AccordionWithCheckboxProps } from './examples/accordion_with_checkbox'; @@ -9,6 +9,8 @@ import AccordionWithPaginationExample, { tableInTableDefaultProps } from './exam import type { TableInTableProps } from './examples/accordion_with_pagination'; import AccordionWithStickyRowsExample, { accordionStickyProps } from './examples/accordion_with_sticky_rows'; import BigTableWithStickyHeaderExample from './examples/big_table_with_sticky_header'; +import CheckboxInBigTableExample, { defaultProps as checkboxInBigTableDefaultProps } from './examples/checkbox_in_big_table'; +import CheckboxInBigTableReactiveExample, { defaultProps as checkboxInBigReactiveTableDefaultProps } from './examples/checkbox_in_big_table_reactive'; import FakeMultiLineHeaderExample from './examples/fake-multi-level-header'; import FixedColumnsWidthWithShadowsExample from './examples/fixed_columns_width_with_shadows'; import FixedColumnsWithDiffWidthsExample from './examples/fixed_columns_with_diff_widths'; @@ -28,6 +30,20 @@ const meta: Meta = { component: DataTable, }; +const checkboxArgTypes: Partial> = { + loading: { control: 'boolean' }, + sideIndents: { + control: 'select', + options: [undefined, 'wide'], + defaultValue: undefined, + }, + compact: { + control: 'radio', + options: [undefined, true, false], + defaultValue: undefined, + }, +}; + export default meta; type Story = StoryObj; @@ -72,6 +88,18 @@ export const BigTableWithStickyHeader: Story = { render: BigTableWithStickyHeaderExample, }; +export const CheckboxInBigTable: StoryObj = { + render: CheckboxInBigTableExample, + argTypes: checkboxArgTypes, + args: checkboxInBigTableDefaultProps, +}; + +export const CheckboxInBigTableReactive: StoryObj = { + render: CheckboxInBigTableReactiveExample, + argTypes: checkboxArgTypes, + args: checkboxInBigReactiveTableDefaultProps, +}; + export const FixedColumnsWithDiffWidths: Story = { render: FixedColumnsWithDiffWidthsExample, }; diff --git a/stories/components/data-table/advanced/examples/checkbox_in_big_table/index.tsx b/stories/components/data-table/advanced/examples/checkbox_in_big_table/index.tsx new file mode 100644 index 0000000000..bccb92b6a0 --- /dev/null +++ b/stories/components/data-table/advanced/examples/checkbox_in_big_table/index.tsx @@ -0,0 +1,101 @@ +import { + Box, + Flex, + ScreenReaderOnly, +} from '@semcore/ui/base-components'; +import Button from '@semcore/ui/button'; +import { Text } from '@semcore/ui/typography'; +import React from 'react'; + +import { Table } from './table'; + +type CheckboxExampleProps = { + loading: boolean; + sideIndents?: 'wide'; + compact?: boolean; +}; + +const Demo = (props: CheckboxExampleProps) => { + const [selectedRows, setSelectedRows] = React.useState([]); + const [selectedRowsDisplay, setSelectedRowsDisplay] = React.useState(0); + const [ariaMessage, setAriaMessage] = React.useState(''); + const tableRef = React.useRef(null); + + const handleChangeSelectedRows = React.useCallback((value: string[]) => { + setSelectedRows(value); + + if (!selectedRows.length) + setAriaMessage('Action bar appeared before the table'); + setSelectedRowsDisplay(value.length); + }, []); + + const handleDeselectAll = () => { + setSelectedRows([]); + tableRef.current?.focus(); + }; + + React.useEffect(() => { + const timer = setTimeout(() => setAriaMessage(''), 300); + return () => clearTimeout(timer); + }, [ariaMessage]); + + return ( + <> + + + {ariaMessage} + + + + Selected rows: {selectedRowsDisplay} + + + + + + + ); +}; + +export const defaultProps: CheckboxExampleProps = { + loading: false, + sideIndents: undefined, + compact: undefined, +}; + +Demo.defaultProps = defaultProps; + +export default Demo; diff --git a/stories/components/data-table/advanced/examples/checkbox_in_big_table/table.tsx b/stories/components/data-table/advanced/examples/checkbox_in_big_table/table.tsx new file mode 100644 index 0000000000..ad22d4ee35 --- /dev/null +++ b/stories/components/data-table/advanced/examples/checkbox_in_big_table/table.tsx @@ -0,0 +1,1757 @@ +import { DataTable } from '@semcore/ui/data-table'; +import React from 'react'; + +const columns = [ + { name: 'keyword', children: 'Keyword' }, + { name: 'intent', children: 'Intent' }, + { name: 'previousPosition', children: 'Intent' }, + { name: 'position', children: 'Intent' }, + { name: 'traffic', children: 'Traffic' }, + { name: 'trafficPercent', children: 'Traffic' }, + { name: 'vol', children: 'Vol.' }, + { name: 'kd', children: 'KD %' }, + { name: 'cpc', children: 'CPC' }, + { name: 'url', children: 'CPC' }, + { name: 'cost', children: 'Comp.' }, + { name: 'comp', children: 'Comp.' }, + { name: 'results', children: 'Results' }, + { name: 'crawledTime', children: 'Updated' }, +]; + +type TableProps = { + selectedRows: string[]; + handleChangeSelectedRows: (value: string[]) => void; + tableRef: React.Ref; + sideIndents?: 'wide'; + loading: boolean; + compact?: boolean; +}; + +const headerProps = { + sticky: true, + top: 44, +}; + +export const Table = (props: TableProps) => { + return ( + <> + + + ); +}; + +const data = [ + { + id: '1', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '2', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '3', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '4', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '5', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '6', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '7', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '8', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '9', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '10', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '11', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '12', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '13', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '14', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '15', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '16', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '17', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '18', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '19', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '20', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '21', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '22', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '23', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '24', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '25', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '26', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '27', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '28', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '29', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '30', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '31', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '32', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '33', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '34', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '35', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '36', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '37', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '38', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '39', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '40', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '41', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '42', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '43', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '44', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '45', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '46', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '47', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '48', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '49', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '50', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '51', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '52', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '53', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '54', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '55', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '56', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '57', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '58', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '59', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '60', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '61', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '62', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '63', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '64', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '65', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '66', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '67', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '68', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '69', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '70', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '71', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '72', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '73', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '74', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '75', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '76', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '77', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '78', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '79', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '80', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '81', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '82', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '83', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '84', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '85', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '86', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '87', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '88', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '89', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '90', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '91', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '92', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '93', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '94', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '95', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '96', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '97', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '98', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '99', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '100', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, +]; diff --git a/stories/components/data-table/advanced/examples/checkbox_in_big_table_reactive/index.tsx b/stories/components/data-table/advanced/examples/checkbox_in_big_table_reactive/index.tsx new file mode 100644 index 0000000000..05c9805c08 --- /dev/null +++ b/stories/components/data-table/advanced/examples/checkbox_in_big_table_reactive/index.tsx @@ -0,0 +1,123 @@ +import { + Box, + Flex, + ScreenReaderOnly, +} from '@semcore/ui/base-components'; +import Button from '@semcore/ui/button'; +import { SelectableRows } from '@semcore/ui/data-table'; +import { Text } from '@semcore/ui/typography'; +import React from 'react'; + +import { Table } from './table'; + +type CheckboxExampleProps = { + loading: boolean; + sideIndents?: 'wide'; + compact?: boolean; +}; + +const selectedRows = new SelectableRows(); + +const Demo = (props: CheckboxExampleProps) => { + const [selectedRowsDisplay, setSelectedRowsDisplay] = React.useState(0); + const [ariaMessage, setAriaMessage] = React.useState(''); + const tableRef = React.useRef(null); + + const handleDeselectAll = () => { + selectedRows.clearAll(); + tableRef.current?.focus(); + }; + + React.useEffect(() => { + const unsubscribe = selectedRows.subscribe(SelectableRows.TOGGLE_EVENT, () => { + const selectedRowsSize = selectedRows.get().length; + if (selectedRowsSize > 0) { + setAriaMessage('Action bar appeared before the table'); + } + + setSelectedRowsDisplay(selectedRowsSize); + }); + + return unsubscribe; + }, []); + + React.useEffect(() => { + const unsubscribe = selectedRows.subscribe(SelectableRows.SELECT_ALL_EVENT, () => { + const selectedRowsSize = selectedRows.get().length; + if (selectedRowsSize > 0) { + setAriaMessage('Action bar appeared before the table'); + } + + setSelectedRowsDisplay(selectedRowsSize); + }); + + return unsubscribe; + }, []); + + React.useEffect(() => { + const timer = setTimeout(() => setAriaMessage(''), 300); + return () => clearTimeout(timer); + }, [ariaMessage]); + + return ( + <> + + + {ariaMessage} + + + + Selected rows: {selectedRowsDisplay} + + {selectedRowsDisplay > 0 && ( + + )} + +
+ + + ); +}; + +export const defaultProps: CheckboxExampleProps = { + loading: false, + sideIndents: undefined, + compact: undefined, +}; + +Demo.defaultProps = defaultProps; + +export default Demo; diff --git a/stories/components/data-table/advanced/examples/checkbox_in_big_table_reactive/table.tsx b/stories/components/data-table/advanced/examples/checkbox_in_big_table_reactive/table.tsx new file mode 100644 index 0000000000..1946a98188 --- /dev/null +++ b/stories/components/data-table/advanced/examples/checkbox_in_big_table_reactive/table.tsx @@ -0,0 +1,1758 @@ +import type { SelectableRows } from '@semcore/ui/data-table'; +import { DataTable } from '@semcore/ui/data-table'; +import React from 'react'; + +const columns = [ + { name: 'keyword', children: 'Keyword' }, + { name: 'intent', children: 'Intent' }, + { name: 'previousPosition', children: 'Intent' }, + { name: 'position', children: 'Intent' }, + { name: 'traffic', children: 'Traffic' }, + { name: 'trafficPercent', children: 'Traffic' }, + { name: 'vol', children: 'Vol.' }, + { name: 'kd', children: 'KD %' }, + { name: 'cpc', children: 'CPC' }, + { name: 'url', children: 'CPC' }, + { name: 'cost', children: 'Comp.' }, + { name: 'comp', children: 'Comp.' }, + { name: 'results', children: 'Results' }, + { name: 'crawledTime', children: 'Updated' }, +]; + +type TableProps = { + selectedRows: SelectableRows; + tableRef: React.Ref; + + sideIndents?: 'wide'; + loading: boolean; + compact?: boolean; +}; + +const headerProps = { + sticky: true, + top: 44, +}; + +export const Table = (props: TableProps) => { + return ( + <> + + + ); +}; + +const data = [ + { + id: '1', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '2', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '3', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '4', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '5', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '6', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '7', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '8', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '9', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '10', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '11', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '12', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '13', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '14', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '15', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '16', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '17', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '18', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '19', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '20', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '21', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '22', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '23', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '24', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '25', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '26', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '27', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '28', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '29', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '30', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '31', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '32', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '33', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '34', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '35', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '36', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '37', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '38', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '39', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '40', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '41', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '42', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '43', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '44', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '45', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '46', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '47', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '48', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '49', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '50', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '51', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '52', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '53', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '54', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '55', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '56', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '57', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '58', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '59', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '60', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '61', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '62', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '63', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '64', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '65', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '66', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '67', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '68', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '69', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '70', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '71', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '72', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '73', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '74', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '75', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '76', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '77', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '78', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '79', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '80', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '81', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '82', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '83', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '84', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '85', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '86', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '87', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '88', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '89', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '90', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '91', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '92', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '93', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '94', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '95', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '96', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '97', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '98', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '99', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, + { + id: '100', + keyword: 'ebay buy', + intent: 'Commercial', + previousPosition: 3, + position: 1, + traffic: 1500, + trafficPercent: '6.8%', + vol: '22,000', + kd: '31.2', + cpc: '$1.15', + url: 'https://www.ebay.com/buy', + cost: '$1,725', + comp: '0.85', + results: '1,200,000,000', + crawledTime: '2024-06-01 12:00:00', + }, +]; diff --git a/stories/components/data-table/tests/data-table-cells.stories.tsx b/stories/components/data-table/tests/data-table-cells.stories.tsx index 2e42743874..38a7d3d754 100644 --- a/stories/components/data-table/tests/data-table-cells.stories.tsx +++ b/stories/components/data-table/tests/data-table-cells.stories.tsx @@ -3,8 +3,14 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import React from 'react'; import CardFlagInCellExample from './examples/cells-tests/card-flag-in-cell'; -import CheckBoxExample from './examples/cells-tests/checkbox'; -import CheckboxInTableWithNoDataExample from './examples/cells-tests/checkbox-in-table-with-no-data'; +import CheckBoxExample, { defaultProps as checkboxProps } from './examples/cells-tests/checkbox'; +import type { DemoProps as CheckboxProps } from './examples/cells-tests/checkbox'; +import CheckboxInTableWithNoDataExample, { defaultProps as checkboxNoDataProps } from './examples/cells-tests/checkbox-in-table-with-no-data'; +import type { DemoProps as CheckboxNoDataProps } from './examples/cells-tests/checkbox-in-table-with-no-data'; +import CheckboxReactiveExample, { defaultProps as checkboxReactiveProps } from './examples/cells-tests/checkbox-reactive'; +import type { DemoProps as CheckboxReactiveProps } from './examples/cells-tests/checkbox-reactive'; +import CheckboxReactiveWithPaginationExample, { defaultProps as checkboxReactivePaginationProps } from './examples/cells-tests/checkbox-reactive-with-pagination'; +import type { DemoProps as CheckboxReactivePaginationProps } from './examples/cells-tests/checkbox-reactive-with-pagination'; import DDSelectInCellExample from './examples/cells-tests/dd-select-in-cell'; import InteractiveCellsExample from './examples/cells-tests/interactive-elements-in-cells'; import LongTextCellsExample from './examples/cells-tests/long-text-in-cells'; @@ -50,12 +56,52 @@ export const CardFlagInCell: Story = { render: CardFlagInCellExample, }; -export const CheckBox: Story = { - render: CheckBoxExample, +export const CheckBox: StoryObj = { + render: CheckBoxExample as any, + argTypes: { + fixedColumns: { + control: 'boolean', + description: 'Enable fixed left/right columns with horizontal scroll', + }, + }, + args: checkboxProps, }; -export const CheckboxInTableWithNoData: Story = { +export const CheckboxInTableWithNoData: StoryObj = { render: CheckboxInTableWithNoDataExample, + argTypes: { + reactive: { + control: 'boolean', + description: 'Use SelectableRows (reactive) instead of array', + }, + }, + args: checkboxNoDataProps, +}; + +export const CheckboxReactive: StoryObj = { + render: CheckboxReactiveExample as any, + argTypes: { + fixedColumns: { + control: 'boolean', + description: 'Enable fixed left/right columns with horizontal scroll', + }, + }, + args: checkboxReactiveProps, +}; + +export const CheckboxReactiveWithPagination: StoryObj = { + render: CheckboxReactiveWithPaginationExample, + argTypes: { + mergedRows: { + control: 'boolean', + description: 'Use merged rows (ROW_GROUP) data', + }, + loading: { + control: 'boolean', + description: 'Show loading state', + }, + }, + args: checkboxReactivePaginationProps, }; export const MergedRowForMultiLevelHeader: StoryObj = { diff --git a/stories/components/data-table/tests/examples/cells-tests/checkbox-in-table-with-no-data.tsx b/stories/components/data-table/tests/examples/cells-tests/checkbox-in-table-with-no-data.tsx index b3f5122bcc..9b599ba8d5 100644 --- a/stories/components/data-table/tests/examples/cells-tests/checkbox-in-table-with-no-data.tsx +++ b/stories/components/data-table/tests/examples/cells-tests/checkbox-in-table-with-no-data.tsx @@ -1,24 +1,49 @@ -import { DataTable } from '@semcore/ui/data-table'; +import { DataTable, SelectableRows } from '@semcore/ui/data-table'; import React from 'react'; -const Demo = () => { - return ( - <> +export type DemoProps = { + reactive?: boolean; +}; + +const selectableRows = new SelectableRows(); + +const columns = [ + { name: 'keyword', children: 'Keyword' }, + { name: 'kd', children: 'KD %' }, + { name: 'cpc', children: 'CPC' }, + { name: 'vol', children: 'Vol.' }, +]; + +const Demo = ({ reactive = false }: DemoProps) => { + if (reactive) { + return ( - + ); + } + + return ( + ); }; +export const defaultProps: DemoProps = { + reactive: false, +}; + +Demo.defaultProps = defaultProps; + export default Demo; diff --git a/stories/components/data-table/tests/examples/cells-tests/checkbox-reactive-with-pagination.tsx b/stories/components/data-table/tests/examples/cells-tests/checkbox-reactive-with-pagination.tsx new file mode 100644 index 0000000000..797f197db2 --- /dev/null +++ b/stories/components/data-table/tests/examples/cells-tests/checkbox-reactive-with-pagination.tsx @@ -0,0 +1,230 @@ +import { + Box, + Flex, + Collapse, + ScreenReaderOnly, +} from '@semcore/ui/base-components'; +import Button from '@semcore/ui/button'; +import { DataTable, ROW_GROUP, SelectableRows } from '@semcore/ui/data-table'; +import Pagination from '@semcore/ui/pagination'; +import { Text } from '@semcore/ui/typography'; +import React from 'react'; + +export type DemoProps = { + mergedRows?: boolean; + loading?: boolean; +}; + +const selectedRows = new SelectableRows(); + +const columns: { name: string; children: string }[] = [ + { name: 'keyword', children: 'Keyword' }, + { name: 'kd', children: 'KD %' }, + { name: 'cpc', children: 'CPC' }, + { name: 'vol', children: 'Vol.' }, +]; + +const Demo = ({ mergedRows = false, loading = false }: DemoProps) => { + const [selectedRowsDisplay, setSelectedRowsDisplay] = React.useState(0); + const [ariaMessage, setAriaMessage] = React.useState(''); + const [currentPage, setCurrentPage] = React.useState(0); + const tableRef = React.useRef(null); + + const handleDeselectAll = () => { + selectedRows.clearAll(); + tableRef.current?.focus(); + }; + + React.useEffect(() => { + const unsubscribeToggle = selectedRows.subscribe(SelectableRows.TOGGLE_EVENT, () => { + const size = selectedRows.get().length; + if (size > 0) setAriaMessage('Action bar appeared before the table'); + setSelectedRowsDisplay(size); + }); + const unsubscribeSelectAll = selectedRows.subscribe(SelectableRows.SELECT_ALL_EVENT, () => { + const size = selectedRows.get().length; + if (size > 0) setAriaMessage('Action bar appeared before the table'); + setSelectedRowsDisplay(size); + }); + + return () => { + unsubscribeToggle(); + unsubscribeSelectAll(); + }; + }, []); + + React.useEffect(() => { + const timer = setTimeout(() => setAriaMessage(''), 300); + return () => clearTimeout(timer); + }, [ariaMessage]); + + const currentData = mergedRows ? mergedData : flatData; + const limit = 5; + const tableData = currentData.slice(currentPage * limit, currentPage * limit + limit); + + return ( + <> + + + {ariaMessage} + + 0} + duration={0} + style={{ position: 'sticky', top: 0, zIndex: 50 }} + > + + + Selected rows: {selectedRowsDisplay} + + + + + + + setCurrentPage(page - 1)} + aria-label='Table with selectable rows pagination' + /> + + ); +}; + +const flatData = [ + { id: '1', keyword: 'ebay buy', kd: '31.2', cpc: '$1.15', vol: '22,000' }, + { id: '2', keyword: 'amazon shoes', kd: '47', cpc: '$2.95', vol: '48,000' }, + { id: '3', keyword: 'www.nike.com', kd: '66.4', cpc: '$3.80', vol: 'n/a' }, + { id: '4', keyword: 'buy iphone 13', kd: '59', cpc: '$5.20', vol: '71,000' }, + { id: '5', keyword: 'adidas sale', kd: '40.2', cpc: '$1.85', vol: '19,500' }, + { id: '6', keyword: 'cheap flights expedia', kd: '52', cpc: '$4.10', vol: '35,800' }, + { id: '7', keyword: 'booking.com hotels', kd: '73', cpc: '$6.45', vol: 'n/a' }, + { id: '8', keyword: 'ubereats promo code', kd: '38', cpc: '$2.10', vol: '11,700' }, + { id: '9', keyword: 'buy ps5 online', kd: '64', cpc: '$5.95', vol: '44,200' }, + { id: '10', keyword: 'shopify login', kd: '25.8', cpc: '$0.65', vol: '13,600' }, + { id: '11', keyword: 'h&m online store', kd: '36', cpc: '$1.70', vol: '10,300' }, + { id: '12', keyword: 'buy macbook air', kd: '57.4', cpc: '$4.90', vol: '28,400' }, + { id: '13', keyword: 'www.zara.com', kd: '45', cpc: '$3.20', vol: 'n/a' }, + { id: '14', keyword: 'target clearance', kd: '33', cpc: '$1.25', vol: '12,900' }, + { id: '15', keyword: 'asos men jackets', kd: '41', cpc: '$2.55', vol: '6,800' }, + { id: '16', keyword: 'best buy coupons', kd: '48', cpc: '$3.70', vol: '17,100' }, + { id: '17', keyword: 'walmart near me', kd: '60.1', cpc: '$0.95', vol: '50,000' }, + { id: '18', keyword: 'netflix gift card', kd: '39', cpc: '$2.20', vol: '8,900' }, + { id: '19', keyword: 'www.apple.com', kd: '71', cpc: '$6.90', vol: 'n/a' }, + { id: '20', keyword: 'nike running shoes men', kd: '44', cpc: '$3.60', vol: '21,700' }, + { id: '21', keyword: 'download spotify premium', kd: '58', cpc: '$4.75', vol: '26,800' }, + { id: '22', keyword: 'buy dell laptop', kd: '53.1', cpc: '$5.40', vol: '19,600' }, + { id: '23', keyword: 'gap kids sale', kd: '34', cpc: '$1.10', vol: '5,300' }, +]; + +const mergedData = [ + { + id: '.1', + keyword: 'ebay buy', + [ROW_GROUP]: [ + { id: '1', kd: '31.2', cpc: '$1.15', vol: '22,000' }, + { id: '2', kd: '31.2', cpc: '$1.15', vol: '22,000' }, + ], + }, + { + id: '.2', + keyword: 'amazon shoes', + [ROW_GROUP]: [ + { id: '3', kd: '47', cpc: '$2.95', vol: '48,000' }, + { id: '4', kd: '47', cpc: '$2.95', vol: '48,000' }, + ], + }, + { + id: '.3', + keyword: 'www.nike.com', + [ROW_GROUP]: [ + { id: '5', kd: '66.4', cpc: '$3.80', vol: 'n/a' }, + { id: '6', kd: '66.4', cpc: '$3.80', vol: 'n/a' }, + ], + }, + { + id: '.4', + keyword: 'buy iphone 13', + [ROW_GROUP]: [ + { id: '7', kd: '59', cpc: '$5.20', vol: '71,000' }, + { id: '8', kd: '59', cpc: '$5.20', vol: '71,000' }, + ], + }, + { + id: '.5', + keyword: 'adidas sale', + [ROW_GROUP]: [ + { id: '9', kd: '40.2', cpc: '$1.85', vol: '19,500' }, + ], + }, + { + id: '.6', + keyword: 'cheap flights expedia', + [ROW_GROUP]: [ + { id: '10', kd: '52', cpc: '$4.10', vol: '35,800' }, + ], + }, + { + id: '.7', + keyword: 'booking.com hotels', + [ROW_GROUP]: [ + { id: '11', kd: '73', cpc: '$6.45', vol: 'n/a' }, + ], + }, + { + id: '.8', + keyword: 'ubereats promo code', + [ROW_GROUP]: [ + { id: '12', kd: '38', cpc: '$2.10', vol: '11,700' }, + ], + }, + { + id: '.9', + keyword: 'buy ps5 online', + [ROW_GROUP]: [ + { id: '13', kd: '64', cpc: '$5.95', vol: '44,200' }, + ], + }, + { + id: '.10', + keyword: 'shopify login', + [ROW_GROUP]: [ + { id: '14', kd: '25.8', cpc: '$0.65', vol: '13,600' }, + ], + }, +]; + +export const defaultProps: DemoProps = { + mergedRows: false, + loading: false, +}; + +Demo.defaultProps = defaultProps; + +export default Demo; diff --git a/stories/components/data-table/tests/examples/cells-tests/checkbox-reactive.tsx b/stories/components/data-table/tests/examples/cells-tests/checkbox-reactive.tsx new file mode 100644 index 0000000000..b7103e4fbb --- /dev/null +++ b/stories/components/data-table/tests/examples/cells-tests/checkbox-reactive.tsx @@ -0,0 +1,90 @@ +import { Flex } from '@semcore/ui/base-components'; +import Button from '@semcore/ui/button'; +import { DataTable, SelectableRows } from '@semcore/ui/data-table'; +import { Text } from '@semcore/ui/typography'; +import React from 'react'; +export type DemoProps = { + fixedColumns?: boolean; + limitMode?: boolean; + rowsLimit?: number; + columnsLimit?: number; +}; + +const data = Array.from({ length: 50 }).map((_, index) => ({ + id: String(index + 1), + name: `Row ${index + 1}`, + kd: `${(Math.random() * 100).toFixed(1)}`, + cpc: `$${(Math.random() * 10).toFixed(2)}`, + vol: `${Math.floor(Math.random() * 100000)}`, +})); + +const selectedRows = new SelectableRows(); + +const Demo = (props: DemoProps) => { + const { rowsLimit, columnsLimit, fixedColumns, limitMode } = props; + + const columns = fixedColumns + ? [ + { name: 'id', children: 'ID', fixed: 'left' as const, gtcWidth: '100px' }, + { name: 'name', children: 'Name', gtcWidth: '200px' }, + { name: 'kd', children: 'KD %', gtcWidth: '150px' }, + { name: 'cpc', children: 'CPC', gtcWidth: '150px' }, + { name: 'vol', children: 'Vol.', fixed: 'right' as const, gtcWidth: '120px' }, + ] + : [ + { name: 'id', children: 'ID' }, + { name: 'name', children: 'Name' }, + ]; + + const limit = limitMode + ? { + fromRow: rowsLimit, + fromColumn: columnsLimit, + renderOverlay() { + return ( + + + You've reached your report limit for today + + + To increase your daily report limit, upgrade to a Guru plan. + + + + + ); + }, + } + : undefined; + + return ( +
+ +
+ ); +}; + +export const defaultProps: DemoProps = { + fixedColumns: false, + limitMode: false, + columnsLimit: 1, + rowsLimit: 1, +}; + +Demo.defaultProps = defaultProps; + +export default Demo; diff --git a/stories/components/data-table/tests/examples/cells-tests/checkbox.tsx b/stories/components/data-table/tests/examples/cells-tests/checkbox.tsx index b114bf94e6..3464e12f85 100644 --- a/stories/components/data-table/tests/examples/cells-tests/checkbox.tsx +++ b/stories/components/data-table/tests/examples/cells-tests/checkbox.tsx @@ -1,17 +1,37 @@ import { DataTable, type DataTableSort } from '@semcore/ui/data-table'; import React from 'react'; -const data = Array.from({ length: 50 }).map((_, index) => ({ +export type DemoProps = { + fixedColumns?: boolean; +}; + +const baseData = Array.from({ length: 50 }).map((_, index) => ({ + id: index + 1, + name: `Row ${index + 1}`, +})); + +const extendedData = Array.from({ length: 50 }).map((_, index) => ({ id: index + 1, name: `Row ${index + 1}`, + kd: `${(Math.random() * 100).toFixed(1)}`, + cpc: `$${(Math.random() * 10).toFixed(2)}`, + vol: `${Math.floor(Math.random() * 100000)}`, })); -const columns = [ +const baseColumns = [ { name: 'id', children: 'ID', sortable: true }, { name: 'name', children: 'Name', sortable: true }, ]; -const Demo = () => { +const fixedColumns = [ + { name: 'id', children: 'ID', sortable: true, fixed: 'left' as const, gtcWidth: '100px' }, + { name: 'name', children: 'Name', sortable: true, gtcWidth: '200px' }, + { name: 'kd', children: 'KD %', sortable: true, gtcWidth: '150px' }, + { name: 'cpc', children: 'CPC', sortable: true, gtcWidth: '150px' }, + { name: 'vol', children: 'Vol.', sortable: true, fixed: 'right' as const, gtcWidth: '120px' }, +]; + +const Demo = ({ fixedColumns: useFixedColumns = false }: DemoProps) => { const [selectedRows, setSelectedRows] = React.useState([]); const [sort, setSort] = React.useState>(); @@ -19,9 +39,10 @@ const Demo = () => {
{ ); }; +export const defaultProps: DemoProps = { + fixedColumns: false, +}; + +Demo.defaultProps = defaultProps; + export default Demo; diff --git a/website/docs/core-principles/writing-code/examples/complex-components-wrappers.tsx b/website/docs/core-principles/writing-code/examples/complex-components-wrappers.tsx index 91d732b596..4d42202218 100644 --- a/website/docs/core-principles/writing-code/examples/complex-components-wrappers.tsx +++ b/website/docs/core-principles/writing-code/examples/complex-components-wrappers.tsx @@ -9,6 +9,7 @@ const CardDataTable = wrapDataTable<{ title: string }>(({ title, ...restProps }) {title} + {/* @ts-ignore todo: Ilia Brauer remove in 17 */} @@ -17,6 +18,7 @@ const CardDataTable = wrapDataTable<{ title: string }>(({ title, ...restProps }) const Demo = () => { return ( + // @ts-ignore todo: Ilia Brauer remove in 17