@swan-io/lake 18.1.0 → 18.1.2

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": "18.1.0",
3
+ "version": "18.1.2",
4
4
  "engines": {
5
5
  "node": ">22.12.0"
6
6
  },
@@ -3,8 +3,9 @@ import { ViewProps } from "react-native";
3
3
  type Props = {
4
4
  children: ReactNode;
5
5
  trigger: ReactNode;
6
+ triggerEnd?: ReactNode;
6
7
  style?: ViewProps["style"];
7
8
  contentContainerStyle?: ViewProps["style"];
8
9
  };
9
- export declare const Accordion: ({ children, trigger, style, contentContainerStyle }: Props) => import("react").JSX.Element;
10
+ export declare const Accordion: ({ children, trigger, triggerEnd, style, contentContainerStyle, }: Props) => import("react").JSX.Element;
10
11
  export {};
@@ -3,17 +3,29 @@ import { useId } from "react";
3
3
  import { Pressable, StyleSheet, View } from "react-native";
4
4
  import { backgroundColor, colors, spacings } from "../constants/design";
5
5
  import { useBoolean } from "../hooks/useBoolean";
6
+ import { isNotNullish } from "../utils/nullish";
6
7
  import { Icon } from "./Icon";
7
8
  import { LakeText } from "./LakeText";
8
9
  import { Space } from "./Space";
