Add focus handling for the grid

This commit is contained in:
Zoe Roux 2023-01-13 02:25:14 +09:00
parent 35a3c4c4bf
commit a8a8b45f4a
No known key found for this signature in database
GPG Key ID: B2AB52A2636E5C46
10 changed files with 65 additions and 50 deletions

View File

@ -46,7 +46,7 @@
"react-native-screens": "~3.18.0", "react-native-screens": "~3.18.0",
"react-native-svg": "13.4.0", "react-native-svg": "13.4.0",
"react-native-video": "alpha", "react-native-video": "alpha",
"yoshiki": "0.4.5" "yoshiki": "1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19.3", "@babel/core": "^7.19.3",

View File

@ -36,7 +36,7 @@
"react-native-web": "^0.18.10", "react-native-web": "^0.18.10",
"solito": "^2.0.5", "solito": "^2.0.5",
"superjson": "^1.11.0", "superjson": "^1.11.0",
"yoshiki": "0.4.5", "yoshiki": "1.2.0",
"zod": "^3.19.1" "zod": "^3.19.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -19,7 +19,7 @@
*/ */
import { forwardRef, ReactNode } from "react"; import { forwardRef, ReactNode } from "react";
import { Pressable, TextProps, View, PressableProps } from "react-native"; import { Pressable, TextProps, View, PressableProps, Platform } from "react-native";
import { LinkCore, TextLink } from "solito/link"; import { LinkCore, TextLink } from "solito/link";
import { useTheme, useYoshiki } from "yoshiki/native"; import { useTheme, useYoshiki } from "yoshiki/native";
import { alpha } from "./themes"; import { alpha } from "./themes";
@ -59,7 +59,10 @@ export const PressableFeedback = forwardRef<View, PressableProps>(function _Feed
return ( return (
<Pressable <Pressable
ref={ref} ref={ref}
android_ripple={{ foreground: true, color: alpha(theme.contrast, 0.5) as any }} // TODO: Enable ripple on tv. Waiting for https://github.com/react-native-tvos/react-native-tvos/issues/440
{...(Platform.isTV
? {}
: { android_ripple: { foreground: true, color: alpha(theme.contrast, 0.5) as any } })}
{...props} {...props}
> >
{children} {children}

View File

@ -19,10 +19,11 @@
*/ */
import { useWindowDimensions } from "react-native"; import { useWindowDimensions } from "react-native";
import { AtLeastOne, Breakpoints as YoshikiBreakpoint } from "yoshiki/dist/type"; import { Breakpoints as YoshikiBreakpoint } from "yoshiki/dist/type";
import { isBreakpoints } from "yoshiki/dist/utils"; import { isBreakpoints } from "yoshiki/dist/utils";
import { breakpoints } from "yoshiki/native"; import { breakpoints } from "yoshiki/native";
type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];
export type Breakpoint<T> = T | AtLeastOne<YoshikiBreakpoint<T>>; export type Breakpoint<T> = T | AtLeastOne<YoshikiBreakpoint<T>>;
// copied from yoshiki. // copied from yoshiki.

View File

@ -18,8 +18,16 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Platform } from "react-native";
import { px } from "yoshiki/native"; import { px } from "yoshiki/native";
export const ts = (spacing: number) => { export const ts = (spacing: number) => {
return px(spacing * 8); return px(spacing * 8);
}; };
export const focusReset: object =
Platform.OS === "web"
? {
boxShadow: "unset",
}
: {};

View File

@ -8,7 +8,7 @@
"@kyoo/primitives": "workspace:^" "@kyoo/primitives": "workspace:^"
}, },
"devDependencies": { "devDependencies": {
"@shopify/flash-list": "^1.4.0", "@shopify/flash-list": "1.3.1",
"@types/react": "^18.0.25", "@types/react": "^18.0.25",
"typescript": "^4.9.3" "typescript": "^4.9.3"
}, },

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Link, Skeleton, Poster, ts, P, SubP } from "@kyoo/primitives"; import { Link, Skeleton, Poster, ts, focusReset, P, SubP } from "@kyoo/primitives";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { percent, px, Stylable, useYoshiki } from "yoshiki/native"; import { percent, px, Stylable, useYoshiki } from "yoshiki/native";
import { Layout, WithLoading } from "../fetch"; import { Layout, WithLoading } from "../fetch";
@ -29,25 +29,49 @@ export const ItemGrid = ({
subtitle, subtitle,
poster, poster,
isLoading, isLoading,
hasTVPreferredFocus,
...props ...props
}: WithLoading<{ }: WithLoading<{
href: string; href: string;
name: string; name: string;
subtitle?: string; subtitle?: string;
poster?: string | null; poster?: string | null;
hasTVPreferredFocus?: boolean;
}> & }> &
Stylable<"text">) => { Stylable<"text">) => {
const { css } = useYoshiki(); const { css } = useYoshiki("grid");
return ( return (
<Link <Link
href={href ?? ""} href={href ?? ""}
focusable={hasTVPreferredFocus || !isLoading}
accessible={hasTVPreferredFocus || !isLoading}
{...(Platform.isTV
? {
hasTVPreferredFocus: hasTVPreferredFocus,
}
: {})}
{...css( {...css(
[ [
{ {
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
m: { xs: ts(1), sm: ts(2) }, m: { xs: ts(1), sm: ts(4) },
child: {
poster: {
borderColor: "transparent",
borderWidth: px(4),
},
},
fover: {
self: focusReset,
poster: {
borderColor: (theme) => theme.appbar,
},
title: {
textDecorationLine: "underline",
},
},
}, },
// We leave no width on native to fill the list's grid. // We leave no width on native to fill the list's grid.
Platform.OS === "web" && { Platform.OS === "web" && {
@ -59,10 +83,16 @@ export const ItemGrid = ({
props, props,
)} )}
> >
<Poster src={poster} alt={name} isLoading={isLoading} layout={{ width: percent(100) }} /> <Poster
src={poster}
alt={name}
isLoading={isLoading}
layout={{ width: percent(100) }}
{...css("poster")}
/>
<Skeleton> <Skeleton>
{isLoading || ( {isLoading || (
<P numberOfLines={1} {...css({ marginY: 0, textAlign: "center" })}> <P numberOfLines={1} {...css([{ marginY: 0, textAlign: "center" }, "title"])}>
{name} {name}
</P> </P>
)} )}

View File

@ -93,7 +93,7 @@ export const BrowsePage: QueryPage<{ slug?: string }> = ({ slug }) => {
placeholderCount={15} placeholderCount={15}
layout={LayoutComponent.layout} layout={LayoutComponent.layout}
> >
{(item) => <LayoutComponent {...itemMap(item)} />} {(item, i) => <LayoutComponent {...itemMap(item)} hasTVPreferredFocus={i === 0} />}
</InfiniteFetch> </InfiniteFetch>
</> </>
); );

View File

@ -66,9 +66,10 @@ export const InfiniteFetch = <Data,>({
return <EmptyView message={empty} />; return <EmptyView message={empty} />;
} }
const placeholders = [ const count = items ? numColumns - (items.length % numColumns) : placeholderCount;
...Array(items ? numColumns - (items.length % numColumns) + numColumns : placeholderCount), const placeholders = [...Array(count === 0 ? numColumns : count)].map(
].map((_, i) => ({ id: `gen${i}`, isLoading: true } as Data)); (_, i) => ({ id: `gen${i}`, isLoading: true } as Data),
);
return ( return (
<FlashList <FlashList

View File

@ -2251,7 +2251,7 @@ __metadata:
dependencies: dependencies:
"@kyoo/models": "workspace:^" "@kyoo/models": "workspace:^"
"@kyoo/primitives": "workspace:^" "@kyoo/primitives": "workspace:^"
"@shopify/flash-list": ^1.4.0 "@shopify/flash-list": 1.3.1
"@types/react": ^18.0.25 "@types/react": ^18.0.25
typescript: ^4.9.3 typescript: ^4.9.3
peerDependencies: peerDependencies:
@ -3227,20 +3227,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@shopify/flash-list@npm:^1.4.0":
version: 1.4.0
resolution: "@shopify/flash-list@npm:1.4.0"
dependencies:
recyclerlistview: 4.2.0
tslib: 2.4.0
peerDependencies:
"@babel/runtime": "*"
react: "*"
react-native: "*"
checksum: c6510b0d6ae6404fe92ede0c918ba184bc2b27ed39c627eebad16a6542792cb34e750e2004e1a9ce165f9d729f1af0555cba1e4c224fd52bfd2a600fdc9e2a65
languageName: node
linkType: hard
"@sideway/address@npm:^4.1.3": "@sideway/address@npm:^4.1.3":
version: 4.1.4 version: 4.1.4
resolution: "@sideway/address@npm:4.1.4" resolution: "@sideway/address@npm:4.1.4"
@ -10416,7 +10402,7 @@ __metadata:
react-native-svg-transformer: ^1.0.0 react-native-svg-transformer: ^1.0.0
react-native-video: alpha react-native-video: alpha
typescript: ^4.6.3 typescript: ^4.6.3
yoshiki: 0.4.5 yoshiki: 1.2.0
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@ -11996,20 +11982,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"recyclerlistview@npm:4.2.0":
version: 4.2.0
resolution: "recyclerlistview@npm:4.2.0"
dependencies:
lodash.debounce: 4.0.8
prop-types: 15.8.1
ts-object-utils: 0.0.5
peerDependencies:
react: ">= 15.2.1"
react-native: ">= 0.30.0"
checksum: 6cba6a99fb487067c509112b94e3d4d3905d782bbcb7af2cffbd57c601a4650d670e4eee5fec18d195d58ff6ec01a47288c5510379a2f37da3c5fc0a58860441
languageName: node
linkType: hard
"regenerate-unicode-properties@npm:^10.1.0": "regenerate-unicode-properties@npm:^10.1.0":
version: 10.1.0 version: 10.1.0
resolution: "regenerate-unicode-properties@npm:10.1.0" resolution: "regenerate-unicode-properties@npm:10.1.0"
@ -14172,7 +14144,7 @@ __metadata:
superjson: ^1.11.0 superjson: ^1.11.0
typescript: ^4.9.3 typescript: ^4.9.3
webpack: ^5.75.0 webpack: ^5.75.0
yoshiki: 0.4.5 yoshiki: 1.2.0
zod: ^3.19.1 zod: ^3.19.1
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@ -14547,9 +14519,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"yoshiki@npm:0.4.5": "yoshiki@npm:1.2.0":
version: 0.4.5 version: 1.2.0
resolution: "yoshiki@npm:0.4.5" resolution: "yoshiki@npm:1.2.0"
dependencies: dependencies:
"@types/node": 18.x.x "@types/node": 18.x.x
"@types/react": 18.x.x "@types/react": 18.x.x
@ -14564,7 +14536,7 @@ __metadata:
optional: true optional: true
react-native-web: react-native-web:
optional: true optional: true
checksum: 0b2e6576ab0ddf8730da7b38feaa84943728f3c28e07d7472e080da5a9941513a6624dac529918f0efab2bd4bdaac9e4f3dba002f98253a3e26c6be5b32c4e63 checksum: 1ef4bc33563bcf344689a5bfbdc4da1636b99552fcff041ada8fa79224c6c3fac2530a890bf6980981fb7aed9cc4e31b89feb1d0bde920179039fc573935ab42
languageName: node languageName: node
linkType: hard linkType: hard