Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a new InlineLabelSelect component for WCPay admin filter select inputs (with inline label) #8896

Merged
merged 25 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
612254c
Initial CustomSelectControl fork and styling
Jinksi Jun 3, 2024
dc153aa
Ensure custom-select-control styles are inherited
Jinksi Jun 3, 2024
1ac7c43
Update comment to mention the forked-from CustomSelectControl
Jinksi Jun 3, 2024
ab51a06
Show optional hints
Jinksi Jun 3, 2024
7ef73bd
Update hint styles
Jinksi Jun 4, 2024
af32550
Close dropdown by default
Jinksi Jun 4, 2024
008f406
Remove menu item borders
Jinksi Jun 4, 2024
1c0826c
update hover/focus bg color
Jinksi Jun 4, 2024
b5dd1ce
Merge branch 'develop' into add/8788-select-component-for-currency-sw…
Jinksi Jun 4, 2024
1f36782
Adjust border and focus shadow to match design
Jinksi Jun 4, 2024
e173789
Ensure label doesn't wrap
Jinksi Jun 4, 2024
3063ae8
Add more code comments
Jinksi Jun 4, 2024
f18110e
Update list item padding and border radius to match design
Jinksi Jun 4, 2024
6495acc
Update snapshot test to match changes
Jinksi Jun 4, 2024
92dd922
Merge branch 'develop' into add/8788-select-component-for-currency-sw…
Jinksi Jun 4, 2024
f0e8fcc
Add changelog entry
Jinksi Jun 4, 2024
c923579
Update FilterSelectControl tests
Jinksi Jun 4, 2024
b56b86b
Merge branch 'develop' into add/8788-select-component-for-currency-sw…
Jinksi Jun 4, 2024
70b2899
Rename `Props` → `ControlProps` to remain consistent with `CustomSele…
Jinksi Jun 5, 2024
00940a3
Rename `Item`/`ItemType` → `SelectItem`/`SelectItemType` to clarify m…
Jinksi Jun 5, 2024
bb70ee7
Merge branch 'develop' into add/8788-select-component-for-currency-sw…
jessy-p Jun 5, 2024
35a72ff
Merge branch 'develop' into add/8788-select-component-for-currency-sw…
jessy-p Jun 5, 2024
28715a3
Merge branch 'develop' into add/8788-select-component-for-currency-sw…
Jinksi Jun 5, 2024
a19f745
Merge branch 'develop' into add/8788-select-component-for-currency-sw…
Jinksi Jun 6, 2024
c84c9a5
Rename to `InlineLabelSelect` to clarify what component is and differ…
Jinksi Jun 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog/add-8788-select-component-for-currency-switcher
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Add new select component to be used for reporting filters, e.g. Payments overview currency select
250 changes: 250 additions & 0 deletions client/components/inline-label-select/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
/**
* This is a copy of Gutenberg's CustomSelectControl component, found here:
* https://github.com/WordPress/gutenberg/tree/7aa042605ff42bb437e650c39132c0aa8eb4ef95/packages/components/src/custom-select-control
*
* It has been forked from the existing WooPayments copy of this component (client/components/custom-select-control)
* to match this specific select input design with an inline label and option hints.
*/

/**
* External Dependencies
*/
import React from 'react';
import { Button } from '@wordpress/components';
import { check, chevronDown, Icon } from '@wordpress/icons';
import { useCallback } from '@wordpress/element';
import classNames from 'classnames';
import { __, sprintf } from '@wordpress/i18n';
import { useSelect, UseSelectState } from 'downshift';

/**
* Internal Dependencies
*/
import './style.scss';

export interface SelectItem {
/** The unique key for the item. */
key: string;
/** The display name of the item. */
name?: string;
/** Descriptive hint for the item, displayed to the right of the name. */
hint?: string;
/** Additional class name to apply to the item. */
className?: string;
/** Additional inline styles to apply to the item. */
style?: React.CSSProperties;
}

export interface ControlProps< SelectItemType > {
/** The name attribute for the select input. */
name?: string;
/** Additional class name to apply to the select control. */
className?: string;
/** The label for the select control. */
label: string;
/** The ID of an element that describes the select control. */
describedBy?: string;
/** A list of options/items for the select control. */
options: SelectItemType[];
/** The currently selected option/item. */
value?: SelectItemType | null;
/** A placeholder to display when no item is selected. */
placeholder?: string;
/** Callback function to run when the selected item changes. */
onChange?: ( changes: Partial< UseSelectState< SelectItemType > > ) => void;
/** A function to render the children of the item. Takes an item as an argument, must return a JSX element. */
children?: ( item: SelectItemType ) => JSX.Element;
}

/**
* Converts a select option/item object to a string.
*/
const itemToString = ( item: { name?: string } | null ) => item?.name || '';

/**
* State reducer for the select component.
* This is needed so that in Windows, where the menu does not necessarily open on
* key up/down, you can still switch between options with the menu closed.
*/
const stateReducer = (
{ selectedItem }: any,
{ type, changes, props: { items } }: any
) => {
switch ( type ) {
case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowDown:
// If we already have a selected item, try to select the next one,
// without circular navigation. Otherwise, select the first item.
return {
selectedItem:
items[
selectedItem
? Math.min(
items.indexOf( selectedItem ) + 1,
items.length - 1
)
: 0
],
};
case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowUp:
// If we already have a selected item, try to select the previous one,
// without circular navigation. Otherwise, select the last item.
return {
selectedItem:
items[
selectedItem
? Math.max( items.indexOf( selectedItem ) - 1, 0 )
: items.length - 1
],
};
default:
return changes;
}
};

/**
* InlineLabelSelect component.
* A select control with a list of options, inline label, and option hints.
*/
function InlineLabelSelect< ItemType extends SelectItem >( {
name,
className,
label,
describedBy,
options: items,
onChange: onSelectedItemChange,
value,
placeholder,
children,
}: ControlProps< ItemType > ): JSX.Element {
const {
getLabelProps,
getToggleButtonProps,
getMenuProps,
getItemProps,
isOpen,
highlightedIndex,
selectedItem,
} = useSelect( {
initialSelectedItem: items[ 0 ],
items,
itemToString,
onSelectedItemChange,
selectedItem: value || ( {} as ItemType ),
stateReducer,
} );

const itemString = itemToString( selectedItem );

function getDescribedBy() {
if ( describedBy ) {
return describedBy;
}

if ( ! itemString ) {
return __( 'No selection' );
}

// translators: %s: The selected option.
return sprintf( __( 'Currently selected: %s' ), itemString );
}

const menuProps = getMenuProps( {
className: 'wcpay-filter components-custom-select-control__menu',
'aria-hidden': ! isOpen,
} );

const onKeyDownHandler = useCallback(
( e ) => {
e.stopPropagation();
menuProps?.onKeyDown?.( e );
},
[ menuProps ]
);

// We need this here, because the null active descendant is not fully ARIA compliant.
if (
menuProps[ 'aria-activedescendant' ]?.startsWith( 'downshift-null' )
) {
delete menuProps[ 'aria-activedescendant' ];
}
return (
<div
className={ classNames(
'wcpay-filter components-custom-select-control',
className
) }
>
<Button
{ ...getToggleButtonProps( {
// This is needed because some speech recognition software don't support `aria-labelledby`.
'aria-label': label,
'aria-labelledby': undefined,
'aria-describedby': getDescribedBy(),
className: classNames(
'wcpay-filter components-custom-select-control__button',
{ placeholder: ! itemString }
),
name,
} ) }
>
{
/* eslint-disable-next-line jsx-a11y/label-has-associated-control, jsx-a11y/label-has-for */
<label
{ ...getLabelProps( {
className:
'wcpay-filter components-custom-select-control__label',
} ) }
>
{ label }
</label>
}
<span className="wcpay-filter components-custom-select-control__button-value">
{ itemString || placeholder }
</span>
<Icon
icon={ chevronDown }
className="wcpay-filter components-custom-select-control__button-icon"
/>
</Button>
{ /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */ }
<ul { ...menuProps } onKeyDown={ onKeyDownHandler }>
{ isOpen &&
items.map( ( item, index ) => (
// eslint-disable-next-line react/jsx-key
<li
{ ...getItemProps( {
item,
index,
key: item.key,
className: classNames(
item.className,
'wcpay-filter components-custom-select-control__item',
{
'is-highlighted':
index === highlightedIndex,
}
),
style: item.style,
} ) }
>
<Icon
icon={ check }
className="wcpay-filter components-custom-select-control__item-icon"
visibility={
item === selectedItem ? 'visible' : 'hidden'
}
/>
{ children ? children( item ) : item.name }
{ item.hint && (
<span className="wcpay-filter components-custom-select-control__item-hint">
{ item.hint }
</span>
) }
</li>
) ) }
</ul>
</div>
);
}

export default InlineLabelSelect;
95 changes: 95 additions & 0 deletions client/components/inline-label-select/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
.wcpay-filter.components-custom-select-control {
font-size: 13px;
color: $gray-900;

.wcpay-filter.components-custom-select-control__label {
display: inline-block;
margin-bottom: 0;
color: $wp-gray-40;
margin-right: 0.2em;
white-space: nowrap;

&::after {
content: ':';
}
}

.wcpay-filter.components-custom-select-control__item {
padding: $gap-small;
margin: 0;
line-height: initial;
grid-template-columns: auto auto auto;
justify-content: start;
white-space: nowrap;
border-radius: 2px;

&.is-highlighted {
background: $gray-0;
}
}

.wcpay-filter.components-custom-select-control__item-icon {
margin-right: 0.2em;
}

.wcpay-filter.components-custom-select-control__item-hint {
margin-left: 1.8em;
text-align: left;
color: $gray-700;
}

button.wcpay-filter.components-custom-select-control__button {
width: 100%;
background-color: #fff;
margin: 0 1px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 8px;
border: 1px solid $gray-200;
font-size: 13px;

&:hover,
&:focus {
color: initial;
background-color: $gray-0;
box-shadow: none;
}

&.placeholder {
color: $gray-50;
}

.wcpay-filter.components-custom-select-control__button-value {
text-overflow: ellipsis;
white-space: nowrap;
overflow-x: hidden;
}

svg {
fill: initial;
width: 18px;
flex: 0 0 18px;
}

&[aria-expanded='true'] {
.wcpay-filter.components-custom-select-control__button-value {
visibility: hidden;
}
.components-custom-select-control__label::after {
visibility: hidden;
}
}

@media screen and ( max-width: 782px ) {
font-size: 16px;
}
}

.wcpay-filter.components-custom-select-control__menu {
margin: -1px 1px;
border-color: $gray-300;
max-height: 300px;
padding: $grid-unit-10 $grid-unit-15;
}
}
Loading
Loading