Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ After `release.yml` is on `master` and before merging the first release pull req
```

4. Keep Actions' default token read-only and allow Actions to create pull requests. The release job grants only the three write permissions it needs: contents, issues, and pull requests.
5. Require `Check`, `React Native (minimum)`, `React Native (current)`, and `Dependency review` on `master`, and protect `v*` tags from deletion or force updates.
5. Require `Check`, `React Native (minimum)`, `React Native (current)`, `Expo Web`, and `Dependency review` on `master`, and protect `v*` tags from deletion or force updates.

The first automated pull request may require a maintainer to approve its CI run because it is opened with `GITHUB_TOKEN`.

Expand Down
62 changes: 52 additions & 10 deletions .github/fixtures/consumer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,65 @@
import { useState } from "react";
import { Text } from "react-native";
import { Rating } from "react-native-rating";
import type { RatingRenderItemProps } from "react-native-rating";
import { Rating, RatingDisplay, RatingScale } from "react-native-rating";
import type {
RatingRenderItemProps,
RatingScaleItem,
RatingScaleRenderItemProps,
} from "react-native-rating";

type Sentiment = "negative" | "neutral" | "positive";

const sentimentItems = [
{ content: "😞", label: "Negative", value: "negative" },
{ content: "😐", label: "Neutral", value: "neutral" },
{ content: "🙂", label: "Positive", value: "positive" },
] as const satisfies readonly RatingScaleItem<Sentiment>[];

const renderItem = ({ fill, index }: RatingRenderItemProps) => (
<Text>{`${index + 1}:${fill}`}</Text>
);

const renderScaleItem = ({
content,
label,
selected,
}: RatingScaleRenderItemProps<Sentiment>) => (
<Text>{`${content ?? label}:${selected}`}</Text>
);

// @ts-expect-error Interactive accessibility ownership cannot be overridden.
const _hiddenInteractiveRating = <Rating accessible />;

export const RatingConsumer = () => {
const [value, setValue] = useState(3.5);
const [sentiment, setSentiment] = useState<Sentiment | null>(null);

return (
<Rating
accessibilityLabel="Review score"
max={5}
onChange={setValue}
renderItem={renderItem}
step={0.5}
value={value}
/>
<>
<Rating
accessibilityLabel="Review score"
interactionMode="tap-and-drag"
max={5}
onChange={setValue}
renderItem={renderItem}
step={0.5}
value={value}
/>
<RatingDisplay
accessibilityLabel="Average review score"
decorative
renderItem={renderItem}
value={4.37}
/>
<RatingScale
accessibilityLabel="Sentiment"
focusStyle={{ outlineColor: "#0F766E" }}
itemExtent={72}
items={sentimentItems}
onChange={setSentiment}
renderItem={renderScaleItem}
value={sentiment}
/>
</>
);
};
137 changes: 137 additions & 0 deletions .github/scripts/check-expo-web.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/usr/bin/env bash

set -euo pipefail

: "${PACKAGE_TARBALL:?PACKAGE_TARBALL is required}"

example_directory="$(mktemp -d)"
trap 'rm -rf -- "$example_directory"' EXIT

cp -R example/. "$example_directory"

PACKAGE_TARBALL="$(realpath "$PACKAGE_TARBALL")" \
EXAMPLE_DIRECTORY="$example_directory" \
node --input-type=module <<'NODE'
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";

const packagePath = join(
process.env.EXAMPLE_DIRECTORY,
"package.json"
);
const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
packageJson.dependencies["react-native-rating"] =
`file:${process.env.PACKAGE_TARBALL}`;
await writeFile(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
NODE

(
cd "$example_directory"
bun install --ignore-scripts
bun run typecheck
bun run export:web
test -s dist/index.html

mv node_modules/react-native node_modules/react-native-native
ln -s react-native-web node_modules/react-native

node --input-type=module <<'NODE'
import assert from "node:assert/strict";

const diagnostics = [];
const originalError = console.error;
const originalWarn = console.warn;
console.error = (...messages) => {
diagnostics.push(messages.join(" "));
};
console.warn = (...messages) => {
diagnostics.push(messages.join(" "));
};

try {
const { createElement } = await import("react");
const { renderToStaticMarkup } = await import("react-dom/server");
const { StyleSheet } = await import("react-native");
const { Rating, RatingDisplay, RatingScale } =
await import("react-native-rating");

const slider = renderToStaticMarkup(
createElement(Rating, {
animated: false,
interactionMode: "tap-and-drag",
step: 0.5,
testID: "rating",
value: 2.5,
})
);
const decorative = renderToStaticMarkup(
createElement(RatingDisplay, {
decorative: true,
testID: "display",
value: 4.37,
})
);
const scale = renderToStaticMarkup(
createElement(RatingScale, {
animated: false,
items: [
{ label: "Negative", value: -1 },
{ label: "Neutral", value: 0 },
{ label: "Positive", value: 1 },
],
testID: "scale",
value: 0,
})
);
const styleSheet = StyleSheet.getSheet();
const getTag = (markup, testID) => {
const tag = markup.match(
new RegExp(`<[^>]*data-testid="${testID}"[^>]*>`, "u")
)?.[0];

assert.ok(tag, `Missing rendered element: ${testID}`);
return tag;
};
const hasPointerClass = (
markup,
testID,
rootPointerEvents,
childPointerEvents
) => {
const tag = getTag(markup, testID);
const classNames = tag.match(/class="([^"]+)"/u)?.[1]?.split(/\s+/u) ?? [];

return classNames.some(
(className) =>
styleSheet.textContent.includes(
`.${className}{pointer-events:${rootPointerEvents}!important;}`
) &&
styleSheet.textContent.includes(
`.${className}>* {pointer-events:${childPointerEvents};}`
)
);
};

assert.match(slider, /role="slider"/u);
assert.match(slider, /aria-valuenow="2.5"/u);
assert.doesNotMatch(slider, /pointer-events:box-only/u);
assert.equal(
hasPointerClass(slider, "rating-control", "auto", "none"),
true
);
assert.match(decorative, /aria-hidden="true"/u);
assert.doesNotMatch(decorative, /role="img"/u);
assert.equal(
hasPointerClass(decorative, "display-control", "none", "none"),
true
);
assert.match(scale, /role="slider"/u);
assert.match(scale, /aria-valuenow="2"/u);
assert.match(scale, /aria-valuetext="Neutral"/u);
assert.deepEqual(diagnostics, []);
} finally {
console.error = originalError;
console.warn = originalWarn;
}
NODE
)
27 changes: 26 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
include:
- label: minimum
react: 19.1.1
react-native: 0.82.1
react-native: 0.82.0
react-types: 19.1.17
- label: current
react: current
Expand Down Expand Up @@ -105,6 +105,31 @@ jobs:
REACT_VERSION: ${{ matrix.react }}
run: bash .github/scripts/check-package.sh

expo-web:
name: Expo Web
needs: check
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Check out Expo example
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install toolchain
uses: jdx/mise-action@9e7f7633ff6f6d6048a9418a68d48f288f50eb14 # v4.2.3
with:
cache: true
version: 2026.7.15
- name: Download checked package
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: package
path: ${{ runner.temp }}/package
- name: Type-check, export, and server-render Expo Web
env:
PACKAGE_TARBALL: ${{ runner.temp }}/package/react-native-rating.tgz
run: bash .github/scripts/check-expo-web.sh

dependency-review:
name: Dependency review
if: github.event_name == 'pull_request'
Expand Down
Loading