├── .changeset ├── README.md ├── config.json ├── pre.json └── rotten-dolphins-fly.md ├── .editorconfig ├── .github └── workflows │ ├── manual-test.yml │ └── pr.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── jsdom-tests ├── index.html └── jsdom.test.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── CustomStateSet.ts ├── HTMLFormControlsCollection.ts ├── ValidityState.ts ├── aom.ts ├── element-internals.ts ├── index.ts ├── maps.ts ├── mutation-observers.ts ├── patch-form-prototype.ts ├── types.ts └── utils.ts ├── static ├── index.html ├── safari.html ├── scripts │ ├── address.js │ ├── foo-bar.js │ ├── issue30.js │ ├── page.js │ └── x-array.js └── styles │ └── page.css ├── test ├── CustomStateSet.test.ts ├── ElementInternals.test.js ├── FormElements.test.ts ├── fieldset.test.ts ├── lit-ssr.test.ts ├── polyfilledBrowsers.test.js └── ssr-test-el.js ├── tsconfig.json └── web-test-runner.config.mjs /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.changeset/pre.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "pre", 3 | "tag": "next", 4 | "initialVersions": { 5 | "element-internals-polyfill": "1.3.12-alpha.0" 6 | }, 7 | "changesets": [ 8 | "rotten-dolphins-fly" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.changeset/rotten-dolphins-fly.md: -------------------------------------------------------------------------------- 1 | --- 2 | "element-internals-polyfill": major 3 | --- 4 | 5 | Adjust TypeScript types to be more robust 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | insert_final_newline = false 13 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/workflows/manual-test.yml: -------------------------------------------------------------------------------- 1 | name: Manual test 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | build-lint-test: 7 | # Prevents changesets action from creating a PR on forks 8 | if: github.repository == 'calebdwilliams/element-internals-polyfill' 9 | name: Build, Lint, Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Repo 13 | uses: actions/checkout@main 14 | with: 15 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 16 | fetch-depth: 0 17 | 18 | - name: Setup Node 16.x 19 | uses: actions/setup-node@main 20 | with: 21 | node-version: 16.x 22 | cache: 'npm' 23 | registry-url: 'https://registry.npmjs.org' 24 | 25 | - name: Install dependencies 26 | run: npm ci 27 | 28 | - name: Install playwright browsers 29 | run: npx playwright install --with-deps 30 | 31 | - name: Build packages 32 | run: npm run build 33 | 34 | - name: Test 35 | run: npm test 36 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | build-lint-test: 7 | # Prevents changesets action from creating a PR on forks 8 | if: github.repository == 'calebdwilliams/element-internals-polyfill' 9 | name: Build, Lint, Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Repo 13 | uses: actions/checkout@main 14 | with: 15 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 16 | fetch-depth: 0 17 | 18 | - name: Setup Node 16.x 19 | uses: actions/setup-node@main 20 | with: 21 | node-version: 16.x 22 | cache: 'npm' 23 | registry-url: 'https://registry.npmjs.org' 24 | 25 | - name: Install dependencies 26 | run: npm ci 27 | 28 | - name: Install playwright browsers 29 | run: npx playwright install --with-deps 30 | 31 | - name: Build packages 32 | run: npm run build 33 | 34 | - name: Test 35 | run: npm test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .coverage 3 | coverage 4 | node_modules 5 | dist 6 | .npmrc 7 | .idea 8 | jsdom-tests/polyfill.js 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [3.0.1](https://github.com/calebdwilliams/element-internals-polyfill/compare/v3.0.0...v3.0.1) (2025-02-25) 6 | 7 | ## [3.0.0](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.3.11...v3.0.0) (2025-02-21) 8 | 9 | 10 | ### ⚠ BREAKING CHANGES 11 | 12 | * this could result in changes to how TypeScript treats the polyfill 13 | 14 | ### Features 15 | 16 | * update types ([0449145](https://github.com/calebdwilliams/element-internals-polyfill/commit/0449145a14151fa53c87628dca60666cd2b23a14)) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * fix the custom state set behavior ([19f31c4](https://github.com/calebdwilliams/element-internals-polyfill/commit/19f31c4345c9c4172a190eda905168aeaf0cf2f5)) 22 | * fix type for ElementInternals.form ([251c648](https://github.com/calebdwilliams/element-internals-polyfill/commit/251c6484c2bd4b9e5a6d076b9e8e10e75a5101be)) 23 | 24 | ## 2.0.0-next.1 25 | 26 | ### Major Changes 27 | 28 | - Adjust TypeScript types to be more robust 29 | 30 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 31 | 32 | ### [1.3.12-alpha.0](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.3.11...v1.3.12-alpha.0) (2024-09-27) 33 | ### [1.3.13](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.3.10...v1.3.13) (2025-01-24) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * update grammar ([df19136](https://github.com/calebdwilliams/element-internals-polyfill/commit/df1913674f5fdb82de59c752eb2b72ba09f6f09e)) 39 | 40 | ### [1.3.12](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.3.11...v1.3.12) (2024-09-27) 41 | 42 | 43 | ### Bug Fixes 44 | 45 | * update grammar ([f8a1907](https://github.com/calebdwilliams/element-internals-polyfill/commit/f8a19070dd04611fffd4e748a88ff5574a54a584)) 46 | 47 | ### [1.3.11](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.3.10...v1.3.11) (2024-04-10) 48 | 49 | ### [1.3.10](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.3.9...v1.3.10) (2023-12-21) 50 | 51 | ### [1.3.9](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.3.8...v1.3.9) (2023-10-18) 52 | 53 | ### Bug Fixes 54 | 55 | - only inits form association if element is form associated ([a3e742b](https://github.com/calebdwilliams/element-internals-polyfill/commit/a3e742be93f9e15112b204319b7edb3160ed20c1)) 56 | 57 | ### [1.3.8](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.3.7...v1.3.8) (2023-09-07) 58 | 59 | ### Bug Fixes 60 | 61 | - only run document observer if document exists ([7bf53f5](https://github.com/calebdwilliams/element-internals-polyfill/commit/7bf53f54182f47175e20cab7de9b002c6547d29a)) 62 | 63 | ### [1.3.7](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.3.6...v1.3.7) (2023-08-07) 64 | 65 | ### Bug Fixes 66 | 67 | - **types:** reference `ValidityState` from typescript lib ([bbcf7f4](https://github.com/calebdwilliams/element-internals-polyfill/commit/bbcf7f4f0c1f59b9c61444bd7c8f4c0762801a1c)), closes [#119](https://github.com/calebdwilliams/element-internals-polyfill/issues/119) 68 | 69 | ### [1.3.6](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.3.5...v1.3.6) (2023-08-07) 70 | 71 | ### Bug Fixes 72 | 73 | - remove :is in submit selector to prevent error in non-supporting browsers ([f3cb74a](https://github.com/calebdwilliams/element-internals-polyfill/commit/f3cb74adfbfd22684c5bc5e70f0528b35033b160)) 74 | 75 | ### [1.3.5](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.3.4...v1.3.5) (2023-05-04) 76 | 77 | ### Bug Fixes 78 | 79 | - **form submit:** change submit button selector for jsdom compatibility ([ee1269d](https://github.com/calebdwilliams/element-internals-polyfill/commit/ee1269d3b9ddafcf795c67aa74d2de89c492189f)) 80 | 81 | ### [1.3.4](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.3.3...v1.3.4) (2023-04-26) 82 | 83 | ### Bug Fixes 84 | 85 | - custom states will delay if removed in constructor ([74d9ae2](https://github.com/calebdwilliams/element-internals-polyfill/commit/74d9ae2ccbe6eaae065024acccc0bb3357e22b49)) 86 | 87 | ### [1.3.3](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.3.2...v1.3.3) (2023-04-26) 88 | 89 | ### Bug Fixes 90 | 91 | - update src import paths ([35ac8d3](https://github.com/calebdwilliams/element-internals-polyfill/commit/35ac8d3d47082ad0db4adee8e55b3ecbe0320f83)) 92 | 93 | ### [1.3.2](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.3.1...v1.3.2) (2023-04-26) 94 | 95 | ### Bug Fixes 96 | 97 | - custom states will delay if added in constructor ([f81ef5e](https://github.com/calebdwilliams/element-internals-polyfill/commit/f81ef5ec7f2d9e64866436ca7e21dd0464704fea)) 98 | 99 | ### [1.3.1](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.3.0...v1.3.1) (2023-04-24) 100 | 101 | ### Bug Fixes 102 | 103 | - respond to changes to the name attribute and naive jsdom tests ([9af08e9](https://github.com/calebdwilliams/element-internals-polyfill/commit/9af08e9fdf60a2d639274f30d96e61b2892f995e)) 104 | 105 | ## [1.3.0](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.2.6...v1.3.0) (2023-04-14) 106 | 107 | ### Features 108 | 109 | - works with SSR ([bc68c1f](https://github.com/calebdwilliams/element-internals-polyfill/commit/bc68c1f250bbf0887e41edfffd67412253d5f02b)) 110 | 111 | ### [1.2.6](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.2.5...v1.2.6) (2023-03-06) 112 | 113 | ### Bug Fixes 114 | 115 | - fix typeof check for HTMLFormElement ([27c0006](https://github.com/calebdwilliams/element-internals-polyfill/commit/27c0006ccd3ca9045a6ea7dd37d04b0f6f98f1d4)) 116 | 117 | ### [1.2.5](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.2.4...v1.2.5) (2023-03-06) 118 | 119 | ### Bug Fixes 120 | 121 | - only attempt to patch HTMLFormElement if it is defined ([7dd1998](https://github.com/calebdwilliams/element-internals-polyfill/commit/7dd19982f34e1f54835d76e389570bba926f39a6)) 122 | 123 | ### [1.2.4](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.2.3...v1.2.4) (2023-03-03) 124 | 125 | ### Bug Fixes 126 | 127 | - update how polyfill patches customElements.define ([c478d3e](https://github.com/calebdwilliams/element-internals-polyfill/commit/c478d3e3f71d90ac38f272e3c4b414b1dbed3652)) 128 | 129 | ### [1.2.3](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.2.2...v1.2.3) (2023-02-03) 130 | 131 | ### Bug Fixes 132 | 133 | - rollback use of ?? and ?. operators to maintain compatibility with legacy build systems, such as those used in storybook ([#107](https://github.com/calebdwilliams/element-internals-polyfill/issues/107)) ([3fad9fb](https://github.com/calebdwilliams/element-internals-polyfill/commit/3fad9fb3f47a21745b123b5d580b339cfc7349fb)) 134 | 135 | ### [1.2.2](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.2.1...v1.2.2) (2023-01-26) 136 | 137 | ### Bug Fixes 138 | 139 | - **utils:** flip order of nativeControlValidity filter predicate check ([#106](https://github.com/calebdwilliams/element-internals-polyfill/issues/106)) ([ce5aea2](https://github.com/calebdwilliams/element-internals-polyfill/commit/ce5aea287259a8aeaf1c1fdc4a2b4edfd101431f)) 140 | 141 | ### [1.2.1](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.2.0...v1.2.1) (2023-01-26) 142 | 143 | ### Bug Fixes 144 | 145 | - filtering native elements out of setFormValidity#nativeControlValidity ([80442ed](https://github.com/calebdwilliams/element-internals-polyfill/commit/80442edc500db999ab180060918d75232c07e8cd)) 146 | 147 | ## [1.2.0](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.22...v1.2.0) (2023-01-26) 148 | 149 | ### [1.1.22](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.21...v1.1.22) (2023-01-26) 150 | 151 | ### [1.1.21](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.19...v1.1.21) (2023-01-26) 152 | 153 | ### Bug Fixes 154 | 155 | - ensure polyfill generates attributes ([ca506cd](https://github.com/calebdwilliams/element-internals-polyfill/commit/ca506cd5f4c82492dcb4e81fe995487b9eaefaa4)) 156 | - polyfill respects setting disabled on fieldset elements ([ca90197](https://github.com/calebdwilliams/element-internals-polyfill/commit/ca90197dce1c8393400deaaa6b5d304b7e9ac9e9)) 157 | 158 | ### [1.1.20](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.19...v1.1.20) (2023-01-21) 159 | 160 | ### Bug Fixes 161 | 162 | - polyfill respects setting disabled on fieldset elements ([ca90197](https://github.com/calebdwilliams/element-internals-polyfill/commit/ca90197dce1c8393400deaaa6b5d304b7e9ac9e9)) 163 | 164 | ### [1.1.19](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.18...v1.1.19) (2023-01-15) 165 | 166 | ### [1.1.18](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.17...v1.1.18) (2023-01-05) 167 | 168 | ### Bug Fixes 169 | 170 | - add missing aria attributes ([01bc332](https://github.com/calebdwilliams/element-internals-polyfill/commit/01bc33267e1814ed6e84009e7f758f73cd1c155d)) 171 | 172 | ### [1.1.17](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.14...v1.1.17) (2022-12-09) 173 | 174 | ### Bug Fixes 175 | 176 | - click references in label click ([89967dd](https://github.com/calebdwilliams/element-internals-polyfill/commit/89967dd91d0a04726067ca6f36382ac6714eccd5)), closes [#85](https://github.com/calebdwilliams/element-internals-polyfill/issues/85) 177 | - element internals in the LitSSR environment ([5bc5e3e](https://github.com/calebdwilliams/element-internals-polyfill/commit/5bc5e3ef760acc8c1d035aefa0cf46abc2d73074)) 178 | - fixes condition in mutation observer ([3b8ece9](https://github.com/calebdwilliams/element-internals-polyfill/commit/3b8ece99c24d4b70f8f32b3b4ee9d8d72d35dd59)) 179 | 180 | ### [1.1.16](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.14...v1.1.16) (2022-10-31) 181 | 182 | ### Bug Fixes 183 | 184 | - click references in label click ([89967dd](https://github.com/calebdwilliams/element-internals-polyfill/commit/89967dd91d0a04726067ca6f36382ac6714eccd5)), closes [#85](https://github.com/calebdwilliams/element-internals-polyfill/issues/85) 185 | - element internals in the LitSSR environment ([5bc5e3e](https://github.com/calebdwilliams/element-internals-polyfill/commit/5bc5e3ef760acc8c1d035aefa0cf46abc2d73074)) 186 | - fixes condition in mutation observer ([3b8ece9](https://github.com/calebdwilliams/element-internals-polyfill/commit/3b8ece99c24d4b70f8f32b3b4ee9d8d72d35dd59)) 187 | 188 | ### [1.1.15](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.14...v1.1.15) (2022-10-18) 189 | 190 | ### Bug Fixes 191 | 192 | - element internals in the LitSSR environment ([5bc5e3e](https://github.com/calebdwilliams/element-internals-polyfill/commit/5bc5e3ef760acc8c1d035aefa0cf46abc2d73074)) 193 | 194 | ### [1.1.14](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.13...v1.1.14) (2022-09-22) 195 | 196 | ### Bug Fixes 197 | 198 | - fix feature detection ([0775126](https://github.com/calebdwilliams/element-internals-polyfill/commit/07751269a344e703ddf3831c7cee7cf5abf07fe9)) 199 | 200 | ### [1.1.13](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.9...v1.1.13) (2022-09-15) 201 | 202 | ### Bug Fixes 203 | 204 | - fix nodes added in a tree in Safari ([d72172c](https://github.com/calebdwilliams/element-internals-polyfill/commit/d72172c3997440f4e594639a31b3589a4b9913d1)) 205 | - issue with compiled target ([d32d8b2](https://github.com/calebdwilliams/element-internals-polyfill/commit/d32d8b239b54384484105085c497a032cbfcf82a)) 206 | - update types on CustomStateSet ([5d5a7f9](https://github.com/calebdwilliams/element-internals-polyfill/commit/5d5a7f94cf30cb9c7bc72222e5b56b7d8db9ed6d)) 207 | 208 | ### [1.1.12](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.9...v1.1.12) (2022-09-09) 209 | 210 | ### Bug Fixes 211 | 212 | - fix nodes added in a tree in Safari ([d72172c](https://github.com/calebdwilliams/element-internals-polyfill/commit/d72172c3997440f4e594639a31b3589a4b9913d1)) 213 | - issue with compiled target ([62f07a9](https://github.com/calebdwilliams/element-internals-polyfill/commit/62f07a90598eb609bc03f12be4a8a2436f5875f6)) 214 | - update types on CustomStateSet ([0075ceb](https://github.com/calebdwilliams/element-internals-polyfill/commit/0075ceba53ffaf0cc8360c1c842abec8b41e34fc)) 215 | 216 | ### [1.1.11](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.10...v1.1.11) (2022-08-24) 217 | 218 | ### Bug Fixes 219 | 220 | - issue with compiled target ([25bb106](https://github.com/calebdwilliams/element-internals-polyfill/commit/25bb106abaaca4650007eee328a08fa67ed5b483)) 221 | 222 | ### [1.1.10](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.9...v1.1.10) (2022-08-23) 223 | 224 | ### Bug Fixes 225 | 226 | - fix nodes added in a tree in Safari ([d72172c](https://github.com/calebdwilliams/element-internals-polyfill/commit/d72172c3997440f4e594639a31b3589a4b9913d1)) 227 | 228 | ### [1.1.9](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.8...v1.1.9) (2022-08-18) 229 | 230 | ### [1.1.8](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.7...v1.1.8) (2022-08-18) 231 | 232 | ### Bug Fixes 233 | 234 | - fix nodes added in a tree in Safari ([980fa98](https://github.com/calebdwilliams/element-internals-polyfill/commit/980fa984c48a0fd173821a8f96d4ad61189534b6)) 235 | 236 | ### [1.1.7](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.6...v1.1.7) (2022-08-17) 237 | 238 | ### Bug Fixes 239 | 240 | - don't append FormData element in reverse order ([7f9cd4c](https://github.com/calebdwilliams/element-internals-polyfill/commit/7f9cd4c4a1c969d5ae4b4f62eb00b0e058f8ec3b)) 241 | 242 | ### [1.1.6](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.5...v1.1.6) (2022-07-15) 243 | 244 | ### [1.1.5](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.4...v1.1.5) (2022-07-15) 245 | 246 | ### Bug Fixes 247 | 248 | - add aria-disabled to disabled elements ([9eb22d2](https://github.com/calebdwilliams/element-internals-polyfill/commit/9eb22d216f80caac4f81eea678761f96103a2e97)) 249 | 250 | ### [1.1.4](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.3...v1.1.4) (2022-05-04) 251 | 252 | ### Bug Fixes 253 | 254 | - fix error message ([f216022](https://github.com/calebdwilliams/element-internals-polyfill/commit/f2160227361bd1aaec2685c2a2700d14a6374753)) 255 | - fix form initialization ([6cd1b6d](https://github.com/calebdwilliams/element-internals-polyfill/commit/6cd1b6dfa80d3db6a8892b5b1685fd5fa9e9f423)) 256 | - fix types for labels ([574ff66](https://github.com/calebdwilliams/element-internals-polyfill/commit/574ff669c9da98b0debaa999d052ea21a85da1c6)) 257 | - fix typo in variable name ([d2552e7](https://github.com/calebdwilliams/element-internals-polyfill/commit/d2552e74e2a8dc97b33c78d3025a399a2b2be318)) 258 | 259 | ### [1.1.3](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.2...v1.1.3) (2022-04-23) 260 | 261 | ### Bug Fixes 262 | 263 | - attach observers to this if window.ShadyDOM exists ([4bb51bb](https://github.com/calebdwilliams/element-internals-polyfill/commit/4bb51bb2a2660514d1c10b66256520d70f603746)) 264 | 265 | ### [1.1.2](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.1...v1.1.2) (2022-03-24) 266 | 267 | ### [1.1.1](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.1.0...v1.1.1) (2022-03-24) 268 | 269 | ## [1.1.0](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.0.4...v1.1.0) (2022-03-24) 270 | 271 | ### Features 272 | 273 | - removing most polyfilled features due to Firefox support, adding only CustomStateSet to Firefox ([b4b0dee](https://github.com/calebdwilliams/element-internals-polyfill/commit/b4b0dee2ee3c7516551891544a809735dabd3198)) 274 | 275 | ### [1.0.4](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.0.3...v1.0.4) (2022-03-24) 276 | 277 | ### Bug Fixes 278 | 279 | - fixed setFormValidity ([2f58bbb](https://github.com/calebdwilliams/element-internals-polyfill/commit/2f58bbbd8b2afb3c173d7b7fee3be3d2ccf772a4)) 280 | 281 | ### [1.0.3](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.0.2...v1.0.3) (2022-02-27) 282 | 283 | ### Bug Fixes 284 | 285 | - readOnly changes checkValidity ([14b0698](https://github.com/calebdwilliams/element-internals-polyfill/commit/14b06985a40345d52c4647f12c696dc73c30c903)) 286 | 287 | ### [1.0.2](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.0.1...v1.0.2) (2022-02-22) 288 | 289 | ### Bug Fixes 290 | 291 | - polyfill now respects willValidate for check and report validity ([366c43c](https://github.com/calebdwilliams/element-internals-polyfill/commit/366c43cb43e6ad947466267f75be3ce1ab652a1a)) 292 | 293 | ### [1.0.1](https://github.com/calebdwilliams/element-internals-polyfill/compare/v1.0.0...v1.0.1) (2022-02-22) 294 | 295 | ## [1.0.0](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.55...v1.0.0) (2022-02-21) 296 | 297 | ### [0.1.55](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.54...v0.1.55) (2022-02-21) 298 | 299 | ### Features 300 | 301 | - reflect custom states as shadow parts in addition to attributes ([641cac2](https://github.com/calebdwilliams/element-internals-polyfill/commit/641cac268d3b4371fcfee2bb9ee09c152486e5ed)), closes [#62](https://github.com/calebdwilliams/element-internals-polyfill/issues/62) 302 | 303 | ### [0.1.54](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.53...v0.1.54) (2022-01-27) 304 | 305 | ### Bug Fixes 306 | 307 | - fix global declaration ([078ae5e](https://github.com/calebdwilliams/element-internals-polyfill/commit/078ae5ea3ef2179da3fa98d0cfafb47e1b26c4cb)) 308 | 309 | ### [0.1.53](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.52...v0.1.53) (2022-01-26) 310 | 311 | ### Bug Fixes 312 | 313 | - **polyfill:** change attachInternals to not be a getter ([b249f36](https://github.com/calebdwilliams/element-internals-polyfill/commit/b249f362d7c2dd1863d8fc43fc61084d36fb5bc2)) 314 | 315 | ### [0.1.52](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.51...v0.1.52) (2022-01-18) 316 | 317 | ### Bug Fixes 318 | 319 | - **findParentForm:** fixed inconsistency with how Chrome finds forms outside of closed custom elements ([ec7c394](https://github.com/calebdwilliams/element-internals-polyfill/commit/ec7c394cf73efe954a3f007b5f96bebc8184fbe9)) 320 | 321 | ### [0.1.51](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.50...v0.1.51) (2021-12-23) 322 | 323 | ### Bug Fixes 324 | 325 | - respect novalidate ([ff33b37](https://github.com/calebdwilliams/element-internals-polyfill/commit/ff33b37f957e85a34ab4009da79201e9203b296b)) 326 | 327 | ### [0.1.50](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.49...v0.1.50) (2021-12-23) 328 | 329 | ### Bug Fixes 330 | 331 | - update form validity check algorithm to not call checkValidity on all changes ([97c3567](https://github.com/calebdwilliams/element-internals-polyfill/commit/97c356715bc21492c021fd88d746339f49b662cd)) 332 | 333 | ### [0.1.49](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.48...v0.1.49) (2021-12-07) 334 | 335 | ### [0.1.48](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.47...v0.1.48) (2021-12-06) 336 | 337 | ### [0.1.47](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.46...v0.1.47) (2021-11-26) 338 | 339 | ### Bug Fixes 340 | 341 | - add ariaSetSize to AomMixin ([c11fcae](https://github.com/calebdwilliams/element-internals-polyfill/commit/c11fcae78a31c142bb876e63d0c7fbd3db9bde9b)) 342 | - correct typescript@^4.5.0 types ([68d41a4](https://github.com/calebdwilliams/element-internals-polyfill/commit/68d41a42244bd424aff62a34b5fb2c39fcbe677d)) 343 | - fix reconcileValidity spelling error ([2d2ca18](https://github.com/calebdwilliams/element-internals-polyfill/commit/2d2ca189e1b8ac722eb1fae995bddf465f776e97)) 344 | - update project to work with built-in ElementInternals types ([392703b](https://github.com/calebdwilliams/element-internals-polyfill/commit/392703b510ec808ff0ef059506edbb6acf2324f7)) 345 | 346 | ### [0.1.46](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.45...v0.1.46) (2021-10-11) 347 | 348 | ### Bug Fixes 349 | 350 | - add role to aom ([16a1f6c](https://github.com/calebdwilliams/element-internals-polyfill/commit/16a1f6c49d8666276a6b8ef15ec4712b46215853)) 351 | 352 | ### [0.1.45](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.44...v0.1.45) (2021-10-11) 353 | 354 | ### Bug Fixes 355 | 356 | - Improve feature detection to support Firefox 93 ([#46](https://github.com/calebdwilliams/element-internals-polyfill/issues/46)) ([850e835](https://github.com/calebdwilliams/element-internals-polyfill/commit/850e8354d7a10fd561ed2aead52c82faf9167249)) 357 | 358 | ### [0.1.44](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.43...v0.1.44) (2021-09-01) 359 | 360 | ### Bug Fixes 361 | 362 | - add internals-disabled attribute and test ([30b5b41](https://github.com/calebdwilliams/element-internals-polyfill/commit/30b5b4114c7e7f572d3565be9ae2350d46470e7d)) 363 | 364 | ### [0.1.43](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.42...v0.1.43) (2021-07-30) 365 | 366 | ### Bug Fixes 367 | 368 | - polyfill manages onsubmit attributes set before form is initialized ([0e333a2](https://github.com/calebdwilliams/element-internals-polyfill/commit/0e333a27c8b7acd383ecf513a598f51b9eef7534)) 369 | 370 | ### [0.1.42](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.41...v0.1.42) (2021-07-25) 371 | 372 | ### Bug Fixes 373 | 374 | - remove lit-specific code which is unneeded after a previous fix ([d819cef](https://github.com/calebdwilliams/element-internals-polyfill/commit/d819cefe76aa883be2bad44b630bcad2ff5af10c)) 375 | 376 | ### [0.1.41](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.40...v0.1.41) (2021-07-25) 377 | 378 | ### Bug Fixes 379 | 380 | - ensure form has elements before trying to process data ([65a51a2](https://github.com/calebdwilliams/element-internals-polyfill/commit/65a51a2230d2c22cb7970134da2eca35ae8b60d0)) 381 | 382 | ### [0.1.40](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.39...v0.1.40) (2021-06-28) 383 | 384 | ### Bug Fixes 385 | 386 | - fix typing discrepency ([4db7c23](https://github.com/calebdwilliams/element-internals-polyfill/commit/4db7c2345329453683973f8d64a0f25579e8b65b)) 387 | 388 | ### [0.1.39](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.38...v0.1.39) (2021-06-07) 389 | 390 | ### [0.1.38](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.37...v0.1.38) (2021-06-07) 391 | 392 | ### Bug Fixes 393 | 394 | - add attribute filter to disabled observer ([329a620](https://github.com/calebdwilliams/element-internals-polyfill/commit/329a6204186e8d916dd94302eadeed2d3f265ad7)) 395 | 396 | ### [0.1.37](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.36...v0.1.37) (2021-05-27) 397 | 398 | ### Bug Fixes 399 | 400 | - add states to interface ([f800756](https://github.com/calebdwilliams/element-internals-polyfill/commit/f800756a84de16d3dae08b291bee917642e04598)) 401 | 402 | ### [0.1.36](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.35...v0.1.36) (2021-05-23) 403 | 404 | ### Bug Fixes 405 | 406 | - **CustomStateSet:** added to window and will throw illegal constructor by default ([73821d8](https://github.com/calebdwilliams/element-internals-polyfill/commit/73821d8ca05d577a855f2621d75b6893f09aa068)) 407 | 408 | ### [0.1.35](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.34...v0.1.35) (2021-05-23) 409 | 410 | ### Features 411 | 412 | - add CustomStateSet ([2e4c1ad](https://github.com/calebdwilliams/element-internals-polyfill/commit/2e4c1adcdb7d2456fb66e1a552923ee096d36c54)) 413 | 414 | ### [0.1.34](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.33...v0.1.34) (2021-05-08) 415 | 416 | ### Bug Fixes 417 | 418 | - form-associated elements inserted from DocumentFragment now upgrade properly ([cc6e690](https://github.com/calebdwilliams/element-internals-polyfill/commit/cc6e690f173b30e9933c4f68143d9b9e1153ab8d)) 419 | 420 | ### [0.1.33](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.32...v0.1.33) (2021-05-07) 421 | 422 | ### Bug Fixes 423 | 424 | - **polyfill:** polyfill now updates form checkValidity and reportValidity methods ([9830aba](https://github.com/calebdwilliams/element-internals-polyfill/commit/9830aba1862c5939414d99796493ed71dbbfa299)) 425 | 426 | ### [0.1.32](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.31...v0.1.32) (2021-05-01) 427 | 428 | ### [0.1.31](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.30...v0.1.31) (2021-05-01) 429 | 430 | ### [0.1.30](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.29...v0.1.30) (2021-04-06) 431 | 432 | ### Bug Fixes 433 | 434 | - fix types ([7c90479](https://github.com/calebdwilliams/element-internals-polyfill/commit/7c904797c5f04c32bf243587621737a7c464388a)) 435 | 436 | ### [0.1.29](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.25...v0.1.29) (2021-03-16) 437 | 438 | ### Bug Fixes 439 | 440 | - update ICustomElement types ([511e55b](https://github.com/calebdwilliams/element-internals-polyfill/commit/511e55bd35fa23ab3bc5fb2ea19ab5b615f82b90)) 441 | - update types ([a44d744](https://github.com/calebdwilliams/element-internals-polyfill/commit/a44d7441316e10bfcc6c32ca90c730442e0bf34e)) 442 | - **types:** update types ([b0f8860](https://github.com/calebdwilliams/element-internals-polyfill/commit/b0f88609fc6061b64f83c5b68505c06856aad5cb)) 443 | 444 | ### [0.1.28](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.27...v0.1.28) (2021-03-16) 445 | 446 | ### Bug Fixes 447 | 448 | - update types ([c2e8fe3](https://github.com/calebdwilliams/element-internals-polyfill/commit/c2e8fe39fc78bbda144ab410f222d84a6d63a5a0)) 449 | 450 | ### [0.1.27](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.26...v0.1.27) (2021-03-15) 451 | 452 | ### Bug Fixes 453 | 454 | - **types:** update types ([060814d](https://github.com/calebdwilliams/element-internals-polyfill/commit/060814d19cbcb10f979c3d36eb0d343680145b94)) 455 | 456 | ### [0.1.26](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.24...v0.1.26) (2021-03-12) 457 | 458 | ### [0.1.25](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.24...v0.1.25) (2021-03-12) 459 | 460 | ### [0.1.24](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.23...v0.1.24) (2021-03-12) 461 | 462 | ### [0.1.23](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.22...v0.1.23) (2021-03-12) 463 | 464 | ### [0.1.22](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.21...v0.1.22) (2021-03-12) 465 | 466 | ### [0.1.21](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.20...v0.1.21) (2021-03-12) 467 | 468 | ### [0.1.20](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.19...v0.1.20) (2021-03-12) 469 | 470 | ### [0.1.19](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.18...v0.1.19) (2021-03-12) 471 | 472 | ### [0.1.18](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.17...v0.1.18) (2021-03-12) 473 | 474 | ### [0.1.17](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.16...v0.1.17) (2021-03-12) 475 | 476 | ### [0.1.16](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.15...v0.1.16) (2021-03-12) 477 | 478 | ### [0.1.15](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.9...v0.1.15) (2021-03-12) 479 | 480 | ### Bug Fixes 481 | 482 | - add hidden input for lit-element after render completes ([#27](https://github.com/calebdwilliams/element-internals-polyfill/issues/27)) ([ebbb8ef](https://github.com/calebdwilliams/element-internals-polyfill/commit/ebbb8efe15fc923e4828091973739a7a8df1ed0a)) 483 | 484 | ### [0.1.14](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.13...v0.1.14) (2021-03-08) 485 | 486 | ### [0.1.13](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.12...v0.1.13) (2021-03-08) 487 | 488 | ### [0.1.12](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.11...v0.1.12) (2021-03-08) 489 | 490 | ### [0.1.11](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.10...v0.1.11) (2021-03-08) 491 | 492 | ### [0.1.10](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.9...v0.1.10) (2021-03-08) 493 | 494 | ### Bug Fixes 495 | 496 | - add hidden input for lit-element after render completes ([0b93a0a](https://github.com/calebdwilliams/element-internals-polyfill/commit/0b93a0a94158ca83caa82a1b9bd1a3381ca7b322)) 497 | 498 | ### [0.1.9](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.8...v0.1.9) (2021-02-23) 499 | 500 | ### Features 501 | 502 | - add error messages ([1d7c716](https://github.com/calebdwilliams/element-internals-polyfill/commit/1d7c716f5be9b0c9734ae0935e2ea60e34a5983c)) 503 | 504 | ### Bug Fixes 505 | 506 | - anchor element focus order on form submission reversed compared to Chrome ([#24](https://github.com/calebdwilliams/element-internals-polyfill/issues/24)) ([8346b2f](https://github.com/calebdwilliams/element-internals-polyfill/commit/8346b2fd357bd22b1f5ef725ad0a4de18db3a97c)) 507 | - **setValidity:** Added support for setting ValidityState from a native input as Chrome allows this ([#25](https://github.com/calebdwilliams/element-internals-polyfill/issues/25)) ([dbb3c1a](https://github.com/calebdwilliams/element-internals-polyfill/commit/dbb3c1a742044b04a5e81c9855fb9c775831114e)) 508 | 509 | ### [0.1.8](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.7...v0.1.8) (2021-02-19) 510 | 511 | ### Bug Fixes 512 | 513 | - **setFormValue:** accept empty strings like Chrome ([#23](https://github.com/calebdwilliams/element-internals-polyfill/issues/23)) ([8f41892](https://github.com/calebdwilliams/element-internals-polyfill/commit/8f41892f6de7c494be880bcf193870a3c519ee5c)) 514 | 515 | ### [0.1.7](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.6...v0.1.7) (2021-02-11) 516 | 517 | ### Bug Fixes 518 | 519 | - polyfill accepts non-string values like Chrome ([9ac948e](https://github.com/calebdwilliams/element-internals-polyfill/commit/9ac948e78a6ee7403f0d3515c5c8181b34bc366e)) 520 | 521 | ### [0.1.6](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.5...v0.1.6) (2021-02-10) 522 | 523 | ### Bug Fixes 524 | 525 | - **formSubmitCallback:** elements now report validity when a form is submitted ([01b2499](https://github.com/calebdwilliams/element-internals-polyfill/commit/01b249956f3874f54e3b59db5184c0012ee8c324)) 526 | 527 | ### [0.1.5](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.4...v0.1.5) (2021-02-10) 528 | 529 | ### Features 530 | 531 | - **validation anchor:** add validation anchor behavior ([4cc4631](https://github.com/calebdwilliams/element-internals-polyfill/commit/4cc4631cf0f97d5f57554c555689457bb227b4bf)) 532 | 533 | ### [0.1.4](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.3...v0.1.4) (2021-02-09) 534 | 535 | ### Features 536 | 537 | - add shadowRoot to internals ([2477eb2](https://github.com/calebdwilliams/element-internals-polyfill/commit/2477eb2e9a244eedae54809b270fd0d18d58f88f)) 538 | 539 | ### [0.1.3](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.2...v0.1.3) (2021-02-09) 540 | 541 | ### Features 542 | 543 | - **setFormValue:** Add support for FormData to allow settings multiple form values ([a0b2890](https://github.com/calebdwilliams/element-internals-polyfill/commit/a0b2890365ccbc8d2c79073bdf32f1679e60aa0f)) 544 | 545 | ### [0.1.2](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.1...v0.1.2) (2021-02-06) 546 | 547 | ### Bug Fixes 548 | 549 | - **formAssociatedCallback:** make sure internals are available when formAssociatedCallback is called ([df9effa](https://github.com/calebdwilliams/element-internals-polyfill/commit/df9effa3d01c4b88a1de5ece45acdddc7df4e983)) 550 | - **setValidity:** added missing anchor in method signature. ([0290325](https://github.com/calebdwilliams/element-internals-polyfill/commit/029032515dc1ba3ea250fb140f9df3eb67cbb476)) 551 | 552 | ### [0.1.1](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.1.0...v0.1.1) (2020-11-18) 553 | 554 | ### Bug Fixes 555 | 556 | - remove console.log() ([b7292b5](https://github.com/calebdwilliams/element-internals-polyfill/commit/b7292b52ee2baaf14a43436c41153c4f9179cbb1)) 557 | 558 | ## [0.1.0](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.23...v0.1.0) (2020-10-15) 559 | 560 | ### ⚠ BREAKING CHANGES 561 | 562 | - update how input references are added to forms 563 | 564 | ### Features 565 | 566 | - add hidden inputs back to polyfill ([9898822](https://github.com/calebdwilliams/element-internals-polyfill/commit/989882234578c26f69e00fb270ae1d5c38690cd4)) 567 | 568 | ### [0.0.23](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.21...v0.0.23) (2020-10-14) 569 | 570 | ### [0.0.22](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.21...v0.0.22) (2020-07-09) 571 | 572 | ### [0.0.21](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.20...v0.0.21) (2020-06-02) 573 | 574 | ### Bug Fixes 575 | 576 | - **polyfill:** correct labels reference ([9d326d1](https://github.com/calebdwilliams/element-internals-polyfill/commit/9d326d10b6696061408c3f0cfefbe3b6a063fc32)) 577 | 578 | ### [0.0.20](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.19...v0.0.20) (2020-06-02) 579 | 580 | ### [0.0.19](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.18...v0.0.19) (2020-06-02) 581 | 582 | ### Bug Fixes 583 | 584 | - **polyfill:** small improvements and refactor of tests ([846f19d](https://github.com/calebdwilliams/element-internals-polyfill/commit/846f19d0dc03c0aa5d1da6d1f54464bfdd269e3d)) 585 | 586 | ### [0.0.18](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.17...v0.0.18) (2020-05-27) 587 | 588 | ### Bug Fixes 589 | 590 | - **polyfill:** now uses correct event names for validity events ([a2fb8d6](https://github.com/calebdwilliams/element-internals-polyfill/commit/a2fb8d6239995bdff170c63446d41b9421324223)) 591 | 592 | ### [0.0.17](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.16...v0.0.17) (2020-05-27) 593 | 594 | ### Bug Fixes 595 | 596 | - **polyfill:** setValidity no longer calls reportValidity ([52d51b6](https://github.com/calebdwilliams/element-internals-polyfill/commit/52d51b6df2d0ad48c9e120dd4e686bb02616d424)) 597 | 598 | ### [0.0.16](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.15...v0.0.16) (2020-05-19) 599 | 600 | ### Features 601 | 602 | - **polyfill:** element's register self with forms under the prop [el.name] ([c01d1c7](https://github.com/calebdwilliams/element-internals-polyfill/commit/c01d1c71c2acbc95fcad07604795fae02c05aae4)) 603 | 604 | ### [0.0.15](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.14...v0.0.15) (2020-04-28) 605 | 606 | ### Features 607 | 608 | - **polyfill:** add basic aom support ([a2696c2](https://github.com/calebdwilliams/element-internals-polyfill/commit/a2696c2147237849ce165f42411c34adb924169c)) 609 | 610 | ### [0.0.14](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.13...v0.0.14) (2020-04-27) 611 | 612 | ### [0.0.13](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.12...v0.0.13) (2020-04-27) 613 | 614 | ### [0.0.12](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.11...v0.0.12) (2020-04-27) 615 | 616 | ### Bug Fixes 617 | 618 | - **polyfill:** fix typo ([df0b2e6](https://github.com/calebdwilliams/element-internals-polyfill/commit/df0b2e67a0bc2674076b96f7bf3dda191ba385eb)) 619 | 620 | ### [0.0.11](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.10...v0.0.11) (2020-04-27) 621 | 622 | ### [0.0.10](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.9...v0.0.10) (2020-04-27) 623 | 624 | ### Features 625 | 626 | - **remove hidden input:** work to remove hidden input ([bc1345f](https://github.com/calebdwilliams/element-internals-polyfill/commit/bc1345f2b64b7c034fe1514d1f16f6761e76c3c1)) 627 | 628 | ### [0.0.9](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.8...v0.0.9) (2020-04-26) 629 | 630 | ### Bug Fixes 631 | 632 | - **polyfill:** change aria-descrbedby to aria-labelledby ([3cc5cd3](https://github.com/calebdwilliams/element-internals-polyfill/commit/3cc5cd31af66e298f9c8ffdb8a6c5fcf747162d2)) 633 | 634 | ### [0.0.8](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.7...v0.0.8) (2020-04-26) 635 | 636 | ### Features 637 | 638 | - **polyfill:** add support for formAssociatedCallback ([7988907](https://github.com/calebdwilliams/element-internals-polyfill/commit/79889071474d843803bf5dd1087b8c1fb7ddc934)) 639 | 640 | ### [0.0.7](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.6...v0.0.7) (2020-04-16) 641 | 642 | ### Features 643 | 644 | - **polyfill:** now fires invalid and valid events ([1e05937](https://github.com/calebdwilliams/element-internals-polyfill/commit/1e0593702c6c8a56cda40a6936d654d91b1176ce)) 645 | 646 | ### [0.0.6](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.5...v0.0.6) (2020-04-14) 647 | 648 | ### Bug Fixes 649 | 650 | - **polyfill:** fix setValidity ([ee18f41](https://github.com/calebdwilliams/element-internals-polyfill/commit/ee18f4118b2d54ea482f6c7cdb4ae3ccb6ca6830)) 651 | 652 | ### [0.0.5](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.4...v0.0.5) (2019-11-16) 653 | 654 | ### Bug Fixes 655 | 656 | - **structure:** Fix file structure ([5f4cad4](https://github.com/calebdwilliams/element-internals-polyfill/commit/5f4cad44a32cd7f023a69334fe416c14a47de9e2)) 657 | 658 | ### [0.0.4](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.1...v0.0.4) (2019-11-16) 659 | 660 | ### Bug Fixes 661 | 662 | - **polyfill:** Include built files in npm ([ca45513](https://github.com/calebdwilliams/element-internals-polyfill/commit/ca455135e622d6682e489959a79e0aa12dc249ff)) 663 | - **structure:** Fix file structure ([9c4bff2](https://github.com/calebdwilliams/element-internals-polyfill/commit/9c4bff2f0db6253afd39fea20830756482eae595)) 664 | 665 | ### [0.0.3](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.2...v0.0.3) (2019-11-15) 666 | 667 | ### [0.0.2](https://github.com/calebdwilliams/element-internals-polyfill/compare/v0.0.1...v0.0.2) (2019-11-15) 668 | 669 | ### Bug Fixes 670 | 671 | - **polyfill:** Include built files in npm ([ca45513](https://github.com/calebdwilliams/element-internals-polyfill/commit/ca455135e622d6682e489959a79e0aa12dc249ff)) 672 | 673 | ### 0.0.1 (2019-11-15) 674 | 675 | ### Features 676 | 677 | - **polyfill:** Initial commit ([385cb42](https://github.com/calebdwilliams/element-internals-polyfill/commit/385cb427acd5c21946adaf5b9e47bcdfb761d5ab)) 678 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2021 Caleb Williams 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Element Internals Polyfill 2 | 3 | This package is a polyfill for the [`ElementInternals` standard](https://html.spec.whatwg.org/multipage/custom-elements.html#the-elementinternals-interface). The specification is supported by current releases of Chromium and Firefox. 4 | 5 | ## Use case 6 | 7 | The primary use case for `ElementInternals` right now is allowing custom elements full participation in HTML forms. To do this, it provides any element designated as `formAssociated` access to a handful of utilities. 8 | 9 | The `ElementInternals` API also offers users access to increased utilities for accessibility by exposing the [Accessibility Object Model](https://wicg.github.io/aom/explainer.html) to the element. 10 | 11 | ## Installation 12 | 13 | This package is available on `npm` under the name `element-internals-polyfill` 14 | and can be installed with [npm](https://docs.npmjs.com/getting-started), 15 | [yarn](https://yarnpkg.com/en/docs/getting-started), [unpkg](https://unpkg.com) 16 | or however else you consume dependencies. 17 | 18 | ### Example commands: 19 | 20 | npm: 21 | ```bash 22 | npm i element-internals-polyfill 23 | ``` 24 | 25 | yarn: 26 | ```bash 27 | yarn add element-internals-polyfill 28 | ``` 29 | 30 | skypack: 31 | ```javascript 32 | import 'https://cdn.skypack.dev/element-internals-polyfill'; 33 | ``` 34 | 35 | unpkg: 36 | ```javascript 37 | import 'https://unpkg.com/element-internals-polyfill'; 38 | ``` 39 | 40 | ## How it works 41 | 42 | To do this, add the `static get formAssociated` to a custom element and call the `attachInternals` method to return a new instance of the `ElementInternals` interface: 43 | 44 | ```javascript 45 | class MyElement extends HTMLElement { 46 | constructor() { 47 | super(); 48 | this._internals = this.attachInternals(); 49 | } 50 | } 51 | ``` 52 | 53 | This works by doing several things under the hood. First, there is a feature check for the `ElementInternals` object on the window. If that does not exist, the polyfill wires up a global [`MutationObserver`](https://developer.mozilla.org/en/docs/Web/API/MutationObserver) on the document to watch for additions to the DOM that the polyfill might need. 54 | 55 | It also monkey-patches `HTMLElement.prototype.attachShadow` to wire up a similar listener on any created shadow roots and to remove the watcher if the shadow root is removed. 56 | 57 | The polyfill will also monkey-patch `window.FormData` to attach any custom elements to that feature as well. 58 | 59 | The currently-supported features of the polyfill are: 60 | 61 | ### Form-associated custom elements 62 | 63 | To create a form-associated custom element using `ElementInternals`, the element's class must have a static `formAssociated` member that returns `true`. 64 | 65 | ```javascript 66 | class MyFormControl extends HTMLElement { 67 | static get formAssociated() { 68 | return true; 69 | } 70 | 71 | constructor() { 72 | super(); 73 | this.internals = this.attachInternals(); 74 | } 75 | } 76 | ``` 77 | 78 | In the above example, the form control will now have access to several unique APIs for participating in a form: 79 | 80 | - Labels will be wired up properly as they would with any built-in input. The polyfill achieves this by applying an `aria-labelledby` attribute to the host element and referencing any labels with a `for` attribute corresponding to the host's `id`. A reference to these labels can be found under `this.internals.labels`. 81 | - The internals interface will have access to the host element's form if one exists under `this.internals.form`. 82 | - If the element has a name, a refernce to the host element will be saved on the form object. 83 | 84 | In addition to the above the `ElementInternals` prototype has access to several form-specific methods including: 85 | 86 | - `checkValidity`: Will return the validity state of the form control. 87 | - `reportValidity`: Will trigger an `invalid` event if the form control is invalid. For the polyfill this method will not trigger the `validationMessage` to show to the user, that is a task left to the consumer. 88 | - `setFormValue`: Sets the form control's value on the form. This value will be attached to the form's `FormData` method. 89 | - `setValidity`: Takes two arguments, the first being a partial validity object that will update the control's validity object and the second being an optional validation message (required if the form is invalid). If this object is missing the method will throw an error. If the first argument is an object literal the form will be marked as valid. 90 | - `validationMessage`: The element's validation message as set by callse to `ElementInternals.prototype.setValidity`. 91 | - `validity`: The validity controller which is identical to the interface of `HTMLInputElement.prototype.validity`. 92 | - `willValidate`: Will be `true` if the control is set to participate in a form. 93 | 94 | ### Accessibility controls 95 | 96 | In addition to form controls, `ElementInternals` will also surface several accessibility methods for any element with internals attached. A list of supported properties (and their associated attributes) follows: 97 | 98 | - `ariaAtomic`: 'aria-atomic' 99 | - `ariaAutoComplete`: 'aria-autocomplete' 100 | - `ariaBusy`: 'aria-busy' 101 | - `ariaChecked`: 'aria-checked' 102 | - `ariaColCount`: 'aria-colcount' 103 | - `ariaConIndex`: 'aria-colindex' 104 | - `ariaColSpan`: 'aria-colspan' 105 | - `ariaCurrent`: 'aria-current' 106 | - `ariaDisabled`: 'aria-disabled' 107 | - `ariaExpanded`: 'aria-expanded' 108 | - `ariaHasPopup`: 'aria-haspopup' 109 | - `ariaHidden`: 'aria-hidden' 110 | - `ariaKeyShortcuts`: 'aria-keyshortcuts' 111 | - `ariaLabel`: 'aria-label' 112 | - `ariaLevel`: 'aria-level' 113 | - `ariaLive`: 'aria-live' 114 | - `ariaModal`: 'aria-modal' 115 | - `ariaMultiLine`: 'aria-multiline' 116 | - `ariaMultiSelectable`: 'aria-multiselectable' 117 | - `ariaOrientation`: 'aria-orientation' 118 | - `ariaPlaceholder`: 'aria-placeholder' 119 | - `ariaPosInSet`: 'aria-posinset' 120 | - `ariaPressed`: 'aria-pressed' 121 | - `ariaReadOnly`: 'aria-readonly' 122 | - `ariaRelevant`: 'aria-relevant' 123 | - `ariaRequired`: 'aria-required' 124 | - `ariaRoleDescription`: 'aria-roledescription' 125 | - `ariaRowCount`: 'aria-rowcount' 126 | - `ariaRowIndex`: 'aria-rowindex' 127 | - `ariaRowSpan`: 'aria-rowspan' 128 | - `ariaSelected`: 'aria-selected' 129 | - `ariaSort`: 'aria-sort' 130 | - `ariaValueMax`: 'aria-valuemax' 131 | - `ariaValueMin`: 'aria-valuemin' 132 | - `ariaValueNow`: 'aria-valuenow' 133 | - `ariaValueText`: 'aria-valuetext' 134 | 135 | For example, if you are creating a control that has a checked property, you will likely could set the `internals.ariaChecked` property to `'true'`. In polyfilled browsers, this will result in adding `aria-checked="true"` to the host's attributes. In fully-supported browsers, this attribute will not be reflected although the checked property will be reflected in the accessibility object model. 136 | 137 | ```javascript 138 | class CheckedControl extends HTMLElement { 139 | #checked = false; 140 | #internals = this.attachInternals(); 141 | 142 | get checked() { 143 | return this.#checked; 144 | } 145 | 146 | set checked(isChecked) { 147 | this.#checked = isChecked; 148 | this.#internals.ariaChecked = isChecked.toString(); 149 | } 150 | } 151 | ``` 152 | 153 | ### State API 154 | 155 | `ElementInternals` exposes an API for creating custom states on an element. For instance if a developer wanted to signify to users that an element was in state `foo`, they could call `internals.states.add('--foo')`. This would make the element match the selector `:--foo`. Unfortunately in non-supporting browsers this is an invalid selector and will throw an error in JS and would cause the parsing of a CSS rule to fail. As a result, this polyfill will add states using the `state--foo` attribute to the host element, as well as a `state--foo` shadow part in supporting browsers. 156 | 157 | In order to properly select these elements in CSS, you will need to duplicate your rule as follows: 158 | 159 | ```css 160 | /** Supporting browsers */ 161 | :--foo { 162 | color: rebeccapurple; 163 | } 164 | 165 | /** Polyfilled browsers */ 166 | [state--foo] { 167 | color: rebeccapurple; 168 | } 169 | ``` 170 | 171 | The shadow part allows matching the custom state from _outside_ a shadow tree, with similarly duplicated rules: 172 | 173 | ```css 174 | /** Supporting browsers */ 175 | ::part(bar):--foo { 176 | color: rebeccapurple; 177 | } 178 | 179 | /** Polyfilled browsers (that however support shadow parts) */ 180 | ::part(bar state--foo) { 181 | color: rebeccapurple; 182 | } 183 | ``` 184 | 185 | Trying to combine selectors like `:--foo, [state--foo]` will cause the parsing of the rule to fail because `:--foo` is an invalid selector. As a potential optimization, you can use CSS `@supports` as follows: 186 | 187 | ```css 188 | @supports selector(:--foo) { 189 | /** Native supporting code here */ 190 | } 191 | 192 | @supports not selector([state--foo]) { 193 | /** Code for polyfilled browsers here */ 194 | } 195 | ``` 196 | 197 | Be sure to understand how your supported browsers work with CSS `@supports` before using the above strategy. 198 | 199 | ## Current limitations 200 | 201 | - Right now providing a cross-browser compliant version of `ElementInternals.reportValidity` is not supported. The method essentially behaves as a proxy for `ElementInternals.checkValidity`. 202 | - The polyfill does support the outcomes of the [Accessibility Object Model](https://wicg.github.io/aom/explainer.html#) for applying accessibility rules on the DOM object. However, the spec states that updates using AOM will not be reflected by DOM attributes, but only on the element's accesibility object. However, to emulate this behavior before it is fully supported, it is necessary to use the attributes. If you choose to use this feature, please note that behavior in polyfilled browsers and non-polyfilled browsers will be different; however, the outcome for users will be the same. 203 | - It is currently impossible to set form states to `:invalid` and `:valid` so this polyfill replaces those with the `[internals-invalid]` and `[internals-valid]` attributes on the host element. The proper selector for invalid elements will be `:host(:invalid), :host([internals-invalid])`. 204 | - Exposing custom states as shadow parts means that any element using custom states in a shadow tree can be matched using `::part(state--foo)` in polyfilled browsers, even if the author didn't intend to expose it. This was deemed an acceptable trade-off, compared to tracking explicitly exposed elements using a mutation observer. 205 | 206 | ## A note about versioning 207 | 208 | This packages doesn't necessarily follow semantic versioning. As the spec is still under consideration and implementation by browser vendors, the features supported by this package will change (generally following Chrome's implementation). 209 | -------------------------------------------------------------------------------- /jsdom-tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test document in JSDOM 5 | 6 | 7 | 8 | 9 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /jsdom-tests/jsdom.test.js: -------------------------------------------------------------------------------- 1 | import { JSDOM } from "jsdom"; 2 | import { readFileSync } from "fs"; 3 | 4 | const polyfillContents = readFileSync("./jsdom-tests/polyfill.js", "utf-8"); 5 | 6 | function test(title, condition) { 7 | if (!condition) { 8 | throw new Error(`${title} failed with error`); 9 | } else { 10 | console.log(`${title} passed in JSDOM`); 11 | } 12 | } 13 | 14 | JSDOM.fromFile("./jsdom-tests/index.html", { 15 | runScripts: "dangerously", 16 | }).then(async ({ window }) => { 17 | const document = window._document; 18 | 19 | const polyfill = document.createElement("script"); 20 | polyfill.textContent = polyfillContents; 21 | 22 | document.body.append(polyfill); 23 | 24 | const form = document.createElement("form"); 25 | const testElement = document.createElement("test-element"); 26 | testElement.setAttribute("name", "test"); 27 | 28 | form.append(testElement); 29 | document.body.append(form); 30 | 31 | setImmediate(async () => { 32 | test( 33 | "ElementInternals is defined on the window", 34 | typeof window.ElementInternals !== "undefined" 35 | ); 36 | test( 37 | "Polyfilled ElementInternals.prototype.form is working", 38 | testElement.internals.form === form 39 | ); 40 | test( 41 | "Polyfilled form attachment is working", 42 | new window.FormData(form).get("test") === "foo" 43 | ); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "element-internals-polyfill", 3 | "version": "3.0.1", 4 | "description": "A polyfill for the element internals specification", 5 | "main": "dist/index.js", 6 | "module": "dist/index.js", 7 | "type": "module", 8 | "files": [ 9 | "dist" 10 | ], 11 | "types": "dist/index.d.ts", 12 | "scripts": { 13 | "changeset": "changeset", 14 | "pretest": "npm run build", 15 | "test": "web-test-runner test/*.test.* --node-resolve --playwright --browsers chromium firefox webkit && npm run test:jsdom", 16 | "test:watch": "npm run test -- --watch", 17 | "test:coverage": "npm run test -- --coverage", 18 | "test:jsdom": "node ./jsdom-tests/jsdom.test.js", 19 | "pretest:jsdom": "rollup -i dist/index.js -o jsdom-tests/polyfill.js -f iife -n polyfill --context window", 20 | "start": "rollup -c --watch --environment BUILD:dev", 21 | "build": "tsc", 22 | "prerelease": "npm run build", 23 | "release": "standard-version", 24 | "postrelease": "git push --follow-tags origin master; npm publish", 25 | "release:alpha": "npm run release -- --prerelease alpha" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/calebdwilliams/element-internals-polyfill.git" 30 | }, 31 | "keywords": [ 32 | "elementinternals", 33 | "internals", 34 | "element", 35 | "internals", 36 | "formassociated", 37 | "customelements", 38 | "web", 39 | "components", 40 | "forms", 41 | "polyfill" 42 | ], 43 | "author": "Caleb D. Williams ", 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/calebdwilliams/element-internals-polyfill/issues" 47 | }, 48 | "homepage": "https://github.com/calebdwilliams/element-internals-polyfill#readme", 49 | "devDependencies": { 50 | "@changesets/cli": "^2.27.8", 51 | "@lit-labs/testing": "^0.2.7", 52 | "@open-wc/testing": "^3.1.7", 53 | "@open-wc/testing-helpers": "^1.7.1", 54 | "@playwright/test": "^1.45.0", 55 | "@rollup/plugin-node-resolve": "^7.1.3", 56 | "@rollup/plugin-typescript": "^6.0.0", 57 | "@types/mocha": "^10.0.1", 58 | "@web/dev-server-esbuild": "^0.3.3", 59 | "@web/test-runner": "^0.15.0", 60 | "@web/test-runner-playwright": "^0.9.0", 61 | "deepmerge": "^4.2.2", 62 | "jsdom": "^26.0.0", 63 | "karma": "^6.3.3", 64 | "karma-chrome-launcher": "^3.1.0", 65 | "karma-coverage-istanbul-reporter": "^2.1.1", 66 | "karma-detect-browsers": "^2.3.3", 67 | "karma-edge-launcher": "^0.4.2", 68 | "karma-firefox-launcher": "^1.3.0", 69 | "karma-ie-launcher": "^1.0.0", 70 | "karma-rollup-preprocessor": "^7.0.5", 71 | "karma-safari-launcher": "^1.0.0", 72 | "karma-safarinative-launcher": "^1.1.0", 73 | "lit": "^3.3.0", 74 | "lit-html": "^1.2.1", 75 | "rollup": "^2.46.0", 76 | "rollup-plugin-cleanup": "^3.2.1", 77 | "rollup-plugin-commonjs": "^10.1.0", 78 | "rollup-plugin-livereload": "^1.2.0", 79 | "rollup-plugin-serve": "^1.0.1", 80 | "sinon": "^9.2.4", 81 | "standard-version": "^9.0.0", 82 | "tslib": "^2.1.0", 83 | "typescript": "^5.5.4" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import serve from 'rollup-plugin-serve'; 3 | import livereload from 'rollup-plugin-livereload'; 4 | import cleanup from 'rollup-plugin-cleanup'; 5 | 6 | const plugins = [typescript({ 7 | target: 'es2015', 8 | rootDir: 'src' 9 | })]; 10 | const config = { 11 | input: 'src/index.ts', 12 | output: { 13 | format: 'iife', 14 | dir: 'dist' 15 | }, 16 | plugins 17 | }; 18 | 19 | if (process.env.BUILD === 'dev') { 20 | plugins.push([ 21 | serve({ 22 | open: true, 23 | verbose: true, 24 | contentBase: ['static', 'dist'], 25 | historyApiFallback: true, 26 | port: 8181, 27 | }) 28 | ]); 29 | 30 | plugins.push( 31 | livereload({ 32 | watch: 'dist' 33 | }) 34 | ); 35 | } else { 36 | plugins.push(cleanup({ 37 | extensions: [ '.ts', '.js' ] 38 | })) 39 | } 40 | 41 | export default config; 42 | -------------------------------------------------------------------------------- /src/CustomStateSet.ts: -------------------------------------------------------------------------------- 1 | /** Save a reference to the ref for the CustomStateSet */ 2 | const customStateMap = new WeakMap(); 3 | 4 | function addState(ref: HTMLElement, stateName: string): void { 5 | ref.toggleAttribute(stateName, true); 6 | if (ref.part) { 7 | ref.part.add(stateName); 8 | } 9 | } 10 | 11 | export type CustomState = `--${string}` | string; 12 | 13 | export class CustomStateSet extends Set { 14 | static get isPolyfilled() { 15 | return true; 16 | } 17 | 18 | constructor(ref: HTMLElement) { 19 | super(); 20 | if (!ref || !ref.tagName || ref.tagName.indexOf("-") === -1) { 21 | throw new TypeError("Illegal constructor"); 22 | } 23 | 24 | customStateMap.set(this, ref); 25 | } 26 | 27 | add(state: CustomState) { 28 | if (!/^--/.test(state) || typeof state !== "string") { 29 | throw new DOMException( 30 | `Failed to execute 'add' on 'CustomStateSet': The specified value ${state} must start with '--'.` 31 | ); 32 | } 33 | const result = super.add(state); 34 | const ref = customStateMap.get(this); 35 | const stateName = `state${state}`; 36 | 37 | /** 38 | * Only add the state immediately if the ref is connected to the DOM; 39 | * otherwise, wait a tick because the element is likely being constructed 40 | * by document.createElement and would throw otherwise. 41 | */ 42 | if (ref.isConnected) { 43 | addState(ref, stateName); 44 | } else { 45 | setTimeout(() => { 46 | addState(ref, stateName); 47 | }); 48 | } 49 | 50 | return result; 51 | } 52 | 53 | clear() { 54 | for (let [entry] of this.entries()) { 55 | this.delete(entry); 56 | } 57 | super.clear(); 58 | } 59 | 60 | delete(state: CustomState) { 61 | const result = super.delete(state); 62 | const ref = customStateMap.get(this); 63 | 64 | /** 65 | * Only toggle the state/attr immediately if the ref is connected to the DOM; 66 | * otherwise, wait a tick because the element is likely being constructed 67 | * by document.createElement and would throw otherwise. 68 | */ 69 | if (ref.isConnected) { 70 | ref.toggleAttribute(`state${state}`, false); 71 | if (ref.part) { 72 | ref.part.remove(`state${state}`); 73 | } 74 | } else { 75 | setTimeout(() => { 76 | ref.toggleAttribute(`state${state}`, false); 77 | if (ref.part) { 78 | ref.part.remove(`state${state}`); 79 | } 80 | }); 81 | } 82 | 83 | return result; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/HTMLFormControlsCollection.ts: -------------------------------------------------------------------------------- 1 | export class HTMLFormControlsCollection implements HTMLFormControlsCollection { 2 | readonly #elements; 3 | 4 | constructor(elements) { 5 | this.#elements = elements; 6 | 7 | for (let i = 0; i < elements.length; i++) { 8 | let element = elements[i]; 9 | 10 | this[i] = element; 11 | if (element.hasAttribute('name')) { 12 | this[element.getAttribute('name')] = element; 13 | } 14 | } 15 | 16 | Object.freeze(this); 17 | } 18 | 19 | [index: number]: Element; 20 | 21 | get length(): number { 22 | return this.#elements.length; 23 | } 24 | 25 | [Symbol.iterator]() { 26 | return this.#elements[Symbol.iterator](); 27 | } 28 | 29 | item(i): Element { 30 | return this[i] == null ? null : this[i]; 31 | } 32 | 33 | namedItem(name): Element { 34 | return this[name] == null ? null : this[name]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ValidityState.ts: -------------------------------------------------------------------------------- 1 | import { setFormValidity } from './utils.js'; 2 | 3 | /** Emulate the browser's default ValidityState object */ 4 | export class ValidityState implements ValidityState { 5 | badInput = false; 6 | customError = false; 7 | patternMismatch = false; 8 | rangeOverflow = false; 9 | rangeUnderflow = false; 10 | stepMismatch = false; 11 | tooLong = false; 12 | tooShort = false; 13 | typeMismatch = false; 14 | valid = true; 15 | valueMissing = false; 16 | 17 | constructor() { 18 | Object.seal(this); 19 | } 20 | } 21 | 22 | /** 23 | * Reset a ValidityState object back to valid 24 | * @param {ValidityState} validityObject - The object to modify 25 | * @return {ValidityState} - The modified ValidityStateObject 26 | */ 27 | export const setValid = (validityObject: ValidityState): ValidityState => { 28 | validityObject.badInput = false; 29 | validityObject.customError = false; 30 | validityObject.patternMismatch = false; 31 | validityObject.rangeOverflow = false; 32 | validityObject.rangeUnderflow = false; 33 | validityObject.stepMismatch = false; 34 | validityObject.tooLong = false; 35 | validityObject.tooShort = false; 36 | validityObject.typeMismatch = false; 37 | validityObject.valid = true; 38 | validityObject.valueMissing = false; 39 | return validityObject; 40 | }; 41 | 42 | /** 43 | * Reconcile a ValidityState object with a new state object 44 | * @param {ValidityState} - The base object to reconcile with new state 45 | * @param {Object} - A partial ValidityState object to override the original 46 | * @return {ValidityState} - The updated ValidityState object 47 | */ 48 | export const reconcileValidity = (validityObject: ValidityState, newState: Partial, form: HTMLFormElement): ValidityState => { 49 | validityObject.valid = isValid(newState); 50 | Object.keys(newState).forEach(key => validityObject[key] = newState[key]); 51 | if (form) { 52 | setFormValidity(form); 53 | } 54 | return validityObject; 55 | }; 56 | 57 | /** 58 | * Check if a partial ValidityState object should be valid 59 | * @param {Object} - A partial ValidityState object 60 | * @return {Boolean} - Should the new object be valid 61 | */ 62 | export const isValid = (validityState: Partial): boolean => { 63 | let valid = true; 64 | for (let key in validityState) { 65 | if (key !== 'valid' && validityState[key] !== false) { 66 | valid = false; 67 | } 68 | } 69 | return valid; 70 | }; 71 | -------------------------------------------------------------------------------- /src/aom.ts: -------------------------------------------------------------------------------- 1 | import { upgradeMap } from "./maps.js"; 2 | import { setAttribute } from "./utils.js"; 3 | import "./types.js"; 4 | 5 | export const aom: Record = { 6 | ariaAtomic: "aria-atomic", 7 | ariaAutoComplete: "aria-autocomplete", 8 | ariaBrailleLabel: "aria-braillelabel", 9 | ariaBrailleRoleDescription: "aria-brailleroledescription", 10 | ariaBusy: "aria-busy", 11 | ariaChecked: "aria-checked", 12 | ariaColCount: "aria-colcount", 13 | ariaColIndex: "aria-colindex", 14 | ariaColIndexText: "aria-colindextext", 15 | ariaColSpan: "aria-colspan", 16 | ariaCurrent: "aria-current", 17 | ariaDescription: "aria-description", 18 | ariaDisabled: "aria-disabled", 19 | ariaExpanded: "aria-expanded", 20 | ariaHasPopup: "aria-haspopup", 21 | ariaHidden: "aria-hidden", 22 | ariaInvalid: "aria-invalid", 23 | ariaKeyShortcuts: "aria-keyshortcuts", 24 | ariaLabel: "aria-label", 25 | ariaLevel: "aria-level", 26 | ariaLive: "aria-live", 27 | ariaModal: "aria-modal", 28 | ariaMultiLine: "aria-multiline", 29 | ariaMultiSelectable: "aria-multiselectable", 30 | ariaOrientation: "aria-orientation", 31 | ariaPlaceholder: "aria-placeholder", 32 | ariaPosInSet: "aria-posinset", 33 | ariaPressed: "aria-pressed", 34 | ariaReadOnly: "aria-readonly", 35 | ariaRelevant: "aria-relevant", 36 | ariaRequired: "aria-required", 37 | ariaRoleDescription: "aria-roledescription", 38 | ariaRowCount: "aria-rowcount", 39 | ariaRowIndex: "aria-rowindex", 40 | ariaRowIndexText: "aria-rowindextext", 41 | ariaRowSpan: "aria-rowspan", 42 | ariaSelected: "aria-selected", 43 | ariaSetSize: "aria-setsize", 44 | ariaSort: "aria-sort", 45 | ariaValueMax: "aria-valuemax", 46 | ariaValueMin: "aria-valuemin", 47 | ariaValueNow: "aria-valuenow", 48 | ariaValueText: "aria-valuetext", 49 | role: "role", 50 | }; 51 | 52 | export const initAom = ( 53 | ref: FormAssociatedCustomElement, 54 | internals: ElementInternals 55 | ) => { 56 | for (let key in aom) { 57 | internals[key] = null; 58 | 59 | let closureValue = null; 60 | const attributeName = aom[key]; 61 | Object.defineProperty(internals, key, { 62 | get() { 63 | return closureValue; 64 | }, 65 | set(value) { 66 | closureValue = value; 67 | if (ref.isConnected) { 68 | setAttribute(ref, attributeName, value); 69 | } else { 70 | upgradeMap.set(ref, internals); 71 | } 72 | }, 73 | }); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /src/element-internals.ts: -------------------------------------------------------------------------------- 1 | import { 2 | connectedCallbackMap, 3 | internalsMap, 4 | refMap, 5 | refValueMap, 6 | shadowHostsMap, 7 | shadowRootMap, 8 | validationAnchorMap, 9 | validityMap, 10 | validationMessageMap, 11 | validityUpgradeMap, 12 | } from "./maps.js"; 13 | import { 14 | setAttribute, 15 | createHiddenInput, 16 | findParentForm, 17 | mutationObserverExists, 18 | removeHiddenInputs, 19 | setDisabled, 20 | throwIfNotFormAssociated, 21 | upgradeInternals, 22 | } from "./utils.js"; 23 | import { 24 | initRef, 25 | } from "./mutation-observers.js"; 26 | import { initAom } from "./aom.js"; 27 | import { ValidityState, reconcileValidity, setValid } from "./ValidityState.js"; 28 | import { 29 | deferUpgrade, 30 | observerCallback, 31 | observerConfig, 32 | } from "./mutation-observers.js"; 33 | import { LabelsList } from "./types.js"; 34 | import { CustomStateSet } from "./CustomStateSet.js"; 35 | import { patchFormPrototype } from "./patch-form-prototype.js"; 36 | 37 | export class ElementInternals implements globalThis.ElementInternals { 38 | ariaAtomic: string; 39 | ariaAutoComplete: string; 40 | ariaBrailleLabel: string; 41 | ariaBrailleRoleDescription: string; 42 | ariaBusy: string; 43 | ariaChecked: string; 44 | ariaColCount: string; 45 | ariaColIndex: string; 46 | ariaColIndexText: string; 47 | ariaColSpan: string; 48 | ariaCurrent: string; 49 | ariaDescription: string; 50 | ariaDisabled: string; 51 | ariaExpanded: string; 52 | ariaHasPopup: string; 53 | ariaHidden: string; 54 | ariaInvalid: string; 55 | ariaKeyShortcuts: string; 56 | ariaLabel: string; 57 | ariaLevel: string; 58 | ariaLive: string; 59 | ariaModal: string; 60 | ariaMultiLine: string; 61 | ariaMultiSelectable: string; 62 | ariaOrientation: string; 63 | ariaPlaceholder: string; 64 | ariaPosInSet: string; 65 | ariaPressed: string; 66 | ariaReadOnly: string; 67 | ariaRelevant: string; 68 | ariaRequired: string; 69 | ariaRoleDescription: string; 70 | ariaRowCount: string; 71 | ariaRowIndex: string; 72 | ariaRowIndexText: string; 73 | ariaRowSpan: string; 74 | ariaSelected: string; 75 | ariaSetSize: string; 76 | ariaSort: string; 77 | ariaValueMax: string; 78 | ariaValueMin: string; 79 | ariaValueNow: string; 80 | ariaValueText: string; 81 | role: string; 82 | 83 | states: CustomStateSet; 84 | 85 | static get isPolyfilled() { 86 | return true; 87 | } 88 | 89 | constructor(ref: FormAssociatedCustomElement) { 90 | if (!ref || !ref.tagName || ref.tagName.indexOf("-") === -1) { 91 | throw new TypeError("Illegal constructor"); 92 | } 93 | const rootNode = ref.getRootNode(); 94 | const validity = new ValidityState(); 95 | this.states = new CustomStateSet(ref); 96 | refMap.set(this, ref); 97 | validityMap.set(this, validity); 98 | internalsMap.set(ref, this); 99 | initAom(ref, this); 100 | initRef(ref, this); 101 | Object.seal(this); 102 | 103 | /** 104 | * If appended from a DocumentFragment, wait until it is connected 105 | * before attempting to upgrade the internals instance 106 | */ 107 | if (rootNode instanceof DocumentFragment) { 108 | deferUpgrade(rootNode); 109 | } 110 | } 111 | 112 | /** 113 | * Will return true if the element is in a valid state 114 | */ 115 | checkValidity(): boolean { 116 | const ref = refMap.get(this); 117 | throwIfNotFormAssociated( 118 | ref, 119 | `Failed to execute 'checkValidity' on 'ElementInternals': The target element is not a form-associated custom element.` 120 | ); 121 | /** If the element will not validate, it is necessarily valid by default */ 122 | if (!this.willValidate) { 123 | return true; 124 | } 125 | const validity = validityMap.get(this); 126 | if (!validity.valid) { 127 | const validityEvent = new Event("invalid", { 128 | bubbles: false, 129 | cancelable: true, 130 | composed: false, 131 | }); 132 | ref.dispatchEvent(validityEvent); 133 | } 134 | return validity.valid; 135 | } 136 | 137 | /** The form element the custom element is associated with */ 138 | get form(): HTMLFormElement { 139 | const ref = refMap.get(this); 140 | throwIfNotFormAssociated( 141 | ref, 142 | `Failed to read the 'form' property from 'ElementInternals': The target element is not a form-associated custom element.` 143 | ); 144 | let form; 145 | if (ref.constructor["formAssociated"] === true) { 146 | form = findParentForm(ref); 147 | } 148 | return form; 149 | } 150 | 151 | /** A list of all relative form labels for this element */ 152 | get labels(): LabelsList { 153 | const ref = refMap.get(this); 154 | throwIfNotFormAssociated( 155 | ref, 156 | `Failed to read the 'labels' property from 'ElementInternals': The target element is not a form-associated custom element.` 157 | ); 158 | const id = ref.getAttribute("id"); 159 | const hostRoot = ref.getRootNode() as Element; 160 | if (hostRoot && id) { 161 | return hostRoot.querySelectorAll( 162 | `[for="${id}"]` 163 | ) as unknown as LabelsList; 164 | } 165 | return [] as unknown as LabelsList; 166 | } 167 | 168 | /** Will report the elements validity state */ 169 | reportValidity(): boolean { 170 | const ref = refMap.get(this); 171 | throwIfNotFormAssociated( 172 | ref, 173 | `Failed to execute 'reportValidity' on 'ElementInternals': The target element is not a form-associated custom element.` 174 | ); 175 | /** If the element will not validate, it is valid by default */ 176 | if (!this.willValidate) { 177 | return true; 178 | } 179 | const valid = this.checkValidity(); 180 | const anchor = validationAnchorMap.get(this); 181 | if (anchor && !ref.constructor["formAssociated"]) { 182 | throw new DOMException( 183 | `Failed to execute 'reportValidity' on 'ElementInternals': The target element is not a form-associated custom element.` 184 | ); 185 | } 186 | if (!valid && anchor) { 187 | ref.focus(); 188 | anchor.focus(); 189 | } 190 | return valid; 191 | } 192 | 193 | /** Sets the element's value within the form */ 194 | setFormValue(value: string | FormData | null): void { 195 | const ref = refMap.get(this); 196 | throwIfNotFormAssociated( 197 | ref, 198 | `Failed to execute 'setFormValue' on 'ElementInternals': The target element is not a form-associated custom element.` 199 | ); 200 | removeHiddenInputs(this); 201 | if (value != null && !(value instanceof FormData)) { 202 | if (ref.getAttribute("name")) { 203 | const hiddenInput = createHiddenInput(ref, this); 204 | hiddenInput.value = value; 205 | } 206 | } else if (value != null && value instanceof FormData) { 207 | Array.from(value) 208 | .reverse() 209 | .forEach(([formDataKey, formDataValue]) => { 210 | if (typeof formDataValue === "string") { 211 | const hiddenInput = createHiddenInput(ref, this); 212 | hiddenInput.name = formDataKey; 213 | hiddenInput.value = formDataValue; 214 | } 215 | }); 216 | } 217 | refValueMap.set(ref, value); 218 | } 219 | 220 | /** 221 | * Sets the element's validity. The first argument is a partial ValidityState object 222 | * reflecting the changes to be made to the element's validity. If the element is invalid, 223 | * the second argument sets the element's validation message. 224 | * 225 | * If the field is valid and a message is specified, the method will throw a TypeError. 226 | */ 227 | setValidity( 228 | validityChanges: Partial, 229 | validationMessage?: string, 230 | anchor?: HTMLElement 231 | ) { 232 | const ref = refMap.get(this); 233 | throwIfNotFormAssociated( 234 | ref, 235 | `Failed to execute 'setValidity' on 'ElementInternals': The target element is not a form-associated custom element.` 236 | ); 237 | if (!validityChanges) { 238 | throw new TypeError( 239 | "Failed to execute 'setValidity' on 'ElementInternals': 1 argument required, but only 0 present." 240 | ); 241 | } 242 | validationAnchorMap.set(this, anchor); 243 | const validity = validityMap.get(this); 244 | const validityChangesObj: Partial = {}; 245 | for (const key in validityChanges) { 246 | validityChangesObj[key] = validityChanges[key]; 247 | } 248 | if (Object.keys(validityChangesObj).length === 0) { 249 | setValid(validity); 250 | } 251 | const check = { ...validity, ...validityChangesObj }; 252 | delete check.valid; 253 | const { valid } = reconcileValidity(validity, check, this.form); 254 | 255 | if (!valid && !validationMessage) { 256 | throw new DOMException( 257 | `Failed to execute 'setValidity' on 'ElementInternals': The second argument should not be empty if one or more flags in the first argument are true.` 258 | ); 259 | } 260 | validationMessageMap.set(this, valid ? "" : validationMessage); 261 | 262 | // check to make sure the host element is connected before adding attributes 263 | // because safari doesnt allow elements to have attributes added in the constructor 264 | if (ref.isConnected) { 265 | ref.toggleAttribute("internals-invalid", !valid); 266 | ref.toggleAttribute("internals-valid", valid); 267 | setAttribute(ref, "aria-invalid", `${!valid}`); 268 | } else { 269 | validityUpgradeMap.set(ref, this); 270 | } 271 | } 272 | 273 | get shadowRoot(): ShadowRoot | null { 274 | const ref = refMap.get(this); 275 | const shadowRoot = shadowRootMap.get(ref); 276 | if (shadowRoot) { 277 | return shadowRoot; 278 | } 279 | return null; 280 | } 281 | 282 | /** The element's validation message set during a call to ElementInternals.setValidity */ 283 | get validationMessage(): string { 284 | const ref = refMap.get(this); 285 | throwIfNotFormAssociated( 286 | ref, 287 | `Failed to read the 'validationMessage' property from 'ElementInternals': The target element is not a form-associated custom element.` 288 | ); 289 | return validationMessageMap.get(this); 290 | } 291 | 292 | /** The current validity state of the object */ 293 | get validity(): ValidityState { 294 | const ref = refMap.get(this); 295 | throwIfNotFormAssociated( 296 | ref, 297 | `Failed to read the 'validity' property from 'ElementInternals': The target element is not a form-associated custom element.` 298 | ); 299 | const validity = validityMap.get(this); 300 | return validity; 301 | } 302 | 303 | /** If true the element will participate in a form's constraint validation. */ 304 | get willValidate(): boolean { 305 | const ref = refMap.get(this); 306 | throwIfNotFormAssociated( 307 | ref, 308 | `Failed to read the 'willValidate' property from 'ElementInternals': The target element is not a form-associated custom element.` 309 | ); 310 | if ( 311 | ref.matches(":disabled") || 312 | ref["disabled"] || 313 | ref.hasAttribute("disabled") || 314 | ref.hasAttribute("readonly") 315 | ) { 316 | return false; 317 | } 318 | return true; 319 | } 320 | } 321 | 322 | declare global { 323 | interface CustomElementConstructor { 324 | formAssociated?: boolean; 325 | } 326 | 327 | interface Window { 328 | ElementInternals: typeof ElementInternals; 329 | } 330 | } 331 | 332 | export function isElementInternalsSupported(): boolean { 333 | if ( 334 | typeof window === "undefined" || 335 | !window.ElementInternals || 336 | !HTMLElement.prototype.attachInternals 337 | ) { 338 | return false; 339 | } 340 | 341 | class ElementInternalsFeatureDetection extends HTMLElement { 342 | internals: ElementInternals; 343 | 344 | constructor() { 345 | super(); 346 | this.internals = this.attachInternals(); 347 | } 348 | } 349 | const randomName = `element-internals-feature-detection-${Math.random() 350 | .toString(36) 351 | .replace(/[^a-z]+/g, "")}`; 352 | customElements.define(randomName, ElementInternalsFeatureDetection); 353 | const featureDetectionElement = new ElementInternalsFeatureDetection(); 354 | return [ 355 | "shadowRoot", 356 | "form", 357 | "willValidate", 358 | "validity", 359 | "validationMessage", 360 | "labels", 361 | "setFormValue", 362 | "setValidity", 363 | "checkValidity", 364 | "reportValidity", 365 | ].every((prop) => prop in featureDetectionElement.internals); 366 | } 367 | 368 | let hasElementInternalsPolyfillBeenApplied = false; 369 | let hasCustomStateSetPolyfillBeenApplied = false; 370 | 371 | /** 372 | * Forcibly applies the polyfill for CustomStateSet. 373 | * 374 | * https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet 375 | */ 376 | export function forceCustomStateSetPolyfill( 377 | attachInternals?: HTMLElement["attachInternals"] 378 | ) { 379 | if (hasCustomStateSetPolyfillBeenApplied) { 380 | return; 381 | } 382 | 383 | hasCustomStateSetPolyfillBeenApplied = true; 384 | /** @ts-expect-error These types won't match because this is a polyfill */ 385 | window.CustomStateSet = CustomStateSet; 386 | 387 | if (attachInternals) { 388 | HTMLElement.prototype.attachInternals = function (...args) { 389 | const internals = attachInternals.call(this, args); 390 | internals.states = new CustomStateSet(this); 391 | return internals; 392 | }; 393 | } 394 | } 395 | 396 | /** 397 | * Forcibly applies the polyfill for ElementInternals. Useful for situations 398 | * like Chrome extensions where Chrome supports ElementInternals, but the 399 | * CustomElements polyfill is required. 400 | * 401 | * https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals 402 | * 403 | * @param forceCustomStateSet Optional: when true, forces the 404 | * [CustomStateSet](https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet) 405 | * polyfill as well. 406 | */ 407 | export function forceElementInternalsPolyfill(forceCustomStateSet = true) { 408 | /** 409 | * This is a flag to prevent a DOMException from being thrown when 410 | * attachInternals is called in upgradeInternals. 411 | */ 412 | let attachedFlag = false; 413 | if (hasElementInternalsPolyfillBeenApplied) { 414 | return; 415 | } 416 | 417 | hasElementInternalsPolyfillBeenApplied = true; 418 | 419 | if (typeof window !== "undefined") { 420 | /** @ts-expect-error: we need to replace the default ElementInternals */ 421 | window.ElementInternals = ElementInternals; 422 | } 423 | 424 | if (typeof CustomElementRegistry !== "undefined") { 425 | const define = CustomElementRegistry.prototype.define; 426 | CustomElementRegistry.prototype.define = function ( 427 | name, 428 | constructor, 429 | options 430 | ) { 431 | if (constructor.formAssociated) { 432 | const connectedCallback = constructor.prototype.connectedCallback; 433 | constructor.prototype.connectedCallback = function () { 434 | if (!connectedCallbackMap.has(this)) { 435 | connectedCallbackMap.set(this, true); 436 | 437 | if (this.hasAttribute("disabled")) { 438 | setDisabled(this, true); 439 | } 440 | } 441 | 442 | if (connectedCallback != null) { 443 | connectedCallback.apply(this); 444 | } 445 | // always upgradeInternals in connectedCallback instead of constructor 446 | attachedFlag = upgradeInternals(this); 447 | }; 448 | } 449 | 450 | define.call(this, name, constructor, options); 451 | }; 452 | } 453 | 454 | /** 455 | * Attaches an ElementInternals instance to a custom element. Calling this method 456 | * on a built-in element will throw an error. 457 | */ 458 | if (typeof HTMLElement !== "undefined") { 459 | HTMLElement.prototype.attachInternals = function (): ElementInternals { 460 | if (!this.tagName) { 461 | /** This happens in the LitSSR environment. Here we can generally ignore internals for now */ 462 | return {} as object as ElementInternals; 463 | } else if (this.tagName.indexOf("-") === -1) { 464 | throw new Error( 465 | `Failed to execute 'attachInternals' on 'HTMLElement': Unable to attach ElementInternals to non-custom elements.` 466 | ); 467 | } 468 | if (internalsMap.has(this) && !attachedFlag) { 469 | throw new DOMException( 470 | `DOMException: Failed to execute 'attachInternals' on 'HTMLElement': ElementInternals for the specified element was already attached.` 471 | ); 472 | } 473 | return new ElementInternals(this); 474 | }; 475 | } 476 | 477 | if (typeof Element !== "undefined") { 478 | function attachShadowObserver(...args) { 479 | const shadowRoot = attachShadow.apply(this, args); 480 | shadowRootMap.set(this, shadowRoot); 481 | 482 | if (mutationObserverExists()) { 483 | const observer = new MutationObserver(observerCallback); 484 | if (window.ShadyDOM) { 485 | observer.observe(this, observerConfig); 486 | } else { 487 | observer.observe(shadowRoot, observerConfig); 488 | } 489 | shadowHostsMap.set(this, observer); 490 | } 491 | return shadowRoot; 492 | } 493 | 494 | const attachShadow = Element.prototype.attachShadow; 495 | Element.prototype.attachShadow = attachShadowObserver; 496 | } 497 | 498 | if (mutationObserverExists() && typeof document !== "undefined") { 499 | const documentObserver = new MutationObserver(observerCallback); 500 | documentObserver.observe(document.documentElement, observerConfig); 501 | } 502 | 503 | /** 504 | * Keeps the polyfill from throwing in environments where HTMLFormElement 505 | * is undefined like in a server environment 506 | */ 507 | if (typeof HTMLFormElement !== "undefined") { 508 | patchFormPrototype(); 509 | } 510 | 511 | if ( 512 | forceCustomStateSet || 513 | (typeof window !== "undefined" && !window.CustomStateSet) 514 | ) { 515 | forceCustomStateSetPolyfill(); 516 | } 517 | } 518 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ElementInternals, 3 | forceCustomStateSetPolyfill, 4 | forceElementInternalsPolyfill, 5 | isElementInternalsSupported, 6 | } from "./element-internals.js"; 7 | import { CustomStateSet } from "./CustomStateSet.js"; 8 | import "./element-internals.js"; 9 | 10 | export * from "./types.js"; 11 | export { 12 | forceCustomStateSetPolyfill, 13 | forceElementInternalsPolyfill, 14 | } from "./element-internals.js"; 15 | 16 | declare global { 17 | interface Window { 18 | CustomStateSet: typeof CustomStateSet; 19 | ElementInternals: typeof ElementInternals; 20 | ShadyDOM: any; 21 | } 22 | interface HTMLElement { 23 | /** 24 | * Attaches an ElementInternals instance to a custom element. Calling this method 25 | * on a built-in element will throw an error. 26 | */ 27 | attachInternals(): ElementInternals; 28 | } 29 | } 30 | 31 | // Deteermine whether the webcomponents polyfill has been applied. 32 | const isCePolyfill = !!( 33 | customElements as unknown as { 34 | polyfillWrapFlushCallback: () => void; 35 | } 36 | ).polyfillWrapFlushCallback; 37 | 38 | // custom elements polyfill is on. Do not auto-apply. User should determine 39 | // whether to force or not. 40 | if (!isCePolyfill) { 41 | if (!isElementInternalsSupported()) { 42 | forceElementInternalsPolyfill(false); 43 | } else if (typeof window !== "undefined" && !window.CustomStateSet) { 44 | forceCustomStateSetPolyfill(HTMLElement.prototype.attachInternals); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/maps.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the WeakMaps used throughout this project. The WeakMaps exist to tie 3 | * objects together without polluting the objects themselves with references we'd rather keep 4 | * hidden. This allows the polyfill to work as transparently as possible. 5 | */ 6 | 7 | /** Use an ElementInternals instance to get a reference to the element it is attached to */ 8 | export const refMap = new WeakMap< 9 | ElementInternals, 10 | FormAssociatedCustomElement 11 | >(); 12 | 13 | /** Usee an ElementsInternals instance to get its ValidityState object */ 14 | export const validityMap = new WeakMap(); 15 | 16 | /** Use an ElementInternals instance to get its attached input[type="hidden"] */ 17 | export const hiddenInputMap = new WeakMap< 18 | ElementInternals, 19 | HTMLInputElement[] 20 | >(); 21 | 22 | /** Use a custom element to get its attached ElementInternals instance */ 23 | export const internalsMap = new WeakMap< 24 | FormAssociatedCustomElement, 25 | ElementInternals 26 | >(); 27 | 28 | /** Use an ElementInternals instance to get the attached validation message */ 29 | export const validationMessageMap = new WeakMap(); 30 | 31 | /** Use a form element to get attached custom elements and ElementInternals instances */ 32 | export const formsMap = new WeakMap(); 33 | 34 | /** Use a custom element or other object to get their associated MutationObservers */ 35 | export const shadowHostsMap = new WeakMap< 36 | FormAssociatedCustomElement, 37 | MutationObserver 38 | >(); 39 | 40 | /** Use a form element to get a set of attached custom elements */ 41 | export const formElementsMap = new WeakMap< 42 | HTMLFormElement, 43 | Set 44 | >(); 45 | 46 | /** Use an ElementInternals instance to get a reference to an element's value */ 47 | export const refValueMap = new WeakMap(); 48 | 49 | /** Elements that need to be upgraded once added to the DOM */ 50 | export const upgradeMap = new WeakMap(); 51 | 52 | /** Save references to shadow roots for inclusion in internals instance */ 53 | export const shadowRootMap = new WeakMap< 54 | FormAssociatedCustomElement, 55 | ShadowRoot 56 | >(); 57 | 58 | /** Save a reference to the internals' validation anchor */ 59 | export const validationAnchorMap = new WeakMap(); 60 | 61 | /** Map DocumentFragments to their MutationObservers so we can disconnect once elements are removed */ 62 | export const documentFragmentMap = new WeakMap< 63 | DocumentFragment, 64 | MutationObserver 65 | >(); 66 | 67 | /** Whether connectedCallback has already been called. */ 68 | export const connectedCallbackMap = new WeakMap< 69 | FormAssociatedCustomElement, 70 | boolean 71 | >(); 72 | 73 | /** Save a reference to validity state for elements that need to upgrade after being connected */ 74 | export const validityUpgradeMap = new WeakMap< 75 | FormAssociatedCustomElement, 76 | ElementInternals 77 | >(); 78 | -------------------------------------------------------------------------------- /src/mutation-observers.ts: -------------------------------------------------------------------------------- 1 | import { internalsMap, shadowHostsMap, upgradeMap, hiddenInputMap, documentFragmentMap, formElementsMap, validityUpgradeMap, refValueMap } from './maps.js'; 2 | import { aom } from './aom.js'; 3 | import { setAttribute, removeHiddenInputs, initForm, initLabels, upgradeInternals, setDisabled, mutationObserverExists } from './utils.js'; 4 | import { ICustomElement } from './types.js'; 5 | 6 | 7 | /** 8 | * Initialize a ref by setting up an attribute observe on it 9 | * looking for changes to disabled 10 | * @param {HTMLElement} ref - The element to watch 11 | * @param {ElementInternals} internals - The element internals instance for the ref 12 | * @return {void} 13 | */ 14 | export const initRef = ( 15 | ref: HTMLElement, 16 | internals: ElementInternals 17 | ): void => { 18 | hiddenInputMap.set(internals, []); 19 | disabledOrNameObserver.observe?.(ref, disabledOrNameObserverConfig); 20 | }; 21 | 22 | 23 | function initNode(node: ICustomElement): void { 24 | const internals = internalsMap.get(node); 25 | const { form } = internals; 26 | initForm(node, form, internals); 27 | initLabels(node, internals.labels); 28 | } 29 | 30 | /** 31 | * If a fieldset's disabled state is toggled, the formDisabledCallback 32 | * on any child form-associated cusotm elements. 33 | */ 34 | export const walkFieldset = (node: HTMLFieldSetElement, firstRender: boolean = false): void => { 35 | const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT, { 36 | acceptNode(node: ICustomElement): number { 37 | return internalsMap.has(node) ? 38 | NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; 39 | } 40 | }); 41 | 42 | let current = walker.nextNode() as ICustomElement; 43 | /** 44 | * We don't need to call anything on first render if 45 | * the element isn't disabled 46 | */ 47 | const isCallNecessary = (!firstRender || node.disabled) 48 | 49 | while (current) { 50 | if (current.formDisabledCallback && isCallNecessary) { 51 | setDisabled(current, node.disabled); 52 | } 53 | current = walker.nextNode() as ICustomElement; 54 | } 55 | }; 56 | 57 | export const disabledOrNameObserverConfig: MutationObserverInit = { attributes: true, attributeFilter: ['disabled', 'name'] }; 58 | 59 | export const disabledOrNameObserver = mutationObserverExists() ? new MutationObserver((mutationsList: MutationRecord[]) => { 60 | for (const mutation of mutationsList) { 61 | const target = mutation.target as ICustomElement; 62 | 63 | /** Manage changes to the ref's disabled state */ 64 | if (mutation.attributeName === 'disabled') { 65 | if (target.constructor['formAssociated']) { 66 | setDisabled(target, target.hasAttribute('disabled')); 67 | } else if (target.localName === 'fieldset') { 68 | /** 69 | * Repurpose the observer for fieldsets which need 70 | * to be walked whenever the disabled attribute is set 71 | */ 72 | walkFieldset(target as unknown as HTMLFieldSetElement); 73 | } 74 | } 75 | /** Manage changes to the ref's name */ 76 | if (mutation.attributeName === 'name') { 77 | if (target.constructor['formAssociated']) { 78 | const internals = internalsMap.get(target); 79 | const value = refValueMap.get(target); 80 | internals.setFormValue(value); 81 | } 82 | } 83 | } 84 | }) : {} as MutationObserver; 85 | 86 | export function observerCallback(mutationList: MutationRecord[]) { 87 | mutationList.forEach(mutationRecord => { 88 | const { addedNodes, removedNodes } = mutationRecord; 89 | const added = Array.from(addedNodes) as ICustomElement[]; 90 | const removed = Array.from(removedNodes) as ICustomElement[]; 91 | 92 | added.forEach(node => { 93 | /** Allows for dynamic addition of elements to forms */ 94 | if (internalsMap.has(node) && node.constructor['formAssociated']) { 95 | initNode(node); 96 | } 97 | 98 | 99 | /** Upgrade the accessibility information on any previously connected */ 100 | if (upgradeMap.has(node)) { 101 | const internals = upgradeMap.get(node); 102 | const aomKeys = Object.keys(aom); 103 | aomKeys 104 | .filter(key => internals[key] !== null) 105 | .forEach(key => { 106 | setAttribute(node, aom[key], internals[key]); 107 | }); 108 | upgradeMap.delete(node); 109 | } 110 | 111 | /** Upgrade the validity state when the element is connected */ 112 | if (validityUpgradeMap.has(node)) { 113 | const internals = validityUpgradeMap.get(node); 114 | setAttribute(node, 'internals-valid', internals.validity.valid.toString()); 115 | setAttribute(node, 'internals-invalid', (!internals.validity.valid).toString()); 116 | setAttribute(node, 'aria-invalid', (!internals.validity.valid).toString()); 117 | validityUpgradeMap.delete(node); 118 | } 119 | 120 | /** If the node that's added is a form, check the validity */ 121 | if (node.localName === 'form') { 122 | const formElements = formElementsMap.get(node as unknown as HTMLFormElement); 123 | const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT, { 124 | acceptNode(node: ICustomElement): number { 125 | return ( 126 | internalsMap.has(node) && node.constructor['formAssociated'] && !(formElements && formElements.has(node)) 127 | ) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; 128 | } 129 | }); 130 | 131 | let current = walker.nextNode() as ICustomElement; 132 | 133 | while (current) { 134 | initNode(current); 135 | current = walker.nextNode() as ICustomElement; 136 | } 137 | } 138 | 139 | if (node.localName === 'fieldset') { 140 | disabledOrNameObserver.observe?.(node, disabledOrNameObserverConfig); 141 | walkFieldset(node as unknown as HTMLFieldSetElement, true); 142 | } 143 | }); 144 | 145 | removed.forEach(node => { 146 | const internals = internalsMap.get(node); 147 | /** Clean up any hidden input elements left after an element is disconnected */ 148 | if (internals && hiddenInputMap.get(internals)) { 149 | removeHiddenInputs(internals); 150 | } 151 | /** Disconnect any unneeded MutationObservers */ 152 | if (shadowHostsMap.has(node)) { 153 | const observer = shadowHostsMap.get(node); 154 | observer.disconnect(); 155 | } 156 | }); 157 | }); 158 | } 159 | 160 | /** 161 | * This observer callback is just for document fragments 162 | * it will upgrade an ElementInternals instance if was appended 163 | * from a document fragment. 164 | */ 165 | export function fragmentObserverCallback(mutationList: MutationRecord[]): void { 166 | mutationList.forEach(mutation => { 167 | const { removedNodes } = mutation; 168 | 169 | removedNodes.forEach(node => { 170 | const observer = documentFragmentMap.get(mutation.target as DocumentFragment); 171 | if (internalsMap.has(node as ICustomElement)) { 172 | upgradeInternals(node as ICustomElement); 173 | } 174 | observer.disconnect(); 175 | }); 176 | }); 177 | } 178 | 179 | /** 180 | * Defer the upgrade of nodes withing a DocumentFragment 181 | * @param fragment {DocumentFragment} 182 | */ 183 | export const deferUpgrade = (fragment: DocumentFragment) => { 184 | const observer = new MutationObserver(fragmentObserverCallback); 185 | // is this using shady DOM and is not actually a DocumentFragment? 186 | if ( 187 | window?.ShadyDOM?.inUse && 188 | (fragment as unknown as { mode: string }).mode && 189 | (fragment as unknown as { host: HTMLElement | null }).host 190 | ) { 191 | // using shady DOM polyfill. Best to just observe the host. 192 | fragment = (fragment as ShadowRoot).host as unknown as DocumentFragment; 193 | } 194 | observer.observe?.(fragment, { childList: true }); 195 | documentFragmentMap.set(fragment, observer); 196 | }; 197 | 198 | export const observer = mutationObserverExists() ? new MutationObserver(observerCallback) : {} as MutationObserver; 199 | export const observerConfig: MutationObserverInit = { 200 | childList: true, 201 | subtree: true 202 | }; 203 | -------------------------------------------------------------------------------- /src/patch-form-prototype.ts: -------------------------------------------------------------------------------- 1 | import { HTMLFormControlsCollection } from './HTMLFormControlsCollection.js'; 2 | import { formElementsMap } from './maps.js'; 3 | import { overrideFormMethod } from './utils.js'; 4 | 5 | /** 6 | * Patch the HTMLElement prototype 7 | * 8 | * This function patches checkValidity, reportValidity and elements 9 | */ 10 | export function patchFormPrototype(): void { 11 | const checkValidity = HTMLFormElement.prototype.checkValidity; 12 | HTMLFormElement.prototype.checkValidity = checkValidityOverride; 13 | 14 | const reportValidity = HTMLFormElement.prototype.reportValidity; 15 | HTMLFormElement.prototype.reportValidity = reportValidityOverride; 16 | 17 | function checkValidityOverride(...args): boolean { 18 | let returnValue = checkValidity.apply(this, args); 19 | return overrideFormMethod(this, returnValue, 'checkValidity'); 20 | } 21 | 22 | function reportValidityOverride(...args): boolean { 23 | let returnValue = reportValidity.apply(this, args); 24 | return overrideFormMethod(this, returnValue, 'reportValidity'); 25 | } 26 | 27 | const { get } = Object.getOwnPropertyDescriptor(HTMLFormElement.prototype, 'elements'); 28 | Object.defineProperty(HTMLFormElement.prototype, 'elements', { 29 | get(...args) { 30 | const elements = get.call(this, ...args); 31 | const polyfilledElements = Array.from(formElementsMap.get(this) || []); 32 | 33 | // If there are no polyfilled elements, return the native elements collection 34 | if (polyfilledElements.length === 0) { 35 | return elements; 36 | } 37 | 38 | // Merge the native elements with the polyfilled elements 39 | // and order them by their position in the DOM 40 | const orderedElements = Array.from(elements).concat(polyfilledElements).sort((a: Element, b: Element) => { 41 | if (a.compareDocumentPosition) { 42 | return a.compareDocumentPosition(b) & 2 ? 1 : -1; 43 | } 44 | return 0; 45 | }); 46 | 47 | return new HTMLFormControlsCollection(orderedElements); 48 | }, 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { CustomState } from "./CustomStateSet.js"; 2 | import { ElementInternals } from "./element-internals.js"; 3 | 4 | declare global { 5 | interface ARIAMixin { 6 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaBrailleLabel) */ 7 | ariaBrailleLabel: string | null; 8 | 9 | /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaBrailleRoleDescription) */ 10 | ariaBrailleRoleDescription: string | null; 11 | 12 | /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaColIndexText) */ 13 | ariaColIndexText: string | null; 14 | 15 | /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaRelevant) */ 16 | ariaRelevant: string | null; 17 | 18 | /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaRowIndexText) */ 19 | ariaRowIndexText: string | null; 20 | } 21 | 22 | interface ElementInternals extends ARIAMixin { 23 | checkValidity: () => boolean; 24 | readonly form: HTMLFormElement | null; 25 | readonly labels: NodeList; 26 | reportValidity: () => boolean; 27 | setFormValue: ( 28 | value: File | string | FormData | null, 29 | state?: File | string | FormData | null 30 | ) => void; 31 | setValidity: ( 32 | flags?: ValidityStateFlags, 33 | message?: string, 34 | anchor?: HTMLElement 35 | ) => void; 36 | readonly shadowRoot: ShadowRoot | null; 37 | readonly states: CustomStateSet; 38 | readonly validationMessage: string; 39 | readonly validity: ValidityState; 40 | readonly willValidate: boolean; 41 | } 42 | 43 | interface FormAssociatedCustomElement extends HTMLElement { 44 | formDisabledCallback?: (isDisabled: boolean) => void; 45 | formResetCallback: () => void; 46 | formAssociatedCallback: (form: HTMLFormElement) => void; 47 | } 48 | 49 | interface CustomStateSet extends Set {} 50 | } 51 | 52 | export interface ICustomElement extends HTMLElement { 53 | constructor: (...args: any[]) => HTMLElement; 54 | attributeChangedCallback(name: string, oldValue: any, newValue: any): void; 55 | connectedCallback(): void; 56 | disconnectedCallback(): void; 57 | attachedCallback(): void; 58 | attachInternals(): ElementInternals; 59 | formDisabledCallback(isDisabled: boolean): void; 60 | formResetCallback(): void; 61 | formAssociatedCallback(form: HTMLFormElement): void; 62 | disabled?: boolean; 63 | } 64 | 65 | export type LabelsList = NodeListOf; 66 | 67 | declare global { 68 | interface HTMLElement { 69 | attachInternals(): ElementInternals; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | hiddenInputMap, 3 | formsMap, 4 | formElementsMap, 5 | internalsMap, 6 | } from "./maps.js"; 7 | import { 8 | disabledOrNameObserver, 9 | disabledOrNameObserverConfig, 10 | } from "./mutation-observers.js"; 11 | 12 | /** 13 | * Set attribute if its value differs from existing one. 14 | * 15 | * In comparison to other attribute modification methods (removeAttribute and 16 | * toggleAttribute), setAttribute always triggers attributeChangedCallback 17 | * even if the actual value has not changed. 18 | * 19 | * This polyfill relies heavily on attributes to pass aria information to 20 | * screen readers. This behaviour differs from native implementation which does 21 | * not change attributes. 22 | * 23 | * To limit this difference we only set attribute value when it is different 24 | * from the current state. 25 | * 26 | * @param {ICustomElement | Element} ref - The custom element instance 27 | * @param {string} name - The attribute name 28 | * @param {string} value - The attribute value 29 | * @returns 30 | */ 31 | export const setAttribute = ( 32 | ref: Element, 33 | name: string, 34 | value: string 35 | ): void => { 36 | if (ref.getAttribute(name) === value) { 37 | return; 38 | } 39 | ref.setAttribute(name, value); 40 | }; 41 | 42 | /** 43 | * Toggle's the disabled state (attributes & callback) on the given element 44 | * @param {HTMLElement} ref - The custom element instance 45 | * @param {boolean} disabled - The disabled state 46 | */ 47 | export const setDisabled = ( 48 | ref: FormAssociatedCustomElement, 49 | disabled: boolean 50 | ): void => { 51 | ref.toggleAttribute("internals-disabled", disabled); 52 | 53 | if (disabled) { 54 | setAttribute(ref, "aria-disabled", "true"); 55 | } else { 56 | ref.removeAttribute("aria-disabled"); 57 | } 58 | 59 | if (ref.formDisabledCallback) { 60 | ref.formDisabledCallback.apply(ref, [disabled]); 61 | } 62 | }; 63 | 64 | /** 65 | * Removes all hidden inputs for the given element internals instance 66 | * @param {ElementInternals} internals - The element internals instance 67 | * @return {void} 68 | */ 69 | export const removeHiddenInputs = (internals: ElementInternals): void => { 70 | const hiddenInputs = hiddenInputMap.get(internals); 71 | hiddenInputs.forEach((hiddenInput) => { 72 | hiddenInput.remove(); 73 | }); 74 | hiddenInputMap.set(internals, []); 75 | }; 76 | 77 | /** 78 | * Creates a hidden input for the given ref 79 | * @param {HTMLElement} ref - The element to watch 80 | * @param {ElementInternals} internals - The element internals instance for the ref 81 | * @return {HTMLInputElement} The hidden input 82 | */ 83 | export const createHiddenInput = ( 84 | ref: HTMLElement, 85 | internals: ElementInternals 86 | ): HTMLInputElement | null => { 87 | const input = document.createElement("input"); 88 | input.type = "hidden"; 89 | input.name = ref.getAttribute("name"); 90 | ref.after(input); 91 | hiddenInputMap.get(internals).push(input); 92 | return input; 93 | }; 94 | 95 | /** 96 | * Set up labels for the ref 97 | * @param {HTMLElement} ref - The ref to add labels to 98 | * @param {NodeList} labels - A list of the labels 99 | * @return {void} 100 | */ 101 | export const initLabels = (ref: HTMLElement, labels: NodeList): void => { 102 | if (labels.length) { 103 | const labelList = Array.from(labels) as HTMLLabelElement[]; 104 | labelList.forEach((label) => 105 | label.addEventListener("click", ref.click.bind(ref)) 106 | ); 107 | const [firstLabel] = labelList; 108 | let firstLabelId = firstLabel.id; 109 | if (!firstLabel.id) { 110 | firstLabelId = `${firstLabel.htmlFor}_Label`; 111 | firstLabel.id = firstLabelId; 112 | } 113 | setAttribute(ref, "aria-labelledby", firstLabelId); 114 | } 115 | }; 116 | 117 | /** 118 | * Sets the internals-valid and internals-invalid attributes 119 | * based on form validity. 120 | * @param {HTMLFormElement} - The target form 121 | * @return {void} 122 | */ 123 | export const setFormValidity = (form: HTMLFormElement) => { 124 | const nativeControlValidity = Array.from(form.elements) 125 | .filter( 126 | (element: Element & { validity: ValidityState }) => 127 | !element.tagName.includes("-") && element.validity 128 | ) 129 | .map( 130 | (element: Element & { validity: ValidityState }) => element.validity.valid 131 | ); 132 | const polyfilledElements = formElementsMap.get(form) || []; 133 | const polyfilledValidity = Array.from(polyfilledElements) 134 | .filter((control) => control.isConnected) 135 | .map( 136 | (control: HTMLElement) => 137 | internalsMap.get(control as FormAssociatedCustomElement).validity.valid 138 | ); 139 | const hasInvalid = [...nativeControlValidity, ...polyfilledValidity].includes( 140 | false 141 | ); 142 | form.toggleAttribute("internals-invalid", hasInvalid); 143 | form.toggleAttribute("internals-valid", !hasInvalid); 144 | }; 145 | 146 | /** 147 | * The global form input callback. Updates the form's validity 148 | * attributes on input. 149 | * @param {Event} - The form input event 150 | * @return {void} 151 | */ 152 | export const formInputCallback = (event: Event) => { 153 | setFormValidity(findParentForm(event.target)); 154 | }; 155 | 156 | /** 157 | * The global form change callback. Updates the form's validity 158 | * attributes on change. 159 | * @param {Event} - The form change event 160 | * @return {void} 161 | */ 162 | export const formChangeCallback = (event: Event) => { 163 | setFormValidity(findParentForm(event.target)); 164 | }; 165 | 166 | /** 167 | * The global form submit callback. We need to cancel any submission 168 | * if a nested internals is invalid. 169 | * @param {HTMLFormElement} - The form element 170 | * @return {void} 171 | */ 172 | export const wireSubmitLogic = (form: HTMLFormElement) => { 173 | const submitButtonSelector = [ 174 | "button[type=submit]", 175 | "input[type=submit]", 176 | "button:not([type])", 177 | ] 178 | .map((sel) => `${sel}:not([disabled])`) 179 | .map( 180 | (sel) => 181 | `${sel}:not([form])${form.id ? `,${sel}[form='${form.id}']` : ""}` 182 | ) 183 | .join(","); 184 | 185 | form.addEventListener("click", (event) => { 186 | const target = event.target as Element; 187 | if (target.closest(submitButtonSelector)) { 188 | // validate 189 | const elements = formElementsMap.get(form); 190 | 191 | /** 192 | * If this form does not validate then we're done 193 | */ 194 | if (form.noValidate) { 195 | return; 196 | } 197 | 198 | /** If the Set has items, continue */ 199 | if (elements.size) { 200 | const nodes = Array.from(elements); 201 | /** Check the internals.checkValidity() of all nodes */ 202 | const validityList = nodes.reverse().map((node) => { 203 | const internals = internalsMap.get(node); 204 | return internals.reportValidity(); 205 | }); 206 | 207 | /** If any node is false, stop the event */ 208 | if (validityList.includes(false)) { 209 | event.preventDefault(); 210 | } 211 | } 212 | } 213 | }); 214 | }; 215 | 216 | /** 217 | * The global form reset callback. This will loop over added 218 | * inputs and call formResetCallback if applicable 219 | * @return {void} 220 | */ 221 | export const formResetCallback = (event: Event) => { 222 | /** Get the Set of elements attached to this form */ 223 | const elements = formElementsMap.get(event.target as HTMLFormElement); 224 | 225 | /** Some forms won't contain form associated custom elements */ 226 | if (elements && elements.size) { 227 | /** Loop over the elements and call formResetCallback if applicable */ 228 | elements.forEach((element) => { 229 | if ( 230 | (element.constructor as any).formAssociated && 231 | element.formResetCallback 232 | ) { 233 | element.formResetCallback.apply(element); 234 | } 235 | }); 236 | } 237 | }; 238 | 239 | /** 240 | * Initialize the form. We will need to add submit and reset listeners 241 | * if they don't already exist. If they do, just add the new ref to the form. 242 | * @param {HTMLElement} ref - The element ref that includes internals 243 | * @param {HTMLFormElement} form - The form the ref belongs to 244 | * @param {ElementInternals} internals - The internals for ref 245 | * @return {void} 246 | */ 247 | export const initForm = ( 248 | ref: FormAssociatedCustomElement, 249 | form: HTMLFormElement, 250 | internals: ElementInternals 251 | ) => { 252 | if (form) { 253 | /** This will be a WeakMap */ 254 | const formElements = formElementsMap.get(form); 255 | 256 | if (formElements) { 257 | /** If formElements exists, add to it */ 258 | formElements.add(ref); 259 | } else { 260 | /** If formElements doesn't exist, create it and add to it */ 261 | const initSet = new Set(); 262 | initSet.add(ref); 263 | formElementsMap.set(form, initSet); 264 | 265 | /** Add listeners to emulate validation and reset behavior */ 266 | wireSubmitLogic(form); 267 | form.addEventListener("reset", formResetCallback); 268 | form.addEventListener("input", formInputCallback); 269 | form.addEventListener("change", formChangeCallback); 270 | } 271 | 272 | formsMap.set(form, { ref, internals }); 273 | 274 | /** Call formAssociatedCallback if applicable */ 275 | if (ref.constructor["formAssociated"] && ref.formAssociatedCallback) { 276 | setTimeout(() => { 277 | ref.formAssociatedCallback.apply(ref, [form]); 278 | }, 0); 279 | } 280 | setFormValidity(form); 281 | } 282 | }; 283 | 284 | /** 285 | * Recursively look for an element's parent form 286 | * @param {Element} elem - The element to look for a parent form 287 | * @return {HTMLFormElement|null} - The parent form, if one exists 288 | */ 289 | export const findParentForm = (elem) => { 290 | let parent = elem.parentNode; 291 | if (parent && parent.tagName !== "FORM") { 292 | parent = findParentForm(parent); 293 | } 294 | return parent; 295 | }; 296 | 297 | /** 298 | * Throw an error if the element ref is not form associated 299 | * @param ref {HTMLElement} - The element to check if it is form associated 300 | * @param message {string} - The error message to throw 301 | * @param ErrorType {any} - The error type to throw, defaults to DOMException 302 | */ 303 | export const throwIfNotFormAssociated = ( 304 | ref: HTMLElement, 305 | message: string, 306 | ErrorType: any = DOMException 307 | ): void => { 308 | if (!ref.constructor["formAssociated"]) { 309 | throw new ErrorType(message); 310 | } 311 | }; 312 | 313 | /** 314 | * Called for each HTMLFormElement.checkValidity|reportValidity 315 | * will loop over a form's added components and call the respective 316 | * method modifying the default return value if needed 317 | * @param form {HTMLFormElement} - The form element to run the method on 318 | * @param returnValue {boolean} - The initial result of the original method 319 | * @param method {'checkValidity'|'reportValidity'} - The original method 320 | * @returns {boolean} The form's validity state 321 | */ 322 | export const overrideFormMethod = ( 323 | form: HTMLFormElement, 324 | returnValue: boolean, 325 | method: "checkValidity" | "reportValidity" 326 | ): boolean => { 327 | const elements = formElementsMap.get(form); 328 | 329 | /** Some forms won't contain form associated custom elements */ 330 | if (elements && elements.size) { 331 | elements.forEach((element) => { 332 | const internals = internalsMap.get(element); 333 | const valid = internals[method](); 334 | if (!valid) { 335 | returnValue = false; 336 | } 337 | }); 338 | } 339 | return returnValue; 340 | }; 341 | 342 | /** 343 | * Will upgrade an ElementInternals instance by initializing the 344 | * instance's form and labels. This is called when the element is 345 | * either constructed or appended from a DocumentFragment 346 | * @param ref {HTMLElement} - The custom element to upgrade 347 | */ 348 | export const upgradeInternals = (ref: FormAssociatedCustomElement): boolean => { 349 | let attached = false; 350 | if (ref.constructor["formAssociated"]) { 351 | let internals = internalsMap.get(ref); 352 | // we might have cases where the internals are not set 353 | if (internals === undefined) { 354 | ref.attachInternals(); 355 | internals = internalsMap.get(ref); 356 | attached = true; 357 | } 358 | const { labels, form } = internals; 359 | initLabels(ref, labels); 360 | initForm(ref, form, internals); 361 | } 362 | return attached; 363 | }; 364 | 365 | /** 366 | * Check to see if MutationObserver exists in the current 367 | * execution context. Will likely return false on the server 368 | * @returns {boolean} 369 | */ 370 | export function mutationObserverExists(): boolean { 371 | return typeof MutationObserver !== "undefined"; 372 | } 373 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ElementInternals polyfill 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 |
24 |
25 | 26 |

Issue 31

27 |
28 | 29 |

Issue 30

30 |
31 | 32 |
33 | 34 | 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /static/safari.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Appended via tree test 5 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /static/scripts/address.js: -------------------------------------------------------------------------------- 1 | const addressSheet = new CSSStyleSheet(); 2 | addressSheet.replace('input { display: block; } fieldset { display: flex; flex-flow: column; gap: 16px; border: 1px solid #ccc}'); 3 | 4 | class Address extends HTMLElement { 5 | static get formAssociated() { 6 | return true; 7 | } 8 | 9 | connectedCallback() { 10 | const root = this.attachShadow({ mode: 'open' }); 11 | root.adoptedStyleSheets = [addressSheet]; 12 | this.internals = this.attachInternals(); 13 | const form = document.createElement('form'); 14 | form.innerHTML = ` 15 |
16 | Address 17 | 18 | 19 | 20 | 21 | 22 | 23 |
`; 24 | root.append(form); 25 | 26 | // this.log(); 27 | form.addEventListener('submit', this.log.bind(this)); 28 | 29 | root.querySelector('[type="button"]') 30 | .addEventListener('click', this._other.bind(this)); 31 | } 32 | 33 | _other() { 34 | this.shadowRoot.querySelector('input').remove(); 35 | } 36 | 37 | log(event) { 38 | if (event) { 39 | event.preventDefault(); 40 | } 41 | const internalFormData = new FormData( 42 | this.shadowRoot.querySelector('form') 43 | ); 44 | this.internals.setFormValue( 45 | internalFormData 46 | ); 47 | 48 | const allData = new FormData(this.internals.form); 49 | 50 | for (let [key, value] of allData.entries()) { 51 | console.log({ [key]: value }); 52 | } 53 | } 54 | } 55 | 56 | customElements.define('x-address', Address); 57 | -------------------------------------------------------------------------------- /static/scripts/foo-bar.js: -------------------------------------------------------------------------------- 1 | export class FooBar extends HTMLElement { 2 | static get formAssociated() { return true; } 3 | static get observedAttributes() { return ['required', 'disabled']; } 4 | 5 | constructor() { 6 | super(); 7 | this.attachShadow({ 8 | mode: 'open', 9 | delegatesFocus: true 10 | }); 11 | this.internals_ = this.attachInternals(); 12 | this.internals_.ariaChecked = true; 13 | this._handleChanges = this._handleChanges.bind(this); 14 | } 15 | 16 | connectedCallback() { 17 | this.shadowRoot.innerHTML = ` 18 | 23 | 24 | `; 25 | this.input = this.shadowRoot.querySelector('input'); 26 | 27 | this._init(); 28 | this.required = this.hasAttribute('required'); 29 | } 30 | 31 | attributeChangedCallback(name, oldValue, newValue) { 32 | const booleanAttributes = ['required', 'disabled']; 33 | if (booleanAttributes.includes(name)) { 34 | if (this.hasAttribute(name) && !this[name]) { 35 | this[name] = true; 36 | } else if (!this.hasAttribute(name) && this[name]) { 37 | this[name] = false; 38 | } 39 | } else if (newValue !== oldValue) { 40 | this[name] = newValue; 41 | } 42 | } 43 | 44 | formAssociatedCallback(form) { 45 | console.log({form}) 46 | } 47 | 48 | formDisabledCallback(isDisabled) { 49 | console.log({isDisabled}) 50 | this.input.disabled = isDisabled; 51 | } 52 | 53 | formResetCallback() { 54 | this.input.value = ''; 55 | this.internals_.setFormValue(''); 56 | } 57 | 58 | _init() { 59 | this.input.addEventListener('input', this._handleChanges); 60 | 61 | if (this.required) { 62 | this._handleRequired(this.value); 63 | } 64 | } 65 | 66 | _handleChanges(event) { 67 | const { value } = event.target; 68 | this.value = value; 69 | 70 | this._handleRequired(value); 71 | } 72 | 73 | _handleRequired(value) { 74 | if (!value) { 75 | this.internals_.setValidity({ 76 | valueMissing: true 77 | }, 'This field is required'); 78 | } else { 79 | this.internals_.setValidity({}); 80 | } 81 | } 82 | 83 | get required() { 84 | return this.hasAttribute('required'); 85 | } 86 | 87 | set required(required) { 88 | this.toggleAttribute('required', required); 89 | this.setAttribute('aria-required', !!required); 90 | this._handleRequired(!required); 91 | } 92 | 93 | get value() { 94 | return this._value; 95 | } 96 | 97 | set value(value) { 98 | this._value = value; 99 | this.internals_.setFormValue(value); 100 | return true; 101 | } 102 | 103 | get checkValidity() { 104 | return () => this.internals_.checkValidity(); 105 | } 106 | 107 | get validity() { 108 | return this.internals_.validity; 109 | } 110 | 111 | focus() { 112 | super.focus(); 113 | this.input.focus(); 114 | } 115 | } 116 | 117 | customElements.define('foo-bar', FooBar); 118 | -------------------------------------------------------------------------------- /static/scripts/issue30.js: -------------------------------------------------------------------------------- 1 | import { html, render } from 'https://cdn.skypack.dev/lit'; 2 | 3 | const sheet = new CSSStyleSheet(); 4 | sheet.replace(`:host { 5 | display: block; 6 | height: 30px; 7 | background: green; 8 | } 9 | :host(:invalid), :host([internals-invalid]) { 10 | background: red; 11 | } 12 | :host(:valid) { 13 | background: tomato; 14 | }`); 15 | 16 | const template = document.createElement('template'); 17 | template.innerHTML = ' invalid events'; 18 | 19 | class MyComponent extends HTMLElement { 20 | static get formAssociated() { return true; } 21 | 22 | constructor() { 23 | super(); 24 | this.internals = this.attachInternals(); 25 | this.count = 0; 26 | this.addEventListener('invalid', () => this.render(1)); 27 | const root = this.attachShadow({mode: 'open'}); 28 | root.adoptedStyleSheets = [sheet]; 29 | root.append(template.content.cloneNode(true)); 30 | this._count = root.querySelector('.count'); 31 | // console.log({constructed: this.getAttribute('name'), internals: this.internals.form }); 32 | } 33 | 34 | connectedCallback() { 35 | // console.log({connected: this.getAttribute('name')}); 36 | this.internals.setValidity({ valueMissing: true }, 'aaaa'); 37 | this.render(0); 38 | } 39 | 40 | render(increment) { 41 | this.count += increment; 42 | this._count.innerText = this.count; 43 | } 44 | } 45 | 46 | customElements.define('my-component', MyComponent); 47 | 48 | const form = document.querySelector('form'); 49 | 50 | // const submit = document.querySelector('#submit').addEventListener('click', (e)=>{ 51 | // form.submit(); 52 | // }); 53 | form.addEventListener('invalid', console.log); 54 | const reportValidity = document.querySelector('#reportValidity').addEventListener('click', (e)=>{ 55 | alert(form.reportValidity()); 56 | }); 57 | const checkValidity = document.querySelector('#checkValidity').addEventListener('click', (e)=>{ 58 | alert(form.checkValidity()); 59 | }); 60 | 61 | render( 62 | html`
63 | ${html``} 64 | 65 | 66 | ${html``} 67 | 68 |
`, 69 | document.getElementById('test') 70 | ); 71 | -------------------------------------------------------------------------------- /static/scripts/page.js: -------------------------------------------------------------------------------- 1 | import 'https://cdn.skypack.dev/construct-style-sheets-polyfill'; 2 | import './x-array.js'; 3 | import './address.js'; 4 | import './issue30.js'; 5 | 6 | const form = document.getElementById('form'); 7 | 8 | form.addEventListener('submit', event => { 9 | event.preventDefault(); 10 | 11 | const formData = new FormData(event.target); 12 | const formValue = {}; 13 | 14 | formData.forEach((value, name) => { 15 | formValue[name] = value; 16 | }); 17 | 18 | console.log(formValue); 19 | }); 20 | 21 | const div = document.createElement('div'); 22 | document.body.append(div); 23 | const testRoot = div.attachShadow({ mode: 'open' }); 24 | const shadowForm = document.createElement('form'); 25 | shadowForm.id = 'shadowTestForm'; 26 | const fooBar = document.createElement('foo-bar'); 27 | fooBar.required = true; 28 | testRoot.append(shadowForm); 29 | testRoot.append(fooBar); 30 | setTimeout(() => { 31 | console.log('start'); 32 | shadowForm.append(fooBar); 33 | console.log('finish', fooBar); 34 | }, 2000); 35 | -------------------------------------------------------------------------------- /static/scripts/x-array.js: -------------------------------------------------------------------------------- 1 | class XArray extends HTMLElement { 2 | static get formAssociated() { 3 | return true; 4 | } 5 | 6 | connectedCallback() { 7 | this.internals = this.attachInternals(); 8 | this.internals.setFormValue({a:1,b:2}); 9 | 10 | console.log(this.getAttribute('name'), 11 | new FormData(this.internals.form) 12 | .get(this.getAttribute('name')) 13 | ) 14 | } 15 | } 16 | 17 | customElements.define('x-array', XArray); 18 | -------------------------------------------------------------------------------- /static/styles/page.css: -------------------------------------------------------------------------------- 1 | label { 2 | display: block; 3 | } 4 | 5 | label + * { 6 | margin-bottom: 1rem; 7 | } 8 | 9 | button { 10 | display: block; 11 | } 12 | -------------------------------------------------------------------------------- /test/CustomStateSet.test.ts: -------------------------------------------------------------------------------- 1 | import '../dist/index.js'; 2 | 3 | import { fixture, html, expect, fixtureCleanup, aTimeout } from '@open-wc/testing'; 4 | import { ICustomElement } from '../dist'; 5 | import { CustomStateSet } from '../src/CustomStateSet'; 6 | 7 | class CustomElementAddStateInConstructor extends HTMLElement { 8 | internals = this.attachInternals(); 9 | constructor() { 10 | super(); 11 | 12 | this.internals.states.add('--foo'); 13 | } 14 | } 15 | 16 | const addStateTagName = 'custom-element-add-state-in-constructor'; 17 | customElements.define(addStateTagName, CustomElementAddStateInConstructor); 18 | 19 | class CustomElementDeleteStateInConstructor extends HTMLElement { 20 | internals = this.attachInternals(); 21 | constructor() { 22 | super(); 23 | 24 | this.internals.states.delete('--foo'); 25 | } 26 | } 27 | 28 | const deleteStateTagName = 'custom-element-delete-state-in-constructor'; 29 | customElements.define(deleteStateTagName, CustomElementDeleteStateInConstructor); 30 | 31 | describe('CustomStateSet polyfill', () => { 32 | let el: HTMLElement; 33 | let set: CustomStateSet; 34 | 35 | beforeEach(async () => { 36 | el = await fixture(html``); 37 | set = new CustomStateSet(el as ICustomElement); 38 | }); 39 | 40 | afterEach(() => { 41 | fixtureCleanup(); 42 | }); 43 | 44 | it('will add attributes and parts', async () => { 45 | set.add('--foo'); 46 | expect(el.hasAttribute('state--foo')).to.be.true; 47 | if (el.part) { 48 | expect(el.part.contains('state--foo')).to.be.true; 49 | } 50 | }); 51 | 52 | it('will remove attributes and parts', async () => { 53 | set.add('--foo'); 54 | expect(el.hasAttribute('state--foo')).to.be.true; 55 | if (el.part) { 56 | expect(el.part.contains('state--foo')).to.be.true; 57 | } 58 | 59 | set.delete('--foo'); 60 | expect(el.hasAttribute('state--foo')).to.be.false; 61 | if (el.part) { 62 | expect(el.part.contains('state--foo')).to.be.false; 63 | } 64 | }); 65 | 66 | it('will clear all attributes and parts', async () => { 67 | set.add('--foo'); 68 | set.add('--bar'); 69 | 70 | expect(el.hasAttribute('state--foo')).to.be.true; 71 | expect(el.hasAttribute('state--bar')).to.be.true; 72 | if (el.part) { 73 | expect(el.part.contains('state--foo')).to.be.true; 74 | expect(el.part.contains('state--bar')).to.be.true; 75 | } 76 | 77 | set.clear(); 78 | expect(el.hasAttribute('state--foo')).to.be.false; 79 | expect(el.hasAttribute('state--bar')).to.be.false; 80 | if (el.part) { 81 | expect(el.part.contains('state--foo')).to.be.false; 82 | expect(el.part.contains('state--bar')).to.be.false; 83 | } 84 | }); 85 | 86 | it('will use a timeout if a state is added in a constructor', async () => { 87 | let el; 88 | expect(() => { 89 | el = document.createElement(addStateTagName); 90 | }).not.to.throw(); 91 | 92 | if (window.CustomStateSet.isPolyfilled) { 93 | await aTimeout(100); 94 | expect(el.internals.states.has('--foo')).to.be.true; 95 | } else { 96 |     expect(el.internals.states.has('--foo')).to.be.true; 97 | } 98 | }); 99 | 100 | it('will use a timeout if a state is deleted in a constructor', async () => { 101 | let el; 102 | expect(() => { 103 | el = document.createElement(deleteStateTagName); 104 | }).not.to.throw(); 105 | 106 | if (window.CustomStateSet.isPolyfilled) { 107 | await aTimeout(100); 108 | expect(el.matches('[state--foo]')).to.be.false; 109 | } else { 110 |     expect(el.internals.states.has('--foo')).to.be.false; 111 | } 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /test/ElementInternals.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | aTimeout, 3 | elementUpdated, 4 | expect, 5 | fixture, 6 | fixtureCleanup, 7 | html, 8 | } from '@open-wc/testing'; 9 | import '../dist/index.js'; 10 | 11 | let callCount = 0; 12 | let internalsAvailableInFormAssociatedCallback = false; 13 | 14 | window.onFormSubmit = (event) => { 15 | event.preventDefault(); 16 | callCount += 1; 17 | }; 18 | 19 | class CustomElement extends HTMLElement { 20 | static get formAssociated() { 21 | return true; 22 | } 23 | 24 | constructor() { 25 | super(); 26 | const root = this.attachShadow({ mode: 'open' }); 27 | this.internals = this.attachInternals(); 28 | root.innerHTML = ''; 29 | 30 | this._value = ''; 31 | } 32 | 33 | connectedCallback() { 34 | this.tabIndex = -1; 35 | this.input = this.shadowRoot.querySelector('input'); 36 | } 37 | 38 | set disabled(disabled) { 39 | this._disabled = disabled; 40 | this.toggleAttribute('disabled', disabled); 41 | } 42 | 43 | get disabled() { 44 | return this._disabled; 45 | } 46 | 47 | set required(required) { 48 | this._required = required; 49 | this.toggleAttribute('required', required); 50 | if (!this.value) { 51 | this.internals.setValidity({ 52 | valueMissing: true 53 | }, 'This field is required'); 54 | } else { 55 | this.internals.setValidity({ 56 | valueMissing: false 57 | }); 58 | } 59 | } 60 | 61 | get required() { 62 | return this._required; 63 | } 64 | 65 | set value(value) { 66 | this._value = value; 67 | this.internals.setFormValue(value); 68 | } 69 | 70 | get value() { 71 | return this._value; 72 | } 73 | 74 | formAssociatedCallback() { 75 | internalsAvailableInFormAssociatedCallback = !!this.internals; 76 | } 77 | 78 | formDisabledCallback() { 79 | callCount += 1; 80 | } 81 | formResetCallback() { 82 | callCount += 1; 83 | } 84 | 85 | checkValidity() { return this.internals.checkValidity(); } 86 | reportValidity() { return this.internals.reportValidity(); } 87 | } 88 | 89 | customElements.define('test-el', CustomElement); 90 | 91 | describe('The ElementInternals polyfill', () => { 92 | describe('form validity', () => { 93 | let form, input; 94 | const simultateEvent = (eventType, value = undefined, options = {"bubbles":true, "cancelable":false}) => { 95 | input.value = value; 96 | input.required = input.required; 97 | input.dispatchEvent(new Event(eventType, options)); 98 | } 99 | 100 | beforeEach(async () => { 101 | form = await fixture(`
`); 102 | input = form.querySelector('test-el'); 103 | input.required = true; 104 | simultateEvent('input'); 105 | }); 106 | 107 | afterEach(async () => { 108 | await fixtureCleanup(form); 109 | }); 110 | 111 | it('form should be invalid when form-associated custom element is invalid', () => { 112 | expect(form.checkValidity()).to.be.false; 113 | }); 114 | 115 | it('checkValidity will be true if the element is disabled', () => { 116 | input.toggleAttribute('disabled', true); 117 | expect(input.checkValidity()).to.be.true; 118 | }); 119 | 120 | it('checkValidity will be true if the element is readOnly', async () => { 121 | input.toggleAttribute('readonly', true); 122 | /** Inconsistent behavior in Chrome version, bug reported */ 123 | if (ElementInternals.isPolyfilled) { 124 | expect(input.checkValidity()).to.be.true; 125 | } 126 | }); 127 | 128 | it('form should match form:invalid CSS selector when form-associated custom element is invalid', () => { 129 | expect(form.matches('form:is(:invalid, [internals-invalid])')).to.be.true; 130 | }); 131 | 132 | it('After input event, form should be valid if all inputs are invalid', () => { 133 | simultateEvent('input', 'test'); 134 | expect(form.checkValidity()).to.be.true; 135 | }); 136 | 137 | it('After input event, form should match form:valid CSS selector if all inputs are valid', () => { 138 | simultateEvent('input', 'test'); 139 | expect(form.matches('form:is(:valid:not([internals-invalid]), [internals-valid])')).to.be.true; 140 | }); 141 | 142 | it('After change event, form should be valid if all inputs are invalid', () => { 143 | simultateEvent('change', 'test'); 144 | expect(form.checkValidity()).to.be.true; 145 | }); 146 | 147 | it('After change event, form should match form:valid CSS selector if all inputs are valid', () => { 148 | simultateEvent('change', 'test'); 149 | expect(form.matches('form:is(:valid:not([internals-invalid]), [internals-valid])')).to.be.true; 150 | }); 151 | 152 | }); 153 | 154 | describe('outside the proper context', () => { 155 | let el, internals; 156 | 157 | beforeEach(async () => { 158 | el = await fixture(html``); 159 | internals = el.internals; 160 | }); 161 | 162 | afterEach(async () => { 163 | await fixtureCleanup(el); 164 | }); 165 | 166 | it('will throw if called directly', () => { 167 | expect(() => { 168 | new ElementInternals() 169 | }).to.throw(); 170 | }); 171 | 172 | it('will throw if called by a non custom element', async () => { 173 | const el = await fixture(html`
`); 174 | expect(() => el.attachInternals()).to.throw(); 175 | }); 176 | }); 177 | 178 | describe('Non-formAssociated elements', () => { 179 | let element, internals; 180 | 181 | class NotFormAssociated extends HTMLElement { 182 | constructor() { 183 | super(); 184 | this.internals = this.attachInternals(); 185 | this.internals.role = 'generic'; 186 | } 187 | } 188 | 189 | customElements.define('not-associated', NotFormAssociated); 190 | 191 | beforeEach(async () => { 192 | element = await fixture(html``); 193 | }); 194 | 195 | afterEach(async () => await fixtureCleanup(element)); 196 | 197 | it('will attach an object to internals even if not form associated', async () => { 198 | expect(element.internals).to.exist; 199 | }); 200 | 201 | it('will throw from setFormValue', async () => { 202 | expect(() => element.internals.setFormValue('foo')).to.throw(); 203 | }); 204 | 205 | describe('inside a form', () => { 206 | let form; 207 | 208 | afterEach(async () => await fixtureCleanup(form)); 209 | 210 | it('will not throw', async () => { 211 | form = await fixture(html`
`); 212 | }) 213 | }) 214 | }); 215 | 216 | describe('inside a custom element with a form', () => { 217 | let form, el, noname, label, button, internals; 218 | 219 | beforeEach(async () => { 220 | form = await fixture(html` 221 |
222 | 223 | 224 | 225 | 226 |
227 | `); 228 | callCount = 0; 229 | label = form.querySelector('label'); 230 | el = form.querySelector('test-el[id=foo]'); 231 | noname = form.querySelector('test-el[id=noname]'); 232 | button = form.querySelector('button'); 233 | internals = el.internals; 234 | }); 235 | 236 | afterEach(async () => { 237 | fixtureCleanup(form) 238 | }); 239 | 240 | it('will have the proper structure with a form', () => { 241 | expect(internals.form).to.equal(form); 242 | }); 243 | 244 | it('will be valid by default', () => { 245 | expect(internals.validity.valid).to.be.true; 246 | expect(internals.checkValidity()).to.be.true; 247 | }); 248 | 249 | it('will be invalid if the validity has been set to false', () => { 250 | internals.setValidity({ 251 | valueMissing: true 252 | }, 'This field is required'); 253 | 254 | expect(internals.validity.valid).to.be.false; 255 | expect(internals.checkValidity()).to.be.false; 256 | }); 257 | 258 | it('will be valid if toggled back to true from false', () => { 259 | internals.setValidity({ 260 | valueMissing: true 261 | }, 'This field is required'); 262 | 263 | expect(internals.validity.valid).to.be.false; 264 | expect(internals.checkValidity()).to.be.false; 265 | 266 | internals.setValidity({ 267 | valueMissing: false 268 | }); 269 | 270 | expect(internals.validity.valid).to.be.true; 271 | expect(internals.checkValidity()).to.be.true; 272 | }); 273 | 274 | it('will set the validation message from a call to setValidity', () => { 275 | internals.setValidity({ 276 | valueMissing: true 277 | }, 'This field is required'); 278 | 279 | expect(internals.validationMessage).to.equal('This field is required'); 280 | }); 281 | 282 | it('will unset the validation message from a call to setValidity', () => { 283 | internals.setValidity({ 284 | valueMissing: true 285 | }, 'This field is required'); 286 | 287 | expect(internals.validationMessage).to.equal('This field is required'); 288 | 289 | internals.setValidity({ valueMissing: false }); 290 | expect(internals.validationMessage).to.equal(''); 291 | }); 292 | 293 | it('will reset validity if an object literal is passed to setValidity', () => { 294 | internals.setValidity({ 295 | valueMissing: true 296 | }, 'This field is required'); 297 | expect(internals.validity.valid).to.be.false; 298 | internals.setValidity({}); 299 | expect(internals.validity.valid).to.be.true; 300 | }); 301 | 302 | it('will throw if setValidity is called with a flag and no validation message', () => { 303 | expect(() => { 304 | internals.setValidity({ 305 | valueMissing: true 306 | }); 307 | }).to.throw(); 308 | }); 309 | 310 | it ('will accept ValidityState from a native form input', () => { 311 | el.input.required = true; 312 | el.input.reportValidity(); 313 | internals.setValidity(el.input.validity, el.input.validationMessage, el.input); 314 | expect(internals.validity.valueMissing).to.be.true; 315 | }); 316 | 317 | it('will return true for willValidate if the field can participate in the form', () => { 318 | expect(internals.willValidate).to.be.true; 319 | }); 320 | 321 | it('will return false from willValidate if the field is disabled', async () => { 322 | el.disabled = true; 323 | await elementUpdated(el); 324 | expect(internals.willValidate).to.be.false; 325 | if (ElementInternals.isPolyfilled) { 326 | expect(el.getAttribute('aria-disabled')).to.equal('true'); 327 | } 328 | }); 329 | 330 | it('will participate in forms', async () => { 331 | el.value = 'testing'; 332 | expect(new FormData(form).get('foo')).to.equal('testing'); 333 | }); 334 | 335 | it('will respond to name changes', async () => { 336 | el.value = 'testing'; 337 | const formData = new FormData(form); 338 | expect(formData.get('xyz')).to.equal(null); 339 | expect(formData.get('foo')).to.equal('testing'); 340 | el.setAttribute('name', 'xyz'); 341 | await aTimeout(0); 342 | const newFormData = new FormData(form); 343 | expect(newFormData.get('xyz')).to.equal('testing'); 344 | expect(newFormData.get('foo')).to.equal(null); 345 | }); 346 | 347 | it('will trigger the formDisabledCallback when disabled', async () => { 348 | el.disabled = true; 349 | await elementUpdated(el); 350 | // Lifecycle methods are stripped off at definition time 351 | // and added elsewhere so we can't use a spy. Instead 352 | // we're going to look for a side-effect 353 | expect(callCount).to.equal(1); 354 | }); 355 | 356 | it('will respond to form reset events', async () => { 357 | form.reset(); 358 | // Lifecycle methods are stripped off at definition time 359 | // and added elsewhere so we can't use a spy. Instead 360 | // we're going to look for a side-effect 361 | expect(callCount).to.equal(2); 362 | }); 363 | 364 | it('will cancel form submission if invalid', (done) => { 365 | el.addEventListener('invalid', event => { 366 | expect(event).to.exist; 367 | done(); 368 | }); 369 | internals.setValidity({ 370 | valueMissing: true 371 | }, 'This field is required'); 372 | button.click(); 373 | }); 374 | 375 | it('will wire up labels', async () => { 376 | expect([...internals.labels]).to.deep.equal([label]); 377 | }); 378 | 379 | it('will dispatch an invalid event on checkValidity if invalid', (done) => { 380 | el.addEventListener('invalid', event => { 381 | expect(event).to.exist; 382 | done(); 383 | }); 384 | internals.setValidity({ 385 | valueMissing: true 386 | }, 'This field is required'); 387 | el.internals.checkValidity(); 388 | }); 389 | 390 | it('will call formAssociatedCallback after internals have been set', () => { 391 | expect(internalsAvailableInFormAssociatedCallback).to.be.true; 392 | }); 393 | 394 | it('will not include null values set via setFormValue', () => { 395 | internals.setFormValue('test'); 396 | internals.setFormValue(null); 397 | const output = new FormData(form); 398 | expect(Array.from(output.keys()).length).to.equal(0); 399 | }); 400 | 401 | it('will not include undefined values set via setFormValue', () => { 402 | internals.setFormValue('test'); 403 | internals.setFormValue(undefined); 404 | const output = new FormData(form); 405 | expect(Array.from(output.keys()).length).to.equal(0); 406 | }); 407 | 408 | it('will include multiple form values passed via FormData to setFormValue', () => { 409 | let input; 410 | let output; 411 | input = new FormData(); 412 | input.set('first', '1'); 413 | input.set('second', '2'); 414 | input.append('second', '22'); // Multi-value keys should also work 415 | internals.setFormValue(input); 416 | output = new FormData(form); 417 | expect(Array.from(output.values()).length).to.equal(3); 418 | expect(output.get('first')).to.equal('1'); 419 | expect(output.getAll('second').length).to.equal(2); 420 | input = new FormData(); 421 | input.set('override', '3'); 422 | internals.setFormValue(input); 423 | output = new FormData(form); 424 | expect(Array.from(output.keys()).length).to.equal(1); 425 | expect(output.get('override')).to.equal('3'); 426 | }); 427 | 428 | it('will not include form values from elements without a name', () => { 429 | noname.internals.setFormValue('noop'); 430 | const output = new FormData(form); 431 | expect(Array.from(output.keys()).length).to.equal(0); 432 | }); 433 | 434 | it('will include form values from elements without a name if set with FormData', () => { 435 | const formData = new FormData(); 436 | formData.set('formdata', 'works'); 437 | noname.internals.setFormValue(formData); 438 | const output = new FormData(form); 439 | expect(Array.from(output.keys()).length).to.equal(1); 440 | expect(output.get('formdata')).to.equal('works'); 441 | }); 442 | 443 | it('will append FormData in correct order', () => { 444 | const formData = new FormData(); 445 | formData.append('one', '1') 446 | formData.append('two', '2') 447 | noname.internals.setFormValue(formData); 448 | const output = new FormData(form); 449 | 450 | expect(Array.from(output.keys())).to.eql(['one', 'two']) 451 | }); 452 | 453 | it('saves a reference to all shadow roots', () => { 454 | expect(internals.shadowRoot).to.equal(el.shadowRoot); 455 | }); 456 | 457 | it('will focus the element if validated with anchor', async () => { 458 | internals.setValidity({ 459 | customError: true 460 | }, 'Error message', el.input); 461 | internals.reportValidity(); 462 | expect(document.activeElement).to.equal(el); 463 | }); 464 | 465 | it('will focus anchor elements in document order on form submission failure', () => { 466 | internals.setValidity({ 467 | customError: true 468 | }, 'Error message', el.input); 469 | noname.internals.setValidity({ 470 | customError: true 471 | }, 'Error message', noname.input); 472 | button.click(); 473 | expect(document.activeElement).to.equal(el); 474 | }); 475 | 476 | it('will accept non strings', async () => { 477 | internals.setFormValue(['a', 'b']); 478 | expect( 479 | new FormData(internals.form).get('foo') 480 | ).to.equal('a,b'); 481 | }); 482 | 483 | it('will accept empty strings', () => { 484 | internals.setFormValue(''); 485 | expect(new FormData(internals.form).get('foo')).to.equal(''); 486 | }); 487 | 488 | it('will set the form to the proper validity state', async () => { 489 | internals.setValidity({ valueMissing: true }, 'Error message'); 490 | expect(form.checkValidity()).to.be.false; 491 | expect(form.reportValidity()).to.be.false; 492 | 493 | internals.setValidity({}); 494 | expect(form.checkValidity()).to.be.true; 495 | expect(form.reportValidity()).to.be.true; 496 | }); 497 | }); 498 | 499 | describe('closed shadow root element', () => { 500 | let shadowRoot; 501 | let el; 502 | let internals; 503 | 504 | class ClosedRoot extends HTMLElement { 505 | constructor() { 506 | super(); 507 | shadowRoot = this.attachShadow({ mode: 'closed' }); 508 | this.internals = this.attachInternals(); 509 | } 510 | } 511 | 512 | customElements.define('closed-root', ClosedRoot); 513 | 514 | beforeEach(async () => { 515 | el = await fixture(html``); 516 | internals = el.internals; 517 | }); 518 | 519 | afterEach(async () => { 520 | await fixtureCleanup(el); 521 | }); 522 | 523 | it('maintains a reference to closed shadow roots', () => { 524 | expect(internals.shadowRoot).to.equal(shadowRoot); 525 | }); 526 | }); 527 | 528 | describe('Forms with onsubmit', () => { 529 | let form; 530 | let el; 531 | let internals; 532 | let button; 533 | 534 | beforeEach(async () => { 535 | form = await fixture(html`
536 | 537 | 538 |
`); 539 | el = form.querySelector('test-el'); 540 | internals = el.internals; 541 | button = form.querySelector('button'); 542 | callCount = 0; 543 | }); 544 | 545 | it('will not call onsubmit if invalid', async () => { 546 | expect(callCount).to.equal(0); 547 | internals.setValidity({ valueMissing: true }, 'Error message'); 548 | button.click(); 549 | expect(callCount).to.equal(0); 550 | }); 551 | 552 | it('will call onsubmit if valid', async () => { 553 | expect(callCount).to.equal(0); 554 | internals.setValidity({}); 555 | button.click(); 556 | expect(callCount).to.equal(1); 557 | }); 558 | }); 559 | 560 | describe('Forms with novalidate', () => { 561 | it('will not block submit', async () => { 562 | let submitCount = 0; 563 | const onSubmit = (event) => { 564 | event.preventDefault(); 565 | submitCount += 1; 566 | } 567 | const form = await fixture(html`
568 | 569 | 570 |
`); 571 | const testEl = form.querySelector('test-el'); 572 | const button = form.querySelector('button'); 573 | testEl.internals.setValidity({ 574 | customError: true 575 | }, 'error message'); 576 | button.click(); 577 | expect(submitCount).to.equal(1); 578 | }); 579 | }); 580 | 581 | describe('forms outside closed custom elements', () => { 582 | it('will not find forms outside of closed custom element', async () => { 583 | class ClosedElementWithCustomFormElement extends HTMLElement { 584 | constructor() { 585 | super(); 586 | this.renderRoot = this.attachShadow({ mode: 'closed' }); 587 | this.renderRoot.innerHTML = ``; 588 | } 589 | } 590 | customElements.define('closed-element-with-custom-form-element', ClosedElementWithCustomFormElement); 591 | 592 | const form = await fixture(html`
593 | 594 |
`); 595 | const shadowElement = form.querySelector('closed-element-with-custom-form-element'); 596 | const testEl = shadowElement.renderRoot.querySelector("test-el") 597 | expect(testEl.internals.form).to.be.null; 598 | }); 599 | }); 600 | 601 | describe('disabled custom element', () => { 602 | class DisabledElement extends HTMLElement { 603 | static formAssociated = true; 604 | 605 | order = []; 606 | 607 | constructor() { 608 | super(); 609 | 610 | this.attachShadow({ mode: 'open' }); 611 | this.attachInternals(); 612 | 613 | this.order.push('constructor'); 614 | } 615 | 616 | connectedCallback() { 617 | this.order.push('connectedCallback'); 618 | } 619 | 620 | disconnectedCallback() { 621 | this.order.push('disconnectedCallback'); 622 | } 623 | 624 | formDisabledCallback() { 625 | this.order.push('formDisabledCallback'); 626 | } 627 | } 628 | 629 | customElements.define('test-disabled', DisabledElement); 630 | 631 | let el; 632 | 633 | beforeEach(async () => { 634 | el = await fixture(html``); 635 | }); 636 | 637 | it('should have called the callbacks in the correct order', async () => { 638 | await el.updateComplete; 639 | 640 | expect(el.order).to.eql(['constructor', 'formDisabledCallback', 'connectedCallback']); 641 | }); 642 | 643 | it('should only call formDisabledCallback once', async () => { 644 | await el.updateComplete; 645 | 646 | const container = el.parentElement; 647 | container.removeChild(el); 648 | container.appendChild(el); 649 | 650 | expect(el.order).to.eql([ 651 | 'constructor', 652 | 'formDisabledCallback', 653 | 'connectedCallback', 654 | 'disconnectedCallback', 655 | 'connectedCallback' 656 | ]); 657 | }); 658 | }); 659 | }); 660 | -------------------------------------------------------------------------------- /test/FormElements.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, fixture, html } from '@open-wc/testing'; 2 | import '../dist/index.js'; 3 | class TestInput extends HTMLElement { 4 | static formAssociated = true; 5 | internals = this.attachInternals(); 6 | } 7 | 8 | customElements.define('test-input', TestInput); 9 | 10 | class TestDummy extends HTMLElement { 11 | } 12 | 13 | 14 | customElements.define('test-dummy', TestDummy); 15 | class TestFormAssociatedNoAttachInternals extends HTMLElement { 16 | static formAssociated = true; 17 | } 18 | customElements.define('test-no-attach-internals', TestFormAssociatedNoAttachInternals); 19 | 20 | async function createForm(): Promise { 21 | return await fixture(html` 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
`); 31 | } 32 | 33 | it('must contains the custom elements associated to the current form, in the correct order', async () => { 34 | const form = await createForm(); 35 | expect(form.elements).to.have.length(6); 36 | 37 | expect(form.elements[0]).to.be.an.instanceof(HTMLInputElement); 38 | expect(form.elements[1]).to.be.an.instanceof(TestInput); 39 | expect(form.elements[2]).to.be.an.instanceof(TestInput); 40 | expect(form.elements[3]).to.be.an.instanceof(TestInput); 41 | expect(form.elements[4]).to.be.an.instanceof(HTMLButtonElement); 42 | expect(form.elements[5]).to.be.an.instanceof(TestFormAssociatedNoAttachInternals); 43 | 44 | expect(form.elements[0].id).to.equal('foo'); 45 | expect(form.elements[1].id).to.equal('ti1'); 46 | expect(form.elements[2].id).to.equal('ti2'); 47 | expect(form.elements[3].id).to.equal('ti3'); 48 | 49 | expect(form.elements.namedItem('foo')).to.be.an.instanceof(HTMLInputElement); 50 | expect(form.elements.namedItem('first')).to.be.an.instanceof(TestInput); 51 | expect(form.elements.namedItem('avoid-me')).to.be.null; 52 | expect(form.elements.namedItem('second')).to.be.an.instanceof(TestInput); 53 | expect(form.elements.namedItem('third')).to.be.an.instanceof(TestInput); 54 | }); 55 | -------------------------------------------------------------------------------- /test/fieldset.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | aTimeout, 3 | expect, 4 | fixture, 5 | fixtureCleanup, 6 | html, 7 | } from '@open-wc/testing'; 8 | import '../dist/index.js'; 9 | 10 | let formDisabledCallbackCalled = false; 11 | class FieldsetTestElement extends HTMLElement { 12 | static formAssociated = true; 13 | internals = this.attachInternals(); 14 | 15 | formDisabledCallback() { 16 | formDisabledCallbackCalled = true; 17 | } 18 | } 19 | 20 | customElements.define('fieldset-test-element', FieldsetTestElement); 21 | interface FieldsetTestElementFixtureConfig { 22 | disabled?: boolean; 23 | } 24 | 25 | async function createFieldset({ disabled }: FieldsetTestElementFixtureConfig = {}): Promise { 26 | return await fixture(html`
27 | Demo 28 | 29 |
`); 30 | } 31 | 32 | describe('Internals behaviors with fieldset', () => { 33 | afterEach(fixtureCleanup); 34 | afterEach(() => { 35 | formDisabledCallbackCalled = false; 36 | }); 37 | 38 | it('will trigger the formDisabledCallback of a nested component when the fieldset is disabled', async () => { 39 | const fieldset = await createFieldset(); 40 | expect(formDisabledCallbackCalled).to.be.false; 41 | fieldset.disabled = true; 42 | await aTimeout(0); 43 | expect(formDisabledCallbackCalled).to.be.true; 44 | }); 45 | 46 | it('will trigger the formDisabledCallback of a nested component when the fieldset is disabled on first render', async () => { 47 | await createFieldset({ 48 | disabled: true 49 | }); 50 | expect(formDisabledCallbackCalled).to.be.true; 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/lit-ssr.test.ts: -------------------------------------------------------------------------------- 1 | import '../dist/index.js'; 2 | 3 | import { cleanupFixtures, ssrFixture } from '@lit-labs/testing/fixtures.js'; 4 | import { expect } from '@open-wc/testing'; 5 | import { LitElement, html } from 'lit'; 6 | 7 | class SsrTestEl extends LitElement { 8 | internals = this.attachInternals(); 9 | constructor() { 10 | super(); 11 | this.internals.role = 'widget'; 12 | } 13 | } 14 | 15 | customElements.define('ssr-test-el', SsrTestEl); 16 | 17 | describe('The polyfill on the server', () => { 18 | afterEach(cleanupFixtures); 19 | 20 | it('should not throw', async () => { 21 | const el = await ssrFixture(html``, { 22 | hydrate: false, 23 | modules: ['../dist/index.js', './ssr-test-el.js'] 24 | }); 25 | 26 | expect(el.internals).to.exist; 27 | if (ElementInternals['isPolyfilled'] === true) { 28 | expect(el.getAttribute('role')).to.equal('widget'); 29 | } 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/polyfilledBrowsers.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | aTimeout, 3 | expect, 4 | fixture, 5 | fixtureCleanup, 6 | html, 7 | } from '@open-wc/testing'; 8 | import '../dist/index.js'; 9 | 10 | describe('ElementInternals polyfill behavior', () => { 11 | describe('accessibility object model', () => { 12 | let el, internals; 13 | 14 | class AomTest extends HTMLElement { 15 | constructor() { 16 | super(); 17 | this.internals = this.attachInternals(); 18 | this.internals.ariaHidden = true; 19 | } 20 | } 21 | 22 | customElements.define('aom-test', AomTest); 23 | 24 | beforeEach(async () => { 25 | el = await fixture(html``); 26 | internals = el.internals; 27 | }); 28 | 29 | afterEach(async () => { 30 | await fixtureCleanup(el); 31 | }); 32 | 33 | it('will add aria attributes if polyfilled', () => { 34 | if (ElementInternals.isPolyfilled) { 35 | expect(el.getAttribute('aria-hidden')).to.equal('true'); 36 | } 37 | }); 38 | 39 | it('will modify aria attributes if polyfilled', () => { 40 | internals.ariaHidden = false; 41 | if (ElementInternals.isPolyfilled) { 42 | expect(el.getAttribute('aria-hidden')).to.equal('false'); 43 | } 44 | }); 45 | }); 46 | 47 | describe('form associated behavior', () => { 48 | let form; 49 | let label; 50 | let el; 51 | let internals; 52 | 53 | class FormAssociated extends HTMLElement { 54 | static get formAssociated() { 55 | return true; 56 | } 57 | 58 | constructor() { 59 | super(); 60 | this.internals = this.attachInternals(); 61 | } 62 | 63 | connectedCallback() { 64 | this.tabIndex = -1; 65 | const root = this.attachShadow({ mode: 'open' }); 66 | root.innerHTML = ``; 67 | this.input = root.querySelector('input'); 68 | } 69 | } 70 | 71 | class ValidateInConstructor extends FormAssociated { 72 | constructor() { 73 | super(); 74 | this.internals.setValidity({ valueMissing: true }, 'Test'); 75 | } 76 | } 77 | 78 | customElements.define('form-associated', FormAssociated); 79 | customElements.define('validate-in-constructor', ValidateInConstructor); 80 | 81 | beforeEach(async () => { 82 | form = await fixture(html`
83 | 84 | 85 |
`); 86 | label = form.querySelector('label'); 87 | el = form.querySelector('form-associated'); 88 | internals = el.internals; 89 | }); 90 | 91 | afterEach(async () => await fixtureCleanup(form)); 92 | 93 | it('will set the aria atttributes on label', async () => { 94 | expect(internals.labels.length).to.equal(1); 95 | expect(Array.from(internals.labels)).to.deep.equal([label]); 96 | }); 97 | 98 | it('will toggle the internals-disabled attribute when disabled is set', async () => { 99 | if (ElementInternals.isPolyfilled) { 100 | el.setAttribute('disabled', true); 101 | el.disabled = true; 102 | await aTimeout(); 103 | expect(el.hasAttribute('internals-disabled')).to.be.true; 104 | el.toggleAttribute('disabled', false); 105 | await aTimeout(); 106 | expect(el.hasAttribute('internals-disabled')).to.be.false; 107 | } 108 | }); 109 | 110 | it('will reflect internals.role', async () => { 111 | if (ElementInternals.isPolyfilled) { 112 | el.internals.role = 'button'; 113 | await aTimeout(); 114 | expect(el.getAttribute('role')).to.equal('button'); 115 | } 116 | }); 117 | 118 | it('will not throw and will upgrade if constructed using document.createElement', async () => { 119 | let el; 120 | expect(() => { 121 | el = document.createElement('validate-in-constructor'); 122 | }).not.to.throw(); 123 | document.body.append(el); 124 | await aTimeout(0); 125 | if (ElementInternals.isPolyfilled) { 126 | expect(el.getAttribute('internals-valid')).to.equal('false'); 127 | expect(el.getAttribute('internals-invalid')).to.equal('true'); 128 | } 129 | }); 130 | }); 131 | 132 | describe('CustomStateSet', () => { 133 | let el; 134 | let states; 135 | 136 | class StateSetElement extends HTMLElement { 137 | constructor() { 138 | super(); 139 | 140 | this.internals = this.attachInternals(); 141 | } 142 | } 143 | 144 | customElements.define('state-set-element', StateSetElement); 145 | 146 | beforeEach(async () => { 147 | el = await fixture(html``); 148 | console.log({el}) 149 | states = el.internals.states; 150 | }); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /test/ssr-test-el.js: -------------------------------------------------------------------------------- 1 | import { LitElement } from 'lit'; 2 | 3 | export class SsrTestEl extends LitElement { 4 | constructor() { 5 | super(); 6 | this.internals = this.attachInternals(); 7 | this.internals.role = 'widget'; 8 | } 9 | } 10 | 11 | export const tagName = 'ssr-test-el' 12 | customElements.define(tagName, SsrTestEl); 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "target": "ES2021", 5 | "declaration": true, 6 | "declarationDir": "dist", 7 | "outDir": "dist", 8 | "rootDir": "src", 9 | "moduleResolution": "Node" 10 | }, 11 | "lib": ["dom", "ESNext"], 12 | "include": ["./src/**/*"], 13 | "exclude": ["node_modules", "dist"] 14 | } 15 | -------------------------------------------------------------------------------- /web-test-runner.config.mjs: -------------------------------------------------------------------------------- 1 | import { litSsrPlugin } from '@lit-labs/testing/web-test-runner-ssr-plugin.js'; 2 | import { esbuildPlugin } from '@web/dev-server-esbuild'; 3 | 4 | export default { 5 | plugins: [ 6 | litSsrPlugin(), 7 | esbuildPlugin({ ts: true, target: 'auto' }) 8 | ], 9 | }; 10 | --------------------------------------------------------------------------------