@khanacademy/wonder-blocks-core 5.0.4 → 5.2.0

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/components/render-state-root.d.ts +1 -1
  3. package/dist/components/render-state-root.js.flow +1 -1
  4. package/dist/es/index.js +12 -9
  5. package/dist/hooks/use-latest-ref.d.ts +12 -0
  6. package/dist/hooks/use-latest-ref.js.flow +16 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +12 -8
  9. package/dist/index.js.flow +1 -0
  10. package/dist/util/add-style.d.ts +182 -2
  11. package/dist/util/add-style.js.flow +10 -18
  12. package/dist/util/types.d.ts +10 -0
  13. package/dist/util/types.js.flow +12 -0
  14. package/package.json +2 -2
  15. package/src/components/render-state-root.tsx +3 -9
  16. package/src/components/view.tsx +5 -5
  17. package/src/hooks/__tests__/use-force-update.test.tsx +1 -1
  18. package/src/hooks/__tests__/use-latest-ref.test.ts +40 -0
  19. package/src/hooks/use-latest-ref.js.flow +16 -0
  20. package/src/hooks/use-latest-ref.ts +17 -0
  21. package/src/index.ts +1 -0
  22. package/src/util/__tests__/add-style.test.tsx +21 -1
  23. package/src/util/add-style.js.flow +13 -0
  24. package/src/util/add-style.tsx +214 -11
  25. package/src/util/types.ts +12 -0
  26. package/tsconfig-build.tsbuildinfo +1 -0
  27. package/src/util/__docs__/add-style.stories.mdx +0 -158
  28. package/src/util/__docs__/server.stories.mdx +0 -34
  29. package/tsconfig.tsbuildinfo +0 -1
  30. /package/dist/util/{add-styles.flowtest.d.ts → add-styles.typestest.d.ts} +0 -0
  31. /package/dist/util/{add-styles.flowtest.js.flow → add-styles.typestest.js.flow} +0 -0
  32. /package/src/util/{add-styles.flowtest.tsx → add-styles.typestest.tsx} +0 -0
  33. /package/{tsconfig.json → tsconfig-build.json} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @khanacademy/wonder-blocks-core
2
2
 
3
+ ## 5.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - fa70c895: Add 'lang', 'className' and 'htmlFor' properties to Text and View
8
+
9
+ ### Patch Changes
10
+
11
+ - 19ab0408: Update flowgen to convert React.ForwardRefExoticComponent<> and React.FowardedRef<> properly
12
+ - fa70c895: Remove all TypeScript error suppressions on JSX attributes
13
+
14
+ ## 5.1.0
15
+
16
+ ### Minor Changes
17
+
18
+ - 3c400719: Add useLatestRef hook to wonder-blocks-core
19
+
20
+ ### Patch Changes
21
+
22
+ - a6164ed0: Don't use React.FC<> for functional components
23
+
3
24
  ## 5.0.4
4
25
 
5
26
  ### Patch Changes
@@ -6,5 +6,5 @@ type Props = {
6
6
  */
7
7
  throwIfNested?: boolean;
8
8
  };
9
- declare const RenderStateRoot: React.FC<Props>;
9
+ declare const RenderStateRoot: ({ children, throwIfNested, }: Props) => React.ReactElement;
10
10
  export { RenderStateRoot };
@@ -13,5 +13,5 @@ declare type Props = {|
13
13
  */
14
14
  throwIfNested?: boolean,
15
15
  |};
16
- declare var RenderStateRoot: React.StatelessFunctionalComponent<Props>;
16
+ declare var RenderStateRoot: (x: Props) => React.Element<any>;
17
17
  declare export { RenderStateRoot };
