diff --git a/packages/ui/fixtures/RadioGroup.fixture.tsx b/packages/ui/fixtures/RadioGroup.fixture.tsx new file mode 100644 index 0000000..f7a3ee6 --- /dev/null +++ b/packages/ui/fixtures/RadioGroup.fixture.tsx @@ -0,0 +1,70 @@ +import { useEffect, useRef } from "react"; +import { useValue } from "react-cosmos/client"; + +import { RadioGroup } from "../lib/main"; + +import styles from "./radioGroup.module.css"; + +export default function Fixture() { + const [valueA, setValueA] = useValue("ValueA", { defaultValue: "foo" }); + const [valueB, setValueB] = useValue("ValueB", { defaultValue: "foo" }); + + const groupRef = useRef(null); + const labelRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + if (groupRef.current) { + groupRef.current.style.background = "magenta"; + } + if (inputRef.current) { + inputRef.current.style.transform = "rotate(45deg)"; + } + if (labelRef.current) { + labelRef.current.style.color = "lime"; + } + }, []); + + return ( +
+ + + + + + +
+ The rain in Spain falls mainly on the plain. +
+ +
+ + + + + + +
+ ); +} diff --git a/packages/ui/fixtures/radioGroup.module.css b/packages/ui/fixtures/radioGroup.module.css new file mode 100644 index 0000000..d3cf2f2 --- /dev/null +++ b/packages/ui/fixtures/radioGroup.module.css @@ -0,0 +1,18 @@ +.radioGroup_horizontal { + margin-top: 1rem; + border-color: orangered; + background: green; + + label { + display: inline-block; + margin: 0; + } + + label + label { + margin-left: 1rem; + } +} + +.radioGroupItem_special { + border-width: 1rem; +} diff --git a/packages/ui/lib/RadioGroup/context.ts b/packages/ui/lib/RadioGroup/context.ts new file mode 100644 index 0000000..12550d6 --- /dev/null +++ b/packages/ui/lib/RadioGroup/context.ts @@ -0,0 +1,10 @@ +import { createContext } from "react"; + +export type RadioGroupContextValue = { + value: string; + onChange: (value: string) => void; +}; + +export const RadioGroupContext = createContext( + null, +); diff --git a/packages/ui/lib/RadioGroup/index.tsx b/packages/ui/lib/RadioGroup/index.tsx new file mode 100644 index 0000000..bb19c17 --- /dev/null +++ b/packages/ui/lib/RadioGroup/index.tsx @@ -0,0 +1,65 @@ +import { forwardRef, HTMLAttributes } from "react"; + +import { renderRadioGroup, renderRadioItem } from "./render"; +import { RadioGroupProps, useRadioGroup } from "./useRadioGroup"; +import { RadioGroupItemProps, useRadioGroupItem } from "./useRadioGroupItem"; + +/* + + A: BASE HOOK + + - Functionality: Implement bespoke functionality by extending vanilla Elements. + - Interface: Define an interface which is as element-agnostic as possible. + Consists of a ref, and `Props`, which is an extension or custom implementation + of vanilla Element(s). + + B: HOOK + + - Extends the [HOOK::BASE] by injecting styles. + + C: RENDER_FUNCTION + + - Connects a HOOK to Element(s). + - Elements can be of type: + 1. `Context`: ReactContext Provider. + 2. `Element`: Wrapper element nested within the context provider. + 3. ``: `Main` element(s) that are the primary focus of the component. + - Props reflect this pattern, for example: + + const renderArgs = { + ctx: {}, // Context + elementProps: {}, // Wrapper + inputProps: {}, // Custom + }; + + + D: COMPONENT (MAIN EXPORT) + + - Connects HOOKs to RENDER_FUNCTIONs. + +*/ + +const Group = forwardRef< + HTMLDivElement, + RadioGroupProps> +>((props, forwardedRef) => { + const renderArgs = useRadioGroup({ + props, + forwardedRef, + }); + return renderRadioGroup(renderArgs); +}); + +const Item = forwardRef( + (props, forwardedRef) => { + const renderArgs = useRadioGroupItem({ + props, + forwardedRef, + }); + return renderRadioItem(renderArgs); + }, +); + +export const RadioGroup = Object.assign(Group, { + Item, +}); diff --git a/packages/ui/lib/RadioGroup/render.tsx b/packages/ui/lib/RadioGroup/render.tsx new file mode 100644 index 0000000..867022b --- /dev/null +++ b/packages/ui/lib/RadioGroup/render.tsx @@ -0,0 +1,34 @@ +import { RadioGroupContext, RadioGroupContextValue } from "./context"; + +export function renderRadioItem({ + labelProps, + inputProps, + children, +}: { + labelProps: React.LabelHTMLAttributes; + inputProps: React.HTMLAttributes; + children: React.ReactNode; +}) { + return ( + + ); +} + +export function renderRadioGroup({ + ctx, + elementProps, + elementRef, +}: { + ctx: RadioGroupContextValue; + elementProps: React.HTMLAttributes; + elementRef: React.Ref; +}) { + return ( + +
+ + ); +} diff --git a/packages/ui/lib/RadioGroup/styles.module.css b/packages/ui/lib/RadioGroup/styles.module.css new file mode 100644 index 0000000..01f554f --- /dev/null +++ b/packages/ui/lib/RadioGroup/styles.module.css @@ -0,0 +1,21 @@ +.radioGroup { + width: fit-content; + border: 1rem solid black; + font-family: monospace; +} + +.radioGroupItemLabel { + display: block; + border: 3px solid yellow; + background: black; + color: white; +} + +.radioGroupItemLabel + .radioGroupItemLabel { + margin-top: 0.5rem; +} + +.radioGroupItemInput { + box-shadow: 0 0 0 5px rgb(0, 136, 255); + margin-right: 1rem; +} diff --git a/packages/ui/lib/RadioGroup/useBaseRadioGroup.ts b/packages/ui/lib/RadioGroup/useBaseRadioGroup.ts new file mode 100644 index 0000000..0946993 --- /dev/null +++ b/packages/ui/lib/RadioGroup/useBaseRadioGroup.ts @@ -0,0 +1,35 @@ +import { HTMLAttributes } from "react"; + +import { RadioGroupContextValue } from "./context"; + +export type BaseRadioGroupProps = Sys42Props< + { + value: string; + // TODO: Change to native onChange type. + // Confirm also if we will add onChangeValue. + onChange: (value: string) => void; + children: React.ReactNode; + }, + ElemProps +>; + +export type UseBaseRadioGroupProps = { + props: Props; + forwardedRef: React.ForwardedRef; +}; + +export function useBaseRadioGroup< + Props extends BaseRadioGroupProps>, + Elem extends HTMLElement, +>({ props, forwardedRef }: UseBaseRadioGroupProps) { + const { value, onChange, ...elementProps } = props; + + return { + ctx: { + value: value, + onChange, + } as RadioGroupContextValue, + elementProps, + elementRef: forwardedRef, + }; +} diff --git a/packages/ui/lib/RadioGroup/useBaseRadioGroupItem.ts b/packages/ui/lib/RadioGroup/useBaseRadioGroupItem.ts new file mode 100644 index 0000000..d65927c --- /dev/null +++ b/packages/ui/lib/RadioGroup/useBaseRadioGroupItem.ts @@ -0,0 +1,53 @@ +import { HTMLAttributes, useContext } from "react"; + +import { RadioGroupContext } from "./context"; + +export type BaseRadioGroupItemProps = Sys42Props< + { + value: string; + label: string; + name?: string; + inputRef?: React.Ref; + }, + Omit, "children"> +>; + +export type UseBaseRadioGroupItemProps = { + props: Props; + forwardedRef: React.ForwardedRef; +}; + +export function useBaseRadioGroupItem({ + props, + forwardedRef, +}: UseBaseRadioGroupItemProps) { + const { value, label, name, inputRef, ...passedOnProps } = props; + const context = useContext(RadioGroupContext); + + if (!context) { + throw new Error("RadioGroupItem must be used within a RadioGroup"); + } + + const { value: activeValue, onChange } = context; + + function handleChange() { + onChange(value); + } + + return { + labelProps: { + ref: forwardedRef, + ...passedOnProps, + }, + inputProps: { + value, + name, + onChange: handleChange, + ref: inputRef, + checked: activeValue === value, + type: "radio", + className: "", + }, + children: label, + }; +} diff --git a/packages/ui/lib/RadioGroup/useRadioGroup.ts b/packages/ui/lib/RadioGroup/useRadioGroup.ts new file mode 100644 index 0000000..6eae6c3 --- /dev/null +++ b/packages/ui/lib/RadioGroup/useRadioGroup.ts @@ -0,0 +1,26 @@ +import { HTMLAttributes } from "react"; +import { cn } from "@sys42/utils"; + +import { + BaseRadioGroupProps, + useBaseRadioGroup, + UseBaseRadioGroupProps, +} from "./useBaseRadioGroup"; + +import styles from "./styles.module.css"; + +export type RadioGroupProps = BaseRadioGroupProps; + +export function useRadioGroup< + Props extends RadioGroupProps>, + Elem extends HTMLElement, +>(options: UseBaseRadioGroupProps) { + const radioGroup = useBaseRadioGroup(options); + + radioGroup.elementProps.className = cn( + radioGroup.elementProps.className, + styles.radioGroup, + ); + + return radioGroup; +} diff --git a/packages/ui/lib/RadioGroup/useRadioGroupItem.ts b/packages/ui/lib/RadioGroup/useRadioGroupItem.ts new file mode 100644 index 0000000..e71dfba --- /dev/null +++ b/packages/ui/lib/RadioGroup/useRadioGroupItem.ts @@ -0,0 +1,26 @@ +import { cn } from "@sys42/utils"; + +import { + BaseRadioGroupItemProps, + useBaseRadioGroupItem, + UseBaseRadioGroupItemProps, +} from "./useBaseRadioGroupItem"; + +import styles from "./styles.module.css"; + +export type RadioGroupItemProps = BaseRadioGroupItemProps; + +export function useRadioGroupItem( + props: UseBaseRadioGroupItemProps, +) { + const radioGroupItem = useBaseRadioGroupItem(props); + + radioGroupItem.labelProps.className = cn( + radioGroupItem.labelProps.className, + styles.radioGroupItemLabel, + ); + + radioGroupItem.inputProps.className = styles.radioGroupItemInput; + + return radioGroupItem; +}