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

Add design polish to TextField, SearchField, and Select #2072

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6f42d6e
fix: fix textfield error hover
jordankoschei-okta Dec 12, 2023
f0d6509
feat: add code for manual resizing of multiline textfields
jordankoschei-okta Dec 12, 2023
39a45fa
fix: adjust TextField height
jordankoschei-okta Dec 12, 2023
c532948
feat: add SearchField filled variant
jordankoschei-okta Dec 12, 2023
18b2f83
feat: add delete to Select chips
jordankoschei-okta Dec 12, 2023
3b5012d
fix: update types in handleDelete
jordankoschei-okta Dec 14, 2023
62045a7
Merge branch 'main' into jk/polish-inputs-2
jordankoschei-okta Jan 11, 2024
28fff02
refactor: update the Select
jordankoschei-okta Jan 16, 2024
9047f3e
refactor: move styles from sx to components
jordankoschei-okta Jan 17, 2024
44f70d8
fix: set large padding for full-width icon buttons
jordankoschei-okta Jan 29, 2024
a614e36
Merge branch 'main' into jk/polish-inputs-2
jordankoschei-okta Jan 29, 2024
b13aebe
refactor: move styles to styled
jordankoschei-okta Jan 29, 2024
f86f685
Merge branch 'main' into jk/polish-inputs-2
jordankoschei-okta Jan 30, 2024
18f6c5f
fix: update spacing on the placeholder chips
jordankoschei-okta Jan 30, 2024
fcc026d
Merge branch 'main' into jk/polish-inputs-2
jordankoschei-okta Feb 13, 2024
993913e
Merge branch 'main' into jk/polish-inputs-2
jordankoschei-okta Feb 14, 2024
b289148
Update packages/odyssey-react-mui/src/Select.tsx
jordankoschei-okta Feb 15, 2024
654cf4f
refactor: convert function to subcomponent
jordankoschei-okta Feb 20, 2024
3ff252b
refactor: kg feedback
jordankoschei-okta Feb 20, 2024
1729e1b
fix: update styled with shouldForwardProp
jordankoschei-okta Feb 20, 2024
fa44c58
Merge branch 'main' into jk/polish-inputs-2
jordankoschei-okta Feb 20, 2024
94942a5
Merge branch 'main' into jk/polish-inputs-2
jordankoschei-okta Feb 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/odyssey-react-mui/src/SearchField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { FieldComponentProps } from "./FieldComponentProps";
import type { HtmlProps } from "./HtmlProps";
import { getControlState, useInputValues } from "./inputUtils";

export const searchVariantValues = ["outline", "filled"] as const;

export type SearchFieldProps = {
/**
* This prop helps users to fill forms faster, especially on mobile devices.
Expand Down Expand Up @@ -81,6 +83,10 @@ export type SearchFieldProps = {
* The value of the `input` element, to use when controlled.
*/
value?: string;
/**
* Whether the SearchField has a gray or white background
*/
variant?: (typeof searchVariantValues)[number];
} & Pick<
FieldComponentProps,
"ariaDescribedBy" | "id" | "isDisabled" | "name" | "isFullWidth"
Expand Down Expand Up @@ -108,6 +114,7 @@ const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(
testId,
translate,
value,
variant = "outline",
jordankoschei-okta marked this conversation as resolved.
Show resolved Hide resolved
},
ref
) => {
Expand Down Expand Up @@ -162,6 +169,8 @@ const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(
)
}
id={id}
data-ods-type="search"
data-ods-variant={variant}
name={nameOverride ?? id}
onBlur={onBlur}
onChange={onChange}
Expand Down Expand Up @@ -193,6 +202,7 @@ const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(
tabIndex,
testId,
translate,
variant,
]
);

Expand Down
205 changes: 164 additions & 41 deletions packages/odyssey-react-mui/src/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,20 @@
useImperativeHandle,
} from "react";
import {
Box,
Box as MuiBox,
Checkbox as MuiCheckbox,
Chip,
Chip as MuiChip,
ListItemSecondaryAction,
ListSubheader,
MenuItem,
MenuItem as MuiMenuItem,
Select as MuiSelect,
SelectChangeEvent,
} from "@mui/material";
jordankoschei-okta marked this conversation as resolved.
Show resolved Hide resolved
import { SelectProps as MuiSelectProps } from "@mui/material";

