@oxy.so/bloom 2.11.0 → 2.12.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/app-shell.mdx +90 -4
- package/docs/content-panel.mdx +65 -19
- package/docs/date-picker.mdx +80 -2
- package/docs/listing-card.mdx +54 -0
- package/docs/rating.mdx +52 -2
- package/docs/stay-search.mdx +38 -0
- package/lib/commonjs/app-shell/AppShell.js +121 -5
- package/lib/commonjs/app-shell/AppShell.js.map +1 -1
- package/lib/commonjs/app-shell/layout.js +10 -5
- package/lib/commonjs/app-shell/layout.js.map +1 -1
- package/lib/commonjs/content-panel/ContentPanel.js +8 -2
- package/lib/commonjs/content-panel/ContentPanel.js.map +1 -1
- package/lib/commonjs/content-panel/ContentPanel.web.js +38 -25
- package/lib/commonjs/content-panel/ContentPanel.web.js.map +1 -1
- package/lib/commonjs/date-picker/TimeField.js +175 -0
- package/lib/commonjs/date-picker/TimeField.js.map +1 -0
- package/lib/commonjs/date-picker/index.js +7 -0
- package/lib/commonjs/date-picker/index.js.map +1 -1
- package/lib/commonjs/date-picker/time.js +160 -0
- package/lib/commonjs/date-picker/time.js.map +1 -0
- 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/listing-card/ListingCard.js +34 -1
- package/lib/commonjs/listing-card/ListingCard.js.map +1 -1
- package/lib/commonjs/listing-card/WishlistCard.js +50 -8
- package/lib/commonjs/listing-card/WishlistCard.js.map +1 -1
- package/lib/commonjs/listing-card/shared.js +23 -1
- package/lib/commonjs/listing-card/shared.js.map +1 -1
- package/lib/commonjs/rating/RatingInput.js +218 -0
- package/lib/commonjs/rating/RatingInput.js.map +1 -0
- package/lib/commonjs/rating/index.js +7 -0
- package/lib/commonjs/rating/index.js.map +1 -1
- package/lib/commonjs/stay-search/DestinationSuggestions.js +46 -9
- package/lib/commonjs/stay-search/DestinationSuggestions.js.map +1 -1
- package/lib/commonjs/stay-search/GuestPicker.js +9 -0
- package/lib/commonjs/stay-search/GuestPicker.js.map +1 -1
- package/lib/commonjs/track-list/parts.js +0 -13
- package/lib/commonjs/track-list/parts.js.map +1 -1
- package/lib/module/app-shell/AppShell.js +121 -5
- package/lib/module/app-shell/AppShell.js.map +1 -1
- package/lib/module/app-shell/layout.js +10 -5
- package/lib/module/app-shell/layout.js.map +1 -1
- package/lib/module/content-panel/ContentPanel.js +8 -2
- package/lib/module/content-panel/ContentPanel.js.map +1 -1
- package/lib/module/content-panel/ContentPanel.web.js +38 -25
- package/lib/module/content-panel/ContentPanel.web.js.map +1 -1
- package/lib/module/date-picker/TimeField.js +170 -0
- package/lib/module/date-picker/TimeField.js.map +1 -0
- package/lib/module/date-picker/index.js +3 -0
- package/lib/module/date-picker/index.js.map +1 -1
- package/lib/module/date-picker/time.js +151 -0
- package/lib/module/date-picker/time.js.map +1 -0
- 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/listing-card/ListingCard.js +34 -1
- package/lib/module/listing-card/ListingCard.js.map +1 -1
- package/lib/module/listing-card/WishlistCard.js +50 -8
- package/lib/module/listing-card/WishlistCard.js.map +1 -1
- package/lib/module/listing-card/shared.js +22 -0
- package/lib/module/listing-card/shared.js.map +1 -1
- package/lib/module/rating/RatingInput.js +212 -0
- package/lib/module/rating/RatingInput.js.map +1 -0
- package/lib/module/rating/index.js +1 -0
- package/lib/module/rating/index.js.map +1 -1
- package/lib/module/stay-search/DestinationSuggestions.js +45 -9
- package/lib/module/stay-search/DestinationSuggestions.js.map +1 -1
- package/lib/module/stay-search/GuestPicker.js +9 -0
- package/lib/module/stay-search/GuestPicker.js.map +1 -1
- package/lib/module/track-list/parts.js +1 -14
- package/lib/module/track-list/parts.js.map +1 -1
- package/lib/typescript/commonjs/app-shell/AppShell.d.ts.map +1 -1
- package/lib/typescript/commonjs/app-shell/layout.d.ts +5 -4
- package/lib/typescript/commonjs/app-shell/layout.d.ts.map +1 -1
- package/lib/typescript/commonjs/app-shell/types.d.ts +12 -2
- package/lib/typescript/commonjs/app-shell/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/content-panel/ContentPanel.d.ts.map +1 -1
- package/lib/typescript/commonjs/content-panel/ContentPanel.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/content-panel/types.d.ts +53 -42
- package/lib/typescript/commonjs/content-panel/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/date-picker/TimeField.d.ts +6 -0
- package/lib/typescript/commonjs/date-picker/TimeField.d.ts.map +1 -0
- package/lib/typescript/commonjs/date-picker/index.d.ts +2 -1
- package/lib/typescript/commonjs/date-picker/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/date-picker/time.d.ts +56 -0
- package/lib/typescript/commonjs/date-picker/time.d.ts.map +1 -0
- package/lib/typescript/commonjs/date-picker/types.d.ts +49 -3
- package/lib/typescript/commonjs/date-picker/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -2
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/listing-card/ListingCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/listing-card/WishlistCard.d.ts +1 -1
- package/lib/typescript/commonjs/listing-card/WishlistCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/listing-card/shared.d.ts +8 -1
- package/lib/typescript/commonjs/listing-card/shared.d.ts.map +1 -1
- package/lib/typescript/commonjs/listing-card/types.d.ts +37 -0
- package/lib/typescript/commonjs/listing-card/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/rating/RatingInput.d.ts +8 -0
- package/lib/typescript/commonjs/rating/RatingInput.d.ts.map +1 -0
- package/lib/typescript/commonjs/rating/index.d.ts +2 -1
- package/lib/typescript/commonjs/rating/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/rating/types.d.ts +31 -0
- package/lib/typescript/commonjs/rating/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/stay-search/DestinationSuggestions.d.ts +8 -0
- package/lib/typescript/commonjs/stay-search/DestinationSuggestions.d.ts.map +1 -1
- package/lib/typescript/commonjs/stay-search/GuestPicker.d.ts +6 -1
- package/lib/typescript/commonjs/stay-search/GuestPicker.d.ts.map +1 -1
- package/lib/typescript/commonjs/stay-search/types.d.ts +20 -0
- package/lib/typescript/commonjs/stay-search/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/track-list/parts.d.ts.map +1 -1
- package/lib/typescript/module/app-shell/AppShell.d.ts.map +1 -1
- package/lib/typescript/module/app-shell/layout.d.ts +5 -4
- package/lib/typescript/module/app-shell/layout.d.ts.map +1 -1
- package/lib/typescript/module/app-shell/types.d.ts +12 -2
- package/lib/typescript/module/app-shell/types.d.ts.map +1 -1
- package/lib/typescript/module/content-panel/ContentPanel.d.ts.map +1 -1
- package/lib/typescript/module/content-panel/ContentPanel.web.d.ts.map +1 -1
- package/lib/typescript/module/content-panel/types.d.ts +53 -42
- package/lib/typescript/module/content-panel/types.d.ts.map +1 -1
- package/lib/typescript/module/date-picker/TimeField.d.ts +6 -0
- package/lib/typescript/module/date-picker/TimeField.d.ts.map +1 -0
- package/lib/typescript/module/date-picker/index.d.ts +2 -1
- package/lib/typescript/module/date-picker/index.d.ts.map +1 -1
- package/lib/typescript/module/date-picker/time.d.ts +56 -0
- package/lib/typescript/module/date-picker/time.d.ts.map +1 -0
- package/lib/typescript/module/date-picker/types.d.ts +49 -3
- package/lib/typescript/module/date-picker/types.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -2
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/listing-card/ListingCard.d.ts.map +1 -1
- package/lib/typescript/module/listing-card/WishlistCard.d.ts +1 -1
- package/lib/typescript/module/listing-card/WishlistCard.d.ts.map +1 -1
- package/lib/typescript/module/listing-card/shared.d.ts +8 -1
- package/lib/typescript/module/listing-card/shared.d.ts.map +1 -1
- package/lib/typescript/module/listing-card/types.d.ts +37 -0
- package/lib/typescript/module/listing-card/types.d.ts.map +1 -1
- package/lib/typescript/module/rating/RatingInput.d.ts +8 -0
- package/lib/typescript/module/rating/RatingInput.d.ts.map +1 -0
- package/lib/typescript/module/rating/index.d.ts +2 -1
- package/lib/typescript/module/rating/index.d.ts.map +1 -1
- package/lib/typescript/module/rating/types.d.ts +31 -0
- package/lib/typescript/module/rating/types.d.ts.map +1 -1
- package/lib/typescript/module/stay-search/DestinationSuggestions.d.ts +8 -0
- package/lib/typescript/module/stay-search/DestinationSuggestions.d.ts.map +1 -1
- package/lib/typescript/module/stay-search/GuestPicker.d.ts +6 -1
- package/lib/typescript/module/stay-search/GuestPicker.d.ts.map +1 -1
- package/lib/typescript/module/stay-search/types.d.ts +20 -0
- package/lib/typescript/module/stay-search/types.d.ts.map +1 -1
- package/lib/typescript/module/track-list/parts.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/app-shell/AppShell.tsx +91 -4
- package/src/app-shell/layout.ts +10 -5
- package/src/app-shell/types.ts +12 -2
- package/src/content-panel/ContentPanel.tsx +8 -2
- package/src/content-panel/ContentPanel.web.tsx +39 -25
- package/src/content-panel/types.ts +50 -42
- package/src/date-picker/TimeField.tsx +162 -0
- package/src/date-picker/index.ts +6 -0
- package/src/date-picker/time.ts +158 -0
- package/src/date-picker/types.ts +52 -3
- package/src/index.ts +9 -2
- package/src/index.web.ts +9 -2
- package/src/listing-card/ListingCard.tsx +34 -1
- package/src/listing-card/WishlistCard.tsx +42 -6
- package/src/listing-card/shared.ts +22 -0
- package/src/listing-card/types.ts +37 -0
- package/src/rating/RatingInput.tsx +214 -0
- package/src/rating/index.ts +9 -1
- package/src/rating/types.ts +33 -0
- package/src/stay-search/DestinationSuggestions.tsx +48 -9
- package/src/stay-search/GuestPicker.tsx +9 -0
- package/src/stay-search/types.ts +20 -0
- package/src/track-list/parts.tsx +1 -20
package/src/date-picker/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StyleProp, ViewStyle } from 'react-native';
|
|
1
|
+
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
|
|
2
2
|
|
|
3
3
|
/** An inclusive range of calendar days. Both ends are local-midnight `Date`s. */
|
|
4
4
|
export interface DateRange {
|
|
@@ -126,8 +126,14 @@ export interface MeetingSchedulerValue {
|
|
|
126
126
|
time: string | null;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
/**
|
|
130
|
-
|
|
129
|
+
/**
|
|
130
|
+
* How a time of day is drawn. The VALUE is always a 24h `"HH:mm"` string —
|
|
131
|
+
* this decides only what the reader sees.
|
|
132
|
+
*/
|
|
133
|
+
export type HourFormat = '12h' | '24h';
|
|
134
|
+
|
|
135
|
+
/** The hour-format toggle. An alias of {@link HourFormat}. */
|
|
136
|
+
export type MeetingSchedulerHourFormat = HourFormat;
|
|
131
137
|
|
|
132
138
|
/** Fixed strings, overridable for localisation. */
|
|
133
139
|
export interface MeetingSchedulerLabels {
|
|
@@ -169,3 +175,46 @@ export interface MeetingSchedulerProps extends CalendarConstraintProps {
|
|
|
169
175
|
style?: StyleProp<ViewStyle>;
|
|
170
176
|
testID?: string;
|
|
171
177
|
}
|
|
178
|
+
|
|
179
|
+
/** `small` is a 32-tall field with body-2 text; `medium` (default) 38 with body. */
|
|
180
|
+
export type TimeFieldSize = 'small' | 'medium';
|
|
181
|
+
|
|
182
|
+
export interface TimeFieldProps {
|
|
183
|
+
/**
|
|
184
|
+
* The time as a 24h `"HH:mm"` string, or `null` while the field is empty.
|
|
185
|
+
* Fully controlled. The same shape `MeetingScheduler` uses, so a value drops
|
|
186
|
+
* straight into one of its `timeSlots`.
|
|
187
|
+
*/
|
|
188
|
+
value: string | null;
|
|
189
|
+
/**
|
|
190
|
+
* Called with the committed time — on blur, on submit, or on an arrow key.
|
|
191
|
+
* `null` when the field has been emptied. Never called while typing.
|
|
192
|
+
*/
|
|
193
|
+
onChange: (value: string | null) => void;
|
|
194
|
+
/** Earliest time, 24h `"HH:mm"`. A typed time before it is pulled up to it. */
|
|
195
|
+
min?: string;
|
|
196
|
+
/** Latest time, 24h `"HH:mm"`. A typed time after it is pulled back to it. */
|
|
197
|
+
max?: string;
|
|
198
|
+
/**
|
|
199
|
+
* Minutes one arrow press moves, and the grid a typed time snaps to. Default
|
|
200
|
+
* `1` (no snapping); pass `15` or `30` for a booking grid. Counted from
|
|
201
|
+
* midnight, so 15 always means :00 :15 :30 :45.
|
|
202
|
+
*/
|
|
203
|
+
step?: number;
|
|
204
|
+
/** How the time is DRAWN. Default `'24h'`; the value stays 24h either way. */
|
|
205
|
+
hourFormat?: HourFormat;
|
|
206
|
+
/** Default `medium`. */
|
|
207
|
+
size?: TimeFieldSize;
|
|
208
|
+
disabled?: boolean;
|
|
209
|
+
/**
|
|
210
|
+
* The NAME of the time ("Viewing time", "Check-in"). Required: the field
|
|
211
|
+
* draws no label of its own, and `"--:--"` names nothing.
|
|
212
|
+
*/
|
|
213
|
+
accessibilityLabel: string;
|
|
214
|
+
/** The text while empty. Default `"--:--"`, or `"--:-- --"` in 12h. */
|
|
215
|
+
placeholder?: string;
|
|
216
|
+
/** Fixed width. Default: 104 at `medium`, 96 at `small`. */
|
|
217
|
+
width?: number;
|
|
218
|
+
style?: StyleProp<TextStyle>;
|
|
219
|
+
testID?: string;
|
|
220
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -292,8 +292,15 @@ export type { BottomSheetRef, BottomSheetProps } from './bottom-sheet';
|
|
|
292
292
|
// Data display
|
|
293
293
|
export * from './card';
|
|
294
294
|
export * from './badge';
|
|
295
|
-
export { Rating, RatingBar } from './rating';
|
|
296
|
-
export type {
|
|
295
|
+
export { Rating, RatingBar, RatingInput } from './rating';
|
|
296
|
+
export type {
|
|
297
|
+
RatingBarProps,
|
|
298
|
+
RatingCountStyle,
|
|
299
|
+
RatingInputProps,
|
|
300
|
+
RatingInputSize,
|
|
301
|
+
RatingProps,
|
|
302
|
+
RatingSize,
|
|
303
|
+
} from './rating';
|
|
297
304
|
export {
|
|
298
305
|
ExplicitBadge,
|
|
299
306
|
formatDuration,
|
package/src/index.web.ts
CHANGED
|
@@ -297,8 +297,15 @@ export type { BottomSheetRef, BottomSheetProps } from './bottom-sheet/index.web'
|
|
|
297
297
|
// Data display
|
|
298
298
|
export * from './card';
|
|
299
299
|
export * from './badge';
|
|
300
|
-
export { Rating, RatingBar } from './rating';
|
|
301
|
-
export type {
|
|
300
|
+
export { Rating, RatingBar, RatingInput } from './rating';
|
|
301
|
+
export type {
|
|
302
|
+
RatingBarProps,
|
|
303
|
+
RatingCountStyle,
|
|
304
|
+
RatingInputProps,
|
|
305
|
+
RatingInputSize,
|
|
306
|
+
RatingProps,
|
|
307
|
+
RatingSize,
|
|
308
|
+
} from './rating';
|
|
302
309
|
export {
|
|
303
310
|
ExplicitBadge,
|
|
304
311
|
formatDuration,
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
LISTING_CARD_STYLE_ID,
|
|
49
49
|
PHOTO_ASPECT_RATIO,
|
|
50
50
|
PHOTO_RADIUS,
|
|
51
|
+
PHOTO_ZOOM_SCALE,
|
|
51
52
|
resolveListingCardPaint,
|
|
52
53
|
locationText,
|
|
53
54
|
resolvePhoto,
|
|
@@ -91,6 +92,21 @@ import type { ListingCardLayout, ListingCardProps } from './types';
|
|
|
91
92
|
*
|
|
92
93
|
* The overlay repeats the photo's geometry (the same width share and aspect
|
|
93
94
|
* ratio) rather than measuring it, so it is right on the first frame.
|
|
95
|
+
*
|
|
96
|
+
* `hoverZoom` scales the photo to {@link PHOTO_ZOOM_SCALE} while a pointer is
|
|
97
|
+
* over the card — a CSS transform on the photo inside the clipping tile, WEB
|
|
98
|
+
* ONLY (`@media (any-hover: hover)`, so a touch screen that reports a hover it
|
|
99
|
+
* cannot deliver is excluded) and nothing at all under reduced motion. It is
|
|
100
|
+
* off by default: a card that grows under the pointer is a decision an app
|
|
101
|
+
* makes, not one Bloom makes for every app that already ships this card. The
|
|
102
|
+
* compact density ignores it — a 112px thumbnail has nothing to come forward
|
|
103
|
+
* from.
|
|
104
|
+
*
|
|
105
|
+
* `onLongPress` and `onContextMenu` are the shortcut to a sheet or a menu, and
|
|
106
|
+
* they carry `MessageBubble`'s contract verbatim: a long press on native (and
|
|
107
|
+
* on a web touch screen), right-click on web with the event defaulted. Neither
|
|
108
|
+
* has a keyboard spelling, so whatever they reach must be reachable another
|
|
109
|
+
* way.
|
|
94
110
|
*/
|
|
95
111
|
|
|
96
112
|
const TEXT_GAP = 12;
|
|
@@ -198,6 +214,7 @@ function PhotoTrack({
|
|
|
198
214
|
source={{ uri }}
|
|
199
215
|
resizeMode="cover"
|
|
200
216
|
accessibilityIgnoresInvertColors
|
|
217
|
+
{...webDataSet({ bloomListingCardPhoto: '' })}
|
|
201
218
|
style={{ width: '100%', height: '100%' }}
|
|
202
219
|
/>
|
|
203
220
|
) : null}
|
|
@@ -623,6 +640,9 @@ function ListingCardComponent(props: ListingCardProps) {
|
|
|
623
640
|
favorite = false,
|
|
624
641
|
onFavoriteChange,
|
|
625
642
|
onPress,
|
|
643
|
+
onLongPress,
|
|
644
|
+
onContextMenu,
|
|
645
|
+
hoverZoom = false,
|
|
626
646
|
href,
|
|
627
647
|
loading = false,
|
|
628
648
|
layout = 'vertical',
|
|
@@ -727,6 +747,17 @@ function ListingCardComponent(props: ListingCardProps) {
|
|
|
727
747
|
role: href ? ('link' as const) : onPress ? ('button' as const) : undefined,
|
|
728
748
|
accessibilityLabel: name,
|
|
729
749
|
onPress: onPress || (!IS_WEB && href) ? handlePress : undefined,
|
|
750
|
+
onLongPress,
|
|
751
|
+
// Same shape as `MessageBubble`'s: the event is defaulted here, so the
|
|
752
|
+
// browser's own menu does not open over the card's shortcut.
|
|
753
|
+
...(IS_WEB && onContextMenu !== undefined
|
|
754
|
+
? {
|
|
755
|
+
onContextMenu: (event: { preventDefault?: () => void }) => {
|
|
756
|
+
event.preventDefault?.();
|
|
757
|
+
onContextMenu();
|
|
758
|
+
},
|
|
759
|
+
}
|
|
760
|
+
: null),
|
|
730
761
|
testID: testID ? `${testID}-link` : undefined,
|
|
731
762
|
};
|
|
732
763
|
|
|
@@ -752,7 +783,9 @@ function ListingCardComponent(props: ListingCardProps) {
|
|
|
752
783
|
|
|
753
784
|
return (
|
|
754
785
|
<View
|
|
755
|
-
{...webDataSet(
|
|
786
|
+
{...webDataSet(
|
|
787
|
+
hoverZoom ? { bloomListingCard: layout, bloomListingCardZoom: '' } : { bloomListingCard: layout },
|
|
788
|
+
)}
|
|
756
789
|
style={[{ position: 'relative' }, style]}
|
|
757
790
|
testID={testID}
|
|
758
791
|
>
|
|
@@ -22,15 +22,22 @@ import type { WishlistCardProps } from './types';
|
|
|
22
22
|
* A saved collection of stays.
|
|
23
23
|
*
|
|
24
24
|
* cover a square, radius 16, 2px seams in the page colour:
|
|
25
|
+
* 0 photos the placeholder square, holding `empty` centred
|
|
25
26
|
* 1 photo fills it
|
|
26
27
|
* 2 side by side
|
|
27
28
|
* 3 one tall photo left, two stacked right
|
|
28
29
|
* 4+ a 2×2 (the first four)
|
|
29
|
-
* text 12 below: the name body-semibold,
|
|
30
|
-
* text-secondary
|
|
30
|
+
* text 12 below: `icon` at 16 (in `color`), the name body-semibold, then
|
|
31
|
+
* the description body-regular text-secondary
|
|
31
32
|
*
|
|
32
33
|
* A link on web with `href` (a real `<a>`), otherwise a button with `onPress`;
|
|
33
34
|
* its name is "name, description".
|
|
35
|
+
*
|
|
36
|
+
* `icon` is DECORATIVE and sits beside the name rather than inside it — a glyph
|
|
37
|
+
* put in the `name` string is read out as its character, and cannot be coloured
|
|
38
|
+
* or sized. `color` paints that glyph and tints the empty cover; it is the
|
|
39
|
+
* caller's colour, not a token, because a collection's colour is the person's
|
|
40
|
+
* choice and Bloom has no ramp for it.
|
|
34
41
|
*/
|
|
35
42
|
|
|
36
43
|
const SEAM = 2;
|
|
@@ -57,6 +64,9 @@ function WishlistCardComponent({
|
|
|
57
64
|
photoVariant,
|
|
58
65
|
onPress,
|
|
59
66
|
href,
|
|
67
|
+
icon: Icon,
|
|
68
|
+
color,
|
|
69
|
+
empty,
|
|
60
70
|
accessibilityLabel,
|
|
61
71
|
style,
|
|
62
72
|
testID,
|
|
@@ -67,10 +77,25 @@ function WishlistCardComponent({
|
|
|
67
77
|
const resolver = useImageResolver();
|
|
68
78
|
const uris = photos.slice(0, 4).map((photo) => resolvePhoto(photo, resolver, photoVariant));
|
|
69
79
|
const count = Math.max(1, uris.length);
|
|
80
|
+
const isEmpty = uris.length === 0;
|
|
70
81
|
|
|
71
82
|
const fill: ViewStyle = { flex: 1 };
|
|
72
83
|
let cover: React.ReactNode;
|
|
73
|
-
if (
|
|
84
|
+
if (isEmpty) {
|
|
85
|
+
cover = (
|
|
86
|
+
<View
|
|
87
|
+
style={{
|
|
88
|
+
flex: 1,
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
justifyContent: 'center',
|
|
91
|
+
backgroundColor: color ?? paint.photoPlaceholder,
|
|
92
|
+
}}
|
|
93
|
+
testID={testID ? `${testID}-empty` : undefined}
|
|
94
|
+
>
|
|
95
|
+
{empty ?? null}
|
|
96
|
+
</View>
|
|
97
|
+
);
|
|
98
|
+
} else if (count === 1) {
|
|
74
99
|
cover = <Tile uri={uris[0]} paint={paint} style={fill} />;
|
|
75
100
|
} else if (count === 2) {
|
|
76
101
|
cover = (
|
|
@@ -133,9 +158,20 @@ function WishlistCardComponent({
|
|
|
133
158
|
{cover}
|
|
134
159
|
</View>
|
|
135
160
|
<View style={{ marginTop: 12, gap: 2 }}>
|
|
136
|
-
<
|
|
137
|
-
{
|
|
138
|
-
|
|
161
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
|
162
|
+
{Icon ? (
|
|
163
|
+
<View aria-hidden style={{ flexShrink: 0 }} testID={testID ? `${testID}-icon` : undefined}>
|
|
164
|
+
<Icon width={16} height={16} fill={color ?? paint.textSecondary} />
|
|
165
|
+
</View>
|
|
166
|
+
) : null}
|
|
167
|
+
<Text
|
|
168
|
+
variant="body-semibold"
|
|
169
|
+
numberOfLines={1}
|
|
170
|
+
style={{ flexShrink: 1, minWidth: 0, color: paint.text }}
|
|
171
|
+
>
|
|
172
|
+
{name}
|
|
173
|
+
</Text>
|
|
174
|
+
</View>
|
|
139
175
|
{description ? (
|
|
140
176
|
<Text variant="body-regular" numberOfLines={1} style={{ color: paint.textSecondary }}>
|
|
141
177
|
{description}
|
|
@@ -20,6 +20,13 @@ export const MAX_DOTS = 5;
|
|
|
20
20
|
export const DOT_SIZE = 6;
|
|
21
21
|
/** A dot at a window edge that has more photos beyond it. */
|
|
22
22
|
export const DOT_EDGE_SIZE = 4;
|
|
23
|
+
/**
|
|
24
|
+
* How far `hoverZoom` scales the photo under a pointer. 1.06 — enough to read
|
|
25
|
+
* as the photo coming forward, small enough that the crop barely moves and a
|
|
26
|
+
* face near an edge is not pushed out of frame. The tile clips, so nothing
|
|
27
|
+
* overflows the card.
|
|
28
|
+
*/
|
|
29
|
+
export const PHOTO_ZOOM_SCALE = 1.06;
|
|
23
30
|
|
|
24
31
|
export interface ListingCardPaint {
|
|
25
32
|
/** The placeholder behind a photo that has not loaded, and the mosaic gaps' tiles. */
|
|
@@ -209,6 +216,7 @@ export const LISTING_CARD_STYLE_ID = 'bloom-listing-card-web-css';
|
|
|
209
216
|
const CARD = '[data-bloom-listing-card]';
|
|
210
217
|
const LINK = '[data-bloom-listing-card-link]';
|
|
211
218
|
const TRACK = '[data-bloom-listing-card-track]';
|
|
219
|
+
const PHOTO = '[data-bloom-listing-card-photo]';
|
|
212
220
|
const ARROW = '[data-bloom-listing-card-arrow]';
|
|
213
221
|
const DOT = '[data-bloom-listing-card-dot]';
|
|
214
222
|
const FAVORITE = '[data-bloom-favorite-button]';
|
|
@@ -262,9 +270,23 @@ ${FAVORITE}:focus-visible {
|
|
|
262
270
|
outline: 2px solid var(--bloom-listing-card-ring, currentColor);
|
|
263
271
|
outline-offset: 0;
|
|
264
272
|
}
|
|
273
|
+
${CARD}[data-bloom-listing-card-zoom] ${PHOTO} {
|
|
274
|
+
transition: transform 400ms cubic-bezier(0.2, 0, 0.2, 1);
|
|
275
|
+
transform-origin: center;
|
|
276
|
+
}
|
|
277
|
+
@media (any-hover: hover) {
|
|
278
|
+
${CARD}[data-bloom-listing-card-zoom]:hover ${PHOTO} {
|
|
279
|
+
transform: scale(${PHOTO_ZOOM_SCALE});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
265
282
|
@media (prefers-reduced-motion: reduce) {
|
|
266
283
|
${ARROW}, ${DOT} {
|
|
267
284
|
transition: none;
|
|
268
285
|
}
|
|
286
|
+
${CARD}[data-bloom-listing-card-zoom] ${PHOTO},
|
|
287
|
+
${CARD}[data-bloom-listing-card-zoom]:hover ${PHOTO} {
|
|
288
|
+
transition: none;
|
|
289
|
+
transform: none;
|
|
290
|
+
}
|
|
269
291
|
}
|
|
270
292
|
`;
|
|
@@ -128,6 +128,26 @@ export interface ListingCardProps {
|
|
|
128
128
|
onFavoriteChange?: (favorite: boolean) => void;
|
|
129
129
|
/** Called when the card is pressed. */
|
|
130
130
|
onPress?: () => void;
|
|
131
|
+
/**
|
|
132
|
+
* A long press on the card — a shortcut to a sheet or a menu ("save to a
|
|
133
|
+
* folder"). A long press has no keyboard equivalent and no name to announce,
|
|
134
|
+
* so whatever it reaches must be reachable another way too.
|
|
135
|
+
*/
|
|
136
|
+
onLongPress?: () => void;
|
|
137
|
+
/**
|
|
138
|
+
* Web only — right-click, the pointer's spelling of the same shortcut. The
|
|
139
|
+
* event is defaulted for you, so the browser's own menu ("open in a new tab")
|
|
140
|
+
* does not appear on the card while this is set. Same contract as
|
|
141
|
+
* `MessageBubble`'s.
|
|
142
|
+
*/
|
|
143
|
+
onContextMenu?: () => void;
|
|
144
|
+
/**
|
|
145
|
+
* Zooms the photo a little while the pointer is over the card (WEB ONLY —
|
|
146
|
+
* there is no hover on a touch screen, and no reduced-motion-safe way to do
|
|
147
|
+
* it on native). Default `false`. Skipped under
|
|
148
|
+
* `prefers-reduced-motion: reduce`.
|
|
149
|
+
*/
|
|
150
|
+
hoverZoom?: boolean;
|
|
131
151
|
/**
|
|
132
152
|
* The stay's URL. On web the card is a real `<a href>` (open in a new tab,
|
|
133
153
|
* copy link); with `onPress` too the default navigation is prevented so a
|
|
@@ -206,6 +226,23 @@ export interface WishlistCardProps {
|
|
|
206
226
|
onPress?: () => void;
|
|
207
227
|
/** The collection's URL; a real `<a href>` on web, like `ListingCard`. */
|
|
208
228
|
href?: string;
|
|
229
|
+
/**
|
|
230
|
+
* A glyph left of the name, 16px — what the collection IS ("Coast weekends"
|
|
231
|
+
* beside a heart, "Viewings booked" beside a calendar). Decorative: the name
|
|
232
|
+
* beside it is what a screen reader reads.
|
|
233
|
+
*/
|
|
234
|
+
icon?: BloomIconComponent;
|
|
235
|
+
/**
|
|
236
|
+
* The `icon`'s colour, and the tint of the cover while the collection is
|
|
237
|
+
* empty. Default: the secondary text colour, and the plain placeholder.
|
|
238
|
+
*/
|
|
239
|
+
color?: string;
|
|
240
|
+
/**
|
|
241
|
+
* What the cover shows while `photos` is empty — a glyph, a line of text, an
|
|
242
|
+
* "Add your first save" affordance. Without it an empty collection draws the
|
|
243
|
+
* plain placeholder square it draws today.
|
|
244
|
+
*/
|
|
245
|
+
empty?: ReactNode;
|
|
209
246
|
/** Replaces the composed name ("Coast weekends, 12 saved"). */
|
|
210
247
|
accessibilityLabel?: string;
|
|
211
248
|
style?: StyleProp<ViewStyle>;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import React, { memo, useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { Platform, Pressable, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { resolveButtonRamps } from '../button/shared';
|
|
5
|
+
import { RiStarFill } from '../icons/remix/RiStarFill';
|
|
6
|
+
import { RiStarLine } from '../icons/remix/RiStarLine';
|
|
7
|
+
import { focusRingShadow, interactiveWebCss, useInteractiveWebCss } from '../styles/interactive-web-css';
|
|
8
|
+
import { useRingOffsetStyle } from '../styles/surface-levels';
|
|
9
|
+
import { DISABLED_OPACITY } from '../styles/tokens';
|
|
10
|
+
import { webDataSet } from '../styles/web-data';
|
|
11
|
+
import type { WebCssStyle } from '../styles/web-view-style';
|
|
12
|
+
import { useTheme } from '../theme/use-theme';
|
|
13
|
+
import type { RatingInputProps, RatingInputSize } from './types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The star PICKER — `Rating`'s counterpart for a review form. `Rating` draws a
|
|
17
|
+
* rating that exists; this one is how a rating comes to exist.
|
|
18
|
+
*
|
|
19
|
+
* star gap
|
|
20
|
+
* small 24 4
|
|
21
|
+
* medium 32 6
|
|
22
|
+
* large 40 8
|
|
23
|
+
*
|
|
24
|
+
* Chosen stars are filled with the ACCENT, the rest are outlines in the
|
|
25
|
+
* neutral ramp — the same reasoning as `RatingBar`'s fill: a chosen star is a
|
|
26
|
+
* choice the person made, and text-primary reads as ink rather than as an
|
|
27
|
+
* answer. Under a pointer the stars up to the hovered one fill as a preview,
|
|
28
|
+
* which never touches `value`.
|
|
29
|
+
*
|
|
30
|
+
* Accessibility: a `radiogroup` named by `accessibilityLabel`, holding one
|
|
31
|
+
* `radio` per star named by `formatStarLabel` ("4 stars"). Exactly one of N is
|
|
32
|
+
* what a rating is, so `radio` is the role rather than a `slider` — it is also
|
|
33
|
+
* the only one that gives each star its own touch target and its own name.
|
|
34
|
+
* Roving tab stop: the chosen star (the first when nothing is chosen) is the
|
|
35
|
+
* one in the tab order, and ArrowRight/Up, ArrowLeft/Down, Home and End move
|
|
36
|
+
* AND choose, which is how an ARIA radio group behaves. The focus ring is drawn
|
|
37
|
+
* on the star itself.
|
|
38
|
+
*
|
|
39
|
+
* **No half stars, and no clearing.** Half a star doubles the keyboard stops
|
|
40
|
+
* and halves the target — 16px of a 32px star, under any touch floor — and the
|
|
41
|
+
* icon set has no half-star glyph, so the state could not be drawn honestly
|
|
42
|
+
* either. Clearing is left out for the reason `Radio` re-choosing is a no-op: a
|
|
43
|
+
* radio has no "off", and a form that needs "no rating yet" holds `null` and
|
|
44
|
+
* does not render a cleared state the group cannot express. `Rating` still
|
|
45
|
+
* DISPLAYS fractional values — a display has no target and no keyboard.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
const SIZE_CONFIG: Record<RatingInputSize, { star: number; gap: number }> = {
|
|
49
|
+
small: { star: 24, gap: 4 },
|
|
50
|
+
medium: { star: 32, gap: 6 },
|
|
51
|
+
large: { star: 40, gap: 8 },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const IS_WEB = Platform.OS === 'web';
|
|
55
|
+
|
|
56
|
+
const HIT_SLOP = { top: 6, bottom: 6, left: 2, right: 2 } as const;
|
|
57
|
+
|
|
58
|
+
const STYLE_ID = 'bloom-rating-input-web-css';
|
|
59
|
+
const STAR = '[data-bloom-rating-star]';
|
|
60
|
+
|
|
61
|
+
const BLOOM_RATING_INPUT_CSS = interactiveWebCss({
|
|
62
|
+
selector: STAR,
|
|
63
|
+
varPrefix: 'bloom-rating-input',
|
|
64
|
+
// A star is a `View`-based press target, not a `<button>`: the button reset's
|
|
65
|
+
// `inline-flex` changes an RNW `View`'s layout.
|
|
66
|
+
reset: 'none',
|
|
67
|
+
base: 'border-radius: 6px;',
|
|
68
|
+
transition: 'box-shadow 120ms ease',
|
|
69
|
+
disabled: { opacity: null },
|
|
70
|
+
extraRules: `${STAR}:focus-visible {
|
|
71
|
+
outline: none;
|
|
72
|
+
box-shadow: ${focusRingShadow('--bloom-rating-input-ring')};
|
|
73
|
+
}`,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/** `1 star`, `4 stars`. English — `formatStarLabel` replaces it. */
|
|
77
|
+
export function defaultStarLabel(value: number): string {
|
|
78
|
+
return value === 1 ? '1 star' : `${value} stars`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function RatingInputComponent({
|
|
82
|
+
value,
|
|
83
|
+
onChange,
|
|
84
|
+
max = 5,
|
|
85
|
+
size = 'medium',
|
|
86
|
+
disabled = false,
|
|
87
|
+
accessibilityLabel,
|
|
88
|
+
formatStarLabel = defaultStarLabel,
|
|
89
|
+
style,
|
|
90
|
+
testID,
|
|
91
|
+
}: RatingInputProps) {
|
|
92
|
+
const theme = useTheme();
|
|
93
|
+
const ringOffset = useRingOffsetStyle();
|
|
94
|
+
useInteractiveWebCss(STYLE_ID, BLOOM_RATING_INPUT_CSS);
|
|
95
|
+
const { accent, neutral } = useMemo(() => resolveButtonRamps(theme), [theme]);
|
|
96
|
+
const config = SIZE_CONFIG[size];
|
|
97
|
+
const [hovered, setHovered] = useState<number | null>(null);
|
|
98
|
+
|
|
99
|
+
const total = Math.max(1, Math.round(max));
|
|
100
|
+
const chosen = value == null ? null : Math.min(total, Math.max(1, Math.round(value)));
|
|
101
|
+
// The hover preview never survives a disabled group.
|
|
102
|
+
const shown = disabled ? chosen : (hovered ?? chosen);
|
|
103
|
+
|
|
104
|
+
const choose = useCallback(
|
|
105
|
+
(next: number) => {
|
|
106
|
+
if (disabled) return;
|
|
107
|
+
const clamped = Math.min(total, Math.max(1, next));
|
|
108
|
+
// Re-choosing the chosen star is a no-op, as on `Radio`: firing would
|
|
109
|
+
// report a change that did not happen.
|
|
110
|
+
if (clamped === chosen) return;
|
|
111
|
+
onChange(clamped);
|
|
112
|
+
},
|
|
113
|
+
[disabled, total, chosen, onChange],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const onKeyDown = useCallback(
|
|
117
|
+
(e: { key: string; preventDefault: () => void }) => {
|
|
118
|
+
const from = chosen ?? 0;
|
|
119
|
+
switch (e.key) {
|
|
120
|
+
case 'ArrowRight':
|
|
121
|
+
case 'ArrowUp':
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
choose(Math.min(total, from + 1));
|
|
124
|
+
break;
|
|
125
|
+
case 'ArrowLeft':
|
|
126
|
+
case 'ArrowDown':
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
choose(Math.max(1, from - 1));
|
|
129
|
+
break;
|
|
130
|
+
case 'Home':
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
choose(1);
|
|
133
|
+
break;
|
|
134
|
+
case 'End':
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
choose(total);
|
|
137
|
+
break;
|
|
138
|
+
default:
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
[chosen, total, choose],
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// The roving tab stop: the chosen star, or the first one while nothing is
|
|
146
|
+
// chosen, so a keyboard reaches the group in one Tab either way.
|
|
147
|
+
const tabStop = chosen ?? 1;
|
|
148
|
+
const emptyColor = theme.isDark ? neutral[600] : neutral[300];
|
|
149
|
+
|
|
150
|
+
const starStyle: WebCssStyle = {
|
|
151
|
+
padding: 2,
|
|
152
|
+
borderRadius: 6,
|
|
153
|
+
'--bloom-rating-input-ring': accent[500],
|
|
154
|
+
...ringOffset,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<View
|
|
159
|
+
testID={testID}
|
|
160
|
+
accessibilityRole="radiogroup"
|
|
161
|
+
accessibilityLabel={accessibilityLabel}
|
|
162
|
+
aria-label={accessibilityLabel}
|
|
163
|
+
aria-disabled={disabled || undefined}
|
|
164
|
+
style={[
|
|
165
|
+
{
|
|
166
|
+
flexDirection: 'row',
|
|
167
|
+
alignItems: 'center',
|
|
168
|
+
gap: config.gap,
|
|
169
|
+
opacity: disabled ? DISABLED_OPACITY : 1,
|
|
170
|
+
},
|
|
171
|
+
style,
|
|
172
|
+
]}
|
|
173
|
+
>
|
|
174
|
+
{Array.from({ length: total }, (_, index) => {
|
|
175
|
+
const star = index + 1;
|
|
176
|
+
const filled = shown != null && star <= shown;
|
|
177
|
+
const Glyph = filled ? RiStarFill : RiStarLine;
|
|
178
|
+
const name = formatStarLabel(star, total);
|
|
179
|
+
const webProps: Record<string, unknown> = IS_WEB
|
|
180
|
+
? { tabIndex: disabled || star !== tabStop ? -1 : 0, onKeyDown }
|
|
181
|
+
: {};
|
|
182
|
+
return (
|
|
183
|
+
<Pressable
|
|
184
|
+
key={star}
|
|
185
|
+
accessibilityRole="radio"
|
|
186
|
+
// `aria-checked`, not `accessibilityState`: react-native-web never
|
|
187
|
+
// reads the latter. React Native folds this one back, so it is the
|
|
188
|
+
// spelling both platforms honour.
|
|
189
|
+
aria-checked={star === chosen}
|
|
190
|
+
accessibilityLabel={name}
|
|
191
|
+
disabled={disabled}
|
|
192
|
+
onPress={() => choose(star)}
|
|
193
|
+
onHoverIn={() => setHovered(star)}
|
|
194
|
+
onHoverOut={() => setHovered((current) => (current === star ? null : current))}
|
|
195
|
+
hitSlop={HIT_SLOP}
|
|
196
|
+
{...webProps}
|
|
197
|
+
{...webDataSet({ bloomRatingStar: '' })}
|
|
198
|
+
testID={testID ? `${testID}-star-${star}` : undefined}
|
|
199
|
+
style={starStyle}
|
|
200
|
+
>
|
|
201
|
+
<Glyph
|
|
202
|
+
width={config.star}
|
|
203
|
+
height={config.star}
|
|
204
|
+
fill={filled ? accent[500] : emptyColor}
|
|
205
|
+
/>
|
|
206
|
+
</Pressable>
|
|
207
|
+
);
|
|
208
|
+
})}
|
|
209
|
+
</View>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const RatingInput = memo(RatingInputComponent);
|
|
214
|
+
RatingInput.displayName = 'RatingInput';
|
package/src/rating/index.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
export { Rating } from './Rating';
|
|
2
2
|
export { RatingBar } from './RatingBar';
|
|
3
|
-
export
|
|
3
|
+
export { RatingInput } from './RatingInput';
|
|
4
|
+
export type {
|
|
5
|
+
RatingBarProps,
|
|
6
|
+
RatingCountStyle,
|
|
7
|
+
RatingInputProps,
|
|
8
|
+
RatingInputSize,
|
|
9
|
+
RatingProps,
|
|
10
|
+
RatingSize,
|
|
11
|
+
} from './types';
|
package/src/rating/types.ts
CHANGED
|
@@ -32,6 +32,39 @@ export interface RatingProps {
|
|
|
32
32
|
testID?: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** `small` is a 24px star, `medium` (default) 32, `large` 40. */
|
|
36
|
+
export type RatingInputSize = 'small' | 'medium' | 'large';
|
|
37
|
+
|
|
38
|
+
export interface RatingInputProps {
|
|
39
|
+
/**
|
|
40
|
+
* The chosen rating, `1..max`, or `null` while nothing is chosen. Fully
|
|
41
|
+
* controlled — the component keeps no value of its own.
|
|
42
|
+
*/
|
|
43
|
+
value: number | null;
|
|
44
|
+
/** Called with the chosen rating. Never called with the value already chosen. */
|
|
45
|
+
onChange: (value: number) => void;
|
|
46
|
+
/** How many stars. Default `5`. */
|
|
47
|
+
max?: number;
|
|
48
|
+
/** Default `medium`. */
|
|
49
|
+
size?: RatingInputSize;
|
|
50
|
+
/** Disables the whole group: no press, no keyboard, dimmed. */
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The NAME of what is being rated ("Overall rating", "Cleanliness").
|
|
54
|
+
* Required: the group draws nothing but stars, so nothing else can tell a
|
|
55
|
+
* screen reader which of several ratings this one is.
|
|
56
|
+
*/
|
|
57
|
+
accessibilityLabel: string;
|
|
58
|
+
/**
|
|
59
|
+
* Each star's own name, which is English by default (`"1 star"`,
|
|
60
|
+
* `"4 stars"`). Pass a translated formatter.
|
|
61
|
+
*/
|
|
62
|
+
formatStarLabel?: (value: number, max: number) => string;
|
|
63
|
+
style?: StyleProp<ViewStyle>;
|
|
64
|
+
/** Derives `-star-<n>` for each star. */
|
|
65
|
+
testID?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
35
68
|
export interface RatingBarProps {
|
|
36
69
|
/** What the row measures ("Cleanliness", "5 stars"). Also the bar's accessible name. */
|
|
37
70
|
label: string;
|