Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Popover component #51

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ module.exports = {
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
rules: {
'@typescript-eslint/explicit-module-boundary-types': 'off',
},
},

// node files
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
4 changes: 2 additions & 2 deletions addon/components/announcement/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import Component from '@glimmer/component';
*
* @see https://www.w3.org/TR/wai-aria-1.1/#attrs_liveregions
*/
export default class Text extends Component {
export default class Announcement extends Component {
appParent;

constructor(owner, args) {
constructor(owner: unknown, args: Record<string, unknown>) {
super(owner, args);

// eslint-disable-next-line
Expand Down
23 changes: 23 additions & 0 deletions addon/components/popover/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Moo
{{yield
(hash
isOpen=this.isOpen
openPopover=this.openPopover
closePopover=this.closePopover
Trigger=(component
"popover/trigger"
triggerGuid=this.triggerGuid
panelGuid=this.panelGuid
isOpen=this.isOpen
openPopover=this.openPopover
closePopover=this.closePopover
)
Panel=(component
"popover/panel"
triggerGuid=this.triggerGuid
panelGuid=this.panelGuid
isOpen=this.isOpen
closePopover=this.closePopover
)
)
}}
48 changes: 48 additions & 0 deletions addon/components/popover/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { action } from '@ember/object';
import { guidFor } from '@ember/object/internals';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

interface PopoverArgs {
className?: string;
}

export default class Popover extends Component<PopoverArgs> {
guid = `${guidFor(this)}-popover`;
@tracked isOpen = false;

@action
openPopover() {
this.isOpen = true;
}

@action
closePopover(focusableElement?: HTMLElement) {
this.isOpen = false;

const elementToFocus = (() => {
if (!focusableElement) return this.triggerElement;
if (focusableElement instanceof HTMLElement) return focusableElement;

return this.triggerElement;
})();

elementToFocus?.focus();
}

get triggerGuid() {
return `${this.guid}-trigger`;
}

get panelGuid() {
return `${this.guid}-panel`;
}

get triggerElement() {
// eslint-disable-next-line
// @ts-ignore
return typeof FastBoot === 'undefined'
? document.getElementById(this.triggerGuid)
: null;
}
}
10 changes: 10 additions & 0 deletions addon/components/popover/panel/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{{#if @isOpen}}
<div
id={{@panelGuid}}
aria-labelledby={{@buttonGuid}}
tabindex="-1"
...attributes
>
{{yield}}
</div>
{{/if}}
13 changes: 13 additions & 0 deletions addon/components/popover/trigger/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{{! template-lint-disable no-down-event-binding }}
<button
id={{@triggerGuid}}
type="button"
aria-haspopup="true"
aria-controls={{if @isOpen @panelGuid}}
aria-expanded={{@isOpen}}
...attributes
{{on "click" @toggleMenu}}
{{on "keydown" this.handleKeydown}}
>
{{yield}}
</button>
38 changes: 38 additions & 0 deletions addon/components/popover/trigger/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { action } from '@ember/object';
import { next } from '@ember/runloop';
import Component from '@glimmer/component';

import { normalizeKey } from 'backpack-ember/utils/normalize-keycode';
stormwarning marked this conversation as resolved.
Show resolved Hide resolved

interface TriggerArgs {
triggerGuid: string;
isOpen: boolean;
openPopover: () => void;
closePopover: () => void;
}

export default class Trigger extends Component<TriggerArgs> {
@action
handleKeydown(event: KeyboardEvent) {
// @ts-expect-error -- `disabled` does exist on evt.target
if (event.target?.disabled) return;

const eventKey = normalizeKey(event);
switch (eventKey) {
case 'Space':
case 'Enter':
event.preventDefault();
event.stopPropagation();

if (this.args.isOpen && eventKey === 'Enter') {
this.args.closePopover();
} else {
this.args.openPopover();
next(() => {
// Focus first item?
});
}
break;
}
}
}
14 changes: 14 additions & 0 deletions addon/utils/normalize-keycode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
type KeyboardEventValues = Pick<KeyboardEvent, 'key' | 'keyCode'>;

/**
* Normalizes the 'key' property of a KeyboardEvent in IE/Edge.
*/
export function normalizeKey({ key, keyCode }: KeyboardEventValues) {
if (keyCode >= 37 && keyCode <= 40 && key.indexOf('Arrow') !== 0) {
return `Arrow${key}`;
}
if (keyCode === 27) {
return 'Escape';
}
return key;
}
1 change: 1 addition & 0 deletions app/components/popover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@showbie/backpack-ember/components/popover';
1 change: 1 addition & 0 deletions app/components/popover/panel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@showbie/backpack-ember/components/popover/panel';
1 change: 1 addition & 0 deletions app/components/popover/trigger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@showbie/backpack-ember/components/popover/trigger';
1 change: 1 addition & 0 deletions app/utils/normalize-keycode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { normalizeKey } from '@showbie/backpack-ember/utils/normalize-keycode';
2 changes: 1 addition & 1 deletion ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module.exports = function (defaults) {

/** @see https://github.com/ef4/prember */
prember: {
urls: ['/', '/badge', '/stack', '/text'],
urls: ['/', '/badge', '/popover', '/stack', '/text'],
},
});

Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
"@glimmer"
],
"knownFirstparty": [
"backpack-ember",
"dummy"
]
}
Expand All @@ -182,7 +183,10 @@
"@ember",
"@glimmer"
],
"knownFirstparty": []
"knownFirstparty": [
"backpack-ember",
"dummy"
]
}
}
}
Expand Down
1 change: 1 addition & 0 deletions tests/dummy/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ Router.map(function () {
this.route('stack');
this.route('text');
this.route('badge');
this.route('popover');
this.route('announcement');
});
3 changes: 3 additions & 0 deletions tests/dummy/app/templates/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
<h3>
<LinkTo @route="badge">Badge</LinkTo>
</h3>
<h3>
<LinkTo @route="popover">Popover</LinkTo>
</h3>
</div>
</Doc.Item>
<Doc.Item>
Expand Down
21 changes: 21 additions & 0 deletions tests/dummy/app/templates/popover.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{{page-title "Popover"}}

