@14ch/svelte-ui 0.0.18 → 0.0.19

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.
@@ -27,6 +27,7 @@
27
27
  // 状態/動作
28
28
  isOpen?: boolean;
29
29
  closeIfClickOutside?: boolean;
30
+ focusFirstOnOpen?: boolean;
30
31
 
31
32
  // イベントハンドラー
32
33
  onSubmit?: () => void;
@@ -51,6 +52,7 @@
51
52
  // 状態/動作
52
53
  isOpen = $bindable(false),
53
54
  closeIfClickOutside = true,
55
+ focusFirstOnOpen = false,
54
56
 
55
57
  // イベントハンドラー
56
58
  onSubmit = () => {}, // No params for type inference
@@ -92,6 +94,7 @@
92
94
  {width}
93
95
  {scrollable}
94
96
  {closeIfClickOutside}
97
+ {focusFirstOnOpen}
95
98
  id={id ? `${id}-dialog` : undefined}
96
99
  >
97
100
  <div class="confirm-dialog-message">
@@ -9,6 +9,7 @@ export type ConfirmDialogProps = {
9
9
  scrollable?: boolean;
10
10
  isOpen?: boolean;
11
11
  closeIfClickOutside?: boolean;
12
+ focusFirstOnOpen?: boolean;
12
13
  onSubmit?: () => void;
13
14
  onCancel?: () => void;
14
15
  };
@@ -43,6 +43,7 @@
43
43
  scrollable?: boolean;
44
44
  closeIfClickOutside?: boolean;
45
45
  restoreFocus?: boolean;
46
+ focusFirstOnOpen?: boolean;
46
47
 
47
48
  // ARIA/アクセシビリティ
48
49
  ariaLabel?: string;
@@ -72,6 +73,7 @@
72
73
  scrollable = false,
73
74
  closeIfClickOutside = true,
74
75
  restoreFocus = false,
76
+ focusFirstOnOpen = false,
75
77
 
76
78
  // ARIA/アクセシビリティ
77
79
  ariaLabel,
@@ -130,6 +132,7 @@
130
132
  bind:isOpen
131
133
  {closeIfClickOutside}
132
134
  {restoreFocus}
135
+ {focusFirstOnOpen}
133
136
  componentType="Dialog"
134
137
  {ariaLabel}
135
138
  {ariaLabelledby}
@@ -25,6 +25,7 @@ export type DialogProps = {
25
25
  scrollable?: boolean;
26
26
  closeIfClickOutside?: boolean;
27
27
  restoreFocus?: boolean;
28
+ focusFirstOnOpen?: boolean;
28
29
  ariaLabel?: string;
29
30
  ariaDescribedby?: string;
30
31
  };
@@ -44,6 +44,7 @@
44
44
  scrollable?: boolean;
45
45
  closeIfClickOutside?: boolean;
46
46
  restoreFocus?: boolean;
47
+ focusFirstOnOpen?: boolean;
47
48
 
48
49
  // ARIA/アクセシビリティ
49
50
  ariaLabel?: string;
@@ -74,6 +75,7 @@
74
75
  scrollable = false,
75
76
  closeIfClickOutside = true,
76
77
  restoreFocus = false,
78
+ focusFirstOnOpen = false,
77
79
 
78
80
  // ARIA/アクセシビリティ
79
81
  ariaLabel = 'Drawer',
@@ -143,6 +145,7 @@
143
145
  bind:isOpen
144
146
  {closeIfClickOutside}
145
147
  {restoreFocus}
148
+ {focusFirstOnOpen}
146
149
  componentType="Drawer"
147
150
  {ariaLabel}
148
151
  {ariaLabelledby}
@@ -267,18 +270,10 @@
267
270
  animation: fadeInFromRight var(--svelte-ui-transition-duration, 300ms) forwards;
268
271
  }
269
272
 
270
- :global(.drawer-wrapper--right.fade-in::backdrop) {
271
- animation: fadeIn var(--svelte-ui-transition-duration, 300ms) forwards;
272
- }
273
-
274
273
  :global(.drawer-wrapper--left.fade-in) {
275
274
  animation: fadeInFromLeft var(--svelte-ui-transition-duration, 300ms) forwards;
276
275
  }
