@tamagui/button 1.142.0 → 2.0.0-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 (81) hide show
  1. package/dist/cjs/Button.cjs +126 -158
  2. package/dist/cjs/Button.js +102 -120
  3. package/dist/cjs/Button.js.map +2 -2
  4. package/dist/cjs/Button.native.js +128 -160
  5. package/dist/cjs/Button.native.js.map +1 -1
  6. package/dist/cjs/v1/Button.cjs +259 -0
  7. package/dist/cjs/v1/Button.js +208 -0
  8. package/dist/cjs/v1/Button.js.map +6 -0
  9. package/dist/cjs/v1/Button.native.js +265 -0
  10. package/dist/cjs/v1/Button.native.js.map +1 -0
  11. package/dist/cjs/v1/Button.test.cjs +9 -0
  12. package/dist/cjs/v1/Button.test.js +8 -0
  13. package/dist/cjs/v1/Button.test.js.map +6 -0
  14. package/dist/cjs/v1/Button.test.native.js +12 -0
  15. package/dist/cjs/v1/Button.test.native.js.map +1 -0
  16. package/dist/cjs/v1/index.cjs +18 -0
  17. package/dist/cjs/v1/index.js +15 -0
  18. package/dist/cjs/v1/index.js.map +6 -0
  19. package/dist/cjs/v1/index.native.js +21 -0
  20. package/dist/cjs/v1/index.native.js.map +1 -0
  21. package/dist/esm/Button.js +108 -126
  22. package/dist/esm/Button.js.map +2 -2
  23. package/dist/esm/Button.mjs +127 -155
  24. package/dist/esm/Button.mjs.map +1 -1
  25. package/dist/esm/Button.native.js +130 -158
  26. package/dist/esm/Button.native.js.map +1 -1
  27. package/dist/esm/v1/Button.js +201 -0
  28. package/dist/esm/v1/Button.js.map +6 -0
  29. package/dist/esm/v1/Button.mjs +231 -0
  30. package/dist/esm/v1/Button.mjs.map +1 -0
  31. package/dist/esm/v1/Button.native.js +234 -0
  32. package/dist/esm/v1/Button.native.js.map +1 -0
  33. package/dist/esm/v1/Button.test.js +10 -0
  34. package/dist/esm/v1/Button.test.js.map +6 -0
  35. package/dist/esm/v1/Button.test.mjs +10 -0
  36. package/dist/esm/v1/Button.test.mjs.map +1 -0
  37. package/dist/esm/v1/Button.test.native.js +10 -0
  38. package/dist/esm/v1/Button.test.native.js.map +1 -0
  39. package/dist/esm/v1/index.js +2 -0
  40. package/dist/esm/v1/index.js.map +6 -0
  41. package/dist/esm/v1/index.mjs +2 -0
  42. package/dist/esm/v1/index.mjs.map +1 -0
  43. package/dist/esm/v1/index.native.js +2 -0
  44. package/dist/esm/v1/index.native.js.map +1 -0
  45. package/dist/jsx/Button.js +108 -126
  46. package/dist/jsx/Button.js.map +2 -2
  47. package/dist/jsx/Button.mjs +127 -155
  48. package/dist/jsx/Button.mjs.map +1 -1
  49. package/dist/jsx/Button.native.js +128 -160
  50. package/dist/jsx/Button.native.js.map +1 -1
  51. package/dist/jsx/v1/Button.js +201 -0
  52. package/dist/jsx/v1/Button.js.map +6 -0
  53. package/dist/jsx/v1/Button.mjs +231 -0
  54. package/dist/jsx/v1/Button.mjs.map +1 -0
  55. package/dist/jsx/v1/Button.native.js +265 -0
  56. package/dist/jsx/v1/Button.native.js.map +1 -0
  57. package/dist/jsx/v1/Button.test.js +10 -0
  58. package/dist/jsx/v1/Button.test.js.map +6 -0
  59. package/dist/jsx/v1/Button.test.mjs +10 -0
  60. package/dist/jsx/v1/Button.test.mjs.map +1 -0
  61. package/dist/jsx/v1/Button.test.native.js +12 -0
  62. package/dist/jsx/v1/Button.test.native.js.map +1 -0
  63. package/dist/jsx/v1/index.js +2 -0
  64. package/dist/jsx/v1/index.js.map +6 -0
  65. package/dist/jsx/v1/index.mjs +2 -0
  66. package/dist/jsx/v1/index.mjs.map +1 -0
  67. package/dist/jsx/v1/index.native.js +21 -0
  68. package/dist/jsx/v1/index.native.js.map +1 -0
  69. package/package.json +12 -11
  70. package/src/Button.tsx +172 -243
  71. package/src/v1/Button.test.tsx +21 -0
  72. package/src/v1/Button.tsx +336 -0
  73. package/src/v1/index.ts +1 -0
  74. package/types/Button.d.ts +80 -323
  75. package/types/Button.d.ts.map +1 -1
  76. package/types/v1/Button.d.ts +338 -0
  77. package/types/v1/Button.d.ts.map +1 -0
  78. package/types/v1/Button.test.d.ts +2 -0
  79. package/types/v1/Button.test.d.ts.map +1 -0
  80. package/types/v1/index.d.ts +2 -0
  81. package/types/v1/index.d.ts.map +1 -0
