diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index 2877acecb5e5..bd8eafaac3a2 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -153,7 +153,7 @@ jobs: - run: cat package.json | jq '.resolutions."react-dom" = "^17.0.1"' > package.json.tmp && mv package.json.tmp package.json - run: yarn install --check-files - run: yarn list webpack react react-dom - - run: xvfb-run node run-tests.js test/integration/link-ref/test/index.test.js test/integration/production/test/index.test.js test/integration/basic/test/index.test.js test/integration/async-modules/test/index.test.js test/integration/font-optimization/test/index.test.js test/acceptance/*.test.js + - run: xvfb-run node run-tests.js test/integration/{link-ref,production,basic,async-modules,font-optimization,ssr-ctx}/test/index.test.js test/acceptance/*.test.js testFirefox: name: Test Firefox (production) diff --git a/docs/api-reference/next.config.js/rewrites.md b/docs/api-reference/next.config.js/rewrites.md index 28c040178919..c32a370eddab 100644 --- a/docs/api-reference/next.config.js/rewrites.md +++ b/docs/api-reference/next.config.js/rewrites.md @@ -154,9 +154,10 @@ module.exports = { destination: '/another', // automatically becomes /docs/another }, { - // does not add /docs since basePath: false is set + // does not add /docs to /without-basePath since basePath: false is set + // Note: this can not be used for internal rewrites e.g. `destination: '/another'` source: '/without-basePath', - destination: '/another', + destination: 'https://example.com', basePath: false, }, ] diff --git a/docs/api-reference/next/router.md b/docs/api-reference/next/router.md index 3c92861ce46e..56de35581fb5 100644 --- a/docs/api-reference/next/router.md +++ b/docs/api-reference/next/router.md @@ -49,6 +49,7 @@ The following is the definition of the `router` object returned by both [`useRou - `locale`: `String` - The active locale (if enabled). - `locales`: `String[]` - All supported locales (if enabled). - `defaultLocale`: `String` - The current default locale (if enabled). +- `isReady`: `boolean` - Whether the router fields are updated client-side and ready for use. Should only be used inside of `useEffect` methods and not for conditionally rendering on the server. Additionally, the following methods are also included inside `router`: diff --git a/errors/incompatible-href-as.md b/errors/incompatible-href-as.md index b6c4e68e1d88..f6677e90b6e7 100644 --- a/errors/incompatible-href-as.md +++ b/errors/incompatible-href-as.md @@ -11,13 +11,15 @@ Note: this error will only show when the `next/link` component is clicked not wh ```jsx import Link from 'next/link' -export default () => ( - <> - - Invalid link - - -) +export default function Page(props) { + return ( + <> + + Invalid link + + + ) +} ``` **Compatible `href` and `as`** @@ -25,13 +27,15 @@ export default () => ( ```jsx import Link from 'next/link' -export default () => ( - <> - - Valid link - - -) +export default function Page(props) { + return ( + <> + + Valid link + + + ) +} ``` #### Possible Ways to Fix It diff --git a/errors/invalid-relative-url-external-as.md b/errors/invalid-relative-url-external-as.md new file mode 100644 index 000000000000..848b56d422e4 --- /dev/null +++ b/errors/invalid-relative-url-external-as.md @@ -0,0 +1,47 @@ +# Invalid relative `href` and external `as` values + +#### Why This Error Occurred + +Somewhere you are utilizing the `next/link` component, `Router#push`, or `Router#replace` with a relative route in your `href` that has an external `as` value. The `as` value must be relative also or only `href` should be used with an external URL. + +Note: this error will only show when the `next/link` component is clicked not when only rendered. + +**Incompatible `href` and `as`** + +```jsx +import Link from 'next/link' + +export default function Page(props) { + return ( + <> + + Invalid link + + + ) +} +``` + +**Compatible `href` and `as`** + +```jsx +import Link from 'next/link' + +export default function Page(props) { + return ( + <> + + Invalid link + + + ) +} +``` + +#### Possible Ways to Fix It + +Look for any usage of the `next/link` component, `Router#push`, or `Router#replace` and make sure that the provided `href` and `as` values are compatible + +### Useful Links + +- [Routing section in Documentation](https://nextjs.org/docs/routing/introduction) diff --git a/examples/blog-starter-typescript/README.md b/examples/blog-starter-typescript/README.md index e8db3fc2498c..671760cb4b37 100644 --- a/examples/blog-starter-typescript/README.md +++ b/examples/blog-starter-typescript/README.md @@ -30,4 +30,6 @@ Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&ut # Notes -This blog-starter-typescript uses [Tailwind CSS](https://tailwindcss.com). To control the generated stylesheet's filesize, this example uses Tailwind CSS' v1.4 [`purge` option](https://tailwindcss.com/docs/controlling-file-size/#removing-unused-css) to remove unused CSS. +This blog-starter-typescript uses [Tailwind CSS](https://tailwindcss.com). To control the generated stylesheet's filesize, this example uses Tailwind CSS' v2.0 [`purge` option](https://tailwindcss.com/docs/controlling-file-size/#removing-unused-css) to remove unused CSS. + +[Tailwind CSS v2.0 no longer supports Node.js 8 or 10](https://tailwindcss.com/docs/upgrading-to-v2#upgrade-to-node-js-12-13-or-higher). To build your CSS you'll need to ensure you are running Node.js 12.13.0 or higher in both your local and CI environments. diff --git a/examples/blog-starter-typescript/components/hero-post.tsx b/examples/blog-starter-typescript/components/hero-post.tsx index 5bd84f9bbe0d..cd0f121c01fa 100644 --- a/examples/blog-starter-typescript/components/hero-post.tsx +++ b/examples/blog-starter-typescript/components/hero-post.tsx @@ -26,7 +26,7 @@ const HeroPost = ({
-
+

diff --git a/examples/blog-starter-typescript/components/more-stories.tsx b/examples/blog-starter-typescript/components/more-stories.tsx index 205c548f665a..1c2c038266e2 100644 --- a/examples/blog-starter-typescript/components/more-stories.tsx +++ b/examples/blog-starter-typescript/components/more-stories.tsx @@ -11,7 +11,7 @@ const MoreStories = ({ posts }: Props) => {

More Stories

-
+
{posts.map((post) => ( +} + +export default MyApp diff --git a/examples/with-mdbreact/pages/index.js b/examples/with-mdbreact/pages/index.js new file mode 100644 index 000000000000..63110767b2ef --- /dev/null +++ b/examples/with-mdbreact/pages/index.js @@ -0,0 +1,118 @@ +import Head from 'next/head' +import { + MDBBtn, + MDBCard, + MDBCardBody, + MDBCardText, + MDBCardTitle, + MDBCol, + MDBContainer, + MDBFooter, + MDBRow, +} from 'mdbreact' + +export default function Home() { + return ( + <> + + NextJS with Material Design Bootstrap for React + + + +

+ Welcome to Next.js! +

+

+ Get started by editing pages/index.js +

+ + + + + Documentation + + Find in-depth information about Next.js features and API. + + + More → + + + + + + + + Learn + + Learn about Next.js in an interactive course with quizzes! + + + More → + + + + + + + + + + Examples + + Discover and deploy boilerplate example Next.js projects. + + + More → + + + + + + + + Deploy + + Instantly deploy your Next.js site to a public URL with + Vercel. + + + More → + + + + + + + Powered by + + Vercel Logo + + +
+ + ) +} diff --git a/examples/with-mdbreact/public/favicon.ico b/examples/with-mdbreact/public/favicon.ico new file mode 100644 index 000000000000..4965832f2c9b Binary files /dev/null and b/examples/with-mdbreact/public/favicon.ico differ diff --git a/examples/with-mdbreact/public/vercel.svg b/examples/with-mdbreact/public/vercel.svg new file mode 100644 index 000000000000..fbf0e25a651c --- /dev/null +++ b/examples/with-mdbreact/public/vercel.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/examples/with-mdbreact/styles/globals.css b/examples/with-mdbreact/styles/globals.css new file mode 100644 index 000000000000..e5e2dcc23baf --- /dev/null +++ b/examples/with-mdbreact/styles/globals.css @@ -0,0 +1,16 @@ +html, +body { + padding: 0; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, + Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} + +* { + box-sizing: border-box; +} diff --git a/examples/with-mongodb-mongoose/package.json b/examples/with-mongodb-mongoose/package.json index 241b1abf9837..964cbcfc6094 100644 --- a/examples/with-mongodb-mongoose/package.json +++ b/examples/with-mongodb-mongoose/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "mongoose": "^5.9.13", - "next": "^9.4.2", + "next": "latest", "react": "^16.13.1", "react-dom": "^16.13.1", "swr": "0.2.2" diff --git a/examples/with-sentry/next.config.js b/examples/with-sentry/next.config.js index 1579ed872c50..96d8cd909a0a 100644 --- a/examples/with-sentry/next.config.js +++ b/examples/with-sentry/next.config.js @@ -1,7 +1,3 @@ -// Use the hidden-source-map option when you don't want the source maps to be -// publicly available on the servers, only to the error reporting -const withSourceMaps = require('@zeit/next-source-maps')() - // Use the SentryWebpack plugin to upload the source maps during build step const SentryWebpackPlugin = require('@sentry/webpack-plugin') const { @@ -23,7 +19,8 @@ const COMMIT_SHA = process.env.SENTRY_DSN = SENTRY_DSN const basePath = '' -module.exports = withSourceMaps({ +module.exports = { + productionBrowserSourceMaps: true, env: { // Make the COMMIT_SHA available to the client so that Sentry events can be // marked for the release they belong to. It may be undefined if running @@ -85,4 +82,4 @@ module.exports = withSourceMaps({ return config }, basePath, -}) +} diff --git a/examples/with-sentry/package.json b/examples/with-sentry/package.json index fc423c0f9523..ceeab004b551 100644 --- a/examples/with-sentry/package.json +++ b/examples/with-sentry/package.json @@ -12,7 +12,6 @@ "@sentry/integrations": "^5.21.3", "@sentry/node": "^5.21.3", "@sentry/webpack-plugin": "^1.12.1", - "@zeit/next-source-maps": "0.0.4-canary.1", "next": "latest", "react": "^16.8.6", "react-dom": "^16.8.6" diff --git a/lerna.json b/lerna.json index 708bf98ca957..f99045d90a5a 100644 --- a/lerna.json +++ b/lerna.json @@ -17,5 +17,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "10.0.5-canary.6" + "version": "10.0.5-canary.7" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 794badf4373d..ed6b4a01587d 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "10.0.5-canary.6", + "version": "10.0.5-canary.7", "keywords": [ "react", "next", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 6f111cdd8159..d34fde30e487 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "10.0.5-canary.6", + "version": "10.0.5-canary.7", "description": "ESLint plugin for NextJS.", "main": "lib/index.js", "license": "MIT", diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index a5647c72e7c5..10f1febdd7cd 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "10.0.5-canary.6", + "version": "10.0.5-canary.7", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index f41333cb8223..eb5235bd6cf6 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "10.0.5-canary.6", + "version": "10.0.5-canary.7", "license": "MIT", "dependencies": { "chalk": "4.1.0", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index ebb243d29484..e9ddd484316c 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "10.0.5-canary.6", + "version": "10.0.5-canary.7", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 3f7b1f46553f..3df3c34b49e5 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "10.0.5-canary.6", + "version": "10.0.5-canary.7", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-google-analytics/package.json b/packages/next-plugin-google-analytics/package.json index e26e8dbc5676..d566ef4ed4fc 100644 --- a/packages/next-plugin-google-analytics/package.json +++ b/packages/next-plugin-google-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-google-analytics", - "version": "10.0.5-canary.6", + "version": "10.0.5-canary.7", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-google-analytics" diff --git a/packages/next-plugin-sentry/package.json b/packages/next-plugin-sentry/package.json index 87d2d1c89a78..db357a6111d1 100644 --- a/packages/next-plugin-sentry/package.json +++ b/packages/next-plugin-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-sentry", - "version": "10.0.5-canary.6", + "version": "10.0.5-canary.7", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-sentry" diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 2e57cd71f092..b89590ad28d9 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "10.0.5-canary.6", + "version": "10.0.5-canary.7", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index bc0635da25d3..efa55d3b9bbc 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "10.0.5-canary.6", + "version": "10.0.5-canary.7", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 487c66bdc45e..626d37e42f13 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "10.0.5-canary.6", + "version": "10.0.5-canary.7", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next/build/babel/plugins/next-data.ts b/packages/next/build/babel/plugins/next-data.ts deleted file mode 100644 index 2ad51c9d74cd..000000000000 --- a/packages/next/build/babel/plugins/next-data.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - NodePath, - PluginObj, - types as BabelTypes, -} from 'next/dist/compiled/babel/core' - -export default function ({ - types: t, -}: { - types: typeof BabelTypes -}): PluginObj { - return { - visitor: { - ImportDeclaration(path: NodePath, state) { - const source = path.node.source.value - if (source !== 'next/data') return - - const createHookSpecifier = path.get('specifiers').find((specifier) => { - return ( - specifier.isImportSpecifier() && - (t.isIdentifier(specifier.node.imported) - ? specifier.node.imported.name - : specifier.node.imported.value) === 'createHook' - ) - }) - - if (!createHookSpecifier) return - - const bindingName = createHookSpecifier.node.local.name - const binding = path.scope.getBinding(bindingName) - - if (!binding) { - return - } - - binding.referencePaths.forEach((refPath) => { - let callExpression = refPath.parentPath - - if (!callExpression.isCallExpression()) return - - let args: any = callExpression.get('arguments') - - if (!args[0]) { - throw callExpression.buildCodeFrameError( - 'first argument to createHook should be a function' - ) - } - - if (!args[1]) { - callExpression.node.arguments.push(t.objectExpression([])) - } - - args = callExpression.get('arguments') - - args[1].node.properties.push( - t.objectProperty( - t.identifier('key'), - t.stringLiteral(state.opts.key) - ) - ) - }) - }, - }, - } -} diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 39a1c1371b9d..d370ca5970a5 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -211,8 +211,6 @@ export default async function getBaseWebpackConfig( rewrites: Rewrite[] } ): Promise { - const productionBrowserSourceMaps = - config.productionBrowserSourceMaps && !isServer let plugins: PluginMetaData[] = [] let babelPresetPlugins: { dir: string; config: any }[] = [] @@ -844,7 +842,6 @@ export default async function getBaseWebpackConfig( 'error-loader', 'next-babel-loader', 'next-client-pages-loader', - 'next-data-loader', 'next-serverless-loader', 'noop-loader', 'next-plugin-loader', @@ -1216,7 +1213,7 @@ export default async function getBaseWebpackConfig( isServer, assetPrefix: config.assetPrefix || '', sassOptions: config.sassOptions, - productionBrowserSourceMaps, + productionBrowserSourceMaps: config.productionBrowserSourceMaps, }) let originalDevtool = webpackConfig.devtool diff --git a/packages/next/build/webpack/config/blocks/base.ts b/packages/next/build/webpack/config/blocks/base.ts index 386a30369431..34b117a44081 100644 --- a/packages/next/build/webpack/config/blocks/base.ts +++ b/packages/next/build/webpack/config/blocks/base.ts @@ -35,8 +35,8 @@ export const base = curry(function base( config.devtool = 'eval-source-map' } } else { - // Enable browser sourcemaps - if (ctx.productionBrowserSourceMaps) { + // Enable browser sourcemaps: + if (ctx.productionBrowserSourceMaps && ctx.isClient) { config.devtool = 'source-map' } else { config.devtool = false diff --git a/packages/next/build/webpack/loaders/next-babel-loader.js b/packages/next/build/webpack/loaders/next-babel-loader.js index c1ba84b6b8a8..3f47a6cb4eee 100644 --- a/packages/next/build/webpack/loaders/next-babel-loader.js +++ b/packages/next/build/webpack/loaders/next-babel-loader.js @@ -1,7 +1,6 @@ -import babelLoader from './babel-loader/src/index' -import hash from 'next/dist/compiled/string-hash' -import { basename, join } from 'path' +import { join } from 'path' import * as Log from '../../output/log' +import babelLoader from './babel-loader/src/index' // increment 'o' to invalidate cache // eslint-disable-next-line no-useless-concat @@ -149,17 +148,6 @@ const customBabelLoader = babelLoader((babel) => { options.plugins.push(diallowExportAll) } - if (isServer && source.indexOf('next/data') !== -1) { - const nextDataPlugin = babel.createConfigItem( - [ - require('../../babel/plugins/next-data'), - { key: basename(filename) + '-' + hash(filename) }, - ], - { type: 'plugin' } - ) - options.plugins.push(nextDataPlugin) - } - // If the file has `module.exports` we have to transpile commonjs because Babel adds `import` statements // That break webpack, since webpack doesn't support combining commonjs and esmodules if (source.indexOf('module.exports') !== -1) { diff --git a/packages/next/build/webpack/loaders/next-data-loader.ts b/packages/next/build/webpack/loaders/next-data-loader.ts deleted file mode 100644 index db1ef36351f2..000000000000 --- a/packages/next/build/webpack/loaders/next-data-loader.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { loader } from 'webpack' -import hash from 'next/dist/compiled/string-hash' -import { basename } from 'path' -const nextDataLoader: loader.Loader = function () { - const filename = this.resourcePath - return ` - import {createHook} from 'next/data' - - export default createHook(undefined, {key: ${JSON.stringify( - basename(filename) + '-' + hash(filename) - )}}) - ` -} - -export default nextDataLoader diff --git a/packages/next/build/webpack/plugins/nextjs-require-cache-hot-reloader.ts b/packages/next/build/webpack/plugins/nextjs-require-cache-hot-reloader.ts index 3d9276b25314..cdc3df03dcb7 100644 --- a/packages/next/build/webpack/plugins/nextjs-require-cache-hot-reloader.ts +++ b/packages/next/build/webpack/plugins/nextjs-require-cache-hot-reloader.ts @@ -38,20 +38,28 @@ export class NextJsRequireCacheHotReloader implements Plugin { compilation.outputOptions.path, 'webpack-runtime.js' ) - deleteCache(runtimeChunkPath) - for (const outputPath of this.previousOutputPathsWebpack5) { - if (!this.currentOutputPathsWebpack5.has(outputPath)) { - deleteCache(outputPath) - } - } - - this.previousOutputPathsWebpack5 = new Set( - this.currentOutputPathsWebpack5 + // we need to make sure to clear all server entries from cache + // since they can have a stale webpack-runtime cache + // which needs to always be in-sync + const entries = [...compilation.entries.keys()].filter((entry) => + entry.toString().startsWith('pages/') ) - this.currentOutputPathsWebpack5.clear() + + entries.forEach((page) => { + const outputPath = path.join( + compilation.outputOptions.path, + page + '.js' + ) + deleteCache(outputPath) + }) }) + + this.previousOutputPathsWebpack5 = new Set( + this.currentOutputPathsWebpack5 + ) + this.currentOutputPathsWebpack5.clear() return } diff --git a/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js b/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js index d4d4edd3a143..0d9273ba4cf1 100644 --- a/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js +++ b/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js @@ -226,11 +226,9 @@ class TerserPlugin { return traceAsyncFn(assetSpan, async () => { if (!output) { - let inputSourceMap - const { source: sourceFromInputSource, - map, + map: inputSourceMap, } = inputSource.sourceAndMap() const input = Buffer.isBuffer(sourceFromInputSource) @@ -240,7 +238,7 @@ class TerserPlugin { const options = { name, input, - inputSourceMap: map, + inputSourceMap, terserOptions: { ...this.options.terserOptions }, } @@ -337,8 +335,8 @@ class TerserPlugin { }) const handleHashForChunk = (hash, chunk) => { - // increment 'b' to invalidate cache - hash.update('b') + // increment 'c' to invalidate cache + hash.update('c') } if (isWebpack5) { diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index a7a10867df27..52e9421302e3 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -130,10 +130,11 @@ type GenImgAttrsData = { sizes?: string } -type GenImgAttrsResult = Pick< - JSX.IntrinsicElements['img'], - 'src' | 'sizes' | 'srcSet' -> +type GenImgAttrsResult = { + src: string + srcSet: string | undefined + sizes: string | undefined +} function generateImgAttrs({ src, @@ -144,7 +145,7 @@ function generateImgAttrs({ sizes, }: GenImgAttrsData): GenImgAttrsResult { if (unoptimized) { - return { src } + return { src, srcSet: undefined, sizes: undefined } } const { widths, kind } = getWidths(width, layout) @@ -364,6 +365,8 @@ export default function Image({ let imgAttributes: GenImgAttrsResult = { src: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', + srcSet: undefined, + sizes: undefined, } if (isVisible) { diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index 8386a646713a..c155ce3e1d14 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -45,7 +45,7 @@ declare global { type RenderRouteInfo = PrivateRouteInfo & { App: AppComponent - scroll?: boolean + scroll?: { x: number; y: number } | null } type RenderErrorProps = Omit @@ -753,7 +753,7 @@ function doRender(input: RenderRouteInfo): Promise { } if (input.scroll) { - window.scrollTo(0, 0) + window.scrollTo(input.scroll.x, input.scroll.y) } } diff --git a/packages/next/client/router.ts b/packages/next/client/router.ts index cff79525bd50..093a310f3b60 100644 --- a/packages/next/client/router.ts +++ b/packages/next/client/router.ts @@ -40,6 +40,7 @@ const urlPropertyFields = [ 'locale', 'locales', 'defaultLocale', + 'isReady', ] const routerEvents = [ 'routeChangeStart', diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index 9b141348f8b9..1208d86e4e08 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -25,6 +25,7 @@ import { loadGetInitialProps, NextPageContext, ST, + NEXT_DATA, } from '../utils' import { isDynamicRoute } from './utils/is-dynamic' import { parseRelativeUrl } from './utils/parse-relative-url' @@ -33,6 +34,13 @@ import resolveRewrites from './utils/resolve-rewrites' import { getRouteMatcher } from './utils/route-matcher' import { getRouteRegex } from './utils/route-regex' +declare global { + interface Window { + /* prod */ + __NEXT_DATA__: NEXT_DATA + } +} + interface RouteProperties { shallow: boolean } @@ -49,7 +57,10 @@ interface NextHistoryState { options: TransitionOptions } -type HistoryState = null | { __N: false } | ({ __N: true } & NextHistoryState) +type HistoryState = + | null + | { __N: false } + | ({ __N: true; idx: number } & NextHistoryState) let detectDomainLocale: typeof import('../i18n/detect-domain-locale').detectDomainLocale @@ -355,7 +366,7 @@ export type AppComponent = ComponentType type Subscription = ( data: PrivateRouteInfo, App: AppComponent, - resetScroll: boolean + resetScroll: { x: number; y: number } | null ) => Promise type BeforePopStateCallback = (state: NextHistoryState) => boolean @@ -367,7 +378,14 @@ type HistoryMethod = 'replaceState' | 'pushState' const manualScrollRestoration = process.env.__NEXT_SCROLL_RESTORATION && typeof window !== 'undefined' && - 'scrollRestoration' in window.history + 'scrollRestoration' in window.history && + !!(function () { + try { + let v = '__next' + // eslint-disable-next-line no-sequences + return sessionStorage.setItem(v, v), sessionStorage.removeItem(v), true + } catch (n) {} + })() const SSG_DATA_NOT_FOUND = Symbol('SSG_DATA_NOT_FOUND') @@ -444,6 +462,9 @@ export default class Router implements BaseRouter { locales?: string[] defaultLocale?: string domainLocales?: DomainLocales + isReady: boolean + + private _idx: number = 0 static events: MittEmitter = mitt() @@ -515,8 +536,7 @@ export default class Router implements BaseRouter { // if auto prerendered and dynamic route wait to update asPath // until after mount to prevent hydration mismatch this.asPath = - // @ts-ignore this is temporarily global (attached to window) - isDynamicRoute(pathname) && __NEXT_DATA__.autoExport ? pathname : as + isDynamicRoute(pathname) && self.__NEXT_DATA__.autoExport ? pathname : as this.basePath = basePath this.sub = subscription this.clc = null @@ -527,6 +547,12 @@ export default class Router implements BaseRouter { this.isFallback = isFallback + this.isReady = !!( + self.__NEXT_DATA__.gssp || + self.__NEXT_DATA__.gip || + !self.location.search + ) + if (process.env.__NEXT_I18N_SUPPORT) { this.locale = locale this.locales = locales @@ -555,27 +581,6 @@ export default class Router implements BaseRouter { if (process.env.__NEXT_SCROLL_RESTORATION) { if (manualScrollRestoration) { window.history.scrollRestoration = 'manual' - - let scrollDebounceTimeout: undefined | NodeJS.Timeout - - const debouncedScrollSave = () => { - if (scrollDebounceTimeout) clearTimeout(scrollDebounceTimeout) - - scrollDebounceTimeout = setTimeout(() => { - const { url, as: curAs, options } = history.state - this.changeState( - 'replaceState', - url, - curAs, - Object.assign({}, options, { - _N_X: window.scrollX, - _N_Y: window.scrollY, - }) - ) - }, 10) - } - - window.addEventListener('scroll', debouncedScrollSave) } } } @@ -607,7 +612,30 @@ export default class Router implements BaseRouter { return } - const { url, as, options } = state + let forcedScroll: { x: number; y: number } | undefined + const { url, as, options, idx } = state + if (process.env.__NEXT_SCROLL_RESTORATION) { + if (manualScrollRestoration) { + if (this._idx !== idx) { + // Snapshot current scroll position: + try { + sessionStorage.setItem( + '__next_scroll_' + this._idx, + JSON.stringify({ x: self.pageXOffset, y: self.pageYOffset }) + ) + } catch {} + + // Restore old scroll position: + try { + const v = sessionStorage.getItem('__next_scroll_' + idx) + forcedScroll = JSON.parse(v!) + } catch { + forcedScroll = { x: 0, y: 0 } + } + } + } + } + this._idx = idx const { pathname } = parseRelativeUrl(url) @@ -627,10 +655,11 @@ export default class Router implements BaseRouter { 'replaceState', url, as, - Object.assign({}, options, { + Object.assign<{}, TransitionOptions, TransitionOptions>({}, options, { shallow: options.shallow && this._shallow, locale: options.locale || this.defaultLocale, - }) + }), + forcedScroll ) } @@ -652,6 +681,19 @@ export default class Router implements BaseRouter { * @param options object you can define `shallow` and other options */ push(url: Url, as?: Url, options: TransitionOptions = {}) { + if (process.env.__NEXT_SCROLL_RESTORATION) { + // TODO: remove in the future when we update history before route change + // is complete, as the popstate event should handle this capture. + if (manualScrollRestoration) { + try { + // Snapshot scroll position right before navigating to a new page: + sessionStorage.setItem( + '__next_scroll_' + this._idx, + JSON.stringify({ x: self.pageXOffset, y: self.pageYOffset }) + ) + } catch {} + } + } ;({ url, as } = prepareUrlAs(this, url, as)) return this.change('pushState', url, as, options) } @@ -667,17 +709,24 @@ export default class Router implements BaseRouter { return this.change('replaceState', url, as, options) } - async change( + private async change( method: HistoryMethod, url: string, as: string, - options: TransitionOptions + options: TransitionOptions, + forcedScroll?: { x: number; y: number } ): Promise { if (!isLocalURL(url)) { window.location.href = url return false } + // for static pages with query params in the URL we delay + // marking the router ready until after the query is updated + if ((options as any)._h) { + this.isReady = true + } + // Default to scroll reset behavior unless explicitly specified to be // `false`! This makes the behavior between using `Router#push` and a // `` consistent. @@ -804,7 +853,7 @@ export default class Router implements BaseRouter { // TODO: do we need the resolved href when only a hash change? this.changeState(method, url, as, options) this.scrollToHash(cleanedAs) - this.notify(this.components[this.route], false) + this.notify(this.components[this.route], null) Router.events.emit('hashChangeComplete', as, routeProps) return true } @@ -855,7 +904,7 @@ export default class Router implements BaseRouter { // pages to allow building the data URL correctly let resolvedAs = as - if (process.env.__NEXT_HAS_REWRITES) { + if (process.env.__NEXT_HAS_REWRITES && as.startsWith('/')) { resolvedAs = resolveRewrites( addBasePath( addLocale(delBasePath(parseRelativeUrl(as).pathname), this.locale) @@ -891,6 +940,19 @@ export default class Router implements BaseRouter { } } } + + if (!isLocalURL(as)) { + if (process.env.NODE_ENV !== 'production') { + throw new Error( + `Invalid href: "${url}" and as: "${as}", received relative href and external as` + + `\nSee more info: https://err.sh/next.js/invalid-relative-url-external-as` + ) + } + + window.location.href = as + return false + } + resolvedAs = delLocale(delBasePath(resolvedAs), this.locale) if (isDynamicRoute(route)) { @@ -1024,7 +1086,7 @@ export default class Router implements BaseRouter { query, cleanedAs, routeInfo, - !!options.scroll + forcedScroll || (options.scroll ? { x: 0, y: 0 } : null) ).catch((e) => { if (e.cancelled) error = error || e else throw e @@ -1035,12 +1097,6 @@ export default class Router implements BaseRouter { throw error } - if (process.env.__NEXT_SCROLL_RESTORATION) { - if (manualScrollRestoration && '_N_X' in options) { - window.scrollTo((options as any)._N_X, (options as any)._N_Y) - } - } - if (process.env.__NEXT_I18N_SUPPORT) { if (this.locale) { document.documentElement.lang = this.locale @@ -1083,6 +1139,7 @@ export default class Router implements BaseRouter { as, options, __N: true, + idx: this._idx = method !== 'pushState' ? this._idx : this._idx + 1, } as HistoryState, // Most browsers currently ignores this parameter, although they may use it in the future. // Passing the empty string here should be safe against future changes to the method. @@ -1250,7 +1307,7 @@ export default class Router implements BaseRouter { query: ParsedUrlQuery, as: string, data: PrivateRouteInfo, - resetScroll: boolean + resetScroll: { x: number; y: number } | null ): Promise { this.isFallback = false @@ -1497,7 +1554,10 @@ export default class Router implements BaseRouter { } } - notify(data: PrivateRouteInfo, resetScroll: boolean): Promise { + notify( + data: PrivateRouteInfo, + resetScroll: { x: number; y: number } | null + ): Promise { return this.sub( data, this.components['/_app'].Component as AppComponent, diff --git a/packages/next/next-server/lib/router/utils/parse-relative-url.ts b/packages/next/next-server/lib/router/utils/parse-relative-url.ts index 6bbe188c4c8d..219dd3b5b420 100644 --- a/packages/next/next-server/lib/router/utils/parse-relative-url.ts +++ b/packages/next/next-server/lib/router/utils/parse-relative-url.ts @@ -17,7 +17,7 @@ export function parseRelativeUrl(url: string, base?: string) { resolvedBase ) if (origin !== globalBase.origin) { - throw new Error('invariant: invalid relative URL') + throw new Error(`invariant: invalid relative URL, router received ${url}`) } return { pathname, diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index 25ebe5c47a52..8601f3109f69 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -72,6 +72,7 @@ class ServerRouter implements NextRouter { events: any isFallback: boolean locale?: string + isReady: boolean locales?: string[] defaultLocale?: string domainLocales?: DomainLocales @@ -83,6 +84,7 @@ class ServerRouter implements NextRouter { query: ParsedUrlQuery, as: string, { isFallback }: { isFallback: boolean }, + isReady: boolean, basePath: string, locale?: string, locales?: string[], @@ -98,8 +100,10 @@ class ServerRouter implements NextRouter { this.locale = locale this.locales = locales this.defaultLocale = defaultLocale + this.isReady = isReady this.domainLocales = domainLocales } + push(): any { noRouter() } @@ -526,6 +530,7 @@ export async function renderToHTML( // url will always be set const asPath: string = renderOpts.resolvedAsPath || (req.url as string) + const routerIsReady = !!(getServerSideProps || hasPageGetInitialProps) const router = new ServerRouter( pathname, query, @@ -533,6 +538,7 @@ export async function renderToHTML( { isFallback: isFallback, }, + routerIsReady, basePath, renderOpts.locale, renderOpts.locales, diff --git a/packages/next/package.json b/packages/next/package.json index 73b9583e7db7..dbb067bcba58 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "10.0.5-canary.6", + "version": "10.0.5-canary.7", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -63,10 +63,10 @@ "@ampproject/toolbox-optimizer": "2.7.1-alpha.0", "@babel/runtime": "7.12.5", "@hapi/accept": "5.0.1", - "@next/env": "10.0.5-canary.6", - "@next/polyfill-module": "10.0.5-canary.6", - "@next/react-dev-overlay": "10.0.5-canary.6", - "@next/react-refresh-utils": "10.0.5-canary.6", + "@next/env": "10.0.5-canary.7", + "@next/polyfill-module": "10.0.5-canary.7", + "@next/react-dev-overlay": "10.0.5-canary.7", + "@next/react-refresh-utils": "10.0.5-canary.7", "@opentelemetry/api": "0.14.0", "ast-types": "0.13.2", "babel-plugin-transform-define": "2.0.0", @@ -130,7 +130,7 @@ "@babel/preset-react": "7.12.10", "@babel/preset-typescript": "7.12.7", "@babel/types": "7.12.12", - "@next/polyfill-nomodule": "10.0.5-canary.6", + "@next/polyfill-nomodule": "10.0.5-canary.7", "@taskr/clear": "1.1.0", "@taskr/esnext": "1.1.0", "@taskr/watch": "1.1.0", diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index fce3b3da0287..c7d59f27f041 100644 --- a/packages/react-dev-overlay/package.json +++ b/packages/react-dev-overlay/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-dev-overlay", - "version": "10.0.5-canary.6", + "version": "10.0.5-canary.7", "description": "A development-only overlay for developing React applications.", "repository": { "url": "vercel/next.js", @@ -17,11 +17,12 @@ }, "dependencies": { "@babel/code-frame": "7.12.11", - "ally.js": "1.4.1", "anser": "1.4.9", "chalk": "4.0.0", "classnames": "2.2.6", + "css.escape": "1.5.1", "data-uri-to-buffer": "3.0.1", + "platform": "1.3.6", "shell-quote": "1.7.2", "source-map": "0.8.0-beta.0", "stacktrace-parser": "0.1.10", diff --git a/packages/react-dev-overlay/src/internal/components/CodeFrame/styles.tsx b/packages/react-dev-overlay/src/internal/components/CodeFrame/styles.tsx index 594fb8c1270f..0c2ea8311815 100644 --- a/packages/react-dev-overlay/src/internal/components/CodeFrame/styles.tsx +++ b/packages/react-dev-overlay/src/internal/components/CodeFrame/styles.tsx @@ -44,7 +44,7 @@ const styles = css` [data-nextjs-codeframe] > p > svg { width: auto; height: 1em; - margin-left: 0.5rem; + margin-left: 8px; } ` diff --git a/packages/react-dev-overlay/src/internal/components/Dialog/styles.ts b/packages/react-dev-overlay/src/internal/components/Dialog/styles.ts index f47b7db36605..d4730a77c85a 100644 --- a/packages/react-dev-overlay/src/internal/components/Dialog/styles.ts +++ b/packages/react-dev-overlay/src/internal/components/Dialog/styles.ts @@ -12,7 +12,7 @@ const styles = css` border-radius: var(--size-gap); box-shadow: 0 var(--size-gap-half) var(--size-gap-double) rgba(0, 0, 0, 0.25); - max-height: calc(100% - 3.5rem); + max-height: calc(100% - 56px); overflow-y: hidden; } diff --git a/packages/react-dev-overlay/src/internal/components/Overlay/Overlay.tsx b/packages/react-dev-overlay/src/internal/components/Overlay/Overlay.tsx index 3d3bdaae3993..22249c716d91 100644 --- a/packages/react-dev-overlay/src/internal/components/Overlay/Overlay.tsx +++ b/packages/react-dev-overlay/src/internal/components/Overlay/Overlay.tsx @@ -1,7 +1,5 @@ // @ts-ignore -import allyDisable from 'ally.js/maintain/disabled' -// @ts-ignore -import allyTrap from 'ally.js/maintain/tab-focus' +import allyTrap from './maintain--tab-focus' import * as React from 'react' import { lock, unlock } from './body-locker' @@ -29,10 +27,8 @@ const Overlay: React.FC = function Overlay({ return } - const handle1 = allyDisable({ filter: overlay }) const handle2 = allyTrap({ context: overlay }) return () => { - handle1.disengage() handle2.disengage() } }, [overlay]) diff --git a/packages/react-dev-overlay/src/internal/components/Overlay/maintain--tab-focus.ts b/packages/react-dev-overlay/src/internal/components/Overlay/maintain--tab-focus.ts new file mode 100644 index 000000000000..879db0409401 --- /dev/null +++ b/packages/react-dev-overlay/src/internal/components/Overlay/maintain--tab-focus.ts @@ -0,0 +1,3562 @@ +/* eslint-disable */ +// @ts-nocheck +// Copied from https://github.com/medialize/ally.js +// License: MIT +// Copyright (c) 2015 Rodney Rehm +// +// Entrypoint: ally.js/maintain/tab-focus + +import _platform from 'platform' +import cssEscape from 'css.escape' + +// input may be undefined, selector-tring, Node, NodeList, HTMLCollection, array of Nodes +// yes, to some extent this is a bad replica of jQuery's constructor function +function nodeArray(input) { + if (!input) { + return [] + } + + if (Array.isArray(input)) { + return input + } + + // instanceof Node - does not work with iframes + if (input.nodeType !== undefined) { + return [input] + } + + if (typeof input === 'string') { + input = document.querySelectorAll(input) + } + + if (input.length !== undefined) { + return [].slice.call(input, 0) + } + + throw new TypeError('unexpected input ' + String(input)) +} + +function contextToElement(_ref) { + var context = _ref.context, + _ref$label = _ref.label, + label = _ref$label === undefined ? 'context-to-element' : _ref$label, + resolveDocument = _ref.resolveDocument, + defaultToDocument = _ref.defaultToDocument + + var element = nodeArray(context)[0] + + if (resolveDocument && element && element.nodeType === Node.DOCUMENT_NODE) { + element = element.documentElement + } + + if (!element && defaultToDocument) { + return document.documentElement + } + + if (!element) { + throw new TypeError(label + ' requires valid options.context') + } + + if ( + element.nodeType !== Node.ELEMENT_NODE && + element.nodeType !== Node.DOCUMENT_FRAGMENT_NODE + ) { + throw new TypeError(label + ' requires options.context to be an Element') + } + + return element +} + +function getShadowHost() { + var _ref = + arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, + context = _ref.context + + var element = contextToElement({ + label: 'get/shadow-host', + context: context, + }) + + // walk up to the root + var container = null + + while (element) { + container = element + element = element.parentNode + } + + // https://developer.mozilla.org/en-US/docs/Web/API/Node.nodeType + // NOTE: Firefox 34 does not expose ShadowRoot.host (but 37 does) + if ( + container.nodeType === container.DOCUMENT_FRAGMENT_NODE && + container.host + ) { + // the root is attached to a fragment node that has a host + return container.host + } + + return null +} + +function getDocument(node) { + if (!node) { + return document + } + + if (node.nodeType === Node.DOCUMENT_NODE) { + return node + } + + return node.ownerDocument || document +} + +function isActiveElement(context) { + var element = contextToElement({ + label: 'is/active-element', + resolveDocument: true, + context: context, + }) + + var _document = getDocument(element) + if (_document.activeElement === element) { + return true + } + + var shadowHost = getShadowHost({ context: element }) + if (shadowHost && shadowHost.shadowRoot.activeElement === element) { + return true + } + + return false +} + +// [elem, elem.parent, elem.parent.parent, …, html] +// will not contain the shadowRoot (DOCUMENT_FRAGMENT_NODE) and shadowHost +function getParents() { + var _ref = + arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, + context = _ref.context + + var list = [] + var element = contextToElement({ + label: 'get/parents', + context: context, + }) + + while (element) { + list.push(element) + // IE does know support parentElement on SVGElement + element = element.parentNode + if (element && element.nodeType !== Node.ELEMENT_NODE) { + element = null + } + } + + return list +} + +// Element.prototype.matches may be available at a different name +// https://developer.mozilla.org/en/docs/Web/API/Element/matches + +var names = [ + 'matches', + 'webkitMatchesSelector', + 'mozMatchesSelector', + 'msMatchesSelector', +] +var name = null + +function findMethodName(element) { + names.some(function (_name) { + if (!element[_name]) { + return false + } + + name = _name + return true + }) +} + +function elementMatches(element, selector) { + if (!name) { + findMethodName(element) + } + + return element[name](selector) +} + +// deep clone of original platform +var platform = JSON.parse(JSON.stringify(_platform)) + +// operating system +var os = platform.os.family || '' +var ANDROID = os === 'Android' +var WINDOWS = os.slice(0, 7) === 'Windows' +var OSX = os === 'OS X' +var IOS = os === 'iOS' + +// layout +var BLINK = platform.layout === 'Blink' +var GECKO = platform.layout === 'Gecko' +var TRIDENT = platform.layout === 'Trident' +var EDGE = platform.layout === 'EdgeHTML' +var WEBKIT = platform.layout === 'WebKit' + +// browser version (not layout engine version!) +var version = parseFloat(platform.version) +var majorVersion = Math.floor(version) +platform.majorVersion = majorVersion + +platform.is = { + // operating system + ANDROID: ANDROID, + WINDOWS: WINDOWS, + OSX: OSX, + IOS: IOS, + // layout + BLINK: BLINK, // "Chrome", "Chrome Mobile", "Opera" + GECKO: GECKO, // "Firefox" + TRIDENT: TRIDENT, // "Internet Explorer" + EDGE: EDGE, // "Microsoft Edge" + WEBKIT: WEBKIT, // "Safari" + // INTERNET EXPLORERS + IE9: TRIDENT && majorVersion === 9, + IE10: TRIDENT && majorVersion === 10, + IE11: TRIDENT && majorVersion === 11, +} + +function before() { + var data = { + // remember what had focus to restore after test + activeElement: document.activeElement, + // remember scroll positions to restore after test + windowScrollTop: window.scrollTop, + windowScrollLeft: window.scrollLeft, + bodyScrollTop: document.body.scrollTop, + bodyScrollLeft: document.body.scrollLeft, + } + + // wrap tests in an element hidden from screen readers to prevent them + // from announcing focus, which can be quite irritating to the user + var iframe = document.createElement('iframe') + iframe.setAttribute( + 'style', + 'position:absolute; position:fixed; top:0; left:-2px; width:1px; height:1px; overflow:hidden;' + ) + iframe.setAttribute('aria-live', 'off') + iframe.setAttribute('aria-busy', 'true') + iframe.setAttribute('aria-hidden', 'true') + document.body.appendChild(iframe) + + var _window = iframe.contentWindow + var _document = _window.document + + _document.open() + _document.close() + var wrapper = _document.createElement('div') + _document.body.appendChild(wrapper) + + data.iframe = iframe + data.wrapper = wrapper + data.window = _window + data.document = _document + + return data +} + +// options.element: +// {string} element name +// {function} callback(wrapper, document) to generate an element +// options.mutate: (optional) +// {function} callback(element, wrapper, document) to manipulate element prior to focus-test. +// Can return DOMElement to define focus target (default: element) +// options.validate: (optional) +// {function} callback(element, focusTarget, document) to manipulate test-result +function test(data, options) { + // make sure we operate on a clean slate + data.wrapper.innerHTML = '' + // create dummy element to test focusability of + var element = + typeof options.element === 'string' + ? data.document.createElement(options.element) + : options.element(data.wrapper, data.document) + // allow callback to further specify dummy element + // and optionally define element to focus + var focus = + options.mutate && options.mutate(element, data.wrapper, data.document) + if (!focus && focus !== false) { + focus = element + } + // element needs to be part of the DOM to be focusable + !element.parentNode && data.wrapper.appendChild(element) + // test if the element with invalid tabindex can be focused + focus && focus.focus && focus.focus() + // validate test's result + return options.validate + ? options.validate(element, focus, data.document) + : data.document.activeElement === focus +} + +function after(data) { + // restore focus to what it was before test and cleanup + if (data.activeElement === document.body) { + document.activeElement && + document.activeElement.blur && + document.activeElement.blur() + if (platform.is.IE10) { + // IE10 does not redirect focus to when the activeElement is removed + document.body.focus() + } + } else { + data.activeElement && data.activeElement.focus && data.activeElement.focus() + } + + document.body.removeChild(data.iframe) + + // restore scroll position + window.scrollTop = data.windowScrollTop + window.scrollLeft = data.windowScrollLeft + document.body.scrollTop = data.bodyScrollTop + document.body.scrollLeft = data.bodyScrollLeft +} + +function detectFocus(tests) { + var data = before() + + var results = {} + Object.keys(tests).map(function (key) { + results[key] = test(data, tests[key]) + }) + + after(data) + return results +} + +// this file is overwritten by `npm run build:pre` +var version$1 = '1.4.1' + +/* + Facility to cache test results in localStorage. + + USAGE: + cache.get('key'); + cache.set('key', 'value'); + */ + +function readLocalStorage(key) { + // allow reading from storage to retrieve previous support results + // even while the document does not have focus + var data = void 0 + + try { + data = window.localStorage && window.localStorage.getItem(key) + data = data ? JSON.parse(data) : {} + } catch (e) { + data = {} + } + + return data +} + +function writeLocalStorage(key, value) { + if (!document.hasFocus()) { + // if the document does not have focus when tests are executed, focus() may + // not be handled properly and events may not be dispatched immediately. + // This can happen when a document is reloaded while Developer Tools have focus. + try { + window.localStorage && window.localStorage.removeItem(key) + } catch (e) { + // ignore + } + + return + } + + try { + window.localStorage && + window.localStorage.setItem(key, JSON.stringify(value)) + } catch (e) { + // ignore + } +} + +var userAgent = + (typeof window !== 'undefined' && window.navigator.userAgent) || '' +var cacheKey = 'ally-supports-cache' +var cache = readLocalStorage(cacheKey) + +// update the cache if ally or the user agent changed (newer version, etc) +if (cache.userAgent !== userAgent || cache.version !== version$1) { + cache = {} +} + +cache.userAgent = userAgent +cache.version = version$1 + +var cache$1 = { + get: function get() { + return cache + }, + set: function set(values) { + Object.keys(values).forEach(function (key) { + cache[key] = values[key] + }) + + cache.time = new Date().toISOString() + writeLocalStorage(cacheKey, cache) + }, +} + +function cssShadowPiercingDeepCombinator() { + var combinator = void 0 + + // see https://dev.w3.org/csswg/css-scoping-1/#deep-combinator + // https://bugzilla.mozilla.org/show_bug.cgi?id=1117572 + // https://code.google.com/p/chromium/issues/detail?id=446051 + try { + document.querySelector('html >>> :first-child') + combinator = '>>>' + } catch (noArrowArrowArrow) { + try { + // old syntax supported at least up to Chrome 41 + // https://code.google.com/p/chromium/issues/detail?id=446051 + document.querySelector('html /deep/ :first-child') + combinator = '/deep/' + } catch (noDeep) { + combinator = '' + } + } + + return combinator +} + +var gif = + 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' + +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-usemap +var focusAreaImgTabindex = { + element: 'div', + mutate: function mutate(element) { + element.innerHTML = + '' + + '' + + '' + + return element.querySelector('area') + }, +} + +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-usemap +var focusAreaTabindex = { + element: 'div', + mutate: function mutate(element) { + element.innerHTML = + '' + + '' + + '' + + return false + }, + validate: function validate(element, focusTarget, _document) { + if (platform.is.GECKO) { + // fixes https://github.com/medialize/ally.js/issues/35 + // Firefox loads the DataURI asynchronously, causing a false-negative + return true + } + + var focus = element.querySelector('area') + focus.focus() + return _document.activeElement === focus + }, +} + +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-usemap +var focusAreaWithoutHref = { + element: 'div', + mutate: function mutate(element) { + element.innerHTML = + '' + + '' + + '' + + return element.querySelector('area') + }, + validate: function validate(element, focusTarget, _document) { + if (platform.is.GECKO) { + // fixes https://github.com/medialize/ally.js/issues/35 + // Firefox loads the DataURI asynchronously, causing a false-negative + return true + } + + return _document.activeElement === focusTarget + }, +} + +var focusAudioWithoutControls = { + name: 'can-focus-audio-without-controls', + element: 'audio', + mutate: function mutate(element) { + try { + // invalid media file can trigger warning in console, data-uri to prevent HTTP request + element.setAttribute('src', gif) + } catch (e) { + // IE9 may throw "Error: Not implemented" + } + }, +} + +var invalidGif = + 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ' + +// NOTE: https://github.com/medialize/ally.js/issues/35 +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-usemap +var focusBrokenImageMap = { + element: 'div', + mutate: function mutate(element) { + element.innerHTML = + '' + + '' + + return element.querySelector('area') + }, +} + +// Children of focusable elements with display:flex are focusable in IE10-11 +var focusChildrenOfFocusableFlexbox = { + element: 'div', + mutate: function mutate(element) { + element.setAttribute('tabindex', '-1') + element.setAttribute( + 'style', + 'display: -webkit-flex; display: -ms-flexbox; display: flex;' + ) + element.innerHTML = 'hello' + return element.querySelector('span') + }, +} + +// fieldset[tabindex=0][disabled] should not be focusable, but Blink and WebKit disagree +// @specification https://www.w3.org/TR/html5/disabled-elements.html#concept-element-disabled +// @browser-issue Chromium https://crbug.com/453847 +// @browser-issue WebKit https://bugs.webkit.org/show_bug.cgi?id=141086 +var focusFieldsetDisabled = { + element: 'fieldset', + mutate: function mutate(element) { + element.setAttribute('tabindex', 0) + element.setAttribute('disabled', 'disabled') + }, +} + +var focusFieldset = { + element: 'fieldset', + mutate: function mutate(element) { + element.innerHTML = 'legend

content

' + }, +} + +// elements with display:flex are focusable in IE10-11 +var focusFlexboxContainer = { + element: 'span', + mutate: function mutate(element) { + element.setAttribute( + 'style', + 'display: -webkit-flex; display: -ms-flexbox; display: flex;' + ) + element.innerHTML = 'hello' + }, +} + +// form[tabindex=0][disabled] should be focusable as the +// specification doesn't know the disabled attribute on the form element +// @specification https://www.w3.org/TR/html5/forms.html#the-form-element +var focusFormDisabled = { + element: 'form', + mutate: function mutate(element) { + element.setAttribute('tabindex', 0) + element.setAttribute('disabled', 'disabled') + }, +} + +// NOTE: https://github.com/medialize/ally.js/issues/35 +// fixes https://github.com/medialize/ally.js/issues/20 +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-ismap +var focusImgIsmap = { + element: 'a', + mutate: function mutate(element) { + element.href = '#void' + element.innerHTML = '' + return element.querySelector('img') + }, +} + +// NOTE: https://github.com/medialize/ally.js/issues/35 +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-usemap +var focusImgUsemapTabindex = { + element: 'div', + mutate: function mutate(element) { + element.innerHTML = + '' + + '' + + return element.querySelector('img') + }, +} + +var focusInHiddenIframe = { + element: function element(wrapper, _document) { + var iframe = _document.createElement('iframe') + + // iframe must be part of the DOM before accessing the contentWindow is possible + wrapper.appendChild(iframe) + + // create the iframe's default document () + var iframeDocument = iframe.contentWindow.document + iframeDocument.open() + iframeDocument.close() + return iframe + }, + mutate: function mutate(iframe) { + iframe.style.visibility = 'hidden' + + var iframeDocument = iframe.contentWindow.document + var input = iframeDocument.createElement('input') + iframeDocument.body.appendChild(input) + return input + }, + validate: function validate(iframe) { + var iframeDocument = iframe.contentWindow.document + var focus = iframeDocument.querySelector('input') + return iframeDocument.activeElement === focus + }, +} + +var result = !platform.is.WEBKIT + +function focusInZeroDimensionObject() { + return result +} + +// Firefox allows *any* value and treats invalid values like tabindex="-1" +// @browser-issue Gecko https://bugzilla.mozilla.org/show_bug.cgi?id=1128054 +var focusInvalidTabindex = { + element: 'div', + mutate: function mutate(element) { + element.setAttribute('tabindex', 'invalid-value') + }, +} + +var focusLabelTabindex = { + element: 'label', + mutate: function mutate(element) { + element.setAttribute('tabindex', '-1') + }, + validate: function validate(element, focusTarget, _document) { + // force layout in Chrome 49, otherwise the element won't be focusable + /* eslint-disable no-unused-vars */ + var variableToPreventDeadCodeElimination = element.offsetHeight + /* eslint-enable no-unused-vars */ + element.focus() + return _document.activeElement === element + }, +} + +var svg = + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtb' + + 'G5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBpZD0ic3ZnIj48dGV4dCB4PSIxMCIgeT0iMjAiIGlkPSJ' + + 'zdmctbGluay10ZXh0Ij50ZXh0PC90ZXh0Pjwvc3ZnPg==' + +// Note: IE10 on BrowserStack does not like this test + +var focusObjectSvgHidden = { + element: 'object', + mutate: function mutate(element) { + element.setAttribute('type', 'image/svg+xml') + element.setAttribute('data', svg) + element.setAttribute('width', '200') + element.setAttribute('height', '50') + element.style.visibility = 'hidden' + }, +} + +// Note: IE10 on BrowserStack does not like this test + +var focusObjectSvg = { + name: 'can-focus-object-svg', + element: 'object', + mutate: function mutate(element) { + element.setAttribute('type', 'image/svg+xml') + element.setAttribute('data', svg) + element.setAttribute('width', '200') + element.setAttribute('height', '50') + }, + validate: function validate(element, focusTarget, _document) { + if (platform.is.GECKO) { + // Firefox seems to be handling the object creation asynchronously and thereby produces a false negative test result. + // Because we know Firefox is able to focus object elements referencing SVGs, we simply cheat by sniffing the user agent string + return true + } + + return _document.activeElement === element + }, +} + +// Every Environment except IE9 considers SWF objects focusable +var result$1 = !platform.is.IE9 + +function focusObjectSwf() { + return result$1 +} + +var focusRedirectImgUsemap = { + element: 'div', + mutate: function mutate(element) { + element.innerHTML = + '' + + '' + + // focus the , not the
+ return element.querySelector('img') + }, + validate: function validate(element, focusTarget, _document) { + var target = element.querySelector('area') + return _document.activeElement === target + }, +} + +// see https://jsbin.com/nenirisage/edit?html,js,console,output + +var focusRedirectLegend = { + element: 'fieldset', + mutate: function mutate(element) { + element.innerHTML = + 'legend' + // take care of focus in validate(); + return false + }, + validate: function validate(element, focusTarget, _document) { + var focusable = element.querySelector('input[tabindex="-1"]') + var tabbable = element.querySelector('input[tabindex="0"]') + + // Firefox requires this test to focus the
first, while this is not necessary in + // https://jsbin.com/nenirisage/edit?html,js,console,output + element.focus() + + element.querySelector('legend').focus() + return ( + (_document.activeElement === focusable && 'focusable') || + (_document.activeElement === tabbable && 'tabbable') || + '' + ) + }, +} + +// https://github.com/medialize/ally.js/issues/21 +var focusScrollBody = { + element: 'div', + mutate: function mutate(element) { + element.setAttribute('style', 'width: 100px; height: 50px; overflow: auto;') + element.innerHTML = + '
scrollable content
' + return element.querySelector('div') + }, +} + +// https://github.com/medialize/ally.js/issues/21 +var focusScrollContainerWithoutOverflow = { + element: 'div', + mutate: function mutate(element) { + element.setAttribute('style', 'width: 100px; height: 50px;') + element.innerHTML = + '
scrollable content
' + }, +} + +// https://github.com/medialize/ally.js/issues/21 +var focusScrollContainer = { + element: 'div', + mutate: function mutate(element) { + element.setAttribute('style', 'width: 100px; height: 50px; overflow: auto;') + element.innerHTML = + '
scrollable content
' + }, +} + +var focusSummary = { + element: 'details', + mutate: function mutate(element) { + element.innerHTML = 'foo

content

' + return element.firstElementChild + }, +} + +function makeFocusableForeignObject() { + var fragment = document.createElement('div') + fragment.innerHTML = + '\n \n ' + + return fragment.firstChild.firstChild +} + +function focusSvgForeignObjectHack(element) { + // Edge13, Edge14: foreignObject focus hack + // https://jsbin.com/kunehinugi/edit?html,js,output + // https://jsbin.com/fajagi/3/edit?html,js,output + var isSvgElement = + element.ownerSVGElement || element.nodeName.toLowerCase() === 'svg' + if (!isSvgElement) { + return false + } + + // inject and focus an element into the SVG element to receive focus + var foreignObject = makeFocusableForeignObject() + element.appendChild(foreignObject) + var input = foreignObject.querySelector('input') + input.focus() + + // upon disabling the activeElement, IE and Edge + // will not shift focus to like all the other + // browsers, but instead find the first focusable + // ancestor and shift focus to that + input.disabled = true + + // clean up + element.removeChild(foreignObject) + return true +} + +function generate(element) { + return ( + '' + + element + + '' + ) +} + +function focus(element) { + if (element.focus) { + return + } + + try { + HTMLElement.prototype.focus.call(element) + } catch (e) { + focusSvgForeignObjectHack(element) + } +} + +function validate(element, focusTarget, _document) { + focus(focusTarget) + return _document.activeElement === focusTarget +} + +var focusSvgFocusableAttribute = { + element: 'div', + mutate: function mutate(element) { + element.innerHTML = generate('a') + return element.querySelector('text') + }, + validate: validate, +} + +var focusSvgTabindexAttribute = { + element: 'div', + mutate: function mutate(element) { + element.innerHTML = generate('a') + return element.querySelector('text') + }, + validate: validate, +} + +var focusSvgNegativeTabindexAttribute = { + element: 'div', + mutate: function mutate(element) { + element.innerHTML = generate('a') + return element.querySelector('text') + }, + validate: validate, +} + +var focusSvgUseTabindex = { + element: 'div', + mutate: function mutate(element) { + element.innerHTML = generate( + [ + 'link', + '', + ].join('') + ) + + return element.querySelector('use') + }, + validate: validate, +} + +var focusSvgForeignobjectTabindex = { + element: 'div', + mutate: function mutate(element) { + element.innerHTML = generate( + '' + ) + // Safari 8's quersSelector() can't identify foreignObject, but getElementyByTagName() can + return ( + element.querySelector('foreignObject') || + element.getElementsByTagName('foreignObject')[0] + ) + }, + validate: validate, +} + +// Firefox seems to be handling the SVG-document-in-iframe creation asynchronously +// and thereby produces a false negative test result. Thus the test is pointless +// and we resort to UA sniffing once again. +// see http://jsbin.com/vunadohoko/1/edit?js,console,output + +var result$2 = Boolean( + platform.is.GECKO && + typeof SVGElement !== 'undefined' && + SVGElement.prototype.focus +) + +function focusSvgInIframe() { + return result$2 +} + +var focusSvg = { + element: 'div', + mutate: function mutate(element) { + element.innerHTML = generate('') + return element.firstChild + }, + validate: validate, +} + +// Firefox allows *any* value and treats invalid values like tabindex="-1" +// @browser-issue Gecko https://bugzilla.mozilla.org/show_bug.cgi?id=1128054 +var focusTabindexTrailingCharacters = { + element: 'div', + mutate: function mutate(element) { + element.setAttribute('tabindex', '3x') + }, +} + +var focusTable = { + element: 'table', + mutate: function mutate(element, wrapper, _document) { + // IE9 has a problem replacing TBODY contents with innerHTML. + // https://stackoverflow.com/a/8097055/515124 + // element.innerHTML = 'cell'; + var fragment = _document.createDocumentFragment() + fragment.innerHTML = 'cell' + element.appendChild(fragment) + }, +} + +var focusVideoWithoutControls = { + element: 'video', + mutate: function mutate(element) { + try { + // invalid media file can trigger warning in console, data-uri to prevent HTTP request + element.setAttribute('src', gif) + } catch (e) { + // IE9 may throw "Error: Not implemented" + } + }, +} + +// https://jsbin.com/vafaba/3/edit?html,js,console,output +var result$3 = platform.is.GECKO || platform.is.TRIDENT || platform.is.EDGE + +function tabsequenceAreaAtImgPosition() { + return result$3 +} + +var testCallbacks = { + cssShadowPiercingDeepCombinator: cssShadowPiercingDeepCombinator, + focusInZeroDimensionObject: focusInZeroDimensionObject, + focusObjectSwf: focusObjectSwf, + focusSvgInIframe: focusSvgInIframe, + tabsequenceAreaAtImgPosition: tabsequenceAreaAtImgPosition, +} + +var testDescriptions = { + focusAreaImgTabindex: focusAreaImgTabindex, + focusAreaTabindex: focusAreaTabindex, + focusAreaWithoutHref: focusAreaWithoutHref, + focusAudioWithoutControls: focusAudioWithoutControls, + focusBrokenImageMap: focusBrokenImageMap, + focusChildrenOfFocusableFlexbox: focusChildrenOfFocusableFlexbox, + focusFieldsetDisabled: focusFieldsetDisabled, + focusFieldset: focusFieldset, + focusFlexboxContainer: focusFlexboxContainer, + focusFormDisabled: focusFormDisabled, + focusImgIsmap: focusImgIsmap, + focusImgUsemapTabindex: focusImgUsemapTabindex, + focusInHiddenIframe: focusInHiddenIframe, + focusInvalidTabindex: focusInvalidTabindex, + focusLabelTabindex: focusLabelTabindex, + focusObjectSvg: focusObjectSvg, + focusObjectSvgHidden: focusObjectSvgHidden, + focusRedirectImgUsemap: focusRedirectImgUsemap, + focusRedirectLegend: focusRedirectLegend, + focusScrollBody: focusScrollBody, + focusScrollContainerWithoutOverflow: focusScrollContainerWithoutOverflow, + focusScrollContainer: focusScrollContainer, + focusSummary: focusSummary, + focusSvgFocusableAttribute: focusSvgFocusableAttribute, + focusSvgTabindexAttribute: focusSvgTabindexAttribute, + focusSvgNegativeTabindexAttribute: focusSvgNegativeTabindexAttribute, + focusSvgUseTabindex: focusSvgUseTabindex, + focusSvgForeignobjectTabindex: focusSvgForeignobjectTabindex, + focusSvg: focusSvg, + focusTabindexTrailingCharacters: focusTabindexTrailingCharacters, + focusTable: focusTable, + focusVideoWithoutControls: focusVideoWithoutControls, +} + +function executeTests() { + var results = detectFocus(testDescriptions) + Object.keys(testCallbacks).forEach(function (key) { + results[key] = testCallbacks[key]() + }) + + return results +} + +var supportsCache = null + +function _supports() { + if (supportsCache) { + return supportsCache + } + + supportsCache = cache$1.get() + if (!supportsCache.time) { + cache$1.set(executeTests()) + supportsCache = cache$1.get() + } + + return supportsCache +} + +var supports = void 0 + +// https://www.w3.org/TR/html5/infrastructure.html#rules-for-parsing-integers +// NOTE: all browsers agree to allow trailing spaces as well +var validIntegerPatternNoTrailing = /^\s*(-|\+)?[0-9]+\s*$/ +var validIntegerPatternWithTrailing = /^\s*(-|\+)?[0-9]+.*$/ + +function isValidTabindex(context) { + if (!supports) { + supports = _supports() + } + + var validIntegerPattern = supports.focusTabindexTrailingCharacters + ? validIntegerPatternWithTrailing + : validIntegerPatternNoTrailing + + var element = contextToElement({ + label: 'is/valid-tabindex', + resolveDocument: true, + context: context, + }) + + // Edge 14 has a capitalization problem on SVG elements, + // see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/9282058/ + var hasTabindex = element.hasAttribute('tabindex') + var hasTabIndex = element.hasAttribute('tabIndex') + + if (!hasTabindex && !hasTabIndex) { + return false + } + + // older Firefox and Internet Explorer don't support tabindex on SVG elements + var isSvgElement = + element.ownerSVGElement || element.nodeName.toLowerCase() === 'svg' + if (isSvgElement && !supports.focusSvgTabindexAttribute) { + return false + } + + // @browser-issue Gecko https://bugzilla.mozilla.org/show_bug.cgi?id=1128054 + if (supports.focusInvalidTabindex) { + return true + } + + // an element matches the tabindex selector even if its value is invalid + var tabindex = element.getAttribute(hasTabindex ? 'tabindex' : 'tabIndex') + // IE11 parses tabindex="" as the value "-32768" + // @browser-issue Trident https://connect.microsoft.com/IE/feedback/details/1072965 + if (tabindex === '-32768') { + return false + } + + return Boolean(tabindex && validIntegerPattern.test(tabindex)) +} + +function tabindexValue(element) { + if (!isValidTabindex(element)) { + return null + } + + // Edge 14 has a capitalization problem on SVG elements, + // see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/9282058/ + var hasTabindex = element.hasAttribute('tabindex') + var attributeName = hasTabindex ? 'tabindex' : 'tabIndex' + + // @browser-issue Gecko https://bugzilla.mozilla.org/show_bug.cgi?id=1128054 + var tabindex = parseInt(element.getAttribute(attributeName), 10) + return isNaN(tabindex) ? -1 : tabindex +} + +// this is a shared utility file for focus-relevant.js and tabbable.js +// separate testing of this file's functions is not necessary, +// as they're implicitly tested by way of the consumers + +function isUserModifyWritable(style) { + // https://www.w3.org/TR/1999/WD-css3-userint-19990916#user-modify + // https://github.com/medialize/ally.js/issues/17 + var userModify = style.webkitUserModify || '' + return Boolean(userModify && userModify.indexOf('write') !== -1) +} + +function hasCssOverflowScroll(style) { + return [ + style.getPropertyValue('overflow'), + style.getPropertyValue('overflow-x'), + style.getPropertyValue('overflow-y'), + ].some(function (overflow) { + return overflow === 'auto' || overflow === 'scroll' + }) +} + +function hasCssDisplayFlex(style) { + return style.display.indexOf('flex') > -1 +} + +function isScrollableContainer(element, nodeName, parentNodeName, parentStyle) { + if (nodeName !== 'div' && nodeName !== 'span') { + // Internet Explorer advances scrollable containers and bodies to focusable + // only if the scrollable container is
or - this does *not* + // happen for
,
, … + return false + } + + if ( + parentNodeName && + parentNodeName !== 'div' && + parentNodeName !== 'span' && + !hasCssOverflowScroll(parentStyle) + ) { + return false + } + + return ( + element.offsetHeight < element.scrollHeight || + element.offsetWidth < element.scrollWidth + ) +} + +var supports$1 = void 0 + +function isFocusRelevantRules() { + var _ref = + arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, + context = _ref.context, + _ref$except = _ref.except, + except = + _ref$except === undefined + ? { + flexbox: false, + scrollable: false, + shadow: false, + } + : _ref$except + + if (!supports$1) { + supports$1 = _supports() + } + + var element = contextToElement({ + label: 'is/focus-relevant', + resolveDocument: true, + context: context, + }) + + if (!except.shadow && element.shadowRoot) { + // a ShadowDOM host receives focus when the focus moves to its content + return true + } + + var nodeName = element.nodeName.toLowerCase() + + if (nodeName === 'input' && element.type === 'hidden') { + // input[type="hidden"] supports.cannot be focused + return false + } + + if ( + nodeName === 'input' || + nodeName === 'select' || + nodeName === 'button' || + nodeName === 'textarea' + ) { + return true + } + + if (nodeName === 'legend' && supports$1.focusRedirectLegend) { + // specifics filtered in is/focusable + return true + } + + if (nodeName === 'label') { + // specifics filtered in is/focusable + return true + } + + if (nodeName === 'area') { + // specifics filtered in is/focusable + return true + } + + if (nodeName === 'a' && element.hasAttribute('href')) { + return true + } + + if (nodeName === 'object' && element.hasAttribute('usemap')) { + // object[usemap] is not focusable in any browser + return false + } + + if (nodeName === 'object') { + var svgType = element.getAttribute('type') + if (!supports$1.focusObjectSvg && svgType === 'image/svg+xml') { + // object[type="image/svg+xml"] is not focusable in Internet Explorer + return false + } else if ( + !supports$1.focusObjectSwf && + svgType === 'application/x-shockwave-flash' + ) { + // object[type="application/x-shockwave-flash"] is not focusable in Internet Explorer 9 + return false + } + } + + if (nodeName === 'iframe' || nodeName === 'object') { + // browsing context containers + return true + } + + if (nodeName === 'embed' || nodeName === 'keygen') { + // embed is considered focus-relevant but not focusable + // see https://github.com/medialize/ally.js/issues/82 + return true + } + + if (element.hasAttribute('contenteditable')) { + // also see CSS property user-modify below + return true + } + + if ( + nodeName === 'audio' && + (supports$1.focusAudioWithoutControls || element.hasAttribute('controls')) + ) { + return true + } + + if ( + nodeName === 'video' && + (supports$1.focusVideoWithoutControls || element.hasAttribute('controls')) + ) { + return true + } + + if (supports$1.focusSummary && nodeName === 'summary') { + return true + } + + var validTabindex = isValidTabindex(element) + + if (nodeName === 'img' && element.hasAttribute('usemap')) { + // Gecko, Trident and Edge do not allow an image with an image map and tabindex to be focused, + // it appears the tabindex is overruled so focus is still forwarded to the + return ( + (validTabindex && supports$1.focusImgUsemapTabindex) || + supports$1.focusRedirectImgUsemap + ) + } + + if (supports$1.focusTable && (nodeName === 'table' || nodeName === 'td')) { + // IE10-11 supports.can focus and
+ return true + } + + if (supports$1.focusFieldset && nodeName === 'fieldset') { + // IE10-11 supports.can focus
+ return true + } + + var isSvgElement = nodeName === 'svg' + var isSvgContent = element.ownerSVGElement + var focusableAttribute = element.getAttribute('focusable') + var tabindex = tabindexValue(element) + + if ( + nodeName === 'use' && + tabindex !== null && + !supports$1.focusSvgUseTabindex + ) { + // cannot be made focusable by adding a tabindex attribute anywhere but Blink and WebKit + return false + } + + if (nodeName === 'foreignobject') { + // can only be made focusable in Blink and WebKit + return tabindex !== null && supports$1.focusSvgForeignobjectTabindex + } + + if (elementMatches(element, 'svg a') && element.hasAttribute('xlink:href')) { + return true + } + + if ( + (isSvgElement || isSvgContent) && + element.focus && + !supports$1.focusSvgNegativeTabindexAttribute && + tabindex < 0 + ) { + // Firefox 51 and 52 treat any natively tabbable SVG element with + // tabindex="-1" as tabbable and everything else as inert + // see https://bugzilla.mozilla.org/show_bug.cgi?id=1302340 + return false + } + + if (isSvgElement) { + return ( + validTabindex || + supports$1.focusSvg || + supports$1.focusSvgInIframe || + // Internet Explorer understands the focusable attribute introduced in SVG Tiny 1.2 + Boolean( + supports$1.focusSvgFocusableAttribute && + focusableAttribute && + focusableAttribute === 'true' + ) + ) + } + + if (isSvgContent) { + if (supports$1.focusSvgTabindexAttribute && validTabindex) { + return true + } + + if (supports$1.focusSvgFocusableAttribute) { + // Internet Explorer understands the focusable attribute introduced in SVG Tiny 1.2 + return focusableAttribute === 'true' + } + } + + // https://www.w3.org/TR/html5/editing.html#sequential-focus-navigation-and-the-tabindex-attribute + if (validTabindex) { + return true + } + + var style = window.getComputedStyle(element, null) + if (isUserModifyWritable(style)) { + return true + } + + if ( + supports$1.focusImgIsmap && + nodeName === 'img' && + element.hasAttribute('ismap') + ) { + // IE10-11 considers the in focusable + // https://github.com/medialize/ally.js/issues/20 + var hasLinkParent = getParents({ context: element }).some(function ( + parent + ) { + return ( + parent.nodeName.toLowerCase() === 'a' && parent.hasAttribute('href') + ) + }) + + if (hasLinkParent) { + return true + } + } + + // https://github.com/medialize/ally.js/issues/21 + if (!except.scrollable && supports$1.focusScrollContainer) { + if (supports$1.focusScrollContainerWithoutOverflow) { + // Internet Explorer does will consider the scrollable area focusable + // if the element is a
or a and it is in fact scrollable, + // regardless of the CSS overflow property + if (isScrollableContainer(element, nodeName)) { + return true + } + } else if (hasCssOverflowScroll(style)) { + // Firefox requires proper overflow setting, IE does not necessarily + // https://developer.mozilla.org/en-US/docs/Web/CSS/overflow + return true + } + } + + if ( + !except.flexbox && + supports$1.focusFlexboxContainer && + hasCssDisplayFlex(style) + ) { + // elements with display:flex are focusable in IE10-11 + return true + } + + var parent = element.parentElement + if (!except.scrollable && parent) { + var parentNodeName = parent.nodeName.toLowerCase() + var parentStyle = window.getComputedStyle(parent, null) + if ( + supports$1.focusScrollBody && + isScrollableContainer(parent, nodeName, parentNodeName, parentStyle) + ) { + // scrollable bodies are focusable Internet Explorer + // https://github.com/medialize/ally.js/issues/21 + return true + } + + // Children of focusable elements with display:flex are focusable in IE10-11 + if (supports$1.focusChildrenOfFocusableFlexbox) { + if (hasCssDisplayFlex(parentStyle)) { + return true + } + } + } + + // NOTE: elements marked as inert are not focusable, + // but that property is not exposed to the DOM + // https://www.w3.org/TR/html5/editing.html#inert + + return false +} + +// bind exceptions to an iterator callback +isFocusRelevantRules.except = function () { + var except = + arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {} + + var isFocusRelevant = function isFocusRelevant(context) { + return isFocusRelevantRules({ + context: context, + except: except, + }) + } + + isFocusRelevant.rules = isFocusRelevantRules + return isFocusRelevant +} + +// provide isFocusRelevant(context) as default iterator callback +var isFocusRelevant = isFocusRelevantRules.except({}) + +function findIndex(array, callback) { + // attempt to use native or polyfilled Array#findIndex first + if (array.findIndex) { + return array.findIndex(callback) + } + + var length = array.length + + // shortcut if the array is empty + if (length === 0) { + return -1 + } + + // otherwise loop over array + for (var i = 0; i < length; i++) { + if (callback(array[i], i, array)) { + return i + } + } + + return -1 +} + +function getContentDocument(node) { + try { + // works on and