@swan-io/lake 13.6.7 → 13.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swan-io/lake",
3
- "version": "13.6.7",
3
+ "version": "13.7.1",
4
4
  "engines": {
5
5
  "node": ">22.12.0"
6
6
  },
@@ -471,3 +471,7 @@ input[type="range"]::-webkit-slider-thumb {
471
471
  transform: scale(0.98);
472
472
  filter: blur(4px);
473
473
  }
474
+
475
+ ::highlight(lake-highlight) {
476
+ background-color: #fff4cc;
477
+ }
@@ -6,6 +6,7 @@ export type Item<V> = {
6
6
  name: string;
7
7
  value: V;
8
8
  icon?: ReactNode;
9
+ searchTerms?: string[];
9
10
  };
10
11
  export type SelectProps<V, T extends Item<V> = Item<V>> = {
11
12
  ref?: Ref<View>;
@@ -34,5 +35,7 @@ export type SelectProps<V, T extends Item<V> = Item<V>> = {
34
35
  error?: string;
35
36
  readOnly?: boolean;
36
37
  style?: StyleProp<ViewStyle>;
38
+ hasSearch?: boolean;
39
+ searchPlaceholder?: string;
37
40
  };
38
- export declare const LakeSelect: <V, T extends Item<V> = Item<V>>({ ref, title, items, valueStyle, size, color, disabled, mode, placeholder, readOnly, id, matchReferenceWidth, value, error, hideErrors, icon, onValueChange, disabledItems, renderItem, PopoverFooter, style, }: SelectProps<V, T>) => import("react/jsx-runtime").JSX.Element;
41
+ export declare const LakeSelect: <V, T extends Item<V> = Item<V>>({ ref, title, items, valueStyle, size, color, disabled, mode, placeholder, readOnly, id, matchReferenceWidth, value, error, hideErrors, icon, onValueChange, disabledItems, renderItem, PopoverFooter, style, hasSearch, searchPlaceholder, }: SelectProps<V, T>) => import("react/jsx-runtime").JSX.Element;
@@ -1,11 +1,13 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useRef } from "react";
3
- import { StyleSheet, View, } from "react-native";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
3
+ import { StyleSheet, TextInput, View, } from "react-native";
4
4
  import { commonStyles } from "../constants/commonStyles";
5
- import { colors, invariantColors, radii, shadows, spacings, texts, } from "../constants/design";
5
+ import { backgroundColor, colors, invariantColors, radii, shadows, spacings, texts, } from "../constants/design";
6
+ import { useBoolean } from "../hooks/useBoolean";
6
7
  import { useDisclosure } from "../hooks/useDisclosure";
7
8
  import { useMergeRefs } from "../hooks/useMergeRefs";
8
9
  import { getFocusableElements } from "../utils/a11y";
10
+ import { setHighlightApi } from "../utils/highlights";
9
11
  import { isNotNullish, isNullishOrEmpty } from "../utils/nullish";
10
12
  import { Box } from "./Box";
11
13
  import { Fill } from "./Fill";
@@ -111,9 +113,37 @@ const styles = StyleSheet.create({
111
113
  errorContainer: {
112
114
  borderColor: colors.negative[500],
113
115
  },
116
+ filterContainer: {
117
+ flexGrow: 1,
118
+ flexShrink: 1,
119
+ margin: 16,
120
+ },
121
+ filterInput: {
122
+ ...texts.regular,
123
+ backgroundColor: backgroundColor.accented,
124
+ borderColor: colors.gray[100],
125
+ borderRadius: 4,
126
+ borderWidth: 1,
127
+ flexGrow: 1,
128
+ flexShrink: 1,
129
+ height: 40,
130
+ outlineStyle: "none",
131
+ paddingHorizontal: 12,
132
+ paddingLeft: 40,
133
+ placeholderTextColor: colors.gray[300],
134
+ },
135
+ filterFocused: {
136
+ borderColor: colors.gray[200],
137
+ },
138
+ searchIcon: {
139
+ position: "absolute",
140
+ left: 14,
141
+ },
114
142
  });
115
- export const LakeSelect = ({ ref, title, items, valueStyle, size, color = "current", disabled = false, mode = "normal", placeholder, readOnly = false, id, matchReferenceWidth = true, value, error, hideErrors = false, icon, onValueChange, disabledItems = [], renderItem, PopoverFooter, style, }) => {
143
+ export const LakeSelect = ({ ref, title, items, valueStyle, size, color = "current", disabled = false, mode = "normal", placeholder, readOnly = false, id, matchReferenceWidth = true, value, error, hideErrors = false, icon, onValueChange, disabledItems = [], renderItem, PopoverFooter, style, hasSearch = false, searchPlaceholder, }) => {
116
144
  var _a;
145
+ const [filter, setFilter] = useState("");
146
+ const [filterFocused, setFilterFocused] = useBoolean(false);
117
147
  const inputRef = useRef(null);
118
148
  const listRef = useRef(null);
119
149
  const typingTimeoutRef = useRef(undefined);
@@ -152,6 +182,30 @@ export const LakeSelect = ({ ref, title, items, valueStyle, size, color = "curre
152
182
  }, 300);
153
183
  }, [items, onValueChange, visible]);
