@utilitywarehouse/hearth-react-native 0.16.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +14 -14
- package/CHANGELOG.md +132 -0
- package/build/components/Card/CardAction/CardActionRoot.js +12 -2
- package/build/components/Card/CardActions.context.d.ts +6 -0
- package/build/components/Card/CardActions.context.js +5 -0
- package/build/components/Card/CardActions.d.ts +7 -0
- package/build/components/Card/CardActions.js +29 -0
- package/build/components/Card/CardRoot.js +16 -104
- package/build/components/Card/helpers.d.ts +8 -0
- package/build/components/Card/helpers.js +146 -0
- package/build/components/Card/index.d.ts +2 -0
- package/build/components/Card/index.js +2 -0
- package/build/components/ExpandableCard/ExpandableCardGroup.d.ts +1 -1
- package/build/components/ExpandableCard/ExpandableCardGroup.js +2 -2
- package/build/components/ExpandableCard/ExpandableCardGroup.props.d.ts +4 -0
- package/build/components/Input/Input.js +4 -3
- package/build/components/Input/Input.props.d.ts +9 -0
- package/build/components/List/List.context.d.ts +4 -2
- package/build/components/List/List.context.js +0 -2
- package/build/components/List/List.d.ts +1 -1
- package/build/components/List/List.js +25 -38
- package/build/components/List/List.props.d.ts +1 -0
- package/build/components/List/ListAction/ListAction.js +24 -7
- package/build/components/List/ListAction/ListAction.props.d.ts +1 -0
- package/build/components/List/ListItem/ListItemRoot.js +12 -4
- package/build/utils/isThemedImageProps.d.ts +1 -1
- package/package.json +3 -3
- package/src/components/Card/Card.docs.mdx +224 -66
- package/src/components/Card/Card.stories.tsx +29 -25
- package/src/components/Card/CardAction/CardAction.stories.tsx +239 -93
- package/src/components/Card/CardAction/CardActionRoot.tsx +15 -2
- package/src/components/Card/CardActions.context.ts +12 -0
- package/src/components/Card/CardActions.tsx +40 -0
- package/src/components/Card/CardRoot.tsx +27 -132
- package/src/components/Card/helpers.tsx +195 -0
- package/src/components/Card/index.ts +2 -0
- package/src/components/ExpandableCard/ExpandableCard.figma.tsx +33 -38
- package/src/components/ExpandableCard/ExpandableCardGroup.figma.tsx +34 -17
- package/src/components/ExpandableCard/ExpandableCardGroup.props.ts +5 -0
- package/src/components/ExpandableCard/ExpandableCardGroup.tsx +2 -0
- package/src/components/HighlightBanner/HighlightBanner.figma.tsx +46 -0
- package/src/components/IconButton/IconButton.figma.tsx +20 -30
- package/src/components/IconContainer/IconContainer.figma.tsx +7 -13
- package/src/components/IndicatorIconButton/IndicatorIconButton.figma.tsx +16 -0
- package/src/components/Input/Input.docs.mdx +55 -15
- package/src/components/Input/Input.figma.tsx +106 -40
- package/src/components/Input/Input.props.ts +9 -0
- package/src/components/Input/Input.tsx +21 -0
- package/src/components/Link/Link.figma.tsx +31 -38
- package/src/components/List/List.context.ts +2 -4
- package/src/components/List/List.docs.mdx +10 -5
- package/src/components/List/List.figma.tsx +42 -28
- package/src/components/List/List.props.ts +1 -0
- package/src/components/List/List.stories.tsx +43 -0
- package/src/components/List/List.tsx +38 -51
- package/src/components/List/ListAction/ListAction.figma.tsx +5 -13
- package/src/components/List/ListAction/ListAction.props.ts +1 -0
- package/src/components/List/ListAction/ListAction.tsx +40 -10
- package/src/components/List/ListItem/ListItem.figma.tsx +43 -27
- package/src/components/List/ListItem/ListItemRoot.tsx +15 -4
- package/src/utils/isThemedImageProps.ts +1 -1
- package/src/components/InlineLink/InlineLink.figma.tsx +0 -33
package/.turbo/turbo-build.log
CHANGED
package/.turbo/turbo-lint.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @utilitywarehouse/hearth-react-native@0.
|
|
2
|
+
> @utilitywarehouse/hearth-react-native@0.17.0 lint /home/runner/work/hearth/hearth/packages/react-native
|
|
3
3
|
> TIMING=1 eslint .
|
|
4
4
|
|
|
5
5
|
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
52:6 warning React Hook useCallback has a missing dependency: 'containerHeight'. Either include it or remove the dependency array. Outer scope values like 'styles' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
|
|
29
29
|
|
|
30
30
|
/home/runner/work/hearth/hearth/packages/react-native/src/components/Input/Input.tsx
|
|
31
|
-
|
|
31
|
+
78:8 warning React Hook useEffect has a missing dependency: 'formFieldContext'. Either include it or remove the dependency array react-hooks/exhaustive-deps
|
|
32
32
|
|
|
33
33
|
/home/runner/work/hearth/hearth/packages/react-native/src/components/Modal/Modal.tsx
|
|
34
34
|
72:6 warning React Hook useCallback has an unnecessary dependency: 'Platform.OS'. Either exclude it or remove the dependency array. Outer scope values like 'Platform.OS' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
|
|
@@ -55,15 +55,15 @@
|
|
|
55
55
|
|
|
56
56
|
✖ 24 problems (0 errors, 24 warnings)
|
|
57
57
|
|
|
58
|
-
Rule
|
|
59
|
-
|
|
60
|
-
@typescript-eslint/no-unused-vars
|
|
61
|
-
react-hooks/rules-of-hooks
|
|
62
|
-
react-hooks/exhaustive-deps
|
|
63
|
-
no-global-assign
|
|
64
|
-
no-loss-of-precision
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
no-
|
|
68
|
-
|
|
69
|
-
|
|
58
|
+
Rule | Time (ms) | Relative
|
|
59
|
+
:-------------------------------------------------|----------:|--------:
|
|
60
|
+
@typescript-eslint/no-unused-vars | 1454.377 | 60.3%
|
|
61
|
+
react-hooks/rules-of-hooks | 109.992 | 4.6%
|
|
62
|
+
react-hooks/exhaustive-deps | 91.815 | 3.8%
|
|
63
|
+
no-global-assign | 66.528 | 2.8%
|
|
64
|
+
no-loss-of-precision | 57.391 | 2.4%
|
|
65
|
+
@typescript-eslint/ban-ts-comment | 54.505 | 2.3%
|
|
66
|
+
no-misleading-character-class | 45.527 | 1.9%
|
|
67
|
+
no-unexpected-multiline | 39.063 | 1.6%
|
|
68
|
+
no-regex-spaces | 29.197 | 1.2%
|
|
69
|
+
@typescript-eslint/no-unnecessary-type-constraint | 28.583 | 1.2%
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,137 @@
|
|
|
1
1
|
# @utilitywarehouse/hearth-react-native
|
|
2
2
|
|
|
3
|
+
## 0.17.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#867](https://github.com/utilitywarehouse/hearth/pull/867) [`9a15eb8`](https://github.com/utilitywarehouse/hearth/commit/9a15eb8c659aa541988da6f28f6c50261f3557f9) Thanks [@jordmccord](https://github.com/jordmccord)! - 💔 [BREAKING CHANGE]: Require `CardActions` wrapper for `CardAction` groups.
|
|
8
|
+
|
|
9
|
+
`Card` now only treats actions as such when they are wrapped in `CardActions`. This removes wrapper heuristics and makes action grouping explicit while keeping automatic content wrapping.
|
|
10
|
+
|
|
11
|
+
**Components affected**:
|
|
12
|
+
- `Card`
|
|
13
|
+
- `CardActions`
|
|
14
|
+
- `CardAction`
|
|
15
|
+
|
|
16
|
+
**Developer changes**:
|
|
17
|
+
|
|
18
|
+
Wrap all `CardAction` items in `CardActions`:
|
|
19
|
+
|
|
20
|
+
```diff
|
|
21
|
+
- <Card>
|
|
22
|
+
- <CardAction heading="Action 1" onPress={() => {}} />
|
|
23
|
+
- <CardAction heading="Action 2" onPress={() => {}} />
|
|
24
|
+
- </Card>
|
|
25
|
+
+ <Card>
|
|
26
|
+
+ <CardActions>
|
|
27
|
+
+ <CardAction heading="Action 1" onPress={() => {}} />
|
|
28
|
+
+ <CardAction heading="Action 2" onPress={() => {}} />
|
|
29
|
+
+ </CardActions>
|
|
30
|
+
+ </Card>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- [#860](https://github.com/utilitywarehouse/hearth/pull/860) [`ec44a9d`](https://github.com/utilitywarehouse/hearth/commit/ec44a9d3d7a2d95ab69b6e4c461104402d82659d) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `prefix` and `suffix` props to `Input` component
|
|
34
|
+
|
|
35
|
+
The `Input` component now supports `prefix` and `suffix` props, allowing you to display text or custom content before and after the input field. This is useful for adding units, currency symbols, or other contextual information.
|
|
36
|
+
|
|
37
|
+
**Components affected**:
|
|
38
|
+
- `Input`
|
|
39
|
+
|
|
40
|
+
**Developer changes**:
|
|
41
|
+
|
|
42
|
+
Use the `prefix` and `suffix` props to add content before or after the input:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
<Input label="Amount" prefix="£" suffix="GBP" placeholder="0.00" />
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You can also pass custom React nodes:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
<Input label="Email" prefix={<CustomIcon />} suffix={<BodyText>@example.com</BodyText>} />
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Note**: The `prefix` and `suffix` props are not available on `password` and `search` input types, as these have specific UI patterns.
|
|
55
|
+
|
|
56
|
+
- [#862](https://github.com/utilitywarehouse/hearth/pull/862) [`654552e`](https://github.com/utilitywarehouse/hearth/commit/654552e33d56e4b2b2ba8fb783b7f9a7c57ba212) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `loading` prop to `ListAction` component
|
|
57
|
+
|
|
58
|
+
The `ListAction` component now supports a `loading` prop that displays a skeleton loading state while content is being fetched. This provides better user feedback during asynchronous operations.
|
|
59
|
+
|
|
60
|
+
**Components affected**:
|
|
61
|
+
- `ListAction`
|
|
62
|
+
|
|
63
|
+
**Developer changes**:
|
|
64
|
+
|
|
65
|
+
Use the `loading` prop to show a loading state:
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
<List>
|
|
69
|
+
<ListItem heading="Account details" />
|
|
70
|
+
<ListAction heading="View transactions" loading={isLoading} onPress={handlePress} />
|
|
71
|
+
</List>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
When `loading` is true, the action will display skeleton placeholders instead of the heading and icon.
|
|
75
|
+
|
|
76
|
+
- [#862](https://github.com/utilitywarehouse/hearth/pull/862) [`654552e`](https://github.com/utilitywarehouse/hearth/commit/654552e33d56e4b2b2ba8fb783b7f9a7c57ba212) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `invalidText` prop to `List` component
|
|
77
|
+
|
|
78
|
+
The `List` component now supports an `invalidText` prop for displaying validation error messages in the section header. This provides consistent validation feedback across list-based forms and grouped content.
|
|
79
|
+
|
|
80
|
+
**Components affected**:
|
|
81
|
+
- `List`
|
|
82
|
+
|
|
83
|
+
**Developer changes**:
|
|
84
|
+
|
|
85
|
+
Use the `invalidText` prop to display validation errors:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
<List
|
|
89
|
+
heading="Payment methods"
|
|
90
|
+
helperText="Select at least one payment method"
|
|
91
|
+
invalidText="You must select a payment method"
|
|
92
|
+
>
|
|
93
|
+
<ListItem heading="Credit card" />
|
|
94
|
+
<ListItem heading="Direct debit" />
|
|
95
|
+
</List>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Patch Changes
|
|
99
|
+
|
|
100
|
+
- [#845](https://github.com/utilitywarehouse/hearth/pull/845) [`9c034f9`](https://github.com/utilitywarehouse/hearth/commit/9c034f98f9d6aa4596a45296d02e01703ef1c762) Thanks [@jordmccord](https://github.com/jordmccord)! - 💅 [ENHANCEMENT]: Add `invalidText` prop to `ExpandableCardGroup`
|
|
101
|
+
|
|
102
|
+
The `ExpandableCardGroup` component now supports an `invalidText` prop that displays validation text below the helper text when the group is in an invalid state.
|
|
103
|
+
|
|
104
|
+
**Components affected**:
|
|
105
|
+
- `ExpandableCardGroup`
|
|
106
|
+
|
|
107
|
+
**Developer changes**:
|
|
108
|
+
|
|
109
|
+
No changes required. If you want to display validation text, you can now use the `invalidText` prop:
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
<ExpandableCardGroup
|
|
113
|
+
heading="Select an option"
|
|
114
|
+
helperText="Choose one of the options below"
|
|
115
|
+
invalidText="Please select at least one option"
|
|
116
|
+
>
|
|
117
|
+
{/* ExpandableCard components */}
|
|
118
|
+
</ExpandableCardGroup>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- [#867](https://github.com/utilitywarehouse/hearth/pull/867) [`9a15eb8`](https://github.com/utilitywarehouse/hearth/commit/9a15eb8c659aa541988da6f28f6c50261f3557f9) Thanks [@jordmccord](https://github.com/jordmccord)! - 💅 [ENHANCEMENT]: Improve first-item border detection in `Card` and `List` components
|
|
122
|
+
|
|
123
|
+
The `Card` and `List` components now use a more reliable method to detect and style the first rendered `CardAction`, `ListItem`, or `ListAction`. This fixes edge cases where wrapper components that conditionally render `null` would previously interfere with first-item border removal.
|
|
124
|
+
|
|
125
|
+
**Components affected**:
|
|
126
|
+
- `Card` / `CardAction`
|
|
127
|
+
- `List` / `ListItem` / `ListAction`
|
|
128
|
+
|
|
129
|
+
**Developer changes**:
|
|
130
|
+
|
|
131
|
+
No changes required. The improvement is automatic and maintains the same visual behavior. Components that wrap card actions or list items will now work correctly even when some wrappers return `null` conditionally.
|
|
132
|
+
|
|
133
|
+
**Note**: The `useCardFirstActionContext` hook has been removed as it was an internal implementation detail.
|
|
134
|
+
|
|
3
135
|
## 0.16.2
|
|
4
136
|
|
|
5
137
|
### Patch Changes
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { ChevronRightSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
3
|
-
import { useMemo } from 'react';
|
|
3
|
+
import { useId, useLayoutEffect, useMemo } from 'react';
|
|
4
4
|
import { Pressable, View } from 'react-native';
|
|
5
5
|
import { StyleSheet } from 'react-native-unistyles';
|
|
6
6
|
import { IconContainer } from '../../IconContainer';
|
|
7
7
|
import { Skeleton } from '../../Skeleton';
|
|
8
8
|
import { useCardContext } from '../Card.context';
|
|
9
|
+
import { useCardActionsContext } from '../CardActions.context';
|
|
9
10
|
import { CardActionContext } from './CardAction.context';
|
|
10
11
|
import CardActionContent from './CardActionContent';
|
|
11
12
|
import CardActionHelperText from './CardActionHelperText';
|
|
@@ -23,7 +24,16 @@ const CardActionRoot = ({ heading, helperText, leadingContent, trailingContent,
|
|
|
23
24
|
const testID = props.testID || 'card-action';
|
|
24
25
|
const loadingTestID = isLoading ? `${testID}-loading` : testID;
|
|
25
26
|
const { variant, hasOnlyActions } = useCardContext();
|
|
26
|
-
const
|
|
27
|
+
const actionId = useId();
|
|
28
|
+
const actionsContext = useCardActionsContext();
|
|
29
|
+
useLayoutEffect(() => {
|
|
30
|
+
if (!actionsContext) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
return actionsContext.registerAction(actionId);
|
|
34
|
+
}, [actionId, actionsContext]);
|
|
35
|
+
const isFirstFromContext = actionsContext?.firstActionId === actionId;
|
|
36
|
+
const isFirst = props.isFirst ?? isFirstFromContext;
|
|
27
37
|
styles.useVariants({
|
|
28
38
|
showPressed,
|
|
29
39
|
active,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface CardActionsContextValue {
|
|
2
|
+
firstActionId?: string;
|
|
3
|
+
registerAction: (id: string) => () => void;
|
|
4
|
+
}
|
|
5
|
+
export declare const CardActionsContext: import("react").Context<CardActionsContextValue | null>;
|
|
6
|
+
export declare const useCardActionsContext: () => CardActionsContextValue | null;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PropsWithChildren } from 'react';
|
|
2
|
+
import { ViewProps } from 'react-native';
|
|
3
|
+
declare const CardActions: {
|
|
4
|
+
({ children, style, ...props }: PropsWithChildren<ViewProps>): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
displayName: string;
|
|
6
|
+
};
|
|
7
|
+
export default CardActions;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useRef, useState } from 'react';
|
|
3
|
+
import { View } from 'react-native';
|
|
4
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
5
|
+
import { CardActionsContext } from './CardActions.context';
|
|
6
|
+
const CardActions = ({ children, style, ...props }) => {
|
|
7
|
+
const orderRef = useRef([]);
|
|
8
|
+
const [firstActionId, setFirstActionId] = useState(undefined);
|
|
9
|
+
const registerAction = useCallback((id) => {
|
|
10
|
+
if (!orderRef.current.includes(id)) {
|
|
11
|
+
orderRef.current.push(id);
|
|
12
|
+
}
|
|
13
|
+
const nextFirst = orderRef.current[0];
|
|
14
|
+
setFirstActionId(prev => (prev === nextFirst ? prev : nextFirst));
|
|
15
|
+
return () => {
|
|
16
|
+
orderRef.current = orderRef.current.filter(currentId => currentId !== id);
|
|
17
|
+
const nextFirst = orderRef.current[0];
|
|
18
|
+
setFirstActionId(prev => (prev === nextFirst ? prev : nextFirst));
|
|
19
|
+
};
|
|
20
|
+
}, []);
|
|
21
|
+
return (_jsx(CardActionsContext.Provider, { value: { firstActionId, registerAction }, children: _jsx(View, { ...props, style: [styles.container, style], children: children }) }));
|
|
22
|
+
};
|
|
23
|
+
CardActions.displayName = 'CardActions';
|
|
24
|
+
const styles = StyleSheet.create({
|
|
25
|
+
container: {
|
|
26
|
+
width: '100%',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
export default CardActions;
|
|
@@ -1,93 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
3
|
import { Pressable } from 'react-native';
|
|
4
4
|
import { StyleSheet } from 'react-native-unistyles';
|
|
5
5
|
import { useStyleProps } from '../../hooks';
|
|
6
6
|
import { CardContext } from './Card.context';
|
|
7
|
+
import CardActions from './CardActions';
|
|
7
8
|
import CardContent from './CardContent';
|
|
8
|
-
|
|
9
|
-
const checkForComponentType = (children, displayName) => {
|
|
10
|
-
return React.Children.toArray(children).some(child => {
|
|
11
|
-
if (React.isValidElement(child)) {
|
|
12
|
-
// @ts-expect-error - type
|
|
13
|
-
if (child.type.displayName === displayName) {
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
const childProps = child.props;
|
|
17
|
-
if (childProps.children) {
|
|
18
|
-
return checkForComponentType(childProps.children, displayName);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return false;
|
|
22
|
-
});
|
|
23
|
-
};
|
|
24
|
-
// Helper to filter out specific component types from children
|
|
25
|
-
const filterChildren = (children, excludeDisplayName) => {
|
|
26
|
-
return React.Children.map(children, child => {
|
|
27
|
-
if (React.isValidElement(child)) {
|
|
28
|
-
// @ts-expect-error - type
|
|
29
|
-
if (child.type.displayName === excludeDisplayName) {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
const childProps = child.props;
|
|
33
|
-
if (childProps.children) {
|
|
34
|
-
return React.cloneElement(child, {
|
|
35
|
-
...childProps,
|
|
36
|
-
children: filterChildren(childProps.children, excludeDisplayName),
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return child;
|
|
41
|
-
});
|
|
42
|
-
};
|
|
43
|
-
// Helper to extract specific component types from children
|
|
44
|
-
const extractChildren = (children, includeDisplayName, markFirst = false) => {
|
|
45
|
-
let isFirstFound = false;
|
|
46
|
-
return React.Children.map(children, child => {
|
|
47
|
-
if (React.isValidElement(child)) {
|
|
48
|
-
// @ts-expect-error - type
|
|
49
|
-
if (child.type.displayName === includeDisplayName) {
|
|
50
|
-
const isFirst = markFirst && !isFirstFound;
|
|
51
|
-
if (isFirst) {
|
|
52
|
-
isFirstFound = true;
|
|
53
|
-
}
|
|
54
|
-
return markFirst
|
|
55
|
-
? React.cloneElement(child, { ...(child.props || {}), isFirst })
|
|
56
|
-
: child;
|
|
57
|
-
}
|
|
58
|
-
const childProps = child.props;
|
|
59
|
-
if (childProps.children) {
|
|
60
|
-
return extractChildren(childProps.children, includeDisplayName, markFirst);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return null;
|
|
64
|
-
});
|
|
65
|
-
};
|
|
66
|
-
// Helper that recursively collects onPress or other defined handlers from descendants
|
|
67
|
-
const collectChildActionHandlers = (children) => React.Children.toArray(children).reduce((handlers, child) => {
|
|
68
|
-
if (React.isValidElement(child)) {
|
|
69
|
-
const childProps = child.props;
|
|
70
|
-
// @ts-expect-error - type
|
|
71
|
-
if (child.type.displayName === 'CardPressHandler') {
|
|
72
|
-
const actionChildren = React.Children.toArray(childProps.children);
|
|
73
|
-
const handlerToInherit = childProps['handlerToInherit'] || 'onPress';
|
|
74
|
-
const firstChild = actionChildren[0];
|
|
75
|
-
if (React.isValidElement(firstChild) &&
|
|
76
|
-
typeof firstChild.props[handlerToInherit] === 'function') {
|
|
77
|
-
handlers.push(firstChild.props[handlerToInherit]);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (childProps.children) {
|
|
81
|
-
handlers.push(...collectChildActionHandlers(childProps.children));
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return handlers;
|
|
85
|
-
}, []);
|
|
9
|
+
import { checkForComponentType, collectChildActionHandlers, extractCardActions, filterChildren, hasOnlyPotentialActions, } from './helpers';
|
|
86
10
|
const Card = ({ children, variant = 'subtle', colorScheme = 'neutralStrong', shadowColor, noPadding = false, style, states, space, disabled = false, onPress, ...rest }) => {
|
|
87
11
|
const { active } = states || { active: false };
|
|
88
12
|
const childActionHandlers = collectChildActionHandlers(children);
|
|
89
|
-
const hasActions = checkForComponentType(children,
|
|
90
|
-
const hasContent = checkForComponentType(children,
|
|
13
|
+
const hasActions = checkForComponentType(children, CardActions);
|
|
14
|
+
const hasContent = checkForComponentType(children, CardContent);
|
|
91
15
|
// Extract style props using our custom hook
|
|
92
16
|
const { computedStyles, remainingProps } = useStyleProps(rest);
|
|
93
17
|
const handlePress = (e) => {
|
|
@@ -98,14 +22,9 @@ const Card = ({ children, variant = 'subtle', colorScheme = 'neutralStrong', sha
|
|
|
98
22
|
};
|
|
99
23
|
const inheritChildAction = childActionHandlers.length > 0;
|
|
100
24
|
const showPressed = inheritChildAction || !!onPress;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const hasOnlyActions =
|
|
104
|
-
!hasContent &&
|
|
105
|
-
React.Children.toArray(filteredChildren).filter(child => child != null).length === 0;
|
|
106
|
-
const filteredCardActions = !hasContent && hasActions
|
|
107
|
-
? extractChildren(children, 'CardAction', hasOnlyActions)
|
|
108
|
-
: null;
|
|
25
|
+
// Check if all children are action groups (CardActions)
|
|
26
|
+
const potentiallyOnlyActions = hasOnlyPotentialActions(children, CardActions);
|
|
27
|
+
const hasOnlyActions = potentiallyOnlyActions && !hasContent;
|
|
109
28
|
const context = useMemo(() => ({
|
|
110
29
|
pressed: showPressed && active,
|
|
111
30
|
noPadding,
|
|
@@ -118,29 +37,22 @@ const Card = ({ children, variant = 'subtle', colorScheme = 'neutralStrong', sha
|
|
|
118
37
|
styles.useVariants({
|
|
119
38
|
variant,
|
|
120
39
|
colorScheme,
|
|
121
|
-
noPadding: noPadding || hasActions || hasContent,
|
|
40
|
+
noPadding: noPadding || hasActions || hasContent || hasOnlyActions,
|
|
122
41
|
active,
|
|
123
42
|
showPressed,
|
|
124
43
|
disabled,
|
|
125
|
-
space: hasActions || hasContent ? 'none' : space,
|
|
44
|
+
space: hasActions || hasContent || hasOnlyActions ? 'none' : space,
|
|
126
45
|
shadowColor,
|
|
127
46
|
});
|
|
128
47
|
const renderChildren = () => {
|
|
129
|
-
//
|
|
130
|
-
if (hasContent || !hasActions) {
|
|
48
|
+
// Explicit CardContent used - render as-is or Card has only actions (or potential action wrappers) - render children directly
|
|
49
|
+
if (hasContent || hasOnlyActions || !hasActions) {
|
|
131
50
|
return children;
|
|
132
51
|
}
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
if (filteredChildren) {
|
|
139
|
-
// Has both actions and other content - wrap content and render actions below
|
|
140
|
-
return (_jsxs(_Fragment, { children: [_jsx(CardContent, { children: filteredChildren }), filteredCardActions] }));
|
|
141
|
-
}
|
|
142
|
-
// Fallback
|
|
143
|
-
return children;
|
|
52
|
+
// Has both actions and other content - wrap non-action content and render actions separately
|
|
53
|
+
const filteredNonActionChildren = filterChildren(children, CardActions);
|
|
54
|
+
const cardActions = extractCardActions(children, CardActions);
|
|
55
|
+
return (_jsxs(_Fragment, { children: [_jsx(CardContent, { children: filteredNonActionChildren }), cardActions] }));
|
|
144
56
|
};
|
|
145
57
|
return (_jsx(CardContext.Provider, { value: context, children: _jsx(Pressable, { ...remainingProps, disabled: disabled, style: [styles.card, computedStyles, style], onPress: handlePress, accessible: showPressed, importantForAccessibility: showPressed ? 'yes' : 'no', children: renderChildren() }) }));
|
|
146
58
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { GestureResponderEvent } from 'react-native';
|
|
3
|
+
export declare const checkForComponentType: (children: React.ReactNode, componentType: React.ComponentType<any>) => boolean;
|
|
4
|
+
export declare const hasContentInChildren: (child: React.ReactNode, actionType: React.ComponentType<any>) => boolean;
|
|
5
|
+
export declare const hasOnlyPotentialActions: (children: React.ReactNode, actionType: React.ComponentType<any>) => boolean;
|
|
6
|
+
export declare const filterChildren: (children: React.ReactNode, excludeComponentType: React.ComponentType<any>) => React.ReactNode;
|
|
7
|
+
export declare const extractCardActions: (children: React.ReactNode, actionType: React.ComponentType<any>) => React.ReactNode;
|
|
8
|
+
export declare const collectChildActionHandlers: (children: React.ReactNode) => Array<(e: GestureResponderEvent) => void>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
const getInheritableHandler = (child, handlerToInherit) => {
|
|
3
|
+
if (!React.isValidElement(child)) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
const childProps = child.props;
|
|
7
|
+
const isDisabled = !!childProps.disabled || !!childProps.loading;
|
|
8
|
+
if (isDisabled || typeof childProps[handlerToInherit] !== 'function') {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return childProps[handlerToInherit];
|
|
12
|
+
};
|
|
13
|
+
// Helper to check if children contain specific component types
|
|
14
|
+
export const checkForComponentType = (children, componentType) => {
|
|
15
|
+
return React.Children.toArray(children).some(child => {
|
|
16
|
+
if (React.isValidElement(child)) {
|
|
17
|
+
if (child.type === componentType) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
const childProps = child.props;
|
|
21
|
+
if (childProps.children) {
|
|
22
|
+
return checkForComponentType(childProps.children, componentType);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
// Check if a component has "content" in its children (not just actions)
|
|
29
|
+
export const hasContentInChildren = (child, actionType) => {
|
|
30
|
+
if (!React.isValidElement(child)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const childProps = child.props;
|
|
34
|
+
if (!childProps?.children) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
// Check if children contain anything other than the action type
|
|
38
|
+
const childrenArray = React.Children.toArray(childProps.children);
|
|
39
|
+
return childrenArray.some(c => {
|
|
40
|
+
if (!React.isValidElement(c)) {
|
|
41
|
+
// Text, numbers, etc - this is content
|
|
42
|
+
return c != null;
|
|
43
|
+
}
|
|
44
|
+
return c.type !== actionType;
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
// Check if all children are CardActions groups
|
|
48
|
+
export const hasOnlyPotentialActions = (children, actionType) => {
|
|
49
|
+
const childArray = React.Children.toArray(children);
|
|
50
|
+
if (childArray.length === 0) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
let hasActionCandidate = false;
|
|
54
|
+
for (const child of childArray) {
|
|
55
|
+
if (!React.isValidElement(child)) {
|
|
56
|
+
if (child != null) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (child.type === actionType) {
|
|
62
|
+
hasActionCandidate = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (checkForComponentType(child, actionType)) {
|
|
66
|
+
hasActionCandidate = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (typeof child.type === 'string') {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (hasContentInChildren(child, actionType)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return hasActionCandidate || checkForComponentType(children, actionType);
|
|
78
|
+
};
|
|
79
|
+
// Helper to filter out specific component types from children
|
|
80
|
+
export const filterChildren = (children, excludeComponentType) => {
|
|
81
|
+
return React.Children.map(children, child => {
|
|
82
|
+
if (React.isValidElement(child)) {
|
|
83
|
+
if (child.type === excludeComponentType) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
// Check if this child contains the excludeComponentType
|
|
87
|
+
if (checkForComponentType(child, excludeComponentType)) {
|
|
88
|
+
return null; // This child or its descendants contain the action
|
|
89
|
+
}
|
|
90
|
+
const childProps = child.props;
|
|
91
|
+
if (childProps.children) {
|
|
92
|
+
const filteredChildren = filterChildren(childProps.children, excludeComponentType);
|
|
93
|
+
// Only preserve wrapper if it has non-null children
|
|
94
|
+
const hasContent = React.Children.toArray(filteredChildren).some(c => c != null);
|
|
95
|
+
if (!hasContent) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return React.cloneElement(child, {
|
|
99
|
+
...childProps,
|
|
100
|
+
children: filteredChildren,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return child;
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
// Helper to extract only CardActions (preserving wrapper components)
|
|
108
|
+
export const extractCardActions = (children, actionType) => {
|
|
109
|
+
const recursiveExtract = (children) => {
|
|
110
|
+
return React.Children.map(children, child => {
|
|
111
|
+
if (!React.isValidElement(child))
|
|
112
|
+
return null;
|
|
113
|
+
// Direct action
|
|
114
|
+
if (child.type === actionType) {
|
|
115
|
+
return child;
|
|
116
|
+
}
|
|
117
|
+
// If this child contains a CardActions wrapper in its tree, keep it
|
|
118
|
+
if (checkForComponentType(child, actionType) && !hasContentInChildren(child, actionType)) {
|
|
119
|
+
return child;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
return recursiveExtract(children);
|
|
125
|
+
};
|
|
126
|
+
// Helper that recursively collects onPress or other defined handlers from descendants
|
|
127
|
+
export const collectChildActionHandlers = (children) => React.Children.toArray(children).reduce((handlers, child) => {
|
|
128
|
+
if (React.isValidElement(child)) {
|
|
129
|
+
const childProps = child.props;
|
|
130
|
+
// Check using displayName as CardPressHandler might not be directly importable
|
|
131
|
+
// @ts-expect-error - type
|
|
132
|
+
if (child.type?.displayName === 'CardPressHandler') {
|
|
133
|
+
const actionChildren = React.Children.toArray(childProps.children);
|
|
134
|
+
const handlerToInherit = childProps['handlerToInherit'] || 'onPress';
|
|
135
|
+
const firstChild = actionChildren[0];
|
|
136
|
+
const handler = getInheritableHandler(firstChild, handlerToInherit);
|
|
137
|
+
if (handler) {
|
|
138
|
+
handlers.push(handler);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (childProps.children) {
|
|
142
|
+
handlers.push(...collectChildActionHandlers(childProps.children));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return handlers;
|
|
146
|
+
}, []);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { default as Card } from './Card';
|
|
2
2
|
export { useCardContext } from './Card.context';
|
|
3
3
|
export * from './CardAction';
|
|
4
|
+
export { default as CardActions } from './CardActions';
|
|
5
|
+
export { default as CardContent } from './CardContent';
|
|
4
6
|
export { default as CardPressHandler } from './CardPressHandler';
|
|
5
7
|
export { useCardPressHandlerContext } from './CardPressHandler.context';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { default as Card } from './Card';
|
|
2
2
|
export { useCardContext } from './Card.context';
|
|
3
3
|
export * from './CardAction';
|
|
4
|
+
export { default as CardActions } from './CardActions';
|
|
5
|
+
export { default as CardContent } from './CardContent';
|
|
4
6
|
export { default as CardPressHandler } from './CardPressHandler';
|
|
5
7
|
export { useCardPressHandlerContext } from './CardPressHandler.context';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type ExpandableCardGroupProps from './ExpandableCardGroup.props';
|
|
2
2
|
declare const ExpandableCardGroup: {
|
|
3
|
-
({ heading, helperText, headerTrailingContent, children, style, testID, ...props }: ExpandableCardGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
({ heading, helperText, headerTrailingContent, children, style, testID, invalidText, ...props }: ExpandableCardGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
4
4
|
displayName: string;
|
|
5
5
|
};
|
|
6
6
|
export default ExpandableCardGroup;
|
|
@@ -2,8 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { View } from 'react-native';
|
|
3
3
|
import { StyleSheet } from 'react-native-unistyles';
|
|
4
4
|
import { SectionHeader } from '../SectionHeader';
|
|
5
|
-
const ExpandableCardGroup = ({ heading, helperText, headerTrailingContent, children, style, testID = 'expandable-card-group', ...props }) => {
|
|
6
|
-
return (_jsxs(View, { style: [styles.container, style], testID: testID, ...props, children: [heading ? (_jsx(SectionHeader, { heading: heading, helperText: helperText, trailingContent: headerTrailingContent })) : null, _jsx(View, { style: styles.cardsContainer, children: children })] }));
|
|
5
|
+
const ExpandableCardGroup = ({ heading, helperText, headerTrailingContent, children, style, testID = 'expandable-card-group', invalidText, ...props }) => {
|
|
6
|
+
return (_jsxs(View, { style: [styles.container, style], testID: testID, ...props, children: [heading ? (_jsx(SectionHeader, { heading: heading, helperText: helperText, trailingContent: headerTrailingContent, invalidText: invalidText })) : null, _jsx(View, { style: styles.cardsContainer, children: children })] }));
|
|
7
7
|
};
|
|
8
8
|
ExpandableCardGroup.displayName = 'ExpandableCardGroup';
|
|
9
9
|
const styles = StyleSheet.create(theme => ({
|
|
@@ -21,5 +21,9 @@ export interface ExpandableCardGroupProps extends ViewProps {
|
|
|
21
21
|
* Test ID for testing
|
|
22
22
|
*/
|
|
23
23
|
testID?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Validation text displayed below the helper text when in an invalid state
|
|
26
|
+
*/
|
|
27
|
+
invalidText?: string;
|
|
24
28
|
}
|
|
25
29
|
export default ExpandableCardGroupProps;
|