@tamagui/button 3.0.0-beta.807.1 → 3.0.0-beta.831.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/Button.tsx CHANGED
@@ -1,57 +1,14 @@
1
1
  import { useGetThemedIcon } from '@tamagui/helpers-tamagui'
2
2
  import { ButtonNestingContext } from '@tamagui/stacks'
3
- import type { TextContextStyles, TextParentStyles } from '@tamagui/text'
4
- import { wrapChildrenInText } from '@tamagui/text'
5
- import type { GetProps } from '@tamagui/web'
6
- import {
7
- createStyledContext,
8
- createStyledHOC,
9
- isVariable,
10
- styled,
11
- Text,
12
- useProps,
13
- View,
14
- withStaticProperties,
15
- } from '@tamagui/web'
3
+ import type { TextParentStyles } from '@tamagui/text'
4
+ import { textParentProps, wrapChildrenInText } from '@tamagui/text'
5
+ import type { GetProps, TamaguiComponentPropsBaseBase } from '@tamagui/web'
6
+ import { splitStyleProps, styled, Text, View } from '@tamagui/web'
16
7
  import type { FunctionComponent, JSX, ReactNode } from 'react'
17
8
  import { useContext } from 'react'
18
- import type { OpaqueColorValue } from 'react-native'
19
-
20
- const buttonContextKeys = [
21
- 'color',
22
- 'ellipsis',
23
- 'fontFamily',
24
- 'fontSize',
25
- 'fontStyle',
26
- 'fontWeight',
27
- 'letterSpacing',
28
- 'maxFontSizeMultiplier',
29
- 'textAlign',
30
- ] as const
31
-
32
- export const ButtonContext = createStyledContext<
33
- TextContextStyles,
34
- (typeof buttonContextKeys)[number]
35
- >(
36
- {
37
- color: undefined,
38
- ellipsis: undefined,
39
- fontFamily: undefined,
40
- fontSize: undefined,
41
- fontStyle: undefined,
42
- fontWeight: undefined,
43
- letterSpacing: undefined,
44
- maxFontSizeMultiplier: undefined,
45
- textAlign: undefined,
46
- },
47
- {
48
- keys: buttonContextKeys,
49
- }
50
- )
51
9
 