package/dist/es/index.js CHANGED
@@ -119,7 +119,7 @@ Text.defaultProps = {
119
119
 
120
120
  const _excluded$1 = ["className", "style"];
121
121
  function addStyle(Component, defaultStyle) {
122
- const StyleComponent = props => {
122
+ return React.forwardRef((props, ref) => {
123
123
  const {
124
124
  className,
125
125
  style
@@ -131,11 +131,11 @@ function addStyle(Component, defaultStyle) {
131
131
  style: inlineStyles
132
132
  } = processStyleList([reset, defaultStyle, style]);
133
133
  return React.createElement(Component, _extends({}, otherProps, {
134
+ ref: ref,
134
135
  className: [aphroditeClassName, className].filter(Boolean).join(" "),
135
136
  style: inlineStyles
136
137
  }));
137
- };
138
- return StyleComponent;
138
+ });
139
139
  }
140
140
  const overrides = StyleSheet.create({
141
141
  button: {
@@ -422,6 +422,12 @@ const useIsMounted = () => {
422
422
  return React.useCallback(() => isMounted.current, []);
423
423
  };
424
424
 
425
+ function useLatestRef(value) {
426
+ const ref = React.useRef(value);
427
+ ref.current = value;
428
+ return ref;
429
+ }
430
+
425
431
  const useOnline = () => {
426
432
  const forceUpdate = useForceUpdate();
427
433
  useEffect$1(() => {
@@ -442,7 +448,7 @@ const {
442
448
  } = React;
443
449
  const RenderStateRoot = ({
444
450
  children,
445
- throwIfNested
451
+ throwIfNested: _throwIfNested = true
446
452
  }) => {
447
453
  const [firstRender, setFirstRender] = useState(true);
448
454
  const renderState = useRenderState();
@@ -450,7 +456,7 @@ const RenderStateRoot = ({
450
456
  setFirstRender(false);
451
457
  }, []);
452
458
  if (renderState !== RenderState.Root) {
453
- if (throwIfNested) {
459
+ if (_throwIfNested) {
454
460
  throw new Error("There's already a <RenderStateRoot> above this instance in " + "the render tree. This instance should be removed.");
455
461
  }
456
462
  return children;
@@ -460,8 +466,5 @@ const RenderStateRoot = ({
460
466
  value: value
461
467
  }, children);
462
468
  };
463
- RenderStateRoot.defaultProps = {
464
- throwIfNested: true
465
- };
466
469
 
467
- export { IDProvider, RenderState, RenderStateRoot, server as Server, Text, UniqueIDProvider, View, WithSSRPlaceholder, addStyle, useForceUpdate, useIsMounted, useOnMountEffect, useOnline, useRenderState, useUniqueIdWithMock, useUniqueIdWithoutMock };
470
+ export { IDProvider, RenderState, RenderStateRoot, server as Server, Text, UniqueIDProvider, View, WithSSRPlaceholder, addStyle, useForceUpdate, useIsMounted, useLatestRef, useOnMountEffect, useOnline, useRenderState, useUniqueIdWithMock, useUniqueIdWithoutMock };
@@ -0,0 +1,12 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Return a ref that always contains the `value` passed.
4
+ *
5
+ * The useLatestRef hook returns a ref that always contains the `value` passed
6
+ * to the hook during the most recent render.
7
+ *
8
+ * It can be used to wrap a possibly-changing prop in a stable value that can
9
+ * be passed to useEffect without causing unnecessary re-renders. See the
10
+ * Storybook docs for a usage example.
11
+ */
12
+ export declare function useLatestRef<T>(value: T): React.RefObject<T>;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Flowtype definitions for useLatestRef
3
+ * @flow
4
+ */
5
+
6
+ /**
7
+ * Return a ref that always contains the `value` passed.
8
+ *
9
+ * The useLatestRef hook returns a ref that always contains the `value` passed
10
+ * to the hook during the most recent render.
11
+ *
12
+ * It can be used to wrap a possibly-changing prop in a stable value that can
13
+ * be passed to useEffect without causing unnecessary re-renders. See the
14
+ * Storybook docs for a usage example.
15
+ */
16
+ declare export function useLatestRef<T>(value: T): {|current: T|};
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export { default as Server } from "./util/server";
9
9
  export { useUniqueIdWithMock, useUniqueIdWithoutMock, } from "./hooks/use-unique-id";
10
10
  export { useForceUpdate } from "./hooks/use-force-update";
11
11
  export { useIsMounted } from "./hooks/use-is-mounted";
12
+ export { useLatestRef } from "./hooks/use-latest-ref";
12
13
  export { useOnMountEffect } from "./hooks/use-on-mount-effect";
13
14
  export { useOnline } from "./hooks/use-online";
14
15
  export { useRenderState } from "./hooks/use-render-state";
package/dist/index.js CHANGED
@@ -142,7 +142,7 @@ Text.defaultProps = {
142
142
 
143
143
  const _excluded$1 = ["className", "style"];
144
144
  function addStyle(Component, defaultStyle) {
145
- const StyleComponent = props => {
145
+ return React__namespace.forwardRef((props, ref) => {
146
146
  const {
147
147
  className,
148
148
  style
@@ -154,11 +154,11 @@ function addStyle(Component, defaultStyle) {
154
154
  style: inlineStyles
155
155
  } = processStyleList([reset, defaultStyle, style]);
156
156
  return React__namespace.createElement(Component, _extends({}, otherProps, {
157
+ ref: ref,
157
158
  className: [aphroditeClassName, className].filter(Boolean).join(" "),
158
159
  style: inlineStyles
159
160
  }));
160
- };
161
- return StyleComponent;
161
+ });
162
162
  }
163
163
  const overrides = aphrodite.StyleSheet.create({
164
164
  button: {
@@ -445,6 +445,12 @@ const useIsMounted = () => {
445
445
  return React__namespace.useCallback(() => isMounted.current, []);
446
446
  };
447
447
 
448
+ function useLatestRef(value) {
449
+ const ref = React__namespace.useRef(value);
450
+ ref.current = value;
451
+ return ref;
452
+ }
453
+
448
454
  const useOnline = () => {
449
455
  const forceUpdate = useForceUpdate();
450
456
  React.useEffect(() => {
@@ -465,7 +471,7 @@ const {
465
471
  } = React__namespace;
466
472
  const RenderStateRoot = ({
467
473
  children,
468
- throwIfNested
474
+ throwIfNested: _throwIfNested = true
469
475
  }) => {
470
476
  const [firstRender, setFirstRender] = useState(true);
471
477
  const renderState = useRenderState();
@@ -473,7 +479,7 @@ const RenderStateRoot = ({
473
479
  setFirstRender(false);
474
480
  }, []);
475
481
  if (renderState !== RenderState.Root) {
476
- if (throwIfNested) {
482
+ if (_throwIfNested) {
477
483
  throw new Error("There's already a <RenderStateRoot> above this instance in " + "the render tree. This instance should be removed.");
478
484
  }
479
485
  return children;
@@ -483,9 +489,6 @@ const RenderStateRoot = ({
483
489
  value: value
484
490
  }, children);
485
491
  };
486
- RenderStateRoot.defaultProps = {
487
- throwIfNested: true
488
- };
489
492
 
490
493
  exports.IDProvider = IDProvider;
491
494
  exports.RenderState = RenderState;
@@ -498,6 +501,7 @@ exports.WithSSRPlaceholder = WithSSRPlaceholder;
498
501
  exports.addStyle = addStyle;
499
502
  exports.useForceUpdate = useForceUpdate;
500
503
  exports.useIsMounted = useIsMounted;
504
+ exports.useLatestRef = useLatestRef;
501
505
  exports.useOnMountEffect = useOnMountEffect;
502
506
  exports.useOnline = useOnline;
503
507
  exports.useRenderState = useRenderState;
@@ -18,6 +18,7 @@ declare export {
18
18
  } from "./hooks/use-unique-id";
19
19
  declare export { useForceUpdate } from "./hooks/use-force-update";
20
20
  declare export { useIsMounted } from "./hooks/use-is-mounted";
21
+ declare export { useLatestRef } from "./hooks/use-latest-ref";
21
22
  declare export { useOnMountEffect } from "./hooks/use-on-mount-effect";
22
23
  declare export { useOnline } from "./hooks/use-online";
23
24
  declare export { useRenderState } from "./hooks/use-render-state";
@@ -1,5 +1,185 @@
1
1
  import * as React from "react";
2
2
  import type { StyleType } from "./types";
3
- export default function addStyle<T extends React.ComponentType<any> | keyof JSX.IntrinsicElements>(Component: T, defaultStyle?: StyleType): React.FC<SpreadType<JSX.LibraryManagedAttributes<T, React.ComponentProps<T>>, {
3
+ export default function addStyle<T extends React.ComponentType<any> | keyof JSX.IntrinsicElements, Props extends {
4
+ className?: string;
4
5
  style?: StyleType;
5
- }>>;
6
+ children?: React.ReactNode;
7
+ } & Omit<React.ComponentProps<T>, "style">>(Component: T, defaultStyle?: StyleType): React.ForwardRefExoticComponent<React.PropsWithoutRef<Props> & React.RefAttributes<T extends keyof JSX.IntrinsicElements ? IntrinsicElementsMap[T] : T>>;
8
+ type IntrinsicElementsMap = {
9
+ a: HTMLAnchorElement;
10
+ abbr: HTMLElement;
11
+ address: HTMLElement;
12
+ area: HTMLAreaElement;
13
+ article: HTMLElement;
14
+ aside: HTMLElement;
15
+ audio: HTMLAudioElement;
16
+ b: HTMLElement;
17
+ base: HTMLBaseElement;
18
+ bdi: HTMLElement;
19
+ bdo: HTMLElement;
20
+ big: HTMLElement;
21
+ blockquote: HTMLElement;
22
+ body: HTMLBodyElement;
23
+ br: HTMLBRElement;
24
+ button: HTMLButtonElement;
25
+ canvas: HTMLCanvasElement;
26
+ caption: HTMLElement;
27
+ cite: HTMLElement;
28
+ code: HTMLElement;
29
+ col: HTMLTableColElement;
30
+ colgroup: HTMLTableColElement;
31
+ data: HTMLDataElement;
32
+ datalist: HTMLDataListElement;
33
+ dd: HTMLElement;
34
+ del: HTMLElement;
35
+ details: HTMLElement;
36
+ dfn: HTMLElement;
37
+ dialog: HTMLDialogElement;
38
+ div: HTMLDivElement;
39
+ dl: HTMLDListElement;
40
+ dt: HTMLElement;
41
+ em: HTMLElement;
42
+ embed: HTMLEmbedElement;
43
+ fieldset: HTMLFieldSetElement;
44
+ figcaption: HTMLElement;
45
+ figure: HTMLElement;
46
+ footer: HTMLElement;
47
+ form: HTMLFormElement;
48
+ h1: HTMLHeadingElement;
49
+ h2: HTMLHeadingElement;
50
+ h3: HTMLHeadingElement;
51
+ h4: HTMLHeadingElement;
52
+ h5: HTMLHeadingElement;
53
+ h6: HTMLHeadingElement;
54
+ head: HTMLHeadElement;
55
+ header: HTMLElement;
56
+ hgroup: HTMLElement;
57
+ hr: HTMLHRElement;
58
+ html: HTMLHtmlElement;
59
+ i: HTMLElement;
60
+ iframe: HTMLIFrameElement;
61
+ img: HTMLImageElement;
62
+ input: HTMLInputElement;
63
+ ins: HTMLModElement;
64
+ kbd: HTMLElement;
65
+ keygen: HTMLElement;
66
+ label: HTMLLabelElement;
67
+ legend: HTMLLegendElement;
68
+ li: HTMLLIElement;
69
+ link: HTMLLinkElement;
70
+ main: HTMLElement;
71
+ map: HTMLMapElement;
72
+ mark: HTMLElement;
73
+ menu: HTMLElement;
74
+ menuitem: HTMLElement;
75
+ meta: HTMLMetaElement;
76
+ meter: HTMLElement;
77
+ nav: HTMLElement;
78
+ noindex: HTMLElement;
79
+ noscript: HTMLElement;
80
+ object: HTMLObjectElement;
81
+ ol: HTMLOListElement;
82
+ optgroup: HTMLOptGroupElement;
83
+ option: HTMLOptionElement;
84
+ output: HTMLElement;
85
+ p: HTMLParagraphElement;
86
+ param: HTMLParamElement;
87
+ picture: HTMLElement;
88
+ pre: HTMLPreElement;
89
+ progress: HTMLProgressElement;
90
+ q: HTMLQuoteElement;
91
+ rp: HTMLElement;
92
+ rt: HTMLElement;
93
+ ruby: HTMLElement;
94
+ s: HTMLElement;
95
+ samp: HTMLElement;
96
+ slot: HTMLSlotElement;
97
+ script: HTMLScriptElement;
98
+ section: HTMLElement;
99
+ select: HTMLSelectElement;
100
+ small: HTMLElement;
101
+ source: HTMLSourceElement;
102
+ span: HTMLSpanElement;
103
+ strong: HTMLElement;
104
+ style: HTMLStyleElement;
105
+ sub: HTMLElement;
106
+ summary: HTMLElement;
107
+ sup: HTMLElement;
108
+ table: HTMLTableElement;
109
+ template: HTMLTemplateElement;
110
+ tbody: HTMLTableSectionElement;
111
+ td: HTMLElement;
112
+ textarea: HTMLTextAreaElement;
113
+ tfoot: HTMLTableSectionElement;
114
+ th: HTMLElement;
115
+ thead: HTMLTableSectionElement;
116
+ time: HTMLElement;
117
+ title: HTMLTitleElement;
118
+ tr: HTMLTableRowElement;
119
+ track: HTMLTrackElement;
120
+ u: HTMLElement;
121
+ ul: HTMLUListElement;
122
+ var: HTMLElement;
123
+ video: HTMLVideoElement;
124
+ wbr: HTMLElement;
125
+ webview: HTMLElement;
126
+ svg: SVGSVGElement;
127
+ animate: SVGElement;
128
+ animateMotion: SVGElement;
129
+ animateTransform: SVGElement;
130
+ circle: SVGCircleElement;
131
+ clipPath: SVGClipPathElement;
132
+ defs: SVGDefsElement;
133
+ desc: SVGDescElement;
134
+ ellipse: SVGEllipseElement;
135
+ feBlend: SVGFEBlendElement;
136
+ feColorMatrix: SVGFEColorMatrixElement;
137
+ feComponentTransfer: SVGFEComponentTransferElement;
138
+ feComposite: SVGFECompositeElement;
139
+ feConvolveMatrix: SVGFEConvolveMatrixElement;
140
+ feDiffuseLighting: SVGFEDiffuseLightingElement;
141
+ feDisplacementMap: SVGFEDisplacementMapElement;
142
+ feDistantLight: SVGFEDistantLightElement;
143
+ feDropShadow: SVGFEDropShadowElement;
144
+ feFlood: SVGFEFloodElement;
145
+ feFuncA: SVGFEFuncAElement;
146
+ feFuncB: SVGFEFuncBElement;
147
+ feFuncG: SVGFEFuncGElement;
148
+ feFuncR: SVGFEFuncRElement;
149
+ feGaussianBlur: SVGFEGaussianBlurElement;
150
+ feImage: SVGFEImageElement;
151
+ feMerge: SVGFEMergeElement;
152
+ feMergeNode: SVGFEMergeNodeElement;
153
+ feMorphology: SVGFEMorphologyElement;
154
+ feOffset: SVGFEOffsetElement;
155
+ fePointLight: SVGFEPointLightElement;
156
+ feSpecularLighting: SVGFESpecularLightingElement;
157
+ feSpotLight: SVGFESpotLightElement;
158
+ feTile: SVGFETileElement;
159
+ feTurbulence: SVGFETurbulenceElement;
160
+ filter: SVGFilterElement;
161
+ foreignObject: SVGForeignObjectElement;
162
+ g: SVGGElement;
163
+ image: SVGImageElement;
164
+ line: SVGLineElement;
165
+ linearGradient: SVGLinearGradientElement;
166
+ marker: SVGMarkerElement;
167
+ mask: SVGMaskElement;
168
+ metadata: SVGMetadataElement;
169
+ mpath: SVGElement;
170
+ path: SVGPathElement;
171
+ pattern: SVGPatternElement;
172
+ polygon: SVGPolygonElement;
173
+ polyline: SVGPolylineElement;
174
+ radialGradient: SVGRadialGradientElement;
175
+ rect: SVGRectElement;
176
+ stop: SVGStopElement;
177
+ switch: SVGSwitchElement;
178
+ symbol: SVGSymbolElement;
179
+ text: SVGTextElement;
180
+ textPath: SVGTextPathElement;
181
+ tspan: SVGTSpanElement;
182
+ use: SVGUseElement;
183
+ view: SVGViewElement;
184
+ };
185
+ export {};
@@ -1,21 +1,13 @@
1
- /**
2
- * Flowtype definitions for data
3
- * Generated by Flowgen from a Typescript Definition
4
- * Flowgen v1.21.0
5
- * @flow
6
- */
7
1
  import * as React from "react";
8
- import type { StyleType } from "./types";
2
+ import type {StyleType} from "./types";
3
+
9
4
  declare export default function addStyle<
10
- T: React.ComponentType<any> | $Keys<JSX.IntrinsicElements>
5
+ T: React.AbstractComponent<any> | string,
11
6
  >(
12
- Component: T,
13
- defaultStyle?: StyleType
14
- ): React.StatelessFunctionalComponent<
15
- SpreadType<
16
- JSX.LibraryManagedAttributes<T, React.ElementProps<T>>,
17
- {|
18
- style?: StyleType,
19
- |}
20
- >
21
- >;
7
+ Component: T,
8
+ defaultStyle?: StyleType,
9
+ ): React.AbstractComponent<{
10
+ ...React.ElementConfig<T>,
11
+ style?: StyleType,
12
+ ...
13
+ }>;
@@ -62,6 +62,16 @@ export type TextViewSharedProps = {
62
62
  * Test ID used for e2e testing.
63
63
  */
64
64
  testId?: string;
65
+ /**
66
+ * Optional attribute to indicate to the Screen Reader which language the
67
+ * item text is in.
68
+ */
69
+ lang?: string;
70
+ /**
71
+ * Optional CSS classes for the entire dropdown component.
72
+ */
73
+ className?: string;
74
+ htmlFor?: string;
65
75
  tabIndex?: number;
66
76
  id?: string;
67
77
  "data-modal-launcher-portal"?: boolean;
@@ -82,6 +82,18 @@ export type TextViewSharedProps = {|
82
82
  * Test ID used for e2e testing.
83
83
  */
84
84
  testId?: string,
85
+
86
+ /**
87
+ * Optional attribute to indicate to the Screen Reader which language the
88
+ * item text is in.
89
+ */
90
+ lang?: string,
91
+
92
+ /**
93
+ * Optional CSS classes for the entire dropdown component.
94
+ */
95
+ className?: string,
96
+ htmlFor?: string,
85
97
  tabIndex?: number,
86
98
  id?: string,
87
99
  "data-modal-launcher-portal"?: boolean,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-core",
3
- "version": "5.0.4",
3
+ "version": "5.2.0",
4
4
  "design": "v1",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -23,7 +23,7 @@
23
23
  "react-router-dom": "5.3.0"
24
24
  },
25
25
  "devDependencies": {
26
- "wb-dev-build-settings": "^0.9.5"
26
+ "wb-dev-build-settings": "^0.9.7"
27
27
  },
28
28
  "author": "",
29
29
  "license": "MIT"
@@ -13,10 +13,10 @@ type Props = {
13
13
  throwIfNested?: boolean;
14
14
  };
15
15
 
16
- const RenderStateRoot: React.FC<Props> = ({
16
+ const RenderStateRoot = ({
17
17
  children,
18
- throwIfNested,
19
- }): React.ReactElement => {
18
+ throwIfNested = true,
19
+ }: Props): React.ReactElement => {
20
20
  const [firstRender, setFirstRender] = useState<boolean>(true);
21
21
  const renderState = useRenderState();
22
22
  useEffect(() => {
@@ -45,10 +45,4 @@ const RenderStateRoot: React.FC<Props> = ({
45
45
  );
46
46
  };
47
47
 
48
- // We can set `defaultProps` on a functional component if we move the `export` to appear
49
- // afterwards.
50
- RenderStateRoot.defaultProps = {
51
- throwIfNested: true,
52
- };
53
-
54
48
  export {RenderStateRoot};
@@ -37,11 +37,11 @@ type DefaultProps = {
37
37
  tag: Props["tag"];
38
38
  };
39
39
 
40
- const StyledDiv = addStyle<"div">("div", styles.default);
41
- const StyledArticle = addStyle<"article">("article", styles.default);
42
- const StyledAside = addStyle<"aside">("aside", styles.default);
43
- const StyledNav = addStyle<"nav">("nav", styles.default);
44
- const StyledSection = addStyle<"section">("section", styles.default);
40
+ const StyledDiv = addStyle("div", styles.default);
41
+ const StyledArticle = addStyle("article", styles.default);
42
+ const StyledAside = addStyle("aside", styles.default);
43
+ const StyledNav = addStyle("nav", styles.default);
44
+ const StyledSection = addStyle("section", styles.default);
45
45
 
46
46
  /**
47
47
  * View is a building block for constructing other components. `View` roughly
@@ -24,7 +24,7 @@ describe("#useForceUpdate", () => {
24
24
 
25
25
  it("should cause component to render", () => {
26
26
  // Arrange
27
- const Component: React.FC<any> = (props): React.ReactElement => {
27
+ const Component = (): React.ReactElement => {
28
28
  const countRef = React.useRef(0);
29
29
  const forceUpdate = useForceUpdate();
30
30
  React.useEffect(() => {
@@ -0,0 +1,40 @@
1
+ import {renderHook} from "@testing-library/react-hooks";
2
+ import {useLatestRef} from "../use-latest-ref";
3
+
4
+ describe("useLatestRef", () => {
5
+ it("returns a ref to the value passed in", () => {
6
+ const {
7
+ result: {current: ref},
8
+ } = renderHook(() => useLatestRef(123));
9
+ expect(ref.current).toBe(123);
10
+ });
11
+
12
+ it("returns a ref to the most recent value passed in", () => {
13
+ // Arrange: render the component with props {value: 123}
14
+ const {result, rerender} = renderHook(
15
+ ({value}) => useLatestRef(value),
16
+ {initialProps: {value: 123}},
17
+ );
18
+
19
+ // Act
20
+ rerender({value: 456});
21
+
22
+ // Assert
23
+ expect(result.current.current).toBe(456);
24
+ });
25
+
26
+ it("returns a stable ref object for the lifetime of the component", () => {
27
+ // Arrange: render the component and remember the ref
28
+ const {result, rerender} = renderHook(
29
+ ({value}) => useLatestRef(value),
30
+ {initialProps: {value: 123}},
31
+ );
32
+ const refFromFirstRender = result.current;
33
+
34
+ // Act
35
+ rerender({value: 456});
36
+
37
+ // Assert
38
+ expect(result.current).toBe(refFromFirstRender);
39
+ });
40
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Flowtype definitions for useLatestRef
3
+ * @flow
4
+ */
5
+
6
+ /**
7
+ * Return a ref that always contains the `value` passed.
8
+ *
9
+ * The useLatestRef hook returns a ref that always contains the `value` passed
10
+ * to the hook during the most recent render.
11
+ *
12
+ * It can be used to wrap a possibly-changing prop in a stable value that can
13
+ * be passed to useEffect without causing unnecessary re-renders. See the
14
+ * Storybook docs for a usage example.
15
+ */
16
+ declare export function useLatestRef<T>(value: T): {|current: T|};
@@ -0,0 +1,17 @@
1
+ import * as React from "react";
2
+
3
+ /**
4
+ * Return a ref that always contains the `value` passed.
5
+ *
6
+ * The useLatestRef hook returns a ref that always contains the `value` passed
7
+ * to the hook during the most recent render.
8
+ *
9
+ * It can be used to wrap a possibly-changing prop in a stable value that can
10
+ * be passed to useEffect without causing unnecessary re-renders. See the
11
+ * Storybook docs for a usage example.
12
+ */
13
+ export function useLatestRef<T>(value: T): React.RefObject<T> {
14
+ const ref = React.useRef(value);
15
+ ref.current = value;
16
+ return ref;
17
+ }
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ export {
13
13
  } from "./hooks/use-unique-id";
14
14
  export {useForceUpdate} from "./hooks/use-force-update";
15
15
  export {useIsMounted} from "./hooks/use-is-mounted";
16
+ export {useLatestRef} from "./hooks/use-latest-ref";
16
17
  export {useOnMountEffect} from "./hooks/use-on-mount-effect";
17
18
  export {useOnline} from "./hooks/use-online";
18
19
  export {useRenderState} from "./hooks/use-render-state";
@@ -4,7 +4,7 @@ import {screen, render} from "@testing-library/react";
4
4
 
5
5
  import addStyle from "../add-style";
6
6
 
7
- const StyledDiv = addStyle<"div">("div");
7
+ const StyledDiv = addStyle("div");
8
8
 
9
9
  const styles = StyleSheet.create({
10
10
  foo: {
@@ -88,4 +88,24 @@ describe("addStyle", () => {
88
88
  expect(classNames[0]).toEqual(expect.any(String));
89
89
  expect(classNames[1]).toEqual("foo");
90
90
  });
91
+
92
+ it("should forward a ref to the component", () => {
93
+ // Arrange
94
+ const ref = React.createRef<HTMLDivElement>();
95
+
96
+ render(
97
+ <StyledDiv
98
+ className="foo"
99
+ style={styles.foo}
100
+ data-test-id="styled-div"
101
+ ref={ref}
102
+ />,
103
+ );
104
+
105
+ // Act
106
+ const div = screen.getByTestId("styled-div");
107
+
108
+ // Assert
109
+ expect(div).toBe(ref.current);
110
+ });
91
111
  });