Skip to content

fix(ui-modal): skip Modal.Body tab stop when it has focusable children#2584

Open
ToMESSKa wants to merge 1 commit into
masterfrom
INSTUI-5046-modal-body-does-not-need-tab-index-0-if-it-contains-interactive-elements
Open

fix(ui-modal): skip Modal.Body tab stop when it has focusable children#2584
ToMESSKa wants to merge 1 commit into
masterfrom
INSTUI-5046-modal-body-does-not-need-tab-index-0-if-it-contains-interactive-elements

Conversation

@ToMESSKa

@ToMESSKa ToMESSKa commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

INSTUI-5046

ISSUE:

  • Modal.Body gets a redundant tabIndex={0} when it's scrollable but already contains focusable elements, creating a redundant tab stop. It should only get a tabIndex={0} when it has no interactive elements.

TEST PLAN:

  • Copy the code below into the documentation.

  • Open the modal using only the keyboard.

    • Navigate to the modal body using the Tab key.
    • The modal body should receive focus.
    • Once focused, the body should be scrollable using the Down Arrow and Up Arrow keys.
  • Keep the modal open.

    • Navigate to the "Scrollable body" toggle and turn it off.
    • While the modal remains open, navigate back to the modal body.
    • This time, the modal body should not receive focus.
  • Keep the modal open.

    • Turn the "Scrollable body" toggle back on.
    • Verify that the modal body can be focused again.
    • While the modal is open, turn on the "Focusable element in body" toggle.
    • Navigate to the modal body again — this time, the whole body should not be focused. Instead, focus should jump only to the button inside the body.
  • Turn the "Focusable element in body" toggle back off.

    • The modal body should once again be focusable.
  • Check that Modal.Body gets tabIndex={0} when it contains no focusable elements.

  • Check that Modal.Body does NOT get tabIndex={0} when it contains at least one focusable element.

  • should work in v1 and v2 too

const longText = lorem.paragraphs(100)
const shortText = lorem.paragraphs(1)

const Example = () => {
  const [open, setOpen] = useState(false)
  const [scrollable, setScrollable] = useState(true)
  const [hasFocusable, setHasFocusable] = useState(false)

  const handleButtonClick = () => {
    setOpen((state) => !state)
  }

  const renderCloseButton = () => {
    return (
      <CloseButton
        placement="end"
        offset="small"
        onClick={handleButtonClick}
        screenReaderLabel="Close"
      />
    )
  }

  return (
    <div style={{ padding: '0 0 11rem 0', margin: '0 auto' }}>
      <Button onClick={handleButtonClick}>
        {open ? 'Close' : 'Open'} the Modal
      </Button>
      <Modal
        open={open}
        onDismiss={() => {
          setOpen(false)
        }}
        size="medium"
        label="Hello World"
        shouldCloseOnDocumentClick
      >
        <Modal.Header>
          {renderCloseButton()}
          <Heading>Hello World</Heading>
        </Modal.Header>
        <Modal.Body>
          {hasFocusable && (
            <View as="div" margin="0 0 small 0">
              <Button>A focusable element inside the body</Button>
            </View>
          )}
          <Text lineHeight="double">{scrollable ? longText : shortText}</Text>
        </Modal.Body>
        <Modal.Footer>
          <View as="div" margin="0 auto 0 0">
            <View as="div" display="block" margin="0 0 small 0">
              <Checkbox
                label="Scrollable body"
                variant="toggle"
                size="small"
                checked={scrollable}
                onChange={() => setScrollable((v) => !v)}
              />
            </View>
            <Checkbox
              label="Focusable element in body"
              variant="toggle"
              size="small"
              checked={hasFocusable}
              onChange={() => setHasFocusable((v) => !v)}
            />
          </View>
          <Button onClick={handleButtonClick}>Close</Button>
        </Modal.Footer>
      </Modal>
    </div>
  )
}

render(<Example />)

@ToMESSKa ToMESSKa self-assigned this Jun 9, 2026
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://instructure.design/pr-preview/pr-2584/

Built to branch gh-pages at 2026-06-09 14:42 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

Visual regression report

No changes.

Status Count
Unchanged 32
Changed 0
New 0
Removed 0

📊 View full report

Baselines come from the visual-baselines branch. They refresh on every merge to master.

github-actions Bot pushed a commit that referenced this pull request Jun 9, 2026
'type',
'class',
'style'
]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

based on the set of attributes that findTabbable reads

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put this as a comment to findTabbble? This is important because we might replace findTabbable in the future, it has several flaws

if (isFirefox) {
this.setState({ isFirefox })
}

@ToMESSKa ToMESSKa Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was needed to handle the the case when the modal body content changes dinamically (a scrollbar or a focusable element appears or disappears)

@ToMESSKa ToMESSKa requested review from joyenjoyer and matyasf June 10, 2026 07:37

@matyasf matyasf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good, just a small comment. In the future we should look into replacing this with some native code (e.g. https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog ) because there are constantly issues with this code

'type',
'class',
'style'
]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put this as a comment to findTabbble? This is important because we might replace findTabbable in the future, it has several flaws

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants