Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add slot name to amplify-toast #6479

Closed
nkint opened this issue Aug 3, 2020 · 16 comments
Closed

Add slot name to amplify-toast #6479

nkint opened this issue Aug 3, 2020 · 16 comments
Labels
feature-request Request a new feature UI Related to UI Components

Comments

@nkint
Copy link

nkint commented Aug 3, 2020

Is your feature request related to a problem? Please describe.
I'd like to add custom style to toast notification. I've seen other issue about this, like #5604 #6167 #2834

Describe the solution you'd like
Fully customize toast notification via slot as other ui components do.

I think it's matter of few lines in this file: https://github.com/aws-amplify/amplify-js/blob/3642e6af455ee1848c1cc743a7a503e96baefa41/packages/amplify-ui-components/src/components/amplify-toast/amplify-toast.tsx

render() {
    return (
      <slot name="toast-notification">
        <div class="toast">
          <amplify-icon class="toast-icon" name="warning" />
          {this.message ? <span>{this.message}</span> : null}
          <slot />
          <button class="toast-close" onClick={this.handleClose} />
        </div>
      </slot name="toast-notification">
    );
  }

Could you consider a PR?

@nkint nkint added the feature-request Request a new feature label Aug 3, 2020
@ashika01
Copy link
Contributor

ashika01 commented Aug 3, 2020

@nkint If you are open to getting a PR out I can work with you to get it merged in.

@ashika01
Copy link
Contributor

ashika01 commented Aug 4, 2020

@nkint In your case, I think adding slot wouldn't ideally fix the problem. Since all the error message for toast comes from within amplify, having a slot would just override that. Rather if styling is what you are after, could you explain your style use case deeper so we could see if we can incorporate that into css variables.

@nkint
Copy link
Author

nkint commented Aug 4, 2020

Thanks @ashika01 for the answer

Toast notification are really important part of the ui: feedback against user actions. Due to its importance is something really hard to just incorporate in a css variables and override it. Moreover, notifications can go throughout all my application (not only where amplify is involved).

