diff --git a/playbook/app/pb_kits/playbook/pb_collapsible/collapsible.html.erb b/playbook/app/pb_kits/playbook/pb_collapsible/collapsible.html.erb index 919544cc91..2f6f319cff 100644 --- a/playbook/app/pb_kits/playbook/pb_collapsible/collapsible.html.erb +++ b/playbook/app/pb_kits/playbook/pb_collapsible/collapsible.html.erb @@ -1,3 +1,5 @@ -<%= pb_content_tag do %> +<%= pb_content_tag( + object.tag +) do %> <%= content.presence %> <% end %> diff --git a/playbook/app/pb_kits/playbook/pb_collapsible/collapsible.rb b/playbook/app/pb_kits/playbook/pb_collapsible/collapsible.rb index d30c8e07de..84a8c56333 100644 --- a/playbook/app/pb_kits/playbook/pb_collapsible/collapsible.rb +++ b/playbook/app/pb_kits/playbook/pb_collapsible/collapsible.rb @@ -3,6 +3,9 @@ module Playbook module PbCollapsible class Collapsible < Playbook::KitBase + prop :tag, type: Playbook::Props::Enum, + values: %w[h1 h2 h3 h4 h5 h6 p div span tr th td thead col], + default: "div" def classname generate_classname("pb_collapsible_kit") end diff --git a/playbook/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible.html.erb b/playbook/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible.html.erb new file mode 100644 index 0000000000..01ee795bfd --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible.html.erb @@ -0,0 +1,47 @@ +<% content = capture do %> + <%= pb_rails("card", props: { border_none: true, border_radius: "none", padding: "md" }) do %> + <%= pb_rails("body", props: { text: "Nested content inside a Table Row" }) %> + <% end %> +<% end %> + +<%= pb_rails("table", props: { size: "sm" }) do %> + <%= pb_rails("table/table_head") do %> + <%= pb_rails("table/table_row") do %> + <%= pb_rails("table/table_header", props: { text: "Column 1"}) %> + <%= pb_rails("table/table_header", props: { text: "Column 2"}) %> + <%= pb_rails("table/table_header", props: { text: "Column 3"}) %> + <%= pb_rails("table/table_header", props: { text: "Column 4"}) %> + <%= pb_rails("table/table_header", props: { text: "Column 5"}) %> + <%= pb_rails("table/table_header", props: { text: ""}) %> + <% end %> + <% end %> + <%= pb_rails("table/table_body") do %> + <%= pb_rails("table/table_row", props: { collapsible: true, collapsible_content: content, collapsible_side_highlight: true, id: "1" }) do %> + <%= pb_rails("table/table_cell", props: { text: "Value 1"}) %> + <%= pb_rails("table/table_cell", props: { text: "Value 2"}) %> + <%= pb_rails("table/table_cell", props: { text: "Value 3"}) %> + <%= pb_rails("table/table_cell", props: { text: "Value 4"}) %> + <%= pb_rails("table/table_cell", props: { text: "Value 5"}) %> + <%= pb_rails("table/table_cell", props: { text_align: "right"}) do %> + <%= pb_rails("icon", props: { icon: "chevron-down", fixed_width: true, color: "primary" }) %> + <% end %> + <% end %> + <%= pb_rails("table/table_row") do %> + <%= pb_rails("table/table_cell", props: { text: "Value 1"}) %> + <%= pb_rails("table/table_cell", props: { text: "Value 2"}) %> + <%= pb_rails("table/table_cell", props: { text: "Value 3"}) %> + <%= pb_rails("table/table_cell", props: { text: "Value 4"}) %> + <%= pb_rails("table/table_cell", props: { text: "Value 5"}) %> + <%= pb_rails("table/table_cell", props: { text: ""}) %> + <% end %> + <%= pb_rails("table/table_row") do %> + <%= pb_rails("table/table_cell", props: { text: "Value 1"}) %> + <%= pb_rails("table/table_cell", props: { text: "Value 2"}) %> + <%= pb_rails("table/table_cell", props: { text: "Value 3"}) %> + <%= pb_rails("table/table_cell", props: { text: "Value 4"}) %> + <%= pb_rails("table/table_cell", props: { text: "Value 5"}) %> + <%= pb_rails("table/table_cell", props: { text: ""}) %> + <% end %> + <% end %> +<% end %> + \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_rails.md b/playbook/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_rails.md new file mode 100644 index 0000000000..35f800e10a --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_rails.md @@ -0,0 +1,2 @@ +The `collapsible` prop can be used on any Table Row to add a collapsible area. Use the additional `collapsible_content` prop to add any content to the collapsible Row. +Please be aware that you will need to pass in an `id` to any table rows you want to be collapsible. Make sure every `id` is unique if you are using multipe collapsibles. \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible.md b/playbook/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_react.md similarity index 100% rename from playbook/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible.md rename to playbook/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_react.md diff --git a/playbook/app/pb_kits/playbook/pb_table/docs/example.yml b/playbook/app/pb_kits/playbook/pb_table/docs/example.yml index 3633a3b928..268db9b6d7 100755 --- a/playbook/app/pb_kits/playbook/pb_table/docs/example.yml +++ b/playbook/app/pb_kits/playbook/pb_table/docs/example.yml @@ -30,6 +30,7 @@ examples: - table_with_subcomponents_rails: Table with Sub Components (Table Elements) - table_with_subcomponents_as_divs: Table with Sub Components (Divs) - table_outer_padding: Outer Padding + - table_with_collapsible: Table with Collapsible react: - table_sm: Small diff --git a/playbook/app/pb_kits/playbook/pb_table/index.ts b/playbook/app/pb_kits/playbook/pb_table/index.ts index ce0b340f11..54c26ace91 100644 --- a/playbook/app/pb_kits/playbook/pb_table/index.ts +++ b/playbook/app/pb_kits/playbook/pb_table/index.ts @@ -1,167 +1,207 @@ import PbEnhancedElement from '../pb_enhanced_element' const TABLE_WRAPPER_SELECTOR = "[data-pb-table-wrapper]"; +const TABLE_COLLAPSIBLE_WRAPPER_SELECTOR = "[data-pb-table-collapsible-wrapper]"; export default class PbTable extends PbEnhancedElement { - stickyLeftColumns: string[] = []; - stickyRightColumns: string[] = []; - stickyRightColumnsReversed: string[] = []; - - static get selector(): string { - return TABLE_WRAPPER_SELECTOR; - } - - connect() { - if (this.element.classList.contains('table-responsive-collapse')) { - const headers: string[] = []; + stickyLeftColumns: string[] = []; + stickyRightColumns: string[] = []; + stickyRightColumnsReversed: string[] = []; + + static get selector(): string { + return TABLE_WRAPPER_SELECTOR; + } + + connect() { + if (this.element.classList.contains('table-responsive-collapse')) { + const headers: string[] = []; + + [].forEach.call(this.element.querySelectorAll('th'), (header: HTMLTableCellElement) => { + const colSpan = header.colSpan + for (let i = 0; i < colSpan; i++) { + headers.push(header.textContent.replace(/\r?\n|\r/, '')); + } + }); - [].forEach.call(this.element.querySelectorAll('th'), (header: HTMLTableCellElement) => { - const colSpan = header.colSpan - for (let i = 0; i < colSpan; i++) { - headers.push(header.textContent.replace(/\r?\n|\r/, '')); - } - }); - - [].forEach.call(this.element.querySelectorAll('tbody tr'), (row: HTMLTableRowElement) => { - [].forEach.call(row.cells, (cell: HTMLTableCellElement, headerIndex: number) => { - cell.setAttribute('data-title', headers[headerIndex]) - }) + [].forEach.call(this.element.querySelectorAll('tbody tr'), (row: HTMLTableRowElement) => { + [].forEach.call(row.cells, (cell: HTMLTableCellElement, headerIndex: number) => { + cell.setAttribute('data-title', headers[headerIndex]) }) - } - - this.initStickyLeftColumns(); - this.initStickyRightColumns(); + }) } - initStickyLeftColumns() { - const table = this.element.querySelector('.sticky-left-column'); - - if (table) { - const classList = Array.from(table.classList); - const stickyColumnClass = classList.find(cls => cls.startsWith('sticky-left-columns-ids-')); - - if (stickyColumnClass) { - this.stickyLeftColumns = stickyColumnClass - .replace('sticky-left-columns-ids-', '') - .split('-'); - - if (this.stickyLeftColumns.length > 0) { - setTimeout(() => { - this.handleStickyLeftColumns(); - window.addEventListener('resize', () => this.handleStickyLeftColumns()); - }, 10); - } + this.initStickyLeftColumns(); + this.initStickyRightColumns(); + this.handleCollapsibleClick(); + this.handleCollapsibleRow(); + } + + initStickyLeftColumns() { + const table = this.element.querySelector('.sticky-left-column'); + + if (table) { + const classList = Array.from(table.classList); + const stickyColumnClass = classList.find(cls => cls.startsWith('sticky-left-columns-ids-')); + + if (stickyColumnClass) { + this.stickyLeftColumns = stickyColumnClass + .replace('sticky-left-columns-ids-', '') + .split('-'); + + if (this.stickyLeftColumns.length > 0) { + setTimeout(() => { + this.handleStickyLeftColumns(); + window.addEventListener('resize', () => this.handleStickyLeftColumns()); + }, 10); } } } + } + + handleStickyLeftColumns() { + let accumulatedWidth = 0; + + this.stickyLeftColumns.forEach((colId, index) => { + const isLastColumn = index === this.stickyLeftColumns.length - 1; + const header = this.element.querySelector(`th[id="${colId}"]`); + const cells = this.element.querySelectorAll(`td[id="${colId}"]`); + + if (header) { + header.classList.add('sticky'); + (header as HTMLElement).style.left = `${accumulatedWidth}px`; + + if (!isLastColumn) { + header.classList.add('with-border-right'); + header.classList.remove('sticky-left-shadow'); + } else { + header.classList.remove('with-border-right'); + header.classList.add('sticky-left-shadow'); + } - handleStickyLeftColumns() { - let accumulatedWidth = 0; - - this.stickyLeftColumns.forEach((colId, index) => { - const isLastColumn = index === this.stickyLeftColumns.length - 1; - const header = this.element.querySelector(`th[id="${colId}"]`); - const cells = this.element.querySelectorAll(`td[id="${colId}"]`); - - if (header) { - header.classList.add('sticky'); - (header as HTMLElement).style.left = `${accumulatedWidth}px`; + accumulatedWidth += (header as HTMLElement).offsetWidth; + } - if (!isLastColumn) { - header.classList.add('with-border-right'); - header.classList.remove('sticky-left-shadow'); - } else { - header.classList.remove('with-border-right'); - header.classList.add('sticky-left-shadow'); - } + cells.forEach((cell) => { + cell.classList.add('sticky'); + (cell as HTMLElement).style.left = `${accumulatedWidth - (header as HTMLElement).offsetWidth}px`; - accumulatedWidth += (header as HTMLElement).offsetWidth; + if (!isLastColumn) { + cell.classList.add('with-border-right'); + cell.classList.remove('sticky-left-shadow'); + } else { + cell.classList.remove('with-border-right'); + cell.classList.add('sticky-left-shadow'); } - - cells.forEach((cell) => { - cell.classList.add('sticky'); - (cell as HTMLElement).style.left = `${accumulatedWidth - (header as HTMLElement).offsetWidth}px`; - - if (!isLastColumn) { - cell.classList.add('with-border-right'); - cell.classList.remove('sticky-left-shadow'); - } else { - cell.classList.remove('with-border-right'); - cell.classList.add('sticky-left-shadow'); - } - }); }); - } - - initStickyRightColumns() { - const table = this.element.querySelector('.sticky-right-column'); - - if (table) { - const classList = Array.from(table.classList); - const stickyColumnClass = classList.find(cls => cls.startsWith('sticky-right-columns-ids-')); - - if (stickyColumnClass) { - this.stickyRightColumns = stickyColumnClass - .replace('sticky-right-columns-ids-', '') - .split('-'); - this.stickyRightColumnsReversed = this.stickyRightColumns.reverse(); - - if (this.stickyRightColumns.length > 0) { - setTimeout(() => { - this.handleStickyRightColumns(); - window.addEventListener('resize', () => this.handleStickyRightColumns()); - }, 10); - } + }); + } + + initStickyRightColumns() { + const table = this.element.querySelector('.sticky-right-column'); + + if (table) { + const classList = Array.from(table.classList); + const stickyColumnClass = classList.find(cls => cls.startsWith('sticky-right-columns-ids-')); + + if (stickyColumnClass) { + this.stickyRightColumns = stickyColumnClass + .replace('sticky-right-columns-ids-', '') + .split('-'); + this.stickyRightColumnsReversed = this.stickyRightColumns.reverse(); + + if (this.stickyRightColumns.length > 0) { + setTimeout(() => { + this.handleStickyRightColumns(); + window.addEventListener('resize', () => this.handleStickyRightColumns()); + }, 10); } } } + } + + handleStickyRightColumns() { + let accumulatedWidth = 0; + + this.stickyRightColumnsReversed.forEach((colId, index) => { + const isLastColumn = index === this.stickyRightColumns.length - 1; + const header = this.element.querySelector(`th[id="${colId}"]`); + const cells = this.element.querySelectorAll(`td[id="${colId}"]`); + + if (header) { + header.classList.add('sticky'); + (header as HTMLElement).style.right = `${accumulatedWidth}px`; + + if (!isLastColumn) { + header.classList.add('with-border-left'); + header.classList.remove('sticky-right-shadow'); + } else { + header.classList.remove('with-border-right'); + header.classList.add('sticky-right-shadow'); + } - handleStickyRightColumns() { - let accumulatedWidth = 0; - - this.stickyRightColumnsReversed.forEach((colId, index) => { - const isLastColumn = index === this.stickyRightColumns.length - 1; - const header = this.element.querySelector(`th[id="${colId}"]`); - const cells = this.element.querySelectorAll(`td[id="${colId}"]`); - - if (header) { - header.classList.add('sticky'); - (header as HTMLElement).style.right = `${accumulatedWidth}px`; + accumulatedWidth += (header as HTMLElement).offsetWidth; + } - if (!isLastColumn) { - header.classList.add('with-border-left'); - header.classList.remove('sticky-right-shadow'); - } else { - header.classList.remove('with-border-right'); - header.classList.add('sticky-right-shadow'); - } + cells.forEach((cell) => { + cell.classList.add('sticky'); + (cell as HTMLElement).style.right = `${accumulatedWidth - (header as HTMLElement).offsetWidth}px`; - accumulatedWidth += (header as HTMLElement).offsetWidth; + if (!isLastColumn) { + cell.classList.add('with-border-left'); + cell.classList.remove('sticky-right-shadow'); + } else { + cell.classList.remove('with-border-left'); + cell.classList.add('sticky-right-shadow'); } - - cells.forEach((cell) => { - cell.classList.add('sticky'); - (cell as HTMLElement).style.right = `${accumulatedWidth - (header as HTMLElement).offsetWidth}px`; - - if (!isLastColumn) { - cell.classList.add('with-border-left'); - cell.classList.remove('sticky-right-shadow'); - } else { - cell.classList.remove('with-border-left'); - cell.classList.add('sticky-right-shadow'); - } + }); + }); + } + + handleCollapsibleClick() { + const collapsibleElements = this.element.querySelectorAll(TABLE_COLLAPSIBLE_WRAPPER_SELECTOR); + collapsibleElements.forEach((collapsibleElement) => { + collapsibleElement.addEventListener('click', (event) => { + document.dispatchEvent(new CustomEvent(`collapsed-toggle${(event.currentTarget as HTMLElement).id}`)) + + const toggleElements = this.element.querySelectorAll(`.collapsible_border_toggle${(event.currentTarget as HTMLElement).id}`); + toggleElements.forEach(element => { + element.classList.toggle('no-border'); + element.classList.toggle('border-active'); }); + }) + }) + } + + handleCollapsibleRow() { + const collapsibleRows = this.element.querySelectorAll('.pb_table_collapsible_row'); + if (collapsibleRows.length > 0) { + collapsibleRows.forEach((row) => { + const previousRow = row.previousElementSibling; + + if ( + previousRow && + previousRow.tagName === 'TR' + ) { + const tdCount = previousRow.querySelectorAll('td').length; + const collapsibleTd = row.querySelector('td'); + if (collapsibleTd) { + collapsibleTd.colSpan = tdCount; + } + } else { + return + } }); } + } - // Cleanup method to remove event listener - disconnect() { - if (this.stickyLeftColumns.length > 0) { - window.removeEventListener('resize', () => this.handleStickyLeftColumns()); - } + // Cleanup method to remove event listener + disconnect() { + if (this.stickyLeftColumns.length > 0) { + window.removeEventListener('resize', () => this.handleStickyLeftColumns()); + } - if (this.stickyRightColumns.length > 0) { - window.removeEventListener('resize', () => this.handleStickyRightColumns()); - } + if (this.stickyRightColumns.length > 0) { + window.removeEventListener('resize', () => this.handleStickyRightColumns()); } -} \ No newline at end of file + } +} diff --git a/playbook/app/pb_kits/playbook/pb_table/styles/_collapsible.scss b/playbook/app/pb_kits/playbook/pb_table/styles/_collapsible.scss index 5274ac80fd..bdda3343d0 100644 --- a/playbook/app/pb_kits/playbook/pb_table/styles/_collapsible.scss +++ b/playbook/app/pb_kits/playbook/pb_table/styles/_collapsible.scss @@ -32,4 +32,16 @@ } } } + + .collapsible-tr { + cursor: pointer; + } + + .no-border { + border-bottom: none !important; + } + + .border-active { + border-bottom: 1px; + } } diff --git a/playbook/app/pb_kits/playbook/pb_table/table_row.html.erb b/playbook/app/pb_kits/playbook/pb_table/table_row.html.erb index 1d76854fac..b0ef868509 100644 --- a/playbook/app/pb_kits/playbook/pb_table/table_row.html.erb +++ b/playbook/app/pb_kits/playbook/pb_table/table_row.html.erb @@ -1,4 +1,23 @@ -<% if object.tag == "table" %> +<% if object.collapsible && object.tag == "table" %> + <%= content_tag(:tr, + aria: object.aria, + class: object.classname + " collapsible-tr", + data: object.data.merge(id: object.id), + id: object.id, + 'data-pb-table-collapsible-wrapper' => true, + **combined_html_options) do %> + <%= content.presence %> + <% end %> + + + <%= pb_rails("collapsible", props: { classname: "collapsible_border_toggle#{object.id}" + " no-border", name: "default-example", tag: "td", padding: "none" }) do %> + <%= pb_rails("flex", props: { data: { "collapsible-main": "true"} }) %> + <%= pb_rails("collapsible/collapsible_content", props: { classname: object.collapsible_side_highlight ? "table_collapsible_side_highlight" : "", padding: "none", margin: "none", id: "collapsed-toggle#{object.id}" }) do %> + <%= object.collapsible_content %> + <% end %> + <% end %> + +<% elsif object.tag == "table" %> <%= content_tag(:tr, aria: object.aria, class: object.classname, diff --git a/playbook/app/pb_kits/playbook/pb_table/table_row.rb b/playbook/app/pb_kits/playbook/pb_table/table_row.rb index bb32cd7262..e56607a5fa 100644 --- a/playbook/app/pb_kits/playbook/pb_table/table_row.rb +++ b/playbook/app/pb_kits/playbook/pb_table/table_row.rb @@ -8,6 +8,11 @@ class TableRow < Playbook::KitBase prop :tag, type: Playbook::Props::Enum, values: %w[table div], default: "table" + prop :collapsible, type: Playbook::Props::Boolean, + default: false + prop :collapsible_content + prop :collapsible_side_highlight, type: Playbook::Props::Boolean, + default: false def classname generate_classname("pb_table_row_kit", side_highlight_class) + tag_class