@astryxdesign/lab 0.4.7-canary.fe95cd5 → 0.5.0-canary.055e75f
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.
- package/dist/TransferList/TransferList.d.ts +2 -2
- package/dist/TransferList/TransferList.d.ts.map +1 -1
- package/dist/TransferList/TransferList.js +104 -44
- package/dist/TransferList/TransferListSelector.d.ts +2 -2
- package/dist/TransferList/TransferListSelector.d.ts.map +1 -1
- package/dist/TransferList/TransferListSelector.js +24 -7
- package/dist/lab.css +1 -0
- package/package.json +6 -6
- package/src/TransferList/TransferList.tsx +103 -46
- package/src/TransferList/TransferListSelector.test.tsx +45 -2
- package/src/TransferList/TransferListSelector.tsx +34 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file TransferList.tsx
|
|
3
|
-
* @input Controlled option data, React DOM portals, local action glyphs,
|
|
3
|
+
* @input Controlled option data, React DOM portals, local action glyphs, shared Lab reorder styles, and the core i18n catalog
|
|
4
4
|
* @output Exports TransferList with immediate transfers, vertical reordering, and responsive scroll ownership
|
|
5
5
|
* @position Lab collection input; consumed by index.ts, TransferListSelector, docs, and tests
|
|
6
6
|
*
|
|
@@ -83,7 +83,7 @@ export interface TransferListProps<T extends string = string> extends Omit<BaseP
|
|
|
83
83
|
* />
|
|
84
84
|
* ```
|
|
85
85
|
*/
|
|
86
|
-
export declare function TransferList<T extends string = string>({ label, isLabelHidden, description, options, value, onChange, selectedLabel, availableLabel, hasSearch, searchLabel, searchPlaceholder, isReorderable, hasSelectAll, hasClear, renderOption, selectedEmptyText, availableEmptyText, noResultsText, xstyle, className, style, ref, role, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, ...restProps }: TransferListProps<T>): ReactNode;
|
|
86
|
+
export declare function TransferList<T extends string = string>({ label, isLabelHidden, description, options, value, onChange, selectedLabel: selectedLabelFromProps, availableLabel: availableLabelFromProps, hasSearch, searchLabel, searchPlaceholder: searchPlaceholderFromProps, isReorderable, hasSelectAll, hasClear, renderOption, selectedEmptyText: selectedEmptyTextFromProps, availableEmptyText: availableEmptyTextFromProps, noResultsText: noResultsTextFromProps, xstyle, className, style, ref, role, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, ...restProps }: TransferListProps<T>): ReactNode;
|
|
87
87
|
export declare namespace TransferList {
|
|
88
88
|
var displayName: string;
|
|
89
89
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TransferList.d.ts","sourceRoot":"","sources":["../../src/TransferList/TransferList.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAWL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"TransferList.d.ts","sourceRoot":"","sources":["../../src/TransferList/TransferList.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAWL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAwFlD,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM;IAC3D,0DAA0D;IAC1D,KAAK,EAAE,CAAC,CAAC;IACT,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,4EAA4E;IAC5E,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,gEAAgE;IAChE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,IAAI,CACxE,SAAS,CAAC,cAAc,CAAC,EACzB,UAAU,CACX;IACC,uCAAuC;IACvC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,OAAO,EAAE,SAAS,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1C,yEAAyE;IACzE,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;IACpB,uEAAuE;IACvE,QAAQ,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,KAAK,IAAI,CAAC;IACxC,0DAA0D;IAC1D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,+EAA+E;IAC/E,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,8CAA8C;IAC9C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC;IAC5D,yCAAyC;IACzC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,0CAA0C;IAC1C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iDAAiD;IACjD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA8MD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,EACtD,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,OAAO,EACP,KAAK,EACL,QAAQ,EACR,aAAa,EAAE,sBAAsB,EACrC,cAAc,EAAE,uBAAuB,EACvC,SAAiB,EACjB,WAAW,EACX,iBAAiB,EAAE,0BAA0B,EAC7C,aAAoB,EACpB,YAAoB,EACpB,QAAgB,EAChB,YAAY,EACZ,iBAAiB,EAAE,0BAA0B,EAC7C,kBAAkB,EAAE,2BAA2B,EAC/C,aAAa,EAAE,sBAAsB,EACrC,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,IAAI,EACJ,iBAAiB,EAAE,cAAc,EACjC,kBAAkB,EAAE,eAAe,EACnC,GAAG,SAAS,EACb,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,SAAS,CAo8BlC;yBA/9Be,YAAY"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file TransferList.tsx
|
|
7
|
-
* @input Controlled option data, React DOM portals, local action glyphs,
|
|
7
|
+
* @input Controlled option data, React DOM portals, local action glyphs, shared Lab reorder styles, and the core i18n catalog
|
|
8
8
|
* @output Exports TransferList with immediate transfers, vertical reordering, and responsive scroll ownership
|
|
9
9
|
* @position Lab collection input; consumed by index.ts, TransferListSelector, docs, and tests
|
|
10
10
|
*
|
|
@@ -26,6 +26,7 @@ import { Text } from '@astryxdesign/core/Text';
|
|
|
26
26
|
import { TextInput } from '@astryxdesign/core/TextInput';
|
|
27
27
|
import { VisuallyHidden } from '@astryxdesign/core/VisuallyHidden';
|
|
28
28
|
import { useAnnounce } from '@astryxdesign/core/hooks';
|
|
29
|
+
import { useTranslator } from '@astryxdesign/core/i18n';
|
|
29
30
|
import '@astryxdesign/core/theme/tokens.stylex';
|
|
30
31
|
import { borderVars, colorVars, spacingVars, typeScaleVars } from '@astryxdesign/core/theme/tokens.stylex';
|
|
31
32
|
import { focusOutlineStyles, mergeProps, mergeRefs, themeProps } from '@astryxdesign/core/utils';
|
|
@@ -186,15 +187,16 @@ function moveItem(value, fromIndex, toIndex) {
|
|
|
186
187
|
nextValue.splice(toIndex, 0, item);
|
|
187
188
|
return nextValue;
|
|
188
189
|
}
|
|
190
|
+
|
|
191
|
+
// Case folding is locale-independent here, matching Selector, MultiSelector,
|
|
192
|
+
// and CommandPalette. Locale-sensitive folding would mean raw Intl access,
|
|
193
|
+
// which the shipped packages route through the provider instead.
|
|
189
194
|
function matchesQuery(option, query) {
|
|
190
195
|
if (query === '') {
|
|
191
196
|
return true;
|
|
192
197
|
}
|
|
193
198
|
const description = typeof option.description === 'string' ? option.description : '';
|
|
194
|
-
return [option.label, description, option.group ?? ''].some(part => part.
|
|
195
|
-
}
|
|
196
|
-
function itemCount(count) {
|
|
197
|
-
return `${count} ${count === 1 ? 'item' : 'items'}`;
|
|
199
|
+
return [option.label, description, option.group ?? ''].some(part => part.toLowerCase().includes(query));
|
|
198
200
|
}
|
|
199
201
|
|
|
200
202
|
/**
|
|
@@ -220,18 +222,18 @@ export function TransferList({
|
|
|
220
222
|
options,
|
|
221
223
|
value,
|
|
222
224
|
onChange,
|
|
223
|
-
selectedLabel
|
|
224
|
-
availableLabel
|
|
225
|
+
selectedLabel: selectedLabelFromProps,
|
|
226
|
+
availableLabel: availableLabelFromProps,
|
|
225
227
|
hasSearch = false,
|
|
226
228
|
searchLabel,
|
|
227
|
-
searchPlaceholder
|
|
229
|
+
searchPlaceholder: searchPlaceholderFromProps,
|
|
228
230
|
isReorderable = true,
|
|
229
231
|
hasSelectAll = false,
|
|
230
232
|
hasClear = false,
|
|
231
233
|
renderOption,
|
|
232
|
-
selectedEmptyText
|
|
233
|
-
availableEmptyText
|
|
234
|
-
noResultsText
|
|
234
|
+
selectedEmptyText: selectedEmptyTextFromProps,
|
|
235
|
+
availableEmptyText: availableEmptyTextFromProps,
|
|
236
|
+
noResultsText: noResultsTextFromProps,
|
|
235
237
|
xstyle,
|
|
236
238
|
className,
|
|
237
239
|
style,
|
|
@@ -241,6 +243,13 @@ export function TransferList({
|
|
|
241
243
|
'aria-describedby': ariaDescribedBy,
|
|
242
244
|
...restProps
|
|
243
245
|
}) {
|
|
246
|
+
const t = useTranslator();
|
|
247
|
+
const selectedLabel = selectedLabelFromProps ?? t('@astryx.transferList.selectedLabel');
|
|
248
|
+
const availableLabel = availableLabelFromProps ?? t('@astryx.transferList.availableLabel');
|
|
249
|
+
const searchPlaceholder = searchPlaceholderFromProps ?? t('@astryx.transferList.searchPlaceholder');
|
|
250
|
+
const selectedEmptyText = selectedEmptyTextFromProps ?? t('@astryx.transferList.selectedEmpty');
|
|
251
|
+
const availableEmptyText = availableEmptyTextFromProps ?? t('@astryx.transferList.availableEmpty');
|
|
252
|
+
const noResultsText = noResultsTextFromProps ?? t('@astryx.transferList.noResults');
|
|
244
253
|
const labelId = useId();
|
|
245
254
|
const descriptionId = useId();
|
|
246
255
|
const selectedHeadingId = useId();
|
|
@@ -276,7 +285,7 @@ export function TransferList({
|
|
|
276
285
|
return map;
|
|
277
286
|
}, [options]);
|
|
278
287
|
const selectedValues = useMemo(() => new Set(value), [value]);
|
|
279
|
-
const normalizedQuery = query.trim().
|
|
288
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
280
289
|
const selectedOptions = useMemo(() => value.map(optionValue => optionByValue.get(optionValue)).filter(option => option != null && matchesQuery(option, normalizedQuery)), [normalizedQuery, optionByValue, value]);
|
|
281
290
|
const availableOptions = useMemo(() => options.filter(option => !selectedValues.has(option.value) && matchesQuery(option, normalizedQuery)), [normalizedQuery, options, selectedValues]);
|
|
282
291
|
const groupedAvailable = useMemo(() => {
|
|
@@ -371,25 +380,33 @@ export function TransferList({
|
|
|
371
380
|
}
|
|
372
381
|
const nextValue = [...currentValueRef.current, option.value];
|
|
373
382
|
commit(nextValue);
|
|
374
|
-
announce(
|
|
383
|
+
announce(t('@astryx.transferList.announceAdded', {
|
|
384
|
+
label: option.label,
|
|
385
|
+
count: nextValue.length
|
|
386
|
+
}));
|
|
375
387
|
focusAfterTransfer('available', index);
|
|
376
|
-
}, [announce, commit, focusAfterTransfer]);
|
|
388
|
+
}, [announce, commit, focusAfterTransfer, t]);
|
|
377
389
|
const removeOption = useCallback((option, index) => {
|
|
378
390
|
if (option.isTransferDisabled) {
|
|
379
391
|
return;
|
|
380
392
|
}
|
|
381
393
|
const nextValue = currentValueRef.current.filter(item => item !== option.value);
|
|
382
394
|
commit(nextValue);
|
|
383
|
-
announce(
|
|
395
|
+
announce(t('@astryx.transferList.announceRemoved', {
|
|
396
|
+
label: option.label,
|
|
397
|
+
count: nextValue.length
|
|
398
|
+
}));
|
|
384
399
|
focusAfterTransfer('selected', index);
|
|
385
|
-
}, [announce, commit, focusAfterTransfer]);
|
|
400
|
+
}, [announce, commit, focusAfterTransfer, t]);
|
|
386
401
|
const addAll = useCallback(() => {
|
|
387
402
|
const additions = options.filter(option => !option.isTransferDisabled && !currentValueRef.current.includes(option.value)).map(option => option.value);
|
|
388
403
|
if (additions.length > 0) {
|
|
389
404
|
commit([...currentValueRef.current, ...additions]);
|
|
390
|
-
announce(
|
|
405
|
+
announce(t('@astryx.transferList.announceBulkAdded', {
|
|
406
|
+
count: additions.length
|
|
407
|
+
}));
|
|
391
408
|
}
|
|
392
|
-
}, [announce, commit, options]);
|
|
409
|
+
}, [announce, commit, options, t]);
|
|
393
410
|
const clearSelected = useCallback(() => {
|
|
394
411
|
const nextValue = currentValueRef.current.filter(optionValue => {
|
|
395
412
|
const option = optionByValue.get(optionValue);
|
|
@@ -398,9 +415,11 @@ export function TransferList({
|
|
|
398
415
|
const removed = currentValueRef.current.length - nextValue.length;
|
|
399
416
|
if (removed > 0) {
|
|
400
417
|
commit(nextValue);
|
|
401
|
-
announce(
|
|
418
|
+
announce(t('@astryx.transferList.announceBulkRemoved', {
|
|
419
|
+
count: removed
|
|
420
|
+
}));
|
|
402
421
|
}
|
|
403
|
-
}, [announce, commit, optionByValue]);
|
|
422
|
+
}, [announce, commit, optionByValue, t]);
|
|
404
423
|
const movableRange = useCallback((optionValue, orderedValue) => {
|
|
405
424
|
const index = orderedValue.indexOf(optionValue);
|
|
406
425
|
let start = 0;
|
|
@@ -463,9 +482,13 @@ export function TransferList({
|
|
|
463
482
|
hasPointerMoved: false
|
|
464
483
|
});
|
|
465
484
|
if (mode === 'keyboard') {
|
|
466
|
-
announce(
|
|
485
|
+
announce(t('@astryx.transferList.announceGrabbed', {
|
|
486
|
+
label: option.label,
|
|
487
|
+
position: index + 1,
|
|
488
|
+
total: currentValueRef.current.length
|
|
489
|
+
}));
|
|
467
490
|
}
|
|
468
|
-
}, [announce, isReorderable, setReorderSession]);
|
|
491
|
+
}, [announce, isReorderable, setReorderSession, t]);
|
|
469
492
|
const finishReorder = useCallback(cancelled => {
|
|
470
493
|
const session = reorderSessionRef.current;
|
|
471
494
|
if (!session) {
|
|
@@ -475,7 +498,9 @@ export function TransferList({
|
|
|
475
498
|
if (cancelled || !session.hasPointerMoved) {
|
|
476
499
|
setReorderSession(null);
|
|
477
500
|
if (cancelled) {
|
|
478
|
-
announce(
|
|
501
|
+
announce(t('@astryx.transferList.announceMoveCancelled', {
|
|
502
|
+
label: session.label
|
|
503
|
+
}));
|
|
479
504
|
}
|
|
480
505
|
return;
|
|
481
506
|
}
|
|
@@ -484,21 +509,34 @@ export function TransferList({
|
|
|
484
509
|
flushSync(() => setReorderSession(null));
|
|
485
510
|
if (hasChanged) {
|
|
486
511
|
commit(nextValue);
|
|
487
|
-
announce(
|
|
512
|
+
announce(t('@astryx.transferList.announceDropped', {
|
|
513
|
+
label: session.label,
|
|
514
|
+
position: session.toIndex + 1,
|
|
515
|
+
total: session.originalValue.length
|
|
516
|
+
}));
|
|
488
517
|
} else {
|
|
489
|
-
announce(
|
|
518
|
+
announce(t('@astryx.transferList.announceReturned', {
|
|
519
|
+
label: session.label,
|
|
520
|
+
position: session.fromIndex + 1
|
|
521
|
+
}));
|
|
490
522
|
}
|
|
491
523
|
return;
|
|
492
524
|
}
|
|
493
525
|
if (cancelled) {
|
|
494
526
|
commit(session.originalValue);
|
|
495
|
-
announce(
|
|
527
|
+
announce(t('@astryx.transferList.announceMoveCancelled', {
|
|
528
|
+
label: session.label
|
|
529
|
+
}));
|
|
496
530
|
} else {
|
|
497
531
|
const index = currentValueRef.current.indexOf(session.value);
|
|
498
|
-
announce(
|
|
532
|
+
announce(t('@astryx.transferList.announceDropped', {
|
|
533
|
+
label: session.label,
|
|
534
|
+
position: index + 1,
|
|
535
|
+
total: currentValueRef.current.length
|
|
536
|
+
}));
|
|
499
537
|
}
|
|
500
538
|
setReorderSession(null);
|
|
501
|
-
}, [announce, commit, setReorderSession]);
|
|
539
|
+
}, [announce, commit, setReorderSession, t]);
|
|
502
540
|
const handleReorderClick = useCallback(option => {
|
|
503
541
|
if (suppressClickRef.current) {
|
|
504
542
|
suppressClickRef.current = false;
|
|
@@ -543,9 +581,13 @@ export function TransferList({
|
|
|
543
581
|
event.preventDefault();
|
|
544
582
|
if (moveOption(option.value, target)) {
|
|
545
583
|
const nextIndex = currentValueRef.current.indexOf(option.value);
|
|
546
|
-
announce(
|
|
584
|
+
announce(t('@astryx.transferList.announceMovedToPosition', {
|
|
585
|
+
label: option.label,
|
|
586
|
+
position: nextIndex + 1,
|
|
587
|
+
total: currentValueRef.current.length
|
|
588
|
+
}));
|
|
547
589
|
}
|
|
548
|
-
}, [announce, finishReorder, movableRange, moveOption]);
|
|
590
|
+
}, [announce, finishReorder, movableRange, moveOption, t]);
|
|
549
591
|
const handlePointerDown = useCallback((event, option) => {
|
|
550
592
|
if (event.button !== 0 || option.isReorderDisabled) {
|
|
551
593
|
return;
|
|
@@ -608,9 +650,13 @@ export function TransferList({
|
|
|
608
650
|
};
|
|
609
651
|
setReorderSession(nextSession);
|
|
610
652
|
if (hasCrossedThreshold && session.toIndex !== targetIndex) {
|
|
611
|
-
announce(
|
|
653
|
+
announce(t('@astryx.transferList.announceMovedToPosition', {
|
|
654
|
+
label: option.label,
|
|
655
|
+
position: targetIndex + 1,
|
|
656
|
+
total: session.originalValue.length
|
|
657
|
+
}));
|
|
612
658
|
}
|
|
613
|
-
}, [announce, movableRange, setReorderSession]);
|
|
659
|
+
}, [announce, movableRange, setReorderSession, t]);
|
|
614
660
|
const handlePointerEnd = useCallback((event, cancelled) => {
|
|
615
661
|
const session = reorderSessionRef.current;
|
|
616
662
|
if (session?.mode !== 'pointer' || session.pointerId !== event.pointerId) {
|
|
@@ -630,20 +676,26 @@ export function TransferList({
|
|
|
630
676
|
}, [finishReorder]);
|
|
631
677
|
const handleSearch = useCallback(nextQuery => {
|
|
632
678
|
setQuery(nextQuery);
|
|
633
|
-
const normalized = nextQuery.trim().
|
|
679
|
+
const normalized = nextQuery.trim().toLowerCase();
|
|
634
680
|
if (normalized === '') {
|
|
635
681
|
announce('');
|
|
636
682
|
} else {
|
|
637
683
|
const count = options.filter(option => matchesQuery(option, normalized)).length;
|
|
638
|
-
announce(
|
|
684
|
+
announce(t('@astryx.transferList.announceSearchResults', {
|
|
685
|
+
count
|
|
686
|
+
}));
|
|
639
687
|
}
|
|
640
|
-
}, [announce, options]);
|
|
688
|
+
}, [announce, options, t]);
|
|
641
689
|
const optionActions = (option, side, index) => {
|
|
642
690
|
const isTransferDisabled = option.isTransferDisabled === true;
|
|
643
|
-
const disabledReason = option.disabledMessage ??
|
|
691
|
+
const disabledReason = option.disabledMessage ?? t('@astryx.transferList.transferDisabled', {
|
|
692
|
+
label: option.label
|
|
693
|
+
});
|
|
644
694
|
if (side === 'available') {
|
|
645
695
|
return /*#__PURE__*/_jsx(IconButton, {
|
|
646
|
-
label:
|
|
696
|
+
label: t('@astryx.transferList.addOption', {
|
|
697
|
+
label: option.label
|
|
698
|
+
}),
|
|
647
699
|
icon: /*#__PURE__*/_jsx(PlusIcon, {}),
|
|
648
700
|
size: "sm",
|
|
649
701
|
variant: "ghost",
|
|
@@ -654,7 +706,9 @@ export function TransferList({
|
|
|
654
706
|
});
|
|
655
707
|
}
|
|
656
708
|
return /*#__PURE__*/_jsx(IconButton, {
|
|
657
|
-
label:
|
|
709
|
+
label: t('@astryx.transferList.removeOption', {
|
|
710
|
+
label: option.label
|
|
711
|
+
}),
|
|
658
712
|
icon: /*#__PURE__*/_jsx(RemoveIcon, {}),
|
|
659
713
|
size: "sm",
|
|
660
714
|
variant: "ghost",
|
|
@@ -687,9 +741,13 @@ export function TransferList({
|
|
|
687
741
|
}
|
|
688
742
|
const active = reorderSession?.value === option.value;
|
|
689
743
|
const isReorderDisabled = option.isReorderDisabled === true;
|
|
690
|
-
const disabledReason = option.disabledMessage ??
|
|
744
|
+
const disabledReason = option.disabledMessage ?? t('@astryx.transferList.reorderDisabled', {
|
|
745
|
+
label: option.label
|
|
746
|
+
});
|
|
691
747
|
return /*#__PURE__*/_jsx(IconButton, {
|
|
692
|
-
label:
|
|
748
|
+
label: t('@astryx.transferList.reorderOption', {
|
|
749
|
+
label: option.label
|
|
750
|
+
}),
|
|
693
751
|
"aria-describedby": reorderInstructionsId,
|
|
694
752
|
"aria-pressed": active,
|
|
695
753
|
icon: /*#__PURE__*/_jsx(GripVerticalIcon, {}),
|
|
@@ -778,7 +836,9 @@ export function TransferList({
|
|
|
778
836
|
children: /*#__PURE__*/_jsx(TextInput, {
|
|
779
837
|
ref: searchRef,
|
|
780
838
|
role: "searchbox",
|
|
781
|
-
label: searchLabel ??
|
|
839
|
+
label: searchLabel ?? t('@astryx.transferList.searchLabel', {
|
|
840
|
+
label
|
|
841
|
+
}),
|
|
782
842
|
isLabelHidden: true,
|
|
783
843
|
value: query,
|
|
784
844
|
placeholder: searchPlaceholder,
|
|
@@ -794,7 +854,7 @@ export function TransferList({
|
|
|
794
854
|
})
|
|
795
855
|
}), /*#__PURE__*/_jsx(VisuallyHidden, {
|
|
796
856
|
id: reorderInstructionsId,
|
|
797
|
-
children:
|
|
857
|
+
children: t('@astryx.transferList.reorderInstructions')
|
|
798
858
|
}), /*#__PURE__*/_jsxs("div", {
|
|
799
859
|
...mergeProps(themeProps('transfer-list-collection'), {
|
|
800
860
|
className: "xrvj5dj xcjjqqt x1sp3fdr xxhr3t xeuugli"
|
|
@@ -817,7 +877,7 @@ export function TransferList({
|
|
|
817
877
|
color: "secondary",
|
|
818
878
|
children: selectedLabel
|
|
819
879
|
}), hasClear && /*#__PURE__*/_jsx(Button, {
|
|
820
|
-
label:
|
|
880
|
+
label: t('@astryx.transferList.clear'),
|
|
821
881
|
size: "sm",
|
|
822
882
|
variant: "ghost",
|
|
823
883
|
xstyle: styles.headerAction,
|
|
@@ -866,7 +926,7 @@ export function TransferList({
|
|
|
866
926
|
color: "secondary",
|
|
867
927
|
children: availableLabel
|
|
868
928
|
}), hasSelectAll && /*#__PURE__*/_jsx(Button, {
|
|
869
|
-
label:
|
|
929
|
+
label: t('@astryx.transferList.addAll'),
|
|
870
930
|
size: "sm",
|
|
871
931
|
variant: "ghost",
|
|
872
932
|
xstyle: styles.headerAction,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file TransferListSelector.tsx
|
|
3
|
-
* @input Controlled option data, uncontrolled ComplexSelector state, TransferList, and
|
|
3
|
+
* @input Controlled option data, uncontrolled ComplexSelector state, TransferList, action controls, and the core i18n catalog
|
|
4
4
|
* @output Exports the immediate-or-staged TransferListSelector composition with draft cleanup after dismissal
|
|
5
5
|
* @position Lab collection input shell; consumed by the TransferList entry point, docs, and tests
|
|
6
6
|
*
|
|
@@ -49,7 +49,7 @@ export type TransferListSelectorProps<T extends string = string> = Pick<ComplexS
|
|
|
49
49
|
* />
|
|
50
50
|
* ```
|
|
51
51
|
*/
|
|
52
|
-
export declare function TransferListSelector<T extends string = string>({ label, description, options, value, onChange, changeAction, commitBehavior, triggerLabel, isDisabled, isLoading, width, applyLabel, cancelLabel, selectedLabel, availableLabel, hasSearch, searchLabel, searchPlaceholder, isReorderable, hasSelectAll, hasClear, renderOption, selectedEmptyText, availableEmptyText, noResultsText, ...selectorProps }: TransferListSelectorProps<T>): ReactNode;
|
|
52
|
+
export declare function TransferListSelector<T extends string = string>({ label, description, options, value, onChange, changeAction, commitBehavior, triggerLabel, isDisabled, isLoading, width, applyLabel: applyLabelFromProps, cancelLabel: cancelLabelFromProps, selectedLabel, availableLabel, hasSearch, searchLabel, searchPlaceholder, isReorderable, hasSelectAll, hasClear, renderOption, selectedEmptyText, availableEmptyText, noResultsText, ...selectorProps }: TransferListSelectorProps<T>): ReactNode;
|
|
53
53
|
export declare namespace TransferListSelector {
|
|
54
54
|
var displayName: string;
|
|
55
55
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TransferListSelector.d.ts","sourceRoot":"","sources":["../../src/TransferList/TransferListSelector.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAA2C,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAG/E,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"TransferListSelector.d.ts","sourceRoot":"","sources":["../../src/TransferList/TransferListSelector.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAA2C,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAG/E,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,oCAAoC,CAAC;AAI5C,OAAO,EAAe,KAAK,iBAAiB,EAAC,MAAM,gBAAgB,CAAC;AAsDpE,KAAK,6BAA6B,GAC9B,OAAO,GACP,cAAc,GACd,eAAe,GACf,aAAa,GACb,YAAY,GACZ,YAAY,GACZ,YAAY,GACZ,WAAW,GACX,QAAQ,GACR,eAAe,GACf,cAAc,GACd,MAAM,GACN,OAAO,GACP,WAAW,GACX,QAAQ,GACR,WAAW,GACX,OAAO,GACP,aAAa,CAAC;AAElB,KAAK,uBAAuB,GACxB,SAAS,GACT,eAAe,GACf,gBAAgB,GAChB,WAAW,GACX,aAAa,GACb,mBAAmB,GACnB,eAAe,GACf,cAAc,GACd,UAAU,GACV,cAAc,GACd,mBAAmB,GACnB,oBAAoB,GACpB,eAAe,CAAC;AAEpB,MAAM,MAAM,kCAAkC,GAAG,WAAW,GAAG,QAAQ,CAAC;AAExE,MAAM,MAAM,yBAAyB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI,IAAI,CACrE,oBAAoB,CAAC,SAAS,CAAC,EAAE,CAAC,EAClC,6BAA6B,CAC9B,GACC,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,uBAAuB,CAAC,GAAG;IACpD,mDAAmD;IACnD,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;IACpB,4CAA4C;IAC5C,QAAQ,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,KAAK,IAAI,CAAC;IACxC,8DAA8D;IAC9D,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D;;;OAGG;IACH,cAAc,CAAC,EAAE,kCAAkC,CAAC;IACpD,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AA+BJ;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,EAC9D,KAAK,EACL,WAAW,EACX,OAAO,EACP,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,cAA4B,EAC5B,YAAY,EACZ,UAAU,EACV,SAAS,EACT,KAAK,EACL,UAAU,EAAE,mBAAmB,EAC/B,WAAW,EAAE,oBAAoB,EACjC,aAAa,EACb,cAAc,EACd,SAAS,EACT,WAAW,EACX,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,QAAQ,EACR,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,EACb,GAAG,aAAa,EACjB,EAAE,yBAAyB,CAAC,CAAC,CAAC,GAAG,SAAS,CAqI1C;yBAhKe,oBAAoB"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file TransferListSelector.tsx
|
|
7
|
-
* @input Controlled option data, uncontrolled ComplexSelector state, TransferList, and
|
|
7
|
+
* @input Controlled option data, uncontrolled ComplexSelector state, TransferList, action controls, and the core i18n catalog
|
|
8
8
|
* @output Exports the immediate-or-staged TransferListSelector composition with draft cleanup after dismissal
|
|
9
9
|
* @position Lab collection input shell; consumed by the TransferList entry point, docs, and tests
|
|
10
10
|
*
|
|
@@ -17,6 +17,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
17
17
|
import * as stylex from '@stylexjs/stylex';
|
|
18
18
|
import { Button } from '@astryxdesign/core/Button';
|
|
19
19
|
import { ComplexSelector } from '@astryxdesign/core/ComplexSelector';
|
|
20
|
+
import { useTranslator } from '@astryxdesign/core/i18n';
|
|
20
21
|
import { HStack } from '@astryxdesign/core/Stack';
|
|
21
22
|
import '@astryxdesign/core/theme/tokens.stylex';
|
|
22
23
|
import { borderVars, colorVars } from '@astryxdesign/core/theme/tokens.stylex';
|
|
@@ -94,8 +95,8 @@ export function TransferListSelector({
|
|
|
94
95
|
isDisabled,
|
|
95
96
|
isLoading,
|
|
96
97
|
width,
|
|
97
|
-
applyLabel
|
|
98
|
-
cancelLabel
|
|
98
|
+
applyLabel: applyLabelFromProps,
|
|
99
|
+
cancelLabel: cancelLabelFromProps,
|
|
99
100
|
selectedLabel,
|
|
100
101
|
availableLabel,
|
|
101
102
|
hasSearch,
|
|
@@ -110,6 +111,9 @@ export function TransferListSelector({
|
|
|
110
111
|
noResultsText,
|
|
111
112
|
...selectorProps
|
|
112
113
|
}) {
|
|
114
|
+
const t = useTranslator();
|
|
115
|
+
const applyLabel = applyLabelFromProps ?? t('@astryx.transferListSelector.apply');
|
|
116
|
+
const cancelLabel = cancelLabelFromProps ?? t('@astryx.transferListSelector.cancel');
|
|
113
117
|
const [draftValue, setDraftValue] = useState(value);
|
|
114
118
|
const previousAppliedValueRef = useRef([...value]);
|
|
115
119
|
const previousCommitBehaviorRef = useRef(commitBehavior);
|
|
@@ -137,7 +141,9 @@ export function TransferListSelector({
|
|
|
137
141
|
value: value,
|
|
138
142
|
onChange: onChange,
|
|
139
143
|
changeAction: changeAction,
|
|
140
|
-
triggerLabel: triggerLabel ??
|
|
144
|
+
triggerLabel: triggerLabel ?? t('@astryx.transferListSelector.triggerLabel', {
|
|
145
|
+
count: value.length
|
|
146
|
+
}),
|
|
141
147
|
isDisabled: isDisabled,
|
|
142
148
|
isLoading: isLoading,
|
|
143
149
|
width: resolvedWidth,
|
|
@@ -154,16 +160,27 @@ export function TransferListSelector({
|
|
|
154
160
|
isOpen: state.isOpen,
|
|
155
161
|
resetDraft: resetDraft
|
|
156
162
|
}), /*#__PURE__*/_jsx("fieldset", {
|
|
157
|
-
disabled: isDisabled
|
|
163
|
+
disabled: isDisabled,
|
|
158
164
|
...{
|
|
159
|
-
|
|
160
|
-
|
|
165
|
+
0: {
|
|
166
|
+
className: "x1iyjqo2 xs83m0k xdl72j9 xeuugli x2lwn1j x1ghz6dp x1717udv xc342km xng3xce x1odjw0f xish69e"
|
|
167
|
+
},
|
|
168
|
+
1: {
|
|
169
|
+
className: "x1iyjqo2 xs83m0k xdl72j9 xeuugli x2lwn1j x1ghz6dp x1717udv xc342km xng3xce x1odjw0f xish69e x19v85br x16khyan"
|
|
170
|
+
}
|
|
171
|
+
}[!!state.isBusy << 0],
|
|
161
172
|
children: /*#__PURE__*/_jsx(TransferList, {
|
|
162
173
|
label: label,
|
|
163
174
|
isLabelHidden: true,
|
|
164
175
|
options: options,
|
|
165
176
|
value: transferListValue,
|
|
166
177
|
onChange: nextValue => {
|
|
178
|
+
// Now that busy no longer disables the fieldset, the controls
|
|
179
|
+
// stay live while a change is in flight; ignore transfers
|
|
180
|
+
// until it settles so one pending action cannot be stacked.
|
|
181
|
+
if (state.isBusy) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
167
184
|
if (isStaged) {
|
|
168
185
|
setDraftValue(nextValue);
|
|
169
186
|
} else {
|
package/dist/lab.css
CHANGED
|
@@ -301,6 +301,7 @@
|
|
|
301
301
|
.x1jm3nie:not(#\#):not(#\#):not(#\#){cursor:grab}
|
|
302
302
|
.xi9pz9s:not(#\#):not(#\#):not(#\#){cursor:grabbing}
|
|
303
303
|
.x1ypdohk:not(#\#):not(#\#):not(#\#){cursor:pointer}
|
|
304
|
+
.x19v85br:not(#\#):not(#\#):not(#\#){cursor:progress}
|
|
304
305
|
.x1lliihq:not(#\#):not(#\#):not(#\#){display:block}
|
|
305
306
|
.xjp7ctv:not(#\#):not(#\#):not(#\#){display:contents}
|
|
306
307
|
.x78zum5:not(#\#):not(#\#):not(#\#){display:flex}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/lab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-canary.055e75f",
|
|
4
4
|
"description": "Experimental Astryx components — published to npm only under the @canary dist-tag for early testing; never released as a stable (latest) version.",
|
|
5
5
|
"author": "Meta Open Source",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"canaryOnly": true,
|
|
24
24
|
"_comment": "Published ONLY to the @canary dist-tag (see .github/workflows/release.yml). Never published as a stable `latest` release. `private: true` is intentional and must stay — it is npm's hard guarantee against an accidental stable publish."
|
|
25
25
|
},
|
|
26
|
+
"type": "module",
|
|
26
27
|
"main": "./dist/index.js",
|
|
27
|
-
"module": "./dist/index.js",
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
29
29
|
"sideEffects": false,
|
|
30
30
|
"files": [
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@stylexjs/stylex": ">=0.10.0",
|
|
48
|
-
"@astryxdesign/core": "0.
|
|
48
|
+
"@astryxdesign/core": "0.5.0-canary.055e75f",
|
|
49
49
|
"react": ">=19.0.0",
|
|
50
50
|
"react-dom": ">=19.0.0"
|
|
51
51
|
},
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"d3-shape": "^3.2.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@astryxdesign/cli": "0.
|
|
58
|
+
"@astryxdesign/cli": "0.5.0-canary.055e75f",
|
|
59
59
|
"@types/d3-array": "^3.2.2",
|
|
60
60
|
"@types/d3-scale": "^4.0.9",
|
|
61
61
|
"@types/d3-shape": "^3.1.8",
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"build": "rimraf dist && pnpm build:esm && tsc --project tsconfig.build.json && pnpm build:css && node ../../scripts/check-no-dev-jsx.mjs && node ../../scripts/check-fully-specified.mjs",
|
|
66
|
-
"build:esm": "babel src --out-dir dist --extensions .ts,.tsx --out-file-extension .js --ignore \"**/*.test.ts,**/*.test.tsx,**/__tests__/**,**/*.perf.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css,**/*.css.d.ts\" --config-file ./babel.config.
|
|
66
|
+
"build:esm": "babel src --out-dir dist --extensions .ts,.tsx --out-file-extension .js --ignore \"**/*.test.ts,**/*.test.tsx,**/__tests__/**,**/*.perf.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css,**/*.css.d.ts\" --config-file ./babel.config.json",
|
|
67
67
|
"build:css": "node ../../scripts/build-css.mjs --package lab",
|
|
68
|
-
"dev": "babel src --watch --out-dir dist --extensions .ts,.tsx --out-file-extension .js --ignore \"**/*.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css*\" --config-file ./babel.config.
|
|
68
|
+
"dev": "babel src --watch --out-dir dist --extensions .ts,.tsx --out-file-extension .js --ignore \"**/*.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css*\" --config-file ./babel.config.json",
|
|
69
69
|
"test": "vitest run --root ../..",
|
|
70
70
|
"test:watch": "vitest --root ../..",
|
|
71
71
|
"lint": "eslint src",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file TransferList.tsx
|
|
7
|
-
* @input Controlled option data, React DOM portals, local action glyphs,
|
|
7
|
+
* @input Controlled option data, React DOM portals, local action glyphs, shared Lab reorder styles, and the core i18n catalog
|
|
8
8
|
* @output Exports TransferList with immediate transfers, vertical reordering, and responsive scroll ownership
|
|
9
9
|
* @position Lab collection input; consumed by index.ts, TransferListSelector, docs, and tests
|
|
10
10
|
*
|
|
@@ -40,6 +40,7 @@ import {Text} from '@astryxdesign/core/Text';
|
|
|
40
40
|
import {TextInput} from '@astryxdesign/core/TextInput';
|
|
41
41
|
import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
|
|
42
42
|
import {useAnnounce} from '@astryxdesign/core/hooks';
|
|
43
|
+
import {useTranslator} from '@astryxdesign/core/i18n';
|
|
43
44
|
import {
|
|
44
45
|
borderVars,
|
|
45
46
|
colorVars,
|
|
@@ -364,6 +365,9 @@ function moveItem<T>(
|
|
|
364
365
|
return nextValue;
|
|
365
366
|
}
|
|
366
367
|
|
|
368
|
+
// Case folding is locale-independent here, matching Selector, MultiSelector,
|
|
369
|
+
// and CommandPalette. Locale-sensitive folding would mean raw Intl access,
|
|
370
|
+
// which the shipped packages route through the provider instead.
|
|
367
371
|
function matchesQuery<T extends string>(
|
|
368
372
|
option: TransferListOption<T>,
|
|
369
373
|
query: string,
|
|
@@ -374,14 +378,10 @@ function matchesQuery<T extends string>(
|
|
|
374
378
|
const description =
|
|
375
379
|
typeof option.description === 'string' ? option.description : '';
|
|
376
380
|
return [option.label, description, option.group ?? ''].some(part =>
|
|
377
|
-
part.
|
|
381
|
+
part.toLowerCase().includes(query),
|
|
378
382
|
);
|
|
379
383
|
}
|
|
380
384
|
|
|
381
|
-
function itemCount(count: number): string {
|
|
382
|
-
return `${count} ${count === 1 ? 'item' : 'items'}`;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
385
|
/**
|
|
386
386
|
* A controlled dual-panel collection input for choosing and ordering values.
|
|
387
387
|
* Selection, ordering, drafts, persistence, and saved views stay explicit in
|
|
@@ -405,18 +405,18 @@ export function TransferList<T extends string = string>({
|
|
|
405
405
|
options,
|
|
406
406
|
value,
|
|
407
407
|
onChange,
|
|
408
|
-
selectedLabel
|
|
409
|
-
availableLabel
|
|
408
|
+
selectedLabel: selectedLabelFromProps,
|
|
409
|
+
availableLabel: availableLabelFromProps,
|
|
410
410
|
hasSearch = false,
|
|
411
411
|
searchLabel,
|
|
412
|
-
searchPlaceholder
|
|
412
|
+
searchPlaceholder: searchPlaceholderFromProps,
|
|
413
413
|
isReorderable = true,
|
|
414
414
|
hasSelectAll = false,
|
|
415
415
|
hasClear = false,
|
|
416
416
|
renderOption,
|
|
417
|
-
selectedEmptyText
|
|
418
|
-
availableEmptyText
|
|
419
|
-
noResultsText
|
|
417
|
+
selectedEmptyText: selectedEmptyTextFromProps,
|
|
418
|
+
availableEmptyText: availableEmptyTextFromProps,
|
|
419
|
+
noResultsText: noResultsTextFromProps,
|
|
420
420
|
xstyle,
|
|
421
421
|
className,
|
|
422
422
|
style,
|
|
@@ -426,6 +426,19 @@ export function TransferList<T extends string = string>({
|
|
|
426
426
|
'aria-describedby': ariaDescribedBy,
|
|
427
427
|
...restProps
|
|
428
428
|
}: TransferListProps<T>): ReactNode {
|
|
429
|
+
const t = useTranslator();
|
|
430
|
+
const selectedLabel =
|
|
431
|
+
selectedLabelFromProps ?? t('@astryx.transferList.selectedLabel');
|
|
432
|
+
const availableLabel =
|
|
433
|
+
availableLabelFromProps ?? t('@astryx.transferList.availableLabel');
|
|
434
|
+
const searchPlaceholder =
|
|
435
|
+
searchPlaceholderFromProps ?? t('@astryx.transferList.searchPlaceholder');
|
|
436
|
+
const selectedEmptyText =
|
|
437
|
+
selectedEmptyTextFromProps ?? t('@astryx.transferList.selectedEmpty');
|
|
438
|
+
const availableEmptyText =
|
|
439
|
+
availableEmptyTextFromProps ?? t('@astryx.transferList.availableEmpty');
|
|
440
|
+
const noResultsText =
|
|
441
|
+
noResultsTextFromProps ?? t('@astryx.transferList.noResults');
|
|
429
442
|
const labelId = useId();
|
|
430
443
|
const descriptionId = useId();
|
|
431
444
|
const selectedHeadingId = useId();
|
|
@@ -466,7 +479,7 @@ export function TransferList<T extends string = string>({
|
|
|
466
479
|
return map;
|
|
467
480
|
}, [options]);
|
|
468
481
|
const selectedValues = useMemo(() => new Set(value), [value]);
|
|
469
|
-
const normalizedQuery = query.trim().
|
|
482
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
470
483
|
const selectedOptions = useMemo(
|
|
471
484
|
() =>
|
|
472
485
|
value
|
|
@@ -603,11 +616,14 @@ export function TransferList<T extends string = string>({
|
|
|
603
616
|
const nextValue = [...currentValueRef.current, option.value];
|
|
604
617
|
commit(nextValue);
|
|
605
618
|
announce(
|
|
606
|
-
|
|
619
|
+
t('@astryx.transferList.announceAdded', {
|
|
620
|
+
label: option.label,
|
|
621
|
+
count: nextValue.length,
|
|
622
|
+
}),
|
|
607
623
|
);
|
|
608
624
|
focusAfterTransfer('available', index);
|
|
609
625
|
},
|
|
610
|
-
[announce, commit, focusAfterTransfer],
|
|
626
|
+
[announce, commit, focusAfterTransfer, t],
|
|
611
627
|
);
|
|
612
628
|
|
|
613
629
|
const removeOption = useCallback(
|
|
@@ -620,11 +636,14 @@ export function TransferList<T extends string = string>({
|
|
|
620
636
|
);
|
|
621
637
|
commit(nextValue);
|
|
622
638
|
announce(
|
|
623
|
-
|
|
639
|
+
t('@astryx.transferList.announceRemoved', {
|
|
640
|
+
label: option.label,
|
|
641
|
+
count: nextValue.length,
|
|
642
|
+
}),
|
|
624
643
|
);
|
|
625
644
|
focusAfterTransfer('selected', index);
|
|
626
645
|
},
|
|
627
|
-
[announce, commit, focusAfterTransfer],
|
|
646
|
+
[announce, commit, focusAfterTransfer, t],
|
|
628
647
|
);
|
|
629
648
|
|
|
630
649
|
const addAll = useCallback(() => {
|
|
@@ -637,9 +656,13 @@ export function TransferList<T extends string = string>({
|
|
|
637
656
|
.map(option => option.value);
|
|
638
657
|
if (additions.length > 0) {
|
|
639
658
|
commit([...currentValueRef.current, ...additions]);
|
|
640
|
-
announce(
|
|
659
|
+
announce(
|
|
660
|
+
t('@astryx.transferList.announceBulkAdded', {
|
|
661
|
+
count: additions.length,
|
|
662
|
+
}),
|
|
663
|
+
);
|
|
641
664
|
}
|
|
642
|
-
}, [announce, commit, options]);
|
|
665
|
+
}, [announce, commit, options, t]);
|
|
643
666
|
|
|
644
667
|
const clearSelected = useCallback(() => {
|
|
645
668
|
const nextValue = currentValueRef.current.filter(optionValue => {
|
|
@@ -649,9 +672,9 @@ export function TransferList<T extends string = string>({
|
|
|
649
672
|
const removed = currentValueRef.current.length - nextValue.length;
|
|
650
673
|
if (removed > 0) {
|
|
651
674
|
commit(nextValue);
|
|
652
|
-
announce(
|
|
675
|
+
announce(t('@astryx.transferList.announceBulkRemoved', {count: removed}));
|
|
653
676
|
}
|
|
654
|
-
}, [announce, commit, optionByValue]);
|
|
677
|
+
}, [announce, commit, optionByValue, t]);
|
|
655
678
|
|
|
656
679
|
const movableRange = useCallback(
|
|
657
680
|
(optionValue: T, orderedValue: readonly T[]) => {
|
|
@@ -721,11 +744,15 @@ export function TransferList<T extends string = string>({
|
|
|
721
744
|
});
|
|
722
745
|
if (mode === 'keyboard') {
|
|
723
746
|
announce(
|
|
724
|
-
|
|
747
|
+
t('@astryx.transferList.announceGrabbed', {
|
|
748
|
+
label: option.label,
|
|
749
|
+
position: index + 1,
|
|
750
|
+
total: currentValueRef.current.length,
|
|
751
|
+
}),
|
|
725
752
|
);
|
|
726
753
|
}
|
|
727
754
|
},
|
|
728
|
-
[announce, isReorderable, setReorderSession],
|
|
755
|
+
[announce, isReorderable, setReorderSession, t],
|
|
729
756
|
);
|
|
730
757
|
|
|
731
758
|
const finishReorder = useCallback(
|
|
@@ -738,7 +765,11 @@ export function TransferList<T extends string = string>({
|
|
|
738
765
|
if (cancelled || !session.hasPointerMoved) {
|
|
739
766
|
setReorderSession(null);
|
|
740
767
|
if (cancelled) {
|
|
741
|
-
announce(
|
|
768
|
+
announce(
|
|
769
|
+
t('@astryx.transferList.announceMoveCancelled', {
|
|
770
|
+
label: session.label,
|
|
771
|
+
}),
|
|
772
|
+
);
|
|
742
773
|
}
|
|
743
774
|
return;
|
|
744
775
|
}
|
|
@@ -752,27 +783,42 @@ export function TransferList<T extends string = string>({
|
|
|
752
783
|
if (hasChanged) {
|
|
753
784
|
commit(nextValue);
|
|
754
785
|
announce(
|
|
755
|
-
|
|
786
|
+
t('@astryx.transferList.announceDropped', {
|
|
787
|
+
label: session.label,
|
|
788
|
+
position: session.toIndex + 1,
|
|
789
|
+
total: session.originalValue.length,
|
|
790
|
+
}),
|
|
756
791
|
);
|
|
757
792
|
} else {
|
|
758
793
|
announce(
|
|
759
|
-
|
|
794
|
+
t('@astryx.transferList.announceReturned', {
|
|
795
|
+
label: session.label,
|
|
796
|
+
position: session.fromIndex + 1,
|
|
797
|
+
}),
|
|
760
798
|
);
|
|
761
799
|
}
|
|
762
800
|
return;
|
|
763
801
|
}
|
|
764
802
|
if (cancelled) {
|
|
765
803
|
commit(session.originalValue);
|
|
766
|
-
announce(
|
|
804
|
+
announce(
|
|
805
|
+
t('@astryx.transferList.announceMoveCancelled', {
|
|
806
|
+
label: session.label,
|
|
807
|
+
}),
|
|
808
|
+
);
|
|
767
809
|
} else {
|
|
768
810
|
const index = currentValueRef.current.indexOf(session.value);
|
|
769
811
|
announce(
|
|
770
|
-
|
|
812
|
+
t('@astryx.transferList.announceDropped', {
|
|
813
|
+
label: session.label,
|
|
814
|
+
position: index + 1,
|
|
815
|
+
total: currentValueRef.current.length,
|
|
816
|
+
}),
|
|
771
817
|
);
|
|
772
818
|
}
|
|
773
819
|
setReorderSession(null);
|
|
774
820
|
},
|
|
775
|
-
[announce, commit, setReorderSession],
|
|
821
|
+
[announce, commit, setReorderSession, t],
|
|
776
822
|
);
|
|
777
823
|
|
|
778
824
|
const handleReorderClick = useCallback(
|
|
@@ -827,11 +873,15 @@ export function TransferList<T extends string = string>({
|
|
|
827
873
|
if (moveOption(option.value, target)) {
|
|
828
874
|
const nextIndex = currentValueRef.current.indexOf(option.value);
|
|
829
875
|
announce(
|
|
830
|
-
|
|
876
|
+
t('@astryx.transferList.announceMovedToPosition', {
|
|
877
|
+
label: option.label,
|
|
878
|
+
position: nextIndex + 1,
|
|
879
|
+
total: currentValueRef.current.length,
|
|
880
|
+
}),
|
|
831
881
|
);
|
|
832
882
|
}
|
|
833
883
|
},
|
|
834
|
-
[announce, finishReorder, movableRange, moveOption],
|
|
884
|
+
[announce, finishReorder, movableRange, moveOption, t],
|
|
835
885
|
);
|
|
836
886
|
|
|
837
887
|
const handlePointerDown = useCallback(
|
|
@@ -922,11 +972,15 @@ export function TransferList<T extends string = string>({
|
|
|
922
972
|
setReorderSession(nextSession);
|
|
923
973
|
if (hasCrossedThreshold && session.toIndex !== targetIndex) {
|
|
924
974
|
announce(
|
|
925
|
-
|
|
975
|
+
t('@astryx.transferList.announceMovedToPosition', {
|
|
976
|
+
label: option.label,
|
|
977
|
+
position: targetIndex + 1,
|
|
978
|
+
total: session.originalValue.length,
|
|
979
|
+
}),
|
|
926
980
|
);
|
|
927
981
|
}
|
|
928
982
|
},
|
|
929
|
-
[announce, movableRange, setReorderSession],
|
|
983
|
+
[announce, movableRange, setReorderSession, t],
|
|
930
984
|
);
|
|
931
985
|
|
|
932
986
|
const handlePointerEnd = useCallback(
|
|
@@ -956,17 +1010,17 @@ export function TransferList<T extends string = string>({
|
|
|
956
1010
|
const handleSearch = useCallback(
|
|
957
1011
|
(nextQuery: string) => {
|
|
958
1012
|
setQuery(nextQuery);
|
|
959
|
-
const normalized = nextQuery.trim().
|
|
1013
|
+
const normalized = nextQuery.trim().toLowerCase();
|
|
960
1014
|
if (normalized === '') {
|
|
961
1015
|
announce('');
|
|
962
1016
|
} else {
|
|
963
1017
|
const count = options.filter(option =>
|
|
964
1018
|
matchesQuery(option, normalized),
|
|
965
1019
|
).length;
|
|
966
|
-
announce(
|
|
1020
|
+
announce(t('@astryx.transferList.announceSearchResults', {count}));
|
|
967
1021
|
}
|
|
968
1022
|
},
|
|
969
|
-
[announce, options],
|
|
1023
|
+
[announce, options, t],
|
|
970
1024
|
);
|
|
971
1025
|
|
|
972
1026
|
const optionActions = (
|
|
@@ -976,11 +1030,12 @@ export function TransferList<T extends string = string>({
|
|
|
976
1030
|
) => {
|
|
977
1031
|
const isTransferDisabled = option.isTransferDisabled === true;
|
|
978
1032
|
const disabledReason =
|
|
979
|
-
option.disabledMessage ??
|
|
1033
|
+
option.disabledMessage ??
|
|
1034
|
+
t('@astryx.transferList.transferDisabled', {label: option.label});
|
|
980
1035
|
if (side === 'available') {
|
|
981
1036
|
return (
|
|
982
1037
|
<IconButton
|
|
983
|
-
label={
|
|
1038
|
+
label={t('@astryx.transferList.addOption', {label: option.label})}
|
|
984
1039
|
icon={<PlusIcon />}
|
|
985
1040
|
size="sm"
|
|
986
1041
|
variant="ghost"
|
|
@@ -993,7 +1048,7 @@ export function TransferList<T extends string = string>({
|
|
|
993
1048
|
}
|
|
994
1049
|
return (
|
|
995
1050
|
<IconButton
|
|
996
|
-
label={
|
|
1051
|
+
label={t('@astryx.transferList.removeOption', {label: option.label})}
|
|
997
1052
|
icon={<RemoveIcon />}
|
|
998
1053
|
size="sm"
|
|
999
1054
|
variant="ghost"
|
|
@@ -1034,10 +1089,11 @@ export function TransferList<T extends string = string>({
|
|
|
1034
1089
|
const active = reorderSession?.value === option.value;
|
|
1035
1090
|
const isReorderDisabled = option.isReorderDisabled === true;
|
|
1036
1091
|
const disabledReason =
|
|
1037
|
-
option.disabledMessage ??
|
|
1092
|
+
option.disabledMessage ??
|
|
1093
|
+
t('@astryx.transferList.reorderDisabled', {label: option.label});
|
|
1038
1094
|
return (
|
|
1039
1095
|
<IconButton
|
|
1040
|
-
label={
|
|
1096
|
+
label={t('@astryx.transferList.reorderOption', {label: option.label})}
|
|
1041
1097
|
aria-describedby={reorderInstructionsId}
|
|
1042
1098
|
aria-pressed={active}
|
|
1043
1099
|
icon={<GripVerticalIcon />}
|
|
@@ -1164,7 +1220,9 @@ export function TransferList<T extends string = string>({
|
|
|
1164
1220
|
<TextInput
|
|
1165
1221
|
ref={searchRef}
|
|
1166
1222
|
role="searchbox"
|
|
1167
|
-
label={
|
|
1223
|
+
label={
|
|
1224
|
+
searchLabel ?? t('@astryx.transferList.searchLabel', {label})
|
|
1225
|
+
}
|
|
1168
1226
|
isLabelHidden
|
|
1169
1227
|
value={query}
|
|
1170
1228
|
placeholder={searchPlaceholder}
|
|
@@ -1178,8 +1236,7 @@ export function TransferList<T extends string = string>({
|
|
|
1178
1236
|
)}
|
|
1179
1237
|
|
|
1180
1238
|
<VisuallyHidden id={reorderInstructionsId}>
|
|
1181
|
-
|
|
1182
|
-
or End to move it. Press Space or Enter to drop, or Escape to cancel.
|
|
1239
|
+
{t('@astryx.transferList.reorderInstructions')}
|
|
1183
1240
|
</VisuallyHidden>
|
|
1184
1241
|
|
|
1185
1242
|
<div
|
|
@@ -1202,7 +1259,7 @@ export function TransferList<T extends string = string>({
|
|
|
1202
1259
|
</Text>
|
|
1203
1260
|
{hasClear && (
|
|
1204
1261
|
<Button
|
|
1205
|
-
label=
|
|
1262
|
+
label={t('@astryx.transferList.clear')}
|
|
1206
1263
|
size="sm"
|
|
1207
1264
|
variant="ghost"
|
|
1208
1265
|
xstyle={styles.headerAction}
|
|
@@ -1259,7 +1316,7 @@ export function TransferList<T extends string = string>({
|
|
|
1259
1316
|
</Text>
|
|
1260
1317
|
{hasSelectAll && (
|
|
1261
1318
|
<Button
|
|
1262
|
-
label=
|
|
1319
|
+
label={t('@astryx.transferList.addAll')}
|
|
1263
1320
|
size="sm"
|
|
1264
1321
|
variant="ghost"
|
|
1265
1322
|
xstyle={styles.headerAction}
|
|
@@ -420,7 +420,7 @@ describe('TransferListSelector', () => {
|
|
|
420
420
|
).toHaveTextContent('3 selected');
|
|
421
421
|
});
|
|
422
422
|
|
|
423
|
-
it('disables list controls
|
|
423
|
+
it('disables list controls when disabled, but never when merely busy', async () => {
|
|
424
424
|
const view = render(
|
|
425
425
|
<TransferListSelector
|
|
426
426
|
label="Visible fields"
|
|
@@ -477,7 +477,10 @@ describe('TransferListSelector', () => {
|
|
|
477
477
|
await waitFor(() => {
|
|
478
478
|
expect(dialog.querySelector('[aria-busy="true"]')).not.toBeNull();
|
|
479
479
|
});
|
|
480
|
-
|
|
480
|
+
// A pending change is announced with aria-busy, never by disabling the
|
|
481
|
+
// fieldset: `disabled` here would blur the row button the user just
|
|
482
|
+
// activated and drop focus to <body>.
|
|
483
|
+
expect(dialog.querySelector('fieldset')).toBeEnabled();
|
|
481
484
|
expect(
|
|
482
485
|
within(dialog).getByRole('button', {name: 'Cancel', hidden: true}),
|
|
483
486
|
).toBeEnabled();
|
|
@@ -486,6 +489,46 @@ describe('TransferListSelector', () => {
|
|
|
486
489
|
).toBeDisabled();
|
|
487
490
|
});
|
|
488
491
|
|
|
492
|
+
it('keeps focus on the activated row control while a change is pending', async () => {
|
|
493
|
+
const user = userEvent.setup();
|
|
494
|
+
const view = render(
|
|
495
|
+
<TransferListSelector
|
|
496
|
+
label="Visible fields"
|
|
497
|
+
options={OPTIONS}
|
|
498
|
+
value={['name']}
|
|
499
|
+
onChange={() => {}}
|
|
500
|
+
/>,
|
|
501
|
+
);
|
|
502
|
+
await openSelector();
|
|
503
|
+
const dialog = screen.getByRole('dialog', {
|
|
504
|
+
name: 'Visible fields',
|
|
505
|
+
...hidden,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const addStatus = within(dialog).getByRole('button', {
|
|
509
|
+
name: 'Add Status',
|
|
510
|
+
hidden: true,
|
|
511
|
+
});
|
|
512
|
+
await user.click(addStatus);
|
|
513
|
+
expect(addStatus).toHaveFocus();
|
|
514
|
+
|
|
515
|
+
view.rerender(
|
|
516
|
+
<TransferListSelector
|
|
517
|
+
label="Visible fields"
|
|
518
|
+
options={OPTIONS}
|
|
519
|
+
value={['name']}
|
|
520
|
+
onChange={() => {}}
|
|
521
|
+
isLoading
|
|
522
|
+
/>,
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
await waitFor(() => {
|
|
526
|
+
expect(dialog.querySelector('[aria-busy="true"]')).not.toBeNull();
|
|
527
|
+
});
|
|
528
|
+
expect(addStatus).toHaveFocus();
|
|
529
|
+
expect(document.body).not.toHaveFocus();
|
|
530
|
+
});
|
|
531
|
+
|
|
489
532
|
it('forwards intentional selector shell and transfer-list content props', async () => {
|
|
490
533
|
render(
|
|
491
534
|
<TransferListSelector
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file TransferListSelector.tsx
|
|
7
|
-
* @input Controlled option data, uncontrolled ComplexSelector state, TransferList, and
|
|
7
|
+
* @input Controlled option data, uncontrolled ComplexSelector state, TransferList, action controls, and the core i18n catalog
|
|
8
8
|
* @output Exports the immediate-or-staged TransferListSelector composition with draft cleanup after dismissal
|
|
9
9
|
* @position Lab collection input shell; consumed by the TransferList entry point, docs, and tests
|
|
10
10
|
*
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
ComplexSelector,
|
|
22
22
|
type ComplexSelectorProps,
|
|
23
23
|
} from '@astryxdesign/core/ComplexSelector';
|
|
24
|
+
import {useTranslator} from '@astryxdesign/core/i18n';
|
|
24
25
|
import {HStack} from '@astryxdesign/core/Stack';
|
|
25
26
|
import {borderVars, colorVars} from '@astryxdesign/core/theme/tokens.stylex';
|
|
26
27
|
import {TransferList, type TransferListProps} from './TransferList';
|
|
@@ -58,6 +59,16 @@ const styles = stylex.create({
|
|
|
58
59
|
overflowY: 'auto',
|
|
59
60
|
overscrollBehavior: 'contain',
|
|
60
61
|
},
|
|
62
|
+
// Busy is not disabled: `disabled` on this fieldset would blur the Add or
|
|
63
|
+
// Remove button the user just activated, dropping focus to <body>. The
|
|
64
|
+
// pending state is carried by aria-busy plus the trigger's spinner, so this
|
|
65
|
+
// only has to read as unavailable to the pointer.
|
|
66
|
+
fieldsetBusy: {
|
|
67
|
+
cursor: {
|
|
68
|
+
default: 'progress',
|
|
69
|
+
':is(:disabled,[aria-disabled="true"])': 'default',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
61
72
|
footer: {
|
|
62
73
|
flexShrink: 0,
|
|
63
74
|
borderBlockStartWidth: borderVars['--border-width'],
|
|
@@ -184,8 +195,8 @@ export function TransferListSelector<T extends string = string>({
|
|
|
184
195
|
isDisabled,
|
|
185
196
|
isLoading,
|
|
186
197
|
width,
|
|
187
|
-
applyLabel
|
|
188
|
-
cancelLabel
|
|
198
|
+
applyLabel: applyLabelFromProps,
|
|
199
|
+
cancelLabel: cancelLabelFromProps,
|
|
189
200
|
selectedLabel,
|
|
190
201
|
availableLabel,
|
|
191
202
|
hasSearch,
|
|
@@ -200,6 +211,11 @@ export function TransferListSelector<T extends string = string>({
|
|
|
200
211
|
noResultsText,
|
|
201
212
|
...selectorProps
|
|
202
213
|
}: TransferListSelectorProps<T>): ReactNode {
|
|
214
|
+
const t = useTranslator();
|
|
215
|
+
const applyLabel =
|
|
216
|
+
applyLabelFromProps ?? t('@astryx.transferListSelector.apply');
|
|
217
|
+
const cancelLabel =
|
|
218
|
+
cancelLabelFromProps ?? t('@astryx.transferListSelector.cancel');
|
|
203
219
|
const [draftValue, setDraftValue] = useState<readonly T[]>(value);
|
|
204
220
|
const previousAppliedValueRef = useRef<readonly T[]>([...value]);
|
|
205
221
|
const previousCommitBehaviorRef = useRef(commitBehavior);
|
|
@@ -236,7 +252,10 @@ export function TransferListSelector<T extends string = string>({
|
|
|
236
252
|
value={value}
|
|
237
253
|
onChange={onChange}
|
|
238
254
|
changeAction={changeAction}
|
|
239
|
-
triggerLabel={
|
|
255
|
+
triggerLabel={
|
|
256
|
+
triggerLabel ??
|
|
257
|
+
t('@astryx.transferListSelector.triggerLabel', {count: value.length})
|
|
258
|
+
}
|
|
240
259
|
isDisabled={isDisabled}
|
|
241
260
|
isLoading={isLoading}
|
|
242
261
|
width={resolvedWidth}
|
|
@@ -256,14 +275,23 @@ export function TransferListSelector<T extends string = string>({
|
|
|
256
275
|
/>
|
|
257
276
|
)}
|
|
258
277
|
<fieldset
|
|
259
|
-
disabled={isDisabled
|
|
260
|
-
{...stylex.props(
|
|
278
|
+
disabled={isDisabled}
|
|
279
|
+
{...stylex.props(
|
|
280
|
+
styles.fieldset,
|
|
281
|
+
state.isBusy && styles.fieldsetBusy,
|
|
282
|
+
)}>
|
|
261
283
|
<TransferList
|
|
262
284
|
label={label}
|
|
263
285
|
isLabelHidden
|
|
264
286
|
options={options}
|
|
265
287
|
value={transferListValue}
|
|
266
288
|
onChange={nextValue => {
|
|
289
|
+
// Now that busy no longer disables the fieldset, the controls
|
|
290
|
+
// stay live while a change is in flight; ignore transfers
|
|
291
|
+
// until it settles so one pending action cannot be stacked.
|
|
292
|
+
if (state.isBusy) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
267
295
|
if (isStaged) {
|
|
268
296
|
setDraftValue(nextValue);
|
|
269
297
|
} else {
|