package/src/Button.tsx CHANGED
@@ -1,94 +1,43 @@
1
1
  import { getFontSize } from '@tamagui/font-size'
2
2
  import { getButtonSized } from '@tamagui/get-button-sized'
3
- import { withStaticProperties } from '@tamagui/helpers'
4
- import { useGetThemedIcon } from '@tamagui/helpers-tamagui'
5
- import { ButtonNestingContext, ThemeableStack } from '@tamagui/stacks'
6
- import type { TextContextStyles, TextParentStyles } from '@tamagui/text'
3
+ import { ButtonNestingContext, getElevation, themeableVariants } from '@tamagui/stacks'
7
4
  import { SizableText, wrapChildrenInText } from '@tamagui/text'
8
- import type { FontSizeTokens, GetProps, SizeTokens, ThemeableProps } from '@tamagui/web'
5
+ import type { ColorTokens, GetProps, RNExtraProps, SizeTokens, Token } from '@tamagui/web'
9
6
  import {
10
7
  createStyledContext,
11
- getVariableValue,
12
- spacedChildren,
8
+ getTokenValue,
13
9
  styled,
14
10
  useProps,
11
+ View,
12
+ withStaticProperties,
15
13
  } from '@tamagui/web'
16
- import type { FunctionComponent, JSX } from 'react'
17
14
  import { useContext } from 'react'
15
+ import { useGetIcon } from '@tamagui/helpers-tamagui'
18
16
 
19
17
  type ButtonVariant = 'outlined'
20
18
 
