mertani-web-toolkit 0.1.55 → 0.1.57

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.
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import Icon from '../../Icon/Icon.svelte';
3
+ import { SvelteDate } from 'svelte/reactivity';
3
4
  import './DatePicker.css';
4
5
 
5
6
  interface Props {
@@ -32,8 +33,8 @@
32
33
  placement?: 'top' | 'bottom' | 'auto';
33
34
 
34
35
  // Events
35
- onSelect?: (value: Date) => void;
36
- onChange?: (value: Date) => void;
36
+ onSelect?: (value: Date | null) => void;
37
+ onChange?: (value: Date | null) => void;
37
38
 
38
39
  // Validation
39
40
  isMandatory?: boolean;
@@ -243,7 +244,7 @@
243
244
 
244
245
  const dates = $derived(() => {
245
246
  const cells: DateCell[] = [];
246
- const cur = new Date(calendarDate.getFullYear(), calendarDate.getMonth(), 1);
247
+ const cur = new SvelteDate(calendarDate.getFullYear(), calendarDate.getMonth(), 1);
247
248
  if (cur.getDay() > 0) cur.setDate(cur.getDate() - cur.getDay());
248
249
 
249
250
  for (let i = 0; i < 42; i++) {
@@ -287,6 +288,15 @@
287
288
  error = validateInput(newDate);
288
289
  }
289
290
 
291
+ function clearDate() {
292
+ selectedDate = null;
293
+ value = null;
294
+ show = false;
295
+ error = validateInput(null);
296
+ onSelect?.(null);
297
+ onChange?.(null);
298
+ }
299
+
290
300
  function goToToday() {
291
301
  calendarDate = new Date(globalNow.getFullYear(), globalNow.getMonth(), 1);
292
302
  selectDate({
@@ -374,7 +384,34 @@
374
384
  <span class:placeholder={!displayValue}>
375
385
  {displayValue ?? placeholder}
376
386
  </span>
377
- <Icon name="bs-calendar2-date" />
387
+ <span class="date-picker-actions">
388
+ {#if displayValue && !disabled && !isLoading}
389
+ <span
390
+ class="date-picker-clear"
391
+ role="button"
392
+ tabindex="0"
393
+ aria-label="Clear date"
394
+ onmousedown={(e: MouseEvent) => {
395
+ e.preventDefault();
396
+ e.stopPropagation();
397
+ }}
398
+ onclick={(e: MouseEvent) => {
399
+ e.stopPropagation();
400
+ clearDate();
401
+ }}
402
+ onkeydown={(e: KeyboardEvent) => {
403
+ if (e.key === 'Enter' || e.key === ' ') {
404
+ e.preventDefault();
405
+ e.stopPropagation();
406
+ clearDate();
407
+ }
408
+ }}
409
+ >
410
+ <Icon name="bs-x-lg" />
411
+ </span>
412
+ {/if}
413
+ <Icon name="bs-calendar2-date" />
414
+ </span>
378
415
  </button>
379
416
 
380
417
  {#if show}
@@ -391,7 +428,7 @@
391
428
 
392
429
  <div class="calendar-body">
393
430
  <div class="date-grid">
394
- {#each DAY_LABELS[locale] as day}
431
+ {#each DAY_LABELS[locale] as day, i (i)}
395
432
  <p class="day-label">{day}</p>
396
433
  {/each}
397
434
  {#each dates() as cell (cell.year + '-' + cell.month + '-' + cell.date)}
@@ -470,4 +507,33 @@
470
507
  background: #f6f8fa;
471
508
  }
472
509
  }
510
+
511
+ .date-picker-trigger {
512
+ display: flex;
513
+ align-items: center;
514
+ justify-content: space-between;
515
+ gap: 8px;
516
+ }
517
+
518
+ .date-picker-actions {
519
+ display: inline-flex;
520
+ align-items: center;
521
+ gap: 8px;
522
+ flex: 0 0 auto;
523
+ }
524
+
525
+ .date-picker-clear {
526
+ display: inline-flex;
527
+ align-items: center;
528
+ justify-content: center;
529
+ width: 24px;
530
+ height: 24px;
531
+ border-radius: 6px;
532
+ opacity: 0.8;
533
+ }
534
+
535
+ .date-picker-clear:hover {
536
+ background: rgba(0, 0, 0, 0.06);
537
+ opacity: 1;
538
+ }
473
539
  </style>
@@ -20,8 +20,8 @@ interface Props {
20
20
  minDate?: Date | string | null;
21
21
  maxDate?: Date | string | null;
22
22
  placement?: 'top' | 'bottom' | 'auto';
23
- onSelect?: (value: Date) => void;
24
- onChange?: (value: Date) => void;
23
+ onSelect?: (value: Date | null) => void;
24
+ onChange?: (value: Date | null) => void;
25
25
  isMandatory?: boolean;
26
26
  customValidation?: (value: Date | null) => string | null;
27
27
  isLoading?: boolean;
@@ -0,0 +1,199 @@
1
+ .dropdown-container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ width: 100%;
5
+ }
6
+
7
+ .dropdown-container.dropdown-side {
8
+ flex-direction: row;
9
+ align-items: center;
10
+ gap: 12px;
11
+ }
12
+
13
+ .dropdown-label {
14
+ font-size: 14px;
15
+ font-weight: 400;
16
+ color: var(--color-text-primary);
17
+ margin-bottom: 4px;
18
+ display: block;
19
+ }
20
+
21
+ .dropdown-container.dropdown-side .dropdown-label {
22
+ margin-bottom: 0;
23
+ min-width: 120px;
24
+ }
25
+
26
+ .dropdown-label.label-left {
27
+ text-align: left;
28
+ }
29
+
30
+ .dropdown-label.label-right {
31
+ text-align: right;
32
+ }
33
+
34
+ .label-required {
35
+ color: var(--color-text-error-ti);
36
+ margin-left: 4px;
37
+ }
38
+
39
+ .label-subLabel {
40
+ font-weight: 400;
41
+ color: var(--color-text-tertiary);
42
+ margin-left: 4px;
43
+ }
44
+
45
+ .dropdown-trigger {
46
+ flex: 1;
47
+ display: flex;
48
+ width: 100%;
49
+ align-items: center;
50
+ justify-content: space-between;
51
+ gap: 0.5rem;
52
+ border-radius: 6px;
53
+ border: 1px solid var(--color-border-form);
54
+ padding: 0.625rem 1rem;
55
+ text-align: left;
56
+ font-size: 14px;
57
+ color: var(--color-text-primary);
58
+ transition:
59
+ border-color 0.2s,
60
+ box-shadow 0.2s;
61
+ }
62
+
63
+ .dropdown-trigger:not(:disabled) {
64
+ background: var(--color-bg-surface);
65
+ cursor: pointer;
66
+ }
67
+
68
+ .dropdown-trigger:not(:disabled):hover {
69
+ border-color: var(--color-border-form);
70
+ }
71
+
72
+ .dropdown-trigger:not(:disabled):focus,
73
+ .dropdown-trigger:not(:disabled).open {
74
+ outline: none;
75
+ border-color: var(--color-bg-act-primary);
76
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-bg-act-primary) 25%, transparent);
77
+ }
78
+
79
+ .dropdown-trigger:disabled {
80
+ cursor: not-allowed;
81
+ background: var(--color-bg-disabled);
82
+ }
83
+
84
+ .dropdown-trigger.error:not(:disabled) {
85
+ border-color: var(--color-text-error-ti);
86
+ }
87
+
88
+ .dropdown-trigger.error:not(:disabled):focus {
89
+ border-color: var(--color-text-error-ti);
90
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-text-error-ti) 25%, transparent);
91
+ }
92
+
93
+ .dropdown-value {
94
+ flex: 1;
95
+ min-width: 0;
96
+ white-space: nowrap;
97
+ overflow: hidden;
98
+ text-overflow: ellipsis;
99
+ text-align: left;
100
+ }
101
+
102
+ .dropdown-chevron {
103
+ flex-shrink: 0;
104
+ display: inline-flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ width: 16px;
108
+ height: 16px;
109
+ opacity: 0.7;
110
+ transition: transform 0.2s;
111
+ }
112
+
113
+ .dropdown-trigger.open .dropdown-chevron {
114
+ transform: rotate(180deg);
115
+ }
116
+
117
+ .dropdown-chevron :global(svg) {
118
+ width: 100%;
119
+ height: 100%;
120
+ }
121
+
122
+ .dropdown-menu {
123
+ position: absolute;
124
+ top: calc(100% + 4px);
125
+ left: 0;
126
+ right: 0;
127
+ min-width: 100%;
128
+ z-index: 50;
129
+ overflow: hidden;
130
+ border-radius: 6px;
131
+ background: var(--color-bg-surface);
132
+ border: 1px solid var(--color-border-form);
133
+ box-shadow:
134
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1),
135
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
136
+ }
137
+
138
+ .dropdown-options {
139
+ max-height: 14rem;
140
+ overflow-y: auto;
141
+ padding: 0.25rem 0;
142
+ }
143
+
144
+ .dropdown-option {
145
+ display: flex;
146
+ width: 100%;
147
+ align-items: center;
148
+ gap: 0.5rem;
149
+ padding: 0.5rem 0.75rem;
150
+ text-align: left;
151
+ font-size: 14px;
152
+ transition: background-color 0.15s;
153
+ color: var(--color-text-primary);
154
+ background: transparent;
155
+ border: none;
156
+ cursor: pointer;
157
+ }
158
+
159
+ .dropdown-option:hover:not(.selected) {
160
+ background: var(--color-bg-disabled);
161
+ }
162
+
163
+ .dropdown-option.selected {
164
+ color: var(--color-bg-act-primary);
165
+ font-weight: 500;
166
+ }
167
+
168
+ .dropdown-option-leading {
169
+ flex-shrink: 0;
170
+ display: inline-flex;
171
+ align-items: center;
172
+ justify-content: center;
173
+ }
174
+
175
+ .dropdown-empty {
176
+ padding: 0.5rem 0.75rem;
177
+ font-size: 12px;
178
+ color: var(--color-text-tertiary);
179
+ }
180
+
181
+ .error-message {
182
+ margin-top: 4px;
183
+ font-size: 12px;
184
+ color: var(--color-text-error-ti);
185
+ }
186
+
187
+ .relative {
188
+ position: relative;
189
+ }
190
+
191
+ .flex-1 {
192
+ flex: 1;
193
+ }
194
+
195
+ .truncate {
196
+ overflow: hidden;
197
+ text-overflow: ellipsis;
198
+ white-space: nowrap;
199
+ }
@@ -0,0 +1,316 @@
1
+ <script lang="ts">
2
+ import Icon from '../../Icon/Icon.svelte';
3
+ import type { Snippet } from 'svelte';
4
+ import './Dropdown.css';
5
+
6
+ export type DropdownOption = { value: string; label: string };
7
+
8
+ interface Props {
9
+ // ===Styles===
10
+ // Label
11
+ labelColor?: string;
12
+ aligment?: 'side' | 'top';
13
+ position?: 'left' | 'right';
14
+
15
+ // Field
16
+ size?: 48 | 40 | 32;
17
+ backgroundColor?: string;
18
+ borderColor?: string;
19
+ accentColor?: string;
20
+ textColor?: string;
21
+ errorColor?: string;
22
+ borderRadius?: number;
23
+ boxShadow?: string;
24
+ minWidth?: string;
25
+
26
+ // ===Properties===
27
+ // Data
28
+ id?: string;
29
+ label?: string;
30
+ subLabel?: string;
31
+ options?: DropdownOption[];
32
+ value?: string;
33
+ placeholder?: string;
34
+ emptyMessage?: string;
35
+ ariaLabel?: string;
36
+
37
+ // Events
38
+ onSelect?: (value: string) => void;
39
+
40
+ // Validation
41
+ isMandatory?: boolean;
42
+ customValidation?: (value: string) => string | null;
43
+
44
+ // Additional Actions
45
+ isLoading?: boolean;
46
+ isShow?: boolean;
47
+ disabled?: boolean;
48
+ tooltip?: string;
49
+
50
+ // Slots
51
+ optionLeading?: Snippet<[DropdownOption]>;
52
+
53
+ // Any
54
+ class?: string;
55
+ style?: string;
56
+ }
57
+
58
+ let {
59
+ // ===Styles===
60
+ labelColor = 'var(--color-text-primary)',
61
+ aligment = 'top',
62
+ position = 'left',
63
+
64
+ size = 40,
65
+ backgroundColor = 'var(--color-bg-surface)',
66
+ borderColor = 'var(--color-border-form)',
67
+ accentColor = 'var(--color-bg-act-primary)',
68
+ textColor = 'var(--color-text-primary)',
69
+ errorColor = 'var(--color-text-error-ti)',
70
+ borderRadius,
71
+ boxShadow = '',
72
+ minWidth = '160px',
73
+
74
+ // ===Properties===
75
+ id = '',
76
+ label = '',
77
+ subLabel = '',
78
+ options = [],
79
+ value = $bindable<string>(''),
80
+ placeholder = 'Pilih...',
81
+ emptyMessage = 'Tidak ada hasil',
82
+ ariaLabel = 'Pilih opsi',
83
+
84
+ // Events
85
+ onSelect,
86
+
87
+ // Validation
88
+ isMandatory = false,
89
+ customValidation,
90
+
91
+ // Additional Actions
92
+ isLoading = false,
93
+ isShow = true,
94
+ disabled = false,
95
+ tooltip = '',
96
+
97
+ // Slots
98
+ optionLeading,
99
+
100
+ // Any
101
+ class: className = '',
102
+ style: customStyle = ''
103
+ }: Props = $props();
104
+
105
+ let open = $state(false);
106
+ let triggerEl: HTMLButtonElement | null = $state(null);
107
+ let menuEl: HTMLDivElement | null = $state(null);
108
+ let isFocused = $state(false);
109
+
110
+ const error = $derived(validateInput(value));
111
+
112
+ const sizeConfig = $derived.by(() => {
113
+ switch (size) {
114
+ case 48:
115
+ return {
116
+ height: '48px',
117
+ labelFontSize: '16px',
118
+ fontSize: '16px',
119
+ borderRadius: 8,
120
+ padding: '8px 12px'
121
+ };
122
+ case 40:
123
+ return {
124
+ height: '40px',
125
+ labelFontSize: '14px',
126
+ fontSize: '14px',
127
+ borderRadius: 8,
128
+ padding: '6px 12px'
129
+ };
130
+ case 32:
131
+ return {
132
+ height: '32px',
133
+ labelFontSize: '14px',
134
+ fontSize: '14px',
135
+ borderRadius: 6,
136
+ padding: '6px 12px'
137
+ };
138
+ default:
139
+ return {
140
+ height: '40px',
141
+ labelFontSize: '14px',
142
+ fontSize: '14px',
143
+ borderRadius: 8,
144
+ padding: '6px 12px'
145
+ };
146
+ }
147
+ });
148
+
149
+ const labelStyles = $derived.by(() => {
150
+ const s: string[] = [`font-size: ${sizeConfig.labelFontSize};`];
151
+ if (labelColor) s.push(`color: ${labelColor};`);
152
+ return s.join(' ');
153
+ });
154
+
155
+ const triggerStyles = $derived.by(() => {
156
+ const s: string[] = [];
157
+ s.push(`height: ${sizeConfig.height};`);
158
+ s.push(`padding: ${sizeConfig.padding};`);
159
+ s.push(`font-size: ${sizeConfig.fontSize};`);
160
+ s.push(`color: ${textColor};`);
161
+ const radius = borderRadius ?? sizeConfig.borderRadius;
162
+ s.push(`border-radius: ${radius}px;`);
163
+ if (minWidth) s.push(`min-width: ${minWidth};`);
164
+ if (boxShadow) s.push(`box-shadow: ${boxShadow};`);
165
+ if (disabled || isLoading) {
166
+ s.push('background: var(--color-bg-disabled);');
167
+ } else if (backgroundColor) {
168
+ s.push(`background: ${backgroundColor};`);
169
+ }
170
+ if (error) {
171
+ s.push(`border-color: ${errorColor};`);
172
+ s.push(`box-shadow: 0 0 0 2px color-mix(in srgb, ${errorColor} 25%, transparent);`);
173
+ } else if (isFocused || open) {
174
+ s.push(`border-color: ${accentColor};`);
175
+ s.push(`box-shadow: 0 0 0 2px color-mix(in srgb, ${accentColor} 25%, transparent);`);
176
+ } else if (borderColor) {
177
+ s.push(`border-color: ${borderColor};`);
178
+ }
179
+ if (customStyle) s.push(customStyle);
180
+ return s.join(' ');
181
+ });
182
+
183
+ const currentLabel = $derived(options.find((o) => o.value === value)?.label ?? placeholder);
184
+
185
+ function validateInput(val: string): string {
186
+ if (customValidation) {
187
+ const err = customValidation(val);
188
+ if (err) return err;
189
+ }
190
+ if (isMandatory && !val) return 'Field ini wajib diisi';
191
+ return '';
192
+ }
193
+
194
+ function toggle() {
195
+ if (disabled || isLoading) return;
196
+ open = !open;
197
+ }
198
+
199
+ function select(option: DropdownOption) {
200
+ value = option.value;
201
+ open = false;
202
+ onSelect?.(option.value);
203
+ }
204
+
205
+ function handleClickOutside(e: MouseEvent) {
206
+ const target = e.target as Node;
207
+ if (triggerEl && menuEl && !triggerEl.contains(target) && !menuEl.contains(target)) {
208
+ open = false;
209
+ }
210
+ }
211
+
212
+ function handleKeydown(e: KeyboardEvent) {
213
+ if (e.key === 'Escape') {
214
+ open = false;
215
+ }
216
+ if (e.key === 'Enter' || e.key === ' ') {
217
+ e.preventDefault();
218
+ toggle();
219
+ }
220
+ }
221
+
222
+ function handleFocus() {
223
+ isFocused = true;
224
+ }
225
+
226
+ function handleBlur() {
227
+ isFocused = false;
228
+ }
229
+
230
+ $effect(() => {
231
+ if (open) {
232
+ document.addEventListener('click', handleClickOutside);
233
+ } else {
234
+ document.removeEventListener('click', handleClickOutside);
235
+ }
236
+ return () => document.removeEventListener('click', handleClickOutside);
237
+ });
238
+ </script>
239
+
240
+ {#if isShow}
241
+ <div class="dropdown-container" class:dropdown-side={aligment === 'side'}>
242
+ {#if label}
243
+ <label
244
+ for={id}
245
+ class="dropdown-label"
246
+ class:label-left={position === 'left'}
247
+ class:label-right={position === 'right'}
248
+ style={labelStyles}
249
+ >
250
+ {label}
251
+ {#if subLabel}
252
+ <span class="label-subLabel">{subLabel}</span>
253
+ {/if}
254
+ {#if isMandatory}
255
+ <span class="label-required">*</span>
256
+ {/if}
257
+ </label>
258
+ {/if}
259
+ <div class="relative flex-1" style="--dropdown-min-width: {minWidth}">
260
+ <button
261
+ bind:this={triggerEl}
262
+ type="button"
263
+ class="dropdown-trigger {className} {error ? 'error' : ''}"
264
+ class:open
265
+ style={triggerStyles}
266
+ aria-haspopup="listbox"
267
+ aria-expanded={open}
268
+ aria-label={ariaLabel}
269
+ onclick={toggle}
270
+ onkeydown={handleKeydown}
271
+ onfocus={handleFocus}
272
+ onblur={handleBlur}
273
+ disabled={disabled || isLoading}
274
+ title={tooltip || undefined}
275
+ >
276
+ <span class="dropdown-value">{currentLabel}</span>
277
+ <span class="dropdown-chevron">
278
+ <Icon name="bs-chevron-down" />
279
+ </span>
280
+ </button>
281
+
282
+ {#if open}
283
+ <div bind:this={menuEl} class="dropdown-menu" role="listbox">
284
+ <div class="dropdown-options">
285
+ {#if options.length}
286
+ {#each options as option (option.value)}
287
+ {@const selected = value === option.value}
288
+ <button
289
+ type="button"
290
+ class="dropdown-option"
291
+ class:selected
292
+ role="option"
293
+ aria-selected={selected}
294
+ onclick={() => select(option)}
295
+ title={option.label}
296
+ >
297
+ {#if optionLeading}
298
+ <span class="dropdown-option-leading">
299
+ {@render optionLeading(option)}
300
+ </span>
301
+ {/if}
302
+ <span class="truncate">{option.label}</span>
303
+ </button>
304
+ {/each}
305
+ {:else}
306
+ <div class="dropdown-empty">{emptyMessage}</div>
307
+ {/if}
308
+ </div>
309
+ </div>
310
+ {/if}
311
+ </div>
312
+ {#if error}
313
+ <p class="error-message" style={errorColor ? `color: ${errorColor};` : ''}>{error}</p>
314
+ {/if}
315
+ </div>
316
+ {/if}
@@ -0,0 +1,41 @@
1
+ import type { Snippet } from 'svelte';
2
+ import './Dropdown.css';
3
+ export type DropdownOption = {
4
+ value: string;
5
+ label: string;
6
+ };
7
+ interface Props {
8
+ labelColor?: string;
9
+ aligment?: 'side' | 'top';
10
+ position?: 'left' | 'right';
11
+ size?: 48 | 40 | 32;
12
+ backgroundColor?: string;
13
+ borderColor?: string;
14
+ accentColor?: string;
15
+ textColor?: string;
16
+ errorColor?: string;
17
+ borderRadius?: number;
18
+ boxShadow?: string;
19
+ minWidth?: string;
20
+ id?: string;
21
+ label?: string;
22
+ subLabel?: string;
23
+ options?: DropdownOption[];
24
+ value?: string;
25
+ placeholder?: string;
26
+ emptyMessage?: string;
27
+ ariaLabel?: string;
28
+ onSelect?: (value: string) => void;
29
+ isMandatory?: boolean;
30
+ customValidation?: (value: string) => string | null;
31
+ isLoading?: boolean;
32
+ isShow?: boolean;
33
+ disabled?: boolean;
34
+ tooltip?: string;
35
+ optionLeading?: Snippet<[DropdownOption]>;
36
+ class?: string;
37
+ style?: string;
38
+ }
39
+ declare const Dropdown: import("svelte").Component<Props, {}, "value">;
40
+ type Dropdown = ReturnType<typeof Dropdown>;
41
+ export default Dropdown;
@@ -131,6 +131,10 @@
131
131
  padding-right: 2.5rem;
132
132
  }
133
133
 
134
+ .input-wrapper:has(.input-clear):not(:has(.suffix-wrapper)) .input-field {
135
+ padding-right: 2.5rem;
136
+ }
137
+
134
138
  .input-wrapper:has(.input-icon-left):not(:has(.prefix-wrapper)) .input-field {
135
139
  padding-left: 2.5rem;
136
140
  }
@@ -165,6 +169,35 @@
165
169
  right: 12px;
166
170
  }
167
171
 
172
+ .input-clear {
173
+ position: absolute;
174
+ top: 50%;
175
+ right: 12px;
176
+ transform: translateY(-50%);
177
+ display: flex;
178
+ align-items: center;
179
+ justify-content: center;
180
+ padding: 0;
181
+ margin: 0;
182
+ border: none;
183
+ background: transparent;
184
+ cursor: pointer;
185
+ color: var(--color-text-tertiary);
186
+ border-radius: 4px;
187
+ transition: color 0.15s, background 0.15s;
188
+ }
189
+
190
+ .input-clear:hover {
191
+ color: var(--color-text-primary);
192
+ background: color-mix(in srgb, var(--color-border-form) 50%, transparent);
193
+ }
194
+
195
+ .input-clear:focus-visible {
196
+ outline: none;
197
+ color: var(--color-bg-act-primary);
198
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-bg-act-primary) 30%, transparent);
199
+ }
200
+
168
201
  .input-loader {
169
202
  position: absolute;
170
203
  top: 50%;
@@ -41,6 +41,7 @@
41
41
  onfocus?: (event: FocusEvent) => void;
42
42
  onblur?: (event: FocusEvent) => void;
43
43
  onkeydown?: (event: KeyboardEvent) => void;
44
+ onClear?: () => void;
44
45
 
45
46
  // Validation
46
47
  isMandatory?: boolean;
@@ -54,6 +55,7 @@
54
55
  isShow?: boolean;
55
56
  disabled?: boolean;
56
57
  readOnly?: boolean;
58
+ clearable?: boolean;
57
59
  tooltip?: string;
58
60
 
59
61
  // Any
@@ -98,6 +100,7 @@
98
100
  onfocus,
99
101
  onblur,
100
102
  onkeydown,
103
+ onClear,
101
104
 
102
105
  // Validation
103
106
  isMandatory = false,
@@ -111,6 +114,7 @@
111
114
  isShow = true,
112
115
  disabled = false,
113
116
  readOnly = false,
117
+ clearable = true,
114
118
  tooltip = '',
115
119
 
116
120
  class: className = '',
@@ -118,7 +122,7 @@
118
122
  ...props
119
123
  }: Props = $props();
120
124
 
121
- let inputValue = $state(value);
125
+ let inputValue = $derived(value);
122
126
  let errorMessage = $state('');
123
127
  let isFocused = $state(false);
124
128
 
@@ -335,6 +339,19 @@
335
339
  onclick(e);
336
340
  }
337
341
  }
342
+
343
+ function handleClear(e: MouseEvent) {
344
+ e.preventDefault();
345
+ e.stopPropagation();
346
+ if (disabled || readOnly || isLoading) return;
347
+ inputValue = '';
348
+ errorMessage = validateInput('');
349
+ onClear?.();
350
+ }
351
+
352
+ const showClear = $derived(
353
+ clearable && inputValue !== '' && !disabled && !readOnly && !isLoading && !suffix
354
+ );
338
355
  </script>
339
356
 
340
357
  {#if isShow}
@@ -388,7 +405,17 @@
388
405
  title={tooltip || ''}
389
406
  {...props}
390
407
  />
391
- {#if isLoading && iconPosition === 'right'}
408
+ {#if showClear}
409
+ <button
410
+ type="button"
411
+ class="input-clear input-icon-right"
412
+ onclick={handleClear}
413
+ aria-label="Hapus isian"
414
+ tabindex="-1"
415
+ >
416
+ <Icon name="bs-x-lg" color="currentColor" width={16} height={16} />
417
+ </button>
418
+ {:else if isLoading && iconPosition === 'right'}
392
419
  <div class="input-loader input-icon-right">
393
420
  <span class="loader-spinner"></span>
394
421
  </div>
@@ -29,6 +29,7 @@ interface Props extends HTMLInputAttributes {
29
29
  onfocus?: (event: FocusEvent) => void;
30
30
  onblur?: (event: FocusEvent) => void;
31
31
  onkeydown?: (event: KeyboardEvent) => void;
32
+ onClear?: () => void;
32
33
  isMandatory?: boolean;
33
34
  regex?: RegExp;
34
35
  minLength?: number;
@@ -38,6 +39,7 @@ interface Props extends HTMLInputAttributes {
38
39
  isShow?: boolean;
39
40
  disabled?: boolean;
40
41
  readOnly?: boolean;
42
+ clearable?: boolean;
41
43
  tooltip?: string;
42
44
  class?: string;
43
45
  style?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mertani-web-toolkit",
3
- "version": "0.1.55",
3
+ "version": "0.1.57",
4
4
  "homepage": "https://storybook.mertani.com/",
5
5
  "scripts": {
6
6
  "dev": "vite dev",