@transferwise/components 0.0.0-experimental-fa47f3d → 0.0.0-experimental-98fd351

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,16 +26,16 @@ const Snackbar = props => {
26
26
  }, []);
27
27
  // Runs after every render to mirror the componentDidUpdate lifecycle this component previously
28
28
  // relied on, including the shouldComponentUpdate guard that ignored every update without text.
29
- // Both rules below are disabled on purpose: the dependency-less effect and its synchronous state
30
- // updates are what keep this conversion behaviour-identical to the class component.
29
+ // Layout, not passive, so a notification is shown or replaced before the browser paints, as
30
+ // componentDidUpdate did. Both rules below are disabled on purpose: the dependency-less effect
31
+ // and its synchronous state updates are what keep this conversion behaviour-identical.
31
32
  /* eslint-disable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */
32
- React.useEffect(() => {
33
+ React.useLayoutEffect(() => {
33
34
  const previousProps = committedProps.current;
34
35
  committedProps.current = props;
35
36
  const {
36
37
  action,
37
38
  text,
38
- timeout,
39
39
  timestamp
40
40
  } = props;
41
41
  // shouldComponentUpdate bailed out whenever the incoming text was empty. That also blocked the
@@ -52,7 +52,7 @@ const Snackbar = props => {
52
52
  ...current,
53
53
  visible: false
54
54
  })));
55
- }, timeout);
55
+ }, committedProps.current.timeout);
56
56
  };
57
57
  const show = () => {
58
58
  setState(current => ({
@@ -1 +1 @@
1
- {"version":3,"file":"Snackbar.js","sources":["../../src/Snackbar/Snackbar.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport { CSSTransition } from 'react-transition-group';\n\nimport Button from '../Button';\nimport Body from '../Body';\nimport type { ThemeDark, ThemeLight } from '../common';\nimport withNextPortal from '../WithNextPortal/WithNextPortal';\n\nconst CSS_TRANSITION_DURATION = 400;\n\nexport interface SnackbarProps {\n action?: {\n label: string;\n onClick?: React.MouseEventHandler<HTMLButtonElement>;\n };\n text: React.ReactNode;\n /** @deprecated */\n theme?: ThemeLight | ThemeDark;\n timeout: number;\n timestamp: number;\n}\n\ninterface SnackbarState extends Pick<SnackbarProps, 'action' | 'text' | 'theme'> {\n visible: boolean;\n}\n\nexport const Snackbar = (props: SnackbarProps) => {\n const bodyRef = useRef<HTMLSpanElement>(null);\n const leaveTimeout = useRef(0);\n const transitionTimeout = useRef(0);\n /** The props of the last committed render, as `this.props` was in the class component. */\n const committedProps = useRef(props);\n\n const [state, setState] = useState<SnackbarState>({\n visible: false,\n text: '',\n });\n\n useEffect(\n () => () => {\n window.clearTimeout(leaveTimeout.current);\n window.clearTimeout(transitionTimeout.current);\n },\n [],\n );\n\n // Runs after every render to mirror the componentDidUpdate lifecycle this component previously\n // relied on, including the shouldComponentUpdate guard that ignored every update without text.\n // Both rules below are disabled on purpose: the dependency-less effect and its synchronous state\n // updates are what keep this conversion behaviour-identical to the class component.\n /* eslint-disable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */\n useEffect(() => {\n const previousProps = committedProps.current;\n committedProps.current = props;\n\n const { action, text, timeout, timestamp } = props;\n\n // shouldComponentUpdate bailed out whenever the incoming text was empty. That also blocked the\n // updates scheduled below, so a Snackbar whose text is cleared stays on screen until it is\n // given text again. Preserved here deliberately to keep this refactor behaviour-identical.\n const updateWhileTextIsPresent = (update: () => void) => {\n if (committedProps.current.text) {\n update();\n }\n };\n\n const setLeaveTimeout = () => {\n leaveTimeout.current = window.setTimeout(() => {\n updateWhileTextIsPresent(() => setState((current) => ({ ...current, visible: false })));\n }, timeout);\n };\n\n const show = () => {\n setState((current) => ({ ...current, visible: true, action, text }));\n setLeaveTimeout();\n };\n\n if (!text) {\n return;\n }\n\n if (!previousProps.text) {\n show();\n } else if (previousProps.timestamp !== timestamp) {\n window.clearTimeout(leaveTimeout.current);\n\n if (state.visible) {\n setState((current) => ({ ...current, visible: false }));\n transitionTimeout.current = window.setTimeout(() => {\n updateWhileTextIsPresent(show);\n }, CSS_TRANSITION_DURATION);\n } else {\n show();\n }\n }\n });\n /* eslint-enable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */\n\n const { action, text, visible } = state;\n const { timeout } = props;\n\n return (\n <div className=\"snackbar\" aria-live=\"assertive\" role=\"alert\">\n <CSSTransition\n nodeRef={bodyRef}\n in={visible}\n classNames=\"snackbar__text-container\"\n timeout={{\n appear: 0,\n enter: timeout,\n exit: CSS_TRANSITION_DURATION,\n }}\n unmountOnExit\n >\n <Body ref={bodyRef} as=\"span\" className=\"snackbar__text\">\n {text}\n {action ? (\n <Button className=\"snackbar__text__action\" v2 size=\"sm\" onClick={action.onClick}>\n {action.label}\n </Button>\n ) : null}\n </Body>\n </CSSTransition>\n </div>\n );\n};\n\nexport default withNextPortal(Snackbar);\n"],"names":["CSS_TRANSITION_DURATION","Snackbar","props","bodyRef","useRef","leaveTimeout","transitionTimeout","committedProps","state","setState","useState","visible","text","useEffect","window","clearTimeout","current","previousProps","action","timeout","timestamp","updateWhileTextIsPresent","update","setLeaveTimeout","setTimeout","show","_jsx","className","role","children","CSSTransition","nodeRef","in","classNames","appear","enter","exit","unmountOnExit","_jsxs","Body","ref","as","Button","v2","size","onClick","label","withNextPortal"],"mappings":"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"}
1
+ {"version":3,"file":"Snackbar.js","sources":["../../src/Snackbar/Snackbar.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport { CSSTransition } from 'react-transition-group';\n\nimport Button from '../Button';\nimport Body from '../Body';\nimport type { ThemeDark, ThemeLight } from '../common';\nimport withNextPortal from '../WithNextPortal/WithNextPortal';\n\nconst CSS_TRANSITION_DURATION = 400;\n\nexport interface SnackbarProps {\n action?: {\n label: string;\n onClick?: React.MouseEventHandler<HTMLButtonElement>;\n };\n text: React.ReactNode;\n /** @deprecated */\n theme?: ThemeLight | ThemeDark;\n timeout: number;\n timestamp: number;\n}\n\ninterface SnackbarState extends Pick<SnackbarProps, 'action' | 'text' | 'theme'> {\n visible: boolean;\n}\n\nexport const Snackbar = (props: SnackbarProps) => {\n const bodyRef = useRef<HTMLSpanElement>(null);\n const leaveTimeout = useRef(0);\n const transitionTimeout = useRef(0);\n /** The props of the last committed render, as `this.props` was in the class component. */\n const committedProps = useRef(props);\n\n const [state, setState] = useState<SnackbarState>({\n visible: false,\n text: '',\n });\n\n useEffect(\n () => () => {\n window.clearTimeout(leaveTimeout.current);\n window.clearTimeout(transitionTimeout.current);\n },\n [],\n );\n\n // Runs after every render to mirror the componentDidUpdate lifecycle this component previously\n // relied on, including the shouldComponentUpdate guard that ignored every update without text.\n // Layout, not passive, so a notification is shown or replaced before the browser paints, as\n // componentDidUpdate did. Both rules below are disabled on purpose: the dependency-less effect\n // and its synchronous state updates are what keep this conversion behaviour-identical.\n /* eslint-disable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */\n useLayoutEffect(() => {\n const previousProps = committedProps.current;\n committedProps.current = props;\n\n const { action, text, timestamp } = props;\n\n // shouldComponentUpdate bailed out whenever the incoming text was empty. That also blocked the\n // updates scheduled below, so a Snackbar whose text is cleared stays on screen until it is\n // given text again. Preserved here deliberately to keep this refactor behaviour-identical.\n const updateWhileTextIsPresent = (update: () => void) => {\n if (committedProps.current.text) {\n update();\n }\n };\n\n const setLeaveTimeout = () => {\n leaveTimeout.current = window.setTimeout(() => {\n updateWhileTextIsPresent(() => setState((current) => ({ ...current, visible: false })));\n }, committedProps.current.timeout);\n };\n\n const show = () => {\n setState((current) => ({ ...current, visible: true, action, text }));\n setLeaveTimeout();\n };\n\n if (!text) {\n return;\n }\n\n if (!previousProps.text) {\n show();\n } else if (previousProps.timestamp !== timestamp) {\n window.clearTimeout(leaveTimeout.current);\n\n if (state.visible) {\n setState((current) => ({ ...current, visible: false }));\n transitionTimeout.current = window.setTimeout(() => {\n updateWhileTextIsPresent(show);\n }, CSS_TRANSITION_DURATION);\n } else {\n show();\n }\n }\n });\n /* eslint-enable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */\n\n const { action, text, visible } = state;\n const { timeout } = props;\n\n return (\n <div className=\"snackbar\" aria-live=\"assertive\" role=\"alert\">\n <CSSTransition\n nodeRef={bodyRef}\n in={visible}\n classNames=\"snackbar__text-container\"\n timeout={{\n appear: 0,\n enter: timeout,\n exit: CSS_TRANSITION_DURATION,\n }}\n unmountOnExit\n >\n <Body ref={bodyRef} as=\"span\" className=\"snackbar__text\">\n {text}\n {action ? (\n <Button className=\"snackbar__text__action\" v2 size=\"sm\" onClick={action.onClick}>\n {action.label}\n </Button>\n ) : null}\n </Body>\n </CSSTransition>\n </div>\n );\n};\n\nexport default withNextPortal(Snackbar);\n"],"names":["CSS_TRANSITION_DURATION","Snackbar","props","bodyRef","useRef","leaveTimeout","transitionTimeout","committedProps","state","setState","useState","visible","text","useEffect","window","clearTimeout","current","useLayoutEffect","previousProps","action","timestamp","updateWhileTextIsPresent","update","setLeaveTimeout","setTimeout","timeout","show","_jsx","className","role","children","CSSTransition","nodeRef","in","classNames","appear","enter","exit","unmountOnExit","_jsxs","Body","ref","as","Button","v2","size","onClick","label","withNextPortal"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { useRef, useState, useEffect } from 'react';
1
+ import { useRef, useState, useEffect, useLayoutEffect } from 'react';
2
2
  import { CSSTransition } from 'react-transition-group';
3
3
  import Button from '../Button/Button.resolver.mjs';
4
4
  import Body from '../Body/Body.mjs';
@@ -22,16 +22,16 @@ const Snackbar = props => {
22
22
  }, []);
23
23
  // Runs after every render to mirror the componentDidUpdate lifecycle this component previously
24
24
  // relied on, including the shouldComponentUpdate guard that ignored every update without text.
25
- // Both rules below are disabled on purpose: the dependency-less effect and its synchronous state
26
- // updates are what keep this conversion behaviour-identical to the class component.
25
+ // Layout, not passive, so a notification is shown or replaced before the browser paints, as
26
+ // componentDidUpdate did. Both rules below are disabled on purpose: the dependency-less effect
27
+ // and its synchronous state updates are what keep this conversion behaviour-identical.
27
28
  /* eslint-disable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */
28
- useEffect(() => {
29
+ useLayoutEffect(() => {
29
30
  const previousProps = committedProps.current;
30
31
  committedProps.current = props;
31
32
  const {
32
33
  action,
33
34
  text,
34
- timeout,
35
35
  timestamp
36
36
  } = props;
37
37
  // shouldComponentUpdate bailed out whenever the incoming text was empty. That also blocked the
@@ -48,7 +48,7 @@ const Snackbar = props => {
48
48
  ...current,
49
49
  visible: false
50
50
  })));
51
- }, timeout);
51
+ }, committedProps.current.timeout);
52
52
  };
53
53
  const show = () => {
54
54
  setState(current => ({
@@ -1 +1 @@
1
- {"version":3,"file":"Snackbar.mjs","sources":["../../src/Snackbar/Snackbar.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport { CSSTransition } from 'react-transition-group';\n\nimport Button from '../Button';\nimport Body from '../Body';\nimport type { ThemeDark, ThemeLight } from '../common';\nimport withNextPortal from '../WithNextPortal/WithNextPortal';\n\nconst CSS_TRANSITION_DURATION = 400;\n\nexport interface SnackbarProps {\n action?: {\n label: string;\n onClick?: React.MouseEventHandler<HTMLButtonElement>;\n };\n text: React.ReactNode;\n /** @deprecated */\n theme?: ThemeLight | ThemeDark;\n timeout: number;\n timestamp: number;\n}\n\ninterface SnackbarState extends Pick<SnackbarProps, 'action' | 'text' | 'theme'> {\n visible: boolean;\n}\n\nexport const Snackbar = (props: SnackbarProps) => {\n const bodyRef = useRef<HTMLSpanElement>(null);\n const leaveTimeout = useRef(0);\n const transitionTimeout = useRef(0);\n /** The props of the last committed render, as `this.props` was in the class component. */\n const committedProps = useRef(props);\n\n const [state, setState] = useState<SnackbarState>({\n visible: false,\n text: '',\n });\n\n useEffect(\n () => () => {\n window.clearTimeout(leaveTimeout.current);\n window.clearTimeout(transitionTimeout.current);\n },\n [],\n );\n\n // Runs after every render to mirror the componentDidUpdate lifecycle this component previously\n // relied on, including the shouldComponentUpdate guard that ignored every update without text.\n // Both rules below are disabled on purpose: the dependency-less effect and its synchronous state\n // updates are what keep this conversion behaviour-identical to the class component.\n /* eslint-disable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */\n useEffect(() => {\n const previousProps = committedProps.current;\n committedProps.current = props;\n\n const { action, text, timeout, timestamp } = props;\n\n // shouldComponentUpdate bailed out whenever the incoming text was empty. That also blocked the\n // updates scheduled below, so a Snackbar whose text is cleared stays on screen until it is\n // given text again. Preserved here deliberately to keep this refactor behaviour-identical.\n const updateWhileTextIsPresent = (update: () => void) => {\n if (committedProps.current.text) {\n update();\n }\n };\n\n const setLeaveTimeout = () => {\n leaveTimeout.current = window.setTimeout(() => {\n updateWhileTextIsPresent(() => setState((current) => ({ ...current, visible: false })));\n }, timeout);\n };\n\n const show = () => {\n setState((current) => ({ ...current, visible: true, action, text }));\n setLeaveTimeout();\n };\n\n if (!text) {\n return;\n }\n\n if (!previousProps.text) {\n show();\n } else if (previousProps.timestamp !== timestamp) {\n window.clearTimeout(leaveTimeout.current);\n\n if (state.visible) {\n setState((current) => ({ ...current, visible: false }));\n transitionTimeout.current = window.setTimeout(() => {\n updateWhileTextIsPresent(show);\n }, CSS_TRANSITION_DURATION);\n } else {\n show();\n }\n }\n });\n /* eslint-enable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */\n\n const { action, text, visible } = state;\n const { timeout } = props;\n\n return (\n <div className=\"snackbar\" aria-live=\"assertive\" role=\"alert\">\n <CSSTransition\n nodeRef={bodyRef}\n in={visible}\n classNames=\"snackbar__text-container\"\n timeout={{\n appear: 0,\n enter: timeout,\n exit: CSS_TRANSITION_DURATION,\n }}\n unmountOnExit\n >\n <Body ref={bodyRef} as=\"span\" className=\"snackbar__text\">\n {text}\n {action ? (\n <Button className=\"snackbar__text__action\" v2 size=\"sm\" onClick={action.onClick}>\n {action.label}\n </Button>\n ) : null}\n </Body>\n </CSSTransition>\n </div>\n );\n};\n\nexport default withNextPortal(Snackbar);\n"],"names":["CSS_TRANSITION_DURATION","Snackbar","props","bodyRef","useRef","leaveTimeout","transitionTimeout","committedProps","state","setState","useState","visible","text","useEffect","window","clearTimeout","current","previousProps","action","timeout","timestamp","updateWhileTextIsPresent","update","setLeaveTimeout","setTimeout","show","_jsx","className","role","children","CSSTransition","nodeRef","in","classNames","appear","enter","exit","unmountOnExit","_jsxs","Body","ref","as","Button","v2","size","onClick","label","withNextPortal"],"mappings":"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"}
1
+ {"version":3,"file":"Snackbar.mjs","sources":["../../src/Snackbar/Snackbar.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport { CSSTransition } from 'react-transition-group';\n\nimport Button from '../Button';\nimport Body from '../Body';\nimport type { ThemeDark, ThemeLight } from '../common';\nimport withNextPortal from '../WithNextPortal/WithNextPortal';\n\nconst CSS_TRANSITION_DURATION = 400;\n\nexport interface SnackbarProps {\n action?: {\n label: string;\n onClick?: React.MouseEventHandler<HTMLButtonElement>;\n };\n text: React.ReactNode;\n /** @deprecated */\n theme?: ThemeLight | ThemeDark;\n timeout: number;\n timestamp: number;\n}\n\ninterface SnackbarState extends Pick<SnackbarProps, 'action' | 'text' | 'theme'> {\n visible: boolean;\n}\n\nexport const Snackbar = (props: SnackbarProps) => {\n const bodyRef = useRef<HTMLSpanElement>(null);\n const leaveTimeout = useRef(0);\n const transitionTimeout = useRef(0);\n /** The props of the last committed render, as `this.props` was in the class component. */\n const committedProps = useRef(props);\n\n const [state, setState] = useState<SnackbarState>({\n visible: false,\n text: '',\n });\n\n useEffect(\n () => () => {\n window.clearTimeout(leaveTimeout.current);\n window.clearTimeout(transitionTimeout.current);\n },\n [],\n );\n\n // Runs after every render to mirror the componentDidUpdate lifecycle this component previously\n // relied on, including the shouldComponentUpdate guard that ignored every update without text.\n // Layout, not passive, so a notification is shown or replaced before the browser paints, as\n // componentDidUpdate did. Both rules below are disabled on purpose: the dependency-less effect\n // and its synchronous state updates are what keep this conversion behaviour-identical.\n /* eslint-disable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */\n useLayoutEffect(() => {\n const previousProps = committedProps.current;\n committedProps.current = props;\n\n const { action, text, timestamp } = props;\n\n // shouldComponentUpdate bailed out whenever the incoming text was empty. That also blocked the\n // updates scheduled below, so a Snackbar whose text is cleared stays on screen until it is\n // given text again. Preserved here deliberately to keep this refactor behaviour-identical.\n const updateWhileTextIsPresent = (update: () => void) => {\n if (committedProps.current.text) {\n update();\n }\n };\n\n const setLeaveTimeout = () => {\n leaveTimeout.current = window.setTimeout(() => {\n updateWhileTextIsPresent(() => setState((current) => ({ ...current, visible: false })));\n }, committedProps.current.timeout);\n };\n\n const show = () => {\n setState((current) => ({ ...current, visible: true, action, text }));\n setLeaveTimeout();\n };\n\n if (!text) {\n return;\n }\n\n if (!previousProps.text) {\n show();\n } else if (previousProps.timestamp !== timestamp) {\n window.clearTimeout(leaveTimeout.current);\n\n if (state.visible) {\n setState((current) => ({ ...current, visible: false }));\n transitionTimeout.current = window.setTimeout(() => {\n updateWhileTextIsPresent(show);\n }, CSS_TRANSITION_DURATION);\n } else {\n show();\n }\n }\n });\n /* eslint-enable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */\n\n const { action, text, visible } = state;\n const { timeout } = props;\n\n return (\n <div className=\"snackbar\" aria-live=\"assertive\" role=\"alert\">\n <CSSTransition\n nodeRef={bodyRef}\n in={visible}\n classNames=\"snackbar__text-container\"\n timeout={{\n appear: 0,\n enter: timeout,\n exit: CSS_TRANSITION_DURATION,\n }}\n unmountOnExit\n >\n <Body ref={bodyRef} as=\"span\" className=\"snackbar__text\">\n {text}\n {action ? (\n <Button className=\"snackbar__text__action\" v2 size=\"sm\" onClick={action.onClick}>\n {action.label}\n </Button>\n ) : null}\n </Body>\n </CSSTransition>\n </div>\n );\n};\n\nexport default withNextPortal(Snackbar);\n"],"names":["CSS_TRANSITION_DURATION","Snackbar","props","bodyRef","useRef","leaveTimeout","transitionTimeout","committedProps","state","setState","useState","visible","text","useEffect","window","clearTimeout","current","useLayoutEffect","previousProps","action","timestamp","updateWhileTextIsPresent","update","setLeaveTimeout","setTimeout","timeout","show","_jsx","className","role","children","CSSTransition","nodeRef","in","classNames","appear","enter","exit","unmountOnExit","_jsxs","Body","ref","as","Button","v2","size","onClick","label","withNextPortal"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Snackbar.d.ts","sourceRoot":"","sources":["../../../src/Snackbar/Snackbar.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAKvD,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;KACtD,CAAC;IACF,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC;IACtB,kBAAkB;IAClB,KAAK,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD,eAAO,MAAM,QAAQ,GAAI,OAAO,aAAa,gCAmG5C,CAAC;;AAEF,wBAAwC"}
1
+ {"version":3,"file":"Snackbar.d.ts","sourceRoot":"","sources":["../../../src/Snackbar/Snackbar.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAKvD,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;KACtD,CAAC;IACF,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC;IACtB,kBAAkB;IAClB,KAAK,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD,eAAO,MAAM,QAAQ,GAAI,OAAO,aAAa,gCAoG5C,CAAC;;AAEF,wBAAwC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transferwise/components",
3
- "version": "0.0.0-experimental-fa47f3d",
3
+ "version": "0.0.0-experimental-98fd351",
4
4
  "description": "Neptune React components",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -122,6 +122,22 @@ describe('Snackbar', () => {
122
122
  expect(screen.queryByText('Second message')).not.toBeInTheDocument();
123
123
  });
124
124
 
125
+ it('dismisses a delayed replacement on the timeout current when it appears', () => {
126
+ const { rerender } = renderSnackbar();
127
+
128
+ rerender(<Snackbar text="First message" timeout={TIMEOUT} timestamp={1} />);
129
+ rerender(<Snackbar text="Second message" timeout={TIMEOUT} timestamp={2} />);
130
+ // Shortened while the replacement is still waiting out the exit transition.
131
+ rerender(<Snackbar text="Second message" timeout={1000} timestamp={2} />);
132
+
133
+ advanceTimers(CSS_TRANSITION_DURATION);
134
+ expect(screen.getByText('Second message')).toBeInTheDocument();
135
+
136
+ advanceTimers(1000);
137
+ advanceTimers(CSS_TRANSITION_DURATION);
138
+ expect(screen.queryByText('Second message')).not.toBeInTheDocument();
139
+ });
140
+
125
141
  // Documents existing behaviour rather than endorsing it: clearing the text leaves the notification
126
142
  // on screen indefinitely, because the previous shouldComponentUpdate blocked the dismissal too.
127
143
  it('keeps the notification visible for as long as text is empty', () => {
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef, useState } from 'react';
1
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
2
2
  import { CSSTransition } from 'react-transition-group';
3
3
 
4
4
  import Button from '../Button';
@@ -46,14 +46,15 @@ export const Snackbar = (props: SnackbarProps) => {
46
46
 
47
47
  // Runs after every render to mirror the componentDidUpdate lifecycle this component previously
48
48
  // relied on, including the shouldComponentUpdate guard that ignored every update without text.
49
- // Both rules below are disabled on purpose: the dependency-less effect and its synchronous state
50
- // updates are what keep this conversion behaviour-identical to the class component.
49
+ // Layout, not passive, so a notification is shown or replaced before the browser paints, as
50
+ // componentDidUpdate did. Both rules below are disabled on purpose: the dependency-less effect
51
+ // and its synchronous state updates are what keep this conversion behaviour-identical.
51
52
  /* eslint-disable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */
52
- useEffect(() => {
53
+ useLayoutEffect(() => {
53
54
  const previousProps = committedProps.current;
54
55
  committedProps.current = props;
55
56
 
56
- const { action, text, timeout, timestamp } = props;
57
+ const { action, text, timestamp } = props;
57
58
 
58
59
  // shouldComponentUpdate bailed out whenever the incoming text was empty. That also blocked the
59
60
  // updates scheduled below, so a Snackbar whose text is cleared stays on screen until it is
@@ -67,7 +68,7 @@ export const Snackbar = (props: SnackbarProps) => {
67
68
  const setLeaveTimeout = () => {
68
69
  leaveTimeout.current = window.setTimeout(() => {
69
70
  updateWhileTextIsPresent(() => setState((current) => ({ ...current, visible: false })));
70
- }, timeout);
71
+ }, committedProps.current.timeout);
71
72
  };
72
73
 
73
74
  const show = () => {