From 929729a67f96aa205440e4b44425a21b75fdaeac Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Tue, 14 Oct 2025 15:40:51 -0600 Subject: [PATCH] Toggle component with storybook and testing --- app/components/Toggle.js | 194 +++++++++++++++++ app/forms/page.js | 171 +++++++-------- stories/Toggle.stories.js | 122 +++++++++++ tests/accessibility/Toggle.a11y.test.jsx | 112 ++++++++++ tests/integration/Toggle.integration.test.jsx | 185 +++++++++++++++++ tests/unit/Toggle.test.jsx | 195 ++++++++++++++++++ 6 files changed, 886 insertions(+), 93 deletions(-) create mode 100644 app/components/Toggle.js create mode 100644 stories/Toggle.stories.js create mode 100644 tests/accessibility/Toggle.a11y.test.jsx create mode 100644 tests/integration/Toggle.integration.test.jsx create mode 100644 tests/unit/Toggle.test.jsx diff --git a/app/components/Toggle.js b/app/components/Toggle.js new file mode 100644 index 0000000..01a04f4 --- /dev/null +++ b/app/components/Toggle.js @@ -0,0 +1,194 @@ +import React, { memo, useCallback, useId, forwardRef } from "react"; + +const Toggle = forwardRef( + ( + { + label, + checked = false, + onChange, + onFocus, + onBlur, + disabled = false, + state = "default", + showIcon = false, + showText = false, + icon = "I", + text = "Toggle", + className = "", + ...props + }, + ref + ) => { + const toggleId = useId(); + const labelId = useId(); + + // Size styles - single size with specific dimensions + const sizeStyles = { + toggle: "h-[var(--measures-sizing-032)] px-[16px] py-[8px] gap-[4px]", + label: "text-[12px] leading-[16px]", + }; + + // State styles + const getStateStyles = () => { + if (disabled) { + return { + toggle: + "bg-[var(--color-surface-default-tertiary)] text-[var(--color-content-default-tertiary)] cursor-not-allowed", + label: "text-[var(--color-content-default-secondary)]", + }; + } + + if (checked) { + switch (state) { + case "hover": + return { + toggle: + "bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)]", + label: "text-[var(--color-content-default-secondary)]", + }; + case "focus": + return { + toggle: + "bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] shadow-[0_0_5px_1px_#3281F8]", + label: "text-[var(--color-content-default-secondary)]", + }; + default: + return { + toggle: + "bg-[var(--color-magenta-magenta100)] text-[var(--color-content-default-primary)] shadow-[0_0_0_1px_var(--color-border-default-brand-primary)]", + label: "text-[var(--color-content-default-secondary)]", + }; + } + } else { + switch (state) { + case "hover": + return { + toggle: + "bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)]", + label: "text-[var(--color-content-default-secondary)]", + }; + case "focus": + return { + toggle: + "bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] shadow-[0_0_5px_1px_#3281F8]", + label: "text-[var(--color-content-default-secondary)]", + }; + default: + return { + toggle: + "bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)]", + label: "text-[var(--color-content-default-secondary)]", + }; + } + } + }; + + const stateStyles = getStateStyles(); + const currentSize = sizeStyles; + + // Container classes + const containerClasses = "flex flex-col gap-[4px]"; + + const labelClasses = `${currentSize.label} font-inter font-medium`; + + const toggleClasses = ` + ${currentSize.toggle} + ${stateStyles.toggle} + rounded-full + font-inter + font-normal + text-[12px] + leading-[16px] + cursor-pointer + transition-all + duration-200 + focus:outline-none + focus-visible:shadow-[0_0_5px_1px_#3281F8] + ${!checked ? "hover:!bg-[var(--color-surface-default-secondary)]" : ""} + flex + items-center + justify-center + gap-[4px] + ${className} + ` + .trim() + .replace(/\s+/g, " "); + + const handleChange = useCallback( + (e) => { + if (!disabled && onChange) { + onChange(e); + } + }, + [disabled, onChange] + ); + + const handleFocus = useCallback( + (e) => { + if (!disabled && onFocus) { + onFocus(e); + } + }, + [disabled, onFocus] + ); + + const handleBlur = useCallback( + (e) => { + if (!disabled && onBlur) { + onBlur(e); + } + }, + [disabled, onBlur] + ); + + const handleKeyDown = useCallback( + (e) => { + if (!disabled && (e.key === "Enter" || e.key === " ")) { + e.preventDefault(); + if (onChange) { + onChange(e); + } + } + }, + [disabled, onChange] + ); + + return ( +
+ {label && ( + + )} +
+ +
+
+ ); + } +); + +Toggle.displayName = "Toggle"; + +export default memo(Toggle); diff --git a/app/forms/page.js b/app/forms/page.js index 9de3f8b..3453d6c 100644 --- a/app/forms/page.js +++ b/app/forms/page.js @@ -1,111 +1,96 @@ "use client"; import React, { useState } from "react"; -import TextArea from "../components/TextArea"; +import Toggle from "../components/Toggle"; export default function FormsPlayground() { - const [smallValue, setSmallValue] = useState(""); - const [mediumValue, setMediumValue] = useState(""); - const [largeValue, setLargeValue] = useState(""); - const [defaultLabelValue, setDefaultLabelValue] = useState(""); - const [horizontalLabelValue, setHorizontalLabelValue] = useState(""); - const [smallHorizontalValue, setSmallHorizontalValue] = useState(""); - const [smallDefaultValue, setSmallDefaultValue] = useState(""); - const [errorStateValue, setErrorStateValue] = useState(""); - const [disabledStateValue, setDisabledStateValue] = useState(""); + const [toggleStates, setToggleStates] = useState({ + default: false, + hover: false, + selected: true, + focus: false, + disabled: false, + icon: false, + text: false, + both: false, + }); + + const handleToggleChange = (key) => (e) => { + setToggleStates((prev) => ({ + ...prev, + [key]: !prev[key], + })); + }; return (

Forms Playground

-

TextArea Examples

-
-
-

Sizes

-
-