Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 37 additions & 0 deletions packages/components/src/custom-select/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* External dependencies
*/
// eslint-disable-next-line no-restricted-imports
import * as Ariakit from '@ariakit/react';

/**
* Internal dependencies
*/
import {
CustomSelectButton,
CustomSelectPopover,
CustomSelectItem as SelectItem,
} from './styles';
import { type CustomSelectProps } from './types';

export function CustomSelect( props: CustomSelectProps ) {
const { label, onChange, options } = props;

const store = Ariakit.useSelectStore( {
setValue: ( value ) => onChange?.( value ),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our current CustomSelectControl expects an object to be returned via the onChange event:

} = useSelect( {
initialSelectedItem: items[ 0 ],
items,
itemToString,
onSelectedItemChange,
...( typeof _selectedItem !== 'undefined' && _selectedItem !== null
? { selectedItem: _selectedItem }
: undefined ),

Which is managed by useSelect; part of the package downshift.

In Ariakit, setValue appears to be what we'd use in lieu of useSelect, but value/setValue expect a string (or an array of strings for multi-select).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears that the object passed to onSelectItemChange has the following structure:

{
  selectedItem: "Curium",
  type: "__item_click__",
  highlightedIndex: -1,
  isOpen: false,
  inputValue: "",
}

I'm not sure if all the elements in this structure are part of the public API. The type value, in particular, seems obscure. I'm also unsure about the meaning of highlightedIndex, as it always appears to be -1.

Regardless, everything except for type can be readily accessed from within the setValue callback. You can use store.getState() in this context as well:

const store = useSelectStore({
  async setValue(selectedItem) {
    if (!onChange) return;
    // Executes the logic in a microtask after the popup is closed. This might not be necessary.
    // This is simply to ensure the isOpen state matches that in Downshift.
    await Promise.resolve();
    const state = store.getState();
    const changeObject = {
      selectedItem,
      type: "__item_click__",
      highlightedIndex: state.renderedItems.findIndex((item) => item.value === selectedItem),
      isOpen: state.open,
      inputValue: "",
    };
    onChange(changeObject);
  },
});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this! We finally have gotten a chance to try this out. 🙂

} );

return (
<>
<Ariakit.SelectLabel store={ store }>{ label }</Ariakit.SelectLabel>
<CustomSelectButton store={ store } />
<CustomSelectPopover sameWidth store={ store }>
{ options?.map( ( { name, key, ...rest } ) => {
return (
<SelectItem value={ name } key={ key } { ...rest } />
);
} ) }
</CustomSelectPopover>
</>
);
}
59 changes: 59 additions & 0 deletions packages/components/src/custom-select/stories/index.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//@ts-nocheck
/**
* External dependencies
*/
import type { Meta, StoryFn } from '@storybook/react';

/**
* WordPress dependencies
*/
import { useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import { CustomSelect, CustomSelectItem } from '..';

const meta: Meta< typeof CustomSelect > = {
title: 'Components/CustomSelect',
component: CustomSelect,
argTypes: {},
parameters: {
controls: { expanded: true },
docs: { canvas: { sourceState: 'shown' } },
},
};
export default meta;

const Template: StoryFn = () => {
const options = [
{
key: 'small',
name: 'Small',
style: { fontSize: '50%' },
},
{
key: 'normal',
name: 'Normal',
style: { fontSize: '100%' },
},
{
key: 'large',
name: 'Large',
style: { fontSize: '200%' },
},
{
key: 'huge',
name: 'Huge',
style: { fontSize: '300%' },
},
];

return (
<>
<CustomSelect label="Font Size" options={ options } />
</>
);
};

export const Default = Template.bind( {} );
51 changes: 51 additions & 0 deletions packages/components/src/custom-select/styles.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To note, this is just temporary CSS added from the Ariakit example.

Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* External dependencies
*/
import styled from '@emotion/styled';
// eslint-disable-next-line no-restricted-imports
import * as Ariakit from '@ariakit/react';

/**
* Internal dependencies
*/
import { COLORS } from '../utils';
import { space } from '../utils/space';

export const CustomSelectButton = styled( Ariakit.Select )`
display: flex;
align-items: center;
justify-content: space-between;
gap: ${ space( 1 ) };
white-space: nowrap;
font-size: 1rem;
border-style: none;
border-radius: ${ space( 1 ) };
min-width: 250px;
height: auto;
margin-top: 1rem;
padding: ${ space( 4 ) };
background: ${ COLORS.white };
box-shadow: 0 0 0 var( --wp-admin-border-width-focus )
${ COLORS.gray[ 400 ] };
&:hover {
background-color: ${ COLORS.gray[ 100 ] };
}
`;

export const CustomSelectPopover = styled( Ariakit.SelectPopover )`
z-index: 50;
display: flex;
flex-direction: column;
border-radius: ${ space( 1 ) };
background: ${ COLORS.white };
box-shadow: 0 0 0 var( --wp-admin-border-width-focus )
${ COLORS.gray[ 400 ] };
`;
export const CustomSelectItem = styled( Ariakit.SelectItem )`
cursor: pointer;
padding: ${ space( 2 ) };
&:hover {
background-color: ${ COLORS.theme.accent };
color: ${ COLORS.white };
}
`;
17 changes: 17 additions & 0 deletions packages/components/src/custom-select/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export type CustomSelectProps = {
label?: string;
/**
* Function called with the control's internal state changes. The `selectedItem` property contains the next selected item.
*/
onChange?: Function;
/**
* The options that can be chosen from.
*/
options: Array< {
key: string;
name: string;
style?: {};
className?: string;
__experimentalHint?: string;
} >;
};