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

fix(PPDSC-2661): add controlled prop to Select #540

Merged
merged 12 commits into from
Jan 23, 2023
6 changes: 6 additions & 0 deletions site/pages/components/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ const commonPropsRows = [
type: '(value:boolean):void',
description: `Callback fired when the select panel opens or close with value of true/false`,
},
{
name: 'controlled',
type: 'boolean',
description: `Force Select component to be controlled`,
default: 'false',
},
];

const commonOverridesRows = [
Expand Down
249 changes: 249 additions & 0 deletions src/select/__tests__/__snapshots__/select.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,254 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Select force select to controlled tate 1`] = `
<DocumentFragment>
.emotion-0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center;
box-sizing: border-box;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
width: 100%;
min-height: 48px;
margin-bottom: 8px;
background-color: #F1F1F1;
border-style: solid;
border-color: transparent;
border-width: 1px;
border-radius: 8px;
color: #3B3B3B;
text-overflow: ellipsis;
outline-color: #3768FB;
outline-style: solid;
outline-width: 2px;
}

.emotion-0 svg {
fill: #3B3B3B;
}

@media not all and (min-resolution: 0.001dpcm) {
@supports (-webkit-appearance: none) and (stroke-color: transparent) {
.emotion-0 {
outline-style: auto;
}
}
}

.emotion-1 {
display: block;
clip: rect(0 0 0 0);
-webkit-clip-path: inset(100%);
clip-path: inset(100%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
margin: -1px;
}

.emotion-2 {
all: unset;
-webkit-box-flex: 1;
-webkit-flex-grow: 1;
-ms-flex-positive: 1;
flex-grow: 1;
width: 100%;
box-sizing: border-box;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
justify-content: space-between;
overflow: hidden;
cursor: pointer;
padding: 8px;
}

.emotion-3 {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #3B3B3B;
font-family: "DM Sans",sans-serif;
font-size: 14px;
line-height: 1.5;
font-weight: 400;
letter-spacing: 0;
}

.emotion-4 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
width: 100%;
overflow: hidden;
box-sizing: border-box;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
justify-content: space-between;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: pointer;
}

.emotion-4 * {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

.emotion-5 {
-webkit-align-self: center;
-ms-flex-item-align: center;
align-self: center;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
margin-right: 8px;
}

.emotion-6 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-column-gap: 8px;
column-gap: 8px;
cursor: pointer;
}

.emotion-7 {
all: unset;
}

.emotion-8 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}

.emotion-9 {
display: inline-block;
vertical-align: middle;
overflow: hidden;
fill: #3B3B3B;
vertical-align: unset;
display: inline-block;
}

@media screen and (prefers-reduced-motion: no-preference) {
.emotion-9 {
transition-property: fill;
transition-duration: 200ms;
transition-timing-function: cubic-bezier(0, 0, .5, 1);
}
}

@media screen and (prefers-reduced-motion: reduce) {
.emotion-9 {
transition-property: fill;
transition-duration: 0ms;
transition-timing-function: cubic-bezier(0, 0, .5, 1);
}
}

.emotion-9.emotion-9 {
width: 24px;
height: 24px;
}

