[
  {
    "path": ".browserslistrc",
    "content": "Chrome >= 49\nSafari >= 8\nios_saf >= 8\nie >= 11\nEdge >= 12\nFirefox >= 45\nSamsung >= 2\n"
  },
  {
    "path": ".editorconfig",
    "content": "# This file is for unifying the coding style for different editors and IDEs\n# editorconfig.org\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  \"parser\": \"@babel/eslint-parser\",\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:prettier/recommended\"\n  ],\n  \"env\": {\n    \"browser\": true,\n    \"es6\": true,\n    \"jest\": true\n  },\n  \"globals\": {\n    \"Promise\": true,\n    \"require\": true,\n    \"module\": true\n  },\n  \"rules\": {\n    \"no-console\": 0,\n    \"indent\": \"off\"\n  }\n}\n"
  },
  {
    "path": ".github/workflows/node.js.yml",
    "content": "# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node\n# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions\n\nname: Node.js CI\n\non:\n  push:\n    branches: [ develop ]\n  pull_request:\n    branches: [ develop ]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v3\n    - uses: actions/setup-node@v3\n      with:\n          node-version-file: package.json\n    - run: npm run ci\n    - run: npm run test\n    - run: npm run build\n"
  },
  {
    "path": ".gitignore",
    "content": "# IntelliJ folders\n.idea_modules/\n.idea/\n\n# Node modules\nnode_modules/\n\n# Jest code coverage\ncoverage/\n\n# Built code\ndist/\n\n# macOS\n.DS_Store\n"
  },
  {
    "path": ".nvmrc",
    "content": "20.0.0\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"trailingComma\": \"es5\",\n  \"useTabs\": false,\n  \"tabWidth\": 2,\n  \"semi\": false,\n  \"singleQuote\": true,\n  \"printWidth\": 120\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n\t\"editor.formatOnSave\": true,\n\t\"editor.codeActionsOnSave\": {\n\t\t\"source.organizeImports\": true\n\t},\n\t\"[typescript]\": {\n\t\t\"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n\t},\n\t\"[javascript]\": {\n\t\t\"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n\t}\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Q42\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">Floating Focus</h1>\n\n<p align=\"center\">\n\t<a href=\"https://github.com/Q42/floating-focus-a11y\"><img src=\"https://github.com/q42/floating-focus-a11y/actions/workflows/node.js.yml/badge.svg\" alt=\"Build status\"></a>\n\t<a href=\"https://www.npmjs.com/package/@q42/floating-focus-a11y\"><img src=\"https://img.shields.io/npm/v/@q42/floating-focus-a11y.svg?sanitize=true\" alt=\"Package version\"></a>\n\t<a href=\"https://www.npmjs.com/package/@q42/floating-focus-a11y\"><img src=\"https://img.shields.io/npm/l/@q42/floating-focus-a11y.svg?sanitize=true\" alt=\"Package license\"></a>\n</p>\n\n<p align=\"center\">\n\tA clear, beautiful and easy to implement focus-state solution that improves accessibility and usability.\n</p>\n\t\n<p align=\"center\">\n<a href=\"https://engineering.q42.nl/floating-focus/\">https://engineering.q42.nl/floating-focus/</a>\n</p>\n\n![Drag Racing](https://thumbs.gfycat.com/GrimLoneKakapo-size_restricted.gif)\n\n---\n\n## Installation\nWith [npm](https://www.npmjs.com/) installed, run\n```bash\n$ npm install @q42/floating-focus-a11y --save\n```\n\n## Usage\nImport the package and instantiate the class on page load:\n```javascript\nimport FloatingFocus from '@q42/floating-focus-a11y';\nnew FloatingFocus(containerElement); // Element is an optional parameter which defaults to `document.body`\n```\n\nDefine a default outline and outline-offset. Either of these values can be overruled per component:\n```css\n/* Hide all default focus states if a mouse is used, this is completely optional ofcourse */\n*:focus {\n  outline: none;\n}\n\n/* Default outline value, which will be applied to all elements receiving focus, this is a required step. */\n/* The .focus class is used by the focus target, more below. */\n.floating-focus-enabled :focus, .floating-focus-enabled .focus {\n  outline: dodgerblue solid 2px;\n  outline-offset: 8px;\n}\n\n/* Give all buttons a green focus state instead of dodgerblue, this is optional in case it's needed. */\n.floating-focus-enabled [type=\"button\"]:focus {\n  outline-color: green;\n  outline-offset: 4px;\n}\n```\n\n### Focus target\n\nSometimes the actual element that receives focus is hidden from view, as is common with a custom input field. In this case it's possible to define a `focus-target` attribute on the focusable element.\n\n```html\n<input type=\"file\" class=\"hidden\" id=\"file-upload-123\" focus-target=\"file-upload-123-label\"/>\n<label id=\"file-upload-123-label\" for=\"file-upload-123\">Please upload a file</label>\n```\n\nThis will append the `focus` class to the target element and make the visual focus box appear around the target element, instead of the element that actually has the native focus.\n\n### Separate stylesheet\n\nFor convenience, the styles are included in the script by default. There is also an option to include the stylesheet separately. This is particularly useful with strict `style-src 'self'` CORS headers.\n\nImport unstyled dist file:\n```javascript\nimport FloatingFocus from '@q42/floating-focus-a11y/dist/unstyled';\n```\n\nThe stylesheet can then be separately imported with your favorite CSS preprocessor:\n```css\n@import '@q42/floating-focus-a11y/dist/unstyled';\n```\n\n### Extra cautions\n\n- Watch out with CSS transitions: if an element that will be focused has a `transition` for `outline-color` / `outline-width` / `outline-style` (including `all` !), the floating focus will not display correctly on that element.\n\n## Develop\n```bash\n$ npm run build\n$ npm run watch\n$ npm run test\n```\n\n## Deploy\n```bash\n# bump version\n$ npm version [major | minor | patch | premajor | preminor | prepatch | prerelease]\n\n# publish\n$ npm publish\n```\n\n## License\n[MIT](https://opensource.org/licenses/MIT)\n"
  },
  {
    "path": "__mocks__/generalMocks.js",
    "content": "// This mocks general things like browser APIs or scripts we include in our html.\n// Runs once after jest setup.\n// Modules can be mocked by using jest.mock('module') in your test or, if you\n// (always) need specific behaviour, by putting a mock in this folder.\n\nwindow.addEventListener = document.addEventListener = jest.fn((type, listener, options) => {\n});\n\nwindow.removeEventListener = document.removeEventListener = jest.fn((type, listener, options) => {\n});\n"
  },
  {
    "path": "babel.config.js",
    "content": "module.exports = (api) => {\n  const isTest = api.env('test')\n  // Cache the returned result\n  api.cache(true)\n\n  return {\n    presets: [\n      [\n        '@babel/preset-env',\n        {\n          modules: isTest ? 'commonjs' : false,\n          loose: true,\n          corejs: 3,\n          useBuiltIns: 'entry',\n          exclude: ['es.regexp.to-string'],\n        },\n      ],\n    ],\n  }\n}\n"
  },
  {
    "path": "example/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n\t<title>Document</title>\n\t<script src=\"../dist/styled/index.js\"></script>\n\t<script>\n\t\twindow.addEventListener('load', function() {\n\t\t\tnew window['floating-focus'].default();\n\t\t});\n\t</script>\n\t<style>\n\t\t*, *::after, *::before {\n\t\t\tbox-sizing: border-box;\n\t\t}\n\n\t\t*:focus {\n\t\t\toutline: none;\n\t\t}\n\n\t\t.floating-focus-enabled :focus, .floating-focus-enabled .focus {\n\t\t\toutline: dodgerblue solid 3px;\n\t\t\toutline-offset: 4px;\n\t\t}\n\n\t\thtml {\n\t\t\theight: 100%;\n\t\t}\n\n\t\tbody {\n\t\t\tmin-height: 150%;\n\t\t\tdisplay: flex;\n\t\t\tjustify-content: center;\n\t\t\talign-items: center;\n\t\t\tflex-direction: column;\n\t\t\tmargin: 20px;\n\t\t}\n\n\t\tbody > fieldset,\n\t\tbody > input,\n\t\tbody > label {\n\t\t\tmargin: 10px;\n\t\t\twidth: 200px;\n\t\t}\n\n\t\tfieldset {\n\t\t\tborder: 0;\n\t\t\tpadding: 0;\n\t\t\twhite-space: nowrap;\n\t\t\toverflow-x: scroll;\n\t\t\toverflow-y: hidden;\n\t\t\tpadding-bottom: 20px;\n\t\t}\n\n\t\tfieldset > button {\n\t\t\twidth: 100%;\n\t\t}\n\n\t\tbutton, input, label, p {\n\t\t\tpadding: 5px 10px;\n\t\t\tfont: 15px sans-serif;\n\t\t}\n\n\t\tbutton, label {\n\t\t\tbackground: darkgoldenrod;\n\t\t\tcolor: white;\n\t\t\tborder-radius: 3px;\n\t\t\tborder: 0;\n\t\t}\n\n\t\tlabel {\n\t\t\tposition: relative;\n\t\t\toverflow: hidden;\n\t\t}\n\n\t\t.file-upload-label.focus {\n\t\t\toutline-offset: 0;\n\t\t\toutline-color: orange;\n\t\t\toutline-width: 2px;\n\t\t\toutline-style: dashed;\n\t\t}\n\n\t\tinput[type=\"file\"] {\n\t\t\tposition: absolute;\n\t\t\tbottom: 200%;\n\t\t\tright: 200%;\n\t\t}\n\n\t\t.input-warning-wrapper {\n\t\t\tbackground: orangered;\n\t\t\twidth: 200px;\n\t\t\tcolor: white;\n\t\t\tmax-height: 0;\n\t\t\tbox-sizing: border-box;\n\t\t\ttransition: max-height .25s ease;\n\t\t}\n\t\t.input-warning-wrapper > p {\n\t\t\tmargin: 0;\n\t\t}\n\t\t.input-warning-wrapper.show {\n\t\t\tmax-height: 50px;\n\t\t}\n\t</style>\n</head>\n<body>\n\t<fieldset>\n\t\t<button>Test 1</button>\n\t\t<button>Test 2</button>\n\t\t<button>Test 3</button>\n\t</fieldset>\n\n\t<input type=\"text\">\n\t<input type=\"text\">\n\t<input type=\"text\">\n\n\t<label id=\"file-upload-label\" class=\"file-upload-label\">\n\t\t<input type=\"file\" focus-target=\"file-upload-label\"/>\n\t\tPlease upload a file\n\t</label>\n\n\t<div class=\"input-warning-wrapper\" id=\"input-warning-wrapper\"><p>Please input 3 or more characters!</p></div>\n\t<input id=\"warning-field\" type=\"text\">\n\n\t<script>\n\t\tconst warningField = document.querySelector('#warning-field');\n\t\tconst inputWarningWrapper = document.querySelector('#input-warning-wrapper');\n\t\twarningField.addEventListener('input', function () {\n\t\t\tif (warningField.value && warningField.value.length < 3) {\n\t\t\t\tinputWarningWrapper.classList.add('show');\n\t\t\t} else {\n\t\t\t\tinputWarningWrapper.classList.remove('show');\n\t\t\t}\n\t\t})\n\t</script>\n</body>\n</html>\n"
  },
  {
    "path": "index.d.ts",
    "content": "declare class FloatingFocus {\n\tconstructor(container?: Element)\n}\n\nexport default FloatingFocus\n"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n  testEnvironment: 'jsdom',\n  clearMocks: true,\n  moduleFileExtensions: ['js', 'jsx', 'json'],\n  setupFilesAfterEnv: ['<rootDir>/__mocks__/generalMocks'],\n  transform: {\n    // Stub all styling & assets\n    '.+\\\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',\n    '^.+\\\\.jsx?$': 'babel-jest',\n  },\n  transformIgnorePatterns: ['/node_modules/'],\n  testEnvironmentOptions: {\n    url: 'http://localhost/',\n  },\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"@q42/floating-focus-a11y\",\n\t\"version\": \"1.4.0\",\n\t\"description\": \"An a11y focus solution that is clear, beautiful and easy to implement.\",\n\t\"keywords\": [\n\t\t\"a11y\",\n\t\t\"focus\",\n\t\t\"floating\"\n\t],\n\t\"author\": {\n\t\t\"name\": \"Ricardo Snoek\",\n\t\t\"email\": \"ricardo@q42.nl\",\n\t\t\"url\": \"https://q42.nl\"\n\t},\n\t\"homepage\": \"https://github.com/Q42/FloatingFocus\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"https://github.com/Q42/FloatingFocus.git\"\n\t},\n\t\"bugs\": {\n\t\t\"url\": \"https://github.com/Q42/FloatingFocus/issues\"\n\t},\n\t\"main\": \"dist/styled/index.js\",\n\t\"types\": \"index.d.ts\",\n\t\"scripts\": {\n\t\t\"ci\": \"npm ci --omit=optional\",\n\t\t\"build\": \"webpack --config webpack.prod.js\",\n\t\t\"watch\": \"webpack --config webpack.dev.js\",\n\t\t\"lint:check\": \"prettier --check ./src/**/*.{scss,js}\",\n\t\t\"lint:fix\": \"prettier --write ./src/**/*.{scss,js}\",\n\t\t\"test\": \"jest --verbose --coverage\",\n\t\t\"prepublishOnly\": \"npm run lint:check && npm run test && npm run build\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@babel/core\": \"^7.21.4\",\n\t\t\"@babel/eslint-parser\": \"^7.21.3\",\n\t\t\"@babel/preset-env\": \"^7.21.4\",\n\t\t\"@jest/globals\": \"^29.5.0\",\n\t\t\"autoprefixer\": \"^10.4.14\",\n\t\t\"babel-jest\": \"^29.5.0\",\n\t\t\"babel-loader\": \"^9.1.2\",\n\t\t\"clean-webpack-plugin\": \"^4.0.0\",\n\t\t\"copy-webpack-plugin\": \"^11.0.0\",\n\t\t\"core-js\": \"^3.30.1\",\n\t\t\"css-loader\": \"^6.7.3\",\n\t\t\"eslint\": \"^8.38.0\",\n\t\t\"eslint-config-prettier\": \"^8.8.0\",\n\t\t\"eslint-plugin-prettier\": \"^4.2.1\",\n\t\t\"eslint-webpack-plugin\": \"^4.0.1\",\n\t\t\"jest\": \"^29.5.0\",\n\t\t\"jest-cli\": \"^29.5.0\",\n\t\t\"jest-environment-jsdom\": \"^29.5.0\",\n\t\t\"jest-transform-stub\": \"^2.0.0\",\n\t\t\"mini-css-extract-plugin\": \"^2.7.5\",\n\t\t\"postcss-loader\": \"^7.2.4\",\n\t\t\"prettier\": \"^2.8.7\",\n\t\t\"regenerator-runtime\": \"^0.13.11\",\n\t\t\"sass\": \"^1.62.0\",\n\t\t\"sass-loader\": \"^13.2.2\",\n\t\t\"style-loader\": \"^3.3.2\",\n\t\t\"webpack\": \"^5.79.0\",\n\t\t\"webpack-cli\": \"^5.0.1\",\n\t\t\"webpack-merge\": \"^5.8.0\"\n\t},\n\t\"license\": \"MIT\",\n\t\"files\": [\n\t\t\"/dist\",\n\t\t\"index.d.ts\"\n\t],\n\t\"volta\": {\n\t\t\"node\": \"20.0.0\"\n\t}\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: [require('autoprefixer')({ flexbox: 'no-2009' })],\n}\n"
  },
  {
    "path": "src/floating-focus.js",
    "content": "import './floating-focus.scss'\n\nexport const HELPER_FADE_TIME = 800\nexport const MONITOR_INTERVAL = 250\n\nexport default class FloatingFocus {\n  constructor(container = document.body) {\n    this.container = container\n    this.previousTargetRect = null\n    this.floaterIsMoving = false\n\n    this.addEventListeners()\n  }\n\n  addEventListeners() {\n    this.handleKeyDown = this.handleKeyDown.bind(this)\n    this.handleMouseDown = this.handleMouseDown.bind(this)\n    this.handleFocus = this.handleFocus.bind(this)\n    this.handleBlur = this.handleBlur.bind(this)\n    this.handleScrollResize = this.handleScrollResize.bind(this)\n    this.monitorElementPosition = this.monitorElementPosition.bind(this)\n\n    document.addEventListener('keydown', this.handleKeyDown, false)\n    document.addEventListener('mousedown', this.handleMouseDown, false)\n    document.addEventListener('focus', this.handleFocus, true)\n    document.addEventListener('blur', this.handleBlur, true)\n    document.addEventListener('scroll', this.handleScrollResize, true)\n    window.addEventListener('resize', this.handleScrollResize, true)\n  }\n\n  handleKeyDown(e) {\n    // Show animation only upon Tab or Arrow keys press.\n    if (e.keyCode !== 9 && !(e.keyCode > 36 && e.keyCode < 41)) {\n      return\n    }\n\n    if (!this.floater) {\n      this.floater = this.constructFloatingElement()\n    }\n\n    this.enableFloatingFocus()\n  }\n\n  handleMouseDown() {\n    if (!this.floater) {\n      return\n    }\n\n    this.disableFloatingFocus()\n  }\n\n  handleScrollResize() {\n    if (!this.floater || !this.target) {\n      return\n    }\n\n    requestAnimationFrame(() => this.repositionElement(this.target, this.floater))\n  }\n\n  constructFloatingElement() {\n    const element = document.createElement('div')\n    element.classList.add('floating-focus')\n\n    this.container.appendChild(element)\n    return element\n  }\n\n  handleFocus(e) {\n    let target = e.target\n\n    if (!this.floater || !this.container) {\n      return\n    }\n\n    if (target === this.floater) {\n      this.handleBlur()\n      return\n    }\n\n    if (!this.container.contains(target)) {\n      this.handleBlur()\n      return\n    }\n\n    this.floater.classList.add('visible')\n    this.floater.classList.add('helper')\n    this.floater.classList.add('moving')\n\n    const focusTargetAttribute = target.getAttribute('focus-target')\n    if (focusTargetAttribute) {\n      target = document.querySelector(`#${focusTargetAttribute}`) || target\n    }\n\n    this.target = target\n\n    // Make sure we can read the target style (even when refocussing the viewport)\n    this.target.classList.remove('floating-focused')\n    this.target.classList.add('focus')\n\n    this.resolveTargetOutlineStyle(this.target, this.floater)\n    this.repositionElement(this.target, this.floater)\n\n    this.target.classList.add('floating-focused')\n\n    this.handleFloaterMove()\n\n    clearTimeout(this.helperFadeTimeout)\n    this.helperFadeTimeout = setTimeout(() => this.floater.classList.remove('helper'), HELPER_FADE_TIME)\n  }\n\n  handleBlur() {\n    if (!this.floater) {\n      return\n    }\n\n    this.floater.classList.remove('visible')\n    this.floater.classList.remove('helper')\n    this.floater.classList.remove('moving')\n\n    if (!this.target) {\n      return\n    }\n\n    this.target.classList.remove('floating-focused')\n    this.target.classList.remove('focus')\n  }\n\n  enableFloatingFocus() {\n    this.container.classList.add('floating-focus-enabled')\n    this.floater.classList.add('enabled')\n    clearInterval(this.monitorElementPositionInterval)\n    this.monitorElementPositionInterval = setInterval(this.monitorElementPosition, MONITOR_INTERVAL)\n  }\n\n  disableFloatingFocus() {\n    this.container.classList.remove('floating-focus-enabled')\n    this.floater.classList.remove('enabled')\n    clearInterval(this.monitorElementPositionInterval)\n  }\n\n  handleFloaterMove() {\n    if (this.floaterIsMoving) {\n      return\n    }\n\n    this.floaterIsMoving = true\n\n    const removeMovingClass = () => {\n      this.floater.classList.remove('moving')\n      this.floater.removeEventListener('transitionend', removeMovingClass)\n      this.floaterIsMoving = false\n    }\n    this.floater.addEventListener('transitionend', removeMovingClass.bind(this))\n  }\n\n  addPixels(pixels1, pixels2) {\n    const result = parseFloat(pixels1) + parseFloat(pixels2)\n    return !isNaN(result) ? `${result}px` : null\n  }\n\n  getOffsetBorderRadius(baseRadius, offset) {\n    if (!baseRadius || parseFloat(baseRadius) === 0) {\n      return '0px'\n    }\n    if (!offset) {\n      return baseRadius\n    }\n\n    offset = Math.max(parseFloat(offset), 0)\n\n    return this.addPixels(baseRadius, offset) || '0px'\n  }\n\n  resolveTargetOutlineStyle(target, floater) {\n    const targetStyle = window.getComputedStyle(target)\n    const padding = targetStyle.outlineOffset || null\n\n    Object.assign(floater.style, {\n      color: targetStyle.outlineColor,\n      borderWidth: targetStyle.outlineWidth,\n      borderStyle: targetStyle.outlineStyle,\n      borderBottomLeftRadius: this.getOffsetBorderRadius(targetStyle.borderBottomLeftRadius, padding),\n      borderBottomRightRadius: this.getOffsetBorderRadius(targetStyle.borderBottomRightRadius, padding),\n      borderTopLeftRadius: this.getOffsetBorderRadius(targetStyle.borderTopLeftRadius, padding),\n      borderTopRightRadius: this.getOffsetBorderRadius(targetStyle.borderTopRightRadius, padding),\n    })\n  }\n\n  getFloaterPosition(target) {\n    const targetStyle = window.getComputedStyle(target)\n    const padding = parseFloat(targetStyle.outlineOffset || '0px')\n\n    const rect = target.getBoundingClientRect()\n    this.previousTargetRect = rect\n\n    const width = rect.width + padding * 2\n    const height = rect.height + padding * 2\n    const left = window.scrollX + rect.left - padding + width / 2\n    const top = window.scrollY + rect.top - padding + height / 2\n\n    return {\n      left: `${left}px`,\n      top: `${top}px`,\n      width: `${width}px`,\n      height: `${height}px`,\n    }\n  }\n\n  monitorElementPosition() {\n    if (!this.target || !this.previousTargetRect || this.floaterIsMoving) {\n      return\n    }\n\n    const { left, top, width, height } = this.target.getBoundingClientRect()\n    const { left: leftPrev, top: topPrev, width: widthPrev, height: heightPrev } = this.previousTargetRect\n\n    if (left === leftPrev && top === topPrev && width === widthPrev && height === heightPrev) {\n      return\n    }\n\n    this.floater.classList.add('moving')\n    this.repositionElement(this.target, this.floater)\n    this.handleFloaterMove()\n  }\n\n  repositionElement(target, floater) {\n    Object.assign(floater.style, this.getFloaterPosition(target))\n  }\n}\n"
  },
  {
    "path": "src/floating-focus.scss",
    "content": ".floating-focus {\n  border: 0 solid currentColor;\n  position: absolute;\n  transform: translate(-50%, -50%);\n  opacity: 0;\n  will-change: top, left, width, height;\n  box-sizing: content-box;\n  pointer-events: none;\n  overflow: hidden;\n  z-index: 9999999999; // It should always be on top of everything, no matter what.\n\n  &.moving {\n    transition-property: opacity, left, top, width, height, border-width, border-radius;\n    transition-duration: 0.2s, 0.1s, 0.1s, 0.1s, 0.1s, 0.1s, 0.1s;\n    transition-timing-function: linear, ease, ease, ease, ease, ease, ease;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    &.moving {\n      transition: none;\n    }\n  }\n\n  &.enabled.visible {\n    opacity: 1;\n  }\n\n  &::after {\n    content: '';\n    background: currentColor;\n    position: absolute;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    opacity: 0;\n\n    transition: opacity 0.2s linear;\n  }\n\n  &.helper::after {\n    transition-duration: 0.1s;\n    opacity: 0.3;\n  }\n}\n\n.floating-focused {\n  outline-style: none !important;\n\n  &::-moz-focus-inner {\n    border: 0 !important;\n  }\n}\n"
  },
  {
    "path": "src/floating-focus.spec.js",
    "content": "import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals'\nimport FloatingFocus, { HELPER_FADE_TIME, MONITOR_INTERVAL } from './floating-focus'\n\ndescribe('Floating focus', () => {\n  beforeEach(() => {\n    jest.useFakeTimers()\n    jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb())\n  })\n\n  afterEach(() => {\n    document.body.className = ''\n    document.body.innerHTML = ''\n    jest.clearAllTimers()\n    window.requestAnimationFrame.mockRestore()\n  })\n\n  it('Should bind all required event listeners on construction', () => {\n    expect(document.addEventListener).not.toHaveBeenCalled()\n\n    const floatingFocus = new FloatingFocus()\n\n    expect(document.addEventListener).toHaveBeenNthCalledWith(1, 'keydown', floatingFocus.handleKeyDown, false)\n    expect(document.addEventListener).toHaveBeenNthCalledWith(2, 'mousedown', floatingFocus.handleMouseDown, false)\n    expect(document.addEventListener).toHaveBeenNthCalledWith(3, 'focus', floatingFocus.handleFocus, true)\n    expect(document.addEventListener).toHaveBeenNthCalledWith(4, 'blur', floatingFocus.handleBlur, true)\n    expect(document.addEventListener).toHaveBeenNthCalledWith(5, 'scroll', floatingFocus.handleScrollResize, true)\n    expect(window.addEventListener).toHaveBeenNthCalledWith(6, 'resize', floatingFocus.handleScrollResize, true)\n  })\n\n  it('Should not do anything if the keyboard input is not Tab or Arrow keys', () => {\n    const floatingFocus = new FloatingFocus()\n    floatingFocus.enableFloatingFocus = jest.fn()\n\n    floatingFocus.handleKeyDown({ keyCode: 21 })\n    expect(floatingFocus.enableFloatingFocus).not.toHaveBeenCalled()\n\n    floatingFocus.handleKeyDown({ keyCode: 42 })\n    expect(floatingFocus.enableFloatingFocus).not.toHaveBeenCalled()\n\n    floatingFocus.handleKeyDown({ keyCode: 9 })\n    expect(floatingFocus.enableFloatingFocus).toHaveBeenCalled()\n  })\n\n  it('Should construct, append and return a floating element', () => {\n    const floatingFocus = new FloatingFocus()\n    const floatingElement = floatingFocus.constructFloatingElement()\n\n    expect(floatingElement instanceof Element).toBe(true)\n    expect(floatingElement.classList.contains('floating-focus')).toBe(true)\n    expect(floatingElement.tagName).toBe('DIV')\n    expect(document.body.contains(floatingElement)).toBe(true)\n  })\n\n  it(\"Should create the 'floater' element when it is not present yet\", () => {\n    const floatingFocus = new FloatingFocus()\n    floatingFocus.constructFloatingElement = jest.fn().mockImplementation(() => document.createElement('div'))\n\n    expect(floatingFocus.constructFloatingElement).not.toHaveBeenCalled()\n\n    floatingFocus.handleKeyDown({ keyCode: 9 })\n    expect(floatingFocus.constructFloatingElement).toHaveBeenCalled()\n  })\n\n  it(\"Should not recreate the 'floater' element when it's already present created\", () => {\n    const floatingFocus = new FloatingFocus()\n    floatingFocus.constructFloatingElement = jest.fn().mockImplementation(() => document.createElement('div'))\n\n    expect(floatingFocus.constructFloatingElement).not.toHaveBeenCalled()\n\n    floatingFocus.handleKeyDown({ keyCode: 9 })\n    expect(floatingFocus.constructFloatingElement).toHaveBeenCalled()\n\n    floatingFocus.handleKeyDown({ keyCode: 9 })\n    expect(floatingFocus.constructFloatingElement).toHaveBeenCalledTimes(1)\n  })\n\n  it(\"Should only try to disable focus it the 'element' was created before\", () => {\n    const floatingFocus = new FloatingFocus()\n    floatingFocus.disableFloatingFocus = jest.fn()\n\n    expect(floatingFocus.disableFloatingFocus).not.toHaveBeenCalled()\n\n    floatingFocus.handleMouseDown()\n\n    expect(floatingFocus.disableFloatingFocus).not.toHaveBeenCalled()\n\n    floatingFocus.handleKeyDown({ keyCode: 9 })\n    floatingFocus.handleMouseDown()\n\n    expect(floatingFocus.disableFloatingFocus).toHaveBeenCalled()\n  })\n\n  it(\"Should only reposition if a target and 'floater' was set, when scrolling or resizing\", async () => {\n    const floatingFocus = new FloatingFocus()\n    floatingFocus.repositionElement = jest.fn()\n\n    floatingFocus.handleScrollResize()\n\n    expect(floatingFocus.repositionElement).not.toHaveBeenCalled()\n\n    floatingFocus.floater = document.createElement('div')\n    floatingFocus.target = document.createElement('div')\n\n    floatingFocus.handleScrollResize()\n\n    await new Promise((resolve) => requestAnimationFrame(resolve))\n\n    expect(floatingFocus.repositionElement).toHaveBeenCalled()\n  })\n\n  it('Should enable the floating element by setting the appropriate classes', () => {\n    const floatingFocus = new FloatingFocus()\n    floatingFocus.floater = document.createElement('div')\n\n    expect(document.body.classList.contains('floating-focus-enabled')).toBe(false)\n    expect(floatingFocus.floater.classList.contains('enabled')).toBe(false)\n\n    floatingFocus.enableFloatingFocus()\n\n    expect(document.body.classList.contains('floating-focus-enabled')).toBe(true)\n    expect(floatingFocus.floater.classList.contains('enabled')).toBe(true)\n  })\n\n  it('Should disable the floating element by removing the appropriate classes', () => {\n    const floatingFocus = new FloatingFocus()\n    floatingFocus.floater = document.createElement('div')\n\n    floatingFocus.enableFloatingFocus()\n\n    expect(document.body.classList.contains('floating-focus-enabled')).toBe(true)\n    expect(floatingFocus.floater.classList.contains('enabled')).toBe(true)\n\n    floatingFocus.disableFloatingFocus()\n\n    expect(document.body.classList.contains('floating-focus-enabled')).toBe(false)\n    expect(floatingFocus.floater.classList.contains('enabled')).toBe(false)\n  })\n\n  it('Should early return if not meeting requirements yet, when calling for focus handling', () => {\n    const floatingFocus = new FloatingFocus()\n    floatingFocus.resolveTargetOutlineStyle = jest.fn()\n    const target = document.createElement('div')\n\n    floatingFocus.handleFocus({ target }) // Missing 'floater' element\n\n    expect(floatingFocus.resolveTargetOutlineStyle).not.toHaveBeenCalled()\n\n    floatingFocus.floater = floatingFocus.constructFloatingElement()\n    floatingFocus.handleFocus({ target: floatingFocus.floater }) // Target is the same as 'floater' element\n    floatingFocus.handleFocus({ target }) // Target is not inside the body\n\n    expect(floatingFocus.resolveTargetOutlineStyle).not.toHaveBeenCalled()\n\n    document.body.appendChild(target)\n    floatingFocus.handleFocus({ target }) // Successful handleFocus call\n\n    expect(floatingFocus.resolveTargetOutlineStyle).toHaveBeenCalled()\n  })\n\n  it('Should set all appropriate classes when handling focus', async () => {\n    const floatingFocus = new FloatingFocus()\n    floatingFocus.floater = floatingFocus.constructFloatingElement()\n    const target = document.createElement('div')\n    document.body.appendChild(target)\n\n    floatingFocus.handleFocus({ target })\n\n    expect(floatingFocus.floater.classList.contains('visible')).toBe(true)\n    expect(floatingFocus.floater.classList.contains('helper')).toBe(true)\n    expect(floatingFocus.floater.classList.contains('moving')).toBe(true)\n\n    expect(floatingFocus.target).toBe(target)\n    expect(floatingFocus.target.classList.contains('floating-focused')).toBe(true)\n\n    floatingFocus.floater.dispatchEvent(new Event('transitionend'))\n\n    expect(floatingFocus.floater.classList.contains('moving')).toBe(false)\n\n    jest.advanceTimersByTime(HELPER_FADE_TIME)\n\n    expect(floatingFocus.floater.classList.contains('helper')).toBe(false)\n  })\n\n  it('Should change the target to a different element when the focused element has a focus-target attribute', async () => {\n    const floatingFocus = new FloatingFocus()\n    floatingFocus.floater = floatingFocus.constructFloatingElement()\n    const target = document.createElement('div')\n    const focusTarget = document.createElement('div')\n    target.setAttribute('focus-target', 'element123')\n    focusTarget.id = 'element123'\n    document.body.appendChild(target)\n    document.body.appendChild(focusTarget)\n\n    floatingFocus.handleFocus({ target })\n\n    expect(floatingFocus.target).toEqual(focusTarget)\n    expect(focusTarget.classList.contains('focus')).toBe(true)\n  })\n\n  it('Should use the existing target if its focus-target cannot be found', () => {\n    const floatingFocus = new FloatingFocus()\n    floatingFocus.floater = floatingFocus.constructFloatingElement()\n    const target = document.createElement('div')\n    target.setAttribute('focus-target', 'element123')\n    document.body.appendChild(target)\n\n    floatingFocus.handleFocus({ target })\n\n    expect(floatingFocus.target).toEqual(target)\n    expect(target.classList.contains('focus')).toBe(true)\n  })\n\n  it('Should resolve the target outline style and reposition the element when handling focus', () => {\n    const floatingFocus = new FloatingFocus()\n    floatingFocus.resolveTargetOutlineStyle = jest.fn()\n    floatingFocus.repositionElement = jest.fn()\n    floatingFocus.floater = floatingFocus.constructFloatingElement()\n    const target = document.createElement('div')\n    document.body.appendChild(target)\n\n    expect(floatingFocus.resolveTargetOutlineStyle).not.toHaveBeenCalled()\n    expect(floatingFocus.resolveTargetOutlineStyle).not.toHaveBeenCalled()\n\n    floatingFocus.handleFocus({ target })\n\n    expect(floatingFocus.resolveTargetOutlineStyle).toHaveBeenCalled()\n    expect(floatingFocus.resolveTargetOutlineStyle).toHaveBeenCalled()\n  })\n\n  it(\"Should early return when 'floater' is not present when handling blur\", () => {\n    const floatingFocus = new FloatingFocus()\n\n    expect(floatingFocus.handleBlur()).toBe(undefined)\n  })\n\n  it(\"Should remove all visibility classes from the 'floater' when handleBlur is called\", () => {\n    const floatingFocus = new FloatingFocus()\n    floatingFocus.target = document.createElement('div')\n    floatingFocus.floater = floatingFocus.constructFloatingElement()\n    floatingFocus.floater.classList.add('visible')\n    floatingFocus.floater.classList.add('helper')\n    floatingFocus.floater.classList.add('moving')\n\n    expect(floatingFocus.floater.classList.contains('visible')).toBe(true)\n    expect(floatingFocus.floater.classList.contains('helper')).toBe(true)\n    expect(floatingFocus.floater.classList.contains('moving')).toBe(true)\n\n    floatingFocus.handleBlur()\n\n    expect(floatingFocus.floater.classList.contains('visible')).toBe(false)\n    expect(floatingFocus.floater.classList.contains('helper')).toBe(false)\n    expect(floatingFocus.floater.classList.contains('moving')).toBe(false)\n  })\n\n  it('Should resolve and append the outline styling from the target element', () => {\n    const floatingFocus = new FloatingFocus()\n    const target = document.createElement('div')\n    const floater = floatingFocus.constructFloatingElement()\n\n    const targetStyle = {\n      outlineOffset: '8px',\n      outlineColor: 'dodgerblue',\n      outlineStyle: 'dashed',\n      outlineWidth: '2px',\n      borderBottomLeftRadius: '0px',\n      borderBottomRightRadius: '0px',\n      borderTopLeftRadius: '0px',\n      borderTopRightRadius: '0px',\n    }\n\n    window.getComputedStyle = jest.fn().mockImplementation(() => targetStyle)\n\n    floatingFocus.resolveTargetOutlineStyle(target, floater)\n\n    expect(floater.style.color).toBe(targetStyle.outlineColor)\n    expect(floater.style.borderWidth).toBe(targetStyle.outlineWidth)\n    expect(floater.style.borderStyle).toBe(targetStyle.outlineStyle)\n    expect(floater.style.borderBottomLeftRadius).toBe(targetStyle.borderBottomLeftRadius)\n    expect(floater.style.borderBottomRightRadius).toBe(targetStyle.borderBottomRightRadius)\n    expect(floater.style.borderTopLeftRadius).toBe(targetStyle.borderTopLeftRadius)\n    expect(floater.style.borderTopRightRadius).toBe(targetStyle.borderTopRightRadius)\n  })\n\n  it(\"Should correctly offset the target element's border radii by its outline offset\", () => {\n    const floatingFocus = new FloatingFocus()\n    const target = document.createElement('div')\n    const floater = floatingFocus.constructFloatingElement()\n\n    const targetStyle = {\n      outlineOffset: '8px',\n      borderBottomLeftRadius: '6px',\n      borderBottomRightRadius: '0px',\n      borderTopLeftRadius: null,\n    }\n\n    window.getComputedStyle = jest.fn().mockImplementation(() => targetStyle)\n\n    floatingFocus.resolveTargetOutlineStyle(target, floater)\n\n    expect(floater.style.borderBottomLeftRadius).toBe('14px')\n    expect(floater.style.borderBottomRightRadius).toBe('0px')\n    expect(floater.style.borderTopLeftRadius).toBe('0px')\n    expect(floater.style.borderTopRightRadius).toBe('0px')\n\n    targetStyle.outlineOffset = null\n\n    floatingFocus.resolveTargetOutlineStyle(target, floater)\n\n    expect(floater.style.borderBottomLeftRadius).toBe(targetStyle.borderBottomLeftRadius)\n  })\n\n  it.each([4, 0])(\"Should reposition 'floater' based on target position -- outline offset %d\", (outlineOffset) => {\n    const floatingFocus = new FloatingFocus()\n    const target = document.createElement('div')\n    const floater = floatingFocus.constructFloatingElement()\n    const targetStyle = window.getComputedStyle(target)\n    targetStyle.outlineOffset = outlineOffset\n\n    const rect = {\n      left: 42,\n      top: 84,\n      width: 42,\n      height: 128,\n    }\n\n    target.getBoundingClientRect = jest.fn().mockImplementation(() => rect)\n\n    floatingFocus.repositionElement(target, floater)\n\n    expect(floater.style.left).toBe(`${rect.left + rect.width / 2}px`)\n    expect(floater.style.top).toBe(`${rect.top + rect.height / 2}px`)\n    expect(floater.style.width).toBe(`${rect.width + outlineOffset * 2}px`)\n    expect(floater.style.height).toBe(`${rect.height + outlineOffset * 2}px`)\n  })\n\n  it(\"Should automatically reposition the 'floater' when the target element's position changes\", async () => {\n    const floatingFocus = new FloatingFocus()\n    const target = document.createElement('div')\n    document.body.appendChild(target)\n\n    const rect = {\n      left: 42,\n      top: 84,\n      width: 42,\n      height: 128,\n    }\n\n    target.getBoundingClientRect = jest.fn().mockImplementation(() => ({ ...rect }))\n\n    floatingFocus.handleKeyDown({ keyCode: 9 })\n    floatingFocus.enableFloatingFocus()\n    floatingFocus.handleFocus({ target }, true)\n\n    // Cleanup because transitionend is not called in this setup of jsdom\n    floatingFocus.floater.classList.remove('moving')\n    floatingFocus.floaterIsMoving = false\n\n    expect(floatingFocus.floater.style.left).toBe(`${rect.left + rect.width / 2}px`)\n    expect(floatingFocus.floater.style.top).toBe(`${rect.top + rect.height / 2}px`)\n\n    jest.advanceTimersByTime(MONITOR_INTERVAL)\n    expect(floatingFocus.floater.classList.contains('moving')).toBe(false)\n\n    jest.advanceTimersByTime(MONITOR_INTERVAL)\n    expect(floatingFocus.floater.classList.contains('moving')).toBe(false)\n\n    rect.left += 42\n    rect.top += 42\n\n    expect(floatingFocus.floater.classList.contains('moving')).toBe(false)\n    expect(floatingFocus.floater.style.left).not.toBe(`${rect.left + rect.width / 2}px`)\n    expect(floatingFocus.floater.style.top).not.toBe(`${rect.top + rect.height / 2}px`)\n\n    jest.advanceTimersByTime(MONITOR_INTERVAL)\n\n    expect(floatingFocus.floater.classList.contains('moving')).toBe(true)\n    expect(floatingFocus.floater.style.left).toBe(`${rect.left + rect.width / 2}px`)\n    expect(floatingFocus.floater.style.top).toBe(`${rect.top + rect.height / 2}px`)\n  })\n\n  describe('addPixels', () => {\n    it(\"Should correctly add up pixel amounts as if it's a normal calculation\", () => {\n      const floatingFocus = new FloatingFocus()\n\n      const number1 = Math.random() * 10\n      const number2 = Math.random() * 10\n\n      expect(floatingFocus.addPixels(`${number1}px`, `${number2}px`)).toBe(`${number1 + number2}px`)\n    })\n\n    it('Should return null in case of invalid input', () => {\n      const floatingFocus = new FloatingFocus()\n\n      const number1 = '10px'\n      const number2 = 'apx'\n\n      expect(floatingFocus.addPixels(number1, number2)).toBeNull()\n    })\n  })\n})\n"
  },
  {
    "path": "webpack.common.js",
    "content": "const ESLintPlugin = require('eslint-webpack-plugin')\n\nmodule.exports = {\n  entry: {\n    'floating-focus': './src/floating-focus.js',\n  },\n  output: {\n    filename: 'index.js',\n    library: 'floating-focus',\n    libraryTarget: 'umd',\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules|dist/,\n        loader: 'babel-loader',\n      },\n    ],\n  },\n  plugins: [\n    new ESLintPlugin({\n      files: 'src/**/*.js',\n      failOnWarning: true,\n    }),\n  ],\n}\n"
  },
  {
    "path": "webpack.dev.js",
    "content": "const merge = require('webpack-merge')\nconst styled = require('./webpack.styled.js')\n\nmodule.exports = merge(styled, {\n  mode: 'development',\n  watch: true,\n})\n"
  },
  {
    "path": "webpack.prod.js",
    "content": "const { merge } = require('webpack-merge')\nconst styled = require('./webpack.styled.js')\nconst unstyled = require('./webpack.unstyled.js')\nconst { CleanWebpackPlugin } = require('clean-webpack-plugin')\n\nconst prodConfig = {\n  mode: 'production',\n  plugins: [new CleanWebpackPlugin()],\n}\n\nmodule.exports = [merge(styled, prodConfig), merge(unstyled, prodConfig)]\n"
  },
  {
    "path": "webpack.styled.js",
    "content": "/* global __dirname */\nconst path = require('path')\nconst { merge } = require('webpack-merge')\nconst common = require('./webpack.common.js')\n\nmodule.exports = merge(common, {\n  output: {\n    path: path.resolve(__dirname, 'dist/styled'),\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.(css|scss)$/,\n        use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],\n      },\n    ],\n  },\n})\n"
  },
  {
    "path": "webpack.unstyled.js",
    "content": "/* global __dirname */\nconst path = require('path')\nconst { merge } = require('webpack-merge')\nconst common = require('./webpack.common.js')\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin')\nconst CopyWebpackPlugin = require('copy-webpack-plugin')\n\nmodule.exports = merge(common, {\n  output: {\n    path: path.resolve(__dirname, 'dist/unstyled'),\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.(css|scss)$/,\n        use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'],\n      },\n    ],\n  },\n  plugins: [\n    new MiniCssExtractPlugin({\n      filename: 'index.css',\n    }),\n    new CopyWebpackPlugin({\n      patterns: ['index.d.ts'],\n    }),\n  ],\n})\n"
  }
]