@oxy.so/bloom 2.11.0 → 2.12.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/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/message-bubble/MessageBubble.js +16 -1
- package/lib/commonjs/message-bubble/MessageBubble.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/message-bubble/MessageBubble.js +16 -1
- package/lib/module/message-bubble/MessageBubble.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/message-bubble/MessageBubble.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/message-bubble/MessageBubble.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/message-bubble/MessageBubble.tsx +21 -1
- 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
|
@@ -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>;
|
|
@@ -182,6 +182,25 @@ function MessageBubbleComponent({
|
|
|
182
182
|
onContextMenu !== undefined ||
|
|
183
183
|
selected !== undefined;
|
|
184
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Whether the bubble CONTAINS controls of its own.
|
|
187
|
+
*
|
|
188
|
+
* A pressable bubble is a button, and on the web a button may not contain
|
|
189
|
+
* another one: a picture, a file row, a voice note's play control, a reaction
|
|
190
|
+
* chip, a retry action and a reply quote are all buttons, so a bubble holding
|
|
191
|
+
* any of them and announcing itself as one renders invalid DOM — React logs
|
|
192
|
+
* "<button> cannot contain a nested <button>" once per bubble and the inner
|
|
193
|
+
* control's own semantics are lost. Such a bubble keeps its press handlers
|
|
194
|
+
* and its accessible name, and gives up the ROLE: it is a container that
|
|
195
|
+
* happens to be pressable, which is what it actually is.
|
|
196
|
+
*/
|
|
197
|
+
const holdsControls =
|
|
198
|
+
media !== undefined ||
|
|
199
|
+
(reactions !== undefined && reactions.length > 0) ||
|
|
200
|
+
onRetry !== undefined ||
|
|
201
|
+
onPressReply !== undefined ||
|
|
202
|
+
onAddReaction !== undefined;
|
|
203
|
+
|
|
185
204
|
const name =
|
|
186
205
|
accessibilityLabel ??
|
|
187
206
|
bubbleAccessibleName({
|
|
@@ -324,7 +343,8 @@ function MessageBubbleComponent({
|
|
|
324
343
|
|
|
325
344
|
const bubble = interactive ? (
|
|
326
345
|
<Pressable
|
|
327
|
-
role=
|
|
346
|
+
role={holdsControls ? undefined : 'button'}
|
|
347
|
+
accessible={holdsControls ? undefined : true}
|
|
328
348
|
accessibilityLabel={name}
|
|
329
349
|
aria-pressed={selected === undefined ? undefined : selected}
|
|
330
350
|
accessibilityState={selected === undefined ? undefined : { selected }}
|
|
@@ -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;
|
|
@@ -4,6 +4,7 @@ import { Platform, Pressable, View } from 'react-native';
|
|
|
4
4
|
import { resolveButtonRamps } from '../button/shared';
|
|
5
5
|
import { RiMapPinLine } from '../icons/remix/RiMapPinLine';
|
|
6
6
|
import { adoptStyleSheet } from '../styles/adopt-style-sheet';
|
|
7
|
+
import { DISABLED_OPACITY } from '../styles/tokens';
|
|
7
8
|
import type { WebCssStyle } from '../styles/web-view-style';
|
|
8
9
|
import { webDataSet } from '../styles/web-data';
|
|
9
10
|
import { useTheme } from '../theme/use-theme';
|
|
@@ -20,6 +21,13 @@ import type { DestinationSuggestionsProps } from './types';
|
|
|
20
21
|
*
|
|
21
22
|
* A `listbox` of `option`s. The list takes focus (web) and owns the arrow
|
|
22
23
|
* keys; the rows stay out of the tab order.
|
|
24
|
+
*
|
|
25
|
+
* A row can be `disabled` — "Use my location" while location permission is
|
|
26
|
+
* off. It stays in the list rather than disappearing, because a row that
|
|
27
|
+
* vanishes teaches nothing: it dims to `DISABLED_OPACITY`, carries
|
|
28
|
+
* `aria-disabled`, answers no press or Enter, is never highlighted by hover, and
|
|
29
|
+
* the arrow keys step OVER it. `disabledReason` replaces its description and is
|
|
30
|
+
* read after the title, so the row says why.
|
|
23
31
|
*/
|
|
24
32
|
|
|
25
33
|
const IS_WEB = Platform.OS === 'web';
|
|
@@ -36,6 +44,28 @@ const CSS = `
|
|
|
36
44
|
}
|
|
37
45
|
`;
|
|
38
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The next row the arrow keys land on, stepping OVER every disabled one and
|
|
49
|
+
* wrapping once. `-1` when every row is disabled — the highlight then stays
|
|
50
|
+
* where it is rather than landing on a row that answers nothing. Pure.
|
|
51
|
+
*/
|
|
52
|
+
export function nextSelectable(
|
|
53
|
+
items: readonly { disabled?: boolean }[],
|
|
54
|
+
from: number,
|
|
55
|
+
step: 1 | -1,
|
|
56
|
+
): number {
|
|
57
|
+
const count = items.length;
|
|
58
|
+
if (count === 0) return -1;
|
|
59
|
+
// From "nothing highlighted", ArrowDown starts at the top and ArrowUp at the
|
|
60
|
+
// bottom, which is what the list did before any row could be disabled.
|
|
61
|
+
const start = from < 0 ? (step === 1 ? 0 : count - 1) : (from + step + count) % count;
|
|
62
|
+
for (let i = 0; i < count; i++) {
|
|
63
|
+
const index = (start + step * i + count * count) % count;
|
|
64
|
+
if (!items[index]?.disabled) return index;
|
|
65
|
+
}
|
|
66
|
+
return -1;
|
|
67
|
+
}
|
|
68
|
+
|
|
39
69
|
function DestinationSuggestionsComponent({
|
|
40
70
|
items,
|
|
41
71
|
onSelect,
|
|
@@ -68,13 +98,16 @@ function DestinationSuggestionsComponent({
|
|
|
68
98
|
if (items.length === 0) return;
|
|
69
99
|
if (e.key === 'ArrowDown') {
|
|
70
100
|
e.preventDefault();
|
|
71
|
-
|
|
101
|
+
const next = nextSelectable(items, current, 1);
|
|
102
|
+
if (next >= 0) highlight(next);
|
|
72
103
|
} else if (e.key === 'ArrowUp') {
|
|
73
104
|
e.preventDefault();
|
|
74
|
-
|
|
105
|
+
const next = nextSelectable(items, current, -1);
|
|
106
|
+
if (next >= 0) highlight(next);
|
|
75
107
|
} else if (e.key === 'Enter' && current >= 0 && current < items.length) {
|
|
76
108
|
e.preventDefault();
|
|
77
|
-
|
|
109
|
+
const item = items[current]!;
|
|
110
|
+
if (!item.disabled) onSelect(item);
|
|
78
111
|
}
|
|
79
112
|
};
|
|
80
113
|
|
|
@@ -103,17 +136,22 @@ function DestinationSuggestionsComponent({
|
|
|
103
136
|
>
|
|
104
137
|
{items.map((item, index) => {
|
|
105
138
|
const Icon = item.icon ?? RiMapPinLine;
|
|
106
|
-
const
|
|
139
|
+
const disabled = item.disabled === true;
|
|
140
|
+
const selected = index === current && !disabled;
|
|
141
|
+
const secondLine = disabled ? (item.disabledReason ?? item.description) : item.description;
|
|
107
142
|
return (
|
|
108
143
|
<Pressable
|
|
109
144
|
key={item.id}
|
|
110
145
|
role="option"
|
|
111
|
-
accessibilityLabel={
|
|
146
|
+
accessibilityLabel={secondLine ? `${item.title}, ${secondLine}` : item.title}
|
|
112
147
|
aria-selected={selected}
|
|
113
|
-
accessibilityState={{ selected }}
|
|
148
|
+
accessibilityState={{ selected, disabled }}
|
|
149
|
+
disabled={disabled}
|
|
114
150
|
focusable={false}
|
|
115
151
|
onPress={() => onSelect(item)}
|
|
116
|
-
onHoverIn={() =>
|
|
152
|
+
onHoverIn={() => {
|
|
153
|
+
if (!disabled) highlight(index);
|
|
154
|
+
}}
|
|
117
155
|
testID={testID ? `${testID}-${index}` : undefined}
|
|
118
156
|
style={{
|
|
119
157
|
flexDirection: 'row',
|
|
@@ -124,6 +162,7 @@ function DestinationSuggestionsComponent({
|
|
|
124
162
|
paddingLeft: 12,
|
|
125
163
|
paddingRight: 12,
|
|
126
164
|
borderRadius: 16,
|
|
165
|
+
opacity: disabled ? DISABLED_OPACITY : 1,
|
|
127
166
|
backgroundColor: selected ? palette.rowHighlight : 'transparent',
|
|
128
167
|
}}
|
|
129
168
|
>
|
|
@@ -143,9 +182,9 @@ function DestinationSuggestionsComponent({
|
|
|
143
182
|
<Text variant="body-medium" numberOfLines={1} style={{ color: palette.text }}>
|
|
144
183
|
{item.title}
|
|
145
184
|
</Text>
|
|
146
|
-
{
|
|
185
|
+
{secondLine ? (
|
|
147
186
|
<Text variant="body-2-regular" numberOfLines={1} style={{ color: palette.textSecondary }}>
|
|
148
|
-
{
|
|
187
|
+
{secondLine}
|
|
149
188
|
</Text>
|
|
150
189
|
) : null}
|
|
151
190
|
</View>
|
|
@@ -19,6 +19,11 @@ const COUNTED: readonly GuestKind[] = ['adults', 'children'];
|
|
|
19
19
|
* them — the one guests picker, used by the search bar's "Who" panel and the
|
|
20
20
|
* booking card's guests popover alike. Then an optional note and "Close".
|
|
21
21
|
*
|
|
22
|
+
* The stepper button names travel too: `decrementLabel` / `incrementLabel`
|
|
23
|
+
* reach every row, so the two words a screen reader reads on the eight buttons
|
|
24
|
+
* are translatable like everything else the family draws. Their default is
|
|
25
|
+
* `Stepper`'s own English pair.
|
|
26
|
+
*
|
|
22
27
|
* Two rules:
|
|
23
28
|
* - while any child, infant or pet is counted, adults cannot go below 1 (the
|
|
24
29
|
* adults `−` disables there), and adding one of them with no adults sets
|
|
@@ -35,6 +40,8 @@ function GuestPickerComponent({
|
|
|
35
40
|
kinds = GUEST_KINDS,
|
|
36
41
|
labels,
|
|
37
42
|
descriptions,
|
|
43
|
+
decrementLabel,
|
|
44
|
+
incrementLabel,
|
|
38
45
|
note,
|
|
39
46
|
onClose,
|
|
40
47
|
closeLabel = 'Close',
|
|
@@ -71,6 +78,8 @@ function GuestPickerComponent({
|
|
|
71
78
|
min={kind === 'adults' ? minimumAdults(value) : 0}
|
|
72
79
|
max={maxFor(kind)}
|
|
73
80
|
size={size}
|
|
81
|
+
decrementLabel={decrementLabel}
|
|
82
|
+
incrementLabel={incrementLabel}
|
|
74
83
|
onValueChange={(n) => onChange(applyGuestCount(value, kind, n))}
|
|
75
84
|
divider={index < kinds.length - 1}
|
|
76
85
|
testID={testID ? `${testID}-${kind}` : undefined}
|
package/src/stay-search/types.ts
CHANGED
|
@@ -97,6 +97,18 @@ export interface DestinationSuggestion {
|
|
|
97
97
|
description?: string;
|
|
98
98
|
/** A Remix icon component (`RiMapPinLine`, `RiTimeLine`…). Default `RiMapPinLine`. */
|
|
99
99
|
icon?: ButtonIconComponent;
|
|
100
|
+
/**
|
|
101
|
+
* The row is offered but cannot be chosen — "Use my location" while location
|
|
102
|
+
* is off. It dims, answers no press, and the arrow keys step over it.
|
|
103
|
+
*/
|
|
104
|
+
disabled?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Why it cannot be chosen ("Location is off"). Drawn in place of
|
|
107
|
+
* `description` while `disabled`, and read after the title, so the row says
|
|
108
|
+
* what is wrong rather than only looking wrong. Ignored when the row is not
|
|
109
|
+
* disabled.
|
|
110
|
+
*/
|
|
111
|
+
disabledReason?: string;
|
|
100
112
|
}
|
|
101
113
|
|
|
102
114
|
export interface DestinationSuggestionsProps {
|
|
@@ -135,6 +147,14 @@ export interface GuestPickerProps {
|
|
|
135
147
|
labels?: Partial<Record<GuestKind, string>>;
|
|
136
148
|
/** Override descriptions (`null` hides one). */
|
|
137
149
|
descriptions?: Partial<Record<GuestKind, string | null>>;
|
|
150
|
+
/**
|
|
151
|
+
* The name of every row's `−` button, which is English by default
|
|
152
|
+
* (`"Decrease"`). Same spelling as `Stepper`'s, and passed to every row — the
|
|
153
|
+
* row's own title is what says WHICH counter it belongs to.
|
|
154
|
+
*/
|
|
155
|
+
decrementLabel?: string;
|
|
156
|
+
/** The name of every row's `+` button. Default `"Increase"`. */
|
|
157
|
+
incrementLabel?: string;
|
|
138
158
|
/**
|
|
139
159
|
* The most guests the place takes, counting adults and children (infants and
|
|
140
160
|
* pets do not count). Their `+` buttons disable once the sum reaches it.
|
package/src/track-list/parts.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { Fragment, memo, useMemo
|
|
1
|
+
import React, { Fragment, memo, useMemo } from 'react';
|
|
2
2
|
import { Image, View, type GestureResponderEvent } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { GlyphButton } from '../button';
|
|
@@ -13,8 +13,6 @@ import { RiMore2Fill } from '../icons/remix/RiMore2Fill';
|
|
|
13
13
|
import { RiMusic2Line } from '../icons/remix/RiMusic2Line';
|
|
14
14
|
import { resolvePhoto } from '../listing-card/shared';
|
|
15
15
|
import { useImageResolver } from '../image-resolver/context';
|
|
16
|
-
import { DISABLED_OPACITY, borderRadius } from '../styles/tokens';
|
|
17
|
-
import type { WebCssStyle } from '../styles/web-view-style';
|
|
18
16
|
import type { WebAriaProps } from '../styles/styled-primitives';
|
|
19
17
|
import { useTheme } from '../theme/use-theme';
|
|
20
18
|
import { IS_WEB, resolveTrackListPaint } from './shared';
|
|
@@ -58,23 +56,6 @@ export function TrackIconButton({
|
|
|
58
56
|
}: TrackIconButtonProps) {
|
|
59
57
|
const theme = useTheme();
|
|
60
58
|
const paint = useMemo(() => resolveTrackListPaint(theme), [theme]);
|
|
61
|
-
const [hovered, setHovered] = useState(false);
|
|
62
|
-
const color =
|
|
63
|
-
pressed && activeColor
|
|
64
|
-
? activeColor
|
|
65
|
-
: hovered && !disabled
|
|
66
|
-
? paint.text
|
|
67
|
-
: paint.textMuted;
|
|
68
|
-
const rootStyle: WebCssStyle = {
|
|
69
|
-
width: 32,
|
|
70
|
-
height: 32,
|
|
71
|
-
borderRadius: borderRadius.full,
|
|
72
|
-
alignItems: 'center',
|
|
73
|
-
justifyContent: 'center',
|
|
74
|
-
opacity: disabled ? DISABLED_OPACITY : 1,
|
|
75
|
-
'--bloom-track-ring': paint.ring,
|
|
76
|
-
};
|
|
77
|
-
const toggle = pressed !== undefined;
|
|
78
59
|
return (
|
|
79
60
|
<GlyphButton
|
|
80
61
|
{...aria}
|