Skip to content

Commit

Permalink
Merge pull request #232 from US-CBP/feature/table
Browse files Browse the repository at this point in the history
Feature/table
  • Loading branch information
dgibson666 authored Dec 12, 2024
2 parents e8ad8bf + 252e78f commit ec576d9
Show file tree
Hide file tree
Showing 11 changed files with 385 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const CbpApp = /*@__PURE__*/createReactComponent<JSX.CbpApp, HTMLCbpAppEl
export const CbpAppHeader = /*@__PURE__*/createReactComponent<JSX.CbpAppHeader, HTMLCbpAppHeaderElement>('cbp-app-header');
export const CbpBadge = /*@__PURE__*/createReactComponent<JSX.CbpBadge, HTMLCbpBadgeElement>('cbp-badge');
export const CbpBanner = /*@__PURE__*/createReactComponent<JSX.CbpBanner, HTMLCbpBannerElement>('cbp-banner');
export const CbpBreadcrumb = /*@__PURE__*/createReactComponent<JSX.CbpBreadcrumb, HTMLCbpBreadcrumbElement>('cbp-breadcrumb');
export const CbpButton = /*@__PURE__*/createReactComponent<JSX.CbpButton, HTMLCbpButtonElement>('cbp-button');
export const CbpCard = /*@__PURE__*/createReactComponent<JSX.CbpCard, HTMLCbpCardElement>('cbp-card');
export const CbpCheckbox = /*@__PURE__*/createReactComponent<JSX.CbpCheckbox, HTMLCbpCheckboxElement>('cbp-checkbox');
Expand All @@ -35,6 +36,7 @@ export const CbpHide = /*@__PURE__*/createReactComponent<JSX.CbpHide, HTMLCbpHid
export const CbpIcon = /*@__PURE__*/createReactComponent<JSX.CbpIcon, HTMLCbpIconElement>('cbp-icon');
export const CbpLink = /*@__PURE__*/createReactComponent<JSX.CbpLink, HTMLCbpLinkElement>('cbp-link');
export const CbpList = /*@__PURE__*/createReactComponent<JSX.CbpList, HTMLCbpListElement>('cbp-list');
export const CbpNavItem = /*@__PURE__*/createReactComponent<JSX.CbpNavItem, HTMLCbpNavItemElement>('cbp-nav-item');
export const CbpNotice = /*@__PURE__*/createReactComponent<JSX.CbpNotice, HTMLCbpNoticeElement>('cbp-notice');
export const CbpPagination = /*@__PURE__*/createReactComponent<JSX.CbpPagination, HTMLCbpPaginationElement>('cbp-pagination');
export const CbpPanel = /*@__PURE__*/createReactComponent<JSX.CbpPanel, HTMLCbpPanelElement>('cbp-panel');
Expand All @@ -46,9 +48,11 @@ export const CbpStructuredList = /*@__PURE__*/createReactComponent<JSX.CbpStruct
export const CbpStructuredListItem = /*@__PURE__*/createReactComponent<JSX.CbpStructuredListItem, HTMLCbpStructuredListItemElement>('cbp-structured-list-item');
export const CbpTab = /*@__PURE__*/createReactComponent<JSX.CbpTab, HTMLCbpTabElement>('cbp-tab');
export const CbpTabPanel = /*@__PURE__*/createReactComponent<JSX.CbpTabPanel, HTMLCbpTabPanelElement>('cbp-tab-panel');
export const CbpTable = /*@__PURE__*/createReactComponent<JSX.CbpTable, HTMLCbpTableElement>('cbp-table');
export const CbpTabs = /*@__PURE__*/createReactComponent<JSX.CbpTabs, HTMLCbpTabsElement>('cbp-tabs');
export const CbpTag = /*@__PURE__*/createReactComponent<JSX.CbpTag, HTMLCbpTagElement>('cbp-tag');
export const CbpToast = /*@__PURE__*/createReactComponent<JSX.CbpToast, HTMLCbpToastElement>('cbp-toast');
export const CbpTooltip = /*@__PURE__*/createReactComponent<JSX.CbpTooltip, HTMLCbpTooltipElement>('cbp-tooltip');
export const CbpTypography = /*@__PURE__*/createReactComponent<JSX.CbpTypography, HTMLCbpTypographyElement>('cbp-typography');
export const CbpUniversalHeader = /*@__PURE__*/createReactComponent<JSX.CbpUniversalHeader, HTMLCbpUniversalHeaderElement>('cbp-universal-header');
export const CbpUsaBanner = /*@__PURE__*/createReactComponent<JSX.CbpUsaBanner, HTMLCbpUsaBannerElement>('cbp-usa-banner');
9 changes: 9 additions & 0 deletions packages/web-components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ This CHANGELOG.md tracks the updates to the web components package of the CBP de
The React components are wrappers generated from this package and will share the same changes.

