@14ch/svelte-ui 0.0.67 → 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.
@@ -139,10 +139,13 @@
139
139
  * ==================================== */
140
140
  /* 基本テキスト色の定義 */
141
141
  --svelte-ui-text-color: var(--base-800-transparent);
142
- --svelte-ui-text-subtle-color: var(--base-500-transparent);
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-subtle-color: var(--white-500-transparent);
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-subtle-color: var(--white-500-transparent);
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-subtle-color: var(--base-500-transparent);
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-subtle-color: CanvasText;
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-subtle-color: var(--base-500-transparent);
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-subtle-color: var(--white-500-transparent);
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-subtle-color: var(--white-500-transparent);
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-subtle-color: var(--base-500-transparent);
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-subtle-color: CanvasText;
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
 
@@ -124,7 +124,7 @@
124
124
  // 基本プロパティ
125
125
  name = '',
126
126
  value = $bindable(false),
127
- indeterminate = $bindable(false),
127
+ indeterminate = $bindable(),
128
128
 
129
129
  // HTML属性系
130
130
  id = `checkbox-${Math.random().toString(36).substring(2, 15)}`,
@@ -123,7 +123,7 @@
123
123
 
124
124
  let {
125
125
  // 基本プロパティ
126
- value = $bindable(''),
126
+ value = $bindable(),
127
127
 
128
128
  // HTML属性系
129
129
  id = `colorpicker-${Math.random().toString(36).substring(2, 15)}`,
@@ -377,8 +377,7 @@
377
377
  </script>
378
378
 
379
379
  <div
380
- class="color-picker
381
- focus-style-{focusStyle}"
380
+ class="color-picker"
382
381
  class:color-picker--full-width={fullWidth}
383
382
  class:color-picker--rounded={rounded}
384
383
  class:color-picker--clearable={clearable}
@@ -436,7 +435,7 @@
436
435
  <input
437
436
  type="color"
438
437
  aria-label={ariaLabel ?? t('colorpicker.chooseColor')}
439
- bind:value
438
+ bind:value={localValue}
440
439
  onchange={handleValueChange}
441
440
  onfocus={handleFocus}
442
441
  onblur={handleBlur}
@@ -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.filter((o) => value.includes(o.value)).map((o) => o.label)
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:min-width="{triggerWidth}px"
204
+ style:--internal-multi-select-trigger-width="{triggerWidth}px"
203
205
  >
204
- {#each options as option, i (option.value)}
205
- <li role="presentation" class="multi-select__item">
206
- <Checkbox
207
- value={value.includes(option.value)}
208
- disabled={option.disabled}
209
- fullWidth
210
- customStyle="padding: 8px 12px"
211
- onchange={() => toggleOption(option.value)}
212
- >
213
- {option.label}
214
- </Checkbox>
215
- </li>
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>
@@ -250,10 +256,6 @@
250
256
  display: flex;
251
257
  align-items: center;
252
258
  }
253
- .multi-select__trigger:focus, .multi-select__trigger:focus-visible {
254
- outline: var(--svelte-ui-focus-outline-inner);
255
- outline-offset: var(--svelte-ui-focus-outline-offset-inner);
256
- }
257
259
 
258
260
  .multi-select__inner {
259
261
  position: relative;
@@ -311,10 +313,16 @@
311
313
  outline-offset: var(--svelte-ui-focus-outline-offset-inner);
312
314
  }
313
315
 
314
- .multi-select--focus-background .multi-select__trigger:focus {
316
+ /* デザインバリアントの背景指定より詳細度を上げるため .multi-select を併記 */
317
+ .multi-select.multi-select--focus-background .multi-select__trigger:focus {
318
+ outline: none;
315
319
  background: var(--svelte-ui-hover-overlay);
316
320
  }
317
321
 
322
+ .multi-select--focus-none .multi-select__trigger:focus {
323
+ outline: none;
324
+ }
325
+
318
326
  /* =============================================
319
327
  * 状態管理
320
328
  * ============================================= */
@@ -344,7 +352,7 @@
344
352
  * ============================================= */
345
353
  .multi-select.multi-select--inline .multi-select__trigger {
346
354
  padding: inherit;
347
- padding-right: var(--svelte-ui-input-icon-space-inline);
355
+ padding-right: var(--svelte-ui-select-icon-space-inline);
348
356
  background: transparent;
349
357
  border: none;
350
358
  border-radius: 0;
@@ -362,7 +370,7 @@
362
370
  right: 0;
363
371
  margin-top: 0;
364
372
  margin-bottom: 0;
365
- margin-right: calc(0px - var(--svelte-ui-input-icon-space-inline));
373
+ margin-right: calc(0px - var(--svelte-ui-select-icon-space-inline));
366
374
  }
367
375
 
368
376
  /* =============================================
@@ -379,8 +387,9 @@
379
387
  list-style: none;
380
388
  margin: 0;
381
389
  padding: 0;
382
- min-width: 160px;
383
- max-height: var(--svelte-ui-combobox-options-max-height);
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);
384
393
  overflow-y: auto;
385
394
  }
386
395
 
@@ -395,11 +404,8 @@
395
404
  }
396
405
  }
397
406
 
398
- .multi-select__option-cover {
399
- position: absolute;
400
- inset: 0;
401
- cursor: pointer;
402
- }
403
- .multi-select__option-cover--disabled {
404
- 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);
405
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;
@@ -125,7 +125,7 @@
125
125
  // 基本プロパティ
126
126
  name = '',
127
127
  value = '',
128
- currentValue = $bindable(null),
128
+ currentValue = $bindable(),
129
129
 
130
130
  // HTML属性系
131
131
  id = `radio-${Math.random().toString(36).substring(2, 15)}`,
@@ -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 { HTMLSelectAttributes } from 'svelte/elements';
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 = Omit<
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
- value: string | number | null | undefined;
56
- /** `{ label, value, disabled? }[]` */
57
- options: Option[];
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<string | number | null | undefined>;
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 = () => {}, // No params for type inference
175
-
176
- // その他
177
- ...restProps
151
+ onchange = () => {} // No params for type inference
178
152
  }: SelectProps = $props();
179
153
 
180
- let isFocused: boolean = $state(false);
181
- let selectRef = $state<HTMLSelectElement>();
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
- // Methods
169
+ // $derived
185
170
  // =========================================================================
186
- const handleFocus = (event: FocusEvent) => {
187
- if (disabled) return;
188
- isFocused = true;
189
- onfocus?.(event);
190
- };
191
-
192
- const handleBlur = (event: FocusEvent) => {
193
- if (disabled) return;
194
- isFocused = false;
195
- onblur?.(event);
196
- };
197
-
198
- const handleKeydown = (event: KeyboardEvent) => {
199
- if (disabled) return;
200
- onkeydown?.(event);
201
- };
202
-
203
- const handleKeyup = (event: KeyboardEvent) => {
204
- if (disabled) return;
205
- onkeyup?.(event);
206
- };
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
+ );
207
189
 
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 handleMouseDown = (event: MouseEvent) => {
215
- if (disabled) return;
216
- onmousedown?.(event);
195
+ const getOptionAt = (index: number): Option | undefined => {
196
+ const option = options[index];
197
+ return option === 'separator' ? undefined : option;
217
198
  };
218
199
 
219
- const handleMouseUp = (event: MouseEvent) => {
220
- if (disabled) return;
221
- onmouseup?.(event);
200
+ const setActiveIndex = async (index: number) => {
201
+ activeIndex = index;
202
+ await tick();
203
+ optionEls[index]?.scrollIntoView({ block: 'nearest' });
222
204
  };
223
205
 
224
- const handleMouseEnter = (event: MouseEvent) => {
225
- if (disabled) return;
226
- onmouseenter?.(event);
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
- const handleMouseLeave = (event: MouseEvent) => {
230
- if (disabled) return;
231
- onmouseleave?.(event);
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 handleMouseOver = (event: MouseEvent) => {
235
- if (disabled) return;
236
- onmouseover?.(event);
237
- };
238
+ const selectOption = (option: Option) => {
239
+ if (disabled || option.disabled) return;
238
240
 
239
- const handleMouseOut = (event: MouseEvent) => {
240
- if (disabled) return;
241
- onmouseout?.(event);
241
+ value = option.value;
242
+ announceSelection(option.label);
243
+ onchange?.(value);
244
+ popupRef?.close();
242
245
  };
243
246
 
244
- const handleContextMenu = (event: MouseEvent) => {
245
- if (disabled) return;
246
- oncontextmenu?.(event);
247
+ const handleFocus = (event: FocusEvent) => {
248
+ isFocused = true;
249
+ onfocus?.(event);
247
250
  };
248
251
 
249
- const handleAuxClick = (event: MouseEvent) => {
250
- if (disabled) return;
251
- onauxclick?.(event);
252
+ const handleBlur = (event: FocusEvent) => {
253
+ isFocused = false;
254
+ onblur?.(event);
252
255
  };
253
256
 
254
- // タッチイベント
255
- const handleTouchStart = (event: TouchEvent) => {
256
- if (disabled) return;
257
- ontouchstart?.(event);
257
+ const handleTriggerClick = (event: MouseEvent) => {
258
+ popupRef?.toggle();
259
+ onclick?.(event);
258
260
  };
259
261
 
260
- const handleTouchEnd = (event: TouchEvent) => {
261
- if (disabled) return;
262
- ontouchend?.(event);
263
- };
262
+ const handleTriggerKeydown = (event: KeyboardEvent) => {
263
+ onkeydown?.(event);
264
264
 
265
- const handleTouchMove = (event: TouchEvent) => {
266
- if (disabled) return;
267
- ontouchmove?.(event);
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 handleTouchCancel = (event: TouchEvent) => {
271
- if (disabled) return;
272
- ontouchcancel?.(event);
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
- const handlePointerDown = (event: PointerEvent) => {
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 handlePointerUp = (event: PointerEvent) => {
282
- if (disabled) return;
283
- onpointerup?.(event);
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 handlePointerEnter = (event: PointerEvent) => {
287
- if (disabled) return;
288
- onpointerenter?.(event);
289
- };
324
+ const handlePopupOpen = () => {
325
+ triggerWidth = triggerEl?.offsetWidth ?? 0;
290
326
 
291
- const handlePointerLeave = (event: PointerEvent) => {
292
- if (disabled) return;
293
- onpointerleave?.(event);
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
- const handlePointerMove = (event: PointerEvent) => {
297
- if (disabled) return;
298
- onpointermove?.(event);
332
+ tick().then(() => {
333
+ listEl?.focus();
334
+ });
299
335
  };
300
336
 
301
- const handlePointerCancel = (event: PointerEvent) => {
302
- if (disabled) return;
303
- onpointercancel?.(event);
304
- };
337
+ const handlePopupClose = (reason: CloseReason) => {
338
+ activeIndex = -1;
339
+ typeaheadBuffer = '';
340
+ clearTimeout(typeaheadTimer);
305
341
 
306
- // 選択イベント
307
- const handleChange = () => {
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
- <select
327
- bind:this={selectRef}
357
+ <button
358
+ bind:this={triggerEl}
328
359
  {id}
329
- {name}
330
- aria-label={ariaLabel ?? t('select.accessibleName')}
331
- bind:value
332
- {disabled}
333
- {required}
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
- {size}
369
+ {disabled}
336
370
  style={customStyle}
337
371
  onfocus={handleFocus}
338
372
  onblur={handleBlur}
339
- onkeydown={handleKeydown}
340
- onkeyup={handleKeyup}
341
- onclick={handleClick}
342
- onmousedown={handleMouseDown}
343
- onmouseup={handleMouseUp}
344
- onmouseenter={handleMouseEnter}
345
- onmouseleave={handleMouseLeave}
346
- onmouseover={handleMouseOver}
347
- onmouseout={handleMouseOut}
348
- oncontextmenu={handleContextMenu}
349
- onauxclick={handleAuxClick}
350
- ontouchstart={handleTouchStart}
351
- ontouchend={handleTouchEnd}
352
- ontouchmove={handleTouchMove}
353
- ontouchcancel={handleTouchCancel}
354
- onpointerdown={handlePointerDown}
355
- onpointerup={handlePointerUp}
356
- onpointerenter={handlePointerEnter}
357
- onpointerleave={handlePointerLeave}
358
- onpointermove={handlePointerMove}
359
- onpointercancel={handlePointerCancel}
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
- {#if placeholder}
365
- <option value="" disabled selected={value === undefined || value === null}>
366
- {placeholder}
367
- </option>
368
- {/if}
369
- <!-- 通常のオプション -->
370
- {#each options as option (option.value)}
371
- <option value={option.value} disabled={option.disabled}>
372
- {option.label}
373
- </option>
374
- {/each}
375
- </select>
376
- <!-- ドロップダウンアイコン -->
377
- <div class="select__dropdown-icon" aria-hidden="true">
378
- <Icon>arrow_drop_down</Icon>
379
- </div>
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
- .select {
387
- display: inline-block;
388
- position: relative;
389
- width: auto;
390
- max-width: 100%;
391
- vertical-align: top;
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
- select {
398
- width: 100%;
399
- min-height: var(--svelte-ui-select-height);
400
- padding: var(--svelte-ui-select-padding);
401
- padding-right: var(--svelte-ui-select-icon-space);
402
- background: transparent;
403
- border: none;
404
- border-radius: 0;
405
- font-family: inherit;
406
- font-size: inherit;
407
- font-weight: inherit;
408
- color: inherit;
409
- line-height: inherit;
410
- text-align: inherit;
411
- cursor: pointer;
412
- -webkit-appearance: none;
413
- -moz-appearance: none;
414
- appearance: none;
415
-
416
- &:focus,
417
- &:focus-visible {
418
- outline: var(--svelte-ui-focus-outline-inner);
419
- outline-offset: var(--svelte-ui-focus-outline-offset-inner);
420
- }
421
- }
422
-
423
- .select__dropdown-icon {
424
- display: flex;
425
- justify-content: center;
426
- align-items: center;
427
- position: absolute;
428
- top: 50%;
429
- right: 4px;
430
- width: 32px;
431
- height: 32px;
432
- transform: translateY(-50%);
433
- color: var(--svelte-ui-select-dropdown-icon-color);
434
- pointer-events: none;
435
- }
436
-
437
- /* =============================================
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
+ /* =============================================
438
540
  * レイアウトバリエーション
439
541
  * ============================================= */
440
- .select--full-width {
441
- width: 100%;
442
- }
542
+ .select--full-width {
543
+ width: 100%;
544
+ }
443
545
 
444
- /* =============================================
445
- * プレースホルダー・オプション表示
446
- * ============================================= */
447
- option[value=''] {
448
- color: var(--svelte-ui-select-placeholder-color);
449
- }
450
-
451
- /* =============================================
546
+ /* =============================================
452
547
  * フォーカス効果バリエーション
453
548
  * ============================================= */
454
- .select--focus-outline select:focus {
455
- outline: var(--svelte-ui-focus-outline-inner);
456
- outline-offset: var(--svelte-ui-focus-outline-offset-inner);
457
- }
458
-
459
- .select--focus-background select:focus {
460
- background: var(--svelte-ui-hover-overlay);
461
- }
462
-
463
- /* =============================================
464
- * 状態管理(disabled, readonly等)
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
- .select--disabled {
467
- opacity: var(--svelte-ui-input-disabled-opacity);
468
- cursor: not-allowed;
469
- }
470
-
471
- .select--disabled select {
472
- cursor: not-allowed;
473
- }
474
-
475
- select:disabled {
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
- .select:not(.select--inline) {
483
- select {
484
- min-height: var(--svelte-ui-select-height);
485
- background-color: var(--svelte-ui-select-bg);
486
- box-shadow: 0 0 0 var(--svelte-ui-border-width) inset var(--svelte-ui-select-border-color);
487
- border: none;
488
- border-radius: var(--svelte-ui-select-border-radius);
489
- font-size: var(--svelte-ui-font-size);
490
- line-height: var(--svelte-ui-select-height);
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
- .select.select--inline {
498
- select {
499
- padding: inherit;
500
- padding-right: var(--svelte-ui-input-icon-space-inline);
501
- background: transparent;
502
- border: none;
503
- border-radius: 0;
504
- color: inherit;
505
- min-height: auto;
506
- line-height: inherit;
507
- }
508
-
509
- .select__dropdown-icon {
510
- right: 0;
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
- .select--rounded:not(.select--inline) select {
518
- border-radius: var(--svelte-ui-select-border-radius-rounded);
519
- }
520
- </style>
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 { HTMLSelectAttributes } from 'svelte/elements';
4
- export type SelectProps = Omit<HTMLSelectAttributes, 'value' | 'id' | 'tabindex' | 'size' | 'onfocus' | 'onblur' | 'onkeydown' | 'onkeyup' | 'onclick' | 'onmousedown' | 'onmouseup' | 'onmouseenter' | 'onmouseleave' | 'onmouseover' | 'onmouseout' | 'oncontextmenu' | 'onauxclick' | 'ontouchstart' | 'ontouchend' | 'ontouchmove' | 'ontouchcancel' | 'onpointerdown' | 'onpointerup' | 'onpointerenter' | 'onpointerleave' | 'onpointermove' | 'onpointercancel' | 'onchange'> & {
2
+ import type { Option, OptionValue } from '../types/options';
3
+ import type { PopupPosition } from '../types/propOptions';
4
+ export type SelectProps = {
5
5
  name?: string;
6
- value: string | number | null | undefined;
7
- /** `{ label, value, disabled? }[]` */
8
- options: Option[];
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<string | number | null | undefined>;
48
+ onchange?: BivariantValueHandler<OptionValue>;
48
49
  };
49
50
  declare const Select: import("svelte").Component<SelectProps, {}, "value">;
50
51
  type Select = ReturnType<typeof Select>;
@@ -88,7 +88,7 @@
88
88
  // 基本プロパティ
89
89
  stepItems,
90
90
  items: itemsAlias,
91
- value = $bindable(''),
91
+ value = $bindable(),
92
92
  progress,
93
93
 
94
94
  // URL 方式
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@14ch/svelte-ui",
3
3
  "description": "Modern Svelte UI components library with TypeScript support",
4
4
  "private": false,
5
- "version": "0.0.67",
5
+ "version": "0.0.69",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "svelte",