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

Components: Replace/old custom select control with v2 legacy adapter #61272

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cc17f9f
Replace old `CustomSelectControl` select control with the V2 legacy a…
fullofcaffeine Apr 18, 2024
33bb57d
Fix option handling logic for legacy adater and update test to reflec…
fullofcaffeine May 10, 2024
f2c059c
Remove debug code
fullofcaffeine May 11, 2024
0e83151
Debug why selectedItem is not selected on mount
fullofcaffeine May 14, 2024
db8379a
Ensure correct item is selected upon mount based on the value passed …
fullofcaffeine May 15, 2024
751b295
Add new test case to make sure initial passed value is selected upon …
fullofcaffeine May 16, 2024
d18f88d
CSS tweaks
fullofcaffeine May 16, 2024
218cc67
More CSS adjustments to make the component behave closer to classic
fullofcaffeine May 16, 2024
ee619f0
Remove unused prop
fullofcaffeine May 16, 2024
ac751d9
Fix experimentalHint custom styling on legacy adapter
fullofcaffeine May 17, 2024
0db8e28
Fix width of popover not always matching the width of the select
fullofcaffeine May 17, 2024
98a0fc4
Improve export name
fullofcaffeine May 17, 2024
e88b0c4
Fix label margin not applying in certain contexts
fullofcaffeine May 18, 2024
d2cfd1d
Add a wrapper around the select as it the main container needs to hav…
fullofcaffeine May 18, 2024
e47adcf
Fix withHint handling
fullofcaffeine May 18, 2024
e38a4c8
Fix tests
fullofcaffeine May 18, 2024
17f6bbd
Revert accidental change
fullofcaffeine May 18, 2024
cd537af
Make the select item text non-selectable
fullofcaffeine May 21, 2024
fd5c051
Merge branch 'trunk' into replace/old-custom-select-control-with-v2-l…
fullofcaffeine May 28, 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
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function _CustomSelect(
} = props;

