@juspay/svelte-ui-components 2.133.1 → 2.134.1

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.
@@ -0,0 +1,593 @@
1
+ <script lang="ts">
2
+ import { tick } from 'svelte';
3
+ import type { Snippet } from 'svelte';
4
+ import { fade } from 'svelte/transition';
5
+ import type { GalleryImage, GalleryProperties } from './properties';
6
+ import Img from '../Img/Img.svelte';
7
+ import Button from '../Button/Button.svelte';
8
+ import Icon from '../Icon/Icon.svelte';
9
+ import closeSvg from '../assets/close.svg?raw';
10
+ import chevronLeftSvg from '../assets/chevron-left-lg.svg?raw';
11
+ import chevronRightSvg from '../assets/chevron-right-lg.svg?raw';
12
+ import editSvg from '../assets/edit.svg?raw';
13
+ import deleteSvg from '../assets/delete.svg?raw';
14
+
15
+ let {
16
+ images,
17
+ view = 'grid',
18
+ open = $bindable(false),
19
+ activeIndex = $bindable(0),
20
+ enableLightbox = true,
21
+ loop = false,
22
+ showCounter = true,
23
+ showCaption = true,
24
+ previousIcon,
25
+ nextIcon,
26
+ closeIcon,
27
+ editIcon,
28
+ deleteIcon,
29
+ itemFooter,
30
+ testId,
31
+ onImageClick,
32
+ onEditClick,
33
+ onDeleteClick,
34
+ onOpen,
35
+ onDismiss,
36
+ onIndexChange,
37
+ onkeydown,
38
+ classes
39
+ }: GalleryProperties = $props();
40
+
41
+ let lightboxDiv: HTMLDivElement | null = $state(null);
42
+ let closeButtonWrap: HTMLDivElement | null = $state(null);
43
+ let previousButtonWrap: HTMLDivElement | null = $state(null);
44
+ let nextButtonWrap: HTMLDivElement | null = $state(null);
45
+ let openerElement: HTMLElement | null = null;
46
+
47
+ let activeImage = $derived(images.at(activeIndex));
48
+ let hasPrevious = $derived(loop ? images.length > 1 : activeIndex > 0);
49
+ let hasNext = $derived(loop ? images.length > 1 : activeIndex < images.length - 1);
50
+ let itemsAreInteractive = $derived(enableLightbox || typeof onImageClick === 'function');
51
+ let hasItemFooter = $derived(view === 'grid' && typeof itemFooter === 'function');
52
+ let showEditButton = $derived(typeof onEditClick === 'function');
53
+ let showDeleteButton = $derived(typeof onDeleteClick === 'function');
54
+ let showItemActions = $derived(showEditButton || showDeleteButton);
55
+
56
+ function lightboxAction(node: HTMLElement) {
57
+ if (openerElement === null && document.activeElement instanceof HTMLElement) {
58
+ openerElement = document.activeElement;
59
+ }
60
+ document.body.style.overflow = 'hidden';
61
+ tick().then(() => {
62
+ node.focus();
63
+ });
64
+ return {
65
+ destroy() {
66
+ document.body.style.overflow = '';
67
+ if (openerElement !== null) {
68
+ openerElement.focus();
69
+ openerElement = null;
70
+ }
71
+ }
72
+ };
73
+ }
74
+
75
+ function openLightbox(index: number, opener: EventTarget | null): void {
76
+ if (opener instanceof HTMLElement) {
77
+ openerElement = opener;
78
+ }
79
+ activeIndex = index;
80
+ open = true;
81
+ onOpen?.(index);
82
+ }
83
+
84
+ function closeLightbox(): void {
85
+ open = false;
86
+ onDismiss?.();
87
+ }
88
+
89
+ async function goToIndex(index: number): Promise<void> {
90
+ if (index < 0 || index >= images.length) {
91
+ return;
92
+ }
93
+ activeIndex = index;
94
+ onIndexChange?.(activeIndex);
95
+ await tick();
96
+ if (lightboxDiv !== null && !lightboxDiv.contains(document.activeElement)) {
97
+ lightboxDiv.focus();
98
+ }
99
+ }
100
+
101
+ function showPrevious(): void {
102
+ if (activeIndex > 0) {
103
+ goToIndex(activeIndex - 1);
104
+ } else if (loop && images.length > 1) {
105
+ goToIndex(images.length - 1);
106
+ }
107
+ }
108
+
109
+ function showNext(): void {
110
+ if (activeIndex < images.length - 1) {
111
+ goToIndex(activeIndex + 1);
112
+ } else if (loop && images.length > 1) {
113
+ goToIndex(0);
114
+ }
115
+ }
116
+
117
+ function handleImageClick(index: number, event: MouseEvent): void {
118
+ onImageClick?.(index, event);
119
+ if (enableLightbox) {
120
+ openLightbox(index, event.currentTarget);
121
+ }
122
+ }
123
+
124
+ function handleEditClick(index: number, event: MouseEvent): void {
125
+ onEditClick?.(index, event);
126
+ }
127
+
128
+ function handleDeleteClick(index: number, event: MouseEvent): void {
129
+ onDeleteClick?.(index, event);
130
+ }
131
+
132
+ function trapFocus(event: KeyboardEvent): void {
133
+ const first = closeButtonWrap?.querySelector('button') ?? null;
134
+ const last =
135
+ (nextButtonWrap ?? previousButtonWrap ?? closeButtonWrap)?.querySelector('button') ?? null;
136
+ if (first === null || last === null) {
137
+ return;
138
+ }
139
+ if (
140
+ event.shiftKey &&
141
+ (document.activeElement === first || document.activeElement === lightboxDiv)
142
+ ) {
143
+ event.preventDefault();
144
+ last.focus();
145
+ } else if (!event.shiftKey && document.activeElement === last) {
146
+ event.preventDefault();
147
+ first.focus();
148
+ }
149
+ }
150
+
151
+ function handleLightboxKeydown(event: KeyboardEvent): void {
152
+ onkeydown?.(event);
153
+ if (event.key === 'Escape') {
154
+ closeLightbox();
155
+ } else if (event.key === 'ArrowLeft') {
156
+ event.preventDefault();
157
+ showPrevious();
158
+ } else if (event.key === 'ArrowRight') {
159
+ event.preventDefault();
160
+ showNext();
161
+ } else if (event.key === 'Home') {
162
+ event.preventDefault();
163
+ goToIndex(0);
164
+ } else if (event.key === 'End') {
165
+ event.preventDefault();
166
+ goToIndex(images.length - 1);
167
+ } else if (event.key === 'Tab') {
168
+ trapFocus(event);
169
+ }
170
+ }
171
+
172
+ function handleBackdropClick(event: MouseEvent): void {
173
+ if (event.target === lightboxDiv) {
174
+ closeLightbox();
175
+ }
176
+ }
177
+ </script>
178
+
179
+ {#snippet controlIcon(fallbackMarkup: string, custom?: Snippet)}
180
+ {#if typeof custom === 'function'}
181
+ {@render custom()}
182
+ {:else}
183
+ <Icon svg={fallbackMarkup} />
184
+ {/if}
185
+ {/snippet}
186
+
187
+ {#snippet itemContent(image: GalleryImage, index: number)}
188
+ {#if hasItemFooter}
189
+ <span class="grid-image-wrap">
190
+ <Img src={image.thumbnail ?? image.src} alt={image.alt} fallback={image.fallback} />
191
+ </span>
192
+ {#if typeof itemFooter === 'function'}
193
+ {@render itemFooter(image, index)}
194
+ {/if}
195
+ {:else}
196
+ <Img src={image.thumbnail ?? image.src} alt={image.alt} fallback={image.fallback} />
197
+ {#if view === 'list'}
198
+ <span class="list-text">
199
+ <span class="list-title">{image.alt}</span>
200
+ {#if typeof image.caption === 'string' && image.caption.length > 0}
201
+ <span class="list-caption">{image.caption}</span>
202
+ {/if}
203
+ </span>
204
+ {/if}
205
+ {/if}
206
+ {/snippet}
207
+
208
+ <div
209
+ class="gallery {view} {hasItemFooter ? 'has-item-footer' : ''} {classes ?? ''}"
210
+ data-pw={typeof testId === 'string' ? testId : null}
211
+ role="list"
212
+ >
213
+ {#each images as image, index (index)}
214
+ <div class="gallery-item" role="listitem">
215
+ {#if itemsAreInteractive}
216
+ <button
217
+ class="gallery-item-content"
218
+ onclick={(event) => handleImageClick(index, event)}
219
+ aria-label={enableLightbox
220
+ ? `View image ${index + 1} of ${images.length}: ${image.alt}`
221
+ : image.alt}
222
+ >
223
+ {@render itemContent(image, index)}
224
+ </button>
225
+ {:else}
226
+ <div class="gallery-item-content">
227
+ {@render itemContent(image, index)}
228
+ </div>
229
+ {/if}
230
+ {#if showItemActions}
231
+ <div class="gallery-item-actions">
232
+ {#if showEditButton}
233
+ <div class="gallery-item-action">
234
+ <Button
235
+ ariaLabel={`Edit image ${index + 1}: ${image.alt}`}
236
+ onclick={(event) => handleEditClick(index, event)}
237
+ >
238
+ {@render controlIcon(editSvg, editIcon)}
239
+ </Button>
240
+ </div>
241
+ {/if}
242
+ {#if showDeleteButton}
243
+ <div class="gallery-item-action">
244
+ <Button
245
+ ariaLabel={`Delete image ${index + 1}: ${image.alt}`}
246
+ onclick={(event) => handleDeleteClick(index, event)}
247
+ >
248
+ {@render controlIcon(deleteSvg, deleteIcon)}
249
+ </Button>
250
+ </div>
251
+ {/if}
252
+ </div>
253
+ {/if}
254
+ </div>
255
+ {/each}
256
+ </div>
257
+
258
+ {#if open}
259
+ <div
260
+ class="lightbox {classes ?? ''}"
261
+ role="dialog"
262
+ aria-modal="true"
263
+ aria-label={typeof activeImage === 'object' ? activeImage.alt : 'Image gallery'}
264
+ tabindex="-1"
265
+ bind:this={lightboxDiv}
266
+ use:lightboxAction
267
+ onclick={handleBackdropClick}
268
+ onkeydown={handleLightboxKeydown}
269
+ transition:fade={{ duration: 200 }}
270
+ >
271
+ <div class="lightbox-close" bind:this={closeButtonWrap}>
272
+ <Button ariaLabel="Close gallery" onclick={closeLightbox}>
273
+ {@render controlIcon(closeSvg, closeIcon)}
274
+ </Button>
275
+ </div>
276
+ {#if hasPrevious}
277
+ <div class="lightbox-previous" bind:this={previousButtonWrap}>
278
+ <Button ariaLabel="Previous image" onclick={showPrevious}>
279
+ {@render controlIcon(chevronLeftSvg, previousIcon)}
280
+ </Button>
281
+ </div>
282
+ {/if}
283
+ {#if typeof activeImage === 'object'}
284
+ <figure class="lightbox-figure">
285
+ <Img src={activeImage.src} alt={activeImage.alt} fallback={activeImage.fallback} />
286
+ {#if showCaption && typeof activeImage.caption === 'string' && activeImage.caption.length > 0}
287
+ <figcaption class="lightbox-caption">{activeImage.caption}</figcaption>
288
+ {/if}
289
+ </figure>
290
+ {/if}
291
+ {#if hasNext}
292
+ <div class="lightbox-next" bind:this={nextButtonWrap}>
293
+ <Button ariaLabel="Next image" onclick={showNext}>
294
+ {@render controlIcon(chevronRightSvg, nextIcon)}
295
+ </Button>
296
+ </div>
297
+ {/if}
298
+ {#if showCounter && typeof activeImage === 'object'}
299
+ <div class="lightbox-counter" aria-live="polite">
300
+ {activeIndex + 1} / {images.length}
301
+ </div>
302
+ {/if}
303
+ </div>
304
+ {/if}
305
+
306
+ <style>
307
+ .gallery {
308
+ width: var(--gallery-width, 100%);
309
+ padding: var(--gallery-padding, 0px);
310
+ margin: var(--gallery-margin, 0px);
311
+ background: var(--gallery-background, transparent);
312
+ }
313
+
314
+ .gallery.grid {
315
+ display: grid;
316
+ grid-template-columns: var(
317
+ --gallery-grid-template-columns,
318
+ repeat(var(--gallery-columns, 3), 1fr)
319
+ );
320
+ gap: var(--gallery-gap, 8px);
321
+ }
322
+
323
+ .gallery.list {
324
+ display: flex;
325
+ flex-direction: column;
326
+ gap: var(--gallery-gap, 8px);
327
+ }
328
+
329
+ .gallery-item {
330
+ position: relative;
331
+ }
332
+
333
+ .grid .gallery-item {
334
+ aspect-ratio: var(--gallery-item-aspect-ratio, 1);
335
+ border-radius: var(--gallery-item-border-radius, 0px);
336
+ overflow: hidden;
337
+ --image-width: 100%;
338
+ --image-height: 100%;
339
+ --image-object-fit: var(--gallery-item-image-fit, cover);
340
+ --image-border-radius: var(--gallery-item-border-radius, 0px);
341
+ --image-transition: var(--gallery-item-image-transition);
342
+ }
343
+
344
+ .grid.has-item-footer .gallery-item {
345
+ aspect-ratio: auto;
346
+ overflow: visible;
347
+ border-radius: 0px;
348
+ }
349
+
350
+ .list .gallery-item {
351
+ display: flex;
352
+ align-items: center;
353
+ border-radius: var(--gallery-list-item-border-radius, 0px);
354
+ background: var(--gallery-list-item-background, transparent);
355
+ --image-width: var(--gallery-list-thumbnail-width, 56px);
356
+ --image-height: var(--gallery-list-thumbnail-height, 56px);
357
+ --image-object-fit: var(--gallery-list-thumbnail-fit, cover);
358
+ --image-border-radius: var(--gallery-list-thumbnail-border-radius, 0px);
359
+ --image-transition: var(--gallery-item-image-transition);
360
+ --image-flex-shrink: 0;
361
+ }
362
+
363
+ .gallery-item-content {
364
+ font: inherit;
365
+ color: inherit;
366
+ border: var(--gallery-item-border, none);
367
+ background: transparent;
368
+ }
369
+
370
+ button.gallery-item-content {
371
+ cursor: var(--gallery-item-cursor, pointer);
372
+ transition: var(--gallery-item-transition);
373
+ }
374
+
375
+ .grid .gallery-item-content {
376
+ display: block;
377
+ width: 100%;
378
+ height: 100%;
379
+ padding: 0;
380
+ border-radius: var(--gallery-item-border-radius, 0px);
381
+ }
382
+
383
+ .grid.has-item-footer .gallery-item-content {
384
+ display: flex;
385
+ flex-direction: column;
386
+ height: auto;
387
+ border-radius: var(--gallery-item-border-radius, 0px);
388
+ overflow: hidden;
389
+ }
390
+
391
+ .grid-image-wrap {
392
+ display: block;
393
+ aspect-ratio: var(--gallery-item-aspect-ratio, 1);
394
+ overflow: hidden;
395
+ --image-width: 100%;
396
+ --image-height: 100%;
397
+ --image-object-fit: var(--gallery-item-image-fit, cover);
398
+ --image-border-radius: 0px;
399
+ --image-transition: var(--gallery-item-image-transition);
400
+ }
401
+
402
+ .list .gallery-item-content {
403
+ display: flex;
404
+ align-items: center;
405
+ text-align: left;
406
+ flex: 1;
407
+ min-width: 0;
408
+ gap: var(--gallery-list-item-gap, 12px);
409
+ padding: var(--gallery-list-item-padding, 8px);
410
+ }
411
+
412
+ .grid button.gallery-item-content:hover {
413
+ opacity: var(--gallery-item-hover-opacity, 1);
414
+ transform: var(--gallery-item-hover-transform);
415
+ }
416
+
417
+ .grid button.gallery-item-content:focus-visible {
418
+ outline: var(--gallery-item-focus-outline, 2px solid currentColor);
419
+ outline-offset: var(--gallery-item-focus-outline-offset, 2px);
420
+ }
421
+
422
+ .list button.gallery-item-content:focus-visible {
423
+ outline: none;
424
+ }
425
+
426
+ .list .gallery-item:has(button.gallery-item-content):hover {
427
+ background: var(--gallery-list-item-hover-background, transparent);
428
+ }
429
+
430
+ .list .gallery-item:has(button.gallery-item-content:focus-visible) {
431
+ outline: var(--gallery-item-focus-outline, 2px solid currentColor);
432
+ outline-offset: var(--gallery-item-focus-outline-offset, 2px);
433
+ }
434
+
435
+ .list-text {
436
+ display: flex;
437
+ flex-direction: column;
438
+ gap: var(--gallery-list-text-gap, 2px);
439
+ min-width: 0;
440
+ }
441
+
442
+ .list-title,
443
+ .list-caption {
444
+ overflow: hidden;
445
+ text-overflow: ellipsis;
446
+ white-space: nowrap;
447
+ }
448
+
449
+ .list-title {
450
+ color: var(--gallery-list-title-color, inherit);
451
+ font-size: var(--gallery-list-title-font-size, 14px);
452
+ font-weight: var(--gallery-list-title-font-weight, 500);
453
+ font-family: var(--gallery-list-title-font-family);
454
+ }
455
+
456
+ .list-caption {
457
+ color: var(--gallery-list-caption-color, inherit);
458
+ font-size: var(--gallery-list-caption-font-size, 12px);
459
+ font-family: var(--gallery-list-caption-font-family);
460
+ }
461
+
462
+ .gallery-item-actions {
463
+ display: flex;
464
+ align-items: center;
465
+ gap: var(--gallery-item-actions-gap, 4px);
466
+ }
467
+
468
+ .grid .gallery-item-actions {
469
+ position: absolute;
470
+ top: var(--gallery-item-actions-top, 8px);
471
+ right: var(--gallery-item-actions-right, 8px);
472
+ }
473
+
474
+ .list .gallery-item-actions {
475
+ flex-shrink: 0;
476
+ padding-right: var(--gallery-item-actions-right, 8px);
477
+ }
478
+
479
+ .gallery-item-action {
480
+ border-radius: var(--gallery-item-action-border-radius, 8px);
481
+ --button-color: transparent;
482
+ --button-hover-color: transparent;
483
+ --button-padding: var(--gallery-item-action-padding, 6px);
484
+ --button-border-radius: var(--gallery-item-action-border-radius, 8px);
485
+ --icon-container-padding: 0px;
486
+ --icon-padding: 0px;
487
+ --icon-width: var(--gallery-item-action-icon-size, 16px);
488
+ --icon-height: var(--gallery-item-action-icon-size, 16px);
489
+ }
490
+
491
+ .grid .gallery-item-action {
492
+ background: var(--gallery-item-action-background, #00000066);
493
+ -webkit-backdrop-filter: var(--gallery-item-action-backdrop-filter, blur(8px));
494
+ backdrop-filter: var(--gallery-item-action-backdrop-filter, blur(8px));
495
+ --button-text-color: var(--gallery-item-action-color, #ffffff);
496
+ }
497
+
498
+ .grid .gallery-item-action:hover {
499
+ background: var(--gallery-item-action-hover-background, #00000099);
500
+ }
501
+
502
+ .list .gallery-item-action {
503
+ background: var(--gallery-item-action-background, transparent);
504
+ --button-text-color: var(--gallery-item-action-color, currentColor);
505
+ }
506
+
507
+ .list .gallery-item-action:hover {
508
+ background: var(--gallery-item-action-hover-background, #80808026);
509
+ }
510
+
511
+ .lightbox {
512
+ position: fixed;
513
+ top: 0;
514
+ bottom: 0;
515
+ left: 0;
516
+ right: 0;
517
+ z-index: var(--gallery-lightbox-z-index, 15);
518
+ display: flex;
519
+ justify-content: center;
520
+ align-items: center;
521
+ background: var(--gallery-lightbox-background, #000000e6);
522
+ -webkit-tap-highlight-color: transparent;
523
+ }
524
+
525
+ .lightbox:focus {
526
+ outline: none;
527
+ }
528
+
529
+ .lightbox-figure {
530
+ display: flex;
531
+ flex-direction: column;
532
+ align-items: center;
533
+ gap: var(--gallery-lightbox-caption-gap, 12px);
534
+ margin: 0;
535
+ pointer-events: none;
536
+ --image-width: var(--gallery-lightbox-image-width, 85vw);
537
+ --image-height: var(--gallery-lightbox-image-height, 75vh);
538
+ --image-object-fit: var(--gallery-lightbox-image-fit, contain);
539
+ --image-border-radius: var(--gallery-lightbox-image-border-radius, 0px);
540
+ }
541
+
542
+ .lightbox-caption {
543
+ color: var(--gallery-lightbox-caption-color, #ffffff);
544
+ font-size: var(--gallery-lightbox-caption-font-size, 14px);
545
+ font-family: var(--gallery-lightbox-caption-font-family);
546
+ text-align: center;
547
+ max-width: var(--gallery-lightbox-image-width, 85vw);
548
+ }
549
+
550
+ .lightbox-counter {
551
+ position: absolute;
552
+ bottom: var(--gallery-lightbox-counter-bottom, 16px);
553
+ left: 50%;
554
+ transform: translateX(-50%);
555
+ color: var(--gallery-lightbox-counter-color, #ffffff);
556
+ font-size: var(--gallery-lightbox-counter-font-size, 13px);
557
+ font-family: var(--gallery-lightbox-counter-font-family);
558
+ }
559
+
560
+ .lightbox-close,
561
+ .lightbox-previous,
562
+ .lightbox-next {
563
+ position: absolute;
564
+ --button-color: var(--gallery-lightbox-control-background, transparent);
565
+ --button-text-color: var(--gallery-lightbox-control-color, #ffffff);
566
+ --button-hover-color: var(--gallery-lightbox-control-hover-background, #ffffff1f);
567
+ --button-padding: var(--gallery-lightbox-control-padding, 8px);
568
+ --button-border-radius: var(--gallery-lightbox-control-border-radius, 50%);
569
+ --icon-container-padding: 0px;
570
+ --icon-padding: 0px;
571
+ --icon-width: var(--gallery-lightbox-control-icon-size, 24px);
572
+ --icon-height: var(--gallery-lightbox-control-icon-size, 24px);
573
+ }
574
+
575
+ .lightbox-close {
576
+ top: var(--gallery-lightbox-close-top, 16px);
577
+ right: var(--gallery-lightbox-close-right, 16px);
578
+ }
579
+
580
+ .lightbox-previous,
581
+ .lightbox-next {
582
+ top: 50%;
583
+ transform: translateY(-50%);
584
+ }
585
+
586
+ .lightbox-previous {
587
+ left: var(--gallery-lightbox-nav-inset, 16px);
588
+ }
589
+
590
+ .lightbox-next {
591
+ right: var(--gallery-lightbox-nav-inset, 16px);
592
+ }
593
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { GalleryProperties } from './properties';
2
+ declare const Gallery: import("svelte").Component<GalleryProperties, {}, "open" | "activeIndex">;
3
+ type Gallery = ReturnType<typeof Gallery>;
4
+ export default Gallery;
@@ -0,0 +1,39 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type GalleryView = 'grid' | 'list';
3
+ export type GalleryImage = {
4
+ src: string;
5
+ alt: string;
6
+ thumbnail?: string;
7
+ fallback?: string;
8
+ caption?: string;
9
+ };
10
+ export type GalleryProperties = MandatoryGalleryProperties & OptionalGalleryProperties & GalleryEventProperties;
11
+ export type MandatoryGalleryProperties = {
12
+ images: GalleryImage[];
13
+ };
14
+ export type OptionalGalleryProperties = {
15
+ view?: GalleryView;
16
+ open?: boolean;
17
+ activeIndex?: number;
18
+ enableLightbox?: boolean;
19
+ loop?: boolean;
20
+ showCounter?: boolean;
21
+ showCaption?: boolean;
22
+ previousIcon?: Snippet;
23
+ nextIcon?: Snippet;
24
+ closeIcon?: Snippet;
25
+ editIcon?: Snippet;
26
+ deleteIcon?: Snippet;
27
+ itemFooter?: Snippet<[GalleryImage, number]>;
28
+ testId?: string;
29
+ classes?: string;
30
+ };
31
+ export type GalleryEventProperties = {
32
+ onImageClick?: (index: number, event: MouseEvent) => void;
33
+ onEditClick?: (index: number, event: MouseEvent) => void;
34
+ onDeleteClick?: (index: number, event: MouseEvent) => void;
35
+ onOpen?: (index: number) => void;
36
+ onDismiss?: () => void;
37
+ onIndexChange?: (index: number) => void;
38
+ onkeydown?: (event: KeyboardEvent) => void;
39
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -122,6 +122,26 @@
122
122
  // forceError lets consumers drive the error border from server/runtime validation,
123
123
  // independent of validationPattern.
124
124
  const showError = $derived(showErrorMessage || forceError);
125
+ // The error text has to be reachable FROM the field it describes and announced when it
126
+ // appears. Without both, a screen-reader user submits, hears nothing, and is left on a form
127
+ // that did not move -- the message is on screen and absent from the accessibility tree.
128
+ const errorMessageId = $derived(`${effectiveId}-error`);
129
+ // `onErrorMessage` is `string | null`, so null has to be excluded explicitly: `null !== ''`
130
+ // is true, which would describe the field by an alert element carrying no message.
131
+ const isShowingError = $derived(
132
+ onErrorMessage != null && onErrorMessage !== '' && showError && !actionInput
133
+ );
134
+ const infoMessageId = $derived(`${effectiveId}-info`);
135
+ const isShowingInfo = $derived(infoMessage !== '' && !actionInput);
136
+ // Helper text is part of the field's description, not decoration: a consumer's `infoMessage`
137
+ // ("Enter a percentage between 1 and 100") is exactly the guidance a screen-reader user needs
138
+ // BEFORE they trip an error. Reference both, in reading order, so the field is described by
139
+ // everything visibly attached to it rather than only by its failure.
140
+ const describedBy = $derived(
141
+ [isShowingError ? errorMessageId : null, isShowingInfo ? infoMessageId : null]
142
+ .filter(Boolean)
143
+ .join(' ')
144
+ );
125
145
  const hasLeftIcon = $derived(typeof leftIcon === 'function');
126
146
  const hasRightIcon = $derived(typeof rightIcon === 'function');
127
147
 
@@ -301,6 +321,8 @@
301
321
  aria-controls={ariaControls}
302
322
  aria-activedescendant={ariaActivedescendant}
303
323
  aria-required={mandatory || null}
324
+ aria-invalid={showError ? 'true' : null}
325
+ aria-describedby={describedBy || null}
304
326
  required={mandatory || null}
305
327
  onfocus={onFocus}
306
328
  onfocusout={_onFocusOut}
@@ -337,6 +359,8 @@
337
359
  aria-controls={ariaControls}
338
360
  aria-activedescendant={ariaActivedescendant}
339
361
  aria-required={mandatory || null}
362
+ aria-invalid={showError ? 'true' : null}
363
+ aria-describedby={describedBy || null}
340
364
  required={mandatory || null}
341
365
  onfocus={onFocus}
342
366
  onfocusout={_onFocusOut}
@@ -399,8 +423,10 @@
399
423
  {@render fieldElement()}
400
424
  {/if}
401
425
 
402
- {#if onErrorMessage !== '' && showError && !actionInput}
426
+ {#if isShowingError}
403
427
  <div
428
+ id={errorMessageId}
429
+ role="alert"
404
430
  class="error-message"
405
431
  data-pw={typeof testId === 'string' && testId.length > 0 ? `${testId}-error-message` : null}
406
432
  testID={typeof testId === 'string' && testId.length > 0 ? `${testId}-error-message` : null}
@@ -408,8 +434,8 @@
408
434
  {onErrorMessage}
409
435
  </div>
410
436
  {/if}
411
- {#if infoMessage !== '' && !actionInput}
412
- <div class="info-message">
437
+ {#if isShowingInfo}
438
+ <div id={infoMessageId} class="info-message">
413
439
  {infoMessage}
414
440
  </div>
415
441
  {/if}