Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .Jules/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
## [Unreleased]

### Added
- **Mobile Skeleton Loading:** Implemented skeleton loading state for HomeScreen.
- **Features:**
- Created reusable `Skeleton` component with pulsing animation and theme support.
- Created `GroupListSkeleton` to mimic the layout of group cards.
- Replaced `ActivityIndicator` in `HomeScreen` with the skeleton loader.
- **Technical:** Created `mobile/components/ui/Skeleton.js` and `mobile/components/skeletons/GroupListSkeleton.js`.

- **Password Strength Meter:** Added a visual password strength indicator to the signup form.
- **Features:**
- Real-time strength calculation (Length, Uppercase, Lowercase, Number, Symbol).
Expand Down
11 changes: 11 additions & 0 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,17 @@ Commonly used components:

Most screens use `<View style={{ flex: 1 }}>` - consider wrapping in `SafeAreaView` for notched devices.

### Skeleton Loading Pattern

**Date:** 2026-02-09
**Context:** Improving mobile loading states

To create a skeleton loader that matches the content layout:

1. **Base Component:** Create a `Skeleton` component using `Animated` for pulsing opacity and `useTheme` for colors (`surfaceVariant`).
2. **Layout Component:** Create a composite skeleton (e.g., `GroupListSkeleton`) that uses `Card` (or other containers) to mimic the exact structure of the loaded content (including padding/margins).
3. **Integration:** Replace `ActivityIndicator` with the skeleton component in the `isLoading` state.

### Accessibility Patterns

**Date:** 2026-01-29
Expand Down
12 changes: 6 additions & 6 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@
- Impact: Native feel, users can easily refresh data
- Size: ~150 lines

- [ ] **[ux]** Complete skeleton loading for HomeScreen groups
- File: `mobile/screens/HomeScreen.js`
- Context: Replace ActivityIndicator with skeleton group cards
- Impact: Better loading experience, less jarring
- Size: ~40 lines
- Added: 2026-01-01
- [x] **[ux]** Complete skeleton loading for HomeScreen groups
- Completed: 2026-02-09
- Files modified: `mobile/screens/HomeScreen.js`, `mobile/components/ui/Skeleton.js`, `mobile/components/skeletons/GroupListSkeleton.js`
- Context: Replaced ActivityIndicator with skeleton group cards
- Impact: Better loading experience, less jarring, matches list layout
- Size: ~80 lines

- [x] **[a11y]** Complete accessibility labels for all screens
- Completed: 2026-01-29
Expand Down
38 changes: 38 additions & 0 deletions mobile/components/skeletons/GroupListSkeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { Card } from 'react-native-paper';
import Skeleton from '../ui/Skeleton';

const GroupListSkeleton = () => {
// Render 5 items to fill the screen
const items = Array.from({ length: 5 }, (_, i) => i);

return (
<View style={styles.container} accessible={true} accessibilityLabel="Loading groups">
{items.map((key) => (
<Card key={key} style={styles.card}>
<Card.Title
title={<Skeleton width={150} height={20} />}
left={(props) => (
<Skeleton width={40} height={40} borderRadius={20} />
)}
/>
<Card.Content>
<Skeleton width={200} height={16} style={{ marginTop: 4 }} />
</Card.Content>
</Card>
))}
</View>
);
};

const styles = StyleSheet.create({
container: {
padding: 16,
},
card: {
marginBottom: 16,
},
});

export default GroupListSkeleton;
47 changes: 47 additions & 0 deletions mobile/components/ui/Skeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, { useEffect, useRef } from 'react';
import { Animated, View, StyleSheet } from 'react-native';
import { useTheme } from 'react-native-paper';

const Skeleton = ({ width, height, borderRadius = 4, style }) => {
const theme = useTheme();
const opacity = useRef(new Animated.Value(0.3)).current;

useEffect(() => {
const animation = Animated.loop(
Animated.sequence([
Animated.timing(opacity, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 0.3,
duration: 1000,
useNativeDriver: true,
}),
])
);
animation.start();

return () => animation.stop();
}, [opacity]);