## [unpublished] TBD
* First cut of the `cbp-nav-item` component for including navigation links in the Application Header.
* BREAKING: updated the `cbp-app-header` implementation. The pattern now uses the new `cbp-nav-item` component for the first "Application Name" link. Code should be updated from Storybook for the latest implementation.
* First cut of the `cbp-table` component.
* First cut of the `cbp-breadcrumb` component.
* First cut of the `cbp-tooltip` component.
* Major refactor of `cbp-dropdown`, to include:
* Cycling through options based on alphanumeric keypress by default, like a native `select`.
* Combobox functionality for filtering by search string, enabled with the `filter` property.
* Revamped the accessibility model using `aria-activedescendant` rather than sending focus to the dropdown items. Focus remains on the combobox control (button).
* Published the Change log to Storybook.
* Published Stencil-generated component API docs to Storybook. We will continually revisit these for completion.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ The Dropdown component offers an alternative to the native select element that c

* The Dropdown component can replace the native `select` element (with caveats*)
* The Dropdown component supports both single select and multi-select functionality.
* The Dropdown component supports type-ahead filtering of options (when enabled via property).
* The Dropdown component will render an `input type="hidden"` element to pass the field value in a native form submission.
* The Dropdown component is meant to be used in the default slot of the `cbp-form-field` component, like a native form field, providing:
* Form field styles without duplication
* An accessible form field pattern
* The Dropdown options will consist of slotted child `cbp-dropdown-item` components.
* For single select dropdowns, dropdown items may have individual values specified, separate from the visible label (similar to a native select option element).
* For multi-select dropdowns, a `cbp-checkbox` is expected to be slotted within each dropdown item and a native checkbox control within the `cbp-checkbox`.
* The Dropdown and dropdown items must support keyboard navigation similar to a native select or menu widget.
* The Dropdown supports keyboard navigation using the combobox accessibility paradigm.
* The single-select Dropdown may have default "placeholder" text specified when there is no selection.

## Technical Specifications
Expand All @@ -31,20 +32,31 @@ The Dropdown component offers an alternative to the native select element that c
* A disabled Dropdown does not submit a form value, as the corresponding hidden input is also disabled.
* Activating the Dropdown menu may be achieved by:
* Clicking on the Dropdown label or control.
* Placing focus on the Dropdown control and pressing `Enter`, `Space`, or `Down Arrow`.
* Upon activation of the Dropdown control, the dropdown menu will open and focus will automatically be moved to the selected element or the first available item (if none are selected).
* Placing focus on the Dropdown control and pressing `Enter`, `Space`, `Up Arrow`, or `Down Arrow`.
* Upon activation of the Dropdown control, the dropdown menu will open and visible focus will automatically be moved to the selected element or the first available item if none are selected.
* Keyboard navigation within the dropdown menu works as follows:
* `Arrow Down` and `Arrow Up` navigate the dropdown items, with wrapping at the start and end items.
* `Home` jumps to the first dropdown item.
* `End` jumps to the last dropdown item.
* Pressing `Escape` will close the menu and return focus to the dropdown control.
* Pressing `Tab` will close the menu and send focus to the next focusable element on the page.
* Clicking outside of the dropdown component will close the menu.
* Pressing `Enter` will activate the focused item and mark is as selected (see below for more details) and close the menu.

