Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ft ui async stylesheets #611

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ dist/
Thumbs.db
npm-debug.log
package-lock.json
yarn.lock
.vscode
.cache
1 change: 1 addition & 0 deletions examples/ft-ui-async-stylesheets/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public/
2 changes: 2 additions & 0 deletions examples/ft-ui-async-stylesheets/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
shrinkwrap = false
package-lock = false
74 changes: 74 additions & 0 deletions examples/ft-ui-async-stylesheets/__test__/integration.test.js
Original file line number Diff line number Diff line change
@@ -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"]')
})
})
})
16 changes: 16 additions & 0 deletions examples/ft-ui-async-stylesheets/client/async.scss
Original file line number Diff line number Diff line change
@@ -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";
20 changes: 20 additions & 0 deletions examples/ft-ui-async-stylesheets/client/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +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) => {
adambraimbridge marked this conversation as resolved.
Show resolved Hide resolved
element.media = 'all'
})
}
adambraimbridge marked this conversation as resolved.
Show resolved Hide resolved

domLoaded.then(() => {
asyncStylesheetsInit()
layout.init()
})
8 changes: 8 additions & 0 deletions examples/ft-ui-async-stylesheets/client/main.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@import "n-ui-foundations/main";

// 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);

@import '@financial-times/dotcom-ui-layout/styles';
10 changes: 10 additions & 0 deletions examples/ft-ui-async-stylesheets/jest-puppeteer.config.js
Original file line number Diff line number Diff line change
@@ -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']
}
}
3 changes: 3 additions & 0 deletions examples/ft-ui-async-stylesheets/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
preset: 'jest-puppeteer'
}
31 changes: 31 additions & 0 deletions examples/ft-ui-async-stylesheets/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "example-ft-ui-async-stylesheets",
"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",
"@financial-times/dotcom-ui-header": "file:../../packages/dotcom-ui-header",
"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"
}
}
22 changes: 22 additions & 0 deletions examples/ft-ui-async-stylesheets/page-kit.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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',
async: './client/async.scss'
},
outputPath: path.resolve('./public')
}
}
}
5 changes: 5 additions & 0 deletions examples/ft-ui-async-stylesheets/readme.md
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions examples/ft-ui-async-stylesheets/server/app.js
Original file line number Diff line number Diff line change
@@ -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)
40 changes: 40 additions & 0 deletions examples/ft-ui-async-stylesheets/server/controllers/home.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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: '<div align="center"><p>Hello, welcome to Page Kit.</p></div>'
}

const shellProps = {
scripts: ['public/scripts.bundle.js'],
stylesheets: ['public/styles.css'],
asyncStylesheets: ['public/async.css'],
pageTitle: pageData.title,
context: appContext
}

const layoutProps = {
navigationData: response.locals.navigation
}

try {
const document = (
<Shell {...shellProps}>
<Layout {...layoutProps} contents={pageData.contents} />
</Shell>
)

response.send('<!DOCTYPE html>' + ReactDOM.renderToString(document))
} catch (error) {
next(error)
}
}
9 changes: 9 additions & 0 deletions examples/ft-ui-async-stylesheets/server/start.js
Original file line number Diff line number Diff line change
@@ -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
})
4 changes: 4 additions & 0 deletions packages/dotcom-ui-shell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ An array of stylesheet URLs to be loaded using `<link rel="stylesheet" />` 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 `<link rel="preload" />` tags.
Copy link
Contributor Author

@adambraimbridge adambraimbridge Oct 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarify this reasoning: Why? (not just how)
Also update the 'stylesheets' above


#### `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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Subject {...props} />).toJSON()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,29 @@ exports[`dotcom-ui-shell/src/components/Shell renders the GTM script when the en
id="initial-props"
type="application/json"
/>
<noscript />
<script
dangerouslySetInnerHTML={
Object {
"__html": "(function loadAsyncStylesheets() {
var currentScript = document.scripts[document.scripts.length - 1];
var stylesheets = currentScript.getAttribute('data-stylesheets').split(',');
for (var i = 0, len = stylesheets.length; i < len; i++) {
var link = document.createElement('link');
link.href = stylesheets[i];
link.rel = 'stylesheet';
link.media = 'print'; // <-- 'print' is intentional; on load, it changes to 'all'.
link.onload = function (event) {
var target = event.target;
target.media = 'all';
};
currentScript.parentNode.insertBefore(link, currentScript);
}
})()",
}
}
data-stylesheets=""
/>
<script
dangerouslySetInnerHTML={
Object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,33 @@ Array [
href="path/to/styles.css"
rel="stylesheet"
/>,
<noscript>
<link
href="path/to/async.css"
rel="stylesheet"
/>
</noscript>,
<script
dangerouslySetInnerHTML={
Object {
"__html": "(function loadAsyncStylesheets() {
var currentScript = document.scripts[document.scripts.length - 1];
var stylesheets = currentScript.getAttribute('data-stylesheets').split(',');
for (var i = 0, len = stylesheets.length; i < len; i++) {
var link = document.createElement('link');
link.href = stylesheets[i];
link.rel = 'stylesheet';
link.media = 'print'; // <-- 'print' is intentional; on load, it changes to 'all'.
link.onload = function (event) {
var target = event.target;
target.media = 'all';
};
currentScript.parentNode.insertBefore(link, currentScript);
}
})()",
}
}
data-stylesheets="path/to/async.css"
/>,
]
`;
6 changes: 5 additions & 1 deletion packages/dotcom-ui-shell/src/components/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ function Shell(props: TShellProps) {
type="application/json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(props.initialProps) }}
/>
<StyleSheets stylesheets={props.stylesheets} criticalStyles={props.criticalStyles} />
<StyleSheets
stylesheets={props.stylesheets}
criticalStyles={props.criticalStyles}
asyncStylesheets={props.asyncStylesheets}
/>
<Bootstrap {...bootstrapProps} />
<GTMHead flags={props.flags} />
</head>
Expand Down
Loading