@aquera/nile-elements 2.0.11 → 2.0.13
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 +7 -0
- package/demo/app-shell.html +21 -21
- package/dist/index.js +170 -167
- 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/nile-select/nile-select.cjs.js +2 -2
- package/dist/nile-select/nile-select.cjs.js.map +1 -1
- package/dist/nile-select/nile-select.css.cjs.js +1 -1
- package/dist/nile-select/nile-select.css.cjs.js.map +1 -1
- package/dist/nile-select/nile-select.css.esm.js +12 -17
- package/dist/nile-select/nile-select.esm.js +43 -42
- package/dist/nile-select/virtual-scroll-helper.cjs.js +1 -1
- package/dist/nile-select/virtual-scroll-helper.cjs.js.map +1 -1
- package/dist/nile-select/virtual-scroll-helper.esm.js +2 -0
- package/dist/nile-virtual-select/nile-virtual-select.cjs.js +4 -4
- package/dist/nile-virtual-select/nile-virtual-select.cjs.js.map +1 -1
- package/dist/nile-virtual-select/nile-virtual-select.css.cjs.js +1 -1
- package/dist/nile-virtual-select/nile-virtual-select.css.cjs.js.map +1 -1
- package/dist/nile-virtual-select/nile-virtual-select.css.esm.js +12 -17
- package/dist/nile-virtual-select/nile-virtual-select.esm.js +26 -26
- 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/nile-select/nile-select.css.js +12 -17
- package/dist/src/nile-select/nile-select.css.js.map +1 -1
- package/dist/src/nile-select/nile-select.d.ts +18 -2
- package/dist/src/nile-select/nile-select.js +226 -64
- package/dist/src/nile-select/nile-select.js.map +1 -1
- package/dist/src/nile-select/nile-select.test.d.ts +1 -0
- package/dist/src/nile-select/nile-select.test.js +896 -17
- package/dist/src/nile-select/nile-select.test.js.map +1 -1
- package/dist/src/nile-select/virtual-scroll-helper.js +2 -0
- package/dist/src/nile-select/virtual-scroll-helper.js.map +1 -1
- package/dist/src/nile-virtual-select/nile-virtual-select.css.js +12 -17
- package/dist/src/nile-virtual-select/nile-virtual-select.css.js.map +1 -1
- package/dist/src/nile-virtual-select/nile-virtual-select.d.ts +10 -2
- package/dist/src/nile-virtual-select/nile-virtual-select.js +142 -30
- package/dist/src/nile-virtual-select/nile-virtual-select.js.map +1 -1
- package/dist/src/nile-virtual-select/nile-virtual-select.test.d.ts +1 -0
- package/dist/src/nile-virtual-select/nile-virtual-select.test.js +331 -16
- package/dist/src/nile-virtual-select/nile-virtual-select.test.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/src/nile-select/nile-select.css.ts +12 -17
- package/src/nile-select/nile-select.test.ts +1112 -17
- package/src/nile-select/nile-select.ts +237 -67
- package/src/nile-select/virtual-scroll-helper.ts +2 -0
- package/src/nile-virtual-select/nile-virtual-select.css.ts +12 -17
- package/src/nile-virtual-select/nile-virtual-select.test.ts +404 -16
- package/src/nile-virtual-select/nile-virtual-select.ts +200 -79
- package/vscode-html-custom-data.json +36 -41
|
@@ -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';
|
|
@@ -270,6 +271,51 @@ describe('NileAppShell — Ask Aquera panel', () => {
|
|
|
270
271
|
expect(part(el, 'panel-title')!.textContent!.trim()).to.equal('Ask Aquera');
|
|
271
272
|
});
|
|
272
273
|
|
|
274
|
+
// Amendment A (A-8): Figma's Assistant Title is Text lg/Semibold, 18/28; the shell sets it on the title it renders itself.
|
|
275
|
+
it('17b. sets its own panel title at 18/28 in text-primary-900', async () => {
|
|
276
|
+
const tokens =
|
|
277
|
+
'--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)';
|
|
278
|
+
const el = await sized(1440);
|
|
279
|
+
el.setAttribute('style', `${el.getAttribute('style')}; ${tokens}`);
|
|
280
|
+
el.openPanel();
|
|
281
|
+
await el.updateComplete;
|
|
282
|
+
const own = getComputedStyle(part(el, 'panel-title')!);
|
|
283
|
+
expect(own.fontSize).to.equal('18px');
|
|
284
|
+
expect(own.lineHeight).to.equal('28px');
|
|
285
|
+
expect(own.fontWeight).to.equal('600');
|
|
286
|
+
expect(own.color).to.equal('rgb(24, 29, 39)');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// A-8: a slotted nile-app-shell-panel (part of App Shell) draws the same Figma title: 18/28 SemiBold, text-primary-900.
|
|
290
|
+
it('17c. sets the nile-app-shell-panel title at 18/28 in text-primary-900', async () => {
|
|
291
|
+
const tokens =
|
|
292
|
+
'--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)';
|
|
293
|
+
const slotted = await fixture<HTMLElement>(html`
|
|
294
|
+
<nile-app-shell-panel label="Ask Aquera" style="${tokens}"></nile-app-shell-panel>
|
|
295
|
+
`);
|
|
296
|
+
await (slotted as unknown as { updateComplete: Promise<unknown> }).updateComplete;
|
|
297
|
+
const style = getComputedStyle(slotted.shadowRoot!.querySelector('.title') as HTMLElement);
|
|
298
|
+
expect(style.fontSize).to.equal('18px');
|
|
299
|
+
expect(style.lineHeight).to.equal('28px');
|
|
300
|
+
expect(style.fontWeight).to.equal('600');
|
|
301
|
+
expect(style.color).to.equal('rgb(24, 29, 39)');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// The enterprise sheet keeps its own panel title: --nile-type-scale-3 and an inherited line height, exactly as before.
|
|
305
|
+
// Enterprise defines no --ng-* tokens, so the NxtGen line height does not apply there.
|
|
306
|
+
it('17d. leaves the enterprise nile-app-shell-panel title as it was', async () => {
|
|
307
|
+
const slotted = await fixture<HTMLElement>(html`
|
|
308
|
+
<nile-app-shell-panel
|
|
309
|
+
label="Ask Aquera"
|
|
310
|
+
style="--nile-type-scale-3: 14px; line-height: 20px"
|
|
311
|
+
></nile-app-shell-panel>
|
|
312
|
+
`);
|
|
313
|
+
await (slotted as unknown as { updateComplete: Promise<unknown> }).updateComplete;
|
|
314
|
+
const style = getComputedStyle(slotted.shadowRoot!.querySelector('.title') as HTMLElement);
|
|
315
|
+
expect(style.fontSize).to.equal('14px');
|
|
316
|
+
expect(style.lineHeight).to.equal('20px');
|
|
317
|
+
});
|
|
318
|
+
|
|
273
319
|
it('18. Cmd/Ctrl+J toggles the panel', async () => {
|
|
274
320
|
const el = await sized(1440);
|
|
275
321
|
document.dispatchEvent(
|
|
@@ -690,6 +736,53 @@ describe('NileAppShell — the built-in Ask button', () => {
|
|
|
690
736
|
expect(actions.getBoundingClientRect().width).to.be.greaterThan(0);
|
|
691
737
|
});
|
|
692
738
|
|
|
739
|
+
// Amendment A (M8): the design's Ask is the secondary button, not the filled primary.
|
|
740
|
+
it('34b. renders Ask as the secondary variant', async () => {
|
|
741
|
+
const el = await sized(1440);
|
|
742
|
+
expect(ask(el)!.getAttribute('variant')).to.equal('secondary');
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// Amendment A (M9, M10): app actions, a 16px divider, then Ask, 16px apart; with no app actions, no divider.
|
|
746
|
+
it('34c. draws a divider before Ask only when the page slots actions, 16px from both', async () => {
|
|
747
|
+
const alone = await sized(1440);
|
|
748
|
+
await settled(alone);
|
|
749
|
+
expect(part(alone, 'ask-rule')).to.be.null;
|
|
750
|
+
|
|
751
|
+
const el = await fixture<NileAppShell>(html`
|
|
752
|
+
<nile-app-shell style="width: 1440px; height: 600px; --ng-spacing-xl: 16px;">
|
|
753
|
+
<nav slot="side-nav">nav</nav>
|
|
754
|
+
<span slot="top-bar">Title</span>
|
|
755
|
+
<button slot="top-bar-actions" id="one">One</button>
|
|
756
|
+
<button slot="top-bar-actions" id="two">Two</button>
|
|
757
|
+
<p>page</p>
|
|
758
|
+
<div slot="panel">assistant</div>
|
|
759
|
+
</nile-app-shell>
|
|
760
|
+
`);
|
|
761
|
+
await waitUntil(() => el.hasAttribute('has-top-bar-actions'));
|
|
762
|
+
await el.updateComplete;
|
|
763
|
+
|
|
764
|
+
const rule = part(el, 'ask-rule')!;
|
|
765
|
+
expect(rule).to.exist;
|
|
766
|
+
const one = el.querySelector('#one')!.getBoundingClientRect();
|
|
767
|
+
const two = el.querySelector('#two')!.getBoundingClientRect();
|
|
768
|
+
const ruleBox = rule.getBoundingClientRect();
|
|
769
|
+
const askBox = ask(el)!.getBoundingClientRect();
|
|
770
|
+
|
|
771
|
+
expect(Math.round(ruleBox.height)).to.equal(16);
|
|
772
|
+
expect(Math.round(two.left - one.right)).to.equal(16);
|
|
773
|
+
expect(Math.round(ruleBox.left - two.right)).to.equal(16);
|
|
774
|
+
expect(Math.round(askBox.left - ruleBox.left)).to.equal(16);
|
|
775
|
+
|
|
776
|
+
// Order: the page's actions, the divider, then Ask.
|
|
777
|
+
/* eslint-disable-next-line no-bitwise */
|
|
778
|
+
const ruleBeforeAsk = rule.compareDocumentPosition(ask(el)!) & Node.DOCUMENT_POSITION_FOLLOWING;
|
|
779
|
+
expect(ruleBeforeAsk).to.be.greaterThan(0);
|
|
780
|
+
|
|
781
|
+
el.noAsk = true;
|
|
782
|
+
await el.updateComplete;
|
|
783
|
+
expect(part(el, 'ask-rule'), 'no Ask, no divider').to.be.null;
|
|
784
|
+
});
|
|
785
|
+
|
|
693
786
|
// The shell draws its icon controls as bare glyphs via ::part(base), overriding nile-button's circle and ghost surface.
|
|
694
787
|
const utilityBase = (el: NileAppShell, name: string) =>
|
|
695
788
|
part(el, name)!.shadowRoot!.querySelector('[part~="base"]') as HTMLElement;
|
|
@@ -932,6 +1025,7 @@ const HEADER_TOKENS = [
|
|
|
932
1025
|
'--ng-spacing-xxs: 2px',
|
|
933
1026
|
'--ng-spacing-xs: 4px',
|
|
934
1027
|
'--ng-spacing-md: 8px',
|
|
1028
|
+
'--ng-spacing-md-alt: 10px',
|
|
935
1029
|
'--ng-spacing-lg: 12px',
|
|
936
1030
|
'--ng-spacing-xl: 16px',
|
|
937
1031
|
'--ng-spacing-3xl: 24px',
|
|
@@ -943,15 +1037,23 @@ const HEADER_TOKENS = [
|
|
|
943
1037
|
'--ng-line-height-text-md: 24px',
|
|
944
1038
|
].join('; ');
|
|
945
1039
|
|
|
946
|
-
|
|
1040
|
+
// What sits before the title: `headerBack` and the `app-icon` slot are independent, so the four combinations are named here for looping.
|
|
1041
|
+
type Leading = 'none' | 'back' | 'app-icon' | 'back+app-icon';
|
|
1042
|
+
|
|
1043
|
+
const LEADINGS: readonly Leading[] = ['none', 'back', 'app-icon', 'back+app-icon'];
|
|
1044
|
+
|
|
1045
|
+
const hasBack = (leading?: Leading) => leading === 'back' || leading === 'back+app-icon';
|
|
1046
|
+
const hasIcon = (leading?: Leading) =>
|
|
1047
|
+
leading === 'app-icon' || leading === 'back+app-icon';
|
|
947
1048
|
|
|
948
1049
|
interface HeaderAxes {
|
|
949
1050
|
pageTitle?: string;
|
|
950
1051
|
description?: string;
|
|
951
1052
|
badge?: string;
|
|
952
1053
|
leading?: Leading;
|
|
953
|
-
|
|
1054
|
+
headerBackLabel?: string;
|
|
954
1055
|
tabs?: boolean;
|
|
1056
|
+
actions?: boolean;
|
|
955
1057
|
}
|
|
956
1058
|
|
|
957
1059
|
// A shell carrying whatever header axes a test asks for; an unset axis is the empty string, meaning "absent".
|
|
@@ -962,13 +1064,16 @@ const header = async (axes: HeaderAxes = {}) => {
|
|
|
962
1064
|
.pageTitle=${axes.pageTitle ?? ''}
|
|
963
1065
|
.description=${axes.description ?? ''}
|
|
964
1066
|
.badge=${axes.badge ?? ''}
|
|
965
|
-
|
|
966
|
-
.
|
|
1067
|
+
.headerBack=${hasBack(axes.leading)}
|
|
1068
|
+
.headerBackLabel=${axes.headerBackLabel ?? 'Back'}
|
|
967
1069
|
>
|
|
968
1070
|
<nav slot="side-nav">nav</nav>
|
|
969
|
-
${axes.leading
|
|
1071
|
+
${hasIcon(axes.leading)
|
|
970
1072
|
? html`<span slot="app-icon" id="mark">A</span>`
|
|
971
1073
|
: ''}
|
|
1074
|
+
${axes.actions
|
|
1075
|
+
? html`<button slot="top-bar-actions" id="cta">Button CTA</button>`
|
|
1076
|
+
: ''}
|
|
972
1077
|
${axes.tabs
|
|
973
1078
|
? html`<nile-nav-tab-group slot="tabs">
|
|
974
1079
|
<nile-nav-tab slot="nav" panel="apps" active>Connected</nile-nav-tab>
|
|
@@ -979,6 +1084,8 @@ const header = async (axes: HeaderAxes = {}) => {
|
|
|
979
1084
|
</nile-app-shell>
|
|
980
1085
|
`);
|
|
981
1086
|
if (axes.tabs) await waitUntil(() => el.hasAttribute('has-tabs'));
|
|
1087
|
+
if (hasIcon(axes.leading)) await waitUntil(() => el.hasAttribute('has-app-icon'));
|
|
1088
|
+
if (axes.actions) await waitUntil(() => el.hasAttribute('has-top-bar-actions'));
|
|
982
1089
|
await el.updateComplete;
|
|
983
1090
|
return el;
|
|
984
1091
|
};
|
|
@@ -987,7 +1094,8 @@ const header = async (axes: HeaderAxes = {}) => {
|
|
|
987
1094
|
describe('NileAppShell — the header title block', () => {
|
|
988
1095
|
it('44. draws no title block at all until an axis asks for one', async () => {
|
|
989
1096
|
const el = await header();
|
|
990
|
-
expect(el.
|
|
1097
|
+
expect(el.headerBack).to.be.false;
|
|
1098
|
+
expect(el.hasAttribute('has-app-icon')).to.be.false;
|
|
991
1099
|
expect(part(el, 'title-area')).to.be.null;
|
|
992
1100
|
expect(part(el, 'page-title')).to.be.null;
|
|
993
1101
|
expect(part(el, 'description')).to.be.null;
|
|
@@ -1029,8 +1137,8 @@ describe('NileAppShell — the header title block', () => {
|
|
|
1029
1137
|
expect(el.hasAttribute('has-description')).to.be.true;
|
|
1030
1138
|
});
|
|
1031
1139
|
|
|
1032
|
-
//
|
|
1033
|
-
it('75. mounts
|
|
1140
|
+
// The back arrow is the exception: it stands on its own but brings no empty title stack.
|
|
1141
|
+
it('75. mounts the back arrow on its own, without an empty title stack', async () => {
|
|
1034
1142
|
const el = await header({ leading: 'back' });
|
|
1035
1143
|
|
|
1036
1144
|
expect(part(el, 'title-area')).to.exist;
|
|
@@ -1232,153 +1340,176 @@ describe('NileAppShell — the header title block', () => {
|
|
|
1232
1340
|
expect(part(el, 'description')).to.be.null;
|
|
1233
1341
|
});
|
|
1234
1342
|
|
|
1235
|
-
it('50.
|
|
1343
|
+
it('50. headerBack defaults to false and draws no arrow, and no slotted icon draws no icon', async () => {
|
|
1236
1344
|
const el = await header({ pageTitle: 'Applications' });
|
|
1237
|
-
expect(el.
|
|
1345
|
+
expect(el.headerBack).to.be.false;
|
|
1346
|
+
expect(el.hasAttribute('headerback')).to.be.false;
|
|
1238
1347
|
expect(part(el, 'back')).to.be.null;
|
|
1239
|
-
expect(
|
|
1348
|
+
expect(el.hasAttribute('has-app-icon')).to.be.false;
|
|
1349
|
+
expect(getComputedStyle(part(el, 'app-icon')!).display).to.equal('none');
|
|
1240
1350
|
expect(el.shadowRoot!.querySelector('.crumb-divider')).to.be.null;
|
|
1241
1351
|
});
|
|
1242
1352
|
|
|
1243
|
-
//
|
|
1244
|
-
it('51.
|
|
1353
|
+
// Amendment A: the back control is the arrow alone, with no previous-page label and no slash.
|
|
1354
|
+
it('51. headerBack draws the narrow-left arrow only, then the title', async () => {
|
|
1245
1355
|
const el = await header({ pageTitle: 'Applications', leading: 'back' });
|
|
1246
1356
|
const back = part(el, 'back')!;
|
|
1247
|
-
const divider = el.shadowRoot!.querySelector(
|
|
1248
|
-
'.crumb-divider'
|
|
1249
|
-
) as HTMLElement;
|
|
1250
1357
|
const title = part(el, 'page-title')!;
|
|
1251
1358
|
|
|
1252
1359
|
expect(back).to.exist;
|
|
1253
1360
|
expect(back.tagName).to.equal('BUTTON');
|
|
1254
1361
|
expect(back.getAttribute('type')).to.equal('button');
|
|
1255
|
-
expect(divider).to.exist;
|
|
1256
|
-
expect(divider.getAttribute('aria-hidden')).to.equal('true');
|
|
1257
1362
|
|
|
1258
1363
|
// The icons go through nile-glyph, so assert the glyph, not an <svg>.
|
|
1259
|
-
const
|
|
1260
|
-
expect(
|
|
1261
|
-
expect(
|
|
1262
|
-
expect(divider.querySelector('nile-glyph')!.getAttribute('name')).to.equal(
|
|
1263
|
-
'ng-slash'
|
|
1264
|
-
);
|
|
1364
|
+
const glyphs = back.querySelectorAll('nile-glyph');
|
|
1365
|
+
expect(glyphs.length).to.equal(1);
|
|
1366
|
+
expect(glyphs[0].getAttribute('name')).to.equal('ng-arrow-narrow-left');
|
|
1265
1367
|
|
|
1266
|
-
//
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
expect(glyphIsFirst).to.be.greaterThan(0);
|
|
1272
|
-
|
|
1273
|
-
// control, then divider, then title.
|
|
1274
|
-
for (const [first, second] of [
|
|
1275
|
-
[back, divider],
|
|
1276
|
-
[divider, title],
|
|
1277
|
-
]) {
|
|
1278
|
-
/* eslint-disable-next-line no-bitwise */
|
|
1279
|
-
const ordered =
|
|
1280
|
-
first.compareDocumentPosition(second) &
|
|
1281
|
-
Node.DOCUMENT_POSITION_FOLLOWING;
|
|
1282
|
-
expect(ordered).to.be.greaterThan(0);
|
|
1283
|
-
}
|
|
1368
|
+
// No label, no divider, no slash anywhere in the header.
|
|
1369
|
+
expect(back.textContent!.trim()).to.equal('');
|
|
1370
|
+
expect(back.querySelector('span')).to.be.null;
|
|
1371
|
+
expect(el.shadowRoot!.querySelector('.crumb-divider')).to.be.null;
|
|
1372
|
+
expect(el.shadowRoot!.querySelector('nile-glyph[name="ng-slash"]')).to.be.null;
|
|
1284
1373
|
|
|
1285
|
-
|
|
1374
|
+
/* eslint-disable-next-line no-bitwise */
|
|
1375
|
+
const backIsFirst =
|
|
1376
|
+
back.compareDocumentPosition(title) & Node.DOCUMENT_POSITION_FOLLOWING;
|
|
1377
|
+
expect(backIsFirst).to.be.greaterThan(0);
|
|
1286
1378
|
expect(back.getBoundingClientRect().width).to.be.greaterThan(0);
|
|
1287
|
-
expect(getComputedStyle(divider).display).to.not.equal('none');
|
|
1288
1379
|
});
|
|
1289
1380
|
|
|
1290
|
-
//
|
|
1291
|
-
it('51b.
|
|
1292
|
-
const
|
|
1381
|
+
// camelCase property, attribute: true + reflect: true — the title-block pattern.
|
|
1382
|
+
it('51b. headerBack and headerBackLabel work from markup and reflect to their attributes', async () => {
|
|
1383
|
+
const marked = await fixture<NileAppShell>(html`
|
|
1384
|
+
<nile-app-shell pageTitle="Applications" headerBack headerBackLabel="Retour">
|
|
1385
|
+
<p>page</p>
|
|
1386
|
+
</nile-app-shell>
|
|
1387
|
+
`);
|
|
1388
|
+
await marked.updateComplete;
|
|
1389
|
+
expect(marked.headerBack).to.be.true;
|
|
1390
|
+
expect(marked.headerBackLabel).to.equal('Retour');
|
|
1391
|
+
expect(part(marked, 'back')).to.exist;
|
|
1392
|
+
|
|
1393
|
+
const el = await header({ pageTitle: 'Applications' });
|
|
1394
|
+
el.headerBack = true;
|
|
1395
|
+
el.headerBackLabel = 'Go back';
|
|
1396
|
+
await el.updateComplete;
|
|
1397
|
+
expect(el.hasAttribute('headerback')).to.be.true;
|
|
1398
|
+
expect(el.getAttribute('headerbacklabel')).to.equal('Go back');
|
|
1293
1399
|
expect(part(el, 'back')).to.exist;
|
|
1294
|
-
expect(el.shadowRoot!.querySelector('.crumb-divider')).to.be.null;
|
|
1295
1400
|
|
|
1296
|
-
el.
|
|
1401
|
+
el.headerBack = false;
|
|
1297
1402
|
await el.updateComplete;
|
|
1298
|
-
expect(el.
|
|
1403
|
+
expect(el.hasAttribute('headerback')).to.be.false;
|
|
1404
|
+
expect(part(el, 'back')).to.be.null;
|
|
1299
1405
|
});
|
|
1300
1406
|
|
|
1301
|
-
|
|
1407
|
+
// headerBackLabel names the control for assistive tech and is never drawn as text.
|
|
1408
|
+
it('52. headerBackLabel is the accessible name only, never visible text', async () => {
|
|
1302
1409
|
const el = await header({ pageTitle: 'Applications', leading: 'back' });
|
|
1303
|
-
|
|
1304
|
-
expect(
|
|
1410
|
+
const back = part(el, 'back')!;
|
|
1411
|
+
expect(back.getAttribute('aria-label')).to.equal('Back');
|
|
1412
|
+
expect(back.getAttribute('title')).to.equal('Back');
|
|
1413
|
+
expect(back.querySelector('nile-glyph')!.getAttribute('aria-hidden')).to.not.equal('false');
|
|
1305
1414
|
|
|
1306
|
-
el.
|
|
1415
|
+
el.headerBackLabel = 'Integrations';
|
|
1307
1416
|
await el.updateComplete;
|
|
1308
|
-
expect(
|
|
1417
|
+
expect(back.getAttribute('aria-label')).to.equal('Integrations');
|
|
1418
|
+
// Not rendered anywhere in the header as text.
|
|
1419
|
+
expect(back.textContent!.trim()).to.equal('');
|
|
1420
|
+
expect(part(el, 'title-area')!.textContent!.includes('Integrations')).to.be.false;
|
|
1421
|
+
expect(part(el, 'top-bar')!.textContent!.includes('Integrations')).to.be.false;
|
|
1309
1422
|
});
|
|
1310
1423
|
|
|
1311
|
-
// The shell owns no history, so the control only announces the press; the page navigates itself.
|
|
1312
|
-
it('53. the back control
|
|
1424
|
+
// The shell owns no history, so the control only announces the press, as the standard nile-click; the page navigates itself.
|
|
1425
|
+
it('53. the back control and goBackHeader() emit nile-click { value: "back" } from the shell', async () => {
|
|
1313
1426
|
const el = await header({ pageTitle: 'Applications', leading: 'back' });
|
|
1427
|
+
let retired = 0;
|
|
1428
|
+
el.addEventListener('nile-header-back', () => {
|
|
1429
|
+
retired += 1;
|
|
1430
|
+
});
|
|
1314
1431
|
|
|
1315
|
-
const clicked = oneEvent(el, 'nile-
|
|
1432
|
+
const clicked = oneEvent(el, 'nile-click');
|
|
1316
1433
|
(part(el, 'back') as HTMLButtonElement).click();
|
|
1317
|
-
const fromClick = await clicked;
|
|
1318
|
-
expect(fromClick).to.
|
|
1434
|
+
const fromClick = (await clicked) as CustomEvent;
|
|
1435
|
+
expect(fromClick.detail).to.deep.equal({ value: 'back' });
|
|
1436
|
+
expect(fromClick.target).to.equal(el);
|
|
1437
|
+
expect(fromClick.bubbles).to.be.true;
|
|
1438
|
+
expect(fromClick.composed).to.be.true;
|
|
1319
1439
|
|
|
1320
|
-
const called = oneEvent(el, 'nile-
|
|
1440
|
+
const called = oneEvent(el, 'nile-click');
|
|
1321
1441
|
el.goBackHeader();
|
|
1322
|
-
|
|
1442
|
+
const fromCall = (await called) as CustomEvent;
|
|
1443
|
+
expect(fromCall.detail).to.deep.equal({ value: 'back' });
|
|
1444
|
+
expect(fromCall.target).to.equal(el);
|
|
1323
1445
|
|
|
1324
|
-
// goBackPage()
|
|
1325
|
-
|
|
1326
|
-
el.goBackPage
|
|
1327
|
-
expect(await aliased).to.exist;
|
|
1446
|
+
// nile-header-back and goBackPage() are gone in 2.0.12 (Amendment A, A-12).
|
|
1447
|
+
expect(retired).to.equal(0);
|
|
1448
|
+
expect((el as unknown as { goBackPage?: unknown }).goBackPage).to.be.undefined;
|
|
1328
1449
|
|
|
1329
1450
|
// It is the page's event, not the panel's: nothing about the panel moves.
|
|
1330
1451
|
expect(el.panelOpen).to.be.false;
|
|
1331
1452
|
});
|
|
1332
1453
|
|
|
1333
|
-
//
|
|
1334
|
-
it('
|
|
1335
|
-
|
|
1454
|
+
// A nile-click bubbling up from page content must not read as the header back press.
|
|
1455
|
+
it('53b. a nile-click from slotted content is distinguishable by its target', async () => {
|
|
1456
|
+
const el = await header({ pageTitle: 'Applications', leading: 'back' });
|
|
1457
|
+
const page = el.querySelector('p')!;
|
|
1458
|
+
const seen: EventTarget[] = [];
|
|
1459
|
+
el.addEventListener('nile-click', e => seen.push(e.target!));
|
|
1460
|
+
page.dispatchEvent(
|
|
1461
|
+
new CustomEvent('nile-click', {
|
|
1462
|
+
bubbles: true,
|
|
1463
|
+
composed: true,
|
|
1464
|
+
detail: { value: 'back' },
|
|
1465
|
+
})
|
|
1466
|
+
);
|
|
1467
|
+
(part(el, 'back') as HTMLButtonElement).click();
|
|
1468
|
+
expect(seen).to.deep.equal([page, el]);
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
// The four combinations are independent; with an app icon the description sits under the icon, otherwise under the title.
|
|
1472
|
+
it('63. aligns the description under the app icon when there is one, else the title', async () => {
|
|
1473
|
+
for (const leading of LEADINGS) {
|
|
1336
1474
|
const el = await header({
|
|
1337
1475
|
pageTitle: 'Applications',
|
|
1338
1476
|
description: 'Manage the apps connected to this tenant',
|
|
1339
1477
|
leading,
|
|
1340
1478
|
});
|
|
1341
1479
|
|
|
1342
|
-
const
|
|
1480
|
+
const anchor = hasIcon(leading) ? part(el, 'app-icon')! : part(el, 'page-title')!;
|
|
1343
1481
|
const description = part(el, 'description')!.getBoundingClientRect();
|
|
1344
1482
|
expect(Math.round(description.left), leading).to.equal(
|
|
1345
|
-
Math.round(
|
|
1483
|
+
Math.round(anchor.getBoundingClientRect().left)
|
|
1346
1484
|
);
|
|
1347
1485
|
}
|
|
1348
1486
|
});
|
|
1349
1487
|
|
|
1350
|
-
//
|
|
1351
|
-
it('64. renders
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
pageTitle: 'Applications',
|
|
1355
|
-
leading: 'app-icon',
|
|
1356
|
-
});
|
|
1357
|
-
|
|
1358
|
-
for (const [el, name] of [
|
|
1359
|
-
[backShell, 'back'],
|
|
1360
|
-
[iconShell, 'app-icon'],
|
|
1361
|
-
] as const) {
|
|
1362
|
-
const control = part(el, name)!;
|
|
1488
|
+
// The arrow sits beside the stack; the app icon is the first thing in the title row.
|
|
1489
|
+
it('64. renders the arrow beside the title stack and the app icon inside the title row', async () => {
|
|
1490
|
+
for (const leading of LEADINGS) {
|
|
1491
|
+
const el = await header({ pageTitle: 'Applications', leading });
|
|
1363
1492
|
const row = el.shadowRoot!.querySelector('.title-row') as HTMLElement;
|
|
1364
1493
|
const area = part(el, 'title-area')!;
|
|
1365
|
-
|
|
1366
|
-
|
|
1494
|
+
const back = part(el, 'back');
|
|
1495
|
+
const icon = part(el, 'app-icon')!;
|
|
1496
|
+
|
|
1497
|
+
expect(!!back, `${leading} back`).to.equal(hasBack(leading));
|
|
1498
|
+
expect(el.hasAttribute('has-app-icon'), `${leading} icon`).to.equal(hasIcon(leading));
|
|
1499
|
+
if (back) {
|
|
1500
|
+
expect(row.contains(back), leading).to.be.false;
|
|
1501
|
+
expect(area.contains(back), leading).to.be.true;
|
|
1502
|
+
}
|
|
1503
|
+
expect(row.contains(icon), leading).to.be.true;
|
|
1504
|
+
if (hasIcon(leading)) {
|
|
1505
|
+
expect(row.firstElementChild, leading).to.equal(icon);
|
|
1506
|
+
}
|
|
1367
1507
|
}
|
|
1368
|
-
|
|
1369
|
-
// The breadcrumb divider travels with the control it belongs to.
|
|
1370
|
-
const divider = backShell.shadowRoot!.querySelector(
|
|
1371
|
-
'.crumb-divider'
|
|
1372
|
-
) as HTMLElement;
|
|
1373
|
-
const row = backShell.shadowRoot!.querySelector(
|
|
1374
|
-
'.title-row'
|
|
1375
|
-
) as HTMLElement;
|
|
1376
|
-
expect(row.contains(divider)).to.be.false;
|
|
1377
1508
|
});
|
|
1378
1509
|
|
|
1379
|
-
// The
|
|
1380
|
-
it('65. sits the
|
|
1381
|
-
for (const leading of ['back', 'app-icon'] as const) {
|
|
1510
|
+
// The arrow sits on the title's row, and its offset does not move when a description appears.
|
|
1511
|
+
it('65. sits the back arrow on the title row, description or not', async () => {
|
|
1512
|
+
for (const leading of ['back', 'back+app-icon'] as const) {
|
|
1382
1513
|
for (const description of ['', 'Manage the apps connected here']) {
|
|
1383
1514
|
const el = await header({
|
|
1384
1515
|
pageTitle: 'Applications',
|
|
@@ -1387,9 +1518,7 @@ describe('NileAppShell — the header title block', () => {
|
|
|
1387
1518
|
});
|
|
1388
1519
|
|
|
1389
1520
|
const title = part(el, 'page-title')!.getBoundingClientRect();
|
|
1390
|
-
const box = el.shadowRoot!.querySelector(
|
|
1391
|
-
'.leading'
|
|
1392
|
-
) as HTMLElement;
|
|
1521
|
+
const box = el.shadowRoot!.querySelector('.back-line') as HTMLElement;
|
|
1393
1522
|
const label = `${leading} description=${!!description}`;
|
|
1394
1523
|
|
|
1395
1524
|
// The line box is the title's own, and it starts where the title does.
|
|
@@ -1403,6 +1532,120 @@ describe('NileAppShell — the header title block', () => {
|
|
|
1403
1532
|
}
|
|
1404
1533
|
});
|
|
1405
1534
|
|
|
1535
|
+
// Figma Title Area: arrow → 16px → [icon → 8px → title]; Breadcrumb / 01: toggle glyph → 16px → divider (16px tall) → 16px.
|
|
1536
|
+
it('65b. spaces the toggle, divider, arrow, icon and title as the design does', async () => {
|
|
1537
|
+
const el = await header({ pageTitle: 'Applications', leading: 'back+app-icon' });
|
|
1538
|
+
const toggle = part(el, 'nav-toggle')!;
|
|
1539
|
+
const glyph = toggle.querySelector('nile-glyph') ?? toggle;
|
|
1540
|
+
const rule = part(el, 'rule')!.getBoundingClientRect();
|
|
1541
|
+
const backGlyph = part(el, 'back')!.querySelector('nile-glyph')!.getBoundingClientRect();
|
|
1542
|
+
const icon = part(el, 'app-icon')!.getBoundingClientRect();
|
|
1543
|
+
const title = part(el, 'page-title')!.getBoundingClientRect();
|
|
1544
|
+
const toggleGlyph = glyph.getBoundingClientRect();
|
|
1545
|
+
|
|
1546
|
+
expect(Math.round(rule.height)).to.equal(16);
|
|
1547
|
+
expect(Math.round(rule.left - toggleGlyph.right)).to.equal(16);
|
|
1548
|
+
expect(Math.round(backGlyph.left - rule.left)).to.equal(16);
|
|
1549
|
+
expect(Math.round(backGlyph.width)).to.equal(20);
|
|
1550
|
+
expect(Math.round(icon.left - backGlyph.right)).to.equal(16);
|
|
1551
|
+
expect(Math.round(title.left - icon.right)).to.equal(8);
|
|
1552
|
+
|
|
1553
|
+
// Without an icon the title takes the icon's place, 16px after the arrow.
|
|
1554
|
+
const plain = await header({ pageTitle: 'Applications', leading: 'back' });
|
|
1555
|
+
const plainArrow = part(plain, 'back')!.querySelector('nile-glyph')!.getBoundingClientRect();
|
|
1556
|
+
const plainTitle = part(plain, 'page-title')!.getBoundingClientRect();
|
|
1557
|
+
expect(Math.round(plainTitle.left - plainArrow.right)).to.equal(16);
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
// The app-icon slot works with or without headerBack, and with or without a title.
|
|
1561
|
+
it('65c. shows a slotted app icon independently of headerBack', async () => {
|
|
1562
|
+
const iconOnly = await header({ pageTitle: 'Applications', leading: 'app-icon' });
|
|
1563
|
+
expect(part(iconOnly, 'back')).to.be.null;
|
|
1564
|
+
expect(part(iconOnly, 'app-icon')!.getBoundingClientRect().width).to.equal(20);
|
|
1565
|
+
|
|
1566
|
+
// An icon on its own still mounts the title block.
|
|
1567
|
+
const bare = await fixture<NileAppShell>(html`
|
|
1568
|
+
<nile-app-shell style="width: 1176px; height: 800px; ${HEADER_TOKENS}">
|
|
1569
|
+
<nav slot="side-nav">nav</nav>
|
|
1570
|
+
<span slot="app-icon" id="mark">A</span>
|
|
1571
|
+
<p>page</p>
|
|
1572
|
+
</nile-app-shell>
|
|
1573
|
+
`);
|
|
1574
|
+
await waitUntil(() => bare.hasAttribute('has-app-icon'));
|
|
1575
|
+
await bare.updateComplete;
|
|
1576
|
+
expect(part(bare, 'title-area')).to.exist;
|
|
1577
|
+
expect(part(bare, 'app-icon')!.getBoundingClientRect().width).to.equal(20);
|
|
1578
|
+
|
|
1579
|
+
// Removing the mark takes the icon (and the block it alone mounted) away again.
|
|
1580
|
+
bare.querySelector('#mark')!.remove();
|
|
1581
|
+
await waitUntil(() => !bare.hasAttribute('has-app-icon'));
|
|
1582
|
+
await bare.updateComplete;
|
|
1583
|
+
expect(part(bare, 'title-area')).to.be.null;
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
// Amendment A (M11): the inactive tab colour is scoped to the shell's strip; a standalone group keeps its defaults.
|
|
1587
|
+
it('65e. colours the inactive header tabs through the shell only', async () => {
|
|
1588
|
+
const colours = [
|
|
1589
|
+
'--ng-colors-fg-brand-primary-alt: rgb(21, 94, 239)',
|
|
1590
|
+
'--ng-colors-text-brand-secondary-700: rgb(0, 78, 235)',
|
|
1591
|
+
'--ng-colors-text-quaternary-500: rgb(113, 118, 128)',
|
|
1592
|
+
'--ng-colors-text-secondary-700: rgb(65, 70, 81)',
|
|
1593
|
+
].join('; ');
|
|
1594
|
+
const el = await header({ pageTitle: 'Applications', tabs: true });
|
|
1595
|
+
el.setAttribute('style', `${el.getAttribute('style')}; ${colours}`);
|
|
1596
|
+
await el.updateComplete;
|
|
1597
|
+
|
|
1598
|
+
const group = el.querySelector('nile-nav-tab-group') as HTMLElement;
|
|
1599
|
+
const inactive = el.querySelectorAll('nile-nav-tab')[1] as HTMLElement;
|
|
1600
|
+
// Known limitation (M12, A-7): the shell sets no indicator colour, because nile-nav-tab-group's top indicator
|
|
1601
|
+
// hard-codes its token in its own shadow tree; the indicator keeps the group's default.
|
|
1602
|
+
const indicatorOf = (g: HTMLElement) =>
|
|
1603
|
+
getComputedStyle(g.shadowRoot!.querySelector('.nav-tab-group__indicator') as HTMLElement)
|
|
1604
|
+
.borderBottomColor;
|
|
1605
|
+
expect(indicatorOf(group)).to.equal('rgb(0, 78, 235)');
|
|
1606
|
+
// The inactive label is remapped inside the shell only: nile-nav-tab itself is unchanged and still paints
|
|
1607
|
+
// var(--nile-colors-dark-500, var(--ng-colors-text-secondary-700)); the shell points that fallback token at quaternary-500.
|
|
1608
|
+
// (The painted ink sits inside nile-link, which this runner does not render; it is checked in the browser instead.)
|
|
1609
|
+
const secondaryOf = (node: HTMLElement) =>
|
|
1610
|
+
getComputedStyle(node).getPropertyValue('--ng-colors-text-secondary-700').trim();
|
|
1611
|
+
expect(secondaryOf(inactive)).to.equal('rgb(113, 118, 128)');
|
|
1612
|
+
type Sheets = { cssText: string } | { cssText: string }[];
|
|
1613
|
+
const sheets = (customElements.get('nile-nav-tab') as unknown as { styles: Sheets }).styles;
|
|
1614
|
+
const tabStyles = ([] as { cssText: string }[]).concat(sheets).map(sheet => sheet.cssText).join('\n');
|
|
1615
|
+
expect(tabStyles).to.include(
|
|
1616
|
+
'color: var(--nile-colors-dark-500, var(--ng-colors-text-secondary-700));'
|
|
1617
|
+
);
|
|
1618
|
+
expect(tabStyles, 'no new hook on the shared component').to.not.include('--nile-nav-tab-text-color:');
|
|
1619
|
+
// Nothing else in the shell's tree is remapped.
|
|
1620
|
+
expect(secondaryOf(el)).to.equal('rgb(65, 70, 81)');
|
|
1621
|
+
|
|
1622
|
+
// Outside the shell nothing changes.
|
|
1623
|
+
const standalone = await fixture<HTMLElement>(html`
|
|
1624
|
+
<nile-nav-tab-group style="${colours}">
|
|
1625
|
+
<nile-nav-tab slot="nav" panel="apps" active>Connected</nile-nav-tab>
|
|
1626
|
+
<nile-nav-tab slot="nav" panel="approvals">Approvals</nile-nav-tab>
|
|
1627
|
+
</nile-nav-tab-group>
|
|
1628
|
+
`);
|
|
1629
|
+
const loneTab = standalone.querySelectorAll('nile-nav-tab')[1] as HTMLElement;
|
|
1630
|
+
expect(secondaryOf(loneTab), 'the global default is untouched').to.equal('rgb(65, 70, 81)');
|
|
1631
|
+
await (standalone as unknown as { updateComplete: Promise<unknown> }).updateComplete;
|
|
1632
|
+
expect(indicatorOf(standalone), 'a standalone group paints its own default').to.equal(
|
|
1633
|
+
'rgb(0, 78, 235)'
|
|
1634
|
+
);
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
it('65d. colours the description text-quaternary-500', async () => {
|
|
1638
|
+
const el = await header({
|
|
1639
|
+
pageTitle: 'Applications',
|
|
1640
|
+
description: 'Manage the apps connected here',
|
|
1641
|
+
});
|
|
1642
|
+
el.style.setProperty('--ng-colors-text-quaternary-500', 'rgb(113, 118, 128)');
|
|
1643
|
+
el.style.setProperty('--ng-colors-text-tertiary-600', 'rgb(1, 2, 3)');
|
|
1644
|
+
expect(getComputedStyle(part(el, 'description')!).color).to.equal(
|
|
1645
|
+
'rgb(113, 118, 128)'
|
|
1646
|
+
);
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1406
1649
|
it('66. centres a 20px app icon in the 24px title row, two pixels down', async () => {
|
|
1407
1650
|
const el = await header({
|
|
1408
1651
|
pageTitle: 'Applications',
|
|
@@ -1439,7 +1682,7 @@ describe('NileAppShell — the header title block', () => {
|
|
|
1439
1682
|
expect(base.getBoundingClientRect().width).to.equal(32);
|
|
1440
1683
|
});
|
|
1441
1684
|
|
|
1442
|
-
it('54.
|
|
1685
|
+
it('54. a slotted app icon draws a 20px block around the mark', async () => {
|
|
1443
1686
|
const el = await header({ pageTitle: 'Applications', leading: 'app-icon' });
|
|
1444
1687
|
const box = part(el, 'app-icon')!;
|
|
1445
1688
|
expect(box).to.exist;
|
|
@@ -1495,7 +1738,7 @@ describe('NileAppShell — the four header heights', () => {
|
|
|
1495
1738
|
};
|
|
1496
1739
|
|
|
1497
1740
|
it('55. is 68px with neither axis on, whatever the leading', async () => {
|
|
1498
|
-
for (const leading of
|
|
1741
|
+
for (const leading of LEADINGS) {
|
|
1499
1742
|
const el = await header({ pageTitle: 'Applications', leading });
|
|
1500
1743
|
expect(bar(el).getBoundingClientRect().height, leading).to.equal(68);
|
|
1501
1744
|
// The leading axis changes what is in the bar, never how tall it is.
|
|
@@ -1542,8 +1785,8 @@ describe('NileAppShell — the four header heights', () => {
|
|
|
1542
1785
|
});
|
|
1543
1786
|
|
|
1544
1787
|
// The three axes are independent, so all twelve combinations land on one of the four heights.
|
|
1545
|
-
it('58. resolves
|
|
1546
|
-
for (const leading of
|
|
1788
|
+
it('58. resolves every variant to exactly the four heights', async () => {
|
|
1789
|
+
for (const leading of LEADINGS) {
|
|
1547
1790
|
for (const tabs of [false, true]) {
|
|
1548
1791
|
for (const described of [false, true]) {
|
|
1549
1792
|
/* eslint-disable-next-line no-nested-ternary */
|