diff --git a/.changeset/chilly-lobsters-talk.md b/.changeset/chilly-lobsters-talk.md new file mode 100644 index 000000000..c0677e145 --- /dev/null +++ b/.changeset/chilly-lobsters-talk.md @@ -0,0 +1,8 @@ +--- +"@wizleap-inc/wiz-ui-react": minor +"@wizleap-inc/wiz-ui-next": minor +"@wizleap-inc/wiz-ui-constants": minor +"@wizleap-inc/wiz-ui-styles": minor +--- + +Feat: WizNumberInput diff --git a/packages/constants/component/name.ts b/packages/constants/component/name.ts index e32fb8505..c583f9b8f 100644 --- a/packages/constants/component/name.ts +++ b/packages/constants/component/name.ts @@ -68,6 +68,7 @@ export const ComponentName = { ChatCard: "WizChatCard", ChatForm: "WizChatForm", ChatItem: "WizChatItem", + NumberInput: "WizNumberInput", Notification: "WizNotification", NotificationList: "WizNotificationList", NotificationPanel: "WizNotificationPanel", diff --git a/packages/styles/bases/number-input.css.ts b/packages/styles/bases/number-input.css.ts new file mode 100644 index 000000000..4f0d9b9ee --- /dev/null +++ b/packages/styles/bases/number-input.css.ts @@ -0,0 +1,87 @@ +import { style } from "@vanilla-extract/css"; +import { THEME } from "@wizleap-inc/wiz-ui-constants"; + +import { fontSizeStyle } from "../commons"; + +export const container = style({ + position: "relative", + background: THEME.color.white["800"], + borderRadius: THEME.spacing.xs2, + boxSizing: "border-box", + overflow: "hidden", +}); + +export const disabled = style({ + backgroundColor: THEME.color.gray[300], + cursor: "not-allowed", +}); + +export const input = style({ + "::-webkit-outer-spin-button": { + appearance: "none", + margin: 0, + }, + "::-webkit-inner-spin-button": { + appearance: "none", + margin: 0, + }, + MozAppearance: "textfield", + textAlign: "right", + minWidth: "30%", + border: "none", + outline: "none", + padding: `${THEME.spacing.xs2} ${THEME.spacing.xs}`, + lineHeight: THEME.fontSize.xl, + flexGrow: 1, + fontSize: THEME.fontSize.sm, + color: THEME.color.gray["800"], + "::placeholder": { + color: THEME.color.gray["500"], + userSelect: "none", + }, + ":disabled": { + cursor: "not-allowed", + backgroundColor: THEME.color.gray["300"], + color: THEME.color.gray["500"], + }, +}); + +export const button = style({ + lineHeight: 0.2, + position: "relative", + cursor: "pointer", + padding: THEME.spacing.xs2, + borderRadius: THEME.spacing.no, + boxSizing: "border-box", + borderColor: THEME.color.gray["400"], + border: "none", + borderLeft: `1px solid ${THEME.color.gray["400"]}`, + background: "transparent", + fill: THEME.color.gray["500"], + selectors: { + "&:hover:not(:disabled)": { + "@media": { + "(any-hover: hover)": { + backgroundColor: THEME.color.green["300"], + fill: THEME.color.green["800"], + }, + }, + }, + "&:active:not(:disabled)": { + backgroundColor: THEME.color.green["800"], + fill: THEME.color.white["800"], + }, + }, + ":disabled": { + cursor: "not-allowed", + }, +}); + +export const arrowIcon = style([ + fontSizeStyle.xs2, + { + transform: "scale(2)", + pointerEvents: "none", + color: "currentcolor", + }, +]); diff --git a/packages/wiz-ui-next/src/components/base/inputs/number-input/index.ts b/packages/wiz-ui-next/src/components/base/inputs/number-input/index.ts new file mode 100644 index 000000000..dd5b23851 --- /dev/null +++ b/packages/wiz-ui-next/src/components/base/inputs/number-input/index.ts @@ -0,0 +1 @@ +export { default as WizNumberInput } from "./number-input.vue"; diff --git a/packages/wiz-ui-next/src/components/base/inputs/number-input/number-input.stories.ts b/packages/wiz-ui-next/src/components/base/inputs/number-input/number-input.stories.ts new file mode 100644 index 000000000..c1c2d3cbd --- /dev/null +++ b/packages/wiz-ui-next/src/components/base/inputs/number-input/number-input.stories.ts @@ -0,0 +1,41 @@ +import { Meta, StoryFn } from "@storybook/vue3"; + +import { WizHStack } from "@/components"; + +import { WizNumberInput } from "."; + +export default { + title: "Base/Input/NumberInput", + component: WizNumberInput, +} as Meta; + +const Template: StoryFn = (args) => ({ + components: { WizNumberInput, WizHStack }, + setup() { + return { args }; + }, + template: ` +
+ +
value : {{ args.modelValue }}
+
+ `, +}); +export const Default = Template.bind({}); +Default.args = {}; + +export const Placeholder = Template.bind({}); +Placeholder.args = { + placeholder: "数字を入力", +}; + +export const WithValue = Template.bind({}); +WithValue.args = { + modelValue: 100, +}; + +export const Disabled = Template.bind({}); +Disabled.args = { + modelValue: 100, + disabled: true, +}; diff --git a/packages/wiz-ui-next/src/components/base/inputs/number-input/number-input.vue b/packages/wiz-ui-next/src/components/base/inputs/number-input/number-input.vue new file mode 100644 index 000000000..e79d2d258 --- /dev/null +++ b/packages/wiz-ui-next/src/components/base/inputs/number-input/number-input.vue @@ -0,0 +1,129 @@ + + + diff --git a/packages/wiz-ui-next/src/components/custom/form/group.stories.ts b/packages/wiz-ui-next/src/components/custom/form/group.stories.ts index 0d740d29f..3dea3a676 100644 --- a/packages/wiz-ui-next/src/components/custom/form/group.stories.ts +++ b/packages/wiz-ui-next/src/components/custom/form/group.stories.ts @@ -24,6 +24,7 @@ import { DateRange, DateRangePickerSelectBoxOption, } from "@/components/base/inputs/date-range-picker/types"; +import { WizNumberInput } from "@/components/base/inputs/number-input"; import { RadioOption } from "@/components/base/inputs/radio/types"; import { SelectBoxOption } from "@/components/base/inputs/selectbox/types"; @@ -211,6 +212,7 @@ export const AllInput: StoryFn = () => ({ WizDatePicker, WizTimePicker, WizDateRangePicker, + WizNumberInput, }, setup() { const textInput = ref(""); @@ -256,6 +258,7 @@ export const AllInput: StoryFn = () => ({ { label: "選択肢3", value: "3" }, ]); const selectBoxInput = ref(""); + const numberInput = ref(); return { textInput, passwordInput, @@ -271,6 +274,7 @@ export const AllInput: StoryFn = () => ({ dateRangeInput, selectBoxOptions, selectBoxInput, + numberInput, }; }, template: ` @@ -302,6 +306,9 @@ export const AllInput: StoryFn = () => ({ + + + `, }); @@ -319,6 +326,7 @@ export const AllInputError: StoryFn = () => ({ WizDatePicker, WizTimePicker, WizDateRangePicker, + WizNumberInput, }, setup() { const textInput = ref(""); @@ -364,6 +372,7 @@ export const AllInputError: StoryFn = () => ({ { label: "選択肢3", value: "3" }, ]); const selectBoxInput = ref(""); + const numberInput = ref(); return { textInput, passwordInput, @@ -379,6 +388,7 @@ export const AllInputError: StoryFn = () => ({ dateRangeInput, selectBoxOptions, selectBoxInput, + numberInput, }; }, template: ` @@ -410,6 +420,9 @@ export const AllInputError: StoryFn = () => ({ + + + `, }); diff --git a/packages/wiz-ui-react/src/components/base/inputs/number-input/components/index.ts b/packages/wiz-ui-react/src/components/base/inputs/number-input/components/index.ts new file mode 100644 index 000000000..5f0382f95 --- /dev/null +++ b/packages/wiz-ui-react/src/components/base/inputs/number-input/components/index.ts @@ -0,0 +1 @@ +export { WizNumberInput } from "./number-input"; diff --git a/packages/wiz-ui-react/src/components/base/inputs/number-input/components/number-input.tsx b/packages/wiz-ui-react/src/components/base/inputs/number-input/components/number-input.tsx new file mode 100644 index 000000000..e25bc75a3 --- /dev/null +++ b/packages/wiz-ui-react/src/components/base/inputs/number-input/components/number-input.tsx @@ -0,0 +1,122 @@ +import { ComponentName } from "@wizleap-inc/wiz-ui-constants"; +import * as styles from "@wizleap-inc/wiz-ui-styles/bases/number-input.css"; +import { inputBorderStyle } from "@wizleap-inc/wiz-ui-styles/commons"; +import clsx from "clsx"; +import { useContext, useRef, useState } from "react"; + +import { + WizDivider, + WizIArrowDropDown, + WizIArrowDropUp, + WizVStack, +} from "@/components"; +import { FormControlContext } from "@/components/custom/form/components/form-control-context"; +import { BaseProps } from "@/types"; + +type Props = BaseProps & { + value?: number; + onChange?: (e: number | undefined) => void; + placeholder?: string; + disabled?: boolean; + width?: string; + error?: boolean; + min?: number; + max?: number; + step?: number; + precision?: number; +}; + +const NumberInput = (props: Props) => { + const { + className, + style, + error, + value, + width = "7rem", + onChange, + disabled, + ...rest + } = props; + const inputRef = useRef(null); + const formControl = useContext(FormControlContext); + + const isError = error || formControl.error; + + const triggerChangeEvent = () => { + const event = new Event("input", { bubbles: true }); + if (inputRef.current) { + inputRef.current.dispatchEvent(event); + } + }; + const handleStepUp = () => { + inputRef.current?.stepUp(); + triggerChangeEvent(); + }; + const handleStepDown = () => { + inputRef.current?.stepDown(); + triggerChangeEvent(); + }; + + const [isFocused, setIsFocused] = useState(false); + const borderStyle = (() => { + if (isError) return "error"; + if (isFocused) return "active"; + return "default"; + })(); + return ( +
setIsFocused(true)} + onBlur={() => setIsFocused(false)} + > + { + onChange?.( + isNaN(e.currentTarget.valueAsNumber) + ? undefined + : e.currentTarget.valueAsNumber + ); + }} + disabled={disabled} + {...rest} + /> + + + + + +
+ ); +}; + +NumberInput.displayName = ComponentName.NumberInput; + +export const WizNumberInput = NumberInput; diff --git a/packages/wiz-ui-react/src/components/base/inputs/number-input/index.ts b/packages/wiz-ui-react/src/components/base/inputs/number-input/index.ts new file mode 100644 index 000000000..40b494c5f --- /dev/null +++ b/packages/wiz-ui-react/src/components/base/inputs/number-input/index.ts @@ -0,0 +1 @@ +export * from "./components"; diff --git a/packages/wiz-ui-react/src/components/base/inputs/number-input/stories/number-input.stories.tsx b/packages/wiz-ui-react/src/components/base/inputs/number-input/stories/number-input.stories.tsx new file mode 100644 index 000000000..31682859d --- /dev/null +++ b/packages/wiz-ui-react/src/components/base/inputs/number-input/stories/number-input.stories.tsx @@ -0,0 +1,51 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +import { Meta, StoryObj } from "@storybook/react"; +import { useState } from "react"; + +import { WizNumberInput } from ".."; + +const meta: Meta = { + title: "Base/Input/NumberInput", + component: WizNumberInput, +}; + +export default meta; +type Story = StoryObj; + +const Template: Story = { + render: (args) => { + const [value, setValue] = useState(args.value); + return ( + <> + +
value : {value}
+ + ); + }, +}; + +export const Default: Story = { + ...Template, + args: {}, +}; + +export const Placeholder: Story = { + ...Template, + args: { + placeholder: "数字を入力", + }, +}; + +export const WithValue: Story = { + ...Template, + args: { + value: 100, + }, +}; +export const Disabled: Story = { + ...Template, + args: { + value: 100, + disabled: true, + }, +}; diff --git a/packages/wiz-ui-react/src/components/custom/form/stories/all-input.tsx b/packages/wiz-ui-react/src/components/custom/form/stories/all-input.tsx index 20952c2ca..5e1cafbc5 100644 --- a/packages/wiz-ui-react/src/components/custom/form/stories/all-input.tsx +++ b/packages/wiz-ui-react/src/components/custom/form/stories/all-input.tsx @@ -7,6 +7,7 @@ import { WizTextInput, WizTimePicker, } from "@/components"; +import { WizNumberInput } from "@/components/base/inputs/number-input"; import { WizFormControl, WizFormGroup } from ".."; @@ -24,6 +25,7 @@ const TemplateComponent = ({ const [selectBoxValue, setSelectBoxValue] = useState(null); const [checkBoxValues, setCheckBoxValues] = useState([]); const [radioValue, setRadioValue] = useState(null); + const [numberValue, setNumberValue] = useState(undefined); const [timePickerValue, setTimePickerValue] = useState["time"]>(null); return ( @@ -91,6 +93,13 @@ const TemplateComponent = ({ {/* TODO: WizTextarea 追加待ち */} {/* TODO: WizDateRangePicker 追加待ち */} + + + ); };