@@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
2121import React , {
2222 cloneElement ,
2323 forwardRef ,
24+ useCallback ,
2425 useContext ,
2526 useEffect ,
2627 useLayoutEffect ,
@@ -46,6 +47,7 @@ import ListBox, {
4647 type ListBoxSize ,
4748 type ListBoxType ,
4849} from '../ListBox' ;
50+ import Checkbox from '../Checkbox' ;
4951import { ListBoxTrigger , ListBoxSelection } from '../ListBox/next' ;
5052import { match , keys } from '../../internal/keyboard' ;
5153import { defaultItemToString } from './tools/itemToString' ;
@@ -389,17 +391,73 @@ export const FilterableMultiSelect = forwardRef(function FilterableMultiSelect<
389391 ) ;
390392 const [ inputFocused , setInputFocused ] = useState < boolean > ( false ) ;
391393
394+ const filteredItems = useMemo (
395+ ( ) => filterItems ( items , { itemToString, inputValue } ) ,
396+ [ items , inputValue , itemToString , filterItems ]
397+ ) ;
398+
399+ const nonSelectAllItems = useMemo (
400+ ( ) => filteredItems . filter ( ( item ) => ! ( item as any ) . isSelectAll ) ,
401+ [ filteredItems ]
402+ ) ;
403+ let selectAll = filteredItems . some ( ( item ) => ( item as any ) . isSelectAll ) ;
404+ if ( ( selected ?? [ ] ) . length > 0 && selectAll ) {
405+ console . warn (
406+ 'Warning: `selectAll` should not be used when `selectedItems` is provided. Please pass either `selectAll` or `selectedItems`, not both.'
407+ ) ;
408+ selectAll = false ;
409+ }
410+
392411 const {
393412 selectedItems : controlledSelectedItems ,
394413 onItemChange,
395414 clearSelection,
415+ toggleAll,
396416 } = useSelection ( {
397417 disabled,
398418 initialSelectedItems,
399419 onChange,
400420 selectedItems : selected ,
421+ selectAll,
422+ filteredItems,
401423 } ) ;
402424
425+ const selectAllStatus = useMemo ( ( ) => {
426+ const selectable = nonSelectAllItems . filter (
427+ ( item ) => ! ( item as any ) . disabled
428+ ) ;
429+
430+ const nonSelectedCount = selectable . filter (
431+ ( item ) => ! controlledSelectedItems . some ( ( sel ) => isEqual ( sel , item ) )
432+ ) . length ;
433+
434+ const totalCount = selectable . length ;
435+ return {
436+ checked : totalCount > 0 && nonSelectedCount === 0 ,
437+ indeterminate : nonSelectedCount > 0 && nonSelectedCount < totalCount ,
438+ } ;
439+ } , [ controlledSelectedItems , nonSelectAllItems ] ) ;
440+
441+ const handleSelectAllClick = useCallback ( ( ) => {
442+ const selectable = nonSelectAllItems . filter ( ( i ) => ! ( i as any ) . disabled ) ;
443+ const { checked, indeterminate } = selectAllStatus ;
444+
445+ // clear all options if select-all state is checked or indeterminate
446+ if ( checked || indeterminate ) {
447+ const remainingSelectedItems = controlledSelectedItems . filter (
448+ ( sel ) => ! filteredItems . some ( ( e ) => isEqual ( e , sel ) )
449+ ) ;
450+ toggleAll ( remainingSelectedItems ) ;
451+
452+ // select all options if select-all state is empty
453+ } else {
454+ const toSelect = selectable . filter (
455+ ( e ) => ! controlledSelectedItems . some ( ( sel ) => isEqual ( sel , e ) )
456+ ) ;
457+ toggleAll ( [ ...controlledSelectedItems , ...toSelect ] ) ;
458+ }
459+ } , [ nonSelectAllItems , selectAllStatus , controlledSelectedItems , toggleAll ] ) ;
460+
403461 const { refs, floatingStyles, middlewareData } = useFloating (
404462 autoAlign
405463 ? {
@@ -455,7 +513,14 @@ export const FilterableMultiSelect = forwardRef(function FilterableMultiSelect<
455513 // memoize sorted items to reduce unnecessary expensive sort on rerender
456514 const sortedItems = useMemo ( ( ) => {
457515 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
458- return sortItems ! ( filterItems ( items , { itemToString, inputValue } ) , {
516+ const selectAllItem = items . find ( ( item ) => ( item as any ) . isSelectAll ) ;
517+
518+ const selectableRealItems = nonSelectAllItems . filter (
519+ ( item ) => ! ( item as any ) . disabled
520+ ) ;
521+
522+ // Sort only non-select-all items, select-all item must stay at the top
523+ const sortedReal = sortItems ! ( nonSelectAllItems , {
459524 selectedItems : {
460525 top : controlledSelectedItems ,
461526 fixed : [ ] ,
@@ -465,6 +530,12 @@ export const FilterableMultiSelect = forwardRef(function FilterableMultiSelect<
465530 compareItems,
466531 locale,
467532 } ) ;
533+
534+ // Only show select-all-item if there exist non-disabled filtered items to select
535+ if ( selectAllItem && selectableRealItems . length > 0 ) {
536+ return [ selectAllItem , ...sortedReal ] ;
537+ }
538+ return sortedReal ;
468539 } , [
469540 items ,
470541 inputValue ,
@@ -474,6 +545,8 @@ export const FilterableMultiSelect = forwardRef(function FilterableMultiSelect<
474545 itemToString ,
475546 compareItems ,
476547 locale ,
548+ sortItems ,
549+ nonSelectAllItems ,
477550 ] ) ;
478551
479552 const inline = type === 'inline' ;
@@ -614,14 +687,23 @@ export const FilterableMultiSelect = forwardRef(function FilterableMultiSelect<
614687 }
615688 switch ( type ) {
616689 case InputKeyDownEnter :
690+ if ( sortedItems . length === 0 ) {
691+ return changes ;
692+ }
617693 if ( changes . selectedItem && changes . selectedItem . disabled !== true ) {
618- onItemChange ( changes . selectedItem ) ;
694+ if ( changes . selectedItem . isSelectAll ) {
695+ handleSelectAllClick ( ) ;
696+ } else {
697+ onItemChange ( changes . selectedItem ) ;
698+ }
619699 }
620700 setHighlightedIndex ( changes . selectedItem ) ;
621701
622702 return { ...changes , highlightedIndex : state . highlightedIndex } ;
623703 case ItemClick :
624- if ( changes . selectedItem ) {
704+ if ( changes . selectedItem . isSelectAll ) {
705+ handleSelectAllClick ( ) ;
706+ } else {
625707 onItemChange ( changes . selectedItem ) ;
626708 }
627709 setHighlightedIndex ( changes . selectedItem ) ;
@@ -752,6 +834,11 @@ export const FilterableMultiSelect = forwardRef(function FilterableMultiSelect<
752834 ? cloneElement ( candidate , { size : 'mini' } )
753835 : null ;
754836
837+ // exclude the select-all item from the count
838+ const selectedItemsLength = controlledSelectedItems . filter (
839+ ( item : any ) => ! ( item as any ) . isSelectAll
840+ ) . length ;
841+
755842 const className = cx (
756843 `${ prefix } --multi-select` ,
757844 `${ prefix } --combo-box` ,
@@ -765,6 +852,7 @@ export const FilterableMultiSelect = forwardRef(function FilterableMultiSelect<
765852 controlledSelectedItems ?. length > 0 ,
766853 [ `${ prefix } --multi-select--filterable--input-focused` ] : inputFocused ,
767854 [ `${ prefix } --multi-select--readonly` ] : readOnly ,
855+ [ `${ prefix } --multi-select--selectall` ] : selectAll ,
768856 }
769857 ) ;
770858
@@ -938,7 +1026,7 @@ export const FilterableMultiSelect = forwardRef(function FilterableMultiSelect<
9381026 textInput . current . focus ( ) ;
9391027 }
9401028 } }
941- selectionCount = { controlledSelectedItems . length }
1029+ selectionCount = { selectedItemsLength }
9421030 translateWithId = { translateWithId }
9431031 disabled = { disabled }
9441032 />
@@ -993,10 +1081,17 @@ export const FilterableMultiSelect = forwardRef(function FilterableMultiSelect<
9931081 < ListBox . Menu { ...menuProps } >
9941082 { isOpen
9951083 ? sortedItems . map ( ( item , index ) => {
996- const isChecked =
997- controlledSelectedItems . filter ( ( selected ) =>
998- isEqual ( selected , item )
999- ) . length > 0 ;
1084+ let isChecked : boolean ;
1085+ let isIndeterminate = false ;
1086+ if ( ( item as any ) . isSelectAll ) {
1087+ isChecked = selectAllStatus . checked ;
1088+ isIndeterminate = selectAllStatus . indeterminate ;
1089+ } else {
1090+ isChecked =
1091+ controlledSelectedItems . filter ( ( selected ) =>
1092+ isEqual ( selected , item )
1093+ ) . length > 0 ;
1094+ }
10001095 const itemProps = getItemProps ( {
10011096 item,
10021097 [ 'aria-selected' ] : isChecked ,
@@ -1017,23 +1112,27 @@ export const FilterableMultiSelect = forwardRef(function FilterableMultiSelect<
10171112 < ListBox . MenuItem
10181113 key = { itemProps . id }
10191114 aria-label = { itemText }
1020- isActive = { isChecked }
1115+ isActive = { isChecked && ! item [ 'isSelectAll' ] }
10211116 isHighlighted = { highlightedIndex === index }
10221117 title = { itemText }
10231118 disabled = { disabled }
10241119 { ...modifiedItemProps } >
10251120 < div className = { `${ prefix } --checkbox-wrapper` } >
1026- < span
1121+ < Checkbox
1122+ id = { `${ itemProps . id } -item` }
1123+ labelText = {
1124+ ItemToElement ? (
1125+ < ItemToElement key = { itemProps . id } { ...item } />
1126+ ) : (
1127+ itemText
1128+ )
1129+ }
1130+ checked = { isChecked }
10271131 title = { useTitleInItem ? itemText : undefined }
1028- className = { `${ prefix } --checkbox-label` }
1029- data-contained-checkbox-state = { isChecked }
1030- id = { `${ itemProps . id } -item` } >
1031- { ItemToElement ? (
1032- < ItemToElement key = { itemProps . id } { ...item } />
1033- ) : (
1034- itemText
1035- ) }
1036- </ span >
1132+ indeterminate = { isIndeterminate }
1133+ disabled = { disabled }
1134+ tabIndex = { - 1 }
1135+ />
10371136 </ div >
10381137 </ ListBox . MenuItem >
10391138 ) ;
0 commit comments