diff --git a/.changeset/healthy-emus-destroy.md b/.changeset/healthy-emus-destroy.md new file mode 100644 index 0000000000..db61dc8c9e --- /dev/null +++ b/.changeset/healthy-emus-destroy.md @@ -0,0 +1,47 @@ +--- +'@rhds/elements': minor +--- + +✨ Added ``. + +A table is a container for displaying information. It allows a user to scan, examine, and compare large amounts of data. + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Concerts +
DateEventVenue
12 FebruaryWaltz with StraussMain Hall
24 MarchThe ObelisksWest Wing
14 AprilThe WhatMain Hall
+ Dates and venues subject to change. +
+``` diff --git a/docs/_data/relatedItems.yaml b/docs/_data/relatedItems.yaml index 778a3236e2..ec5e4b634c 100644 --- a/docs/_data/relatedItems.yaml +++ b/docs/_data/relatedItems.yaml @@ -74,6 +74,8 @@ rh-subnav: - rh-navigation - rh-navigation-secondary - skip-navigation +rh-table: + - rh-code-block rh-tabs: - rh-jump-links - rh-pagination diff --git a/docs/elements/elements.html b/docs/elements/elements.html index 42b638ed59..6a5c8d25a4 100644 --- a/docs/elements/elements.html +++ b/docs/elements/elements.html @@ -10,6 +10,7 @@ - /assets/cem.css - /assets/packages/@rhds/elements/elements/rh-subnav/rh-subnav-lightdom.css - /assets/packages/@rhds/elements/elements/rh-pagination/rh-pagination-lightdom.css + - /assets/packages/@rhds/elements/elements/rh-table/rh-table-lightdom.css eleventyComputed: title: "{{ doc.pageTitle }} | {{ doc.slug | deslugify }}" importElements: @@ -18,6 +19,7 @@ - rh-footer - rh-subnav - rh-code-block + - rh-table - "{{ doc.tagName }}" --- diff --git a/docs/scss/styles.scss b/docs/scss/styles.scss index a5b420ac0d..38e0e7694e 100644 --- a/docs/scss/styles.scss +++ b/docs/scss/styles.scss @@ -148,6 +148,10 @@ body.page-docs rh-cta a[href^="http"]::after { white-space: nowrap; /* fix orphan issue on the Feedback shortcode's contact us link */ } +:is(rh-table) th { + font-size: unset; +} + rh-playground pre { max-height: 785px; } diff --git a/elements/rh-table/README.md b/elements/rh-table/README.md new file mode 100644 index 0000000000..61876cbc7b --- /dev/null +++ b/elements/rh-table/README.md @@ -0,0 +1,151 @@ +# Table + +A table is a container for displaying information. It allows a user to scan, examine, and compare large amounts of data. + +## Usage + +### Title + +Specify the title of the table using a `caption` element. + +```html + + + + +
+ Concerts +
+
+``` + +### Column highlighting + +To enable column highlighting, the `table` element must also include a `col` element for each column in the table, typically wrapped with a `colgroup`. + +```html + + + + + + + + + +
+ Concerts +
+
+``` + +### Sorting + +To enable sorting on a column, add an `rh-sort-button` as the last child of the `th` cell. + +```html + + + + + + + + + + + + + + + + +
+ Concerts +
DateEventVenue
+
+``` + +### Summary + +Additional information about the data in the table should be slotted into the `summary` slot after the `table` element. + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Concerts +
DateEventVenue
12 FebruaryWaltz with StraussMain Hall
24 MarchThe ObelisksWest Wing
14 AprilThe WhatMain Hall
+ Dates and venues subject to change. +
+``` + +## Example + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Concerts +
DateEventVenue
12 FebruaryWaltz with StraussMain Hall
24 MarchThe ObelisksWest Wing
14 AprilThe WhatMain Hall
+ Dates and venues subject to change. +
\ No newline at end of file diff --git a/elements/rh-table/demo/column-headers.html b/elements/rh-table/demo/column-headers.html new file mode 100644 index 0000000000..5aff74d830 --- /dev/null +++ b/elements/rh-table/demo/column-headers.html @@ -0,0 +1,44 @@ + + + + +
+

Column headers

+

Note: Tables with no thead will not stack on mobile.

