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

Convert MultiSelect and SingleSelect to functional components #2370

Merged
merged 7 commits into from
Dec 10, 2024

Conversation

beaesguerra
Copy link
Member

@beaesguerra beaesguerra commented Nov 23, 2024

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!)

@beaesguerra beaesguerra self-assigned this Nov 23, 2024
Copy link

changeset-bot bot commented Nov 23, 2024

🦋 Changeset detected

Latest commit: e08647d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@khanacademy/wonder-blocks-dropdown Patch
@khanacademy/wonder-blocks-birthday-picker Patch

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

Copy link
Contributor

github-actions bot commented Nov 23, 2024

Size Change: -15 B (-0.01%)

Total Size: 101 kB

Filename Size Change
packages/wonder-blocks-dropdown/dist/es/index.js 18.4 kB -15 B (-0.08%)
ℹ️ View Unchanged
Filename Size
packages/wonder-blocks-accordion/dist/es/index.js 3.78 kB
packages/wonder-blocks-banner/dist/es/index.js 1.53 kB
packages/wonder-blocks-birthday-picker/dist/es/index.js 1.77 kB
packages/wonder-blocks-breadcrumbs/dist/es/index.js 887 B
packages/wonder-blocks-button/dist/es/index.js 4.04 kB
packages/wonder-blocks-cell/dist/es/index.js 2.01 kB
packages/wonder-blocks-clickable/dist/es/index.js 3.06 kB
packages/wonder-blocks-core/dist/es/index.js 3.44 kB
packages/wonder-blocks-data/dist/es/index.js 6.24 kB
packages/wonder-blocks-form/dist/es/index.js 6.28 kB
packages/wonder-blocks-grid/dist/es/index.js 1.36 kB
packages/wonder-blocks-i18n/dist/es/index.js 4.77 kB
packages/wonder-blocks-icon-button/dist/es/index.js 3 kB
packages/wonder-blocks-icon/dist/es/index.js 871 B
packages/wonder-blocks-labeled-field/dist/es/index.js 72 B
packages/wonder-blocks-layout/dist/es/index.js 1.82 kB
packages/wonder-blocks-link/dist/es/index.js 2.28 kB
packages/wonder-blocks-modal/dist/es/index.js 5.37 kB
packages/wonder-blocks-pill/dist/es/index.js 1.65 kB
packages/wonder-blocks-popover/dist/es/index.js 4.87 kB
packages/wonder-blocks-progress-spinner/dist/es/index.js 1.52 kB
packages/wonder-blocks-search-field/dist/es/index.js 1.3 kB
packages/wonder-blocks-switch/dist/es/index.js 1.94 kB
packages/wonder-blocks-testing-core/dist/es/index.js 3.74 kB
packages/wonder-blocks-testing/dist/es/index.js 1.07 kB
packages/wonder-blocks-theming/dist/es/index.js 693 B
packages/wonder-blocks-timing/dist/es/index.js 1.8 kB
packages/wonder-blocks-tokens/dist/es/index.js 2.36 kB
packages/wonder-blocks-toolbar/dist/es/index.js 905 B
packages/wonder-blocks-tooltip/dist/es/index.js 7.08 kB
packages/wonder-blocks-typography/dist/es/index.js 1.23 kB

compressed-size-action

Copy link
Contributor

github-actions bot commented Nov 23, 2024

A new build was pushed to Chromatic! 🚀

https://5e1bf4b385e3fb0020b7073c-hwglmdzfvu.chromatic.com/

Chromatic results:

Metric Total
Captured snapshots 372
Tests with visual changes 0
Total stories 506
Inherited (not captured) snapshots [TurboSnap] 0
Tests on the build 372

Comment on lines -174 to -206
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;
}>;
Copy link
Member Author

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

// 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:

// 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 = {
Copy link
Member Author

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

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};
Copy link
Member Author

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:

// Merge custom labels with the default ones
const labels = {...defaultLabels, ...propLabels};

Comment on lines 246 to -268
// open should always be false if select is disabled
open: props.disabled
? false
: typeof props.opened === "boolean"
? props.opened
: state.open,
Copy link
Member Author

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 to true, we update the local open state to false.
  • If the opened prop changes, we update the local open state to the value of the prop
  • Otherwise, don't change local open state

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]);

Comment on lines -264 to -277
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},
});
Copy link
Member Author

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

// Merge custom labels with the default ones
const labels = {...defaultLabels, ...propLabels};

Copy link
Member

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
Copy link
Member Author

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.

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";
Copy link
Member Author

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;
Copy link
Member Author

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:

@beaesguerra beaesguerra marked this pull request as ready for review November 25, 2024 22:37
@khan-actions-bot khan-actions-bot requested a review from a team November 25, 2024 22:38
@khan-actions-bot
Copy link
Contributor

khan-actions-bot commented Nov 25, 2024

Gerald

Required Reviewers
  • @Khan/wonder-blocks for changes to .changeset/mean-sheep-shout.md, packages/wonder-blocks-dropdown/src/components/multi-select.tsx, packages/wonder-blocks-dropdown/src/components/single-select.tsx

Don't want to be involved in this pull request? Comment #removeme and we won't notify you of further changes.

Copy link
Contributor

github-actions bot commented Nov 25, 2024

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

Copy link
Member

@jandrade jandrade left a 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) :shipit: 👏

Comment on lines -264 to -277
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},
});
Copy link
Member

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.

@beaesguerra beaesguerra changed the base branch from dropdown-updates to main December 2, 2024 23:23
@beaesguerra beaesguerra merged commit c7178e1 into main Dec 10, 2024
14 checks passed
@beaesguerra beaesguerra deleted the dropdown-fc branch December 10, 2024 20:59
Copy link

codecov bot commented Dec 10, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 0.00%. Comparing base (d67761d) to head (e08647d).
Report is 6 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@     Coverage Diff      @@
##   main   #2370   +/-   ##
============================
============================

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d67761d...e08647d. Read the comment docs.

beaesguerra added a commit that referenced this pull request Dec 16, 2024
## 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants