@aquera/nile-elements 2.0.11 → 2.0.12
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 +3 -0
- package/demo/app-shell.html +21 -21
- package/dist/index.js +76 -66
- 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 +35 -32
- package/dist/nile-app-shell/nile-app-shell.esm.js +39 -34
- 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 +3 -1
- package/dist/src/nile-app-shell/nile-app-shell-regions.test.js +9 -5
- 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.js +33 -30
- 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 +22 -22
- package/dist/src/nile-app-shell/nile-app-shell.js +86 -76
- 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 +1 -0
- package/dist/src/nile-app-shell/nile-app-shell.test.js +307 -89
- package/dist/src/nile-app-shell/nile-app-shell.test.js.map +1 -1
- package/dist/src/nile-app-shell-panel/nile-app-shell-panel.css.js +3 -1
- package/dist/src/nile-app-shell-panel/nile-app-shell-panel.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/package.json +1 -1
- package/src/nile-app-shell/nile-app-shell-regions.test.ts +9 -5
- package/src/nile-app-shell/nile-app-shell.css.ts +33 -30
- package/src/nile-app-shell/nile-app-shell.test.ts +348 -105
- package/src/nile-app-shell/nile-app-shell.ts +89 -82
- package/src/nile-app-shell-panel/nile-app-shell-panel.css.ts +3 -1
- package/vscode-html-custom-data.json +27 -37
|
@@ -2,6 +2,7 @@ import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
|
|
2
2
|
import { setViewport } from '@web/test-runner-commands';
|
|
3
3
|
import './nile-app-shell';
|
|
4
4
|
import '../nile-app-shell-nav-toggle/nile-app-shell-nav-toggle';
|
|
5
|
+
import '../nile-app-shell-panel/nile-app-shell-panel';
|
|
5
6
|
import '../nile-nav-tab-group/nile-nav-tab-group';
|
|
6
7
|
import '../nile-nav-tab/nile-nav-tab';
|
|
7
8
|
import '../nile-side-bar/nile-side-bar';
|
|
@@ -224,6 +225,46 @@ describe('NileAppShell — Ask Aquera panel', () => {
|
|
|
224
225
|
expect(part(el, 'panel').getAttribute('aria-label')).to.equal('Ask Aquera');
|
|
225
226
|
expect(part(el, 'panel-title').textContent.trim()).to.equal('Ask Aquera');
|
|
226
227
|
});
|
|
228
|
+
// Amendment A (A-8): Figma's Assistant Title is Text lg/Semibold, 18/28; the shell sets it on the title it renders itself.
|
|
229
|
+
it('17b. sets its own panel title at 18/28 in text-primary-900', async () => {
|
|
230
|
+
const tokens = '--ng-font-size-text-lg: 18px; --ng-line-height-text-lg: 28px; --ng-font-weight-semibold: 600; --ng-colors-text-primary-900: rgb(24, 29, 39)';
|
|
231
|
+
const el = await sized(1440);
|
|
232
|
+
el.setAttribute('style', `${el.getAttribute('style')}; ${tokens}`);
|
|
233
|
+
el.openPanel();
|
|
234
|
+
await el.updateComplete;
|
|
235
|
+
const own = getComputedStyle(part(el, 'panel-title'));
|
|
236
|
+
expect(own.fontSize).to.equal('18px');
|
|
237
|
+
expect(own.lineHeight).to.equal('28px');
|
|
238
|
+
expect(own.fontWeight).to.equal('600');
|
|
239
|
+
expect(own.color).to.equal('rgb(24, 29, 39)');
|
|
240
|
+
});
|
|
241
|
+
// A-8: a slotted nile-app-shell-panel (part of App Shell) draws the same Figma title: 18/28 SemiBold, text-primary-900.
|
|
242
|
+
it('17c. sets the nile-app-shell-panel title at 18/28 in text-primary-900', async () => {
|
|
243
|
+
const tokens = '--ng-font-size-text-sm: 14px; --ng-font-size-text-lg: 18px; --ng-line-height-text-lg: 28px; --ng-font-weight-semibold: 600; --ng-colors-text-primary-900: rgb(24, 29, 39)';
|
|
244
|
+
const slotted = await fixture(html `
|
|
245
|
+
<nile-app-shell-panel label="Ask Aquera" style="${tokens}"></nile-app-shell-panel>
|
|
246
|
+
`);
|
|
247
|
+
await slotted.updateComplete;
|
|
248
|
+
const style = getComputedStyle(slotted.shadowRoot.querySelector('.title'));
|
|
249
|
+
expect(style.fontSize).to.equal('18px');
|
|
250
|
+
expect(style.lineHeight).to.equal('28px');
|
|
251
|
+
expect(style.fontWeight).to.equal('600');
|
|
252
|
+
expect(style.color).to.equal('rgb(24, 29, 39)');
|
|
253
|
+
});
|
|
254
|
+
// The enterprise sheet keeps its own panel title: --nile-type-scale-3 and an inherited line height, exactly as before.
|
|
255
|
+
// Enterprise defines no --ng-* tokens, so the NxtGen line height does not apply there.
|
|
256
|
+
it('17d. leaves the enterprise nile-app-shell-panel title as it was', async () => {
|
|
257
|
+
const slotted = await fixture(html `
|
|
258
|
+
<nile-app-shell-panel
|
|
259
|
+
label="Ask Aquera"
|
|
260
|
+
style="--nile-type-scale-3: 14px; line-height: 20px"
|
|
261
|
+
></nile-app-shell-panel>
|
|
262
|
+
`);
|
|
263
|
+
await slotted.updateComplete;
|
|
264
|
+
const style = getComputedStyle(slotted.shadowRoot.querySelector('.title'));
|
|
265
|
+
expect(style.fontSize).to.equal('14px');
|
|
266
|
+
expect(style.lineHeight).to.equal('20px');
|
|
267
|
+
});
|
|
227
268
|
it('18. Cmd/Ctrl+J toggles the panel', async () => {
|
|
228
269
|
const el = await sized(1440);
|
|
229
270
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'j', ctrlKey: true }));
|
|
@@ -575,6 +616,46 @@ describe('NileAppShell — the built-in Ask button', () => {
|
|
|
575
616
|
expect(getComputedStyle(actions).display).to.not.equal('none');
|
|
576
617
|
expect(actions.getBoundingClientRect().width).to.be.greaterThan(0);
|
|
577
618
|
});
|
|
619
|
+
// Amendment A (M8): the design's Ask is the secondary button, not the filled primary.
|
|
620
|
+
it('34b. renders Ask as the secondary variant', async () => {
|
|
621
|
+
const el = await sized(1440);
|
|
622
|
+
expect(ask(el).getAttribute('variant')).to.equal('secondary');
|
|
623
|
+
});
|
|
624
|
+
// Amendment A (M9, M10): app actions, a 16px divider, then Ask, 16px apart; with no app actions, no divider.
|
|
625
|
+
it('34c. draws a divider before Ask only when the page slots actions, 16px from both', async () => {
|
|
626
|
+
const alone = await sized(1440);
|
|
627
|
+
await settled(alone);
|
|
628
|
+
expect(part(alone, 'ask-rule')).to.be.null;
|
|
629
|
+
const el = await fixture(html `
|
|
630
|
+
<nile-app-shell style="width: 1440px; height: 600px; --ng-spacing-xl: 16px;">
|
|
631
|
+
<nav slot="side-nav">nav</nav>
|
|
632
|
+
<span slot="top-bar">Title</span>
|
|
633
|
+
<button slot="top-bar-actions" id="one">One</button>
|
|
634
|
+
<button slot="top-bar-actions" id="two">Two</button>
|
|
635
|
+
<p>page</p>
|
|
636
|
+
<div slot="panel">assistant</div>
|
|
637
|
+
</nile-app-shell>
|
|
638
|
+
`);
|
|
639
|
+
await waitUntil(() => el.hasAttribute('has-top-bar-actions'));
|
|
640
|
+
await el.updateComplete;
|
|
641
|
+
const rule = part(el, 'ask-rule');
|
|
642
|
+
expect(rule).to.exist;
|
|
643
|
+
const one = el.querySelector('#one').getBoundingClientRect();
|
|
644
|
+
const two = el.querySelector('#two').getBoundingClientRect();
|
|
645
|
+
const ruleBox = rule.getBoundingClientRect();
|
|
646
|
+
const askBox = ask(el).getBoundingClientRect();
|
|
647
|
+
expect(Math.round(ruleBox.height)).to.equal(16);
|
|
648
|
+
expect(Math.round(two.left - one.right)).to.equal(16);
|
|
649
|
+
expect(Math.round(ruleBox.left - two.right)).to.equal(16);
|
|
650
|
+
expect(Math.round(askBox.left - ruleBox.left)).to.equal(16);
|
|
651
|
+
// Order: the page's actions, the divider, then Ask.
|
|
652
|
+
/* eslint-disable-next-line no-bitwise */
|
|
653
|
+
const ruleBeforeAsk = rule.compareDocumentPosition(ask(el)) & Node.DOCUMENT_POSITION_FOLLOWING;
|
|
654
|
+
expect(ruleBeforeAsk).to.be.greaterThan(0);
|
|
655
|
+
el.noAsk = true;
|
|
656
|
+
await el.updateComplete;
|
|
657
|
+
expect(part(el, 'ask-rule'), 'no Ask, no divider').to.be.null;
|
|
658
|
+
});
|
|
578
659
|
// The shell draws its icon controls as bare glyphs via ::part(base), overriding nile-button's circle and ghost surface.
|
|
579
660
|
const utilityBase = (el, name) => part(el, name).shadowRoot.querySelector('[part~="base"]');
|
|
580
661
|
it('35. draws its icon controls with no resting surface', async () => {
|
|
@@ -769,6 +850,7 @@ const HEADER_TOKENS = [
|
|
|
769
850
|
'--ng-spacing-xxs: 2px',
|
|
770
851
|
'--ng-spacing-xs: 4px',
|
|
771
852
|
'--ng-spacing-md: 8px',
|
|
853
|
+
'--ng-spacing-md-alt: 10px',
|
|
772
854
|
'--ng-spacing-lg: 12px',
|
|
773
855
|
'--ng-spacing-xl: 16px',
|
|
774
856
|
'--ng-spacing-3xl: 24px',
|
|
@@ -779,6 +861,9 @@ const HEADER_TOKENS = [
|
|
|
779
861
|
'--ng-font-size-text-md: 16px',
|
|
780
862
|
'--ng-line-height-text-md: 24px',
|
|
781
863
|
].join('; ');
|
|
864
|
+
const LEADINGS = ['none', 'back', 'app-icon', 'back+app-icon'];
|
|
865
|
+
const hasBack = (leading) => leading === 'back' || leading === 'back+app-icon';
|
|
866
|
+
const hasIcon = (leading) => leading === 'app-icon' || leading === 'back+app-icon';
|
|
782
867
|
// A shell carrying whatever header axes a test asks for; an unset axis is the empty string, meaning "absent".
|
|
783
868
|
const header = async (axes = {}) => {
|
|
784
869
|
const el = await fixture(html `
|
|
@@ -787,13 +872,16 @@ const header = async (axes = {}) => {
|
|
|
787
872
|
.pageTitle=${axes.pageTitle ?? ''}
|
|
788
873
|
.description=${axes.description ?? ''}
|
|
789
874
|
.badge=${axes.badge ?? ''}
|
|
790
|
-
|
|
791
|
-
.
|
|
875
|
+
.headerBack=${hasBack(axes.leading)}
|
|
876
|
+
.headerBackLabel=${axes.headerBackLabel ?? 'Back'}
|
|
792
877
|
>
|
|
793
878
|
<nav slot="side-nav">nav</nav>
|
|
794
|
-
${axes.leading
|
|
879
|
+
${hasIcon(axes.leading)
|
|
795
880
|
? html `<span slot="app-icon" id="mark">A</span>`
|
|
796
881
|
: ''}
|
|
882
|
+
${axes.actions
|
|
883
|
+
? html `<button slot="top-bar-actions" id="cta">Button CTA</button>`
|
|
884
|
+
: ''}
|
|
797
885
|
${axes.tabs
|
|
798
886
|
? html `<nile-nav-tab-group slot="tabs">
|
|
799
887
|
<nile-nav-tab slot="nav" panel="apps" active>Connected</nile-nav-tab>
|
|
@@ -805,6 +893,10 @@ const header = async (axes = {}) => {
|
|
|
805
893
|
`);
|
|
806
894
|
if (axes.tabs)
|
|
807
895
|
await waitUntil(() => el.hasAttribute('has-tabs'));
|
|
896
|
+
if (hasIcon(axes.leading))
|
|
897
|
+
await waitUntil(() => el.hasAttribute('has-app-icon'));
|
|
898
|
+
if (axes.actions)
|
|
899
|
+
await waitUntil(() => el.hasAttribute('has-top-bar-actions'));
|
|
808
900
|
await el.updateComplete;
|
|
809
901
|
return el;
|
|
810
902
|
};
|
|
@@ -812,7 +904,8 @@ const header = async (axes = {}) => {
|
|
|
812
904
|
describe('NileAppShell — the header title block', () => {
|
|
813
905
|
it('44. draws no title block at all until an axis asks for one', async () => {
|
|
814
906
|
const el = await header();
|
|
815
|
-
expect(el.
|
|
907
|
+
expect(el.headerBack).to.be.false;
|
|
908
|
+
expect(el.hasAttribute('has-app-icon')).to.be.false;
|
|
816
909
|
expect(part(el, 'title-area')).to.be.null;
|
|
817
910
|
expect(part(el, 'page-title')).to.be.null;
|
|
818
911
|
expect(part(el, 'description')).to.be.null;
|
|
@@ -848,8 +941,8 @@ describe('NileAppShell — the header title block', () => {
|
|
|
848
941
|
expect(part(el, 'badge')).to.exist;
|
|
849
942
|
expect(el.hasAttribute('has-description')).to.be.true;
|
|
850
943
|
});
|
|
851
|
-
//
|
|
852
|
-
it('75. mounts
|
|
944
|
+
// The back arrow is the exception: it stands on its own but brings no empty title stack.
|
|
945
|
+
it('75. mounts the back arrow on its own, without an empty title stack', async () => {
|
|
853
946
|
const el = await header({ leading: 'back' });
|
|
854
947
|
expect(part(el, 'title-area')).to.exist;
|
|
855
948
|
expect(part(el, 'back')).to.exist;
|
|
@@ -1015,120 +1108,150 @@ describe('NileAppShell — the header title block', () => {
|
|
|
1015
1108
|
expect(el.hasAttribute('has-description')).to.be.false;
|
|
1016
1109
|
expect(part(el, 'description')).to.be.null;
|
|
1017
1110
|
});
|
|
1018
|
-
it('50.
|
|
1111
|
+
it('50. headerBack defaults to false and draws no arrow, and no slotted icon draws no icon', async () => {
|
|
1019
1112
|
const el = await header({ pageTitle: 'Applications' });
|
|
1020
|
-
expect(el.
|
|
1113
|
+
expect(el.headerBack).to.be.false;
|
|
1114
|
+
expect(el.hasAttribute('headerback')).to.be.false;
|
|
1021
1115
|
expect(part(el, 'back')).to.be.null;
|
|
1022
|
-
expect(
|
|
1116
|
+
expect(el.hasAttribute('has-app-icon')).to.be.false;
|
|
1117
|
+
expect(getComputedStyle(part(el, 'app-icon')).display).to.equal('none');
|
|
1023
1118
|
expect(el.shadowRoot.querySelector('.crumb-divider')).to.be.null;
|
|
1024
1119
|
});
|
|
1025
|
-
//
|
|
1026
|
-
it('51.
|
|
1120
|
+
// Amendment A: the back control is the arrow alone, with no previous-page label and no slash.
|
|
1121
|
+
it('51. headerBack draws the narrow-left arrow only, then the title', async () => {
|
|
1027
1122
|
const el = await header({ pageTitle: 'Applications', leading: 'back' });
|
|
1028
1123
|
const back = part(el, 'back');
|
|
1029
|
-
const divider = el.shadowRoot.querySelector('.crumb-divider');
|
|
1030
1124
|
const title = part(el, 'page-title');
|
|
1031
1125
|
expect(back).to.exist;
|
|
1032
1126
|
expect(back.tagName).to.equal('BUTTON');
|
|
1033
1127
|
expect(back.getAttribute('type')).to.equal('button');
|
|
1034
|
-
expect(divider).to.exist;
|
|
1035
|
-
expect(divider.getAttribute('aria-hidden')).to.equal('true');
|
|
1036
1128
|
// The icons go through nile-glyph, so assert the glyph, not an <svg>.
|
|
1037
|
-
const
|
|
1038
|
-
expect(
|
|
1039
|
-
expect(
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1129
|
+
const glyphs = back.querySelectorAll('nile-glyph');
|
|
1130
|
+
expect(glyphs.length).to.equal(1);
|
|
1131
|
+
expect(glyphs[0].getAttribute('name')).to.equal('ng-arrow-narrow-left');
|
|
1132
|
+
// No label, no divider, no slash anywhere in the header.
|
|
1133
|
+
expect(back.textContent.trim()).to.equal('');
|
|
1134
|
+
expect(back.querySelector('span')).to.be.null;
|
|
1135
|
+
expect(el.shadowRoot.querySelector('.crumb-divider')).to.be.null;
|
|
1136
|
+
expect(el.shadowRoot.querySelector('nile-glyph[name="ng-slash"]')).to.be.null;
|
|
1043
1137
|
/* eslint-disable-next-line no-bitwise */
|
|
1044
|
-
const
|
|
1045
|
-
expect(
|
|
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.
|
|
1138
|
+
const backIsFirst = back.compareDocumentPosition(title) & Node.DOCUMENT_POSITION_FOLLOWING;
|
|
1139
|
+
expect(backIsFirst).to.be.greaterThan(0);
|
|
1057
1140
|
expect(back.getBoundingClientRect().width).to.be.greaterThan(0);
|
|
1058
|
-
expect(getComputedStyle(divider).display).to.not.equal('none');
|
|
1059
1141
|
});
|
|
1060
|
-
//
|
|
1061
|
-
it('51b.
|
|
1062
|
-
const
|
|
1142
|
+
// camelCase property, attribute: true + reflect: true — the title-block pattern.
|
|
1143
|
+
it('51b. headerBack and headerBackLabel work from markup and reflect to their attributes', async () => {
|
|
1144
|
+
const marked = await fixture(html `
|
|
1145
|
+
<nile-app-shell pageTitle="Applications" headerBack headerBackLabel="Retour">
|
|
1146
|
+
<p>page</p>
|
|
1147
|
+
</nile-app-shell>
|
|
1148
|
+
`);
|
|
1149
|
+
await marked.updateComplete;
|
|
1150
|
+
expect(marked.headerBack).to.be.true;
|
|
1151
|
+
expect(marked.headerBackLabel).to.equal('Retour');
|
|
1152
|
+
expect(part(marked, 'back')).to.exist;
|
|
1153
|
+
const el = await header({ pageTitle: 'Applications' });
|
|
1154
|
+
el.headerBack = true;
|
|
1155
|
+
el.headerBackLabel = 'Go back';
|
|
1156
|
+
await el.updateComplete;
|
|
1157
|
+
expect(el.hasAttribute('headerback')).to.be.true;
|
|
1158
|
+
expect(el.getAttribute('headerbacklabel')).to.equal('Go back');
|
|
1063
1159
|
expect(part(el, 'back')).to.exist;
|
|
1064
|
-
|
|
1065
|
-
el.pageTitle = 'Applications';
|
|
1160
|
+
el.headerBack = false;
|
|
1066
1161
|
await el.updateComplete;
|
|
1067
|
-
expect(el.
|
|
1162
|
+
expect(el.hasAttribute('headerback')).to.be.false;
|
|
1163
|
+
expect(part(el, 'back')).to.be.null;
|
|
1068
1164
|
});
|
|
1069
|
-
|
|
1165
|
+
// headerBackLabel names the control for assistive tech and is never drawn as text.
|
|
1166
|
+
it('52. headerBackLabel is the accessible name only, never visible text', async () => {
|
|
1070
1167
|
const el = await header({ pageTitle: 'Applications', leading: 'back' });
|
|
1071
|
-
|
|
1072
|
-
expect(
|
|
1073
|
-
|
|
1168
|
+
const back = part(el, 'back');
|
|
1169
|
+
expect(back.getAttribute('aria-label')).to.equal('Back');
|
|
1170
|
+
expect(back.getAttribute('title')).to.equal('Back');
|
|
1171
|
+
expect(back.querySelector('nile-glyph').getAttribute('aria-hidden')).to.not.equal('false');
|
|
1172
|
+
el.headerBackLabel = 'Integrations';
|
|
1074
1173
|
await el.updateComplete;
|
|
1075
|
-
expect(
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1174
|
+
expect(back.getAttribute('aria-label')).to.equal('Integrations');
|
|
1175
|
+
// Not rendered anywhere in the header as text.
|
|
1176
|
+
expect(back.textContent.trim()).to.equal('');
|
|
1177
|
+
expect(part(el, 'title-area').textContent.includes('Integrations')).to.be.false;
|
|
1178
|
+
expect(part(el, 'top-bar').textContent.includes('Integrations')).to.be.false;
|
|
1179
|
+
});
|
|
1180
|
+
// The shell owns no history, so the control only announces the press, as the standard nile-click; the page navigates itself.
|
|
1181
|
+
it('53. the back control and goBackHeader() emit nile-click { value: "back" } from the shell', async () => {
|
|
1079
1182
|
const el = await header({ pageTitle: 'Applications', leading: 'back' });
|
|
1080
|
-
|
|
1183
|
+
let retired = 0;
|
|
1184
|
+
el.addEventListener('nile-header-back', () => {
|
|
1185
|
+
retired += 1;
|
|
1186
|
+
});
|
|
1187
|
+
const clicked = oneEvent(el, 'nile-click');
|
|
1081
1188
|
part(el, 'back').click();
|
|
1082
|
-
const fromClick = await clicked;
|
|
1083
|
-
expect(fromClick).to.
|
|
1084
|
-
|
|
1189
|
+
const fromClick = (await clicked);
|
|
1190
|
+
expect(fromClick.detail).to.deep.equal({ value: 'back' });
|
|
1191
|
+
expect(fromClick.target).to.equal(el);
|
|
1192
|
+
expect(fromClick.bubbles).to.be.true;
|
|
1193
|
+
expect(fromClick.composed).to.be.true;
|
|
1194
|
+
const called = oneEvent(el, 'nile-click');
|
|
1085
1195
|
el.goBackHeader();
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
expect(
|
|
1196
|
+
const fromCall = (await called);
|
|
1197
|
+
expect(fromCall.detail).to.deep.equal({ value: 'back' });
|
|
1198
|
+
expect(fromCall.target).to.equal(el);
|
|
1199
|
+
// nile-header-back and goBackPage() are gone in 2.0.12 (Amendment A, A-12).
|
|
1200
|
+
expect(retired).to.equal(0);
|
|
1201
|
+
expect(el.goBackPage).to.be.undefined;
|
|
1091
1202
|
// It is the page's event, not the panel's: nothing about the panel moves.
|
|
1092
1203
|
expect(el.panelOpen).to.be.false;
|
|
1093
1204
|
});
|
|
1094
|
-
//
|
|
1095
|
-
it('
|
|
1096
|
-
|
|
1205
|
+
// A nile-click bubbling up from page content must not read as the header back press.
|
|
1206
|
+
it('53b. a nile-click from slotted content is distinguishable by its target', async () => {
|
|
1207
|
+
const el = await header({ pageTitle: 'Applications', leading: 'back' });
|
|
1208
|
+
const page = el.querySelector('p');
|
|
1209
|
+
const seen = [];
|
|
1210
|
+
el.addEventListener('nile-click', e => seen.push(e.target));
|
|
1211
|
+
page.dispatchEvent(new CustomEvent('nile-click', {
|
|
1212
|
+
bubbles: true,
|
|
1213
|
+
composed: true,
|
|
1214
|
+
detail: { value: 'back' },
|
|
1215
|
+
}));
|
|
1216
|
+
part(el, 'back').click();
|
|
1217
|
+
expect(seen).to.deep.equal([page, el]);
|
|
1218
|
+
});
|
|
1219
|
+
// The four combinations are independent; with an app icon the description sits under the icon, otherwise under the title.
|
|
1220
|
+
it('63. aligns the description under the app icon when there is one, else the title', async () => {
|
|
1221
|
+
for (const leading of LEADINGS) {
|
|
1097
1222
|
const el = await header({
|
|
1098
1223
|
pageTitle: 'Applications',
|
|
1099
1224
|
description: 'Manage the apps connected to this tenant',
|
|
1100
1225
|
leading,
|
|
1101
1226
|
});
|
|
1102
|
-
const
|
|
1227
|
+
const anchor = hasIcon(leading) ? part(el, 'app-icon') : part(el, 'page-title');
|
|
1103
1228
|
const description = part(el, 'description').getBoundingClientRect();
|
|
1104
|
-
expect(Math.round(description.left), leading).to.equal(Math.round(
|
|
1229
|
+
expect(Math.round(description.left), leading).to.equal(Math.round(anchor.getBoundingClientRect().left));
|
|
1105
1230
|
}
|
|
1106
1231
|
});
|
|
1107
|
-
//
|
|
1108
|
-
it('64. renders
|
|
1109
|
-
|
|
1110
|
-
|
|
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);
|
|
1232
|
+
// The arrow sits beside the stack; the app icon is the first thing in the title row.
|
|
1233
|
+
it('64. renders the arrow beside the title stack and the app icon inside the title row', async () => {
|
|
1234
|
+
for (const leading of LEADINGS) {
|
|
1235
|
+
const el = await header({ pageTitle: 'Applications', leading });
|
|
1119
1236
|
const row = el.shadowRoot.querySelector('.title-row');
|
|
1120
1237
|
const area = part(el, 'title-area');
|
|
1121
|
-
|
|
1122
|
-
|
|
1238
|
+
const back = part(el, 'back');
|
|
1239
|
+
const icon = part(el, 'app-icon');
|
|
1240
|
+
expect(!!back, `${leading} back`).to.equal(hasBack(leading));
|
|
1241
|
+
expect(el.hasAttribute('has-app-icon'), `${leading} icon`).to.equal(hasIcon(leading));
|
|
1242
|
+
if (back) {
|
|
1243
|
+
expect(row.contains(back), leading).to.be.false;
|
|
1244
|
+
expect(area.contains(back), leading).to.be.true;
|
|
1245
|
+
}
|
|
1246
|
+
expect(row.contains(icon), leading).to.be.true;
|
|
1247
|
+
if (hasIcon(leading)) {
|
|
1248
|
+
expect(row.firstElementChild, leading).to.equal(icon);
|
|
1249
|
+
}
|
|
1123
1250
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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']) {
|
|
1251
|
+
});
|
|
1252
|
+
// The arrow sits on the title's row, and its offset does not move when a description appears.
|
|
1253
|
+
it('65. sits the back arrow on the title row, description or not', async () => {
|
|
1254
|
+
for (const leading of ['back', 'back+app-icon']) {
|
|
1132
1255
|
for (const description of ['', 'Manage the apps connected here']) {
|
|
1133
1256
|
const el = await header({
|
|
1134
1257
|
pageTitle: 'Applications',
|
|
@@ -1136,7 +1259,7 @@ describe('NileAppShell — the header title block', () => {
|
|
|
1136
1259
|
description,
|
|
1137
1260
|
});
|
|
1138
1261
|
const title = part(el, 'page-title').getBoundingClientRect();
|
|
1139
|
-
const box = el.shadowRoot.querySelector('.
|
|
1262
|
+
const box = el.shadowRoot.querySelector('.back-line');
|
|
1140
1263
|
const label = `${leading} description=${!!description}`;
|
|
1141
1264
|
// The line box is the title's own, and it starts where the title does.
|
|
1142
1265
|
expect(Math.round(box.getBoundingClientRect().top), label).to.equal(Math.round(title.top));
|
|
@@ -1144,6 +1267,101 @@ describe('NileAppShell — the header title block', () => {
|
|
|
1144
1267
|
}
|
|
1145
1268
|
}
|
|
1146
1269
|
});
|
|
1270
|
+
// Figma Title Area: arrow → 16px → [icon → 8px → title]; Breadcrumb / 01: toggle glyph → 16px → divider (16px tall) → 16px.
|
|
1271
|
+
it('65b. spaces the toggle, divider, arrow, icon and title as the design does', async () => {
|
|
1272
|
+
const el = await header({ pageTitle: 'Applications', leading: 'back+app-icon' });
|
|
1273
|
+
const toggle = part(el, 'nav-toggle');
|
|
1274
|
+
const glyph = toggle.querySelector('nile-glyph') ?? toggle;
|
|
1275
|
+
const rule = part(el, 'rule').getBoundingClientRect();
|
|
1276
|
+
const backGlyph = part(el, 'back').querySelector('nile-glyph').getBoundingClientRect();
|
|
1277
|
+
const icon = part(el, 'app-icon').getBoundingClientRect();
|
|
1278
|
+
const title = part(el, 'page-title').getBoundingClientRect();
|
|
1279
|
+
const toggleGlyph = glyph.getBoundingClientRect();
|
|
1280
|
+
expect(Math.round(rule.height)).to.equal(16);
|
|
1281
|
+
expect(Math.round(rule.left - toggleGlyph.right)).to.equal(16);
|
|
1282
|
+
expect(Math.round(backGlyph.left - rule.left)).to.equal(16);
|
|
1283
|
+
expect(Math.round(backGlyph.width)).to.equal(20);
|
|
1284
|
+
expect(Math.round(icon.left - backGlyph.right)).to.equal(16);
|
|
1285
|
+
expect(Math.round(title.left - icon.right)).to.equal(8);
|
|
1286
|
+
// Without an icon the title takes the icon's place, 16px after the arrow.
|
|
1287
|
+
const plain = await header({ pageTitle: 'Applications', leading: 'back' });
|
|
1288
|
+
const plainArrow = part(plain, 'back').querySelector('nile-glyph').getBoundingClientRect();
|
|
1289
|
+
const plainTitle = part(plain, 'page-title').getBoundingClientRect();
|
|
1290
|
+
expect(Math.round(plainTitle.left - plainArrow.right)).to.equal(16);
|
|
1291
|
+
});
|
|
1292
|
+
// The app-icon slot works with or without headerBack, and with or without a title.
|
|
1293
|
+
it('65c. shows a slotted app icon independently of headerBack', async () => {
|
|
1294
|
+
const iconOnly = await header({ pageTitle: 'Applications', leading: 'app-icon' });
|
|
1295
|
+
expect(part(iconOnly, 'back')).to.be.null;
|
|
1296
|
+
expect(part(iconOnly, 'app-icon').getBoundingClientRect().width).to.equal(20);
|
|
1297
|
+
// An icon on its own still mounts the title block.
|
|
1298
|
+
const bare = await fixture(html `
|
|
1299
|
+
<nile-app-shell style="width: 1176px; height: 800px; ${HEADER_TOKENS}">
|
|
1300
|
+
<nav slot="side-nav">nav</nav>
|
|
1301
|
+
<span slot="app-icon" id="mark">A</span>
|
|
1302
|
+
<p>page</p>
|
|
1303
|
+
</nile-app-shell>
|
|
1304
|
+
`);
|
|
1305
|
+
await waitUntil(() => bare.hasAttribute('has-app-icon'));
|
|
1306
|
+
await bare.updateComplete;
|
|
1307
|
+
expect(part(bare, 'title-area')).to.exist;
|
|
1308
|
+
expect(part(bare, 'app-icon').getBoundingClientRect().width).to.equal(20);
|
|
1309
|
+
// Removing the mark takes the icon (and the block it alone mounted) away again.
|
|
1310
|
+
bare.querySelector('#mark').remove();
|
|
1311
|
+
await waitUntil(() => !bare.hasAttribute('has-app-icon'));
|
|
1312
|
+
await bare.updateComplete;
|
|
1313
|
+
expect(part(bare, 'title-area')).to.be.null;
|
|
1314
|
+
});
|
|
1315
|
+
// Amendment A (M11): the inactive tab colour is scoped to the shell's strip; a standalone group keeps its defaults.
|
|
1316
|
+
it('65e. colours the inactive header tabs through the shell only', async () => {
|
|
1317
|
+
const colours = [
|
|
1318
|
+
'--ng-colors-fg-brand-primary-alt: rgb(21, 94, 239)',
|
|
1319
|
+
'--ng-colors-text-brand-secondary-700: rgb(0, 78, 235)',
|
|
1320
|
+
'--ng-colors-text-quaternary-500: rgb(113, 118, 128)',
|
|
1321
|
+
'--ng-colors-text-secondary-700: rgb(65, 70, 81)',
|
|
1322
|
+
].join('; ');
|
|
1323
|
+
const el = await header({ pageTitle: 'Applications', tabs: true });
|
|
1324
|
+
el.setAttribute('style', `${el.getAttribute('style')}; ${colours}`);
|
|
1325
|
+
await el.updateComplete;
|
|
1326
|
+
const group = el.querySelector('nile-nav-tab-group');
|
|
1327
|
+
const inactive = el.querySelectorAll('nile-nav-tab')[1];
|
|
1328
|
+
// Known limitation (M12, A-7): the shell sets no indicator colour, because nile-nav-tab-group's top indicator
|
|
1329
|
+
// hard-codes its token in its own shadow tree; the indicator keeps the group's default.
|
|
1330
|
+
const indicatorOf = (g) => getComputedStyle(g.shadowRoot.querySelector('.nav-tab-group__indicator'))
|
|
1331
|
+
.borderBottomColor;
|
|
1332
|
+
expect(indicatorOf(group)).to.equal('rgb(0, 78, 235)');
|
|
1333
|
+
// The inactive label is remapped inside the shell only: nile-nav-tab itself is unchanged and still paints
|
|
1334
|
+
// var(--nile-colors-dark-500, var(--ng-colors-text-secondary-700)); the shell points that fallback token at quaternary-500.
|
|
1335
|
+
// (The painted ink sits inside nile-link, which this runner does not render; it is checked in the browser instead.)
|
|
1336
|
+
const secondaryOf = (node) => getComputedStyle(node).getPropertyValue('--ng-colors-text-secondary-700').trim();
|
|
1337
|
+
expect(secondaryOf(inactive)).to.equal('rgb(113, 118, 128)');
|
|
1338
|
+
const sheets = customElements.get('nile-nav-tab').styles;
|
|
1339
|
+
const tabStyles = [].concat(sheets).map(sheet => sheet.cssText).join('\n');
|
|
1340
|
+
expect(tabStyles).to.include('color: var(--nile-colors-dark-500, var(--ng-colors-text-secondary-700));');
|
|
1341
|
+
expect(tabStyles, 'no new hook on the shared component').to.not.include('--nile-nav-tab-text-color:');
|
|
1342
|
+
// Nothing else in the shell's tree is remapped.
|
|
1343
|
+
expect(secondaryOf(el)).to.equal('rgb(65, 70, 81)');
|
|
1344
|
+
// Outside the shell nothing changes.
|
|
1345
|
+
const standalone = await fixture(html `
|
|
1346
|
+
<nile-nav-tab-group style="${colours}">
|
|
1347
|
+
<nile-nav-tab slot="nav" panel="apps" active>Connected</nile-nav-tab>
|
|
1348
|
+
<nile-nav-tab slot="nav" panel="approvals">Approvals</nile-nav-tab>
|
|
1349
|
+
</nile-nav-tab-group>
|
|
1350
|
+
`);
|
|
1351
|
+
const loneTab = standalone.querySelectorAll('nile-nav-tab')[1];
|
|
1352
|
+
expect(secondaryOf(loneTab), 'the global default is untouched').to.equal('rgb(65, 70, 81)');
|
|
1353
|
+
await standalone.updateComplete;
|
|
1354
|
+
expect(indicatorOf(standalone), 'a standalone group paints its own default').to.equal('rgb(0, 78, 235)');
|
|
1355
|
+
});
|
|
1356
|
+
it('65d. colours the description text-quaternary-500', async () => {
|
|
1357
|
+
const el = await header({
|
|
1358
|
+
pageTitle: 'Applications',
|
|
1359
|
+
description: 'Manage the apps connected here',
|
|
1360
|
+
});
|
|
1361
|
+
el.style.setProperty('--ng-colors-text-quaternary-500', 'rgb(113, 118, 128)');
|
|
1362
|
+
el.style.setProperty('--ng-colors-text-tertiary-600', 'rgb(1, 2, 3)');
|
|
1363
|
+
expect(getComputedStyle(part(el, 'description')).color).to.equal('rgb(113, 118, 128)');
|
|
1364
|
+
});
|
|
1147
1365
|
it('66. centres a 20px app icon in the 24px title row, two pixels down', async () => {
|
|
1148
1366
|
const el = await header({
|
|
1149
1367
|
pageTitle: 'Applications',
|
|
@@ -1169,7 +1387,7 @@ describe('NileAppShell — the header title block', () => {
|
|
|
1169
1387
|
const base = toggle.shadowRoot.querySelector('[part~="base"]');
|
|
1170
1388
|
expect(base.getBoundingClientRect().width).to.equal(32);
|
|
1171
1389
|
});
|
|
1172
|
-
it('54.
|
|
1390
|
+
it('54. a slotted app icon draws a 20px block around the mark', async () => {
|
|
1173
1391
|
const el = await header({ pageTitle: 'Applications', leading: 'app-icon' });
|
|
1174
1392
|
const box = part(el, 'app-icon');
|
|
1175
1393
|
expect(box).to.exist;
|
|
@@ -1212,7 +1430,7 @@ describe('NileAppShell — the four header heights', () => {
|
|
|
1212
1430
|
return bottom - top;
|
|
1213
1431
|
};
|
|
1214
1432
|
it('55. is 68px with neither axis on, whatever the leading', async () => {
|
|
1215
|
-
for (const leading of
|
|
1433
|
+
for (const leading of LEADINGS) {
|
|
1216
1434
|
const el = await header({ pageTitle: 'Applications', leading });
|
|
1217
1435
|
expect(bar(el).getBoundingClientRect().height, leading).to.equal(68);
|
|
1218
1436
|
// The leading axis changes what is in the bar, never how tall it is.
|
|
@@ -1250,8 +1468,8 @@ describe('NileAppShell — the four header heights', () => {
|
|
|
1250
1468
|
expect(headerBlock(described)).to.equal(109);
|
|
1251
1469
|
});
|
|
1252
1470
|
// The three axes are independent, so all twelve combinations land on one of the four heights.
|
|
1253
|
-
it('58. resolves
|
|
1254
|
-
for (const leading of
|
|
1471
|
+
it('58. resolves every variant to exactly the four heights', async () => {
|
|
1472
|
+
for (const leading of LEADINGS) {
|
|
1255
1473
|
for (const tabs of [false, true]) {
|
|
1256
1474
|
for (const described of [false, true]) {
|
|
1257
1475
|
/* eslint-disable-next-line no-nested-ternary */
|