<Stack @spaceClass="space-y-8" as |Doc|>
<Doc.Item>
<Heading>Popover</Heading>
</Doc.Item>
<Doc.Item>
<Popover as |popover|>
<popover.Trigger
class="text-white group bg-orange-700 px-3 py-2 rounded-md inline-flex items-center text-base font-medium hover:text-opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
>Open popover</popover.Trigger>
<popover.Panel
class="absolute z-10 w-screen max-w-sm px-4 mt-3 transform -translate-x-1/2 left-1/2 sm:px-0 lg:max-w-3xl"
>
<div
class="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5"
>Welcome to popover country.</div>
</popover.Panel>
</Popover>
</Doc.Item>
</Stack>
25 changes: 25 additions & 0 deletions tests/integration/components/popover/component-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { render } from '@ember/test-helpers';

import { setupRenderingTest } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import { module, test } from 'qunit';

module('Integration | Component | popover', function(hooks) {
setupRenderingTest(hooks);

test('it renders', async function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });

await render(hbs`
<Popover as |popover|>
<popover.Trigger>Trigger</popover.Trigger>
<popover.Panel>
template block text
</popover.Panel>
</Popover>
`);

assert.equal(this.element.textContent?.trim(), 'template block text');
});
});
21 changes: 21 additions & 0 deletions tests/integration/components/popover/panel/component-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';

module('Integration | Component | panel', function(hooks) {
setupRenderingTest(hooks);

test('it renders', async function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });

await render(hbs`
<Popover::Panel>
template block text
</Popover::Panel>
`);

assert.equal(this.element.textContent?.trim(), 'template block text');
});
});
21 changes: 21 additions & 0 deletions tests/integration/components/popover/trigger/component-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';

module('Integration | Component | trigger', function(hooks) {
setupRenderingTest(hooks);

test('it renders', async function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });

await render(hbs`
<Popover::Trigger>
template block text
</Popover::Trigger>
`);

assert.equal(this.element.textContent?.trim(), 'template block text');
});
});
26 changes: 26 additions & 0 deletions tests/unit/utils/normalize-keycode-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { module, test } from 'qunit';

import { normalizeKey } from 'dummy/utils/normalize-keycode';

module('Unit | Utility | normalize-keycode', function () {
test('it normalizes arrow keys', function (assert) {
const mockEvent = { keyCode: 38, key: 'Up' };
const keypress = normalizeKey(mockEvent);

assert.strictEqual(keypress, 'ArrowUp');
});

test('it normalizes escape keys', (assert) => {
const mockEvent = { keyCode: 27, key: 'Esc' };
const keypress = normalizeKey(mockEvent);

assert.strictEqual(keypress, 'Escape');
});

test('it does not normalize other keys', (assert) => {
const mockEvent = { keyCode: 35, key: 'End' };
const keypress = normalizeKey(mockEvent);

assert.strictEqual(keypress, 'End');
});
});