@nurihaus/web-design-system 3.6.0 → 3.7.0

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.
@@ -1019,7 +1019,22 @@ import { ModalWithDoubleButton } from '@nurihaus/web-design-system';
1019
1019
 
1020
1020
  **언제 쓴다.** 삭제 확인처럼 제목, 설명, 버튼 두 개로 된 확인 대화상자에 쓴다. `ModalWithDoubleButton.Row`는 버튼을 가로로, `ModalWithDoubleButton.Column`은 세로로 배치하며, `left`와 `right`는 CTAButton props를 그대로 받는다.
1021
1021
 
1022
- **쓰지 않는다.** `open` prop이 없고 렌더되는 동안 항상 보이므로, 표시 여부는 부모가 조건부 렌더링으로 정하고 버튼 `onClick`에서 언마운트한다. `showCloseIcon`의 X 아이콘은 클릭해도 동작이 없으므로 닫기 수단으로 쓰지 않는다. 일반 모달은 Modal 을 쓴다.
1022
+ **열고 닫기.** `open`·`onOpenChange`를 넘기면 부모가 열림을 정한다. 열리면 포커스가 창 안으로 들어가고, Esc·바깥 누름·X 아이콘(`showCloseIcon`)·두 버튼이 `onOpenChange(false)`를 부르며, 닫히면 포커스가 연 요소로 돌아온다. 바텀시트 안의 삭제 확인처럼 키보드로 닿아야 하는 곳은 이 방식으로 쓴다.
1023
+
1024
+ ```tsx
1025
+ <ModalWithDoubleButton.Row
1026
+ open
1027
+ onOpenChange={() => {}}
1028
+ showCloseIcon
1029
+ title="항목을 삭제할까요?"
1030
+ left={{ text: '취소' }}
1031
+ right={{ text: '삭제', onClick: () => {} }}
1032
+ />
1033
+ ```
1034
+
1035
+ `open`을 넘기지 않으면 예전처럼 렌더되는 동안 항상 보이고 포커스를 가두지 않는다. 이때는 부모가 조건부 렌더링으로 정하고 버튼 `onClick`에서 언마운트하며, X 아이콘은 동작이 없다.
1036
+
1037
+ **쓰지 않는다.** 제목·설명·버튼 두 개를 벗어나는 일반 모달은 Modal 을 쓴다.
1023
1038
 
