Skip to content

Commit

Permalink
fix: Change SelectField.selectedOption to value. (#66)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephenh authored May 5, 2021
1 parent ec491b0 commit 38e8f64
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 92 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"devDependencies": {
"@babel/core": "^7.13.1",
"@emotion/babel-preset-css-prop": "^11.2.0",
"@emotion/jest": "^11.2.1",
"@emotion/jest": "^11.3.0",
"@emotion/react": "^11.1.5",
"@homebound/rtl-utils": "^1.39.0",
"@homebound/tsconfig": "^1.0.2",
Expand All @@ -71,7 +71,7 @@
"@testing-library/jest-dom": "^5.11.9",
"@tsconfig/recommended": "^1.0.1",
"@types/jest": "^26.0.20",
"@types/react": "^16.14.5",
"@types/react": "^17.0.5",
"@types/react-dom": "^16.9.11",
"@types/react-router-dom": "^5.1.7",
"@types/tinycolor2": "^1.4.2",
Expand Down Expand Up @@ -104,7 +104,7 @@
"ts-node": "^9.1.1",
"tslib": "^2.1.0",
"ttypescript": "^1.5.12",
"typescript": "^4.2.3",
"typescript": "^4.2.4",
"typescript-transform-paths": "^2.0.1",
"watch": "^1.0.2"
}
Expand Down
85 changes: 19 additions & 66 deletions src/components/SelectField.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Meta } from "@storybook/react";
import { useState } from "react";
import { Key, useState } from "react";
import { Icon, Icons, SelectField, SelectFieldProps } from "src/components";
import { Css } from "src/Css";

