diff --git a/.changeset/kind-experts-kneel.md b/.changeset/kind-experts-kneel.md new file mode 100644 index 000000000..803dc0915 --- /dev/null +++ b/.changeset/kind-experts-kneel.md @@ -0,0 +1,5 @@ +--- +"@suid/material": patch +--- + +Add `FilledInput` component diff --git a/ROADMAP.md b/ROADMAP.md index 799655047..472a74642 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -72,7 +72,7 @@ | Drawer | ✅ | | Fab | ✅ | | Fade | ✅ | -| FilledInput | ⏳ | +| FilledInput | ✅ | | FormControl | ✅ | | FormControlLabel | ✅ | | FormGroup | ✅ | diff --git a/packages/material/src/FilledInput/FilledInput.tsx b/packages/material/src/FilledInput/FilledInput.tsx new file mode 100644 index 000000000..afbb0cdd9 --- /dev/null +++ b/packages/material/src/FilledInput/FilledInput.tsx @@ -0,0 +1,226 @@ +import InputBase from "../InputBase"; +import { + rootOverridesResolver as inputBaseRootOverridesResolver, + inputOverridesResolver as inputBaseInputOverridesResolver, + InputBaseRoot, + InputBaseComponent as InputBaseInput, +} from "../InputBase/InputBase"; +import { PaletteColorName } from "../styles/createPalette"; +import styled from "../styles/styled"; +import { FilledInputTypeMap } from "./FilledInputProps"; +import filledInputClasses, { + getFilledInputUtilityClass, +} from "./filledInputClasses"; +import createComponentFactory from "@suid/base/createComponentFactory"; +import { ComponentProps } from "@suid/types"; +import deepmerge from "@suid/utils/deepmerge"; +import { createMemo, mergeProps } from "solid-js"; + +const $ = createComponentFactory()({ + name: "MuiFilledInput", + propDefaults: ({ set }) => + set({ + components: {}, + fullWidth: false, + inputComponent: "input", + multiline: false, + type: "text", + hiddenLabel: false, + }), + selfPropNames: ["classes", "disableUnderline", "hiddenLabel"], + utilityClass: getFilledInputUtilityClass, + slotClasses: (ownerState) => ({ + root: ["root", !ownerState.disableUnderline && "underline"], + input: ["input"], + }), +}); + +const FilledInputRoot = styled(InputBaseRoot, { + /*shouldForwardProp: (prop) => + rootShouldForwardProp(prop) || prop === "classes",*/ + name: "MuiFilledInput", + slot: "Root", + overridesResolver: (props, styles) => { + const { ownerState } = props; + return [ + ...inputBaseRootOverridesResolver(props as any, styles), + !ownerState.disableUnderline && styles.underline, + ]; + }, +})>(({ theme, ownerState }) => { + const light = theme.palette.mode === "light"; + const bottomLineColor = light + ? "rgba(0, 0, 0, 0.42)" + : "rgba(255, 255, 255, 0.7)"; + const backgroundColor = light + ? "rgba(0, 0, 0, 0.06)" + : "rgba(255, 255, 255, 0.09)"; + return { + position: "relative", + backgroundColor, + // [review] unitless + borderTopLeftRadius: `${theme.shape.borderRadius}px`, + borderTopRightRadius: `${theme.shape.borderRadius}px`, + transition: theme.transitions.create("background-color", { + duration: theme.transitions.duration.shorter, + easing: theme.transitions.easing.easeOut, + }), + "&:hover": { + backgroundColor: light + ? "rgba(0, 0, 0, 0.09)" + : "rgba(255, 255, 255, 0.13)", + // Reset on touch devices, it doesn't add specificity + "@media (hover: none)": { + backgroundColor, + }, + }, + [`&.${filledInputClasses.focused}`]: { + backgroundColor, + }, + [`&.${filledInputClasses.disabled}`]: { + backgroundColor: light + ? "rgba(0, 0, 0, 0.12)" + : "rgba(255, 255, 255, 0.12)", + }, + ...(!ownerState.disableUnderline && { + "&:after": { + borderBottom: `2px solid ${ + theme.palette[ownerState.color as PaletteColorName].main + }`, + left: 0, + bottom: 0, + // Doing the other way around crash on IE11 "''" https://github.com/cssinjs/jss/issues/242 + content: '""', + position: "absolute", + right: 0, + transform: "scaleX(0)", + transition: theme.transitions.create("transform", { + duration: theme.transitions.duration.shorter, + easing: theme.transitions.easing.easeOut, + }), + pointerEvents: "none", // Transparent to the hover style. + }, + [`&.${filledInputClasses.focused}:after`]: { + transform: "scaleX(1)", + }, + [`&.${filledInputClasses.error}:after`]: { + borderBottomColor: theme.palette.error.main, + transform: "scaleX(1)", // error is always underlined in red + }, + "&:before": { + borderBottom: `1px solid ${bottomLineColor}`, + left: 0, + bottom: 0, + // Doing the other way around crash on IE11 "''" https://github.com/cssinjs/jss/issues/242 + content: '"\\00a0"', + position: "absolute", + right: 0, + transition: theme.transitions.create("border-bottom-color", { + duration: theme.transitions.duration.shorter, + }), + pointerEvents: "none", // Transparent to the hover style. + }, + [`&:hover:not(.${filledInputClasses.disabled}):before`]: { + borderBottom: `1px solid ${theme.palette.text.primary}`, + }, + [`&.${filledInputClasses.disabled}:before`]: { + borderBottomStyle: "dotted", + }, + }), + ...(ownerState.startAdornment && { + paddingLeft: 12, + }), + ...(ownerState.endAdornment && { + paddingRight: 12, + }), + ...(ownerState.multiline && { + padding: "25px 12px 8px", + ...(ownerState.size === "small" && { + paddingTop: 21, + paddingBottom: 4, + }), + ...(ownerState.hiddenLabel && { + paddingTop: 16, + paddingBottom: 17, + }), + }), + }; +}); + +const FilledInputInput = styled(InputBaseInput, { + name: "MuiFilledInput", + slot: "Input", + overridesResolver: inputBaseInputOverridesResolver as any, +})>(({ theme, ownerState }) => ({ + paddingTop: 25, + paddingRight: 12, + paddingBottom: 8, + paddingLeft: 12, + "&:-webkit-autofill": { + WebkitBoxShadow: + theme.palette.mode === "light" ? null : "0 0 0 100px #266798 inset", + WebkitTextFillColor: theme.palette.mode === "light" ? null : "#fff", + caretColor: theme.palette.mode === "light" ? null : "#fff", + borderTopLeftRadius: "inherit", + borderTopRightRadius: "inherit", + }, + ...(ownerState.size === "small" && { + paddingTop: 21, + paddingBottom: 4, + }), + ...(ownerState.hiddenLabel && { + paddingTop: 16, + paddingBottom: 17, + }), + ...(ownerState.multiline && { + paddingTop: 0, + paddingBottom: 0, + paddingLeft: 0, + paddingRight: 0, + }), + ...(ownerState.startAdornment && { + paddingLeft: 0, + }), + ...(ownerState.endAdornment && { + paddingRight: 0, + }), + ...(ownerState.hiddenLabel && + ownerState.size === "small" && { + paddingTop: 8, + paddingBottom: 9, + }), +})); + +const FilledInput = $.component(function FilledInput({ + allProps, + classes, + otherProps, + props, +}) { + const componentProps = createMemo(() => { + const filledInputComponentsProps = { + root: { ownerState: allProps }, + input: { ownerState: allProps }, + }; + return otherProps.componentsProps + ? deepmerge(otherProps.componentsProps, filledInputComponentsProps) + : filledInputComponentsProps; + }); + + const allClasses = mergeProps(() => props.classes || {}, classes); + + return ( + + ); +}); + +export default FilledInput; diff --git a/packages/material/src/FilledInput/FilledInputProps.tsx b/packages/material/src/FilledInput/FilledInputProps.tsx new file mode 100644 index 000000000..254a97bbf --- /dev/null +++ b/packages/material/src/FilledInput/FilledInputProps.tsx @@ -0,0 +1,36 @@ +import { InputBaseProps } from "../InputBase"; +import { FilledInputClasses } from "./filledInputClasses"; +import SxProps from "@suid/system/sxProps"; +import { ElementType } from "@suid/types"; + +export interface FilledInputTypeMap

{ + name: "MuiFilledInput"; + defaultPropNames: "hiddenLabel"; + selfProps: { + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + /** + * If `true`, the label is hidden. + * This is used to increase density for a `FilledInput`. + * Be sure to add `aria-label` to the `input` element. + * @default false + */ + hiddenLabel?: boolean; + /** + * If `true`, the input will not have an underline. + */ + disableUnderline?: boolean; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; + }; + props: P & InputBaseProps & FilledInputTypeMap["selfProps"]; + defaultComponent: D; +} + +export type FilledInputProps = FilledInputTypeMap["props"]; + +export default FilledInputProps; diff --git a/packages/material/src/FilledInput/filledInputClasses.ts b/packages/material/src/FilledInput/filledInputClasses.ts new file mode 100644 index 000000000..d86120858 --- /dev/null +++ b/packages/material/src/FilledInput/filledInputClasses.ts @@ -0,0 +1,69 @@ +import { generateUtilityClasses, generateUtilityClass } from "@suid/base"; + +export interface FilledInputClasses { + /** Styles applied to the root element. */ + root: string; + /** Styles applied to the root element if color secondary. */ + colorSecondary: string; + /** Styles applied to the root element unless `disableUnderline={true}`. */ + underline: string; + /** State class applied to the root element if the component is focused. */ + focused: string; + /** State class applied to the root element if `disabled={true}`. */ + disabled: string; + /** Styles applied to the root element if `startAdornment` is provided. */ + adornedStart: string; + /** Styles applied to the root element if `endAdornment` is provided. */ + adornedEnd: string; + /** State class applied to the root element if `error={true}`. */ + error: string; + /** Styles applied to the input element if `size="small"`. */ + sizeSmall: string; + /** Styles applied to the root element if `multiline={true}`. */ + multiline: string; + /** Styles applied to the root element if `hiddenLabel={true}`. */ + hiddenLabel: string; + /** Styles applied to the input element. */ + input: string; + /** Styles applied to the input element if `size="small"`. */ + inputSizeSmall: string; + /** Styles applied to the `input` if in ``. */ + inputHiddenLabel: string; + /** Styles applied to the input element if `multiline={true}`. */ + inputMultiline: string; + /** Styles applied to the input element if `startAdornment` is provided. */ + inputAdornedStart: string; + /** Styles applied to the input element if `endAdornment` is provided. */ + inputAdornedEnd: string; +} + +export type FilledInputClassKey = keyof FilledInputClasses; + +export function getFilledInputUtilityClass(slot: string): string { + return generateUtilityClass("MuiFilledInput", slot); +} + +const filledInputClasses: FilledInputClasses = generateUtilityClasses( + "MuiFilledInput", + [ + "root", + "colorSecondary", + "underline", + "focused", + "disabled", + "adornedStart", + "adornedEnd", + "error", + "sizeSmall", + "multiline", + "hiddenLabel", + "input", + "inputSizeSmall", + "inputHiddenLabel", + "inputMultiline", + "inputAdornedStart", + "inputAdornedEnd", + ] +); + +export default filledInputClasses; diff --git a/packages/material/src/FilledInput/index.tsx b/packages/material/src/FilledInput/index.tsx new file mode 100644 index 000000000..1c19e8b2e --- /dev/null +++ b/packages/material/src/FilledInput/index.tsx @@ -0,0 +1,6 @@ +export { default } from "./FilledInput"; +export * from "./FilledInput"; +export * from "./FilledInputProps"; + +export { default as FilledInputClasses } from "./filledInputClasses"; +export * from "./filledInputClasses"; diff --git a/packages/material/src/styles/components-types.ts b/packages/material/src/styles/components-types.ts index 685e788e6..40c87a36f 100644 --- a/packages/material/src/styles/components-types.ts +++ b/packages/material/src/styles/components-types.ts @@ -23,6 +23,7 @@ export { default as MuiDivider } from "../Divider"; export { default as MuiDrawer } from "../Drawer"; export { default as MuiFab } from "../Fab"; export { default as MuiFade } from "../Fade"; +export { default as MuiFilledInput } from "../FilledInput"; export { default as MuiFormControl } from "../FormControl"; export { default as MuiFormControlLabel } from "../FormControlLabel"; export { default as MuiFormGroup } from "../FormGroup"; diff --git a/scripts/actions/genRoadmap.ts b/scripts/actions/genRoadmap.ts index e2eb8cc7f..190119bb9 100644 --- a/scripts/actions/genRoadmap.ts +++ b/scripts/actions/genRoadmap.ts @@ -29,12 +29,7 @@ const codemodTransformers: Record = { "`React.Fragment`": true, }; -const pendingComponents = [ - "TextField", - "FilledInput", - "OutlinedInput", - "Select", -]; +const pendingComponents = ["TextField", "OutlinedInput", "Select"]; function stateIcon(state: boolean | "pending") { if (state === true) return "✅";