diff --git a/docs/public/screenshots/fab-android.png b/docs/public/screenshots/fab-android.png new file mode 100644 index 0000000000..c8cfcd5e5c Binary files /dev/null and b/docs/public/screenshots/fab-android.png differ diff --git a/docs/public/screenshots/fab-general.png b/docs/public/screenshots/fab-general.png new file mode 100644 index 0000000000..e4eefe5efe Binary files /dev/null and b/docs/public/screenshots/fab-general.png differ diff --git a/docs/public/screenshots/fab-ios.png b/docs/public/screenshots/fab-ios.png new file mode 100644 index 0000000000..cf9769b5b7 Binary files /dev/null and b/docs/public/screenshots/fab-ios.png differ diff --git a/docs/public/screenshots/fab-web.png b/docs/public/screenshots/fab-web.png new file mode 100644 index 0000000000..4ee79ea973 Binary files /dev/null and b/docs/public/screenshots/fab-web.png differ diff --git a/src/components/FAB/Menu.tsx b/src/components/FAB/Menu.tsx index e4884647d6..7070429ae0 100644 --- a/src/components/FAB/Menu.tsx +++ b/src/components/FAB/Menu.tsx @@ -1,28 +1,35 @@ import * as React from 'react'; -import { Platform, StyleSheet, View } from 'react-native'; -import type { ColorValue, GestureResponderEvent } from 'react-native'; +import { Platform, ScrollView, StyleSheet, View } from 'react-native'; +import type { + ColorValue, + GestureResponderEvent, + ViewStyle, +} from 'react-native'; import Animated, { + cubicBezier, interpolate, useAnimatedStyle, useDerivedValue, useSharedValue, - withDelay, withSpring, } from 'react-native-reanimated'; +import type { AnimatedStyle } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import Content from './Content'; +import { getFocusableElement } from './focus'; import Shell from './Shell'; import { MenuTokens, + STATE_LAYER_OPACITY, Tokens, FOCUS_RING_INSET, FOCUS_RING_THICKNESS, + webNoHoverTint, webNoOutline, } from './tokens'; -import type { Size, Variant } from './tokens'; -import { useFocusRing } from './useFocusRing'; +import type { Interaction, Size, Variant } from './tokens'; import { resolveColors } from './utils'; import { useLocale } from '../../core/locale'; import { useInternalTheme } from '../../core/theming'; @@ -30,8 +37,10 @@ import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext'; import { toRawSpring } from '../../theme/tokens/sys/motion'; import type { InternalTheme, ThemeProp } from '../../theme/types'; import { resolveCornerRadius } from '../../theme/utils/shape'; +import { isKeyboardFocusEvent } from '../../utils/isKeyboardFocusEvent'; import Icon from '../Icon'; import type { IconSource } from '../Icon'; +import Surface from '../Surface'; import TouchableRipple from '../TouchableRipple/TouchableRipple'; export type MenuItemProps = { @@ -87,7 +96,9 @@ export type MenuProps = { */ expanded: boolean; /** - * Called when the user taps the close button or taps an item. + * Called when the menu should close: the user taps the close button, an + * item, or outside the menu, presses Escape (web), the hardware back button + * (Android) or performs the screen reader escape gesture (iOS). */ onDismiss: () => void; /** @@ -146,6 +157,11 @@ const getItemsVariant = (triggerVariant: Variant): Variant => { // fixed delay per index. const STAGGER_MS = 30; +// Room the items scroll view keeps around the items, so it doesn't clip the +// focus ring and the hover shadow. Taps in this margin don't reach the content +// below on Android, so keep it small. +const ITEMS_SCROLL_INSET = FOCUS_RING_INSET + 4; + type AnimatedItemProps = { expanded: boolean; index: number; @@ -166,46 +182,38 @@ const AnimatedItem = ({ children, }: AnimatedItemProps) => { const reduceMotion = useReduceMotion(); - // Initial values match the resting state for the current `expanded` prop so - // first mount doesn't animate unexpectedly. - const scaleX = useSharedValue(expanded ? 1 : 0); - const alpha = useSharedValue(expanded ? 1 : 0); - - React.useEffect(() => { - const target = expanded ? 1 : 0; - // Bottom-up on open, top-down on close (matches Compose). - const delay = expanded - ? (itemCount - 1 - index) * STAGGER_MS - : index * STAGGER_MS; - - if (reduceMotion) { - scaleX.value = target; - alpha.value = target; - return; - } - scaleX.value = withDelay( - delay, - withSpring(target, toRawSpring(theme.motion.spring.fast.spatial)) - ); - alpha.value = withDelay( - delay, - withSpring(target, toRawSpring(theme.motion.spring.fast.effects)) - ); - }, [expanded, index, itemCount, theme, reduceMotion, scaleX, alpha]); + const { scale } = theme.animation; + const { duration, easing } = theme.motion; + + // Bottom-up on open, top-down on close (matches Compose). + const staggerIndex = expanded ? itemCount - 1 - index : index; + + // Reduced motion keeps the opacity fade but drops the horizontal scale and + // the stagger. + const transitionStyle: AnimatedStyle = { + transitionProperty: ['opacity', 'transform'], + transitionDuration: [ + duration.short2 * scale, + reduceMotion ? 0 : duration.short4 * scale, + ], + transitionDelay: reduceMotion ? 0 : staggerIndex * STAGGER_MS * scale, + transitionTimingFunction: cubicBezier(...easing.standard), + }; // Only scaleX and opacity animate. Layout height stays at the item's // natural size — the items container is absolutely positioned above the // trigger, so this fixed height never affects the trigger's position. - const animStyle = useAnimatedStyle(() => ({ - transform: [{ scaleX: scaleX.value }], - opacity: alpha.value, - })); + const stateStyle: AnimatedStyle = { + opacity: expanded ? 1 : 0, + transform: [{ scaleX: expanded ? 1 : 0 }], + }; return ( | null + ) => void; onPress: (e: GestureResponderEvent) => void; /** * Accessibility label. Falls back to `label`. @@ -231,15 +252,19 @@ type ItemProps = { /** * A single FAB Menu item. Visually a tonal pill with an icon and a label, - * but it is not a floating action button: no shadow, no enter/exit scaling - * of its own (the surrounding `AnimatedItem` handles entrance), and its - * shape and dimensions come from the menu spec rather than FAB tokens. + * but it is not a floating action button: no enter/exit scaling of its own + * (the surrounding `AnimatedItem` handles entrance), and its shape and + * dimensions come from the menu spec rather than FAB tokens. It follows the + * FAB's state elevation and state layers. */ const MenuItem = ({ icon, label, variant, theme, + expanded, + index, + registerRef, onPress, 'aria-label': ariaLabel, testID, @@ -249,52 +274,96 @@ const MenuItem = ({ MenuTokens.listItem; const borderRadius = resolveCornerRadius(theme, shape); - const { focusedSV, onFocus, onBlur } = useFocusRing(); - const focusRingStyle = useAnimatedStyle(() => ({ - opacity: focusedSV.value ? 1 : 0, - })); + const [hovered, setHovered] = React.useState(false); + const [focused, setFocused] = React.useState(false); + const [pressed, setPressed] = React.useState(false); + + // Items stay mounted while collapsed. Drop their interaction state with the + // menu, otherwise a pointer left over an item shows its state layer again on + // the next open. + React.useEffect(() => { + if (!expanded) { + setHovered(false); + setFocused(false); + setPressed(false); + } + }, [expanded]); + + const interaction: Interaction = pressed + ? 'pressed' + : focused + ? 'focus' + : hovered + ? 'hover' + : 'enabled'; + const stateLayerStyle: AnimatedStyle = { + backgroundColor: colors.content, + opacity: STATE_LAYER_OPACITY[interaction], + transitionProperty: 'opacity', + transitionDuration: theme.motion.duration.short2 * theme.animation.scale, + }; + + const itemRef = React.useCallback( + (node: React.ComponentRef | null) => registerRef(index, node), + [registerRef, index] + ); return ( - - - - - - + + setFocused(isKeyboardFocusEvent(e))} + onBlur={() => setFocused(false)} + onHoverIn={() => setHovered(true)} + onHoverOut={() => setHovered(false)} + onPressIn={() => setPressed(true)} + onPressOut={() => setPressed(false)} + role={Platform.OS === 'web' ? 'menuitem' : 'button'} + aria-label={ariaLabel ?? label} + // `aria-hidden` and `pointerEvents` don't take collapsed items + // out of the tab order. + focusable={expanded} + tabIndex={expanded ? 0 : -1} + style={[ + { borderRadius }, + // The state layer replaces the hover tint of TouchableRipple. + Platform.OS === 'web' ? [webNoOutline, webNoHoverTint] : null, + ]} + testID={testID} + > + + + + + @@ -320,6 +389,7 @@ type MorphingTriggerProps = { 'aria-label'?: string; theme: InternalTheme; testID?: string; + ref?: React.Ref>; }; const MorphingTrigger = ({ @@ -337,8 +407,10 @@ const MorphingTrigger = ({ 'aria-label': ariaLabel, theme, testID, + ref, }: MorphingTriggerProps) => { const reduceMotion = useReduceMotion(); + const { scale } = theme.animation; const closedSpec = Tokens.sizes[size]; const closedContainer = closedSpec.container; @@ -403,23 +475,28 @@ const MorphingTrigger = ({ [closedBorderRadius, openBorderRadius] ); - const openPlaneStyle = useAnimatedStyle(() => ({ - opacity: 1 - progress.value, - })); - const closePlaneStyle = useAnimatedStyle(() => ({ - opacity: progress.value, - })); + // The morph itself (size, corner, icon position) is the spring above. The + // color and icon cross-fades are effects, so they are CSS transitions. + const fadeTransition: AnimatedStyle = { + transitionProperty: 'opacity', + transitionDuration: theme.motion.duration.short3 * scale, + transitionTimingFunction: cubicBezier(...theme.motion.easing.standard), + }; + const openPlaneStyle: AnimatedStyle = { + opacity: expanded ? 0 : 1, + }; + const closePlaneStyle: AnimatedStyle = { + opacity: expanded ? 1 : 0, + }; const openIconAnimStyle = useAnimatedStyle(() => { const offset = (widthShared.value - closedIconSize) / 2; return { - opacity: 1 - progress.value, transform: [{ translateX: offset }, { translateY: offset }], }; }); const closeIconAnimStyle = useAnimatedStyle(() => { const offset = (widthShared.value - openIconSize) / 2; return { - opacity: progress.value, transform: [{ translateX: offset }, { translateY: offset }], }; }); @@ -454,7 +531,10 @@ const MorphingTrigger = ({ visible={visible} onPress={onPress} aria-label={ariaLabel} + aria-expanded={expanded} + aria-haspopup="menu" testID={testID} + buttonRef={ref} widthShared={widthShared} heightShared={heightShared} borderRadiusShared={borderRadiusShared} @@ -469,6 +549,7 @@ const MorphingTrigger = ({ styles.colorPlane, { backgroundColor: triggerColors.container }, openPlaneStyle, + fadeTransition, ]} /> @@ -484,18 +566,22 @@ const MorphingTrigger = ({ > - + + + - + + + @@ -508,9 +594,12 @@ const MorphingTrigger = ({ * items appear stacked above and the trigger morphs into the spec'd close * button (`shape: 'full'`, 56 dp, saturated role color). * - * No visual backdrop and no outside-tap dismiss — that matches the MD3 spec - * and lets the user keep interacting with the content underneath. Dismiss - * via the close button or by tapping an item. + * There is no visual scrim. Pressing outside the menu, the close button or an + * item, pressing Escape (web), the hardware back button (Android) or the + * screen reader escape gesture (iOS) calls `onDismiss`. Focus moves to the + * first item when the menu opens and returns to the trigger when it closes. + * On web, the arrow keys, Home and End move focus between the items and the + * close button. * * ## Usage * ```tsx @@ -554,12 +643,87 @@ const Menu = ({ const effectiveExpanded = triggerVisible && expanded; + const triggerRef = React.useRef>(null); + const scrollRef = React.useRef>(null); + const itemRefs = React.useRef<(React.ComponentRef | null)[]>([]); + + const [containerHeight, setContainerHeight] = React.useState(); + + const registerItemRef = React.useCallback( + (index: number, node: React.ComponentRef | null) => { + itemRefs.current[index] = node; + }, + [] + ); + const handleItemPress = (item: MenuItemProps) => (e: GestureResponderEvent) => { item.onPress(e); onDismiss(); }; + // A long list scrolls, so start from the first item every time it opens. + React.useEffect(() => { + if (effectiveExpanded) { + scrollRef.current?.scrollTo({ y: 0, animated: false }); + } + }, [effectiveExpanded]); + + // Arrow keys, Home and End move focus between the items and the close + // button. Opening, closing and dismissing the menu are left to the overlay + // handling. + React.useEffect(() => { + if (Platform.OS !== 'web' || !effectiveExpanded) { + return undefined; + } + + const onKeyDown = (e: KeyboardEvent) => { + // Something else, such as a dialog on top, already handled the key. + if (e.defaultPrevented) { + return; + } + + if ( + !['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.key) || + e.altKey || + e.ctrlKey || + e.metaKey || + e.shiftKey + ) { + return; + } + + // Items are in visual order, top to bottom, followed by the close + // button. + const stops = [...itemRefs.current, triggerRef.current].flatMap( + (node) => getFocusableElement(node) ?? [] + ); + const current = stops.findIndex((el) => el === document.activeElement); + + if (current === -1) { + return; + } + + e.preventDefault(); + + const last = stops.length - 1; + const next = + e.key === 'Home' + ? 0 + : e.key === 'End' + ? last + : e.key === 'ArrowDown' + ? (current + 1) % stops.length + : (current - 1 + stops.length) % stops.length; + + stops[next].focus(); + }; + + document.addEventListener('keydown', onKeyDown); + + return () => document.removeEventListener('keydown', onKeyDown); + }, [effectiveExpanded]); + const flexAlign: 'flex-start' | 'center' | 'flex-end' = alignment === 'start' ? 'flex-start' @@ -579,17 +743,43 @@ const Menu = ({ const triggerSlotSize = Tokens.sizes[size].container; + const paddingBottom = insets.bottom + 16; + // Space left above the trigger. The scroll inset is added back because the + // scroll view reserves room for the shadow and focus ring of every item. + const itemsMaxHeight = + containerHeight === undefined + ? undefined + : Math.max( + 0, + containerHeight - + insets.top - + paddingBottom - + triggerSlotSize - + MenuTokens.spacing.closeToLastItem + ) + + ITEMS_SCROLL_INSET * 2; + + // Only the items take touches, so taps next to them in the scroll view's + // margin reach the content below. When collapsed, nothing does. Android + // doesn't scroll with `box-none`. + const itemsPointerEvents = !effectiveExpanded + ? styles.pointerEventsNone + : Platform.OS === 'android' + ? null + : styles.pointerEventsBoxNone; + return ( setContainerHeight(e.nativeEvent.layout.height)} testID={testID} > @@ -606,33 +796,54 @@ const Menu = ({ alignItems: flexAlign, }, ]} + // A menu without items would be announced as an empty one. + role={Platform.OS === 'web' && effectiveExpanded ? 'menu' : undefined} > - {items.map((item, index) => { - const isLast = index === items.length - 1; - return ( - - + {items.map((item, index) => { + const isLast = index === items.length - 1; + return ( + - - ); - })} + transformOrigin={itemTransformOrigin} + marginBottom={isLast ? 0 : MenuTokens.spacing.betweenItems} + > + + + ); + })} + >; + /** + * Ref to the pressable inside the shell, the element that receives focus. + */ + buttonRef?: React.Ref>; }; /** @@ -202,6 +210,7 @@ const Shell = ({ 'aria-selected': ariaSelected, 'aria-busy': ariaBusy, 'aria-expanded': ariaExpanded, + 'aria-haspopup': ariaHasPopup, labelMaxFontSizeMultiplier, labelAnimatedStyle, background, @@ -215,6 +224,7 @@ const Shell = ({ testID, theme: themeOverrides, ref, + buttonRef, }: ShellProps) => { const theme = useInternalTheme(themeOverrides); @@ -324,6 +334,7 @@ const Shell = ({ {overlay} | null | undefined +): HTMLElement | null => { + if (Platform.OS !== 'web' || !(el instanceof HTMLElement)) { + return null; + } + + return ( + el.querySelector(FOCUSABLE_SELECTOR) ?? + (el.matches(FOCUSABLE_SELECTOR) ? el : null) + ); +}; diff --git a/src/components/FAB/tokens.ts b/src/components/FAB/tokens.ts index 0fb79d1d9c..ba3cfd7c44 100644 --- a/src/components/FAB/tokens.ts +++ b/src/components/FAB/tokens.ts @@ -100,6 +100,8 @@ const closeButton = { shape: 'full', } as const; +export type Interaction = keyof typeof stateElevation; + const listItem = { height: 56, shape: 'full', @@ -107,6 +109,14 @@ const listItem = { leading: 24, trailing: 24, iconLabelGap: 8, + labelTypescale: 'titleMedium' satisfies TypescaleKey, + // Items are tonal, so they sit flat and lift on hover like tonal buttons. + elevation: { + enabled: 0, + hover: 1, + focus: 0, + pressed: 0, + } as const satisfies Record, } as const; const spacing = { @@ -120,6 +130,14 @@ export const MenuTokens = { spacing, }; +const stateOpacity = tokens.md.sys.state.opacity; +export const STATE_LAYER_OPACITY = { + enabled: 0, + hover: stateOpacity.hovered, + focus: stateOpacity.focused, + pressed: stateOpacity.pressed, +} as const satisfies Record; + const focusIndicator = tokens.md.sys.state.focusIndicator; export const FOCUS_RING_THICKNESS = focusIndicator.thickness; export const FOCUS_RING_OUTER_OFFSET = focusIndicator.outerOffset; @@ -127,3 +145,5 @@ export const FOCUS_RING_INSET = FOCUS_RING_OUTER_OFFSET + FOCUS_RING_THICKNESS; // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion export const webNoOutline = { outline: 'none' } as unknown as ViewStyle; + +export const webNoHoverTint: ViewStyle = { backgroundColor: 'transparent' }; diff --git a/src/components/__tests__/FABMenu.test.tsx b/src/components/__tests__/FABMenu.test.tsx index 0e03ff6823..05013fa4a6 100644 --- a/src/components/__tests__/FABMenu.test.tsx +++ b/src/components/__tests__/FABMenu.test.tsx @@ -192,3 +192,59 @@ it('calls onDismiss when trigger is pressed while menu is open', async () => { await userEvent.press(screen.getByTestId('fab-shell')); expect(onDismiss).toHaveBeenCalledTimes(1); }); + +it('exposes the expanded state on the trigger', async () => { + const { rerender } = await render( + + ); + expect(screen.getByTestId('fab-shell')).toBeCollapsed(); + + await rerender( + + ); + expect(screen.getByTestId('fab-shell')).toBeExpanded(); +}); + +it('renders item labels with the title medium typescale', async () => { + await render( + + ); + expect(screen.getByText('Send email')).toHaveStyle({ fontSize: 16 }); +}); + +it('hides collapsed items from assistive technology and exposes expanded ones', async () => { + const { rerender } = await render( + + ); + expect(screen.queryByRole('button', { name: 'Send email' })).toBeNull(); + + await rerender( + + ); + expect(screen.getByRole('button', { name: 'Send email' })).toBeOnTheScreen(); +}); diff --git a/src/components/__tests__/__snapshots__/FABMenu.test.tsx.snap b/src/components/__tests__/__snapshots__/FABMenu.test.tsx.snap index 8b66657112..e31c8bc648 100644 --- a/src/components/__tests__/__snapshots__/FABMenu.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/FABMenu.test.tsx.snap @@ -2,6 +2,7 @@ exports[`renders FAB.Menu closed 1`] = ` - - + + - + - Send email - + + + + Send email + + + + + - - - - - + - + - Set reminder - + + + + Set reminder + + + + + - - + @@ -579,6 +828,7 @@ exports[`renders FAB.Menu closed 1`] = ` { "opacity": 0, }, + {}, ] } /> @@ -588,7 +838,7 @@ exports[`renders FAB.Menu closed 1`] = ` "busy": undefined, "checked": undefined, "disabled": true, - "expanded": undefined, + "expanded": false, "selected": undefined, } } @@ -601,6 +851,7 @@ exports[`renders FAB.Menu closed 1`] = ` } } accessible={true} + aria-haspopup="menu" collapsable={false} focusable={true} onBlur={[Function]} @@ -648,7 +899,6 @@ exports[`renders FAB.Menu closed 1`] = ` "top": 0, }, { - "opacity": 1, "transform": [ { "translateX": 16, @@ -661,48 +911,59 @@ exports[`renders FAB.Menu closed 1`] = ` ] } > - - plus - - - + plus + + + + - - close - + + close + + @@ -779,6 +1052,7 @@ exports[`renders FAB.Menu closed 1`] = ` exports[`renders FAB.Menu not expanded when trigger is not visible 1`] = ` - - + + - + - Send email - + + + + Send email + + + + + - - - - - + - + - Set reminder - + + + + Set reminder + + + + - - - + + + - + @@ -1356,6 +1878,7 @@ exports[`renders FAB.Menu not expanded when trigger is not visible 1`] = ` { "opacity": 0, }, + {}, ] } /> @@ -1365,7 +1888,7 @@ exports[`renders FAB.Menu not expanded when trigger is not visible 1`] = ` "busy": undefined, "checked": undefined, "disabled": true, - "expanded": undefined, + "expanded": false, "selected": undefined, } } @@ -1378,6 +1901,7 @@ exports[`renders FAB.Menu not expanded when trigger is not visible 1`] = ` } } accessible={true} + aria-haspopup="menu" collapsable={false} focusable={true} onBlur={[Function]} @@ -1425,7 +1949,6 @@ exports[`renders FAB.Menu not expanded when trigger is not visible 1`] = ` "top": 0, }, { - "opacity": 1, "transform": [ { "translateX": 16, @@ -1438,35 +1961,47 @@ exports[`renders FAB.Menu not expanded when trigger is not visible 1`] = ` ] } > - - plus - + + plus + + - - close - + + close + + @@ -1556,6 +2102,7 @@ exports[`renders FAB.Menu not expanded when trigger is not visible 1`] = ` exports[`renders FAB.Menu open 1`] = ` - - + + - + - Send email - + + + + Send email + + + + + - - - - - - + + + - Set reminder - + + + + Set reminder + + + + + - - + @@ -2133,6 +2928,7 @@ exports[`renders FAB.Menu open 1`] = ` { "opacity": 1, }, + {}, ] } /> @@ -2142,7 +2938,7 @@ exports[`renders FAB.Menu open 1`] = ` "busy": undefined, "checked": undefined, "disabled": false, - "expanded": undefined, + "expanded": true, "selected": undefined, } } @@ -2155,6 +2951,7 @@ exports[`renders FAB.Menu open 1`] = ` } } accessible={true} + aria-haspopup="menu" collapsable={false} focusable={true} onBlur={[Function]} @@ -2202,7 +2999,6 @@ exports[`renders FAB.Menu open 1`] = ` "top": 0, }, { - "opacity": 0, "transform": [ { "translateX": 16, @@ -2215,35 +3011,47 @@ exports[`renders FAB.Menu open 1`] = ` ] } > - - plus - + + plus + + - - close - + + close + + @@ -2333,6 +3152,7 @@ exports[`renders FAB.Menu open 1`] = ` exports[`renders FAB.Menu with 6 items 1`] = ` - - + + - + - Item 1 - + + + + Item 1 + + + + + - - - - - + - + - Item 2 - + + + + Item 2 + + + + + - - - - - + - + - Item 3 - + + + + Item 3 + + + + + - - - - - + - + - Item 4 - + + + + Item 4 + + + + + - - - - - + - - Item 5 - - - - - - - - - - + /> + + + + + Item 5 + + + + + + + + + + - + - Item 6 - + + + + Item 6 + + + + + - - + @@ -3614,6 +5110,7 @@ exports[`renders FAB.Menu with 6 items 1`] = ` { "opacity": 1, }, + {}, ] } /> @@ -3623,7 +5120,7 @@ exports[`renders FAB.Menu with 6 items 1`] = ` "busy": undefined, "checked": undefined, "disabled": false, - "expanded": undefined, + "expanded": true, "selected": undefined, } } @@ -3636,6 +5133,7 @@ exports[`renders FAB.Menu with 6 items 1`] = ` } } accessible={true} + aria-haspopup="menu" collapsable={false} focusable={true} onBlur={[Function]} @@ -3683,7 +5181,6 @@ exports[`renders FAB.Menu with 6 items 1`] = ` "top": 0, }, { - "opacity": 0, "transform": [ { "translateX": 16, @@ -3696,35 +5193,47 @@ exports[`renders FAB.Menu with 6 items 1`] = ` ] } > - - plus - + + plus + + - - close - + + close + + @@ -3814,6 +5334,7 @@ exports[`renders FAB.Menu with 6 items 1`] = ` exports[`renders FAB.Menu with center alignment 1`] = ` - - + + - + - Send email - + + + + Send email + + + + + - - - - - + - + - Set reminder - + + + + Set reminder + + + + + - - + @@ -4392,6 +6161,7 @@ exports[`renders FAB.Menu with center alignment 1`] = ` { "opacity": 1, }, + {}, ] } /> @@ -4401,7 +6171,7 @@ exports[`renders FAB.Menu with center alignment 1`] = ` "busy": undefined, "checked": undefined, "disabled": false, - "expanded": undefined, + "expanded": true, "selected": undefined, } } @@ -4414,6 +6184,7 @@ exports[`renders FAB.Menu with center alignment 1`] = ` } } accessible={true} + aria-haspopup="menu" collapsable={false} focusable={true} onBlur={[Function]} @@ -4461,7 +6232,6 @@ exports[`renders FAB.Menu with center alignment 1`] = ` "top": 0, }, { - "opacity": 0, "transform": [ { "translateX": 16, @@ -4474,35 +6244,47 @@ exports[`renders FAB.Menu with center alignment 1`] = ` ] } > - - plus - + + plus + + - - close - + + close + + @@ -4592,6 +6385,7 @@ exports[`renders FAB.Menu with center alignment 1`] = ` exports[`renders FAB.Menu with items having icons 1`] = ` - - + - - email - + /> - + - Send email - + + + email + + + + Send email + + + + + - - - - - - - bell - + /> - + - Set reminder - + + + bell + + + + Set reminder + + + + + - - + @@ -5229,6 +7271,7 @@ exports[`renders FAB.Menu with items having icons 1`] = ` { "opacity": 1, }, + {}, ] } /> @@ -5238,7 +7281,7 @@ exports[`renders FAB.Menu with items having icons 1`] = ` "busy": undefined, "checked": undefined, "disabled": false, - "expanded": undefined, + "expanded": true, "selected": undefined, } } @@ -5251,6 +7294,7 @@ exports[`renders FAB.Menu with items having icons 1`] = ` } } accessible={true} + aria-haspopup="menu" collapsable={false} focusable={true} onBlur={[Function]} @@ -5298,7 +7342,6 @@ exports[`renders FAB.Menu with items having icons 1`] = ` "top": 0, }, { - "opacity": 0, "transform": [ { "translateX": 16, @@ -5311,35 +7354,47 @@ exports[`renders FAB.Menu with items having icons 1`] = ` ] } > - - plus - + + plus + + - - close - + + close + + @@ -5429,6 +7495,7 @@ exports[`renders FAB.Menu with items having icons 1`] = ` exports[`renders FAB.Menu with start alignment 1`] = ` - - + + - + - Send email - + + + + Send email + + + + + - - - - - + - + - Set reminder - + + + + Set reminder + + + + + - - + @@ -6006,6 +8321,7 @@ exports[`renders FAB.Menu with start alignment 1`] = ` { "opacity": 1, }, + {}, ] } /> @@ -6015,7 +8331,7 @@ exports[`renders FAB.Menu with start alignment 1`] = ` "busy": undefined, "checked": undefined, "disabled": false, - "expanded": undefined, + "expanded": true, "selected": undefined, } } @@ -6028,6 +8344,7 @@ exports[`renders FAB.Menu with start alignment 1`] = ` } } accessible={true} + aria-haspopup="menu" collapsable={false} focusable={true} onBlur={[Function]} @@ -6075,7 +8392,6 @@ exports[`renders FAB.Menu with start alignment 1`] = ` "top": 0, }, { - "opacity": 0, "transform": [ { "translateX": 16, @@ -6088,35 +8404,47 @@ exports[`renders FAB.Menu with start alignment 1`] = ` ] } > - - plus - + + plus + + - - close - + + close + +