From 6c91ec2077cb8c721da2e85e3da12701cc636cb7 Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Sat, 8 Oct 2022 15:11:44 +0800 Subject: [PATCH] [ui][radio list] implementation --- apps/storybook/stories/radio-list.stories.tsx | 135 ++++++++++++++++++ packages/ui/src/RadioList/RadioList.tsx | 59 ++++++++ packages/ui/src/RadioList/RadioListContext.ts | 20 +++ packages/ui/src/RadioList/RadioListItem.tsx | 42 ++++++ packages/ui/src/index.tsx | 3 + 5 files changed, 259 insertions(+) create mode 100644 apps/storybook/stories/radio-list.stories.tsx create mode 100644 packages/ui/src/RadioList/RadioList.tsx create mode 100644 packages/ui/src/RadioList/RadioListContext.ts create mode 100644 packages/ui/src/RadioList/RadioListItem.tsx diff --git a/apps/storybook/stories/radio-list.stories.tsx b/apps/storybook/stories/radio-list.stories.tsx new file mode 100644 index 00000000..7da530bb --- /dev/null +++ b/apps/storybook/stories/radio-list.stories.tsx @@ -0,0 +1,135 @@ +import React, { useState } from 'react'; +import type { ComponentMeta } from '@storybook/react'; +import type { RadioListOrientation } from '@tih/ui'; +import { HorizontalDivider } from '@tih/ui'; +import { RadioList } from '@tih/ui'; + +const RadioListOrientations: ReadonlyArray = [ + 'horizontal', + 'vertical', +]; + +export default { + argTypes: { + description: { + control: 'text', + }, + label: { + control: 'text', + }, + orientation: { + control: { type: 'select' }, + options: RadioListOrientations, + }, + }, + component: RadioList, + title: 'RadioList', +} as ComponentMeta; + +export function Basic({ + description, + label, +}: Pick, 'description' | 'label'>) { + const items = [ + { + label: 'Apple', + value: 'apple', + }, + { + label: 'Banana', + value: 'banana', + }, + { + label: 'Orange', + value: 'orange', + }, + ]; + + return ( + + {items.map(({ label: itemLabel, value }) => ( + + ))} + + ); +} + +Basic.args = { + description: 'Your favorite fruit', + label: 'Choose a fruit', +}; + +export function Controlled() { + const items = [ + { + label: 'Apple', + value: 'apple', + }, + { + label: 'Banana', + value: 'banana', + }, + { + label: 'Orange', + value: 'orange', + }, + ]; + + const [value, setValue] = useState('apple'); + + return ( + setValue(newValue)}> + {items.map(({ label: itemLabel, value: itemValue }) => ( + + ))} + + ); +} + +export function Orientation() { + const items = [ + { + label: 'Apple', + value: 'apple', + }, + { + label: 'Banana', + value: 'banana', + }, + { + label: 'Orange', + value: 'orange', + }, + ]; + + return ( +
+ + {items.map(({ label: itemLabel, value: itemValue }) => ( + + ))} + + + + {items.map(({ label: itemLabel, value: itemValue }) => ( + + ))} + +
+ ); +} diff --git a/packages/ui/src/RadioList/RadioList.tsx b/packages/ui/src/RadioList/RadioList.tsx new file mode 100644 index 00000000..1d94fe8b --- /dev/null +++ b/packages/ui/src/RadioList/RadioList.tsx @@ -0,0 +1,59 @@ +import clsx from 'clsx'; +import type { ChangeEvent } from 'react'; + +import { RadioListContext } from './RadioListContext'; +import RadioListItem from './RadioListItem'; + +export type RadioListOrientation = 'horizontal' | 'vertical'; + +type Props = Readonly<{ + children: ReadonlyArray>; + defaultValue?: T; + description?: string; + isLabelHidden?: boolean; + label: string; + name?: string; + onChange?: (value: T, event: ChangeEvent) => void; + orientation?: RadioListOrientation; + value?: T; +}>; + +RadioList.Item = RadioListItem; + +export default function RadioList({ + children, + defaultValue, + description, + orientation = 'vertical', + isLabelHidden, + name, + label, + value, + onChange, +}: Props) { + return ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TODO: Figure out how to type the onChange. + +
+
+ + {description && ( +

{description}

+ )} +
+
+ TODO +
+ {children} +
+
+
+
+ ); +} diff --git a/packages/ui/src/RadioList/RadioListContext.ts b/packages/ui/src/RadioList/RadioListContext.ts new file mode 100644 index 00000000..6058bc09 --- /dev/null +++ b/packages/ui/src/RadioList/RadioListContext.ts @@ -0,0 +1,20 @@ +import type { ChangeEvent } from 'react'; +import { createContext, useContext } from 'react'; + +type RadioListContextValue = { + defaultValue?: T; + name?: string; + onChange?: ( + value: T, + event: ChangeEvent, + ) => undefined | void; + value?: T; +}; + +export const RadioListContext = + createContext | null>(null); +export function useRadioListContext(): RadioListContextValue | null { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TODO: Figure out how to type useContext with generics. + return useContext(RadioListContext); +} diff --git a/packages/ui/src/RadioList/RadioListItem.tsx b/packages/ui/src/RadioList/RadioListItem.tsx new file mode 100644 index 00000000..fd0e8c43 --- /dev/null +++ b/packages/ui/src/RadioList/RadioListItem.tsx @@ -0,0 +1,42 @@ +import { useId } from 'react'; + +import { useRadioListContext } from './RadioListContext'; + +type Props = Readonly<{ + label?: string; + value: T; +}>; + +export default function RadioListItem({ label, value }: Props) { + const id = useId(); + const context = useRadioListContext(); + + return ( +
+ { + context?.onChange?.(value, event); + } + : undefined + } + /> + +
+ ); +} diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index 2e14a0fe..545db2db 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -22,6 +22,9 @@ export { default as HorizontalDivider } from './HorizontalDivider/HorizontalDivi // Pagination export * from './Pagination/Pagination'; export { default as Pagination } from './Pagination/Pagination'; +// RadioList +export * from './RadioList/RadioList'; +export { default as RadioList } from './RadioList/RadioList'; // Select export * from './Select/Select'; export { default as Select } from './Select/Select';