From cb87b2d9f9ad83d556ffb12d1038b4ac4981d918 Mon Sep 17 00:00:00 2001 From: Adam Braimbridge Date: Fri, 27 Sep 2019 10:33:04 +0100 Subject: [PATCH 1/5] Unaltered copy from examples/ft-ui/ to examples/ft-ui-async-stylesheets --- examples/ft-ui-async-stylesheets/.gitignore | 1 + examples/ft-ui-async-stylesheets/.npmrc | 2 + .../__test__/integration.test.js | 74 +++++++++++++++++++ .../ft-ui-async-stylesheets/client/main.js | 6 ++ .../ft-ui-async-stylesheets/client/main.scss | 10 +++ .../jest-puppeteer.config.js | 10 +++ .../ft-ui-async-stylesheets/jest.config.js | 3 + examples/ft-ui-async-stylesheets/package.json | 30 ++++++++ .../page-kit.config.js | 21 ++++++ examples/ft-ui-async-stylesheets/readme.md | 5 ++ .../ft-ui-async-stylesheets/server/app.js | 11 +++ .../server/controllers/home.jsx | 39 ++++++++++ .../ft-ui-async-stylesheets/server/start.js | 9 +++ 13 files changed, 221 insertions(+) create mode 100644 examples/ft-ui-async-stylesheets/.gitignore create mode 100644 examples/ft-ui-async-stylesheets/.npmrc create mode 100644 examples/ft-ui-async-stylesheets/__test__/integration.test.js create mode 100644 examples/ft-ui-async-stylesheets/client/main.js create mode 100644 examples/ft-ui-async-stylesheets/client/main.scss create mode 100644 examples/ft-ui-async-stylesheets/jest-puppeteer.config.js create mode 100644 examples/ft-ui-async-stylesheets/jest.config.js create mode 100644 examples/ft-ui-async-stylesheets/package.json create mode 100644 examples/ft-ui-async-stylesheets/page-kit.config.js create mode 100644 examples/ft-ui-async-stylesheets/readme.md create mode 100644 examples/ft-ui-async-stylesheets/server/app.js create mode 100644 examples/ft-ui-async-stylesheets/server/controllers/home.jsx create mode 100644 examples/ft-ui-async-stylesheets/server/start.js diff --git a/examples/ft-ui-async-stylesheets/.gitignore b/examples/ft-ui-async-stylesheets/.gitignore new file mode 100644 index 000000000..364fdec1a --- /dev/null +++ b/examples/ft-ui-async-stylesheets/.gitignore @@ -0,0 +1 @@ +public/ diff --git a/examples/ft-ui-async-stylesheets/.npmrc b/examples/ft-ui-async-stylesheets/.npmrc new file mode 100644 index 000000000..6e8708804 --- /dev/null +++ b/examples/ft-ui-async-stylesheets/.npmrc @@ -0,0 +1,2 @@ +shrinkwrap = false +package-lock = false diff --git a/examples/ft-ui-async-stylesheets/__test__/integration.test.js b/examples/ft-ui-async-stylesheets/__test__/integration.test.js new file mode 100644 index 000000000..c656d3765 --- /dev/null +++ b/examples/ft-ui-async-stylesheets/__test__/integration.test.js @@ -0,0 +1,74 @@ +describe('examples/ft-ui', () => { + beforeAll(async () => { + await page.setViewport({ width: 1920, height: 1080 }) + await page.goto('http://localhost:3456', { waitUntil: 'load' }) + }) + + describe('JavaScript bootstrap', () => { + it('flags that JavaScript is enabled', async () => { + await expect(page).toMatchElement('html.js') + }) + + it('flags that the browser passes the cut the mustard test', async () => { + await expect(page).toMatchElement('html.enhanced') + }) + + it('loads the configured scripts', async () => { + await expect(page).toMatchElement('script[src^="https://polyfill.io"]') + await expect(page).toMatchElement('script[src="public/scripts.bundle.js"]') + }) + }) + + describe('UI components', () => { + it('renders a11y skip links', async () => { + await expect(page).toMatchElement('a[href="#site-navigation"]') + await expect(page).toMatchElement('a[href="#site-content"]') + await expect(page).toMatchElement('a[href="#site-footer"]') + }) + + it('renders the site header', async () => { + await expect(page).toMatchElement('#site-navigation .o-header__top') + await expect(page).toMatchElement('#site-navigation .o-header__top-logo') + }) + + it('renders the desktop navigation elements', async () => { + await expect(page).toMatchElement('.o-header__nav--desktop') + await expect(page).toMatchElement('.o-header__mega-column--articles') + await expect(page).toMatchElement('.o-header__mega-column--subsections') + }) + + it('renders the desktop search bar', async () => { + await expect(page).toMatchElement('#o-header-search-primary') + }) + + it('renders the small screen navigation elements', async () => { + await expect(page).toMatchElement('#site-navigation .o-header__nav--mobile') + }) + + it('renders the sticky header', async () => { + await expect(page).toMatchElement('.o-header--sticky') + }) + + it('renders the site footer', async () => { + await expect(page).toMatchElement('#site-footer') + }) + + it('renders the drawer menu', async () => { + await expect(page).toMatchElement('#o-header-drawer') + }) + }) + + describe('basic UI interactivity', () => { + it('enables the drawer menu to be toggled', async () => { + await expect(page).toMatchElement('#o-header-drawer[aria-hidden="true"]') + await page.click('a[aria-controls="o-header-drawer"]') + await expect(page).toMatchElement('#o-header-drawer[aria-hidden="false"]') + }) + + it('enables the search bar to be toggled', async () => { + await expect(page).toMatchElement('#o-header-search-primary[aria-hidden="true"]') + await page.click('a[aria-controls="o-header-search-primary"]') + await expect(page).toMatchElement('#o-header-search-primary[aria-hidden="false"]') + }) + }) +}) diff --git a/examples/ft-ui-async-stylesheets/client/main.js b/examples/ft-ui-async-stylesheets/client/main.js new file mode 100644 index 000000000..8589026eb --- /dev/null +++ b/examples/ft-ui-async-stylesheets/client/main.js @@ -0,0 +1,6 @@ +import domLoaded from 'dom-loaded' +import * as layout from '@financial-times/dotcom-ui-layout' + +domLoaded.then(() => { + layout.init() +}) diff --git a/examples/ft-ui-async-stylesheets/client/main.scss b/examples/ft-ui-async-stylesheets/client/main.scss new file mode 100644 index 000000000..8cf71db6d --- /dev/null +++ b/examples/ft-ui-async-stylesheets/client/main.scss @@ -0,0 +1,10 @@ +@import 'dotcom-ui-layout/styles'; + +.core .o--if-js, +.enhanced .o--if-no-js { + display: none !important; +} + +p { + @include oTypographySans($scale: 7); +} diff --git a/examples/ft-ui-async-stylesheets/jest-puppeteer.config.js b/examples/ft-ui-async-stylesheets/jest-puppeteer.config.js new file mode 100644 index 000000000..65b07a1f3 --- /dev/null +++ b/examples/ft-ui-async-stylesheets/jest-puppeteer.config.js @@ -0,0 +1,10 @@ +module.exports = { + server: { + command: 'npm run start', + port: process.env.PORT || 3456 + }, + launch: { + // https://discuss.circleci.com/t/puppeteer-fails-on-circleci/22650 + args: ['--no-sandbox', '--disable-setuid-sandbox'] + } +} diff --git a/examples/ft-ui-async-stylesheets/jest.config.js b/examples/ft-ui-async-stylesheets/jest.config.js new file mode 100644 index 000000000..1688ea979 --- /dev/null +++ b/examples/ft-ui-async-stylesheets/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: 'jest-puppeteer' +} diff --git a/examples/ft-ui-async-stylesheets/package.json b/examples/ft-ui-async-stylesheets/package.json new file mode 100644 index 000000000..cf32fa581 --- /dev/null +++ b/examples/ft-ui-async-stylesheets/package.json @@ -0,0 +1,30 @@ +{ + "name": "example-ft-ui", + "private": true, + "version": "0.0.0", + "license": "MIT", + "scripts": { + "build": "page-kit build -d", + "dev": "nodemon --ext js,jsx", + "start": "node server/start.js", + "pretest": "npm run build", + "test": "../../node_modules/.bin/jest" + }, + "dependencies": { + "@financial-times/dotcom-middleware-navigation": "file:../../packages/dotcom-middleware-navigation", + "@financial-times/dotcom-ui-layout": "file:../../packages/dotcom-ui-layout", + "@financial-times/dotcom-ui-shell": "file:../../packages/dotcom-ui-shell", + "express": "^4.16.2", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "sucrase": "^3.10.1" + }, + "devDependencies": { + "@financial-times/dotcom-page-kit-cli": "file:../../packages/dotcom-page-kit-cli", + "@financial-times/dotcom-build-bower-resolve": "file:../../packages/dotcom-build-bower-resolve", + "@financial-times/dotcom-build-js": "file:../../packages/dotcom-build-js", + "@financial-times/dotcom-build-sass": "file:../../packages/dotcom-build-sass", + "dom-loaded": "^1.2.0", + "nodemon": "^1.18.9" + } +} diff --git a/examples/ft-ui-async-stylesheets/page-kit.config.js b/examples/ft-ui-async-stylesheets/page-kit.config.js new file mode 100644 index 000000000..f6da4e269 --- /dev/null +++ b/examples/ft-ui-async-stylesheets/page-kit.config.js @@ -0,0 +1,21 @@ +const path = require('path') +const bower = require('@financial-times/dotcom-build-bower-resolve') +const sass = require('@financial-times/dotcom-build-sass') +const js = require('@financial-times/dotcom-build-js') + +module.exports = { + plugins: [ + bower.plugin(), + sass.plugin({ includePaths: [path.resolve('../../bower_components')] }), + js.plugin(), + ], + settings: { + build: { + entry: { + scripts: './client/main.js', + styles: './client/main.scss' + }, + outputPath: path.resolve('./public') + } + } +} diff --git a/examples/ft-ui-async-stylesheets/readme.md b/examples/ft-ui-async-stylesheets/readme.md new file mode 100644 index 000000000..89d807b66 --- /dev/null +++ b/examples/ft-ui-async-stylesheets/readme.md @@ -0,0 +1,5 @@ +# FT UI + +This example demonstrates all the FT.com layout which includes the header, navigation, and footer components. It uses [Sucrase] to provide support for JSX syntax and ESM. + +[Sucrase]: https://github.com/alangpierce/sucrase diff --git a/examples/ft-ui-async-stylesheets/server/app.js b/examples/ft-ui-async-stylesheets/server/app.js new file mode 100644 index 000000000..04d984083 --- /dev/null +++ b/examples/ft-ui-async-stylesheets/server/app.js @@ -0,0 +1,11 @@ +import express from 'express' +import * as navigation from '@financial-times/dotcom-middleware-navigation' +import { homeController } from './controllers/home.jsx' + +export const app = express() + +app.use(navigation.init()) + +app.use('/public', express.static('./public')) + +app.get('/', homeController) diff --git a/examples/ft-ui-async-stylesheets/server/controllers/home.jsx b/examples/ft-ui-async-stylesheets/server/controllers/home.jsx new file mode 100644 index 000000000..35eb8e108 --- /dev/null +++ b/examples/ft-ui-async-stylesheets/server/controllers/home.jsx @@ -0,0 +1,39 @@ +import React from 'react' +import ReactDOM from 'react-dom/server' +import { Shell } from '@financial-times/dotcom-ui-shell' +import { Layout } from '@financial-times/dotcom-ui-layout' + +export function homeController(_, response, next) { + const appContext = { + appName: 'ft-ui', + edition: response.locals.navigation.editions.current.id + } + + const pageData = { + title: 'Hello World!', + contents: '

Hello, welcome to Page Kit.

' + } + + const shellProps = { + scripts: ['public/scripts.bundle.js'], + stylesheets: ['public/styles.css'], + pageTitle: pageData.title, + context: appContext + } + + const layoutProps = { + navigationData: response.locals.navigation + } + + try { + const document = ( + + + + ) + + response.send('' + ReactDOM.renderToString(document)) + } catch (error) { + next(error) + } +} diff --git a/examples/ft-ui-async-stylesheets/server/start.js b/examples/ft-ui-async-stylesheets/server/start.js new file mode 100644 index 000000000..8604fbc1e --- /dev/null +++ b/examples/ft-ui-async-stylesheets/server/start.js @@ -0,0 +1,9 @@ +require('sucrase/register') + +const { app } = require('./app') + +const PORT = process.env.PORT || 3456 + +app.listen(PORT, () => { + console.log(`Listening on http://localhost:${PORT}`) // eslint-disable-line no-console +}) From b842d05b445e3c7b80195d38796d73bec6d7250f Mon Sep 17 00:00:00 2001 From: Adam Braimbridge Date: Fri, 27 Sep 2019 10:51:44 +0100 Subject: [PATCH 2/5] Git ignore yarn.lock --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8d8448a53..ea082a76f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ dist/ Thumbs.db npm-debug.log package-lock.json +yarn.lock .vscode .cache From a00fd1d1705802c5a666618facff2c3667c01107 Mon Sep 17 00:00:00 2001 From: Adam Braimbridge Date: Fri, 4 Oct 2019 16:56:56 +0100 Subject: [PATCH 3/5] Working proof of concept for loading stylesheets asynchronously. --- .../ft-ui-async-stylesheets/client/async.scss | 16 ++++++++++++++++ examples/ft-ui-async-stylesheets/client/main.js | 14 ++++++++++++++ .../ft-ui-async-stylesheets/client/main.scss | 14 ++++++-------- examples/ft-ui-async-stylesheets/package.json | 2 +- .../ft-ui-async-stylesheets/page-kit.config.js | 3 ++- .../server/controllers/home.jsx | 1 + packages/dotcom-ui-shell/README.md | 4 ++++ .../src/__test__/components/Stylesheets.test.tsx | 3 ++- .../__snapshots__/Stylesheets.test.tsx.snap | 5 +++++ .../dotcom-ui-shell/src/components/Shell.tsx | 6 +++++- .../src/components/StyleSheets.tsx | 15 +++++++++++++-- 11 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 examples/ft-ui-async-stylesheets/client/async.scss diff --git a/examples/ft-ui-async-stylesheets/client/async.scss b/examples/ft-ui-async-stylesheets/client/async.scss new file mode 100644 index 000000000..150d52f96 --- /dev/null +++ b/examples/ft-ui-async-stylesheets/client/async.scss @@ -0,0 +1,16 @@ +// This will already have been output in blocking.scss +$n-ui-foundations-applied: true; +@import "n-ui-foundations/main"; + +// Output anything which isn't visible on page load +@import "o-header/main"; +@include oHeader($features: ('drawer', 'megamenu', 'sticky'), $include-base-styles: false); + +// Megamenu z-indexes etc. +@import "@financial-times/dotcom-ui-header/styles"; + +@import "n-topic-search/main"; +@include nTopicSearch(); + +$o-footer-is-silent: false; +@import "o-footer/main"; diff --git a/examples/ft-ui-async-stylesheets/client/main.js b/examples/ft-ui-async-stylesheets/client/main.js index 8589026eb..5c3562da7 100644 --- a/examples/ft-ui-async-stylesheets/client/main.js +++ b/examples/ft-ui-async-stylesheets/client/main.js @@ -1,6 +1,20 @@ import domLoaded from 'dom-loaded' import * as layout from '@financial-times/dotcom-ui-layout' +/** + * + * Load stylesheets asyncronously. See: + * https://www.filamentgroup.com/lab/load-css-simpler/ + * https://w3c.github.io/preload/#example-5 + */ +const asyncStylesheetsInit = () => { + const asyncStylesheets = document.getElementsByClassName('asyncStylesheet') + Array.from(asyncStylesheets).forEach((element) => { + element.media = 'all' + }) +} + domLoaded.then(() => { + asyncStylesheetsInit() layout.init() }) diff --git a/examples/ft-ui-async-stylesheets/client/main.scss b/examples/ft-ui-async-stylesheets/client/main.scss index 8cf71db6d..a07ff681c 100644 --- a/examples/ft-ui-async-stylesheets/client/main.scss +++ b/examples/ft-ui-async-stylesheets/client/main.scss @@ -1,10 +1,8 @@ -@import 'dotcom-ui-layout/styles'; +@import "n-ui-foundations/main"; -.core .o--if-js, -.enhanced .o--if-no-js { - display: none !important; -} +// We don't need the sub-brand or transparent header styles so disable them. +// NOTE: Don't output anything which isn't visible on page load! +@import "o-header/main"; +@include oHeader($features: ('nav', 'subnav', 'search', 'anon', 'simple'), $include-base-styles: true); -p { - @include oTypographySans($scale: 7); -} +@import '@financial-times/dotcom-ui-layout/styles'; diff --git a/examples/ft-ui-async-stylesheets/package.json b/examples/ft-ui-async-stylesheets/package.json index cf32fa581..64e5dd7c5 100644 --- a/examples/ft-ui-async-stylesheets/package.json +++ b/examples/ft-ui-async-stylesheets/package.json @@ -1,5 +1,5 @@ { - "name": "example-ft-ui", + "name": "example-ft-ui-async-stylesheets", "private": true, "version": "0.0.0", "license": "MIT", diff --git a/examples/ft-ui-async-stylesheets/page-kit.config.js b/examples/ft-ui-async-stylesheets/page-kit.config.js index f6da4e269..1faa4b3cd 100644 --- a/examples/ft-ui-async-stylesheets/page-kit.config.js +++ b/examples/ft-ui-async-stylesheets/page-kit.config.js @@ -13,7 +13,8 @@ module.exports = { build: { entry: { scripts: './client/main.js', - styles: './client/main.scss' + styles: './client/main.scss', + async: './client/async.scss' }, outputPath: path.resolve('./public') } diff --git a/examples/ft-ui-async-stylesheets/server/controllers/home.jsx b/examples/ft-ui-async-stylesheets/server/controllers/home.jsx index 35eb8e108..20cb74eb2 100644 --- a/examples/ft-ui-async-stylesheets/server/controllers/home.jsx +++ b/examples/ft-ui-async-stylesheets/server/controllers/home.jsx @@ -17,6 +17,7 @@ export function homeController(_, response, next) { const shellProps = { scripts: ['public/scripts.bundle.js'], stylesheets: ['public/styles.css'], + asyncStylesheets: ['public/async.css'], pageTitle: pageData.title, context: appContext } diff --git a/packages/dotcom-ui-shell/README.md b/packages/dotcom-ui-shell/README.md index 8fd4e992c..cc7affd1a 100644 --- a/packages/dotcom-ui-shell/README.md +++ b/packages/dotcom-ui-shell/README.md @@ -101,6 +101,10 @@ An array of stylesheet URLs to be loaded using `` tags. An optional string of CSS to embed into the page. Defaults to setting the background colour to FT pink. +#### `asyncStylesheets` (string) + +An optional array of stylesheet URLs to be loaded using `` tags. + #### `resourceHints` (string[]) An optional array of resource URLs to append [resource hints] for. The values provided for the `stylesheets` and `scripts` options will be appended by default. diff --git a/packages/dotcom-ui-shell/src/__test__/components/Stylesheets.test.tsx b/packages/dotcom-ui-shell/src/__test__/components/Stylesheets.test.tsx index 786385138..003f277ee 100644 --- a/packages/dotcom-ui-shell/src/__test__/components/Stylesheets.test.tsx +++ b/packages/dotcom-ui-shell/src/__test__/components/Stylesheets.test.tsx @@ -6,7 +6,8 @@ describe('dotcom-ui-shell/src/components/StyleSheets', () => { it('renders the given stylesheets and critical styles', () => { const props = { stylesheets: ['path/to/styles.css'], - criticalStyles: 'html { margin: 0 } body { font-family: "Comic Sans" }' + criticalStyles: 'html { margin: 0 } body { font-family: "Comic Sans" }', + asyncStylesheets: ['path/to/async.css'] } const tree = renderer.create().toJSON() diff --git a/packages/dotcom-ui-shell/src/__test__/components/__snapshots__/Stylesheets.test.tsx.snap b/packages/dotcom-ui-shell/src/__test__/components/__snapshots__/Stylesheets.test.tsx.snap index 8fbcbaa62..4c6d06128 100644 --- a/packages/dotcom-ui-shell/src/__test__/components/__snapshots__/Stylesheets.test.tsx.snap +++ b/packages/dotcom-ui-shell/src/__test__/components/__snapshots__/Stylesheets.test.tsx.snap @@ -13,5 +13,10 @@ Array [ href="path/to/styles.css" rel="stylesheet" />, + , ] `; diff --git a/packages/dotcom-ui-shell/src/components/Shell.tsx b/packages/dotcom-ui-shell/src/components/Shell.tsx index 3415fa0e2..29462b145 100644 --- a/packages/dotcom-ui-shell/src/components/Shell.tsx +++ b/packages/dotcom-ui-shell/src/components/Shell.tsx @@ -62,7 +62,11 @@ function Shell(props: TShellProps) { type="application/json" dangerouslySetInnerHTML={{ __html: JSON.stringify(props.initialProps) }} /> - + diff --git a/packages/dotcom-ui-shell/src/components/StyleSheets.tsx b/packages/dotcom-ui-shell/src/components/StyleSheets.tsx index a0e0bf928..a4bd9f9f0 100644 --- a/packages/dotcom-ui-shell/src/components/StyleSheets.tsx +++ b/packages/dotcom-ui-shell/src/components/StyleSheets.tsx @@ -3,18 +3,29 @@ import React from 'react' export type TStylesheetProps = { stylesheets?: string[] criticalStyles?: string + asyncStylesheets?: string[] } -const Stylesheets = ({ stylesheets, criticalStyles }: TStylesheetProps) => ( +const Stylesheets = ({ stylesheets, criticalStyles, asyncStylesheets }: TStylesheetProps) => ( {criticalStyles &&