Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/dev/s2-docs/pages/react-aria/useFocusVisible.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ in CSS.
To determine whether a focus ring should be visible for an individual component rather than
globally, see [useFocusRing](useFocusRing).

### Making focus visible

For the most part, focus visibility is handled automatically by `useFocusVisible`. However, if you need to
manually show the focus ring, such as when moving focus programmatically to the first item
of an invalid form, you can use `setInteractionModality`.

```tsx
import {setInteractionModality} from 'react-aria/useFocusVisible';

setInteractionModality('keyboard');
```



## API

<FunctionAPI function={docs.exports.useFocusVisible} links={docs.links} />
Expand Down
2 changes: 2 additions & 0 deletions packages/react-aria-components/exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ export {Focusable} from 'react-aria/Focusable';
export {VisuallyHidden} from 'react-aria/VisuallyHidden';
export {FormValidationContext} from 'react-stately/private/form/useFormValidationState';
export {parseColor, getColorChannels} from 'react-stately/Color';
export {setInteractionModality} from 'react-aria/useFocusVisible';
export {ToastQueue as UNSTABLE_ToastQueue} from 'react-stately/useToastState';
export {useListData} from 'react-stately/useListData';
export {useTreeData} from 'react-stately/useTreeData';
Expand Down Expand Up @@ -608,6 +609,7 @@ export type {DateRangePickerState} from 'react-stately/useDateRangePickerState';
export type {DisclosureState} from 'react-stately/useDisclosureState';
export type {DisclosureGroupState} from 'react-stately/useDisclosureGroupState';
export type {ListState} from 'react-stately/useListState';
export type {Modality} from 'react-aria/useFocusVisible';
export type {NumberFieldState} from 'react-stately/useNumberFieldState';
export type {OverlayTriggerState} from 'react-stately/useOverlayTriggerState';
export type {QueuedToast, ToastOptions, ToastState} from 'react-stately/useToastState';
Expand Down
9 changes: 6 additions & 3 deletions packages/react-aria/exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ export {
export {useNumberFormatter} from '../src/i18n/useNumberFormatter';
export {useListFormatter} from '../src/i18n/useListFormatter';
export {useFocus} from '../src/interactions/useFocus';
export {useFocusVisible} from '../src/interactions/useFocusVisible';
export {useShowFocusIndicator} from '../src/interactions/useFocusVisible';
export {setInteractionModality, useFocusVisible} from '../src/interactions/useFocusVisible';
export {useFocusWithin} from '../src/interactions/useFocusWithin';
export {useHover} from '../src/interactions/useHover';
export {useInteractOutside} from '../src/interactions/useInteractOutside';
Expand Down Expand Up @@ -331,7 +330,11 @@ export type {
} from '../src/dnd/useDroppableCollection';
export type {DroppableItemOptions, DroppableItemResult} from '../src/dnd/useDroppableItem';
export type {FocusProps, FocusResult} from '../src/interactions/useFocus';
export type {FocusVisibleProps, FocusVisibleResult} from '../src/interactions/useFocusVisible';
export type {
Modality,
FocusVisibleProps,
FocusVisibleResult
} from '../src/interactions/useFocusVisible';
export type {FocusWithinProps, FocusWithinResult} from '../src/interactions/useFocusWithin';
export type {HoverProps, HoverResult} from '../src/interactions/useHover';
export type {InteractOutsideProps} from '../src/interactions/useInteractOutside';
Expand Down
3 changes: 2 additions & 1 deletion packages/react-aria/exports/useFocusVisible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* governing permissions and limitations under the License.
*/

export {useFocusVisible} from '../src/interactions/useFocusVisible';
export {setInteractionModality, useFocusVisible} from '../src/interactions/useFocusVisible';
export type {FocusVisibleProps, FocusVisibleResult} from '../src/interactions/useFocusVisible';
export type {FocusEvents} from '@react-types/shared';
export type {Modality} from '../src/interactions/useFocusVisible';
13 changes: 0 additions & 13 deletions packages/react-aria/exports/useShowFocusIndicator.ts

This file was deleted.

9 changes: 1 addition & 8 deletions packages/react-aria/src/interactions/useFocusVisible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {isMac} from '../utils/platform';
import {isVirtualClick} from '../utils/isVirtualEvent';
import {openLink} from '../utils/openLink';
import {PointerType} from '@react-types/shared';
import {useCallback, useEffect, useState} from 'react';
import {useEffect, useState} from 'react';
import {useIsSSR} from '../ssr/SSRProvider';

export type Modality = 'keyboard' | 'pointer' | 'virtual';
Expand Down Expand Up @@ -303,13 +303,6 @@ export function setInteractionModality(modality: Modality): void {
triggerChangeHandlers(modality, null);
}

/**
* Returns a callback that makes the focus indicator visible.
*/
export function useShowFocusIndicator(): () => void {
return useCallback(() => setInteractionModality('keyboard'), []);
}

/** @private */
export function getPointerType(): PointerType {
return currentPointerType;
Expand Down
37 changes: 1 addition & 36 deletions packages/react-aria/test/interactions/useFocusVisible.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ import {
import {
addWindowFocusTracking,
useFocusVisible,
useFocusVisibleListener,
useShowFocusIndicator
useFocusVisibleListener
} from '../../src/interactions/useFocusVisible';
import {changeHandlers, hasSetupGlobalListeners} from '../../src/interactions/useFocusVisible';
import {mergeProps} from '../../src/utils/mergeProps';
Expand Down Expand Up @@ -376,40 +375,6 @@ describe('useFocusVisible', function () {
});
});

describe('useShowFocusIndicator', function () {
// A form library that moves focus to the first invalid field does so by calling
// element.focus() from the submit handler. Clicking submit leaves the modality on
// 'pointer', so the programmatic focus lands without a visible indicator.
function FormExample() {
let {focusProps, isFocusVisible} = useFocusRing();
let showFocusIndicator = useShowFocusIndicator();
let ref = React.useRef(null);

let onSubmit = e => {
e.preventDefault();
showFocusIndicator();
ref.current.focus();
};

return (
<form onSubmit={onSubmit}>
<input {...focusProps} ref={ref} data-focus-visible={isFocusVisible || undefined} />
<button type="submit">Submit</button>
</form>
);
}

it('shows the focus indicator on programmatic focus after a pointer interaction', async function () {
let user = userEvent.setup({delay: null, pointerMap});
render(<FormExample />);
await user.click(screen.getByRole('button', {name: 'Submit'}));

let input = screen.getByRole('textbox');
expect(input).toHaveFocus();
expect(input).toHaveAttribute('data-focus-visible');
});
});

describe('useFocusVisibleListener', function () {
it('emits on modality change (non-text input)', function () {
let fnMock = jest.fn();
Expand Down