From b92f4802b11aa936302c5cadaad443e4df7aac92 Mon Sep 17 00:00:00 2001 From: Cory Rylan Date: Fri, 31 Jul 2026 15:16:49 -0500 Subject: [PATCH] feat(code): iframe component - Added the nve-iframe component to render isolated content with dynamic height and theme synchronization. - Implemented iframe styling and functionality to handle resizing and theme updates. Signed-off-by: Cory Rylan --- pnpm-lock.yaml | 65 ++-- pnpm-workspace.yaml | 2 +- projects/code/DEVELOPMENT.md | 1 + projects/code/package.json | 26 +- .../code/src/codeblock/codeblock.test.ssr.ts | 25 ++ projects/code/src/index.test.lighthouse.ts | 6 +- projects/code/vitest.ssr.ts | 15 + projects/core/.visual/iframe.dark.png | 3 + projects/core/.visual/iframe.png | 3 + projects/core/package.json | 13 + projects/core/src/bundle.ts | 2 + projects/core/src/iframe/define.ts | 13 + projects/core/src/iframe/iframe.css | 29 ++ projects/core/src/iframe/iframe.examples.ts | 112 ++++++ projects/core/src/iframe/iframe.test.axe.ts | 29 ++ .../core/src/iframe/iframe.test.lighthouse.ts | 21 + projects/core/src/iframe/iframe.test.ssr.ts | 24 ++ projects/core/src/iframe/iframe.test.ts | 365 ++++++++++++++++++ .../core/src/iframe/iframe.test.visual.ts | 50 +++ projects/core/src/iframe/iframe.ts | 294 ++++++++++++++ projects/core/src/iframe/index.ts | 4 + projects/core/src/index.test.lighthouse.ts | 6 +- projects/core/vitest.config.ts | 16 + .../src/local/example-approved-domains.js | 2 +- projects/site/src/_11ty/layouts/common.js | 1 + projects/site/src/docs/elements/iframe.md | 44 +++ 26 files changed, 1127 insertions(+), 44 deletions(-) create mode 100644 projects/code/src/codeblock/codeblock.test.ssr.ts create mode 100644 projects/code/vitest.ssr.ts create mode 100644 projects/core/.visual/iframe.dark.png create mode 100644 projects/core/.visual/iframe.png create mode 100644 projects/core/src/iframe/define.ts create mode 100644 projects/core/src/iframe/iframe.css create mode 100644 projects/core/src/iframe/iframe.examples.ts create mode 100644 projects/core/src/iframe/iframe.test.axe.ts create mode 100644 projects/core/src/iframe/iframe.test.lighthouse.ts create mode 100644 projects/core/src/iframe/iframe.test.ssr.ts create mode 100644 projects/core/src/iframe/iframe.test.ts create mode 100644 projects/core/src/iframe/iframe.test.visual.ts create mode 100644 projects/core/src/iframe/iframe.ts create mode 100644 projects/core/src/iframe/index.ts create mode 100644 projects/site/src/docs/elements/iframe.md diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index facf5d3890..4a01b0fb2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -279,8 +279,8 @@ catalogs: specifier: 4.0.1 version: 4.0.1 playwright: - specifier: 1.62.1 - version: 1.62.1 + specifier: 1.63.0 + version: 1.63.0 postcss: specifier: 8.5.26 version: 8.5.26 @@ -444,7 +444,7 @@ importers: version: 17.4.1 playwright: specifier: 'catalog:' - version: 1.62.1 + version: 1.63.0 prettier: specifier: 3.4.2 version: 3.4.2 @@ -658,6 +658,9 @@ importers: '@vitest/browser': specifier: 'catalog:' version: 5.0.0(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) + '@vitest/browser-playwright': + specifier: 'catalog:' + version: 5.0.0(playwright@1.63.0)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) '@vitest/coverage-istanbul': specifier: 'catalog:' version: 5.0.0(vitest@5.0.0) @@ -1059,7 +1062,7 @@ importers: version: 5.0.0(vite@8.2.2(@types/node@26.5.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) '@vitest/browser-playwright': specifier: 'catalog:' - version: 5.0.0(playwright@1.62.1)(vite@8.2.2(@types/node@26.5.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) + version: 5.0.0(playwright@1.63.0)(vite@8.2.2(@types/node@26.5.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) '@vitest/coverage-istanbul': specifier: 'catalog:' version: 5.0.0(vitest@5.0.0) @@ -1095,7 +1098,7 @@ importers: version: 6.0.0 playwright: specifier: 'catalog:' - version: 1.62.1 + version: 1.63.0 pngjs: specifier: 7.0.0 version: 7.0.0 @@ -1724,7 +1727,7 @@ importers: version: 7.1.2 playwright: specifier: 'catalog:' - version: 1.62.1 + version: 1.63.0 stylelint: specifier: 'catalog:' version: 17.14.1(supports-color@10.2.2)(typescript@6.0.3) @@ -2080,7 +2083,7 @@ importers: version: 5.0.0(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) '@vitest/browser-playwright': specifier: 'catalog:' - version: 5.0.0(playwright@1.62.1)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) + version: 5.0.0(playwright@1.63.0)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) eslint: specifier: 10.11.0 version: 10.11.0(jiti@2.7.0)(supports-color@10.2.2) @@ -9601,11 +9604,6 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -11962,13 +11960,13 @@ packages: pkg-types@2.3.1: resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} - playwright-core@1.62.1: - resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + playwright-core@1.63.0: + resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} engines: {node: '>=20'} hasBin: true - playwright@1.62.1: - resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + playwright@1.63.0: + resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==} engines: {node: '>=20'} hasBin: true @@ -19811,11 +19809,11 @@ snapshots: vite: 8.2.2(@types/node@26.5.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0) vue: 3.5.42(typescript@6.0.3) - '@vitest/browser-playwright@5.0.0(playwright@1.62.1)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.24))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0)': + '@vitest/browser-playwright@5.0.0(playwright@1.63.0)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.24))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0)': dependencies: '@vitest/browser': 5.0.0(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.24))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) '@vitest/mocker': 5.0.0(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.24))(terser@5.48.0)(yaml@2.9.0)) - playwright: 1.62.1 + playwright: 1.63.0 tinyrainbow: 3.1.1 vitest: 5.0.0(@opentelemetry/api@1.9.1)(@types/node@26.4.1)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-istanbul@5.0.0)(@vitest/ui@5.0.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.24))(terser@5.48.0)(yaml@2.9.0)) transitivePeerDependencies: @@ -19825,11 +19823,11 @@ snapshots: - vite optional: true - '@vitest/browser-playwright@5.0.0(playwright@1.62.1)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0)': + '@vitest/browser-playwright@5.0.0(playwright@1.63.0)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0)': dependencies: '@vitest/browser': 5.0.0(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) '@vitest/mocker': 5.0.0(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0)) - playwright: 1.62.1 + playwright: 1.63.0 tinyrainbow: 3.1.1 vitest: 5.0.0(@opentelemetry/api@1.9.1)(@types/node@26.4.1)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-istanbul@5.0.0)(@vitest/ui@5.0.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0)) transitivePeerDependencies: @@ -19838,11 +19836,11 @@ snapshots: - utf-8-validate - vite - '@vitest/browser-playwright@5.0.0(playwright@1.62.1)(vite@8.2.2(@types/node@26.5.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0)': + '@vitest/browser-playwright@5.0.0(playwright@1.63.0)(vite@8.2.2(@types/node@26.5.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0)': dependencies: '@vitest/browser': 5.0.0(vite@8.2.2(@types/node@26.5.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) '@vitest/mocker': 5.0.0(vite@8.2.2(@types/node@26.5.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0)) - playwright: 1.62.1 + playwright: 1.63.0 tinyrainbow: 3.1.1 vitest: 5.0.0(@opentelemetry/api@1.9.1)(@types/node@26.5.0)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-istanbul@5.0.0)(@vitest/ui@5.0.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.5.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0)) transitivePeerDependencies: @@ -19852,11 +19850,11 @@ snapshots: - vite optional: true - '@vitest/browser-playwright@5.0.0(playwright@1.62.1)(vite@8.2.2(@types/node@26.5.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0)': + '@vitest/browser-playwright@5.0.0(playwright@1.63.0)(vite@8.2.2(@types/node@26.5.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0)': dependencies: '@vitest/browser': 5.0.0(vite@8.2.2(@types/node@26.5.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) '@vitest/mocker': 5.0.0(vite@8.2.2(@types/node@26.5.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0)) - playwright: 1.62.1 + playwright: 1.63.0 tinyrainbow: 3.1.1 vitest: 5.0.0(@opentelemetry/api@1.9.1)(@types/node@26.5.0)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-istanbul@5.0.0)(@vitest/ui@5.0.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.5.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0)) transitivePeerDependencies: @@ -22476,9 +22474,6 @@ snapshots: fs.realpath@1.0.0: {} - fsevents@2.3.2: - optional: true - fsevents@2.3.3: optional: true @@ -25353,13 +25348,11 @@ snapshots: exsolve: 1.1.1 pathe: 2.0.3 - playwright-core@1.62.1: {} + playwright-core@1.63.0: {} - playwright@1.62.1: + playwright@1.63.0: dependencies: - playwright-core: 1.62.1 - optionalDependencies: - fsevents: 2.3.2 + playwright-core: 1.63.0 please-upgrade-node@3.2.0: dependencies: @@ -28104,7 +28097,7 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 26.4.1 - '@vitest/browser-playwright': 5.0.0(playwright@1.62.1)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.24))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) + '@vitest/browser-playwright': 5.0.0(playwright@1.63.0)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.24))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) '@vitest/coverage-istanbul': 5.0.0(vitest@5.0.0) '@vitest/ui': 5.0.0(vitest@5.0.0) jsdom: 30.0.1 @@ -28130,7 +28123,7 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 26.4.1 - '@vitest/browser-playwright': 5.0.0(playwright@1.62.1)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.24))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) + '@vitest/browser-playwright': 5.0.0(playwright@1.63.0)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) '@vitest/coverage-istanbul': 5.0.0(vitest@5.0.0) '@vitest/ui': 5.0.0(vitest@5.0.0) jsdom: 30.0.1 @@ -28156,7 +28149,7 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 26.5.0 - '@vitest/browser-playwright': 5.0.0(playwright@1.62.1)(vite@8.2.2(@types/node@26.5.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) + '@vitest/browser-playwright': 5.0.0(playwright@1.63.0)(vite@8.2.2(@types/node@26.5.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) '@vitest/coverage-istanbul': 5.0.0(vitest@5.0.0) '@vitest/ui': 5.0.0(vitest@5.0.0) jsdom: 30.0.1 @@ -28182,7 +28175,7 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 26.5.0 - '@vitest/browser-playwright': 5.0.0(playwright@1.62.1)(vite@8.2.2(@types/node@26.5.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) + '@vitest/browser-playwright': 5.0.0(playwright@1.63.0)(vite@8.2.2(@types/node@26.5.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0) '@vitest/coverage-istanbul': 5.0.0(vitest@5.0.0) '@vitest/ui': 5.0.0(vitest@5.0.0) jsdom: 30.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ba672d15c5..2be543e301 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -119,7 +119,7 @@ catalog: markdown-it: 15.0.1 markdown-it-link-attributes: 4.0.1 minisearch: 7.2.0 - playwright: 1.62.1 + playwright: 1.63.0 postcss: 8.5.26 publint: 0.3.24 react: 19.2.8 diff --git a/projects/code/DEVELOPMENT.md b/projects/code/DEVELOPMENT.md index 599835d4a1..ff4b5b3d0d 100644 --- a/projects/code/DEVELOPMENT.md +++ b/projects/code/DEVELOPMENT.md @@ -8,6 +8,7 @@ | `pnpm run test` | Run unit tests | | `pnpm run test:watch` | Run unit tests in watch mode | | `pnpm run test:axe` | Run accessibility tests | +| `pnpm run test:ssr` | Run server-side rendering tests | | `pnpm run test:coverage` | Run unit tests with coverage | | `pnpm run test:lighthouse` | Run Lighthouse performance tests | | `pnpm run ci` | Run the full CI pipeline | diff --git a/projects/code/package.json b/projects/code/package.json index 28250aa096..ed053bb55e 100644 --- a/projects/code/package.json +++ b/projects/code/package.json @@ -4,7 +4,7 @@ "release": { "extends": "../../release.config.js" }, - "description": "Code authoring related components. Providing syntax-highlighted code blocks with support for multiple programming languages.", + "description": "NVIDIA Elements code authoring related components such as syntax-highlighted code blocks.", "keywords": [ "nvidia", "web-components", @@ -186,6 +186,7 @@ "lint": "wireit", "test": "wireit", "test:axe": "wireit", + "test:ssr": "wireit", "test:coverage": "wireit", "test:watch": "wireit", "test:lighthouse": "wireit" @@ -235,6 +236,7 @@ "lint", "publint", "test:axe", + "test:ssr", "test:coverage" ] }, @@ -380,6 +382,28 @@ "NODE_ENV": "production" } }, + "test:ssr": { + "command": "vitest run --config=vitest.ssr.ts", + "files": [ + "dist/**/*.js", + "src/**/*.test.ssr.ts", + "tsconfig.json", + "vitest.ssr.ts" + ], + "output": [ + "coverage/ssr/**" + ], + "dependencies": [ + "../internals/vite:ci", + { + "script": "build", + "cascade": false + } + ], + "env": { + "NODE_ENV": "production" + } + }, "lint": { "dependencies": [ "lint:eslint", diff --git a/projects/code/src/codeblock/codeblock.test.ssr.ts b/projects/code/src/codeblock/codeblock.test.ssr.ts new file mode 100644 index 0000000000..3adb718c40 --- /dev/null +++ b/projects/code/src/codeblock/codeblock.test.ssr.ts @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { html } from 'lit'; +import { describe, expect, it } from 'vitest'; +import { ssrRunner } from '@internals/vite'; +import { CodeBlock } from '@nvidia-elements/code/codeblock'; +import '@nvidia-elements/code/codeblock/define.js'; + +describe(CodeBlock.metadata.tag, () => { + it('should render its shadow root and preserve source and actions', async () => { + const result = await ssrRunner.render(html` + +
echo hello
+ +
+ `); + + expect(result).toContain('shadowroot="open"'); + expect(result).toContain('nve-codeblock'); + expect(result).toContain('class="hljs"'); + expect(result).toContain('
echo hello
'); + expect(result).toContain(''); + }); +}); diff --git a/projects/code/src/index.test.lighthouse.ts b/projects/code/src/index.test.lighthouse.ts index 03a3bda7c5..6717e2eacc 100644 --- a/projects/code/src/index.test.lighthouse.ts +++ b/projects/code/src/index.test.lighthouse.ts @@ -24,7 +24,7 @@ describe('lighthouse report', () => { `); - expect(report.payload.javascript.kb).toBeLessThan(45); + expect(report.payload.javascript.kb).toBeLessThan(46.5); expect(report.payload.javascript.requests['define.js']!.kb).toBeLessThan(11); expect(report.payload.javascript.requests['core.js']!.kb).toBeLessThan(9); expect(report.payload.javascript.requests['bash.js']!.kb).toBeLessThan(3); @@ -52,7 +52,7 @@ describe('lighthouse report', () => { expect(report.scores.bestPractices).toBe(100); const bundleKb = report.payload.javascript.requests[Object.keys(report.payload.javascript.requests)[0]!]!.kb; expect(bundleKb).toBeGreaterThan(30); - expect(bundleKb).toBeLessThan(31.5); + expect(bundleKb).toBeLessThan(32.1); }); }); @@ -65,6 +65,6 @@ describe('lighthouse report', () => { `); expect(report.payload.javascript.kb).toBeGreaterThan(31); - expect(report.payload.javascript.kb).toBeLessThan(32.6); + expect(report.payload.javascript.kb).toBeLessThan(33.2); }); }); diff --git a/projects/code/vitest.ssr.ts b/projects/code/vitest.ssr.ts new file mode 100644 index 0000000000..260dee001d --- /dev/null +++ b/projects/code/vitest.ssr.ts @@ -0,0 +1,15 @@ +import { resolve } from 'path'; +import { mergeConfig } from 'vitest/config'; +import { libraryLitSSRTestConfig } from '@internals/vite/configs/ssr.js'; + +export default mergeConfig(libraryLitSSRTestConfig, { + resolve: { + alias: { '@nvidia-elements/code': resolve(import.meta.dirname, './dist') } + }, + test: { + include: ['./src/**/*.test.ssr.ts'], + outputFile: { + junit: './coverage/ssr/junit.xml' + } + } +}); diff --git a/projects/core/.visual/iframe.dark.png b/projects/core/.visual/iframe.dark.png new file mode 100644 index 0000000000..745647248a --- /dev/null +++ b/projects/core/.visual/iframe.dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65ef665b043d2a6bbbc13345fb8d693ec116bebc890fab56dd2bf20db1a6ad2b +size 1029 diff --git a/projects/core/.visual/iframe.png b/projects/core/.visual/iframe.png new file mode 100644 index 0000000000..31c79d3d09 --- /dev/null +++ b/projects/core/.visual/iframe.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:053575e9579be233fdb51e2bad34691381a07fe7f06dbb42f2cfcb25e813036d +size 1037 diff --git a/projects/core/package.json b/projects/core/package.json index 85ddda7016..db9a33d556 100644 --- a/projects/core/package.json +++ b/projects/core/package.json @@ -507,6 +507,18 @@ "types": "./dist/icon-button/define.d.ts", "default": "./dist/icon-button/define.js" }, + "./iframe": { + "types": "./dist/iframe/index.d.ts", + "default": "./dist/iframe/index.js" + }, + "./iframe/index.js": { + "types": "./dist/iframe/index.d.ts", + "default": "./dist/iframe/index.js" + }, + "./iframe/define.js": { + "types": "./dist/iframe/define.d.ts", + "default": "./dist/iframe/define.js" + }, "./input": { "types": "./dist/input/index.d.ts", "default": "./dist/input/index.js" @@ -1016,6 +1028,7 @@ "@nvidia-elements/styles": "workspace:^", "@typescript/lib-dom": "npm:@types/web@0.0.312", "@vitest/browser": "catalog:", + "@vitest/browser-playwright": "catalog:", "@vitest/coverage-istanbul": "catalog:", "axe-core": "catalog:", "eslint": "catalog:", diff --git a/projects/core/src/bundle.ts b/projects/core/src/bundle.ts index ce448142a0..4ceea00a89 100644 --- a/projects/core/src/bundle.ts +++ b/projects/core/src/bundle.ts @@ -37,6 +37,7 @@ import '@nvidia-elements/core/gauge/define.js'; import '@nvidia-elements/core/grid/define.js'; import '@nvidia-elements/core/icon/define.js'; import '@nvidia-elements/core/icon-button/define.js'; +import '@nvidia-elements/core/iframe/define.js'; import '@nvidia-elements/core/input/define.js'; import '@nvidia-elements/core/logo/define.js'; import '@nvidia-elements/core/menu/define.js'; @@ -109,6 +110,7 @@ export * from '@nvidia-elements/core/gauge'; export * from '@nvidia-elements/core/grid'; export * from '@nvidia-elements/core/icon'; export * from '@nvidia-elements/core/icon-button'; +export * from '@nvidia-elements/core/iframe'; export * from '@nvidia-elements/core/input'; export * from '@nvidia-elements/core/logo'; export * from '@nvidia-elements/core/menu'; diff --git a/projects/core/src/iframe/define.ts b/projects/core/src/iframe/define.ts new file mode 100644 index 0000000000..06bd8e433e --- /dev/null +++ b/projects/core/src/iframe/define.ts @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { define } from '@nvidia-elements/core/internal'; +import { Iframe } from './iframe.js'; + +define(Iframe); + +declare global { + interface HTMLElementTagNameMap { + 'nve-iframe': Iframe; + } +} diff --git a/projects/core/src/iframe/iframe.css b/projects/core/src/iframe/iframe.css new file mode 100644 index 0000000000..dce2c68689 --- /dev/null +++ b/projects/core/src/iframe/iframe.css @@ -0,0 +1,29 @@ +/* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. */ +/* SPDX-License-Identifier: Apache-2.0 */ + +:host { + --width: var(--_width, fit-content); + --height: var(--_height, fit-content); + --border: none; + display: block; + width: fit-content; + height: fit-content; + border: var(--border); +} + +iframe { + width: var(--width); + height: var(--height); + border: 0; +} + +@supports (frame-sizing: content-height) { + :host { + /* Native sizing locks the initial viewport height as its minimum. */ + --height: var(--_height, 0px); + } + + iframe { + frame-sizing: content-height; + } +} diff --git a/projects/core/src/iframe/iframe.examples.ts b/projects/core/src/iframe/iframe.examples.ts new file mode 100644 index 0000000000..155efc8c4c --- /dev/null +++ b/projects/core/src/iframe/iframe.examples.ts @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { html } from 'lit'; +import '@nvidia-elements/core/iframe/define.js'; + +export default { + title: 'Elements/Iframe', + component: 'nve-iframe' +}; + +/** + * @summary Supplies the iframe with Elements themes, utilities, fonts, and component registrations through its head template. Use this structure because iframe documents do not inherit resources from the parent document. + */ +export const Default = { + render: () => html` + + + + + ` +}; + +/** + * @summary Synchronizes the iframe height with expandable content so the host layout avoids empty space or internal scrollbars. Use for previews whose intrinsic height changes after interaction. + * @tags test-case + */ +export const DynamicHeight = { + render: () => html` + + + + + ` +}; + +/** + * @summary Overrides intrinsic iframe dimensions with `--width` and `--height` to create a stable preview viewport. Use when you need to inspect embedded content at a fixed size regardless of its rendered bounds. + * @tags test-case + */ +export const FixedSize = { + render: () => html` + + + + ` +}; + +/** + * @summary The iframe browsing-context boundary clips popovers at its viewport and prevents them from escaping. Keep overlays inside the frame, or render them outside the iframe when they must overlap surrounding content. + * @tags test-case + */ +export const OverflowClip = { + render: () => html` + + + + + ` +}; + +/** + * @summary Regenerates the iframe document when its source template changes. Use for live previews or generated output that must stay synchronized with edits made in the parent document. + * @tags test-case + */ +export const DynamicallyUpdatedContent = { + render: () => html` + + + + + ` +}; diff --git a/projects/core/src/iframe/iframe.test.axe.ts b/projects/core/src/iframe/iframe.test.axe.ts new file mode 100644 index 0000000000..ad8f17b4d3 --- /dev/null +++ b/projects/core/src/iframe/iframe.test.axe.ts @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { html } from 'lit'; +import { describe, expect, it, beforeEach, afterEach } from 'vitest'; +import { createFixture, elementIsStable, removeFixture } from '@internals/testing'; +import { runAxe } from '@internals/testing/axe'; +import { Iframe } from '@nvidia-elements/core/iframe'; +import '@nvidia-elements/core/iframe/define.js'; + +describe(Iframe.metadata.tag, () => { + let fixture: HTMLElement; + let element: Iframe; + + beforeEach(async () => { + fixture = await createFixture(html``); + element = fixture.querySelector`; + } + + connectedCallback() { + super.connectedCallback(); + globalThis.window.addEventListener('message', this.#handleResizeMessage); + if (this.hasUpdated) { + this.#setupTemplateUpdates(); + this.#setupTemplateHeadUpdates(); + this.#setupLightDomUpdates(); + this.#setupThemeUpdates(); + this.#updateSource(); + } + } + + firstUpdated(props: PropertyValues) { + super.firstUpdated(props); + this.#setupTemplateUpdates(); + this.#setupTemplateHeadUpdates(); + this.#setupLightDomUpdates(); + this.#setupThemeUpdates(); + this.#updateSource(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.#templateObserver?.disconnect(); + this.#headTemplateObserver?.disconnect(); + this.#lightDomObserver?.disconnect(); + this.#themeObserver?.disconnect(); + this.#templateObserver = undefined; + this.#headTemplateObserver = undefined; + this.#lightDomObserver = undefined; + this.#themeObserver = undefined; + globalThis.window.removeEventListener('message', this.#handleResizeMessage); + } + + // and can't be slotted https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/template#usage_notes + #updateSource() { + const iframe = this.#iframe; + if (iframe === null) { + return; + } + + // Start each document with the initial height rather than the previous resize result. + this.#documentId++; + this.style.setProperty('--_height', '0px'); + if (CSS.supports('frame-sizing', 'content-height')) { + // Keep the initial viewport until the new document finishes loading. + iframe.style.height = getComputedStyle(iframe).height; + } + const theme = this.#themeHost?.getAttribute('nve-theme') ?? ''; + iframe.srcdoc = ` + + + + + + ${this.#headTemplate?.innerHTML ?? ''} + + + ${this.#template?.innerHTML ?? ''} + ${iframeScript(this.#documentId)} + + + `; + } + + #setupTemplateUpdates() { + this.#templateObserver?.disconnect(); + const template = this.#template; + if (template) { + this.#templateObserver = new MutationObserver(() => this.#updateSource()); + this.#templateObserver.observe(template.content, { childList: true, subtree: true, characterData: true }); + } else { + this.#templateObserver = undefined; + } + } + + #setupTemplateHeadUpdates() { + this.#headTemplateObserver?.disconnect(); + const headTemplate = this.#headTemplate; + if (headTemplate) { + this.#headTemplateObserver = new MutationObserver(() => this.#updateSource()); + this.#headTemplateObserver.observe(headTemplate.content, { + childList: true, + subtree: true, + characterData: true + }); + } else { + this.#headTemplateObserver = undefined; + } + } + + #setupLightDomUpdates() { + if (!this.#lightDomObserver) { + this.#lightDomObserver = new MutationObserver(() => { + this.#setupTemplateUpdates(); + this.#setupTemplateHeadUpdates(); + this.#updateSource(); + }); + this.#lightDomObserver.observe(this, { childList: true }); + } + } + + #setupThemeUpdates() { + const themeHost = this.#themeHost; + if (themeHost && !this.#themeObserver) { + this.#themeObserver = new MutationObserver(() => this.#syncTheme()); + this.#themeObserver.observe(themeHost, { attributeFilter: ['nve-theme'] }); + } + } + + #handleLoad = () => { + const iframe = this.#iframe; + const documentId = this.#documentId; + requestAnimationFrame(() => { + if (this.isConnected && documentId === this.#documentId) { + iframe?.style.removeProperty('height'); + } + }); + this.#syncTheme(); + }; + + #syncTheme = () => { + const theme = this.#themeHost?.getAttribute('nve-theme'); + const contentWindow = this.#iframe?.contentWindow; + if (theme === null || theme === undefined || contentWindow === null || contentWindow === undefined) { + return; + } + + const message: ThemeMessage = { theme }; + // Sandboxed srcdoc documents have opaque origins, so authenticate messages by their window reference. + contentWindow.postMessage(JSON.stringify(message), '*'); + }; + + #handleResizeMessage = (event: MessageEvent) => { + const iframe = this.#iframe; + if (iframe === null || event.source !== iframe.contentWindow || typeof event.data !== 'string') { + return; + } + + let message: unknown; + try { + message = JSON.parse(event.data); + } catch { + return; + } + + if (!isResizeMessage(message) || message.documentId !== this.#documentId) { + return; + } + + this.style.setProperty('--_width', `${message.width}px`); + this.style.setProperty('--_height', message.height === undefined ? 'auto' : `${message.height}px`); + }; +} diff --git a/projects/core/src/iframe/index.ts b/projects/core/src/iframe/index.ts new file mode 100644 index 0000000000..b369ae3187 --- /dev/null +++ b/projects/core/src/iframe/index.ts @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export * from './iframe.js'; diff --git a/projects/core/src/index.test.lighthouse.ts b/projects/core/src/index.test.lighthouse.ts index fcb5391055..d32133909c 100644 --- a/projects/core/src/index.test.lighthouse.ts +++ b/projects/core/src/index.test.lighthouse.ts @@ -18,7 +18,8 @@ describe('lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.requests['index.js'].kb).toBeLessThan(123.5); + // Includes the iframe component moved from the code package. + expect(report.payload.javascript.requests['index.js'].kb).toBeLessThan(125.0); // if sudden drop in size, check vite bundle config and bundle demo to ensure side effects are properly preserved expect(report.payload.javascript.requests['index.js'].kb).toBeGreaterThan(100); @@ -62,6 +63,7 @@ describe('lighthouse report', () => { import '@nvidia-elements/core/grid/define.js'; import '@nvidia-elements/core/icon/define.js'; import '@nvidia-elements/core/icon-button/define.js'; + import '@nvidia-elements/core/iframe/define.js'; import '@nvidia-elements/core/input/define.js'; import '@nvidia-elements/core/logo/define.js'; import '@nvidia-elements/core/menu/define.js'; @@ -108,7 +110,7 @@ describe('lighthouse report', () => { expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); expect(report.payload.javascript.requests[Object.keys(report.payload.javascript.requests)[0]].kb).toBeLessThan( - 104.0 + 105.5 ); }); }); diff --git a/projects/core/vitest.config.ts b/projects/core/vitest.config.ts index 6329d4c86b..d9ed21d610 100644 --- a/projects/core/vitest.config.ts +++ b/projects/core/vitest.config.ts @@ -1,5 +1,6 @@ import { resolve } from 'path'; import { mergeConfig } from 'vitest/config'; +import { playwright } from '@vitest/browser-playwright'; import { libraryTestConfig } from '@internals/vite/configs/test.js'; export default mergeConfig(libraryTestConfig, { @@ -9,6 +10,21 @@ export default mergeConfig(libraryTestConfig, { }, test: { include: ['./src/**/*.test.ts'], + browser: { + provider: playwright(), + instances: [ + { + browser: 'chromium', + name: 'chromium-native-iframe', + include: ['./src/iframe/iframe.test.ts'], + provider: playwright({ + launchOptions: { + args: ['--enable-blink-features=ResponsiveIframes'] + } + }) + } + ] + }, coverage: { exclude: [ '**/docs/**', diff --git a/projects/internals/eslint/src/local/example-approved-domains.js b/projects/internals/eslint/src/local/example-approved-domains.js index ed59feffad..e629dfb558 100644 --- a/projects/internals/eslint/src/local/example-approved-domains.js +++ b/projects/internals/eslint/src/local/example-approved-domains.js @@ -1,6 +1,6 @@ import { findHtmlTemplate, getTemplateText, hasTag } from './example-helpers.js'; -const APPROVED_DOMAINS = ['nvidia.com', 'github.com', 'huggingface.co']; +const APPROVED_DOMAINS = ['nvidia.com', 'github.com', 'huggingface.co', 'cdn.jsdelivr.net']; const URL_ATTRIBUTES = ['href', 'src', 'srcset']; const URL_ATTRIBUTE_PATTERN = new RegExp(`\\b(${URL_ATTRIBUTES.join('|')})\\s*=\\s*("([^"]*)"|'([^']*)')`, 'gi'); diff --git a/projects/site/src/_11ty/layouts/common.js b/projects/site/src/_11ty/layouts/common.js index ea7a16799f..1c7c8a195f 100644 --- a/projects/site/src/_11ty/layouts/common.js +++ b/projects/site/src/_11ty/layouts/common.js @@ -310,6 +310,7 @@ export const renderDocsNav = data => /* html */ ` Gauge Icon Icon Button + Iframe Input Input Group Logo diff --git a/projects/site/src/docs/elements/iframe.md b/projects/site/src/docs/elements/iframe.md new file mode 100644 index 0000000000..24f3c52485 --- /dev/null +++ b/projects/site/src/docs/elements/iframe.md @@ -0,0 +1,44 @@ +--- +{ + title: 'Iframe', + layout: 'docs.11ty.js', + tag: 'nve-iframe' +} +--- + +## Installation + +```typescript +import '@nvidia-elements/core/iframe/define.js'; +``` + +```html + + + + +``` + +## Iframe + +{% example '@nvidia-elements/core/iframe/iframe.examples.json', 'Default' %} + +## Dynamic Height + +The iframe grows and shrinks with its content. Browsers that support native responsive iframe sizing use `frame-sizing: content-height` and `window.requestResize()`. If native sizing is unavailable or the document cannot use it, the component uses resize messages. The component handles both paths without changes to your templates. + +{% example '@nvidia-elements/core/iframe/iframe.examples.json', 'DynamicHeight' %} + +## Fixed Size + +Set `--width` and `--height` to keep a fixed viewport with either sizing path. + +{% example '@nvidia-elements/core/iframe/iframe.examples.json', 'FixedSize' %}