+ + + + + + + + + + + + + + + + + + + + + + +
Date12 February24 March14 April
EventWaltz with StraussThe ObelisksThe What
VenueMain HallWest WingMain Hall
+
+
+ + \ No newline at end of file diff --git a/elements/rh-table/demo/demo.css b/elements/rh-table/demo/demo.css new file mode 100644 index 0000000000..8fc9178a51 --- /dev/null +++ b/elements/rh-table/demo/demo.css @@ -0,0 +1,11 @@ +section { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +h2, p { + margin-top: var(--rh-space-5xl, 80px); + text-align: center; +} \ No newline at end of file diff --git a/elements/rh-table/demo/rh-table.html b/elements/rh-table/demo/rh-table.html new file mode 100644 index 0000000000..e044fca4bb --- /dev/null +++ b/elements/rh-table/demo/rh-table.html @@ -0,0 +1,58 @@ + + + + +
+

Basic

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Concerts +
DateEvent + Venue +
12 FebruaryWaltz with StraussMain Hall
24 MarchThe ObelisksWest Wing
14 AprilThe WhatMain Hall
+ Dates and venues subject to change. +
+
+ + \ No newline at end of file diff --git a/elements/rh-table/demo/rh-table.js b/elements/rh-table/demo/rh-table.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/elements/rh-table/demo/row-and-column-headers.html b/elements/rh-table/demo/row-and-column-headers.html new file mode 100644 index 0000000000..68a1dedf46 --- /dev/null +++ b/elements/rh-table/demo/row-and-column-headers.html @@ -0,0 +1,74 @@ + + + + +
+

Row and column headers

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Delivery slots:
MondayTuesdayWednesdayThursdayFriday
09:00 – 11:00ClosedOpenOpenClosedClosed
11:00 – 13:00OpenOpenClosedClosedClosed
13:00 – 15:00OpenOpenOpenClosedClosed
15:00 – 17:00ClosedClosedClosedOpenOpen
+
+
+ + \ No newline at end of file diff --git a/elements/rh-table/demo/row-headers.html b/elements/rh-table/demo/row-headers.html new file mode 100644 index 0000000000..a0539056e3 --- /dev/null +++ b/elements/rh-table/demo/row-headers.html @@ -0,0 +1,52 @@ + + + + +
+

Row headers

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateEventVenue
12 February + Waltz with Strauss + Main Hall
24 MarchThe ObelisksWest Wing
14 AprilThe WhatMain Hall
+
+
+ + \ No newline at end of file diff --git a/elements/rh-table/demo/variants.html b/elements/rh-table/demo/variants.html new file mode 100644 index 0000000000..743ea824fe --- /dev/null +++ b/elements/rh-table/demo/variants.html @@ -0,0 +1,328 @@ + + + + +
+

Title, headers, and summary

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Concerts
DateEvent + Venue +
12 February + Waltz with Strauss + Main Hall
24 MarchThe ObelisksWest Wing
14 AprilThe WhatMain Hall
+ Dates and venues subject to change. +
+
+
+

Headers and summary, but no title

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateEventVenue
12 February + Waltz with Strauss + Main Hall
24 MarchThe ObelisksWest Wing
14 AprilThe WhatMain Hall
+
+
+
+

Title and summary, but no headers

+ + + + + + + + + + + + + + + + + + + + + + + +
Concerts
12 February + Waltz with Strauss + Main Hall
24 MarchThe ObelisksWest Wing
14 AprilThe WhatMain Hall
+ Dates and venues subject to change. Anything longer should wrap. +
+
+
+

No title, headers, and summary

+ + + + + + + + + + + + + + + + + + + + + + +
12 February + Waltz with Strauss + Main Hall
24 MarchThe ObelisksWest Wing
14 AprilThe WhatMain Hall
+
+ +
+
+

Horizontal overflow

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Main HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain Hall
Main HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain Hall
Main HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain HallMain Hall
+
+
+
+

Vertical overflow

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Main HallMain HallMain Hall
Main HallMain HallMain Hall
Main HallMain HallMain Hall
Main HallMain HallMain Hall
Main HallMain HallMain Hall
Main HallMain HallMain Hall
Main HallMain HallMain Hall
Main HallMain HallMain Hall
Main HallMain HallMain Hall
Main HallMain HallMain Hall
Main HallMain HallMain Hall
Main HallMain HallMain Hall
Main HallMain HallMain Hall
Main HallMain HallMain Hall
Main HallMain HallMain Hall
Main HallMain HallMain Hall
Main HallMain HallMain Hall
Main HallMain HallMain Hall
+
+
+
+ + \ No newline at end of file diff --git a/elements/rh-table/docs/00-overview.md b/elements/rh-table/docs/00-overview.md new file mode 100644 index 0000000000..3ff3990f3b --- /dev/null +++ b/elements/rh-table/docs/00-overview.md @@ -0,0 +1,64 @@ +## Overview + +{{ tagName | getElementDescription }} + +{% example palette="light", + alt="Image of table with four columns and three rows", + src="./table-sample-element.png" %} + +## Sample element + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Column 1Column 2Column 3Column 4
OneTwoThreeFour
FiveSixSevenEight
NineTenElevenTwelve
+
+ +## Demos + +View a live version of this element and see how it can be customized. +{% playground tagName=tagName %}{% endplayground %} +{% cta href="./demo/", target="_blank" %} +View the `` demo in a new tab +{% endcta %} + +## When to use + +- To organize and display structured data +- If a user needs to scan, examine, and compare data +- If a user must navigate to a specific piece of data to complete a task + +{% repoStatus type="Element" %} diff --git a/elements/rh-table/docs/10-style.md b/elements/rh-table/docs/10-style.md new file mode 100644 index 0000000000..4388f3e15c --- /dev/null +++ b/elements/rh-table/docs/10-style.md @@ -0,0 +1,75 @@ +## Style + +Use a table to organize and display data efficiently in a grid with columns and rows. When using a table, consider the structure of the data and how to make it easy for a user to scan, examine, and compare. Although a table can share space with other components and content, consider giving a table extra space on the page to help a user view dense data. + +{% example palette="light", + alt="Image of table with numbers next to various parts", + src="../table-anatomy.png" %} + + 1. Title + 2. Column + 3. Column title + 4. Row + 5. Row title + 6. Cell + 7. Divider + 8. Caption + {.example-notes} + +## Column and row titles + +Column and row titles should be a few words that describe the data in that column or row. + +{% example palette="light", + alt="Image of various tables with no titles, column titles, row titles, and both", + src="../table-column-row-titles.png" %} + +## Table title and caption + +The table title should make it clear to a user what the data is and what purpose it serves. A caption can be added under the table to provide more information about the data or its source. + +{% example palette="light", + alt="Image of table with a title on top and a caption underneath", + src="../table-title-caption.png" %} + +## Scrolling + +A scrollbar is visible if content exceeds the width or height of a table. Content can scroll horizontally, vertically, or both. + +{% example palette="light", + alt="Image of various tables with a scrollbar on the right, on the bottom, and both", + src="../table-scrolling.png" %} + +## Space + +A table has equal spacing within columns, rows, and in between divider lines. The same spacing is also maintained across large and small viewport sizes. + +{% example palette="light", + alt="Image of table with spacers in between elements", + src="../table-space.png" %} + +{% spacerTokensTable + caption='', + headingLevel="3", + tokens="--rh-space-lg" %} +{% endspacerTokensTable %} + +## Interaction states + +Interaction states are visual representations used to communicate the status of an element or pattern. + +### Hover + +Cell rows and columns are highlighted on hover. + +{% example palette="light", + alt="Image of table cell hover state", + src="../table-interaction-state-hover.png" %} + +### Focus + +{% alert state="warning", title="Warning" %} A cell with focus does not display row and column highlighting unless it is hovered. {% endalert %} + +{% example palette="light", + alt="Image of table cell active state", + src="../table-interaction-state-focus.png" %} \ No newline at end of file diff --git a/elements/rh-table/docs/20-guidelines.md b/elements/rh-table/docs/20-guidelines.md new file mode 100644 index 0000000000..fa9e930061 --- /dev/null +++ b/elements/rh-table/docs/20-guidelines.md @@ -0,0 +1,149 @@ +## Usage + +A table is a set of data that can be easily scanned and compared. Each row in a table represents an item and each cell of that row is an attribute of that item. This means that cells in a particular column will be the same data type such as dates, numerals, text, etc. Ideally, there should be one value per cell. If there is more than one piece of information within a cell, use another component. + +### Number of columns or rows + +There is no maximum number of columns or rows. To reduce cognitive load and a cluttered user interface, set a `max-width` or `max-height` after five or six of each. + +{% example palette="light", + alt="Image of table with a section of columns and rows highlighted", + src="../table-usage-columns-and-rows.png" %} + +### Padding + +In some edge cases, table rows can have double padding if there are more element types than just text. + +{% example palette="light", + alt="Image of two tables, one with default vertical padding and the other one with double vertical padding", + src="../table-usage-padding.png" %} + +## Writing content + +### Column and row titles + +Titles should be concise, scannable, and descriptive of content in the column or row. Header labels should have two or three words maximum. If more words are included, the label might break to a second line. + +{% example palette="light", + alt="Image of two tables with examples of short and long column and row titles", + src="../table-content-column-row-titles.png" %} + +## Character count + +In general, header labels should be as short as possible. However, if columns have more width, more words can be added. + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Column countCharacter count (including spaces)
Two40 - 50
Four20 - 30
More than four10 - 20
+
+ + + +## Layout + +### Placement + +A table should be the same width as nearby blocks of content on the page. + +{% example palette="none", + alt="Image of examples of placeholder content and a table having the same width, one is wide and one is narrow", + src="../table-layout-placement.png" %} + +### Scrolling + +A table will scroll horizontally or vertically if content exceeds the max-width or max-height. + +{% example palette="light", + alt="Image of two tables, one with no scrolling and the other with scrolling columns and rows", + src="../table-layout-scrolling.png" %} + +### Logos + +Logos can be used in cells along with text if necessary. + +{% example palette="light", + alt="Image of table with logos and links among text", + src="../table-layout-logos.png" %} + +## Behavior + +### Column sorting + +Columns can be sorted in ascending or descending order. Sorting controls are located in the column headers and the icon indicates the current sorted state. A sorted table has three states: + +- Unsorted (both arrows visible) +- Sorted up (arrow pointing up) +- Sorted down (arrow pointing down) + + +{% example palette="light", + alt="Image of tables with various sorting options", + src="../table-behavior-sorting.png" %} + +## Responsive design + +### Large viewport sizes + +{% example palette="none", + alt="Image of table on large viewport sizes", + src="../table-viewport-sizes-large.png" %} + +### Small viewport sizes + +{% example palette="none", + alt="Image of table on small viewport sizes", + src="../table-viewport-sizes-small.png" %} + +## Best practices + +### One-column table + +A table should display at least two columns. + +{% example palette="wrong", + alt="Image of table with one column which is incorrect usage", + src="../table-best-practices-1.png" %} + +### Large cell height + +In some edge cases, a table can have large cell height if there are more element types than just text. + +{% example palette="wrong", + alt="Image of table with lots of vertical padding which is incorrect usage", + src="../table-best-practices-2.png" %} + +### Wrong size + +Do not use the small viewport size table on large viewports. + +{% example palette="wrong", + alt="Image of small viewport table used on a large viewport which is incorrect usage", + src="../table-best-practices-3.png" %} \ No newline at end of file diff --git a/elements/rh-table/docs/30-code.md b/elements/rh-table/docs/30-code.md new file mode 100644 index 0000000000..e09be7ebc5 --- /dev/null +++ b/elements/rh-table/docs/30-code.md @@ -0,0 +1,152 @@ +{% renderInstallation lightdomcss=true %}{% endrenderInstallation %} + +## Usage + +### Title + +Specify the title of the table using a `caption` element. + +```html + + + + +
+ Concerts +
+
+``` + +### Column highlighting + +To enable column highlighting, the `table` element must also include a `col` element for each column in the table, typically wrapped with a `colgroup`. + +```html + + + + + + + + + +
+ Concerts +
+
+``` + +### Sorting + +To enable sorting on a column, add an `rh-sort-button` as the last child of the `th` cell. + +```html + + + + + + + + + + + + + + + + +
+ Concerts +
DateEventVenue
+
+``` + +### Summary + +Additional information about the data in the table should be slotted into the `summary` slot after the `table` element. + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Concerts +
DateEventVenue
12 FebruaryWaltz with StraussMain Hall
24 MarchThe ObelisksWest Wing
14 AprilThe WhatMain Hall
+ Dates and venues subject to change. +
+``` + +## Example + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Concerts +
DateEventVenue
12 FebruaryWaltz with StraussMain Hall
24 MarchThe ObelisksWest Wing
14 AprilThe WhatMain Hall
+ Dates and venues subject to change. +
+ + +{% renderCodeDocs hideDescription=true %}{% endrenderCodeDocs %} diff --git a/elements/rh-table/docs/40-accessibility.md b/elements/rh-table/docs/40-accessibility.md new file mode 100644 index 0000000000..3d95eff5b5 --- /dev/null +++ b/elements/rh-table/docs/40-accessibility.md @@ -0,0 +1,88 @@ +## Keyboard interactions + +If a table is in a container that can receive keyboard focus (e.g., with a `tabindex="0"` attribute), then a user can place focus on the container and scroll the table horizontally or vertically using the arrow keys. + +{% example palette="light", + alt="Image of table with scrollbars and purple buttons showing keyboard navigation", + src="../table-a11y-keyboard-navigation.png" %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyResult
Up ArrowMoves the table view up
Left ArrowMoves the table view left
Right ArrowMoves the table view right
Down ArrowMoves the table view down
TabMoves focus to next interactive element within a cell or outside of the table
Shift+TabMoves focus to previous interactive element within a cell or outside of the table
+
+ + + +## Focus order + +A logical focus order helps keyboard users operate our websites and apps. Elements need to receive focus in an order that preserves meaning, therefore the focus order should make sense and not jump around randomly. Focus within a table moves from top to bottom and left to right. + +{% example palette="light", + alt="Image of table with links, focus indicators, and numbers showing the focus order", + src="../table-a11y-focus-order.png" %} + +## Touch targets + +Each cell includes enough spacing for selecting interactive elements. + +{% example palette="light", + alt="Image of table with links and focus indicators showing touch target size", + src="../table-a11y-touch-targets.png" %} + +## Additional guidelines + +- No column title cells should be blank +- Column titles must use `` elements with `scope="col"` attributes +- Each cell should only have one piece of data +- Do not place multiple inactive elements in a single cell + +{% include 'accessibility/ariaguide.md' %} + +{% include 'accessibility/wcag.md' %} +{% include 'accessibility/2.1.1-A.md' %} +{% include 'accessibility/2.1.3-AAA.md' %} +{% include 'accessibility/2.4.3-A.md' %} +{% include 'accessibility/2.5.5-AAA.md' %} \ No newline at end of file diff --git a/elements/rh-table/docs/screenshot.png b/elements/rh-table/docs/screenshot.png new file mode 100644 index 0000000000..dceac1cd79 Binary files /dev/null and b/elements/rh-table/docs/screenshot.png differ diff --git a/elements/rh-table/docs/table-a11y-focus-order.png b/elements/rh-table/docs/table-a11y-focus-order.png new file mode 100755 index 0000000000..c84eb4c352 Binary files /dev/null and b/elements/rh-table/docs/table-a11y-focus-order.png differ diff --git a/elements/rh-table/docs/table-a11y-keyboard-navigation.png b/elements/rh-table/docs/table-a11y-keyboard-navigation.png new file mode 100755 index 0000000000..b2d2eea9cb Binary files /dev/null and b/elements/rh-table/docs/table-a11y-keyboard-navigation.png differ diff --git a/elements/rh-table/docs/table-a11y-touch-targets.png b/elements/rh-table/docs/table-a11y-touch-targets.png new file mode 100755 index 0000000000..f9ce286314 Binary files /dev/null and b/elements/rh-table/docs/table-a11y-touch-targets.png differ diff --git a/elements/rh-table/docs/table-anatomy.png b/elements/rh-table/docs/table-anatomy.png new file mode 100755 index 0000000000..9dca3e97f0 Binary files /dev/null and b/elements/rh-table/docs/table-anatomy.png differ diff --git a/elements/rh-table/docs/table-behavior-sorting.png b/elements/rh-table/docs/table-behavior-sorting.png new file mode 100755 index 0000000000..b6ac59cfce Binary files /dev/null and b/elements/rh-table/docs/table-behavior-sorting.png differ diff --git a/elements/rh-table/docs/table-best-practices-1.png b/elements/rh-table/docs/table-best-practices-1.png new file mode 100755 index 0000000000..c30399525a Binary files /dev/null and b/elements/rh-table/docs/table-best-practices-1.png differ diff --git a/elements/rh-table/docs/table-best-practices-2.png b/elements/rh-table/docs/table-best-practices-2.png new file mode 100755 index 0000000000..ab59884418 Binary files /dev/null and b/elements/rh-table/docs/table-best-practices-2.png differ diff --git a/elements/rh-table/docs/table-best-practices-3.png b/elements/rh-table/docs/table-best-practices-3.png new file mode 100644 index 0000000000..32c08dda19 Binary files /dev/null and b/elements/rh-table/docs/table-best-practices-3.png differ diff --git a/elements/rh-table/docs/table-column-row-titles.png b/elements/rh-table/docs/table-column-row-titles.png new file mode 100755 index 0000000000..810b6dd707 Binary files /dev/null and b/elements/rh-table/docs/table-column-row-titles.png differ diff --git a/elements/rh-table/docs/table-content-column-row-titles.png b/elements/rh-table/docs/table-content-column-row-titles.png new file mode 100755 index 0000000000..4eac11f5dc Binary files /dev/null and b/elements/rh-table/docs/table-content-column-row-titles.png differ diff --git a/elements/rh-table/docs/table-interaction-state-focus.png b/elements/rh-table/docs/table-interaction-state-focus.png new file mode 100755 index 0000000000..edaf92f781 Binary files /dev/null and b/elements/rh-table/docs/table-interaction-state-focus.png differ diff --git a/elements/rh-table/docs/table-interaction-state-hover.png b/elements/rh-table/docs/table-interaction-state-hover.png new file mode 100755 index 0000000000..d63cf85e7d Binary files /dev/null and b/elements/rh-table/docs/table-interaction-state-hover.png differ diff --git a/elements/rh-table/docs/table-layout-logos.png b/elements/rh-table/docs/table-layout-logos.png new file mode 100755 index 0000000000..e7254ab8d2 Binary files /dev/null and b/elements/rh-table/docs/table-layout-logos.png differ diff --git a/elements/rh-table/docs/table-layout-placement.png b/elements/rh-table/docs/table-layout-placement.png new file mode 100755 index 0000000000..74890e75d0 Binary files /dev/null and b/elements/rh-table/docs/table-layout-placement.png differ diff --git a/elements/rh-table/docs/table-layout-scrolling.png b/elements/rh-table/docs/table-layout-scrolling.png new file mode 100755 index 0000000000..cf103af114 Binary files /dev/null and b/elements/rh-table/docs/table-layout-scrolling.png differ diff --git a/elements/rh-table/docs/table-sample-element.png b/elements/rh-table/docs/table-sample-element.png new file mode 100755 index 0000000000..820bb98f50 Binary files /dev/null and b/elements/rh-table/docs/table-sample-element.png differ diff --git a/elements/rh-table/docs/table-scrolling.png b/elements/rh-table/docs/table-scrolling.png new file mode 100755 index 0000000000..4448c5b100 Binary files /dev/null and b/elements/rh-table/docs/table-scrolling.png differ diff --git a/elements/rh-table/docs/table-space.png b/elements/rh-table/docs/table-space.png new file mode 100755 index 0000000000..813d763c15 Binary files /dev/null and b/elements/rh-table/docs/table-space.png differ diff --git a/elements/rh-table/docs/table-title-caption.png b/elements/rh-table/docs/table-title-caption.png new file mode 100755 index 0000000000..6b7477192e Binary files /dev/null and b/elements/rh-table/docs/table-title-caption.png differ diff --git a/elements/rh-table/docs/table-usage-columns-and-rows.png b/elements/rh-table/docs/table-usage-columns-and-rows.png new file mode 100755 index 0000000000..51f08666ec Binary files /dev/null and b/elements/rh-table/docs/table-usage-columns-and-rows.png differ diff --git a/elements/rh-table/docs/table-usage-padding.png b/elements/rh-table/docs/table-usage-padding.png new file mode 100755 index 0000000000..4e9fb86aa3 Binary files /dev/null and b/elements/rh-table/docs/table-usage-padding.png differ diff --git a/elements/rh-table/docs/table-viewport-sizes-large.png b/elements/rh-table/docs/table-viewport-sizes-large.png new file mode 100755 index 0000000000..48bd535bad Binary files /dev/null and b/elements/rh-table/docs/table-viewport-sizes-large.png differ diff --git a/elements/rh-table/docs/table-viewport-sizes-small.png b/elements/rh-table/docs/table-viewport-sizes-small.png new file mode 100755 index 0000000000..66036bda52 Binary files /dev/null and b/elements/rh-table/docs/table-viewport-sizes-small.png differ diff --git a/elements/rh-table/rh-sort-button.css b/elements/rh-table/rh-sort-button.css new file mode 100644 index 0000000000..6ad6ed1f57 --- /dev/null +++ b/elements/rh-table/rh-sort-button.css @@ -0,0 +1,25 @@ +#sort-button { + background-color: transparent; + border: 0; +} + +#sort-button:after { + content: ""; + position: absolute; + inset: 0; + cursor: pointer; +} + +#sort-button #sort-indicator { + color: currentcolor; +} + +.visually-hidden { + position: fixed; + top: 0; + left: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} \ No newline at end of file diff --git a/elements/rh-table/rh-sort-button.ts b/elements/rh-table/rh-sort-button.ts new file mode 100644 index 0000000000..49375b206c --- /dev/null +++ b/elements/rh-table/rh-sort-button.ts @@ -0,0 +1,76 @@ +import { LitElement, html, svg } from 'lit'; +import { customElement } from 'lit/decorators/custom-element.js'; +import { property } from 'lit/decorators/property.js'; +import styles from './rh-sort-button.css'; +import { ComposedEvent } from '@patternfly/pfe-core'; + +const DIRECTIONS_OPPOSITES = { asc: 'desc', desc: 'asc' } as const; + +export class RequestSortEvent extends ComposedEvent { + constructor(public direction: 'asc' | 'desc') { + super('request-sort', { + cancelable: true, + }); + } +} + +const paths = new Map(Object.entries({ + asc: 'M279 224H41c-21.4 0-32.1-25.9-17-41L143 64c9.4-9.4 24.6-9.4 33.9 0l119 119c15.2 15.1 4.5 41-16.9 41z', + desc: 'M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41z', + sort: 'M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41zm255-105L177 64c-9.4-9.4-24.6-9.4-33.9 0L24 183c-15.1 15.1-4.4 41 17 41h238c21.4 0 32.1-25.9 17-41z', +})); + +/** + * Table sort button + * + * @csspart sort-button - button element + * @csspart sort-indicator - icon wrapper element + * + * @fires {RequestSortEvent} request-sort - when the button is clicked + */ +@customElement('rh-sort-button') +export class RhSortButton extends LitElement { + static readonly styles = [styles]; + + /** The button's sorting order */ + @property({ + reflect: true, + attribute: 'sort-direction', + }) sortDirection?: 'asc' | 'desc'; + + /** The column name associated with this button (for screen readers) */ + @property() column?: string; + + render() { + return html` + + `; + } + + /** + * Dispatch a request-sort event in ascending (asc) or descending (desc) order + */ + sort() { + const next = DIRECTIONS_OPPOSITES[this.sortDirection ?? 'asc']; + this.dispatchEvent(new RequestSortEvent(next)); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'rh-sort-button': RhSortButton; + } +} diff --git a/elements/rh-table/rh-table-lightdom.css b/elements/rh-table/rh-table-lightdom.css new file mode 100644 index 0000000000..bd48f4f352 --- /dev/null +++ b/elements/rh-table/rh-table-lightdom.css @@ -0,0 +1,150 @@ +/* stylelint-disable max-line-length */ +:is(rh-table) table { + min-width: 100%; + margin: 0 auto; + table-layout: fixed; + border: 0; + border-collapse: collapse; + + --rh-table-row-border: var(--rh-border-width-sm, 1px) solid var(--rh-color-border-subtle-on-light, #c7c7c7); + --rh-table-row-background-color: rgb(var(--rh-color-gray-10-rgb, 224 224 224) / var(--rh-opacity-40, 40%)); + --rh-table-column-background-color: rgb(var(--rh-color-blue-50-rgb, 231 241 250)); +} + +:is(rh-table) thead th { + position: relative; + padding-top: var(--rh-space-lg, 16px); + padding-bottom: var(--rh-space-lg, 16px); + text-align: left; + font-weight: var(--rh-font-weight-heading-bold, 700); +} + +:is(rh-table) tr { + border-bottom: var(--rh-table-row-border); +} + +:is(rh-table) tr:hover { + background: var(--rh-table-row-background-color); +} + +:is(rh-table) tr > * { + border: none; +} + +:is(rh-table) :is(tr, col){ + transition: background .3s ease-out; +} + +:is(rh-table) a { + color: var(--rh-color-interactive-blue-darker, #0066cc); + text-decoration: none +} + +:is(rh-table) a:hover { + color: var(--rh-color-interactive-blue-darkest, #004080); + text-decoration: underline +} + +:is(rh-table) caption { + font-family: var(--rh-font-family-body-text, RedHatText, "Red Hat Text", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans JP", "Noto Sans KR", "Noto Sans Malayalam", "Noto Sans SC", "Noto Sans TC", "Noto Sans Thai", Helvetica, Arial, sans-serif); + font-size: var(--rh-font-size-body-text-lg, 1.125rem); + font-weight: var(--rh-font-weight-heading-bold, 700); + line-height: var(--rh-line-height-body-text, 1.5); + margin-bottom: var(--rh-space-xl, 24px); + text-align: center; + font-style: normal; +} + +:is(rh-table) :is(th, td) { + padding-right: var(--rh-space-lg, 16px); + padding-left: var(--rh-space-lg, 16px); +} + +:is(rh-table) td { + padding-top: var(--rh-space-xl, 24px); + padding-bottom: var(--rh-space-xl, 24px); +} + +:is(rh-table) :is(col.active) { + background: var(--rh-table-column-background-color); +} + +@media (max-width: 768px) { + :is(rh-table) table { + display: grid; + } + + :is(rh-table) thead { + display: none; + visibility: hidden; + } + + :is(rh-table) tbody { + display: block; + } + + :is(rh-table) :not(thead) ~ tbody tr { + display: grid; + grid-auto-columns: auto; + grid-auto-flow: column; + } + + :is(rh-table) thead ~ tbody tr { + border: none; + display: grid; + grid-template-columns: 1fr; + height: auto; + grid-auto-columns: max-content; + grid-auto-flow: unset; + } + + :is(rh-table) thead ~ tbody tr:first-child { + border-top: var(--rh-table-row-border); + } + + :is(rh-table) thead ~ tbody tr:last-child { + border-bottom: var(--rh-table-row-border); + } + + :is(rh-table) thead ~ tbody tr:nth-child(even) { + background: var(--rh-table-row-background-color); + } + + :is(rh-table) thead ~ tbody tr:hover { + background: var(--rh-table-column-background-color); + } + + :is(rh-table) thead ~ tbody tr > * { + padding: var(--rh-space-lg, 16px); + } + + :is(rh-table) thead ~ tbody tr th { + text-align: center; + } + + :is(rh-table) thead ~ tbody tr td { + padding-top: calc(var(--rh-space-md, 8px) + var(--rh-space-xs, 4px)); + padding-bottom: calc(var(--rh-space-md, 8px) + var(--rh-space-xs, 4px)); + display: grid; + grid-column-gap: var(--rh-space-lg, 16px); + grid-template-columns: 1fr minmax(0, 1.5fr); + align-items: start; + white-space: normal; + word-wrap: break-word; + } + + :is(rh-table) thead ~ tbody tr td:before { + font-weight: var(--rh-font-weight-heading-bold, 700); + text-align: left; + content: attr(data-label); + display: inline-block; + } + + :is(rh-table) thead ~ tbody tr:first-child td:first-child { + padding-top: var(--rh-space-lg, 16px); + } + + :is(rh-table) thead ~ tbody tr:last-child td:last-child { + padding-bottom: var(--rh-space-lg, 16px); + } +} \ No newline at end of file diff --git a/elements/rh-table/rh-table.css b/elements/rh-table/rh-table.css new file mode 100644 index 0000000000..0edd431043 --- /dev/null +++ b/elements/rh-table/rh-table.css @@ -0,0 +1,46 @@ +* { + box-sizing: border-box; +} + +:host { + position: relative; + display: block; + width: 100%; + height: 100%; + overflow: auto; + scrollbar-color: var(--_scrollbar-track-color) var(--_scrollbar-thum-color); + + --_scrollbar-size: calc(10 / 16 * 1rem); + --_scrollbar-thumb-color: var(--rh-color-gray-40, #707070); + --_scrollbar-track-color: var(--rh-color-border-subtle-on-light, #c7c7c7) +} + +:host::-webkit-scrollbar { + width: var(--_scrollbar-size); + height: var(--_scrollbar-size); +} + +:host::-webkit-scrollbar, +:host::-webkit-scrollbar-track { + background-color: var(--_scrollbar-track-color); +} + +:host::-webkit-scrollbar-thumb { + background-color: var(--_scrollbar-thumb-color); +} + +[slot] { + display: block; +} + +/* @todo: should I move these styles to light dom css file? */ +::slotted([slot="summary"]) { + display: block; + padding: var(--rh-space-xl, 24px) var(--rh-space-lg, 16px) 0 var(--rh-space-lg, 16px); + color: var(--rh-color-text-secondary-on-light, #4d4d4d); + font-family: var(--rh-font-family-body-text, RedHatText, "Red Hat Text", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans JP", "Noto Sans KR", "Noto Sans Malayalam", "Noto Sans SC", "Noto Sans TC", "Noto Sans Thai", Helvetica, Arial, sans-serif); + font-size: var(--rh-font-size-body-text-md, 1rem); + font-style: italic; + font-weight: var(--rh-font-weight-body-text-regular, 400); + line-height: var(--rh-line-height-body-text, 1.5); +} \ No newline at end of file diff --git a/elements/rh-table/rh-table.ts b/elements/rh-table/rh-table.ts new file mode 100644 index 0000000000..7d174ba3ff --- /dev/null +++ b/elements/rh-table/rh-table.ts @@ -0,0 +1,175 @@ +import { LitElement, html } from 'lit'; +import { customElement } from 'lit/decorators/custom-element.js'; + +import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; + +import styles from './rh-table.css'; +import { RequestSortEvent, RhSortButton } from './rh-sort-button.js'; + +/** + * A table is a container for displaying information. It allows a user to scan, examine, and compare large amounts of data. + * + * @summary Organizes and displays information from a data set + * + * @slot - an HTML table + * @slot summary - a brief description of the data + */ +@customElement('rh-table') +export class RhTable extends LitElement { + static readonly styles = [styles]; + + private static getNodeContentForSort( + columnIndexToSort: number, + node: Element, + ) { + const content = node.querySelector(` + :is(th, td):nth-child(${columnIndexToSort + 1}), + tr > :is(th, td):nth-child(${columnIndexToSort + 1}) + `.trim())?.textContent?.trim()?.toLowerCase() ?? ''; + return { node, content }; + } + + private static sortByContent( + direction: 'asc' | 'desc', + a: { content: string }, + b: { content: string }, + ) { + if (direction === 'asc') { + return (a.content < b.content ? -1 : a.content > b.content ? 1 : 0); + } else { + return (b.content < a.content ? -1 : b.content > a.content ? 1 : 0); + } + } + + get #table(): HTMLTableElement | undefined { + return this.querySelector('table') as HTMLTableElement | undefined; + } + + get #cols(): NodeListOf | undefined { + return this.querySelectorAll('col') as NodeListOf | undefined; + } + + get #rows(): NodeListOf | undefined { + return this.querySelectorAll('tbody > tr') as NodeListOf | undefined; + } + + get #summary(): HTMLElement | undefined { + return this.querySelector('[slot="summary"]') as HTMLElement | undefined; + } + + #logger = new Logger(this); + + connectedCallback() { + super.connectedCallback(); + this.#init(); + } + + render() { + return html` + + + `; + } + + #onPointerleave() { + if (!this.#cols) { + return; + } + + this.#cols.forEach(col => col.classList.remove('active')); + } + + #onPointerover(event: PointerEvent) { + if (!this.#cols) { + return; + } + + let { target } = event; + + if (!(target instanceof Element)) { + return; + } + + if (!['td', 'th'].includes(target.tagName)) { + const ancestorCell = target.closest('td, th'); + if (ancestorCell) { + target = ancestorCell; + } else { + return; + } + } + + event.preventDefault(); + + this.#cols.forEach((col, index) => { + const { cellIndex } = target as HTMLTableCellElement; + col.classList.toggle('active', index === cellIndex); + }); + } + + #init() { + if (this.#table && this.#summary) { + this.#table.setAttribute('aria-describedby', 'summary'); + } + } + + #onSlotChange() { + this.#init(); + } + + #onRequestSort(event: Event) { + if (event instanceof RequestSortEvent) { + for (const button of this.querySelectorAll('rh-sort-button')) { + const header = button.closest('th'); + if (button === event.target) { + header?.setAttribute('aria-sort', `${event.direction}ending`); + } else { + button.removeAttribute('sort-direction'); + header?.removeAttribute('aria-sort'); + } + } + if (!event.defaultPrevented && event.target instanceof RhSortButton) { + event.target.sortDirection = event.direction; + this.#performSort(event.target, event.direction); + } + } + } + + // @todo: should we move the remaining methods into a controller to share with pf-table? + #performSort(button: RhSortButton, direction: 'asc' | 'desc') { + const header = button.closest('th'); + const children = header?.parentElement?.children; + if (children) { + const columnIndexToSort = [...children].indexOf(header); + + if (!this.#rows) { + this.#logger.warn('Could not perform sort: no rows found'); + return; + } + + Array + .from(this.#rows, node => RhTable.getNodeContentForSort(columnIndexToSort, node)) + .sort((a, b) => RhTable.sortByContent(direction, a, b)) + .forEach(({ node }, index) => { + if (!this.#rows) { + return; + } + const target = this.#rows[index]; + if (this.#rows[index] !== node) { + const position: InsertPosition = + direction === 'desc' ? 'afterend' : 'beforebegin'; + target.insertAdjacentElement(position, node); + } + }); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + 'rh-table': RhTable; + } +} diff --git a/elements/rh-table/test/rh-table.e2e.ts b/elements/rh-table/test/rh-table.e2e.ts new file mode 100644 index 0000000000..c2c1c587f2 --- /dev/null +++ b/elements/rh-table/test/rh-table.e2e.ts @@ -0,0 +1,12 @@ +import { test } from '@playwright/test'; +import { PfeDemoPage } from '@patternfly/pfe-tools/test/playwright/PfeDemoPage.js'; + +const tagName = 'rh-table'; + +test.describe(tagName, () => { + test('snapshot', async ({ page }) => { + const componentPage = new PfeDemoPage(page, tagName); + await componentPage.navigate(); + await componentPage.snapshot(); + }); +}); diff --git a/elements/rh-table/test/rh-table.spec.ts b/elements/rh-table/test/rh-table.spec.ts new file mode 100644 index 0000000000..740a84c181 --- /dev/null +++ b/elements/rh-table/test/rh-table.spec.ts @@ -0,0 +1,325 @@ +import { expect, html, fixture } from '@open-wc/testing'; +import { a11ySnapshot, type A11yTreeSnapshot } from '@patternfly/pfe-tools/test/a11y-snapshot.js'; +import { RhTable } from '@rhds/elements/rh-table/rh-table.js'; +import { sendKeys } from '@web/test-runner-commands'; +import { oneEvent } from '@open-wc/testing'; + +const takeProps = (props: string[]) => (obj: object) => + Object.fromEntries(Object.entries(obj).filter(([k]) => props.includes(k))); + +function press(key: string) { + return async function() { + await sendKeys({ press: key }); + }; +} + +describe('', async function() { + let element: RhTable; + + /** create a simple test fixture */ + async function setupSimpleInstance() { + element = await fixture(html``); + } + + /** create a test fixture with a slotted table and sortable columns */ + async function setupInstanceWithSlottedTable() { + element = await fixture(html` + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ The Jackson 5 +
NumbersLettersWords
1AYou
2BAnd
3CMe
+
+ `); + } + + /** Wait on the element's update cycle */ + async function updateComplete() { + await element.updateComplete; + } + + /** Asserts that an aXe audit on the page passes */ + async function expectA11yAxe() { + await expect(element).to.be.accessible(); + } + + /** + * Assert that the accessibility tree reports the expected snapshot + * If the expected children snapshot is undefined, then assistive technology + * reports nothing at all + */ + function expectA11ySnapshot(expected?: Pick[]) { + return async function() { + const snapshot = await a11ySnapshot(); + expect(snapshot.children?.map(takeProps(['name', 'role']))).to.deep.equal(expected); + }; + } + + /** + * Catch request sort event and prevent default + */ + function preventDefaultOnSort() { + element.querySelector('table').addEventListener('request-sort', (event: Event) => event.preventDefault()); + } + + describe('simply instantiating', function() { + beforeEach(setupSimpleInstance); + it('should upgrade', async function() { + const klass = customElements.get('rh-table'); + expect(element).to.be.an.instanceOf(klass).and.to.be.an.instanceOf(RhTable); + }); + it('should be accessible', expectA11yAxe); + it('imperatively instantiates', function() { + expect(document.createElement('rh-table')).to.be.an.instanceof(RhTable); + }); + it('should not report anything to assistive technology', expectA11ySnapshot()); + }); + + describe('with sortable columns', async () => { + /** Setup the a11y tree snapshot expected results for this suite */ + const snapshots = { + default: [ + { + name: 'The Jackson 5', + role: 'text', + }, + { + name: 'Numbers', + role: 'text', + }, + { + name: 'Letters', + role: 'text', + }, + { + name: 'Sort', + role: 'button', + }, + { + name: 'Words', + role: 'text', + }, + { + name: 'Sort', + role: 'button', + }, + { + name: '1', + role: 'text', + }, + { + name: 'A', + role: 'text', + }, + { + name: 'You', + role: 'text', + }, + { + name: '2', + role: 'text', + }, + { + name: 'B', + role: 'text', + }, + { + name: 'And', + role: 'text', + }, + { + name: '3', + role: 'text', + }, + { + name: 'C', + role: 'text', + }, + { + name: 'Me', + role: 'text', + }, + ], + ['sort-by-words-asc']: [ + { + name: 'The Jackson 5', + role: 'text', + }, + { + name: 'Numbers', + role: 'text', + }, + { + name: 'Letters', + role: 'text', + }, + { + name: 'Sort', + role: 'button', + }, + { + name: 'Words', + role: 'text', + }, + { + name: 'Sort', + role: 'button', + }, + { + name: '1', + role: 'text', + }, + { + name: 'A', + role: 'text', + }, + { + name: 'You', + role: 'text', + }, + { + name: '3', + role: 'text', + }, + { + name: 'C', + role: 'text', + }, + { + name: 'Me', + role: 'text', + }, + { + name: '2', + role: 'text', + }, + { + name: 'B', + role: 'text', + }, + { + name: 'And', + role: 'text', + }, + ], + ['sort-by-words-desc']: [ + { + name: 'The Jackson 5', + role: 'text', + }, + { + name: 'Numbers', + role: 'text', + }, + { + name: 'Letters', + role: 'text', + }, + { + name: 'Sort', + role: 'button', + }, + { + name: 'Words', + role: 'text', + }, + { + name: 'Sort', + role: 'button', + }, + { + name: '2', + role: 'text', + }, + { + name: 'B', + role: 'text', + }, + { + name: 'And', + role: 'text', + }, + { + name: '3', + role: 'text', + }, + { + name: 'C', + role: 'text', + }, + { + name: 'Me', + role: 'text', + }, + { + name: '1', + role: 'text', + }, + { + name: 'A', + role: 'text', + }, + { + name: 'You', + role: 'text', + }, + ], + }; + beforeEach(setupInstanceWithSlottedTable); + it('should be accessible', expectA11yAxe); + describe('tabbing to the second sort button', function() { + beforeEach(updateComplete); + beforeEach(press('Tab')); + beforeEach(press('Tab')); + beforeEach(updateComplete); + describe('when default not prevented', function() { + describe('and pressing Enter', function() { + beforeEach(updateComplete); + beforeEach(press('Enter')); + beforeEach(updateComplete); + it('should sort by ascending order by default', expectA11ySnapshot(snapshots['sort-by-words-asc'])); + describe('and pressing Enter again', function() { + beforeEach(press('Enter')); + it('should sort by descending order', expectA11ySnapshot(snapshots['sort-by-words-desc'])); + }); + }); + }); + describe('when default prevented', async function() { + beforeEach(updateComplete); + beforeEach(preventDefaultOnSort); + beforeEach(updateComplete); + describe('and pressing Enter', function() { + beforeEach(updateComplete); + beforeEach(press('Enter')); + beforeEach(updateComplete); + it('should not sort', expectA11ySnapshot(snapshots.default)); + }); + }); + }); + }); +});