diff --git a/src/contexts/network-connection-context.ts b/src/contexts/network-connection-context.ts
new file mode 100644
index 0000000..79128f0
--- /dev/null
+++ b/src/contexts/network-connection-context.ts
@@ -0,0 +1,6 @@
+import { NetworkConnection } from "andculturecode-javascript-core";
+import React from "react";
+
+export const NetworkConnectionContext = React.createContext<
+ NetworkConnection | undefined
+>(undefined);
diff --git a/src/hooks/use-network-connection.test.tsx b/src/hooks/use-network-connection.test.tsx
new file mode 100644
index 0000000..c547c94
--- /dev/null
+++ b/src/hooks/use-network-connection.test.tsx
@@ -0,0 +1,59 @@
+import { render } from "@testing-library/react";
+import { renderHook } from "@testing-library/react-hooks";
+import {
+ NetworkConnection,
+ NetworkInformationUtils,
+} from "andculturecode-javascript-core";
+import React from "react";
+import { NetworkConnectionProvider } from "../providers/network-connection-provider";
+import { useNetworkConnection } from "./use-network-connection";
+
+// -----------------------------------------------------------------------------------------
+// #region Mocks
+// -----------------------------------------------------------------------------------------
+
+const getNetworkConnectionMock = jest.spyOn(
+ NetworkInformationUtils,
+ "getNetworkConnection"
+);
+
+// #endregion Mocks
+
+describe("useNetworkConnection", () => {
+ describe("when used outside NetworkConnectionProvider", () => {
+ it("throws error", () => {
+ // Arrange & Act
+ const { result } = renderHook(() => useNetworkConnection());
+
+ // Assert
+ expect(result.error).toBeDefined();
+ });
+ });
+
+ describe("when used inside NetworkConnectionProvider", () => {
+ it("returns network connection information", () => {
+ // Arrange
+ let networkConnection: NetworkConnection;
+ const expectedNetworkConnection: NetworkConnection = {
+ isOnline: true,
+ };
+
+ getNetworkConnectionMock.mockReturnValue(expectedNetworkConnection);
+
+ const TestComponent = () => {
+ networkConnection = useNetworkConnection();
+ return
;
+ };
+
+ // Act
+ render(
+
+
+
+ );
+
+ // Assert
+ expect(networkConnection).toEqual(expectedNetworkConnection);
+ });
+ });
+});
diff --git a/src/hooks/use-network-connection.ts b/src/hooks/use-network-connection.ts
new file mode 100644
index 0000000..77e4d61
--- /dev/null
+++ b/src/hooks/use-network-connection.ts
@@ -0,0 +1,18 @@
+import { useContext } from "react";
+import { NetworkConnection } from "andculturecode-javascript-core";
+import { NetworkConnectionContext } from "../contexts/network-connection-context";
+
+/**
+ * Hook that returns the current network connection information
+ */
+export const useNetworkConnection = (): NetworkConnection => {
+ const networkConnection = useContext(NetworkConnectionContext);
+
+ if (networkConnection == null) {
+ throw new Error(
+ "useNetworkConnection must be used within a NetworkConnectionProvider component"
+ );
+ }
+
+ return networkConnection;
+};
diff --git a/src/index.ts b/src/index.ts
index f9ecc62..e132a1f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -18,6 +18,7 @@ export {
NestedRoutesByProperty,
NestedRoutesByPropertyProps,
} from "./components/routing/nested-routes-by-property";
+export { NetworkConnectionProvider } from "./providers/network-connection-provider";
export { Redirects, RedirectsProps } from "./components/routing/redirects";
// #endregion Components
@@ -31,6 +32,7 @@ export { useAsyncEffect } from "./hooks/use-async-effect";
export { useCancellablePromise } from "./hooks/use-cancellable-promise";
export { useDebounce } from "./hooks/use-debounce";
export { useLocalization } from "./hooks/use-localization";
+export { useNetworkConnection } from "./hooks/use-network-connection";
export { useOnClickOutside } from "./hooks/use-onclick-outside";
export { usePageErrors } from "./hooks/use-page-errors";
export { useSortedAlphabetically } from "./hooks/use-sorted-alphabetically";
diff --git a/src/providers/network-connection-provider.test.tsx b/src/providers/network-connection-provider.test.tsx
new file mode 100644
index 0000000..c58d59a
--- /dev/null
+++ b/src/providers/network-connection-provider.test.tsx
@@ -0,0 +1,216 @@
+import React from "react";
+import { act, cleanup, render } from "@testing-library/react";
+import { NetworkConnectionProvider } from "./network-connection-provider";
+import {
+ NetworkConnection,
+ NetworkInformationUtils,
+} from "andculturecode-javascript-core";
+import { useNetworkConnection } from "../hooks/use-network-connection";
+import { FactoryType } from "../tests/factories/factory-type";
+import { Factory } from "rosie";
+
+// -----------------------------------------------------------------------------------------
+// #region Types
+// -----------------------------------------------------------------------------------------
+
+type TypeOfKey = Type[Key];
+
+// #endregion Types
+
+// -----------------------------------------------------------------------------------------
+// #region Interfaces
+// -----------------------------------------------------------------------------------------
+
+interface SetupSutOptions {
+ mockConnections?: Array>;
+}
+
+interface SetupSutResults {
+ TestComponent: () => JSX.Element;
+ networkConnectionResults: {
+ all: Array;
+ current?: NetworkConnection;
+ };
+}
+
+// #endregion Interfaces
+
+// -----------------------------------------------------------------------------------------
+// #region Setup
+// -----------------------------------------------------------------------------------------
+
+const getNetworkConnectionMock = jest.spyOn(
+ NetworkInformationUtils,
+ "getNetworkConnection"
+);
+
+const setupMocks = (mockConnections: Array>) => {
+ getNetworkConnectionMock.mockReset();
+ for (let index = 0; index < mockConnections.length; index++) {
+ const mockImplementation =
+ index === mockConnections.length - 1
+ ? getNetworkConnectionMock.mockImplementation
+ : getNetworkConnectionMock.mockImplementationOnce;
+
+ mockImplementation(() => {
+ return {
+ isOnline: true,
+ ...mockConnections[index],
+ };
+ });
+ }
+};
+
+const setupSut = (options?: SetupSutOptions): SetupSutResults => {
+ const { mockConnections = [] } = options ?? {};
+
+ setupMocks(mockConnections);
+
+ const networkConnectionResults: TypeOfKey<
+ SetupSutResults,
+ "networkConnectionResults"
+ > = {
+ all: [] as NetworkConnection[],
+ };
+
+ function TestComponent() {
+ const connection = useNetworkConnection();
+ networkConnectionResults.all.push(connection);
+ networkConnectionResults.current = connection;
+
+ return ;
+ }
+
+ return {
+ TestComponent,
+ networkConnectionResults,
+ };
+};
+
+// #endregion Setup
+
+describe("NetworkConnectionProvider", () => {
+ it("renders initial network connection state", () => {
+ // Arrange
+ const expectedConnection = Factory.build(
+ FactoryType.NetworkConnection
+ );
+ const { networkConnectionResults, TestComponent } = setupSut({
+ mockConnections: [
+ Factory.build(FactoryType.NetworkConnection),
+ expectedConnection,
+ ],
+ });
+
+ // Act
+ render(
+
+
+
+ );
+
+ // Assert
+ expect(networkConnectionResults.all.length).toEqual(2);
+ expect(networkConnectionResults.current).toEqual(expectedConnection);
+ });
+
+ it("adds an event listener", () => {
+ // Arrange
+ const addEventListener = jest.fn();
+ const networkConnection = Factory.build(
+ FactoryType.NetworkConnection,
+ {
+ addEventListener,
+ }
+ );
+ const { TestComponent } = setupSut({
+ mockConnections: [networkConnection],
+ });
+
+ // Act
+ render(
+
+
+
+ );
+
+ // Assert
+ expect(addEventListener).toBeCalled();
+ });
+
+ describe("when change event is called", () => {
+ it("loads network connection into state", () => {
+ // Arrange
+ let changeEventCallback = () => {};
+
+ const expectedNetworkConnection = Factory.build(
+ FactoryType.NetworkConnection
+ );
+
+ const mockConnections: Array = [
+ Factory.build(FactoryType.NetworkConnection),
+ Factory.build(
+ FactoryType.NetworkConnection,
+ {
+ addEventListener: (
+ event: "change",
+ callback: VoidFunction
+ ) => {
+ changeEventCallback = callback;
+ },
+ }
+ ),
+ expectedNetworkConnection,
+ ];
+
+ const { networkConnectionResults, TestComponent } = setupSut({
+ mockConnections,
+ });
+
+ // Act
+ render(
+
+
+
+ );
+
+ act(() => changeEventCallback());
+
+ // Assert
+ expect(networkConnectionResults.all.length).toEqual(
+ mockConnections.length
+ );
+ expect(networkConnectionResults.current).toEqual(
+ expectedNetworkConnection
+ );
+ });
+ });
+
+ describe("when unmounted", () => {
+ it("calls removeEventlistener for cleanup", async () => {
+ // Arrange
+ const removeEventListener = jest.fn();
+ const networkConnection = Factory.build(
+ FactoryType.NetworkConnection,
+ {
+ removeEventListener,
+ }
+ );
+ const { TestComponent } = setupSut({
+ mockConnections: [networkConnection],
+ });
+
+ // Act
+ render(
+
+
+
+ );
+
+ await cleanup();
+
+ // Assert
+ expect(removeEventListener).toBeCalled();
+ });
+ });
+});
diff --git a/src/providers/network-connection-provider.tsx b/src/providers/network-connection-provider.tsx
new file mode 100644
index 0000000..77772f0
--- /dev/null
+++ b/src/providers/network-connection-provider.tsx
@@ -0,0 +1,76 @@
+import React, {
+ PropsWithChildren,
+ useCallback,
+ useEffect,
+ useState,
+} from "react";
+import {
+ NetworkConnection,
+ NetworkInformationUtils,
+} from "andculturecode-javascript-core";
+import { NetworkConnectionContext } from "../contexts/network-connection-context";
+
+/**
+ * Wrapper provider component that provides context to the `useNetworkConnection` hook
+ */
+export const NetworkConnectionProvider: React.FC = (
+ props: PropsWithChildren
+) => {
+ const { children } = props;
+
+ const [state, setState] = useState(
+ NetworkInformationUtils.getNetworkConnection()
+ );
+
+ const loadNetworkInformation = useCallback((isOnline?: boolean) => {
+ const networkConnection = NetworkInformationUtils.getNetworkConnection();
+
+ if (networkConnection == null) {
+ return;
+ }
+
+ setState((prev) => ({
+ ...prev,
+ ...networkConnection,
+ isOnline: isOnline ?? networkConnection.isOnline,
+ }));
+ }, []);
+
+ useEffect(
+ function handleNetworkChangeEvents() {
+ const networkConnection = NetworkInformationUtils.getNetworkConnection();
+
+ const createNetworkChangeHandler = (isOnline?: boolean) => () =>
+ loadNetworkInformation(isOnline);
+
+ const handleNetworkChange = createNetworkChangeHandler();
+ const handleOffline = createNetworkChangeHandler(false);
+ const handleOnline = createNetworkChangeHandler(true);
+
+ networkConnection?.addEventListener?.(
+ "change",
+ handleNetworkChange
+ );
+ window?.addEventListener?.("online", handleOnline);
+ window?.addEventListener?.("offline", handleOffline);
+
+ loadNetworkInformation();
+
+ return function cleanup() {
+ networkConnection?.removeEventListener?.(
+ "change",
+ handleNetworkChange
+ );
+ window?.removeEventListener?.("online", handleOnline);
+ window?.removeEventListener?.("offline", handleOffline);
+ };
+ },
+ [loadNetworkInformation]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/tests/factories/factory-type.ts b/src/tests/factories/factory-type.ts
index 6b72648..13c1c38 100644
--- a/src/tests/factories/factory-type.ts
+++ b/src/tests/factories/factory-type.ts
@@ -1,4 +1,5 @@
const FactoryType = {
+ NetworkConnection: "NetworkConnection",
RouteDefinition: {
Default: "RouteDefinition",
Nested: "RouteDefinition.Nested",
diff --git a/src/tests/factories/index.ts b/src/tests/factories/index.ts
index 65eb52c..26f68cb 100644
--- a/src/tests/factories/index.ts
+++ b/src/tests/factories/index.ts
@@ -4,3 +4,4 @@ export {
} from "./route-definition-factory";
export { RouteMapFactory } from "./route-map-factory";
export { StubResourceRecordFactory } from "andculturecode-javascript-testing";
+export { NetworkConnectionFactory } from "./network-connection-factory";
diff --git a/src/tests/factories/network-connection-factory.ts b/src/tests/factories/network-connection-factory.ts
new file mode 100644
index 0000000..c60e361
--- /dev/null
+++ b/src/tests/factories/network-connection-factory.ts
@@ -0,0 +1,26 @@
+import { NetworkConnection } from "andculturecode-javascript-core";
+import { Factory } from "rosie";
+import { FactoryType } from "./factory-type";
+
+// -----------------------------------------------------------------------------------------
+// #region Factory
+// -----------------------------------------------------------------------------------------
+
+export const NetworkConnectionFactory = Factory.define(
+ FactoryType.NetworkConnection
+)
+ .attr("isOnline", true)
+ .attr("addEventListener", eventListenerFactory)
+ .attr("removeEventListener", eventListenerFactory);
+
+// #endregion Factory
+
+// -----------------------------------------------------------------------------------------
+// #region Functions
+// -----------------------------------------------------------------------------------------
+
+function eventListenerFactory() {
+ return () => {};
+}
+
+// #endregion Functions