Skip to content

Commit

Permalink
feat: switch (#416)
Browse files Browse the repository at this point in the history
  • Loading branch information
junghyeonsu committed Jun 20, 2024
1 parent 45573df commit a83c373
Show file tree
Hide file tree
Showing 17 changed files with 682 additions and 221 deletions.
1 change: 1 addition & 0 deletions examples/stackflow-spa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@radix-ui/react-slot": "^1.0.2",
"@seed-design/icon": "^0.6.1",
"@seed-design/react-radio-group": "0.0.0",
"@seed-design/react-switch": "0.0.0",
"@seed-design/recipe": "0.0.0",
"@seed-design/stylesheet": "1.0.4",
"@stackflow/core": "^1.0.8",
Expand Down
2 changes: 2 additions & 0 deletions examples/stackflow-spa/src/activities/ActivityHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const ActivityHome: ActivityComponentType = () => {
<BoxButton onClick={() => push("ActivityChip", {})}>ChipButton</BoxButton>
<BoxButton onClick={() => push("ActivityAlertDialog", {})}>Dialog</BoxButton>
<BoxButton onClick={() => push("ActivityCallout", {})}>Callout</BoxButton>
<BoxButton onClick={() => push("ActivityCallout", {})}>Callout</BoxButton>
<BoxButton onClick={() => push("ActivitySwitch", {})}>Switch</BoxButton>
</div>
</AppScreen>
);
Expand Down
14 changes: 14 additions & 0 deletions examples/stackflow-spa/src/activities/ActivitySwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ActivityComponentType } from "@stackflow/react";

import { AppScreen } from "@stackflow/plugin-basic-ui";
import { Switch } from "../design-system/components";

const ActivitySwitch: ActivityComponentType = () => {
return (
<AppScreen appBar={{ title: "Switch" }}>
<Switch onCheckedChange={(checked) => console.log("checked", checked)} />
</AppScreen>
);
};

export default ActivitySwitch;
31 changes: 31 additions & 0 deletions examples/stackflow-spa/src/design-system/components/Switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { type UseSwitchProps, useSwitch } from "@seed-design/react-switch";
import { type SwitchVariantProps, switchStyle } from "@seed-design/recipe/switch";
import clsx from "clsx";
import * as React from "react";

import type { Assign } from "../util/types";
import { visuallyHidden } from "../util/visuallyHidden";

import "@seed-design/stylesheet/switch.css";

export interface SwitchProps
extends Assign<React.HTMLAttributes<HTMLInputElement>, UseSwitchProps>,
SwitchVariantProps {}

export const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
({ className, ...otherProps }, ref) => {
const { stateProps, restProps, controlProps, hiddenInputProps, rootProps, thumbProps } =
useSwitch(otherProps);
const classNames = switchStyle();

return (
<label className={clsx(classNames.root, className)} {...rootProps}>
<div {...controlProps} className={classNames.control}>
<div {...stateProps} {...thumbProps} className={classNames.thumb} />
</div>
<input ref={ref} {...hiddenInputProps} {...restProps} style={visuallyHidden} />
</label>
);
},
);
Switch.displayName = "Switch";
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from "./ChipToggleButton";
export * from "./Flex";
export * from "./RadioGroup";
export * from "./Text";
export * from "./Switch";
2 changes: 2 additions & 0 deletions examples/stackflow-spa/src/stackflow/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const { Stack, useFlow, useStepFlow } = stackflow({
ActivityAlertDialog: React.lazy(() => import("../activities/ActivityAlertDialog")),
ActivityChip: React.lazy(() => import("../activities/ActivityChip")),
ActivityCallout: React.lazy(() => import("../activities/ActivityCallout")),
ActivitySwitch: React.lazy(() => import("../activities/ActivitySwitch")),
ActivityNotFound,
},
plugins: [
Expand Down Expand Up @@ -54,6 +55,7 @@ const { Stack, useFlow, useStepFlow } = stackflow({
ActivityAlertDialog: "/alert",
ActivityChip: "/chip",
ActivityCallout: "/callout",
ActivitySwitch: "/switch",
ActivityNotFound: "/404",
},
}),
Expand Down
25 changes: 25 additions & 0 deletions packages/component-spec/artifacts/switch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
base:
enabled:
root:
height: 31px
width: 51px
control:
color: $color.palette.gray[500]
cornerRadius: $radii.full
paddingX: 2px
paddingY: 2px
thumb:
height: 27px
width: 27px
color: $color.fg.static-white
cornerRadius: $radii.full
shadow: 0px 3px 8px 0px rgba(0, 0, 0, 0.15), 0px 1px 3px 0px rgba(0, 0, 0, 0.06)
enabled,selected:
control:
color: $color.bg.brand-emphasis
disabled:
control:
color: $color.bg.disabled
disabled,selected:
root:
opacity: 0.38
46 changes: 46 additions & 0 deletions packages/react-headless/switch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "@seed-design/react-switch",
"version": "0.0.0",
"repository": {
"type": "git",
"url": "git+https://github.com/daangn/seed-design.git",
"directory": "packages/react-headless/switch"
},
"sideEffects": false,
"exports": {
".": {
"types": "./lib/index.d.ts",
"require": "./lib/index.js",
"import": "./lib/index.mjs"
}
},
"main": "./lib/index.js",
"files": [
"lib",
"src"
],
"scripts": {
"prepack": "rm -rf lib && yarn build",
"build": "nanobundle build"
},
"dependencies": {
"@radix-ui/react-use-controllable-state": "1.0.1",
"@seed-design/dom-utils": "0.0.0"
},
"devDependencies": {
"nanobundle": "^1.6.0"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
},
"publishConfig": {
"access": "public"
},
"ultra": {
"concurrent": [
"dev",
"build"
]
}
}
160 changes: 160 additions & 0 deletions packages/react-headless/switch/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { useControllableState } from "@radix-ui/react-use-controllable-state";
import { useId, useState } from "react";

