Skip to content

Commit

Permalink
feat(web): add spacing input field and number input (#636)
Browse files Browse the repository at this point in the history
Co-authored-by: nina992 <[email protected]>
  • Loading branch information
nina992 and nina992 authored Sep 1, 2023
1 parent b13b187 commit f921133
Show file tree
Hide file tree
Showing 9 changed files with 377 additions and 2 deletions.
11 changes: 11 additions & 0 deletions web/src/beta/components/fields/PropertyFields/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import TextInput from "@reearth/beta/components/fields/TextInput";
import { type Item } from "@reearth/services/api/propertyApi/utils";

import ColorField from "../ColorField";
import SpacingInput, { SpacingValues } from "../SpacingInput";
import ToggleField from "../ToggleField";

import useHooks from "./hooks";
Expand Down Expand Up @@ -42,6 +43,16 @@ const PropertyFields: React.FC<Props> = ({ propertyId, item }) => {
onChange={handlePropertyValueUpdate(item.schemaGroup, propertyId, sf.id, sf.type)}
/>
)
) : sf.type === "spacing" ? (
<SpacingInput
key={sf.id}
name={sf.name}
value={(value as SpacingValues) ?? ""}
description={sf.description}
min={sf.min}
max={sf.max}
onChange={handlePropertyValueUpdate(item.schemaGroup, propertyId, sf.id, sf.type)}
/>
) : sf.type == "bool" ? (
<ToggleField
key={sf.id}
Expand Down
27 changes: 27 additions & 0 deletions web/src/beta/components/fields/SpacingInput/index.stories.tsx
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",
},
};
87 changes: 87 additions & 0 deletions web/src/beta/components/fields/SpacingInput/index.tsx
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];
}
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 web/src/beta/components/fields/common/NumberInput/index.tsx
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;
2 changes: 1 addition & 1 deletion web/src/beta/components/fields/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default Property;
const Wrapper = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
gap: 8px;
`;

const Description = styled(Text)`
Expand Down
Loading

0 comments on commit f921133

Please sign in to comment.