├── .editorconfig ├── .eslintrc.json ├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ ├── nodejs.yml │ └── publish.yml ├── .gitignore ├── CODEOWNERS ├── LICENSE ├── README.md ├── docs ├── favicon.ico └── index.html ├── package-lock.json ├── package.json ├── src ├── clipboarditem.ts ├── element-checkvisibility.ts ├── index.ts ├── navigator-clipboard.ts ├── promise-withResolvers.ts └── requestidlecallback.ts ├── test ├── clipboarditem.js ├── element-checkvisibility.js ├── index.js ├── navigator-clipboard.js ├── promise-withResolvers.js └── requestidlecallback.js ├── tsconfig.json └── web-test-runner.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | max_line_length = 120 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "plugins": ["github"], 4 | "extends": ["plugin:github/recommended", "plugin:github/typescript", "plugin:github/browser"], 5 | "rules": { 6 | "no-invalid-this": "off", 7 | "@typescript-eslint/no-invalid-this": ["error"], 8 | "import/extensions": ["error", "ignorePackages"], 9 | "import/no-namespace": "off", 10 | "import/no-unresolved": "off", 11 | "@typescript-eslint/consistent-type-imports": ["error", {"prefer": "type-imports"}] 12 | }, 13 | "overrides": [ 14 | { 15 | "files": "test/*", 16 | "rules": { 17 | "@typescript-eslint/no-empty-function": "off" 18 | }, 19 | "globals": { 20 | "chai": false, 21 | "expect": false, 22 | "globalThis": false 23 | }, 24 | "env": { 25 | "mocha": true 26 | } 27 | }, 28 | { 29 | "files": "*.cjs", 30 | "env": { 31 | "node": true 32 | }, 33 | "rules": { 34 | "import/no-commonjs": "off", 35 | "filenames/match-regex": "off", 36 | "@typescript-eslint/no-var-requires": "off" 37 | } 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup project 2 | description: Sets up the repo code, Node.js, and npm dependencies 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Set up Node.js 8 | uses: actions/setup-node@v2 9 | with: 10 | node-version: '16.x' 11 | cache: npm 12 | registry-url: https://registry.npmjs.org 13 | - name: Install npm dependencies 14 | run: npm ci 15 | shell: bash 16 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 22 16 | registry-url: https://registry.npmjs.org/ 17 | cache: npm 18 | - run: npm ci 19 | - run: npm test 20 | env: 21 | CI: true 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish-npm: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | id-token: write 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 22 18 | registry-url: https://registry.npmjs.org/ 19 | cache: npm 20 | - run: npm ci 21 | - run: npm run build 22 | - run: npm test 23 | - run: npm version ${TAG_NAME} --git-tag-version=false 24 | env: 25 | TAG_NAME: ${{ github.event.release.tag_name }} 26 | - run: npm whoami; npm --ignore-scripts publish --provenance 27 | env: 28 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @github/web-systems-reviewers 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 GitHub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # browser-support 2 | 3 | This library allows websites to maintain compatibility with older browsers, which do not implement newer features. It does so using [polyfills](https://developer.mozilla.org/en-US/docs/Glossary/Polyfill) for small new features, plus functions to determine if a browser supports a set of features natively or with polyfills. 4 | 5 | If you would like to see what features the browser you are currently using implements, you [can visit the documentation site](https://github.github.com/browser-support/) which displays a compatibility table that detects which features are natively supported in your browser. 6 | 7 | ### How is this used on GitHub? 8 | 9 | We use all of these polyfills on GitHub.com. We also use the `isSupported()` function to determine if the browser meets a minimum set of functionality which we expect, browser that return false from `isSupported()` do not send errors or statistics to our backend monitoring. 10 | 11 | ## Installation 12 | 13 | ``` 14 | $ npm install @github/browser-support 15 | ``` 16 | 17 | ## Usage 18 | 19 | ### JS 20 | 21 | ```js 22 | import {isSupported, isPolyfilled, apply} from '@github/browser-support' 23 | 24 | // Check if a browser is supported 25 | if (!isSupported()) { 26 | apply() 27 | console.assert(isSupported() === true) 28 | console.assert(isPolyfilled() === true) 29 | } 30 | ``` 31 | 32 | ## Development 33 | 34 | ``` 35 | npm install 36 | npm test 37 | ``` 38 | 39 | ## Upgrading browser-support in Dotcom 40 | 41 | During upgrades, it is crucial to ensure that browser error reporting to Sentry is not disrupted. Use the following steps to validate this functionality: 42 | 43 | ### Review lab testing 44 | - Create a PR to upgrade the `browser-support` version in Dotcom. 45 | - Trigger a browser error from your `review-lab` instance and confirm it is reported in Sentry: 46 | - Append `#b00m` to your `review-lab` URL (e.g. `https://branchname.review-lab.github.com#b00m`) and refresh the page. 47 | - Confirm the error is reported in [review-lab Sentry](https://github.sentry.io/issues/?environment=review-lab&groupStatsPeriod=auto&project=1890375&query=b00m&referrer=issue-list&statsPeriod=5m). 48 | - Perform these steps in Chrome, Firefox, Edge, and Opera. Note: Errors are currently not reported in Safari due to an [open issue](https://github.com/github/web-systems/issues/3162). 49 | 50 | ### Production deployment 51 | 52 | - Check the [browser-reporting](https://app.datadoghq.com/monitors/168685099) monitor. 53 | - If the rate of reported browser errors drops, the monitor will trigger an alert in the [#web-systems-ops](https://github-grid.enterprise.slack.com/archives/C046W1V95FV) channel. 54 | - After deploying to canary: 55 | - Trigger a browser error by appending `#b00m` to your URL. 56 | - Confirm the error is reported in [canary Sentry](https://github.sentry.io/issues/?environment=canary&groupStatsPeriod=auto&project=1890375&query=b00m&referrer=issue-list&statsPeriod=5m). 57 | - After deploying to production: 58 | - Trigger a browser error by appending `#b00m` to your URL. 59 | - Confirm the error is reported in [production Sentry](https://github.sentry.io/issues/?environment=production&groupStatsPeriod=auto&project=1890375&query=b00m&referrer=issue-list&statsPeriod=5m). 60 | - Check the [browser-reporting monitor](https://app.datadoghq.com/monitors/168685099) to ensure there are no anomalies in the error reporting rate. 61 | 62 | ## Contributing 63 | 64 | ### Adding polyfills 65 | 66 | Please do not add any polyfills for ECMA features that are Stage 3 or below. We _only_ wish to polyfill features from ECMAScript that are Stage 4 (about to be included in a new years specification) or already specified. 67 | 68 | ### Removing polyfills 69 | 70 | Polyfills should only be removed after consulting with the `@github/web-systems` who will determine if a polyfill can be removed. This code is designed to be kept lightweight, we do not want to ship dozens of kb of polyfills. 71 | 72 | As a polyfill is removed, it may be worth adding feature detection to the `baseSupport` const, to ensure that our baseline moves with our browser support matrix. 73 | 74 | ## License 75 | 76 | Distributed under the MIT license. See LICENSE for details. 77 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/browser-support/c616162d8f816c7b2d16afbabbd8c59d1b6998e6/docs/favicon.ico -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GitHub Feature Support Table 5 | 6 | 7 | 155 | 156 | 157 |
158 |

GitHub Feature Support Table

159 |

160 | The table below details some of the client-side ECMAScript features we use to provide the largest and most 161 | advanced development platform in the world. 162 |

163 | 164 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 662 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 |
165 |
166 | Key 167 |

Required feature available in this browser.

168 |

!Required feature, not available in this browser.

169 |

*Not avaible in this browser, but polyfilled using this library.

170 |

171 | Required feature, but polyfilled to smooth over bugs in this browser. 173 |

174 |

175 | **Not available in this browser, but transpiled to a compatible syntax. 176 |

177 |
178 |
...Google ChromeMicrosoft EdgeMozilla FirefoxApple SafariOperaSamsung Internet

Base Objects & Functions

198 | 199 | queueMicrotask Function 200 | 201 |
!
71+
79+
69+
12.1+
58+
10.0+
212 | 213 | HTMLDialogElement Constructor 214 | 215 |
!
37+
79+
98+
15.4+
24+
4.0+
226 | 227 | globalThis Object 228 | 229 |
!
71+
79+
65+
12.1+
58+
10.0+
240 | 243 | Object.fromEntries 244 | 245 |
!
73+
79+
63+
12.1+
60+
11.0+
256 | 257 | Array.flatMap 258 | 259 |
!
69+
79+
62+
12+
56+
10.0+
270 | 271 | String.trimEnd 272 | 273 |
!
66+
79+
61+
12+
53+
9.0+
284 | 287 | String.matchAll 288 | 289 |
!
73+
79+
67+
13+
60+
11.0+
300 | 303 | String.replaceAll 304 | 305 |
!
85+
85+
77+
13.1+
71+
14.0+
316 | 319 | Promise.allSettled 320 | 321 |
!
76+
79+
71+
13+
63+
12.0+
332 | 333 | Promise.any 334 | 335 |
!
85+
85+
79+
14+
71+
14+
346 | 349 | String.prototype.at 350 | 351 |
!
92+
92+
90+
15.4+
65+
16.0+
362 | 365 | Array.prototype.at 366 | 367 |
!
92+
92+
90+
15.4+
65+
16.0+
378 | 379 | Object.hasOwn 380 | 381 |
!
93+
93+
92+
15.4+
79+
17.0+
392 | 393 | AbortSignal.abort 394 | 395 |
!
93+
93+
88+
15+
79+
17.0+
406 | 407 | AbortSignal.timeout 408 | 409 |
!
103+
103+
100+
16+
89+
16+
420 | 421 | AggregateError 422 | 423 |
!
85+
85+
79+
14+
71+
14.0+
434 | 435 | BroadcastChannel 436 | 437 |
!
54+
79+
38+
15.4+
41+
6.0+
448 | 449 | Crypto.randomUUID 450 | 451 |
!
92+
92+
95+
15.4+
78+
16.0+
462 | 463 | Element.replaceChildren 464 | 465 |
!
86+
86+
78+
14+
72+
14.0+
476 | 477 | HTMLFormElement.requestSubmit 478 | 479 |
!
76+
79+
75+
16+
63+
12.0+

Polyfilled Features

494 | 495 | ClipboardItem 496 | 497 |
*
66+ †
79+ †
87+ †
13.1+
53+ †
9.0+ †
508 | 509 | Element.checkVisibility 510 | 511 |
*
105+
105+
106+
17.4+
91+
20.0+
522 | 523 | navigator.clipboard 524 | 525 |
*
86+
79+
63+ †
13.1+
63+ †
12.0+ †
536 | 537 | requestIdleCallback 538 | 539 |
*
47+
79+
55+
*
34+
5.0+
550 | 551 | HTMLElement.popover 552 | 553 |
*
114+
114+
125+
17+
100+
23.0+
564 | 565 | HTMLElement.popover = 'hint' 566 | 567 |
*
114+
114+
125+
17+
+
12.0+
578 | 579 | beforetoggle on Dialog 580 | 581 |
*
132+
132+
133+
*
117+
*
592 | 593 | command & commandfor 594 | 595 |
*
135+
*
*
*
*
*

Native Syntax

610 | 613 | Exponentiation Operator 614 | 615 |
!
52+
14+
52+
10.1+
39+
6.0+
626 | 629 | Object Rest/Spead 630 | 631 |
!
60+
79+
55+
11.1+
47+
8.2+
642 | 645 | RegExp Named Capture Groups 646 | 647 |
!
64+
79+
78+
11.1+
51+
9.0+
658 | 659 | Async Generators & for await 660 | 661 | 665 |
!
666 |
63+
79+
57+
11+
50+
8.0+
676 | 677 | Optional Catch Binding 678 | 679 |
!
66+
79+
58+
11.1+
53+
9.0+
690 | 691 | Optional Chaining Operator (?.) 692 | 693 |
!
80+
80+
74+
13.1+
67+
13.0+
704 | 707 | Nullish Coalescing Operator (??) 708 | 709 |
!
80+
80+
72+
13.1+
67+
13.0+
720 | 723 | Logical Nullish Assignment (??=) 724 | 725 |
**
85+
85+
79+
14+
71+
14.0+
736 | 739 | Public Class Fields 740 | 741 |
**
72+
79+
69+
14.1+
60+
11.0+
752 | 753 | Private Class Fields 754 | 755 |
**
74+
79+
90+
14.1+
62+
11.0+
766 | 769 | Static Class Blocks 770 | 771 |
**
94+
94+
93+
16.4+
80+
17.0+

Transpiled Native Syntax

786 | 787 | Decorators 788 | 789 |
**
**
**
**
**
**
**
800 |
801 | 817 | 832 | 865 | 866 | 867 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@github/browser-support", 3 | "version": "1.2.2", 4 | "description": "Polyfills and Capable Browser detection", 5 | "homepage": "https://github.github.io/browser-support", 6 | "bugs": { 7 | "url": "https://github.com/github/browser-support/issues" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/github/browser-support.git" 12 | }, 13 | "license": "MIT", 14 | "author": "GitHub Inc.", 15 | "contributors": [ 16 | "Keith Cirkel (https://keithcirkel.co.uk/)", 17 | "Kristján Oddsson " 18 | ], 19 | "type": "module", 20 | "main": "lib/index.js", 21 | "module": "lib/index.js", 22 | "files": [ 23 | "lib" 24 | ], 25 | "scripts": { 26 | "build": "tsc --build", 27 | "check": "tsc --noEmit", 28 | "lint": "eslint . --ignore-path .gitignore", 29 | "pretest": "npm run lint && npm run check", 30 | "test": "wtr", 31 | "prepack": "npm run build", 32 | "postpublish": "npm publish --ignore-scripts --@github:registry='https://npm.pkg.github.com'" 33 | }, 34 | "prettier": "@github/prettier-config", 35 | "devDependencies": { 36 | "@github/prettier-config": "^0.0.6", 37 | "@types/node": "^20.5.7", 38 | "@typescript-eslint/eslint-plugin": "^6.5.0", 39 | "@typescript-eslint/parser": "^6.5.0", 40 | "@web/dev-server-esbuild": "^1.0.4", 41 | "@web/test-runner": "^0.20.0", 42 | "chai": "^5.2.0", 43 | "eslint": "^8.48.0", 44 | "eslint-plugin-github": "^4.10.0", 45 | "tslib": "^2.6.2", 46 | "typescript": "^5.2.2" 47 | }, 48 | "dependencies": { 49 | "@oddbird/popover-polyfill": "^0.5.2", 50 | "invokers-polyfill": "^0.5.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/clipboarditem.ts: -------------------------------------------------------------------------------- 1 | type ClipboardItems = Record> 2 | const records = new WeakMap() 3 | const presentationStyles = new WeakMap() 4 | export class ClipboardItem { 5 | constructor(items: ClipboardItems, options: ClipboardItemOptions | undefined = {}) { 6 | if (Object.keys(items).length === 0) throw new TypeError('Empty dictionary argument') 7 | records.set(this, items) 8 | presentationStyles.set(this, options.presentationStyle || 'unspecified') 9 | } 10 | 11 | get presentationStyle(): PresentationStyle { 12 | return presentationStyles.get(this) || 'unspecified' 13 | } 14 | 15 | get types() { 16 | return Object.freeze(Object.keys(records.get(this) || {})) 17 | } 18 | 19 | async getType(type: string): Promise { 20 | const record = records.get(this) 21 | if (record && type in record) { 22 | const item = await record[type]! 23 | if (typeof item === 'string') return new Blob([item], {type}) 24 | return item 25 | } 26 | throw new DOMException("Failed to execute 'getType' on 'ClipboardItem': The type was not found", 'NotFoundError') 27 | } 28 | } 29 | 30 | export function isSupported(): boolean { 31 | try { 32 | new globalThis.ClipboardItem({'text/plain': Promise.resolve('')}) 33 | return true 34 | } catch { 35 | return false 36 | } 37 | } 38 | 39 | export function isPolyfilled(): boolean { 40 | return globalThis.ClipboardItem === ClipboardItem 41 | } 42 | 43 | export function apply(): void { 44 | if (!isSupported()) { 45 | globalThis.ClipboardItem = ClipboardItem 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/element-checkvisibility.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Element { 3 | checkVisibility(options?: Partial): boolean 4 | } 5 | } 6 | 7 | interface CheckVisibilityOptions { 8 | checkOpacity: boolean 9 | checkVisibilityCSS: boolean 10 | } 11 | 12 | export function checkVisibility( 13 | this: Element, 14 | {checkOpacity = false, checkVisibilityCSS = false}: Partial = {}, 15 | ) { 16 | if (!this.isConnected) return false 17 | const styles = getComputedStyle(this) 18 | if (styles.getPropertyValue('display') === 'contents') return false 19 | if (checkVisibilityCSS && styles.getPropertyValue('visibility') !== 'visible') return false 20 | // eslint-disable-next-line @typescript-eslint/no-this-alias 21 | let node: Element | null = this 22 | while (node) { 23 | const nodeStyles = node === this ? styles : getComputedStyle(node) 24 | if (nodeStyles.getPropertyValue('display') === 'none') return false 25 | if (checkOpacity && nodeStyles.getPropertyValue('opacity') === '0') return false 26 | if (node !== this && nodeStyles.getPropertyValue('content-visibility') === 'hidden') { 27 | return false 28 | } 29 | if (!node.parentElement && node.getRootNode() instanceof ShadowRoot) { 30 | node = (node.getRootNode() as ShadowRoot).host 31 | } else { 32 | node = node.parentElement 33 | } 34 | } 35 | return true 36 | } 37 | 38 | export function isSupported(): boolean { 39 | return 'checkVisibility' in Element.prototype && typeof Element.prototype.checkVisibility === 'function' 40 | } 41 | 42 | export function isPolyfilled(): boolean { 43 | return Element.prototype.checkVisibility === checkVisibility 44 | } 45 | 46 | export function apply(): void { 47 | if (!isSupported()) { 48 | Element.prototype.checkVisibility = checkVisibility 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as clipboardItem from './clipboarditem.js' 2 | import * as elementCheckVisibility from './element-checkvisibility.js' 3 | import * as navigatorClipboard from './navigator-clipboard.js' 4 | import * as withResolvers from './promise-withResolvers.js' 5 | import * as requestIdleCallback from './requestidlecallback.js' 6 | import * as popover from '@oddbird/popover-polyfill/fn' 7 | import * as commandAndCommandFor from 'invokers-polyfill/fn' 8 | 9 | let supportsModalPseudo = false 10 | try { 11 | // This will error in older browsers 12 | supportsModalPseudo = document.body.matches(':modal') === false 13 | } catch { 14 | supportsModalPseudo = false 15 | } 16 | 17 | export const baseSupport = 18 | typeof globalThis === 'object' && 19 | // ES2019 20 | 'fromEntries' in Object && 21 | 'flatMap' in Array.prototype && 22 | 'trimEnd' in String.prototype && 23 | // ES2020 24 | 'allSettled' in Promise && 25 | 'matchAll' in String.prototype && 26 | // ES2021 27 | 'replaceAll' in String.prototype && 28 | 'any' in Promise && 29 | // ES2022 30 | 'at' in String.prototype && 31 | 'at' in Array.prototype && 32 | 'hasOwn' in Object && 33 | // ESNext 34 | 'abort' in AbortSignal && 35 | 'timeout' in AbortSignal && 36 | // DOM / HTML and other specs 37 | typeof queueMicrotask === 'function' && 38 | typeof HTMLDialogElement === 'function' && 39 | supportsModalPseudo && 40 | typeof AggregateError === 'function' && 41 | typeof BroadcastChannel === 'function' && 42 | 'randomUUID' in crypto && 43 | 'replaceChildren' in Element.prototype && 44 | 'requestSubmit' in HTMLFormElement.prototype && 45 | // 'requestIdleCallback' in window && // Polyfilled 46 | true 47 | 48 | export const polyfills = { 49 | clipboardItem, 50 | elementCheckVisibility, 51 | navigatorClipboard, 52 | requestIdleCallback, 53 | withResolvers, 54 | popover, 55 | commandAndCommandFor, 56 | } 57 | 58 | export function isSupported() { 59 | return baseSupport && Object.values(polyfills).every(polyfill => polyfill.isSupported()) 60 | } 61 | 62 | export function isPolyfilled() { 63 | return Object.values(polyfills).every(polyfill => polyfill.isPolyfilled()) 64 | } 65 | 66 | export function apply() { 67 | for (const polyfill of Object.values(polyfills)) { 68 | if (!polyfill.isSupported()) polyfill.apply() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/navigator-clipboard.ts: -------------------------------------------------------------------------------- 1 | export async function clipboardWrite(data: ClipboardItems) { 2 | if (data.length === 0) return 3 | const item = data[0] 4 | const blob = await item.getType(item.types.includes('text/plain') ? 'text/plain' : item.types[0]) 5 | return navigator.clipboard.writeText(typeof blob == 'string' ? blob : await blob.text()) 6 | } 7 | 8 | export async function clipboardRead() { 9 | const str = navigator.clipboard.readText() 10 | return [new ClipboardItem({'text/plain': str})] 11 | } 12 | 13 | export function isSupported(): boolean { 14 | return ( 15 | 'clipboard' in navigator && 16 | typeof navigator.clipboard.read === 'function' && 17 | typeof navigator.clipboard.write === 'function' 18 | ) 19 | } 20 | 21 | export function isPolyfilled(): boolean { 22 | return ( 23 | 'clipboard' in navigator && 24 | (navigator.clipboard.write === clipboardWrite || navigator.clipboard.read === clipboardRead) 25 | ) 26 | } 27 | 28 | export function apply(): void { 29 | if ('clipboard' in navigator && !isSupported()) { 30 | navigator.clipboard.write = clipboardWrite 31 | navigator.clipboard.read = clipboardRead 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/promise-withResolvers.ts: -------------------------------------------------------------------------------- 1 | /*#__PURE__*/ 2 | export function withResolvers(this: PromiseConstructor) { 3 | const out = {} as { 4 | promise: Promise 5 | resolve: (value: T | PromiseLike) => void 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | reject: (reason?: any) => void 8 | } 9 | out.promise = new Promise((resolve, reject) => { 10 | out.resolve = resolve 11 | out.reject = reject 12 | }) 13 | return out 14 | } 15 | 16 | /*#__PURE__*/ 17 | export function isSupported(): boolean { 18 | return 'withResolvers' in Promise && typeof Promise.withResolvers === 'function' 19 | } 20 | 21 | /*#__PURE__*/ 22 | export function isPolyfilled(): boolean { 23 | return 'withResolvers' in Promise && Promise.withResolvers === withResolvers 24 | } 25 | 26 | export function apply(): void { 27 | if (!isSupported()) { 28 | Object.assign(Promise, {withResolvers}) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/requestidlecallback.ts: -------------------------------------------------------------------------------- 1 | // https://w3c.github.io/requestidlecallback/#why50 2 | // "Capping idle deadlines to 50ms means that even if the user input occurs 3 | // immediately after the idle task has begun, the user agent still has a 4 | // remaining 50ms in which to respond to the user input without producing user 5 | // perceptible lag" 6 | const maxDeadline = 50 7 | 8 | export function requestIdleCallback(callback: IdleRequestCallback, options: IdleRequestOptions = {}): number { 9 | const start = Date.now() 10 | const timeout = options.timeout || 0 11 | const deadline: IdleDeadline = Object.defineProperty( 12 | { 13 | didTimeout: false, 14 | timeRemaining() { 15 | return Math.max(0, maxDeadline - (Date.now() - start)) 16 | }, 17 | }, 18 | 'didTimeout', 19 | { 20 | get() { 21 | return Date.now() - start > timeout 22 | }, 23 | }, 24 | ) 25 | return window.setTimeout(() => { 26 | callback(deadline) 27 | }) 28 | } 29 | 30 | export function cancelIdleCallback(id: number): void { 31 | clearTimeout(id) 32 | } 33 | 34 | /*#__PURE__*/ 35 | export function isSupported(): boolean { 36 | return typeof globalThis.requestIdleCallback === 'function' 37 | } 38 | 39 | /*#__PURE__*/ 40 | export function isPolyfilled(): boolean { 41 | return globalThis.requestIdleCallback === requestIdleCallback && globalThis.cancelIdleCallback === cancelIdleCallback 42 | } 43 | 44 | export function apply(): void { 45 | if (!isSupported()) { 46 | globalThis.requestIdleCallback = requestIdleCallback 47 | globalThis.cancelIdleCallback = cancelIdleCallback 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/clipboarditem.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {ClipboardItem, apply, isSupported, isPolyfilled} from '../src/clipboarditem.ts' 3 | 4 | describe('ClipboardItem', () => { 5 | it('has standard isSupported, isPolyfilled, apply API', () => { 6 | expect(isSupported).to.be.a('function') 7 | expect(isPolyfilled).to.be.a('function') 8 | expect(apply).to.be.a('function') 9 | expect(isSupported()).to.be.a('boolean') 10 | expect(isPolyfilled()).to.equal(false) 11 | }) 12 | 13 | it('takes a Promise type, that can resolve', async () => { 14 | const c = new ClipboardItem({'text/plain': Promise.resolve('hi')}) 15 | expect(c.types).to.eql(['text/plain']) 16 | expect(await c.getType('text/plain')).to.be.instanceof(Blob) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/element-checkvisibility.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {apply, isPolyfilled, isSupported, checkVisibility} from '../src/element-checkvisibility.ts' 3 | 4 | describe('checkVisibility', () => { 5 | it('has standard isSupported, isPolyfilled, apply API', () => { 6 | expect(isSupported).to.be.a('function') 7 | expect(isPolyfilled).to.be.a('function') 8 | expect(apply).to.be.a('function') 9 | expect(isSupported()).to.be.a('boolean') 10 | expect(isPolyfilled()).to.equal(false) 11 | }) 12 | 13 | it('checks visibility of elements', async () => { 14 | // These tests originate from 15 | // https://github.com/web-platform-tests/wpt/blob/master/css/cssom-view/checkVisibility.html 16 | const el = document.createElement('div') 17 | // eslint-disable-next-line github/no-inner-html 18 | el.innerHTML = ` 19 | 20 |
21 |
hello
22 |
23 |
24 |
hello
25 |
26 | 27 | 30 |
31 |
hello
32 |
33 |
hello
34 |
35 |
slotted
36 |
37 |
38 |
39 |
40 |
41 |
42 |
slotted
43 |
44 |
spacer
45 |
46 |
hello
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | ` 56 | document.body.append(el) 57 | for (const host of document.querySelectorAll('.shadow-host-with-slot')) { 58 | const shadowRoot = host.attachShadow({mode: 'open'}) 59 | const slot = document.createElement('slot') 60 | slot.name = 'slot' 61 | shadowRoot.appendChild(slot) 62 | } 63 | expect(checkVisibility.call(document.getElementById('visibilityhidden'), {checkVisibilityCSS: true})).to.equal( 64 | false, 65 | ) 66 | expect(checkVisibility.call(document.getElementById('visibilityhidden'), {checkVisibilityCSS: false})).to.equal( 67 | true, 68 | ) 69 | expect(checkVisibility.call(document.getElementById('cvhidden'))).to.equal(false) 70 | expect(checkVisibility.call(document.getElementById('slottedincvhidden'))).to.equal(false) 71 | expect(checkVisibility.call(document.getElementById('cvauto'))).to.equal(true) 72 | expect(checkVisibility.call(document.getElementById('cvautooffscreen'))).to.equal(true) 73 | expect(checkVisibility.call(document.getElementById('displaynone'))).to.equal(false) 74 | expect(checkVisibility.call(document.getElementById('slottedindisplaynone'))).to.equal(false) 75 | expect(checkVisibility.call(document.getElementById('displaycontents'))).to.equal(false) 76 | expect(checkVisibility.call(document.getElementById('displaycontentschild'))).to.equal(true) 77 | expect(checkVisibility.call(document.getElementById('opacityzero'), {checkOpacity: true})).to.equal(false) 78 | expect(checkVisibility.call(document.getElementById('opacityzero'), {checkOpacity: false})).to.equal(true) 79 | expect(checkVisibility.call(document.getElementById('slottedinopacityzero'), {checkOpacity: true})).to.equal(false) 80 | expect(checkVisibility.call(document.getElementById('slottedinopacityzero'), {checkOpacity: false})).to.equal(true) 81 | const cvautocontainer = document.getElementById('cvautocontainer') 82 | const cvautochild = document.getElementById('cvautochild') 83 | cvautocontainer.style.contentVisibility = 'auto' 84 | cvautochild.style.visibility = 'hidden' 85 | expect(checkVisibility.call(cvautochild, {checkVisibilityCSS: true})).to.equal(false) 86 | cvautochild.style.visibility = 'visible' 87 | expect(checkVisibility.call(cvautochild, {checkVisibilityCSS: true})).to.equal(true) 88 | expect(checkVisibility.call(document.getElementById('nestedcvautochild'))).to.equal(true) 89 | const cvhiddenchildwithupdate = document.getElementById('cvhiddenchildwithupdate') 90 | cvhiddenchildwithupdate.getBoundingClientRect() 91 | expect(checkVisibility.call(cvhiddenchildwithupdate)).to.equal(false) 92 | const cvhiddenwithupdate = document.getElementById('cvhiddenwithupdate') 93 | cvhiddenwithupdate.getBoundingClientRect() 94 | expect(checkVisibility.call(cvhiddenwithupdate)).to.equal(true) 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {apply, isPolyfilled, isSupported} from '../src/index.ts' 3 | 4 | describe('abortSignalAbort', () => { 5 | it('has standard isSupported, isPolyfilled, apply API', () => { 6 | expect(isSupported).to.be.a('function') 7 | expect(isPolyfilled).to.be.a('function') 8 | expect(apply).to.be.a('function') 9 | expect(isSupported()).to.be.a('boolean') 10 | expect(isPolyfilled()).to.equal(false) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /test/navigator-clipboard.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {clipboardRead, clipboardWrite, apply, isPolyfilled, isSupported} from '../src/navigator-clipboard.ts' 3 | 4 | describe('navigator clipboard', () => { 5 | it('has standard isSupported, isPolyfilled, apply API', () => { 6 | expect(isSupported).to.be.a('function') 7 | expect(isPolyfilled).to.be.a('function') 8 | expect(apply).to.be.a('function') 9 | expect(isSupported()).to.be.a('boolean') 10 | expect(isPolyfilled()).to.equal(false) 11 | }) 12 | 13 | describe('read', () => { 14 | it('read returns array of 1 clipboard entry with plaintext of readText value', async () => { 15 | navigator.clipboard.readText = () => Promise.resolve('foo') 16 | const arr = await clipboardRead() 17 | expect(arr).to.have.lengthOf(1) 18 | expect(arr[0]).to.be.an.instanceof(globalThis.ClipboardItem) 19 | expect(arr[0].types).to.eql(['text/plain']) 20 | expect(await (await arr[0].getType('text/plain')).text()).to.eql('foo') 21 | }) 22 | }) 23 | 24 | describe('write', () => { 25 | it('unpacks text/plain content to writeText', async () => { 26 | const calls = [] 27 | navigator.clipboard.writeText = (...args) => calls.push(args) 28 | await clipboardWrite([ 29 | new globalThis.ClipboardItem({ 30 | 'foo/bar': 'horrible', 31 | 'text/plain': Promise.resolve('foo'), 32 | }), 33 | ]) 34 | expect(calls).to.have.lengthOf(1) 35 | expect(calls[0]).to.eql(['foo']) 36 | }) 37 | 38 | it('accepts multiple clipboard items, picking the first', async () => { 39 | const calls = [] 40 | navigator.clipboard.writeText = (...args) => calls.push(args) 41 | await clipboardWrite([ 42 | new globalThis.ClipboardItem({ 43 | 'foo/bar': 'horrible', 44 | 'text/plain': Promise.resolve('multiple-pass'), 45 | }), 46 | new globalThis.ClipboardItem({ 47 | 'foo/bar': 'multiple-fail', 48 | 'text/plain': Promise.resolve('multiple-fail'), 49 | }), 50 | ]) 51 | expect(calls).to.have.lengthOf(1) 52 | expect(calls[0]).to.eql(['multiple-pass']) 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /test/promise-withResolvers.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {apply, isPolyfilled, isSupported, withResolvers} from '../src/promise-withResolvers.ts' 3 | 4 | describe('withResolvers', () => { 5 | it('has standard isSupported, isPolyfilled, apply API', () => { 6 | expect(isSupported).to.be.a('function') 7 | expect(isPolyfilled).to.be.a('function') 8 | expect(apply).to.be.a('function') 9 | expect(isSupported()).to.be.a('boolean') 10 | expect(isPolyfilled()).to.equal(false) 11 | }) 12 | 13 | it('resolves to first resolving value', async () => { 14 | const arg = withResolvers() 15 | expect(Object.keys(arg).sort()).to.eql(['promise', 'reject', 'resolve']) 16 | expect(arg).to.have.property('promise').to.be.a('promise') 17 | expect(arg).to.have.property('resolve').to.be.a('function') 18 | expect(arg).to.have.property('reject').to.be.a('function') 19 | 20 | arg.resolve(1) 21 | expect(await arg.promise).to.be.eql(1) 22 | }) 23 | 24 | it('rejects to first rejecting reason', async () => { 25 | const arg = withResolvers() 26 | expect(Object.keys(arg).sort()).to.eql(['promise', 'reject', 'resolve']) 27 | expect(arg).to.have.property('promise').to.be.a('promise') 28 | expect(arg).to.have.property('resolve').to.be.a('function') 29 | expect(arg).to.have.property('reject').to.be.a('function') 30 | 31 | const err = new Error('rejected') 32 | 33 | try { 34 | arg.reject(err) 35 | await arg.promise 36 | expect.fail('should fail') 37 | } catch (e) { 38 | expect(e).to.be.eql(err) 39 | } 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /test/requestidlecallback.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {apply, isPolyfilled, isSupported, requestIdleCallback} from '../src/requestidlecallback.ts' 3 | 4 | describe('requestIdleCallback', () => { 5 | it('has standard isSupported, isPolyfilled, apply API', () => { 6 | expect(isSupported).to.be.a('function') 7 | expect(isPolyfilled).to.be.a('function') 8 | expect(apply).to.be.a('function') 9 | expect(isSupported()).to.be.a('boolean') 10 | expect(isPolyfilled()).to.equal(false) 11 | }) 12 | 13 | it('resolves to first resolving value', async () => { 14 | const arg = await new Promise(resolve => requestIdleCallback(resolve)) 15 | expect(Object.keys(arg)).to.eql(['didTimeout', 'timeRemaining']) 16 | expect(arg).to.have.property('didTimeout').to.be.a('boolean') 17 | expect(arg).to.have.property('timeRemaining').to.be.a('function') 18 | expect(arg.timeRemaining()).to.be.a('number').lessThanOrEqual(50).greaterThanOrEqual(0) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "lib": ["es2023", "dom"], 10 | "module": "ESNext", 11 | "moduleResolution": "node", 12 | "noEmit": false, 13 | "outDir": "./lib", 14 | "sourceMap": true, 15 | "strict": true, 16 | "target": "ES2020" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web-test-runner.config.js: -------------------------------------------------------------------------------- 1 | import {esbuildPlugin} from '@web/dev-server-esbuild' 2 | 3 | export default { 4 | files: ['test/*'], 5 | nodeResolve: true, 6 | plugins: [esbuildPlugin({ts: true, target: 'es2020'})], 7 | } 8 | --------------------------------------------------------------------------------