diff --git a/docs/6.x/docs/guides/migration.md b/docs/6.x/docs/guides/migration.md index a4d7123a09..ac56958eae 100644 --- a/docs/6.x/docs/guides/migration.md +++ b/docs/6.x/docs/guides/migration.md @@ -279,3 +279,54 @@ const theme = { style={{ fontSize: 16, color: '#1C1B1F' }} /> ``` + +### Switch + +#### Explicit operability + +A `Switch` now has to declare how it can be operated. Previously a switch with no `onValueChange` still rendered as an enabled, focusable control that did nothing when activated, and screen readers announced it as operable. + +Pass `onValueChange` to make it interactive, or mark it `readOnly` or `disabled` to render it as a state indicator. A read-only switch keeps its enabled appearance and is still announced with its on/off state, but it is neither focusable nor pressable — it is not reported as disabled. + +```tsx +// Before (v5) — an enabled switch that does nothing + + +// After (v6) — say which it is + + + +``` + +This most often shows up where the switch sits inside a row that owns the press: + +```tsx +// After (v6) + + + Dark Theme + + + + + +``` + +#### Selected icon color + +The icon inside a selected switch now uses `onPrimaryContainer` instead of `primary`, matching the Material Design 3 spec and improving its contrast against the handle. If you theme `Switch` with a partial `theme.colors` override, add `onPrimaryContainer` alongside the roles you already override — omitted roles fall back to the app theme, which would leave the icon in the default palette. + +```diff + const switchTheme = { + colors: { + primary: theme.colors.tertiary, + onPrimary: theme.colors.onTertiary, + primaryContainer: theme.colors.tertiaryContainer, ++ onPrimaryContainer: theme.colors.onTertiaryContainer, + }, + }; +``` + +#### Touch target height + +`Switch` now reserves the 48dp minimum touch target Material Design requires, so it occupies 48dp of height instead of 40dp. Nothing painted changed size — the track is still 52×32 — but rows containing a switch may become slightly taller. diff --git a/example/src/DrawerItems.tsx b/example/src/DrawerItems.tsx index 94afa3136c..00489864cc 100644 --- a/example/src/DrawerItems.tsx +++ b/example/src/DrawerItems.tsx @@ -178,7 +178,7 @@ function DrawerItems() { Use Dynamic Theme - + @@ -187,7 +187,7 @@ function DrawerItems() { Dark Theme - + @@ -196,7 +196,7 @@ function DrawerItems() { RTL - + @@ -205,7 +205,7 @@ function DrawerItems() { Collapsed drawer * - + @@ -214,7 +214,7 @@ function DrawerItems() { Custom font * - + @@ -225,7 +225,7 @@ function DrawerItems() { {isIOS ? 'Highlight' : 'Ripple'} effect * - + diff --git a/example/src/Examples/FABExample.tsx b/example/src/Examples/FABExample.tsx index 09ce39aa14..1b8c3d61f9 100644 --- a/example/src/Examples/FABExample.tsx +++ b/example/src/Examples/FABExample.tsx @@ -154,7 +154,7 @@ const FABExample = () => { title="Show FAB" right={() => ( - + )} onPress={() => setShowFab((v) => !v)} diff --git a/example/src/Examples/SwitchExample.tsx b/example/src/Examples/SwitchExample.tsx index 545dbf8dd3..09a3c7ec92 100644 --- a/example/src/Examples/SwitchExample.tsx +++ b/example/src/Examples/SwitchExample.tsx @@ -33,6 +33,7 @@ const SwitchExample = () => { primary: theme.colors.tertiary, onPrimary: theme.colors.onTertiary, primaryContainer: theme.colors.tertiaryContainer, + onPrimaryContainer: theme.colors.onTertiaryContainer, secondary: theme.colors.tertiary, }, }), @@ -88,6 +89,10 @@ const SwitchExample = () => { /> + + + + { {label} - + diff --git a/src/components/Switch/Switch.tsx b/src/components/Switch/Switch.tsx index b0ca4b4bd7..97d6feabd5 100644 --- a/src/components/Switch/Switch.tsx +++ b/src/components/Switch/Switch.tsx @@ -3,7 +3,9 @@ import { Platform, Pressable, StyleSheet, + type NativeSyntheticEvent, type StyleProp, + type TargetedEvent, View, type ViewStyle, } from 'react-native'; @@ -15,7 +17,6 @@ import Animated, { useAnimatedStyle, useDerivedValue, useSharedValue, - withDelay, withSequence, withSpring, withTiming, @@ -33,19 +34,11 @@ import type { StateOpacityKey, ThemeProp } from '../../types'; import { isKeyboardFocusEvent } from '../../utils/isKeyboardFocusEvent'; import Icon, { type IconSource } from '../Icon'; -export type Props = { +type SwitchBaseProps = { /** * Whether the switch is on. */ value?: boolean; - /** - * Called with the new value when the user toggles the switch. - */ - onValueChange?: (value: boolean) => void; - /** - * Disables interaction and renders the disabled visual state. - */ - disabled?: boolean; /** * Icon shown inside the handle when checked */ @@ -63,11 +56,51 @@ export type Props = { 'aria-label'?: string; }; +export type Props = SwitchBaseProps & { + /** + * Called with the new value when the user toggles the switch. Required + * unless the switch is `readOnly` or `disabled`. + */ + onValueChange?: (value: boolean) => void; + /** + * Reports state the user cannot change here. The switch keeps its enabled + * appearance and is still announced by a screen reader, but it is neither + * focusable nor pressable. + */ + readOnly?: boolean; + /** + * Disables interaction and renders the disabled visual state. + */ + disabled?: boolean; +}; + +/** + * `Props`, narrowed so a switch always declares how it can be operated and can + * never render as an enabled control that does nothing when activated. + * + * This is applied to the component's declared type rather than folded into + * `Props`, because the docs generator reads props off the parameter annotation + * and silently drops a union (a top-level one drops the whole page). So the + * parameter below stays annotated as the flat `Props`, and callers still get + * the narrowed type through this one. + */ +export type OperableProps = SwitchBaseProps & + ( + | { + onValueChange: (value: boolean) => void; + readOnly?: boolean; + disabled?: boolean; + } + | { onValueChange?: never; readOnly: true; disabled?: boolean } + | { onValueChange?: never; readOnly?: boolean; disabled: true } + ); + const { trackWidth: TRACK_WIDTH, trackHeight: TRACK_HEIGHT, trackOutlineWidth: TRACK_OUTLINE_WIDTH, stateLayerSize: STATE_LAYER_SIZE, + touchTargetSize: TOUCH_TARGET_SIZE, selectedHandleSize: SELECTED_HANDLE, unselectedHandleSize: UNSELECTED_HANDLE, iconHandleSize: ICON_HANDLE, @@ -86,11 +119,10 @@ const stateOpacity = stateTokens.opacity; const { thickness: FOCUS_THICKNESS, outerOffset: FOCUS_OUTER_OFFSET } = stateTokens.focusIndicator; const FOCUS_RING_INSET = -(FOCUS_OUTER_OFFSET + FOCUS_THICKNESS); -const OVERLAY_TOP = (STATE_LAYER_SIZE - TRACK_HEIGHT) / 2; - -// Hold-then-grow: a brief delay before snapping to PRESSED_HANDLE so a quick -// tap doesn't flash the press-grow visual. -const PRESS_GROW_DELAY = 100; +// Every painted layer is smaller than the touch target and absolutely +// positioned, so each one is centred against it. +const centreY = (size: number) => (TOUCH_TARGET_SIZE - size) / 2; +const TRACK_TOP = centreY(TRACK_HEIGHT); function restingHandleSize(checked: boolean, hasIcon: boolean): number { if (hasIcon) return ICON_HANDLE; @@ -117,18 +149,20 @@ const CHECKED_CENTER = TRACK_WIDTH - HANDLE_PADDING - SELECTED_HANDLE / 2; * * ## Theming * Customize by overriding these `theme.colors` roles: - * - `primary` / `onPrimary`: selected track + icon / selected handle - * - `primaryContainer`: selected handle on hover, press + * - `primary` / `onPrimary`: selected track / selected handle + * - `onPrimaryContainer`: selected icon + * - `primaryContainer`: selected handle on hover, focus, press * - `surfaceContainerHighest`: unselected track + icon * - `outline`: unselected resting handle, unselected track outline - * - `onSurfaceVariant`: unselected handle on hover, press + * - `onSurfaceVariant`: unselected handle on hover, focus, press * - `onSurface`: disabled track, handle, and icon fills * - `surface`: disabled selected handle * - `secondary`: focus indicator */ -const Switch = ({ +const Switch: (props: OperableProps) => React.JSX.Element = ({ value, disabled, + readOnly, onValueChange, checkedIcon, uncheckedIcon, @@ -145,6 +179,22 @@ const Switch = ({ const checked = !!value; const isDisabled = !!disabled; const isEnabled = !isDisabled; + const isReadOnly = !!readOnly; + // `OperableProps` already requires one of these, but untyped callers can slip + // past it, so guard here too rather than rendering an enabled no-op. + const isMissingOperability = !onValueChange && !isReadOnly && !isDisabled; + + if (isMissingOperability) { + console.warn( + 'Switch: pass `onValueChange` to make the switch operable, or set `readOnly` or `disabled` to render it as a state indicator.' + ); + } + + const isInteractive = !isDisabled && !isReadOnly && !isMissingOperability; + // Non-operable but not disabled: an explicit `readOnly`, or the fallback for + // a missing handler. Both render as state indicators, so both are announced + // that way; `aria-disabled` carries the disabled case on its own. + const isAnnouncedReadOnly = !isDisabled && !isInteractive; const iconSource = checked ? checkedIcon : uncheckedIcon; const hasIcon = iconSource !== undefined; @@ -164,6 +214,17 @@ const Switch = ({ isDisabledSV.value = isDisabled ? 1 : 0; }, [checked, hasIcon, isDisabled, checkedSV, hasIconSV, isDisabledSV]); + // A switch that stops being interactive loses its handlers, so one that was + // hovered, pressed, or focused at that moment would never get the matching + // hover-out, press-out, or blur and would keep painting that state. + React.useEffect(() => { + if (isInteractive) return; + + pressedSV.value = 0; + hoveredSV.value = 0; + focusedSV.value = 0; + }, [isInteractive, pressedSV, hoveredSV, focusedSV]); + const colors = React.useMemo(() => getDefaultSwitchColors(theme), [theme]); const reanimatedReduceMotion = reduceMotion @@ -229,14 +290,10 @@ const Switch = ({ ({ p, c, hi }) => { const restingSize = hi === 1 ? ICON_HANDLE : c === 1 ? SELECTED_HANDLE : UNSELECTED_HANDLE; - if (p === 1) { - handleSize.value = withDelay( - PRESS_GROW_DELAY, - withTiming(PRESSED_HANDLE, { duration: 0 }) - ); - } else { - handleSize.value = withSpring(restingSize, springConfig); - } + handleSize.value = withSpring( + p === 1 ? PRESSED_HANDLE : restingSize, + springConfig + ); }, [springConfig] ); @@ -251,12 +308,7 @@ const Switch = ({ (current, prev) => { if (current === prev) return; const wasPressed = prev === 'pressed'; - const target = - current === 'pressed' - ? stateOpacity.pressed - : current === 'hovered' - ? stateOpacity.hovered - : 0; + const target = current ? stateOpacity[current] : 0; if (wasPressed && current !== 'pressed') { // On release: rise to peak, then fall to the next state. stateLayerAlpha.value = withSequence( @@ -284,6 +336,11 @@ const Switch = ({ ? colors.checkedPressedHandleColor : colors.uncheckedPressedHandleColor; } + if (focusedSV.value === 1) { + return isCheckedNow + ? colors.checkedFocusHandleColor + : colors.uncheckedFocusHandleColor; + } if (hoveredSV.value === 1) { return isCheckedNow ? colors.checkedHoverHandleColor @@ -297,7 +354,7 @@ const Switch = ({ const handleAnimatedStyle = useAnimatedStyle(() => ({ width: handleSize.value, height: handleSize.value, - top: (STATE_LAYER_SIZE - handleSize.value) / 2, + top: (TOUCH_TARGET_SIZE - handleSize.value) / 2, transform: [ { translateX: xSign * (handleCenter.value - handleSize.value / 2) }, ], @@ -345,30 +402,40 @@ const Switch = ({ const trackOpacityValue = isDisabled ? DISABLED_TRACK_OPACITY : 1; const iconSize = checked ? SELECTED_ICON : UNSELECTED_ICON; - return ( - - onValueChange?.(!checked)} - onPressIn={() => { + // A non-interactive switch gets no press, hover, or focus affordances at all. + // It stays in the accessibility tree, so its state is still announced. + const interactionProps = isInteractive + ? { + onPress: () => onValueChange?.(!checked), + onPressIn: () => { pressedSV.value = 1; - }} - onPressOut={() => { + }, + onPressOut: () => { pressedSV.value = 0; - }} - onHoverIn={() => { + }, + onHoverIn: () => { hoveredSV.value = 1; - }} - onHoverOut={() => { + }, + onHoverOut: () => { hoveredSV.value = 0; - }} - onFocus={(e) => { + }, + onFocus: (e: NativeSyntheticEvent) => { if (!isKeyboardFocusEvent(e)) return; focusedSV.value = 1; - }} - onBlur={() => { + }, + onBlur: () => { focusedSV.value = 0; - }} + }, + } + : null; + + return ( + + - + {/* Disabled-only: opaque `surface` backdrop. The tinted fill above composites over it, reproducing the native math avoiding the PlatformColor alpha limitation. */} {isDisabled ? ( @@ -413,6 +484,7 @@ const Switch = ({ /> ) : null} + Reanimated.getAnimatedStyle(screen.getByTestId(testID)); + +afterEach(() => { + jest.restoreAllMocks(); +}); + describe('Switch render', () => { it('renders on', async () => { - expect((await render()).toJSON()).toMatchSnapshot(); + expect( + (await render()).toJSON() + ).toMatchSnapshot(); }); it('renders off', async () => { - expect((await render()).toJSON()).toMatchSnapshot(); + expect( + ( + await render() + ).toJSON() + ).toMatchSnapshot(); }); it('renders disabled on', async () => { @@ -26,14 +45,25 @@ describe('Switch render', () => { it('renders with checked icon', async () => { expect( - (await render()).toJSON() + ( + await render( + + ) + ).toJSON() ).toMatchSnapshot(); }); it('renders with per-state icons', async () => { expect( ( - await render() + await render( + + ) ).toJSON() ).toMatchSnapshot(); }); @@ -41,10 +71,199 @@ describe('Switch render', () => { describe('Switch accessibility', () => { it('has switch role', async () => { - await render(); + await render(); expect(screen.getByRole('switch')).toBeOnTheScreen(); }); + + it('exposes a touch target meeting the 48dp minimum', async () => { + await render(); + + expect(screen.getByRole('switch')).toHaveStyle({ width: 52, height: 48 }); + }); +}); + +describe('Switch focus state', () => { + const renderAndFocus = async (element: React.ReactElement) => { + await render(element); + + await fireEvent(screen.getByTestId('switch'), 'focus'); + await jest.runAllTimersAsync(); + }; + + it('shows the focus indicator on keyboard focus', async () => { + await renderAndFocus( + + ); + + expect(animatedStyle('switch-focus-ring')).toMatchObject({ opacity: 1 }); + }); + + it('hides the focus indicator again on blur', async () => { + await renderAndFocus( + + ); + + await fireEvent(screen.getByTestId('switch'), 'blur'); + await jest.runAllTimersAsync(); + + expect(animatedStyle('switch-focus-ring')).toMatchObject({ opacity: 0 }); + }); + + it('raises the state layer to the focused opacity', async () => { + await renderAndFocus( + + ); + + expect(animatedStyle('switch-state-layer')).toMatchObject({ + opacity: tokens.md.sys.state.opacity.focused, + }); + }); + + it('leaves the indicator hidden for pointer focus on web', async () => { + jest.replaceProperty(Platform, 'OS', 'web'); + + await render( + + ); + await fireEvent(screen.getByTestId('switch'), 'focus', { + currentTarget: { matches: () => false }, + }); + await jest.runAllTimersAsync(); + + expect(animatedStyle('switch-focus-ring')).toMatchObject({ opacity: 0 }); + }); + + it('shows the indicator for keyboard focus on web', async () => { + jest.replaceProperty(Platform, 'OS', 'web'); + + await render( + + ); + await fireEvent(screen.getByTestId('switch'), 'focus', { + currentTarget: { matches: () => true }, + }); + await jest.runAllTimersAsync(); + + expect(animatedStyle('switch-focus-ring')).toMatchObject({ opacity: 1 }); + }); + + it('clears the focus state when the switch stops being interactive', async () => { + const view = await render( + + ); + + await fireEvent(screen.getByTestId('switch'), 'focus'); + await jest.runAllTimersAsync(); + expect(animatedStyle('switch-focus-ring')).toMatchObject({ opacity: 1 }); + + await view.rerender(); + await jest.runAllTimersAsync(); + + expect(animatedStyle('switch-focus-ring')).toMatchObject({ opacity: 0 }); + }); + + it('paints the focus handle color when selected and focused', async () => { + await renderAndFocus( + + ); + + expect(animatedStyle('switch-handle-fill')).toMatchObject({ + backgroundColor: defaultThemes.light.colors.primaryContainer, + }); + }); +}); + +describe('Switch operability', () => { + it('warns when it is given no way to be operated', async () => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); + + // @ts-expect-error -- the props type requires a handler, `readOnly`, or + // `disabled`; this covers untyped callers that get past it. + await render(); + + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining('onValueChange') + ); + }); + + it('is not focusable when it has no way to be operated', async () => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); + + // @ts-expect-error -- see above. + await render(); + + expect(screen.getByRole('switch')).toHaveProp('focusable', false); + }); + + it('announces a read-only switch without making it focusable', async () => { + await render(); + + const control = screen.getByRole('switch'); + + expect(control).toHaveProp('focusable', false); + expect(control).toHaveProp('aria-readonly', true); + expect(control).toBeChecked(); + expect(control).toBeEnabled(); + }); + + it('announces a switch with no way to be operated as read-only', async () => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); + + // @ts-expect-error -- the fallback exists for untyped callers. + await render(); + + const control = screen.getByRole('switch'); + + expect(control).toHaveProp('aria-readonly', true); + expect(control).toBeEnabled(); + }); + + it('does not announce a disabled switch as read-only', async () => { + await render(); + + const control = screen.getByRole('switch'); + + expect(control).toHaveProp('aria-readonly', false); + expect(control).toBeDisabled(); + }); + + it('keeps an interactive switch focusable', async () => { + await render(); + + expect(screen.getByRole('switch')).toHaveProp('focusable', true); + }); +}); + +describe('Switch press feedback', () => { + // MD3 resting (selected) and pressed handle sizes. + const RESTING = 24; + const PRESSED = 28; + const handleWidth = () => Number(animatedStyle('switch-handle').width); + + it('starts growing the handle immediately on press', async () => { + await render(); + expect(handleWidth()).toBe(RESTING); + + await fireEvent(screen.getByTestId('switch'), 'pressIn'); + // Two frames in -- well inside the 100ms delay the old implementation + // waited out before snapping, so a reintroduced delay still reads RESTING. + jest.advanceTimersByTime(32); + + expect(handleWidth()).toBeGreaterThan(RESTING); + }); + + it('settles at the pressed size and returns on release', async () => { + await render(); + + await fireEvent(screen.getByTestId('switch'), 'pressIn'); + await jest.runAllTimersAsync(); + expect(handleWidth()).toBe(PRESSED); + + await fireEvent(screen.getByTestId('switch'), 'pressOut'); + await jest.runAllTimersAsync(); + expect(handleWidth()).toBe(RESTING); + }); }); describe('Switch interaction', () => { @@ -64,6 +283,19 @@ describe('Switch interaction', () => { expect(onValueChange).toHaveBeenCalledWith(false); }); + it('does not toggle a read-only switch that still has a handler', async () => { + const user = userEvent.setup(); + const onValueChange = jest.fn(); + // The props type permits this pairing, so read-only has to win at runtime. + await render( + + ); + + await user.press(screen.getByRole('switch')); + + expect(onValueChange).not.toHaveBeenCalled(); + }); + it('does not fire onValueChange when disabled', async () => { const user = userEvent.setup(); const onValueChange = jest.fn(); diff --git a/src/components/__tests__/__snapshots__/Switch.test.tsx.snap b/src/components/__tests__/__snapshots__/Switch.test.tsx.snap index e38a5145e1..45aed5023f 100644 --- a/src/components/__tests__/__snapshots__/Switch.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Switch.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Switch render renders disabled off 1`] = ` [ { "alignItems": "center", - "height": 40, + "height": 48, "justifyContent": "center", "overflow": "visible", "width": 52, @@ -34,8 +34,9 @@ exports[`Switch render renders disabled off 1`] = ` } } accessible={true} + aria-readonly={false} collapsable={false} - focusable={true} + focusable={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -50,7 +51,7 @@ exports[`Switch render renders disabled off 1`] = ` [ { "alignItems": "center", - "height": 40, + "height": 48, "justifyContent": "center", "width": 52, }, @@ -104,7 +105,7 @@ exports[`Switch render renders disabled off 1`] = ` "height": 40, "pointerEvents": "none", "position": "absolute", - "top": 0, + "top": 4, "width": 40, }, { @@ -139,7 +140,7 @@ exports[`Switch render renders disabled off 1`] = ` }, { "height": 16, - "top": 12, + "top": 16, "transform": [ { "translateX": 8, @@ -201,10 +202,10 @@ exports[`Switch render renders disabled off 1`] = ` "borderColor": "rgba(98, 91, 113, 1)", "borderRadius": 9999, "borderWidth": 3, - "bottom": -1, + "bottom": 3, "left": -5, "right": -5, - "top": -1, + "top": 3, }, { "opacity": 0, @@ -221,7 +222,7 @@ exports[`Switch render renders disabled on 1`] = ` [ { "alignItems": "center", - "height": 40, + "height": 48, "justifyContent": "center", "overflow": "visible", "width": 52, @@ -249,8 +250,9 @@ exports[`Switch render renders disabled on 1`] = ` } } accessible={true} + aria-readonly={false} collapsable={false} - focusable={true} + focusable={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -265,7 +267,7 @@ exports[`Switch render renders disabled on 1`] = ` [ { "alignItems": "center", - "height": 40, + "height": 48, "justifyContent": "center", "width": 52, }, @@ -299,7 +301,7 @@ exports[`Switch render renders disabled on 1`] = ` "height": 40, "pointerEvents": "none", "position": "absolute", - "top": 0, + "top": 4, "width": 40, }, { @@ -334,7 +336,7 @@ exports[`Switch render renders disabled on 1`] = ` }, { "height": 24, - "top": 8, + "top": 12, "transform": [ { "translateX": 24, @@ -396,10 +398,10 @@ exports[`Switch render renders disabled on 1`] = ` "borderColor": "rgba(98, 91, 113, 1)", "borderRadius": 9999, "borderWidth": 3, - "bottom": -1, + "bottom": 3, "left": -5, "right": -5, - "top": -1, + "top": 3, }, { "opacity": 0, @@ -416,7 +418,7 @@ exports[`Switch render renders off 1`] = ` [ { "alignItems": "center", - "height": 40, + "height": 48, "justifyContent": "center", "overflow": "visible", "width": 52, @@ -444,6 +446,7 @@ exports[`Switch render renders off 1`] = ` } } accessible={true} + aria-readonly={false} collapsable={false} focusable={true} onBlur={[Function]} @@ -460,7 +463,7 @@ exports[`Switch render renders off 1`] = ` [ { "alignItems": "center", - "height": 40, + "height": 48, "justifyContent": "center", "width": 52, }, @@ -514,7 +517,7 @@ exports[`Switch render renders off 1`] = ` "height": 40, "pointerEvents": "none", "position": "absolute", - "top": 0, + "top": 4, "width": 40, }, { @@ -549,7 +552,7 @@ exports[`Switch render renders off 1`] = ` }, { "height": 16, - "top": 12, + "top": 16, "transform": [ { "translateX": 8, @@ -594,10 +597,10 @@ exports[`Switch render renders off 1`] = ` "borderColor": "rgba(98, 91, 113, 1)", "borderRadius": 9999, "borderWidth": 3, - "bottom": -1, + "bottom": 3, "left": -5, "right": -5, - "top": -1, + "top": 3, }, { "opacity": 0, @@ -614,7 +617,7 @@ exports[`Switch render renders on 1`] = ` [ { "alignItems": "center", - "height": 40, + "height": 48, "justifyContent": "center", "overflow": "visible", "width": 52, @@ -642,6 +645,7 @@ exports[`Switch render renders on 1`] = ` } } accessible={true} + aria-readonly={false} collapsable={false} focusable={true} onBlur={[Function]} @@ -658,7 +662,7 @@ exports[`Switch render renders on 1`] = ` [ { "alignItems": "center", - "height": 40, + "height": 48, "justifyContent": "center", "width": 52, }, @@ -692,7 +696,7 @@ exports[`Switch render renders on 1`] = ` "height": 40, "pointerEvents": "none", "position": "absolute", - "top": 0, + "top": 4, "width": 40, }, { @@ -727,7 +731,7 @@ exports[`Switch render renders on 1`] = ` }, { "height": 24, - "top": 8, + "top": 12, "transform": [ { "translateX": 24, @@ -772,10 +776,10 @@ exports[`Switch render renders on 1`] = ` "borderColor": "rgba(98, 91, 113, 1)", "borderRadius": 9999, "borderWidth": 3, - "bottom": -1, + "bottom": 3, "left": -5, "right": -5, - "top": -1, + "top": 3, }, { "opacity": 0, @@ -792,7 +796,7 @@ exports[`Switch render renders with checked icon 1`] = ` [ { "alignItems": "center", - "height": 40, + "height": 48, "justifyContent": "center", "overflow": "visible", "width": 52, @@ -820,6 +824,7 @@ exports[`Switch render renders with checked icon 1`] = ` } } accessible={true} + aria-readonly={false} collapsable={false} focusable={true} onBlur={[Function]} @@ -836,7 +841,7 @@ exports[`Switch render renders with checked icon 1`] = ` [ { "alignItems": "center", - "height": 40, + "height": 48, "justifyContent": "center", "width": 52, }, @@ -870,7 +875,7 @@ exports[`Switch render renders with checked icon 1`] = ` "height": 40, "pointerEvents": "none", "position": "absolute", - "top": 0, + "top": 4, "width": 40, }, { @@ -905,7 +910,7 @@ exports[`Switch render renders with checked icon 1`] = ` }, { "height": 24, - "top": 8, + "top": 12, "transform": [ { "translateX": 24, @@ -946,7 +951,7 @@ exports[`Switch render renders with checked icon 1`] = ` "height": 16, "pointerEvents": "none", "position": "absolute", - "top": 12, + "top": 16, "width": 16, }, { @@ -986,7 +991,7 @@ exports[`Switch render renders with checked icon 1`] = ` style={ [ { - "color": "rgba(103, 80, 164, 1)", + "color": "rgba(33, 0, 93, 1)", "fontSize": 16, }, [ @@ -1021,10 +1026,10 @@ exports[`Switch render renders with checked icon 1`] = ` "borderColor": "rgba(98, 91, 113, 1)", "borderRadius": 9999, "borderWidth": 3, - "bottom": -1, + "bottom": 3, "left": -5, "right": -5, - "top": -1, + "top": 3, }, { "opacity": 0, @@ -1041,7 +1046,7 @@ exports[`Switch render renders with per-state icons 1`] = ` [ { "alignItems": "center", - "height": 40, + "height": 48, "justifyContent": "center", "overflow": "visible", "width": 52, @@ -1069,6 +1074,7 @@ exports[`Switch render renders with per-state icons 1`] = ` } } accessible={true} + aria-readonly={false} collapsable={false} focusable={true} onBlur={[Function]} @@ -1085,7 +1091,7 @@ exports[`Switch render renders with per-state icons 1`] = ` [ { "alignItems": "center", - "height": 40, + "height": 48, "justifyContent": "center", "width": 52, }, @@ -1119,7 +1125,7 @@ exports[`Switch render renders with per-state icons 1`] = ` "height": 40, "pointerEvents": "none", "position": "absolute", - "top": 0, + "top": 4, "width": 40, }, { @@ -1154,7 +1160,7 @@ exports[`Switch render renders with per-state icons 1`] = ` }, { "height": 24, - "top": 8, + "top": 12, "transform": [ { "translateX": 24, @@ -1195,7 +1201,7 @@ exports[`Switch render renders with per-state icons 1`] = ` "height": 16, "pointerEvents": "none", "position": "absolute", - "top": 12, + "top": 16, "width": 16, }, { @@ -1235,7 +1241,7 @@ exports[`Switch render renders with per-state icons 1`] = ` style={ [ { - "color": "rgba(103, 80, 164, 1)", + "color": "rgba(33, 0, 93, 1)", "fontSize": 16, }, [ @@ -1270,10 +1276,10 @@ exports[`Switch render renders with per-state icons 1`] = ` "borderColor": "rgba(98, 91, 113, 1)", "borderRadius": 9999, "borderWidth": 3, - "bottom": -1, + "bottom": 3, "left": -5, "right": -5, - "top": -1, + "top": 3, }, { "opacity": 0, diff --git a/src/index.tsx b/src/index.tsx index 8863e2fa20..8571269cb7 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -127,7 +127,7 @@ export type { Props as RadioButtonItemProps } from './components/RadioButton/Rad export type { Props as SearchbarProps } from './components/Searchbar'; export type { Props as SnackbarProps } from './components/Snackbar'; export type { Props as SurfaceProps } from './components/Surface'; -export type { Props as SwitchProps } from './components/Switch/Switch'; +export type { OperableProps as SwitchProps } from './components/Switch/Switch'; export type { TextInputProps, TextInputRenderProps,