@delacour/react-native-ui 0.4.0-alpha.20261020143428 → 0.4.0-alpha.20261020154654
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": "@delacour/react-native-ui",
|
|
3
|
-
"version": "0.4.0-alpha.
|
|
3
|
+
"version": "0.4.0-alpha.20261020154654",
|
|
4
4
|
"description": "React Native UI components — Uniwind, Reanimated, Gesture Handler",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"expo-blur": ">=15",
|
|
182
182
|
"@maplibre/maplibre-react-native": "^11.5.0",
|
|
183
183
|
"@central-icons-react-native/round-outlined-radius-1-stroke-1.5": "^1.1",
|
|
184
|
-
"@delacour/react-native-bottom-sheet": "0.2.0-alpha.
|
|
184
|
+
"@delacour/react-native-bottom-sheet": "0.2.0-alpha.20261020154654",
|
|
185
185
|
"@delacour/react-native-charts": ">=0.1.0",
|
|
186
186
|
"@legendapp/list": ">=3.3",
|
|
187
187
|
"@shopify/react-native-skia": ">=2",
|
|
@@ -46,14 +46,27 @@ five slots: `ItemPrefix`, `ItemContent`, `ItemTitle`, `ItemDescription`,
|
|
|
46
46
|
- **The root clips.** `overflow-hidden` is load-bearing: a pressed row fades to
|
|
47
47
|
the edge of its own box, and the first and last rows would square off the
|
|
48
48
|
group's corners without it.
|
|
49
|
-
- **A row is a `Pressable
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
- **A row is a `Pressable` only when it has a handler.**
|
|
50
|
+
`resolveListGroupItemRender` returns `pressable` for an `onPress` or
|
|
51
|
+
`onLongPress` — role `button`, children merged into one accessible element;
|
|
52
|
+
disabled, it keeps the role, reports `disabled` and dims through
|
|
53
|
+
`opacity-50`, which `Pressable` composes with its press animation — and
|
|
54
|
+
`static` for neither: a plain `Animated.View` with the same `item` slot, no
|
|
55
|
+
role and no press feedback. A Settings row showing a value is not a control,
|
|
56
|
+
and VoiceOver announcing it as a button on every swipe is the bug this
|
|
57
|
+
prevents. The rule is `Item`'s `resolveItemRender`, restated in this folder
|
|
58
|
+
rather than imported so a list group copied by the CLI does not drag `item`
|
|
59
|
+
along; the tests pin both. `Animated.View` rather than `View` keeps the `ref`
|
|
60
|
+
one type across both branches.
|
|
61
|
+
- **The pressable props are `Pressable`'s.** `ListGroupItemProps` extends
|
|
62
|
+
`PressableProps`, so `feedback`, `haptic`, `pressedScale` and the rest are
|
|
63
|
+
inherited rather than restated — the row owns no vocabulary of its own. A
|
|
64
|
+
static row ignores them. Only the default differs: `feedback` defaults to
|
|
65
|
+
`fade`, because a full-bleed row that scales reads as the whole card flexing
|
|
66
|
+
rather than as one row responding. A prop is only redeclared where it
|
|
67
|
+
genuinely changes, as [`Button`](../button/AGENTS.md) does with its narrower
|
|
68
|
+
union; redeclaring one unchanged just to hang a doc comment on it puts a
|
|
69
|
+
second definition in the tree that can drift.
|
|
57
70
|
- **Icons are composed, never passed as props.** `ItemPrefix` wraps its subtree
|
|
58
71
|
in an `IconDefaultsProvider` carrying the `prefixIcon` slot — a step on the
|
|
59
72
|
shared icon scale, like every other glyph in the library — and `foreground`,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Children, type ReactElement, type ReactNode, useMemo } from "react";
|
|
2
|
+
import type { AccessibilityState } from "react-native";
|
|
3
|
+
import Animated from "react-native-reanimated";
|
|
2
4
|
import { Pressable, type PressableProps } from "../pressable";
|
|
3
5
|
import { useListGroupPart } from "./list-group.context";
|
|
4
|
-
import { listGroupVariants } from "./list-group.variants";
|
|
6
|
+
import { listGroupVariants, resolveListGroupItemRender } from "./list-group.variants";
|
|
5
7
|
import { ListGroupItemContent } from "./list-group-item-content";
|
|
6
8
|
import { ListGroupItemTitle } from "./list-group-item-title";
|
|
7
9
|
|
|
@@ -19,30 +21,63 @@ export type ListGroupItemProps = Omit<PressableProps, "children" | "disabled"> &
|
|
|
19
21
|
* not a convenience — without it `<ListGroup.Item>Wi-Fi</ListGroup.Item>` would
|
|
20
22
|
* crash.
|
|
21
23
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* row
|
|
24
|
+
* Give it an `onPress` or `onLongPress` and it is a `Pressable`, announced as a
|
|
25
|
+
* button — disabled too, when it keeps the role and reports `disabled`. Leave
|
|
26
|
+
* both off and it is a plain view with the same layout, no role and no press
|
|
27
|
+
* feedback, so a static Settings row never claims to be a control.
|
|
28
|
+
* `resolveListGroupItemRender` makes that call.
|
|
29
|
+
*
|
|
30
|
+
* `feedback`, `haptic` and the rest come from `Pressable` rather than being
|
|
31
|
+
* restated here. Only the default differs: `fade`, because a full-bleed row
|
|
32
|
+
* that scales reads as the whole card flexing rather than as one row
|
|
33
|
+
* responding.
|
|
26
34
|
*/
|
|
27
35
|
export function ListGroupItem({
|
|
28
36
|
feedback = "fade",
|
|
29
37
|
isDisabled = false,
|
|
38
|
+
haptic,
|
|
39
|
+
pressedScale,
|
|
40
|
+
pressedOpacity,
|
|
41
|
+
busy,
|
|
42
|
+
asChild,
|
|
43
|
+
onPress,
|
|
44
|
+
onLongPress,
|
|
45
|
+
accessibilityState,
|
|
30
46
|
className,
|
|
31
47
|
children,
|
|
32
48
|
...props
|
|
33
49
|
}: ListGroupItemProps): ReactElement {
|
|
34
50
|
const { size } = useListGroupPart("ListGroup.Item");
|
|
35
51
|
const content = useMemo(() => wrapTextChildren(children), [children]);
|
|
52
|
+
const itemClassName = listGroupVariants({ isDisabled, size }).item({ className });
|
|
53
|
+
|
|
54
|
+
if (resolveListGroupItemRender({ isDisabled, onLongPress, onPress }) === "pressable") {
|
|
55
|
+
return (
|
|
56
|
+
<Pressable
|
|
57
|
+
accessibilityState={accessibilityState}
|
|
58
|
+
asChild={asChild}
|
|
59
|
+
busy={busy}
|
|
60
|
+
className={itemClassName}
|
|
61
|
+
disabled={isDisabled}
|
|
62
|
+
feedback={feedback}
|
|
63
|
+
haptic={haptic}
|
|
64
|
+
onLongPress={onLongPress}
|
|
65
|
+
onPress={onPress}
|
|
66
|
+
pressedOpacity={pressedOpacity}
|
|
67
|
+
pressedScale={pressedScale}
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
{content}
|
|
71
|
+
</Pressable>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const state: AccessibilityState = { ...accessibilityState, busy, disabled: isDisabled };
|
|
36
76
|
|
|
37
77
|
return (
|
|
38
|
-
<
|
|
39
|
-
className={listGroupVariants({ isDisabled, size }).item({ className })}
|
|
40
|
-
disabled={isDisabled}
|
|
41
|
-
feedback={feedback}
|
|
42
|
-
{...props}
|
|
43
|
-
>
|
|
78
|
+
<Animated.View accessibilityState={state} className={itemClassName} {...props}>
|
|
44
79
|
{content}
|
|
45
|
-
</
|
|
80
|
+
</Animated.View>
|
|
46
81
|
);
|
|
47
82
|
}
|
|
48
83
|
ListGroupItem.displayName = "DelacourUI.ListGroup.Item";
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { ICON_SIZE_TOKENS } from "../../styles/tokens";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
LIST_GROUP_SIZES,
|
|
5
|
+
LIST_GROUP_VARIANTS,
|
|
6
|
+
listGroupVariants,
|
|
7
|
+
resolveListGroupItemRender,
|
|
8
|
+
} from "./list-group.variants";
|
|
4
9
|
|
|
5
10
|
/** Pulls the horizontal padding step out of a class string — `px-4` yields 4. */
|
|
6
11
|
function paddingStep(cls: string): string | undefined {
|
|
@@ -221,3 +226,26 @@ describe("listGroupVariants icon slots", () => {
|
|
|
221
226
|
}
|
|
222
227
|
});
|
|
223
228
|
});
|
|
229
|
+
|
|
230
|
+
describe("resolveListGroupItemRender", () => {
|
|
231
|
+
const noop = () => {};
|
|
232
|
+
|
|
233
|
+
// A Settings row with only a value or a switch-for-show is not a control;
|
|
234
|
+
// announcing it as a button is a lie VoiceOver tells on every swipe.
|
|
235
|
+
test("is static without a press handler, disabled or not", () => {
|
|
236
|
+
expect(resolveListGroupItemRender({ isDisabled: false })).toBe("static");
|
|
237
|
+
expect(resolveListGroupItemRender({ isDisabled: true })).toBe("static");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("is a pressable with a handler", () => {
|
|
241
|
+
expect(resolveListGroupItemRender({ isDisabled: false, onPress: noop })).toBe("pressable");
|
|
242
|
+
expect(resolveListGroupItemRender({ isDisabled: false, onLongPress: noop })).toBe("pressable");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// A disabled action is still an action: it keeps the button role and
|
|
246
|
+
// reports `disabled`, rather than vanishing into a plain view.
|
|
247
|
+
test("stays a pressable when disabled", () => {
|
|
248
|
+
expect(resolveListGroupItemRender({ isDisabled: true, onPress: noop })).toBe("pressable");
|
|
249
|
+
expect(resolveListGroupItemRender({ isDisabled: true, onLongPress: noop })).toBe("pressable");
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -105,3 +105,27 @@ export const listGroupVariants = tv({
|
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
export type ListGroupVariantProps = VariantProps<typeof listGroupVariants>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* What a list group row renders as.
|
|
111
|
+
*
|
|
112
|
+
* - `pressable` — it has a handler: a `Pressable`, announced as a button. A
|
|
113
|
+
* disabled one keeps the role, reports `disabled`, and dims through
|
|
114
|
+
* `opacity-50`, which `Pressable` composes with its press animation.
|
|
115
|
+
* - `static` — no handler: a plain view with no role and no press feedback. A
|
|
116
|
+
* static row announcing itself as a button is a lie VoiceOver tells on every
|
|
117
|
+
* swipe.
|
|
118
|
+
*
|
|
119
|
+
* The same rule as `resolveItemRender`, restated rather than imported so the
|
|
120
|
+
* list group does not take a registry dependency on `item`.
|
|
121
|
+
*/
|
|
122
|
+
export type ListGroupItemRender = "pressable" | "static";
|
|
123
|
+
|
|
124
|
+
export function resolveListGroupItemRender(options: {
|
|
125
|
+
onPress?: () => void;
|
|
126
|
+
onLongPress?: () => void;
|
|
127
|
+
isDisabled: boolean;
|
|
128
|
+
}): ListGroupItemRender {
|
|
129
|
+
const hasHandler = options.onPress !== undefined || options.onLongPress !== undefined;
|
|
130
|
+
return hasHandler ? "pressable" : "static";
|
|
131
|
+
}
|