@tamagui/list-item 2.7.7 → 3.0.0-beta.1091.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ListItem.tsx CHANGED
@@ -1,34 +1,24 @@
1
- import { getFontSize } from '@tamagui/font-size'
2
1
  import { getFontSized } from '@tamagui/get-font-sized'
3
- import { getSize, getSpace } from '@tamagui/get-token'
4
- import { withStaticProperties } from '@tamagui/helpers'
5
- import { getIcon, useCurrentColor } from '@tamagui/helpers-tamagui'
2
+ import { getThemedIconSize, useGetThemedIcon } from '@tamagui/helpers-tamagui'
3
+ import { oneSizeSmaller, resolveSize } from '@tamagui/size'
6
4
  import { YStack } from '@tamagui/stacks'
7
- import { SizableText, wrapChildrenInText } from '@tamagui/text'
5
+ import type { TextParentStyles } from '@tamagui/text'
6
+ import { SizableText, textParentProps, wrapChildrenInText } from '@tamagui/text'
8
7
  import type { ColorTokens, FontSizeTokens, GetProps, SizeTokens } from '@tamagui/web'
9
- import { createStyledContext, styled, View } from '@tamagui/web'
8
+ import { createStyledContext, splitStyleProps, styled, View } from '@tamagui/web'
10
9
  import type { FunctionComponent, JSX, ReactNode } from 'react'
11
10
 
12
11
  type IconProp = JSX.Element | FunctionComponent<{ color?: any; size?: any }> | null
13
12
 