52
10
  export const ButtonFrame = styled(View, {
53
- context: ButtonContext,
54
- displayName: 'ButtonFrame',
11
+ // role is the cross-platform one; render only lands on web
55
12
  role: 'button',
56
13
  render: <button type="button" />,
57
14
  tabIndex: 0,
@@ -70,8 +27,6 @@ export const ButtonFrame = styled(View, {
70
27
  })
71
28
 
72
29
  export const ButtonText = styled(Text, {
73
- context: ButtonContext,
74
- displayName: 'ButtonText',
75
30
  // flexGrow 1 pushes text to the start of its parent on native
76
31
  flexGrow: 0,
77
32
  flexShrink: 1,
@@ -79,30 +34,14 @@ export const ButtonText = styled(Text, {
79
34
 
80
35
  export type ButtonIconProps = {
81
36
  children: ReactNode
82
- color?: TextContextStyles['color']
37
+ color?: string
83
38
  scaleIcon?: number
84
39
  size?: number
85
40
  }
86
41
 
87
- // react-native opaque colors (PlatformColor, DynamicColorIOS) are plain
88
- // objects and remain valid single icon colors, unlike conditional flat objects
89
- const isOpaqueColor = (value: unknown): value is OpaqueColorValue =>
90
- typeof value === 'object' &&
91
- value !== null &&
92
- ('dynamic' in value || 'semantic' in value || 'resource_paths' in value)
93
-
94
42
  export const ButtonIcon = ({ children, color, scaleIcon = 1, size }: ButtonIconProps) => {
95
- const styledContext = ButtonContext.useStyledContext()
96
- const iconColor = color ?? styledContext?.color
97
43
  const getThemedIcon = useGetThemedIcon({
98
- // icons take one concrete color: conditional values (numbers, flat
99
- // objects) fall back to the theme color
100
- color:
101
- (typeof iconColor === 'string' && iconColor !== 'unset') ||
102
- isVariable(iconColor) ||
103
- isOpaqueColor(iconColor)
104
- ? iconColor
105
- : undefined,
44
+ color,
106
45
  size: size === undefined ? undefined : size * scaleIcon,
107
46
  })
108
47
 
@@ -115,13 +54,23 @@ type ButtonIconInput =
115
54
  | ((props: { color?: any; size?: any }) => ReactNode)
116
55
  | null
117
56
 
118
- export type ButtonBehaviorProps = TextParentStyles & {
57
+ /**
58
+ * The props `useButton` reads and replaces. It hands the frame everything else
59
+ * untouched, so this is exactly what its result omits.
60
+ */
61
+ type ButtonConsumedProps = {
62
+ children?: ReactNode
63
+ disabled?: boolean
64
+ render?: TamaguiComponentPropsBaseBase['render']
65
+
119
66
  icon?: ButtonIconInput
120
67
  iconAfter?: ButtonIconInput
121
68
  iconSize?: number
122
69
  scaleIcon?: number
70
+ }
123
71
 
124
- // native button html props
72
+ /** passed straight through to the rendered `<button>` */
73
+ type ButtonHTMLProps = {
125
74
  type?: 'submit' | 'reset' | 'button'
126
75
  form?: string
127
76
  formAction?: string
@@ -133,140 +82,87 @@ export type ButtonBehaviorProps = TextParentStyles & {
133
82
  value?: string | readonly string[] | number
134
83
  }
135
84
 
85
+ export type ButtonBehaviorProps = TextParentStyles & ButtonConsumedProps & ButtonHTMLProps
86
+
87
+ /**
88
+ * What `useButton` returns: the caller's props minus the ones it consumed, plus
89
+ * the ones it decides. Spelled out rather than cast, so a skin that spreads the
90
+ * result onto a frame is type-checked on exactly what it will receive.
91
+ */
92
+ export type UseButtonProps<Props> = Omit<
93
+ Omit<Props, keyof TextParentStyles | keyof typeof textParentProps>,
94
+ keyof ButtonConsumedProps
95
+ > & {
96
+ children: ReactNode
97
+ 'aria-disabled'?: boolean
98
+ disabled?: boolean
99
+ render?: TamaguiComponentPropsBaseBase['render']
100
+ tabIndex?: number
101
+ }
102
+
136
103
  export type UseButtonOptions = {
137
104
  Text?: any
138
- iconColor?: TextContextStyles['color']
105
+ iconColor?: string
139
106
  iconSize?: number
140
107
  textProps?: Record<string, unknown>
141
108
  }
142
109
 
143
- const buttonInternalPropNames = [
144
- 'children',
145
- 'color',
146
- 'disabled',
147
- 'ellipsis',
148
- 'fontFamily',
149
- 'fontSize',
150
- 'fontStyle',
151
- 'fontWeight',
152
- 'icon',
153
- 'iconAfter',
154
- 'iconSize',
155
- 'letterSpacing',
156
- 'maxFontSizeMultiplier',
157
- 'noTextWrap',
158
- 'render',
159
- 'scaleIcon',
160
- 'textAlign',
161
- 'textProps',
162
- ] as const
163
-
110
+ /**
111
+ * Button behavior: icon theming, wrapping bare children in a text, and the html
112
+ * nesting rules. Flat text styles are partitioned in one pass and handed to the
113
+ * wrapped text, with no style resolution hook or text context.
114
+ */
164
115
  export function useButton<Props extends ButtonBehaviorProps>(
165
116
  propsIn: Props,
166
117
  {
167
118
  Text = ButtonText,
168
- iconColor: iconColorOption,
119
+ iconColor,
169
120
  iconSize: iconSizeOption,
170
121
  textProps: textPropsOption,
171
122
  }: UseButtonOptions = {}
172
- ) {
123
+ ): { isNested: boolean; props: UseButtonProps<Props> } {
173
124
  const isNested = useContext(ButtonNestingContext)
174
- const styledContext = ButtonContext.useStyledContext()
175
- const processedProps = useProps(propsIn, {
176
- noNormalize: true,
177
- noExpand: true,
178
- }) as Props & {
179
- accessibilityRole?: string
180
- children?: ReactNode
181
- color?: TextContextStyles['color']
182
- disabled?: boolean
183
- render?: unknown
184
- role?: string
185
- size?: unknown
186
- }
125
+ const [wrappedTextProps, buttonProps] = splitStyleProps(propsIn, {
126
+ expandShorthands: true,
127
+ filter: textParentProps,
128
+ })
187
129
 
188
130
  const {
189
131
  children,
190
- color,
191
132
  disabled,
192
- ellipsis,
193
- fontFamily,
194
- fontSize,
195
- fontStyle,
196
- fontWeight,
197
133
  icon,
198
134
  iconAfter,
199
135
  iconSize,
200
- letterSpacing,
201
- maxFontSizeMultiplier,
202
- noTextWrap,
203
136
  render,
204
137
  scaleIcon = 1,
205
- textAlign,
206
- textProps,
207
- } = processedProps
208
-
209
- // useProps resolves styles for the component running the hook. This behavior
210
- // HOC renders a styled frame, so its frame styles must keep the raw flat
211
- // programs for the frame's own hover/press/focus state and merge pass.
212
- const frameProps = { ...propsIn }
213
- for (const key of buttonInternalPropNames) {
214
- delete (frameProps as Record<string, any>)[key]
215
- }
138
+ ...frameProps
139
+ } = buttonProps
140
+ const { noTextWrap, textProps, ...textStyleProps } = wrappedTextProps
216
141
 
217
- const contextColor = color ?? styledContext?.color
218
- const iconColor = iconColorOption ?? contextColor
219
142
  const resolvedIconSize = iconSize ?? iconSizeOption
220
143
  const getThemedIcon = useGetThemedIcon({
221
- // icons take one concrete color: conditional values (numbers, flat
222
- // objects) fall back to the theme color
223
- color:
224
- (typeof iconColor === 'string' && iconColor !== 'unset') ||
225
- isVariable(iconColor) ||
226
- isOpaqueColor(iconColor)
227
- ? iconColor
228
- : undefined,
144
+ color: iconColor,
229
145
  size: resolvedIconSize === undefined ? undefined : resolvedIconSize * scaleIcon,
230
146
  })
231
147
  const [themedIcon, themedIconAfter] = [icon, iconAfter].map((item) => {
232
148
  return item ? getThemedIcon(item) : null
233
149
  })
234
150
 
235
- const textContext: TextContextStyles = {
236
- color: contextColor,
237
- ellipsis: ellipsis ?? styledContext?.ellipsis,
238
- fontFamily: fontFamily ?? styledContext?.fontFamily,
239
- fontSize: fontSize ?? styledContext?.fontSize,
240
- fontStyle: fontStyle ?? styledContext?.fontStyle,
241
- fontWeight: fontWeight ?? styledContext?.fontWeight,
242
- letterSpacing: letterSpacing ?? styledContext?.letterSpacing,
243
- maxFontSizeMultiplier: maxFontSizeMultiplier ?? styledContext?.maxFontSizeMultiplier,
244
- textAlign: textAlign ?? styledContext?.textAlign,
245
- }
151
+ // adjacent text children join into one Text: `<Button>hi {name}</Button>` is
152
+ // two children, and wrapping them separately lays them out with a gap,
153
+ // ellipsizes per fragment, reads as fragments to assistive tech, and throws
154
+ // "Text strings must be rendered within a <Text>" for a bare number on native
246
155
  const wrappedChildren = wrapChildrenInText(
247
156
  Text,
248
- {
249
- children,
250
- ...textContext,
251
- noTextWrap,
252
- textProps,
253
- },
254
- {
255
- ...textPropsOption,
256
- ...(processedProps.size === undefined ? null : { size: processedProps.size }),
257
- }
157
+ { children, noTextWrap, textProps },
158
+ { ...textPropsOption, ...textStyleProps }
258
159
  )
160
+
259
161
  const resolvedRender =
260
162
  render ??
261
- (isNested ? (
262
- 'span'
263
- ) : processedProps.accessibilityRole === 'link' || processedProps.role === 'link' ? (
264
- 'a'
265
- ) : disabled ? (
266
- <button type="button" disabled />
267
- ) : undefined)
163
+ (isNested ? 'span' : disabled ? <button type="button" disabled /> : undefined)
268
164
 
269
- const props = {
165
+ const resolvedProps = {
270
166
  ...frameProps,
271
167
  ...(disabled && {
272
168
  'aria-disabled': true,
@@ -294,35 +190,17 @@ export function useButton<Props extends ButtonBehaviorProps>(
294
190
  }),
295
191
  children: (
296
192
  <ButtonNestingContext.Provider value={true}>
297
- <ButtonContext.Provider {...textContext}>
298
- {themedIcon}
299
- {wrappedChildren}
300
- {themedIconAfter}
301
- </ButtonContext.Provider>
193
+ {themedIcon}
194
+ {wrappedChildren}
195
+ {themedIconAfter}
302
196
  </ButtonNestingContext.Provider>
303
197
  ),
304
- } as Props
198
+ }
305
199
 
306
200
  return {
307
201
  isNested,
308
- props,
202
+ props: resolvedProps,
309
203
  }
310
204
  }
311
205
 
312
- const ButtonComponent = createStyledHOC(
313
- ButtonFrame,
314
- function Button(props: ButtonBehaviorProps, ref) {
315
- const { props: buttonProps } = useButton(props)
316
-
317
- return <ButtonFrame ref={ref} {...buttonProps} />
318
- }
319
- )
320
-
321
- export const Button = withStaticProperties(ButtonComponent, {
322
- Apply: ButtonContext.Provider,
323
- Frame: ButtonFrame,
324
- Icon: ButtonIcon,
325
- Text: ButtonText,
326
- })
327
-
328
- export type ButtonProps = GetProps<typeof ButtonComponent>
206
+ export type ButtonFrameProps = GetProps<typeof ButtonFrame>