@oxy.so/bloom 4.18.1 → 4.19.1
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/docs/adoption-matrix.mdx +2 -2
- package/docs/overlay.mdx +11 -0
- package/docs/portal.mdx +37 -0
- package/docs/rating.mdx +19 -2
- package/lib/commonjs/bottom-sheet/BottomSheet.web.js +1 -0
- package/lib/commonjs/bottom-sheet/BottomSheet.web.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +4 -1
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +7 -0
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/input-otp/InputOtp.js +13 -3
- package/lib/commonjs/input-otp/InputOtp.js.map +1 -1
- package/lib/commonjs/overlay/Overlay.js +4 -0
- package/lib/commonjs/overlay/Overlay.js.map +1 -1
- package/lib/commonjs/overlay/OverlayInertBoundary.js +78 -0
- package/lib/commonjs/overlay/OverlayInertBoundary.js.map +1 -0
- package/lib/commonjs/overlay/index.js +14 -0
- package/lib/commonjs/overlay/index.js.map +1 -1
- package/lib/commonjs/overlay/modal-registry.js +104 -0
- package/lib/commonjs/overlay/modal-registry.js.map +1 -0
- package/lib/commonjs/overlay/use-modal-overlay-active.js +33 -0
- package/lib/commonjs/overlay/use-modal-overlay-active.js.map +1 -0
- package/lib/commonjs/portal/index.js +7 -0
- package/lib/commonjs/portal/index.js.map +1 -1
- package/lib/commonjs/portal/index.web.js +7 -0
- package/lib/commonjs/portal/index.web.js.map +1 -1
- package/lib/commonjs/rating/Rating.js +33 -2
- package/lib/commonjs/rating/Rating.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheet.web.js +1 -0
- package/lib/module/bottom-sheet/BottomSheet.web.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +4 -1
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/input-otp/InputOtp.js +13 -3
- package/lib/module/input-otp/InputOtp.js.map +1 -1
- package/lib/module/overlay/Overlay.js +4 -0
- package/lib/module/overlay/Overlay.js.map +1 -1
- package/lib/module/overlay/OverlayInertBoundary.js +73 -0
- package/lib/module/overlay/OverlayInertBoundary.js.map +1 -0
- package/lib/module/overlay/index.js +2 -0
- package/lib/module/overlay/index.js.map +1 -1
- package/lib/module/overlay/modal-registry.js +96 -0
- package/lib/module/overlay/modal-registry.js.map +1 -0
- package/lib/module/overlay/use-modal-overlay-active.js +28 -0
- package/lib/module/overlay/use-modal-overlay-active.js.map +1 -0
- package/lib/module/portal/index.js +4 -0
- package/lib/module/portal/index.js.map +1 -1
- package/lib/module/portal/index.web.js +4 -0
- package/lib/module/portal/index.web.js.map +1 -1
- package/lib/module/rating/Rating.js +32 -2
- package/lib/module/rating/Rating.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheet.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts +2 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +1 -1
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/input-otp/InputOtp.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/Overlay.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/OverlayInertBoundary.d.ts +6 -0
- package/lib/typescript/commonjs/overlay/OverlayInertBoundary.d.ts.map +1 -0
- package/lib/typescript/commonjs/overlay/index.d.ts +3 -1
- package/lib/typescript/commonjs/overlay/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/modal-registry.d.ts +53 -0
- package/lib/typescript/commonjs/overlay/modal-registry.d.ts.map +1 -0
- package/lib/typescript/commonjs/overlay/types.d.ts +21 -3
- package/lib/typescript/commonjs/overlay/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/use-modal-overlay-active.d.ts +7 -0
- package/lib/typescript/commonjs/overlay/use-modal-overlay-active.d.ts.map +1 -0
- package/lib/typescript/commonjs/portal/index.d.ts +2 -0
- package/lib/typescript/commonjs/portal/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/portal/index.web.d.ts +2 -0
- package/lib/typescript/commonjs/portal/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/rating/Rating.d.ts +9 -1
- package/lib/typescript/commonjs/rating/Rating.d.ts.map +1 -1
- package/lib/typescript/commonjs/rating/types.d.ts +9 -0
- package/lib/typescript/commonjs/rating/types.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheet.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts +2 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +1 -1
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/input-otp/InputOtp.d.ts.map +1 -1
- package/lib/typescript/module/overlay/Overlay.d.ts.map +1 -1
- package/lib/typescript/module/overlay/OverlayInertBoundary.d.ts +6 -0
- package/lib/typescript/module/overlay/OverlayInertBoundary.d.ts.map +1 -0
- package/lib/typescript/module/overlay/index.d.ts +3 -1
- package/lib/typescript/module/overlay/index.d.ts.map +1 -1
- package/lib/typescript/module/overlay/modal-registry.d.ts +53 -0
- package/lib/typescript/module/overlay/modal-registry.d.ts.map +1 -0
- package/lib/typescript/module/overlay/types.d.ts +21 -3
- package/lib/typescript/module/overlay/types.d.ts.map +1 -1
- package/lib/typescript/module/overlay/use-modal-overlay-active.d.ts +7 -0
- package/lib/typescript/module/overlay/use-modal-overlay-active.d.ts.map +1 -0
- package/lib/typescript/module/portal/index.d.ts +2 -0
- package/lib/typescript/module/portal/index.d.ts.map +1 -1
- package/lib/typescript/module/portal/index.web.d.ts +2 -0
- package/lib/typescript/module/portal/index.web.d.ts.map +1 -1
- package/lib/typescript/module/rating/Rating.d.ts +9 -1
- package/lib/typescript/module/rating/Rating.d.ts.map +1 -1
- package/lib/typescript/module/rating/types.d.ts +9 -0
- package/lib/typescript/module/rating/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/bottom-sheet/BottomSheet.web.tsx +6 -2
- package/src/dialog/Dialog.web.tsx +7 -4
- package/src/index.ts +1 -0
- package/src/index.web.ts +1 -0
- package/src/input-otp/InputOtp.tsx +11 -2
- package/src/overlay/Overlay.tsx +4 -0
- package/src/overlay/OverlayInertBoundary.tsx +72 -0
- package/src/overlay/index.ts +3 -1
- package/src/overlay/modal-registry.ts +101 -0
- package/src/overlay/types.ts +22 -3
- package/src/overlay/use-modal-overlay-active.ts +31 -0
- package/src/portal/index.ts +5 -0
- package/src/portal/index.web.ts +5 -0
- package/src/rating/Rating.tsx +37 -2
- package/src/rating/types.ts +9 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/portal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,YAAY,EACZ,MAAM,GACP,MAAM,UAAU,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/portal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,YAAY,EACZ,MAAM,GACP,MAAM,UAAU,CAAC;AAIlB,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AACvE,YAAY,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/portal/index.web.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,cAAc,EACd,YAAY,EACZ,MAAM,GACP,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/portal/index.web.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,cAAc,EACd,YAAY,EACZ,MAAM,GACP,MAAM,cAAc,CAAC;AAItB,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AACvE,YAAY,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -2,9 +2,17 @@ import React from 'react';
|
|
|
2
2
|
import type { RatingProps } from './types';
|
|
3
3
|
/** 5 → "5.0", 4.9 → "4.9", 4.923 → "4.92". */
|
|
4
4
|
export declare function formatRatingValue(value: number | string): string;
|
|
5
|
+
/**
|
|
6
|
+
* The number a drawn rating value stands for, for the `stars` fill: a number
|
|
7
|
+
* is itself; a string is read after dropping bidi/format characters, with a
|
|
8
|
+
* comma or Arabic decimal separator (U+066B) accepted as the decimal point and
|
|
9
|
+
* Arabic-Indic digits accepted — so "4,5" wrapped in FSI…PDI is 4.5. Otherwise it is
|
|
10
|
+
* `Number()`'s reading, so anything unreadable is `NaN` (no fill).
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseRatingValue(value: number | string): number;
|
|
5
13
|
/** How much of star `index` (0-based) a rating fills, `0..1`. */
|
|
6
14
|
export declare function starFill(rating: number, index: number): number;
|
|
7
|
-
declare function RatingComponent({ value, count, countStyle, reviewsLabel, newLabel, size, variant, color, starColor, countColor, emptyStarColor, accessibilityLabel, style, testID, }: RatingProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
declare function RatingComponent({ value, count, countStyle, reviewsLabel, newLabel, size, variant, color, starColor, countColor, emptyStarColor, fillValue, accessibilityLabel, style, testID, }: RatingProps): import("react/jsx-runtime").JSX.Element;
|
|
8
16
|
export declare const Rating: React.MemoExoticComponent<typeof RatingComponent>;
|
|
9
17
|
export {};
|
|
10
18
|
//# sourceMappingURL=Rating.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Rating.d.ts","sourceRoot":"","sources":["../../../../src/rating/Rating.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAe,MAAM,OAAO,CAAC;AAQpC,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"Rating.d.ts","sourceRoot":"","sources":["../../../../src/rating/Rating.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAe,MAAM,OAAO,CAAC;AAQpC,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,SAAS,CAAC;AAkCvD,8CAA8C;AAC9C,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAIhE;AAkBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAS/D;AAED,iEAAiE;AACjE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAG9D;AAkDD,iBAAS,eAAe,CAAC,EACvB,KAAK,EACL,KAAK,EACL,UAA0B,EAC1B,YAAwB,EACxB,QAAgB,EAChB,IAAe,EACf,OAAmB,EACnB,KAAK,EACL,SAAS,EACT,UAAU,EACV,cAAc,EACd,SAAS,EACT,kBAAkB,EAClB,KAAK,EACL,MAAM,GACP,EAAE,WAAW,2CAmDb;AAED,eAAO,MAAM,MAAM,mDAAwB,CAAC"}
|
|
@@ -40,6 +40,15 @@ export interface RatingProps {
|
|
|
40
40
|
countColor?: string;
|
|
41
41
|
/** `stars` variant: the unfilled part of each star. Default the theme's border colour. */
|
|
42
42
|
emptyStarColor?: string;
|
|
43
|
+
/**
|
|
44
|
+
* `stars` variant: the number the stars are filled to, clamped to `0..5`,
|
|
45
|
+
* independent of the drawn `value`. Pass it with a localised `value` string
|
|
46
|
+
* (`value="4,5"` with `fillValue={4.5}`) so the text follows the locale and
|
|
47
|
+
* the fill does not depend on parsing it. Without it a string `value` is
|
|
48
|
+
* parsed (bidi marks dropped, a comma decimal accepted). Ignored by
|
|
49
|
+
* `compact` and while there is no rating (`value` empty).
|
|
50
|
+
*/
|
|
51
|
+
fillValue?: number;
|
|
43
52
|
/**
|
|
44
53
|
* Overrides the composed accessible name ("Rated 4.92 out of 5, 128
|
|
45
54
|
* reviews"), which is English — pass a translated sentence here.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/rating/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,8FAA8F;AAC9F,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE5C,oEAAoE;AACpE,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG,SAAS,CAAC;AAEzD;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,OAAO,CAAC;AAEhD,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC/B,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,uGAAuG;IACvG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,sFAAsF;IACtF,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,iEAAiE;AACjE,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,iFAAiF;IACjF,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mCAAmC;IACnC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IACzD,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,yCAAyC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,wFAAwF;IACxF,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/rating/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,8FAA8F;AAC9F,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE5C,oEAAoE;AACpE,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG,SAAS,CAAC;AAEzD;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,OAAO,CAAC;AAEhD,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC/B,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,uGAAuG;IACvG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,sFAAsF;IACtF,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,iEAAiE;AACjE,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,iFAAiF;IACjF,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mCAAmC;IACnC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IACzD,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,yCAAyC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,wFAAwF;IACxF,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
package/package.json
CHANGED
|
@@ -32,8 +32,12 @@ function WebShell({ children }: BottomSheetShellProps) {
|
|
|
32
32
|
{/* `OverlayRoot` owns the portal-root pointer-events opt-in (see
|
|
33
33
|
`src/overlay`). It used to ride in the style array below as an
|
|
34
34
|
RN-only box-none value, which never reached the DOM — the whole
|
|
35
|
-
sheet, backdrop included, was click-through on web.
|
|
36
|
-
|
|
35
|
+
sheet, backdrop included, was click-through on web. `modal`:
|
|
36
|
+
the sheet dims and blocks the page, so the app's
|
|
37
|
+
`OverlayInertBoundary` makes that page inert while it is open.
|
|
38
|
+
(Native needs no flag — its shell is an RN `Modal`, a separate
|
|
39
|
+
window the screen reader is already confined to.) */}
|
|
40
|
+
<OverlayRoot style={webStyles.rootView} modal>
|
|
37
41
|
<GestureHandlerRootView style={StyleSheet.absoluteFill}>
|
|
38
42
|
{children}
|
|
39
43
|
</GestureHandlerRootView>
|
|
@@ -144,7 +144,8 @@ function useDialogCss(): void {
|
|
|
144
144
|
* Accessibility: the panel has `role="dialog"` and the `title`/`description`
|
|
145
145
|
* props (when provided) become the `aria-labelledby` / `aria-describedby`
|
|
146
146
|
* targets. Pressing the backdrop dismisses. Pressing `Escape` dismisses.
|
|
147
|
-
*
|
|
147
|
+
* While it is open the page behind is `inert` — provided the app wraps its
|
|
148
|
+
* content in `OverlayInertBoundary` (`OverlayRoot modal` below registers it).
|
|
148
149
|
*
|
|
149
150
|
* Web motion never uses reanimated `exiting` (which throws `removeChild` on
|
|
150
151
|
* concurrent React unmounts): the centered card uses CSS keyframes and the
|
|
@@ -354,8 +355,10 @@ function CenterOrSideDialog({
|
|
|
354
355
|
overlay stack. It mounts here, inside the `isOpen` guard, which
|
|
355
356
|
is what makes the rank track OPENING rather than mounting — the
|
|
356
357
|
dialog is declared once at the app root and stays mounted for
|
|
357
|
-
the app's lifetime.
|
|
358
|
-
|
|
358
|
+
the app's lifetime. `modal` makes the app's
|
|
359
|
+
`OverlayInertBoundary` take the page out of the tab order and
|
|
360
|
+
the accessibility tree while it is open. */}
|
|
361
|
+
<OverlayRoot modal>
|
|
359
362
|
{/* The press target IS the full-viewport box, so it uses
|
|
360
363
|
`Backdrop` (which opts back in from the Portal root's
|
|
361
364
|
`pointer-events: none` via the `pointerEvents` PROP — the style
|
|
@@ -746,7 +749,7 @@ function SheetSurface({
|
|
|
746
749
|
);
|
|
747
750
|
|
|
748
751
|
return (
|
|
749
|
-
<OverlayRoot style={[sheetStyles.root, containerStyle]} className={containerClassName}>
|
|
752
|
+
<OverlayRoot style={[sheetStyles.root, containerStyle]} className={containerClassName} modal>
|
|
750
753
|
<Backdrop
|
|
751
754
|
testID={testID ? `${testID}-backdrop` : DIALOG_SHEET_BACKDROP_TESTID}
|
|
752
755
|
accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
|
package/src/index.ts
CHANGED
package/src/index.web.ts
CHANGED
|
@@ -291,7 +291,13 @@ export function InputOtp({
|
|
|
291
291
|
autoFocus={autoFocus && index === 0}
|
|
292
292
|
// Long enough to accept a full autofilled code in one box.
|
|
293
293
|
maxLength={length}
|
|
294
|
-
|
|
294
|
+
// Typing into a filled box must REPLACE its digit, so the digit is
|
|
295
|
+
// selected on focus. Web's `selectTextOnFocus` does that. Android's
|
|
296
|
+
// does not: it selects on the input's next layout, and focusing a box
|
|
297
|
+
// lays nothing out, so the keystroke landed beside the old digit and
|
|
298
|
+
// `writeFrom` spread both across two boxes. Native selects in
|
|
299
|
+
// `onFocus` instead (see Search, OxyHQ/Mention#1126).
|
|
300
|
+
selectTextOnFocus={IS_WEB}
|
|
295
301
|
caretHidden={false}
|
|
296
302
|
editable={!disabled}
|
|
297
303
|
accessibilityLabel={`Digit ${index + 1} of ${length}`}
|
|
@@ -300,7 +306,10 @@ export function InputOtp({
|
|
|
300
306
|
value={digit === ' ' ? '' : digit}
|
|
301
307
|
onChangeText={(text) => writeFrom(index, text)}
|
|
302
308
|
onKeyPress={(event) => onKeyPress(event, index)}
|
|
303
|
-
onFocus={() =>
|
|
309
|
+
onFocus={() => {
|
|
310
|
+
setFocusedIndex(index);
|
|
311
|
+
if (!IS_WEB && digit !== '' && digit !== ' ') inputsRef.current[index]?.setSelection(0, 1);
|
|
312
|
+
}}
|
|
304
313
|
onBlur={() => setFocusedIndex((current) => (current === index ? null : current))}
|
|
305
314
|
keyboardAppearance={theme.isDark ? 'dark' : 'light'}
|
|
306
315
|
style={[
|
package/src/overlay/Overlay.tsx
CHANGED
|
@@ -58,6 +58,7 @@ import { StyledView } from '../styles/styled-primitives';
|
|
|
58
58
|
import { SurfaceLevelProvider } from '../styles/surface-levels';
|
|
59
59
|
import { layerForRank, type OverlayLayer } from './stack';
|
|
60
60
|
import { useHardwareBack } from './use-hardware-back';
|
|
61
|
+
import { useModalOverlayRegistration } from './use-modal-overlay-active';
|
|
61
62
|
import { useOverlayLayer } from './use-overlay-layer';
|
|
62
63
|
import type { OverlayRootProps, BackdropProps } from './types';
|
|
63
64
|
|
|
@@ -181,6 +182,9 @@ function OverlayRootView({
|
|
|
181
182
|
layer,
|
|
182
183
|
modal,
|
|
183
184
|
}: OverlayRootViewProps & { layer: OverlayLayer }) {
|
|
185
|
+
// Android cannot confine TalkBack from this side of the outlet — the app's
|
|
186
|
+
// `OverlayInertBoundary` hides the content once it hears a modal is open.
|
|
187
|
+
useModalOverlayRegistration(modal);
|
|
184
188
|
return (
|
|
185
189
|
<OverlayLayerContext.Provider value={layer}>
|
|
186
190
|
{/* `pointerEvents` stays a PROP: react-native-web resolves the RN-only
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `OverlayInertBoundary` — the app content's half of a modal overlay.
|
|
3
|
+
*
|
|
4
|
+
* A modal Bloom surface renders at the portal outlet, which is the app
|
|
5
|
+
* content's SIBLING. It can mark itself modal (`accessibilityViewIsModal`, which
|
|
6
|
+
* iOS honours by hiding the siblings), but it cannot hide the app on Android:
|
|
7
|
+
* TalkBack only skips a subtree whose OWN root says
|
|
8
|
+
* `importantForAccessibility="no-hide-descendants"`, and that root belongs to
|
|
9
|
+
* the app. Without it, TalkBack walked from the settings modal straight into
|
|
10
|
+
* the Home feed behind it (OxyHQ/Mention#1126).
|
|
11
|
+
*
|
|
12
|
+
* The app wraps its content — everything that is NOT the `PortalOutlet` — in
|
|
13
|
+
* this boundary. While any modal overlay is open (`./modal-registry.ts`) it
|
|
14
|
+
* takes that content out of the accessibility tree:
|
|
15
|
+
*
|
|
16
|
+
* - Android: `importantForAccessibility="no-hide-descendants"`.
|
|
17
|
+
* - iOS: `accessibilityElementsHidden`, alongside the surface's own
|
|
18
|
+
* `accessibilityViewIsModal` (either alone is enough; both costs nothing).
|
|
19
|
+
* - Web: `inert`, which removes the subtree from the tab order AND the
|
|
20
|
+
* accessibility tree, plus `aria-hidden` for engines that lag on `inert`.
|
|
21
|
+
* The web portal root is a child of `document.body`, never of the app
|
|
22
|
+
* root, so the surface itself is untouched. Web dialogs set no focus trap
|
|
23
|
+
* of their own, so nothing is applied twice.
|
|
24
|
+
*
|
|
25
|
+
* Layout: on web the boundary is `display: contents`, so it generates no box
|
|
26
|
+
* and a document-scrolling page lays out exactly as without it. On native it is
|
|
27
|
+
* a `flex: 1` view — the content it wraps already fills the root there — and it
|
|
28
|
+
* is never collapsed, because flattening a layout-only view would drop the
|
|
29
|
+
* accessibility prop the moment it is needed.
|
|
30
|
+
*/
|
|
31
|
+
import { Platform, StyleSheet, View } from 'react-native';
|
|
32
|
+
|
|
33
|
+
import type { OverlayInertBoundaryProps } from './types';
|
|
34
|
+
import { useModalOverlayActive } from './use-modal-overlay-active';
|
|
35
|
+
|
|
36
|
+
export function OverlayInertBoundary({ children, style, testID }: OverlayInertBoundaryProps) {
|
|
37
|
+
const active = useModalOverlayActive();
|
|
38
|
+
|
|
39
|
+
if (Platform.OS === 'web') {
|
|
40
|
+
return (
|
|
41
|
+
<View
|
|
42
|
+
testID={testID}
|
|
43
|
+
style={[styles.web, style]}
|
|
44
|
+
aria-hidden={active ? true : undefined}
|
|
45
|
+
{...(active ? { inert: true } : null)}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
</View>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<View
|
|
54
|
+
testID={testID}
|
|
55
|
+
collapsable={false}
|
|
56
|
+
style={[styles.native, style]}
|
|
57
|
+
importantForAccessibility={active ? 'no-hide-descendants' : 'auto'}
|
|
58
|
+
accessibilityElementsHidden={active}
|
|
59
|
+
>
|
|
60
|
+
{children}
|
|
61
|
+
</View>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
OverlayInertBoundary.displayName = 'OverlayInertBoundary';
|
|
66
|
+
|
|
67
|
+
const styles = StyleSheet.create({
|
|
68
|
+
native: { flex: 1 },
|
|
69
|
+
// No box of its own on web: the wrapped content lays out against the
|
|
70
|
+
// boundary's parent, flex tree or document scroll alike.
|
|
71
|
+
web: { display: 'contents' },
|
|
72
|
+
});
|
package/src/overlay/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ export {
|
|
|
7
7
|
type OverlayLayer,
|
|
8
8
|
} from './stack';
|
|
9
9
|
export { useOverlayLayer } from './use-overlay-layer';
|
|
10
|
+
export { useModalOverlayActive } from './use-modal-overlay-active';
|
|
11
|
+
export { OverlayInertBoundary } from './OverlayInertBoundary';
|
|
10
12
|
export {
|
|
11
13
|
useOverlayLayerContext,
|
|
12
14
|
BACKDROP_BLUR_INTENSITY,
|
|
@@ -14,4 +16,4 @@ export {
|
|
|
14
16
|
OverlayRoot,
|
|
15
17
|
Backdrop,
|
|
16
18
|
} from './Overlay';
|
|
17
|
-
export type { OverlayRootProps, BackdropProps } from './types';
|
|
19
|
+
export type { OverlayRootProps, BackdropProps, OverlayInertBoundaryProps } from './types';
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which MODAL overlays are open right now, as a subscribable store.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* A modal surface has to keep a screen reader inside it. iOS can do that from
|
|
7
|
+
* the overlay's side: `accessibilityViewIsModal` on the `OverlayRoot` hides its
|
|
8
|
+
* siblings, and at the native portal outlet the app content IS a sibling.
|
|
9
|
+
* Android has no equivalent. TalkBack keeps walking into the app behind the
|
|
10
|
+
* open surface (OxyHQ/Mention#1126: the Home feed stayed reachable under the
|
|
11
|
+
* settings modal), and the only switch that stops it —
|
|
12
|
+
* `importantForAccessibility="no-hide-descendants"` — has to sit ON the app
|
|
13
|
+
* content. A portaled surface cannot reach that view: the outlet renders it as
|
|
14
|
+
* the content's sibling, not its ancestor.
|
|
15
|
+
*
|
|
16
|
+
* So the knowledge travels the other way. Every `OverlayRoot` with `modal`
|
|
17
|
+
* registers here while it is mounted, and `OverlayInertBoundary` — which the
|
|
18
|
+
* app wraps around its own content — subscribes and hides what it wraps while
|
|
19
|
+
* anything modal is open.
|
|
20
|
+
*
|
|
21
|
+
* Non-modal overlays (menus, popovers, tooltips, toasts, the shell drawers)
|
|
22
|
+
* never register: the content under an anchored menu must stay reachable.
|
|
23
|
+
*
|
|
24
|
+
* Tokens rather than a counter so StrictMode's mount/unmount/remount and an
|
|
25
|
+
* unmatched release can never drift the count: a token is either in the set or
|
|
26
|
+
* not.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
interface ModalRegistry {
|
|
30
|
+
live: Set<object>;
|
|
31
|
+
listeners: Set<() => void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare global {
|
|
35
|
+
// eslint-disable-next-line no-var
|
|
36
|
+
var __oxy_so_bloom_modal_overlays__: ModalRegistry | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `globalThis`-anchored for the same reason as `./stack.ts`: the surfaces that
|
|
41
|
+
* register (`./dialog`, `./settings-modal`, …) and the boundary the app mounts
|
|
42
|
+
* (`./portal`) are imported through different subpaths, and a bundler can
|
|
43
|
+
* resolve those through different export conditions. Two physical copies of
|
|
44
|
+
* this module would each keep their own set, and the boundary would never hear
|
|
45
|
+
* about the surfaces.
|
|
46
|
+
*/
|
|
47
|
+
function registry(): ModalRegistry {
|
|
48
|
+
globalThis.__oxy_so_bloom_modal_overlays__ ??= { live: new Set(), listeners: new Set() };
|
|
49
|
+
return globalThis.__oxy_so_bloom_modal_overlays__;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function notify(reg: ModalRegistry): void {
|
|
53
|
+
for (const listener of Array.from(reg.listeners)) listener();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Mark a modal overlay as open. Returns its release. Called from an effect in
|
|
58
|
+
* `OverlayRoot`, which React balances even under StrictMode.
|
|
59
|
+
*/
|
|
60
|
+
export function registerModalOverlay(token: object): () => void {
|
|
61
|
+
const reg = registry();
|
|
62
|
+
const wasActive = reg.live.size > 0;
|
|
63
|
+
reg.live.add(token);
|
|
64
|
+
if (!wasActive) notify(reg);
|
|
65
|
+
return () => {
|
|
66
|
+
const current = registry();
|
|
67
|
+
if (!current.live.delete(token)) return;
|
|
68
|
+
if (current.live.size === 0) notify(current);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Whether any modal overlay is open. */
|
|
73
|
+
export function hasActiveModalOverlays(): boolean {
|
|
74
|
+
return registry().live.size > 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** How many modal overlays are open. Exported for tests. */
|
|
78
|
+
export function activeModalOverlayCount(): number {
|
|
79
|
+
return registry().live.size;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Subscribe to changes in `hasActiveModalOverlays()`. Listeners fire only when
|
|
84
|
+
* the answer flips — a second modal opening over a first changes nothing for
|
|
85
|
+
* the content underneath.
|
|
86
|
+
*/
|
|
87
|
+
export function subscribeModalOverlays(listener: () => void): () => void {
|
|
88
|
+
const reg = registry();
|
|
89
|
+
reg.listeners.add(listener);
|
|
90
|
+
return () => {
|
|
91
|
+
registry().listeners.delete(listener);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Test seam — drops all open modals, notifying current subscribers. */
|
|
96
|
+
export function resetModalOverlays(): void {
|
|
97
|
+
const reg = registry();
|
|
98
|
+
const wasActive = reg.live.size > 0;
|
|
99
|
+
reg.live.clear();
|
|
100
|
+
if (wasActive) notify(reg);
|
|
101
|
+
}
|
package/src/overlay/types.ts
CHANGED
|
@@ -36,13 +36,32 @@ export interface OverlayRootProps {
|
|
|
36
36
|
onRequestClose?: () => void;
|
|
37
37
|
/**
|
|
38
38
|
* The surface is modal: a screen reader must not wander into the app behind
|
|
39
|
-
* it.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* it. Two effects:
|
|
40
|
+
*
|
|
41
|
+
* - Native: sets `accessibilityViewIsModal` on this root, which is the app
|
|
42
|
+
* content's sibling at the portal outlet (iOS confines VoiceOver to it).
|
|
43
|
+
* - Every platform: registers the surface as an open modal while mounted
|
|
44
|
+
* (`./modal-registry.ts`), so the app's `OverlayInertBoundary` hides the
|
|
45
|
+
* content behind it — the only way to confine TalkBack on Android, and
|
|
46
|
+
* what makes the content `inert` on web.
|
|
47
|
+
*
|
|
48
|
+
* Dialogs, sheets, the settings modal and the media viewer set it; anchored
|
|
49
|
+
* surfaces (menus, popovers, tooltips), toasts and the shell drawers do not.
|
|
42
50
|
*/
|
|
43
51
|
modal?: boolean;
|
|
44
52
|
}
|
|
45
53
|
|
|
54
|
+
export interface OverlayInertBoundaryProps {
|
|
55
|
+
/** The app's own content — everything EXCEPT the `PortalOutlet`. */
|
|
56
|
+
children?: ReactNode;
|
|
57
|
+
/**
|
|
58
|
+
* Native only (web renders no box). Defaults to `flex: 1`; override when the
|
|
59
|
+
* boundary's parent is not a column that the content should fill.
|
|
60
|
+
*/
|
|
61
|
+
style?: StyleProp<ViewStyle>;
|
|
62
|
+
testID?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
46
65
|
export interface BackdropProps {
|
|
47
66
|
/** Dismiss handler. Omit (or pass `disabled`) for a backdrop that only dims. */
|
|
48
67
|
onPress?: () => void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useModalOverlayActive` — whether any modal Bloom overlay is open.
|
|
3
|
+
*
|
|
4
|
+
* Re-renders only when the answer flips. See `./modal-registry.ts` for who
|
|
5
|
+
* registers and why.
|
|
6
|
+
*/
|
|
7
|
+
import { useEffect, useSyncExternalStore } from 'react';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
hasActiveModalOverlays,
|
|
11
|
+
registerModalOverlay,
|
|
12
|
+
subscribeModalOverlays,
|
|
13
|
+
} from './modal-registry';
|
|
14
|
+
|
|
15
|
+
/** `false` on the server: nothing is open before hydration. */
|
|
16
|
+
const serverSnapshot = () => false;
|
|
17
|
+
|
|
18
|
+
export function useModalOverlayActive(): boolean {
|
|
19
|
+
return useSyncExternalStore(subscribeModalOverlays, hasActiveModalOverlays, serverSnapshot);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register as an open modal overlay while mounted and `modal` is true. Used by
|
|
24
|
+
* `OverlayRoot`; exported only within the family.
|
|
25
|
+
*/
|
|
26
|
+
export function useModalOverlayRegistration(modal: boolean): void {
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!modal) return undefined;
|
|
29
|
+
return registerModalOverlay({});
|
|
30
|
+
}, [modal]);
|
|
31
|
+
}
|
package/src/portal/index.ts
CHANGED
|
@@ -3,3 +3,8 @@ export {
|
|
|
3
3
|
PortalOutlet,
|
|
4
4
|
Portal,
|
|
5
5
|
} from './Portal';
|
|
6
|
+
// The app-content half of a modal overlay: wrap everything EXCEPT the
|
|
7
|
+
// `PortalOutlet` in it. Lives in `overlay/` (it reads the modal registry) and is
|
|
8
|
+
// offered here because this is the subpath the app root already imports.
|
|
9
|
+
export { OverlayInertBoundary } from '../overlay/OverlayInertBoundary';
|
|
10
|
+
export type { OverlayInertBoundaryProps } from '../overlay/types';
|
package/src/portal/index.web.ts
CHANGED
|
@@ -8,3 +8,8 @@ export {
|
|
|
8
8
|
PortalOutlet,
|
|
9
9
|
Portal,
|
|
10
10
|
} from './Portal.web';
|
|
11
|
+
// The app-content half of a modal overlay: wrap everything EXCEPT the
|
|
12
|
+
// `PortalOutlet` in it. Lives in `overlay/` (it reads the modal registry) and is
|
|
13
|
+
// offered here because this is the subpath the app root already imports.
|
|
14
|
+
export { OverlayInertBoundary } from '../overlay/OverlayInertBoundary';
|
|
15
|
+
export type { OverlayInertBoundaryProps } from '../overlay/types';
|
package/src/rating/Rating.tsx
CHANGED
|
@@ -26,7 +26,8 @@ import type { RatingProps, RatingSize } from './types';
|
|
|
26
26
|
* `variant="stars"` draws five stars before the value instead of one, 2px
|
|
27
27
|
* apart, each filled to its share of the value: the unfilled star in
|
|
28
28
|
* `emptyStarColor` (border), and over it the filled star clipped to the
|
|
29
|
-
* fraction. The
|
|
29
|
+
* fraction. The fill comes from `fillValue` when given, else from `value`
|
|
30
|
+
* (parsed by `parseRatingValue` when it is a localised string). The clip is anchored with a LOGICAL inset (`insetInlineStart`) so a
|
|
30
31
|
* right-to-left row fills from the right — the root carries
|
|
31
32
|
* `useDirectionProps()` for react-native-web to resolve it.
|
|
32
33
|
*
|
|
@@ -52,12 +53,45 @@ const IS_WEB = Platform.OS === 'web';
|
|
|
52
53
|
const STAR_COUNT = 5;
|
|
53
54
|
const STAR_GAP = 2;
|
|
54
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Bidi isolates and marks (U+2066–U+2069 LRI/RLI/FSI/PDI, U+200E/U+200F
|
|
58
|
+
* LRM/RLM, U+061C ALM) a localised string is wrapped in. None of them are
|
|
59
|
+
* part of the number.
|
|
60
|
+
*/
|
|
61
|
+
const FORMAT_CHARACTERS = /[\u2066-\u2069\u200E\u200F\u061C]/g;
|
|
62
|
+
/** Arabic-Indic (U+0660) and extended Arabic-Indic (U+06F0) digit zeros. */
|
|
63
|
+
const ARABIC_ZERO = 0x0660;
|
|
64
|
+
const EXTENDED_ARABIC_ZERO = 0x06f0;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The number a drawn rating value stands for, for the `stars` fill: a number
|
|
68
|
+
* is itself; a string is read after dropping bidi/format characters, with a
|
|
69
|
+
* comma or Arabic decimal separator (U+066B) accepted as the decimal point and
|
|
70
|
+
* Arabic-Indic digits accepted — so "4,5" wrapped in FSI…PDI is 4.5. Otherwise it is
|
|
71
|
+
* `Number()`'s reading, so anything unreadable is `NaN` (no fill).
|
|
72
|
+
*/
|
|
73
|
+
export function parseRatingValue(value: number | string): number {
|
|
74
|
+
if (typeof value === 'number') return value;
|
|
75
|
+
const text = value
|
|
76
|
+
.replace(FORMAT_CHARACTERS, '')
|
|
77
|
+
.trim()
|
|
78
|
+
.replace(/[\u0660-\u0669]/g, (d) => String(d.charCodeAt(0) - ARABIC_ZERO))
|
|
79
|
+
.replace(/[\u06F0-\u06F9]/g, (d) => String(d.charCodeAt(0) - EXTENDED_ARABIC_ZERO))
|
|
80
|
+
.replace(/[,\u066B]/, '.');
|
|
81
|
+
return Number(text);
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
/** How much of star `index` (0-based) a rating fills, `0..1`. */
|
|
56
85
|
export function starFill(rating: number, index: number): number {
|
|
57
86
|
if (!Number.isFinite(rating)) return 0;
|
|
58
87
|
return Math.min(1, Math.max(0, rating - index));
|
|
59
88
|
}
|
|
60
89
|
|
|
90
|
+
/** A fill on the five-star scale, clamped to `0..5`; `NaN` stays `NaN` (no fill). */
|
|
91
|
+
function clampRating(rating: number): number {
|
|
92
|
+
return Math.min(STAR_COUNT, Math.max(0, rating));
|
|
93
|
+
}
|
|
94
|
+
|
|
61
95
|
function StarRow({
|
|
62
96
|
rating,
|
|
63
97
|
size,
|
|
@@ -113,6 +147,7 @@ function RatingComponent({
|
|
|
113
147
|
starColor,
|
|
114
148
|
countColor,
|
|
115
149
|
emptyStarColor,
|
|
150
|
+
fillValue,
|
|
116
151
|
accessibilityLabel,
|
|
117
152
|
style,
|
|
118
153
|
testID,
|
|
@@ -148,7 +183,7 @@ function RatingComponent({
|
|
|
148
183
|
>
|
|
149
184
|
{variant === 'stars' && rated ? (
|
|
150
185
|
<StarRow
|
|
151
|
-
rating={
|
|
186
|
+
rating={clampRating(fillValue ?? parseRatingValue(value))}
|
|
152
187
|
size={config.star}
|
|
153
188
|
fill={starPaint}
|
|
154
189
|
empty={emptyStarColor ?? theme.colors.border}
|
package/src/rating/types.ts
CHANGED
|
@@ -44,6 +44,15 @@ export interface RatingProps {
|
|
|
44
44
|
countColor?: string;
|
|
45
45
|
/** `stars` variant: the unfilled part of each star. Default the theme's border colour. */
|
|
46
46
|
emptyStarColor?: string;
|
|
47
|
+
/**
|
|
48
|
+
* `stars` variant: the number the stars are filled to, clamped to `0..5`,
|
|
49
|
+
* independent of the drawn `value`. Pass it with a localised `value` string
|
|
50
|
+
* (`value="4,5"` with `fillValue={4.5}`) so the text follows the locale and
|
|
51
|
+
* the fill does not depend on parsing it. Without it a string `value` is
|
|
52
|
+
* parsed (bidi marks dropped, a comma decimal accepted). Ignored by
|
|
53
|
+
* `compact` and while there is no rating (`value` empty).
|
|
54
|
+
*/
|
|
55
|
+
fillValue?: number;
|
|
47
56
|
/**
|
|
48
57
|
* Overrides the composed accessible name ("Rated 4.92 out of 5, 128
|
|
49
58
|
* reviews"), which is English — pass a translated sentence here.
|