Soundorv0.13.0

FocusScope

A boundary for keyboard focus, to trap it, start it and restore it.

FocusScope groups its children for keyboard focus. It adds no node, so layout is as if it were not there.

import { FocusScope, Pressable, Text, TextInput, View } from '@soundor/react';
import { useState } from 'react';

function Button({ label, onPress }: { label: string; onPress: () => void }) {
  return (
    <Pressable
      onPress={onPress}
      style={({ focused }) => ({
        padding: 8,
        borderRadius: 6,
        borderWidth: 2,
        borderColor: focused ? '#8fb3ff' : 'transparent',
        backgroundColor: '#3a7bfd',
      })}
    >
      <Text style={{ color: 'white' }}>{label}</Text>
    </Pressable>
  );
}

// Click into the panel, then press Tab: focus cycles inside it while
// trapped, starts in it (autoFocus), and goes back where it was when the
// panel closes (restoreFocus).
export default function TrappedPanel() {
  const [open, setOpen] = useState(false);

  return (
    <View style={{ width: 300, gap: 10 }}>
      <Button
        label={open ? 'Close panel' : 'Open panel'}
        onPress={() => setOpen(!open)}
      />
      {open && (
        <FocusScope trapped autoFocus restoreFocus>
          <View
            style={{
              gap: 8,
              padding: 10,
              borderRadius: 8,
              backgroundColor: '#24262c',
            }}
          >
            <TextInput
              accessibilityLabel="Preset name"
              placeholder="Preset name"
              style={{
                padding: 6,
                borderRadius: 6,
                backgroundColor: '#17181c',
                color: '#e6e7ea',
              }}
            />
            <View style={{ flexDirection: 'row', gap: 8 }}>
              <Button label="Save" onPress={() => setOpen(false)} />
              <Button label="Cancel" onPress={() => setOpen(false)} />
            </View>
          </View>
        </FocusScope>
      )}
    </View>
  );
}
focus-scope.tsxSource
  • trapped: Tab and Shift+Tab cycle through the scope's focusable nodes, wrapping at the ends, and focus moved outside comes back. The innermost trapping scope wins.
  • autoFocus: on mount, focuses the first focusable node, unless focus is already inside.
  • restoreFocus: on unmount, gives focus back to the node that had it on mount, if that node is still there and focusable.

All three are false by default. Membership follows React, not the node tree: what the scope's children render belongs to it wherever it shows, through a Portal too. A Modal is a trapped scope with autoFocus and restoreFocus.

Props

Prop

Type

On this page