Skip to content

Commit

Permalink
fix: id and name not changing in Storybook (#1987)
Browse files Browse the repository at this point in the history
fix: id and name not changing for  in Storybook
  • Loading branch information
KevinGhadyani-Okta authored Sep 26, 2023
1 parent 11aa9ee commit 2f57e15
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 174 deletions.
299 changes: 147 additions & 152 deletions packages/odyssey-react-mui/src/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and limitations under the License.
*/

import { ReactNode, forwardRef, memo, useCallback, useState } from "react";
import { ReactNode, memo, useCallback, useMemo, useState } from "react";
import {
Box,
Chip,
Expand Down Expand Up @@ -100,173 +100,168 @@ export type SelectProps = {
* - { text: string, type: "heading" } — Used to display a group heading with the text
*/

const Select = forwardRef<HTMLSelectElement, SelectProps>(
(
{
errorMessage,
hint,
id: idOverride,
isDisabled = false,
isMultiSelect = false,
isOptional = false,
label,
name: nameOverride,
onBlur,
onChange: onChangeProp,
onFocus,
value,
testId,
options,
},
ref
) => {
// If there's no value set, we set it to a blank string (if it's a single-select)
// or an empty array (if it's a multi-select)
if (typeof value === "undefined") {
value = isMultiSelect ? [] : "";
}

const [selectedValue, setSelectedValue] = useState<string | string[]>(
value
);

const onChange = useCallback(
(event: SelectChangeEvent<string | string[]>, child: ReactNode) => {
const {
target: { value },
} = event;
const Select = ({
errorMessage,
hint,
id: idOverride,
isDisabled = false,
isMultiSelect = false,
isOptional = false,
label,
name: nameOverride,
onBlur,
onChange: onChangeProp,
onFocus,
value,
testId,
options,
}: SelectProps) => {
// If there's no value set, we set it to a blank string (if it's a single-select)
// or an empty array (if it's a multi-select)
if (typeof value === "undefined") {
value = isMultiSelect ? [] : "";
}

// Set the field value, with some additional logic to handle array values
// for multi-selects
if (isMultiSelect) {
setSelectedValue(
typeof value === "string" ? value.split(",") : value
);
} else {
setSelectedValue(value);
}
const [selectedValue, setSelectedValue] = useState<string | string[]>(value);

// Trigger the onChange event, if one has been passed
if (onChangeProp) {
onChangeProp(event, child);
}
},
[isMultiSelect, onChangeProp, setSelectedValue]
);
const onChange = useCallback(
(event: SelectChangeEvent<string | string[]>, child: ReactNode) => {
const {
target: { value },
} = event;

// Normalize the options array to accommodate the various
// data types that might be passed
const normalizedOptions = options.map((option) => {
if (typeof option === "object") {
return {
text: option.text,
value: option.value || option.text,
type: option.type === "heading" ? "heading" : "option",
};
// Set the field value, with some additional logic to handle array values
// for multi-selects
if (isMultiSelect) {
setSelectedValue(typeof value === "string" ? value.split(",") : value);
} else {
setSelectedValue(value);
}

return { text: option, value: option, type: "option" };
});

const renderValue = useCallback(
(selected: string | string[]) => {
// If the selected value isn't an array, then we don't need to display
// chips and should fall back to the default render behavior
if (typeof selected === "string") {
return undefined;
}

// Convert the selected options array into <Chip>s
const renderedChips = selected
.map((item: string) => {
const selectedOption = normalizedOptions.find(
(option) => option.value === item
);
// Trigger the onChange event, if one has been passed
if (onChangeProp) {
onChangeProp(event, child);
}
},
[isMultiSelect, onChangeProp, setSelectedValue]
);

if (!selectedOption) {
return null;
// Normalize the options array to accommodate the various
// data types that might be passed
const normalizedOptions = useMemo(
() =>
options.map((option) =>
typeof option === "object"
? {
text: option.text,
value: option.value || option.text,
type: option.type === "heading" ? "heading" : "option",
}
: { text: option, value: option, type: "option" }
),
[options]
);

return <Chip key={item} label={selectedOption.text} />;
})
.filter(Boolean);
const renderValue = useCallback(
(selected: string | string[]) => {
// If the selected value isn't an array, then we don't need to display
// chips and should fall back to the default render behavior
if (typeof selected === "string") {
return undefined;
}

if (renderedChips.length === 0) {
return null;
}
// Convert the selected options array into <Chip>s
const renderedChips = selected
.map((item: string) => {
const selectedOption = normalizedOptions.find(
(option) => option.value === item
);

// We need the <Box> to surround the <Chip>s for
// proper styling
return <Box>{renderedChips}</Box>;
},
[normalizedOptions]
);
if (!selectedOption) {
return null;
}

// Convert the options into the ReactNode children
// that will populate the <Select>
const children = normalizedOptions.map((option) => {
if (option.type === "heading") {
return <ListSubheader key={option.text}>{option.text}</ListSubheader>;
return <Chip key={item} label={selectedOption.text} />;
})
.filter(Boolean);

if (renderedChips.length === 0) {
return null;
}

return (
<MenuItem key={option.value} value={option.value}>
{isMultiSelect && (
<MuiCheckbox checked={selectedValue.includes(option.value)} />
)}
{option.text}
</MenuItem>
);
});
// We need the <Box> to surround the <Chip>s for
// proper styling
return <Box>{renderedChips}</Box>;
},
[normalizedOptions]
);

const renderFieldComponent = useCallback(
() => (
<MuiSelect
children={children}
data-se={testId}
id={idOverride}
multiple={isMultiSelect}
name={nameOverride ?? idOverride}
onBlur={onBlur}
onChange={onChange}
onFocus={onFocus}
ref={ref}
renderValue={isMultiSelect ? renderValue : undefined}
value={selectedValue}
labelId={label}
/>
),
[
children,
idOverride,
isMultiSelect,
label,
nameOverride,
onBlur,
onChange,
onFocus,
ref,
renderValue,
selectedValue,
testId,
]
);
// Convert the options into the ReactNode children
// that will populate the <Select>
const children = useMemo(
() =>
normalizedOptions.map((option) => {
if (option.type === "heading") {
return <ListSubheader key={option.text}>{option.text}</ListSubheader>;
}

return (
<Field
errorMessage={errorMessage}
fieldType="single"
hasVisibleLabel
hint={hint}
id={idOverride}
isDisabled={isDisabled}
isOptional={isOptional}
label={label}
renderFieldComponent={renderFieldComponent}
return (
<MenuItem key={option.value} value={option.value}>
{isMultiSelect && (
<MuiCheckbox checked={selectedValue.includes(option.value)} />
)}
{option.text}
</MenuItem>
);
}),
[isMultiSelect, normalizedOptions, selectedValue]
);

const renderFieldComponent = useCallback(
({ ariaDescribedBy, id }) => (
<MuiSelect
aria-describedby={ariaDescribedBy}
children={children}
data-se={testId}
id={id}
labelId={label}
multiple={isMultiSelect}
name={nameOverride ?? id}
onBlur={onBlur}
onChange={onChange}
onFocus={onFocus}
renderValue={isMultiSelect ? renderValue : undefined}
value={selectedValue}
/>
);
}
);
),
[
children,
isMultiSelect,
label,
nameOverride,
onBlur,
onChange,
onFocus,
renderValue,
selectedValue,
testId,
]
);

return (
<Field
errorMessage={errorMessage}
fieldType="single"
hasVisibleLabel
hint={hint}
id={idOverride}
isDisabled={isDisabled}
isOptional={isOptional}
label={label}
renderFieldComponent={renderFieldComponent}
/>
);
};

const MemoizedSelect = memo(Select);
MemoizedSelect.displayName = "Select";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,24 +240,7 @@ const storybookMeta: Meta<SelectProps> = {

export default storybookMeta;

const Template: StoryObj<SelectProps> = {
render: function C(args) {
return (
<Select
label={args.label}
hint={args.hint}
errorMessage={args.errorMessage}
isDisabled={args.isDisabled}
isMultiSelect={args.isMultiSelect}
isOptional={args.isOptional}
options={args.options}
/>
);
},
};

export const Default: StoryObj<SelectProps> = {
...Template,
play: async ({ canvasElement, step }) => {
await step("Select Earth from the listbox", async () => {
const comboBoxElement = canvasElement.querySelector(
Expand All @@ -281,14 +264,12 @@ export const Default: StoryObj<SelectProps> = {
Default.args = {};

export const Disabled: StoryObj<SelectProps> = {
...Template,
args: {
isDisabled: true,
},
};

export const Error: StoryObj<SelectProps> = {
...Template,
args: {
errorMessage: "Select your destination.",
},
Expand All @@ -300,7 +281,6 @@ export const Error: StoryObj<SelectProps> = {
};

export const OptionsObject: StoryObj<SelectProps> = {
...Template,
args: {
options: optionsObject,
},
Expand All @@ -315,7 +295,6 @@ export const OptionsObject: StoryObj<SelectProps> = {
};

export const OptionsGrouped: StoryObj<SelectProps> = {
...Template,
args: {
options: optionsGrouped,
},
Expand All @@ -330,7 +309,6 @@ export const OptionsGrouped: StoryObj<SelectProps> = {
};

export const MultiSelect: StoryObj<SelectProps> = {
...Template,
args: {
isMultiSelect: true,
},
Expand Down

0 comments on commit 2f57e15

Please sign in to comment.