From dbe1e626f877a9ef7bbffc2f360bae809874790d Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Thu, 31 Dec 2020 11:08:12 -0500 Subject: [PATCH 01/15] fix(experimental scroll): use `sessionStorage` instead of `history` (#20633) This pull request adjusts our experimental scroll restoration behavior to use `sessionStorage` as opposed to `History#replaceState` to track scroll position. In addition, **it eliminates a scroll event listener** and only captures when a `pushState` event happens (thereby leaving state that needs snapshotted). These merely adjusts implementation detail, and is covered by existing tests: ``` test/integration/scroll-back-restoration/ ``` --- Fixes #16690 Fixes #17073 Fixes #20486 --- packages/next/client/index.tsx | 4 +- .../next/next-server/lib/router/router.ts | 105 +++++++++++------- .../build-output/test/index.test.js | 6 +- 3 files changed, 71 insertions(+), 44 deletions(-) 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/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index 9b141348f8b9..3431db68ec8f 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -49,7 +49,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 +358,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 +370,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') @@ -445,6 +455,8 @@ export default class Router implements BaseRouter { defaultLocale?: string domainLocales?: DomainLocales + private _idx: number = 0 + static events: MittEmitter = mitt() constructor( @@ -555,27 +567,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 +598,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 +641,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 +667,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,11 +695,12 @@ 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 @@ -804,7 +833,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 } @@ -1024,7 +1053,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 +1064,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 +1106,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 +1274,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 +1521,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/test/integration/build-output/test/index.test.js b/test/integration/build-output/test/index.test.js index 2b7498188d96..91871b8d0306 100644 --- a/test/integration/build-output/test/index.test.js +++ b/test/integration/build-output/test/index.test.js @@ -94,8 +94,8 @@ describe('Build Output', () => { expect(parseFloat(indexSize) - 266).toBeLessThanOrEqual(0) expect(indexSize.endsWith('B')).toBe(true) - // should be no bigger than 62.1 kb - expect(parseFloat(indexFirstLoad) - 62.1).toBeLessThanOrEqual(0) + // should be no bigger than 62.2 kb + expect(parseFloat(indexFirstLoad)).toBeCloseTo(62.2, 1) expect(indexFirstLoad.endsWith('kB')).toBe(true) expect(parseFloat(err404Size) - 3.7).toBeLessThanOrEqual(0) @@ -104,7 +104,7 @@ describe('Build Output', () => { expect(parseFloat(err404FirstLoad)).toBeCloseTo(65.3, 1) expect(err404FirstLoad.endsWith('kB')).toBe(true) - expect(parseFloat(sharedByAll) - 61.8).toBeLessThanOrEqual(0) + expect(parseFloat(sharedByAll)).toBeCloseTo(61.9, 1) expect(sharedByAll.endsWith('kB')).toBe(true) if (_appSize.endsWith('kB')) { From bd4eb9ea410f6e68f85c640b637ba237a6abc80b Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 31 Dec 2020 17:33:24 +0100 Subject: [PATCH 02/15] fix mongoose not latest next.js version (#20644) --- examples/with-mongodb-mongoose/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From 3a9d18b549d064583bd715a3815691d7b33ceaa5 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 31 Dec 2020 10:54:32 -0600 Subject: [PATCH 03/15] Add isReady field on router (#20628) Adds an `isReady` field on `next/router` specifying 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. Closes: https://github.com/vercel/next.js/issues/8259 Closes: https://github.com/vercel/next.js/pull/9370 --- docs/api-reference/next/router.md | 1 + packages/next/client/router.ts | 1 + .../next/next-server/lib/router/router.ts | 24 ++++- packages/next/next-server/server/render.tsx | 6 ++ .../build-output/test/index.test.js | 4 +- .../router-is-ready/pages/auto-export.js | 18 ++++ test/integration/router-is-ready/pages/gip.js | 26 ++++++ test/integration/router-is-ready/pages/gsp.js | 28 ++++++ .../integration/router-is-ready/pages/gssp.js | 28 ++++++ .../router-is-ready/pages/invalid.js | 15 ++++ .../router-is-ready/test/index.test.js | 88 +++++++++++++++++++ .../integration/size-limit/test/index.test.js | 2 +- 12 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 test/integration/router-is-ready/pages/auto-export.js create mode 100644 test/integration/router-is-ready/pages/gip.js create mode 100644 test/integration/router-is-ready/pages/gsp.js create mode 100644 test/integration/router-is-ready/pages/gssp.js create mode 100644 test/integration/router-is-ready/pages/invalid.js create mode 100644 test/integration/router-is-ready/test/index.test.js 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/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 3431db68ec8f..627db3e30418 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 } @@ -454,6 +462,7 @@ export default class Router implements BaseRouter { locales?: string[] defaultLocale?: string domainLocales?: DomainLocales + isReady: boolean private _idx: number = 0 @@ -527,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 @@ -539,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 @@ -707,6 +721,12 @@ export default class Router implements BaseRouter { 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. 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/test/integration/build-output/test/index.test.js b/test/integration/build-output/test/index.test.js index 91871b8d0306..9a5cf136836a 100644 --- a/test/integration/build-output/test/index.test.js +++ b/test/integration/build-output/test/index.test.js @@ -101,10 +101,10 @@ describe('Build Output', () => { expect(parseFloat(err404Size) - 3.7).toBeLessThanOrEqual(0) expect(err404Size.endsWith('kB')).toBe(true) - expect(parseFloat(err404FirstLoad)).toBeCloseTo(65.3, 1) + expect(parseFloat(err404FirstLoad)).toBeCloseTo(65.4, 1) expect(err404FirstLoad.endsWith('kB')).toBe(true) - expect(parseFloat(sharedByAll)).toBeCloseTo(61.9, 1) + expect(parseFloat(sharedByAll)).toBeCloseTo(62, 1) expect(sharedByAll.endsWith('kB')).toBe(true) if (_appSize.endsWith('kB')) { diff --git a/test/integration/router-is-ready/pages/auto-export.js b/test/integration/router-is-ready/pages/auto-export.js new file mode 100644 index 000000000000..f325029ae588 --- /dev/null +++ b/test/integration/router-is-ready/pages/auto-export.js @@ -0,0 +1,18 @@ +import { useRouter } from 'next/router' + +export default function Page(props) { + const router = useRouter() + + if (typeof window !== 'undefined') { + if (!window.isReadyValues) { + window.isReadyValues = [] + } + window.isReadyValues.push(router.isReady) + } + + return ( + <> +

auto-export page

+ + ) +} diff --git a/test/integration/router-is-ready/pages/gip.js b/test/integration/router-is-ready/pages/gip.js new file mode 100644 index 000000000000..861fb516946b --- /dev/null +++ b/test/integration/router-is-ready/pages/gip.js @@ -0,0 +1,26 @@ +import { useRouter } from 'next/router' + +export default function Page(props) { + const router = useRouter() + + if (typeof window !== 'undefined') { + if (!window.isReadyValues) { + window.isReadyValues = [] + } + window.isReadyValues.push(router.isReady) + } + + return ( + <> +

gssp page

+

{JSON.stringify(props)}

+ + ) +} + +Page.getInitialProps = () => { + return { + hello: 'world', + random: Math.random(), + } +} diff --git a/test/integration/router-is-ready/pages/gsp.js b/test/integration/router-is-ready/pages/gsp.js new file mode 100644 index 000000000000..b2bf1b290ab0 --- /dev/null +++ b/test/integration/router-is-ready/pages/gsp.js @@ -0,0 +1,28 @@ +import { useRouter } from 'next/router' + +export default function Page(props) { + const router = useRouter() + + if (typeof window !== 'undefined') { + if (!window.isReadyValues) { + window.isReadyValues = [] + } + window.isReadyValues.push(router.isReady) + } + + return ( + <> +

gsp page

+

{JSON.stringify(props)}

+ + ) +} + +export const getStaticProps = () => { + return { + props: { + hello: 'world', + random: Math.random(), + }, + } +} diff --git a/test/integration/router-is-ready/pages/gssp.js b/test/integration/router-is-ready/pages/gssp.js new file mode 100644 index 000000000000..8a103d094a2d --- /dev/null +++ b/test/integration/router-is-ready/pages/gssp.js @@ -0,0 +1,28 @@ +import { useRouter } from 'next/router' + +export default function Page(props) { + const router = useRouter() + + if (typeof window !== 'undefined') { + if (!window.isReadyValues) { + window.isReadyValues = [] + } + window.isReadyValues.push(router.isReady) + } + + return ( + <> +

gssp page

+

{JSON.stringify(props)}

+ + ) +} + +export const getServerSideProps = () => { + return { + props: { + hello: 'world', + random: Math.random(), + }, + } +} diff --git a/test/integration/router-is-ready/pages/invalid.js b/test/integration/router-is-ready/pages/invalid.js new file mode 100644 index 000000000000..d5c2d696cdd2 --- /dev/null +++ b/test/integration/router-is-ready/pages/invalid.js @@ -0,0 +1,15 @@ +import { useRouter } from 'next/router' + +export default function Page(props) { + // eslint-disable-next-line + const router = useRouter() + + // console.log(router.isReady) + + return ( + <> +

invalid page

+

{JSON.stringify(props)}

+ + ) +} diff --git a/test/integration/router-is-ready/test/index.test.js b/test/integration/router-is-ready/test/index.test.js new file mode 100644 index 000000000000..b2a9509e52a0 --- /dev/null +++ b/test/integration/router-is-ready/test/index.test.js @@ -0,0 +1,88 @@ +/* eslint-env jest */ + +import { join } from 'path' +import webdriver from 'next-webdriver' +import { + findPort, + launchApp, + killApp, + nextStart, + nextBuild, + File, +} from 'next-test-utils' + +jest.setTimeout(1000 * 60 * 1) + +let app +let appPort +const appDir = join(__dirname, '../') +const invalidPage = new File(join(appDir, 'pages/invalid.js')) + +function runTests(isDev) { + it('isReady should be true immediately for getInitialProps page', async () => { + const browser = await webdriver(appPort, '/gip') + expect(await browser.eval('window.isReadyValues')).toEqual([true]) + }) + + it('isReady should be true immediately for getInitialProps page with query', async () => { + const browser = await webdriver(appPort, '/gip?hello=world') + expect(await browser.eval('window.isReadyValues')).toEqual([true]) + }) + + it('isReady should be true immediately for getServerSideProps page', async () => { + const browser = await webdriver(appPort, '/gssp') + expect(await browser.eval('window.isReadyValues')).toEqual([true]) + }) + + it('isReady should be true immediately for getServerSideProps page with query', async () => { + const browser = await webdriver(appPort, '/gssp?hello=world') + expect(await browser.eval('window.isReadyValues')).toEqual([true]) + }) + + it('isReady should be true immediately for auto-export page without query', async () => { + const browser = await webdriver(appPort, '/auto-export') + expect(await browser.eval('window.isReadyValues')).toEqual([true]) + }) + + it('isReady should be true after query update for auto-export page with query', async () => { + const browser = await webdriver(appPort, '/auto-export?hello=world') + expect(await browser.eval('window.isReadyValues')).toEqual([false, true]) + }) + + it('isReady should be true after query update for getStaticProps page with query', async () => { + const browser = await webdriver(appPort, '/gsp?hello=world') + expect(await browser.eval('window.isReadyValues')).toEqual([false, true]) + }) + + it('isReady should be true immediately for getStaticProps page without query', async () => { + const browser = await webdriver(appPort, '/gsp') + expect(await browser.eval('window.isReadyValues')).toEqual([true]) + }) +} + +describe('router.isReady', () => { + describe('dev mode', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + afterAll(async () => { + await killApp(app) + invalidPage.restore() + }) + + runTests(true) + }) + + describe('production mode', () => { + beforeAll(async () => { + await nextBuild(appDir) + + appPort = await findPort() + app = await nextStart(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests() + }) +}) diff --git a/test/integration/size-limit/test/index.test.js b/test/integration/size-limit/test/index.test.js index 4a761552ca50..a2bc467eec59 100644 --- a/test/integration/size-limit/test/index.test.js +++ b/test/integration/size-limit/test/index.test.js @@ -81,6 +81,6 @@ describe('Production response size', () => { const delta = responseSizesBytes / 1024 // Expected difference: < 0.5 - expect(delta).toBeCloseTo(281.5, 0) + expect(delta).toBeCloseTo(282, 0) }) }) From 44ee7de664b1e67be5d60ee497d54eb5d602688d Mon Sep 17 00:00:00 2001 From: enoch ndika <51413750+enochndika@users.noreply.github.com> Date: Thu, 31 Dec 2020 19:46:10 +0100 Subject: [PATCH 04/15] example with-mdbreact (#19879) This example illustrates how to integrate mdbreact (material design bootstrap for react) with next.js --- examples/with-mdbreact/.gitignore | 35 +++++++ examples/with-mdbreact/README.md | 21 ++++ examples/with-mdbreact/package.json | 16 +++ examples/with-mdbreact/pages/_app.js | 10 ++ examples/with-mdbreact/pages/index.js | 118 ++++++++++++++++++++++ examples/with-mdbreact/public/favicon.ico | Bin 0 -> 15086 bytes examples/with-mdbreact/public/vercel.svg | 4 + examples/with-mdbreact/styles/globals.css | 16 +++ 8 files changed, 220 insertions(+) create mode 100644 examples/with-mdbreact/.gitignore create mode 100644 examples/with-mdbreact/README.md create mode 100644 examples/with-mdbreact/package.json create mode 100644 examples/with-mdbreact/pages/_app.js create mode 100644 examples/with-mdbreact/pages/index.js create mode 100644 examples/with-mdbreact/public/favicon.ico create mode 100644 examples/with-mdbreact/public/vercel.svg create mode 100644 examples/with-mdbreact/styles/globals.css diff --git a/examples/with-mdbreact/.gitignore b/examples/with-mdbreact/.gitignore new file mode 100644 index 000000000000..87af23af33e8 --- /dev/null +++ b/examples/with-mdbreact/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +/.idea +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel diff --git a/examples/with-mdbreact/README.md b/examples/with-mdbreact/README.md new file mode 100644 index 000000000000..ac623cc3d214 --- /dev/null +++ b/examples/with-mdbreact/README.md @@ -0,0 +1,21 @@ +# mdbreact Example + +This example shows how to use [MDBReact](https://mdbootstrap.com/docs/react) with Next.js. + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/git?s=https://github.com/vercel/next.js/tree/canary/examples/with-mdbreact) + +## How to use + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: + +```bash +npx create-next-app --example with-mdbreact with-mdbreact-app +# or +yarn create next-app --example with-mdbreact with-mdbreact-app +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). diff --git a/examples/with-mdbreact/package.json b/examples/with-mdbreact/package.json new file mode 100644 index 000000000000..e76af95a4d3a --- /dev/null +++ b/examples/with-mdbreact/package.json @@ -0,0 +1,16 @@ +{ + "name": "with-mdbreact", + "version": "0.1.0", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "mdbreact": "^5.0.0", + "next": "latest", + "react": "17.0.1", + "react-dom": "17.0.1" + }, + "license": "MIT" +} diff --git a/examples/with-mdbreact/pages/_app.js b/examples/with-mdbreact/pages/_app.js new file mode 100644 index 000000000000..d19b16ba08f6 --- /dev/null +++ b/examples/with-mdbreact/pages/_app.js @@ -0,0 +1,10 @@ +import '@fortawesome/fontawesome-free/css/all.min.css' +import 'bootstrap-css-only/css/bootstrap.min.css' +import 'mdbreact/dist/css/mdb.css' +import '../styles/globals.css' + +function MyApp({ Component, pageProps }) { + return +} + +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 0000000000000000000000000000000000000000..4965832f2c9b0605eaa189b7c7fb11124d24e48a GIT binary patch literal 15086 zcmeHOOH5Q(7(R0cc?bh2AT>N@1PWL!LLfZKyG5c!MTHoP7_p!sBz0k$?pjS;^lmgJ zU6^i~bWuZYHL)9$wuvEKm~qo~(5=Lvx5&Hv;?X#m}i|`yaGY4gX+&b>tew;gcnRQA1kp zBbm04SRuuE{Hn+&1wk%&g;?wja_Is#1gKoFlI7f`Gt}X*-nsMO30b_J@)EFNhzd1QM zdH&qFb9PVqQOx@clvc#KAu}^GrN`q5oP(8>m4UOcp`k&xwzkTio*p?kI4BPtIwX%B zJN69cGsm=x90<;Wmh-bs>43F}ro$}Of@8)4KHndLiR$nW?*{Rl72JPUqRr3ta6e#A z%DTEbi9N}+xPtd1juj8;(CJt3r9NOgb>KTuK|z7!JB_KsFW3(pBN4oh&M&}Nb$Ee2 z$-arA6a)CdsPj`M#1DS>fqj#KF%0q?w50GN4YbmMZIoF{e1yTR=4ablqXHBB2!`wM z1M1ke9+<);|AI;f=2^F1;G6Wfpql?1d5D4rMr?#f(=hkoH)U`6Gb)#xDLjoKjp)1;Js@2Iy5yk zMXUqj+gyk1i0yLjWS|3sM2-1ECc;MAz<4t0P53%7se$$+5Ex`L5TQO_MMXXi04UDIU+3*7Ez&X|mj9cFYBXqM{M;mw_ zpw>azP*qjMyNSD4hh)XZt$gqf8f?eRSFX8VQ4Y+H3jAtvyTrXr`qHAD6`m;aYmH2zOhJC~_*AuT} zvUxC38|JYN94i(05R)dVKgUQF$}#cxV7xZ4FULqFCNX*Forhgp*yr6;DsIk=ub0Hv zpk2L{9Q&|uI^b<6@i(Y+iSxeO_n**4nRLc`P!3ld5jL=nZRw6;DEJ*1z6Pvg+eW|$lnnjO zjd|8>6l{i~UxI244CGn2kK@cJ|#ecwgSyt&HKA2)z zrOO{op^o*- + + \ 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; +} From e4a744653da0567edca794553536f3262b128b6d Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Thu, 31 Dec 2020 14:04:46 -0500 Subject: [PATCH 05/15] fix(overlay): skip disable & upgrade platform (#20647) This bundles ally.js into Next.js itself to upgrade a dependency they have pinned. I tried every other major focus trap solution, even those used by some modal libraries, and they all failed. `ally.js` is the only library that can do it correctly, so we're going to stick with it. I also removed the `maintain/disabled` as we have a backdrop that would effectively result in the same. This reduces CPU strain. --- Fixes #19893 Fixes #14369 Closes #14372 --- packages/react-dev-overlay/package.json | 3 +- .../internal/components/Overlay/Overlay.tsx | 6 +- .../components/Overlay/maintain--tab-focus.ts | 3562 +++++++++++++++++ yarn.lock | 17 +- 4 files changed, 3571 insertions(+), 17 deletions(-) create mode 100644 packages/react-dev-overlay/src/internal/components/Overlay/maintain--tab-focus.ts diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index fce3b3da0287..b4d1ff77884a 100644 --- a/packages/react-dev-overlay/package.json +++ b/packages/react-dev-overlay/package.json @@ -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/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