Skip to content

Commit

Permalink
feat: Announce offline status (#57178)
Browse files Browse the repository at this point in the history
* refactor: Rename `useIsConnected` to `useNetworkConnectivity`

Attempt to better communicate the Hook intent.

* test: Add `useNetworkConnectivity` Hook tests

* docs: Document `useNetworkConnectivity` Hook

* refactor: Rename `withIsConnected` to `withNetworkConnectivity`

* fix: Optimistically presume network connectivity

Prior to making the asynchronous request to the host app across the
bridge, it is a better UX to presume network connectivity is present
rather than displaying network connectivity messages briefly.

* test: Create realistic default `requestConnectionStatus` mock

* fix: Prevent duplicative offline status indicators

Hoist the `OfflineStatus` indicator from the block list to the editor.
The block list is leveraged for inner blocks, which means it rendered
nested `OfflineStatus` indicators for blocks with inner blocks.

Additionally, the `editor` package feels like an appropriate location
for the offline detection component.

* feat: Announce offline status

Improve the UX for screen reader users by announcing the state of
network connectivity whenever it changes.

* test: Add automated `OfflineStatus` tests

* refactor: Fix typo

* Add missing closing tag and fix indentation from merge

---------

Co-authored-by: Derek Blank <[email protected]>
  • Loading branch information
dcalhoun and derekblank authored Dec 28, 2023
1 parent 1fface7 commit a50e4c7
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 4 deletions.
61 changes: 57 additions & 4 deletions packages/editor/src/components/offline-status/index.native.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,71 @@
/**
* External dependencies
*/
import { Text, View } from 'react-native';
import { AccessibilityInfo, Text, View } from 'react-native';

/**
* WordPress dependencies
*/
import {
usePreferredColorSchemeStyle,
useNetworkConnectivity,
usePrevious,
} from '@wordpress/compose';
import { Icon } from '@wordpress/components';
import { offline as offlineIcon } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { useEffect } from '@wordpress/element';

/**
* Internal dependencies
*/
import styles from './style.native.scss';

/**
* Conditionally announces messages for screen reader users. This Hook provides
* two benefits over React Native's `accessibilityLiveRegion`:
*
* 1. It works on both iOS and Android.
* 2. It allows announcing a secondary message when the component is inactive.
*
* @param {string} message The message to announce.
* @param {Object} options Options for the Hook.
* @param {boolean} [options.isActive] Whether the message should be announced.
* @param {string} [options.inactiveMessage] The message to announce when inactive.
*/
function useAccessibilityLiveRegion( message, { isActive, inactiveMessage } ) {
const { announceForAccessibility } = AccessibilityInfo;
const prevIsActive = usePrevious( isActive );

useEffect( () => {
const unconditionalMessage = typeof isActive === 'undefined';
const initialRender = typeof prevIsActive === 'undefined';

if (
unconditionalMessage ||
( isActive && ! prevIsActive && ! initialRender )
) {
announceForAccessibility( message );
} else if ( ! isActive && prevIsActive && inactiveMessage ) {
announceForAccessibility( inactiveMessage );
}
}, [
message,
isActive,
prevIsActive,
inactiveMessage,
announceForAccessibility,
] );
}

const OfflineStatus = () => {
const { isConnected } = useNetworkConnectivity();

useAccessibilityLiveRegion( __( 'Network connection re-established' ), {
isActive: isConnected,
inactiveMessage: __( 'Network connection lost, working offline' ),
} );

const containerStyle = usePreferredColorSchemeStyle(
styles.offline,
styles.offline__dark
Expand All @@ -38,9 +82,18 @@ const OfflineStatus = () => {
);

return ! isConnected ? (
<View style={ containerStyle }>
<Icon fill={ iconStyle.fill } icon={ offlineIcon } />
<Text style={ textStyle }>{ __( 'Working Offline' ) }</Text>
<View
accessible
accessibilityRole="alert"
accessibilityLabel={ __(
'Network connection lost, working offline'
) }
style={ containerStyle }
>
<View style={ containerStyle }>
<Icon fill={ iconStyle.fill } icon={ offlineIcon } />
<Text style={ textStyle }>{ __( 'Working Offline' ) }</Text>
</View>
</View>
) : null;
};
Expand Down
108 changes: 108 additions & 0 deletions packages/editor/src/components/offline-status/test/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* External dependencies
*/
import { act, render, screen } from 'test/helpers';

/**
* WordPress dependencies
*/
import {
requestConnectionStatus,
subscribeConnectionStatus,
} from '@wordpress/react-native-bridge';

/**
* Internal dependencies
*/
import OfflineStatus from '../index';
import { AccessibilityInfo } from 'react-native';

jest.mock( '../style.native.scss', () => ( {
'offline--icon': {
fill: '',
},
} ) );

describe( 'when network connectivity is unavailable', () => {
beforeAll( () => {
requestConnectionStatus.mockImplementation( ( callback ) => {
callback( false );
return { remove: jest.fn() };
} );
} );

it( 'should display a helpful message', () => {
render( <OfflineStatus /> );

expect( screen.getByText( 'Working Offline' ) ).toBeVisible();
} );

it( 'should display an accessible message', () => {
render( <OfflineStatus /> );

expect(
screen.getByLabelText( 'Network connection lost, working offline' )
).toBeVisible();
} );

it( 'should announce network status', () => {
render( <OfflineStatus /> );

expect(
AccessibilityInfo.announceForAccessibility
).toHaveBeenCalledWith( 'Network connection lost, working offline' );
} );

it( 'should announce changes to network status', () => {
let subscriptionCallback;
subscribeConnectionStatus.mockImplementation( ( callback ) => {
subscriptionCallback = callback;
return { remove: jest.fn() };
} );
render( <OfflineStatus /> );

act( () => subscriptionCallback( { isConnected: false } ) );

expect(
AccessibilityInfo.announceForAccessibility
).toHaveBeenCalledWith( 'Network connection lost, working offline' );
} );
} );

describe( 'when network connectivity is available', () => {
beforeAll( () => {
requestConnectionStatus.mockImplementation( ( callback ) => {
callback( true );
return { remove: jest.fn() };
} );
} );

it( 'should not display a helpful message', () => {
render( <OfflineStatus /> );

expect( screen.queryByText( 'Working Offline' ) ).toBeNull();
} );

it( 'should not announce network status', () => {
render( <OfflineStatus /> );

expect(
AccessibilityInfo.announceForAccessibility
).not.toHaveBeenCalled();
} );

it( 'should announce changes to network status', () => {
let subscriptionCallback;
subscribeConnectionStatus.mockImplementation( ( callback ) => {
subscriptionCallback = callback;
return { remove: jest.fn() };
} );
render( <OfflineStatus /> );

act( () => subscriptionCallback( { isConnected: false } ) );

expect(
AccessibilityInfo.announceForAccessibility
).toHaveBeenCalledWith( 'Network connection lost, working offline' );
} );
} );

0 comments on commit a50e4c7

Please sign in to comment.