154
184
  const name = (_a = itemValue === null || itemValue === void 0 ? void 0 : itemValue.name) !== null && _a !== void 0 ? _a : value;
185
+ const filteredItems = useMemo(() => {
186
+ if (isNullishOrEmpty(filter)) {
187
+ return items;
188
+ }
189
+ const lowerFilter = filter.toLowerCase();
190
+ return items.filter(item => {
191
+ var _a;
192
+ return item.name.toLowerCase().includes(lowerFilter) ||
193
+ ((_a = item.searchTerms) === null || _a === void 0 ? void 0 : _a.some(term => term.toLowerCase() === lowerFilter));
194
+ });
195
+ }, [items, filter]);
196
+ useEffect(() => {
197
+ if (!visible) {
198
+ setFilter("");
199
+ }
200
+ }, [visible]);
201
+ useEffect(() => {
202
+ var _a;
203
+ if (!hasSearch) {
204
+ return;
205
+ }
206
+ setHighlightApi(filter, (_a = listRef.current) === null || _a === void 0 ? void 0 : _a.element);
207
+ }, [filter, hasSearch]);
208
+ const ListHeaderComponent = useMemo(() => (_jsxs(Box, { direction: "row", alignItems: "center", style: styles.filterContainer, children: [_jsx(TextInput, { autoComplete: "off", inputMode: "search", multiline: false, rows: 1, onChangeText: filterValue => setFilter(filterValue), placeholder: searchPlaceholder, value: filter, onFocus: setFilterFocused.on, onBlur: setFilterFocused.off, style: [styles.filterInput, filterFocused && styles.filterFocused] }), _jsx(Icon, { name: "search-filled", color: colors[color].primary, size: 20, style: styles.searchIcon })] })), [filter, filterFocused, setFilterFocused, searchPlaceholder, color]);
155
209
  return (_jsxs(View, { style: commonStyles.fill, children: [_jsx(Pressable, { id: id, ref: mergedRef, "aria-haspopup": "listbox", role: "button", "aria-expanded": visible, disabled: readOnly || disabled, style: ({ focused, hovered, pressed }) => [
156
210
  mode === "normal" ? styles.normal : styles.borderless,
157
211
  size === "small" && styles.small,
@@ -174,7 +228,7 @@ export const LakeSelect = ({ ref, title, items, valueStyle, size, color = "curre
174
228
  styles.itemText,
175
229
  styles.selectPlaceholder,
176
230
  isSmall && styles.selectSmallPlaceholder,
177
- ], children: placeholder !== null && placeholder !== void 0 ? placeholder : " " }))] }), _jsx(Fill, { minWidth: 8 }), !disabled && (_jsx(Icon, { color: colors.gray[900], name: visible ? "chevron-up-filled" : "chevron-down-filled", size: 16 }))] })] })) }), !hideErrors && (_jsx(LakeText, { variant: "smallRegular", color: colors.negative[500], style: styles.errorText, children: error !== null && error !== void 0 ? error : " " })), _jsxs(Popover, { role: "listbox", matchReferenceMinWidth: matchReferenceWidth, onDismiss: close, referenceRef: inputRef, returnFocus: true, visible: visible, children: [isNotNullish(title) && (_jsxs(_Fragment, { children: [_jsx(LakeText, { variant: "semibold", color: colors.gray[900], style: styles.selectListTitle, children: title }), _jsx(Separator, {})] })), _jsx(FlatList, { role: "list", data: items, ref: listRef, contentContainerStyle: styles.listContent, onKeyDown: (event) => {
231
+ ], children: placeholder !== null && placeholder !== void 0 ? placeholder : " " }))] }), _jsx(Fill, { minWidth: 8 }), !disabled && (_jsx(Icon, { color: colors.gray[900], name: visible ? "chevron-up-filled" : "chevron-down-filled", size: 16 }))] })] })) }), !hideErrors && (_jsx(LakeText, { variant: "smallRegular", color: colors.negative[500], style: styles.errorText, children: error !== null && error !== void 0 ? error : " " })), _jsxs(Popover, { role: "listbox", matchReferenceMinWidth: matchReferenceWidth, onDismiss: close, referenceRef: inputRef, returnFocus: true, visible: visible, children: [isNotNullish(title) && (_jsxs(_Fragment, { children: [_jsx(LakeText, { variant: "semibold", color: colors.gray[900], style: styles.selectListTitle, children: title }), _jsx(Separator, {})] })), _jsx(FlatList, { role: "list", data: filteredItems, ref: listRef, contentContainerStyle: styles.listContent, onKeyDown: (event) => {
178
232
  var _a;
179
233
  const { key } = event.nativeEvent;
180
234
  if (key === "ArrowDown" || key === "ArrowUp") {
@@ -186,7 +240,7 @@ export const LakeSelect = ({ ref, title, items, valueStyle, size, color = "curre
186
240
  (_a = focusableElements[nextIndex]) === null || _a === void 0 ? void 0 : _a.focus();
187
241
  }
188
242
  }
189
- }, keyExtractor: (_, index) => `select-item-${index}`, renderItem: ({ item, index }) => {
243
+ }, keyExtractor: (_, index) => `select-item-${index}`, ListHeaderComponent: hasSearch ? ListHeaderComponent : undefined, renderItem: ({ item, index }) => {
190
244
  const isSelected = value === item.value;
191
245
  const disablement = disabledItems.find(({ value }) => value === item.value);
192
246
  const content = renderItem != null ? (renderItem(item, isSelected)) : (_jsxs(_Fragment, { children: [isNotNullish(item.icon) && (_jsxs(_Fragment, { children: [item.icon, _jsx(Space, { width: 12 })] })), _jsx(LakeText, { color: colors.gray[900], numberOfLines: 1, style: [styles.itemText, isSelected && styles.selected], children: item.name })] }));
@@ -2,6 +2,7 @@ import { Option } from "@swan-io/boxed";
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import { match } from "ts-pattern";
4
4
  const MAX_OFFSET_FOR_CENTER_PLACEMENT = 100;
5
+ const HORIZONTAL_SAFETY_MARGIN = 16;
5
6
  export const useContextualLayer = ({ placement, visible, matchReferenceWidth = false, matchReferenceMinWidth = false, referenceRef: externalReferenceRef, }) => {
6
7
  const referenceRef = useRef(null);
7
8
  const usedRef = externalReferenceRef !== null && externalReferenceRef !== void 0 ? externalReferenceRef : referenceRef;
@@ -51,10 +52,16 @@ export const useContextualLayer = ({ placement, visible, matchReferenceWidth = f
51
52
  height: "100vh",
52
53
  pointerEvents: "none",
53
54
  };
55
+ const maxWidth = match(inferedPlacement)
56
+ .with("left", () => viewportWidth - rect.left - HORIZONTAL_SAFETY_MARGIN)
57
+ .with("right", () => rect.right - HORIZONTAL_SAFETY_MARGIN)
58
+ .with("center", () => Math.min(availableSpaceBefore + width / 2, availableSpaceAfter + width / 2) * 2 - HORIZONTAL_SAFETY_MARGIN)
59
+ .exhaustive();
54
60
  const style = {
55
61
  ...verticalPosition,
56
62
  ...horizontalPosition,
57
63
  maxHeight,
64
+ maxWidth,
58
65
  ...(matchReferenceWidth === true ? { width } : undefined),
59
66
  ...(matchReferenceMinWidth === true ? { minWidth: width } : undefined),
60
67
  pointerEvents: "auto",
@@ -0,0 +1 @@
1
+ export declare const setHighlightApi: (text: string, element: HTMLElement | null | undefined, minLength?: number) => (() => void) | undefined;
@@ -0,0 +1,41 @@
1
+ import { isNullishOrEmpty } from "./nullish";
2
+ const highlightName = "lake-highlight";
3
+ export const setHighlightApi = (text, element, minLength = 2) => {
4
+ var _a, _b;
5
+ if (!("highlights" in CSS)) {
6
+ return;
7
+ }
8
+ if (element == null || isNullishOrEmpty(text) || text.length < minLength) {
9
+ CSS.highlights.delete(highlightName);
10
+ return;
11
+ }
12
+ const str = text.toLowerCase();
13
+ const treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
14
+ const ranges = [];
15
+ let currentNode = treeWalker.nextNode();
16
+ while (currentNode != null) {
17
+ const text = (_b = (_a = currentNode.textContent) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : "";
18
+ let startPos = 0;
19
+ while (startPos < text.length) {
20
+ const index = text.indexOf(str, startPos);
21
+ if (index === -1) {
22
+ break;
23
+ }
24
+ const range = new Range();
25
+ range.setStart(currentNode, index);
26
+ range.setEnd(currentNode, index + str.length);
27
+ ranges.push(range);
28
+ startPos = index + str.length;
29
+ }
30
+ currentNode = treeWalker.nextNode();
31
+ }
32
+ if (ranges.length > 0) {
33
+ CSS.highlights.set(highlightName, new Highlight(...ranges));
34
+ }
35
+ else {
36
+ CSS.highlights.delete(highlightName);
37
+ }
38
+ return () => {
39
+ CSS.highlights.delete(highlightName);
40
+ };
41
+ };