passwordStrength.tsx 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. import {css} from '@emotion/core';
  4. import styled from '@emotion/styled';
  5. import throttle from 'lodash/throttle';
  6. import zxcvbn from 'zxcvbn';
  7. import {tct} from 'app/locale';
  8. import space from 'app/styles/space';
  9. import theme from 'app/utils/theme';
  10. /**
  11. * NOTE: Do not import this component synchronously. The zxcvbn library is
  12. * relatively large. This component should be loaded async as a split chunk.
  13. */
  14. /**
  15. * The maximum score that zxcvbn reports
  16. */
  17. const MAX_SCORE = 5;
  18. type Props = {
  19. /**
  20. * The password value.
  21. */
  22. value: string;
  23. /**
  24. * A set of labels to display for each password strength level. 5 levels.
  25. */
  26. labels?: [string, string, string, string, string];
  27. /**
  28. * The color to make the progress bar for each strength level. 5 levels.
  29. */
  30. colors?: [string, string, string, string, string];
  31. };
  32. const PasswordStrength = ({
  33. value,
  34. labels = ['Very Weak', 'Very Weak', 'Weak', 'Strong', 'Very Strong'],
  35. colors = [theme.red300, theme.red300, theme.yellow300, theme.green300, theme.green300],
  36. }: Props) => {
  37. if (value === '') {
  38. return null;
  39. }
  40. const result = zxcvbn(value);
  41. if (!result) {
  42. return null;
  43. }
  44. const {score} = result;
  45. const percent = Math.round(((score + 1) / MAX_SCORE) * 100);
  46. const styles = css`
  47. background: ${colors[score]};
  48. width: ${percent}%;
  49. height: 100%;
  50. `;
  51. return (
  52. <React.Fragment>
  53. <StrengthProgress
  54. role="progressbar"
  55. aria-valuenow={score}
  56. aria-valuemin={0}
  57. aria-valuemax={100}
  58. >
  59. <div css={styles} />
  60. </StrengthProgress>
  61. <StrengthLabel>
  62. {tct('Strength: [textScore]', {
  63. textScore: <ScoreText>{labels[score]}</ScoreText>,
  64. })}
  65. </StrengthLabel>
  66. </React.Fragment>
  67. );
  68. };
  69. const StrengthProgress = styled('div')`
  70. background: ${theme.gray200};
  71. height: 8px;
  72. border-radius: 2px;
  73. overflow: hidden;
  74. `;
  75. const StrengthLabel = styled('div')`
  76. font-size: 0.8em;
  77. margin-top: ${space(0.25)};
  78. color: ${theme.gray400};
  79. `;
  80. const ScoreText = styled('strong')`
  81. color: ${p => p.theme.black};
  82. `;
  83. export default PasswordStrength;
  84. /**
  85. * This is a shim that allows the password strength component to be used
  86. * outside of our main react application. Mostly useful since all of our
  87. * registration pages aren't in the react app.
  88. */
  89. export const attachTo = ({input, element}) =>
  90. element &&
  91. input &&
  92. input.addEventListener(
  93. 'input',
  94. throttle(e => {
  95. ReactDOM.render(<PasswordStrength value={e.target.value} />, element);
  96. })
  97. );