diff --git a/packages/odyssey-react-mui/src/Field.tsx b/packages/odyssey-react-mui/src/Field.tsx
new file mode 100644
index 0000000000..b27f8586f4
--- /dev/null
+++ b/packages/odyssey-react-mui/src/Field.tsx
@@ -0,0 +1,112 @@
+/*!
+ * Copyright (c) 2022-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 { memo, ReactElement, useMemo } from "react";
+
+import {
+ FieldError,
+ FieldHint,
+ FieldLabel,
+ FormControl,
+ useUniqueId,
+} from "./";
+
+export type FieldProps = {
+ /**
+ * If `error` is not undefined, the `input` will indicate an error.
+ */
+ errorMessage?: string;
+ hasVisibleLabel: boolean;
+ /**
+ * The helper text content.
+ */
+ hint?: string;
+ /**
+ * The id of the `input` element.
+ */
+ id?: string;
+ /**
+ * If `true`, the component is disabled.
+ */
+ isDisabled?: boolean;
+ /**
+ * If `true`, the `input` element is required.
+ */
+ isRequired?: boolean;
+ /**
+ * The label for the `input` element.
+ */
+ label: string;
+ /**
+ * The label for the `input` element if the it's not optional
+ */
+ optionalLabel?: string;
+ /**
+ * The short hint displayed in the `input` before the user enters a value.
+ */
+ placeholder?: string;
+ renderFieldComponent: ({
+ ariaDescribedBy,
+ id,
+ }: {
+ ariaDescribedBy?: string;
+ id: string;
+ }) => ReactElement;
+};
+
+const Field = ({
+ errorMessage,
+ hasVisibleLabel,
+ hint,
+ id: idOverride,
+ isDisabled = false,
+ isRequired = true,
+ label,
+ optionalLabel,
+ renderFieldComponent,
+}: FieldProps) => {
+ const id = useUniqueId(idOverride);
+ const hintId = hint ? `${id}-hint` : undefined;
+ const errorId = errorMessage ? `${id}-error` : undefined;
+ const labelId = `${id}-label`;
+
+ const ariaDescribedBy = useMemo(
+ () => [hintId, errorId].join(" ").trim() || undefined,
+ [errorId, hintId]
+ );
+
+ return (
+
+
+
+ {hint && }
+
+ {renderFieldComponent({
+ ariaDescribedBy,
+ id,
+ })}
+
+ {errorMessage && }
+
+ );
+};
+
+const MemoizedField = memo(Field);
+
+export { MemoizedField as Field };
diff --git a/packages/odyssey-react-mui/src/FieldError.tsx b/packages/odyssey-react-mui/src/FieldError.tsx
new file mode 100644
index 0000000000..4edadc8dc2
--- /dev/null
+++ b/packages/odyssey-react-mui/src/FieldError.tsx
@@ -0,0 +1,34 @@
+/*!
+ * 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 { memo } from "react";
+
+import { FormHelperText } from ".";
+import { ScreenReaderText } from "./ScreenReaderText";
+
+export type FieldErrorProps = {
+ id?: string;
+ text: string;
+};
+
+const FieldError = ({ id, text }: FieldErrorProps) => {
+ return (
+
+ Error:
+ {text}
+
+ );
+};
+
+const MemoizedFieldError = memo(FieldError);
+
+export { MemoizedFieldError as FieldError };
diff --git a/packages/odyssey-react-mui/src/FieldHint.tsx b/packages/odyssey-react-mui/src/FieldHint.tsx
new file mode 100644
index 0000000000..76196b5d65
--- /dev/null
+++ b/packages/odyssey-react-mui/src/FieldHint.tsx
@@ -0,0 +1,28 @@
+/*!
+ * 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 { memo } from "react";
+
+import { FormHelperText } from "./";
+
+export type FieldHintProps = {
+ id?: string;
+ text: string;
+};
+
+const FieldHint = ({ id, text }: FieldHintProps) => {
+ return {text};
+};
+
+const MemoizedFieldHint = memo(FieldHint);
+
+export { MemoizedFieldHint as FieldHint };
diff --git a/packages/odyssey-react-mui/src/FieldLabel.tsx b/packages/odyssey-react-mui/src/FieldLabel.tsx
new file mode 100644
index 0000000000..c59cec3574
--- /dev/null
+++ b/packages/odyssey-react-mui/src/FieldLabel.tsx
@@ -0,0 +1,57 @@
+/*!
+ * 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 { InputLabel } from "@mui/material";
+import { memo, useMemo } from "react";
+
+import { ScreenReaderText } from "./ScreenReaderText";
+import { Typography } from ".";
+
+export type FieldLabelProps = {
+ hasVisibleLabel: boolean;
+ id: string;
+ inputId: string;
+ isRequired: boolean;
+ optionalText?: string;
+ text: string;
+};
+
+const FieldLabel = ({
+ hasVisibleLabel,
+ id,
+ inputId,
+ isRequired,
+ optionalText,
+ text,
+}: FieldLabelProps) => {
+ const inputLabel = useMemo(
+ () => (
+
+ {text}
+ {!isRequired && (
+ {optionalText}
+ )}
+
+ ),
+ [id, inputId, isRequired, optionalText, text]
+ );
+
+ return hasVisibleLabel ? (
+ inputLabel
+ ) : (
+ {inputLabel}
+ );
+};
+
+const MemoizedFieldLabel = memo(FieldLabel);
+
+export { MemoizedFieldLabel as FieldLabel };
diff --git a/packages/odyssey-react-mui/src/PasswordField.tsx b/packages/odyssey-react-mui/src/PasswordField.tsx
new file mode 100644
index 0000000000..315d9dcd54
--- /dev/null
+++ b/packages/odyssey-react-mui/src/PasswordField.tsx
@@ -0,0 +1,174 @@
+/*!
+ * Copyright (c) 2022-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 { InputAdornment, InputBase } from "@mui/material";
+import {
+ ChangeEventHandler,
+ FocusEventHandler,
+ forwardRef,
+ memo,
+ useCallback,
+ useState,
+} from "react";
+
+import { EyeIcon, EyeOffIcon, IconButton } from "./";
+import { Field } from "./Field";
+
+export type PasswordFieldProps = {
+ /**
+ * If `true`, the component will receive focus automatically.
+ */
+ autoFocus?: boolean;
+ /**
+ * This prop helps users to fill forms faster, especially on mobile devices.
+ * The name can be confusing, as it's more like an autofill.
+ * You can learn more about it [following the specification](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill).
+ */
+ autoCompleteType?: "current-password" | "new-password";
+ /**
+ * If `error` is not undefined, the `input` will indicate an error.
+ */
+ errorMessage?: string;
+ /**
+ * The helper text content.
+ */
+ hint?: string;
+ /**
+ * The id of the `input` element.
+ */
+ id?: string;
+ /**
+ * If `true`, the component is disabled.
+ */
+ isDisabled?: boolean;
+ /**
+ * It prevents the user from changing the value of the field
+ */
+ isReadOnly?: boolean;
+ /**
+ * If `true`, the `input` element is required.
+ */
+ isRequired?: boolean;
+ /**
+ * The label for the `input` element.
+ */
+ label: string;
+ /**
+ * Callback fired when the `input` element loses focus.
+ */
+ onBlur?: FocusEventHandler;
+ /**
+ * Callback fired when the value is changed.
+ */
+ onChange?: ChangeEventHandler;
+ /**
+ * Callback fired when the `input` element get focus.
+ */
+ onFocus?: FocusEventHandler;
+ /**
+ * The short hint displayed in the `input` before the user enters a value.
+ */
+ placeholder?: string;
+ /**
+ * The value of the `input` element, required for a controlled component.
+ */
+ value?: string;
+};
+
+const PasswordField = forwardRef(
+ (
+ {
+ autoCompleteType,
+ autoFocus,
+ errorMessage,
+ hint,
+ id: idOverride,
+ isDisabled = false,
+ isReadOnly,
+ label,
+ onChange,
+ onFocus,
+ onBlur,
+ placeholder,
+ value,
+ },
+ ref
+ ) => {
+ const [inputType, setInputType] = useState("password");
+
+ const togglePasswordVisibility = useCallback(() => {
+ setInputType((inputType) =>
+ inputType === "password" ? "text" : "password"
+ );
+ }, []);
+
+ const renderFieldComponent = useCallback(
+ ({ ariaDescribedBy, id }) => (
+
+
+ {inputType === "password" ? : }
+
+
+ }
+ id={id}
+ onChange={onChange}
+ onFocus={onFocus}
+ onBlur={onBlur}
+ placeholder={placeholder}
+ readOnly={isReadOnly}
+ ref={ref}
+ type={inputType}
+ value={value}
+ />
+ ),
+ [
+ autoCompleteType,
+ autoFocus,
+ togglePasswordVisibility,
+ inputType,
+ onChange,
+ onFocus,
+ onBlur,
+ placeholder,
+ isReadOnly,
+ ref,
+ value,
+ ]
+ );
+
+ return (
+
+ );
+ }
+);
+
+const MemoizedPasswordField = memo(PasswordField);
+
+export { MemoizedPasswordField as PasswordField };
diff --git a/packages/odyssey-react-mui/src/PasswordInput.test.tsx b/packages/odyssey-react-mui/src/PasswordInput.test.tsx
deleted file mode 100644
index 5d4170452a..0000000000
--- a/packages/odyssey-react-mui/src/PasswordInput.test.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-/*!
- * Copyright (c) 2022-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 { render, screen } from "@testing-library/react";
-
-import { a11yCheck } from "./a11yCheck";
-import { PasswordInput } from "./PasswordInput";
-
-const label = "Password";
-const tooltipOnLabel = "Show password";
-const tooltipOffLabel = "Hide password";
-
-describe("PasswordInput", () => {
- it("renders into the document", () => {
- render();
- expect(screen.getByLabelText(label)).toBeVisible();
- });
-
- it("has a button that changes the type when clicked", () => {
- const tooltipLabel = (isHidden: boolean) => {
- return isHidden ? tooltipOnLabel : tooltipOffLabel;
- };
- render(
-
- );
- const inputElement = screen.getByLabelText(label);
- expect(inputElement).toHaveValue("Imma password");
- expect(inputElement).toHaveAttribute("type", "password");
- const eyeButton = screen.getByRole("button");
- expect(eyeButton).toBeInTheDocument();
- eyeButton.click();
- expect(inputElement).toHaveAttribute("type", "text");
- });
-
- a11yCheck(() =>
- render()
- );
-});
diff --git a/packages/odyssey-react-mui/src/PasswordInput.tsx b/packages/odyssey-react-mui/src/PasswordInput.tsx
deleted file mode 100644
index 08ae1d8f2e..0000000000
--- a/packages/odyssey-react-mui/src/PasswordInput.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-/*!
- * Copyright (c) 2022-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 { forwardRef, useState, useMemo } from "react";
-import type { Ref, ChangeEvent, MouseEvent } from "react";
-import type { OutlinedInputProps, TooltipProps } from "@mui/material";
-import {
- Tooltip,
- IconButton,
- Box,
- InputLabel,
- OutlinedInput,
- InputAdornment,
-} from "@mui/material";
-import { Visibility, VisibilityOff } from "@mui/icons-material";
-import { useUniqueId } from "./useUniqueId";
-
-interface State {
- password: string;
- showPassword: boolean;
-}
-
-export interface PasswordInputProps
- extends Omit {
- ref?: Ref;
- defaultValue?: string;
- label: string;
- tooltipLabel?:
- | TooltipProps["title"]
- | ((isHidden: boolean) => TooltipProps["title"]);
-}
-
-export const PasswordInput = forwardRef(
- (props, ref) => {
- const {
- tooltipLabel,
- id,
- label,
- defaultValue: password = "",
- inputProps,
- ...rest
- } = props;
-
- const [values, setValues] = useState({
- password,
- showPassword: false,
- });
-
- const handlePasswordChange = (event: ChangeEvent) => {
- setValues({ ...values, password: event.target.value });
- props.onChange?.(event);
- };
-
- const handleClickShowPassword = () => {
- setValues({
- ...values,
- showPassword: !values.showPassword,
- });
- };
-
- const handleMouseDownPassword = (event: MouseEvent) => {
- event.preventDefault();
- };
-
- const tooltipTitle = useMemo(() => {
- return typeof tooltipLabel === "function"
- ? tooltipLabel(values.showPassword === false)
- : tooltipLabel;
- }, [values, tooltipLabel]);
-
- const uniqueId = useUniqueId(id);
-
- return (
-
- {label}
-
-
-
- {values.showPassword ? : }
-
-
-
- }
- />
-
- );
- }
-);
diff --git a/packages/odyssey-react-mui/src/SearchField.tsx b/packages/odyssey-react-mui/src/SearchField.tsx
new file mode 100644
index 0000000000..84c168e108
--- /dev/null
+++ b/packages/odyssey-react-mui/src/SearchField.tsx
@@ -0,0 +1,136 @@
+/*!
+ * Copyright (c) 2022-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 { InputAdornment, InputBase } from "@mui/material";
+import {
+ ChangeEventHandler,
+ FocusEventHandler,
+ forwardRef,
+ InputHTMLAttributes,
+ memo,
+ useCallback,
+} from "react";
+
+import { SearchIcon } from "./";
+import { Field } from "./Field";
+
+export type SearchFieldProps = {
+ /**
+ * If `true`, the component will receive focus automatically.
+ */
+ autoFocus?: boolean;
+ /**
+ * This prop helps users to fill forms faster, especially on mobile devices.
+ * The name can be confusing, as it's more like an autofill.
+ * You can learn more about it [following the specification](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill).
+ */
+ autoCompleteType?: InputHTMLAttributes["autoComplete"];
+ /**
+ * The id of the `input` element.
+ */
+ id?: string;
+ /**
+ * If `true`, the component is disabled.
+ */
+ isDisabled?: boolean;
+ /**
+ * This label won't show up visually, but it's required for accessibility.
+ */
+ label: string;
+ /**
+ * Callback fired when the `input` element loses focus.
+ */
+ onBlur?: FocusEventHandler;
+ /**
+ * Callback fired when the value is changed.
+ */
+ onChange?: ChangeEventHandler;
+ /**
+ * Callback fired when the `input` element get focus.
+ */
+ onFocus?: FocusEventHandler;
+ /**
+ * The short hint displayed in the `input` before the user enters a value.
+ */
+ placeholder?: string;
+ /**
+ * The value of the `input` element, required for a controlled component.
+ */
+ value?: string;
+};
+
+const SearchField = forwardRef(
+ (
+ {
+ autoCompleteType,
+ autoFocus,
+ id: idOverride,
+ isDisabled = false,
+ label,
+ onChange,
+ onFocus,
+ onBlur,
+ placeholder,
+ value,
+ },
+ ref
+ ) => {
+ const renderFieldComponent = useCallback(
+ ({ ariaDescribedBy, id }) => (
+
+
+
+ }
+ type="search"
+ value={value}
+ />
+ ),
+ [
+ autoCompleteType,
+ autoFocus,
+ onChange,
+ onFocus,
+ onBlur,
+ placeholder,
+ ref,
+ value,
+ ]
+ );
+
+ return (
+
+ );
+ }
+);
+
+const MemoizedSearchField = memo(SearchField);
+
+export { MemoizedSearchField as SearchField };
diff --git a/packages/odyssey-react-mui/src/TextField.tsx b/packages/odyssey-react-mui/src/TextField.tsx
index 3fd474b87d..47e330dbca 100644
--- a/packages/odyssey-react-mui/src/TextField.tsx
+++ b/packages/odyssey-react-mui/src/TextField.tsx
@@ -10,12 +10,7 @@
* See the License for the specific language governing permissions and limitations under the License.
*/
-import {
- InputAdornment,
- InputBase,
- InputBaseProps,
- InputLabel,
-} from "@mui/material";
+import { InputBase } from "@mui/material";
import {
ChangeEventHandler,
FocusEventHandler,
@@ -24,22 +19,9 @@ import {
memo,
ReactNode,
useCallback,
- useEffect,
- useMemo,
- useState,
} from "react";
-import {
- EyeIcon,
- EyeOffIcon,
- FormControl,
- FormHelperText,
- IconButton,
- SearchIcon,
- Typography,
- useUniqueId,
- ScreenReaderText,
-} from "./";
+import { Field } from "./Field";
export type TextFieldProps = {
/**
@@ -68,10 +50,6 @@ export type TextFieldProps = {
* The id of the `input` element.
*/
id?: string;
- /**
- * Props that go onto the HTML `input` element.
- */
- inputProps?: InputBaseProps["inputProps"];
/**
* If `true`, the component is disabled.
*/
@@ -91,7 +69,7 @@ export type TextFieldProps = {
/**
* The label for the `input` element.
*/
- label?: string;
+ label: string;
/**
* Callback fired when the `input` element loses focus.
*/
@@ -119,11 +97,11 @@ export type TextFieldProps = {
/**
* Type of the `input` element. It should be [a valid HTML5 input type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types).
*/
- type?: string;
+ type?: "email" | "number" | "tel" | "text" | "url";
/**
* The value of the `input` element, required for a controlled component.
*/
- value?: unknown;
+ value?: string;
};
const TextField = forwardRef(
@@ -135,15 +113,14 @@ const TextField = forwardRef(
errorMessage,
hint,
id: idOverride,
- inputProps = {},
isDisabled = false,
isMultiline = false,
isReadOnly,
isRequired = true,
label,
+ onBlur,
onChange,
onFocus,
- onBlur,
optionalLabel,
placeholder,
startAdornment,
@@ -152,91 +129,56 @@ const TextField = forwardRef(
},
ref
) => {
- const [inputType, setInputType] = useState(type);
-
- useEffect(() => {
- setInputType(type);
- }, [type]);
-
- const togglePasswordVisibility = useCallback(() => {
- setInputType((currentType) =>
- currentType === "password" ? "text" : "password"
- );
- }, []);
-
- const id = useUniqueId(idOverride);
- const hintId = hint ? `${id}-hint` : undefined;
- const errorId = errorMessage ? `${id}-error` : undefined;
- const labelId = label ? `${id}-label` : undefined;
-
- const localInputProps = useMemo(() => {
- const ariaDescribedBy =
- errorId && hintId ? `${hintId} ${errorId}` : errorId || hintId;
-
- return {
- ...inputProps,
- "aria-describedby":
- inputProps["aria-describedby"]?.concat(` ${ariaDescribedBy}`) ??
- ariaDescribedBy,
- };
- }, [errorId, hintId, inputProps]);
-
- return (
-
-
- {label}
- {!isRequired && (
- {optionalLabel}
- )}
-
- {hint && {hint}}
+ const renderFieldComponent = useCallback(
+ ({ ariaDescribedBy, id }) => (
-
- {inputType === "password" ? : }
-
-
- ) : (
- endAdornment
- )
- }
+ endAdornment={endAdornment}
id={id}
- inputProps={localInputProps}
multiline={isMultiline}
+ onBlur={onBlur}
onChange={onChange}
onFocus={onFocus}
- onBlur={onBlur}
placeholder={placeholder}
readOnly={isReadOnly}
ref={ref}
- startAdornment={
- inputType === "search" ? (
-
-
-
- ) : (
- startAdornment
- )
- }
- type={inputType}
+ startAdornment={startAdornment}
+ type={type}
value={value}
/>
- {errorMessage && (
-
- Error:
- {errorMessage}
-
- )}
-
+ ),
+ [
+ autoCompleteType,
+ autoFocus,
+ endAdornment,
+ isMultiline,
+ onChange,
+ onFocus,
+ onBlur,
+ placeholder,
+ isReadOnly,
+ ref,
+ startAdornment,
+ type,
+ value,
+ ]
+ );
+
+ return (
+
);
}
);
diff --git a/packages/odyssey-react-mui/src/index.ts b/packages/odyssey-react-mui/src/index.ts
index 25259e1c4d..ba941e406a 100644
--- a/packages/odyssey-react-mui/src/index.ts
+++ b/packages/odyssey-react-mui/src/index.ts
@@ -127,6 +127,9 @@ export * from "./Checkbox";
export * from "./CheckboxGroup";
export * from "./CircularProgress";
export * from "./createUniqueId";
+export * from "./FieldError";
+export * from "./FieldHint";
+export * from "./FieldLabel";
export * from "./Icon";
export * from "./iconDictionary";
export * from "./Infobox";
@@ -135,10 +138,11 @@ export * from "./MenuButton";
export * from "./MenuItem";
export * from "./OdysseyCacheProvider";
export * from "./OdysseyThemeProvider";
-export * from "./PasswordInput";
+export * from "./PasswordField";
export * from "./Radio";
export * from "./RadioGroup";
export * from "./ScreenReaderText";
+export * from "./SearchField";
export * from "./Status";
export * from "./TextField";
export * from "./theme";
diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/PasswordField/PasswordField.mdx b/packages/odyssey-storybook/src/components/odyssey-mui/PasswordField/PasswordField.mdx
new file mode 100644
index 0000000000..af241daf1d
--- /dev/null
+++ b/packages/odyssey-storybook/src/components/odyssey-mui/PasswordField/PasswordField.mdx
@@ -0,0 +1,11 @@
+import { ArgsTable, Canvas, Meta, Story } from "@storybook/addon-docs";
+
+
+
+# Password Field
+
+Password inputs ensure that sensitive content is safely obscured.
+
+
diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/PasswordField/PasswordField.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/PasswordField/PasswordField.stories.tsx
new file mode 100644
index 0000000000..30cc26cd24
--- /dev/null
+++ b/packages/odyssey-storybook/src/components/odyssey-mui/PasswordField/PasswordField.stories.tsx
@@ -0,0 +1,90 @@
+/*!
+ * 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 { ComponentMeta, ComponentStory } from "@storybook/react";
+import { PasswordField } from "@okta/odyssey-react-mui";
+
+import { MuiThemeDecorator } from "../../../../.storybook/components";
+import PasswordFieldMdx from "./PasswordField.mdx";
+
+const storybookMeta: ComponentMeta = {
+ title: `MUI Components/Forms/PasswordField`,
+ component: PasswordField,
+ parameters: {
+ docs: {
+ page: PasswordFieldMdx,
+ },
+ },
+ argTypes: {
+ autoCompleteType: {
+ control: "text",
+ defaultValue: "name",
+ },
+ autoFocus: {
+ control: "boolean",
+ defaultValue: false,
+ },
+ isDisabled: {
+ control: "boolean",
+ defaultValue: false,
+ },
+ errorMessage: {
+ control: "text",
+ },
+ hint: {
+ control: "text",
+ },
+ id: {
+ control: "text",
+ },
+ label: {
+ control: "text",
+ defaultValue: "Destination",
+ },
+ onBlur: {
+ control: "function",
+ },
+ onChange: {
+ control: "function",
+ },
+ onFocus: {
+ control: "function",
+ },
+ placeholder: {
+ control: "text",
+ },
+ isReadOnly: {
+ control: "boolean",
+ defaultValue: false,
+ },
+ isRequired: {
+ control: "boolean",
+ defaultValue: true,
+ },
+ value: {
+ control: "text",
+ },
+ },
+ decorators: [MuiThemeDecorator],
+};
+
+export default storybookMeta;
+
+const Template: ComponentStory = (args) => {
+ return ;
+};
+
+export const Default = Template.bind({});
+Default.args = {
+ autoCompleteType: "current-password",
+ label: "Password",
+};
diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/SearchField/SearchField.mdx b/packages/odyssey-storybook/src/components/odyssey-mui/SearchField/SearchField.mdx
new file mode 100644
index 0000000000..29417cfc04
--- /dev/null
+++ b/packages/odyssey-storybook/src/components/odyssey-mui/SearchField/SearchField.mdx
@@ -0,0 +1,11 @@
+import { ArgsTable, Canvas, Meta, Story } from "@storybook/addon-docs";
+
+
+
+# Search Field
+
+Search inputs allow the user to submit queries. This is the only input type where `placeholder` may be used.
+
+
diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/SearchField/SearchField.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/SearchField/SearchField.stories.tsx
new file mode 100644
index 0000000000..4623c4ade0
--- /dev/null
+++ b/packages/odyssey-storybook/src/components/odyssey-mui/SearchField/SearchField.stories.tsx
@@ -0,0 +1,76 @@
+/*!
+ * 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 { ComponentMeta, ComponentStory } from "@storybook/react";
+import { SearchField } from "@okta/odyssey-react-mui";
+
+import { MuiThemeDecorator } from "../../../../.storybook/components";
+import SearchFieldMdx from "./SearchField.mdx";
+
+const storybookMeta: ComponentMeta = {
+ title: `MUI Components/Forms/SearchField`,
+ component: SearchField,
+ parameters: {
+ docs: {
+ page: SearchFieldMdx,
+ },
+ },
+ argTypes: {
+ autoCompleteType: {
+ control: "text",
+ defaultValue: "name",
+ },
+ autoFocus: {
+ control: "boolean",
+ defaultValue: false,
+ },
+ isDisabled: {
+ control: "boolean",
+ defaultValue: false,
+ },
+ id: {
+ control: "text",
+ },
+ label: {
+ control: "text",
+ defaultValue: "Destination",
+ },
+ onBlur: {
+ control: "function",
+ },
+ onChange: {
+ control: "function",
+ },
+ onFocus: {
+ control: "function",
+ },
+ placeholder: {
+ control: "text",
+ },
+ value: {
+ control: "text",
+ },
+ },
+ decorators: [MuiThemeDecorator],
+};
+
+export default storybookMeta;
+
+const Template: ComponentStory = (args) => {
+ return ;
+};
+
+export const Default = Template.bind({});
+Default.args = {
+ label: "Search",
+ placeholder: "Search planets",
+};
diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.mdx b/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.mdx
index 4563e37dbe..c8f229d572 100644
--- a/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.mdx
+++ b/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.mdx
@@ -84,12 +84,6 @@ The values of read-only inputs will be submitted.
-### Optional
-
-Odyssey assumes inputs are required by default. Optional inputs should be used to indicate when data is not required for the user to complete a task.
-
-**STORY MISSING**
-
### Invalid
The invalid state is for inputs with incorrect values or values of the wrong format.
@@ -119,53 +113,3 @@ Unlike email fields, tel inputs are not automatically validated because global f
-
-### Password
-
-Passwords inputs ensure that sensitive content is safely obscured.
-
-
-
-### Search
-
-Search inputs allow the user to submit queries. This is the only input type where `placeholder` may be used.
-
-
-
-## Accessibility
-
-### Placeholders
-
-Except for Search inputs, we advise against using placeholder text for inputs.
-
-#### Translation
-
-To prevent triggering a change in page layout, browsers don't translate certain attributes. Because of this, users will see untranslated placeholder text.
-
-#### Recall
-
-Placeholder text disappears when a field is interacted with. For this reason, it's not suitable for formatting guidelines or necessary context.
-
-#### Utility
-
-Placeholder content is limited to static text. Additionally, placeholder text is truncated beyond the width of its input.
-
-#### Field value confusion
-
-Low-contrast placeholders may be illegible for some users. Yet, placeholders with compliant contrast can be mistaken for field values. High Contrast Mode will make placeholders and values appear identical.
-
-Finally, Users with low digital literacy may not understand the purpose or behavior of placeholder text.
-
-### Purpose
-
-When collecting an individual's personal data, you must define the input's purpose via the `autocomplete` attribute. This allows users to automate the filling of fields and ensures the purpose is known, regardless of the label. A complete list of fields this is required for may be found in the WCAG spec.
-
-### Autofocus
-
-Except for very specific cases, we advise against using the `autoFocus` prop; unless used considerately, the sense of focus being "teleported" to an unexpected part of the page can be jarring to users, especially those using screen readers.
-
-More details can be found [on MDN's `autofocus` page](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus#accessibility_considerations).
diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.stories.tsx
index 57e1dd2033..5638f328fe 100644
--- a/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.stories.tsx
+++ b/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.stories.tsx
@@ -86,7 +86,7 @@ const storybookMeta: ComponentMeta = {
},
type: {
control: "select",
- options: ["text", "email", "search", "tel", "password"],
+ options: ["email", "number", "tel", "text", "url"],
defaultValue: "text",
},
value: {
@@ -141,7 +141,6 @@ Adornment.args = {
};
// Types
-
export const Email = Template.bind({});
Email.args = {
autoCompleteType: "work email",
@@ -156,20 +155,6 @@ Multiline.args = {
isMultiline: true,
};
-export const Password = Template.bind({});
-Password.args = {
- autoCompleteType: "current-password",
- label: "Password",
- type: "password",
-};
-
-export const Search = Template.bind({});
-Search.args = {
- label: "Search",
- placeholder: "Search planets",
- type: "search",
-};
-
export const Tel = Template.bind({});
Tel.args = {
autoCompleteType: "mobile tel",
diff --git a/packages/odyssey-storybook/src/components/customization/CustomTheme.mdx b/packages/odyssey-storybook/src/contributing/CustomTheme.mdx
similarity index 100%
rename from packages/odyssey-storybook/src/components/customization/CustomTheme.mdx
rename to packages/odyssey-storybook/src/contributing/CustomTheme.mdx
diff --git a/packages/odyssey-storybook/src/components/customization/CustomTheme.stories.tsx b/packages/odyssey-storybook/src/contributing/CustomTheme.stories.tsx
similarity index 97%
rename from packages/odyssey-storybook/src/components/customization/CustomTheme.stories.tsx
rename to packages/odyssey-storybook/src/contributing/CustomTheme.stories.tsx
index 6ce723cc93..77591ac5ff 100644
--- a/packages/odyssey-storybook/src/components/customization/CustomTheme.stories.tsx
+++ b/packages/odyssey-storybook/src/contributing/CustomTheme.stories.tsx
@@ -70,7 +70,7 @@ export const TextFieldStory: StoryFn = () => {
return (
-
+
);
diff --git a/packages/odyssey-storybook/src/guidelines/Form Field Accessibility.stories.mdx b/packages/odyssey-storybook/src/guidelines/Form Field Accessibility.stories.mdx
new file mode 100644
index 0000000000..8a74300511
--- /dev/null
+++ b/packages/odyssey-storybook/src/guidelines/Form Field Accessibility.stories.mdx
@@ -0,0 +1,37 @@
+import { Meta } from "@storybook/addon-docs";
+
+
+
+## Form Field Accessibility
+
+### Placeholders
+
+Except for Search inputs, we advise against using placeholder text for inputs.
+
+#### Translation
+
+To prevent triggering a change in page layout, browsers don't translate certain attributes. Because of this, users will see untranslated placeholder text.
+
+#### Recall
+
+Placeholder text disappears when a field is interacted with. For this reason, it's not suitable for formatting guidelines or necessary context.
+
+#### Utility
+
+Placeholder content is limited to static text. Additionally, placeholder text is truncated beyond the width of its input.
+
+#### Field value confusion
+
+Low-contrast placeholders may be illegible for some users. Yet, placeholders with compliant contrast can be mistaken for field values. High Contrast Mode will make placeholders and values appear identical.
+
+Finally, Users with low digital literacy may not understand the purpose or behavior of placeholder text.
+
+### Purpose
+
+When collecting an individual's personal data, you must define the input's purpose via the `autocomplete` attribute. This allows users to automate the filling of fields and ensures the purpose is known, regardless of the label. A complete list of fields this is required for may be found in the WCAG spec.
+
+### Autofocus
+
+Except for very specific cases, we advise against using the `autoFocus` prop; unless used considerately, the sense of focus being "teleported" to an unexpected part of the page can be jarring to users, especially those using screen readers.
+
+More details can be found [on MDN's `autofocus` page](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus#accessibility_considerations).