return (
<>
<Styled.SelectWrapper>
{ hideLabelFromVision ? ( // TODO: Replace with BaseControl
<VisuallyHidden as="label">{ label }</VisuallyHidden>
) : (
Expand All @@ -116,7 +116,7 @@ function _CustomSelect(
</CustomSelectContext.Provider>
</Styled.SelectPopover>
</InputBase>
</>
</Styled.SelectWrapper>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
*/
// eslint-disable-next-line no-restricted-imports
import * as Ariakit from '@ariakit/react';

/**
* WordPress dependencies
*/
import { useEffect } from '@wordpress/element';
/**
* Internal dependencies
*/
Expand Down Expand Up @@ -34,24 +37,33 @@ function CustomSelectControl( props: LegacyCustomSelectProps ) {
// Executes the logic in a microtask after the popup is closed.
// This is simply to ensure the isOpen state matches that in Downshift.
await Promise.resolve();

const state = store.getState();

const option = options.find( ( item ) => item.name === nextValue );

const changeObject = {
highlightedIndex: state.renderedItems.findIndex(
highlightedIndex: state.items.findIndex(
( item ) => item.value === nextValue
),
inputValue: '',
isOpen: state.open,
selectedItem: {
Copy link
Member Author

@fullofcaffeine fullofcaffeine May 1, 2024

Choose a reason for hiding this comment

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

The old object here didn't include any custom attributes that might was part of the original options array passed to the component. Not sure we should instead pass these values through the Ariakit's API (e.g use value to store the style data for example), but selecting the option by name is a (hacky) workaround that works for now.

Copy link
Member Author

Choose a reason for hiding this comment

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

cc @mirka

Copy link
Member Author

@fullofcaffeine fullofcaffeine May 11, 2024

Choose a reason for hiding this comment

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

OK, after spending more time with the codebase for V2/LegacyAdapter and the V1 component, I think I grasped what was wrong here. There were two issues:

  1. The selectedItem object had its name and key both set to the nextValue argument passed to the setValue callback, resulting in both having the name attribute from the options array. This didn't look right.
  2. The same structure was missing the style metadata (style and/or className) that was considered to be implicitly available in the item passed to the onChange callback in V1, which some consumers expect.

I've fixed both in 56461bd, but I'm wondering if this is the right approach and if should we keep the same API in V2 (as in keep the style metadata in the item passed to onChange?), or should we refactor the consumers to not rely on this self-contained data and instead move it to a store somewhere else? Of course, this would be work to be done in follow-up PRs.

See the test / comment here (I'll simplify or remove that comment before merging).

name: nextValue as string,
key: nextValue as string,
},
selectedItem: option!,
type: '',
};

onChange( changeObject );
},
} );

useEffect( () => {
// This is a workaround for selecting the right item upon mount
if ( value ) {
store.setValue( value.name );
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [] );

const children = options.map(
( { name, key, __experimentalHint, ...rest } ) => {
const withHint = (
Expand Down Expand Up @@ -121,4 +133,15 @@ function CustomSelectControl( props: LegacyCustomSelectProps ) {
);
}

export default CustomSelectControl;
export function ClassicCustomSelectControlV2Adapter(
props: LegacyCustomSelectProps
) {
return (
<CustomSelectControl
__experimentalShowSelectedHint={ false }
{ ...props }
/>
);
}

export default ClassicCustomSelectControlV2Adapter;
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import { useState } from '@wordpress/element';
*/
import UncontrolledCustomSelectControl from '..';

const customClass = 'amber-skies';
const className = 'amber-skies';
const style = {
backgroundColor: 'rgb(127, 255, 212)',
rotate: '13deg',
};

const legacyProps = {
label: 'label!',
Expand All @@ -26,7 +30,7 @@ const legacyProps = {
{
key: 'flower2',
name: 'crimson clover',
className: customClass,
className,
},
{
key: 'flower3',
Expand All @@ -35,15 +39,18 @@ const legacyProps = {
{
key: 'color1',
name: 'amber',
className: customClass,
className,
},
{
key: 'color2',
name: 'aquamarine',
style: {
backgroundColor: 'rgb(127, 255, 212)',
rotate: '13deg',
},
style,
},
{
key: 'aquarela-key',
name: 'aquarela',
className,
style,
},
],
};
Expand All @@ -53,7 +60,9 @@ const ControlledCustomSelectControl = ( {
onChange,
...restProps
}: React.ComponentProps< typeof UncontrolledCustomSelectControl > ) => {
const { value: overrideValue } = restProps;
const [ value, setValue ] = useState( options[ 0 ] );
const initialValue = overrideValue ?? value;
return (
<UncontrolledCustomSelectControl
{ ...restProps }
Expand All @@ -63,7 +72,7 @@ const ControlledCustomSelectControl = ( {
setValue( args.selectedItem );
} }
value={ options.find(
( option: any ) => option.key === value.key
( option: any ) => option.key === initialValue.key
) }
/>
);
Expand Down Expand Up @@ -148,7 +157,7 @@ describe.each( [
// assert against filtered array
itemsWithClass.map( ( { name } ) =>
expect( screen.getByRole( 'option', { name } ) ).toHaveClass(
customClass
className
)
);

Expand All @@ -160,7 +169,7 @@ describe.each( [
// assert against filtered array
itemsWithoutClass.map( ( { name } ) =>
expect( screen.getByRole( 'option', { name } ) ).not.toHaveClass(
customClass
className
)
);
} );
Expand Down Expand Up @@ -286,7 +295,7 @@ describe.each( [
expect.objectContaining( {
inputValue: '',
isOpen: false,
selectedItem: { key: 'violets', name: 'violets' },
selectedItem: { key: 'flower1', name: 'violets' },
Copy link
Member Author

@fullofcaffeine fullofcaffeine May 10, 2024

Choose a reason for hiding this comment

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

The fact it had the same key and name looked like a bug caused by the previous logic here.

type: '',
} )
);
Expand All @@ -302,6 +311,7 @@ describe.each( [
expect.objectContaining( {
inputValue: '',
isOpen: false,

selectedItem: expect.objectContaining( {
name: 'aquamarine',
} ),
Expand All @@ -328,7 +338,7 @@ describe.each( [
await type( 'p' );
await press.Enter();

expect( mockOnChange ).toHaveReturnedWith( 'poppy' );
expect( mockOnChange ).toHaveReturnedWith( 'flower1' );
} );

describe( 'Keyboard behavior and accessibility', () => {
Expand Down Expand Up @@ -457,4 +467,57 @@ describe.each( [
).toBeVisible();
} );
} );

// V1 styles items via a `style` or `className` metadata property in the option item object. Some consumers still expect it, e.g:
//
// - https://github.com/WordPress/gutenberg/blob/trunk/packages/block-editor/src/components/font-appearance-control/index.js#L216
//
// Returning these properties as part of the item object was not tested as part of the V1 test. Possibly this was an accidental API?
// or was it intentional? If intentional, we might need to implement something similar in V2, too? The alternative is to rely on the
// `key` attriute for the item and get the actual data from some dictionary in a store somewhere, which would require refactoring
// consumers that rely on the self-contained `style` and `className` attributes.
it( 'Should return style metadata as part of the selected option from onChange', async () => {
const mockOnChange = jest.fn();

render( <Component { ...legacyProps } onChange={ mockOnChange } /> );

await click(
screen.getByRole( 'combobox', {
expanded: false,
} )
);

await click(
screen.getByRole( 'option', {
name: 'aquarela',
} )
);

expect( mockOnChange ).toHaveBeenCalledWith(
expect.objectContaining( {
selectedItem: expect.objectContaining( {
className,
style,
} ),
} )
);
} );

it( 'Should display the initial value passed as the selected value', async () => {
const initialSelectedItem = legacyProps.options[ 5 ];

const testProps = {
...legacyProps,
value: initialSelectedItem,
};

render( <Component { ...testProps } /> );

const currentSelectedItem = await screen.findByRole( 'combobox', {
expanded: false,
} );

// Verify the initial selected value
expect( currentSelectedItem ).toHaveTextContent( 'aquarela' );
} );
} );
12 changes: 12 additions & 0 deletions packages/components/src/custom-select-control-v2/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ import type { CustomSelectButtonSize } from './types';

const ITEM_PADDING = space( 2 );

export const SelectWrapper = styled.div`
display: block;
`;

export const WithHintWrapper = styled.div`
display: flex;
justify-content: space-between;
flex: 1;
flex-wrap: wrap;
`;

export const SelectedExperimentalHintItem = styled.span`
Expand All @@ -37,6 +42,7 @@ export const SelectLabel = styled( Ariakit.SelectLabel )`
line-height: 1.4;
text-transform: uppercase;
margin-bottom: ${ space( 2 ) };
display: block;
`;

export const Select = styled( Ariakit.Select, {
Expand Down Expand Up @@ -96,6 +102,11 @@ export const SelectPopover = styled( Ariakit.SelectPopover )`
border-radius: 2px;
background: ${ COLORS.theme.background };
border: 1px solid ${ COLORS.theme.foreground };
z-index: 9999; // Ensure the popover is on top
position: absolute !important;
max-height: 400px;
overflow: auto;
min-width: 100%;

&[data-focus-visible] {
outline: none; // outline will be on the trigger, rather than the popover
Expand All @@ -109,6 +120,7 @@ export const SelectItem = styled( Ariakit.SelectItem )`
padding: ${ ITEM_PADDING };
font-size: ${ CONFIG.fontSize };
line-height: 2.15rem; // TODO: Remove this in default but keep for back-compat in legacy
user-select: none;
&[data-active-item] {
background-color: ${ COLORS.theme.gray[ 300 ] };
}
Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ export {
useCompositeState as __unstableUseCompositeState,
} from './composite';
export { ConfirmDialog as __experimentalConfirmDialog } from './confirm-dialog';
export { StableCustomSelectControl as CustomSelectControl } from './custom-select-control';
export { ClassicCustomSelectControlV2Adapter as CustomSelectControl } from './custom-select-control-v2/legacy-component';
export { default as CustomSelectControlV2 } from './custom-select-control-v2';
export { default as Dashicon } from './dashicon';
export { default as DateTimePicker, DatePicker, TimePicker } from './date-time';
export { default as __experimentalDimensionControl } from './dimension-control';
Expand Down
Loading