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

Open
wants to merge 5 commits into
base: dropdown-updates
Choose a base branch
from

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: 32b4f84

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: +1 B (0%)

Total Size: 101 kB

Filename Size Change
packages/wonder-blocks-dropdown/dist/es/index.js 18.2 kB +1 B (+0.01%)
ℹ️ 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.76 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.36 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 827 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-hhnmdfvape.chromatic.com/

Chromatic results:

Metric Total
Captured snapshots 79
Tests with visual changes 0
Total stories 503
Inherited (not captured) snapshots [TurboSnap] 290
Tests on the build 369

Comment on lines -174 to -199
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 -260
// 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 -269
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};

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

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

npm Snapshot: Published

🎉 Good news!! We've packaged up the latest commit from this PR (af050b1) 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

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.

2 participants