What about using some other library (in #2834 they told about react-notification-alert)?

I have access to the error anyway, let's take React as example:

	React.useEffect(() => {
		Auth.currentAuthenticatedUser()
			.then((user) => updateUser(user))
			.catch(() => console.log('No signed in user.'));
		Hub.listen('auth', (data) => {

			console.log({ data });  // <------------ here I have error payload anyway

			switch (data.payload.event) {
				case 'signIn':
					return updateUser(data.payload.data);
				case 'signOut':
					return updateUser(null);
				default:
					return null;
			}
		});
	}, []);

And I can override amplify-toast hiding it and using my own feedback notification library as I prefer.

@ashika01
Copy link
Contributor

ashika01 commented Aug 4, 2020

@nkint Definitely a great use case I totally don't deny that. I can open up a slot for toast as a presentational component. But wanted to see if you have considered/understood the implication. Since overriding slot would mean you wont get the default ones anymore. I am looking into this. Any feedback on CSS based style improvements? (since not everyone would want to write their own notification component) I can try to bake those in a PR too.

@nikevp
Copy link

nikevp commented Aug 31, 2020

+1

@mjhecht
Copy link

mjhecht commented Sep 9, 2020

+1. I am trying to figure out how to customize the toast. It looks like the theme property on the AmplifyAuthenticator component isn't being used any more (as of the 4/2020 release). Using .toast {} in my app's CSS isn't affecting the toast. Our issue is that our sign-up form is long, so the toast shows off the screen, at the top of the page (because it has position: absolute). I've been digging for a while and not finding much information about how to override the toast styles.

Update: AmplifyAuthenticator is behind a shadow DOM; I can grab and style that via JS. But the toast is behind yet another shadow DOM, and I'm having trouble getting to that. It would need to be done via polling, or another hacky solution.

@ghost
Copy link

ghost commented Sep 14, 2020

Two options seem appropriate:

  1. the toast should be stylistically compatible with the rest of the Amplify UI - at the moment it isn't and looks like an afterthought
  2. It would be great if you could register a toast callback, which would enable integration with, for example, Bootstrap toast

@hrhosni
Copy link

hrhosni commented Sep 27, 2020

Here's a very ugly workaround until a better solution is proposed. The idea is to catch error messages and display them ourselves in our own custom Toast.
The ugliest part is probably having to hide the default Toast. I couldn't do it without using a setTimeout tbh :(

import React from "react";

// Amplify
import {
  AmplifyAuthenticator,
  AmplifySignUp,
  AmplifySignIn,
  AmplifyConfirmSignUp,
} from "@aws-amplify/ui-react";
import { Hub } from "aws-amplify";

// Material UI
import IconButton from "@material-ui/core/IconButton";
import ClearIcon from "@material-ui/icons/Clear";
import WarningIcon from "@material-ui/icons/Warning";

// CSS
import classes from "./index.module.css";

const CustomAmplifyAuthenticator = ({ signinHeader, signupHeader }) => {
  const [errorMsg, setErrorMsg] = React.useState(null);

  React.useEffect(() => {
    Hub.listen("auth", (res) => {
      let errorMsg = res?.payload?.data?.message
        ? res.payload.data.message
        : null;

      if (!errorMsg) return;

      switch (res.payload.data.message) {
        case "Custom auth lambda trigger is not configured for the user pool.":
          errorMsg = "Password cannot be empty";
          break;

        case "1 validation error detected: Value at 'password' failed to satisfy constraint: Member must have length greater than or equal to 6":
          errorMsg = "Password not long enough";
          break;

        case "Password did not conform with policy: Password not long enough":
          errorMsg = "Password not long enough";
          break;

        case "Attribute value for given_name must not be null":
          errorMsg = "First Name required";
          break;

        case "Attribute value for family_name must not be null":
          errorMsg = "Last Name required";
          break;

        case "Username/client id combination not found.":
          errorMsg = "User not found";
          break;

        default:
      }

      // Hide Amplify's default Toast since we're showing our own
      const target = document.getElementsByTagName("amplify-authenticator")[0];
      if (target?.shadowRoot?.children) {
        setTimeout(() => {
          target.shadowRoot.children[0].style.display = "none";
        }, 100); // needed because node isn't injected straight away
      }

      setErrorMsg(errorMsg);
    });
  }, []);

  const CustomToast = () => {
    return (
      <div className={classes.customToast}>
        <div className={classes.notice}>
          <WarningIcon />
          <span className={classes.errorMsg}>{errorMsg}</span>
        </div>
        <div className={classes.clearIcon}>
          <IconButton
            onClick={() => setErrorMsg(null)}
            color="inherit"
            size="small"
            aria-label="delete"
          >
            <ClearIcon />
          </IconButton>
        </div>
      </div>
    );
  };

  return (
    <>
      {errorMsg && <CustomToast />}
      <AmplifyAuthenticator usernameAlias="email">
        <AmplifySignUp
          slot="sign-up"
          headerText={signupHeader || "Create a new account"}
          usernameAlias="email"
          formFields={[
            {
              type: "email",
              required: true,
            },
            {
              type: "password",
              required: true,
            },
            {
              type: "given_name",
              label: "First Name *",
              placeholder: "First Name",
              required: true,
            },
            {
              type: "family_name",
              label: "Last Name *",
              placeholder: "Last Name",
              required: true,
            },
          ]}
        />
        <AmplifySignIn
          slot="sign-in"
          headerText={signinHeader || "Sign in to your account"}
          usernameAlias="email"
        />
        <AmplifyConfirmSignUp
          slot="confirm-sign-up"
          headerText="Enter the Confirmation Code you received by email"
          usernameAlias="email"
        ></AmplifyConfirmSignUp>
      </AmplifyAuthenticator>
    </>
  );
};

export default CustomAmplifyAuthenticator;

Here's the css in case anyone's interested:

.customToast {
  display: flex;
  justify-content: space-between;
  position: absolute;
  top: 0px;
  left: 0px;
  width: 100%;
  z-index: 99;
  box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 5px 0px;
  padding: 16px;
  background-color: var(--amplify-secondary-tint);
  font-size: var(--amplify-text-sm);
  color: var(--amplify-white);
  box-sizing: border-box;
  border-radius: 5px;
  font-family: var(--amplify-font-family);
}

.clearIcon {
  text-align: right;
}

.notice {
  display: flex;
  align-items: center;
}

.errorMsg {
  padding: 0px 5px;
}

N.B. If your goal is NOT to create your own Toast but rather to just overwrite the error messages, then this is the way to go:

import { I18n } from "aws-amplify";

// Overwrite error messages
  I18n.putVocabularies({
    en: {
      "Custom auth lambda trigger is not configured for the user pool.":
        "Password cannot be empty",
      "1 validation error detected: Value at 'password' failed to satisfy constraint: Member must have length greater than or equal to 6":
        "Password not long enough",
      "Password did not conform with policy: Password not long enough":
        "Password not long enough",
      "Attribute value for given_name must not be null": "First Name required",
      "Attribute value for family_name must not be null": "Last Name required",
      "Username/client id combination not found.": "User not found",
    },
  });

@1443658
Copy link

1443658 commented Nov 19, 2020

@hrhosni
Nice solution, but it doesn't seems to work if both username and password are empty, which no auth request is sent in this case..
Do you have any ideas to deal with it? Thanks.

@hrhosni
Copy link

hrhosni commented Nov 19, 2020

@1443658 Note that I ended up going for the I18n.putVocabularies solution (bottom of my answer above). It's a neater workaround.

My objective was not to create my own toast but rather to overwrite the error messages.
The default error message for leaving both username and password empty is Username cannot be empty I believe?
I'm totally fine with it so I didn't have to worry about overwriting it.

@1443658
Copy link

1443658 commented Dec 23, 2020

I have solved the problem.

It should listen to UI Auth instead of auth channel, then check whether the res.payload.event is ToastAuthError. It would literally captured all events related to authorization including case that no actual auth request is sent.

Here is the code :

Hub.listen("UI Auth", (res) => {  //Listen to UI Auth channel
      if (res?.payload?.event === 'ToastAuthError') {
        let errorMsg = res?.payload?.message
          ? res.payload.message
          : null;

        switch (res.payload.message?.trim()) {
          case "Custom auth lambda trigger is not configured for the user pool.":
            errorMsg = "Password cannot be empty";
            break;
           .......  // The rest is the same

On hiding the default Toast, I've changed to use interval to loop on it instead of just once, so that it is still safe to set a shorter waiting time (Not sure whether it has significant improvement though).

  const hideToast = () => {
    const target: HTMLElement = document.getElementsByTagName("amplify-authenticator")[0];
    if (target?.shadowRoot?.children) {
      let el!: HTMLElement;

      let interval = setInterval(function () {
        el = target!.shadowRoot!.children[0] as HTMLElement;
        if (el.tagName.toLocaleLowerCase() === "amplify-toast") {
          el.style.display = "none";
          clearInterval(interval);
        }
      }, 1);  // delay time = 1
    }
  }

@DerekFei
Copy link

I have solved the problem.

It should listen to UI Auth instead of auth channel, then check whether the res.payload.event is ToastAuthError. It would literally captured all events related to authorization including case that no actual auth request is sent.

Here is the code :

Hub.listen("UI Auth", (res) => {  //Listen to UI Auth channel
      if (res?.payload?.event === 'ToastAuthError') {
        let errorMsg = res?.payload?.message
          ? res.payload.message
          : null;

        switch (res.payload.message?.trim()) {
          case "Custom auth lambda trigger is not configured for the user pool.":
            errorMsg = "Password cannot be empty";
            break;
           .......  // The rest is the same

On hiding the default Toast, I've changed to use interval to loop on it instead of just once, so that it is still safe to set a shorter waiting time (Not sure whether it has significant improvement though).

  const hideToast = () => {
    const target: HTMLElement = document.getElementsByTagName("amplify-authenticator")[0];
    if (target?.shadowRoot?.children) {
      let el!: HTMLElement;

      let interval = setInterval(function () {
        el = target!.shadowRoot!.children[0] as HTMLElement;
        if (el.tagName.toLocaleLowerCase() === "amplify-toast") {
          el.style.display = "none";
          clearInterval(interval);
        }
      }, 1);  // delay time = 1
    }
  }

so hacky but so far it's the best way I guess.

@grimm2x
Copy link

grimm2x commented Mar 9, 2021

On hiding the default Toast, I've changed to use interval to loop on it instead of just once, so that it is still safe to set a shorter waiting time (Not sure whether it has significant improvement though).

  const hideToast = () => {
    const target: HTMLElement = document.getElementsByTagName("amplify-authenticator")[0];
    if (target?.shadowRoot?.children) {
      let el!: HTMLElement;

      let interval = setInterval(function () {
        el = target!.shadowRoot!.children[0] as HTMLElement;
        if (el.tagName.toLocaleLowerCase() === "amplify-toast") {
          el.style.display = "none";
          clearInterval(interval);
        }
      }, 1);  // delay time = 1
    }
  }

I found setting the message to undefined prevents the default Toast from showing:

    Hub.listen("UI Auth", (res) => {
      if (res?.payload?.event == 'ToastAuthError') {
        const msg = res.payload.message
        res.payload.message = undefined
        // custom handler
        console.log(msg)
      }
    })

@Luke-Davies
Copy link
Contributor

Luke-Davies commented Mar 12, 2021

You can now prevent the default toast from showing using the hideToast prop on AmplifyAuthenticator.

See this simple example from #7129 for how to implement your own notification/alert component in react (typescript):

import React, { useState, useEffect } from 'react';
import { Hub, HubCallback } from '@aws-amplify/core';
import {
  AmplifyAuthenticator,
} from '@aws-amplify/ui-react';
import {
 UI_AUTH_CHANNEL, TOAST_AUTH_ERROR_EVENT
} from '@aws-amplify/ui-components';

const MyAuth: React.FC = ({ children }) => {

  const [alertMessage, setAlertMessage] = useState('');

  const handleToastErrors: HubCallback = ({ payload }) => {
    if (payload.event === TOAST_AUTH_ERROR_EVENT && payload.message) {
      setAlertMessage(payload.message);
    }
  };

  useEffect(() => {
    Hub.listen(UI_AUTH_CHANNEL, handleToastErrors);
    return () => Hub.remove(UI_AUTH_CHANNEL, handleToastErrors);
  });

  return (
    <>
      {alertMessage && (<div>{alertMessage}</div>)}
      <AmplifyAuthenticator hideToast>
        // ...........
      </AmplifyAuthenticator>
    </>
  );
}

You can just replace <div>{alertMessage}</div> with your own component, using your own styles etc (for example a modal).

For anyone wondering why this ended up as a prop instead of a slot as suggested at the top of this issue; a slot approach was investigated (#7601) but the implementation would have required making trade-offs that the prop approach simply avoids.

@wlee221
Copy link
Contributor

wlee221 commented Mar 16, 2021

Closing as #7129 resolves this issue. Please feel free to ask any questions. Thanks again, @Luke-Davies!

@github-actions
Copy link

This issue has been automatically locked since there hasn't been any recent activity after it was closed. Please open a new issue for related bugs.

Looking for a help forum? We recommend joining the Amplify Community Discord server *-help channels or Discussions for those types of questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 20, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
feature-request Request a new feature UI Related to UI Components
Projects
None yet
Development

No branches or pull requests