Skip to content

Commit

Permalink
fix: support controlled and uncontrolled states in form components (#…
Browse files Browse the repository at this point in the history
…2045)

* fix: support controlled and uncontrolled states for form components
  • Loading branch information
chrispulsinelli-okta committed Dec 1, 2023
1 parent 279ce97 commit eacf2b4
Show file tree
Hide file tree
Showing 120 changed files with 1,785 additions and 909 deletions.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed .yarn/cache/c8-npm-7.13.0-9ac8f17e2c-491abf4cf3.zip
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed .yarn/cache/ms-npm-2.1.1-5b4fd72c86-0078a23cd9.zip
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
19 changes: 19 additions & 0 deletions packages/odyssey-react-mui/src/@types/react-augment.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*!
* Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
*
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and limitations under the License.
*/

import { FC } from "react";

export interface ForwardRefWithType extends FC<WithForwardRefProps<Option>> {
<T extends Option>(props: WithForwardRefProps<T>): ReturnType<
FC<WithForwardRefProps<T>>
>;
}
92 changes: 49 additions & 43 deletions packages/odyssey-react-mui/src/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ import {
UseAutocompleteProps,
AutocompleteValue,
} from "@mui/material";
import { memo, useCallback, useMemo } from "react";
import { memo, useCallback, useMemo, useRef } from "react";

import { Field } from "./Field";
import { FieldComponentProps } from "./FieldComponentProps";
import type { SeleniumProps } from "./SeleniumProps";
import { useControlledState } from "./useControlledState";
import {
ComponentControlledState,
useInputValues,
getControlState,
} from "./inputUtils";

export type AutocompleteProps<
OptionType,
Expand All @@ -31,7 +35,6 @@ export type AutocompleteProps<
> = {
/**
* The default value. Use when the component is not controlled.
* @default props.multiple ? [] : null
*/
defaultValue?: UseAutocompleteProps<
OptionType,
Expand Down Expand Up @@ -189,6 +192,45 @@ const Autocomplete = <
getIsOptionEqualToValue,
testId,
}: AutocompleteProps<OptionType, HasMultipleChoices, IsCustomValueAllowed>) => {
const controlledStateRef = useRef(
getControlState({ controlledValue: value, uncontrolledValue: defaultValue })
);
const defaultValueProp = useMemo<
| AutocompleteValue<
OptionType,
HasMultipleChoices,
undefined,
IsCustomValueAllowed
>
| undefined
>(() => {
if (hasMultipleChoices) {
if (value === undefined) {
return defaultValue;
}
return [] as AutocompleteValue<
OptionType,
HasMultipleChoices,
undefined,
IsCustomValueAllowed
>;
}
return value === undefined ? defaultValue : undefined;
}, [defaultValue, hasMultipleChoices, value]);

const valueProps = useInputValues({
defaultValue: defaultValueProp,
value: value,
controlState: controlledStateRef.current,
});

const inputValueProp = useMemo(() => {
if (controlledStateRef.current === ComponentControlledState.CONTROLLED) {
return { inputValue };
}
return undefined;
}, [inputValue]);

const renderInput = useCallback(
({ InputLabelProps, InputProps, ...params }) => (
<Field
Expand Down Expand Up @@ -223,39 +265,6 @@ const Autocomplete = <
),
[errorMessage, hint, isOptional, label, nameOverride]
);

const defaultValuesProp = useMemo<
| AutocompleteValue<
OptionType,
HasMultipleChoices,
undefined,
IsCustomValueAllowed
>
| undefined
>(() => {
if (hasMultipleChoices) {
return defaultValue === undefined
? ([] as AutocompleteValue<
OptionType,
HasMultipleChoices,
undefined,
IsCustomValueAllowed
>)
: defaultValue;
}
return defaultValue ?? undefined;
}, [defaultValue, hasMultipleChoices]);

const [localValue, setLocalValue] = useControlledState({
controlledValue: value,
uncontrolledValue: defaultValuesProp,
});

const [localInputValue, setLocalInputValue] = useControlledState({
controlledValue: inputValue,
uncontrolledValue: undefined,
});

const onChange = useCallback<
NonNullable<
UseAutocompleteProps<
Expand All @@ -267,10 +276,9 @@ const Autocomplete = <
>
>(
(event, value, reason, details) => {
setLocalValue(value);
onChangeProp?.(event, value, reason, details);
},
[onChangeProp, setLocalValue]
[onChangeProp]
);

const onInputChange = useCallback<
Expand All @@ -284,18 +292,18 @@ const Autocomplete = <
>
>(
(event, value, reason) => {
setLocalInputValue(value);
onInputChangeProp?.(event, value, reason);
},
[onInputChangeProp, setLocalInputValue]
[onInputChangeProp]
);

return (
<MuiAutocomplete
{...valueProps}
{...inputValueProp}
// AutoComplete is wrapped in a div within MUI which does not get the disabled attr. So this aria-disabled gets set in the div
aria-disabled={isDisabled}
data-se={testId}
defaultValue={defaultValuesProp}
disableCloseOnSelect={hasMultipleChoices}
disabled={isDisabled}
freeSolo={isCustomValueAllowed}
Expand All @@ -310,8 +318,6 @@ const Autocomplete = <
options={options}
readOnly={isReadOnly}
renderInput={renderInput}
value={localValue}
inputValue={localInputValue}
isOptionEqualToValue={getIsOptionEqualToValue}
/>
);
Expand Down
25 changes: 16 additions & 9 deletions packages/odyssey-react-mui/src/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

import { useTranslation } from "react-i18next";
import { memo, useCallback, useMemo } from "react";
import { memo, useCallback, useMemo, useRef } from "react";
import {
Checkbox as MuiCheckbox,
CheckboxProps as MuiCheckboxProps,
Expand All @@ -22,7 +22,7 @@ import {
import { FieldComponentProps } from "./FieldComponentProps";
import { Typography } from "./Typography";
import type { SeleniumProps } from "./SeleniumProps";
import { useControlledState } from "./useControlledState";
import { ComponentControlledState, getControlState } from "./inputUtils";
import { CheckedFieldProps } from "./FormCheckedProps";

export const checkboxValidityValues = ["valid", "invalid", "inherit"] as const;
Expand Down Expand Up @@ -90,10 +90,18 @@ const Checkbox = ({
value,
}: CheckboxProps) => {
const { t } = useTranslation();
const [isLocalChecked, setIsLocalChecked] = useControlledState({
controlledValue: isChecked,
uncontrolledValue: isDefaultChecked,
});
const controlledStateRef = useRef(
getControlState({
controlledValue: isChecked,
uncontrolledValue: isDefaultChecked,
})
);
const inputValues = useMemo(() => {
if (controlledStateRef.current === ComponentControlledState.CONTROLLED) {
return { checked: isChecked };
}
return { defaultChecked: isDefaultChecked };
}, [isDefaultChecked, isChecked]);

const label = useMemo(() => {
return (
Expand All @@ -114,10 +122,9 @@ const Checkbox = ({

const onChange = useCallback<NonNullable<MuiCheckboxProps["onChange"]>>(
(event, checked) => {
setIsLocalChecked(checked);
onChangeProp?.(event, checked);
},
[onChangeProp, setIsLocalChecked]
[onChangeProp]
);

return (
Expand All @@ -134,7 +141,7 @@ const Checkbox = ({
}
control={
<MuiCheckbox
checked={isLocalChecked}
{...inputValues}
indeterminate={isIndeterminate}
onChange={onChange}
required={isRequired}
Expand Down

0 comments on commit eacf2b4

Please sign in to comment.