mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
- Replace root `eslint-plugin-lingui` jsPlugin with `@e18e/eslint-plugin`; move lingui rules into per-app `.oxlintrc.json` overrides for native and web
- Add `jsx-a11y`, `react-hooks-js` (native), and expanded lingui rule sets to per-app configs
- Add `oxc/no-barrel-file` warning at threshold 0 to root config
- Fix `e18e/prefer-url-canparse`: replace `try { new URL() } catch` with `URL.canParse()` in server-url screen and server lib
- Fix `e18e/prefer-timer-args`: pass callback args directly to `setTimeout` instead of wrapping in arrow functions (use-debounce, integration-card)
- Fix `e18e/prefer-static-regex`: hoist `/\/+$/` to module-level constant in server-url screen
- Fix `react-hooks-js/refs` and `react-hooks-js/set-state-in-effect`: convert `useRef` tracking patterns to `useState` + render-time derived updates in `use-server-connection`, `expandable-text`, and settings screen
- Fix `react-hooks-js/immutability`: extract stable palette sub-values before `useMemo` deps in title detail screen
- Apply `e18e` and other rule fixes across core, tmdb, web components, and i18n packages
78 lines
2.2 KiB
TypeScript
78 lines
2.2 KiB
TypeScript
import { useLingui } from "@lingui/react/macro";
|
|
import { useCallback, useState } from "react";
|
|
import type { NativeSyntheticEvent, TextLayoutEventData } from "react-native";
|
|
import { Platform, Pressable, View } from "react-native";
|
|
|
|
import { Text } from "@/components/ui/text";
|
|
|
|
export function ExpandableText({
|
|
text,
|
|
maxLines = 3,
|
|
actionColor,
|
|
}: {
|
|
text: string;
|
|
maxLines?: number;
|
|
actionColor?: string;
|
|
}) {
|
|
const { t } = useLingui();
|
|
const [prevText, setPrevText] = useState(text);
|
|
const [expandedState, setExpanded] = useState(false);
|
|
const [needsTruncationState, setNeedsTruncation] = useState(false);
|
|
|
|
let expanded: boolean;
|
|
let needsTruncation: boolean;
|
|
|
|
if (prevText !== text) {
|
|
setPrevText(text);
|
|
setExpanded(false);
|
|
setNeedsTruncation(false);
|
|
expanded = false;
|
|
needsTruncation = false;
|
|
} else {
|
|
expanded = expandedState;
|
|
needsTruncation = needsTruncationState;
|
|
}
|
|
|
|
const onMeasureLayout = useCallback(
|
|
(e: NativeSyntheticEvent<TextLayoutEventData>) => {
|
|
setNeedsTruncation(e.nativeEvent.lines.length > maxLines);
|
|
},
|
|
[maxLines],
|
|
);
|
|
|
|
return (
|
|
<View>
|
|
{/* Hidden text without numberOfLines to measure true line count */}
|
|
<Text
|
|
onTextLayout={onMeasureLayout}
|
|
className="pointer-events-none absolute text-sm leading-relaxed opacity-0"
|
|
>
|
|
{text}
|
|
</Text>
|
|
{/* selectable on Android uses EditText internally, which ignores numberOfLines and scrolls instead */}
|
|
<Text
|
|
selectable={Platform.OS === "ios"}
|
|
numberOfLines={expanded ? undefined : maxLines}
|
|
className="text-foreground text-sm leading-relaxed"
|
|
>
|
|
{text}
|
|
</Text>
|
|
{needsTruncation && (
|
|
<Pressable
|
|
onPress={() => setExpanded(!expanded)}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={expanded ? t`Show less` : t`Show more`}
|
|
className="mt-1"
|
|
>
|
|
<Text
|
|
className="text-primary font-sans text-sm font-medium"
|
|
style={actionColor ? { color: actionColor } : undefined}
|
|
>
|
|
{expanded ? t`Show less` : t`Show more`}
|
|
</Text>
|
|
</Pressable>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|