@oxy.so/bloom 4.17.0 → 4.18.0
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 +4 -4
- package/docs/breadcrumb.mdx +11 -0
- package/docs/cart-panel.mdx +4 -2
- package/docs/checkout-summary.mdx +4 -0
- package/docs/chip.mdx +14 -1
- package/docs/dialog.mdx +5 -4
- package/docs/loading.mdx +11 -0
- package/docs/price-breakdown.mdx +17 -0
- package/docs/rating.mdx +22 -0
- package/docs/stepper.mdx +25 -0
- package/lib/commonjs/breadcrumb/Breadcrumb.js +36 -6
- package/lib/commonjs/breadcrumb/Breadcrumb.js.map +1 -1
- package/lib/commonjs/cart-panel/CartLine.js +32 -10
- package/lib/commonjs/cart-panel/CartLine.js.map +1 -1
- package/lib/commonjs/cart-panel/CartPanel.js +3 -0
- package/lib/commonjs/cart-panel/CartPanel.js.map +1 -1
- package/lib/commonjs/cart-panel/shared.js +2 -0
- package/lib/commonjs/cart-panel/shared.js.map +1 -1
- package/lib/commonjs/checkout-summary/CheckoutConfirm.js +11 -1
- package/lib/commonjs/checkout-summary/CheckoutConfirm.js.map +1 -1
- package/lib/commonjs/chip/Chip.js +3 -1
- package/lib/commonjs/chip/Chip.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.js +15 -6
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +11 -5
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/dialog/placement.js +18 -1
- package/lib/commonjs/dialog/placement.js.map +1 -1
- package/lib/commonjs/loading/Loading.js +7 -3
- package/lib/commonjs/loading/Loading.js.map +1 -1
- package/lib/commonjs/loading/Loading.web.js +7 -3
- package/lib/commonjs/loading/Loading.web.js.map +1 -1
- package/lib/commonjs/loading/LoadingBase.js +49 -2
- package/lib/commonjs/loading/LoadingBase.js.map +1 -1
- package/lib/commonjs/price-breakdown/PriceSummary.js +11 -1
- package/lib/commonjs/price-breakdown/PriceSummary.js.map +1 -1
- package/lib/commonjs/price-breakdown/PriceSummaryLine.js +13 -1
- package/lib/commonjs/price-breakdown/PriceSummaryLine.js.map +1 -1
- package/lib/commonjs/rating/Rating.js +90 -4
- package/lib/commonjs/rating/Rating.js.map +1 -1
- package/lib/commonjs/stepper/Stepper.js +18 -5
- package/lib/commonjs/stepper/Stepper.js.map +1 -1
- package/lib/module/breadcrumb/Breadcrumb.js +36 -6
- package/lib/module/breadcrumb/Breadcrumb.js.map +1 -1
- package/lib/module/cart-panel/CartLine.js +32 -10
- package/lib/module/cart-panel/CartLine.js.map +1 -1
- package/lib/module/cart-panel/CartPanel.js +3 -0
- package/lib/module/cart-panel/CartPanel.js.map +1 -1
- package/lib/module/cart-panel/shared.js +2 -0
- package/lib/module/cart-panel/shared.js.map +1 -1
- package/lib/module/checkout-summary/CheckoutConfirm.js +11 -1
- package/lib/module/checkout-summary/CheckoutConfirm.js.map +1 -1
- package/lib/module/chip/Chip.js +3 -1
- package/lib/module/chip/Chip.js.map +1 -1
- package/lib/module/dialog/Dialog.js +16 -7
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +12 -6
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/dialog/placement.js +17 -1
- package/lib/module/dialog/placement.js.map +1 -1
- package/lib/module/loading/Loading.js +8 -4
- package/lib/module/loading/Loading.js.map +1 -1
- package/lib/module/loading/Loading.web.js +8 -4
- package/lib/module/loading/Loading.web.js.map +1 -1
- package/lib/module/loading/LoadingBase.js +49 -3
- package/lib/module/loading/LoadingBase.js.map +1 -1
- package/lib/module/price-breakdown/PriceSummary.js +11 -1
- package/lib/module/price-breakdown/PriceSummary.js.map +1 -1
- package/lib/module/price-breakdown/PriceSummaryLine.js +13 -1
- package/lib/module/price-breakdown/PriceSummaryLine.js.map +1 -1
- package/lib/module/rating/Rating.js +89 -4
- package/lib/module/rating/Rating.js.map +1 -1
- package/lib/module/stepper/Stepper.js +18 -5
- package/lib/module/stepper/Stepper.js.map +1 -1
- package/lib/typescript/commonjs/breadcrumb/Breadcrumb.d.ts.map +1 -1
- package/lib/typescript/commonjs/breadcrumb/types.d.ts +7 -0
- package/lib/typescript/commonjs/breadcrumb/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/cart-panel/CartLine.d.ts +10 -5
- package/lib/typescript/commonjs/cart-panel/CartLine.d.ts.map +1 -1
- package/lib/typescript/commonjs/cart-panel/CartPanel.d.ts +1 -1
- package/lib/typescript/commonjs/cart-panel/CartPanel.d.ts.map +1 -1
- package/lib/typescript/commonjs/cart-panel/shared.d.ts +1 -1
- package/lib/typescript/commonjs/cart-panel/shared.d.ts.map +1 -1
- package/lib/typescript/commonjs/cart-panel/types.d.ts +18 -0
- package/lib/typescript/commonjs/cart-panel/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/checkout-summary/CheckoutConfirm.d.ts +4 -1
- package/lib/typescript/commonjs/checkout-summary/CheckoutConfirm.d.ts.map +1 -1
- package/lib/typescript/commonjs/checkout-summary/types.d.ts +6 -0
- package/lib/typescript/commonjs/checkout-summary/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/chip/Chip.d.ts.map +1 -1
- package/lib/typescript/commonjs/chip/types.d.ts +8 -6
- package/lib/typescript/commonjs/chip/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/placement.d.ts +13 -2
- package/lib/typescript/commonjs/dialog/placement.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/types.d.ts +3 -2
- package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/Loading.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/Loading.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/LoadingBase.d.ts +18 -0
- package/lib/typescript/commonjs/loading/LoadingBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/types.d.ts +11 -0
- package/lib/typescript/commonjs/loading/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/price-breakdown/PriceSummary.d.ts.map +1 -1
- package/lib/typescript/commonjs/price-breakdown/PriceSummaryLine.d.ts +3 -1
- package/lib/typescript/commonjs/price-breakdown/PriceSummaryLine.d.ts.map +1 -1
- package/lib/typescript/commonjs/price-breakdown/types.d.ts +12 -0
- package/lib/typescript/commonjs/price-breakdown/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/rating/Rating.d.ts +3 -1
- package/lib/typescript/commonjs/rating/Rating.d.ts.map +1 -1
- package/lib/typescript/commonjs/rating/index.d.ts +1 -1
- package/lib/typescript/commonjs/rating/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/rating/types.d.ts +20 -0
- package/lib/typescript/commonjs/rating/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/stepper/Stepper.d.ts +1 -1
- package/lib/typescript/commonjs/stepper/Stepper.d.ts.map +1 -1
- package/lib/typescript/commonjs/stepper/types.d.ts +9 -0
- package/lib/typescript/commonjs/stepper/types.d.ts.map +1 -1
- package/lib/typescript/module/breadcrumb/Breadcrumb.d.ts.map +1 -1
- package/lib/typescript/module/breadcrumb/types.d.ts +7 -0
- package/lib/typescript/module/breadcrumb/types.d.ts.map +1 -1
- package/lib/typescript/module/cart-panel/CartLine.d.ts +10 -5
- package/lib/typescript/module/cart-panel/CartLine.d.ts.map +1 -1
- package/lib/typescript/module/cart-panel/CartPanel.d.ts +1 -1
- package/lib/typescript/module/cart-panel/CartPanel.d.ts.map +1 -1
- package/lib/typescript/module/cart-panel/shared.d.ts +1 -1
- package/lib/typescript/module/cart-panel/shared.d.ts.map +1 -1
- package/lib/typescript/module/cart-panel/types.d.ts +18 -0
- package/lib/typescript/module/cart-panel/types.d.ts.map +1 -1
- package/lib/typescript/module/checkout-summary/CheckoutConfirm.d.ts +4 -1
- package/lib/typescript/module/checkout-summary/CheckoutConfirm.d.ts.map +1 -1
- package/lib/typescript/module/checkout-summary/types.d.ts +6 -0
- package/lib/typescript/module/checkout-summary/types.d.ts.map +1 -1
- package/lib/typescript/module/chip/Chip.d.ts.map +1 -1
- package/lib/typescript/module/chip/types.d.ts +8 -6
- package/lib/typescript/module/chip/types.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/placement.d.ts +13 -2
- package/lib/typescript/module/dialog/placement.d.ts.map +1 -1
- package/lib/typescript/module/dialog/types.d.ts +3 -2
- package/lib/typescript/module/dialog/types.d.ts.map +1 -1
- package/lib/typescript/module/loading/Loading.d.ts.map +1 -1
- package/lib/typescript/module/loading/Loading.web.d.ts.map +1 -1
- package/lib/typescript/module/loading/LoadingBase.d.ts +18 -0
- package/lib/typescript/module/loading/LoadingBase.d.ts.map +1 -1
- package/lib/typescript/module/loading/types.d.ts +11 -0
- package/lib/typescript/module/loading/types.d.ts.map +1 -1
- package/lib/typescript/module/price-breakdown/PriceSummary.d.ts.map +1 -1
- package/lib/typescript/module/price-breakdown/PriceSummaryLine.d.ts +3 -1
- package/lib/typescript/module/price-breakdown/PriceSummaryLine.d.ts.map +1 -1
- package/lib/typescript/module/price-breakdown/types.d.ts +12 -0
- package/lib/typescript/module/price-breakdown/types.d.ts.map +1 -1
- package/lib/typescript/module/rating/Rating.d.ts +3 -1
- package/lib/typescript/module/rating/Rating.d.ts.map +1 -1
- package/lib/typescript/module/rating/index.d.ts +1 -1
- package/lib/typescript/module/rating/index.d.ts.map +1 -1
- package/lib/typescript/module/rating/types.d.ts +20 -0
- package/lib/typescript/module/rating/types.d.ts.map +1 -1
- package/lib/typescript/module/stepper/Stepper.d.ts +1 -1
- package/lib/typescript/module/stepper/Stepper.d.ts.map +1 -1
- package/lib/typescript/module/stepper/types.d.ts +9 -0
- package/lib/typescript/module/stepper/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/breadcrumb/Breadcrumb.tsx +29 -4
- package/src/breadcrumb/types.ts +7 -0
- package/src/cart-panel/CartLine.tsx +30 -7
- package/src/cart-panel/CartPanel.tsx +3 -0
- package/src/cart-panel/shared.ts +11 -1
- package/src/cart-panel/types.ts +18 -0
- package/src/checkout-summary/CheckoutConfirm.tsx +9 -0
- package/src/checkout-summary/types.ts +6 -0
- package/src/chip/Chip.tsx +3 -1
- package/src/chip/types.ts +8 -6
- package/src/dialog/Dialog.tsx +19 -8
- package/src/dialog/Dialog.web.tsx +15 -7
- package/src/dialog/placement.ts +19 -2
- package/src/dialog/types.ts +3 -2
- package/src/loading/Loading.tsx +6 -2
- package/src/loading/Loading.web.tsx +6 -2
- package/src/loading/LoadingBase.tsx +55 -5
- package/src/loading/types.ts +11 -0
- package/src/price-breakdown/PriceSummary.tsx +10 -0
- package/src/price-breakdown/PriceSummaryLine.tsx +12 -0
- package/src/price-breakdown/types.ts +12 -0
- package/src/rating/Rating.tsx +87 -3
- package/src/rating/index.ts +1 -0
- package/src/rating/types.ts +21 -0
- package/src/stepper/Stepper.tsx +18 -5
- package/src/stepper/types.ts +9 -0
|
@@ -21,12 +21,17 @@ import type { CartLineProps } from './types';
|
|
|
21
21
|
* leading the square photo (56; 44 compact), washed while it is sold out
|
|
22
22
|
* body the name; the chosen options on one quiet line; the note; then
|
|
23
23
|
* the quantity control and remove, side by side
|
|
24
|
-
* trailing the price, right-aligned, with an optional struck original
|
|
24
|
+
* trailing the price, right-aligned, with an optional struck original, and
|
|
25
|
+
* an optional `secondaryPrice` (a second currency) under it in the
|
|
26
|
+
* secondary colour
|
|
25
27
|
*
|
|
26
|
-
* THE STEPPER DOES NOT REMOVE. Its floor is 1, and removing is its
|
|
27
|
-
* with its own name — a stepper that deletes the line when you
|
|
28
|
-
* too often is a destructive action behind an arithmetic one
|
|
29
|
-
*
|
|
28
|
+
* BY DEFAULT THE STEPPER DOES NOT REMOVE. Its floor is 1, and removing is its
|
|
29
|
+
* own control with its own name — a stepper that deletes the line when you
|
|
30
|
+
* press `−` once too often is a destructive action behind an arithmetic one.
|
|
31
|
+
* `removeInStepper` opts into the storefront pattern instead: at 1 the `−`
|
|
32
|
+
* becomes a trash button NAMED as removal (`Stepper`'s `onRemove`), and the
|
|
33
|
+
* separate remove control is not drawn. A sold-out line keeps the separate
|
|
34
|
+
* control, because its stepper is disabled and could not remove anything.
|
|
30
35
|
*
|
|
31
36
|
* The row is never pressable, which is what lets the controls live in it: a
|
|
32
37
|
* pressable `Item` is a real `<button>` on web and a control inside one is
|
|
@@ -48,6 +53,7 @@ function CartLineComponent(props: CartLineProps) {
|
|
|
48
53
|
note,
|
|
49
54
|
price,
|
|
50
55
|
originalPrice,
|
|
56
|
+
secondaryPrice,
|
|
51
57
|
quantity,
|
|
52
58
|
photo,
|
|
53
59
|
photoVariant,
|
|
@@ -56,6 +62,7 @@ function CartLineComponent(props: CartLineProps) {
|
|
|
56
62
|
onQuantityChange,
|
|
57
63
|
onRemove,
|
|
58
64
|
removeLabel,
|
|
65
|
+
removeInStepper = false,
|
|
59
66
|
density = 'comfortable',
|
|
60
67
|
accessibilityLabel,
|
|
61
68
|
style,
|
|
@@ -68,6 +75,10 @@ function CartLineComponent(props: CartLineProps) {
|
|
|
68
75
|
const g = CART_GEOMETRY[density];
|
|
69
76
|
const compact = density === 'compact';
|
|
70
77
|
const chosen = optionsLine(options);
|
|
78
|
+
const removeName = removeLabel ?? `Remove ${name}`;
|
|
79
|
+
// The stepper carries removal only when it is live; a disabled stepper
|
|
80
|
+
// (sold out) cannot, and the line still needs its remove control.
|
|
81
|
+
const stepperRemoves = removeInStepper && onRemove !== undefined && onQuantityChange !== undefined && !unavailable;
|
|
71
82
|
|
|
72
83
|
return (
|
|
73
84
|
<Item
|
|
@@ -95,6 +106,16 @@ function CartLineComponent(props: CartLineProps) {
|
|
|
95
106
|
secondaryColor={paint.textSecondary}
|
|
96
107
|
testID={testID ? `${testID}-price` : undefined}
|
|
97
108
|
/>
|
|
109
|
+
{secondaryPrice ? (
|
|
110
|
+
<Text
|
|
111
|
+
variant={compact ? 'caption-1-regular' : 'body-2-regular'}
|
|
112
|
+
numberOfLines={1}
|
|
113
|
+
testID={testID ? `${testID}-secondary-price` : undefined}
|
|
114
|
+
style={{ color: paint.textSecondary, fontVariant: ['tabular-nums'] }}
|
|
115
|
+
>
|
|
116
|
+
{secondaryPrice}
|
|
117
|
+
</Text>
|
|
118
|
+
) : null}
|
|
98
119
|
</View>
|
|
99
120
|
}
|
|
100
121
|
style={style}
|
|
@@ -146,6 +167,8 @@ function CartLineComponent(props: CartLineProps) {
|
|
|
146
167
|
min={1}
|
|
147
168
|
disabled={unavailable}
|
|
148
169
|
onValueChange={onQuantityChange}
|
|
170
|
+
onRemove={stepperRemoves ? onRemove : undefined}
|
|
171
|
+
removeLabel={stepperRemoves ? removeName : undefined}
|
|
149
172
|
accessibilityLabel={name}
|
|
150
173
|
testID={testID ? `${testID}-stepper` : undefined}
|
|
151
174
|
/>
|
|
@@ -158,11 +181,11 @@ function CartLineComponent(props: CartLineProps) {
|
|
|
158
181
|
{`×${quantity}`}
|
|
159
182
|
</Text>
|
|
160
183
|
)}
|
|
161
|
-
{onRemove ? (
|
|
184
|
+
{onRemove && !stepperRemoves ? (
|
|
162
185
|
<GlyphButton
|
|
163
186
|
size={32}
|
|
164
187
|
icon={RiDeleteBinLine}
|
|
165
|
-
accessibilityLabel={
|
|
188
|
+
accessibilityLabel={removeName}
|
|
166
189
|
onPress={onRemove}
|
|
167
190
|
testID={testID ? `${testID}-remove` : undefined}
|
|
168
191
|
/>
|
|
@@ -187,6 +187,7 @@ function CartPanelComponent({
|
|
|
187
187
|
lines,
|
|
188
188
|
onLineQuantityChange,
|
|
189
189
|
onLineRemove,
|
|
190
|
+
removeInStepper = false,
|
|
190
191
|
minimumOrder,
|
|
191
192
|
tip,
|
|
192
193
|
promo,
|
|
@@ -239,6 +240,7 @@ function CartPanelComponent({
|
|
|
239
240
|
note={line.note}
|
|
240
241
|
price={line.price}
|
|
241
242
|
originalPrice={line.originalPrice}
|
|
243
|
+
secondaryPrice={line.secondaryPrice}
|
|
242
244
|
quantity={line.quantity}
|
|
243
245
|
photo={line.photo}
|
|
244
246
|
photoVariant={line.photoVariant}
|
|
@@ -251,6 +253,7 @@ function CartPanelComponent({
|
|
|
251
253
|
: undefined
|
|
252
254
|
}
|
|
253
255
|
onRemove={onLineRemove ? () => onLineRemove(line.id) : undefined}
|
|
256
|
+
removeInStepper={removeInStepper}
|
|
254
257
|
testID={testID ? `${testID}-line-${line.id}` : undefined}
|
|
255
258
|
/>
|
|
256
259
|
))}
|
package/src/cart-panel/shared.ts
CHANGED
|
@@ -45,7 +45,15 @@ export function optionsLine(options: ReadonlyArray<string> | undefined): string
|
|
|
45
45
|
export function composeCartLineName(
|
|
46
46
|
line: Pick<
|
|
47
47
|
CartLineEntry,
|
|
48
|
-
|
|
48
|
+
| 'name'
|
|
49
|
+
| 'options'
|
|
50
|
+
| 'note'
|
|
51
|
+
| 'quantity'
|
|
52
|
+
| 'price'
|
|
53
|
+
| 'originalPrice'
|
|
54
|
+
| 'secondaryPrice'
|
|
55
|
+
| 'unavailable'
|
|
56
|
+
| 'unavailableLabel'
|
|
49
57
|
>,
|
|
50
58
|
): string {
|
|
51
59
|
const parts: string[] = [line.name];
|
|
@@ -55,6 +63,8 @@ export function composeCartLineName(
|
|
|
55
63
|
parts.push(String(line.quantity));
|
|
56
64
|
if (line.originalPrice) parts.push(`${line.price}, originally ${line.originalPrice}`);
|
|
57
65
|
else parts.push(line.price);
|
|
66
|
+
// The second currency travels with the price it restates.
|
|
67
|
+
if (line.secondaryPrice) parts.push(line.secondaryPrice);
|
|
58
68
|
if (line.unavailable) parts.push(line.unavailableLabel ?? 'Sold out');
|
|
59
69
|
return parts.join(', ');
|
|
60
70
|
}
|
package/src/cart-panel/types.ts
CHANGED
|
@@ -26,6 +26,12 @@ export interface CartLineEntry {
|
|
|
26
26
|
price: string;
|
|
27
27
|
/** PRE-FORMATTED price before a discount, struck through BEFORE `price`. */
|
|
28
28
|
originalPrice?: string;
|
|
29
|
+
/**
|
|
30
|
+
* A second, PRE-FORMATTED rendering of `price` — the buyer's currency beside
|
|
31
|
+
* the shop's, `"≈ 12,00 €"` — drawn de-emphasised under it. The cart's
|
|
32
|
+
* spelling of `ListingPriceLine.secondary`; drawn as given, nothing converts.
|
|
33
|
+
*/
|
|
34
|
+
secondaryPrice?: string;
|
|
29
35
|
/** How many. Always at least 1 — a line with none of it is a line that was removed. */
|
|
30
36
|
quantity: number;
|
|
31
37
|
/** An absolute URL, or an id the app's `ImageResolver` turns into one. */
|
|
@@ -45,6 +51,13 @@ export interface CartLineProps extends Omit<CartLineEntry, 'id'> {
|
|
|
45
51
|
onRemove?: () => void;
|
|
46
52
|
/** Names the remove control, which draws no text. Default `"Remove <name>"`. */
|
|
47
53
|
removeLabel?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Moves removal INTO the stepper: at quantity 1 its `−` becomes a trash button
|
|
56
|
+
* named `removeLabel` that calls `onRemove`, and no separate remove control is
|
|
57
|
+
* drawn. Needs both `onQuantityChange` and `onRemove`; a sold-out line keeps
|
|
58
|
+
* the separate control. Default `false` — the stepper floors at 1.
|
|
59
|
+
*/
|
|
60
|
+
removeInStepper?: boolean;
|
|
48
61
|
/** Default `comfortable`. */
|
|
49
62
|
density?: CartDensity;
|
|
50
63
|
/** Replaces the composed name ("Ember flatbread, Large, Extra cheese, 2, €24.00"). */
|
|
@@ -151,6 +164,11 @@ export interface CartPanelProps {
|
|
|
151
164
|
onLineQuantityChange?: (id: string, quantity: number) => void;
|
|
152
165
|
/** With it every line draws a remove control. */
|
|
153
166
|
onLineRemove?: (id: string) => void;
|
|
167
|
+
/**
|
|
168
|
+
* Every line removes from its stepper at quantity 1 instead of drawing a
|
|
169
|
+
* separate remove control (`CartLine`'s `removeInStepper`). Default `false`.
|
|
170
|
+
*/
|
|
171
|
+
removeInStepper?: boolean;
|
|
154
172
|
|
|
155
173
|
/** The shortfall warning. Drawn above the totals, where the number it is about is. */
|
|
156
174
|
minimumOrder?: CartMinimumOrder;
|
|
@@ -15,6 +15,9 @@ import type { CheckoutConfirmProps } from './types';
|
|
|
15
15
|
* terms under it, and the state where it must not fire twice.
|
|
16
16
|
*
|
|
17
17
|
* button `Button` primary large, full width, "Place order · €24.80"
|
|
18
|
+
* second `secondaryAmount` ("≈ 12,00 €"), 8 under the button, the same
|
|
19
|
+
* centred secondary line as the terms — the button carries ONE
|
|
20
|
+
* amount, the one the charge is made in
|
|
18
21
|
* terms 12 under it; the centred secondary line every Bloom action card
|
|
19
22
|
* draws under its primary (`ActionCardNote`)
|
|
20
23
|
* footer 16 under the terms
|
|
@@ -44,6 +47,7 @@ import type { CheckoutConfirmProps } from './types';
|
|
|
44
47
|
function CheckoutConfirmComponent({
|
|
45
48
|
label = CHECKOUT_CONFIRM_LABEL,
|
|
46
49
|
amount,
|
|
50
|
+
secondaryAmount,
|
|
47
51
|
terms,
|
|
48
52
|
onConfirm,
|
|
49
53
|
disabled = false,
|
|
@@ -90,6 +94,11 @@ function CheckoutConfirmComponent({
|
|
|
90
94
|
>
|
|
91
95
|
{words}
|
|
92
96
|
</Button>
|
|
97
|
+
{secondaryAmount ? (
|
|
98
|
+
<ActionCardNote style={{ marginTop: 8 }} testID={id('secondary-amount')}>
|
|
99
|
+
{secondaryAmount}
|
|
100
|
+
</ActionCardNote>
|
|
101
|
+
) : null}
|
|
93
102
|
{terms != null ? (
|
|
94
103
|
<ActionCardNote style={{ marginTop: 12 }} testID={id('terms')}>
|
|
95
104
|
{terms}
|
|
@@ -89,6 +89,12 @@ export interface CheckoutConfirmProps {
|
|
|
89
89
|
* press commits to and the press are one decision.
|
|
90
90
|
*/
|
|
91
91
|
amount?: string;
|
|
92
|
+
/**
|
|
93
|
+
* A second, PRE-FORMATTED rendering of `amount` — the buyer's currency beside
|
|
94
|
+
* the shop's, `"≈ 12,00 €"` — drawn de-emphasised, centred under the button.
|
|
95
|
+
* Named like `PriceLine.secondaryAmount`; drawn as given, nothing converts.
|
|
96
|
+
*/
|
|
97
|
+
secondaryAmount?: string;
|
|
92
98
|
/**
|
|
93
99
|
* The terms line under the button — a string, or a node with links in it.
|
|
94
100
|
* `null` draws none.
|
package/src/chip/Chip.tsx
CHANGED
|
@@ -242,8 +242,10 @@ const ChipComponent = forwardRef<View, ChipProps>(function ChipComponent(
|
|
|
242
242
|
// WHICH attribute is a property of the ROLE, not a preference — a `radio`
|
|
243
243
|
// carries `aria-checked` and a `tab` `aria-selected`; `aria-pressed` on
|
|
244
244
|
// either is invalid, and `aria-selected` on a `button` is too.
|
|
245
|
+
// A `checkbox` (a multi-select filter chip) reads `aria-checked` like a
|
|
246
|
+
// `radio`; what differs is that its group allows several at once.
|
|
245
247
|
const stateProps =
|
|
246
|
-
role === 'radio'
|
|
248
|
+
role === 'radio' || role === 'checkbox'
|
|
247
249
|
? { accessibilityState: { checked: selected, disabled }, 'aria-checked': selected }
|
|
248
250
|
: role === 'tab'
|
|
249
251
|
? { accessibilityState: { selected, disabled }, 'aria-selected': selected }
|
package/src/chip/types.ts
CHANGED
|
@@ -26,10 +26,11 @@ export type ChipHue = 'lime' | 'rose' | 'yellow' | 'cyan' | 'blue' | 'purple' |
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* What a pressable chip IS, which decides the state ARIA reads it by:
|
|
29
|
-
* a toggle `button` (`aria-pressed`), a `
|
|
30
|
-
* (`aria-checked`),
|
|
29
|
+
* a toggle `button` (`aria-pressed`), a `checkbox` in a multi-select filter
|
|
30
|
+
* group (`aria-checked`), a `radio` in a radiogroup (`aria-checked`), or a
|
|
31
|
+
* `tab` in a tablist (`aria-selected`).
|
|
31
32
|
*/
|
|
32
|
-
export type ChipRole = 'button' | 'radio' | 'tab';
|
|
33
|
+
export type ChipRole = 'button' | 'checkbox' | 'radio' | 'tab';
|
|
33
34
|
|
|
34
35
|
export interface ChipProps {
|
|
35
36
|
appearance?: BloomAppearance;
|
|
@@ -84,9 +85,10 @@ export interface ChipProps {
|
|
|
84
85
|
selected?: boolean;
|
|
85
86
|
/**
|
|
86
87
|
* What a pressable chip is to assistive technology. Default `button`
|
|
87
|
-
* (`aria-pressed`). Use `
|
|
88
|
-
* `
|
|
89
|
-
* `aria-pressed`
|
|
88
|
+
* (`aria-pressed`). Use `checkbox` for a multi-select filter chip (in a
|
|
89
|
+
* `group`), `radio` inside a `radiogroup` and `tab` inside a `tablist` — the
|
|
90
|
+
* state attribute follows the role, and a `radio` announcing `aria-pressed`
|
|
91
|
+
* is invalid.
|
|
90
92
|
*/
|
|
91
93
|
role?: ChipRole;
|
|
92
94
|
/** Whether the chip is disabled. */
|
package/src/dialog/Dialog.tsx
CHANGED
|
@@ -28,6 +28,7 @@ import Animated, {
|
|
|
28
28
|
import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
|
|
29
29
|
import { Backdrop, OverlayRoot } from '../overlay';
|
|
30
30
|
import { useTheme } from '../theme/use-theme';
|
|
31
|
+
import { useIsRtl } from '../hooks/use-is-rtl';
|
|
31
32
|
import { bloomShadowStyle } from '../design-tokens/shadows';
|
|
32
33
|
import { StyledView } from '../styles/styled-primitives';
|
|
33
34
|
import { SurfaceLevelProvider } from '../styles/surface-levels';
|
|
@@ -51,6 +52,8 @@ import {
|
|
|
51
52
|
PANEL_RADIUS,
|
|
52
53
|
SIDE_SHEET_MIN_GUTTER,
|
|
53
54
|
useResolvedPlacement,
|
|
55
|
+
physicalDialogSide,
|
|
56
|
+
type DialogSidePlacement,
|
|
54
57
|
} from './placement';
|
|
55
58
|
import type {
|
|
56
59
|
DialogAction,
|
|
@@ -137,9 +140,9 @@ function CenteredOrSideDialog({
|
|
|
137
140
|
containerClassName,
|
|
138
141
|
label,
|
|
139
142
|
children,
|
|
140
|
-
}: Omit<DialogProps, 'placement'> & { placement: 'center' |
|
|
143
|
+
}: Omit<DialogProps, 'placement'> & { placement: 'center' | DialogSidePlacement }) {
|
|
141
144
|
const isControlled = controlledOpen !== undefined;
|
|
142
|
-
const isSide = placement
|
|
145
|
+
const isSide = placement !== 'center';
|
|
143
146
|
const headerController = useDialogHeaderController();
|
|
144
147
|
const scrollableResolved = scrollable ?? true;
|
|
145
148
|
const { height: viewportHeight } = useWindowDimensions();
|
|
@@ -422,7 +425,7 @@ function SideSheet({
|
|
|
422
425
|
}: {
|
|
423
426
|
open: boolean;
|
|
424
427
|
onDismiss: () => void;
|
|
425
|
-
side:
|
|
428
|
+
side: DialogSidePlacement;
|
|
426
429
|
width: number;
|
|
427
430
|
inset?: DialogInset;
|
|
428
431
|
dismissOnBackdrop: boolean;
|
|
@@ -453,8 +456,16 @@ function SideSheet({
|
|
|
453
456
|
const insetBottom = inset?.bottom ?? 0;
|
|
454
457
|
const insetLeft = inset?.left ?? 0;
|
|
455
458
|
const insetRight = inset?.right ?? 0;
|
|
456
|
-
|
|
457
|
-
|
|
459
|
+
// `start`/`end` resolve to the physical edge they land on for everything a
|
|
460
|
+
// layout engine does not mirror: the slide sign, and which `inset` applies.
|
|
461
|
+
const rtl = useIsRtl();
|
|
462
|
+
const edge = physicalDialogSide(side, rtl);
|
|
463
|
+
// The POSITION of a logical placement is a logical key, resolved by Yoga
|
|
464
|
+
// against `I18nManager` — a physical `left`/`right` here would be swapped by
|
|
465
|
+
// React Native's default `swapLeftAndRightInRTL` and land on the wrong edge.
|
|
466
|
+
const anchorKey = side === 'start' ? 'insetInlineStart' : side === 'end' ? 'insetInlineEnd' : side;
|
|
467
|
+
const anchorInset = edge === 'left' ? insetLeft : insetRight;
|
|
468
|
+
const oppositeInset = edge === 'left' ? insetRight : insetLeft;
|
|
458
469
|
|
|
459
470
|
const sideWidth = useMemo(() => {
|
|
460
471
|
const available = viewportWidth - anchorInset - oppositeInset - SIDE_SHEET_MIN_GUTTER;
|
|
@@ -491,7 +502,7 @@ function SideSheet({
|
|
|
491
502
|
if (dismissOnBackdrop) onDismiss();
|
|
492
503
|
}, [dismissOnBackdrop, onDismiss]);
|
|
493
504
|
|
|
494
|
-
const hiddenSign =
|
|
505
|
+
const hiddenSign = edge === 'left' ? -1 : 1;
|
|
495
506
|
const panelAnimatedStyle = useAnimatedStyle(() => {
|
|
496
507
|
const hidden = 1 - progress.value;
|
|
497
508
|
return {
|
|
@@ -513,11 +524,11 @@ function SideSheet({
|
|
|
513
524
|
({
|
|
514
525
|
top: insetTop,
|
|
515
526
|
bottom: insetBottom,
|
|
516
|
-
[
|
|
527
|
+
[anchorKey]: anchorInset,
|
|
517
528
|
width: sideWidth,
|
|
518
529
|
borderRadius: PANEL_RADIUS,
|
|
519
530
|
}),
|
|
520
|
-
[insetTop, insetBottom,
|
|
531
|
+
[insetTop, insetBottom, anchorKey, anchorInset, sideWidth],
|
|
521
532
|
);
|
|
522
533
|
|
|
523
534
|
if (!mounted) return null;
|
|
@@ -45,6 +45,7 @@ import { Z_INDEX } from '../styles/z-index';
|
|
|
45
45
|
import { WEB_POSITION_FIXED, type WebCssStyle } from '../styles/web-view-style';
|
|
46
46
|
import { bloomShadowStyle } from '../design-tokens/shadows';
|
|
47
47
|
import { useTheme } from '../theme/use-theme';
|
|
48
|
+
import { useIsRtl } from '../hooks/use-is-rtl';
|
|
48
49
|
import { Context, useDialogContext, useDialogControl } from './context';
|
|
49
50
|
import { DialogBody } from './DialogContent';
|
|
50
51
|
import { DialogBottomSheet } from './DialogBottomSheet';
|
|
@@ -69,6 +70,8 @@ import {
|
|
|
69
70
|
PANEL_RADIUS,
|
|
70
71
|
SIDE_SHEET_MIN_GUTTER,
|
|
71
72
|
useResolvedPlacement,
|
|
73
|
+
physicalDialogSide,
|
|
74
|
+
type DialogSidePlacement,
|
|
72
75
|
} from './placement';
|
|
73
76
|
import type {
|
|
74
77
|
DialogAction,
|
|
@@ -196,7 +199,7 @@ function CenterOrSideDialog({
|
|
|
196
199
|
containerClassName,
|
|
197
200
|
label,
|
|
198
201
|
children,
|
|
199
|
-
}: Omit<DialogProps, 'placement'> & { placement: 'center' |
|
|
202
|
+
}: Omit<DialogProps, 'placement'> & { placement: 'center' | DialogSidePlacement }) {
|
|
200
203
|
// Inject the required @keyframes on mount (before the panel ever appears —
|
|
201
204
|
// this component renders once with `isOpen=false` prior to opening, so the
|
|
202
205
|
// stylesheet is present by the time the animated surface mounts).
|
|
@@ -647,7 +650,7 @@ function SheetSurface({
|
|
|
647
650
|
actions?: DialogAction[];
|
|
648
651
|
header?: DialogProps['header'];
|
|
649
652
|
scrollable?: boolean;
|
|
650
|
-
placement:
|
|
653
|
+
placement: DialogSidePlacement;
|
|
651
654
|
shown: boolean;
|
|
652
655
|
width: number;
|
|
653
656
|
inset?: DialogInset;
|
|
@@ -666,6 +669,7 @@ function SheetSurface({
|
|
|
666
669
|
const descriptionId = useId();
|
|
667
670
|
const headerController = useDialogHeaderController();
|
|
668
671
|
const { width: viewportWidth } = useWindowDimensions();
|
|
672
|
+
const rtl = useIsRtl();
|
|
669
673
|
|
|
670
674
|
// Defer the entry transition by a frame so the start state paints before the
|
|
671
675
|
// browser animates to `shown`. `entered` is false on the first committed
|
|
@@ -707,22 +711,26 @@ function SheetSurface({
|
|
|
707
711
|
const insetBottom = inset?.bottom ?? 0;
|
|
708
712
|
const insetLeft = inset?.left ?? 0;
|
|
709
713
|
const insetRight = inset?.right ?? 0;
|
|
710
|
-
|
|
711
|
-
|
|
714
|
+
// `start`/`end` land on a physical edge read from `useIsRtl()`. The drawer
|
|
715
|
+
// is portaled outside any `dir` context, so react-native-web would resolve
|
|
716
|
+
// a logical key left-to-right; the physical edge is the one that is right.
|
|
717
|
+
const edge = physicalDialogSide(placement, rtl);
|
|
718
|
+
const anchorInset = edge === 'left' ? insetLeft : insetRight;
|
|
719
|
+
const oppositeInset = edge === 'left' ? insetRight : insetLeft;
|
|
712
720
|
const available = viewportWidth - anchorInset - oppositeInset - SIDE_SHEET_MIN_GUTTER;
|
|
713
721
|
const cappedWidth = Math.max(0, Math.min(width, available));
|
|
714
|
-
const hiddenSign =
|
|
722
|
+
const hiddenSign = edge === 'left' ? '-100%' : '100%';
|
|
715
723
|
|
|
716
724
|
return {
|
|
717
725
|
top: insetTop,
|
|
718
726
|
bottom: insetBottom,
|
|
719
|
-
[
|
|
727
|
+
[edge]: anchorInset,
|
|
720
728
|
width: cappedWidth,
|
|
721
729
|
borderRadius: PANEL_RADIUS,
|
|
722
730
|
transform: [{ translateX: visible ? 0 : hiddenSign }],
|
|
723
731
|
opacity: visible ? 1 : 0,
|
|
724
732
|
};
|
|
725
|
-
}, [visible, placement, width, inset, viewportWidth]);
|
|
733
|
+
}, [visible, placement, rtl, width, inset, viewportWidth]);
|
|
726
734
|
|
|
727
735
|
const handleBackdropPress = useCallback(() => {
|
|
728
736
|
if (dismissOnBackdrop) onDismiss();
|
package/src/dialog/placement.ts
CHANGED
|
@@ -7,10 +7,27 @@ import { BREAKPOINTS } from '../styles/breakpoints';
|
|
|
7
7
|
* Anchor a `<Dialog>` can render against.
|
|
8
8
|
*
|
|
9
9
|
* - `'center'` — the default centered modal card (current behavior).
|
|
10
|
-
* - `'left'` / `'right'` — an anchored side-sheet / drawer.
|
|
10
|
+
* - `'left'` / `'right'` — an anchored side-sheet / drawer on that PHYSICAL edge.
|
|
11
|
+
* - `'start'` / `'end'` — the same drawer on the LOGICAL edge: `start` is the
|
|
12
|
+
* left in a left-to-right layout and the right in a right-to-left one, and it
|
|
13
|
+
* slides in from that edge. Resolved with `useIsRtl()`.
|
|
11
14
|
* - `'bottom'` — a bottom-sheet (drag handle, slide-up).
|
|
12
15
|
*/
|
|
13
|
-
export type DialogPlacement = 'center' | 'left' | 'right' | 'bottom';
|
|
16
|
+
export type DialogPlacement = 'center' | 'left' | 'right' | 'start' | 'end' | 'bottom';
|
|
17
|
+
|
|
18
|
+
/** The four side-sheet placements, physical and logical. */
|
|
19
|
+
export type DialogSidePlacement = 'left' | 'right' | 'start' | 'end';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The PHYSICAL edge a side placement lands on. `left`/`right` are themselves;
|
|
23
|
+
* `start` is `left` left-to-right and `right` right-to-left, `end` the reverse.
|
|
24
|
+
* The panel's `translateX` sign, which no layout engine mirrors, follows this.
|
|
25
|
+
*/
|
|
26
|
+
export function physicalDialogSide(side: DialogSidePlacement, rtl: boolean): 'left' | 'right' {
|
|
27
|
+
if (side === 'start') return rtl ? 'right' : 'left';
|
|
28
|
+
if (side === 'end') return rtl ? 'left' : 'right';
|
|
29
|
+
return side;
|
|
30
|
+
}
|
|
14
31
|
|
|
15
32
|
/**
|
|
16
33
|
* A `<Dialog>` placement that may vary by viewport width.
|
package/src/dialog/types.ts
CHANGED
|
@@ -256,8 +256,9 @@ export type DialogProps = React.PropsWithChildren<{
|
|
|
256
256
|
actions?: DialogAction[];
|
|
257
257
|
/**
|
|
258
258
|
* Surface anchor. Defaults to `'center'` (the centered modal — current
|
|
259
|
-
* behavior). `'left'`/`'right'` render an anchored side-sheet
|
|
260
|
-
*
|
|
259
|
+
* behavior). `'left'`/`'right'` render an anchored side-sheet on that
|
|
260
|
+
* physical edge; `'start'`/`'end'` on the logical edge (mirrored, with its
|
|
261
|
+
* slide-in, in a right-to-left layout); `'bottom'` a bottom-sheet. A responsive map resolves by viewport width, e.g.
|
|
261
262
|
* `{ base: 'bottom', md: 'left' }` (drawer on wide, sheet on narrow).
|
|
262
263
|
*/
|
|
263
264
|
placement?: ResponsiveDialogPlacement;
|
package/src/loading/Loading.tsx
CHANGED
|
@@ -18,7 +18,7 @@ import { resolveBloomColors } from '../appearance/colors';
|
|
|
18
18
|
import { useBloomAppearance } from '../appearance';
|
|
19
19
|
import { animation } from '../styles/tokens';
|
|
20
20
|
import { useTheme } from '../theme/use-theme';
|
|
21
|
-
import { bindLoading, SIZE_CONFIG, styles } from './LoadingBase';
|
|
21
|
+
import { bindLoading, LoadingRoot, SIZE_CONFIG, styles } from './LoadingBase';
|
|
22
22
|
import { SpinnerIcon } from './SpinnerIcon';
|
|
23
23
|
import type { TopLoadingProps } from './types';
|
|
24
24
|
|
|
@@ -31,6 +31,7 @@ const TopLoading: React.FC<TopLoadingProps> = ({
|
|
|
31
31
|
iconSize,
|
|
32
32
|
heightOffset = 0,
|
|
33
33
|
spinnerIcon,
|
|
34
|
+
accessibilityLabel,
|
|
34
35
|
testID,
|
|
35
36
|
}) => {
|
|
36
37
|
const theme = useTheme();
|
|
@@ -69,7 +70,10 @@ const TopLoading: React.FC<TopLoadingProps> = ({
|
|
|
69
70
|
return (
|
|
70
71
|
<Animated.View style={[styles.topContainer, containerAnimated]} testID={testID}>
|
|
71
72
|
<Animated.View style={[styles.topLoadingView, { height: targetHeight }, innerAnimated, style]}>
|
|
72
|
-
{
|
|
73
|
+
{/* Named only while it is showing: a collapsed bar is not a wait. */}
|
|
74
|
+
<LoadingRoot accessibilityLabel={showLoading ? accessibilityLabel : undefined}>
|
|
75
|
+
{spinnerIcon ?? <SpinnerIcon size={effectiveIconSize} color={spinnerColor} />}
|
|
76
|
+
</LoadingRoot>
|
|
73
77
|
</Animated.View>
|
|
74
78
|
</Animated.View>
|
|
75
79
|
);
|
|
@@ -16,7 +16,7 @@ import { useBloomAppearance } from '../appearance';
|
|
|
16
16
|
import { animation } from '../styles/tokens';
|
|
17
17
|
import type { WebCssStyle } from '../styles/web-view-style';
|
|
18
18
|
import { useTheme } from '../theme/use-theme';
|
|
19
|
-
import { bindLoading, SIZE_CONFIG, styles } from './LoadingBase';
|
|
19
|
+
import { bindLoading, LoadingRoot, SIZE_CONFIG, styles } from './LoadingBase';
|
|
20
20
|
import { SpinnerIcon } from './SpinnerIcon.web';
|
|
21
21
|
import type { TopLoadingProps } from './types';
|
|
22
22
|
|
|
@@ -41,6 +41,7 @@ const TopLoading: React.FC<TopLoadingProps> = ({
|
|
|
41
41
|
iconSize,
|
|
42
42
|
heightOffset = 0,
|
|
43
43
|
spinnerIcon,
|
|
44
|
+
accessibilityLabel,
|
|
44
45
|
testID,
|
|
45
46
|
}) => {
|
|
46
47
|
const theme = useTheme();
|
|
@@ -84,7 +85,10 @@ const TopLoading: React.FC<TopLoadingProps> = ({
|
|
|
84
85
|
style,
|
|
85
86
|
]}
|
|
86
87
|
>
|
|
87
|
-
{
|
|
88
|
+
{/* Named only while it is showing: a collapsed bar is not a wait. */}
|
|
89
|
+
<LoadingRoot accessibilityLabel={showLoading ? accessibilityLabel : undefined}>
|
|
90
|
+
{spinnerIcon ?? <SpinnerIcon size={effectiveIconSize} color={spinnerColor} />}
|
|
91
|
+
</LoadingRoot>
|
|
88
92
|
</View>
|
|
89
93
|
</View>
|
|
90
94
|
);
|
|
@@ -23,7 +23,7 @@ import { normalizeBloomSize } from '../appearance/legacy';
|
|
|
23
23
|
import { resolveBloomColors } from '../appearance/colors';
|
|
24
24
|
import { useBloomAppearance } from '../appearance';
|
|
25
25
|
import React, { memo } from 'react';
|
|
26
|
-
import { View, Text, StyleSheet } from 'react-native';
|
|
26
|
+
import { View, Text, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
27
27
|
|
|
28
28
|
import { useTheme } from '../theme/use-theme';
|
|
29
29
|
import type {
|
|
@@ -41,6 +41,54 @@ export const SIZE_CONFIG = {
|
|
|
41
41
|
lg: { spinner: 44, text: 16 },
|
|
42
42
|
} as const;
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The indicator's root: an indeterminate, NAMED `progressbar` when the caller
|
|
46
|
+
* gave `accessibilityLabel`, a plain view (today's behaviour) when not.
|
|
47
|
+
*
|
|
48
|
+
* Flat `aria-*` because react-native-web drops `accessibilityState` /
|
|
49
|
+
* `accessibilityValue`; React Native folds them back. `aria-valuenow` is
|
|
50
|
+
* deliberately absent (`undefined`): that is what makes a progressbar
|
|
51
|
+
* INDETERMINATE in ARIA, and a spinner has no amount done to report. The role
|
|
52
|
+
* is a literal on its own branch so `aria-state-source-census.test.ts` can read
|
|
53
|
+
* it; a computed role would be invisible to it.
|
|
54
|
+
*/
|
|
55
|
+
export function LoadingRoot({
|
|
56
|
+
accessibilityLabel,
|
|
57
|
+
style,
|
|
58
|
+
testID,
|
|
59
|
+
children,
|
|
60
|
+
}: {
|
|
61
|
+
accessibilityLabel?: string;
|
|
62
|
+
style?: StyleProp<ViewStyle>;
|
|
63
|
+
testID?: string;
|
|
64
|
+
children: React.ReactNode;
|
|
65
|
+
}) {
|
|
66
|
+
if (accessibilityLabel) {
|
|
67
|
+
return (
|
|
68
|
+
<View
|
|
69
|
+
role="progressbar"
|
|
70
|
+
accessible
|
|
71
|
+
accessibilityLabel={accessibilityLabel}
|
|
72
|
+
aria-valuemin={0}
|
|
73
|
+
aria-valuemax={100}
|
|
74
|
+
aria-valuenow={undefined}
|
|
75
|
+
aria-busy
|
|
76
|
+
style={style}
|
|
77
|
+
testID={testID}
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
// Nothing to carry (the `top` variant's inner slot): no extra node at all.
|
|
84
|
+
if (style === undefined && testID === undefined) return <>{children}</>;
|
|
85
|
+
return (
|
|
86
|
+
<View style={style} testID={testID}>
|
|
87
|
+
{children}
|
|
88
|
+
</View>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
44
92
|
/** The two platform pieces a fork supplies. */
|
|
45
93
|
export interface LoadingPlatform {
|
|
46
94
|
/** Forked: react-native-svg on native, a DOM `<svg>` on web. */
|
|
@@ -61,6 +109,7 @@ export function bindLoading({ SpinnerIcon, TopLoading }: LoadingPlatform) {
|
|
|
61
109
|
showText = true,
|
|
62
110
|
iconSize,
|
|
63
111
|
spinnerIcon,
|
|
112
|
+
accessibilityLabel,
|
|
64
113
|
testID,
|
|
65
114
|
}) => {
|
|
66
115
|
const theme = useTheme();
|
|
@@ -71,7 +120,7 @@ export function bindLoading({ SpinnerIcon, TopLoading }: LoadingPlatform) {
|
|
|
71
120
|
const textColor = color ?? theme.colors.textSecondary;
|
|
72
121
|
|
|
73
122
|
return (
|
|
74
|
-
<
|
|
123
|
+
<LoadingRoot accessibilityLabel={accessibilityLabel} style={[styles.container, style]} testID={testID}>
|
|
75
124
|
{spinnerIcon ?? <SpinnerIcon size={effectiveIconSize} color={spinnerColor} className={className} />}
|
|
76
125
|
{showText && text && (
|
|
77
126
|
<Text
|
|
@@ -84,7 +133,7 @@ export function bindLoading({ SpinnerIcon, TopLoading }: LoadingPlatform) {
|
|
|
84
133
|
{text}
|
|
85
134
|
</Text>
|
|
86
135
|
)}
|
|
87
|
-
</
|
|
136
|
+
</LoadingRoot>
|
|
88
137
|
);
|
|
89
138
|
};
|
|
90
139
|
|
|
@@ -96,6 +145,7 @@ export function bindLoading({ SpinnerIcon, TopLoading }: LoadingPlatform) {
|
|
|
96
145
|
style,
|
|
97
146
|
textStyle,
|
|
98
147
|
spinnerIcon,
|
|
148
|
+
accessibilityLabel,
|
|
99
149
|
testID,
|
|
100
150
|
}) => {
|
|
101
151
|
const theme = useTheme();
|
|
@@ -105,7 +155,7 @@ export function bindLoading({ SpinnerIcon, TopLoading }: LoadingPlatform) {
|
|
|
105
155
|
const textColor = theme.colors.textSecondary;
|
|
106
156
|
|
|
107
157
|
return (
|
|
108
|
-
<
|
|
158
|
+
<LoadingRoot accessibilityLabel={accessibilityLabel} style={[styles.inlineContainer, style]} testID={testID}>
|
|
109
159
|
{spinnerIcon ?? <SpinnerIcon size={sizeConfig.spinner} color={spinnerColor} />}
|
|
110
160
|
{text && (
|
|
111
161
|
<Text
|
|
@@ -117,7 +167,7 @@ export function bindLoading({ SpinnerIcon, TopLoading }: LoadingPlatform) {
|
|
|
117
167
|
{text}
|
|
118
168
|
</Text>
|
|
119
169
|
)}
|
|
120
|
-
</
|
|
170
|
+
</LoadingRoot>
|
|
121
171
|
);
|
|
122
172
|
};
|
|
123
173
|
|
package/src/loading/types.ts
CHANGED
|
@@ -20,6 +20,17 @@ interface BaseLoadingProps {
|
|
|
20
20
|
style?: ViewStyle;
|
|
21
21
|
/** Whether loading is active (for animated variants) */
|
|
22
22
|
showLoading?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Names the indicator and makes it an indeterminate `progressbar` to
|
|
25
|
+
* assistive technology (`role="progressbar"` with `aria-label` and
|
|
26
|
+
* `aria-busy` on web; the same role, label and busy state natively). A
|
|
27
|
+
* spinner draws no words, so nothing else can say what is loading.
|
|
28
|
+
*
|
|
29
|
+
* Opt-in: without it the indicator carries no role, as before — which is
|
|
30
|
+
* right when it sits INSIDE a named progressbar already (an upload overlay),
|
|
31
|
+
* where a second one would announce the same wait twice.
|
|
32
|
+
*/
|
|
33
|
+
accessibilityLabel?: string;
|
|
23
34
|
testID?: string;
|
|
24
35
|
}
|
|
25
36
|
|