277
276
 
278
- :global(.drawer-wrapper--left.fade-in::backdrop) {
279
- animation: fadeIn var(--svelte-ui-transition-duration, 300ms) forwards;
280
- }
281
-
282
277
  :global(.drawer-wrapper--left.fade-out) {
283
278
  animation: fadeOutToLeft var(--svelte-ui-transition-duration, 300ms) forwards;
284
279
  }
@@ -287,10 +282,6 @@
287
282
  animation: fadeOutToRight var(--svelte-ui-transition-duration, 300ms) forwards;
288
283
  }
289
284
 
290
- :global(.drawer-wrapper.fade-out::backdrop) {
291
- animation: fadeOut var(--svelte-ui-transition-duration, 300ms) forwards;
292
- }
293
-
294
285
  .drawer {
295
286
  display: flex;
296
287
  flex-direction: column;
@@ -362,14 +353,10 @@
362
353
  /* Reduced motion support */
363
354
  @media (prefers-reduced-motion: reduce) {
364
355
  :global(.drawer-wrapper.fade-in),
365
- :global(.drawer-wrapper.fade-in::backdrop),
366
356
  :global(.drawer-wrapper.fade-out),
367
- :global(.drawer-wrapper.fade-out::backdrop),
368
357
  :global(.drawer-wrapper--left.fade-in),
369
- :global(.drawer-wrapper--left.fade-in::backdrop),
370
358
  :global(.drawer-wrapper--left.fade-out),
371
359
  :global(.drawer-wrapper--right.fade-in),
372
- :global(.drawer-wrapper--right.fade-in::backdrop),
373
360
  :global(.drawer-wrapper--right.fade-out) {
374
361
  animation-duration: 0.01s;
375
362
  }
@@ -26,6 +26,7 @@ export type DrawerProps = {
26
26
  scrollable?: boolean;
27
27
  closeIfClickOutside?: boolean;
28
28
  restoreFocus?: boolean;
29
+ focusFirstOnOpen?: boolean;
29
30
  ariaLabel?: string;
30
31
  ariaDescribedby?: string;
31
32
  };
@@ -33,6 +33,7 @@
33
33
  isOpen?: boolean;
34
34
  closeIfClickOutside?: boolean;
35
35
  restoreFocus?: boolean;
36
+ focusFirstOnOpen?: boolean;
36
37
 
37
38
  // ARIA/アクセシビリティ
38
39
  ariaLabel?: string;
@@ -58,6 +59,7 @@
58
59
  isOpen = $bindable(false),
59
60
  closeIfClickOutside = true,
60
61
  restoreFocus = false,
62
+ focusFirstOnOpen = false,
61
63
 
62
64
  // ARIA/アクセシビリティ
63
65
  ariaLabel,
@@ -172,19 +174,11 @@
172
174
  dialogRef.showModal();
173
175
 
174
176
  setTimeout(() => {
175
- const firstFocusableElement = dialogRef?.querySelector(
176
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
177
- ) as HTMLElement;
178
- firstFocusableElement?.focus();
179
-
177
+ if (!focusFirstOnOpen) {
178
+ dialogRef?.focus();
179
+ }
180
180
  announceOpenClose(componentType, true, title || ariaLabel || '');
181
181
  }, 0);
182
-
183
- // 自動フォーカス時の枠線制御用クラス
184
- dialogRef.classList.add('modal-opening');
185
- setTimeout(() => {
186
- dialogRef?.classList.remove('modal-opening');
187
- }, 100); // 短い時間で制御
188
182
  };
189
183
 
190
184
  export const close = (title?: string): void => {
@@ -222,6 +216,7 @@
222
216
  bind:this={dialogRef}
223
217
  class="modal {customClass} {isOpen ? 'fade-in' : 'fade-out'}"
224
218
  style={customStyles}
219
+ tabindex="-1"
225
220
  aria-modal="true"
226
221
  aria-label={ariaLabel}
227
222
  aria-labelledby={ariaLabelledby}
@@ -265,11 +260,6 @@
265
260
  outline-offset: var(--svelte-ui-focus-outline-offset-outer);
266
261
  }
267
262
 
268
- /* 自動フォーカス時の枠線制御用 */
269
- .modal.modal-opening *:focus {
270
- outline: none !important;
271
- }
272
-
273
263
  .modal-contents {
274
264
  width: 100%;
275
265
  height: 100%;
@@ -294,22 +284,27 @@
294
284
  opacity: 0;
295
285
  }
296
286
  }
