mui / material-ui

Material UI: Comprehensive React component library that implements Google's Material Design. Free forever.
https://mui.com/material-ui/
MIT License
93.74k stars 32.24k forks source link

[FloatingActionButton] Animate expanding `Icon` and apply `IconStyle` to `EnhancedButton` #1574

Closed TrySpace closed 8 years ago

TrySpace commented 9 years ago

I'm trying to expand the FloatingActionButton with style (height&width++) but when trying to do this for the inner element with iconStyle with the FontIcon it won't apply to the background (EnhancedButton), only the icon itself. And there will still be a floating smaller button in the FloatingActionButton

image

Even when not using FontIcon (which the documentation implies it would work on) but just a div, the EnhancedButton won't adhere to my overwrite styles:

image

Also, the TouchRipple doesn't seem to accept the new width:

image

What I'm trying to achieve here is animating the FloatingActionButton to a particular size, change the borderRadius to 0, then replacing it's content with some other element:

image

floatiy-expand

The animation works very nicely already, I just need to be freed from the restrictions of it always being a button.

In the end I think there should be two extra options like: expandStyle to apply to the enhancedButton (which wouldn't function as a button anymore) and the FloatingActionButton's Paper element to resize them uniformly on expand; And an expandButton function or state true/false, or preferably both. Or maybe make a seperate element like ExpandingFloatingActionButton to keep the code clean.

TrySpace commented 9 years ago

Disabling the height, width and border-radius on the button would fix this actually.

image

But, the animation would break, so the height and width should be changed with the iconStyle styles.

So in conclusion, I think the iconStyle just needs to be applied to the button in this case, this way I could also overwrite the borderRadius to 0

TrySpace commented 9 years ago

My code for the curious:

image

TrySpace commented 9 years ago

Just adding iconStyle to the EnhancedButton solved this (in floating-action-button.jsx):

   style={this.mergeAndPrefix(
        styles.container,
        this.props.mini && styles.containerWhenMini,
        iconStyle
      )}

much-wow

the animation might need some tweaking, because the bottom has some delay though.

TrySpace commented 9 years ago

Where I am now:

image

getDefaultProps: ->
    floatyAddButton:
      floatyAddButtonStyle:
        position: 'fixed'
        right: 16
        bottom: 16
        height: 56
        width: 56
        borderRadius: '50%'
      floatyAddButtonStyleExpanded:
        position: 'fixed'
        right: 16
        bottom: 16
        height: 256
        width: 356
        borderRadius: 0
      floatyAddButtonIconStyle:
        height: 56
        width: 56
        borderRadius: '50%'
      floatyAddButtonIconStyleExpanded:
        height: 256
        width: 356
        borderRadius: 0
    floatyAppButton:
      floatyAppButtonStyle:
        position: 'fixed'
        left: 16
        bottom: 16
        height: 56
        width: 56
        borderRadius: '50%'
      floatyAppButtonStyleExpanded:
        position: 'fixed'
        left: 16
        bottom: 16
        height: 256
        width: 356
        borderRadius: 0
      floatyAppButtonIconStyle:
        height: 56
        width: 56
        borderRadius: '50%'
      floatyAppButtonIconStyleExpanded:
        height: 256
        width: 356
        borderRadius: 0
  getInitialState: ->
    floatyAddButtonExpanded: false
    floatyAddButtonStyle: @props.floatyAddButton.floatyAddButtonStyle
    floatyAddButtonIconStyle: @props.floatyAddButton.floatyAddButtonIconStyle
    floatyAppButtonExpanded: false
    floatyAppButtonStyle: @props.floatyAppButton.floatyAppButtonStyle
    floatyAppButtonIconStyle: @props.floatyAppButton.floatyAppButtonIconStyle
    floatyAppButton: @props.floatyAppButton
  expandAdd: ->
    if !@state.floatyAddButtonExpanded
      @setState
        floatyAddButtonExpanded: true
        floatyAddButtonStyle: @props.floatyAddButton.floatyAddButtonStyleExpanded
        floatyAddButtonIconStyle: @props.floatyAddButton.floatyAddButtonIconStyleExpanded
    else
      @setState
        floatyAddButtonExpanded: false
        floatyAddButtonStyle: @props.floatyAddButton.floatyAddButtonStyle
        floatyAddButtonIconStyle: @props.floatyAddButton.floatyAddButtonIconStyle
  expandApps: ->
    if !@state.floatyAppButtonExpanded
       @setState
        floatyAppButtonExpanded: true
        floatyAppButtonStyle: @props.floatyAppButton.floatyAppButtonStyleExpanded
        floatyAppButtonIconStyle: @props.floatyAppButton.floatyAppButtonIconStyleExpanded
    else
      @setState
        floatyAppButtonExpanded: false
        floatyAppButtonStyle: @props.floatyAppButton.floatyAppButtonStyle
        floatyAppButtonIconStyle: @props.floatyAppButton.floatyAppButtonIconStyle
  render: ->
    <div>
      <FloatingActionButton onClick={@expandAdd} style={@state.floatyAddButtonStyle} iconStyle={@state.floatyAddButtonIconStyle} secondary={true} >
        {if @state.floatyAddButtonExpanded then <div className="app-add"> much wow </div> else <FontIcon className="material-icons">add</FontIcon>}
      </FloatingActionButton>
      <FloatingActionButton onClick={@expandApps} style={@state.floatyAppButtonStyle} iconStyle={@state.floatyAppButtonIconStyle}  >
        {if @state.floatyAppButtonExpanded then <div className="app-app"> much wow </div> else <FontIcon className="material-icons">apps</FontIcon>}
      </FloatingActionButton>
    </div>

It is getting a little repetitious after adding a second button. And I'm thinking about maybe needing a background div/shade, so that when you click outside the box it will 'dismiss' the expansion and revert to a button. Sort of like how the dialogue or sidebar works. And the ripple would need some tweaking.

expandybutotns

Not sure right now how to use less state variables right now.

TrySpace commented 9 years ago

I put in a Card

cardinbutton

I set the height of the expanded styles to auto And can stop the button click from closing it by using: e.stopPropagation() on the onClick of FlatButton

But if I use 80% as height, the animation is smoother, but the CardText element doesn't show up:

notxtfloat

But we're getting there..

Also, the CardHeader elements get centered.

TrySpace commented 9 years ago

Next problem, when putting a TextInput element inside, it triggers the onClick on parent element FloatingActionButton, when pressing space:

space close

Also e.stopPropagation() doesn't help.

And the FloatingActionButton should "only listen to left clicks" it says in the source at the _handleMouseDown method

I think it might be loosely related to: https://github.com/callemall/material-ui/issues/1595 , not directly, but in the way that events are handled in mui, without keeping edge cases in sight. What I'm trying to say is that the FloatingButton has a very singular purpose right now and is not very dynamic.

Update: I solved it by using onMouseDown instead of onClick

chrismcv commented 9 years ago

have you considered having separate elements and animating between them? using something like http://kushagragour.in/lab/ctajs/

TrySpace commented 9 years ago

@chrismcv Not yet, but good tip, thanks!

TrySpace commented 8 years ago

Any progression on implementing animations natively with ctajs or react-motion?

mbrookes commented 8 years ago

No, still a dream away right now. The focus at the moment is on getting core cleaned up to make those sorts of changes easier.

I hadn't come across this post before, but I have to say, that FAB Morph is pretty nifty! :heart_eyes: Have you developed them further since?

kn327 commented 10 months ago

This issue is closed, however for simply animating the MuiFab component transitioning between an "open" and "closed" state (ie. showing and hiding the text/description) below is a slim wrapper which will animate the opening and closing action

"use client";

import { HTMLAttributeAnchorTarget, useEffect, useState } from "react";

import MuiFab, { FabProps as MuiFabProps } from "@mui/material/Fab";
import MuiCollapse from "@mui/material/Collapse";
import MuiTypography from "@mui/material/Typography";

export interface FabProps extends React.PropsWithChildren {
    icon?: React.ReactNode;
    iconPosition?: "start" | "end";
    size?: MuiFabProps["size"];
    href?: MuiFabProps["href"];
    target?: HTMLAttributeAnchorTarget;
    "aria-label"?: MuiFabProps["aria-label"];
    open?: boolean;
    expandable?: boolean | "hover" | "click";
    onClick?: MuiFabProps["onClick"];
    sx?: MuiFabProps["sx"];
    disablePadding?: boolean;
}
export function Fab({
    icon,
    iconPosition = "start",
    size,
    onClick,
    href: defaultHref,
    target: defaultTarget,
    "aria-label": ariaLabel,
    open,
    expandable,
    disablePadding,

    children,
}: FabProps) {

    const [ expanded, setExpanded ] = useState(false);

    const hover = expandable === "hover" || expandable === true;
    const click = expandable === "click";

    const isHovered = 
        (hover || click) && (open || expanded);

    function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
        e.stopPropagation();
        e.nativeEvent.stopImmediatePropagation();
        if (isHovered && onClick) {
            onClick(e);
        }
        // kinda a shitty hack
        // but without this, the href will never fire
        // no idea why...
        window.setTimeout(_ => {
            setExpanded(prev => !prev);
        }, 0);
    }

    function handleHoverIn() {
        setExpanded(true);
    }

    function handleHoverOut() {
        setExpanded(false);
    }

    /**
     * Pseudo ClickAwayListener
     * 
     * The MuiClickAwayListener has a delay when clicking
     * away from the fab button causing a need to double-click
     * to re-collapse the fab button
     */
    useEffect(() => {
        // hovering is not set to click
        if (!click) {
            return;
        }

        function handleClickAway() {
            setExpanded(false);
        }

        const documentEvents: (keyof DocumentEventMap)[] = [
            "click",
            "touchstart"
        ];
        const eventOptions: (boolean | AddEventListenerOptions) = false;

        documentEvents.forEach(e =>
            document.addEventListener(e, handleClickAway, eventOptions)
        );

        return () => {
            documentEvents.forEach(e =>
                document.removeEventListener(e, handleClickAway, eventOptions)
            );
        };

    }, [ click ]);

    const startIcon = iconPosition === "start" && icon;
    const endIcon = iconPosition === "end" && icon;

    const href = defaultHref ? (!click || isHovered ? defaultHref : "#") : undefined;

    const target = href && href === "#" ? undefined : defaultTarget;

    return (
    <MuiFab
        size={size}
        variant="extended"
        aria-label={ariaLabel}

        href={href}
        {...{
            // directly setting this value will cause
            // typescript errors
            target: target
        }}
        onClick={click ? handleClick : onClick}
        onMouseOver={hover ? handleHoverIn : undefined}
        onMouseOut={hover ? handleHoverOut : undefined}
        sx={{
            paddingX: disablePadding || isHovered ? undefined : 0
        }}
        >
        {startIcon}
        <MuiCollapse
            in={isHovered}
            orientation="horizontal"
            sx={{
                alignSelf: "center"
            }}>
            <MuiTypography
                component="span"
                sx={{
                    ml: Boolean(startIcon) ? 1 : 0,
                    mr: Boolean(endIcon) ? 1 : 0,
                    fontSize: "inherit" }}
                >
                {children}
            </MuiTypography>
        </MuiCollapse>
        {endIcon}
    </MuiFab>
    );
}

Usage:

export function DonateFab() {
    return (
    <Fab
        size="medium"
        icon={<VolunteerActivismIcon />}
        href="example.com"
        target="_blank"
        expandable
        >
        Donate Now!
    </Fab>);
}
export function DonateFab() {
    return (
    <Fab
        size="medium"
        icon={<VolunteerActivismIcon />}
        href="example.com"
        target="_blank"
        expandable="click"
        >
        Donate Now!
    </Fab>);
}