@wavv/ui 2.5.1 → 2.5.2-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { SelectProps } from './typeDefs/reactAriaTypes';
3
+ import type { ListOption, OpenStateProps, SelectInputProps, SelectItem } from './typeDefs/selectionTypes';
4
+ export type FilterableSelectItem = SelectItem & {
5
+ /** Extra text matched when filtering, in addition to value, header, and body */
6
+ searchText?: string;
7
+ };
8
+ type BaseProps = Omit<Extract<SelectInputProps, {
9
+ selectionMode?: 'single';
10
+ }>, 'selectionMode' | 'options' | 'allowRepeatSelection'>;
11
+ type Props = {
12
+ options?: ListOption<FilterableSelectItem>[];
13
+ /** Placeholder text for the search input shown at the top of the popover */
14
+ searchPlaceholder?: string;
15
+ /** Fixed row height in px used by the virtualizer */
16
+ itemHeight?: number;
17
+ /** Fixed section header height in px used by the virtualizer */
18
+ sectionHeaderHeight?: number;
19
+ /** Gap in px between items */
20
+ itemGap?: number;
21
+ /** Node to render when the filtered list is empty */
22
+ emptyFallback?: ReactNode;
23
+ ref?: React.Ref<HTMLButtonElement>;
24
+ } & BaseProps & OpenStateProps & SelectProps;
25
+ declare const FilterableSelect: ({ backgroundColor, menuBackground, fontSize, fontWeight, color, disabled, invalid, required, readOnly, loading, label, options, placeholder, placeholderColor, description, errorMessage, textOnly, value, defaultValue, width, height, maxHeight, hideCaret, position, fixedPosition, borderRadius, iconLeft, leftElement, rightElement, before, after, open, onOpenChange, onChange, afterShow, afterHide, searchPlaceholder, itemHeight, sectionHeaderHeight, itemGap, emptyFallback, ref, ...props }: Props) => import("react/jsx-runtime").JSX.Element;
26
+ export default FilterableSelect;
@@ -0,0 +1,311 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import styled from "@emotion/styled";
3
+ import { useMemo, useState } from "react";
4
+ import { Autocomplete, Button, Collection, Input, ListBox, ListBoxSection, ListLayout, SearchField, Select, SelectValue, Virtualizer, useFilter } from "react-aria-components";
5
+ import { useControlledOpenState } from "../hooks/index.js";
6
+ import getIcon from "./helpers/getIcon.js";
7
+ import { paddingProps as styledProps_js_paddingProps } from "./helpers/styledProps.js";
8
+ import Icon from "./Icon/index.js";
9
+ import InputContainerStyles from "./Inputs/helpers/InputContainerStyles.js";
10
+ import InputMessage from "./Inputs/helpers/InputMessage.js";
11
+ import Label from "./Inputs/helpers/Label.js";
12
+ import ListBoxItem from "./ListBoxParts/ListBoxItem.js";
13
+ import ListHeader from "./ListHelpers/ListHeader.js";
14
+ import ListRootStyles, { preventProps } from "./ListHelpers/ListRootStyles.js";
15
+ import MotionPopover from "./MotionPopover.js";
16
+ import Spinner from "./Spinner.js";
17
+ const DEFAULT_MAX_HEIGHT = 300;
18
+ const DEFAULT_ITEM_HEIGHT = 36;
19
+ const DEFAULT_SECTION_HEIGHT = 28;
20
+ const DEFAULT_ITEM_GAP = 2;
21
+ const isSection = (item)=>'object' == typeof item && null !== item && 'options' in item;
22
+ const getItemTextValue = (item)=>[
23
+ item.value,
24
+ item.header,
25
+ item.body,
26
+ item.searchText
27
+ ].filter(Boolean).join(' ');
28
+ const FilterableSelect = ({ backgroundColor, menuBackground, fontSize, fontWeight, color, disabled, invalid, required, readOnly, loading, label, options, placeholder = 'Select', placeholderColor, description, errorMessage, textOnly, value, defaultValue, width, height, maxHeight = DEFAULT_MAX_HEIGHT, hideCaret, position, fixedPosition, borderRadius, iconLeft, leftElement, rightElement, before, after, open, onOpenChange, onChange, afterShow, afterHide, searchPlaceholder = 'Search', itemHeight = DEFAULT_ITEM_HEIGHT, sectionHeaderHeight = DEFAULT_SECTION_HEIGHT, itemGap = DEFAULT_ITEM_GAP, emptyFallback, ref, ...props })=>{
29
+ const [isOpen, handleOpenChange] = useControlledOpenState({
30
+ open,
31
+ onOpenChange,
32
+ afterShow,
33
+ afterHide
34
+ });
35
+ const [internalValue, setInternalValue] = useState(defaultValue ?? '');
36
+ const { contains } = useFilter({
37
+ sensitivity: 'base'
38
+ });
39
+ const isControlled = void 0 !== value;
40
+ const currentValue = isControlled ? value : internalValue;
41
+ const { padding, paddingTop, paddingBottom, paddingRight, paddingLeft } = props;
42
+ const { margin, marginTop, marginBottom, marginRight, marginLeft } = props;
43
+ const paddingProps = {
44
+ padding,
45
+ paddingTop,
46
+ paddingBottom,
47
+ paddingRight,
48
+ paddingLeft
49
+ };
50
+ const marginProps = {
51
+ margin,
52
+ marginTop,
53
+ marginBottom,
54
+ marginRight,
55
+ marginLeft
56
+ };
57
+ const handleSelect = (val)=>{
58
+ if (!val) return;
59
+ const stringVal = String(val);
60
+ if (!isControlled) setInternalValue(stringVal);
61
+ if (onChange) onChange(stringVal);
62
+ };
63
+ const hasValue = !!(currentValue || defaultValue);
64
+ const hidePlaceholder = !hasValue && label;
65
+ const caret = hideCaret ? null : /*#__PURE__*/ jsx(Icon, {
66
+ name: isOpen ? 'caret-up' : 'caret-down',
67
+ color: color
68
+ });
69
+ const layoutOptions = useMemo(()=>({
70
+ rowHeight: itemHeight,
71
+ headingHeight: sectionHeaderHeight,
72
+ gap: itemGap
73
+ }), [
74
+ itemHeight,
75
+ sectionHeaderHeight,
76
+ itemGap
77
+ ]);
78
+ const renderItem = (item)=>/*#__PURE__*/ jsx(ListBoxItem, {
79
+ id: item.id || item.value,
80
+ value: item.value,
81
+ header: item.header ?? item.value,
82
+ body: item.body,
83
+ leftElement: item.leftElement,
84
+ rightElement: item.rightElement,
85
+ inline: item.inline,
86
+ disabled: item.disabled,
87
+ textValue: getItemTextValue(item),
88
+ selectionMode: "single"
89
+ });
90
+ const renderNode = (node)=>{
91
+ if (isSection(node)) return /*#__PURE__*/ jsxs(ListBoxSection, {
92
+ id: node.id,
93
+ children: [
94
+ node.title ? /*#__PURE__*/ jsx(ListHeader, {
95
+ children: node.title
96
+ }) : null,
97
+ /*#__PURE__*/ jsx(Collection, {
98
+ items: node.options,
99
+ children: renderItem
100
+ })
101
+ ]
102
+ });
103
+ return renderItem(node);
104
+ };
105
+ return /*#__PURE__*/ jsxs(SelectRoot, {
106
+ "aria-label": label || placeholder,
107
+ placeholder: placeholder,
108
+ isOpen: isOpen,
109
+ onOpenChange: handleOpenChange,
110
+ onSelectionChange: handleSelect,
111
+ isDisabled: disabled || readOnly || loading,
112
+ isInvalid: invalid,
113
+ isRequired: required,
114
+ selectedKey: currentValue || null,
115
+ defaultSelectedKey: defaultValue,
116
+ textOnly: textOnly,
117
+ width: width,
118
+ ...props,
119
+ children: [
120
+ /*#__PURE__*/ jsxs(SelectTrigger, {
121
+ backgroundColor: backgroundColor,
122
+ textOnly: textOnly,
123
+ hasLabel: !!label,
124
+ isDisabled: disabled,
125
+ isReadOnly: readOnly || loading,
126
+ isInvalid: invalid,
127
+ borderRadius: borderRadius,
128
+ height: height,
129
+ pointer: true,
130
+ ref: ref,
131
+ iconLeft: !!iconLeft,
132
+ ...paddingProps,
133
+ ...marginProps,
134
+ children: [
135
+ iconLeft && getIcon(iconLeft, {
136
+ marginRight: 8
137
+ }),
138
+ leftElement,
139
+ /*#__PURE__*/ jsx(Label, {
140
+ label: label,
141
+ filled: hasValue,
142
+ disabled: disabled,
143
+ disablePointerEvents: true,
144
+ required: required,
145
+ children: /*#__PURE__*/ jsx(DisplayValue, {
146
+ fontSize: fontSize,
147
+ fontWeight: fontWeight,
148
+ color: color,
149
+ placeholderColor: placeholderColor,
150
+ isDisabled: disabled,
151
+ style: ({ isPlaceholder })=>isPlaceholder && hidePlaceholder ? {
152
+ opacity: 0,
153
+ position: 'absolute'
154
+ } : {},
155
+ children: ({ isPlaceholder, selectedText, defaultChildren })=>{
156
+ if (isPlaceholder) return defaultChildren;
157
+ return selectedText;
158
+ }
159
+ })
160
+ }),
161
+ rightElement,
162
+ loading ? /*#__PURE__*/ jsx(Spinner, {
163
+ size: "small"
164
+ }) : caret,
165
+ /*#__PURE__*/ jsx(InputMessage, {
166
+ description: description,
167
+ errorMessage: errorMessage
168
+ })
169
+ ]
170
+ }),
171
+ /*#__PURE__*/ jsx(MotionPopover, {
172
+ offset: 2,
173
+ placement: position,
174
+ shouldFlip: fixedPosition ? false : void 0,
175
+ maxHeight: maxHeight,
176
+ children: /*#__PURE__*/ jsxs(PopoverBody, {
177
+ background: menuBackground,
178
+ autoWidth: textOnly,
179
+ children: [
180
+ before,
181
+ /*#__PURE__*/ jsxs(Autocomplete, {
182
+ filter: contains,
183
+ children: [
184
+ /*#__PURE__*/ jsxs(SearchRow, {
185
+ children: [
186
+ /*#__PURE__*/ jsx(Icon, {
187
+ name: "search",
188
+ marginRight: 8
189
+ }),
190
+ /*#__PURE__*/ jsx(StyledSearchField, {
191
+ "aria-label": "Search",
192
+ children: /*#__PURE__*/ jsx(SearchFieldInput, {
193
+ autoFocus: true,
194
+ placeholder: searchPlaceholder
195
+ })
196
+ })
197
+ ]
198
+ }),
199
+ /*#__PURE__*/ jsx(Virtualizer, {
200
+ layout: ListLayout,
201
+ layoutOptions: layoutOptions,
202
+ children: /*#__PURE__*/ jsx(StyledListBox, {
203
+ selectionMode: "single",
204
+ shouldFocusWrap: true,
205
+ items: options,
206
+ "aria-label": label || placeholder || 'Options',
207
+ renderEmptyState: emptyFallback ? ()=>/*#__PURE__*/ jsx(EmptyState, {
208
+ children: emptyFallback
209
+ }) : void 0,
210
+ style: {
211
+ maxHeight: Number(maxHeight)
212
+ },
213
+ children: (node)=>renderNode(node)
214
+ })
215
+ })
216
+ ]
217
+ }),
218
+ after
219
+ ]
220
+ })
221
+ })
222
+ ]
223
+ });
224
+ };
225
+ const SelectRoot = styled(Select, preventProps)(ListRootStyles);
226
+ const SelectTrigger = styled(Button)(InputContainerStyles, ({ iconLeft, textOnly })=>({
227
+ paddingLeft: iconLeft ? 8 : void 0,
228
+ paddingRight: textOnly ? void 0 : 8
229
+ }), styledProps_js_paddingProps);
230
+ const DisplayValue = styled(SelectValue)(({ theme: { input, font }, fontSize, fontWeight, color, placeholderColor, isDisabled })=>({
231
+ color: color || (isDisabled ? input.color.disabled : input.color.default),
232
+ fontFamily: font.family.default,
233
+ fontSize: fontSize || font.size.lg,
234
+ fontWeight: fontWeight || font.weight.default,
235
+ width: '100%',
236
+ textAlign: 'start',
237
+ overflow: 'hidden',
238
+ textOverflow: 'ellipsis',
239
+ whiteSpace: 'nowrap',
240
+ '&[data-placeholder]': {
241
+ color: placeholderColor || (isDisabled ? input.color.disabled : input.color.placeholder)
242
+ }
243
+ }));
244
+ const PopoverBody = styled('div', {
245
+ shouldForwardProp: (prop)=>![
246
+ 'background',
247
+ 'autoWidth'
248
+ ].includes(prop)
249
+ })(({ theme, background, autoWidth })=>({
250
+ background: background || theme.options.background,
251
+ backdropFilter: theme.options.backdropFilter,
252
+ borderRadius: 4,
253
+ boxShadow: theme.elevation3,
254
+ boxSizing: 'border-box',
255
+ fontFamily: theme.font.family.default,
256
+ fontSize: theme.font.size.md,
257
+ width: autoWidth ? 'max-content' : 'var(--trigger-width)',
258
+ minWidth: 220,
259
+ maxHeight: 'inherit',
260
+ display: 'flex',
261
+ flexDirection: 'column',
262
+ outline: 'none',
263
+ border: theme.defaultBorder,
264
+ overflow: 'hidden'
265
+ }));
266
+ const SearchRow = styled.div(({ theme })=>({
267
+ display: 'flex',
268
+ alignItems: 'center',
269
+ padding: '8px 12px',
270
+ borderBottom: `1px solid ${theme.scale2}`,
271
+ flexShrink: 0,
272
+ color: theme.input.labelColor.default
273
+ }));
274
+ const StyledSearchField = styled(SearchField)({
275
+ display: 'flex',
276
+ alignItems: 'center',
277
+ flex: 1,
278
+ minWidth: 0
279
+ });
280
+ const SearchFieldInput = styled(Input)(({ theme: { font, input, accent } })=>({
281
+ width: '100%',
282
+ border: 'none',
283
+ outline: 'none',
284
+ background: 'transparent',
285
+ color: input.color.default,
286
+ caretColor: accent,
287
+ fontFamily: font.family.default,
288
+ fontSize: font.size.md,
289
+ padding: 0,
290
+ '&::placeholder': {
291
+ color: input.color.placeholder,
292
+ opacity: 1
293
+ },
294
+ '&::-webkit-search-decoration, &::-webkit-search-cancel-button, &::-webkit-search-results-button, &::-webkit-search-results-decoration': {
295
+ WebkitAppearance: 'none'
296
+ }
297
+ }));
298
+ const StyledListBox = styled(ListBox)({
299
+ outline: 'none',
300
+ width: '100%',
301
+ overflow: 'auto',
302
+ flex: 1,
303
+ minHeight: 0
304
+ });
305
+ const EmptyState = styled.div(({ theme })=>({
306
+ color: theme.scale6,
307
+ fontSize: theme.font.size.md,
308
+ padding: '8px 16px'
309
+ }));
310
+ const components_FilterableSelect = FilterableSelect;
311
+ export { components_FilterableSelect as default };
@@ -29,7 +29,7 @@ const ListBoxItem_ListBoxItem = ({ id, value, header, body, leftElement, rightEl
29
29
  body: body,
30
30
  inline: inline,
31
31
  disabled: disabled
32
- }) : value,
32
+ }) : headerText,
33
33
  rightElement && /*#__PURE__*/ jsx("div", {
34
34
  style: {
35
35
  marginLeft: 'auto'
package/build/index.d.ts CHANGED
@@ -19,6 +19,7 @@ export { default as DropdownSelect } from './components/DropdownSelect';
19
19
  export { default as Dropdown } from './components/Dropdown';
20
20
  export { default as Ellipsis } from './components/Ellipsis';
21
21
  export { default as FileTrigger } from './components/FileTrigger';
22
+ export { default as FilterableSelect } from './components/FilterableSelect';
22
23
  export { default as Form } from './components/Form';
23
24
  export { default as Grid } from './components/Grid';
24
25
  export { default as Icon } from './components/Icon';
@@ -71,6 +72,7 @@ export { default as darkScale } from './theme/core/dark/darkScale';
71
72
  export { default as lightScale } from './theme/core/light/lightScale';
72
73
  export type { IconName } from './components/Icon/Icon';
73
74
  export type { Action as TransferAction, Item as TransferItem, Next as TransferNext } from './components/TransferList';
75
+ export type { FilterableSelectItem } from './components/FilterableSelect';
74
76
  export type { ListItem, ListOption, ListSection, MultiSelectItem, SelectItem, } from './components/typeDefs/selectionTypes';
75
77
  export type { TagItem } from './components/typeDefs/tagTypes';
76
78
  export type { AsProp as As, AudioRef, DraftEditorRef, EditorRef, FlexPosition, Height, InputRef, Margin, MarginPadding, MaxHeight, MaxWidth, MaxWidthHeight, MinHeight, MinWidth, MinWidthHeight, MultiSelectRef, Padding, Position, Placement, Sizes, Width, WidthHeight, } from './components/types';
package/build/index.js CHANGED
@@ -19,6 +19,7 @@ import DropdownSelect from "./components/DropdownSelect.js";
19
19
  import Dropdown from "./components/Dropdown.js";
20
20
  import Ellipsis from "./components/Ellipsis.js";
21
21
  import FileTrigger from "./components/FileTrigger.js";
22
+ import FilterableSelect from "./components/FilterableSelect.js";
22
23
  import Form from "./components/Form.js";
23
24
  import Grid from "./components/Grid.js";
24
25
  import Icon from "./components/Icon/index.js";
@@ -83,4 +84,4 @@ import formatDate from "./utils/formatDate.js";
83
84
  import formatNumber from "./utils/formatNumber.js";
84
85
  import numberWithCommas from "./utils/numberWithCommas.js";
85
86
  import matchesFileTypes from "./utils/matchesFileTypes.js";
86
- export { Accordion, Audio, Avatar, BarChart, Button, Calendar, Checkbox, Code, ComboBox, CommandMenu, DatePicker, DateRangePicker, DateRangeSelect, Dialog, DocTable, Dot, DraftEditor, DropZone, Dropdown, DropdownMenu, DropdownSelect, Editor, Ellipsis, FileTrigger, Form, Grid, Icon, ImageViewer, InlineCode, InlineInput, InputHelpers as InputUtils, Label, LineChart, Menu, Message, MessageHr, Modal, MultiSelect, NumberInput, Pagination, PaymentLogo, PhoneInput, PieChart, Popover, PortalScope, Progress, Radio, RangeCalendar, ResetStyles, ScrollbarStyles, SearchInput, Select, Slider, Spinner, Table, Tabs, Tag, TextArea, TextInput, TimeInput, ToastStyles, Toggle, ToggleButton, ToggleButtonGroup, Tooltip, TransferList, Tree, UnstyledButton, colors, copyToClipboard, createEditorContent, darkScale, editorContentToText, formatDate, formatNumber, lightScale, marginProps, matchesFileTypes, numberWithCommas, paddingProps, positionProps, theme, themeClasses, themeOptions, useConfirm, useCopy, useElementObserver, useEventListener, useOnClickOutside, usePrevious, useSelectAll, useWindowSize, widthHeightProps };
87
+ export { Accordion, Audio, Avatar, BarChart, Button, Calendar, Checkbox, Code, ComboBox, CommandMenu, DatePicker, DateRangePicker, DateRangeSelect, Dialog, DocTable, Dot, DraftEditor, DropZone, Dropdown, DropdownMenu, DropdownSelect, Editor, Ellipsis, FileTrigger, FilterableSelect, Form, Grid, Icon, ImageViewer, InlineCode, InlineInput, InputHelpers as InputUtils, Label, LineChart, Menu, Message, MessageHr, Modal, MultiSelect, NumberInput, Pagination, PaymentLogo, PhoneInput, PieChart, Popover, PortalScope, Progress, Radio, RangeCalendar, ResetStyles, ScrollbarStyles, SearchInput, Select, Slider, Spinner, Table, Tabs, Tag, TextArea, TextInput, TimeInput, ToastStyles, Toggle, ToggleButton, ToggleButtonGroup, Tooltip, TransferList, Tree, UnstyledButton, colors, copyToClipboard, createEditorContent, darkScale, editorContentToText, formatDate, formatNumber, lightScale, marginProps, matchesFileTypes, numberWithCommas, paddingProps, positionProps, theme, themeClasses, themeOptions, useConfirm, useCopy, useElementObserver, useEventListener, useOnClickOutside, usePrevious, useSelectAll, useWindowSize, widthHeightProps };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavv/ui",
3
- "version": "2.5.1",
3
+ "version": "2.5.2-alpha.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {