Skip to content

Commit

Permalink
Add support for keyboard via a useFocus argument
Browse files Browse the repository at this point in the history
  • Loading branch information
Pjaerr committed Nov 12, 2024
1 parent 2e076cb commit 4cf01a7
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 27 deletions.
93 changes: 82 additions & 11 deletions addon/components/tooltip.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { getPosition, getCoords } from '@zestia/position-utils';
import { guidFor } from '@ember/object/internals';
import { htmlSafe } from '@ember/template';
import { inject } from '@ember/service';
import { on } from '@ember/modifier';
import { tracked } from '@glimmer/tracking';
import { waitFor } from '@ember/test-waiters';
import { waitForAnimation } from '@zestia/animation-utils';
Expand All @@ -29,6 +28,8 @@ export default class TooltipComponent extends Component {
isLoaded;
isOverTooltipElement;
isOverTooltipperElement;
tooltipperElementIsFocused;
tooltipElementIsFocused;
loadDuration = 0;
showTimer;
stickyTimer;
Expand Down Expand Up @@ -84,12 +85,22 @@ export default class TooltipComponent extends Component {
}

get needsToShowTooltip() {
return (
!(this.isOverTooltipperElement && this.args.show === false) &&
(this.isOverTooltipperElement ||
this.isOverTooltipElement ||
this.args.show)
);
if (this.args.show === false) {
return false;
}

if (this.isOverTooltipperElement || this.isOverTooltipElement) {
return true;
}

if (
this.args.useFocus &&
(this.tooltipperElementIsFocused || this.tooltipElementIsFocused)
) {
return true;
}

return this.args.show;
}

get tooltips() {
Expand Down Expand Up @@ -200,6 +211,37 @@ export default class TooltipComponent extends Component {
this._scheduleHideTooltip();
}

@action
async handleFocusTooltipperElement() {
this.tooltipperElementIsFocused = true;
this.loadDuration = 0;

if (this.shouldLoadEagerly) {
await this._load();
}

this._scheduleShowTooltip();
}

@action
handleBlurTooltipperElement() {
this.tooltipperElementIsFocused = false;

this._scheduleHideTooltip();
}

@action
handleFocusTooltipElement() {
this.tooltipElementIsFocused = true;
}

@action
handleBlurTooltipElement() {
this.tooltipElementIsFocused = false;

this._scheduleHideTooltip();
}

@action
hide() {
return this._hideTooltip();
Expand Down Expand Up @@ -418,16 +460,46 @@ export default class TooltipComponent extends Component {
return () => this._stopTether();
});

events = modifier((element, [otherElement]) => {
tooltipperEvents = modifier((element, [otherElement]) => {
this.element = element;
const { tooltipperElement: el } = this;

this._add(el, 'mouseenter', this.handleMouseEnterTooltipperElement);
this._add(el, 'mouseleave', this.handleMouseLeaveTooltipperElement);

if (this.args.useFocus) {
this._add(el, 'focus', this.handleFocusTooltipperElement);
this._add(el, 'blur', this.handleBlurTooltipperElement);
}

return () => {
this._remove(el, 'mouseenter', this.handleMouseEnterTooltipperElement);
this._remove(el, 'mouseleave', this.handleMouseLeaveTooltipperElement);

if (this.args.useFocus) {
this._remove(el, 'focus', this.handleFocusTooltipperElement);
this._remove(el, 'blur', this.handleBlurTooltipperElement);
}
};
});

tooltipEvents = modifier((element) => {
this._add(element, 'mouseenter', this.handleMouseEnterTooltip);
this._add(element, 'mouseleave', this.handleMouseLeaveTooltip);

if (this.args.useFocus) {
this._add(element, 'focus', this.handleFocusTooltipElement);
this._add(element, 'blur', this.handleBlurTooltipElement);
}

return () => {
this._remove(element, 'mouseenter', this.handleMouseEnterTooltip);
this._remove(element, 'mouseleave', this.handleMouseLeaveTooltip);

if (this.args.useFocus) {
this._remove(element, 'focus', this.handleFocusTooltipElement);
this._remove(element, 'blur', this.handleBlurTooltipElement);
}
};
});

Expand Down Expand Up @@ -462,7 +534,7 @@ export default class TooltipComponent extends Component {
<span
class="__tooltip__"
hidden
{{this.events @element}}
{{this.tooltipperEvents @element}}
{{this.className}}
{{this.visibility @show}}
{{this.loading this.isLoading}}
Expand All @@ -478,8 +550,7 @@ export default class TooltipComponent extends Component {
style={{this.tooltipStyle}}
role="tooltip"
aria-live="polite"
{{on "mouseenter" this.handleMouseEnterTooltip}}
{{on "mouseleave" this.handleMouseLeaveTooltip}}
{{this.tooltipEvents}}
{{this.register}}
{{this.aria}}
{{this.position @position @columns @rows @destination @attachTo}}
Expand Down
9 changes: 2 additions & 7 deletions tests/dummy/app/controllers/manual.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@ export default class ManualPositioningController extends Controller {
@tracked shouldShowTooltip;

@action
showTooltip() {
this.shouldShowTooltip = true;
}

@action
hideTooltip() {
this.shouldShowTooltip = false;
toggleTooltip() {
this.shouldShowTooltip = !this.shouldShowTooltip;
}
}
1 change: 1 addition & 0 deletions tests/dummy/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ Router.map(function () {
this.route('attach-to');
this.route('sticky');
this.route('tether');
this.route('use-focus');
});
6 changes: 6 additions & 0 deletions tests/dummy/app/templates/application.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@
<LinkTo @route="tether">
Tether
</LinkTo>

|

<LinkTo @route="use-focus">
Use Focus
</LinkTo>
</p>

{{outlet}}
Expand Down
8 changes: 1 addition & 7 deletions tests/dummy/app/templates/manual.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@
</p>

<div>
<input
type="text"
placeholder="Focus me"
aria-label="Example text area"
{{on "focus" this.showTooltip}}
{{on "blur" this.hideTooltip}}
/>
<button type="button" {{on "click" this.toggleTooltip}}>Toggle Tooltip</button>

{{#if this.shouldShowTooltip}}
<Tooltip @show={{true}}>
Expand Down
21 changes: 21 additions & 0 deletions tests/dummy/app/templates/use-focus.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<p>
Use Focus (for keyboard support)
</p>

<a href="#">
I should show when focused with a keyboard

<Tooltip @useFocus={{true}}>
Hello World
</Tooltip>
</a>

<a href="#">
I should
<i>not</i>
show when focused with a keyboard

<Tooltip @useFocus={{false}}>
Goodbye World
</Tooltip>
</a>
20 changes: 19 additions & 1 deletion tests/integration/components/tooltip/focus-events-test.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Tooltip from '@zestia/ember-async-tooltips/components/tooltip';
module('tooltip | focus', function (hooks) {
setupRenderingTest(hooks);

test('focusing', async function (assert) {
test('focusing (default)', async function (assert) {
assert.expect(1);

await render(<template>
Expand All @@ -24,4 +24,22 @@ module('tooltip | focus', function (hooks) {
`
);
});

test('focusing (@useFocus)', async function (assert) {
assert.expect(1);

await render(<template>
<a href="#">
<Tooltip @useFocus={{true}} />
</a>
</template>);

await focus('.tooltipper');

assert.dom('.tooltip').exists(
`
tooltips are displayed when focus enters a reference element if @useFocus is true.
`
);
});
});
41 changes: 41 additions & 0 deletions tests/integration/components/tooltip/loading-data-test.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,47 @@ module('tooltip | loading data', function (hooks) {
assert.dom('.tooltip').doesNotExist();
});

test('focus / loading data', async function (assert) {
assert.expect(3);

const load = () => assert.step('loading data');

await render(<template>
<div>
<Tooltip @onLoad={{load}} @useFocus={{true}} />
</div>
</template>);

triggerEvent('.tooltipper', 'focus');

await settled();

assert.verifySteps(['loading data']);

assert.dom('.tooltip').exists();
});

test('blur / loading data', async function (assert) {
assert.expect(3);

const load = () => assert.step('loading data');

await render(<template>
<div>
<Tooltip @onLoad={{load}} @useFocus={{true}} />
</div>
</template>);

triggerEvent('.tooltipper', 'focus');
triggerEvent('.tooltipper', 'blur');

await settled();

assert.verifySteps(['loading data']);

assert.dom('.tooltip').doesNotExist();
});

test('loading data with show arg renders correct position straight away', async function (assert) {
assert.expect(2);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'dummy/tests/helpers';
import { render, settled } from '@ember/test-helpers';
import { render, settled, click, triggerEvent } from '@ember/test-helpers';
import { on } from '@ember/modifier';
import { tracked } from '@glimmer/tracking';
import Tooltip from '@zestia/ember-async-tooltips/components/tooltip';

Expand Down Expand Up @@ -34,4 +35,58 @@ module('tooltip | manual', function (hooks) {

assert.dom('.tooltip').doesNotExist();
});

test('mouse/keyboard events do not effect tooltip when manually showing / hiding', async function (assert) {
assert.expect(6);

const state = new (class {
@tracked show;
})();

const toggleTooltip = () => {
state.show = !state.show;
};

await render(<template>
<button
type="button"
class="tooltipper-element"
{{on "click" toggleTooltip}}
>
<Tooltip @show={{state.show}} />
</button>
</template>);

assert.dom('.tooltip').doesNotExist();

await triggerEvent('.tooltipper-element', 'mouseenter');
await click('.tooltipper-element');

assert.dom('.tooltip').exists();

await triggerEvent('.tooltipper-element', 'mouseleave');

assert
.dom('.tooltip')
.exists(
'it still shows the tooltip despite the mouse leaving the tooltipper as we are manually showing and hiding'
);

await click('.tooltipper-element');

assert.dom('.tooltip').doesNotExist();

await triggerEvent('.tooltipper-element', 'focus');
await click('.tooltipper-element');

assert.dom('.tooltip').exists();

await triggerEvent('.tooltipper-element', 'blur');

assert
.dom('.tooltip')
.exists(
'it still shows the tooltip despite the blur event as we are manually showing and hiding'
);
});
});

0 comments on commit 4cf01a7

Please sign in to comment.