Skip to content

Commit

Permalink
feat(material): add NativeSelect component
Browse files Browse the repository at this point in the history
  • Loading branch information
juanrgm committed Jul 9, 2022
1 parent 79336bc commit 5855779
Show file tree
Hide file tree
Showing 8 changed files with 465 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/four-keys-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@suid/material": minor
---

Add `NativeSelect` component
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
| MenuList | |
| MobileStepper | |
| Modal ||
| NativeSelect | |
| NativeSelect | |
| NoSsr | |
| OutlinedInput ||
| Pagination | |
Expand Down
114 changes: 114 additions & 0 deletions packages/material/src/NativeSelect/NativeSelect.tsx
Original file line number Diff line number Diff line change
@@ -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<NativeSelectTypeMap>()({
name: "MuiNativeSelect",
selfPropNames: [
"children",
"classes",
"IconComponent",
"input",
"inputProps",
"onChange",
"value",
"variant",
],
utilityClass: getNativeSelectUtilityClasses,
slotClasses: () => ({
root: ["root"],
}),
});

const defaultInput = () => <Input />;
/**
* An alternative to `<Select native />` with a much smaller bundle size footprint.
*
* Demos:
*
* - [Selects](https://mui.com/components/selects/)
*
* API:
*
* - [NativeSelect API](https://mui.com/api/native-select/)
* - inherits [Input API](https://mui.com/api/input/)
*/
const NativeSelect = $.defineComponent(function NativeSelect(inProps) {
const props = $.useThemeProps({ props: inProps });
const [, other] = splitProps(props, [
"className",
"children",
"classes",
"IconComponent",
"input",
"inputProps",
"variant",
]);

const baseProps = mergeProps(
{
classes: {},
IconComponent: ArrowDropDownIcon,
input: defaultInput,
},
props
);

if (baseProps.input !== defaultInput)
// This feat requires component introspection (not supported by SolidJS)
throw new Error(`NativeSelect 'input' custom property is not supported.`);

const muiFormControl = useFormControl();
const fcs = formControlState({
props: props,
muiFormControl: muiFormControl,
states: ["variant"],
});

const ownerState = mergeProps(props, {
get classes() {
return baseProps.classes;
},
});
const classes = $.useClasses(ownerState);
const [, otherClasses] = splitProps(baseProps.classes, ["root"]);

const inputProps = mergeProps(
{
get children() {
return props.children;
},
classes: otherClasses,
get IconComponent() {
return baseProps.IconComponent;
},
get variant() {
return fcs.variant;
},
type: undefined,
},
() => props.inputProps || {}
//() => (baseProps.input ? baseProps.input.props.inputProps : {})
);

return (
<Input
inputComponent={NativeSelectInput as JSXElement}
inputProps={inputProps as InputProps["inputProps"]}
{...(other as InputProps)}
className={clsx(
classes.root,
//baseProps.input.props.className,
props.className
)}
/>
);
});

export default NativeSelect;
203 changes: 203 additions & 0 deletions packages/material/src/NativeSelect/NativeSelectInput.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLSelectElement>;
variant?: "standard" | "outlined" | "filled";
sx?: SxProps<Theme>;
}

const useUtilityClasses = (
ownerState: NativeSelectInputProps & {
open?: boolean;
classes?: Record<string, string>;
}
) => {
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<Theme> = ({ 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<Theme> = ({ 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 (
<>
<NativeSelectSelect
ownerState={ownerState}
class={clsx(classes.select, props.className)}
disabled={props.disabled}
ref={props.inputRef || ref}
{...other}
/>
<Show when={!props.multiple}>
<NativeSelectIcon
component={props.IconComponent}
ownerState={ownerState}
class={classes.icon}
/>
</Show>
</>
);
};
export default NativeSelectInput;
Loading

0 comments on commit 5855779

Please sign in to comment.