@tamagui/button 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.
Files changed (48) hide show
  1. package/dist/cjs/Button.cjs +97 -268
  2. package/dist/cjs/Button.native.cjs +122 -0
  3. package/dist/cjs/Button.native.js +100 -273
  4. package/dist/cjs/Button.native.js.map +1 -1
  5. package/dist/cjs/index.cjs +9 -11
  6. package/dist/cjs/index.native.cjs +20 -0
  7. package/dist/cjs/index.native.js +9 -10
  8. package/dist/cjs/index.native.js.map +1 -1
  9. package/dist/esm/Button.mjs +88 -257
  10. package/dist/esm/Button.mjs.map +1 -1
  11. package/dist/esm/Button.native.js +91 -263
  12. package/dist/esm/Button.native.js.map +1 -1
  13. package/dist/esm/index.js +1 -2
  14. package/dist/esm/index.mjs +1 -2
  15. package/dist/esm/index.native.js +1 -2
  16. package/dist/jsx/Button.mjs +88 -257
  17. package/dist/jsx/Button.mjs.map +1 -1
  18. package/dist/jsx/Button.native.cjs +122 -0
  19. package/dist/jsx/Button.native.js +100 -273
  20. package/dist/jsx/Button.native.js.map +1 -1
  21. package/dist/jsx/index.js +1 -2
  22. package/dist/jsx/index.mjs +1 -2
  23. package/dist/jsx/index.native.cjs +20 -0
  24. package/dist/jsx/index.native.js +9 -10
  25. package/dist/jsx/index.native.js.map +1 -1
  26. package/package.json +14 -14
  27. package/src/Button.tsx +152 -309
  28. package/types/Button.d.ts +75 -208
  29. package/types/Button.d.ts.map +1 -1
  30. package/dist/cjs/Button.test.cjs +0 -37
  31. package/dist/cjs/Button.test.native.js +0 -40
  32. package/dist/cjs/Button.test.native.js.map +0 -1
  33. package/dist/esm/Button.test.mjs +0 -38
  34. package/dist/esm/Button.test.mjs.map +0 -1
  35. package/dist/esm/Button.test.native.js +0 -38
  36. package/dist/esm/Button.test.native.js.map +0 -1
  37. package/dist/esm/index.js.map +0 -1
  38. package/dist/esm/index.mjs.map +0 -1
  39. package/dist/esm/index.native.js.map +0 -1
  40. package/dist/jsx/Button.test.mjs +0 -38
  41. package/dist/jsx/Button.test.mjs.map +0 -1
  42. package/dist/jsx/Button.test.native.js +0 -40
  43. package/dist/jsx/Button.test.native.js.map +0 -1
  44. package/dist/jsx/index.js.map +0 -1
  45. package/dist/jsx/index.mjs.map +0 -1
  46. package/src/Button.test.tsx +0 -45
  47. package/types/Button.test.d.ts +0 -2
  48. package/types/Button.test.d.ts.map +0 -1
package/src/Button.tsx CHANGED
@@ -1,226 +1,76 @@
1
- import { getFontSize } from '@tamagui/font-size'
2
- import { getButtonSized } from '@tamagui/get-button-sized'
3
- import { getIcon, useCurrentColor } from '@tamagui/helpers-tamagui'
4
- import { ButtonNestingContext, getElevation, themeableVariants } from '@tamagui/stacks'
5
- import type { TextContextStyles, TextParentStyles } from '@tamagui/text'
6
- import { SizableText, wrapChildrenInText } from '@tamagui/text'
7
- import type { GetProps, SizeTokens, Token } from '@tamagui/web'
8
- import {
9
- createStyledContext,
10
- getTokenValue,
11
- styled,
12
- useProps,
13
- View,
14
- withStaticProperties,
15
- } from '@tamagui/web'
16
- import type { FunctionComponent, JSX } from 'react'
1
+ import { useGetThemedIcon } from '@tamagui/helpers-tamagui'
2
+ import { ButtonNestingContext } from '@tamagui/stacks'
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'
7
+ import type { FunctionComponent, JSX, ReactNode } from 'react'
17
8
  import { useContext } from 'react'
18
9
 
