diff --git a/.changeset/four-keys-study.md b/.changeset/four-keys-study.md new file mode 100644 index 000000000..9dfd2b76d --- /dev/null +++ b/.changeset/four-keys-study.md @@ -0,0 +1,5 @@ +--- +"@suid/material": minor +--- + +Add `NativeSelect` component diff --git a/ROADMAP.md b/ROADMAP.md index 74d17f1c8..f4b367470 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -122,7 +122,7 @@ | MenuList | | | MobileStepper | | | Modal | ✅ | -| NativeSelect | | +| NativeSelect | ✅ | | NoSsr | | | OutlinedInput | ✅ | | Pagination | | diff --git a/packages/material/src/NativeSelect/NativeSelect.tsx b/packages/material/src/NativeSelect/NativeSelect.tsx new file mode 100644 index 000000000..64948c5fb --- /dev/null +++ b/packages/material/src/NativeSelect/NativeSelect.tsx @@ -0,0 +1,114 @@ +import { getNativeSelectUtilityClasses, NativeSelectTypeMap } from "."; +import formControlState from "../FormControl/formControlState"; +import useFormControl from "../FormControl/useFormControl"; +import Input, { InputProps } from "../Input"; +import ArrowDropDownIcon from "../internal/svg-icons/ArrowDropDown"; +import NativeSelectInput from "./NativeSelectInput"; +import createComponentFactory from "@suid/base/createComponentFactory"; +import clsx from "clsx"; +import { JSXElement, mergeProps, splitProps } from "solid-js"; + +const $ = createComponentFactory()({ + name: "MuiNativeSelect", + selfPropNames: [ + "children", + "classes", + "IconComponent", + "input", + "inputProps", + "onChange", + "value", + "variant", + ], + utilityClass: getNativeSelectUtilityClasses, + slotClasses: () => ({ + root: ["root"], + }), +}); + +const defaultInput = () => ; +/** + * An alternative to ` + ); +}); + +export default NativeSelect; diff --git a/packages/material/src/NativeSelect/NativeSelectInput.tsx b/packages/material/src/NativeSelect/NativeSelectInput.tsx new file mode 100644 index 000000000..bca429205 --- /dev/null +++ b/packages/material/src/NativeSelect/NativeSelectInput.tsx @@ -0,0 +1,203 @@ +import { Theme } from "../styles"; +import styled, { skipRootProps } from "../styles/styled"; +import capitalize from "../utils/capitalize"; +import { getNativeSelectUtilityClasses } from "./nativeSelectClasses"; +import nativeSelectClasses from "./nativeSelectClasses"; +import composeClasses from "@suid/base/composeClasses"; +import { SxProps } from "@suid/system"; +import createRef from "@suid/system/createRef"; +import { SxPropsObject } from "@suid/system/sxProps"; +import * as ST from "@suid/types"; +import clsx from "clsx"; +import { splitProps, mergeProps, Show } from "solid-js"; + +export interface NativeSelectInputProps extends ST.PropsOf<"select"> { + disabled?: boolean; + IconComponent: ST.ElementType; + inputRef?: ST.Ref; + variant?: "standard" | "outlined" | "filled"; + sx?: SxProps; +} + +const useUtilityClasses = ( + ownerState: NativeSelectInputProps & { + open?: boolean; + classes?: Record; + } +) => { + const slots = { + select: [ + "select", + ownerState.variant, + ownerState.disabled && "disabled", + ownerState.multiple && "multiple", + ], + icon: [ + "icon", + `icon${capitalize(ownerState.variant!)}`, + ownerState.open && "iconOpen", + ownerState.disabled && "disabled", + ], + }; + + return composeClasses( + slots, + getNativeSelectUtilityClasses, + ownerState.classes + ); +}; + +export const nativeSelectSelectStyles: (data: { + ownerState: any; + theme: Theme; +}) => SxPropsObject = ({ ownerState, theme }) => ({ + MozAppearance: "none", // Reset + WebkitAppearance: "none", // Reset + // When interacting quickly, the text can end up selected. + // Native select can't be selected either. + userSelect: "none", + borderRadius: 0, // Reset + cursor: "pointer", + "&:focus": { + // Show that it's not an text input + backgroundColor: + theme.palette.mode === "light" + ? "rgba(0, 0, 0, 0.05)" + : "rgba(255, 255, 255, 0.05)", + borderRadius: 0, // Reset Chrome style + }, + // Remove IE11 arrow + "&::-ms-expand": { + display: "none", + }, + [`&.${nativeSelectClasses.disabled}`]: { + cursor: "default", + }, + "&[multiple]": { + height: "auto", + }, + "&:not([multiple]) option, &:not([multiple]) optgroup": { + backgroundColor: theme.palette.background.paper, + }, + // Bump specificity to allow extending custom inputs + "&&&": { + paddingRight: 24, + minWidth: 16, // So it doesn't collapse. + }, + ...(ownerState.variant === "filled" && { + "&&&": { + paddingRight: 32, + }, + }), + ...(ownerState.variant === "outlined" && { + borderRadius: theme.shape.borderRadius, + "&:focus": { + borderRadius: theme.shape.borderRadius, // Reset the reset for Chrome style + }, + "&&&": { + paddingRight: 32, + }, + }), +}); + +const NativeSelectSelect = styled("select", { + name: "MuiNativeSelect", + slot: "Select", + skipProps: skipRootProps, + overridesResolver: (props, styles) => { + const { ownerState } = props; + + return [ + styles.select, + styles[ownerState.variant], + { [`&.${nativeSelectClasses.multiple}`]: styles.multiple }, + ]; + }, +})(nativeSelectSelectStyles); + +export const nativeSelectIconStyles: (data: { + ownerState: any; + theme: Theme; +}) => SxPropsObject = ({ ownerState, theme }) => ({ + // We use a position absolute over a flexbox in order to forward the pointer events + // to the input and to support wrapping tags.. + position: "absolute", + right: 0, + top: "calc(50% - .5em)", // Center vertically, height is 1em + pointerEvents: "none", // Don't block pointer events on the select under the icon. + color: theme.palette.action.active, + [`&.${nativeSelectClasses.disabled}`]: { + color: theme.palette.action.disabled, + }, + ...(ownerState.open && { + transform: "rotate(180deg)", + }), + ...(ownerState.variant === "filled" && { + right: 7, + }), + ...(ownerState.variant === "outlined" && { + right: 7, + }), +}); + +const NativeSelectIcon = styled("svg", { + name: "MuiNativeSelect", + slot: "Icon", + overridesResolver: (props, styles) => { + const { ownerState } = props; + return [ + styles.icon, + ownerState.variant && styles[`icon${capitalize(ownerState.variant)}`], + ownerState.open && styles.iconOpen, + ]; + }, +})(nativeSelectIconStyles); + +/** + * @ignore - internal component. + */ +const NativeSelectInput = function NativeSelectInput( + props: NativeSelectInputProps +) { + const ref = createRef(props); + + const [, other] = splitProps(props, [ + "className", + "disabled", + "IconComponent", + "inputRef", + "variant", + ]); + + const baseProps = mergeProps({ variant: "standard" }, props); + + const ownerState = mergeProps(props, { + get disabled() { + return props.disabled; + }, + get variant() { + return baseProps.variant; + }, + }); + + const classes = useUtilityClasses(ownerState); + return ( + <> + + + + + + ); +}; +export default NativeSelectInput; diff --git a/packages/material/src/NativeSelect/NativeSelectProps.tsx b/packages/material/src/NativeSelect/NativeSelectProps.tsx new file mode 100644 index 000000000..3663280cd --- /dev/null +++ b/packages/material/src/NativeSelect/NativeSelectProps.tsx @@ -0,0 +1,76 @@ +import { Theme } from ".."; +import { InputProps } from "../Input"; +import { NativeSelectInputProps } from "./NativeSelectInput"; +import { NativeSelectClasses } from "./nativeSelectClasses"; +import { SxProps } from "@suid/system"; +import * as ST from "@suid/types"; +import { JSXElement } from "solid-js"; + +export type NativeSelectTypeMap

= { + name: "MuiNativeSelect"; + defaultPropNames: "classes" | "IconComponent" | "input"; + selfProps: { + /** + * The option elements to populate the select with. + * Can be some `