@uniai-fe/uds-primitives 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/styles.css +56 -0
  2. package/package.json +1 -1
  3. package/src/components/input/hooks/index.ts +3 -0
  4. package/src/components/input/hooks/useInputFile.ts +254 -0
  5. package/src/components/input/hooks/useInputFileContext.ts +244 -0
  6. package/src/components/input/hooks/useInputFileRHF.ts +105 -0
  7. package/src/components/input/index.scss +1 -0
  8. package/src/components/input/markup/file/DragAndDrop.tsx +59 -0
  9. package/src/components/input/markup/file/UploadButton.tsx +44 -0
  10. package/src/components/input/markup/file/UploadedChip.tsx +66 -0
  11. package/src/components/input/markup/file/index.tsx +25 -0
  12. package/src/components/input/markup/file/list/Body.tsx +55 -0
  13. package/src/components/input/markup/file/list/Container.tsx +97 -0
  14. package/src/components/input/markup/file/list/Header.tsx +86 -0
  15. package/src/components/input/markup/file/list/Item.tsx +35 -0
  16. package/src/components/input/markup/file/list/Provider.tsx +35 -0
  17. package/src/components/input/markup/file/list/Remove.tsx +54 -0
  18. package/src/components/input/markup/file/list/index.tsx +31 -0
  19. package/src/components/input/markup/foundation/SideSlot.tsx +0 -2
  20. package/src/components/input/markup/foundation/Utility.tsx +2 -0
  21. package/src/components/input/markup/foundation/index.tsx +0 -2
  22. package/src/components/input/markup/index.tsx +2 -0
  23. package/src/components/input/markup/text/Password.tsx +2 -0
  24. package/src/components/input/markup/text/Search.tsx +0 -2
  25. package/src/components/input/styles/file.scss +60 -0
  26. package/src/components/input/types/file.ts +531 -0
  27. package/src/components/input/types/index.ts +1 -0
  28. package/src/components/input/utils/file.ts +103 -0
  29. package/src/components/input/utils/index.tsx +1 -0
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import type { InputFileDragAndDropProps } from "../../types";
5
+ import { useInputFile } from "../../hooks/useInputFile";
6
+
7
+ /**
8
+ * Input Markup; File DragAndDrop 컴포넌트
9
+ * @component
10
+ * @param {InputFileDragAndDropProps} props drag-and-drop props
11
+ * @param {React.ReactNode} props.children drop zone 내부 콘텐츠
12
+ * @param {InputFileChangeHandler} props.onFilesChange 파일 변경 콜백
13
+ * @param {boolean} [props.disabled] drop 비활성화 여부
14
+ * @param {string} [props.className] root className
15
+ * @example
16
+ * ```tsx
17
+ * // DragAndDrop 단독 도입 예시
18
+ * <Input.File.DragAndDrop onFilesChange={files => console.log(files)}>
19
+ * <span>파일을 드래그해 업로드하세요</span>
20
+ * </Input.File.DragAndDrop>
21
+ * ```
22
+ */
23
+ const InputFileDragAndDrop = ({
24
+ className,
25
+ children,
26
+ onFilesChange,
27
+ disabled = false,
28
+ ...restProps
29
+ }: InputFileDragAndDropProps) => {
30
+ // DragAndDrop에서 필요한 이벤트 핸들러만 useInputFile에서 선택적으로 가져온다.
31
+ const { isDragging, onDropFiles, onDragEnter, onDragLeave, onDragOver } =
32
+ useInputFile({
33
+ onFilesChange,
34
+ disabled,
35
+ });
36
+
37
+ return (
38
+ <div
39
+ // 외부 className을 병합하되, 루트 훅 class는 고정한다.
40
+ className={clsx("input-file-drag-and-drop", className)}
41
+ // 시각 상태는 class 추가 대신 data-*로 노출한다.
42
+ data-dragging={isDragging ? "true" : undefined}
43
+ data-disabled={disabled ? "true" : undefined}
44
+ // DnD 이벤트는 훅에서 생성한 핸들러를 그대로 연결한다.
45
+ onDrop={onDropFiles}
46
+ onDragEnter={onDragEnter}
47
+ onDragLeave={onDragLeave}
48
+ onDragOver={onDragOver}
49
+ // div native props(aria-*, id 등)는 외부에서 확장 가능하게 유지한다.
50
+ {...restProps}
51
+ >
52
+ {children}
53
+ </div>
54
+ );
55
+ };
56
+
57
+ InputFileDragAndDrop.displayName = "InputFileDragAndDrop";
58
+
59
+ export default InputFileDragAndDrop;
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ import { Button } from "../../../button";
4
+ import type { MouseEvent } from "react";
5
+ import type { InputFileUploadButtonProps } from "../../types";
6
+
7
+ /**
8
+ * Input Markup; File Upload 트리거 버튼 컴포넌트
9
+ * @component
10
+ * @param {InputFileUploadButtonProps} props 업로드 버튼 props
11
+ * @param {InputFileOpenDialogHandler} [props.onOpenFileDialog] file dialog open 핸들러
12
+ * @param {ButtonProps} ... Button.Default props 전체를 그대로 전달
13
+ * @example
14
+ * ```tsx
15
+ * // 파일 선택창 오픈 동작만 추가된 Button.Default preset
16
+ * <Input.File.UploadButton
17
+ * fill="outlined"
18
+ * size="small"
19
+ * priority="primary"
20
+ * onOpenFileDialog={fileUpload.onOpenFileDialog}
21
+ * >
22
+ * 파일 선택
23
+ * </Input.File.UploadButton>
24
+ * ```
25
+ */
26
+ export default function InputFileUploadButton({
27
+ onOpenFileDialog,
28
+ onClick,
29
+ ...buttonProps
30
+ }: InputFileUploadButtonProps) {
31
+ /**
32
+ * Input Markup; File Upload 버튼 클릭 병합 핸들러
33
+ * @param {MouseEvent<HTMLElement>} event 클릭 이벤트
34
+ */
35
+ const handleButtonClick = (event: MouseEvent<HTMLElement>) => {
36
+ // 1) 업로드 preset 동작을 우선 실행한다.
37
+ onOpenFileDialog?.(event);
38
+ // 2) 소비자 onClick이 있으면 후속 실행한다.
39
+ onClick?.(event as never);
40
+ };
41
+
42
+ // Button.Default의 렌더링/스타일 계약은 그대로 유지한다.
43
+ return <Button.Default {...buttonProps} onClick={handleButtonClick} />;
44
+ }
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ import type { MouseEvent } from "react";
4
+ import { Chip } from "../../../chip";
5
+ import type { InputFileUploadedChipProps } from "../../types";
6
+
7
+ /**
8
+ * Input Markup; File Upload 완료 상태 Chip 컴포넌트
9
+ * @component
10
+ * @param {InputFileUploadedChipProps} props 업로드 완료 칩 props
11
+ * @param {string} [props.fileId] 업로드 엔트리 id
12
+ * @param {string} props.fileName 업로드된 파일명
13
+ * @param {(fileId: string) => void} [props.onRemoveFromInput] input file 컨텍스트 제거 핸들러
14
+ * @param {() => void} [props.onClearSelectedFiles] fileId가 없을 때 선택 파일 초기화 핸들러
15
+ * @param {(event: MouseEvent<HTMLButtonElement>) => void} [props.onRemove] 파일 제거 핸들러
16
+ * @param {string} [props.removeButtonLabel="업로드 파일 삭제"] 제거 버튼 aria-label
17
+ * @example
18
+ * ```tsx
19
+ * // UploadedChip은 Chip(kind="input") preset으로 동작한다.
20
+ * <Input.File.UploadedChip
21
+ * fileId={meta.id}
22
+ * fileName={meta.name}
23
+ * onRemoveFromInput={onRemoveFileById}
24
+ * onRemove={onTrackRemove}
25
+ * />
26
+ * ```
27
+ */
28
+ export default function InputFileUploadedChip({
29
+ fileId,
30
+ fileName,
31
+ onRemoveFromInput,
32
+ onClearSelectedFiles,
33
+ onRemove,
34
+ removeButtonLabel = "업로드 파일 삭제",
35
+ ...chipProps
36
+ }: InputFileUploadedChipProps) {
37
+ /**
38
+ * Input Markup; File Upload Chip 제거 병합 핸들러
39
+ * @param {MouseEvent<HTMLButtonElement>} event 제거 클릭 이벤트
40
+ */
41
+ const handleRemove = (event: MouseEvent<HTMLButtonElement>) => {
42
+ // 기본 동작: fileId가 있으면 엔트리 제거, 없으면 선택 파일 전체를 초기화한다.
43
+ if (typeof fileId === "string" && fileId.length > 0) {
44
+ onRemoveFromInput?.(fileId);
45
+ } else {
46
+ onClearSelectedFiles?.();
47
+ }
48
+ // 추가 동작: 소비자 콜백을 후속으로 실행한다.
49
+ onRemove?.(event);
50
+ };
51
+
52
+ return (
53
+ <Chip
54
+ // 소비자 전달 Chip props(aria/className/data-*)를 유지한다.
55
+ {...chipProps}
56
+ // preset 목적상 kind는 input으로 고정한다.
57
+ kind="input"
58
+ // 기본 제거 동작 + 소비자 후속 콜백을 병합한 핸들러를 주입한다.
59
+ onRemove={handleRemove}
60
+ removeButtonLabel={removeButtonLabel}
61
+ >
62
+ {/* 파일명은 children slot으로 렌더링한다. */}
63
+ {fileName}
64
+ </Chip>
65
+ );
66
+ }
@@ -0,0 +1,25 @@
1
+ import InputFileDragAndDrop from "./DragAndDrop";
2
+ import InputFileUploadButton from "./UploadButton";
3
+ import InputFileUploadedChip from "./UploadedChip";
4
+ import { InputFileList } from "./list";
5
+
6
+ /**
7
+ * Input Markup; File 네임스페이스
8
+ * @desc
9
+ * - DragAndDrop: 드래그 앤 드롭 입력 영역
10
+ * - UploadButton: 파일 선택창 트리거 버튼
11
+ * - UploadedChip: 업로드 완료 파일 표현/제거 preset
12
+ * - List: headless 파일 목록 구조(Container/Header/Body/Item/Remove)
13
+ * @example
14
+ * ```tsx
15
+ * <Input.File.UploadButton onOpenFileDialog={fileUpload.onOpenFileDialog}>
16
+ * 파일 추가
17
+ * </Input.File.UploadButton>
18
+ * ```
19
+ */
20
+ export const InputFile = {
21
+ DragAndDrop: InputFileDragAndDrop,
22
+ UploadButton: InputFileUploadButton,
23
+ UploadedChip: InputFileUploadedChip,
24
+ List: InputFileList,
25
+ };
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import type { InputFileListBodyProps } from "../../../types";
5
+ import { useInputFileListContext } from "./Provider";
6
+
7
+ /**
8
+ * Input Markup; File List 본문 컴포넌트
9
+ * @component
10
+ * @param {InputFileListBodyProps} props list 본문 props
11
+ * @param {ReactNode} props.children 목록 콘텐츠
12
+ * @param {boolean} [props.forceMount=false] 접힘 상태에서도 DOM 유지 여부
13
+ * @example
14
+ * ```tsx
15
+ * // forceMount=true면 collapsed 상태에서도 DOM을 유지한다.
16
+ * <Input.File.List.Body>
17
+ * <Input.File.List.Item>...</Input.File.List.Item>
18
+ * </Input.File.List.Body>
19
+ * ```
20
+ */
21
+ const InputFileListBody = ({
22
+ className,
23
+ children,
24
+ forceMount = false,
25
+ ...restProps
26
+ }: InputFileListBodyProps) => {
27
+ // List 공유 컨텍스트(접힘 상태)를 조회한다.
28
+ const context = useInputFileListContext();
29
+ // collapsible + collapsed 조합일 때만 실제 접힘 상태로 간주한다.
30
+ const isCollapsed = Boolean(context?.collapsible && context.collapsed);
31
+
32
+ // forceMount=false인 경우 접힘 상태에서는 렌더를 생략한다.
33
+ if (isCollapsed && !forceMount) {
34
+ return null;
35
+ }
36
+
37
+ return (
38
+ <div
39
+ // 루트 class + 외부 className 병합
40
+ className={clsx("input-file-list-body", className)}
41
+ // hidden 속성으로 스크린리더/레이아웃 동작을 일관되게 제어한다.
42
+ hidden={isCollapsed}
43
+ data-collapsed={isCollapsed ? "true" : undefined}
44
+ // div native props(id, aria-*)는 확장 가능하게 전달한다.
45
+ {...restProps}
46
+ >
47
+ {/* 접힘 상태에서도 forceMount 옵션으로 측정/애니메이션 요구를 대응할 수 있다. */}
48
+ {children}
49
+ </div>
50
+ );
51
+ };
52
+
53
+ InputFileListBody.displayName = "InputFileListBody";
54
+
55
+ export default InputFileListBody;
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import { useCallback, useMemo, useState } from "react";
4
+ import clsx from "clsx";
5
+ import type { InputFileListContainerProps } from "../../../types";
6
+ import { InputFileListContextProvider } from "./Provider";
7
+
8
+ /**
9
+ * Input Markup; File List 컨테이너 컴포넌트
10
+ * @component
11
+ * @param {InputFileListContainerProps} props list 컨테이너 props
12
+ * @param {ReactNode} props.children list 하위 슬롯 콘텐츠
13
+ * @param {boolean} [props.collapsible] 헤더 트리거 기반 접기/펼치기 활성화 여부
14
+ * @param {boolean} [props.defaultCollapsed=false] uncontrolled 초기 접힘 상태
15
+ * @param {boolean} [props.collapsed] controlled 접힘 상태
16
+ * @param {(collapsed: boolean) => void} [props.onCollapsedChange] 접힘 상태 변경 콜백
17
+ * @example
18
+ * ```tsx
19
+ * // Header를 trigger로 사용해 List Body를 접고 펼친다.
20
+ * <Input.File.List.Container collapsible>
21
+ * <Input.File.List.Header asTrigger>첨부파일 3</Input.File.List.Header>
22
+ * <Input.File.List.Body>
23
+ * {fileContext.fileMetas.map(meta => (
24
+ * // 렌더링 key는 domain/category/item depth 포맷을 사용한다.
25
+ * <Input.File.List.Item key={`input/file/list/item/${meta.id}`}>
26
+ * {meta.name}
27
+ * </Input.File.List.Item>
28
+ * ))}
29
+ * </Input.File.List.Body>
30
+ * </Input.File.List.Container>
31
+ * ```
32
+ */
33
+ const InputFileListContainer = ({
34
+ className,
35
+ children,
36
+ collapsible = false,
37
+ defaultCollapsed = false,
38
+ collapsed,
39
+ onCollapsedChange,
40
+ ...restProps
41
+ }: InputFileListContainerProps) => {
42
+ // uncontrolled 모드에서만 사용하는 내부 접힘 상태다.
43
+ const [internalCollapsed, setInternalCollapsed] = useState(defaultCollapsed);
44
+ // collapsed prop 제공 여부로 controlled/uncontrolled 모드를 구분한다.
45
+ const isControlled = typeof collapsed === "boolean";
46
+ // 최종 접힘 상태는 controlled 우선으로 계산한다.
47
+ const resolvedCollapsed = isControlled ? collapsed : internalCollapsed;
48
+
49
+ /**
50
+ * Input Markup; File List 접힘 상태 토글 핸들러
51
+ */
52
+ const onToggleCollapsed = useCallback(() => {
53
+ // collapsible=false면 토글 동작을 비활성화한다.
54
+ if (!collapsible) {
55
+ return;
56
+ }
57
+ // 현재 상태를 반전한 다음 상태를 계산한다.
58
+ const nextCollapsed = !resolvedCollapsed;
59
+ // uncontrolled 모드일 때만 내부 상태를 갱신한다.
60
+ if (!isControlled) {
61
+ setInternalCollapsed(nextCollapsed);
62
+ }
63
+ // 외부 콜백이 있으면 상태 변경을 통지한다.
64
+ onCollapsedChange?.(nextCollapsed);
65
+ }, [collapsible, isControlled, onCollapsedChange, resolvedCollapsed]);
66
+
67
+ // Provider value를 메모화해 불필요한 하위 리렌더를 줄인다.
68
+ const contextValue = useMemo(
69
+ () => ({
70
+ collapsible,
71
+ collapsed: resolvedCollapsed,
72
+ onToggleCollapsed,
73
+ }),
74
+ [collapsible, resolvedCollapsed, onToggleCollapsed],
75
+ );
76
+
77
+ return (
78
+ <InputFileListContextProvider value={contextValue}>
79
+ <section
80
+ // 루트 클래스는 고정하고 외부 className을 병합한다.
81
+ className={clsx("input-file-list-container", className)}
82
+ // 상태는 data-*로 노출해 SCSS에서 조건 스타일을 처리한다.
83
+ data-collapsible={collapsible ? "true" : undefined}
84
+ data-collapsed={resolvedCollapsed ? "true" : undefined}
85
+ // section native props(id, aria-*)는 확장 가능하게 전달한다.
86
+ {...restProps}
87
+ >
88
+ {/* List 루트 상태를 data-* 속성으로 고정해 외부 SCSS 제어를 단순화한다. */}
89
+ {children}
90
+ </section>
91
+ </InputFileListContextProvider>
92
+ );
93
+ };
94
+
95
+ InputFileListContainer.displayName = "InputFileListContainer";
96
+
97
+ export default InputFileListContainer;
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import type { KeyboardEvent, MouseEvent } from "react";
5
+ import type { InputFileListHeaderProps } from "../../../types";
6
+ import { useInputFileListContext } from "./Provider";
7
+
8
+ /**
9
+ * Input Markup; File List 헤더 컴포넌트
10
+ * @component
11
+ * @param {InputFileListHeaderProps} props list 헤더 props
12
+ * @param {ReactNode} props.children 헤더 콘텐츠
13
+ * @param {boolean} [props.asTrigger=false] 클릭/키보드 트리거로 접기/펼치기 제어 여부
14
+ * @example
15
+ * ```tsx
16
+ * // Header 클릭/키보드 Enter, Space로 collapsed 상태를 전환한다.
17
+ * <Input.File.List.Header asTrigger>
18
+ * <span>첨부파일 3</span>
19
+ * </Input.File.List.Header>
20
+ * ```
21
+ */
22
+ const InputFileListHeader = ({
23
+ className,
24
+ children,
25
+ asTrigger = false,
26
+ onClick,
27
+ onKeyDown,
28
+ ...restProps
29
+ }: InputFileListHeaderProps) => {
30
+ // Provider에서 공유한 collapsible/collapsed 컨텍스트를 조회한다.
31
+ const context = useInputFileListContext();
32
+
33
+ /**
34
+ * Input Markup; File List 헤더 클릭 핸들러
35
+ * @param {MouseEvent<HTMLDivElement>} event 헤더 클릭 이벤트
36
+ */
37
+ const handleClick = (event: MouseEvent<HTMLDivElement>) => {
38
+ // 1) 소비자 onClick을 우선 실행한다.
39
+ onClick?.(event);
40
+ // 2) preventDefault되지 않았고 trigger 모드면 collapsed를 토글한다.
41
+ if (!event.defaultPrevented && asTrigger) {
42
+ context?.onToggleCollapsed();
43
+ }
44
+ };
45
+
46
+ /**
47
+ * Input Markup; File List 헤더 키보드 핸들러
48
+ * @param {KeyboardEvent<HTMLDivElement>} event 키보드 이벤트
49
+ */
50
+ const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
51
+ // 1) 소비자 onKeyDown을 우선 실행한다.
52
+ onKeyDown?.(event);
53
+ // 2) trigger가 아니거나 preventDefault면 토글을 수행하지 않는다.
54
+ if (event.defaultPrevented || !asTrigger) {
55
+ return;
56
+ }
57
+ // 3) Enter/Space에서 button과 유사한 접근성 동작을 제공한다.
58
+ if (event.key === "Enter" || event.key === " ") {
59
+ event.preventDefault();
60
+ context?.onToggleCollapsed();
61
+ }
62
+ };
63
+
64
+ return (
65
+ <div
66
+ // 루트 class + 외부 className 병합
67
+ className={clsx("input-file-list-header", className)}
68
+ // 헤더의 트리거/상태 정보를 data-*로 노출한다.
69
+ data-trigger={asTrigger ? "true" : undefined}
70
+ data-collapsible={context?.collapsible ? "true" : undefined}
71
+ data-collapsed={context?.collapsed ? "true" : undefined}
72
+ // trigger 모드면 div를 button처럼 접근 가능하게 설정한다.
73
+ role={asTrigger ? "button" : undefined}
74
+ tabIndex={asTrigger ? 0 : restProps.tabIndex}
75
+ onClick={handleClick}
76
+ onKeyDown={handleKeyDown}
77
+ {...restProps}
78
+ >
79
+ {children}
80
+ </div>
81
+ );
82
+ };
83
+
84
+ InputFileListHeader.displayName = "InputFileListHeader";
85
+
86
+ export default InputFileListHeader;
@@ -0,0 +1,35 @@
1
+ import clsx from "clsx";
2
+ import type { InputFileListItemProps } from "../../../types";
3
+
4
+ /**
5
+ * Input Markup; File List 항목 컴포넌트
6
+ * @component
7
+ * @param {InputFileListItemProps} props list 항목 props
8
+ * @param {ReactNode} props.children 파일 항목 콘텐츠
9
+ * @example
10
+ * ```tsx
11
+ * // Item 내부 배치는 서비스/도메인에서 자유롭게 구성한다.
12
+ * fileContext.fileMetas.map(meta => (
13
+ * <Input.File.List.Item key={`input/file/list/item/${meta.id}`}>
14
+ * <span>{meta.name}</span>
15
+ * <span>{meta.sizeLabel}</span>
16
+ * </Input.File.List.Item>
17
+ * ));
18
+ * ```
19
+ */
20
+ const InputFileListItem = ({
21
+ className,
22
+ children,
23
+ ...restProps
24
+ }: InputFileListItemProps) => {
25
+ return (
26
+ // Item은 레이아웃 뼈대만 제공하고 콘텐츠/행동은 children으로 위임한다.
27
+ <div className={clsx("input-file-list-item", className)} {...restProps}>
28
+ {children}
29
+ </div>
30
+ );
31
+ };
32
+
33
+ InputFileListItem.displayName = "InputFileListItem";
34
+
35
+ export default InputFileListItem;
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+ import type { InputFileListContextValue } from "../../../types";
5
+
6
+ /**
7
+ * Input Markup; File List 컨텍스트 인스턴스
8
+ */
9
+ const InputFileListContext = createContext<InputFileListContextValue | null>(
10
+ null,
11
+ );
12
+
13
+ /**
14
+ * Input Markup; File List 컨텍스트 생성 헬퍼
15
+ */
16
+ export const InputFileListContextProvider = InputFileListContext.Provider;
17
+
18
+ /**
19
+ * Input Hook; File List 컨텍스트 조회 Hook
20
+ * @hook
21
+ * @returns {InputFileListContextValue | null} List 컨텍스트 값(null이면 Container 외부)
22
+ * @example
23
+ * ```tsx
24
+ * // Container 내부에서는 컨텍스트 값이 존재한다.
25
+ * const fileListContext = useInputFileListContext();
26
+ * if (!fileListContext) {
27
+ * return null;
28
+ * }
29
+ * return <span>{fileListContext.collapsed ? "닫힘" : "열림"}</span>;
30
+ * ```
31
+ */
32
+ export const useInputFileListContext = (): InputFileListContextValue | null => {
33
+ // createContext로 생성한 List 컨텍스트를 조회한다.
34
+ return useContext(InputFileListContext);
35
+ };
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import type { MouseEvent } from "react";
5
+ import type { InputFileListRemoveProps } from "../../../types";
6
+
7
+ /**
8
+ * Input Markup; File List 제거 액션 컴포넌트
9
+ * @component
10
+ * @param {InputFileListRemoveProps} props 제거 액션 props
11
+ * @param {ReactNode} [props.children] 제거 버튼 콘텐츠
12
+ * @param {(event: MouseEvent<HTMLButtonElement>) => void} [props.onRemove] 제거 핸들러
13
+ * @example
14
+ * ```tsx
15
+ * // Remove는 삭제 동작 트리거만 제공하고 라벨/아이콘은 외부에서 주입한다.
16
+ * <Input.File.List.Remove onRemove={() => onRemoveFileById(meta.id)}>
17
+ * 삭제
18
+ * </Input.File.List.Remove>
19
+ * ```
20
+ */
21
+ const InputFileListRemove = ({
22
+ className,
23
+ children,
24
+ onRemove,
25
+ type = "button",
26
+ ...restProps
27
+ }: InputFileListRemoveProps) => {
28
+ /**
29
+ * Input Markup; File List 제거 클릭 핸들러
30
+ * @param {MouseEvent<HTMLButtonElement>} event 제거 클릭 이벤트
31
+ */
32
+ const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
33
+ // 소비자가 전달한 제거 콜백을 실행한다.
34
+ onRemove?.(event);
35
+ };
36
+
37
+ return (
38
+ <button
39
+ // 제출 버튼 오작동을 방지하기 위해 기본 type은 button으로 고정한다.
40
+ type={type}
41
+ // 루트 class + 외부 className 병합
42
+ className={clsx("input-file-list-remove", className)}
43
+ onClick={handleClick}
44
+ // button native props(aria-*, disabled 등)는 그대로 전달한다.
45
+ {...restProps}
46
+ >
47
+ {children}
48
+ </button>
49
+ );
50
+ };
51
+
52
+ InputFileListRemove.displayName = "InputFileListRemove";
53
+
54
+ export default InputFileListRemove;
@@ -0,0 +1,31 @@
1
+ import InputFileListContainer from "./Container";
2
+ import InputFileListHeader from "./Header";
3
+ import InputFileListBody from "./Body";
4
+ import InputFileListItem from "./Item";
5
+ import InputFileListRemove from "./Remove";
6
+
7
+ /**
8
+ * Input Markup; File List 네임스페이스
9
+ * @desc
10
+ * - Container: collapsible 상태를 관리하는 루트 래퍼
11
+ * - Header: trigger 모드(asTrigger) 지원 헤더
12
+ * - Body: collapsed 상태를 반영하는 목록 본문
13
+ * - Item: 파일 행 레이아웃 뼈대
14
+ * - Remove: 파일 제거 액션 버튼
15
+ * @example
16
+ * ```tsx
17
+ * <Input.File.List.Container collapsible>
18
+ * <Input.File.List.Header asTrigger>첨부파일 2</Input.File.List.Header>
19
+ * <Input.File.List.Body>
20
+ * <Input.File.List.Item>...</Input.File.List.Item>
21
+ * </Input.File.List.Body>
22
+ * </Input.File.List.Container>
23
+ * ```
24
+ */
25
+ export const InputFileList = {
26
+ Container: InputFileListContainer,
27
+ Header: InputFileListHeader,
28
+ Body: InputFileListBody,
29
+ Item: InputFileListItem,
30
+ Remove: InputFileListRemove,
31
+ };
@@ -1,5 +1,3 @@
1
- "use client";
2
-
3
1
  import clsx from "clsx";
4
2
 
5
3
  /**
@@ -77,6 +77,8 @@ export default function InputBaseUtil({
77
77
  <button
78
78
  type="button"
79
79
  className="input-affix input-affix-clear"
80
+ // Tab 이동 시 clear 버튼을 건너뛰고 다음 입력/액션으로 이동하도록 포커스를 제외한다.
81
+ tabIndex={-1}
80
82
  data-slot="clear"
81
83
  data-visible="true"
82
84
  onClick={onClear}
@@ -1,5 +1,3 @@
1
- "use client";
2
-
3
1
  import InputBase from "./Input";
4
2
  import InputBaseSideSlot from "./SideSlot";
5
3
  import InputBaseUtil from "./Utility";
@@ -2,10 +2,12 @@ import { InputFoundation } from "./foundation";
2
2
  import { InputText } from "./text";
3
3
  import { InputDate } from "./date";
4
4
  import { InputAddress } from "./address";
5
+ import { InputFile } from "./file";
5
6
 
6
7
  export const Input = {
7
8
  ...InputFoundation,
8
9
  Text: InputText,
9
10
  Date: InputDate,
10
11
  Address: InputAddress,
12
+ File: InputFile,
11
13
  };
@@ -36,6 +36,8 @@ const PasswordInput = forwardRef<HTMLInputElement, InputPasswordProps>(
36
36
  <button
37
37
  type="button"
38
38
  className="input-password-toggle"
39
+ // Tab 이동 시 다음 입력 필드로 바로 넘어가도록 토글 버튼은 순서에서 제외한다.
40
+ tabIndex={-1}
39
41
  onClick={handleToggle}
40
42
  aria-pressed={visible}
41
43
  aria-label={visible ? toggleLabel.hide : toggleLabel.show}
@@ -1,5 +1,3 @@
1
- "use client";
2
-
3
1
  import { forwardRef } from "react";
4
2
  import type { SearchInputProps } from "../../types";
5
3
  import InputBase from "../foundation/Input";