0
\$\begingroup\$

I've written this Email component which obfuscates the email until the user hovers over it:

import React, {
  AnchorHTMLAttributes,
  DetailedHTMLProps,
  ReactNode,
  useState,
} from "react";
import { percentEncodeParams } from "./utils";

function obfuscateEmail(email: string): JSX.Element {
  const [username, domain] = email.split("@");
  return (
    <>
      <style
        dangerouslySetInnerHTML={{
          __html: `
          a>span.roe::after {
            content: "@";
          }
        `,
        }}
      />
      {username}
      <span className="roe" />
      {domain}
    </>
  );
}

type Props = DetailedHTMLProps<
  AnchorHTMLAttributes<HTMLAnchorElement>,
  HTMLAnchorElement
> & {
  /** blind carbon copy e-mail addresses */
  bcc?: string[];
  /** body of e-mail */
  body?: string;
  /** carbon copy e-mail addresses */
  cc?: string[];
  children?: ReactNode;
  /** e-mail recipient address */
  email: string;
  /** subject of e-mail */
  subject?: string;
};
export function Email({
  bcc = [],
  body = "",
  cc = [],
  children,
  email,
  subject = "",
  ...props
}: Props): JSX.Element {
  const [hovered, setHovered] = useState(false);
  const emailUrl = new URL(`mailto:${email}`);

  // https://github.com/whatwg/url/issues/18#issuecomment-369865339
  emailUrl.search = percentEncodeParams({ bcc, body, cc, subject });

  function handleHover() {
    setHovered(true);
  }

  const displayText = children || email;
  const obfuscatedText = children || obfuscateEmail(email);

  return (
    <a
      href={hovered ? emailUrl.href : "#"}
      onFocus={handleHover}
      onMouseOver={handleHover}
      {...props}
    >
      {hovered ? displayText : obfuscatedText}
    </a>
  );
}

This is utils.js:

type StringParam = [string, string];

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isValidStringParam(param: any): param is StringParam {
  const [, value] = param;
  return typeof value === "string" && value.length > 0;
}

function paramToStringParam([key, value]: [
  string,
  string | string[]
]): StringParam {
  return Array.isArray(value) ? [key, value.join(",")] : [key, value];
}

/*
  Do not use URLSearchParams.

  URLSearchParams turns spaces into '+':
      https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams#examples

  Some email clients on mobile devices don't replace the '+' with spaces.

  According to the mailto spec, spaces should be percent encoded:
      https://datatracker.ietf.org/doc/html/rfc6068#ref-STD66
*/
export function percentEncodeParams(params: {
  bcc?: string[];
  body?: string;
  cc?: string[];
  subject?: string;
}): string {
  return Object.entries(params)
    .map(paramToStringParam)
    .filter(isValidStringParam)
    .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
    .join("&");
}

The idea is to strip out the @ symbol and use CSS to render it until the user hovers over it, in which case it swaps it for the original email so the user can copy paste it or interact with it.

Any drawbacks to this approach or ideas to make it more robust and bullet proof?

Any a11y issues?

Any feedback in general is appreciated.

CodeSandbox working example: https://codesandbox.io/s/hopeful-hamilton-oolrz

\$\endgroup\$
3
  • 1
    \$\begingroup\$ Would you be so kind a provide a codesandbox.io working example? thats helps aaaaaaaaaalot \$\endgroup\$
    – flx
    Aug 24, 2021 at 8:56
  • \$\begingroup\$ @flx Thanks for the suggestion! Included a link to a codesandbox.io working example. \$\endgroup\$ Aug 24, 2021 at 20:51
  • \$\begingroup\$ The only drawback I can is that you are using dangerouslySetInnerHTML and that "hover" and mobile devices are not the best friends. Another thing that MIGHT (probably not) happen is if a browser tries to transform your email automatically to a link it won't be able to as it's not recognized as such \$\endgroup\$
    – flx
    Aug 25, 2021 at 5:45

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.