9
10
  const styles = StyleSheet.create({
11
+ header: {
12
+ flexDirection: "row",
13
+ alignItems: "stretch",
14
+ },
10
15
  trigger: {
16
+ flexGrow: 1,
17
+ flexShrink: 1,
11
18
  flexDirection: "row",
12
19
  alignItems: "center",
13
20
  backgroundColor: backgroundColor.accented,
14
21
  paddingVertical: spacings[12],
15
22
  paddingHorizontal: spacings[20],
16
23
  },
24
+ triggerEnd: {
25
+ justifyContent: "center",
26
+ backgroundColor: backgroundColor.accented,
27
+ paddingRight: spacings[20],
28
+ },
17
29
  arrow: {
18
30
  transitionProperty: "transform",
19
31
  transitionDuration: "200ms",
@@ -24,11 +36,13 @@ const styles = StyleSheet.create({
24
36
  contentContainer: {
25
37
  display: "grid",
26
38
  gridTemplateRows: "0fr",
27
- transitionProperty: "grid-template-rows",
39
+ transitionProperty: "grid-template-rows, visibility",
28
40
  transitionDuration: "300ms",
41
+ visibility: "hidden",
29
42
  },
30
43
  contentContainerDisplayed: {
31
44
  gridTemplateRows: "1fr",
45
+ visibility: "visible",
32
46
  },
33
47
  contentInner: {
34
48
  overflow: "hidden",
@@ -38,8 +52,9 @@ const styles = StyleSheet.create({
38
52
  paddingHorizontal: spacings[20],
39
53
  },
40
54
  });
41
- export const Accordion = ({ children, trigger, style, contentContainerStyle }) => {
42
- const id = useId();
55
+ export const Accordion = ({ children, trigger, triggerEnd, style, contentContainerStyle, }) => {
56
+ const triggerId = useId();
57
+ const contentId = useId();
43
58
  const [isOpen, { toggle }] = useBoolean(false);
44
- return (_jsxs(View, { children: [_jsxs(Pressable, { "aria-controls": id, role: "button", "aria-expanded": isOpen, onPress: toggle, style: [styles.trigger, style], children: [_jsx(Icon, { name: "chevron-right-filled", size: 12, color: colors.gray[500], style: [styles.arrow, isOpen && styles.arrowOpen] }), _jsx(Space, { width: 20 }), typeof trigger === "string" ? (_jsx(LakeText, { variant: "smallMedium", color: colors.gray[900], children: trigger })) : (trigger)] }), _jsx(View, { "aria-labelledby": id, "aria-hidden": !isOpen, role: "region", style: [styles.contentContainer, isOpen && styles.contentContainerDisplayed], children: _jsx(View, { style: styles.contentInner, children: _jsx(View, { style: [styles.content, contentContainerStyle], children: children }) }) })] }));
59
+ return (_jsxs(View, { children: [_jsxs(View, { style: styles.header, children: [_jsxs(Pressable, { id: triggerId, "aria-controls": contentId, role: "button", "aria-expanded": isOpen, onPress: toggle, style: [styles.trigger, style], children: [_jsx(Icon, { name: "chevron-right-filled", size: 12, color: colors.gray[500], style: [styles.arrow, isOpen && styles.arrowOpen] }), _jsx(Space, { width: 20 }), typeof trigger === "string" ? (_jsx(LakeText, { variant: "smallMedium", color: colors.gray[900], children: trigger })) : (trigger)] }), isNotNullish(triggerEnd) && _jsx(View, { style: styles.triggerEnd, children: triggerEnd })] }), _jsx(View, { id: contentId, "aria-labelledby": triggerId, role: "region", style: [styles.contentContainer, isOpen && styles.contentContainerDisplayed], children: _jsx(View, { style: styles.contentInner, children: _jsx(View, { style: [styles.content, contentContainerStyle], children: children }) }) })] }));
45
60
  };
@@ -4,6 +4,7 @@ import { StyleSheet, unstable_createElement, View, } from "react-native";
4
4
  import { match } from "ts-pattern";
5
5
  import { commonStyles } from "../constants/commonStyles";
6
6
  import { colors, fonts, spacings, texts } from "../constants/design";
7
+ import { getLabelId } from "../utils/a11y";
7
8
  import { isNotNullish } from "../utils/nullish";
8
9
  import { Box } from "./Box";
9
10
  import { LakeText } from "./LakeText";
@@ -44,7 +45,7 @@ export const LakeLabel = ({ label, optionalLabel, description, extra, readOnly =
44
45
  target === null || target === void 0 ? void 0 : target.focus();
45
46
  }, [id]);
46
47
  const renderActions = () => isNotNullish(actions) && (_jsxs(_Fragment, { children: [_jsx(Space, { width: 16 }), actionsAlign === "content" && isFormLabel ? (_jsx(View, { style: styles.topActions, children: actions })) : (actions)] }));
47
- return (_jsxs(Box, { style: [styles.container, style], direction: "row", alignItems: "center", children: [_jsxs(View, { style: commonStyles.fill, children: [_jsxs(Box, { direction: "row", justifyContent: "spaceBetween", alignItems: "center", children: [_jsxs(Box, { direction: "row", alignItems: "center", shrink: 1, children: [isFormLabel ? (_jsxs(Box, { shrink: 1, children: [_jsxs(Label, { onClick: onClick, htmlFor: id, style: [styles.label, readOnly && { color: readOnlyColor }], children: [label, optionalLabel != null && (_jsx(LakeText, { color: colors.gray[400], style: styles.optionalLabel, children: ` - ${optionalLabel}` }))] }), description != null && (_jsxs(_Fragment, { children: [_jsx(LakeText, { variant: "smallRegular", children: description }), _jsx(Space, { height: 8 })] }))] })) : (_jsxs(LakeText, { variant: "medium", color: readOnlyColor, children: [label, optionalLabel != null && (_jsx(LakeText, { color: colors.gray[400], style: styles.optionalLabel, children: ` - ${optionalLabel}` }))] })), isNotNullish(extra) && extra()] }), isNotNullish(help) && (_jsxs(_Fragment, { children: [_jsx(Space, { width: 16 }), help] }))] }), _jsx(Space, { height: match(type)
48
+ return (_jsxs(Box, { style: [styles.container, style], direction: "row", alignItems: "center", children: [_jsxs(View, { style: commonStyles.fill, children: [_jsxs(Box, { direction: "row", justifyContent: "spaceBetween", alignItems: "center", children: [_jsxs(Box, { direction: "row", alignItems: "center", shrink: 1, children: [isFormLabel ? (_jsxs(Box, { shrink: 1, children: [_jsxs(Label, { id: getLabelId(id), onClick: onClick, htmlFor: id, style: [styles.label, readOnly && { color: readOnlyColor }], children: [label, optionalLabel != null && (_jsx(LakeText, { color: colors.gray[400], style: styles.optionalLabel, children: ` - ${optionalLabel}` }))] }), description != null && (_jsxs(_Fragment, { children: [_jsx(LakeText, { variant: "smallRegular", children: description }), _jsx(Space, { height: 8 })] }))] })) : (_jsxs(LakeText, { variant: "medium", color: readOnlyColor, children: [label, optionalLabel != null && (_jsx(LakeText, { color: colors.gray[400], style: styles.optionalLabel, children: ` - ${optionalLabel}` }))] })), isNotNullish(extra) && extra()] }), isNotNullish(help) && (_jsxs(_Fragment, { children: [_jsx(Space, { width: 16 }), help] }))] }), _jsx(Space, { height: match(type)
48
49
  .returnType()
49
50
  .with("formSmall", "viewSmall", () => 4)
50
51
  .with("form", "view", () => 8)
@@ -6,7 +6,7 @@ import { commonStyles } from "../constants/commonStyles";
6
6
  import { backgroundColor, colors, invariantColors, radii, shadows, spacings, texts, } from "../constants/design";
7
7
  import { useBoolean } from "../hooks/useBoolean";
8
8
  import { useMergeRefs } from "../hooks/useMergeRefs";
9
- import { getFocusableElements } from "../utils/a11y";
9
+ import { getFocusableElements, getLabelId } from "../utils/a11y";
10
10
  import { setHighlightApi } from "../utils/highlights";
11
11
  import { isNotNullish, isNullishOrEmpty } from "../utils/nullish";
12
12
  import { Box } from "./Box";
@@ -217,7 +217,7 @@ export const LakeSelect = ({ ref, title, items, valueStyle, size, color = "curre
217
217
  }, [filter, hasSearch]);
218
218
  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]);
219
219
  const ListEmptyComponent = (_jsxs(Box, { justifyContent: "center", alignItems: "center", style: styles.emptyList, children: [_jsx(Icon, { name: "clipboard-search-regular", size: 24, color: colors.gray.primary }), _jsx(Space, { height: 8 }), _jsx(Text, { style: styles.emptyListText, children: t("common.noResult") })] }));
220
- 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 }) => [
220
+ return (_jsxs(View, { style: commonStyles.fill, children: [_jsx(Pressable, { id: id, ref: mergedRef, "aria-labelledby": id != null ? `${getLabelId(id)} ${id}` : undefined, "aria-haspopup": "listbox", role: "button", "aria-expanded": visible, disabled: readOnly || disabled, style: ({ focused, hovered, pressed }) => [
221
221
  mode === "normal" ? styles.normal : styles.borderless,
222
222
  size === "small" && styles.small,
223
223
  (disabled || readOnly) && styles.inputDisabled,
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
4
4
  import { Pressable, StyleSheet, Text, TextInput, View, } from "react-native";
5
5
  import { backgroundColor, colors, radii, shadows, texts } from "../constants/design";
6
6
  import { useBoolean } from "../hooks/useBoolean";
7
+ import { getLabelId } from "../utils/a11y";
7
8
  import { groupBy } from "../utils/array";
8
9
  import { isNotNullish, isNotNullishOrEmpty } from "../utils/nullish";
9
10
  import { safeSplitAround } from "../utils/string";
@@ -196,7 +197,7 @@ export const MultiSelect = ({ color = "gray", disabled = false, emptyResultText,
196
197
  }, [filteredItems]);
197
198
  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: filterPlaceholder, 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, filterPlaceholder, color]);
198
199
  const ListEmptyComponent = useMemo(() => (_jsxs(Box, { justifyContent: "center", alignItems: "center", style: styles.emptyList, children: [_jsx(Icon, { name: "clipboard-search-regular", size: 24, color: colors.gray.primary }), isNotNullishOrEmpty(emptyResultText) && (_jsxs(_Fragment, { children: [_jsx(Space, { height: 8 }), _jsx(Text, { style: styles.emptyListText, children: emptyResultText })] }))] })), [emptyResultText]);
199
- return (_jsxs(View, { style: style, children: [_jsxs(Pressable, { id: id, ref: inputRef, role: "button", "aria-haspopup": "listbox", "aria-expanded": visible, disabled: disabled, onPress: open, style: ({ hovered, focused }) => [
200
+ return (_jsxs(View, { style: style, children: [_jsxs(Pressable, { id: id, ref: inputRef, "aria-labelledby": id != null ? `${getLabelId(id)} ${id}` : undefined, role: "button", "aria-haspopup": "listbox", "aria-expanded": visible, disabled: disabled, onPress: open, style: ({ hovered, focused }) => [
200
201
  styles.base,
201
202
  hovered && styles.hovered,
202
203
  (focused || visible) && styles.focused,
@@ -166,13 +166,13 @@ export const PlainListView = ({ data: originalData, keyExtractor, rowHeight, gro
166
166
  onActiveRowChange === null || onActiveRowChange === void 0 ? void 0 : onActiveRowChange(element);
167
167
  }
168
168
  }, [activeRowId, onActiveRowChange]);
169
- const listWrapper = useMemo(() => withoutScroll ? (_jsx(View, { style: styles.contentContainer })) : (_jsx(ScrollView, { scrollEventThrottle: 32, contentContainerStyle: [
169
+ const listWrapper = useMemo(() => withoutScroll ? (_jsx(View, { role: "table", "aria-busy": isLoading, style: styles.contentContainer })) : (_jsx(ScrollView, { role: "table", "aria-busy": isLoading, tabIndex: isNullish(getRowLink) ? 0 : undefined, scrollEventThrottle: 32, contentContainerStyle: [
170
170
  styles.contentContainer,
171
171
  {
172
172
  minHeight: totalHeight + (isLoading ? loading.count * rowHeight : 0),
173
173
  },
174
- ] })), [isLoading, loading === null || loading === void 0 ? void 0 : loading.count, rowHeight, totalHeight, withoutScroll]);
175
- return (_jsx(ResponsiveContainer, { role: "table", style: withoutScroll ? (isEmpty ? commonStyles.fill : undefined) : styles.root, breakpoint: breakpoint, children: ({ large }) => {
174
+ ] })), [isLoading, loading === null || loading === void 0 ? void 0 : loading.count, rowHeight, totalHeight, withoutScroll, getRowLink]);
175
+ return (_jsx(ResponsiveContainer, { style: withoutScroll ? (isEmpty ? commonStyles.fill : undefined) : styles.root, breakpoint: breakpoint, children: ({ large }) => {
176
176
  const displayColumns = large ? columns : smallColumns;
177
177
  return (_jsx(_Fragment, { children: cloneElement(listWrapper, {
178
178
  children: (_jsxs(_Fragment, { children: [!isEmpty && large ? (_jsx(View, { role: "row", style: [
@@ -216,7 +216,7 @@ export const PlainListView = ({ data: originalData, keyExtractor, rowHeight, gro
216
216
  children: (_jsx(Row, { id: key, rowHeight: rowHeight, columns: displayColumns, item: item, index: index, extraInfo: extraInfo, isActive: isActive, isHovered: isHovered, large: large, style: rowStyle, onMouseEnter: setHoveredRow, onMouseLeave: removeHoveredRow })),
217
217
  });
218
218
  })] }, groupName));
219
- }) }), _jsx(View, { children: _jsx(View, { "aria-busy": isLoading, style: styles.loadingPlaceholder, children: isLoading ? (_jsx(PlainListViewPlaceholder, { count: loading.count, rowHeight: rowHeight })) : null }) }), isEmpty && isNotNullish(renderEmptyList) && !isLoading ? (_jsx(View, { style: styles.emptyListContainer, children: renderEmptyList() })) : null, _jsx(View, { style: [styles.scrollTracker, { height: onEndReachedThresholdPx }], ref: scrollTrackerRef })] })),
219
+ }) }), _jsx(View, { children: _jsx(View, { style: styles.loadingPlaceholder, children: isLoading ? (_jsx(PlainListViewPlaceholder, { count: loading.count, rowHeight: rowHeight })) : null }) }), isEmpty && isNotNullish(renderEmptyList) && !isLoading ? (_jsx(View, { style: styles.emptyListContainer, children: renderEmptyList() })) : null, _jsx(View, { style: [styles.scrollTracker, { height: onEndReachedThresholdPx }], ref: scrollTrackerRef })] })),
220
220
  }) }));
221
221
  } }));
222
222
  };
@@ -87,11 +87,13 @@ const styles = StyleSheet.create({
87
87
  collapsibleContent: {
88
88
  display: "grid",
89
89
  gridTemplateRows: "0fr",
90
- transitionProperty: "grid-template-rows",
90
+ transitionProperty: "grid-template-rows, visibility",
91
91
  transitionDuration: "300ms",
92
+ visibility: "hidden",
92
93
  },
93
94
  collapsibleContentOpen: {
94
95
  gridTemplateRows: "1fr",
96
+ visibility: "visible",
95
97
  },
96
98
  collapsibleInner: {
97
99
  overflow: "hidden",
@@ -103,7 +105,7 @@ export const Tile = ({ icon, title, description, headerEnd, footer, flexBasis, c
103
105
  const hasHeader = isNotNullish(title) || isNotNullish(headerEnd) || isNotNullish(description) || collapsible;
104
106
  const header = (_jsxs(_Fragment, { children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTitleIcon, children: [isNotNullish(icon) && (_jsxs(_Fragment, { children: [icon, _jsx(Space, { height: 12 })] })), isNotNullish(title) && (_jsx(Text, { style: styles.title, role: "heading", "aria-level": 3, children: title }))] }), collapsible ? (_jsx(Icon, { name: "chevron-down-filled", size: 16, color: colors.gray[500], style: [styles.chevron, expanded && styles.chevronOpen] })) : isNotNullish(headerEnd) ? (_jsx(View, { children: headerEnd })) : null] }), isNotNullish(description) && (_jsxs(_Fragment, { children: [isNotNullish(title) && _jsx(Space, { height: 12 }), _jsx(View, { style: styles.descriptionContainer, children: _jsx(LakeText, { numberOfLines: numberOfLines, style: styles.description, children: description }) })] }))] }));
105
107
  const content = (_jsxs(_Fragment, { children: [children, isNotNullish(footer) && (_jsx(View, { style: [styles.footer, { marginTop: paddingVertical, marginBottom: -paddingVertical }], children: footer }))] }));
106
- return (_jsx(View, { role: "region", style: [
108
+ return (_jsx(View, { style: [
107
109
  styles.container,
108
110
  selected === false && styles.unselectedContainer,
109
111
  {
@@ -117,7 +119,7 @@ export const Tile = ({ icon, title, description, headerEnd, footer, flexBasis, c
117
119
  selected === false && hovered && styles.unselectedHoveredContainer,
118
120
  disabled && styles.disabled,
119
121
  style,
120
- ], children: collapsible ? (_jsxs(_Fragment, { children: [hasHeader && (_jsx(Pressable, { role: "button", "aria-expanded": expanded, "aria-controls": contentId, onPress: toggle, style: styles.headerContainer, children: header })), _jsx(View, { id: contentId, "aria-hidden": !expanded, style: [styles.collapsibleContent, expanded && styles.collapsibleContentOpen], children: _jsxs(View, { style: styles.collapsibleInner, children: [isNotNullish(children) && hasHeader && _jsx(Space, { height: 24 }), content] }) })] })) : (_jsxs(_Fragment, { children: [hasHeader && (_jsxs(View, { style: styles.headerContainer, children: [header, isNotNullish(children) && _jsx(Space, { height: 24 })] })), content] })) }));
122
+ ], children: collapsible ? (_jsxs(_Fragment, { children: [hasHeader && (_jsx(Pressable, { role: "button", "aria-expanded": expanded, "aria-controls": contentId, onPress: toggle, style: styles.headerContainer, children: header })), _jsx(View, { id: contentId, style: [styles.collapsibleContent, expanded && styles.collapsibleContentOpen], children: _jsxs(View, { style: styles.collapsibleInner, children: [isNotNullish(children) && hasHeader && _jsx(Space, { height: 24 }), content] }) })] })) : (_jsxs(_Fragment, { children: [hasHeader && (_jsxs(View, { style: styles.headerContainer, children: [header, isNotNullish(children) && _jsx(Space, { height: 24 })] })), content] })) }));
121
123
  };
122
124
  export const TileFullWidthContent = ({ children, flexGrow, fitToBottom = false, }) => {
123
125
  return (_jsx(View, { style: [styles.fullWidthContent, fitToBottom && styles.fitToBottom, { flexGrow }], children: children }));
@@ -4,6 +4,7 @@ type Props = {
4
4
  labelOff: string;
5
5
  labelOn: string;
6
6
  onToggle: (value: boolean) => void;
7
+ ariaLabel?: string;
7
8
  };
8
- export declare const Toggle: ({ compact, value, labelOff, labelOn, onToggle }: Props) => import("react").JSX.Element;
9
+ export declare const Toggle: ({ compact, value, labelOff, labelOn, onToggle, ariaLabel, }: Props) => import("react").JSX.Element;
9
10
  export {};
@@ -50,7 +50,7 @@ const getItemWidth = (node) => match(node)
50
50
  .returnType()
51
51
  .with(P.instanceOf(HTMLElement), element => element.offsetWidth)
52
52
  .otherwise(() => 0);
53
- export const Toggle = ({ compact = false, value, labelOff, labelOn, onToggle }) => {
53
+ export const Toggle = ({ compact = false, value, labelOff, labelOn, onToggle, ariaLabel, }) => {
54
54
  const [itemsWidth, setItemsWidth] = useState();
55
55
  const onViewRef = useRef(null);
56
56
  const offViewRef = useRef(null);
@@ -67,7 +67,7 @@ export const Toggle = ({ compact = false, value, labelOff, labelOn, onToggle })
67
67
  const onPress = useCallback(() => {
68
68
  onToggle(!value);
69
69
  }, [onToggle, value]);
70
- return (_jsxs(Pressable, { role: "switch", onPress: onPress, "aria-checked": value, style: styles.base, children: [_jsx(View, { role: "presentation", style: [
70
+ return (_jsxs(Pressable, { role: "switch", onPress: onPress, "aria-checked": value, "aria-label": ariaLabel, style: styles.base, children: [_jsx(View, { role: "presentation", style: [
71
71
  styles.handle,
72
72
  itemsWidth == null
73
73
  ? styles.hidden
@@ -261,13 +261,13 @@ const ResizeHandle = ({ id, end = false, width, onResize, scrollViewRef }) => {
261
261
  };
262
262
  }
263
263
  }, [id, end]);
264
- return (_jsx(Pressable, { ref: ref, role: "none", style: [
264
+ return (_jsx(Pressable, { ref: ref, "aria-hidden": true, tabIndex: -1, style: [
265
265
  styles.resizeHandleZone,
266
266
  end ? styles.resizeHandleZoneEnd : styles.resizeHandleZoneStart,
267
267
  ], children: ({ hovered }) => (_jsx(View, { style: [styles.resizeHandle, (hovered || isDragging) && styles.resizeHandleActive] })) }));
268
268
  };
269
269
  export const VirtualizedList = ({ variant, data, stickedToStartColumns, columns, stickedToEndColumns, headerHeight, rowHeight, renderThreshold = 1000, onEndReached, onEndReachedThreshold = 200, loading, extraInfo, keyExtractor, marginHorizontal, renderEmptyList, onColumnResize, getRowLink, }) => {
270
- // Used for unique IDs generation (usefull for header IDs and cells aria-describedBy pointing to them)
270
+ // Used for unique IDs generation (usefull for header IDs)
271
271
  const viewId = useId();
272
272
  const scrollViewRef = useRef(null);
273
273
  const scrollTrackerRef = useRef(null);
@@ -481,7 +481,9 @@ export const VirtualizedList = ({ variant, data, stickedToStartColumns, columns,
481
481
  .getOr(false)) {
482
482
  return (_jsx(ScrollView, { style: styles.container, contentContainerStyle: styles.emptyListContentContainer, children: renderEmptyList() }));
483
483
  }
484
- return (_jsxs(ScrollView, { ref: scrollViewRef, role: "table", both: true, style: [styles.container, { marginHorizontal }], onScroll: onScroll, scrollEventThrottle: 32, contentContainerStyle: [
484
+ return (_jsxs(ScrollView, { ref: scrollViewRef, role: "table", "aria-busy": Option.fromNullable(loading)
485
+ .map(({ isLoading }) => isLoading)
486
+ .getOr(false), tabIndex: getRowLink == null ? 0 : undefined, both: true, style: [styles.container, { marginHorizontal }], onScroll: onScroll, scrollEventThrottle: 32, contentContainerStyle: [
485
487
  styles.contentContainer,
486
488
  {
487
489
  height: containerContainerHeight,
@@ -490,7 +492,7 @@ export const VirtualizedList = ({ variant, data, stickedToStartColumns, columns,
490
492
  ], children: [header, rowsToRender
491
493
  .map(({ startIndex, endIndex, data }) => (_jsxs(View, { style: styles.rowsContainer, ref: rowsContainerRef, children: [data.map((item, index) => (_jsx(VirtualizedRow, { viewId: viewId, item: item, rowHeight: rowHeight, absoluteIndex: startIndex + index, variant: variant, stickedToStartColumnsWidth: stickedToStartColumnsWidth, centerColumnsWidth: centerColumnsWidth, stickedToEndColumnsWidth: stickedToEndColumnsWidth, stickedToStartColumns: stickedToStartColumns, columns: columns, stickedToEndColumns: stickedToEndColumns, extraInfo: extraInfo, getRowLink: getRowLink, horizontalScrollPosition: horizontalScrollPosition !== null && horizontalScrollPosition !== void 0 ? horizontalScrollPosition : "NoScroll", stickedToStartFirstCellLeftPadding: stickedToStartFirstCellLeftPadding, centerFirstCellLeftPadding: centerFirstCellLeftPadding, centerLastCellLeftPadding: centerLastCellLeftPadding, stickedToEndLastCellRightPadding: stickedToEndLastCellRightPadding }, keyExtractor(item, startIndex + index)))), Option.fromNullable(loading)
492
494
  .flatMap(({ isLoading, count }) => (isLoading ? Option.Some(count) : Option.None()))
493
- .map(count => (_jsx(View, { "aria-busy": true, style: [
495
+ .map(count => (_jsx(View, { style: [
494
496
  styles.loadingPlaceholder,
495
497
  {
496
498
  top: endIndex * rowHeight,
@@ -541,7 +543,7 @@ const RawVirtualizedRow = ({ viewId, rowHeight, absoluteIndex, variant, stickedT
541
543
  ], children: columns.map(({ id, width, renderCell }, index) => {
542
544
  const columnId = `${viewId}_${id}`;
543
545
  const paddingLeft = index === 0 ? stickedToStartFirstCellLeftPadding : 0;
544
- return (_jsx(View, { style: [styles.cell, { width: width + paddingLeft, paddingLeft }], "aria-describedby": columnId, children: renderCell({
546
+ return (_jsx(View, { style: [styles.cell, { width: width + paddingLeft, paddingLeft }], children: renderCell({
545
547
  columnId,
546
548
  item,
547
549
  index: absoluteIndex,
@@ -567,7 +569,7 @@ const RawVirtualizedRow = ({ viewId, rowHeight, absoluteIndex, variant, stickedT
567
569
  styles.cell,
568
570
  grow && styles.grow,
569
571
  { width: width + paddingLeft + paddingRight, paddingLeft, paddingRight },
570
- ], "aria-describedby": columnId, children: renderCell({
572
+ ], children: renderCell({
571
573
  columnId,
572
574
  item,
573
575
  index: absoluteIndex,
@@ -589,7 +591,7 @@ const RawVirtualizedRow = ({ viewId, rowHeight, absoluteIndex, variant, stickedT
589
591
  ], children: columns.map(({ id, width, renderCell }, index) => {
590
592
  const columnId = `${viewId}_${id}`;
591
593
  const paddingRight = index === columns.length - 1 ? stickedToEndLastCellRightPadding : 0;
592
- return (_jsx(View, { style: [styles.cell, { width: width + paddingRight, paddingRight }], "aria-describedby": columnId, children: renderCell({
594
+ return (_jsx(View, { style: [styles.cell, { width: width + paddingRight, paddingRight }], children: renderCell({
593
595
  columnId,
594
596
  item,
595
597
  index: absoluteIndex,
@@ -1 +1,2 @@
1
1
  export declare const getFocusableElements: (rootNode?: Element, includeRoot?: boolean) => HTMLElement[];
2
+ export declare const getLabelId: (fieldId: string) => string;
package/src/utils/a11y.js CHANGED
@@ -16,3 +16,6 @@ const FOCUSABLE_SELECTOR = [
16
16
  export const getFocusableElements = (rootNode = document.documentElement, includeRoot = true) => {
17
17
  return (includeRoot && rootNode.matches(FOCUSABLE_SELECTOR) ? [rootNode] : []).concat(Array.from(rootNode.querySelectorAll(FOCUSABLE_SELECTOR)));
18
18
  };
19
+ // `<label for>` doesn't apply to elements that aren't labelable (like a `div` with `role="button"`),
20
+ // so LakeLabel also gives its label this id for fields to reference with `aria-labelledby`
21
+ export const getLabelId = (fieldId) => `${fieldId}-label`;