Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { PropsWithChildren } from 'react';

import { Text } from '../Text';
import { Text } from '../../Text';
import { helperTextStyle } from './helperText.css';

interface HelperTextProps extends PropsWithChildren {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { InputHTMLAttributes } from 'react';
import { useRef } from 'react';

import { useSafeContext } from '../../hooks/useSafeContext';
import { vars } from '../../theme/index.css';
import { ChevronDown } from '../Icon';
import { InputContext } from './InputContext';
import { ChevronDown } from '@/components/Icon';
import { useSafeContext } from '@/hooks/useSafeContext';
import { vars } from '@/theme/index.css';
import clsx from '@/utils/clsx';

import { interactableBorderStyle } from '../index.css';
import { InputContext } from '../InputContext';
import { inputFieldContainerStyle, inputFieldStyle, selectIconStyle } from './inputField.css';

interface InputFieldProps extends Omit<
Expand All @@ -15,7 +18,7 @@ interface InputFieldProps extends Omit<
}

const InputField = ({ placeholder, onClick, ...inputProps }: InputFieldProps) => {
const { isValid, type } = useSafeContext(InputContext);
const { isValid, type, borderPlacement } = useSafeContext(InputContext);
const inputRef = useRef<HTMLInputElement>(null);

const handleContainerClick = () => {
Expand All @@ -24,7 +27,13 @@ const InputField = ({ placeholder, onClick, ...inputProps }: InputFieldProps) =>
};

return (
<div className={inputFieldContainerStyle({ isValid, type })} onClick={handleContainerClick}>
<div
className={clsx(
inputFieldContainerStyle({ type }),
borderPlacement === 'inputField' && interactableBorderStyle({ isValid }),
)}
onClick={handleContainerClick}
>
<input
className={inputFieldStyle}
placeholder={placeholder}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { PropsWithChildren } from 'react';

import { Text } from '../Text';
import { Text } from '../../Text';
import { labelContainerStyle, requiredMarkerStyle } from './label.css';

interface LabelProps extends PropsWithChildren {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { recipe } from '@vanilla-extract/recipes';

import { vars } from '../../theme/index.css';
import { vars } from '@/theme/index.css';

export const helperTextStyle = recipe({
base: {},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,28 @@
import { style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';

import { font } from '../../theme/font';
import { vars } from '../../theme/index.css';
import { font } from '@/theme/font';
import { vars } from '@/theme/index.css';

export const inputFieldContainerStyle = recipe({
base: {
flex: 1,
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: vars.color.Ref.Netural.White,
padding: `0 ${vars.spacing[400]}`,
borderRadius: vars.radius[200],
border: `1px solid ${vars.color.Ref.Netural[300]}`,
borderWidth: 1.5,
backgroundColor: vars.color.Ref.Netural.White,
padding: vars.spacing[400],
height: 40,
boxSizing: 'border-box',
},
variants: {
isValid: {
true: {
':hover': {
borderColor: vars.color.Ref.Primary[200],
},
':focus-within': {
borderColor: vars.color.Ref.Primary[500],
},
},
false: {
borderColor: vars.color.Ref.Red[500],
},
},
type: {
text: { cursor: 'text' },
select: { cursor: 'pointer' },
},
},
defaultVariants: {
isValid: true,
},
});

export const selectIconStyle = style({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import { style } from '@vanilla-extract/css';

import { vars } from '../../theme/index.css';
import { vars } from '@/theme/index.css';

export const labelContainerStyle = style({
display: 'inline-flex',
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/components/Input/Input.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const Default: StoryObj<typeof Input.Single> = {
placeholder: '이메일을 입력하세요',
},
render: (args) => (
<Input.Single {...args}/>
<Input.Single {...args} />
),
};

Expand All @@ -64,6 +64,7 @@ export const MultiInput = () => {

return (
<Input.Multi
borderPlacement='inputField'
isValid={true}
label='시간'
required={true}
Expand Down Expand Up @@ -94,10 +95,11 @@ export const CustomSeparatorIcon = () => {

return (
<Input.Multi
borderPlacement='container'
isValid={true}
label='시간'
required={true}
separator={<Check fill={vars.color.Ref.Netural[600]}/>}
separator={<Check fill={vars.color.Ref.Netural[600]} />}
type='text'
>
<Input.Multi.InputField
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/Input/InputContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { CommonInputProps } from '.';
interface InputContextProps {
isValid: boolean;
type: CommonInputProps['type'];
borderPlacement?: 'container' | 'inputField';
}

/**
Expand Down
46 changes: 23 additions & 23 deletions frontend/src/components/Input/MultiInput.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import type { JSX, PropsWithChildren, ReactNode } from 'react';
import { isValidElement } from 'react';

import clsx from '@/utils/clsx';
import { intersperseElement } from '@/utils/jsxUtils';

import { Text } from '../Text';
import { type CommonInputProps, ICON_WIDTH } from '.';
import HelperText from './HelperText';
import { containerStyle, inputFieldsContainerStyle, separatorStyle } from './index.css';
import HelperText from './Core/HelperText';
import InputField from './Core/InputField';
import Label from './Core/Label';
import {
containerStyle,
inputFieldsContainerStyle,
interactableBorderStyle,
separatorStyle,
} from './index.css';
import { InputContext } from './InputContext';
import InputField from './InputField';
import Label from './Label';

export interface MultiInputProps extends CommonInputProps, PropsWithChildren {
separator?: string | JSX.Element;
borderPlacement?: 'container' | 'inputField';
}

export const MultiInput = ({
Expand All @@ -21,27 +30,31 @@ export const MultiInput = ({
separator = '',
hint,
error,
borderPlacement = 'inputField',
children,
}: MultiInputProps) => {
const childElements = children ?
Array.from(children as ReactNode[]).filter(isValidElement)
: [];
const separatorElement = prepareSeparatorLayout(separator);
const childrenWithSeparators =
childElements.length > 1 ? intersperse(childElements, separatorElement) : childElements;
childElements.length > 1 ? intersperseElement(childElements, separatorElement) : childElements;

return (
<InputContext.Provider value={{ isValid, type }}>
<InputContext.Provider value={{ isValid, type, borderPlacement }}>
<div className={containerStyle}>
<Label required={required}>{label}</Label>
<div className={inputFieldsContainerStyle}>
<div className={clsx(
inputFieldsContainerStyle,
borderPlacement === 'container' && interactableBorderStyle({ isValid }),
)}
>
{childrenWithSeparators}
</div>
{isValid ?
<HelperText type='hint'>{hint}</HelperText>
:
<HelperText type='error'>{error}</HelperText>
}
<HelperText type='error'>{error}</HelperText>}
</div>
</InputContext.Provider>
);
Expand All @@ -60,22 +73,9 @@ const prepareSeparatorLayout = (separator: string | (JSX.Element & { props: Sepa
</div>
);
}
return <separator.type {...separator.props} width={ICON_WIDTH}/>;
return <separator.type {...separator.props} width={ICON_WIDTH} />;
};

const intersperse = (
childElements: ReactNode[],
separator?: JSX.Element,
): ReactNode[] => {
const result: ReactNode[] = [];
childElements.forEach((child, index) => {
result.push(child);
if (separator && index < childElements.length - 1) {
result.push(<separator.type {...separator.props} key={index} />);
}
});
return result;
};
MultiInput.InputField = InputField;

export default MultiInput;
10 changes: 5 additions & 5 deletions frontend/src/components/Input/SingleInput.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { InputHTMLAttributes } from 'react';

import { type CommonInputProps } from '.';
import HelperText from './HelperText';
import { containerStyle, inputFieldsContainerStyle } from './index.css';
import HelperText from './Core/HelperText';
import InputField from './Core/InputField';
import Label from './Core/Label';
import { containerStyle, inputFieldsContainerStyle, interactableBorderStyle } from './index.css';
import { InputContext } from './InputContext';
import InputField from './InputField';
import Label from './Label';

export interface SingleInputProps extends CommonInputProps {
inputProps?: Omit<InputHTMLAttributes<HTMLInputElement>, 'placeholder' | 'onClick' | 'readOnly'>;
Expand All @@ -25,7 +25,7 @@ export const SingleInput = ({
<InputContext.Provider value={{ isValid, type }}>
<div className={containerStyle}>
<Label required={required}>{label}</Label>
<div className={inputFieldsContainerStyle}>
<div className={`${inputFieldsContainerStyle} ${interactableBorderStyle({ isValid })}`}>
<InputField
{...inputProps}
onClick={onClick}
Expand Down
28 changes: 27 additions & 1 deletion frontend/src/components/Input/index.css.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';

import { vars } from '../../theme/index.css';

Expand All @@ -11,7 +12,10 @@ export const containerStyle = style({
export const inputFieldsContainerStyle = style({
display: 'flex',
alignItems: 'center',
gap: vars.spacing[300],
height: 40,
gap: vars.spacing[200],
borderRadius: vars.radius[200],
boxSizing: 'border-box',
});

export const separatorStyle = style({
Expand All @@ -20,4 +24,26 @@ export const separatorStyle = style({
justifyContent: 'center',
width: 20,
color: vars.color.Ref.Netural[500],
});

export const interactableBorderStyle = recipe({
base: {
border: `1px solid ${vars.color.Ref.Netural[300]}`,
borderWidth: 1.5,
},
variants: {
isValid: {
true: {
':hover': {
borderColor: vars.color.Ref.Primary[200],
},
':focus-within': {
borderColor: vars.color.Ref.Primary[500],
},
},
false: {
borderColor: vars.color.Ref.Red[500],
},
},
},
});
16 changes: 16 additions & 0 deletions frontend/src/utils/jsxUtils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { ReactNode } from '@tanstack/react-router';
import type { JSX } from 'react';
Comment on lines +1 to +2
Copy link

@coderabbitai coderabbitai bot Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incorrect import source for ReactNode

The ReactNode type should be imported from 'react', not '@tanstack/react-router'. Importing from the incorrect source may lead to unexpected type issues.

Apply this fix:

-import type { ReactNode } from '@tanstack/react-router';
+import type { ReactNode } from 'react';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { ReactNode } from '@tanstack/react-router';
import type { JSX } from 'react';
import type { ReactNode } from 'react';
import type { JSX } from 'react';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요 리뷰 한번 확인해주세요!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@hamo-o 네, 제가 드린 리뷰가 정확합니다. ReactNode 타입은 React의 핵심 타입으로, 'react' 패키지에서 직접 import하는 것이 best practice입니다. 제안드린 수정사항을 적용해주시면 감사하겠습니다.

(_/)
(•ω• )
ヽ(  、ヽ)


🌐 Web query:

What is the correct import source for ReactNode type in TypeScript? Is it available in @tanstack/react-router?

💡 Result:



In TypeScript, the `ReactNode` type should be imported directly from the `react` package, not from `@tanstack/react-router`. According to the search results:

1. **ReactNode Definition**:
   - `ReactNode` represents any renderable React content (elements, strings, numbers, etc.) and is part of React's core type system[1][7][10].
   - Example import:
     ```tsx
     import { ReactNode } from 'react';
     ```

2. **@tanstack/react-router Context**:
   - TanStack Router focuses on routing-specific types like `Route`, `LinkProps`, and router configuration utilities[2][5][8].
   - It does **not** export `ReactNode` - this type remains exclusively part of React's type definitions[1][4][7].

For TypeScript projects using both libraries:
```tsx
// Correct usage:
import { ReactNode } from 'react'; 
import { Link } from '@tanstack/react-router';

function Component({ children }: { children: ReactNode }) {
  return <Link to="/">{children}</Link>;
}
```

The error "Type Element is Not Assignable to Type Reactnode" typically occurs when mixing incompatible type declarations[4], which can be resolved by ensuring proper imports from `react` rather than routing libraries[1][7].

Citations:

- 1: https://dev.to/elhamnajeebullah/react-typescript-what-is-reactnode-and-when-to-use-it-3660
- 2: https://tanstack.com/router/v1/docs/framework/react/guide/type-safety
- 3: https://tanstack.com/start/latest/docs/framework/react/learn-the-basics
- 4: https://www.dhiwise.com/post/resolve-type-element-is-not-assignable-to-type-reactnode
- 5: https://www.npmjs.com/package/@tanstack/react-router
- 6: https://dev.to/franklin030601/managing-state-with-react-query-1842
- 7: https://www.totaltypescript.com/jsx-element-vs-react-reactnode
- 8: https://tanstack.com/router/latest/docs/framework/react/overview
- 9: https://github.com/TanStack/router/issues/1271
- 10: https://react.dev/learn/typescript

@hamo-o 네, 제가 드린 리뷰가 정확합니다. 좀 더 자세히 설명드리면:

  1. ReactNode는 React의 핵심 타입으로, 'react' 패키지에서 직접 import 해야 합니다.
  2. @tanstack/react-router는 라우팅 관련 타입들만 제공하며, ReactNode 타입은 제공하지 않습니다.
  3. 잘못된 패키지에서 타입을 import하면 타입 호환성 문제가 발생할 수 있습니다.

제안드린 수정사항을 적용해주시면 감사하겠습니다.

(_/)
(•ω• )
ヽ(  、ヽ)


✏️ Learnings added
Learnt from: hamo-o
PR: softeer5th/Team4-enDolphin#100
File: frontend/src/utils/jsxUtils.tsx:1-2
Timestamp: 2025-02-06T02:50:41.482Z
Learning: ReactNode type should be imported directly from 'react' package, not from third-party packages that might re-export it, to follow best practices and reduce potential version conflicts.

Learnt from: hamo-o
PR: softeer5th/Team4-enDolphin#100
File: frontend/src/utils/jsxUtils.tsx:1-2
Timestamp: 2025-02-06T02:50:41.482Z
Learning: @tanstack/react-router package focuses on routing-specific types and does not export ReactNode type. Always import ReactNode from 'react' package to avoid type compatibility issues.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


export const intersperseElement = (
childElements: ReactNode[],
separator?: JSX.Element,
): ReactNode[] => {
const result: ReactNode[] = [];
childElements.forEach((child, index) => {
result.push(child);
if (separator && index < childElements.length - 1) {
result.push(<separator.type {...separator.props} key={index} />);
}
});
return result;
};