Skip to content

Commit

Permalink
fix(PPDSC-2661): add controlled prop to Select (#540)
Browse files Browse the repository at this point in the history
* fix(PPDSC-2661): add controlled prop to Select

* fix(PPDSC-2661): add unit tests

* fix(PPDSC-2661): fix types

* fix(PPDSC-2661): fix docs

* fix(PPDSC-2661): fix types

* fix(PPDSC-2661): remove story

* fix(PPDSC-2661): fix typo
  • Loading branch information
mutebg authored Jan 23, 2023
1 parent 58da12d commit 6f1cca2
Show file tree
Hide file tree
Showing 5 changed files with 317 additions and 9 deletions.
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

0 comments on commit 6f1cca2

Please sign in to comment.