├── .babelrc ├── .eslintrc.json ├── .flowconfig ├── .github ├── dependabot.yml └── workflows │ ├── actionlint.yml │ └── ci.yml ├── .gitignore ├── .sass-lint.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── examples.css ├── index.esm.js ├── index.umd.js └── miller-columns.css ├── examples.scss ├── examples ├── checkboxes-checked.html ├── index.html └── miller-columns-test.html ├── index.js ├── miller-columns-selected.scss ├── miller-columns.scss ├── package-lock.json ├── package.json ├── prettier.config.js └── test ├── .eslintrc.json ├── karma.config.js └── test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "esm": { 4 | "plugins": [ 5 | "transform-custom-element-classes" 6 | ], 7 | "presets": [ 8 | ["es2015", {"modules": false}], 9 | "flow" 10 | ] 11 | }, 12 | "umd": { 13 | "plugins": [ 14 | "transform-custom-element-classes", 15 | "transform-es2015-modules-umd" 16 | ], 17 | "presets": ["es2015", "flow"] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:github/es6", 4 | "plugin:github/browser" 5 | ], 6 | "parser": "babel-eslint" 7 | } 8 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | 9 | [lints] 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | dependency-type: production 6 | schedule: 7 | interval: daily 8 | time: "03:00" 9 | open-pull-requests-limit: 10 10 | ignore: 11 | - dependency-name: eslint-plugin-github 12 | versions: 13 | - "> 1.6.0" 14 | - dependency-name: flow-bin 15 | versions: 16 | - "> 0.133.0, < 1" 17 | - dependency-name: flow-bin 18 | versions: 19 | - ">= 0.145.a, < 0.146" 20 | - dependency-name: y18n 21 | versions: 22 | - 4.0.1 23 | - 4.0.2 24 | - 4.0.3 25 | - dependency-name: eslint 26 | versions: 27 | - 7.19.0 28 | - 7.21.0 29 | - 7.23.0 30 | - dependency-name: karma 31 | versions: 32 | - 6.0.3 33 | - 6.0.4 34 | - 6.1.0 35 | - 6.1.2 36 | - 6.3.0 37 | - 6.3.1 38 | - dependency-name: mocha 39 | versions: 40 | - 8.3.1 41 | - dependency-name: chai 42 | versions: 43 | - 4.3.1 44 | - 4.3.3 45 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint GitHub Actions 2 | on: 3 | push: 4 | paths: ['.github/**'] 5 | jobs: 6 | actionlint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | show-progress: false 12 | - uses: alphagov/govuk-infrastructure/.github/actions/actionlint@main 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | jobs: 3 | codeql-sast: 4 | name: CodeQL SAST scan 5 | uses: alphagov/govuk-infrastructure/.github/workflows/codeql-analysis.yml@main 6 | permissions: 7 | security-events: write 8 | 9 | dependency-review: 10 | name: Dependency Review scan 11 | uses: alphagov/govuk-infrastructure/.github/workflows/dependency-review.yml@main 12 | 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | cache: npm 21 | - run: npm ci 22 | - run: npm run test 23 | - run: npm run build 24 | 25 | release: 26 | needs: test 27 | runs-on: ubuntu-latest 28 | if: ${{ github.ref == 'refs/heads/main' }} 29 | permissions: 30 | contents: write 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: actions/setup-node@v4 34 | with: 35 | node-version: 20 36 | cache: npm 37 | registry-url: 'https://registry.npmjs.org' 38 | - run: npm ci 39 | - run: npm run build 40 | - name: Deploy to GitHub Pages 41 | uses: JamesIves/github-pages-deploy-action@0f24da7de3e7e135102609a4c9633b025be8411b 42 | with: 43 | branch: gh-pages 44 | folder: examples 45 | - name: Establish version 46 | run: | 47 | LOCAL=$(node -p "require('./package.json').version") 48 | echo "local=${LOCAL}" >> "$GITHUB_OUTPUT" 49 | echo "remote=$(npm view miller-columns-element version)" >> "$GITHUB_OUTPUT" 50 | if git ls-remote --tags --exit-code origin "${LOCAL}"; then 51 | echo "tagged=yes" >> "$GITHUB_OUTPUT" 52 | fi 53 | id: version 54 | - name: Tag version 55 | if: ${{ steps.version.outputs.tagged != 'yes' }} 56 | run: git tag ${{ steps.version.outputs.local }} && git push --tags 57 | - name: Release to NPM 58 | if: ${{ steps.version.outputs.local != steps.version.outputs.remote }} 59 | run: npm publish 60 | env: 61 | NODE_AUTH_TOKEN: ${{ secrets.ALPHAGOV_NPM_AUTOMATION_TOKEN }} 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | examples/dist 3 | -------------------------------------------------------------------------------- /.sass-lint.yml: -------------------------------------------------------------------------------- 1 | # Not yet supported by sass-lint: 2 | # ChainedClasses, DisableLinterReason, ElsePlacement, PropertyCount 3 | # PseudoElement, SelectorDepth, SpaceAroundOperator, TrailingWhitespace 4 | # UnnecessaryParentReference, Compass::* 5 | # 6 | # The following settings/values are unsupported by sass-lint: 7 | # Linter Comment, option "style" 8 | # Linter Indentation, option "allow_non_nested_indentation" 9 | # Linter Indentation, option "character" 10 | # Linter NestingDepth, option "ignore_parent_selectors" 11 | # Linter SpaceBeforeBrace, option "allow_single_line_padding" 12 | 13 | files: 14 | include: '**/*.s+(a|c)ss' 15 | options: 16 | formatter: stylish 17 | merge-default-rules: true 18 | rules: 19 | 20 | # Rule bem-depth will enforce how many elements a BEM selector can contain. 21 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/bem-depth.md 22 | bem-depth: 0 23 | 24 | # Rule border-zero will enforce whether one should use 0 or none when specifying a zero border value 25 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/border-zero.md 26 | border-zero: 2 27 | 28 | # Rule brace-style will enforce the use of the chosen brace style. 29 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/brace-style.md 30 | brace-style: 31 | - 2 32 | - allow-single-line: false 33 | 34 | # Rule class-name-format will enforce a convention for class names. 35 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/class-name-format.md#example-7 36 | # will allow .block {}, .block__element{}, .block--modifier {} 37 | class-name-format: 38 | - 2 39 | - convention: hyphenatedbem 40 | 41 | # Rule clean-import-paths will enforce whether or not @import paths should have leading underscores and/or filename extensions. 42 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/clean-import-paths.md 43 | clean-import-paths: 44 | - 2 45 | - filename-extension: false 46 | leading-underscore: false 47 | 48 | # TODO: empty-args 49 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/empty-args.md 50 | 51 | # Rule empty-line-between-blocks will enforce whether or not nested blocks should include a space between the last non-comment declaration or not. 52 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/empty-line-between-blocks.md 53 | empty-line-between-blocks: 54 | - 2 55 | - ignore-single-line-rulesets: true 56 | 57 | # Rule extends-before-declarations will enforce that extends should be written before declarations in a ruleset. 58 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/extends-before-declarations.md 59 | extends-before-declarations: 0 60 | 61 | # Rule extends-before-mixins will enforce that extends should be written before mixins in a ruleset. 62 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/extends-before-mixins.md 63 | extends-before-mixins: 0 64 | 65 | # Rule final-newline will enforce whether or not files should end with a newline. 66 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/final-newline.md 67 | final-newline: 68 | - 2 69 | - include: true 70 | 71 | # Rule force-attribute-nesting will enforce the nesting of attributes 72 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/force-attribute-nesting.md 73 | force-attribute-nesting: 74 | - 0 75 | 76 | # Rule force-element-nesting will enforce the nesting of elements 77 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/force-element-nesting.md 78 | force-element-nesting: 79 | - 0 80 | 81 | # Rule force-pseudo-nesting will enforce the nesting of pseudo elements/classes. 82 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/force-pseudo-nesting.md 83 | force-pseudo-nesting: 84 | - 0 85 | 86 | # Rule function-name-format will enforce a convention for function names. 87 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/function-name-format.md 88 | function-name-format: 89 | - 2 90 | - allow-leading-underscore: true 91 | convention: hyphenatedlowercase 92 | 93 | # Rule hex-length will enforce the length of hexadecimal values 94 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/hex-length.md 95 | hex-length: 96 | - 2 97 | - style: long 98 | 99 | # Rule hex-notation will enforce the case of hexadecimal values 100 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/hex-notation.md 101 | hex-notation: 102 | - 2 103 | - style: lowercase 104 | 105 | # Rule id-name-format will enforce a convention for ids. 106 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/id-name-format.md 107 | id-name-format: 108 | - 2 109 | - convention: hyphenatedlowercase 110 | 111 | # Rule indentation will enforce an indentation size (tabs and spaces) and it will also ensure that tabs and spaces are not mixed. 112 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/indentation.md 113 | indentation: 114 | - 2 115 | - size: 2 116 | 117 | # Rule leading-zero will enforce whether or not decimal numbers should include a leading zero. 118 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/leading-zero.md 119 | leading-zero: 120 | - 2 121 | - include: false 122 | 123 | # Rule mixin-name-format will enforce a convention for mixin names. 124 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/mixin-name-format.md 125 | mixin-name-format: 126 | - 2 127 | - allow-leading-underscore: true 128 | convention: hyphenatedlowercase 129 | 130 | # Rule mixins-before-declarations will enforce that mixins should be written before declarations in a ruleset. 131 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/mixins-before-declarations.md 132 | mixins-before-declarations: 0 133 | 134 | # Rule nesting-depth will enforce how deeply a selector can be nested. 135 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/nesting-depth.md 136 | nesting-depth: 137 | - 2 138 | - max-depth: 3 139 | 140 | # TODO: no-attribute-selectors 141 | # Rule no-attribute-selectors will warn against the use of attribute selectors. 142 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-attribute-selectors.md 143 | 144 | # TODO: no-colour-hex 145 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-color-hex.md 146 | 147 | # Rule no-color-keywords will enforce the use of hexadecimal color values rather than literals. 148 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-color-keywords.md 149 | no-color-keywords: 2 150 | 151 | # Rule no-color-literals will disallow the use of color literals and basic color functions in any declarations other than variables or maps/lists. 152 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-color-literals.md 153 | no-color-literals: 154 | - 2 155 | - allow-rgba: true 156 | 157 | # TODO: no-combniators 158 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-combinators.md 159 | 160 | # Rule no-css-comments will enforce the use of Sass single-line comments and disallow CSS comments. Bang comments (/*! */, will be printed even in minified mode) are still allowed. 161 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-css-comments.md 162 | no-css-comments: 2 163 | 164 | # Rule no-debug will enforce that @debug statements are not allowed to be used. 165 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-debug.md 166 | no-debug: 2 167 | 168 | # TODO: no-disallowed-properties 169 | # Rule no-disallowed-properties will warn against the use of certain properties. 170 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-disallowed-properties.md 171 | 172 | # Rule no-duplicate-properties will enforce that duplicate properties are not allowed within the same block. 173 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-duplicate-properties.md 174 | no-duplicate-properties: 2 175 | 176 | # Rule no-empty-rulesets will enforce that rulesets are not empty. 177 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-empty-rulesets.md 178 | no-empty-rulesets: 2 179 | 180 | # Rule no-extends will enforce that @extend is not allowed to be used. 181 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-extends.md 182 | no-extends: 0 183 | 184 | # Rule no-ids will enforce that ID selectors are not allowed to be used. 185 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-ids.md 186 | no-ids: 2 187 | 188 | # Rule no-important will enforce that important declarations are not allowed to be used. 189 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-important.md 190 | no-important: 0 191 | 192 | # Rule no-invalid-hex will enforce that only valid of hexadecimal values are written. 193 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-invalid-hex.md 194 | no-invalid-hex: 2 195 | 196 | # Rule no-mergeable-selectors will enforce that selectors aren't repeated and that their properties are merged. 197 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-mergeable-selectors.md 198 | no-mergeable-selectors: 0 199 | 200 | # Rule no-misspelled-properties will enforce the correct spelling of CSS properties and prevent the use of unknown CSS properties. 201 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-misspelled-properties.md 202 | no-misspelled-properties: 2 203 | 204 | # Rule no-qualifying-elements will enforce that selectors are not allowed to have qualifying elements. 205 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-qualifying-elements.md 206 | no-qualifying-elements: 207 | - 2 208 | - allow-element-with-attribute: true 209 | allow-element-with-class: false 210 | allow-element-with-id: false 211 | 212 | # TODO: no-trailing-whitespace 213 | # Rule no-trailing-whitespace will enforce that trailing whitespace is not allowed. 214 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-trailing-whitespace.md 215 | 216 | # Rule no-trailing-zero will enforce that trailing zeros are not allowed. 217 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-trailing-zero.md 218 | no-trailing-zero: 2 219 | 220 | # Rule no-transition-all will enforce whether the keyword all can be used with the transition or transition-property property. 221 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-transition-all.md 222 | no-transition-all: 2 223 | 224 | # TODO: no-universal-selectors 225 | # Rule no-universal-selectors will warn against the use of * (universal) selectors. 226 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-universal-selectors.md 227 | 228 | # Rule no-url-protocols will enforce that protocols and domains are not used within urls. 229 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-url-protocols.md 230 | no-url-protocols: 2 231 | 232 | # Rule no-vendor-prefixes will enforce that vendor prefixes are not allowed to be used. 233 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-vendor-prefixes.md 234 | no-vendor-prefixes: 0 235 | 236 | # Rule no-warn will enforce that @warn statements are not allowed to be used. 237 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-warn.md 238 | no-warn: 0 239 | 240 | # Rule placeholder-in-extend will enforce whether extends should only include placeholder selectors. 241 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/placeholder-in-extend.md 242 | placeholder-in-extend: 2 243 | 244 | # Rule placeholder-name-format will enforce a convention for placeholder names. 245 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/placeholder-name-format.md 246 | placeholder-name-format: 247 | - 2 248 | - convention: hyphenatedlowercase 249 | 250 | # Rule property-sort-order will enforce the order in which declarations are written. 251 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/property-sort-order.md 252 | property-sort-order: 253 | - 2 254 | - order: 255 | - 'content' 256 | - 'quotes' 257 | # Box-sizing - Allow here until global is decided 258 | - 'box-sizing' 259 | - 260 | - 'display' 261 | - 'visibility' 262 | - 263 | - 'position' 264 | - 'z-index' 265 | - 'top' 266 | - 'right' 267 | - 'bottom' 268 | - 'left' 269 | - 270 | - 'width' 271 | - 'min-width' 272 | - 'max-width' 273 | - 'height' 274 | - 'min-height' 275 | - 'max-height' 276 | - 277 | - 'margin' 278 | - 'margin-top' 279 | - 'margin-right' 280 | - 'margin-bottom' 281 | - 'margin-left' 282 | - 283 | - 'padding' 284 | - 'padding-top' 285 | - 'padding-right' 286 | - 'padding-bottom' 287 | - 'padding-left' 288 | - 289 | - 'float' 290 | - 'clear' 291 | - 292 | - 'overflow' 293 | - 'overflow-x' 294 | - 'overflow-y' 295 | - 296 | - 'clip' 297 | - 'clip-path' 298 | - 'zoom' 299 | - 'resize' 300 | - 301 | - 'columns' 302 | - 303 | - 'table-layout' 304 | - 'empty-cells' 305 | - 'caption-side' 306 | - 'border-spacing' 307 | - 'border-collapse' 308 | - 309 | - 'list-style' 310 | - 'list-style-position' 311 | - 'list-style-type' 312 | - 'list-style-image' 313 | - 314 | - 'transform' 315 | - 'transition' 316 | - 'animation' 317 | - 318 | - 'border' 319 | - 'border-top' 320 | - 'border-right' 321 | - 'border-bottom' 322 | - 'border-left' 323 | - 324 | - 'border-width' 325 | - 'border-top-width' 326 | - 'border-right-width' 327 | - 'border-bottom-width' 328 | - 'border-left-width' 329 | - 330 | - 'border-style' 331 | - 'border-top-style' 332 | - 'border-right-style' 333 | - 'border-bottom-style' 334 | - 'border-left-style' 335 | - 336 | - 'border-radius' 337 | - 'border-top-left-radius' 338 | - 'border-top-right-radius' 339 | - 'border-bottom-left-radius' 340 | - 'border-bottom-right-radius' 341 | - 342 | - 'border-color' 343 | - 'border-top-color' 344 | - 'border-right-color' 345 | - 'border-bottom-color' 346 | - 'border-left-color' 347 | - 348 | - 'outline' 349 | - 'outline-color' 350 | - 'outline-offset' 351 | - 'outline-style' 352 | - 'outline-width' 353 | - 354 | - 'opacity' 355 | # Color has been moved to ensure it appears before background 356 | - 'color' 357 | - 'background' 358 | - 'background-color' 359 | - 'background-image' 360 | - 'background-repeat' 361 | - 'background-position' 362 | - 'background-size' 363 | - 'box-shadow' 364 | - 'fill' 365 | - 366 | - 'font' 367 | - 'font-family' 368 | - 'font-size' 369 | - 'font-style' 370 | - 'font-variant' 371 | - 'font-weight' 372 | - 373 | - 'font-emphasize' 374 | - 375 | - 'letter-spacing' 376 | - 'line-height' 377 | - 'list-style' 378 | - 'word-spacing' 379 | - 380 | - 'text-align' 381 | - 'text-align-last' 382 | - 'text-decoration' 383 | - 'text-indent' 384 | - 'text-justify' 385 | - 'text-overflow' 386 | - 'text-overflow-ellipsis' 387 | - 'text-overflow-mode' 388 | - 'text-rendering' 389 | - 'text-outline' 390 | - 'text-shadow' 391 | - 'text-transform' 392 | - 'text-wrap' 393 | - 'word-wrap' 394 | - 'word-break' 395 | - 396 | - 'text-emphasis' 397 | - 398 | - 'vertical-align' 399 | - 'white-space' 400 | - 'word-spacing' 401 | - 'hyphens' 402 | - 403 | - 'src' 404 | - 'cursor' 405 | - '-webkit-appearance' 406 | 407 | # Rule property-units will disallow the use of units not specified in global or per-property. 408 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/property-units.md 409 | property-units: 410 | - 2 411 | - global: [ 412 | 'cm', 413 | 'em', 414 | 'pt', 415 | 'px', 416 | 'rem', 417 | 'vh', 418 | 'ex' 419 | ] 420 | 421 | # Rule pseudo-element will enforce that: 422 | # - pseudo-elements must start with double colons 423 | # - pseudo-classes must start with single colon 424 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/pseudo-element.md 425 | pseudo-element: 426 | - 0 427 | 428 | # Rule quotes will enforce whether single quotes ('') or double quotes ("") should be used for all strings. 429 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/quotes.md 430 | quotes: 431 | - 2 432 | - style: double 433 | 434 | # Rule shorthand-values will enforce that values in their shorthand form are as concise as specified. 435 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/shorthand-values.md 436 | shorthand-values: 437 | - 0 438 | - allowed-shorthands: 439 | - 1 440 | - 2 441 | - 3 442 | 443 | # Rule single-line-per-selector will enforce whether selectors should be placed on a new line. 444 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/single-line-per-selector.md 445 | single-line-per-selector: 2 446 | 447 | # Rule space-after-bang will enforce whether or not a space should be included after a bang (!). 448 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/space-after-bang.md 449 | space-after-bang: 450 | - 2 451 | - include: false 452 | 453 | # Rule space-after-colon will enforce whether or not a space should be included after a colon (:). 454 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/space-after-colon.md 455 | space-after-colon: 456 | - 2 457 | - include: true 458 | 459 | # Rule space-after-comma will enforce whether or not a space should be included after a comma (,). 460 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/space-after-comma.md 461 | space-after-comma: 462 | - 2 463 | - include: true 464 | 465 | # Rule space-around-operator will enforce whether or not a single space should be included before and after the following operators: +, -, /, *, %, <, > ==, !=, <= and >=. 466 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/space-around-operator.md 467 | space-around-operator: 468 | - 2 469 | - include: true 470 | 471 | # Rule space-before-bang will enforce whether or not a space should be included before a bang (!). 472 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/space-before-bang.md 473 | space-before-bang: 474 | - 2 475 | - include: true 476 | 477 | # Rule space-before-brace will enforce whether or not a space should be included before a brace ({). 478 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/space-before-brace.md 479 | space-before-brace: 480 | - 2 481 | - include: true 482 | 483 | # Rule space-before-colon will enforce whether or not a space should be included before a colon (:). 484 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/space-before-colon.md 485 | space-before-colon: 2 486 | 487 | # Rule space-between-parens will enforce whether or not a space should be included before the first item and after the last item inside parenthesis (()). 488 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/space-between-parens.md 489 | space-between-parens: 2 490 | 491 | # Rule trailing-semicolon will enforce whether the last declaration in a block should include a semicolon (;) or not. 492 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/trailing-semicolon.md 493 | trailing-semicolon: 2 494 | 495 | # Rule url-quotes will enforce that URLs are wrapped in quotes. 496 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/url-quotes.md 497 | url-quotes: 2 498 | 499 | # Rule variable-for-property will enforce the use of variables for the values of specified properties. 500 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/variable-for-property.md 501 | variable-for-property: 502 | - 0 503 | - properties: [] 504 | 505 | # Rule variable-name-format will enforce a convention for variable names. 506 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/variable-name-format.md 507 | variable-name-format: 508 | - 2 509 | - allow-leading-underscore: true 510 | convention: hyphenatedlowercase 511 | 512 | # Rule zero-unit will enforce whether or not values of 0 used for length should be unitless. 513 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/zero-unit.md 514 | zero-unit: 2 515 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | - We use [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 4 | - Mark breaking changes with `BREAKING:`. Be sure to include instructions on 5 | how applications should be upgraded. 6 | - Include a link to your pull request. 7 | - Don't include changes that are purely internal. The CHANGELOG should be a 8 | useful summary for people upgrading their application, not a replication 9 | of the commit log. 10 | 11 | ## 2.0.1 12 | 13 | - Add Github Action Linting and update Github Workflow to address deprecation errors 14 | 15 | ## 2.0.0 16 | 17 | - BREAKING: govuk-frontend classes are no longer bundled with this module, apps using this are expected to already be using `govuk-frontend` (PR #97) 18 | 19 | ## 1.3.2 20 | 21 | - Fix back-link styles by updating miller-columns-element to `govuk-frontend` 3.7 (PR #75) 22 | 23 | ## 1.3.1 24 | 25 | - Ensure checkboxes are focusable without JS (PR #35) 26 | 27 | ## 1.3.0 28 | 29 | - Fix active column on root taxons (PR #30) 30 | - Enable keyboard navigation (PR #31) 31 | - Enable navigation instructions for assistive technologies (PR #32) 32 | 33 | ## 1.2.1 34 | 35 | - Improve remove action for screen readers (PR #28) 36 | 37 | ## 1.2.0 38 | 39 | - Enhance mobile view (PR #25) 40 | - Update miller-columns-element to `govuk-frontend` 3.3 (PR #25) 41 | 42 | ## 1.1.0 43 | 44 | - Update miller-columns-element to `govuk-frontend` 3.2 (PR #23) 45 | 46 | ## 1.0.0 47 | 48 | - Trigger click event on checkboxes when selecting an item in the MillerColumnsElement (PR #16) 49 | - BREAKING: Remove `selectedTopicNames` as we don't need this API method for analytics anymore (PR #17) 50 | 51 | ## 0.1.1 52 | 53 | - Scope pointer events on checkboxes to the miller-columns element (PR #14) 54 | - Trigger remove-topic event on MillerColumnsSelectedElement (PR #15) 55 | 56 | ## 0.1.0 57 | 58 | - Initial release 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2019 Crown Copyright (Government Digital Service) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # <miller-columns> element 2 | 3 | Express a hierarchy by showing selectable lists of the items in each hierarchy level. 4 | 5 | Selection of any item shows that item’s children in the next list. 6 | 7 | ## Installation 8 | 9 | ``` 10 | npm install --save miller-columns-element 11 | ``` 12 | 13 | ## Usage 14 | 15 | This element is expected to be used in an application with [govuk-frontend](https://github.com/alphagov/govuk-frontend) installed. The expected CSS dependencies are outlined in [examples.scss](./examples.scss). 16 | 17 | ```html 18 | 21 | 22 | 23 | 24 | 25 | 47 | 48 | ``` 49 | 50 | ## Browser support 51 | 52 | Browsers without native [custom element support][support] require a [polyfill][]. 53 | 54 | - Chrome 55 | - Firefox 56 | - Safari 57 | - Internet Explorer 11 58 | - Microsoft Edge 59 | 60 | [support]: https://caniuse.com/#feat=custom-elementsv1 61 | [polyfill]: https://github.com/webcomponents/custom-elements 62 | 63 | ## Development 64 | 65 | ``` 66 | npm install 67 | npm test 68 | ``` 69 | 70 | To continuously build files while developing run: 71 | 72 | ``` 73 | npm run watch 74 | ``` 75 | 76 | To install and run a local HTTP server using Node.js: 77 | 78 | ``` 79 | npm install -g http-server 80 | http-server 81 | ``` 82 | 83 | To manually check examples in a web browser or using BrowserStack: 84 | 85 | - `http://127.0.0.1:8080/examples/index.html` (default example) 86 | - `http://127.0.0.1:8080/examples/checkboxes-checked.html` (with pre-selected items at page load) 87 | - `http://127.0.0.1:8080/examples/miller-columns-test.html` (example used for tests) 88 | 89 | Alternatively, you can open one of the HTML files in the [`/examples`](https://github.com/alphagov/miller-columns-element/tree/master/examples) directory for a quick preview. 90 | 91 | ## License 92 | 93 | Distributed under the MIT license. See LICENSE for details. 94 | -------------------------------------------------------------------------------- /dist/index.esm.js: -------------------------------------------------------------------------------- 1 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 6 | 7 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 8 | 9 | function _CustomElement() { 10 | return Reflect.construct(HTMLElement, [], this.__proto__.constructor); 11 | } 12 | 13 | ; 14 | Object.setPrototypeOf(_CustomElement.prototype, HTMLElement.prototype); 15 | Object.setPrototypeOf(_CustomElement, HTMLElement); 16 | 17 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 18 | 19 | function nodesToArray(nodes) { 20 | return Array.prototype.slice.call(nodes); 21 | } 22 | 23 | function triggerEvent(element, eventName, detail) { 24 | var params = { bubbles: true, cancelable: true, detail: detail || null }; 25 | var event = void 0; 26 | 27 | if (typeof window.CustomEvent === 'function') { 28 | event = new window.CustomEvent(eventName, params); 29 | } else { 30 | event = document.createEvent('CustomEvent'); 31 | event.initCustomEvent(eventName, params.bubbles, params.cancelable, params.detail); 32 | } 33 | 34 | element.dispatchEvent(event); 35 | } 36 | 37 | /** 38 | * This models the taxonomy shown in the miller columns and the current state 39 | * of it. 40 | * It notifies the miller columns element when it has changed state to update 41 | * the UI 42 | */ 43 | 44 | var Taxonomy = function () { 45 | function Taxonomy(topics, millerColumns) { 46 | _classCallCheck(this, Taxonomy); 47 | 48 | this.topics = topics; 49 | this.millerColumns = millerColumns; 50 | this.active = this.selectedTopics[0]; 51 | } 52 | 53 | /** fetches all the topics that are currently selected */ 54 | 55 | // At any time there is one or no active topic, the active topic determines 56 | // what part of the taxonomy is currently shown to the user (i.e which level) 57 | // if this is null a user is shown the root column 58 | 59 | 60 | _createClass(Taxonomy, [{ 61 | key: 'topicClicked', 62 | 63 | 64 | /** Handler for a topic in the miller columns being clicked */ 65 | value: function topicClicked(topic) { 66 | // if this is the active topic or a parent of it we deselect 67 | if (topic === this.active || topic.parentOf(this.active)) { 68 | topic.deselect(true); 69 | this.active = topic.parent; 70 | } else if (topic.selected || topic.selectedChildren.length) { 71 | // if this is a selected topic with children we make it active to allow 72 | // picking the children 73 | if (topic.children.length) { 74 | this.active = topic; 75 | } else { 76 | // otherwise we deselect it as we know the user can't be traversing 77 | topic.deselect(true); 78 | this.active = topic.parent; 79 | } 80 | } else { 81 | // otherwise this is a new selection 82 | topic.select(); 83 | this.active = topic; 84 | } 85 | this.millerColumns.update(); 86 | } 87 | 88 | /** Handler for when a topic is removed via the selected element */ 89 | 90 | }, { 91 | key: 'removeTopic', 92 | value: function removeTopic(topic) { 93 | topic.deselect(false); 94 | // determine which topic to mark as active, if any 95 | this.active = this.determineActiveFromRemoved(topic); 96 | this.millerColumns.update(); 97 | } 98 | 99 | /** Calculate most relevant topic to show user after they've removed a topic */ 100 | 101 | }, { 102 | key: 'determineActiveFromRemoved', 103 | value: function determineActiveFromRemoved(topic) { 104 | // if there is already an active item with selected children lets not 105 | // change anything 106 | if (this.active && (this.active.selected || this.active.selectedChildren.length)) { 107 | return this.active; 108 | } 109 | 110 | // see if there is a parent with selected topics, that feels like the most 111 | // natural place to end up 112 | var _iteratorNormalCompletion = true; 113 | var _didIteratorError = false; 114 | var _iteratorError = undefined; 115 | 116 | try { 117 | for (var _iterator = topic.parents.reverse()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 118 | var parent = _step.value; 119 | 120 | if (parent.selectedChildren.length) { 121 | return parent; 122 | } 123 | } 124 | 125 | // if we've still not got one we'll go for the first selected one 126 | } catch (err) { 127 | _didIteratorError = true; 128 | _iteratorError = err; 129 | } finally { 130 | try { 131 | if (!_iteratorNormalCompletion && _iterator.return) { 132 | _iterator.return(); 133 | } 134 | } finally { 135 | if (_didIteratorError) { 136 | throw _iteratorError; 137 | } 138 | } 139 | } 140 | 141 | return this.selectedTopics[0]; 142 | } 143 | }, { 144 | key: 'selectedTopics', 145 | get: function get() { 146 | return this.topics.reduce(function (memo, topic) { 147 | if (topic.selected) { 148 | memo.push(topic); 149 | } 150 | 151 | return memo.concat(topic.selectedChildren); 152 | }, []); 153 | } 154 | }, { 155 | key: 'flattenedTopics', 156 | get: function get() { 157 | return this.topics.reduce(function (memo, topic) { 158 | memo.push(topic); 159 | return memo.concat(topic.flattenedChildren); 160 | }, []); 161 | } 162 | }]); 163 | 164 | return Taxonomy; 165 | }(); 166 | 167 | /** 168 | * Represents a single topic in the taxonomy and knows whether it is currently 169 | * selected or not 170 | */ 171 | 172 | 173 | var Topic = function () { 174 | _createClass(Topic, null, [{ 175 | key: 'fromList', 176 | value: function fromList(list) { 177 | var parent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 178 | 179 | var topics = []; 180 | if (!list) { 181 | return topics; 182 | } 183 | 184 | var children = Array.from(list.children); 185 | 186 | var _iteratorNormalCompletion2 = true; 187 | var _didIteratorError2 = false; 188 | var _iteratorError2 = undefined; 189 | 190 | try { 191 | for (var _iterator2 = children.entries()[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { 192 | var _step2$value = _slicedToArray(_step2.value, 2), 193 | index = _step2$value[0], 194 | item = _step2$value[1]; 195 | 196 | var label = item.querySelector('label'); 197 | var checkbox = item.querySelector('input'); 198 | if (label instanceof HTMLLabelElement && checkbox instanceof HTMLInputElement) { 199 | var childList = item.querySelector('ul'); 200 | childList = childList instanceof HTMLUListElement ? childList : null; 201 | 202 | checkbox.tabIndex = -1; 203 | 204 | var previous = index > 0 ? topics[index - 1] : null; 205 | 206 | var topic = new Topic(label, checkbox, childList, parent, previous); 207 | 208 | if (index > 0) { 209 | topics[index - 1].next = topic; 210 | } 211 | 212 | topics.push(topic); 213 | } 214 | } 215 | } catch (err) { 216 | _didIteratorError2 = true; 217 | _iteratorError2 = err; 218 | } finally { 219 | try { 220 | if (!_iteratorNormalCompletion2 && _iterator2.return) { 221 | _iterator2.return(); 222 | } 223 | } finally { 224 | if (_didIteratorError2) { 225 | throw _iteratorError2; 226 | } 227 | } 228 | } 229 | 230 | return topics; 231 | } 232 | // Whether this topic is selected, we only allow one item in a branch of the 233 | // taxonomy to be selected. 234 | // E.g. given education > school > 6th form only one of these can be selected 235 | // at a time and the parents are implicity selected from it 236 | 237 | }]); 238 | 239 | function Topic(label, checkbox, childList, parent, previous) { 240 | _classCallCheck(this, Topic); 241 | 242 | this.label = label; 243 | this.checkbox = checkbox; 244 | this.parent = parent; 245 | this.children = Topic.fromList(childList, this); 246 | this.previous = previous; 247 | 248 | if (this.checkbox.checked) { 249 | this.select(); 250 | } else { 251 | this.selected = false; 252 | } 253 | } 254 | 255 | _createClass(Topic, [{ 256 | key: 'parentOf', 257 | 258 | 259 | /** Whether this topic is the parent of a different one */ 260 | value: function parentOf(other) { 261 | if (!other) { 262 | return false; 263 | } 264 | 265 | var _iteratorNormalCompletion3 = true; 266 | var _didIteratorError3 = false; 267 | var _iteratorError3 = undefined; 268 | 269 | try { 270 | for (var _iterator3 = this.children[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { 271 | var topic = _step3.value; 272 | 273 | if (topic === other || topic.parentOf(other)) { 274 | return true; 275 | } 276 | } 277 | } catch (err) { 278 | _didIteratorError3 = true; 279 | _iteratorError3 = err; 280 | } finally { 281 | try { 282 | if (!_iteratorNormalCompletion3 && _iterator3.return) { 283 | _iterator3.return(); 284 | } 285 | } finally { 286 | if (_didIteratorError3) { 287 | throw _iteratorError3; 288 | } 289 | } 290 | } 291 | 292 | return false; 293 | } 294 | }, { 295 | key: 'withParents', 296 | value: function withParents() { 297 | return this.parents.concat([this]); 298 | } 299 | 300 | /** Attempts to select this topic assuming it's not alrerady selected or has selected children */ 301 | 302 | }, { 303 | key: 'select', 304 | value: function select() { 305 | // if already selected or a child is selected do nothing 306 | if (this.selected || this.selectedChildren.length) { 307 | return; 308 | } 309 | this.selected = true; 310 | this.checkbox.checked = true; 311 | if (this.parent) { 312 | this.parent.childWasSelected(); 313 | } 314 | } 315 | 316 | /** 317 | * Deselects this topic. If this item is not itself selected but a child of it 318 | * is then it traverses to that child and deselects it. 319 | * Takes an optional argument as to whether to select the parent after deselection 320 | * Doing this allows a user to stay in context of their selection in the miller 321 | * column element as deselecting the whole tree would take them back to root 322 | */ 323 | 324 | }, { 325 | key: 'deselect', 326 | value: function deselect() { 327 | var selectParent = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; 328 | 329 | // if this item is selected explicitly we can deselect it 330 | if (this.selected) { 331 | this.deselectSelfAndParents(); 332 | } else { 333 | // otherwise we need to find the selected children to start deselecting 334 | var selectedChildren = this.selectedChildren; 335 | 336 | // if we have none it's a no-op 337 | if (!selectedChildren.length) { 338 | return; 339 | } 340 | 341 | var _iteratorNormalCompletion4 = true; 342 | var _didIteratorError4 = false; 343 | var _iteratorError4 = undefined; 344 | 345 | try { 346 | for (var _iterator4 = selectedChildren[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { 347 | var child = _step4.value; 348 | 349 | child.deselect(false); 350 | } 351 | } catch (err) { 352 | _didIteratorError4 = true; 353 | _iteratorError4 = err; 354 | } finally { 355 | try { 356 | if (!_iteratorNormalCompletion4 && _iterator4.return) { 357 | _iterator4.return(); 358 | } 359 | } finally { 360 | if (_didIteratorError4) { 361 | throw _iteratorError4; 362 | } 363 | } 364 | } 365 | } 366 | 367 | if (selectParent && this.parent) { 368 | this.parent.select(); 369 | } 370 | } 371 | }, { 372 | key: 'deselectSelfAndParents', 373 | value: function deselectSelfAndParents() { 374 | // loop through the parents only deselecting items that don't have other 375 | // selected children 376 | var _iteratorNormalCompletion5 = true; 377 | var _didIteratorError5 = false; 378 | var _iteratorError5 = undefined; 379 | 380 | try { 381 | for (var _iterator5 = this.withParents().reverse()[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { 382 | var topic = _step5.value; 383 | 384 | if (topic.selectedChildren.length) { 385 | break; 386 | } else { 387 | topic.selected = false; 388 | topic.checkbox.checked = false; 389 | } 390 | } 391 | } catch (err) { 392 | _didIteratorError5 = true; 393 | _iteratorError5 = err; 394 | } finally { 395 | try { 396 | if (!_iteratorNormalCompletion5 && _iterator5.return) { 397 | _iterator5.return(); 398 | } 399 | } finally { 400 | if (_didIteratorError5) { 401 | throw _iteratorError5; 402 | } 403 | } 404 | } 405 | } 406 | 407 | /** If a child is selected we need to implicitly select all the parents */ 408 | 409 | }, { 410 | key: 'childWasSelected', 411 | value: function childWasSelected() { 412 | this.checkbox.checked = true; 413 | this.selected = false; 414 | if (this.parent) { 415 | this.parent.childWasSelected(); 416 | } 417 | } 418 | }, { 419 | key: 'topicName', 420 | get: function get() { 421 | return this.label.textContent.replace(/(^\s+|\s+$)/g, ''); 422 | } 423 | }, { 424 | key: 'topicNames', 425 | get: function get() { 426 | var items = []; 427 | var _iteratorNormalCompletion6 = true; 428 | var _didIteratorError6 = false; 429 | var _iteratorError6 = undefined; 430 | 431 | try { 432 | for (var _iterator6 = this.parents[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { 433 | var parent = _step6.value; 434 | 435 | items.push(parent.topicName); 436 | } 437 | } catch (err) { 438 | _didIteratorError6 = true; 439 | _iteratorError6 = err; 440 | } finally { 441 | try { 442 | if (!_iteratorNormalCompletion6 && _iterator6.return) { 443 | _iterator6.return(); 444 | } 445 | } finally { 446 | if (_didIteratorError6) { 447 | throw _iteratorError6; 448 | } 449 | } 450 | } 451 | 452 | items.push(this.topicName); 453 | return items; 454 | } 455 | 456 | /** The presence of selected children determines whether this item is considered selected */ 457 | 458 | }, { 459 | key: 'selectedChildren', 460 | get: function get() { 461 | return this.children.reduce(function (memo, topic) { 462 | var selected = topic.selectedChildren; 463 | if (topic.selected) { 464 | selected.push(topic); 465 | } 466 | return memo.concat(selected); 467 | }, []); 468 | } 469 | }, { 470 | key: 'parents', 471 | get: function get() { 472 | if (this.parent) { 473 | return this.parent.parents.concat([this.parent]); 474 | } else { 475 | return []; 476 | } 477 | } 478 | }, { 479 | key: 'flattenedChildren', 480 | get: function get() { 481 | return this.children.reduce(function (memo, topic) { 482 | memo.push(topic); 483 | return memo.concat(topic.flattenedChildren); 484 | }, []); 485 | } 486 | }]); 487 | 488 | return Topic; 489 | }(); 490 | 491 | var MillerColumnsElement = function (_CustomElement2) { 492 | _inherits(MillerColumnsElement, _CustomElement2); 493 | 494 | function MillerColumnsElement() { 495 | _classCallCheck(this, MillerColumnsElement); 496 | 497 | var _this = _possibleConstructorReturn(this, (MillerColumnsElement.__proto__ || Object.getPrototypeOf(MillerColumnsElement)).call(this)); 498 | 499 | _this.classNames = { 500 | column: 'miller-columns__column', 501 | columnHeading: 'miller-columns__column-heading', 502 | backLink: 'govuk-back-link', 503 | columnList: 'miller-columns__column-list', 504 | columnCollapse: 'miller-columns__column--collapse', 505 | columnMedium: 'miller-columns__column--medium', 506 | columnNarrow: 'miller-columns__column--narrow', 507 | columnActive: 'miller-columns__column--active', 508 | item: 'miller-columns__item', 509 | itemParent: 'miller-columns__item--parent', 510 | itemActive: 'miller-columns__item--active', 511 | itemSelected: 'miller-columns__item--selected' 512 | }; 513 | return _this; 514 | } 515 | 516 | _createClass(MillerColumnsElement, [{ 517 | key: 'connectedCallback', 518 | value: function connectedCallback() { 519 | this.describedbyId = this.getAttribute('aria-describedby'); 520 | 521 | var source = document.getElementById(this.getAttribute('for') || ''); 522 | if (source) { 523 | this.taxonomy = new Taxonomy(Topic.fromList(source), this); 524 | this.renderTaxonomyColumn(this.taxonomy.topics, true); 525 | this.update(); 526 | if (source.parentNode) { 527 | source.parentNode.removeChild(source); 528 | } 529 | this.style.display = 'block'; 530 | } 531 | } 532 | 533 | /** Returns the element which shows the selections a user has made */ 534 | 535 | }, { 536 | key: 'renderTaxonomyColumn', 537 | 538 | 539 | /** Build and insert a column of the taxonomy */ 540 | value: function renderTaxonomyColumn(topics) { 541 | var _this2 = this; 542 | 543 | var root = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 544 | 545 | var div = document.createElement('div'); 546 | 547 | if (!root) { 548 | // Append back link 549 | var backLink = document.createElement('button'); 550 | backLink.className = this.classNames.backLink; 551 | backLink.type = 'button'; 552 | backLink.innerHTML = 'Back'; 553 | backLink.addEventListener('click', function () { 554 | if (topics[0].parent) { 555 | _this2.showCurrentColumns(topics[0].parent.parent); 556 | } 557 | }, false); 558 | div.appendChild(backLink); 559 | 560 | // Append heading 561 | var h3 = document.createElement('h3'); 562 | h3.className = this.classNames.columnHeading; 563 | var parentTopicName = topics[0].parent ? topics[0].parent.topicName : null; 564 | if (parentTopicName) { 565 | h3.innerHTML = parentTopicName; 566 | } 567 | div.appendChild(h3); 568 | } 569 | 570 | // Append list 571 | var ul = document.createElement('ul'); 572 | ul.className = this.classNames.columnList; 573 | div.className = this.classNames.column; 574 | if (root) { 575 | div.dataset.root = 'true'; 576 | } else { 577 | div.classList.add(this.classNames.columnCollapse); 578 | } 579 | div.appendChild(ul); 580 | 581 | // Append column 582 | this.appendChild(div); 583 | 584 | var _iteratorNormalCompletion7 = true; 585 | var _didIteratorError7 = false; 586 | var _iteratorError7 = undefined; 587 | 588 | try { 589 | for (var _iterator7 = topics[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) { 590 | var topic = _step7.value; 591 | 592 | this.renderTopic(topic, ul); 593 | } 594 | } catch (err) { 595 | _didIteratorError7 = true; 596 | _iteratorError7 = err; 597 | } finally { 598 | try { 599 | if (!_iteratorNormalCompletion7 && _iterator7.return) { 600 | _iterator7.return(); 601 | } 602 | } finally { 603 | if (_didIteratorError7) { 604 | throw _iteratorError7; 605 | } 606 | } 607 | } 608 | } 609 | 610 | /** Build and insert a list item for a topic */ 611 | 612 | }, { 613 | key: 'renderTopic', 614 | value: function renderTopic(topic, list) { 615 | var li = document.createElement('li'); 616 | li.classList.add(this.classNames.item); 617 | li.classList.add('govuk-checkboxes--small'); 618 | if (this.describedbyId) { 619 | li.setAttribute('aria-describedby', this.describedbyId); 620 | } 621 | 622 | var div = document.createElement('div'); 623 | div.className = 'govuk-checkboxes__item'; 624 | div.appendChild(topic.checkbox); 625 | div.appendChild(topic.label); 626 | li.appendChild(div); 627 | list.appendChild(li); 628 | this.attachEvents(li, topic); 629 | 630 | if (topic.children.length) { 631 | li.classList.add(this.classNames.itemParent); 632 | this.renderTaxonomyColumn(topic.children); 633 | } 634 | } 635 | 636 | /** Focus the miller columns item associated with a topic */ 637 | 638 | }, { 639 | key: 'focusTopic', 640 | value: function focusTopic(topic) { 641 | if (topic instanceof Topic && topic.checkbox) { 642 | var item = topic.checkbox.closest('.' + this.classNames.item); 643 | if (item instanceof HTMLElement) { 644 | item.focus(); 645 | } 646 | } 647 | } 648 | 649 | /** Sets up the event handling for a list item and a topic */ 650 | 651 | }, { 652 | key: 'attachEvents', 653 | value: function attachEvents(trigger, topic) { 654 | var _this3 = this; 655 | 656 | trigger.tabIndex = 0; 657 | trigger.addEventListener('click', function () { 658 | _this3.taxonomy.topicClicked(topic); 659 | topic.checkbox.dispatchEvent(new Event('click')); 660 | }, false); 661 | trigger.addEventListener('keydown', function (event) { 662 | switch (event.key) { 663 | case ' ': 664 | case 'Enter': 665 | event.preventDefault(); 666 | _this3.taxonomy.topicClicked(topic); 667 | topic.checkbox.dispatchEvent(new Event('click')); 668 | break; 669 | case 'ArrowUp': 670 | event.preventDefault(); 671 | if (topic.previous) { 672 | _this3.showCurrentColumns(topic.previous); 673 | _this3.focusTopic(topic.previous); 674 | } 675 | break; 676 | case 'ArrowDown': 677 | event.preventDefault(); 678 | if (topic.next) { 679 | _this3.showCurrentColumns(topic.next); 680 | _this3.focusTopic(topic.next); 681 | } 682 | break; 683 | case 'ArrowLeft': 684 | event.preventDefault(); 685 | if (topic.parent) { 686 | _this3.showCurrentColumns(topic.parent); 687 | _this3.focusTopic(topic.parent); 688 | } 689 | break; 690 | case 'ArrowRight': 691 | event.preventDefault(); 692 | if (topic.children) { 693 | _this3.showCurrentColumns(topic.children[0]); 694 | _this3.focusTopic(topic.children[0]); 695 | } 696 | break; 697 | default: 698 | return; 699 | } 700 | }, false); 701 | } 702 | 703 | /** Update this element to show a change in the state */ 704 | 705 | }, { 706 | key: 'update', 707 | value: function update() { 708 | this.showSelectedTopics(this.taxonomy.selectedTopics); 709 | this.showActiveTopic(this.taxonomy.active); 710 | this.showCurrentColumns(this.taxonomy.active); 711 | 712 | if (this.selectedElement) { 713 | this.selectedElement.update(this.taxonomy); 714 | } 715 | } 716 | 717 | /** 718 | * Utility method to swap class names over for a group of elements 719 | * Takes an array of all elements that should have a class and removes it 720 | * from any other items that have it 721 | */ 722 | 723 | }, { 724 | key: 'updateClassName', 725 | value: function updateClassName(className, items) { 726 | var currentlyWithClass = nodesToArray(this.getElementsByClassName(className)); 727 | 728 | var _iteratorNormalCompletion8 = true; 729 | var _didIteratorError8 = false; 730 | var _iteratorError8 = undefined; 731 | 732 | try { 733 | for (var _iterator8 = currentlyWithClass.concat(items)[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) { 734 | var item = _step8.value; 735 | 736 | if (!item) { 737 | continue; 738 | } 739 | 740 | if (items.indexOf(item) !== -1) { 741 | item.classList.add(className); 742 | } else { 743 | item.classList.remove(className); 744 | } 745 | } 746 | } catch (err) { 747 | _didIteratorError8 = true; 748 | _iteratorError8 = err; 749 | } finally { 750 | try { 751 | if (!_iteratorNormalCompletion8 && _iterator8.return) { 752 | _iterator8.return(); 753 | } 754 | } finally { 755 | if (_didIteratorError8) { 756 | throw _iteratorError8; 757 | } 758 | } 759 | } 760 | } 761 | 762 | /** Given an array of selected topics update the UI */ 763 | 764 | }, { 765 | key: 'showSelectedTopics', 766 | value: function showSelectedTopics(selectedTopics) { 767 | var _this4 = this; 768 | 769 | var selectedItems = selectedTopics.reduce(function (memo, child) { 770 | var _iteratorNormalCompletion9 = true; 771 | var _didIteratorError9 = false; 772 | var _iteratorError9 = undefined; 773 | 774 | try { 775 | for (var _iterator9 = child.withParents()[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) { 776 | var topic = _step9.value; 777 | 778 | var item = topic.checkbox.closest('.' + _this4.classNames.item); 779 | if (item instanceof HTMLElement) { 780 | memo.push(item); 781 | } 782 | } 783 | } catch (err) { 784 | _didIteratorError9 = true; 785 | _iteratorError9 = err; 786 | } finally { 787 | try { 788 | if (!_iteratorNormalCompletion9 && _iterator9.return) { 789 | _iterator9.return(); 790 | } 791 | } finally { 792 | if (_didIteratorError9) { 793 | throw _iteratorError9; 794 | } 795 | } 796 | } 797 | 798 | return memo; 799 | }, []); 800 | 801 | this.updateClassName(this.classNames.itemSelected, selectedItems); 802 | } 803 | 804 | /** Update the topic items for the presence (or not) of an active topic */ 805 | 806 | }, { 807 | key: 'showActiveTopic', 808 | value: function showActiveTopic(activeTopic) { 809 | var _this5 = this; 810 | 811 | var activeItems = void 0; 812 | 813 | if (!activeTopic) { 814 | activeItems = []; 815 | } else { 816 | activeItems = activeTopic.withParents().reduce(function (memo, topic) { 817 | var item = topic.checkbox.closest('.' + _this5.classNames.item); 818 | 819 | if (item instanceof HTMLElement) { 820 | memo.push(item); 821 | } 822 | 823 | return memo; 824 | }, []); 825 | } 826 | this.updateClassName(this.classNames.itemActive, activeItems); 827 | } 828 | 829 | /** Change what columns are visible based on the active (or not) topic */ 830 | 831 | }, { 832 | key: 'showCurrentColumns', 833 | value: function showCurrentColumns(activeTopic) { 834 | var allColumns = nodesToArray(this.getElementsByClassName(this.classNames.column)); 835 | var columnsToShow = activeTopic ? this.columnsForActiveTopic(activeTopic) : [allColumns[0]]; 836 | var narrowThreshold = Math.max(3, columnsToShow.length - 1); 837 | var showNarrow = columnsToShow.length > narrowThreshold; 838 | var showMedium = showNarrow && narrowThreshold === 3; 839 | var _classNames = this.classNames, 840 | collapseClass = _classNames.columnCollapse, 841 | narrowClass = _classNames.columnNarrow, 842 | mediumClass = _classNames.columnMedium, 843 | activeClass = _classNames.columnActive; 844 | var _iteratorNormalCompletion10 = true; 845 | var _didIteratorError10 = false; 846 | var _iteratorError10 = undefined; 847 | 848 | try { 849 | 850 | for (var _iterator10 = allColumns[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) { 851 | var item = _step10.value; 852 | 853 | if (!item) { 854 | continue; 855 | } 856 | 857 | item.classList.remove(activeClass); 858 | // we always want to show the root column 859 | if (item.dataset.root === 'true') { 860 | item.classList.remove(narrowClass, mediumClass); 861 | if (showMedium) { 862 | item.classList.add(mediumClass); 863 | } else if (showNarrow) { 864 | item.classList.add(narrowClass); 865 | } 866 | if (columnsToShow.length === 1) { 867 | item.classList.add(activeClass); 868 | } 869 | continue; 870 | } 871 | 872 | var index = columnsToShow.indexOf(item); 873 | 874 | if (index === -1) { 875 | // this is not a column to show 876 | item.classList.add(collapseClass); 877 | } else if (showNarrow && index < narrowThreshold) { 878 | // show this column but narrow 879 | item.classList.remove(collapseClass, narrowClass, mediumClass); 880 | if (showMedium) { 881 | item.classList.add(mediumClass); 882 | } else if (showNarrow) { 883 | item.classList.add(narrowClass); 884 | } 885 | } else { 886 | // show this column in all it's glory 887 | item.classList.remove(collapseClass, narrowClass, mediumClass); 888 | } 889 | 890 | // mark last visible column as active 891 | if (item === columnsToShow[columnsToShow.length - 1]) { 892 | item.classList.add(activeClass); 893 | } 894 | } 895 | } catch (err) { 896 | _didIteratorError10 = true; 897 | _iteratorError10 = err; 898 | } finally { 899 | try { 900 | if (!_iteratorNormalCompletion10 && _iterator10.return) { 901 | _iterator10.return(); 902 | } 903 | } finally { 904 | if (_didIteratorError10) { 905 | throw _iteratorError10; 906 | } 907 | } 908 | } 909 | } 910 | 911 | /** Determine which columns should be shown based on the active topic */ 912 | 913 | }, { 914 | key: 'columnsForActiveTopic', 915 | value: function columnsForActiveTopic(activeTopic) { 916 | if (!activeTopic) { 917 | return []; 918 | } 919 | 920 | var columnSelector = '.' + this.classNames.column; 921 | var columns = activeTopic.withParents().reduce(function (memo, topic) { 922 | var column = topic.checkbox.closest(columnSelector); 923 | if (column instanceof HTMLElement) { 924 | memo.push(column); 925 | } 926 | 927 | return memo; 928 | }, []); 929 | 930 | // we'll want to show the next column too for the next choices 931 | if (activeTopic.children.length) { 932 | var nextColumn = activeTopic.children[0].checkbox.closest(columnSelector); 933 | if (nextColumn instanceof HTMLElement) { 934 | columns.push(nextColumn); 935 | } 936 | } 937 | return columns; 938 | } 939 | }, { 940 | key: 'selectedElement', 941 | get: function get() { 942 | var selected = document.getElementById(this.getAttribute('selected') || ''); 943 | return selected instanceof MillerColumnsSelectedElement ? selected : null; 944 | } 945 | }]); 946 | 947 | return MillerColumnsElement; 948 | }(_CustomElement); 949 | 950 | var MillerColumnsSelectedElement = function (_CustomElement3) { 951 | _inherits(MillerColumnsSelectedElement, _CustomElement3); 952 | 953 | function MillerColumnsSelectedElement() { 954 | _classCallCheck(this, MillerColumnsSelectedElement); 955 | 956 | return _possibleConstructorReturn(this, (MillerColumnsSelectedElement.__proto__ || Object.getPrototypeOf(MillerColumnsSelectedElement)).call(this)); 957 | } 958 | 959 | _createClass(MillerColumnsSelectedElement, [{ 960 | key: 'connectedCallback', 961 | value: function connectedCallback() { 962 | this.list = document.createElement('ol'); 963 | this.list.className = 'miller-columns-selected__list'; 964 | this.appendChild(this.list); 965 | if (this.millerColumnsElement && this.millerColumnsElement.taxonomy) { 966 | this.update(this.millerColumnsElement.taxonomy); 967 | } 968 | } 969 | }, { 970 | key: 'update', 971 | 972 | 973 | /** Update the UI to show the selected topics */ 974 | value: function update(taxonomy) { 975 | this.taxonomy = taxonomy; 976 | var selectedTopics = taxonomy.selectedTopics; 977 | // seems simpler to nuke the list and re-build it 978 | while (this.list.lastChild) { 979 | this.list.removeChild(this.list.lastChild); 980 | } 981 | 982 | if (selectedTopics.length) { 983 | var _iteratorNormalCompletion11 = true; 984 | var _didIteratorError11 = false; 985 | var _iteratorError11 = undefined; 986 | 987 | try { 988 | for (var _iterator11 = selectedTopics[Symbol.iterator](), _step11; !(_iteratorNormalCompletion11 = (_step11 = _iterator11.next()).done); _iteratorNormalCompletion11 = true) { 989 | var topic = _step11.value; 990 | 991 | this.addSelectedTopic(topic); 992 | } 993 | } catch (err) { 994 | _didIteratorError11 = true; 995 | _iteratorError11 = err; 996 | } finally { 997 | try { 998 | if (!_iteratorNormalCompletion11 && _iterator11.return) { 999 | _iterator11.return(); 1000 | } 1001 | } finally { 1002 | if (_didIteratorError11) { 1003 | throw _iteratorError11; 1004 | } 1005 | } 1006 | } 1007 | } else { 1008 | var li = document.createElement('li'); 1009 | li.className = 'miller-columns-selected__list-item'; 1010 | li.textContent = 'No selected topics'; 1011 | this.list.appendChild(li); 1012 | } 1013 | } 1014 | }, { 1015 | key: 'addSelectedTopic', 1016 | value: function addSelectedTopic(topic) { 1017 | var li = document.createElement('li'); 1018 | li.className = 'miller-columns-selected__list-item'; 1019 | li.appendChild(this.breadcrumbsElement(topic)); 1020 | li.appendChild(this.removeTopicElement(topic)); 1021 | this.list.appendChild(li); 1022 | } 1023 | }, { 1024 | key: 'breadcrumbsElement', 1025 | value: function breadcrumbsElement(topic) { 1026 | var div = document.createElement('div'); 1027 | div.className = 'govuk-breadcrumbs'; 1028 | var ol = document.createElement('ol'); 1029 | ol.className = 'govuk-breadcrumbs__list'; 1030 | var _iteratorNormalCompletion12 = true; 1031 | var _didIteratorError12 = false; 1032 | var _iteratorError12 = undefined; 1033 | 1034 | try { 1035 | for (var _iterator12 = topic.withParents()[Symbol.iterator](), _step12; !(_iteratorNormalCompletion12 = (_step12 = _iterator12.next()).done); _iteratorNormalCompletion12 = true) { 1036 | var current = _step12.value; 1037 | 1038 | var li = document.createElement('li'); 1039 | li.className = 'govuk-breadcrumbs__list-item'; 1040 | li.textContent = current.label.textContent; 1041 | ol.appendChild(li); 1042 | } 1043 | } catch (err) { 1044 | _didIteratorError12 = true; 1045 | _iteratorError12 = err; 1046 | } finally { 1047 | try { 1048 | if (!_iteratorNormalCompletion12 && _iterator12.return) { 1049 | _iterator12.return(); 1050 | } 1051 | } finally { 1052 | if (_didIteratorError12) { 1053 | throw _iteratorError12; 1054 | } 1055 | } 1056 | } 1057 | 1058 | div.appendChild(ol); 1059 | return div; 1060 | } 1061 | }, { 1062 | key: 'removeTopicElement', 1063 | value: function removeTopicElement(topic) { 1064 | var _this7 = this; 1065 | 1066 | var button = document.createElement('button'); 1067 | button.className = 'miller-columns-selected__remove-topic'; 1068 | button.textContent = 'Remove topic'; 1069 | button.setAttribute('type', 'button'); 1070 | button.addEventListener('click', function () { 1071 | triggerEvent(button, 'remove-topic', topic); 1072 | if (_this7.taxonomy) { 1073 | _this7.taxonomy.removeTopic(topic); 1074 | } 1075 | }); 1076 | 1077 | var span = document.createElement('span'); 1078 | span.className = 'miller-columns-selected__remove-topic-name'; 1079 | span.textContent = ': ' + topic.topicName; 1080 | button.appendChild(span); 1081 | 1082 | return button; 1083 | } 1084 | }, { 1085 | key: 'millerColumnsElement', 1086 | get: function get() { 1087 | var millerColumns = document.getElementById(this.getAttribute('for') || ''); 1088 | return millerColumns instanceof MillerColumnsElement ? millerColumns : null; 1089 | } 1090 | }]); 1091 | 1092 | return MillerColumnsSelectedElement; 1093 | }(_CustomElement); 1094 | 1095 | if (!window.customElements.get('miller-columns')) { 1096 | window.MillerColumnsElement = MillerColumnsElement; 1097 | window.customElements.define('miller-columns', MillerColumnsElement); 1098 | } 1099 | 1100 | if (!window.customElements.get('miller-columns-selected')) { 1101 | window.MillerColumnsSelectedElement = MillerColumnsSelectedElement; 1102 | window.customElements.define('miller-columns-selected', MillerColumnsSelectedElement); 1103 | } 1104 | 1105 | export { MillerColumnsElement, MillerColumnsSelectedElement }; 1106 | -------------------------------------------------------------------------------- /dist/index.umd.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | if (typeof define === "function" && define.amd) { 3 | define(['exports'], factory); 4 | } else if (typeof exports !== "undefined") { 5 | factory(exports); 6 | } else { 7 | var mod = { 8 | exports: {} 9 | }; 10 | factory(mod.exports); 11 | global.index = mod.exports; 12 | } 13 | })(this, function (exports) { 14 | 'use strict'; 15 | 16 | Object.defineProperty(exports, "__esModule", { 17 | value: true 18 | }); 19 | 20 | function _possibleConstructorReturn(self, call) { 21 | if (!self) { 22 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 23 | } 24 | 25 | return call && (typeof call === "object" || typeof call === "function") ? call : self; 26 | } 27 | 28 | function _inherits(subClass, superClass) { 29 | if (typeof superClass !== "function" && superClass !== null) { 30 | throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 31 | } 32 | 33 | subClass.prototype = Object.create(superClass && superClass.prototype, { 34 | constructor: { 35 | value: subClass, 36 | enumerable: false, 37 | writable: true, 38 | configurable: true 39 | } 40 | }); 41 | if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 42 | } 43 | 44 | function _CustomElement() { 45 | return Reflect.construct(HTMLElement, [], this.__proto__.constructor); 46 | } 47 | 48 | ; 49 | Object.setPrototypeOf(_CustomElement.prototype, HTMLElement.prototype); 50 | Object.setPrototypeOf(_CustomElement, HTMLElement); 51 | 52 | var _slicedToArray = function () { 53 | function sliceIterator(arr, i) { 54 | var _arr = []; 55 | var _n = true; 56 | var _d = false; 57 | var _e = undefined; 58 | 59 | try { 60 | for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { 61 | _arr.push(_s.value); 62 | 63 | if (i && _arr.length === i) break; 64 | } 65 | } catch (err) { 66 | _d = true; 67 | _e = err; 68 | } finally { 69 | try { 70 | if (!_n && _i["return"]) _i["return"](); 71 | } finally { 72 | if (_d) throw _e; 73 | } 74 | } 75 | 76 | return _arr; 77 | } 78 | 79 | return function (arr, i) { 80 | if (Array.isArray(arr)) { 81 | return arr; 82 | } else if (Symbol.iterator in Object(arr)) { 83 | return sliceIterator(arr, i); 84 | } else { 85 | throw new TypeError("Invalid attempt to destructure non-iterable instance"); 86 | } 87 | }; 88 | }(); 89 | 90 | function _classCallCheck(instance, Constructor) { 91 | if (!(instance instanceof Constructor)) { 92 | throw new TypeError("Cannot call a class as a function"); 93 | } 94 | } 95 | 96 | var _createClass = function () { 97 | function defineProperties(target, props) { 98 | for (var i = 0; i < props.length; i++) { 99 | var descriptor = props[i]; 100 | descriptor.enumerable = descriptor.enumerable || false; 101 | descriptor.configurable = true; 102 | if ("value" in descriptor) descriptor.writable = true; 103 | Object.defineProperty(target, descriptor.key, descriptor); 104 | } 105 | } 106 | 107 | return function (Constructor, protoProps, staticProps) { 108 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 109 | if (staticProps) defineProperties(Constructor, staticProps); 110 | return Constructor; 111 | }; 112 | }(); 113 | 114 | function nodesToArray(nodes) { 115 | return Array.prototype.slice.call(nodes); 116 | } 117 | 118 | function triggerEvent(element, eventName, detail) { 119 | var params = { bubbles: true, cancelable: true, detail: detail || null }; 120 | var event = void 0; 121 | 122 | if (typeof window.CustomEvent === 'function') { 123 | event = new window.CustomEvent(eventName, params); 124 | } else { 125 | event = document.createEvent('CustomEvent'); 126 | event.initCustomEvent(eventName, params.bubbles, params.cancelable, params.detail); 127 | } 128 | 129 | element.dispatchEvent(event); 130 | } 131 | 132 | /** 133 | * This models the taxonomy shown in the miller columns and the current state 134 | * of it. 135 | * It notifies the miller columns element when it has changed state to update 136 | * the UI 137 | */ 138 | 139 | var Taxonomy = function () { 140 | function Taxonomy(topics, millerColumns) { 141 | _classCallCheck(this, Taxonomy); 142 | 143 | this.topics = topics; 144 | this.millerColumns = millerColumns; 145 | this.active = this.selectedTopics[0]; 146 | } 147 | 148 | /** fetches all the topics that are currently selected */ 149 | 150 | // At any time there is one or no active topic, the active topic determines 151 | // what part of the taxonomy is currently shown to the user (i.e which level) 152 | // if this is null a user is shown the root column 153 | 154 | 155 | _createClass(Taxonomy, [{ 156 | key: 'topicClicked', 157 | value: function topicClicked(topic) { 158 | // if this is the active topic or a parent of it we deselect 159 | if (topic === this.active || topic.parentOf(this.active)) { 160 | topic.deselect(true); 161 | this.active = topic.parent; 162 | } else if (topic.selected || topic.selectedChildren.length) { 163 | // if this is a selected topic with children we make it active to allow 164 | // picking the children 165 | if (topic.children.length) { 166 | this.active = topic; 167 | } else { 168 | // otherwise we deselect it as we know the user can't be traversing 169 | topic.deselect(true); 170 | this.active = topic.parent; 171 | } 172 | } else { 173 | // otherwise this is a new selection 174 | topic.select(); 175 | this.active = topic; 176 | } 177 | this.millerColumns.update(); 178 | } 179 | }, { 180 | key: 'removeTopic', 181 | value: function removeTopic(topic) { 182 | topic.deselect(false); 183 | // determine which topic to mark as active, if any 184 | this.active = this.determineActiveFromRemoved(topic); 185 | this.millerColumns.update(); 186 | } 187 | }, { 188 | key: 'determineActiveFromRemoved', 189 | value: function determineActiveFromRemoved(topic) { 190 | // if there is already an active item with selected children lets not 191 | // change anything 192 | if (this.active && (this.active.selected || this.active.selectedChildren.length)) { 193 | return this.active; 194 | } 195 | 196 | // see if there is a parent with selected topics, that feels like the most 197 | // natural place to end up 198 | var _iteratorNormalCompletion = true; 199 | var _didIteratorError = false; 200 | var _iteratorError = undefined; 201 | 202 | try { 203 | for (var _iterator = topic.parents.reverse()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 204 | var parent = _step.value; 205 | 206 | if (parent.selectedChildren.length) { 207 | return parent; 208 | } 209 | } 210 | 211 | // if we've still not got one we'll go for the first selected one 212 | } catch (err) { 213 | _didIteratorError = true; 214 | _iteratorError = err; 215 | } finally { 216 | try { 217 | if (!_iteratorNormalCompletion && _iterator.return) { 218 | _iterator.return(); 219 | } 220 | } finally { 221 | if (_didIteratorError) { 222 | throw _iteratorError; 223 | } 224 | } 225 | } 226 | 227 | return this.selectedTopics[0]; 228 | } 229 | }, { 230 | key: 'selectedTopics', 231 | get: function get() { 232 | return this.topics.reduce(function (memo, topic) { 233 | if (topic.selected) { 234 | memo.push(topic); 235 | } 236 | 237 | return memo.concat(topic.selectedChildren); 238 | }, []); 239 | } 240 | }, { 241 | key: 'flattenedTopics', 242 | get: function get() { 243 | return this.topics.reduce(function (memo, topic) { 244 | memo.push(topic); 245 | return memo.concat(topic.flattenedChildren); 246 | }, []); 247 | } 248 | }]); 249 | 250 | return Taxonomy; 251 | }(); 252 | 253 | var Topic = function () { 254 | _createClass(Topic, null, [{ 255 | key: 'fromList', 256 | value: function fromList(list) { 257 | var parent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 258 | 259 | var topics = []; 260 | if (!list) { 261 | return topics; 262 | } 263 | 264 | var children = Array.from(list.children); 265 | 266 | var _iteratorNormalCompletion2 = true; 267 | var _didIteratorError2 = false; 268 | var _iteratorError2 = undefined; 269 | 270 | try { 271 | for (var _iterator2 = children.entries()[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { 272 | var _step2$value = _slicedToArray(_step2.value, 2), 273 | index = _step2$value[0], 274 | item = _step2$value[1]; 275 | 276 | var label = item.querySelector('label'); 277 | var checkbox = item.querySelector('input'); 278 | if (label instanceof HTMLLabelElement && checkbox instanceof HTMLInputElement) { 279 | var childList = item.querySelector('ul'); 280 | childList = childList instanceof HTMLUListElement ? childList : null; 281 | 282 | checkbox.tabIndex = -1; 283 | 284 | var previous = index > 0 ? topics[index - 1] : null; 285 | 286 | var topic = new Topic(label, checkbox, childList, parent, previous); 287 | 288 | if (index > 0) { 289 | topics[index - 1].next = topic; 290 | } 291 | 292 | topics.push(topic); 293 | } 294 | } 295 | } catch (err) { 296 | _didIteratorError2 = true; 297 | _iteratorError2 = err; 298 | } finally { 299 | try { 300 | if (!_iteratorNormalCompletion2 && _iterator2.return) { 301 | _iterator2.return(); 302 | } 303 | } finally { 304 | if (_didIteratorError2) { 305 | throw _iteratorError2; 306 | } 307 | } 308 | } 309 | 310 | return topics; 311 | } 312 | }]); 313 | 314 | function Topic(label, checkbox, childList, parent, previous) { 315 | _classCallCheck(this, Topic); 316 | 317 | this.label = label; 318 | this.checkbox = checkbox; 319 | this.parent = parent; 320 | this.children = Topic.fromList(childList, this); 321 | this.previous = previous; 322 | 323 | if (this.checkbox.checked) { 324 | this.select(); 325 | } else { 326 | this.selected = false; 327 | } 328 | } 329 | 330 | _createClass(Topic, [{ 331 | key: 'parentOf', 332 | value: function parentOf(other) { 333 | if (!other) { 334 | return false; 335 | } 336 | 337 | var _iteratorNormalCompletion3 = true; 338 | var _didIteratorError3 = false; 339 | var _iteratorError3 = undefined; 340 | 341 | try { 342 | for (var _iterator3 = this.children[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { 343 | var topic = _step3.value; 344 | 345 | if (topic === other || topic.parentOf(other)) { 346 | return true; 347 | } 348 | } 349 | } catch (err) { 350 | _didIteratorError3 = true; 351 | _iteratorError3 = err; 352 | } finally { 353 | try { 354 | if (!_iteratorNormalCompletion3 && _iterator3.return) { 355 | _iterator3.return(); 356 | } 357 | } finally { 358 | if (_didIteratorError3) { 359 | throw _iteratorError3; 360 | } 361 | } 362 | } 363 | 364 | return false; 365 | } 366 | }, { 367 | key: 'withParents', 368 | value: function withParents() { 369 | return this.parents.concat([this]); 370 | } 371 | }, { 372 | key: 'select', 373 | value: function select() { 374 | // if already selected or a child is selected do nothing 375 | if (this.selected || this.selectedChildren.length) { 376 | return; 377 | } 378 | this.selected = true; 379 | this.checkbox.checked = true; 380 | if (this.parent) { 381 | this.parent.childWasSelected(); 382 | } 383 | } 384 | }, { 385 | key: 'deselect', 386 | value: function deselect() { 387 | var selectParent = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; 388 | 389 | // if this item is selected explicitly we can deselect it 390 | if (this.selected) { 391 | this.deselectSelfAndParents(); 392 | } else { 393 | // otherwise we need to find the selected children to start deselecting 394 | var selectedChildren = this.selectedChildren; 395 | 396 | // if we have none it's a no-op 397 | if (!selectedChildren.length) { 398 | return; 399 | } 400 | 401 | var _iteratorNormalCompletion4 = true; 402 | var _didIteratorError4 = false; 403 | var _iteratorError4 = undefined; 404 | 405 | try { 406 | for (var _iterator4 = selectedChildren[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { 407 | var child = _step4.value; 408 | 409 | child.deselect(false); 410 | } 411 | } catch (err) { 412 | _didIteratorError4 = true; 413 | _iteratorError4 = err; 414 | } finally { 415 | try { 416 | if (!_iteratorNormalCompletion4 && _iterator4.return) { 417 | _iterator4.return(); 418 | } 419 | } finally { 420 | if (_didIteratorError4) { 421 | throw _iteratorError4; 422 | } 423 | } 424 | } 425 | } 426 | 427 | if (selectParent && this.parent) { 428 | this.parent.select(); 429 | } 430 | } 431 | }, { 432 | key: 'deselectSelfAndParents', 433 | value: function deselectSelfAndParents() { 434 | var _iteratorNormalCompletion5 = true; 435 | var _didIteratorError5 = false; 436 | var _iteratorError5 = undefined; 437 | 438 | try { 439 | // loop through the parents only deselecting items that don't have other 440 | // selected children 441 | for (var _iterator5 = this.withParents().reverse()[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { 442 | var topic = _step5.value; 443 | 444 | if (topic.selectedChildren.length) { 445 | break; 446 | } else { 447 | topic.selected = false; 448 | topic.checkbox.checked = false; 449 | } 450 | } 451 | } catch (err) { 452 | _didIteratorError5 = true; 453 | _iteratorError5 = err; 454 | } finally { 455 | try { 456 | if (!_iteratorNormalCompletion5 && _iterator5.return) { 457 | _iterator5.return(); 458 | } 459 | } finally { 460 | if (_didIteratorError5) { 461 | throw _iteratorError5; 462 | } 463 | } 464 | } 465 | } 466 | }, { 467 | key: 'childWasSelected', 468 | value: function childWasSelected() { 469 | this.checkbox.checked = true; 470 | this.selected = false; 471 | if (this.parent) { 472 | this.parent.childWasSelected(); 473 | } 474 | } 475 | }, { 476 | key: 'topicName', 477 | get: function get() { 478 | return this.label.textContent.replace(/(^\s+|\s+$)/g, ''); 479 | } 480 | }, { 481 | key: 'topicNames', 482 | get: function get() { 483 | var items = []; 484 | var _iteratorNormalCompletion6 = true; 485 | var _didIteratorError6 = false; 486 | var _iteratorError6 = undefined; 487 | 488 | try { 489 | for (var _iterator6 = this.parents[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { 490 | var parent = _step6.value; 491 | 492 | items.push(parent.topicName); 493 | } 494 | } catch (err) { 495 | _didIteratorError6 = true; 496 | _iteratorError6 = err; 497 | } finally { 498 | try { 499 | if (!_iteratorNormalCompletion6 && _iterator6.return) { 500 | _iterator6.return(); 501 | } 502 | } finally { 503 | if (_didIteratorError6) { 504 | throw _iteratorError6; 505 | } 506 | } 507 | } 508 | 509 | items.push(this.topicName); 510 | return items; 511 | } 512 | }, { 513 | key: 'selectedChildren', 514 | get: function get() { 515 | return this.children.reduce(function (memo, topic) { 516 | var selected = topic.selectedChildren; 517 | if (topic.selected) { 518 | selected.push(topic); 519 | } 520 | return memo.concat(selected); 521 | }, []); 522 | } 523 | }, { 524 | key: 'parents', 525 | get: function get() { 526 | if (this.parent) { 527 | return this.parent.parents.concat([this.parent]); 528 | } else { 529 | return []; 530 | } 531 | } 532 | }, { 533 | key: 'flattenedChildren', 534 | get: function get() { 535 | return this.children.reduce(function (memo, topic) { 536 | memo.push(topic); 537 | return memo.concat(topic.flattenedChildren); 538 | }, []); 539 | } 540 | }]); 541 | 542 | return Topic; 543 | }(); 544 | 545 | var MillerColumnsElement = function (_CustomElement2) { 546 | _inherits(MillerColumnsElement, _CustomElement2); 547 | 548 | function MillerColumnsElement() { 549 | _classCallCheck(this, MillerColumnsElement); 550 | 551 | var _this = _possibleConstructorReturn(this, (MillerColumnsElement.__proto__ || Object.getPrototypeOf(MillerColumnsElement)).call(this)); 552 | 553 | _this.classNames = { 554 | column: 'miller-columns__column', 555 | columnHeading: 'miller-columns__column-heading', 556 | backLink: 'govuk-back-link', 557 | columnList: 'miller-columns__column-list', 558 | columnCollapse: 'miller-columns__column--collapse', 559 | columnMedium: 'miller-columns__column--medium', 560 | columnNarrow: 'miller-columns__column--narrow', 561 | columnActive: 'miller-columns__column--active', 562 | item: 'miller-columns__item', 563 | itemParent: 'miller-columns__item--parent', 564 | itemActive: 'miller-columns__item--active', 565 | itemSelected: 'miller-columns__item--selected' 566 | }; 567 | return _this; 568 | } 569 | 570 | _createClass(MillerColumnsElement, [{ 571 | key: 'connectedCallback', 572 | value: function connectedCallback() { 573 | this.describedbyId = this.getAttribute('aria-describedby'); 574 | 575 | var source = document.getElementById(this.getAttribute('for') || ''); 576 | if (source) { 577 | this.taxonomy = new Taxonomy(Topic.fromList(source), this); 578 | this.renderTaxonomyColumn(this.taxonomy.topics, true); 579 | this.update(); 580 | if (source.parentNode) { 581 | source.parentNode.removeChild(source); 582 | } 583 | this.style.display = 'block'; 584 | } 585 | } 586 | }, { 587 | key: 'renderTaxonomyColumn', 588 | value: function renderTaxonomyColumn(topics) { 589 | var _this2 = this; 590 | 591 | var root = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 592 | 593 | var div = document.createElement('div'); 594 | 595 | if (!root) { 596 | // Append back link 597 | var backLink = document.createElement('button'); 598 | backLink.className = this.classNames.backLink; 599 | backLink.type = 'button'; 600 | backLink.innerHTML = 'Back'; 601 | backLink.addEventListener('click', function () { 602 | if (topics[0].parent) { 603 | _this2.showCurrentColumns(topics[0].parent.parent); 604 | } 605 | }, false); 606 | div.appendChild(backLink); 607 | 608 | // Append heading 609 | var h3 = document.createElement('h3'); 610 | h3.className = this.classNames.columnHeading; 611 | var parentTopicName = topics[0].parent ? topics[0].parent.topicName : null; 612 | if (parentTopicName) { 613 | h3.innerHTML = parentTopicName; 614 | } 615 | div.appendChild(h3); 616 | } 617 | 618 | // Append list 619 | var ul = document.createElement('ul'); 620 | ul.className = this.classNames.columnList; 621 | div.className = this.classNames.column; 622 | if (root) { 623 | div.dataset.root = 'true'; 624 | } else { 625 | div.classList.add(this.classNames.columnCollapse); 626 | } 627 | div.appendChild(ul); 628 | 629 | // Append column 630 | this.appendChild(div); 631 | 632 | var _iteratorNormalCompletion7 = true; 633 | var _didIteratorError7 = false; 634 | var _iteratorError7 = undefined; 635 | 636 | try { 637 | for (var _iterator7 = topics[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) { 638 | var topic = _step7.value; 639 | 640 | this.renderTopic(topic, ul); 641 | } 642 | } catch (err) { 643 | _didIteratorError7 = true; 644 | _iteratorError7 = err; 645 | } finally { 646 | try { 647 | if (!_iteratorNormalCompletion7 && _iterator7.return) { 648 | _iterator7.return(); 649 | } 650 | } finally { 651 | if (_didIteratorError7) { 652 | throw _iteratorError7; 653 | } 654 | } 655 | } 656 | } 657 | }, { 658 | key: 'renderTopic', 659 | value: function renderTopic(topic, list) { 660 | var li = document.createElement('li'); 661 | li.classList.add(this.classNames.item); 662 | li.classList.add('govuk-checkboxes--small'); 663 | if (this.describedbyId) { 664 | li.setAttribute('aria-describedby', this.describedbyId); 665 | } 666 | 667 | var div = document.createElement('div'); 668 | div.className = 'govuk-checkboxes__item'; 669 | div.appendChild(topic.checkbox); 670 | div.appendChild(topic.label); 671 | li.appendChild(div); 672 | list.appendChild(li); 673 | this.attachEvents(li, topic); 674 | 675 | if (topic.children.length) { 676 | li.classList.add(this.classNames.itemParent); 677 | this.renderTaxonomyColumn(topic.children); 678 | } 679 | } 680 | }, { 681 | key: 'focusTopic', 682 | value: function focusTopic(topic) { 683 | if (topic instanceof Topic && topic.checkbox) { 684 | var item = topic.checkbox.closest('.' + this.classNames.item); 685 | if (item instanceof HTMLElement) { 686 | item.focus(); 687 | } 688 | } 689 | } 690 | }, { 691 | key: 'attachEvents', 692 | value: function attachEvents(trigger, topic) { 693 | var _this3 = this; 694 | 695 | trigger.tabIndex = 0; 696 | trigger.addEventListener('click', function () { 697 | _this3.taxonomy.topicClicked(topic); 698 | topic.checkbox.dispatchEvent(new Event('click')); 699 | }, false); 700 | trigger.addEventListener('keydown', function (event) { 701 | switch (event.key) { 702 | case ' ': 703 | case 'Enter': 704 | event.preventDefault(); 705 | _this3.taxonomy.topicClicked(topic); 706 | topic.checkbox.dispatchEvent(new Event('click')); 707 | break; 708 | case 'ArrowUp': 709 | event.preventDefault(); 710 | if (topic.previous) { 711 | _this3.showCurrentColumns(topic.previous); 712 | _this3.focusTopic(topic.previous); 713 | } 714 | break; 715 | case 'ArrowDown': 716 | event.preventDefault(); 717 | if (topic.next) { 718 | _this3.showCurrentColumns(topic.next); 719 | _this3.focusTopic(topic.next); 720 | } 721 | break; 722 | case 'ArrowLeft': 723 | event.preventDefault(); 724 | if (topic.parent) { 725 | _this3.showCurrentColumns(topic.parent); 726 | _this3.focusTopic(topic.parent); 727 | } 728 | break; 729 | case 'ArrowRight': 730 | event.preventDefault(); 731 | if (topic.children) { 732 | _this3.showCurrentColumns(topic.children[0]); 733 | _this3.focusTopic(topic.children[0]); 734 | } 735 | break; 736 | default: 737 | return; 738 | } 739 | }, false); 740 | } 741 | }, { 742 | key: 'update', 743 | value: function update() { 744 | this.showSelectedTopics(this.taxonomy.selectedTopics); 745 | this.showActiveTopic(this.taxonomy.active); 746 | this.showCurrentColumns(this.taxonomy.active); 747 | 748 | if (this.selectedElement) { 749 | this.selectedElement.update(this.taxonomy); 750 | } 751 | } 752 | }, { 753 | key: 'updateClassName', 754 | value: function updateClassName(className, items) { 755 | var currentlyWithClass = nodesToArray(this.getElementsByClassName(className)); 756 | 757 | var _iteratorNormalCompletion8 = true; 758 | var _didIteratorError8 = false; 759 | var _iteratorError8 = undefined; 760 | 761 | try { 762 | for (var _iterator8 = currentlyWithClass.concat(items)[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) { 763 | var item = _step8.value; 764 | 765 | if (!item) { 766 | continue; 767 | } 768 | 769 | if (items.indexOf(item) !== -1) { 770 | item.classList.add(className); 771 | } else { 772 | item.classList.remove(className); 773 | } 774 | } 775 | } catch (err) { 776 | _didIteratorError8 = true; 777 | _iteratorError8 = err; 778 | } finally { 779 | try { 780 | if (!_iteratorNormalCompletion8 && _iterator8.return) { 781 | _iterator8.return(); 782 | } 783 | } finally { 784 | if (_didIteratorError8) { 785 | throw _iteratorError8; 786 | } 787 | } 788 | } 789 | } 790 | }, { 791 | key: 'showSelectedTopics', 792 | value: function showSelectedTopics(selectedTopics) { 793 | var _this4 = this; 794 | 795 | var selectedItems = selectedTopics.reduce(function (memo, child) { 796 | var _iteratorNormalCompletion9 = true; 797 | var _didIteratorError9 = false; 798 | var _iteratorError9 = undefined; 799 | 800 | try { 801 | for (var _iterator9 = child.withParents()[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) { 802 | var topic = _step9.value; 803 | 804 | var item = topic.checkbox.closest('.' + _this4.classNames.item); 805 | if (item instanceof HTMLElement) { 806 | memo.push(item); 807 | } 808 | } 809 | } catch (err) { 810 | _didIteratorError9 = true; 811 | _iteratorError9 = err; 812 | } finally { 813 | try { 814 | if (!_iteratorNormalCompletion9 && _iterator9.return) { 815 | _iterator9.return(); 816 | } 817 | } finally { 818 | if (_didIteratorError9) { 819 | throw _iteratorError9; 820 | } 821 | } 822 | } 823 | 824 | return memo; 825 | }, []); 826 | 827 | this.updateClassName(this.classNames.itemSelected, selectedItems); 828 | } 829 | }, { 830 | key: 'showActiveTopic', 831 | value: function showActiveTopic(activeTopic) { 832 | var _this5 = this; 833 | 834 | var activeItems = void 0; 835 | 836 | if (!activeTopic) { 837 | activeItems = []; 838 | } else { 839 | activeItems = activeTopic.withParents().reduce(function (memo, topic) { 840 | var item = topic.checkbox.closest('.' + _this5.classNames.item); 841 | 842 | if (item instanceof HTMLElement) { 843 | memo.push(item); 844 | } 845 | 846 | return memo; 847 | }, []); 848 | } 849 | this.updateClassName(this.classNames.itemActive, activeItems); 850 | } 851 | }, { 852 | key: 'showCurrentColumns', 853 | value: function showCurrentColumns(activeTopic) { 854 | var allColumns = nodesToArray(this.getElementsByClassName(this.classNames.column)); 855 | var columnsToShow = activeTopic ? this.columnsForActiveTopic(activeTopic) : [allColumns[0]]; 856 | var narrowThreshold = Math.max(3, columnsToShow.length - 1); 857 | var showNarrow = columnsToShow.length > narrowThreshold; 858 | var showMedium = showNarrow && narrowThreshold === 3; 859 | var _classNames = this.classNames, 860 | collapseClass = _classNames.columnCollapse, 861 | narrowClass = _classNames.columnNarrow, 862 | mediumClass = _classNames.columnMedium, 863 | activeClass = _classNames.columnActive; 864 | var _iteratorNormalCompletion10 = true; 865 | var _didIteratorError10 = false; 866 | var _iteratorError10 = undefined; 867 | 868 | try { 869 | 870 | for (var _iterator10 = allColumns[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) { 871 | var item = _step10.value; 872 | 873 | if (!item) { 874 | continue; 875 | } 876 | 877 | item.classList.remove(activeClass); 878 | // we always want to show the root column 879 | if (item.dataset.root === 'true') { 880 | item.classList.remove(narrowClass, mediumClass); 881 | if (showMedium) { 882 | item.classList.add(mediumClass); 883 | } else if (showNarrow) { 884 | item.classList.add(narrowClass); 885 | } 886 | if (columnsToShow.length === 1) { 887 | item.classList.add(activeClass); 888 | } 889 | continue; 890 | } 891 | 892 | var index = columnsToShow.indexOf(item); 893 | 894 | if (index === -1) { 895 | // this is not a column to show 896 | item.classList.add(collapseClass); 897 | } else if (showNarrow && index < narrowThreshold) { 898 | // show this column but narrow 899 | item.classList.remove(collapseClass, narrowClass, mediumClass); 900 | if (showMedium) { 901 | item.classList.add(mediumClass); 902 | } else if (showNarrow) { 903 | item.classList.add(narrowClass); 904 | } 905 | } else { 906 | // show this column in all it's glory 907 | item.classList.remove(collapseClass, narrowClass, mediumClass); 908 | } 909 | 910 | // mark last visible column as active 911 | if (item === columnsToShow[columnsToShow.length - 1]) { 912 | item.classList.add(activeClass); 913 | } 914 | } 915 | } catch (err) { 916 | _didIteratorError10 = true; 917 | _iteratorError10 = err; 918 | } finally { 919 | try { 920 | if (!_iteratorNormalCompletion10 && _iterator10.return) { 921 | _iterator10.return(); 922 | } 923 | } finally { 924 | if (_didIteratorError10) { 925 | throw _iteratorError10; 926 | } 927 | } 928 | } 929 | } 930 | }, { 931 | key: 'columnsForActiveTopic', 932 | value: function columnsForActiveTopic(activeTopic) { 933 | if (!activeTopic) { 934 | return []; 935 | } 936 | 937 | var columnSelector = '.' + this.classNames.column; 938 | var columns = activeTopic.withParents().reduce(function (memo, topic) { 939 | var column = topic.checkbox.closest(columnSelector); 940 | if (column instanceof HTMLElement) { 941 | memo.push(column); 942 | } 943 | 944 | return memo; 945 | }, []); 946 | 947 | // we'll want to show the next column too for the next choices 948 | if (activeTopic.children.length) { 949 | var nextColumn = activeTopic.children[0].checkbox.closest(columnSelector); 950 | if (nextColumn instanceof HTMLElement) { 951 | columns.push(nextColumn); 952 | } 953 | } 954 | return columns; 955 | } 956 | }, { 957 | key: 'selectedElement', 958 | get: function get() { 959 | var selected = document.getElementById(this.getAttribute('selected') || ''); 960 | return selected instanceof MillerColumnsSelectedElement ? selected : null; 961 | } 962 | }]); 963 | 964 | return MillerColumnsElement; 965 | }(_CustomElement); 966 | 967 | var MillerColumnsSelectedElement = function (_CustomElement3) { 968 | _inherits(MillerColumnsSelectedElement, _CustomElement3); 969 | 970 | function MillerColumnsSelectedElement() { 971 | _classCallCheck(this, MillerColumnsSelectedElement); 972 | 973 | return _possibleConstructorReturn(this, (MillerColumnsSelectedElement.__proto__ || Object.getPrototypeOf(MillerColumnsSelectedElement)).call(this)); 974 | } 975 | 976 | _createClass(MillerColumnsSelectedElement, [{ 977 | key: 'connectedCallback', 978 | value: function connectedCallback() { 979 | this.list = document.createElement('ol'); 980 | this.list.className = 'miller-columns-selected__list'; 981 | this.appendChild(this.list); 982 | if (this.millerColumnsElement && this.millerColumnsElement.taxonomy) { 983 | this.update(this.millerColumnsElement.taxonomy); 984 | } 985 | } 986 | }, { 987 | key: 'update', 988 | value: function update(taxonomy) { 989 | this.taxonomy = taxonomy; 990 | var selectedTopics = taxonomy.selectedTopics; 991 | // seems simpler to nuke the list and re-build it 992 | while (this.list.lastChild) { 993 | this.list.removeChild(this.list.lastChild); 994 | } 995 | 996 | if (selectedTopics.length) { 997 | var _iteratorNormalCompletion11 = true; 998 | var _didIteratorError11 = false; 999 | var _iteratorError11 = undefined; 1000 | 1001 | try { 1002 | for (var _iterator11 = selectedTopics[Symbol.iterator](), _step11; !(_iteratorNormalCompletion11 = (_step11 = _iterator11.next()).done); _iteratorNormalCompletion11 = true) { 1003 | var topic = _step11.value; 1004 | 1005 | this.addSelectedTopic(topic); 1006 | } 1007 | } catch (err) { 1008 | _didIteratorError11 = true; 1009 | _iteratorError11 = err; 1010 | } finally { 1011 | try { 1012 | if (!_iteratorNormalCompletion11 && _iterator11.return) { 1013 | _iterator11.return(); 1014 | } 1015 | } finally { 1016 | if (_didIteratorError11) { 1017 | throw _iteratorError11; 1018 | } 1019 | } 1020 | } 1021 | } else { 1022 | var li = document.createElement('li'); 1023 | li.className = 'miller-columns-selected__list-item'; 1024 | li.textContent = 'No selected topics'; 1025 | this.list.appendChild(li); 1026 | } 1027 | } 1028 | }, { 1029 | key: 'addSelectedTopic', 1030 | value: function addSelectedTopic(topic) { 1031 | var li = document.createElement('li'); 1032 | li.className = 'miller-columns-selected__list-item'; 1033 | li.appendChild(this.breadcrumbsElement(topic)); 1034 | li.appendChild(this.removeTopicElement(topic)); 1035 | this.list.appendChild(li); 1036 | } 1037 | }, { 1038 | key: 'breadcrumbsElement', 1039 | value: function breadcrumbsElement(topic) { 1040 | var div = document.createElement('div'); 1041 | div.className = 'govuk-breadcrumbs'; 1042 | var ol = document.createElement('ol'); 1043 | ol.className = 'govuk-breadcrumbs__list'; 1044 | var _iteratorNormalCompletion12 = true; 1045 | var _didIteratorError12 = false; 1046 | var _iteratorError12 = undefined; 1047 | 1048 | try { 1049 | for (var _iterator12 = topic.withParents()[Symbol.iterator](), _step12; !(_iteratorNormalCompletion12 = (_step12 = _iterator12.next()).done); _iteratorNormalCompletion12 = true) { 1050 | var current = _step12.value; 1051 | 1052 | var li = document.createElement('li'); 1053 | li.className = 'govuk-breadcrumbs__list-item'; 1054 | li.textContent = current.label.textContent; 1055 | ol.appendChild(li); 1056 | } 1057 | } catch (err) { 1058 | _didIteratorError12 = true; 1059 | _iteratorError12 = err; 1060 | } finally { 1061 | try { 1062 | if (!_iteratorNormalCompletion12 && _iterator12.return) { 1063 | _iterator12.return(); 1064 | } 1065 | } finally { 1066 | if (_didIteratorError12) { 1067 | throw _iteratorError12; 1068 | } 1069 | } 1070 | } 1071 | 1072 | div.appendChild(ol); 1073 | return div; 1074 | } 1075 | }, { 1076 | key: 'removeTopicElement', 1077 | value: function removeTopicElement(topic) { 1078 | var _this7 = this; 1079 | 1080 | var button = document.createElement('button'); 1081 | button.className = 'miller-columns-selected__remove-topic'; 1082 | button.textContent = 'Remove topic'; 1083 | button.setAttribute('type', 'button'); 1084 | button.addEventListener('click', function () { 1085 | triggerEvent(button, 'remove-topic', topic); 1086 | if (_this7.taxonomy) { 1087 | _this7.taxonomy.removeTopic(topic); 1088 | } 1089 | }); 1090 | 1091 | var span = document.createElement('span'); 1092 | span.className = 'miller-columns-selected__remove-topic-name'; 1093 | span.textContent = ': ' + topic.topicName; 1094 | button.appendChild(span); 1095 | 1096 | return button; 1097 | } 1098 | }, { 1099 | key: 'millerColumnsElement', 1100 | get: function get() { 1101 | var millerColumns = document.getElementById(this.getAttribute('for') || ''); 1102 | return millerColumns instanceof MillerColumnsElement ? millerColumns : null; 1103 | } 1104 | }]); 1105 | 1106 | return MillerColumnsSelectedElement; 1107 | }(_CustomElement); 1108 | 1109 | if (!window.customElements.get('miller-columns')) { 1110 | window.MillerColumnsElement = MillerColumnsElement; 1111 | window.customElements.define('miller-columns', MillerColumnsElement); 1112 | } 1113 | 1114 | if (!window.customElements.get('miller-columns-selected')) { 1115 | window.MillerColumnsSelectedElement = MillerColumnsSelectedElement; 1116 | window.customElements.define('miller-columns-selected', MillerColumnsSelectedElement); 1117 | } 1118 | 1119 | exports.MillerColumnsElement = MillerColumnsElement; 1120 | exports.MillerColumnsSelectedElement = MillerColumnsSelectedElement; 1121 | }); 1122 | -------------------------------------------------------------------------------- /dist/miller-columns.css: -------------------------------------------------------------------------------- 1 | .miller-columns-selected { 2 | margin-bottom: 20px; 3 | display: block; } 4 | @media (min-width: 40.0625em) { 5 | .miller-columns-selected { 6 | margin-bottom: 30px; } } 7 | .miller-columns-selected .govuk-breadcrumbs { 8 | display: block; 9 | margin: 0; 10 | margin-right: 110px; } 11 | .miller-columns-selected .govuk-breadcrumbs__list-item { 12 | margin-bottom: 0; } 13 | 14 | .miller-columns-selected__list { 15 | font-family: "GDS Transport", arial, sans-serif; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | font-weight: 400; 19 | font-size: 14px; 20 | font-size: 0.875rem; 21 | line-height: 1.14286; 22 | color: #0b0c0c; 23 | margin: 0; 24 | padding: 0; 25 | list-style-type: none; } 26 | @media print { 27 | .miller-columns-selected__list { 28 | font-family: sans-serif; } } 29 | @media (min-width: 40.0625em) { 30 | .miller-columns-selected__list { 31 | font-size: 16px; 32 | font-size: 1rem; 33 | line-height: 1.25; } } 34 | @media print { 35 | .miller-columns-selected__list { 36 | font-size: 14pt; 37 | line-height: 1.2; } } 38 | @media print { 39 | .miller-columns-selected__list { 40 | color: #000000; } } 41 | 42 | .miller-columns-selected__list-item { 43 | position: relative; 44 | margin-bottom: 0; 45 | padding: 10px 0; 46 | border-top: 1px solid #b1b4b6; } 47 | .miller-columns-selected__list-item:last-child { 48 | border-bottom: 1px solid #b1b4b6; } 49 | 50 | .miller-columns-selected__remove-topic { 51 | font-family: "GDS Transport", arial, sans-serif; 52 | -webkit-font-smoothing: antialiased; 53 | -moz-osx-font-smoothing: grayscale; 54 | text-decoration: underline; 55 | text-decoration-thickness: max(1px, .0625rem); 56 | text-underline-offset: 0.1em; 57 | font-family: "GDS Transport", arial, sans-serif; 58 | -webkit-font-smoothing: antialiased; 59 | -moz-osx-font-smoothing: grayscale; 60 | font-weight: 400; 61 | font-size: 14px; 62 | font-size: 0.875rem; 63 | line-height: 1.14286; 64 | position: absolute; 65 | top: 10px; 66 | right: 0; 67 | margin: 0; 68 | padding: 0; 69 | border: 0; 70 | color: #1d70b8; 71 | background: transparent; 72 | cursor: pointer; } 73 | @media print { 74 | .miller-columns-selected__remove-topic { 75 | font-family: sans-serif; } } 76 | .miller-columns-selected__remove-topic:hover { 77 | text-decoration-thickness: max(3px, .1875rem, .12em); } 78 | .miller-columns-selected__remove-topic:focus { 79 | outline: 3px solid transparent; 80 | color: #0b0c0c; 81 | background-color: #ffdd00; 82 | box-shadow: 0 -2px #ffdd00, 0 4px #0b0c0c; 83 | text-decoration: none; } 84 | .miller-columns-selected__remove-topic:link { 85 | color: #1d70b8; } 86 | .miller-columns-selected__remove-topic:visited { 87 | color: #1d70b8; } 88 | .miller-columns-selected__remove-topic:hover { 89 | color: #003078; } 90 | .miller-columns-selected__remove-topic:active { 91 | color: #0b0c0c; } 92 | .miller-columns-selected__remove-topic:focus { 93 | color: #0b0c0c; } 94 | @media print { 95 | .miller-columns-selected__remove-topic { 96 | font-family: sans-serif; } } 97 | @media (min-width: 40.0625em) { 98 | .miller-columns-selected__remove-topic { 99 | font-size: 16px; 100 | font-size: 1rem; 101 | line-height: 1.25; } } 102 | @media print { 103 | .miller-columns-selected__remove-topic { 104 | font-size: 14pt; 105 | line-height: 1.2; } } 106 | .miller-columns-selected__remove-topic:focus { 107 | outline: 3px solid transparent; 108 | color: #0b0c0c; 109 | background-color: #ffdd00; 110 | box-shadow: 0 -2px #ffdd00, 0 4px #0b0c0c; 111 | text-decoration: none; } 112 | 113 | .miller-columns-selected__remove-topic-name { 114 | position: absolute !important; 115 | width: 1px !important; 116 | height: 1px !important; 117 | margin: 0 !important; 118 | padding: 0 !important; 119 | overflow: hidden !important; 120 | clip: rect(0 0 0 0) !important; 121 | -webkit-clip-path: inset(50%) !important; 122 | clip-path: inset(50%) !important; 123 | border: 0 !important; 124 | white-space: nowrap !important; } 125 | 126 | .js-enabled .miller-columns { 127 | display: none; } 128 | .js-enabled .miller-columns .govuk-checkboxes__input, 129 | .js-enabled .miller-columns .govuk-checkboxes__label { 130 | pointer-events: none; } 131 | 132 | .miller-columns { 133 | display: block; 134 | width: 100%; 135 | height: 100%; 136 | margin-bottom: 20px; 137 | outline: 0; 138 | font-size: 0; 139 | white-space: nowrap; } 140 | @media (min-width: 40.0625em) { 141 | .miller-columns { 142 | margin-bottom: 30px; } } 143 | 144 | .miller-columns__column { 145 | display: none; 146 | width: 100%; 147 | height: 100%; 148 | vertical-align: top; 149 | white-space: normal; 150 | transition-duration: 400ms; 151 | transition-property: width; } 152 | .miller-columns__column.miller-columns__column--active { 153 | display: inline-block; } 154 | @media (min-width: 40.0625em) { 155 | .miller-columns__column { 156 | display: inline-block; 157 | width: 33.3%; 158 | border-right: 1px solid #b1b4b6; } } 159 | 160 | .miller-columns__column--narrow { 161 | width: 16.6%; 162 | overflow-x: hidden; } 163 | .miller-columns__column--narrow .miller-columns__item, 164 | .miller-columns__column--narrow .miller-columns__item label { 165 | white-space: nowrap; } 166 | .miller-columns__column--narrow .miller-columns__item--parent:after { 167 | display: none; } 168 | 169 | .miller-columns__column--medium { 170 | width: 22.2%; 171 | overflow-x: hidden; } 172 | .miller-columns__column--medium .miller-columns__item, 173 | .miller-columns__column--medium .miller-columns__item label { 174 | white-space: nowrap; } 175 | .miller-columns__column--medium .miller-columns__item--parent:after { 176 | display: none; } 177 | 178 | .miller-columns__column--collapse { 179 | display: none; } 180 | 181 | .miller-columns__column-heading { 182 | color: #0b0c0c; 183 | font-family: "GDS Transport", arial, sans-serif; 184 | -webkit-font-smoothing: antialiased; 185 | -moz-osx-font-smoothing: grayscale; 186 | font-weight: 700; 187 | font-size: 16px; 188 | font-size: 1rem; 189 | line-height: 1.25; 190 | margin-bottom: 15px; 191 | margin-top: 0; 192 | padding: 0; } 193 | @media print { 194 | .miller-columns__column-heading { 195 | color: #000000; } } 196 | @media print { 197 | .miller-columns__column-heading { 198 | font-family: sans-serif; } } 199 | @media (min-width: 40.0625em) { 200 | .miller-columns__column-heading { 201 | font-size: 19px; 202 | font-size: 1.1875rem; 203 | line-height: 1.31579; } } 204 | @media print { 205 | .miller-columns__column-heading { 206 | font-size: 14pt; 207 | line-height: 1.15; } } 208 | @media (min-width: 40.0625em) { 209 | .miller-columns__column-heading { 210 | display: none; } } 211 | 212 | .miller-columns__column-list { 213 | margin: 0; 214 | padding: 0; } 215 | 216 | .miller-columns__item { 217 | position: relative; 218 | margin-bottom: 1px; 219 | padding: 2px 9px; 220 | list-style: none; 221 | color: #0b0c0c; 222 | cursor: pointer; 223 | font-family: "GDS Transport", arial, sans-serif; 224 | -webkit-font-smoothing: antialiased; 225 | -moz-osx-font-smoothing: grayscale; 226 | font-weight: 400; 227 | font-size: 14px; 228 | font-size: 0.875rem; 229 | line-height: 1.14286; } 230 | @media print { 231 | .miller-columns__item { 232 | font-family: sans-serif; } } 233 | @media (min-width: 40.0625em) { 234 | .miller-columns__item { 235 | font-size: 16px; 236 | font-size: 1rem; 237 | line-height: 1.25; } } 238 | @media print { 239 | .miller-columns__item { 240 | font-size: 14pt; 241 | line-height: 1.2; } } 242 | .miller-columns__item:hover { 243 | color: #0b0c0c; 244 | background-color: #b1b4b6; } 245 | .miller-columns__item:focus { 246 | outline: 3px solid transparent; 247 | color: #0b0c0c; 248 | background-color: #ffdd00; 249 | box-shadow: 0 -2px #ffdd00, 0 4px #0b0c0c; 250 | text-decoration: none; } 251 | .miller-columns__item .govuk-checkboxes__item { 252 | float: none; } 253 | .miller-columns__item .govuk-checkboxes__label { 254 | font-family: "GDS Transport", arial, sans-serif; 255 | -webkit-font-smoothing: antialiased; 256 | -moz-osx-font-smoothing: grayscale; 257 | font-weight: 400; 258 | font-size: 14px; 259 | font-size: 0.875rem; 260 | line-height: 1.14286; 261 | padding: 12px 15px 10px 1px; } 262 | @media print { 263 | .miller-columns__item .govuk-checkboxes__label { 264 | font-family: sans-serif; } } 265 | @media (min-width: 40.0625em) { 266 | .miller-columns__item .govuk-checkboxes__label { 267 | font-size: 16px; 268 | font-size: 1rem; 269 | line-height: 1.25; } } 270 | @media print { 271 | .miller-columns__item .govuk-checkboxes__label { 272 | font-size: 14pt; 273 | line-height: 1.2; } } 274 | .miller-columns__item .govuk-checkboxes__item:hover .govuk-checkboxes__input:focus + .govuk-checkboxes__label::before, 275 | .miller-columns__item .govuk-checkboxes__item:hover .govuk-checkboxes__input:not(:disabled) + .govuk-checkboxes__label::before { 276 | box-shadow: none; } 277 | 278 | .miller-columns__item--parent:after { 279 | content: "\203A" / ""; 280 | position: absolute; 281 | top: 50%; 282 | right: 5px; 283 | margin-top: -17px; 284 | float: right; 285 | font-size: 24px; } 286 | 287 | .miller-columns__item--selected, 288 | .miller-columns__item--selected:hover { 289 | color: #ffffff; 290 | background-color: #505a5f; } 291 | .miller-columns__item--selected .govuk-checkboxes__label, 292 | .miller-columns__item--selected:hover .govuk-checkboxes__label { 293 | color: #ffffff; } 294 | 295 | .miller-columns__item--selected:focus { 296 | outline: 3px solid transparent; 297 | color: #0b0c0c; 298 | background-color: #ffdd00; 299 | box-shadow: 0 -2px #ffdd00, 0 4px #0b0c0c; 300 | text-decoration: none; } 301 | .miller-columns__item--selected:focus .govuk-checkboxes__label { 302 | color: #0b0c0c; } 303 | 304 | .miller-columns__item--active, 305 | .miller-columns__item--active:hover { 306 | color: #ffffff; 307 | background-color: #1d70b8; 308 | box-shadow: none; } 309 | .miller-columns__item--active .govuk-checkboxes__label, 310 | .miller-columns__item--active:hover .govuk-checkboxes__label { 311 | color: #ffffff; } 312 | 313 | .miller-columns__item--active:focus { 314 | outline: 3px solid transparent; 315 | color: #0b0c0c; 316 | background-color: #ffdd00; 317 | box-shadow: 0 -2px #ffdd00, 0 4px #0b0c0c; 318 | text-decoration: none; } 319 | .miller-columns__item--active:focus .govuk-checkboxes__label { 320 | color: #0b0c0c; } 321 | 322 | .miller-columns .govuk-list .govuk-list { 323 | margin-left: 30px; } 324 | 325 | .miller-columns .govuk-back-link { 326 | margin-bottom: 20px; 327 | padding-top: 0; 328 | padding-right: 0; 329 | padding-bottom: 0; 330 | border: 0; 331 | background: transparent; 332 | text-decoration: underline; } 333 | @media (min-width: 40.0625em) { 334 | .miller-columns .govuk-back-link { 335 | display: none; } } 336 | -------------------------------------------------------------------------------- /examples.scss: -------------------------------------------------------------------------------- 1 | // Don't include the font since it needs to be served from the node modules 2 | // directory it'll only 404 on apps. 3 | $govuk-include-default-font-face: false; 4 | 5 | // Core styles used for the example pages 6 | @import "node_modules/govuk-frontend/dist/govuk/base"; 7 | @import "node_modules/govuk-frontend/dist/govuk/objects/all"; 8 | @import "node_modules/govuk-frontend/dist/govuk/core/all"; 9 | @import "node_modules/govuk-frontend/dist/govuk/components/checkboxes/checkboxes"; 10 | @import "node_modules/govuk-frontend/dist/govuk/components/back-link/back-link"; 11 | @import "node_modules/govuk-frontend/dist/govuk/components/breadcrumbs/breadcrumbs"; 12 | 13 | // Component specific styles 14 | @import "miller-columns"; 15 | -------------------------------------------------------------------------------- /examples/miller-columns-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | govuk-miller-columns examples 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | function nodesToArray(nodes: NodeList | HTMLCollection): Array { 4 | return Array.prototype.slice.call(nodes) 5 | } 6 | 7 | function triggerEvent(element: HTMLElement, eventName: string, detail: Object) { 8 | const params = {bubbles: true, cancelable: true, detail: detail || null} 9 | let event 10 | 11 | if (typeof window.CustomEvent === 'function') { 12 | event = new window.CustomEvent(eventName, params) 13 | } else { 14 | event = document.createEvent('CustomEvent') 15 | event.initCustomEvent(eventName, params.bubbles, params.cancelable, params.detail) 16 | } 17 | 18 | element.dispatchEvent(event) 19 | } 20 | 21 | /** 22 | * This models the taxonomy shown in the miller columns and the current state 23 | * of it. 24 | * It notifies the miller columns element when it has changed state to update 25 | * the UI 26 | */ 27 | class Taxonomy { 28 | topics: Array 29 | millerColumns: MillerColumnsElement 30 | // At any time there is one or no active topic, the active topic determines 31 | // what part of the taxonomy is currently shown to the user (i.e which level) 32 | // if this is null a user is shown the root column 33 | active: ?Topic 34 | 35 | constructor(topics: Array, millerColumns: MillerColumnsElement) { 36 | this.topics = topics 37 | this.millerColumns = millerColumns 38 | this.active = this.selectedTopics[0] 39 | } 40 | 41 | /** fetches all the topics that are currently selected */ 42 | get selectedTopics(): Array { 43 | return this.topics.reduce((memo, topic) => { 44 | if (topic.selected) { 45 | memo.push(topic) 46 | } 47 | 48 | return memo.concat(topic.selectedChildren) 49 | }, []) 50 | } 51 | 52 | get flattenedTopics(): Array { 53 | return this.topics.reduce((memo, topic) => { 54 | memo.push(topic) 55 | return memo.concat(topic.flattenedChildren) 56 | }, []) 57 | } 58 | 59 | /** Handler for a topic in the miller columns being clicked */ 60 | topicClicked(topic: Topic) { 61 | // if this is the active topic or a parent of it we deselect 62 | if (topic === this.active || topic.parentOf(this.active)) { 63 | topic.deselect(true) 64 | this.active = topic.parent 65 | } else if (topic.selected || topic.selectedChildren.length) { 66 | // if this is a selected topic with children we make it active to allow 67 | // picking the children 68 | if (topic.children.length) { 69 | this.active = topic 70 | } else { 71 | // otherwise we deselect it as we know the user can't be traversing 72 | topic.deselect(true) 73 | this.active = topic.parent 74 | } 75 | } else { 76 | // otherwise this is a new selection 77 | topic.select() 78 | this.active = topic 79 | } 80 | this.millerColumns.update() 81 | } 82 | 83 | /** Handler for when a topic is removed via the selected element */ 84 | removeTopic(topic: Topic) { 85 | topic.deselect(false) 86 | // determine which topic to mark as active, if any 87 | this.active = this.determineActiveFromRemoved(topic) 88 | this.millerColumns.update() 89 | } 90 | 91 | /** Calculate most relevant topic to show user after they've removed a topic */ 92 | determineActiveFromRemoved(topic: Topic): ?Topic { 93 | // if there is already an active item with selected children lets not 94 | // change anything 95 | if (this.active && (this.active.selected || this.active.selectedChildren.length)) { 96 | return this.active 97 | } 98 | 99 | // see if there is a parent with selected topics, that feels like the most 100 | // natural place to end up 101 | for (const parent of topic.parents.reverse()) { 102 | if (parent.selectedChildren.length) { 103 | return parent 104 | } 105 | } 106 | 107 | // if we've still not got one we'll go for the first selected one 108 | return this.selectedTopics[0] 109 | } 110 | } 111 | 112 | /** 113 | * Represents a single topic in the taxonomy and knows whether it is currently 114 | * selected or not 115 | */ 116 | class Topic { 117 | static fromList(list: ?HTMLElement, parent: ?Topic = null) { 118 | const topics = [] 119 | if (!list) { 120 | return topics 121 | } 122 | 123 | const children = Array.from(list.children) 124 | 125 | for (const [index, item] of children.entries()) { 126 | const label = item.querySelector('label') 127 | const checkbox = item.querySelector('input') 128 | if (label instanceof HTMLLabelElement && checkbox instanceof HTMLInputElement) { 129 | let childList = item.querySelector('ul') 130 | childList = childList instanceof HTMLUListElement ? childList : null 131 | 132 | checkbox.tabIndex = -1 133 | 134 | const previous = index > 0 ? topics[index - 1] : null 135 | 136 | const topic = new Topic(label, checkbox, childList, parent, previous) 137 | 138 | if (index > 0) { 139 | topics[index - 1].next = topic 140 | } 141 | 142 | topics.push(topic) 143 | } 144 | } 145 | 146 | return topics 147 | } 148 | 149 | label: HTMLLabelElement 150 | checkbox: HTMLInputElement 151 | children: Array 152 | parent: ?Topic 153 | next: ?Topic 154 | previous: ?Topic 155 | // Whether this topic is selected, we only allow one item in a branch of the 156 | // taxonomy to be selected. 157 | // E.g. given education > school > 6th form only one of these can be selected 158 | // at a time and the parents are implicity selected from it 159 | selected: boolean 160 | 161 | constructor( 162 | label: HTMLLabelElement, 163 | checkbox: HTMLInputElement, 164 | childList: ?HTMLUListElement, 165 | parent: ?Topic, 166 | previous: ?Topic 167 | ) { 168 | this.label = label 169 | this.checkbox = checkbox 170 | this.parent = parent 171 | this.children = Topic.fromList(childList, this) 172 | this.previous = previous 173 | 174 | if (this.checkbox.checked) { 175 | this.select() 176 | } else { 177 | this.selected = false 178 | } 179 | } 180 | 181 | get topicName(): string { 182 | return this.label.textContent.replace(/(^\s+|\s+$)/g, '') 183 | } 184 | 185 | get topicNames(): Array { 186 | const items = [] 187 | for (const parent of this.parents) { 188 | items.push(parent.topicName) 189 | } 190 | items.push(this.topicName) 191 | return items 192 | } 193 | 194 | /** The presence of selected children determines whether this item is considered selected */ 195 | get selectedChildren(): Array { 196 | return this.children.reduce((memo, topic) => { 197 | const selected = topic.selectedChildren 198 | if (topic.selected) { 199 | selected.push(topic) 200 | } 201 | return memo.concat(selected) 202 | }, []) 203 | } 204 | 205 | get parents(): Array { 206 | if (this.parent) { 207 | return this.parent.parents.concat([this.parent]) 208 | } else { 209 | return [] 210 | } 211 | } 212 | 213 | get flattenedChildren(): Array { 214 | return this.children.reduce((memo, topic) => { 215 | memo.push(topic) 216 | return memo.concat(topic.flattenedChildren) 217 | }, []) 218 | } 219 | 220 | /** Whether this topic is the parent of a different one */ 221 | parentOf(other: ?Topic): boolean { 222 | if (!other) { 223 | return false 224 | } 225 | 226 | for (const topic of this.children) { 227 | if (topic === other || topic.parentOf(other)) { 228 | return true 229 | } 230 | } 231 | 232 | return false 233 | } 234 | 235 | withParents(): Array { 236 | return this.parents.concat([this]) 237 | } 238 | 239 | /** Attempts to select this topic assuming it's not alrerady selected or has selected children */ 240 | select() { 241 | // if already selected or a child is selected do nothing 242 | if (this.selected || this.selectedChildren.length) { 243 | return 244 | } 245 | this.selected = true 246 | this.checkbox.checked = true 247 | if (this.parent) { 248 | this.parent.childWasSelected() 249 | } 250 | } 251 | 252 | /** 253 | * Deselects this topic. If this item is not itself selected but a child of it 254 | * is then it traverses to that child and deselects it. 255 | * Takes an optional argument as to whether to select the parent after deselection 256 | * Doing this allows a user to stay in context of their selection in the miller 257 | * column element as deselecting the whole tree would take them back to root 258 | */ 259 | deselect(selectParent: boolean = true) { 260 | // if this item is selected explicitly we can deselect it 261 | if (this.selected) { 262 | this.deselectSelfAndParents() 263 | } else { 264 | // otherwise we need to find the selected children to start deselecting 265 | const selectedChildren = this.selectedChildren 266 | 267 | // if we have none it's a no-op 268 | if (!selectedChildren.length) { 269 | return 270 | } 271 | 272 | for (const child of selectedChildren) { 273 | child.deselect(false) 274 | } 275 | } 276 | 277 | if (selectParent && this.parent) { 278 | this.parent.select() 279 | } 280 | } 281 | 282 | deselectSelfAndParents() { 283 | // loop through the parents only deselecting items that don't have other 284 | // selected children 285 | for (const topic of this.withParents().reverse()) { 286 | if (topic.selectedChildren.length) { 287 | break 288 | } else { 289 | topic.selected = false 290 | topic.checkbox.checked = false 291 | } 292 | } 293 | } 294 | 295 | /** If a child is selected we need to implicitly select all the parents */ 296 | childWasSelected() { 297 | this.checkbox.checked = true 298 | this.selected = false 299 | if (this.parent) { 300 | this.parent.childWasSelected() 301 | } 302 | } 303 | } 304 | 305 | class MillerColumnsElement extends HTMLElement { 306 | taxonomy: Taxonomy 307 | classNames: Object 308 | describedbyId: ?string 309 | 310 | constructor() { 311 | super() 312 | this.classNames = { 313 | column: 'miller-columns__column', 314 | columnHeading: 'miller-columns__column-heading', 315 | backLink: 'govuk-back-link', 316 | columnList: 'miller-columns__column-list', 317 | columnCollapse: 'miller-columns__column--collapse', 318 | columnMedium: 'miller-columns__column--medium', 319 | columnNarrow: 'miller-columns__column--narrow', 320 | columnActive: 'miller-columns__column--active', 321 | item: 'miller-columns__item', 322 | itemParent: 'miller-columns__item--parent', 323 | itemActive: 'miller-columns__item--active', 324 | itemSelected: 'miller-columns__item--selected' 325 | } 326 | } 327 | 328 | connectedCallback() { 329 | this.describedbyId = this.getAttribute('aria-describedby') 330 | 331 | const source = document.getElementById(this.getAttribute('for') || '') 332 | if (source) { 333 | this.taxonomy = new Taxonomy(Topic.fromList(source), this) 334 | this.renderTaxonomyColumn(this.taxonomy.topics, true) 335 | this.update() 336 | if (source.parentNode) { 337 | source.parentNode.removeChild(source) 338 | } 339 | this.style.display = 'block' 340 | } 341 | } 342 | 343 | /** Returns the element which shows the selections a user has made */ 344 | get selectedElement(): ?MillerColumnsSelectedElement { 345 | const selected = document.getElementById(this.getAttribute('selected') || '') 346 | return selected instanceof MillerColumnsSelectedElement ? selected : null 347 | } 348 | 349 | /** Build and insert a column of the taxonomy */ 350 | renderTaxonomyColumn(topics: Array, root: boolean = false) { 351 | const div = document.createElement('div') 352 | 353 | if (!root) { 354 | // Append back link 355 | const backLink = document.createElement('button') 356 | backLink.className = this.classNames.backLink 357 | backLink.type = 'button' 358 | backLink.innerHTML = 'Back' 359 | backLink.addEventListener( 360 | 'click', 361 | () => { 362 | if (topics[0].parent) { 363 | this.showCurrentColumns(topics[0].parent.parent) 364 | } 365 | }, 366 | false 367 | ) 368 | div.appendChild(backLink) 369 | 370 | // Append heading 371 | const h3 = document.createElement('h3') 372 | h3.className = this.classNames.columnHeading 373 | const parentTopicName = topics[0].parent ? topics[0].parent.topicName : null 374 | if (parentTopicName) { 375 | h3.innerHTML = parentTopicName 376 | } 377 | div.appendChild(h3) 378 | } 379 | 380 | // Append list 381 | const ul = document.createElement('ul') 382 | ul.className = this.classNames.columnList 383 | div.className = this.classNames.column 384 | if (root) { 385 | div.dataset.root = 'true' 386 | } else { 387 | div.classList.add(this.classNames.columnCollapse) 388 | } 389 | div.appendChild(ul) 390 | 391 | // Append column 392 | this.appendChild(div) 393 | 394 | for (const topic of topics) { 395 | this.renderTopic(topic, ul) 396 | } 397 | } 398 | 399 | /** Build and insert a list item for a topic */ 400 | renderTopic(topic: Topic, list: HTMLElement) { 401 | const li = document.createElement('li') 402 | li.classList.add(this.classNames.item) 403 | li.classList.add('govuk-checkboxes--small') 404 | if (this.describedbyId) { 405 | li.setAttribute('aria-describedby', this.describedbyId) 406 | } 407 | 408 | const div = document.createElement('div') 409 | div.className = 'govuk-checkboxes__item' 410 | div.appendChild(topic.checkbox) 411 | div.appendChild(topic.label) 412 | li.appendChild(div) 413 | list.appendChild(li) 414 | this.attachEvents(li, topic) 415 | 416 | if (topic.children.length) { 417 | li.classList.add(this.classNames.itemParent) 418 | this.renderTaxonomyColumn(topic.children) 419 | } 420 | } 421 | 422 | /** Focus the miller columns item associated with a topic */ 423 | focusTopic(topic: ?Topic) { 424 | if (topic instanceof Topic && topic.checkbox) { 425 | const item = topic.checkbox.closest(`.${this.classNames.item}`) 426 | if (item instanceof HTMLElement) { 427 | item.focus() 428 | } 429 | } 430 | } 431 | 432 | /** Sets up the event handling for a list item and a topic */ 433 | attachEvents(trigger: HTMLElement, topic: Topic) { 434 | trigger.tabIndex = 0 435 | trigger.addEventListener( 436 | 'click', 437 | () => { 438 | this.taxonomy.topicClicked(topic) 439 | topic.checkbox.dispatchEvent(new Event('click')) 440 | }, 441 | false 442 | ) 443 | trigger.addEventListener( 444 | 'keydown', 445 | (event: KeyboardEvent) => { 446 | switch (event.key) { 447 | case ' ': 448 | case 'Enter': 449 | event.preventDefault() 450 | this.taxonomy.topicClicked(topic) 451 | topic.checkbox.dispatchEvent(new Event('click')) 452 | break 453 | case 'ArrowUp': 454 | event.preventDefault() 455 | if (topic.previous) { 456 | this.showCurrentColumns(topic.previous) 457 | this.focusTopic(topic.previous) 458 | } 459 | break 460 | case 'ArrowDown': 461 | event.preventDefault() 462 | if (topic.next) { 463 | this.showCurrentColumns(topic.next) 464 | this.focusTopic(topic.next) 465 | } 466 | break 467 | case 'ArrowLeft': 468 | event.preventDefault() 469 | if (topic.parent) { 470 | this.showCurrentColumns(topic.parent) 471 | this.focusTopic(topic.parent) 472 | } 473 | break 474 | case 'ArrowRight': 475 | event.preventDefault() 476 | if (topic.children) { 477 | this.showCurrentColumns(topic.children[0]) 478 | this.focusTopic(topic.children[0]) 479 | } 480 | break 481 | default: 482 | return 483 | } 484 | }, 485 | false 486 | ) 487 | } 488 | 489 | /** Update this element to show a change in the state */ 490 | update() { 491 | this.showSelectedTopics(this.taxonomy.selectedTopics) 492 | this.showActiveTopic(this.taxonomy.active) 493 | this.showCurrentColumns(this.taxonomy.active) 494 | 495 | if (this.selectedElement) { 496 | this.selectedElement.update(this.taxonomy) 497 | } 498 | } 499 | 500 | /** 501 | * Utility method to swap class names over for a group of elements 502 | * Takes an array of all elements that should have a class and removes it 503 | * from any other items that have it 504 | */ 505 | updateClassName(className: string, items: Array) { 506 | const currentlyWithClass = nodesToArray(this.getElementsByClassName(className)) 507 | 508 | for (const item of currentlyWithClass.concat(items)) { 509 | if (!item) { 510 | continue 511 | } 512 | 513 | if (items.indexOf(item) !== -1) { 514 | item.classList.add(className) 515 | } else { 516 | item.classList.remove(className) 517 | } 518 | } 519 | } 520 | 521 | /** Given an array of selected topics update the UI */ 522 | showSelectedTopics(selectedTopics: Array) { 523 | const selectedItems = selectedTopics.reduce((memo, child) => { 524 | for (const topic of child.withParents()) { 525 | const item = topic.checkbox.closest(`.${this.classNames.item}`) 526 | if (item instanceof HTMLElement) { 527 | memo.push(item) 528 | } 529 | } 530 | 531 | return memo 532 | }, []) 533 | 534 | this.updateClassName(this.classNames.itemSelected, selectedItems) 535 | } 536 | 537 | /** Update the topic items for the presence (or not) of an active topic */ 538 | showActiveTopic(activeTopic: ?Topic) { 539 | let activeItems 540 | 541 | if (!activeTopic) { 542 | activeItems = [] 543 | } else { 544 | activeItems = activeTopic.withParents().reduce((memo, topic) => { 545 | const item = topic.checkbox.closest(`.${this.classNames.item}`) 546 | 547 | if (item instanceof HTMLElement) { 548 | memo.push(item) 549 | } 550 | 551 | return memo 552 | }, []) 553 | } 554 | this.updateClassName(this.classNames.itemActive, activeItems) 555 | } 556 | 557 | /** Change what columns are visible based on the active (or not) topic */ 558 | showCurrentColumns(activeTopic: ?Topic) { 559 | const allColumns = nodesToArray(this.getElementsByClassName(this.classNames.column)) 560 | const columnsToShow = activeTopic ? this.columnsForActiveTopic(activeTopic) : [allColumns[0]] 561 | const narrowThreshold = Math.max(3, columnsToShow.length - 1) 562 | const showNarrow = columnsToShow.length > narrowThreshold 563 | const showMedium = showNarrow && narrowThreshold === 3 564 | const { 565 | columnCollapse: collapseClass, 566 | columnNarrow: narrowClass, 567 | columnMedium: mediumClass, 568 | columnActive: activeClass 569 | } = this.classNames 570 | 571 | for (const item of allColumns) { 572 | if (!item) { 573 | continue 574 | } 575 | 576 | item.classList.remove(activeClass) 577 | // we always want to show the root column 578 | if (item.dataset.root === 'true') { 579 | item.classList.remove(narrowClass, mediumClass) 580 | if (showMedium) { 581 | item.classList.add(mediumClass) 582 | } else if (showNarrow) { 583 | item.classList.add(narrowClass) 584 | } 585 | if (columnsToShow.length === 1) { 586 | item.classList.add(activeClass) 587 | } 588 | continue 589 | } 590 | 591 | const index = columnsToShow.indexOf(item) 592 | 593 | if (index === -1) { 594 | // this is not a column to show 595 | item.classList.add(collapseClass) 596 | } else if (showNarrow && index < narrowThreshold) { 597 | // show this column but narrow 598 | item.classList.remove(collapseClass, narrowClass, mediumClass) 599 | if (showMedium) { 600 | item.classList.add(mediumClass) 601 | } else if (showNarrow) { 602 | item.classList.add(narrowClass) 603 | } 604 | } else { 605 | // show this column in all it's glory 606 | item.classList.remove(collapseClass, narrowClass, mediumClass) 607 | } 608 | 609 | // mark last visible column as active 610 | if (item === columnsToShow[columnsToShow.length - 1]) { 611 | item.classList.add(activeClass) 612 | } 613 | } 614 | } 615 | 616 | /** Determine which columns should be shown based on the active topic */ 617 | columnsForActiveTopic(activeTopic: ?Topic): Array { 618 | if (!activeTopic) { 619 | return [] 620 | } 621 | 622 | const columnSelector = `.${this.classNames.column}` 623 | const columns = activeTopic.withParents().reduce((memo, topic) => { 624 | const column = topic.checkbox.closest(columnSelector) 625 | if (column instanceof HTMLElement) { 626 | memo.push(column) 627 | } 628 | 629 | return memo 630 | }, []) 631 | 632 | // we'll want to show the next column too for the next choices 633 | if (activeTopic.children.length) { 634 | const nextColumn = activeTopic.children[0].checkbox.closest(columnSelector) 635 | if (nextColumn instanceof HTMLElement) { 636 | columns.push(nextColumn) 637 | } 638 | } 639 | return columns 640 | } 641 | } 642 | 643 | class MillerColumnsSelectedElement extends HTMLElement { 644 | list: HTMLElement 645 | taxonomy: ?Taxonomy 646 | 647 | constructor() { 648 | super() 649 | } 650 | 651 | connectedCallback() { 652 | this.list = document.createElement('ol') 653 | this.list.className = 'miller-columns-selected__list' 654 | this.appendChild(this.list) 655 | if (this.millerColumnsElement && this.millerColumnsElement.taxonomy) { 656 | this.update(this.millerColumnsElement.taxonomy) 657 | } 658 | } 659 | 660 | get millerColumnsElement(): ?MillerColumnsElement { 661 | const millerColumns = document.getElementById(this.getAttribute('for') || '') 662 | return millerColumns instanceof MillerColumnsElement ? millerColumns : null 663 | } 664 | 665 | /** Update the UI to show the selected topics */ 666 | update(taxonomy: Taxonomy) { 667 | this.taxonomy = taxonomy 668 | const selectedTopics = taxonomy.selectedTopics 669 | // seems simpler to nuke the list and re-build it 670 | while (this.list.lastChild) { 671 | this.list.removeChild(this.list.lastChild) 672 | } 673 | 674 | if (selectedTopics.length) { 675 | for (const topic of selectedTopics) { 676 | this.addSelectedTopic(topic) 677 | } 678 | } else { 679 | const li = document.createElement('li') 680 | li.className = 'miller-columns-selected__list-item' 681 | li.textContent = 'No selected topics' 682 | this.list.appendChild(li) 683 | } 684 | } 685 | 686 | addSelectedTopic(topic: Topic) { 687 | const li = document.createElement('li') 688 | li.className = 'miller-columns-selected__list-item' 689 | li.appendChild(this.breadcrumbsElement(topic)) 690 | li.appendChild(this.removeTopicElement(topic)) 691 | this.list.appendChild(li) 692 | } 693 | 694 | breadcrumbsElement(topic: Topic): HTMLElement { 695 | const div = document.createElement('div') 696 | div.className = 'govuk-breadcrumbs' 697 | const ol = document.createElement('ol') 698 | ol.className = 'govuk-breadcrumbs__list' 699 | for (const current of topic.withParents()) { 700 | const li = document.createElement('li') 701 | li.className = 'govuk-breadcrumbs__list-item' 702 | li.textContent = current.label.textContent 703 | ol.appendChild(li) 704 | } 705 | div.appendChild(ol) 706 | return div 707 | } 708 | 709 | removeTopicElement(topic: Topic): HTMLElement { 710 | const button = document.createElement('button') 711 | button.className = 'miller-columns-selected__remove-topic' 712 | button.textContent = 'Remove topic' 713 | button.setAttribute('type', 'button') 714 | button.addEventListener('click', () => { 715 | triggerEvent(button, 'remove-topic', topic) 716 | if (this.taxonomy) { 717 | this.taxonomy.removeTopic(topic) 718 | } 719 | }) 720 | 721 | const span = document.createElement('span') 722 | span.className = 'miller-columns-selected__remove-topic-name' 723 | span.textContent = `: ${topic.topicName}` 724 | button.appendChild(span) 725 | 726 | return button 727 | } 728 | } 729 | 730 | if (!window.customElements.get('miller-columns')) { 731 | window.MillerColumnsElement = MillerColumnsElement 732 | window.customElements.define('miller-columns', MillerColumnsElement) 733 | } 734 | 735 | if (!window.customElements.get('miller-columns-selected')) { 736 | window.MillerColumnsSelectedElement = MillerColumnsSelectedElement 737 | window.customElements.define('miller-columns-selected', MillerColumnsSelectedElement) 738 | } 739 | 740 | export {MillerColumnsElement, MillerColumnsSelectedElement} 741 | -------------------------------------------------------------------------------- /miller-columns-selected.scss: -------------------------------------------------------------------------------- 1 | .miller-columns-selected { 2 | @include govuk-responsive-margin(6, "bottom"); 3 | display: block; 4 | 5 | .govuk-breadcrumbs { 6 | display: block; 7 | margin: 0; 8 | margin-right: 110px; 9 | } 10 | 11 | .govuk-breadcrumbs__list-item { 12 | margin-bottom: 0; 13 | } 14 | } 15 | 16 | .miller-columns-selected__list { 17 | @include govuk-font($size: 16); 18 | @include govuk-text-colour; 19 | margin: 0; 20 | padding: 0; 21 | list-style-type: none; 22 | } 23 | 24 | .miller-columns-selected__list-item { 25 | position: relative; 26 | margin-bottom: 0; 27 | padding: govuk-spacing(2) 0; 28 | border-top: 1px solid $govuk-border-colour; 29 | 30 | &:last-child { 31 | border-bottom: 1px solid $govuk-border-colour; 32 | } 33 | } 34 | 35 | .miller-columns-selected__remove-topic { 36 | @include govuk-link-common; 37 | @include govuk-link-style-no-visited-state; 38 | @include govuk-font($size: 16); 39 | position: absolute; 40 | top: govuk-spacing(2); 41 | right: 0; 42 | margin: 0; 43 | padding: 0; 44 | border: 0; 45 | color: $govuk-link-colour; 46 | background: transparent; 47 | cursor: pointer; 48 | 49 | &:focus { 50 | @include govuk-focused-text; 51 | } 52 | } 53 | 54 | .miller-columns-selected__remove-topic-name { 55 | @include govuk-visually-hidden; 56 | } 57 | -------------------------------------------------------------------------------- /miller-columns.scss: -------------------------------------------------------------------------------- 1 | $govuk-include-default-font-face: false; 2 | $govuk-new-link-styles: true; 3 | 4 | @import "node_modules/govuk-frontend/dist/govuk/base"; 5 | @import "miller-columns-selected"; 6 | 7 | $mc-transition-time: 400ms; 8 | $mc-selected-item-colour: govuk-colour("white"); 9 | $mc-selected-item-background: govuk-colour("dark-grey"); 10 | $mc-active-item-colour: govuk-colour("white"); 11 | $mc-active-item-background: govuk-colour("blue"); 12 | 13 | .js-enabled { 14 | // Hide nested lists 15 | .miller-columns { 16 | display: none; 17 | 18 | // Disable pointer events on checkboxes to prevent them from interfering with the miller-columns items 19 | .govuk-checkboxes__input, 20 | .govuk-checkboxes__label { 21 | pointer-events: none; 22 | } 23 | } 24 | } 25 | 26 | .miller-columns { 27 | display: block; 28 | width: 100%; 29 | height: 100%; 30 | @include govuk-responsive-margin(6, "bottom"); 31 | outline: 0; 32 | font-size: 0; 33 | white-space: nowrap; 34 | } 35 | 36 | .miller-columns__column { 37 | display: none; 38 | width: 100%; 39 | height: 100%; 40 | vertical-align: top; 41 | white-space: normal; 42 | transition-duration: $mc-transition-time; 43 | transition-property: width; 44 | 45 | &.miller-columns__column--active { 46 | display: inline-block; 47 | } 48 | 49 | @include govuk-media-query($from: tablet) { 50 | display: inline-block; 51 | width: 33.3%; 52 | border-right: 1px solid $govuk-border-colour; 53 | } 54 | } 55 | 56 | .miller-columns__column--narrow { 57 | width: 16.6%; 58 | overflow-x: hidden; 59 | 60 | .miller-columns__item, 61 | .miller-columns__item label { 62 | white-space: nowrap; 63 | } 64 | 65 | .miller-columns__item--parent:after { 66 | display: none; 67 | } 68 | } 69 | 70 | .miller-columns__column--medium { 71 | width: 22.2%; 72 | overflow-x: hidden; 73 | 74 | .miller-columns__item, 75 | .miller-columns__item label { 76 | white-space: nowrap; 77 | } 78 | 79 | .miller-columns__item--parent:after { 80 | display: none; 81 | } 82 | } 83 | 84 | .miller-columns__column--collapse { 85 | display: none; 86 | } 87 | 88 | .miller-columns__column-heading { 89 | @include govuk-text-colour; 90 | @include govuk-font($size: 19, $weight: bold); 91 | margin-bottom: govuk-spacing(3); 92 | margin-top: 0; 93 | padding: 0; 94 | 95 | @include govuk-media-query($from: tablet) { 96 | display: none; 97 | } 98 | } 99 | 100 | .miller-columns__column-list { 101 | margin: 0; 102 | padding: 0; 103 | } 104 | 105 | .miller-columns__item { 106 | position: relative; 107 | margin-bottom: 1px; 108 | padding: 2px 9px; 109 | list-style: none; 110 | color: $govuk-text-colour; 111 | cursor: pointer; 112 | @include govuk-font($size: 16); 113 | 114 | &:hover { 115 | color: govuk-colour("black"); 116 | background-color: $govuk-hover-colour; 117 | } 118 | 119 | &:focus { 120 | @include govuk-focused-text; 121 | } 122 | 123 | .govuk-checkboxes__item { 124 | float: none; 125 | } 126 | 127 | .govuk-checkboxes__label { 128 | @include govuk-font($size: 16); 129 | padding: 12px 15px 10px 1px; 130 | } 131 | 132 | // Remove box-shadow set in govuk-frontend for govuk-checkboxes__input 133 | // as we rely on the parent element miller-columns__item to show the target area on hover 134 | .govuk-checkboxes__item:hover .govuk-checkboxes__input:focus + .govuk-checkboxes__label::before, 135 | .govuk-checkboxes__item:hover .govuk-checkboxes__input:not(:disabled) + .govuk-checkboxes__label::before { 136 | box-shadow: none; 137 | } 138 | } 139 | 140 | .miller-columns__item--parent:after { 141 | content: "\203A" / ""; 142 | position: absolute; 143 | top: 50%; 144 | right: 5px; 145 | margin-top: -17px; 146 | float: right; 147 | font-size: 24px; 148 | } 149 | 150 | .miller-columns__item--selected, 151 | .miller-columns__item--selected:hover { 152 | color: $mc-selected-item-colour; 153 | background-color: $mc-selected-item-background; 154 | 155 | .govuk-checkboxes__label { 156 | color: $mc-selected-item-colour; 157 | } 158 | } 159 | 160 | .miller-columns__item--selected:focus { 161 | @include govuk-focused-text; 162 | 163 | .govuk-checkboxes__label { 164 | color: $govuk-text-colour; 165 | } 166 | } 167 | 168 | .miller-columns__item--active, 169 | .miller-columns__item--active:hover { 170 | color: $mc-active-item-colour; 171 | background-color: $mc-active-item-background; 172 | box-shadow: none; 173 | 174 | .govuk-checkboxes__label { 175 | color: $mc-active-item-colour; 176 | } 177 | } 178 | 179 | .miller-columns__item--active:focus { 180 | @include govuk-focused-text; 181 | 182 | .govuk-checkboxes__label { 183 | color: $govuk-text-colour; 184 | } 185 | } 186 | 187 | .miller-columns { 188 | .govuk-list { 189 | .govuk-list { 190 | margin-left: govuk-spacing(6); 191 | } 192 | } 193 | 194 | .govuk-back-link { 195 | margin-bottom: govuk-spacing(4); 196 | padding-top: 0; 197 | padding-right: 0; 198 | padding-bottom: 0; 199 | border: 0; 200 | background: transparent; 201 | text-decoration: underline; 202 | 203 | @include govuk-media-query($from: tablet) { 204 | display: none; 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miller-columns-element", 3 | "version": "2.0.1", 4 | "description": "Miller columns (cascading lists) for hierarchical topic selection on GOV.UK taxonomy", 5 | "main": "dist/index.umd.js", 6 | "module": "dist/index.esm.js", 7 | "license": "MIT", 8 | "repository": "alphagov/miller-columns-element", 9 | "files": [ 10 | "dist" 11 | ], 12 | "watch": { 13 | "build": [ 14 | "index.js", 15 | "examples.scss", 16 | "miller-columns.scss", 17 | "miller-columns-selected.scss" 18 | ] 19 | }, 20 | "scripts": { 21 | "clean": "rm -rf dist", 22 | "lint": "sass-lint examples.scss -v -q && eslint index.js test/ && flow check", 23 | "prebuild": "npm run clean && mkdir dist", 24 | "build-css": "node-sass examples.scss dist/examples.css && node-sass miller-columns.scss dist/miller-columns.css", 25 | "build-umd": "BABEL_ENV=umd babel index.js -o dist/index.umd.js", 26 | "build-esm": "BABEL_ENV=esm babel index.js -o dist/index.esm.js", 27 | "build-examples": "rm -rf examples/dist && cp -r dist examples/dist", 28 | "build": "npm run build-css && npm run build-umd && npm run build-esm && npm run build-examples", 29 | "pretest": "npm run build && npm run lint", 30 | "test": "karma start test/karma.config.js", 31 | "watch": "npm-watch" 32 | }, 33 | "devDependencies": { 34 | "babel-cli": "^6.18.0", 35 | "babel-plugin-transform-custom-element-classes": "^0.1.0", 36 | "babel-preset-es2015": "^6.24.1", 37 | "babel-preset-flow": "^6.23.0", 38 | "chai": "^4.2.0", 39 | "eslint": "^7.2.0", 40 | "eslint-plugin-github": "1.6.0", 41 | "eslint-plugin-relay": "^2.0.0", 42 | "flow-bin": "^0.133.0", 43 | "govuk-frontend": "^5.0.0", 44 | "karma": "^6.0.0", 45 | "karma-chai": "^0.1.0", 46 | "karma-chrome-launcher": "^3.1.0", 47 | "karma-mocha": "^2.0.0", 48 | "karma-mocha-reporter": "^2.2.5", 49 | "mocha": "^11.1.0", 50 | "node-sass": "^9.0.0", 51 | "npm-watch": "^0.13.0", 52 | "sass-lint": "^1.13.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('eslint-plugin-github/prettier.config') 2 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "assert": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/karma.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | frameworks: ['mocha', 'chai'], 4 | files: ['../dist/index.umd.js', 'test.js'], 5 | reporters: ['mocha'], 6 | port: 9876, 7 | colors: true, 8 | logLevel: config.LOG_INFO, 9 | browsers: ['ChromeHeadless'], 10 | autoWatch: false, 11 | singleRun: true, 12 | concurrency: Infinity 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | describe('miller-columns', function() { 2 | describe('element creation', function() { 3 | it('creates from document.createElement', function() { 4 | const el = document.createElement('miller-columns') 5 | assert.equal('MILLER-COLUMNS', el.nodeName) 6 | }) 7 | 8 | it('creates from constructor', function() { 9 | const el = new window.MillerColumnsElement() 10 | assert.equal('MILLER-COLUMNS', el.nodeName) 11 | }) 12 | }) 13 | 14 | describe('after tree insertion', function() { 15 | function pressKey(key, element) { 16 | const event = document.createEvent('Event') 17 | event.initEvent('keydown', true, true) 18 | event.key = key 19 | element.dispatchEvent(event) 20 | } 21 | 22 | beforeEach(function() { 23 | const container = document.createElement('div') 24 | container.innerHTML = ` 25 | 26 | 27 | 28 |
    29 |
  • 30 |
    31 | 32 | 35 |
    36 |
      37 |
    • 38 |
      39 | 40 | 43 |
      44 |
        45 |
      • 46 |
        47 | 48 | 51 |
        52 |
      • 53 |
      • 54 |
        55 | 56 | 59 |
        60 |
      • 61 |
      62 |
    • 63 |
    • 64 |
      65 | 66 | 69 |
      70 |
        71 |
      • 72 |
        73 | 74 | 77 |
        78 |
      • 79 |
      80 |
    • 81 |
    82 |
  • 83 |
  • 84 |
    85 | 86 | 89 |
    90 |
  • 91 |
92 |
93 | ` 94 | document.body.append(container) 95 | }) 96 | 97 | afterEach(function() { 98 | document.body.innerHTML = undefined 99 | }) 100 | 101 | it('unnests lists', function() { 102 | const lists = document.querySelectorAll('ul') 103 | assert.equal(lists.length, 4) 104 | }) 105 | 106 | it('marks items with children as parents', function() { 107 | const firstItem = document.querySelector('ul li') 108 | assert.isTrue(firstItem.classList.contains('miller-columns__item--parent')) 109 | }) 110 | 111 | it('marks selected item as active when clicked', function() { 112 | const firstItem = document.querySelector('ul li') 113 | const firstItemCheckbox = firstItem.querySelector('input') 114 | firstItemCheckbox.addEventListener('click', function(e) { 115 | assert.deepEqual(e.target, firstItemCheckbox) 116 | }) 117 | 118 | firstItem.click() 119 | 120 | assert.isTrue(firstItem.classList.contains('miller-columns__item--active')) 121 | assert.isTrue(firstItem.querySelector('input').checked) 122 | }) 123 | 124 | it('marks selected item as active when pressing Enter', function() { 125 | const firstItem = document.querySelector('ul li') 126 | const firstItemCheckbox = firstItem.querySelector('input') 127 | firstItemCheckbox.addEventListener('keydown', function(e) { 128 | assert.deepEqual(e.target, firstItemCheckbox) 129 | }) 130 | 131 | firstItem.focus() 132 | pressKey('Enter', firstItemCheckbox) 133 | 134 | assert.isTrue(firstItem.classList.contains('miller-columns__item--active')) 135 | assert.isTrue(firstItem.querySelector('input').checked) 136 | }) 137 | 138 | it('marks selected item as active when pressing Space', function() { 139 | const firstItem = document.querySelector('ul li') 140 | const firstItemCheckbox = firstItem.querySelector('input') 141 | firstItemCheckbox.addEventListener('keydown', function(e) { 142 | assert.deepEqual(e.target, firstItemCheckbox) 143 | }) 144 | 145 | firstItem.focus() 146 | pressKey(' ', firstItemCheckbox) 147 | 148 | assert.isTrue(firstItem.classList.contains('miller-columns__item--active')) 149 | assert.isTrue(firstItem.querySelector('input').checked) 150 | }) 151 | 152 | it('marks next item as focused when pressing ArrowDown', function() { 153 | const firstItem = document.querySelector('.miller-columns__column li:nth-of-type(1)') 154 | const secondItem = document.querySelector('.miller-columns__column li:nth-of-type(2)') 155 | 156 | firstItem.focus() 157 | pressKey('ArrowDown', firstItem) 158 | 159 | assert.deepEqual(secondItem, document.activeElement) 160 | }) 161 | 162 | it('marks previous item as focused when pressing ArrowUp', function() { 163 | const firstItem = document.querySelector('.miller-columns__column li:nth-of-type(1)') 164 | const secondItem = document.querySelector('.miller-columns__column li:nth-of-type(2)') 165 | 166 | secondItem.focus() 167 | pressKey('ArrowUp', secondItem) 168 | 169 | assert.deepEqual(firstItem, document.activeElement) 170 | }) 171 | 172 | it('marks first child item as focused when pressing ArrowRight', function() { 173 | const firstItemL1 = document.querySelector('.miller-columns__column:nth-of-type(1) li') 174 | const firstItemL2 = document.querySelector('.miller-columns__column:nth-of-type(2) li') 175 | 176 | firstItemL1.focus() 177 | pressKey('ArrowRight', firstItemL1) 178 | 179 | assert.deepEqual(firstItemL2, document.activeElement) 180 | }) 181 | 182 | it('marks parent item as focused when pressing ArrowLeft', function() { 183 | const firstItemL1 = document.querySelector('.miller-columns__column:nth-of-type(1) li') 184 | const firstItemL2 = document.querySelector('.miller-columns__column:nth-of-type(2) li') 185 | 186 | firstItemL2.focus() 187 | pressKey('ArrowLeft', firstItemL2) 188 | 189 | assert.deepEqual(firstItemL1, document.activeElement) 190 | }) 191 | 192 | it('shows the child list for active list item', function() { 193 | const firstItem = document.querySelector('ul li') 194 | const l2List = document.querySelectorAll('.miller-columns__column')[1] 195 | assert.isTrue(l2List.classList.contains('miller-columns__column--collapse')) 196 | firstItem.click() 197 | assert.isFalse(l2List.classList.contains('miller-columns__column--collapse')) 198 | }) 199 | 200 | it('unselects children when item is unselected', function() { 201 | const firstItemL1 = document.querySelector('.miller-columns__column:nth-of-type(1) li') 202 | const firstItemL2 = document.querySelector('.miller-columns__column:nth-of-type(2) li') 203 | firstItemL1.click() 204 | firstItemL2.click() 205 | firstItemL1.click() 206 | 207 | assert.isFalse(firstItemL2.classList.contains('miller-columns__item--selected')) 208 | assert.isFalse(firstItemL2.querySelector('input').checked) 209 | }) 210 | 211 | it("doesn't unselect items above the item that was clicked in the tree", function() { 212 | const firstItemL1 = document.querySelector('.miller-columns__column:nth-of-type(1) li') 213 | firstItemL1.click() 214 | const firstItemL2 = document.querySelector( 215 | '.miller-columns__column:not(.miller-columns__column--collapse):nth-of-type(2) li' 216 | ) 217 | firstItemL2.click() 218 | 219 | firstItemL2.click() 220 | 221 | assert.isFalse(firstItemL2.classList.contains('miller-columns__item--selected')) 222 | assert.isFalse(firstItemL2.querySelector('input').checked) 223 | assert.isTrue(firstItemL1.classList.contains('miller-columns__item--selected')) 224 | assert.isTrue(firstItemL1.querySelector('input').checked) 225 | }) 226 | 227 | it('shows active items in selected items', function() { 228 | const firstItemL1 = document.querySelector('.miller-columns__column:nth-of-type(1) li') 229 | const firstLabelL1 = firstItemL1.querySelector('label') 230 | const firstItemL2 = document.querySelector('.miller-columns__column:nth-of-type(2) li') 231 | const firstLabelL2 = firstItemL2.querySelector('label') 232 | firstItemL1.click() 233 | firstItemL2.click() 234 | const selected = document.querySelector('#selected-items ol') 235 | assert.equal(selected.childNodes.length, 1) 236 | assert.isTrue(selected.textContent.includes(firstLabelL1.textContent)) 237 | assert.isTrue(selected.textContent.includes(firstLabelL2.textContent)) 238 | }) 239 | 240 | it('removes a chain from stored selected items', function() { 241 | const firstItemL1 = document.querySelector('.miller-columns__column li') 242 | const millerColumnsSelected = document.querySelector('miller-columns-selected') 243 | millerColumnsSelected.addEventListener('remove-topic', function(e) { 244 | assert.equal(e.detail.topicName, "Parenting, childcare and children's services") 245 | }) 246 | 247 | firstItemL1.click() 248 | 249 | const firstItemRemove = document.querySelector('#selected-items button') 250 | const firstItemRemoveHiddenText = firstItemRemove.querySelector('.miller-columns-selected__remove-topic-name') 251 | assert.include(firstItemRemoveHiddenText.textContent, "Parenting, childcare and children's services") 252 | firstItemRemove.click() 253 | 254 | const selectedItems = document.querySelector('#selected-items') 255 | assert.equal(selectedItems.textContent, 'No selected topics') 256 | }) 257 | 258 | it('creates entries of selected item for adjacent topics', function() { 259 | const firstItemL1 = document.querySelector('.miller-columns__column:nth-of-type(1) li') 260 | const firstItemL2 = document.querySelector('.miller-columns__column:nth-of-type(2) li') 261 | const secondItemL2 = document.querySelector('.miller-columns__column:nth-of-type(2) li:nth-of-type(2)') 262 | 263 | const firstLabelL2 = firstItemL2.querySelector('label') 264 | const secondLabelL2 = secondItemL2.querySelector('label') 265 | 266 | firstItemL1.click() 267 | firstItemL2.click() 268 | secondItemL2.click() 269 | 270 | const selectedItems = document.querySelector('#selected-items ol') 271 | assert.equal(selectedItems.childNodes.length, 2) 272 | assert.isTrue(selectedItems.textContent.includes(firstLabelL2.textContent)) 273 | assert.isTrue(selectedItems.textContent.includes(secondLabelL2.textContent)) 274 | }) 275 | 276 | it('provides an API to access the breadcrumb trail of a topic', function() { 277 | const firstItemL2 = document.querySelector('miller-columns').taxonomy.topics[0].children[0] 278 | assert.deepEqual(firstItemL2.topicNames, [ 279 | "Parenting, childcare and children's services", 280 | 'Divorce, separation and legal issues' 281 | ]) 282 | }) 283 | 284 | it('provides an API to access children topics in a flat list', function() { 285 | const firstItemL1 = document.querySelector('miller-columns').taxonomy.topics[0] 286 | assert.equal(firstItemL1.flattenedChildren.length, 5) 287 | }) 288 | 289 | it('provides an API to access all topics in a flat list', function() { 290 | const millerColumns = document.querySelector('miller-columns') 291 | assert.equal(millerColumns.taxonomy.flattenedTopics.length, 7) 292 | }) 293 | 294 | it('shows active column while selecting items', function() { 295 | const firstColumn = document.querySelectorAll('.miller-columns__column')[0] 296 | const firstItemL1 = firstColumn.querySelector('li') 297 | const secondItemL1 = firstColumn.querySelector('li:nth-of-type(2)') 298 | const secondColumn = document.querySelectorAll('.miller-columns__column')[1] 299 | const firstItemL2 = secondColumn.querySelector('li') 300 | const thirdColumn = document.querySelectorAll('.miller-columns__column')[2] 301 | 302 | assert.equal(document.querySelector('.miller-columns__column--active'), firstColumn) 303 | firstItemL1.click() 304 | assert.equal(document.querySelector('.miller-columns__column--active'), secondColumn) 305 | firstItemL2.click() 306 | assert.equal(document.querySelector('.miller-columns__column--active'), thirdColumn) 307 | secondItemL1.click() 308 | assert.equal(document.querySelector('.miller-columns__column--active'), firstColumn) 309 | }) 310 | 311 | it('shows parent element as heading for each column', function() { 312 | const firstColumn = document.querySelectorAll('.miller-columns__column')[0] 313 | const firstLabelL1 = firstColumn.querySelector('label') 314 | const secondColumn = document.querySelectorAll('.miller-columns__column')[1] 315 | const headingL2 = secondColumn.querySelector('.miller-columns__column-heading') 316 | assert.isTrue(firstLabelL1.textContent.includes(headingL2.textContent)) 317 | }) 318 | 319 | it('shows previous column when back link button is clicked', function() { 320 | const firstColumn = document.querySelectorAll('.miller-columns__column')[0] 321 | const firstItemL1 = firstColumn.querySelector('li') 322 | const secondColumn = document.querySelectorAll('.miller-columns__column')[1] 323 | const backButtonL2 = secondColumn.querySelector('.govuk-back-link') 324 | 325 | firstItemL1.click() 326 | assert.equal(document.querySelector('.miller-columns__column--active'), secondColumn) 327 | 328 | backButtonL2.click() 329 | assert.equal(document.querySelector('.miller-columns__column--active'), firstColumn) 330 | }) 331 | 332 | it('applies aria-describedby to each individual item', function() { 333 | const millerColumnsElement = document.querySelector('miller-columns') 334 | const describedbyId = millerColumnsElement.getAttribute('aria-describedby') 335 | 336 | const millerColumnsItem = document.querySelector('.miller-columns__item') 337 | 338 | assert.equal(millerColumnsItem.getAttribute('aria-describedby'), describedbyId) 339 | }) 340 | 341 | it('adds each item to the tab order', function() { 342 | const millerColumnsItem = document.querySelector('.miller-columns__item') 343 | 344 | assert.equal(millerColumnsItem.getAttribute('tabindex'), '0') 345 | }) 346 | 347 | it('removes checkboxes from the focus order', function() { 348 | const item = document.querySelector('ul li') 349 | const itemCheckbox = item.querySelector('input') 350 | 351 | assert.equal(itemCheckbox.getAttribute('tabindex'), '-1') 352 | }) 353 | }) 354 | 355 | describe('when loading pre-selected items', function() { 356 | beforeEach(function() { 357 | const container = document.createElement('div') 358 | container.innerHTML = ` 359 | 360 | 361 |
    362 |
  • 363 |
    364 | 365 | 368 |
    369 |
      370 |
    • 371 |
      372 | 373 | 376 |
      377 |
        378 |
      • 379 |
        380 | 381 | 384 |
        385 |
      • 386 |
      • 387 |
        388 | 389 | 392 |
        393 |
      • 394 |
      395 |
    • 396 |
    • 397 |
      398 | 399 | 402 |
      403 |
        404 |
      • 405 |
        406 | 407 | 410 |
        411 |
      • 412 |
      413 |
    • 414 |
    415 |
  • 416 |
417 |
418 | ` 419 | document.body.append(container) 420 | }) 421 | 422 | afterEach(function() { 423 | document.body.innerHTML = undefined 424 | }) 425 | 426 | it('marks items with checked inputs as selected', function() { 427 | const selectedCheckbox = document.querySelector('ul li input:checked') 428 | const listItem = selectedCheckbox.closest('li') 429 | assert.isTrue(listItem.classList.contains('miller-columns__item--active')) 430 | assert.isTrue(listItem.querySelector('input').checked) 431 | }) 432 | 433 | it('marks selected item’s parent as selected', function() { 434 | const listItem = document.querySelector('ul li') 435 | assert.isTrue(listItem.classList.contains('miller-columns__item--active')) 436 | assert.isTrue(listItem.querySelector('input').checked) 437 | }) 438 | }) 439 | }) 440 | --------------------------------------------------------------------------------