1024
1039
  ```tsx
1025
1040
  <ModalWithDoubleButton.Row
@@ -1034,6 +1049,8 @@ import { ModalWithDoubleButton } from '@nurihaus/web-design-system';
1034
1049
 
1035
1050
  | prop | 타입 | 필수 | 기본값 |
1036
1051
  | --- | --- | --- | --- |
1052
+ | `open` | `boolean` | | |
1053
+ | `onOpenChange` | `((open: boolean) => void)` | | |
1037
1054
  | `title` | `string` | | |
1038
1055
  | `subTitle` | `ReactNode` | | |
1039
1056
  | `titleIcon` | `ReactNode` | | |
@@ -1050,6 +1067,8 @@ import { ModalWithDoubleButton } from '@nurihaus/web-design-system';
1050
1067
 
1051
1068
  | prop | 타입 | 필수 | 기본값 |
1052
1069
  | --- | --- | --- | --- |
1070
+ | `open` | `boolean` | | |
1071
+ | `onOpenChange` | `((open: boolean) => void)` | | |
1053
1072
  | `title` | `string` | | |
1054
1073
  | `subTitle` | `ReactNode` | | |
1055
1074
  | `titleIcon` | `ReactNode` | | |
@@ -10,6 +10,9 @@ export interface ModalRootProps {
10
10
  }
11
11
  export interface ModalContainerProps extends AsChildProps, StyleProps {
12
12
  forceMount?: true;
13
+ /** 열릴 때·닫힐 때 포커스를 옮기기 직전. Radix `Dialog.Content` 에 그대로 넘긴다 */
14
+ onOpenAutoFocus?: (event: Event) => void;
15
+ onCloseAutoFocus?: (event: Event) => void;
13
16
  }
14
17
  export interface ModalTriggerProps extends AsChildProps {
15
18
  className?: string;
@@ -27,6 +30,14 @@ export interface ModalCloseProps extends AsChildProps {
27
30
  className?: string;
28
31
  }
29
32
  export interface ModalWithDoubleButtonProps {
33
+ /**
34
+ * 넘기면 열림을 부모가 정한다. 열리면 포커스가 창 안으로 들어가고 닫히면 연 요소로 돌아오며,
35
+ * Esc·바깥 누름·X 아이콘·두 버튼이 `onOpenChange(false)` 를 부른다.
36
+ * 안 넘기면 예전처럼 렌더되는 동안 항상 보인다 (포커스를 가두지 않는다).
37
+ * `onOpenChange` 를 함께 넘겨야 닫힌다.
38
+ */
39
+ open?: boolean;
40
+ onOpenChange?: (open: boolean) => void;
30
41
  title?: string;
31
42
  subTitle?: string | ReactNode;
32
43
  titleIcon?: ReactNode;
@@ -38,10 +38,9 @@ declare const meta: {
38
38
  animated: true;
39
39
  };
40
40
  decorators: ((Story: import("storybook/internal/csf").PartialStoryFn<import("@storybook/react-webpack5").ReactRenderer, {
41
- slot?: string | undefined | undefined;
41
+ className?: string | undefined | undefined;
42
42
  style?: import("react").CSSProperties | undefined;
43
- title?: string | undefined | undefined;
44
- key?: import("react").Key | null | undefined;
43
+ children?: import("react").ReactNode;
45
44
  defaultChecked?: boolean | undefined | undefined;
46
45
  defaultValue?: string | number | readonly string[] | undefined;
47
46
  suppressContentEditableWarning?: boolean | undefined | undefined;
@@ -49,7 +48,6 @@ declare const meta: {
49
48
  accessKey?: string | undefined | undefined;
50
49
  autoCapitalize?: "off" | "none" | "on" | "sentences" | "words" | "characters" | undefined | (string & {}) | undefined;
51
50
  autoFocus?: boolean | undefined | undefined;
52
- className?: string | undefined | undefined;
53
51
  contentEditable?: (boolean | "true" | "false") | "inherit" | "plaintext-only" | undefined;
54
52
  contextMenu?: string | undefined | undefined;
55
53
  dir?: string | undefined | undefined;
@@ -59,8 +57,10 @@ declare const meta: {
59
57
  id?: string | undefined | undefined;
60
58
  lang?: string | undefined | undefined;
61
59
  nonce?: string | undefined | undefined;
60
+ slot?: string | undefined | undefined;
62
61
  spellCheck?: (boolean | "true" | "false") | undefined;
63
62
  tabIndex?: number | undefined | undefined;
63
+ title?: string | undefined | undefined;
64
64
  translate?: "yes" | "no" | undefined | undefined;
65
65
  radioGroup?: string | undefined | undefined;
66
66
  role?: import("react").AriaRole | undefined;
@@ -143,7 +143,6 @@ declare const meta: {
143
143
  "aria-valuemin"?: number | undefined | undefined;
144
144
  "aria-valuenow"?: number | undefined | undefined;
145
145
  "aria-valuetext"?: string | undefined | undefined;
146
- children?: import("react").ReactNode;
147
146
  dangerouslySetInnerHTML?: {
148
147
  __html: string | TrustedHTML;
149
148
  } | undefined | undefined;
@@ -305,6 +304,7 @@ declare const meta: {
305
304
  onAnimationIterationCapture?: import("react").AnimationEventHandler<HTMLSpanElement> | undefined;
306
305
  onTransitionEnd?: import("react").TransitionEventHandler<HTMLSpanElement> | undefined;
307
306
  onTransitionEndCapture?: import("react").TransitionEventHandler<HTMLSpanElement> | undefined;
307
+ key?: import("react").Key | null | undefined;
308
308
  height?: number | string | undefined;
309
309
  width?: number | string | undefined;
310
310
  shape?: (typeof shapes)[number] | undefined;
@@ -17,12 +17,8 @@ declare const meta: {
17
17
  variant: "default";
18
18
  };
19
19
  decorators: ((Story: import("storybook/internal/csf").PartialStoryFn<import("@storybook/react-webpack5").ReactRenderer, {
20
- slot?: string | undefined | undefined;
20
+ className?: string | undefined;
21
21
  style?: import("react").CSSProperties | undefined;
22
- title: import("react").ReactNode;
23
- track?: string | undefined;
24
- key?: import("react").Key | null | undefined;
25
- value?: string | number | readonly string[] | undefined;
26
22
  defaultChecked?: boolean | undefined | undefined;
27
23
  defaultValue?: string | number | readonly string[] | undefined;
28
24
  suppressContentEditableWarning?: boolean | undefined | undefined;
@@ -30,7 +26,6 @@ declare const meta: {
30
26
  accessKey?: string | undefined | undefined;
31
27
  autoCapitalize?: "off" | "none" | "on" | "sentences" | "words" | "characters" | undefined | (string & {}) | undefined;
32
28
  autoFocus?: boolean | undefined | undefined;
33
- className?: string | undefined;
34
29
  contentEditable?: (boolean | "true" | "false") | "inherit" | "plaintext-only" | undefined;
35
30
  contextMenu?: string | undefined | undefined;
36
31
  dir?: string | undefined | undefined;
@@ -40,8 +35,10 @@ declare const meta: {
40
35
  id?: string | undefined | undefined;
41
36
  lang?: string | undefined | undefined;
42
37
  nonce?: string | undefined | undefined;
38
+ slot?: string | undefined | undefined;
43
39
  spellCheck?: (boolean | "true" | "false") | undefined;
44
40
  tabIndex?: number | undefined | undefined;
41
+ title: import("react").ReactNode;
45
42
  translate?: "yes" | "no" | undefined | undefined;
46
43
  radioGroup?: string | undefined | undefined;
47
44
  role?: import("react").AriaRole | undefined;
@@ -285,6 +282,9 @@ declare const meta: {
285
282
  onAnimationIterationCapture?: import("react").AnimationEventHandler<HTMLLIElement> | undefined;
286
283
  onTransitionEnd?: import("react").TransitionEventHandler<HTMLLIElement> | undefined;
287
284
  onTransitionEndCapture?: import("react").TransitionEventHandler<HTMLLIElement> | undefined;
285
+ track?: string | undefined;
286
+ key?: import("react").Key | null | undefined;
287
+ value?: string | number | readonly string[] | undefined;
288
288
  variant?: (typeof variants)[number] | undefined;
289
289
  trackProps?: Record<string, string | number | boolean> | undefined;
290
290
  open?: boolean | undefined;
@@ -2238,6 +2238,14 @@
2238
2238
  width: 100%;
2239
2239
  }
2240
2240
 
2241
+ /* open 을 넘기면 X 가 진짜 닫기 버튼이 된다. 버튼 기본 모양을 지워 예전 아이콘과 같게 보이게 한다 */
2242
+
2243
+ button.modal-with-double-button-close-button {
2244
+ padding: 0;
2245
+ background-color: transparent;
2246
+ color: var(--color-grey-scale-600);
2247
+ }
2248
+
2241
2249
  .modal-with-double-button-container.size-s {
2242
2250
  width: var(--modal-width-s);
2243
2251
  }
package/dist/index.js CHANGED
@@ -3183,9 +3183,28 @@ var GHOST_SIDE = {
3183
3183
  row: 'left',
3184
3184
  column: 'right'
3185
3185
  };
3186
+ // Radix 는 Dialog.Trigger 로 열었을 때만 포커스를 돌려준다. 부모가 연 경우를 위해 연 요소를 기억한다
3187
+ var useReturnFocus = function useReturnFocus() {
3188
+ var opener = (0,react__WEBPACK_IMPORTED_MODULE_1__.useRef)(null);
3189
+ var rememberOpener = function rememberOpener() {
3190
+ opener.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
3191
+ };
3192
+ var returnFocus = function returnFocus(event) {
3193
+ var _a;
3194
+ if (!((_a = opener.current) === null || _a === void 0 ? void 0 : _a.isConnected)) return;
3195
+ event.preventDefault();
3196
+ opener.current.focus();
3197
+ };
3198
+ return {
3199
+ rememberOpener: rememberOpener,
3200
+ returnFocus: returnFocus
3201
+ };
3202
+ };
3186
3203
  var ModalWithDoubleButtonBody = /*#__PURE__*/(0,react__WEBPACK_IMPORTED_MODULE_1__.forwardRef)(function (props, ref) {
3187
3204
  var _a = (0,_helpers_utils_getMergedProps__WEBPACK_IMPORTED_MODULE_4__/* .getMergedProps */ .p)(props, _modal_props__WEBPACK_IMPORTED_MODULE_8__/* .modalWithDoubleButtonProps */ .r4),
3188
3205
  layout = _a.layout,
3206
+ open = _a.open,
3207
+ onOpenChange = _a.onOpenChange,
3189
3208
  content = _a.content,
3190
3209
  title = _a.title,
3191
3210
  subTitle = _a.subTitle,
@@ -3195,16 +3214,32 @@ var ModalWithDoubleButtonBody = /*#__PURE__*/(0,react__WEBPACK_IMPORTED_MODULE_1
3195
3214
  right = _a.right,
3196
3215
  className = _a.className;
3197
3216
  var ghostSide = GHOST_SIDE[layout];
3217
+ // open 을 넘기면 Radix 가 열림·포커스·Esc 를 맡는다. 안 넘기면 예전처럼 forceMount 로 늘 보인다.
3218
+ var controlled = open !== undefined;
3219
+ var _b = useReturnFocus(),
3220
+ rememberOpener = _b.rememberOpener,
3221
+ returnFocus = _b.returnFocus;
3198
3222
  return (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A, {
3223
+ open: open,
3224
+ onOpenChange: onOpenChange,
3199
3225
  children: (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A.Container, {
3200
3226
  ref: ref,
3201
3227
  className: (0,_helpers_utils_classNames__WEBPACK_IMPORTED_MODULE_3__/* .classNames */ .x)(className, 'modal-with-double-button-container'),
3202
- forceMount: true,
3203
- children: [showCloseIcon && (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_button_base_button__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A, {
3228
+ forceMount: controlled ? undefined : true,
3229
+ onOpenAutoFocus: controlled ? rememberOpener : undefined,
3230
+ onCloseAutoFocus: controlled ? returnFocus : undefined,
3231
+ children: [showCloseIcon && (controlled ? (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A.Close, {
3232
+ className: 'modal-with-double-button-close-button',
3233
+ "aria-label": "\uB2EB\uAE30",
3234
+ asChild: true,
3235
+ children: (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_button_base_button__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A, {
3236
+ children: (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_assets_icons_multifly_svg__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .A, {})
3237
+ })
3238
+ }) : (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_button_base_button__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A, {
3204
3239
  className: 'modal-with-double-button-close-button',
3205
3240
  asChild: true,
3206
3241
  children: (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_assets_icons_multifly_svg__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .A, {})
3207
- }), titleIcon && (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A.Title, {
3242
+ })), titleIcon && (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A.Title, {
3208
3243
  className: 'modal-with-double-button-title-icon',
3209
3244
  asChild: true,
3210
3245
  children: titleIcon
@@ -62,9 +62,28 @@ var GHOST_SIDE = {
62
62
  row: 'left',
63
63
  column: 'right'
64
64
  };
65
+ // Radix 는 Dialog.Trigger 로 열었을 때만 포커스를 돌려준다. 부모가 연 경우를 위해 연 요소를 기억한다
66
+ var useReturnFocus = function useReturnFocus() {
67
+ var opener = (0,react__WEBPACK_IMPORTED_MODULE_1__.useRef)(null);
68
+ var rememberOpener = function rememberOpener() {
69
+ opener.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
70
+ };
71
+ var returnFocus = function returnFocus(event) {
72
+ var _a;
73
+ if (!((_a = opener.current) === null || _a === void 0 ? void 0 : _a.isConnected)) return;
74
+ event.preventDefault();
75
+ opener.current.focus();
76
+ };
77
+ return {
78
+ rememberOpener: rememberOpener,
79
+ returnFocus: returnFocus
80
+ };
81
+ };
65
82
  var ModalWithDoubleButtonBody = /*#__PURE__*/(0,react__WEBPACK_IMPORTED_MODULE_1__.forwardRef)(function (props, ref) {
66
83
  var _a = (0,_helpers_utils_getMergedProps__WEBPACK_IMPORTED_MODULE_4__/* .getMergedProps */ .p)(props, _modal_props__WEBPACK_IMPORTED_MODULE_8__/* .modalWithDoubleButtonProps */ .r4),
67
84
  layout = _a.layout,
85
+ open = _a.open,
86
+ onOpenChange = _a.onOpenChange,
68
87
  content = _a.content,
69
88
  title = _a.title,
70
89
  subTitle = _a.subTitle,
@@ -74,16 +93,32 @@ var ModalWithDoubleButtonBody = /*#__PURE__*/(0,react__WEBPACK_IMPORTED_MODULE_1
74
93
  right = _a.right,
75
94
  className = _a.className;
76
95
  var ghostSide = GHOST_SIDE[layout];
96
+ // open 을 넘기면 Radix 가 열림·포커스·Esc 를 맡는다. 안 넘기면 예전처럼 forceMount 로 늘 보인다.
97
+ var controlled = open !== undefined;
98
+ var _b = useReturnFocus(),
99
+ rememberOpener = _b.rememberOpener,
100
+ returnFocus = _b.returnFocus;
77
101
  return (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A, {
102
+ open: open,
103
+ onOpenChange: onOpenChange,
78
104
  children: (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A.Container, {
79
105
  ref: ref,
80
106
  className: (0,_helpers_utils_classNames__WEBPACK_IMPORTED_MODULE_3__/* .classNames */ .x)(className, 'modal-with-double-button-container'),
81
- forceMount: true,
82
- children: [showCloseIcon && (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_button_base_button__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A, {
107
+ forceMount: controlled ? undefined : true,
108
+ onOpenAutoFocus: controlled ? rememberOpener : undefined,
109
+ onCloseAutoFocus: controlled ? returnFocus : undefined,
110
+ children: [showCloseIcon && (controlled ? (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A.Close, {
111
+ className: 'modal-with-double-button-close-button',
112
+ "aria-label": "\uB2EB\uAE30",
113
+ asChild: true,
114
+ children: (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_button_base_button__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A, {
115
+ children: (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_assets_icons_multifly_svg__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .A, {})
116
+ })
117
+ }) : (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_button_base_button__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A, {
83
118
  className: 'modal-with-double-button-close-button',
84
119
  asChild: true,
85
120
  children: (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_assets_icons_multifly_svg__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .A, {})
86
- }), titleIcon && (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A.Title, {
121
+ })), titleIcon && (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A.Title, {
87
122
  className: 'modal-with-double-button-title-icon',
88
123
  asChild: true,
89
124
  children: titleIcon
package/dist/styles.css CHANGED
@@ -2280,6 +2280,12 @@ p {
2280
2280
  align-items: center;
2281
2281
  width: 100%;
2282
2282
  }
2283
+ /* open 을 넘기면 X 가 진짜 닫기 버튼이 된다. 버튼 기본 모양을 지워 예전 아이콘과 같게 보이게 한다 */
2284
+ button.modal-with-double-button-close-button {
2285
+ padding: 0;
2286
+ background-color: transparent;
2287
+ color: var(--color-grey-scale-600);
2288
+ }
2283
2289
  .modal-with-double-button-container.size-s {
2284
2290
  width: var(--modal-width-s);
2285
2291
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurihaus/web-design-system",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "Nurihaus 디자인 시스템 — 컴포넌트와 디자인 토큰",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",