Skip to content

Commit

Permalink
feat(material): add Switch component
Browse files Browse the repository at this point in the history
  • Loading branch information
juanrgm committed Mar 29, 2022
1 parent b720f68 commit b2ed678
Show file tree
Hide file tree
Showing 5 changed files with 387 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/neat-poems-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@suid/material": patch
---

Add `Switch` component
249 changes: 249 additions & 0 deletions packages/material/src/Switch/Switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import SwitchBase from "../internal/SwitchBase";
import styled from "../styles/styled";
import capitalize from "../utils/capitalize";
import { SwitchTypeMap } from "./SwitchProps";
import switchClasses, { getSwitchUtilityClass } from "./switchClasses";
import createComponentFactory from "@suid/base/createComponentFactory";
import { alpha, darken, lighten } from "@suid/system";
import clsx from "clsx";
import { createMemo, mergeProps, splitProps } from "solid-js";

const $ = createComponentFactory<SwitchTypeMap>()({
name: "MuiSwitch",
selfPropNames: [
"checkedIcon",
"classes",
"color",
"disabled",
"icon",
"size",
"value",
],
propDefaults: ({ set }) =>
set({
color: "primary",
size: "medium",
}),
utilityClass: getSwitchUtilityClass,
slotClasses: (ownerState) => ({
root: [
"root",
!!ownerState.edge && `edge${capitalize(ownerState.edge)}`,
`size${capitalize(ownerState.size)}`,
],
switchBase: [
"switchBase",
`color${capitalize(ownerState.color)}`,
!!ownerState.checked && "checked",
!!ownerState.disabled && "disabled",
],
thumb: ["thumb"],
track: ["track"],
input: ["input"],
}),
});

const SwitchRoot = styled("span", {
name: "MuiSwitch",
slot: "Root",
overridesResolver: (props, styles) => {
const { ownerState } = props;

return [
styles.root,
ownerState.edge && styles[`edge${capitalize(ownerState.edge)}`],
styles[`size${capitalize(ownerState.size)}`],
];
},
})(({ ownerState }) => ({
display: "inline-flex",
width: 34 + 12 * 2,
height: 14 + 12 * 2,
overflow: "hidden",
padding: 12,
boxSizing: "border-box",
position: "relative",
flexShrink: 0,
zIndex: 0, // Reset the stacking context.
verticalAlign: "middle", // For correct alignment with the text.
"@media print": {
colorAdjust: "exact",
},
...(ownerState.edge === "start" && {
marginLeft: -8,
}),
...(ownerState.edge === "end" && {
marginRight: -8,
}),
...(ownerState.size === "small" && {
width: 40,
height: 24,
padding: 7,
[`& .${switchClasses.thumb}`]: {
width: 16,
height: 16,
},
[`& .${switchClasses.switchBase}`]: {
padding: 4,
[`&.${switchClasses.checked}`]: {
transform: "translateX(16px)",
},
},
}),
}));

const SwitchSwitchBase = styled(SwitchBase, {
name: "MuiSwitch",
slot: "SwitchBase",
overridesResolver: (props, styles) => {
const { ownerState } = props;

return [
styles.switchBase,
{ [`& .${switchClasses.input}`]: styles.input },
ownerState.color !== "default" &&
styles[`color${capitalize(ownerState.color)}`],
];
},
})(
({ theme }) => ({
position: "absolute",
top: 0,
left: 0,
zIndex: 1, // Render above the focus ripple.
color:
theme.palette.mode === "light"
? theme.palette.common.white
: theme.palette.grey[300],
transition: theme.transitions.create(["left", "transform"], {
duration: theme.transitions.duration.shortest,
}),
[`&.${switchClasses.checked}`]: {
transform: "translateX(20px)",
},
[`&.${switchClasses.disabled}`]: {
color:
theme.palette.mode === "light"
? theme.palette.grey[100]
: theme.palette.grey[600],
},
[`&.${switchClasses.checked} + .${switchClasses.track}`]: {
opacity: 0.5,
},
[`&.${switchClasses.disabled} + .${switchClasses.track}`]: {
opacity: theme.palette.mode === "light" ? 0.12 : 0.2,
},
[`& .${switchClasses.input}`]: {
left: "-100%",
width: "300%",
},
}),
({ theme, ownerState }) => ({
"&:hover": {
backgroundColor: alpha(
theme.palette.action.active,
theme.palette.action.hoverOpacity
),
// Reset on touch devices, it doesn't add specificity
"@media (hover: none)": {
backgroundColor: "transparent",
},
},
...(ownerState.color !== "default" && {
[`&.${switchClasses.checked}`]: {
color: theme.palette[ownerState.color].main,
"&:hover": {
backgroundColor: alpha(
theme.palette[ownerState.color].main,
theme.palette.action.hoverOpacity
),
"@media (hover: none)": {
backgroundColor: "transparent",
},
},
[`&.${switchClasses.disabled}`]: {
color:
theme.palette.mode === "light"
? lighten(theme.palette[ownerState.color].main, 0.62)
: darken(theme.palette[ownerState.color].main, 0.55),
},
},
[`&.${switchClasses.checked} + .${switchClasses.track}`]: {
backgroundColor: theme.palette[ownerState.color].main,
},
}),
})
);

const SwitchTrack = styled("span", {
name: "MuiSwitch",
slot: "Track",
overridesResolver: (props, styles) => styles.track,
})(({ theme }) => ({
height: "100%",
width: "100%",
borderRadius: 14 / 2,
zIndex: -1,
transition: theme.transitions.create(["opacity", "background-color"], {
duration: theme.transitions.duration.shortest,
}),
backgroundColor:
theme.palette.mode === "light"
? theme.palette.common.black
: theme.palette.common.white,
opacity: theme.palette.mode === "light" ? 0.38 : 0.3,
}));