import { Field } from "./Field";
import { FieldComponentProps } from "./FieldComponentProps";
import { CheckIcon } from "./icons.generated";
import { CheckIcon, CloseCircleFilledIcon } from "./icons.generated";
import type { HtmlProps } from "./HtmlProps";
import {
ComponentControlledState,
Expand All @@ -41,13 +42,58 @@
getControlState,
} from "./inputUtils";
import { normalizedKey } from "./useNormalizedKey";
import styled from "@emotion/styled";
import {
useOdysseyDesignTokens,
DesignTokens,
} from "./OdysseyDesignTokensContext";

export type SelectOption = {
text: string;
type?: "heading" | "option";
value?: string;
};

const SelectContainer = styled.div`
position: relative;
width: 100%;
display: flex;
`;

const PlaceholderValuesContainer = styled.div<{
odysseyDesignTokens: DesignTokens;
}>`
display: flex;
align-items: center;
position: absolute;
top: ${(props) => props.odysseyDesignTokens.Spacing0};
right: ${(props) => props.odysseyDesignTokens.Spacing5};
bottom: ${(props) => props.odysseyDesignTokens.Spacing0};
left: ${(props) => props.odysseyDesignTokens.Spacing1};
margin-inline-start: ${(props) => props.odysseyDesignTokens.BorderWidthMain};
opacity: 1;
pointer-events: none;
`;

const PlaceholderIcon = styled(CloseCircleFilledIcon)<{
odysseyDesignTokens: DesignTokens;
}>`
font-size: 1em;
margin-inline-start: ${(props) => props.odysseyDesignTokens.Spacing2};
margin-inline-end: -${(props) => props.odysseyDesignTokens.Spacing1};
`;

const ChipContainer = styled(MuiBox)<{
isPlaceholder?: boolean;
odysseyDesignTokens: DesignTokens;
}>`
display: flex;
flex-wrap: wrap;
gap: ${(props) => props.odysseyDesignTokens.Spacing1};
pointer-events: none;
opacity: ${(props) => (props.isPlaceholder ? 1 : 0)};
jordankoschei-okta marked this conversation as resolved.
Show resolved Hide resolved
`;

export type SelectValueType<HasMultipleChoices> =
HasMultipleChoices extends true ? string[] : string;

Expand Down Expand Up @@ -168,6 +214,7 @@
controlledStateRef.current === CONTROLLED ? value : defaultValue
);
const localInputRef = useRef<HTMLSelectElement>(null);
const odysseyDesignTokens = useOdysseyDesignTokens();

useImperativeHandle(
inputRef,
Expand Down Expand Up @@ -208,6 +255,25 @@
[onChangeProp]
);

const removeSelection = useCallback(
(itemToRemove: string) => {
if (!Array.isArray(internalSelectedValues)) {
return;
}

const newValue = internalSelectedValues!.filter(
(item: string) => item !== itemToRemove
);
jordankoschei-okta marked this conversation as resolved.
Show resolved Hide resolved

const syntheticEvent = {
target: { value: newValue },
} as SelectChangeEvent<Value>;

onChange(syntheticEvent, null);
},
[internalSelectedValues, onChange]
);

// Normalize the options array to accommodate the various
// data types that might be passed
const normalizedOptions = useMemo(
Expand All @@ -233,7 +299,13 @@
);

const renderValue = useCallback(
(selected: Value) => {
({
selected,
isPlaceholder = false,
}: {
selected: Value;
isPlaceholder?: boolean;
}) => {
jordankoschei-okta marked this conversation as resolved.
Show resolved Hide resolved
// 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") {
Expand All @@ -251,19 +323,63 @@
return null;
}

return <Chip key={item} label={selectedOption.text} />;
return (
<MuiChip
key={item}
label={
<>
{selectedOption.text}
{!isPlaceholder &&
controlledStateRef.current === CONTROLLED &&
hasMultipleChoices && (
<PlaceholderIcon
odysseyDesignTokens={odysseyDesignTokens}
/>
jordankoschei-okta marked this conversation as resolved.
Show resolved Hide resolved
)}
</>
}
tabIndex={-1}
onDelete={
isPlaceholder
? controlledStateRef.current === CONTROLLED
? () => removeSelection(item)
: undefined
: undefined
}
jordankoschei-okta marked this conversation as resolved.
Show resolved Hide resolved
deleteIcon={
// We need to stop event propagation on mouse down to prevent the deletion
// from being blocked by the Select list opening, and also ensure that
// the pointerEvent is registered even when the parent's are not
<CloseCircleFilledIcon
sx={{ pointerEvents: "auto" }}
onMouseDown={(event) => event.stopPropagation()}
jordankoschei-okta marked this conversation as resolved.
Show resolved Hide resolved
/>
}
/>
);
})
.filter(Boolean);

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

