From 6f1cca2d2354eaf4a5240e9a1ae2d0f67c73f768 Mon Sep 17 00:00:00 2001 From: Stoyan Delev Date: Mon, 23 Jan 2023 16:50:56 +0200 Subject: [PATCH] fix(PPDSC-2661): add controlled prop to Select (#540) * 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 --- site/pages/components/select.tsx | 6 + .../__snapshots__/select.test.tsx.snap | 249 ++++++++++++++++++ src/select/__tests__/select.test.tsx | 37 +++ src/select/select.tsx | 32 ++- src/select/types.ts | 2 + 5 files changed, 317 insertions(+), 9 deletions(-) diff --git a/site/pages/components/select.tsx b/site/pages/components/select.tsx index 6525773665..dcfabbf3ee 100644 --- a/site/pages/components/select.tsx +++ b/site/pages/components/select.tsx @@ -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 = [ diff --git a/src/select/__tests__/__snapshots__/select.test.tsx.snap b/src/select/__tests__/__snapshots__/select.test.tsx.snap index 7c1f394c58..7aa4dcb253 100644 --- a/src/select/__tests__/__snapshots__/select.test.tsx.snap +++ b/src/select/__tests__/__snapshots__/select.test.tsx.snap @@ -1,5 +1,254 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Select force select to controlled tate 1`] = ` + + .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; +} + +
+ + 1 + + +
+
+ +
+
+
+
+`; + exports[`Select icon component overrides 1`] = ` .emotion-0 { diff --git a/src/select/__tests__/select.test.tsx b/src/select/__tests__/select.test.tsx index e41a2a674c..4da301f71f 100644 --- a/src/select/__tests__/select.test.tsx +++ b/src/select/__tests__/select.test.tsx @@ -694,6 +694,43 @@ describe('Select', () => { expect(fragment).toMatchSnapshot(); }); + const TestSelectControlledComponent = () => { + const [selected, setSelected] = React.useState(''); + + return ( + + ); + }; + + 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(); diff --git a/src/select/select.tsx b/src/select/select.tsx index fa8f2d7cd1..ef83d82f3e 100644 --- a/src/select/select.tsx +++ b/src/select/select.tsx @@ -44,6 +44,8 @@ const ThemelessSelect = React.forwardRef( eventContext = {}, eventOriginator = 'select', onOpenChange, + // force select in controlled mode + controlled = false, ...restProps } = props; @@ -64,10 +66,14 @@ const ThemelessSelect = React.forwardRef( [onFocus], ); - const programmaticallySelectedItem = children.find( + const childrenArray = React.Children.toArray( + children, + ) as React.ReactElement[]; + + const programmaticallySelectedItem = childrenArray.find( option => option.props.selected, ); - const defaultSelectedItem = children.find( + const defaultSelectedItem = childrenArray.find( option => option.props.defaultSelected, ); @@ -126,6 +132,18 @@ const ThemelessSelect = React.forwardRef( 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, @@ -135,7 +153,7 @@ const ThemelessSelect = React.forwardRef( openMenu, closeMenu, } = useSelect({ - items: children, + items: childrenArray, defaultSelectedItem, onSelectedItemChange: onInputChange, itemToString, @@ -158,15 +176,11 @@ const ThemelessSelect = React.forwardRef( return changes; }, - ...(programmaticallySelectedItem - ? {selectedItem: programmaticallySelectedItem} - : {}), + ...getSelectedItem(), }); const {children: optionsAsChildren, scrollToIndex} = useVirtualizedList({ - items: React.Children.toArray( - children, - ) as React.ReactElement[], + items: childrenArray, listRef: panelRef, getItemProps, limit: virtualized, diff --git a/src/select/types.ts b/src/select/types.ts index 4408d8b1ea..4234ee9b20 100644 --- a/src/select/types.ts +++ b/src/select/types.ts @@ -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 {