Expand Down Expand Up @@ -28,10 +28,8 @@ export function SelectFields() {
<h1 css={Css.lg.mb2.$}>Regular</h1>
<TestSelectField
label="Favorite Icon"
selectedOption={options[2]}
value={options[2].id}
options={options}
getOptionValue={(o) => o.id}
getOptionLabel={(o) => o.name}
getOptionMenuLabel={(o) => (
<div css={Css.df.itemsCenter.$}>
{o.icon && (
Expand All @@ -46,10 +44,8 @@ export function SelectFields() {
<TestSelectField
label="Favorite Icon - with field decoration"
options={options}
getOptionValue={(o) => o.id}
getOptionLabel={(o) => o.name}
fieldDecoration={(o) => o.icon && <Icon icon={o.icon} />}
selectedOption={options[1]}
value={options[1].id}
getOptionMenuLabel={(o) => (
<div css={Css.df.itemsCenter.$}>
{o.icon && (
Expand All @@ -61,38 +57,18 @@ export function SelectFields() {
</div>
)}
/>
<TestSelectField
label="Favorite Icon - Disabled"
options={options}
getOptionValue={(o) => o.id}
getOptionLabel={(o) => o.name}
disabled
/>
<TestSelectField
label="Favorite Icon - Read Only"
options={options}
getOptionValue={(o) => o.id}
getOptionLabel={(o) => o.name}
selectedOption={options[2]}
readOnly
/>
<TestSelectField
label="Favorite Icon"
options={options}
getOptionValue={(o) => o.id}
getOptionLabel={(o) => o.name}
/>
<TestSelectField label="Favorite Icon - Disabled" value={undefined} options={options} disabled />
<TestSelectField label="Favorite Icon - Read Only" options={options} value={options[2].id} readOnly />
<TestSelectField label="Favorite Icon" value={undefined} options={options} />
</div>

<div css={Css.df.flexColumn.childGap3.$}>
<h1 css={Css.lg.mb2.$}>Compact</h1>
<TestSelectField
compact
label="Favorite Icon"
selectedOption={options[2]}
value={options[2].id}
options={options}
getOptionValue={(o) => o.id}
getOptionLabel={(o) => o.name}
getOptionMenuLabel={(o) => (
<div css={Css.df.itemsCenter.$}>
{o.icon && (
Expand All @@ -108,10 +84,8 @@ export function SelectFields() {
compact
label="Favorite Icon - with field decoration"
options={options}
getOptionValue={(o) => o.id}
getOptionLabel={(o) => o.name}
fieldDecoration={(o) => o.icon && <Icon icon={o.icon} />}
selectedOption={options[1]}
value={options[1].id}
getOptionMenuLabel={(o) => (
<div css={Css.df.itemsCenter.$}>
{o.icon && (
Expand All @@ -123,44 +97,23 @@ export function SelectFields() {
</div>
)}
/>
<TestSelectField
compact
label="Favorite Icon - Disabled"
options={options}
getOptionValue={(o) => o.id}
getOptionLabel={(o) => o.name}
disabled
/>
<TestSelectField
compact
label="Favorite Icon - Read Only"
options={options}
getOptionValue={(o) => o.id}
getOptionLabel={(o) => o.name}
selectedOption={options[2]}
readOnly
/>
<TestSelectField
compact
label="Favorite Icon"
options={options}
getOptionValue={(o) => o.id}
getOptionLabel={(o) => o.name}
/>
<TestSelectField compact label="Favorite Icon - Disabled" value={undefined} options={options} disabled />
<TestSelectField compact label="Favorite Icon - Read Only" options={options} value={options[2].id} readOnly />
<TestSelectField compact label="Favorite Icon" options={options} value={undefined} />
</div>
</div>
);
}

function TestSelectField<T extends object>(
props: Partial<SelectFieldProps<T>> & Pick<SelectFieldProps<T>, "getOptionLabel" | "getOptionValue" | "options">,
) {
const [selectedOption, setSelectedOption] = useState<T | undefined>(props.selectedOption);
function TestSelectField<T extends object, V extends Key>(props: Omit<SelectFieldProps<T, V>, "onSelect">) {
const [selectedOption, setSelectedOption] = useState<V | undefined>(props.value);
return (
<SelectField
{...props}
selectedOption={selectedOption}
onSelect={(o) => setSelectedOption(o)}
<SelectField<T, V>
// The `as any` is due to something related to https://github.com/emotion-js/emotion/issues/2169
// We may have to redo the conditional getOptionValue/getOptionLabel
{...(props as any)}
value={selectedOption}
onSelect={(v) => setSelectedOption(v)}
errorMsg={selectedOption || props.disabled ? "" : "Select an option. Plus more error text to force it to wrap."}
/>
);
Expand Down
70 changes: 50 additions & 20 deletions src/components/SelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,55 +20,83 @@ import { Label } from "src/components/Label";
import { Css, Palette, px } from "src/Css";
import { BeamFocusableProps } from "src/interfaces";

export interface SelectFieldProps<T extends object> extends BeamSelectFieldBaseProps<T> {
getOptionLabel: (opt: T) => string;
getOptionMenuLabel?: (opt: T) => string | ReactNode;
getOptionValue: (opt: T) => Key;
onSelect: (opt: T | undefined) => void;
options: T[];
selectedOption: T | undefined;
interface SelectFieldPropsBase<O extends object, V extends Key> extends BeamSelectFieldBaseProps<O> {
/** Renders `opt` in the dropdown menu, defaults to the `getOptionLabel` prop. */
getOptionMenuLabel?: (opt: O) => string | ReactNode;
/** The current value; it can be `undefined`, even if `V` cannot be. */
value: V | undefined;
onSelect: (value: V, opt: O) => void;
options: O[];
}

export function SelectField<T extends object>(props: SelectFieldProps<T>) {
type HasId<V> = { id: V };
type HasName = { name: string };

type MaybeOptionValue<O extends object, V extends Key> = O extends HasId<V>
? { getOptionValue?: (opt: O) => V }
: { getOptionValue: (opt: O) => V };

type MaybeOptionLabel<O extends object> = O extends HasName
? { getOptionLabel?: (opt: O) => string }
: { getOptionLabel: (opt: O) => string };

// We use mapped types to conditionally require getOptionLabel, getOptionValue
export type SelectFieldProps<O extends object, V extends Key> = SelectFieldPropsBase<O, V> &
MaybeOptionValue<O, V> &
MaybeOptionLabel<O>;

/**
* Provides a non-native select/dropdown widget.
*
* The `O` type is a list of options to show, the `V` is the primitive value of a
* given `O` (i.e. it's id) that you want to use as the current/selected value.
*
* Note that the `O extends object` and `V extends Key` constraints come from react-aria,
* and so we cannot easily change them.
*/
export function SelectField<O extends object, V extends Key>(props: SelectFieldProps<O, V>): JSX.Element {
const {
getOptionLabel,
getOptionLabel = (opt: O) => (opt as HasName).name, // if unset, assume O implements HasName
getOptionMenuLabel = getOptionLabel,
getOptionValue,
getOptionValue = (opt: O) => (opt as HasId<V>).id, // if unset, assume O implements HasId
onSelect,
options,
selectedOption,
value,
...beamSelectFieldProps
} = props;

const { contains } = useFilter({ sensitivity: "base" });

// Use the current value to find the option
const selectedOption = options.find((opt) => getOptionValue(opt) === value);

const [fieldState, setFieldState] = useState<{
isOpen: boolean;
selectedKey: Key | undefined;
selectedKey: V | undefined;
inputValue: string;
filteredOptions: T[];
filteredOptions: O[];
}>({
isOpen: false,
selectedKey: selectedOption && getOptionValue(selectedOption),
selectedKey: value,
inputValue: selectedOption ? getOptionLabel(selectedOption) : "",
filteredOptions: options,
});

return (
<ComboBox<T>
<ComboBox<O>
{...beamSelectFieldProps}
filteredOptions={fieldState.filteredOptions}
inputValue={fieldState.inputValue}
selectedKey={fieldState.selectedKey}
onSelectionChange={(key) => {
const selectedItem = options.find((o) => getOptionValue(o) === key);
const newOption = options.find((o) => getOptionValue(o) === key);
setFieldState({
isOpen: false,
inputValue: selectedItem ? getOptionLabel(selectedItem) : "",
selectedKey: key,
inputValue: newOption ? getOptionLabel(newOption) : "",
selectedKey: key as V,
filteredOptions: options,
});
onSelect && onSelect(selectedItem);
onSelect && newOption && onSelect(getOptionValue(newOption), newOption);
}}
onInputChange={(value) => {
setFieldState((prevState) => ({
Expand Down Expand Up @@ -387,11 +415,13 @@ function Option<T extends object>({ item, state }: { item: Node<T>; state: Combo

const getFieldWidth = (compact: boolean) => (compact ? 248 : 320);

interface BeamSelectFieldBaseProps<T extends object> extends BeamFocusableProps {
interface BeamSelectFieldBaseProps<T> extends BeamFocusableProps {
compact?: boolean;
disabled?: boolean;
errorMsg?: string;
/** Allow placing an icon/decoration within the input field. */
fieldDecoration?: (opt: T) => ReactNode;
/** Sets the form field label. */
label?: string;
readOnly?: boolean;
}
15 changes: 12 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1238,7 +1238,7 @@
dependencies:
"@emotion/memoize" "0.7.4"

"@emotion/jest@^11.2.1":
"@emotion/jest@^11.3.0":
version "11.3.0"
resolved "https://registry.yarnpkg.com/@emotion/jest/-/jest-11.3.0.tgz#43bed6dcb47c8691b346cee231861ebc8f9b0016"
integrity sha512-LZqYc3yerhic1IvAcEwBLRs1DsUt3oY7Oz6n+e+HU32iYOK/vpfzlhgmQURE94BHfv6eCOj6DV38f3jSnIkBkQ==
Expand Down Expand Up @@ -3973,7 +3973,7 @@
"@types/scheduler" "*"
csstype "^3.0.2"

"@types/react@^16", "@types/react@^16.14.5":
"@types/react@^16":
version "16.14.5"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.5.tgz#2c39b5cadefaf4829818f9219e5e093325979f4d"
integrity sha512-YRRv9DNZhaVTVRh9Wmmit7Y0UFhEVqXqCSw3uazRWMxa2x85hWQZ5BN24i7GXZbaclaLXEcodEeIHsjBA8eAMw==
Expand All @@ -3982,6 +3982,15 @@
"@types/scheduler" "*"
csstype "^3.0.2"

"@types/react@^17.0.5":
version "17.0.5"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.5.tgz#3d887570c4489011f75a3fc8f965bf87d09a1bea"
integrity sha512-bj4biDB9ZJmGAYTWSKJly6bMr4BLUiBrx9ujiJEoP9XIDY9CTaPGxE5QWN/1WjpPLzYF7/jRNnV2nNxNe970sw==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"

"@types/retry@^0.12.0":
version "0.12.0"
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
Expand Down Expand Up @@ -14834,7 +14843,7 @@ typescript-transform-paths@^2.0.1:
dependencies:
minimatch "^3.0.4"

typescript@^4.2.3:
typescript@^4.2.4:
version "4.2.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961"
integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==
Expand Down

0 comments on commit 38e8f64

Please sign in to comment.