From b2ed678fcc4e6f6618eb14854b9aa8bb7f52fa43 Mon Sep 17 00:00:00 2001 From: Juanra GM Date: Tue, 29 Mar 2022 02:37:31 +0200 Subject: [PATCH] feat(material): add `Switch` component --- .changeset/neat-poems-melt.md | 5 + packages/material/src/Switch/Switch.tsx | 249 ++++++++++++++++++ packages/material/src/Switch/SwitchProps.tsx | 73 +++++ packages/material/src/Switch/index.tsx | 6 + packages/material/src/Switch/switchClasses.ts | 54 ++++ 5 files changed, 387 insertions(+) create mode 100644 .changeset/neat-poems-melt.md create mode 100644 packages/material/src/Switch/Switch.tsx create mode 100644 packages/material/src/Switch/SwitchProps.tsx create mode 100644 packages/material/src/Switch/index.tsx create mode 100644 packages/material/src/Switch/switchClasses.ts diff --git a/.changeset/neat-poems-melt.md b/.changeset/neat-poems-melt.md new file mode 100644 index 000000000..1450c0d83 --- /dev/null +++ b/.changeset/neat-poems-melt.md @@ -0,0 +1,5 @@ +--- +"@suid/material": patch +--- + +Add `Switch` component diff --git a/packages/material/src/Switch/Switch.tsx b/packages/material/src/Switch/Switch.tsx new file mode 100644 index 000000000..4feac452f --- /dev/null +++ b/packages/material/src/Switch/Switch.tsx @@ -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()({ + 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(() => ( + + )); + const allClasses = mergeProps(classes, () => ({ + root: classes.switchBase, + })); + const [, baseProps] = splitProps(otherProps, ["sx"]); + + return ( + + + + + ); +}); + +export default Switch; diff --git a/packages/material/src/Switch/SwitchProps.tsx b/packages/material/src/Switch/SwitchProps.tsx new file mode 100644 index 000000000..791be8c60 --- /dev/null +++ b/packages/material/src/Switch/SwitchProps.tsx @@ -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

{ + 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; + /** + * 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; + /** + * 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; + defaultComponent: D; +} + +export type SwitchProps< + D extends ElementType = SwitchTypeMap["defaultComponent"], + P = {} +> = OverrideProps, D>; + +export default SwitchProps; diff --git a/packages/material/src/Switch/index.tsx b/packages/material/src/Switch/index.tsx new file mode 100644 index 000000000..fd5ed94d7 --- /dev/null +++ b/packages/material/src/Switch/index.tsx @@ -0,0 +1,6 @@ +export { default } from "./Switch"; +export * from "./Switch"; +export * from "./SwitchProps"; + +export { default as switchClasses } from "./switchClasses"; +export * from "./switchClasses"; diff --git a/packages/material/src/Switch/switchClasses.ts b/packages/material/src/Switch/switchClasses.ts new file mode 100644 index 000000000..b20fb9965 --- /dev/null +++ b/packages/material/src/Switch/switchClasses.ts @@ -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;