297
- .fade-in,
298
- .fade-in::backdrop {
287
+ .modal.fade-in:not([class*=drawer-wrapper]) {
299
288
  animation: fadeIn var(--svelte-ui-transition-duration, 300ms) forwards;
300
289
  }
301
290
 
302
- .fade-out,
303
- .fade-out::backdrop {
291
+ .modal.fade-in::backdrop {
292
+ animation: fadeIn var(--svelte-ui-transition-duration, 300ms) forwards;
293
+ }
294
+
295
+ .modal.fade-out:not([class*=drawer-wrapper]) {
296
+ animation: fadeOut var(--svelte-ui-transition-duration, 300ms) forwards;
297
+ }
298
+
299
+ .modal.fade-out::backdrop {
304
300
  animation: fadeOut var(--svelte-ui-transition-duration, 300ms) forwards;
305
301
  }
306
302
 
307
- /* Reduced motion support */
308
303
  @media (prefers-reduced-motion: reduce) {
309
- .fade-in,
310
- .fade-in::backdrop,
311
- .fade-out,
312
- .fade-out::backdrop {
304
+ .modal.fade-in,
305
+ .modal.fade-in::backdrop,
306
+ .modal.fade-out,
307
+ .modal.fade-out::backdrop {
313
308
  animation-duration: 0.01s;
314
309
  }
315
310
  }</style>
@@ -19,6 +19,7 @@ export type ModalProps = {
19
19
  isOpen?: boolean;
20
20
  closeIfClickOutside?: boolean;
21
21
  restoreFocus?: boolean;
22
+ focusFirstOnOpen?: boolean;
22
23
  ariaLabel?: string;
23
24
  ariaLabelledby?: string;
24
25
  ariaDescribedby?: string;
@@ -28,9 +28,6 @@ export const t = (key, params) => {
28
28
  const locale = (globalLocale && globalLocale in TRANSLATIONS) ? globalLocale : 'en';
29
29
  const message = key.split('.').reduce((obj, k) => obj?.[k], TRANSLATIONS[locale]);
30
30
  if (typeof message !== 'string') {
31
- if (import.meta.env.DEV) {
32
- console.warn(`Translation key "${key}" not found for locale "${locale}"`);
33
- }
34
31
  return key;
35
32
  }
36
33
  return replaceParams(message, params);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@14ch/svelte-ui",
3
3
  "description": "Modern Svelte UI components library with TypeScript support",
4
4
  "private": false,
5
- "version": "0.0.18",
5
+ "version": "0.0.19",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "svelte",
@@ -106,6 +106,7 @@
106
106
  "sass": "^1.89.2"
107
107
  },
108
108
  "peerDependencies": {
109
+ "@sveltejs/kit": "^2.0.0",
109
110
  "svelte": "^5.0.0"
110
111
  }
111
112
  }
@@ -1,127 +0,0 @@
1
- # コンポーネント設計ガイドライン
2
-
3
- ## ID命名ルール
4
-
5
- ### コンポーネント内包時のID受け渡しルール
6
-
7
- #### 基本ルール
8
-
9
- - **親コンポーネント**: `id`プロパティを受け取る
10
- - **子コンポーネント**: 受け取った`id`にsuffixを付けて自身のIDを生成
11
- - **内部要素**: 受け取った`id`にsuffixを付けてIDを生成
12
-
13
- #### 命名パターン
14
-
15
- ```typescript
16
- // 親 → 子コンポーネント
17
- <ChildComponent id={id ? `${id}-child-suffix` : undefined} />
18
-
19
- // 内部要素
20
- <div id={id ? `${id}-element-suffix` : undefined}>
21
- ```
22
-
23
- #### 具体例
24
-
25
- ```typescript
26
- // ConfirmDialog → Dialog → Modal
27
- <Dialog id={id ? `${id}-dialog` : undefined} />
28
- <Modal id={id ? `${id}-modal` : undefined} />
29
-
30
- // 内部要素
31
- <div id={id ? `${id}-dialog-title` : undefined}>
32
- <div id={id ? `${id}-modal-description` : undefined}>
33
-
34
- // 他の例
35
- <Input id={id ? `${id}-input` : undefined} />
36
- <Popup id={id ? `${id}-popup` : undefined} />
37
- <DatepickerCalendar id={id ? `${id}-calendar` : undefined} />
38
- ```
39
-
40
- #### 判断基準
41
-
42
- - **✅ IDを渡すべき**: 1対1の親子関係、アクセシビリティ上重要な要素、テストで個別特定が必要
43
- - **❌ IDを渡さない**: 同じコンポーネントが複数存在、内部実装の詳細、動的生成要素
44
-
45
- ### 理由
46
-
47
- - **階層構造の明確化**: `${id}-suffix`パターンにより、コンポーネントの階層関係が分かりやすい
48
- - **グローバル一意性**: 親のIDをベースにすることで、DOM全体で一意なIDを保証
49
- - **アクセシビリティ**: ARIA属性との連携で、スクリーンリーダーなどの支援技術に対応
50
- - **テストの安定性**: 一意なIDにより、テストでの要素特定が確実
51
-
52
- ## コンポーネント設計原則
53
-
54
- ### プロパティ設計
55
-
56
- - **基本プロパティ**: 必須の機能に関わるプロパティ
57
- - **HTML属性**: 標準的なHTML属性(id, class, style等)
58
- - **スタイル/レイアウト**: 見た目やレイアウトに関わるプロパティ
59
- - **状態/動作**: コンポーネントの動作状態に関わるプロパティ
60
- - **ARIA/アクセシビリティ**: アクセシビリティに関わるプロパティ
61
- - **イベントハンドラー**: イベント処理に関わるプロパティ
62
-
63
- ### イベントハンドラー設計
64
-
65
- ```typescript
66
- // Svelte 5の推奨パターン
67
- onclick = () => {}, // パラメータなしで型推論を可能にする
68
- onchange = () => {}, // パラメータなしで型推論を可能にする
69
- ```
70
-
71
- ### データテストID
72
-
73
- - 最上位のDOM要素に`data-testid`を設定
74
- - テストでの要素特定を容易にする
75
- - コンポーネント名をベースに命名(例: `data-testid="dialog"`)
76
-
77
- ## 実装例
78
-
79
- ### 基本的なコンポーネント構造
80
-
81
- ```svelte
82
- <script lang="ts">
83
- // Props, States & Constants
84
- let {
85
- // 基本プロパティ
86
- title = 'Default Title',
87
-
88
- // HTML属性
89
- id,
90
-
91
- // スタイル/レイアウト
92
- variant = 'default',
93
-
94
- // 状態/動作
95
- isOpen = $bindable(false),
96
-
97
- // イベントハンドラー
98
- onclick = () => {}
99
- }: {
100
- // 型定義
101
- title?: string;
102
- id?: string;
103
- variant?: 'default' | 'primary';
104
- isOpen?: boolean;
105
- onclick?: () => void;
106
- } = $props();
107
- </script>
108
-
109
- <div class="component" {id} data-testid="component">
110
- <!-- コンテンツ -->
111
- </div>
112
- ```
113
-
114
- ### 内包コンポーネントの例
115
-
116
- ```svelte
117
- <script lang="ts">
118
- let { id }: { id?: string } = $props();
119
- </script>
120
-
121
- <div class="parent" {id} data-testid="parent">
122
- <ChildComponent id={id ? `${id}-child` : undefined} />
123
- <div id={id ? `${id}-element` : undefined}>
124
- <!-- 内部要素 -->
125
- </div>
126
- </div>
127
- ```