@14ch/svelte-ui 0.0.68 → 0.0.70
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/import.css +22 -5
- package/dist/assets/styles/variables.scss +22 -5
- package/dist/components/ColorPicker.svelte +1 -1
- package/dist/components/Input.svelte +56 -94
- package/dist/components/MultiSelect.svelte +40 -36
- package/dist/components/MultiSelect.svelte.d.ts +2 -2
- package/dist/components/Select.svelte +492 -318
- package/dist/components/Select.svelte.d.ts +10 -9
- package/dist/components/StepNav.svelte +20 -6
- package/dist/components/StepNav.svelte.d.ts +4 -0
- package/dist/components/Textarea.svelte +64 -54
- package/package.json +1 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<!-- Select.svelte -->
|
|
2
2
|
|
|
3
3
|
<script lang="ts">
|
|
4
|
+
import { tick } from 'svelte';
|
|
4
5
|
import Icon from './Icon.svelte';
|
|
6
|
+
import Popup from './Popup.svelte';
|
|
7
|
+
import { announceSelection } from '../utils/accessibility';
|
|
5
8
|
import { t } from '../i18n';
|
|
6
9
|
import type {
|
|
7
10
|
FocusHandler,
|
|
@@ -11,59 +14,27 @@
|
|
|
11
14
|
PointerHandler,
|
|
12
15
|
BivariantValueHandler
|
|
13
16
|
} from '../types/callbackHandlers';
|
|
14
|
-
import type { Option } from '../types/options';
|
|
15
|
-
import type {
|
|
17
|
+
import type { Option, OptionValue } from '../types/options';
|
|
18
|
+
import type { CloseReason, PopupPosition } from '../types/propOptions';
|
|
16
19
|
|
|
17
20
|
// =========================================================================
|
|
18
21
|
// Props, States & Constants
|
|
19
22
|
// =========================================================================
|
|
20
|
-
export type SelectProps =
|
|
21
|
-
HTMLSelectAttributes,
|
|
22
|
-
// 独自の型で再定義するためベースの HTML 属性から除外
|
|
23
|
-
| 'value'
|
|
24
|
-
| 'id'
|
|
25
|
-
| 'tabindex'
|
|
26
|
-
| 'size'
|
|
27
|
-
// 独自ハンドラ型(callbackHandlers)で再定義するイベント系
|
|
28
|
-
| 'onfocus'
|
|
29
|
-
| 'onblur'
|
|
30
|
-
| 'onkeydown'
|
|
31
|
-
| 'onkeyup'
|
|
32
|
-
| 'onclick'
|
|
33
|
-
| 'onmousedown'
|
|
34
|
-
| 'onmouseup'
|
|
35
|
-
| 'onmouseenter'
|
|
36
|
-
| 'onmouseleave'
|
|
37
|
-
| 'onmouseover'
|
|
38
|
-
| 'onmouseout'
|
|
39
|
-
| 'oncontextmenu'
|
|
40
|
-
| 'onauxclick'
|
|
41
|
-
| 'ontouchstart'
|
|
42
|
-
| 'ontouchend'
|
|
43
|
-
| 'ontouchmove'
|
|
44
|
-
| 'ontouchcancel'
|
|
45
|
-
| 'onpointerdown'
|
|
46
|
-
| 'onpointerup'
|
|
47
|
-
| 'onpointerenter'
|
|
48
|
-
| 'onpointerleave'
|
|
49
|
-
| 'onpointermove'
|
|
50
|
-
| 'onpointercancel'
|
|
51
|
-
| 'onchange'
|
|
52
|
-
> & {
|
|
23
|
+
export type SelectProps = {
|
|
53
24
|
// 基本プロパティ
|
|
54
25
|
name?: string;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
26
|
+
/** Supports `bind:value`. */
|
|
27
|
+
value: OptionValue;
|
|
28
|
+
/** `{ label, value, disabled? }[]` or `'separator'` for dividers. */
|
|
29
|
+
options: (Option | 'separator')[];
|
|
58
30
|
|
|
59
31
|
// HTML属性系
|
|
60
32
|
id?: string | null;
|
|
61
33
|
ariaLabel?: string;
|
|
62
34
|
tabindex?: number | null;
|
|
63
35
|
placeholder?: string;
|
|
36
|
+
|
|
64
37
|
// スタイル/レイアウト
|
|
65
|
-
/** Number of visible rows (multi-line mode). @default null */
|
|
66
|
-
size?: number | null;
|
|
67
38
|
/** Renders inline. @default false */
|
|
68
39
|
inline?: boolean;
|
|
69
40
|
/** @default 'outline' */
|
|
@@ -76,6 +47,10 @@
|
|
|
76
47
|
disabled?: boolean;
|
|
77
48
|
required?: boolean;
|
|
78
49
|
|
|
50
|
+
// ポップアップ
|
|
51
|
+
/** @default 'bottom-left' */
|
|
52
|
+
position?: PopupPosition;
|
|
53
|
+
|
|
79
54
|
// フォーカスイベント
|
|
80
55
|
onfocus?: FocusHandler;
|
|
81
56
|
onblur?: FocusHandler;
|
|
@@ -110,7 +85,7 @@
|
|
|
110
85
|
onpointercancel?: PointerHandler;
|
|
111
86
|
|
|
112
87
|
// 入力イベント
|
|
113
|
-
onchange?: BivariantValueHandler<
|
|
88
|
+
onchange?: BivariantValueHandler<OptionValue>;
|
|
114
89
|
};
|
|
115
90
|
|
|
116
91
|
let {
|
|
@@ -126,7 +101,6 @@
|
|
|
126
101
|
placeholder = '',
|
|
127
102
|
|
|
128
103
|
// スタイル/レイアウト
|
|
129
|
-
size = null,
|
|
130
104
|
inline = false,
|
|
131
105
|
focusStyle = 'outline',
|
|
132
106
|
fullWidth = false,
|
|
@@ -137,6 +111,9 @@
|
|
|
137
111
|
disabled = false,
|
|
138
112
|
required = false,
|
|
139
113
|
|
|
114
|
+
// ポップアップ
|
|
115
|
+
position = 'bottom-left',
|
|
116
|
+
|
|
140
117
|
// フォーカスイベント
|
|
141
118
|
onfocus = () => {}, // No params for type inference
|
|
142
119
|
onblur = () => {}, // No params for type inference
|
|
@@ -171,151 +148,212 @@
|
|
|
171
148
|
onpointercancel = () => {}, // No params for type inference
|
|
172
149
|
|
|
173
150
|
// 入力イベント
|
|
174
|
-
onchange = () => {}
|
|
175
|
-
|
|
176
|
-
// その他
|
|
177
|
-
...restProps
|
|
151
|
+
onchange = () => {} // No params for type inference
|
|
178
152
|
}: SelectProps = $props();
|
|
179
153
|
|
|
180
|
-
|
|
181
|
-
|
|
154
|
+
const TYPEAHEAD_RESET_MS = 600;
|
|
155
|
+
|
|
156
|
+
let popupRef = $state<Popup>();
|
|
157
|
+
let triggerEl = $state<HTMLButtonElement>();
|
|
158
|
+
let listEl = $state<HTMLUListElement>();
|
|
159
|
+
let optionEls: HTMLLIElement[] = $state([]);
|
|
160
|
+
let isPopupOpen = $state(false);
|
|
161
|
+
let isFocused = $state(false);
|
|
162
|
+
let triggerWidth = $state(0);
|
|
163
|
+
/** ハイライト中の選択肢の options 内インデックス(未選択は -1) */
|
|
164
|
+
let activeIndex = $state(-1);
|
|
165
|
+
let typeaheadBuffer = '';
|
|
166
|
+
let typeaheadTimer: ReturnType<typeof setTimeout> | undefined;
|
|
182
167
|
|
|
183
168
|
// =========================================================================
|
|
184
|
-
//
|
|
169
|
+
// $derived
|
|
185
170
|
// =========================================================================
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
171
|
+
// アイコンの領域は customStyle で消されないよう、customStyle より後ろに指定する
|
|
172
|
+
const triggerStyle = $derived(
|
|
173
|
+
[customStyle, `padding-right: var(--svelte-ui-select-icon-space${inline ? '-inline' : ''})`]
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
.join('; ')
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const fallbackId = `select-${Math.random().toString(36).substring(2, 15)}`;
|
|
179
|
+
const baseId = $derived(id ?? fallbackId);
|
|
180
|
+
const listboxId = $derived(`${baseId}-listbox`);
|
|
181
|
+
const accessibleName = $derived(ariaLabel ?? t('select.accessibleName'));
|
|
182
|
+
|
|
183
|
+
const selectedOption = $derived(
|
|
184
|
+
options.find((option) => option !== 'separator' && option.value === value) as Option | undefined
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
/** キーボード操作で移動できる選択肢の options 内インデックス */
|
|
188
|
+
const navigableIndexes = $derived(
|
|
189
|
+
options.reduce<number[]>((indexes, option, index) => {
|
|
190
|
+
if (option !== 'separator' && !option.disabled) {
|
|
191
|
+
indexes.push(index);
|
|
192
|
+
}
|
|
193
|
+
return indexes;
|
|
194
|
+
}, [])
|
|
195
|
+
);
|
|
197
196
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const handleKeyup = (event: KeyboardEvent) => {
|
|
204
|
-
if (disabled) return;
|
|
205
|
-
onkeyup?.(event);
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
// マウスイベント
|
|
209
|
-
const handleClick = (event: MouseEvent) => {
|
|
210
|
-
if (disabled) return;
|
|
211
|
-
onclick?.(event);
|
|
212
|
-
};
|
|
197
|
+
// =========================================================================
|
|
198
|
+
// Methods
|
|
199
|
+
// =========================================================================
|
|
200
|
+
const getOptionId = (index: number): string => `${baseId}-option-${index}`;
|
|
213
201
|
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
202
|
+
const getOptionAt = (index: number): Option | undefined => {
|
|
203
|
+
const option = options[index];
|
|
204
|
+
return option === 'separator' ? undefined : option;
|
|
217
205
|
};
|
|
218
206
|
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
207
|
+
const setActiveIndex = async (index: number) => {
|
|
208
|
+
activeIndex = index;
|
|
209
|
+
await tick();
|
|
210
|
+
optionEls[index]?.scrollIntoView({ block: 'nearest' });
|
|
222
211
|
};
|
|
223
212
|
|
|
224
|
-
const
|
|
225
|
-
if (
|
|
226
|
-
|
|
213
|
+
const moveActiveIndex = (step: number) => {
|
|
214
|
+
if (navigableIndexes.length === 0) return;
|
|
215
|
+
const current = navigableIndexes.indexOf(activeIndex);
|
|
216
|
+
const next =
|
|
217
|
+
current === -1
|
|
218
|
+
? step > 0
|
|
219
|
+
? 0
|
|
220
|
+
: navigableIndexes.length - 1
|
|
221
|
+
: (current + step + navigableIndexes.length) % navigableIndexes.length;
|
|
222
|
+
setActiveIndex(navigableIndexes[next]);
|
|
227
223
|
};
|
|
228
224
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
225
|
+
/** 入力した文字で始まる選択肢へ移動する(ネイティブ select 相当の挙動) */
|
|
226
|
+
const moveByTypeahead = (key: string) => {
|
|
227
|
+
clearTimeout(typeaheadTimer);
|
|
228
|
+
typeaheadBuffer += key.toLowerCase();
|
|
229
|
+
typeaheadTimer = setTimeout(() => {
|
|
230
|
+
typeaheadBuffer = '';
|
|
231
|
+
}, TYPEAHEAD_RESET_MS);
|
|
232
|
+
|
|
233
|
+
// 1文字目は現在位置の次から探し、同じ文字の連打で候補を巡回できるようにする
|
|
234
|
+
const offset = typeaheadBuffer.length === 1 ? navigableIndexes.indexOf(activeIndex) + 1 : 0;
|
|
235
|
+
const searchOrder = [...navigableIndexes.slice(offset), ...navigableIndexes.slice(0, offset)];
|
|
236
|
+
const matched = searchOrder.find((index) =>
|
|
237
|
+
getOptionAt(index)?.label.toLowerCase().startsWith(typeaheadBuffer)
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
if (matched !== undefined) {
|
|
241
|
+
setActiveIndex(matched);
|
|
242
|
+
}
|
|
232
243
|
};
|
|
233
244
|
|
|
234
|
-
const
|
|
235
|
-
if (disabled) return;
|
|
236
|
-
onmouseover?.(event);
|
|
237
|
-
};
|
|
245
|
+
const selectOption = (option: Option) => {
|
|
246
|
+
if (disabled || option.disabled) return;
|
|
238
247
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
248
|
+
value = option.value;
|
|
249
|
+
announceSelection(option.label);
|
|
250
|
+
onchange?.(value);
|
|
251
|
+
popupRef?.close();
|
|
242
252
|
};
|
|
243
253
|
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
254
|
+
const handleFocus = (event: FocusEvent) => {
|
|
255
|
+
isFocused = true;
|
|
256
|
+
onfocus?.(event);
|
|
247
257
|
};
|
|
248
258
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
259
|
+
const handleBlur = (event: FocusEvent) => {
|
|
260
|
+
isFocused = false;
|
|
261
|
+
onblur?.(event);
|
|
252
262
|
};
|
|
253
263
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
ontouchstart?.(event);
|
|
264
|
+
const handleTriggerClick = (event: MouseEvent) => {
|
|
265
|
+
popupRef?.toggle();
|
|
266
|
+
onclick?.(event);
|
|
258
267
|
};
|
|
259
268
|
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
ontouchend?.(event);
|
|
263
|
-
};
|
|
269
|
+
const handleTriggerKeydown = (event: KeyboardEvent) => {
|
|
270
|
+
onkeydown?.(event);
|
|
264
271
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
272
|
+
switch (event.key) {
|
|
273
|
+
case 'Enter':
|
|
274
|
+
case ' ':
|
|
275
|
+
case 'ArrowDown':
|
|
276
|
+
case 'ArrowUp':
|
|
277
|
+
event.preventDefault();
|
|
278
|
+
popupRef?.open();
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
268
281
|
};
|
|
269
282
|
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
283
|
+
const handleListClick = (event: MouseEvent) => {
|
|
284
|
+
const optionEl = (event.target as HTMLElement | null)?.closest<HTMLElement>(
|
|
285
|
+
'[data-option-index]'
|
|
286
|
+
);
|
|
287
|
+
if (!optionEl) return;
|
|
274
288
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (disabled) return;
|
|
278
|
-
onpointerdown?.(event);
|
|
289
|
+
const option = getOptionAt(Number(optionEl.dataset.optionIndex));
|
|
290
|
+
if (option) selectOption(option);
|
|
279
291
|
};
|
|
280
292
|
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
293
|
+
const handleListKeydown = (event: KeyboardEvent) => {
|
|
294
|
+
switch (event.key) {
|
|
295
|
+
case 'ArrowDown':
|
|
296
|
+
event.preventDefault();
|
|
297
|
+
moveActiveIndex(1);
|
|
298
|
+
break;
|
|
299
|
+
case 'ArrowUp':
|
|
300
|
+
event.preventDefault();
|
|
301
|
+
moveActiveIndex(-1);
|
|
302
|
+
break;
|
|
303
|
+
case 'Home':
|
|
304
|
+
event.preventDefault();
|
|
305
|
+
if (navigableIndexes.length > 0) setActiveIndex(navigableIndexes[0]);
|
|
306
|
+
break;
|
|
307
|
+
case 'End':
|
|
308
|
+
event.preventDefault();
|
|
309
|
+
if (navigableIndexes.length > 0)
|
|
310
|
+
setActiveIndex(navigableIndexes[navigableIndexes.length - 1]);
|
|
311
|
+
break;
|
|
312
|
+
case 'Enter':
|
|
313
|
+
case ' ': {
|
|
314
|
+
event.preventDefault();
|
|
315
|
+
const option = getOptionAt(activeIndex);
|
|
316
|
+
if (option) selectOption(option);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
case 'Tab':
|
|
320
|
+
popupRef?.close();
|
|
321
|
+
break;
|
|
322
|
+
default:
|
|
323
|
+
if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
324
|
+
event.preventDefault();
|
|
325
|
+
moveByTypeahead(event.key);
|
|
326
|
+
}
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
284
329
|
};
|
|
285
330
|
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
onpointerenter?.(event);
|
|
289
|
-
};
|
|
331
|
+
const handlePopupOpen = () => {
|
|
332
|
+
triggerWidth = triggerEl?.offsetWidth ?? 0;
|
|
290
333
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
334
|
+
const selectedIndex = options.findIndex(
|
|
335
|
+
(option) => option !== 'separator' && option.value === value && !option.disabled
|
|
336
|
+
);
|
|
337
|
+
setActiveIndex(selectedIndex >= 0 ? selectedIndex : (navigableIndexes[0] ?? -1));
|
|
295
338
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
339
|
+
tick().then(() => {
|
|
340
|
+
listEl?.focus();
|
|
341
|
+
});
|
|
299
342
|
};
|
|
300
343
|
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
344
|
+
const handlePopupClose = (reason: CloseReason) => {
|
|
345
|
+
activeIndex = -1;
|
|
346
|
+
typeaheadBuffer = '';
|
|
347
|
+
clearTimeout(typeaheadTimer);
|
|
305
348
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
onchange?.(value);
|
|
309
|
-
// 選択完了後にフォーカスを解除
|
|
310
|
-
if (selectRef) {
|
|
311
|
-
selectRef.blur();
|
|
349
|
+
if (reason !== 'outside') {
|
|
350
|
+
triggerEl?.focus();
|
|
312
351
|
}
|
|
313
352
|
};
|
|
314
353
|
</script>
|
|
315
354
|
|
|
316
355
|
<div
|
|
317
|
-
class="select
|
|
318
|
-
select--focus-{focusStyle}"
|
|
356
|
+
class="select select--focus-{focusStyle}"
|
|
319
357
|
class:select--inline={inline}
|
|
320
358
|
class:select--full-width={fullWidth}
|
|
321
359
|
class:select--disabled={disabled}
|
|
@@ -323,198 +361,334 @@ select--focus-{focusStyle}"
|
|
|
323
361
|
class:select--rounded={rounded}
|
|
324
362
|
data-testid="select"
|
|
325
363
|
>
|
|
326
|
-
<
|
|
327
|
-
bind:this={
|
|
364
|
+
<button
|
|
365
|
+
bind:this={triggerEl}
|
|
328
366
|
{id}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
{
|
|
333
|
-
|
|
367
|
+
type="button"
|
|
368
|
+
class="select__trigger"
|
|
369
|
+
role="combobox"
|
|
370
|
+
aria-expanded={isPopupOpen}
|
|
371
|
+
aria-haspopup="listbox"
|
|
372
|
+
aria-controls={isPopupOpen ? listboxId : undefined}
|
|
373
|
+
aria-label={accessibleName}
|
|
374
|
+
aria-required={required ? 'true' : undefined}
|
|
334
375
|
{tabindex}
|
|
335
|
-
{
|
|
336
|
-
style={
|
|
376
|
+
{disabled}
|
|
377
|
+
style={triggerStyle}
|
|
337
378
|
onfocus={handleFocus}
|
|
338
379
|
onblur={handleBlur}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
onmousedown
|
|
343
|
-
onmouseup
|
|
344
|
-
onmouseenter
|
|
345
|
-
onmouseleave
|
|
346
|
-
onmouseover
|
|
347
|
-
onmouseout
|
|
348
|
-
oncontextmenu
|
|
349
|
-
onauxclick
|
|
350
|
-
ontouchstart
|
|
351
|
-
ontouchend
|
|
352
|
-
ontouchmove
|
|
353
|
-
ontouchcancel
|
|
354
|
-
onpointerdown
|
|
355
|
-
onpointerup
|
|
356
|
-
onpointerenter
|
|
357
|
-
onpointerleave
|
|
358
|
-
onpointermove
|
|
359
|
-
onpointercancel
|
|
360
|
-
onchange={handleChange}
|
|
361
|
-
{...restProps}
|
|
380
|
+
onclick={handleTriggerClick}
|
|
381
|
+
onkeydown={handleTriggerKeydown}
|
|
382
|
+
{onkeyup}
|
|
383
|
+
{onmousedown}
|
|
384
|
+
{onmouseup}
|
|
385
|
+
{onmouseenter}
|
|
386
|
+
{onmouseleave}
|
|
387
|
+
{onmouseover}
|
|
388
|
+
{onmouseout}
|
|
389
|
+
{oncontextmenu}
|
|
390
|
+
{onauxclick}
|
|
391
|
+
{ontouchstart}
|
|
392
|
+
{ontouchend}
|
|
393
|
+
{ontouchmove}
|
|
394
|
+
{ontouchcancel}
|
|
395
|
+
{onpointerdown}
|
|
396
|
+
{onpointerup}
|
|
397
|
+
{onpointerenter}
|
|
398
|
+
{onpointerleave}
|
|
399
|
+
{onpointermove}
|
|
400
|
+
{onpointercancel}
|
|
362
401
|
>
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
402
|
+
<span class="select__inner">
|
|
403
|
+
{#if selectedOption}
|
|
404
|
+
<span class="select__display-text">{selectedOption.label}</span>
|
|
405
|
+
{:else if placeholder}
|
|
406
|
+
<span class="select__placeholder">{placeholder}</span>
|
|
407
|
+
{/if}
|
|
408
|
+
<span class="select__dropdown-icon" aria-hidden="true">
|
|
409
|
+
<Icon>arrow_drop_down</Icon>
|
|
410
|
+
</span>
|
|
411
|
+
</span>
|
|
412
|
+
</button>
|
|
413
|
+
|
|
414
|
+
<!-- フォーム送信用(ネイティブ select を使わないため値を hidden で送る) -->
|
|
415
|
+
{#if name}
|
|
416
|
+
<input type="hidden" {name} value={value ?? ''} />
|
|
417
|
+
{/if}
|
|
418
|
+
|
|
419
|
+
<Popup
|
|
420
|
+
bind:this={popupRef}
|
|
421
|
+
bind:isOpen={isPopupOpen}
|
|
422
|
+
anchorElement={triggerEl}
|
|
423
|
+
{position}
|
|
424
|
+
role="presentation"
|
|
425
|
+
mobileFullscreen={true}
|
|
426
|
+
onOpen={handlePopupOpen}
|
|
427
|
+
onClose={handlePopupClose}
|
|
428
|
+
margin={4}
|
|
429
|
+
id={id ? `${id}-popup` : undefined}
|
|
430
|
+
>
|
|
431
|
+
<ul
|
|
432
|
+
bind:this={listEl}
|
|
433
|
+
id={listboxId}
|
|
434
|
+
class="select__options"
|
|
435
|
+
role="listbox"
|
|
436
|
+
tabindex="-1"
|
|
437
|
+
aria-label={accessibleName}
|
|
438
|
+
aria-activedescendant={activeIndex >= 0 ? getOptionId(activeIndex) : undefined}
|
|
439
|
+
style:--internal-select-trigger-width="{triggerWidth}px"
|
|
440
|
+
onclick={handleListClick}
|
|
441
|
+
onkeydown={handleListKeydown}
|
|
442
|
+
>
|
|
443
|
+
{#each options as option, index (option === 'separator' ? `separator-${index}` : option.value)}
|
|
444
|
+
{#if option === 'separator'}
|
|
445
|
+
<li class="select__separator" role="separator"></li>
|
|
446
|
+
{:else}
|
|
447
|
+
<li
|
|
448
|
+
bind:this={optionEls[index]}
|
|
449
|
+
id={getOptionId(index)}
|
|
450
|
+
data-option-index={index}
|
|
451
|
+
class="select__option"
|
|
452
|
+
class:select__option--selected={option.value === value}
|
|
453
|
+
class:select__option--active={index === activeIndex}
|
|
454
|
+
class:select__option--disabled={option.disabled}
|
|
455
|
+
role="option"
|
|
456
|
+
aria-selected={option.value === value}
|
|
457
|
+
aria-disabled={option.disabled ? 'true' : undefined}
|
|
458
|
+
onmouseenter={() => {
|
|
459
|
+
if (!option.disabled) activeIndex = index;
|
|
460
|
+
}}
|
|
461
|
+
>
|
|
462
|
+
<span class="select__option-check">
|
|
463
|
+
{#if option.value === value}
|
|
464
|
+
<Icon size="var(--svelte-ui-select-option-check-size)">check</Icon>
|
|
465
|
+
{/if}
|
|
466
|
+
</span>
|
|
467
|
+
<span class="select__option-label">{option.label}</span>
|
|
468
|
+
</li>
|
|
469
|
+
{/if}
|
|
470
|
+
{/each}
|
|
471
|
+
</ul>
|
|
472
|
+
</Popup>
|
|
380
473
|
</div>
|
|
381
474
|
|
|
382
|
-
<style
|
|
383
|
-
|
|
475
|
+
<style>@charset "UTF-8";
|
|
476
|
+
/* =============================================
|
|
384
477
|
* 基本構造・レイアウト
|
|
385
478
|
* ============================================= */
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
*
|
|
479
|
+
.select {
|
|
480
|
+
display: inline-block;
|
|
481
|
+
position: relative;
|
|
482
|
+
width: auto;
|
|
483
|
+
max-width: 100%;
|
|
484
|
+
vertical-align: top;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/* =============================================
|
|
488
|
+
* トリガーボタン
|
|
396
489
|
* ============================================= */
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
490
|
+
.select__trigger {
|
|
491
|
+
display: flex;
|
|
492
|
+
align-items: center;
|
|
493
|
+
width: 100%;
|
|
494
|
+
min-height: var(--svelte-ui-select-height);
|
|
495
|
+
padding: var(--svelte-ui-select-padding);
|
|
496
|
+
background: transparent;
|
|
497
|
+
border: none;
|
|
498
|
+
font-family: inherit;
|
|
499
|
+
font-size: inherit;
|
|
500
|
+
font-weight: inherit;
|
|
501
|
+
color: inherit;
|
|
502
|
+
line-height: inherit;
|
|
503
|
+
text-align: left;
|
|
504
|
+
cursor: pointer;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.select__inner {
|
|
508
|
+
flex: 1;
|
|
509
|
+
align-self: stretch;
|
|
510
|
+
display: flex;
|
|
511
|
+
align-items: center;
|
|
512
|
+
min-width: 0;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.select__display-text,
|
|
516
|
+
.select__placeholder {
|
|
517
|
+
flex: 1;
|
|
518
|
+
display: block;
|
|
519
|
+
overflow: hidden;
|
|
520
|
+
text-overflow: ellipsis;
|
|
521
|
+
white-space: nowrap;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.select__placeholder {
|
|
525
|
+
color: var(--svelte-ui-select-placeholder-color);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.select__dropdown-icon {
|
|
529
|
+
display: flex;
|
|
530
|
+
justify-content: center;
|
|
531
|
+
align-items: center;
|
|
532
|
+
position: absolute;
|
|
533
|
+
top: 50%;
|
|
534
|
+
right: 4px;
|
|
535
|
+
transform: translateY(-50%);
|
|
536
|
+
width: 32px;
|
|
537
|
+
height: 32px;
|
|
538
|
+
color: var(--svelte-ui-select-dropdown-icon-color);
|
|
539
|
+
pointer-events: none;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/* =============================================
|
|
432
543
|
* レイアウトバリエーション
|
|
433
544
|
* ============================================= */
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
/* =============================================
|
|
439
|
-
* プレースホルダー・オプション表示
|
|
440
|
-
* ============================================= */
|
|
441
|
-
option[value=''] {
|
|
442
|
-
color: var(--svelte-ui-select-placeholder-color);
|
|
443
|
-
}
|
|
545
|
+
.select--full-width {
|
|
546
|
+
width: 100%;
|
|
547
|
+
}
|
|
444
548
|
|
|
445
|
-
|
|
549
|
+
/* =============================================
|
|
446
550
|
* フォーカス効果バリエーション
|
|
447
551
|
* ============================================= */
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
* 状態管理(disabled
|
|
552
|
+
.select--focus-outline .select__trigger:focus-visible {
|
|
553
|
+
outline: var(--svelte-ui-focus-outline-inner);
|
|
554
|
+
outline-offset: var(--svelte-ui-focus-outline-offset-inner);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/* デザインバリアントの背景指定より詳細度を上げるため .select を併記 */
|
|
558
|
+
.select.select--focus-background .select__trigger:focus-visible {
|
|
559
|
+
outline: none;
|
|
560
|
+
background: var(--svelte-ui-hover-overlay);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.select--focus-none .select__trigger:focus-visible {
|
|
564
|
+
outline: none;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/* =============================================
|
|
568
|
+
* 状態管理(disabled等)
|
|
465
569
|
* ============================================= */
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
opacity: 1;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/* =============================================
|
|
570
|
+
.select--disabled {
|
|
571
|
+
opacity: var(--svelte-ui-input-disabled-opacity);
|
|
572
|
+
cursor: not-allowed;
|
|
573
|
+
}
|
|
574
|
+
.select--disabled .select__trigger {
|
|
575
|
+
cursor: not-allowed;
|
|
576
|
+
pointer-events: none;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/* =============================================
|
|
480
580
|
* デザインバリアント:default
|
|
481
581
|
* ============================================= */
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/* =============================================
|
|
582
|
+
.select:not(.select--inline) .select__trigger {
|
|
583
|
+
min-height: var(--svelte-ui-select-height);
|
|
584
|
+
background-color: var(--svelte-ui-select-bg);
|
|
585
|
+
box-shadow: 0 0 0 var(--svelte-ui-border-width) inset var(--svelte-ui-select-border-color);
|
|
586
|
+
border: none;
|
|
587
|
+
border-radius: var(--svelte-ui-select-border-radius);
|
|
588
|
+
font-size: var(--svelte-ui-font-size);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/* =============================================
|
|
495
592
|
* デザインバリアント:inline
|
|
496
593
|
* ============================================= */
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
594
|
+
.select.select--inline .select__trigger {
|
|
595
|
+
padding: inherit;
|
|
596
|
+
background: transparent;
|
|
597
|
+
border: none;
|
|
598
|
+
border-radius: 0;
|
|
599
|
+
color: inherit;
|
|
600
|
+
min-height: auto;
|
|
601
|
+
line-height: inherit;
|
|
602
|
+
}
|
|
603
|
+
.select.select--inline .select__inner {
|
|
604
|
+
align-self: auto;
|
|
605
|
+
align-items: flex-start;
|
|
606
|
+
min-height: 1lh;
|
|
607
|
+
}
|
|
608
|
+
.select.select--inline .select__dropdown-icon {
|
|
609
|
+
top: calc((1lh - 32px) / 2);
|
|
610
|
+
right: 0;
|
|
611
|
+
transform: none;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/* =============================================
|
|
515
615
|
* デザインバリアント:rounded
|
|
516
616
|
* ============================================= */
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
617
|
+
.select--rounded:not(.select--inline) .select__trigger {
|
|
618
|
+
border-radius: var(--svelte-ui-select-border-radius-rounded);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/* =============================================
|
|
622
|
+
* オプションリスト(ポップアップ内)
|
|
623
|
+
* ============================================= */
|
|
624
|
+
.select__options {
|
|
625
|
+
list-style: none;
|
|
626
|
+
margin: 0;
|
|
627
|
+
padding: 0;
|
|
628
|
+
/* トリガー幅に合わせつつ、トリガーが狭いときはテーマの下限を保つ */
|
|
629
|
+
min-width: max(var(--internal-select-trigger-width, 0px), var(--svelte-ui-select-options-min-width));
|
|
630
|
+
max-height: var(--svelte-ui-select-options-max-height);
|
|
631
|
+
overflow-y: auto;
|
|
632
|
+
outline: none;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.select__option {
|
|
636
|
+
display: flex;
|
|
637
|
+
align-items: center;
|
|
638
|
+
gap: var(--svelte-ui-select-option-gap);
|
|
639
|
+
padding: var(--svelte-ui-select-option-padding);
|
|
640
|
+
color: var(--svelte-ui-text-color);
|
|
641
|
+
white-space: nowrap;
|
|
642
|
+
cursor: pointer;
|
|
643
|
+
transition: background-color var(--svelte-ui-transition-duration) ease;
|
|
644
|
+
}
|
|
645
|
+
@media (hover: hover) {
|
|
646
|
+
.select__option:hover {
|
|
647
|
+
background-color: var(--svelte-ui-select-option-hover-bg);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
.select__option.select__option--active {
|
|
651
|
+
background-color: var(--svelte-ui-select-option-hover-bg);
|
|
652
|
+
}
|
|
653
|
+
.select__option.select__option--selected {
|
|
654
|
+
background-color: var(--svelte-ui-select-option-selected-bg);
|
|
655
|
+
}
|
|
656
|
+
.select__option.select__option--disabled {
|
|
657
|
+
opacity: var(--svelte-ui-input-disabled-opacity);
|
|
658
|
+
cursor: not-allowed;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.select__option-check {
|
|
662
|
+
display: flex;
|
|
663
|
+
justify-content: center;
|
|
664
|
+
align-items: center;
|
|
665
|
+
flex: 0 0 auto;
|
|
666
|
+
width: var(--svelte-ui-select-option-check-size);
|
|
667
|
+
height: var(--svelte-ui-select-option-check-size);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.select__separator {
|
|
671
|
+
height: 0;
|
|
672
|
+
margin: var(--svelte-ui-select-separator-margin);
|
|
673
|
+
border-bottom: solid 1px var(--svelte-ui-select-separator-color);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/* =============================================
|
|
677
|
+
* モバイル(タッチターゲット確保)
|
|
678
|
+
* ============================================= */
|
|
679
|
+
@media (max-width: 768px) {
|
|
680
|
+
.select__option {
|
|
681
|
+
min-height: var(--svelte-ui-touch-target);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
:global(.popup--fullscreen) .select__option {
|
|
685
|
+
min-height: var(--svelte-ui-touch-target-lg);
|
|
686
|
+
padding: 16px 24px;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/* Reduced motion support */
|
|
690
|
+
@media (prefers-reduced-motion: reduce) {
|
|
691
|
+
.select__option {
|
|
692
|
+
transition: none;
|
|
693
|
+
}
|
|
694
|
+
}</style>
|