Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement RadioGroup #21

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/ui/fixtures/RadioGroup.fixture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useValue } from "react-cosmos/client";

import { RadioGroup } from "../lib/main";

export default function Fixture() {
const [value, setValue] = useValue("Value", { defaultValue: "foo" });

return (
<div style={{ padding: "2rem" }}>
<RadioGroup value={value} onChange={setValue}>
<RadioGroup.Item value="foo" label="Foo" />
<RadioGroup.Item value="bar" label="Bar" />
<RadioGroup.Item value="baz" label="Baz" />
<RadioGroup.Item value="qux" label="Qux" />
<RadioGroup.Item value="quux" label="Quux" />
<RadioGroup.Item value="corge" label="Corge" />
</RadioGroup>
</div>
);
}
144 changes: 144 additions & 0 deletions packages/ui/lib/RadioGroup/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import {
createContext,
InputHTMLAttributes,
ReactNode,
useContext,
} from "react";
import { cn } from "@sys42/utils";

import { Sys42Props } from "../types";

import styles from "./styles.module.css";

/*

GENERAL NOTES:

- Something I don't like so much about this pattern is that the value prop type
of Group and Item aren't linked. The dream would be that the enum type that is
set to <Group value={<T>} /> would match and check the value
prop of its children <Item value={<T>} />. Not possible as far as I'm aware.

- I don't like so much the fact that the hook `useBaseRadioGroup` does not return
any relevant props for the child input. I wish we could do something like this,
although the execution is shitty:

```ts
const useBaseRadioGroup = ()=>({
groupElem,
inputProps,
})

const RadioGroup = (()=>{
let _inputProps = null;

const Main = (props)=>{
const { groupElem, inputProps } = useBaseRadioGroup(props);
_inputProps = inputProps
return groupElem;
};

Main.Item = ()=> <input {..._inputProps} />;

return Main;
})();
```

*/

// ------------------------------------------------------------------------------
// 👉 CONTEXT
// ------------------------------------------------------------------------------

type BaseRadioGroupContextProps = {
activeValue: string;
onChange: (value: string) => void;
};

// QUESTION: Is it really neccessary to define null first?
// The children won't render without the value being set, right?
const BaseRadioGroupContext = createContext({} as BaseRadioGroupContextProps);

// ------------------------------------------------------------------------------
// 👉 GROUP
// ------------------------------------------------------------------------------

type UseBaseRadioGroupProps = {
value: string;
onChange: (value: string) => void;
children: ReactNode;
};

function useBaseRadioGroup({
value,
onChange,
children,
}: UseBaseRadioGroupProps) {
return {
// TODO: Should probably have the groupElem return a wrapperDiv so that it would
wattsjay marked this conversation as resolved.
Show resolved Hide resolved
// be easy to for example change the Radios into a horizontal layout? Or maybe -
// this should be left to the consumer?
groupElem: (
<BaseRadioGroupContext.Provider value={{ activeValue: value, onChange }}>
{children}
</BaseRadioGroupContext.Provider>
),
};
}

export const Group = (props: UseBaseRadioGroupProps) => {
const { groupElem } = useBaseRadioGroup(props);
return groupElem;
};

// ------------------------------------------------------------------------------
// 👉 ITEM
// ------------------------------------------------------------------------------

type RadioGroupItemProps = {
value: string;
label: string;
};

// QUESTION: In this case, we do not need to merge any label
// props as we are not exposing it to the user?
wattsjay marked this conversation as resolved.
Show resolved Hide resolved
export type BaseRadioGroupItemProps = Sys42Props<
RadioGroupItemProps,
Partial<
Omit<InputHTMLAttributes<HTMLInputElement>, "checked" | "readOnly" | "type">
>
>;

function Item({
value,
label,
onClick,
className,
...nativeProps
}: BaseRadioGroupItemProps) {
const { activeValue, onChange } = useContext(BaseRadioGroupContext);

function handleClick(e: React.MouseEvent<HTMLInputElement>) {
onClick?.(e);
onChange(value);
}

return (
<label className={cn(className, styles.radio)}>
wattsjay marked this conversation as resolved.
Show resolved Hide resolved
<input
{...nativeProps}
readOnly
wattsjay marked this conversation as resolved.
Show resolved Hide resolved
type="radio"
onClick={handleClick}
checked={value === activeValue}
/>
{label}
</label>
);
}

// HMMM: Wondering why I'm able to do this more simply -
wattsjay marked this conversation as resolved.
Show resolved Hide resolved
// I might just need to make sure my TS is setup the same way.
export const RadioGroup = Object.assign(Group, {
Item,
});
48 changes: 48 additions & 0 deletions packages/ui/lib/RadioGroup/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
.radio {
display: flex;
flex-direction: row;
align-items: baseline;
gap: 0.5rem;

input[type="radio"] {
/* Add if not using autoprefixer */
-webkit-appearance: none;
/* Remove most all native input styles */
-moz-appearance: none;
appearance: none;
/* For iOS < 15 */
background-color: blue;
/* Not removed via appearance */
margin: 0;
font: inherit;
color: currentColor;
width: 1.15em;
height: 1.15em;
border: 0.15em solid red;
border-radius: 50%;
transform: translateY(-0.075em);
display: grid;
place-content: center;
}

input[type="radio"]::before {
content: "";
width: 0.65em;
height: 0.65em;
border-radius: 50%;
transform: scale(0);
transition: 120ms transform ease-in-out;
box-shadow: inset 1em 1em yellow;
/* Windows High Contrast Mode */
background-color: CanvasText;
}

input[type="radio"]:checked::before {
transform: scale(1);
}

input[type="radio"]:focus {
outline: max(2px, 0.15em) solid green;
outline-offset: max(2px, 0.15em);
}
}