<div
class="emotion-0"
>
<span
class="emotion-1"
>
1
</span>
<button
aria-expanded="false"
aria-haspopup="listbox"
class="emotion-2"
data-testid="select-button"
id="downshift-0-toggle-button"
type="button"
value="1"
>
<div
class="emotion-3"
>
<div
class="emotion-4"
value="1"
>
<div>
option 1
</div>
</div>
</div>
</button>
<div
class="emotion-5"
>
<div
class="emotion-6"
>
<button
aria-hidden="true"
class="emotion-7"
data-testid="select-chevron-button"
tabindex="-1"
type="button"
>
<div
class="emotion-8"
>
<svg
aria-hidden="true"
class="emotion-9 emotion-10"
fill="currentColor"
focusable="false"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 0h24v24H0V0z"
fill="none"
/>
<path
d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"
/>
</svg>
</div>
</button>
</div>
</div>
</div>
</DocumentFragment>
`;

exports[`Select icon component overrides 1`] = `
<DocumentFragment>
.emotion-0 {
Expand Down
37 changes: 37 additions & 0 deletions src/select/__tests__/select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,43 @@ describe('Select', () => {
expect(fragment).toMatchSnapshot();
});

const TestSelectControlledComponent = () => {
const [selected, setSelected] = React.useState<string>('');

return (
<Select
controlled
onChange={v => {
setSelected(v.target.value);
}}
>
{['1', '2'].map(v => (
<SelectOption key={v} value={v} selected={v === selected}>
option {v}
</SelectOption>
))}
</Select>
);
};

test('force select to controlled tate', async () => {
const {getByTestId, getAllByRole, asFragment} = renderWithTheme(
TestSelectControlledComponent,
);

await waitFor(() => {
fireEvent.click(getByTestId('select-button'));
});

await waitFor(() => {
fireEvent.click(getAllByRole('option')[0]);
});

// this test throw error without controlled prop

expect(asFragment()).toMatchSnapshot();
});

describe('in Modal', () => {
afterEach(() => {
cleanup();
Expand Down
32 changes: 23 additions & 9 deletions src/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const ThemelessSelect = React.forwardRef<HTMLInputElement, SelectProps>(
eventContext = {},
eventOriginator = 'select',
onOpenChange,
// force select in controlled mode
controlled = false,
...restProps
} = props;

Expand All @@ -64,10 +66,14 @@ const ThemelessSelect = React.forwardRef<HTMLInputElement, SelectProps>(
[onFocus],
);

const programmaticallySelectedItem = children.find(
const childrenArray = React.Children.toArray(
children,
) as React.ReactElement<SelectOptionProps>[];

const programmaticallySelectedItem = childrenArray.find(
option => option.props.selected,
);
const defaultSelectedItem = children.find(
const defaultSelectedItem = childrenArray.find(
option => option.props.defaultSelected,
);

Expand Down Expand Up @@ -126,6 +132,18 @@ const ThemelessSelect = React.forwardRef<HTMLInputElement, SelectProps>(
item?.props?.['aria-label'] || item?.props?.value;
/* eslint-enable @typescript-eslint/no-explicit-any */

const getSelectedItem = () => {
if (programmaticallySelectedItem) {
return {selectedItem: programmaticallySelectedItem};
}

if (controlled) {
return {selectedItem: programmaticallySelectedItem || null};
}

return {};
};

const {
isOpen,
selectedItem,
Expand All @@ -135,7 +153,7 @@ const ThemelessSelect = React.forwardRef<HTMLInputElement, SelectProps>(
openMenu,
closeMenu,
} = useSelect({
items: children,
items: childrenArray,
defaultSelectedItem,
onSelectedItemChange: onInputChange,
itemToString,
Expand All @@ -158,15 +176,11 @@ const ThemelessSelect = React.forwardRef<HTMLInputElement, SelectProps>(

return changes;
},
...(programmaticallySelectedItem
? {selectedItem: programmaticallySelectedItem}
: {}),
...getSelectedItem(),
});

const {children: optionsAsChildren, scrollToIndex} = useVirtualizedList({
items: React.Children.toArray(
children,
) as React.ReactElement<SelectOptionProps>[],
items: childrenArray,
listRef: panelRef,
getItemProps,
limit: virtualized,
Expand Down
2 changes: 2 additions & 0 deletions src/select/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export interface SelectProps extends CommonInputProps, EventData {
overrides?: SelectPropsOverrides;
virtualized?: number;
onOpenChange?: (value: boolean) => void;
// force select in controlled mode
controlled?: boolean;
}

export interface SelectOptionProps {
Expand Down