Skip to content

Commit

Permalink
Merge pull request #286 from josemarluedke/feat/prevent-focus-restore
Browse files Browse the repository at this point in the history
feat: add option to prevent focus restore on overlay and popover when trigger is hover
  • Loading branch information
josemarluedke committed Apr 4, 2024
2 parents d5c8d64 + d320756 commit ffb9b34
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 1 deletion.
5 changes: 5 additions & 0 deletions packages/overlays/src/components/overlay.gts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ interface OverlaySignature {
disableFlexContent?: boolean;
customContentModifier?: ModifierLike<{ Element: HTMLElement }>;
class?: string;
/**
* @defaultValue false
*/
preventFocusRestore?: boolean;

/**
* @defaultValue true
Expand Down Expand Up @@ -280,6 +284,7 @@ class Overlay extends Component<OverlaySignature> {

// restore focus
if (
!this.args.preventFocusRestore &&
this.focusedElement &&
(this.focusedElement as HTMLElement).tabIndex > -1 &&
typeof (this.focusedElement as HTMLElement).focus !== 'undefined'
Expand Down
9 changes: 9 additions & 0 deletions packages/overlays/src/components/popover.gts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class Popover extends Component<PopoverSignature> {
menuId = guidFor(this);
@tracked _isOpen = false;
@tracked isClosing = false;
@tracked preventFocusRestore = false;

get isOpen(): boolean {
if (
Expand Down Expand Up @@ -136,6 +137,7 @@ class Popover extends Component<PopoverSignature> {
(el: HTMLElement, [eventType]: [eventType?: 'click' | 'hover']) => {
this.triggerEl = el as HTMLLIElement;
if (eventType === 'hover') {
this.preventFocusRestore = true;
el.addEventListener('mouseenter', this.open);
el.addEventListener('mouseleave', this.close);
} else {
Expand Down Expand Up @@ -192,6 +194,7 @@ class Popover extends Component<PopoverSignature> {
isOpen=this.isOpen
toggle=this.toggle
internalDidClose=this.didClose
preventFocusRestore=this.preventFocusRestore
)
)
}}
Expand Down Expand Up @@ -239,6 +242,11 @@ interface ContentArgs
*/
id: string;

/**
* @internal
*/
preventFocusRestore?: boolean;

class?: string;

/**
Expand Down Expand Up @@ -345,6 +353,7 @@ class Content extends Component<ContentSignature> {
@closeOnEscapeKey={{@closeOnEscapeKey}}
@backdropTransition={{@backdropTransition}}
@class={{this.classNames}}
@preventFocusRestore={{@preventFocusRestore}}
id={{@id}}
...attributes
>
Expand Down
65 changes: 65 additions & 0 deletions site/app/components/signature-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7075,6 +7075,15 @@ const data: ComponentDoc[] = [
description: 'A function that will be called when opened',
tags: {}
},
{
identifier: 'preventFocusRestore',
type: { type: '<span class="hljs-built_in">boolean</span>' },
isRequired: false,
isInternal: false,
description: '',
tags: { defaultValue: { name: 'defaultValue', value: 'false' } },
defaultValue: '<span class="hljs-literal">false</span>'
},
{
identifier: 'renderInPlace',
type: { type: '<span class="hljs-built_in">boolean</span>' },
Expand Down Expand Up @@ -7726,6 +7735,14 @@ const data: ComponentDoc[] = [
description: 'A function that will be called when opened',
tags: {}
},
{
identifier: 'preventFocusRestore',
type: { type: '<span class="hljs-built_in">boolean</span>' },
isRequired: false,
isInternal: true,
description: '',
tags: { internal: { name: 'internal', value: '' } }
},
{
identifier: 'renderInPlace',
type: { type: '<span class="hljs-built_in">boolean</span>' },
Expand Down Expand Up @@ -8139,6 +8156,54 @@ const data: ComponentDoc[] = [
description: '',
tags: {}
},
{
package: 'utilities',
module: 'collapsible',
name: 'Collapsible',
fileName: 'packages/utilities/declarations/components/collapsible.d.ts',
Args: [
{
identifier: 'isOpen',
type: { type: '<span class="hljs-built_in">boolean</span>' },
isRequired: true,
isInternal: false,
description: 'If true, the content will be visible',
tags: {}
},
{
identifier: 'initialHeight',
type: { type: '<span class="hljs-built_in">string</span>' },
isRequired: false,
isInternal: false,
description:
"The height for the content in it's collapsed state.\nThe unit of the value should be included, eg. '10px'.",
tags: { defaultValue: { name: 'defaultValue', value: '0' } },
defaultValue: '<span class="hljs-number">0</span>'
}
],
Blocks: [
{
identifier: 'default',
type: {
type: '<span class="hljs-built_in">Array</span>',
raw: '[]',
items: []
},
isRequired: true,
isInternal: false,
description: '',
tags: {}
}
],
Element: {
identifier: 'Element',
type: { type: 'HTMLDivElement' },
description: '',
url: 'https://developer.mozilla.org/en-US/docs/Web/API/HTMLDivElement'
},
description: '',
tags: {}
},
{
package: 'utilities',
module: 'divider',
Expand Down
60 changes: 60 additions & 0 deletions test-app/tests/integration/components/overlays/overlay-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module(
});

const template = hbs`
<button type="button" data-test-id="some-button">Button</button>
<div id="my-destination"></div>
<Overlay
@isOpen={{this.isOpen}}
Expand All @@ -40,6 +41,7 @@ module(
@transitionDuration={{this.transitionDuration}}
@backdrop={{this.backdrop}}
@disableTransitions={{this.disableTransitions}}
@preventFocusRestore={{this.preventFocusRestore}}
@disableFocusTrap={{this.disableFocusTrap}}
@closeOnOutsideClick={{this.closeOnOutsideClick}}
@closeOnEscapeKey={{this.closeOnEscapeKey}}
Expand Down Expand Up @@ -209,5 +211,63 @@ module(
this.set('isOpen', false);
this.set('isOpen', true);
});

test('it manages focusing in content and restoration when focusTrap is disabled', async function (assert) {
this.set('disableTransitions', true);
this.set('disableFocusTrap', true);
this.set('isOpen', false);

await render(template);

(find('[data-test-id="some-button"]') as HTMLButtonElement).focus();
this.set('isOpen', true);
await settled();
assert.dom('[data-test-id="overlay"]').exists();

assert
.dom(document.activeElement)
.hasAttribute(
'data-test-id',
'overlay',
'should have focused in the overlay'
);

this.set('isOpen', false);
await settled();
assert.dom('[data-test-id="overlay"]').doesNotExist();

assert
.dom(document.activeElement)
.hasAttribute(
'data-test-id',
'some-button',
'should have restored the focus'
);

// Test when preventFocusRestore is true
// *************************************

this.set('preventFocusRestore', true);
this.set('isOpen', true);
await settled();
assert
.dom(document.activeElement)
.hasAttribute(
'data-test-id',
'overlay',
'should have focused in the overlay'
);

this.set('isOpen', false);
await settled();
assert.dom('[data-test-id="overlay"]').doesNotExist();

assert
.dom(document.activeElement)
.doesNotHaveAttribute(
'data-test-id',
'should have not restored the focus'
);
});
}
);
61 changes: 60 additions & 1 deletion test-app/tests/integration/components/overlays/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 { click, render } from '@ember/test-helpers';
import { click, render, triggerEvent, find } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import { registerCustomStyles } from '@frontile/theme';
import { tv } from 'tailwind-variants';
Expand Down Expand Up @@ -45,6 +45,58 @@ module(

assert.dom('[data-test-id="content"]').exists();
assert.dom('[data-test-id="content"]').containsText('Content here');
assert
.dom(document.activeElement)
.hasAttribute(
'data-test-id',
'content',
'should have focused in the content'
);
});

test('it works with trigger hover mode, prevents focus restore', async function (assert) {
await render(
hbs`
<div id="my-destination"></div>
<button type="button" data-test-id="focused-element">Button</button>
<Popover as |p|>
<button {{p.trigger "hover"}} {{p.anchor}} data-test-id="trigger">
Trigger
</button>
<p.Content
@destinationElementId="my-destination"
data-test-id="content"
>
Content here
</p.Content>
</Popover>`
);

(find('[data-test-id="focused-element"]') as HTMLButtonElement).focus();

assert.dom('[data-test-id="content"]').doesNotExist();
await triggerEvent('[data-test-id="trigger"]', 'mouseenter');

assert.dom('[data-test-id="content"]').exists();
assert.dom('[data-test-id="content"]').containsText('Content here');
assert
.dom(document.activeElement)
.hasAttribute(
'data-test-id',
'content',
'should have focused in the content'
);

await triggerEvent('[data-test-id="trigger"]', 'mouseleave');
assert.dom('[data-test-id="content"]').doesNotExist();

assert
.dom(document.activeElement)
.doesNotHaveAttribute(
'data-test-id',
'should have not restored the focus'
);
});

test('it renders accessibility attributes', async function (assert) {
Expand Down Expand Up @@ -138,6 +190,13 @@ module(
await click('#my-destination');
assert.dom('[data-test-id="content"]').doesNotExist();
assert.equal(calledClosed, true, 'should called didClose argument');
assert
.dom(document.activeElement)
.hasAttribute(
'data-test-id',
'trigger',
'should have restored the focus to the triggeer'
);
});

test('controlled isOpen', async function (assert) {
Expand Down

0 comments on commit ffb9b34

Please sign in to comment.