Skip to content

Commit

Permalink
Merge pull request #209 from US-CBP/feature/pagination
Browse files Browse the repository at this point in the history
Feature/pagination
  • Loading branch information
dgibson666 authored Oct 4, 2024
2 parents c9d37af + 61597e1 commit 2922e74
Show file tree
Hide file tree
Showing 15 changed files with 694 additions and 166 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const CbpIcon = /*@__PURE__*/createReactComponent<JSX.CbpIcon, HTMLCbpIco
export const CbpLink = /*@__PURE__*/createReactComponent<JSX.CbpLink, HTMLCbpLinkElement>('cbp-link');
export const CbpList = /*@__PURE__*/createReactComponent<JSX.CbpList, HTMLCbpListElement>('cbp-list');
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');
export const CbpSection = /*@__PURE__*/createReactComponent<JSX.CbpSection, HTMLCbpSectionElement>('cbp-section');
export const CbpSegmentedButtonGroup = /*@__PURE__*/createReactComponent<JSX.CbpSegmentedButtonGroup, HTMLCbpSegmentedButtonGroupElement>('cbp-segmented-button-group');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,18 @@ export class CbpDropdownItem {
host: this.host,
target: target,
label: label,
value: (this.value) ? this.value : label
value: (!!this.value) ? this.value : label
});
//console.log('Dropdown Item Click: ', this.value, (!!this.value) ? this.value : label);
}
//this.selected=true; delegate this to the parent level because we don't know if this is single or multiselect here
}

@Watch('selected')
watchSelected(newValue) {
//console.log('Selected Watch fired in dropdown-item: ', this.host);
if (this.checkbox) this.checkbox.checked=newValue; // sync a slotted checkbox (if any) with the selected state
if (newValue && this.parent.open) this.host.focus(); // If the dropdown is open, send focus to the selected dropdown item (not its children)
if (newValue && this.parent?.open) this.host.focus(); // If the dropdown is open, send focus to the selected dropdown item (not its children)
}

handleKeyUp(e) {
Expand All @@ -49,13 +51,14 @@ export class CbpDropdownItem {
}
}


componentWillLoad() {
this.parent=this.host.closest('cbp-dropdown');
this.checkbox = this.host.querySelector('input[type=checkbox]');
}

componentDidLoad() {
if (this.selected) this.checkbox.checked=true;
if (this.selected && this.checkbox) this.checkbox.checked=true;
}

