@utilitywarehouse/hearth-react-native 0.32.3 โ 0.32.5
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/.storybook/vitest.setup.ts +0 -8
- package/.turbo/turbo-lint$colon$fix.log +12 -12
- package/CHANGELOG.md +36 -0
- package/build/components/Accordion/Accordion.d.ts +1 -1
- package/build/components/Button/Button.d.ts +5 -5
- package/build/components/Carousel/CarouselControlItem.d.ts +1 -1
- package/build/components/FormField/FormField.d.ts +1 -1
- package/build/components/Input/Input.d.ts +4 -4
- package/build/components/List/ListAction/ListAction.d.ts +1 -1
- package/build/components/Menu/MenuItem.d.ts +1 -1
- package/build/components/PillGroup/Pill.d.ts +1 -1
- package/build/components/RadioCard/RadioCard.d.ts +7 -7
- package/build/components/Rating/Rating.d.ts +1 -1
- package/build/components/Rating/Rating.js +40 -8
- package/build/components/Rating/Rating.props.d.ts +10 -2
- package/build/components/Rating/Rating.utils.d.ts +6 -0
- package/build/components/Rating/Rating.utils.js +7 -0
- package/build/components/Rating/Rating.utils.test.d.ts +1 -0
- package/build/components/Rating/Rating.utils.test.js +13 -0
- package/build/components/Rating/RatingEmoji.d.ts +3 -0
- package/build/components/Rating/RatingEmoji.js +21 -0
- package/build/components/Rating/index.d.ts +1 -1
- package/build/components/SegmentedControl/SegmentedControlOption.d.ts +1 -1
- package/build/components/Tabs/Tab.d.ts +1 -1
- package/build/components/Textarea/Textarea.d.ts +2 -2
- package/docs/changelog.mdx +36 -0
- package/package.json +12 -11
- package/src/components/Rating/Rating.docs.mdx +44 -5
- package/src/components/Rating/Rating.figma.tsx +19 -0
- package/src/components/Rating/Rating.props.ts +8 -2
- package/src/components/Rating/Rating.stories.tsx +17 -0
- package/src/components/Rating/Rating.tsx +86 -28
- package/src/components/Rating/Rating.utils.test.ts +15 -0
- package/src/components/Rating/Rating.utils.ts +14 -0
- package/src/components/Rating/RatingEmoji.tsx +28 -0
- package/src/components/Rating/index.ts +1 -1
- package/src/vite-env.d.ts +7 -0
- package/vitest.config.js +3 -2
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { setProjectAnnotations } from '@storybook/react-native-web-vite';
|
|
2
1
|
import { vi } from 'vitest';
|
|
3
2
|
|
|
4
3
|
// react-native-unistyles/mocks relies on Jest globals.
|
|
@@ -30,10 +29,3 @@ StyleSheet.configure({
|
|
|
30
29
|
adaptiveThemes: false,
|
|
31
30
|
},
|
|
32
31
|
});
|
|
33
|
-
|
|
34
|
-
const a11yAddonAnnotations = await import('@storybook/addon-a11y/preview');
|
|
35
|
-
const projectAnnotations = await import('./preview');
|
|
36
|
-
|
|
37
|
-
// This is an important step to apply the right configuration when testing your stories.
|
|
38
|
-
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
|
|
39
|
-
setProjectAnnotations([a11yAddonAnnotations as any, projectAnnotations as any]);
|
|
@@ -42,15 +42,15 @@ $ TIMING=1 eslint --fix .
|
|
|
42
42
|
|
|
43
43
|
โ 20 problems (0 errors, 20 warnings)
|
|
44
44
|
|
|
45
|
-
Rule
|
|
46
|
-
|
|
47
|
-
@typescript-eslint/no-unused-vars
|
|
48
|
-
react-hooks/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
no-useless-escape
|
|
52
|
-
no-
|
|
53
|
-
no-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
no-
|
|
45
|
+
Rule | Time (ms) | Relative
|
|
46
|
+
:----------------------------------------|----------:|--------:
|
|
47
|
+
@typescript-eslint/no-unused-vars | 1547.609 | 55.9%
|
|
48
|
+
react-hooks/exhaustive-deps | 169.235 | 6.1%
|
|
49
|
+
no-global-assign | 113.807 | 4.1%
|
|
50
|
+
react-hooks/rules-of-hooks | 96.673 | 3.5%
|
|
51
|
+
no-useless-escape | 62.311 | 2.2%
|
|
52
|
+
@typescript-eslint/no-unused-expressions | 60.700 | 2.2%
|
|
53
|
+
no-misleading-character-class | 59.072 | 2.1%
|
|
54
|
+
no-unexpected-multiline | 53.691 | 1.9%
|
|
55
|
+
@typescript-eslint/ban-ts-comment | 42.984 | 1.6%
|
|
56
|
+
no-loss-of-precision | 41.540 | 1.5%
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @utilitywarehouse/hearth-react-native
|
|
2
2
|
|
|
3
|
+
## 0.32.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1261](https://github.com/utilitywarehouse/hearth/pull/1261) [`745df73`](https://github.com/utilitywarehouse/hearth/commit/745df73a245dfe89d07aae5bac14256a4ff89e0a) Thanks [@robphoenix](https://github.com/robphoenix)! - ๐งน [HOUSEKEEPING]: Update dependencies
|
|
8
|
+
|
|
9
|
+
## 0.32.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#1222](https://github.com/utilitywarehouse/hearth/pull/1222) [`3f35a43`](https://github.com/utilitywarehouse/hearth/commit/3f35a431ba4a0fae45cc640a62ea6cb53f85c384) Thanks [@Utakato](https://github.com/Utakato)! - ๐ [FEATURE]: `Rating` component `emojis` variant
|
|
14
|
+
|
|
15
|
+
The `Rating` component now supports an `emojis` variant that renders emoji faces
|
|
16
|
+
instead of stars. When selected, the chosen emoji appears larger whilst
|
|
17
|
+
unselected emojis become grayscale. Two static labels ("Very dissatisfied" /
|
|
18
|
+
"Very satisfied") are displayed at the extremes.
|
|
19
|
+
|
|
20
|
+
**Components affected**:
|
|
21
|
+
- `Rating`
|
|
22
|
+
|
|
23
|
+
**Developer changes**:
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { Rating } from '@utilitywarehouse/hearth-react-native';
|
|
27
|
+
|
|
28
|
+
<Rating value={rating} onChange={setRating} variant="emojis" />;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
All existing props (`value`, `defaultValue`, `onChange`, `disabled`, `hideLabel`)
|
|
32
|
+
work with the emoji variant. The new `rangeLabels` prop allows overriding the
|
|
33
|
+
endpoint labels (defaulting to "Very dissatisfied" / "Very satisfied"):
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
<Rating variant="emojis" rangeLabels={{ low: 'Not at all', high: 'Absolutely' }} />
|
|
37
|
+
```
|
|
38
|
+
|
|
3
39
|
## 0.32.3
|
|
4
40
|
|
|
5
41
|
### Patch Changes
|
|
@@ -5,7 +5,7 @@ export declare const AccordionTrigger: import("react").ForwardRefExoticComponent
|
|
|
5
5
|
states?: {
|
|
6
6
|
active?: boolean;
|
|
7
7
|
};
|
|
8
|
-
}, "children"> & import("@gluestack-ui/accordion/lib/
|
|
8
|
+
}, "children"> & import("@gluestack-ui/accordion/lib/types").IAccordionTriggerProps & import("react").RefAttributes<import("react-native/types").PressableProps & {
|
|
9
9
|
states?: {
|
|
10
10
|
active?: boolean;
|
|
11
11
|
};
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { ButtonProps } from './Button.props';
|
|
2
|
-
export declare const ButtonText: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("react-native
|
|
2
|
+
export declare const ButtonText: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("react-native").TextProps> & import("react-native").TextProps>;
|
|
3
3
|
export declare const ButtonSpinner: import("react").ForwardRefExoticComponent<Omit<Omit<import("..").SpinnerProps, "size">, "ref"> & import("react").RefAttributes<Omit<import("..").SpinnerProps, "size">>>;
|
|
4
4
|
export declare const ButtonIcon: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("..").IconProps> & import("..").IconProps>;
|
|
5
|
-
export declare const ButtonGroupComponent: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("react-native
|
|
6
|
-
flexDirection?: import("react-native
|
|
5
|
+
export declare const ButtonGroupComponent: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("react-native").ViewProps & {
|
|
6
|
+
flexDirection?: import("react-native").ViewStyle["flexDirection"];
|
|
7
7
|
reversed?: boolean;
|
|
8
8
|
attached?: boolean;
|
|
9
9
|
space?: import("../../types").SpacingValues;
|
|
10
10
|
spacing?: import("../../types").SpacingValues;
|
|
11
|
-
}> & import("react-native
|
|
12
|
-
flexDirection?: import("react-native
|
|
11
|
+
}> & import("react-native").ViewProps & {
|
|
12
|
+
flexDirection?: import("react-native").ViewStyle["flexDirection"];
|
|
13
13
|
reversed?: boolean;
|
|
14
14
|
attached?: boolean;
|
|
15
15
|
space?: import("../../types").SpacingValues;
|
|
@@ -10,7 +10,7 @@ declare const CarouselControlItem: import("react").ForwardRefExoticComponent<Car
|
|
|
10
10
|
active?: boolean;
|
|
11
11
|
disabled?: boolean;
|
|
12
12
|
};
|
|
13
|
-
} & Omit<import("react-native
|
|
13
|
+
} & Omit<import("react-native").PressableProps, "children"> & {
|
|
14
14
|
tabIndex?: 0 | -1 | undefined;
|
|
15
15
|
} & {
|
|
16
16
|
children?: import("react").ReactNode | (({ hovered, pressed, focused, focusVisible, disabled, }: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { View } from 'react-native';
|
|
2
2
|
import FormFieldProps from './FormField.props';
|
|
3
|
-
export declare const FormFieldComponent: import("@gluestack-ui/form-control/lib/types").IFormControlComponentType<import("react-native
|
|
3
|
+
export declare const FormFieldComponent: import("@gluestack-ui/form-control/lib/types").IFormControlComponentType<import("react-native").ViewProps, Omit<import("../Helper/Helper.props").default, "validationStatus">, Omit<import("../Helper/Helper.props").default, "validationStatus">, Omit<import("..").IconProps, "as">, unknown, Omit<import("../Label/Label.props").default, "disabled">, unknown, Omit<import("../Helper/Helper.props").default, "validationStatus">, import("../BodyText").BodyTextProps>;
|
|
4
4
|
export declare const FormFieldLabel: import("react").ForwardRefExoticComponent<import("react").RefAttributes<unknown>> & {
|
|
5
5
|
Text: import("react").ForwardRefExoticComponent<Omit<Omit<import("../Label/Label.props").default, "disabled">, "ref"> & import("react").RefAttributes<Omit<import("../Label/Label.props").default, "disabled">>>;
|
|
6
6
|
};
|
|
@@ -9,13 +9,13 @@ export declare const InputComponent: import("@gluestack-ui/input/lib/typescript/
|
|
|
9
9
|
};
|
|
10
10
|
}, import("..").IconProps & {
|
|
11
11
|
as?: ComponentType;
|
|
12
|
-
}, import("react-native
|
|
12
|
+
}, import("react-native").ViewProps, import("react-native").TextInputProps & {
|
|
13
13
|
inBottomSheet?: boolean;
|
|
14
14
|
} & import("react").RefAttributes<TextInput>>;
|
|
15
|
-
export declare const InputSlot: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("react-native
|
|
16
|
-
export declare const InputField: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("react-native
|
|
15
|
+
export declare const InputSlot: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("react-native").ViewProps> & import("react-native").ViewProps & import("@gluestack-ui/input/lib/typescript/types").IInputSlotProps>;
|
|
16
|
+
export declare const InputField: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("react-native").TextInputProps & {
|
|
17
17
|
inBottomSheet?: boolean;
|
|
18
|
-
} & import("react").RefAttributes<TextInput>> & Omit<import("react-native
|
|
18
|
+
} & import("react").RefAttributes<TextInput>> & Omit<import("react-native").TextInputProps & {
|
|
19
19
|
inBottomSheet?: boolean;
|
|
20
20
|
} & import("react").RefAttributes<TextInput>, "ref"> & import("@gluestack-ui/input/lib/typescript/types").IInputProps>;
|
|
21
21
|
export declare const InputIcon: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("..").IconProps & {
|
|
@@ -4,7 +4,7 @@ declare const ListAction: import("react").ForwardRefExoticComponent<ListActionPr
|
|
|
4
4
|
active?: boolean;
|
|
5
5
|
disabled?: boolean;
|
|
6
6
|
};
|
|
7
|
-
} & Omit<import("react-native
|
|
7
|
+
} & Omit<import("react-native").PressableProps, "children"> & {
|
|
8
8
|
tabIndex?: 0 | -1 | undefined;
|
|
9
9
|
} & {
|
|
10
10
|
children?: import("react").ReactNode | (({ hovered, pressed, focused, focusVisible, disabled, }: {
|
|
@@ -4,7 +4,7 @@ declare const MenuItem: import("react").ForwardRefExoticComponent<MenuItemProps
|
|
|
4
4
|
active?: boolean;
|
|
5
5
|
disabled?: boolean;
|
|
6
6
|
};
|
|
7
|
-
} & Omit<import("react-native
|
|
7
|
+
} & Omit<import("react-native").PressableProps, "children"> & {
|
|
8
8
|
tabIndex?: 0 | -1 | undefined;
|
|
9
9
|
} & {
|
|
10
10
|
children?: import("react").ReactNode | (({ hovered, pressed, focused, focusVisible, disabled, }: {
|
|
@@ -3,7 +3,7 @@ export declare const Pill: import("react").ForwardRefExoticComponent<PillProps &
|
|
|
3
3
|
states?: {
|
|
4
4
|
active?: boolean;
|
|
5
5
|
};
|
|
6
|
-
} & Omit<import("react-native
|
|
6
|
+
} & Omit<import("react-native").PressableProps, "children"> & {
|
|
7
7
|
tabIndex?: 0 | -1 | undefined;
|
|
8
8
|
} & {
|
|
9
9
|
children?: import("react").ReactNode | (({ hovered, pressed, focused, focusVisible, disabled, }: {
|
|
@@ -5,25 +5,25 @@ declare const RadioCardGroup: import("react").ForwardRefExoticComponent<import("
|
|
|
5
5
|
onChange?: (value: string) => void;
|
|
6
6
|
gap?: keyof typeof import("../../tokens").space;
|
|
7
7
|
ref?: import("react").Ref<View>;
|
|
8
|
-
} & import("react-native
|
|
8
|
+
} & import("react-native").ViewProps & {
|
|
9
9
|
columns?: never;
|
|
10
|
-
flexDirection?: import("react-native
|
|
11
|
-
flexWrap?: import("react-native
|
|
12
|
-
alignItems?: import("react-native
|
|
13
|
-
justifyContent?: import("react-native
|
|
10
|
+
flexDirection?: import("react-native").ViewStyle["flexDirection"];
|
|
11
|
+
flexWrap?: import("react-native").ViewStyle["flexWrap"];
|
|
12
|
+
alignItems?: import("react-native").ViewStyle["alignItems"];
|
|
13
|
+
justifyContent?: import("react-native").ViewStyle["justifyContent"];
|
|
14
14
|
}, "ref"> | Omit<{
|
|
15
15
|
value?: string;
|
|
16
16
|
onChange?: (value: string) => void;
|
|
17
17
|
gap?: keyof typeof import("../../tokens").space;
|
|
18
18
|
ref?: import("react").Ref<View>;
|
|
19
|
-
} & import("react-native
|
|
19
|
+
} & import("react-native").ViewProps & {
|
|
20
20
|
columns: import("..").GridProps["columns"];
|
|
21
21
|
flexDirection?: never;
|
|
22
22
|
flexWrap?: never;
|
|
23
23
|
alignItems?: never;
|
|
24
24
|
justifyContent?: never;
|
|
25
25
|
}, "ref">) & import("@gluestack-ui/radio/lib/typescript/types").IRadioGroupProps)>;
|
|
26
|
-
declare const RadioCardIndicator: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("react-native
|
|
26
|
+
declare const RadioCardIndicator: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("react-native").ViewProps> & import("react-native").ViewProps>;
|
|
27
27
|
declare const RadioCardIcon: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("..").IconProps> & import("..").IconProps & {
|
|
28
28
|
forceMount?: boolean;
|
|
29
29
|
}>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type RatingProps from './Rating.props';
|
|
2
2
|
declare const Rating: {
|
|
3
|
-
({ value, defaultValue, onChange, disabled, labels, hideLabel, style, accessibilityLabel, ...props }: RatingProps): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
({ variant, value, defaultValue, onChange, disabled, labels, rangeLabels, hideLabel, style, accessibilityLabel, ...props }: RatingProps): import("react/jsx-runtime").JSX.Element;
|
|
4
4
|
displayName: string;
|
|
5
5
|
};
|
|
6
6
|
export default Rating;
|
|
@@ -3,12 +3,21 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
3
3
|
import { Pressable, View } from 'react-native';
|
|
4
4
|
import { StyleSheet } from 'react-native-unistyles';
|
|
5
5
|
import { BodyText } from '../BodyText';
|
|
6
|
+
import { getEmojiSvg } from './RatingEmoji';
|
|
6
7
|
import RatingStarEmpty from './RatingStarEmpty';
|
|
7
8
|
import RatingStarFilled from './RatingStarFilled';
|
|
9
|
+
import { EMOJI_LIST } from './Rating.utils';
|
|
8
10
|
const MAX_RATING = 5;
|
|
9
11
|
const STAR_WIDTH = 32;
|
|
10
12
|
const STAR_HEIGHT = 30;
|
|
11
13
|
const STAR_CONTAINER_SIZE = 40;
|
|
14
|
+
const EMOJI_SIZE_DEFAULT = 32;
|
|
15
|
+
const EMOJI_SIZE_SELECTED = 40;
|
|
16
|
+
const EMOJI_CONTAINER_SIZE = 44;
|
|
17
|
+
const DEFAULT_RANGE_LABELS = {
|
|
18
|
+
low: EMOJI_LIST[0].accessibilityLabel,
|
|
19
|
+
high: EMOJI_LIST[EMOJI_LIST.length - 1].accessibilityLabel,
|
|
20
|
+
};
|
|
12
21
|
const DEFAULT_LABELS = {
|
|
13
22
|
0: 'Select a rating',
|
|
14
23
|
1: 'Awful',
|
|
@@ -18,7 +27,7 @@ const DEFAULT_LABELS = {
|
|
|
18
27
|
5: 'Great!',
|
|
19
28
|
};
|
|
20
29
|
const clampRating = (value) => Math.min(MAX_RATING, Math.max(0, Math.round(value)));
|
|
21
|
-
const Rating = ({ value, defaultValue = 0, onChange, disabled = false, labels, hideLabel = false, style, accessibilityLabel, ...props }) => {
|
|
30
|
+
const Rating = ({ variant = 'stars', value, defaultValue = 0, onChange, disabled = false, labels, rangeLabels = DEFAULT_RANGE_LABELS, hideLabel = false, style, accessibilityLabel, ...props }) => {
|
|
22
31
|
const isControlled = value !== undefined;
|
|
23
32
|
const [internalValue, setInternalValue] = useState(clampRating(defaultValue));
|
|
24
33
|
useEffect(() => {
|
|
@@ -39,11 +48,22 @@ const Rating = ({ value, defaultValue = 0, onChange, disabled = false, labels, h
|
|
|
39
48
|
onChange?.(nextValue);
|
|
40
49
|
}, [disabled, isControlled, onChange]);
|
|
41
50
|
styles.useVariants({ disabled });
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
const isEmojis = variant === 'emojis';
|
|
52
|
+
const hasSelection = resolvedValue > 0;
|
|
53
|
+
const startLabel = rangeLabels.low;
|
|
54
|
+
const endLabel = rangeLabels.high;
|
|
55
|
+
return (_jsxs(View, { ...props, accessibilityRole: "radiogroup", accessibilityState: { disabled }, accessibilityLabel: accessibilityLabel ?? (isEmojis ? 'Rate your experience' : currentLabel), style: [styles.container, style], children: [_jsx(View, { style: styles.items, children: isEmojis
|
|
56
|
+
? EMOJI_LIST.map(entry => {
|
|
57
|
+
const isSelected = resolvedValue === entry.value;
|
|
58
|
+
const size = isSelected ? EMOJI_SIZE_SELECTED : EMOJI_SIZE_DEFAULT;
|
|
59
|
+
const EmojiSvg = getEmojiSvg(entry.value, hasSelection && !isSelected);
|
|
60
|
+
return (_jsx(Pressable, { accessibilityRole: "radio", accessibilityState: { selected: isSelected, disabled }, accessibilityLabel: `Rate ${entry.accessibilityLabel}`, disabled: disabled, hitSlop: 8, onPress: () => handlePress(entry.value), style: styles.emojiItem, children: _jsx(EmojiSvg, { width: size, height: size }) }, entry.value));
|
|
61
|
+
})
|
|
62
|
+
: [1, 2, 3, 4, 5].map(starValue => {
|
|
63
|
+
const isFilled = starValue <= resolvedValue;
|
|
64
|
+
const starLabel = resolvedLabels[starValue] ?? DEFAULT_LABELS[starValue];
|
|
65
|
+
return (_jsx(Pressable, { accessibilityRole: "radio", accessibilityState: { selected: resolvedValue === starValue, disabled }, accessibilityLabel: `Rate ${starLabel}`, disabled: disabled, hitSlop: 8, onPress: () => handlePress(starValue), style: styles.starItem, children: isFilled ? (_jsx(RatingStarFilled, { width: STAR_WIDTH, height: STAR_HEIGHT })) : (_jsx(RatingStarEmpty, { width: STAR_WIDTH, height: STAR_HEIGHT })) }, starValue));
|
|
66
|
+
}) }), isEmojis ? (!hideLabel ? (_jsxs(View, { style: styles.emojiLabels, children: [_jsx(BodyText, { size: "md", color: "secondary", children: startLabel }), _jsx(BodyText, { size: "md", color: "secondary", children: endLabel })] })) : null) : !hideLabel ? (_jsx(BodyText, { size: "md", color: labelColor, style: styles.label, children: currentLabel })) : null] }));
|
|
47
67
|
};
|
|
48
68
|
Rating.displayName = 'Rating';
|
|
49
69
|
const styles = StyleSheet.create(theme => ({
|
|
@@ -58,19 +78,31 @@ const styles = StyleSheet.create(theme => ({
|
|
|
58
78
|
},
|
|
59
79
|
},
|
|
60
80
|
},
|
|
61
|
-
|
|
81
|
+
items: {
|
|
62
82
|
flexDirection: 'row',
|
|
63
83
|
gap: theme.components.rating.gap,
|
|
64
84
|
},
|
|
65
|
-
|
|
85
|
+
starItem: {
|
|
66
86
|
width: STAR_CONTAINER_SIZE,
|
|
67
87
|
height: STAR_CONTAINER_SIZE,
|
|
68
88
|
alignItems: 'center',
|
|
69
89
|
justifyContent: 'center',
|
|
70
90
|
padding: theme.components.rating.borderWidth,
|
|
71
91
|
},
|
|
92
|
+
emojiItem: {
|
|
93
|
+
width: EMOJI_CONTAINER_SIZE,
|
|
94
|
+
height: EMOJI_CONTAINER_SIZE,
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
justifyContent: 'center',
|
|
97
|
+
padding: theme.components.rating.borderWidth,
|
|
98
|
+
},
|
|
72
99
|
label: {
|
|
73
100
|
textAlign: 'center',
|
|
74
101
|
},
|
|
102
|
+
emojiLabels: {
|
|
103
|
+
flexDirection: 'row',
|
|
104
|
+
justifyContent: 'space-between',
|
|
105
|
+
width: '100%',
|
|
106
|
+
},
|
|
75
107
|
}));
|
|
76
108
|
export default Rating;
|
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import type { ViewProps } from 'react-native';
|
|
2
2
|
export type RatingValue = 0 | 1 | 2 | 3 | 4 | 5;
|
|
3
3
|
export type RatingLabels = Partial<Record<RatingValue, string>>;
|
|
4
|
+
export type RatingVariant = 'stars' | 'emojis';
|
|
4
5
|
export interface RatingProps extends Omit<ViewProps, 'children'> {
|
|
6
|
+
/** Visual variant for the rating indicators. */
|
|
7
|
+
variant?: RatingVariant;
|
|
5
8
|
/** Current rating value. */
|
|
6
9
|
value?: RatingValue;
|
|
7
10
|
/** Initial rating value when uncontrolled. */
|
|
8
11
|
defaultValue?: RatingValue;
|
|
9
|
-
/** Called when a
|
|
12
|
+
/** Called when a rating is selected. */
|
|
10
13
|
onChange?: (value: RatingValue) => void;
|
|
11
14
|
/** Disables the rating input. */
|
|
12
15
|
disabled?: boolean;
|
|
13
16
|
/** Override labels for specific rating values. */
|
|
14
17
|
labels?: RatingLabels;
|
|
15
|
-
/**
|
|
18
|
+
/** Override the low and high end labels shown below the emoji variant. */
|
|
19
|
+
rangeLabels?: {
|
|
20
|
+
low: string;
|
|
21
|
+
high: string;
|
|
22
|
+
};
|
|
23
|
+
/** Hide the label text below the rating. */
|
|
16
24
|
hideLabel?: boolean;
|
|
17
25
|
}
|
|
18
26
|
export default RatingProps;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const EMOJI_LIST = [
|
|
2
|
+
{ value: 1, accessibilityLabel: 'Very dissatisfied' },
|
|
3
|
+
{ value: 2, accessibilityLabel: 'Dissatisfied' },
|
|
4
|
+
{ value: 3, accessibilityLabel: 'Neutral' },
|
|
5
|
+
{ value: 4, accessibilityLabel: 'Satisfied' },
|
|
6
|
+
{ value: 5, accessibilityLabel: 'Very satisfied' },
|
|
7
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { EMOJI_LIST } from './Rating.utils';
|
|
3
|
+
describe('EMOJI_LIST', () => {
|
|
4
|
+
it('has exactly 5 emojis mapped to values 1-5', () => {
|
|
5
|
+
expect(EMOJI_LIST).toHaveLength(5);
|
|
6
|
+
expect(EMOJI_LIST.map(e => e.value)).toEqual([1, 2, 3, 4, 5]);
|
|
7
|
+
});
|
|
8
|
+
it('each entry has an accessibility label', () => {
|
|
9
|
+
for (const entry of EMOJI_LIST) {
|
|
10
|
+
expect(entry.accessibilityLabel).toBeTruthy();
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import BeamingFace from '@utilitywarehouse/hearth-svg-assets/lib/beaming-face.svg';
|
|
2
|
+
import BeamingFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/beaming-face-grey.svg';
|
|
3
|
+
import DissapointedFace from '@utilitywarehouse/hearth-svg-assets/lib/dissapointed-face.svg';
|
|
4
|
+
import DissapointedFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/dissapointed-face-grey.svg';
|
|
5
|
+
import FrowningFace from '@utilitywarehouse/hearth-svg-assets/lib/frowning-face.svg';
|
|
6
|
+
import FrowningFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/frowning-face-grey.svg';
|
|
7
|
+
import NeutralFace from '@utilitywarehouse/hearth-svg-assets/lib/neutral-face.svg';
|
|
8
|
+
import NeutralFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/neutral-face-grey.svg';
|
|
9
|
+
import SlightlySmilingFace from '@utilitywarehouse/hearth-svg-assets/lib/slightly-smiling-face.svg';
|
|
10
|
+
import SlightlySmilingFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/slightly-smiling-face-grey.svg';
|
|
11
|
+
const EMOJI_ASSETS = {
|
|
12
|
+
1: { color: DissapointedFace, grey: DissapointedFaceGrey },
|
|
13
|
+
2: { color: FrowningFace, grey: FrowningFaceGrey },
|
|
14
|
+
3: { color: NeutralFace, grey: NeutralFaceGrey },
|
|
15
|
+
4: { color: SlightlySmilingFace, grey: SlightlySmilingFaceGrey },
|
|
16
|
+
5: { color: BeamingFace, grey: BeamingFaceGrey },
|
|
17
|
+
};
|
|
18
|
+
export const getEmojiSvg = (value, grayscale) => {
|
|
19
|
+
const assets = EMOJI_ASSETS[value];
|
|
20
|
+
return grayscale ? assets.grey : assets.color;
|
|
21
|
+
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as Rating } from './Rating';
|
|
2
|
-
export type { RatingLabels, RatingProps, RatingValue } from './Rating.props';
|
|
2
|
+
export type { RatingLabels, RatingProps, RatingValue, RatingVariant } from './Rating.props';
|
|
@@ -4,7 +4,7 @@ declare const SegmentedControlOption: import("react").ForwardRefExoticComponent<
|
|
|
4
4
|
active?: boolean;
|
|
5
5
|
disabled?: boolean;
|
|
6
6
|
};
|
|
7
|
-
} & Omit<import("react-native
|
|
7
|
+
} & Omit<import("react-native").PressableProps, "children"> & {
|
|
8
8
|
tabIndex?: 0 | -1 | undefined;
|
|
9
9
|
} & {
|
|
10
10
|
children?: import("react").ReactNode | (({ hovered, pressed, focused, focusVisible, disabled, }: {
|
|
@@ -4,7 +4,7 @@ declare const PressableTab: import("react").ForwardRefExoticComponent<TabProps &
|
|
|
4
4
|
active?: boolean;
|
|
5
5
|
disabled?: boolean;
|
|
6
6
|
};
|
|
7
|
-
} & Omit<import("react-native
|
|
7
|
+
} & Omit<import("react-native").PressableProps, "children"> & {
|
|
8
8
|
tabIndex?: 0 | -1 | undefined;
|
|
9
9
|
} & {
|
|
10
10
|
children?: import("react").ReactNode | (({ hovered, pressed, focused, focusVisible, disabled, }: {
|
|
@@ -5,7 +5,7 @@ export declare const TextareaComponent: import("@gluestack-ui/textarea/lib/types
|
|
|
5
5
|
disabled?: boolean;
|
|
6
6
|
readonly?: boolean;
|
|
7
7
|
};
|
|
8
|
-
}, import("react-native
|
|
9
|
-
export declare const TextareaField: import("react").ForwardRefExoticComponent<import("react-native
|
|
8
|
+
}, import("react-native").TextInputProps>;
|
|
9
|
+
export declare const TextareaField: import("react").ForwardRefExoticComponent<import("react-native").TextInputProps & import("react").RefAttributes<import("react-native").TextInputProps> & import("@gluestack-ui/textarea/lib/typescript/types").IInputProps>;
|
|
10
10
|
declare const Textarea: ({ validationStatus, children, resizable, defaultHeight, disabled, focused, readonly, label, labelVariant, helperText, validText, invalidText, required, helperIcon, onLayout, ...props }: TextareaProps) => import("react/jsx-runtime").JSX.Element;
|
|
11
11
|
export default Textarea;
|
package/docs/changelog.mdx
CHANGED
|
@@ -9,6 +9,42 @@ import { BackToTopButton, NextPrevPage } from './components';
|
|
|
9
9
|
The changelog for the Hearth React Native library. Here you can find all the changes, improvements, and bug fixes for each version.
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
## 0.32.5
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- [#1261](https://github.com/utilitywarehouse/hearth/pull/1261) [`745df73`](https://github.com/utilitywarehouse/hearth/commit/745df73a245dfe89d07aae5bac14256a4ff89e0a) Thanks [@robphoenix](https://github.com/robphoenix)! - ๐งน [HOUSEKEEPING]: Update dependencies
|
|
17
|
+
|
|
18
|
+
## 0.32.4
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- [#1222](https://github.com/utilitywarehouse/hearth/pull/1222) [`3f35a43`](https://github.com/utilitywarehouse/hearth/commit/3f35a431ba4a0fae45cc640a62ea6cb53f85c384) Thanks [@Utakato](https://github.com/Utakato)! - ๐ [FEATURE]: `Rating` component `emojis` variant
|
|
23
|
+
|
|
24
|
+
The `Rating` component now supports an `emojis` variant that renders emoji faces
|
|
25
|
+
instead of stars. When selected, the chosen emoji appears larger whilst
|
|
26
|
+
unselected emojis become grayscale. Two static labels ("Very dissatisfied" /
|
|
27
|
+
"Very satisfied") are displayed at the extremes.
|
|
28
|
+
|
|
29
|
+
**Components affected**:
|
|
30
|
+
- `Rating`
|
|
31
|
+
|
|
32
|
+
**Developer changes**:
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { Rating } from '@utilitywarehouse/hearth-react-native';
|
|
36
|
+
|
|
37
|
+
<Rating value={rating} onChange={setRating} variant="emojis" />;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
All existing props (`value`, `defaultValue`, `onChange`, `disabled`, `hideLabel`)
|
|
41
|
+
work with the emoji variant. The new `rangeLabels` prop allows overriding the
|
|
42
|
+
endpoint labels (defaulting to "Very dissatisfied" / "Very satisfied"):
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
<Rating variant="emojis" rangeLabels={{ low: 'Not at all', high: 'Absolutely' }} />
|
|
46
|
+
```
|
|
47
|
+
|
|
12
48
|
## 0.32.3
|
|
13
49
|
|
|
14
50
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilitywarehouse/hearth-react-native",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.5",
|
|
4
4
|
"description": "Utility Warehouse React Native UI library",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
"type": "module",
|
|
9
9
|
"sideEffects": false,
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@gluestack-ui/accordion": "1.0.
|
|
12
|
-
"@gluestack-ui/form-control": "0.1.19",
|
|
11
|
+
"@gluestack-ui/accordion": "1.0.14",
|
|
13
12
|
"@gluestack-ui/alert": "0.1.15",
|
|
14
13
|
"@gluestack-ui/button": "1.0.7",
|
|
15
14
|
"@gluestack-ui/checkbox": "0.1.32",
|
|
15
|
+
"@gluestack-ui/form-control": "0.1.19",
|
|
16
16
|
"@gluestack-ui/icon": "0.1.22",
|
|
17
17
|
"@gluestack-ui/input": "0.1.31",
|
|
18
18
|
"@gluestack-ui/link": "0.1.22",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"@gluestack-ui/switch": "0.1.22",
|
|
23
23
|
"@gluestack-ui/textarea": "0.1.23",
|
|
24
24
|
"@quidone/react-native-wheel-picker": "^1.6.1",
|
|
25
|
+
"@react-stately/utils": "^3.12.1",
|
|
25
26
|
"dayjs": "^1.11.13",
|
|
26
27
|
"nanoid": "3.3.11"
|
|
27
28
|
},
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
"@vitest/coverage-v8": "^3.2.4",
|
|
43
44
|
"chromatic": "^13.3.0",
|
|
44
45
|
"globals": "^15.15.0",
|
|
45
|
-
"playwright": "^1.
|
|
46
|
+
"playwright": "^1.59.1",
|
|
46
47
|
"prismjs": "^1.30.0",
|
|
47
48
|
"react": "^19.1.0",
|
|
48
49
|
"react-dom": "^19.1.0",
|
|
@@ -63,10 +64,10 @@
|
|
|
63
64
|
"vite-plugin-svgr": "^4.5.0",
|
|
64
65
|
"vitest": "^3.2.4",
|
|
65
66
|
"@utilitywarehouse/hearth-fonts": "^0.0.4",
|
|
66
|
-
"@utilitywarehouse/hearth-react-icons": "^0.8.
|
|
67
|
+
"@utilitywarehouse/hearth-react-icons": "^0.8.2",
|
|
67
68
|
"@utilitywarehouse/hearth-react-native-icons": "^0.8.1",
|
|
68
|
-
"@utilitywarehouse/hearth-svg-assets": "^0.6.
|
|
69
|
-
"@utilitywarehouse/hearth-tokens": "^0.
|
|
69
|
+
"@utilitywarehouse/hearth-svg-assets": "^0.6.2",
|
|
70
|
+
"@utilitywarehouse/hearth-tokens": "^0.3.0"
|
|
70
71
|
},
|
|
71
72
|
"peerDependencies": {
|
|
72
73
|
"@gorhom/bottom-sheet": ">=5.0.0",
|
|
@@ -81,7 +82,7 @@
|
|
|
81
82
|
"react-native-web": ">=0.19"
|
|
82
83
|
},
|
|
83
84
|
"scripts": {
|
|
84
|
-
"clean": "rm -rf node_modules
|
|
85
|
+
"clean": "rm -rf node_modules build .turbo",
|
|
85
86
|
"generateColours": "node ./scripts/generateColours.js",
|
|
86
87
|
"copyTokens": "node ./scripts/copyTokens.js",
|
|
87
88
|
"copy:changelog": "node ./scripts/copyChangelog.js",
|
|
@@ -93,10 +94,10 @@
|
|
|
93
94
|
"figma:publish": "figma connect publish",
|
|
94
95
|
"test": "vitest run --config vitest.unit.config.ts",
|
|
95
96
|
"test:storybook": "vitest run --project storybook",
|
|
96
|
-
"dev": "
|
|
97
|
+
"dev": "storybook dev -p 6006",
|
|
97
98
|
"dev:docs": "storybook dev -p 6002 --no-open",
|
|
98
|
-
"build:storybook": "
|
|
99
|
-
"build:storybook:docs": "
|
|
99
|
+
"build:storybook": "storybook build",
|
|
100
|
+
"build:storybook:docs": "storybook build --docs",
|
|
100
101
|
"chromatic": "npx chromatic --project-token=chpt_cce0fb1ebd95d2a --build-script-name build:storybook"
|
|
101
102
|
}
|
|
102
103
|
}
|
|
@@ -11,12 +11,13 @@ import * as Stories from './Rating.stories';
|
|
|
11
11
|
|
|
12
12
|
# Rating
|
|
13
13
|
|
|
14
|
-
Use Rating to collect a
|
|
14
|
+
Use Rating to collect a score with an optional descriptive label. Supports star and emoji variants.
|
|
15
15
|
|
|
16
16
|
- [Playground](#playground)
|
|
17
17
|
- [Usage](#usage)
|
|
18
18
|
- [Props](#props)
|
|
19
19
|
- [Examples](#examples)
|
|
20
|
+
- [Emoji variant](#emoji-variant)
|
|
20
21
|
- [Accessibility](#accessibility)
|
|
21
22
|
|
|
22
23
|
## Playground
|
|
@@ -52,10 +53,12 @@ const MyComponent = () => {
|
|
|
52
53
|
| -------------- | -------------------------------------- | ------------------------------------------- | ----------- |
|
|
53
54
|
| `value` | `0 \| 1 \| 2 \| 3 \| 4 \| 5` | Current rating value. | `0` |
|
|
54
55
|
| `defaultValue` | `0 \| 1 \| 2 \| 3 \| 4 \| 5` | Initial rating value when uncontrolled. | `0` |
|
|
55
|
-
| `onChange` | `(value: RatingValue) => void` | Called when a
|
|
56
|
+
| `onChange` | `(value: RatingValue) => void` | Called when a rating is selected. | `undefined` |
|
|
56
57
|
| `disabled` | `boolean` | Disables the rating input. | `false` |
|
|
57
58
|
| `labels` | `Partial<Record<RatingValue, string>>` | Override labels for specific rating values. | `undefined` |
|
|
58
|
-
| `
|
|
59
|
+
| `rangeLabels` | `{ low: string; high: string }` | Override the low and high end labels shown below the emoji variant. | `{ low: 'Very dissatisfied', high: 'Very satisfied' }` |
|
|
60
|
+
| `hideLabel` | `boolean` | Hide the label text below the rating. | `false` |
|
|
61
|
+
| `variant` | `'stars' \| 'emojis'` | Visual variant for the rating indicators. | `'stars'` |
|
|
59
62
|
|
|
60
63
|
## Examples
|
|
61
64
|
|
|
@@ -171,8 +174,44 @@ import { Rating } from '@utilitywarehouse/hearth-react-native';
|
|
|
171
174
|
const MyComponent = () => <Rating value={4} disabled />;
|
|
172
175
|
```
|
|
173
176
|
|
|
177
|
+
### Emoji variant
|
|
178
|
+
|
|
179
|
+
Use `variant="emojis"` to show emoji faces instead of stars. Labels default to "Very dissatisfied" and "Very satisfied" at the extremes. Override them with `rangeLabels={{ low: 'Custom low', high: 'Custom high' }}`.
|
|
180
|
+
|
|
181
|
+
<UsageWrap>
|
|
182
|
+
<Center>
|
|
183
|
+
<Box>
|
|
184
|
+
<Rating value={4} variant="emojis" />
|
|
185
|
+
</Box>
|
|
186
|
+
</Center>
|
|
187
|
+
</UsageWrap>
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
import { Rating } from '@utilitywarehouse/hearth-react-native';
|
|
191
|
+
|
|
192
|
+
const MyComponent = () => <Rating value={4} variant="emojis" />;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Emoji variant (no selection)
|
|
196
|
+
|
|
197
|
+
When no emoji is selected, all emojis display at normal size with full colour.
|
|
198
|
+
|
|
199
|
+
<UsageWrap>
|
|
200
|
+
<Center>
|
|
201
|
+
<Box>
|
|
202
|
+
<Rating value={0} variant="emojis" />
|
|
203
|
+
</Box>
|
|
204
|
+
</Center>
|
|
205
|
+
</UsageWrap>
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { Rating } from '@utilitywarehouse/hearth-react-native';
|
|
209
|
+
|
|
210
|
+
const MyComponent = () => <Rating value={0} variant="emojis" />;
|
|
211
|
+
```
|
|
212
|
+
|
|
174
213
|
## Accessibility
|
|
175
214
|
|
|
176
|
-
- Rating uses a `radiogroup` container with `radio` items for each star.
|
|
177
|
-
- Each star announces a descriptive label (e.g., "Rate Okay"). Override labels with the `labels` prop to match your content.
|
|
215
|
+
- Rating uses a `radiogroup` container with `radio` items for each star or emoji.
|
|
216
|
+
- Each star announces a descriptive label (e.g., "Rate Okay"). Each emoji announces its sentiment (e.g., "Rate Neutral"). Override labels with the `labels` prop to match your content.
|
|
178
217
|
- Provide `accessibilityLabel` when the default label text is not sufficient for your screen reader context.
|
|
@@ -18,3 +18,22 @@ figma.connect(
|
|
|
18
18
|
example: props => <Rating value={props.value} />,
|
|
19
19
|
}
|
|
20
20
|
);
|
|
21
|
+
|
|
22
|
+
figma.connect(
|
|
23
|
+
Rating,
|
|
24
|
+
'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=10620-4185',
|
|
25
|
+
{
|
|
26
|
+
// variant: { Variant: 'Emojis' },
|
|
27
|
+
props: {
|
|
28
|
+
value: figma.enum('Rating', {
|
|
29
|
+
'0 Star': 0,
|
|
30
|
+
'1 Star': 1,
|
|
31
|
+
'2 Star': 2,
|
|
32
|
+
'3 Star': 3,
|
|
33
|
+
'4 Star': 4,
|
|
34
|
+
'5 Star': 5,
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
example: props => <Rating value={props.value} variant="emojis" />,
|
|
38
|
+
}
|
|
39
|
+
);
|
|
@@ -4,18 +4,24 @@ export type RatingValue = 0 | 1 | 2 | 3 | 4 | 5;
|
|
|
4
4
|
|
|
5
5
|
export type RatingLabels = Partial<Record<RatingValue, string>>;
|
|
6
6
|
|
|
7
|
+
export type RatingVariant = 'stars' | 'emojis';
|
|
8
|
+
|
|
7
9
|
export interface RatingProps extends Omit<ViewProps, 'children'> {
|
|
10
|
+
/** Visual variant for the rating indicators. */
|
|
11
|
+
variant?: RatingVariant;
|
|
8
12
|
/** Current rating value. */
|
|
9
13
|
value?: RatingValue;
|
|
10
14
|
/** Initial rating value when uncontrolled. */
|
|
11
15
|
defaultValue?: RatingValue;
|
|
12
|
-
/** Called when a
|
|
16
|
+
/** Called when a rating is selected. */
|
|
13
17
|
onChange?: (value: RatingValue) => void;
|
|
14
18
|
/** Disables the rating input. */
|
|
15
19
|
disabled?: boolean;
|
|
16
20
|
/** Override labels for specific rating values. */
|
|
17
21
|
labels?: RatingLabels;
|
|
18
|
-
/**
|
|
22
|
+
/** Override the low and high end labels shown below the emoji variant. */
|
|
23
|
+
rangeLabels?: { low: string; high: string };
|
|
24
|
+
/** Hide the label text below the rating. */
|
|
19
25
|
hideLabel?: boolean;
|
|
20
26
|
}
|
|
21
27
|
|
|
@@ -23,17 +23,25 @@ const meta = {
|
|
|
23
23
|
labels: {
|
|
24
24
|
control: 'object',
|
|
25
25
|
},
|
|
26
|
+
rangeLabels: {
|
|
27
|
+
control: 'object',
|
|
28
|
+
},
|
|
26
29
|
hideLabel: {
|
|
27
30
|
control: 'boolean',
|
|
28
31
|
},
|
|
29
32
|
disabled: {
|
|
30
33
|
control: 'boolean',
|
|
31
34
|
},
|
|
35
|
+
variant: {
|
|
36
|
+
options: ['stars', 'emojis'],
|
|
37
|
+
control: 'radio',
|
|
38
|
+
},
|
|
32
39
|
},
|
|
33
40
|
args: {
|
|
34
41
|
value: 3,
|
|
35
42
|
hideLabel: false,
|
|
36
43
|
disabled: false,
|
|
44
|
+
variant: 'stars',
|
|
37
45
|
},
|
|
38
46
|
} satisfies Meta<typeof Rating>;
|
|
39
47
|
|
|
@@ -90,6 +98,15 @@ export const Variants: Story = {
|
|
|
90
98
|
<VariantTitle title="Disabled">
|
|
91
99
|
<Rating value={5} disabled />
|
|
92
100
|
</VariantTitle>
|
|
101
|
+
<VariantTitle title="Emojis">
|
|
102
|
+
<Rating value={4} variant="emojis" />
|
|
103
|
+
</VariantTitle>
|
|
104
|
+
<VariantTitle title="Emojis (No Selection)">
|
|
105
|
+
<Rating value={0} variant="emojis" />
|
|
106
|
+
</VariantTitle>
|
|
107
|
+
<VariantTitle title="Emojis (Disabled)">
|
|
108
|
+
<Rating value={3} variant="emojis" disabled />
|
|
109
|
+
</VariantTitle>
|
|
93
110
|
</Box>
|
|
94
111
|
),
|
|
95
112
|
};
|
|
@@ -4,13 +4,23 @@ import { StyleSheet } from 'react-native-unistyles';
|
|
|
4
4
|
import { BodyText } from '../BodyText';
|
|
5
5
|
import type RatingProps from './Rating.props';
|
|
6
6
|
import type { RatingLabels, RatingValue } from './Rating.props';
|
|
7
|
+
import { getEmojiSvg } from './RatingEmoji';
|
|
7
8
|
import RatingStarEmpty from './RatingStarEmpty';
|
|
8
9
|
import RatingStarFilled from './RatingStarFilled';
|
|
10
|
+
import { EMOJI_LIST } from './Rating.utils';
|
|
9
11
|
|
|
10
12
|
const MAX_RATING: RatingValue = 5;
|
|
11
13
|
const STAR_WIDTH = 32;
|
|
12
14
|
const STAR_HEIGHT = 30;
|
|
13
15
|
const STAR_CONTAINER_SIZE = 40;
|
|
16
|
+
const EMOJI_SIZE_DEFAULT = 32;
|
|
17
|
+
const EMOJI_SIZE_SELECTED = 40;
|
|
18
|
+
const EMOJI_CONTAINER_SIZE = 44;
|
|
19
|
+
|
|
20
|
+
const DEFAULT_RANGE_LABELS = {
|
|
21
|
+
low: EMOJI_LIST[0].accessibilityLabel,
|
|
22
|
+
high: EMOJI_LIST[EMOJI_LIST.length - 1].accessibilityLabel,
|
|
23
|
+
};
|
|
14
24
|
|
|
15
25
|
const DEFAULT_LABELS: Record<RatingValue, string> = {
|
|
16
26
|
0: 'Select a rating',
|
|
@@ -25,11 +35,13 @@ const clampRating = (value: number) =>
|
|
|
25
35
|
Math.min(MAX_RATING, Math.max(0, Math.round(value))) as RatingValue;
|
|
26
36
|
|
|
27
37
|
const Rating = ({
|
|
38
|
+
variant = 'stars',
|
|
28
39
|
value,
|
|
29
40
|
defaultValue = 0,
|
|
30
41
|
onChange,
|
|
31
42
|
disabled = false,
|
|
32
43
|
labels,
|
|
44
|
+
rangeLabels = DEFAULT_RANGE_LABELS,
|
|
33
45
|
hideLabel = false,
|
|
34
46
|
style,
|
|
35
47
|
accessibilityLabel,
|
|
@@ -65,40 +77,74 @@ const Rating = ({
|
|
|
65
77
|
|
|
66
78
|
styles.useVariants({ disabled });
|
|
67
79
|
|
|
80
|
+
const isEmojis = variant === 'emojis';
|
|
81
|
+
const hasSelection = resolvedValue > 0;
|
|
82
|
+
|
|
83
|
+
const startLabel = rangeLabels.low;
|
|
84
|
+
const endLabel = rangeLabels.high;
|
|
85
|
+
|
|
68
86
|
return (
|
|
69
87
|
<View
|
|
70
88
|
{...props}
|
|
71
89
|
accessibilityRole="radiogroup"
|
|
72
90
|
accessibilityState={{ disabled }}
|
|
73
|
-
accessibilityLabel={accessibilityLabel ?? currentLabel}
|
|
91
|
+
accessibilityLabel={accessibilityLabel ?? (isEmojis ? 'Rate your experience' : currentLabel)}
|
|
74
92
|
style={[styles.container, style]}
|
|
75
93
|
>
|
|
76
|
-
<View style={styles.
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
)
|
|
99
|
-
|
|
94
|
+
<View style={styles.items}>
|
|
95
|
+
{isEmojis
|
|
96
|
+
? EMOJI_LIST.map(entry => {
|
|
97
|
+
const isSelected = resolvedValue === entry.value;
|
|
98
|
+
const size = isSelected ? EMOJI_SIZE_SELECTED : EMOJI_SIZE_DEFAULT;
|
|
99
|
+
const EmojiSvg = getEmojiSvg(entry.value, hasSelection && !isSelected);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Pressable
|
|
103
|
+
key={entry.value}
|
|
104
|
+
accessibilityRole="radio"
|
|
105
|
+
accessibilityState={{ selected: isSelected, disabled }}
|
|
106
|
+
accessibilityLabel={`Rate ${entry.accessibilityLabel}`}
|
|
107
|
+
disabled={disabled}
|
|
108
|
+
hitSlop={8}
|
|
109
|
+
onPress={() => handlePress(entry.value)}
|
|
110
|
+
style={styles.emojiItem}
|
|
111
|
+
>
|
|
112
|
+
<EmojiSvg width={size} height={size} />
|
|
113
|
+
</Pressable>
|
|
114
|
+
);
|
|
115
|
+
})
|
|
116
|
+
: ([1, 2, 3, 4, 5] as RatingValue[]).map(starValue => {
|
|
117
|
+
const isFilled = starValue <= resolvedValue;
|
|
118
|
+
const starLabel = resolvedLabels[starValue] ?? DEFAULT_LABELS[starValue];
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<Pressable
|
|
122
|
+
key={starValue}
|
|
123
|
+
accessibilityRole="radio"
|
|
124
|
+
accessibilityState={{ selected: resolvedValue === starValue, disabled }}
|
|
125
|
+
accessibilityLabel={`Rate ${starLabel}`}
|
|
126
|
+
disabled={disabled}
|
|
127
|
+
hitSlop={8}
|
|
128
|
+
onPress={() => handlePress(starValue)}
|
|
129
|
+
style={styles.starItem}
|
|
130
|
+
>
|
|
131
|
+
{isFilled ? (
|
|
132
|
+
<RatingStarFilled width={STAR_WIDTH} height={STAR_HEIGHT} />
|
|
133
|
+
) : (
|
|
134
|
+
<RatingStarEmpty width={STAR_WIDTH} height={STAR_HEIGHT} />
|
|
135
|
+
)}
|
|
136
|
+
</Pressable>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
100
139
|
</View>
|
|
101
|
-
{
|
|
140
|
+
{isEmojis ? (
|
|
141
|
+
!hideLabel ? (
|
|
142
|
+
<View style={styles.emojiLabels}>
|
|
143
|
+
<BodyText size="md" color="secondary">{startLabel}</BodyText>
|
|
144
|
+
<BodyText size="md" color="secondary">{endLabel}</BodyText>
|
|
145
|
+
</View>
|
|
146
|
+
) : null
|
|
147
|
+
) : !hideLabel ? (
|
|
102
148
|
<BodyText size="md" color={labelColor} style={styles.label}>
|
|
103
149
|
{currentLabel}
|
|
104
150
|
</BodyText>
|
|
@@ -121,20 +167,32 @@ const styles = StyleSheet.create(theme => ({
|
|
|
121
167
|
},
|
|
122
168
|
},
|
|
123
169
|
},
|
|
124
|
-
|
|
170
|
+
items: {
|
|
125
171
|
flexDirection: 'row',
|
|
126
172
|
gap: theme.components.rating.gap,
|
|
127
173
|
},
|
|
128
|
-
|
|
174
|
+
starItem: {
|
|
129
175
|
width: STAR_CONTAINER_SIZE,
|
|
130
176
|
height: STAR_CONTAINER_SIZE,
|
|
131
177
|
alignItems: 'center',
|
|
132
178
|
justifyContent: 'center',
|
|
133
179
|
padding: theme.components.rating.borderWidth,
|
|
134
180
|
},
|
|
181
|
+
emojiItem: {
|
|
182
|
+
width: EMOJI_CONTAINER_SIZE,
|
|
183
|
+
height: EMOJI_CONTAINER_SIZE,
|
|
184
|
+
alignItems: 'center',
|
|
185
|
+
justifyContent: 'center',
|
|
186
|
+
padding: theme.components.rating.borderWidth,
|
|
187
|
+
},
|
|
135
188
|
label: {
|
|
136
189
|
textAlign: 'center',
|
|
137
190
|
},
|
|
191
|
+
emojiLabels: {
|
|
192
|
+
flexDirection: 'row',
|
|
193
|
+
justifyContent: 'space-between',
|
|
194
|
+
width: '100%',
|
|
195
|
+
},
|
|
138
196
|
}));
|
|
139
197
|
|
|
140
198
|
export default Rating;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { EMOJI_LIST } from './Rating.utils';
|
|
3
|
+
|
|
4
|
+
describe('EMOJI_LIST', () => {
|
|
5
|
+
it('has exactly 5 emojis mapped to values 1-5', () => {
|
|
6
|
+
expect(EMOJI_LIST).toHaveLength(5);
|
|
7
|
+
expect(EMOJI_LIST.map(e => e.value)).toEqual([1, 2, 3, 4, 5]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('each entry has an accessibility label', () => {
|
|
11
|
+
for (const entry of EMOJI_LIST) {
|
|
12
|
+
expect(entry.accessibilityLabel).toBeTruthy();
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RatingValue } from './Rating.props';
|
|
2
|
+
|
|
3
|
+
export interface EmojiEntry {
|
|
4
|
+
value: Exclude<RatingValue, 0>;
|
|
5
|
+
accessibilityLabel: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const EMOJI_LIST: EmojiEntry[] = [
|
|
9
|
+
{ value: 1, accessibilityLabel: 'Very dissatisfied' },
|
|
10
|
+
{ value: 2, accessibilityLabel: 'Dissatisfied' },
|
|
11
|
+
{ value: 3, accessibilityLabel: 'Neutral' },
|
|
12
|
+
{ value: 4, accessibilityLabel: 'Satisfied' },
|
|
13
|
+
{ value: 5, accessibilityLabel: 'Very satisfied' },
|
|
14
|
+
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import BeamingFace from '@utilitywarehouse/hearth-svg-assets/lib/beaming-face.svg';
|
|
2
|
+
import BeamingFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/beaming-face-grey.svg';
|
|
3
|
+
import DissapointedFace from '@utilitywarehouse/hearth-svg-assets/lib/dissapointed-face.svg';
|
|
4
|
+
import DissapointedFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/dissapointed-face-grey.svg';
|
|
5
|
+
import FrowningFace from '@utilitywarehouse/hearth-svg-assets/lib/frowning-face.svg';
|
|
6
|
+
import FrowningFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/frowning-face-grey.svg';
|
|
7
|
+
import NeutralFace from '@utilitywarehouse/hearth-svg-assets/lib/neutral-face.svg';
|
|
8
|
+
import NeutralFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/neutral-face-grey.svg';
|
|
9
|
+
import SlightlySmilingFace from '@utilitywarehouse/hearth-svg-assets/lib/slightly-smiling-face.svg';
|
|
10
|
+
import SlightlySmilingFaceGrey from '@utilitywarehouse/hearth-svg-assets/lib/slightly-smiling-face-grey.svg';
|
|
11
|
+
import type { FC } from 'react';
|
|
12
|
+
import type { SvgProps } from 'react-native-svg';
|
|
13
|
+
|
|
14
|
+
const EMOJI_ASSETS: Record<1 | 2 | 3 | 4 | 5, { color: FC<SvgProps>; grey: FC<SvgProps> }> = {
|
|
15
|
+
1: { color: DissapointedFace, grey: DissapointedFaceGrey },
|
|
16
|
+
2: { color: FrowningFace, grey: FrowningFaceGrey },
|
|
17
|
+
3: { color: NeutralFace, grey: NeutralFaceGrey },
|
|
18
|
+
4: { color: SlightlySmilingFace, grey: SlightlySmilingFaceGrey },
|
|
19
|
+
5: { color: BeamingFace, grey: BeamingFaceGrey },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const getEmojiSvg = (
|
|
23
|
+
value: 1 | 2 | 3 | 4 | 5,
|
|
24
|
+
grayscale: boolean
|
|
25
|
+
): FC<SvgProps> => {
|
|
26
|
+
const assets = EMOJI_ASSETS[value];
|
|
27
|
+
return grayscale ? assets.grey : assets.color;
|
|
28
|
+
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as Rating } from './Rating';
|
|
2
|
-
export type { RatingLabels, RatingProps, RatingValue } from './Rating.props';
|
|
2
|
+
export type { RatingLabels, RatingProps, RatingValue, RatingVariant } from './Rating.props';
|
package/src/vite-env.d.ts
CHANGED
|
@@ -10,3 +10,10 @@ declare module '*.svg' {
|
|
|
10
10
|
const content: React.FC<SvgProps>;
|
|
11
11
|
export default content;
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
declare module '@utilitywarehouse/hearth-svg-assets/lib/*.svg' {
|
|
15
|
+
import React from 'react';
|
|
16
|
+
import { SvgProps } from 'react-native-svg';
|
|
17
|
+
const content: React.FC<SvgProps>;
|
|
18
|
+
export default content;
|
|
19
|
+
}
|
package/vitest.config.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
|
|
4
3
|
import { defineConfig } from 'vitest/config';
|
|
5
|
-
|
|
6
4
|
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
|
|
7
5
|
|
|
8
6
|
const dirname =
|
|
@@ -10,6 +8,9 @@ const dirname =
|
|
|
10
8
|
|
|
11
9
|
// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
|
|
12
10
|
export default defineConfig({
|
|
11
|
+
optimizeDeps: {
|
|
12
|
+
include: ['@react-stately/utils'],
|
|
13
|
+
},
|
|
13
14
|
test: {
|
|
14
15
|
projects: [
|
|
15
16
|
{
|