const SwitchThumb = styled("span", {
name: "MuiSwitch",
slot: "Thumb",
overridesResolver: (props, styles) => styles.thumb,
})(({ theme }) => ({
boxShadow: theme.shadows[1],
backgroundColor: "currentColor",
width: 20,
height: 20,
borderRadius: "50%",
}));

/**
*
* Demos:
*
* - [Switches](https://mui.com/components/switches/)
* - [Transfer List](https://mui.com/components/transfer-list/)
*
* API:
*
* - [Switch API](https://mui.com/api/switch/)
* - inherits [IconButton API](https://mui.com/api/icon-button/)
*/
const Switch = $.component(function Switch({ allProps, classes, otherProps }) {
const icon = createMemo(() => (
<SwitchThumb className={classes.thumb} ownerState={allProps} />
));
const allClasses = mergeProps(classes, () => ({
root: classes.switchBase,
}));
const [, baseProps] = splitProps(otherProps, ["sx"]);

return (
<SwitchRoot
className={clsx(classes.root, otherProps.className)}
sx={otherProps.sx}
ownerState={allProps}
>
<SwitchSwitchBase
type="checkbox"
icon={icon()}
checkedIcon={icon()}
ownerState={allProps}
{...baseProps}
classes={allClasses}
/>
<SwitchTrack className={classes.track} ownerState={allProps} />
</SwitchRoot>
);
});

export default Switch;
73 changes: 73 additions & 0 deletions packages/material/src/Switch/SwitchProps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Theme } from "..";
import { OverrideProps } from "../OverridableComponent";
import SwitchBaseProps from "../internal/SwitchBaseProps";
import { SwitchClasses } from "./switchClasses";
import { SxProps } from "@suid/system";
import { ElementType, OverridableStringUnion } from "@suid/types";
import { JSXElement } from "solid-js";

export interface SwitchPropsSizeOverrides {}
export interface SwitchPropsColorOverrides {}

export interface SwitchTypeMap<P = {}, D extends ElementType = "div"> {
name: "MuiSwitch";
defaultPropNames: "color" | "size";
selfProps: {
/**
* The icon to display when the component is checked.
*/
checkedIcon?: JSXElement;
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<SwitchClasses>;
/**
* The color of the component. It supports those theme colors that make sense for this component.
* @default 'primary'
*/
color?: OverridableStringUnion<
| "primary"
| "secondary"
| "error"
| "info"
| "success"
| "warning"
| "default",
SwitchPropsColorOverrides
>;
/**
* If `true`, the component is disabled.
*/
disabled?: boolean;
/**
* The icon to display when the component is unchecked.
*/
icon?: JSXElement;
/**
* The size of the component.
* `small` is equivalent to the dense switch styling.
* @default 'medium'
*/
size?: OverridableStringUnion<"small" | "medium", SwitchPropsSizeOverrides>;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
/**
* The value of the component. The DOM API casts this to a string.
* The browser uses "on" as the default value.
*/
value?: unknown;
};
props: P &
SwitchTypeMap["selfProps"] &
Omit<SwitchBaseProps, "checkedIcon" | "color" | "icon">;
defaultComponent: D;
}

export type SwitchProps<
D extends ElementType = SwitchTypeMap["defaultComponent"],
P = {}
> = OverrideProps<SwitchTypeMap<P, D>, D>;

export default SwitchProps;
6 changes: 6 additions & 0 deletions packages/material/src/Switch/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { default } from "./Switch";
export * from "./Switch";
export * from "./SwitchProps";

export { default as switchClasses } from "./switchClasses";
export * from "./switchClasses";
54 changes: 54 additions & 0 deletions packages/material/src/Switch/switchClasses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { generateUtilityClass, generateUtilityClasses } from "@suid/base";

export interface SwitchClasses {
/** Styles applied to the root element. */
root: string;
/** Styles applied to the root element if `edge="start"`. */
edgeStart: string;
/** Styles applied to the root element if `edge="end"`. */
edgeEnd: string;
/** Styles applied to the internal `SwitchBase` component's `root` class. */
switchBase: string;
/** Styles applied to the internal SwitchBase component's root element if `color="primary"`. */
colorPrimary: string;
/** Styles applied to the internal SwitchBase component's root element if `color="secondary"`. */
colorSecondary: string;
/** Styles applied to the root element if `size="small"`. */
sizeSmall: string;
/** Styles applied to the root element if `size="medium"`. */
sizeMedium: string;
/** State class applied to the internal `SwitchBase` component's `checked` class. */
checked: string;
/** State class applied to the internal SwitchBase component's disabled class. */
disabled: string;
/** Styles applied to the internal SwitchBase component's input element. */
input: string;
/** Styles used to create the thumb passed to the internal `SwitchBase` component `icon` prop. */
thumb: string;
/** Styles applied to the track element. */
track: string;
}

export type SwitchClassKey = keyof SwitchClasses;

export function getSwitchUtilityClass(slot: string): string {
return generateUtilityClass("MuiSwitch", slot);
}

const switchClasses: SwitchClasses = generateUtilityClasses("MuiSwitch", [
"root",
"edgeStart",
"edgeEnd",
"switchBase",
"colorPrimary",
"colorSecondary",
"sizeSmall",
"sizeMedium",
"checked",
"disabled",
"input",
"thumb",
"track",
]);

export default switchClasses;

0 comments on commit b2ed678

Please sign in to comment.