render() {
Expand All @@ -64,7 +67,7 @@ export class CbpDropdownItem {
role="option"
tabindex={-1}
onClick={ (e) => this.handleClick(e)}
onKeyDown={e => this.handleKeyUp(e)}
onKeyDown={ (e) => this.handleKeyUp(e)}
aria-selected={this.selected ? "true" : "false"}
>
<div class="cbp-dropdown-item-content">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@

--cbp-dropdown-menu-color-counter-outline-focus: var(--cbp-color-white);
--cbp-dropdown-menu-color-counter-outline-focus-dark: var(--cbp-form-field-color-border);

// These are used by the component and are not intended to be used by consumers
--cbp-dropdown-attached-button-start-width: 0;
--cbp-dropdown-attached-button-end-width: 0;
}

/*
Expand All @@ -53,12 +57,56 @@
--cbp-dropdown-menu-color-border: var(--cbp-dropdown-menu-color-border-dark);
--cbp-dropdown-menu-color-counter: var(--cbp-dropdown-menu-color-counter-dark);
--cbp-dropdown-menu-color-bg-counter: var(--cbp-dropdown-menu-color-bg-counter-dark);
--cbp-dropdown-menu-color-counter-outline-focus: var(--cbp-dropdown-menu-color-counter-outline-focus-dark)
--cbp-dropdown-menu-color-counter-outline-focus: var(--cbp-dropdown-menu-color-counter-outline-focus-dark);

--cbp-form-field-select-chevron: var(--cbp-form-field-select-chevron-dark);
}

cbp-dropdown {
display: block;

&:has([slot="cbp-dropdown-attached-button-start"]),
&:has([slot="cbp-dropdown-attached-button-end"]) {
--cbp-form-field-select-chevron: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 320 512" fill="%231b1b1b"><path d="M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z"/></svg>');
--cbp-form-field-select-chevron-dark: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 320 512" fill="%23fcfcfc"><path d="M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z"/></svg>');

.cbp-custom-form-control {
background: right calc(var(--cbp-dropdown-attached-button-end-width) + 1.125rem - 8px) top calc(1rem - 8px) no-repeat var(--cbp-form-field-select-chevron),
var(--cbp-form-field-color-bg);
}
}

.cbp-dropdown-shrinkwrap {
position: relative;
display: block;
flex-basis: 100%; // for child flex context

// Override the input padding based on overlay size to prevent input text from being obscured (text may still be obscured if there's not enough space for it)
.cbp-custom-form-control {
padding-inline-start: calc(var(--cbp-dropdown-attached-button-start-width) + var(--cbp-form-field-padding-inline));
padding-inline-end: calc(var(--cbp-dropdown-attached-button-end-width) + var(--cbp-form-field-padding-inline) + var(--cbp-space-9x));
}

// All named slots within the shrinkwrap element are overlays
[slot] {
position: absolute;
inset-block-start: 0; // Needed for Firefox
}

// Attached buttons act like an overlay in order to wrap the input focus highlight around them.
[slot="cbp-dropdown-attached-button-start"] {
--cbp-button-border-radius: var(--cbp-border-radius-soft) 0 0 var(--cbp-border-radius-soft);
inset-inline-start: 0;
}

[slot="cbp-dropdown-attached-button-end"] {
--cbp-button-border-radius: 0 var(--cbp-border-radius-soft) var(--cbp-border-radius-soft) 0;
inset-inline-end: 0;
}

}


button {
appearance: none;
white-space: nowrap;
Expand All @@ -67,6 +115,7 @@ cbp-dropdown {
.cbp-dropdown-placeholder {
text-overflow: ellipsis;
overflow: hidden;
padding-inline-end: var(--cbp-space-2x);
}

.cbp-dropdown-placeholder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ The Dropdown component offers an alternative to the native select element that c
## Functional Requirements

* 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 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 would, providing:
* Form field styles without duplication
* An accessible form field pattern
* The Dropdown options will consist of slotted child `cbp-dropdown-item` components.
* Dropdown items may have individual values specified, separate from the visible label (similar to a native select option element).
* 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.

## Technical Specifications
Expand All @@ -39,14 +41,23 @@ The Dropdown component offers an alternative to the native select element that c
* 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).
* Activating a Dropdown Item will do the following:
* Emit an event, which is listened for by the parent Dropdown component
* 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).
* 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.
* Close the dropdown menu
* Return focus to the control
* 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.
* Emit a valueChange event from the parent Dropdown component.
* Close the dropdown menu.
* Return focus to the control.

* Activating a Dropdown Item will do the following for multi-select dropdowns:
* Emit an event, which is listened for by the parent Dropdown component.
* Toggle its `selected` value and checkbox's checked state.
* Update the Dropdown control to show the number of selected items.
* Update the value of the hidden input field to the value of the clicked Dropdown Item. For multi-select dropdowns, this field's value is an array of checkbox values of the selected items.
* Emit a valueChange event from the parent Dropdown component.
* The Dropdown remains open for additional navigation and selection.

### Responsiveness

Expand All @@ -69,8 +80,38 @@ The Dropdown component offers an alternative to the native select element that c
* 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?
* 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`.
* TODO: Dynamically shift menu opening top/bottom based on form field positioning in the viewport and viewport size.
* TODO: Implement faux placeholder value.
* TODO: How should Dropdown Items handle wrapping or multi-line text?

#### Dropdown logic flow:

* On Component Load:
* Look for selected children
* If there are selected items, set the component value, hidden form field value (via render), and display label
* If not, check if the `value` prop was set.
* If so, set the corresponding children as selected.
* Note: children must have a `value` property set for this to work (no guessing by text labels).
* For multi-select dropdowns, setting children as selected will also check the child checkbox.

* On User Interaction (a dropdown item is clicked/activated):
* The dropdown item emits an event (containing host, target, label, and value), that is listened for by the parent dropdown.
* If no value is explicitly set, the value passed is derived from the text label (similar to a native select option with no value).
* Toggling the selected state is deferred to the parent dropdown
* Parent dropdown receives the event
*

* 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.
* 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.
* TODO: This has not yet been implemented for multi-select dropdowns.



Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default {

function generateItems(buttons) {
const html = buttons.map(({ label, value, selected }) => {
return `<cbp-dropdown-item 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 @@ -141,14 +141,14 @@ SingleSelectDropdown.args = {

function generateMultiSelectItems(buttons, context) {
const html = buttons.map(({ label, name, value, selected }) => {
return `<cbp-dropdown-item value="${value}" ${selected == true ? 'selected' : ''}>
return `<cbp-dropdown-item ${value ? `value=${value}` : ''} ${selected == true ? 'selected' : ''}>
<cbp-checkbox
${context && context != 'light-inverts' ? `context=${context}` : ''}
>
<input
type="checkbox"
name="${name}"
value="${value}"
${value ? `value=${value}` : ''}
/>
${label}
</cbp-checkbox>
Expand Down
Loading

0 comments on commit 2922e74

Please sign in to comment.