@14ch/svelte-ui 0.0.15 → 0.0.16

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.
@@ -14,6 +14,8 @@ export type InputProps = {
14
14
  max?: number | null;
15
15
  step?: number | null;
16
16
  size?: number | null;
17
+ decimalPlaces?: number | null;
18
+ enableThousandsSeparator?: boolean;
17
19
  autocomplete?: HTMLInputElement['autocomplete'] | null;
18
20
  spellcheck?: boolean | null;
19
21
  inputAttributes?: HTMLInputAttributes | undefined;
@@ -41,6 +43,8 @@ export type InputProps = {
41
43
  required?: boolean;
42
44
  clearable?: boolean;
43
45
  linkify?: boolean;
46
+ enablePasswordVisibilityToggle?: boolean;
47
+ enableNumberStepper?: boolean;
44
48
  onfocus?: FocusHandler;
45
49
  onblur?: FocusHandler;
46
50
  onkeydown?: KeyboardHandler;
@@ -68,6 +72,18 @@ export type InputProps = {
68
72
  oninput?: BivariantValueHandler<string | number>;
69
73
  onRightIconClick?: MouseHandler;
70
74
  onLeftIconClick?: MouseHandler;
75
+ onRightIconMouseDown?: MouseHandler;
76
+ onLeftIconMouseDown?: MouseHandler;
77
+ onRightIconMouseUp?: MouseHandler;
78
+ onLeftIconMouseUp?: MouseHandler;
79
+ onRightIconMouseLeave?: MouseHandler;
80
+ onLeftIconMouseLeave?: MouseHandler;
81
+ onRightIconTouchStart?: TouchHandler;
82
+ onLeftIconTouchStart?: TouchHandler;
83
+ onRightIconTouchEnd?: TouchHandler;
84
+ onLeftIconTouchEnd?: TouchHandler;
85
+ onRightIconTouchCancel?: TouchHandler;
86
+ onLeftIconTouchCancel?: TouchHandler;
71
87
  [key: string]: any;
72
88
  };
73
89
  declare const Input: import("svelte").Component<InputProps, {
@@ -6,7 +6,7 @@
6
6
  import { t } from '../i18n';
7
7
  import { convertToHtml, convertToHtmlWithLink } from '../utils/formatText';
8
8
  import type { HTMLTextareaAttributes } from 'svelte/elements';
9
- import type { IconVariant } from '../types/icon';
9
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
10
10
  import type {
11
11
  FocusHandler,
12
12
  KeyboardHandler,
@@ -47,15 +47,21 @@
47
47
  rounded?: boolean;
48
48
  customStyle?: string;
49
49
 
50
+ // アイコン関連
51
+ clearButtonAriaLabel?: string;
52
+ iconFilled?: boolean;
53
+ iconWeight?: IconWeight;
54
+ iconGrade?: IconGrade;
55
+ iconOpticalSize?: IconOpticalSize;
56
+ iconVariant?: IconVariant;
57
+
50
58
  // 状態/動作
51
59
  disabled?: boolean;
52
60
  autoResize?: boolean;
53
61
  resizable?: boolean;
54
62
  clearable?: boolean;
55
- clearButtonAriaLabel?: string;
56
63
  readonly?: boolean;
57
64
  required?: boolean;
58
- iconVariant?: IconVariant;
59
65
  linkify?: boolean;
60
66
 
61
67
  // フォーカスイベント
@@ -127,15 +133,21 @@
127
133
  rounded = false,
128
134
  customStyle = '',
129
135
 
136
+ // アイコン関連
137
+ clearButtonAriaLabel = t('input.clear'),
138
+ iconFilled = false,
139
+ iconWeight = 300,
140
+ iconGrade = 0,
141
+ iconOpticalSize = 24,
142
+ iconVariant = 'outlined',
143
+
130
144
  // 状態/動作
131
145
  disabled = false,
132
146
  autoResize = true,
133
147
  resizable = false,
134
148
  clearable = false,
135
- clearButtonAriaLabel = t('input.clear'),
136
149
  readonly = false,
137
150
  required = false,
138
- iconVariant = 'outlined',
139
151
  linkify = false,
140
152
 
141
153
  // フォーカスイベント
@@ -179,10 +191,38 @@
179
191
  ...restProps
180
192
  }: TextareaProps = $props();
181
193
 
182
- let ref: HTMLTextAreaElement | null = null;
194
+ let textareaRef: HTMLTextAreaElement | null = null;
195
+ let containerRef: HTMLDivElement | null = null;
183
196
  let displayTextRef: HTMLDivElement | null = $state(null);
184
197
  let linkTextRef: HTMLDivElement | null = $state(null);
185
198
  let isFocused: boolean = $state(false);
199
+ let resizeObserver: ResizeObserver | null = null;
200
+
201
+ // =========================================================================
202
+ // $effect
203
+ // =========================================================================
204
+ // autoResize=false のとき、コンポーネント全体のサイズに display-text / link-text を合わせる
205
+ $effect(() => {
206
+ if (!containerRef || !displayTextRef || !textareaRef) {
207
+ cleanupSizeSync();
208
+ return;
209
+ }
210
+
211
+ // autoResize=true のときは、サイズ同期を行わず従来の挙動を維持
212
+ if (autoResize) {
213
+ cleanupSizeSync();
214
+ return;
215
+ }
216
+
217
+ // 初期同期
218
+ syncSizeFromTextarea();
219
+ // resizable=true のときにのみ ResizeObserver をセットアップ
220
+ setupResizeObserver();
221
+
222
+ return () => {
223
+ cleanupSizeSync();
224
+ };
225
+ });
186
226
 
187
227
  // =========================================================================
188
228
  // Methods
@@ -190,16 +230,16 @@
190
230
  const clear = (): void => {
191
231
  if (disabled || readonly) return;
192
232
  value = '';
193
- ref?.focus();
233
+ textareaRef?.focus();
194
234
  onchange?.('');
195
235
  };
196
236
 
197
237
  // 外部からフォーカスを当てる(キャレットを先頭に移動)
198
238
  export const focus = () => {
199
- if (ref) {
200
- ref.focus();
201
- ref.setSelectionRange(0, 0);
202
- ref.scrollTop = 0;
239
+ if (textareaRef) {
240
+ textareaRef.focus();
241
+ textareaRef.setSelectionRange(0, 0);
242
+ textareaRef.scrollTop = 0;
203
243
  }
204
244
  };
205
245
 
@@ -334,8 +374,8 @@
334
374
 
335
375
  // スクロール同期
336
376
  const handleScroll = () => {
337
- if (!ref) return;
338
- const scrollTop = ref.scrollTop;
377
+ if (!textareaRef) return;
378
+ const scrollTop = textareaRef.scrollTop;
339
379
  if (displayTextRef) {
340
380
  displayTextRef.scrollTop = scrollTop;
341
381
  }
@@ -344,15 +384,89 @@
344
384
  }
345
385
  };
346
386
 
387
+ // resizable=true のときに textarea のサイズ変更を監視
388
+ const setupResizeObserver = () => {
389
+ if (!textareaRef) return;
390
+
391
+ // resizable でなければ監視は不要
392
+ if (!resizable) {
393
+ if (resizeObserver) {
394
+ resizeObserver.disconnect();
395
+ resizeObserver = null;
396
+ }
397
+ return;
398
+ }
399
+
400
+ // 既存のオブザーバがあれば一旦解除
401
+ if (resizeObserver) {
402
+ resizeObserver.disconnect();
403
+ resizeObserver = null;
404
+ }
405
+
406
+ resizeObserver = new ResizeObserver(() => {
407
+ // ユーザーのリサイズ後は textarea の幅でコンテナ幅を上書きする
408
+ syncSizeFromTextarea({ forceWidth: true });
409
+ });
410
+ resizeObserver.observe(textareaRef);
411
+ };
412
+
413
+ // --------------------------------
414
+ // display-text / link-text と textarea のサイズ同期
415
+ // --------------------------------
416
+ // サイズ同期
417
+ const syncSizeFromTextarea = ({ forceWidth = false }: { forceWidth?: boolean } = {}) => {
418
+ if (!containerRef || !displayTextRef || !textareaRef) return;
419
+ const rect = textareaRef.getBoundingClientRect();
420
+ const height = rect.height;
421
+ const width = rect.width;
422
+ if (!height || !width) return;
423
+
424
+ // コンポーネント全体の高さは textarea に合わせる
425
+ containerRef.style.height = `${height}px`;
426
+
427
+ // 幅は、初期状態では width プロップを優先し、
428
+ // ユーザーがリサイズ(ResizeObserver 経由)したあとは textarea の幅で上書きする
429
+ if (forceWidth || !widthStyle) {
430
+ containerRef.style.width = `${width}px`;
431
+ }
432
+
433
+ // display-text / link-text はコンテナにフィットさせる
434
+ displayTextRef.style.height = '100%';
435
+ displayTextRef.style.width = '100%';
436
+ if (linkTextRef) {
437
+ linkTextRef.style.height = '100%';
438
+ linkTextRef.style.width = '100%';
439
+ }
440
+ };
441
+
442
+ // サイズ同期を解除
443
+ const cleanupSizeSync = () => {
444
+ if (resizeObserver) {
445
+ resizeObserver.disconnect();
446
+ resizeObserver = null;
447
+ }
448
+ if (containerRef) {
449
+ containerRef.style.removeProperty('height');
450
+ // width は width プロップで制御されるため、ここでは削除しない
451
+ }
452
+ if (displayTextRef) {
453
+ displayTextRef.style.removeProperty('height');
454
+ displayTextRef.style.removeProperty('width');
455
+ }
456
+ if (linkTextRef) {
457
+ linkTextRef.style.removeProperty('height');
458
+ linkTextRef.style.removeProperty('width');
459
+ }
460
+ };
461
+
347
462
  // =========================================================================
348
463
  // $derived
349
464
  // =========================================================================
350
- const minHeightStyle = $derived(getStyleFromNumber(minHeight));
351
- const maxHeightStyle = $derived(getStyleFromNumber(maxHeight));
352
- const widthStyle = $derived(getStyleFromNumber(width));
353
-
465
+ // --------------------------------
466
+ // display & link value
467
+ // --------------------------------
354
468
  // HTML表示用の値(autoResize時の高さ調整用)
355
- const htmlValue = $derived.by(() => {
469
+ const displayValue = $derived.by(() => {
356
470
  const normalizedValue = value ?? '';
357
471
  if (normalizedValue !== '') {
358
472
  const converted = convertToHtml(normalizedValue);
@@ -364,9 +478,9 @@
364
478
  }
365
479
  return html;
366
480
  } else {
367
- // inline かつ value が空のとき、placeholder がなければ
481
+ // inline かつ value が空のとき
368
482
  // 1行分の高さを確保するためにダミーの &nbsp; を入れる
369
- if (inline && !placeholder) {
483
+ if (inline) {
370
484
  return '&nbsp;';
371
485
  }
372
486
  return '';
@@ -382,9 +496,17 @@
382
496
  const result = convertToHtmlWithLink(normalizedValue);
383
497
  return String(result ?? '');
384
498
  });
499
+
500
+ // --------------------------------
501
+ // style from number
502
+ // --------------------------------
503
+ const minHeightStyle = $derived(getStyleFromNumber(minHeight));
504
+ const maxHeightStyle = $derived(getStyleFromNumber(maxHeight));
505
+ const widthStyle = $derived(getStyleFromNumber(width));
385
506
  </script>
386
507
 
387
508
  <div
509
+ bind:this={containerRef}
388
510
  class="textarea
389
511
  textarea--focus-{focusStyle}"
390
512
  class:textarea--inline={inline}
@@ -398,23 +520,21 @@
398
520
  class:textarea--readonly={readonly}
399
521
  class:textarea--focused={isFocused}
400
522
  data-testid="textarea"
401
- style={!inline ? `max-height: ${maxHeightStyle};` : ''}
523
+ style="width: {widthStyle}; {!inline ? `max-height: ${maxHeightStyle};` : ''}"
402
524
  >
403
- <!-- autoResize時の表示用要素(HTMLレンダリングで高さ調整) -->
404
525
  <div
405
526
  bind:this={displayTextRef}
406
527
  class="textarea__display-text"
407
- data-placeholder={placeholder}
408
528
  style="min-height: {minHeightStyle}; max-height: {maxHeightStyle}; {customStyle}"
409
529
  >
410
- {@html htmlValue}
530
+ {@html displayValue}
411
531
  </div>
412
532
  <div class="textarea__wrapper">
413
533
  <textarea
414
534
  {id}
415
535
  {name}
416
536
  bind:value
417
- bind:this={ref}
537
+ bind:this={textareaRef}
418
538
  {rows}
419
539
  {placeholder}
420
540
  {disabled}
@@ -427,7 +547,7 @@
427
547
  {spellcheck}
428
548
  {autocapitalize}
429
549
  class:resizable
430
- style="min-height: {minHeightStyle}; width: {widthStyle}; {customStyle}"
550
+ style="min-height: {minHeightStyle}; {customStyle}"
431
551
  onchange={handleChange}
432
552
  oninput={handleInput}
433
553
  onfocus={handleFocus}
@@ -472,13 +592,13 @@
472
592
  <div class="textarea__clear-button">
473
593
  <IconButton
474
594
  ariaLabel={clearButtonAriaLabel}
475
- color="var(--svelte-ui-text-color)"
595
+ color="var(--svelte-ui-textarea-icon-color)"
476
596
  onclick={(event) => {
477
597
  event.stopPropagation();
478
598
  clear();
479
599
  }}
480
600
  tabindex={-1}
481
- iconFilled={true}
601
+ {iconFilled}
482
602
  {iconVariant}
483
603
  fontSize={18}>cancel</IconButton
484
604
  >
@@ -519,10 +639,35 @@
519
639
  /* =============================================
520
640
  * 基本コンポーネント
521
641
  * ============================================= */
642
+ textarea {
643
+ width: 100%;
644
+ height: auto;
645
+ min-height: var(--svelte-ui-textarea-min-height);
646
+ padding: var(--svelte-ui-textarea-padding);
647
+ background-color: var(--svelte-ui-textarea-bg);
648
+ box-shadow: 0 0 0 var(--svelte-ui-border-width) inset var(--svelte-ui-textarea-border-color);
649
+ border: none;
650
+ border-radius: var(--svelte-ui-textarea-border-radius);
651
+ font-size: inherit;
652
+ font-weight: inherit;
653
+ color: inherit;
654
+ line-height: inherit;
655
+ text-align: inherit;
656
+ -webkit-appearance: none;
657
+ -moz-appearance: none;
658
+ appearance: none;
659
+
660
+ &:not(.resizable) {
661
+ resize: none;
662
+ }
663
+ }
664
+
522
665
  .textarea__display-text,
523
666
  .textarea__link-text {
524
667
  display: block;
525
668
  width: 100%;
669
+ min-height: var(--svelte-ui-textarea-min-height);
670
+ padding: var(--svelte-ui-textarea-padding);
526
671
  background: inherit;
527
672
  border: inherit;
528
673
  font-size: inherit;
@@ -559,40 +704,10 @@
559
704
  left: 0;
560
705
  width: 100%;
561
706
  height: 100%;
562
- padding: inherit;
563
707
  pointer-events: none;
564
708
  z-index: 1;
565
709
  }
566
710
 
567
- .textarea__link-text :global(a) {
568
- pointer-events: auto;
569
- text-decoration: underline;
570
- }
571
-
572
- textarea {
573
- position: absolute;
574
- top: 0;
575
- left: 0;
576
- width: 100%;
577
- height: 100%;
578
- padding: inherit;
579
- background: transparent;
580
- border: none;
581
- font-size: inherit;
582
- font-weight: inherit;
583
- color: inherit;
584
- line-height: inherit;
585
- text-align: inherit;
586
- opacity: 0;
587
- -webkit-appearance: none;
588
- -moz-appearance: none;
589
- appearance: none;
590
-
591
- &:not(.resizable) {
592
- resize: none;
593
- }
594
- }
595
-
596
711
  .textarea--clearable .textarea__clear-button {
597
712
  position: absolute;
598
713
  top: var(--svelte-ui-textarea-icon-top);
@@ -610,44 +725,11 @@
610
725
 
611
726
  .textarea--auto-resize {
612
727
  textarea {
728
+ height: 100%;
613
729
  overflow-y: auto;
614
730
  }
615
731
  }
616
732
 
617
- .textarea:not(.textarea--auto-resize) {
618
- .textarea__display-text {
619
- display: none;
620
- }
621
-
622
- textarea {
623
- position: static;
624
- height: auto;
625
- }
626
- }
627
-
628
- /* =============================================
629
- * 機能バリエーション
630
- * ============================================= */
631
- .textarea--clearable {
632
- textarea,
633
- .textarea__display-text,
634
- .textarea__link-text {
635
- padding-right: var(--svelte-ui-textarea-icon-space);
636
- }
637
- }
638
-
639
- @media (hover: hover) {
640
- .textarea--clearable:hover .textarea__clear-button {
641
- opacity: 1;
642
- }
643
- }
644
-
645
- @media (hover: none) {
646
- .textarea--clearable .textarea__clear-button {
647
- opacity: 1;
648
- }
649
- }
650
-
651
733
  /* =============================================
652
734
  * プレースホルダー・テキスト表示
653
735
  * ============================================= */
@@ -699,112 +781,103 @@
699
781
  }
700
782
 
701
783
  /* =============================================
702
- * 表示切り替え(フォーカス時・非inline)
784
+ * 表示切り替え
703
785
  * ============================================= */
704
- .textarea--focused,
705
- .textarea:not(.textarea--inline) {
786
+ .textarea:not(.textarea--focused) textarea {
787
+ color: transparent;
788
+ caret-color: transparent;
789
+ text-shadow: none;
790
+ }
791
+
792
+ .textarea--focused {
706
793
  .textarea__display-text {
707
794
  opacity: 0;
708
795
  }
709
-
710
- textarea {
711
- opacity: 1;
712
- }
713
796
  }
714
797
 
715
- /* linkify=true かつ非 inline のときは、display-text は常に非表示(レイアウトだけ保持) */
716
- .textarea--linkify:not(.textarea--inline) .textarea__display-text {
717
- opacity: 0;
798
+ /* =============================================
799
+ * 機能バリエーション
800
+ * ============================================= */
801
+ /* clearable */
802
+ .textarea--clearable {
803
+ textarea,
804
+ .textarea__display-text,
805
+ .textarea__link-text {
806
+ padding-right: var(--svelte-ui-textarea-icon-space);
807
+ }
718
808
  }
719
809
 
720
- /* フォーカス時はリンク用オーバーレイも非表示(opacity: 0)にして、リンクが反応しないようにする */
721
- .textarea--focused .textarea__link-text {
722
- opacity: 0;
810
+ @media (hover: hover) {
811
+ .textarea--clearable:hover .textarea__clear-button {
812
+ opacity: 1;
813
+ }
723
814
  }
724
815
 
725
- .textarea--focused .textarea__link-text :global(a) {
726
- pointer-events: none;
816
+ @media (hover: none) {
817
+ .textarea--clearable .textarea__clear-button {
818
+ opacity: 1;
819
+ }
727
820
  }
728
821
 
729
- /* =============================================
730
- * デザインバリアント:default
731
- * ============================================= */
732
- .textarea:not(.textarea--inline) {
733
- .textarea__display-text,
734
- .textarea__link-text,
735
- textarea {
736
- min-height: var(--svelte-ui-textarea-min-height);
737
- padding: var(--svelte-ui-textarea-padding);
822
+ /* linkify */
823
+ .textarea--linkify {
824
+ .textarea__display-text {
825
+ opacity: 0;
738
826
  }
739
827
 
740
- textarea {
741
- position: static;
742
- background-color: var(--svelte-ui-textarea-bg);
743
- box-shadow: 0 0 0 var(--svelte-ui-border-width) inset var(--svelte-ui-textarea-border-color);
744
- border: none;
745
- border-radius: var(--svelte-ui-textarea-border-radius);
828
+ .textarea__link-text :global(a) {
829
+ pointer-events: auto;
746
830
  }
747
831
 
748
- &.textarea--clearable {
749
- textarea,
750
- .textarea__display-text,
832
+ &.textarea--focused {
751
833
  .textarea__link-text {
752
- padding-right: var(--svelte-ui-textarea-icon-space);
834
+ opacity: 0;
753
835
  }
754
- }
755
- }
756
836
 
757
- /* linkify=true かつフォーカスがないときは、textarea のテキストカラーだけ透明にして二重描画を防ぐ
758
- * placeholder の色は textarea::placeholder 側で指定しているため、この指定の影響を受けない
759
- */
760
- .textarea--linkify:not(.textarea--focused) textarea {
761
- color: transparent;
762
- caret-color: transparent;
763
- text-shadow: none;
837
+ :global(a) {
838
+ pointer-events: none;
839
+ }
840
+ }
764
841
  }
765
842
 
766
843
  /* =============================================
767
- * デザインバリアント:rounded
844
+ * デザインバリエーション
768
845
  * ============================================= */
846
+ /* rounded */
769
847
  .textarea--rounded:not(.textarea--inline) {
770
848
  textarea {
771
849
  border-radius: var(--svelte-ui-textarea-border-radius-rounded);
772
850
  }
773
851
  }
774
852
 
775
- /* =============================================
776
- * デザインバリアント:inline
777
- * ============================================= */
853
+ /* inline */
778
854
  .textarea--inline {
779
- .textarea__display-text {
780
- opacity: 1;
781
- }
782
-
855
+ .textarea__display-text,
856
+ .textarea__link-text,
783
857
  textarea {
784
- opacity: 0;
858
+ min-height: auto;
859
+ padding-top: inherit;
860
+ padding-bottom: inherit;
861
+ padding-left: inherit;
785
862
  }
786
863
 
787
- &.textarea--focused {
788
- .textarea__display-text {
789
- opacity: 0;
790
- }
791
-
792
- textarea {
793
- opacity: 1;
794
- }
864
+ textarea {
865
+ padding: inherit;
866
+ background: transparent;
867
+ box-shadow: none;
868
+ border-radius: 0;
869
+ font-size: inherit;
870
+ font-weight: inherit;
871
+ color: inherit;
872
+ line-height: inherit;
873
+ text-align: inherit;
874
+ -webkit-appearance: none;
875
+ -moz-appearance: none;
876
+ appearance: none;
795
877
  }
796
878
 
797
879
  &.textarea--clearable .textarea__clear-button {
798
880
  top: var(--svelte-ui-textarea-icon-top-inline);
799
881
  }
800
882
  }
801
-
802
- /* inline + linkify のときは、display-text を常に隠し、textarea を常に表示 */
803
- .textarea--inline.textarea--linkify .textarea__display-text {
804
- opacity: 0;
805
- }
806
-
807
- .textarea--inline.textarea--linkify textarea {
808
- opacity: 1;
809
- }
810
883
  </style>
@@ -1,5 +1,5 @@
1
1
  import type { HTMLTextareaAttributes } from 'svelte/elements';
2
- import type { IconVariant } from '../types/icon';
2
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
3
3
  import type { FocusHandler, KeyboardHandler, MouseHandler, TouchHandler, PointerHandler } from '../types/callbackHandlers';
4
4
  import type { FocusStyle } from '../types/propOptions';
5
5
  export type TextareaProps = {
@@ -24,14 +24,18 @@ export type TextareaProps = {
24
24
  width?: string | number | null;
25
25
  rounded?: boolean;
26
26
  customStyle?: string;
27
+ clearButtonAriaLabel?: string;
28
+ iconFilled?: boolean;
29
+ iconWeight?: IconWeight;
30
+ iconGrade?: IconGrade;
31
+ iconOpticalSize?: IconOpticalSize;
32
+ iconVariant?: IconVariant;
27
33
  disabled?: boolean;
28
34
  autoResize?: boolean;
29
35
  resizable?: boolean;
30
36
  clearable?: boolean;
31
- clearButtonAriaLabel?: string;
32
37
  readonly?: boolean;
33
38
  required?: boolean;
34
- iconVariant?: IconVariant;
35
39
  linkify?: boolean;
36
40
  onfocus?: FocusHandler;
37
41
  onblur?: FocusHandler;