@14ch/svelte-ui 0.0.68 → 0.0.69
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/MultiSelect.svelte +31 -27
- package/dist/components/MultiSelect.svelte.d.ts +2 -2
- package/dist/components/Select.svelte +491 -317
- package/dist/components/Select.svelte.d.ts +10 -9
- package/package.json +1 -1
|
@@ -139,10 +139,13 @@
|
|
|
139
139
|
* ==================================== */
|
|
140
140
|
/* 基本テキスト色の定義 */
|
|
141
141
|
--svelte-ui-text-color: var(--base-800-transparent);
|
|
142
|
-
--svelte-ui-text-
|
|
142
|
+
--svelte-ui-text-weak-color: var(--base-500-transparent);
|
|
143
|
+
/* weak のエイリアス。weak から派生させることでどちらを上書きしても効く */
|
|
144
|
+
--svelte-ui-text-subtle-color: var(--svelte-ui-text-weak-color);
|
|
143
145
|
--svelte-ui-text-on-filled-color: #ffffff;
|
|
144
146
|
--svelte-ui-text-inverse-color: var(--white-800-transparent);
|
|
145
|
-
--svelte-ui-text-inverse-
|
|
147
|
+
--svelte-ui-text-inverse-weak-color: var(--white-500-transparent);
|
|
148
|
+
--svelte-ui-text-inverse-subtle-color: var(--svelte-ui-text-inverse-weak-color);
|
|
146
149
|
--svelte-ui-text-placeholder-color: var(--base-500-transparent);
|
|
147
150
|
/* ボーダーカラー */
|
|
148
151
|
--svelte-ui-border-color: var(--base-200-transparent);
|
|
@@ -179,10 +182,12 @@
|
|
|
179
182
|
[data-theme=dark] {
|
|
180
183
|
/* 基本テキスト色の定義 */
|
|
181
184
|
--svelte-ui-text-color: var(--white-800-transparent);
|
|
182
|
-
--svelte-ui-text-
|
|
185
|
+
--svelte-ui-text-weak-color: var(--white-500-transparent);
|
|
186
|
+
--svelte-ui-text-subtle-color: var(--svelte-ui-text-weak-color);
|
|
183
187
|
--svelte-ui-text-on-filled-color: #ffffff;
|
|
184
188
|
--svelte-ui-text-inverse-color: var(--base-800-transparent);
|
|
185
|
-
--svelte-ui-text-inverse-
|
|
189
|
+
--svelte-ui-text-inverse-weak-color: var(--base-500-transparent);
|
|
190
|
+
--svelte-ui-text-inverse-subtle-color: var(--svelte-ui-text-inverse-weak-color);
|
|
186
191
|
--svelte-ui-text-placeholder-color: var(--white-500-transparent);
|
|
187
192
|
/* ボーダーカラー */
|
|
188
193
|
--svelte-ui-border-color: var(--white-200-transparent);
|
|
@@ -460,6 +465,15 @@
|
|
|
460
465
|
--svelte-ui-select-border-color: var(--svelte-ui-border-weak-color);
|
|
461
466
|
--svelte-ui-select-dropdown-icon-color: var(--svelte-ui-dropdown-icon-color);
|
|
462
467
|
--svelte-ui-select-placeholder-color: var(--svelte-ui-text-placeholder-color);
|
|
468
|
+
--svelte-ui-select-options-min-width: 160px;
|
|
469
|
+
--svelte-ui-select-options-max-height: 280px;
|
|
470
|
+
--svelte-ui-select-option-padding: 8px 12px;
|
|
471
|
+
--svelte-ui-select-option-gap: 4px;
|
|
472
|
+
--svelte-ui-select-option-check-size: 18px;
|
|
473
|
+
--svelte-ui-select-option-hover-bg: var(--svelte-ui-hover-overlay);
|
|
474
|
+
--svelte-ui-select-option-selected-bg: var(--svelte-ui-select-overlay);
|
|
475
|
+
--svelte-ui-select-separator-margin: 4px 0;
|
|
476
|
+
--svelte-ui-select-separator-color: var(--svelte-ui-border-color);
|
|
463
477
|
/* Checkbox */
|
|
464
478
|
--svelte-ui-checkbox-size: 16px;
|
|
465
479
|
--svelte-ui-checkbox-size-sm: 14px;
|
|
@@ -789,7 +803,7 @@
|
|
|
789
803
|
[data-theme=dark] {
|
|
790
804
|
/* ===== 基本色の上書き ===== */
|
|
791
805
|
--svelte-ui-text-color: CanvasText;
|
|
792
|
-
--svelte-ui-text-
|
|
806
|
+
--svelte-ui-text-weak-color: CanvasText;
|
|
793
807
|
--svelte-ui-surface-accent-color: CanvasText;
|
|
794
808
|
--svelte-ui-text-on-filled-color: HighlightText;
|
|
795
809
|
--svelte-ui-text-placeholder-color: CanvasText;
|
|
@@ -827,6 +841,7 @@
|
|
|
827
841
|
--svelte-ui-select-border-color: CanvasText;
|
|
828
842
|
--svelte-ui-select-text-color: CanvasText;
|
|
829
843
|
--svelte-ui-select-dropdown-icon-color: CanvasText;
|
|
844
|
+
--svelte-ui-select-separator-color: CanvasText;
|
|
830
845
|
--svelte-ui-colorpicker-border-color: CanvasText;
|
|
831
846
|
--svelte-ui-colorpicker-trigger-border-color: CanvasText;
|
|
832
847
|
--svelte-ui-colorpicker-bg: Canvas;
|
|
@@ -875,6 +890,8 @@
|
|
|
875
890
|
/* ===== オーバーレイ・インタラクション色の上書き ===== */
|
|
876
891
|
--svelte-ui-hover-overlay: Highlight;
|
|
877
892
|
--svelte-ui-select-overlay: Highlight;
|
|
893
|
+
--svelte-ui-select-option-hover-bg: Highlight;
|
|
894
|
+
--svelte-ui-select-option-selected-bg: Highlight;
|
|
878
895
|
--svelte-ui-combobox-option-hover-bg: Highlight;
|
|
879
896
|
--svelte-ui-combobox-option-selected-bg: Highlight;
|
|
880
897
|
/* ===== アイコンボタンの上書き ===== */
|
|
@@ -141,10 +141,13 @@
|
|
|
141
141
|
|
|
142
142
|
/* 基本テキスト色の定義 */
|
|
143
143
|
--svelte-ui-text-color: var(--base-800-transparent);
|
|
144
|
-
--svelte-ui-text-
|
|
144
|
+
--svelte-ui-text-weak-color: var(--base-500-transparent);
|
|
145
|
+
/* weak のエイリアス。weak から派生させることでどちらを上書きしても効く */
|
|
146
|
+
--svelte-ui-text-subtle-color: var(--svelte-ui-text-weak-color);
|
|
145
147
|
--svelte-ui-text-on-filled-color: #ffffff;
|
|
146
148
|
--svelte-ui-text-inverse-color: var(--white-800-transparent);
|
|
147
|
-
--svelte-ui-text-inverse-
|
|
149
|
+
--svelte-ui-text-inverse-weak-color: var(--white-500-transparent);
|
|
150
|
+
--svelte-ui-text-inverse-subtle-color: var(--svelte-ui-text-inverse-weak-color);
|
|
148
151
|
--svelte-ui-text-placeholder-color: var(--base-500-transparent);
|
|
149
152
|
|
|
150
153
|
/* ボーダーカラー */
|
|
@@ -193,10 +196,12 @@
|
|
|
193
196
|
[data-theme='dark'] {
|
|
194
197
|
/* 基本テキスト色の定義 */
|
|
195
198
|
--svelte-ui-text-color: var(--white-800-transparent);
|
|
196
|
-
--svelte-ui-text-
|
|
199
|
+
--svelte-ui-text-weak-color: var(--white-500-transparent);
|
|
200
|
+
--svelte-ui-text-subtle-color: var(--svelte-ui-text-weak-color);
|
|
197
201
|
--svelte-ui-text-on-filled-color: #ffffff;
|
|
198
202
|
--svelte-ui-text-inverse-color: var(--base-800-transparent);
|
|
199
|
-
--svelte-ui-text-inverse-
|
|
203
|
+
--svelte-ui-text-inverse-weak-color: var(--base-500-transparent);
|
|
204
|
+
--svelte-ui-text-inverse-subtle-color: var(--svelte-ui-text-inverse-weak-color);
|
|
200
205
|
--svelte-ui-text-placeholder-color: var(--white-500-transparent);
|
|
201
206
|
|
|
202
207
|
/* ボーダーカラー */
|
|
@@ -502,6 +507,15 @@
|
|
|
502
507
|
--svelte-ui-select-border-color: var(--svelte-ui-border-weak-color);
|
|
503
508
|
--svelte-ui-select-dropdown-icon-color: var(--svelte-ui-dropdown-icon-color);
|
|
504
509
|
--svelte-ui-select-placeholder-color: var(--svelte-ui-text-placeholder-color);
|
|
510
|
+
--svelte-ui-select-options-min-width: 160px;
|
|
511
|
+
--svelte-ui-select-options-max-height: 280px;
|
|
512
|
+
--svelte-ui-select-option-padding: 8px 12px;
|
|
513
|
+
--svelte-ui-select-option-gap: 4px;
|
|
514
|
+
--svelte-ui-select-option-check-size: 18px;
|
|
515
|
+
--svelte-ui-select-option-hover-bg: var(--svelte-ui-hover-overlay);
|
|
516
|
+
--svelte-ui-select-option-selected-bg: var(--svelte-ui-select-overlay);
|
|
517
|
+
--svelte-ui-select-separator-margin: 4px 0;
|
|
518
|
+
--svelte-ui-select-separator-color: var(--svelte-ui-border-color);
|
|
505
519
|
|
|
506
520
|
/* Checkbox */
|
|
507
521
|
--svelte-ui-checkbox-size: 16px;
|
|
@@ -860,7 +874,7 @@
|
|
|
860
874
|
[data-theme='dark'] {
|
|
861
875
|
/* ===== 基本色の上書き ===== */
|
|
862
876
|
--svelte-ui-text-color: CanvasText;
|
|
863
|
-
--svelte-ui-text-
|
|
877
|
+
--svelte-ui-text-weak-color: CanvasText;
|
|
864
878
|
--svelte-ui-surface-accent-color: CanvasText;
|
|
865
879
|
--svelte-ui-text-on-filled-color: HighlightText;
|
|
866
880
|
--svelte-ui-text-placeholder-color: CanvasText;
|
|
@@ -905,6 +919,7 @@
|
|
|
905
919
|
--svelte-ui-select-border-color: CanvasText;
|
|
906
920
|
--svelte-ui-select-text-color: CanvasText;
|
|
907
921
|
--svelte-ui-select-dropdown-icon-color: CanvasText;
|
|
922
|
+
--svelte-ui-select-separator-color: CanvasText;
|
|
908
923
|
--svelte-ui-colorpicker-border-color: CanvasText;
|
|
909
924
|
--svelte-ui-colorpicker-trigger-border-color: CanvasText;
|
|
910
925
|
--svelte-ui-colorpicker-bg: Canvas;
|
|
@@ -960,6 +975,8 @@
|
|
|
960
975
|
/* ===== オーバーレイ・インタラクション色の上書き ===== */
|
|
961
976
|
--svelte-ui-hover-overlay: Highlight;
|
|
962
977
|
--svelte-ui-select-overlay: Highlight;
|
|
978
|
+
--svelte-ui-select-option-hover-bg: Highlight;
|
|
979
|
+
--svelte-ui-select-option-selected-bg: Highlight;
|
|
963
980
|
--svelte-ui-combobox-option-hover-bg: Highlight;
|
|
964
981
|
--svelte-ui-combobox-option-selected-bg: Highlight;
|
|
965
982
|
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
// 基本プロパティ
|
|
17
17
|
/** Selected values array. Supports `bind:value`. */
|
|
18
18
|
value: OptionValue[];
|
|
19
|
-
/** `{ label, value, disabled? }[]` */
|
|
20
|
-
options: Option[];
|
|
19
|
+
/** `{ label, value, disabled? }[]` or `'separator'` for dividers. */
|
|
20
|
+
options: (Option | 'separator')[];
|
|
21
21
|
|
|
22
22
|
// HTML属性系
|
|
23
23
|
id?: string | null;
|
|
@@ -86,7 +86,9 @@
|
|
|
86
86
|
const listboxId = $derived(`${id ?? fallbackId}-listbox`);
|
|
87
87
|
|
|
88
88
|
const selectedLabels = $derived(
|
|
89
|
-
options
|
|
89
|
+
options
|
|
90
|
+
.filter((option): option is Option => option !== 'separator' && value.includes(option.value))
|
|
91
|
+
.map((option) => option.label)
|
|
90
92
|
);
|
|
91
93
|
|
|
92
94
|
// =========================================================================
|
|
@@ -199,20 +201,24 @@
|
|
|
199
201
|
role="listbox"
|
|
200
202
|
aria-multiselectable="true"
|
|
201
203
|
aria-label={ariaLabel ?? t('multiSelect.accessibleName')}
|
|
202
|
-
style
|
|
204
|
+
style:--internal-multi-select-trigger-width="{triggerWidth}px"
|
|
203
205
|
>
|
|
204
|
-
{#each options as option (option.value)}
|
|
205
|
-
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
206
|
+
{#each options as option, index (option === 'separator' ? `separator-${index}` : option.value)}
|
|
207
|
+
{#if option === 'separator'}
|
|
208
|
+
<li class="multi-select__separator" role="separator"></li>
|
|
209
|
+
{:else}
|
|
210
|
+
<li role="presentation" class="multi-select__item">
|
|
211
|
+
<Checkbox
|
|
212
|
+
value={value.includes(option.value)}
|
|
213
|
+
disabled={option.disabled}
|
|
214
|
+
fullWidth
|
|
215
|
+
customStyle="padding: 8px 12px"
|
|
216
|
+
onchange={() => toggleOption(option.value)}
|
|
217
|
+
>
|
|
218
|
+
{option.label}
|
|
219
|
+
</Checkbox>
|
|
220
|
+
</li>
|
|
221
|
+
{/if}
|
|
216
222
|
{/each}
|
|
217
223
|
</ul>
|
|
218
224
|
</Popup>
|
|
@@ -346,7 +352,7 @@
|
|
|
346
352
|
* ============================================= */
|
|
347
353
|
.multi-select.multi-select--inline .multi-select__trigger {
|
|
348
354
|
padding: inherit;
|
|
349
|
-
padding-right: var(--svelte-ui-
|
|
355
|
+
padding-right: var(--svelte-ui-select-icon-space-inline);
|
|
350
356
|
background: transparent;
|
|
351
357
|
border: none;
|
|
352
358
|
border-radius: 0;
|
|
@@ -364,7 +370,7 @@
|
|
|
364
370
|
right: 0;
|
|
365
371
|
margin-top: 0;
|
|
366
372
|
margin-bottom: 0;
|
|
367
|
-
margin-right: calc(0px - var(--svelte-ui-
|
|
373
|
+
margin-right: calc(0px - var(--svelte-ui-select-icon-space-inline));
|
|
368
374
|
}
|
|
369
375
|
|
|
370
376
|
/* =============================================
|
|
@@ -381,8 +387,9 @@
|
|
|
381
387
|
list-style: none;
|
|
382
388
|
margin: 0;
|
|
383
389
|
padding: 0;
|
|
384
|
-
|
|
385
|
-
|
|
390
|
+
/* トリガー幅に合わせつつ、トリガーが狭いときはテーマの下限を保つ */
|
|
391
|
+
min-width: max(var(--internal-multi-select-trigger-width, 0px), var(--svelte-ui-select-options-min-width));
|
|
392
|
+
max-height: var(--svelte-ui-select-options-max-height);
|
|
386
393
|
overflow-y: auto;
|
|
387
394
|
}
|
|
388
395
|
|
|
@@ -397,11 +404,8 @@
|
|
|
397
404
|
}
|
|
398
405
|
}
|
|
399
406
|
|
|
400
|
-
.multi-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
.multi-select__option-cover--disabled {
|
|
406
|
-
cursor: not-allowed;
|
|
407
|
+
.multi-select__separator {
|
|
408
|
+
height: 0;
|
|
409
|
+
margin: var(--svelte-ui-select-separator-margin);
|
|
410
|
+
border-bottom: solid 1px var(--svelte-ui-select-separator-color);
|
|
407
411
|
}</style>
|
|
@@ -4,8 +4,8 @@ import type { PopupPosition } from '../types/propOptions';
|
|
|
4
4
|
export type MultiSelectProps = {
|
|
5
5
|
/** Selected values array. Supports `bind:value`. */
|
|
6
6
|
value: OptionValue[];
|
|
7
|
-
/** `{ label, value, disabled? }[]` */
|
|
8
|
-
options: Option[];
|
|
7
|
+
/** `{ label, value, disabled? }[]` or `'separator'` for dividers. */
|
|
8
|
+
options: (Option | 'separator')[];
|
|
9
9
|
id?: string | null;
|
|
10
10
|
ariaLabel?: string;
|
|
11
11
|
tabindex?: number | null;
|
|
@@ -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,205 @@
|
|
|
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
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
171
|
+
const fallbackId = `select-${Math.random().toString(36).substring(2, 15)}`;
|
|
172
|
+
const baseId = $derived(id ?? fallbackId);
|
|
173
|
+
const listboxId = $derived(`${baseId}-listbox`);
|
|
174
|
+
const accessibleName = $derived(ariaLabel ?? t('select.accessibleName'));
|
|
175
|
+
|
|
176
|
+
const selectedOption = $derived(
|
|
177
|
+
options.find((option) => option !== 'separator' && option.value === value) as Option | undefined
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
/** キーボード操作で移動できる選択肢の options 内インデックス */
|
|
181
|
+
const navigableIndexes = $derived(
|
|
182
|
+
options.reduce<number[]>((indexes, option, index) => {
|
|
183
|
+
if (option !== 'separator' && !option.disabled) {
|
|
184
|
+
indexes.push(index);
|
|
185
|
+
}
|
|
186
|
+
return indexes;
|
|
187
|
+
}, [])
|
|
188
|
+
);
|
|
197
189
|
|
|
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
|
-
};
|
|
190
|
+
// =========================================================================
|
|
191
|
+
// Methods
|
|
192
|
+
// =========================================================================
|
|
193
|
+
const getOptionId = (index: number): string => `${baseId}-option-${index}`;
|
|
213
194
|
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
195
|
+
const getOptionAt = (index: number): Option | undefined => {
|
|
196
|
+
const option = options[index];
|
|
197
|
+
return option === 'separator' ? undefined : option;
|
|
217
198
|
};
|
|
218
199
|
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
200
|
+
const setActiveIndex = async (index: number) => {
|
|
201
|
+
activeIndex = index;
|
|
202
|
+
await tick();
|
|
203
|
+
optionEls[index]?.scrollIntoView({ block: 'nearest' });
|
|
222
204
|
};
|
|
223
205
|
|
|
224
|
-
const
|
|
225
|
-
if (
|
|
226
|
-
|
|
206
|
+
const moveActiveIndex = (step: number) => {
|
|
207
|
+
if (navigableIndexes.length === 0) return;
|
|
208
|
+
const current = navigableIndexes.indexOf(activeIndex);
|
|
209
|
+
const next =
|
|
210
|
+
current === -1
|
|
211
|
+
? step > 0
|
|
212
|
+
? 0
|
|
213
|
+
: navigableIndexes.length - 1
|
|
214
|
+
: (current + step + navigableIndexes.length) % navigableIndexes.length;
|
|
215
|
+
setActiveIndex(navigableIndexes[next]);
|
|
227
216
|
};
|
|
228
217
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
218
|
+
/** 入力した文字で始まる選択肢へ移動する(ネイティブ select 相当の挙動) */
|
|
219
|
+
const moveByTypeahead = (key: string) => {
|
|
220
|
+
clearTimeout(typeaheadTimer);
|
|
221
|
+
typeaheadBuffer += key.toLowerCase();
|
|
222
|
+
typeaheadTimer = setTimeout(() => {
|
|
223
|
+
typeaheadBuffer = '';
|
|
224
|
+
}, TYPEAHEAD_RESET_MS);
|
|
225
|
+
|
|
226
|
+
// 1文字目は現在位置の次から探し、同じ文字の連打で候補を巡回できるようにする
|
|
227
|
+
const offset = typeaheadBuffer.length === 1 ? navigableIndexes.indexOf(activeIndex) + 1 : 0;
|
|
228
|
+
const searchOrder = [...navigableIndexes.slice(offset), ...navigableIndexes.slice(0, offset)];
|
|
229
|
+
const matched = searchOrder.find((index) =>
|
|
230
|
+
getOptionAt(index)?.label.toLowerCase().startsWith(typeaheadBuffer)
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (matched !== undefined) {
|
|
234
|
+
setActiveIndex(matched);
|
|
235
|
+
}
|
|
232
236
|
};
|
|
233
237
|
|
|
234
|
-
const
|
|
235
|
-
if (disabled) return;
|
|
236
|
-
onmouseover?.(event);
|
|
237
|
-
};
|
|
238
|
+
const selectOption = (option: Option) => {
|
|
239
|
+
if (disabled || option.disabled) return;
|
|
238
240
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
241
|
+
value = option.value;
|
|
242
|
+
announceSelection(option.label);
|
|
243
|
+
onchange?.(value);
|
|
244
|
+
popupRef?.close();
|
|
242
245
|
};
|
|
243
246
|
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
+
const handleFocus = (event: FocusEvent) => {
|
|
248
|
+
isFocused = true;
|
|
249
|
+
onfocus?.(event);
|
|
247
250
|
};
|
|
248
251
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
+
const handleBlur = (event: FocusEvent) => {
|
|
253
|
+
isFocused = false;
|
|
254
|
+
onblur?.(event);
|
|
252
255
|
};
|
|
253
256
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
ontouchstart?.(event);
|
|
257
|
+
const handleTriggerClick = (event: MouseEvent) => {
|
|
258
|
+
popupRef?.toggle();
|
|
259
|
+
onclick?.(event);
|
|
258
260
|
};
|
|
259
261
|
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
ontouchend?.(event);
|
|
263
|
-
};
|
|
262
|
+
const handleTriggerKeydown = (event: KeyboardEvent) => {
|
|
263
|
+
onkeydown?.(event);
|
|
264
264
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
265
|
+
switch (event.key) {
|
|
266
|
+
case 'Enter':
|
|
267
|
+
case ' ':
|
|
268
|
+
case 'ArrowDown':
|
|
269
|
+
case 'ArrowUp':
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
popupRef?.open();
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
268
274
|
};
|
|
269
275
|
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
276
|
+
const handleListClick = (event: MouseEvent) => {
|
|
277
|
+
const optionEl = (event.target as HTMLElement | null)?.closest<HTMLElement>(
|
|
278
|
+
'[data-option-index]'
|
|
279
|
+
);
|
|
280
|
+
if (!optionEl) return;
|
|
274
281
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (disabled) return;
|
|
278
|
-
onpointerdown?.(event);
|
|
282
|
+
const option = getOptionAt(Number(optionEl.dataset.optionIndex));
|
|
283
|
+
if (option) selectOption(option);
|
|
279
284
|
};
|
|
280
285
|
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
286
|
+
const handleListKeydown = (event: KeyboardEvent) => {
|
|
287
|
+
switch (event.key) {
|
|
288
|
+
case 'ArrowDown':
|
|
289
|
+
event.preventDefault();
|
|
290
|
+
moveActiveIndex(1);
|
|
291
|
+
break;
|
|
292
|
+
case 'ArrowUp':
|
|
293
|
+
event.preventDefault();
|
|
294
|
+
moveActiveIndex(-1);
|
|
295
|
+
break;
|
|
296
|
+
case 'Home':
|
|
297
|
+
event.preventDefault();
|
|
298
|
+
if (navigableIndexes.length > 0) setActiveIndex(navigableIndexes[0]);
|
|
299
|
+
break;
|
|
300
|
+
case 'End':
|
|
301
|
+
event.preventDefault();
|
|
302
|
+
if (navigableIndexes.length > 0)
|
|
303
|
+
setActiveIndex(navigableIndexes[navigableIndexes.length - 1]);
|
|
304
|
+
break;
|
|
305
|
+
case 'Enter':
|
|
306
|
+
case ' ': {
|
|
307
|
+
event.preventDefault();
|
|
308
|
+
const option = getOptionAt(activeIndex);
|
|
309
|
+
if (option) selectOption(option);
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
case 'Tab':
|
|
313
|
+
popupRef?.close();
|
|
314
|
+
break;
|
|
315
|
+
default:
|
|
316
|
+
if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
317
|
+
event.preventDefault();
|
|
318
|
+
moveByTypeahead(event.key);
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
284
322
|
};
|
|
285
323
|
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
onpointerenter?.(event);
|
|
289
|
-
};
|
|
324
|
+
const handlePopupOpen = () => {
|
|
325
|
+
triggerWidth = triggerEl?.offsetWidth ?? 0;
|
|
290
326
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
327
|
+
const selectedIndex = options.findIndex(
|
|
328
|
+
(option) => option !== 'separator' && option.value === value && !option.disabled
|
|
329
|
+
);
|
|
330
|
+
setActiveIndex(selectedIndex >= 0 ? selectedIndex : (navigableIndexes[0] ?? -1));
|
|
295
331
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
332
|
+
tick().then(() => {
|
|
333
|
+
listEl?.focus();
|
|
334
|
+
});
|
|
299
335
|
};
|
|
300
336
|
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
337
|
+
const handlePopupClose = (reason: CloseReason) => {
|
|
338
|
+
activeIndex = -1;
|
|
339
|
+
typeaheadBuffer = '';
|
|
340
|
+
clearTimeout(typeaheadTimer);
|
|
305
341
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
onchange?.(value);
|
|
309
|
-
// 選択完了後にフォーカスを解除
|
|
310
|
-
if (selectRef) {
|
|
311
|
-
selectRef.blur();
|
|
342
|
+
if (reason !== 'outside') {
|
|
343
|
+
triggerEl?.focus();
|
|
312
344
|
}
|
|
313
345
|
};
|
|
314
346
|
</script>
|
|
315
347
|
|
|
316
348
|
<div
|
|
317
|
-
class="select
|
|
318
|
-
select--focus-{focusStyle}"
|
|
349
|
+
class="select select--focus-{focusStyle}"
|
|
319
350
|
class:select--inline={inline}
|
|
320
351
|
class:select--full-width={fullWidth}
|
|
321
352
|
class:select--disabled={disabled}
|
|
@@ -323,198 +354,341 @@ select--focus-{focusStyle}"
|
|
|
323
354
|
class:select--rounded={rounded}
|
|
324
355
|
data-testid="select"
|
|
325
356
|
>
|
|
326
|
-
<
|
|
327
|
-
bind:this={
|
|
357
|
+
<button
|
|
358
|
+
bind:this={triggerEl}
|
|
328
359
|
{id}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
{
|
|
333
|
-
|
|
360
|
+
type="button"
|
|
361
|
+
class="select__trigger"
|
|
362
|
+
role="combobox"
|
|
363
|
+
aria-expanded={isPopupOpen}
|
|
364
|
+
aria-haspopup="listbox"
|
|
365
|
+
aria-controls={isPopupOpen ? listboxId : undefined}
|
|
366
|
+
aria-label={accessibleName}
|
|
367
|
+
aria-required={required ? 'true' : undefined}
|
|
334
368
|
{tabindex}
|
|
335
|
-
{
|
|
369
|
+
{disabled}
|
|
336
370
|
style={customStyle}
|
|
337
371
|
onfocus={handleFocus}
|
|
338
372
|
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}
|
|
373
|
+
onclick={handleTriggerClick}
|
|
374
|
+
onkeydown={handleTriggerKeydown}
|
|
375
|
+
{onkeyup}
|
|
376
|
+
{onmousedown}
|
|
377
|
+
{onmouseup}
|
|
378
|
+
{onmouseenter}
|
|
379
|
+
{onmouseleave}
|
|
380
|
+
{onmouseover}
|
|
381
|
+
{onmouseout}
|
|
382
|
+
{oncontextmenu}
|
|
383
|
+
{onauxclick}
|
|
384
|
+
{ontouchstart}
|
|
385
|
+
{ontouchend}
|
|
386
|
+
{ontouchmove}
|
|
387
|
+
{ontouchcancel}
|
|
388
|
+
{onpointerdown}
|
|
389
|
+
{onpointerup}
|
|
390
|
+
{onpointerenter}
|
|
391
|
+
{onpointerleave}
|
|
392
|
+
{onpointermove}
|
|
393
|
+
{onpointercancel}
|
|
362
394
|
>
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
395
|
+
<span class="select__inner">
|
|
396
|
+
{#if selectedOption}
|
|
397
|
+
<span class="select__display-text">{selectedOption.label}</span>
|
|
398
|
+
{:else if placeholder}
|
|
399
|
+
<span class="select__placeholder">{placeholder}</span>
|
|
400
|
+
{/if}
|
|
401
|
+
<span class="select__dropdown-icon" aria-hidden="true">
|
|
402
|
+
<Icon>arrow_drop_down</Icon>
|
|
403
|
+
</span>
|
|
404
|
+
</span>
|
|
405
|
+
</button>
|
|
406
|
+
|
|
407
|
+
<!-- フォーム送信用(ネイティブ select を使わないため値を hidden で送る) -->
|
|
408
|
+
{#if name}
|
|
409
|
+
<input type="hidden" {name} value={value ?? ''} />
|
|
410
|
+
{/if}
|
|
411
|
+
|
|
412
|
+
<Popup
|
|
413
|
+
bind:this={popupRef}
|
|
414
|
+
bind:isOpen={isPopupOpen}
|
|
415
|
+
anchorElement={triggerEl}
|
|
416
|
+
{position}
|
|
417
|
+
role="presentation"
|
|
418
|
+
mobileFullscreen={true}
|
|
419
|
+
onOpen={handlePopupOpen}
|
|
420
|
+
onClose={handlePopupClose}
|
|
421
|
+
margin={4}
|
|
422
|
+
id={id ? `${id}-popup` : undefined}
|
|
423
|
+
>
|
|
424
|
+
<ul
|
|
425
|
+
bind:this={listEl}
|
|
426
|
+
id={listboxId}
|
|
427
|
+
class="select__options"
|
|
428
|
+
role="listbox"
|
|
429
|
+
tabindex="-1"
|
|
430
|
+
aria-label={accessibleName}
|
|
431
|
+
aria-activedescendant={activeIndex >= 0 ? getOptionId(activeIndex) : undefined}
|
|
432
|
+
style:--internal-select-trigger-width="{triggerWidth}px"
|
|
433
|
+
onclick={handleListClick}
|
|
434
|
+
onkeydown={handleListKeydown}
|
|
435
|
+
>
|
|
436
|
+
{#each options as option, index (option === 'separator' ? `separator-${index}` : option.value)}
|
|
437
|
+
{#if option === 'separator'}
|
|
438
|
+
<li class="select__separator" role="separator"></li>
|
|
439
|
+
{:else}
|
|
440
|
+
<li
|
|
441
|
+
bind:this={optionEls[index]}
|
|
442
|
+
id={getOptionId(index)}
|
|
443
|
+
data-option-index={index}
|
|
444
|
+
class="select__option"
|
|
445
|
+
class:select__option--selected={option.value === value}
|
|
446
|
+
class:select__option--active={index === activeIndex}
|
|
447
|
+
class:select__option--disabled={option.disabled}
|
|
448
|
+
role="option"
|
|
449
|
+
aria-selected={option.value === value}
|
|
450
|
+
aria-disabled={option.disabled ? 'true' : undefined}
|
|
451
|
+
onmouseenter={() => {
|
|
452
|
+
if (!option.disabled) activeIndex = index;
|
|
453
|
+
}}
|
|
454
|
+
>
|
|
455
|
+
<span class="select__option-check">
|
|
456
|
+
{#if option.value === value}
|
|
457
|
+
<Icon size="var(--svelte-ui-select-option-check-size)">check</Icon>
|
|
458
|
+
{/if}
|
|
459
|
+
</span>
|
|
460
|
+
<span class="select__option-label">{option.label}</span>
|
|
461
|
+
</li>
|
|
462
|
+
{/if}
|
|
463
|
+
{/each}
|
|
464
|
+
</ul>
|
|
465
|
+
</Popup>
|
|
380
466
|
</div>
|
|
381
467
|
|
|
382
|
-
<style
|
|
383
|
-
|
|
468
|
+
<style>@charset "UTF-8";
|
|
469
|
+
/* =============================================
|
|
384
470
|
* 基本構造・レイアウト
|
|
385
471
|
* ============================================= */
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
*
|
|
472
|
+
.select {
|
|
473
|
+
display: inline-block;
|
|
474
|
+
position: relative;
|
|
475
|
+
width: auto;
|
|
476
|
+
max-width: 100%;
|
|
477
|
+
vertical-align: top;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/* =============================================
|
|
481
|
+
* トリガーボタン
|
|
396
482
|
* ============================================= */
|
|
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
|
-
|
|
483
|
+
.select__trigger {
|
|
484
|
+
display: flex;
|
|
485
|
+
align-items: center;
|
|
486
|
+
width: 100%;
|
|
487
|
+
min-height: var(--svelte-ui-select-height);
|
|
488
|
+
padding: var(--svelte-ui-select-padding);
|
|
489
|
+
padding-right: var(--svelte-ui-select-icon-space);
|
|
490
|
+
background: transparent;
|
|
491
|
+
border: none;
|
|
492
|
+
font-family: inherit;
|
|
493
|
+
font-size: inherit;
|
|
494
|
+
font-weight: inherit;
|
|
495
|
+
color: inherit;
|
|
496
|
+
line-height: inherit;
|
|
497
|
+
text-align: left;
|
|
498
|
+
cursor: pointer;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.select__inner {
|
|
502
|
+
position: relative;
|
|
503
|
+
flex: 1;
|
|
504
|
+
align-self: stretch;
|
|
505
|
+
display: flex;
|
|
506
|
+
align-items: center;
|
|
507
|
+
min-width: 0;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.select__display-text,
|
|
511
|
+
.select__placeholder {
|
|
512
|
+
flex: 1;
|
|
513
|
+
display: block;
|
|
514
|
+
overflow: hidden;
|
|
515
|
+
text-overflow: ellipsis;
|
|
516
|
+
white-space: nowrap;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.select__placeholder {
|
|
520
|
+
color: var(--svelte-ui-select-placeholder-color);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.select__dropdown-icon {
|
|
524
|
+
display: flex;
|
|
525
|
+
justify-content: center;
|
|
526
|
+
align-items: center;
|
|
527
|
+
position: absolute;
|
|
528
|
+
top: 0;
|
|
529
|
+
right: 4px;
|
|
530
|
+
margin-top: calc((var(--svelte-ui-select-height) - 32px) / 2);
|
|
531
|
+
margin-bottom: calc((var(--svelte-ui-select-height) - 32px) / 2);
|
|
532
|
+
margin-right: calc(0px - var(--svelte-ui-select-icon-space));
|
|
533
|
+
width: 32px;
|
|
534
|
+
height: 32px;
|
|
535
|
+
color: var(--svelte-ui-select-dropdown-icon-color);
|
|
536
|
+
pointer-events: none;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/* =============================================
|
|
432
540
|
* レイアウトバリエーション
|
|
433
541
|
* ============================================= */
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
/* =============================================
|
|
439
|
-
* プレースホルダー・オプション表示
|
|
440
|
-
* ============================================= */
|
|
441
|
-
option[value=''] {
|
|
442
|
-
color: var(--svelte-ui-select-placeholder-color);
|
|
443
|
-
}
|
|
542
|
+
.select--full-width {
|
|
543
|
+
width: 100%;
|
|
544
|
+
}
|
|
444
545
|
|
|
445
|
-
|
|
546
|
+
/* =============================================
|
|
446
547
|
* フォーカス効果バリエーション
|
|
447
548
|
* ============================================= */
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
* 状態管理(disabled
|
|
549
|
+
.select--focus-outline .select__trigger:focus-visible {
|
|
550
|
+
outline: var(--svelte-ui-focus-outline-inner);
|
|
551
|
+
outline-offset: var(--svelte-ui-focus-outline-offset-inner);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/* デザインバリアントの背景指定より詳細度を上げるため .select を併記 */
|
|
555
|
+
.select.select--focus-background .select__trigger:focus-visible {
|
|
556
|
+
outline: none;
|
|
557
|
+
background: var(--svelte-ui-hover-overlay);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.select--focus-none .select__trigger:focus-visible {
|
|
561
|
+
outline: none;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/* =============================================
|
|
565
|
+
* 状態管理(disabled等)
|
|
465
566
|
* ============================================= */
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
opacity: 1;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/* =============================================
|
|
567
|
+
.select--disabled {
|
|
568
|
+
opacity: var(--svelte-ui-input-disabled-opacity);
|
|
569
|
+
cursor: not-allowed;
|
|
570
|
+
}
|
|
571
|
+
.select--disabled .select__trigger {
|
|
572
|
+
cursor: not-allowed;
|
|
573
|
+
pointer-events: none;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/* =============================================
|
|
480
577
|
* デザインバリアント:default
|
|
481
578
|
* ============================================= */
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/* =============================================
|
|
579
|
+
.select:not(.select--inline) .select__trigger {
|
|
580
|
+
min-height: var(--svelte-ui-select-height);
|
|
581
|
+
background-color: var(--svelte-ui-select-bg);
|
|
582
|
+
box-shadow: 0 0 0 var(--svelte-ui-border-width) inset var(--svelte-ui-select-border-color);
|
|
583
|
+
border: none;
|
|
584
|
+
border-radius: var(--svelte-ui-select-border-radius);
|
|
585
|
+
font-size: var(--svelte-ui-font-size);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/* =============================================
|
|
495
589
|
* デザインバリアント:inline
|
|
496
590
|
* ============================================= */
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
591
|
+
.select.select--inline .select__trigger {
|
|
592
|
+
padding: inherit;
|
|
593
|
+
padding-right: var(--svelte-ui-select-icon-space-inline);
|
|
594
|
+
background: transparent;
|
|
595
|
+
border: none;
|
|
596
|
+
border-radius: 0;
|
|
597
|
+
color: inherit;
|
|
598
|
+
min-height: auto;
|
|
599
|
+
line-height: inherit;
|
|
600
|
+
}
|
|
601
|
+
.select.select--inline .select__inner {
|
|
602
|
+
align-self: auto;
|
|
603
|
+
align-items: flex-start;
|
|
604
|
+
min-height: 1lh;
|
|
605
|
+
}
|
|
606
|
+
.select.select--inline .select__dropdown-icon {
|
|
607
|
+
top: calc((1lh - 32px) / 2);
|
|
608
|
+
right: 0;
|
|
609
|
+
margin-top: 0;
|
|
610
|
+
margin-bottom: 0;
|
|
611
|
+
margin-right: calc(0px - var(--svelte-ui-select-icon-space-inline));
|
|
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>
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import type { FocusHandler, KeyboardHandler, MouseHandler, TouchHandler, PointerHandler, BivariantValueHandler } from '../types/callbackHandlers';
|
|
2
|
-
import type { Option } from '../types/options';
|
|
3
|
-
import type {
|
|
4
|
-
export type SelectProps =
|
|
2
|
+
import type { Option, OptionValue } from '../types/options';
|
|
3
|
+
import type { PopupPosition } from '../types/propOptions';
|
|
4
|
+
export type SelectProps = {
|
|
5
5
|
name?: string;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/** Supports `bind:value`. */
|
|
7
|
+
value: OptionValue;
|
|
8
|
+
/** `{ label, value, disabled? }[]` or `'separator'` for dividers. */
|
|
9
|
+
options: (Option | 'separator')[];
|
|
9
10
|
id?: string | null;
|
|
10
11
|
ariaLabel?: string;
|
|
11
12
|
tabindex?: number | null;
|
|
12
13
|
placeholder?: string;
|
|
13
|
-
/** Number of visible rows (multi-line mode). @default null */
|
|
14
|
-
size?: number | null;
|
|
15
14
|
/** Renders inline. @default false */
|
|
16
15
|
inline?: boolean;
|
|
17
16
|
/** @default 'outline' */
|
|
@@ -21,6 +20,8 @@ export type SelectProps = Omit<HTMLSelectAttributes, 'value' | 'id' | 'tabindex'
|
|
|
21
20
|
customStyle?: string;
|
|
22
21
|
disabled?: boolean;
|
|
23
22
|
required?: boolean;
|
|
23
|
+
/** @default 'bottom-left' */
|
|
24
|
+
position?: PopupPosition;
|
|
24
25
|
onfocus?: FocusHandler;
|
|
25
26
|
onblur?: FocusHandler;
|
|
26
27
|
onkeydown?: KeyboardHandler;
|
|
@@ -44,7 +45,7 @@ export type SelectProps = Omit<HTMLSelectAttributes, 'value' | 'id' | 'tabindex'
|
|
|
44
45
|
onpointerleave?: PointerHandler;
|
|
45
46
|
onpointermove?: PointerHandler;
|
|
46
47
|
onpointercancel?: PointerHandler;
|
|
47
|
-
onchange?: BivariantValueHandler<
|
|
48
|
+
onchange?: BivariantValueHandler<OptionValue>;
|
|
48
49
|
};
|
|
49
50
|
declare const Select: import("svelte").Component<SelectProps, {}, "value">;
|
|
50
51
|
type Select = ReturnType<typeof Select>;
|