// We need the <Box> to surround the <Chip>s for
// proper styling
return <Box>{renderedChips}</Box>;
// We need the <Box> to surround the <Chip>s for proper styling
// and disable
jordankoschei-okta marked this conversation as resolved.
Show resolved Hide resolved
if (hasMultipleChoices && controlledStateRef.current === CONTROLLED) {
return (
<ChipContainer
isPlaceholder={isPlaceholder}
odysseyDesignTokens={odysseyDesignTokens}
>
{renderedChips}
</ChipContainer>
);
}

return <MuiBox>{renderedChips}</MuiBox>;
},
[normalizedOptions]
[normalizedOptions, removeSelection]

Check warning on line 382 in packages/odyssey-react-mui/src/Select.tsx

View workflow job for this annotation

GitHub Actions / ci

React Hook useCallback has missing dependencies: 'hasMultipleChoices' and 'odysseyDesignTokens'. Either include them or remove the dependency array
);

// Convert the options into the ReactNode children
Expand All @@ -274,19 +390,18 @@
if (option.type === "heading") {
return <ListSubheader key={option.text}>{option.text}</ListSubheader>;
}

const isSelected = hasMultipleChoices
? internalSelectedValues?.includes(option.value)
: internalSelectedValues === option.value;

return (
<MenuItem
<MuiMenuItem
key={normalizedKey(option.text, index.toString())}
value={option.value}
selected={isSelected}
>
{hasMultipleChoices && (
<MuiCheckbox
checked={
option.value !== undefined &&
internalSelectedValues?.includes(option.value)
}
/>
)}
{hasMultipleChoices && <MuiCheckbox checked={isSelected} />}
{option.text}
{!hasMultipleChoices &&
(internalSelectedValues?.includes(option.value) ||
Expand All @@ -295,40 +410,48 @@
<CheckIcon />
</ListItemSecondaryAction>
)}
</MenuItem>
</MuiMenuItem>
);
}),
[hasMultipleChoices, normalizedOptions, internalSelectedValues]
);

const renderFieldComponent = useCallback(
({ ariaDescribedBy, errorMessageElementId, id, labelElementId }) => (
<MuiSelect
{...inputValues}
aria-describedby={ariaDescribedBy}
aria-errormessage={errorMessageElementId}
children={children}
data-se={testId}
displayEmpty={
inputValues?.value === "" || inputValues?.defaultValue === ""
}
id={id}
inputProps={{ "data-se": testId }}
inputRef={localInputRef}
labelId={labelElementId}
multiple={hasMultipleChoices}
name={nameOverride ?? id}
onBlur={onBlur}
onChange={onChange}
onFocus={onFocus}
renderValue={hasMultipleChoices ? renderValue : undefined}
translate={translate}
/>
<SelectContainer>
<MuiSelect
{...inputValues}
aria-describedby={ariaDescribedBy}
aria-errormessage={errorMessageElementId}
children={children}
id={id}
inputProps={{ "data-se": testId }}
inputRef={localInputRef}
labelId={labelElementId}
multiple={hasMultipleChoices}
name={nameOverride ?? id}
onBlur={onBlur}
onChange={onChange}
onFocus={onFocus}
renderValue={
hasMultipleChoices
? (value) => renderValue({ selected: value })
: undefined
}
translate={translate}
/>
{hasMultipleChoices && value && (
<PlaceholderValuesContainer odysseyDesignTokens={odysseyDesignTokens}>
{renderValue({ selected: value, isPlaceholder: true })}
jordankoschei-okta marked this conversation as resolved.
Show resolved Hide resolved
</PlaceholderValuesContainer>
jordankoschei-okta marked this conversation as resolved.
Show resolved Hide resolved
)}
</SelectContainer>
),
[

Check warning on line 450 in packages/odyssey-react-mui/src/Select.tsx

View workflow job for this annotation

GitHub Actions / ci

React Hook useCallback has missing dependencies: 'odysseyDesignTokens' and 'value'. Either include them or remove the dependency array
children,
inputValues,
hasMultipleChoices,
normalizedOptions,
nameOverride,
onBlur,
onChange,
Expand Down
Loading
Loading