@wallarm-org/design-system 1.28.2-rc-fix-table-window-scroll-root.1 → 1.29.0
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/components/FilterDropdown/FilterDropdown.d.ts +58 -0
- package/dist/components/FilterDropdown/FilterDropdown.js +168 -0
- package/dist/components/FilterDropdown/FilterDropdownAllOption.d.ts +14 -0
- package/dist/components/FilterDropdown/FilterDropdownAllOption.js +25 -0
- package/dist/components/FilterDropdown/FilterDropdownClear.d.ts +12 -0
- package/dist/components/FilterDropdown/FilterDropdownClear.js +33 -0
- package/dist/components/FilterDropdown/FilterDropdownContent.d.ts +13 -0
- package/dist/components/FilterDropdown/FilterDropdownContent.js +57 -0
- package/dist/components/FilterDropdown/FilterDropdownContext.d.ts +60 -0
- package/dist/components/FilterDropdown/FilterDropdownContext.js +21 -0
- package/dist/components/FilterDropdown/FilterDropdownEmpty.d.ts +9 -0
- package/dist/components/FilterDropdown/FilterDropdownEmpty.js +21 -0
- package/dist/components/FilterDropdown/FilterDropdownFooter.d.ts +9 -0
- package/dist/components/FilterDropdown/FilterDropdownFooter.js +19 -0
- package/dist/components/FilterDropdown/FilterDropdownFooterClear.d.ts +9 -0
- package/dist/components/FilterDropdown/FilterDropdownFooterClear.js +40 -0
- package/dist/components/FilterDropdown/FilterDropdownGroup.d.ts +5 -0
- package/dist/components/FilterDropdown/FilterDropdownGroup.js +8 -0
- package/dist/components/FilterDropdown/FilterDropdownGroupLabel.d.ts +4 -0
- package/dist/components/FilterDropdown/FilterDropdownGroupLabel.js +8 -0
- package/dist/components/FilterDropdown/FilterDropdownOption.d.ts +15 -0
- package/dist/components/FilterDropdown/FilterDropdownOption.js +32 -0
- package/dist/components/FilterDropdown/FilterDropdownSearch.d.ts +12 -0
- package/dist/components/FilterDropdown/FilterDropdownSearch.js +38 -0
- package/dist/components/FilterDropdown/FilterDropdownSelected.d.ts +16 -0
- package/dist/components/FilterDropdown/FilterDropdownSelected.js +38 -0
- package/dist/components/FilterDropdown/FilterDropdownTrigger.d.ts +16 -0
- package/dist/components/FilterDropdown/FilterDropdownTrigger.js +139 -0
- package/dist/components/FilterDropdown/classes.d.ts +16 -0
- package/dist/components/FilterDropdown/classes.js +43 -0
- package/dist/components/FilterDropdown/index.d.ts +15 -0
- package/dist/components/FilterDropdown/index.js +15 -0
- package/dist/components/FilterDropdown/lib/index.d.ts +1 -0
- package/dist/components/FilterDropdown/lib/index.js +1 -0
- package/dist/components/FilterDropdown/lib/values.d.ts +59 -0
- package/dist/components/FilterDropdown/lib/values.js +58 -0
- package/dist/components/Select/Select.d.ts +1 -1
- package/dist/components/Select/SelectContent.d.ts +2 -1
- package/dist/components/Select/SelectContent.js +3 -2
- package/dist/components/Select/SelectEmptyState.d.ts +1 -0
- package/dist/components/Select/SelectEmptyState.js +2 -1
- package/dist/components/Select/SelectFooter.d.ts +7 -2
- package/dist/components/Select/SelectFooter.js +6 -3
- package/dist/components/Select/SelectGroup.d.ts +8 -2
- package/dist/components/Select/SelectGroup.js +3 -2
- package/dist/components/Select/SelectGroupLabel.d.ts +5 -2
- package/dist/components/Select/SelectGroupLabel.js +2 -2
- package/dist/components/Select/SelectOption.js +1 -1
- package/dist/components/Select/SelectOptionHint.d.ts +11 -0
- package/dist/components/Select/SelectOptionHint.js +15 -0
- package/dist/components/Select/SelectPositioner.d.ts +13 -3
- package/dist/components/Select/SelectPositioner.js +2 -1
- package/dist/components/Select/SelectSearchInput.d.ts +5 -2
- package/dist/components/Select/SelectSearchInput.js +11 -1
- package/dist/components/Select/classes.d.ts +3 -0
- package/dist/components/Select/classes.js +13 -0
- package/dist/components/Select/index.d.ts +4 -2
- package/dist/components/Select/index.js +2 -0
- package/dist/components/Table/TableBody/TableBodyVirtualizedCore.js +1 -1
- package/dist/components/Table/TableBody/TableBodyVirtualizedWindow.js +9 -25
- package/dist/components/Table/TableInner/TableInnerWindow.js +0 -1
- package/dist/components/Table/hooks/infiniteScroll/useInfiniteScroll.d.ts +1 -1
- package/dist/components/Table/hooks/infiniteScroll/usePrependScrollAnchor.d.ts +0 -1
- package/dist/components/Table/hooks/infiniteScroll/usePrependScrollAnchor.js +10 -12
- package/dist/components/Table/hooks/infiniteScroll/useScrollEdge.d.ts +1 -4
- package/dist/components/Table/hooks/infiniteScroll/useScrollEdge.js +17 -4
- package/dist/components/Table/lib/index.d.ts +0 -1
- package/dist/components/Table/lib/index.js +0 -1
- package/dist/components/Table/types.d.ts +1 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/metadata/components.json +6160 -983
- package/package.json +1 -1
- package/dist/components/Table/lib/scrollRoot.d.ts +0 -18
- package/dist/components/Table/lib/scrollRoot.js +0 -22
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type CollectionItem, type ListCollection } from '@ark-ui/react/collection';
|
|
3
|
+
import type { TestableProps } from '../../utils/testId';
|
|
4
|
+
import { type SelectProps } from '../Select/Select';
|
|
5
|
+
import { type UseSelectSearchOptions } from '../Select/useSelectSearch';
|
|
6
|
+
export interface FilterDropdownValueChangeDetails<T extends CollectionItem = CollectionItem> {
|
|
7
|
+
/** Picked values; `[]` means unset («All» in single mode). */
|
|
8
|
+
value: string[];
|
|
9
|
+
items: T[];
|
|
10
|
+
}
|
|
11
|
+
export interface FilterDropdownOpenChangeDetails {
|
|
12
|
+
open: boolean;
|
|
13
|
+
value: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Select props FilterDropdown owns itself: the value model (alias and sentinel values must never
|
|
17
|
+
* reach the consumer), the highlight (reset when the query changes), and form wiring (the hidden
|
|
18
|
+
* select would submit internal values).
|
|
19
|
+
*/
|
|
20
|
+
type FilterDropdownOmittedSelectProps = 'collection' | 'multiple' | 'value' | 'defaultValue' | 'onValueChange' | 'onOpenChange' | 'highlightedValue' | 'defaultHighlightedValue' | 'onHighlightChange' | 'onSelect' | 'deselectable' | 'name' | 'form' | 'loading' | 'children';
|
|
21
|
+
export type FilterDropdownProps<T extends CollectionItem = CollectionItem> = Omit<SelectProps<T>, FilterDropdownOmittedSelectProps> & TestableProps & {
|
|
22
|
+
/** Attribute name: trigger text in multi mode, the trigger's aria-label, «Clear {label}». */
|
|
23
|
+
label: string;
|
|
24
|
+
/**
|
|
25
|
+
* Single mode: trigger text while unset and the default text of `FilterDropdownAllOption`.
|
|
26
|
+
* @default 'All'
|
|
27
|
+
*/
|
|
28
|
+
allLabel?: string;
|
|
29
|
+
/** Items to pick from. `groupBy` is supported. */
|
|
30
|
+
collection: ListCollection<T>;
|
|
31
|
+
/** Checkbox mode: several values, OR-ed, the menu stays open. */
|
|
32
|
+
multiple?: boolean;
|
|
33
|
+
/** Controlled picked values; `[]` = unset. */
|
|
34
|
+
value?: string[];
|
|
35
|
+
defaultValue?: string[];
|
|
36
|
+
onValueChange?: (details: FilterDropdownValueChangeDetails<T>) => void;
|
|
37
|
+
onOpenChange?: (details: FilterDropdownOpenChangeDetails) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Item count from which the search field and (multi) the "Selected" section appear. Counts
|
|
40
|
+
* the collection's own items only.
|
|
41
|
+
* @default 8
|
|
42
|
+
*/
|
|
43
|
+
searchThreshold?: number;
|
|
44
|
+
/** Overrides the default case-insensitive match on the item label. */
|
|
45
|
+
filterFn?: UseSelectSearchOptions<T>['filterFn'];
|
|
46
|
+
/**
|
|
47
|
+
* Multi mode: pin the picked values under «Selected» on top of the list (from
|
|
48
|
+
* `searchThreshold` items, hidden while searching). Render `FilterDropdownSelected` for it.
|
|
49
|
+
* @default true
|
|
50
|
+
*/
|
|
51
|
+
selectedOnTop?: boolean;
|
|
52
|
+
children?: ReactNode;
|
|
53
|
+
};
|
|
54
|
+
export declare function FilterDropdown<T extends CollectionItem>({ label, allLabel, collection, multiple, value: valueProp, defaultValue, onValueChange, open: openProp, defaultOpen, onOpenChange, searchThreshold, filterFn, selectedOnTop, disabled, className, children, 'data-testid': testId, ...props }: FilterDropdownProps<T>): import("react").JSX.Element;
|
|
55
|
+
export declare namespace FilterDropdown {
|
|
56
|
+
var displayName: string;
|
|
57
|
+
}
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useRef, useState } from "react";
|
|
3
|
+
import { createListCollection } from "@ark-ui/react/collection";
|
|
4
|
+
import { cn } from "../../utils/cn.js";
|
|
5
|
+
import { Select } from "../Select/Select.js";
|
|
6
|
+
import { useSelectSearch } from "../Select/useSelectSearch.js";
|
|
7
|
+
import { FilterDropdownContext } from "./FilterDropdownContext.js";
|
|
8
|
+
import { createAliasItem, createAllItem, isSpecialItem, toConsumerValue, toInternalValue } from "./lib/index.js";
|
|
9
|
+
const DEFAULT_SEARCH_THRESHOLD = 8;
|
|
10
|
+
const EMPTY = [];
|
|
11
|
+
const FilterDropdown = ({ label, allLabel = 'All', collection, multiple = false, value: valueProp, defaultValue, onValueChange, open: openProp, defaultOpen, onOpenChange, searchThreshold = DEFAULT_SEARCH_THRESHOLD, filterFn, selectedOnTop = true, disabled = false, className, children, 'data-testid': testId, ...props })=>{
|
|
12
|
+
const [valueState, setValueState] = useState(defaultValue ?? EMPTY);
|
|
13
|
+
const value = valueProp ?? valueState;
|
|
14
|
+
const [openState, setOpenState] = useState(defaultOpen ?? false);
|
|
15
|
+
const open = openProp ?? openState;
|
|
16
|
+
const baseCollection = collection;
|
|
17
|
+
const searchOptions = useMemo(()=>({
|
|
18
|
+
filterFn: filterFn
|
|
19
|
+
}), [
|
|
20
|
+
filterFn
|
|
21
|
+
]);
|
|
22
|
+
const { searchValue: query, onSearchChange, filteredCollection } = useSelectSearch(baseCollection, searchOptions);
|
|
23
|
+
const [snapshot, setSnapshot] = useState(value);
|
|
24
|
+
const [lockedWidth, setLockedWidth] = useState();
|
|
25
|
+
const [highlightState, setHighlightState] = useState(null);
|
|
26
|
+
const [resetHighlight, setResetHighlight] = useState(false);
|
|
27
|
+
const [prevOpen, setPrevOpen] = useState(open);
|
|
28
|
+
if (open !== prevOpen) {
|
|
29
|
+
setPrevOpen(open);
|
|
30
|
+
onSearchChange('');
|
|
31
|
+
setLockedWidth(void 0);
|
|
32
|
+
setResetHighlight(false);
|
|
33
|
+
if (open) setSnapshot(value);
|
|
34
|
+
}
|
|
35
|
+
const selectedValues = open ? snapshot : value;
|
|
36
|
+
const triggerRef = useRef(null);
|
|
37
|
+
const contentRef = useRef(null);
|
|
38
|
+
const isSearchable = baseCollection.size >= searchThreshold;
|
|
39
|
+
const isSearchActive = isSearchable && query.length > 0;
|
|
40
|
+
const showAll = !multiple && !isSearchActive;
|
|
41
|
+
const showSelected = selectedOnTop && multiple && isSearchable && !isSearchActive && selectedValues.length > 0;
|
|
42
|
+
const allItem = useMemo(()=>createAllItem(allLabel), [
|
|
43
|
+
allLabel
|
|
44
|
+
]);
|
|
45
|
+
const groups = useMemo(()=>filteredCollection.group(), [
|
|
46
|
+
filteredCollection
|
|
47
|
+
]);
|
|
48
|
+
const aliasItems = useMemo(()=>showSelected ? baseCollection.findMany(selectedValues).map((item)=>createAliasItem(item, baseCollection)) : [], [
|
|
49
|
+
showSelected,
|
|
50
|
+
baseCollection,
|
|
51
|
+
selectedValues
|
|
52
|
+
]);
|
|
53
|
+
const arkCollection = useMemo(()=>createListCollection({
|
|
54
|
+
items: [
|
|
55
|
+
...showAll ? [
|
|
56
|
+
allItem
|
|
57
|
+
] : [],
|
|
58
|
+
...aliasItems,
|
|
59
|
+
...groups.flatMap(([, items])=>items)
|
|
60
|
+
],
|
|
61
|
+
itemToValue: (item)=>isSpecialItem(item) ? item.value : baseCollection.getItemValue(item) ?? '',
|
|
62
|
+
itemToString: (item)=>isSpecialItem(item) ? item.label : baseCollection.stringifyItem(item) ?? '',
|
|
63
|
+
isItemDisabled: (item)=>{
|
|
64
|
+
if (!isSpecialItem(item)) return baseCollection.getItemDisabled(item);
|
|
65
|
+
return item.source ? baseCollection.getItemDisabled(item.source) : false;
|
|
66
|
+
}
|
|
67
|
+
}), [
|
|
68
|
+
showAll,
|
|
69
|
+
allItem,
|
|
70
|
+
aliasItems,
|
|
71
|
+
groups,
|
|
72
|
+
baseCollection
|
|
73
|
+
]);
|
|
74
|
+
const aliased = useMemo(()=>new Set(aliasItems.map((item)=>baseCollection.getItemValue(item.source ?? null) ?? '')), [
|
|
75
|
+
aliasItems,
|
|
76
|
+
baseCollection
|
|
77
|
+
]);
|
|
78
|
+
const internalValue = useMemo(()=>toInternalValue(value, {
|
|
79
|
+
multiple,
|
|
80
|
+
aliased,
|
|
81
|
+
hasAll: showAll
|
|
82
|
+
}), [
|
|
83
|
+
value,
|
|
84
|
+
multiple,
|
|
85
|
+
aliased,
|
|
86
|
+
showAll
|
|
87
|
+
]);
|
|
88
|
+
const highlightedValue = resetHighlight ? arkCollection.firstValue : highlightState;
|
|
89
|
+
const commit = (next)=>{
|
|
90
|
+
if (void 0 === valueProp) setValueState(next);
|
|
91
|
+
onValueChange?.({
|
|
92
|
+
value: next,
|
|
93
|
+
items: collection.findMany(next)
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
const clear = ()=>{
|
|
97
|
+
if (disabled || 0 === value.length) return;
|
|
98
|
+
commit([]);
|
|
99
|
+
};
|
|
100
|
+
const handleValueChange = (details)=>{
|
|
101
|
+
commit(toConsumerValue(details.value, {
|
|
102
|
+
multiple,
|
|
103
|
+
previousInternal: internalValue,
|
|
104
|
+
previousValue: value
|
|
105
|
+
}));
|
|
106
|
+
};
|
|
107
|
+
const handleOpenChange = ({ open: nextOpen })=>{
|
|
108
|
+
setOpenState(nextOpen);
|
|
109
|
+
onOpenChange?.({
|
|
110
|
+
open: nextOpen,
|
|
111
|
+
value
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
const handleHighlightChange = (details)=>{
|
|
115
|
+
setHighlightState(details.highlightedValue);
|
|
116
|
+
setResetHighlight(false);
|
|
117
|
+
};
|
|
118
|
+
const setQuery = (next)=>{
|
|
119
|
+
if (!query && next) setLockedWidth(contentRef.current?.offsetWidth || void 0);
|
|
120
|
+
if (query && !next) setSnapshot(value);
|
|
121
|
+
onSearchChange(next);
|
|
122
|
+
setResetHighlight(true);
|
|
123
|
+
};
|
|
124
|
+
const context = {
|
|
125
|
+
label,
|
|
126
|
+
allLabel,
|
|
127
|
+
multiple,
|
|
128
|
+
disabled,
|
|
129
|
+
value,
|
|
130
|
+
collection: baseCollection,
|
|
131
|
+
filteredCollection,
|
|
132
|
+
groups,
|
|
133
|
+
query,
|
|
134
|
+
setQuery,
|
|
135
|
+
isSearchable,
|
|
136
|
+
isSearchActive,
|
|
137
|
+
showAll,
|
|
138
|
+
allItem,
|
|
139
|
+
showSelected,
|
|
140
|
+
aliasItems,
|
|
141
|
+
clear,
|
|
142
|
+
triggerRef,
|
|
143
|
+
contentRef,
|
|
144
|
+
lockedWidth
|
|
145
|
+
};
|
|
146
|
+
return /*#__PURE__*/ jsx(FilterDropdownContext.Provider, {
|
|
147
|
+
value: context,
|
|
148
|
+
children: /*#__PURE__*/ jsx(Select, {
|
|
149
|
+
...props,
|
|
150
|
+
"data-slot": "filter-dropdown",
|
|
151
|
+
"data-testid": testId,
|
|
152
|
+
className: cn('inline-flex max-w-full', className),
|
|
153
|
+
collection: arkCollection,
|
|
154
|
+
multiple: multiple,
|
|
155
|
+
disabled: disabled,
|
|
156
|
+
value: internalValue,
|
|
157
|
+
onValueChange: handleValueChange,
|
|
158
|
+
open: openProp,
|
|
159
|
+
defaultOpen: defaultOpen,
|
|
160
|
+
onOpenChange: handleOpenChange,
|
|
161
|
+
highlightedValue: highlightedValue,
|
|
162
|
+
onHighlightChange: handleHighlightChange,
|
|
163
|
+
children: children
|
|
164
|
+
})
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
FilterDropdown.displayName = 'FilterDropdown';
|
|
168
|
+
export { FilterDropdown };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentProps, FC, ReactNode, Ref } from 'react';
|
|
2
|
+
import { SelectOption } from '../Select/SelectOption';
|
|
3
|
+
export interface FilterDropdownAllOptionProps extends Omit<ComponentProps<typeof SelectOption>, 'item' | 'children'> {
|
|
4
|
+
ref?: Ref<HTMLDivElement>;
|
|
5
|
+
/** @default the root's `allLabel` */
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The «All …» row of single mode: first in the list, picked while the filter is unset, and
|
|
10
|
+
* picking it resets the filter to `[]`. Hidden in multi mode and while a query is active.
|
|
11
|
+
* Always render it in single mode: the row is part of keyboard navigation whenever the menu is
|
|
12
|
+
* open in single mode.
|
|
13
|
+
*/
|
|
14
|
+
export declare const FilterDropdownAllOption: FC<FilterDropdownAllOptionProps>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useTestId } from "../../utils/testId.js";
|
|
3
|
+
import { SelectOption } from "../Select/SelectOption.js";
|
|
4
|
+
import { SelectOptionIndicator } from "../Select/SelectOptionIndicator.js";
|
|
5
|
+
import { SelectOptionText } from "../Select/SelectOptionText.js";
|
|
6
|
+
import { useFilterDropdownContext } from "./FilterDropdownContext.js";
|
|
7
|
+
const FilterDropdownAllOption = ({ children, 'data-testid': testIdProp, ...props })=>{
|
|
8
|
+
const { showAll, allItem, allLabel } = useFilterDropdownContext();
|
|
9
|
+
const testId = useTestId('all-option', testIdProp);
|
|
10
|
+
if (!showAll) return null;
|
|
11
|
+
return /*#__PURE__*/ jsxs(SelectOption, {
|
|
12
|
+
...props,
|
|
13
|
+
item: allItem,
|
|
14
|
+
"data-slot": "filter-dropdown-all-option",
|
|
15
|
+
"data-testid": testId,
|
|
16
|
+
children: [
|
|
17
|
+
/*#__PURE__*/ jsx(SelectOptionText, {
|
|
18
|
+
children: children ?? allLabel
|
|
19
|
+
}),
|
|
20
|
+
/*#__PURE__*/ jsx(SelectOptionIndicator, {})
|
|
21
|
+
]
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
FilterDropdownAllOption.displayName = 'FilterDropdownAllOption';
|
|
25
|
+
export { FilterDropdownAllOption };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, FC, Ref } from 'react';
|
|
2
|
+
import { type TestableProps } from '../../utils/testId';
|
|
3
|
+
export interface FilterDropdownClearProps extends ButtonHTMLAttributes<HTMLButtonElement>, TestableProps {
|
|
4
|
+
ref?: Ref<HTMLButtonElement>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The ✕ that clears a multi filter. `FilterDropdownTrigger` renders one on its own; pass a
|
|
8
|
+
* `FilterDropdownClear` as the trigger's child to replace it (for example to add attributes).
|
|
9
|
+
* Renders only in multi mode while something is picked, and hands focus back to the trigger
|
|
10
|
+
* before it disappears.
|
|
11
|
+
*/
|
|
12
|
+
export declare const FilterDropdownClear: FC<FilterDropdownClearProps>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { X } from "../../icons/index.js";
|
|
3
|
+
import { cn } from "../../utils/cn.js";
|
|
4
|
+
import { useTestId } from "../../utils/testId.js";
|
|
5
|
+
import { filterDropdownClearClassName } from "./classes.js";
|
|
6
|
+
import { useFilterDropdownContext } from "./FilterDropdownContext.js";
|
|
7
|
+
const FilterDropdownClear = ({ ref, className, children, disabled, onClick, 'aria-label': ariaLabel, 'data-testid': testIdProp, ...props })=>{
|
|
8
|
+
const { multiple, value, label, clear, triggerRef, disabled: rootDisabled } = useFilterDropdownContext();
|
|
9
|
+
const testId = useTestId('clear', testIdProp);
|
|
10
|
+
if (!multiple || 0 === value.length) return null;
|
|
11
|
+
const handleClick = (event)=>{
|
|
12
|
+
onClick?.(event);
|
|
13
|
+
if (event.defaultPrevented) return;
|
|
14
|
+
triggerRef.current?.focus();
|
|
15
|
+
clear();
|
|
16
|
+
};
|
|
17
|
+
return /*#__PURE__*/ jsx("button", {
|
|
18
|
+
type: "button",
|
|
19
|
+
...props,
|
|
20
|
+
ref: ref,
|
|
21
|
+
"data-slot": "filter-dropdown-clear",
|
|
22
|
+
"data-testid": testId,
|
|
23
|
+
"aria-label": ariaLabel ?? `Clear ${label}`,
|
|
24
|
+
disabled: rootDisabled || disabled,
|
|
25
|
+
onClick: handleClick,
|
|
26
|
+
className: cn(filterDropdownClearClassName, className),
|
|
27
|
+
children: children ?? /*#__PURE__*/ jsx(X, {
|
|
28
|
+
size: "md"
|
|
29
|
+
})
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
FilterDropdownClear.displayName = 'FilterDropdownClear';
|
|
33
|
+
export { FilterDropdownClear };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type FC, type ReactNode, type Ref } from 'react';
|
|
2
|
+
import type { Select as ArkUiSelect } from '@ark-ui/react/select';
|
|
3
|
+
import { type TestableProps } from '../../utils/testId';
|
|
4
|
+
export interface FilterDropdownContentProps extends Omit<ArkUiSelect.ContentProps, 'asChild' | 'children'>, TestableProps {
|
|
5
|
+
ref?: Ref<HTMLDivElement>;
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The menu panel. `FilterDropdownSearch` children are pinned above the scrolling list and
|
|
10
|
+
* `FilterDropdownFooter` children below it; everything else scrolls. Hugs its options up to
|
|
11
|
+
* 360px and at most 340px tall, and keeps its width while a query narrows the list.
|
|
12
|
+
*/
|
|
13
|
+
export declare const FilterDropdownContent: FC<FilterDropdownContentProps>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Children, isValidElement } from "react";
|
|
3
|
+
import { cn } from "../../utils/cn.js";
|
|
4
|
+
import { mergeRefs } from "../../utils/mergeRefs.js";
|
|
5
|
+
import { TestIdProvider, useTestId } from "../../utils/testId.js";
|
|
6
|
+
import { SelectContent } from "../Select/SelectContent.js";
|
|
7
|
+
import { SelectHeader } from "../Select/SelectHeader.js";
|
|
8
|
+
import { SelectPositioner } from "../Select/SelectPositioner.js";
|
|
9
|
+
import { filterDropdownContentClassName } from "./classes.js";
|
|
10
|
+
import { useFilterDropdownContext } from "./FilterDropdownContext.js";
|
|
11
|
+
import { FilterDropdownFooter } from "./FilterDropdownFooter.js";
|
|
12
|
+
import { FilterDropdownSearch } from "./FilterDropdownSearch.js";
|
|
13
|
+
const FilterDropdownContent = ({ ref, className, style, children, 'data-testid': testIdProp, ...props })=>{
|
|
14
|
+
const { isSearchable, isSearchActive, lockedWidth, contentRef } = useFilterDropdownContext();
|
|
15
|
+
const testId = useTestId('content', testIdProp);
|
|
16
|
+
const listTestId = useTestId('list');
|
|
17
|
+
const baseTestId = useTestId();
|
|
18
|
+
const header = [];
|
|
19
|
+
const footer = [];
|
|
20
|
+
const body = [];
|
|
21
|
+
Children.forEach(children, (child)=>{
|
|
22
|
+
if (/*#__PURE__*/ isValidElement(child) && child.type === FilterDropdownSearch) header.push(child);
|
|
23
|
+
else if (/*#__PURE__*/ isValidElement(child) && child.type === FilterDropdownFooter) footer.push(child);
|
|
24
|
+
else body.push(child);
|
|
25
|
+
});
|
|
26
|
+
const width = isSearchActive && lockedWidth ? lockedWidth : void 0;
|
|
27
|
+
return /*#__PURE__*/ jsxs(SelectPositioner, {
|
|
28
|
+
className: cn(filterDropdownContentClassName, className),
|
|
29
|
+
contentProps: {
|
|
30
|
+
...props,
|
|
31
|
+
ref: mergeRefs(ref, contentRef),
|
|
32
|
+
style: void 0 === width ? style : {
|
|
33
|
+
...style,
|
|
34
|
+
width
|
|
35
|
+
},
|
|
36
|
+
'data-slot': 'filter-dropdown-content',
|
|
37
|
+
'data-testid': testId
|
|
38
|
+
},
|
|
39
|
+
children: [
|
|
40
|
+
isSearchable && header.length > 0 && /*#__PURE__*/ jsx(SelectHeader, {
|
|
41
|
+
children: header
|
|
42
|
+
}),
|
|
43
|
+
/*#__PURE__*/ jsx(SelectContent, {
|
|
44
|
+
"data-testid": listTestId,
|
|
45
|
+
tabIndex: -1,
|
|
46
|
+
"data-no-autofocus": "",
|
|
47
|
+
children: /*#__PURE__*/ jsx(TestIdProvider, {
|
|
48
|
+
value: baseTestId,
|
|
49
|
+
children: body
|
|
50
|
+
})
|
|
51
|
+
}),
|
|
52
|
+
footer
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
FilterDropdownContent.displayName = 'FilterDropdownContent';
|
|
57
|
+
export { FilterDropdownContent };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import type { CollectionItem, ListCollection } from '@ark-ui/react/collection';
|
|
3
|
+
import type { FilterDropdownSpecialItem } from './lib';
|
|
4
|
+
export interface FilterDropdownContextValue {
|
|
5
|
+
/** Attribute name: trigger text in multi, aria labels, «Clear {label}». */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Single mode: trigger text while unset and the default «All» option text. */
|
|
8
|
+
allLabel: string;
|
|
9
|
+
multiple: boolean;
|
|
10
|
+
disabled: boolean;
|
|
11
|
+
/** The consumer value (base values only, `[]` = unset). */
|
|
12
|
+
value: string[];
|
|
13
|
+
/** The consumer's collection, unfiltered. */
|
|
14
|
+
collection: ListCollection<CollectionItem>;
|
|
15
|
+
/** The consumer's collection narrowed by the search query. */
|
|
16
|
+
filteredCollection: ListCollection<CollectionItem>;
|
|
17
|
+
/** `filteredCollection.group()` — groups with no matches are absent. */
|
|
18
|
+
groups: [string, CollectionItem[]][];
|
|
19
|
+
query: string;
|
|
20
|
+
setQuery: (query: string) => void;
|
|
21
|
+
/** The search field is shown: the collection has at least `searchThreshold` items. */
|
|
22
|
+
isSearchable: boolean;
|
|
23
|
+
/** A non-empty query is narrowing the list. */
|
|
24
|
+
isSearchActive: boolean;
|
|
25
|
+
/** Single mode and no query: «All» is part of the list. */
|
|
26
|
+
showAll: boolean;
|
|
27
|
+
allItem: FilterDropdownSpecialItem;
|
|
28
|
+
/**
|
|
29
|
+
* Multi mode, `selectedOnTop`, searchable, no query and something was picked when the menu
|
|
30
|
+
* opened.
|
|
31
|
+
*/
|
|
32
|
+
showSelected: boolean;
|
|
33
|
+
/** Alias rows of the "Selected" snapshot, in pick order. */
|
|
34
|
+
aliasItems: FilterDropdownSpecialItem[];
|
|
35
|
+
clear: () => void;
|
|
36
|
+
triggerRef: RefObject<HTMLButtonElement | null>;
|
|
37
|
+
contentRef: RefObject<HTMLDivElement | null>;
|
|
38
|
+
/** Width the menu is pinned to while a query is active, so it does not jump as rows drop. */
|
|
39
|
+
lockedWidth: number | undefined;
|
|
40
|
+
}
|
|
41
|
+
export declare const FilterDropdownContext: import("react").Context<FilterDropdownContextValue | null>;
|
|
42
|
+
export declare const useFilterDropdownContext: () => FilterDropdownContextValue;
|
|
43
|
+
/** Set by `FilterDropdownSelected` around each of its rows: the alias that row stands for. */
|
|
44
|
+
export declare const FilterDropdownAliasContext: import("react").Context<FilterDropdownSpecialItem<any> | null>;
|
|
45
|
+
export interface UseFilterDropdownResult<T extends CollectionItem = CollectionItem> {
|
|
46
|
+
/** The collection narrowed by the current query (the whole collection when it is empty). */
|
|
47
|
+
filteredCollection: ListCollection<T>;
|
|
48
|
+
/** `filteredCollection.group()`; render these so the list follows the search. */
|
|
49
|
+
groups: [string, T[]][];
|
|
50
|
+
query: string;
|
|
51
|
+
isSearchActive: boolean;
|
|
52
|
+
multiple: boolean;
|
|
53
|
+
value: string[];
|
|
54
|
+
clear: () => void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Reads the state of the surrounding `FilterDropdown` — chiefly the filtered items and groups
|
|
58
|
+
* to render inside `FilterDropdownContent`.
|
|
59
|
+
*/
|
|
60
|
+
export declare const useFilterDropdown: <T extends CollectionItem = CollectionItem>() => UseFilterDropdownResult<T>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
const FilterDropdownContext = /*#__PURE__*/ createContext(null);
|
|
3
|
+
const useFilterDropdownContext = ()=>{
|
|
4
|
+
const context = useContext(FilterDropdownContext);
|
|
5
|
+
if (!context) throw new Error('FilterDropdown parts must be rendered inside <FilterDropdown>');
|
|
6
|
+
return context;
|
|
7
|
+
};
|
|
8
|
+
const FilterDropdownAliasContext = /*#__PURE__*/ createContext(null);
|
|
9
|
+
const useFilterDropdown = ()=>{
|
|
10
|
+
const { filteredCollection, groups, query, isSearchActive, multiple, value, clear } = useFilterDropdownContext();
|
|
11
|
+
return {
|
|
12
|
+
filteredCollection: filteredCollection,
|
|
13
|
+
groups: groups,
|
|
14
|
+
query,
|
|
15
|
+
isSearchActive,
|
|
16
|
+
multiple,
|
|
17
|
+
value,
|
|
18
|
+
clear
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export { FilterDropdownAliasContext, FilterDropdownContext, useFilterDropdown, useFilterDropdownContext };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FC, HTMLAttributes, ReactNode, Ref } from 'react';
|
|
2
|
+
import { type TestableProps } from '../../utils/testId';
|
|
3
|
+
export interface FilterDropdownEmptyProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'>, TestableProps {
|
|
4
|
+
ref?: Ref<HTMLDivElement>;
|
|
5
|
+
/** @default 'Nothing matches' */
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
/** Shown in place of the options when the query matches none of them. */
|
|
9
|
+
export declare const FilterDropdownEmpty: FC<FilterDropdownEmptyProps>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useTestId } from "../../utils/testId.js";
|
|
3
|
+
import { SelectEmptyState } from "../Select/SelectEmptyState.js";
|
|
4
|
+
import { useFilterDropdownContext } from "./FilterDropdownContext.js";
|
|
5
|
+
const FilterDropdownEmpty = ({ ref, children, 'data-testid': testIdProp, ...props })=>{
|
|
6
|
+
const { filteredCollection } = useFilterDropdownContext();
|
|
7
|
+
const testId = useTestId('empty', testIdProp);
|
|
8
|
+
if (filteredCollection.size > 0) return null;
|
|
9
|
+
return /*#__PURE__*/ jsx("div", {
|
|
10
|
+
...props,
|
|
11
|
+
ref: ref,
|
|
12
|
+
"data-slot": "filter-dropdown-empty",
|
|
13
|
+
"data-testid": testId,
|
|
14
|
+
children: /*#__PURE__*/ jsx(SelectEmptyState, {
|
|
15
|
+
className: "w-full max-w-240",
|
|
16
|
+
description: children ?? 'Nothing matches'
|
|
17
|
+
})
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
FilterDropdownEmpty.displayName = 'FilterDropdownEmpty';
|
|
21
|
+
export { FilterDropdownEmpty };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FC } from 'react';
|
|
2
|
+
import { type TestableProps } from '../../utils/testId';
|
|
3
|
+
import { type SelectFooterProps } from '../Select/SelectFooter';
|
|
4
|
+
export type FilterDropdownFooterProps = Omit<SelectFooterProps, 'variant' | 'data-testid'> & TestableProps;
|
|
5
|
+
/**
|
|
6
|
+
* Footer pinned under the list. Multi mode only, and only while something is picked; its default
|
|
7
|
+
* content is `FilterDropdownFooterClear`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const FilterDropdownFooter: FC<FilterDropdownFooterProps>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useTestId } from "../../utils/testId.js";
|
|
3
|
+
import { SelectFooter } from "../Select/SelectFooter.js";
|
|
4
|
+
import { useFilterDropdownContext } from "./FilterDropdownContext.js";
|
|
5
|
+
import { FilterDropdownFooterClear } from "./FilterDropdownFooterClear.js";
|
|
6
|
+
const FilterDropdownFooter = ({ children, 'data-testid': testIdProp, ...props })=>{
|
|
7
|
+
const { multiple, value } = useFilterDropdownContext();
|
|
8
|
+
const testId = useTestId('footer', testIdProp);
|
|
9
|
+
if (!multiple || 0 === value.length) return null;
|
|
10
|
+
return /*#__PURE__*/ jsx(SelectFooter, {
|
|
11
|
+
...props,
|
|
12
|
+
variant: "actions",
|
|
13
|
+
"data-slot": "filter-dropdown-footer",
|
|
14
|
+
"data-testid": testId,
|
|
15
|
+
children: children ?? /*#__PURE__*/ jsx(FilterDropdownFooterClear, {})
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
FilterDropdownFooter.displayName = 'FilterDropdownFooter';
|
|
19
|
+
export { FilterDropdownFooter };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FC } from 'react';
|
|
2
|
+
import { type TestableProps } from '../../utils/testId';
|
|
3
|
+
import { type ButtonProps } from '../Button';
|
|
4
|
+
export type FilterDropdownFooterClearProps = Omit<ButtonProps<'button'>, 'variant' | 'color' | 'size' | 'as' | 'asChild'> & TestableProps;
|
|
5
|
+
/**
|
|
6
|
+
* The «Clear» button of the multi-mode footer. Keeps the menu open and moves focus back into it
|
|
7
|
+
* (the search, or the list) before the footer disappears.
|
|
8
|
+
*/
|
|
9
|
+
export declare const FilterDropdownFooterClear: FC<FilterDropdownFooterClearProps>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useTestId } from "../../utils/testId.js";
|
|
3
|
+
import { Button } from "../Button/index.js";
|
|
4
|
+
import { useFilterDropdownContext } from "./FilterDropdownContext.js";
|
|
5
|
+
const BUTTON_OWNED_KEYS = new Set([
|
|
6
|
+
'Enter',
|
|
7
|
+
' '
|
|
8
|
+
]);
|
|
9
|
+
const FilterDropdownFooterClear = ({ children, onClick, onKeyDown, 'data-testid': testIdProp, ...props })=>{
|
|
10
|
+
const { clear } = useFilterDropdownContext();
|
|
11
|
+
const testId = useTestId('footer-clear', testIdProp);
|
|
12
|
+
const handleKeyDown = (event)=>{
|
|
13
|
+
onKeyDown?.(event);
|
|
14
|
+
if (BUTTON_OWNED_KEYS.has(event.key)) event.stopPropagation();
|
|
15
|
+
};
|
|
16
|
+
const handleClick = (event)=>{
|
|
17
|
+
onClick?.(event);
|
|
18
|
+
if (event.defaultPrevented) return;
|
|
19
|
+
const content = event.currentTarget.closest('[data-scope="select"][data-part="content"]');
|
|
20
|
+
const search = content?.querySelector('[data-slot="filter-dropdown-search"] input');
|
|
21
|
+
(search ?? content)?.focus({
|
|
22
|
+
preventScroll: true
|
|
23
|
+
});
|
|
24
|
+
clear();
|
|
25
|
+
};
|
|
26
|
+
return /*#__PURE__*/ jsx(Button, {
|
|
27
|
+
...props,
|
|
28
|
+
"data-slot": "filter-dropdown-footer-clear",
|
|
29
|
+
"data-testid": testId,
|
|
30
|
+
"data-no-autofocus": "",
|
|
31
|
+
variant: "ghost",
|
|
32
|
+
color: "neutral",
|
|
33
|
+
size: "small",
|
|
34
|
+
onClick: handleClick,
|
|
35
|
+
onKeyDown: handleKeyDown,
|
|
36
|
+
children: children ?? 'Clear'
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
FilterDropdownFooterClear.displayName = 'FilterDropdownFooterClear';
|
|
40
|
+
export { FilterDropdownFooterClear };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ComponentProps, FC } from 'react';
|
|
2
|
+
import { SelectGroup } from '../Select/SelectGroup';
|
|
3
|
+
export type FilterDropdownGroupProps = ComponentProps<typeof SelectGroup>;
|
|
4
|
+
/** A labelled run of options. Render one per entry of `useFilterDropdown().groups`. */
|
|
5
|
+
export declare const FilterDropdownGroup: FC<FilterDropdownGroupProps>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { SelectGroup } from "../Select/SelectGroup.js";
|
|
3
|
+
const FilterDropdownGroup = (props)=>/*#__PURE__*/ jsx(SelectGroup, {
|
|
4
|
+
...props,
|
|
5
|
+
"data-slot": "filter-dropdown-group"
|
|
6
|
+
});
|
|
7
|
+
FilterDropdownGroup.displayName = 'FilterDropdownGroup';
|
|
8
|
+
export { FilterDropdownGroup };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ComponentProps, FC } from 'react';
|
|
2
|
+
import { SelectGroupLabel } from '../Select/SelectGroupLabel';
|
|
3
|
+
export type FilterDropdownGroupLabelProps = ComponentProps<typeof SelectGroupLabel>;
|
|
4
|
+
export declare const FilterDropdownGroupLabel: FC<FilterDropdownGroupLabelProps>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { SelectGroupLabel } from "../Select/SelectGroupLabel.js";
|
|
3
|
+
const FilterDropdownGroupLabel = (props)=>/*#__PURE__*/ jsx(SelectGroupLabel, {
|
|
4
|
+
...props,
|
|
5
|
+
"data-slot": "filter-dropdown-group-label"
|
|
6
|
+
});
|
|
7
|
+
FilterDropdownGroupLabel.displayName = 'FilterDropdownGroupLabel';
|
|
8
|
+
export { FilterDropdownGroupLabel };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode, type Ref } from 'react';
|
|
2
|
+
import type { CollectionItem } from '@ark-ui/react/collection';
|
|
3
|
+
import { SelectOption } from '../Select/SelectOption';
|
|
4
|
+
export interface FilterDropdownOptionProps<T extends CollectionItem = CollectionItem> extends Omit<ComponentProps<typeof SelectOption>, 'item' | 'children'> {
|
|
5
|
+
ref?: Ref<HTMLDivElement>;
|
|
6
|
+
item: T;
|
|
7
|
+
/** Secondary text at the right end of the row, such as a count. */
|
|
8
|
+
hint?: ReactNode;
|
|
9
|
+
/** Row text. Defaults to the item's label from the collection. */
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
export declare function FilterDropdownOption<T extends CollectionItem>({ item, hint, children, 'data-testid': testIdProp, ...props }: FilterDropdownOptionProps<T>): import("react").JSX.Element;
|
|
13
|
+
export declare namespace FilterDropdownOption {
|
|
14
|
+
var displayName: string;
|
|
15
|
+
}
|