19
- type ButtonVariant = 'outlined'
20
-
21
- type ButtonContextStyles = TextContextStyles & {
22
- size?: SizeTokens
23
- variant?: ButtonVariant
24
- elevation?: SizeTokens | number
25
- }
26
-
27
- const context = createStyledContext<ButtonContextStyles>({
28
- size: undefined,
29
- variant: undefined,
30
- color: undefined,
31
- elevation: undefined,
32
- ellipsis: undefined,
33
- fontFamily: undefined,
34
- fontSize: undefined,
35
- fontStyle: undefined,
36
- fontWeight: undefined,
37
- letterSpacing: undefined,
38
- maxFontSizeMultiplier: undefined,
39
- textAlign: undefined,
40
- })
41
-
42
- const Frame = styled(View, {
43
- context,
44
- name: 'Button',
10
+ export const ButtonFrame = styled(View, {
11
+ // role is the cross-platform one; render only lands on web
45
12
  role: 'button',
46
13
  render: <button type="button" />,
47
14
  tabIndex: 0,
15
+ alignItems: 'center',
16
+ flexDirection: 'row',
17
+ flexWrap: 'nowrap',
18
+ justifyContent: 'center',
48
19
 
49
20
  variants: {
50
- unstyled: {
51
- true: {
52
- // reset browser <button> defaults
53
- outlineWidth: 0,
54
- borderWidth: 0,
55
- backgroundColor: 'transparent',
56
- },
57
- false: {
58
- size: '$true',
59
- justifyContent: 'center',
60
- alignItems: 'center',
61
- flexWrap: 'nowrap',
62
- flexDirection: 'row',
63
- backgroundColor: '$background',
64
- borderWidth: 1,
65
- borderColor: 'transparent',
66
-
67
- '$platform-web': {
68
- cursor: 'pointer',
69
- },
70
-
71
- hoverStyle: {
72
- backgroundColor: '$backgroundHover',
73
- borderColor: '$borderColorHover',
74
- },
75
-
76
- pressStyle: {
77
- backgroundColor: '$backgroundPress',
78
- borderColor: '$borderColorHover',
79
- },
80
-
81
- focusVisibleStyle: {
82
- outlineColor: '$outlineColor',
83
- outlineStyle: 'solid',
84
- outlineWidth: 2,
85
- },
86
- },
87
- },
88
-
89
- variant: {
90
- outlined:
91
- process.env.TAMAGUI_HEADLESS === '1'
92
- ? {}
93
- : {
94
- backgroundColor: 'transparent',
95
- borderWidth: 1,
96
- borderColor: '$borderColor',
97
-
98
- hoverStyle: {
99
- backgroundColor: 'transparent',
100
- borderColor: '$borderColorHover',
101
- },
102
-
103
- pressStyle: {
104
- backgroundColor: 'transparent',
105
- borderColor: '$borderColorPress',
106
- },
107
- },
108
- },
109
-
110
- circular: themeableVariants.circular,
111
-
112
- chromeless: themeableVariants.chromeless,
113
-
114
- size: {
115
- '...size': (val, extras) => {
116
- const buttonStyle = getButtonSized(val, extras)
117
- const gap = getTokenValue(val as Token)
118
- return {
119
- ...buttonStyle,
120
- gap,
121
- }
122
- },
123
- ':number': (val, extras) => {
124
- const buttonStyle = getButtonSized(val, extras)
125
- const gap = val * 0.4
126
- return {
127
- ...buttonStyle,
128
- gap,
129
- }
130
- },
131
- },
132
-
133
- elevation: {
134
- '...size': getElevation,
135
- ':number': getElevation,
136
- },
137
-
138
21
  disabled: {
139
22
  true: {
140
23
  pointerEvents: 'none',
141
- // @ts-ignore
142
- 'aria-disabled': true,
143
24
  },
144
25
  },
145
26
  } as const,
146
-
147
- defaultVariants: {
148
- unstyled: process.env.TAMAGUI_HEADLESS === '1',
149
- },
150
27
  })
151
28
 
152
- const Text = styled(SizableText, {
153
- context,
154
-
155
- variants: {
156
- unstyled: {
157
- false: {
158
- userSelect: 'none',
159
- // flexGrow 1 leads to inconsistent native style where text pushes to start of view
160
- flexGrow: 0,
161
- flexShrink: 1,
162
- ellipsis: true,
163
- color: '$color',
164
-
165
- '$platform-web': {
166
- cursor: 'pointer',
167
- },
168
- },
169
- },
170
- } as const,
171
-
172
- defaultVariants: {
173
- unstyled: process.env.TAMAGUI_HEADLESS === '1',
174
- },
29
+ export const ButtonText = styled(Text, {
30
+ // flexGrow 1 pushes text to the start of its parent on native
31
+ flexGrow: 0,
32
+ flexShrink: 1,
175
33
  })
176
34
 
177
- const Icon = (props: {
178
- children: React.ReactNode
35
+ export type ButtonIconProps = {
36
+ children: ReactNode
37
+ color?: string
179
38
  scaleIcon?: number
180
- size?: SizeTokens
181
- }) => {
182
- const { children, scaleIcon = 1, size } = props
183
- const styledContext = context.useStyledContext()
184
- if (!styledContext) {
185
- throw new Error('Button.Icon must be used within a Button')
186
- }
187
-
188
- const sizeToken = size ?? styledContext.size
189
- const iconColorProp =
190
- styledContext.color === 'unset' || typeof styledContext.color === 'number'
191
- ? undefined
192
- : styledContext.color
193
- const iconColor = useCurrentColor(iconColorProp)
194
-
195
- const iconSize =
196
- (typeof sizeToken === 'number' ? sizeToken * 0.5 : getFontSize(sizeToken as Token)) *
197
- scaleIcon
198
-
199
- return getIcon(children, {
200
- size: iconSize,
201
- color: iconColor,
202
- })
39
+ size?: number
203
40
  }
204
41
 
205
- export const ButtonContext = createStyledContext<{
206
- size?: SizeTokens
207
- variant?: ButtonVariant
208
- color?: ButtonContextStyles['color']
209
- }>({
210
- size: undefined,
211
- variant: undefined,
212
- color: undefined,
213
- })
42
+ export const ButtonIcon = ({ children, color, scaleIcon = 1, size }: ButtonIconProps) => {
43
+ const getThemedIcon = useGetThemedIcon({
44
+ color,
45
+ size: size === undefined ? undefined : size * scaleIcon,
46
+ })
214
47
 
215
- type IconProp = JSX.Element | FunctionComponent<{ color?: any; size?: any }> | null
48
+ return getThemedIcon(children)
49
+ }
216
50
 
217
- type ButtonExtraProps = TextParentStyles & {
218
- icon?: IconProp
219
- iconAfter?: IconProp
51
+ type ButtonIconInput =
52
+ | JSX.Element
53
+ | FunctionComponent<{ color?: any; size?: any }>
54
+ | ((props: { color?: any; size?: any }) => ReactNode)
55
+ | null
56
+
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
+
66
+ icon?: ButtonIconInput
67
+ iconAfter?: ButtonIconInput
68
+ iconSize?: number
220
69
  scaleIcon?: number
221
- iconSize?: SizeTokens
70
+ }
222
71
 
223
- // native button html props
72
+ /** passed straight through to the rendered `<button>` */
73
+ type ButtonHTMLProps = {
224
74
  type?: 'submit' | 'reset' | 'button'
225
75
  form?: string
226
76
  formAction?: string
@@ -232,132 +82,125 @@ type ButtonExtraProps = TextParentStyles & {
232
82
  value?: string | readonly string[] | number
233
83
  }
234
84
 
235
- const ButtonComponent = Frame.styleable<ButtonExtraProps>((propsIn, ref) => {
236
- const isNested = useContext(ButtonNestingContext)
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
+
103
+ export type UseButtonOptions = {
104
+ Text?: any
105
+ iconColor?: string
106
+ iconSize?: number
107
+ textProps?: Record<string, unknown>
108
+ }
237
109
 
238
- // Process props through useProps to expand shorthands (like br -> borderRadius)
239
- const processedProps = useProps(propsIn, {
240
- noNormalize: true,
241
- noExpand: true,
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
+ */
115
+ export function useButton<Props extends ButtonBehaviorProps>(
116
+ propsIn: Props,
117
+ {
118
+ Text = ButtonText,
119
+ iconColor,
120
+ iconSize: iconSizeOption,
121
+ textProps: textPropsOption,
122
+ }: UseButtonOptions = {}
123
+ ): { isNested: boolean; props: UseButtonProps<Props> } {
124
+ const isNested = useContext(ButtonNestingContext)
125
+ const [wrappedTextProps, buttonProps] = splitStyleProps(propsIn, {
126
+ expandShorthands: true,
127
+ filter: textParentProps,
242
128
  })
243
129
 
244
130
  const {
245
131
  children,
246
- iconSize,
132
+ disabled,
247
133
  icon,
248
134
  iconAfter,
135
+ iconSize,
136
+ render,
249
137
  scaleIcon = 1,
250
- noTextWrap,
251
- textProps,
252
- color,
253
- ellipsis,
254
- fontFamily,
255
- fontSize,
256
- fontStyle,
257
- fontWeight,
258
- letterSpacing,
259
- maxFontSizeMultiplier,
260
- textAlign,
261
- ...props
262
- } = processedProps
263
-
264
- const size = propsIn.size || (propsIn.unstyled ? undefined : '$true')
138
+ ...frameProps
139
+ } = buttonProps
140
+ const { noTextWrap, textProps, ...textStyleProps } = wrappedTextProps
265
141
 
266
- const styledContext = context.useStyledContext()
267
- const contextColor = color ?? propsIn.color ?? styledContext?.color
268
- const iconColorProp =
269
- contextColor === 'unset' || typeof contextColor === 'number'
270
- ? undefined
271
- : contextColor
272
- const iconColor = useCurrentColor(iconColorProp)
273
- const finalSize = iconSize ?? size ?? styledContext?.size
274
- const iconSizeNumber =
275
- (typeof finalSize === 'number' ? finalSize * 0.5 : getFontSize(finalSize as Token)) *
276
- scaleIcon
277
-
278
- const [themedIcon, themedIconAfter] = [icon, iconAfter].map((icon) => {
279
- if (!icon) return null
280
- return getIcon(icon, {
281
- size: iconSizeNumber,
282
- color: iconColor,
283
- // No marginLeft or marginRight needed - spacing is handled by the gap property in Frame's size variants
284
- })
142
+ const resolvedIconSize = iconSize ?? iconSizeOption
143
+ const getThemedIcon = useGetThemedIcon({
144
+ color: iconColor,
145
+ size: resolvedIconSize === undefined ? undefined : resolvedIconSize * scaleIcon,
146
+ })
147
+ const [themedIcon, themedIconAfter] = [icon, iconAfter].map((item) => {
148
+ return item ? getThemedIcon(item) : null
285
149
  })
286
150
 
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
287
155
  const wrappedChildren = wrapChildrenInText(
288
156
  Text,
289
- {
290
- children,
291
- color: contextColor,
292
- ellipsis: ellipsis ?? propsIn.ellipsis ?? styledContext?.ellipsis,
293
- fontFamily: fontFamily ?? propsIn.fontFamily ?? styledContext?.fontFamily,
294
- fontSize: fontSize ?? propsIn.fontSize ?? styledContext?.fontSize,
295
- fontStyle: fontStyle ?? propsIn.fontStyle ?? styledContext?.fontStyle,
296
- fontWeight: fontWeight ?? propsIn.fontWeight ?? styledContext?.fontWeight,
297
- letterSpacing:
298
- letterSpacing ?? propsIn.letterSpacing ?? styledContext?.letterSpacing,
299
- maxFontSizeMultiplier:
300
- maxFontSizeMultiplier ??
301
- propsIn.maxFontSizeMultiplier ??
302
- styledContext?.maxFontSizeMultiplier,
303
- noTextWrap: noTextWrap ?? propsIn.noTextWrap,
304
- textAlign: textAlign ?? propsIn.textAlign ?? styledContext?.textAlign,
305
- textProps: textProps ?? propsIn.textProps,
306
- },
307
- {
308
- unstyled: process.env.TAMAGUI_HEADLESS === '1',
309
- size: finalSize ?? styledContext?.size,
310
- }
157
+ { children, noTextWrap, textProps },
158
+ { ...textPropsOption, ...textStyleProps }
311
159
  )
312
160
 
313
- const textContext: TextContextStyles = {
314
- color: contextColor,
315
- ellipsis: ellipsis ?? propsIn.ellipsis ?? styledContext?.ellipsis,
316
- fontFamily: fontFamily ?? propsIn.fontFamily ?? styledContext?.fontFamily,
317
- fontSize: fontSize ?? propsIn.fontSize ?? styledContext?.fontSize,
318
- fontStyle: fontStyle ?? propsIn.fontStyle ?? styledContext?.fontStyle,
319
- fontWeight: fontWeight ?? propsIn.fontWeight ?? styledContext?.fontWeight,
320
- letterSpacing: letterSpacing ?? propsIn.letterSpacing ?? styledContext?.letterSpacing,
321
- maxFontSizeMultiplier:
322
- maxFontSizeMultiplier ??
323
- propsIn.maxFontSizeMultiplier ??
324
- styledContext?.maxFontSizeMultiplier,
325
- textAlign: textAlign ?? propsIn.textAlign ?? styledContext?.textAlign,
161
+ const resolvedRender =
162
+ render ??
163
+ (isNested ? 'span' : disabled ? <button type="button" disabled /> : undefined)
164
+
165
+ const resolvedProps = {
166
+ ...frameProps,
167
+ ...(disabled && {
168
+ 'aria-disabled': true,
169
+ disabled: true,
170
+ tabIndex: -1,
171
+ }),
172
+ ...(resolvedRender === undefined ? null : { render: resolvedRender }),
173
+ ...(isNested && {
174
+ // a nested Button is a presentation part, not a control: strip the
175
+ // interactive semantics so we never place a focusable role=button (with
176
+ // its own press/keyboard handlers) inside the outer native <button>. that
177
+ // nesting is invalid html, adds an extra focus stop, and bubbles
178
+ // activation to the outer control. resolvedRender already makes it a span.
179
+ role: 'none',
180
+ tabIndex: -1,
181
+ 'aria-disabled': undefined,
182
+ disabled: undefined,
183
+ onPress: undefined,
184
+ onPressIn: undefined,
185
+ onPressOut: undefined,
186
+ onLongPress: undefined,
187
+ onClick: undefined,
188
+ onKeyDown: undefined,
189
+ onKeyUp: undefined,
190
+ }),
191
+ children: (
192
+ <ButtonNestingContext.Provider value={true}>
193
+ {themedIcon}
194
+ {wrappedChildren}
195
+ {themedIconAfter}
196
+ </ButtonNestingContext.Provider>
197
+ ),
326
198
  }
327
199
 
328
- const buttonContext: ButtonContextStyles = {
329
- ...styledContext,
330
- ...textContext,
331
- size: props.size ?? propsIn.size ?? styledContext?.size,
332
- variant: props.variant ?? propsIn.variant ?? styledContext?.variant,
333
- elevation: props.elevation ?? propsIn.elevation ?? styledContext?.elevation,
200
+ return {
201
+ isNested,
202
+ props: resolvedProps,
334
203
  }
204
+ }
335
205
 
336
- return (
337
- <ButtonNestingContext.Provider value={true}>
338
- <context.Provider {...buttonContext}>
339
- <Frame
340
- ref={ref}
341
- {...props}
342
- {...(isNested && { render: 'span' })}
343
- // pass resolved size to circular variant when no explicit size provided
344
- {...(props.circular && !propsIn.size && { size })}
345
- tabIndex={0}
346
- >
347
- {themedIcon}
348
- {wrappedChildren}
349
- {themedIconAfter}
350
- </Frame>
351
- </context.Provider>
352
- </ButtonNestingContext.Provider>
353
- )
354
- })
355
-
356
- export const Button = withStaticProperties(ButtonComponent, {
357
- Apply: context.Provider,
358
- Frame,
359
- Text,
360
- Icon,
361
- })
362
-
363
- export type ButtonProps = GetProps<typeof ButtonComponent>
206
+ export type ButtonFrameProps = GetProps<typeof ButtonFrame>