return (
<Animated.View
style={[
{
opacity,
width,
height,
borderRadius,
backgroundColor: theme.colors.surfaceVariant,
},
style,
]}
accessibilityRole="progressbar"
accessibilityLabel="Loading..."
/>
Comment on lines +30 to +43
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find mobile -name "Skeleton.js" -type f

Repository: Devasy/splitwiser

Length of output: 93


🏁 Script executed:

cat -n mobile/components/ui/Skeleton.js

Repository: Devasy/splitwiser

Length of output: 1476


🏁 Script executed:

find mobile -name "*GroupList*" -o -name "*Skeleton*" | head -20

Repository: Devasy/splitwiser

Length of output: 142


🏁 Script executed:

cat -n mobile/components/skeletons/GroupListSkeleton.js

Repository: Devasy/splitwiser

Length of output: 1315


🌐 Web query:

React Native accessible={false} importantForAccessibility="no" accessibilityElementsHidden

💡 Result:

  • accessible={false}: only means the View itself is not treated as a single “accessibility element” (iOS: isAccessibilityElement, Android: focusable). It does not automatically remove the view (or its children) from the screen reader’s accessibility tree. [1]

  • importantForAccessibility="no" (Android): tells Android accessibility services the view itself is not important (won’t be reported), but it does not hide descendants. To hide a view and all children on Android, use importantForAccessibility="no-hide-descendants". [1][2]

  • accessibilityElementsHidden (iOS): when true, hides the view and any accessibility elements it contains from VoiceOver. This is essentially the iOS equivalent of Android’s importantForAccessibility="no-hide-descendants". [1][2]

Practical mapping (hide subtree from screen readers)

<View
  accessibilityElementsHidden={true}          // iOS: hide subtree
  importantForAccessibility="no-hide-descendants" // Android: hide subtree
>
  ...
</View>

Using accessible={false} and/or importantForAccessibility="no" is not sufficient if your goal is “screen reader should ignore this whole section.” [1][2]

Sources: React Native Accessibility docs [1]; React Native Testing Library notes on what actually makes elements inaccessible [2].


🌐 Web query:

React Native accessibility best practices skeleton loading screen reader announcement

💡 Result:

Best practices (React Native) for an accessible skeleton loading state + screen reader announcement

  1. Don’t expose the skeleton UI to screen readers

    • Treat skeleton blocks as decorative placeholders and hide them from accessibility (so they don’t become dozens of meaningless focus stops). Common guidance is to hide skeletons and instead expose one loading message/state on a parent region. [4], [7]
  2. Expose a single “loading” state on the real content container

    • Put the container that will eventually hold the loaded content into a “busy/loading” state:
      • accessibilityState={{ busy: true }} while loading, then busy: false when done. (RN supports busy in accessibilityState.) [1]
    • Optionally pair with a meaningful role/label such as:
      • accessibilityRole="progressbar" (or a status-like pattern) and accessibilityLabel="Loading results" so the purpose is clear. [1], [2]
  3. Announce loading changes via “live region” where possible (Android)

    • On Android, RN supports accessibilityLiveRegion="polite" / "assertive" to announce updates when text changes. Prefer polite for loading. [1]
    • Note: accessibilityLiveRegion is Android-focused and has had consistency gaps across platforms. [9]
  4. Use direct announcements sparingly

    • AccessibilityInfo.announceForAccessibility(...) exists, but some platform docs caution against relying on forced announcements and recommend semantic/live-region approaches where possible. [2]

Minimal pattern (container announces; skeleton hidden)

import {AccessibilityInfo, Platform, View, Text} from "react-native";

function Screen({loading}: {loading: boolean}) {
  // Optional: only when loading is long enough / user-triggered
  // if (loading) AccessibilityInfo.announceForAccessibility("Loading content");
  // else AccessibilityInfo.announceForAccessibility("Content loaded");

  return (
    <View
      accessible
      accessibilityState={{ busy: loading }}
      accessibilityLabel={loading ? "Loading content" : "Content"}
    >
      {/* Android: live region announcement when this text changes */}
      <Text accessibilityLiveRegion={Platform.OS === "android" ? "polite" : undefined}>
        {loading ? "Loading…" : ""}
      </Text>

      {loading ? (
        // Skeleton UI: hide from screen readers (decorative)
        <View importantForAccessibility="no-hide-descendants">
          {/* skeleton blocks here */}
        </View>
      ) : (
        <View>{/* real content here */}</View>
      )}
    </View>
  );
}

Key idea: one clear, non-spammy loading message/state on the container (busy + label / live update), while keeping skeleton visuals out of the accessibility tree. [1], [4], [7]


Remove individual accessibility announcements from decorative skeletons.

Each skeleton element exposes itself as a progressbar with "Loading..." label. With ~15 skeleton elements across the list (5 cards × 3 skeletons each), this creates excessive screen reader noise. The container already provides a single "Loading groups" announcement, which is sufficient.

Remove the accessibility role and label from individual skeletons since they are decorative placeholders:

🔧 Suggested adjustment
     <Animated.View
       style={[
         {
           opacity,
           width,
           height,
           borderRadius,
           backgroundColor: theme.colors.surfaceVariant,
         },
         style,
       ]}
-      accessibilityRole="progressbar"
-      accessibilityLabel="Loading..."
+      importantForAccessibility="no-hide-descendants"
+      accessibilityElementsHidden={true}
     />

Note: If simply removing the role/label is preferred, use both props above for proper cross-platform support (Android + iOS).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/components/ui/Skeleton.js` around lines 30 - 43, The Skeleton
component's Animated.View currently sets accessibilityRole="progressbar" and
accessibilityLabel="Loading..." which causes excessive screen reader
announcements; remove both accessibilityRole and accessibilityLabel from the
Animated.View in mobile/components/ui/Skeleton.js (the Animated.View instance
rendering the skeleton with style including
opacity/width/height/borderRadius/backgroundColor) so individual skeletons are
treated as decorative and rely on the container's single "Loading groups"
announcement for accessibility.

);
};

export default Skeleton;
11 changes: 2 additions & 9 deletions mobile/screens/HomeScreen.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useContext, useEffect, useState } from "react";
import { Alert, FlatList, RefreshControl, StyleSheet, View } from "react-native";
import {
ActivityIndicator,
Appbar,
Avatar,
Modal,
Expand All @@ -15,6 +14,7 @@ import HapticCard from '../components/ui/HapticCard';
import { HapticAppbarAction } from '../components/ui/HapticAppbar';
import * as Haptics from "expo-haptics";
import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
import GroupListSkeleton from '../components/skeletons/GroupListSkeleton';
import { AuthContext } from "../context/AuthContext";
import { formatCurrency, getCurrencySymbol } from "../utils/currency";

Expand Down Expand Up @@ -257,9 +257,7 @@ const HomeScreen = ({ navigation }) => {
</Appbar.Header>

{isLoading ? (
<View style={styles.loaderContainer}>
<ActivityIndicator size="large" />
</View>
<GroupListSkeleton />
) : (
<FlatList
data={groups}
Expand Down Expand Up @@ -289,11 +287,6 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
loaderContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
list: {
padding: 16,
},
Expand Down
Loading