* Dropdown Items may be activated either by clicking with the mouse or pressing `Enter` on the focused item (same as a native `select` element).
* By default (without type-ahead filtering), pressing an alphanumeric character will jump to the first item starting with that character (non-case sensitive), mimicking the behavior of a native `select` element.
* Subsequent presses of the same character will cycle through items starting with that character.
* Any other keypress than the same character will interrupt this sequence and take appropriate action.
* When type-ahead filtering is enabled (via the `filter` property):
* Alphanumeric keypresses will be appended to the the search string and dropdown items filter by those that contain the search string.
* Using the arrows for keyboard navigation will move visible focus through the filtered list of options.
* The search string will be shown on the control.
* Backspace or Clear will remove a character from the search string.
* Closing the dropdown will clear the search string and filtering automatically.

* Dropdown Items may be activated either by clicking with the mouse or pressing `Enter` or `Spacebar`.
* Activating a Dropdown Item will do the following for single select dropdowns:
* Emit an event, which is listened for by the parent Dropdown component.
* Set the activated item as `selected` (true).
* Update the `aria-activedescendant` attribute on the control to reference the select item's `id`.
* Set the `selected` property for all other Dropdown Items to `false`.
* Update the visible selected value on the Dropdown control to that of the activated Dropdown Item.
* Update the value of the hidden input field to the value of the clicked Dropdown Item. If no value is explicitly set, then the item's `innerText` will be used as the value. For multi-select dropdowns, this field's value is an array of checkbox values of the selected items.
Expand All @@ -69,18 +81,20 @@ The Dropdown component offers an alternative to the native select element that c
### Accessibility

* The associated label and `aria-describedby` are provided by the parent `cbp-form-field` component.
* The Dropdown control is actually a button with a `role="combobox"` (for single select variant).
* The Dropdown menu has a `role="listbox"`.
* The Dropdown Items have `role="option"`.
* The selected Dropdown Item has `aria-selected="true"` while the others have `aria-selected="false"`.
* The component fully supports keyboard navigation, as described above in the User Interactions section.
* The Dropdown is implemented using the combobox pattern for accessibility, which means:
* The Dropdown control is actually a button with a `role="combobox"`.
* When the dropdown is opened, focus remains on the control, while the "current" option is indicated by the `aria-activedescendant` attribute on the control, which references the visibly highlighted option's `id`.
* The Dropdown menu has a `role="listbox"`.
* The Dropdown Items have `role="option"`.
* The selected Dropdown Item has `aria-selected="true"` while the others have `aria-selected="false"`. (is this relevant since focus is not sent to the options?)
* The component fully supports keyboard navigation, as described above in the User Interactions section.

### Additional Notes and Considerations

* While visually more consistent with the CBP Design System, the Dropdown component has some drawbacks compared to a native `select` element:
* A native `select` allows you to press the initial letter multiple times to cycle through those options that begin with that letter. This creates familiar interactions for common patterns such as State and Country selection where the lists are well-known.
* On mobile devices, a native `select` will be presented by an enlarged overlay of radio options, which is drastically more usable in this context.
* Since the main control is a button, it is disabled in "readonly" mode (while the hidden input may still submit the value). Does this cause issues in user expectations?
* For common patterns such as State and Country selection where the lists are well-known, do not enable search string filtering. Cycling through those options that begin with a letter by pressing that letter multiple times is a familiar interaction supported for many years by native select fields and search string filtering may be confusing and slower.
* The `value` property should not be set explicitly if there are items marked as selected. But in the case that this occurs, the selected items will override the specified value; the value will be updated to match the selected items.
* TODO: Requires additional testing with `cbp-form-field-wrapper`.