14
- type ListItemVariant = 'outlined'
15
-
16
- export type ListItemExtraProps = {
17
- icon?: IconProp
18
- iconAfter?: IconProp
19
- scaleIcon?: number
20
- title?: ReactNode
21
- subTitle?: ReactNode
22
- iconSize?: SizeTokens
23
- }
24
-
25
- export type ListItemProps = GetProps<typeof ListItemFrame> & ListItemExtraProps
26
-
27
- const NAME = 'ListItem'
28
-
29
- const context = createStyledContext<{
30
- size?: SizeTokens
31
- variant?: ListItemVariant
13
+ /**
14
+ * The three props every part shares. A styled component that declares this
15
+ * context reads these from an ancestor and, for any it was passed directly,
16
+ * republishes them to its own descendants — which is the whole mechanism for
17
+ * getting size and color from a ListItem down to its text and icons.
18
+ */
19
+ export const ListItemContext = createStyledContext<{
20
+ size?: SizeTokens | true
21
+ variant?: 'outlined'
32
22
  color?: ColorTokens | string
33
23
  }>({
34
24
  size: undefined,
@@ -36,262 +26,197 @@ const context = createStyledContext<{
36
26
  color: undefined,
37
27
  })
38
28
 
39
- const ListItemFrame = styled(View, {
40
- context,
41
- name: NAME,
42
- render: 'li',
43
- role: 'listitem',
44
-
45
- variants: {
46
- unstyled: {
47
- false: {
48
- size: '$true',
49
- alignItems: 'center',
50
- justifyContent: 'space-between',
51
- flexWrap: 'nowrap',
52
- borderColor: '$borderColor',
53
- width: '100%',
54
- maxWidth: '100%',
55
- overflow: 'hidden',
56
- flexDirection: 'row',
57
- backgroundColor: '$background',
58
- cursor: 'default',
59
-
60
- hoverStyle: {
61
- backgroundColor: '$backgroundHover',
62
- borderColor: '$borderColorHover',
63
- },
64
-
65
- pressStyle: {
66
- backgroundColor: '$backgroundPress',
67
- borderColor: '$borderColorPress',
68
- },
69
- },
70
- },
71
-
72
- variant: {
73
- outlined:
74
- process.env.TAMAGUI_HEADLESS === '1'
75
- ? {}
76
- : {
77
- backgroundColor: 'transparent',
78
- borderWidth: 1,
79
- borderColor: '$borderColor',
80
-
81
- hoverStyle: {
82
- backgroundColor: 'transparent',
83
- borderColor: '$borderColorHover',
84
- },
29
+ export const listItemSizeVariant = styled.dynamic<SizeTokens | true>((val, env) => {
30
+ const { frame } = resolveSize(val, env)
31
+ return {
32
+ minHeight: frame.minHeight,
33
+ paddingHorizontal: frame.paddingHorizontal,
34
+ paddingVertical: frame.paddingVertical,
35
+ gap: frame.gap,
36
+ }
37
+ })
85
38
 
86
- pressStyle: {
87
- backgroundColor: 'transparent',
88
- borderColor: '$borderColorPress',
89
- },
90
- },
91
- },
39
+ const listItemSubtitleSizeVariant = styled.dynamic<SizeTokens | true>((val, env) => {
40
+ return getFontSized(oneSizeSmaller(val, env.sizes) as FontSizeTokens, env)
41
+ })
92
42
 
93
- size: {
94
- '...size': (val: SizeTokens, { tokens }) => {
95
- return {
96
- minHeight: tokens.size[val],
97
- paddingHorizontal: tokens.space[val],
98
- paddingVertical: getSpace(tokens.space[val], {
99
- shift: -4,
100
- }),
101
- }
102
- },
103
- },
43
+ // structural layout, the size mechanism, and the disabled pointer-event block.
44
+ // theme decoration (palette, border, cursor, the outlined/active appearance,
45
+ // disabled dimming) lives in a skin — see tamagui's components/ListItem.tsx
46
+ export const ListItemFrame = styled(View, {
47
+ context: ListItemContext,
48
+ displayName: 'ListItemFrame',
49
+ // role is the cross-platform one; render only lands on web
50
+ role: 'listitem',
51
+ render: 'li',
52
+ size: true,
53
+ alignItems: 'center',
54
+ justifyContent: 'space-between',
55
+ flexWrap: 'nowrap',
56
+ width: '100%',
57
+ maxWidth: '100%',
58
+ overflow: 'hidden',
59
+ flexDirection: 'row',
104
60
 
105
- active: {
106
- true: {
107
- hoverStyle: {
108
- backgroundColor: '$background',
109
- },
110
- },
111
- },
61
+ variants: {
62
+ size: listItemSizeVariant,
112
63
 
113
64
  disabled: {
114
65
  true: {
115
- opacity: 0.5,
116
66
  pointerEvents: 'none',
117
67
  },
118
68
  },
119
69
  } as const,
120
-
121
- defaultVariants: {
122
- unstyled: process.env.TAMAGUI_HEADLESS === '1',
123
- },
124
70
  })
125
71
 
126
- const ListItemText = styled(SizableText, {
127
- context,
128
- name: 'ListItemText',
129
-
130
- variants: {
131
- unstyled: {
132
- false: {
133
- color: '$color',
134
- size: '$true',
135
- flexGrow: 1,
136
- flexShrink: 1,
137
- ellipsis: true,
138
- cursor: 'inherit',
139
- },
140
- },
141
- } as const,
142
-
143
- defaultVariants: {
144
- unstyled: process.env.TAMAGUI_HEADLESS === '1',
145
- },
72
+ export const ListItemText = styled(SizableText, {
73
+ context: ListItemContext,
74
+ displayName: 'ListItemText',
75
+ size: true,
76
+ flexGrow: 1,
77
+ flexShrink: 1,
78
+ ellipsis: true,
79
+ cursor: 'inherit',
146
80
  })
147
81
 
148
- const ListItemSubtitle = styled(ListItemText, {
149
- name: 'ListItemSubtitle',
150
- context,
151
- variants: {
152
- unstyled: {
153
- false: {
154
- opacity: 0.6,
155
- maxWidth: '100%',
156
- color: '$color',
157
- },
158
- },
82
+ export const ListItemSubtitle = styled(ListItemText, {
83
+ context: ListItemContext,
84
+ displayName: 'ListItemSubtitle',
85
+ opacity: 0.6,
86
+ maxWidth: '100%',
159
87
 
160
- size: {
161
- '...size': (val, extras) => {
162
- const oneSmaller = getSize(val, {
163
- shift: -1,
164
- excludeHalfSteps: true,
165
- })
166
- const fontStyle = getFontSized(oneSmaller.key as FontSizeTokens, extras as any)
167
- return fontStyle
168
- },
169
- },
88
+ variants: {
89
+ size: listItemSubtitleSizeVariant,
170
90
  } as const,
171
-
172
- defaultVariants: {
173
- unstyled: process.env.TAMAGUI_HEADLESS === '1',
174
- },
175
91
  })
176
92
 
177
- const ListItemTitle = styled(ListItemText, {
178
- name: 'ListItemTitle',
179
- context,
180
- variants: {
181
- unstyled: {
182
- false: {},
183
- },
184
- } as const,
185
- defaultVariants: {
186
- unstyled: process.env.TAMAGUI_HEADLESS === '1',
187
- },
93
+ export const ListItemTitle = styled(ListItemText, {
94
+ context: ListItemContext,
95
+ displayName: 'ListItemTitle',
188
96
  })
189
97
 
190
- const ListItemIcon = (props: {
191
- children: React.ReactNode
192
- size?: SizeTokens
98
+ export type ListItemIconProps = {
99
+ children: ReactNode
100
+ size?: SizeTokens | true
193
101
  scaleIcon?: number
194
- }) => {
195
- const { children, size, scaleIcon = 1 } = props
196
- const styledContext = context.useStyledContext()
197
- if (!styledContext) {
198
- throw new Error('ListItem.Icon must be used within a ListItem')
199
- }
200
-
201
- const sizeToken = size ?? styledContext.size ?? '$true'
202
- const iconColor = useCurrentColor(styledContext.color)
203
-
204
- const iconSize = getFontSize(sizeToken as any) * scaleIcon
102
+ }
205
103
 
206
- return getIcon(children, {
207
- size: iconSize,
208
- color: iconColor,
104
+ export const ListItemIcon = ({ children, size, scaleIcon = 1 }: ListItemIconProps) => {
105
+ const context = ListItemContext.useStyledContext()
106
+ const getThemedIcon = useGetThemedIcon({
107
+ size: getThemedIconSize(size ?? context.size ?? true, scaleIcon),
108
+ color: context.color,
209
109
  })
110
+
111
+ return getThemedIcon(children)
210
112
  }
211
113
 
212
- const ListItemComponent = ListItemFrame.styleable<ListItemExtraProps>(
213
- function ListItem(propsIn, ref) {
214
- const {
215
- children,
216
- icon,
217
- iconAfter,
218
- scaleIcon = 1,
219
- unstyled = false,
220
- subTitle,
221
- title,
222
- iconSize,
223
- ...rest
224
- } = propsIn
114
+ /**
115
+ * The props `useListItem` reads and replaces, so exactly what its result omits.
116
+ */
117
+ type ListItemConsumedProps = {
118
+ children?: ReactNode
119
+ icon?: IconProp
120
+ iconAfter?: IconProp
121
+ iconSize?: SizeTokens | true
122
+ scaleIcon?: number
123
+ subTitle?: ReactNode
124
+ title?: ReactNode
125
+ }
225
126
 
226
- const size = propsIn.size || '$true'
227
- const styledContext = context.useStyledContext()
228
- const iconColor = useCurrentColor(styledContext?.color)
229
- const iconSizeNumber = getFontSize(iconSize || (size as any)) * scaleIcon
127
+ export type ListItemBehaviorProps = TextParentStyles &
128
+ ListItemConsumedProps & {
129
+ // read to theme an `icon` prop, then passed through untouched: the frame
130
+ // declares the styled context, so it is what publishes these to the parts
131
+ color?: ColorTokens | string
132
+ size?: SizeTokens | true
133
+ }
230
134
 
231
- const iconSpacing = iconSizeNumber * 0.4
232
- const themedIcon = icon ? (
233
- <View marginRight={iconSpacing}>
234
- {getIcon(icon, { size: iconSizeNumber, color: iconColor })}
235
- </View>
236
- ) : null
237
- const themedIconAfter = iconAfter ? (
238
- <View marginLeft={iconSpacing}>
239
- {getIcon(iconAfter, { size: iconSizeNumber, color: iconColor })}
240
- </View>
241
- ) : null
135
+ /**
136
+ * What `useListItem` returns: the caller's props minus the ones it consumed.
137
+ * Spelled out rather than cast, so a skin that spreads the result onto a frame
138
+ * is type-checked on exactly what it will receive.
139
+ */
140
+ export type UseListItemProps<Props extends ListItemBehaviorProps> = Omit<
141
+ Omit<Props, keyof TextParentStyles | keyof typeof textParentProps>,
142
+ keyof ListItemConsumedProps
143
+ > & {
144
+ children: ReactNode
145
+ color?: Props['color']
146
+ }
242
147
 
243
- const wrappedChildren = wrapChildrenInText(
244
- ListItemText,
245
- { children },
246
- propsIn.unstyled !== true
247
- ? {
248
- unstyled: process.env.TAMAGUI_HEADLESS === '1',
249
- fontSize: propsIn.size,
250
- }
251
- : undefined
252
- )
148
+ /**
149
+ * ListItem behavior: theming the icon props and assembling title, subtitle, and
150
+ * children into the frame's single child. Flat text styles are handed directly
151
+ * to generated text, while size and color still reach the parts through the
152
+ * styled context.
153
+ */
154
+ export function useListItem<Props extends ListItemBehaviorProps>(
155
+ propsIn: Props
156
+ ): { props: UseListItemProps<Props> } {
157
+ const [wrappedTextProps, listItemProps] = splitStyleProps(propsIn, {
158
+ expandShorthands: true,
159
+ filter: textParentProps,
160
+ })
161
+ const {
162
+ children,
163
+ icon,
164
+ iconAfter,
165
+ iconSize,
166
+ scaleIcon = 1,
167
+ subTitle,
168
+ title,
169
+ ...frameProps
170
+ } = listItemProps
171
+ const { noTextWrap, textProps, ...textStyleProps } = wrappedTextProps
172
+
173
+ // the frame publishes size and color to every part below it, but an `icon`
174
+ // prop is themed here, before the frame renders, so it reads them itself
175
+ const context = ListItemContext.useStyledContext()
176
+ const getThemedIcon = useGetThemedIcon({
177
+ size: getThemedIconSize(iconSize ?? propsIn.size ?? context.size ?? true, scaleIcon),
178
+ color: propsIn.color ?? context.color,
179
+ })
253
180
 
254
- return (
255
- <ListItemFrame ref={ref} {...rest}>
256
- {themedIcon}
257
- {title || subTitle ? (
258
- <YStack flex={1}>
259
- {title ? (
260
- typeof title === 'string' ? (
261
- <ListItemTitle unstyled={unstyled} size={size as any}>
181
+ const wrappedChildren = wrapChildrenInText(
182
+ ListItemText,
183
+ { children, noTextWrap, textProps },
184
+ textStyleProps
185
+ )
186
+
187
+ return {
188
+ props: {
189
+ ...frameProps,
190
+ color: propsIn.color,
191
+ children: (
192
+ <>
193
+ {icon ? getThemedIcon(icon) : null}
194
+ {title || subTitle ? (
195
+ <YStack flex={1}>
196
+ {typeof title === 'string' ? (
197
+ <ListItemTitle {...textStyleProps} {...textProps}>
262
198
  {title}
263
199
  </ListItemTitle>
264
200
  ) : (
265
201
  title
266
- )
267
- ) : null}
268
- {subTitle ? (
269
- <>
270
- {typeof subTitle === 'string' ? (
271
- <ListItemSubtitle unstyled={unstyled} size={size}>
272
- {subTitle}
273
- </ListItemSubtitle>
274
- ) : (
275
- subTitle
276
- )}
277
- </>
278
- ) : null}
279
- {wrappedChildren}
280
- </YStack>
281
- ) : (
282
- wrappedChildren
283
- )}
284
- {themedIconAfter}
285
- </ListItemFrame>
286
- )
202
+ )}
203
+ {typeof subTitle === 'string' ? (
204
+ <ListItemSubtitle {...textStyleProps} {...textProps}>
205
+ {subTitle}
206
+ </ListItemSubtitle>
207
+ ) : (
208
+ subTitle
209
+ )}
210
+ {wrappedChildren}
211
+ </YStack>
212
+ ) : (
213
+ wrappedChildren
214
+ )}
215
+ {iconAfter ? getThemedIcon(iconAfter) : null}
216
+ </>
217
+ ),
218
+ },
287
219
  }
288
- )
220
+ }
289
221
 
290
- export const ListItem = withStaticProperties(ListItemComponent, {
291
- Apply: context.Provider,
292
- Frame: ListItemFrame,
293
- Text: ListItemText,
294
- Subtitle: ListItemSubtitle,
295
- Icon: ListItemIcon,
296
- Title: ListItemTitle,
297
- })
222
+ export type ListItemFrameProps = GetProps<typeof ListItemFrame>