@14ch/svelte-ui 0.0.6 → 0.0.7
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/assets/styles/variables.scss +20 -1
- package/dist/components/Button.svelte +1 -1
- package/dist/components/ColorPicker.svelte +5 -5
- package/dist/components/Input.svelte +23 -23
- package/dist/components/Input.svelte.d.ts +0 -3
- package/dist/components/SegmentedControl.svelte +622 -0
- package/dist/components/SegmentedControl.svelte.d.ts +49 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/types/segmentedControlItem.d.ts +13 -0
- package/dist/types/segmentedControlItem.js +1 -0
- package/package.json +1 -1
|
@@ -341,6 +341,25 @@
|
|
|
341
341
|
--svelte-ui-iconbutton-hover-overlay: var(--svelte-ui-hover-overlay);
|
|
342
342
|
--svelte-ui-iconbutton-focus-color: var(--svelte-ui-focus-color);
|
|
343
343
|
|
|
344
|
+
/* SegmentedControl */
|
|
345
|
+
--svelte-ui-segmented-control-base-padding: 4px;
|
|
346
|
+
--svelte-ui-segmented-control-base-border-radius: var(--svelte-ui-border-radius);
|
|
347
|
+
--svelte-ui-segmented-control-gap: 4px;
|
|
348
|
+
--svelte-ui-segmented-control-button-radius: calc(var(--svelte-ui-border-radius) - 1px);
|
|
349
|
+
--svelte-ui-segmented-control-button-height-sm: 24px;
|
|
350
|
+
--svelte-ui-segmented-control-button-height: 32px;
|
|
351
|
+
--svelte-ui-segmented-control-button-height-lg: 40px;
|
|
352
|
+
--svelte-ui-segmented-control-font-size-sm: 12px;
|
|
353
|
+
--svelte-ui-segmented-control-font-size: 14px;
|
|
354
|
+
--svelte-ui-segmented-control-font-size-lg: 16px;
|
|
355
|
+
--svelte-ui-segmented-control-button-padding-sm: 4px 8px;
|
|
356
|
+
--svelte-ui-segmented-control-button-padding: 6px 12px;
|
|
357
|
+
--svelte-ui-segmented-control-button-padding-lg: 8px 16px;
|
|
358
|
+
--svelte-ui-segmented-control-base-bg: var(--svelte-ui-surface-accent-color);
|
|
359
|
+
--svelte-ui-segmented-control-selected-bg: var(--svelte-ui-primary-color);
|
|
360
|
+
--svelte-ui-segmented-control-selected-text-color: var(--svelte-ui-text-on-filled-color);
|
|
361
|
+
--svelte-ui-segmented-control-hover-overlay: var(--svelte-ui-hover-overlay);
|
|
362
|
+
|
|
344
363
|
/* Input */
|
|
345
364
|
--svelte-ui-input-height: var(--svelte-ui-form-height);
|
|
346
365
|
--svelte-ui-input-padding: var(--svelte-ui-form-padding);
|
|
@@ -490,7 +509,7 @@
|
|
|
490
509
|
/* ColorPicker */
|
|
491
510
|
--svelte-ui-colorpicker-trigger-size: 28px;
|
|
492
511
|
--svelte-ui-colorpicker-text-padding-left: 40px;
|
|
493
|
-
--svelte-ui-colorpicker-trigger-border-radius: calc(var(--svelte-ui-border-radius) -
|
|
512
|
+
--svelte-ui-colorpicker-trigger-border-radius: calc(var(--svelte-ui-border-radius) - 1px);
|
|
494
513
|
--svelte-ui-colorpicker-trigger-border-style: dashed;
|
|
495
514
|
--svelte-ui-colorpicker-trigger-border-width: 1px;
|
|
496
515
|
--svelte-ui-colorpicker-trigger-offset: 4px;
|
|
@@ -167,10 +167,6 @@
|
|
|
167
167
|
// Methods
|
|
168
168
|
// =========================================================================
|
|
169
169
|
|
|
170
|
-
const handleSubmit = (event: SubmitEvent) => {
|
|
171
|
-
event?.preventDefault?.();
|
|
172
|
-
handleChange();
|
|
173
|
-
};
|
|
174
170
|
|
|
175
171
|
const handleChange = (event?: Event): void => {
|
|
176
172
|
// 空文字列の場合はそのまま処理
|
|
@@ -207,6 +203,11 @@
|
|
|
207
203
|
onclick?.(event);
|
|
208
204
|
};
|
|
209
205
|
const handleKeydown = (event: KeyboardEvent) => {
|
|
206
|
+
// Enterキーで色の変更を確定
|
|
207
|
+
if (event.key === 'Enter' && !disabled && !readonly) {
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
handleChange();
|
|
210
|
+
}
|
|
210
211
|
if (disabled) return;
|
|
211
212
|
onkeydown(event);
|
|
212
213
|
};
|
|
@@ -361,7 +362,6 @@
|
|
|
361
362
|
onpointerleave={handlePointerLeave}
|
|
362
363
|
onpointermove={handlePointerMove}
|
|
363
364
|
onpointercancel={handlePointerCancel}
|
|
364
|
-
onsubmit={handleSubmit}
|
|
365
365
|
{...restProps}
|
|
366
366
|
/>
|
|
367
367
|
|
|
@@ -95,11 +95,6 @@
|
|
|
95
95
|
// 入力イベント
|
|
96
96
|
onchange = () => {}, // No params for type inference
|
|
97
97
|
oninput = () => {}, // No params for type inference
|
|
98
|
-
onsubmit = () => {}, // No params for type inference
|
|
99
|
-
|
|
100
|
-
// IMEイベント
|
|
101
|
-
oncompositionstart = () => {}, // No params for type inference
|
|
102
|
-
oncompositionend = () => {}, // No params for type inference
|
|
103
98
|
|
|
104
99
|
// アイコンイベント
|
|
105
100
|
onRightIconClick,
|
|
@@ -190,11 +185,6 @@
|
|
|
190
185
|
// 入力イベント
|
|
191
186
|
onchange?: (value: any) => void;
|
|
192
187
|
oninput?: (value: any) => void;
|
|
193
|
-
onsubmit?: (value: any) => void;
|
|
194
|
-
|
|
195
|
-
// IMEイベント
|
|
196
|
-
oncompositionstart?: Function; // No params for type inference
|
|
197
|
-
oncompositionend?: Function; // No params for type inference
|
|
198
188
|
|
|
199
189
|
// アイコンイベント
|
|
200
190
|
onRightIconClick?: Function; // No params for type inference
|
|
@@ -206,6 +196,7 @@
|
|
|
206
196
|
|
|
207
197
|
let ref: HTMLInputElement | undefined = $state();
|
|
208
198
|
let isFocused: boolean = $state(false);
|
|
199
|
+
let isComposing: boolean = $state(false);
|
|
209
200
|
|
|
210
201
|
// =========================================================================
|
|
211
202
|
// Methods
|
|
@@ -243,6 +234,10 @@
|
|
|
243
234
|
|
|
244
235
|
// キーボードイベント
|
|
245
236
|
const handleKeydown = (event: KeyboardEvent) => {
|
|
237
|
+
// Enterキーで入力確定(blur)する(IME変換中は除く)
|
|
238
|
+
if (event.key === 'Enter' && !disabled && !readonly && !isComposing) {
|
|
239
|
+
ref?.blur();
|
|
240
|
+
}
|
|
246
241
|
onkeydown?.(event);
|
|
247
242
|
};
|
|
248
243
|
|
|
@@ -251,12 +246,6 @@
|
|
|
251
246
|
};
|
|
252
247
|
|
|
253
248
|
// 入力イベント
|
|
254
|
-
const handleSubmit = (event: SubmitEvent) => {
|
|
255
|
-
if (disabled || readonly) return;
|
|
256
|
-
event?.preventDefault?.();
|
|
257
|
-
ref?.blur();
|
|
258
|
-
onsubmit?.(value);
|
|
259
|
-
};
|
|
260
249
|
|
|
261
250
|
const handleChange = () => {
|
|
262
251
|
if (disabled || readonly) return;
|
|
@@ -366,6 +355,15 @@
|
|
|
366
355
|
onpointercancel?.(event);
|
|
367
356
|
};
|
|
368
357
|
|
|
358
|
+
// IMEイベント
|
|
359
|
+
const handleCompositionStart = () => {
|
|
360
|
+
isComposing = true;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const handleCompositionEnd = () => {
|
|
364
|
+
isComposing = false;
|
|
365
|
+
};
|
|
366
|
+
|
|
369
367
|
// =========================================================================
|
|
370
368
|
// $derived
|
|
371
369
|
// =========================================================================
|
|
@@ -405,8 +403,8 @@
|
|
|
405
403
|
{getDisplayValue()}
|
|
406
404
|
</div>
|
|
407
405
|
{/if}
|
|
408
|
-
<!--
|
|
409
|
-
<
|
|
406
|
+
<!-- 入力用要素 -->
|
|
407
|
+
<div class="input__wrapper">
|
|
410
408
|
<input
|
|
411
409
|
{id}
|
|
412
410
|
{name}
|
|
@@ -452,10 +450,12 @@
|
|
|
452
450
|
onpointerleave={handlePointerLeave}
|
|
453
451
|
onpointermove={handlePointerMove}
|
|
454
452
|
onpointercancel={handlePointerCancel}
|
|
453
|
+
oncompositionstart={handleCompositionStart}
|
|
454
|
+
oncompositionend={handleCompositionEnd}
|
|
455
455
|
{...inputAttributes}
|
|
456
456
|
{...restProps}
|
|
457
457
|
/>
|
|
458
|
-
</
|
|
458
|
+
</div>
|
|
459
459
|
<!-- クリアボタン -->
|
|
460
460
|
{#if clearable && !disabled && !readonly}
|
|
461
461
|
<div class="input__clear-button">
|
|
@@ -546,7 +546,7 @@
|
|
|
546
546
|
height: inherit;
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
-
|
|
549
|
+
.input__wrapper {
|
|
550
550
|
padding: inherit;
|
|
551
551
|
border: none;
|
|
552
552
|
font-size: inherit;
|
|
@@ -688,7 +688,7 @@
|
|
|
688
688
|
* デザインバリアント:default
|
|
689
689
|
* ============================================= */
|
|
690
690
|
.input:not(.input--inline) {
|
|
691
|
-
|
|
691
|
+
.input__wrapper {
|
|
692
692
|
position: static;
|
|
693
693
|
opacity: 1;
|
|
694
694
|
}
|
|
@@ -749,7 +749,7 @@
|
|
|
749
749
|
text-align: right;
|
|
750
750
|
}
|
|
751
751
|
|
|
752
|
-
|
|
752
|
+
.input__wrapper {
|
|
753
753
|
position: absolute;
|
|
754
754
|
top: 0;
|
|
755
755
|
left: 0;
|
|
@@ -790,7 +790,7 @@
|
|
|
790
790
|
opacity: 0;
|
|
791
791
|
}
|
|
792
792
|
|
|
793
|
-
|
|
793
|
+
.input__wrapper {
|
|
794
794
|
opacity: 1;
|
|
795
795
|
}
|
|
796
796
|
}
|
|
@@ -62,9 +62,6 @@ type $$ComponentProps = {
|
|
|
62
62
|
onpointercancel?: Function;
|
|
63
63
|
onchange?: (value: any) => void;
|
|
64
64
|
oninput?: (value: any) => void;
|
|
65
|
-
onsubmit?: (value: any) => void;
|
|
66
|
-
oncompositionstart?: Function;
|
|
67
|
-
oncompositionend?: Function;
|
|
68
65
|
onRightIconClick?: Function;
|
|
69
66
|
onLeftIconClick?: Function;
|
|
70
67
|
[key: string]: any;
|
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
<!-- SegmentedControl.svelte -->
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import Icon from './Icon.svelte';
|
|
5
|
+
import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
|
|
6
|
+
import type { SegmentedControlItem } from '../types/segmentedControlItem';
|
|
7
|
+
|
|
8
|
+
// =========================================================================
|
|
9
|
+
// Props, States & Constants
|
|
10
|
+
// =========================================================================
|
|
11
|
+
let {
|
|
12
|
+
// 基本プロパティ
|
|
13
|
+
items = [],
|
|
14
|
+
value = $bindable(''),
|
|
15
|
+
|
|
16
|
+
// HTML属性系
|
|
17
|
+
id,
|
|
18
|
+
name = `segmented-control-${Math.random().toString(36).substring(2, 15)}`,
|
|
19
|
+
|
|
20
|
+
// スタイル/レイアウト
|
|
21
|
+
size = 'medium',
|
|
22
|
+
fullWidth = false,
|
|
23
|
+
color,
|
|
24
|
+
rounded = false,
|
|
25
|
+
|
|
26
|
+
// アイコン関連
|
|
27
|
+
iconFilled = false,
|
|
28
|
+
iconWeight = 300,
|
|
29
|
+
iconGrade = 0,
|
|
30
|
+
iconOpticalSize,
|
|
31
|
+
iconVariant = 'outlined',
|
|
32
|
+
|
|
33
|
+
// 状態/動作
|
|
34
|
+
disabled = false,
|
|
35
|
+
|
|
36
|
+
// ARIA/アクセシビリティ
|
|
37
|
+
ariaLabel,
|
|
38
|
+
ariaLabelledby,
|
|
39
|
+
reducedMotion = false,
|
|
40
|
+
|
|
41
|
+
// 入力イベント
|
|
42
|
+
onchange = () => {}, // No params for type inference
|
|
43
|
+
|
|
44
|
+
// フォーカスイベント
|
|
45
|
+
onfocus = () => {}, // No params for type inference
|
|
46
|
+
onblur = () => {}, // No params for type inference
|
|
47
|
+
|
|
48
|
+
// キーボードイベント
|
|
49
|
+
onkeydown = () => {}, // No params for type inference
|
|
50
|
+
onkeyup = () => {}, // No params for type inference
|
|
51
|
+
|
|
52
|
+
// マウスイベント
|
|
53
|
+
onclick = () => {}, // No params for type inference
|
|
54
|
+
onmousedown = () => {}, // No params for type inference
|
|
55
|
+
onmouseup = () => {}, // No params for type inference
|
|
56
|
+
onmouseenter = () => {}, // No params for type inference
|
|
57
|
+
onmouseleave = () => {}, // No params for type inference
|
|
58
|
+
onmouseover = () => {}, // No params for type inference
|
|
59
|
+
onmouseout = () => {}, // No params for type inference
|
|
60
|
+
oncontextmenu = () => {}, // No params for type inference
|
|
61
|
+
onauxclick = () => {}, // No params for type inference
|
|
62
|
+
|
|
63
|
+
// タッチイベント
|
|
64
|
+
ontouchstart = () => {}, // No params for type inference
|
|
65
|
+
ontouchend = () => {}, // No params for type inference
|
|
66
|
+
ontouchmove = () => {}, // No params for type inference
|
|
67
|
+
ontouchcancel = () => {}, // No params for type inference
|
|
68
|
+
|
|
69
|
+
// ポインターイベント
|
|
70
|
+
onpointerdown = () => {}, // No params for type inference
|
|
71
|
+
onpointerup = () => {}, // No params for type inference
|
|
72
|
+
onpointerenter = () => {}, // No params for type inference
|
|
73
|
+
onpointerleave = () => {}, // No params for type inference
|
|
74
|
+
onpointermove = () => {}, // No params for type inference
|
|
75
|
+
onpointercancel = () => {}, // No params for type inference
|
|
76
|
+
|
|
77
|
+
// その他
|
|
78
|
+
...restProps
|
|
79
|
+
}: {
|
|
80
|
+
// 基本プロパティ
|
|
81
|
+
items: SegmentedControlItem[];
|
|
82
|
+
value: string;
|
|
83
|
+
|
|
84
|
+
// HTML属性系
|
|
85
|
+
id?: string;
|
|
86
|
+
name?: string;
|
|
87
|
+
|
|
88
|
+
// スタイル/レイアウト
|
|
89
|
+
size?: 'small' | 'medium' | 'large';
|
|
90
|
+
fullWidth?: boolean;
|
|
91
|
+
color?: string;
|
|
92
|
+
rounded?: boolean;
|
|
93
|
+
|
|
94
|
+
// アイコン関連
|
|
95
|
+
iconFilled?: boolean;
|
|
96
|
+
iconWeight?: IconWeight;
|
|
97
|
+
iconGrade?: IconGrade;
|
|
98
|
+
iconOpticalSize?: IconOpticalSize;
|
|
99
|
+
iconVariant?: IconVariant;
|
|
100
|
+
|
|
101
|
+
// 状態/動作
|
|
102
|
+
disabled?: boolean;
|
|
103
|
+
|
|
104
|
+
// ARIA/アクセシビリティ
|
|
105
|
+
ariaLabel?: string;
|
|
106
|
+
ariaLabelledby?: string;
|
|
107
|
+
reducedMotion?: boolean;
|
|
108
|
+
|
|
109
|
+
// 入力イベント
|
|
110
|
+
onchange?: (value: string) => void;
|
|
111
|
+
|
|
112
|
+
// フォーカスイベント
|
|
113
|
+
onfocus?: Function; // No params for type inference
|
|
114
|
+
onblur?: Function; // No params for type inference
|
|
115
|
+
|
|
116
|
+
// キーボードイベント
|
|
117
|
+
onkeydown?: Function; // No params for type inference
|
|
118
|
+
onkeyup?: Function; // No params for type inference
|
|
119
|
+
|
|
120
|
+
// マウスイベント
|
|
121
|
+
onclick?: Function; // No params for type inference
|
|
122
|
+
onmousedown?: Function; // No params for type inference
|
|
123
|
+
onmouseup?: Function; // No params for type inference
|
|
124
|
+
onmouseenter?: Function; // No params for type inference
|
|
125
|
+
onmouseleave?: Function; // No params for type inference
|
|
126
|
+
onmouseover?: Function; // No params for type inference
|
|
127
|
+
onmouseout?: Function; // No params for type inference
|
|
128
|
+
oncontextmenu?: Function; // No params for type inference
|
|
129
|
+
onauxclick?: Function; // No params for type inference
|
|
130
|
+
|
|
131
|
+
// タッチイベント
|
|
132
|
+
ontouchstart?: Function; // No params for type inference
|
|
133
|
+
ontouchend?: Function; // No params for type inference
|
|
134
|
+
ontouchmove?: Function; // No params for type inference
|
|
135
|
+
ontouchcancel?: Function; // No params for type inference
|
|
136
|
+
|
|
137
|
+
// ポインターイベント
|
|
138
|
+
onpointerdown?: Function; // No params for type inference
|
|
139
|
+
onpointerup?: Function; // No params for type inference
|
|
140
|
+
onpointerenter?: Function; // No params for type inference
|
|
141
|
+
onpointerleave?: Function; // No params for type inference
|
|
142
|
+
onpointermove?: Function; // No params for type inference
|
|
143
|
+
onpointercancel?: Function; // No params for type inference
|
|
144
|
+
|
|
145
|
+
// その他
|
|
146
|
+
[key: string]: any;
|
|
147
|
+
} = $props();
|
|
148
|
+
|
|
149
|
+
// =========================================================================
|
|
150
|
+
// Methods
|
|
151
|
+
// =========================================================================
|
|
152
|
+
const handleChange = (item: SegmentedControlItem, event: Event) => {
|
|
153
|
+
if (disabled || item.disabled) return;
|
|
154
|
+
|
|
155
|
+
const target = event.target as HTMLInputElement;
|
|
156
|
+
if (target.checked) {
|
|
157
|
+
value = item.value;
|
|
158
|
+
onchange(item.value);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handleFocus = (event: FocusEvent) => {
|
|
163
|
+
onfocus(event);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const handleBlur = (event: FocusEvent) => {
|
|
167
|
+
onblur(event);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
171
|
+
if (disabled || items.length === 0) {
|
|
172
|
+
onkeydown(event);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Radioコンポーネントと同じキーボードナビゲーション
|
|
177
|
+
if (
|
|
178
|
+
event.key === 'ArrowUp' ||
|
|
179
|
+
event.key === 'ArrowDown' ||
|
|
180
|
+
event.key === 'ArrowLeft' ||
|
|
181
|
+
event.key === 'ArrowRight'
|
|
182
|
+
) {
|
|
183
|
+
const radioInputs = document.querySelectorAll(
|
|
184
|
+
`input[type="radio"][name="${name}"]`
|
|
185
|
+
) as NodeListOf<HTMLInputElement>;
|
|
186
|
+
const currentIndex = Array.from(radioInputs).findIndex((input) => input === event.target);
|
|
187
|
+
|
|
188
|
+
if (currentIndex !== -1) {
|
|
189
|
+
event.preventDefault();
|
|
190
|
+
let nextIndex;
|
|
191
|
+
|
|
192
|
+
if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
|
|
193
|
+
// 前の有効なアイテムを探す
|
|
194
|
+
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
195
|
+
if (!items[i]?.disabled && !radioInputs[i].disabled) {
|
|
196
|
+
nextIndex = i;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// 見つからなければ最後の有効なアイテムへ
|
|
201
|
+
if (nextIndex === undefined) {
|
|
202
|
+
for (let i = items.length - 1; i > currentIndex; i--) {
|
|
203
|
+
if (!items[i]?.disabled && !radioInputs[i].disabled) {
|
|
204
|
+
nextIndex = i;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
// 次の有効なアイテムを探す
|
|
211
|
+
for (let i = currentIndex + 1; i < items.length; i++) {
|
|
212
|
+
if (!items[i]?.disabled && !radioInputs[i].disabled) {
|
|
213
|
+
nextIndex = i;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// 見つからなければ最初の有効なアイテムへ
|
|
218
|
+
if (nextIndex === undefined) {
|
|
219
|
+
for (let i = 0; i < currentIndex; i++) {
|
|
220
|
+
if (!items[i]?.disabled && !radioInputs[i].disabled) {
|
|
221
|
+
nextIndex = i;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (nextIndex !== undefined) {
|
|
229
|
+
const nextInput = radioInputs[nextIndex];
|
|
230
|
+
if (nextInput && !nextInput.disabled) {
|
|
231
|
+
nextInput.focus();
|
|
232
|
+
nextInput.click();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} else if (event.key === 'Home') {
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
const radioInputs = document.querySelectorAll(
|
|
239
|
+
`input[type="radio"][name="${name}"]`
|
|
240
|
+
) as NodeListOf<HTMLInputElement>;
|
|
241
|
+
for (let i = 0; i < items.length; i++) {
|
|
242
|
+
if (!items[i]?.disabled && !radioInputs[i].disabled) {
|
|
243
|
+
radioInputs[i].focus();
|
|
244
|
+
radioInputs[i].click();
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} else if (event.key === 'End') {
|
|
249
|
+
event.preventDefault();
|
|
250
|
+
const radioInputs = document.querySelectorAll(
|
|
251
|
+
`input[type="radio"][name="${name}"]`
|
|
252
|
+
) as NodeListOf<HTMLInputElement>;
|
|
253
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
254
|
+
if (!items[i]?.disabled && !radioInputs[i].disabled) {
|
|
255
|
+
radioInputs[i].focus();
|
|
256
|
+
radioInputs[i].click();
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
onkeydown(event);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const handleKeyup = (event: KeyboardEvent) => {
|
|
266
|
+
onkeyup(event);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// マウスイベント
|
|
270
|
+
const handleClick = (event: MouseEvent) => {
|
|
271
|
+
if (disabled) return;
|
|
272
|
+
onclick(event);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const handleMouseDown = (event: MouseEvent) => {
|
|
276
|
+
if (disabled) return;
|
|
277
|
+
onmousedown(event);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const handleMouseUp = (event: MouseEvent) => {
|
|
281
|
+
if (disabled) return;
|
|
282
|
+
onmouseup(event);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const handleMouseEnter = (event: MouseEvent) => {
|
|
286
|
+
if (disabled) return;
|
|
287
|
+
onmouseenter(event);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const handleMouseLeave = (event: MouseEvent) => {
|
|
291
|
+
if (disabled) return;
|
|
292
|
+
onmouseleave(event);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const handleMouseOver = (event: MouseEvent) => {
|
|
296
|
+
if (disabled) return;
|
|
297
|
+
onmouseover(event);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const handleMouseOut = (event: MouseEvent) => {
|
|
301
|
+
if (disabled) return;
|
|
302
|
+
onmouseout(event);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const handleContextMenu = (event: MouseEvent) => {
|
|
306
|
+
if (disabled) return;
|
|
307
|
+
oncontextmenu(event);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const handleAuxClick = (event: MouseEvent) => {
|
|
311
|
+
if (disabled) return;
|
|
312
|
+
onauxclick(event);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// タッチイベント
|
|
316
|
+
const handleTouchStart = (event: TouchEvent) => {
|
|
317
|
+
if (disabled) return;
|
|
318
|
+
ontouchstart(event);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const handleTouchEnd = (event: TouchEvent) => {
|
|
322
|
+
if (disabled) return;
|
|
323
|
+
ontouchend(event);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const handleTouchMove = (event: TouchEvent) => {
|
|
327
|
+
if (disabled) return;
|
|
328
|
+
ontouchmove(event);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const handleTouchCancel = (event: TouchEvent) => {
|
|
332
|
+
if (disabled) return;
|
|
333
|
+
ontouchcancel(event);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// ポインターイベント
|
|
337
|
+
const handlePointerDown = (event: PointerEvent) => {
|
|
338
|
+
if (disabled) return;
|
|
339
|
+
onpointerdown(event);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const handlePointerUp = (event: PointerEvent) => {
|
|
343
|
+
if (disabled) return;
|
|
344
|
+
onpointerup(event);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const handlePointerEnter = (event: PointerEvent) => {
|
|
348
|
+
if (disabled) return;
|
|
349
|
+
onpointerenter(event);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const handlePointerLeave = (event: PointerEvent) => {
|
|
353
|
+
if (disabled) return;
|
|
354
|
+
onpointerleave(event);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const handlePointerMove = (event: PointerEvent) => {
|
|
358
|
+
if (disabled) return;
|
|
359
|
+
onpointermove(event);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const handlePointerCancel = (event: PointerEvent) => {
|
|
363
|
+
if (disabled) return;
|
|
364
|
+
onpointercancel(event);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// =========================================================================
|
|
368
|
+
// $derived
|
|
369
|
+
// =========================================================================
|
|
370
|
+
const effectiveIconSize = $derived(
|
|
371
|
+
iconOpticalSize || (size === 'small' ? 16 : size === 'large' ? 24 : 20)
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const containerClasses = $derived(
|
|
375
|
+
[
|
|
376
|
+
'segmented-control',
|
|
377
|
+
`segmented-control--${size}`,
|
|
378
|
+
fullWidth && 'segmented-control--full-width',
|
|
379
|
+
rounded && 'segmented-control--rounded',
|
|
380
|
+
disabled && 'segmented-control--disabled',
|
|
381
|
+
reducedMotion && 'segmented-control--no-motion'
|
|
382
|
+
]
|
|
383
|
+
.filter(Boolean)
|
|
384
|
+
.join(' ')
|
|
385
|
+
);
|
|
386
|
+
</script>
|
|
387
|
+
|
|
388
|
+
<div
|
|
389
|
+
class={containerClasses}
|
|
390
|
+
role="radiogroup"
|
|
391
|
+
aria-label={ariaLabelledby ? undefined : ariaLabel}
|
|
392
|
+
aria-labelledby={ariaLabelledby}
|
|
393
|
+
style="--svelte-ui-segmented-control-selected-bg: {color || 'var(--svelte-ui-primary-color)'};"
|
|
394
|
+
{id}
|
|
395
|
+
data-testid="segmented-control"
|
|
396
|
+
{...restProps}
|
|
397
|
+
>
|
|
398
|
+
{#each items as item, index}
|
|
399
|
+
{@const isSelected = value === item.value}
|
|
400
|
+
{@const isDisabled = disabled || item.disabled}
|
|
401
|
+
{@const inputId = `${name}-${index}`}
|
|
402
|
+
<div class="segmented-control__item">
|
|
403
|
+
<input
|
|
404
|
+
type="radio"
|
|
405
|
+
id={inputId}
|
|
406
|
+
{name}
|
|
407
|
+
value={item.value}
|
|
408
|
+
checked={isSelected}
|
|
409
|
+
disabled={isDisabled}
|
|
410
|
+
class="segmented-control__input"
|
|
411
|
+
onchange={(e) => handleChange(item, e)}
|
|
412
|
+
onfocus={handleFocus}
|
|
413
|
+
onblur={handleBlur}
|
|
414
|
+
onkeydown={handleKeydown}
|
|
415
|
+
onkeyup={handleKeyup}
|
|
416
|
+
onclick={handleClick}
|
|
417
|
+
onmousedown={handleMouseDown}
|
|
418
|
+
onmouseup={handleMouseUp}
|
|
419
|
+
onmouseenter={handleMouseEnter}
|
|
420
|
+
onmouseleave={handleMouseLeave}
|
|
421
|
+
onmouseover={handleMouseOver}
|
|
422
|
+
onmouseout={handleMouseOut}
|
|
423
|
+
oncontextmenu={handleContextMenu}
|
|
424
|
+
onauxclick={handleAuxClick}
|
|
425
|
+
ontouchstart={handleTouchStart}
|
|
426
|
+
ontouchend={handleTouchEnd}
|
|
427
|
+
ontouchmove={handleTouchMove}
|
|
428
|
+
ontouchcancel={handleTouchCancel}
|
|
429
|
+
onpointerdown={handlePointerDown}
|
|
430
|
+
onpointerup={handlePointerUp}
|
|
431
|
+
onpointerenter={handlePointerEnter}
|
|
432
|
+
onpointerleave={handlePointerLeave}
|
|
433
|
+
onpointermove={handlePointerMove}
|
|
434
|
+
onpointercancel={handlePointerCancel}
|
|
435
|
+
data-testid="segmented-control-input"
|
|
436
|
+
data-value={item.value}
|
|
437
|
+
/>
|
|
438
|
+
<label
|
|
439
|
+
for={inputId}
|
|
440
|
+
class="segmented-control__label"
|
|
441
|
+
class:segmented-control__label--selected={isSelected}
|
|
442
|
+
class:segmented-control__label--first={index === 0}
|
|
443
|
+
class:segmented-control__label--last={index === items.length - 1}
|
|
444
|
+
aria-label={item.ariaLabel || item.label || undefined}
|
|
445
|
+
>
|
|
446
|
+
{#if item.icon}
|
|
447
|
+
<span class="segmented-control__icon">
|
|
448
|
+
<Icon
|
|
449
|
+
filled={iconFilled || isSelected}
|
|
450
|
+
weight={iconWeight}
|
|
451
|
+
grade={iconGrade}
|
|
452
|
+
opticalSize={effectiveIconSize}
|
|
453
|
+
variant={iconVariant}
|
|
454
|
+
>
|
|
455
|
+
{item.icon}
|
|
456
|
+
</Icon>
|
|
457
|
+
</span>
|
|
458
|
+
{/if}
|
|
459
|
+
{#if item.label}
|
|
460
|
+
<span class="segmented-control__label-text">{item.label}</span>
|
|
461
|
+
{/if}
|
|
462
|
+
</label>
|
|
463
|
+
</div>
|
|
464
|
+
{/each}
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
<style>.segmented-control {
|
|
468
|
+
display: inline-flex;
|
|
469
|
+
position: relative;
|
|
470
|
+
padding: var(--svelte-ui-segmented-control-base-padding);
|
|
471
|
+
background-color: var(--svelte-ui-segmented-control-base-bg);
|
|
472
|
+
border-radius: var(--svelte-ui-segmented-control-base-border-radius);
|
|
473
|
+
gap: var(--svelte-ui-segmented-control-gap);
|
|
474
|
+
box-sizing: border-box;
|
|
475
|
+
width: fit-content;
|
|
476
|
+
max-width: 100%;
|
|
477
|
+
overflow-x: auto;
|
|
478
|
+
overflow-y: visible;
|
|
479
|
+
-ms-overflow-style: none;
|
|
480
|
+
overscroll-behavior: contain;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.segmented-control::-webkit-scrollbar {
|
|
484
|
+
display: none;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.segmented-control--full-width {
|
|
488
|
+
width: 100%;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.segmented-control--full-width .segmented-control__item {
|
|
492
|
+
flex: 1;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.segmented-control--rounded {
|
|
496
|
+
border-radius: var(--svelte-ui-border-radius-rounded, 9999px);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.segmented-control--disabled {
|
|
500
|
+
opacity: var(--svelte-ui-button-disabled-opacity);
|
|
501
|
+
pointer-events: none;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.segmented-control__item {
|
|
505
|
+
position: relative;
|
|
506
|
+
flex: 0 1 auto;
|
|
507
|
+
display: flex;
|
|
508
|
+
align-items: stretch;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.segmented-control__input {
|
|
512
|
+
position: absolute;
|
|
513
|
+
width: 0;
|
|
514
|
+
height: 0;
|
|
515
|
+
margin: 0;
|
|
516
|
+
opacity: 0;
|
|
517
|
+
pointer-events: none;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.segmented-control__label {
|
|
521
|
+
display: flex;
|
|
522
|
+
align-items: center;
|
|
523
|
+
justify-content: center;
|
|
524
|
+
gap: 6px;
|
|
525
|
+
position: relative;
|
|
526
|
+
border: none;
|
|
527
|
+
background-color: transparent;
|
|
528
|
+
color: var(--svelte-ui-text-subtle-color, var(--svelte-ui-text-color));
|
|
529
|
+
white-space: nowrap;
|
|
530
|
+
cursor: pointer;
|
|
531
|
+
transition-property: background-color, color;
|
|
532
|
+
transition-duration: var(--svelte-ui-transition-duration, 0.2s);
|
|
533
|
+
transition-timing-function: ease;
|
|
534
|
+
outline: none;
|
|
535
|
+
font-family: inherit;
|
|
536
|
+
font-weight: 500;
|
|
537
|
+
line-height: 1;
|
|
538
|
+
box-sizing: border-box;
|
|
539
|
+
width: 100%;
|
|
540
|
+
text-align: center;
|
|
541
|
+
border-radius: var(--svelte-ui-segmented-control-button-radius);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.segmented-control--small .segmented-control__label {
|
|
545
|
+
height: var(--svelte-ui-segmented-control-button-height-sm);
|
|
546
|
+
font-size: var(--svelte-ui-segmented-control-font-size-sm);
|
|
547
|
+
padding: var(--svelte-ui-segmented-control-button-padding-sm);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.segmented-control--small .segmented-control__label:not(:has(.segmented-control__label-text)) {
|
|
551
|
+
width: var(--svelte-ui-segmented-control-button-height-sm);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.segmented-control--medium .segmented-control__label {
|
|
555
|
+
height: var(--svelte-ui-segmented-control-button-height);
|
|
556
|
+
font-size: var(--svelte-ui-segmented-control-font-size);
|
|
557
|
+
padding: var(--svelte-ui-segmented-control-button-padding);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.segmented-control--medium .segmented-control__label:not(:has(.segmented-control__label-text)) {
|
|
561
|
+
width: var(--svelte-ui-segmented-control-button-height);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.segmented-control--large .segmented-control__label {
|
|
565
|
+
height: var(--svelte-ui-segmented-control-button-height-lg);
|
|
566
|
+
font-size: var(--svelte-ui-segmented-control-font-size-lg);
|
|
567
|
+
padding: var(--svelte-ui-segmented-control-button-padding-lg);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.segmented-control--large .segmented-control__label:not(:has(.segmented-control__label-text)) {
|
|
571
|
+
width: var(--svelte-ui-segmented-control-button-height-lg);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.segmented-control--rounded .segmented-control__label {
|
|
575
|
+
border-radius: calc(var(--svelte-ui-border-radius-rounded, 9999px) - 2px);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
@media (hover: hover) {
|
|
579
|
+
.segmented-control__input:not(:disabled) + .segmented-control__label:hover:not(.segmented-control__label--selected) {
|
|
580
|
+
background-color: var(--svelte-ui-segmented-control-hover-overlay);
|
|
581
|
+
color: var(--svelte-ui-text-color);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
.segmented-control__input:focus-visible + .segmented-control__label {
|
|
585
|
+
outline: var(--svelte-ui-focus-outline-inner, 2px solid currentColor);
|
|
586
|
+
outline-offset: var(--svelte-ui-focus-outline-offset-inner, 2px);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.segmented-control__input:checked + .segmented-control__label,
|
|
590
|
+
.segmented-control__label--selected {
|
|
591
|
+
background-color: var(--svelte-ui-segmented-control-selected-bg);
|
|
592
|
+
color: var(--svelte-ui-segmented-control-selected-text-color, white);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.segmented-control:not(.segmented-control--disabled) .segmented-control__input:disabled + .segmented-control__label {
|
|
596
|
+
opacity: var(--svelte-ui-button-disabled-opacity);
|
|
597
|
+
cursor: not-allowed;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.segmented-control__icon {
|
|
601
|
+
display: flex;
|
|
602
|
+
align-items: center;
|
|
603
|
+
justify-content: center;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.segmented-control__label-text {
|
|
607
|
+
text-box-trim: trim-both;
|
|
608
|
+
text-box-edge: cap alphabetic;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/* Reduced motion */
|
|
612
|
+
.segmented-control--no-motion,
|
|
613
|
+
.segmented-control--no-motion .segmented-control__label {
|
|
614
|
+
transition-duration: 0.01s;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
@media (prefers-reduced-motion: reduce) {
|
|
618
|
+
.segmented-control,
|
|
619
|
+
.segmented-control__label {
|
|
620
|
+
transition-duration: 0.01s;
|
|
621
|
+
}
|
|
622
|
+
}</style>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
|
|
2
|
+
import type { SegmentedControlItem } from '../types/segmentedControlItem';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
items: SegmentedControlItem[];
|
|
5
|
+
value: string;
|
|
6
|
+
id?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
size?: 'small' | 'medium' | 'large';
|
|
9
|
+
fullWidth?: boolean;
|
|
10
|
+
color?: string;
|
|
11
|
+
rounded?: boolean;
|
|
12
|
+
iconFilled?: boolean;
|
|
13
|
+
iconWeight?: IconWeight;
|
|
14
|
+
iconGrade?: IconGrade;
|
|
15
|
+
iconOpticalSize?: IconOpticalSize;
|
|
16
|
+
iconVariant?: IconVariant;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
ariaLabel?: string;
|
|
19
|
+
ariaLabelledby?: string;
|
|
20
|
+
reducedMotion?: boolean;
|
|
21
|
+
onchange?: (value: string) => void;
|
|
22
|
+
onfocus?: Function;
|
|
23
|
+
onblur?: Function;
|
|
24
|
+
onkeydown?: Function;
|
|
25
|
+
onkeyup?: Function;
|
|
26
|
+
onclick?: Function;
|
|
27
|
+
onmousedown?: Function;
|
|
28
|
+
onmouseup?: Function;
|
|
29
|
+
onmouseenter?: Function;
|
|
30
|
+
onmouseleave?: Function;
|
|
31
|
+
onmouseover?: Function;
|
|
32
|
+
onmouseout?: Function;
|
|
33
|
+
oncontextmenu?: Function;
|
|
34
|
+
onauxclick?: Function;
|
|
35
|
+
ontouchstart?: Function;
|
|
36
|
+
ontouchend?: Function;
|
|
37
|
+
ontouchmove?: Function;
|
|
38
|
+
ontouchcancel?: Function;
|
|
39
|
+
onpointerdown?: Function;
|
|
40
|
+
onpointerup?: Function;
|
|
41
|
+
onpointerenter?: Function;
|
|
42
|
+
onpointerleave?: Function;
|
|
43
|
+
onpointermove?: Function;
|
|
44
|
+
onpointercancel?: Function;
|
|
45
|
+
[key: string]: any;
|
|
46
|
+
};
|
|
47
|
+
declare const SegmentedControl: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
48
|
+
type SegmentedControl = ReturnType<typeof SegmentedControl>;
|
|
49
|
+
export default SegmentedControl;
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export { default as PopupMenuButton } from './components/PopupMenuButton.svelte'
|
|
|
23
23
|
export { default as Radio } from './components/Radio.svelte';
|
|
24
24
|
export { default as RadioGroup } from './components/RadioGroup.svelte';
|
|
25
25
|
export { default as Select } from './components/Select.svelte';
|
|
26
|
+
export { default as SegmentedControl } from './components/SegmentedControl.svelte';
|
|
26
27
|
export { default as Slider } from './components/Slider.svelte';
|
|
27
28
|
export { default as Snackbar } from './components/Snackbar.svelte';
|
|
28
29
|
export { default as SnackbarItem } from './components/SnackbarItem.svelte';
|
|
@@ -40,3 +41,4 @@ export * from './utils/mobile';
|
|
|
40
41
|
export * from './utils/snackbar.svelte';
|
|
41
42
|
export * from './utils/style';
|
|
42
43
|
export type { MenuItem } from './types/menuItem';
|
|
44
|
+
export type { SegmentedControlItem } from './types/segmentedControlItem';
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export { default as PopupMenuButton } from './components/PopupMenuButton.svelte'
|
|
|
24
24
|
export { default as Radio } from './components/Radio.svelte';
|
|
25
25
|
export { default as RadioGroup } from './components/RadioGroup.svelte';
|
|
26
26
|
export { default as Select } from './components/Select.svelte';
|
|
27
|
+
export { default as SegmentedControl } from './components/SegmentedControl.svelte';
|
|
27
28
|
export { default as Slider } from './components/Slider.svelte';
|
|
28
29
|
export { default as Snackbar } from './components/Snackbar.svelte';
|
|
29
30
|
export { default as SnackbarItem } from './components/SnackbarItem.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|