import { dataAttr, elementProps, inputProps } from "@seed-design/dom-utils";

export interface UseSwitchStateProps {
checked?: boolean;

defaultChecked?: boolean;

onCheckedChange?: (checked: boolean) => void;
}

export function useSwitchState(props: UseSwitchStateProps) {
const [isChecked, setIsChecked] = useControllableState({
prop: props.checked,
defaultProp: props.defaultChecked,
onChange: props.onCheckedChange,
});
const [isHovered, setIsHovered] = useState(false);
const [isPressed, setIsPressed] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const [isFocusVisible, setIsFocusVisible] = useState(false);

return {
isChecked,
setIsChecked,
isHovered,
setIsHovered,
isPressed,
setIsPressed,
isFocused,
setIsFocused,
isFocusVisible,
setIsFocusVisible,
};
}

export interface UseSwitchProps extends UseSwitchStateProps {
disabled?: boolean;

required?: boolean;

invalid?: boolean;

name?: string;

form?: string;

value?: string;
}

export function useSwitch(props: UseSwitchProps) {
const id = useId();
const {
checked,
defaultChecked,
disabled,
form,
invalid,
name,
onCheckedChange,
required,
value,
...restProps
} = props;
const {
setIsChecked,
isChecked,
setIsHovered,
isHovered,
setIsPressed,
isPressed,
setIsFocused,
isFocused,
setIsFocusVisible,
isFocusVisible,
} = useSwitchState(props);

const stateProps = {
"data-checked": dataAttr(isChecked),
"data-hover": dataAttr(isHovered),
"data-active": dataAttr(isPressed),
"data-focus": dataAttr(isFocused),
"data-focus-visible": dataAttr(isFocusVisible),
"data-disabled": dataAttr(props.disabled),
};

return {
isChecked,
setIsChecked,
isFocused,
setIsFocused,
setIsFocusVisible,

restProps,
stateProps,
rootProps: elementProps({
...stateProps,
onPointerMove() {
setIsHovered(true);
},
onPointerDown() {
setIsPressed(true);
},
onPointerUp() {
setIsPressed(false);
},
onPointerLeave() {
setIsHovered(false);
setIsPressed(false);
},
}),

controlProps: elementProps({
...stateProps,
"aria-hidden": true,
}),

thumbProps: elementProps({
...stateProps,
"aria-hidden": true,
}),

hiddenInputProps: inputProps({
type: "checkbox",
role: "switch",
checked: isChecked,
disabled: props.disabled,
required: props.required,
"aria-invalid": props.invalid,
name: props.name || id,
form: props.form,
value: props.value,
...stateProps,
onChange(event) {
setIsChecked(event.currentTarget.checked);
setIsFocusVisible(event.target.matches(":focus-visible"));
},
onFocus(event) {
setIsFocused(true);
setIsFocusVisible(event.target.matches(":focus-visible"));
},
onBlur() {
setIsFocused(false);
setIsFocusVisible(false);
},
onKeyDown(event) {
if (event.key === " ") {
setIsPressed(true);
}
},
onKeyUp(event) {
if (event.key === " ") {
setIsPressed(false);
}
},
}),
};
}
9 changes: 9 additions & 0 deletions packages/react-headless/switch/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"rootDir": "src",
"outDir": "lib"
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/recipe-generator/preset/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import checkbox from "./checkbox.recipe";
import chip from "./chip.recipe";
import dialog from "./dialog.recipe";
import radio from "./radio.recipe";
import switchRecipe from "./switch.recipe";

const recipes = {
avatar,
Expand All @@ -14,6 +15,7 @@ const recipes = {
checkbox,
chip,
callout,
switch: switchRecipe,
};

export default recipes;
Loading

0 comments on commit a83c373

Please sign in to comment.