From afaa24f81bf0d51a76108c814b6371835c134b2b Mon Sep 17 00:00:00 2001 From: nina992 <89770889+nina992@users.noreply.github.com> Date: Fri, 8 Sep 2023 10:40:40 +0400 Subject: [PATCH] chore(web): radio and radio group field components (#650) Co-authored-by: nina992 --- .../RadioGroup/RadioBox/index.stories.tsx | 35 ++++++++ .../components/RadioGroup/RadioBox/index.tsx | 86 +++++++++++++++++++ .../components/RadioGroup/index.stories.tsx | 54 ++++++++++++ web/src/beta/components/RadioGroup/index.tsx | 53 ++++++++++++ 4 files changed, 228 insertions(+) create mode 100644 web/src/beta/components/RadioGroup/RadioBox/index.stories.tsx create mode 100644 web/src/beta/components/RadioGroup/RadioBox/index.tsx create mode 100644 web/src/beta/components/RadioGroup/index.stories.tsx create mode 100644 web/src/beta/components/RadioGroup/index.tsx diff --git a/web/src/beta/components/RadioGroup/RadioBox/index.stories.tsx b/web/src/beta/components/RadioGroup/RadioBox/index.stories.tsx new file mode 100644 index 0000000000..f6bbbff586 --- /dev/null +++ b/web/src/beta/components/RadioGroup/RadioBox/index.stories.tsx @@ -0,0 +1,35 @@ +import { useArgs } from "@storybook/preview-api"; +import { Meta, StoryObj } from "@storybook/react"; +import { useCallback } from "react"; + +import RadioBox from "."; + +type Props = React.ComponentProps; + +const meta: Meta = { + component: RadioBox, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = (args: Props) => { + const [_, updateArgs] = useArgs(); + + const handleChange = useCallback( + (value: string) => { + console.log(value); + updateArgs({ value }); + }, + [updateArgs], + ); + + return ; +}; + +Default.args = { + inactive: false, + selected: false, + value: "test", +}; diff --git a/web/src/beta/components/RadioGroup/RadioBox/index.tsx b/web/src/beta/components/RadioGroup/RadioBox/index.tsx new file mode 100644 index 0000000000..ae2e9b728e --- /dev/null +++ b/web/src/beta/components/RadioGroup/RadioBox/index.tsx @@ -0,0 +1,86 @@ +import { useCallback, useState } from "react"; + +import { fonts, styled } from "@reearth/services/theme"; + +export type Props = { + inactive?: boolean; + selected?: boolean; + value: string; + onClick?: (value: string) => void; +}; + +const RadioBox: React.FC = ({ inactive, selected, value, onClick }: Props) => { + const [isChecked, setIsChecked] = useState(selected ?? false); + + const handleRadioClick = useCallback(() => { + setIsChecked(!isChecked); + if (onClick) onClick(value); + }, [isChecked, onClick, value]); + + return ( + + + + {isChecked && } + + {value} + + ); +}; +export default RadioBox; + +const Checkmark = styled.div<{ + inactive?: boolean; + selected?: boolean; +}>` + width: 10px; + height: 10px; + border-radius: 50%; + background-color: white; + background-color: ${({ selected, inactive, theme }) => + selected ? theme.select.main : inactive ? theme.content.weaker : theme.content.main}; +`; + +const Radio = styled.label<{ + inactive?: boolean; + selected?: boolean; +}>` + display: flex; + align-items: center; + min-width: 30px; + min-height: 30px; + list-style: none; + padding: 4px; + font-size: ${fonts.sizes.footnote}px; + cursor: pointer; + box-sizing: border-box; + border-radius: 2px; + :not(:last-child) { + margin-right: 1px; + } +`; + +const RadioInput = styled.input` + opacity: 0; + position: absolute; +`; + +const RadioButton = styled.span<{ + inactive?: boolean; + selected?: boolean; +}>` + width: 16px; + height: 16px; + border-radius: 50%; + border: 2px solid + ${({ selected, inactive, theme }) => + selected ? theme.select.main : inactive ? theme.content.weaker : theme.content.main}; + margin-right: 4px; + display: flex; + justify-content: center; + align-items: center; +`; + +const RadioText = styled.span` + flex-grow: 1; +`; diff --git a/web/src/beta/components/RadioGroup/index.stories.tsx b/web/src/beta/components/RadioGroup/index.stories.tsx new file mode 100644 index 0000000000..6eb1d9d6e1 --- /dev/null +++ b/web/src/beta/components/RadioGroup/index.stories.tsx @@ -0,0 +1,54 @@ +import { useArgs } from "@storybook/preview-api"; +import { Meta, StoryObj } from "@storybook/react"; +import { useCallback } from "react"; + +import RadioGroup from "./index"; + +type Props = React.ComponentProps; + +const meta: Meta = { + component: RadioGroup, +}; + +export default meta; + +type Story = StoryObj; + +const options = [ + { key: "option1", value: "Option 1", selected: false }, + { key: "option2", value: "Option 2", selected: false }, +]; + +export const VerticalRadioGroup: Story = (args: Props) => { + const [_, updateArgs] = useArgs(); + + const handleChange = useCallback((value: string) => updateArgs({ value }), [updateArgs]); + + return ( +
+ +
+ ); +}; +VerticalRadioGroup.args = { + options: options, + layout: "vertical", + onChange: () => console.log("clicked"), +}; + +export const HorizontalRadioGroup: Story = (args: Props) => { + const [_, updateArgs] = useArgs(); + + const handleChange = useCallback((value: string) => updateArgs({ value }), [updateArgs]); + + return ( +
+ +
+ ); +}; +HorizontalRadioGroup.args = { + options: options, + layout: "horizontal", + onChange: () => console.log("clicked"), +}; diff --git a/web/src/beta/components/RadioGroup/index.tsx b/web/src/beta/components/RadioGroup/index.tsx new file mode 100644 index 0000000000..d673bdce85 --- /dev/null +++ b/web/src/beta/components/RadioGroup/index.tsx @@ -0,0 +1,53 @@ +import { memo, useCallback, useState } from "react"; + +import RadioBox from "@reearth/beta/components/RadioGroup/RadioBox"; +import { styled } from "@reearth/services/theme"; + +export type Option = { + key: string; + value: string; + selected: boolean; +}; + +export type RadioGroupProps = { + options: Option[]; + layout?: "vertical" | "horizontal"; + onChange?: (value: string) => void; +}; + +const RadioGroup: React.FC = ({ options, layout, onChange }) => { + const [currentOptions, updateOptions] = useState(options); + const [key, setKey] = useState(0); + + const handleRadioChange = useCallback( + (value: string) => { + updateOptions( + currentOptions.map(option => ({ + ...option, + selected: !option.selected && option.value === value, + })), + ); + setKey(prevKey => prevKey + 1); + onChange?.(value); + }, + [currentOptions, onChange], + ); + return ( + + {currentOptions.map(option => ( + handleRadioChange(option.value)} + /> + ))} + + ); +}; +export default memo(RadioGroup); + +const RadioGroupContainer = styled.div<{ layout?: "vertical" | "horizontal" }>` + display: flex; + flex-direction: ${({ layout }) => (layout === "vertical" ? "column" : "row")}; +`;