Skip to content

Commit df3e5e3

Browse files
added FaIconStack component, styling, and test support
1 parent 2898ade commit df3e5e3

File tree

6 files changed

+321
-0
lines changed

6 files changed

+321
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { collection, create } from 'ember-cli-page-object';
2+
import faIcon from 'ilios-common/page-objects/components/fa-icon';
3+
4+
const definition = {
5+
scope: '[data-test-awesome-icon-stack]',
6+
icons: collection('[data-test-awesome-icon]', faIcon),
7+
};
8+
9+
export default definition;
10+
export const component = create(definition);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import Component from '@glimmer/component';
2+
import or from 'ember-truth-helpers/helpers/or';
3+
import FaIcon from 'ilios-common/components/fa-icon';
4+
5+
export default class FaIconStackComponent extends Component {
6+
get titleId() {
7+
if (!this.args.title) {
8+
return null;
9+
}
10+
11+
return `inline-title-${this.uniqueId}`;
12+
}
13+
get iconClasses() {
14+
return this.args.icons.join('_');
15+
}
16+
<template>
17+
<span
18+
class="fa-layers fa-fw awesome-icon-stack {{this.iconClasses}}"
19+
data-test-awesome-icon-stack
20+
...attributes
21+
>
22+
{{#each @icons as |icon|}}
23+
<FaIcon
24+
@icon={{icon}}
25+
@extraClasses={{@extraClasses}}
26+
@fixedWidth={{@fixedWidth}}
27+
@focusable={{@focusable}}
28+
@flip={{@flip}}
29+
@listItem={{@listItem}}
30+
@spin={{@spin}}
31+
@title={{or @title null}}
32+
height={{@height}}
33+
width={{@width}}
34+
role={{or @role "img"}}
35+
x={{@x}}
36+
y={{@y}}
37+
/>
38+
{{/each}}
39+
</span>
40+
</template>
41+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from 'ilios-common/components/fa-icon-stack';

packages/ilios-common/app/styles/ilios-common/components.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@forward "components/api-version-notice";
22
@forward "components/awesome-icon";
3+
@forward "components/awesome-icon-stack";
34
@forward "components/back-link";
45
@forward "components/body";
56
@forward "components/breadcrumbs";
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
.fa-layers {
2+
display: inline-block;
3+
height: 1em;
4+
position: relative;
5+
text-align: center;
6+
vertical-align: -0.125em;
7+
width: 1em;
8+
9+
svg {
10+
bottom: 0;
11+
left: 0;
12+
margin: auto;
13+
position: absolute;
14+
right: 0;
15+
top: 0;
16+
}
17+
}
18+
19+
.fa-fw {
20+
text-align: center;
21+
width: 1.25em;
22+
}
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import { module, test } from 'qunit';
2+
import { setupRenderingTest } from 'test-app/tests/helpers';
3+
import { render } from '@ember/test-helpers';
4+
import { htmlSafe } from '@ember/template';
5+
import { component } from 'ilios-common/page-objects/components/fa-icon-stack';
6+
import FaIconStack from 'ilios-common/components/fa-icon-stack';
7+
8+
module('Integration | Component | fa-icon-stack', function (hooks) {
9+
setupRenderingTest(hooks);
10+
11+
hooks.beforeEach(function () {
12+
this.set('icons', ['circle-check', 'slash']);
13+
});
14+
15+
test('it renders multiple layered icons', async function (assert) {
16+
await render(<template><FaIconStack @icons={{this.icons}} /></template>);
17+
18+
assert.strictEqual(component.icons.length, 2);
19+
assert.strictEqual(component.icons[0].type, 'circle-check');
20+
assert.ok(component.icons[0].innerUse);
21+
assert.ok(component.icons[0].innerUse.href, '/fontawesome/solid.svg#circle-check');
22+
assert.strictEqual(component.icons[1].type, 'slash');
23+
assert.ok(component.icons[1].innerUse);
24+
assert.ok(component.icons[1].innerUse.href, '/fontawesome/solid.svg#slash');
25+
});
26+
27+
test('it renders extra classes', async function (assert) {
28+
this.set('class', 'foo-xyz');
29+
await render(
30+
<template><FaIconStack @icons={{this.icons}} @extraClasses={{this.class}} /></template>,
31+
);
32+
assert.strictEqual(component.icons[0].classes, 'awesome-icon fa-circle-check foo-xyz');
33+
assert.strictEqual(component.icons[1].classes, 'awesome-icon fa-slash foo-xyz');
34+
this.set('class', 'foo-new-class');
35+
assert.strictEqual(component.icons[0].classes, 'awesome-icon fa-circle-check foo-new-class');
36+
assert.strictEqual(component.icons[1].classes, 'awesome-icon fa-slash foo-new-class');
37+
});
38+
39+
test('it optionally renders spin class', async function (assert) {
40+
this.set('isSpinning', false);
41+
await render(
42+
<template><FaIconStack @icons={{this.icons}} @spin={{this.isSpinning}} /></template>,
43+
);
44+
assert.strictEqual(component.icons[0].classes, 'awesome-icon fa-circle-check');
45+
assert.strictEqual(component.icons[1].classes, 'awesome-icon fa-slash');
46+
this.set('isSpinning', true);
47+
assert.strictEqual(component.icons[0].classes, 'awesome-icon fa-circle-check spin');
48+
assert.strictEqual(component.icons[1].classes, 'awesome-icon fa-slash spin');
49+
});
50+
51+
test('it optionally renders fixed-width class', async function (assert) {
52+
this.set('fixedWidth', false);
53+
await render(
54+
<template><FaIconStack @icons={{this.icons}} @fixedWidth={{this.fixedWidth}} /></template>,
55+
);
56+
assert.strictEqual(component.icons[0].classes, 'awesome-icon fa-circle-check');
57+
assert.strictEqual(component.icons[1].classes, 'awesome-icon fa-slash');
58+
this.set('fixedWidth', true);
59+
assert.strictEqual(component.icons[0].classes, 'awesome-icon fa-circle-check fixed-width');
60+
assert.strictEqual(component.icons[1].classes, 'awesome-icon fa-slash fixed-width');
61+
});
62+
63+
test('it renders vertically and horizontally flipped', async function (assert) {
64+
this.set('flip', '');
65+
await render(<template><FaIconStack @icons={{this.icons}} @flip={{this.flip}} /></template>);
66+
assert.dom('svg').doesNotHaveClass('flip-horizontal');
67+
assert.dom('svg').doesNotHaveClass('flip-vertical');
68+
this.set('flip', 'horizontal');
69+
assert.dom('svg').hasClass('flip-horizontal');
70+
assert.dom('svg').doesNotHaveClass('flip-vertical');
71+
this.set('flip', 'vertical');
72+
assert.dom('svg').doesNotHaveClass('flip-horizontal');
73+
assert.dom('svg').hasClass('flip-vertical');
74+
this.set('flip', 'both');
75+
assert.dom('svg').hasClass('flip-horizontal');
76+
assert.dom('svg').hasClass('flip-vertical');
77+
});
78+
79+
test('it binds title', async function (assert) {
80+
const title = 'awesome is as awesome does';
81+
this.set('title', title);
82+
await render(<template><FaIconStack @icons={{this.icons}} @title={{this.title}} /></template>);
83+
assert.ok(component.icons[0].title, 'first icon layer has title element');
84+
assert.strictEqual(component.icons[0].title.text, title, 'first icon layer has correct title');
85+
assert.ok(component.icons[1].title, 'second icon layer has title element');
86+
assert.strictEqual(component.icons[1].title.text, title, 'second icon layer has correct title');
87+
88+
assert.ok(component.icons[0].ariaLabelledBy);
89+
assert.ok(component.icons[1].ariaLabelledBy);
90+
});
91+
92+
test('no title attribute gives no icon title element', async function (assert) {
93+
await render(<template><FaIconStack @icons={{this.icons}} /></template>);
94+
assert.notOk(component.icons[0].title.exists, 'first icon layer has no title');
95+
assert.notOk(component.icons[1].title.exists, 'second icon layer has no title');
96+
});
97+
98+
test('title from string like object', async function (assert) {
99+
const title = 'awesome is as awesome does';
100+
this.set('title', htmlSafe(title));
101+
await render(<template><FaIconStack @icons={{this.icons}} @title={{this.title}} /></template>);
102+
assert.ok(component.icons[0].title, 'first icon layer has title element');
103+
assert.strictEqual(component.icons[0].title.text, title, 'first icon layer has correct title');
104+
assert.ok(component.icons[1].title, 'second icon layer has title element');
105+
assert.strictEqual(component.icons[1].title.text, title, 'second icon layer has correct title');
106+
});
107+
108+
test('it renders with the default focusable attributes as false', async function (assert) {
109+
await render(<template><FaIconStack @icons={{this.icons}} /></template>);
110+
assert.strictEqual(component.icons[0].focusable, 'false');
111+
assert.strictEqual(component.icons[1].focusable, 'false');
112+
});
113+
114+
test('it should change the focusable attributes to true', async function (assert) {
115+
this.set('title', 'awesome title of awesomeness');
116+
await render(<template><FaIconStack @icons={{this.icons}} @title={{this.title}} /></template>);
117+
assert.strictEqual(component.icons[0].focusable, 'true');
118+
assert.strictEqual(component.icons[1].focusable, 'true');
119+
});
120+
121+
test('it defaults to ariaHidden', async function (assert) {
122+
await render(<template><FaIconStack @icons={{this.icons}} /></template>);
123+
assert.dom('svg').hasAttribute('aria-hidden', 'true');
124+
});
125+
126+
test('it binds ariaHidden', async function (assert) {
127+
this.set('title', 'awesome title of awesomeness');
128+
await render(<template><FaIconStack @icons={{this.icons}} @title={{this.title}} /></template>);
129+
assert.strictEqual(component.icons[0].ariaHidden, 'false');
130+
assert.strictEqual(component.icons[1].ariaHidden, 'false');
131+
this.set('title', null);
132+
assert.strictEqual(component.icons[0].ariaHidden, 'true');
133+
assert.strictEqual(component.icons[1].ariaHidden, 'true');
134+
});
135+
136+
test('role defaults to img', async function (assert) {
137+
await render(<template><FaIconStack @icons={{this.icons}} /></template>);
138+
assert.strictEqual(component.icons[0].role, 'img');
139+
assert.strictEqual(component.icons[1].role, 'img');
140+
});
141+
142+
test('it binds role', async function (assert) {
143+
this.set('role', 'img');
144+
await render(<template><FaIconStack @icons={{this.icons}} @role={{this.role}} /></template>);
145+
assert.strictEqual(component.icons[0].role, 'img', 'first icon has correct img role');
146+
assert.strictEqual(component.icons[1].role, 'img', 'second icon has correct img role');
147+
this.set('role', 'presentation');
148+
assert.strictEqual(
149+
component.icons[0].role,
150+
'presentation',
151+
'first icon has correct presentation role',
152+
);
153+
assert.strictEqual(
154+
component.icons[1].role,
155+
'presentation',
156+
'first icon has correct presentation role',
157+
);
158+
this.set('role', false);
159+
assert.strictEqual(component.icons[0].role, 'img', 'first icon has default img role');
160+
assert.strictEqual(component.icons[1].role, 'img', 'second icon has default img role');
161+
});
162+
163+
test('it binds attributes', async function (assert) {
164+
this.set('height', '5px');
165+
this.set('width', '6px');
166+
this.set('x', '19');
167+
this.set('y', '81');
168+
169+
await render(
170+
<template>
171+
<FaIconStack
172+
@icons={{this.icons}}
173+
@height={{this.height}}
174+
@width={{this.width}}
175+
@x={{this.x}}
176+
@y={{this.y}}
177+
/>
178+
</template>,
179+
);
180+
181+
assert.strictEqual(
182+
component.icons[0].height,
183+
'5px',
184+
'first icon layer has correct height attribute',
185+
);
186+
assert.strictEqual(
187+
component.icons[1].height,
188+
'5px',
189+
'second icon layer has correct height attribute',
190+
);
191+
assert.strictEqual(
192+
component.icons[0].width,
193+
'6px',
194+
'first icon layer has correct width attribute',
195+
);
196+
assert.strictEqual(
197+
component.icons[1].width,
198+
'6px',
199+
'second icon layer has correct width attribute',
200+
);
201+
assert.strictEqual(component.icons[0].x, '19', 'first icon layer has correct x attribute');
202+
assert.strictEqual(component.icons[1].x, '19', 'second icon layer has correct x attribute');
203+
assert.strictEqual(component.icons[0].y, '81', 'first icon layer has correct y attribute');
204+
assert.strictEqual(component.icons[1].y, '81', 'second icon layer has correct y attribute');
205+
this.set('height', '10rem');
206+
this.set('width', '10rem');
207+
this.set('x', '2');
208+
this.set('y', '2');
209+
assert.strictEqual(
210+
component.icons[0].height,
211+
'10rem',
212+
'first icon layer has correct new height attribute',
213+
);
214+
assert.strictEqual(
215+
component.icons[1].height,
216+
'10rem',
217+
'second icon layer has correct new height attribute',
218+
);
219+
assert.strictEqual(
220+
component.icons[0].width,
221+
'10rem',
222+
'first icon layer has correct new width attribute',
223+
);
224+
assert.strictEqual(
225+
component.icons[1].width,
226+
'10rem',
227+
'second icon layer has correct new width attribute',
228+
);
229+
assert.strictEqual(component.icons[0].x, '2', 'first icon layer has correct new x attribute');
230+
assert.strictEqual(component.icons[1].x, '2', 'second icon layer has correct new x attribute');
231+
assert.strictEqual(component.icons[0].y, '2', 'first icon layer has correct new y attribute');
232+
assert.strictEqual(component.icons[1].y, '2', 'second icon layer has correct new y attribute');
233+
});
234+
235+
test('it accepts listItem', async function (assert) {
236+
this.set('listItem', false);
237+
await render(
238+
<template><FaIconStack @icons={{this.icons}} @listItem={{this.listItem}} /></template>,
239+
);
240+
assert.strictEqual(component.icons[0].classes, 'awesome-icon fa-circle-check');
241+
assert.strictEqual(component.icons[1].classes, 'awesome-icon fa-slash');
242+
this.set('listItem', true);
243+
assert.strictEqual(component.icons[0].classes, 'awesome-icon fa-circle-check list-item');
244+
assert.strictEqual(component.icons[1].classes, 'awesome-icon fa-slash list-item');
245+
});
246+
});

0 commit comments

Comments
 (0)