@aquera/nile-elements 2.0.9 → 2.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/demo/app-shell.html +852 -0
- package/demo/nxtgen-classes.css +5 -5
- package/demo/nxtgen-dark.css +25 -25
- package/demo/nxtgen-properties.css +2 -2
- package/demo/nxtgen-utilities.css +5 -5
- package/demo/nxtgen.css +5 -5
- package/demo/utilities.css +2 -2
- package/demo/variables.css +2 -2
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.js +594 -308
- package/dist/internal/app-shell-utility-control-styles.cjs.js +2 -0
- package/dist/internal/app-shell-utility-control-styles.cjs.js.map +1 -0
- package/dist/internal/app-shell-utility-control-styles.esm.js +16 -0
- package/dist/nile-app-shell/index.cjs.js +1 -1
- package/dist/nile-app-shell/index.esm.js +1 -1
- package/dist/nile-app-shell/nile-app-shell.cjs.js +1 -1
- package/dist/nile-app-shell/nile-app-shell.cjs.js.map +1 -1
- package/dist/nile-app-shell/nile-app-shell.css.cjs.js +1 -1
- package/dist/nile-app-shell/nile-app-shell.css.cjs.js.map +1 -1
- package/dist/nile-app-shell/nile-app-shell.css.esm.js +270 -92
- package/dist/nile-app-shell/nile-app-shell.esm.js +61 -10
- package/dist/nile-app-shell/shell-controller.cjs.js.map +1 -1
- package/dist/nile-app-shell-main/nile-app-shell-main.cjs.js.map +1 -1
- package/dist/nile-app-shell-nav-toggle/index.cjs.js +1 -1
- package/dist/nile-app-shell-nav-toggle/index.esm.js +1 -1
- package/dist/nile-app-shell-nav-toggle/nile-app-shell-nav-toggle.cjs.js +1 -1
- package/dist/nile-app-shell-nav-toggle/nile-app-shell-nav-toggle.cjs.js.map +1 -1
- package/dist/nile-app-shell-nav-toggle/nile-app-shell-nav-toggle.css.cjs.js +1 -1
- package/dist/nile-app-shell-nav-toggle/nile-app-shell-nav-toggle.css.cjs.js.map +1 -1
- package/dist/nile-app-shell-nav-toggle/nile-app-shell-nav-toggle.css.esm.js +7 -3
- package/dist/nile-app-shell-nav-toggle/nile-app-shell-nav-toggle.esm.js +3 -3
- package/dist/nile-app-shell-panel/index.cjs.js +1 -1
- package/dist/nile-app-shell-panel/index.esm.js +1 -1
- package/dist/nile-app-shell-panel/nile-app-shell-panel.cjs.js +1 -1
- package/dist/nile-app-shell-panel/nile-app-shell-panel.cjs.js.map +1 -1
- package/dist/nile-app-shell-panel/nile-app-shell-panel.css.cjs.js +1 -1
- package/dist/nile-app-shell-panel/nile-app-shell-panel.css.cjs.js.map +1 -1
- package/dist/nile-app-shell-panel/nile-app-shell-panel.css.esm.js +29 -7
- package/dist/nile-app-shell-panel/nile-app-shell-panel.esm.js +18 -5
- package/dist/nile-app-shell-side-nav/nile-app-shell-side-nav.cjs.js.map +1 -1
- package/dist/nile-app-shell-side-nav/nile-app-shell-side-nav.css.cjs.js.map +1 -1
- package/dist/nile-badge/nile-badge.css.cjs.js +1 -1
- package/dist/nile-badge/nile-badge.css.cjs.js.map +1 -1
- package/dist/nile-badge/nile-badge.css.esm.js +2 -2
- package/dist/nile-button-toggle/nile-button-toggle.css.cjs.js +1 -1
- package/dist/nile-button-toggle/nile-button-toggle.css.cjs.js.map +1 -1
- package/dist/nile-button-toggle/nile-button-toggle.css.esm.js +2 -2
- package/dist/nile-code-editor/nile-code-editor.cjs.js +1 -1
- package/dist/nile-code-editor/nile-code-editor.cjs.js.map +1 -1
- package/dist/nile-code-editor/nile-code-editor.esm.js +1 -1
- package/dist/nile-menu-item/nile-menu-item.css.cjs.js +1 -1
- package/dist/nile-menu-item/nile-menu-item.css.cjs.js.map +1 -1
- package/dist/nile-menu-item/nile-menu-item.css.esm.js +1 -1
- package/dist/nile-rich-text-editor/nile-rich-text-editor.css.cjs.js +1 -1
- package/dist/nile-rich-text-editor/nile-rich-text-editor.css.cjs.js.map +1 -1
- package/dist/nile-rich-text-editor/nile-rich-text-editor.css.esm.js +5 -2
- package/dist/nile-toast/nile-toast.css.cjs.js +1 -1
- package/dist/nile-toast/nile-toast.css.cjs.js.map +1 -1
- package/dist/nile-toast/nile-toast.css.esm.js +5 -5
- package/dist/src/internal/app-shell-utility-control-styles.d.ts +12 -0
- package/dist/src/internal/app-shell-utility-control-styles.js +29 -0
- package/dist/src/internal/app-shell-utility-control-styles.js.map +1 -0
- package/dist/src/nile-app-shell/nile-app-shell-regions.test.d.ts +3 -0
- package/dist/src/nile-app-shell/nile-app-shell-regions.test.js +206 -0
- package/dist/src/nile-app-shell/nile-app-shell-regions.test.js.map +1 -1
- package/dist/src/nile-app-shell/nile-app-shell.css.d.ts +1 -9
- package/dist/src/nile-app-shell/nile-app-shell.css.js +270 -99
- package/dist/src/nile-app-shell/nile-app-shell.css.js.map +1 -1
- package/dist/src/nile-app-shell/nile-app-shell.d.ts +90 -122
- package/dist/src/nile-app-shell/nile-app-shell.js +224 -143
- package/dist/src/nile-app-shell/nile-app-shell.js.map +1 -1
- package/dist/src/nile-app-shell/nile-app-shell.test.d.ts +3 -0
- package/dist/src/nile-app-shell/nile-app-shell.test.js +898 -29
- package/dist/src/nile-app-shell/nile-app-shell.test.js.map +1 -1
- package/dist/src/nile-app-shell/shell-controller.d.ts +1 -8
- package/dist/src/nile-app-shell/shell-controller.js +2 -14
- package/dist/src/nile-app-shell/shell-controller.js.map +1 -1
- package/dist/src/nile-app-shell-main/nile-app-shell-main.d.ts +2 -6
- package/dist/src/nile-app-shell-main/nile-app-shell-main.js +2 -6
- package/dist/src/nile-app-shell-main/nile-app-shell-main.js.map +1 -1
- package/dist/src/nile-app-shell-nav-toggle/nile-app-shell-nav-toggle.css.d.ts +1 -4
- package/dist/src/nile-app-shell-nav-toggle/nile-app-shell-nav-toggle.css.js +7 -5
- package/dist/src/nile-app-shell-nav-toggle/nile-app-shell-nav-toggle.css.js.map +1 -1
- package/dist/src/nile-app-shell-nav-toggle/nile-app-shell-nav-toggle.d.ts +4 -8
- package/dist/src/nile-app-shell-nav-toggle/nile-app-shell-nav-toggle.js +5 -6
- package/dist/src/nile-app-shell-nav-toggle/nile-app-shell-nav-toggle.js.map +1 -1
- package/dist/src/nile-app-shell-panel/nile-app-shell-panel.css.js +28 -5
- package/dist/src/nile-app-shell-panel/nile-app-shell-panel.css.js.map +1 -1
- package/dist/src/nile-app-shell-panel/nile-app-shell-panel.d.ts +18 -13
- package/dist/src/nile-app-shell-panel/nile-app-shell-panel.js +45 -16
- package/dist/src/nile-app-shell-panel/nile-app-shell-panel.js.map +1 -1
- package/dist/src/nile-app-shell-side-nav/nile-app-shell-side-nav.css.d.ts +1 -4
- package/dist/src/nile-app-shell-side-nav/nile-app-shell-side-nav.css.js +1 -4
- package/dist/src/nile-app-shell-side-nav/nile-app-shell-side-nav.css.js.map +1 -1
- package/dist/src/nile-app-shell-side-nav/nile-app-shell-side-nav.d.ts +4 -12
- package/dist/src/nile-app-shell-side-nav/nile-app-shell-side-nav.js +4 -12
- package/dist/src/nile-app-shell-side-nav/nile-app-shell-side-nav.js.map +1 -1
- package/dist/src/nile-badge/nile-badge.css.js +2 -2
- package/dist/src/nile-badge/nile-badge.css.js.map +1 -1
- package/dist/src/nile-button-toggle/nile-button-toggle.css.js +2 -2
- package/dist/src/nile-button-toggle/nile-button-toggle.css.js.map +1 -1
- package/dist/src/nile-code-editor/nile-code-editor.d.ts +1 -1
- package/dist/src/nile-code-editor/nile-code-editor.js +2 -2
- package/dist/src/nile-code-editor/nile-code-editor.js.map +1 -1
- package/dist/src/nile-menu-item/nile-menu-item.css.js +1 -1
- package/dist/src/nile-menu-item/nile-menu-item.css.js.map +1 -1
- package/dist/src/nile-rich-text-editor/nile-rich-text-editor.css.js +5 -2
- package/dist/src/nile-rich-text-editor/nile-rich-text-editor.css.js.map +1 -1
- package/dist/src/nile-toast/nile-toast.css.js +5 -5
- package/dist/src/nile-toast/nile-toast.css.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.cjs.js +1 -1
- package/dist/version.cjs.js.map +1 -1
- package/dist/version.esm.js +1 -1
- package/package.json +1 -1
- package/plop-templates/lit/lit.test.ts.hbs +22 -0
- package/plop-templates/lit/lit.ts.hbs +10 -5
- package/plopfile.js +12 -0
- package/src/internal/app-shell-utility-control-styles.ts +30 -0
- package/src/nile-app-shell/nile-app-shell-regions.test.ts +248 -0
- package/src/nile-app-shell/nile-app-shell.css.ts +270 -99
- package/src/nile-app-shell/nile-app-shell.test.ts +1120 -29
- package/src/nile-app-shell/nile-app-shell.ts +241 -148
- package/src/nile-app-shell/shell-controller.ts +2 -14
- package/src/nile-app-shell-main/nile-app-shell-main.ts +2 -6
- package/src/nile-app-shell-nav-toggle/nile-app-shell-nav-toggle.css.ts +7 -5
- package/src/nile-app-shell-nav-toggle/nile-app-shell-nav-toggle.ts +7 -11
- package/src/nile-app-shell-panel/nile-app-shell-panel.css.ts +28 -5
- package/src/nile-app-shell-panel/nile-app-shell-panel.ts +44 -16
- package/src/nile-app-shell-side-nav/nile-app-shell-side-nav.css.ts +1 -4
- package/src/nile-app-shell-side-nav/nile-app-shell-side-nav.ts +4 -12
- package/src/nile-badge/nile-badge.css.ts +2 -2
- package/src/nile-button-toggle/nile-button-toggle.css.ts +2 -2
- package/src/nile-code-editor/nile-code-editor.ts +2 -2
- package/src/nile-menu-item/nile-menu-item.css.ts +1 -1
- package/src/nile-rich-text-editor/nile-rich-text-editor.css.ts +5 -2
- package/src/nile-toast/nile-toast.css.ts +5 -5
- package/vscode-html-custom-data.json +114 -37
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
|
2
|
+
import { setViewport } from '@web/test-runner-commands';
|
|
2
3
|
import './nile-app-shell';
|
|
3
4
|
import '../nile-app-shell-nav-toggle/nile-app-shell-nav-toggle';
|
|
5
|
+
import '../nile-nav-tab-group/nile-nav-tab-group';
|
|
6
|
+
import '../nile-nav-tab/nile-nav-tab';
|
|
4
7
|
import '../nile-side-bar/nile-side-bar';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*/
|
|
8
|
+
// renderGlyph fails silently on an unknown name, so the glyph tests need the real element.
|
|
9
|
+
import '@aquera/nile-glyph/nile-glyph';
|
|
10
|
+
// Sizes the host explicitly and waits for the ResizeObserver to land.
|
|
9
11
|
const sized = async (width) => {
|
|
10
12
|
const el = await fixture(html `
|
|
11
13
|
<nile-app-shell style="width: ${width}px; height: 600px;">
|
|
@@ -34,11 +36,7 @@ const overlaying = async (width) => {
|
|
|
34
36
|
await waitUntil(() => el.hasAttribute('has-panel'));
|
|
35
37
|
return el;
|
|
36
38
|
};
|
|
37
|
-
|
|
38
|
-
* The side-nav track animates, so a width read straight after a state change
|
|
39
|
-
* catches the row mid-transition and sees room that is about to go away. Waits
|
|
40
|
-
* for two consecutive frames to agree before geometry is asserted.
|
|
41
|
-
*/
|
|
39
|
+
// Waits for two consecutive frames to agree before geometry is asserted.
|
|
42
40
|
const settled = async (el) => {
|
|
43
41
|
const section = el.shadowRoot.querySelector('.content-section');
|
|
44
42
|
let last = -1;
|
|
@@ -85,6 +83,17 @@ describe('NileAppShell — regions', () => {
|
|
|
85
83
|
expect(part(el, 'container').contains(bar)).to.be.false;
|
|
86
84
|
});
|
|
87
85
|
});
|
|
86
|
+
/**
|
|
87
|
+
* The name the control's glyph asks for, once nile-glyph has resolved it to
|
|
88
|
+
* real path data. An unknown name leaves an empty <svg>, so this fails loudly
|
|
89
|
+
* rather than letting a missing icon ship as a blank box.
|
|
90
|
+
*/
|
|
91
|
+
const drawnGlyph = async (control) => {
|
|
92
|
+
const glyph = control.querySelector('nile-glyph');
|
|
93
|
+
expect(glyph, 'the control renders a nile-glyph').to.exist;
|
|
94
|
+
await waitUntil(() => (glyph.shadowRoot?.querySelector('svg')?.childElementCount ?? 0) > 0, `${glyph.getAttribute('name')} draws no paths — is it in the nile-glyph set?`);
|
|
95
|
+
return glyph.getAttribute('name');
|
|
96
|
+
};
|
|
88
97
|
describe('NileAppShell — nav', () => {
|
|
89
98
|
it('6. collapseNav()/expandNav() drive the rail and reflect it', async () => {
|
|
90
99
|
const el = await sized(1440);
|
|
@@ -134,6 +143,13 @@ describe('NileAppShell — nav', () => {
|
|
|
134
143
|
expect(button.disabled).to.be.true;
|
|
135
144
|
expect(el.hasAttribute('rail')).to.be.true;
|
|
136
145
|
});
|
|
146
|
+
it('62. the built-in toggle draws a real glyph in both states', async () => {
|
|
147
|
+
const el = await sized(1440);
|
|
148
|
+
expect(await drawnGlyph(part(el, 'nav-toggle'))).to.equal('ng-panel-left');
|
|
149
|
+
el.collapseNav();
|
|
150
|
+
await el.updateComplete;
|
|
151
|
+
expect(await drawnGlyph(part(el, 'nav-toggle'))).to.equal('ng-panel-right');
|
|
152
|
+
});
|
|
137
153
|
});
|
|
138
154
|
describe('NileAppShell — Ask Aquera panel', () => {
|
|
139
155
|
it('11. openPanel()/closePanel() drive panel-open', async () => {
|
|
@@ -173,7 +189,7 @@ describe('NileAppShell — Ask Aquera panel', () => {
|
|
|
173
189
|
expect(el.getAttribute('panel-mode')).to.equal('docked');
|
|
174
190
|
const container = el.shadowRoot.querySelector('.container');
|
|
175
191
|
const panel = el.shadowRoot.querySelector('.panel');
|
|
176
|
-
//
|
|
192
|
+
// The page sits beside the panel, not under it.
|
|
177
193
|
expect(container.getBoundingClientRect().right).to.be.at.most(Math.round(panel.getBoundingClientRect().left) + 1);
|
|
178
194
|
}
|
|
179
195
|
});
|
|
@@ -263,8 +279,7 @@ describe('NileAppShellNavToggle', () => {
|
|
|
263
279
|
`);
|
|
264
280
|
const toggle = el.querySelector('nile-app-shell-nav-toggle');
|
|
265
281
|
await waitUntil(() => toggle.appShell === el);
|
|
266
|
-
// The control is a nile-button
|
|
267
|
-
// deeper. The part is the contract either way.
|
|
282
|
+
// The control is a nile-button; the part is the contract either way.
|
|
268
283
|
const button = () => toggle.shadowRoot.querySelector('[part~="button"]');
|
|
269
284
|
button().click();
|
|
270
285
|
await el.updateComplete;
|
|
@@ -290,13 +305,27 @@ describe('NileAppShellNavToggle', () => {
|
|
|
290
305
|
await toggle.updateComplete;
|
|
291
306
|
expect(toggle.shadowRoot.querySelector('[part~="button"]').disabled).to.be.true;
|
|
292
307
|
});
|
|
308
|
+
it('63. draws a real glyph in both states', async () => {
|
|
309
|
+
const el = await fixture(html `
|
|
310
|
+
<nile-app-shell style="width: 1440px; height: 600px;" no-nav-toggle>
|
|
311
|
+
<nav slot="side-nav">nav</nav>
|
|
312
|
+
<nile-app-shell-nav-toggle slot="top-bar"></nile-app-shell-nav-toggle>
|
|
313
|
+
<p>page</p>
|
|
314
|
+
</nile-app-shell>
|
|
315
|
+
`);
|
|
316
|
+
const toggle = el.querySelector('nile-app-shell-nav-toggle');
|
|
317
|
+
await waitUntil(() => toggle.appShell === el);
|
|
318
|
+
const button = () => toggle.shadowRoot.querySelector('[part~="button"]');
|
|
319
|
+
expect(await drawnGlyph(button())).to.equal('ng-panel-left');
|
|
320
|
+
button().click();
|
|
321
|
+
await el.updateComplete;
|
|
322
|
+
await toggle.updateComplete;
|
|
323
|
+
expect(await drawnGlyph(button())).to.equal('ng-panel-right');
|
|
324
|
+
});
|
|
293
325
|
});
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
* not give is the row overflowing into :host's clip, which silently cuts the
|
|
298
|
-
* panel off the right edge.
|
|
299
|
-
*/
|
|
326
|
+
// The nav track is theme-dependent, so tests load one token pair or the other explicitly.
|
|
327
|
+
const ENTERPRISE_NAV = '--nile-width-240px: 240px; --nile-width-70px: 70px;';
|
|
328
|
+
const NXTGEN_NAV = '--ng-spacing-296: 296px; --ng-spacing-68: 68px;';
|
|
300
329
|
describe('NileAppShell — the content row never overflows', () => {
|
|
301
330
|
const withAside = async (width) => {
|
|
302
331
|
const el = await fixture(html `
|
|
@@ -317,8 +346,7 @@ describe('NileAppShell — the content row never overflows', () => {
|
|
|
317
346
|
return (section.scrollWidth - Math.round(section.getBoundingClientRect().width));
|
|
318
347
|
};
|
|
319
348
|
it('23. tightens the side columns rather than covering the page', async () => {
|
|
320
|
-
//
|
|
321
|
-
// some width back — and the page still sits beside the panel, not under.
|
|
349
|
+
// Short of what the row prefers, so the panel and aside give width back while the page stays beside the panel.
|
|
322
350
|
const el = await withAside(1500);
|
|
323
351
|
await waitUntil(() => el.navLocked);
|
|
324
352
|
await settled(el);
|
|
@@ -367,11 +395,77 @@ describe('NileAppShell — the content row never overflows', () => {
|
|
|
367
395
|
expect(container.getBoundingClientRect().width).to.be.at.least(640);
|
|
368
396
|
expect(overflow(el)).to.equal(0);
|
|
369
397
|
});
|
|
398
|
+
// Pins the stylesheet defaults to the JS geometry fallbacks so the two copies cannot drift apart.
|
|
399
|
+
it('71. the stylesheet defaults still match the JS geometry fallbacks', async () => {
|
|
400
|
+
const el = await fixture(html `
|
|
401
|
+
<nile-app-shell
|
|
402
|
+
style="width: 1440px; height: 600px; --nile-spacing-xs: 4px;"
|
|
403
|
+
>
|
|
404
|
+
<nav slot="side-nav">nav</nav>
|
|
405
|
+
<p>page</p>
|
|
406
|
+
</nile-app-shell>
|
|
407
|
+
`);
|
|
408
|
+
await el.updateComplete;
|
|
409
|
+
const value = (name) => parseFloat(getComputedStyle(el).getPropertyValue(name));
|
|
410
|
+
// No theme sheet here, so the nav track falls to its own enterprise-pair literals, which is what _geometry() carries too.
|
|
411
|
+
expect(value('--nile-app-shell-side-nav-width')).to.equal(240);
|
|
412
|
+
expect(value('--nile-app-shell-side-nav-rail-width')).to.equal(70);
|
|
413
|
+
expect(value('--nile-app-shell-gutter')).to.equal(4);
|
|
414
|
+
expect(value('--nile-app-shell-content-min-width')).to.equal(640);
|
|
415
|
+
expect(value('--nile-app-shell-aside-width')).to.equal(320);
|
|
416
|
+
expect(value('--nile-app-shell-panel-width')).to.equal(480);
|
|
417
|
+
});
|
|
418
|
+
// The nav track mirrors nile-side-bar's token expressions and resolves per theme.
|
|
419
|
+
it('78. resolves the nav track per theme, as nile-side-bar does', async () => {
|
|
420
|
+
const track = async (tokens) => {
|
|
421
|
+
const el = await fixture(html `
|
|
422
|
+
<nile-app-shell style="width: 1440px; height: 600px; ${tokens}">
|
|
423
|
+
<nav slot="side-nav">nav</nav>
|
|
424
|
+
<p>page</p>
|
|
425
|
+
</nile-app-shell>
|
|
426
|
+
`);
|
|
427
|
+
await el.updateComplete;
|
|
428
|
+
const read = (name) => parseFloat(getComputedStyle(el).getPropertyValue(name));
|
|
429
|
+
return [
|
|
430
|
+
read('--nile-app-shell-side-nav-width'),
|
|
431
|
+
read('--nile-app-shell-side-nav-rail-width'),
|
|
432
|
+
];
|
|
433
|
+
};
|
|
434
|
+
expect(await track('--nile-width-240px: 240px; --nile-width-70px: 70px;'), 'enterprise').to.deep.equal([240, 70]);
|
|
435
|
+
expect(await track('--ng-spacing-296: 296px; --ng-spacing-68: 68px;'), 'nxtgen').to.deep.equal([296, 68]);
|
|
436
|
+
});
|
|
437
|
+
// The nav rails at the row's own theme-dependent width requirement (enterprise 1368, nxtgen 1424), with docked-rail-breakpoint at 0 so the measured value governs.
|
|
438
|
+
it('72. rails the nav at the row’s own width requirement, not a gutter early', async () => {
|
|
439
|
+
const at = async (width, tokens = ENTERPRISE_NAV) => {
|
|
440
|
+
const el = await fixture(html `
|
|
441
|
+
<nile-app-shell
|
|
442
|
+
style="width: ${width}px; height: 600px; ${tokens}"
|
|
443
|
+
docked-rail-breakpoint="0"
|
|
444
|
+
>
|
|
445
|
+
<nav slot="side-nav">nav</nav>
|
|
446
|
+
<p>page</p>
|
|
447
|
+
<div slot="panel">assistant</div>
|
|
448
|
+
</nile-app-shell>
|
|
449
|
+
`);
|
|
450
|
+
await waitUntil(() => el.hasAttribute('has-panel'));
|
|
451
|
+
el.openPanel();
|
|
452
|
+
await el.updateComplete;
|
|
453
|
+
await settled(el);
|
|
454
|
+
return el.navLocked;
|
|
455
|
+
};
|
|
456
|
+
// Exactly the requirement: the row fits, so the nav keeps its width.
|
|
457
|
+
expect(await at(1368)).to.be.false;
|
|
458
|
+
// One pixel under: the track is what the row cannot afford.
|
|
459
|
+
expect(await at(1367)).to.be.true;
|
|
460
|
+
expect(await at(1370)).to.be.false;
|
|
461
|
+
// NxtGen's wider track moves the same boundary up by 56px, following whatever the side bar measures.
|
|
462
|
+
expect(await at(1424, NXTGEN_NAV)).to.be.false;
|
|
463
|
+
expect(await at(1423, NXTGEN_NAV)).to.be.true;
|
|
464
|
+
// And the enterprise boundary is comfortably inside NxtGen's requirement.
|
|
465
|
+
expect(await at(1368, NXTGEN_NAV)).to.be.true;
|
|
466
|
+
});
|
|
370
467
|
});
|
|
371
|
-
|
|
372
|
-
* An empty flex item is not a free one: it still takes the row's gap before
|
|
373
|
-
* it, which is enough to pull the bar's content short of its padding edge.
|
|
374
|
-
*/
|
|
468
|
+
// An empty flex item still takes the row's gap, pulling the bar's content short of its padding edge.
|
|
375
469
|
describe('NileAppShell — the top bar reaches its padding edge', () => {
|
|
376
470
|
it('28. an empty actions group reserves no gap', async () => {
|
|
377
471
|
const el = await sized(1440);
|
|
@@ -400,12 +494,43 @@ describe('NileAppShell — the top bar reaches its padding edge', () => {
|
|
|
400
494
|
expect(getComputedStyle(actions).display).to.not.equal('none');
|
|
401
495
|
expect(actions.getBoundingClientRect().width).to.be.greaterThan(0);
|
|
402
496
|
});
|
|
497
|
+
// The rule that separates the nav toggle from the header's content follows both a filled slot and a title block.
|
|
498
|
+
it('76. draws the toggle rule for a title block, not just a filled slot', async () => {
|
|
499
|
+
const rule = (el) => part(el, 'rule');
|
|
500
|
+
const slotted = await fixture(html `
|
|
501
|
+
<nile-app-shell style="width: 1440px; height: 600px;">
|
|
502
|
+
<nav slot="side-nav">nav</nav>
|
|
503
|
+
<span slot="top-bar">Title</span>
|
|
504
|
+
<p>page</p>
|
|
505
|
+
</nile-app-shell>
|
|
506
|
+
`);
|
|
507
|
+
await waitUntil(() => slotted.hasAttribute('has-top-bar'));
|
|
508
|
+
expect(rule(slotted)).to.exist;
|
|
509
|
+
// The same heading through pageTitle, top-bar slot empty.
|
|
510
|
+
const titled = await fixture(html `
|
|
511
|
+
<nile-app-shell
|
|
512
|
+
style="width: 1440px; height: 600px;"
|
|
513
|
+
pageTitle="Applications"
|
|
514
|
+
>
|
|
515
|
+
<nav slot="side-nav">nav</nav>
|
|
516
|
+
<p>page</p>
|
|
517
|
+
</nile-app-shell>
|
|
518
|
+
`);
|
|
519
|
+
await titled.updateComplete;
|
|
520
|
+
expect(titled.hasAttribute('has-top-bar')).to.be.true;
|
|
521
|
+
expect(rule(titled), 'a title block earns the rule too').to.exist;
|
|
522
|
+
// Neither: the toggle stands alone, so there is nothing to separate.
|
|
523
|
+
const bare = await fixture(html `
|
|
524
|
+
<nile-app-shell style="width: 1440px; height: 600px;">
|
|
525
|
+
<nav slot="side-nav">nav</nav>
|
|
526
|
+
<p>page</p>
|
|
527
|
+
</nile-app-shell>
|
|
528
|
+
`);
|
|
529
|
+
await bare.updateComplete;
|
|
530
|
+
expect(rule(bare)).to.be.null;
|
|
531
|
+
});
|
|
403
532
|
});
|
|
404
|
-
|
|
405
|
-
* The Ask door moved onto the shell when nile-app-shell-header became a slot.
|
|
406
|
-
* It is the one control in the bar that cannot be slotted markup: it turns on
|
|
407
|
-
* whether a panel is mounted for this route, and it calls openPanel().
|
|
408
|
-
*/
|
|
533
|
+
// The built-in Ask button turns on whether a panel is mounted for this route and calls openPanel().
|
|
409
534
|
describe('NileAppShell — the built-in Ask button', () => {
|
|
410
535
|
const ask = (el) => part(el, 'ask');
|
|
411
536
|
it('30. opens the panel', async () => {
|
|
@@ -450,5 +575,749 @@ describe('NileAppShell — the built-in Ask button', () => {
|
|
|
450
575
|
expect(getComputedStyle(actions).display).to.not.equal('none');
|
|
451
576
|
expect(actions.getBoundingClientRect().width).to.be.greaterThan(0);
|
|
452
577
|
});
|
|
578
|
+
// The shell draws its icon controls as bare glyphs via ::part(base), overriding nile-button's circle and ghost surface.
|
|
579
|
+
const utilityBase = (el, name) => part(el, name).shadowRoot.querySelector('[part~="base"]');
|
|
580
|
+
it('35. draws its icon controls with no resting surface', async () => {
|
|
581
|
+
const el = await sized(1440);
|
|
582
|
+
el.openPanel();
|
|
583
|
+
await el.updateComplete;
|
|
584
|
+
for (const name of ['nav-toggle', 'panel-close']) {
|
|
585
|
+
const base = utilityBase(el, name);
|
|
586
|
+
expect(getComputedStyle(base).backgroundColor).to.equal('rgba(0, 0, 0, 0)');
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
it('36. sizes its icon controls to the utility box, not the button circle', async () => {
|
|
590
|
+
const el = await sized(1440);
|
|
591
|
+
el.openPanel();
|
|
592
|
+
await el.updateComplete;
|
|
593
|
+
for (const name of ['nav-toggle', 'panel-close']) {
|
|
594
|
+
const box = utilityBase(el, name).getBoundingClientRect();
|
|
595
|
+
expect(box.width).to.equal(32);
|
|
596
|
+
expect(box.height).to.equal(32);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
it('37. holds the panel back control until panelBack asks for it', async () => {
|
|
600
|
+
const el = await sized(1440);
|
|
601
|
+
el.openPanel();
|
|
602
|
+
await el.updateComplete;
|
|
603
|
+
const mark = el.shadowRoot.querySelector('.panel-mark');
|
|
604
|
+
// Not rendered at all in the root view, so no stylesheet override can surface it.
|
|
605
|
+
expect(part(el, 'panel-back')).to.not.exist;
|
|
606
|
+
expect(getComputedStyle(mark).display).to.not.equal('none');
|
|
607
|
+
el.panelBack = true;
|
|
608
|
+
await el.updateComplete;
|
|
609
|
+
const back = part(el, 'panel-back');
|
|
610
|
+
expect(back).to.exist;
|
|
611
|
+
expect(getComputedStyle(back).display).to.not.equal('none');
|
|
612
|
+
// The back control stands where the brand mark stands in the root view.
|
|
613
|
+
expect(getComputedStyle(mark).display).to.equal('none');
|
|
614
|
+
});
|
|
615
|
+
it('38. emits nile-panel-back and takes its label from panelBackLabel', async () => {
|
|
616
|
+
const el = await sized(1440);
|
|
617
|
+
el.panelBack = true;
|
|
618
|
+
el.panelBackLabel = 'Back to chats';
|
|
619
|
+
el.openPanel();
|
|
620
|
+
await el.updateComplete;
|
|
621
|
+
const back = part(el, 'panel-back');
|
|
622
|
+
expect(back.getAttribute('aria-label')).to.equal('Back to chats');
|
|
623
|
+
const went = oneEvent(el, 'nile-panel-back');
|
|
624
|
+
back.click();
|
|
625
|
+
await went;
|
|
626
|
+
// The shell keeps no view stack, so going back leaves the panel open.
|
|
627
|
+
expect(el.panelOpen).to.be.true;
|
|
628
|
+
});
|
|
629
|
+
it('81. goBackPanel() emits nile-panel-back, and goBack() stays its alias', async () => {
|
|
630
|
+
const el = await sized(1440);
|
|
631
|
+
const named = oneEvent(el, 'nile-panel-back');
|
|
632
|
+
el.goBackPanel();
|
|
633
|
+
expect(await named).to.exist;
|
|
634
|
+
// Deprecated, but still the same event: existing callers keep working.
|
|
635
|
+
const aliased = oneEvent(el, 'nile-panel-back');
|
|
636
|
+
el.goBack();
|
|
637
|
+
expect(await aliased).to.exist;
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
describe('NileAppShell — the tab strip', () => {
|
|
641
|
+
/** The strip is slot-driven, so it needs something in the slot to exist. */
|
|
642
|
+
const tabbed = async () => {
|
|
643
|
+
const el = await fixture(html `
|
|
644
|
+
<nile-app-shell style="width: 1440px; height: 600px; ${HEADER_TOKENS}">
|
|
645
|
+
<nav slot="side-nav">nav</nav>
|
|
646
|
+
<span slot="top-bar">Title</span>
|
|
647
|
+
<nile-nav-tab-group slot="tabs">
|
|
648
|
+
<nile-nav-tab slot="nav" panel="apps" active>Connected</nile-nav-tab>
|
|
649
|
+
<nile-nav-tab slot="nav" panel="approvals">Approvals</nile-nav-tab>
|
|
650
|
+
</nile-nav-tab-group>
|
|
651
|
+
<p>page</p>
|
|
652
|
+
</nile-app-shell>
|
|
653
|
+
`);
|
|
654
|
+
await waitUntil(() => el.hasAttribute('has-tabs'));
|
|
655
|
+
return el;
|
|
656
|
+
};
|
|
657
|
+
const strip = (el) => el.shadowRoot.querySelector('.tabs');
|
|
658
|
+
it('39. mounts the strip for a filled slot', async () => {
|
|
659
|
+
const el = await tabbed();
|
|
660
|
+
expect(el.tabsVisible).to.be.true;
|
|
661
|
+
expect(getComputedStyle(strip(el)).display).to.not.equal('none');
|
|
662
|
+
});
|
|
663
|
+
it('40. noTabs hides the strip without emptying the slot', async () => {
|
|
664
|
+
const el = await tabbed();
|
|
665
|
+
el.noTabs = true;
|
|
666
|
+
await el.updateComplete;
|
|
667
|
+
expect(el.tabsVisible).to.be.false;
|
|
668
|
+
expect(getComputedStyle(strip(el)).display).to.equal('none');
|
|
669
|
+
// The slot is still filled — the strip is switched off, not unmounted — so the assigned element proves the content stays.
|
|
670
|
+
expect(el.querySelector('nile-nav-tab-group')).to.exist;
|
|
671
|
+
const slot = strip(el).querySelector('slot');
|
|
672
|
+
expect(slot.assignedElements()).to.have.lengthOf(1);
|
|
673
|
+
el.noTabs = false;
|
|
674
|
+
await el.updateComplete;
|
|
675
|
+
expect(el.tabsVisible).to.be.true;
|
|
676
|
+
expect(getComputedStyle(strip(el)).display).to.not.equal('none');
|
|
677
|
+
});
|
|
678
|
+
// has-tabs advertises the visible strip, not the filled slot, matching has-panel and has-ask.
|
|
679
|
+
it('68. has-tabs follows the visible strip, not the filled slot', async () => {
|
|
680
|
+
// Slotted, not switched off: the strip is up and the flag is on.
|
|
681
|
+
const el = await tabbed();
|
|
682
|
+
expect(el.tabsVisible).to.be.true;
|
|
683
|
+
expect(el.hasAttribute('has-tabs')).to.be.true;
|
|
684
|
+
// Slotted, switched off: the slot stays filled, the flag goes with the strip.
|
|
685
|
+
el.noTabs = true;
|
|
686
|
+
await el.updateComplete;
|
|
687
|
+
expect(el.querySelector('nile-nav-tab-group')).to.exist;
|
|
688
|
+
expect(el.hasAttribute('has-tabs')).to.be.false;
|
|
689
|
+
expect(getComputedStyle(strip(el)).display).to.equal('none');
|
|
690
|
+
// And back, so the flag is not a one-way latch.
|
|
691
|
+
el.noTabs = false;
|
|
692
|
+
await el.updateComplete;
|
|
693
|
+
expect(el.hasAttribute('has-tabs')).to.be.true;
|
|
694
|
+
// Nothing slotted: no strip, no flag, whatever noTabs says.
|
|
695
|
+
const bare = await sized(1440);
|
|
696
|
+
await settled(bare);
|
|
697
|
+
expect(bare.hasAttribute('has-tabs')).to.be.false;
|
|
698
|
+
bare.noTabs = true;
|
|
699
|
+
await bare.updateComplete;
|
|
700
|
+
expect(bare.hasAttribute('has-tabs')).to.be.false;
|
|
701
|
+
});
|
|
702
|
+
it('41. leaves the strip off when the slot is empty', async () => {
|
|
703
|
+
const el = await sized(1440);
|
|
704
|
+
await settled(el);
|
|
705
|
+
expect(el.hasAttribute('has-tabs')).to.be.false;
|
|
706
|
+
expect(el.tabsVisible).to.be.false;
|
|
707
|
+
});
|
|
708
|
+
it('77. sizes bare slotted tab controls to their labels, not the strip', async () => {
|
|
709
|
+
const el = await fixture(html `
|
|
710
|
+
<nile-app-shell style="width: 1176px; height: 600px; ${HEADER_TOKENS}">
|
|
711
|
+
<nav slot="side-nav">nav</nav>
|
|
712
|
+
<span slot="top-bar">Title</span>
|
|
713
|
+
<button slot="tabs" id="short">A</button>
|
|
714
|
+
<button slot="tabs" id="long">A considerably longer tab label</button>
|
|
715
|
+
<p>page</p>
|
|
716
|
+
</nile-app-shell>
|
|
717
|
+
`);
|
|
718
|
+
await waitUntil(() => el.hasAttribute('has-tabs'));
|
|
719
|
+
await settled(el);
|
|
720
|
+
const short = el.querySelector('#short').getBoundingClientRect().width;
|
|
721
|
+
const long = el.querySelector('#long').getBoundingClientRect().width;
|
|
722
|
+
// Stretched, they would be equal; sized to content they are not close.
|
|
723
|
+
expect(long).to.be.greaterThan(short * 2);
|
|
724
|
+
});
|
|
725
|
+
it('43. leaves the slotted strip one rule, edge to edge', async () => {
|
|
726
|
+
const el = await tabbed();
|
|
727
|
+
const group = el.querySelector('nile-nav-tab-group');
|
|
728
|
+
const groupStyle = getComputedStyle(group);
|
|
729
|
+
expect(groupStyle.getPropertyValue('--track-width').trim()).to.equal('0px');
|
|
730
|
+
expect(groupStyle.alignSelf).to.equal('flex-end');
|
|
731
|
+
// The region's rule is as wide as the container it closes, padding and all.
|
|
732
|
+
const container = el.shadowRoot.querySelector('.container');
|
|
733
|
+
const stripWidth = strip(el).getBoundingClientRect().width;
|
|
734
|
+
const inner = container.getBoundingClientRect().width -
|
|
735
|
+
parseFloat(getComputedStyle(container).borderLeftWidth) -
|
|
736
|
+
parseFloat(getComputedStyle(container).borderRightWidth);
|
|
737
|
+
expect(Math.round(stripWidth)).to.equal(Math.round(inner));
|
|
738
|
+
// The group is wholly inside the region, and ends where the rule is.
|
|
739
|
+
const strippedBottom = strip(el).getBoundingClientRect().bottom;
|
|
740
|
+
const groupBottom = group.getBoundingClientRect().bottom;
|
|
741
|
+
expect(strippedBottom - groupBottom).to.be.lessThan(2);
|
|
742
|
+
});
|
|
743
|
+
// The Header Bar draws no rule of its own; the one divider belongs to the tab rail, so switching the strip off leaves the header with no rule.
|
|
744
|
+
it('42. draws the header rule on the strip only, never on the bar', async () => {
|
|
745
|
+
const el = await tabbed();
|
|
746
|
+
const bar = el.shadowRoot.querySelector('.container > .top-bar');
|
|
747
|
+
expect(parseFloat(getComputedStyle(bar).borderBottomWidth)).to.equal(0);
|
|
748
|
+
expect(parseFloat(getComputedStyle(strip(el)).borderBottomWidth)).to.be.greaterThan(0);
|
|
749
|
+
expect(getComputedStyle(strip(el)).borderBottomColor).to.not.equal('rgba(0, 0, 0, 0)');
|
|
750
|
+
// No tabs, no rule anywhere in the header block.
|
|
751
|
+
el.noTabs = true;
|
|
752
|
+
await el.updateComplete;
|
|
753
|
+
expect(parseFloat(getComputedStyle(bar).borderBottomWidth)).to.equal(0);
|
|
754
|
+
});
|
|
755
|
+
// The bar and the strip sit flush, with the stroke as the separation, not space.
|
|
756
|
+
it('62. leaves no vertical gap between the bar and the strip', async () => {
|
|
757
|
+
const el = await tabbed();
|
|
758
|
+
const bar = el.shadowRoot.querySelector('.container > .top-bar');
|
|
759
|
+
const gap = strip(el).getBoundingClientRect().top -
|
|
760
|
+
bar.getBoundingClientRect().bottom;
|
|
761
|
+
expect(Math.abs(gap)).to.be.lessThan(0.5);
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
// The test page loads no theme sheet, so the header fixtures carry the nxtgen token values the title block and tab row read.
|
|
765
|
+
const HEADER_TOKENS = [
|
|
766
|
+
'--ng-spacing-none: 0px',
|
|
767
|
+
'--ng-border-width-1: 1px',
|
|
768
|
+
'--ng-colors-border-secondary: #E9EAEB',
|
|
769
|
+
'--ng-spacing-xxs: 2px',
|
|
770
|
+
'--ng-spacing-xs: 4px',
|
|
771
|
+
'--ng-spacing-md: 8px',
|
|
772
|
+
'--ng-spacing-lg: 12px',
|
|
773
|
+
'--ng-spacing-xl: 16px',
|
|
774
|
+
'--ng-spacing-3xl: 24px',
|
|
775
|
+
'--ng-font-size-text-xs: 12px',
|
|
776
|
+
'--ng-line-height-text-xs: 18px',
|
|
777
|
+
'--ng-font-size-text-sm: 14px',
|
|
778
|
+
'--ng-line-height-text-sm: 20px',
|
|
779
|
+
'--ng-font-size-text-md: 16px',
|
|
780
|
+
'--ng-line-height-text-md: 24px',
|
|
781
|
+
].join('; ');
|
|
782
|
+
// A shell carrying whatever header axes a test asks for; an unset axis is the empty string, meaning "absent".
|
|
783
|
+
const header = async (axes = {}) => {
|
|
784
|
+
const el = await fixture(html `
|
|
785
|
+
<nile-app-shell
|
|
786
|
+
style="width: 1176px; height: 800px; ${HEADER_TOKENS}"
|
|
787
|
+
.pageTitle=${axes.pageTitle ?? ''}
|
|
788
|
+
.description=${axes.description ?? ''}
|
|
789
|
+
.badge=${axes.badge ?? ''}
|
|
790
|
+
leading=${axes.leading ?? 'none'}
|
|
791
|
+
.backLabel=${axes.backLabel ?? 'Back'}
|
|
792
|
+
>
|
|
793
|
+
<nav slot="side-nav">nav</nav>
|
|
794
|
+
${axes.leading === 'app-icon'
|
|
795
|
+
? html `<span slot="app-icon" id="mark">A</span>`
|
|
796
|
+
: ''}
|
|
797
|
+
${axes.tabs
|
|
798
|
+
? html `<nile-nav-tab-group slot="tabs">
|
|
799
|
+
<nile-nav-tab slot="nav" panel="apps" active>Connected</nile-nav-tab>
|
|
800
|
+
<nile-nav-tab slot="nav" panel="approvals">Approvals</nile-nav-tab>
|
|
801
|
+
</nile-nav-tab-group>`
|
|
802
|
+
: ''}
|
|
803
|
+
<p>page</p>
|
|
804
|
+
</nile-app-shell>
|
|
805
|
+
`);
|
|
806
|
+
if (axes.tabs)
|
|
807
|
+
await waitUntil(() => el.hasAttribute('has-tabs'));
|
|
808
|
+
await el.updateComplete;
|
|
809
|
+
return el;
|
|
810
|
+
};
|
|
811
|
+
// The header title block: the shell's own heading, the Leading axis, the badge and the description line.
|
|
812
|
+
describe('NileAppShell — the header title block', () => {
|
|
813
|
+
it('44. draws no title block at all until an axis asks for one', async () => {
|
|
814
|
+
const el = await header();
|
|
815
|
+
expect(el.leading).to.equal('none');
|
|
816
|
+
expect(part(el, 'title-area')).to.be.null;
|
|
817
|
+
expect(part(el, 'page-title')).to.be.null;
|
|
818
|
+
expect(part(el, 'description')).to.be.null;
|
|
819
|
+
expect(part(el, 'badge')).to.be.null;
|
|
820
|
+
expect(el.hasAttribute('has-description')).to.be.false;
|
|
821
|
+
});
|
|
822
|
+
// description and badge are modifiers on the page title, so neither mounts the title block by itself.
|
|
823
|
+
it('73. does not mount a title block for a description or badge alone', async () => {
|
|
824
|
+
for (const axes of [
|
|
825
|
+
{ description: 'Manage the apps connected to this tenant' },
|
|
826
|
+
{ badge: '94 apps' },
|
|
827
|
+
{ description: 'Manage the apps', badge: '94 apps' },
|
|
828
|
+
]) {
|
|
829
|
+
const el = await header(axes);
|
|
830
|
+
const label = JSON.stringify(axes);
|
|
831
|
+
expect(part(el, 'title-area'), label).to.be.null;
|
|
832
|
+
expect(part(el, 'description'), label).to.be.null;
|
|
833
|
+
expect(part(el, 'badge'), label).to.be.null;
|
|
834
|
+
// And no height is reserved for the line that cannot render.
|
|
835
|
+
expect(el.hasAttribute('has-description'), label).to.be.false;
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
it('74. brings both back as soon as there is a title to hang them on', async () => {
|
|
839
|
+
const el = await header({
|
|
840
|
+
description: 'Manage the apps connected to this tenant',
|
|
841
|
+
badge: '94 apps',
|
|
842
|
+
});
|
|
843
|
+
expect(part(el, 'title-area')).to.be.null;
|
|
844
|
+
el.pageTitle = 'Applications';
|
|
845
|
+
await el.updateComplete;
|
|
846
|
+
expect(part(el, 'title-area')).to.exist;
|
|
847
|
+
expect(part(el, 'description')).to.exist;
|
|
848
|
+
expect(part(el, 'badge')).to.exist;
|
|
849
|
+
expect(el.hasAttribute('has-description')).to.be.true;
|
|
850
|
+
});
|
|
851
|
+
// Leading is the exception: a back control stands on its own but brings no empty title stack.
|
|
852
|
+
it('75. mounts a leading control on its own, without an empty title stack', async () => {
|
|
853
|
+
const el = await header({ leading: 'back' });
|
|
854
|
+
expect(part(el, 'title-area')).to.exist;
|
|
855
|
+
expect(part(el, 'back')).to.exist;
|
|
856
|
+
expect(part(el, 'page-title')).to.be.null;
|
|
857
|
+
expect(el.shadowRoot.querySelector('.title-stack')).to.be.null;
|
|
858
|
+
expect(el.shadowRoot.querySelector('.title-row')).to.be.null;
|
|
859
|
+
});
|
|
860
|
+
it('45. renders pageTitle as the header’s own heading', async () => {
|
|
861
|
+
const el = await header({ pageTitle: 'Applications' });
|
|
862
|
+
const title = part(el, 'page-title');
|
|
863
|
+
expect(title).to.exist;
|
|
864
|
+
// A heading by role, not a hard-coded h1, so the page keeps control of its outline; level 1 by default.
|
|
865
|
+
expect(title.getAttribute('role')).to.equal('heading');
|
|
866
|
+
expect(el.headingLevel).to.equal(1);
|
|
867
|
+
expect(title.getAttribute('aria-level')).to.equal('1');
|
|
868
|
+
expect(title.textContent.trim()).to.equal('Applications');
|
|
869
|
+
expect(part(el, 'title-area')).to.exist;
|
|
870
|
+
});
|
|
871
|
+
// Birat's a11y review: a page with its own h1 needs the shell title one level down.
|
|
872
|
+
it('79. takes its heading level from the headingLevel property and attribute', async () => {
|
|
873
|
+
const el = await header({ pageTitle: 'Applications' });
|
|
874
|
+
el.headingLevel = 2;
|
|
875
|
+
await el.updateComplete;
|
|
876
|
+
const title = part(el, 'page-title');
|
|
877
|
+
expect(title.getAttribute('aria-level')).to.equal('2');
|
|
878
|
+
expect(el.getAttribute('headinglevel')).to.equal('2');
|
|
879
|
+
expect(title.textContent.trim()).to.equal('Applications');
|
|
880
|
+
const marked = await fixture(html `
|
|
881
|
+
<nile-app-shell pageTitle="Applications" headingLevel="3">
|
|
882
|
+
<p>page</p>
|
|
883
|
+
</nile-app-shell>
|
|
884
|
+
`);
|
|
885
|
+
await marked.updateComplete;
|
|
886
|
+
expect(marked.headingLevel).to.equal(3);
|
|
887
|
+
expect(part(marked, 'page-title').getAttribute('aria-level')).to.equal('3');
|
|
888
|
+
});
|
|
889
|
+
it('80. clamps an invalid heading level to a safe aria-level', async () => {
|
|
890
|
+
const el = await header({ pageTitle: 'Applications' });
|
|
891
|
+
const cases = [
|
|
892
|
+
[0, '1'],
|
|
893
|
+
[-3, '1'],
|
|
894
|
+
[7, '6'],
|
|
895
|
+
[2.7, '2'],
|
|
896
|
+
[NaN, '1'],
|
|
897
|
+
['abc', '1'],
|
|
898
|
+
[undefined, '1'],
|
|
899
|
+
];
|
|
900
|
+
for (const [value, expected] of cases) {
|
|
901
|
+
el.headingLevel = value;
|
|
902
|
+
await el.updateComplete;
|
|
903
|
+
expect(part(el, 'page-title').getAttribute('aria-level'), String(value)).to.equal(expected);
|
|
904
|
+
}
|
|
905
|
+
// From markup too: a non-numeric attribute still leaves a level-1 heading.
|
|
906
|
+
const marked = await fixture(html `
|
|
907
|
+
<nile-app-shell pageTitle="Applications" headingLevel="nope">
|
|
908
|
+
<p>page</p>
|
|
909
|
+
</nile-app-shell>
|
|
910
|
+
`);
|
|
911
|
+
await marked.updateComplete;
|
|
912
|
+
expect(part(marked, 'page-title').getAttribute('aria-level')).to.equal('1');
|
|
913
|
+
expect(part(marked, 'page-title').textContent.trim()).to.equal('Applications');
|
|
914
|
+
});
|
|
915
|
+
// The title block renders before the top-bar slot, so a page slotting its own title keeps its output.
|
|
916
|
+
it('46. renders the block before whatever the page slots into top-bar', async () => {
|
|
917
|
+
const el = await fixture(html `
|
|
918
|
+
<nile-app-shell style="width: 1176px; height: 800px;" pageTitle="Apps">
|
|
919
|
+
<nav slot="side-nav">nav</nav>
|
|
920
|
+
<span slot="top-bar" id="own">the page’s own bar</span>
|
|
921
|
+
<p>page</p>
|
|
922
|
+
</nile-app-shell>
|
|
923
|
+
`);
|
|
924
|
+
await el.updateComplete;
|
|
925
|
+
const area = part(el, 'title-area');
|
|
926
|
+
const slot = el.shadowRoot.querySelector('slot[name="top-bar"]');
|
|
927
|
+
expect(el.querySelector('#own').assignedSlot).to.equal(slot);
|
|
928
|
+
/* eslint-disable-next-line no-bitwise */
|
|
929
|
+
const areaIsFirst = area.compareDocumentPosition(slot) & Node.DOCUMENT_POSITION_FOLLOWING;
|
|
930
|
+
expect(areaIsFirst).to.be.greaterThan(0);
|
|
931
|
+
});
|
|
932
|
+
it('47. mounts the top bar for a title block on its own', async () => {
|
|
933
|
+
const el = await fixture(html `
|
|
934
|
+
<nile-app-shell
|
|
935
|
+
style="width: 1176px; height: 800px;"
|
|
936
|
+
pageTitle="Applications"
|
|
937
|
+
no-nav-toggle
|
|
938
|
+
>
|
|
939
|
+
<p>page</p>
|
|
940
|
+
</nile-app-shell>
|
|
941
|
+
`);
|
|
942
|
+
await el.updateComplete;
|
|
943
|
+
// Nothing is slotted anywhere, and there is no nav toggle to mount it.
|
|
944
|
+
expect(el.hasAttribute('has-side-nav')).to.be.false;
|
|
945
|
+
expect(el.hasAttribute('has-top-bar')).to.be.true;
|
|
946
|
+
expect(part(el, 'page-title')).to.exist;
|
|
947
|
+
});
|
|
948
|
+
it('48. renders a badge after the title, and only when set', async () => {
|
|
949
|
+
const plain = await header({ pageTitle: 'Applications' });
|
|
950
|
+
expect(part(plain, 'badge')).to.be.null;
|
|
951
|
+
const el = await header({ pageTitle: 'Applications', badge: '12 active' });
|
|
952
|
+
const badge = part(el, 'badge');
|
|
953
|
+
expect(badge).to.exist;
|
|
954
|
+
expect(badge.textContent.trim()).to.equal('12 active');
|
|
955
|
+
/* eslint-disable-next-line no-bitwise */
|
|
956
|
+
const titleIsFirst = part(el, 'page-title').compareDocumentPosition(badge) &
|
|
957
|
+
Node.DOCUMENT_POSITION_FOLLOWING;
|
|
958
|
+
expect(titleIsFirst).to.be.greaterThan(0);
|
|
959
|
+
});
|
|
960
|
+
// The badge's surface tokens must resolve on both the enterprise and nxtgen sheets, so each test pins one sheet on its own.
|
|
961
|
+
const badged = (tokens) => fixture(html `
|
|
962
|
+
<nile-app-shell
|
|
963
|
+
style="width: 1176px; height: 800px; ${tokens}"
|
|
964
|
+
pageTitle="Applications"
|
|
965
|
+
badge="Beta"
|
|
966
|
+
>
|
|
967
|
+
<nav slot="side-nav">nav</nav>
|
|
968
|
+
<p>page</p>
|
|
969
|
+
</nile-app-shell>
|
|
970
|
+
`);
|
|
971
|
+
it('69. resolves the badge on the enterprise sheet alone', async () => {
|
|
972
|
+
const el = await badged([
|
|
973
|
+
'--nile-border-width-1: 1px',
|
|
974
|
+
'--nile-colors-neutral-100: #FAFAFA',
|
|
975
|
+
'--nile-colors-neutral-400: #E6E9EB',
|
|
976
|
+
'--nile-colors-dark-500: #636363',
|
|
977
|
+
// The ink the badge must not fall back to by inheriting the title's.
|
|
978
|
+
'--nile-colors-dark-900: #000000',
|
|
979
|
+
].join('; '));
|
|
980
|
+
const style = getComputedStyle(part(el, 'badge'));
|
|
981
|
+
expect(style.backgroundColor).to.equal('rgb(250, 250, 250)');
|
|
982
|
+
expect(style.borderTopColor).to.equal('rgb(230, 233, 235)');
|
|
983
|
+
expect(style.borderTopWidth).to.equal('1px');
|
|
984
|
+
expect(style.color).to.equal('rgb(99, 99, 99)');
|
|
985
|
+
});
|
|
986
|
+
it('70. resolves the badge on the nxtgen sheet alone', async () => {
|
|
987
|
+
const el = await badged([
|
|
988
|
+
'--ng-border-width-1: 1px',
|
|
989
|
+
'--ng-componentcolors-utility-gray-50: #FAFAFAFF',
|
|
990
|
+
'--ng-componentcolors-utility-gray-200: #E9EAEBFF',
|
|
991
|
+
'--ng-componentcolors-utility-gray-700: #414651FF',
|
|
992
|
+
'--ng-colors-text-primary-900: #181D27FF',
|
|
993
|
+
].join('; '));
|
|
994
|
+
const style = getComputedStyle(part(el, 'badge'));
|
|
995
|
+
expect(style.backgroundColor).to.equal('rgb(250, 250, 250)');
|
|
996
|
+
expect(style.borderTopColor).to.equal('rgb(233, 234, 235)');
|
|
997
|
+
expect(style.borderTopWidth).to.equal('1px');
|
|
998
|
+
expect(style.color).to.equal('rgb(65, 70, 81)');
|
|
999
|
+
});
|
|
1000
|
+
it('49. renders a description line, and reserves nothing without one', async () => {
|
|
1001
|
+
const off = await header({ pageTitle: 'Applications' });
|
|
1002
|
+
expect(off.hasAttribute('has-description')).to.be.false;
|
|
1003
|
+
expect(part(off, 'description')).to.be.null;
|
|
1004
|
+
const el = await header({
|
|
1005
|
+
pageTitle: 'Applications',
|
|
1006
|
+
description: 'Manage the apps connected to this tenant',
|
|
1007
|
+
});
|
|
1008
|
+
const line = part(el, 'description');
|
|
1009
|
+
expect(el.hasAttribute('has-description')).to.be.true;
|
|
1010
|
+
expect(line).to.exist;
|
|
1011
|
+
expect(line.textContent.trim()).to.equal('Manage the apps connected to this tenant');
|
|
1012
|
+
// Clearing the string takes the axis back off.
|
|
1013
|
+
el.description = '';
|
|
1014
|
+
await el.updateComplete;
|
|
1015
|
+
expect(el.hasAttribute('has-description')).to.be.false;
|
|
1016
|
+
expect(part(el, 'description')).to.be.null;
|
|
1017
|
+
});
|
|
1018
|
+
it('50. leading defaults to none and draws neither control', async () => {
|
|
1019
|
+
const el = await header({ pageTitle: 'Applications' });
|
|
1020
|
+
expect(el.leading).to.equal('none');
|
|
1021
|
+
expect(part(el, 'back')).to.be.null;
|
|
1022
|
+
expect(part(el, 'app-icon')).to.be.null;
|
|
1023
|
+
expect(el.shadowRoot.querySelector('.crumb-divider')).to.be.null;
|
|
1024
|
+
});
|
|
1025
|
+
// Leading="back" draws a breadcrumb: glyph, previous-page label, slash, then the page title.
|
|
1026
|
+
it('51. leading="back" draws glyph, label, divider, then the title', async () => {
|
|
1027
|
+
const el = await header({ pageTitle: 'Applications', leading: 'back' });
|
|
1028
|
+
const back = part(el, 'back');
|
|
1029
|
+
const divider = el.shadowRoot.querySelector('.crumb-divider');
|
|
1030
|
+
const title = part(el, 'page-title');
|
|
1031
|
+
expect(back).to.exist;
|
|
1032
|
+
expect(back.tagName).to.equal('BUTTON');
|
|
1033
|
+
expect(back.getAttribute('type')).to.equal('button');
|
|
1034
|
+
expect(divider).to.exist;
|
|
1035
|
+
expect(divider.getAttribute('aria-hidden')).to.equal('true');
|
|
1036
|
+
// The icons go through nile-glyph, so assert the glyph, not an <svg>.
|
|
1037
|
+
const chevron = back.querySelector('nile-glyph');
|
|
1038
|
+
expect(chevron).to.exist;
|
|
1039
|
+
expect(chevron.getAttribute('name')).to.equal('ng-chevron-left');
|
|
1040
|
+
expect(divider.querySelector('nile-glyph').getAttribute('name')).to.equal('ng-slash');
|
|
1041
|
+
// glyph before label, inside the control.
|
|
1042
|
+
const label = back.querySelector('span');
|
|
1043
|
+
/* eslint-disable-next-line no-bitwise */
|
|
1044
|
+
const glyphIsFirst = chevron.compareDocumentPosition(label) & Node.DOCUMENT_POSITION_FOLLOWING;
|
|
1045
|
+
expect(glyphIsFirst).to.be.greaterThan(0);
|
|
1046
|
+
// control, then divider, then title.
|
|
1047
|
+
for (const [first, second] of [
|
|
1048
|
+
[back, divider],
|
|
1049
|
+
[divider, title],
|
|
1050
|
+
]) {
|
|
1051
|
+
/* eslint-disable-next-line no-bitwise */
|
|
1052
|
+
const ordered = first.compareDocumentPosition(second) &
|
|
1053
|
+
Node.DOCUMENT_POSITION_FOLLOWING;
|
|
1054
|
+
expect(ordered).to.be.greaterThan(0);
|
|
1055
|
+
}
|
|
1056
|
+
// nile-glyph is not registered on this test page, so the divider has no box to measure; only that the leading axis has not switched it off.
|
|
1057
|
+
expect(back.getBoundingClientRect().width).to.be.greaterThan(0);
|
|
1058
|
+
expect(getComputedStyle(divider).display).to.not.equal('none');
|
|
1059
|
+
});
|
|
1060
|
+
// The slash separates the back control from the page title, so there is nothing to point at until a title is set.
|
|
1061
|
+
it('51b. leading="back" without a title draws no divider', async () => {
|
|
1062
|
+
const el = await header({ leading: 'back' });
|
|
1063
|
+
expect(part(el, 'back')).to.exist;
|
|
1064
|
+
expect(el.shadowRoot.querySelector('.crumb-divider')).to.be.null;
|
|
1065
|
+
el.pageTitle = 'Applications';
|
|
1066
|
+
await el.updateComplete;
|
|
1067
|
+
expect(el.shadowRoot.querySelector('.crumb-divider')).to.exist;
|
|
1068
|
+
});
|
|
1069
|
+
it('52. takes the previous-page label from backLabel', async () => {
|
|
1070
|
+
const el = await header({ pageTitle: 'Applications', leading: 'back' });
|
|
1071
|
+
// The label is the control's accessible name, so it lives in the text.
|
|
1072
|
+
expect(part(el, 'back').textContent.trim()).to.equal('Back');
|
|
1073
|
+
el.backLabel = 'Integrations';
|
|
1074
|
+
await el.updateComplete;
|
|
1075
|
+
expect(part(el, 'back').textContent.trim()).to.equal('Integrations');
|
|
1076
|
+
});
|
|
1077
|
+
// The shell owns no history, so the control only announces the press; the page navigates itself.
|
|
1078
|
+
it('53. the back control emits nile-header-back, as do goBackHeader() and goBackPage()', async () => {
|
|
1079
|
+
const el = await header({ pageTitle: 'Applications', leading: 'back' });
|
|
1080
|
+
const clicked = oneEvent(el, 'nile-header-back');
|
|
1081
|
+
part(el, 'back').click();
|
|
1082
|
+
const fromClick = await clicked;
|
|
1083
|
+
expect(fromClick).to.exist;
|
|
1084
|
+
const called = oneEvent(el, 'nile-header-back');
|
|
1085
|
+
el.goBackHeader();
|
|
1086
|
+
expect(await called).to.exist;
|
|
1087
|
+
// goBackPage() is the deprecated alias of goBackHeader(): same event.
|
|
1088
|
+
const aliased = oneEvent(el, 'nile-header-back');
|
|
1089
|
+
el.goBackPage();
|
|
1090
|
+
expect(await aliased).to.exist;
|
|
1091
|
+
// It is the page's event, not the panel's: nothing about the panel moves.
|
|
1092
|
+
expect(el.panelOpen).to.be.false;
|
|
1093
|
+
});
|
|
1094
|
+
// The Description axis must not move when the Leading axis changes, so the description is always the stack's second line under the title.
|
|
1095
|
+
it('63. aligns the description to the page title in every leading mode', async () => {
|
|
1096
|
+
for (const leading of ['none', 'back', 'app-icon']) {
|
|
1097
|
+
const el = await header({
|
|
1098
|
+
pageTitle: 'Applications',
|
|
1099
|
+
description: 'Manage the apps connected to this tenant',
|
|
1100
|
+
leading,
|
|
1101
|
+
});
|
|
1102
|
+
const title = part(el, 'page-title').getBoundingClientRect();
|
|
1103
|
+
const description = part(el, 'description').getBoundingClientRect();
|
|
1104
|
+
expect(Math.round(description.left), leading).to.equal(Math.round(title.left));
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
// Neither leading control lives in the title row, so neither can displace the title away from the description.
|
|
1108
|
+
it('64. renders both leading modes beside the title stack, not in the row', async () => {
|
|
1109
|
+
const backShell = await header({ pageTitle: 'Applications', leading: 'back' });
|
|
1110
|
+
const iconShell = await header({
|
|
1111
|
+
pageTitle: 'Applications',
|
|
1112
|
+
leading: 'app-icon',
|
|
1113
|
+
});
|
|
1114
|
+
for (const [el, name] of [
|
|
1115
|
+
[backShell, 'back'],
|
|
1116
|
+
[iconShell, 'app-icon'],
|
|
1117
|
+
]) {
|
|
1118
|
+
const control = part(el, name);
|
|
1119
|
+
const row = el.shadowRoot.querySelector('.title-row');
|
|
1120
|
+
const area = part(el, 'title-area');
|
|
1121
|
+
expect(row.contains(control), name).to.be.false;
|
|
1122
|
+
expect(area.contains(control), name).to.be.true;
|
|
1123
|
+
}
|
|
1124
|
+
// The breadcrumb divider travels with the control it belongs to.
|
|
1125
|
+
const divider = backShell.shadowRoot.querySelector('.crumb-divider');
|
|
1126
|
+
const row = backShell.shadowRoot.querySelector('.title-row');
|
|
1127
|
+
expect(row.contains(divider)).to.be.false;
|
|
1128
|
+
});
|
|
1129
|
+
// The leading element sits on the title's row, and its offset does not move when a description appears.
|
|
1130
|
+
it('65. sits the leading element on the title row, description or not', async () => {
|
|
1131
|
+
for (const leading of ['back', 'app-icon']) {
|
|
1132
|
+
for (const description of ['', 'Manage the apps connected here']) {
|
|
1133
|
+
const el = await header({
|
|
1134
|
+
pageTitle: 'Applications',
|
|
1135
|
+
leading,
|
|
1136
|
+
description,
|
|
1137
|
+
});
|
|
1138
|
+
const title = part(el, 'page-title').getBoundingClientRect();
|
|
1139
|
+
const box = el.shadowRoot.querySelector('.leading');
|
|
1140
|
+
const label = `${leading} description=${!!description}`;
|
|
1141
|
+
// The line box is the title's own, and it starts where the title does.
|
|
1142
|
+
expect(Math.round(box.getBoundingClientRect().top), label).to.equal(Math.round(title.top));
|
|
1143
|
+
expect(Math.round(box.getBoundingClientRect().height), label).to.equal(24);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
it('66. centres a 20px app icon in the 24px title row, two pixels down', async () => {
|
|
1148
|
+
const el = await header({
|
|
1149
|
+
pageTitle: 'Applications',
|
|
1150
|
+
leading: 'app-icon',
|
|
1151
|
+
description: 'Manage the apps connected here',
|
|
1152
|
+
});
|
|
1153
|
+
const title = part(el, 'page-title').getBoundingClientRect();
|
|
1154
|
+
const icon = part(el, 'app-icon').getBoundingClientRect();
|
|
1155
|
+
// (24 - 20) / 2 — structural, not an offset written anywhere.
|
|
1156
|
+
expect(Math.round(icon.top - title.top)).to.equal(2);
|
|
1157
|
+
});
|
|
1158
|
+
// The app icon is sized apart from the utility controls, so retuning one cannot move the other.
|
|
1159
|
+
it('67. sizes the app icon independently of the utility controls', async () => {
|
|
1160
|
+
const el = await header({ pageTitle: 'Applications', leading: 'app-icon' });
|
|
1161
|
+
const host = getComputedStyle(el);
|
|
1162
|
+
expect(host.getPropertyValue('--nile-app-shell-app-icon-size').trim()).to.equal('20px');
|
|
1163
|
+
expect(host.getPropertyValue('--nile-app-shell-utility-size').trim()).to.equal('32px');
|
|
1164
|
+
el.style.setProperty('--nile-app-shell-app-icon-size', '28px');
|
|
1165
|
+
await el.updateComplete;
|
|
1166
|
+
expect(part(el, 'app-icon').getBoundingClientRect().width).to.equal(28);
|
|
1167
|
+
// The nav toggle is untouched by that.
|
|
1168
|
+
const toggle = part(el, 'nav-toggle');
|
|
1169
|
+
const base = toggle.shadowRoot.querySelector('[part~="base"]');
|
|
1170
|
+
expect(base.getBoundingClientRect().width).to.equal(32);
|
|
1171
|
+
});
|
|
1172
|
+
it('54. leading="app-icon" draws a 20px block around the slotted mark', async () => {
|
|
1173
|
+
const el = await header({ pageTitle: 'Applications', leading: 'app-icon' });
|
|
1174
|
+
const box = part(el, 'app-icon');
|
|
1175
|
+
expect(box).to.exist;
|
|
1176
|
+
expect(part(el, 'back')).to.be.null;
|
|
1177
|
+
const rect = box.getBoundingClientRect();
|
|
1178
|
+
// Its own size, not the 32px utility control box the nav toggle uses.
|
|
1179
|
+
expect(rect.width).to.equal(20);
|
|
1180
|
+
expect(rect.height).to.equal(20);
|
|
1181
|
+
// The page's mark is assigned into the shell's box.
|
|
1182
|
+
const slot = box.querySelector('slot[name="app-icon"]');
|
|
1183
|
+
expect(slot).to.exist;
|
|
1184
|
+
expect(slot.assignedElements()).to.deep.equal([
|
|
1185
|
+
el.querySelector('#mark'),
|
|
1186
|
+
]);
|
|
1187
|
+
// The block sits before the title, not after it.
|
|
1188
|
+
/* eslint-disable-next-line no-bitwise */
|
|
1189
|
+
const boxIsFirst = box.compareDocumentPosition(part(el, 'page-title')) &
|
|
1190
|
+
Node.DOCUMENT_POSITION_FOLLOWING;
|
|
1191
|
+
expect(boxIsFirst).to.be.greaterThan(0);
|
|
1192
|
+
});
|
|
1193
|
+
});
|
|
1194
|
+
// The four header heights are viewport-height sensitive, so these tests set a viewport where the spec numbers apply.
|
|
1195
|
+
describe('NileAppShell — the four header heights', () => {
|
|
1196
|
+
const TALL = { width: 1400, height: 900 };
|
|
1197
|
+
const SHORT = { width: 800, height: 600 };
|
|
1198
|
+
before(async () => {
|
|
1199
|
+
await setViewport(TALL);
|
|
1200
|
+
});
|
|
1201
|
+
after(async () => {
|
|
1202
|
+
await setViewport(SHORT);
|
|
1203
|
+
});
|
|
1204
|
+
const bar = (el) => el.shadowRoot.querySelector('.container > .top-bar');
|
|
1205
|
+
const strip = (el) => el.shadowRoot.querySelector('.tabs');
|
|
1206
|
+
// The header block is the bar plus its strip, measured top-to-bottom so a gap between them is caught.
|
|
1207
|
+
const headerBlock = (el) => {
|
|
1208
|
+
const top = bar(el).getBoundingClientRect().top;
|
|
1209
|
+
const bottom = el.tabsVisible
|
|
1210
|
+
? strip(el).getBoundingClientRect().bottom
|
|
1211
|
+
: bar(el).getBoundingClientRect().bottom;
|
|
1212
|
+
return bottom - top;
|
|
1213
|
+
};
|
|
1214
|
+
it('55. is 68px with neither axis on, whatever the leading', async () => {
|
|
1215
|
+
for (const leading of ['none', 'back', 'app-icon']) {
|
|
1216
|
+
const el = await header({ pageTitle: 'Applications', leading });
|
|
1217
|
+
expect(bar(el).getBoundingClientRect().height, leading).to.equal(68);
|
|
1218
|
+
// The leading axis changes what is in the bar, never how tall it is.
|
|
1219
|
+
const area = part(el, 'title-area').getBoundingClientRect().height;
|
|
1220
|
+
expect(area, leading).to.be.at.most(bar(el).clientHeight);
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
// A description adds its own line (24 title + 2 gap + 18 description = 44) in a 76px bar; asserted as the lines' own boxes.
|
|
1224
|
+
it('56. is 76px with a description, and the second line fits it', async () => {
|
|
1225
|
+
const el = await header({
|
|
1226
|
+
pageTitle: 'Applications',
|
|
1227
|
+
description: 'Manage the apps connected to this tenant',
|
|
1228
|
+
});
|
|
1229
|
+
expect(bar(el).getBoundingClientRect().height).to.equal(76);
|
|
1230
|
+
const stack = el.shadowRoot.querySelector('.title-stack');
|
|
1231
|
+
expect(part(el, 'page-title').getBoundingClientRect().height).to.equal(24);
|
|
1232
|
+
expect(part(el, 'description').getBoundingClientRect().height).to.equal(18);
|
|
1233
|
+
expect(stack.getBoundingClientRect().height).to.equal(44);
|
|
1234
|
+
// Nothing is clipped: the stack sits wholly inside the bar's box.
|
|
1235
|
+
const barBox = bar(el).getBoundingClientRect();
|
|
1236
|
+
const stackBox = stack.getBoundingClientRect();
|
|
1237
|
+
expect(stackBox.top).to.be.at.least(barBox.top);
|
|
1238
|
+
expect(stackBox.bottom).to.be.at.most(barBox.bottom);
|
|
1239
|
+
});
|
|
1240
|
+
it('57. adds a 33px strip for tabs: 101px, and 109px with a description', async () => {
|
|
1241
|
+
const tabbed = await header({ pageTitle: 'Applications', tabs: true });
|
|
1242
|
+
expect(strip(tabbed).getBoundingClientRect().height).to.equal(33);
|
|
1243
|
+
expect(headerBlock(tabbed)).to.equal(101);
|
|
1244
|
+
const described = await header({
|
|
1245
|
+
pageTitle: 'Applications',
|
|
1246
|
+
description: 'Manage the apps connected to this tenant',
|
|
1247
|
+
tabs: true,
|
|
1248
|
+
});
|
|
1249
|
+
expect(strip(described).getBoundingClientRect().height).to.equal(33);
|
|
1250
|
+
expect(headerBlock(described)).to.equal(109);
|
|
1251
|
+
});
|
|
1252
|
+
// The three axes are independent, so all twelve combinations land on one of the four heights.
|
|
1253
|
+
it('58. resolves all twelve variants to exactly the four heights', async () => {
|
|
1254
|
+
for (const leading of ['none', 'back', 'app-icon']) {
|
|
1255
|
+
for (const tabs of [false, true]) {
|
|
1256
|
+
for (const described of [false, true]) {
|
|
1257
|
+
/* eslint-disable-next-line no-nested-ternary */
|
|
1258
|
+
const expected = tabs ? (described ? 109 : 101) : described ? 76 : 68;
|
|
1259
|
+
const el = await header({
|
|
1260
|
+
pageTitle: 'Applications',
|
|
1261
|
+
description: described ? 'Manage connected apps' : '',
|
|
1262
|
+
leading,
|
|
1263
|
+
tabs,
|
|
1264
|
+
});
|
|
1265
|
+
const label = `leading=${leading} tabs=${tabs} description=${described}`;
|
|
1266
|
+
expect(headerBlock(el), label).to.equal(expected);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
// The strip is a control row plus its rule; the row shrinks to fit via the group's own padding hook, staying shorter than the standalone group.
|
|
1272
|
+
it('59. shrinks the slotted tab row to fit the strip, rather than crop it', async () => {
|
|
1273
|
+
const el = await header({ pageTitle: 'Applications', tabs: true });
|
|
1274
|
+
const standalone = await fixture(html `
|
|
1275
|
+
<nile-nav-tab-group style="${HEADER_TOKENS}">
|
|
1276
|
+
<nile-nav-tab slot="nav" panel="apps" active>Connected</nile-nav-tab>
|
|
1277
|
+
<nile-nav-tab slot="nav" panel="approvals">Approvals</nile-nav-tab>
|
|
1278
|
+
</nile-nav-tab-group>
|
|
1279
|
+
`);
|
|
1280
|
+
const stripBox = strip(el).getBoundingClientRect();
|
|
1281
|
+
const slottedRow = el.querySelector('nile-nav-tab-group').getBoundingClientRect();
|
|
1282
|
+
const loneRow = standalone.getBoundingClientRect();
|
|
1283
|
+
// The design's row: 0 + 20px label + 12px = 32, in a 33px strip.
|
|
1284
|
+
expect(slottedRow.height).to.equal(32);
|
|
1285
|
+
expect(stripBox.height).to.equal(33);
|
|
1286
|
+
// The shell's hook is load-bearing: the same group is taller on its own.
|
|
1287
|
+
expect(loneRow.height).to.be.greaterThan(slottedRow.height);
|
|
1288
|
+
expect(loneRow.height).to.be.greaterThan(stripBox.height);
|
|
1289
|
+
// Nothing is cropped and there is no slack: the row fills the strip above the rule it sits on.
|
|
1290
|
+
expect(slottedRow.top).to.be.at.least(stripBox.top);
|
|
1291
|
+
expect(slottedRow.bottom).to.be.at.most(stripBox.bottom);
|
|
1292
|
+
expect(slottedRow.top - stripBox.top).to.equal(0);
|
|
1293
|
+
});
|
|
1294
|
+
it('60. drives the row through the group’s own padding hook', async () => {
|
|
1295
|
+
const el = await header({ pageTitle: 'Applications', tabs: true });
|
|
1296
|
+
const group = el.querySelector('nile-nav-tab-group');
|
|
1297
|
+
const padding = getComputedStyle(group)
|
|
1298
|
+
.getPropertyValue('--nile-nav-tab-padding')
|
|
1299
|
+
.split(/\s+/)
|
|
1300
|
+
.filter(Boolean);
|
|
1301
|
+
// The tab node's padding is 0 / 4 / 12 / 4, the 12px bottom being the active indicator's band.
|
|
1302
|
+
expect(padding).to.deep.equal(['0px', '4px', '12px', '4px']);
|
|
1303
|
+
// The inter-tab gap is the group's own 12px and is deliberately untouched.
|
|
1304
|
+
expect(getComputedStyle(group).getPropertyValue('--tab-gap').trim()).to.equal('12px');
|
|
1305
|
+
});
|
|
1306
|
+
// The compression under a short viewport is documented behaviour, asserted here to keep the resize honest.
|
|
1307
|
+
it('61. compresses to 56/64px under 600px of viewport height', async () => {
|
|
1308
|
+
await setViewport(SHORT);
|
|
1309
|
+
try {
|
|
1310
|
+
const plain = await header({ pageTitle: 'Applications' });
|
|
1311
|
+
expect(bar(plain).getBoundingClientRect().height).to.equal(56);
|
|
1312
|
+
const described = await header({
|
|
1313
|
+
pageTitle: 'Applications',
|
|
1314
|
+
description: 'Manage connected apps',
|
|
1315
|
+
});
|
|
1316
|
+
expect(bar(described).getBoundingClientRect().height).to.equal(64);
|
|
1317
|
+
}
|
|
1318
|
+
finally {
|
|
1319
|
+
await setViewport(TALL);
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
453
1322
|
});
|
|
454
1323
|
//# sourceMappingURL=nile-app-shell.test.js.map
|