@swan-io/lake 18.1.1 → 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.1",
3
+ "version": "18.1.2",
4
4
  "engines": {
5
5
  "node": ">22.12.0"
6
6
  },
@@ -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,
@@ -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`;