-
Notifications
You must be signed in to change notification settings - Fork 10
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
Convert MultiSelect and SingleSelect to functional components #2370
Conversation
…ner function since that's where sharedProps is applied
🦋 Changeset detectedLatest commit: e08647d The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Size Change: -15 B (-0.01%) Total Size: 101 kB
ℹ️ View Unchanged
|
A new build was pushed to Chromatic! 🚀https://5e1bf4b385e3fb0020b7073c-hwglmdzfvu.chromatic.com/ Chromatic results:
|
cad7bd0
to
32b4f84
Compare
type State = Readonly<{ | ||
/** | ||
* Whether or not the dropdown is open. | ||
*/ | ||
open: boolean; | ||
/** | ||
* The text input to filter the items by their label. Defaults to an empty | ||
* string. | ||
*/ | ||
searchText: string; | ||
/** | ||
* The selected values that are set when the dropdown is opened. We use | ||
* this to move the selected items to the top when the dropdown is | ||
* re-opened. | ||
*/ | ||
lastSelectedValues: Array<string>; | ||
/** | ||
* The object containing the custom labels used inside this component. | ||
*/ | ||
labels: Labels; | ||
/** | ||
* The DOM reference to the opener element. This is mainly used to set focus | ||
* to this element, and also to pass the reference to Popper.js. | ||
*/ | ||
openerElement?: HTMLElement; | ||
}>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
open
, searchText
, lastSelectedValues
and openerElement
have useState
equivalents
wonder-blocks/packages/wonder-blocks-dropdown/src/components/multi-select.tsx
Lines 223 to 238 in e02ada1
// Whether or not the dropdown is open. | |
const [open, setOpen] = React.useState(false); | |
// The text input to filter the items by their label. Defaults to an empty | |
// string. | |
const [searchText, setSearchText] = React.useState(""); | |
// The selected values that are set when the dropdown is opened. We use this | |
// to move the selected items to the top when the dropdown is re-opened. | |
const [lastSelectedValues, setLastSelectedValues] = React.useState< | |
string[] | |
>([]); | |
// The DOM reference to the opener element. This is mainly used to set focus | |
// to this element, and also to pass the reference to Popper.js. | |
const [openerElement, setOpenerElement] = React.useState<HTMLElement>(); |
labels
doesn't need to be in state and combines the default labels and prop labels:
wonder-blocks/packages/wonder-blocks-dropdown/src/components/multi-select.tsx
Lines 220 to 221 in e02ada1
// Merge custom labels with the default ones | |
const labels = {...defaultLabels, ...propLabels}; |
export default class MultiSelect extends React.Component<Props, State> { | ||
labels: Labels; | ||
|
||
static defaultProps: DefaultProps = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Default props are set when we deconstruct props in the functional component
wonder-blocks/packages/wonder-blocks-dropdown/src/components/multi-select.tsx
Lines 194 to 218 in e02ada1
const { | |
id, | |
light = false, | |
opener, | |
testId, | |
alignment = "left", | |
dropdownStyle, | |
implicitAllEnabled, | |
isFilterable, | |
labels: propLabels, | |
onChange, | |
onToggle, | |
opened, | |
selectedValues = [], | |
shortcuts = false, | |
style, | |
className, | |
"aria-invalid": ariaInvalid, | |
"aria-required": ariaRequired, | |
disabled = false, | |
error = false, | |
children, | |
dropdownId, | |
...sharedProps | |
} = props; |
labels: {...defaultLabels, ...props.labels}, | ||
}; | ||
// merge custom labels with the default ones | ||
this.labels = {...defaultLabels, ...props.labels}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
labels
on the class wasn't being used so it can be removed. We combine default labels and prop labels:
wonder-blocks/packages/wonder-blocks-dropdown/src/components/multi-select.tsx
Lines 220 to 221 in e02ada1
// Merge custom labels with the default ones | |
const labels = {...defaultLabels, ...propLabels}; |
// open should always be false if select is disabled | ||
open: props.disabled | ||
? false | ||
: typeof props.opened === "boolean" | ||
? props.opened | ||
: state.open, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for getDerivedStateFromProps
is translated to a useEffect:
- If
disabled
prop is set totrue
, we update the localopen
state tofalse
. - If the
opened
prop changes, we update the localopen
state to the value of the prop - Otherwise, don't change local
open
state
wonder-blocks/packages/wonder-blocks-dropdown/src/components/multi-select.tsx
Lines 240 to 248 in 32b4f84
React.useEffect(() => { | |
// Used to sync the `opened` state when this component acts as a controlled component | |
if (disabled) { | |
// open should always be false if select is disabled | |
setOpen(false); | |
} else if (typeof opened === "boolean") { | |
setOpen(opened); | |
} | |
}, [disabled, opened]); |
componentDidUpdate(prevProps: Props) { | ||
if (this.props.labels !== prevProps.labels) { | ||
// eslint-disable-next-line react/no-did-update-set-state | ||
this.setState({ | ||
labels: {...this.state.labels, ...this.props.labels}, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Labels will stay up to date since it will combine the default labels and the prop labels
wonder-blocks/packages/wonder-blocks-dropdown/src/components/multi-select.tsx
Lines 220 to 221 in 32b4f84
// Merge custom labels with the default ones | |
const labels = {...defaultLabels, ...propLabels}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes! one benefit of switching to hooks. These kind of lifecycle events get more simpler.
light, | ||
opener, | ||
testId, | ||
// the following props are being included here to avoid |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we deconstruct the props at the start of the function, we don't need to again in the renderOpener function.
wonder-blocks/packages/wonder-blocks-dropdown/src/components/multi-select.tsx
Lines 194 to 218 in 32b4f84
const { | |
id, | |
light = false, | |
opener, | |
testId, | |
alignment = "left", | |
dropdownStyle, | |
implicitAllEnabled, | |
isFilterable, | |
labels: propLabels, | |
onChange, | |
onToggle, | |
opened, | |
selectedValues = [], | |
shortcuts = false, | |
style, | |
className, | |
"aria-invalid": ariaInvalid, | |
"aria-required": ariaRequired, | |
disabled = false, | |
error = false, | |
children, | |
dropdownId, | |
...sharedProps | |
} = props; |
The deconstruction previously is almost the same. The FC will also extract disabled
, error
, children
, and dropdownId
so they won't be included in sharedProps
when it is spread. The only one that matters to the SelectOpener is error
, which is why we also explicitly set the error
prop: https://github.com/Khan/wonder-blocks/pull/2370/files#diff-f7d0f4de4f6447c1f878dd6c51e6646adae276658cf9d7f29fcea7f630b17430R484
(changes for the disabled
doesn't matter since we were already explicitly setting it https://github.com/Khan/wonder-blocks/pull/2370/files#diff-f7d0f4de4f6447c1f878dd6c51e6646adae276658cf9d7f29fcea7f630b17430R485)
@@ -48,16 +48,16 @@ type DefaultProps = Readonly<{ | |||
* Whether this dropdown should be left-aligned or right-aligned with the | |||
* opener component. Defaults to left-aligned. | |||
*/ | |||
alignment: "left" | "right"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of the changes in SingleSelect are similar to the ones annotated in MultiSelect. Will highlight specific changes related to SingleSelect
constructor(props: Props) { | ||
super(props); | ||
|
||
this.selectedIndex = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use a ref for this now:
const selectedIndex = React.useRef(0); |
GeraldRequired Reviewers
Don't want to be involved in this pull request? Comment |
npm Snapshot: Published🎉 Good news!! We've packaged up the latest commit from this PR (d976be8) and published all packages with changesets to npm. You can install the packages in webapp by running: ./services/static/dev/tools/deploy_wonder_blocks.js --tag="PR2370" Packages can also be installed manually by running: yarn add @khanacademy/wonder-blocks-<package-name>@PR2370 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome!! thanks for all the detailed notes. I did a lot of manual testing and can confidently say that works as expected (also tests passing is a good signal) 👏
componentDidUpdate(prevProps: Props) { | ||
if (this.props.labels !== prevProps.labels) { | ||
// eslint-disable-next-line react/no-did-update-set-state | ||
this.setState({ | ||
labels: {...this.state.labels, ...this.props.labels}, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes! one benefit of switching to hooks. These kind of lifecycle events get more simpler.
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #2370 +/- ##
============================
============================
Continue to review full report in Codecov by Sentry.
|
## Summary: Convert MultiSelect and SingleSelect to functional components to support the validation work to support LabeledField. I was trying to add in the validation logic without refactoring too much since we might replace these components with Combobox. It was more complicated to account for different cases so I am refactoring it first to make it easier! Issue: WB-1782 ## Test plan: - SingleSelect continues to work as expected - MultiSelect continues to work as expected (would appreciate help with extra testing in case I missed something. These components were a bit more complicated to convert to FC!) Author: beaesguerra Reviewers: beaesguerra, jandrade Required Reviewers: Approved By: jandrade Checks: ✅ Chromatic - Get results on regular PRs (ubuntu-latest, 20.x), ✅ Test / Test (ubuntu-latest, 20.x, 2/2), ✅ Lint / Lint (ubuntu-latest, 20.x), ✅ Test / Test (ubuntu-latest, 20.x, 1/2), ✅ Check build sizes (ubuntu-latest, 20.x), ✅ Chromatic - Build on regular PRs / chromatic (ubuntu-latest, 20.x), ⏭️ Chromatic - Skip on Release PR (changesets), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald, ✅ Prime node_modules cache for primary configuration (ubuntu-latest, 20.x), ⏭️ dependabot Pull Request URL: #2370
Summary:
Convert MultiSelect and SingleSelect to functional components to support the validation work to support LabeledField.
I was trying to add in the validation logic without refactoring too much since we might replace these components with Combobox. It was more complicated to account for different cases so I am refactoring it first to make it easier!
Issue: WB-1782
Test plan:
(would appreciate help with extra testing in case I missed something. These components were a bit more complicated to convert to FC!)