Expand Down Expand Up @@ -110,8 +124,8 @@ The Dropdown component offers an alternative to the native select element that c
* On Dropdown value property being updated externally (reactively):
* Check if the new value is different from the old value. (if not, do nothing)
* If so:
* select the specified dropdown item(s) based on value(s).
* Update the selectedItem reference.
* Select the specified dropdown item(s) based on value(s).
* Update the selectedItems reference.
* Update the dropdown's visible label (of the selected item).
* Update the focusIndex for tracking focus for keyboard navigation.
* For single-select dropdowns, deselect all other dropdown items.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default {

function generateItems(items) {
const html = items.map(({ label, value=label, selected }) => {
return `<cbp-dropdown-item ${value ? `value=${value}` : ''} ${selected == true ? 'selected' : ''}>${label}</cbp-dropdown-item>`;
return `<cbp-dropdown-item ${value ? `value="${value}"` : ''} ${selected == true ? 'selected' : ''}>${label}</cbp-dropdown-item>`;
});
return html.join('');
}
Expand Down Expand Up @@ -263,7 +263,7 @@ CountriesDropdown.args = {

function generateMultiSelectItems(items, context) {
const html = items.map(({ label, name, value, selected }) => {
return `<cbp-dropdown-item ${value ? `value=${value}` : ''} ${selected == true ? 'selected' : ''}>
return `<cbp-dropdown-item ${value ? `value="${value}"` : ''} ${selected == true ? 'selected' : ''}>
<cbp-checkbox
${context && context != 'light-inverts' ? `context=${context}` : ''}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export class CbpDropdown {


handleSlotChange(e) {
console.log('Dropdown Slot Change: ', e);
console.log('Dropdown Slot Change: ', e); // Testing: I don't believe this event fires with polyfilled slots.
}

handleCounterClick(e) {
Expand Down Expand Up @@ -279,7 +279,7 @@ export class CbpDropdown {
End: l,
}[key];
if (n !== undefined && key !== 'Tab') {
console.log('i',i,'l',l,'n',n)
//console.log('i',i,'l',l,'n',n)
this.matchIndex = n;
this.setCurrent( (this.filter && this.searchString) ? this.matches[n] : n, this.focusIndex);
if (!this.filter) this.searchString='';
Expand Down Expand Up @@ -335,7 +335,6 @@ export class CbpDropdown {
let matches=[];
this.dropdownItems.forEach( (item, index) => {
const label=item.innerText.toLowerCase();
console.log({label});
// does this item start with the character pressed?
if (label.startsWith(letter)) {
matches=[...matches, index];
Expand Down Expand Up @@ -384,7 +383,6 @@ export class CbpDropdown {
}

filterDropdownItems(matches){
console.log('filterDropdownItems: ', {matches});
this.dropdownItems.forEach( (item, index) => {
matches.includes(index) ? item.removeAttribute('hidden') : item.setAttribute('hidden','');
});
Expand Down Expand Up @@ -519,7 +517,6 @@ export class CbpDropdown {
}

componentWillRender() {
//console.log('Dropdown Component Will Render - how often is re-rendering happening?');
if (this.attachedButtonStart) this.attachedButtonStart.disabled=this.disabled || !this.dropdownItems.length;
if (this.attachedButtonEnd) this.attachedButtonEnd.disabled=this.disabled || !this.dropdownItems.length;
}
Expand Down
6 changes: 6 additions & 0 deletions packages/web-components/src/components/cbp-table/api-docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Meta, Markdown } from "@storybook/blocks";
import Docs from './readme.md?raw';

<Meta title="Components/Table/API Docs" />

<Markdown>{Docs}</Markdown>
144 changes: 144 additions & 0 deletions packages/web-components/src/components/cbp-table/cbp-table.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* @prop --cbp-table-color: var(--cbp-color-text-darkest);
* @prop --cbp-table-color-dark: var(--cbp-color-text-lightest);
* @prop --cbp-table-header-color-bg: var(--cbp-color-gray-cool-20);
* @prop --cbp-table-header-color-bg-dark: var(--cbp-color-gray-cool-60);
* @prop --cbp-table-row-color-bg: var(--cbp-color-white);
* @prop --cbp-table-row-color-bg-dark: var(--cbp-color-gray-cool-70);
* @prop --cbp-table-row-color-bg-striped: var(--cbp-color-gray-cool-4);
* @prop --cbp-table-row-color-bg-striped-dark: var(--cbp-color-gray-cool-80);
* @prop --cbp-table-row-color-bg-hover: var(--cbp-color-gray-cool-10);
* @prop --cbp-table-row-color-bg-hover-dark: var(--cbp-color-gray-cool-90);
* @prop --cbp-table-row-color-bg-selected: var(--cbp-color-interactive-selected-light);
* @prop --cbp-table-row-color-bg-selected-dark: var(--cbp-color-interactive-selected-dark);
* @prop --cbp-table-row-color-border: var(--cbp-color-gray-cool-30);
* @prop --cbp-table-row-color-border-dark: var(--cbp-color-gray-cool-50);
* @prop --cbp-table-row-border-size: var(--cbp-border-size-md);
*/
:root {
--cbp-table-color: var(--cbp-color-text-darkest);
--cbp-table-color-dark: var(--cbp-color-text-lightest);

--cbp-table-header-color-bg: var(--cbp-color-gray-cool-20);
--cbp-table-header-color-bg-dark: var(--cbp-color-gray-cool-60);

--cbp-table-row-color-bg: var(--cbp-color-white);
--cbp-table-row-color-bg-dark: var(--cbp-color-gray-cool-70);
--cbp-table-row-color-bg-striped: var(--cbp-color-gray-cool-4);
--cbp-table-row-color-bg-striped-dark: var(--cbp-color-gray-cool-80);
--cbp-table-row-color-bg-hover: var(--cbp-color-gray-cool-10);
--cbp-table-row-color-bg-hover-dark: var(--cbp-color-gray-cool-90);
--cbp-table-row-color-bg-selected: var(--cbp-color-interactive-selected-light);
--cbp-table-row-color-bg-selected-dark: var(--cbp-color-interactive-selected-dark);

--cbp-table-row-color-border: var(--cbp-color-gray-cool-30);
--cbp-table-row-color-border-dark: var(--cbp-color-gray-cool-50);
--cbp-table-row-border-size: var(--cbp-border-size-md);
}


/*
* Dark Mode - display dark design based on mode or context
*/
[data-cbp-theme=light] cbp-table[context*=dark],
[data-cbp-theme=dark] cbp-table:not([context=dark-inverts]):not([context=light-always]){
--cbp-table-color: var(--cbp-table-color-dark);

--cbp-table-header-color: var(--cbp-table-header-color-dark);
--cbp-table-header-color-bg: var(--cbp-table-header-color-bg-dark);

--cbp-table-row-color-bg: var(--cbp-table-row-color-bg-dark);
--cbp-table-row-color-bg-striped: var(--cbp-table-row-color-bg-striped-dark);
--cbp-table-row-color-bg-hover: var(--cbp-table-row-color-bg-hover-dark);
--cbp-table-row-color-bg-selected: var(--cbp-table-row-color-bg-selected-dark);

--cbp-table-row-color-border: var(--cbp-table-row-color-border-dark);
}

cbp-table {
display: block;
max-width: 100%;
overflow-x: auto;

// Striped rows
&[striped=odd] {
tbody {
tr:nth-child(odd) {
background-color: var(--cbp-table-row-color-bg-striped);
}
}
}

&[striped=even] {
tbody {
tr:nth-child(even) {
background-color: var(--cbp-table-row-color-bg-striped);
}
}
}


table {
margin: 0;
padding: 0;
max-width: 100%;
border-collapse: collapse;

caption {
text-align: left;
font-size: var(--cbp-font-size-heading-xl);
line-height: var(--cbp-line-height-xl);
}

th, td {
text-align: left;
padding: var(--cbp-space-3x) var(--cbp-space-4x);

// TechDebt: may need to do these sorts of overrides for additional components inside of tables and reevaluate where the selectors belong.
&:has(input[type=checkbox]) {
padding-block: 0;
}

cbp-checkbox {
--cbp-checkbox-min-height: 0;
--cbp-checkbox-margin: 0;
}

}

// thead, tfoot

tbody {
tr {
background-color: var(--cbp-table-row-color-bg);

&:has(input[type=checkbox]:checked) {
background-color: var(--cbp-table-row-color-bg-selected);
}

&:hover td {
background-color: var(--cbp-table-row-color-bg-hover);
}
}
}

tr {
border-bottom: solid var(--cbp-table-row-border-size) var(--cbp-table-row-color-border);
}

th {
background-color: var(--cbp-table-header-color-bg);
font-weight: var(--cbp-font-weight-medium);
}

td {
text-wrap: pretty;
}

}


}
Loading

0 comments on commit ec576d9

Please sign in to comment.