@tamagui/switch 1.57.8 → 1.58.2
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/dist/cjs/Switch.js +19 -219
- package/dist/cjs/Switch.js.map +2 -2
- package/dist/cjs/createSwitch.js +257 -0
- package/dist/cjs/createSwitch.js.map +6 -0
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/Switch.js +19 -215
- package/dist/esm/Switch.js.map +2 -2
- package/dist/esm/createSwitch.js +232 -0
- package/dist/esm/createSwitch.js.map +6 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/jsx/Switch.js +19 -194
- package/dist/jsx/Switch.js.map +2 -2
- package/dist/jsx/createSwitch.js +219 -0
- package/dist/jsx/createSwitch.js.map +6 -0
- package/dist/jsx/index.js +1 -0
- package/dist/jsx/index.js.map +1 -1
- package/package.json +11 -11
- package/src/Switch.tsx +20 -284
- package/src/createSwitch.tsx +304 -0
- package/src/index.ts +1 -0
- package/types/Switch.d.ts +46 -191
- package/types/Switch.d.ts.map +1 -1
- package/types/createSwitch.d.ts +41 -0
- package/types/createSwitch.d.ts.map +1 -0
- package/types/index.d.ts +1 -0
- package/types/index.d.ts.map +1 -1
package/src/Switch.tsx
CHANGED
|
@@ -1,58 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
// https://github.com/radix-ui/primitives/blob/main/packages/react/switch/src/Switch.tsx
|
|
3
|
-
|
|
4
|
-
import { useComposedRefs } from '@tamagui/compose-refs'
|
|
5
|
-
import {
|
|
6
|
-
GetProps,
|
|
7
|
-
GetRef,
|
|
8
|
-
NativeValue,
|
|
9
|
-
SizeTokens,
|
|
10
|
-
getVariableValue,
|
|
11
|
-
isWeb,
|
|
12
|
-
styled,
|
|
13
|
-
withStaticProperties,
|
|
14
|
-
} from '@tamagui/core'
|
|
15
|
-
import { ScopedProps, createContextScope } from '@tamagui/create-context'
|
|
16
|
-
import { registerFocusable } from '@tamagui/focusable'
|
|
1
|
+
import { SizeTokens, getVariableValue, styled } from '@tamagui/core'
|
|
17
2
|
import { getSize } from '@tamagui/get-token'
|
|
18
|
-
import { useLabelContext } from '@tamagui/label'
|
|
19
3
|
import { ThemeableStack, XStack } from '@tamagui/stacks'
|
|
20
|
-
import { useControllableState } from '@tamagui/use-controllable-state'
|
|
21
|
-
import { usePrevious } from '@tamagui/use-previous'
|
|
22
|
-
import * as React from 'react'
|
|
23
|
-
import {
|
|
24
|
-
Switch as NativeSwitch,
|
|
25
|
-
SwitchProps as NativeSwitchProps,
|
|
26
|
-
Platform,
|
|
27
|
-
View,
|
|
28
|
-
} from 'react-native'
|
|
29
|
-
|
|
30
|
-
const SWITCH_NAME = 'Switch'
|
|
31
|
-
|
|
32
|
-
// TODO make customizable
|
|
33
|
-
const getSwitchHeight = (val: SizeTokens) =>
|
|
34
|
-
Math.round(getVariableValue(getSize(val)) * 0.65)
|
|
35
|
-
const getSwitchWidth = (val: SizeTokens) => getSwitchHeight(val) * 2
|
|
36
|
-
|
|
37
|
-
const scopeContexts = createContextScope(SWITCH_NAME)
|
|
38
|
-
const [createSwitchContext] = scopeContexts
|
|
39
|
-
export const createSwitchScope = scopeContexts[1]
|
|
40
4
|
|
|
41
|
-
|
|
42
|
-
checked: boolean
|
|
43
|
-
disabled?: boolean
|
|
44
|
-
size: SizeTokens
|
|
45
|
-
unstyled?: boolean
|
|
46
|
-
}>(SWITCH_NAME)
|
|
5
|
+
import { SwitchContext, createSwitch } from './createSwitch'
|
|
47
6
|
|
|
48
|
-
|
|
49
|
-
* SwitchThumb
|
|
50
|
-
* -----------------------------------------------------------------------------------------------*/
|
|
51
|
-
|
|
52
|
-
const THUMB_NAME = 'SwitchThumb'
|
|
53
|
-
|
|
54
|
-
export const SwitchThumbFrame = styled(ThemeableStack, {
|
|
7
|
+
export const SwitchThumb = styled(ThemeableStack, {
|
|
55
8
|
name: 'SwitchThumb',
|
|
9
|
+
context: SwitchContext,
|
|
56
10
|
|
|
57
11
|
variants: {
|
|
58
12
|
unstyled: {
|
|
@@ -79,46 +33,15 @@ export const SwitchThumbFrame = styled(ThemeableStack, {
|
|
|
79
33
|
},
|
|
80
34
|
})
|
|
81
35
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
export const SwitchThumb = SwitchThumbFrame.extractable(
|
|
85
|
-
React.forwardRef<GetRef<typeof SwitchThumbFrame>, SwitchThumbProps>(
|
|
86
|
-
function SwitchThumb(props: ScopedProps<SwitchThumbProps, 'Switch'>, forwardedRef) {
|
|
87
|
-
const { __scopeSwitch, size: sizeProp, ...thumbProps } = props
|
|
88
|
-
const {
|
|
89
|
-
size: sizeContext,
|
|
90
|
-
disabled,
|
|
91
|
-
checked,
|
|
92
|
-
unstyled,
|
|
93
|
-
} = useSwitchContext(THUMB_NAME, __scopeSwitch)
|
|
94
|
-
const size = sizeProp ?? sizeContext
|
|
95
|
-
return (
|
|
96
|
-
<SwitchThumbFrame
|
|
97
|
-
unstyled={unstyled}
|
|
98
|
-
size={size}
|
|
99
|
-
data-state={getState(checked)}
|
|
100
|
-
data-disabled={disabled ? '' : undefined}
|
|
101
|
-
x={
|
|
102
|
-
checked
|
|
103
|
-
? getVariableValue(getSwitchWidth(size)) -
|
|
104
|
-
getVariableValue(getSwitchHeight(size))
|
|
105
|
-
: 0
|
|
106
|
-
}
|
|
107
|
-
{...thumbProps}
|
|
108
|
-
ref={forwardedRef}
|
|
109
|
-
/>
|
|
110
|
-
)
|
|
111
|
-
}
|
|
112
|
-
)
|
|
113
|
-
)
|
|
36
|
+
const getSwitchHeight = (val: SizeTokens) =>
|
|
37
|
+
Math.round(getVariableValue(getSize(val)) * 0.65)
|
|
114
38
|
|
|
115
|
-
|
|
116
|
-
* Switch
|
|
117
|
-
* -----------------------------------------------------------------------------------------------*/
|
|
39
|
+
const getSwitchWidth = (val: SizeTokens) => getSwitchHeight(val) * 2
|
|
118
40
|
|
|
119
41
|
export const SwitchFrame = styled(XStack, {
|
|
120
|
-
name:
|
|
42
|
+
name: 'Switch',
|
|
121
43
|
tag: 'button',
|
|
44
|
+
context: SwitchContext,
|
|
122
45
|
|
|
123
46
|
variants: {
|
|
124
47
|
unstyled: {
|
|
@@ -138,6 +61,14 @@ export const SwitchFrame = styled(XStack, {
|
|
|
138
61
|
},
|
|
139
62
|
},
|
|
140
63
|
|
|
64
|
+
checked: {
|
|
65
|
+
true: {},
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
frameWidth: {
|
|
69
|
+
':number': () => null,
|
|
70
|
+
},
|
|
71
|
+
|
|
141
72
|
size: {
|
|
142
73
|
'...size': (val) => {
|
|
143
74
|
const height = getSwitchHeight(val) + 4
|
|
@@ -156,203 +87,8 @@ export const SwitchFrame = styled(XStack, {
|
|
|
156
87
|
},
|
|
157
88
|
})
|
|
158
89
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
export type SwitchProps = SwitchButtonProps & {
|
|
162
|
-
labeledBy?: string
|
|
163
|
-
name?: string
|
|
164
|
-
value?: string
|
|
165
|
-
checked?: boolean
|
|
166
|
-
defaultChecked?: boolean
|
|
167
|
-
required?: boolean
|
|
168
|
-
native?: NativeValue<'mobile' | 'ios' | 'android'>
|
|
169
|
-
nativeProps?: NativeSwitchProps
|
|
170
|
-
onCheckedChange?(checked: boolean): void
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const SwitchComponent = SwitchFrame.extractable(
|
|
174
|
-
React.forwardRef<HTMLButtonElement | View, SwitchProps>(
|
|
175
|
-
(props: ScopedProps<SwitchProps, 'Switch'>, forwardedRef) => {
|
|
176
|
-
const {
|
|
177
|
-
__scopeSwitch,
|
|
178
|
-
labeledBy: ariaLabelledby,
|
|
179
|
-
name,
|
|
180
|
-
checked: checkedProp,
|
|
181
|
-
defaultChecked,
|
|
182
|
-
required,
|
|
183
|
-
disabled,
|
|
184
|
-
value = 'on',
|
|
185
|
-
onCheckedChange,
|
|
186
|
-
size = '$true',
|
|
187
|
-
unstyled = false,
|
|
188
|
-
native: nativeProp,
|
|
189
|
-
nativeProps,
|
|
190
|
-
...switchProps
|
|
191
|
-
} = props
|
|
192
|
-
const native = Array.isArray(nativeProp) ? nativeProp : [nativeProp]
|
|
193
|
-
const shouldRenderMobileNative =
|
|
194
|
-
(!isWeb && nativeProp === true) ||
|
|
195
|
-
(!isWeb && native.includes('mobile')) ||
|
|
196
|
-
(native.includes('android') && Platform.OS === 'android') ||
|
|
197
|
-
(native.includes('ios') && Platform.OS === 'ios')
|
|
198
|
-
if (shouldRenderMobileNative) {
|
|
199
|
-
return (
|
|
200
|
-
<NativeSwitch
|
|
201
|
-
value={checkedProp}
|
|
202
|
-
onValueChange={onCheckedChange}
|
|
203
|
-
{...nativeProps}
|
|
204
|
-
/>
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
const [button, setButton] = React.useState<HTMLButtonElement | null>(null)
|
|
208
|
-
const composedRefs = useComposedRefs(forwardedRef, (node) => setButton(node as any))
|
|
209
|
-
const labelId = useLabelContext(button)
|
|
210
|
-
const labelledBy = ariaLabelledby || labelId
|
|
211
|
-
const hasConsumerStoppedPropagationRef = React.useRef(false)
|
|
212
|
-
// We set this to true by default so that events bubble to forms without JS (SSR)
|
|
213
|
-
const isFormControl = isWeb
|
|
214
|
-
? button
|
|
215
|
-
? Boolean(button.closest('form'))
|
|
216
|
-
: true
|
|
217
|
-
: false
|
|
218
|
-
const [checked = false, setChecked] = useControllableState({
|
|
219
|
-
prop: checkedProp,
|
|
220
|
-
defaultProp: defaultChecked || false,
|
|
221
|
-
onChange: onCheckedChange,
|
|
222
|
-
transition: true,
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
if (!isWeb) {
|
|
226
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
227
|
-
React.useEffect(() => {
|
|
228
|
-
if (!props.id) return
|
|
229
|
-
return registerFocusable(props.id, {
|
|
230
|
-
focus: () => {
|
|
231
|
-
setChecked((x) => !x)
|
|
232
|
-
},
|
|
233
|
-
})
|
|
234
|
-
}, [props.id, setChecked])
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return (
|
|
238
|
-
<SwitchProvider
|
|
239
|
-
scope={__scopeSwitch}
|
|
240
|
-
checked={checked}
|
|
241
|
-
disabled={disabled}
|
|
242
|
-
size={size}
|
|
243
|
-
unstyled={unstyled}
|
|
244
|
-
>
|
|
245
|
-
<SwitchFrame
|
|
246
|
-
unstyled={unstyled}
|
|
247
|
-
size={size}
|
|
248
|
-
theme={checked ? 'active' : null}
|
|
249
|
-
// @ts-ignore
|
|
250
|
-
role="switch"
|
|
251
|
-
aria-checked={checked}
|
|
252
|
-
aria-labelledby={labelledBy}
|
|
253
|
-
aria-required={required}
|
|
254
|
-
data-state={getState(checked)}
|
|
255
|
-
data-disabled={disabled ? '' : undefined}
|
|
256
|
-
disabled={disabled}
|
|
257
|
-
// @ts-ignore
|
|
258
|
-
tabIndex={disabled ? undefined : 0}
|
|
259
|
-
// @ts-ignore
|
|
260
|
-
value={value}
|
|
261
|
-
{...switchProps}
|
|
262
|
-
ref={composedRefs}
|
|
263
|
-
onPress={(event) => {
|
|
264
|
-
props.onPress?.(event)
|
|
265
|
-
setChecked((prevChecked) => !prevChecked)
|
|
266
|
-
if (isWeb && isFormControl) {
|
|
267
|
-
hasConsumerStoppedPropagationRef.current = event.isPropagationStopped()
|
|
268
|
-
// if switch is in a form, stop propagation from the button so that we only propagate
|
|
269
|
-
// one click event (from the input). We propagate changes from an input so that native
|
|
270
|
-
// form validation works and form events reflect switch updates.
|
|
271
|
-
if (!hasConsumerStoppedPropagationRef.current) event.stopPropagation()
|
|
272
|
-
}
|
|
273
|
-
}}
|
|
274
|
-
/>
|
|
275
|
-
{isWeb && isFormControl && (
|
|
276
|
-
<BubbleInput
|
|
277
|
-
control={button}
|
|
278
|
-
bubbles={!hasConsumerStoppedPropagationRef.current}
|
|
279
|
-
name={name}
|
|
280
|
-
value={value}
|
|
281
|
-
checked={checked}
|
|
282
|
-
required={required}
|
|
283
|
-
disabled={disabled}
|
|
284
|
-
// We transform because the input is absolutely positioned but we have
|
|
285
|
-
// rendered it **after** the button. This pulls it back to sit on top
|
|
286
|
-
// of the button.
|
|
287
|
-
style={{ transform: 'translateX(-100%)' }}
|
|
288
|
-
/>
|
|
289
|
-
)}
|
|
290
|
-
</SwitchProvider>
|
|
291
|
-
)
|
|
292
|
-
}
|
|
293
|
-
),
|
|
294
|
-
{
|
|
295
|
-
// because they may set a variant to be checked, we need to make it also pass checked down
|
|
296
|
-
inlineProps: new Set(['checked']),
|
|
297
|
-
}
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
export const Switch = withStaticProperties(SwitchComponent, {
|
|
90
|
+
export const Switch = createSwitch({
|
|
91
|
+
Frame: SwitchFrame,
|
|
301
92
|
Thumb: SwitchThumb,
|
|
93
|
+
acceptsUnstyled: true,
|
|
302
94
|
})
|
|
303
|
-
|
|
304
|
-
/* ---------------------------------------------------------------------------------------------- */
|
|
305
|
-
|
|
306
|
-
type InputProps = any //Radix.ComponentPropsWithoutRef<'input'>
|
|
307
|
-
interface BubbleInputProps extends Omit<InputProps, 'checked'> {
|
|
308
|
-
checked: boolean
|
|
309
|
-
control: HTMLElement | null
|
|
310
|
-
bubbles: boolean
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// TODO make this native friendly
|
|
314
|
-
const BubbleInput = (props: BubbleInputProps) => {
|
|
315
|
-
const { control, checked, bubbles = true, ...inputProps } = props
|
|
316
|
-
const ref = React.useRef<HTMLInputElement>(null)
|
|
317
|
-
const prevChecked = usePrevious(checked)
|
|
318
|
-
// const controlSize = useSize(control)
|
|
319
|
-
|
|
320
|
-
// Bubble checked change to parents (e.g form change event)
|
|
321
|
-
React.useEffect(() => {
|
|
322
|
-
const input = ref.current!
|
|
323
|
-
const inputProto = window.HTMLInputElement.prototype
|
|
324
|
-
const descriptor = Object.getOwnPropertyDescriptor(
|
|
325
|
-
inputProto,
|
|
326
|
-
'checked'
|
|
327
|
-
) as PropertyDescriptor
|
|
328
|
-
const setChecked = descriptor.set
|
|
329
|
-
if (prevChecked !== checked && setChecked) {
|
|
330
|
-
const event = new Event('click', { bubbles })
|
|
331
|
-
setChecked.call(input, checked)
|
|
332
|
-
input.dispatchEvent(event)
|
|
333
|
-
}
|
|
334
|
-
}, [prevChecked, checked, bubbles])
|
|
335
|
-
|
|
336
|
-
return (
|
|
337
|
-
<input
|
|
338
|
-
type="checkbox"
|
|
339
|
-
aria-hidden
|
|
340
|
-
defaultChecked={checked}
|
|
341
|
-
{...inputProps}
|
|
342
|
-
tabIndex={-1}
|
|
343
|
-
ref={ref}
|
|
344
|
-
style={{
|
|
345
|
-
...props.style,
|
|
346
|
-
// ...controlSize,
|
|
347
|
-
position: 'absolute',
|
|
348
|
-
pointerEvents: 'none',
|
|
349
|
-
opacity: 0,
|
|
350
|
-
margin: 0,
|
|
351
|
-
}}
|
|
352
|
-
/>
|
|
353
|
-
)
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function getState(checked: boolean) {
|
|
357
|
-
return checked ? 'checked' : 'unchecked'
|
|
358
|
-
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { useComposedRefs } from '@tamagui/compose-refs'
|
|
2
|
+
import {
|
|
3
|
+
NativeValue,
|
|
4
|
+
SizeTokens,
|
|
5
|
+
StackProps,
|
|
6
|
+
TamaguiComponentExpectingVariants,
|
|
7
|
+
TamaguiElement,
|
|
8
|
+
composeEventHandlers,
|
|
9
|
+
createStyledContext,
|
|
10
|
+
getVariableValue,
|
|
11
|
+
isWeb,
|
|
12
|
+
useProps,
|
|
13
|
+
withStaticProperties,
|
|
14
|
+
} from '@tamagui/core'
|
|
15
|
+
import { registerFocusable } from '@tamagui/focusable'
|
|
16
|
+
import { getSize } from '@tamagui/get-token'
|
|
17
|
+
import { useLabelContext } from '@tamagui/label'
|
|
18
|
+
import { useControllableState } from '@tamagui/use-controllable-state'
|
|
19
|
+
import { usePrevious } from '@tamagui/use-previous'
|
|
20
|
+
import * as React from 'react'
|
|
21
|
+
import {
|
|
22
|
+
Switch as NativeSwitch,
|
|
23
|
+
SwitchProps as NativeSwitchProps,
|
|
24
|
+
Platform,
|
|
25
|
+
} from 'react-native'
|
|
26
|
+
|
|
27
|
+
export const SwitchContext = createStyledContext<{
|
|
28
|
+
checked: boolean
|
|
29
|
+
disabled?: boolean
|
|
30
|
+
frameWidth: number
|
|
31
|
+
size?: SizeTokens
|
|
32
|
+
unstyled?: boolean
|
|
33
|
+
}>({
|
|
34
|
+
checked: false,
|
|
35
|
+
disabled: false,
|
|
36
|
+
size: undefined,
|
|
37
|
+
frameWidth: 60,
|
|
38
|
+
unstyled: false,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
type SwitchSharedProps = {
|
|
42
|
+
size?: SizeTokens | number
|
|
43
|
+
unstyled?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type SwitchBaseProps = StackProps & SwitchSharedProps
|
|
47
|
+
|
|
48
|
+
export type SwitchExtraProps = {
|
|
49
|
+
labeledBy?: string
|
|
50
|
+
name?: string
|
|
51
|
+
value?: string
|
|
52
|
+
checked?: boolean
|
|
53
|
+
defaultChecked?: boolean
|
|
54
|
+
required?: boolean
|
|
55
|
+
native?: NativeValue<'mobile' | 'ios' | 'android'>
|
|
56
|
+
nativeProps?: NativeSwitchProps
|
|
57
|
+
onCheckedChange?(checked: boolean): void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type SwitchProps = SwitchBaseProps & SwitchExtraProps
|
|
61
|
+
|
|
62
|
+
export function createSwitch<
|
|
63
|
+
F extends TamaguiComponentExpectingVariants<
|
|
64
|
+
SwitchProps,
|
|
65
|
+
SwitchSharedProps & SwitchExtraProps
|
|
66
|
+
>,
|
|
67
|
+
T extends TamaguiComponentExpectingVariants<SwitchBaseProps, SwitchSharedProps>
|
|
68
|
+
>({ Frame, Thumb, acceptsUnstyled }: { Frame: F; Thumb: T; acceptsUnstyled?: boolean }) {
|
|
69
|
+
const SwitchThumb = Thumb.styleable(function SwitchThumb(props, forwardedRef) {
|
|
70
|
+
const { size: sizeProp, ...thumbProps } = props
|
|
71
|
+
const { disabled, checked, unstyled, frameWidth } = React.useContext(SwitchContext)
|
|
72
|
+
const [thumbWidth, setThumbWidth] = React.useState(0)
|
|
73
|
+
return (
|
|
74
|
+
// @ts-ignore
|
|
75
|
+
<Thumb
|
|
76
|
+
theme={unstyled === false && checked ? 'active' : null}
|
|
77
|
+
data-state={getState(checked)}
|
|
78
|
+
data-disabled={disabled ? '' : undefined}
|
|
79
|
+
x={checked ? frameWidth - thumbWidth : 0}
|
|
80
|
+
{...thumbProps}
|
|
81
|
+
// @ts-ignore
|
|
82
|
+
onLayout={composeEventHandlers(props.onLayout, (e) =>
|
|
83
|
+
// @ts-ignore
|
|
84
|
+
setThumbWidth(e.nativeEvent.layout.width)
|
|
85
|
+
)}
|
|
86
|
+
ref={forwardedRef}
|
|
87
|
+
/>
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const SwitchComponent = Frame.extractable(
|
|
92
|
+
React.forwardRef<TamaguiElement, SwitchProps>(function SwitchFrame(
|
|
93
|
+
propsIn,
|
|
94
|
+
forwardedRef
|
|
95
|
+
) {
|
|
96
|
+
const styledContext = React.useContext(SwitchContext)
|
|
97
|
+
const props = useProps(propsIn)
|
|
98
|
+
const {
|
|
99
|
+
labeledBy: ariaLabelledby,
|
|
100
|
+
name,
|
|
101
|
+
checked: checkedProp,
|
|
102
|
+
defaultChecked,
|
|
103
|
+
required,
|
|
104
|
+
disabled,
|
|
105
|
+
value = 'on',
|
|
106
|
+
onCheckedChange,
|
|
107
|
+
size = styledContext.size ?? '$true',
|
|
108
|
+
unstyled = styledContext.unstyled ?? false,
|
|
109
|
+
native: nativeProp,
|
|
110
|
+
nativeProps,
|
|
111
|
+
...switchProps
|
|
112
|
+
} = props
|
|
113
|
+
|
|
114
|
+
const leftBorderWidth = (() => {
|
|
115
|
+
let _: any = undefined
|
|
116
|
+
for (const key in switchProps) {
|
|
117
|
+
if (key === 'borderWidth' || key === 'borderLeftWidth') {
|
|
118
|
+
_ = switchProps[key]
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (acceptsUnstyled && _ === undefined && unstyled === false) {
|
|
122
|
+
_ = 2 // default we use for styled
|
|
123
|
+
}
|
|
124
|
+
if (typeof _ === 'string') {
|
|
125
|
+
_ = getVariableValue(getSize(_))
|
|
126
|
+
}
|
|
127
|
+
return +_
|
|
128
|
+
})()
|
|
129
|
+
|
|
130
|
+
const native = Array.isArray(nativeProp) ? nativeProp : [nativeProp]
|
|
131
|
+
|
|
132
|
+
const shouldRenderMobileNative =
|
|
133
|
+
(!isWeb && nativeProp === true) ||
|
|
134
|
+
(!isWeb && native.includes('mobile')) ||
|
|
135
|
+
(native.includes('android') && Platform.OS === 'android') ||
|
|
136
|
+
(native.includes('ios') && Platform.OS === 'ios')
|
|
137
|
+
|
|
138
|
+
const [button, setButton] = React.useState<HTMLButtonElement | null>(null)
|
|
139
|
+
const composedRefs = useComposedRefs(forwardedRef, (node) => setButton(node as any))
|
|
140
|
+
const labelId = useLabelContext(button)
|
|
141
|
+
const labelledBy = ariaLabelledby || labelId
|
|
142
|
+
const hasConsumerStoppedPropagationRef = React.useRef(false)
|
|
143
|
+
// We set this to true by default so that events bubble to forms without JS (SSR)
|
|
144
|
+
const isFormControl = isWeb
|
|
145
|
+
? button
|
|
146
|
+
? Boolean(button.closest('form'))
|
|
147
|
+
: true
|
|
148
|
+
: false
|
|
149
|
+
|
|
150
|
+
// just guess some value
|
|
151
|
+
const [frameWidth, setFrameWidth] = React.useState(60)
|
|
152
|
+
|
|
153
|
+
const [checked = false, setChecked] = useControllableState({
|
|
154
|
+
prop: checkedProp,
|
|
155
|
+
defaultProp: defaultChecked || false,
|
|
156
|
+
onChange: onCheckedChange,
|
|
157
|
+
transition: true,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
if (shouldRenderMobileNative) {
|
|
161
|
+
return (
|
|
162
|
+
<NativeSwitch
|
|
163
|
+
value={checkedProp}
|
|
164
|
+
onValueChange={onCheckedChange}
|
|
165
|
+
{...nativeProps}
|
|
166
|
+
/>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!isWeb) {
|
|
171
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
172
|
+
React.useEffect(() => {
|
|
173
|
+
if (!props.id) return
|
|
174
|
+
return registerFocusable(props.id, {
|
|
175
|
+
focus: () => {
|
|
176
|
+
setChecked((x) => !x)
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
}, [props.id, setChecked])
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<>
|
|
184
|
+
{/* @ts-ignore */}
|
|
185
|
+
<Frame
|
|
186
|
+
unstyled={unstyled}
|
|
187
|
+
size={size}
|
|
188
|
+
checked={checked}
|
|
189
|
+
disabled={disabled}
|
|
190
|
+
frameWidth={frameWidth - leftBorderWidth * 2}
|
|
191
|
+
theme={checked ? 'active' : null}
|
|
192
|
+
themeShallow
|
|
193
|
+
role="switch"
|
|
194
|
+
aria-checked={checked}
|
|
195
|
+
aria-labelledby={labelledBy}
|
|
196
|
+
aria-required={required}
|
|
197
|
+
data-state={getState(checked)}
|
|
198
|
+
data-disabled={disabled ? '' : undefined}
|
|
199
|
+
// @ts-ignore
|
|
200
|
+
tabIndex={disabled ? undefined : 0}
|
|
201
|
+
// @ts-ignore
|
|
202
|
+
value={value}
|
|
203
|
+
{...switchProps}
|
|
204
|
+
ref={composedRefs}
|
|
205
|
+
onPress={composeEventHandlers(props.onPress, (event) => {
|
|
206
|
+
setChecked((prevChecked) => !prevChecked)
|
|
207
|
+
if (isWeb && isFormControl) {
|
|
208
|
+
hasConsumerStoppedPropagationRef.current = event.isPropagationStopped()
|
|
209
|
+
// if switch is in a form, stop propagation from the button so that we only propagate
|
|
210
|
+
// one click event (from the input). We propagate changes from an input so that native
|
|
211
|
+
// form validation works and form events reflect switch updates.
|
|
212
|
+
if (!hasConsumerStoppedPropagationRef.current) event.stopPropagation()
|
|
213
|
+
}
|
|
214
|
+
})}
|
|
215
|
+
// @ts-ignore
|
|
216
|
+
onLayout={composeEventHandlers(props.onLayout, (e) =>
|
|
217
|
+
// @ts-ignore
|
|
218
|
+
setFrameWidth(e.nativeEvent.layout.width)
|
|
219
|
+
)}
|
|
220
|
+
/>
|
|
221
|
+
{isWeb && isFormControl && (
|
|
222
|
+
<BubbleInput
|
|
223
|
+
control={button}
|
|
224
|
+
bubbles={!hasConsumerStoppedPropagationRef.current}
|
|
225
|
+
name={name}
|
|
226
|
+
value={value}
|
|
227
|
+
checked={checked}
|
|
228
|
+
required={required}
|
|
229
|
+
disabled={disabled}
|
|
230
|
+
// We transform because the input is absolutely positioned but we have
|
|
231
|
+
// rendered it **after** the button. This pulls it back to sit on top
|
|
232
|
+
// of the button.
|
|
233
|
+
style={{ transform: 'translateX(-100%)' }}
|
|
234
|
+
/>
|
|
235
|
+
)}
|
|
236
|
+
</>
|
|
237
|
+
)
|
|
238
|
+
})
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
242
|
+
|
|
243
|
+
type InputProps = React.HTMLProps<'input'>
|
|
244
|
+
|
|
245
|
+
interface BubbleInputProps extends Omit<InputProps, 'checked'> {
|
|
246
|
+
checked: boolean
|
|
247
|
+
control: HTMLElement | null
|
|
248
|
+
bubbles: boolean
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// TODO make this native friendly
|
|
252
|
+
const BubbleInput = (props: BubbleInputProps) => {
|
|
253
|
+
const { control, checked, bubbles = true, ...inputProps } = props
|
|
254
|
+
const ref = React.useRef<HTMLInputElement>(null)
|
|
255
|
+
const prevChecked = usePrevious(checked)
|
|
256
|
+
// const controlSize = useSize(control)
|
|
257
|
+
|
|
258
|
+
// Bubble checked change to parents (e.g form change event)
|
|
259
|
+
React.useEffect(() => {
|
|
260
|
+
const input = ref.current!
|
|
261
|
+
const inputProto = window.HTMLInputElement.prototype
|
|
262
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
263
|
+
inputProto,
|
|
264
|
+
'checked'
|
|
265
|
+
) as PropertyDescriptor
|
|
266
|
+
const setChecked = descriptor.set
|
|
267
|
+
if (prevChecked !== checked && setChecked) {
|
|
268
|
+
const event = new Event('click', { bubbles })
|
|
269
|
+
setChecked.call(input, checked)
|
|
270
|
+
input.dispatchEvent(event)
|
|
271
|
+
}
|
|
272
|
+
}, [prevChecked, checked, bubbles])
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
// @ts-ignore
|
|
276
|
+
<input
|
|
277
|
+
type="checkbox"
|
|
278
|
+
aria-hidden
|
|
279
|
+
defaultChecked={checked}
|
|
280
|
+
{...inputProps}
|
|
281
|
+
tabIndex={-1}
|
|
282
|
+
ref={ref}
|
|
283
|
+
style={{
|
|
284
|
+
...props.style,
|
|
285
|
+
// ...controlSize,
|
|
286
|
+
position: 'absolute',
|
|
287
|
+
pointerEvents: 'none',
|
|
288
|
+
opacity: 0,
|
|
289
|
+
margin: 0,
|
|
290
|
+
}}
|
|
291
|
+
/>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getState(checked: boolean) {
|
|
296
|
+
return checked ? 'checked' : 'unchecked'
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const Switch = withStaticProperties(SwitchComponent, {
|
|
300
|
+
Thumb: SwitchThumb,
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
return Switch
|
|
304
|
+
}
|
package/src/index.ts
CHANGED