@jobber/components-native 0.115.0 → 0.115.2-codexjob--9905233.7
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/dist/docs/ProgressIndicator/ProgressIndicator.md +3 -6
- package/dist/package.json +2 -2
- package/dist/src/Chip/Chip.js +45 -8
- package/dist/src/Chip/Chip.test.js +53 -0
- package/dist/src/Select/SelectRoot.test.js +30 -1
- package/dist/src/primitives/Select/SelectPrimitive.js +10 -3
- package/dist/src/primitives/Select/SelectPrimitive.test.js +30 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/primitives/Select/SelectPrimitive.d.ts +1 -1
- package/package.json +2 -2
- package/src/Chip/Chip.test.tsx +112 -0
- package/src/Chip/Chip.tsx +80 -29
- package/src/Select/Select.composable.stories.tsx +6 -0
- package/src/Select/Select.guide.md +9 -2
- package/src/Select/SelectRoot.test.tsx +58 -1
- package/src/Select/docs/SelectComposableDependentOptions.tsx +78 -0
- package/src/Select/docs/index.ts +1 -0
- package/src/primitives/Select/SelectPrimitive.test.tsx +55 -0
- package/src/primitives/Select/SelectPrimitive.tsx +15 -2
|
@@ -58,13 +58,10 @@ The indicator does not participate in the keyboard tab order.
|
|
|
58
58
|
|
|
59
59
|
### Reduced motion
|
|
60
60
|
|
|
61
|
-
The width transition between `value` updates is
|
|
62
|
-
under
|
|
61
|
+
The width transition between `value` updates is disabled under
|
|
63
62
|
[`prefers-reduced-motion: reduce`](https://developer.mozilla.org/docs/Web/CSS/@media/prefers-reduced-motion).
|
|
64
|
-
The
|
|
65
|
-
|
|
66
|
-
[WCAG 2.3.3 ("Animation from Interactions")](https://www.w3.org/WAI/WCAG21/Understanding/animation-from-interactions.html).
|
|
67
|
-
Removing the transition would make progress harder to perceive, not easier.
|
|
63
|
+
The fill updates immediately to the new value, so current progress remains
|
|
64
|
+
visible without interpolation. The stepped variation does not animate.
|
|
68
65
|
|
|
69
66
|
## Relationship to ProgressBar
|
|
70
67
|
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "0.115.
|
|
3
|
+
"version": "0.115.2-codexjob--9905233.7+9905233c",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React Native implementation of Atlantis",
|
|
6
6
|
"repository": {
|
|
@@ -123,5 +123,5 @@
|
|
|
123
123
|
"react-native-screens": ">=4.18.0",
|
|
124
124
|
"react-native-svg": ">=12.0.0"
|
|
125
125
|
},
|
|
126
|
-
"gitHead": "
|
|
126
|
+
"gitHead": "9905233c18ce012134f57ba140fabba5225e18a6"
|
|
127
127
|
}
|
package/dist/src/Chip/Chip.js
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
import React, { useMemo } from "react";
|
|
2
2
|
import { Pressable, View } from "react-native";
|
|
3
|
+
import { allColors } from "@jobber/design";
|
|
3
4
|
import { useStyles } from "./Chip.style";
|
|
4
5
|
import { Icon } from "../Icon";
|
|
5
6
|
import { Typography } from "../Typography";
|
|
6
7
|
import { useAtlantisTheme } from "../AtlantisThemeContext";
|
|
7
8
|
export function Chip({ icon, label, onPress, isDismissible, isActive, inactiveBackgroundColor = "background", accessibilityLabel, accessibilityRole = "radio", accent, }) {
|
|
8
9
|
const styles = useStyles();
|
|
9
|
-
const { tokens } = useAtlantisTheme();
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const { tokens, effectiveTheme } = useAtlantisTheme();
|
|
11
|
+
const { chipStyle, iconCustomColor, labelStyle, dismissColor } = useMemo(() => {
|
|
12
|
+
const accentColor = accent
|
|
13
|
+
? tokens[`color-${accent}`]
|
|
14
|
+
: tokens["color-surface--reverse"];
|
|
15
|
+
let backgroundColor = accentColor;
|
|
16
|
+
let iconColor = isActive ? tokens["color-surface"] : accentColor;
|
|
17
|
+
let selectedLabelStyle;
|
|
18
|
+
if (isActive && accent) {
|
|
19
|
+
const selected = getSelectedAccentColors(accent, {
|
|
20
|
+
tokens,
|
|
21
|
+
effectiveTheme,
|
|
22
|
+
});
|
|
23
|
+
backgroundColor = selected.background;
|
|
24
|
+
iconColor = selected.foreground;
|
|
25
|
+
selectedLabelStyle = {
|
|
26
|
+
textStyle: { color: selected.foreground },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
14
29
|
const chip = [
|
|
15
30
|
styles.container,
|
|
16
31
|
{
|
|
@@ -18,7 +33,7 @@ export function Chip({ icon, label, onPress, isDismissible, isActive, inactiveBa
|
|
|
18
33
|
? tokens["color-surface"]
|
|
19
34
|
: tokens["color-interactive--background"],
|
|
20
35
|
},
|
|
21
|
-
isActive && { backgroundColor
|
|
36
|
+
isActive && { backgroundColor },
|
|
22
37
|
getBorderStyle(inactiveBackgroundColor, tokens),
|
|
23
38
|
];
|
|
24
39
|
const dismiss = (isActive || inactiveBackgroundColor === "surface") &&
|
|
@@ -26,9 +41,17 @@ export function Chip({ icon, label, onPress, isDismissible, isActive, inactiveBa
|
|
|
26
41
|
return {
|
|
27
42
|
chipStyle: chip,
|
|
28
43
|
iconCustomColor: iconColor,
|
|
44
|
+
labelStyle: selectedLabelStyle,
|
|
29
45
|
dismissColor: dismiss,
|
|
30
46
|
};
|
|
31
|
-
}, [
|
|
47
|
+
}, [
|
|
48
|
+
accent,
|
|
49
|
+
isActive,
|
|
50
|
+
inactiveBackgroundColor,
|
|
51
|
+
styles,
|
|
52
|
+
tokens,
|
|
53
|
+
effectiveTheme,
|
|
54
|
+
]);
|
|
32
55
|
const accessibilityState = useMemo(() => {
|
|
33
56
|
const checkableRoles = ["radio", "switch", "togglebutton", "checkbox"];
|
|
34
57
|
if (checkableRoles.includes(accessibilityRole)) {
|
|
@@ -40,10 +63,24 @@ export function Chip({ icon, label, onPress, isDismissible, isActive, inactiveBa
|
|
|
40
63
|
icon && (React.createElement(View, { style: styles.iconLeft },
|
|
41
64
|
React.createElement(Icon, { name: icon, size: "base", customColor: iconCustomColor }))),
|
|
42
65
|
label && (React.createElement(View, { style: styles.chipText },
|
|
43
|
-
React.createElement(Typography, { color: "base", fontWeight: "medium", maxFontScaleSize: tokens["typography--fontSize-large"], maxLines: "single", reverseTheme: isActive }, label))),
|
|
66
|
+
React.createElement(Typography, { color: "base", fontWeight: "medium", maxFontScaleSize: tokens["typography--fontSize-large"], maxLines: "single", reverseTheme: isActive && !accent, UNSAFE_style: labelStyle }, label))),
|
|
44
67
|
isDismissible && (React.createElement(View, { style: [styles.dismissIcon, dismissColor] },
|
|
45
68
|
React.createElement(Icon, { name: "cross", size: "small" })))));
|
|
46
69
|
}
|
|
70
|
+
// Object labels and leading icons share the selected foreground.
|
|
71
|
+
function getSelectedAccentColors(accent, { tokens, effectiveTheme, }) {
|
|
72
|
+
const colors = {
|
|
73
|
+
background: allColors[`color-${accent}`],
|
|
74
|
+
foreground: allColors["color-text--reverse"],
|
|
75
|
+
};
|
|
76
|
+
if (accent === "client") {
|
|
77
|
+
colors.background = tokens["color-client"];
|
|
78
|
+
if (effectiveTheme === "dark") {
|
|
79
|
+
colors.foreground = allColors["color-text"];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return colors;
|
|
83
|
+
}
|
|
47
84
|
function getBorderStyle(inactiveBackgroundColor, tokens) {
|
|
48
85
|
let borderColor = "transparent";
|
|
49
86
|
if (inactiveBackgroundColor === "surface") {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { allColors, darkTokens } from "@jobber/design";
|
|
2
3
|
import { fireEvent, render } from "@testing-library/react-native";
|
|
4
|
+
import { Path } from "react-native-svg";
|
|
3
5
|
import { Chip } from "./Chip";
|
|
6
|
+
import { AtlantisThemeContextProvider } from "../AtlantisThemeContext";
|
|
4
7
|
import { tokens } from "../utils/design";
|
|
5
8
|
it("renders an active Chip", () => {
|
|
6
9
|
const { getByText, getByLabelText } = render(React.createElement(Chip, { label: "Foo", onPress: jest.fn(), accessibilityLabel: "Foo chip", isActive: true }));
|
|
@@ -67,3 +70,53 @@ describe("accent", () => {
|
|
|
67
70
|
});
|
|
68
71
|
});
|
|
69
72
|
});
|
|
73
|
+
describe("selection color transitions", () => {
|
|
74
|
+
function example(theme, props = {}) {
|
|
75
|
+
return (React.createElement(AtlantisThemeContextProvider, { theme: theme },
|
|
76
|
+
React.createElement(Chip, Object.assign({ label: "Object", icon: "request", isActive: true }, props))));
|
|
77
|
+
}
|
|
78
|
+
function expectColors(view, background, text, icon = text) {
|
|
79
|
+
expect(view.getByTestId("chipTest")).toHaveStyle({
|
|
80
|
+
backgroundColor: background,
|
|
81
|
+
});
|
|
82
|
+
expect(view.getByText("Object")).toHaveStyle({ color: text });
|
|
83
|
+
const paths = view.UNSAFE_root.findAllByType(Path);
|
|
84
|
+
expect(paths.length).toBeGreaterThan(0);
|
|
85
|
+
for (const path of paths) {
|
|
86
|
+
expect(path.props.fill).toBe(icon);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
it.each(["request", "quote", "job", "invoice"])("keeps the selected %s fill and white content when the theme changes", accent => {
|
|
90
|
+
expect.hasAssertions();
|
|
91
|
+
const props = { accent, icon: accent };
|
|
92
|
+
const view = render(example("light", props));
|
|
93
|
+
for (const theme of ["light", "dark", "light"]) {
|
|
94
|
+
view.rerender(example(theme, props));
|
|
95
|
+
expectColors(view, allColors[`color-${accent}`], allColors["color-text--reverse"]);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
it("changes the selected client fill and foreground together with the theme", () => {
|
|
99
|
+
expect.hasAssertions();
|
|
100
|
+
const props = { accent: "client", icon: "clients" };
|
|
101
|
+
const view = render(example("light", props));
|
|
102
|
+
expectColors(view, allColors["color-client"], allColors["color-text--reverse"]);
|
|
103
|
+
view.rerender(example("dark", props));
|
|
104
|
+
expectColors(view, darkTokens["color-client"], allColors["color-text"]);
|
|
105
|
+
view.rerender(example("light", props));
|
|
106
|
+
expectColors(view, allColors["color-client"], allColors["color-text--reverse"]);
|
|
107
|
+
});
|
|
108
|
+
it.each(["request", "client"])("restores the theme's normal colors when %s is deselected", accent => {
|
|
109
|
+
const view = render(example("dark", { accent }));
|
|
110
|
+
view.rerender(example("dark", { accent, isActive: false }));
|
|
111
|
+
expectColors(view, darkTokens["color-interactive--background"], darkTokens["color-text"], darkTokens[`color-${accent}`]);
|
|
112
|
+
expect(view.getByTestId("chipTest").props.accessibilityState.checked).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
it("preserves default selected colors when an accent is removed and the theme changes", () => {
|
|
115
|
+
expect.hasAssertions();
|
|
116
|
+
const view = render(example("dark", { accent: "request" }));
|
|
117
|
+
view.rerender(example("dark"));
|
|
118
|
+
expectColors(view, darkTokens["color-surface--reverse"], darkTokens["color-text--reverse"], darkTokens["color-surface"]);
|
|
119
|
+
view.rerender(example("light"));
|
|
120
|
+
expectColors(view, allColors["color-surface--reverse"], allColors["color-text--reverse"], allColors["color-surface"]);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { render, screen, within } from "@testing-library/react-native";
|
|
2
|
+
import { fireEvent, render, screen, within, } from "@testing-library/react-native";
|
|
3
|
+
import { Pressable } from "react-native";
|
|
3
4
|
import { SelectRoot } from "./SelectRoot";
|
|
4
5
|
import { SelectTrigger } from "./components/SelectTrigger";
|
|
5
6
|
import { SelectLabel } from "./components/SelectLabel";
|
|
@@ -12,6 +13,34 @@ function renderRoot(props = {}) {
|
|
|
12
13
|
React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" }))));
|
|
13
14
|
}
|
|
14
15
|
describe("SelectRoot chrome", () => {
|
|
16
|
+
it("shows the placeholder when a controlled value is cleared to undefined", () => {
|
|
17
|
+
function ControlledSelect() {
|
|
18
|
+
const [value, setValue] = React.useState();
|
|
19
|
+
return (React.createElement(React.Fragment, null,
|
|
20
|
+
React.createElement(SelectRoot, { value: value, onValueChange: setValue },
|
|
21
|
+
React.createElement(SelectTrigger, null,
|
|
22
|
+
React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" })),
|
|
23
|
+
React.createElement(SelectPrimitive.Item, { testID: "toronto-option", value: "tor", label: "Toronto" })),
|
|
24
|
+
React.createElement(Pressable, { testID: "clear-value", onPress: () => setValue(undefined) })));
|
|
25
|
+
}
|
|
26
|
+
render(React.createElement(ControlledSelect, null));
|
|
27
|
+
fireEvent.press(screen.getByTestId("toronto-option"));
|
|
28
|
+
expect(screen.getByText("Toronto")).toBeDefined();
|
|
29
|
+
fireEvent.press(screen.getByTestId("clear-value"));
|
|
30
|
+
expect(screen.queryByText("Toronto")).toBeNull();
|
|
31
|
+
expect(screen.getByText("Select an option")).toHaveStyle({
|
|
32
|
+
color: tokens["color-text--secondary"],
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
it("preserves an uncontrolled default value across rerenders", () => {
|
|
36
|
+
const { rerender } = renderRoot({
|
|
37
|
+
defaultValue: { value: "tor", label: "Toronto" },
|
|
38
|
+
});
|
|
39
|
+
rerender(React.createElement(SelectRoot, { defaultValue: { value: "tor", label: "Toronto" } },
|
|
40
|
+
React.createElement(SelectTrigger, null,
|
|
41
|
+
React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" }))));
|
|
42
|
+
expect(screen.getByText("Toronto")).toBeDefined();
|
|
43
|
+
});
|
|
15
44
|
it("renders a composed label (visible, hidden from assistive tech)", () => {
|
|
16
45
|
render(React.createElement(SelectRoot, null,
|
|
17
46
|
React.createElement(SelectLabel, null, "City"),
|
|
@@ -21,9 +21,16 @@ export const SELECT_TRIGGER_DEFAULT_TEST_ID = "ATL-Select-Trigger";
|
|
|
21
21
|
function useIsDarkTheme() {
|
|
22
22
|
return useAtlantisTheme().effectiveTheme === "dark";
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// @rn-primitives/select treats `undefined` as uncontrolled. Keep an explicitly
|
|
25
|
+
// controlled empty value defined so clearing the prop cannot reveal stale
|
|
26
|
+
// internal state. Its undefined fields cannot match a public string item value.
|
|
27
|
+
const EMPTY_CONTROLLED_VALUE = {};
|
|
28
|
+
function SelectPrimitiveRoot(props) {
|
|
29
|
+
const isControlled = Object.prototype.hasOwnProperty.call(props, "value");
|
|
30
|
+
const value = isControlled && props.value === undefined
|
|
31
|
+
? EMPTY_CONTROLLED_VALUE
|
|
32
|
+
: props.value;
|
|
33
|
+
return React.createElement(Primitive.Root, Object.assign({}, props, { value: value }));
|
|
27
34
|
}
|
|
28
35
|
/**
|
|
29
36
|
* Styled trigger field. Renders only its `children` — compose
|
|
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
};
|
|
10
10
|
import React from "react";
|
|
11
11
|
import { render, screen, userEvent, within, } from "@testing-library/react-native";
|
|
12
|
+
import { Pressable } from "react-native";
|
|
12
13
|
import { darkTokens } from "@jobber/design";
|
|
13
14
|
import { PortalHost } from "@rn-primitives/portal";
|
|
14
15
|
import { SelectPrimitive } from "./SelectPrimitive";
|
|
@@ -30,6 +31,35 @@ function renderSelect({ defaultValue, onOpenChange, disabled, invalid, testID, t
|
|
|
30
31
|
React.createElement(SelectPrimitive.Trigger, { ref: triggerRef, testID: testID, invalid: invalid },
|
|
31
32
|
React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" }))));
|
|
32
33
|
}
|
|
34
|
+
describe("SelectPrimitive.Root", () => {
|
|
35
|
+
it("does not select an empty-string item when a controlled value is cleared", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
36
|
+
function ControlledSelect() {
|
|
37
|
+
const [value, setValue] = React.useState({ value: "tor", label: "Toronto" });
|
|
38
|
+
return (React.createElement(React.Fragment, null,
|
|
39
|
+
React.createElement(SelectPrimitive.Root, { value: value, onValueChange: setValue },
|
|
40
|
+
React.createElement(SelectPrimitive.Trigger, null,
|
|
41
|
+
React.createElement(SelectPrimitive.Value, { placeholder: "Select an option" })),
|
|
42
|
+
React.createElement(SelectPrimitive.Portal, null,
|
|
43
|
+
React.createElement(SelectPrimitive.Overlay, null),
|
|
44
|
+
React.createElement(SelectPrimitive.Content, null,
|
|
45
|
+
React.createElement(SelectPrimitive.Viewport, null,
|
|
46
|
+
React.createElement(SelectPrimitive.Item, { testID: "empty-option", value: "", label: "Empty" },
|
|
47
|
+
React.createElement(SelectPrimitive.ItemText, null),
|
|
48
|
+
React.createElement(SelectPrimitive.ItemIndicator, null,
|
|
49
|
+
React.createElement(Icon, { name: "checkmark" }))),
|
|
50
|
+
React.createElement(SelectPrimitive.Item, { value: "tor", label: "Toronto" },
|
|
51
|
+
React.createElement(SelectPrimitive.ItemText, null),
|
|
52
|
+
React.createElement(SelectPrimitive.ItemIndicator, null,
|
|
53
|
+
React.createElement(Icon, { name: "checkmark" }))))))),
|
|
54
|
+
React.createElement(Pressable, { testID: "clear-value", onPress: () => setValue(undefined) })));
|
|
55
|
+
}
|
|
56
|
+
render(React.createElement(ControlledSelect, null), { wrapper: PortalHostWrapper });
|
|
57
|
+
yield user.press(screen.getByTestId("clear-value"));
|
|
58
|
+
yield openSelect();
|
|
59
|
+
expect(screen.getByTestId("empty-option").props.accessibilityState).toMatchObject({ checked: false });
|
|
60
|
+
expect(screen.queryByTestId("checkmark")).toBeNull();
|
|
61
|
+
}));
|
|
62
|
+
});
|
|
33
63
|
describe("SelectPrimitive.Trigger", () => {
|
|
34
64
|
it("renders with the default testID", () => {
|
|
35
65
|
renderSelect();
|