@14ch/svelte-ui 0.0.38 → 0.0.40

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.
@@ -292,7 +292,6 @@
292
292
  highlightedIndex = -1;
293
293
  isKeyboardNavigation = false;
294
294
  popupRef?.close();
295
- isFocused = false;
296
295
  break;
297
296
  }
298
297
  };
@@ -396,8 +395,10 @@
396
395
  };
397
396
 
398
397
  // Popup が閉じられたときの処理(isPopupOpen は bind:isOpen で同期される)
399
- const handlePopupClose = () => {
400
- isFocused = false;
398
+ const handlePopupClose = (reason: 'escape' | 'outside' | 'explicit') => {
399
+ if (reason !== 'escape') {
400
+ isFocused = false;
401
+ }
401
402
  highlightedIndex = -1;
402
403
  };
403
404
  const handleBlur = (event: FocusEvent) => {
@@ -14,9 +14,8 @@
14
14
  // =========================================================================
15
15
  export type MultiSelectProps = {
16
16
  // 基本プロパティ
17
- name?: string;
18
- /** Selected values array. Supports `bind:values`. */
19
- values: OptionValue[];
17
+ /** Selected values array. Supports `bind:value`. */
18
+ value: OptionValue[];
20
19
  /** `{ label, value, disabled? }[]` */
21
20
  options: Option[];
22
21
 
@@ -33,6 +32,7 @@
33
32
  focusStyle?: 'background' | 'outline' | 'none';
34
33
  fullWidth?: boolean;
35
34
  rounded?: boolean;
35
+ customStyle?: string;
36
36
 
37
37
  // 状態/動作
38
38
  disabled?: boolean;
@@ -49,8 +49,7 @@
49
49
  };
50
50
 
51
51
  let {
52
- name,
53
- values = $bindable([]),
52
+ value = $bindable([]),
54
53
  options = [],
55
54
 
56
55
  id = `multi-select-${Math.random().toString(36).substring(2, 15)}`,
@@ -62,6 +61,7 @@
62
61
  focusStyle = 'outline',
63
62
  fullWidth = false,
64
63
  rounded = false,
64
+ customStyle = '',
65
65
 
66
66
  disabled = false,
67
67
  required = false,
@@ -85,19 +85,19 @@
85
85
  const listboxId = $derived(`${id}-listbox`);
86
86
 
87
87
  const selectedLabels = $derived(
88
- options.filter((o) => values.includes(o.value)).map((o) => o.label)
88
+ options.filter((o) => value.includes(o.value)).map((o) => o.label)
89
89
  );
90
90
 
91
91
  // =========================================================================
92
92
  // Methods
93
93
  // =========================================================================
94
94
  const toggleOption = (optionValue: OptionValue) => {
95
- if (values.includes(optionValue)) {
96
- values = values.filter((v) => v !== optionValue);
95
+ if (value.includes(optionValue)) {
96
+ value = value.filter((v) => v !== optionValue);
97
97
  } else {
98
- values = [...values, optionValue];
98
+ value = [...value, optionValue];
99
99
  }
100
- onchange(values);
100
+ onchange(value);
101
101
  };
102
102
 
103
103
  const handleTriggerClick = () => {
@@ -129,8 +129,10 @@
129
129
  triggerWidth = triggerEl?.offsetWidth ?? 0;
130
130
  };
131
131
 
132
- const handlePopupClose = () => {
133
- triggerEl?.focus();
132
+ const handlePopupClose = (reason: 'escape' | 'outside' | 'explicit') => {
133
+ if (reason !== 'outside') {
134
+ triggerEl?.focus();
135
+ }
134
136
  };
135
137
  </script>
136
138
 
@@ -156,24 +158,27 @@
156
158
  aria-required={required ? 'true' : undefined}
157
159
  {tabindex}
158
160
  {disabled}
161
+ style={customStyle}
159
162
  onfocus={handleFocus}
160
163
  onblur={handleBlur}
161
164
  onclick={handleTriggerClick}
162
165
  onkeydown={handleTriggerKeydown}
163
166
  >
164
- {#if selectedLabels.length > 0}
165
- <span class="multi-select__display-text">
166
- {#each selectedLabels as label, i}
167
- <span>{label}{#if i < selectedLabels.length - 1},{/if}</span>
168
- {/each}
167
+ <span class="multi-select__inner">
168
+ {#if selectedLabels.length > 0}
169
+ <span class="multi-select__display-text">
170
+ {#each selectedLabels as label, i}
171
+ <span>{label}{#if i < selectedLabels.length - 1},{/if}</span>
172
+ {/each}
173
+ </span>
174
+ {:else if placeholder}
175
+ <span class="multi-select__placeholder">{placeholder}</span>
176
+ {/if}
177
+ <span class="multi-select__dropdown-icon" aria-hidden="true">
178
+ <Icon>arrow_drop_down</Icon>
169
179
  </span>
170
- {:else if placeholder}
171
- <span class="multi-select__placeholder">{placeholder}</span>
172
- {/if}
180
+ </span>
173
181
  </button>
174
- <div class="multi-select__dropdown-icon" aria-hidden="true">
175
- <Icon>arrow_drop_down</Icon>
176
- </div>
177
182
 
178
183
  <Popup
179
184
  bind:this={popupRef}
@@ -196,7 +201,7 @@
196
201
  {#each options as option, i (option.value)}
197
202
  <li role="presentation" class="multi-select__item">
198
203
  <Checkbox
199
- value={values.includes(option.value)}
204
+ value={value.includes(option.value)}
200
205
  disabled={option.disabled}
201
206
  fullWidth
202
207
  customStyle="padding: 8px 12px"
@@ -229,7 +234,7 @@
229
234
  width: 100%;
230
235
  min-height: var(--svelte-ui-select-height);
231
236
  padding: var(--svelte-ui-select-padding);
232
- padding-right: var(--svelte-ui-select-icon-space);
237
+ padding-right: 0;
233
238
  background: transparent;
234
239
  border: none;
235
240
  font-family: inherit;
@@ -247,6 +252,15 @@
247
252
  outline-offset: var(--svelte-ui-focus-outline-offset-inner);
248
253
  }
249
254
 
255
+ .multi-select__inner {
256
+ position: relative;
257
+ flex: 1;
258
+ align-self: stretch;
259
+ display: flex;
260
+ align-items: center;
261
+ padding-right: var(--svelte-ui-select-icon-space);
262
+ }
263
+
250
264
  .multi-select__display-text {
251
265
  display: flex;
252
266
  flex-wrap: wrap;
@@ -269,11 +283,12 @@
269
283
  justify-content: center;
270
284
  align-items: center;
271
285
  position: absolute;
272
- top: 50%;
286
+ top: 0;
273
287
  right: 4px;
288
+ margin-top: calc((var(--svelte-ui-select-height) - 32px) / 2);
289
+ margin-bottom: calc((var(--svelte-ui-select-height) - 32px) / 2);
274
290
  width: 32px;
275
291
  height: 32px;
276
- transform: translateY(-50%);
277
292
  font-size: var(--svelte-ui-select-dropdown-icon-size);
278
293
  color: var(--svelte-ui-select-dropdown-icon-color);
279
294
  pointer-events: none;
@@ -327,7 +342,7 @@
327
342
  * ============================================= */
328
343
  .multi-select.multi-select--inline .multi-select__trigger {
329
344
  padding: inherit;
330
- padding-right: var(--svelte-ui-input-icon-space-inline);
345
+ padding-right: 0;
331
346
  background: transparent;
332
347
  border: none;
333
348
  border-radius: 0;
@@ -335,8 +350,16 @@
335
350
  min-height: auto;
336
351
  line-height: inherit;
337
352
  }
353
+ .multi-select.multi-select--inline .multi-select__inner {
354
+ align-self: auto;
355
+ align-items: flex-start;
356
+ min-height: 1lh;
357
+ padding-right: var(--svelte-ui-input-icon-space-inline);
358
+ }
338
359
  .multi-select.multi-select--inline .multi-select__dropdown-icon {
339
360
  right: 0;
361
+ margin-top: 0;
362
+ margin-bottom: 0;
340
363
  }
341
364
 
342
365
  /* =============================================
@@ -2,9 +2,8 @@ import type { Option, OptionValue } from '../types/options';
2
2
  import type { BivariantValueHandler, FocusHandler } from '../types/callbackHandlers';
3
3
  import type { PopupPosition } from '../types/propOptions';
4
4
  export type MultiSelectProps = {
5
- name?: string;
6
- /** Selected values array. Supports `bind:values`. */
7
- values: OptionValue[];
5
+ /** Selected values array. Supports `bind:value`. */
6
+ value: OptionValue[];
8
7
  /** `{ label, value, disabled? }[]` */
9
8
  options: Option[];
10
9
  id?: string | null;
@@ -17,6 +16,7 @@ export type MultiSelectProps = {
17
16
  focusStyle?: 'background' | 'outline' | 'none';
18
17
  fullWidth?: boolean;
19
18
  rounded?: boolean;
19
+ customStyle?: string;
20
20
  disabled?: boolean;
21
21
  required?: boolean;
22
22
  /** @default 'bottom-left' */
@@ -25,6 +25,6 @@ export type MultiSelectProps = {
25
25
  onblur?: FocusHandler;
26
26
  onchange?: BivariantValueHandler<OptionValue[]>;
27
27
  };
28
- declare const MultiSelect: import("svelte").Component<MultiSelectProps, {}, "values">;
28
+ declare const MultiSelect: import("svelte").Component<MultiSelectProps, {}, "value">;
29
29
  type MultiSelect = ReturnType<typeof MultiSelect>;
30
30
  export default MultiSelect;
@@ -19,7 +19,7 @@
19
19
  import { isMobileDevice, disableBodyScroll } from '../utils/mobile';
20
20
  import { announceOpenClose } from '../utils/accessibility';
21
21
  import { popupManager } from '../utils/popupManager';
22
- import type { PopupPosition } from '../types/propOptions';
22
+ import type { CloseReason, PopupPosition } from '../types/propOptions';
23
23
 
24
24
  // =========================================================================
25
25
  // Props, States & Constants
@@ -64,7 +64,7 @@
64
64
 
65
65
  // イベントハンドラー
66
66
  onOpen?: () => void;
67
- onClose?: () => void;
67
+ onClose?: (reason: CloseReason) => void;
68
68
  };
69
69
 
70
70
  let {
@@ -105,6 +105,7 @@
105
105
  let previousActiveElement: HTMLElement | null = null;
106
106
  let isMobile: boolean = $state(false);
107
107
  let bodyScrollCleanup: (() => void) | undefined = $state();
108
+ let pendingCloseReason: CloseReason = 'explicit';
108
109
 
109
110
  // =========================================================================
110
111
  // Lifecycle
@@ -147,7 +148,7 @@
147
148
  switch (event.key) {
148
149
  case 'Escape':
149
150
  event.preventDefault();
150
- close();
151
+ close('escape');
151
152
  break;
152
153
  case 'Tab':
153
154
  if (focusTrap) {
@@ -343,15 +344,17 @@
343
344
  }
344
345
  };
345
346
 
347
+ const closeOnResize = () => close();
348
+
346
349
  const addEventListenersToClose = () => {
347
350
  if (typeof window === 'undefined' || typeof document === 'undefined') return;
348
- window.addEventListener('resize', close);
351
+ window.addEventListener('resize', closeOnResize);
349
352
  document.addEventListener('scroll', handleScroll, true);
350
353
  };
351
354
 
352
355
  const removeEventListenersToClose = () => {
353
356
  if (typeof window === 'undefined' || typeof document === 'undefined') return;
354
- window.removeEventListener('resize', close);
357
+ window.removeEventListener('resize', closeOnResize);
355
358
  document.removeEventListener('scroll', handleScroll, true);
356
359
  };
357
360
 
@@ -413,7 +416,9 @@
413
416
  previousActiveElement = null;
414
417
 
415
418
  // アニメーション完了後にonCloseを呼ぶ
416
- onClose?.();
419
+ const reason = pendingCloseReason;
420
+ pendingCloseReason = 'explicit';
421
+ onClose?.(reason);
417
422
  };
418
423
 
419
424
  const clickOutside = (element: HTMLElement, callbackFunction: () => void) => {
@@ -498,7 +503,10 @@
498
503
  };
499
504
 
500
505
  /** Closes the popup. */
501
- export const close = () => {
506
+ export const close = (reason: CloseReason = 'explicit') => {
507
+ if (pendingCloseReason === 'explicit') {
508
+ pendingCloseReason = reason;
509
+ }
502
510
  isOpen = false;
503
511
  removeEventListenersToClose();
504
512
  removeKeyboardListener();
@@ -544,7 +552,7 @@
544
552
  aria-modal={undefined}
545
553
  id={popupId}
546
554
  use:clickOutside={() => {
547
- close();
555
+ close('outside');
548
556
  }}
549
557
  >
550
558
  {@render children()}
@@ -11,7 +11,7 @@
11
11
  * This prevents state synchronization bugs and ensures consistent behavior.
12
12
  */
13
13
  import type { Snippet } from 'svelte';
14
- import type { PopupPosition } from '../types/propOptions';
14
+ import type { CloseReason, PopupPosition } from '../types/propOptions';
15
15
  export type PopupProps = {
16
16
  children: Snippet;
17
17
  /** The element the popup is anchored to. */
@@ -37,11 +37,11 @@ export type PopupProps = {
37
37
  ariaLabelledby?: string;
38
38
  ariaDescribedby?: string;
39
39
  onOpen?: () => void;
40
- onClose?: () => void;
40
+ onClose?: (reason: CloseReason) => void;
41
41
  };
42
42
  declare const Popup: import("svelte").Component<PopupProps, {
43
43
  /** Opens the popup. */ open: () => Promise<void>;
44
- /** Closes the popup. */ close: () => void;
44
+ /** Closes the popup. */ close: (reason?: CloseReason) => void;
45
45
  /** Toggles between open and closed. */ toggle: () => void;
46
46
  /** Returns whether the popup is currently open. */ getIsOpen: () => boolean;
47
47
  }, "isOpen">;
@@ -189,10 +189,10 @@
189
189
  announceMenuOpened();
190
190
  };
191
191
 
192
- const handlePopupClose = () => {
192
+ const handlePopupClose = (reason: 'escape' | 'outside' | 'explicit') => {
193
193
  activeIndex = -1;
194
194
 
195
- if (anchorElement) {
195
+ if (anchorElement && reason !== 'outside') {
196
196
  anchorElement.focus();
197
197
  }
198
198
  };
@@ -66,3 +66,11 @@ export type NavVariant = 'vertical' | 'horizontal' | 'mobile';
66
66
  * - `bottom-sheet`: child items appear in a fixed bottom sheet overlay (mobile only)
67
67
  */
68
68
  export type ChildrenVariant = 'popup' | 'accordion' | 'expanded' | 'bar' | 'bottom-sheet';
69
+ /**
70
+ * Reason why a Popup was closed.
71
+ * Used by Popup's onClose callback to allow consumers to differentiate close triggers.
72
+ * - `escape`: closed via Escape key
73
+ * - `outside`: closed by clicking outside the popup
74
+ * - `explicit`: closed programmatically (e.g. popupRef.close(), selecting an option)
75
+ */
76
+ export type CloseReason = 'escape' | 'outside' | 'explicit';
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.38",
5
+ "version": "0.0.40",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "svelte",