-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(web): add spacing input field and number input (#636)
Co-authored-by: nina992 <[email protected]>
- Loading branch information
Showing
9 changed files
with
377 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
web/src/beta/components/fields/SpacingInput/index.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { Meta, Story } from "@storybook/react"; | ||
|
||
import SpacingInput from "./index"; | ||
|
||
export default { | ||
component: SpacingInput, | ||
} as Meta; | ||
|
||
const Template: Story = args => <SpacingInput {...args} />; | ||
|
||
export const Default = Template.bind({}); | ||
Default.args = { | ||
name: "Padding", | ||
description: "Adjust the padding values", | ||
}; | ||
|
||
export const WithValues = Template.bind({}); | ||
WithValues.args = { | ||
name: "Padding", | ||
description: "Adjust the padding values", | ||
value: { | ||
top: "10", | ||
left: "20", | ||
right: "30", | ||
bottom: "40", | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import React, { useMemo, useState } from "react"; | ||
|
||
import { styled } from "@reearth/services/theme"; | ||
|
||
import Property from ".."; | ||
import NumberInput from "../common/NumberInput"; | ||
|
||
export type SpacingValues = { | ||
top: number; | ||
left: number; | ||
right: number; | ||
bottom: number; | ||
}; | ||
|
||
type Props = { | ||
name?: string; | ||
description?: string; | ||
value?: SpacingValues; | ||
min?: number; | ||
max?: number; | ||
onChange?: (values: SpacingValues) => void; | ||
}; | ||
type Position = keyof SpacingValues; | ||
|
||
const SpacingInput: React.FC<Props> = ({ name, description, value, min, max, onChange }) => { | ||
const [spacingValues, setSpacingValues] = useState<SpacingValues>( | ||
value || { top: 0, left: 0, right: 0, bottom: 0 }, | ||
); | ||
|
||
const memoizedSpacingValues = useMemo(() => { | ||
return ["top", "left", "right", "bottom"].map(position => { | ||
return getSpacingPosition(spacingValues, position as Position); | ||
}); | ||
}, [spacingValues]); | ||
|
||
const handleInputChange = (position: Position, newValue?: number) => { | ||
const updatedValues = { ...spacingValues, [position]: newValue }; | ||
setSpacingValues(updatedValues); | ||
onChange?.(updatedValues); | ||
}; | ||
|
||
return ( | ||
<Property name={name} description={description}> | ||
<StyledRectangle> | ||
{["top", "left", "right", "bottom"].map((position, index) => ( | ||
<SpacingField | ||
value={memoizedSpacingValues[index]} | ||
suffix="px" | ||
key={position} | ||
position={position} | ||
min={min} | ||
max={max} | ||
onChange={newValue => handleInputChange(position as Position, newValue)} | ||
/> | ||
))} | ||
</StyledRectangle> | ||
</Property> | ||
); | ||
}; | ||
|
||
export default SpacingInput; | ||
|
||
const StyledRectangle = styled.div` | ||
display: flex; | ||
width: 100%; | ||
height: 94px; | ||
border: 1px dashed ${({ theme }) => theme.outline.weak}; | ||
box-sizing: border-box; | ||
position: relative; | ||
`; | ||
|
||
const SpacingField = styled(NumberInput)<{ position: string }>` | ||
width: 40px; | ||
position: absolute; | ||
${({ position }) => | ||
position === "top" | ||
? "top: 0; left: 50%; transform: translateX(-50%);" | ||
: position === "left" | ||
? "left: 0; top: 50%; transform: translateY(-50%);" | ||
: position === "right" | ||
? "right: 0; top: 50%; transform: translateY(-50%);" | ||
: "bottom: 0; left: 50%; transform: translateX(-50%);"}; | ||
`; | ||
|
||
function getSpacingPosition(spacingValue: SpacingValues, position: Position): number { | ||
return spacingValue[position]; | ||
} |
70 changes: 70 additions & 0 deletions
70
web/src/beta/components/fields/common/NumberInput/index.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { Meta, StoryObj } from "@storybook/react"; | ||
|
||
import NumberInput, { Props } from "./index"; | ||
|
||
const meta: Meta<Props> = { | ||
component: NumberInput, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof NumberInput>; | ||
|
||
export const Default: Story = { | ||
args: { | ||
value: 42, | ||
inputDescription: "Value", | ||
suffix: "px", | ||
}, | ||
}; | ||
|
||
export const Disabled: Story = { | ||
args: { | ||
value: 15, | ||
inputDescription: "Disabled", | ||
suffix: "px", | ||
disabled: true, | ||
}, | ||
}; | ||
|
||
export const Range: Story = { | ||
args: { | ||
value: 50, | ||
inputDescription: "Range", | ||
suffix: "px", | ||
min: 4, | ||
max: 100, | ||
}, | ||
}; | ||
|
||
export const NoValue: Story = { | ||
args: { | ||
inputDescription: "No Value", | ||
suffix: "px", | ||
}, | ||
}; | ||
|
||
export const WithMinValue: Story = { | ||
args: { | ||
value: 5, | ||
inputDescription: "With Min Value", | ||
suffix: "px", | ||
min: 0, | ||
}, | ||
}; | ||
|
||
export const WithMaxValue: Story = { | ||
args: { | ||
value: 95, | ||
inputDescription: "With Max Value", | ||
suffix: "px", | ||
max: 100, | ||
}, | ||
}; | ||
|
||
export const Editable: Story = { | ||
args: { | ||
value: 25, | ||
inputDescription: "Editable", | ||
suffix: "px", | ||
}, | ||
}; |
176 changes: 176 additions & 0 deletions
176
web/src/beta/components/fields/common/NumberInput/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
import React, { useState, useCallback, useRef, useEffect } from "react"; | ||
|
||
import Text from "@reearth/beta/components/Text"; | ||
import { useT } from "@reearth/services/i18n"; | ||
import { useNotification } from "@reearth/services/state"; | ||
import { styled, useTheme } from "@reearth/services/theme"; | ||
import { metricsSizes } from "@reearth/services/theme/reearthTheme/common/metrics"; | ||
|
||
export type Props = { | ||
className?: string; | ||
suffix?: string; | ||
min?: number; | ||
max?: number; | ||
disabled?: boolean; | ||
inputDescription?: string; | ||
value?: number; | ||
onChange?: (value?: number | undefined) => void; | ||
}; | ||
|
||
const NumberInput: React.FC<Props> = ({ | ||
className, | ||
value, | ||
inputDescription, | ||
suffix, | ||
onChange, | ||
min, | ||
max, | ||
disabled = false, | ||
}) => { | ||
const [innerValue, setInnerValue] = useState<number | undefined>(value); | ||
const [, setNotification] = useNotification(); | ||
const inputRef = useRef<HTMLInputElement | null>(null); | ||
|
||
const isEditing = useRef(false); | ||
const t = useT(); | ||
|
||
const theme = useTheme(); | ||
|
||
useEffect(() => { | ||
setInnerValue(value); | ||
}, [value]); | ||
|
||
useEffect(() => { | ||
// Calculate and set the minimum width for the input field | ||
if (inputRef.current) { | ||
const minWidth = Math.max(metricsSizes.xs, inputRef.current.value.length * 10); | ||
inputRef.current.style.width = `${minWidth}px`; | ||
} | ||
}, []); | ||
|
||
const handleValueChange = useCallback( | ||
(newValue: number | undefined) => { | ||
if (!onChange || !isEditing.current) { | ||
return; | ||
} | ||
|
||
if (newValue === undefined) { | ||
setInnerValue(undefined); | ||
onChange(undefined); | ||
} else if (typeof max === "number" && isFinite(max) && newValue > max) { | ||
setNotification({ type: "warning", text: t("You have passed the maximum value.") }); | ||
setInnerValue(undefined); | ||
onChange(undefined); | ||
} else if (typeof min === "number" && isFinite(min) && newValue < min) { | ||
setNotification({ type: "warning", text: t("You have passed the minimum value.") }); | ||
setInnerValue(undefined); | ||
onChange(undefined); | ||
} else if (!isNaN(newValue)) { | ||
setInnerValue(newValue); | ||
onChange(newValue); | ||
} | ||
}, | ||
[onChange, max, min, setNotification, t], | ||
); | ||
|
||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { | ||
const newValue = e.currentTarget.value; | ||
setInnerValue(parseFloat(newValue)); | ||
const minWidth = Math.max(metricsSizes.xs, newValue.length * 10); | ||
e.currentTarget.style.width = `${minWidth}px`; | ||
}, []); | ||
|
||
const handleKeyPress = useCallback( | ||
(e: React.KeyboardEvent<HTMLInputElement>) => { | ||
if (e.key === "Enter") { | ||
handleValueChange(parseFloat(e.currentTarget.value)); | ||
} | ||
}, | ||
[handleValueChange], | ||
); | ||
|
||
const handleFocus = useCallback(() => { | ||
isEditing.current = true; | ||
}, []); | ||
|
||
const handleBlur = useCallback( | ||
(e: React.SyntheticEvent<HTMLInputElement>) => { | ||
const newValue = parseFloat(e.currentTarget.value); | ||
handleValueChange(newValue); | ||
isEditing.current = false; | ||
}, | ||
[handleValueChange], | ||
); | ||
|
||
return ( | ||
<Wrapper> | ||
<InputWrapper inactive={!!disabled} className={className}> | ||
<StyledInput | ||
type="number" | ||
value={innerValue} | ||
disabled={disabled} | ||
onChange={handleChange} | ||
onKeyPress={handleKeyPress} | ||
onFocus={handleFocus} | ||
onBlur={handleBlur} | ||
min={min} | ||
ref={inputRef} | ||
max={max} | ||
step={"any"} | ||
/> | ||
{suffix && ( | ||
<Text size="footnote" color={theme.content.weak} otherProperties={{ userSelect: "none" }}> | ||
{suffix} | ||
</Text> | ||
)} | ||
</InputWrapper> | ||
{inputDescription && ( | ||
<Text size="footnote" color={theme.content.weak}> | ||
{inputDescription} | ||
</Text> | ||
)} | ||
</Wrapper> | ||
); | ||
}; | ||
|
||
const Wrapper = styled.div` | ||
width: 100%; | ||
text-align: center; | ||
`; | ||
|
||
const InputWrapper = styled.div<{ inactive: boolean }>` | ||
display: flex; | ||
flex-direction: row; | ||
align-items: center; | ||
background: ${({ theme }) => theme.bg[1]}; | ||
border: 1px solid ${({ theme }) => theme.outline.weak}; | ||
border-radius: 4px; | ||
padding: ${metricsSizes.xs}px ${metricsSizes.s}px; | ||
gap: 12px; | ||
width: auto; | ||
min-width: min-content; | ||
max-width: 64px; | ||
color: ${({ inactive, theme }) => (inactive ? theme.content.weak : theme.content.main)}; | ||
&:focus-within { | ||
border-color: ${({ theme }) => theme.select.main}; | ||
} | ||
box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.25) inset; | ||
`; | ||
|
||
const StyledInput = styled.input` | ||
display: block; | ||
border: none; | ||
background: ${({ theme }) => theme.bg[1]}; | ||
outline: none; | ||
color: inherit; | ||
width: 100%; | ||
&::-webkit-inner-spin-button, | ||
&::-webkit-outer-spin-button { | ||
-webkit-appearance: none; | ||
} | ||
&[type="number"] { | ||
-moz-appearance: textfield; | ||
} | ||
`; | ||
|
||
export default NumberInput; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.