Skip to content
This repository has been archived by the owner on Jan 6, 2023. It is now read-only.

Commit

Permalink
feat: UiPopover focusable content can be tabbed through
Browse files Browse the repository at this point in the history
  • Loading branch information
mdeanjones committed Dec 29, 2022
1 parent 8d8fc10 commit a117aa9
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 7 deletions.
20 changes: 18 additions & 2 deletions addon/components/ui-popover/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import UiContextualContainer, {
SelectorStrategies,
TriggerEvents,
} from '../-internals/contextual-container/component';
import { manageFlowThroughFocus } from '@nsf-open/ember-ui-foundation/utils';

/**
*
Expand Down Expand Up @@ -30,18 +31,33 @@ export default class UiPopoverContextContainer extends UiContextualContainer {
/** @hidden */
readonly overlayComponent = 'ui-popover/element';

private removeFocus?: () => void;

public didInsertElement() {
super.didInsertElement();
this.getAriaElement()?.setAttribute('aria-expanded', 'false');
}

/** @hidden */
onShow = () => {
this.getAriaElement()?.setAttribute('aria-expanded', 'true');
const trigger = this.getAriaElement();

trigger?.setAttribute('aria-expanded', 'true');

// Focus management only needs to occur if the popover is not being
// rendered inline.
if (!this.actuallyRenderInPlace) {
this.removeFocus = manageFlowThroughFocus(
this.getOverlayElement() ?? undefined,
trigger ?? undefined
);
}
};

/** @hidden */
onHide = () => {
this.getAriaElement()?.setAttribute('aria-expanded', 'false');
this.getAriaElement()?.setAttribute('aria-expanded', 'true');
this.removeFocus?.();
this.removeFocus = undefined;
};
}
39 changes: 35 additions & 4 deletions addon/components/ui-popover/ui-popover.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ export default {
component: 'components/ui-popover/component',

parameters: {
layout: 'centered',
docs: {
iframeHeight: 300,
},
},

args: {
textContent: 'Hello World',
title: 'Please Login to Your Account to Continue',
},
};

Expand All @@ -20,12 +22,41 @@ const Template = (context: unknown) => {
// language=handlebars
template: hbs`
<div class="text-center">
<a href="#">Other focus target A</a>
<UiButton @variant="primary">
Log In
<UiPopover @title="Hello World">
Username and password go here
<UiPopover
@title={{this.title}}
@ariaAttachAs={{this.ariaAttachAs}}
@autoPlacement={{this.autoPlacement}}
@delay={{this.delay}}
@distance={{this.distance}}
@enabled={{this.enabled}}
@fade={{this.fade}}
@maxWidth={{this.maxWidth}}
@overlayId={{this.overlayId}}
@placement={{this.placement}}
@renderInPlace={{this.renderInPlace}}
@testId={{this.testId}}
>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" class="form-control" />
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" class="form-control" />
</div>
<div class="text-right">
<button type="button" class="btn btn-primary">Login</button>
</div>
</UiPopover>
</UiButton>
<a href="#">Other focus target B</a>
</div>
`,
};
Expand Down
1 change: 1 addition & 0 deletions addon/styles/addon.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ nice with other libraries/tools. */

.popover {
display: block;
margin: 0 !important;
}

.popover.top .arrow,
Expand Down
56 changes: 55 additions & 1 deletion tests/integration/components/ui-popover-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find, click } from '@ember/test-helpers';
import { render, find, click, focus, triggerKeyEvent } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';

module('Integration | Component | ui-popover', function (hooks) {
Expand Down Expand Up @@ -64,4 +64,58 @@ module('Integration | Component | ui-popover', function (hooks) {
.hasTagName('header')
.hasText('Popover Title');
});

test('it manages focus as though it were inline with its trigger', async function (assert) {
// language=handlebars
await render(hbs`
<a href="#">Other focus target A</a>
<UiButton @variant="primary" id="trigger">
Log In
<UiPopover @title="Please Login to Your Account to Continue" @renderInPlace={{this.renderInPlace}}>
<label for="username">Username</label>
<input type="text" id="username" />
<label for="password">Password</label>
<input type="password" id="password" />
<button type="button" id="submitLogin">Login</button>
</UiPopover>
</UiButton>
<a href="#" id="alternateFocusB">Other focus target B</a>
`);

// See https://github.com/emberjs/ember-test-helpers/issues/738
// Some keyboard interaction are particularly difficult to fake with Javascript,
// and tabbing through focusable elements is one of them. For this, we only
// _really_ want to make sure that focus gets wrapped between first <-> last
// elements in the modal. Going out on a limb here that the browser is capable
// of handing everything in-between.

assert.dom('.popover').isNotVisible();

await click('#trigger');

assert.dom('.popover').isVisible();
assert.dom('#trigger').isFocused();

await triggerKeyEvent('#trigger', 'keydown', 'Tab');

assert.dom('.popover #username').isFocused();

await focus('.popover #submitLogin');
await triggerKeyEvent('.popover', 'keydown', 'Tab');

assert.dom('#alternateFocusB').isFocused();

await triggerKeyEvent('#alternateFocusB', 'keydown', 'Tab', { shiftKey: true });

assert.dom('.popover #submitLogin').isFocused();

await focus('.popover #username');
await triggerKeyEvent('.popover', 'keydown', 'Tab', { shiftKey: true });

assert.dom('#trigger').isFocused();
});
});

0 comments on commit a117aa9

Please sign in to comment.