@uniai-fe/uds-primitives 0.3.12 → 0.3.14
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.
- package/dist/styles.css +159 -6
- package/package.json +1 -1
- package/src/components/calendar/markup/Core.tsx +12 -3
- package/src/components/calendar/types/calendar.ts +0 -5
- package/src/components/input/markup/date/Template.tsx +9 -14
- package/src/components/input/markup/date/Trigger.tsx +68 -3
- package/src/components/input/markup/foundation/Input.tsx +23 -7
- package/src/components/input/markup/foundation/Utility.tsx +3 -2
- package/src/components/input/styles/foundation.scss +6 -0
- package/src/components/input/styles/variables.scss +2 -1
- package/src/components/input/types/date.ts +22 -1
- package/src/components/input/types/foundation.ts +5 -0
- package/src/components/select/styles/select.scss +19 -4
- package/src/components/select/styles/variables.scss +11 -0
- package/src/components/table/markup/Container.tsx +41 -3
- package/src/components/table/markup/foundation/Th.tsx +9 -2
- package/src/components/table/styles/foundation.scss +212 -2
- package/src/components/table/types/foundation.ts +22 -0
package/dist/styles.css
CHANGED
|
@@ -363,7 +363,8 @@
|
|
|
363
363
|
--input-text-disabled-color: var(--color-label-disabled);
|
|
364
364
|
--input-placeholder-color: var(--color-label-alternative);
|
|
365
365
|
--input-placeholder-disabled-color: var(--color-label-disabled);
|
|
366
|
-
|
|
366
|
+
/* 변경: readonly 입력은 placeholder를 숨겨 value 텍스트 대비를 고정한다. */
|
|
367
|
+
--input-placeholder-readonly-color: transparent;
|
|
367
368
|
--input-font-size: var(--font-body-medium-size);
|
|
368
369
|
--input-line-height: var(--font-body-medium-line-height);
|
|
369
370
|
--input-font-weight: 400;
|
|
@@ -472,6 +473,12 @@
|
|
|
472
473
|
--select-table-color-text: var(--input-text-color);
|
|
473
474
|
--select-table-color-text-readonly: var(--select-primary-color-text-readonly);
|
|
474
475
|
--select-table-color-placeholder: var(--input-placeholder-color);
|
|
476
|
+
--select-table-color-placeholder-disabled: var(
|
|
477
|
+
--select-primary-color-placeholder-disabled
|
|
478
|
+
);
|
|
479
|
+
--select-table-color-placeholder-readonly: var(
|
|
480
|
+
--select-primary-color-placeholder-readonly
|
|
481
|
+
);
|
|
475
482
|
--select-table-icon-color-default: var(--select-icon-color-default);
|
|
476
483
|
--select-table-icon-color-focused: var(--select-icon-color-focused);
|
|
477
484
|
--select-table-icon-color-disabled: var(--select-icon-color-disabled);
|
|
@@ -506,6 +513,11 @@
|
|
|
506
513
|
--select-icon-size-medium: 2rem;
|
|
507
514
|
--select-icon-size-large: 2rem;
|
|
508
515
|
--select-primary-color-placeholder: var(--input-placeholder-color);
|
|
516
|
+
/* 변경: placeholder disabled/readonly 토큰을 분리해 상태별 제어 지점을 고정한다. */
|
|
517
|
+
--select-primary-color-placeholder-disabled: var(--color-label-disabled);
|
|
518
|
+
--select-primary-color-placeholder-readonly: var(
|
|
519
|
+
--select-primary-color-placeholder-disabled
|
|
520
|
+
);
|
|
509
521
|
--select-primary-color-surface: var(--input-surface-color);
|
|
510
522
|
--select-primary-color-text: var(--color-label-alternative);
|
|
511
523
|
--select-primary-color-text-focused: var(--color-label-strong);
|
|
@@ -2550,6 +2562,10 @@ figure.chip {
|
|
|
2550
2562
|
border-width: var(--input-border-width-default);
|
|
2551
2563
|
background-color: var(--input-surface-disabled-color);
|
|
2552
2564
|
}
|
|
2565
|
+
.input-field:not([data-priority=secondary])[data-state=error][data-readonly=true] {
|
|
2566
|
+
border-color: var(--input-border-error-color);
|
|
2567
|
+
border-width: var(--input-border-width-emphasis);
|
|
2568
|
+
}
|
|
2553
2569
|
|
|
2554
2570
|
.input-element {
|
|
2555
2571
|
flex: 1 1 auto;
|
|
@@ -3806,12 +3822,24 @@ figure.chip {
|
|
|
3806
3822
|
.select-button[data-priority=table] .select-label-placeholder {
|
|
3807
3823
|
color: var(--select-table-color-placeholder);
|
|
3808
3824
|
}
|
|
3809
|
-
.select-button[data-state=disabled] .select-label-placeholder
|
|
3810
|
-
color: var(--select-primary-color-
|
|
3825
|
+
.select-button[data-state=disabled] .select-label-placeholder {
|
|
3826
|
+
color: var(--select-primary-color-placeholder-disabled);
|
|
3827
|
+
}
|
|
3828
|
+
.select-button[data-readonly=true] .select-label-placeholder {
|
|
3829
|
+
color: var(--select-primary-color-placeholder-readonly);
|
|
3811
3830
|
}
|
|
3812
|
-
.select-button[data-priority=secondary][data-state=disabled] .select-label-placeholder
|
|
3831
|
+
.select-button[data-priority=secondary][data-state=disabled] .select-label-placeholder {
|
|
3813
3832
|
color: var(--select-secondary-color-placeholder-disabled);
|
|
3814
3833
|
}
|
|
3834
|
+
.select-button[data-priority=secondary][data-readonly=true] .select-label-placeholder {
|
|
3835
|
+
color: var(--select-secondary-color-placeholder-readonly);
|
|
3836
|
+
}
|
|
3837
|
+
.select-button[data-priority=table][data-state=disabled] .select-label-placeholder {
|
|
3838
|
+
color: var(--select-table-color-placeholder-disabled);
|
|
3839
|
+
}
|
|
3840
|
+
.select-button[data-priority=table][data-readonly=true] .select-label-placeholder {
|
|
3841
|
+
color: var(--select-table-color-placeholder-readonly);
|
|
3842
|
+
}
|
|
3815
3843
|
|
|
3816
3844
|
.select-button[data-size=small] .select-label {
|
|
3817
3845
|
font-size: var(--select-text-small-size);
|
|
@@ -4193,17 +4221,21 @@ figure.chip {
|
|
|
4193
4221
|
font-weight: 600;
|
|
4194
4222
|
}
|
|
4195
4223
|
|
|
4224
|
+
.table.table-container[data-layout=line] .table-native-cell[rowspan] {
|
|
4225
|
+
border-right: 1px solid var(--table-border-color);
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4196
4228
|
.table.table-container[data-layout=grid] .table-native-cell.table-th,
|
|
4197
4229
|
.table.table-container[data-layout=grid] .table-native-cell.table-td {
|
|
4198
4230
|
--table-cell-padding-inline: var(--table-grid-cell-padding-inline);
|
|
4199
4231
|
--table-cell-padding-block: var(--table-grid-cell-padding-block);
|
|
4200
|
-
border: 1px solid var(--table-border-color);
|
|
4201
4232
|
height: var(--table-grid-cell-height);
|
|
4202
4233
|
}
|
|
4203
4234
|
|
|
4204
4235
|
.table.table-container[data-layout=grid] {
|
|
4205
4236
|
border-radius: var(--table-grid-border-radius);
|
|
4206
|
-
overflow:
|
|
4237
|
+
overflow: visible;
|
|
4238
|
+
isolation: isolate;
|
|
4207
4239
|
}
|
|
4208
4240
|
|
|
4209
4241
|
.table.table-container[data-layout=grid] .table-head .table-native-cell.table-th {
|
|
@@ -4214,11 +4246,124 @@ figure.chip {
|
|
|
4214
4246
|
background-color: var(--table-grid-row-header-background-color);
|
|
4215
4247
|
}
|
|
4216
4248
|
|
|
4249
|
+
.table.table-container[data-layout=grid] .table-foot .table-native-cell.table-th {
|
|
4250
|
+
background-color: var(--table-grid-row-header-background-color);
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4217
4253
|
.table.table-container[data-layout=grid] .table-row[data-highlighted=true] .table-native-cell.table-th,
|
|
4218
4254
|
.table.table-container[data-layout=grid] .table-row[data-highlighted=true] .table-native-cell.table-td {
|
|
4219
4255
|
background-color: var(--table-grid-row-highlight-background-color);
|
|
4220
4256
|
}
|
|
4221
4257
|
|
|
4258
|
+
.table.table-container .table-row > .table-native-cell.table-th:first-child {
|
|
4259
|
+
position: sticky;
|
|
4260
|
+
left: 0;
|
|
4261
|
+
}
|
|
4262
|
+
|
|
4263
|
+
.table.table-container .table-head .table-row > .table-native-cell.table-th:first-child {
|
|
4264
|
+
z-index: 150;
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
.table.table-container .table-body .table-row > .table-native-cell.table-th:first-child {
|
|
4268
|
+
z-index: 100;
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
.table.table-container .table-foot .table-row > .table-native-cell.table-th:first-child {
|
|
4272
|
+
z-index: 50;
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
.table.table-container .table-head .table-row:first-child > .table-native-cell.table-th:first-child {
|
|
4276
|
+
z-index: 200;
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
.table.table-container .table-native-cell.table-td {
|
|
4280
|
+
position: relative;
|
|
4281
|
+
z-index: 0;
|
|
4282
|
+
}
|
|
4283
|
+
|
|
4284
|
+
.table.table-container[data-layout=grid] .table-head .table-row:first-child > .table-native-cell:first-child {
|
|
4285
|
+
border-top-left-radius: var(--table-grid-border-radius);
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
.table.table-container[data-layout=grid] .table-head .table-row:first-child > .table-native-cell:last-child {
|
|
4289
|
+
border-top-right-radius: var(--table-grid-border-radius);
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
.table.table-container[data-layout=grid] .table-foot .table-row:last-child > .table-native-cell:first-child,
|
|
4293
|
+
.table.table-container[data-layout=grid]:not([data-has-footer=true]) .table-body:last-of-type .table-row:last-child > .table-native-cell:first-child {
|
|
4294
|
+
border-bottom-left-radius: var(--table-grid-border-radius);
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
.table.table-container[data-layout=grid] .table-foot .table-row:last-child > .table-native-cell:last-child,
|
|
4298
|
+
.table.table-container[data-layout=grid]:not([data-has-footer=true]) .table-body:last-of-type .table-row:last-child > .table-native-cell:last-child {
|
|
4299
|
+
border-bottom-right-radius: var(--table-grid-border-radius);
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4302
|
+
.table.table-container[data-layout=grid] .table-cell-content {
|
|
4303
|
+
border-right: 1px solid var(--table-border-color);
|
|
4304
|
+
border-bottom: 1px solid var(--table-border-color);
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
.table.table-container[data-layout=grid] .table-head .table-row:first-child > .table-native-cell > .table-cell-content {
|
|
4308
|
+
border-top: 1px solid var(--table-border-color);
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
.table.table-container[data-layout=grid] .table-row > .table-native-cell:first-child > .table-cell-content {
|
|
4312
|
+
border-left: 1px solid var(--table-border-color);
|
|
4313
|
+
}
|
|
4314
|
+
|
|
4315
|
+
.table.table-container[data-layout=grid] .table-row > .table-native-cell[rowspan] > .table-cell-content {
|
|
4316
|
+
border-right: 0;
|
|
4317
|
+
}
|
|
4318
|
+
|
|
4319
|
+
.table.table-container[data-layout=grid] .table-row > .table-native-cell[rowspan] + .table-native-cell > .table-cell-content {
|
|
4320
|
+
border-left: 1px solid var(--table-border-color);
|
|
4321
|
+
}
|
|
4322
|
+
|
|
4323
|
+
.table.table-container[data-layout=grid] .table-row:first-child > .table-native-cell:first-child .table-cell-content {
|
|
4324
|
+
border-top-left-radius: inherit;
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
.table.table-container[data-layout=grid] .table-row:first-child > .table-native-cell:last-child .table-cell-content {
|
|
4328
|
+
border-top-right-radius: inherit;
|
|
4329
|
+
}
|
|
4330
|
+
|
|
4331
|
+
.table.table-container[data-layout=grid] .table-foot .table-row:last-child > .table-native-cell:first-child .table-cell-content,
|
|
4332
|
+
.table.table-container[data-layout=grid]:not([data-has-footer=true]) .table-body:last-of-type .table-row:last-child > .table-native-cell:first-child .table-cell-content {
|
|
4333
|
+
border-bottom-left-radius: inherit;
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
.table.table-container[data-layout=grid] .table-foot .table-row:last-child > .table-native-cell:last-child .table-cell-content,
|
|
4337
|
+
.table.table-container[data-layout=grid]:not([data-has-footer=true]) .table-body:last-of-type .table-row:last-child > .table-native-cell:last-child .table-cell-content {
|
|
4338
|
+
border-bottom-right-radius: inherit;
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4341
|
+
.table-scroll-wrapper {
|
|
4342
|
+
width: 100%;
|
|
4343
|
+
border-radius: var(--table-grid-border-radius);
|
|
4344
|
+
overscroll-behavior: none;
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
.table-scroll-wrapper[data-scroll-axis=x] {
|
|
4348
|
+
overflow-x: auto;
|
|
4349
|
+
overflow-y: visible;
|
|
4350
|
+
overscroll-behavior-x: none;
|
|
4351
|
+
-webkit-overflow-scrolling: auto;
|
|
4352
|
+
}
|
|
4353
|
+
|
|
4354
|
+
.table-scroll-wrapper[data-scroll-axis=y] {
|
|
4355
|
+
overflow-x: visible;
|
|
4356
|
+
overflow-y: auto;
|
|
4357
|
+
overscroll-behavior-y: none;
|
|
4358
|
+
-webkit-overflow-scrolling: auto;
|
|
4359
|
+
}
|
|
4360
|
+
|
|
4361
|
+
.table-scroll-wrapper[data-scroll-axis=both] {
|
|
4362
|
+
overflow: auto;
|
|
4363
|
+
overscroll-behavior: none;
|
|
4364
|
+
-webkit-overflow-scrolling: auto;
|
|
4365
|
+
}
|
|
4366
|
+
|
|
4222
4367
|
.table-native-cell.table-th .table-native-cell-text {
|
|
4223
4368
|
color: var(--table-th-text-color);
|
|
4224
4369
|
font-size: var(--table-th-text-size);
|
|
@@ -4442,6 +4587,14 @@ figure.chip {
|
|
|
4442
4587
|
line-height: var(--table-td-text-line-height);
|
|
4443
4588
|
}
|
|
4444
4589
|
|
|
4590
|
+
.table.table-container .table-foot .table-cell-content,
|
|
4591
|
+
.table.table-container .table-foot .table-cell-text {
|
|
4592
|
+
color: var(--table-td-text-color);
|
|
4593
|
+
font-size: var(--table-td-text-size);
|
|
4594
|
+
font-weight: var(--table-td-text-weight);
|
|
4595
|
+
line-height: var(--table-td-text-line-height);
|
|
4596
|
+
}
|
|
4597
|
+
|
|
4445
4598
|
.table-cell.table-body-cell .radio-label-text,
|
|
4446
4599
|
.table-cell.table-foot-cell .radio-label-text {
|
|
4447
4600
|
color: var(--table-td-text-color);
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { DatePicker } from "@mantine/dates";
|
|
4
4
|
import { CalendarIcon } from "./Icon";
|
|
5
|
-
import type { CalendarGridProps } from "../types";
|
|
5
|
+
import type { CalendarDatePickerProps, CalendarGridProps } from "../types";
|
|
6
6
|
import { mapValueToPicker, parseValueFromPicker } from "../utils";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -20,6 +20,14 @@ export default function CalendarCore({
|
|
|
20
20
|
onChange,
|
|
21
21
|
datePickerProps,
|
|
22
22
|
}: CalendarGridProps) {
|
|
23
|
+
const { valueFormat: _deprecatedValueFormat, ...safeDatePickerProps } =
|
|
24
|
+
(datePickerProps ?? {}) as CalendarDatePickerProps & {
|
|
25
|
+
/**
|
|
26
|
+
* deprecated DatePicker 표시 포맷 옵션
|
|
27
|
+
*/
|
|
28
|
+
valueFormat?: unknown;
|
|
29
|
+
};
|
|
30
|
+
|
|
23
31
|
// 기본 DatePicker 옵션/스타일 책임을 Calendar.Core에 고정한다.
|
|
24
32
|
const resolvedDatePickerProps = {
|
|
25
33
|
size: "sm" as const,
|
|
@@ -27,7 +35,8 @@ export default function CalendarCore({
|
|
|
27
35
|
weekendDays: [] as Array<0 | 1 | 2 | 3 | 4 | 5 | 6>,
|
|
28
36
|
nextIcon: <CalendarIcon.Chevron.right />,
|
|
29
37
|
previousIcon: <CalendarIcon.Chevron.left />,
|
|
30
|
-
|
|
38
|
+
// 변경: 지원하지 않는 valueFormat은 제거한 뒤 DatePicker 옵션을 병합한다.
|
|
39
|
+
...safeDatePickerProps,
|
|
31
40
|
classNames: {
|
|
32
41
|
levelsGroup: "calendar-month-level",
|
|
33
42
|
month: "calendar-month-table",
|
|
@@ -44,7 +53,7 @@ export default function CalendarCore({
|
|
|
44
53
|
yearsList: "calendar-years-list",
|
|
45
54
|
yearsListCell: "calendar-years-list-cell",
|
|
46
55
|
yearsListControl: "calendar-years-list-control",
|
|
47
|
-
...(
|
|
56
|
+
...(safeDatePickerProps.classNames ?? {}),
|
|
48
57
|
},
|
|
49
58
|
};
|
|
50
59
|
|
|
@@ -35,7 +35,6 @@ export type CalendarOnChange = (value: CalendarValue) => void;
|
|
|
35
35
|
/**
|
|
36
36
|
* Mantine DatePicker 공개 옵션.
|
|
37
37
|
* @property {Partial<Record<DatePickerStylesNames, string>>} [classNames] Mantine 내부 스타일 클래스 매핑
|
|
38
|
-
* @property {string} [valueFormat] DatePicker 표시 포맷
|
|
39
38
|
*/
|
|
40
39
|
export type CalendarDatePickerProps = Partial<
|
|
41
40
|
Omit<
|
|
@@ -49,10 +48,6 @@ export type CalendarDatePickerProps = Partial<
|
|
|
49
48
|
| "styles"
|
|
50
49
|
>
|
|
51
50
|
> & {
|
|
52
|
-
/**
|
|
53
|
-
* DatePicker 표시 포맷
|
|
54
|
-
*/
|
|
55
|
-
valueFormat?: string;
|
|
56
51
|
/**
|
|
57
52
|
* Mantine 내부 스타일 classNames override.
|
|
58
53
|
*/
|
|
@@ -23,9 +23,8 @@ const INPUT_DATE_TABLE_FORMAT = "YY-MM-DD";
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Input Date Template; trigger + calendar 조합.
|
|
26
|
-
* priority가 table이면 trigger 표시
|
|
26
|
+
* priority가 table이면 trigger 표시 포맷을 `YY-MM-DD`로 맞춘다.
|
|
27
27
|
* placeholder는 format과 별도 props로 처리되며, format 변경으로 placeholder가 자동 치환되지는 않는다.
|
|
28
|
-
* 단, `datePickerProps.valueFormat`이 주어지면 해당 포맷을 우선한다.
|
|
29
28
|
* @component
|
|
30
29
|
* @param {InputCalendarProps} props
|
|
31
30
|
* @param {CalendarMode} [props.mode="date"] 날짜/시간 모드
|
|
@@ -41,6 +40,7 @@ const INPUT_DATE_TABLE_FORMAT = "YY-MM-DD";
|
|
|
41
40
|
* @param {UseFormRegisterReturn} [props.register] RHF register
|
|
42
41
|
* @param {string} [props.placeholder="YYYY-MM-DD"] placeholder
|
|
43
42
|
* @param {"primary" | "secondary" | "tertiary" | "table"} [props.priority="primary"] trigger input priority
|
|
43
|
+
* @param {"default" | "active" | "focused" | "success" | "error" | "disabled" | "loading"} [props.state="default"] trigger input state
|
|
44
44
|
* @param {ReactNode} [props.header] 패널 header 콘텐츠
|
|
45
45
|
* @param {ReactNode} [props.footer] 패널 footer 콘텐츠
|
|
46
46
|
* @param {unknown} [props.timePicker] TimePicker 확장용 예약 슬롯(현재 미구현)
|
|
@@ -67,6 +67,7 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
|
|
|
67
67
|
register,
|
|
68
68
|
placeholder = "YYYY-MM-DD",
|
|
69
69
|
priority = "primary",
|
|
70
|
+
state = "default",
|
|
70
71
|
className,
|
|
71
72
|
header,
|
|
72
73
|
footer,
|
|
@@ -132,17 +133,6 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
|
|
|
132
133
|
[calendarValue, priority],
|
|
133
134
|
);
|
|
134
135
|
|
|
135
|
-
const resolvedDatePickerProps = useMemo(() => {
|
|
136
|
-
if (priority !== "table" || datePickerProps?.valueFormat) {
|
|
137
|
-
return datePickerProps;
|
|
138
|
-
}
|
|
139
|
-
// table priority는 yy-MM-dd 포맷을 기본값으로 둔다.
|
|
140
|
-
return {
|
|
141
|
-
...datePickerProps,
|
|
142
|
-
valueFormat: INPUT_DATE_TABLE_FORMAT,
|
|
143
|
-
};
|
|
144
|
-
}, [datePickerProps, priority]);
|
|
145
|
-
|
|
146
136
|
const handleTriggerClick = (event: ReactMouseEvent<Element>) => {
|
|
147
137
|
triggerOnClick?.(event);
|
|
148
138
|
};
|
|
@@ -168,8 +158,10 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
|
|
|
168
158
|
placeholder,
|
|
169
159
|
displayValue: triggerValue,
|
|
170
160
|
disabled,
|
|
161
|
+
readOnly,
|
|
171
162
|
onClick: handleTriggerClick,
|
|
172
163
|
priority,
|
|
164
|
+
state,
|
|
173
165
|
};
|
|
174
166
|
|
|
175
167
|
const triggerNode = trigger ?? renderTrigger?.(triggerRenderProps) ?? (
|
|
@@ -180,6 +172,8 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
|
|
|
180
172
|
placeholder={placeholder}
|
|
181
173
|
displayValue={triggerValue}
|
|
182
174
|
disabled={disabled}
|
|
175
|
+
state={state}
|
|
176
|
+
readOnly={readOnly}
|
|
183
177
|
onClick={handleTriggerClick}
|
|
184
178
|
priority={priority}
|
|
185
179
|
/>
|
|
@@ -195,7 +189,8 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
|
|
|
195
189
|
readOnly={readOnly}
|
|
196
190
|
value={calendarValue}
|
|
197
191
|
onChange={handleCalendarChange}
|
|
198
|
-
|
|
192
|
+
// 변경: Mantine DatePicker는 valueFormat prop을 지원하지 않으므로 옵션을 그대로 전달한다.
|
|
193
|
+
datePickerProps={datePickerProps}
|
|
199
194
|
header={header}
|
|
200
195
|
footer={footerContent}
|
|
201
196
|
open={resolvedCalendarOpen}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import clsx from "clsx";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
ClipboardEvent,
|
|
6
|
+
DragEvent,
|
|
7
|
+
FormEvent,
|
|
8
|
+
KeyboardEvent,
|
|
9
|
+
MouseEvent,
|
|
10
|
+
} from "react";
|
|
5
11
|
import { forwardRef } from "react";
|
|
6
12
|
import { Calendar } from "../../../calendar";
|
|
7
13
|
import { InputFoundation } from "../foundation";
|
|
@@ -18,7 +24,9 @@ import type { InputCalendarTriggerViewProps } from "../../types";
|
|
|
18
24
|
* @param {string} [props.displayValue] 표시 문자열
|
|
19
25
|
* @param {(event: MouseEvent<Element>) => void} [props.onClick] 클릭 핸들러
|
|
20
26
|
* @param {boolean} [props.disabled] disabled 여부
|
|
27
|
+
* @param {boolean} [props.readOnly] readOnly 여부
|
|
21
28
|
* @param {"primary" | "secondary" | "tertiary" | "table"} [props.priority] trigger input priority
|
|
29
|
+
* @param {"default" | "active" | "focused" | "success" | "error" | "disabled" | "loading"} [props.state] trigger input state
|
|
22
30
|
* @param {string} [props.id] input id
|
|
23
31
|
* @param {string} [props.name] form name
|
|
24
32
|
* @param {UseFormRegisterReturn} [props.register] RHF register
|
|
@@ -34,7 +42,9 @@ const InputDateTrigger = forwardRef<
|
|
|
34
42
|
displayValue,
|
|
35
43
|
onClick,
|
|
36
44
|
disabled,
|
|
45
|
+
readOnly,
|
|
37
46
|
priority,
|
|
47
|
+
state,
|
|
38
48
|
id,
|
|
39
49
|
name,
|
|
40
50
|
register,
|
|
@@ -53,6 +63,55 @@ const InputDateTrigger = forwardRef<
|
|
|
53
63
|
}
|
|
54
64
|
onClick?.(event as never);
|
|
55
65
|
};
|
|
66
|
+
// 변경: Date trigger는 기본적으로 직접 타이핑을 막고, 달력 선택을 단일 입력 경로로 유지한다.
|
|
67
|
+
const shouldBlockTyping = !disabled && !readOnly;
|
|
68
|
+
|
|
69
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
70
|
+
if (!shouldBlockTyping) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const navigableKeys = new Set([
|
|
75
|
+
"Tab",
|
|
76
|
+
"Shift",
|
|
77
|
+
"Control",
|
|
78
|
+
"Alt",
|
|
79
|
+
"Meta",
|
|
80
|
+
"Escape",
|
|
81
|
+
"ArrowLeft",
|
|
82
|
+
"ArrowRight",
|
|
83
|
+
"ArrowUp",
|
|
84
|
+
"ArrowDown",
|
|
85
|
+
"Home",
|
|
86
|
+
"End",
|
|
87
|
+
"PageUp",
|
|
88
|
+
"PageDown",
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
if (navigableKeys.has(event.key)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handleBeforeInput = (event: FormEvent<HTMLInputElement>) => {
|
|
99
|
+
if (shouldBlockTyping) {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handlePaste = (event: ClipboardEvent<HTMLInputElement>) => {
|
|
105
|
+
if (shouldBlockTyping) {
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleDrop = (event: DragEvent<HTMLInputElement>) => {
|
|
111
|
+
if (shouldBlockTyping) {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
56
115
|
|
|
57
116
|
return (
|
|
58
117
|
<InputFoundation.Base
|
|
@@ -60,17 +119,23 @@ const InputDateTrigger = forwardRef<
|
|
|
60
119
|
ref={ref}
|
|
61
120
|
value={displayValue}
|
|
62
121
|
// PopOver.Trigger(asChild)가 주입한 `type="button"`으로 placeholder가 사라지는 문제를 막기 위해
|
|
63
|
-
// Date trigger는 항상
|
|
122
|
+
// Date trigger는 항상 text type을 유지한다.
|
|
64
123
|
type="text"
|
|
65
|
-
readOnly
|
|
124
|
+
readOnly={readOnly}
|
|
66
125
|
disabled={disabled}
|
|
126
|
+
inputMode={shouldBlockTyping ? "none" : undefined}
|
|
67
127
|
id={id}
|
|
68
128
|
name={name ?? register?.name}
|
|
69
129
|
register={register}
|
|
70
130
|
priority={priority}
|
|
131
|
+
state={state}
|
|
71
132
|
placeholder={placeholder}
|
|
72
133
|
className={clsx("input-date-trigger-input", className)}
|
|
73
134
|
onClick={handleInputClick}
|
|
135
|
+
onKeyDown={handleKeyDown}
|
|
136
|
+
onBeforeInput={handleBeforeInput}
|
|
137
|
+
onPaste={handlePaste}
|
|
138
|
+
onDrop={handleDrop}
|
|
74
139
|
// table priority는 icon을 왼쪽 슬롯에 배치한다.
|
|
75
140
|
left={
|
|
76
141
|
priority === "table" ? (
|
|
@@ -30,9 +30,10 @@ import InputBaseUtil from "./Utility";
|
|
|
30
30
|
* @component
|
|
31
31
|
* @param {InputProps} props Input 컴포넌트 공통 props
|
|
32
32
|
* @param {"primary" | "secondary" | "tertiary" | "table"} [props.priority="primary"] 디자인 토큰 우선순위
|
|
33
|
-
* @param {"small" | "medium" | "large"} [props.size="medium"] 높이/타이포 세트
|
|
34
|
-
* @param {"default" | "active" | "focused" | "success" | "error" | "disabled" | "loading"} [props.state="default"] 시각 상태
|
|
33
|
+
* @param {"small" | "medium" | "large"} [props.size="medium"] 높이/타이포 세트(tertiary는 small 고정)
|
|
34
|
+
* @param {"default" | "active" | "focused" | "success" | "error" | "disabled" | "loading"} [props.state="default"] 시각 상태(tertiary는 disabled 외 feedback color 미적용)
|
|
35
35
|
* @param {boolean} [props.block=false] true면 width 100% (`priority="table"`은 width 미지정 시 full 기본)
|
|
36
|
+
* @param {React.ReactNode} [props.inlineLabel] tertiary border 내부 상단 라벨
|
|
36
37
|
* @param {FormFieldWidth} [props.width] width preset
|
|
37
38
|
* @param {React.ReactNode} [props.left] 입력 왼쪽 슬롯(아이콘/텍스트)
|
|
38
39
|
* @param {React.ReactNode} [props.right] 입력 오른쪽 슬롯
|
|
@@ -63,6 +64,7 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
|
|
|
63
64
|
state: stateProp = "default",
|
|
64
65
|
block = false,
|
|
65
66
|
width,
|
|
67
|
+
inlineLabel,
|
|
66
68
|
left,
|
|
67
69
|
right,
|
|
68
70
|
clear,
|
|
@@ -91,6 +93,8 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
|
|
|
91
93
|
const isTablePriority = priority === "table";
|
|
92
94
|
const resolvedBlock = block || (isTablePriority && width === undefined);
|
|
93
95
|
|
|
96
|
+
// 변경: tertiary는 디자인 계약상 small 단일 사이즈만 사용한다.
|
|
97
|
+
const resolvedSize = priority === "tertiary" ? "small" : size;
|
|
94
98
|
const generatedId = useId();
|
|
95
99
|
const registerRef = register?.ref;
|
|
96
100
|
const registerOnChange = register?.onChange;
|
|
@@ -117,7 +121,15 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
|
|
|
117
121
|
const currentState = disabled ? "disabled" : stateProp;
|
|
118
122
|
const isDisabled =
|
|
119
123
|
currentState === "disabled" || currentState === "loading";
|
|
120
|
-
|
|
124
|
+
// 변경: tertiary는 status feedback(success/error/active/focused) 컬러를 적용하지 않는다.
|
|
125
|
+
const visualState =
|
|
126
|
+
priority === "tertiary"
|
|
127
|
+
? isDisabled
|
|
128
|
+
? "disabled"
|
|
129
|
+
: "default"
|
|
130
|
+
: !isDisabled && isFocused
|
|
131
|
+
? "active"
|
|
132
|
+
: currentState;
|
|
121
133
|
|
|
122
134
|
const setInputRef = useCallback(
|
|
123
135
|
(node: HTMLInputElement | null) => {
|
|
@@ -201,13 +213,13 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
|
|
|
201
213
|
className={clsx(
|
|
202
214
|
"input",
|
|
203
215
|
`input-priority-${priority}`,
|
|
204
|
-
`input-size-${
|
|
216
|
+
`input-size-${resolvedSize}`,
|
|
205
217
|
`input-state-${visualState}`,
|
|
206
218
|
resolvedBlock && "input-block",
|
|
207
219
|
className,
|
|
208
220
|
)}
|
|
209
221
|
data-priority={priority}
|
|
210
|
-
data-size={
|
|
222
|
+
data-size={resolvedSize}
|
|
211
223
|
data-state={visualState}
|
|
212
224
|
data-readonly={isReadOnly ? "true" : undefined}
|
|
213
225
|
data-block={resolvedBlock ? "true" : undefined}
|
|
@@ -219,7 +231,7 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
|
|
|
219
231
|
className={clsx(
|
|
220
232
|
"input-box",
|
|
221
233
|
`input-box-priority-${priority}`,
|
|
222
|
-
`input-box-size-${
|
|
234
|
+
`input-box-size-${resolvedSize}`,
|
|
223
235
|
`input-box-state-${visualState}`,
|
|
224
236
|
resolvedBlock && "input-box-block",
|
|
225
237
|
boxClassNameProp,
|
|
@@ -230,7 +242,7 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
|
|
|
230
242
|
className="input-field"
|
|
231
243
|
data-state={visualState}
|
|
232
244
|
data-priority={priority}
|
|
233
|
-
data-size={
|
|
245
|
+
data-size={resolvedSize}
|
|
234
246
|
data-readonly={isReadOnly ? "true" : undefined}
|
|
235
247
|
data-block={resolvedBlock ? "true" : undefined}
|
|
236
248
|
>
|
|
@@ -239,6 +251,10 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
|
|
|
239
251
|
{left && (
|
|
240
252
|
<InputBaseSideSlot type="left">{left}</InputBaseSideSlot>
|
|
241
253
|
)}
|
|
254
|
+
{priority === "tertiary" && inlineLabel ? (
|
|
255
|
+
// 변경: tertiary 라벨은 FormField header가 아니라 input border 내부에 노출한다.
|
|
256
|
+
<span className="input-inline-label">{inlineLabel}</span>
|
|
257
|
+
) : null}
|
|
242
258
|
<input
|
|
243
259
|
{...restProps}
|
|
244
260
|
id={id ?? generatedId}
|
|
@@ -44,8 +44,9 @@ export default function InputBaseUtil({
|
|
|
44
44
|
: state === "error"
|
|
45
45
|
? (error ?? baseStatusIcon)
|
|
46
46
|
: null;
|
|
47
|
-
// table priority는
|
|
48
|
-
const resolvedStatusIcon =
|
|
47
|
+
// 변경: table/tertiary priority는 상태 아이콘을 렌더하지 않는다.
|
|
48
|
+
const resolvedStatusIcon =
|
|
49
|
+
priority === "table" || priority === "tertiary" ? null : statusIcon;
|
|
49
50
|
const clearIconNode = clear ?? InputStatusIcon.reset;
|
|
50
51
|
const showClearIcon = Boolean(
|
|
51
52
|
clearIconNode &&
|
|
@@ -201,6 +201,12 @@
|
|
|
201
201
|
border-width: var(--input-border-width-default);
|
|
202
202
|
background-color: var(--input-surface-disabled-color);
|
|
203
203
|
}
|
|
204
|
+
|
|
205
|
+
&[data-state="error"][data-readonly="true"] {
|
|
206
|
+
// 변경: calendar trigger처럼 readOnly input에서도 error state를 우선 노출한다.
|
|
207
|
+
border-color: var(--input-border-error-color);
|
|
208
|
+
border-width: var(--input-border-width-emphasis);
|
|
209
|
+
}
|
|
204
210
|
}
|
|
205
211
|
}
|
|
206
212
|
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
--input-text-disabled-color: var(--color-label-disabled);
|
|
52
52
|
--input-placeholder-color: var(--color-label-alternative);
|
|
53
53
|
--input-placeholder-disabled-color: var(--color-label-disabled);
|
|
54
|
-
|
|
54
|
+
/* 변경: readonly 입력은 placeholder를 숨겨 value 텍스트 대비를 고정한다. */
|
|
55
|
+
--input-placeholder-readonly-color: transparent;
|
|
55
56
|
--input-font-size: var(--font-body-medium-size);
|
|
56
57
|
--input-line-height: var(--font-body-medium-line-height);
|
|
57
58
|
--input-font-weight: 400;
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
CalendarOnChange,
|
|
8
8
|
CalendarValue,
|
|
9
9
|
} from "../../calendar";
|
|
10
|
-
import type { InputPriority } from "./foundation";
|
|
10
|
+
import type { InputPriority, InputState } from "./foundation";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Calendar trigger(Input) 영역에서 필요한 속성 묶음.
|
|
@@ -17,8 +17,10 @@ import type { InputPriority } from "./foundation";
|
|
|
17
17
|
* @property {UseFormRegisterReturn} [register] react-hook-form register 결과
|
|
18
18
|
* @property {(event: MouseEvent<Element>) => void} [onClick] trigger 클릭 핸들러
|
|
19
19
|
* @property {boolean} [disabled] disabled 여부
|
|
20
|
+
* @property {boolean} [readOnly] readOnly 여부
|
|
20
21
|
* @property {string} [placeholder] trigger 내부 placeholder
|
|
21
22
|
* @property {InputPriority} [priority] trigger input priority
|
|
23
|
+
* @property {InputState} [state] trigger input state
|
|
22
24
|
*/
|
|
23
25
|
export interface InputCalendarTriggerProps {
|
|
24
26
|
/**
|
|
@@ -41,6 +43,10 @@ export interface InputCalendarTriggerProps {
|
|
|
41
43
|
* disabled 여부
|
|
42
44
|
*/
|
|
43
45
|
disabled?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* readOnly 여부
|
|
48
|
+
*/
|
|
49
|
+
readOnly?: boolean;
|
|
44
50
|
/**
|
|
45
51
|
* trigger 내부 placeholder
|
|
46
52
|
*/
|
|
@@ -50,6 +56,10 @@ export interface InputCalendarTriggerProps {
|
|
|
50
56
|
* - table일 때 Trigger는 left icon 배치를 사용한다.
|
|
51
57
|
*/
|
|
52
58
|
priority?: InputPriority;
|
|
59
|
+
/**
|
|
60
|
+
* trigger input state
|
|
61
|
+
*/
|
|
62
|
+
state?: InputState;
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
/**
|
|
@@ -75,8 +85,10 @@ export interface InputCalendarTriggerViewProps extends InputCalendarTriggerProps
|
|
|
75
85
|
* @property {string} [displayValue] 트리거 표시 문자열
|
|
76
86
|
* @property {string} [placeholder] 트리거 placeholder
|
|
77
87
|
* @property {boolean} [disabled] disabled 여부
|
|
88
|
+
* @property {boolean} [readOnly] readOnly 여부
|
|
78
89
|
* @property {(event: MouseEvent<Element>) => void} [onClick] 트리거 클릭 핸들러
|
|
79
90
|
* @property {InputPriority} [priority] trigger input priority
|
|
91
|
+
* @property {InputState} [state] trigger input state
|
|
80
92
|
*/
|
|
81
93
|
export interface InputCalendarTriggerRenderProps {
|
|
82
94
|
/**
|
|
@@ -99,6 +111,10 @@ export interface InputCalendarTriggerRenderProps {
|
|
|
99
111
|
* disabled 여부
|
|
100
112
|
*/
|
|
101
113
|
disabled?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* readOnly 여부
|
|
116
|
+
*/
|
|
117
|
+
readOnly?: boolean;
|
|
102
118
|
/**
|
|
103
119
|
* 트리거 클릭 핸들러
|
|
104
120
|
*/
|
|
@@ -108,6 +124,10 @@ export interface InputCalendarTriggerRenderProps {
|
|
|
108
124
|
* - table일 때 Trigger는 left icon 배치를 사용한다.
|
|
109
125
|
*/
|
|
110
126
|
priority?: InputPriority;
|
|
127
|
+
/**
|
|
128
|
+
* trigger input state
|
|
129
|
+
*/
|
|
130
|
+
state?: InputState;
|
|
111
131
|
}
|
|
112
132
|
|
|
113
133
|
/**
|
|
@@ -122,6 +142,7 @@ export interface InputCalendarTriggerRenderProps {
|
|
|
122
142
|
* @property {boolean} [disabled] disabled 여부
|
|
123
143
|
* @property {CalendarDatePickerProps} [datePickerProps] Mantine DatePicker 직접 옵션
|
|
124
144
|
* @property {InputPriority} [priority] trigger input priority
|
|
145
|
+
* @property {InputState} [state] trigger input state
|
|
125
146
|
* @property {ReactNode} [header] 커스텀 header 콘텐츠
|
|
126
147
|
* @property {ReactNode} [footer] 커스텀 footer 콘텐츠
|
|
127
148
|
* @property {unknown} [timePicker] TimePicker 확장용 예약 슬롯(현재 미구현)
|
|
@@ -93,6 +93,7 @@ export interface InputIcon {
|
|
|
93
93
|
* @property {InputSize} [size] 스타일 사이즈 타입
|
|
94
94
|
* @property {InputState} [state] input 상태
|
|
95
95
|
* @property {boolean} [block] width: 100% 여부
|
|
96
|
+
* @property {ReactNode} [inlineLabel] tertiary 내부 라벨
|
|
96
97
|
* @property {string} [inputClassName]
|
|
97
98
|
* @property {string} [boxClassName]
|
|
98
99
|
* @property {UseFormRegisterReturn} [register]
|
|
@@ -121,6 +122,10 @@ export interface InputProps extends Omit<NativeInputProps, "size">, InputIcon {
|
|
|
121
122
|
* true면 width:100%
|
|
122
123
|
*/
|
|
123
124
|
block?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* tertiary priority에서 border 내부 상단에 배치할 라벨 텍스트/노드
|
|
127
|
+
*/
|
|
128
|
+
inlineLabel?: ReactNode;
|
|
124
129
|
/**
|
|
125
130
|
* 실제 `<input>` className
|
|
126
131
|
*/
|
|
@@ -272,15 +272,30 @@
|
|
|
272
272
|
color: var(--select-table-color-placeholder);
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
|
|
275
|
+
// 변경: disabled/readonly placeholder 색상을 상태 토큰으로 분리한다.
|
|
276
|
+
.select-button[data-state="disabled"] & {
|
|
277
|
+
color: var(--select-primary-color-placeholder-disabled);
|
|
278
|
+
}
|
|
279
|
+
|
|
276
280
|
.select-button[data-readonly="true"] & {
|
|
277
|
-
color: var(--select-primary-color-
|
|
281
|
+
color: var(--select-primary-color-placeholder-readonly);
|
|
278
282
|
}
|
|
279
283
|
|
|
280
|
-
.select-button[data-priority="secondary"][data-state="disabled"]
|
|
281
|
-
.select-button[data-priority="secondary"][data-readonly="true"] & {
|
|
284
|
+
.select-button[data-priority="secondary"][data-state="disabled"] & {
|
|
282
285
|
color: var(--select-secondary-color-placeholder-disabled);
|
|
283
286
|
}
|
|
287
|
+
|
|
288
|
+
.select-button[data-priority="secondary"][data-readonly="true"] & {
|
|
289
|
+
color: var(--select-secondary-color-placeholder-readonly);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.select-button[data-priority="table"][data-state="disabled"] & {
|
|
293
|
+
color: var(--select-table-color-placeholder-disabled);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.select-button[data-priority="table"][data-readonly="true"] & {
|
|
297
|
+
color: var(--select-table-color-placeholder-readonly);
|
|
298
|
+
}
|
|
284
299
|
}
|
|
285
300
|
|
|
286
301
|
.select-button[data-size="small"] .select-label {
|
|
@@ -42,6 +42,12 @@
|
|
|
42
42
|
--select-table-color-text: var(--input-text-color);
|
|
43
43
|
--select-table-color-text-readonly: var(--select-primary-color-text-readonly);
|
|
44
44
|
--select-table-color-placeholder: var(--input-placeholder-color);
|
|
45
|
+
--select-table-color-placeholder-disabled: var(
|
|
46
|
+
--select-primary-color-placeholder-disabled
|
|
47
|
+
);
|
|
48
|
+
--select-table-color-placeholder-readonly: var(
|
|
49
|
+
--select-primary-color-placeholder-readonly
|
|
50
|
+
);
|
|
45
51
|
--select-table-icon-color-default: var(--select-icon-color-default);
|
|
46
52
|
--select-table-icon-color-focused: var(--select-icon-color-focused);
|
|
47
53
|
--select-table-icon-color-disabled: var(--select-icon-color-disabled);
|
|
@@ -78,6 +84,11 @@
|
|
|
78
84
|
--select-icon-size-large: 2rem;
|
|
79
85
|
|
|
80
86
|
--select-primary-color-placeholder: var(--input-placeholder-color);
|
|
87
|
+
/* 변경: placeholder disabled/readonly 토큰을 분리해 상태별 제어 지점을 고정한다. */
|
|
88
|
+
--select-primary-color-placeholder-disabled: var(--color-label-disabled);
|
|
89
|
+
--select-primary-color-placeholder-readonly: var(
|
|
90
|
+
--select-primary-color-placeholder-disabled
|
|
91
|
+
);
|
|
81
92
|
--select-primary-color-surface: var(--input-surface-color);
|
|
82
93
|
--select-primary-color-text: var(--color-label-alternative);
|
|
83
94
|
--select-primary-color-text-focused: var(--color-label-strong);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
1
2
|
import { forwardRef } from "react";
|
|
2
3
|
import type { TableContainerProps } from "../types";
|
|
3
4
|
import TableBody from "./foundation/Body";
|
|
@@ -11,8 +12,14 @@ import TableRow from "./foundation/Row";
|
|
|
11
12
|
* Table Preset; 기본 Container 조합 컴포넌트
|
|
12
13
|
* @component
|
|
13
14
|
* @param {TableContainerProps} props
|
|
15
|
+
* @desc
|
|
16
|
+
* - columns 기반 colgroup/head 렌더링을 preset으로 제공한다.
|
|
17
|
+
* - `scrollable`이 true면 optional scroll wrapper를 추가해 소비처에서 스크롤 책임을 분리할 수 있다.
|
|
14
18
|
* @param {TableColumnData[]} [props.columns] colgroup/head 자동 렌더링 column 목록
|
|
15
19
|
* @param {boolean} [props.isCustomBody=false] true면 body wrapper 없이 children 직접 렌더링
|
|
20
|
+
* @param {boolean} [props.scrollable=false] true면 외부 스크롤 래퍼를 추가한다.
|
|
21
|
+
* @param {"x" | "y" | "both"} [props.scrollAxis="x"] scrollable일 때 스크롤 축
|
|
22
|
+
* @param {string} [props.scrollClassName] 스크롤 래퍼 className
|
|
16
23
|
* @param {React.ReactNode} [props.footer] footer 노드. `<Table.Foot />`를 직접 전달하는 방식을 권장
|
|
17
24
|
* @param {React.ReactNode} [props.children] table body 콘텐츠
|
|
18
25
|
* @example
|
|
@@ -32,14 +39,32 @@ import TableRow from "./foundation/Row";
|
|
|
32
39
|
* </Table.Container>
|
|
33
40
|
*/
|
|
34
41
|
const TableContainer = forwardRef<HTMLTableElement, TableContainerProps>(
|
|
35
|
-
(
|
|
42
|
+
(
|
|
43
|
+
{
|
|
44
|
+
columns,
|
|
45
|
+
isCustomBody = false,
|
|
46
|
+
scrollable = false,
|
|
47
|
+
scrollAxis = "x",
|
|
48
|
+
scrollClassName,
|
|
49
|
+
footer,
|
|
50
|
+
children,
|
|
51
|
+
...tableProps
|
|
52
|
+
},
|
|
53
|
+
ref,
|
|
54
|
+
) => {
|
|
36
55
|
// 변경: optional columns를 안전하게 순회하기 위한 기본 배열을 고정한다.
|
|
37
56
|
const resolvedColumns = columns ?? [];
|
|
38
57
|
// 변경: columns 존재 여부만 단일 플래그로 관리해 렌더 분기 가독성을 유지한다.
|
|
39
58
|
const hasColumns = resolvedColumns.length > 0;
|
|
59
|
+
// 변경: footer prop 주입 여부를 table data-attr로 노출해 grid radius 규칙 분기에 사용한다.
|
|
60
|
+
const hasFooter = typeof footer !== "undefined";
|
|
40
61
|
|
|
41
|
-
|
|
42
|
-
<TableRoot
|
|
62
|
+
const tableNode = (
|
|
63
|
+
<TableRoot
|
|
64
|
+
{...tableProps}
|
|
65
|
+
ref={ref}
|
|
66
|
+
data-has-footer={hasFooter ? "true" : undefined}
|
|
67
|
+
>
|
|
43
68
|
{hasColumns && (
|
|
44
69
|
<colgroup>
|
|
45
70
|
{resolvedColumns.map(({ key, width, dataKey, className }) => (
|
|
@@ -84,6 +109,19 @@ const TableContainer = forwardRef<HTMLTableElement, TableContainerProps>(
|
|
|
84
109
|
{footer}
|
|
85
110
|
</TableRoot>
|
|
86
111
|
);
|
|
112
|
+
|
|
113
|
+
if (!scrollable) {
|
|
114
|
+
return tableNode;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div
|
|
119
|
+
className={clsx("table-scroll-wrapper", scrollClassName)}
|
|
120
|
+
data-scroll-axis={scrollAxis}
|
|
121
|
+
>
|
|
122
|
+
{tableNode}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
87
125
|
},
|
|
88
126
|
);
|
|
89
127
|
|
|
@@ -6,8 +6,15 @@ import type { TableThProps } from "../../types";
|
|
|
6
6
|
* Table Foundation; Th 마크업 컴포넌트
|
|
7
7
|
* @component
|
|
8
8
|
* @param {TableThProps} props
|
|
9
|
+
* @desc
|
|
10
|
+
* - sticky/레이어 우선순위는 `styles/foundation.scss`에서 section/column 기준으로 제어한다.
|
|
9
11
|
* @param {string} [props.className] th className
|
|
12
|
+
* @param {React.CSSProperties} [props.style] th inline style
|
|
10
13
|
* @param {React.ReactNode} [props.children] 셀 콘텐츠
|
|
14
|
+
* @example
|
|
15
|
+
* <Table.Th scope="row">
|
|
16
|
+
* <Table.Cell section="body">항목</Table.Cell>
|
|
17
|
+
* </Table.Th>
|
|
11
18
|
*/
|
|
12
19
|
const TableTh = forwardRef<HTMLTableCellElement, TableThProps>(
|
|
13
20
|
({ className, children, scope, style, ...cellProps }, ref) => {
|
|
@@ -18,8 +25,8 @@ const TableTh = forwardRef<HTMLTableCellElement, TableThProps>(
|
|
|
18
25
|
className={clsx("table-native-cell", "table-th", className)}
|
|
19
26
|
// th 기본 scope는 col로 유지한다.
|
|
20
27
|
scope={scope ?? "col"}
|
|
21
|
-
// 변경:
|
|
22
|
-
style={
|
|
28
|
+
// 변경: sticky/left/z-index는 CSS 규칙으로 위임해 인라인 우선순위 충돌을 방지한다.
|
|
29
|
+
style={style}
|
|
23
30
|
>
|
|
24
31
|
{/* 변경: 태그 셀렉터 의존을 피하기 위해 className 기반 텍스트 노드를 사용한다. */}
|
|
25
32
|
{typeof children === "string" || typeof children === "number" ? (
|
|
@@ -49,19 +49,26 @@
|
|
|
49
49
|
font-weight: 600;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// 변경: line(list) 타입에서 rowspan 셀은 기본적으로 우측 경계선을 표시한다.
|
|
53
|
+
.table.table-container[data-layout="line"] .table-native-cell[rowspan] {
|
|
54
|
+
border-right: 1px solid var(--table-border-color);
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
// 변경: grid 타입은 모든 셀 border와 헤더 배경을 기본 제공한다.
|
|
53
58
|
.table.table-container[data-layout="grid"] .table-native-cell.table-th,
|
|
54
59
|
.table.table-container[data-layout="grid"] .table-native-cell.table-td {
|
|
55
60
|
// table cell 내부 field full-bleed 계산을 위해 padding 값을 변수로 노출한다.
|
|
56
61
|
--table-cell-padding-inline: var(--table-grid-cell-padding-inline);
|
|
57
62
|
--table-cell-padding-block: var(--table-grid-cell-padding-block);
|
|
58
|
-
border: 1px solid var(--table-border-color);
|
|
59
63
|
height: var(--table-grid-cell-height);
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
.table.table-container[data-layout="grid"] {
|
|
63
67
|
border-radius: var(--table-grid-border-radius);
|
|
64
|
-
overflow
|
|
68
|
+
// 변경: Tooltip/Popover/Dropdown clipping을 막기 위해 grid container overflow를 제거한다.
|
|
69
|
+
overflow: visible;
|
|
70
|
+
// 변경: sticky cell layering 기준점을 분리해 z-index 충돌을 줄인다.
|
|
71
|
+
isolation: isolate;
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
.table.table-container[data-layout="grid"]
|
|
@@ -77,6 +84,13 @@
|
|
|
77
84
|
background-color: var(--table-grid-row-header-background-color);
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
// 변경: grid foot의 th도 body row-header와 동일한 배경으로 유지해 sticky 겹침 시 시각 이탈을 방지한다.
|
|
88
|
+
.table.table-container[data-layout="grid"]
|
|
89
|
+
.table-foot
|
|
90
|
+
.table-native-cell.table-th {
|
|
91
|
+
background-color: var(--table-grid-row-header-background-color);
|
|
92
|
+
}
|
|
93
|
+
|
|
80
94
|
// 변경: grid 타입 highlighted row는 row-header(th)보다 우선해 th/td 모두 blue tint를 적용한다.
|
|
81
95
|
.table.table-container[data-layout="grid"]
|
|
82
96
|
.table-row[data-highlighted="true"]
|
|
@@ -87,6 +101,193 @@
|
|
|
87
101
|
background-color: var(--table-grid-row-highlight-background-color);
|
|
88
102
|
}
|
|
89
103
|
|
|
104
|
+
// 변경: 인라인 스타일 대신 각 row의 첫 번째 th만 sticky로 고정한다.
|
|
105
|
+
.table.table-container .table-row > .table-native-cell.table-th:first-child {
|
|
106
|
+
position: sticky;
|
|
107
|
+
left: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 변경: sticky된 첫 번째 th가 td 위에서 안정적으로 보이도록 섹션별 z-index를 50단위로 분리한다.
|
|
111
|
+
.table.table-container
|
|
112
|
+
.table-head
|
|
113
|
+
.table-row
|
|
114
|
+
> .table-native-cell.table-th:first-child {
|
|
115
|
+
z-index: 150;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.table.table-container
|
|
119
|
+
.table-body
|
|
120
|
+
.table-row
|
|
121
|
+
> .table-native-cell.table-th:first-child {
|
|
122
|
+
z-index: 100;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.table.table-container
|
|
126
|
+
.table-foot
|
|
127
|
+
.table-row
|
|
128
|
+
> .table-native-cell.table-th:first-child {
|
|
129
|
+
z-index: 50;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 변경: scroll 시 index 0 컬럼의 첫 header th가 항상 최상단에 보이도록 z-index를 상향한다.
|
|
133
|
+
.table.table-container
|
|
134
|
+
.table-head
|
|
135
|
+
.table-row:first-child
|
|
136
|
+
> .table-native-cell.table-th:first-child {
|
|
137
|
+
z-index: 200;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 변경: sticky th와 겹치는 td 텍스트가 앞서 보이지 않도록 td를 기본 레이어로 고정한다.
|
|
141
|
+
.table.table-container .table-native-cell.table-td {
|
|
142
|
+
position: relative;
|
|
143
|
+
z-index: 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 변경: grid 코너 라운드는 cell(th/td) 자체에 직접 적용해 sticky/scroll 상황에서도 모서리 형상을 유지한다.
|
|
147
|
+
.table.table-container[data-layout="grid"]
|
|
148
|
+
.table-head
|
|
149
|
+
.table-row:first-child
|
|
150
|
+
> .table-native-cell:first-child {
|
|
151
|
+
border-top-left-radius: var(--table-grid-border-radius);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.table.table-container[data-layout="grid"]
|
|
155
|
+
.table-head
|
|
156
|
+
.table-row:first-child
|
|
157
|
+
> .table-native-cell:last-child {
|
|
158
|
+
border-top-right-radius: var(--table-grid-border-radius);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.table.table-container[data-layout="grid"]
|
|
162
|
+
.table-foot
|
|
163
|
+
.table-row:last-child
|
|
164
|
+
> .table-native-cell:first-child,
|
|
165
|
+
.table.table-container[data-layout="grid"]:not([data-has-footer="true"])
|
|
166
|
+
.table-body:last-of-type
|
|
167
|
+
.table-row:last-child
|
|
168
|
+
> .table-native-cell:first-child {
|
|
169
|
+
border-bottom-left-radius: var(--table-grid-border-radius);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.table.table-container[data-layout="grid"]
|
|
173
|
+
.table-foot
|
|
174
|
+
.table-row:last-child
|
|
175
|
+
> .table-native-cell:last-child,
|
|
176
|
+
.table.table-container[data-layout="grid"]:not([data-has-footer="true"])
|
|
177
|
+
.table-body:last-of-type
|
|
178
|
+
.table-row:last-child
|
|
179
|
+
> .table-native-cell:last-child {
|
|
180
|
+
border-bottom-right-radius: var(--table-grid-border-radius);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 변경: grid border를 native cell이 아닌 cell-content에 그려 sticky 겹침/코너 clipping을 안정화한다.
|
|
184
|
+
.table.table-container[data-layout="grid"] .table-cell-content {
|
|
185
|
+
border-right: 1px solid var(--table-border-color);
|
|
186
|
+
border-bottom: 1px solid var(--table-border-color);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 변경: border-top은 thead 내부 첫 row의 cell에만 적용한다.
|
|
190
|
+
.table.table-container[data-layout="grid"]
|
|
191
|
+
.table-head
|
|
192
|
+
.table-row:first-child
|
|
193
|
+
> .table-native-cell
|
|
194
|
+
> .table-cell-content {
|
|
195
|
+
border-top: 1px solid var(--table-border-color);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.table.table-container[data-layout="grid"]
|
|
199
|
+
.table-row
|
|
200
|
+
> .table-native-cell:first-child
|
|
201
|
+
> .table-cell-content {
|
|
202
|
+
border-left: 1px solid var(--table-border-color);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 변경: grid 병합(rowspan) 셀은 우측 border를 제거하고, 인접 다음 셀의 좌측 border로 경계를 표현한다.
|
|
206
|
+
.table.table-container[data-layout="grid"]
|
|
207
|
+
.table-row
|
|
208
|
+
> .table-native-cell[rowspan]
|
|
209
|
+
> .table-cell-content {
|
|
210
|
+
border-right: 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.table.table-container[data-layout="grid"]
|
|
214
|
+
.table-row
|
|
215
|
+
> .table-native-cell[rowspan]
|
|
216
|
+
+ .table-native-cell
|
|
217
|
+
> .table-cell-content {
|
|
218
|
+
border-left: 1px solid var(--table-border-color);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 변경: 요청사항(and)에 맞춰 코너 셀의 th/td와 내부 cell-content 모두 동일 반경을 공유한다.
|
|
222
|
+
|
|
223
|
+
.table.table-container[data-layout="grid"]
|
|
224
|
+
.table-row:first-child
|
|
225
|
+
> .table-native-cell:first-child
|
|
226
|
+
.table-cell-content {
|
|
227
|
+
border-top-left-radius: inherit;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.table.table-container[data-layout="grid"]
|
|
231
|
+
.table-row:first-child
|
|
232
|
+
> .table-native-cell:last-child
|
|
233
|
+
.table-cell-content {
|
|
234
|
+
border-top-right-radius: inherit;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.table.table-container[data-layout="grid"]
|
|
238
|
+
.table-foot
|
|
239
|
+
.table-row:last-child
|
|
240
|
+
> .table-native-cell:first-child
|
|
241
|
+
.table-cell-content,
|
|
242
|
+
.table.table-container[data-layout="grid"]:not([data-has-footer="true"])
|
|
243
|
+
.table-body:last-of-type
|
|
244
|
+
.table-row:last-child
|
|
245
|
+
> .table-native-cell:first-child
|
|
246
|
+
.table-cell-content {
|
|
247
|
+
border-bottom-left-radius: inherit;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.table.table-container[data-layout="grid"]
|
|
251
|
+
.table-foot
|
|
252
|
+
.table-row:last-child
|
|
253
|
+
> .table-native-cell:last-child
|
|
254
|
+
.table-cell-content,
|
|
255
|
+
.table.table-container[data-layout="grid"]:not([data-has-footer="true"])
|
|
256
|
+
.table-body:last-of-type
|
|
257
|
+
.table-row:last-child
|
|
258
|
+
> .table-native-cell:last-child
|
|
259
|
+
.table-cell-content {
|
|
260
|
+
border-bottom-right-radius: inherit;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 변경: optional scroll 래퍼를 통해 필요한 화면에서만 스크롤 레이어를 분리한다.
|
|
264
|
+
.table-scroll-wrapper {
|
|
265
|
+
width: 100%;
|
|
266
|
+
border-radius: var(--table-grid-border-radius);
|
|
267
|
+
// 변경: 스크롤 경계에서 상위 컨테이너로 전달되는 bounce/chain을 최대한 차단한다.
|
|
268
|
+
overscroll-behavior: none;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.table-scroll-wrapper[data-scroll-axis="x"] {
|
|
272
|
+
overflow-x: auto;
|
|
273
|
+
overflow-y: visible;
|
|
274
|
+
overscroll-behavior-x: none;
|
|
275
|
+
-webkit-overflow-scrolling: auto;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.table-scroll-wrapper[data-scroll-axis="y"] {
|
|
279
|
+
overflow-x: visible;
|
|
280
|
+
overflow-y: auto;
|
|
281
|
+
overscroll-behavior-y: none;
|
|
282
|
+
-webkit-overflow-scrolling: auto;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.table-scroll-wrapper[data-scroll-axis="both"] {
|
|
286
|
+
overflow: auto;
|
|
287
|
+
overscroll-behavior: none;
|
|
288
|
+
-webkit-overflow-scrolling: auto;
|
|
289
|
+
}
|
|
290
|
+
|
|
90
291
|
// 변경: tag selector 대신 className selector로 텍스트 스타일 적용 범위를 고정한다.
|
|
91
292
|
.table-native-cell.table-th .table-native-cell-text {
|
|
92
293
|
color: var(--table-th-text-color);
|
|
@@ -333,6 +534,15 @@
|
|
|
333
534
|
line-height: var(--table-td-text-line-height);
|
|
334
535
|
}
|
|
335
536
|
|
|
537
|
+
// 변경: footer(table-foot) 내부 셀 콘텐츠 타이포를 body와 동일한 td 스케일로 고정한다.
|
|
538
|
+
.table.table-container .table-foot .table-cell-content,
|
|
539
|
+
.table.table-container .table-foot .table-cell-text {
|
|
540
|
+
color: var(--table-td-text-color);
|
|
541
|
+
font-size: var(--table-td-text-size);
|
|
542
|
+
font-weight: var(--table-td-text-weight);
|
|
543
|
+
line-height: var(--table-td-text-line-height);
|
|
544
|
+
}
|
|
545
|
+
|
|
336
546
|
// 변경: Cell(body/foot) 컨텍스트에서는 table priority input/select 텍스트를 15px 축으로 강제한다.
|
|
337
547
|
.table-cell.table-body-cell .radio-label-text,
|
|
338
548
|
.table-cell.table-foot-cell .radio-label-text {
|
|
@@ -31,7 +31,13 @@ export const TABLE_CELL_ALIGN_ITEMS_OPTIONS = [
|
|
|
31
31
|
export const TABLE_CELL_PADDING_OPTIONS = ["default", "none"] as const;
|
|
32
32
|
export const TABLE_SECTIONS = ["head", "body", "foot"] as const;
|
|
33
33
|
export const TABLE_LAYOUT_OPTIONS = ["line", "grid"] as const;
|
|
34
|
+
export const TABLE_SCROLL_AXES = ["x", "y", "both"] as const;
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Table Types; 스크롤 축 타입
|
|
38
|
+
* @typedef {"x" | "y" | "both"} TableScrollAxis
|
|
39
|
+
*/
|
|
40
|
+
export type TableScrollAxis = (typeof TABLE_SCROLL_AXES)[number];
|
|
35
41
|
/**
|
|
36
42
|
* Table Types; Cell 가로 정렬 값 타입
|
|
37
43
|
* @typedef {"left" | "center" | "right" | "normal" | "start" | "end" | "flex-start" | "flex-end" | "space-between" | "space-around" | "space-evenly" | "stretch"} TableCellAlignX
|
|
@@ -126,6 +132,9 @@ export interface TableColumnData {
|
|
|
126
132
|
* Table Types; Container props
|
|
127
133
|
* @property {TableColumnData[]} [columns] colgroup/head 자동 렌더링용 column 데이터
|
|
128
134
|
* @property {boolean} [isCustomBody] true면 body wrapper 없이 children을 직접 렌더링
|
|
135
|
+
* @property {boolean} [scrollable=false] true면 외부 스크롤 래퍼를 추가한다.
|
|
136
|
+
* @property {"x" | "y" | "both"} [scrollAxis="x"] scrollable일 때 스크롤 축
|
|
137
|
+
* @property {string} [scrollClassName] 스크롤 래퍼 className
|
|
129
138
|
* @property {React.ReactNode} [footer] footer 노드
|
|
130
139
|
* @property {React.ReactNode} [children] body 콘텐츠
|
|
131
140
|
*/
|
|
@@ -138,6 +147,18 @@ export interface TableContainerProps extends TableRootProps {
|
|
|
138
147
|
* true면 body wrapper 없이 children을 직접 렌더링
|
|
139
148
|
*/
|
|
140
149
|
isCustomBody?: boolean;
|
|
150
|
+
/**
|
|
151
|
+
* true면 외부 스크롤 래퍼를 추가한다.
|
|
152
|
+
*/
|
|
153
|
+
scrollable?: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* scrollable일 때 스크롤 축
|
|
156
|
+
*/
|
|
157
|
+
scrollAxis?: TableScrollAxis;
|
|
158
|
+
/**
|
|
159
|
+
* 스크롤 래퍼 className
|
|
160
|
+
*/
|
|
161
|
+
scrollClassName?: string;
|
|
141
162
|
/**
|
|
142
163
|
* footer 노드
|
|
143
164
|
*/
|
|
@@ -187,6 +208,7 @@ export interface TableRowProps extends ComponentPropsWithoutRef<"tr"> {
|
|
|
187
208
|
/**
|
|
188
209
|
* Table Types; Th props
|
|
189
210
|
* @property {string} [className] th className
|
|
211
|
+
* @property {React.CSSProperties} [style] sticky 기본값(position/left/z-index/textAlign) override style
|
|
190
212
|
* @property {React.ReactNode} [children] header cell 콘텐츠
|
|
191
213
|
*/
|
|
192
214
|
export interface TableThProps extends ComponentPropsWithoutRef<"th"> {}
|