21
- export const ButtonContext = createStyledContext<
22
- Partial<
23
- TextContextStyles & {
24
- size: SizeTokens
25
- variant?: ButtonVariant
26
- }
27
- >
28
- >({
29
- // keeping these here means they work with styled() passing down color to text
30
- color: undefined,
31
- ellipse: undefined,
32
- fontFamily: undefined,
33
- fontSize: undefined,
34
- fontStyle: undefined,
35
- fontWeight: undefined,
36
- letterSpacing: undefined,
37
- maxFontSizeMultiplier: undefined,
19
+ export type ButtonProps = GetProps<typeof Frame>
20
+
21
+ const context = createStyledContext<{
22
+ size?: SizeTokens
23
+ variant?: ButtonVariant
24
+ color?: ColorTokens | string
25
+ elevation?: SizeTokens | number
26
+ }>({
38
27
  size: undefined,
39
- textAlign: undefined,
40
28
  variant: undefined,
29
+ color: undefined,
30
+ elevation: undefined,
41
31
  })
42
32
 
43
- type ButtonIconProps = { color?: any; size?: any }
44
- type IconProp =
45
- | JSX.Element
46
- | FunctionComponent<ButtonIconProps>
47
- | ((props: ButtonIconProps) => any)
48
- | null
49
-
50
- type ButtonExtraProps = TextParentStyles &
51
- ThemeableProps & {
52
- /**
53
- * add icon before, passes color and size automatically if Component
54
- */
55
- icon?: IconProp
56
- /**
57
- * add icon after, passes color and size automatically if Component
58
- */
59
- iconAfter?: IconProp
60
- /**
61
- * adjust icon relative to size
62
- *
63
- * @default 1
64
- */
65
- scaleIcon?: number
66
- /**
67
- * make the spacing elements flex
68
- */
69
- spaceFlex?: number | boolean
70
- /**
71
- * adjust internal space relative to icon size
72
- */
73
- scaleSpace?: number
74
- /**
75
- * remove default styles
76
- */
77
- unstyled?: boolean
78
- }
79
-
80
- type ButtonProps = ButtonExtraProps & GetProps<typeof ButtonFrame>
81
-
82
- const BUTTON_NAME = 'Button'
83
-
84
- const ButtonFrame = styled(ThemeableStack, {
85
- name: BUTTON_NAME,
86
- tag: 'button',
87
- context: ButtonContext,
33
+ const Frame = styled(View, {
34
+ context,
35
+ name: 'Button',
36
+ group: 'Button' as any,
37
+ containerType: 'normal',
88
38
  role: 'button',
39
+ tag: 'button',
89
40
  focusable: true,
90
- // forces runtime pressStyle so it passes through context text colors
91
- disableClassName: true,
92
41
 
93
42
  variants: {
94
43
  unstyled: {
@@ -99,12 +48,20 @@ const ButtonFrame = styled(ThemeableStack, {
99
48
  flexWrap: 'nowrap',
100
49
  flexDirection: 'row',
101
50
  cursor: 'pointer',
102
- hoverTheme: true,
103
- pressTheme: true,
104
- backgrounded: true,
51
+ backgroundColor: '$background',
105
52
  borderWidth: 1,
106
53
  borderColor: 'transparent',
107
54
 
55
+ hoverStyle: {
56
+ backgroundColor: '$backgroundHover',
57
+ borderColor: '$borderColorHover',
58
+ },
59
+
60
+ pressStyle: {
61
+ backgroundColor: '$backgroundPress',
62
+ borderColor: '$borderColorHover',
63
+ },
64
+
108
65
  focusVisibleStyle: {
109
66
  outlineColor: '$outlineColor',
110
67
  outlineStyle: 'solid',
@@ -114,31 +71,62 @@ const ButtonFrame = styled(ThemeableStack, {
114
71
  },
115
72
 
116
73
  variant: {
117
- outlined: {
118
- backgroundColor: 'transparent',
119
- borderWidth: 2,
120
- borderColor: '$borderColor',
74
+ outlined:
75
+ process.env.TAMAGUI_HEADLESS === '1'
76
+ ? {}
77
+ : {
78
+ backgroundColor: 'transparent',
79
+ borderWidth: 2,
80
+ borderColor: '$borderColor',
81
+
82
+ hoverStyle: {
83
+ backgroundColor: 'transparent',
84
+ borderColor: '$borderColorHover',
85
+ },
86
+
87
+ pressStyle: {
88
+ backgroundColor: 'transparent',
89
+ borderColor: '$borderColorPress',
90
+ },
91
+
92
+ focusVisibleStyle: {
93
+ backgroundColor: 'transparent',
94
+ borderColor: '$borderColorFocus',
95
+ outlineColor: '$outlineColor',
96
+ outlineStyle: 'solid',
97
+ outlineWidth: 2,
98
+ },
99
+ },
100
+ },
121
101
 
122
- hoverStyle: {
123
- backgroundColor: 'transparent',
124
- borderColor: '$borderColorHover',
125
- },
102
+ circular: themeableVariants.circular,
126
103
 
127
- pressStyle: {
128
- backgroundColor: 'transparent',
129
- borderColor: '$borderColorPress',
130
- },
104
+ chromeless: themeableVariants.chromeless,
131
105
 
132
- focusVisibleStyle: {
133
- backgroundColor: 'transparent',
134
- borderColor: '$borderColorFocus',
135
- },
106
+ bordered: themeableVariants.bordered,
107
+
108
+ size: {
109
+ '...size': (val, extras) => {
110
+ const buttonStyle = getButtonSized(val, extras)
111
+ const gap = getTokenValue(val as Token) * 0.4
112
+ return {
113
+ ...buttonStyle,
114
+ gap,
115
+ }
116
+ },
117
+ ':number': (val, extras) => {
118
+ const buttonStyle = getButtonSized(val, extras)
119
+ const gap = val * 0.4
120
+ return {
121
+ ...buttonStyle,
122
+ gap,
123
+ }
136
124
  },
137
125
  },
138
126
 
139
- size: {
140
- '...size': getButtonSized,
141
- ':number': getButtonSized,
127
+ elevation: {
128
+ '...size': getElevation,
129
+ ':number': getElevation,
142
130
  },
143
131
 
144
132
  disabled: {
@@ -153,9 +141,8 @@ const ButtonFrame = styled(ThemeableStack, {
153
141
  },
154
142
  })
155
143
 
156
- const ButtonText = styled(SizableText, {
157
- name: 'Button',
158
- context: ButtonContext,
144
+ const Text = styled(SizableText, {
145
+ context,
159
146
 
160
147
  variants: {
161
148
  unstyled: {
@@ -165,7 +152,7 @@ const ButtonText = styled(SizableText, {
165
152
  // flexGrow 1 leads to inconsistent native style where text pushes to start of view
166
153
  flexGrow: 0,
167
154
  flexShrink: 1,
168
- ellipse: true,
155
+ ellipsis: true,
169
156
  color: '$color',
170
157
  },
171
158
  },
@@ -176,171 +163,113 @@ const ButtonText = styled(SizableText, {
176
163
  },
177
164
  })
178
165
 
179
- const ButtonIcon = (props: { children: React.ReactNode; scaleIcon?: number }) => {
180
- const { children, scaleIcon = 1 } = props
181
- const { size, color } = useContext(ButtonContext)
166
+ const Icon = (props: { children: React.ReactNode; scaleIcon?: number }) => {
167
+ const { children, scaleIcon = 1, marginLeft, marginRight, size } = props as any
168
+ const styledContext = context.useStyledContext()
169
+ if (!styledContext) {
170
+ throw new Error('Button.Icon must be used within a Button')
171
+ }
172
+ const getIcon = useGetIcon()
173
+
174
+ const sizeToken = size ?? styledContext.size
182
175
 
183
176
  const iconSize =
184
- (typeof size === 'number' ? size * 0.5 : getFontSize(size as FontSizeTokens)) *
177
+ (typeof sizeToken === 'number' ? sizeToken * 0.5 : getFontSize(sizeToken as Token)) *
185
178
  scaleIcon
186
179
 
187
- const getThemedIcon = useGetThemedIcon({ size: iconSize, color: color as any })
188
- return getThemedIcon(children)
180
+ return getIcon(children, {
181
+ size: iconSize,
182
+ color: styledContext.color,
183
+ marginLeft,
184
+ marginRight,
185
+ })
189
186
  }
190
187
 
191
- const ButtonComponent = ButtonFrame.styleable<ButtonExtraProps>(
192
- function Button(props, ref) {
193
- // @ts-ignore
194
- const { props: buttonProps } = useButton(props)
195
-
196
- return <ButtonFrame data-disable-theme {...buttonProps} ref={ref} />
197
- }
198
- )
199
- /**
200
- * @summary A Button is a clickable element that can be used to trigger actions such as submitting forms, navigating to other pages, or performing other actions.
201
- * @see — Docs https://tamagui.dev/ui/button
202
- */
203
- const Button = withStaticProperties(ButtonComponent, {
204
- Text: ButtonText,
205
- Icon: ButtonIcon,
188
+ export const ButtonContext = createStyledContext<{
189
+ size?: SizeTokens
190
+ variant?: ButtonVariant
191
+ color?: ColorTokens | string
192
+ }>({
193
+ size: undefined,
194
+ variant: undefined,
195
+ color: undefined,
206
196
  })
207
197
 
208
- /**
209
- * @deprecated Instead of useButton, see the Button docs for the newer and much improved Advanced customization pattern: https://tamagui.dev/docs/components/button
210
- */
211
- function useButton<Props extends ButtonProps>(
212
- { textProps, ...propsIn }: Props,
213
- { Text = Button.Text }: { Text: any } = { Text: Button.Text }
214
- ) {
198
+ const ButtonComponent = Frame.styleable<{
199
+ icon?: any
200
+ iconAfter?: any
201
+ scaleIcon?: number
202
+ iconSize?: SizeTokens
203
+ onLayout?: RNExtraProps['onLayout']
204
+ }>((propsIn: any, ref) => {
215
205
  const isNested = useContext(ButtonNestingContext)
216
- const propsActive = useProps(propsIn, {
206
+
207
+ // Process props through useProps to expand shorthands (like br -> borderRadius)
208
+ const processedProps = useProps(propsIn, {
217
209
  noNormalize: true,
218
210
  noExpand: true,
219
- }) as any as ButtonProps
211
+ })
220
212
 
221
- // careful not to destructure and re-order props, order is important
222
213
  const {
214
+ children,
215
+ iconSize,
223
216
  icon,
224
217
  iconAfter,
225
- space,
226
- spaceFlex,
227
218
  scaleIcon = 1,
228
- scaleSpace = 0.66,
229
- separator,
230
219
  noTextWrap,
231
- fontFamily,
232
- fontSize,
233
- fontWeight,
234
- fontStyle,
235
- letterSpacing,
236
- tag,
237
- ellipse,
238
- maxFontSizeMultiplier,
220
+ ...props
221
+ } = processedProps
239
222
 
240
- ...restProps
241
- } = propsActive
223
+ const size = propsIn.size || (propsIn.unstyled ? undefined : '$true')
242
224
 
243
- const size = propsActive.size || (propsActive.unstyled ? undefined : '$true')
244
-
245
- const color = propsActive.color as any
225
+ const styledContext = context.useStyledContext()
226
+ const finalSize = iconSize ?? size ?? styledContext?.size
227
+ const iconSizeNumber =
228
+ (typeof finalSize === 'number' ? finalSize * 0.5 : getFontSize(finalSize as Token)) *
229
+ scaleIcon
246
230
 
247
- const iconSize =
248
- (typeof size === 'number'
249
- ? size * 0.5
250
- : getFontSize(size as FontSizeTokens, {
251
- font: fontFamily?.[0] === '$' ? (fontFamily as any) : undefined,
252
- })) * scaleIcon
231
+ const getIcon = useGetIcon()
253
232
 
254
- const getThemedIcon = useGetThemedIcon({
255
- size: iconSize,
256
- color,
257
- })
258
-
259
- const [themedIcon, themedIconAfter] = [icon, iconAfter].map(getThemedIcon)
260
- const spaceSize = space ?? getVariableValue(iconSize) * scaleSpace
261
- const contents = noTextWrap
262
- ? [propsIn.children]
263
- : wrapChildrenInText(
264
- Text,
265
- {
266
- children: propsIn.children,
267
- color,
268
- fontFamily,
269
- fontSize,
270
- textProps,
271
- fontWeight,
272
- fontStyle,
273
- letterSpacing,
274
- ellipse,
275
- maxFontSizeMultiplier,
276
- },
277
- Text === ButtonText && propsActive.unstyled !== true
278
- ? {
279
- unstyled: process.env.TAMAGUI_HEADLESS === '1',
280
- size,
281
- }
282
- : undefined
283
- )
284
-
285
- const inner = spacedChildren({
286
- // a bit arbitrary but scaling to font size is necessary so long as button does
287
- space: spaceSize === false ? 0 : spaceSize == true ? '$true' : spaceSize,
288
- spaceFlex,
289
- ensureKeys: true,
290
- separator,
291
- direction:
292
- propsActive.flexDirection === 'column' ||
293
- propsActive.flexDirection === 'column-reverse'
294
- ? 'vertical'
295
- : 'horizontal',
296
- // for keys to stay the same we keep indices as similar a possible
297
- // so even if icons are undefined we still pass them
298
- children: [themedIcon, ...contents, themedIconAfter],
233
+ const [themedIcon, themedIconAfter] = [icon, iconAfter].map((icon) => {
234
+ if (!icon) return null
235
+ return getIcon(icon, {
236
+ size: iconSizeNumber,
237
+ color: styledContext?.color,
238
+ // No marginLeft or marginRight needed - spacing is handled by the gap property in Frame's size variants
239
+ })
299
240
  })
300
241
 
301
- const props = {
302
- size,
303
- ...(propsIn.disabled && {
304
- // in rnw - false still has keyboard tabIndex, undefined = not actually focusable
305
- focusable: undefined,
306
- // even with tabIndex unset, it will keep focusVisibleStyle on web so disable it here
307
- focusVisibleStyle: {
308
- borderColor: '$background',
309
- },
310
- }),
311
- // fixes SSR issue + DOM nesting issue of not allowing button in button
312
- tag:
313
- tag ??
314
- (isNested
315
- ? 'span'
316
- : // defaults to <a /> when accessibilityRole = link
317
- // see https://github.com/tamagui/tamagui/issues/505
318
- propsActive.accessibilityRole === 'link' || propsActive.role === 'link'
319
- ? 'a'
320
- : 'button'),
321
-
322
- ...restProps,
323
-
324
- children: (
325
- <ButtonNestingContext.Provider value={true}>{inner}</ButtonNestingContext.Provider>
326
- ),
327
- // forces it to be a runtime pressStyle so it passes through context text colors
328
- disableClassName: true,
329
- } as Props
330
-
331
- return {
332
- spaceSize,
333
- isNested,
334
- props,
335
- }
336
- }
242
+ const wrappedChildren = wrapChildrenInText(
243
+ Text,
244
+ { children, noTextWrap },
245
+ {
246
+ unstyled: process.env.TAMAGUI_HEADLESS === '1',
247
+ size: finalSize ?? styledContext?.size,
248
+ }
249
+ )
250
+
251
+ return (
252
+ <ButtonNestingContext.Provider value={true}>
253
+ <Frame
254
+ ref={ref}
255
+ {...props}
256
+ {...(isNested && { tag: 'span' })}
257
+ // Pass resolved size to circular variant when no explicit size provided
258
+ {...(props.circular && !propsIn.size && { size })}
259
+ tabIndex={0}
260
+ focusable={true}
261
+ >
262
+ {themedIcon}
263
+ {wrappedChildren}
264
+ {themedIconAfter}
265
+ </Frame>
266
+ </ButtonNestingContext.Provider>
267
+ )
268
+ })
337
269
 
338
- export {
339
- Button,
340
- ButtonFrame,
341
- ButtonIcon,
342
- ButtonText,
343
- // legacy
344
- useButton,
345
- }
346
- export type { ButtonProps }
270
+ export const Button = withStaticProperties(ButtonComponent, {
271
+ Apply: context.Provider,
272
+ Frame,
273
+ Text,
274
+ Icon,
275
+ })
@@ -0,0 +1,21 @@
1
+ import { getDefaultTamaguiConfig } from '@tamagui/config-default'
2
+ import { createTamagui } from '@tamagui/core'
3
+ import { describe, expect, test } from 'vitest'
4
+
5
+ const conf = createTamagui(getDefaultTamaguiConfig())
6
+
7
+ describe('Button', () => {
8
+ test(`123`, () => {
9
+ expect(true).toBeTruthy()
10
+ })
11
+
12
+ // test(`Adapts to a when given accessibilityRole="link"`, async () => {
13
+ // const { container } = render(
14
+ // <TamaguiProvider config={conf} defaultTheme="light">
15
+ // <Button href="http://google.com" accessibilityRole="link" />
16
+ // </TamaguiProvider>
17
+ // )
18
+
19
+ // expect(container.firstChild).toMatchSnapshot()
20
+ // })
21
+ })