From 369008d807e605cf733ea92c649a8b293e3b54fe Mon Sep 17 00:00:00 2001 From: rmccar <42928680+rmccar@users.noreply.github.com> Date: Tue, 4 Jun 2024 10:49:52 +0100 Subject: [PATCH] Update `.editorconfig` and improve and run our linting and formatting process (#3190) --- .babelrc | 11 +- .editorconfig | 12 +- .eslintrc.json | 75 +- .github/ISSUE_TEMPLATE/bug-report.yaml | 196 +- .github/ISSUE_TEMPLATE/config.yaml | 6 +- .github/ISSUE_TEMPLATE/feature-request.yaml | 82 +- .github/PULL_REQUEST_TEMPLATE.md | 4 +- .github/release.yml | 58 +- .github/workflows/check-checklist.yml | 28 +- .github/workflows/check-labels.yml | 36 +- .github/workflows/lighthouse-ci.yml | 40 +- .github/workflows/macro-and-script-tests.yml | 30 +- .github/workflows/npm-bundle.yml | 46 +- .github/workflows/release.yml | 48 +- .github/workflows/visual-regression-tests.yml | 62 +- .remarkrc | 8 +- .stylelintrc.yml | 45 +- README.md | 28 +- babel.conf.esm.js | 30 +- babel.conf.nomodule.js | 30 +- backstop.config.js | 80 +- ci/common.js | 44 +- ci/generate-npm-package.js | 66 +- ci/prepare-templates-for-zip.js | 32 +- config/components.json | 46 +- gulpfile.js | 122 +- jest-puppeteer.config.js | 38 +- jest.config.js | 8 +- lib/build-search-index.js | 88 +- lib/config.indexPage.js | 6 +- lib/create-nunjucks-environment.js | 12 +- lib/create-page-list.js | 52 +- lib/dev-server.js | 88 +- lib/dev-server.spec.js | 20 +- lib/filters/set-attribute.js | 4 +- lib/filters/set-attributes.js | 10 +- lib/generate-static-pages.js | 80 +- lib/handle-routes.js | 12 +- lib/render-example.js | 48 +- lib/render-page-list.js | 50 +- lighthouse/lighthouse-get-urls.js | 58 +- lighthouse/lighthouserc.js | 30 +- package.json | 259 +-- postcss.config.js | 50 +- src/components/access-code/_macro.spec.js | 290 +-- src/components/access-code/access-code.dom.js | 10 +- src/components/access-code/access-code.js | 32 +- src/components/access-code/access-code.scss | 44 +- .../access-code/access-code.spec.js | 34 +- src/components/accordion/_macro.spec.js | 308 +-- src/components/accordion/accordion.dom.js | 20 +- src/components/accordion/accordion.js | 100 +- src/components/accordion/accordion.spec.js | 208 +- src/components/address-input/_macro.spec.js | 840 ++++---- .../address-input/autosuggest.address.dom.js | 10 +- .../autosuggest.address.error.js | 154 +- .../address-input/autosuggest.address.js | 714 +++---- .../autosuggest.address.setter.js | 190 +- .../address-input/autosuggest.address.spec.js | 1319 ++++++------ .../address-output/_address-output.scss | 6 +- src/components/address-output/_macro.spec.js | 168 +- src/components/autosuggest/_autosuggest.scss | 228 +-- src/components/autosuggest/_macro.spec.js | 510 ++--- src/components/autosuggest/autosuggest.dom.js | 10 +- .../autosuggest/autosuggest.helpers.js | 22 +- .../autosuggest/autosuggest.helpers.spec.js | 146 +- src/components/autosuggest/autosuggest.js | 40 +- .../autosuggest/autosuggest.spec.js | 1078 +++++----- src/components/autosuggest/autosuggest.ui.js | 953 ++++----- src/components/autosuggest/fuse-config.js | 34 +- src/components/back-to-top/_back-to-top.scss | 54 +- src/components/back-to-top/_macro.spec.js | 98 +- src/components/back-to-top/back-to-top.dom.js | 10 +- src/components/back-to-top/back-to-top.js | 104 +- .../back-to-top/back-to-top.spec.js | 212 +- .../back-to-top/example-back-to-top.njk | 12 +- src/components/breadcrumbs/_breadcrumbs.scss | 128 +- src/components/breadcrumbs/_macro.spec.js | 182 +- .../browser-banner/_browser-banner.scss | 46 +- src/components/browser-banner/_macro.spec.js | 184 +- src/components/button/_button.scss | 965 +++++---- src/components/button/_macro.spec.js | 726 +++---- src/components/button/button.dom.js | 30 +- src/components/button/button.js | 106 +- src/components/button/button.spec.js | 488 ++--- .../call-to-action/_call-to-action.scss | 10 +- src/components/call-to-action/_macro.spec.js | 56 +- src/components/card/_card.scss | 46 +- src/components/card/_macro.spec.js | 360 ++-- .../char-check-limit/_macro.spec.js | 96 +- .../char-check-limit/character-check.js | 116 +- .../char-check-limit/character-check.spec.js | 346 ++-- .../char-check-limit/character-limit.js | 80 +- .../checkboxes/_checkbox-macro.spec.js | 710 +++---- src/components/checkboxes/_checkbox.scss | 360 ++-- src/components/checkboxes/_checkboxes.scss | 66 +- src/components/checkboxes/_macro.spec.js | 522 ++--- .../checkboxes/checkbox-with-autoselect.js | 64 +- .../checkboxes/checkbox-with-fieldset.js | 42 +- .../checkboxes/checkboxes-with-reveal.js | 20 +- src/components/checkboxes/checkboxes.dom.js | 54 +- src/components/checkboxes/checkboxes.spec.js | 366 ++-- .../_content-pagination.scss | 82 +- .../content-pagination/_macro.spec.js | 318 +-- .../cookies-banner/_cookies-banner.scss | 44 +- src/components/cookies-banner/_macro.spec.js | 354 ++-- .../cookies-banner/cookies-banner.dom.js | 14 +- .../cookies-banner/cookies-banner.js | 152 +- .../cookies-banner/cookies-banner.spec.js | 140 +- src/components/date-input/_macro.spec.js | 676 +++--- .../description-list/_description-list.scss | 46 +- .../description-list/_macro.spec.js | 288 +-- src/components/details/_details.scss | 218 +- src/components/details/_macro.spec.js | 264 +-- src/components/details/details.dom.js | 12 +- src/components/details/details.js | 120 +- src/components/details/details.spec.js | 212 +- src/components/document-list/_macro.spec.js | 888 ++++---- .../document-list/document-list.scss | 294 ++- .../_download-resources.scss | 217 +- .../download-resources/download-resources.js | 1807 +++++++++-------- .../download-resources.spec.js | 922 ++++----- src/components/duration/_macro.spec.js | 582 +++--- src/components/error/_macro.spec.js | 144 +- .../external-link/_external-link.scss | 38 +- src/components/external-link/_macro.spec.js | 136 +- src/components/feedback/_feedback.scss | 62 +- src/components/feedback/_macro.spec.js | 144 +- src/components/field/_field-group.scss | 20 +- src/components/field/_field.scss | 32 +- src/components/field/_macro.spec.js | 160 +- src/components/fieldset/_fieldset.scss | 54 +- src/components/fieldset/_macro.spec.js | 322 +-- src/components/footer/_footer.scss | 90 +- src/components/footer/_macro.spec.js | 904 ++++----- src/components/header/_header.scss | 404 ++-- src/components/header/_macro.spec.js | 1662 +++++++-------- src/components/helpers/_grid.scss | 8 +- src/components/hero/_hero.scss | 96 +- src/components/hero/_macro.spec.js | 118 +- src/components/icon/_icon.scss | 88 +- src/components/icon/_macro.spec.js | 220 +- src/components/image/_image.scss | 22 +- src/components/image/_macro.spec.js | 162 +- src/components/input/_input-type.scss | 175 +- src/components/input/_input.scss | 247 ++- src/components/input/_macro.spec.js | 1208 +++++------ src/components/input/character-check.dom.js | 10 +- src/components/input/input.dom.js | 10 +- src/components/input/input.js | 20 +- src/components/input/input.spec.js | 36 +- src/components/label/_label.scss | 48 +- src/components/label/_macro.spec.js | 343 ++-- .../language-selector/_macro.spec.js | 194 +- .../language-selector/language.scss | 14 +- src/components/list/_list.scss | 168 +- src/components/list/_macro.spec.js | 1166 +++++------ src/components/message-list/_macro.spec.js | 172 +- .../message-list/_message-list.scss | 32 +- src/components/message/_macro.njk | 10 +- src/components/message/_macro.spec.js | 148 +- src/components/message/_message.scss | 78 +- src/components/modal/_macro.spec.js | 138 +- src/components/modal/_modal.scss | 72 +- src/components/modal/modal.dom.js | 12 +- src/components/modal/modal.js | 178 +- src/components/modal/modal.spec.js | 100 +- .../mutually-exclusive/_macro.spec.js | 280 +-- .../mutually-exclusive.checkboxes.spec.js | 376 ++-- .../mutually-exclusive.date.spec.js | 422 ++-- .../mutually-exclusive.dom.js | 10 +- .../mutually-exclusive.duration.spec.js | 420 ++-- .../mutually-exclusive.email.spec.js | 180 +- .../mutually-exclusive/mutually-exclusive.js | 274 +-- ...lusive.multiple-options.checkboxes.spec.js | 394 ++-- .../mutually-exclusive.number.spec.js | 192 +- .../mutually-exclusive.textarea.spec.js | 204 +- src/components/navigation/_macro.spec.js | 714 +++---- src/components/navigation/_navigation.scss | 245 ++- src/components/navigation/navigation.dom.js | 70 +- src/components/navigation/navigation.js | 98 +- src/components/navigation/navigation.spec.js | 498 ++--- src/components/pagination/_macro.spec.js | 677 +++--- src/components/pagination/_pagination.scss | 116 +- src/components/panel/_macro.spec.js | 744 +++---- src/components/panel/_panel.scss | 399 ++-- src/components/password/_macro.spec.js | 190 +- src/components/password/password.dom.js | 10 +- src/components/password/password.js | 20 +- src/components/password/password.spec.js | 52 +- src/components/phase-banner/_macro.spec.js | 172 +- .../phase-banner/_phase-banner.scss | 32 +- src/components/question/_macro.spec.js | 470 ++--- src/components/question/_question.scss | 48 +- src/components/quote/_macro.spec.js | 104 +- src/components/quote/_quote.scss | 48 +- src/components/radios/_macro.spec.js | 1048 +++++----- src/components/radios/_radio.scss | 97 +- src/components/radios/_radios.scss | 30 +- src/components/radios/check-radios.js | 42 +- src/components/radios/clear-radios.js | 90 +- src/components/radios/radio-with-fieldset.js | 44 +- src/components/radios/radios.dom.js | 64 +- src/components/radios/radios.spec.js | 502 ++--- src/components/related-content/_macro.spec.js | 218 +- .../related-content/_related-content.scss | 24 +- .../related-content/_section-macro.spec.js | 40 +- src/components/relationships/_macro.spec.js | 188 +- .../relationships/_relationships.scss | 18 +- .../relationships/relationships.dom.js | 10 +- src/components/relationships/relationships.js | 36 +- .../relationships/relationships.spec.js | 142 +- src/components/reply/_macro.spec.js | 94 +- src/components/reply/reply-input.js | 30 +- src/components/reply/reply.dom.js | 10 +- src/components/reply/reply.spec.js | 114 +- .../section-navigation/_macro.spec.js | 420 ++-- .../_section-navigation.scss | 152 +- src/components/select/_macro.spec.js | 332 +-- src/components/share-page/_macro.spec.js | 136 +- src/components/skip-to-content/_macro.spec.js | 108 +- src/components/skip-to-content/_skip.scss | 60 +- .../skip-to-content/skip-to-content.dom.js | 12 +- .../skip-to-content/skip-to-content.js | 14 +- .../skip-to-content/skip-to-content.spec.js | 42 +- src/components/status/_macro.spec.js | 106 +- src/components/status/_status.scss | 64 +- src/components/summary/_macro.spec.js | 1086 +++++----- src/components/summary/_summary.scss | 386 ++-- .../table-of-contents/_macro.spec.js | 250 +-- src/components/table-of-contents/_toc.scss | 18 +- src/components/table-of-contents/toc.dom.js | 10 +- src/components/table-of-contents/toc.js | 60 +- src/components/table-of-contents/toc.spec.js | 176 +- src/components/table/_macro.spec.js | 998 ++++----- src/components/table/_table.scss | 405 ++-- src/components/table/scrollable-table.dom.js | 10 +- src/components/table/scrollable-table.js | 120 +- src/components/table/sortable-table.dom.js | 10 +- src/components/table/sortable-table.js | 270 +-- src/components/table/table.spec.js | 284 +-- src/components/tabs/_macro.spec.js | 184 +- src/components/tabs/_tabs.scss | 235 +-- src/components/tabs/tabs.dom.js | 10 +- src/components/tabs/tabs.js | 532 ++--- src/components/tabs/tabs.spec.js | 536 ++--- src/components/text-indent/_macro.spec.js | 66 +- src/components/text-indent/_text-indent.scss | 6 +- src/components/textarea/_macro.spec.js | 476 ++--- src/components/textarea/textarea.dom.js | 10 +- src/components/textarea/textarea.spec.js | 152 +- src/components/timeline/_macro.spec.js | 166 +- src/components/timeline/_timeline.scss | 52 +- src/components/timeout-modal/_macro.spec.js | 94 +- .../timeout-modal/timeout-modal.dom.js | 18 +- src/components/timeout-modal/timeout-modal.js | 132 +- .../timeout-modal/timeout-modal.spec.js | 314 +-- src/components/timeout-panel/_macro.spec.js | 82 +- .../timeout-panel/timeout-panel.dom.js | 16 +- .../timeout-panel/timeout-panel.spec.js | 236 +-- src/components/upload/_macro.spec.js | 104 +- src/components/upload/_upload.scss | 56 +- src/components/video/_macro.spec.js | 84 +- src/components/video/_video.scss | 32 +- src/components/video/video.dom.js | 10 +- src/components/video/video.js | 64 +- src/components/video/video.spec.js | 200 +- src/js/abortable-fetch.js | 46 +- src/js/analytics.js | 106 +- src/js/cookies-functions.js | 270 +-- src/js/cookies-settings.dom.js | 14 +- src/js/cookies-settings.js | 154 +- src/js/cookies-settings.spec.js | 212 +- src/js/domready.js | 16 +- src/js/fetch.js | 28 +- src/js/inpagelink.dom.js | 10 +- src/js/inpagelink.js | 38 +- src/js/polyfills.js | 1 - src/js/print-button.js | 12 +- src/js/timeout.js | 422 ++-- src/js/utils/matchMedia.js | 4 +- src/js/utils/objectHasInterface.js | 10 +- src/js/utils/viewport-change.js | 18 +- src/layout/_template.spec.js | 42 +- ...ess-input-non-editable-confirm-address.njk | 68 +- .../example-conversation.njk | 53 +- src/scss/base/_forms.scss | 20 +- src/scss/base/_global.scss | 89 +- src/scss/base/_typography.scss | 40 +- src/scss/helpers/_functions.scss | 33 +- src/scss/helpers/_mixins.scss | 112 +- src/scss/helpers/_mq.scss | 127 +- src/scss/objects/_container.scss | 40 +- src/scss/objects/_page.scss | 66 +- src/scss/objects/_spacing.scss | 20 +- src/scss/overrides/hcm.scss | 474 ++--- src/scss/overrides/rtl.scss | 190 +- src/scss/print.scss | 94 +- src/scss/utilities/_border.scss | 14 +- src/scss/utilities/_colors.scss | 12 +- src/scss/utilities/_display.scss | 16 +- src/scss/utilities/_float.scss | 14 +- src/scss/utilities/_grid.scss | 288 +-- src/scss/utilities/_highlight.scss | 8 +- src/scss/utilities/_margin.scss | 34 +- src/scss/utilities/_pad.scss | 30 +- src/scss/utilities/_typography.scss | 68 +- src/scss/utilities/_utilities.scss | 16 +- src/scss/utilities/_visibility.scss | 50 +- src/scss/vars/_colors.scss | 232 +-- src/scss/vars/_forms.scss | 44 +- src/scss/vars/_grid.scss | 20 +- src/scss/vars/_typography.scss | 108 +- .../examples/data/country-of-birth.json | 2 +- src/static/favicons/manifest.json | 50 +- src/tests/helpers/axe.js | 8 +- src/tests/helpers/cheerio.js | 4 +- src/tests/helpers/cheerio.spec.js | 20 +- src/tests/helpers/debug.js | 82 +- src/tests/helpers/fake-nunjucks-loader.js | 24 +- src/tests/helpers/puppeteer.js | 200 +- src/tests/helpers/puppeteer.spec.js | 392 ++-- src/tests/helpers/rendering.js | 212 +- src/tests/helpers/rendering.spec.js | 230 +-- src/tests/helpers/snapshotResolver.js | 18 +- src/tests/helpers/url-generator.js | 44 +- yarn.lock | 470 ++--- 327 files changed, 29326 insertions(+), 29329 deletions(-) diff --git a/.babelrc b/.babelrc index 6f8e9c6414..4a3debe65a 100644 --- a/.babelrc +++ b/.babelrc @@ -1,9 +1,12 @@ { "presets": [ - ["@babel/preset-env", { - "targets": { - "node": "20" + [ + "@babel/preset-env", + { + "targets": { + "node": "20" + } } - }] + ] ] } diff --git a/.editorconfig b/.editorconfig index 2ec4a16a29..192111c053 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,17 +4,7 @@ root = true [*] end_of_line = lf charset = utf-8 -trim_trailing_whitespace = false +trim_trailing_whitespace = true insert_final_newline = true indent_style = space -[{*.yml,*.json}] -indent_size = 2 -# Sass -[{*.scss, *.sass}] -indent_size = 2 -# Javascript + derivatives -[{*.coffee,*.js,*.ts}] -indent_size = 2 -# Templates -[*.pug,*.jade,*.njk] indent_size = 4 diff --git a/.eslintrc.json b/.eslintrc.json index a5d9b286f9..718a0db4ef 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,40 +1,39 @@ { - "parser": "@babel/eslint-parser", - "plugins": ["prettier", "unused-imports"], - "extends": ["prettier"], - "env": { - "es6": true, - "browser": true - }, - "globals": { - "$": true, - "jQuery": true, - "browser": true, - "ga": true - }, - "rules": { - "prettier/prettier": "error", - "no-unused-vars": [1], - "no-alert": 0, - "strict": [2, "never"], - "new-cap": [0], - "consistent-return": 0, - "no-underscore-dangle": 0, - "no-var": [1], - "one-var": [0], - "max-len": [0, { "code": 140, "ignoreUrls": true }], - "comma-dangle": ["error", "always-multiline"], - "arrow-parens": 1, - "indent": ["error", 2, { "SwitchCase": 1 }], - "unused-imports/no-unused-imports": "error", - "unused-imports/no-unused-vars": [ - "error", - { - "vars": "all", - "varsIgnorePattern": "^_", - "args": "after-used", - "argsIgnorePattern": "^_" - } - ] - } + "parser": "@babel/eslint-parser", + "plugins": ["prettier", "unused-imports"], + "extends": ["prettier"], + "env": { + "es6": true, + "browser": true + }, + "globals": { + "$": true, + "jQuery": true, + "browser": true, + "ga": true + }, + "rules": { + "prettier/prettier": "error", + "no-unused-vars": [1], + "no-alert": 0, + "strict": [2, "never"], + "new-cap": [0], + "consistent-return": 0, + "no-underscore-dangle": 0, + "no-var": [1], + "one-var": [0], + "max-len": [0, { "code": 140, "ignoreUrls": true }], + "comma-dangle": ["error", "always-multiline"], + "arrow-parens": 1, + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "error", + { + "vars": "all", + "varsIgnorePattern": "^_", + "args": "after-used", + "argsIgnorePattern": "^_" + } + ] + } } diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml index 6482275d49..b7d1d2c974 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yaml +++ b/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -1,102 +1,102 @@ name: Bug Report description: File a bug report -title: "[Bug]: " -labels: ["Bug", "Community backlog"] +title: '[Bug]: ' +labels: ['Bug', 'Community backlog'] assignees: [] body: - - type: markdown - attributes: - value: | - Thank you for taking the time to fill out this bug report - - type: textarea - id: what-happened - attributes: - label: What happened? - validations: - required: true - - type: textarea - id: expected-outcome - attributes: - label: What was the expected outcome? - validations: - required: true - - type: textarea - id: impact - attributes: - label: How is this problem impacting your service? - validations: - required: true - - type: textarea - id: reproduction-steps - attributes: - label: What list of steps can be followed to reproduce the bug or issue? - validations: - required: true - - type: textarea - id: example - attributes: - label: Example - description: Add screenshot(s) or examples of the bug or issue - placeholder: Add a file, image or link - validations: - required: false - - type: input - id: version - attributes: - label: What version of the ONS Design System are you using? - placeholder: For example, 32.2.7 - validations: - required: true - - type: dropdown - id: device - attributes: - label: What devices are you seeing the problem on? - options: - - Desktop - - Mobile - - Tablet - multiple: true - - type: input - id: device-details - attributes: - label: Device details - description: Here you can provide further information about the device(s) that are affected by this bug or issue. - validations: - required: false - - type: dropdown - id: operating-system - attributes: - label: What operating systems are you seeing the problem on? - options: - - Windows - - macOS - - Android - - iOS - - Other - multiple: true - - type: input - id: operating-system-details - attributes: - label: Operating system details - description: Here you can provide further information about the operating system(s) that are affected by this bug or issue. - validations: - required: false - - type: dropdown - id: browsers - attributes: - label: What browsers are you seeing the problem on? - options: - - Firefox - - Chrome - - Safari - - Microsoft Edge - - Microsoft Internet Explorer - - Other - multiple: true - - type: input - id: browsers-details - attributes: - label: Browser details - description: Here you can provide further information about the browser(s) that are affected by this bug or issue. - validations: - required: false + - type: markdown + attributes: + value: | + Thank you for taking the time to fill out this bug report + - type: textarea + id: what-happened + attributes: + label: What happened? + validations: + required: true + - type: textarea + id: expected-outcome + attributes: + label: What was the expected outcome? + validations: + required: true + - type: textarea + id: impact + attributes: + label: How is this problem impacting your service? + validations: + required: true + - type: textarea + id: reproduction-steps + attributes: + label: What list of steps can be followed to reproduce the bug or issue? + validations: + required: true + - type: textarea + id: example + attributes: + label: Example + description: Add screenshot(s) or examples of the bug or issue + placeholder: Add a file, image or link + validations: + required: false + - type: input + id: version + attributes: + label: What version of the ONS Design System are you using? + placeholder: For example, 32.2.7 + validations: + required: true + - type: dropdown + id: device + attributes: + label: What devices are you seeing the problem on? + options: + - Desktop + - Mobile + - Tablet + multiple: true + - type: input + id: device-details + attributes: + label: Device details + description: Here you can provide further information about the device(s) that are affected by this bug or issue. + validations: + required: false + - type: dropdown + id: operating-system + attributes: + label: What operating systems are you seeing the problem on? + options: + - Windows + - macOS + - Android + - iOS + - Other + multiple: true + - type: input + id: operating-system-details + attributes: + label: Operating system details + description: Here you can provide further information about the operating system(s) that are affected by this bug or issue. + validations: + required: false + - type: dropdown + id: browsers + attributes: + label: What browsers are you seeing the problem on? + options: + - Firefox + - Chrome + - Safari + - Microsoft Edge + - Microsoft Internet Explorer + - Other + multiple: true + - type: input + id: browsers-details + attributes: + label: Browser details + description: Here you can provide further information about the browser(s) that are affected by this bug or issue. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml index f4bf2585ed..9a3c7f466f 100644 --- a/.github/ISSUE_TEMPLATE/config.yaml +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -1,5 +1,5 @@ blank_issues_enabled: true contact_links: - - name: Community Discussions - url: https://github.com/ONSdigital/design-system/discussions - about: Please ask and answer questions here. + - name: Community Discussions + url: https://github.com/ONSdigital/design-system/discussions + about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yaml b/.github/ISSUE_TEMPLATE/feature-request.yaml index 868bbb874f..74aa4c2147 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yaml +++ b/.github/ISSUE_TEMPLATE/feature-request.yaml @@ -1,45 +1,45 @@ name: New feature proposal description: Propose a new component, pattern, foundation or documentation to add to the ONS Design System -title: "[Feature]: " -labels: ["New feature proposal", "Community backlog"] +title: '[Feature]: ' +labels: ['New feature proposal', 'Community backlog'] assignees: [] body: - - type: markdown - attributes: - value: | - **Before you fill in this proposal template:** - - If you are suggesting a change to a component, pattern or foundation that is already in the ONS Design System, please comment on its [GitHub discussion instead](https://github.com/ONSdigital/design-system/discussions). You can get to the discussion using the direct link at the bottom of each page in the Design System. - - If you are proposing something new, check the [ONS Design System community backlog](https://github.com/ONSdigital/design-system/projects/1) first. There may already be a proposal for something similar. You can add examples of your own evidence to an existing proposal. - - type: textarea - id: what - attributes: - label: What feature would you like to add to the ONS Design System? - description: Give a brief description of the new feature you want to propose for the Design System. This could be a component, pattern or foundation, or new documentation for an existing feature. - validations: - required: true - - type: textarea - id: why - attributes: - label: Why should this new feature be added to the Design System? - description: | - Explain why you think this feature or content should be added to the ONS Design System. - - What evidence do you have that your ONS service needs it? - - What evidence do you have that it meets the needs of the users of that service? - - Have you checked if it’s already in the ONS Design System? - - Have you considered creating a new variant of an existing feature instead of something new? - validations: - required: true - - type: textarea - id: supporting-material - attributes: - label: Supporting material - description: Include links to any examples, research, prototypes or code that support your proposal. - validations: - required: false - - type: textarea - id: contacts - attributes: - label: Contacts - description: The Design System working group has a weekly meeting. We will invite you to one of these meetings to give a show and tell about your proposal and answer any questions. Please tell us who to invite to that meeting. We recommend involving at least your product manager, user researcher and designer. - validations: - required: false + - type: markdown + attributes: + value: | + **Before you fill in this proposal template:** + - If you are suggesting a change to a component, pattern or foundation that is already in the ONS Design System, please comment on its [GitHub discussion instead](https://github.com/ONSdigital/design-system/discussions). You can get to the discussion using the direct link at the bottom of each page in the Design System. + - If you are proposing something new, check the [ONS Design System community backlog](https://github.com/ONSdigital/design-system/projects/1) first. There may already be a proposal for something similar. You can add examples of your own evidence to an existing proposal. + - type: textarea + id: what + attributes: + label: What feature would you like to add to the ONS Design System? + description: Give a brief description of the new feature you want to propose for the Design System. This could be a component, pattern or foundation, or new documentation for an existing feature. + validations: + required: true + - type: textarea + id: why + attributes: + label: Why should this new feature be added to the Design System? + description: | + Explain why you think this feature or content should be added to the ONS Design System. + - What evidence do you have that your ONS service needs it? + - What evidence do you have that it meets the needs of the users of that service? + - Have you checked if it’s already in the ONS Design System? + - Have you considered creating a new variant of an existing feature instead of something new? + validations: + required: true + - type: textarea + id: supporting-material + attributes: + label: Supporting material + description: Include links to any examples, research, prototypes or code that support your proposal. + validations: + required: false + - type: textarea + id: contacts + attributes: + label: Contacts + description: The Design System working group has a weekly meeting. We will invite you to one of these meetings to give a show and tell about your proposal and answer any questions. Please tell us who to invite to that meeting. We recommend involving at least your product manager, user researcher and designer. + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 63118a5edb..8f5b143c1a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,5 +14,5 @@ This needs to be completed by the person raising the PR. -- [ ] I have selected the correct Assignee -- [ ] I have linked the correct Issue +- [ ] I have selected the correct Assignee +- [ ] I have linked the correct Issue diff --git a/.github/release.yml b/.github/release.yml index 1543fec15a..6c46025da6 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -1,30 +1,30 @@ changelog: - categories: - - title: ⚠️ Breaking changes - labels: - - Breaking changes - - title: New features - labels: - - Component - - Pattern - - title: Accessibility improvements - labels: - - Accessibility - - title: Enhancements - labels: - - Enhancement - - CI/CD - - Tech improvements - - title: Fixes - labels: - - Bug - - title: Documentation - labels: - - Documentation - - Example - - title: Dependency updates - labels: - - Dependencies - - title: Other changes - labels: - - "*" + categories: + - title: ⚠️ Breaking changes + labels: + - Breaking changes + - title: New features + labels: + - Component + - Pattern + - title: Accessibility improvements + labels: + - Accessibility + - title: Enhancements + labels: + - Enhancement + - CI/CD + - Tech improvements + - title: Fixes + labels: + - Bug + - title: Documentation + labels: + - Documentation + - Example + - title: Dependency updates + labels: + - Dependencies + - title: Other changes + labels: + - '*' diff --git a/.github/workflows/check-checklist.yml b/.github/workflows/check-checklist.yml index 92cfe63e43..0fa60a0cb2 100644 --- a/.github/workflows/check-checklist.yml +++ b/.github/workflows/check-checklist.yml @@ -1,18 +1,18 @@ name: Checklist Checker on: - pull_request: - types: - - opened - - edited - - synchronize - - reopened + pull_request: + types: + - opened + - edited + - synchronize + - reopened jobs: - check-tasks: - runs-on: ubuntu-latest - if: github.event.pull_request.user.login != 'dependabot[bot]' # ignore pull requests raised by dependabot - steps: - - name: Check tasks - uses: mheap/require-checklist-action@v2 - with: - requireChecklist: true + check-tasks: + runs-on: ubuntu-latest + if: github.event.pull_request.user.login != 'dependabot[bot]' # ignore pull requests raised by dependabot + steps: + - name: Check tasks + uses: mheap/require-checklist-action@v2 + with: + requireChecklist: true diff --git a/.github/workflows/check-labels.yml b/.github/workflows/check-labels.yml index 420e7136d0..e92c31fe7e 100644 --- a/.github/workflows/check-labels.yml +++ b/.github/workflows/check-labels.yml @@ -1,22 +1,22 @@ name: Label Checker on: - pull_request: - types: - - opened - - synchronize - - reopened - - labeled - - unlabeled + pull_request: + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled jobs: - check-labels: - name: Check labels - runs-on: ubuntu-latest - permissions: - pull-requests: read - steps: - - uses: docker://onsdigital/github-pr-label-checker:latest - with: - one_of: Accessibility,Bug,Documentation,Dependencies,Enhancement,Example,Component,Pattern,CI/CD,Tech improvements - none_of: Awaiting resource,Do not merge,Duplicate,Needs validating,Not doing - repo_token: ${{ secrets.GITHUB_TOKEN }} + check-labels: + name: Check labels + runs-on: ubuntu-latest + permissions: + pull-requests: read + steps: + - uses: docker://onsdigital/github-pr-label-checker:latest + with: + one_of: Accessibility,Bug,Documentation,Dependencies,Enhancement,Example,Component,Pattern,CI/CD,Tech improvements + none_of: Awaiting resource,Do not merge,Duplicate,Needs validating,Not doing + repo_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lighthouse-ci.yml b/.github/workflows/lighthouse-ci.yml index 22347872e3..661548623f 100644 --- a/.github/workflows/lighthouse-ci.yml +++ b/.github/workflows/lighthouse-ci.yml @@ -2,25 +2,25 @@ name: Lighthouse CI accessibility checks on: [pull_request] env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} jobs: - lhci: - name: Lighthouse CI - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - - name: Install Node - uses: actions/setup-node@v4 - with: - node-version-file: ".nvmrc" - - name: Install dependencies - run: yarn install - - name: Build pages - run: yarn build - - name: Run Lighthouse CI - run: ./lighthouse/lighthouse.sh - env: - LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} + lhci: + name: Lighthouse CI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + - name: Install dependencies + run: yarn install + - name: Build pages + run: yarn build + - name: Run Lighthouse CI + run: ./lighthouse/lighthouse.sh + env: + LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} diff --git a/.github/workflows/macro-and-script-tests.yml b/.github/workflows/macro-and-script-tests.yml index 203e6947ae..e34598dfd5 100644 --- a/.github/workflows/macro-and-script-tests.yml +++ b/.github/workflows/macro-and-script-tests.yml @@ -2,21 +2,21 @@ name: Run macro and script tests on: [push] env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - RUNNING_IN_CI: true + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + RUNNING_IN_CI: true jobs: - run-macro-and-script-tests: - runs-on: ubuntu-latest + run-macro-and-script-tests: + runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Install Node - uses: actions/setup-node@v4 - with: - node-version-file: ".nvmrc" - - name: Install dependencies - run: yarn install - - name: Run macro and script tests - run: yarn test + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + - name: Install dependencies + run: yarn install + - name: Run macro and script tests + run: yarn test diff --git a/.github/workflows/npm-bundle.yml b/.github/workflows/npm-bundle.yml index 12d729c492..6482768624 100644 --- a/.github/workflows/npm-bundle.yml +++ b/.github/workflows/npm-bundle.yml @@ -1,29 +1,29 @@ name: Publish Design System to npm on: - release: - types: [published] + release: + types: [published] env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Use Node.js - uses: actions/checkout@v4 - with: - ref: ${{ github.event.release.target_commitish }} - - name: Install Node - uses: actions/setup-node@v4 - with: - node-version-file: ".nvmrc" - registry-url: https://registry.npmjs.org/ - - run: yarn install - - run: git config --global user.name "${{ github.actor }}" - - run: git config --global user.email "github-action-${{ github.actor }}@users.noreply.github.com" - - run: npm version ${{ github.event.release.tag_name }} - - run: yarn npm-bundle - - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + build: + runs-on: ubuntu-latest + steps: + - name: Use Node.js + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.target_commitish }} + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + registry-url: https://registry.npmjs.org/ + - run: yarn install + - run: git config --global user.name "${{ github.actor }}" + - run: git config --global user.email "github-action-${{ github.actor }}@users.noreply.github.com" + - run: npm version ${{ github.event.release.tag_name }} + - run: yarn npm-bundle + - run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 22ad0c0815..c62e1bc34b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,28 +1,28 @@ name: Release on: - release: - types: [published] + release: + types: [published] jobs: - add-templates-to-release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Install Node - uses: actions/setup-node@v4 - with: - node-version-file: ".nvmrc" - - name: Install dependencies - run: yarn install - - name: Build templates - run: | - cd src - RELEASE_VERSION=${{ github.event.release.tag_name }} yarn cdn-bundle - - name: Zip templates - run: zip -r templates.zip templates/* - - name: Release - uses: softprops/action-gh-release@v1 - with: - files: templates.zip - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + add-templates-to-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + - name: Install dependencies + run: yarn install + - name: Build templates + run: | + cd src + RELEASE_VERSION=${{ github.event.release.tag_name }} yarn cdn-bundle + - name: Zip templates + run: zip -r templates.zip templates/* + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: templates.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/visual-regression-tests.yml b/.github/workflows/visual-regression-tests.yml index f0dab1d362..2539a2d025 100644 --- a/.github/workflows/visual-regression-tests.yml +++ b/.github/workflows/visual-regression-tests.yml @@ -2,37 +2,37 @@ name: Run visual regression tests on: [push] env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - RUNNING_IN_CI: true + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + RUNNING_IN_CI: true jobs: - run-visual-regression-tests: - runs-on: macos-12 + run-visual-regression-tests: + runs-on: macos-12 - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - lfs: true - - name: Install Node - uses: actions/setup-node@v4 - with: - node-version-file: ".nvmrc" - - name: Install dependencies - run: yarn install - - name: Install Docker - run: | - brew install docker - colima start - - name: Run visual regression tests - id: vr - run: yarn test-visual - # Archive report artifacts if visual regression tests fail - - name: Archive BackstopJS reports - if: ${{ failure() }} - uses: actions/upload-artifact@v4 - with: - name: backstop-reports - path: | - backstop_data/bitmaps_test - backstop_data/html_report + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: true + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + - name: Install dependencies + run: yarn install + - name: Install Docker + run: | + brew install docker + colima start + - name: Run visual regression tests + id: vr + run: yarn test-visual + # Archive report artifacts if visual regression tests fail + - name: Archive BackstopJS reports + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: backstop-reports + path: | + backstop_data/bitmaps_test + backstop_data/html_report diff --git a/.remarkrc b/.remarkrc index 869d2dc8ce..961f56aee6 100644 --- a/.remarkrc +++ b/.remarkrc @@ -1,6 +1,6 @@ { - "plugins": [ - "remark-preset-lint-recommended", - ["remark-lint-list-item-indent", false] - ] + "plugins": [ + "remark-preset-lint-recommended", + ["remark-lint-list-item-indent", false] + ] } diff --git a/.stylelintrc.yml b/.stylelintrc.yml index bc7a9f14a1..288fb8d345 100644 --- a/.stylelintrc.yml +++ b/.stylelintrc.yml @@ -1,7 +1,7 @@ defaultSeverity: warning extends: - stylelint-config-standard - - stylelint-config-recommended + - stylelint-config-recommended-scss - stylelint-config-sass-guidelines plugins: - stylelint-scss @@ -11,40 +11,40 @@ rules: rule-empty-line-before: - always-multi-line - ignore: - - after-comment - - first-nested - - inside-block + - after-comment + - first-nested + - inside-block # Disallows selector-max-id: 2 comment-no-empty: true declaration-block-no-duplicate-properties: true + at-rule-empty-line-before: null block-no-empty: - true - ignore: - - comments + - comments no-descending-specificity: null - no-extra-semicolons: true color-no-invalid-hex: true - number-no-trailing-zeros: true length-zero-no-unit: true - scss/at-extend-no-missing-placeholder: null selector-no-qualifying-type: - true - ignore: - - attribute - - class - - id + - attribute + - class + - id property-no-unknown: - true - ignoreProperties: - - 'box-orient' + - 'box-orient' - ignoreAtRules: ['each', 'else', 'extend', 'for', 'function', 'if', 'include', 'mixin', 'return', 'while'] + # Imports import-notation: 'string' + # Nesting max-nesting-depth: - - 5 + - 6 - ignore: ['blockless-at-rules', 'pseudo-classes'] # Name Formats @@ -52,29 +52,18 @@ rules: scss/at-mixin-pattern: null # Style Guide - indentation: 2 color-named: 'never' - declaration-block-trailing-semicolon: always - unit-case: lower - color-hex-case: lower - number-leading-zero: always unit-no-unknown: - true - ignoreUnits: - - /^[-+][\d$(]/ + - /^[-+][\d$(]/ font-family-no-duplicate-names: true - string-quotes: single selector-max-compound-selectors: 6 order/order: null # Inner Spacing function-calc-no-unspaced-operator: true - declaration-block-semicolon-newline-after: always - block-opening-brace-space-before: always - block-opening-brace-newline-after: always - block-closing-brace-newline-after: always - declaration-bang-space-after: never - declaration-colon-space-after: always - # Final Items - scss/at-import-partial-extension-blacklist: [/css/] + # SCSS + scss/at-extend-no-missing-placeholder: null + scss/operator-no-newline-after: null diff --git a/README.md b/README.md index d900e45f92..acdf2ac56d 100644 --- a/README.md +++ b/README.md @@ -106,9 +106,9 @@ _Note_: This command is of limited use since JavaScript and SCSS files will only It is sometimes useful to adjust the following settings when writing tests or diagnosing issues: -- `headless` in 'jest-puppeteer.config.js' - when set to `false` will show web browser whilst running tests. Many browser windows open since jest runs tests in parallel so it is useful to also adjust the `test` script inside 'package.json' such that it targets a specific test file. `await page.waitForTimeout(100000)` can be temporarily added to a test to allow yourself time to inspect the browser that appears. +- `headless` in 'jest-puppeteer.config.js' - when set to `false` will show web browser whilst running tests. Many browser windows open since jest runs tests in parallel so it is useful to also adjust the `test` script inside 'package.json' such that it targets a specific test file. `await page.waitForTimeout(100000)` can be temporarily added to a test to allow yourself time to inspect the browser that appears. -- `testTimeout` in 'jest.config.js' - set to a high value such as `1000000` to prevent tests from timing out when doing the above. +- `testTimeout` in 'jest.config.js' - set to a high value such as `1000000` to prevent tests from timing out when doing the above. ## Testing - Visual regression tests @@ -146,18 +146,12 @@ yarn build ## Recommended Visual Studio Code Extensions for this project -- [axe Accessibility Linter](https://marketplace.visualstudio.com/items?itemName=deque-systems.vscode-axe-linter) -- [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) -- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) -- [Nunjucks](https://marketplace.visualstudio.com/items?itemName=ronnidc.nunjucks) -- [Prettier - Code Formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) -- [remark](https://marketplace.visualstudio.com/items?itemName=unifiedjs.vscode-remark) -- [SonarLint](https://marketplace.visualstudio.com/items?itemName=SonarSource.sonarlint-vscode) -- [Sass](https://marketplace.visualstudio.com/items?itemName=Syler.sass-indented) -- [Stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint) - -## Recommended Visual Studio Code settings for this project - -- "render whitespace" set to "all" -- "insert spaces when pressing tab" checked -- "trim trailing whitespace" checked +- [axe Accessibility Linter](https://marketplace.visualstudio.com/items?itemName=deque-systems.vscode-axe-linter) +- [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) +- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) +- [Nunjucks](https://marketplace.visualstudio.com/items?itemName=ronnidc.nunjucks) +- [Prettier - Code Formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) +- [remark](https://marketplace.visualstudio.com/items?itemName=unifiedjs.vscode-remark) +- [SonarLint](https://marketplace.visualstudio.com/items?itemName=SonarSource.sonarlint-vscode) +- [Sass](https://marketplace.visualstudio.com/items?itemName=Syler.sass-indented) +- [Stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint) diff --git a/babel.conf.esm.js b/babel.conf.esm.js index 2215b7a049..3c315a5ccc 100644 --- a/babel.conf.esm.js +++ b/babel.conf.esm.js @@ -1,18 +1,18 @@ module.exports = { - babelrc: false, - plugins: ['@babel/plugin-syntax-dynamic-import', '@babel/plugin-proposal-class-properties', '@babel/plugin-transform-runtime'], - global: true, - ignore: [/node_modules\/.*/], - sourceType: 'module', - presets: [ - [ - '@babel/preset-env', - { - modules: 'commonjs', - targets: { - browsers: ['last 2 Chrome versions', 'Safari >= 12', 'iOS >= 10.3', 'last 2 Firefox versions', 'last 2 Edge versions'], - }, - }, + babelrc: false, + plugins: ['@babel/plugin-syntax-dynamic-import', '@babel/plugin-proposal-class-properties', '@babel/plugin-transform-runtime'], + global: true, + ignore: [/node_modules\/.*/], + sourceType: 'module', + presets: [ + [ + '@babel/preset-env', + { + modules: 'commonjs', + targets: { + browsers: ['last 2 Chrome versions', 'Safari >= 12', 'iOS >= 10.3', 'last 2 Firefox versions', 'last 2 Edge versions'], + }, + }, + ], ], - ], }; diff --git a/babel.conf.nomodule.js b/babel.conf.nomodule.js index e5acf08d37..cbd9c0bbc6 100644 --- a/babel.conf.nomodule.js +++ b/babel.conf.nomodule.js @@ -1,18 +1,18 @@ module.exports = { - babelrc: false, - plugins: ['@babel/plugin-syntax-dynamic-import', '@babel/plugin-proposal-class-properties', '@babel/plugin-transform-runtime'], - global: true, - ignore: [/node_modules\/.*/], - sourceType: 'module', - presets: [ - [ - '@babel/preset-env', - { - modules: 'commonjs', - targets: { - browsers: ['ie 11'], - }, - }, + babelrc: false, + plugins: ['@babel/plugin-syntax-dynamic-import', '@babel/plugin-proposal-class-properties', '@babel/plugin-transform-runtime'], + global: true, + ignore: [/node_modules\/.*/], + sourceType: 'module', + presets: [ + [ + '@babel/preset-env', + { + modules: 'commonjs', + targets: { + browsers: ['ie 11'], + }, + }, + ], ], - ], }; diff --git a/backstop.config.js b/backstop.config.js index cddcfa5357..4fea6a2fba 100644 --- a/backstop.config.js +++ b/backstop.config.js @@ -1,44 +1,44 @@ module.exports = { - id: 'ds-vr-test', - viewports: [ - { - name: 'desktop', - width: 1920, - height: 1080, - }, - { - name: 'tablet', - width: 768, - height: 1024, + id: 'ds-vr-test', + viewports: [ + { + name: 'desktop', + width: 1920, + height: 1080, + }, + { + name: 'tablet', + width: 768, + height: 1024, + }, + { + name: 'mobile', + width: 375, + height: 667, + }, + ], + scenarios: [], + paths: { + bitmaps_reference: 'backstop_data/bitmaps_reference', + bitmaps_test: 'backstop_data/bitmaps_test', + engine_scripts: 'backstop_data/engine_scripts', + html_report: 'backstop_data/html_report', + ci_report: 'backstop_data/ci_report', }, - { - name: 'mobile', - width: 375, - height: 667, + engine: 'puppeteer', + engineOptions: { + args: [ + '--disable-gpu', + '--no-sandbox', + '--disable-setuid-sandbox', + '--shm-size=512mb', + '--disable-dev-shm-usage', + '--cap-add=SYS_ADMIN', + ], + headless: 'old', + gotoParameters: { waitUntil: 'networkidle0' }, }, - ], - scenarios: [], - paths: { - bitmaps_reference: 'backstop_data/bitmaps_reference', - bitmaps_test: 'backstop_data/bitmaps_test', - engine_scripts: 'backstop_data/engine_scripts', - html_report: 'backstop_data/html_report', - ci_report: 'backstop_data/ci_report', - }, - engine: 'puppeteer', - engineOptions: { - args: [ - '--disable-gpu', - '--no-sandbox', - '--disable-setuid-sandbox', - '--shm-size=512mb', - '--disable-dev-shm-usage', - '--cap-add=SYS_ADMIN', - ], - headless: 'old', - gotoParameters: { waitUntil: 'networkidle0' }, - }, - report: process.env.RUNNING_IN_CI === 'true' ? [] : ['browser'], - dockerCommandTemplate: - 'docker run --rm -i --user $(id -u):$(id -g) --mount type=bind,source="{cwd}",target=/src backstopjs/backstopjs:{version} {backstopCommand} {args}', + report: process.env.RUNNING_IN_CI === 'true' ? [] : ['browser'], + dockerCommandTemplate: + 'docker run --rm -i --user $(id -u):$(id -g) --mount type=bind,source="{cwd}",target=/src backstopjs/backstopjs:{version} {backstopCommand} {args}', }; diff --git a/ci/common.js b/ci/common.js index 713d358186..058e0383c2 100644 --- a/ci/common.js +++ b/ci/common.js @@ -1,40 +1,40 @@ import * as fs from 'fs-extra'; export async function copyComponents(componentsPath, newComponentsPath) { - await fs.ensureDir(newComponentsPath); + await fs.ensureDir(newComponentsPath); - const items = (await fs.readdir(componentsPath)).filter((path) => !path.includes('.njk')); - for (let item of items) { - await copyComponent(item, componentsPath, newComponentsPath); - } + const items = (await fs.readdir(componentsPath)).filter((path) => !path.includes('.njk')); + for (let item of items) { + await copyComponent(item, componentsPath, newComponentsPath); + } } async function copyComponent(componentName, componentsPath, newComponentsPath) { - const componentPath = `${componentsPath}/${componentName}`; - const newComponentPath = `${newComponentsPath}/${componentName}`; + const componentPath = `${componentsPath}/${componentName}`; + const newComponentPath = `${newComponentsPath}/${componentName}`; - const items = (await fs.readdir(componentPath)).filter( - (item) => !item.includes('.md') && !item.includes('_test-') && !item.includes('index') && item !== 'examples', - ); + const items = (await fs.readdir(componentPath)).filter( + (item) => !item.includes('.md') && !item.includes('_test-') && !item.includes('index') && item !== 'examples', + ); - if (items.length) { - await fs.ensureDir(newComponentPath); - } + if (items.length) { + await fs.ensureDir(newComponentPath); + } - for (let path of items) { - await fs.copy(`${componentPath}/${path}`, `${newComponentPath}/${path}`); - } + for (let path of items) { + await fs.copy(`${componentPath}/${path}`, `${newComponentPath}/${path}`); + } } export async function copyTemplates(templatesPath, newTemplatesPath) { - await fs.ensureDir(newTemplatesPath); + await fs.ensureDir(newTemplatesPath); - const items = (await fs.readdir(templatesPath)).filter((path) => path.includes('.njk') && path.includes('_')); - for (let item of items) { - await copyTemplate(item, templatesPath, newTemplatesPath); - } + const items = (await fs.readdir(templatesPath)).filter((path) => path.includes('.njk') && path.includes('_')); + for (let item of items) { + await copyTemplate(item, templatesPath, newTemplatesPath); + } } async function copyTemplate(templateFileName, templatesPath, newTemplatesPath) { - await fs.copy(`${templatesPath}/${templateFileName}`, `${newTemplatesPath}/${templateFileName}`); + await fs.copy(`${templatesPath}/${templateFileName}`, `${newTemplatesPath}/${templateFileName}`); } diff --git a/ci/generate-npm-package.js b/ci/generate-npm-package.js index 29670074fb..d3ff6b8173 100644 --- a/ci/generate-npm-package.js +++ b/ci/generate-npm-package.js @@ -13,55 +13,55 @@ const builtAssetsFolders = assetFolders.map((folder) => `${cwd}/build/${folder}` const newSassPath = `${cwd}/scss`; async function removeExistingFolders() { - const folders = [newComponentsPath, newTemplatesPath, 'fonts', ...assetFolders]; - for (let folder of folders) { - await fs.remove(folder); - } + const folders = [newComponentsPath, newTemplatesPath, 'fonts', ...assetFolders]; + for (let folder of folders) { + await fs.remove(folder); + } } async function copyAssets() { - for (let assetFolderIndex = 0; assetFolderIndex < assetFolders.length; ++assetFolderIndex) { - const folder = assetFolders[assetFolderIndex]; - const builtPath = builtAssetsFolders[assetFolderIndex]; + for (let assetFolderIndex = 0; assetFolderIndex < assetFolders.length; ++assetFolderIndex) { + const folder = assetFolders[assetFolderIndex]; + const builtPath = builtAssetsFolders[assetFolderIndex]; - await fs.ensureDir(folder); + await fs.ensureDir(folder); - try { - const files = (await fs.readdir(builtPath)).filter((path) => !path.includes('patternlib')); + try { + const files = (await fs.readdir(builtPath)).filter((path) => !path.includes('patternlib')); - for (let file of files) { - if (file.match(/(\.\w+)$/)) { - await fs.copy(`${builtPath}/${file}`, `${folder}/${file}`); - } else { - const newFolderPath = `${folder}/${file}`; - const nestedBuiltPath = `${builtPath}/${file}`; - await fs.ensureDir(newFolderPath); + for (let file of files) { + if (file.match(/(\.\w+)$/)) { + await fs.copy(`${builtPath}/${file}`, `${folder}/${file}`); + } else { + const newFolderPath = `${folder}/${file}`; + const nestedBuiltPath = `${builtPath}/${file}`; + await fs.ensureDir(newFolderPath); - const nestedFiles = (await fs.readdir(nestedBuiltPath)).filter((path) => !path.includes('patternlib')); + const nestedFiles = (await fs.readdir(nestedBuiltPath)).filter((path) => !path.includes('patternlib')); - for (let nestedFile of nestedFiles) { - await fs.copy(`${nestedBuiltPath}/${nestedFile}`, `${newFolderPath}/${nestedFile}`); - } + for (let nestedFile of nestedFiles) { + await fs.copy(`${nestedBuiltPath}/${nestedFile}`, `${newFolderPath}/${nestedFile}`); + } + } + } + } catch (error) { + throw new Error(error); } - } - } catch (error) { - throw new Error(error); } - } } async function copyBaseSassAndFonts() { - await fs.ensureDir(newSassPath); - await fs.copy(`${sourcePath}/scss`, `${newSassPath}`); - await fs.copy(`${sourcePath}/static/fonts`, `${cwd}/fonts`); + await fs.ensureDir(newSassPath); + await fs.copy(`${sourcePath}/scss`, `${newSassPath}`); + await fs.copy(`${sourcePath}/static/fonts`, `${cwd}/fonts`); } async function run() { - await removeExistingFolders(); - await copyComponents(componentsPath, newComponentsPath); - await copyTemplates(templatesPath, newTemplatesPath); - await copyAssets(); - await copyBaseSassAndFonts(); + await removeExistingFolders(); + await copyComponents(componentsPath, newComponentsPath); + await copyTemplates(templatesPath, newTemplatesPath); + await copyAssets(); + await copyBaseSassAndFonts(); } run(); diff --git a/ci/prepare-templates-for-zip.js b/ci/prepare-templates-for-zip.js index 0da39f9963..c5f1cb39df 100644 --- a/ci/prepare-templates-for-zip.js +++ b/ci/prepare-templates-for-zip.js @@ -12,30 +12,30 @@ const newTemplatesPath = `${destPath}/layout`; const copiedPageTemplatePath = `${newTemplatesPath}/_template.njk`; async function removeExistingFolders() { - const folders = [newComponentsPath, newTemplatesPath]; - for (let folder of folders) { - await fs.remove(folder); - } + const folders = [newComponentsPath, newTemplatesPath]; + for (let folder of folders) { + await fs.remove(folder); + } } async function addCDNVersionToPageTemplate() { - const version = process.env.RELEASE_VERSION; + const version = process.env.RELEASE_VERSION; - if (version) { - const insert = `{% set release_version = '${version}' %}\n`; + if (version) { + const insert = `{% set release_version = '${version}' %}\n`; - prependFile.sync(copiedPageTemplatePath, insert); - } else { - throw new Error('RELEASE_VERSION not specified'); - } + prependFile.sync(copiedPageTemplatePath, insert); + } else { + throw new Error('RELEASE_VERSION not specified'); + } } async function run() { - await removeExistingFolders(); - await fs.ensureDir(destPath); - await copyComponents(componentsPath, newComponentsPath); - await copyTemplates(templatesPath, newTemplatesPath); - await addCDNVersionToPageTemplate(); + await removeExistingFolders(); + await fs.ensureDir(destPath); + await copyComponents(componentsPath, newComponentsPath); + await copyTemplates(templatesPath, newTemplatesPath); + await addCDNVersionToPageTemplate(); } run(); diff --git a/config/components.json b/config/components.json index 1ad4b464c2..c78dc996e5 100644 --- a/config/components.json +++ b/config/components.json @@ -1,25 +1,25 @@ { - "access-code": { - "macroName": "onsAccessCode" - }, - "char-check-limit": { - "macroName": "onsCharLimit" - }, - "checkboxes/checkbox": { - "templateName": "components/checkboxes/_checkbox-macro.njk", - "macroName": "onsCheckbox" - }, - "icon": { - "macroName": "onsIcon" - }, - "image": { - "macroName": "onsImage" - }, - "list": { - "macroName": "onsList" - }, - "related-content/section": { - "templateName": "components/related-content/_section-macro.njk", - "macroName": "onsRelatedContentSection" - } + "access-code": { + "macroName": "onsAccessCode" + }, + "char-check-limit": { + "macroName": "onsCharLimit" + }, + "checkboxes/checkbox": { + "templateName": "components/checkboxes/_checkbox-macro.njk", + "macroName": "onsCheckbox" + }, + "icon": { + "macroName": "onsIcon" + }, + "image": { + "macroName": "onsImage" + }, + "list": { + "macroName": "onsList" + }, + "related-content/section": { + "templateName": "components/related-content/_section-macro.njk", + "macroName": "onsRelatedContentSection" + } } diff --git a/gulpfile.js b/gulpfile.js index 64dffcbdd0..b5f17d6c0f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -22,107 +22,107 @@ const isProduction = process.env.NODE_ENV === 'production'; const isDevelopment = !isProduction; const terserOptions = { - compress: { - drop_console: true, - }, + compress: { + drop_console: true, + }, }; const sassOptions = { - includePaths: ['./node_modules/normalize.css'], - outputStyle: 'compressed', + includePaths: ['./node_modules/normalize.css'], + outputStyle: 'compressed', }; const scripts = [ - { - entryPoint: './src/js/main.js', - outputFile: 'main.js', - config: babelEsmConfig, - }, - { - entryPoint: ['./src/js/polyfills.js', './src/js/main.js'], - outputFile: 'main.es5.js', - config: babelNomoduleConfig, - }, + { + entryPoint: './src/js/main.js', + outputFile: 'main.js', + config: babelEsmConfig, + }, + { + entryPoint: ['./src/js/polyfills.js', './src/js/main.js'], + outputFile: 'main.es5.js', + config: babelNomoduleConfig, + }, ]; gulp.task('clean', () => { - return Promise.resolve(); + return Promise.resolve(); }); function createBuildScriptTask({ entryPoint, outputFile, config }) { - const taskName = `build-script:${outputFile}`; - gulp.task(taskName, () => { - return browserify(entryPoint, { debug: isDevelopment }) - .transform('babelify', { ...config, sourceMaps: isDevelopment }) - .bundle() - .pipe(source(outputFile)) - .pipe(buffer()) - .pipe(gulpIf(isDevelopment, gulpSourcemaps.init({ loadMaps: true }))) - .pipe(gulpIf(isProduction, gulpTerser(terserOptions))) - .pipe(gulpIf(isDevelopment, gulpSourcemaps.write('./'))) - .pipe(gulp.dest('./build/scripts')) - .pipe(browserSync.stream()); - }); - return taskName; + const taskName = `build-script:${outputFile}`; + gulp.task(taskName, () => { + return browserify(entryPoint, { debug: isDevelopment }) + .transform('babelify', { ...config, sourceMaps: isDevelopment }) + .bundle() + .pipe(source(outputFile)) + .pipe(buffer()) + .pipe(gulpIf(isDevelopment, gulpSourcemaps.init({ loadMaps: true }))) + .pipe(gulpIf(isProduction, gulpTerser(terserOptions))) + .pipe(gulpIf(isDevelopment, gulpSourcemaps.write('./'))) + .pipe(gulp.dest('./build/scripts')) + .pipe(browserSync.stream()); + }); + return taskName; } gulp.task('build-script', gulp.series(...scripts.map(createBuildScriptTask))); gulp.task('build-styles', () => { - return gulp - .src(`./src/scss/*.scss`) - .pipe(gulpIf(isDevelopment, gulpSourcemaps.init())) - .pipe(gulpDartSass(sassOptions).on('error', gulpDartSass.logError)) - .pipe(gulpPostCss(postCssPlugins())) - .pipe(gulpIf(isDevelopment, gulpSourcemaps.write('./'))) - .pipe(gulp.dest('./build/css')) - .pipe(browserSync.stream()); + return gulp + .src(`./src/scss/*.scss`) + .pipe(gulpIf(isDevelopment, gulpSourcemaps.init())) + .pipe(gulpDartSass(sassOptions).on('error', gulpDartSass.logError)) + .pipe(gulpPostCss(postCssPlugins())) + .pipe(gulpIf(isDevelopment, gulpSourcemaps.write('./'))) + .pipe(gulp.dest('./build/css')) + .pipe(browserSync.stream()); }); gulp.task('copy-static-files', () => { - return gulp.src('./src/static/**/*').pipe(gulp.dest('./build')); + return gulp.src('./src/static/**/*').pipe(gulp.dest('./build')); }); gulp.task('copy-js-files', () => { - return gulp.src('./src/js/*.js').pipe(gulp.dest('./build/js')); + return gulp.src('./src/js/*.js').pipe(gulp.dest('./build/js')); }); gulp.task('generate-pages', async function () { - await generateStaticPages(); + await generateStaticPages(); }); gulp.task('generate-urls', async () => { - const urls = await generateURLs(); - return urls; + const urls = await generateURLs(); + return urls; }); function createBackstopTask(task) { - return (backstopTestTask = async () => { - const urls = await generateURLs(); - const backstopConfig = require('./backstop.config.js'); - backstopConfig.scenarios = urls; - await backstop(task, { - docker: true, - config: backstopConfig, + return (backstopTestTask = async () => { + const urls = await generateURLs(); + const backstopConfig = require('./backstop.config.js'); + backstopConfig.scenarios = urls; + await backstop(task, { + docker: true, + config: backstopConfig, + }); + setTimeout(() => { + process.exit(); + }, 0); }); - setTimeout(() => { - process.exit(); - }, 0); - }); } gulp.task('watch-and-build', async () => { - browserSync.init({ - proxy: 'localhost:3010', - }); + browserSync.init({ + proxy: 'localhost:3010', + }); - gulp.watch('./src/**/*.njk').on('change', browserSync.reload); - gulp.watch('./src/**/*.scss', gulp.series('build-styles')); - gulp.watch('./src/**/*.js', gulp.series('build-script')); + gulp.watch('./src/**/*.njk').on('change', browserSync.reload); + gulp.watch('./src/**/*.scss', gulp.series('build-styles')); + gulp.watch('./src/**/*.js', gulp.series('build-script')); }); gulp.task('start-dev-server', async () => { - await import('./lib/dev-server.js'); + await import('./lib/dev-server.js'); }); gulp.task('build-assets', gulp.series('build-script', 'build-styles')); diff --git a/jest-puppeteer.config.js b/jest-puppeteer.config.js index 12e9590f3b..1e8d561a5b 100644 --- a/jest-puppeteer.config.js +++ b/jest-puppeteer.config.js @@ -1,21 +1,21 @@ module.exports = { - browserContext: 'incognito', - exitOnPageError: false, - launch: { - args: [ - // Workaround for the 'No usable sandbox! Update your kernel' error - // see more https://github.com/Googlechrome/puppeteer/issues/290 - '--no-sandbox', - '--disable-setuid-sandbox', - // Workaround for 'ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer' - '--disable-gpu', - '--disable-software-rasterizer', - ], - dumpio: true, - }, - server: { - command: `yarn test:start-server`, - launchTimeout: 30000, - port: process.env.TEST_PORT, - }, + browserContext: 'incognito', + exitOnPageError: false, + launch: { + args: [ + // Workaround for the 'No usable sandbox! Update your kernel' error + // See more https://github.com/Googlechrome/puppeteer/issues/290 + '--no-sandbox', + '--disable-setuid-sandbox', + // Workaround for 'ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer' + '--disable-gpu', + '--disable-software-rasterizer', + ], + dumpio: true, + }, + server: { + command: `yarn test:start-server`, + launchTimeout: 30000, + port: process.env.TEST_PORT, + }, }; diff --git a/jest.config.js b/jest.config.js index 46284e0dc4..9341bc9fc4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,9 +1,9 @@ /** @type {import('@jest/types').Config.InitialOptions} */ const config = { - verbose: true, - preset: process.env.TEST_WITH_PUPPETEER ? 'jest-puppeteer' : undefined, - snapshotResolver: `${process.cwd()}/src/tests/helpers/snapshotResolver.js`, - modulePathIgnorePatterns: ['templates/components', 'build'], + verbose: true, + preset: process.env.TEST_WITH_PUPPETEER ? 'jest-puppeteer' : undefined, + snapshotResolver: `${process.cwd()}/src/tests/helpers/snapshotResolver.js`, + modulePathIgnorePatterns: ['templates/components', 'build'], }; module.exports = config; diff --git a/lib/build-search-index.js b/lib/build-search-index.js index f8549f7e0b..16584449aa 100644 --- a/lib/build-search-index.js +++ b/lib/build-search-index.js @@ -5,66 +5,66 @@ import frontMatter from 'front-matter'; import nunjucks from 'nunjucks'; function buildEntries(templatePath, type) { - templatePath = path.resolve(templatePath); + templatePath = path.resolve(templatePath); - return { - title: path.basename(templatePath, '.njk'), - uri: templatePath.replace('.njk', '.html').substring(templatePath.indexOf(`/${type}`)), - }; + return { + title: path.basename(templatePath, '.njk'), + uri: templatePath.replace('.njk', '.html').substring(templatePath.indexOf(`/${type}`)), + }; } export default async function buildSearchIndex() { - let types = ['components', 'patterns', 'foundations']; - const pages = []; - types.forEach((type) => { - const hasExample = globSync(`./src/${type}/**/*example-*`).sort(); - const templatePath = Array.from(new Set(hasExample.map((filePath) => path.dirname(filePath)))); + let types = ['components', 'patterns', 'foundations']; + const pages = []; + types.forEach((type) => { + const hasExample = globSync(`./src/${type}/**/*example-*`).sort(); + const templatePath = Array.from(new Set(hasExample.map((filePath) => path.dirname(filePath)))); - if (templatePath.length) { - pages.push(...templatePath.map((folderPath) => buildEntries(folderPath, type))); - } - }); - const result = pages.map((page) => { - const path = []; + if (templatePath.length) { + pages.push(...templatePath.map((folderPath) => buildEntries(folderPath, type))); + } + }); + const result = pages.map((page) => { + const path = []; - return { - en: page.title, - url: page.uri, - category: path.join(' › '), - tags: page.searchTerms?.map((term) => term.title), - }; - }); - const filePath = process.cwd(); - const searchIndexJson = JSON.stringify(result); - fs.writeFile(`${filePath}/src/static/examples/data/search-index.json`, searchIndexJson, (err) => { - if (err) console.log(err); - }); + return { + en: page.title, + url: page.uri, + category: path.join(' › '), + tags: page.searchTerms?.map((term) => term.title), + }; + }); + const filePath = process.cwd(); + const searchIndexJson = JSON.stringify(result); + fs.writeFile(`${filePath}/src/static/examples/data/search-index.json`, searchIndexJson, (err) => { + if (err) console.log(err); + }); } export function renderSearch(templatePath, nunjucksEnvironment) { - try { - const data = frontMatter(fs.readFileSync(templatePath, { encoding: 'utf8' })); - const layout = data.attributes.layout; - nunjucksEnvironment.addGlobal('isDesignSystemExample', true); + try { + const data = frontMatter(fs.readFileSync(templatePath, { encoding: 'utf8' })); + const layout = data.attributes.layout; + nunjucksEnvironment.addGlobal('isDesignSystemExample', true); - let template; - if (layout === null) { - template = data.body; - } else { - template = ` + let template; + if (layout === null) { + template = data.body; + } else { + template = ` {% extends 'layout/_template.njk' %} {% block body %} ${data.body} {% endblock %} `; - } + } - const compiledTemplate = nunjucks.compile(template, nunjucksEnvironment, templatePath); + const compiledTemplate = nunjucks.compile(template, nunjucksEnvironment, templatePath); - return compiledTemplate.render(templatePath); - } catch (err) { - console.error(`An error occurred whilst rendering: ${templatePath}`); - throw err; - } + return compiledTemplate.render(templatePath); + } catch (err) { + console.error(`An error occurred whilst rendering: ${templatePath}`); + throw err; + } } diff --git a/lib/config.indexPage.js b/lib/config.indexPage.js index 83ea7d6609..595ebccb5a 100644 --- a/lib/config.indexPage.js +++ b/lib/config.indexPage.js @@ -2,9 +2,9 @@ import { renderSearch } from './build-search-index'; import createNunjucksEnvironment from './create-nunjucks-environment'; export const pages = [ - { title: 'Components', uri: '/components' }, - { title: 'Patterns', uri: '/patterns' }, - { title: 'Foundations', uri: '/foundations' }, + { title: 'Components', uri: '/components' }, + { title: 'Patterns', uri: '/patterns' }, + { title: 'Foundations', uri: '/foundations' }, ]; export const pagesWithIndex = pages.map((page) => ({ ...page, uri: `${page.uri}/index.html` })); diff --git a/lib/create-nunjucks-environment.js b/lib/create-nunjucks-environment.js index eaed213faf..4d8cbef46f 100644 --- a/lib/create-nunjucks-environment.js +++ b/lib/create-nunjucks-environment.js @@ -4,13 +4,13 @@ import setAttribute from './filters/set-attribute.js'; import setAttributes from './filters/set-attributes.js'; export default function createNunjucksEnvironment(loader) { - const templatePaths = ['src']; - const nunjucksLoader = loader ?? new nunjucks.FileSystemLoader(templatePaths, { noCache: true }); + const templatePaths = ['src']; + const nunjucksLoader = loader ?? new nunjucks.FileSystemLoader(templatePaths, { noCache: true }); - const nunjucksEnvironment = new nunjucks.Environment(nunjucksLoader); + const nunjucksEnvironment = new nunjucks.Environment(nunjucksLoader); - nunjucksEnvironment.addFilter('setAttribute', setAttribute); - nunjucksEnvironment.addFilter('setAttributes', setAttributes); + nunjucksEnvironment.addFilter('setAttribute', setAttribute); + nunjucksEnvironment.addFilter('setAttributes', setAttributes); - return nunjucksEnvironment; + return nunjucksEnvironment; } diff --git a/lib/create-page-list.js b/lib/create-page-list.js index e28641277c..6190efa5d9 100644 --- a/lib/create-page-list.js +++ b/lib/create-page-list.js @@ -3,36 +3,36 @@ import path from 'path'; import { globSync } from 'glob'; function buildEntries(templatePath, type) { - templatePath = path.resolve(templatePath); + templatePath = path.resolve(templatePath); - return { - title: path.basename(templatePath, '.njk'), - uri: templatePath.replace('.njk', '.html').substring(templatePath.indexOf(`/${type}`)), - }; + return { + title: path.basename(templatePath, '.njk'), + uri: templatePath.replace('.njk', '.html').substring(templatePath.indexOf(`/${type}`)), + }; } export default async function createPageList(res, type = null, name = null) { - try { - let pages = []; - if (type && name) { - if (!fs.existsSync(`./src/${type}/${name}`)) { - return res.status(404).send({ error: `${type} not found` }); - } - pages = globSync(`./src/${type}/${name}/example-*.njk`) - .sort() - .map((templatePath) => buildEntries(templatePath, type)); - } else if (type) { - const hasExample = globSync(`./src/${type}/**/*example-*`).sort(); - const templatePath = Array.from(new Set(hasExample.map((filePath) => path.dirname(filePath)))); + try { + let pages = []; + if (type && name) { + if (!fs.existsSync(`./src/${type}/${name}`)) { + return res.status(404).send({ error: `${type} not found` }); + } + pages = globSync(`./src/${type}/${name}/example-*.njk`) + .sort() + .map((templatePath) => buildEntries(templatePath, type)); + } else if (type) { + const hasExample = globSync(`./src/${type}/**/*example-*`).sort(); + const templatePath = Array.from(new Set(hasExample.map((filePath) => path.dirname(filePath)))); - if (templatePath.length) { - pages = templatePath.map((folderPath) => buildEntries(folderPath, type)); - } - } + if (templatePath.length) { + pages = templatePath.map((folderPath) => buildEntries(folderPath, type)); + } + } - return pages; - } catch (err) { - console.error(err); - return res.status(500).send({ error: 'An error occurred while building the page list' }); - } + return pages; + } catch (err) { + console.error(err); + return res.status(500).send({ error: 'An error occurred while building the page list' }); + } } diff --git a/lib/dev-server.js b/lib/dev-server.js index 57e2f3693e..7aea97266d 100644 --- a/lib/dev-server.js +++ b/lib/dev-server.js @@ -9,58 +9,58 @@ process.env.IS_DEV_SERVER = true; const port = process.env.TEST_PORT ?? 3010; (async () => { - const app = express(); - app.use(cors()); + const app = express(); + app.use(cors()); - app.all('/test/fake', (req, res) => { - res.status(200).json({}); - }); + app.all('/test/fake', (req, res) => { + res.status(200).json({}); + }); - app.all('/test/fake/*', (req, res) => { - res.status(200).json({}); - }); + app.all('/test/fake/*', (req, res) => { + res.status(200).json({}); + }); - app.use((req, res, next) => { - if (!req.path.startsWith('/test')) { - next(); - return; - } + app.use((req, res, next) => { + if (!req.path.startsWith('/test')) { + next(); + return; + } - res.send(''); - }); + res.send(''); + }); - app.get('/', async (req, res) => { - const output = renderPageList(pages, title, pageSearch); - await buildSearchIndex(); - res.send(output); - }); + app.get('/', async (req, res) => { + const output = renderPageList(pages, title, pageSearch); + await buildSearchIndex(); + res.send(output); + }); - app.get('/:type(components|patterns|foundations)', async (req, res) => { - const type = req.params.type; - const output = await handleTypeRoute(type, null, res); - res.send(output); - }); + app.get('/:type(components|patterns|foundations)', async (req, res) => { + const type = req.params.type; + const output = await handleTypeRoute(type, null, res); + res.send(output); + }); - app.get('/:type(components|patterns|foundations)/:name', async (req, res) => { - const type = req.params.type; - const name = req.params.name; - const output = await handleTypeRoute(type, name, res); - res.send(output); - }); + app.get('/:type(components|patterns|foundations)/:name', async (req, res) => { + const type = req.params.type; + const name = req.params.name; + const output = await handleTypeRoute(type, name, res); + res.send(output); + }); - app.get('/:type(components|patterns|foundations)/:name/:exampleName', async (req, res) => { - const type = req.params.type; - const name = req.params.name; - const exampleName = req.params.exampleName.replace('.html', ''); - const output = await handleExamplesRoute(type, name, exampleName); - res.send(output); - }); + app.get('/:type(components|patterns|foundations)/:name/:exampleName', async (req, res) => { + const type = req.params.type; + const name = req.params.name; + const exampleName = req.params.exampleName.replace('.html', ''); + const output = await handleExamplesRoute(type, name, exampleName); + res.send(output); + }); - app.use(express.static('./src/static')); - app.use(express.static('./src/static/favicons')); - app.use(express.static('./build')); + app.use(express.static('./src/static')); + app.use(express.static('./src/static/favicons')); + app.use(express.static('./build')); - app.listen(port, () => { - console.log(`Development server listening at http://0.0.0.0:${port}`); - }); + app.listen(port, () => { + console.log(`Development server listening at http://0.0.0.0:${port}`); + }); })(); diff --git a/lib/dev-server.spec.js b/lib/dev-server.spec.js index 36b91a55e7..bae04781f5 100644 --- a/lib/dev-server.spec.js +++ b/lib/dev-server.spec.js @@ -1,13 +1,13 @@ describe('dev-server', () => { - describe('/test/fake/{FAKE_PATH}', () => { - it.each([['/test/fake'], ['/test/fake/'], ['/test/fake/abc'], ['/test/fake/abc/'], ['/test/fake/abc/def']])( - 'returns empty object for any fake request path', - async path => { - await page.goto(`http://localhost:${process.env.TEST_PORT}${path}`); + describe('/test/fake/{FAKE_PATH}', () => { + it.each([['/test/fake'], ['/test/fake/'], ['/test/fake/abc'], ['/test/fake/abc/'], ['/test/fake/abc/def']])( + 'returns empty object for any fake request path', + async (path) => { + await page.goto(`http://localhost:${process.env.TEST_PORT}${path}`); - const body = await page.$eval('body', node => node.textContent); - expect(body).toBe('{}'); - }, - ); - }); + const body = await page.$eval('body', (node) => node.textContent); + expect(body).toBe('{}'); + }, + ); + }); }); diff --git a/lib/filters/set-attribute.js b/lib/filters/set-attribute.js index 1f84b2aa3b..64ba6a4171 100644 --- a/lib/filters/set-attribute.js +++ b/lib/filters/set-attribute.js @@ -1,4 +1,4 @@ export default function setAttribute(dictionary, key, value) { - dictionary[key] = value; - return dictionary; + dictionary[key] = value; + return dictionary; } diff --git a/lib/filters/set-attributes.js b/lib/filters/set-attributes.js index 15bf1b6491..c4d8c41c66 100644 --- a/lib/filters/set-attributes.js +++ b/lib/filters/set-attributes.js @@ -1,9 +1,9 @@ export default function setAttributes(dictionary, attributes) { - for (const key in attributes) { - if (attributes.hasOwnProperty(key)) { - dictionary[key] = attributes[key]; + for (const key in attributes) { + if (attributes.hasOwnProperty(key)) { + dictionary[key] = attributes[key]; + } } - } - return dictionary; + return dictionary; } diff --git a/lib/generate-static-pages.js b/lib/generate-static-pages.js index b9e27e5386..9aac8def39 100644 --- a/lib/generate-static-pages.js +++ b/lib/generate-static-pages.js @@ -7,63 +7,63 @@ import { handleTypeRoute, handleExamplesRoute } from './handle-routes'; import { pages, pagesWithIndex, title } from './config.indexPage'; export default async function generateStaticPages() { - createBuildDirectory(); + createBuildDirectory(); - // Render the index page - const indexPage = renderPageList(pagesWithIndex, title); - fs.writeFileSync('./build/index.html', indexPage); + // Render the index page + const indexPage = renderPageList(pagesWithIndex, title); + fs.writeFileSync('./build/index.html', indexPage); - // Loop through each page type and generate the pages - const types = pages.map((page) => page.uri.slice(1)); - for (const type of types) { - const folderNames = getFolderNames(type); - const typePage = await generateTypePages(type, folderNames); - fs.writeFileSync(`./build/${type}/index.html`, typePage); + // Loop through each page type and generate the pages + const types = pages.map((page) => page.uri.slice(1)); + for (const type of types) { + const folderNames = getFolderNames(type); + const typePage = await generateTypePages(type, folderNames); + fs.writeFileSync(`./build/${type}/index.html`, typePage); - await generateExamplePages(type, folderNames); - } + await generateExamplePages(type, folderNames); + } } function createBuildDirectory() { - if (!fs.existsSync('./build')) { - fs.mkdirSync('./build'); - } + if (!fs.existsSync('./build')) { + fs.mkdirSync('./build'); + } } function getFolderNames(type) { - const path = `./src/${type}`; - return fs.readdirSync(path, { withFileTypes: true }).map((dir) => dir.name); + const path = `./src/${type}`; + return fs.readdirSync(path, { withFileTypes: true }).map((dir) => dir.name); } async function generateExamplePages(type, folderNames) { - const dir = `./build/${type}`; - for (const name of folderNames) { - const exampleFiles = globSync(`./src/${type}/${name}/example-*.njk`).map((filePath) => path.basename(filePath, '.njk')); - for (const file of exampleFiles) { - const exampleName = path.basename(file, '.njk'); - const output = await handleExamplesRoute(type, name, exampleName); - const examplesDir = `${dir}/${name}`; - fs.writeFileSync(`${examplesDir}/${exampleName}.html`, output); + const dir = `./build/${type}`; + for (const name of folderNames) { + const exampleFiles = globSync(`./src/${type}/${name}/example-*.njk`).map((filePath) => path.basename(filePath, '.njk')); + for (const file of exampleFiles) { + const exampleName = path.basename(file, '.njk'); + const output = await handleExamplesRoute(type, name, exampleName); + const examplesDir = `${dir}/${name}`; + fs.writeFileSync(`${examplesDir}/${exampleName}.html`, output); + } } - } } async function generateTypePages(type, folderNames) { - const dir = `./build/${type}`; - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } + const dir = `./build/${type}`; + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } - const files = []; - for (const name of folderNames) { - const output = await handleTypeRoute(type, name); - const examplesDir = `${dir}/${name}`; - if (!fs.existsSync(examplesDir)) { - fs.mkdirSync(examplesDir, { recursive: true }); + const files = []; + for (const name of folderNames) { + const output = await handleTypeRoute(type, name); + const examplesDir = `${dir}/${name}`; + if (!fs.existsSync(examplesDir)) { + fs.mkdirSync(examplesDir, { recursive: true }); + } + fs.writeFileSync(`${examplesDir}/index.html`, output); + files.push({ title: name, uri: `/${type}/${name}/index.html` }); } - fs.writeFileSync(`${examplesDir}/index.html`, output); - files.push({ title: name, uri: `/${type}/${name}/index.html` }); - } - return renderPageList(files, type); + return renderPageList(files, type); } diff --git a/lib/handle-routes.js b/lib/handle-routes.js index 8332a882a7..b4b763d788 100644 --- a/lib/handle-routes.js +++ b/lib/handle-routes.js @@ -7,15 +7,15 @@ const PROJECT_PATH = process.cwd(); const nunjucksEnvironment = createNunjucksEnvironment(); export async function handleTypeRoute(type, name = null, res) { - const pages = name ? await createPageList(res, type, name) : await createPageList(res, type); - const title = name ? `"${name}" examples` : `${type.charAt(0).toUpperCase()}${type.slice(1)}`; - const output = renderPageList(pages, title); + const pages = name ? await createPageList(res, type, name) : await createPageList(res, type); + const title = name ? `"${name}" examples` : `${type.charAt(0).toUpperCase()}${type.slice(1)}`; + const output = renderPageList(pages, title); - return output; + return output; } export async function handleExamplesRoute(type, name, exampleName) { - const output = renderExample(`${PROJECT_PATH}/src/${type}/${name}/${exampleName}.njk`, nunjucksEnvironment); + const output = renderExample(`${PROJECT_PATH}/src/${type}/${name}/${exampleName}.njk`, nunjucksEnvironment); - return output; + return output; } diff --git a/lib/render-example.js b/lib/render-example.js index c4a86fe403..e09b655950 100644 --- a/lib/render-example.js +++ b/lib/render-example.js @@ -3,31 +3,31 @@ import fs from 'fs'; import nunjucks from 'nunjucks'; export default function renderExample(templatePath, nunjucksEnvironment) { - try { - const data = frontMatter(fs.readFileSync(templatePath, { encoding: 'utf8' })); - const layout = data.attributes.layout; - nunjucksEnvironment.addGlobal('isDesignSystemExample', true); + try { + const data = frontMatter(fs.readFileSync(templatePath, { encoding: 'utf8' })); + const layout = data.attributes.layout; + nunjucksEnvironment.addGlobal('isDesignSystemExample', true); - let template; - if (layout === null) { - template = data.body; - } else { - template = ` - {% extends 'layout/_template.njk' %} - - {% block body %} -
- ${data.body} -
- {% endblock %} - `; - } + let template; + if (layout === null) { + template = data.body; + } else { + template = ` + {% extends 'layout/_template.njk' %} + + {% block body %} +
+ ${data.body} +
+ {% endblock %} + `; + } - const compiledTemplate = nunjucks.compile(template, nunjucksEnvironment, templatePath); + const compiledTemplate = nunjucks.compile(template, nunjucksEnvironment, templatePath); - return compiledTemplate.render(templatePath); - } catch (err) { - console.error(`An error occurred whilst rendering: ${templatePath}`); - throw err; - } + return compiledTemplate.render(templatePath); + } catch (err) { + console.error(`An error occurred whilst rendering: ${templatePath}`); + throw err; + } } diff --git a/lib/render-page-list.js b/lib/render-page-list.js index fcaee6428c..4e48b8bdea 100644 --- a/lib/render-page-list.js +++ b/lib/render-page-list.js @@ -1,27 +1,27 @@ export default function renderPageList(pages, title, pageSearch = '') { - return ` - - - - - ${title} - - - -

${title}

- ${pageSearch} - - - -`; + return ` + + + + + ${title} + + + +

${title}

+ ${pageSearch} + + + + `; } diff --git a/lighthouse/lighthouse-get-urls.js b/lighthouse/lighthouse-get-urls.js index 477cfc40af..bd6dfc0710 100644 --- a/lighthouse/lighthouse-get-urls.js +++ b/lighthouse/lighthouse-get-urls.js @@ -4,39 +4,39 @@ const { glob } = require('glob'); const readdir = util.promisify(fs.readdir); async function createURLsFile() { - try { - const urls = await getURLs(); - fs.writeFileSync('./lighthouse/urls.json', urls); - } catch (e) { - console.error(e); - return; - } + try { + const urls = await getURLs(); + fs.writeFileSync('./lighthouse/urls.json', urls); + } catch (e) { + console.error(e); + return; + } } async function getURLs() { - let data = {}; - data.urls = []; - const directories = [ - { - path: './build/components', - }, - { - path: './build/patterns', - }, - { - path: './build/foundations', - }, - ]; - for (const directory of directories) { - const folders = await readdir(directory.path); - for (const folder of folders) { - const files = await glob(`${directory.path}/${folder}/**/*.html`, { ignore: `${directory.path}/${folder}/index.html` }); - for (const file of files) { - data.urls.push(file.replace('./build/', 'http://localhost:9000/')); - } + let data = {}; + data.urls = []; + const directories = [ + { + path: './build/components', + }, + { + path: './build/patterns', + }, + { + path: './build/foundations', + }, + ]; + for (const directory of directories) { + const folders = await readdir(directory.path); + for (const folder of folders) { + const files = await glob(`${directory.path}/${folder}/**/*.html`, { ignore: `${directory.path}/${folder}/index.html` }); + for (const file of files) { + data.urls.push(file.replace('./build/', 'http://localhost:9000/')); + } + } } - } - return JSON.stringify(data); + return JSON.stringify(data); } createURLsFile(); diff --git a/lighthouse/lighthouserc.js b/lighthouse/lighthouserc.js index a9570e757f..17da4d3b4c 100644 --- a/lighthouse/lighthouserc.js +++ b/lighthouse/lighthouserc.js @@ -1,18 +1,18 @@ module.exports = { - ci: { - collect: { - numberOfRuns: 1, - settings: { - onlyCategories: ['accessibility'], - }, + ci: { + collect: { + numberOfRuns: 1, + settings: { + onlyCategories: ['accessibility'], + }, + }, + assert: { + assertions: { + 'categories:accessibility': ['warn', { minScore: 1 }], + }, + }, + upload: { + target: 'temporary-public-storage', + }, }, - assert: { - assertions: { - 'categories:accessibility': ['warn', { minScore: 1 }], - }, - }, - upload: { - target: 'temporary-public-storage', - }, - }, }; diff --git a/package.json b/package.json index 360bff5d25..b9bc63c2ff 100644 --- a/package.json +++ b/package.json @@ -1,131 +1,134 @@ { - "name": "@ons/design-system", - "description": "ONS Design System built CSS, JS, and Nunjucks templates", - "version": "3.0.1", - "main": "index.js", - "license": "MIT", - "author": { - "name": "ONS Digital" - }, - "scripts": { - "start": "gulp start", - "watch": "gulp watch", - "test": "gulp build-assets && TEST_PORT=3020 TEST_WITH_PUPPETEER=1 jest '.*\\.spec\\.js'", - "test:no-build": "TEST_PORT=3020 TEST_WITH_PUPPETEER=1 jest '.*\\.spec\\.js'", - "test:with-log": "yarn test --no-color 2>test.log", - "test:start-server": "TEST_PORT=3020 gulp start-dev-server", - "build": "yarn && yarn tidy-clean && NODE_ENV=production gulp build", - "build-serve": "yarn build && gulp start-dev-server", - "npm-bundle": "NODE_ENV=production yarn tidy-clean && NODE_ENV=production gulp build-package && babel-node ci/generate-npm-package.js", - "cdn-bundle": "NODE_ENV=production yarn tidy-clean && NODE_ENV=production gulp build-package && babel-node ci/prepare-templates-for-zip.js", - "test-visual": "yarn build && gulp run-backstop-tests", - "test-visual:reference": "yarn build && gulp run-backstop-reference", - "test-visual:approve": "gulp run-backstop-approve", - "tidy-clean": "rm -rf build css favicons fonts img components layout scripts coverage scss js", - "check-unused": "npx npm-check-unused", - "dedupe-deps": "npx yarn-deduplicate yarn.lock", - "lint-staged": "lint-staged", - "stylelint": "stylelint '**/*.scss'", - "stylelint-fix": "stylelint '**/*.scss' --fix", - "prepack": "pinst --disable", - "postpack": "pinst --enable" - }, - "lint-staged": { - "*.js": [ - "prettier --print-width 140 --single-quote --parser babel --write", - "eslint --fix" + "name": "@ons/design-system", + "description": "ONS Design System built CSS, JS, and Nunjucks templates", + "version": "3.0.1", + "main": "index.js", + "license": "MIT", + "author": { + "name": "ONS Digital" + }, + "scripts": { + "start": "gulp start", + "watch": "gulp watch", + "test": "gulp build-assets && TEST_PORT=3020 TEST_WITH_PUPPETEER=1 jest '.*\\.spec\\.js'", + "test:no-build": "TEST_PORT=3020 TEST_WITH_PUPPETEER=1 jest '.*\\.spec\\.js'", + "test:with-log": "yarn test --no-color 2>test.log", + "test:start-server": "TEST_PORT=3020 gulp start-dev-server", + "build": "yarn && yarn tidy-clean && NODE_ENV=production gulp build", + "build-serve": "yarn build && gulp start-dev-server", + "npm-bundle": "NODE_ENV=production yarn tidy-clean && NODE_ENV=production gulp build-package && babel-node ci/generate-npm-package.js", + "cdn-bundle": "NODE_ENV=production yarn tidy-clean && NODE_ENV=production gulp build-package && babel-node ci/prepare-templates-for-zip.js", + "test-visual": "yarn build && gulp run-backstop-tests", + "test-visual:reference": "yarn build && gulp run-backstop-reference", + "test-visual:approve": "gulp run-backstop-approve", + "tidy-clean": "rm -rf build css favicons fonts img components layout scripts coverage scss js", + "check-unused": "npx npm-check-unused", + "dedupe-deps": "npx yarn-deduplicate yarn.lock", + "lint-staged": "lint-staged", + "stylelint": "stylelint '**/*.scss'", + "stylelint-fix": "stylelint '**/*.scss' --fix", + "prepack": "pinst --disable", + "postpack": "pinst --enable" + }, + "lint-staged": { + "*.js": [ + "prettier --write", + "eslint --fix" + ], + "*.md": [ + "prettier --write", + "remark" + ], + "*.scss": [ + "prettier --write", + "yarn run stylelint-fix" + ], + "*.{yml,yaml}": [ + "prettier --write" + ] + }, + "browserslist": [ + "last 2 versions", + "not dead" ], - "*.md": [ - "prettier --write", - "remark" - ], - "*.scss": [ - "prettier --print-width 140 --single-quote --parser scss --write", - "yarn run stylelint-fix" - ] - }, - "browserslist": [ - "last 2 versions", - "not dead" - ], - "devDependencies": { - "@babel/core": "^7.14.6", - "@babel/eslint-parser": "^7.22.9", - "@babel/node": "^7.14.7", - "@babel/plugin-proposal-class-properties": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-modules-commonjs": "^7.14.5", - "@babel/plugin-transform-runtime": "^7.14.5", - "@babel/preset-env": "^7.14.7", - "@babel/register": "^7.14.5", - "@babel/runtime": "^7.14.6", - "abortcontroller-polyfill": "^1.2.3", - "autoprefixer": "10.4.14", - "babel-plugin-istanbul": "^6.0.0", - "babelify": "^10.0.0", - "backstopjs": "^6.1.4", - "browser-sync": "^2.27.3", - "browserify": "^17.0.0", - "chalk": "^4.1.2", - "cheerio": "^1.0.0-rc.10", - "codecov": "^3.8.3", - "core-js": "^3.21.1", - "cors": "^2.8.5", - "dialog-polyfill": "^0.5.6", - "eslint": "^8.45.0", - "eslint-cli": "^1.1.1", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", - "eslint-plugin-unused-imports": "^3.1.0", - "express": "^4.17.1", - "front-matter": "^4.0.2", - "fs-extra": "^11.1.1", - "fuse.js": "^3.6.1", - "glob": "^10.2.3", - "gulp": "^4.0.2", - "gulp-babel": "^8.0.0", - "gulp-cli": "^2.3.0", - "gulp-dart-sass": "^1.0.2", - "gulp-if": "^3.0.0", - "gulp-postcss": "^9.0.0", - "gulp-sourcemaps": "^3.0.0", - "gulp-terser": "^2.0.1", - "http-server": "^14.1.1", - "husky": "^8.0.3", - "jest": "^29.6.1", - "jest-axe": "^8.0.0", - "jest-environment-jsdom": "^29.6.1", - "jest-puppeteer": "^9.0.0", - "lighthouse": "^11.0.0", - "lint-staged": "^15.2.0", - "lodash": "^4.17.21", - "mdn-polyfills": "^5.14.0", - "normalize.css": "^8.0.1", - "nunjucks": "^3.2.3", - "pinst": "^3.0.0", - "postcss": "^8.3.5", - "postcss-url": "^10.1.3", - "prepend-file": "^2.0.1", - "prettier": "^3.0.0", - "puppeteer": "^21.0.2", - "remark-cli": "^12.0.0", - "remark-lint": "^9.1.2", - "remark-preset-lint-recommended": "^6.1.3", - "stylelint": "^15.10.1", - "stylelint-config-recommended": "^13.0.0", - "stylelint-config-sass-guidelines": "^10.0.0", - "stylelint-config-standard": "^34.0.0", - "stylelint-order": "^6.0.3", - "stylelint-scss": "^5.0.1", - "through2": "^4.0.2", - "tick-manager": "^1.0.3", - "util": "^0.12.3", - "viewport-details": "^3.0.4", - "vinyl-buffer": "^1.0.1", - "vinyl-source-stream": "^2.0.0", - "whatwg-fetch": "^3.0.0" - }, - "publishConfig": { - "access": "public" - } + "devDependencies": { + "@babel/core": "^7.14.6", + "@babel/eslint-parser": "^7.22.9", + "@babel/node": "^7.14.7", + "@babel/plugin-proposal-class-properties": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-modules-commonjs": "^7.14.5", + "@babel/plugin-transform-runtime": "^7.14.5", + "@babel/preset-env": "^7.14.7", + "@babel/register": "^7.14.5", + "@babel/runtime": "^7.14.6", + "abortcontroller-polyfill": "^1.2.3", + "autoprefixer": "10.4.14", + "babel-plugin-istanbul": "^6.0.0", + "babelify": "^10.0.0", + "backstopjs": "^6.1.4", + "browser-sync": "^2.27.3", + "browserify": "^17.0.0", + "chalk": "^4.1.2", + "cheerio": "^1.0.0-rc.10", + "codecov": "^3.8.3", + "core-js": "^3.21.1", + "cors": "^2.8.5", + "dialog-polyfill": "^0.5.6", + "eslint": "^8.45.0", + "eslint-cli": "^1.1.1", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-unused-imports": "^3.1.0", + "express": "^4.17.1", + "front-matter": "^4.0.2", + "fs-extra": "^11.1.1", + "fuse.js": "^3.6.1", + "glob": "^10.2.3", + "gulp": "^4.0.2", + "gulp-babel": "^8.0.0", + "gulp-cli": "^2.3.0", + "gulp-dart-sass": "^1.0.2", + "gulp-if": "^3.0.0", + "gulp-postcss": "^9.0.0", + "gulp-sourcemaps": "^3.0.0", + "gulp-terser": "^2.0.1", + "http-server": "^14.1.1", + "husky": "^8.0.3", + "jest": "^29.6.1", + "jest-axe": "^8.0.0", + "jest-environment-jsdom": "^29.6.1", + "jest-puppeteer": "^9.0.0", + "lighthouse": "^11.0.0", + "lint-staged": "^15.2.0", + "lodash": "^4.17.21", + "mdn-polyfills": "^5.14.0", + "normalize.css": "^8.0.1", + "nunjucks": "^3.2.3", + "pinst": "^3.0.0", + "postcss": "^8.3.5", + "postcss-url": "^10.1.3", + "prepend-file": "^2.0.1", + "prettier": "^3.0.0", + "puppeteer": "^21.0.2", + "remark-cli": "^12.0.0", + "remark-lint": "^9.1.2", + "remark-preset-lint-recommended": "^6.1.3", + "stylelint": "^16.6.0", + "stylelint-config-recommended-scss": "^14.0.0", + "stylelint-config-sass-guidelines": "^11.1.0", + "stylelint-config-standard": "^36.0.0", + "stylelint-order": "^6.0.4", + "stylelint-scss": "^6.3.0", + "through2": "^4.0.2", + "tick-manager": "^1.0.3", + "util": "^0.12.3", + "viewport-details": "^3.0.4", + "vinyl-buffer": "^1.0.1", + "vinyl-source-stream": "^2.0.0", + "whatwg-fetch": "^3.0.0" + }, + "publishConfig": { + "access": "public" + } } diff --git a/postcss.config.js b/postcss.config.js index 65c0241b3f..7e97f3482c 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -5,29 +5,29 @@ const BASE_HREF = ''; const DEPLOY_URL = ''; export default () => { - return [ - postcssUrl({ - url: ({ url }) => { - // Only convert root relative URLs, which CSS-Loader won't process into require(). - if (!url.startsWith('/') || url.startsWith('//')) { - return url.replace(/\/\/+/g, '/'); - } - if (DEPLOY_URL.match(/:\/\//)) { - // If deployUrl contains a scheme, ignore baseHref use deployUrl as is. - console.log('1', `${DEPLOY_URL.replace(/\/$/, '')}${url}`); - return `${DEPLOY_URL.replace(/\/$/, '')}${url}`; - } else if (BASE_HREF.match(/:\/\//)) { - console.log('2', BASE_HREF.replace(/\/$/, '') + `/${DEPLOY_URL}/${url}`.replace(/\/\/+/g, '/')); - // If baseHref contains a scheme, include it as is. - return BASE_HREF.replace(/\/$/, '') + `/${DEPLOY_URL}/${url}`.replace(/\/\/+/g, '/'); - } else { - // Join together base-href, deploy-url and the original URL. - // Also dedupe multiple slashes into single ones. - console.log('3', `/${BASE_HREF}/${DEPLOY_URL}/${url}`.replace(/\/\/+/g, '/')); - return `/${BASE_HREF}/${DEPLOY_URL}/${url}`.replace(/\/\/+/g, '/'); - } - }, - }), - autoprefixer(), - ]; + return [ + postcssUrl({ + url: ({ url }) => { + // Only convert root relative URLs, which CSS-Loader won't process into require(). + if (!url.startsWith('/') || url.startsWith('//')) { + return url.replace(/\/\/+/g, '/'); + } + if (DEPLOY_URL.match(/:\/\//)) { + // If deployUrl contains a scheme, ignore baseHref use deployUrl as is. + console.log('1', `${DEPLOY_URL.replace(/\/$/, '')}${url}`); + return `${DEPLOY_URL.replace(/\/$/, '')}${url}`; + } else if (BASE_HREF.match(/:\/\//)) { + console.log('2', BASE_HREF.replace(/\/$/, '') + `/${DEPLOY_URL}/${url}`.replace(/\/\/+/g, '/')); + // If baseHref contains a scheme, include it as is. + return BASE_HREF.replace(/\/$/, '') + `/${DEPLOY_URL}/${url}`.replace(/\/\/+/g, '/'); + } else { + // Join together base-href, deploy-url and the original URL. + // Also dedupe multiple slashes into single ones. + console.log('3', `/${BASE_HREF}/${DEPLOY_URL}/${url}`.replace(/\/\/+/g, '/')); + return `/${BASE_HREF}/${DEPLOY_URL}/${url}`.replace(/\/\/+/g, '/'); + } + }, + }), + autoprefixer(), + ]; }; diff --git a/src/components/access-code/_macro.spec.js b/src/components/access-code/_macro.spec.js index f3d982ed18..fca3234649 100644 --- a/src/components/access-code/_macro.spec.js +++ b/src/components/access-code/_macro.spec.js @@ -6,149 +6,149 @@ import axe from '../../tests/helpers/axe'; import { renderComponent } from '../../tests/helpers/rendering'; describe('macro: access-code', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load( - renderComponent('access-code', { - id: 'example-access-code', - label: { - text: 'Enter your 16-character access code', - description: 'Keep this code safe. You will need to enter it every time you access your study', - }, - }), - ); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the provided `id` attribute', () => { - const $ = cheerio.load( - renderComponent('access-code', { - id: 'example-id', - }), - ); - - expect($('#example-id').length).toBe(1); - }); - - it('has the provided `name` attribute', () => { - const $ = cheerio.load( - renderComponent('access-code', { - name: 'special-name', - }), - ); - - expect($('input').attr('name')).toBe('special-name'); - }); - - it('has a default `type` of "text"', () => { - const $ = cheerio.load(renderComponent('access-code')); - - expect($('input').attr('type')).toBe('text'); - }); - - it('has the provided `type` attribute', () => { - const $ = cheerio.load( - renderComponent('access-code', { - type: 'number', - }), - ); - - expect($('input').attr('inputmode')).toBe('numeric'); - }); - - it('has additionally provided style classes', () => { - const $ = cheerio.load( - renderComponent('access-code', { - classes: 'extra-class another-extra-class', - }), - ); - - expect($('.ons-panel--info').hasClass('extra-class')).toBe(true); - expect($('.ons-panel--info').hasClass('another-extra-class')).toBe(true); - }); - - it('has provided label text and description', () => { - const $ = cheerio.load( - renderComponent('access-code', { - label: { - text: 'Enter your 16-character access code', - description: 'Keep this code safe. You will need to enter it every time you access your study', - }, - }), - ); - - expect($('.ons-label--with-description').text()).toBe('Enter your 16-character access code'); - expect($('.ons-input--with-description').text()).toBe( - 'Keep this code safe. You will need to enter it every time you access your study', - ); - }); - - it('has provided maximum length attribute including spaces required for groupSize', () => { - const $ = cheerio.load( - renderComponent('access-code', { - maxlength: 6, - groupSize: 3, - }), - ); - - expect($('input').attr('maxlength')).toBe('7'); - }); - - it('has provided group size attribute', () => { - const $ = cheerio.load( - renderComponent('access-code', { - groupSize: 2, - }), - ); - - expect($('input').attr('data-group-size')).toBe('2'); - }); - - it('has autocomplete disabled on its text input', () => { - const $ = cheerio.load(renderComponent('access-code')); - - expect($('input').attr('autocomplete')).toBe('off'); - }); - - it('has automatic capitalization on its text input', () => { - const $ = cheerio.load(renderComponent('access-code')); - - expect($('input').attr('autocapitalize')).toBe('characters'); - }); - - it('has provided security message text', () => { - const $ = cheerio.load( - renderComponent('access-code', { - securityMessage: 'Example security message.', - }), - ); - - expect($('.ons-panel__body').text().trim()).toBe('Example security message.'); - }); - - it('has provided `postTextBoxLinkText` and `postTextBoxLinkUrl`', () => { - const $ = cheerio.load( - renderComponent('access-code', { - postTextboxLinkText: 'Example link text', - postTextboxLinkUrl: '#3', - }), - ); - - expect($('a[href="#3"]').text().trim()).toBe('Example link text'); - }); - - it('has provided `error` output', () => { - const $ = cheerio.load( - renderComponent('access-code', { - error: { - id: 'access-code-error', - text: 'Enter an access code', - }, - }), - ); - - expect($('#access-code-error').length).toBe(1); - expect($('.ons-panel__error > strong').text()).toBe('Enter an access code'); - }); + it('passes jest-axe checks', async () => { + const $ = cheerio.load( + renderComponent('access-code', { + id: 'example-access-code', + label: { + text: 'Enter your 16-character access code', + description: 'Keep this code safe. You will need to enter it every time you access your study', + }, + }), + ); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has the provided `id` attribute', () => { + const $ = cheerio.load( + renderComponent('access-code', { + id: 'example-id', + }), + ); + + expect($('#example-id').length).toBe(1); + }); + + it('has the provided `name` attribute', () => { + const $ = cheerio.load( + renderComponent('access-code', { + name: 'special-name', + }), + ); + + expect($('input').attr('name')).toBe('special-name'); + }); + + it('has a default `type` of "text"', () => { + const $ = cheerio.load(renderComponent('access-code')); + + expect($('input').attr('type')).toBe('text'); + }); + + it('has the provided `type` attribute', () => { + const $ = cheerio.load( + renderComponent('access-code', { + type: 'number', + }), + ); + + expect($('input').attr('inputmode')).toBe('numeric'); + }); + + it('has additionally provided style classes', () => { + const $ = cheerio.load( + renderComponent('access-code', { + classes: 'extra-class another-extra-class', + }), + ); + + expect($('.ons-panel--info').hasClass('extra-class')).toBe(true); + expect($('.ons-panel--info').hasClass('another-extra-class')).toBe(true); + }); + + it('has provided label text and description', () => { + const $ = cheerio.load( + renderComponent('access-code', { + label: { + text: 'Enter your 16-character access code', + description: 'Keep this code safe. You will need to enter it every time you access your study', + }, + }), + ); + + expect($('.ons-label--with-description').text()).toBe('Enter your 16-character access code'); + expect($('.ons-input--with-description').text()).toBe( + 'Keep this code safe. You will need to enter it every time you access your study', + ); + }); + + it('has provided maximum length attribute including spaces required for groupSize', () => { + const $ = cheerio.load( + renderComponent('access-code', { + maxlength: 6, + groupSize: 3, + }), + ); + + expect($('input').attr('maxlength')).toBe('7'); + }); + + it('has provided group size attribute', () => { + const $ = cheerio.load( + renderComponent('access-code', { + groupSize: 2, + }), + ); + + expect($('input').attr('data-group-size')).toBe('2'); + }); + + it('has autocomplete disabled on its text input', () => { + const $ = cheerio.load(renderComponent('access-code')); + + expect($('input').attr('autocomplete')).toBe('off'); + }); + + it('has automatic capitalization on its text input', () => { + const $ = cheerio.load(renderComponent('access-code')); + + expect($('input').attr('autocapitalize')).toBe('characters'); + }); + + it('has provided security message text', () => { + const $ = cheerio.load( + renderComponent('access-code', { + securityMessage: 'Example security message.', + }), + ); + + expect($('.ons-panel__body').text().trim()).toBe('Example security message.'); + }); + + it('has provided `postTextBoxLinkText` and `postTextBoxLinkUrl`', () => { + const $ = cheerio.load( + renderComponent('access-code', { + postTextboxLinkText: 'Example link text', + postTextboxLinkUrl: '#3', + }), + ); + + expect($('a[href="#3"]').text().trim()).toBe('Example link text'); + }); + + it('has provided `error` output', () => { + const $ = cheerio.load( + renderComponent('access-code', { + error: { + id: 'access-code-error', + text: 'Enter an access code', + }, + }), + ); + + expect($('#access-code-error').length).toBe(1); + expect($('.ons-panel__error > strong').text()).toBe('Enter an access code'); + }); }); diff --git a/src/components/access-code/access-code.dom.js b/src/components/access-code/access-code.dom.js index 45a4aacfe6..2adaac8e3a 100644 --- a/src/components/access-code/access-code.dom.js +++ b/src/components/access-code/access-code.dom.js @@ -1,11 +1,11 @@ import domready from '../../js/domready'; domready(async () => { - const accessCodeInputs = [...document.querySelectorAll('.ons-js-access-code')]; + const accessCodeInputs = [...document.querySelectorAll('.ons-js-access-code')]; - if (accessCodeInputs.length) { - const accessCode = (await import('./access-code')).default; + if (accessCodeInputs.length) { + const accessCode = (await import('./access-code')).default; - accessCodeInputs.forEach((element) => new accessCode(element)); - } + accessCodeInputs.forEach((element) => new accessCode(element)); + } }); diff --git a/src/components/access-code/access-code.js b/src/components/access-code/access-code.js index bd160d5064..8aa6a80cb8 100644 --- a/src/components/access-code/access-code.js +++ b/src/components/access-code/access-code.js @@ -1,24 +1,24 @@ export default class AccessCode { - constructor(context) { - this.input = context; - const groupSize = parseInt(context.getAttribute('data-group-size'), 10); - this.groupingRegex = new RegExp(`.{1,${groupSize}}`, 'g'); + constructor(context) { + this.input = context; + const groupSize = parseInt(context.getAttribute('data-group-size'), 10); + this.groupingRegex = new RegExp(`.{1,${groupSize}}`, 'g'); - this.bindEventListeners(); - } + this.bindEventListeners(); + } - bindEventListeners() { - this.input.addEventListener('input', this.handleInput.bind(this)); - } + bindEventListeners() { + this.input.addEventListener('input', this.handleInput.bind(this)); + } - handleInput() { - const cursorPosition = this.input.selectionStart; - const shouldRepositionCursor = cursorPosition !== this.input.value.length; + handleInput() { + const cursorPosition = this.input.selectionStart; + const shouldRepositionCursor = cursorPosition !== this.input.value.length; - this.input.value = (this.input.value.replace(/\s/g, '').match(this.groupingRegex) || []).join(' '); + this.input.value = (this.input.value.replace(/\s/g, '').match(this.groupingRegex) || []).join(' '); - if (shouldRepositionCursor) { - this.input.setSelectionRange(cursorPosition, cursorPosition); + if (shouldRepositionCursor) { + this.input.setSelectionRange(cursorPosition, cursorPosition); + } } - } } diff --git a/src/components/access-code/access-code.scss b/src/components/access-code/access-code.scss index 5023c5865a..d3695d795b 100644 --- a/src/components/access-code/access-code.scss +++ b/src/components/access-code/access-code.scss @@ -1,29 +1,29 @@ .ons-access-code { - &__input { - font-family: $font-mono !important; - font-weight: $font-weight-bold; - letter-spacing: 0.14em; - text-transform: uppercase; - width: 15.1em; + &__input { + font-family: $font-mono !important; + font-weight: $font-weight-bold; + letter-spacing: 0.14em; + text-transform: uppercase; + width: 15.1em; - @media all and (width >= 375px) { - font-size: 20px !important; - width: 15.1em !important; - } + @media all and (width >= 375px) { + font-size: 20px !important; + width: 15.1em !important; + } - @media all and (width <= 374px) { - letter-spacing: 0; - max-width: 12.6em !important; - } + @media all and (width <= 374px) { + letter-spacing: 0; + max-width: 12.6em !important; + } - @media all and (width <= 299px) { - max-width: 100%; - } + @media all and (width <= 299px) { + max-width: 100%; + } - @include mq('s') { - font-size: 24px !important; - max-width: 14.9em; - width: 14.9em !important; + @include mq('s') { + font-size: 24px !important; + max-width: 14.9em; + width: 14.9em !important; + } } - } } diff --git a/src/components/access-code/access-code.spec.js b/src/components/access-code/access-code.spec.js index 5a690cef28..6de1ebba86 100644 --- a/src/components/access-code/access-code.spec.js +++ b/src/components/access-code/access-code.spec.js @@ -1,26 +1,26 @@ import { renderComponent, setTestPage } from '../../tests/helpers/rendering'; describe('script: access-code', () => { - describe('Data grouping', () => { - it('correctly formats user input with spaces', async () => { - await setTestPage( - '/test', - renderComponent('access-code', { - id: 'test-access-code', - groupSize: 5, - }), - ); + describe('Data grouping', () => { + it('correctly formats user input with spaces', async () => { + await setTestPage( + '/test', + renderComponent('access-code', { + id: 'test-access-code', + groupSize: 5, + }), + ); - await page.focus('#test-access-code'); - await page.keyboard.type('1234'); + await page.focus('#test-access-code'); + await page.keyboard.type('1234'); - const valueSample1 = await page.$eval('#test-access-code', (element) => element.value); - expect(valueSample1).toBe('1234'); + const valueSample1 = await page.$eval('#test-access-code', (element) => element.value); + expect(valueSample1).toBe('1234'); - await page.keyboard.type('5678'); + await page.keyboard.type('5678'); - const valueSample2 = await page.$eval('#test-access-code', (element) => element.value); - expect(valueSample2).toBe('12345 678'); + const valueSample2 = await page.$eval('#test-access-code', (element) => element.value); + expect(valueSample2).toBe('12345 678'); + }); }); - }); }); diff --git a/src/components/accordion/_macro.spec.js b/src/components/accordion/_macro.spec.js index 017c45554f..b9629a4ca2 100644 --- a/src/components/accordion/_macro.spec.js +++ b/src/components/accordion/_macro.spec.js @@ -6,176 +6,176 @@ import axe from '../../tests/helpers/axe'; import { renderComponent } from '../../tests/helpers/rendering'; const EXAMPLE_ACCORDION_WITH_TWO_ITEMS = { - id: 'accordion-identifier', - itemsList: [ - { - title: 'Title for item 1', - content: 'Content for item 1', - }, - { - title: 'Title for item 2', - content: 'Content for item 2', - }, - ], + id: 'accordion-identifier', + itemsList: [ + { + title: 'Title for item 1', + content: 'Content for item 1', + }, + { + title: 'Title for item 2', + content: 'Content for item 2', + }, + ], }; const EXAMPLE_ACCORDION = { - ...EXAMPLE_ACCORDION_WITH_TWO_ITEMS, - allButton: { - open: 'Open label', - close: 'Close label', - }, + ...EXAMPLE_ACCORDION_WITH_TWO_ITEMS, + allButton: { + open: 'Open label', + close: 'Close label', + }, }; describe('macro: accordion', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('accordion', EXAMPLE_ACCORDION)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the provided `id` attribute', () => { - const $ = cheerio.load(renderComponent('accordion', EXAMPLE_ACCORDION_WITH_TWO_ITEMS)); - - expect($('.ons-accordion').attr('id')).toBe('accordion-identifier'); - }); - - it('has additionally provided style classes', () => { - const $ = cheerio.load( - renderComponent('accordion', { - ...EXAMPLE_ACCORDION_WITH_TWO_ITEMS, - classes: 'extra-class another-extra-class', - }), - ); + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('accordion', EXAMPLE_ACCORDION)); - expect($('.ons-accordion').hasClass('extra-class')).toBe(true); - expect($('.ons-accordion').hasClass('another-extra-class')).toBe(true); - }); - - describe('item', () => { - it('has provided title text', () => { - const $ = cheerio.load(renderComponent('accordion', EXAMPLE_ACCORDION_WITH_TWO_ITEMS)); - - const titleText = $('.ons-details__title').first().text().trim(); - expect(titleText).toBe('Title for item 1'); - }); - - it('has title with provided tag override', () => { - const $ = cheerio.load( - renderComponent('accordion', { - itemsList: [ - { - title: 'Title for item 1', - titleTag: 'h5', - content: 'Content for item 1', - }, - ], - }), - ); - - const titleTag = $('.ons-details__title')[0].tagName; - expect(titleTag).toBe('h5'); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); }); - it('has provided content text', () => { - const $ = cheerio.load(renderComponent('accordion', EXAMPLE_ACCORDION_WITH_TWO_ITEMS)); + it('has the provided `id` attribute', () => { + const $ = cheerio.load(renderComponent('accordion', EXAMPLE_ACCORDION_WITH_TWO_ITEMS)); - const titleText = $('.ons-details__content').first().text().trim(); - expect(titleText).toBe('Content for item 1'); + expect($('.ons-accordion').attr('id')).toBe('accordion-identifier'); }); - it('has additionally provided `attributes`', () => { - const $ = cheerio.load( - renderComponent('accordion', { - itemsList: [ - { - title: 'Title for item 1', - attributes: { - a: 123, - b: 456, - }, - }, - ], - }), - ); - - expect($('.ons-details').attr('a')).toBe('123'); - expect($('.ons-details').attr('b')).toBe('456'); - }); + it('has additionally provided style classes', () => { + const $ = cheerio.load( + renderComponent('accordion', { + ...EXAMPLE_ACCORDION_WITH_TWO_ITEMS, + classes: 'extra-class another-extra-class', + }), + ); - it('has additionally provided `headingAttributes`', () => { - const $ = cheerio.load( - renderComponent('accordion', { - itemsList: [ - { - title: 'Title for item 1', - headingAttributes: { - a: 123, - b: 456, - }, - }, - ], - }), - ); - - expect($('.ons-details__heading').attr('a')).toBe('123'); - expect($('.ons-details__heading').attr('b')).toBe('456'); + expect($('.ons-accordion').hasClass('extra-class')).toBe(true); + expect($('.ons-accordion').hasClass('another-extra-class')).toBe(true); }); - it('has additionally provided `contentAttributes`', () => { - const $ = cheerio.load( - renderComponent('accordion', { - itemsList: [ - { - title: 'Title for item 1', - content: 'Content for item 1', - contentAttributes: { - a: 123, - b: 456, - }, - }, - ], - }), - ); - - expect($('.ons-details__content').attr('a')).toBe('123'); - expect($('.ons-details__content').attr('b')).toBe('456'); - }); - }); - - describe('toggle all button', () => { - it('outputs a button with the expected class', () => { - const $ = cheerio.load( - renderComponent('accordion', { - ...EXAMPLE_ACCORDION_WITH_TWO_ITEMS, - allButton: { - open: 'Open label', - close: 'Close label', - }, - }), - ); - - expect($('button.ons-accordion__toggle-all').length).toBe(1); + describe('item', () => { + it('has provided title text', () => { + const $ = cheerio.load(renderComponent('accordion', EXAMPLE_ACCORDION_WITH_TWO_ITEMS)); + + const titleText = $('.ons-details__title').first().text().trim(); + expect(titleText).toBe('Title for item 1'); + }); + + it('has title with provided tag override', () => { + const $ = cheerio.load( + renderComponent('accordion', { + itemsList: [ + { + title: 'Title for item 1', + titleTag: 'h5', + content: 'Content for item 1', + }, + ], + }), + ); + + const titleTag = $('.ons-details__title')[0].tagName; + expect(titleTag).toBe('h5'); + }); + + it('has provided content text', () => { + const $ = cheerio.load(renderComponent('accordion', EXAMPLE_ACCORDION_WITH_TWO_ITEMS)); + + const titleText = $('.ons-details__content').first().text().trim(); + expect(titleText).toBe('Content for item 1'); + }); + + it('has additionally provided `attributes`', () => { + const $ = cheerio.load( + renderComponent('accordion', { + itemsList: [ + { + title: 'Title for item 1', + attributes: { + a: 123, + b: 456, + }, + }, + ], + }), + ); + + expect($('.ons-details').attr('a')).toBe('123'); + expect($('.ons-details').attr('b')).toBe('456'); + }); + + it('has additionally provided `headingAttributes`', () => { + const $ = cheerio.load( + renderComponent('accordion', { + itemsList: [ + { + title: 'Title for item 1', + headingAttributes: { + a: 123, + b: 456, + }, + }, + ], + }), + ); + + expect($('.ons-details__heading').attr('a')).toBe('123'); + expect($('.ons-details__heading').attr('b')).toBe('456'); + }); + + it('has additionally provided `contentAttributes`', () => { + const $ = cheerio.load( + renderComponent('accordion', { + itemsList: [ + { + title: 'Title for item 1', + content: 'Content for item 1', + contentAttributes: { + a: 123, + b: 456, + }, + }, + ], + }), + ); + + expect($('.ons-details__content').attr('a')).toBe('123'); + expect($('.ons-details__content').attr('b')).toBe('456'); + }); }); - it('has additionally provided `attributes`', () => { - const $ = cheerio.load( - renderComponent('accordion', { - ...EXAMPLE_ACCORDION_WITH_TWO_ITEMS, - allButton: { - open: 'Open label', - close: 'Close label', - attributes: { - a: 123, - b: 456, - }, - }, - }), - ); - - expect($('button.ons-accordion__toggle-all').attr('a')).toBe('123'); - expect($('button.ons-accordion__toggle-all').attr('b')).toBe('456'); + describe('toggle all button', () => { + it('outputs a button with the expected class', () => { + const $ = cheerio.load( + renderComponent('accordion', { + ...EXAMPLE_ACCORDION_WITH_TWO_ITEMS, + allButton: { + open: 'Open label', + close: 'Close label', + }, + }), + ); + + expect($('button.ons-accordion__toggle-all').length).toBe(1); + }); + + it('has additionally provided `attributes`', () => { + const $ = cheerio.load( + renderComponent('accordion', { + ...EXAMPLE_ACCORDION_WITH_TWO_ITEMS, + allButton: { + open: 'Open label', + close: 'Close label', + attributes: { + a: 123, + b: 456, + }, + }, + }), + ); + + expect($('button.ons-accordion__toggle-all').attr('a')).toBe('123'); + expect($('button.ons-accordion__toggle-all').attr('b')).toBe('456'); + }); }); - }); }); diff --git a/src/components/accordion/accordion.dom.js b/src/components/accordion/accordion.dom.js index e7c0f7724b..09584930ee 100644 --- a/src/components/accordion/accordion.dom.js +++ b/src/components/accordion/accordion.dom.js @@ -1,19 +1,19 @@ import domready from '../../js/domready'; async function initialiseAccordions() { - const toggleAllButtons = [...document.querySelectorAll('.ons-accordion__toggle-all')]; + const toggleAllButtons = [...document.querySelectorAll('.ons-accordion__toggle-all')]; - if (toggleAllButtons.length) { - const detailsComponents = [...document.querySelectorAll('.ons-js-details')]; + if (toggleAllButtons.length) { + const detailsComponents = [...document.querySelectorAll('.ons-js-details')]; - const Details = (await import('../details/details')).default; - const Accordion = (await import('./accordion')).default; - const detailsEls = detailsComponents.map((element) => new Details(element)); + const Details = (await import('../details/details')).default; + const Accordion = (await import('./accordion')).default; + const detailsEls = detailsComponents.map((element) => new Details(element)); - toggleAllButtons.forEach((button) => { - new Accordion(button, detailsEls); - }); - } + toggleAllButtons.forEach((button) => { + new Accordion(button, detailsEls); + }); + } } domready(initialiseAccordions); diff --git a/src/components/accordion/accordion.js b/src/components/accordion/accordion.js index eb112cc637..cf52dfba01 100644 --- a/src/components/accordion/accordion.js +++ b/src/components/accordion/accordion.js @@ -1,64 +1,64 @@ export default class Accordion { - constructor(button, detailsEls) { - this.openDetailsEls = 0; + constructor(button, detailsEls) { + this.openDetailsEls = 0; - this.button = button; - this.buttonInner = button.querySelector('.ons-accordion__toggle-all-inner'); - this.group = button.getAttribute('data-group'); - this.detailsEls = detailsEls.filter((details) => details.group === this.group); - this.totalDetailsEls = this.detailsEls.length; - this.buttonOpenEl = this.buttonInner.querySelector('.ons-btn__text'); - this.buttonOpen = this.buttonOpenEl.innerHTML.trim(); - this.closeButton = button.getAttribute('data-close-all'); - this.open = this.detailsEls.find((details) => details.open === true); + this.button = button; + this.buttonInner = button.querySelector('.ons-accordion__toggle-all-inner'); + this.group = button.getAttribute('data-group'); + this.detailsEls = detailsEls.filter((details) => details.group === this.group); + this.totalDetailsEls = this.detailsEls.length; + this.buttonOpenEl = this.buttonInner.querySelector('.ons-btn__text'); + this.buttonOpen = this.buttonOpenEl.innerHTML.trim(); + this.closeButton = button.getAttribute('data-close-all'); + this.open = this.detailsEls.find((details) => details.open === true); - this.detailsEls.forEach((details) => { - details.onOpen = this.onOpen.bind(this); - details.onClose = this.onClose.bind(this); - }); + this.detailsEls.forEach((details) => { + details.onOpen = this.onOpen.bind(this); + details.onClose = this.onClose.bind(this); + }); - if (this.open) { - this.openDetailsEls = this.totalDetailsEls; - } + if (this.open) { + this.openDetailsEls = this.totalDetailsEls; + } - this.button.addEventListener('click', this.handleButtonClick.bind(this)); - this.setButton(); - this.button.classList.remove('ons-u-d-no'); - } + this.button.addEventListener('click', this.handleButtonClick.bind(this)); + this.setButton(); + this.button.classList.remove('ons-u-d-no'); + } - handleButtonClick(event) { - event.preventDefault(); + handleButtonClick(event) { + event.preventDefault(); - const open = !this.canClose(); + const open = !this.canClose(); - this.detailsEls.forEach((details) => { - details.setOpen(open); - }); - } + this.detailsEls.forEach((details) => { + details.setOpen(open); + }); + } - onOpen() { - this.openDetailsEls++; - this.setButton(); - } + onOpen() { + this.openDetailsEls++; + this.setButton(); + } - onClose() { - this.openDetailsEls--; - this.setButton(); - } + onClose() { + this.openDetailsEls--; + this.setButton(); + } - canClose() { - return this.openDetailsEls === this.totalDetailsEls; - } + canClose() { + return this.openDetailsEls === this.totalDetailsEls; + } - setButton() { - if (this.canClose()) { - this.buttonInner.innerHTML = this.closeButton; - this.button.setAttribute('data-ga-label', this.buttonOpen); - this.button.setAttribute('aria-expanded', 'true'); - } else { - this.buttonInner.innerHTML = this.buttonOpen; - this.button.setAttribute('data-ga-label', this.closeButton); - this.button.setAttribute('aria-expanded', 'false'); + setButton() { + if (this.canClose()) { + this.buttonInner.innerHTML = this.closeButton; + this.button.setAttribute('data-ga-label', this.buttonOpen); + this.button.setAttribute('aria-expanded', 'true'); + } else { + this.buttonInner.innerHTML = this.buttonOpen; + this.button.setAttribute('data-ga-label', this.closeButton); + this.button.setAttribute('aria-expanded', 'false'); + } } - } } diff --git a/src/components/accordion/accordion.spec.js b/src/components/accordion/accordion.spec.js index c4059ba958..954921731b 100644 --- a/src/components/accordion/accordion.spec.js +++ b/src/components/accordion/accordion.spec.js @@ -1,142 +1,142 @@ import { renderComponent, setTestPage } from '../../tests/helpers/rendering'; const EXAMPLE_ACCORDION_WITH_THREE_ITEMS = { - id: 'example-accordion', - itemsList: [ - { - title: 'Title for item 1', - content: 'Content for item 1', - }, - { - title: 'Title for item 2', - content: 'Content for item 2', - }, - { - title: 'Title for item 3', - content: 'Content for item 3', - }, - ], + id: 'example-accordion', + itemsList: [ + { + title: 'Title for item 1', + content: 'Content for item 1', + }, + { + title: 'Title for item 2', + content: 'Content for item 2', + }, + { + title: 'Title for item 3', + content: 'Content for item 3', + }, + ], }; const EXAMPLE_ACCORDION_WITH_ALL_BUTTON = { - ...EXAMPLE_ACCORDION_WITH_THREE_ITEMS, - allButton: { - open: 'Open all', - close: 'Close all', - attributes: { - 'data-test-trigger': true, + ...EXAMPLE_ACCORDION_WITH_THREE_ITEMS, + allButton: { + open: 'Open all', + close: 'Close all', + attributes: { + 'data-test-trigger': true, + }, }, - }, }; describe('script: accordion', () => { - it('begins with all items open when specified', async () => { - await setTestPage( - '/test', - renderComponent('accordion', { - ...EXAMPLE_ACCORDION_WITH_THREE_ITEMS, - open: true, - }), - ); + it('begins with all items open when specified', async () => { + await setTestPage( + '/test', + renderComponent('accordion', { + ...EXAMPLE_ACCORDION_WITH_THREE_ITEMS, + open: true, + }), + ); - const detailsElementStates = await page.$$eval('.ons-js-details', (nodes) => - nodes.map((node) => node.classList.contains('ons-details--open')), - ); + const detailsElementStates = await page.$$eval('.ons-js-details', (nodes) => + nodes.map((node) => node.classList.contains('ons-details--open')), + ); - expect(detailsElementStates).toEqual([true, true, true]); - }); + expect(detailsElementStates).toEqual([true, true, true]); + }); - it('sets toggle all button label to "Hide all" when open is specified', async () => { - await setTestPage( - '/test', - renderComponent('accordion', { - ...EXAMPLE_ACCORDION_WITH_ALL_BUTTON, - open: true, - }), - ); + it('sets toggle all button label to "Hide all" when open is specified', async () => { + await setTestPage( + '/test', + renderComponent('accordion', { + ...EXAMPLE_ACCORDION_WITH_ALL_BUTTON, + open: true, + }), + ); - const buttonText = await page.$eval('button[data-test-trigger]', (element) => element.innerText); - expect(buttonText.trim()).toBe('Close all'); - }); + const buttonText = await page.$eval('button[data-test-trigger]', (element) => element.innerText); + expect(buttonText.trim()).toBe('Close all'); + }); - it('sets toggle all button aria-expanded set to true when open is specified', async () => { - await setTestPage( - '/test', - renderComponent('accordion', { - ...EXAMPLE_ACCORDION_WITH_ALL_BUTTON, - open: true, - }), - ); + it('sets toggle all button aria-expanded set to true when open is specified', async () => { + await setTestPage( + '/test', + renderComponent('accordion', { + ...EXAMPLE_ACCORDION_WITH_ALL_BUTTON, + open: true, + }), + ); - const ariaExpanded = await page.$eval('button[data-test-trigger]', (element) => element.getAttribute('aria-expanded')); - expect(ariaExpanded).toBe('true'); - }); + const ariaExpanded = await page.$eval('button[data-test-trigger]', (element) => element.getAttribute('aria-expanded')); + expect(ariaExpanded).toBe('true'); + }); - it('opens all items when accordion `allbutton` is clicked', async () => { - await setTestPage('/test', renderComponent('accordion', EXAMPLE_ACCORDION_WITH_ALL_BUTTON)); + it('opens all items when accordion `allbutton` is clicked', async () => { + await setTestPage('/test', renderComponent('accordion', EXAMPLE_ACCORDION_WITH_ALL_BUTTON)); - await page.click('button[data-test-trigger]'); + await page.click('button[data-test-trigger]'); - const detailsElementStates = await page.$$eval('.ons-js-details', (nodes) => - nodes.map((node) => node.classList.contains('ons-details--open')), - ); + const detailsElementStates = await page.$$eval('.ons-js-details', (nodes) => + nodes.map((node) => node.classList.contains('ons-details--open')), + ); - expect(detailsElementStates).toEqual([true, true, true]); - }); + expect(detailsElementStates).toEqual([true, true, true]); + }); - it('closes all items when accordion `allbutton` is clicked twice', async () => { - await setTestPage('/test', renderComponent('accordion', EXAMPLE_ACCORDION_WITH_ALL_BUTTON)); + it('closes all items when accordion `allbutton` is clicked twice', async () => { + await setTestPage('/test', renderComponent('accordion', EXAMPLE_ACCORDION_WITH_ALL_BUTTON)); - await page.click('button[data-test-trigger]'); - await page.click('button[data-test-trigger]'); + await page.click('button[data-test-trigger]'); + await page.click('button[data-test-trigger]'); - const detailsElementStates = await page.$$eval('.ons-js-details', (nodes) => - nodes.map((node) => node.classList.contains('ons-details--open')), - ); + const detailsElementStates = await page.$$eval('.ons-js-details', (nodes) => + nodes.map((node) => node.classList.contains('ons-details--open')), + ); - expect(detailsElementStates).toEqual([false, false, false]); - }); + expect(detailsElementStates).toEqual([false, false, false]); + }); - it('starts with the toggle all button labelled as "Open all"', async () => { - await setTestPage('/test', renderComponent('accordion', EXAMPLE_ACCORDION_WITH_ALL_BUTTON)); + it('starts with the toggle all button labelled as "Open all"', async () => { + await setTestPage('/test', renderComponent('accordion', EXAMPLE_ACCORDION_WITH_ALL_BUTTON)); - const buttonText = await page.$eval('button[data-test-trigger]', (element) => element.innerText); - expect(buttonText.trim()).toBe('Open all'); - }); + const buttonText = await page.$eval('button[data-test-trigger]', (element) => element.innerText); + expect(buttonText.trim()).toBe('Open all'); + }); - it('starts with the toggle all button aria-expanded set to false', async () => { - await setTestPage('/test', renderComponent('accordion', EXAMPLE_ACCORDION_WITH_ALL_BUTTON)); + it('starts with the toggle all button aria-expanded set to false', async () => { + await setTestPage('/test', renderComponent('accordion', EXAMPLE_ACCORDION_WITH_ALL_BUTTON)); - const ariaExpanded = await page.$eval('button[data-test-trigger]', (element) => element.getAttribute('aria-expanded')); - expect(ariaExpanded).toBe('false'); - }); + const ariaExpanded = await page.$eval('button[data-test-trigger]', (element) => element.getAttribute('aria-expanded')); + expect(ariaExpanded).toBe('false'); + }); - it('sets toggle all button label to "Hide all" when clicked', async () => { - await setTestPage('/test', renderComponent('accordion', EXAMPLE_ACCORDION_WITH_ALL_BUTTON)); + it('sets toggle all button label to "Hide all" when clicked', async () => { + await setTestPage('/test', renderComponent('accordion', EXAMPLE_ACCORDION_WITH_ALL_BUTTON)); - await page.click('button[data-test-trigger]'); + await page.click('button[data-test-trigger]'); - const buttonText = await page.$eval('button[data-test-trigger]', (element) => element.innerText); - expect(buttonText.trim()).toBe('Close all'); - }); + const buttonText = await page.$eval('button[data-test-trigger]', (element) => element.innerText); + expect(buttonText.trim()).toBe('Close all'); + }); - it('sets toggle all button aria-expanded set to true when clicked', async () => { - await setTestPage('/test', renderComponent('accordion', EXAMPLE_ACCORDION_WITH_ALL_BUTTON)); + it('sets toggle all button aria-expanded set to true when clicked', async () => { + await setTestPage('/test', renderComponent('accordion', EXAMPLE_ACCORDION_WITH_ALL_BUTTON)); - await page.click('button[data-test-trigger]'); + await page.click('button[data-test-trigger]'); - const ariaExpanded = await page.$eval('button[data-test-trigger]', (element) => element.getAttribute('aria-expanded')); - expect(ariaExpanded).toBe('true'); - }); + const ariaExpanded = await page.$eval('button[data-test-trigger]', (element) => element.getAttribute('aria-expanded')); + expect(ariaExpanded).toBe('true'); + }); - it('sets toggle all button label to "Hide all" when all items are shown', async () => { - await setTestPage('/test', renderComponent('accordion', EXAMPLE_ACCORDION_WITH_ALL_BUTTON)); + it('sets toggle all button label to "Hide all" when all items are shown', async () => { + await setTestPage('/test', renderComponent('accordion', EXAMPLE_ACCORDION_WITH_ALL_BUTTON)); - await page.click('#example-accordion-1 .ons-details__heading'); - await page.click('#example-accordion-2 .ons-details__heading'); - await page.click('#example-accordion-3 .ons-details__heading'); + await page.click('#example-accordion-1 .ons-details__heading'); + await page.click('#example-accordion-2 .ons-details__heading'); + await page.click('#example-accordion-3 .ons-details__heading'); - const buttonText = await page.$eval('button[data-test-trigger]', (element) => element.innerText); - expect(buttonText.trim()).toBe('Close all'); - }); + const buttonText = await page.$eval('button[data-test-trigger]', (element) => element.innerText); + expect(buttonText.trim()).toBe('Close all'); + }); }); diff --git a/src/components/address-input/_macro.spec.js b/src/components/address-input/_macro.spec.js index 53bce430c0..6498f09fb5 100644 --- a/src/components/address-input/_macro.spec.js +++ b/src/components/address-input/_macro.spec.js @@ -6,450 +6,450 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL = { - id: 'address-input-example-id', - legend: 'What is the address?', - label: { - text: 'Enter address or postcode and select from results', - id: 'address-input-example-label-id', - }, - isEditable: false, - instructions: 'Use up and down keys to navigate suggestions.', - ariaYouHaveSelected: 'You have selected', - ariaMinChars: 'Enter 3 or more characters for suggestions.', - ariaOneResult: 'There is one suggestion available.', - ariaNResults: 'There are {n} suggestions available.', - ariaLimitedResults: 'Results have been limited to 10 suggestions. Type more characters to improve your search', - ariaGroupedResults: 'There are {n} for {x}', - groupCount: '{n} addresses', - moreResults: 'Enter more of the address to improve results', - noResults: 'No results found. Try entering a different part of the address', - tooManyResults: '{n} results found. Enter more of the address to improve results', - typeMore: 'Enter more of the address to get results', - resultsTitle: 'Select an address', - resultsTitleId: 'address-suggestions', + id: 'address-input-example-id', + legend: 'What is the address?', + label: { + text: 'Enter address or postcode and select from results', + id: 'address-input-example-label-id', + }, + isEditable: false, + instructions: 'Use up and down keys to navigate suggestions.', + ariaYouHaveSelected: 'You have selected', + ariaMinChars: 'Enter 3 or more characters for suggestions.', + ariaOneResult: 'There is one suggestion available.', + ariaNResults: 'There are {n} suggestions available.', + ariaLimitedResults: 'Results have been limited to 10 suggestions. Type more characters to improve your search', + ariaGroupedResults: 'There are {n} for {x}', + groupCount: '{n} addresses', + moreResults: 'Enter more of the address to improve results', + noResults: 'No results found. Try entering a different part of the address', + tooManyResults: '{n} results found. Enter more of the address to improve results', + typeMore: 'Enter more of the address to get results', + resultsTitle: 'Select an address', + resultsTitleId: 'address-suggestions', }; const EXAMPLE_MANUAL_INPUT_FIELDS = { - organisation: { - label: 'Organisation name', - value: 'Example Organisation', - error: { text: 'Server error: organisation name' }, - }, - line1: { - label: 'Address line 1', - value: 'Flat 12345', - error: { text: 'Server error: address line 1' }, - }, - line2: { - label: 'Address line 2', - value: '12345 The Road', - error: { text: 'Server error: address line 2' }, - }, - town: { - label: 'Town or city', - value: 'The Town', - error: { text: 'Server error: town or city' }, - }, - postcode: { - label: 'Postcode', - value: 'PO57 6ODE', - error: { text: 'Server error: postcode' }, - }, -}; - -describe('macro: address-input', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('address-input', EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - describe('manual entry of address', () => { - it('has class to hide input fields when automatic search is enabled', () => { - const $ = cheerio.load( - renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - isEditable: true, - manualEntry: false, - }), - ); - - expect($('.ons-js-address-input__manual').hasClass('ons-u-db-no-js_enabled')).toBe(true); - }); - - it('does not have class to hide input fields when automatic search is disabled', () => { - const $ = cheerio.load( - renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - isEditable: true, - manualEntry: true, - }), - ); - - expect($('.ons-js-address-input__manual').hasClass('ons-u-db-no-js_enabled')).toBe(false); - }); - - it('renders "organisation" input field with expected parameters', () => { - const faker = templateFaker(); - const inputSpy = faker.spy('input', { suppressOutput: true }); - - faker.renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - ...EXAMPLE_MANUAL_INPUT_FIELDS, - isEditable: true, - }); - - expect(inputSpy.occurrences).toContainEqual({ - id: 'address-input-example-id-organisation', - name: 'address-input-example-id-organisation', - classes: 'ons-js-address-organisation', - label: { - text: 'Organisation name', - }, + organisation: { + label: 'Organisation name', value: 'Example Organisation', - width: '20@m', error: { text: 'Server error: organisation name' }, - }); - }); - - it('renders "address line 1" input field with expected parameters', () => { - const faker = templateFaker(); - const inputSpy = faker.spy('input', { suppressOutput: true }); - - faker.renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - ...EXAMPLE_MANUAL_INPUT_FIELDS, - isEditable: true, - }); - - expect(inputSpy.occurrences).toContainEqual({ - id: 'address-input-example-id-line1', - name: 'address-input-example-id-line1', - classes: 'ons-js-address-line1', - label: { - text: 'Address line 1', - }, + }, + line1: { + label: 'Address line 1', value: 'Flat 12345', - width: '20@m', error: { text: 'Server error: address line 1' }, - }); - }); - - it('renders "address line 2" input field with expected parameters', () => { - const faker = templateFaker(); - const inputSpy = faker.spy('input', { suppressOutput: true }); - - faker.renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - ...EXAMPLE_MANUAL_INPUT_FIELDS, - isEditable: true, - }); - - expect(inputSpy.occurrences).toContainEqual({ - id: 'address-input-example-id-line2', - name: 'address-input-example-id-line2', - classes: 'ons-js-address-line2', - label: { - text: 'Address line 2', - }, + }, + line2: { + label: 'Address line 2', value: '12345 The Road', - width: '20@m', error: { text: 'Server error: address line 2' }, - }); - }); - - it('renders "town or city" input field with expected parameters', () => { - const faker = templateFaker(); - const inputSpy = faker.spy('input', { suppressOutput: true }); - - faker.renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - ...EXAMPLE_MANUAL_INPUT_FIELDS, - isEditable: true, - }); - - expect(inputSpy.occurrences).toContainEqual({ - id: 'address-input-example-id-town', - name: 'address-input-example-id-town', - classes: 'ons-js-address-town', - label: { - text: 'Town or city', - }, + }, + town: { + label: 'Town or city', value: 'The Town', error: { text: 'Server error: town or city' }, - }); - }); - - it('renders "postcode" input field with expected parameters', () => { - const faker = templateFaker(); - const inputSpy = faker.spy('input', { suppressOutput: true }); - - faker.renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - ...EXAMPLE_MANUAL_INPUT_FIELDS, - isEditable: true, - }); - - expect(inputSpy.occurrences).toContainEqual({ - id: 'address-input-example-id-postcode', - name: 'address-input-example-id-postcode', - classes: 'ons-js-address-postcode', - label: { - text: 'Postcode', - }, + }, + postcode: { + label: 'Postcode', value: 'PO57 6ODE', - width: '7', error: { text: 'Server error: postcode' }, - }); - }); - }); - - describe('search button for no-js', () => { - it('is not rendered when automatic search is disabled', () => { - const $ = cheerio.load( - renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - isEditable: true, - manualEntry: true, - searchButton: 'Search for address', - }), - ); - - expect($('.ons-js-address-search-btn').length).toBe(0); - }); - - it('marks field so that it is displayed only when there is no javascript', () => { - const $ = cheerio.load( - renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - isEditable: true, - searchButton: 'Search for address', - }), - ); - - expect($('.ons-js-address-search-btn').hasClass('ons-u-db-no-js_disabled')).toBe(true); - }); - - it('renders provided text for search button', () => { - const $ = cheerio.load( - renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - isEditable: true, - searchButton: 'Search for address', - }), - ); - - expect($('.ons-js-address-search-btn').text().trim()).toBe('Search for address'); - }); - }); - - describe('hidden field for uprn', () => { - it('renders hidden `input` component with expected parameters when `uprn.value` is not provided', () => { - const faker = templateFaker(); - const inputSpy = faker.spy('input', { suppressOutput: true }); - - faker.renderComponent('address-input', EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL); - - expect(inputSpy.occurrences).toContainEqual({ - id: 'address-input-example-id-uprn', - classes: 'ons-js-hidden-uprn ons-u-d-no', - type: 'hidden', - name: 'address-input-example-id-uprn', - value: '', - }); - }); - - it('renders hidden `input` component with expected parameters when `uprn.value` is provided', () => { - const faker = templateFaker(); - const inputSpy = faker.spy('input', { suppressOutput: true }); - - faker.renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - uprn: { - value: '[params.uprn.value]', - }, - }); - - expect(inputSpy.occurrences).toContainEqual({ - id: 'address-input-example-id-uprn', - classes: 'ons-js-hidden-uprn ons-u-d-no', - type: 'hidden', - name: 'address-input-example-id-uprn', - value: '[params.uprn.value]', - }); - }); - }); - - describe('autosuggest search field', () => { - it('is not shown when `manualEntry` is `true`', () => { - const faker = templateFaker(); - const autosuggestSpy = faker.spy('autosuggest', { suppressOutput: true }); + }, +}; - faker.renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - manualEntry: true, - }); +describe('macro: address-input', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('address-input', EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL)); - expect(autosuggestSpy.occurrences.length).toBe(0); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); }); - it('renders `autosuggest` component with expected parameters', () => { - const faker = templateFaker(); - const autosuggestSpy = faker.spy('autosuggest', { suppressOutput: true }); - - // Since `autosuggestSpy` suppresses output the values being tested below do not - // need to represent real values. This test is only interested in verifying that - // the provided values are being passed through to the `autosuggest` component. - faker.renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - label: { - text: '[params.label.text]', - id: '[params.label.id]', - }, - value: '[params.value]', - attributes: '[params.attributes]', - error: '[params.error]', - name: '[params.name]', - mutuallyExclusive: '[params.mutuallyExclusive]', - APIDomain: '[params.APIDomain]', - APIDomainBearerToken: '[params.APIDomainBearerToken]', - APIManualQueryParams: '[params.APIManualQueryParams]', - allowMultiple: '[params.allowMultiple]', - mandatory: '[params.mandatory]', - instructions: '[params.instructions]', - autocomplete: '[params.autocomplete]', - isEditable: '[params.isEditable]', - ariaYouHaveSelected: '[params.ariaYouHaveSelected]', - ariaMinChars: '[params.ariaMinChars]', - minChars: '[params.minChars]', - ariaResultsLabel: '[params.ariaResultsLabel]', - ariaOneResult: '[params.ariaOneResult]', - ariaNResults: '[params.ariaNResults]', - ariaLimitedResults: '[params.ariaLimitedResults]', - ariaGroupedResults: '[params.ariaGroupedResults]', - groupCount: '[params.groupCount]', - moreResults: '[params.moreResults]', - tooManyResults: '[params.tooManyResults]', - resultsTitle: '[params.resultsTitle]', - resultsTitleId: '[params.resultsTitleId]', - noResults: '[params.noResults]', - typeMore: '[params.typeMore]', - errorTitle: '[params.errorTitle]', - errorMessageEnter: '[params.errorMessageEnter]', - errorMessageSelect: '[params.errorMessageSelect]', - errorMessageAPI: '[params.errorMessageAPI]', - errorMessageAPILinkText: '[params.errorMessageAPILinkText]', - options: '[params.options]', - manualLink: '[params.manualLink]', - manualLinkText: '[params.manualLinkText]', - }); - - expect(autosuggestSpy.occurrences[0]).toEqual({ - id: 'address-input-example-id-autosuggest', - classes: 'ons-address-input__autosuggest ons-u-mb-no', - input: { - width: '50', - label: { - text: '[params.label.text]', - id: '[params.label.id]', - classes: 'ons-js-autosuggest-label', - }, - value: '[params.value]', - attributes: '[params.attributes]', - error: '[params.error]', - name: '[params.name]', - mutuallyExclusive: '[params.mutuallyExclusive]', - }, - externalInitialiser: true, - APIDomain: '[params.APIDomain]', - APIDomainBearerToken: '[params.APIDomainBearerToken]', - APIManualQueryParams: '[params.APIManualQueryParams]', - allowMultiple: '[params.allowMultiple]', - mandatory: '[params.mandatory]', - instructions: '[params.instructions]', - autocomplete: '[params.autocomplete]', - isEditable: '[params.isEditable]', - ariaYouHaveSelected: '[params.ariaYouHaveSelected]', - ariaMinChars: '[params.ariaMinChars]', - minChars: '[params.minChars]', - ariaOneResult: '[params.ariaOneResult]', - ariaNResults: '[params.ariaNResults]', - ariaLimitedResults: '[params.ariaLimitedResults]', - ariaGroupedResults: '[params.ariaGroupedResults]', - groupCount: '[params.groupCount]', - moreResults: '[params.moreResults]', - tooManyResults: '[params.tooManyResults]', - resultsTitle: '[params.resultsTitle]', - resultsTitleId: '[params.resultsTitleId]', - noResults: '[params.noResults]', - typeMore: '[params.typeMore]', - errorTitle: '[params.errorTitle]', - errorMessageEnter: '[params.errorMessageEnter]', - errorMessageSelect: '[params.errorMessageSelect]', - errorMessageAPI: '[params.errorMessageAPI]', - errorMessageAPILinkText: '[params.errorMessageAPILinkText]', - options: '[params.options]', - manualLink: '[params.manualLink]', - manualLinkText: '[params.manualLinkText]', - }); + describe('manual entry of address', () => { + it('has class to hide input fields when automatic search is enabled', () => { + const $ = cheerio.load( + renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + isEditable: true, + manualEntry: false, + }), + ); + + expect($('.ons-js-address-input__manual').hasClass('ons-u-db-no-js_enabled')).toBe(true); + }); + + it('does not have class to hide input fields when automatic search is disabled', () => { + const $ = cheerio.load( + renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + isEditable: true, + manualEntry: true, + }), + ); + + expect($('.ons-js-address-input__manual').hasClass('ons-u-db-no-js_enabled')).toBe(false); + }); + + it('renders "organisation" input field with expected parameters', () => { + const faker = templateFaker(); + const inputSpy = faker.spy('input', { suppressOutput: true }); + + faker.renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + ...EXAMPLE_MANUAL_INPUT_FIELDS, + isEditable: true, + }); + + expect(inputSpy.occurrences).toContainEqual({ + id: 'address-input-example-id-organisation', + name: 'address-input-example-id-organisation', + classes: 'ons-js-address-organisation', + label: { + text: 'Organisation name', + }, + value: 'Example Organisation', + width: '20@m', + error: { text: 'Server error: organisation name' }, + }); + }); + + it('renders "address line 1" input field with expected parameters', () => { + const faker = templateFaker(); + const inputSpy = faker.spy('input', { suppressOutput: true }); + + faker.renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + ...EXAMPLE_MANUAL_INPUT_FIELDS, + isEditable: true, + }); + + expect(inputSpy.occurrences).toContainEqual({ + id: 'address-input-example-id-line1', + name: 'address-input-example-id-line1', + classes: 'ons-js-address-line1', + label: { + text: 'Address line 1', + }, + value: 'Flat 12345', + width: '20@m', + error: { text: 'Server error: address line 1' }, + }); + }); + + it('renders "address line 2" input field with expected parameters', () => { + const faker = templateFaker(); + const inputSpy = faker.spy('input', { suppressOutput: true }); + + faker.renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + ...EXAMPLE_MANUAL_INPUT_FIELDS, + isEditable: true, + }); + + expect(inputSpy.occurrences).toContainEqual({ + id: 'address-input-example-id-line2', + name: 'address-input-example-id-line2', + classes: 'ons-js-address-line2', + label: { + text: 'Address line 2', + }, + value: '12345 The Road', + width: '20@m', + error: { text: 'Server error: address line 2' }, + }); + }); + + it('renders "town or city" input field with expected parameters', () => { + const faker = templateFaker(); + const inputSpy = faker.spy('input', { suppressOutput: true }); + + faker.renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + ...EXAMPLE_MANUAL_INPUT_FIELDS, + isEditable: true, + }); + + expect(inputSpy.occurrences).toContainEqual({ + id: 'address-input-example-id-town', + name: 'address-input-example-id-town', + classes: 'ons-js-address-town', + label: { + text: 'Town or city', + }, + value: 'The Town', + error: { text: 'Server error: town or city' }, + }); + }); + + it('renders "postcode" input field with expected parameters', () => { + const faker = templateFaker(); + const inputSpy = faker.spy('input', { suppressOutput: true }); + + faker.renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + ...EXAMPLE_MANUAL_INPUT_FIELDS, + isEditable: true, + }); + + expect(inputSpy.occurrences).toContainEqual({ + id: 'address-input-example-id-postcode', + name: 'address-input-example-id-postcode', + classes: 'ons-js-address-postcode', + label: { + text: 'Postcode', + }, + value: 'PO57 6ODE', + width: '7', + error: { text: 'Server error: postcode' }, + }); + }); }); - it('renders manualLinkText` when provided with a default value for `manualLink`', () => { - const $ = cheerio.load( - renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - manualLinkText: 'Manually enter address', - }), - ); - - expect($('.ons-js-address-manual-btn').attr('href')).toBe('#0'); - expect($('.ons-js-address-manual-btn').text().trim()).toBe('Manually enter address'); + describe('search button for no-js', () => { + it('is not rendered when automatic search is disabled', () => { + const $ = cheerio.load( + renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + isEditable: true, + manualEntry: true, + searchButton: 'Search for address', + }), + ); + + expect($('.ons-js-address-search-btn').length).toBe(0); + }); + + it('marks field so that it is displayed only when there is no javascript', () => { + const $ = cheerio.load( + renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + isEditable: true, + searchButton: 'Search for address', + }), + ); + + expect($('.ons-js-address-search-btn').hasClass('ons-u-db-no-js_disabled')).toBe(true); + }); + + it('renders provided text for search button', () => { + const $ = cheerio.load( + renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + isEditable: true, + searchButton: 'Search for address', + }), + ); + + expect($('.ons-js-address-search-btn').text().trim()).toBe('Search for address'); + }); }); - it('renders `manualLinkText` with `manualLink` when provided', () => { - const $ = cheerio.load( - renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - manualLink: 'https://example.com/edit-address', - manualLinkText: 'Manually enter address', - }), - ); - - expect($('.ons-js-address-manual-btn').attr('href')).toBe('https://example.com/edit-address'); - expect($('.ons-js-address-manual-btn').text().trim()).toBe('Manually enter address'); + describe('hidden field for uprn', () => { + it('renders hidden `input` component with expected parameters when `uprn.value` is not provided', () => { + const faker = templateFaker(); + const inputSpy = faker.spy('input', { suppressOutput: true }); + + faker.renderComponent('address-input', EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL); + + expect(inputSpy.occurrences).toContainEqual({ + id: 'address-input-example-id-uprn', + classes: 'ons-js-hidden-uprn ons-u-d-no', + type: 'hidden', + name: 'address-input-example-id-uprn', + value: '', + }); + }); + + it('renders hidden `input` component with expected parameters when `uprn.value` is provided', () => { + const faker = templateFaker(); + const inputSpy = faker.spy('input', { suppressOutput: true }); + + faker.renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + uprn: { + value: '[params.uprn.value]', + }, + }); + + expect(inputSpy.occurrences).toContainEqual({ + id: 'address-input-example-id-uprn', + classes: 'ons-js-hidden-uprn ons-u-d-no', + type: 'hidden', + name: 'address-input-example-id-uprn', + value: '[params.uprn.value]', + }); + }); }); - }); - - describe('fieldset', () => { - it('does not render `fieldset` component when `dontWrap` is `true`', () => { - const faker = templateFaker(); - const fieldsetSpy = faker.spy('fieldset', { suppressOutput: true }); - - faker.renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - dontWrap: true, - }); - expect(fieldsetSpy.occurrences.length).toBe(0); + describe('autosuggest search field', () => { + it('is not shown when `manualEntry` is `true`', () => { + const faker = templateFaker(); + const autosuggestSpy = faker.spy('autosuggest', { suppressOutput: true }); + + faker.renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + manualEntry: true, + }); + + expect(autosuggestSpy.occurrences.length).toBe(0); + }); + + it('renders `autosuggest` component with expected parameters', () => { + const faker = templateFaker(); + const autosuggestSpy = faker.spy('autosuggest', { suppressOutput: true }); + + // Since `autosuggestSpy` suppresses output the values being tested below do not + // need to represent real values. This test is only interested in verifying that + // the provided values are being passed through to the `autosuggest` component. + faker.renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + label: { + text: '[params.label.text]', + id: '[params.label.id]', + }, + value: '[params.value]', + attributes: '[params.attributes]', + error: '[params.error]', + name: '[params.name]', + mutuallyExclusive: '[params.mutuallyExclusive]', + APIDomain: '[params.APIDomain]', + APIDomainBearerToken: '[params.APIDomainBearerToken]', + APIManualQueryParams: '[params.APIManualQueryParams]', + allowMultiple: '[params.allowMultiple]', + mandatory: '[params.mandatory]', + instructions: '[params.instructions]', + autocomplete: '[params.autocomplete]', + isEditable: '[params.isEditable]', + ariaYouHaveSelected: '[params.ariaYouHaveSelected]', + ariaMinChars: '[params.ariaMinChars]', + minChars: '[params.minChars]', + ariaResultsLabel: '[params.ariaResultsLabel]', + ariaOneResult: '[params.ariaOneResult]', + ariaNResults: '[params.ariaNResults]', + ariaLimitedResults: '[params.ariaLimitedResults]', + ariaGroupedResults: '[params.ariaGroupedResults]', + groupCount: '[params.groupCount]', + moreResults: '[params.moreResults]', + tooManyResults: '[params.tooManyResults]', + resultsTitle: '[params.resultsTitle]', + resultsTitleId: '[params.resultsTitleId]', + noResults: '[params.noResults]', + typeMore: '[params.typeMore]', + errorTitle: '[params.errorTitle]', + errorMessageEnter: '[params.errorMessageEnter]', + errorMessageSelect: '[params.errorMessageSelect]', + errorMessageAPI: '[params.errorMessageAPI]', + errorMessageAPILinkText: '[params.errorMessageAPILinkText]', + options: '[params.options]', + manualLink: '[params.manualLink]', + manualLinkText: '[params.manualLinkText]', + }); + + expect(autosuggestSpy.occurrences[0]).toEqual({ + id: 'address-input-example-id-autosuggest', + classes: 'ons-address-input__autosuggest ons-u-mb-no', + input: { + width: '50', + label: { + text: '[params.label.text]', + id: '[params.label.id]', + classes: 'ons-js-autosuggest-label', + }, + value: '[params.value]', + attributes: '[params.attributes]', + error: '[params.error]', + name: '[params.name]', + mutuallyExclusive: '[params.mutuallyExclusive]', + }, + externalInitialiser: true, + APIDomain: '[params.APIDomain]', + APIDomainBearerToken: '[params.APIDomainBearerToken]', + APIManualQueryParams: '[params.APIManualQueryParams]', + allowMultiple: '[params.allowMultiple]', + mandatory: '[params.mandatory]', + instructions: '[params.instructions]', + autocomplete: '[params.autocomplete]', + isEditable: '[params.isEditable]', + ariaYouHaveSelected: '[params.ariaYouHaveSelected]', + ariaMinChars: '[params.ariaMinChars]', + minChars: '[params.minChars]', + ariaOneResult: '[params.ariaOneResult]', + ariaNResults: '[params.ariaNResults]', + ariaLimitedResults: '[params.ariaLimitedResults]', + ariaGroupedResults: '[params.ariaGroupedResults]', + groupCount: '[params.groupCount]', + moreResults: '[params.moreResults]', + tooManyResults: '[params.tooManyResults]', + resultsTitle: '[params.resultsTitle]', + resultsTitleId: '[params.resultsTitleId]', + noResults: '[params.noResults]', + typeMore: '[params.typeMore]', + errorTitle: '[params.errorTitle]', + errorMessageEnter: '[params.errorMessageEnter]', + errorMessageSelect: '[params.errorMessageSelect]', + errorMessageAPI: '[params.errorMessageAPI]', + errorMessageAPILinkText: '[params.errorMessageAPILinkText]', + options: '[params.options]', + manualLink: '[params.manualLink]', + manualLinkText: '[params.manualLinkText]', + }); + }); + + it('renders manualLinkText` when provided with a default value for `manualLink`', () => { + const $ = cheerio.load( + renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + manualLinkText: 'Manually enter address', + }), + ); + + expect($('.ons-js-address-manual-btn').attr('href')).toBe('#0'); + expect($('.ons-js-address-manual-btn').text().trim()).toBe('Manually enter address'); + }); + + it('renders `manualLinkText` with `manualLink` when provided', () => { + const $ = cheerio.load( + renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + manualLink: 'https://example.com/edit-address', + manualLinkText: 'Manually enter address', + }), + ); + + expect($('.ons-js-address-manual-btn').attr('href')).toBe('https://example.com/edit-address'); + expect($('.ons-js-address-manual-btn').text().trim()).toBe('Manually enter address'); + }); }); - it('renders `fieldset` component with expected parameters', () => { - const faker = templateFaker(); - const fieldsetSpy = faker.spy('fieldset', { suppressOutput: true }); - - faker.renderComponent('address-input', { - ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, - classes: 'extra-field-class', - legendClasses: 'extra-legend-class', - }); - - expect(fieldsetSpy.occurrences[0]).toEqual({ - id: 'address-input-example-id', - classes: 'extra-field-class', - legend: 'What is the address?', - legendClasses: 'extra-legend-class', - }); + describe('fieldset', () => { + it('does not render `fieldset` component when `dontWrap` is `true`', () => { + const faker = templateFaker(); + const fieldsetSpy = faker.spy('fieldset', { suppressOutput: true }); + + faker.renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + dontWrap: true, + }); + + expect(fieldsetSpy.occurrences.length).toBe(0); + }); + + it('renders `fieldset` component with expected parameters', () => { + const faker = templateFaker(); + const fieldsetSpy = faker.spy('fieldset', { suppressOutput: true }); + + faker.renderComponent('address-input', { + ...EXAMPLE_AUTOSUGGEST_ADDRESS_MINIMAL, + classes: 'extra-field-class', + legendClasses: 'extra-legend-class', + }); + + expect(fieldsetSpy.occurrences[0]).toEqual({ + id: 'address-input-example-id', + classes: 'extra-field-class', + legend: 'What is the address?', + legendClasses: 'extra-legend-class', + }); + }); }); - }); }); diff --git a/src/components/address-input/autosuggest.address.dom.js b/src/components/address-input/autosuggest.address.dom.js index 5afb299ba6..3be469d51d 100644 --- a/src/components/address-input/autosuggest.address.dom.js +++ b/src/components/address-input/autosuggest.address.dom.js @@ -1,13 +1,13 @@ import domready from '../../js/domready'; async function initialiseAddressAutosuggests() { - const addressAutosuggests = [...document.querySelectorAll('.ons-js-address-autosuggest')]; + const addressAutosuggests = [...document.querySelectorAll('.ons-js-address-autosuggest')]; - if (addressAutosuggests.length) { - const AutosuggestAddress = (await import('./autosuggest.address')).default; + if (addressAutosuggests.length) { + const AutosuggestAddress = (await import('./autosuggest.address')).default; - addressAutosuggests.forEach((addressAutosuggest) => new AutosuggestAddress(addressAutosuggest)); - } + addressAutosuggests.forEach((addressAutosuggest) => new AutosuggestAddress(addressAutosuggest)); + } } domready(initialiseAddressAutosuggests); diff --git a/src/components/address-input/autosuggest.address.error.js b/src/components/address-input/autosuggest.address.error.js index 469855b43c..9768d09701 100644 --- a/src/components/address-input/autosuggest.address.error.js +++ b/src/components/address-input/autosuggest.address.error.js @@ -7,86 +7,86 @@ const classInput = 'ons-js-autosuggest-input'; const classSearch = 'ons-js-address-input__search'; export default class AddressError { - constructor(context) { - this.context = context; - this.autosuggest = document.querySelector(`.${classAutosuggest}`); - this.inputContainer = context.querySelector(`.${classInputContainer}`); - this.input = this.inputContainer.querySelector(`.${classInput}`); - this.search = context.querySelector(`.${classSearch}`); - this.errorPanel = document.querySelector(`.${classErrorPanel}`); - this.errorTitle = this.inputContainer.getAttribute('data-error-title'); - this.errorMessageEnter = this.inputContainer.getAttribute('data-error-enter'); - this.errorMessageSelect = this.inputContainer.getAttribute('data-error-select'); - this.errorMessage = this.inputContainer.getAttribute('data-error-message'); - } - - showErrorPanel() { - if (!this.errorPanel) { - //error panel - const page = document.querySelector('.ons-question'); - const errorElement = document.createElement('div'); - const errorElementHeader = document.createElement('div'); - const errorElementTitle = document.createElement('div'); - const errorBodyElement = document.createElement('div'); - - const errorListElement = document.createElement('p'); - const errorLinkElement = document.createElement('a'); - - errorElement.className = 'ons-panel ons-panel--error ons-u-mb-m ons-js-autosuggest-error-panel'; - errorElementHeader.className = 'ons-panel__header'; - errorElementTitle.className = 'ons-panel__title ons-u-fs-r--b'; - errorBodyElement.className = 'ons-panel__body'; - errorLinkElement.className = 'ons-list__link ons-js-inpagelink ons-js-error'; - errorLinkElement.href = '#autosuggest-input-error'; - - errorElementTitle.innerHTML = this.errorTitle; - - errorElement.appendChild(errorElementHeader); - errorElementHeader.appendChild(errorElementTitle); - errorElement.appendChild(errorBodyElement); - errorBodyElement.appendChild(errorListElement); - errorListElement.appendChild(errorLinkElement); - - page.insertBefore(errorElement, page.firstChild); - - // fire the inpagelink function - const links = [...document.getElementsByClassName('ons-js-inpagelink')]; - inPageLinks(links); - - //input error - const inputErrorPanel = document.createElement('div'); - const inputErrorPanelBody = document.createElement('div'); - const inputErrorPanelP = document.createElement('p'); - const inputErrorPanelStrong = document.createElement('strong'); - - inputErrorPanel.className = 'ons-panel ons-panel--error ons-panel--no-title'; - inputErrorPanel.id = 'autosuggest-input-error'; - inputErrorPanelBody.className = 'ons-panel__body'; - inputErrorPanelP.className = 'ons-panel__error'; - inputErrorPanelStrong.className = 'ons-panel__error-message'; - inputErrorPanel.appendChild(inputErrorPanelBody); - inputErrorPanelBody.appendChild(inputErrorPanelP); - inputErrorPanelP.appendChild(inputErrorPanelStrong); - inputErrorPanelBody.appendChild(this.search); - - this.input.classList.add('ons-input--error'); - this.context.appendChild(inputErrorPanel); - - this.input.focus(); + constructor(context) { + this.context = context; + this.autosuggest = document.querySelector(`.${classAutosuggest}`); + this.inputContainer = context.querySelector(`.${classInputContainer}`); + this.input = this.inputContainer.querySelector(`.${classInput}`); + this.search = context.querySelector(`.${classSearch}`); + this.errorPanel = document.querySelector(`.${classErrorPanel}`); + this.errorTitle = this.inputContainer.getAttribute('data-error-title'); + this.errorMessageEnter = this.inputContainer.getAttribute('data-error-enter'); + this.errorMessageSelect = this.inputContainer.getAttribute('data-error-select'); + this.errorMessage = this.inputContainer.getAttribute('data-error-message'); } - document.querySelector('.ons-js-error').innerHTML = this.input.value === '' ? this.errorMessageEnter : this.errorMessageSelect; - document.querySelector('.ons-panel__error-message').innerHTML = - this.input.value === '' ? this.errorMessageEnter : this.errorMessageSelect; - } + showErrorPanel() { + if (!this.errorPanel) { + // Error panel + const page = document.querySelector('.ons-question'); + const errorElement = document.createElement('div'); + const errorElementHeader = document.createElement('div'); + const errorElementTitle = document.createElement('div'); + const errorBodyElement = document.createElement('div'); + + const errorListElement = document.createElement('p'); + const errorLinkElement = document.createElement('a'); + + errorElement.className = 'ons-panel ons-panel--error ons-u-mb-m ons-js-autosuggest-error-panel'; + errorElementHeader.className = 'ons-panel__header'; + errorElementTitle.className = 'ons-panel__title ons-u-fs-r--b'; + errorBodyElement.className = 'ons-panel__body'; + errorLinkElement.className = 'ons-list__link ons-js-inpagelink ons-js-error'; + errorLinkElement.href = '#autosuggest-input-error'; + + errorElementTitle.innerHTML = this.errorTitle; + + errorElement.appendChild(errorElementHeader); + errorElementHeader.appendChild(errorElementTitle); + errorElement.appendChild(errorBodyElement); + errorBodyElement.appendChild(errorListElement); + errorListElement.appendChild(errorLinkElement); + + page.insertBefore(errorElement, page.firstChild); + + // Fire the inpagelink function + const links = [...document.getElementsByClassName('ons-js-inpagelink')]; + inPageLinks(links); + + // Input error + const inputErrorPanel = document.createElement('div'); + const inputErrorPanelBody = document.createElement('div'); + const inputErrorPanelP = document.createElement('p'); + const inputErrorPanelStrong = document.createElement('strong'); + + inputErrorPanel.className = 'ons-panel ons-panel--error ons-panel--no-title'; + inputErrorPanel.id = 'autosuggest-input-error'; + inputErrorPanelBody.className = 'ons-panel__body'; + inputErrorPanelP.className = 'ons-panel__error'; + inputErrorPanelStrong.className = 'ons-panel__error-message'; + inputErrorPanel.appendChild(inputErrorPanelBody); + inputErrorPanelBody.appendChild(inputErrorPanelP); + inputErrorPanelP.appendChild(inputErrorPanelStrong); + inputErrorPanelBody.appendChild(this.search); + + this.input.classList.add('ons-input--error'); + this.context.appendChild(inputErrorPanel); + + this.input.focus(); + } + + document.querySelector('.ons-js-error').innerHTML = this.input.value === '' ? this.errorMessageEnter : this.errorMessageSelect; + document.querySelector('.ons-panel__error-message').innerHTML = + this.input.value === '' ? this.errorMessageEnter : this.errorMessageSelect; + } - removeErrorPanel() { - this.errorPanel.remove(); + removeErrorPanel() { + this.errorPanel.remove(); - this.autosuggest.appendChild(this.search); - this.input.classList.remove('ons-input--error'); + this.autosuggest.appendChild(this.search); + this.input.classList.remove('ons-input--error'); - const errorInput = document.getElementById('autosuggest-input-error'); - errorInput.remove(); - } + const errorInput = document.getElementById('autosuggest-input-error'); + errorInput.remove(); + } } diff --git a/src/components/address-input/autosuggest.address.js b/src/components/address-input/autosuggest.address.js index c01607d1a9..b8e1d0c638 100644 --- a/src/components/address-input/autosuggest.address.js +++ b/src/components/address-input/autosuggest.address.js @@ -12,405 +12,405 @@ export const classInput = 'ons-js-autosuggest-input'; export const classInputUPRN = 'ons-js-hidden-uprn'; export default class AutosuggestAddress { - constructor(context) { - this.context = context; - this.input = context.querySelector(`.${classInput}`); - this.search = context.querySelector(`.${classSearch}`); - this.addressReplaceChars = [',']; - this.sanitisedQuerySplitNumsChars = true; - this.form = context.closest('form'); - this.container = context.querySelector(`.${classInputContainer}`); - this.errorMessage = this.container.getAttribute('data-error-message'); - this.groupCount = this.container.getAttribute('data-group-count'); - this.authorizationToken = this.container.getAttribute('data-authorization-token'); - this.uprn = context.querySelector(`.${classInputUPRN}`); - - // State - this.fetch = null; - this.totalResults = 0; - this.errored = false; - this.isEditable = context.querySelector(`.${classNotEditable}`) ? false : true; - this.isMandatory = context.querySelector(`.${classMandatory}`) ? true : false; - this.addressSelected = false; - this.groupQuery = ''; - this.selectedAddressValue = ''; - - // Bind event listeners - if (this.form) { - this.form.addEventListener('submit', this.handleSubmit.bind(this)); + constructor(context) { + this.context = context; + this.input = context.querySelector(`.${classInput}`); + this.search = context.querySelector(`.${classSearch}`); + this.addressReplaceChars = [',']; + this.sanitisedQuerySplitNumsChars = true; + this.form = context.closest('form'); + this.container = context.querySelector(`.${classInputContainer}`); + this.errorMessage = this.container.getAttribute('data-error-message'); + this.groupCount = this.container.getAttribute('data-group-count'); + this.authorizationToken = this.container.getAttribute('data-authorization-token'); + this.uprn = context.querySelector(`.${classInputUPRN}`); + + // State + this.fetch = null; + this.totalResults = 0; + this.errored = false; + this.isEditable = context.querySelector(`.${classNotEditable}`) ? false : true; + this.isMandatory = context.querySelector(`.${classMandatory}`) ? true : false; + this.addressSelected = false; + this.groupQuery = ''; + this.selectedAddressValue = ''; + + // Bind event listeners + if (this.form) { + this.form.addEventListener('submit', this.handleSubmit.bind(this)); + } + + // Initialise address setter + if (this.isEditable) { + this.addressSetter = new AddressSetter(context); + } + + // Initialise autosuggest + this.autosuggest = new AutosuggestUI({ + context: this.container, + onSelect: this.onAddressSelect.bind(this), + lang: this.lang, + suggestionFunction: this.suggestAddresses.bind(this), + sanitisedQueryReplaceChars: this.addressReplaceChars, + sanitisedQuerySplitNumsChars: this.sanitisedQuerySplitNumsChars, + suggestOnBoot: true, + handleUpdate: true, + }); + + // Set up AIMS api variables and auth + this.APIDomain = this.container.getAttribute('data-api-domain'); + this.lookupURL = `${this.APIDomain}/addresses/eq?input=`; + this.lookupGroupURL = `${this.APIDomain}/addresses/eq/bucket?`; + this.retrieveURL = `${this.APIDomain}/addresses/eq/uprn/`; + + // Query string options + this.manualQueryParams = this.container.getAttribute('data-query-params'); + this.regionCode = this.container.getAttribute('data-options-region-code'); + this.epoch = this.container.getAttribute('data-options-one-year-ago'); + this.classificationFilter = this.container.getAttribute('data-options-address-type'); + + // Check API status + this.checkAPIStatus(); } - // Initialise address setter - if (this.isEditable) { - this.addressSetter = new AddressSetter(context); + get lang() { + return document.documentElement.getAttribute('lang').toLowerCase(); } - // Initialise autosuggest - this.autosuggest = new AutosuggestUI({ - context: this.container, - onSelect: this.onAddressSelect.bind(this), - lang: this.lang, - suggestionFunction: this.suggestAddresses.bind(this), - sanitisedQueryReplaceChars: this.addressReplaceChars, - sanitisedQuerySplitNumsChars: this.sanitisedQuerySplitNumsChars, - suggestOnBoot: true, - handleUpdate: true, - }); - - // Set up AIMS api variables and auth - this.APIDomain = this.container.getAttribute('data-api-domain'); - this.lookupURL = `${this.APIDomain}/addresses/eq?input=`; - this.lookupGroupURL = `${this.APIDomain}/addresses/eq/bucket?`; - this.retrieveURL = `${this.APIDomain}/addresses/eq/uprn/`; - - // Query string options - this.manualQueryParams = this.container.getAttribute('data-query-params'); - this.regionCode = this.container.getAttribute('data-options-region-code'); - this.epoch = this.container.getAttribute('data-options-one-year-ago'); - this.classificationFilter = this.container.getAttribute('data-options-address-type'); - - // Check API status - this.checkAPIStatus(); - } - - get lang() { - return document.documentElement.getAttribute('lang').toLowerCase(); - } - - async checkAPIStatus() { - this.fetch = abortableFetch(this.lookupURL + 'cf142&limit=10', { - method: 'GET', - headers: this.setAuthorization(this.authorizationToken), - }); - try { - const response = await this.fetch.send(); - const status = (await response.json()).status.code; - if (status > 400) { - if (this.isEditable) { - this.handleAPIError(); - } else { - this.autosuggest.handleNoResults(status); + async checkAPIStatus() { + this.fetch = abortableFetch(this.lookupURL + 'cf142&limit=10', { + method: 'GET', + headers: this.setAuthorization(this.authorizationToken), + }); + try { + const response = await this.fetch.send(); + const status = (await response.json()).status.code; + if (status > 400) { + if (this.isEditable) { + this.handleAPIError(); + } else { + this.autosuggest.handleNoResults(status); + } + } + } catch (error) { + if (this.isEditable) { + this.handleAPIError(); + } else { + this.autosuggest.handleNoResults(status); + } } - } - } catch (error) { - if (this.isEditable) { - this.handleAPIError(); - } else { - this.autosuggest.handleNoResults(status); - } } - } - async suggestAddresses(query, [], grouped) { - if (this.fetch && this.fetch.status !== 'DONE') { - this.fetch.abort(); + async suggestAddresses(query, [], grouped) { + if (this.fetch && this.fetch.status !== 'DONE') { + this.fetch.abort(); + } + + return await this.findAddress(query, grouped); } - return await this.findAddress(query, grouped); - } + async findAddress(text, grouped) { + let queryURL, fullQueryURL; + + if (this.manualQueryParams) { + const manualQueryParams = this.container.getAttribute('data-query-params'); + fullQueryURL = this.lookupURL + text + manualQueryParams; + } else { + const fullPostcodeQuery = this.testFullPostcodeQuery(text); + let limit = fullPostcodeQuery ? 100 : 10; + + queryURL = grouped ? this.lookupGroupURL + this.groupQuery : this.lookupURL + text + '&limit=' + limit; - async findAddress(text, grouped) { - let queryURL, fullQueryURL; + fullQueryURL = this.generateURLParams(queryURL); + if (fullPostcodeQuery && grouped !== false) { + fullQueryURL = fullQueryURL + '&groupfullpostcodes=combo'; + } + } + + this.fetch = abortableFetch(fullQueryURL, { + method: 'GET', + headers: this.setAuthorization(this.authorizationToken), + }); + const data = await this.fetch.send(); + const response = await data.json(); + const status = response.status.code; + const addresses = response.response; + const limit = response.response.limit; + const results = await this.mapFindResults(addresses, limit, status); + return results; + } - if (this.manualQueryParams) { - const manualQueryParams = this.container.getAttribute('data-query-params'); - fullQueryURL = this.lookupURL + text + manualQueryParams; - } else { - const fullPostcodeQuery = this.testFullPostcodeQuery(text); - let limit = fullPostcodeQuery ? 100 : 10; + async mapFindResults(results, limit, status) { + let mappedResults, currentResults; + const total = results.total; + + if (results.partpostcode || (results.groupfullpostcodes === 'combo' && results.postcodes && results.postcodes.length > 1)) { + mappedResults = await this.postcodeGroupsMapping(results); + currentResults = mappedResults; + } else if (results.addresses) { + mappedResults = await this.addressMapping(results); + currentResults = mappedResults ? mappedResults.results.sort() : null; + } else { + currentResults = results.addresses; + } + return { + results: currentResults, + totalResults: total, + limit: limit, + status: status, + }; + } - queryURL = grouped ? this.lookupGroupURL + this.groupQuery : this.lookupURL + text + '&limit=' + limit; + async addressMapping(results) { + let updatedResults; + const addresses = results.addresses; + if (addresses[0]) { + if (addresses[0] && addresses[0].bestMatchAddress) { + updatedResults = addresses.map(({ uprn, bestMatchAddress, bestMatchAddressType }) => ({ + uprn: uprn, + address: bestMatchAddress, + type: bestMatchAddressType, + })); + } else if (addresses[0] && addresses[0].formattedAddress) { + updatedResults = addresses.map(({ uprn, formattedAddress, addressType }) => ({ + uprn: uprn, + address: formattedAddress, + type: addressType, + })); + } + + results = updatedResults.map(({ uprn, address, type }) => { + const sanitisedText = sanitiseAutosuggestText(address, this.addressReplaceChars); + return { + [this.lang]: address, + sanitisedText, + uprn, + type, + }; + }); + return { + results: results, + limit: results.limit, + }; + } + } - fullQueryURL = this.generateURLParams(queryURL); - if (fullPostcodeQuery && grouped !== false) { - fullQueryURL = fullQueryURL + '&groupfullpostcodes=combo'; - } + async postcodeGroupsMapping(results) { + const postcodeGroups = results.postcodes; + let groups = postcodeGroups.map(({ postcode, postTown, streetName, townName, addressCount, firstUprn }) => { + return { + [this.lang]: + streetName + + ', ' + + (townName === postTown ? postTown : townName + ', ' + postTown) + + ', ' + + postcode + + ' (' + + this.groupCount.replace(`{n}`, addressCount) + + ')', + postcode, + streetName, + townName, + postTown, + firstUprn, + addressCount, + }; + }); + const newGroups = await this.replaceSingleCountAddresses(groups); + return newGroups; } - this.fetch = abortableFetch(fullQueryURL, { - method: 'GET', - headers: this.setAuthorization(this.authorizationToken), - }); - const data = await this.fetch.send(); - const response = await data.json(); - const status = response.status.code; - const addresses = response.response; - const limit = response.response.limit; - const results = await this.mapFindResults(addresses, limit, status); - return results; - } - - async mapFindResults(results, limit, status) { - let mappedResults, currentResults; - const total = results.total; - - if (results.partpostcode || (results.groupfullpostcodes === 'combo' && results.postcodes && results.postcodes.length > 1)) { - mappedResults = await this.postcodeGroupsMapping(results); - currentResults = mappedResults; - } else if (results.addresses) { - mappedResults = await this.addressMapping(results); - currentResults = mappedResults ? mappedResults.results.sort() : null; - } else { - currentResults = results.addresses; + async replaceSingleCountAddresses(items) { + for (const item of items) { + if (item.addressCount === 1 && item.firstUprn !== 0) { + let result = await this.createAddressObject(item.firstUprn); + const matchingItem = items.findIndex((x) => x.firstUprn == result.uprn); + items[matchingItem] = result; + } + } + return items; } - return { - results: currentResults, - totalResults: total, - limit: limit, - status: status, - }; - } - - async addressMapping(results) { - let updatedResults; - const addresses = results.addresses; - if (addresses[0]) { - if (addresses[0] && addresses[0].bestMatchAddress) { - updatedResults = addresses.map(({ uprn, bestMatchAddress, bestMatchAddressType }) => ({ - uprn: uprn, - address: bestMatchAddress, - type: bestMatchAddressType, - })); - } else if (addresses[0] && addresses[0].formattedAddress) { - updatedResults = addresses.map(({ uprn, formattedAddress, addressType }) => ({ - uprn: uprn, - address: formattedAddress, - type: addressType, - })); - } - - results = updatedResults.map(({ uprn, address, type }) => { + + async createAddressObject(id) { + const data = await this.retrieveAddress(id); + const address = data.response.address.formattedAddress; + const uprn = data.response.address.uprn; const sanitisedText = sanitiseAutosuggestText(address, this.addressReplaceChars); return { - [this.lang]: address, - sanitisedText, - uprn, - type, + [this.lang]: address, + sanitisedText, + uprn, }; - }); - return { - results: results, - limit: results.limit, - }; - } - } - - async postcodeGroupsMapping(results) { - const postcodeGroups = results.postcodes; - let groups = postcodeGroups.map(({ postcode, postTown, streetName, townName, addressCount, firstUprn }) => { - return { - [this.lang]: - streetName + - ', ' + - (townName === postTown ? postTown : townName + ', ' + postTown) + - ', ' + - postcode + - ' (' + - this.groupCount.replace(`{n}`, addressCount) + - ')', - postcode, - streetName, - townName, - postTown, - firstUprn, - addressCount, - }; - }); - const newGroups = await this.replaceSingleCountAddresses(groups); - return newGroups; - } - - async replaceSingleCountAddresses(items) { - for (const item of items) { - if (item.addressCount === 1 && item.firstUprn !== 0) { - let result = await this.createAddressObject(item.firstUprn); - const matchingItem = items.findIndex((x) => x.firstUprn == result.uprn); - items[matchingItem] = result; - } } - return items; - } - - async createAddressObject(id) { - const data = await this.retrieveAddress(id); - const address = data.response.address.formattedAddress; - const uprn = data.response.address.uprn; - const sanitisedText = sanitiseAutosuggestText(address, this.addressReplaceChars); - return { - [this.lang]: address, - sanitisedText, - uprn, - }; - } - - testFullPostcodeQuery(input) { - const fullPostcodeRegex = - /\b((?:(?:gir)|(?:[a-pr-uwyz])(?:(?:[0-9](?:[a-hjkpstuw]|[0-9])?)|(?:[a-hk-y][0-9](?:[0-9]|[abehmnprv-y])?)))) ?([0-9][abd-hjlnp-uw-z]{2})\b/i; - const testFullPostcode = fullPostcodeRegex.test(input); - if (testFullPostcode) { - return true; + + testFullPostcodeQuery(input) { + const fullPostcodeRegex = + /\b((?:(?:gir)|(?:[a-pr-uwyz])(?:(?:[0-9](?:[a-hjkpstuw]|[0-9])?)|(?:[a-hk-y][0-9](?:[0-9]|[abehmnprv-y])?)))) ?([0-9][abd-hjlnp-uw-z]{2})\b/i; + const testFullPostcode = fullPostcodeRegex.test(input); + if (testFullPostcode) { + return true; + } } - } - async retrieveAddress(id, type = null) { - let retrieveUrl = this.retrieveURL + id; + async retrieveAddress(id, type = null) { + let retrieveUrl = this.retrieveURL + id; - const fullUPRNURL = this.generateURLParams(retrieveUrl, id, type); + const fullUPRNURL = this.generateURLParams(retrieveUrl, id, type); - this.fetch = abortableFetch(fullUPRNURL, { - method: 'GET', - headers: this.setAuthorization(this.authorizationToken), - }); + this.fetch = abortableFetch(fullUPRNURL, { + method: 'GET', + headers: this.setAuthorization(this.authorizationToken), + }); - const response = await this.fetch.send(); - const data = await response.json(); - return data; - } + const response = await this.fetch.send(); + const data = await response.json(); + return data; + } - async onAddressSelect(selectedResult) { - if (selectedResult.uprn) { - try { - const data = await this.retrieveAddress(selectedResult.uprn, selectedResult.type); - if (this.isEditable) { - if (data.status.code >= 403) { - this.autosuggest.handleNoResults(403); - } else { - this.addressSetter.setAddress(this.createAddressLines(data)); - this.addressSelected = true; - } - } else { - this.selectedAddressValue = selectedResult.displayText; - this.autosuggest.input.value = selectedResult.displayText; - this.uprn.value = selectedResult.uprn; - this.addressSelected = true; - } - } catch (error) { - if (this.isEditable) { - this.handleAPIError(); - } else { - this.autosuggest.handleNoResults(403); + async onAddressSelect(selectedResult) { + if (selectedResult.uprn) { + try { + const data = await this.retrieveAddress(selectedResult.uprn, selectedResult.type); + if (this.isEditable) { + if (data.status.code >= 403) { + this.autosuggest.handleNoResults(403); + } else { + this.addressSetter.setAddress(this.createAddressLines(data)); + this.addressSelected = true; + } + } else { + this.selectedAddressValue = selectedResult.displayText; + this.autosuggest.input.value = selectedResult.displayText; + this.uprn.value = selectedResult.uprn; + this.addressSelected = true; + } + } catch (error) { + if (this.isEditable) { + this.handleAPIError(); + } else { + this.autosuggest.handleNoResults(403); + } + throw error; + } + } else if (selectedResult.postcode) { + this.autosuggest.input.value = + selectedResult.streetName + + ', ' + + (selectedResult.townName === selectedResult.postTown + ? selectedResult.postTown + : selectedResult.townName + ', ' + selectedResult.postTown) + + ', ' + + selectedResult.postcode; + this.autosuggest.input.focus(); + this.groupQuery = + 'postcode=' + selectedResult.postcode + '&streetname=' + selectedResult.streetName + '&townname=' + selectedResult.townName; + this.autosuggest.handleChange(true); } - throw error; - } - } else if (selectedResult.postcode) { - this.autosuggest.input.value = - selectedResult.streetName + - ', ' + - (selectedResult.townName === selectedResult.postTown - ? selectedResult.postTown - : selectedResult.townName + ', ' + selectedResult.postTown) + - ', ' + - selectedResult.postcode; - this.autosuggest.input.focus(); - this.groupQuery = - 'postcode=' + selectedResult.postcode + '&streetname=' + selectedResult.streetName + '&townname=' + selectedResult.townName; - this.autosuggest.handleChange(true); } - } - - createAddressLines(data) { - const values = data.response.address; - return { - addressLine1: values.addressLine1, - addressLine2: values.addressLine2, - addressLine3: values.addressLine3, - townName: values.townName, - postcode: values.postcode, - uprn: values.uprn, - }; - } - - generateURLParams(baseURL, uprn, type) { - let fullURL = baseURL, - addressType; - - const classificationFilterParam = '&classificationfilter=', - eboostParam = '&eboost=10', - wboostParam = '&wboost=10', - nionlyParam = '&eboost=0&sboost=0&wboost=0', - niboostParam = '&nboost=10', - favourwelshParam = '&favourwelsh=true', - addresstypeParam = '?addresstype=', - epochParam = '&epoch=75'; - - if (type) { - addressType = type.toLowerCase(); - } else { - addressType = 'paf'; + + createAddressLines(data) { + const values = data.response.address; + return { + addressLine1: values.addressLine1, + addressLine2: values.addressLine2, + addressLine3: values.addressLine3, + townName: values.townName, + postcode: values.postcode, + uprn: values.uprn, + }; } - if (!uprn) { - if (this.classificationFilter && this.classificationFilter !== 'residential') { - fullURL = fullURL + classificationFilterParam + this.classificationFilter; - } + generateURLParams(baseURL, uprn, type) { + let fullURL = baseURL, + addressType; + + const classificationFilterParam = '&classificationfilter=', + eboostParam = '&eboost=10', + wboostParam = '&wboost=10', + nionlyParam = '&eboost=0&sboost=0&wboost=0', + niboostParam = '&nboost=10', + favourwelshParam = '&favourwelsh=true', + addresstypeParam = '?addresstype=', + epochParam = '&epoch=75'; + + if (type) { + addressType = type.toLowerCase(); + } else { + addressType = 'paf'; + } - if (this.classificationFilter === 'workplace') { - if (this.regionCode === 'gb-eng') { - fullURL = fullURL + eboostParam; - } else if (this.regionCode === 'gb-wls') { - fullURL = fullURL + wboostParam; + if (!uprn) { + if (this.classificationFilter && this.classificationFilter !== 'residential') { + fullURL = fullURL + classificationFilterParam + this.classificationFilter; + } + + if (this.classificationFilter === 'workplace') { + if (this.regionCode === 'gb-eng') { + fullURL = fullURL + eboostParam; + } else if (this.regionCode === 'gb-wls') { + fullURL = fullURL + wboostParam; + } + } + + if (this.regionCode === 'gb-nir') { + if (this.classificationFilter === 'educational') { + fullURL = fullURL + nionlyParam; + } else { + fullURL = fullURL + niboostParam; + } + } + + if (this.lang === 'cy') { + fullURL = fullURL + favourwelshParam; + } + } else if (uprn) { + fullURL = baseURL + addresstypeParam + addressType; } - } - if (this.regionCode === 'gb-nir') { - if (this.classificationFilter === 'educational') { - fullURL = fullURL + nionlyParam; - } else { - fullURL = fullURL + niboostParam; + if (this.epoch === 'true') { + fullURL = fullURL + epochParam; } - } - if (this.lang === 'cy') { - fullURL = fullURL + favourwelshParam; - } - } else if (uprn) { - fullURL = baseURL + addresstypeParam + addressType; + return fullURL; } - if (this.epoch === 'true') { - fullURL = fullURL + epochParam; + handleAPIError() { + this.addressSetter.setManualMode(true, false); + const searchBtn = document.querySelector('.ons-js-address-search-btn'); + searchBtn.classList.add('ons-u-d-no'); } - return fullURL; - } - - handleAPIError() { - this.addressSetter.setManualMode(true, false); - const searchBtn = document.querySelector('.ons-js-address-search-btn'); - searchBtn.classList.add('ons-u-d-no'); - } - - handleSubmit(event) { - let isManualMode = false; + handleSubmit(event) { + let isManualMode = false; - if (this.isEditable) { - isManualMode = this.addressSetter.manualMode; - this.addressSetter.checkManualInputsValues(false); + if (this.isEditable) { + isManualMode = this.addressSetter.manualMode; + this.addressSetter.checkManualInputsValues(false); + } + if (this.isMandatory && !isManualMode) { + if ( + !this.addressSelected || + this.input.value === '' || + (!this.isEditable && this.checkValueHasBeenUpdated(this.selectedAddressValue)) + ) { + event.preventDefault(); + this.handleError = new AddressError(this.context); + this.handleError.showErrorPanel(); + this.autosuggest.setAriaStatus(this.errorMessage); + } + } } - if (this.isMandatory && !isManualMode) { - if ( - !this.addressSelected || - this.input.value === '' || - (!this.isEditable && this.checkValueHasBeenUpdated(this.selectedAddressValue)) - ) { - event.preventDefault(); - this.handleError = new AddressError(this.context); - this.handleError.showErrorPanel(); - this.autosuggest.setAriaStatus(this.errorMessage); - } + + checkValueHasBeenUpdated(value) { + if (value !== this.input.value) { + return true; + } } - } - checkValueHasBeenUpdated(value) { - if (value !== this.input.value) { - return true; + setAuthorization(token) { + this.authorization = `Bearer ${token}`; + return new Headers({ + Authorization: this.authorization, + }); } - } - - setAuthorization(token) { - this.authorization = `Bearer ${token}`; - return new Headers({ - Authorization: this.authorization, - }); - } } diff --git a/src/components/address-input/autosuggest.address.setter.js b/src/components/address-input/autosuggest.address.setter.js index 7d36b53162..b6250c9924 100644 --- a/src/components/address-input/autosuggest.address.setter.js +++ b/src/components/address-input/autosuggest.address.setter.js @@ -15,115 +15,115 @@ export const classErrorPanel = 'ons-panel--error'; export const classJsErrorPanel = 'ons-js-autosuggest-error-panel'; export default class AddressSetter { - constructor(context) { - this.context = context; - this.input = context.querySelector(`.${classAutosuggestInput}`); - this.organisation = context.querySelector(`.${classOrganisation}`); - this.line1 = context.querySelector(`.${classLine1}`); - this.line2 = context.querySelector(`.${classLine2}`); - this.town = context.querySelector(`.${classTown}`); - this.postcode = context.querySelector(`.${classPostcode}`); - this.uprn = context.querySelector(`.${classInputUPRN}`); - this.manualInputs = [this.organisation, this.line1, this.line2, this.town, this.postcode, this.uprn]; - this.search = context.querySelector(`.${classSearch}`); - this.manual = context.querySelector(`.${classManual}`); - this.searchButton = context.querySelector(`.${classSearchButton}`); - this.manualLink = context.querySelector(`.${classmanualLink}`); - this.errorPanel = document.querySelector(`.${classErrorPanel}`); - - // State - this.manualMode = true; - this.originalValues = []; - - // Bind Event Listeners - if (this.searchButton) { - this.searchButton.addEventListener('click', this.toggleMode.bind(this)); - } + constructor(context) { + this.context = context; + this.input = context.querySelector(`.${classAutosuggestInput}`); + this.organisation = context.querySelector(`.${classOrganisation}`); + this.line1 = context.querySelector(`.${classLine1}`); + this.line2 = context.querySelector(`.${classLine2}`); + this.town = context.querySelector(`.${classTown}`); + this.postcode = context.querySelector(`.${classPostcode}`); + this.uprn = context.querySelector(`.${classInputUPRN}`); + this.manualInputs = [this.organisation, this.line1, this.line2, this.town, this.postcode, this.uprn]; + this.search = context.querySelector(`.${classSearch}`); + this.manual = context.querySelector(`.${classManual}`); + this.searchButton = context.querySelector(`.${classSearchButton}`); + this.manualLink = context.querySelector(`.${classmanualLink}`); + this.errorPanel = document.querySelector(`.${classErrorPanel}`); + + // State + this.manualMode = true; + this.originalValues = []; + + // Bind Event Listeners + if (this.searchButton) { + this.searchButton.addEventListener('click', this.toggleMode.bind(this)); + } + + if (this.manualLink) { + this.manualLink.addEventListener('click', this.toggleMode.bind(this)); + } - if (this.manualLink) { - this.manualLink.addEventListener('click', this.toggleMode.bind(this)); + // Set mode + if (this.line1.value || this.line2.value || this.town.value || this.postcode.value || this.errorPanel) { + this.setManualMode(true, false); + } else { + this.toggleMode(); + } } - // Set mode - if (this.line1.value || this.line2.value || this.town.value || this.postcode.value || this.errorPanel) { - this.setManualMode(true, false); - } else { - this.toggleMode(); + toggleMode(clearInputs = true) { + this.setManualMode(!this.manualMode, clearInputs); } - } - toggleMode(clearInputs = true) { - this.setManualMode(!this.manualMode, clearInputs); - } + setManualMode(manual, clearInputs) { + this.manual.classList[manual ? 'remove' : 'add']('ons-u-db-no-js_enabled'); + this.search.classList[manual ? 'add' : 'remove']('ons-u-d-no'); + if (this.errorPanel) { + this.errorPanel.classList[manual ? 'remove' : 'add']('ons-u-d-no'); + } - setManualMode(manual, clearInputs) { - this.manual.classList[manual ? 'remove' : 'add']('ons-u-db-no-js_enabled'); - this.search.classList[manual ? 'add' : 'remove']('ons-u-d-no'); - if (this.errorPanel) { - this.errorPanel.classList[manual ? 'remove' : 'add']('ons-u-d-no'); - } + if (clearInputs) { + this.onUnsetAddress(); + } - if (clearInputs) { - this.onUnsetAddress(); + if (manual) { + this.input.value = ''; + + this.JsErrorPanel = document.querySelector(`.${classJsErrorPanel}`); + if (this.JsErrorPanel) { + const removeError = new AddressError(this.context); + removeError.removeErrorPanel(); + } + this.checkManualInputsValues(true); + } + + this.manualMode = manual; } - if (manual) { - this.input.value = ''; + setAddress(addressLines) { + this.clearManualInputs(false); + if (addressLines.addressLine3) { + this.line1.value = addressLines.addressLine1 + ', ' + addressLines.addressLine2; + this.line2.value = addressLines.addressLine3; + } else { + this.line1.value = addressLines.addressLine1; + this.line2.value = addressLines.addressLine2; + } + + this.town.value = addressLines.townName; + this.postcode.value = addressLines.postcode; + this.uprn.value = addressLines.uprn; + + this.setManualMode(true, false); + } - this.JsErrorPanel = document.querySelector(`.${classJsErrorPanel}`); - if (this.JsErrorPanel) { - const removeError = new AddressError(this.context); - removeError.removeErrorPanel(); - } - this.checkManualInputsValues(true); + onUnsetAddress() { + this.clearManualInputs(); } - this.manualMode = manual; - } - - setAddress(addressLines) { - this.clearManualInputs(false); - if (addressLines.addressLine3) { - this.line1.value = addressLines.addressLine1 + ', ' + addressLines.addressLine2; - this.line2.value = addressLines.addressLine3; - } else { - this.line1.value = addressLines.addressLine1; - this.line2.value = addressLines.addressLine2; + clearManualInputs() { + this.manualInputs.forEach((input) => { + if (input) { + input.value = ''; + } + }); } - this.town.value = addressLines.townName; - this.postcode.value = addressLines.postcode; - this.uprn.value = addressLines.uprn; - - this.setManualMode(true, false); - } - - onUnsetAddress() { - this.clearManualInputs(); - } - - clearManualInputs() { - this.manualInputs.forEach((input) => { - if (input) { - input.value = ''; - } - }); - } - - checkManualInputsValues(onLoad) { - if (onLoad) { - this.originalValues = this.manualInputs.map((input) => { - if (input) { - return input.value; + checkManualInputsValues(onLoad) { + if (onLoad) { + this.originalValues = this.manualInputs.map((input) => { + if (input) { + return input.value; + } + }); + } else if (this.uprn.value !== '' && this.originalValues.length) { + this.newValues = this.manualInputs.map((input) => { + return input.value; + }); + if (this.originalValues.toString() !== this.newValues.toString()) { + this.uprn.value = ''; + } } - }); - } else if (this.uprn.value !== '' && this.originalValues.length) { - this.newValues = this.manualInputs.map((input) => { - return input.value; - }); - if (this.originalValues.toString() !== this.newValues.toString()) { - this.uprn.value = ''; - } } - } } diff --git a/src/components/address-input/autosuggest.address.spec.js b/src/components/address-input/autosuggest.address.spec.js index e0d461e98d..065f3f273f 100644 --- a/src/components/address-input/autosuggest.address.spec.js +++ b/src/components/address-input/autosuggest.address.spec.js @@ -2,729 +2,746 @@ import { PuppeteerEndpointFaker } from '../../tests/helpers/puppeteer'; import { renderComponent, setTestPage } from '../../tests/helpers/rendering'; const EXAMPLE_ADDRESS_INPUT = { - id: 'address', - autocomplete: 'off', - label: { - text: 'Enter address or postcode and select from results', - }, - legend: 'What is the address?', - isEditable: true, - mandatory: true, - dontWrap: true, - instructions: 'Use up and down keys to navigate.', - ariaYouHaveSelected: 'You have selected', - ariaMinChars: 'Enter 3 or more characters for suggestions.', - minChars: 3, - ariaResultsLabel: 'Country suggestions', - ariaOneResult: 'There is one suggestion available.', - ariaNResults: 'There are {n} suggestions available.', - ariaLimitedResults: 'Type more characters to improve your search', - ariaGroupedResults: 'There are {n} for {x}', - groupCount: '{n} addresses', - moreResults: 'Continue entering to improve suggestions', - resultsTitle: 'Suggestions', - resultsTitleId: 'country-of-birth-suggestions', - noResults: 'No suggestions found.', - tooManyResults: '{n} results found. Enter more of the address to improve results', - typeMore: 'Continue entering to get suggestions', - errorTitle: 'There is a problem with your answer', - errorMessageEnter: 'Enter an address', - errorMessageSelect: 'Select an address', - errorMessageAPI: 'Sorry, there is a problem loading addresses', - errorMessageAPILinkText: 'Enter address manually', - options: { - regionCode: 'gb-eng', - addressType: 'residential', - }, - organisation: { - label: 'Organisation', - }, - line1: { - label: 'Address line 1', - }, - line2: { - label: 'Address line 2', - }, - town: { - label: 'Town or city', - }, - postcode: { - label: 'Postcode', - }, - searchButton: 'Search for an address', - manualLinkText: 'Manually enter address', + id: 'address', + autocomplete: 'off', + label: { + text: 'Enter address or postcode and select from results', + }, + legend: 'What is the address?', + isEditable: true, + mandatory: true, + dontWrap: true, + instructions: 'Use up and down keys to navigate.', + ariaYouHaveSelected: 'You have selected', + ariaMinChars: 'Enter 3 or more characters for suggestions.', + minChars: 3, + ariaResultsLabel: 'Country suggestions', + ariaOneResult: 'There is one suggestion available.', + ariaNResults: 'There are {n} suggestions available.', + ariaLimitedResults: 'Type more characters to improve your search', + ariaGroupedResults: 'There are {n} for {x}', + groupCount: '{n} addresses', + moreResults: 'Continue entering to improve suggestions', + resultsTitle: 'Suggestions', + resultsTitleId: 'country-of-birth-suggestions', + noResults: 'No suggestions found.', + tooManyResults: '{n} results found. Enter more of the address to improve results', + typeMore: 'Continue entering to get suggestions', + errorTitle: 'There is a problem with your answer', + errorMessageEnter: 'Enter an address', + errorMessageSelect: 'Select an address', + errorMessageAPI: 'Sorry, there is a problem loading addresses', + errorMessageAPILinkText: 'Enter address manually', + options: { + regionCode: 'gb-eng', + addressType: 'residential', + }, + organisation: { + label: 'Organisation', + }, + line1: { + label: 'Address line 1', + }, + line2: { + label: 'Address line 2', + }, + town: { + label: 'Town or city', + }, + postcode: { + label: 'Postcode', + }, + searchButton: 'Search for an address', + manualLinkText: 'Manually enter address', }; const EXAMPLE_ADDRESS_INPUT_WITH_API = { - ...EXAMPLE_ADDRESS_INPUT, - APIDomain: '/fake/api', - APIDomainBearerToken: 'someToken', - externalInitialiser: true, + ...EXAMPLE_ADDRESS_INPUT, + APIDomain: '/fake/api', + APIDomainBearerToken: 'someToken', + externalInitialiser: true, }; describe('script: address-input', () => { - const apiFaker = new PuppeteerEndpointFaker(EXAMPLE_ADDRESS_INPUT_WITH_API.APIDomain); - - apiFaker.setOverrides( - [ - '/addresses/eq?input=196%20colle&limit=10', - '/addresses/eq?input=cf142&limit=10', - '/addresses/eq?input=cf14%202nt&limit=100&groupfullpostcodes=combo', - ], - { - data: { - status: { code: 200 }, - response: { - addresses: [ - { - uprn: '100070332099', - formattedAddress: '196 College Road, Birmingham, B44 8HF', - addressType: 'PAF', - }, - { - uprn: '100100119969', - formattedAddress: '196 College Road, Whitchurch, Cardiff, CF14 2NZ', - addressType: 'PAF', - }, - ], - }, - }, - }, - ); - - apiFaker.setOverrides(['/addresses/eq?input=cf14%202&limit=10'], { - data: { - status: { code: 200 }, - response: { - partpostcode: 'cf14 2', - postcodes: [ - { - postcode: 'CF14 2AA', - streetName: 'Penlline Road', - townName: 'Whitchurch', - addressCount: 41, - firstUprn: 10002526869, - postTown: 'Cardiff', - }, - { - postcode: 'CF14 2AB', - streetName: 'Penlline Road', - townName: 'Whitchurch', - addressCount: 1, - firstUprn: 10002511038, - postTown: 'Cardiff', - }, + const apiFaker = new PuppeteerEndpointFaker(EXAMPLE_ADDRESS_INPUT_WITH_API.APIDomain); + + apiFaker.setOverrides( + [ + '/addresses/eq?input=196%20colle&limit=10', + '/addresses/eq?input=cf142&limit=10', + '/addresses/eq?input=cf14%202nt&limit=100&groupfullpostcodes=combo', ], - }, - }, - }); - - apiFaker.setOverrides(['/addresses/eq/uprn/100070332099?addresstype=paf', '/addresses/eq/uprn/100070332099?addresstype=paf&epoch=75'], { - data: { - status: { code: 200 }, - response: { - address: { - uprn: '100070332099', - formattedAddress: '196 College Road, Whitchurch, Cardiff, CF14 2NT', - addressLine1: '196 College Road', - addressLine2: 'Whitchurch', - addressLine3: '', - townName: 'Cardiff', - postcode: 'CF14 2NT', - foundAddressType: 'PAF', - }, - }, - }, - }); - - apiFaker.setOverrides(['/addresses/eq/uprn/10002511038?addresstype=paf'], { - data: { - status: { code: 200 }, - response: { - address: { - uprn: '10002511038', - formattedAddress: '197 College Road, Whitchurch, Cardiff, CF14 2AB', - addressLine1: '197 College Road', - addressLine2: 'Whitchurch', - addressLine3: '', - townName: 'Cardiff', - postcode: 'CF14 2AB', - foundAddressType: 'PAF', - }, - }, - }, - }); - - apiFaker.setOverrides( - ['/addresses/eq/bucket?postcode=CF14%202AA&streetname=Penlline%20Road&townname=Whitchurch&groupfullpostcodes=combo'], - { - data: { - status: { code: 200 }, - response: { - addresses: [ - { - uprn: '10002511038', - formattedAddress: '197 College Road, Whitchurch, Cardiff, CF14 2AB', - addressType: 'PAF', + { + data: { + status: { code: 200 }, + response: { + addresses: [ + { + uprn: '100070332099', + formattedAddress: '196 College Road, Birmingham, B44 8HF', + addressType: 'PAF', + }, + { + uprn: '100100119969', + formattedAddress: '196 College Road, Whitchurch, Cardiff, CF14 2NZ', + addressType: 'PAF', + }, + ], + }, }, - ], }, - }, - }, - ); - - beforeAll(async () => { - await apiFaker.setup(page); - }); + ); - beforeEach(async () => { - await apiFaker.reset(); - }); - - describe('When the component initializes', () => { - it('checks api status by trying a request', async () => { - await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); - await page.waitForTimeout(50); - - expect(apiFaker.getRequestCount('/addresses/eq?input=cf142&limit=10')).toBe(1); + apiFaker.setOverrides(['/addresses/eq?input=cf14%202&limit=10'], { + data: { + status: { code: 200 }, + response: { + partpostcode: 'cf14 2', + postcodes: [ + { + postcode: 'CF14 2AA', + streetName: 'Penlline Road', + townName: 'Whitchurch', + addressCount: 41, + firstUprn: 10002526869, + postTown: 'Cardiff', + }, + { + postcode: 'CF14 2AB', + streetName: 'Penlline Road', + townName: 'Whitchurch', + addressCount: 1, + firstUprn: 10002511038, + postTown: 'Cardiff', + }, + ], + }, + }, }); - describe('when api status is okay', () => { - beforeEach(async () => { - await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); - await page.waitForTimeout(50); - }); - - it('does not switch to manual input', async () => { - const isManualElementHidden = await page.$eval('.ons-js-address-input__manual', (node) => - node.classList.contains('ons-u-db-no-js_enabled'), - ); - expect(isManualElementHidden).toBe(true); - const isSearchElementHidden = await page.$eval('.ons-js-address-input__search', (node) => node.classList.contains('ons-u-d-no')); - expect(isSearchElementHidden).toBe(false); - }); + apiFaker.setOverrides(['/addresses/eq/uprn/100070332099?addresstype=paf', '/addresses/eq/uprn/100070332099?addresstype=paf&epoch=75'], { + data: { + status: { code: 200 }, + response: { + address: { + uprn: '100070332099', + formattedAddress: '196 College Road, Whitchurch, Cardiff, CF14 2NT', + addressLine1: '196 College Road', + addressLine2: 'Whitchurch', + addressLine3: '', + townName: 'Cardiff', + postcode: 'CF14 2NT', + foundAddressType: 'PAF', + }, + }, + }, }); - describe('when api status is not okay', () => { - beforeEach(async () => { - apiFaker.setTemporaryOverride('/addresses/eq?input=cf142&limit=10', { - data: { - status: { code: 401 }, - }, - }); - - await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); - await page.waitForTimeout(50); - }); - - it('switches to manual input', async () => { - const isManualElementHidden = await page.$eval('.ons-js-address-input__manual', (node) => - node.classList.contains('ons-u-db-no-js_enabled'), - ); - expect(isManualElementHidden).toBe(false); - const isSearchElementHidden = await page.$eval('.ons-js-address-input__search', (node) => node.classList.contains('ons-u-d-no')); - expect(isSearchElementHidden).toBe(true); - }); - - it('hides the search button', async () => { - const hassClass = await page.$eval('.ons-js-address-search-btn', (node) => node.classList.contains('ons-u-d-no')); - expect(hassClass).toBe(true); - }); + apiFaker.setOverrides(['/addresses/eq/uprn/10002511038?addresstype=paf'], { + data: { + status: { code: 200 }, + response: { + address: { + uprn: '10002511038', + formattedAddress: '197 College Road, Whitchurch, Cardiff, CF14 2AB', + addressLine1: '197 College Road', + addressLine2: 'Whitchurch', + addressLine3: '', + townName: 'Cardiff', + postcode: 'CF14 2AB', + foundAddressType: 'PAF', + }, + }, + }, }); - }); - - describe('When the user inputs', () => { - it('navigates to the first suggestion with the "Down" arrow key', async () => { - await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); - await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = 'CF14')); - await page.type('.ons-js-autosuggest-input', '2', { delay: 20 }); - await page.keyboard.press('ArrowDown'); + apiFaker.setOverrides( + ['/addresses/eq/bucket?postcode=CF14%202AA&streetname=Penlline%20Road&townname=Whitchurch&groupfullpostcodes=combo'], + { + data: { + status: { code: 200 }, + response: { + addresses: [ + { + uprn: '10002511038', + formattedAddress: '197 College Road, Whitchurch, Cardiff, CF14 2AB', + addressType: 'PAF', + }, + ], + }, + }, + }, + ); - const selectedOption = await page.$eval('.ons-autosuggest__option--focused', (node) => node.textContent); - expect(selectedOption.trim()).toBe('196 College Road, Birmingham, B44 8HF'); + beforeAll(async () => { + await apiFaker.setup(page); }); - it('provides expected parameters to the address API', async () => { - await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); - - await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = '196 coll')); - await page.type('.ons-js-autosuggest-input', 'e'); - - expect(apiFaker.getRequestCount('/addresses/eq?input=196%20colle&limit=10')).toBe(1); + beforeEach(async () => { + await apiFaker.reset(); }); - describe('when the value is a full postcode', () => { - beforeEach(async () => { - await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); - - await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = 'CF14 2N')); - await page.type('.ons-js-autosuggest-input', 'T'); - }); + describe('When the component initializes', () => { + it('checks api status by trying a request', async () => { + await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); + await page.waitForTimeout(50); - it('provides expected parameters to the address API where `limit` is 100', async () => { - expect(apiFaker.getRequestCount('/addresses/eq?input=cf14%202nt&limit=100&groupfullpostcodes=combo')).toBe(1); - }); - - it('has expected suggestion entries', async () => { - const suggestions = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.map((node) => node.textContent.trim())); - expect(suggestions).toEqual(['196 College Road, Birmingham, B44 8HF', '196 College Road, Whitchurch, Cardiff, CF14 2NZ']); - }); - }); - - describe('when the query not a partial postcode', () => { - beforeEach(async () => { - apiFaker.setTemporaryOverride( - '/addresses/eq?input=penlline%20road%20whitchurch%20cardiff%20cf14%202nz&limit=100&groupfullpostcodes=combo', - { - data: { - status: { code: 200 }, - response: { - addresses: [ - { - uprn: '100070332099', - formattedAddress: '1 Penlline Road, Whitchurch, Cardiff, CF14 2NZ', - addressType: 'PAF', - }, - { - uprn: '100100119979', - formattedAddress: '2 Penlline Road, Whitchurch, Cardiff, CF14 2NZ', - addressType: 'PAF', - }, - ], - }, - }, - }, - ); - - await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); - - await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = 'Penlline Road, Whitchurch, Cardiff, CF14 2N')); - await page.type('.ons-js-autosuggest-input', 'Z'); - await page.waitForTimeout(100); - }); - - it('provides expected parameters to the address API', async () => { - expect( - apiFaker.getRequestCount( - '/addresses/eq?input=penlline%20road%20whitchurch%20cardiff%20cf14%202nz&limit=100&groupfullpostcodes=combo', - ), - ).toBe(1); - }); - - it('has expected suggestion entries', async () => { - const suggestions = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.map((node) => node.textContent.trim())); - expect(suggestions).toEqual(['1 Penlline Road, Whitchurch, Cardiff, CF14 2NZ', '2 Penlline Road, Whitchurch, Cardiff, CF14 2NZ']); - }); - - describe('when a suggestion is selected', () => { - beforeEach(async () => { - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('Enter'); - await page.waitForTimeout(100); + expect(apiFaker.getRequestCount('/addresses/eq?input=cf142&limit=10')).toBe(1); }); - it('makes expected request when a suggestion is selected', async () => { - expect(apiFaker.getRequestCount('/addresses/eq/uprn/100070332099?addresstype=paf')).toBe(1); + describe('when api status is okay', () => { + beforeEach(async () => { + await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); + await page.waitForTimeout(50); + }); + + it('does not switch to manual input', async () => { + const isManualElementHidden = await page.$eval('.ons-js-address-input__manual', (node) => + node.classList.contains('ons-u-db-no-js_enabled'), + ); + expect(isManualElementHidden).toBe(true); + const isSearchElementHidden = await page.$eval('.ons-js-address-input__search', (node) => + node.classList.contains('ons-u-d-no'), + ); + expect(isSearchElementHidden).toBe(false); + }); }); - it('populates manual input fields with address from selection', async () => { - expect(await page.$eval('.ons-js-address-organisation', (node) => node.value)).toBe(''); - expect(await page.$eval('.ons-js-address-line1', (node) => node.value)).toBe('196 College Road'); - expect(await page.$eval('.ons-js-address-line2', (node) => node.value)).toBe('Whitchurch'); - expect(await page.$eval('.ons-js-address-town', (node) => node.value)).toBe('Cardiff'); - expect(await page.$eval('.ons-js-address-postcode', (node) => node.value)).toBe('CF14 2NT'); - expect(await page.$eval('.ons-js-hidden-uprn', (node) => node.value)).toBe('100070332099'); + describe('when api status is not okay', () => { + beforeEach(async () => { + apiFaker.setTemporaryOverride('/addresses/eq?input=cf142&limit=10', { + data: { + status: { code: 401 }, + }, + }); + + await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); + await page.waitForTimeout(50); + }); + + it('switches to manual input', async () => { + const isManualElementHidden = await page.$eval('.ons-js-address-input__manual', (node) => + node.classList.contains('ons-u-db-no-js_enabled'), + ); + expect(isManualElementHidden).toBe(false); + const isSearchElementHidden = await page.$eval('.ons-js-address-input__search', (node) => + node.classList.contains('ons-u-d-no'), + ); + expect(isSearchElementHidden).toBe(true); + }); + + it('hides the search button', async () => { + const hassClass = await page.$eval('.ons-js-address-search-btn', (node) => node.classList.contains('ons-u-d-no')); + expect(hassClass).toBe(true); + }); }); - }); }); - describe('when the query is a partial postcode', () => { - beforeEach(async () => { - await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); + describe('When the user inputs', () => { + it('navigates to the first suggestion with the "Down" arrow key', async () => { + await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); - await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = 'CF14 ')); - await page.type('.ons-js-autosuggest-input', '2'); - await page.waitForTimeout(200); - }); - - it('provides expected parameters to the address API', async () => { - expect(apiFaker.getRequestCount('/addresses/eq?input=cf14%202&limit=10')).toBe(1); - }); - - it('has expected suggestion entries', async () => { - const suggestions = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.map((node) => node.textContent.trim())); - expect(suggestions).toEqual([ - 'Penlline Road, Whitchurch, Cardiff, CF14 2AA (41 addresses)', - '197 College Road, Whitchurch, Cardiff, CF14 2AB', - ]); - }); - - describe('when a suggestion is selected', () => { - beforeEach(async () => { - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('Enter'); - await page.waitForTimeout(200); - }); + await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = 'CF14')); + await page.type('.ons-js-autosuggest-input', '2', { delay: 20 }); + await page.keyboard.press('ArrowDown'); - it('makes expected request', async () => { - expect( - apiFaker.getRequestCount( - '/addresses/eq/bucket?postcode=CF14%202AA&streetname=Penlline%20Road&townname=Whitchurch&groupfullpostcodes=combo', - ), - ).toBe(1); + const selectedOption = await page.$eval('.ons-autosuggest__option--focused', (node) => node.textContent); + expect(selectedOption.trim()).toBe('196 College Road, Birmingham, B44 8HF'); }); - it('has expected suggestion entries', async () => { - const suggestions = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.map((node) => node.textContent.trim())); - expect(suggestions).toEqual(['197 College Road, Whitchurch, Cardiff, CF14 2AB']); - }); + it('provides expected parameters to the address API', async () => { + await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); - describe('when an inner suggestion is selected', () => { - beforeEach(async () => { - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('Enter'); - await page.waitForTimeout(200); - }); - - it('populates manual input fields with address from selection', async () => { - expect(await page.$eval('.ons-js-address-organisation', (node) => node.value)).toBe(''); - expect(await page.$eval('.ons-js-address-line1', (node) => node.value)).toBe('197 College Road'); - expect(await page.$eval('.ons-js-address-line2', (node) => node.value)).toBe('Whitchurch'); - expect(await page.$eval('.ons-js-address-town', (node) => node.value)).toBe('Cardiff'); - expect(await page.$eval('.ons-js-address-postcode', (node) => node.value)).toBe('CF14 2AB'); - expect(await page.$eval('.ons-js-hidden-uprn', (node) => node.value)).toBe('10002511038'); - }); - }); - }); - }); - - describe('when there is an error retrieving the address', () => { - it('switches to manual mode and hides search button', async () => { - apiFaker.setTemporaryOverride('/addresses/eq?input=cf142&limit=10', { - data: { - status: { code: 200 }, - response: { - addresses: [ - { - uprn: 'bad', - formattedAddress: '196 College Road, Birmingham, B44 8HF', - addressType: 'PAF', - }, - ], - }, - }, - }); + await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = '196 coll')); + await page.type('.ons-js-autosuggest-input', 'e'); - apiFaker.setTemporaryOverride('/addresses/eq/uprn/bad?addresstype=paf', { - data: { - status: { code: 400 }, - }, + expect(apiFaker.getRequestCount('/addresses/eq?input=196%20colle&limit=10')).toBe(1); }); - await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); + describe('when the value is a full postcode', () => { + beforeEach(async () => { + await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); - await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = 'cf14')); - await page.type('.ons-js-autosuggest-input', '2', { delay: 20 }); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('Enter'); - await page.waitForTimeout(100); + await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = 'CF14 2N')); + await page.type('.ons-js-autosuggest-input', 'T'); + }); - const isManualElementHidden = await page.$eval('.ons-js-address-input__manual', (node) => - node.classList.contains('ons-u-db-no-js_enabled'), - ); - expect(isManualElementHidden).toBe(false); + it('provides expected parameters to the address API where `limit` is 100', async () => { + expect(apiFaker.getRequestCount('/addresses/eq?input=cf14%202nt&limit=100&groupfullpostcodes=combo')).toBe(1); + }); - const isSearchElementHidden = await page.$eval('.ons-js-address-input__search', (node) => node.classList.contains('ons-u-d-no')); - expect(isSearchElementHidden).toBe(true); - - const isSearchButtonElementHidden = await page.$eval('.ons-js-address-search-btn', (node) => node.classList.contains('ons-u-d-no')); - expect(isSearchButtonElementHidden).toBe(true); - }); - }); + it('has expected suggestion entries', async () => { + const suggestions = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.map((node) => node.textContent.trim())); + expect(suggestions).toEqual(['196 College Road, Birmingham, B44 8HF', '196 College Road, Whitchurch, Cardiff, CF14 2NZ']); + }); + }); - describe('when the form is submitted', () => { - describe('when the selected address is manually changed', () => { - it('clears the urpn field', async () => { - await setTestPage( - '/test', - ` -
- ${renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)} - -
- `, - ); - - await page.$eval('form', (node) => - node.addEventListener('submit', (event) => { - event.preventDefault(); - return false; - }), - ); - - await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = 'CF14 2N')); - await page.type('.ons-js-autosuggest-input', 'T', { delay: 20 }); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('Enter'); - await page.waitForTimeout(100); - - const urpnValueBefore = await page.$eval('.ons-js-hidden-uprn', (node) => node.value); - expect(urpnValueBefore).toBe('100070332099'); - - await page.$eval('.ons-js-address-line1', (node) => (node.value = 'Something else')); - await page.click('button[type=submit]'); - - const urpnValueAfter = await page.$eval('.ons-js-hidden-uprn', (node) => node.value); - expect(urpnValueAfter).toBe(''); + describe('when the query not a partial postcode', () => { + beforeEach(async () => { + apiFaker.setTemporaryOverride( + '/addresses/eq?input=penlline%20road%20whitchurch%20cardiff%20cf14%202nz&limit=100&groupfullpostcodes=combo', + { + data: { + status: { code: 200 }, + response: { + addresses: [ + { + uprn: '100070332099', + formattedAddress: '1 Penlline Road, Whitchurch, Cardiff, CF14 2NZ', + addressType: 'PAF', + }, + { + uprn: '100100119979', + formattedAddress: '2 Penlline Road, Whitchurch, Cardiff, CF14 2NZ', + addressType: 'PAF', + }, + ], + }, + }, + }, + ); + + await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); + + await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = 'Penlline Road, Whitchurch, Cardiff, CF14 2N')); + await page.type('.ons-js-autosuggest-input', 'Z'); + await page.waitForTimeout(100); + }); + + it('provides expected parameters to the address API', async () => { + expect( + apiFaker.getRequestCount( + '/addresses/eq?input=penlline%20road%20whitchurch%20cardiff%20cf14%202nz&limit=100&groupfullpostcodes=combo', + ), + ).toBe(1); + }); + + it('has expected suggestion entries', async () => { + const suggestions = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.map((node) => node.textContent.trim())); + expect(suggestions).toEqual([ + '1 Penlline Road, Whitchurch, Cardiff, CF14 2NZ', + '2 Penlline Road, Whitchurch, Cardiff, CF14 2NZ', + ]); + }); + + describe('when a suggestion is selected', () => { + beforeEach(async () => { + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + await page.waitForTimeout(100); + }); + + it('makes expected request when a suggestion is selected', async () => { + expect(apiFaker.getRequestCount('/addresses/eq/uprn/100070332099?addresstype=paf')).toBe(1); + }); + + it('populates manual input fields with address from selection', async () => { + expect(await page.$eval('.ons-js-address-organisation', (node) => node.value)).toBe(''); + expect(await page.$eval('.ons-js-address-line1', (node) => node.value)).toBe('196 College Road'); + expect(await page.$eval('.ons-js-address-line2', (node) => node.value)).toBe('Whitchurch'); + expect(await page.$eval('.ons-js-address-town', (node) => node.value)).toBe('Cardiff'); + expect(await page.$eval('.ons-js-address-postcode', (node) => node.value)).toBe('CF14 2NT'); + expect(await page.$eval('.ons-js-hidden-uprn', (node) => node.value)).toBe('100070332099'); + }); + }); }); - }); - describe('when the submit is invalid', () => { - beforeEach(async () => { - await setTestPage( - '/test', - ` -
-
- ${renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)} - -
-
- `, - ); - - await page.$eval('form', (node) => - node.addEventListener('submit', (event) => { - event.preventDefault(); - return false; - }), - ); - - await page.click('button[type=submit]'); + describe('when the query is a partial postcode', () => { + beforeEach(async () => { + await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); + + await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = 'CF14 ')); + await page.type('.ons-js-autosuggest-input', '2'); + await page.waitForTimeout(200); + }); + + it('provides expected parameters to the address API', async () => { + expect(apiFaker.getRequestCount('/addresses/eq?input=cf14%202&limit=10')).toBe(1); + }); + + it('has expected suggestion entries', async () => { + const suggestions = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.map((node) => node.textContent.trim())); + expect(suggestions).toEqual([ + 'Penlline Road, Whitchurch, Cardiff, CF14 2AA (41 addresses)', + '197 College Road, Whitchurch, Cardiff, CF14 2AB', + ]); + }); + + describe('when a suggestion is selected', () => { + beforeEach(async () => { + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + await page.waitForTimeout(200); + }); + + it('makes expected request', async () => { + expect( + apiFaker.getRequestCount( + '/addresses/eq/bucket?postcode=CF14%202AA&streetname=Penlline%20Road&townname=Whitchurch&groupfullpostcodes=combo', + ), + ).toBe(1); + }); + + it('has expected suggestion entries', async () => { + const suggestions = await page.$$eval('.ons-autosuggest__option', (nodes) => + nodes.map((node) => node.textContent.trim()), + ); + expect(suggestions).toEqual(['197 College Road, Whitchurch, Cardiff, CF14 2AB']); + }); + + describe('when an inner suggestion is selected', () => { + beforeEach(async () => { + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + await page.waitForTimeout(200); + }); + + it('populates manual input fields with address from selection', async () => { + expect(await page.$eval('.ons-js-address-organisation', (node) => node.value)).toBe(''); + expect(await page.$eval('.ons-js-address-line1', (node) => node.value)).toBe('197 College Road'); + expect(await page.$eval('.ons-js-address-line2', (node) => node.value)).toBe('Whitchurch'); + expect(await page.$eval('.ons-js-address-town', (node) => node.value)).toBe('Cardiff'); + expect(await page.$eval('.ons-js-address-postcode', (node) => node.value)).toBe('CF14 2AB'); + expect(await page.$eval('.ons-js-hidden-uprn', (node) => node.value)).toBe('10002511038'); + }); + }); + }); }); - it('then an error summary panel should be added to the DOM', async () => { - const panelExists = await page.$$eval('.ons-js-autosuggest-error-panel', (nodes) => nodes.length === 1); - expect(panelExists).toBe(true); + describe('when there is an error retrieving the address', () => { + it('switches to manual mode and hides search button', async () => { + apiFaker.setTemporaryOverride('/addresses/eq?input=cf142&limit=10', { + data: { + status: { code: 200 }, + response: { + addresses: [ + { + uprn: 'bad', + formattedAddress: '196 College Road, Birmingham, B44 8HF', + addressType: 'PAF', + }, + ], + }, + }, + }); + + apiFaker.setTemporaryOverride('/addresses/eq/uprn/bad?addresstype=paf', { + data: { + status: { code: 400 }, + }, + }); + + await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); + + await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = 'cf14')); + await page.type('.ons-js-autosuggest-input', '2', { delay: 20 }); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + await page.waitForTimeout(100); + + const isManualElementHidden = await page.$eval('.ons-js-address-input__manual', (node) => + node.classList.contains('ons-u-db-no-js_enabled'), + ); + expect(isManualElementHidden).toBe(false); + + const isSearchElementHidden = await page.$eval('.ons-js-address-input__search', (node) => + node.classList.contains('ons-u-d-no'), + ); + expect(isSearchElementHidden).toBe(true); + + const isSearchButtonElementHidden = await page.$eval('.ons-js-address-search-btn', (node) => + node.classList.contains('ons-u-d-no'), + ); + expect(isSearchButtonElementHidden).toBe(true); + }); }); - it('then input should be wrapped in an error', async () => { - const inputIsError = await page.$$eval('#autosuggest-input-error', (nodes) => nodes.length === 1); - expect(inputIsError).toBe(true); + describe('when the form is submitted', () => { + describe('when the selected address is manually changed', () => { + it('clears the urpn field', async () => { + await setTestPage( + '/test', + ` +
+ ${renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)} + +
+ `, + ); + + await page.$eval('form', (node) => + node.addEventListener('submit', (event) => { + event.preventDefault(); + return false; + }), + ); + + await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = 'CF14 2N')); + await page.type('.ons-js-autosuggest-input', 'T', { delay: 20 }); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + await page.waitForTimeout(100); + + const urpnValueBefore = await page.$eval('.ons-js-hidden-uprn', (node) => node.value); + expect(urpnValueBefore).toBe('100070332099'); + + await page.$eval('.ons-js-address-line1', (node) => (node.value = 'Something else')); + await page.click('button[type=submit]'); + + const urpnValueAfter = await page.$eval('.ons-js-hidden-uprn', (node) => node.value); + expect(urpnValueAfter).toBe(''); + }); + }); + + describe('when the submit is invalid', () => { + beforeEach(async () => { + await setTestPage( + '/test', + ` +
+
+ ${renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)} + +
+
+ `, + ); + + await page.$eval('form', (node) => + node.addEventListener('submit', (event) => { + event.preventDefault(); + return false; + }), + ); + + await page.click('button[type=submit]'); + }); + + it('then an error summary panel should be added to the DOM', async () => { + const panelExists = await page.$$eval('.ons-js-autosuggest-error-panel', (nodes) => nodes.length === 1); + expect(panelExists).toBe(true); + }); + + it('then input should be wrapped in an error', async () => { + const inputIsError = await page.$$eval('#autosuggest-input-error', (nodes) => nodes.length === 1); + expect(inputIsError).toBe(true); + }); + + describe('when the mode is set to manual', () => { + it('then the error summary should be removed', async () => { + await page.click('.ons-js-address-manual-btn'); + + const panelExists = await page.$$eval('.ons-js-autosuggest-error-panel', (nodes) => nodes.length === 1); + expect(panelExists).toBe(false); + }); + }); + }); }); + }); - describe('when the mode is set to manual', () => { - it('then the error summary should be removed', async () => { + describe('When the manual link is clicked', () => { + beforeEach(async () => { + await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); await page.click('.ons-js-address-manual-btn'); + }); - const panelExists = await page.$$eval('.ons-js-autosuggest-error-panel', (nodes) => nodes.length === 1); - expect(panelExists).toBe(false); - }); + it('shows manual input fields', async () => { + const isManualElementHidden = await page.$eval('.ons-js-address-input__manual', (node) => + node.classList.contains('ons-u-db-no-js_enabled'), + ); + expect(isManualElementHidden).toBe(false); + const isSearchElementHidden = await page.$eval('.ons-js-address-input__search', (node) => + node.classList.contains('ons-u-d-no'), + ); + expect(isSearchElementHidden).toBe(true); }); - }); - }); - }); - describe('When the manual link is clicked', () => { - beforeEach(async () => { - await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); - await page.click('.ons-js-address-manual-btn'); - }); + it('clears autosuggest input', async () => { + const value = await page.$eval('.ons-js-autosuggest-input', (node) => node.value); + expect(value).toBe(''); + }); - it('shows manual input fields', async () => { - const isManualElementHidden = await page.$eval('.ons-js-address-input__manual', (node) => - node.classList.contains('ons-u-db-no-js_enabled'), - ); - expect(isManualElementHidden).toBe(false); - const isSearchElementHidden = await page.$eval('.ons-js-address-input__search', (node) => node.classList.contains('ons-u-d-no')); - expect(isSearchElementHidden).toBe(true); + describe('and then the search link is clicked', () => { + beforeEach(async () => { + await page.$eval('.ons-js-address-organisation', (node) => (node.value = 'Test organisation')); + await page.$eval('.ons-js-address-line1', (node) => (node.value = 'Test address line 1')); + await page.$eval('.ons-js-address-line2', (node) => (node.value = 'Test address line 2')); + await page.$eval('.ons-js-address-town', (node) => (node.value = 'Test town')); + await page.$eval('.ons-js-address-postcode', (node) => (node.value = 'PO37 60DE')); + await page.$eval('.ons-js-hidden-uprn', (node) => (node.value = '100070332099')); + + await page.click('.ons-js-address-search-btn'); + }); + + it('hides manual input fields', async () => { + const isManualElementHidden = await page.$eval('.ons-js-address-input__manual', (node) => + node.classList.contains('ons-u-db-no-js_enabled'), + ); + expect(isManualElementHidden).toBe(true); + const isSearchElementHidden = await page.$eval('.ons-js-address-input__search', (node) => + node.classList.contains('ons-u-d-no'), + ); + expect(isSearchElementHidden).toBe(false); + }); + + it('clears manual input fields', async () => { + expect(await page.$eval('.ons-js-address-organisation', (node) => node.value)).toBe(''); + expect(await page.$eval('.ons-js-address-line1', (node) => node.value)).toBe(''); + expect(await page.$eval('.ons-js-address-line2', (node) => node.value)).toBe(''); + expect(await page.$eval('.ons-js-address-town', (node) => node.value)).toBe(''); + expect(await page.$eval('.ons-js-address-postcode', (node) => node.value)).toBe(''); + expect(await page.$eval('.ons-js-hidden-uprn', (node) => node.value)).toBe(''); + }); + }); }); - it('clears autosuggest input', async () => { - const value = await page.$eval('.ons-js-autosuggest-input', (node) => node.value); - expect(value).toBe(''); - }); + describe('When the language is Welsh', () => { + beforeEach(async () => { + await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); + await page.evaluate(() => document.documentElement.setAttribute('lang', 'cy')); + }); - describe('and then the search link is clicked', () => { - beforeEach(async () => { - await page.$eval('.ons-js-address-organisation', (node) => (node.value = 'Test organisation')); - await page.$eval('.ons-js-address-line1', (node) => (node.value = 'Test address line 1')); - await page.$eval('.ons-js-address-line2', (node) => (node.value = 'Test address line 2')); - await page.$eval('.ons-js-address-town', (node) => (node.value = 'Test town')); - await page.$eval('.ons-js-address-postcode', (node) => (node.value = 'PO37 60DE')); - await page.$eval('.ons-js-hidden-uprn', (node) => (node.value = '100070332099')); - - await page.click('.ons-js-address-search-btn'); - }); - - it('hides manual input fields', async () => { - const isManualElementHidden = await page.$eval('.ons-js-address-input__manual', (node) => - node.classList.contains('ons-u-db-no-js_enabled'), - ); - expect(isManualElementHidden).toBe(true); - const isSearchElementHidden = await page.$eval('.ons-js-address-input__search', (node) => node.classList.contains('ons-u-d-no')); - expect(isSearchElementHidden).toBe(false); - }); - - it('clears manual input fields', async () => { - expect(await page.$eval('.ons-js-address-organisation', (node) => node.value)).toBe(''); - expect(await page.$eval('.ons-js-address-line1', (node) => node.value)).toBe(''); - expect(await page.$eval('.ons-js-address-line2', (node) => node.value)).toBe(''); - expect(await page.$eval('.ons-js-address-town', (node) => node.value)).toBe(''); - expect(await page.$eval('.ons-js-address-postcode', (node) => node.value)).toBe(''); - expect(await page.$eval('.ons-js-hidden-uprn', (node) => node.value)).toBe(''); - }); - }); - }); + it('then the fetch url should contain the favour Welsh parameter', async () => { + await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = '196 coll')); + await page.type('.ons-js-autosuggest-input', 'e'); - describe('When the language is Welsh', () => { - beforeEach(async () => { - await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); - await page.evaluate(() => document.documentElement.setAttribute('lang', 'cy')); + expect(apiFaker.getRequestCount('/addresses/eq?input=196%20colle&limit=10&favourwelsh=true')).toBe(1); + }); }); - it('then the fetch url should contain the favour Welsh parameter', async () => { - await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = '196 coll')); - await page.type('.ons-js-autosuggest-input', 'e'); - - expect(apiFaker.getRequestCount('/addresses/eq?input=196%20colle&limit=10&favourwelsh=true')).toBe(1); - }); - }); + describe('When the component initialises a non-editable address lookup', () => { + describe('when a query is sent and address selected', () => { + beforeEach(async () => { + await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); - describe('When the component initialises a non-editable address lookup', () => { - describe('when a query is sent and address selected', () => { - beforeEach(async () => { - await setTestPage('/test', renderComponent('address-input', EXAMPLE_ADDRESS_INPUT_WITH_API)); + await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = 'CF14')); + await page.type('.ons-js-autosuggest-input', '2', { delay: 20 }); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); - await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = 'CF14')); - await page.type('.ons-js-autosuggest-input', '2', { delay: 20 }); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('Enter'); + await page.waitForTimeout(50); + }); - await page.waitForTimeout(50); - }); + it('then the retrieveAddress function will be called', async () => { + expect(apiFaker.getRequestCount('/addresses/eq/uprn/100070332099?addresstype=paf')).toBe(1); + }); + }); - it('then the retrieveAddress function will be called', async () => { - expect(apiFaker.getRequestCount('/addresses/eq/uprn/100070332099?addresstype=paf')).toBe(1); - }); + describe('when the form is submitted', () => { + describe('when the input is empty', () => { + beforeEach(async () => { + await setTestPage( + '/test', + ` +
+
+ ${renderComponent('address-input', { ...EXAMPLE_ADDRESS_INPUT_WITH_API, isEditable: false })} + +
+
+ `, + ); + + await page.click('button[type=submit]'); + }); + + it('sets aria status message', async () => { + const statusMessage = await page.$eval('.ons-js-autosuggest-aria-status', (node) => node.textContent); + expect(statusMessage).toBe('Enter 3 or more characters for suggestions.'); + }); + }); + }); }); - describe('when the form is submitted', () => { - describe('when the input is empty', () => { + describe.each([ + [ + 'english, epoch, workplace', + '/addresses/eq?input=196%20colle&limit=10&classificationfilter=workplace&eboost=10&epoch=75', + '/addresses/eq/uprn/100070332099?addresstype=paf&epoch=75', + 'en', + { + regionCode: 'gb-eng', + oneYearAgo: true, + addressType: 'workplace', + }, + ], + [ + 'ni, educational', + '/addresses/eq?input=196%20colle&limit=10&classificationfilter=educational&eboost=0&sboost=0&wboost=0', + '/addresses/eq/uprn/100070332099?addresstype=paf', + 'en', + { + regionCode: 'gb-nir', + addressType: 'educational', + }, + ], + [ + 'ni, workplace', + '/addresses/eq?input=196%20colle&limit=10&classificationfilter=workplace&nboost=10', + '/addresses/eq/uprn/100070332099?addresstype=paf', + 'en', + { + regionCode: 'gb-nir', + addressType: 'workplace', + }, + ], + [ + 'wales, workspace', + '/addresses/eq?input=196%20colle&limit=10&classificationfilter=workplace&wboost=10&favourwelsh=true', + '/addresses/eq/uprn/100070332099?addresstype=paf', + 'cy', + { + regionCode: 'gb-wls', + addressType: 'workplace', + }, + ], + ])('When the component initialises with options - %s', (_, searchEndpoint, uprnEndpoint, lang, options) => { beforeEach(async () => { - await setTestPage( - '/test', - ` -
-
- ${renderComponent('address-input', { ...EXAMPLE_ADDRESS_INPUT_WITH_API, isEditable: false })} - -
-
- `, - ); - - await page.click('button[type=submit]'); - }); - - it('sets aria status message', async () => { - const statusMessage = await page.$eval('.ons-js-autosuggest-aria-status', (node) => node.textContent); - expect(statusMessage).toBe('Enter 3 or more characters for suggestions.'); + apiFaker.setTemporaryOverride(searchEndpoint, { + data: { + status: { code: 200 }, + response: { + input: '196 colle', + limit: 10, + addresses: [ + { + uprn: '100070332099', + formattedAddress: '196 College Road, Birmingham, B44 8HF', + addressType: 'PAF', + }, + ], + }, + }, + }); + + await setTestPage( + '/test', + renderComponent('address-input', { + ...EXAMPLE_ADDRESS_INPUT_WITH_API, + options, + }), + ); + + const setLangAttribute = (lang) => document.documentElement.setAttribute('lang', lang); + await page.evaluate(setLangAttribute, lang); }); - }); - }); - }); - - describe.each([ - [ - 'english, epoch, workplace', - '/addresses/eq?input=196%20colle&limit=10&classificationfilter=workplace&eboost=10&epoch=75', - '/addresses/eq/uprn/100070332099?addresstype=paf&epoch=75', - 'en', - { - regionCode: 'gb-eng', - oneYearAgo: true, - addressType: 'workplace', - }, - ], - [ - 'ni, educational', - '/addresses/eq?input=196%20colle&limit=10&classificationfilter=educational&eboost=0&sboost=0&wboost=0', - '/addresses/eq/uprn/100070332099?addresstype=paf', - 'en', - { - regionCode: 'gb-nir', - addressType: 'educational', - }, - ], - [ - 'ni, workplace', - '/addresses/eq?input=196%20colle&limit=10&classificationfilter=workplace&nboost=10', - '/addresses/eq/uprn/100070332099?addresstype=paf', - 'en', - { - regionCode: 'gb-nir', - addressType: 'workplace', - }, - ], - [ - 'wales, workspace', - '/addresses/eq?input=196%20colle&limit=10&classificationfilter=workplace&wboost=10&favourwelsh=true', - '/addresses/eq/uprn/100070332099?addresstype=paf', - 'cy', - { - regionCode: 'gb-wls', - addressType: 'workplace', - }, - ], - ])('When the component initialises with options - %s', (_, searchEndpoint, uprnEndpoint, lang, options) => { - beforeEach(async () => { - apiFaker.setTemporaryOverride(searchEndpoint, { - data: { - status: { code: 200 }, - response: { - input: '196 colle', - limit: 10, - addresses: [ - { - uprn: '100070332099', - formattedAddress: '196 College Road, Birmingham, B44 8HF', - addressType: 'PAF', - }, - ], - }, - }, - }); - - await setTestPage( - '/test', - renderComponent('address-input', { - ...EXAMPLE_ADDRESS_INPUT_WITH_API, - options, - }), - ); - - const setLangAttribute = (lang) => document.documentElement.setAttribute('lang', lang); - await page.evaluate(setLangAttribute, lang); - }); - it('provides expected parameters to the address API', async () => { - await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = '196 coll')); - await page.type('.ons-js-autosuggest-input', 'e'); + it('provides expected parameters to the address API', async () => { + await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = '196 coll')); + await page.type('.ons-js-autosuggest-input', 'e'); - expect(apiFaker.getRequestCount(searchEndpoint)).toBe(1); - }); + expect(apiFaker.getRequestCount(searchEndpoint)).toBe(1); + }); - it('requests further information for the selected address from the API with the expected parameters', async () => { - await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = '196 coll')); - await page.type('.ons-js-autosuggest-input', 'e', { delay: 20 }); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('Enter'); + it('requests further information for the selected address from the API with the expected parameters', async () => { + await page.$eval('.ons-js-autosuggest-input', (node) => (node.value = '196 coll')); + await page.type('.ons-js-autosuggest-input', 'e', { delay: 20 }); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); - await page.waitForTimeout(50); + await page.waitForTimeout(50); - expect(apiFaker.getRequestCount(uprnEndpoint)).toBe(1); + expect(apiFaker.getRequestCount(uprnEndpoint)).toBe(1); + }); }); - }); }); diff --git a/src/components/address-output/_address-output.scss b/src/components/address-output/_address-output.scss index 911659b98a..b592ef14a4 100644 --- a/src/components/address-output/_address-output.scss +++ b/src/components/address-output/_address-output.scss @@ -1,5 +1,5 @@ .ons-address-output { - &__lines { - margin-bottom: 0; - } + &__lines { + margin-bottom: 0; + } } diff --git a/src/components/address-output/_macro.spec.js b/src/components/address-output/_macro.spec.js index ba6046e3e8..d94484c316 100644 --- a/src/components/address-output/_macro.spec.js +++ b/src/components/address-output/_macro.spec.js @@ -6,93 +6,93 @@ import axe from '../../tests/helpers/axe'; import { renderComponent } from '../../tests/helpers/rendering'; const EXAMPLE_ADDRESS_OUTPUT_FULL = { - unit: 'Unit 5', - organisation: 'Trescos', - line1: 'Abingdon Road', - line2: 'Goathill', - town: 'Barry', - postcode: 'AB12 6UH', + unit: 'Unit 5', + organisation: 'Trescos', + line1: 'Abingdon Road', + line2: 'Goathill', + town: 'Barry', + postcode: 'AB12 6UH', }; const EXAMPLE_ADDRESS_OUTPUT_NONE = {}; describe('macro: address-output', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('address-output', EXAMPLE_ADDRESS_OUTPUT_FULL)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has additionally provided container style classes', () => { - const $ = cheerio.load( - renderComponent('address-output', { - ...EXAMPLE_ADDRESS_OUTPUT_FULL, - classes: 'extra-class another-extra-class', - }), - ); - - expect($('.ons-address-output').hasClass('extra-class')).toBe(true); - expect($('.ons-address-output').hasClass('another-extra-class')).toBe(true); - }); - - it('renders no lines when no parameters are provided', () => { - const $ = cheerio.load(renderComponent('address-output', EXAMPLE_ADDRESS_OUTPUT_NONE)); - - expect($('.ons-address-output__lines *').length).toBe(0); - }); - - it.each([ - ['all address lines', EXAMPLE_ADDRESS_OUTPUT_FULL], - ['single line', { unit: 'Unit 5' }], - ])('renders `unit` with %s', (_, params) => { - const $ = cheerio.load(renderComponent('address-output', params)); - - expect($('.ons-address-output__unit').text().trim()).toBe('Unit 5'); - }); - - it.each([ - ['all address lines', EXAMPLE_ADDRESS_OUTPUT_FULL], - ['single line', { organisation: 'Trescos' }], - ])('renders `organisation` with %s', (_, params) => { - const $ = cheerio.load(renderComponent('address-output', params)); - - expect($('.ons-address-output__organisation').text().trim()).toBe('Trescos'); - }); - - it.each([ - ['all address lines', EXAMPLE_ADDRESS_OUTPUT_FULL], - ['single line', { line1: 'Abingdon Road' }], - ])('renders `line1` with %s', (_, params) => { - const $ = cheerio.load(renderComponent('address-output', params)); - - expect($('.ons-address-output__line1').text().trim()).toBe('Abingdon Road'); - }); - - it.each([ - ['all address lines', EXAMPLE_ADDRESS_OUTPUT_FULL], - ['single line', { line2: 'Goathill' }], - ])('renders `line2` with %s', (_, params) => { - const $ = cheerio.load(renderComponent('address-output', params)); - - expect($('.ons-address-output__line2').text().trim()).toBe('Goathill'); - }); - - it.each([ - ['all address lines', EXAMPLE_ADDRESS_OUTPUT_FULL], - ['single line', { town: 'Barry' }], - ])('renders `town` with %s', (_, params) => { - const $ = cheerio.load(renderComponent('address-output', params)); - - expect($('.ons-address-output__town').text().trim()).toBe('Barry'); - }); - - it.each([ - ['all address lines', EXAMPLE_ADDRESS_OUTPUT_FULL], - ['single line', { postcode: 'AB12 6UH' }], - ])('renders `postcode` with %s', (_, params) => { - const $ = cheerio.load(renderComponent('address-output', params)); - - expect($('.ons-address-output__postcode').text().trim()).toBe('AB12 6UH'); - }); + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('address-output', EXAMPLE_ADDRESS_OUTPUT_FULL)); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has additionally provided container style classes', () => { + const $ = cheerio.load( + renderComponent('address-output', { + ...EXAMPLE_ADDRESS_OUTPUT_FULL, + classes: 'extra-class another-extra-class', + }), + ); + + expect($('.ons-address-output').hasClass('extra-class')).toBe(true); + expect($('.ons-address-output').hasClass('another-extra-class')).toBe(true); + }); + + it('renders no lines when no parameters are provided', () => { + const $ = cheerio.load(renderComponent('address-output', EXAMPLE_ADDRESS_OUTPUT_NONE)); + + expect($('.ons-address-output__lines *').length).toBe(0); + }); + + it.each([ + ['all address lines', EXAMPLE_ADDRESS_OUTPUT_FULL], + ['single line', { unit: 'Unit 5' }], + ])('renders `unit` with %s', (_, params) => { + const $ = cheerio.load(renderComponent('address-output', params)); + + expect($('.ons-address-output__unit').text().trim()).toBe('Unit 5'); + }); + + it.each([ + ['all address lines', EXAMPLE_ADDRESS_OUTPUT_FULL], + ['single line', { organisation: 'Trescos' }], + ])('renders `organisation` with %s', (_, params) => { + const $ = cheerio.load(renderComponent('address-output', params)); + + expect($('.ons-address-output__organisation').text().trim()).toBe('Trescos'); + }); + + it.each([ + ['all address lines', EXAMPLE_ADDRESS_OUTPUT_FULL], + ['single line', { line1: 'Abingdon Road' }], + ])('renders `line1` with %s', (_, params) => { + const $ = cheerio.load(renderComponent('address-output', params)); + + expect($('.ons-address-output__line1').text().trim()).toBe('Abingdon Road'); + }); + + it.each([ + ['all address lines', EXAMPLE_ADDRESS_OUTPUT_FULL], + ['single line', { line2: 'Goathill' }], + ])('renders `line2` with %s', (_, params) => { + const $ = cheerio.load(renderComponent('address-output', params)); + + expect($('.ons-address-output__line2').text().trim()).toBe('Goathill'); + }); + + it.each([ + ['all address lines', EXAMPLE_ADDRESS_OUTPUT_FULL], + ['single line', { town: 'Barry' }], + ])('renders `town` with %s', (_, params) => { + const $ = cheerio.load(renderComponent('address-output', params)); + + expect($('.ons-address-output__town').text().trim()).toBe('Barry'); + }); + + it.each([ + ['all address lines', EXAMPLE_ADDRESS_OUTPUT_FULL], + ['single line', { postcode: 'AB12 6UH' }], + ])('renders `postcode` with %s', (_, params) => { + const $ = cheerio.load(renderComponent('address-output', params)); + + expect($('.ons-address-output__postcode').text().trim()).toBe('AB12 6UH'); + }); }); diff --git a/src/components/autosuggest/_autosuggest.scss b/src/components/autosuggest/_autosuggest.scss index c66b39107d..2fcc4a32eb 100644 --- a/src/components/autosuggest/_autosuggest.scss +++ b/src/components/autosuggest/_autosuggest.scss @@ -1,137 +1,137 @@ .ons-autosuggest { - position: relative; + position: relative; - &__combobox { - border-radius: $input-radius; - display: inline-block; + &__combobox { + border-radius: $input-radius; + display: inline-block; - @include mq(xxs, s) { - width: 100%; + @include mq(xxs, s) { + width: 100%; + } } - } - - &__results { - border: 1px solid var(--ons-color-input-border); - border-radius: $input-radius; - display: none; - margin: 0.5rem 0 0; - overflow: hidden; - padding: 0; - width: 100%; - } - - &__group { - color: var(--ons-color-text-link); - text-decoration: underline; - } - - &__results-title { - background: var(--ons-color-grey-15); - border-bottom: 1px solid var(--ons-color-input-border); - padding: 0.25rem 0.5rem; - } - - &__listbox { - background: var(--ons-color-white); - list-style: none; - margin: 0; - padding: 0; - &:focus { - outline: none; - } - } - - &__option { - cursor: pointer; - margin: 0; - outline: none; - padding: $input-padding-vertical $input-padding-horizontal; - &:not(:last-child) { - border-bottom: 1px solid var(--ons-color-input-border); + &__results { + border: 1px solid var(--ons-color-input-border); + border-radius: $input-radius; + display: none; + margin: 0.5rem 0 0; + overflow: hidden; + padding: 0; + width: 100%; } - &:not(&--no-results, &--more-results):hover, - &--focused:not(&--no-results) { - background: var(--ons-color-text-link-hover); - border-color: var(--ons-color-text-link-hover); - color: var(--ons-color-white); - - .ons-autosuggest__group, - .ons-autosuggest__category { - color: var(--ons-color-white); - } + &__group { + color: var(--ons-color-text-link); + text-decoration: underline; } - &:active:not(&--no-results, &--more-results) { - background: var(--ons-color-focus); - color: var(--ons-color-text-link-focus); + &__results-title { + background: var(--ons-color-grey-15); + border-bottom: 1px solid var(--ons-color-input-border); + padding: 0.25rem 0.5rem; + } - .ons-autosuggest__group, - .ons-autosuggest__category { - color: var(--ons-color-text-link-focus); - } + &__listbox { + background: var(--ons-color-white); + list-style: none; + margin: 0; + padding: 0; + &:focus { + outline: none; + } } - &--no-results, - &--more-results { - background: var(--ons-color-grey-15); - cursor: not-allowed; - padding: 0.25rem 0.5rem; + &__option { + cursor: pointer; + margin: 0; + outline: none; + padding: $input-padding-vertical $input-padding-horizontal; + + &:not(:last-child) { + border-bottom: 1px solid var(--ons-color-input-border); + } + + &:not(&--no-results, &--more-results):hover, + &--focused:not(&--no-results) { + background: var(--ons-color-text-link-hover); + border-color: var(--ons-color-text-link-hover); + color: var(--ons-color-white); + + .ons-autosuggest__group, + .ons-autosuggest__category { + color: var(--ons-color-white); + } + } + + &:active:not(&--no-results, &--more-results) { + background: var(--ons-color-focus); + color: var(--ons-color-text-link-focus); + + .ons-autosuggest__group, + .ons-autosuggest__category { + color: var(--ons-color-text-link-focus); + } + } + + &--no-results, + &--more-results { + background: var(--ons-color-grey-15); + cursor: not-allowed; + padding: 0.25rem 0.5rem; + } } - } - &__warning { - background: #f0f0f0; - margin: 0; - padding-left: 0.5rem; + &__warning { + background: #f0f0f0; + margin: 0; + padding-left: 0.5rem; - &:not(:last-child) { - border-bottom: 1px solid var(--ons-color-input-border); - } - } - - &__panel.ons-panel--warn { - background: none; - border: 0; - margin: 0; - .ons-panel__icon { - font-size: 21px; - line-height: 25px; - margin-top: 0; - min-height: 24px; - min-width: 26px; - top: 17px; + &:not(:last-child) { + border-bottom: 1px solid var(--ons-color-input-border); + } } - .ons-panel__body { - font-weight: $font-weight-bold; - padding: 0.8rem 0.8rem 0.8rem 2rem; + &__panel.ons-panel--warn { + background: none; + border: 0; + margin: 0; + .ons-panel__icon { + font-size: 21px; + line-height: 25px; + margin-top: 0; + min-height: 24px; + min-width: 26px; + top: 17px; + } + + .ons-panel__body { + font-weight: $font-weight-bold; + padding: 0.8rem 0.8rem 0.8rem 2rem; + } } - } - - // Modifiers - &:not(&--initialised) & { - &__instructions, - &__listbox, - &__status { - display: none; + + // Modifiers + &:not(&--initialised) & { + &__instructions, + &__listbox, + &__status { + display: none; + } } - } - &--has-results & { - &__results { - display: block; + &--has-results & { + &__results { + display: block; + } } - } - - &--header { - .ons-autosuggest__results { - border: none; - box-shadow: 0 0 5px 0 rgb(34 34 34 / 60%); - left: 0; - position: absolute; - z-index: 10; + + &--header { + .ons-autosuggest__results { + border: 0; + box-shadow: 0 0 5px 0 rgb(34 34 34 / 60%); + left: 0; + position: absolute; + z-index: 10; + } } - } } diff --git a/src/components/autosuggest/_macro.spec.js b/src/components/autosuggest/_macro.spec.js index c3506dc32a..efac0908e7 100644 --- a/src/components/autosuggest/_macro.spec.js +++ b/src/components/autosuggest/_macro.spec.js @@ -6,291 +6,291 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_AUTOSUGGEST = { - id: 'country-of-birth', - input: { - label: { - text: 'Current name of country', - description: 'Enter your own answer or select from suggestions', - id: 'country-of-birth-label', - classes: 'extra-label-class', - }, - autocomplete: 'off', - }, - instructions: 'Use up and down keys to navigate.', - ariaYouHaveSelected: 'You have selected', - ariaMinChars: 'Enter 3 or more characters for suggestions.', - minChars: 2, - ariaResultsLabel: 'Country suggestions', - ariaOneResult: 'There is one suggestion available.', - ariaNResults: 'There are {n} suggestions available.', - ariaLimitedResults: 'Type more characters to improve your search', - moreResults: 'Continue entering to improve suggestions', - resultsTitle: 'Suggestions', - resultsTitleId: 'country-of-birth-suggestions', - autosuggestData: '/examples/data/country-of-birth.json', - noResults: 'No suggestions found. You can enter your own answer', - typeMore: 'Continue entering to get suggestions', -}; - -describe('macro: autosuggest', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has expected id on container element', () => { - const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - - expect($('.ons-autosuggest').attr('id')).toBe('country-of-birth-container'); - }); - - it('has the provided data attributes', () => { - const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - - const $element = $('.ons-autosuggest'); - expect($element.attr('data-allow-multiple')).toBeUndefined(); - expect($element.attr('data-min-chars')).toBe('2'); - expect($element.attr('data-aria-limited-results')).toBe('Type more characters to improve your search'); - expect($element.attr('data-aria-min-chars')).toBe('Enter 3 or more characters for suggestions.'); - expect($element.attr('data-aria-n-results')).toBe('There are {n} suggestions available.'); - expect($element.attr('data-aria-one-result')).toBe('There is one suggestion available.'); - expect($element.attr('data-aria-you-have-selected')).toBe('You have selected'); - expect($element.attr('data-autosuggest-data')).toBe('/examples/data/country-of-birth.json'); - expect($element.attr('data-instructions')).toBe('Use up and down keys to navigate.'); - expect($element.attr('data-more-results')).toBe('Continue entering to improve suggestions'); - expect($element.attr('data-no-results')).toBe('No suggestions found. You can enter your own answer'); - expect($element.attr('data-results-title')).toBe('Suggestions'); - expect($element.attr('data-type-more')).toBe('Continue entering to get suggestions'); - }); - - it('has the `data-allow-multiple` attribute when `allowMultiple` is `true`', () => { - const $ = cheerio.load( - renderComponent('autosuggest', { - ...EXAMPLE_AUTOSUGGEST, - allowMultiple: true, - }), - ); - - expect($('.ons-autosuggest').attr('data-allow-multiple')).toBe('true'); - }); - - it('has a special class that indicates the component should initialise itself', () => { - const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - - expect($('.ons-autosuggest').hasClass('ons-js-autosuggest')).toBe(true); - }); - - it('does not have a special class when the component has an external initialiser', () => { - const $ = cheerio.load( - renderComponent('autosuggest', { - ...EXAMPLE_AUTOSUGGEST, - externalInitialiser: true, - }), - ); - - expect($('.ons-autosuggest').hasClass('ons-js-autosuggest')).toBe(false); - }); - - it('has special class to indicate that component is not editable', () => { - const $ = cheerio.load( - renderComponent('autosuggest', { - ...EXAMPLE_AUTOSUGGEST, - isEditable: false, - }), - ); - - expect($('.ons-autosuggest').hasClass('ons-js-address-not-editable')).toBe(true); - }); - - it('has special class to indicate that component input is mandatory', () => { - const $ = cheerio.load( - renderComponent('autosuggest', { - ...EXAMPLE_AUTOSUGGEST, - mandatory: true, - }), - ); - - expect($('.ons-autosuggest').hasClass('ons-js-address-mandatory')).toBe(true); - }); - - it('has additionally provided container style classes', () => { - const $ = cheerio.load( - renderComponent('autosuggest', { - ...EXAMPLE_AUTOSUGGEST, - containerClasses: 'extra-class another-extra-class', - }), - ); - - expect($('.ons-autosuggest').hasClass('extra-class')).toBe(true); - expect($('.ons-autosuggest').hasClass('another-extra-class')).toBe(true); - }); - - describe('input', () => { - it('uses the `input` component with the expected parameters', () => { - const faker = templateFaker(); - const inputSpy = faker.spy('input'); - - faker.renderComponent('autosuggest', { - ...EXAMPLE_AUTOSUGGEST, - input: { - type: 'text', - classes: 'extra-class another-extra-class', - width: '7', - label: { + id: 'country-of-birth', + input: { + label: { text: 'Current name of country', description: 'Enter your own answer or select from suggestions', id: 'country-of-birth-label', classes: 'extra-label-class', - }, - autocomplete: 'off', - legend: 'this is a legend', - legendClasses: 'legend-extra-class', - value: 'abc', - attributes: { - a: 42, - }, - error: { - id: 'error-id', - text: 'An error occurred.', - }, - mutuallyExclusive: null, - accessiblePlaceholder: true, - name: 'test-params', - minLength: 1, - maxLength: 10, - prefix: { - title: 'Great British Pounds', - text: '£', - id: 'gbp-prefix', - }, - suffix: { - title: 'Percentage of total', - text: '%', - id: 'percentage-suffix', - }, - fieldId: 'field-id-test', - fieldClasses: 'field-class-test', - dontWrap: true, - charCheckLimit: { - limit: 200, - charCountSingular: 'You have {x} character remaining', - charCountPlural: 'You have {x} characters remaining', - charCountOverLimitSingular: '{x} character too many', - charCountOverLimitPlural: '{x} characters too many', - }, - searchButton: { - text: 'Search', - }, - postTextboxLinkText: 'Post textbox link text', - postTextboxLinkUrl: 'https://www.ons.gov.uk', - listeners: { - click: "function() { console.log('click'); }", - }, }, - }); - - expect(inputSpy.occurrences[0]).toHaveProperty('id', 'country-of-birth'); - expect(inputSpy.occurrences[0]).toHaveProperty('type', 'text'); - expect(inputSpy.occurrences[0]).toHaveProperty('classes', 'ons-js-autosuggest-input extra-class another-extra-class'); - expect(inputSpy.occurrences[0]).toHaveProperty('width', '7'); - expect(inputSpy.occurrences[0]).toHaveProperty('label.text', 'Current name of country'); - expect(inputSpy.occurrences[0]).toHaveProperty('label.description', 'Enter your own answer or select from suggestions'); - expect(inputSpy.occurrences[0]).toHaveProperty('label.id', 'country-of-birth-label'); - expect(inputSpy.occurrences[0]).toHaveProperty('label.classes', 'extra-label-class'); - expect(inputSpy.occurrences[0]).toHaveProperty('autocomplete', 'off'); - expect(inputSpy.occurrences[0]).toHaveProperty('legend', 'this is a legend'); - expect(inputSpy.occurrences[0]).toHaveProperty('legendClasses', 'legend-extra-class'); - expect(inputSpy.occurrences[0]).toHaveProperty('value', 'abc'); - expect(inputSpy.occurrences[0]).toHaveProperty('attributes.a', 42); - expect(inputSpy.occurrences[0]).toHaveProperty('error.id', 'error-id'); - expect(inputSpy.occurrences[0]).toHaveProperty('error.text', 'An error occurred.'); - expect(inputSpy.occurrences[0]).toHaveProperty('mutuallyExclusive', null); - expect(inputSpy.occurrences[0]).toHaveProperty('accessiblePlaceholder', true); - expect(inputSpy.occurrences[0]).toHaveProperty('name', 'test-params'); - expect(typeof inputSpy.occurrences[0].autosuggestResults).toBe('string'); - expect(inputSpy.occurrences[0]).toHaveProperty('minLength', 1); - expect(inputSpy.occurrences[0]).toHaveProperty('maxLength', 10); - expect(inputSpy.occurrences[0]).toHaveProperty('prefix.title', 'Great British Pounds'); - expect(inputSpy.occurrences[0]).toHaveProperty('prefix.text', '£'); - expect(inputSpy.occurrences[0]).toHaveProperty('prefix.id', 'gbp-prefix'); - expect(inputSpy.occurrences[0]).toHaveProperty('suffix.title', 'Percentage of total'); - expect(inputSpy.occurrences[0]).toHaveProperty('suffix.text', '%'); - expect(inputSpy.occurrences[0]).toHaveProperty('suffix.id', 'percentage-suffix'); - expect(inputSpy.occurrences[0]).toHaveProperty('fieldId', 'field-id-test'); - expect(inputSpy.occurrences[0]).toHaveProperty('fieldClasses', 'field-class-test'); - expect(inputSpy.occurrences[0]).toHaveProperty('dontWrap', true); - expect(inputSpy.occurrences[0]).toHaveProperty('charCheckLimit.limit', 200); - expect(inputSpy.occurrences[0]).toHaveProperty('charCheckLimit.charCountSingular', 'You have {x} character remaining'); - expect(inputSpy.occurrences[0]).toHaveProperty('charCheckLimit.charCountPlural', 'You have {x} characters remaining'); - expect(inputSpy.occurrences[0]).toHaveProperty('charCheckLimit.charCountOverLimitSingular', '{x} character too many'); - expect(inputSpy.occurrences[0]).toHaveProperty('charCheckLimit.charCountOverLimitPlural', '{x} characters too many'); - expect(inputSpy.occurrences[0]).toHaveProperty('searchButton.text', 'Search'); - expect(inputSpy.occurrences[0]).toHaveProperty('postTextboxLinkText', 'Post textbox link text'); - expect(inputSpy.occurrences[0]).toHaveProperty('postTextboxLinkUrl', 'https://www.ons.gov.uk'); - expect(inputSpy.occurrences[0]).toHaveProperty('listeners.click', "function() { console.log('click'); }"); - }); - }); + autocomplete: 'off', + }, + instructions: 'Use up and down keys to navigate.', + ariaYouHaveSelected: 'You have selected', + ariaMinChars: 'Enter 3 or more characters for suggestions.', + minChars: 2, + ariaResultsLabel: 'Country suggestions', + ariaOneResult: 'There is one suggestion available.', + ariaNResults: 'There are {n} suggestions available.', + ariaLimitedResults: 'Type more characters to improve your search', + moreResults: 'Continue entering to improve suggestions', + resultsTitle: 'Suggestions', + resultsTitleId: 'country-of-birth-suggestions', + autosuggestData: '/examples/data/country-of-birth.json', + noResults: 'No suggestions found. You can enter your own answer', + typeMore: 'Continue entering to get suggestions', +}; - describe('autosuggest results', () => { - it('is rendered `mutallyExclusive` parameter is not defined', () => { - const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); +describe('macro: autosuggest', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - expect($('.ons-autosuggest__results').length).toBe(1); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); }); - it('is not rendered when `mutallyExclusive` parameter is defined', () => { - const $ = cheerio.load( - renderComponent('autosuggest', { - ...EXAMPLE_AUTOSUGGEST, - mutuallyExclusive: { fakeParam: true }, - }), - ); + it('has expected id on container element', () => { + const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + + expect($('.ons-autosuggest').attr('id')).toBe('country-of-birth-container'); + }); - expect($('.ons-autosuggest__results').length).toBe(0); + it('has the provided data attributes', () => { + const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + + const $element = $('.ons-autosuggest'); + expect($element.attr('data-allow-multiple')).toBeUndefined(); + expect($element.attr('data-min-chars')).toBe('2'); + expect($element.attr('data-aria-limited-results')).toBe('Type more characters to improve your search'); + expect($element.attr('data-aria-min-chars')).toBe('Enter 3 or more characters for suggestions.'); + expect($element.attr('data-aria-n-results')).toBe('There are {n} suggestions available.'); + expect($element.attr('data-aria-one-result')).toBe('There is one suggestion available.'); + expect($element.attr('data-aria-you-have-selected')).toBe('You have selected'); + expect($element.attr('data-autosuggest-data')).toBe('/examples/data/country-of-birth.json'); + expect($element.attr('data-instructions')).toBe('Use up and down keys to navigate.'); + expect($element.attr('data-more-results')).toBe('Continue entering to improve suggestions'); + expect($element.attr('data-no-results')).toBe('No suggestions found. You can enter your own answer'); + expect($element.attr('data-results-title')).toBe('Suggestions'); + expect($element.attr('data-type-more')).toBe('Continue entering to get suggestions'); }); - it('renders div with the provided title identifier', () => { - const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('has the `data-allow-multiple` attribute when `allowMultiple` is `true`', () => { + const $ = cheerio.load( + renderComponent('autosuggest', { + ...EXAMPLE_AUTOSUGGEST, + allowMultiple: true, + }), + ); - expect($('.ons-autosuggest__results-title').attr('id')).toBe('country-of-birth-suggestions'); + expect($('.ons-autosuggest').attr('data-allow-multiple')).toBe('true'); }); - it('renders div with the provided title text', () => { - const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('has a special class that indicates the component should initialise itself', () => { + const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - expect($('.ons-autosuggest__results-title').text().trim()).toBe('Suggestions'); + expect($('.ons-autosuggest').hasClass('ons-js-autosuggest')).toBe(true); }); - it('renders list with a generated identifier', () => { - const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('does not have a special class when the component has an external initialiser', () => { + const $ = cheerio.load( + renderComponent('autosuggest', { + ...EXAMPLE_AUTOSUGGEST, + externalInitialiser: true, + }), + ); - expect($('.ons-autosuggest__listbox').attr('id')).toBe('country-of-birth-listbox'); + expect($('.ons-autosuggest').hasClass('ons-js-autosuggest')).toBe(false); }); - it('renders an accessible list', () => { - const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('has special class to indicate that component is not editable', () => { + const $ = cheerio.load( + renderComponent('autosuggest', { + ...EXAMPLE_AUTOSUGGEST, + isEditable: false, + }), + ); - expect($('.ons-autosuggest__listbox').attr('title')).toBe('Suggestions'); + expect($('.ons-autosuggest').hasClass('ons-js-address-not-editable')).toBe(true); }); - it('renders instructions with a generated identifier', () => { - const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('has special class to indicate that component input is mandatory', () => { + const $ = cheerio.load( + renderComponent('autosuggest', { + ...EXAMPLE_AUTOSUGGEST, + mandatory: true, + }), + ); - expect($('.ons-autosuggest__instructions').attr('id')).toBe('country-of-birth-instructions'); + expect($('.ons-autosuggest').hasClass('ons-js-address-mandatory')).toBe(true); }); - it('adds aria-atomic=true value to status container', () => { - const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('has additionally provided container style classes', () => { + const $ = cheerio.load( + renderComponent('autosuggest', { + ...EXAMPLE_AUTOSUGGEST, + containerClasses: 'extra-class another-extra-class', + }), + ); - expect($('.ons-autosuggest__status').attr('aria-atomic')).toBe('true'); + expect($('.ons-autosuggest').hasClass('extra-class')).toBe(true); + expect($('.ons-autosuggest').hasClass('another-extra-class')).toBe(true); }); - it('renders instructions text', () => { - const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + describe('input', () => { + it('uses the `input` component with the expected parameters', () => { + const faker = templateFaker(); + const inputSpy = faker.spy('input'); + + faker.renderComponent('autosuggest', { + ...EXAMPLE_AUTOSUGGEST, + input: { + type: 'text', + classes: 'extra-class another-extra-class', + width: '7', + label: { + text: 'Current name of country', + description: 'Enter your own answer or select from suggestions', + id: 'country-of-birth-label', + classes: 'extra-label-class', + }, + autocomplete: 'off', + legend: 'this is a legend', + legendClasses: 'legend-extra-class', + value: 'abc', + attributes: { + a: 42, + }, + error: { + id: 'error-id', + text: 'An error occurred.', + }, + mutuallyExclusive: null, + accessiblePlaceholder: true, + name: 'test-params', + minLength: 1, + maxLength: 10, + prefix: { + title: 'Great British Pounds', + text: '£', + id: 'gbp-prefix', + }, + suffix: { + title: 'Percentage of total', + text: '%', + id: 'percentage-suffix', + }, + fieldId: 'field-id-test', + fieldClasses: 'field-class-test', + dontWrap: true, + charCheckLimit: { + limit: 200, + charCountSingular: 'You have {x} character remaining', + charCountPlural: 'You have {x} characters remaining', + charCountOverLimitSingular: '{x} character too many', + charCountOverLimitPlural: '{x} characters too many', + }, + searchButton: { + text: 'Search', + }, + postTextboxLinkText: 'Post textbox link text', + postTextboxLinkUrl: 'https://www.ons.gov.uk', + listeners: { + click: "function() { console.log('click'); }", + }, + }, + }); + + expect(inputSpy.occurrences[0]).toHaveProperty('id', 'country-of-birth'); + expect(inputSpy.occurrences[0]).toHaveProperty('type', 'text'); + expect(inputSpy.occurrences[0]).toHaveProperty('classes', 'ons-js-autosuggest-input extra-class another-extra-class'); + expect(inputSpy.occurrences[0]).toHaveProperty('width', '7'); + expect(inputSpy.occurrences[0]).toHaveProperty('label.text', 'Current name of country'); + expect(inputSpy.occurrences[0]).toHaveProperty('label.description', 'Enter your own answer or select from suggestions'); + expect(inputSpy.occurrences[0]).toHaveProperty('label.id', 'country-of-birth-label'); + expect(inputSpy.occurrences[0]).toHaveProperty('label.classes', 'extra-label-class'); + expect(inputSpy.occurrences[0]).toHaveProperty('autocomplete', 'off'); + expect(inputSpy.occurrences[0]).toHaveProperty('legend', 'this is a legend'); + expect(inputSpy.occurrences[0]).toHaveProperty('legendClasses', 'legend-extra-class'); + expect(inputSpy.occurrences[0]).toHaveProperty('value', 'abc'); + expect(inputSpy.occurrences[0]).toHaveProperty('attributes.a', 42); + expect(inputSpy.occurrences[0]).toHaveProperty('error.id', 'error-id'); + expect(inputSpy.occurrences[0]).toHaveProperty('error.text', 'An error occurred.'); + expect(inputSpy.occurrences[0]).toHaveProperty('mutuallyExclusive', null); + expect(inputSpy.occurrences[0]).toHaveProperty('accessiblePlaceholder', true); + expect(inputSpy.occurrences[0]).toHaveProperty('name', 'test-params'); + expect(typeof inputSpy.occurrences[0].autosuggestResults).toBe('string'); + expect(inputSpy.occurrences[0]).toHaveProperty('minLength', 1); + expect(inputSpy.occurrences[0]).toHaveProperty('maxLength', 10); + expect(inputSpy.occurrences[0]).toHaveProperty('prefix.title', 'Great British Pounds'); + expect(inputSpy.occurrences[0]).toHaveProperty('prefix.text', '£'); + expect(inputSpy.occurrences[0]).toHaveProperty('prefix.id', 'gbp-prefix'); + expect(inputSpy.occurrences[0]).toHaveProperty('suffix.title', 'Percentage of total'); + expect(inputSpy.occurrences[0]).toHaveProperty('suffix.text', '%'); + expect(inputSpy.occurrences[0]).toHaveProperty('suffix.id', 'percentage-suffix'); + expect(inputSpy.occurrences[0]).toHaveProperty('fieldId', 'field-id-test'); + expect(inputSpy.occurrences[0]).toHaveProperty('fieldClasses', 'field-class-test'); + expect(inputSpy.occurrences[0]).toHaveProperty('dontWrap', true); + expect(inputSpy.occurrences[0]).toHaveProperty('charCheckLimit.limit', 200); + expect(inputSpy.occurrences[0]).toHaveProperty('charCheckLimit.charCountSingular', 'You have {x} character remaining'); + expect(inputSpy.occurrences[0]).toHaveProperty('charCheckLimit.charCountPlural', 'You have {x} characters remaining'); + expect(inputSpy.occurrences[0]).toHaveProperty('charCheckLimit.charCountOverLimitSingular', '{x} character too many'); + expect(inputSpy.occurrences[0]).toHaveProperty('charCheckLimit.charCountOverLimitPlural', '{x} characters too many'); + expect(inputSpy.occurrences[0]).toHaveProperty('searchButton.text', 'Search'); + expect(inputSpy.occurrences[0]).toHaveProperty('postTextboxLinkText', 'Post textbox link text'); + expect(inputSpy.occurrences[0]).toHaveProperty('postTextboxLinkUrl', 'https://www.ons.gov.uk'); + expect(inputSpy.occurrences[0]).toHaveProperty('listeners.click', "function() { console.log('click'); }"); + }); + }); + + describe('autosuggest results', () => { + it('is rendered `mutallyExclusive` parameter is not defined', () => { + const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + + expect($('.ons-autosuggest__results').length).toBe(1); + }); + + it('is not rendered when `mutallyExclusive` parameter is defined', () => { + const $ = cheerio.load( + renderComponent('autosuggest', { + ...EXAMPLE_AUTOSUGGEST, + mutuallyExclusive: { fakeParam: true }, + }), + ); + + expect($('.ons-autosuggest__results').length).toBe(0); + }); + + it('renders div with the provided title identifier', () => { + const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + + expect($('.ons-autosuggest__results-title').attr('id')).toBe('country-of-birth-suggestions'); + }); + + it('renders div with the provided title text', () => { + const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + + expect($('.ons-autosuggest__results-title').text().trim()).toBe('Suggestions'); + }); + + it('renders list with a generated identifier', () => { + const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + + expect($('.ons-autosuggest__listbox').attr('id')).toBe('country-of-birth-listbox'); + }); + + it('renders an accessible list', () => { + const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + + expect($('.ons-autosuggest__listbox').attr('title')).toBe('Suggestions'); + }); + + it('renders instructions with a generated identifier', () => { + const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + + expect($('.ons-autosuggest__instructions').attr('id')).toBe('country-of-birth-instructions'); + }); + + it('adds aria-atomic=true value to status container', () => { + const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + + expect($('.ons-autosuggest__status').attr('aria-atomic')).toBe('true'); + }); + + it('renders instructions text', () => { + const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - expect($('.ons-autosuggest__instructions').text().trim()).toBe('Use up and down keys to navigate.'); + expect($('.ons-autosuggest__instructions').text().trim()).toBe('Use up and down keys to navigate.'); + }); }); - }); }); diff --git a/src/components/autosuggest/autosuggest.dom.js b/src/components/autosuggest/autosuggest.dom.js index 2a047b120d..4d7a8b00a4 100644 --- a/src/components/autosuggest/autosuggest.dom.js +++ b/src/components/autosuggest/autosuggest.dom.js @@ -1,13 +1,13 @@ import domready from '../../js/domready'; async function initialiseAutosuggests() { - const autosuggests = [...document.querySelectorAll('.ons-js-autosuggest')]; + const autosuggests = [...document.querySelectorAll('.ons-js-autosuggest')]; - if (autosuggests.length) { - const Autosuggest = (await import('./autosuggest')).default; + if (autosuggests.length) { + const Autosuggest = (await import('./autosuggest')).default; - autosuggests.forEach((autosuggest) => new Autosuggest(autosuggest)); - } + autosuggests.forEach((autosuggest) => new Autosuggest(autosuggest)); + } } domready(initialiseAutosuggests); diff --git a/src/components/autosuggest/autosuggest.helpers.js b/src/components/autosuggest/autosuggest.helpers.js index dc1d447658..433e93b2d5 100644 --- a/src/components/autosuggest/autosuggest.helpers.js +++ b/src/components/autosuggest/autosuggest.helpers.js @@ -1,17 +1,17 @@ export function sanitiseAutosuggestText(string, sanitisedQueryRemoveChars = [], sanitisedQuerySplitNumsChars = false, trimEnd = true) { - let sanitisedString = string.toLowerCase(); + let sanitisedString = string.toLowerCase(); - sanitisedQueryRemoveChars.forEach((char) => { - sanitisedString = sanitisedString.replace(new RegExp(char.toLowerCase(), 'g'), ''); - }); + sanitisedQueryRemoveChars.forEach((char) => { + sanitisedString = sanitisedString.replace(new RegExp(char.toLowerCase(), 'g'), ''); + }); - sanitisedString = trimEnd ? sanitisedString.trim() : sanitisedString.trimStart(); - sanitisedString = sanitisedString.replace(/\s\s+/g, ' '); - sanitisedString = sanitisedString.replace(/[&]/g, '%26'); + sanitisedString = trimEnd ? sanitisedString.trim() : sanitisedString.trimStart(); + sanitisedString = sanitisedString.replace(/\s\s+/g, ' '); + sanitisedString = sanitisedString.replace(/[&]/g, '%26'); - if (sanitisedQuerySplitNumsChars) { - sanitisedString = sanitisedString.replace(/\d(?=[a-z]{3,})/gi, '$& '); - } + if (sanitisedQuerySplitNumsChars) { + sanitisedString = sanitisedString.replace(/\d(?=[a-z]{3,})/gi, '$& '); + } - return sanitisedString; + return sanitisedString; } diff --git a/src/components/autosuggest/autosuggest.helpers.spec.js b/src/components/autosuggest/autosuggest.helpers.spec.js index 29d51a0554..949e0f76f6 100644 --- a/src/components/autosuggest/autosuggest.helpers.spec.js +++ b/src/components/autosuggest/autosuggest.helpers.spec.js @@ -1,85 +1,85 @@ import { sanitiseAutosuggestText } from './autosuggest.helpers'; describe('module: autosuggest.helpers', () => { - describe('function: sanitiseAutosuggestText', () => { - it.each([ - ['ABC', 'abc'], - ['ABCdEF', 'abcdef'], - ['1ABC23', '1abc23'], - ])('transforms input characters into lower case (%s)', (input, expectedResult) => { - const result = sanitiseAutosuggestText(input); - expect(result).toBe(expectedResult); - }); + describe('function: sanitiseAutosuggestText', () => { + it.each([ + ['ABC', 'abc'], + ['ABCdEF', 'abcdef'], + ['1ABC23', '1abc23'], + ])('transforms input characters into lower case (%s)', (input, expectedResult) => { + const result = sanitiseAutosuggestText(input); + expect(result).toBe(expectedResult); + }); - it.each([ - ['abccdefge', [], 'abccdefge'], - ['abccdefge', ['c'], 'abdefge'], - ['abccdefge', ['c', 'E'], 'abdfg'], - ])('removes unwanted characters (%s)', (input, removeChars, expectedResult) => { - const result = sanitiseAutosuggestText(input, removeChars); - expect(result).toBe(expectedResult); - }); + it.each([ + ['abccdefge', [], 'abccdefge'], + ['abccdefge', ['c'], 'abdefge'], + ['abccdefge', ['c', 'E'], 'abdfg'], + ])('removes unwanted characters (%s)', (input, removeChars, expectedResult) => { + const result = sanitiseAutosuggestText(input, removeChars); + expect(result).toBe(expectedResult); + }); - it.each([ - ['a b', 'a b'], - ['a b c', 'a b c'], - ['a \n\n\t b', 'a b'], - ])('replaces blocks of whitespace with a single space character', (input, expectedResult) => { - const result = sanitiseAutosuggestText(input); - expect(result).toBe(expectedResult); - }); + it.each([ + ['a b', 'a b'], + ['a b c', 'a b c'], + ['a \n\n\t b', 'a b'], + ])('replaces blocks of whitespace with a single space character', (input, expectedResult) => { + const result = sanitiseAutosuggestText(input); + expect(result).toBe(expectedResult); + }); - it.each([ - ['a&b', 'a%26b'], - ['a&&b', 'a%26%26b'], - ['a&b&c', 'a%26b%26c'], - ])('escapes the "&" character for use in a URL', (input, expectedResult) => { - const result = sanitiseAutosuggestText(input); - expect(result).toBe(expectedResult); - }); + it.each([ + ['a&b', 'a%26b'], + ['a&&b', 'a%26%26b'], + ['a&b&c', 'a%26b%26c'], + ])('escapes the "&" character for use in a URL', (input, expectedResult) => { + const result = sanitiseAutosuggestText(input); + expect(result).toBe(expectedResult); + }); - it.each([ - ['1a', '1a'], - ['1aa', '1aa'], - ['1aaa', '1aaa'], - ['1aaaa', '1aaaa'], - ['11aaaa', '11aaaa'], - ['11aaaa22bbb', '11aaaa22bbb'], - ['11aaaa2b33ccc', '11aaaa2b33ccc'], - ])( - 'does not a space after a digit that is followed by at least 3 letters when `sanitisedQuerySplitNumsChars` is false', - (input, expectedResult) => { - const result = sanitiseAutosuggestText(input, [], false); - expect(result).toBe(expectedResult); - }, - ); + it.each([ + ['1a', '1a'], + ['1aa', '1aa'], + ['1aaa', '1aaa'], + ['1aaaa', '1aaaa'], + ['11aaaa', '11aaaa'], + ['11aaaa22bbb', '11aaaa22bbb'], + ['11aaaa2b33ccc', '11aaaa2b33ccc'], + ])( + 'does not a space after a digit that is followed by at least 3 letters when `sanitisedQuerySplitNumsChars` is false', + (input, expectedResult) => { + const result = sanitiseAutosuggestText(input, [], false); + expect(result).toBe(expectedResult); + }, + ); - it.each([ - ['1a', '1a'], - ['1aa', '1aa'], - ['1aaa', '1 aaa'], - ['1aaaa', '1 aaaa'], - ['11aaaa', '11 aaaa'], - ['11aaaa22bbb', '11 aaaa22 bbb'], - ['11aaaa2b33ccc', '11 aaaa2b33 ccc'], - ])( - 'adds a space after a digit that is followed by at least 3 letters when `sanitisedQuerySplitNumsChars` is true', - (input, expectedResult) => { - const result = sanitiseAutosuggestText(input, [], true); - expect(result).toBe(expectedResult); - }, - ); + it.each([ + ['1a', '1a'], + ['1aa', '1aa'], + ['1aaa', '1 aaa'], + ['1aaaa', '1 aaaa'], + ['11aaaa', '11 aaaa'], + ['11aaaa22bbb', '11 aaaa22 bbb'], + ['11aaaa2b33ccc', '11 aaaa2b33 ccc'], + ])( + 'adds a space after a digit that is followed by at least 3 letters when `sanitisedQuerySplitNumsChars` is true', + (input, expectedResult) => { + const result = sanitiseAutosuggestText(input, [], true); + expect(result).toBe(expectedResult); + }, + ); - it('trims whitespace from start and end of input when `trimEnd` is true', () => { - const result = sanitiseAutosuggestText(' 1a ', [], false, true); - expect(result).toBe('1a'); - }); + it('trims whitespace from start and end of input when `trimEnd` is true', () => { + const result = sanitiseAutosuggestText(' 1a ', [], false, true); + expect(result).toBe('1a'); + }); - it('trims whitespace from only the start of input when `trimEnd` is false', () => { - const result = sanitiseAutosuggestText(' 1a ', [], false, false); - // The trailing space is consolidated into 1 whitespace due to a transformation that - // this implementation applies. - expect(result).toBe('1a '); + it('trims whitespace from only the start of input when `trimEnd` is false', () => { + const result = sanitiseAutosuggestText(' 1a ', [], false, false); + // The trailing space is consolidated into 1 whitespace due to a transformation that + // this implementation applies. + expect(result).toBe('1a '); + }); }); - }); }); diff --git a/src/components/autosuggest/autosuggest.js b/src/components/autosuggest/autosuggest.js index e64e6d9af4..95d079c9fa 100644 --- a/src/components/autosuggest/autosuggest.js +++ b/src/components/autosuggest/autosuggest.js @@ -1,29 +1,29 @@ import AutosuggestUI from './autosuggest.ui'; export default class Autosuggest { - constructor(context) { - this.context = context; - this.language = context.getAttribute('data-lang'); + constructor(context) { + this.context = context; + this.language = context.getAttribute('data-lang'); - this.autosuggest = new AutosuggestUI({ - context, - onSelect: this.onSelect.bind(this), - onUnsetResult: this.onUnsetResult.bind(this), - onError: this.onError.bind(this), - }); - } + this.autosuggest = new AutosuggestUI({ + context, + onSelect: this.onSelect.bind(this), + onUnsetResult: this.onUnsetResult.bind(this), + onError: this.onError.bind(this), + }); + } - get lang() { - return !this.language ? document.documentElement.getAttribute('lang').toLowerCase() : this.language.toLowerCase(); - } + get lang() { + return !this.language ? document.documentElement.getAttribute('lang').toLowerCase() : this.language.toLowerCase(); + } - async onSelect(result) { - this.autosuggest.input.value = result.displayText; - } + async onSelect(result) { + this.autosuggest.input.value = result.displayText; + } - async onUnsetResult() {} + async onUnsetResult() {} - onError(error) { - console.error(error); - } + onError(error) { + console.error(error); + } } diff --git a/src/components/autosuggest/autosuggest.spec.js b/src/components/autosuggest/autosuggest.spec.js index 1655e1af00..ff502c29e0 100644 --- a/src/components/autosuggest/autosuggest.spec.js +++ b/src/components/autosuggest/autosuggest.spec.js @@ -2,678 +2,684 @@ import { getNodeAttributes, PuppeteerEndpointFaker } from '../../tests/helpers/p import { renderComponent, setTestPage } from '../../tests/helpers/rendering'; const EXAMPLE_AUTOSUGGEST = { - id: 'country-of-birth', - input: { - label: { - text: 'Current name of country', - description: 'Enter your own answer or select from suggestions', - id: 'country-of-birth-label', - classes: 'extra-label-class', + id: 'country-of-birth', + input: { + label: { + text: 'Current name of country', + description: 'Enter your own answer or select from suggestions', + id: 'country-of-birth-label', + classes: 'extra-label-class', + }, + autocomplete: 'off', }, - autocomplete: 'off', - }, - instructions: 'Use up and down keys to navigate.', - ariaYouHaveSelected: 'You have selected', - ariaMinChars: 'Enter 3 or more characters for suggestions.', - minChars: 3, - ariaResultsLabel: 'Country suggestions', - ariaOneResult: 'There is one suggestion available.', - ariaNResults: 'There are {n} suggestions available.', - ariaLimitedResults: 'Type more characters to improve your search', - moreResults: 'Continue entering to improve suggestions', - resultsTitle: 'Suggestions', - resultsTitleId: 'country-of-birth-suggestions', - noResults: 'No suggestions found.', - typeMore: 'Continue entering to get suggestions', - autosuggestData: '/test/fake/api/countries', + instructions: 'Use up and down keys to navigate.', + ariaYouHaveSelected: 'You have selected', + ariaMinChars: 'Enter 3 or more characters for suggestions.', + minChars: 3, + ariaResultsLabel: 'Country suggestions', + ariaOneResult: 'There is one suggestion available.', + ariaNResults: 'There are {n} suggestions available.', + ariaLimitedResults: 'Type more characters to improve your search', + moreResults: 'Continue entering to improve suggestions', + resultsTitle: 'Suggestions', + resultsTitleId: 'country-of-birth-suggestions', + noResults: 'No suggestions found.', + typeMore: 'Continue entering to get suggestions', + autosuggestData: '/test/fake/api/countries', }; const EXAMPLE_AUTOSUGGEST_WITH_LANGUAGE = { - id: 'country-of-birth', - label: { - text: 'Current name of country', - description: 'Enter your own answer or select from suggestions', - id: 'country-of-birth-label', - classes: 'extra-label-class', - }, - autocomplete: 'off', - instructions: 'Use up and down keys to navigate.', - ariaYouHaveSelected: 'You have selected', - ariaMinChars: 'Enter 3 or more characters for suggestions.', - minChars: 3, - ariaResultsLabel: 'Country suggestions', - ariaOneResult: 'There is one suggestion available.', - ariaNResults: 'There are {n} suggestions available.', - ariaLimitedResults: 'Type more characters to improve your search', - moreResults: 'Continue entering to improve suggestions', - resultsTitle: 'Suggestions', - resultsTitleId: 'country-of-birth-suggestions', - noResults: 'No suggestions found.', - typeMore: 'Continue entering to get suggestions', - autosuggestData: '/test/fake/api/countries', - language: 'en-gb', + id: 'country-of-birth', + label: { + text: 'Current name of country', + description: 'Enter your own answer or select from suggestions', + id: 'country-of-birth-label', + classes: 'extra-label-class', + }, + autocomplete: 'off', + instructions: 'Use up and down keys to navigate.', + ariaYouHaveSelected: 'You have selected', + ariaMinChars: 'Enter 3 or more characters for suggestions.', + minChars: 3, + ariaResultsLabel: 'Country suggestions', + ariaOneResult: 'There is one suggestion available.', + ariaNResults: 'There are {n} suggestions available.', + ariaLimitedResults: 'Type more characters to improve your search', + moreResults: 'Continue entering to improve suggestions', + resultsTitle: 'Suggestions', + resultsTitleId: 'country-of-birth-suggestions', + noResults: 'No suggestions found.', + typeMore: 'Continue entering to get suggestions', + autosuggestData: '/test/fake/api/countries', + language: 'en-gb', }; describe('script: autosuggest', () => { - const apiFaker = new PuppeteerEndpointFaker('/test/fake/api'); - - apiFaker.setOverride('/countries', { - data: [ - { en: 'England' }, - { en: 'Wales' }, - { en: 'Scotland' }, - { en: 'United States of America' }, - { en: 'United States Virgin Islands' }, - { en: 'Åland Islands' }, - ], - }); - - beforeAll(async () => { - await apiFaker.setup(page); - }); - - beforeEach(async () => { - await apiFaker.reset(); - }); - - describe('when the component initialises', () => { - it('the input should be given the correct aria attributes', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - - const attributes = await getNodeAttributes(page, '.ons-js-autosuggest-input'); - expect(attributes['aria-autocomplete']).toBe('list'); - expect(attributes['aria-controls']).toBe('country-of-birth-listbox'); - expect(attributes['aria-describedby']).toBe('country-of-birth-instructions'); - expect(attributes['aria-haspopup']).toBe('true'); - expect(attributes['aria-owns']).toBe('country-of-birth-listbox'); - expect(attributes['aria-expanded']).toBe('false'); - expect(attributes['role']).toBe('combobox'); + const apiFaker = new PuppeteerEndpointFaker('/test/fake/api'); + + apiFaker.setOverride('/countries', { + data: [ + { en: 'England' }, + { en: 'Wales' }, + { en: 'Scotland' }, + { en: 'United States of America' }, + { en: 'United States Virgin Islands' }, + { en: 'Åland Islands' }, + ], }); - it('the autocomplete attribute be set to be not set to on', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + beforeAll(async () => { + await apiFaker.setup(page); + }); - const attributes = await getNodeAttributes(page, '.ons-js-autosuggest-input'); - expect(attributes['autocomplete']).not.toBe('on'); + beforeEach(async () => { + await apiFaker.reset(); }); - it('the instructions, listbox, and status should become visible', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + describe('when the component initialises', () => { + it('the input should be given the correct aria attributes', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - const instructionsDisplayStyle = await page.$eval('.ons-js-autosuggest-instructions', (node) => getComputedStyle(node).display); - expect(instructionsDisplayStyle).toBe('block'); - const listboxDisplayStyle = await page.$eval('.ons-js-autosuggest-listbox', (node) => getComputedStyle(node).display); - expect(listboxDisplayStyle).toBe('block'); - const statusDisplayStyle = await page.$eval('.ons-js-autosuggest-aria-status', (node) => getComputedStyle(node).display); - expect(statusDisplayStyle).toBe('block'); - }); - }); + const attributes = await getNodeAttributes(page, '.ons-js-autosuggest-input'); + expect(attributes['aria-autocomplete']).toBe('list'); + expect(attributes['aria-controls']).toBe('country-of-birth-listbox'); + expect(attributes['aria-describedby']).toBe('country-of-birth-instructions'); + expect(attributes['aria-haspopup']).toBe('true'); + expect(attributes['aria-owns']).toBe('country-of-birth-listbox'); + expect(attributes['aria-expanded']).toBe('false'); + expect(attributes['role']).toBe('combobox'); + }); - describe('when the user presses the "Down" arrow key', () => { - it('navigates to the first suggestion initially', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('the autocomplete attribute be set to be not set to on', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + + const attributes = await getNodeAttributes(page, '.ons-js-autosuggest-input'); + expect(attributes['autocomplete']).not.toBe('on'); + }); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('ArrowDown'); + it('the instructions, listbox, and status should become visible', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - const selectedOption = await page.$eval('.ons-autosuggest__option--focused', (node) => node.textContent); - expect(selectedOption.trim()).toBe('United States of America'); + const instructionsDisplayStyle = await page.$eval('.ons-js-autosuggest-instructions', (node) => getComputedStyle(node).display); + expect(instructionsDisplayStyle).toBe('block'); + const listboxDisplayStyle = await page.$eval('.ons-js-autosuggest-listbox', (node) => getComputedStyle(node).display); + expect(listboxDisplayStyle).toBe('block'); + const statusDisplayStyle = await page.$eval('.ons-js-autosuggest-aria-status', (node) => getComputedStyle(node).display); + expect(statusDisplayStyle).toBe('block'); + }); }); - it('navigates to the suggestion below the current suggestion', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + describe('when the user presses the "Down" arrow key', () => { + it('navigates to the first suggestion initially', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('ArrowDown'); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('ArrowDown'); - const selectedOption = await page.$eval('.ons-autosuggest__option--focused', (node) => node.textContent); - expect(selectedOption.trim()).toBe('United States Virgin Islands'); - }); + const selectedOption = await page.$eval('.ons-autosuggest__option--focused', (node) => node.textContent); + expect(selectedOption.trim()).toBe('United States of America'); + }); - it('marks suggestion as being selected', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('navigates to the suggestion below the current suggestion', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('ArrowDown'); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); - const ariaSelectedValue = await page.$eval('.ons-autosuggest__option--focused', (node) => node.getAttribute('aria-selected')); - expect(ariaSelectedValue).toBe('true'); + const selectedOption = await page.$eval('.ons-autosuggest__option--focused', (node) => node.textContent); + expect(selectedOption.trim()).toBe('United States Virgin Islands'); + }); - const selectedOptionId = await page.$eval('.ons-autosuggest__option--focused', (node) => node.getAttribute('id')); - const ariaActiveDescendant = await page.$eval('.ons-js-autosuggest-input', (node) => node.getAttribute('aria-activedescendant')); - expect(ariaActiveDescendant).toBe(selectedOptionId); - }); + it('marks suggestion as being selected', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - it('sets aria status to a message showing the selected result', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); - await page.type('.ons-js-autosuggest-input', 'Eng', { delay: 20 }); - await page.keyboard.press('ArrowDown'); + const ariaSelectedValue = await page.$eval('.ons-autosuggest__option--focused', (node) => node.getAttribute('aria-selected')); + expect(ariaSelectedValue).toBe('true'); - const statusMessage = await page.$eval('.ons-js-autosuggest-aria-status', (node) => node.textContent); - expect(statusMessage.trim()).toBe('England'); - }); + const selectedOptionId = await page.$eval('.ons-autosuggest__option--focused', (node) => node.getAttribute('id')); + const ariaActiveDescendant = await page.$eval('.ons-js-autosuggest-input', (node) => + node.getAttribute('aria-activedescendant'), + ); + expect(ariaActiveDescendant).toBe(selectedOptionId); + }); - it('does not mark other suggestions as being selected', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('sets aria status to a message showing the selected result', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('ArrowDown'); + await page.type('.ons-js-autosuggest-input', 'Eng', { delay: 20 }); + await page.keyboard.press('ArrowDown'); - const selectedSuggestionCount = await page.$$eval('.ons-autosuggest__option[aria-selected=true]', (nodes) => nodes.length); - expect(selectedSuggestionCount).toBe(1); - }); + const statusMessage = await page.$eval('.ons-js-autosuggest-aria-status', (node) => node.textContent); + expect(statusMessage.trim()).toBe('England'); + }); - it('does not affect suggestion selection when already on last item', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('does not mark other suggestions as being selected', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('ArrowDown'); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); - const selectedOption = await page.$eval('.ons-autosuggest__option--focused', (node) => node.textContent); - expect(selectedOption.trim()).toBe('United States Virgin Islands'); - }); + const selectedSuggestionCount = await page.$$eval('.ons-autosuggest__option[aria-selected=true]', (nodes) => nodes.length); + expect(selectedSuggestionCount).toBe(1); + }); - // The down arrow will typically move caret to the end of an input field. This should - // not occur when suggestions are presented. - it('does not interfere with text input', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('does not affect suggestion selection when already on last item', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'nited', { delay: 20 }); - // Move to start of input to verify if the down arrow moves the caret to the end. - await page.keyboard.press('Home'); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('U'); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); - const inputValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.value); - expect(inputValue).toBe('United'); - }); - }); + const selectedOption = await page.$eval('.ons-autosuggest__option--focused', (node) => node.textContent); + expect(selectedOption.trim()).toBe('United States Virgin Islands'); + }); - describe('when the user presses the "Up" arrow key', () => { - it('navigates to the first suggestion initially', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + // The down arrow will typically move caret to the end of an input field. This should + // not occur when suggestions are presented. + it('does not interfere with text input', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('ArrowUp'); + await page.type('.ons-js-autosuggest-input', 'nited', { delay: 20 }); + // Move to start of input to verify if the down arrow moves the caret to the end. + await page.keyboard.press('Home'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('U'); - const selectedOption = await page.$eval('.ons-autosuggest__option--focused', (node) => node.textContent); - expect(selectedOption.trim()).toBe('United States of America'); + const inputValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.value); + expect(inputValue).toBe('United'); + }); }); - it('navigates to the suggestion above the current suggestion', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + describe('when the user presses the "Up" arrow key', () => { + it('navigates to the first suggestion initially', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('ArrowUp'); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('ArrowUp'); - const selectedOption = await page.$eval('.ons-autosuggest__option--focused', (node) => node.textContent); - expect(selectedOption.trim()).toBe('United States of America'); - }); + const selectedOption = await page.$eval('.ons-autosuggest__option--focused', (node) => node.textContent); + expect(selectedOption.trim()).toBe('United States of America'); + }); - it('marks suggestion as being selected', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('navigates to the suggestion above the current suggestion', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('ArrowUp'); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowUp'); - const ariaSelectedValue = await page.$eval('.ons-autosuggest__option--focused', (node) => node.getAttribute('aria-selected')); - expect(ariaSelectedValue).toBe('true'); + const selectedOption = await page.$eval('.ons-autosuggest__option--focused', (node) => node.textContent); + expect(selectedOption.trim()).toBe('United States of America'); + }); - const selectedOptionId = await page.$eval('.ons-autosuggest__option--focused', (node) => node.getAttribute('id')); - const ariaActiveDescendant = await page.$eval('.ons-js-autosuggest-input', (node) => node.getAttribute('aria-activedescendant')); - expect(ariaActiveDescendant).toBe(selectedOptionId); - }); + it('marks suggestion as being selected', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - it('does not mark other suggestions as being selected', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowUp'); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('ArrowUp'); + const ariaSelectedValue = await page.$eval('.ons-autosuggest__option--focused', (node) => node.getAttribute('aria-selected')); + expect(ariaSelectedValue).toBe('true'); - const selectedSuggestionCount = await page.$$eval('.ons-autosuggest__option[aria-selected=true]', (nodes) => nodes.length); - expect(selectedSuggestionCount).toBe(1); - }); + const selectedOptionId = await page.$eval('.ons-autosuggest__option--focused', (node) => node.getAttribute('id')); + const ariaActiveDescendant = await page.$eval('.ons-js-autosuggest-input', (node) => + node.getAttribute('aria-activedescendant'), + ); + expect(ariaActiveDescendant).toBe(selectedOptionId); + }); - it('does not affect suggestion selection when already on first item', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('does not mark other suggestions as being selected', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('ArrowUp'); - await page.keyboard.press('ArrowUp'); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowUp'); - const selectedOption = await page.$eval('.ons-autosuggest__option--focused', (node) => node.textContent); - expect(selectedOption.trim()).toBe('United States of America'); - }); + const selectedSuggestionCount = await page.$$eval('.ons-autosuggest__option[aria-selected=true]', (nodes) => nodes.length); + expect(selectedSuggestionCount).toBe(1); + }); + + it('does not affect suggestion selection when already on first item', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('ArrowUp'); + await page.keyboard.press('ArrowUp'); + + const selectedOption = await page.$eval('.ons-autosuggest__option--focused', (node) => node.textContent); + expect(selectedOption.trim()).toBe('United States of America'); + }); - // The down arrow will typically move caret to the start of an input field. This - // should not occur when suggestions are presented. - it('does not interfere with text input', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + // The down arrow will typically move caret to the start of an input field. This + // should not occur when suggestions are presented. + it('does not interfere with text input', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('ArrowUp'); - await page.keyboard.press('d'); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('ArrowUp'); + await page.keyboard.press('d'); - const inputValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.value); - expect(inputValue).toBe('United'); + const inputValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.value); + expect(inputValue).toBe('United'); + }); }); - }); - describe('when the user presses the "Enter" key', () => { - it('accepts the selected suggestion', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + describe('when the user presses the "Enter" key', () => { + it('accepts the selected suggestion', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('Enter'); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); - const inputValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.value); - expect(inputValue).toBe('United States Virgin Islands'); + const inputValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.value); + expect(inputValue).toBe('United States Virgin Islands'); + }); }); - }); - describe('when the user blurs the input', () => { - it('suggestions should remain visible for 300ms', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + describe('when the user blurs the input', () => { + it('suggestions should remain visible for 300ms', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('Tab'); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('Tab'); - const suggestionCountSample1 = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.length); - expect(suggestionCountSample1).toBe(2); + const suggestionCountSample1 = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.length); + expect(suggestionCountSample1).toBe(2); - await page.waitForTimeout(200); - const suggestionCountSample2 = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.length); - expect(suggestionCountSample2).toBe(2); + await page.waitForTimeout(200); + const suggestionCountSample2 = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.length); + expect(suggestionCountSample2).toBe(2); - await page.waitForTimeout(320); - const suggestionCountSample3 = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.length); - expect(suggestionCountSample3).toBe(0); - }); + await page.waitForTimeout(320); + const suggestionCountSample3 = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.length); + expect(suggestionCountSample3).toBe(0); + }); - it('clears innerHTML of listbox', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('clears innerHTML of listbox', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('Tab'); - await page.waitForTimeout(320); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('Tab'); + await page.waitForTimeout(320); - const listboxInnerHTML = await page.$eval('.ons-js-autosuggest-listbox', (node) => node.innerHTML); - expect(listboxInnerHTML).toBe(''); - }); + const listboxInnerHTML = await page.$eval('.ons-js-autosuggest-listbox', (node) => node.innerHTML); + expect(listboxInnerHTML).toBe(''); + }); - it('clears `has-results` modifier of autosuggest component', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('clears `has-results` modifier of autosuggest component', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('Tab'); - await page.waitForTimeout(320); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('Tab'); + await page.waitForTimeout(320); - const hasClass = await page.$eval('.ons-autosuggest', (node) => node.classList.contains('ons-autosuggest--has-results')); - expect(hasClass).toBe(false); - }); + const hasClass = await page.$eval('.ons-autosuggest', (node) => node.classList.contains('ons-autosuggest--has-results')); + expect(hasClass).toBe(false); + }); - it('clears aria attributes of input element', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('clears aria attributes of input element', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('Tab'); - await page.waitForTimeout(320); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('Tab'); + await page.waitForTimeout(320); - const attributes = await getNodeAttributes(page, '.ons-js-autosuggest-input'); - expect(attributes['aria-activedescendant']).toBeUndefined(); - expect(attributes['aria-expanded']).toBe('false'); + const attributes = await getNodeAttributes(page, '.ons-js-autosuggest-input'); + expect(attributes['aria-activedescendant']).toBeUndefined(); + expect(attributes['aria-expanded']).toBe('false'); + }); }); - }); - describe('when the user clicks on a result', () => { - it('accepts the suggestion', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + describe('when the user clicks on a result', () => { + it('accepts the suggestion', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.click('.ons-autosuggest__option:nth-child(2)'); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.click('.ons-autosuggest__option:nth-child(2)'); - const inputValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.value); - expect(inputValue).toBe('United States Virgin Islands'); + const inputValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.value); + expect(inputValue).toBe('United States Virgin Islands'); + }); }); - }); - describe('when the user inputs text', () => { - it('does not show suggestions when input length < minimum characters', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + describe('when the user inputs text', () => { + it('does not show suggestions when input length < minimum characters', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'En', { delay: 20 }); + await page.type('.ons-js-autosuggest-input', 'En', { delay: 20 }); - const suggestionCount = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.length); - expect(suggestionCount).toBe(0); - }); + const suggestionCount = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.length); + expect(suggestionCount).toBe(0); + }); - it('shows suggestions when input length >= minimum characters', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('shows suggestions when input length >= minimum characters', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Eng', { delay: 20 }); + await page.type('.ons-js-autosuggest-input', 'Eng', { delay: 20 }); - const suggestionCount = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.length); - expect(suggestionCount).toBe(1); - }); + const suggestionCount = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.length); + expect(suggestionCount).toBe(1); + }); - it('gets the language if set', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST_WITH_LANGUAGE)); + it('gets the language if set', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST_WITH_LANGUAGE)); - await page.type('.ons-js-autosuggest-input', 'Eng', { delay: 20 }); + await page.type('.ons-js-autosuggest-input', 'Eng', { delay: 20 }); - const attributes = await getNodeAttributes(page, '.ons-js-autosuggest'); - expect(attributes['data-lang']).toBe('en-gb'); + const attributes = await getNodeAttributes(page, '.ons-js-autosuggest'); + expect(attributes['data-lang']).toBe('en-gb'); + }); }); - }); - describe('when the mouse moves over a result and a suggestion is focused', () => { - it('removes the focused class', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + describe('when the mouse moves over a result and a suggestion is focused', () => { + it('removes the focused class', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'state', { delay: 20 }); - await page.keyboard.press('ArrowDown'); - await page.hover('.ons-autosuggest__option:nth-child(2)'); + await page.type('.ons-js-autosuggest-input', 'state', { delay: 20 }); + await page.keyboard.press('ArrowDown'); + await page.hover('.ons-autosuggest__option:nth-child(2)'); - const focusedClassCount = await page.$$eval('.ons-autosuggest__option--focused', (nodes) => nodes.length); - expect(focusedClassCount).toBe(0); - }); - }); - - describe('when the mouse moves off a result and a suggestion was focused', () => { - it('restores the focused class', async () => { - await setTestPage( - '/test', - ` - ${renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)} - Dummy - `, - ); - - await page.type('.ons-js-autosuggest-input', 'state', { delay: 20 }); - await page.keyboard.press('ArrowDown'); - await page.hover('.ons-autosuggest__option:nth-child(2)'); - await page.hover('#dummy'); - - const focusedClassCount = await page.$$eval('.ons-autosuggest__option--focused', (nodes) => nodes.length); - expect(focusedClassCount).toBe(1); + const focusedClassCount = await page.$$eval('.ons-autosuggest__option--focused', (nodes) => nodes.length); + expect(focusedClassCount).toBe(0); + }); }); - }); - describe('when there are results', () => { - it('un-highlights a previously highlighted suggestion', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + describe('when the mouse moves off a result and a suggestion was focused', () => { + it('restores the focused class', async () => { + await setTestPage( + '/test', + ` + ${renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)} + Dummy + `, + ); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - await page.keyboard.press('ArrowDown'); - await page.type('.ons-js-autosuggest-input', 'd', { delay: 20 }); + await page.type('.ons-js-autosuggest-input', 'state', { delay: 20 }); + await page.keyboard.press('ArrowDown'); + await page.hover('.ons-autosuggest__option:nth-child(2)'); + await page.hover('#dummy'); - const focusedNodeCount = await page.$$eval('.ons-autosuggest__option--focused', (nodes) => nodes.length); - expect(focusedNodeCount).toBe(0); + const focusedClassCount = await page.$$eval('.ons-autosuggest__option--focused', (nodes) => nodes.length); + expect(focusedClassCount).toBe(1); + }); }); - it('decorates input element with `aria-expanded` attribute', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + describe('when there are results', () => { + it('un-highlights a previously highlighted suggestion', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.keyboard.press('ArrowDown'); + await page.type('.ons-js-autosuggest-input', 'd', { delay: 20 }); - const ariaExpandedValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.getAttribute('aria-expanded')); - expect(ariaExpandedValue).toBe('true'); - }); + const focusedNodeCount = await page.$$eval('.ons-autosuggest__option--focused', (nodes) => nodes.length); + expect(focusedNodeCount).toBe(0); + }); - it('emboldens matched suggestion text', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('decorates input element with `aria-expanded` attribute', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - const emboldened = await page.$$eval('.ons-autosuggest__option strong', (nodes) => nodes.map((node) => node.textContent)); - expect(emboldened).toEqual(['Unite', 'Unite']); - }); + const ariaExpandedValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.getAttribute('aria-expanded')); + expect(ariaExpandedValue).toBe('true'); + }); - it('does not embolden anything when suggestion does not contain query text', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('emboldens matched suggestion text', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'tland', { delay: 20 }); + await page.type('.ons-js-autosuggest-input', 'Unite', { delay: 20 }); - const matchCount = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.length); - expect(matchCount).toBe(2); - const emboldened = await page.$$eval('.ons-autosuggest__option strong', (nodes) => nodes.map((node) => node.textContent)); - expect(emboldened).toEqual(['tland']); - }); + const emboldened = await page.$$eval('.ons-autosuggest__option strong', (nodes) => nodes.map((node) => node.textContent)); + expect(emboldened).toEqual(['Unite', 'Unite']); + }); - it('sets aria status to a message asking for more characters to be input', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('does not embolden anything when suggestion does not contain query text', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'st', { delay: 20 }); + await page.type('.ons-js-autosuggest-input', 'tland', { delay: 20 }); - const statusMessage = await page.$eval('.ons-js-autosuggest-aria-status', (node) => node.textContent); - expect(statusMessage.trim()).toBe('Enter 3 or more characters for suggestions.'); - }); + const matchCount = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.length); + expect(matchCount).toBe(2); + const emboldened = await page.$$eval('.ons-autosuggest__option strong', (nodes) => nodes.map((node) => node.textContent)); + expect(emboldened).toEqual(['tland']); + }); - it('sets aria status to a message indicating a count of one suggestion', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('sets aria status to a message asking for more characters to be input', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'Engla', { delay: 20 }); + await page.type('.ons-js-autosuggest-input', 'st', { delay: 20 }); - const statusMessage = await page.$eval('.ons-js-autosuggest-aria-status', (node) => node.textContent); - expect(statusMessage.trim()).toBe('There is one suggestion available.'); - }); + const statusMessage = await page.$eval('.ons-js-autosuggest-aria-status', (node) => node.textContent); + expect(statusMessage.trim()).toBe('Enter 3 or more characters for suggestions.'); + }); + + it('sets aria status to a message indicating a count of one suggestion', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + + await page.type('.ons-js-autosuggest-input', 'Engla', { delay: 20 }); - it('sets aria status to a message indicating the count of suggestions', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + const statusMessage = await page.$eval('.ons-js-autosuggest-aria-status', (node) => node.textContent); + expect(statusMessage.trim()).toBe('There is one suggestion available.'); + }); + + it('sets aria status to a message indicating the count of suggestions', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'sta', { delay: 20 }); + await page.type('.ons-js-autosuggest-input', 'sta', { delay: 20 }); - const statusMessage = await page.$eval('.ons-js-autosuggest-aria-status', (node) => node.textContent); - expect(statusMessage.trim()).toBe('There are 2 suggestions available.'); + const statusMessage = await page.$eval('.ons-js-autosuggest-aria-status', (node) => node.textContent); + expect(statusMessage.trim()).toBe('There are 2 suggestions available.'); + }); }); - }); - describe('when there are no results', () => { - describe('where `noResults` content is provided', () => { - it('outputs `noResults` content', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + describe('when there are no results', () => { + describe('where `noResults` content is provided', () => { + it('outputs `noResults` content', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'abc', { delay: 20 }); + await page.type('.ons-js-autosuggest-input', 'abc', { delay: 20 }); - const noResultsContent = await page.$eval('.ons-autosuggest__option--no-results', (node) => node.textContent); - expect(noResultsContent).toBe('No suggestions found.'); - }); + const noResultsContent = await page.$eval('.ons-autosuggest__option--no-results', (node) => node.textContent); + expect(noResultsContent).toBe('No suggestions found.'); + }); - it('decorates input element with `aria-expanded` attribute', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('decorates input element with `aria-expanded` attribute', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'abc', { delay: 20 }); + await page.type('.ons-js-autosuggest-input', 'abc', { delay: 20 }); - const ariaExpandedValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.getAttribute('aria-expanded')); - expect(ariaExpandedValue).toBe('true'); - }); + const ariaExpandedValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.getAttribute('aria-expanded')); + expect(ariaExpandedValue).toBe('true'); + }); - it('sets aria status to a message indicating that query is too short for suggestions', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('sets aria status to a message indicating that query is too short for suggestions', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'ab', { delay: 20 }); + await page.type('.ons-js-autosuggest-input', 'ab', { delay: 20 }); - const statusMessage = await page.$eval('.ons-js-autosuggest-aria-status', (node) => node.textContent); - expect(statusMessage.trim()).toBe('Enter 3 or more characters for suggestions.'); - }); + const statusMessage = await page.$eval('.ons-js-autosuggest-aria-status', (node) => node.textContent); + expect(statusMessage.trim()).toBe('Enter 3 or more characters for suggestions.'); + }); - it('sets aria status to a message indicating the zero count of suggestions', async () => { - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + it('sets aria status to a message indicating the zero count of suggestions', async () => { + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.type('.ons-js-autosuggest-input', 'abc', { delay: 20 }); + await page.type('.ons-js-autosuggest-input', 'abc', { delay: 20 }); - const statusMessage = await page.$eval('.ons-js-autosuggest-aria-status', (node) => node.textContent); - expect(statusMessage.trim()).toBe('No suggestions found.: "abc"'); - }); - }); + const statusMessage = await page.$eval('.ons-js-autosuggest-aria-status', (node) => node.textContent); + expect(statusMessage.trim()).toBe('No suggestions found.: "abc"'); + }); + }); - describe('where `noResults` content is not provided', () => { - it('has an empty listbox', async () => { - await setTestPage( - '/test', - renderComponent('autosuggest', { - ...EXAMPLE_AUTOSUGGEST, - noResults: undefined, - }), - ); - - await page.type('.ons-js-autosuggest-input', 'abc', { delay: 20 }); - - const matchCount = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.length); - expect(matchCount).toBe(0); - }); - - it('decorates input element with `aria-expanded` attribute', async () => { - await setTestPage( - '/test', - renderComponent('autosuggest', { - ...EXAMPLE_AUTOSUGGEST, - noResults: undefined, - }), - ); - - await page.type('.ons-js-autosuggest-input', 'abc', { delay: 20 }); - - const ariaExpandedValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.getAttribute('aria-expanded')); - expect(ariaExpandedValue).toBe('false'); - }); + describe('where `noResults` content is not provided', () => { + it('has an empty listbox', async () => { + await setTestPage( + '/test', + renderComponent('autosuggest', { + ...EXAMPLE_AUTOSUGGEST, + noResults: undefined, + }), + ); + + await page.type('.ons-js-autosuggest-input', 'abc', { delay: 20 }); + + const matchCount = await page.$$eval('.ons-autosuggest__option', (nodes) => nodes.length); + expect(matchCount).toBe(0); + }); + + it('decorates input element with `aria-expanded` attribute', async () => { + await setTestPage( + '/test', + renderComponent('autosuggest', { + ...EXAMPLE_AUTOSUGGEST, + noResults: undefined, + }), + ); + + await page.type('.ons-js-autosuggest-input', 'abc', { delay: 20 }); + + const ariaExpandedValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.getAttribute('aria-expanded')); + expect(ariaExpandedValue).toBe('false'); + }); + }); }); - }); - describe('when there are no results due to an error', () => { - describe('when the status code is 400', () => { - beforeEach(async () => { - apiFaker.setTemporaryOverride('/countries', { - status: 400, - data: {}, - }); + describe('when there are no results due to an error', () => { + describe('when the status code is 400', () => { + beforeEach(async () => { + apiFaker.setTemporaryOverride('/countries', { + status: 400, + data: {}, + }); - await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); + await setTestPage('/test', renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST)); - await page.focus('.ons-js-autosuggest-input'); - await page.type('.ons-js-autosuggest-input', 'tes', { delay: 20 }); - }); + await page.focus('.ons-js-autosuggest-input'); + await page.type('.ons-js-autosuggest-input', 'tes', { delay: 20 }); + }); - it('shows the type more message', async () => { - const listItemCount = await page.$$eval('.ons-js-autosuggest-listbox > *', (nodes) => nodes.length); - expect(listItemCount).toBe(1); - const noResultsText = await page.$eval('.ons-autosuggest__option--no-results', (node) => node.innerText); - expect(noResultsText.trim()).toBe('Continue entering to get suggestions'); - }); - }); + it('shows the type more message', async () => { + const listItemCount = await page.$$eval('.ons-js-autosuggest-listbox > *', (nodes) => nodes.length); + expect(listItemCount).toBe(1); + const noResultsText = await page.$eval('.ons-autosuggest__option--no-results', (node) => node.innerText); + expect(noResultsText.trim()).toBe('Continue entering to get suggestions'); + }); + }); - describe.each([ - ['when the status code is greater than 400', {}, 401], - ['when there is no status code', null, undefined], - ])('%s', (_, fakeAutosuggestData, fakeStatusCode) => { - beforeEach(async () => { - apiFaker.setTemporaryOverride('/countries', { - status: fakeStatusCode, - data: fakeAutosuggestData, - }); - - await setTestPage( - '/test', - renderComponent('autosuggest', { - ...EXAMPLE_AUTOSUGGEST, - errorTitle: 'There is a problem with your answer', - errorMessage: 'Enter an address ', - errorMessageAPI: 'Sorry, there is a problem.', - }), - ); - - await page.type('.ons-js-autosuggest-input', 'tes', { delay: 20 }); - }); - - it('shows the API error message', async () => { - const resultsItemCount = await page.$$eval('.ons-js-autosuggest-results > *', (nodes) => nodes.length); - expect(resultsItemCount).toBe(1); - const warningText = await page.$eval('.ons-autosuggest__warning', (node) => node.textContent); - expect(warningText.trim()).toBe('!Sorry, there is a problem.'); - }); - - it('the list and results element should be removed from the page', async () => { - const hasListBox = await page.$eval('.ons-autosuggest', (node) => node.classList.contains('.ons-js-autosuggest-listbox')); - const hasResultsTitle = await page.$eval('.ons-autosuggest', (node) => node.classList.contains('.ons-autosuggest__results-title')); - - expect(hasListBox).toBe(false); - expect(hasResultsTitle).toBe(false); - }); - - it('the input should be disabled', async () => { - const hasDisabledAttribute = await page.$eval('.ons-js-autosuggest-input', (node) => node.hasAttribute('disabled')); - expect(hasDisabledAttribute).toBe(true); - }); - - it('the input value should be empty', async () => { - const inputValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.value); - expect(inputValue).toBe(''); - }); - - it('the label class should be added', async () => { - const hasClass = await page.$eval('.ons-label', (node) => node.classList.contains('ons-u-lighter')); - expect(hasClass).toBe(true); - }); - - it('the aria status should be set', async () => { - const statusMessage = await page.$eval('.ons-js-autosuggest-aria-status', (node) => node.textContent); - expect(statusMessage.trim()).toBe('Sorry, there is a problem.'); - }); - }); - }); - - describe('when the component initialises with the allowMultiple parameter', () => { - describe('when a result is selected', () => { - it('the input value should contain a comma when focused', async () => { - await setTestPage( - '/test', - renderComponent('autosuggest', { - ...EXAMPLE_AUTOSUGGEST, - allowMultiple: true, - }), - ); - - await page.type('.ons-js-autosuggest-input', 'England', { delay: 20 }); - await page.keyboard.press('ArrowUp'); - await page.keyboard.press('Enter'); - // Defocus the autosuggest input. - await page.keyboard.press('Tab'); - await page.focus('.ons-js-autosuggest-input'); - - const inputValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.value); - expect(inputValue).toBe('England, '); - }); + describe.each([ + ['when the status code is greater than 400', {}, 401], + ['when there is no status code', null, undefined], + ])('%s', (_, fakeAutosuggestData, fakeStatusCode) => { + beforeEach(async () => { + apiFaker.setTemporaryOverride('/countries', { + status: fakeStatusCode, + data: fakeAutosuggestData, + }); + + await setTestPage( + '/test', + renderComponent('autosuggest', { + ...EXAMPLE_AUTOSUGGEST, + errorTitle: 'There is a problem with your answer', + errorMessage: 'Enter an address ', + errorMessageAPI: 'Sorry, there is a problem.', + }), + ); + + await page.type('.ons-js-autosuggest-input', 'tes', { delay: 20 }); + }); + + it('shows the API error message', async () => { + const resultsItemCount = await page.$$eval('.ons-js-autosuggest-results > *', (nodes) => nodes.length); + expect(resultsItemCount).toBe(1); + const warningText = await page.$eval('.ons-autosuggest__warning', (node) => node.textContent); + expect(warningText.trim()).toBe('!Sorry, there is a problem.'); + }); + + it('the list and results element should be removed from the page', async () => { + const hasListBox = await page.$eval('.ons-autosuggest', (node) => node.classList.contains('.ons-js-autosuggest-listbox')); + const hasResultsTitle = await page.$eval('.ons-autosuggest', (node) => + node.classList.contains('.ons-autosuggest__results-title'), + ); + + expect(hasListBox).toBe(false); + expect(hasResultsTitle).toBe(false); + }); + + it('the input should be disabled', async () => { + const hasDisabledAttribute = await page.$eval('.ons-js-autosuggest-input', (node) => node.hasAttribute('disabled')); + expect(hasDisabledAttribute).toBe(true); + }); + + it('the input value should be empty', async () => { + const inputValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.value); + expect(inputValue).toBe(''); + }); + + it('the label class should be added', async () => { + const hasClass = await page.$eval('.ons-label', (node) => node.classList.contains('ons-u-lighter')); + expect(hasClass).toBe(true); + }); + + it('the aria status should be set', async () => { + const statusMessage = await page.$eval('.ons-js-autosuggest-aria-status', (node) => node.textContent); + expect(statusMessage.trim()).toBe('Sorry, there is a problem.'); + }); + }); }); - describe('when the user blurs the input', () => { - it('the input value should not contain a comma', async () => { - await setTestPage( - '/test', - renderComponent('autosuggest', { - ...EXAMPLE_AUTOSUGGEST, - allowMultiple: true, - }), - ); - - await page.type('.ons-js-autosuggest-input', 'England, ', { delay: 20 }); - await page.keyboard.press('Tab'); - - const inputValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.value); - expect(inputValue).toBe('England'); - }); + describe('when the component initialises with the allowMultiple parameter', () => { + describe('when a result is selected', () => { + it('the input value should contain a comma when focused', async () => { + await setTestPage( + '/test', + renderComponent('autosuggest', { + ...EXAMPLE_AUTOSUGGEST, + allowMultiple: true, + }), + ); + + await page.type('.ons-js-autosuggest-input', 'England', { delay: 20 }); + await page.keyboard.press('ArrowUp'); + await page.keyboard.press('Enter'); + // Defocus the autosuggest input. + await page.keyboard.press('Tab'); + await page.focus('.ons-js-autosuggest-input'); + + const inputValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.value); + expect(inputValue).toBe('England, '); + }); + }); + + describe('when the user blurs the input', () => { + it('the input value should not contain a comma', async () => { + await setTestPage( + '/test', + renderComponent('autosuggest', { + ...EXAMPLE_AUTOSUGGEST, + allowMultiple: true, + }), + ); + + await page.type('.ons-js-autosuggest-input', 'England, ', { delay: 20 }); + await page.keyboard.press('Tab'); + + const inputValue = await page.$eval('.ons-js-autosuggest-input', (node) => node.value); + expect(inputValue).toBe('England'); + }); + }); }); - }); }); diff --git a/src/components/autosuggest/autosuggest.ui.js b/src/components/autosuggest/autosuggest.ui.js index 95f1943a82..c91c7b826f 100644 --- a/src/components/autosuggest/autosuggest.ui.js +++ b/src/components/autosuggest/autosuggest.ui.js @@ -12,541 +12,544 @@ export const classAutosuggestHasResults = 'ons-autosuggest--has-results'; export const classAutosuggestResultsTitle = 'ons-autosuggest__results-title'; export default class AutosuggestUI { - constructor({ - context, - autosuggestData, - sanitisedQueryReplaceChars, - sanitisedQuerySplitNumsChars, - minChars, - resultLimit, - suggestOnBoot, - onSelect, - onUnsetResult, - suggestionFunction, - handleUpdate, - ariaYouHaveSelected, - ariaMinChars, - ariaOneResult, - ariaNResults, - ariaLimitedResults, - ariaGroupedResults, - moreResults, - resultsTitle, - noResults, - tooManyResults, - errorAPI, - errorAPILinkText, - typeMore, - }) { - // DOM Elements - this.context = context; - this.input = context.querySelector(`.${baseClass}-input`); - this.resultsContainer = context.querySelector(`.${baseClass}-results`); - this.listbox = this.resultsContainer.querySelector(`.${baseClass}-listbox`); - this.resultsTitleContainer = this.resultsContainer.querySelector(`.ons-autosuggest__results-title`); - this.instructions = context.querySelector(`.${baseClass}-instructions`); - this.ariaStatus = context.querySelector(`.${baseClass}-aria-status`); - this.form = context.closest('form'); - this.label = document.querySelector('.ons-label'); - - // Settings - this.autosuggestData = autosuggestData || context.getAttribute('data-autosuggest-data'); - this.ariaYouHaveSelected = ariaYouHaveSelected || context.getAttribute('data-aria-you-have-selected'); - this.minChars = minChars || context.getAttribute('data-min-chars') || 3; - this.ariaMinChars = ariaMinChars || context.getAttribute('data-aria-min-chars'); - this.ariaOneResult = ariaOneResult || context.getAttribute('data-aria-one-result'); - this.ariaNResults = ariaNResults || context.getAttribute('data-aria-n-results'); - this.ariaLimitedResults = ariaLimitedResults || context.getAttribute('data-aria-limited-results'); - this.ariaGroupedResults = ariaGroupedResults || context.getAttribute('data-aria-grouped-results'); - this.moreResults = moreResults || context.getAttribute('data-more-results'); - this.resultsTitle = resultsTitle || context.getAttribute('data-results-title'); - this.noResults = noResults || context.getAttribute('data-no-results'); - this.tooManyResults = tooManyResults || context.getAttribute('data-too-many-results'); - this.errorAPI = errorAPI || context.getAttribute('data-error-api'); - this.errorAPILinkText = errorAPILinkText || context.getAttribute('data-error-api-link-text'); - this.typeMore = typeMore || context.getAttribute('data-type-more'); - this.language = context.getAttribute('data-lang'); - this.allowMultiple = context.getAttribute('data-allow-multiple') || false; - this.listboxId = this.listbox.getAttribute('id'); - this.resultLimit = resultLimit || 10; - this.suggestOnBoot = suggestOnBoot; - - // Callbacks - this.onSelect = onSelect; - this.onUnsetResult = onUnsetResult; - this.handleUpdate = handleUpdate; - - if (suggestionFunction) { - this.fetchSuggestions = suggestionFunction; - } else { - this.fetchData(); - } - - // State - this.ctrlKey = false; - this.deleting = false; - this.query = ''; - this.sanitisedQuery = ''; - this.previousQuery = ''; - this.results = []; - this.resultOptions = []; - this.allSelections = []; - this.data = []; - this.foundResults = 0; - this.numberOfResults = 0; - this.highlightedResultIndex = 0; - this.settingResult = false; - this.resultSelected = false; - this.blurring = false; - this.blurTimeout = null; - this.sanitisedQueryReplaceChars = sanitisedQueryReplaceChars || []; - this.sanitisedQuerySplitNumsChars = sanitisedQuerySplitNumsChars || false; - - this.initialiseUI(); - } - - get lang() { - return !this.language ? document.documentElement.getAttribute('lang').toLowerCase() : this.language.toLowerCase(); - } - - initialiseUI() { - this.input.setAttribute('aria-autocomplete', 'list'); - this.input.setAttribute('aria-controls', this.listbox.getAttribute('id')); - this.input.setAttribute('aria-describedby', this.instructions.getAttribute('id')); - this.input.setAttribute('aria-haspopup', true); - this.input.setAttribute('aria-owns', this.listbox.getAttribute('id')); - this.input.setAttribute('aria-expanded', false); - this.input.setAttribute('autocomplete', this.input.getAttribute('autocomplete') || 'off'); - this.input.setAttribute('role', 'combobox'); - - this.context.classList.add('ons-autosuggest--initialised'); - - this.bindEventListeners(); - } - - async fetchData() { - this.fetch = abortableFetch(this.autosuggestData); - const response = await this.fetch.send(); - this.data = await response.json(); - this.responseStatus = response.status; - } - - bindEventListeners() { - this.input.addEventListener('keydown', this.handleKeydown.bind(this)); - this.input.addEventListener('keyup', this.handleKeyup.bind(this)); - this.input.addEventListener('input', this.handleChange.bind(this)); - this.input.addEventListener('focus', this.handleFocus.bind(this)); - this.input.addEventListener('blur', this.handleBlur.bind(this)); - - this.listbox.addEventListener('mouseover', this.handleMouseover.bind(this)); - this.listbox.addEventListener('mouseout', this.handleMouseout.bind(this)); - } - - handleKeydown(event) { - this.ctrlKey = (event.ctrlKey || event.metaKey) && event.key !== 'v'; - - switch (event.keyCode) { - case 38: { - event.preventDefault(); - this.navigateResults(-1); - break; - } - case 40: { - event.preventDefault(); - this.navigateResults(1); - break; - } - case 13: { - if (this.highlightedResultIndex !== null) { - event.preventDefault(); - } - break; - } - } - } - - handleKeyup(event) { - switch (event.keyCode) { - case 40: - case 38: { - event.preventDefault(); - break; - } - case 13: { - if (this.highlightedResultIndex !== null) { - this.selectResult(); + constructor({ + context, + autosuggestData, + sanitisedQueryReplaceChars, + sanitisedQuerySplitNumsChars, + minChars, + resultLimit, + suggestOnBoot, + onSelect, + onUnsetResult, + suggestionFunction, + handleUpdate, + ariaYouHaveSelected, + ariaMinChars, + ariaOneResult, + ariaNResults, + ariaLimitedResults, + ariaGroupedResults, + moreResults, + resultsTitle, + noResults, + tooManyResults, + errorAPI, + errorAPILinkText, + typeMore, + }) { + // DOM Elements + this.context = context; + this.input = context.querySelector(`.${baseClass}-input`); + this.resultsContainer = context.querySelector(`.${baseClass}-results`); + this.listbox = this.resultsContainer.querySelector(`.${baseClass}-listbox`); + this.resultsTitleContainer = this.resultsContainer.querySelector(`.ons-autosuggest__results-title`); + this.instructions = context.querySelector(`.${baseClass}-instructions`); + this.ariaStatus = context.querySelector(`.${baseClass}-aria-status`); + this.form = context.closest('form'); + this.label = document.querySelector('.ons-label'); + + // Settings + this.autosuggestData = autosuggestData || context.getAttribute('data-autosuggest-data'); + this.ariaYouHaveSelected = ariaYouHaveSelected || context.getAttribute('data-aria-you-have-selected'); + this.minChars = minChars || context.getAttribute('data-min-chars') || 3; + this.ariaMinChars = ariaMinChars || context.getAttribute('data-aria-min-chars'); + this.ariaOneResult = ariaOneResult || context.getAttribute('data-aria-one-result'); + this.ariaNResults = ariaNResults || context.getAttribute('data-aria-n-results'); + this.ariaLimitedResults = ariaLimitedResults || context.getAttribute('data-aria-limited-results'); + this.ariaGroupedResults = ariaGroupedResults || context.getAttribute('data-aria-grouped-results'); + this.moreResults = moreResults || context.getAttribute('data-more-results'); + this.resultsTitle = resultsTitle || context.getAttribute('data-results-title'); + this.noResults = noResults || context.getAttribute('data-no-results'); + this.tooManyResults = tooManyResults || context.getAttribute('data-too-many-results'); + this.errorAPI = errorAPI || context.getAttribute('data-error-api'); + this.errorAPILinkText = errorAPILinkText || context.getAttribute('data-error-api-link-text'); + this.typeMore = typeMore || context.getAttribute('data-type-more'); + this.language = context.getAttribute('data-lang'); + this.allowMultiple = context.getAttribute('data-allow-multiple') || false; + this.listboxId = this.listbox.getAttribute('id'); + this.resultLimit = resultLimit || 10; + this.suggestOnBoot = suggestOnBoot; + + // Callbacks + this.onSelect = onSelect; + this.onUnsetResult = onUnsetResult; + this.handleUpdate = handleUpdate; + + if (suggestionFunction) { + this.fetchSuggestions = suggestionFunction; + } else { + this.fetchData(); } - break; - } - } - this.ctrlKey = false; - } - - handleChange(groupResults = false) { - if ((!this.blurring && this.input.value.trim()) || groupResults === true) { - this.settingResult = false; - if (groupResults === true) { - this.getSuggestions(false, groupResults); - } else { - this.getSuggestions(); - } - } else { - this.abortFetch(); - this.clearListbox(); + // State + this.ctrlKey = false; + this.deleting = false; + this.query = ''; + this.sanitisedQuery = ''; + this.previousQuery = ''; + this.results = []; + this.resultOptions = []; + this.allSelections = []; + this.data = []; + this.foundResults = 0; + this.numberOfResults = 0; + this.highlightedResultIndex = 0; + this.settingResult = false; + this.resultSelected = false; + this.blurring = false; + this.blurTimeout = null; + this.sanitisedQueryReplaceChars = sanitisedQueryReplaceChars || []; + this.sanitisedQuerySplitNumsChars = sanitisedQuerySplitNumsChars || false; + + this.initialiseUI(); } - } - handleFocus() { - if (this.allowMultiple === 'true' && this.allSelections.length && this.input.value.slice(-1) !== ' ' && this.input.value !== '') { - this.input.value = `${this.input.value}, `; + get lang() { + return !this.language ? document.documentElement.getAttribute('lang').toLowerCase() : this.language.toLowerCase(); } - } - handleBlur() { - clearTimeout(this.blurTimeout); - this.blurring = true; + initialiseUI() { + this.input.setAttribute('aria-autocomplete', 'list'); + this.input.setAttribute('aria-controls', this.listbox.getAttribute('id')); + this.input.setAttribute('aria-describedby', this.instructions.getAttribute('id')); + this.input.setAttribute('aria-haspopup', true); + this.input.setAttribute('aria-owns', this.listbox.getAttribute('id')); + this.input.setAttribute('aria-expanded', false); + this.input.setAttribute('autocomplete', this.input.getAttribute('autocomplete') || 'off'); + this.input.setAttribute('role', 'combobox'); - this.blurTimeout = setTimeout(() => { - this.blurring = false; - if (!this.settingResult) { - this.clearListbox(); - } - }, 300); + this.context.classList.add('ons-autosuggest--initialised'); - if (this.allowMultiple === 'true' && this.input.value.slice(-2) === ', ') { - this.input.value = this.input.value.slice(0, -2); + this.bindEventListeners(); } - } - - checkCharCount() { - if (this.input.value.length > 1 && this.input.value.length < this.minChars) { - this.inputTimeout = setTimeout(() => { - this.handleNoResults(false); - }, 2000); - } else { - clearTimeout(this.inputTimeout); + + async fetchData() { + this.fetch = abortableFetch(this.autosuggestData); + const response = await this.fetch.send(); + this.data = await response.json(); + this.responseStatus = response.status; } - } - handleMouseover() { - const focusedItem = this.resultOptions[this.highlightedResultIndex]; + bindEventListeners() { + this.input.addEventListener('keydown', this.handleKeydown.bind(this)); + this.input.addEventListener('keyup', this.handleKeyup.bind(this)); + this.input.addEventListener('input', this.handleChange.bind(this)); + this.input.addEventListener('focus', this.handleFocus.bind(this)); + this.input.addEventListener('blur', this.handleBlur.bind(this)); + + this.listbox.addEventListener('mouseover', this.handleMouseover.bind(this)); + this.listbox.addEventListener('mouseout', this.handleMouseout.bind(this)); + } - if (focusedItem) { - focusedItem.classList.remove(classAutosuggestOptionFocused); + handleKeydown(event) { + this.ctrlKey = (event.ctrlKey || event.metaKey) && event.key !== 'v'; + + switch (event.keyCode) { + case 38: { + event.preventDefault(); + this.navigateResults(-1); + break; + } + case 40: { + event.preventDefault(); + this.navigateResults(1); + break; + } + case 13: { + if (this.highlightedResultIndex !== null) { + event.preventDefault(); + } + break; + } + } } - } - handleMouseout() { - const focusedItem = this.resultOptions[this.highlightedResultIndex]; + handleKeyup(event) { + switch (event.keyCode) { + case 40: + case 38: { + event.preventDefault(); + break; + } + case 13: { + if (this.highlightedResultIndex !== null) { + this.selectResult(); + } + break; + } + } + + this.ctrlKey = false; + } - if (focusedItem) { - focusedItem.classList.add(classAutosuggestOptionFocused); + handleChange(groupResults = false) { + if ((!this.blurring && this.input.value.trim()) || groupResults === true) { + this.settingResult = false; + if (groupResults === true) { + this.getSuggestions(false, groupResults); + } else { + this.getSuggestions(); + } + } else { + this.abortFetch(); + this.clearListbox(); + } } - } - navigateResults(direction) { - let index = 0; - if (this.highlightedResultIndex !== null) { - index = this.highlightedResultIndex + direction; + handleFocus() { + if (this.allowMultiple === 'true' && this.allSelections.length && this.input.value.slice(-1) !== ' ' && this.input.value !== '') { + this.input.value = `${this.input.value}, `; + } } - if (index < this.numberOfResults) { - if (index < 0) { - index = null; - } + handleBlur() { + clearTimeout(this.blurTimeout); + this.blurring = true; + + this.blurTimeout = setTimeout(() => { + this.blurring = false; + if (!this.settingResult) { + this.clearListbox(); + } + }, 300); - this.setHighlightedResult(index); + if (this.allowMultiple === 'true' && this.input.value.slice(-2) === ', ') { + this.input.value = this.input.value.slice(0, -2); + } } - } - - getSuggestions(force, groupResults) { - if (!this.settingResult) { - if (this.allowMultiple === 'true' && this.allSelections.length) { - const newQuery = this.input.value.split(', ').find((item) => !this.allSelections.includes(item)); - this.query = newQuery ? newQuery : this.input.value; - } else { - this.query = this.input.value; - } - - const sanitisedQuery = sanitiseAutosuggestText(this.query, this.sanitisedQueryReplaceChars, this.sanitisedQuerySplitNumsChars); - - if (sanitisedQuery !== this.sanitisedQuery || (force && !this.resultSelected)) { - this.sanitisedQuery = sanitisedQuery; - this.unsetResults(); - this.checkCharCount(); - if (this.sanitisedQuery.length >= this.minChars) { - this.fetchSuggestions(this.sanitisedQuery, this.data, groupResults) - .then(this.handleResults.bind(this)) - .catch((error) => { - if (error.name !== 'AbortError') { - console.log('error:', error); - this.handleNoResults(500); - } - }); + + checkCharCount() { + if (this.input.value.length > 1 && this.input.value.length < this.minChars) { + this.inputTimeout = setTimeout(() => { + this.handleNoResults(false); + }, 2000); } else { - this.clearListbox(); + clearTimeout(this.inputTimeout); } - } } - } - - async fetchSuggestions(sanitisedQuery, data) { - this.abortFetch(); - const results = await runFuse(sanitisedQuery, data, this.lang, this.resultLimit); - results.forEach((result) => { - result.sanitisedText = sanitiseAutosuggestText(result[this.lang], this.sanitisedQueryReplaceChars); - }); - return { - status: this.responseStatus, - results, - totalResults: results.length, - }; - } - - abortFetch() { - if (this.fetch && this.fetch.status !== 'DONE') { - this.fetch.abort(); + + handleMouseover() { + const focusedItem = this.resultOptions[this.highlightedResultIndex]; + + if (focusedItem) { + focusedItem.classList.remove(classAutosuggestOptionFocused); + } } - } - unsetResults() { - this.results = []; - this.resultOptions = []; - this.resultSelected = false; + handleMouseout() { + const focusedItem = this.resultOptions[this.highlightedResultIndex]; - if (this.onUnsetResult) { - this.onUnsetResult(); + if (focusedItem) { + focusedItem.classList.add(classAutosuggestOptionFocused); + } } - } - clearListbox(preventAriaStatusUpdate) { - this.listbox.innerHTML = ''; - this.context.classList.remove(classAutosuggestHasResults); - this.input.removeAttribute('aria-activedescendant'); - this.input.setAttribute('aria-expanded', false); + navigateResults(direction) { + let index = 0; + if (this.highlightedResultIndex !== null) { + index = this.highlightedResultIndex + direction; + } + + if (index < this.numberOfResults) { + if (index < 0) { + index = null; + } - if (!preventAriaStatusUpdate) { - this.setAriaStatus(); + this.setHighlightedResult(index); + } } - } - handleResults(result) { - this.resultLimit = result.limit ? result.limit : this.resultLimit; - this.foundResults = result.totalResults; - if (this.foundResults > this.resultLimit) { - result.results = result.results.slice(0, this.resultLimit); + getSuggestions(force, groupResults) { + if (!this.settingResult) { + if (this.allowMultiple === 'true' && this.allSelections.length) { + const newQuery = this.input.value.split(', ').find((item) => !this.allSelections.includes(item)); + this.query = newQuery ? newQuery : this.input.value; + } else { + this.query = this.input.value; + } + + const sanitisedQuery = sanitiseAutosuggestText(this.query, this.sanitisedQueryReplaceChars, this.sanitisedQuerySplitNumsChars); + + if (sanitisedQuery !== this.sanitisedQuery || (force && !this.resultSelected)) { + this.sanitisedQuery = sanitisedQuery; + this.unsetResults(); + this.checkCharCount(); + if (this.sanitisedQuery.length >= this.minChars) { + this.fetchSuggestions(this.sanitisedQuery, this.data, groupResults) + .then(this.handleResults.bind(this)) + .catch((error) => { + if (error.name !== 'AbortError') { + console.log('error:', error); + this.handleNoResults(500); + } + }); + } else { + this.clearListbox(); + } + } + } } - this.results = result.results; - this.numberOfResults = this.results ? Math.max(this.results.length, 0) : 0; - this.setAriaStatus(); - if (!this.deleting || (this.numberOfResults && this.deleting)) { - this.listbox.innerHTML = ''; - if (this.results) { - this.resultOptions = this.results.map((result, index) => { - let innerHTML = this.emboldenMatch(result[this.lang], this.query); - - const listElement = document.createElement('li'); - listElement.className = classAutosuggestOption; - listElement.setAttribute('id', `${this.listboxId}__option--${index}`); - listElement.setAttribute('role', 'option'); - if (result.category) { - innerHTML = innerHTML + `${result.category}`; - } - listElement.innerHTML = innerHTML; - listElement.addEventListener('click', () => { - this.selectResult(index); - }); - - this.listbox.appendChild(listElement); - - this.context.querySelector(`.${classAutosuggestResultsTitle}`).classList.remove('ons-u-d-no'); - - return listElement; + async fetchSuggestions(sanitisedQuery, data) { + this.abortFetch(); + const results = await runFuse(sanitisedQuery, data, this.lang, this.resultLimit); + results.forEach((result) => { + result.sanitisedText = sanitiseAutosuggestText(result[this.lang], this.sanitisedQueryReplaceChars); }); - } - - if (this.numberOfResults < this.foundResults) { - const listElement = document.createElement('li'); - listElement.className = `${classAutosuggestOption} ${classAutosuggestOptionMoreResults}`; - listElement.setAttribute('aria-hidden', 'true'); - listElement.innerHTML = this.moreResults; - this.listbox.appendChild(listElement); - } + return { + status: this.responseStatus, + results, + totalResults: results.length, + }; + } - if (this.resultLimit === 100 && this.foundResults > this.resultLimit) { - let message = this.tooManyResults.replace('{n}', this.foundResults); - this.resultsContainer.insertBefore(this.createWarningElement(message), this.resultsContainer.firstChild); - this.ariaStatus.setAttribute('aria-hidden', 'true'); - this.listbox.remove(); - this.resultsTitleContainer.remove(); - } + abortFetch() { + if (this.fetch && this.fetch.status !== 'DONE') { + this.fetch.abort(); + } + } - this.setHighlightedResult(null); + unsetResults() { + this.results = []; + this.resultOptions = []; + this.resultSelected = false; - this.input.setAttribute('aria-expanded', !!this.numberOfResults); + if (this.onUnsetResult) { + this.onUnsetResult(); + } + } - if (!!this.numberOfResults && this.sanitisedQuery.length >= this.minChars) { - this.context.classList.add(classAutosuggestHasResults); - } else { + clearListbox(preventAriaStatusUpdate) { + this.listbox.innerHTML = ''; this.context.classList.remove(classAutosuggestHasResults); - this.clearListbox(); - } - } + this.input.removeAttribute('aria-activedescendant'); + this.input.setAttribute('aria-expanded', false); - if (this.numberOfResults === 0 && this.noResults) { - this.handleNoResults(result.status); + if (!preventAriaStatusUpdate) { + this.setAriaStatus(); + } } - } - - handleNoResults(status) { - let message; - this.context.classList.add(classAutosuggestHasResults); - this.context.querySelector(`.${classAutosuggestResultsTitle}`).classList.add('ons-u-d-no'); - this.input.setAttribute('aria-expanded', true); - - if (status === 400 || status === false) { - message = this.typeMore; - this.setAriaStatus(message); - this.listbox.innerHTML = `
  • ${message}
  • `; - } else if (status > 400 || status === '') { - message = this.errorAPI + (this.errorAPILinkText ? ' ' + this.errorAPILinkText + '.' : ''); - let ariaMessage = this.errorAPI + (this.errorAPILinkText ? ' ' + this.errorAPILinkText : ''); - - this.input.setAttribute('disabled', true); - this.input.value = ''; - this.label.classList.add('ons-u-lighter'); - - this.resultsContainer.insertBefore(this.createWarningElement(message), this.resultsContainer.firstChild); - this.ariaStatus.setAttribute('aria-hidden', 'true'); - this.setAriaStatus(ariaMessage); - this.listbox.remove(); - this.resultsTitleContainer.remove(); - } else { - message = this.noResults; - this.listbox.innerHTML = `
  • ${message}
  • `; + + handleResults(result) { + this.resultLimit = result.limit ? result.limit : this.resultLimit; + this.foundResults = result.totalResults; + if (this.foundResults > this.resultLimit) { + result.results = result.results.slice(0, this.resultLimit); + } + + this.results = result.results; + this.numberOfResults = this.results ? Math.max(this.results.length, 0) : 0; + this.setAriaStatus(); + if (!this.deleting || (this.numberOfResults && this.deleting)) { + this.listbox.innerHTML = ''; + if (this.results) { + this.resultOptions = this.results.map((result, index) => { + let innerHTML = this.emboldenMatch(result[this.lang], this.query); + + const listElement = document.createElement('li'); + listElement.className = classAutosuggestOption; + listElement.setAttribute('id', `${this.listboxId}__option--${index}`); + listElement.setAttribute('role', 'option'); + if (result.category) { + innerHTML = + innerHTML + + `${result.category}`; + } + listElement.innerHTML = innerHTML; + listElement.addEventListener('click', () => { + this.selectResult(index); + }); + + this.listbox.appendChild(listElement); + + this.context.querySelector(`.${classAutosuggestResultsTitle}`).classList.remove('ons-u-d-no'); + + return listElement; + }); + } + + if (this.numberOfResults < this.foundResults) { + const listElement = document.createElement('li'); + listElement.className = `${classAutosuggestOption} ${classAutosuggestOptionMoreResults}`; + listElement.setAttribute('aria-hidden', 'true'); + listElement.innerHTML = this.moreResults; + this.listbox.appendChild(listElement); + } + + if (this.resultLimit === 100 && this.foundResults > this.resultLimit) { + let message = this.tooManyResults.replace('{n}', this.foundResults); + this.resultsContainer.insertBefore(this.createWarningElement(message), this.resultsContainer.firstChild); + this.ariaStatus.setAttribute('aria-hidden', 'true'); + this.listbox.remove(); + this.resultsTitleContainer.remove(); + } + + this.setHighlightedResult(null); + + this.input.setAttribute('aria-expanded', !!this.numberOfResults); + + if (!!this.numberOfResults && this.sanitisedQuery.length >= this.minChars) { + this.context.classList.add(classAutosuggestHasResults); + } else { + this.context.classList.remove(classAutosuggestHasResults); + this.clearListbox(); + } + } + + if (this.numberOfResults === 0 && this.noResults) { + this.handleNoResults(result.status); + } } - } - - setHighlightedResult(index) { - this.highlightedResultIndex = index; - - if (this.highlightedResultIndex === null) { - this.input.removeAttribute('aria-activedescendant'); - } else if (this.numberOfResults) { - this.resultOptions.forEach((option, optionIndex) => { - if (optionIndex === index) { - option.classList.add(classAutosuggestOptionFocused); - option.setAttribute('aria-selected', true); - this.input.setAttribute('aria-activedescendant', option.getAttribute('id')); - const groupedResult = option.querySelector('.ons-autosuggest__group'); - const optionText = option.innerHTML.replace('', '').replace('', ''); - if (groupedResult) { - let groupedAriaMsg = this.ariaGroupedResults.replace('{n}', groupedResult.innerHTML); - groupedAriaMsg = groupedAriaMsg.replace('{x}', optionText); - this.setAriaStatus(groupedAriaMsg); - } else { - this.setAriaStatus(optionText); - } + + handleNoResults(status) { + let message; + this.context.classList.add(classAutosuggestHasResults); + this.context.querySelector(`.${classAutosuggestResultsTitle}`).classList.add('ons-u-d-no'); + this.input.setAttribute('aria-expanded', true); + + if (status === 400 || status === false) { + message = this.typeMore; + this.setAriaStatus(message); + this.listbox.innerHTML = `
  • ${message}
  • `; + } else if (status > 400 || status === '') { + message = + this.errorAPI + (this.errorAPILinkText ? ' ' + this.errorAPILinkText + '.' : ''); + let ariaMessage = this.errorAPI + (this.errorAPILinkText ? ' ' + this.errorAPILinkText : ''); + + this.input.setAttribute('disabled', true); + this.input.value = ''; + this.label.classList.add('ons-u-lighter'); + + this.resultsContainer.insertBefore(this.createWarningElement(message), this.resultsContainer.firstChild); + this.ariaStatus.setAttribute('aria-hidden', 'true'); + this.setAriaStatus(ariaMessage); + this.listbox.remove(); + this.resultsTitleContainer.remove(); } else { - option.classList.remove(classAutosuggestOptionFocused); - option.removeAttribute('aria-selected'); + message = this.noResults; + this.listbox.innerHTML = `
  • ${message}
  • `; } - }); } - } - - setAriaStatus(content) { - if (!content) { - const queryTooShort = this.sanitisedQuery.length < this.minChars; - const noResults = this.numberOfResults === 0; - if (queryTooShort) { - content = this.ariaMinChars; - } else if (noResults) { - content = `${this.noResults}: "${this.query}"`; - } else if (this.numberOfResults === 1) { - content = this.ariaOneResult; - } else { - content = this.ariaNResults.replace('{n}', this.numberOfResults); - - if (this.resultLimit && this.foundResults > this.resultLimit) { - content += ` ${this.ariaLimitedResults}`; + + setHighlightedResult(index) { + this.highlightedResultIndex = index; + + if (this.highlightedResultIndex === null) { + this.input.removeAttribute('aria-activedescendant'); + } else if (this.numberOfResults) { + this.resultOptions.forEach((option, optionIndex) => { + if (optionIndex === index) { + option.classList.add(classAutosuggestOptionFocused); + option.setAttribute('aria-selected', true); + this.input.setAttribute('aria-activedescendant', option.getAttribute('id')); + const groupedResult = option.querySelector('.ons-autosuggest__group'); + const optionText = option.innerHTML.replace('', '').replace('', ''); + if (groupedResult) { + let groupedAriaMsg = this.ariaGroupedResults.replace('{n}', groupedResult.innerHTML); + groupedAriaMsg = groupedAriaMsg.replace('{x}', optionText); + this.setAriaStatus(groupedAriaMsg); + } else { + this.setAriaStatus(optionText); + } + } else { + option.classList.remove(classAutosuggestOptionFocused); + option.removeAttribute('aria-selected'); + } + }); } - } } - this.ariaStatus.innerHTML = content; - } - - selectResult(index) { - if (this.results.length) { - this.settingResult = true; - const result = this.results[index || this.highlightedResultIndex || 0]; - this.resultSelected = true; - - if (this.allowMultiple === 'true') { - let value = this.storeExistingSelections(result[this.lang]); - result.displayText = value; - } else if (result.url) { - result.displayText = result[this.lang]; - window.location = result.url; - } else { - result.displayText = result[this.lang]; - } - this.onSelect(result).then(() => (this.settingResult = false)); - - const ariaMessage = `${this.ariaYouHaveSelected}: ${result.displayText}.`; - - this.clearListbox(); - this.setAriaStatus(ariaMessage); + + setAriaStatus(content) { + if (!content) { + const queryTooShort = this.sanitisedQuery.length < this.minChars; + const noResults = this.numberOfResults === 0; + if (queryTooShort) { + content = this.ariaMinChars; + } else if (noResults) { + content = `${this.noResults}: "${this.query}"`; + } else if (this.numberOfResults === 1) { + content = this.ariaOneResult; + } else { + content = this.ariaNResults.replace('{n}', this.numberOfResults); + + if (this.resultLimit && this.foundResults > this.resultLimit) { + content += ` ${this.ariaLimitedResults}`; + } + } + } + this.ariaStatus.innerHTML = content; } - } - createWarningElement(content) { - const warningContainer = document.createElement('div'); - const warningElement = document.createElement('div'); - const warningSpanElement = document.createElement('span'); - const warningBodyElement = document.createElement('div'); + selectResult(index) { + if (this.results.length) { + this.settingResult = true; + const result = this.results[index || this.highlightedResultIndex || 0]; + this.resultSelected = true; + + if (this.allowMultiple === 'true') { + let value = this.storeExistingSelections(result[this.lang]); + result.displayText = value; + } else if (result.url) { + result.displayText = result[this.lang]; + window.location = result.url; + } else { + result.displayText = result[this.lang]; + } + this.onSelect(result).then(() => (this.settingResult = false)); + + const ariaMessage = `${this.ariaYouHaveSelected}: ${result.displayText}.`; + + this.clearListbox(); + this.setAriaStatus(ariaMessage); + } + } - warningContainer.className = 'ons-autosuggest__warning'; - warningElement.className = 'ons-panel ons-panel--warn ons-autosuggest__panel'; + createWarningElement(content) { + const warningContainer = document.createElement('div'); + const warningElement = document.createElement('div'); + const warningSpanElement = document.createElement('span'); + const warningBodyElement = document.createElement('div'); - warningSpanElement.className = 'ons-panel__icon'; - warningSpanElement.setAttribute('aria-hidden', 'true'); - warningSpanElement.innerHTML = '!'; + warningContainer.className = 'ons-autosuggest__warning'; + warningElement.className = 'ons-panel ons-panel--warn ons-autosuggest__panel'; - warningBodyElement.className = 'ons-panel__body'; - warningBodyElement.innerHTML = content; + warningSpanElement.className = 'ons-panel__icon'; + warningSpanElement.setAttribute('aria-hidden', 'true'); + warningSpanElement.innerHTML = '!'; - warningElement.appendChild(warningSpanElement); - warningElement.appendChild(warningBodyElement); - warningContainer.appendChild(warningElement); + warningBodyElement.className = 'ons-panel__body'; + warningBodyElement.innerHTML = content; - return warningContainer; - } + warningElement.appendChild(warningSpanElement); + warningElement.appendChild(warningBodyElement); + warningContainer.appendChild(warningElement); - storeExistingSelections(value) { - this.currentSelections = this.input.value.split(', ').filter((items) => this.allSelections.includes(items)); - this.allSelections = []; - if (this.currentSelections.length) { - this.allSelections = this.currentSelections; + return warningContainer; } - this.allSelections.push(value); - this.allSelections = this.allSelections.filter(function (value, index, array) { - return array.indexOf(value) == index; - }); + storeExistingSelections(value) { + this.currentSelections = this.input.value.split(', ').filter((items) => this.allSelections.includes(items)); + this.allSelections = []; + if (this.currentSelections.length) { + this.allSelections = this.currentSelections; + } + this.allSelections.push(value); - return this.allSelections.join(', '); - } + this.allSelections = this.allSelections.filter(function (value, index, array) { + return array.indexOf(value) == index; + }); - emboldenMatch(string, query) { - let reg = new RegExp(this.escapeRegExp(query).split(' ').join('[\\s,]*'), 'gi'); + return this.allSelections.join(', '); + } - return string.replace(reg, '$&'); - } + emboldenMatch(string, query) { + let reg = new RegExp(this.escapeRegExp(query).split(' ').join('[\\s,]*'), 'gi'); - escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - } + return string.replace(reg, '$&'); + } + + escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } } diff --git a/src/components/autosuggest/fuse-config.js b/src/components/autosuggest/fuse-config.js index 8fcaf1be6d..2b7724a47e 100644 --- a/src/components/autosuggest/fuse-config.js +++ b/src/components/autosuggest/fuse-config.js @@ -1,22 +1,22 @@ import Fuse from 'fuse.js'; export default function runFuse(query, data, searchFields) { - const options = { - shouldSort: true, - threshold: 0.2, - keys: [ - { - name: searchFields, - weight: 0.9, - }, - { - name: 'tags', - weight: 0.1, - }, - ], - }; + const options = { + shouldSort: true, + threshold: 0.2, + keys: [ + { + name: searchFields, + weight: 0.9, + }, + { + name: 'tags', + weight: 0.1, + }, + ], + }; - const fuse = new Fuse(data, options); - let result = fuse.search(query); - return result; + const fuse = new Fuse(data, options); + let result = fuse.search(query); + return result; } diff --git a/src/components/back-to-top/_back-to-top.scss b/src/components/back-to-top/_back-to-top.scss index 0e72bd8029..9bd4d15518 100644 --- a/src/components/back-to-top/_back-to-top.scss +++ b/src/components/back-to-top/_back-to-top.scss @@ -1,34 +1,34 @@ .ons-back-to-top { - &__description { - margin-left: 0.2rem; - } + &__description { + margin-left: 0.2rem; + } - &__enabled { - background: none; - width: fit-content; - padding: 0.5em 0; - } + &__enabled { + background: none; + width: fit-content; + padding: 0.5em 0; + } - &__sticky { - bottom: -1px; - position: fixed; - z-index: 10; - background: var(--ons-color-grey-15); - width: 100%; - height: fit-content; - left: 0; + &__sticky { + bottom: -1px; + position: fixed; + z-index: 10; + background: var(--ons-color-grey-15); + width: 100%; + height: fit-content; + left: 0; - > .ons-back-to-top__link { - width: 100%; - padding: 0.7em 0; - position: relative; - display: block; - color: var(--ons-color-black); + > .ons-back-to-top__link { + width: 100%; + padding: 0.7em 0; + position: relative; + display: block; + color: var(--ons-color-black); - &:focus { - box-shadow: none; - text-decoration: underline solid var(--ons-color-text-link-focus) 4px; - } + &:focus { + box-shadow: none; + text-decoration: underline solid var(--ons-color-text-link-focus) 4px; + } + } } - } } diff --git a/src/components/back-to-top/_macro.spec.js b/src/components/back-to-top/_macro.spec.js index c94a39bd6a..b44be197ed 100644 --- a/src/components/back-to-top/_macro.spec.js +++ b/src/components/back-to-top/_macro.spec.js @@ -6,55 +6,55 @@ import axe from '../../tests/helpers/axe'; import { renderComponent } from '../../tests/helpers/rendering'; describe('macro: back-to-top', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load( - renderComponent('back-to-top', { - description: 'Back to top', - anchor: 'example-target', - }), - ); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has back to top link with the provided description', async () => { - const $ = cheerio.load( - renderComponent('back-to-top', { - description: 'Scroll to top', - anchor: 'example-target', - }), - ); - expect($('.ons-back-to-top .ons-back-to-top__link').text().trim()).toBe('Scroll to top'); - }); - - it('has back to top link with the provided anchor', async () => { - const $ = cheerio.load( - renderComponent('back-to-top', { - description: 'Back to top', - anchor: 'example-target', - }), - ); - - expect($('.ons-back-to-top .ons-back-to-top__link').attr('href')).toBe('#example-target'); - }); - - it('has back to top link with the default description if no description provided', async () => { - const $ = cheerio.load( - renderComponent('back-to-top', { - anchor: 'example-target', - }), - ); - - expect($('.ons-back-to-top .ons-back-to-top__link').text().trim()).toBe('Back to top'); - }); - - it('has back to top link with the default anchor if no anchor provided', async () => { - const renderedComponent = renderComponent('back-to-top', { - description: 'Back to top', + it('passes jest-axe checks', async () => { + const $ = cheerio.load( + renderComponent('back-to-top', { + description: 'Back to top', + anchor: 'example-target', + }), + ); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); }); - const $ = cheerio.load(renderedComponent); - expect($('.ons-back-to-top .ons-back-to-top__link').attr('href')).toBe('#top'); - }); + it('has back to top link with the provided description', async () => { + const $ = cheerio.load( + renderComponent('back-to-top', { + description: 'Scroll to top', + anchor: 'example-target', + }), + ); + expect($('.ons-back-to-top .ons-back-to-top__link').text().trim()).toBe('Scroll to top'); + }); + + it('has back to top link with the provided anchor', async () => { + const $ = cheerio.load( + renderComponent('back-to-top', { + description: 'Back to top', + anchor: 'example-target', + }), + ); + + expect($('.ons-back-to-top .ons-back-to-top__link').attr('href')).toBe('#example-target'); + }); + + it('has back to top link with the default description if no description provided', async () => { + const $ = cheerio.load( + renderComponent('back-to-top', { + anchor: 'example-target', + }), + ); + + expect($('.ons-back-to-top .ons-back-to-top__link').text().trim()).toBe('Back to top'); + }); + + it('has back to top link with the default anchor if no anchor provided', async () => { + const renderedComponent = renderComponent('back-to-top', { + description: 'Back to top', + }); + const $ = cheerio.load(renderedComponent); + + expect($('.ons-back-to-top .ons-back-to-top__link').attr('href')).toBe('#top'); + }); }); diff --git a/src/components/back-to-top/back-to-top.dom.js b/src/components/back-to-top/back-to-top.dom.js index 25dd709857..c01aa4fefb 100644 --- a/src/components/back-to-top/back-to-top.dom.js +++ b/src/components/back-to-top/back-to-top.dom.js @@ -1,12 +1,12 @@ import domready from '../../js/domready'; async function backToTop() { - const bttElement = [...document.querySelectorAll('.ons-js-back-to-top')]; + const bttElement = [...document.querySelectorAll('.ons-js-back-to-top')]; - if (bttElement) { - const Btt = (await import('./back-to-top')).default; - bttElement.forEach((btn) => new Btt(btn)); - } + if (bttElement) { + const Btt = (await import('./back-to-top')).default; + bttElement.forEach((btn) => new Btt(btn)); + } } domready(backToTop); diff --git a/src/components/back-to-top/back-to-top.js b/src/components/back-to-top/back-to-top.js index 5b680f121d..96c15f6303 100644 --- a/src/components/back-to-top/back-to-top.js +++ b/src/components/back-to-top/back-to-top.js @@ -1,58 +1,58 @@ export default class backToTop { - constructor(component) { - this.component = component; - this.content = this.component.previousElementSibling; - this.target = document.getElementById(this.component.firstElementChild.href.split('#')[1]); - this.contentleft; - this.updateContentDetails(); - - this.handleScroll = this.handleScroll.bind(this); - window.addEventListener('scroll', this.handleScroll); - - window.addEventListener('resize', () => { - this.setEnabled(); - this.updateContentDetails(); - this.handleScroll(); - }); - - this.component.addEventListener('click', () => { - this.component.firstElementChild.blur(); - }); - } - - handleScroll() { - let scrollPosition = window.scrollY + window.innerHeight; - - if (this.target) { - scrollPosition = -this.target.getBoundingClientRect().top + window.innerHeight; + constructor(component) { + this.component = component; + this.content = this.component.previousElementSibling; + this.target = document.getElementById(this.component.firstElementChild.href.split('#')[1]); + this.contentleft; + this.updateContentDetails(); + + this.handleScroll = this.handleScroll.bind(this); + window.addEventListener('scroll', this.handleScroll); + + window.addEventListener('resize', () => { + this.setEnabled(); + this.updateContentDetails(); + this.handleScroll(); + }); + + this.component.addEventListener('click', () => { + this.component.firstElementChild.blur(); + }); } - const contentRect = this.content.getBoundingClientRect(); - const windowHeight = window.innerHeight; - const contentBottom = contentRect.bottom; + handleScroll() { + let scrollPosition = window.scrollY + window.innerHeight; - const stickyThreshold = windowHeight * 2; - if (scrollPosition > stickyThreshold && windowHeight < contentBottom) { - this.setSticky(); - } else { - this.setEnabled(); + if (this.target) { + scrollPosition = -this.target.getBoundingClientRect().top + window.innerHeight; + } + + const contentRect = this.content.getBoundingClientRect(); + const windowHeight = window.innerHeight; + const contentBottom = contentRect.bottom; + + const stickyThreshold = windowHeight * 2; + if (scrollPosition > stickyThreshold && windowHeight < contentBottom) { + this.setSticky(); + } else { + this.setEnabled(); + } + } + + setSticky() { + this.component.classList.remove('ons-back-to-top__enabled'); + this.component.classList.add('ons-back-to-top__sticky'); + this.component.firstElementChild.children[0].style.marginLeft = `${this.contentleft}px`; + } + + setEnabled() { + this.updateContentDetails(); + this.component.classList.remove('ons-back-to-top__sticky'); + this.component.classList.add('ons-back-to-top__enabled'); + this.component.firstElementChild.children[0].style.marginLeft = ''; + } + + updateContentDetails() { + this.contentleft = this.component.getBoundingClientRect().left; } - } - - setSticky() { - this.component.classList.remove('ons-back-to-top__enabled'); - this.component.classList.add('ons-back-to-top__sticky'); - this.component.firstElementChild.children[0].style.marginLeft = `${this.contentleft}px`; - } - - setEnabled() { - this.updateContentDetails(); - this.component.classList.remove('ons-back-to-top__sticky'); - this.component.classList.add('ons-back-to-top__enabled'); - this.component.firstElementChild.children[0].style.marginLeft = ''; - } - - updateContentDetails() { - this.contentleft = this.component.getBoundingClientRect().left; - } } diff --git a/src/components/back-to-top/back-to-top.spec.js b/src/components/back-to-top/back-to-top.spec.js index 74c86ebb39..b0033b340b 100644 --- a/src/components/back-to-top/back-to-top.spec.js +++ b/src/components/back-to-top/back-to-top.spec.js @@ -1,117 +1,117 @@ import { renderComponent, setTestPage } from '../../tests/helpers/rendering'; describe('script: back-to-top', () => { - beforeEach(async () => { - await setTestPage( - '/test', - ` -
    -
    -
    -
    - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Massa tincidunt nunc pulvinar sapien et ligula ullamcorper. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in. Eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis. Enim neque volutpat ac tincidunt. Tortor pretium viverra suspendisse potenti nullam ac tortor. Sed vulputate mi sit amet mauris commodo quis imperdiet massa. Odio morbi quis commodo odio. Lobortis feugiat vivamus at augue eget. Aliquam id diam maecenas ultricies mi eget. Dictum non consectetur a erat nam at lectus urna. Justo laoreet sit amet cursus sit amet. Tristique senectus et netus et malesuada fames. Etiam dignissim diam quis enim. Urna id volutpat lacus laoreet non curabitur. Proin sagittis nisl rhoncus mattis. Vel pretium lectus quam id leo in vitae turpis. - Orci nulla pellentesque dignissim enim sit amet venenatis urna. Scelerisque eleifend donec pretium vulputate sapien nec sagittis. Quis commodo odio aenean sed adipiscing. Metus vulputate eu scelerisque felis imperdiet proin fermentum. Pellentesque sit amet porttitor eget dolor. Habitant morbi tristique senectus et netus et malesuada. Curabitur vitae nunc sed velit. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat. Id leo in vitae turpis massa. Vitae ultricies leo integer malesuada. - Elit at imperdiet dui accumsan sit amet nulla. Pretium quam vulputate dignissim suspendisse in est ante. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Mattis aliquam faucibus purus in massa tempor nec. Orci a scelerisque purus semper. Vel fringilla est ullamcorper eget nulla facilisi. Ac ut consequat semper viverra nam libero justo laoreet sit. Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Lobortis scelerisque fermentum dui faucibus in ornare quam. Diam vel quam elementum pulvinar etiam. Amet tellus cras adipiscing enim eu turpis egestas pretium aenean. - Pulvinar sapien et ligula ullamcorper malesuada. Risus pretium quam vulputate dignissim suspendisse in est. Facilisi etiam dignissim diam quis enim. Sodales ut eu sem integer vitae. Eget nunc scelerisque viverra mauris. Malesuada bibendum arcu vitae elementum curabitur. Elementum nisi quis eleifend quam adipiscing vitae proin. Ultrices vitae auctor eu augue. Hac habitasse platea dictumst vestibulum rhoncus est. Urna neque viverra justo nec ultrices dui sapien eget mi. Quisque egestas diam in arcu cursus euismod quis viverra. Nullam ac tortor vitae purus faucibus. Fames ac turpis egestas maecenas. Magna fermentum iaculis eu non diam phasellus vestibulum. Nisl nunc mi ipsum faucibus vitae aliquet nec. Nibh ipsum consequat nisl vel pretium lectus quam id leo. Faucibus turpis in eu mi bibendum neque egestas. - Lectus nulla at volutpat diam ut venenatis tellus. Tellus rutrum tellus pellentesque eu tincidunt tortor. Purus sit amet volutpat consequat mauris nunc congue. Dignissim cras tincidunt lobortis feugiat vivamus at. Ac felis donec et odio pellentesque diam volutpat commodo. Arcu dui vivamus arcu felis. Pulvinar proin gravida hendrerit lectus a. Venenatis lectus magna fringilla urna porttitor rhoncus dolor purus. Maecenas sed enim ut sem viverra aliquet eget. Lacus laoreet non curabitur gravida arcu ac tortor. Laoreet sit amet cursus sit amet dictum. Maecenas accumsan lacus vel facilisis volutpat est velit. -
    -
    -
    -
    - Elementum integer enim neque volutpat ac tincidunt vitae semper quis. Sem integer vitae justo eget magna fermentum iaculis. Nunc lobortis mattis aliquam faucibus purus in massa tempor nec. Imperdiet proin fermentum leo vel orci porta non. Sed enim ut sem viverra aliquet eget sit amet. Rhoncus mattis rhoncus urna neque viverra justo. At tellus at urna condimentum mattis pellentesque. Vel orci porta non pulvinar neque laoreet suspendisse. Consectetur purus ut faucibus pulvinar elementum integer enim. Urna condimentum mattis pellentesque id nibh. Sem integer vitae justo eget magna fermentum. Ultrices vitae auctor eu augue ut lectus arcu bibendum. Adipiscing bibendum est ultricies integer quis auctor elit. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Sapien faucibus et molestie ac feugiat. Tempor id eu nisl nunc mi ipsum. Arcu non sodales neque sodales ut etiam sit amet nisl. Et malesuada fames ac turpis egestas integer eget aliquet nibh. Pellentesque adipiscing commodo elit at imperdiet. Commodo sed egestas egestas fringilla phasellus faucibus scelerisque eleifend. - Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim. Semper risus in hendrerit gravida rutrum quisque. Tempor nec feugiat nisl pretium fusce id. Ipsum consequat nisl vel pretium. Pellentesque eu tincidunt tortor aliquam nulla facilisi cras fermentum. Etiam tempor orci eu lobortis elementum nibh. Eget nullam non nisi est sit amet facilisis magna. Diam vel quam elementum pulvinar. Dolor morbi non arcu risus quis. Nullam ac tortor vitae purus. - Morbi leo urna molestie at elementum eu facilisis sed odio. Purus in mollis nunc sed id. Sit amet est placerat in egestas erat imperdiet sed. Diam sollicitudin tempor id eu nisl nunc mi. Nulla aliquet porttitor lacus luctus accumsan tortor posuere. Ac feugiat sed lectus vestibulum mattis. Potenti nullam ac tortor vitae purus faucibus ornare suspendisse. Faucibus vitae aliquet nec ullamcorper sit amet. Eu consequat ac felis donec et odio pellentesque diam. Morbi tincidunt ornare massa eget egestas purus viverra accumsan. A erat nam at lectus urna duis convallis. Pellentesque elit eget gravida cum sociis. Cursus sit amet dictum sit amet justo donec enim. Vitae justo eget magna fermentum iaculis. Enim ut sem viverra aliquet. Convallis tellus id interdum velit. Orci phasellus egestas tellus rutrum tellus pellentesque eu tincidunt tortor. Venenatis a condimentum vitae sapien. Lacus viverra vitae congue eu consequat ac felis. Diam donec adipiscing tristique risus nec feugiat. - Sit amet mattis vulputate enim nulla aliquet. Quis commodo odio aenean sed adipiscing diam donec adipiscing tristique. Vulputate odio ut enim blandit volutpat. Elit pellentesque habitant morbi tristique senectus et netus. Aliquet lectus proin nibh nisl condimentum id. A iaculis at erat pellentesque adipiscing commodo elit at. Quis ipsum suspendisse ultrices gravida dictum fusce. Sit amet mauris commodo quis imperdiet massa tincidunt. Adipiscing elit ut aliquam purus. A diam maecenas sed enim ut sem viverra aliquet eget. Enim neque volutpat ac tincidunt vitae. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Amet nisl suscipit adipiscing bibendum. Nunc pulvinar sapien et ligula ullamcorper malesuada proin. Nulla facilisi cras fermentum odio eu feugiat. -
    -
    -
    -
    -

    Contents

    -
    -
    -
    -
    - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Massa tincidunt nunc pulvinar sapien et ligula ullamcorper. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in. Eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis. Enim neque volutpat ac tincidunt. Tortor pretium viverra suspendisse potenti nullam ac tortor. Sed vulputate mi sit amet mauris commodo quis imperdiet massa. Odio morbi quis commodo odio. Lobortis feugiat vivamus at augue eget. Aliquam id diam maecenas ultricies mi eget. Dictum non consectetur a erat nam at lectus urna. Justo laoreet sit amet cursus sit amet. Tristique senectus et netus et malesuada fames. Etiam dignissim diam quis enim. Urna id volutpat lacus laoreet non curabitur. Proin sagittis nisl rhoncus mattis. Vel pretium lectus quam id leo in vitae turpis. - Orci nulla pellentesque dignissim enim sit amet venenatis urna. Scelerisque eleifend donec pretium vulputate sapien nec sagittis. Quis commodo odio aenean sed adipiscing. Metus vulputate eu scelerisque felis imperdiet proin fermentum. Pellentesque sit amet porttitor eget dolor. Habitant morbi tristique senectus et netus et malesuada. Curabitur vitae nunc sed velit. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat. Id leo in vitae turpis massa. Vitae ultricies leo integer malesuada. - Elit at imperdiet dui accumsan sit amet nulla. Pretium quam vulputate dignissim suspendisse in est ante. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Mattis aliquam faucibus purus in massa tempor nec. Orci a scelerisque purus semper. Vel fringilla est ullamcorper eget nulla facilisi. Ac ut consequat semper viverra nam libero justo laoreet sit. Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Lobortis scelerisque fermentum dui faucibus in ornare quam. Diam vel quam elementum pulvinar etiam. Amet tellus cras adipiscing enim eu turpis egestas pretium aenean. - Pulvinar sapien et ligula ullamcorper malesuada. Risus pretium quam vulputate dignissim suspendisse in est. Facilisi etiam dignissim diam quis enim. Sodales ut eu sem integer vitae. Eget nunc scelerisque viverra mauris. Malesuada bibendum arcu vitae elementum curabitur. Elementum nisi quis eleifend quam adipiscing vitae proin. Ultrices vitae auctor eu augue. Hac habitasse platea dictumst vestibulum rhoncus est. Urna neque viverra justo nec ultrices dui sapien eget mi. Quisque egestas diam in arcu cursus euismod quis viverra. Nullam ac tortor vitae purus faucibus. Fames ac turpis egestas maecenas. Magna fermentum iaculis eu non diam phasellus vestibulum. Nisl nunc mi ipsum faucibus vitae aliquet nec. Nibh ipsum consequat nisl vel pretium lectus quam id leo. Faucibus turpis in eu mi bibendum neque egestas. - Lectus nulla at volutpat diam ut venenatis tellus. Tellus rutrum tellus pellentesque eu tincidunt tortor. Purus sit amet volutpat consequat mauris nunc congue. Dignissim cras tincidunt lobortis feugiat vivamus at. Ac felis donec et odio pellentesque diam volutpat commodo. Arcu dui vivamus arcu felis. Pulvinar proin gravida hendrerit lectus a. Venenatis lectus magna fringilla urna porttitor rhoncus dolor purus. Maecenas sed enim ut sem viverra aliquet eget. Lacus laoreet non curabitur gravida arcu ac tortor. Laoreet sit amet cursus sit amet dictum. Maecenas accumsan lacus vel facilisis volutpat est velit. -
    -
    -
    -
    - Elementum integer enim neque volutpat ac tincidunt vitae semper quis. Sem integer vitae justo eget magna fermentum iaculis. Nunc lobortis mattis aliquam faucibus purus in massa tempor nec. Imperdiet proin fermentum leo vel orci porta non. Sed enim ut sem viverra aliquet eget sit amet. Rhoncus mattis rhoncus urna neque viverra justo. At tellus at urna condimentum mattis pellentesque. Vel orci porta non pulvinar neque laoreet suspendisse. Consectetur purus ut faucibus pulvinar elementum integer enim. Urna condimentum mattis pellentesque id nibh. Sem integer vitae justo eget magna fermentum. Ultrices vitae auctor eu augue ut lectus arcu bibendum. Adipiscing bibendum est ultricies integer quis auctor elit. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Sapien faucibus et molestie ac feugiat. Tempor id eu nisl nunc mi ipsum. Arcu non sodales neque sodales ut etiam sit amet nisl. Et malesuada fames ac turpis egestas integer eget aliquet nibh. Pellentesque adipiscing commodo elit at imperdiet. Commodo sed egestas egestas fringilla phasellus faucibus scelerisque eleifend. - Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim. Semper risus in hendrerit gravida rutrum quisque. Tempor nec feugiat nisl pretium fusce id. Ipsum consequat nisl vel pretium. Pellentesque eu tincidunt tortor aliquam nulla facilisi cras fermentum. Etiam tempor orci eu lobortis elementum nibh. Eget nullam non nisi est sit amet facilisis magna. Diam vel quam elementum pulvinar. Dolor morbi non arcu risus quis. Nullam ac tortor vitae purus. - Morbi leo urna molestie at elementum eu facilisis sed odio. Purus in mollis nunc sed id. Sit amet est placerat in egestas erat imperdiet sed. Diam sollicitudin tempor id eu nisl nunc mi. Nulla aliquet porttitor lacus luctus accumsan tortor posuere. Ac feugiat sed lectus vestibulum mattis. Potenti nullam ac tortor vitae purus faucibus ornare suspendisse. Faucibus vitae aliquet nec ullamcorper sit amet. Eu consequat ac felis donec et odio pellentesque diam. Morbi tincidunt ornare massa eget egestas purus viverra accumsan. A erat nam at lectus urna duis convallis. Pellentesque elit eget gravida cum sociis. Cursus sit amet dictum sit amet justo donec enim. Vitae justo eget magna fermentum iaculis. Enim ut sem viverra aliquet. Convallis tellus id interdum velit. Orci phasellus egestas tellus rutrum tellus pellentesque eu tincidunt tortor. Venenatis a condimentum vitae sapien. Lacus viverra vitae congue eu consequat ac felis. Diam donec adipiscing tristique risus nec feugiat. - Sit amet mattis vulputate enim nulla aliquet. Quis commodo odio aenean sed adipiscing diam donec adipiscing tristique. Vulputate odio ut enim blandit volutpat. Elit pellentesque habitant morbi tristique senectus et netus. Aliquet lectus proin nibh nisl condimentum id. A iaculis at erat pellentesque adipiscing commodo elit at. Quis ipsum suspendisse ultrices gravida dictum fusce. Sit amet mauris commodo quis imperdiet massa tincidunt. Adipiscing elit ut aliquam purus. A diam maecenas sed enim ut sem viverra aliquet eget. Enim neque volutpat ac tincidunt vitae. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Amet nisl suscipit adipiscing bibendum. Nunc pulvinar sapien et ligula ullamcorper malesuada proin. Nulla facilisi cras fermentum odio eu feugiat. -
    -
    -
    -
    -
    -
    - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Massa tincidunt nunc pulvinar sapien et ligula ullamcorper. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in. Eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis. Enim neque volutpat ac tincidunt. Tortor pretium viverra suspendisse potenti nullam ac tortor. Sed vulputate mi sit amet mauris commodo quis imperdiet massa. Odio morbi quis commodo odio. Lobortis feugiat vivamus at augue eget. Aliquam id diam maecenas ultricies mi eget. Dictum non consectetur a erat nam at lectus urna. Justo laoreet sit amet cursus sit amet. Tristique senectus et netus et malesuada fames. Etiam dignissim diam quis enim. Urna id volutpat lacus laoreet non curabitur. Proin sagittis nisl rhoncus mattis. Vel pretium lectus quam id leo in vitae turpis. - Orci nulla pellentesque dignissim enim sit amet venenatis urna. Scelerisque eleifend donec pretium vulputate sapien nec sagittis. Quis commodo odio aenean sed adipiscing. Metus vulputate eu scelerisque felis imperdiet proin fermentum. Pellentesque sit amet porttitor eget dolor. Habitant morbi tristique senectus et netus et malesuada. Curabitur vitae nunc sed velit. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat. Id leo in vitae turpis massa. Vitae ultricies leo integer malesuada. - Elit at imperdiet dui accumsan sit amet nulla. Pretium quam vulputate dignissim suspendisse in est ante. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Mattis aliquam faucibus purus in massa tempor nec. Orci a scelerisque purus semper. Vel fringilla est ullamcorper eget nulla facilisi. Ac ut consequat semper viverra nam libero justo laoreet sit. Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Lobortis scelerisque fermentum dui faucibus in ornare quam. Diam vel quam elementum pulvinar etiam. Amet tellus cras adipiscing enim eu turpis egestas pretium aenean. - Pulvinar sapien et ligula ullamcorper malesuada. Risus pretium quam vulputate dignissim suspendisse in est. Facilisi etiam dignissim diam quis enim. Sodales ut eu sem integer vitae. Eget nunc scelerisque viverra mauris. Malesuada bibendum arcu vitae elementum curabitur. Elementum nisi quis eleifend quam adipiscing vitae proin. Ultrices vitae auctor eu augue. Hac habitasse platea dictumst vestibulum rhoncus est. Urna neque viverra justo nec ultrices dui sapien eget mi. Quisque egestas diam in arcu cursus euismod quis viverra. Nullam ac tortor vitae purus faucibus. Fames ac turpis egestas maecenas. Magna fermentum iaculis eu non diam phasellus vestibulum. Nisl nunc mi ipsum faucibus vitae aliquet nec. Nibh ipsum consequat nisl vel pretium lectus quam id leo. Faucibus turpis in eu mi bibendum neque egestas. - Lectus nulla at volutpat diam ut venenatis tellus. Tellus rutrum tellus pellentesque eu tincidunt tortor. Purus sit amet volutpat consequat mauris nunc congue. Dignissim cras tincidunt lobortis feugiat vivamus at. Ac felis donec et odio pellentesque diam volutpat commodo. Arcu dui vivamus arcu felis. Pulvinar proin gravida hendrerit lectus a. Venenatis lectus magna fringilla urna porttitor rhoncus dolor purus. Maecenas sed enim ut sem viverra aliquet eget. Lacus laoreet non curabitur gravida arcu ac tortor. Laoreet sit amet cursus sit amet dictum. Maecenas accumsan lacus vel facilisis volutpat est velit. -
    -
    -
    -
    - Elementum integer enim neque volutpat ac tincidunt vitae semper quis. Sem integer vitae justo eget magna fermentum iaculis. Nunc lobortis mattis aliquam faucibus purus in massa tempor nec. Imperdiet proin fermentum leo vel orci porta non. Sed enim ut sem viverra aliquet eget sit amet. Rhoncus mattis rhoncus urna neque viverra justo. At tellus at urna condimentum mattis pellentesque. Vel orci porta non pulvinar neque laoreet suspendisse. Consectetur purus ut faucibus pulvinar elementum integer enim. Urna condimentum mattis pellentesque id nibh. Sem integer vitae justo eget magna fermentum. Ultrices vitae auctor eu augue ut lectus arcu bibendum. Adipiscing bibendum est ultricies integer quis auctor elit. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Sapien faucibus et molestie ac feugiat. Tempor id eu nisl nunc mi ipsum. Arcu non sodales neque sodales ut etiam sit amet nisl. Et malesuada fames ac turpis egestas integer eget aliquet nibh. Pellentesque adipiscing commodo elit at imperdiet. Commodo sed egestas egestas fringilla phasellus faucibus scelerisque eleifend. - Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim. Semper risus in hendrerit gravida rutrum quisque. Tempor nec feugiat nisl pretium fusce id. Ipsum consequat nisl vel pretium. Pellentesque eu tincidunt tortor aliquam nulla facilisi cras fermentum. Etiam tempor orci eu lobortis elementum nibh. Eget nullam non nisi est sit amet facilisis magna. Diam vel quam elementum pulvinar. Dolor morbi non arcu risus quis. Nullam ac tortor vitae purus. - Morbi leo urna molestie at elementum eu facilisis sed odio. Purus in mollis nunc sed id. Sit amet est placerat in egestas erat imperdiet sed. Diam sollicitudin tempor id eu nisl nunc mi. Nulla aliquet porttitor lacus luctus accumsan tortor posuere. Ac feugiat sed lectus vestibulum mattis. Potenti nullam ac tortor vitae purus faucibus ornare suspendisse. Faucibus vitae aliquet nec ullamcorper sit amet. Eu consequat ac felis donec et odio pellentesque diam. Morbi tincidunt ornare massa eget egestas purus viverra accumsan. A erat nam at lectus urna duis convallis. Pellentesque elit eget gravida cum sociis. Cursus sit amet dictum sit amet justo donec enim. Vitae justo eget magna fermentum iaculis. Enim ut sem viverra aliquet. Convallis tellus id interdum velit. Orci phasellus egestas tellus rutrum tellus pellentesque eu tincidunt tortor. Venenatis a condimentum vitae sapien. Lacus viverra vitae congue eu consequat ac felis. Diam donec adipiscing tristique risus nec feugiat. - Sit amet mattis vulputate enim nulla aliquet. Quis commodo odio aenean sed adipiscing diam donec adipiscing tristique. Vulputate odio ut enim blandit volutpat. Elit pellentesque habitant morbi tristique senectus et netus. Aliquet lectus proin nibh nisl condimentum id. A iaculis at erat pellentesque adipiscing commodo elit at. Quis ipsum suspendisse ultrices gravida dictum fusce. Sit amet mauris commodo quis imperdiet massa tincidunt. Adipiscing elit ut aliquam purus. A diam maecenas sed enim ut sem viverra aliquet eget. Enim neque volutpat ac tincidunt vitae. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Amet nisl suscipit adipiscing bibendum. Nunc pulvinar sapien et ligula ullamcorper malesuada proin. Nulla facilisi cras fermentum odio eu feugiat. -
    -
    -
    - ${renderComponent('back-to-top', {})} -
    - `, - ); - }); - - it('is enabled on page when the page is rendered', async () => { - const backToTop = await page.$('.ons-back-to-top'); - expect(backToTop).not.toBeNull(); - }); - - it('is sticky when scrolled past 2 times the window height', async () => { - await page.evaluate(() => { - window.scrollTo(0, window.innerHeight * 2 + 10); + beforeEach(async () => { + await setTestPage( + '/test', + ` +
    +
    +
    +
    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Massa tincidunt nunc pulvinar sapien et ligula ullamcorper. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in. Eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis. Enim neque volutpat ac tincidunt. Tortor pretium viverra suspendisse potenti nullam ac tortor. Sed vulputate mi sit amet mauris commodo quis imperdiet massa. Odio morbi quis commodo odio. Lobortis feugiat vivamus at augue eget. Aliquam id diam maecenas ultricies mi eget. Dictum non consectetur a erat nam at lectus urna. Justo laoreet sit amet cursus sit amet. Tristique senectus et netus et malesuada fames. Etiam dignissim diam quis enim. Urna id volutpat lacus laoreet non curabitur. Proin sagittis nisl rhoncus mattis. Vel pretium lectus quam id leo in vitae turpis. + Orci nulla pellentesque dignissim enim sit amet venenatis urna. Scelerisque eleifend donec pretium vulputate sapien nec sagittis. Quis commodo odio aenean sed adipiscing. Metus vulputate eu scelerisque felis imperdiet proin fermentum. Pellentesque sit amet porttitor eget dolor. Habitant morbi tristique senectus et netus et malesuada. Curabitur vitae nunc sed velit. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat. Id leo in vitae turpis massa. Vitae ultricies leo integer malesuada. + Elit at imperdiet dui accumsan sit amet nulla. Pretium quam vulputate dignissim suspendisse in est ante. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Mattis aliquam faucibus purus in massa tempor nec. Orci a scelerisque purus semper. Vel fringilla est ullamcorper eget nulla facilisi. Ac ut consequat semper viverra nam libero justo laoreet sit. Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Lobortis scelerisque fermentum dui faucibus in ornare quam. Diam vel quam elementum pulvinar etiam. Amet tellus cras adipiscing enim eu turpis egestas pretium aenean. + Pulvinar sapien et ligula ullamcorper malesuada. Risus pretium quam vulputate dignissim suspendisse in est. Facilisi etiam dignissim diam quis enim. Sodales ut eu sem integer vitae. Eget nunc scelerisque viverra mauris. Malesuada bibendum arcu vitae elementum curabitur. Elementum nisi quis eleifend quam adipiscing vitae proin. Ultrices vitae auctor eu augue. Hac habitasse platea dictumst vestibulum rhoncus est. Urna neque viverra justo nec ultrices dui sapien eget mi. Quisque egestas diam in arcu cursus euismod quis viverra. Nullam ac tortor vitae purus faucibus. Fames ac turpis egestas maecenas. Magna fermentum iaculis eu non diam phasellus vestibulum. Nisl nunc mi ipsum faucibus vitae aliquet nec. Nibh ipsum consequat nisl vel pretium lectus quam id leo. Faucibus turpis in eu mi bibendum neque egestas. + Lectus nulla at volutpat diam ut venenatis tellus. Tellus rutrum tellus pellentesque eu tincidunt tortor. Purus sit amet volutpat consequat mauris nunc congue. Dignissim cras tincidunt lobortis feugiat vivamus at. Ac felis donec et odio pellentesque diam volutpat commodo. Arcu dui vivamus arcu felis. Pulvinar proin gravida hendrerit lectus a. Venenatis lectus magna fringilla urna porttitor rhoncus dolor purus. Maecenas sed enim ut sem viverra aliquet eget. Lacus laoreet non curabitur gravida arcu ac tortor. Laoreet sit amet cursus sit amet dictum. Maecenas accumsan lacus vel facilisis volutpat est velit. +
    +
    +
    +
    + Elementum integer enim neque volutpat ac tincidunt vitae semper quis. Sem integer vitae justo eget magna fermentum iaculis. Nunc lobortis mattis aliquam faucibus purus in massa tempor nec. Imperdiet proin fermentum leo vel orci porta non. Sed enim ut sem viverra aliquet eget sit amet. Rhoncus mattis rhoncus urna neque viverra justo. At tellus at urna condimentum mattis pellentesque. Vel orci porta non pulvinar neque laoreet suspendisse. Consectetur purus ut faucibus pulvinar elementum integer enim. Urna condimentum mattis pellentesque id nibh. Sem integer vitae justo eget magna fermentum. Ultrices vitae auctor eu augue ut lectus arcu bibendum. Adipiscing bibendum est ultricies integer quis auctor elit. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Sapien faucibus et molestie ac feugiat. Tempor id eu nisl nunc mi ipsum. Arcu non sodales neque sodales ut etiam sit amet nisl. Et malesuada fames ac turpis egestas integer eget aliquet nibh. Pellentesque adipiscing commodo elit at imperdiet. Commodo sed egestas egestas fringilla phasellus faucibus scelerisque eleifend. + Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim. Semper risus in hendrerit gravida rutrum quisque. Tempor nec feugiat nisl pretium fusce id. Ipsum consequat nisl vel pretium. Pellentesque eu tincidunt tortor aliquam nulla facilisi cras fermentum. Etiam tempor orci eu lobortis elementum nibh. Eget nullam non nisi est sit amet facilisis magna. Diam vel quam elementum pulvinar. Dolor morbi non arcu risus quis. Nullam ac tortor vitae purus. + Morbi leo urna molestie at elementum eu facilisis sed odio. Purus in mollis nunc sed id. Sit amet est placerat in egestas erat imperdiet sed. Diam sollicitudin tempor id eu nisl nunc mi. Nulla aliquet porttitor lacus luctus accumsan tortor posuere. Ac feugiat sed lectus vestibulum mattis. Potenti nullam ac tortor vitae purus faucibus ornare suspendisse. Faucibus vitae aliquet nec ullamcorper sit amet. Eu consequat ac felis donec et odio pellentesque diam. Morbi tincidunt ornare massa eget egestas purus viverra accumsan. A erat nam at lectus urna duis convallis. Pellentesque elit eget gravida cum sociis. Cursus sit amet dictum sit amet justo donec enim. Vitae justo eget magna fermentum iaculis. Enim ut sem viverra aliquet. Convallis tellus id interdum velit. Orci phasellus egestas tellus rutrum tellus pellentesque eu tincidunt tortor. Venenatis a condimentum vitae sapien. Lacus viverra vitae congue eu consequat ac felis. Diam donec adipiscing tristique risus nec feugiat. + Sit amet mattis vulputate enim nulla aliquet. Quis commodo odio aenean sed adipiscing diam donec adipiscing tristique. Vulputate odio ut enim blandit volutpat. Elit pellentesque habitant morbi tristique senectus et netus. Aliquet lectus proin nibh nisl condimentum id. A iaculis at erat pellentesque adipiscing commodo elit at. Quis ipsum suspendisse ultrices gravida dictum fusce. Sit amet mauris commodo quis imperdiet massa tincidunt. Adipiscing elit ut aliquam purus. A diam maecenas sed enim ut sem viverra aliquet eget. Enim neque volutpat ac tincidunt vitae. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Amet nisl suscipit adipiscing bibendum. Nunc pulvinar sapien et ligula ullamcorper malesuada proin. Nulla facilisi cras fermentum odio eu feugiat. +
    +
    +
    +
    +

    Contents

    +
    +
    +
    +
    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Massa tincidunt nunc pulvinar sapien et ligula ullamcorper. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in. Eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis. Enim neque volutpat ac tincidunt. Tortor pretium viverra suspendisse potenti nullam ac tortor. Sed vulputate mi sit amet mauris commodo quis imperdiet massa. Odio morbi quis commodo odio. Lobortis feugiat vivamus at augue eget. Aliquam id diam maecenas ultricies mi eget. Dictum non consectetur a erat nam at lectus urna. Justo laoreet sit amet cursus sit amet. Tristique senectus et netus et malesuada fames. Etiam dignissim diam quis enim. Urna id volutpat lacus laoreet non curabitur. Proin sagittis nisl rhoncus mattis. Vel pretium lectus quam id leo in vitae turpis. + Orci nulla pellentesque dignissim enim sit amet venenatis urna. Scelerisque eleifend donec pretium vulputate sapien nec sagittis. Quis commodo odio aenean sed adipiscing. Metus vulputate eu scelerisque felis imperdiet proin fermentum. Pellentesque sit amet porttitor eget dolor. Habitant morbi tristique senectus et netus et malesuada. Curabitur vitae nunc sed velit. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat. Id leo in vitae turpis massa. Vitae ultricies leo integer malesuada. + Elit at imperdiet dui accumsan sit amet nulla. Pretium quam vulputate dignissim suspendisse in est ante. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Mattis aliquam faucibus purus in massa tempor nec. Orci a scelerisque purus semper. Vel fringilla est ullamcorper eget nulla facilisi. Ac ut consequat semper viverra nam libero justo laoreet sit. Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Lobortis scelerisque fermentum dui faucibus in ornare quam. Diam vel quam elementum pulvinar etiam. Amet tellus cras adipiscing enim eu turpis egestas pretium aenean. + Pulvinar sapien et ligula ullamcorper malesuada. Risus pretium quam vulputate dignissim suspendisse in est. Facilisi etiam dignissim diam quis enim. Sodales ut eu sem integer vitae. Eget nunc scelerisque viverra mauris. Malesuada bibendum arcu vitae elementum curabitur. Elementum nisi quis eleifend quam adipiscing vitae proin. Ultrices vitae auctor eu augue. Hac habitasse platea dictumst vestibulum rhoncus est. Urna neque viverra justo nec ultrices dui sapien eget mi. Quisque egestas diam in arcu cursus euismod quis viverra. Nullam ac tortor vitae purus faucibus. Fames ac turpis egestas maecenas. Magna fermentum iaculis eu non diam phasellus vestibulum. Nisl nunc mi ipsum faucibus vitae aliquet nec. Nibh ipsum consequat nisl vel pretium lectus quam id leo. Faucibus turpis in eu mi bibendum neque egestas. + Lectus nulla at volutpat diam ut venenatis tellus. Tellus rutrum tellus pellentesque eu tincidunt tortor. Purus sit amet volutpat consequat mauris nunc congue. Dignissim cras tincidunt lobortis feugiat vivamus at. Ac felis donec et odio pellentesque diam volutpat commodo. Arcu dui vivamus arcu felis. Pulvinar proin gravida hendrerit lectus a. Venenatis lectus magna fringilla urna porttitor rhoncus dolor purus. Maecenas sed enim ut sem viverra aliquet eget. Lacus laoreet non curabitur gravida arcu ac tortor. Laoreet sit amet cursus sit amet dictum. Maecenas accumsan lacus vel facilisis volutpat est velit. +
    +
    +
    +
    + Elementum integer enim neque volutpat ac tincidunt vitae semper quis. Sem integer vitae justo eget magna fermentum iaculis. Nunc lobortis mattis aliquam faucibus purus in massa tempor nec. Imperdiet proin fermentum leo vel orci porta non. Sed enim ut sem viverra aliquet eget sit amet. Rhoncus mattis rhoncus urna neque viverra justo. At tellus at urna condimentum mattis pellentesque. Vel orci porta non pulvinar neque laoreet suspendisse. Consectetur purus ut faucibus pulvinar elementum integer enim. Urna condimentum mattis pellentesque id nibh. Sem integer vitae justo eget magna fermentum. Ultrices vitae auctor eu augue ut lectus arcu bibendum. Adipiscing bibendum est ultricies integer quis auctor elit. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Sapien faucibus et molestie ac feugiat. Tempor id eu nisl nunc mi ipsum. Arcu non sodales neque sodales ut etiam sit amet nisl. Et malesuada fames ac turpis egestas integer eget aliquet nibh. Pellentesque adipiscing commodo elit at imperdiet. Commodo sed egestas egestas fringilla phasellus faucibus scelerisque eleifend. + Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim. Semper risus in hendrerit gravida rutrum quisque. Tempor nec feugiat nisl pretium fusce id. Ipsum consequat nisl vel pretium. Pellentesque eu tincidunt tortor aliquam nulla facilisi cras fermentum. Etiam tempor orci eu lobortis elementum nibh. Eget nullam non nisi est sit amet facilisis magna. Diam vel quam elementum pulvinar. Dolor morbi non arcu risus quis. Nullam ac tortor vitae purus. + Morbi leo urna molestie at elementum eu facilisis sed odio. Purus in mollis nunc sed id. Sit amet est placerat in egestas erat imperdiet sed. Diam sollicitudin tempor id eu nisl nunc mi. Nulla aliquet porttitor lacus luctus accumsan tortor posuere. Ac feugiat sed lectus vestibulum mattis. Potenti nullam ac tortor vitae purus faucibus ornare suspendisse. Faucibus vitae aliquet nec ullamcorper sit amet. Eu consequat ac felis donec et odio pellentesque diam. Morbi tincidunt ornare massa eget egestas purus viverra accumsan. A erat nam at lectus urna duis convallis. Pellentesque elit eget gravida cum sociis. Cursus sit amet dictum sit amet justo donec enim. Vitae justo eget magna fermentum iaculis. Enim ut sem viverra aliquet. Convallis tellus id interdum velit. Orci phasellus egestas tellus rutrum tellus pellentesque eu tincidunt tortor. Venenatis a condimentum vitae sapien. Lacus viverra vitae congue eu consequat ac felis. Diam donec adipiscing tristique risus nec feugiat. + Sit amet mattis vulputate enim nulla aliquet. Quis commodo odio aenean sed adipiscing diam donec adipiscing tristique. Vulputate odio ut enim blandit volutpat. Elit pellentesque habitant morbi tristique senectus et netus. Aliquet lectus proin nibh nisl condimentum id. A iaculis at erat pellentesque adipiscing commodo elit at. Quis ipsum suspendisse ultrices gravida dictum fusce. Sit amet mauris commodo quis imperdiet massa tincidunt. Adipiscing elit ut aliquam purus. A diam maecenas sed enim ut sem viverra aliquet eget. Enim neque volutpat ac tincidunt vitae. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Amet nisl suscipit adipiscing bibendum. Nunc pulvinar sapien et ligula ullamcorper malesuada proin. Nulla facilisi cras fermentum odio eu feugiat. +
    +
    +
    +
    +
    +
    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Massa tincidunt nunc pulvinar sapien et ligula ullamcorper. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in. Eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis. Enim neque volutpat ac tincidunt. Tortor pretium viverra suspendisse potenti nullam ac tortor. Sed vulputate mi sit amet mauris commodo quis imperdiet massa. Odio morbi quis commodo odio. Lobortis feugiat vivamus at augue eget. Aliquam id diam maecenas ultricies mi eget. Dictum non consectetur a erat nam at lectus urna. Justo laoreet sit amet cursus sit amet. Tristique senectus et netus et malesuada fames. Etiam dignissim diam quis enim. Urna id volutpat lacus laoreet non curabitur. Proin sagittis nisl rhoncus mattis. Vel pretium lectus quam id leo in vitae turpis. + Orci nulla pellentesque dignissim enim sit amet venenatis urna. Scelerisque eleifend donec pretium vulputate sapien nec sagittis. Quis commodo odio aenean sed adipiscing. Metus vulputate eu scelerisque felis imperdiet proin fermentum. Pellentesque sit amet porttitor eget dolor. Habitant morbi tristique senectus et netus et malesuada. Curabitur vitae nunc sed velit. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat. Id leo in vitae turpis massa. Vitae ultricies leo integer malesuada. + Elit at imperdiet dui accumsan sit amet nulla. Pretium quam vulputate dignissim suspendisse in est ante. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Mattis aliquam faucibus purus in massa tempor nec. Orci a scelerisque purus semper. Vel fringilla est ullamcorper eget nulla facilisi. Ac ut consequat semper viverra nam libero justo laoreet sit. Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Lobortis scelerisque fermentum dui faucibus in ornare quam. Diam vel quam elementum pulvinar etiam. Amet tellus cras adipiscing enim eu turpis egestas pretium aenean. + Pulvinar sapien et ligula ullamcorper malesuada. Risus pretium quam vulputate dignissim suspendisse in est. Facilisi etiam dignissim diam quis enim. Sodales ut eu sem integer vitae. Eget nunc scelerisque viverra mauris. Malesuada bibendum arcu vitae elementum curabitur. Elementum nisi quis eleifend quam adipiscing vitae proin. Ultrices vitae auctor eu augue. Hac habitasse platea dictumst vestibulum rhoncus est. Urna neque viverra justo nec ultrices dui sapien eget mi. Quisque egestas diam in arcu cursus euismod quis viverra. Nullam ac tortor vitae purus faucibus. Fames ac turpis egestas maecenas. Magna fermentum iaculis eu non diam phasellus vestibulum. Nisl nunc mi ipsum faucibus vitae aliquet nec. Nibh ipsum consequat nisl vel pretium lectus quam id leo. Faucibus turpis in eu mi bibendum neque egestas. + Lectus nulla at volutpat diam ut venenatis tellus. Tellus rutrum tellus pellentesque eu tincidunt tortor. Purus sit amet volutpat consequat mauris nunc congue. Dignissim cras tincidunt lobortis feugiat vivamus at. Ac felis donec et odio pellentesque diam volutpat commodo. Arcu dui vivamus arcu felis. Pulvinar proin gravida hendrerit lectus a. Venenatis lectus magna fringilla urna porttitor rhoncus dolor purus. Maecenas sed enim ut sem viverra aliquet eget. Lacus laoreet non curabitur gravida arcu ac tortor. Laoreet sit amet cursus sit amet dictum. Maecenas accumsan lacus vel facilisis volutpat est velit. +
    +
    +
    +
    + Elementum integer enim neque volutpat ac tincidunt vitae semper quis. Sem integer vitae justo eget magna fermentum iaculis. Nunc lobortis mattis aliquam faucibus purus in massa tempor nec. Imperdiet proin fermentum leo vel orci porta non. Sed enim ut sem viverra aliquet eget sit amet. Rhoncus mattis rhoncus urna neque viverra justo. At tellus at urna condimentum mattis pellentesque. Vel orci porta non pulvinar neque laoreet suspendisse. Consectetur purus ut faucibus pulvinar elementum integer enim. Urna condimentum mattis pellentesque id nibh. Sem integer vitae justo eget magna fermentum. Ultrices vitae auctor eu augue ut lectus arcu bibendum. Adipiscing bibendum est ultricies integer quis auctor elit. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Sapien faucibus et molestie ac feugiat. Tempor id eu nisl nunc mi ipsum. Arcu non sodales neque sodales ut etiam sit amet nisl. Et malesuada fames ac turpis egestas integer eget aliquet nibh. Pellentesque adipiscing commodo elit at imperdiet. Commodo sed egestas egestas fringilla phasellus faucibus scelerisque eleifend. + Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim. Semper risus in hendrerit gravida rutrum quisque. Tempor nec feugiat nisl pretium fusce id. Ipsum consequat nisl vel pretium. Pellentesque eu tincidunt tortor aliquam nulla facilisi cras fermentum. Etiam tempor orci eu lobortis elementum nibh. Eget nullam non nisi est sit amet facilisis magna. Diam vel quam elementum pulvinar. Dolor morbi non arcu risus quis. Nullam ac tortor vitae purus. + Morbi leo urna molestie at elementum eu facilisis sed odio. Purus in mollis nunc sed id. Sit amet est placerat in egestas erat imperdiet sed. Diam sollicitudin tempor id eu nisl nunc mi. Nulla aliquet porttitor lacus luctus accumsan tortor posuere. Ac feugiat sed lectus vestibulum mattis. Potenti nullam ac tortor vitae purus faucibus ornare suspendisse. Faucibus vitae aliquet nec ullamcorper sit amet. Eu consequat ac felis donec et odio pellentesque diam. Morbi tincidunt ornare massa eget egestas purus viverra accumsan. A erat nam at lectus urna duis convallis. Pellentesque elit eget gravida cum sociis. Cursus sit amet dictum sit amet justo donec enim. Vitae justo eget magna fermentum iaculis. Enim ut sem viverra aliquet. Convallis tellus id interdum velit. Orci phasellus egestas tellus rutrum tellus pellentesque eu tincidunt tortor. Venenatis a condimentum vitae sapien. Lacus viverra vitae congue eu consequat ac felis. Diam donec adipiscing tristique risus nec feugiat. + Sit amet mattis vulputate enim nulla aliquet. Quis commodo odio aenean sed adipiscing diam donec adipiscing tristique. Vulputate odio ut enim blandit volutpat. Elit pellentesque habitant morbi tristique senectus et netus. Aliquet lectus proin nibh nisl condimentum id. A iaculis at erat pellentesque adipiscing commodo elit at. Quis ipsum suspendisse ultrices gravida dictum fusce. Sit amet mauris commodo quis imperdiet massa tincidunt. Adipiscing elit ut aliquam purus. A diam maecenas sed enim ut sem viverra aliquet eget. Enim neque volutpat ac tincidunt vitae. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Amet nisl suscipit adipiscing bibendum. Nunc pulvinar sapien et ligula ullamcorper malesuada proin. Nulla facilisi cras fermentum odio eu feugiat. +
    +
    +
    + ${renderComponent('back-to-top', {})} +
    + `, + ); }); - const backToTopSticky = await page.$eval('.ons-back-to-top', (node) => node.classList.contains('ons-back-to-top__sticky')); - expect(backToTopSticky).toBe(true); - }); - it('is enabled when the page is scrolled to the bottom', async () => { - await page.evaluate(() => { - window.scrollTo(0, document.body.scrollHeight); + it('is enabled on page when the page is rendered', async () => { + const backToTop = await page.$('.ons-back-to-top'); + expect(backToTop).not.toBeNull(); }); - const backToTopEnabled = await page.$eval('.ons-back-to-top', (node) => node.classList.contains('ons-back-to-top__enabled')); - expect(backToTopEnabled).toBe(true); - }); - it('changes left margin when the window is resized', async () => { - await page.setViewport({ width: 1300, height: 800 }); - await page.evaluate(() => { - window.scrollTo(0, window.innerHeight * 2); - }); - await new Promise((r) => setTimeout(r, 250)); - const previousWidth = await page.evaluate(() => { - const node = document.querySelector('.ons-back-to-top > .ons-back-to-top__link').children[0]; - return window.getComputedStyle(node).marginLeft; + it('is sticky when scrolled past 2 times the window height', async () => { + await page.evaluate(() => { + window.scrollTo(0, window.innerHeight * 2 + 10); + }); + const backToTopSticky = await page.$eval('.ons-back-to-top', (node) => node.classList.contains('ons-back-to-top__sticky')); + expect(backToTopSticky).toBe(true); }); - await page.setViewport({ width: 2000, height: 800 }); - await page.evaluate(() => { - window.scrollTo(0, window.innerHeight * 2); + + it('is enabled when the page is scrolled to the bottom', async () => { + await page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + const backToTopEnabled = await page.$eval('.ons-back-to-top', (node) => node.classList.contains('ons-back-to-top__enabled')); + expect(backToTopEnabled).toBe(true); }); - await new Promise((r) => setTimeout(r, 250)); - const newWidth = await page.evaluate(() => { - const node = document.querySelector('.ons-back-to-top > .ons-back-to-top__link').children[0]; - return window.getComputedStyle(node).marginLeft; + + it('changes left margin when the window is resized', async () => { + await page.setViewport({ width: 1300, height: 800 }); + await page.evaluate(() => { + window.scrollTo(0, window.innerHeight * 2); + }); + await new Promise((r) => setTimeout(r, 250)); + const previousWidth = await page.evaluate(() => { + const node = document.querySelector('.ons-back-to-top > .ons-back-to-top__link').children[0]; + return window.getComputedStyle(node).marginLeft; + }); + await page.setViewport({ width: 2000, height: 800 }); + await page.evaluate(() => { + window.scrollTo(0, window.innerHeight * 2); + }); + await new Promise((r) => setTimeout(r, 250)); + const newWidth = await page.evaluate(() => { + const node = document.querySelector('.ons-back-to-top > .ons-back-to-top__link').children[0]; + return window.getComputedStyle(node).marginLeft; + }); + expect(previousWidth).not.toEqual(newWidth); }); - expect(previousWidth).not.toEqual(newWidth); - }); }); diff --git a/src/components/back-to-top/example-back-to-top.njk b/src/components/back-to-top/example-back-to-top.njk index 836a6fdff0..1ef433e9d3 100644 --- a/src/components/back-to-top/example-back-to-top.njk +++ b/src/components/back-to-top/example-back-to-top.njk @@ -3,10 +3,10 @@
    -
    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Massa tincidunt nunc pulvinar sapien et ligula ullamcorper. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in. Eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis. Enim neque volutpat ac tincidunt. Tortor pretium viverra suspendisse potenti nullam ac tortor. Sed vulputate mi sit amet mauris commodo quis imperdiet massa. Odio morbi quis commodo odio. Lobortis feugiat vivamus at augue eget. Aliquam id diam maecenas ultricies mi eget. Dictum non consectetur a erat nam at lectus urna. Justo laoreet sit amet cursus sit amet. Tristique senectus et netus et malesuada fames. Etiam dignissim diam quis enim. Urna id volutpat lacus laoreet non curabitur. Proin sagittis nisl rhoncus mattis. Vel pretium lectus quam id leo in vitae turpis. Orci nulla pellentesque dignissim enim sit amet venenatis urna. Scelerisque eleifend donec pretium vulputate sapien nec sagittis. Quis commodo odio aenean sed adipiscing. Metus vulputate eu scelerisque felis imperdiet proin fermentum. Pellentesque sit amet porttitor eget dolor. Habitant morbi tristique senectus et netus et malesuada. Curabitur vitae nunc sed velit. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat. Id leo in vitae turpis massa. Vitae ultricies leo integer malesuada. Elit at imperdiet dui accumsan sit amet nulla. Pretium quam vulputate dignissim suspendisse in est ante. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Mattis aliquam faucibus purus in massa tempor nec. Orci a scelerisque purus semper. Vel fringilla est ullamcorper eget nulla facilisi. Ac ut consequat semper viverra nam libero justo laoreet sit. Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Lobortis scelerisque fermentum dui faucibus in ornare quam. Diam vel quam elementum pulvinar etiam. Amet tellus cras adipiscing enim eu turpis egestas pretium aenean. Pulvinar sapien et ligula ullamcorper malesuada. Risus pretium quam vulputate dignissim suspendisse in est. Facilisi etiam dignissim diam quis enim. Sodales ut eu sem integer vitae. Eget nunc scelerisque viverra mauris. Malesuada bibendum arcu vitae elementum curabitur. Elementum nisi quis eleifend quam adipiscing vitae proin. Ultrices vitae auctor eu augue. Hac habitasse platea dictumst vestibulum rhoncus est. Urna neque viverra justo nec ultrices dui sapien eget mi. Quisque egestas diam in arcu cursus euismod quis viverra. Nullam ac tortor vitae purus faucibus. Fames ac turpis egestas maecenas. Magna fermentum iaculis eu non diam phasellus vestibulum. Nisl nunc mi ipsum faucibus vitae aliquet nec. Nibh ipsum consequat nisl vel pretium lectus quam id leo. Faucibus turpis in eu mi bibendum neque egestas. Lectus nulla at volutpat diam ut venenatis tellus. Tellus rutrum tellus pellentesque eu tincidunt tortor. Purus sit amet volutpat consequat mauris nunc congue. Dignissim cras tincidunt lobortis feugiat vivamus at. Ac felis donec et odio pellentesque diam volutpat commodo. Arcu dui vivamus arcu felis. Pulvinar proin gravida hendrerit lectus a. Venenatis lectus magna fringilla urna porttitor rhoncus dolor purus. Maecenas sed enim ut sem viverra aliquet eget. Lacus laoreet non curabitur gravida arcu ac tortor. Laoreet sit amet cursus sit amet dictum. Maecenas accumsan lacus vel facilisis volutpat est velit.
    +
    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Massa tincidunt nunc pulvinar sapien et ligula ullamcorper. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in. Eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis. Enim neque volutpat ac tincidunt. Tortor pretium viverra suspendisse potenti nullam ac tortor. Sed vulputate mi sit amet mauris commodo quis imperdiet massa. Odio morbi quis commodo odio. Lobortis feugiat vivamus at augue eget. Aliquam id diam maecenas ultricies mi eget. Dictum non consectetur a erat nam at lectus urna. Justo laoreet sit amet cursus sit amet. Tristique senectus et netus et malesuada fames. Etiam dignissim diam quis enim. Urna id volutpat lacus laoreet non curabitur. Proin sagittis nisl rhoncus mattis. Vel pretium lectus quam id leo in vitae turpis. Orci nulla pellentesque dignissim enim sit amet venenatis urna. Scelerisque eleifend donec pretium vulputate sapien nec sagittis. Quis commodo odio aenean sed adipiscing. Metus vulputate eu scelerisque felis imperdiet proin fermentum. Pellentesque sit amet porttitor eget dolor. Habitant morbi tristique senectus et netus et malesuada. Curabitur vitae nunc sed velit. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat. Id leo in vitae turpis massa. Vitae ultricies leo integer malesuada. Elit at imperdiet dui accumsan sit amet nulla. Pretium quam vulputate dignissim suspendisse in est ante. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Mattis aliquam faucibus purus in massa tempor nec. Orci a scelerisque purus semper. Vel fringilla est ullamcorper eget nulla facilisi. Ac ut consequat semper viverra nam libero justo laoreet sit. Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Lobortis scelerisque fermentum dui faucibus in ornare quam. Diam vel quam elementum pulvinar etiam. Amet tellus cras adipiscing enim eu turpis egestas pretium aenean. Pulvinar sapien et ligula ullamcorper malesuada. Risus pretium quam vulputate dignissim suspendisse in est. Facilisi etiam dignissim diam quis enim. Sodales ut eu sem integer vitae. Eget nunc scelerisque viverra mauris. Malesuada bibendum arcu vitae elementum curabitur. Elementum nisi quis eleifend quam adipiscing vitae proin. Ultrices vitae auctor eu augue. Hac habitasse platea dictumst vestibulum rhoncus est. Urna neque viverra justo nec ultrices dui sapien eget mi. Quisque egestas diam in arcu cursus euismod quis viverra. Nullam ac tortor vitae purus faucibus. Fames ac turpis egestas maecenas. Magna fermentum iaculis eu non diam phasellus vestibulum. Nisl nunc mi ipsum faucibus vitae aliquet nec. Nibh ipsum consequat nisl vel pretium lectus quam id leo. Faucibus turpis in eu mi bibendum neque egestas. Lectus nulla at volutpat diam ut venenatis tellus. Tellus rutrum tellus pellentesque eu tincidunt tortor. Purus sit amet volutpat consequat mauris nunc congue. Dignissim cras tincidunt lobortis feugiat vivamus at. Ac felis donec et odio pellentesque diam volutpat commodo. Arcu dui vivamus arcu felis. Pulvinar proin gravida hendrerit lectus a. Venenatis lectus magna fringilla urna porttitor rhoncus dolor purus. Maecenas sed enim ut sem viverra aliquet eget. Lacus laoreet non curabitur gravida arcu ac tortor. Laoreet sit amet cursus sit amet dictum. Maecenas accumsan lacus vel facilisis volutpat est velit.
    -
    Elementum integer enim neque volutpat ac tincidunt vitae semper quis. Sem integer vitae justo eget magna fermentum iaculis. Nunc lobortis mattis aliquam faucibus purus in massa tempor nec. Imperdiet proin fermentum leo vel orci porta non. Sed enim ut sem viverra aliquet eget sit amet. Rhoncus mattis rhoncus urna neque viverra justo. At tellus at urna condimentum mattis pellentesque. Vel orci porta non pulvinar neque laoreet suspendisse. Consectetur purus ut faucibus pulvinar elementum integer enim. Urna condimentum mattis pellentesque id nibh. Sem integer vitae justo eget magna fermentum. Ultrices vitae auctor eu augue ut lectus arcu bibendum. Adipiscing bibendum est ultricies integer quis auctor elit. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Sapien faucibus et molestie ac feugiat. Tempor id eu nisl nunc mi ipsum. Arcu non sodales neque sodales ut etiam sit amet nisl. Et malesuada fames ac turpis egestas integer eget aliquet nibh. Pellentesque adipiscing commodo elit at imperdiet. Commodo sed egestas egestas fringilla phasellus faucibus scelerisque eleifend. Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim. Semper risus in hendrerit gravida rutrum quisque. Tempor nec feugiat nisl pretium fusce id. Ipsum consequat nisl vel pretium. Pellentesque eu tincidunt tortor aliquam nulla facilisi cras fermentum. Etiam tempor orci eu lobortis elementum nibh. Eget nullam non nisi est sit amet facilisis magna. Diam vel quam elementum pulvinar. Dolor morbi non arcu risus quis. Nullam ac tortor vitae purus. Morbi leo urna molestie at elementum eu facilisis sed odio. Purus in mollis nunc sed id. Sit amet est placerat in egestas erat imperdiet sed. Diam sollicitudin tempor id eu nisl nunc mi. Nulla aliquet porttitor lacus luctus accumsan tortor posuere. Ac feugiat sed lectus vestibulum mattis. Potenti nullam ac tortor vitae purus faucibus ornare suspendisse. Faucibus vitae aliquet nec ullamcorper sit amet. Eu consequat ac felis donec et odio pellentesque diam. Morbi tincidunt ornare massa eget egestas purus viverra accumsan. A erat nam at lectus urna duis convallis. Pellentesque elit eget gravida cum sociis. Cursus sit amet dictum sit amet justo donec enim. Vitae justo eget magna fermentum iaculis. Enim ut sem viverra aliquet. Convallis tellus id interdum velit. Orci phasellus egestas tellus rutrum tellus pellentesque eu tincidunt tortor. Venenatis a condimentum vitae sapien. Lacus viverra vitae congue eu consequat ac felis. Diam donec adipiscing tristique risus nec feugiat. Sit amet mattis vulputate enim nulla aliquet. Quis commodo odio aenean sed adipiscing diam donec adipiscing tristique. Vulputate odio ut enim blandit volutpat. Elit pellentesque habitant morbi tristique senectus et netus. Aliquet lectus proin nibh nisl condimentum id. A iaculis at erat pellentesque adipiscing commodo elit at. Quis ipsum suspendisse ultrices gravida dictum fusce. Sit amet mauris commodo quis imperdiet massa tincidunt. Adipiscing elit ut aliquam purus. A diam maecenas sed enim ut sem viverra aliquet eget. Enim neque volutpat ac tincidunt vitae. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Amet nisl suscipit adipiscing bibendum. Nunc pulvinar sapien et ligula ullamcorper malesuada proin. Nulla facilisi cras fermentum odio eu feugiat.
    +
    Elementum integer enim neque volutpat ac tincidunt vitae semper quis. Sem integer vitae justo eget magna fermentum iaculis. Nunc lobortis mattis aliquam faucibus purus in massa tempor nec. Imperdiet proin fermentum leo vel orci porta non. Sed enim ut sem viverra aliquet eget sit amet. Rhoncus mattis rhoncus urna neque viverra justo. At tellus at urna condimentum mattis pellentesque. Vel orci porta non pulvinar neque laoreet suspendisse. Consectetur purus ut faucibus pulvinar elementum integer enim. Urna condimentum mattis pellentesque id nibh. Sem integer vitae justo eget magna fermentum. Ultrices vitae auctor eu augue ut lectus arcu bibendum. Adipiscing bibendum est ultricies integer quis auctor elit. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Sapien faucibus et molestie ac feugiat. Tempor id eu nisl nunc mi ipsum. Arcu non sodales neque sodales ut etiam sit amet nisl. Et malesuada fames ac turpis egestas integer eget aliquet nibh. Pellentesque adipiscing commodo elit at imperdiet. Commodo sed egestas egestas fringilla phasellus faucibus scelerisque eleifend. Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim. Semper risus in hendrerit gravida rutrum quisque. Tempor nec feugiat nisl pretium fusce id. Ipsum consequat nisl vel pretium. Pellentesque eu tincidunt tortor aliquam nulla facilisi cras fermentum. Etiam tempor orci eu lobortis elementum nibh. Eget nullam non nisi est sit amet facilisis magna. Diam vel quam elementum pulvinar. Dolor morbi non arcu risus quis. Nullam ac tortor vitae purus. Morbi leo urna molestie at elementum eu facilisis sed odio. Purus in mollis nunc sed id. Sit amet est placerat in egestas erat imperdiet sed. Diam sollicitudin tempor id eu nisl nunc mi. Nulla aliquet porttitor lacus luctus accumsan tortor posuere. Ac feugiat sed lectus vestibulum mattis. Potenti nullam ac tortor vitae purus faucibus ornare suspendisse. Faucibus vitae aliquet nec ullamcorper sit amet. Eu consequat ac felis donec et odio pellentesque diam. Morbi tincidunt ornare massa eget egestas purus viverra accumsan. A erat nam at lectus urna duis convallis. Pellentesque elit eget gravida cum sociis. Cursus sit amet dictum sit amet justo donec enim. Vitae justo eget magna fermentum iaculis. Enim ut sem viverra aliquet. Convallis tellus id interdum velit. Orci phasellus egestas tellus rutrum tellus pellentesque eu tincidunt tortor. Venenatis a condimentum vitae sapien. Lacus viverra vitae congue eu consequat ac felis. Diam donec adipiscing tristique risus nec feugiat. Sit amet mattis vulputate enim nulla aliquet. Quis commodo odio aenean sed adipiscing diam donec adipiscing tristique. Vulputate odio ut enim blandit volutpat. Elit pellentesque habitant morbi tristique senectus et netus. Aliquet lectus proin nibh nisl condimentum id. A iaculis at erat pellentesque adipiscing commodo elit at. Quis ipsum suspendisse ultrices gravida dictum fusce. Sit amet mauris commodo quis imperdiet massa tincidunt. Adipiscing elit ut aliquam purus. A diam maecenas sed enim ut sem viverra aliquet eget. Enim neque volutpat ac tincidunt vitae. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Amet nisl suscipit adipiscing bibendum. Nunc pulvinar sapien et ligula ullamcorper malesuada proin. Nulla facilisi cras fermentum odio eu feugiat.
    @@ -14,18 +14,18 @@
    -
    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Massa tincidunt nunc pulvinar sapien et ligula ullamcorper. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in. Eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis. Enim neque volutpat ac tincidunt. Tortor pretium viverra suspendisse potenti nullam ac tortor. Sed vulputate mi sit amet mauris commodo quis imperdiet massa. Odio morbi quis commodo odio. Lobortis feugiat vivamus at augue eget. Aliquam id diam maecenas ultricies mi eget. Dictum non consectetur a erat nam at lectus urna. Justo laoreet sit amet cursus sit amet. Tristique senectus et netus et malesuada fames. Etiam dignissim diam quis enim. Urna id volutpat lacus laoreet non curabitur. Proin sagittis nisl rhoncus mattis. Vel pretium lectus quam id leo in vitae turpis. Orci nulla pellentesque dignissim enim sit amet venenatis urna. Scelerisque eleifend donec pretium vulputate sapien nec sagittis. Quis commodo odio aenean sed adipiscing. Metus vulputate eu scelerisque felis imperdiet proin fermentum. Pellentesque sit amet porttitor eget dolor. Habitant morbi tristique senectus et netus et malesuada. Curabitur vitae nunc sed velit. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat. Id leo in vitae turpis massa. Vitae ultricies leo integer malesuada. Elit at imperdiet dui accumsan sit amet nulla. Pretium quam vulputate dignissim suspendisse in est ante. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Mattis aliquam faucibus purus in massa tempor nec. Orci a scelerisque purus semper. Vel fringilla est ullamcorper eget nulla facilisi. Ac ut consequat semper viverra nam libero justo laoreet sit. Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Lobortis scelerisque fermentum dui faucibus in ornare quam. Diam vel quam elementum pulvinar etiam. Amet tellus cras adipiscing enim eu turpis egestas pretium aenean. Pulvinar sapien et ligula ullamcorper malesuada. Risus pretium quam vulputate dignissim suspendisse in est. Facilisi etiam dignissim diam quis enim. Sodales ut eu sem integer vitae. Eget nunc scelerisque viverra mauris. Malesuada bibendum arcu vitae elementum curabitur. Elementum nisi quis eleifend quam adipiscing vitae proin. Ultrices vitae auctor eu augue. Hac habitasse platea dictumst vestibulum rhoncus est. Urna neque viverra justo nec ultrices dui sapien eget mi. Quisque egestas diam in arcu cursus euismod quis viverra. Nullam ac tortor vitae purus faucibus. Fames ac turpis egestas maecenas. Magna fermentum iaculis eu non diam phasellus vestibulum. Nisl nunc mi ipsum faucibus vitae aliquet nec. Nibh ipsum consequat nisl vel pretium lectus quam id leo. Faucibus turpis in eu mi bibendum neque egestas. Lectus nulla at volutpat diam ut venenatis tellus. Tellus rutrum tellus pellentesque eu tincidunt tortor. Purus sit amet volutpat consequat mauris nunc congue. Dignissim cras tincidunt lobortis feugiat vivamus at. Ac felis donec et odio pellentesque diam volutpat commodo. Arcu dui vivamus arcu felis. Pulvinar proin gravida hendrerit lectus a. Venenatis lectus magna fringilla urna porttitor rhoncus dolor purus. Maecenas sed enim ut sem viverra aliquet eget. Lacus laoreet non curabitur gravida arcu ac tortor. Laoreet sit amet cursus sit amet dictum. Maecenas accumsan lacus vel facilisis volutpat est velit.
    +
    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Massa tincidunt nunc pulvinar sapien et ligula ullamcorper. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in. Eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis. Enim neque volutpat ac tincidunt. Tortor pretium viverra suspendisse potenti nullam ac tortor. Sed vulputate mi sit amet mauris commodo quis imperdiet massa. Odio morbi quis commodo odio. Lobortis feugiat vivamus at augue eget. Aliquam id diam maecenas ultricies mi eget. Dictum non consectetur a erat nam at lectus urna. Justo laoreet sit amet cursus sit amet. Tristique senectus et netus et malesuada fames. Etiam dignissim diam quis enim. Urna id volutpat lacus laoreet non curabitur. Proin sagittis nisl rhoncus mattis. Vel pretium lectus quam id leo in vitae turpis. Orci nulla pellentesque dignissim enim sit amet venenatis urna. Scelerisque eleifend donec pretium vulputate sapien nec sagittis. Quis commodo odio aenean sed adipiscing. Metus vulputate eu scelerisque felis imperdiet proin fermentum. Pellentesque sit amet porttitor eget dolor. Habitant morbi tristique senectus et netus et malesuada. Curabitur vitae nunc sed velit. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat. Id leo in vitae turpis massa. Vitae ultricies leo integer malesuada. Elit at imperdiet dui accumsan sit amet nulla. Pretium quam vulputate dignissim suspendisse in est ante. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Mattis aliquam faucibus purus in massa tempor nec. Orci a scelerisque purus semper. Vel fringilla est ullamcorper eget nulla facilisi. Ac ut consequat semper viverra nam libero justo laoreet sit. Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Lobortis scelerisque fermentum dui faucibus in ornare quam. Diam vel quam elementum pulvinar etiam. Amet tellus cras adipiscing enim eu turpis egestas pretium aenean. Pulvinar sapien et ligula ullamcorper malesuada. Risus pretium quam vulputate dignissim suspendisse in est. Facilisi etiam dignissim diam quis enim. Sodales ut eu sem integer vitae. Eget nunc scelerisque viverra mauris. Malesuada bibendum arcu vitae elementum curabitur. Elementum nisi quis eleifend quam adipiscing vitae proin. Ultrices vitae auctor eu augue. Hac habitasse platea dictumst vestibulum rhoncus est. Urna neque viverra justo nec ultrices dui sapien eget mi. Quisque egestas diam in arcu cursus euismod quis viverra. Nullam ac tortor vitae purus faucibus. Fames ac turpis egestas maecenas. Magna fermentum iaculis eu non diam phasellus vestibulum. Nisl nunc mi ipsum faucibus vitae aliquet nec. Nibh ipsum consequat nisl vel pretium lectus quam id leo. Faucibus turpis in eu mi bibendum neque egestas. Lectus nulla at volutpat diam ut venenatis tellus. Tellus rutrum tellus pellentesque eu tincidunt tortor. Purus sit amet volutpat consequat mauris nunc congue. Dignissim cras tincidunt lobortis feugiat vivamus at. Ac felis donec et odio pellentesque diam volutpat commodo. Arcu dui vivamus arcu felis. Pulvinar proin gravida hendrerit lectus a. Venenatis lectus magna fringilla urna porttitor rhoncus dolor purus. Maecenas sed enim ut sem viverra aliquet eget. Lacus laoreet non curabitur gravida arcu ac tortor. Laoreet sit amet cursus sit amet dictum. Maecenas accumsan lacus vel facilisis volutpat est velit.
    -
    Elementum integer enim neque volutpat ac tincidunt vitae semper quis. Sem integer vitae justo eget magna fermentum iaculis. Nunc lobortis mattis aliquam faucibus purus in massa tempor nec. Imperdiet proin fermentum leo vel orci porta non. Sed enim ut sem viverra aliquet eget sit amet. Rhoncus mattis rhoncus urna neque viverra justo. At tellus at urna condimentum mattis pellentesque. Vel orci porta non pulvinar neque laoreet suspendisse. Consectetur purus ut faucibus pulvinar elementum integer enim. Urna condimentum mattis pellentesque id nibh. Sem integer vitae justo eget magna fermentum. Ultrices vitae auctor eu augue ut lectus arcu bibendum. Adipiscing bibendum est ultricies integer quis auctor elit. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Sapien faucibus et molestie ac feugiat. Tempor id eu nisl nunc mi ipsum. Arcu non sodales neque sodales ut etiam sit amet nisl. Et malesuada fames ac turpis egestas integer eget aliquet nibh. Pellentesque adipiscing commodo elit at imperdiet. Commodo sed egestas egestas fringilla phasellus faucibus scelerisque eleifend. Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim. Semper risus in hendrerit gravida rutrum quisque. Tempor nec feugiat nisl pretium fusce id. Ipsum consequat nisl vel pretium. Pellentesque eu tincidunt tortor aliquam nulla facilisi cras fermentum. Etiam tempor orci eu lobortis elementum nibh. Eget nullam non nisi est sit amet facilisis magna. Diam vel quam elementum pulvinar. Dolor morbi non arcu risus quis. Nullam ac tortor vitae purus. Morbi leo urna molestie at elementum eu facilisis sed odio. Purus in mollis nunc sed id. Sit amet est placerat in egestas erat imperdiet sed. Diam sollicitudin tempor id eu nisl nunc mi. Nulla aliquet porttitor lacus luctus accumsan tortor posuere. Ac feugiat sed lectus vestibulum mattis. Potenti nullam ac tortor vitae purus faucibus ornare suspendisse. Faucibus vitae aliquet nec ullamcorper sit amet. Eu consequat ac felis donec et odio pellentesque diam. Morbi tincidunt ornare massa eget egestas purus viverra accumsan. A erat nam at lectus urna duis convallis. Pellentesque elit eget gravida cum sociis. Cursus sit amet dictum sit amet justo donec enim. Vitae justo eget magna fermentum iaculis. Enim ut sem viverra aliquet. Convallis tellus id interdum velit. Orci phasellus egestas tellus rutrum tellus pellentesque eu tincidunt tortor. Venenatis a condimentum vitae sapien. Lacus viverra vitae congue eu consequat ac felis. Diam donec adipiscing tristique risus nec feugiat. Sit amet mattis vulputate enim nulla aliquet. Quis commodo odio aenean sed adipiscing diam donec adipiscing tristique. Vulputate odio ut enim blandit volutpat. Elit pellentesque habitant morbi tristique senectus et netus. Aliquet lectus proin nibh nisl condimentum id. A iaculis at erat pellentesque adipiscing commodo elit at. Quis ipsum suspendisse ultrices gravida dictum fusce. Sit amet mauris commodo quis imperdiet massa tincidunt. Adipiscing elit ut aliquam purus. A diam maecenas sed enim ut sem viverra aliquet eget. Enim neque volutpat ac tincidunt vitae. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Amet nisl suscipit adipiscing bibendum. Nunc pulvinar sapien et ligula ullamcorper malesuada proin. Nulla facilisi cras fermentum odio eu feugiat.
    +
    Elementum integer enim neque volutpat ac tincidunt vitae semper quis. Sem integer vitae justo eget magna fermentum iaculis. Nunc lobortis mattis aliquam faucibus purus in massa tempor nec. Imperdiet proin fermentum leo vel orci porta non. Sed enim ut sem viverra aliquet eget sit amet. Rhoncus mattis rhoncus urna neque viverra justo. At tellus at urna condimentum mattis pellentesque. Vel orci porta non pulvinar neque laoreet suspendisse. Consectetur purus ut faucibus pulvinar elementum integer enim. Urna condimentum mattis pellentesque id nibh. Sem integer vitae justo eget magna fermentum. Ultrices vitae auctor eu augue ut lectus arcu bibendum. Adipiscing bibendum est ultricies integer quis auctor elit. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Sapien faucibus et molestie ac feugiat. Tempor id eu nisl nunc mi ipsum. Arcu non sodales neque sodales ut etiam sit amet nisl. Et malesuada fames ac turpis egestas integer eget aliquet nibh. Pellentesque adipiscing commodo elit at imperdiet. Commodo sed egestas egestas fringilla phasellus faucibus scelerisque eleifend. Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim. Semper risus in hendrerit gravida rutrum quisque. Tempor nec feugiat nisl pretium fusce id. Ipsum consequat nisl vel pretium. Pellentesque eu tincidunt tortor aliquam nulla facilisi cras fermentum. Etiam tempor orci eu lobortis elementum nibh. Eget nullam non nisi est sit amet facilisis magna. Diam vel quam elementum pulvinar. Dolor morbi non arcu risus quis. Nullam ac tortor vitae purus. Morbi leo urna molestie at elementum eu facilisis sed odio. Purus in mollis nunc sed id. Sit amet est placerat in egestas erat imperdiet sed. Diam sollicitudin tempor id eu nisl nunc mi. Nulla aliquet porttitor lacus luctus accumsan tortor posuere. Ac feugiat sed lectus vestibulum mattis. Potenti nullam ac tortor vitae purus faucibus ornare suspendisse. Faucibus vitae aliquet nec ullamcorper sit amet. Eu consequat ac felis donec et odio pellentesque diam. Morbi tincidunt ornare massa eget egestas purus viverra accumsan. A erat nam at lectus urna duis convallis. Pellentesque elit eget gravida cum sociis. Cursus sit amet dictum sit amet justo donec enim. Vitae justo eget magna fermentum iaculis. Enim ut sem viverra aliquet. Convallis tellus id interdum velit. Orci phasellus egestas tellus rutrum tellus pellentesque eu tincidunt tortor. Venenatis a condimentum vitae sapien. Lacus viverra vitae congue eu consequat ac felis. Diam donec adipiscing tristique risus nec feugiat. Sit amet mattis vulputate enim nulla aliquet. Quis commodo odio aenean sed adipiscing diam donec adipiscing tristique. Vulputate odio ut enim blandit volutpat. Elit pellentesque habitant morbi tristique senectus et netus. Aliquet lectus proin nibh nisl condimentum id. A iaculis at erat pellentesque adipiscing commodo elit at. Quis ipsum suspendisse ultrices gravida dictum fusce. Sit amet mauris commodo quis imperdiet massa tincidunt. Adipiscing elit ut aliquam purus. A diam maecenas sed enim ut sem viverra aliquet eget. Enim neque volutpat ac tincidunt vitae. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Amet nisl suscipit adipiscing bibendum. Nunc pulvinar sapien et ligula ullamcorper malesuada proin. Nulla facilisi cras fermentum odio eu feugiat.
    -
    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Massa tincidunt nunc pulvinar sapien et ligula ullamcorper. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in. Eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis. Enim neque volutpat ac tincidunt. Tortor pretium viverra suspendisse potenti nullam ac tortor. Sed vulputate mi sit amet mauris commodo quis imperdiet massa. Odio morbi quis commodo odio. Lobortis feugiat vivamus at augue eget. Aliquam id diam maecenas ultricies mi eget. Dictum non consectetur a erat nam at lectus urna. Justo laoreet sit amet cursus sit amet. Tristique senectus et netus et malesuada fames. Etiam dignissim diam quis enim. Urna id volutpat lacus laoreet non curabitur. Proin sagittis nisl rhoncus mattis. Vel pretium lectus quam id leo in vitae turpis. Orci nulla pellentesque dignissim enim sit amet venenatis urna. Scelerisque eleifend donec pretium vulputate sapien nec sagittis. Quis commodo odio aenean sed adipiscing. Metus vulputate eu scelerisque felis imperdiet proin fermentum. Pellentesque sit amet porttitor eget dolor. Habitant morbi tristique senectus et netus et malesuada. Curabitur vitae nunc sed velit. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat. Id leo in vitae turpis massa. Vitae ultricies leo integer malesuada. Elit at imperdiet dui accumsan sit amet nulla. Pretium quam vulputate dignissim suspendisse in est ante. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Mattis aliquam faucibus purus in massa tempor nec. Orci a scelerisque purus semper. Vel fringilla est ullamcorper eget nulla facilisi. Ac ut consequat semper viverra nam libero justo laoreet sit. Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Lobortis scelerisque fermentum dui faucibus in ornare quam. Diam vel quam elementum pulvinar etiam. Amet tellus cras adipiscing enim eu turpis egestas pretium aenean. Pulvinar sapien et ligula ullamcorper malesuada. Risus pretium quam vulputate dignissim suspendisse in est. Facilisi etiam dignissim diam quis enim. Sodales ut eu sem integer vitae. Eget nunc scelerisque viverra mauris. Malesuada bibendum arcu vitae elementum curabitur. Elementum nisi quis eleifend quam adipiscing vitae proin. Ultrices vitae auctor eu augue. Hac habitasse platea dictumst vestibulum rhoncus est. Urna neque viverra justo nec ultrices dui sapien eget mi. Quisque egestas diam in arcu cursus euismod quis viverra. Nullam ac tortor vitae purus faucibus. Fames ac turpis egestas maecenas. Magna fermentum iaculis eu non diam phasellus vestibulum. Nisl nunc mi ipsum faucibus vitae aliquet nec. Nibh ipsum consequat nisl vel pretium lectus quam id leo. Faucibus turpis in eu mi bibendum neque egestas. Lectus nulla at volutpat diam ut venenatis tellus. Tellus rutrum tellus pellentesque eu tincidunt tortor. Purus sit amet volutpat consequat mauris nunc congue. Dignissim cras tincidunt lobortis feugiat vivamus at. Ac felis donec et odio pellentesque diam volutpat commodo. Arcu dui vivamus arcu felis. Pulvinar proin gravida hendrerit lectus a. Venenatis lectus magna fringilla urna porttitor rhoncus dolor purus. Maecenas sed enim ut sem viverra aliquet eget. Lacus laoreet non curabitur gravida arcu ac tortor. Laoreet sit amet cursus sit amet dictum. Maecenas accumsan lacus vel facilisis volutpat est velit.
    +
    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Massa tincidunt nunc pulvinar sapien et ligula ullamcorper. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in. Eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis. Enim neque volutpat ac tincidunt. Tortor pretium viverra suspendisse potenti nullam ac tortor. Sed vulputate mi sit amet mauris commodo quis imperdiet massa. Odio morbi quis commodo odio. Lobortis feugiat vivamus at augue eget. Aliquam id diam maecenas ultricies mi eget. Dictum non consectetur a erat nam at lectus urna. Justo laoreet sit amet cursus sit amet. Tristique senectus et netus et malesuada fames. Etiam dignissim diam quis enim. Urna id volutpat lacus laoreet non curabitur. Proin sagittis nisl rhoncus mattis. Vel pretium lectus quam id leo in vitae turpis. Orci nulla pellentesque dignissim enim sit amet venenatis urna. Scelerisque eleifend donec pretium vulputate sapien nec sagittis. Quis commodo odio aenean sed adipiscing. Metus vulputate eu scelerisque felis imperdiet proin fermentum. Pellentesque sit amet porttitor eget dolor. Habitant morbi tristique senectus et netus et malesuada. Curabitur vitae nunc sed velit. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat. Id leo in vitae turpis massa. Vitae ultricies leo integer malesuada. Elit at imperdiet dui accumsan sit amet nulla. Pretium quam vulputate dignissim suspendisse in est ante. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Mattis aliquam faucibus purus in massa tempor nec. Orci a scelerisque purus semper. Vel fringilla est ullamcorper eget nulla facilisi. Ac ut consequat semper viverra nam libero justo laoreet sit. Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Lobortis scelerisque fermentum dui faucibus in ornare quam. Diam vel quam elementum pulvinar etiam. Amet tellus cras adipiscing enim eu turpis egestas pretium aenean. Pulvinar sapien et ligula ullamcorper malesuada. Risus pretium quam vulputate dignissim suspendisse in est. Facilisi etiam dignissim diam quis enim. Sodales ut eu sem integer vitae. Eget nunc scelerisque viverra mauris. Malesuada bibendum arcu vitae elementum curabitur. Elementum nisi quis eleifend quam adipiscing vitae proin. Ultrices vitae auctor eu augue. Hac habitasse platea dictumst vestibulum rhoncus est. Urna neque viverra justo nec ultrices dui sapien eget mi. Quisque egestas diam in arcu cursus euismod quis viverra. Nullam ac tortor vitae purus faucibus. Fames ac turpis egestas maecenas. Magna fermentum iaculis eu non diam phasellus vestibulum. Nisl nunc mi ipsum faucibus vitae aliquet nec. Nibh ipsum consequat nisl vel pretium lectus quam id leo. Faucibus turpis in eu mi bibendum neque egestas. Lectus nulla at volutpat diam ut venenatis tellus. Tellus rutrum tellus pellentesque eu tincidunt tortor. Purus sit amet volutpat consequat mauris nunc congue. Dignissim cras tincidunt lobortis feugiat vivamus at. Ac felis donec et odio pellentesque diam volutpat commodo. Arcu dui vivamus arcu felis. Pulvinar proin gravida hendrerit lectus a. Venenatis lectus magna fringilla urna porttitor rhoncus dolor purus. Maecenas sed enim ut sem viverra aliquet eget. Lacus laoreet non curabitur gravida arcu ac tortor. Laoreet sit amet cursus sit amet dictum. Maecenas accumsan lacus vel facilisis volutpat est velit.
    -
    Elementum integer enim neque volutpat ac tincidunt vitae semper quis. Sem integer vitae justo eget magna fermentum iaculis. Nunc lobortis mattis aliquam faucibus purus in massa tempor nec. Imperdiet proin fermentum leo vel orci porta non. Sed enim ut sem viverra aliquet eget sit amet. Rhoncus mattis rhoncus urna neque viverra justo. At tellus at urna condimentum mattis pellentesque. Vel orci porta non pulvinar neque laoreet suspendisse. Consectetur purus ut faucibus pulvinar elementum integer enim. Urna condimentum mattis pellentesque id nibh. Sem integer vitae justo eget magna fermentum. Ultrices vitae auctor eu augue ut lectus arcu bibendum. Adipiscing bibendum est ultricies integer quis auctor elit. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Sapien faucibus et molestie ac feugiat. Tempor id eu nisl nunc mi ipsum. Arcu non sodales neque sodales ut etiam sit amet nisl. Et malesuada fames ac turpis egestas integer eget aliquet nibh. Pellentesque adipiscing commodo elit at imperdiet. Commodo sed egestas egestas fringilla phasellus faucibus scelerisque eleifend. Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim. Semper risus in hendrerit gravida rutrum quisque. Tempor nec feugiat nisl pretium fusce id. Ipsum consequat nisl vel pretium. Pellentesque eu tincidunt tortor aliquam nulla facilisi cras fermentum. Etiam tempor orci eu lobortis elementum nibh. Eget nullam non nisi est sit amet facilisis magna. Diam vel quam elementum pulvinar. Dolor morbi non arcu risus quis. Nullam ac tortor vitae purus. Morbi leo urna molestie at elementum eu facilisis sed odio. Purus in mollis nunc sed id. Sit amet est placerat in egestas erat imperdiet sed. Diam sollicitudin tempor id eu nisl nunc mi. Nulla aliquet porttitor lacus luctus accumsan tortor posuere. Ac feugiat sed lectus vestibulum mattis. Potenti nullam ac tortor vitae purus faucibus ornare suspendisse. Faucibus vitae aliquet nec ullamcorper sit amet. Eu consequat ac felis donec et odio pellentesque diam. Morbi tincidunt ornare massa eget egestas purus viverra accumsan. A erat nam at lectus urna duis convallis. Pellentesque elit eget gravida cum sociis. Cursus sit amet dictum sit amet justo donec enim. Vitae justo eget magna fermentum iaculis. Enim ut sem viverra aliquet. Convallis tellus id interdum velit. Orci phasellus egestas tellus rutrum tellus pellentesque eu tincidunt tortor. Venenatis a condimentum vitae sapien. Lacus viverra vitae congue eu consequat ac felis. Diam donec adipiscing tristique risus nec feugiat. Sit amet mattis vulputate enim nulla aliquet. Quis commodo odio aenean sed adipiscing diam donec adipiscing tristique. Vulputate odio ut enim blandit volutpat. Elit pellentesque habitant morbi tristique senectus et netus. Aliquet lectus proin nibh nisl condimentum id. A iaculis at erat pellentesque adipiscing commodo elit at. Quis ipsum suspendisse ultrices gravida dictum fusce. Sit amet mauris commodo quis imperdiet massa tincidunt. Adipiscing elit ut aliquam purus. A diam maecenas sed enim ut sem viverra aliquet eget. Enim neque volutpat ac tincidunt vitae. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Amet nisl suscipit adipiscing bibendum. Nunc pulvinar sapien et ligula ullamcorper malesuada proin. Nulla facilisi cras fermentum odio eu feugiat.
    +
    Elementum integer enim neque volutpat ac tincidunt vitae semper quis. Sem integer vitae justo eget magna fermentum iaculis. Nunc lobortis mattis aliquam faucibus purus in massa tempor nec. Imperdiet proin fermentum leo vel orci porta non. Sed enim ut sem viverra aliquet eget sit amet. Rhoncus mattis rhoncus urna neque viverra justo. At tellus at urna condimentum mattis pellentesque. Vel orci porta non pulvinar neque laoreet suspendisse. Consectetur purus ut faucibus pulvinar elementum integer enim. Urna condimentum mattis pellentesque id nibh. Sem integer vitae justo eget magna fermentum. Ultrices vitae auctor eu augue ut lectus arcu bibendum. Adipiscing bibendum est ultricies integer quis auctor elit. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Sapien faucibus et molestie ac feugiat. Tempor id eu nisl nunc mi ipsum. Arcu non sodales neque sodales ut etiam sit amet nisl. Et malesuada fames ac turpis egestas integer eget aliquet nibh. Pellentesque adipiscing commodo elit at imperdiet. Commodo sed egestas egestas fringilla phasellus faucibus scelerisque eleifend. Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim. Semper risus in hendrerit gravida rutrum quisque. Tempor nec feugiat nisl pretium fusce id. Ipsum consequat nisl vel pretium. Pellentesque eu tincidunt tortor aliquam nulla facilisi cras fermentum. Etiam tempor orci eu lobortis elementum nibh. Eget nullam non nisi est sit amet facilisis magna. Diam vel quam elementum pulvinar. Dolor morbi non arcu risus quis. Nullam ac tortor vitae purus. Morbi leo urna molestie at elementum eu facilisis sed odio. Purus in mollis nunc sed id. Sit amet est placerat in egestas erat imperdiet sed. Diam sollicitudin tempor id eu nisl nunc mi. Nulla aliquet porttitor lacus luctus accumsan tortor posuere. Ac feugiat sed lectus vestibulum mattis. Potenti nullam ac tortor vitae purus faucibus ornare suspendisse. Faucibus vitae aliquet nec ullamcorper sit amet. Eu consequat ac felis donec et odio pellentesque diam. Morbi tincidunt ornare massa eget egestas purus viverra accumsan. A erat nam at lectus urna duis convallis. Pellentesque elit eget gravida cum sociis. Cursus sit amet dictum sit amet justo donec enim. Vitae justo eget magna fermentum iaculis. Enim ut sem viverra aliquet. Convallis tellus id interdum velit. Orci phasellus egestas tellus rutrum tellus pellentesque eu tincidunt tortor. Venenatis a condimentum vitae sapien. Lacus viverra vitae congue eu consequat ac felis. Diam donec adipiscing tristique risus nec feugiat. Sit amet mattis vulputate enim nulla aliquet. Quis commodo odio aenean sed adipiscing diam donec adipiscing tristique. Vulputate odio ut enim blandit volutpat. Elit pellentesque habitant morbi tristique senectus et netus. Aliquet lectus proin nibh nisl condimentum id. A iaculis at erat pellentesque adipiscing commodo elit at. Quis ipsum suspendisse ultrices gravida dictum fusce. Sit amet mauris commodo quis imperdiet massa tincidunt. Adipiscing elit ut aliquam purus. A diam maecenas sed enim ut sem viverra aliquet eget. Enim neque volutpat ac tincidunt vitae. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Amet nisl suscipit adipiscing bibendum. Nunc pulvinar sapien et ligula ullamcorper malesuada proin. Nulla facilisi cras fermentum odio eu feugiat.
    {{ diff --git a/src/components/breadcrumbs/_breadcrumbs.scss b/src/components/breadcrumbs/_breadcrumbs.scss index bf417c674f..d19951c6d0 100644 --- a/src/components/breadcrumbs/_breadcrumbs.scss +++ b/src/components/breadcrumbs/_breadcrumbs.scss @@ -1,84 +1,82 @@ $breadcrumb-chevron-height: 0.65rem; .ons-breadcrumbs { - align-items: center; - display: flex; - padding: 1rem 0; + align-items: center; + display: flex; + padding: 1rem 0; - &__items { - margin: 0; - padding: 0; - } - - &__item { - display: inline-block; - margin: 0; - white-space: nowrap; // Stop items from wrapping, break on chevron only - - &:not(:nth-last-child(1)) { - //small screen not last child - display: none; - } - - .ons-icon { - height: $breadcrumb-chevron-height; - margin: 0 0.2rem; - vertical-align: middle; - width: $breadcrumb-chevron-height; - } - - &:nth-last-child(1) { - // small screen last child - direction: rtl; - .ons-icon { - margin-left: -0.13rem; - transform: rotate(180deg); - } + &__items { + margin: 0; + padding: 0; } - @include mq(s) { - //big screen - &:not(:nth-last-child(1)) { - //not last child + &__item { display: inline-block; - } + margin: 0; + white-space: nowrap; // Stop items from wrapping, break on chevron only - &:nth-last-child(1) { - // last child - direction: ltr; - .ons-icon { - display: none; + &:not(:nth-last-child(1)) { + // Small screen not last child + display: none; } - } - &:first-child:nth-last-child(1) { - //first and last child - direction: rtl; .ons-icon { - display: inline-block; - vertical-align: middle; + height: $breadcrumb-chevron-height; + margin: 0 0.2rem; + vertical-align: middle; + width: $breadcrumb-chevron-height; } - } - &:not(:last-child).ons-icon { - //not last child - margin: 0; + &:nth-last-child(1) { + // Small screen last child + direction: rtl; + .ons-icon { + margin-left: -0.13rem; + transform: rotate(180deg); + } + } + + @include mq(s) { + // Big screen + &:not(:nth-last-child(1)) { + // not last child + display: inline-block; + } + + &:nth-last-child(1) { + // Last child + direction: ltr; + .ons-icon { + display: none; + } + } - /* stylelint-disable */ - // We have to override the icon settings so it renders correctly in ie11 - background-position: center center; - vertical-align: middle; - /* stylelint-enable */ - } + &:first-child:nth-last-child(1) { + // First and last child + direction: rtl; + .ons-icon { + display: inline-block; + vertical-align: middle; + } + } + + &:not(:last-child).ons-icon { + // Not last child + margin: 0; + + // We have to override the icon settings so it renders correctly in ie11 + background-position: center center; + vertical-align: middle; + } + } } - } - &__link { - color: var(--ons-color-text); - text-decoration: underline; + &__link { + color: var(--ons-color-text); + text-decoration: underline; - &:hover { - color: var(--ons-color-text); + &:hover { + color: var(--ons-color-text); + } } - } } diff --git a/src/components/breadcrumbs/_macro.spec.js b/src/components/breadcrumbs/_macro.spec.js index 7e82a357d8..9e4f49246b 100644 --- a/src/components/breadcrumbs/_macro.spec.js +++ b/src/components/breadcrumbs/_macro.spec.js @@ -7,123 +7,123 @@ import { mapAll } from '../../tests/helpers/cheerio'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_BREADCRUMBS_MINIMAL = { - itemsList: [ - { - url: 'https://example.com/', - text: 'Home', - }, - { - url: 'https://example.com/guide/', - text: 'Guide', - }, - ], + itemsList: [ + { + url: 'https://example.com/', + text: 'Home', + }, + { + url: 'https://example.com/guide/', + text: 'Guide', + }, + ], }; const EXAMPLE_BREADCRUMBS = { - classes: 'extra-class another-extra-class', - ariaLabel: 'Breadcrumbs label', - id: 'example-breadcrumbs', - itemsList: [ - { - itemClasses: 'item-extra-class item-another-extra-class', - linkClasses: 'link-extra-class link-another-extra-class', - url: 'https://example.com/', - text: 'Home', - attributes: { - 'data-a': '123', - 'data-b': '456', - }, - id: 'first-breadcrumb', - }, - { - url: 'https://example.com/guide/', - text: 'Guide', - id: 'second-breadcrumb', - attributes: { - 'data-a': '789', - 'data-b': 'ABC', - }, - }, - ], + classes: 'extra-class another-extra-class', + ariaLabel: 'Breadcrumbs label', + id: 'example-breadcrumbs', + itemsList: [ + { + itemClasses: 'item-extra-class item-another-extra-class', + linkClasses: 'link-extra-class link-another-extra-class', + url: 'https://example.com/', + text: 'Home', + attributes: { + 'data-a': '123', + 'data-b': '456', + }, + id: 'first-breadcrumb', + }, + { + url: 'https://example.com/guide/', + text: 'Guide', + id: 'second-breadcrumb', + attributes: { + 'data-a': '789', + 'data-b': 'ABC', + }, + }, + ], }; describe('macro: breadcrumbs', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - it('has additionally provided style classes', () => { - const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); + it('has additionally provided style classes', () => { + const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); - expect($('.ons-breadcrumbs').hasClass('extra-class')).toBe(true); - expect($('.ons-breadcrumbs').hasClass('another-extra-class')).toBe(true); - }); + expect($('.ons-breadcrumbs').hasClass('extra-class')).toBe(true); + expect($('.ons-breadcrumbs').hasClass('another-extra-class')).toBe(true); + }); - it('has a default `aria-label` of "Breadcrumbs"', () => { - const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS_MINIMAL)); + it('has a default `aria-label` of "Breadcrumbs"', () => { + const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS_MINIMAL)); - expect($('.ons-breadcrumbs').attr('aria-label')).toBe('Breadcrumbs'); - }); + expect($('.ons-breadcrumbs').attr('aria-label')).toBe('Breadcrumbs'); + }); - it('has the provided `ariaLabel` for `aria-label`', () => { - const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); + it('has the provided `ariaLabel` for `aria-label`', () => { + const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); - expect($('.ons-breadcrumbs').attr('aria-label')).toBe('Breadcrumbs label'); - }); + expect($('.ons-breadcrumbs').attr('aria-label')).toBe('Breadcrumbs label'); + }); - it('has the provided `id`', () => { - const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); + it('has the provided `id`', () => { + const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); - expect($('.ons-breadcrumbs').attr('id')).toBe('example-breadcrumbs'); - }); + expect($('.ons-breadcrumbs').attr('id')).toBe('example-breadcrumbs'); + }); - it('has additionally provided style classes on `item` element', () => { - const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); + it('has additionally provided style classes on `item` element', () => { + const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); - expect($('.ons-breadcrumbs__item:first').hasClass('item-extra-class')).toBe(true); - expect($('.ons-breadcrumbs__item:first').hasClass('item-another-extra-class')).toBe(true); - }); + expect($('.ons-breadcrumbs__item:first').hasClass('item-extra-class')).toBe(true); + expect($('.ons-breadcrumbs__item:first').hasClass('item-another-extra-class')).toBe(true); + }); - it('has additionally provided style classes on `link` element', () => { - const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); + it('has additionally provided style classes on `link` element', () => { + const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); - expect($('.ons-breadcrumbs__link:first').hasClass('link-extra-class')).toBe(true); - expect($('.ons-breadcrumbs__link:first').hasClass('link-another-extra-class')).toBe(true); - }); + expect($('.ons-breadcrumbs__link:first').hasClass('link-extra-class')).toBe(true); + expect($('.ons-breadcrumbs__link:first').hasClass('link-another-extra-class')).toBe(true); + }); - it('has provided `url` on `link` elements', () => { - const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); + it('has provided `url` on `link` elements', () => { + const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); - const urls = mapAll($('.ons-breadcrumbs__link'), (node) => node.attr('href')); - expect(urls).toEqual(['https://example.com/', 'https://example.com/guide/']); - }); + const urls = mapAll($('.ons-breadcrumbs__link'), (node) => node.attr('href')); + expect(urls).toEqual(['https://example.com/', 'https://example.com/guide/']); + }); - it('has provided `text` on `link` elements', () => { - const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); + it('has provided `text` on `link` elements', () => { + const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); - const labels = mapAll($('.ons-breadcrumbs__link'), (node) => node.text().trim()); - expect(labels).toEqual(['Home', 'Guide']); - }); + const labels = mapAll($('.ons-breadcrumbs__link'), (node) => node.text().trim()); + expect(labels).toEqual(['Home', 'Guide']); + }); - it('has provided `attributes` on `link` elements', () => { - const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); + it('has provided `attributes` on `link` elements', () => { + const $ = cheerio.load(renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS)); - const testValuesA = mapAll($('.ons-breadcrumbs__link'), (node) => node.attr('data-a')); - expect(testValuesA).toEqual(['123', '789']); - const testValuesB = mapAll($('.ons-breadcrumbs__link'), (node) => node.attr('data-b')); - expect(testValuesB).toEqual(['456', 'ABC']); - }); + const testValuesA = mapAll($('.ons-breadcrumbs__link'), (node) => node.attr('data-a')); + expect(testValuesA).toEqual(['123', '789']); + const testValuesB = mapAll($('.ons-breadcrumbs__link'), (node) => node.attr('data-b')); + expect(testValuesB).toEqual(['456', 'ABC']); + }); - it('has a "chevron" icon for each breadcrumb item', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); + it('has a "chevron" icon for each breadcrumb item', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); - faker.renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS_MINIMAL); + faker.renderComponent('breadcrumbs', EXAMPLE_BREADCRUMBS_MINIMAL); - const iconTypes = iconsSpy.occurrences.map((occurrence) => occurrence.iconType); - expect(iconTypes).toEqual(['chevron', 'chevron']); - }); + const iconTypes = iconsSpy.occurrences.map((occurrence) => occurrence.iconType); + expect(iconTypes).toEqual(['chevron', 'chevron']); + }); }); diff --git a/src/components/browser-banner/_browser-banner.scss b/src/components/browser-banner/_browser-banner.scss index 227d983637..4b4b1ea746 100644 --- a/src/components/browser-banner/_browser-banner.scss +++ b/src/components/browser-banner/_browser-banner.scss @@ -1,39 +1,39 @@ .ons-browser-banner { - background-color: var(--ons-color-banner-browser-bg); - display: none; // Hides unsupported browser banner unless targeted below - padding: 0.8rem 0; + background-color: var(--ons-color-banner-browser-bg); + display: none; // Hides unsupported browser banner unless targeted below + padding: 0.8rem 0; - &__content { - @extend .ons-u-fs-s; + &__content { + @extend .ons-u-fs-s; - color: var(--ons-color-text-inverse); - margin: 0; - } + color: var(--ons-color-text-inverse); + margin: 0; + } - &__lead { - @extend .ons-u-fw-b; - } + &__lead { + @extend .ons-u-fw-b; + } - &__link { - color: var(--ons-color-text-inverse-link); + &__link { + color: var(--ons-color-text-inverse-link); - &:hover { - color: var(--ons-color-text-inverse-link-hover); - text-decoration: underline solid var(--ons-color-text-inverse-link-hover) 2px; + &:hover { + color: var(--ons-color-text-inverse-link-hover); + text-decoration: underline solid var(--ons-color-text-inverse-link-hover) 2px; + } } - } } // Targets browsers IE10 & IE11 and displays unsupported browser banner @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { - .ons-browser-banner { - display: block; - } + .ons-browser-banner { + display: block; + } } // Targets browsers IE8 & IE9 and displays unsupported browser banner @media screen\0 { - .ons-browser-banner { - display: block; - } + .ons-browser-banner { + display: block; + } } diff --git a/src/components/browser-banner/_macro.spec.js b/src/components/browser-banner/_macro.spec.js index e06d7f49be..0263410bd9 100644 --- a/src/components/browser-banner/_macro.spec.js +++ b/src/components/browser-banner/_macro.spec.js @@ -8,99 +8,99 @@ import { renderComponent } from '../../tests/helpers/rendering'; const EXAMPLE_BROWSER_BANNER_DEFAULT = {}; describe('macro: browser-banner', () => { - it('passes jest-axe checks with', async () => { - const $ = cheerio.load(renderComponent('browser-banner', EXAMPLE_BROWSER_BANNER_DEFAULT)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the default `bannerLeadingText`', () => { - const $ = cheerio.load(renderComponent('browser-banner', EXAMPLE_BROWSER_BANNER_DEFAULT)); - - const bannerLeadingText = $('.ons-browser-banner__lead').text().trim(); - expect(bannerLeadingText).toBe('This website no longer supports your browser.'); - }); - - it('has the default `bannerCTA`', () => { - const $ = cheerio.load(renderComponent('browser-banner', EXAMPLE_BROWSER_BANNER_DEFAULT)); - - const bannerCtaHtml = $('.ons-browser-banner__cta').text().trim(); - expect(bannerCtaHtml).toBe('You can upgrade your browser to the latest version.'); - }); - - it('has the default `bannerLinkUrl`', () => { - const $ = cheerio.load(renderComponent('browser-banner', EXAMPLE_BROWSER_BANNER_DEFAULT)); - - expect($('.ons-browser-banner__link').attr('href')).toBe('https://www.ons.gov.uk/help/browsers'); - }); - - it('has `container--wide` class when `wide` is true', () => { - const $ = cheerio.load( - renderComponent('browser-banner', { - ...EXAMPLE_BROWSER_BANNER_DEFAULT, - wide: true, - }), - ); - - expect($('.ons-container').hasClass('ons-container--wide')).toBe(true); - }); - - it('does not have `container--wide` class when `wide` is not set', () => { - const $ = cheerio.load( - renderComponent('browser-banner', { - ...EXAMPLE_BROWSER_BANNER_DEFAULT, - }), - ); - - expect($('.ons-container').hasClass('ons-container--wide')).toBe(false); - }); - - it('has `container--full-width` class when `fullWidth` is true', () => { - const $ = cheerio.load( - renderComponent('browser-banner', { - ...EXAMPLE_BROWSER_BANNER_DEFAULT, - fullWidth: true, - }), - ); - - expect($('.ons-container').hasClass('ons-container--full-width')).toBe(true); - }); - - it('does not have `container--full-width` class when `fullWidth` is not set', () => { - const $ = cheerio.load( - renderComponent('browser-banner', { - ...EXAMPLE_BROWSER_BANNER_DEFAULT, - }), - ); - - expect($('.ons-container').hasClass('ons-container--full-width')).toBe(false); - }); + it('passes jest-axe checks with', async () => { + const $ = cheerio.load(renderComponent('browser-banner', EXAMPLE_BROWSER_BANNER_DEFAULT)); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has the default `bannerLeadingText`', () => { + const $ = cheerio.load(renderComponent('browser-banner', EXAMPLE_BROWSER_BANNER_DEFAULT)); + + const bannerLeadingText = $('.ons-browser-banner__lead').text().trim(); + expect(bannerLeadingText).toBe('This website no longer supports your browser.'); + }); + + it('has the default `bannerCTA`', () => { + const $ = cheerio.load(renderComponent('browser-banner', EXAMPLE_BROWSER_BANNER_DEFAULT)); + + const bannerCtaHtml = $('.ons-browser-banner__cta').text().trim(); + expect(bannerCtaHtml).toBe('You can upgrade your browser to the latest version.'); + }); + + it('has the default `bannerLinkUrl`', () => { + const $ = cheerio.load(renderComponent('browser-banner', EXAMPLE_BROWSER_BANNER_DEFAULT)); + + expect($('.ons-browser-banner__link').attr('href')).toBe('https://www.ons.gov.uk/help/browsers'); + }); + + it('has `container--wide` class when `wide` is true', () => { + const $ = cheerio.load( + renderComponent('browser-banner', { + ...EXAMPLE_BROWSER_BANNER_DEFAULT, + wide: true, + }), + ); + + expect($('.ons-container').hasClass('ons-container--wide')).toBe(true); + }); + + it('does not have `container--wide` class when `wide` is not set', () => { + const $ = cheerio.load( + renderComponent('browser-banner', { + ...EXAMPLE_BROWSER_BANNER_DEFAULT, + }), + ); + + expect($('.ons-container').hasClass('ons-container--wide')).toBe(false); + }); + + it('has `container--full-width` class when `fullWidth` is true', () => { + const $ = cheerio.load( + renderComponent('browser-banner', { + ...EXAMPLE_BROWSER_BANNER_DEFAULT, + fullWidth: true, + }), + ); + + expect($('.ons-container').hasClass('ons-container--full-width')).toBe(true); + }); + + it('does not have `container--full-width` class when `fullWidth` is not set', () => { + const $ = cheerio.load( + renderComponent('browser-banner', { + ...EXAMPLE_BROWSER_BANNER_DEFAULT, + }), + ); + + expect($('.ons-container').hasClass('ons-container--full-width')).toBe(false); + }); }); describe('mode: Welsh language', () => { - it('has the welsh version of default `bannerLeadingText`', () => { - const $ = cheerio.load( - renderComponent('browser-banner', { - ...EXAMPLE_BROWSER_BANNER_DEFAULT, - lang: 'cy', - }), - ); - - const bannerLeadingText = $('.ons-browser-banner__lead').text().trim(); - expect(bannerLeadingText).toBe('Nid yw’r wefan hon yn cefnogi eich porwr mwyach.'); - }); - - it('has the welsh version of default `bannerCTA`', () => { - const $ = cheerio.load(renderComponent('browser-banner', { lang: 'cy' })); - - const bannerCtaHtml = $('.ons-browser-banner__cta').text().trim(); - expect(bannerCtaHtml).toBe('Gallwch ddiweddaru eich porwr i’r fersiwn ddiweddaraf.'); - }); - - it('has the welsh version of default `bannerLinkUrl`', () => { - const $ = cheerio.load(renderComponent('browser-banner', { lang: 'cy' })); - - expect($('.ons-browser-banner__link').attr('href')).toBe('https://cy.ons.gov.uk/help/browsers'); - }); + it('has the welsh version of default `bannerLeadingText`', () => { + const $ = cheerio.load( + renderComponent('browser-banner', { + ...EXAMPLE_BROWSER_BANNER_DEFAULT, + lang: 'cy', + }), + ); + + const bannerLeadingText = $('.ons-browser-banner__lead').text().trim(); + expect(bannerLeadingText).toBe('Nid yw’r wefan hon yn cefnogi eich porwr mwyach.'); + }); + + it('has the welsh version of default `bannerCTA`', () => { + const $ = cheerio.load(renderComponent('browser-banner', { lang: 'cy' })); + + const bannerCtaHtml = $('.ons-browser-banner__cta').text().trim(); + expect(bannerCtaHtml).toBe('Gallwch ddiweddaru eich porwr i’r fersiwn ddiweddaraf.'); + }); + + it('has the welsh version of default `bannerLinkUrl`', () => { + const $ = cheerio.load(renderComponent('browser-banner', { lang: 'cy' })); + + expect($('.ons-browser-banner__link').attr('href')).toBe('https://cy.ons.gov.uk/help/browsers'); + }); }); diff --git a/src/components/button/_button.scss b/src/components/button/_button.scss index 6fbe515620..5fd7b22711 100644 --- a/src/components/button/_button.scss +++ b/src/components/button/_button.scss @@ -1,532 +1,531 @@ $button-shadow-size: 3px; .ons-btn { - background: transparent; - border: 0; - border-radius: 0; - cursor: pointer; - display: inline-block; - font-family: inherit; - font-size: 1rem; - font-weight: $font-weight-bold; - line-height: 1.35; - margin: 0; - padding: 0; - position: relative; - text-align: center; - text-decoration: none; - text-rendering: optimizelegibility; - vertical-align: top; - white-space: nowrap; - - // Transparent border for IE11 High Contrast mode support due to 'border: 0' on buttons - &::after { - border: ems($button-shadow-size) solid transparent; - inset: 0 0 - (ems($button-shadow-size)) 0; // makes sure button shadow is selectable - content: ''; - position: absolute; - } - - &--search { - .ons-icon { - @include mq(s, l) { - margin-right: 0.5rem; - } - } - } - - &__inner { - background: var(--ons-color-button); - border-radius: $input-radius; - box-shadow: 0 ems($button-shadow-size) 0 var(--ons-color-button-shadow); - color: var(--ons-color-text-inverse); - display: inherit; - padding: 0.7em 1em 0.8em; - // Required for Google Tag Manager - pointer-events: none; + background: transparent; + border: 0; + border-radius: 0; + cursor: pointer; + display: inline-block; + font-family: inherit; + font-size: 1rem; + font-weight: $font-weight-bold; + line-height: 1.35; + margin: 0; + padding: 0; position: relative; - - .ons-icon { - fill: var(--ons-color-text-inverse); - height: 18px; - margin-top: -$button-shadow-size; - vertical-align: middle; - width: 18px; - } - } - - // When preceded by another button (for example, in a group) - & + & { - margin-left: 0.5rem; - } - - // When focused - &:focus, - &:focus-visible { - // Add transparent outline because Windows High Contrast Mode doesn't show box-shadows - outline: 3px solid transparent; - outline-offset: 1px; - } - - &:focus &__inner { - background: var(--ons-color-focus); - box-shadow: 0 ems($button-shadow-size) 0 var(--ons-color-text-link-focus); - color: var(--ons-color-text-link-focus); - - .ons-icon { - fill: var(--ons-color-text-link-focus); - } - } - - &:focus:hover:not(:active, .active) &__inner { - background: var(--ons-color-focus-dark); - } - - // When down - &:active &, - &:active:focus &, - &.active &, - &.active:focus & { - &__inner { - background: var(--ons-color-button); - box-shadow: none; - color: var(--ons-color-text-inverse); - - .ons-icon { - fill: var(--ons-color-text-inverse); - } - } - } - - &:active, - &.active { - top: ems($button-shadow-size); - } - - // Small buttons - &--small, - &--mobile { - font-size: 0.9rem; - } - - &--small & { - &__inner { - padding: 0.5em 0.7em; - .ons-icon { - height: 16px; - width: 16px; - } - } - } - - &--small.ons-btn--ghost &, - &--mobile & { - &__inner { - padding: 0.5em 0.7em; - } - } - - // Secondary button style - &--secondary & { - &__inner { - box-shadow: 0 ems($button-shadow-size) 0 var(--ons-color-button-secondary-shadow); - } - } - - &--secondary &, - &--secondary:active &, - &--secondary.active &, - &--secondary:active:focus &, - &--secondary.active:focus & { - &__inner { - background: var(--ons-color-button-secondary); - color: var(--ons-color-text); - font-weight: $font-weight-regular; - - .ons-icon { - fill: var(--ons-color-text); - } - } - } - - // When hovered - &:hover & { - &__inner { - background: var(--ons-color-button-hover); - } - } - - &--secondary:hover:not(&--disabled) & { - &__inner { - background: var(--ons-color-button-secondary-hover); - } - } - - &--disabled:not(&--secondary) &, - &--loader.ons-is-loading:hover:not(&--secondary) & { - &__inner { - background: var(--ons-color-button); - } - } - - // Link button when hovered - &--link:hover { + text-align: center; text-decoration: none; - } - - &--link:focus:not(:active, .active, &--secondary) &, - &--link:focus:hover:not(:active, .active, &--secondary) & { - outline: inherit; - - &__inner { - .ons-icon { - fill: var(--ons-color-text); - } + text-rendering: optimizelegibility; + vertical-align: top; + white-space: nowrap; + + // Transparent border for IE11 High Contrast mode support due to 'border: 0' on buttons + &::after { + border: ems($button-shadow-size) solid transparent; + inset: 0 0 - (ems($button-shadow-size)) 0; // Makes sure button shadow is selectable + content: ''; + position: absolute; } - } - - &--text-link { - vertical-align: baseline; - } - &--text-link & { - &__inner { - background: transparent; - border: none; - border-radius: 0; - box-shadow: none; - color: var(--ons-color-text-link); - font-weight: $font-weight-regular; - padding: 0; - .ons-icon { - fill: var(--ons-color-text-link); - } - } - } - - &--text-link-inverse & { - &__inner { - color: var(--ons-color-text-inverse-link); - .ons-icon { - fill: var(--ons-color-text-inverse-link); - } + &--search { + .ons-icon { + @include mq(s, l) { + margin-right: 0.5rem; + } + } } - } - &--text-link:hover &, - &--text-link:active &, - &--text-link.active & { &__inner { - background: none; - color: var(--ons-color-text-link-hover); - .ons-icon { - fill: var(--ons-color-text-link-hover); - } - } - } + background: var(--ons-color-button); + border-radius: $input-radius; + box-shadow: 0 ems($button-shadow-size) 0 var(--ons-color-button-shadow); + color: var(--ons-color-text-inverse); + display: inherit; + padding: 0.7em 1em 0.8em; + // Required for Google Tag Manager + pointer-events: none; + position: relative; - &--text-link-inverse:hover &, - &--text-link-inverse:active &, - &--text-link-inverse.active & { - &__inner { - color: var(--ons-color-text-inverse-link-hover); - .ons-icon { - fill: var(--ons-color-text-inverse-link-hover); - } + .ons-icon { + fill: var(--ons-color-text-inverse); + height: 18px; + margin-top: -$button-shadow-size; + vertical-align: middle; + width: 18px; + } } - } - &--text-link:focus:hover & { - &__inner { - color: var(--ons-color-black); + // When preceded by another button (for example, in a group) + & + & { + margin-left: 0.5rem; } - } - &--text-link:focus &, - &--text-link:active:focus &, - &--text-link.active:focus & { - &__inner { - background-color: var(--ons-color-focus); - box-shadow: 0 -2px var(--ons-color-focus), - 0 4px var(--ons-color-text-link-focus) !important; - color: var(--ons-color-text-link-focus); - .ons-icon { - fill: var(--ons-color-text-link-focus); - } + // When focused + &:focus, + &:focus-visible { + // Add transparent outline because Windows High Contrast Mode doesn't show box-shadows + outline: 3px solid transparent; + outline-offset: 1px; } - } - &--ghost & { - &__inner { - background: transparent; - border: 2px solid rgb(255 255 255 / 60%); - box-shadow: none; - color: var(--ons-color-text-inverse); - .ons-icon { - fill: var(--ons-color-text-inverse); - } - } - } + &:focus &__inner { + background: var(--ons-color-focus); + box-shadow: 0 ems($button-shadow-size) 0 var(--ons-color-text-link-focus); + color: var(--ons-color-text-link-focus); - &--ghost-dark & { - &__inner { - background: transparent; - border: 2px solid var(--ons-color-black); - box-shadow: none; - color: var(--ons-color-text); - .ons-icon { - fill: var(--ons-color-text); - } - } - } - - &--ghost, - &--ghost-dark, - &--dropdown, - &--text-link, - &--disabled, - &--loader.ons-is-loading { - &:active, - .active { - top: 0 !important; // Override 'pressed' state for flat and non-selectable buttons - } - } - - &--ghost:active:focus, - &--ghost.active:focus - &--ghost-dark:active:focus - &--ghost-dark.active:focus { - box-shadow: none; - outline: 3px solid transparent; - } - - &--ghost:focus:hover, - &--ghost-dark:focus:hover, - &--dropdown:focus:hover, - &--text-link:focus:hover { - outline: none; - } - - &--ghost-dark:focus:hover &, - &--dropdown:focus:hover & { - &__inner { - color: var(--ons-color-text); - .ons-icon { - fill: var(--ons-color-text); - } + .ons-icon { + fill: var(--ons-color-text-link-focus); + } } - } - &--ghost:hover & { - &__inner { - background: rgb(0 0 0 / 10%); - border-color: var(--ons-color-white); + &:focus:hover:not(:active, .active) &__inner { + background: var(--ons-color-focus-dark); } - } - &--ghost:active &, - &--ghost:active:focus &, - &--ghost.active & { - &__inner { - background: rgb(0 0 0 / 20%); - border-color: rgb(255 255 255 / 60%); - color: var(--ons-color-text-inverse); - .ons-icon { - fill: var(--ons-color-text-inverse); - } - } - } - - &--ghost-dark:hover &, - &--ghost-dark:active &, - &--ghost-dark:active:focus &, - &--ghost-dark.active & { - &__inner { - background: var(--ons-color-black); - border-color: var(--ons-color-black); - color: var(--ons-color-white); - .ons-icon { - fill: var(--ons-color-white); - } - } - } - - &--ghost.active:focus &, - &--ghost-dark.active:focus &, - &--dropdown.active:focus & { - &__inner { - background: var(--ons-color-focus); - color: var(--ons-color-text-link-focus); - .ons-icon { - fill: var(--ons-color-text-link-focus); - } - } - } + // When down + &:active &, + &:active:focus &, + &.active &, + &.active:focus & { + &__inner { + background: var(--ons-color-button); + box-shadow: none; + color: var(--ons-color-text-inverse); - &--ghost:focus &, - &--ghost-dark:focus &, - &--dropdown:focus & { - &__inner { - border-color: var(--ons-color-text-link-focus); - box-shadow: 0 0 0 1px var(--ons-color-text-link-focus); - .ons-icon { - fill: var(--ons-color-black); - } + .ons-icon { + fill: var(--ons-color-text-inverse); + } + } } - } - &--loader & { - &__inner { - position: relative; - transition: color 0.3s ease-in-out; - .ons-icon { - height: 27px; - left: 50%; - margin: 0; - opacity: 0; - position: absolute; - top: 50%; - transform: translate(-50%, -50%); - transition: opacity 0.3s ease-in-out; - width: 27px; - } + &:active, + &.active { + top: ems($button-shadow-size); } - } - &--loader.ons-btn--small { - .ons-icon { - height: 24px; - width: 24px; + // Small buttons + &--small, + &--mobile { + font-size: 0.9rem; } - } - &--loader.ons-is-loading & { - &__inner { - color: transparent; - .ons-icon { - margin-left: 0 !important; - opacity: 1; - } + &--small & { + &__inner { + padding: 0.5em 0.7em; + .ons-icon { + height: 16px; + width: 16px; + } + } } - } - &--loader.ons-is-loading:active &, - &--loader.ons-is-loading.active & { - &__inner { - box-shadow: 0 ems($button-shadow-size) 0 var(--ons-color-button-shadow); + &--small.ons-btn--ghost &, + &--mobile & { + &__inner { + padding: 0.5em 0.7em; + } } - } - &--loader.ons-is-loading:hover { - cursor: not-allowed; - } + // Secondary button style + &--secondary & { + &__inner { + box-shadow: 0 ems($button-shadow-size) 0 var(--ons-color-button-secondary-shadow); + } + } + + &--secondary &, + &--secondary:active &, + &--secondary.active &, + &--secondary:active:focus &, + &--secondary.active:focus & { + &__inner { + background: var(--ons-color-button-secondary); + color: var(--ons-color-text); + font-weight: $font-weight-regular; - &--dropdown:focus & { - &__inner { - box-shadow: inset 0 -4px 0 0 var(--ons-color-text-link-focus); + .ons-icon { + fill: var(--ons-color-text); + } + } + } + + // When hovered + &:hover & { + &__inner { + background: var(--ons-color-button-hover); + } + } + + &--secondary:hover:not(&--disabled) & { + &__inner { + background: var(--ons-color-button-secondary-hover); + } + } + + &--disabled:not(&--secondary) &, + &--loader.ons-is-loading:hover:not(&--secondary) & { + &__inner { + background: var(--ons-color-button); + } + } + + // Link button when hovered + &--link:hover { + text-decoration: none; + } + + &--link:focus:not(:active, .active, &--secondary) &, + &--link:focus:hover:not(:active, .active, &--secondary) & { + outline: inherit; + + &__inner { + .ons-icon { + fill: var(--ons-color-text); + } + } + } + + &--text-link { + vertical-align: baseline; + } + + &--text-link & { + &__inner { + background: transparent; + border: 0; + border-radius: 0; + box-shadow: none; + color: var(--ons-color-text-link); + font-weight: $font-weight-regular; + padding: 0; + .ons-icon { + fill: var(--ons-color-text-link); + } + } + } + + &--text-link-inverse & { + &__inner { + color: var(--ons-color-text-inverse-link); + .ons-icon { + fill: var(--ons-color-text-inverse-link); + } + } + } + + &--text-link:hover &, + &--text-link:active &, + &--text-link.active & { + &__inner { + background: none; + color: var(--ons-color-text-link-hover); + .ons-icon { + fill: var(--ons-color-text-link-hover); + } + } + } + + &--text-link-inverse:hover &, + &--text-link-inverse:active &, + &--text-link-inverse.active & { + &__inner { + color: var(--ons-color-text-inverse-link-hover); + .ons-icon { + fill: var(--ons-color-text-inverse-link-hover); + } + } + } + + &--text-link:focus:hover & { + &__inner { + color: var(--ons-color-black); + } + } + + &--text-link:focus &, + &--text-link:active:focus &, + &--text-link.active:focus & { + &__inner { + background-color: var(--ons-color-focus); + box-shadow: + 0 -2px var(--ons-color-focus), + 0 4px var(--ons-color-text-link-focus) !important; + color: var(--ons-color-text-link-focus); + .ons-icon { + fill: var(--ons-color-text-link-focus); + } + } + } + + &--ghost & { + &__inner { + background: transparent; + border: 2px solid rgb(255 255 255 / 60%); + box-shadow: none; + color: var(--ons-color-text-inverse); + .ons-icon { + fill: var(--ons-color-text-inverse); + } + } + } + + &--ghost-dark & { + &__inner { + background: transparent; + border: 2px solid var(--ons-color-black); + box-shadow: none; + color: var(--ons-color-text); + .ons-icon { + fill: var(--ons-color-text); + } + } + } + + &--ghost, + &--ghost-dark, + &--dropdown, + &--text-link, + &--disabled, + &--loader.ons-is-loading { + &:active, + .active { + top: 0 !important; // Override 'pressed' state for flat and non-selectable buttons + } + } + + &--ghost:active:focus, + &--ghost.active:focus &--ghost-dark:active:focus &--ghost-dark.active:focus { + box-shadow: none; + outline: 3px solid transparent; + } + + &--ghost:focus:hover, + &--ghost-dark:focus:hover, + &--dropdown:focus:hover, + &--text-link:focus:hover { + outline: none; + } + + &--ghost-dark:focus:hover &, + &--dropdown:focus:hover & { + &__inner { + color: var(--ons-color-text); + .ons-icon { + fill: var(--ons-color-text); + } + } + } + + &--ghost:hover & { + &__inner { + background: rgb(0 0 0 / 10%); + border-color: var(--ons-color-white); + } + } + + &--ghost:active &, + &--ghost:active:focus &, + &--ghost.active & { + &__inner { + background: rgb(0 0 0 / 20%); + border-color: rgb(255 255 255 / 60%); + color: var(--ons-color-text-inverse); + .ons-icon { + fill: var(--ons-color-text-inverse); + } + } + } + + &--ghost-dark:hover &, + &--ghost-dark:active &, + &--ghost-dark:active:focus &, + &--ghost-dark.active & { + &__inner { + background: var(--ons-color-black); + border-color: var(--ons-color-black); + color: var(--ons-color-white); + .ons-icon { + fill: var(--ons-color-white); + } + } + } + + &--ghost.active:focus &, + &--ghost-dark.active:focus &, + &--dropdown.active:focus & { + &__inner { + background: var(--ons-color-focus); + color: var(--ons-color-text-link-focus); + .ons-icon { + fill: var(--ons-color-text-link-focus); + } + } + } + + &--ghost:focus &, + &--ghost-dark:focus &, + &--dropdown:focus & { + &__inner { + border-color: var(--ons-color-text-link-focus); + box-shadow: 0 0 0 1px var(--ons-color-text-link-focus); + .ons-icon { + fill: var(--ons-color-black); + } + } + } + + &--loader & { + &__inner { + position: relative; + transition: color 0.3s ease-in-out; + .ons-icon { + height: 27px; + left: 50%; + margin: 0; + opacity: 0; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + transition: opacity 0.3s ease-in-out; + width: 27px; + } + } + } + + &--loader.ons-btn--small { + .ons-icon { + height: 24px; + width: 24px; + } + } + + &--loader.ons-is-loading & { + &__inner { + color: transparent; + .ons-icon { + margin-left: 0 !important; + opacity: 1; + } + } + } + + &--loader.ons-is-loading:active &, + &--loader.ons-is-loading.active & { + &__inner { + box-shadow: 0 ems($button-shadow-size) 0 var(--ons-color-button-shadow); + } + } + + &--loader.ons-is-loading:hover { + cursor: not-allowed; + } + + &--dropdown:focus & { + &__inner { + box-shadow: inset 0 -4px 0 0 var(--ons-color-text-link-focus); + } + } + + &--mobile[aria-expanded='true'], + &--text-link[aria-expanded='true'] { + .ons-icon { + transform: rotate(270deg); + } + } + + &--mobile, + &--text-link { + .ons-icon { + transform: rotate(90deg); + } + + @include mq(l) { + display: none; + } + } + + // Disabled buttons + &--disabled & { + &__inner { + opacity: 0.4; + } + } + + &--disabled:hover { + cursor: not-allowed; + } + + &--disabled:active &, + &--disabled.active & { + &__inner { + box-shadow: 0 ems($button-shadow-size) 0 var(--ons-color-button-shadow); + } } - } - &--mobile[aria-expanded='true'], - &--text-link[aria-expanded='true'] { - .ons-icon { - transform: rotate(270deg); - } - } + &--dropdown { + @extend .ons-btn--mobile; + + width: 100%; + } + + &--dropdown & { + @extend .ons-btn--mobile; + + width: 100%; - &--mobile, - &--text-link { - .ons-icon { - transform: rotate(90deg); - } + &__inner { + background: var(--ons-color-branded-tint); + border: 0; + border-radius: 0; + box-shadow: none; + color: var(--ons-color-branded-text); + display: block; + font-size: 1rem; + font-weight: $font-weight-regular; + padding: 0.6rem 1rem; + text-align: left; - @include mq(l) { - display: none; + .ons-icon { + fill: var(--ons-color-branded-text); + float: right; + margin-top: 3px; + } + } } - } - - // Disabled buttons - &--disabled & { - &__inner { - opacity: 0.4; - } - } - - &--disabled:hover { - cursor: not-allowed; - } - &--disabled:active &, - &--disabled.active & { - &__inner { - box-shadow: 0 ems($button-shadow-size) 0 var(--ons-color-button-shadow); + &--dropdown:hover & { + &__inner { + background: var(--ons-color-branded-secondary); + color: var(--ons-color-white); + .ons-icon { + fill: var(--ons-color-white); + } + } } - } - - &--dropdown { - @extend .ons-btn--mobile; - - width: 100%; - } - - &--dropdown & { - @extend .ons-btn--mobile; - - width: 100%; - &__inner { - background: var(--ons-color-branded-tint); - border: none; - border-radius: 0; - box-shadow: none; - color: var(--ons-color-branded-text); - display: block; - font-size: 1rem; - font-weight: $font-weight-regular; - padding: 0.6rem 1rem; - text-align: left; - - .ons-icon { - fill: var(--ons-color-branded-text); - float: right; - margin-top: 3px; - } - } - } - - &--dropdown:hover & { - &__inner { - background: var(--ons-color-branded-secondary); - color: var(--ons-color-white); - .ons-icon { - fill: var(--ons-color-white); - } - } - } - - &--dropdown:active &, - &--dropdown.active &, - &--dropdown:active:focus &, - &--dropdown.active:focus & { - &__inner { - background: var(--ons-color-branded-secondary); - color: var(--ons-color-white); - .ons-icon { - fill: var(--ons-color-white); - } + &--dropdown:active &, + &--dropdown.active &, + &--dropdown:active:focus &, + &--dropdown.active:focus & { + &__inner { + background: var(--ons-color-branded-secondary); + color: var(--ons-color-white); + .ons-icon { + fill: var(--ons-color-white); + } + } } - } } .ons-btn-group { - @extend .ons-u-mb-m; + @extend .ons-u-mb-m; - align-items: baseline; - display: flex; - flex-flow: row wrap; + align-items: baseline; + display: flex; + flex-flow: row wrap; - & .ons-btn, - & a { - margin: 0 1rem 1rem 0; - } + .ons-btn, + a { + margin: 0 1rem 1rem 0; + } } diff --git a/src/components/button/_macro.spec.js b/src/components/button/_macro.spec.js index ce28b6d03e..19c2786ea3 100644 --- a/src/components/button/_macro.spec.js +++ b/src/components/button/_macro.spec.js @@ -6,427 +6,427 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; describe('macro: button', () => { - it('has the provided `id` attribute', () => { - const $ = cheerio.load( - renderComponent('button', { - id: 'example-id', - }), - ); - - expect($('#example-id').length).toBe(1); - }); - - it('has additionally provided `attributes`', () => { - const $ = cheerio.load( - renderComponent('button', { - attributes: { - a: 123, - b: 456, - }, - }), - ); - - expect($('button').attr('a')).toBe('123'); - expect($('button').attr('b')).toBe('456'); - }); - - it('has expected style classes', () => { - const $ = cheerio.load(renderComponent('button')); - - expect($('.ons-btn .ons-btn__inner').length).toBe(1); - }); - - it('has provided variant style classes', () => { - const $ = cheerio.load( - renderComponent('button', { - variants: ['variant-a', 'variant-b'], - }), - ); - - expect($('.ons-btn').hasClass('ons-btn--variant-a')).toBe(true); - expect($('.ons-btn').hasClass('ons-btn--variant-b')).toBe(true); - }); - - it('has download variant style class when `variants` contains `download`', () => { - const $ = cheerio.load( - renderComponent('button', { - url: 'http://example.com', - variants: 'download', - }), - ); - - expect($('.ons-btn').hasClass('ons-btn--download')).toBe(true); - }); - - it('has `download` icon when `variants` contains "download"', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); - - faker.renderComponent('button', { - url: 'http://example.com', - variants: 'download', + it('has the provided `id` attribute', () => { + const $ = cheerio.load( + renderComponent('button', { + id: 'example-id', + }), + ); + + expect($('#example-id').length).toBe(1); }); - expect(iconsSpy.occurrences[0].iconType).toBe('download'); - }); - - it('has provided variant style classes when `variants` contains "print"', () => { - const $ = cheerio.load( - renderComponent('button', { - variants: 'print', - }), - ); - - expect($('.ons-btn').hasClass('ons-btn--print')).toBe(true); - expect($('.ons-btn').hasClass('ons-u-d-no')).toBe(true); - expect($('.ons-btn').hasClass('ons-js-print-btn')).toBe(true); - }); - - it('has `print` icon when `variants` contains "print"', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); - - faker.renderComponent('button', { - url: 'http://example.com', - variants: 'print', + it('has additionally provided `attributes`', () => { + const $ = cheerio.load( + renderComponent('button', { + attributes: { + a: 123, + b: 456, + }, + }), + ); + + expect($('button').attr('a')).toBe('123'); + expect($('button').attr('b')).toBe('456'); }); - expect(iconsSpy.occurrences[0].iconType).toBe('print'); - }); - - it('has provided variant style classes when `variants` contains "loader"', () => { - const $ = cheerio.load( - renderComponent('button', { - variants: 'loader', - }), - ); + it('has expected style classes', () => { + const $ = cheerio.load(renderComponent('button')); - expect($('.ons-btn').hasClass('ons-btn--loader')).toBe(true); - expect($('.ons-btn').hasClass('ons-js-loader')).toBe(true); - expect($('.ons-btn').hasClass('ons-js-submit-btn')).toBe(true); - }); + expect($('.ons-btn .ons-btn__inner').length).toBe(1); + }); - it('has `loader` icon when `variants` contains "loader"', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); + it('has provided variant style classes', () => { + const $ = cheerio.load( + renderComponent('button', { + variants: ['variant-a', 'variant-b'], + }), + ); - faker.renderComponent('button', { - variants: 'loader', + expect($('.ons-btn').hasClass('ons-btn--variant-a')).toBe(true); + expect($('.ons-btn').hasClass('ons-btn--variant-b')).toBe(true); }); - expect(iconsSpy.occurrences[0].iconType).toBe('loader'); - }); - - it('has `chevron` icon when `variants` contains "mobile"', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); + it('has download variant style class when `variants` contains `download`', () => { + const $ = cheerio.load( + renderComponent('button', { + url: 'http://example.com', + variants: 'download', + }), + ); - faker.renderComponent('button', { - variants: 'mobile', + expect($('.ons-btn').hasClass('ons-btn--download')).toBe(true); }); - expect(iconsSpy.occurrences[0].iconType).toBe('chevron'); - }); - - it('has provided variant style classes when `variants` contains "timer"', () => { - const $ = cheerio.load( - renderComponent('button', { - variants: 'timer', - }), - ); - - expect($('.ons-btn').hasClass('ons-js-timer')).toBe(true); - expect($('.ons-btn').hasClass('ons-js-submit-btn')).toBe(true); - }); - - it('has additionally provided style classes', () => { - const $ = cheerio.load( - renderComponent('button', { - classes: 'extra-class another-extra-class', - }), - ); - - expect($('.ons-btn').hasClass('extra-class')).toBe(true); - expect($('.ons-btn').hasClass('another-extra-class')).toBe(true); - }); - - it('has additionally provided inner style classes', () => { - const $ = cheerio.load( - renderComponent('button', { - innerClasses: 'extra-inner-class another-extra-inner-class', - }), - ); - - expect($('.ons-btn__inner').hasClass('extra-inner-class')).toBe(true); - expect($('.ons-btn__inner').hasClass('another-extra-inner-class')).toBe(true); - }); - - it('has label text when `text` is provided', () => { - const $ = cheerio.load( - renderComponent('button', { - text: 'Click > me!', - }), - ); - - expect($('.ons-btn__text').html()).toBe('Click > me!'); - }); - - it('has label text when `html` is provided', () => { - const $ = cheerio.load( - renderComponent('button', { - html: 'Click me!', - }), - ); - - expect($('.ons-btn__text').html()).toBe('Click me!'); - }); - - it('has button context text when `buttonContext` is provided', () => { - const $ = cheerio.load( - renderComponent('button', { - buttonContext: 'button context text', - }), - ); - - expect($('.ons-btn__context').text()).toBe('button context text'); - }); - - it('has custom icon before button text', () => { - const $ = cheerio.load( - renderComponent('button', { - text: 'Click me!', - iconPosition: 'before', - iconType: 'exit', - }), - ); - - expect($('.ons-icon + .ons-btn__text').text()).toBe('Click me!'); - }); - - it('has custom icon after button text', () => { - const $ = cheerio.load( - renderComponent('button', { - text: 'Click me!', - iconPosition: 'after', - iconType: 'exit', - }), - ); - - expect($('.ons-btn__text + .ons-icon').prev().text()).toBe('Click me!'); - }); - - describe('mode: standard', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load( - renderComponent('button', { - text: 'Example button', - name: 'example', - value: 'example-value', - }), - ); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + it('has `download` icon when `variants` contains "download"', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); - it('is an `button` element', () => { - const $ = cheerio.load(renderComponent('button')); + faker.renderComponent('button', { + url: 'http://example.com', + variants: 'download', + }); - expect($('button').length).toBe(1); + expect(iconsSpy.occurrences[0].iconType).toBe('download'); }); - it('has the provided `type` attribute', () => { - const $ = cheerio.load( - renderComponent('button', { - type: 'special-type', - }), - ); + it('has provided variant style classes when `variants` contains "print"', () => { + const $ = cheerio.load( + renderComponent('button', { + variants: 'print', + }), + ); - expect($('button').attr('type')).toBe('special-type'); + expect($('.ons-btn').hasClass('ons-btn--print')).toBe(true); + expect($('.ons-btn').hasClass('ons-u-d-no')).toBe(true); + expect($('.ons-btn').hasClass('ons-js-print-btn')).toBe(true); }); - it('has the provided `type` attribute even if print variant is provided', () => { - const $ = cheerio.load( - renderComponent('button', { - type: 'special-type', - variants: 'print', - }), - ); + it('has `print` icon when `variants` contains "print"', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); - expect($('button').attr('type')).toBe('special-type'); - }); + faker.renderComponent('button', { + url: 'http://example.com', + variants: 'print', + }); - it('defaults to being a "submit" button when `type` is not provided', () => { - const $ = cheerio.load(renderComponent('button')); - - expect($('button').attr('type')).toBe('submit'); + expect(iconsSpy.occurrences[0].iconType).toBe('print'); }); - it('defaults to being a "button" when `type` is not provided and `variants` contains "print"', () => { - const $ = cheerio.load( - renderComponent('button', { - variants: 'print', - }), - ); + it('has provided variant style classes when `variants` contains "loader"', () => { + const $ = cheerio.load( + renderComponent('button', { + variants: 'loader', + }), + ); - expect($('button').attr('type')).toBe('button'); + expect($('.ons-btn').hasClass('ons-btn--loader')).toBe(true); + expect($('.ons-btn').hasClass('ons-js-loader')).toBe(true); + expect($('.ons-btn').hasClass('ons-js-submit-btn')).toBe(true); }); - it('has the provided `value` attribute', () => { - const $ = cheerio.load( - renderComponent('button', { - value: 'special-value', - }), - ); - - expect($('button').attr('value')).toBe('special-value'); - }); + it('has `loader` icon when `variants` contains "loader"', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); - it('has the provided `name` attribute', () => { - const $ = cheerio.load( - renderComponent('button', { - name: 'special-name', - }), - ); + faker.renderComponent('button', { + variants: 'loader', + }); - expect($('button').attr('name')).toBe('special-name'); - }); - }); - - describe('mode: link', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load( - renderComponent('button', { - text: 'Example button', - name: 'example', - value: 'example-value', - url: 'http://example.com', - }), - ); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); + expect(iconsSpy.occurrences[0].iconType).toBe('loader'); }); - it('is an `a` element', () => { - const $ = cheerio.load( - renderComponent('button', { - url: 'http://example.com', - }), - ); + it('has `chevron` icon when `variants` contains "mobile"', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); - expect($('a').length).toBe(1); - }); + faker.renderComponent('button', { + variants: 'mobile', + }); - it('has expected style classes', () => { - const $ = cheerio.load( - renderComponent('button', { - url: 'http://example.com', - }), - ); - - expect($('a').hasClass('ons-btn')).toBe(true); - expect($('.ons-btn').hasClass('ons-btn--link')).toBe(true); - expect($('.ons-btn').hasClass('ons-js-submit-btn')).toBe(true); + expect(iconsSpy.occurrences[0].iconType).toBe('chevron'); }); - it('has the provided link', () => { - const $ = cheerio.load( - renderComponent('button', { - url: 'http://example.com', - }), - ); + it('has provided variant style classes when `variants` contains "timer"', () => { + const $ = cheerio.load( + renderComponent('button', { + variants: 'timer', + }), + ); - expect($('a').attr('href')).toBe('http://example.com'); + expect($('.ons-btn').hasClass('ons-js-timer')).toBe(true); + expect($('.ons-btn').hasClass('ons-js-submit-btn')).toBe(true); }); - it('has `arrow-next` icon by default', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); - - faker.renderComponent('button', { - url: 'http://example.com', - }); + it('has additionally provided style classes', () => { + const $ = cheerio.load( + renderComponent('button', { + classes: 'extra-class another-extra-class', + }), + ); - expect(iconsSpy.occurrences[0].iconType).toBe('arrow-next'); + expect($('.ons-btn').hasClass('extra-class')).toBe(true); + expect($('.ons-btn').hasClass('another-extra-class')).toBe(true); }); - it('opens in a new window when `newWindow` is `true`', () => { - const $ = cheerio.load( - renderComponent('button', { - url: 'http://example.com', - newWindow: true, - }), - ); + it('has additionally provided inner style classes', () => { + const $ = cheerio.load( + renderComponent('button', { + innerClasses: 'extra-inner-class another-extra-inner-class', + }), + ); - expect($('a').attr('target')).toBe('_blank'); - expect($('a').attr('rel')).toBe('noopener'); + expect($('.ons-btn__inner').hasClass('extra-inner-class')).toBe(true); + expect($('.ons-btn__inner').hasClass('another-extra-inner-class')).toBe(true); }); - it('has `external-link` icon when `newWindow` is `true`', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); + it('has label text when `text` is provided', () => { + const $ = cheerio.load( + renderComponent('button', { + text: 'Click > me!', + }), + ); - faker.renderComponent('button', { - url: 'http://example.com', - newWindow: true, - }); - - expect(iconsSpy.occurrences[0].iconType).toBe('external-link'); + expect($('.ons-btn__text').html()).toBe('Click > me!'); }); - it('has the `button` role', () => { - const $ = cheerio.load( - renderComponent('button', { - url: 'http://example.com', - }), - ); + it('has label text when `html` is provided', () => { + const $ = cheerio.load( + renderComponent('button', { + html: 'Click me!', + }), + ); - expect($('.ons-btn').attr('role')).toBe('button'); + expect($('.ons-btn__text').html()).toBe('Click me!'); }); - it('has a default new window description when `newWindow` is `true`', () => { - const $ = cheerio.load( - renderComponent('button', { - url: 'http://example.com', - newWindow: true, - }), - ); + it('has button context text when `buttonContext` is provided', () => { + const $ = cheerio.load( + renderComponent('button', { + buttonContext: 'button context text', + }), + ); - expect($('.ons-btn__new-window-description').text()).toBe(' (opens in a new tab)'); + expect($('.ons-btn__context').text()).toBe('button context text'); }); - it('has a custom new window description when `newWindow` is `true` and `newWindowDescription` is provided', () => { - const $ = cheerio.load( - renderComponent('button', { - url: 'http://example.com', - newWindow: true, - newWindowDescription: 'custom opens in a new window text', - }), - ); + it('has custom icon before button text', () => { + const $ = cheerio.load( + renderComponent('button', { + text: 'Click me!', + iconPosition: 'before', + iconType: 'exit', + }), + ); - expect($('.ons-btn__new-window-description').text()).toBe(' (custom opens in a new window text)'); + expect($('.ons-icon + .ons-btn__text').text()).toBe('Click me!'); }); - it('has the `download` attribute when `variants` contains "download"', () => { - const $ = cheerio.load( - renderComponent('button', { - variants: 'download', - }), - ); + it('has custom icon after button text', () => { + const $ = cheerio.load( + renderComponent('button', { + text: 'Click me!', + iconPosition: 'after', + iconType: 'exit', + }), + ); - expect($('.ons-btn').attr('download')).toBeDefined(); + expect($('.ons-btn__text + .ons-icon').prev().text()).toBe('Click me!'); }); - it('does not have the `download` attribute when `variants` contains "download" and `removeDownloadAttribute` is `true`', () => { - const $ = cheerio.load( - renderComponent('button', { - variants: 'download', - removeDownloadAttribute: true, - }), - ); + describe('mode: standard', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load( + renderComponent('button', { + text: 'Example button', + name: 'example', + value: 'example-value', + }), + ); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('is an `button` element', () => { + const $ = cheerio.load(renderComponent('button')); + + expect($('button').length).toBe(1); + }); + + it('has the provided `type` attribute', () => { + const $ = cheerio.load( + renderComponent('button', { + type: 'special-type', + }), + ); + + expect($('button').attr('type')).toBe('special-type'); + }); + + it('has the provided `type` attribute even if print variant is provided', () => { + const $ = cheerio.load( + renderComponent('button', { + type: 'special-type', + variants: 'print', + }), + ); + + expect($('button').attr('type')).toBe('special-type'); + }); + + it('defaults to being a "submit" button when `type` is not provided', () => { + const $ = cheerio.load(renderComponent('button')); + + expect($('button').attr('type')).toBe('submit'); + }); + + it('defaults to being a "button" when `type` is not provided and `variants` contains "print"', () => { + const $ = cheerio.load( + renderComponent('button', { + variants: 'print', + }), + ); + + expect($('button').attr('type')).toBe('button'); + }); + + it('has the provided `value` attribute', () => { + const $ = cheerio.load( + renderComponent('button', { + value: 'special-value', + }), + ); + + expect($('button').attr('value')).toBe('special-value'); + }); + + it('has the provided `name` attribute', () => { + const $ = cheerio.load( + renderComponent('button', { + name: 'special-name', + }), + ); + + expect($('button').attr('name')).toBe('special-name'); + }); + }); - expect($('.ons-btn').attr('download')).toBeUndefined(); + describe('mode: link', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load( + renderComponent('button', { + text: 'Example button', + name: 'example', + value: 'example-value', + url: 'http://example.com', + }), + ); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('is an `a` element', () => { + const $ = cheerio.load( + renderComponent('button', { + url: 'http://example.com', + }), + ); + + expect($('a').length).toBe(1); + }); + + it('has expected style classes', () => { + const $ = cheerio.load( + renderComponent('button', { + url: 'http://example.com', + }), + ); + + expect($('a').hasClass('ons-btn')).toBe(true); + expect($('.ons-btn').hasClass('ons-btn--link')).toBe(true); + expect($('.ons-btn').hasClass('ons-js-submit-btn')).toBe(true); + }); + + it('has the provided link', () => { + const $ = cheerio.load( + renderComponent('button', { + url: 'http://example.com', + }), + ); + + expect($('a').attr('href')).toBe('http://example.com'); + }); + + it('has `arrow-next` icon by default', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); + + faker.renderComponent('button', { + url: 'http://example.com', + }); + + expect(iconsSpy.occurrences[0].iconType).toBe('arrow-next'); + }); + + it('opens in a new window when `newWindow` is `true`', () => { + const $ = cheerio.load( + renderComponent('button', { + url: 'http://example.com', + newWindow: true, + }), + ); + + expect($('a').attr('target')).toBe('_blank'); + expect($('a').attr('rel')).toBe('noopener'); + }); + + it('has `external-link` icon when `newWindow` is `true`', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); + + faker.renderComponent('button', { + url: 'http://example.com', + newWindow: true, + }); + + expect(iconsSpy.occurrences[0].iconType).toBe('external-link'); + }); + + it('has the `button` role', () => { + const $ = cheerio.load( + renderComponent('button', { + url: 'http://example.com', + }), + ); + + expect($('.ons-btn').attr('role')).toBe('button'); + }); + + it('has a default new window description when `newWindow` is `true`', () => { + const $ = cheerio.load( + renderComponent('button', { + url: 'http://example.com', + newWindow: true, + }), + ); + + expect($('.ons-btn__new-window-description').text()).toBe(' (opens in a new tab)'); + }); + + it('has a custom new window description when `newWindow` is `true` and `newWindowDescription` is provided', () => { + const $ = cheerio.load( + renderComponent('button', { + url: 'http://example.com', + newWindow: true, + newWindowDescription: 'custom opens in a new window text', + }), + ); + + expect($('.ons-btn__new-window-description').text()).toBe(' (custom opens in a new window text)'); + }); + + it('has the `download` attribute when `variants` contains "download"', () => { + const $ = cheerio.load( + renderComponent('button', { + variants: 'download', + }), + ); + + expect($('.ons-btn').attr('download')).toBeDefined(); + }); + + it('does not have the `download` attribute when `variants` contains "download" and `removeDownloadAttribute` is `true`', () => { + const $ = cheerio.load( + renderComponent('button', { + variants: 'download', + removeDownloadAttribute: true, + }), + ); + + expect($('.ons-btn').attr('download')).toBeUndefined(); + }); }); - }); }); diff --git a/src/components/button/button.dom.js b/src/components/button/button.dom.js index 870dc18d86..23b8cd77ed 100644 --- a/src/components/button/button.dom.js +++ b/src/components/button/button.dom.js @@ -1,22 +1,22 @@ import domready from '../../js/domready'; async function submitButton() { - const buttons = [...document.querySelectorAll('.ons-js-submit-btn')]; - let submitType; + const buttons = [...document.querySelectorAll('.ons-js-submit-btn')]; + let submitType; - if (buttons.length) { - const SubmitButton = (await import('./button')).default; - buttons.forEach((button) => { - if (button.classList.contains('ons-js-timer')) { - submitType = 'timer'; - } else if (button.classList.contains('ons-js-loader')) { - submitType = 'loader'; - } else if (button.classList.contains('ons-btn--link')) { - submitType = 'link'; - } - new SubmitButton(button, submitType); - }); - } + if (buttons.length) { + const SubmitButton = (await import('./button')).default; + buttons.forEach((button) => { + if (button.classList.contains('ons-js-timer')) { + submitType = 'timer'; + } else if (button.classList.contains('ons-js-loader')) { + submitType = 'loader'; + } else if (button.classList.contains('ons-btn--link')) { + submitType = 'link'; + } + new SubmitButton(button, submitType); + }); + } } domready(submitButton); diff --git a/src/components/button/button.js b/src/components/button/button.js index 3df8ed1696..7cf30f76cf 100644 --- a/src/components/button/button.js +++ b/src/components/button/button.js @@ -1,65 +1,65 @@ let i = 0; export default class SubmitButton { - constructor(button, submitType) { - this.button = button; - this.form = this.button.closest('form'); - this.submitType = submitType; + constructor(button, submitType) { + this.button = button; + this.form = this.button.closest('form'); + this.submitType = submitType; - if (this.submitType == 'loader') { - if (this.form && this.form.length) { - this.form.addEventListener('submit', this.loaderButton.bind(this)); - } else { - this.button.addEventListener('click', this.loaderButton.bind(this)); - } - } else if (this.submitType == 'timer') { - if (this.form && this.form.length) { - this.form.addEventListener('submit', this.timerButton.bind(this)); - } else { - this.button.addEventListener('click', this.timerButton.bind(this)); - } - } else if (this.submitType == 'link') { - this.button.addEventListener('keydown', this.linkButtonDown.bind(this)); - this.button.addEventListener('keyup', this.linkButtonUp.bind(this)); + if (this.submitType == 'loader') { + if (this.form && this.form.length) { + this.form.addEventListener('submit', this.loaderButton.bind(this)); + } else { + this.button.addEventListener('click', this.loaderButton.bind(this)); + } + } else if (this.submitType == 'timer') { + if (this.form && this.form.length) { + this.form.addEventListener('submit', this.timerButton.bind(this)); + } else { + this.button.addEventListener('click', this.timerButton.bind(this)); + } + } else if (this.submitType == 'link') { + this.button.addEventListener('keydown', this.linkButtonDown.bind(this)); + this.button.addEventListener('keyup', this.linkButtonUp.bind(this)); + } } - } - loaderButton(event) { - const loaderButtonEl = event.submitter ? event.submitter : this.button; - loaderButtonEl.classList.add('ons-is-loading'); - loaderButtonEl.setAttribute('disabled', true); - } + loaderButton(event) { + const loaderButtonEl = event.submitter ? event.submitter : this.button; + loaderButtonEl.classList.add('ons-is-loading'); + loaderButtonEl.setAttribute('disabled', true); + } - timerButton(event) { - const timerButtonEl = event.submitter ? event.submitter : this.button; - if (timerButtonEl.tagName === 'A') { - i++; - if (i > 1) { - event.preventDefault(); - } - } else { - timerButtonEl.setAttribute('disabled', true); + timerButton(event) { + const timerButtonEl = event.submitter ? event.submitter : this.button; + if (timerButtonEl.tagName === 'A') { + i++; + if (i > 1) { + event.preventDefault(); + } + } else { + timerButtonEl.setAttribute('disabled', true); + } + setTimeout( + (timerButtonEl) => { + timerButtonEl.removeAttribute('disabled'); + i = 0; + }, + 1000, + timerButtonEl, + ); } - setTimeout( - (timerButtonEl) => { - timerButtonEl.removeAttribute('disabled'); - i = 0; - }, - 1000, - timerButtonEl, - ); - } - linkButtonDown(e) { - if (e.keyCode == 32 || e.keyCode == 13) { - this.button.classList.add('active'); + linkButtonDown(e) { + if (e.keyCode == 32 || e.keyCode == 13) { + this.button.classList.add('active'); + } } - } - linkButtonUp(e) { - if (e.keyCode == 32 || e.keyCode == 13) { - this.button.classList.remove('active'); - e.preventDefault(); - this.button.click(); + linkButtonUp(e) { + if (e.keyCode == 32 || e.keyCode == 13) { + this.button.classList.remove('active'); + e.preventDefault(); + this.button.click(); + } } - } } diff --git a/src/components/button/button.spec.js b/src/components/button/button.spec.js index ca94970c2b..d268382b0c 100644 --- a/src/components/button/button.spec.js +++ b/src/components/button/button.spec.js @@ -1,268 +1,276 @@ import { renderComponent, setTestPage } from '../../tests/helpers/rendering'; describe('script: button', () => { - describe('mode: link', () => { - it('navigates to url when button is clicked with the spacebar', async () => { - await setTestPage( - '/test', - renderComponent('button', { - id: 'test-button', - url: '/test/path#abc', - }), - ); - - await page.focus('#test-button'); - - await Promise.all([page.waitForNavigation(), page.keyboard.press('Space')]); - - const url = await page.url(); - expect(url).toBe(`http://localhost:${process.env.TEST_PORT}/test/path#abc`); - }); - }); - - describe('mode: standard', () => { - it('allow rapidly repeated submissions when in a form', async () => { - await setTestPage( - '/test', - ` -
    - ${renderComponent('button', { id: 'test-button' })} -
    - `, - ); - - await page.evaluate(() => { - window.__COUNTER = 0; - document.querySelector('form').addEventListener('submit', (event) => { - window.__COUNTER++; - event.preventDefault(); + describe('mode: link', () => { + it('navigates to url when button is clicked with the spacebar', async () => { + await setTestPage( + '/test', + renderComponent('button', { + id: 'test-button', + url: '/test/path#abc', + }), + ); + + await page.focus('#test-button'); + + await Promise.all([page.waitForNavigation(), page.keyboard.press('Space')]); + + const url = await page.url(); + expect(url).toBe(`http://localhost:${process.env.TEST_PORT}/test/path#abc`); }); - }); - - await page.click('#test-button'); - await page.click('#test-button'); - - const counter = await page.evaluate(() => window.__COUNTER); - expect(counter).toBe(2); - }); - }); - - describe('mode: loader', () => { - it('disables button when clicked', async () => { - await setTestPage( - '/test', - renderComponent('button', { - id: 'test-button', - text: 'Submit', - variants: 'loader', - }), - ); - - await page.click('#test-button'); - - const isButtonDisabled = await page.evaluate(() => document.querySelector('#test-button').getAttribute('disabled')); - expect(isButtonDisabled).toBe('true'); }); - it('initialised without loading style applied', async () => { - await setTestPage( - '/test', - renderComponent('button', { - id: 'test-button', - text: 'Submit', - variants: 'loader', - }), - ); - - const hasIsLoadingClass = await page.evaluate(() => document.querySelector('#test-button').classList.contains('ons-is-loading')); - expect(hasIsLoadingClass).toBe(false); - }); - - it('applies loading style when clicked', async () => { - await setTestPage( - '/test', - renderComponent('button', { - id: 'test-button', - text: 'Submit', - variants: 'loader', - }), - ); - - await page.click('#test-button'); - - const hasIsLoadingClass = await page.evaluate(() => document.querySelector('#test-button').classList.contains('ons-is-loading')); - expect(hasIsLoadingClass).toBe(true); + describe('mode: standard', () => { + it('allow rapidly repeated submissions when in a form', async () => { + await setTestPage( + '/test', + ` +
    + ${renderComponent('button', { id: 'test-button' })} +
    + `, + ); + + await page.evaluate(() => { + window.__COUNTER = 0; + document.querySelector('form').addEventListener('submit', (event) => { + window.__COUNTER++; + event.preventDefault(); + }); + }); + + await page.click('#test-button'); + await page.click('#test-button'); + + const counter = await page.evaluate(() => window.__COUNTER); + expect(counter).toBe(2); + }); }); - it('disables button when clicked when in a form', async () => { - await setTestPage( - '/test', - ` -
    - ${renderComponent('button', { id: 'test-button', text: 'Submit', variants: 'loader' })} -
    - `, - ); - - await page.click('#test-button'); + describe('mode: loader', () => { + it('disables button when clicked', async () => { + await setTestPage( + '/test', + renderComponent('button', { + id: 'test-button', + text: 'Submit', + variants: 'loader', + }), + ); + + await page.click('#test-button'); + + const isButtonDisabled = await page.evaluate(() => document.querySelector('#test-button').getAttribute('disabled')); + expect(isButtonDisabled).toBe('true'); + }); - const isButtonDisabled = (await page.$('#test-button[disabled]')) !== null; - expect(isButtonDisabled).toBe(true); - }); + it('initialised without loading style applied', async () => { + await setTestPage( + '/test', + renderComponent('button', { + id: 'test-button', + text: 'Submit', + variants: 'loader', + }), + ); + + const hasIsLoadingClass = await page.evaluate(() => + document.querySelector('#test-button').classList.contains('ons-is-loading'), + ); + expect(hasIsLoadingClass).toBe(false); + }); - it('initialised without loading style applied when in a form', async () => { - await setTestPage( - '/test', - ` -
    - ${renderComponent('button', { id: 'test-button', text: 'Submit', variants: 'loader' })} -
    - `, - ); - - const hasIsLoadingClass = await page.evaluate(() => document.querySelector('#test-button').classList.contains('ons-is-loading')); - expect(hasIsLoadingClass).toBe(false); - }); + it('applies loading style when clicked', async () => { + await setTestPage( + '/test', + renderComponent('button', { + id: 'test-button', + text: 'Submit', + variants: 'loader', + }), + ); + + await page.click('#test-button'); + + const hasIsLoadingClass = await page.evaluate(() => + document.querySelector('#test-button').classList.contains('ons-is-loading'), + ); + expect(hasIsLoadingClass).toBe(true); + }); - it('applies loading style when clicked when in a form', async () => { - await setTestPage( - '/test', - ` -
    - ${renderComponent('button', { id: 'test-button', text: 'Submit', variants: 'loader' })} -
    - `, - ); + it('disables button when clicked when in a form', async () => { + await setTestPage( + '/test', + ` +
    + ${renderComponent('button', { id: 'test-button', text: 'Submit', variants: 'loader' })} +
    + `, + ); - await page.click('#test-button'); + await page.click('#test-button'); - const hasIsLoadingClass = await page.evaluate(() => document.querySelector('#test-button').classList.contains('ons-is-loading')); - expect(hasIsLoadingClass).toBe(true); - }); - }); - - describe('mode: timer', () => { - it('allow intentional repeated submissions', async () => { - await setTestPage( - '/test', - renderComponent('button', { - id: 'test-button', - variants: 'timer', - text: 'Submit', - }), - ); - - await page.evaluate(() => { - window.__COUNTER = 0; - document.querySelector('#test-button').addEventListener('click', (event) => { - window.__COUNTER++; - event.preventDefault(); + const isButtonDisabled = (await page.$('#test-button[disabled]')) !== null; + expect(isButtonDisabled).toBe(true); }); - }); - await page.click('#test-button'); - await page.click('#test-button', { delay: 1000 }); + it('initialised without loading style applied when in a form', async () => { + await setTestPage( + '/test', + ` +
    + ${renderComponent('button', { id: 'test-button', text: 'Submit', variants: 'loader' })} +
    + `, + ); + + const hasIsLoadingClass = await page.evaluate(() => + document.querySelector('#test-button').classList.contains('ons-is-loading'), + ); + expect(hasIsLoadingClass).toBe(false); + }); - const counter = await page.evaluate(() => window.__COUNTER); - expect(counter).toBe(2); + it('applies loading style when clicked when in a form', async () => { + await setTestPage( + '/test', + ` +
    + ${renderComponent('button', { id: 'test-button', text: 'Submit', variants: 'loader' })} +
    + `, + ); + + await page.click('#test-button'); + + const hasIsLoadingClass = await page.evaluate(() => + document.querySelector('#test-button').classList.contains('ons-is-loading'), + ); + expect(hasIsLoadingClass).toBe(true); + }); }); - it('prevents unintentional repeated submissions', async () => { - await setTestPage( - '/test', - renderComponent('button', { - id: 'test-button', - variants: 'timer', - text: 'Submit', - }), - ); - - await page.evaluate(() => { - window.__COUNTER = 0; - document.querySelector('#test-button').addEventListener('click', (event) => { - window.__COUNTER++; - event.preventDefault(); + describe('mode: timer', () => { + it('allow intentional repeated submissions', async () => { + await setTestPage( + '/test', + renderComponent('button', { + id: 'test-button', + variants: 'timer', + text: 'Submit', + }), + ); + + await page.evaluate(() => { + window.__COUNTER = 0; + document.querySelector('#test-button').addEventListener('click', (event) => { + window.__COUNTER++; + event.preventDefault(); + }); + }); + + await page.click('#test-button'); + await page.click('#test-button', { delay: 1000 }); + + const counter = await page.evaluate(() => window.__COUNTER); + expect(counter).toBe(2); }); - }); - - await page.click('#test-button'); - await page.click('#test-button'); - const counter = await page.evaluate(() => window.__COUNTER); - expect(counter).toBe(1); - }); - - it('allow intentional repeated submissions when in a form', async () => { - await setTestPage( - '/test', - ` -
    - ${renderComponent('button', { id: 'test-button', variants: 'timer', text: 'Submit' })} -
    - `, - ); - - await page.evaluate(() => { - window.__COUNTER = 0; - document.querySelector('form').addEventListener('submit', (event) => { - window.__COUNTER++; - event.preventDefault(); + it('prevents unintentional repeated submissions', async () => { + await setTestPage( + '/test', + renderComponent('button', { + id: 'test-button', + variants: 'timer', + text: 'Submit', + }), + ); + + await page.evaluate(() => { + window.__COUNTER = 0; + document.querySelector('#test-button').addEventListener('click', (event) => { + window.__COUNTER++; + event.preventDefault(); + }); + }); + + await page.click('#test-button'); + await page.click('#test-button'); + + const counter = await page.evaluate(() => window.__COUNTER); + expect(counter).toBe(1); }); - }); - await page.click('#test-button'); - await page.click('#test-button', { delay: 1000 }); + it('allow intentional repeated submissions when in a form', async () => { + await setTestPage( + '/test', + ` +
    + ${renderComponent('button', { id: 'test-button', variants: 'timer', text: 'Submit' })} +
    + `, + ); + + await page.evaluate(() => { + window.__COUNTER = 0; + document.querySelector('form').addEventListener('submit', (event) => { + window.__COUNTER++; + event.preventDefault(); + }); + }); + + await page.click('#test-button'); + await page.click('#test-button', { delay: 1000 }); + + const counter = await page.evaluate(() => window.__COUNTER); + expect(counter).toBe(2); + }); - const counter = await page.evaluate(() => window.__COUNTER); - expect(counter).toBe(2); + it('prevents unintentional repeated submissions when in a form', async () => { + await setTestPage( + '/test', + ` +
    + ${renderComponent('button', { id: 'test-button', variants: 'timer', text: 'Submit' })} +
    + `, + ); + + await page.evaluate(() => { + window.__COUNTER = 0; + document.querySelector('form').addEventListener('submit', (event) => { + window.__COUNTER++; + event.preventDefault(); + }); + }); + + await page.click('#test-button'); + await page.click('#test-button'); + + const counter = await page.evaluate(() => window.__COUNTER); + expect(counter).toBe(1); + }); }); - it('prevents unintentional repeated submissions when in a form', async () => { - await setTestPage( - '/test', - ` -
    - ${renderComponent('button', { id: 'test-button', variants: 'timer', text: 'Submit' })} -
    - `, - ); - - await page.evaluate(() => { - window.__COUNTER = 0; - document.querySelector('form').addEventListener('submit', (event) => { - window.__COUNTER++; - event.preventDefault(); + describe('style: print', () => { + it('displays the browsers print interface', async () => { + await setTestPage( + '/test', + renderComponent('button', { + id: 'test-button', + type: 'button', + text: 'Print this page', + variants: 'print', + }), + ); + + await page.evaluate(() => { + window.print = () => (window.wasPrinted = 'yes'); + }); + + await page.click('#test-button'); + + const wasPrinted = await page.evaluate(() => window.wasPrinted); + expect(wasPrinted).toBe('yes'); }); - }); - - await page.click('#test-button'); - await page.click('#test-button'); - - const counter = await page.evaluate(() => window.__COUNTER); - expect(counter).toBe(1); - }); - }); - - describe('style: print', () => { - it('displays the browsers print interface', async () => { - await setTestPage( - '/test', - renderComponent('button', { - id: 'test-button', - type: 'button', - text: 'Print this page', - variants: 'print', - }), - ); - - await page.evaluate(() => { - window.print = () => (window.wasPrinted = 'yes'); - }); - - await page.click('#test-button'); - - const wasPrinted = await page.evaluate(() => window.wasPrinted); - expect(wasPrinted).toBe('yes'); }); - }); }); diff --git a/src/components/call-to-action/_call-to-action.scss b/src/components/call-to-action/_call-to-action.scss index 2cbb68e7ee..0e0ec82338 100644 --- a/src/components/call-to-action/_call-to-action.scss +++ b/src/components/call-to-action/_call-to-action.scss @@ -1,8 +1,8 @@ .ons-call-to-action { - background: var(--ons-color-cta-bg); - padding: 0.85rem 0; + background: var(--ons-color-cta-bg); + padding: 0.85rem 0; - &__heading { - padding-right: 0.2rem; - } + &__heading { + padding-right: 0.2rem; + } } diff --git a/src/components/call-to-action/_macro.spec.js b/src/components/call-to-action/_macro.spec.js index fe86743f9f..b0396300b0 100644 --- a/src/components/call-to-action/_macro.spec.js +++ b/src/components/call-to-action/_macro.spec.js @@ -6,43 +6,43 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_CALL_TO_ACTION = { - headingText: 'Call to action heading.', - paragraphText: 'Descriptive text about call to action', - button: { - text: 'Start', - url: 'https://example.com/start', - }, + headingText: 'Call to action heading.', + paragraphText: 'Descriptive text about call to action', + button: { + text: 'Start', + url: 'https://example.com/start', + }, }; describe('macro: call-to-action', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('call-to-action', EXAMPLE_CALL_TO_ACTION)); + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('call-to-action', EXAMPLE_CALL_TO_ACTION)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - it('has the provided `headingText`', () => { - const $ = cheerio.load(renderComponent('call-to-action', EXAMPLE_CALL_TO_ACTION)); + it('has the provided `headingText`', () => { + const $ = cheerio.load(renderComponent('call-to-action', EXAMPLE_CALL_TO_ACTION)); - const headingText = $('.ons-call-to-action__heading').text().trim(); - expect(headingText).toBe('Call to action heading.'); - }); + const headingText = $('.ons-call-to-action__heading').text().trim(); + expect(headingText).toBe('Call to action heading.'); + }); - it('has the provided `paragraphText`', () => { - const $ = cheerio.load(renderComponent('call-to-action', EXAMPLE_CALL_TO_ACTION)); + it('has the provided `paragraphText`', () => { + const $ = cheerio.load(renderComponent('call-to-action', EXAMPLE_CALL_TO_ACTION)); - const paragraphText = $('.ons-call-to-action__text').text().trim(); - expect(paragraphText).toBe('Descriptive text about call to action'); - }); + const paragraphText = $('.ons-call-to-action__text').text().trim(); + expect(paragraphText).toBe('Descriptive text about call to action'); + }); - it('outputs the expected call-to-action button', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); + it('outputs the expected call-to-action button', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); - faker.renderComponent('call-to-action', EXAMPLE_CALL_TO_ACTION); + faker.renderComponent('call-to-action', EXAMPLE_CALL_TO_ACTION); - expect(buttonSpy.occurrences[0]).toHaveProperty('text', 'Start'); - expect(buttonSpy.occurrences[0]).toHaveProperty('url', 'https://example.com/start'); - }); + expect(buttonSpy.occurrences[0]).toHaveProperty('text', 'Start'); + expect(buttonSpy.occurrences[0]).toHaveProperty('url', 'https://example.com/start'); + }); }); diff --git a/src/components/card/_card.scss b/src/components/card/_card.scss index c489c98ae9..852da8e0a1 100644 --- a/src/components/card/_card.scss +++ b/src/components/card/_card.scss @@ -1,32 +1,32 @@ .ons-card { - margin: 0 0 2rem; - width: 100%; + margin: 0 0 2rem; + width: 100%; - &__link { - display: flex; - flex-direction: column; - margin-bottom: 1rem; - width: fit-content; + &__link { + display: flex; + flex-direction: column; + margin-bottom: 1rem; + width: fit-content; - & > .ons-card__title { - margin-bottom: 0; + > .ons-card__title { + margin-bottom: 0; + } } - } - &__link:hover { - text-decoration-thickness: 3px; - } + &__link:hover { + text-decoration-thickness: 3px; + } - &__title { - // This is to allow the focus state for multi lined title links to render the focus style correctly. - // This should be corrected when the typography scale is improved. - line-height: 1.65 !important; - } + &__title { + // This is to allow the focus state for multi lined title links to render the focus style correctly. + // This should be corrected when the typography scale is improved. + line-height: 1.65 !important; + } - @include mq(m) { - margin: 0; - .ons-grid__col & { - padding-right: 1rem; + @include mq(m) { + margin: 0; + .ons-grid__col & { + padding-right: 1rem; + } } - } } diff --git a/src/components/card/_macro.spec.js b/src/components/card/_macro.spec.js index a3488f5011..ba6bf0261d 100644 --- a/src/components/card/_macro.spec.js +++ b/src/components/card/_macro.spec.js @@ -6,232 +6,232 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_CARD_WITHOUT_IMAGE = { - title: 'Example card title', - text: 'Example card text.', - textId: 'example-text-id', + title: 'Example card title', + text: 'Example card text.', + textId: 'example-text-id', }; const EXAMPLE_CARD_WITH_IMAGE = { - title: 'Example card title', - text: 'Example card text.', - textId: 'example-text-id', - image: { - smallSrc: 'example-small.png', - largeSrc: 'example-large.png', - alt: 'Example alt text', - }, + title: 'Example card title', + text: 'Example card text.', + textId: 'example-text-id', + image: { + smallSrc: 'example-small.png', + largeSrc: 'example-large.png', + alt: 'Example alt text', + }, }; const EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE = { - title: 'Example card title', - text: 'Example card text.', - textId: 'example-text-id', - image: true, + title: 'Example card title', + text: 'Example card text.', + textId: 'example-text-id', + image: true, }; const EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE_WITH_PATH = { - title: 'Example card title', - text: 'Example card text.', - textId: 'example-text-id', - image: { - placeholderURL: '/placeholder-image-url', - }, + title: 'Example card title', + text: 'Example card text.', + textId: 'example-text-id', + image: { + placeholderURL: '/placeholder-image-url', + }, }; describe('macro: card', () => { - describe('mode: without image', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITHOUT_IMAGE)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the provided `title` text', () => { - const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITHOUT_IMAGE)); - - expect($('.ons-card__title').text().trim()).toBe('Example card title'); - }); - - it.each([ - [1, 'h1'], - [4, 'h4'], - ])('has the correct element type for the provided `headingLevel` (%i -> %s)', (headingLevel, expectedTitleTag) => { - const $ = cheerio.load( - renderComponent('card', { - ...EXAMPLE_CARD_WITHOUT_IMAGE, - headingLevel, - }), - ); - - expect($(`${expectedTitleTag}.ons-card__title`).text().trim()).toBe('Example card title'); - }); - - it('has the provided `text` accessible via the `textId` identifier', () => { - const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITHOUT_IMAGE)); - - expect($('#example-text-id').text().trim()).toBe('Example card text.'); - }); - - it('renders the provided `itemsList` using the `list` component', () => { - const faker = templateFaker(); - const listsSpy = faker.spy('list'); - - faker.renderComponent('card', { - ...EXAMPLE_CARD_WITHOUT_IMAGE, - itemsList: [{ text: 'Test item 1' }, { text: 'Test item 2' }], - }); - - expect(listsSpy.occurrences[0]).toEqual({ - variants: 'dashed', - itemsList: [{ text: 'Test item 1' }, { text: 'Test item 2' }], - }); + describe('mode: without image', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITHOUT_IMAGE)); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has the provided `title` text', () => { + const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITHOUT_IMAGE)); + + expect($('.ons-card__title').text().trim()).toBe('Example card title'); + }); + + it.each([ + [1, 'h1'], + [4, 'h4'], + ])('has the correct element type for the provided `headingLevel` (%i -> %s)', (headingLevel, expectedTitleTag) => { + const $ = cheerio.load( + renderComponent('card', { + ...EXAMPLE_CARD_WITHOUT_IMAGE, + headingLevel, + }), + ); + + expect($(`${expectedTitleTag}.ons-card__title`).text().trim()).toBe('Example card title'); + }); + + it('has the provided `text` accessible via the `textId` identifier', () => { + const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITHOUT_IMAGE)); + + expect($('#example-text-id').text().trim()).toBe('Example card text.'); + }); + + it('renders the provided `itemsList` using the `list` component', () => { + const faker = templateFaker(); + const listsSpy = faker.spy('list'); + + faker.renderComponent('card', { + ...EXAMPLE_CARD_WITHOUT_IMAGE, + itemsList: [{ text: 'Test item 1' }, { text: 'Test item 2' }], + }); + + expect(listsSpy.occurrences[0]).toEqual({ + variants: 'dashed', + itemsList: [{ text: 'Test item 1' }, { text: 'Test item 2' }], + }); + }); + + it('outputs a hyperlink with the provided `url`', () => { + const $ = cheerio.load( + renderComponent('card', { + ...EXAMPLE_CARD_WITHOUT_IMAGE, + url: 'https://example.com', + }), + ); + + expect($('.ons-card__link').attr('href')).toBe('https://example.com'); + }); }); - it('outputs a hyperlink with the provided `url`', () => { - const $ = cheerio.load( - renderComponent('card', { - ...EXAMPLE_CARD_WITHOUT_IMAGE, - url: 'https://example.com', - }), - ); + describe('mode: with image', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_IMAGE)); - expect($('.ons-card__link').attr('href')).toBe('https://example.com'); - }); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - describe('mode: with image', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_IMAGE)); + it('has the provided `title` text', () => { + const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_IMAGE)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + expect($('.ons-card__title').text().trim()).toBe('Example card title'); + }); - it('has the provided `title` text', () => { - const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_IMAGE)); + it.each([ + [1, 'h1'], + [4, 'h4'], + ])('has the correct element type for the provided `headingLevel` (%i -> %s)', (headingLevel, expectedTitleTag) => { + const $ = cheerio.load( + renderComponent('card', { + ...EXAMPLE_CARD_WITH_IMAGE, + headingLevel, + }), + ); - expect($('.ons-card__title').text().trim()).toBe('Example card title'); - }); + expect($(`${expectedTitleTag}.ons-card__title`).text().trim()).toBe('Example card title'); + }); - it.each([ - [1, 'h1'], - [4, 'h4'], - ])('has the correct element type for the provided `headingLevel` (%i -> %s)', (headingLevel, expectedTitleTag) => { - const $ = cheerio.load( - renderComponent('card', { - ...EXAMPLE_CARD_WITH_IMAGE, - headingLevel, - }), - ); - - expect($(`${expectedTitleTag}.ons-card__title`).text().trim()).toBe('Example card title'); - }); + it('has the provided `text`', () => { + const $ = cheerio.load( + renderComponent('card', { + ...EXAMPLE_CARD_WITH_IMAGE, + }), + ); - it('has the provided `text`', () => { - const $ = cheerio.load( - renderComponent('card', { - ...EXAMPLE_CARD_WITH_IMAGE, - }), - ); + expect($('#example-text-id').text().trim()).toBe('Example card text.'); + }); - expect($('#example-text-id').text().trim()).toBe('Example card text.'); - }); + it('outputs a hyperlink with the provided `url`', () => { + const $ = cheerio.load( + renderComponent('card', { + ...EXAMPLE_CARD_WITH_IMAGE, + url: 'https://example.com', + }), + ); - it('outputs a hyperlink with the provided `url`', () => { - const $ = cheerio.load( - renderComponent('card', { - ...EXAMPLE_CARD_WITH_IMAGE, - url: 'https://example.com', - }), - ); + expect($('.ons-card__link').attr('href')).toBe('https://example.com'); + }); - expect($('.ons-card__link').attr('href')).toBe('https://example.com'); - }); + describe('with a custom image', () => { + it('outputs an `img` element', () => { + const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_IMAGE)); - describe('with a custom image', () => { - it('outputs an `img` element', () => { - const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_IMAGE)); + expect($('.ons-card__image')[0].tagName).toBe('img'); + }); - expect($('.ons-card__image')[0].tagName).toBe('img'); - }); + it('outputs an `img` element with the expected `srcset`', () => { + const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_IMAGE)); - it('outputs an `img` element with the expected `srcset`', () => { - const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_IMAGE)); + expect($('.ons-card__image').attr('srcset')).toBe('example-small.png 1x, example-large.png 2x'); + }); - expect($('.ons-card__image').attr('srcset')).toBe('example-small.png 1x, example-large.png 2x'); - }); + it('outputs an `img` element with the expected `src`', () => { + const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_IMAGE)); - it('outputs an `img` element with the expected `src`', () => { - const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_IMAGE)); + expect($('.ons-card__image').attr('src')).toBe('example-small.png'); + }); - expect($('.ons-card__image').attr('src')).toBe('example-small.png'); - }); + it('outputs an `img` element with the expected alt text', () => { + const $ = cheerio.load( + renderComponent('card', { + ...EXAMPLE_CARD_WITH_IMAGE, + alt: 'Example alt text', + }), + ); - it('outputs an `img` element with the expected alt text', () => { - const $ = cheerio.load( - renderComponent('card', { - ...EXAMPLE_CARD_WITH_IMAGE, - alt: 'Example alt text', - }), - ); + expect($('.ons-card__image').attr('alt')).toBe('Example alt text'); + }); + }); - expect($('.ons-card__image').attr('alt')).toBe('Example alt text'); - }); - }); + describe('with a default placeholder image', () => { + it('outputs an `img` element', () => { + const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE)); - describe('with a default placeholder image', () => { - it('outputs an `img` element', () => { - const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE)); + expect($('.ons-card__image')[0].tagName).toBe('img'); + }); - expect($('.ons-card__image')[0].tagName).toBe('img'); - }); + it('outputs an `img` element with the expected `srcset`', () => { + const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE)); - it('outputs an `img` element with the expected `srcset`', () => { - const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE)); + expect($('.ons-card__image').attr('srcset')).toBe('/img/small/placeholder-card.png 1x, /img/large/placeholder-card.png 2x'); + }); - expect($('.ons-card__image').attr('srcset')).toBe('/img/small/placeholder-card.png 1x, /img/large/placeholder-card.png 2x'); - }); + it('outputs an `img` element with the expected `src`', () => { + const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE)); - it('outputs an `img` element with the expected `src`', () => { - const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE)); + expect($('.ons-card__image').attr('src')).toBe('/img/small/placeholder-card.png'); + }); - expect($('.ons-card__image').attr('src')).toBe('/img/small/placeholder-card.png'); - }); + it('outputs an `img` element with the expected empty alt text', () => { + const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE)); - it('outputs an `img` element with the expected empty alt text', () => { - const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE)); - - expect($('.ons-card__image').attr('alt')).toBe(''); - }); - }); + expect($('.ons-card__image').attr('alt')).toBe(''); + }); + }); - describe('with a default placeholder image with `placeholderURL`', () => { - it('outputs an `img` element', () => { - const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE_WITH_PATH)); + describe('with a default placeholder image with `placeholderURL`', () => { + it('outputs an `img` element', () => { + const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE_WITH_PATH)); - expect($('.ons-card__image')[0].tagName).toBe('img'); - }); + expect($('.ons-card__image')[0].tagName).toBe('img'); + }); - it('outputs an `img` element with the expected `srcset`', () => { - const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE_WITH_PATH)); + it('outputs an `img` element with the expected `srcset`', () => { + const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE_WITH_PATH)); - expect($('.ons-card__image').attr('srcset')).toBe( - '/placeholder-image-url/img/small/placeholder-card.png 1x, /placeholder-image-url/img/large/placeholder-card.png 2x', - ); - }); + expect($('.ons-card__image').attr('srcset')).toBe( + '/placeholder-image-url/img/small/placeholder-card.png 1x, /placeholder-image-url/img/large/placeholder-card.png 2x', + ); + }); - it('outputs an `img` element with the expected `src`', () => { - const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE_WITH_PATH)); + it('outputs an `img` element with the expected `src`', () => { + const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE_WITH_PATH)); - expect($('.ons-card__image').attr('src')).toBe('/placeholder-image-url/img/small/placeholder-card.png'); - }); + expect($('.ons-card__image').attr('src')).toBe('/placeholder-image-url/img/small/placeholder-card.png'); + }); - it('outputs an `img` element with the expected empty alt text', () => { - const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE_WITH_PATH)); + it('outputs an `img` element with the expected empty alt text', () => { + const $ = cheerio.load(renderComponent('card', EXAMPLE_CARD_WITH_PLACEHOLDER_IMAGE_WITH_PATH)); - expect($('.ons-card__image').attr('alt')).toBe(''); - }); + expect($('.ons-card__image').attr('alt')).toBe(''); + }); + }); }); - }); }); diff --git a/src/components/char-check-limit/_macro.spec.js b/src/components/char-check-limit/_macro.spec.js index 0b00499707..6eba91e262 100644 --- a/src/components/char-check-limit/_macro.spec.js +++ b/src/components/char-check-limit/_macro.spec.js @@ -6,64 +6,64 @@ import axe from '../../tests/helpers/axe'; import { renderComponent } from '../../tests/helpers/rendering'; const EXAMPLE_CHAR_CHECK_LIMIT = { - id: 'example-char-check-limit', - charCountSingular: 'You have {x} character remaining', - charCountPlural: 'You have {x} characters remaining', - charCountOverLimitSingular: '{x} character too many', - charCountOverLimitPlural: '{x} characters too many', + id: 'example-char-check-limit', + charCountSingular: 'You have {x} character remaining', + charCountPlural: 'You have {x} characters remaining', + charCountOverLimitSingular: '{x} character too many', + charCountOverLimitPlural: '{x} characters too many', }; describe('macro: char-check-limit', () => { - it('passes jest-axe checks without check', async () => { - const $ = cheerio.load(renderComponent('char-check-limit', EXAMPLE_CHAR_CHECK_LIMIT)); + it('passes jest-axe checks without check', async () => { + const $ = cheerio.load(renderComponent('char-check-limit', EXAMPLE_CHAR_CHECK_LIMIT)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - it('passes jest-axe checks with check', async () => { - const $ = cheerio.load( - renderComponent( - 'char-check-limit', - { - ...EXAMPLE_CHAR_CHECK_LIMIT, - variant: 'check', - }, - ['

    Test content.

    '], - ), - ); + it('passes jest-axe checks with check', async () => { + const $ = cheerio.load( + renderComponent( + 'char-check-limit', + { + ...EXAMPLE_CHAR_CHECK_LIMIT, + variant: 'check', + }, + ['

    Test content.

    '], + ), + ); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - it('has the provided `id` attribute', () => { - const $ = cheerio.load(renderComponent('char-check-limit', EXAMPLE_CHAR_CHECK_LIMIT)); + it('has the provided `id` attribute', () => { + const $ = cheerio.load(renderComponent('char-check-limit', EXAMPLE_CHAR_CHECK_LIMIT)); - expect($('.ons-input__limit').attr('id')).toBe('example-char-check-limit'); - }); + expect($('.ons-input__limit').attr('id')).toBe('example-char-check-limit'); + }); - it('has the provided data attributes', () => { - const $ = cheerio.load(renderComponent('char-check-limit', EXAMPLE_CHAR_CHECK_LIMIT)); + it('has the provided data attributes', () => { + const $ = cheerio.load(renderComponent('char-check-limit', EXAMPLE_CHAR_CHECK_LIMIT)); - expect($('.ons-input__limit').attr('data-charcount-singular')).toBe('You have {x} character remaining'); - expect($('.ons-input__limit').attr('data-charcount-plural')).toBe('You have {x} characters remaining'); - expect($('.ons-input__limit').attr('data-charcount-limit-singular')).toBe('{x} character too many'); - expect($('.ons-input__limit').attr('data-charcount-limit-plural')).toBe('{x} characters too many'); - }); + expect($('.ons-input__limit').attr('data-charcount-singular')).toBe('You have {x} character remaining'); + expect($('.ons-input__limit').attr('data-charcount-plural')).toBe('You have {x} characters remaining'); + expect($('.ons-input__limit').attr('data-charcount-limit-singular')).toBe('{x} character too many'); + expect($('.ons-input__limit').attr('data-charcount-limit-plural')).toBe('{x} characters too many'); + }); - it('has expected nested content', () => { - const $ = cheerio.load( - renderComponent( - 'char-check-limit', - { - ...EXAMPLE_CHAR_CHECK_LIMIT, - variant: 'check', - }, - ['

    Test content.

    '], - ), - ); + it('has expected nested content', () => { + const $ = cheerio.load( + renderComponent( + 'char-check-limit', + { + ...EXAMPLE_CHAR_CHECK_LIMIT, + variant: 'check', + }, + ['

    Test content.

    '], + ), + ); - expect($('.ons-js-char-check-input').html().trim()).toBe('

    Test content.

    '); - }); + expect($('.ons-js-char-check-input').html().trim()).toBe('

    Test content.

    '); + }); }); diff --git a/src/components/char-check-limit/character-check.js b/src/components/char-check-limit/character-check.js index c54e022057..4c3a156622 100644 --- a/src/components/char-check-limit/character-check.js +++ b/src/components/char-check-limit/character-check.js @@ -5,75 +5,75 @@ const attrCharCheckCountdown = 'data-char-check-countdown'; const attrCharCheckVal = 'data-char-check-num'; export default class CharCheck { - constructor(context) { - this.context = context; - this.input = this.context.querySelector('input'); - this.button = this.context.parentNode.querySelector('button'); - this.checkElement = document.getElementById(this.input.getAttribute(attrCharCheckRef)); - this.checkVal = this.input.getAttribute(attrCharCheckVal); - this.countdown = this.input.getAttribute(attrCharCheckCountdown) || false; - this.singularMessage = this.checkElement.getAttribute('data-charcount-singular') || null; - this.pluralMessage = this.checkElement.getAttribute('data-charcount-plural') || null; - this.charLimitSingularMessage = this.checkElement.getAttribute('data-charcount-limit-singular') || null; - this.charLimitPluralMessage = this.checkElement.getAttribute('data-charcount-limit-plural') || null; + constructor(context) { + this.context = context; + this.input = this.context.querySelector('input'); + this.button = this.context.parentNode.querySelector('button'); + this.checkElement = document.getElementById(this.input.getAttribute(attrCharCheckRef)); + this.checkVal = this.input.getAttribute(attrCharCheckVal); + this.countdown = this.input.getAttribute(attrCharCheckCountdown) || false; + this.singularMessage = this.checkElement.getAttribute('data-charcount-singular') || null; + this.pluralMessage = this.checkElement.getAttribute('data-charcount-plural') || null; + this.charLimitSingularMessage = this.checkElement.getAttribute('data-charcount-limit-singular') || null; + this.charLimitPluralMessage = this.checkElement.getAttribute('data-charcount-limit-plural') || null; - this.updateCheckReadout(this.input); + this.updateCheckReadout(this.input); - if (this.button) { - this.setButtonState(this.checkVal); + if (this.button) { + this.setButtonState(this.checkVal); + } + this.input.addEventListener('input', this.updateCheckReadout.bind(this)); } - this.input.addEventListener('input', this.updateCheckReadout.bind(this)); - } - updateCheckReadout(event, firstRun) { - const value = this.input.value; - const remaining = this.checkVal - value.length; - // Prevent aria live announcement when component initialises - if (!firstRun && event.inputType) { - this.checkElement.setAttribute('aria-live', 'polite'); - } else { - this.checkElement.removeAttribute('aria-live'); + updateCheckReadout(event, firstRun) { + const value = this.input.value; + const remaining = this.checkVal - value.length; + // Prevent aria live announcement when component initialises + if (!firstRun && event.inputType) { + this.checkElement.setAttribute('aria-live', 'polite'); + } else { + this.checkElement.removeAttribute('aria-live'); + } + + this.checkRemaining(remaining); + this.setCheckClass(remaining, this.input, inputClassLimitReached); + this.setCheckClass(remaining, this.checkElement, remainingClassLimitReached); } - this.checkRemaining(remaining); - this.setCheckClass(remaining, this.input, inputClassLimitReached); - this.setCheckClass(remaining, this.checkElement, remainingClassLimitReached); - } + checkRemaining(remaining) { + let message; + if (this.countdown && remaining === 1) { + message = this.singularMessage; + } else if (remaining === -1) { + message = this.charLimitSingularMessage; + } else if (remaining < -1) { + message = this.charLimitPluralMessage; + } else { + message = this.pluralMessage; + } - checkRemaining(remaining) { - let message; - if (this.countdown && remaining === 1) { - message = this.singularMessage; - } else if (remaining === -1) { - message = this.charLimitSingularMessage; - } else if (remaining < -1) { - message = this.charLimitPluralMessage; - } else { - message = this.pluralMessage; - } + this.checkElement.innerText = message.replace('{x}', Math.abs(remaining)); - this.checkElement.innerText = message.replace('{x}', Math.abs(remaining)); + if (this.button) { + this.setButtonState(remaining); + } - if (this.button) { - this.setButtonState(remaining); + this.setShowMessage(remaining); } - this.setShowMessage(remaining); - } - - setButtonState(remaining) { - this.button.classList[remaining === 0 ? 'remove' : 'add']('ons-btn--disabled'); - this.button.disabled = remaining === 0 ? null : 'true'; - } + setButtonState(remaining) { + this.button.classList[remaining === 0 ? 'remove' : 'add']('ons-btn--disabled'); + this.button.disabled = remaining === 0 ? null : 'true'; + } - setShowMessage(remaining) { - this.checkElement.classList[(remaining < this.checkVal && remaining > 0 && this.countdown) || remaining < 0 ? 'remove' : 'add']( - 'ons-u-d-no', - ); - } + setShowMessage(remaining) { + this.checkElement.classList[(remaining < this.checkVal && remaining > 0 && this.countdown) || remaining < 0 ? 'remove' : 'add']( + 'ons-u-d-no', + ); + } - setCheckClass(remaining, element, setClass) { - element.classList[remaining < 0 ? 'add' : 'remove'](setClass); - this.checkElement.setAttribute('aria-live', [remaining > 0 ? 'polite' : 'assertive']); - } + setCheckClass(remaining, element, setClass) { + element.classList[remaining < 0 ? 'add' : 'remove'](setClass); + this.checkElement.setAttribute('aria-live', [remaining > 0 ? 'polite' : 'assertive']); + } } diff --git a/src/components/char-check-limit/character-check.spec.js b/src/components/char-check-limit/character-check.spec.js index 710fc7abdd..9985bbb87a 100644 --- a/src/components/char-check-limit/character-check.spec.js +++ b/src/components/char-check-limit/character-check.spec.js @@ -1,200 +1,200 @@ import { renderComponent, setTestPage } from '../../tests/helpers/rendering'; const EXAMPLE_INPUT_WITH_CHARACTER_CHECK = { - id: 'search-field', - variant: 'number', - width: '6', - label: { - text: 'Filter results', - }, - searchButton: { - text: 'Filter', - }, - charCheckLimit: { - charcheckCountdown: true, - limit: 11, - charCountOverLimitSingular: '{x} number too many', - charCountOverLimitPlural: '{x} numbers too many', - charCountSingular: 'You have {x} character remaining', - charCountPlural: 'You have {x} characters remaining', - }, + id: 'search-field', + variant: 'number', + width: '6', + label: { + text: 'Filter results', + }, + searchButton: { + text: 'Filter', + }, + charCheckLimit: { + charcheckCountdown: true, + limit: 11, + charCountOverLimitSingular: '{x} number too many', + charCountOverLimitPlural: '{x} numbers too many', + charCountSingular: 'You have {x} character remaining', + charCountPlural: 'You have {x} characters remaining', + }, }; const EXAMPLE_CHARACTER_CHECK_WITH_MUTUALLY_EXCLUSIVE = { - id: 'feedback', - name: 'feedback', - width: '30', - legend: 'Feedback legend', - label: { - text: 'Enter your feedback', - }, - charCheckLimit: { - limit: 200, - charCountSingular: 'You have {x} character remaining', - charCountPlural: 'You have {x} characters remaining', - }, - mutuallyExclusive: { - or: 'Or', - deselectMessage: 'Selecting this will clear your feedback', - deselectGroupAdjective: 'cleared', - deselectExclusiveOptionAdjective: 'deselected', - exclusiveOptions: [ - { - id: 'feedback-checkbox', - name: 'no-feedback', - value: 'no-feedback', - label: { - text: 'I dont want to provide feedback', - }, - }, - ], - }, + id: 'feedback', + name: 'feedback', + width: '30', + legend: 'Feedback legend', + label: { + text: 'Enter your feedback', + }, + charCheckLimit: { + limit: 200, + charCountSingular: 'You have {x} character remaining', + charCountPlural: 'You have {x} characters remaining', + }, + mutuallyExclusive: { + or: 'Or', + deselectMessage: 'Selecting this will clear your feedback', + deselectGroupAdjective: 'cleared', + deselectExclusiveOptionAdjective: 'deselected', + exclusiveOptions: [ + { + id: 'feedback-checkbox', + name: 'no-feedback', + value: 'no-feedback', + label: { + text: 'I dont want to provide feedback', + }, + }, + ], + }, }; describe('script: character-check', () => { - describe('mode: basic', () => { - beforeEach(async () => { - await setTestPage('/test', renderComponent('input', EXAMPLE_INPUT_WITH_CHARACTER_CHECK)); - }); - - describe('Given that the char check helper has initialised correctly', () => { - it('the char check readout should be invisible', async () => { - const hasClass = await page.$eval('#search-field-check', (element) => element.classList.contains('ons-u-d-no')); - expect(hasClass).toBe(true); - }); - - it('then the character limit readout should reflect the number of characters remaining', async () => { - const innerHtml = await page.$eval('#search-field-check', (element) => element.innerHTML); - expect(innerHtml.trim()).toBe('You have 11 characters remaining'); - }); - }); - - describe('Given that the user has not typed into the search input', () => { - describe('when the user types into the search input', () => { + describe('mode: basic', () => { beforeEach(async () => { - await page.type('#search-field', '1', { delay: 20 }); + await setTestPage('/test', renderComponent('input', EXAMPLE_INPUT_WITH_CHARACTER_CHECK)); }); - it('then the characters remaining readout reflect the number of characters remaining', async () => { - const innerHtml = await page.$eval('#search-field-check', (element) => element.innerHTML); - expect(innerHtml.trim()).toBe('You have 10 characters remaining'); - }); + describe('Given that the char check helper has initialised correctly', () => { + it('the char check readout should be invisible', async () => { + const hasClass = await page.$eval('#search-field-check', (element) => element.classList.contains('ons-u-d-no')); + expect(hasClass).toBe(true); + }); - it('the char check readout should be visible', async () => { - const hasClass = await page.$eval('#search-field-check', (element) => element.classList.contains('ons-u-d-no')); - expect(hasClass).toBe(false); + it('then the character limit readout should reflect the number of characters remaining', async () => { + const innerHtml = await page.$eval('#search-field-check', (element) => element.innerHTML); + expect(innerHtml.trim()).toBe('You have 11 characters remaining'); + }); }); - it('then aria-live should be set to polite', async () => { - const ariaLiveAttribute = await page.$eval('#search-field-check', (element) => element.getAttribute('aria-live')); - expect(ariaLiveAttribute).toBe('polite'); + describe('Given that the user has not typed into the search input', () => { + describe('when the user types into the search input', () => { + beforeEach(async () => { + await page.type('#search-field', '1', { delay: 20 }); + }); + + it('then the characters remaining readout reflect the number of characters remaining', async () => { + const innerHtml = await page.$eval('#search-field-check', (element) => element.innerHTML); + expect(innerHtml.trim()).toBe('You have 10 characters remaining'); + }); + + it('the char check readout should be visible', async () => { + const hasClass = await page.$eval('#search-field-check', (element) => element.classList.contains('ons-u-d-no')); + expect(hasClass).toBe(false); + }); + + it('then aria-live should be set to polite', async () => { + const ariaLiveAttribute = await page.$eval('#search-field-check', (element) => element.getAttribute('aria-live')); + expect(ariaLiveAttribute).toBe('polite'); + }); + }); + + describe('when the user reaches the charcheck limit of the input', () => { + beforeEach(async () => { + await page.type('#search-field', '11111111111', { delay: 20 }); + }); + + it('the char check readout should be invisible', async () => { + const hasClass = await page.$eval('#search-field-check', (element) => element.classList.contains('ons-u-d-no')); + expect(hasClass).toBe(true); + }); + }); + + describe('when the user has 1 character remaining before the limit is reached', () => { + beforeEach(async () => { + await page.type('#search-field', '1111111111', { delay: 20 }); + }); + + it('then the characters remaining readout reflect the number of characters remaining', async () => { + const innerHtml = await page.$eval('#search-field-check', (element) => element.innerHTML); + expect(innerHtml.trim()).toBe('You have 1 character remaining'); + }); + }); }); - }); - describe('when the user reaches the charcheck limit of the input', () => { - beforeEach(async () => { - await page.type('#search-field', '11111111111', { delay: 20 }); + describe('Given that the user has exceeded the charcheck limit of the input by 1', () => { + beforeEach(async () => { + await page.type('#search-field', '111111111111', { delay: 20 }); + }); + + it('the char check readout should be visible', async () => { + const hasClass = await page.$eval('#search-field-check', (element) => element.classList.contains('ons-u-d-no')); + expect(hasClass).toBe(false); + }); + + it('then the characters remaining readout reflect the number of characters exceeded', async () => { + const innerHtml = await page.$eval('#search-field-check', (element) => element.innerHTML); + expect(innerHtml.trim()).toBe('1 number too many'); + }); + + it('then aria-live should be set to assertive', async () => { + const ariaLiveAttribute = await page.$eval('#search-field-check', (element) => element.getAttribute('aria-live')); + expect(ariaLiveAttribute).toBe('assertive'); + }); + + it('then the input and readout should be given limit reached classes', async () => { + const hasClassOnSearchInput = await page.$eval('#search-field', (element) => + element.classList.contains('ons-input--limit-reached'), + ); + expect(hasClassOnSearchInput).toBe(true); + const hasClassOnLimitReadout = await page.$eval('#search-field-check', (element) => + element.classList.contains('ons-input__limit--reached'), + ); + expect(hasClassOnLimitReadout).toBe(true); + }); }); - it('the char check readout should be invisible', async () => { - const hasClass = await page.$eval('#search-field-check', (element) => element.classList.contains('ons-u-d-no')); - expect(hasClass).toBe(true); + describe('Given that the user has exceeded the charcheck limit of the input by more than 1', () => { + beforeEach(async () => { + await page.type('#search-field', '1111111111111', { delay: 20 }); + }); + + it('the char check readout should be visible', async () => { + const hasClass = await page.$eval('#search-field-check', (element) => element.classList.contains('ons-u-d-no')); + expect(hasClass).toBe(false); + }); + + it('then the characters remaining readout reflect the number of characters exceeded', async () => { + const innerHtml = await page.$eval('#search-field-check', (element) => element.innerHTML); + expect(innerHtml.trim()).toBe('2 numbers too many'); + }); + + it('then aria-live should be set to assertive', async () => { + const ariaLiveAttribute = await page.$eval('#search-field-check', (element) => element.getAttribute('aria-live')); + expect(ariaLiveAttribute).toBe('assertive'); + }); + + it('then the input and readout should be given limit reached classes', async () => { + const hasClassOnSearchInput = await page.$eval('#search-field', (element) => + element.classList.contains('ons-input--limit-reached'), + ); + expect(hasClassOnSearchInput).toBe(true); + const hasClassOnLimitReadout = await page.$eval('#search-field-check', (element) => + element.classList.contains('ons-input__limit--reached'), + ); + expect(hasClassOnLimitReadout).toBe(true); + }); }); - }); + }); - describe('when the user has 1 character remaining before the limit is reached', () => { + describe('mode: mutually exclusive', () => { beforeEach(async () => { - await page.type('#search-field', '1111111111', { delay: 20 }); + await setTestPage('/test', renderComponent('textarea', EXAMPLE_CHARACTER_CHECK_WITH_MUTUALLY_EXCLUSIVE)); }); - it('then the characters remaining readout reflect the number of characters remaining', async () => { - const innerHtml = await page.$eval('#search-field-check', (element) => element.innerHTML); - expect(innerHtml.trim()).toBe('You have 1 character remaining'); + describe('Given that the input value is cleared programatically via mutually exclusive option', () => { + beforeEach(async () => { + await page.type('#feedback', '1', { delay: 20 }); + await page.focus('#feedback-checkbox'); + await page.keyboard.press('Space'); + }); + + it('then aria-live attribute should removed', async () => { + const hasAriaLiveAttribute = await page.$eval('#feedback-lim', (element) => element.hasAttribute('aria-live')); + expect(hasAriaLiveAttribute).toBe(false); + }); }); - }); - }); - - describe('Given that the user has exceeded the charcheck limit of the input by 1', () => { - beforeEach(async () => { - await page.type('#search-field', '111111111111', { delay: 20 }); - }); - - it('the char check readout should be visible', async () => { - const hasClass = await page.$eval('#search-field-check', (element) => element.classList.contains('ons-u-d-no')); - expect(hasClass).toBe(false); - }); - - it('then the characters remaining readout reflect the number of characters exceeded', async () => { - const innerHtml = await page.$eval('#search-field-check', (element) => element.innerHTML); - expect(innerHtml.trim()).toBe('1 number too many'); - }); - - it('then aria-live should be set to assertive', async () => { - const ariaLiveAttribute = await page.$eval('#search-field-check', (element) => element.getAttribute('aria-live')); - expect(ariaLiveAttribute).toBe('assertive'); - }); - - it('then the input and readout should be given limit reached classes', async () => { - const hasClassOnSearchInput = await page.$eval('#search-field', (element) => - element.classList.contains('ons-input--limit-reached'), - ); - expect(hasClassOnSearchInput).toBe(true); - const hasClassOnLimitReadout = await page.$eval('#search-field-check', (element) => - element.classList.contains('ons-input__limit--reached'), - ); - expect(hasClassOnLimitReadout).toBe(true); - }); - }); - - describe('Given that the user has exceeded the charcheck limit of the input by more than 1', () => { - beforeEach(async () => { - await page.type('#search-field', '1111111111111', { delay: 20 }); - }); - - it('the char check readout should be visible', async () => { - const hasClass = await page.$eval('#search-field-check', (element) => element.classList.contains('ons-u-d-no')); - expect(hasClass).toBe(false); - }); - - it('then the characters remaining readout reflect the number of characters exceeded', async () => { - const innerHtml = await page.$eval('#search-field-check', (element) => element.innerHTML); - expect(innerHtml.trim()).toBe('2 numbers too many'); - }); - - it('then aria-live should be set to assertive', async () => { - const ariaLiveAttribute = await page.$eval('#search-field-check', (element) => element.getAttribute('aria-live')); - expect(ariaLiveAttribute).toBe('assertive'); - }); - - it('then the input and readout should be given limit reached classes', async () => { - const hasClassOnSearchInput = await page.$eval('#search-field', (element) => - element.classList.contains('ons-input--limit-reached'), - ); - expect(hasClassOnSearchInput).toBe(true); - const hasClassOnLimitReadout = await page.$eval('#search-field-check', (element) => - element.classList.contains('ons-input__limit--reached'), - ); - expect(hasClassOnLimitReadout).toBe(true); - }); - }); - }); - - describe('mode: mutually exclusive', () => { - beforeEach(async () => { - await setTestPage('/test', renderComponent('textarea', EXAMPLE_CHARACTER_CHECK_WITH_MUTUALLY_EXCLUSIVE)); - }); - - describe('Given that the input value is cleared programatically via mutually exclusive option', () => { - beforeEach(async () => { - await page.type('#feedback', '1', { delay: 20 }); - await page.focus('#feedback-checkbox'); - await page.keyboard.press('Space'); - }); - - it('then aria-live attribute should removed', async () => { - const hasAriaLiveAttribute = await page.$eval('#feedback-lim', (element) => element.hasAttribute('aria-live')); - expect(hasAriaLiveAttribute).toBe(false); - }); }); - }); }); diff --git a/src/components/char-check-limit/character-limit.js b/src/components/char-check-limit/character-limit.js index 504def1bfd..b6fce84fbd 100644 --- a/src/components/char-check-limit/character-limit.js +++ b/src/components/char-check-limit/character-limit.js @@ -5,51 +5,51 @@ const remainingClassLimitReached = 'ons-input__limit--reached'; const attrCharLimitRef = 'data-char-limit-ref'; export default class CharLimit { - constructor(input) { - this.input = input; - this.maxLength = input.maxLength; - this.limitElement = document.getElementById(input.getAttribute(attrCharLimitRef)); - this.singularMessage = this.limitElement.getAttribute('data-charcount-singular'); - this.pluralMessage = this.limitElement.getAttribute('data-charcount-plural'); - - this.updateLimitReadout(null, true); - this.limitElement.classList.remove('ons-u-d-no'); - - input.addEventListener('input', this.updateLimitReadout.bind(this)); - } - - updateLimitReadout(event, firstRun) { - const value = this.input.value; - const remaining = this.maxLength - value.length; - const message = remaining === 1 ? this.singularMessage : this.pluralMessage; - // Prevent aria live announcement when component initialises - if (!firstRun && event.inputType) { - this.limitElement.setAttribute('aria-live', 'polite'); - this.limitElement.setAttribute('aria-live', [remaining > 0 ? 'polite' : 'assertive']); - } else { - this.limitElement.removeAttribute('aria-live'); + constructor(input) { + this.input = input; + this.maxLength = input.maxLength; + this.limitElement = document.getElementById(input.getAttribute(attrCharLimitRef)); + this.singularMessage = this.limitElement.getAttribute('data-charcount-singular'); + this.pluralMessage = this.limitElement.getAttribute('data-charcount-plural'); + + this.updateLimitReadout(null, true); + this.limitElement.classList.remove('ons-u-d-no'); + + input.addEventListener('input', this.updateLimitReadout.bind(this)); } - this.limitElement.innerText = message.replace('{x}', remaining); + updateLimitReadout(event, firstRun) { + const value = this.input.value; + const remaining = this.maxLength - value.length; + const message = remaining === 1 ? this.singularMessage : this.pluralMessage; + // Prevent aria live announcement when component initialises + if (!firstRun && event.inputType) { + this.limitElement.setAttribute('aria-live', 'polite'); + this.limitElement.setAttribute('aria-live', [remaining > 0 ? 'polite' : 'assertive']); + } else { + this.limitElement.removeAttribute('aria-live'); + } - this.setLimitClass(remaining, this.input, inputClassLimitReached); - this.setLimitClass(remaining, this.limitElement, remainingClassLimitReached); + this.limitElement.innerText = message.replace('{x}', remaining); - this.track(remaining); - } + this.setLimitClass(remaining, this.input, inputClassLimitReached); + this.setLimitClass(remaining, this.limitElement, remainingClassLimitReached); - setLimitClass(remaining, element, limitClass) { - element.classList[remaining > 0 ? 'remove' : 'add'](limitClass); - } + this.track(remaining); + } + + setLimitClass(remaining, element, limitClass) { + element.classList[remaining > 0 ? 'remove' : 'add'](limitClass); + } - track(remaining) { - if (remaining < 1) { - trackEvent({ - event_type: 'event', - event_category: 'Error', - event_action: 'Textarea limit reached', - event_label: `Limit of ${this.maxLength} reached/exceeded`, - }); + track(remaining) { + if (remaining < 1) { + trackEvent({ + event_type: 'event', + event_category: 'Error', + event_action: 'Textarea limit reached', + event_label: `Limit of ${this.maxLength} reached/exceeded`, + }); + } } - } } diff --git a/src/components/checkboxes/_checkbox-macro.spec.js b/src/components/checkboxes/_checkbox-macro.spec.js index ce1402896f..e39ba25bb5 100644 --- a/src/components/checkboxes/_checkbox-macro.spec.js +++ b/src/components/checkboxes/_checkbox-macro.spec.js @@ -6,412 +6,412 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_CHECKBOX = { - id: 'example-checkbox-id', - name: 'example-checkbox-name', - value: '123', - label: { - classes: 'extra-label-class', - text: 'Example checkbox', - description: 'Example label description.', - }, + id: 'example-checkbox-id', + name: 'example-checkbox-name', + value: '123', + label: { + classes: 'extra-label-class', + text: 'Example checkbox', + description: 'Example label description.', + }, }; const EXAMPLE_CHECKBOXES_ITEM_INPUT = { - value: 'input', - other: { - id: 'example-text-input', - name: 'example-text-input-name', - type: 'text', - label: { - text: 'Enter your own answer', + value: 'input', + other: { + id: 'example-text-input', + name: 'example-text-input-name', + type: 'text', + label: { + text: 'Enter your own answer', + }, + classes: 'extra-textbox-class', + width: 42, + value: '42', + attributes: { a: 42 }, }, - classes: 'extra-textbox-class', - width: 42, - value: '42', - attributes: { a: 42 }, - }, }; const EXAMPLE_CHECKBOXES_ITEM_SELECT = { - value: 'select', - other: { - otherType: 'select', - id: 'example-select', - name: 'example-select-name', - label: { - text: 'Enter your own answer', + value: 'select', + other: { + otherType: 'select', + id: 'example-select', + name: 'example-select-name', + label: { + text: 'Enter your own answer', + }, + classes: 'extra-select-class', + options: [ + { text: 'First', value: '1' }, + { text: 'Second', value: '2' }, + ], + value: '1', }, - classes: 'extra-select-class', - options: [ - { text: 'First', value: '1' }, - { text: 'Second', value: '2' }, - ], - value: '1', - }, }; const EXAMPLE_CHECKBOXES_ITEM_CHECKBOXES = { - value: 'checkboxes', - other: { - otherType: 'checkboxes', - selectAllChildren: true, - id: 'example-checkboxes', - name: 'example-checkboxes-name', - legend: 'Select preferred times of day', - legendClasses: 'extra-legend-class', - attributes: { a: 42 }, - checkboxes: [ - { - value: 'morning', - id: 'morning', - label: { - text: 'Morning', - }, - }, - { - value: 'afternoon', - id: 'afternoon', - label: { - text: 'Afternoon', + value: 'checkboxes', + other: { + otherType: 'checkboxes', + selectAllChildren: true, + id: 'example-checkboxes', + name: 'example-checkboxes-name', + legend: 'Select preferred times of day', + legendClasses: 'extra-legend-class', + attributes: { a: 42 }, + checkboxes: [ + { + value: 'morning', + id: 'morning', + label: { + text: 'Morning', + }, + }, + { + value: 'afternoon', + id: 'afternoon', + label: { + text: 'Afternoon', + }, + }, + ], + autoSelect: { + selectAllText: 'Select all', + unselectAllText: 'Unselect all', + context: 'checkboxes', }, - }, - ], - autoSelect: { - selectAllText: 'Select all', - unselectAllText: 'Unselect all', - context: 'checkboxes', }, - }, }; const EXAMPLE_CHECKBOXES_ITEM_RADIOS = { - value: 'radios', - other: { - otherType: 'radios', - id: 'example-radios', - name: 'example-radios-name', - legend: 'Select preferred times of day', - legendClasses: 'extra-legend-class', - attributes: { a: 42 }, - radios: [EXAMPLE_CHECKBOX], - }, + value: 'radios', + other: { + otherType: 'radios', + id: 'example-radios', + name: 'example-radios-name', + legend: 'Select preferred times of day', + legendClasses: 'extra-legend-class', + attributes: { a: 42 }, + radios: [EXAMPLE_CHECKBOX], + }, }; describe('macro: checkboxes/checkbox', () => { - it('passes jest-axe checks without check', async () => { - const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has additionally provided `classes`', () => { - const $ = cheerio.load( - renderComponent('checkboxes/checkbox', { - ...EXAMPLE_CHECKBOX, - classes: 'extra-class another-extra-class', - }), - ); - - expect($('.ons-checkbox').hasClass('extra-class')).toBe(true); - expect($('.ons-checkbox').hasClass('another-extra-class')).toBe(true); - }); - - it('does not have `no-label` modifier class', () => { - const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); - - expect($('.ons-checkbox').hasClass('ons-checkbox--no-label')).toBe(false); - }); - - it('has `no-label` modifier class when `hideLabel` is `true`', () => { - const $ = cheerio.load( - renderComponent('checkboxes/checkbox', { - ...EXAMPLE_CHECKBOX, - hideLabel: true, - }), - ); - - expect($('.ons-checkbox').hasClass('ons-checkbox--no-label')).toBe(true); - }); - - describe('input element', () => { - it('renders `input` element of type "checkbox"', () => { - const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); - - expect($('.ons-checkbox__input').is('input')).toBe(true); - expect($('.ons-checkbox__input').attr('type')).toBe('checkbox'); - }); - - it('has the provided `id` attribute', () => { - const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); + it('passes jest-axe checks without check', async () => { + const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); - expect($('.ons-checkbox__input').attr('id')).toBe('example-checkbox-id'); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); }); - it('has the provided `name` attribute', () => { - const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); + it('has additionally provided `classes`', () => { + const $ = cheerio.load( + renderComponent('checkboxes/checkbox', { + ...EXAMPLE_CHECKBOX, + classes: 'extra-class another-extra-class', + }), + ); - expect($('.ons-checkbox__input').attr('name')).toBe('example-checkbox-name'); + expect($('.ons-checkbox').hasClass('extra-class')).toBe(true); + expect($('.ons-checkbox').hasClass('another-extra-class')).toBe(true); }); - it('does not have the `disabled` attribute when not disabled', () => { - const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); + it('does not have `no-label` modifier class', () => { + const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); - expect($('.ons-checkbox__input').attr('disabled')).toBeUndefined(); - expect($('.ons-checkbox__input').attr('aria-disabled')).toBeUndefined(); + expect($('.ons-checkbox').hasClass('ons-checkbox--no-label')).toBe(false); }); - it('has the `disabled` attribute when disabled', () => { - const $ = cheerio.load( - renderComponent('checkboxes/checkbox', { - ...EXAMPLE_CHECKBOX, - disabled: true, - }), - ); + it('has `no-label` modifier class when `hideLabel` is `true`', () => { + const $ = cheerio.load( + renderComponent('checkboxes/checkbox', { + ...EXAMPLE_CHECKBOX, + hideLabel: true, + }), + ); - expect($('.ons-checkbox__input').attr('disabled')).toBe('disabled'); - expect($('.ons-checkbox__input').attr('aria-disabled')).toBe('true'); + expect($('.ons-checkbox').hasClass('ons-checkbox--no-label')).toBe(true); }); - it('does not have the `checked` attribute when not checked', () => { - const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); - - expect($('.ons-checkbox__input').attr('checked')).toBeUndefined(); + describe('input element', () => { + it('renders `input` element of type "checkbox"', () => { + const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); + + expect($('.ons-checkbox__input').is('input')).toBe(true); + expect($('.ons-checkbox__input').attr('type')).toBe('checkbox'); + }); + + it('has the provided `id` attribute', () => { + const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); + + expect($('.ons-checkbox__input').attr('id')).toBe('example-checkbox-id'); + }); + + it('has the provided `name` attribute', () => { + const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); + + expect($('.ons-checkbox__input').attr('name')).toBe('example-checkbox-name'); + }); + + it('does not have the `disabled` attribute when not disabled', () => { + const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); + + expect($('.ons-checkbox__input').attr('disabled')).toBeUndefined(); + expect($('.ons-checkbox__input').attr('aria-disabled')).toBeUndefined(); + }); + + it('has the `disabled` attribute when disabled', () => { + const $ = cheerio.load( + renderComponent('checkboxes/checkbox', { + ...EXAMPLE_CHECKBOX, + disabled: true, + }), + ); + + expect($('.ons-checkbox__input').attr('disabled')).toBe('disabled'); + expect($('.ons-checkbox__input').attr('aria-disabled')).toBe('true'); + }); + + it('does not have the `checked` attribute when not checked', () => { + const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); + + expect($('.ons-checkbox__input').attr('checked')).toBeUndefined(); + }); + + it('has the `checked` attribute when checked', () => { + const $ = cheerio.load( + renderComponent('checkboxes/checkbox', { + ...EXAMPLE_CHECKBOX, + checked: true, + }), + ); + + expect($('.ons-checkbox__input').attr('checked')).toBe('checked'); + }); + + it('has additionally provided `inputClasses`', () => { + const $ = cheerio.load( + renderComponent('checkboxes/checkbox', { + ...EXAMPLE_CHECKBOX, + inputClasses: 'extra-input-class another-extra-input-class', + }), + ); + + expect($('.ons-checkbox__input').hasClass('extra-input-class')).toBe(true); + expect($('.ons-checkbox__input').hasClass('another-extra-input-class')).toBe(true); + }); + + it('does not associate with "other" input when there is none', () => { + const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); + + expect($('.ons-checkbox__input').attr('aria-controls')).toBeUndefined(); + expect($('.ons-checkbox__input').attr('aria-haspopup')).toBeUndefined(); + }); + + it('does not associate with "other" input when marked `open`', () => { + const $ = cheerio.load( + renderComponent('checkboxes/checkbox', { + ...EXAMPLE_CHECKBOX, + other: { + open: true, + id: 'other-input-id', + label: { + text: 'Other input', + }, + }, + }), + ); + + expect($('.ons-checkbox__input').attr('aria-controls')).toBeUndefined(); + expect($('.ons-checkbox__input').attr('aria-haspopup')).toBeUndefined(); + }); + + it('associates with "other" input when not marked `open`', () => { + const $ = cheerio.load( + renderComponent('checkboxes/checkbox', { + ...EXAMPLE_CHECKBOX, + other: { + id: 'other-input-id', + label: { + text: 'Other input', + }, + }, + }), + ); + + expect($('.ons-checkbox__input').attr('aria-controls')).toBe('example-checkbox-id-other-wrap'); + expect($('.ons-checkbox__input').attr('aria-haspopup')).toBe('true'); + }); + + it('has additionally provided attributes', () => { + const $ = cheerio.load( + renderComponent('checkboxes/checkbox', { + ...EXAMPLE_CHECKBOX, + attributes: { a: '123', b: '456' }, + }), + ); + + expect($('.ons-checkbox__input').attr('a')).toBe('123'); + expect($('.ons-checkbox__input').attr('b')).toBe('456'); + }); + + it('does not have `data-deselect-message` when `deselectMessage` is not provided', () => { + const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); + + expect($('.ons-checkbox__input').attr('data-deselect-message')).toBeUndefined(); + }); + + it('has `data-deselect-message` when `deselectMessage` is provided', () => { + const $ = cheerio.load( + renderComponent('checkboxes/checkbox', { + ...EXAMPLE_CHECKBOX, + deselectMessage: 'Selecting this will clear your feedback', + }), + ); + + expect($('.ons-checkbox__input').attr('data-deselect-message')).toBe('Selecting this will clear your feedback'); + }); }); - it('has the `checked` attribute when checked', () => { - const $ = cheerio.load( - renderComponent('checkboxes/checkbox', { - ...EXAMPLE_CHECKBOX, - checked: true, - }), - ); - - expect($('.ons-checkbox__input').attr('checked')).toBe('checked'); + describe('label element', () => { + it('renders label using `label` component', () => { + const faker = templateFaker(); + const labelSpy = faker.spy('label'); + + faker.renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX); + + expect(labelSpy.occurrences).toContainEqual({ + id: 'example-checkbox-id-label', + for: 'example-checkbox-id', + inputType: 'checkbox', + text: 'Example checkbox', + classes: 'ons-checkbox__label extra-label-class', + description: 'Example label description.', + }); + }); }); - it('has additionally provided `inputClasses`', () => { - const $ = cheerio.load( - renderComponent('checkboxes/checkbox', { - ...EXAMPLE_CHECKBOX, - inputClasses: 'extra-input-class another-extra-input-class', - }), - ); + it('wraps `other` component without a class indicating that it is open', () => { + const $ = cheerio.load( + renderComponent('checkboxes/checkbox', { + ...EXAMPLE_CHECKBOX, + ...EXAMPLE_CHECKBOXES_ITEM_INPUT, + }), + ); - expect($('.ons-checkbox__input').hasClass('extra-input-class')).toBe(true); - expect($('.ons-checkbox__input').hasClass('another-extra-input-class')).toBe(true); + expect($('.ons-checkbox__other').hasClass('ons-checkbox__other--open')).toBe(false); + expect($('.ons-checkbox__other').attr('id')).toBe('example-checkbox-id-other-wrap'); }); - it('does not associate with "other" input when there is none', () => { - const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); - - expect($('.ons-checkbox__input').attr('aria-controls')).toBeUndefined(); - expect($('.ons-checkbox__input').attr('aria-haspopup')).toBeUndefined(); + it('wraps `other` component with class indicating that it is open', () => { + const $ = cheerio.load( + renderComponent('checkboxes/checkbox', { + ...EXAMPLE_CHECKBOX, + ...EXAMPLE_CHECKBOXES_ITEM_INPUT, + other: { + ...EXAMPLE_CHECKBOXES_ITEM_INPUT.other, + open: true, + }, + }), + ); + + expect($('.ons-checkbox__other').hasClass('ons-checkbox__other--open')).toBe(true); + expect($('.ons-checkbox__other').attr('id')).toBe('example-checkbox-id-other-wrap'); }); - it('does not associate with "other" input when marked `open`', () => { - const $ = cheerio.load( - renderComponent('checkboxes/checkbox', { - ...EXAMPLE_CHECKBOX, - other: { - open: true, - id: 'other-input-id', - label: { - text: 'Other input', - }, - }, - }), - ); + it('renders other "input" component for item', () => { + const faker = templateFaker(); + const inputSpy = faker.spy('input'); - expect($('.ons-checkbox__input').attr('aria-controls')).toBeUndefined(); - expect($('.ons-checkbox__input').attr('aria-haspopup')).toBeUndefined(); - }); + faker.renderComponent('checkboxes/checkbox', { + ...EXAMPLE_CHECKBOX, + ...EXAMPLE_CHECKBOXES_ITEM_INPUT, + }); - it('associates with "other" input when not marked `open`', () => { - const $ = cheerio.load( - renderComponent('checkboxes/checkbox', { - ...EXAMPLE_CHECKBOX, - other: { - id: 'other-input-id', + expect(inputSpy.occurrences).toContainEqual({ + id: 'example-text-input', + name: 'example-text-input-name', + type: 'text', label: { - text: 'Other input', + id: 'example-text-input-label', + text: 'Enter your own answer', + classes: 'ons-u-fw-n', }, - }, - }), - ); - - expect($('.ons-checkbox__input').attr('aria-controls')).toBe('example-checkbox-id-other-wrap'); - expect($('.ons-checkbox__input').attr('aria-haspopup')).toBe('true'); - }); - - it('has additionally provided attributes', () => { - const $ = cheerio.load( - renderComponent('checkboxes/checkbox', { - ...EXAMPLE_CHECKBOX, - attributes: { a: '123', b: '456' }, - }), - ); - - expect($('.ons-checkbox__input').attr('a')).toBe('123'); - expect($('.ons-checkbox__input').attr('b')).toBe('456'); + classes: 'extra-textbox-class', + width: 42, + attributes: EXAMPLE_CHECKBOXES_ITEM_INPUT.other.attributes, + dontWrap: true, + value: '42', + }); }); - it('does not have `data-deselect-message` when `deselectMessage` is not provided', () => { - const $ = cheerio.load(renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX)); + it('renders other "select" component for item', () => { + const faker = templateFaker(); + const selectSpy = faker.spy('select'); - expect($('.ons-checkbox__input').attr('data-deselect-message')).toBeUndefined(); - }); - - it('has `data-deselect-message` when `deselectMessage` is provided', () => { - const $ = cheerio.load( - renderComponent('checkboxes/checkbox', { - ...EXAMPLE_CHECKBOX, - deselectMessage: 'Selecting this will clear your feedback', - }), - ); - - expect($('.ons-checkbox__input').attr('data-deselect-message')).toBe('Selecting this will clear your feedback'); - }); - }); - - describe('label element', () => { - it('renders label using `label` component', () => { - const faker = templateFaker(); - const labelSpy = faker.spy('label'); - - faker.renderComponent('checkboxes/checkbox', EXAMPLE_CHECKBOX); - - expect(labelSpy.occurrences).toContainEqual({ - id: 'example-checkbox-id-label', - for: 'example-checkbox-id', - inputType: 'checkbox', - text: 'Example checkbox', - classes: 'ons-checkbox__label extra-label-class', - description: 'Example label description.', - }); - }); - }); - - it('wraps `other` component without a class indicating that it is open', () => { - const $ = cheerio.load( - renderComponent('checkboxes/checkbox', { - ...EXAMPLE_CHECKBOX, - ...EXAMPLE_CHECKBOXES_ITEM_INPUT, - }), - ); - - expect($('.ons-checkbox__other').hasClass('ons-checkbox__other--open')).toBe(false); - expect($('.ons-checkbox__other').attr('id')).toBe('example-checkbox-id-other-wrap'); - }); - - it('wraps `other` component with class indicating that it is open', () => { - const $ = cheerio.load( - renderComponent('checkboxes/checkbox', { - ...EXAMPLE_CHECKBOX, - ...EXAMPLE_CHECKBOXES_ITEM_INPUT, - other: { - ...EXAMPLE_CHECKBOXES_ITEM_INPUT.other, - open: true, - }, - }), - ); - - expect($('.ons-checkbox__other').hasClass('ons-checkbox__other--open')).toBe(true); - expect($('.ons-checkbox__other').attr('id')).toBe('example-checkbox-id-other-wrap'); - }); - - it('renders other "input" component for item', () => { - const faker = templateFaker(); - const inputSpy = faker.spy('input'); - - faker.renderComponent('checkboxes/checkbox', { - ...EXAMPLE_CHECKBOX, - ...EXAMPLE_CHECKBOXES_ITEM_INPUT, - }); - - expect(inputSpy.occurrences).toContainEqual({ - id: 'example-text-input', - name: 'example-text-input-name', - type: 'text', - label: { - id: 'example-text-input-label', - text: 'Enter your own answer', - classes: 'ons-u-fw-n', - }, - classes: 'extra-textbox-class', - width: 42, - attributes: EXAMPLE_CHECKBOXES_ITEM_INPUT.other.attributes, - dontWrap: true, - value: '42', - }); - }); - - it('renders other "select" component for item', () => { - const faker = templateFaker(); - const selectSpy = faker.spy('select'); - - faker.renderComponent('checkboxes/checkbox', { - ...EXAMPLE_CHECKBOX, - ...EXAMPLE_CHECKBOXES_ITEM_SELECT, - }); - - expect(selectSpy.occurrences).toContainEqual({ - id: 'example-select', - name: 'example-select-name', - label: { - id: 'example-select-label', - text: 'Enter your own answer', - classes: 'ons-u-fw-n', - }, - classes: 'extra-select-class', - dontWrap: true, - options: EXAMPLE_CHECKBOXES_ITEM_SELECT.other.options, - value: '1', - }); - }); - - it('renders other "checkboxes" component for item', () => { - const faker = templateFaker(); - const checkboxesSpy = faker.spy('checkboxes'); - - faker.renderComponent('checkboxes/checkbox', { - ...EXAMPLE_CHECKBOX, - ...EXAMPLE_CHECKBOXES_ITEM_CHECKBOXES, - }); + faker.renderComponent('checkboxes/checkbox', { + ...EXAMPLE_CHECKBOX, + ...EXAMPLE_CHECKBOXES_ITEM_SELECT, + }); - expect(checkboxesSpy.occurrences).toContainEqual({ - id: 'example-checkboxes', - name: 'example-checkboxes-name', - checked: undefined, - borderlessParent: false, - borderless: true, - legend: 'Select preferred times of day', - legendClasses: 'extra-legend-class', - attributes: { a: 42 }, - classes: 'ons-js-other-fieldset-checkbox', - checkboxes: EXAMPLE_CHECKBOXES_ITEM_CHECKBOXES.other.checkboxes, - autoSelect: EXAMPLE_CHECKBOXES_ITEM_CHECKBOXES.other.autoSelect, + expect(selectSpy.occurrences).toContainEqual({ + id: 'example-select', + name: 'example-select-name', + label: { + id: 'example-select-label', + text: 'Enter your own answer', + classes: 'ons-u-fw-n', + }, + classes: 'extra-select-class', + dontWrap: true, + options: EXAMPLE_CHECKBOXES_ITEM_SELECT.other.options, + value: '1', + }); }); - }); - - it('renders other "radios" component for item', () => { - const faker = templateFaker(); - const radiosSpy = faker.spy('radios'); - faker.renderComponent('checkboxes/checkbox', { - ...EXAMPLE_CHECKBOX, - ...EXAMPLE_CHECKBOXES_ITEM_RADIOS, + it('renders other "checkboxes" component for item', () => { + const faker = templateFaker(); + const checkboxesSpy = faker.spy('checkboxes'); + + faker.renderComponent('checkboxes/checkbox', { + ...EXAMPLE_CHECKBOX, + ...EXAMPLE_CHECKBOXES_ITEM_CHECKBOXES, + }); + + expect(checkboxesSpy.occurrences).toContainEqual({ + id: 'example-checkboxes', + name: 'example-checkboxes-name', + checked: undefined, + borderlessParent: false, + borderless: true, + legend: 'Select preferred times of day', + legendClasses: 'extra-legend-class', + attributes: { a: 42 }, + classes: 'ons-js-other-fieldset-checkbox', + checkboxes: EXAMPLE_CHECKBOXES_ITEM_CHECKBOXES.other.checkboxes, + autoSelect: EXAMPLE_CHECKBOXES_ITEM_CHECKBOXES.other.autoSelect, + }); }); - expect(radiosSpy.occurrences).toContainEqual({ - id: 'example-radios', - name: 'example-radios-name', - borderless: true, - legend: 'Select preferred times of day', - legendClasses: 'extra-legend-class', - attributes: { a: 42 }, - classes: 'ons-js-other-fieldset-checkbox', - radios: EXAMPLE_CHECKBOXES_ITEM_RADIOS.other.radios, + it('renders other "radios" component for item', () => { + const faker = templateFaker(); + const radiosSpy = faker.spy('radios'); + + faker.renderComponent('checkboxes/checkbox', { + ...EXAMPLE_CHECKBOX, + ...EXAMPLE_CHECKBOXES_ITEM_RADIOS, + }); + + expect(radiosSpy.occurrences).toContainEqual({ + id: 'example-radios', + name: 'example-radios-name', + borderless: true, + legend: 'Select preferred times of day', + legendClasses: 'extra-legend-class', + attributes: { a: 42 }, + classes: 'ons-js-other-fieldset-checkbox', + radios: EXAMPLE_CHECKBOXES_ITEM_RADIOS.other.radios, + }); }); - }); }); diff --git a/src/components/checkboxes/_checkbox.scss b/src/components/checkboxes/_checkbox.scss index c8364fdd46..7482380a48 100644 --- a/src/components/checkboxes/_checkbox.scss +++ b/src/components/checkboxes/_checkbox.scss @@ -2,224 +2,224 @@ $checkbox-input-width: 22px; $checkbox-padding: 11px; .ons-checkbox { - display: inline-block; - position: relative; - width: 100%; - z-index: 1; - - &__input { - appearance: none; - background-color: var(--ons-color-input-bg); - border: 2px solid var(--ons-color-input-border); - border-radius: 0.2rem; - box-sizing: border-box; - cursor: pointer; - height: $checkbox-input-width; - left: $checkbox-padding; - position: absolute; - top: $checkbox-padding + 3px; - width: $checkbox-input-width; + display: inline-block; + position: relative; + width: 100%; z-index: 1; - // Check icon - &::after { - border: solid var(--ons-color-input-border); - border-top-color: var(--ons-color-input-bg); - border-width: 0 0 3px 3px; - box-sizing: border-box; - content: ''; - height: 7px; - left: 2px; - opacity: 0; - position: absolute; - top: 4px; - transform: rotate(-45deg); - width: 14px; - } + &__input { + appearance: none; + background-color: var(--ons-color-input-bg); + border: 2px solid var(--ons-color-input-border); + border-radius: 0.2rem; + box-sizing: border-box; + cursor: pointer; + height: $checkbox-input-width; + left: $checkbox-padding; + position: absolute; + top: $checkbox-padding + 3px; + width: $checkbox-input-width; + z-index: 1; + + // Check icon + &::after { + border: solid var(--ons-color-input-border); + border-top-color: var(--ons-color-input-bg); + border-width: 0 0 3px 3px; + box-sizing: border-box; + content: ''; + height: 7px; + left: 2px; + opacity: 0; + position: absolute; + top: 4px; + transform: rotate(-45deg); + width: 14px; + } - &:focus, - &:checked { - outline: none; - } + &:focus, + &:checked { + outline: none; + } - &:checked::after { - opacity: 1; - } + &:checked::after { + opacity: 1; + } - &:disabled { - border: 2px solid var(--ons-color-border-disabled); - cursor: not-allowed; - } + &:disabled { + border: 2px solid var(--ons-color-border-disabled); + cursor: not-allowed; + } - &:disabled:checked::after { - border-color: var(--ons-color-border-disabled); - } + &:disabled:checked::after { + border-color: var(--ons-color-border-disabled); + } - &:disabled + .ons-checkbox__label, - &:disabled:checked + .ons-checkbox__label { - color: var(--ons-color-border-disabled); - cursor: not-allowed; + &:disabled + .ons-checkbox__label, + &:disabled:checked + .ons-checkbox__label { + color: var(--ons-color-border-disabled); + cursor: not-allowed; - &::before { - border: 1px solid var(--ons-color-border-disabled); - } - } + &::before { + border: 1px solid var(--ons-color-border-disabled); + } + } - &:disabled:checked + .ons-checkbox__label { - &::before { - box-shadow: 0 0 0 1px var(--ons-color-border-disabled); - } + &:disabled:checked + .ons-checkbox__label { + &::before { + box-shadow: 0 0 0 1px var(--ons-color-border-disabled); + } + } } - } - &--no-border { - & > .ons-checkbox__label { - padding: 0 0 0 1.85rem; + &--no-border { + > .ons-checkbox__label { + padding: 0 0 0 1.85rem; - &::before { - background: none !important; - border: none !important; - box-shadow: none !important; - } + &::before { + background: none !important; + border: 0 !important; + box-shadow: none !important; + } - & > .ons-checkbox__label--with-description { - padding: 0; - } - } - - & > .ons-checkbox__input { - left: 0.05rem; - top: 0.15rem; + > .ons-checkbox__label--with-description { + padding: 0; + } + } - &:checked, - &:focus { - & + .ons-checkbox__label::before { - background: none; - border: none; - box-shadow: none; - outline: none; + > .ons-checkbox__input { + left: 0.05rem; + top: 0.15rem; + + &:checked, + &:focus { + + .ons-checkbox__label::before { + background: none; + border: 0; + box-shadow: none; + outline: none; + } + } + + &:focus { + @extend %ons-input-focus; + } } - } - &:focus { - @extend %ons-input-focus; - } + .ons-checkbox__other { + margin: 0.5rem 0 0.5rem 0.5rem; + } } - & .ons-checkbox__other { - margin: 0.5rem 0 0.5rem 0.5rem; + &--no-label { + > .ons-checkbox__input { + left: auto; + position: relative; + top: auto; + vertical-align: middle; + + &:checked, + &:focus { + + .ons-checkbox__label::before { + background: none; + border: 0; + box-shadow: none; + } + } + + &:focus { + @extend %ons-input-focus; + } + + + .ons-checkbox__label { + @extend .ons-u-vh; + } + } } - } - &--no-label { - & > .ons-checkbox__input { - left: auto; - position: relative; - top: auto; - vertical-align: middle; + &__label { + cursor: pointer; + display: block; + padding: $checkbox-padding 1rem $checkbox-padding ($checkbox-padding * 2 + $checkbox-input-width); + width: 100%; - &:checked, - &:focus { - & + .ons-checkbox__label::before { - background: none; - border: none; - box-shadow: none; + &--with-description { + padding: 0 1rem $checkbox-padding 0; } - } - &:focus { - @extend %ons-input-focus; - } + &::before { + background: var(--ons-color-white); + border: 1px solid var(--ons-color-input-border); + border-radius: 3px; + inset: 0; + content: ''; + position: absolute; + z-index: -1; + } - & + .ons-checkbox__label { - @extend .ons-u-vh; - } + * { + pointer-events: none; + } } - } - &__label { - cursor: pointer; - display: block; - padding: $checkbox-padding 1rem $checkbox-padding ($checkbox-padding * 2 + $checkbox-input-width); - width: 100%; - - &--with-description { - padding: 0 1rem $checkbox-padding 0; + &__description { + display: block; + margin-top: 0.25rem; } - &::before { - background: var(--ons-color-white); - border: 1px solid var(--ons-color-input-border); - border-radius: 3px; - inset: 0; - content: ''; - position: absolute; - z-index: -1; + &__other { + border-left: 4px solid var(--ons-color-borders-indent); + display: block; + margin: 0 1rem 0.5rem 1.1rem; + padding: 0 $checkbox-padding $checkbox-padding $checkbox-padding * 2 - 1; } - * { - pointer-events: none; + &__input:checked + &__label::before { + background: var(--ons-color-grey-5); + box-shadow: 0 0 0 1px var(--ons-color-input-border); + outline: 1px solid transparent; // Add transparent outline because Windows High Contrast Mode doesn't show box-shadows } - } - - &__description { - display: block; - margin-top: 0.25rem; - } - - &__other { - border-left: 4px solid var(--ons-color-borders-indent); - display: block; - margin: 0 1rem 0.5rem 1.1rem; - padding: 0 $checkbox-padding $checkbox-padding $checkbox-padding * 2 - 1; - } - - &__input:checked + &__label::before { - background: var(--ons-color-grey-5); - box-shadow: 0 0 0 1px var(--ons-color-input-border); - outline: 1px solid transparent; // Add transparent outline because Windows High Contrast Mode doesn't show box-shadows - } - - &__input:focus + &__label::before { - @extend %ons-input-focus; - } - - &__input:not(:checked) ~ &__other { - display: none; - } - - &__input:not(:checked) ~ &__other--open { - display: block; - } - - &--toggle & { - &__input { - left: 0; - top: 0.2rem; - &:focus { + &__input:focus + &__label::before { @extend %ons-input-focus; - } } - &__label { - padding: 0 0 0 ($checkbox-input-width + $checkbox-padding); - - &::before { - background: none; - border: 0; - } + &__input:not(:checked) ~ &__other { + display: none; } - &__input:checked + .ons-checkbox__label::before, - &__input:focus + .ons-checkbox__label::before { - background: transparent; - box-shadow: none; + &__input:not(:checked) ~ &__other--open { + display: block; } - &__input:focus + .ons-checkbox__label::before { - border: 0; - outline: none; + &--toggle & { + &__input { + left: 0; + top: 0.2rem; + + &:focus { + @extend %ons-input-focus; + } + } + + &__label { + padding: 0 0 0 ($checkbox-input-width + $checkbox-padding); + + &::before { + background: none; + border: 0; + } + } + + &__input:checked + .ons-checkbox__label::before, + &__input:focus + .ons-checkbox__label::before { + background: transparent; + box-shadow: none; + } + + &__input:focus + .ons-checkbox__label::before { + border: 0; + outline: none; + } } - } } diff --git a/src/components/checkboxes/_checkboxes.scss b/src/components/checkboxes/_checkboxes.scss index 0f016f48a2..44aecee5f7 100644 --- a/src/components/checkboxes/_checkboxes.scss +++ b/src/components/checkboxes/_checkboxes.scss @@ -1,45 +1,45 @@ .ons-checkboxes { - &__label { - display: block; - margin: 0 0 1rem; - } - - &__items { - display: block; - - @include mq(s) { - max-width: max-content; + &__label { + display: block; + margin: 0 0 1rem; } - } - &__item { - display: inline-block; - margin: 0 0 0.5rem; - width: 100%; + &__items { + display: block; - &:last-child { - margin-bottom: 0; + @include mq(s) { + max-width: max-content; + } } - @include mq(s) { - min-width: 20rem; - :has(select):first-child { - min-width: 25rem; - } - - &--no-border { - min-width: 0; - } + &__item { + display: inline-block; + margin: 0 0 0.5rem; + width: 100%; + + &:last-child { + margin-bottom: 0; + } + + @include mq(s) { + min-width: 20rem; + :has(select):first-child { + min-width: 25rem; + } + + &--no-border { + min-width: 0; + } + } } - } - &--mutually-exclusive__item { - @extend .ons-checkboxes__item; + &--mutually-exclusive__item { + @extend .ons-checkboxes__item; - margin-bottom: 0; + margin-bottom: 0; - @include mq(s) { - max-width: max-content; + @include mq(s) { + max-width: max-content; + } } - } } diff --git a/src/components/checkboxes/_macro.spec.js b/src/components/checkboxes/_macro.spec.js index d098b2cf6f..2288ccb3a8 100644 --- a/src/components/checkboxes/_macro.spec.js +++ b/src/components/checkboxes/_macro.spec.js @@ -6,297 +6,297 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_CHECKBOX_ITEM = { - id: 'example-checkbox-id', - name: 'example-checkbox-name', - value: '123', - label: { - classes: 'extra-label-class', - text: 'Example checkbox', - description: 'Example label description.', - }, + id: 'example-checkbox-id', + name: 'example-checkbox-name', + value: '123', + label: { + classes: 'extra-label-class', + text: 'Example checkbox', + description: 'Example label description.', + }, }; const EXAMPLE_CHECKBOX_ITEM_CHECKBOXES = { - id: 'example-item-checkboxes', - name: 'example-item-checkboxes', - value: 'checkboxes', - label: { - text: 'Example item with checkboxes', - }, - other: { - otherType: 'checkboxes', - selectAllChildren: true, - id: 'example-checkboxes', - name: 'example-checkboxes-name', - legend: 'Select preferred times of day', - legendClasses: 'extra-legend-class', - attributes: { a: 42 }, - checkboxes: [ - { - value: 'morning', - id: 'morning', - label: { - text: 'Morning', - }, - }, - { - value: 'afternoon', - id: 'afternoon', - label: { - text: 'Afternoon', - }, - }, - ], - }, + id: 'example-item-checkboxes', + name: 'example-item-checkboxes', + value: 'checkboxes', + label: { + text: 'Example item with checkboxes', + }, + other: { + otherType: 'checkboxes', + selectAllChildren: true, + id: 'example-checkboxes', + name: 'example-checkboxes-name', + legend: 'Select preferred times of day', + legendClasses: 'extra-legend-class', + attributes: { a: 42 }, + checkboxes: [ + { + value: 'morning', + id: 'morning', + label: { + text: 'Morning', + }, + }, + { + value: 'afternoon', + id: 'afternoon', + label: { + text: 'Afternoon', + }, + }, + ], + }, }; const EXAMPLE_CHECKBOXES = { - name: 'example-checkboxes-name', - legend: 'Legend text', - checkboxesLabel: 'Select all that apply', - checkboxesLabelClasses: 'extra-checkboxes-label-class', - checkboxes: [EXAMPLE_CHECKBOX_ITEM], + name: 'example-checkboxes-name', + legend: 'Legend text', + checkboxesLabel: 'Select all that apply', + checkboxesLabelClasses: 'extra-checkboxes-label-class', + checkboxes: [EXAMPLE_CHECKBOX_ITEM], }; const EXAMPLE_CHECKBOXES_WITH_ERROR = { - ...EXAMPLE_CHECKBOXES, - error: { - id: 'example-error-id', - text: 'An unexpected error occurred.', - }, + ...EXAMPLE_CHECKBOXES, + error: { + id: 'example-error-id', + text: 'An unexpected error occurred.', + }, }; const EXAMPLE_CHECKBOXES_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR = { - ...EXAMPLE_CHECKBOXES_WITH_ERROR, - dontWrap: true, - mutuallyExclusive: { - or: 'Or', - deselectMessage: 'Selecting this will clear your feedback', - deselectGroupAdjective: 'cleared', - deselectExclusiveOptionAdjective: 'deselected', - exclusiveOptions: [ - { - id: 'feedback-exclusive-option', - name: 'no-feedback', - value: 'no-feedback', - label: { - text: 'I dont want to provide feedback', - }, - }, - ], - }, -}; - -describe('macro: checkboxes', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('checkboxes', EXAMPLE_CHECKBOXES)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('renders `fieldset` component with the expected parameters', () => { - const faker = templateFaker(); - const fieldsetSpy = faker.spy('fieldset'); - - faker.renderComponent('checkboxes', { - ...EXAMPLE_CHECKBOXES_WITH_ERROR, - id: 'example-id', - classes: 'extra-class', - legendClasses: 'extra-legend-class', - description: 'An example description.', - dontWrap: true, - legendIsQuestionTitle: false, - }); - - expect(fieldsetSpy.occurrences[0]).toEqual({ - id: 'example-id', - classes: 'extra-class', - legend: 'Legend text', - legendClasses: 'extra-legend-class', - description: 'An example description.', - dontWrap: true, - legendIsQuestionTitle: false, - error: EXAMPLE_CHECKBOXES_WITH_ERROR.error, - }); - }); - - describe('label', () => { - it('has the expected text', () => { - const $ = cheerio.load(renderComponent('checkboxes', EXAMPLE_CHECKBOXES)); - - expect($('.ons-checkboxes__label').text().trim()).toBe('Select all that apply'); - }); - - it('has additionally provided `checkboxesLabelClasses`', () => { - const $ = cheerio.load(renderComponent('checkboxes', EXAMPLE_CHECKBOXES)); - - expect($('.ons-checkboxes__label').hasClass('extra-checkboxes-label-class')).toBe(true); - }); - }); - - describe('mutually exclusive', () => { - it('has the `ons-js-exclusive-group-item` class', () => { - const $ = cheerio.load(renderComponent('checkboxes', EXAMPLE_CHECKBOXES_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR)); - - expect($('.ons-js-exclusive-group-item').length).toBe(1); - }); - - it('renders mutually exclusive component', () => { - const faker = templateFaker(); - const mutuallyExclusiveSpy = faker.spy('mutually-exclusive'); - - faker.renderComponent('checkboxes', { - ...EXAMPLE_CHECKBOXES_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR, - id: 'example-field-id', - classes: 'extra-class', - legend: 'Legend text', - legendClasses: 'extra-legend-class', - description: 'Example description text', - legendIsQuestionTitle: true, - attributes: { a: 42 }, - }); - - expect(mutuallyExclusiveSpy.occurrences).toContainEqual({ - id: 'example-field-id', - description: 'Example description text', - classes: 'extra-class', - legend: 'Legend text', - legendClasses: 'extra-legend-class', - dontWrap: true, - legendIsQuestionTitle: true, - attributes: { a: 42 }, - exclusiveOptions: EXAMPLE_CHECKBOXES_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR.mutuallyExclusive.exclusiveOptions, + ...EXAMPLE_CHECKBOXES_WITH_ERROR, + dontWrap: true, + mutuallyExclusive: { or: 'Or', deselectMessage: 'Selecting this will clear your feedback', deselectGroupAdjective: 'cleared', deselectExclusiveOptionAdjective: 'deselected', - error: EXAMPLE_CHECKBOXES_WITH_ERROR.error, - }); - }); - }); + exclusiveOptions: [ + { + id: 'feedback-exclusive-option', + name: 'no-feedback', + value: 'no-feedback', + label: { + text: 'I dont want to provide feedback', + }, + }, + ], + }, +}; - describe('checkbox items', () => { - it('renders a border around each item by default', () => { - const $ = cheerio.load(renderComponent('checkboxes', EXAMPLE_CHECKBOXES)); +describe('macro: checkboxes', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('checkboxes', EXAMPLE_CHECKBOXES)); - expect($('.ons-checkboxes__item').hasClass('ons-checkboxes__item--no-border')).toBe(false); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); }); - it('does not render a border around each item when `borderless` is `false`', () => { - const $ = cheerio.load( - renderComponent('checkboxes', { - ...EXAMPLE_CHECKBOXES, - borderless: true, - }), - ); - - expect($('.ons-checkboxes__item').hasClass('ons-checkboxes__item--no-border')).toBe(true); + it('renders `fieldset` component with the expected parameters', () => { + const faker = templateFaker(); + const fieldsetSpy = faker.spy('fieldset'); + + faker.renderComponent('checkboxes', { + ...EXAMPLE_CHECKBOXES_WITH_ERROR, + id: 'example-id', + classes: 'extra-class', + legendClasses: 'extra-legend-class', + description: 'An example description.', + dontWrap: true, + legendIsQuestionTitle: false, + }); + + expect(fieldsetSpy.occurrences[0]).toEqual({ + id: 'example-id', + classes: 'extra-class', + legend: 'Legend text', + legendClasses: 'extra-legend-class', + description: 'An example description.', + dontWrap: true, + legendIsQuestionTitle: false, + error: EXAMPLE_CHECKBOXES_WITH_ERROR.error, + }); }); - it('does not mark radio with a class indicating that all child options should be selected', () => { - const $ = cheerio.load( - renderComponent('checkboxes', { - ...EXAMPLE_CHECKBOXES, - checkboxes: [EXAMPLE_CHECKBOX_ITEM], - }), - ); + describe('label', () => { + it('has the expected text', () => { + const $ = cheerio.load(renderComponent('checkboxes', EXAMPLE_CHECKBOXES)); - expect($('.ons-js-other').hasClass('ons-js-select-all-children')).toBe(false); - }); + expect($('.ons-checkboxes__label').text().trim()).toBe('Select all that apply'); + }); - it('marks radio with a class indicating that all child options should be selected', () => { - const $ = cheerio.load( - renderComponent('checkboxes', { - ...EXAMPLE_CHECKBOXES, - checkboxes: [EXAMPLE_CHECKBOX_ITEM_CHECKBOXES], - }), - ); + it('has additionally provided `checkboxesLabelClasses`', () => { + const $ = cheerio.load(renderComponent('checkboxes', EXAMPLE_CHECKBOXES)); - expect($('.ons-js-other').hasClass('ons-js-select-all-children')).toBe(true); + expect($('.ons-checkboxes__label').hasClass('extra-checkboxes-label-class')).toBe(true); + }); }); - it('renders `checkboxes/checkbox` component', () => { - const faker = templateFaker(); - const checkboxSpy = faker.spy('checkboxes/checkbox'); - - faker.renderComponent('checkboxes', { - ...EXAMPLE_CHECKBOXES, - borderless: true, - checkboxes: [ - { - ...EXAMPLE_CHECKBOX_ITEM, - classes: 'extra-checkbox-item-class', - checked: false, - disabled: false, - attributes: { a: '123', b: '456' }, - }, - ], - }); - - expect(checkboxSpy.occurrences).toContainEqual({ - id: 'example-checkbox-id', - name: 'example-checkbox-name', - classes: 'extra-checkbox-item-class ons-checkbox--no-border', - inputClasses: '', - borderless: true, - checked: false, - disabled: false, - attributes: { a: '123', b: '456' }, - label: { - classes: 'extra-label-class', - description: 'Example label description.', - for: 'example-checkbox-id', - text: 'Example checkbox', - }, - value: '123', - }); - }); - }); - - describe('toggle selection button', () => { - const params = { - ...EXAMPLE_CHECKBOXES, - autoSelect: { - selectAllText: 'Select all', - unselectAllText: 'Unselect all', - context: 'following checkboxes', - }, - }; - - it('renders `button` component', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); - - faker.renderComponent('checkboxes', params); - - expect(buttonSpy.occurrences[0]).toHaveProperty( - 'html', - 'Select all following checkboxes', - ); - expect(buttonSpy.occurrences[0]).toHaveProperty('variants', ['small', 'secondary']); - expect(buttonSpy.occurrences[0]).toHaveProperty('classes', 'ons-u-mb-s ons-js-auto-selector'); - expect(buttonSpy.occurrences[0]).toHaveProperty('attributes.data-select-all', 'Select all'); - expect(buttonSpy.occurrences[0]).toHaveProperty('attributes.data-unselect-all', 'Unselect all'); + describe('mutually exclusive', () => { + it('has the `ons-js-exclusive-group-item` class', () => { + const $ = cheerio.load(renderComponent('checkboxes', EXAMPLE_CHECKBOXES_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR)); + + expect($('.ons-js-exclusive-group-item').length).toBe(1); + }); + + it('renders mutually exclusive component', () => { + const faker = templateFaker(); + const mutuallyExclusiveSpy = faker.spy('mutually-exclusive'); + + faker.renderComponent('checkboxes', { + ...EXAMPLE_CHECKBOXES_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR, + id: 'example-field-id', + classes: 'extra-class', + legend: 'Legend text', + legendClasses: 'extra-legend-class', + description: 'Example description text', + legendIsQuestionTitle: true, + attributes: { a: 42 }, + }); + + expect(mutuallyExclusiveSpy.occurrences).toContainEqual({ + id: 'example-field-id', + description: 'Example description text', + classes: 'extra-class', + legend: 'Legend text', + legendClasses: 'extra-legend-class', + dontWrap: true, + legendIsQuestionTitle: true, + attributes: { a: 42 }, + exclusiveOptions: EXAMPLE_CHECKBOXES_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR.mutuallyExclusive.exclusiveOptions, + or: 'Or', + deselectMessage: 'Selecting this will clear your feedback', + deselectGroupAdjective: 'cleared', + deselectExclusiveOptionAdjective: 'deselected', + error: EXAMPLE_CHECKBOXES_WITH_ERROR.error, + }); + }); }); - it('does not render `button` component when a checkbox has an `other` input', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); + describe('checkbox items', () => { + it('renders a border around each item by default', () => { + const $ = cheerio.load(renderComponent('checkboxes', EXAMPLE_CHECKBOXES)); + + expect($('.ons-checkboxes__item').hasClass('ons-checkboxes__item--no-border')).toBe(false); + }); + + it('does not render a border around each item when `borderless` is `false`', () => { + const $ = cheerio.load( + renderComponent('checkboxes', { + ...EXAMPLE_CHECKBOXES, + borderless: true, + }), + ); + + expect($('.ons-checkboxes__item').hasClass('ons-checkboxes__item--no-border')).toBe(true); + }); + + it('does not mark radio with a class indicating that all child options should be selected', () => { + const $ = cheerio.load( + renderComponent('checkboxes', { + ...EXAMPLE_CHECKBOXES, + checkboxes: [EXAMPLE_CHECKBOX_ITEM], + }), + ); + + expect($('.ons-js-other').hasClass('ons-js-select-all-children')).toBe(false); + }); + + it('marks radio with a class indicating that all child options should be selected', () => { + const $ = cheerio.load( + renderComponent('checkboxes', { + ...EXAMPLE_CHECKBOXES, + checkboxes: [EXAMPLE_CHECKBOX_ITEM_CHECKBOXES], + }), + ); + + expect($('.ons-js-other').hasClass('ons-js-select-all-children')).toBe(true); + }); + + it('renders `checkboxes/checkbox` component', () => { + const faker = templateFaker(); + const checkboxSpy = faker.spy('checkboxes/checkbox'); + + faker.renderComponent('checkboxes', { + ...EXAMPLE_CHECKBOXES, + borderless: true, + checkboxes: [ + { + ...EXAMPLE_CHECKBOX_ITEM, + classes: 'extra-checkbox-item-class', + checked: false, + disabled: false, + attributes: { a: '123', b: '456' }, + }, + ], + }); + + expect(checkboxSpy.occurrences).toContainEqual({ + id: 'example-checkbox-id', + name: 'example-checkbox-name', + classes: 'extra-checkbox-item-class ons-checkbox--no-border', + inputClasses: '', + borderless: true, + checked: false, + disabled: false, + attributes: { a: '123', b: '456' }, + label: { + classes: 'extra-label-class', + description: 'Example label description.', + for: 'example-checkbox-id', + text: 'Example checkbox', + }, + value: '123', + }); + }); + }); - faker.renderComponent('checkboxes', { - ...params, - checkboxes: [ - { - ...EXAMPLE_CHECKBOX_ITEM, - other: { - label: { text: 'Example input' }, + describe('toggle selection button', () => { + const params = { + ...EXAMPLE_CHECKBOXES, + autoSelect: { + selectAllText: 'Select all', + unselectAllText: 'Unselect all', + context: 'following checkboxes', }, - }, - ], - }); - - expect(buttonSpy.occurrences.length).toBe(0); + }; + + it('renders `button` component', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); + + faker.renderComponent('checkboxes', params); + + expect(buttonSpy.occurrences[0]).toHaveProperty( + 'html', + 'Select all following checkboxes', + ); + expect(buttonSpy.occurrences[0]).toHaveProperty('variants', ['small', 'secondary']); + expect(buttonSpy.occurrences[0]).toHaveProperty('classes', 'ons-u-mb-s ons-js-auto-selector'); + expect(buttonSpy.occurrences[0]).toHaveProperty('attributes.data-select-all', 'Select all'); + expect(buttonSpy.occurrences[0]).toHaveProperty('attributes.data-unselect-all', 'Unselect all'); + }); + + it('does not render `button` component when a checkbox has an `other` input', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); + + faker.renderComponent('checkboxes', { + ...params, + checkboxes: [ + { + ...EXAMPLE_CHECKBOX_ITEM, + other: { + label: { text: 'Example input' }, + }, + }, + ], + }); + + expect(buttonSpy.occurrences.length).toBe(0); + }); }); - }); }); diff --git a/src/components/checkboxes/checkbox-with-autoselect.js b/src/components/checkboxes/checkbox-with-autoselect.js index a2bc68b55e..8e0e7f43a9 100644 --- a/src/components/checkboxes/checkbox-with-autoselect.js +++ b/src/components/checkboxes/checkbox-with-autoselect.js @@ -1,38 +1,38 @@ export default class CheckboxWithAutoSelect { - constructor(context, button, insideReveal) { - this.context = context; - this.button = button; - this.checkboxes = [...this.context.querySelectorAll('input')]; - this.insideReveal = insideReveal; - // Event listeners - this.button.addEventListener('click', this.handleButtonEvent.bind(this)); - this.checkboxes.forEach((checkbox) => checkbox.addEventListener('change', this.handleCheckboxEvent.bind(this))); - if (this.insideReveal) { - this.insideReveal.addEventListener('change', this.handleCheckboxEvent.bind(this)); - } + constructor(context, button, insideReveal) { + this.context = context; + this.button = button; + this.checkboxes = [...this.context.querySelectorAll('input')]; + this.insideReveal = insideReveal; + // Event listeners + this.button.addEventListener('click', this.handleButtonEvent.bind(this)); + this.checkboxes.forEach((checkbox) => checkbox.addEventListener('change', this.handleCheckboxEvent.bind(this))); + if (this.insideReveal) { + this.insideReveal.addEventListener('change', this.handleCheckboxEvent.bind(this)); + } - // Status - this.numberOfCheckboxes = this.checkboxes.length; - this.allChecked = false; + // Status + this.numberOfCheckboxes = this.checkboxes.length; + this.allChecked = false; - // Text replacement - this.buttonText = this.button.querySelector('.ons-js-button-text'); - this.selectAllText = this.button.getAttribute('data-select-all'); - this.unselectAllText = this.button.getAttribute('data-unselect-all'); - } + // Text replacement + this.buttonText = this.button.querySelector('.ons-js-button-text'); + this.selectAllText = this.button.getAttribute('data-select-all'); + this.unselectAllText = this.button.getAttribute('data-unselect-all'); + } - handleButtonEvent(event) { - event.preventDefault(); - this.checkboxes.forEach((checkbox) => { - checkbox.checked = this.allChecked === false ? true : false; - }); - this.buttonText.innerHTML = this.allChecked === false ? this.unselectAllText : this.selectAllText; - this.allChecked = this.allChecked === false ? true : false; - } + handleButtonEvent(event) { + event.preventDefault(); + this.checkboxes.forEach((checkbox) => { + checkbox.checked = this.allChecked === false ? true : false; + }); + this.buttonText.innerHTML = this.allChecked === false ? this.unselectAllText : this.selectAllText; + this.allChecked = this.allChecked === false ? true : false; + } - handleCheckboxEvent() { - const totalChecked = this.checkboxes.filter((checkbox) => checkbox.checked).length; - this.buttonText.innerHTML = totalChecked === this.numberOfCheckboxes ? this.unselectAllText : this.selectAllText; - this.allChecked = totalChecked === this.numberOfCheckboxes ? true : false; - } + handleCheckboxEvent() { + const totalChecked = this.checkboxes.filter((checkbox) => checkbox.checked).length; + this.buttonText.innerHTML = totalChecked === this.numberOfCheckboxes ? this.unselectAllText : this.selectAllText; + this.allChecked = totalChecked === this.numberOfCheckboxes ? true : false; + } } diff --git a/src/components/checkboxes/checkbox-with-fieldset.js b/src/components/checkboxes/checkbox-with-fieldset.js index 9ed6b9c5fd..7027c79710 100644 --- a/src/components/checkboxes/checkbox-with-fieldset.js +++ b/src/components/checkboxes/checkbox-with-fieldset.js @@ -1,28 +1,28 @@ export default class CheckboxWithFieldset { - constructor(context) { - this.context = context; - this.checkbox = context.querySelector('.ons-js-checkbox'); - this.childInputs = [...this.context.querySelectorAll('input')]; - this.selectAllChildrenInput = this.context.querySelector('.ons-js-select-all-children'); + constructor(context) { + this.context = context; + this.checkbox = context.querySelector('.ons-js-checkbox'); + this.childInputs = [...this.context.querySelectorAll('input')]; + this.selectAllChildrenInput = this.context.querySelector('.ons-js-select-all-children'); - if (this.selectAllChildrenInput) { - this.selectAllChildrenInput.addEventListener('change', this.checkChildInputsOnSelect.bind(this)); - } else { - this.checkbox.addEventListener('change', this.uncheckChildInputsOnDeselect.bind(this)); + if (this.selectAllChildrenInput) { + this.selectAllChildrenInput.addEventListener('change', this.checkChildInputsOnSelect.bind(this)); + } else { + this.checkbox.addEventListener('change', this.uncheckChildInputsOnDeselect.bind(this)); + } } - } - checkChildInputsOnSelect() { - this.childInputs.forEach((input) => { - input.checked = this.selectAllChildrenInput.checked === true ? true : false; - }); - } + checkChildInputsOnSelect() { + this.childInputs.forEach((input) => { + input.checked = this.selectAllChildrenInput.checked === true ? true : false; + }); + } - uncheckChildInputsOnDeselect() { - if (this.checkbox.checked === false) { - this.childInputs.forEach((input) => { - input.checked = false; - }); + uncheckChildInputsOnDeselect() { + if (this.checkbox.checked === false) { + this.childInputs.forEach((input) => { + input.checked = false; + }); + } } - } } diff --git a/src/components/checkboxes/checkboxes-with-reveal.js b/src/components/checkboxes/checkboxes-with-reveal.js index 073bbec09f..043c1e0ed0 100644 --- a/src/components/checkboxes/checkboxes-with-reveal.js +++ b/src/components/checkboxes/checkboxes-with-reveal.js @@ -1,13 +1,13 @@ export default class Checkboxes { - constructor(inputs) { - this.inputs = inputs; - this.inputs.forEach((input) => input.addEventListener('change', this.setExpandedAttributes.bind(this))); - this.setExpandedAttributes(); - } + constructor(inputs) { + this.inputs = inputs; + this.inputs.forEach((input) => input.addEventListener('change', this.setExpandedAttributes.bind(this))); + this.setExpandedAttributes(); + } - setExpandedAttributes() { - this.inputs - .filter((input) => input.hasAttribute('aria-haspopup')) - .forEach((input) => input.setAttribute('aria-expanded', input.checked)); - } + setExpandedAttributes() { + this.inputs + .filter((input) => input.hasAttribute('aria-haspopup')) + .forEach((input) => input.setAttribute('aria-expanded', input.checked)); + } } diff --git a/src/components/checkboxes/checkboxes.dom.js b/src/components/checkboxes/checkboxes.dom.js index d8cc03f996..8e985ee36f 100644 --- a/src/components/checkboxes/checkboxes.dom.js +++ b/src/components/checkboxes/checkboxes.dom.js @@ -1,37 +1,37 @@ import domready from '../../js/domready'; domready(async () => { - const checkboxes = [...document.querySelectorAll('.ons-js-checkbox')]; + const checkboxes = [...document.querySelectorAll('.ons-js-checkbox')]; - if (checkboxes.length) { - const Checkboxes = (await import('./checkboxes-with-reveal')).default; - new Checkboxes(checkboxes); + if (checkboxes.length) { + const Checkboxes = (await import('./checkboxes-with-reveal')).default; + new Checkboxes(checkboxes); - const openOther = document.querySelector('.ons-checkbox__other--open'); - if (openOther) { - const checkboxInput = openOther.parentNode.querySelector('.ons-checkbox__input'); - const CheckCheckbox = (await import('../radios/check-radios')).default; + const openOther = document.querySelector('.ons-checkbox__other--open'); + if (openOther) { + const checkboxInput = openOther.parentNode.querySelector('.ons-checkbox__input'); + const CheckCheckbox = (await import('../radios/check-radios')).default; - new CheckCheckbox(checkboxInput, openOther); - } + new CheckCheckbox(checkboxInput, openOther); + } - const otherFieldsets = [...document.querySelectorAll('.ons-js-other-fieldset-checkbox')]; - if (otherFieldsets) { - const CheckboxWithInnerFieldset = (await import('./checkbox-with-fieldset')).default; - otherFieldsets.forEach((otherFieldset) => { - const context = otherFieldset.closest('.ons-checkbox'); - new CheckboxWithInnerFieldset(context); - }); - } + const otherFieldsets = [...document.querySelectorAll('.ons-js-other-fieldset-checkbox')]; + if (otherFieldsets) { + const CheckboxWithInnerFieldset = (await import('./checkbox-with-fieldset')).default; + otherFieldsets.forEach((otherFieldset) => { + const context = otherFieldset.closest('.ons-checkbox'); + new CheckboxWithInnerFieldset(context); + }); + } - const autoSelectButtons = [...document.querySelectorAll('.ons-js-auto-selector')]; - if (autoSelectButtons) { - const CheckboxWithAutoSelect = (await import('./checkbox-with-autoselect')).default; - autoSelectButtons.forEach((button) => { - const context = button.parentNode; - const insideReveal = context.parentNode.parentNode.querySelector('.ons-js-other'); - new CheckboxWithAutoSelect(context, button, insideReveal); - }); + const autoSelectButtons = [...document.querySelectorAll('.ons-js-auto-selector')]; + if (autoSelectButtons) { + const CheckboxWithAutoSelect = (await import('./checkbox-with-autoselect')).default; + autoSelectButtons.forEach((button) => { + const context = button.parentNode; + const insideReveal = context.parentNode.parentNode.querySelector('.ons-js-other'); + new CheckboxWithAutoSelect(context, button, insideReveal); + }); + } } - } }); diff --git a/src/components/checkboxes/checkboxes.spec.js b/src/components/checkboxes/checkboxes.spec.js index f8e3988303..b8c3c72cd3 100644 --- a/src/components/checkboxes/checkboxes.spec.js +++ b/src/components/checkboxes/checkboxes.spec.js @@ -1,208 +1,208 @@ import { renderComponent, setTestPage } from '../../tests/helpers/rendering'; describe('script: checkboxes', () => { - describe('automatic selection', () => { - const params = { - legend: 'What are your favourite pizza toppings?', - checkboxesLabel: 'Select all that apply', - name: 'food-other', - autoSelect: { - selectAllText: 'Select all', - unselectAllText: 'Unselect all', - context: 'following checkboxes', - }, - checkboxes: [ - { - id: 'bacon-other', - label: { - text: 'Bacon', - }, - value: 'bacon', - }, - { - id: 'olives-other', - label: { - text: 'Olives', - }, - value: 'olives', - }, - { - id: 'other-checkbox', - label: { - text: 'Other', - description: 'An answer is required', - }, - value: 'other', - }, - ], - }; - - beforeEach(async () => { - await setTestPage('/test', renderComponent('checkboxes', params)); - }); - - describe('and the autoselect button is clicked', () => { - beforeEach(async () => { - await page.click('.ons-js-auto-selector'); - }); - - it('all checkboxes should be checked', async () => { - const checkedStates = await page.$$eval('.ons-js-checkbox', (nodes) => nodes.map((node) => node.checked)); - expect(checkedStates).toEqual([true, true, true]); - }); - - it('the button text should have changed', async () => { - const buttonText = await page.$eval('.ons-js-button-text', (node) => node.textContent); - expect(buttonText).toBe('Unselect all'); - }); - }); - - describe('and the autoselect button is clicked to select all and clicked again', () => { - beforeEach(async () => { - await page.click('.ons-js-auto-selector'); - await page.click('.ons-js-auto-selector'); - }); - - it('all checkboxes should be unchecked', async () => { - const checkedStates = await page.$$eval('.ons-js-checkbox', (nodes) => nodes.map((node) => node.checked)); - expect(checkedStates).toEqual([false, false, false]); - }); - - it('the button text should have changed', async () => { - const buttonText = await page.$eval('.ons-js-button-text', (node) => node.textContent); - expect(buttonText).toBe('Select all'); - }); - }); - - describe('when all except one checkbox is checked', () => { - beforeEach(async () => { - await page.$eval('#olives-other', (node) => (node.checked = true)); - }); - - it('the button text should be select all', async () => { - const buttonText = await page.$eval('.ons-js-button-text', (node) => node.textContent); - expect(buttonText).toBe('Select all'); - }); - }); - - describe('when the only unchecked checkbox is checked', () => { - beforeEach(async () => { - await page.click('#bacon-other'); - await page.click('#olives-other'); - await page.click('#other-checkbox'); - }); - - it('the button text should be unselect all', async () => { - const buttonText = await page.$eval('.ons-js-button-text', (node) => node.textContent); - expect(buttonText).toBe('Unselect all'); - }); - }); - }); - - describe('reveal and fieldset', function () { - const params = { - legend: 'What are your favourite pizza toppings?', - checkboxesLabel: 'Select all that apply', - name: 'food-other', - checkboxes: [ - { - id: 'bacon-other', - label: { - text: 'Bacon', - }, - value: 'bacon', - }, - { - id: 'olives-other', - label: { - text: 'Olives', - }, - value: 'olives', - }, - { - id: 'other-checkbox', - label: { - text: 'Other', - description: 'An answer is required', - }, - value: 'other', - other: { - id: 'other-textbox', - name: 'other-answer', - legend: 'Please specify other', - otherType: 'checkboxes', + describe('automatic selection', () => { + const params = { + legend: 'What are your favourite pizza toppings?', + checkboxesLabel: 'Select all that apply', + name: 'food-other', + autoSelect: { + selectAllText: 'Select all', + unselectAllText: 'Unselect all', + context: 'following checkboxes', + }, checkboxes: [ - { - id: 'inner-bacon-other', - label: { - text: 'Bacon', + { + id: 'bacon-other', + label: { + text: 'Bacon', + }, + value: 'bacon', }, - value: 'bacon', - }, - { - id: 'inner-olives-other', - label: { - text: 'Olives', + { + id: 'olives-other', + label: { + text: 'Olives', + }, + value: 'olives', + }, + { + id: 'other-checkbox', + label: { + text: 'Other', + description: 'An answer is required', + }, + value: 'other', }, - value: 'olives', - }, ], - }, - }, - ], - }; + }; - beforeEach(async () => { - await setTestPage('/test', renderComponent('checkboxes', params)); - }); + beforeEach(async () => { + await setTestPage('/test', renderComponent('checkboxes', params)); + }); - it('checkboxes with other options should be given aria-expanded attributes', async () => { - const ariaExpanded = await page.$eval('#other-checkbox', (node) => node.getAttribute('aria-expanded')); - expect(ariaExpanded).toBe('false'); - }); + describe('and the autoselect button is clicked', () => { + beforeEach(async () => { + await page.click('.ons-js-auto-selector'); + }); - describe('and a checkbox with an other input is checked', () => { - beforeEach(async () => { - await page.click('#other-checkbox'); - }); + it('all checkboxes should be checked', async () => { + const checkedStates = await page.$$eval('.ons-js-checkbox', (nodes) => nodes.map((node) => node.checked)); + expect(checkedStates).toEqual([true, true, true]); + }); - it('has aria-expanded attribute should be set to true', async () => { - const ariaExpanded = await page.$eval('#other-checkbox', (node) => node.getAttribute('aria-expanded')); - expect(ariaExpanded).toBe('true'); - }); + it('the button text should have changed', async () => { + const buttonText = await page.$eval('.ons-js-button-text', (node) => node.textContent); + expect(buttonText).toBe('Unselect all'); + }); + }); - describe('and any other checkbox is changed', () => { - beforeEach(async () => { - await page.click('#bacon-other'); + describe('and the autoselect button is clicked to select all and clicked again', () => { + beforeEach(async () => { + await page.click('.ons-js-auto-selector'); + await page.click('.ons-js-auto-selector'); + }); + + it('all checkboxes should be unchecked', async () => { + const checkedStates = await page.$$eval('.ons-js-checkbox', (nodes) => nodes.map((node) => node.checked)); + expect(checkedStates).toEqual([false, false, false]); + }); + + it('the button text should have changed', async () => { + const buttonText = await page.$eval('.ons-js-button-text', (node) => node.textContent); + expect(buttonText).toBe('Select all'); + }); }); - it('the checkbox with an other input aria-expanded attribute not change', async () => { - const ariaExpanded = await page.$eval('#other-checkbox', (node) => node.getAttribute('aria-expanded')); - expect(ariaExpanded).toBe('true'); + describe('when all except one checkbox is checked', () => { + beforeEach(async () => { + await page.$eval('#olives-other', (node) => (node.checked = true)); + }); + + it('the button text should be select all', async () => { + const buttonText = await page.$eval('.ons-js-button-text', (node) => node.textContent); + expect(buttonText).toBe('Select all'); + }); }); - }); - describe('and a child of other checkbox is checked', () => { - beforeEach(async () => { - await page.click('#inner-bacon-other'); + describe('when the only unchecked checkbox is checked', () => { + beforeEach(async () => { + await page.click('#bacon-other'); + await page.click('#olives-other'); + await page.click('#other-checkbox'); + }); + + it('the button text should be unselect all', async () => { + const buttonText = await page.$eval('.ons-js-button-text', (node) => node.textContent); + expect(buttonText).toBe('Unselect all'); + }); }); + }); - describe('and a checkbox with an other input is unchecked', () => { - beforeEach(async () => { - await page.click('#other-checkbox'); - }); + describe('reveal and fieldset', function () { + const params = { + legend: 'What are your favourite pizza toppings?', + checkboxesLabel: 'Select all that apply', + name: 'food-other', + checkboxes: [ + { + id: 'bacon-other', + label: { + text: 'Bacon', + }, + value: 'bacon', + }, + { + id: 'olives-other', + label: { + text: 'Olives', + }, + value: 'olives', + }, + { + id: 'other-checkbox', + label: { + text: 'Other', + description: 'An answer is required', + }, + value: 'other', + other: { + id: 'other-textbox', + name: 'other-answer', + legend: 'Please specify other', + otherType: 'checkboxes', + checkboxes: [ + { + id: 'inner-bacon-other', + label: { + text: 'Bacon', + }, + value: 'bacon', + }, + { + id: 'inner-olives-other', + label: { + text: 'Olives', + }, + value: 'olives', + }, + ], + }, + }, + ], + }; - it('its aria-expanded attribute should be set to false', async () => { + beforeEach(async () => { + await setTestPage('/test', renderComponent('checkboxes', params)); + }); + + it('checkboxes with other options should be given aria-expanded attributes', async () => { const ariaExpanded = await page.$eval('#other-checkbox', (node) => node.getAttribute('aria-expanded')); expect(ariaExpanded).toBe('false'); - }); + }); - it('the child of other checkbox should be unchecked', async () => { - const innerInputChecked = await page.$eval('#inner-bacon-other', (node) => node.checked); - expect(innerInputChecked).toBe(false); - }); + describe('and a checkbox with an other input is checked', () => { + beforeEach(async () => { + await page.click('#other-checkbox'); + }); + + it('has aria-expanded attribute should be set to true', async () => { + const ariaExpanded = await page.$eval('#other-checkbox', (node) => node.getAttribute('aria-expanded')); + expect(ariaExpanded).toBe('true'); + }); + + describe('and any other checkbox is changed', () => { + beforeEach(async () => { + await page.click('#bacon-other'); + }); + + it('the checkbox with an other input aria-expanded attribute not change', async () => { + const ariaExpanded = await page.$eval('#other-checkbox', (node) => node.getAttribute('aria-expanded')); + expect(ariaExpanded).toBe('true'); + }); + }); + + describe('and a child of other checkbox is checked', () => { + beforeEach(async () => { + await page.click('#inner-bacon-other'); + }); + + describe('and a checkbox with an other input is unchecked', () => { + beforeEach(async () => { + await page.click('#other-checkbox'); + }); + + it('its aria-expanded attribute should be set to false', async () => { + const ariaExpanded = await page.$eval('#other-checkbox', (node) => node.getAttribute('aria-expanded')); + expect(ariaExpanded).toBe('false'); + }); + + it('the child of other checkbox should be unchecked', async () => { + const innerInputChecked = await page.$eval('#inner-bacon-other', (node) => node.checked); + expect(innerInputChecked).toBe(false); + }); + }); + }); }); - }); }); - }); }); diff --git a/src/components/content-pagination/_content-pagination.scss b/src/components/content-pagination/_content-pagination.scss index 7337b7b9b3..73cc89ee7a 100644 --- a/src/components/content-pagination/_content-pagination.scss +++ b/src/components/content-pagination/_content-pagination.scss @@ -1,52 +1,52 @@ .ons-content-pagination { - display: block; - margin: 1.5rem 0 2.5rem; - - &__list { - list-style: none; - margin: 0; - padding: 0; - } - - &__item { - margin: 0 0 1.5rem; - } - - &__link { - display: inline-block; - text-decoration: none; - - &:hover { - text-decoration: none; - .ons-content-pagination__link-label { - text-decoration: underline solid var(--ons-color-text-link-hover) 2px; - } + display: block; + margin: 1.5rem 0 2.5rem; + + &__list { + list-style: none; + margin: 0; + padding: 0; } - &:focus { - text-decoration: none; + &__item { + margin: 0 0 1.5rem; + } - .ons-content-pagination__link-label { + &__link { + display: inline-block; text-decoration: none; - } + + &:hover { + text-decoration: none; + .ons-content-pagination__link-label { + text-decoration: underline solid var(--ons-color-text-link-hover) 2px; + } + } + + &:focus { + text-decoration: none; + + .ons-content-pagination__link-label { + text-decoration: none; + } + } } - } - &__link-title { - display: block; - } + &__link-title { + display: block; + } - &__link-text { - @extend .ons-u-fs-r--b; + &__link-text { + @extend .ons-u-fs-r--b; - margin: 0 0 0 0.5rem; - vertical-align: middle; - } + margin: 0 0 0 0.5rem; + vertical-align: middle; + } - &__link-label { - display: inline-block; - font-size: 0.9rem; - margin: 0 0 0 2rem; - text-decoration: underline; - } + &__link-label { + display: inline-block; + font-size: 0.9rem; + margin: 0 0 0 2rem; + text-decoration: underline; + } } diff --git a/src/components/content-pagination/_macro.spec.js b/src/components/content-pagination/_macro.spec.js index ee0beb2286..7d74201957 100644 --- a/src/components/content-pagination/_macro.spec.js +++ b/src/components/content-pagination/_macro.spec.js @@ -6,194 +6,194 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_CONTENT_PAGINATION_NONE = { - contentPaginationItems: [], + contentPaginationItems: [], }; const EXAMPLE_CONTENT_PAGINATION_PREV_ONLY = { - contentPaginationItems: [ - { - rel: 'prev', - text: 'Previous', - url: '/guide/overview', - label: 'Overview', - }, - ], + contentPaginationItems: [ + { + rel: 'prev', + text: 'Previous', + url: '/guide/overview', + label: 'Overview', + }, + ], }; const EXAMPLE_CONTENT_PAGINATION_NEXT_ONLY = { - contentPaginationItems: [ - { - rel: 'next', - text: 'Next', - url: '/guide/who-and-why', - label: 'Who should take part and why', - }, - ], + contentPaginationItems: [ + { + rel: 'next', + text: 'Next', + url: '/guide/who-and-why', + label: 'Who should take part and why', + }, + ], }; const EXAMPLE_CONTENT_PAGINATION_BOTH = { - contentPaginationItems: [ - { - rel: 'prev', - text: 'Previous', - url: '/guide/overview', - label: 'Overview', - }, - { - rel: 'next', - text: 'Next', - url: '/guide/who-and-why', - label: 'Who should take part and why', - }, - ], + contentPaginationItems: [ + { + rel: 'prev', + text: 'Previous', + url: '/guide/overview', + label: 'Overview', + }, + { + rel: 'next', + text: 'Next', + url: '/guide/who-and-why', + label: 'Who should take part and why', + }, + ], }; describe('macro: content-pagination', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('content-pagination', EXAMPLE_CONTENT_PAGINATION_NONE)); + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('content-pagination', EXAMPLE_CONTENT_PAGINATION_NONE)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has a default `aria-label` of "Guide pagination"', () => { - const $ = cheerio.load(renderComponent('content-pagination', EXAMPLE_CONTENT_PAGINATION_NONE)); - - expect($('.ons-content-pagination').attr('aria-label')).toBe('Guide pagination'); - }); - - it('has the provided `aria-label`', () => { - const $ = cheerio.load( - renderComponent('content-pagination', { - ...EXAMPLE_CONTENT_PAGINATION_NONE, - ariaLabel: 'Other pages', - }), - ); - - expect($('.ons-content-pagination').attr('aria-label')).toBe('Other pages'); - }); - - it('outputs no list items when `contentPaginationItems` is empty', () => { - const $ = cheerio.load(renderComponent('content-pagination', EXAMPLE_CONTENT_PAGINATION_NONE)); - - expect($('.ons-content-pagination__item').length).toBe(0); - }); - - it('renders the `arrow-previous` icon for the previous link entry', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); - - faker.renderComponent('content-pagination', EXAMPLE_CONTENT_PAGINATION_PREV_ONLY); - - expect(iconsSpy.occurrences[0]).toHaveProperty('iconType', 'arrow-previous'); - expect(iconsSpy.occurrences[0]).toHaveProperty('iconSize', 'm'); - }); - - it('renders the `arrow-next` icon for the next link entry', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); - - faker.renderComponent('content-pagination', EXAMPLE_CONTENT_PAGINATION_NEXT_ONLY); - - expect(iconsSpy.occurrences[0]).toHaveProperty('iconType', 'arrow-next'); - expect(iconsSpy.occurrences[0]).toHaveProperty('iconSize', 'm'); - }); - - describe('previous link', () => { - describe.each([ - ['with only the previous link', EXAMPLE_CONTENT_PAGINATION_PREV_ONLY], - ['with both the previous and next links', EXAMPLE_CONTENT_PAGINATION_BOTH], - ])('%s', (_, params) => { - it('renders a link with the correct URL', () => { - const $ = cheerio.load(renderComponent('content-pagination', params)); - - const prevLink = $('.ons-content-pagination__link[rel="prev"]'); - expect(prevLink.attr('href')).toBe('/guide/overview'); - }); - - it('renders the correct pagination item text', () => { - const $ = cheerio.load(renderComponent('content-pagination', params)); - - const linkText = $('.ons-content-pagination__link[rel="prev"] .ons-content-pagination__link-text'); - expect(linkText.text().trim()).toBe('Previous'); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - it('renders the expected default `bridgingText`', () => { - const $ = cheerio.load(renderComponent('content-pagination', params)); + it('has a default `aria-label` of "Guide pagination"', () => { + const $ = cheerio.load(renderComponent('content-pagination', EXAMPLE_CONTENT_PAGINATION_NONE)); - const bridgingText = $('.ons-content-pagination__link[rel="prev"] .ons-content-pagination__link-divider'); - expect(bridgingText.text().trim()).toBe('page in this guide is:'); - }); + expect($('.ons-content-pagination').attr('aria-label')).toBe('Guide pagination'); + }); - it('renders the provided `bridgingText`', () => { + it('has the provided `aria-label`', () => { const $ = cheerio.load( - renderComponent('content-pagination', { - contentPaginationItems: params.contentPaginationItems.map((item) => ({ - ...item, - bridgingText: 'custom bridging text:', - })), - }), + renderComponent('content-pagination', { + ...EXAMPLE_CONTENT_PAGINATION_NONE, + ariaLabel: 'Other pages', + }), ); - const bridgingText = $('.ons-content-pagination__link[rel="prev"] .ons-content-pagination__link-divider'); - expect(bridgingText.text().trim()).toBe('custom bridging text:'); - }); - - it('renders the provided `label`', () => { - const $ = cheerio.load(renderComponent('content-pagination', params)); - - const labelText = $('.ons-content-pagination__link[rel="prev"] .ons-content-pagination__link-label'); - expect(labelText.text().trim()).toBe('Overview'); - }); + expect($('.ons-content-pagination').attr('aria-label')).toBe('Other pages'); }); - }); - describe('next link', () => { - describe.each([ - ['with only the next link', EXAMPLE_CONTENT_PAGINATION_NEXT_ONLY], - ['with both the previous and next links', EXAMPLE_CONTENT_PAGINATION_BOTH], - ])('%s', (_, params) => { - it('renders a link with the correct URL', () => { - const $ = cheerio.load(renderComponent('content-pagination', params)); + it('outputs no list items when `contentPaginationItems` is empty', () => { + const $ = cheerio.load(renderComponent('content-pagination', EXAMPLE_CONTENT_PAGINATION_NONE)); - const prevLink = $('.ons-content-pagination__link[rel="next"]'); - expect(prevLink.attr('href')).toBe('/guide/who-and-why'); - }); + expect($('.ons-content-pagination__item').length).toBe(0); + }); - it('renders the correct pagination item text', () => { - const $ = cheerio.load(renderComponent('content-pagination', params)); + it('renders the `arrow-previous` icon for the previous link entry', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); - const linkText = $('.ons-content-pagination__link[rel="next"] .ons-content-pagination__link-text'); - expect(linkText.text().trim()).toBe('Next'); - }); + faker.renderComponent('content-pagination', EXAMPLE_CONTENT_PAGINATION_PREV_ONLY); - it('renders the expected default `bridgingText`', () => { - const $ = cheerio.load(renderComponent('content-pagination', params)); + expect(iconsSpy.occurrences[0]).toHaveProperty('iconType', 'arrow-previous'); + expect(iconsSpy.occurrences[0]).toHaveProperty('iconSize', 'm'); + }); - const bridgingText = $('.ons-content-pagination__link[rel="next"] .ons-content-pagination__link-divider'); - expect(bridgingText.text().trim()).toBe('page in this guide is:'); - }); + it('renders the `arrow-next` icon for the next link entry', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); - it('renders the provided `bridgingText`', () => { - const $ = cheerio.load( - renderComponent('content-pagination', { - contentPaginationItems: params.contentPaginationItems.map((item) => ({ - ...item, - bridgingText: 'custom bridging text:', - })), - }), - ); + faker.renderComponent('content-pagination', EXAMPLE_CONTENT_PAGINATION_NEXT_ONLY); - const bridgingText = $('.ons-content-pagination__link[rel="next"] .ons-content-pagination__link-divider'); - expect(bridgingText.text().trim()).toBe('custom bridging text:'); - }); + expect(iconsSpy.occurrences[0]).toHaveProperty('iconType', 'arrow-next'); + expect(iconsSpy.occurrences[0]).toHaveProperty('iconSize', 'm'); + }); - it('renders the provided `label`', () => { - const $ = cheerio.load(renderComponent('content-pagination', params)); + describe('previous link', () => { + describe.each([ + ['with only the previous link', EXAMPLE_CONTENT_PAGINATION_PREV_ONLY], + ['with both the previous and next links', EXAMPLE_CONTENT_PAGINATION_BOTH], + ])('%s', (_, params) => { + it('renders a link with the correct URL', () => { + const $ = cheerio.load(renderComponent('content-pagination', params)); + + const prevLink = $('.ons-content-pagination__link[rel="prev"]'); + expect(prevLink.attr('href')).toBe('/guide/overview'); + }); + + it('renders the correct pagination item text', () => { + const $ = cheerio.load(renderComponent('content-pagination', params)); + + const linkText = $('.ons-content-pagination__link[rel="prev"] .ons-content-pagination__link-text'); + expect(linkText.text().trim()).toBe('Previous'); + }); + + it('renders the expected default `bridgingText`', () => { + const $ = cheerio.load(renderComponent('content-pagination', params)); + + const bridgingText = $('.ons-content-pagination__link[rel="prev"] .ons-content-pagination__link-divider'); + expect(bridgingText.text().trim()).toBe('page in this guide is:'); + }); + + it('renders the provided `bridgingText`', () => { + const $ = cheerio.load( + renderComponent('content-pagination', { + contentPaginationItems: params.contentPaginationItems.map((item) => ({ + ...item, + bridgingText: 'custom bridging text:', + })), + }), + ); + + const bridgingText = $('.ons-content-pagination__link[rel="prev"] .ons-content-pagination__link-divider'); + expect(bridgingText.text().trim()).toBe('custom bridging text:'); + }); + + it('renders the provided `label`', () => { + const $ = cheerio.load(renderComponent('content-pagination', params)); + + const labelText = $('.ons-content-pagination__link[rel="prev"] .ons-content-pagination__link-label'); + expect(labelText.text().trim()).toBe('Overview'); + }); + }); + }); - const labelText = $('.ons-content-pagination__link[rel="next"] .ons-content-pagination__link-label'); - expect(labelText.text().trim()).toBe('Who should take part and why'); - }); + describe('next link', () => { + describe.each([ + ['with only the next link', EXAMPLE_CONTENT_PAGINATION_NEXT_ONLY], + ['with both the previous and next links', EXAMPLE_CONTENT_PAGINATION_BOTH], + ])('%s', (_, params) => { + it('renders a link with the correct URL', () => { + const $ = cheerio.load(renderComponent('content-pagination', params)); + + const prevLink = $('.ons-content-pagination__link[rel="next"]'); + expect(prevLink.attr('href')).toBe('/guide/who-and-why'); + }); + + it('renders the correct pagination item text', () => { + const $ = cheerio.load(renderComponent('content-pagination', params)); + + const linkText = $('.ons-content-pagination__link[rel="next"] .ons-content-pagination__link-text'); + expect(linkText.text().trim()).toBe('Next'); + }); + + it('renders the expected default `bridgingText`', () => { + const $ = cheerio.load(renderComponent('content-pagination', params)); + + const bridgingText = $('.ons-content-pagination__link[rel="next"] .ons-content-pagination__link-divider'); + expect(bridgingText.text().trim()).toBe('page in this guide is:'); + }); + + it('renders the provided `bridgingText`', () => { + const $ = cheerio.load( + renderComponent('content-pagination', { + contentPaginationItems: params.contentPaginationItems.map((item) => ({ + ...item, + bridgingText: 'custom bridging text:', + })), + }), + ); + + const bridgingText = $('.ons-content-pagination__link[rel="next"] .ons-content-pagination__link-divider'); + expect(bridgingText.text().trim()).toBe('custom bridging text:'); + }); + + it('renders the provided `label`', () => { + const $ = cheerio.load(renderComponent('content-pagination', params)); + + const labelText = $('.ons-content-pagination__link[rel="next"] .ons-content-pagination__link-label'); + expect(labelText.text().trim()).toBe('Who should take part and why'); + }); + }); }); - }); }); diff --git a/src/components/cookies-banner/_cookies-banner.scss b/src/components/cookies-banner/_cookies-banner.scss index 0e84a06b51..e4fb994e24 100644 --- a/src/components/cookies-banner/_cookies-banner.scss +++ b/src/components/cookies-banner/_cookies-banner.scss @@ -1,30 +1,30 @@ .ons-cookies-banner { - background: var(--ons-color-banner-bg); - display: none; - padding: 1rem 0 1.5rem; + background: var(--ons-color-banner-bg); + display: none; + padding: 1rem 0 1.5rem; - &__title { - color: var(--ons-color-text); - } + &__title { + color: var(--ons-color-text); + } - &__statement { - color: var(--ons-color-text); - word-break: break-word; - p { - margin: 0 0 0.5rem; + &__statement { + color: var(--ons-color-text); + word-break: break-word; + p { + margin: 0 0 0.5rem; - @include mq(xxs, s) { - font-size: 0.9rem; - line-height: 1.4; - } + @include mq(xxs, s) { + font-size: 0.9rem; + line-height: 1.4; + } + } } - } - &__link { - line-height: 2.1rem; - } + &__link { + line-height: 2.1rem; + } - &__btn { - margin: 0 0 0.8rem; - } + &__btn { + margin: 0 0 0.8rem; + } } diff --git a/src/components/cookies-banner/_macro.spec.js b/src/components/cookies-banner/_macro.spec.js index 9ad0d07aca..2bfa65b3f5 100644 --- a/src/components/cookies-banner/_macro.spec.js +++ b/src/components/cookies-banner/_macro.spec.js @@ -6,246 +6,246 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_COOKIES_BANNER_PARAMS = { - ariaLabel: 'Cookies banner override', - serviceName: 'ons.gov.uk override', - statementTitle: 'Cookies on override', - settingsLinkText: 'Cookie settings override', - settingsLinkTextURL: '/cookiesoverride', - statementText: 'Statement override', - acceptButtonText: 'Accept additional cookies override', - rejectButtonText: 'Reject additional cookies override', - preferencesText: 'Text override', - confirmationButtonText: 'Hide override', - contextSuffix: 'the cookie message override', + ariaLabel: 'Cookies banner override', + serviceName: 'ons.gov.uk override', + statementTitle: 'Cookies on override', + settingsLinkText: 'Cookie settings override', + settingsLinkTextURL: '/cookiesoverride', + statementText: 'Statement override', + acceptButtonText: 'Accept additional cookies override', + rejectButtonText: 'Reject additional cookies override', + preferencesText: 'Text override', + confirmationButtonText: 'Hide override', + contextSuffix: 'the cookie message override', }; describe('macro: cookies-banner', () => { - describe.each([ - ['default parameters', {}], - ['provided parameters', EXAMPLE_COOKIES_BANNER_PARAMS], - ])('mode: %s', (_, params) => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('cookies-banner', params)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); + describe.each([ + ['default parameters', {}], + ['provided parameters', EXAMPLE_COOKIES_BANNER_PARAMS], + ])('mode: %s', (_, params) => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('cookies-banner', params)); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); }); - }); - describe('mode: provided parameters', () => { - it('has the correct `aria-label`', () => { - const $ = cheerio.load(renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS)); + describe('mode: provided parameters', () => { + it('has the correct `aria-label`', () => { + const $ = cheerio.load(renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS)); - expect($('.ons-cookies-banner').attr('aria-label')).toBe('Cookies banner override'); - }); + expect($('.ons-cookies-banner').attr('aria-label')).toBe('Cookies banner override'); + }); - describe('initial banner', () => { - it('has `statementTitle` title text', () => { - const $ = cheerio.load(renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS)); + describe('initial banner', () => { + it('has `statementTitle` title text', () => { + const $ = cheerio.load(renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS)); - const statementTitle = $('.ons-cookies-banner__title').text().trim(); - expect(statementTitle).toBe('Cookies on override ons.gov.uk override'); - }); + const statementTitle = $('.ons-cookies-banner__title').text().trim(); + expect(statementTitle).toBe('Cookies on override ons.gov.uk override'); + }); - it('has `statementText` text', () => { - const $ = cheerio.load(renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS)); + it('has `statementText` text', () => { + const $ = cheerio.load(renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS)); - const statementText = $('.ons-cookies-banner__primary .ons-cookies-banner__statement').html().trim(); - expect(statementText).toBe('Statement override'); - }); + const statementText = $('.ons-cookies-banner__primary .ons-cookies-banner__statement').html().trim(); + expect(statementText).toBe('Statement override'); + }); - it('Renders an `accept` button with correct text', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); + it('Renders an `accept` button with correct text', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); - faker.renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS); + faker.renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS); - expect(buttonSpy.occurrences[0].text).toBe('Accept additional cookies override'); - }); + expect(buttonSpy.occurrences[0].text).toBe('Accept additional cookies override'); + }); - it('Renders a `reject` button with correct text', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); + it('Renders a `reject` button with correct text', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); - faker.renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS); + faker.renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS); - expect(buttonSpy.occurrences[1].text).toBe('Reject additional cookies override'); - }); + expect(buttonSpy.occurrences[1].text).toBe('Reject additional cookies override'); + }); - it('Renders a link with text', () => { - const $ = cheerio.load(renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS)); + it('Renders a link with text', () => { + const $ = cheerio.load(renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS)); - const linkText = $('.ons-cookies-banner__link').text().trim(); - expect(linkText).toBe('Cookie settings override'); - }); + const linkText = $('.ons-cookies-banner__link').text().trim(); + expect(linkText).toBe('Cookie settings override'); + }); - it('Renders a link with url', () => { - const $ = cheerio.load(renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS)); + it('Renders a link with url', () => { + const $ = cheerio.load(renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS)); - const linkText = $('.ons-cookies-banner__link').attr('href'); - expect(linkText).toBe('/cookiesoverride'); - }); - }); + const linkText = $('.ons-cookies-banner__link').attr('href'); + expect(linkText).toBe('/cookiesoverride'); + }); + }); - describe('confirmation banner', () => { - it('has `preferencesText` text', () => { - const $ = cheerio.load(renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS)); + describe('confirmation banner', () => { + it('has `preferencesText` text', () => { + const $ = cheerio.load(renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS)); - const preferencesText = $('.ons-cookies-banner__confirmation .ons-cookies-banner__preferences-text').html().trim(); - expect(preferencesText).toBe('Text override'); - }); + const preferencesText = $('.ons-cookies-banner__confirmation .ons-cookies-banner__preferences-text').html().trim(); + expect(preferencesText).toBe('Text override'); + }); - it('renders a button with text', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); + it('renders a button with text', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); - faker.renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS); + faker.renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS); - expect(buttonSpy.occurrences[2].text).toBe('Hide override'); - }); + expect(buttonSpy.occurrences[2].text).toBe('Hide override'); + }); - it('has the correct `contextSuffix` for `buttonContext`', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); + it('has the correct `contextSuffix` for `buttonContext`', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); - faker.renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS); + faker.renderComponent('cookies-banner', EXAMPLE_COOKIES_BANNER_PARAMS); - expect(buttonSpy.occurrences[2].buttonContext).toBe('the cookie message override'); - }); + expect(buttonSpy.occurrences[2].buttonContext).toBe('the cookie message override'); + }); + }); }); - }); - describe('mode: default parameters', () => { - it('has the correct `aria-label`', () => { - const $ = cheerio.load(renderComponent('cookies-banner', {})); + describe('mode: default parameters', () => { + it('has the correct `aria-label`', () => { + const $ = cheerio.load(renderComponent('cookies-banner', {})); - expect($('.ons-cookies-banner').attr('aria-label')).toBe('Cookies banner'); - }); + expect($('.ons-cookies-banner').attr('aria-label')).toBe('Cookies banner'); + }); - describe('initial banner', () => { - it('has `statementTitle` title text', () => { - const $ = cheerio.load(renderComponent('cookies-banner', {})); + describe('initial banner', () => { + it('has `statementTitle` title text', () => { + const $ = cheerio.load(renderComponent('cookies-banner', {})); - const statementTitle = $('.ons-cookies-banner__title').text().trim(); - expect(statementTitle).toBe('Cookies on ons.gov.uk'); - }); + const statementTitle = $('.ons-cookies-banner__title').text().trim(); + expect(statementTitle).toBe('Cookies on ons.gov.uk'); + }); - it('has `statementText` text', () => { - const $ = cheerio.load(renderComponent('cookies-banner', {})); + it('has `statementText` text', () => { + const $ = cheerio.load(renderComponent('cookies-banner', {})); - const statementText = $('.ons-cookies-banner__primary .ons-cookies-banner__statement').html().trim(); - expect(statementText).toBe( - '

    Cookies are small files stored on your device when you visit a website. We use some essential cookies to make this website work.

    We would like to set additional cookies to remember your settings and understand how you use the site. This helps us to improve our services.

    ', - ); - }); + const statementText = $('.ons-cookies-banner__primary .ons-cookies-banner__statement').html().trim(); + expect(statementText).toBe( + '

    Cookies are small files stored on your device when you visit a website. We use some essential cookies to make this website work.

    We would like to set additional cookies to remember your settings and understand how you use the site. This helps us to improve our services.

    ', + ); + }); - it('Renders an `accept` button with correct text', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); + it('Renders an `accept` button with correct text', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); - faker.renderComponent('cookies-banner', {}); + faker.renderComponent('cookies-banner', {}); - expect(buttonSpy.occurrences[0].text).toBe('Accept additional cookies'); - }); + expect(buttonSpy.occurrences[0].text).toBe('Accept additional cookies'); + }); - it('Renders a `reject` button with correct text', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); + it('Renders a `reject` button with correct text', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); - faker.renderComponent('cookies-banner', {}); + faker.renderComponent('cookies-banner', {}); - expect(buttonSpy.occurrences[1].text).toBe('Reject additional cookies'); - }); + expect(buttonSpy.occurrences[1].text).toBe('Reject additional cookies'); + }); - it('Renders a link with text', () => { - const $ = cheerio.load(renderComponent('cookies-banner', {})); + it('Renders a link with text', () => { + const $ = cheerio.load(renderComponent('cookies-banner', {})); - const linkText = $('.ons-cookies-banner__link').text().trim(); - expect(linkText).toBe('View cookies'); - }); + const linkText = $('.ons-cookies-banner__link').text().trim(); + expect(linkText).toBe('View cookies'); + }); - it('Renders a link with url', () => { - const $ = cheerio.load(renderComponent('cookies-banner', {})); + it('Renders a link with url', () => { + const $ = cheerio.load(renderComponent('cookies-banner', {})); - const linkText = $('.ons-cookies-banner__link').attr('href'); - expect(linkText).toBe('/cookies'); - }); + const linkText = $('.ons-cookies-banner__link').attr('href'); + expect(linkText).toBe('/cookies'); + }); - it('has `container--wide` class when `wide` is true', () => { - const $ = cheerio.load( - renderComponent('cookies-banner', { - wide: true, - }), - ); + it('has `container--wide` class when `wide` is true', () => { + const $ = cheerio.load( + renderComponent('cookies-banner', { + wide: true, + }), + ); - expect($('.ons-container.ons-cookies-banner__primary').hasClass('ons-container--wide')).toBe(true); - }); + expect($('.ons-container.ons-cookies-banner__primary').hasClass('ons-container--wide')).toBe(true); + }); - it('has `container--full-width` class when `fullWidth` is true', () => { - const $ = cheerio.load( - renderComponent('cookies-banner', { - fullWidth: true, - }), - ); + it('has `container--full-width` class when `fullWidth` is true', () => { + const $ = cheerio.load( + renderComponent('cookies-banner', { + fullWidth: true, + }), + ); - expect($('.ons-container.ons-cookies-banner__primary').hasClass('ons-container--full-width')).toBe(true); - }); - }); + expect($('.ons-container.ons-cookies-banner__primary').hasClass('ons-container--full-width')).toBe(true); + }); + }); - describe('confirmation banner', () => { - it('has `preferencesText` text', () => { - const $ = cheerio.load(renderComponent('cookies-banner', {})); + describe('confirmation banner', () => { + it('has `preferencesText` text', () => { + const $ = cheerio.load(renderComponent('cookies-banner', {})); - const preferencesText = $('.ons-cookies-banner__confirmation .ons-cookies-banner__preferences-text').html().trim(); - expect(preferencesText).toBe('You can change your cookie preferences at any time.'); - }); + const preferencesText = $('.ons-cookies-banner__confirmation .ons-cookies-banner__preferences-text').html().trim(); + expect(preferencesText).toBe('You can change your cookie preferences at any time.'); + }); - it('renders a button with text', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); + it('renders a button with text', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); - faker.renderComponent('cookies-banner', {}); + faker.renderComponent('cookies-banner', {}); - expect(buttonSpy.occurrences[2].text).toBe('Hide'); - }); + expect(buttonSpy.occurrences[2].text).toBe('Hide'); + }); - it('has the correct `confirmationButtonTextAria` for `buttonContext`', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); + it('has the correct `confirmationButtonTextAria` for `buttonContext`', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); - faker.renderComponent('cookies-banner', {}); + faker.renderComponent('cookies-banner', {}); - expect(buttonSpy.occurrences[2].buttonContext).toBe('cookie message'); - }); + expect(buttonSpy.occurrences[2].buttonContext).toBe('cookie message'); + }); - it('has `container--wide` class when `wide` is true', () => { - const $ = cheerio.load( - renderComponent('cookies-banner', { - wide: true, - }), - ); + it('has `container--wide` class when `wide` is true', () => { + const $ = cheerio.load( + renderComponent('cookies-banner', { + wide: true, + }), + ); - expect($('.ons-container.ons-cookies-banner__confirmation').hasClass('ons-container--wide')).toBe(true); - }); + expect($('.ons-container.ons-cookies-banner__confirmation').hasClass('ons-container--wide')).toBe(true); + }); - it('has `container--full-width` class when `fullWidth` is true', () => { - const $ = cheerio.load( - renderComponent('cookies-banner', { - fullWidth: true, - }), - ); + it('has `container--full-width` class when `fullWidth` is true', () => { + const $ = cheerio.load( + renderComponent('cookies-banner', { + fullWidth: true, + }), + ); - expect($('.ons-container.ons-cookies-banner__confirmation').hasClass('ons-container--full-width')).toBe(true); - }); + expect($('.ons-container.ons-cookies-banner__confirmation').hasClass('ons-container--full-width')).toBe(true); + }); + }); }); - }); - describe('mode: Welsh language', () => { - it('has the welsh version of default values', () => { - const $ = cheerio.load(renderComponent('cookies-banner', { lang: 'cy' })); + describe('mode: Welsh language', () => { + it('has the welsh version of default values', () => { + const $ = cheerio.load(renderComponent('cookies-banner', { lang: 'cy' })); - const statementTitle = $('.ons-cookies-banner__title').text().trim(); - expect(statementTitle).toBe('Cwcis ar ons.gov.uk'); + const statementTitle = $('.ons-cookies-banner__title').text().trim(); + expect(statementTitle).toBe('Cwcis ar ons.gov.uk'); + }); }); - }); }); diff --git a/src/components/cookies-banner/cookies-banner.dom.js b/src/components/cookies-banner/cookies-banner.dom.js index f53f365307..7de3981e7b 100644 --- a/src/components/cookies-banner/cookies-banner.dom.js +++ b/src/components/cookies-banner/cookies-banner.dom.js @@ -1,14 +1,14 @@ import domready from '../../js/domready'; async function cookiesBanner() { - const cookiesBanner = [...document.querySelectorAll('.ons-cookies-banner')]; + const cookiesBanner = [...document.querySelectorAll('.ons-cookies-banner')]; - if (cookiesBanner.length) { - const CookiesBanner = (await import('./cookies-banner')).default; - cookiesBanner.forEach((banner) => { - new CookiesBanner(banner); - }); - } + if (cookiesBanner.length) { + const CookiesBanner = (await import('./cookies-banner')).default; + cookiesBanner.forEach((banner) => { + new CookiesBanner(banner); + }); + } } domready(cookiesBanner); diff --git a/src/components/cookies-banner/cookies-banner.js b/src/components/cookies-banner/cookies-banner.js index 88db169f4c..a1436cd975 100644 --- a/src/components/cookies-banner/cookies-banner.js +++ b/src/components/cookies-banner/cookies-banner.js @@ -1,97 +1,97 @@ import { approveAllCookieTypes, cookie, setConsentCookie, setDefaultConsentCookie } from '../../js/cookies-functions'; export default class CookiesBanner { - constructor(component) { - this.component = component; - this.primaryBanner = this.component.querySelector('.ons-cookies-banner__primary'); - this.confirmBanner = this.component.querySelector('.ons-cookies-banner__confirmation'); - this.acceptButton = this.component.querySelector('.ons-js-accept-cookies'); - this.rejectButton = this.component.querySelector('.ons-js-reject-cookies'); - this.hideButton = this.component.querySelector('.ons-js-hide-button'); - this.acceptedText = this.component.querySelector('.ons-js-accepted-text'); - this.rejectedText = this.component.querySelector('.ons-js-rejected-text'); - this.setupCookiesEvents(); - } - - setupCookiesEvents() { - this.acceptButton.addEventListener('click', this.setCookiesConsent.bind(this)); - this.rejectButton.addEventListener('click', this.setCookiesConsent.bind(this)); - this.hideButton.addEventListener('click', this.hideConfirmBanner.bind(this)); + constructor(component) { + this.component = component; + this.primaryBanner = this.component.querySelector('.ons-cookies-banner__primary'); + this.confirmBanner = this.component.querySelector('.ons-cookies-banner__confirmation'); + this.acceptButton = this.component.querySelector('.ons-js-accept-cookies'); + this.rejectButton = this.component.querySelector('.ons-js-reject-cookies'); + this.hideButton = this.component.querySelector('.ons-js-hide-button'); + this.acceptedText = this.component.querySelector('.ons-js-accepted-text'); + this.rejectedText = this.component.querySelector('.ons-js-rejected-text'); + this.setupCookiesEvents(); + } - this.showCookiesMessage(); - } + setupCookiesEvents() { + this.acceptButton.addEventListener('click', this.setCookiesConsent.bind(this)); + this.rejectButton.addEventListener('click', this.setCookiesConsent.bind(this)); + this.hideButton.addEventListener('click', this.hideConfirmBanner.bind(this)); - showCookiesMessage() { - const displayCookiesBanner = this.component && cookie('ons_cookie_message_displayed') !== 'true'; - let policy = cookie('ons_cookie_policy'); - if (policy) { - setConsentCookie(JSON.parse(policy.replace(/'/g, '"'))); + this.showCookiesMessage(); } - if (displayCookiesBanner) { - this.component.style.display = 'block'; - if (!cookie('ons_cookie_policy')) { - setDefaultConsentCookie(); - } + showCookiesMessage() { + const displayCookiesBanner = this.component && cookie('ons_cookie_message_displayed') !== 'true'; + let policy = cookie('ons_cookie_policy'); + if (policy) { + setConsentCookie(JSON.parse(policy.replace(/'/g, '"'))); + } + if (displayCookiesBanner) { + this.component.style.display = 'block'; + + if (!cookie('ons_cookie_policy')) { + setDefaultConsentCookie(); + } + } } - } - setCookiesConsent(event) { - event.preventDefault(); - let actionText; - const action = event.target.getAttribute('data-button'); + setCookiesConsent(event) { + event.preventDefault(); + let actionText; + const action = event.target.getAttribute('data-button'); - if (action == 'accept') { - this.acceptCookies(); - actionText = this.acceptedText; - } else if (action == 'reject') { - setDefaultConsentCookie(); - actionText = this.rejectedText; - } + if (action == 'accept') { + this.acceptCookies(); + actionText = this.acceptedText; + } else if (action == 'reject') { + setDefaultConsentCookie(); + actionText = this.rejectedText; + } - this.hidePrimaryCookiesBanner(actionText); - this.checkPage(action); - cookie('ons_cookie_message_displayed', 'true', { days: 365 }); - } + this.hidePrimaryCookiesBanner(actionText); + this.checkPage(action); + cookie('ons_cookie_message_displayed', 'true', { days: 365 }); + } - acceptCookies() { - approveAllCookieTypes(); - if (typeof loadGTM != 'undefined') { - loadGTM(); + acceptCookies() { + approveAllCookieTypes(); + if (typeof loadGTM != 'undefined') { + loadGTM(); + } } - } - checkPage(action) { - const isOnSettingsPage = document.querySelector('[data-module="cookie-settings"]'); - if (isOnSettingsPage) { - this.updateRadios(action); + checkPage(action) { + const isOnSettingsPage = document.querySelector('[data-module="cookie-settings"]'); + if (isOnSettingsPage) { + this.updateRadios(action); + } } - } - updateRadios(action) { - const radios = [...document.querySelectorAll('.ons-js-radio')]; - radios.forEach((radio) => { - if (action == 'reject') { - radio.value == 'off' ? (radio.checked = true) : (radio.checked = false); - } else if (action == 'accept') { - radio.value == 'off' ? (radio.checked = false) : (radio.checked = true); - } - }); - } + updateRadios(action) { + const radios = [...document.querySelectorAll('.ons-js-radio')]; + radios.forEach((radio) => { + if (action == 'reject') { + radio.value == 'off' ? (radio.checked = true) : (radio.checked = false); + } else if (action == 'accept') { + radio.value == 'off' ? (radio.checked = false) : (radio.checked = true); + } + }); + } - hidePrimaryCookiesBanner(text) { - if (this.component) { - this.primaryBanner.style.display = 'none'; - this.confirmBanner.classList.remove('ons-u-d-no'); - this.confirmBanner.setAttribute('aria-live', 'polite'); - this.confirmBanner.setAttribute('role', 'status'); - text.classList.remove('ons-u-d-no'); + hidePrimaryCookiesBanner(text) { + if (this.component) { + this.primaryBanner.style.display = 'none'; + this.confirmBanner.classList.remove('ons-u-d-no'); + this.confirmBanner.setAttribute('aria-live', 'polite'); + this.confirmBanner.setAttribute('role', 'status'); + text.classList.remove('ons-u-d-no'); + } } - } - hideConfirmBanner() { - if (this.component) { - this.component.style.display = 'none'; + hideConfirmBanner() { + if (this.component) { + this.component.style.display = 'none'; + } } - } } diff --git a/src/components/cookies-banner/cookies-banner.spec.js b/src/components/cookies-banner/cookies-banner.spec.js index 0d0dc14906..7fdd4e262e 100644 --- a/src/components/cookies-banner/cookies-banner.spec.js +++ b/src/components/cookies-banner/cookies-banner.spec.js @@ -3,92 +3,96 @@ import { renderComponent, setTestPage } from '../../tests/helpers/rendering'; const EXAMPLE_COOKIES_BANNER_PAGE = renderComponent('cookies-banner', {}); describe('script: cookies-banner', () => { - beforeEach(async () => { - const client = await page.target().createCDPSession(); - await client.send('Network.clearBrowserCookies'); - }); - - it('should show the cookie banner', async () => { - await setTestPage('/test', EXAMPLE_COOKIES_BANNER_PAGE); - - const displayStyle = await page.$eval('.ons-cookies-banner', (node) => window.getComputedStyle(node).getPropertyValue('display')); - expect(displayStyle).toBe('block'); - }); - describe.each([ - ['accepting cookies', 'accept', true], - ['rejecting cookies', 'reject', false], - ])('action: %s', (_, action, value) => { - it(`sets all cookies to ${value} when ${_}`, async () => { - await setTestPage('/test', EXAMPLE_COOKIES_BANNER_PAGE); - - await page.click(`.ons-js-${action}-cookies`); - - const cookies = await page.cookies(); - const ons_cookie_policy = cookies.find((cookie) => cookie.name === 'ons_cookie_policy'); - const policy = JSON.parse(ons_cookie_policy.value.replace(/'/g, '"')); - - expect(policy).toEqual({ - essential: true, - settings: value, - usage: value, - campaigns: value, - }); + beforeEach(async () => { + const client = await page.target().createCDPSession(); + await client.send('Network.clearBrowserCookies'); }); - it(`sets seen cookie message when ${_}`, async () => { - await setTestPage('/test', EXAMPLE_COOKIES_BANNER_PAGE); + it('should show the cookie banner', async () => { + await setTestPage('/test', EXAMPLE_COOKIES_BANNER_PAGE); - await page.click(`.ons-js-${action}-cookies`); + const displayStyle = await page.$eval('.ons-cookies-banner', (node) => window.getComputedStyle(node).getPropertyValue('display')); + expect(displayStyle).toBe('block'); + }); + describe.each([ + ['accepting cookies', 'accept', true], + ['rejecting cookies', 'reject', false], + ])('action: %s', (_, action, value) => { + it(`sets all cookies to ${value} when ${_}`, async () => { + await setTestPage('/test', EXAMPLE_COOKIES_BANNER_PAGE); - const cookies = await page.cookies(); - const ons_cookie_message_displayed = cookies.find((cookie) => cookie.name === 'ons_cookie_message_displayed'); + await page.click(`.ons-js-${action}-cookies`); - expect(ons_cookie_message_displayed.value).toBe('true'); - }); + const cookies = await page.cookies(); + const ons_cookie_policy = cookies.find((cookie) => cookie.name === 'ons_cookie_policy'); + const policy = JSON.parse(ons_cookie_policy.value.replace(/'/g, '"')); - it(`should hide the primary message when pressing the ${action} button`, async () => { - await setTestPage('/test', EXAMPLE_COOKIES_BANNER_PAGE); + expect(policy).toEqual({ + essential: true, + settings: value, + usage: value, + campaigns: value, + }); + }); - await page.click(`.ons-js-${action}-cookies`); + it(`sets seen cookie message when ${_}`, async () => { + await setTestPage('/test', EXAMPLE_COOKIES_BANNER_PAGE); - const displayStyle = await page.$eval('.ons-cookies-banner__primary', (node) => - window.getComputedStyle(node).getPropertyValue('display'), - ); - expect(displayStyle).toBe('none'); - }); + await page.click(`.ons-js-${action}-cookies`); + + const cookies = await page.cookies(); + const ons_cookie_message_displayed = cookies.find((cookie) => cookie.name === 'ons_cookie_message_displayed'); + + expect(ons_cookie_message_displayed.value).toBe('true'); + }); + + it(`should hide the primary message when pressing the ${action} button`, async () => { + await setTestPage('/test', EXAMPLE_COOKIES_BANNER_PAGE); + + await page.click(`.ons-js-${action}-cookies`); + + const displayStyle = await page.$eval('.ons-cookies-banner__primary', (node) => + window.getComputedStyle(node).getPropertyValue('display'), + ); + expect(displayStyle).toBe('none'); + }); - it(`should show the secondary message when pressing the ${action} button`, async () => { - await setTestPage('/test', EXAMPLE_COOKIES_BANNER_PAGE); + it(`should show the secondary message when pressing the ${action} button`, async () => { + await setTestPage('/test', EXAMPLE_COOKIES_BANNER_PAGE); - await page.click(`.ons-js-${action}-cookies`); + await page.click(`.ons-js-${action}-cookies`); - const displayStyle = await page.$eval('.ons-cookies-banner__confirmation', (node) => - window.getComputedStyle(node).getPropertyValue('display'), - ); - expect(displayStyle).not.toBe('none'); + const displayStyle = await page.$eval('.ons-cookies-banner__confirmation', (node) => + window.getComputedStyle(node).getPropertyValue('display'), + ); + expect(displayStyle).not.toBe('none'); + }); }); - }); - describe('confirmation banner', () => { - it('should hide the confirmation message when pressing the hide button', async () => { - await setTestPage('/test', EXAMPLE_COOKIES_BANNER_PAGE); + describe('confirmation banner', () => { + it('should hide the confirmation message when pressing the hide button', async () => { + await setTestPage('/test', EXAMPLE_COOKIES_BANNER_PAGE); - await page.click('.ons-js-accept-cookies'); - await page.click('.ons-js-hide-button'); + await page.click('.ons-js-accept-cookies'); + await page.click('.ons-js-hide-button'); - const displayStyle = await page.$eval('.ons-cookies-banner', (node) => window.getComputedStyle(node).getPropertyValue('display')); - expect(displayStyle).toBe('none'); + const displayStyle = await page.$eval('.ons-cookies-banner', (node) => + window.getComputedStyle(node).getPropertyValue('display'), + ); + expect(displayStyle).toBe('none'); + }); }); - }); - describe('cookie preferences confirmed', () => { - it('does not show the banner if user has acknowledged the banner previously and consent cookie is present', async () => { - await page.setCookie({ name: 'ons_cookie_message_displayed', value: 'true' }); + describe('cookie preferences confirmed', () => { + it('does not show the banner if user has acknowledged the banner previously and consent cookie is present', async () => { + await page.setCookie({ name: 'ons_cookie_message_displayed', value: 'true' }); - await setTestPage('/test', EXAMPLE_COOKIES_BANNER_PAGE); + await setTestPage('/test', EXAMPLE_COOKIES_BANNER_PAGE); - const displayStyle = await page.$eval('.ons-cookies-banner', (node) => window.getComputedStyle(node).getPropertyValue('display')); - expect(displayStyle).toBe('none'); + const displayStyle = await page.$eval('.ons-cookies-banner', (node) => + window.getComputedStyle(node).getPropertyValue('display'), + ); + expect(displayStyle).toBe('none'); + }); }); - }); }); diff --git a/src/components/date-input/_macro.spec.js b/src/components/date-input/_macro.spec.js index 1db0f77a73..a453807f6d 100644 --- a/src/components/date-input/_macro.spec.js +++ b/src/components/date-input/_macro.spec.js @@ -6,405 +6,405 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_DATE_INPUT_BASE = { - id: 'date', - legendOrLabel: 'Date of birth', - description: 'For example, 31 3 1980', + id: 'date', + legendOrLabel: 'Date of birth', + description: 'For example, 31 3 1980', }; const EXAMPLE_DATE_INPUT_BASE_WITH_ERROR = { - id: 'date', - legendOrLabel: 'Date of birth', - description: 'For example, 31 3 1980', - error: { - text: 'Enter a date that is after 1 January 2019', - }, + id: 'date', + legendOrLabel: 'Date of birth', + description: 'For example, 31 3 1980', + error: { + text: 'Enter a date that is after 1 January 2019', + }, }; const EXAMPLE_DAY_FIELD = { - day: { - label: { - text: 'Day', - description: 'The day', - }, - value: 'Day', - name: 'day', - attributes: { - autocomplete: 'bday-day', + day: { + label: { + text: 'Day', + description: 'The day', + }, + value: 'Day', + name: 'day', + attributes: { + autocomplete: 'bday-day', + }, }, - }, }; const EXAMPLE_MONTH_FIELD = { - month: { - label: { - text: 'Month', - description: 'The month', - }, - value: 'Month', - name: 'month', - attributes: { - autocomplete: 'bday-month', + month: { + label: { + text: 'Month', + description: 'The month', + }, + value: 'Month', + name: 'month', + attributes: { + autocomplete: 'bday-month', + }, }, - }, }; const EXAMPLE_YEAR_FIELD = { - year: { - label: { - text: 'Year', - description: 'The year', - }, - value: 'Year', - name: 'year', - attributes: { - autocomplete: 'bday-year', + year: { + label: { + text: 'Year', + description: 'The year', + }, + value: 'Year', + name: 'year', + attributes: { + autocomplete: 'bday-year', + }, }, - }, }; const EXAMPLE_DAY_FIELD_WITH_ERROR = { - day: { - label: { - text: 'Day', - description: 'The day', - }, - value: 'Day', - error: true, - name: 'day', - attributes: { - autocomplete: 'bday-day', + day: { + label: { + text: 'Day', + description: 'The day', + }, + value: 'Day', + error: true, + name: 'day', + attributes: { + autocomplete: 'bday-day', + }, }, - }, }; const EXAMPLE_MONTH_FIELD_WITH_ERROR = { - month: { - label: { - text: 'Month', - description: 'The month', - }, - value: 'Month', - error: true, - name: 'month', - attributes: { - autocomplete: 'bday-month', + month: { + label: { + text: 'Month', + description: 'The month', + }, + value: 'Month', + error: true, + name: 'month', + attributes: { + autocomplete: 'bday-month', + }, }, - }, }; const EXAMPLE_YEAR_FIELD_WITH_ERROR = { - year: { - label: { - text: 'Year', - description: 'The year', - }, - value: 'Year', - error: true, - name: 'year', - attributes: { - autocomplete: 'bday-year', + year: { + label: { + text: 'Year', + description: 'The year', + }, + value: 'Year', + error: true, + name: 'year', + attributes: { + autocomplete: 'bday-year', + }, }, - }, }; const EXAMPLE_YEAR_FIELD_WITH_ERROR_FALSE = { - year: { - label: { - text: 'Year', - description: 'The year', - }, - value: 'Year', - name: 'year', - error: false, - attributes: { - autocomplete: 'bday-year', + year: { + label: { + text: 'Year', + description: 'The year', + }, + value: 'Year', + name: 'year', + error: false, + attributes: { + autocomplete: 'bday-year', + }, }, - }, }; const EXAMPLE_DATE_SINGLE_FIELD = { - ...EXAMPLE_DATE_INPUT_BASE, - ...EXAMPLE_YEAR_FIELD, + ...EXAMPLE_DATE_INPUT_BASE, + ...EXAMPLE_YEAR_FIELD, }; const EXAMPLE_DATE_MULTIPLE_FIELDS = { - ...EXAMPLE_DATE_INPUT_BASE, - ...EXAMPLE_DAY_FIELD, - ...EXAMPLE_MONTH_FIELD, - ...EXAMPLE_YEAR_FIELD, + ...EXAMPLE_DATE_INPUT_BASE, + ...EXAMPLE_DAY_FIELD, + ...EXAMPLE_MONTH_FIELD, + ...EXAMPLE_YEAR_FIELD, }; const EXAMPLE_DATE_MULTIPLE_FIELDS_WITH_SINGLE_ERROR = { - ...EXAMPLE_DATE_INPUT_BASE_WITH_ERROR, - ...EXAMPLE_DAY_FIELD, - ...EXAMPLE_MONTH_FIELD, - ...EXAMPLE_YEAR_FIELD_WITH_ERROR, + ...EXAMPLE_DATE_INPUT_BASE_WITH_ERROR, + ...EXAMPLE_DAY_FIELD, + ...EXAMPLE_MONTH_FIELD, + ...EXAMPLE_YEAR_FIELD_WITH_ERROR, }; const EXAMPLE_DATE_MULTIPLE_FIELDS_WITH_ERROR = { - ...EXAMPLE_DATE_INPUT_BASE_WITH_ERROR, - ...EXAMPLE_DAY_FIELD_WITH_ERROR, - ...EXAMPLE_MONTH_FIELD_WITH_ERROR, - ...EXAMPLE_YEAR_FIELD_WITH_ERROR, + ...EXAMPLE_DATE_INPUT_BASE_WITH_ERROR, + ...EXAMPLE_DAY_FIELD_WITH_ERROR, + ...EXAMPLE_MONTH_FIELD_WITH_ERROR, + ...EXAMPLE_YEAR_FIELD_WITH_ERROR, }; const EXAMPLE_DATE_SINGLE_FIELD_WITH_ERROR_FALSE = { - ...EXAMPLE_DATE_INPUT_BASE, - ...EXAMPLE_YEAR_FIELD_WITH_ERROR_FALSE, + ...EXAMPLE_DATE_INPUT_BASE, + ...EXAMPLE_YEAR_FIELD_WITH_ERROR_FALSE, }; describe('macro: date input', () => { - describe('mode: multiple fields', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the expected `input` output for the `day` field', () => { - const faker = templateFaker(); - const inputSpy = faker.spy('input'); - - faker.renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS); - - expect(inputSpy.occurrences[0]).toEqual({ - id: 'date-day', - type: 'number', - name: 'day', - width: '2', - maxLength: 2, - classes: '', - error: '', - label: { - text: 'Day', - description: 'The day', - id: 'date-day-label', - }, - value: 'Day', - attributes: { - autocomplete: 'bday-day', - }, - }); - }); - - it('has the expected `input` output for the `month` field', () => { - const faker = templateFaker(); - const inputSpy = faker.spy('input'); - - faker.renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS); - - expect(inputSpy.occurrences[1]).toEqual({ - id: 'date-month', - type: 'number', - name: 'month', - width: '2', - maxLength: 2, - classes: '', - error: '', - label: { - text: 'Month', - description: 'The month', - id: 'date-month-label', - }, - value: 'Month', - attributes: { - autocomplete: 'bday-month', - }, - }); - }); - - it('has the expected `input` output for the `year` field', () => { - const faker = templateFaker(); - const inputSpy = faker.spy('input'); - - faker.renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS); - - expect(inputSpy.occurrences[2]).toEqual({ - id: 'date-year', - type: 'number', - name: 'year', - width: '4', - maxLength: 4, - classes: '', - error: '', - label: { - text: 'Year', - description: 'The year', - id: 'date-year-label', - }, - value: 'Year', - attributes: { - autocomplete: 'bday-year', - }, - }); - }); - - it('has the group class div', () => { - const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS)); - const div = $('.ons-field:first-child').parent(); - expect($(div).hasClass('ons-field-group')).toBe(true); - }); - - it('has the correct number of inputs', () => { - const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS)); - const $inputs = $('.ons-input'); - expect($inputs.length).toBe(3); - }); - - it('has the expected `fieldset` output', () => { - const faker = templateFaker(); - const fieldsetSpy = faker.spy('fieldset'); - - faker.renderComponent('date-input', { - ...EXAMPLE_DATE_MULTIPLE_FIELDS, - legendClasses: 'custom-legend-class', - classes: 'custom-class', - dontWrap: true, - legendIsQuestionTitle: true, - error: false, - }); - - expect(fieldsetSpy.occurrences[0]).toEqual({ - id: 'date', - legend: 'Date of birth', - description: 'For example, 31 3 1980', - legendClasses: 'custom-legend-class', - classes: 'custom-class', - dontWrap: true, - legendIsQuestionTitle: true, - error: false, - }); - }); - }); - - describe('mode: multiple fields with mutually exclusive', () => { - it('has the correct class on each input', async () => { - const $ = cheerio.load(renderComponent('date-input', { ...EXAMPLE_DATE_MULTIPLE_FIELDS, mutuallyExclusive: {} })); - - const exclusiveClassCount = $('.ons-js-exclusive-group-item').length; - expect(exclusiveClassCount).toBe(3); - }); - - it('has the expected `mutuallyExclusive` output', () => { - const faker = templateFaker(); - const mutuallyExclusiveSpy = faker.spy('mutually-exclusive'); - - faker.renderComponent('date-input', { - ...EXAMPLE_DATE_MULTIPLE_FIELDS, - legendClasses: 'custom-legend-class', - classes: 'custom-class', - dontWrap: true, - legendIsQuestionTitle: true, - error: false, - mutuallyExclusive: { - exclusiveOptions: {}, - or: 'Or', - deselectMessage: 'Deselect message', - deselectGroupAdjective: 'Deselect group adjective', - deselectExclusiveOptionAdjective: 'Deselect checkbox adjective', - }, - }); - - expect(mutuallyExclusiveSpy.occurrences[0]).toEqual({ - id: 'date', - legend: 'Date of birth', - description: 'For example, 31 3 1980', - legendClasses: 'custom-legend-class', - classes: 'custom-class', - dontWrap: true, - legendIsQuestionTitle: true, - error: false, - exclusiveOptions: {}, - or: 'Or', - deselectMessage: 'Deselect message', - deselectGroupAdjective: 'Deselect group adjective', - deselectExclusiveOptionAdjective: 'Deselect checkbox adjective', - }); - }); - }); - - describe('mode: single field', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_SINGLE_FIELD)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the expected `input` output for the field', () => { - const faker = templateFaker(); - const inputSpy = faker.spy('input'); - - faker.renderComponent('date-input', EXAMPLE_DATE_SINGLE_FIELD); - - expect(inputSpy.occurrences[0]).toEqual({ - id: 'date-year', - type: 'number', - name: 'year', - width: '4', - maxLength: 4, - classes: '', - label: { - text: 'Date of birth', - description: 'For example, 31 3 1980', - id: 'date-year-label', - }, - value: 'Year', - attributes: { - autocomplete: 'bday-year', - }, - }); + describe('mode: multiple fields', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS)); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has the expected `input` output for the `day` field', () => { + const faker = templateFaker(); + const inputSpy = faker.spy('input'); + + faker.renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS); + + expect(inputSpy.occurrences[0]).toEqual({ + id: 'date-day', + type: 'number', + name: 'day', + width: '2', + maxLength: 2, + classes: '', + error: '', + label: { + text: 'Day', + description: 'The day', + id: 'date-day-label', + }, + value: 'Day', + attributes: { + autocomplete: 'bday-day', + }, + }); + }); + + it('has the expected `input` output for the `month` field', () => { + const faker = templateFaker(); + const inputSpy = faker.spy('input'); + + faker.renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS); + + expect(inputSpy.occurrences[1]).toEqual({ + id: 'date-month', + type: 'number', + name: 'month', + width: '2', + maxLength: 2, + classes: '', + error: '', + label: { + text: 'Month', + description: 'The month', + id: 'date-month-label', + }, + value: 'Month', + attributes: { + autocomplete: 'bday-month', + }, + }); + }); + + it('has the expected `input` output for the `year` field', () => { + const faker = templateFaker(); + const inputSpy = faker.spy('input'); + + faker.renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS); + + expect(inputSpy.occurrences[2]).toEqual({ + id: 'date-year', + type: 'number', + name: 'year', + width: '4', + maxLength: 4, + classes: '', + error: '', + label: { + text: 'Year', + description: 'The year', + id: 'date-year-label', + }, + value: 'Year', + attributes: { + autocomplete: 'bday-year', + }, + }); + }); + + it('has the group class div', () => { + const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS)); + const div = $('.ons-field:first-child').parent(); + expect($(div).hasClass('ons-field-group')).toBe(true); + }); + + it('has the correct number of inputs', () => { + const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS)); + const $inputs = $('.ons-input'); + expect($inputs.length).toBe(3); + }); + + it('has the expected `fieldset` output', () => { + const faker = templateFaker(); + const fieldsetSpy = faker.spy('fieldset'); + + faker.renderComponent('date-input', { + ...EXAMPLE_DATE_MULTIPLE_FIELDS, + legendClasses: 'custom-legend-class', + classes: 'custom-class', + dontWrap: true, + legendIsQuestionTitle: true, + error: false, + }); + + expect(fieldsetSpy.occurrences[0]).toEqual({ + id: 'date', + legend: 'Date of birth', + description: 'For example, 31 3 1980', + legendClasses: 'custom-legend-class', + classes: 'custom-class', + dontWrap: true, + legendIsQuestionTitle: true, + error: false, + }); + }); }); - it('has the correct number of inputs', () => { - const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_SINGLE_FIELD)); - const $inputs = $('.ons-input'); - expect($inputs.length).toBe(1); + describe('mode: multiple fields with mutually exclusive', () => { + it('has the correct class on each input', async () => { + const $ = cheerio.load(renderComponent('date-input', { ...EXAMPLE_DATE_MULTIPLE_FIELDS, mutuallyExclusive: {} })); + + const exclusiveClassCount = $('.ons-js-exclusive-group-item').length; + expect(exclusiveClassCount).toBe(3); + }); + + it('has the expected `mutuallyExclusive` output', () => { + const faker = templateFaker(); + const mutuallyExclusiveSpy = faker.spy('mutually-exclusive'); + + faker.renderComponent('date-input', { + ...EXAMPLE_DATE_MULTIPLE_FIELDS, + legendClasses: 'custom-legend-class', + classes: 'custom-class', + dontWrap: true, + legendIsQuestionTitle: true, + error: false, + mutuallyExclusive: { + exclusiveOptions: {}, + or: 'Or', + deselectMessage: 'Deselect message', + deselectGroupAdjective: 'Deselect group adjective', + deselectExclusiveOptionAdjective: 'Deselect checkbox adjective', + }, + }); + + expect(mutuallyExclusiveSpy.occurrences[0]).toEqual({ + id: 'date', + legend: 'Date of birth', + description: 'For example, 31 3 1980', + legendClasses: 'custom-legend-class', + classes: 'custom-class', + dontWrap: true, + legendIsQuestionTitle: true, + error: false, + exclusiveOptions: {}, + or: 'Or', + deselectMessage: 'Deselect message', + deselectGroupAdjective: 'Deselect group adjective', + deselectExclusiveOptionAdjective: 'Deselect checkbox adjective', + }); + }); }); - it('has the expected `error` output', () => { - const faker = templateFaker(); - const errorSpy = faker.spy('error'); - - faker.renderComponent('date-input', { - ...EXAMPLE_DATE_SINGLE_FIELD, - error: { text: 'Enter a date that is after 1 January 2019' }, - }); - - expect(errorSpy.occurrences[0]).toEqual({ - text: 'Enter a date that is after 1 January 2019', - }); + describe('mode: single field', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_SINGLE_FIELD)); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has the expected `input` output for the field', () => { + const faker = templateFaker(); + const inputSpy = faker.spy('input'); + + faker.renderComponent('date-input', EXAMPLE_DATE_SINGLE_FIELD); + + expect(inputSpy.occurrences[0]).toEqual({ + id: 'date-year', + type: 'number', + name: 'year', + width: '4', + maxLength: 4, + classes: '', + label: { + text: 'Date of birth', + description: 'For example, 31 3 1980', + id: 'date-year-label', + }, + value: 'Year', + attributes: { + autocomplete: 'bday-year', + }, + }); + }); + + it('has the correct number of inputs', () => { + const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_SINGLE_FIELD)); + const $inputs = $('.ons-input'); + expect($inputs.length).toBe(1); + }); + + it('has the expected `error` output', () => { + const faker = templateFaker(); + const errorSpy = faker.spy('error'); + + faker.renderComponent('date-input', { + ...EXAMPLE_DATE_SINGLE_FIELD, + error: { text: 'Enter a date that is after 1 January 2019' }, + }); + + expect(errorSpy.occurrences[0]).toEqual({ + text: 'Enter a date that is after 1 January 2019', + }); + }); }); - }); - describe('mode: multiple fields with errors', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS_WITH_SINGLE_ERROR)); + describe('mode: multiple fields with errors', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS_WITH_SINGLE_ERROR)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - it('has the provided error class on one input', async () => { - const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS_WITH_SINGLE_ERROR)); - const $errorContent = $('.ons-input--error'); + it('has the provided error class on one input', async () => { + const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS_WITH_SINGLE_ERROR)); + const $errorContent = $('.ons-input--error'); - expect($errorContent.length).toBe(1); - }); + expect($errorContent.length).toBe(1); + }); - it('has the provided error class on multiple inputs', async () => { - const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS_WITH_ERROR)); - const $errorContent = $('.ons-input--error'); + it('has the provided error class on multiple inputs', async () => { + const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_MULTIPLE_FIELDS_WITH_ERROR)); + const $errorContent = $('.ons-input--error'); - expect($errorContent.length).toBe(3); - }); + expect($errorContent.length).toBe(3); + }); - it('does not provide error class when error parameter set to false', async () => { - const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_SINGLE_FIELD_WITH_ERROR_FALSE)); - const $errorContent = $('.ons-input--error'); + it('does not provide error class when error parameter set to false', async () => { + const $ = cheerio.load(renderComponent('date-input', EXAMPLE_DATE_SINGLE_FIELD_WITH_ERROR_FALSE)); + const $errorContent = $('.ons-input--error'); - expect($errorContent.length).toBe(0); + expect($errorContent.length).toBe(0); + }); }); - }); }); diff --git a/src/components/description-list/_description-list.scss b/src/components/description-list/_description-list.scss index 2073ee9842..c96a09c8c5 100644 --- a/src/components/description-list/_description-list.scss +++ b/src/components/description-list/_description-list.scss @@ -1,32 +1,32 @@ .ons-description-list { - &__items { - margin: 0 0 2rem; - } + &__items { + margin: 0 0 2rem; + } - &__term { - clear: both; - float: left; - font-weight: $font-weight-bold; + &__term { + clear: both; + float: left; + font-weight: $font-weight-bold; - &:not(:first-child) { - margin-top: 0.5rem; + &:not(:first-child) { + margin-top: 0.5rem; + } } - } - &__value { - float: right; - margin-left: 0; /* As normalize adds a 40px left margin */ + &__value { + float: right; + margin-left: 0; /* As normalize adds a 40px left margin */ - &:not(:nth-of-type(1)) { - @include mq(m) { - margin-top: 0.5rem; - } - } + &:not(:nth-of-type(1)) { + @include mq(m) { + margin-top: 0.5rem; + } + } - & + & { - @include mq(m) { - margin-top: 0; - } + & + & { + @include mq(m) { + margin-top: 0; + } + } } - } } diff --git a/src/components/description-list/_macro.spec.js b/src/components/description-list/_macro.spec.js index 2f111c429d..77734837f3 100644 --- a/src/components/description-list/_macro.spec.js +++ b/src/components/description-list/_macro.spec.js @@ -6,162 +6,162 @@ import axe from '../../tests/helpers/axe'; import { renderComponent } from '../../tests/helpers/rendering'; const EXAMPLE_DESCRIPTION_LIST_FULL = { - id: 'example-id', - classes: 'ons-u-mb-no', - descriptionListLabel: 'This is an example of the description list component', - termCol: 2, - descriptionCol: 10, - itemsList: [ - { - term: 'Survey:', - descriptions: [ + id: 'example-id', + classes: 'ons-u-mb-no', + descriptionListLabel: 'This is an example of the description list component', + termCol: 2, + descriptionCol: 10, + itemsList: [ { - id: 'description-1', - description: 'Bricks & Blocks', + term: 'Survey:', + descriptions: [ + { + id: 'description-1', + description: 'Bricks & Blocks', + }, + ], }, - ], - }, - { - term: 'RU Refs:', - descriptions: [ { - id: 'description-2', - description: '49900000118', + term: 'RU Refs:', + descriptions: [ + { + id: 'description-2', + description: '49900000118', + }, + { + id: 'description-3', + description: '49300005832', + }, + ], }, - { - id: 'description-3', - description: '49300005832', - }, - ], - }, - ], + ], }; const EXAMPLE_DESCRIPTION_LIST_MINIMAL = { - itemsList: [ - { - term: 'Survey:', - descriptions: [ - { - description: 'Bricks & Blocks', - }, - ], - }, - { - term: 'RU Refs:', - descriptions: [ + itemsList: [ { - description: '49900000118', + term: 'Survey:', + descriptions: [ + { + description: 'Bricks & Blocks', + }, + ], }, { - description: '49300005832', + term: 'RU Refs:', + descriptions: [ + { + description: '49900000118', + }, + { + description: '49300005832', + }, + ], }, - ], - }, - ], + ], }; describe('macro: description-list', () => { - it('passes jest-axe checks when all parameters are provided', async () => { - const $ = cheerio.load(renderComponent('description-list', EXAMPLE_DESCRIPTION_LIST_FULL)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('passes jest-axe checks when minimal parameters are provided', async () => { - const $ = cheerio.load(renderComponent('description-list', EXAMPLE_DESCRIPTION_LIST_MINIMAL)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the provided `id` attribute', () => { - const $ = cheerio.load( - renderComponent('description-list', { - ...EXAMPLE_DESCRIPTION_LIST_MINIMAL, - id: 'example-id', - }), - ); - - expect($('#example-id').length).toBe(1); - }); - - it('has additionally provided style classes', () => { - const $ = cheerio.load( - renderComponent('description-list', { - ...EXAMPLE_DESCRIPTION_LIST_MINIMAL, - classes: 'extra-class another-extra-class', - }), - ); - - expect($('.ons-description-list').hasClass('extra-class')).toBe(true); - expect($('.ons-description-list').hasClass('another-extra-class')).toBe(true); - }); - - it('outputs `title` and `aria-label` attributes when `descriptionListLabel` is provided', () => { - const $ = cheerio.load( - renderComponent('description-list', { - ...EXAMPLE_DESCRIPTION_LIST_MINIMAL, - descriptionListLabel: 'This is an example of the description list component', - }), - ); - - expect($('.ons-description-list').attr('title')).toBe('This is an example of the description list component'); - expect($('.ons-description-list').attr('aria-label')).toBe('This is an example of the description list component'); - }); - - it('outputs list items as expected', () => { - const $ = cheerio.load(renderComponent('description-list', EXAMPLE_DESCRIPTION_LIST_FULL)); - - const $listElements = $('.ons-description-list__term, .ons-description-list__value'); - - expect($listElements[0].tagName).toBe('dt'); - expect($($listElements[0]).text()).toBe('Survey:'); - - expect($listElements[1].tagName).toBe('dd'); - expect($($listElements[1]).attr('id')).toBe('description-1'); - expect($($listElements[1]).text()).toBe('Bricks & Blocks'); - - expect($listElements[2].tagName).toBe('dt'); - expect($($listElements[2]).text()).toBe('RU Refs:'); - - expect($listElements[3].tagName).toBe('dd'); - expect($($listElements[3]).attr('id')).toBe('description-2'); - expect($($listElements[3]).text()).toBe('49900000118'); - - expect($listElements[4].tagName).toBe('dd'); - expect($($listElements[4]).attr('id')).toBe('description-3'); - expect($($listElements[4]).text()).toBe('49300005832'); - }); - - it.each([ - [1, 'ons-col-1\\@m'], - [4, 'ons-col-4\\@m'], - ])('applies class for the provided `termCol` (%i -> %s)', (termCol, expectedClass) => { - const $ = cheerio.load( - renderComponent('description-list', { - ...EXAMPLE_DESCRIPTION_LIST_MINIMAL, - termCol, - }), - ); - - const $termElements = $(`.ons-description-list__term.${expectedClass}`); - expect($termElements.length).toBe(2); - }); - - it.each([ - [1, 'ons-col-1\\@m'], - [4, 'ons-col-4\\@m'], - ])('applies class for the provided `descriptionCol` (%i -> %s)', (descriptionCol, expectedClass) => { - const $ = cheerio.load( - renderComponent('description-list', { - ...EXAMPLE_DESCRIPTION_LIST_MINIMAL, - descriptionCol, - }), - ); - - const $valueElements = $(`.ons-description-list__value.${expectedClass}`); - expect($valueElements.length).toBe(3); - }); + it('passes jest-axe checks when all parameters are provided', async () => { + const $ = cheerio.load(renderComponent('description-list', EXAMPLE_DESCRIPTION_LIST_FULL)); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('passes jest-axe checks when minimal parameters are provided', async () => { + const $ = cheerio.load(renderComponent('description-list', EXAMPLE_DESCRIPTION_LIST_MINIMAL)); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has the provided `id` attribute', () => { + const $ = cheerio.load( + renderComponent('description-list', { + ...EXAMPLE_DESCRIPTION_LIST_MINIMAL, + id: 'example-id', + }), + ); + + expect($('#example-id').length).toBe(1); + }); + + it('has additionally provided style classes', () => { + const $ = cheerio.load( + renderComponent('description-list', { + ...EXAMPLE_DESCRIPTION_LIST_MINIMAL, + classes: 'extra-class another-extra-class', + }), + ); + + expect($('.ons-description-list').hasClass('extra-class')).toBe(true); + expect($('.ons-description-list').hasClass('another-extra-class')).toBe(true); + }); + + it('outputs `title` and `aria-label` attributes when `descriptionListLabel` is provided', () => { + const $ = cheerio.load( + renderComponent('description-list', { + ...EXAMPLE_DESCRIPTION_LIST_MINIMAL, + descriptionListLabel: 'This is an example of the description list component', + }), + ); + + expect($('.ons-description-list').attr('title')).toBe('This is an example of the description list component'); + expect($('.ons-description-list').attr('aria-label')).toBe('This is an example of the description list component'); + }); + + it('outputs list items as expected', () => { + const $ = cheerio.load(renderComponent('description-list', EXAMPLE_DESCRIPTION_LIST_FULL)); + + const $listElements = $('.ons-description-list__term, .ons-description-list__value'); + + expect($listElements[0].tagName).toBe('dt'); + expect($($listElements[0]).text()).toBe('Survey:'); + + expect($listElements[1].tagName).toBe('dd'); + expect($($listElements[1]).attr('id')).toBe('description-1'); + expect($($listElements[1]).text()).toBe('Bricks & Blocks'); + + expect($listElements[2].tagName).toBe('dt'); + expect($($listElements[2]).text()).toBe('RU Refs:'); + + expect($listElements[3].tagName).toBe('dd'); + expect($($listElements[3]).attr('id')).toBe('description-2'); + expect($($listElements[3]).text()).toBe('49900000118'); + + expect($listElements[4].tagName).toBe('dd'); + expect($($listElements[4]).attr('id')).toBe('description-3'); + expect($($listElements[4]).text()).toBe('49300005832'); + }); + + it.each([ + [1, 'ons-col-1\\@m'], + [4, 'ons-col-4\\@m'], + ])('applies class for the provided `termCol` (%i -> %s)', (termCol, expectedClass) => { + const $ = cheerio.load( + renderComponent('description-list', { + ...EXAMPLE_DESCRIPTION_LIST_MINIMAL, + termCol, + }), + ); + + const $termElements = $(`.ons-description-list__term.${expectedClass}`); + expect($termElements.length).toBe(2); + }); + + it.each([ + [1, 'ons-col-1\\@m'], + [4, 'ons-col-4\\@m'], + ])('applies class for the provided `descriptionCol` (%i -> %s)', (descriptionCol, expectedClass) => { + const $ = cheerio.load( + renderComponent('description-list', { + ...EXAMPLE_DESCRIPTION_LIST_MINIMAL, + descriptionCol, + }), + ); + + const $valueElements = $(`.ons-description-list__value.${expectedClass}`); + expect($valueElements.length).toBe(3); + }); }); diff --git a/src/components/details/_details.scss b/src/components/details/_details.scss index ba795b0351..7d95dd989f 100644 --- a/src/components/details/_details.scss +++ b/src/components/details/_details.scss @@ -1,135 +1,135 @@ $details-caret-width: 1.5rem; .ons-details { - &--initialised & { - &__heading { - color: var(--ons-color-text-link); - cursor: pointer; - display: inline-block; - outline: none; - padding: 0 0 0 $details-caret-width; - pointer-events: initial; - position: relative; - - &::marker, - &::-webkit-details-marker { - display: none; - } - - &:focus { - .ons-details__title { - @extend %a-focus; - // extend details focus background behind caret - margin-left: -$details-caret-width; - padding-left: $details-caret-width; + &--initialised & { + &__heading { + color: var(--ons-color-text-link); + cursor: pointer; + display: inline-block; + outline: none; + padding: 0 0 0 $details-caret-width; + pointer-events: initial; + position: relative; + + &::marker, + &::-webkit-details-marker { + display: none; + } + + &:focus { + .ons-details__title { + @extend %a-focus; + // extend details focus background behind caret + margin-left: -$details-caret-width; + padding-left: $details-caret-width; + } + + .ons-details__icon .ons-icon { + fill: var(--ons-color-text-link-focus); + } + } + + &:hover:not(:focus) { + color: var(--ons-color-text-link-hover); + + .ons-details__icon { + fill: var(--ons-color-text-link-hover); + } + + .ons-details__title { + text-decoration: underline solid var(--ons-color-text-link-hover) 2px; + } + } } - .ons-details__icon .ons-icon { - fill: var(--ons-color-text-link-focus); + &__icon { + display: inline-block; + fill: var(--ons-color-text-link); + height: $details-caret-width; + left: -0.15rem; + position: absolute; + top: -0.2rem; + width: $details-caret-width; } - } - - &:hover:not(:focus) { - color: var(--ons-color-text-link-hover); - .ons-details__icon { - fill: var(--ons-color-text-link-hover); + &__content { + display: none; } - .ons-details__title { - text-decoration: underline solid var(--ons-color-text-link-hover) 2px; + &__title { + display: inline-block; + font-size: 1rem; + font-weight: $font-weight-bold; + margin-bottom: 0; + text-underline-position: under; + transform: translateY(-1px); + pointer-events: none; } - } } - &__icon { - display: inline-block; - fill: var(--ons-color-text-link); - height: $details-caret-width; - left: -0.15rem; - position: absolute; - top: -0.2rem; - width: $details-caret-width; - } - - &__content { - display: none; - } + &--open & { + &__icon { + left: -0.1rem; + top: 0.2rem; + transform: rotate(90deg); + } - &__title { - display: inline-block; - font-size: 1rem; - font-weight: $font-weight-bold; - margin-bottom: 0; - text-underline-position: under; - transform: translateY(-1px); - pointer-events: none; + &__content { + border-left: 4px solid var(--ons-color-borders-indent); + display: block; + margin: 1rem 0 0; + padding: 0 0 0 1.3em; + } } - } - &--open & { - &__icon { - left: -0.1rem; - top: 0.2rem; - transform: rotate(90deg); - } + &--accordion & { + &__heading { + border-top: 1px solid var(--ons-color-borders); + margin: 0; + padding-bottom: 0.9rem; + padding-top: 1rem; + width: 100%; + + &:focus { + outline: none; + } + } - &__content { - border-left: 4px solid var(--ons-color-borders-indent); - display: block; - margin: 1rem 0 0; - padding: 0 0 0 1.3em; - } - } - - &--accordion & { - &__heading { - border-top: 1px solid var(--ons-color-borders); - margin: 0; - padding-bottom: 0.9rem; - padding-top: 1rem; - width: 100%; - - &:focus { - outline: none; - } - } + &__title { + margin: 0 1rem 0 0; + } - &__title { - margin: 0 1rem 0 0; - } + &__icon { + top: 0.8rem; + } - &__icon { - top: 0.8rem; - } + &__controls { + align-items: center; + display: flex; + justify-content: space-between; + } - &__controls { - align-items: center; - display: flex; - justify-content: space-between; - } + &__btn { + align-self: flex-start; + width: auto; + } - &__btn { - align-self: flex-start; - width: auto; + &__content { + border-left: 0; + margin: 0; + padding: 0; + } } - &__content { - border-left: 0; - margin: 0; - padding: 0; + &--accordion.ons-details--open { + .ons-details__icon { + position: absolute; + top: 1.2rem; + } } - } - &--accordion.ons-details--open { - .ons-details__icon { - position: absolute; - top: 1.2rem; + &__icon { + display: none; + pointer-events: none; } - } - - &__icon { - display: none; - pointer-events: none; - } } diff --git a/src/components/details/_macro.spec.js b/src/components/details/_macro.spec.js index ba7dca9ace..3a3bec494d 100644 --- a/src/components/details/_macro.spec.js +++ b/src/components/details/_macro.spec.js @@ -6,139 +6,139 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_DETAILS_BASIC = { - id: 'details-id', - title: 'Title for details', - content: 'Content for details', + id: 'details-id', + title: 'Title for details', + content: 'Content for details', }; describe('macro: details', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('details', EXAMPLE_DETAILS_BASIC)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the provided `id` attribute', () => { - const $ = cheerio.load(renderComponent('details', EXAMPLE_DETAILS_BASIC)); - - expect($('.ons-details').attr('id')).toBe('details-id'); - }); - - it('has additionally provided style classes', () => { - const $ = cheerio.load( - renderComponent('details', { - ...EXAMPLE_DETAILS_BASIC, - classes: 'extra-class another-extra-class', - }), - ); - - expect($('.ons-details').hasClass('extra-class')).toBe(true); - expect($('.ons-details').hasClass('another-extra-class')).toBe(true); - }); - - it('has provided title text', () => { - const $ = cheerio.load(renderComponent('details', EXAMPLE_DETAILS_BASIC)); - - const titleText = $('.ons-details__title').first().text().trim(); - expect(titleText).toBe('Title for details'); - }); - - it('has title with provided tag override', () => { - const $ = cheerio.load( - renderComponent('details', { - ...EXAMPLE_DETAILS_BASIC, - titleTag: 'h4', - }), - ); - - const titleTag = $('.ons-details__title')[0].tagName; - expect(titleTag).toBe('h4'); - }); - - it('has provided content text', () => { - const $ = cheerio.load(renderComponent('details', EXAMPLE_DETAILS_BASIC)); - - const titleText = $('.ons-details__content').text().trim(); - expect(titleText).toEqual(expect.stringContaining('Content for details')); - }); - - it('has additionally provided `attributes`', () => { - const $ = cheerio.load( - renderComponent('details', { - ...EXAMPLE_DETAILS_BASIC, - attributes: { - a: 123, - b: 456, - }, - }), - ); - expect($('.ons-details').attr('a')).toBe('123'); - expect($('.ons-details').attr('b')).toBe('456'); - }); - - it('has the correct data attribute when `saveState` is provided', () => { - const $ = cheerio.load( - renderComponent('details', { - ...EXAMPLE_DETAILS_BASIC, - saveState: true, - }), - ); - expect($('.ons-details').attr('data-save-state')).toBe('true'); - }); - - it('has the correct data attribute when `open` is provided', () => { - const $ = cheerio.load( - renderComponent('details', { - ...EXAMPLE_DETAILS_BASIC, - open: true, - }), - ); - expect($('.ons-details').attr('data-open')).toBe('true'); - }); - - it('has additionally provided `headingAttributes`', () => { - const $ = cheerio.load( - renderComponent('details', { - ...EXAMPLE_DETAILS_BASIC, - headingAttributes: { - a: 123, - b: 456, - }, - }), - ); - - expect($('.ons-details__heading').attr('a')).toBe('123'); - expect($('.ons-details__heading').attr('b')).toBe('456'); - }); - - it('has additionally provided `contentAttributes`', () => { - const $ = cheerio.load( - renderComponent('details', { - ...EXAMPLE_DETAILS_BASIC, - contentAttributes: { - a: 123, - b: 456, - }, - }), - ); - - expect($('.ons-details__content').attr('a')).toBe('123'); - expect($('.ons-details__content').attr('b')).toBe('456'); - }); - - it('has `chevron` icon', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); - faker.renderComponent('details', EXAMPLE_DETAILS_BASIC); - - expect(iconsSpy.occurrences[0].iconType).toBe('chevron'); - }); - - it('calls with content', () => { - const $ = cheerio.load(renderComponent('details', { EXAMPLE_DETAILS_BASIC }, 'Example content...')); - - const content = $('.ons-details__content').text().trim(); - expect(content).toEqual(expect.stringContaining('Example content...')); - }); + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('details', EXAMPLE_DETAILS_BASIC)); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has the provided `id` attribute', () => { + const $ = cheerio.load(renderComponent('details', EXAMPLE_DETAILS_BASIC)); + + expect($('.ons-details').attr('id')).toBe('details-id'); + }); + + it('has additionally provided style classes', () => { + const $ = cheerio.load( + renderComponent('details', { + ...EXAMPLE_DETAILS_BASIC, + classes: 'extra-class another-extra-class', + }), + ); + + expect($('.ons-details').hasClass('extra-class')).toBe(true); + expect($('.ons-details').hasClass('another-extra-class')).toBe(true); + }); + + it('has provided title text', () => { + const $ = cheerio.load(renderComponent('details', EXAMPLE_DETAILS_BASIC)); + + const titleText = $('.ons-details__title').first().text().trim(); + expect(titleText).toBe('Title for details'); + }); + + it('has title with provided tag override', () => { + const $ = cheerio.load( + renderComponent('details', { + ...EXAMPLE_DETAILS_BASIC, + titleTag: 'h4', + }), + ); + + const titleTag = $('.ons-details__title')[0].tagName; + expect(titleTag).toBe('h4'); + }); + + it('has provided content text', () => { + const $ = cheerio.load(renderComponent('details', EXAMPLE_DETAILS_BASIC)); + + const titleText = $('.ons-details__content').text().trim(); + expect(titleText).toEqual(expect.stringContaining('Content for details')); + }); + + it('has additionally provided `attributes`', () => { + const $ = cheerio.load( + renderComponent('details', { + ...EXAMPLE_DETAILS_BASIC, + attributes: { + a: 123, + b: 456, + }, + }), + ); + expect($('.ons-details').attr('a')).toBe('123'); + expect($('.ons-details').attr('b')).toBe('456'); + }); + + it('has the correct data attribute when `saveState` is provided', () => { + const $ = cheerio.load( + renderComponent('details', { + ...EXAMPLE_DETAILS_BASIC, + saveState: true, + }), + ); + expect($('.ons-details').attr('data-save-state')).toBe('true'); + }); + + it('has the correct data attribute when `open` is provided', () => { + const $ = cheerio.load( + renderComponent('details', { + ...EXAMPLE_DETAILS_BASIC, + open: true, + }), + ); + expect($('.ons-details').attr('data-open')).toBe('true'); + }); + + it('has additionally provided `headingAttributes`', () => { + const $ = cheerio.load( + renderComponent('details', { + ...EXAMPLE_DETAILS_BASIC, + headingAttributes: { + a: 123, + b: 456, + }, + }), + ); + + expect($('.ons-details__heading').attr('a')).toBe('123'); + expect($('.ons-details__heading').attr('b')).toBe('456'); + }); + + it('has additionally provided `contentAttributes`', () => { + const $ = cheerio.load( + renderComponent('details', { + ...EXAMPLE_DETAILS_BASIC, + contentAttributes: { + a: 123, + b: 456, + }, + }), + ); + + expect($('.ons-details__content').attr('a')).toBe('123'); + expect($('.ons-details__content').attr('b')).toBe('456'); + }); + + it('has `chevron` icon', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); + faker.renderComponent('details', EXAMPLE_DETAILS_BASIC); + + expect(iconsSpy.occurrences[0].iconType).toBe('chevron'); + }); + + it('calls with content', () => { + const $ = cheerio.load(renderComponent('details', { EXAMPLE_DETAILS_BASIC }, 'Example content...')); + + const content = $('.ons-details__content').text().trim(); + expect(content).toEqual(expect.stringContaining('Example content...')); + }); }); diff --git a/src/components/details/details.dom.js b/src/components/details/details.dom.js index 81112d0b99..060a9509e1 100644 --- a/src/components/details/details.dom.js +++ b/src/components/details/details.dom.js @@ -1,13 +1,13 @@ import domready from '../../js/domready'; async function initialiseDetailsEls() { - const detailsComponents = [...document.querySelectorAll('.ons-js-details')]; - const accordionComponents = [...document.querySelectorAll('.ons-js-accordion')]; + const detailsComponents = [...document.querySelectorAll('.ons-js-details')]; + const accordionComponents = [...document.querySelectorAll('.ons-js-accordion')]; - if (detailsComponents.length && !accordionComponents.length) { - const Details = (await import('./details')).default; - detailsComponents.map((element) => new Details(element)); - } + if (detailsComponents.length && !accordionComponents.length) { + const Details = (await import('./details')).default; + detailsComponents.map((element) => new Details(element)); + } } domready(initialiseDetailsEls); diff --git a/src/components/details/details.js b/src/components/details/details.js index ddf986911f..35f1370b48 100644 --- a/src/components/details/details.js +++ b/src/components/details/details.js @@ -1,78 +1,78 @@ export default class Details { - constructor(detailsElement) { - this.saveState = detailsElement.getAttribute('data-save-state') === 'true'; - this.open = detailsElement.getAttribute('data-open') === 'true'; - this.group = detailsElement.getAttribute('data-group'); + constructor(detailsElement) { + this.saveState = detailsElement.getAttribute('data-save-state') === 'true'; + this.open = detailsElement.getAttribute('data-open') === 'true'; + this.group = detailsElement.getAttribute('data-group'); - // Elements - this.details = detailsElement; - this.detailsHeader = this.details.querySelector('.ons-js-details-heading'); - this.content = this.details.querySelector('.ons-js-details-content'); + // Elements + this.details = detailsElement; + this.detailsHeader = this.details.querySelector('.ons-js-details-heading'); + this.content = this.details.querySelector('.ons-js-details-content'); - // Initialise - const detailsId = detailsElement.getAttribute('id'); + // Initialise + const detailsId = detailsElement.getAttribute('id'); - this.details.setAttribute('role', 'group'); - this.detailsHeader.setAttribute('role', 'link'); - this.detailsHeader.setAttribute('aria-controls', detailsId); - this.detailsHeader.setAttribute('tabindex', 0); + this.details.setAttribute('role', 'group'); + this.detailsHeader.setAttribute('role', 'link'); + this.detailsHeader.setAttribute('aria-controls', detailsId); + this.detailsHeader.setAttribute('tabindex', 0); - if (localStorage.getItem(detailsId) || this.open) { - this.setOpen(true); - } else { - this.setOpen(false); + if (localStorage.getItem(detailsId) || this.open) { + this.setOpen(true); + } else { + this.setOpen(false); + } + + this.detailsHeader.addEventListener('click', this.toggle.bind(this)); + this.detailsHeader.addEventListener('keydown', this.keyboardInteraction.bind(this)); + this.details.classList.add('ons-details--initialised'); } - this.detailsHeader.addEventListener('click', this.toggle.bind(this)); - this.detailsHeader.addEventListener('keydown', this.keyboardInteraction.bind(this)); - this.details.classList.add('ons-details--initialised'); - } + toggle(event) { + event.preventDefault(); + this.setOpen(!this.isOpen); + } - toggle(event) { - event.preventDefault(); - this.setOpen(!this.isOpen); - } + setOpen(open) { + if (open !== this.isOpen) { + const action = open ? 'Open' : 'Close'; + const cls = open ? 'add' : 'remove'; + const openAttribute = open ? 'set' : 'remove'; - setOpen(open) { - if (open !== this.isOpen) { - const action = open ? 'Open' : 'Close'; - const cls = open ? 'add' : 'remove'; - const openAttribute = open ? 'set' : 'remove'; + this.isOpen = open; + this.details[`${openAttribute}Attribute`]('open', ''); + this.details.classList[cls]('ons-details--open'); + this.detailsHeader.setAttribute('aria-expanded', open); + this.content.setAttribute('aria-hidden', !open); + this.detailsHeader.setAttribute('data-ga-action', `${action} panel`); - this.isOpen = open; - this.details[`${openAttribute}Attribute`]('open', ''); - this.details.classList[cls]('ons-details--open'); - this.detailsHeader.setAttribute('aria-expanded', open); - this.content.setAttribute('aria-hidden', !open); - this.detailsHeader.setAttribute('data-ga-action', `${action} panel`); + if (this.onOpen && this.onClose) { + if (open) { + this.onOpen(); + } else { + this.onClose(); + } + } + } - if (this.onOpen && this.onClose) { - if (open) { - this.onOpen(); + if (this.saveState === true && open === true) { + localStorage.setItem(this.details.getAttribute('id'), true); } else { - this.onClose(); + localStorage.removeItem(this.details.getAttribute('id')); } - } } - if (this.saveState === true && open === true) { - localStorage.setItem(this.details.getAttribute('id'), true); - } else { - localStorage.removeItem(this.details.getAttribute('id')); - } - } + keyboardInteraction(event) { + const keyCode = event.which; + switch (keyCode) { + // Enter/Space + case 13: + case 32: + event.preventDefault(); + event.stopPropagation(); - keyboardInteraction(event) { - const keyCode = event.which; - switch (keyCode) { - // Enter/Space - case 13: - case 32: - event.preventDefault(); - event.stopPropagation(); - - this.toggle(event); - break; + this.toggle(event); + break; + } } - } } diff --git a/src/components/details/details.spec.js b/src/components/details/details.spec.js index b43576d00e..021bdcef32 100644 --- a/src/components/details/details.spec.js +++ b/src/components/details/details.spec.js @@ -1,135 +1,135 @@ import { renderComponent, renderTemplate, setTestPage } from '../../tests/helpers/rendering'; const EXAMPLE_DETAILS_BASIC = { - id: 'details-id', - title: 'Title for details', - content: 'Content for details', -}; - -const EXAMPLE_PAGE = ` - ${renderComponent('details', { id: 'details-id', title: 'Title for details', content: 'Content for details', - })} +}; - ${renderComponent('details', { - id: 'details-id-2', - title: 'Title for details', - content: 'Content for details', - })} +const EXAMPLE_PAGE = ` + ${renderComponent('details', { + id: 'details-id', + title: 'Title for details', + content: 'Content for details', + })} + + ${renderComponent('details', { + id: 'details-id-2', + title: 'Title for details', + content: 'Content for details', + })} `; const RENDERED_EXAMPLE_PAGE = renderTemplate(EXAMPLE_PAGE); describe('script: details', () => { - it('begins open when specified', async () => { - await setTestPage( - '/test', - renderComponent('details', { - ...EXAMPLE_DETAILS_BASIC, - open: true, - }), - ); - - const detailsOpenClass = await page.$eval('.ons-js-details', (node) => node.classList.contains('ons-details--open')); - expect(detailsOpenClass).toBe(true); - }); - - describe('when the details heading is clicked to open the details', () => { - beforeEach(async () => { - await setTestPage('/test', renderComponent('details', EXAMPLE_DETAILS_BASIC)); - await page.click('.ons-js-details-heading'); - }); - - it('sets the `open` attribute and adds the correct class', async () => { - const detailsOpenClass = await page.$eval('.ons-js-details', (node) => node.classList.contains('ons-details--open')); + it('begins open when specified', async () => { + await setTestPage( + '/test', + renderComponent('details', { + ...EXAMPLE_DETAILS_BASIC, + open: true, + }), + ); - expect(detailsOpenClass).toBe(true); + const detailsOpenClass = await page.$eval('.ons-js-details', (node) => node.classList.contains('ons-details--open')); + expect(detailsOpenClass).toBe(true); }); - it('sets the `ga` attributes', async () => { - const gaHeadingAttribute = await page.$eval('.ons-js-details-heading', (element) => element.getAttribute('data-ga-action')); - - expect(gaHeadingAttribute).toBe('Open panel'); - }); - }); + describe('when the details heading is clicked to open the details', () => { + beforeEach(async () => { + await setTestPage('/test', renderComponent('details', EXAMPLE_DETAILS_BASIC)); + await page.click('.ons-js-details-heading'); + }); - describe('when there is more than one details component and a details heading is clicked to open the details', () => { - beforeEach(async () => { - await setTestPage('/test', RENDERED_EXAMPLE_PAGE); - await page.click('#details-id > .ons-js-details-heading'); - }); + it('sets the `open` attribute and adds the correct class', async () => { + const detailsOpenClass = await page.$eval('.ons-js-details', (node) => node.classList.contains('ons-details--open')); - it('sets the `open` attribute and open class on the right component', async () => { - const detailsOpenClass = await page.$eval('#details-id', (node) => node.classList.contains('ons-details--open')); - const detailsOpenClass2 = await page.$eval('#details-id-2', (node) => node.classList.contains('ons-details--open')); + expect(detailsOpenClass).toBe(true); + }); - expect(detailsOpenClass).toBe(true); - expect(detailsOpenClass2).toBe(false); - }); - }); + it('sets the `ga` attributes', async () => { + const gaHeadingAttribute = await page.$eval('.ons-js-details-heading', (element) => element.getAttribute('data-ga-action')); - describe('when the details heading is focused', () => { - beforeEach(async () => { - await setTestPage('/test', renderComponent('details', EXAMPLE_DETAILS_BASIC)); - await page.focus('.ons-js-details-heading'); + expect(gaHeadingAttribute).toBe('Open panel'); + }); }); - describe('when the space bar is pressed', () => { - beforeEach(async () => { - await page.keyboard.press('Space'); - }); - - it('opens the details content', async () => { - const detailsOpenClass = await page.$eval('.ons-js-details', (node) => node.classList.contains('ons-details--open')); - expect(detailsOpenClass).toBe(true); - }); - }); + describe('when there is more than one details component and a details heading is clicked to open the details', () => { + beforeEach(async () => { + await setTestPage('/test', RENDERED_EXAMPLE_PAGE); + await page.click('#details-id > .ons-js-details-heading'); + }); - describe('when the Enter key is pressed', () => { - beforeEach(async () => { - await page.keyboard.press('Enter'); - }); + it('sets the `open` attribute and open class on the right component', async () => { + const detailsOpenClass = await page.$eval('#details-id', (node) => node.classList.contains('ons-details--open')); + const detailsOpenClass2 = await page.$eval('#details-id-2', (node) => node.classList.contains('ons-details--open')); - it('opens the details content', async () => { - const detailsOpenClass = await page.$eval('.ons-js-details', (node) => node.classList.contains('ons-details--open')); - expect(detailsOpenClass).toBe(true); - }); + expect(detailsOpenClass).toBe(true); + expect(detailsOpenClass2).toBe(false); + }); }); - }); - - describe('when the state is set to save', () => { - beforeEach(async () => { - await setTestPage( - '/test', - renderComponent('details', { - ...EXAMPLE_DETAILS_BASIC, - saveState: true, - }), - ); - }); - - describe('when the details is opened', () => { - beforeEach(async () => { - await page.click('.ons-js-details-heading'); - }); - it('sets state in localStorage', async () => { - const localStorage = await page.evaluate(() => localStorage.getItem('details-id')); - expect(localStorage).toBe('true'); - }); + describe('when the details heading is focused', () => { + beforeEach(async () => { + await setTestPage('/test', renderComponent('details', EXAMPLE_DETAILS_BASIC)); + await page.focus('.ons-js-details-heading'); + }); + + describe('when the space bar is pressed', () => { + beforeEach(async () => { + await page.keyboard.press('Space'); + }); + + it('opens the details content', async () => { + const detailsOpenClass = await page.$eval('.ons-js-details', (node) => node.classList.contains('ons-details--open')); + expect(detailsOpenClass).toBe(true); + }); + }); + + describe('when the Enter key is pressed', () => { + beforeEach(async () => { + await page.keyboard.press('Enter'); + }); + + it('opens the details content', async () => { + const detailsOpenClass = await page.$eval('.ons-js-details', (node) => node.classList.contains('ons-details--open')); + expect(detailsOpenClass).toBe(true); + }); + }); }); - describe('when the details is closed', () => { - beforeEach(async () => { - await page.click('.ons-js-details-heading'); - }); - - it('removes state in localStorage', async () => { - const localStorage = await page.evaluate(() => localStorage.getItem('details-id')); - expect(localStorage).toBe(null); - }); + describe('when the state is set to save', () => { + beforeEach(async () => { + await setTestPage( + '/test', + renderComponent('details', { + ...EXAMPLE_DETAILS_BASIC, + saveState: true, + }), + ); + }); + + describe('when the details is opened', () => { + beforeEach(async () => { + await page.click('.ons-js-details-heading'); + }); + + it('sets state in localStorage', async () => { + const localStorage = await page.evaluate(() => localStorage.getItem('details-id')); + expect(localStorage).toBe('true'); + }); + }); + + describe('when the details is closed', () => { + beforeEach(async () => { + await page.click('.ons-js-details-heading'); + }); + + it('removes state in localStorage', async () => { + const localStorage = await page.evaluate(() => localStorage.getItem('details-id')); + expect(localStorage).toBe(null); + }); + }); }); - }); }); diff --git a/src/components/document-list/_macro.spec.js b/src/components/document-list/_macro.spec.js index c77d3f1ef7..d382751cf2 100644 --- a/src/components/document-list/_macro.spec.js +++ b/src/components/document-list/_macro.spec.js @@ -6,468 +6,468 @@ import axe from '../../tests/helpers/axe'; import { renderComponent } from '../../tests/helpers/rendering'; const EXAMPLE_DOCUMENT_LIST_BASIC = { - url: '#0', - title: 'Crime and justice', - description: 'Some description', + url: '#0', + title: 'Crime and justice', + description: 'Some description', }; const EXAMPLE_DOCUMENT_LIST_WITH_THUMBNAIL = { - ...EXAMPLE_DOCUMENT_LIST_BASIC, - thumbnail: { - smallSrc: '/example-small.png', - largeSrc: '/example-large.png', - }, + ...EXAMPLE_DOCUMENT_LIST_BASIC, + thumbnail: { + smallSrc: '/example-small.png', + largeSrc: '/example-large.png', + }, }; const EXAMPLE_DOCUMENT_LIST_WITH_METADATA_FILE = { - ...EXAMPLE_DOCUMENT_LIST_BASIC, - metadata: { - file: { - fileType: 'PDF', - fileSize: '499KB', - filePages: '1 page', + ...EXAMPLE_DOCUMENT_LIST_BASIC, + metadata: { + file: { + fileType: 'PDF', + fileSize: '499KB', + filePages: '1 page', + }, }, - }, }; const EXAMPLE_DOCUMENT_LIST_WITH_METADATA_TYPE = { - ...EXAMPLE_DOCUMENT_LIST_BASIC, - metadata: { - type: { - text: 'Poster', - url: '#0', - ref: 'some ref', + ...EXAMPLE_DOCUMENT_LIST_BASIC, + metadata: { + type: { + text: 'Poster', + url: '#0', + ref: 'some ref', + }, }, - }, }; const EXAMPLE_DOCUMENT_LIST_WITH_MULTIPLE = { - ...EXAMPLE_DOCUMENT_LIST_BASIC, - id: 'some-id', - thumbnail: { - smallSrc: '/example-small.png', - largeSrc: '/example-large.png', - }, - metadata: { - type: { - text: 'Poster', - url: '#0', - ref: 'some ref', - }, - file: { - fileType: 'PDF', - fileSize: '499KB', - filePages: '1 page', + ...EXAMPLE_DOCUMENT_LIST_BASIC, + id: 'some-id', + thumbnail: { + smallSrc: '/example-small.png', + largeSrc: '/example-large.png', }, - date: { - iso: '2022-01-01', - short: '1 January 2022', - showPrefix: true, - prefix: 'Released', + metadata: { + type: { + text: 'Poster', + url: '#0', + ref: 'some ref', + }, + file: { + fileType: 'PDF', + fileSize: '499KB', + filePages: '1 page', + }, + date: { + iso: '2022-01-01', + short: '1 January 2022', + showPrefix: true, + prefix: 'Released', + }, }, - }, }; describe('macro: document list', () => { - describe('global configuration', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load( - renderComponent('document-list', { - id: 'some-id', - documents: [EXAMPLE_DOCUMENT_LIST_BASIC, EXAMPLE_DOCUMENT_LIST_BASIC, EXAMPLE_DOCUMENT_LIST_BASIC], - }), - ); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the provided `id` attribute', () => { - const $ = cheerio.load( - renderComponent('document-list', { - id: 'some-id', - documents: [EXAMPLE_DOCUMENT_LIST_BASIC, EXAMPLE_DOCUMENT_LIST_BASIC, EXAMPLE_DOCUMENT_LIST_BASIC], - }), - ); - - expect($('#some-id').length).toBe(1); - }); - - it('has custom classes applied', () => { - const $ = cheerio.load( - renderComponent('document-list', { - classes: 'custom-class', - documents: [EXAMPLE_DOCUMENT_LIST_BASIC, EXAMPLE_DOCUMENT_LIST_BASIC, EXAMPLE_DOCUMENT_LIST_BASIC], - }), - ); - - expect($('.ons-document-list').hasClass('custom-class')).toBe(true); - }); - - it('outputs the correct number of document items', () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [EXAMPLE_DOCUMENT_LIST_BASIC, EXAMPLE_DOCUMENT_LIST_BASIC, EXAMPLE_DOCUMENT_LIST_BASIC], - }), - ); - - expect($('.ons-document-list__item').length).toBe(3); - }); - - it('has the correct container if `fullWidth`', () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [{ ...EXAMPLE_DOCUMENT_LIST_BASIC, featured: true, fullWidth: true }], - }), - ); - - expect($('.ons-container').length).toBe(1); - }); - - it('has the correct container class if `fullWidth` and `wide`', () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [{ ...EXAMPLE_DOCUMENT_LIST_BASIC, featured: true, fullWidth: true, wide: true }], - }), - ); - - expect($('.ons-container--wide').length).toBe(1); - }); - - it('has the correct container class if `featured`', () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [{ ...EXAMPLE_DOCUMENT_LIST_BASIC, featured: true }], - }), - ); - - expect($('.ons-document-list__item--featured').length).toBe(1); - }); - - it('has the correct class for `showMetadataFirst`', () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [{ ...EXAMPLE_DOCUMENT_LIST_BASIC, showMetadataFirst: true }], - }), - ); - - expect($('.ons-document-list__item-header--reverse').length).toBe(1); - }); - - it('overrides the heading title tag when `titleTag` is provided', () => { - const $ = cheerio.load( - renderComponent('document-list', { - titleTag: 'h1', - documents: [EXAMPLE_DOCUMENT_LIST_BASIC], - }), - ); - const titleTag = $('.ons-document-list__item-title')[0].tagName; - expect(titleTag).toBe('h1'); - }); - - it('has expected `title`', () => { - const $ = cheerio.load(renderComponent('document-list', { documents: [EXAMPLE_DOCUMENT_LIST_BASIC] })); - const title = $('.ons-document-list__item-title a').html().trim(); - expect(title).toBe('Crime and justice'); - }); - - it('has expected `url` for the title', () => { - const $ = cheerio.load(renderComponent('document-list', { documents: [EXAMPLE_DOCUMENT_LIST_BASIC] })); - expect($('.ons-document-list__item-title a').attr('href')).toBe('#0'); - }); - - it('has expected `description`', () => { - const $ = cheerio.load(renderComponent('document-list', { documents: [EXAMPLE_DOCUMENT_LIST_BASIC] })); - const title = $('.ons-document-list__item-description').html().trim(); - expect(title).toBe('Some description'); - }); - }); - - describe('mode: with thumbnail', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load( - renderComponent('document-list', { - id: 'some-id', - documents: [EXAMPLE_DOCUMENT_LIST_WITH_THUMBNAIL], - }), - ); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has expected `srcset` attribute', () => { - const $ = cheerio.load(renderComponent('document-list', { documents: [EXAMPLE_DOCUMENT_LIST_WITH_THUMBNAIL] })); - - const srcset = $('.ons-document-list__image-link img').attr('srcset'); - expect(srcset).toBe('/example-small.png 1x, /example-large.png 2x'); - }); - - it('has expected `src` attribute', () => { - const $ = cheerio.load(renderComponent('document-list', { documents: [EXAMPLE_DOCUMENT_LIST_WITH_THUMBNAIL] })); - - const src = $('.ons-document-list__image-link img').attr('src'); - expect(src).toBe('/example-small.png'); - }); - - it('has the placeholder class if `thumbnail` is true', () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [{ ...EXAMPLE_DOCUMENT_LIST_BASIC, thumbnail: true }], - }), - ); - - expect($('.ons-document-list__image-link').hasClass('ons-document-list__image-link--placeholder')).toBe(true); - }); - }); - - describe('mode: with metadata `file` configuration', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [EXAMPLE_DOCUMENT_LIST_WITH_METADATA_FILE], - }), - ); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has visually hidden `file` information after the title', () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [EXAMPLE_DOCUMENT_LIST_WITH_METADATA_FILE], - }), - ); - - const hiddenText = $('.ons-document-list__item-title a .ons-u-vh').text().trim(); - expect(hiddenText).toBe(', PDF document download, 499KB, 1 page'); - }); - - it('has `file` information displayed', () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [EXAMPLE_DOCUMENT_LIST_WITH_METADATA_FILE], - }), - ); - - const hiddenText = $('.ons-document-list__item-attribute').text().trim(); - expect(hiddenText).toBe('PDF, 499KB, 1 page'); - }); - }); - - describe('mode: with metadata `type` configuration', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [EXAMPLE_DOCUMENT_LIST_WITH_METADATA_TYPE], - }), - ); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the provided `url`', () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [EXAMPLE_DOCUMENT_LIST_WITH_METADATA_TYPE], - }), - ); - - const url = $('.ons-document-list__attribute-link').attr('href'); - expect(url).toBe('#0'); - }); - - it('has expected `text`', () => { - const $ = cheerio.load(renderComponent('document-list', { documents: [EXAMPLE_DOCUMENT_LIST_WITH_METADATA_TYPE] })); - const text = $('.ons-document-list__attribute-link > span').text().trim(); - expect(text).toBe('Poster:'); - }); - - it('has expected `ref`', () => { - const $ = cheerio.load(renderComponent('document-list', { documents: [EXAMPLE_DOCUMENT_LIST_WITH_METADATA_TYPE] })); - const text = $('.ons-document-list__attribute-link + span').text().trim(); - expect(text).toBe('some ref'); - }); - }); - - describe('mode: with metadata `date` configuration', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [ - { - ...EXAMPLE_DOCUMENT_LIST_BASIC, - metadata: { - date: { - iso: '2022-01-01', - short: '1 January 2022', - }, - }, - }, - ], - }), - ); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the default `prefix` text', () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [ - { - ...EXAMPLE_DOCUMENT_LIST_BASIC, - metadata: { - date: { - iso: '2022-01-01', - short: '1 January 2022', - }, - }, - }, - ], - }), - ); - - const text = $('.ons-document-list__item-attribute > span').text().trim(); - expect(text).toBe('Published:'); - }); - - it('has the visually hidden class for `prefix` text', () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [ - { - ...EXAMPLE_DOCUMENT_LIST_BASIC, - metadata: { - date: { - iso: '2022-01-01', - short: '1 January 2022', - }, - }, - }, - ], - }), - ); - expect($('.ons-document-list__item-attribute > span').hasClass('ons-u-vh')).toBe(true); - }); - - it('has the provided `prefix` text', () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [ - { - ...EXAMPLE_DOCUMENT_LIST_BASIC, - metadata: { - date: { - prefix: 'Released', - iso: '2022-01-01', - short: '1 January 2022', - }, - }, - }, - ], - }), - ); - - const text = $('.ons-document-list__item-attribute > span').text().trim(); - expect(text).toBe('Released:'); - }); - - it('has the correct class for `showPrefix`', () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [ - { - ...EXAMPLE_DOCUMENT_LIST_BASIC, - metadata: { - date: { - showPrefix: true, - iso: '2022-01-01', - short: '1 January 2022', - }, - }, - }, - ], - }), - ); - - expect($('.ons-document-list__item-attribute > span').hasClass('ons-u-fw-b')).toBe(true); - }); - - it('has the correct datetime attribute value', () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [ - { - ...EXAMPLE_DOCUMENT_LIST_BASIC, - metadata: { - date: { - iso: '2022-01-01', - short: '1 January 2022', - }, - }, - }, - ], - }), - ); - expect($('time').attr('datetime')).toBe('2022-01-01'); - }); - - it('has the correct `time` value', () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [ - { - ...EXAMPLE_DOCUMENT_LIST_BASIC, - metadata: { - date: { - iso: '2022-01-01', - short: '1 January 2022', - }, - }, - }, - ], - }), - ); - - const time = $('.ons-document-list__item-attribute time').text().trim(); - expect(time).toBe('1 January 2022'); - }); - }); - - describe('mode: with all parameters', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [EXAMPLE_DOCUMENT_LIST_WITH_MULTIPLE], - }), - ); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the correct document thumbnail class', async () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [EXAMPLE_DOCUMENT_LIST_WITH_MULTIPLE], - }), - ); - - expect($('.ons-document-list__item-image').hasClass('ons-document-list__item-image--file')).toBe(true); - }); - - it('has the correct document list class', async () => { - const $ = cheerio.load( - renderComponent('document-list', { - documents: [EXAMPLE_DOCUMENT_LIST_WITH_MULTIPLE], - }), - ); - - expect($('.ons-document-list__item-attribute').hasClass('ons-u-mr-no')).toBe(true); + describe('global configuration', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load( + renderComponent('document-list', { + id: 'some-id', + documents: [EXAMPLE_DOCUMENT_LIST_BASIC, EXAMPLE_DOCUMENT_LIST_BASIC, EXAMPLE_DOCUMENT_LIST_BASIC], + }), + ); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has the provided `id` attribute', () => { + const $ = cheerio.load( + renderComponent('document-list', { + id: 'some-id', + documents: [EXAMPLE_DOCUMENT_LIST_BASIC, EXAMPLE_DOCUMENT_LIST_BASIC, EXAMPLE_DOCUMENT_LIST_BASIC], + }), + ); + + expect($('#some-id').length).toBe(1); + }); + + it('has custom classes applied', () => { + const $ = cheerio.load( + renderComponent('document-list', { + classes: 'custom-class', + documents: [EXAMPLE_DOCUMENT_LIST_BASIC, EXAMPLE_DOCUMENT_LIST_BASIC, EXAMPLE_DOCUMENT_LIST_BASIC], + }), + ); + + expect($('.ons-document-list').hasClass('custom-class')).toBe(true); + }); + + it('outputs the correct number of document items', () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [EXAMPLE_DOCUMENT_LIST_BASIC, EXAMPLE_DOCUMENT_LIST_BASIC, EXAMPLE_DOCUMENT_LIST_BASIC], + }), + ); + + expect($('.ons-document-list__item').length).toBe(3); + }); + + it('has the correct container if `fullWidth`', () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [{ ...EXAMPLE_DOCUMENT_LIST_BASIC, featured: true, fullWidth: true }], + }), + ); + + expect($('.ons-container').length).toBe(1); + }); + + it('has the correct container class if `fullWidth` and `wide`', () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [{ ...EXAMPLE_DOCUMENT_LIST_BASIC, featured: true, fullWidth: true, wide: true }], + }), + ); + + expect($('.ons-container--wide').length).toBe(1); + }); + + it('has the correct container class if `featured`', () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [{ ...EXAMPLE_DOCUMENT_LIST_BASIC, featured: true }], + }), + ); + + expect($('.ons-document-list__item--featured').length).toBe(1); + }); + + it('has the correct class for `showMetadataFirst`', () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [{ ...EXAMPLE_DOCUMENT_LIST_BASIC, showMetadataFirst: true }], + }), + ); + + expect($('.ons-document-list__item-header--reverse').length).toBe(1); + }); + + it('overrides the heading title tag when `titleTag` is provided', () => { + const $ = cheerio.load( + renderComponent('document-list', { + titleTag: 'h1', + documents: [EXAMPLE_DOCUMENT_LIST_BASIC], + }), + ); + const titleTag = $('.ons-document-list__item-title')[0].tagName; + expect(titleTag).toBe('h1'); + }); + + it('has expected `title`', () => { + const $ = cheerio.load(renderComponent('document-list', { documents: [EXAMPLE_DOCUMENT_LIST_BASIC] })); + const title = $('.ons-document-list__item-title a').html().trim(); + expect(title).toBe('Crime and justice'); + }); + + it('has expected `url` for the title', () => { + const $ = cheerio.load(renderComponent('document-list', { documents: [EXAMPLE_DOCUMENT_LIST_BASIC] })); + expect($('.ons-document-list__item-title a').attr('href')).toBe('#0'); + }); + + it('has expected `description`', () => { + const $ = cheerio.load(renderComponent('document-list', { documents: [EXAMPLE_DOCUMENT_LIST_BASIC] })); + const title = $('.ons-document-list__item-description').html().trim(); + expect(title).toBe('Some description'); + }); + }); + + describe('mode: with thumbnail', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load( + renderComponent('document-list', { + id: 'some-id', + documents: [EXAMPLE_DOCUMENT_LIST_WITH_THUMBNAIL], + }), + ); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has expected `srcset` attribute', () => { + const $ = cheerio.load(renderComponent('document-list', { documents: [EXAMPLE_DOCUMENT_LIST_WITH_THUMBNAIL] })); + + const srcset = $('.ons-document-list__image-link img').attr('srcset'); + expect(srcset).toBe('/example-small.png 1x, /example-large.png 2x'); + }); + + it('has expected `src` attribute', () => { + const $ = cheerio.load(renderComponent('document-list', { documents: [EXAMPLE_DOCUMENT_LIST_WITH_THUMBNAIL] })); + + const src = $('.ons-document-list__image-link img').attr('src'); + expect(src).toBe('/example-small.png'); + }); + + it('has the placeholder class if `thumbnail` is true', () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [{ ...EXAMPLE_DOCUMENT_LIST_BASIC, thumbnail: true }], + }), + ); + + expect($('.ons-document-list__image-link').hasClass('ons-document-list__image-link--placeholder')).toBe(true); + }); + }); + + describe('mode: with metadata `file` configuration', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [EXAMPLE_DOCUMENT_LIST_WITH_METADATA_FILE], + }), + ); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has visually hidden `file` information after the title', () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [EXAMPLE_DOCUMENT_LIST_WITH_METADATA_FILE], + }), + ); + + const hiddenText = $('.ons-document-list__item-title a .ons-u-vh').text().trim(); + expect(hiddenText).toBe(', PDF document download, 499KB, 1 page'); + }); + + it('has `file` information displayed', () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [EXAMPLE_DOCUMENT_LIST_WITH_METADATA_FILE], + }), + ); + + const hiddenText = $('.ons-document-list__item-attribute').text().trim(); + expect(hiddenText).toBe('PDF, 499KB, 1 page'); + }); + }); + + describe('mode: with metadata `type` configuration', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [EXAMPLE_DOCUMENT_LIST_WITH_METADATA_TYPE], + }), + ); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has the provided `url`', () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [EXAMPLE_DOCUMENT_LIST_WITH_METADATA_TYPE], + }), + ); + + const url = $('.ons-document-list__attribute-link').attr('href'); + expect(url).toBe('#0'); + }); + + it('has expected `text`', () => { + const $ = cheerio.load(renderComponent('document-list', { documents: [EXAMPLE_DOCUMENT_LIST_WITH_METADATA_TYPE] })); + const text = $('.ons-document-list__attribute-link > span').text().trim(); + expect(text).toBe('Poster:'); + }); + + it('has expected `ref`', () => { + const $ = cheerio.load(renderComponent('document-list', { documents: [EXAMPLE_DOCUMENT_LIST_WITH_METADATA_TYPE] })); + const text = $('.ons-document-list__attribute-link + span').text().trim(); + expect(text).toBe('some ref'); + }); + }); + + describe('mode: with metadata `date` configuration', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [ + { + ...EXAMPLE_DOCUMENT_LIST_BASIC, + metadata: { + date: { + iso: '2022-01-01', + short: '1 January 2022', + }, + }, + }, + ], + }), + ); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has the default `prefix` text', () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [ + { + ...EXAMPLE_DOCUMENT_LIST_BASIC, + metadata: { + date: { + iso: '2022-01-01', + short: '1 January 2022', + }, + }, + }, + ], + }), + ); + + const text = $('.ons-document-list__item-attribute > span').text().trim(); + expect(text).toBe('Published:'); + }); + + it('has the visually hidden class for `prefix` text', () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [ + { + ...EXAMPLE_DOCUMENT_LIST_BASIC, + metadata: { + date: { + iso: '2022-01-01', + short: '1 January 2022', + }, + }, + }, + ], + }), + ); + expect($('.ons-document-list__item-attribute > span').hasClass('ons-u-vh')).toBe(true); + }); + + it('has the provided `prefix` text', () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [ + { + ...EXAMPLE_DOCUMENT_LIST_BASIC, + metadata: { + date: { + prefix: 'Released', + iso: '2022-01-01', + short: '1 January 2022', + }, + }, + }, + ], + }), + ); + + const text = $('.ons-document-list__item-attribute > span').text().trim(); + expect(text).toBe('Released:'); + }); + + it('has the correct class for `showPrefix`', () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [ + { + ...EXAMPLE_DOCUMENT_LIST_BASIC, + metadata: { + date: { + showPrefix: true, + iso: '2022-01-01', + short: '1 January 2022', + }, + }, + }, + ], + }), + ); + + expect($('.ons-document-list__item-attribute > span').hasClass('ons-u-fw-b')).toBe(true); + }); + + it('has the correct datetime attribute value', () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [ + { + ...EXAMPLE_DOCUMENT_LIST_BASIC, + metadata: { + date: { + iso: '2022-01-01', + short: '1 January 2022', + }, + }, + }, + ], + }), + ); + expect($('time').attr('datetime')).toBe('2022-01-01'); + }); + + it('has the correct `time` value', () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [ + { + ...EXAMPLE_DOCUMENT_LIST_BASIC, + metadata: { + date: { + iso: '2022-01-01', + short: '1 January 2022', + }, + }, + }, + ], + }), + ); + + const time = $('.ons-document-list__item-attribute time').text().trim(); + expect(time).toBe('1 January 2022'); + }); + }); + + describe('mode: with all parameters', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [EXAMPLE_DOCUMENT_LIST_WITH_MULTIPLE], + }), + ); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has the correct document thumbnail class', async () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [EXAMPLE_DOCUMENT_LIST_WITH_MULTIPLE], + }), + ); + + expect($('.ons-document-list__item-image').hasClass('ons-document-list__item-image--file')).toBe(true); + }); + + it('has the correct document list class', async () => { + const $ = cheerio.load( + renderComponent('document-list', { + documents: [EXAMPLE_DOCUMENT_LIST_WITH_MULTIPLE], + }), + ); + + expect($('.ons-document-list__item-attribute').hasClass('ons-u-mr-no')).toBe(true); + }); }); - }); }); diff --git a/src/components/document-list/document-list.scss b/src/components/document-list/document-list.scss index ea4e06ed1d..e60d70365b 100644 --- a/src/components/document-list/document-list.scss +++ b/src/components/document-list/document-list.scss @@ -1,184 +1,180 @@ .ons-document-list { - @extend .ons-list--bare; + @extend .ons-list--bare; - margin-bottom: 0; + margin-bottom: 0; - &__item { - border-bottom: 1px solid var(--ons-color-borders-light); - margin: 0 0 1.5rem; - padding: 0 0 1.5rem; + &__item { + border-bottom: 1px solid var(--ons-color-borders-light); + margin: 0 0 1.5rem; + padding: 0 0 1.5rem; - @include mq(xs) { - align-items: flex-start; - display: flex; - } + @include mq(xs) { + align-items: flex-start; + display: flex; + } - &:last-of-type { - border-bottom: 0; - margin: 0; - } - } + &:last-of-type { + border-bottom: 0; + margin: 0; + } - &__item-image { - flex: 0 0 auto; - margin-bottom: 1rem; - width: 136px; + // Featured document + &--featured { + background-color: var(--ons-color-banner-bg); + border-bottom: 0; + display: block; + outline: 2px solid transparent; // Add transparent outline because Windows High Contrast Mode doesn't show background + outline-offset: -2px; + padding: 2rem; + + @include mq(m) { + align-items: flex-start; + display: flex; + + .ons-container { + display: flex; + } + } - @include mq(xs) { - margin-right: 1.5rem; - } + // Increase thumbnail image size + .ons-document-list__item-image { + width: 248px; + + & &__image-link { + &--placeholder { + height: 96px; + } + } + + @include mq(m) { + margin-right: 2.5rem; + width: 379px; + + & &__image-link { + &--placeholder { + height: 248px; + } + } + } + } - @include mq(m) { - margin-bottom: 0; - } - } + // Show metadata above title + .ons-document-list__item-header { + &--reverse { + display: flex; + flex-direction: column-reverse; + margin-bottom: 0.5rem; + } + } - &__item-image & { - &__image-link { - &--placeholder { - height: 96px; - } + // Increase font size + .ons-document-list__item-title { + @extend .ons-u-fs-l; + } + + // If featured item is first in same list + + .ons-document-list__item { + border-top: 0; + margin-top: 0; + } + } + + // If featured item is full width + &--full-width { + padding: 2rem 0; + } } - } - &__item-image--file { - width: 96px; - } + &__item-image { + flex: 0 0 auto; + margin-bottom: 1rem; + width: 136px; - &__item-image--file & { - &__image-link { - border-color: var(--ons-color-borders-document-image); + @include mq(xs) { + margin-right: 1.5rem; + } - &--placeholder { - height: 136px; - } + @include mq(m) { + margin-bottom: 0; + } } - } - - &__image-link { - border: 2px solid transparent; - box-sizing: content-box; - display: block; - width: 100%; - - &:focus { - background-color: var(--ons-color-image-placeholder) !important; - border: 2px solid var(--ons-color-borders-document-image-focus); - box-shadow: none; - outline: 4px solid var(--ons-color-focus) !important; - outline-offset: 0; + + &__item-image & { + &__image-link { + &--placeholder { + height: 96px; + } + } } - &--placeholder { - background-clip: padding-box; - background-color: var(--ons-color-borders-document-image); - background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3e%3cpath fill='%23fff' d='M0 19.39c.49-1 1-2 1.55-2.93A31.59 31.59 0 0 1 0 11.72v7.67ZM3 0S0 0 0 3.7v2a34.85 34.85 0 0 0 2.17 9.76A31.2 31.2 0 0 1 8.3 8.3c4.84-4.16 11.36-7 20.21-8.29Zm28.84 2c-10.11 1-17 3.86-22 8.1a29.78 29.78 0 0 0-6.49 8C7.26 25.65 14.66 31.19 27 32h1.21A3.71 3.71 0 0 0 32 27.91V2a.41.41 0 0 1-.16 0Zm-26 21.49a25.94 25.94 0 0 1-3-4.4A48 48 0 0 0 0 25.71V32h20.23a26.41 26.41 0 0 1-14.39-8.49Z'/%3e%3c/svg%3e"); - background-position: center; - background-repeat: no-repeat; - background-size: 32px 32px; - height: 100%; + &__item-image--file { + width: 96px; } - } - &__item-metadata { - @extend .ons-list--bare; + &__item-image--file & { + &__image-link { + border-color: var(--ons-color-borders-document-image); - line-height: 1.2 !important; - } + &--placeholder { + height: 136px; + } + } + } - &__item-attribute { - color: var(--ons-color-text-metadata); - display: inline-block; - margin: 0 1rem 0 0; + &__image-link { + border: 2px solid transparent; + box-sizing: content-box; + display: block; + width: 100%; + + &:focus { + background-color: var(--ons-color-image-placeholder) !important; + border: 2px solid var(--ons-color-borders-document-image-focus); + box-shadow: none; + outline: 4px solid var(--ons-color-focus) !important; + outline-offset: 0; + } - @extend .ons-u-fs-s; - } + &--placeholder { + background-clip: padding-box; + background-color: var(--ons-color-borders-document-image); + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3e%3cpath fill='%23fff' d='M0 19.39c.49-1 1-2 1.55-2.93A31.59 31.59 0 0 1 0 11.72v7.67ZM3 0S0 0 0 3.7v2a34.85 34.85 0 0 0 2.17 9.76A31.2 31.2 0 0 1 8.3 8.3c4.84-4.16 11.36-7 20.21-8.29Zm28.84 2c-10.11 1-17 3.86-22 8.1a29.78 29.78 0 0 0-6.49 8C7.26 25.65 14.66 31.19 27 32h1.21A3.71 3.71 0 0 0 32 27.91V2a.41.41 0 0 1-.16 0Zm-26 21.49a25.94 25.94 0 0 1-3-4.4A48 48 0 0 0 0 25.71V32h20.23a26.41 26.41 0 0 1-14.39-8.49Z'/%3e%3c/svg%3e"); + background-position: center; + background-repeat: no-repeat; + background-size: 32px 32px; + height: 100%; + } + } - &__attribute-link { - color: inherit; + &__item-metadata { + @extend .ons-list--bare; - &:hover { - color: var(--ons-color-text-metadata); - text-decoration: underline solid var(--ons-color-text-metadata) 2px; + line-height: 1.2 !important; } - } - &__item-description { - margin-bottom: 0; - max-width: 660px; // Equivalent to 8 Columns + &__item-attribute { + color: var(--ons-color-text-metadata); + display: inline-block; + margin: 0 1rem 0 0; - p:last-of-type { - margin-bottom: 0; + @extend .ons-u-fs-s; } - } -} - -// Featured document -.ons-document-list { - &__item { - &--featured { - background-color: var(--ons-color-banner-bg); - border-bottom: none; - display: block; - outline: 2px solid transparent; // Add transparent outline because Windows High Contrast Mode doesn't show background - outline-offset: -2px; - padding: 2rem; - - @include mq(m) { - align-items: flex-start; - display: flex; - - .ons-container { - display: flex; - } - } - // Increase thumbnail image size - .ons-document-list__item-image { - width: 248px; + &__attribute-link { + color: inherit; - & &__image-link { - &--placeholder { - height: 96px; - } + &:hover { + color: var(--ons-color-text-metadata); + text-decoration: underline solid var(--ons-color-text-metadata) 2px; } + } - @include mq(m) { - margin-right: 2.5rem; - width: 379px; + &__item-description { + margin-bottom: 0; + max-width: 660px; // Equivalent to 8 Columns - & &__image-link { - &--placeholder { - height: 248px; - } - } - } - } - - // Show metadata above title - .ons-document-list__item-header { - &--reverse { - display: flex; - flex-direction: column-reverse; - margin-bottom: 0.5rem; + p:last-of-type { + margin-bottom: 0; } - } - - // Increase font size - .ons-document-list__item-title { - @extend .ons-u-fs-l; - } - - // If featured item is first in same list - & + .ons-document-list__item { - border-top: none; - margin-top: 0; - } - } - - // If featured item is full width - &--full-width { - padding: 2rem 0; } - } } diff --git a/src/components/download-resources/_download-resources.scss b/src/components/download-resources/_download-resources.scss index ccfacdee02..22a3cb884d 100644 --- a/src/components/download-resources/_download-resources.scss +++ b/src/components/download-resources/_download-resources.scss @@ -1,142 +1,143 @@ .ons-adv-filter { - &__gallery { - border-top: 1px solid var(--ons-color-borders-light); - margin-left: 0; - padding-left: 0; - padding-top: 1.5rem; - } - - &__item { - margin-top: 1.5rem; - - &:first-of-type { - margin-top: 1.5rem; + &__gallery { + border-top: 1px solid var(--ons-color-borders-light); + margin-left: 0; + padding-left: 0; + padding-top: 1.5rem; } - .ons-checkboxes__item { - min-width: auto; - } - } - - &__selection { - color: var(--ons-color-grey-100); - margin-bottom: 0.5rem; - } - - &__results-options { - margin-bottom: 1.5rem; - } - - &__results-sort { - align-items: center; - display: flex; - margin-top: 0.5rem; - - .ons-label { - font-weight: $font-weight-regular; - margin-bottom: 0; - margin-right: 0.5rem; - } + &__item { + margin-top: 1.5rem; - .ons-input--select { - width: auto !important; - } - } - - &__panel { - display: none; - } - - &__panel--is-visible { - background-color: var(--ons-color-white); - inset: 0; - display: block; - height: calc(100% - 76px); // Height of action buttons - overflow-y: scroll; - padding: 1rem; - position: fixed; - z-index: 10; - } - - &__actions { - background-color: var(--ons-color-white); - bottom: 0; - box-shadow: 0 0 5px 0 rgb(34 34 34 / 50%), - 0 -1px 0 0 rgb(65 64 66 / 50%); - display: flex; - left: 0; - padding: 1rem; - position: fixed; - width: 100%; - z-index: 11; - - .ons-btn:first-of-type { - flex-grow: 1; + &:first-of-type { + margin-top: 1.5rem; + } + + .ons-checkboxes__item { + min-width: auto; + } } - .ons-btn + .ons-btn { - margin-left: 1rem; + &__selection { + color: var(--ons-color-grey-100); + margin-bottom: 0.5rem; } - } - @include mq(s) { &__results-options { - align-items: center; - display: flex; - justify-content: space-between; - margin-bottom: 1rem; + margin-bottom: 1.5rem; } &__results-sort { - margin-top: 0; // Reset - } - } + align-items: center; + display: flex; + margin-top: 0.5rem; - @include mq(m) { - &__actions { - display: none; - } + .ons-label { + font-weight: $font-weight-regular; + margin-bottom: 0; + margin-right: 0.5rem; + } - &__trigger { - display: none; + .ons-input--select { + width: auto !important; + } } &__panel { - display: block; + display: none; + } + + &__panel--is-visible { + background-color: var(--ons-color-white); + inset: 0; + display: block; + height: calc(100% - 76px); // Height of action buttons + overflow-y: scroll; + padding: 1rem; + position: fixed; + z-index: 10; } - .ons-no-scroll { - overflow: auto; + &__actions { + background-color: var(--ons-color-white); + bottom: 0; + box-shadow: + 0 0 5px 0 rgb(34 34 34 / 50%), + 0 -1px 0 0 rgb(65 64 66 / 50%); + display: flex; + left: 0; + padding: 1rem; + position: fixed; + width: 100%; + z-index: 11; + + .ons-btn:first-of-type { + flex-grow: 1; + } + + .ons-btn + .ons-btn { + margin-left: 1rem; + } + } + + @include mq(s) { + &__results-options { + align-items: center; + display: flex; + justify-content: space-between; + margin-bottom: 1rem; + } + + &__results-sort { + margin-top: 0; // Reset + } + } + + @include mq(m) { + &__actions { + display: none; + } + + &__trigger { + display: none; + } + + &__panel { + display: block; + } + + .ons-no-scroll { + overflow: auto; + } } - } } // No JavaScript body:not(.ons-js-enabled) { - .ons-adv-filter { - &__actions { - display: none; - } + .ons-adv-filter { + &__actions { + display: none; + } - &__panel { - display: block; - } + &__panel { + display: block; + } - &__trigger { - display: none; - } + &__trigger { + display: none; + } - &__no-results { - display: none; - } + &__no-results { + display: none; + } - &__selection { - display: none; + &__selection { + display: none; + } } - } } // Prevent scrollbars on body .ons-no-scroll { - overflow-y: hidden; + overflow-y: hidden; } diff --git a/src/components/download-resources/download-resources.js b/src/components/download-resources/download-resources.js index c2285d7fe2..44281a47e4 100644 --- a/src/components/download-resources/download-resources.js +++ b/src/components/download-resources/download-resources.js @@ -2,1079 +2,1086 @@ function Util() {} /* - Class manipulation functions + Class manipulation functions */ Util.hasClass = function (el, className) { - if (el.classList) return el.classList.contains(className); - else return !!el.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)')); + if (el.classList) return el.classList.contains(className); + else return !!el.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)')); }; Util.addClass = function (el, className) { - let classList = className.split(' '); - if (el.classList) el.classList.add(classList[0]); - else if (!Util.hasClass(el, classList[0])) el.className += ' ' + classList[0]; - if (classList.length > 1) Util.addClass(el, classList.slice(1).join(' ')); + let classList = className.split(' '); + if (el.classList) el.classList.add(classList[0]); + else if (!Util.hasClass(el, classList[0])) el.className += ' ' + classList[0]; + if (classList.length > 1) Util.addClass(el, classList.slice(1).join(' ')); }; Util.removeClass = function (el, className) { - let classList = className.split(' '); - if (el.classList) el.classList.remove(classList[0]); - else if (Util.hasClass(el, classList[0])) { - let reg = new RegExp('(\\s|^)' + classList[0] + '(\\s|$)'); - el.className = el.className.replace(reg, ' '); - } - if (classList.length > 1) Util.removeClass(el, classList.slice(1).join(' ')); + let classList = className.split(' '); + if (el.classList) el.classList.remove(classList[0]); + else if (Util.hasClass(el, classList[0])) { + let reg = new RegExp('(\\s|^)' + classList[0] + '(\\s|$)'); + el.className = el.className.replace(reg, ' '); + } + if (classList.length > 1) Util.removeClass(el, classList.slice(1).join(' ')); }; Util.toggleClass = function (el, className, bool) { - if (bool) Util.addClass(el, className); - else Util.removeClass(el, className); + if (bool) Util.addClass(el, className); + else Util.removeClass(el, className); }; Util.setAttributes = function (el, attrs) { - for (let key in attrs) { - el.setAttribute(key, attrs[key]); - } + for (let key in attrs) { + el.setAttribute(key, attrs[key]); + } }; /* - DOM manipulation + DOM manipulation */ Util.getChildrenByClassName = function (el, className) { - let childrenByClass = []; - for (let i = 0; i < el.children.length; i++) { - if (Util.hasClass(el.children[i], className)) childrenByClass.push(el.children[i]); - } - return childrenByClass; + let childrenByClass = []; + for (let i = 0; i < el.children.length; i++) { + if (Util.hasClass(el.children[i], className)) childrenByClass.push(el.children[i]); + } + return childrenByClass; }; Util.is = function (elem, selector) { - if (selector.nodeType) { - return elem === selector; - } + if (selector.nodeType) { + return elem === selector; + } - let qa = typeof selector === 'string' ? document.querySelectorAll(selector) : selector, - length = qa.length; + let qa = typeof selector === 'string' ? document.querySelectorAll(selector) : selector, + length = qa.length; - while (length--) { - if (qa[length] === elem) { - return true; + while (length--) { + if (qa[length] === elem) { + return true; + } } - } - return false; + return false; }; // Animate height of an element Util.setHeight = function (start, to, element, duration, cb) { - let change = to - start, - currentTime = null; - - let animateHeight = function (timestamp) { - if (!currentTime) currentTime = timestamp; - let progress = timestamp - currentTime; - let val = parseInt((progress / duration) * change + start); - element.style.height = val + 'px'; - if (progress < duration) { - window.requestAnimationFrame(animateHeight); - } else { - cb(); - } - }; + let change = to - start, + currentTime = null; + + let animateHeight = function (timestamp) { + if (!currentTime) currentTime = timestamp; + let progress = timestamp - currentTime; + let val = parseInt((progress / duration) * change + start); + element.style.height = val + 'px'; + if (progress < duration) { + window.requestAnimationFrame(animateHeight); + } else { + cb(); + } + }; - // Set the height of the element before starting animation -> fix bug on Safari - element.style.height = start + 'px'; - window.requestAnimationFrame(animateHeight); + // Set the height of the element before starting animation -> fix bug on Safari + element.style.height = start + 'px'; + window.requestAnimationFrame(animateHeight); }; // Smooth Scroll Util.scrollTo = function (final, duration, cb, scrollEl) { - let element = scrollEl || window; - let start = element.scrollTop || document.documentElement.scrollTop, - currentTime = null; - - if (!scrollEl) start = window.scrollY || document.documentElement.scrollTop; - - let animateScroll = function (timestamp) { - if (!currentTime) currentTime = timestamp; - let progress = timestamp - currentTime; - if (progress > duration) progress = duration; - let val = Math.easeInOutQuad(progress, start, final - start, duration); - element.scrollTo(0, val); - if (progress < duration) { - window.requestAnimationFrame(animateScroll); - } else { - cb && cb(); - } - }; + let element = scrollEl || window; + let start = element.scrollTop || document.documentElement.scrollTop, + currentTime = null; + + if (!scrollEl) start = window.scrollY || document.documentElement.scrollTop; + + let animateScroll = function (timestamp) { + if (!currentTime) currentTime = timestamp; + let progress = timestamp - currentTime; + if (progress > duration) progress = duration; + let val = Math.easeInOutQuad(progress, start, final - start, duration); + element.scrollTo(0, val); + if (progress < duration) { + window.requestAnimationFrame(animateScroll); + } else { + cb && cb(); + } + }; - window.requestAnimationFrame(animateScroll); + window.requestAnimationFrame(animateScroll); }; /* - Focus utility classes + Focus utility classes */ // Move focus to an element Util.moveFocus = function (element) { - if (!element) element = document.getElementsByTagName('body')[0]; - element.focus(); - if (document.activeElement !== element) { - element.setAttribute('tabindex', '-1'); + if (!element) element = document.getElementsByTagName('body')[0]; element.focus(); - } + if (document.activeElement !== element) { + element.setAttribute('tabindex', '-1'); + element.focus(); + } }; // Misc Util.getIndexInArray = function (array, el) { - return Array.prototype.indexOf.call(array, el); + return Array.prototype.indexOf.call(array, el); }; Util.cssSupports = function (property, value) { - if ('CSS' in window) { - return CSS.supports(property, value); - } else { - let jsProperty = property.replace(/-([a-z])/g, function (g) { - return g[1].toUpperCase(); - }); - return jsProperty in document.body.style; - } + if ('CSS' in window) { + return CSS.supports(property, value); + } else { + let jsProperty = property.replace(/-([a-z])/g, function (g) { + return g[1].toUpperCase(); + }); + return jsProperty in document.body.style; + } }; // Merge a set of user options into plugin defaults // https://gomakethings.com/vanilla-javascript-version-of-jquery-extend/ Util.extend = function () { - // Variables - let extended = {}; - let deep = false; - let i = 0; - let length = arguments.length; - - // Check if a deep merge - if (Object.prototype.toString.call(arguments[0]) === '[object Boolean]') { - deep = arguments[0]; - i++; - } - - // Merge the object into the extended object - let merge = function (obj) { - for (let prop in obj) { - if (Object.prototype.hasOwnProperty.call(obj, prop)) { - // If deep merge and property is an object, merge properties - if (deep && Object.prototype.toString.call(obj[prop]) === '[object Object]') { - extended[prop] = extend(true, extended[prop], obj[prop]); - } else { - extended[prop] = obj[prop]; - } - } + // Variables + let extended = {}; + let deep = false; + let i = 0; + let length = arguments.length; + + // Check if a deep merge + if (Object.prototype.toString.call(arguments[0]) === '[object Boolean]') { + deep = arguments[0]; + i++; } - }; - // Loop through each object and conduct a merge - for (; i < length; i++) { - let obj = arguments[i]; - merge(obj); - } + // Merge the object into the extended object + let merge = function (obj) { + for (let prop in obj) { + if (Object.prototype.hasOwnProperty.call(obj, prop)) { + // If deep merge and property is an object, merge properties + if (deep && Object.prototype.toString.call(obj[prop]) === '[object Object]') { + extended[prop] = extend(true, extended[prop], obj[prop]); + } else { + extended[prop] = obj[prop]; + } + } + } + }; - return extended; + // Loop through each object and conduct a merge + for (; i < length; i++) { + let obj = arguments[i]; + merge(obj); + } + + return extended; }; // Check if Reduced Motion is enabled Util.osHasReducedMotion = function () { - if (!window.matchMedia) return false; - let matchMediaObj = window.matchMedia('(prefers-reduced-motion: reduce)'); - if (matchMediaObj) return matchMediaObj.matches; - return false; // Return false if not supported + if (!window.matchMedia) return false; + let matchMediaObj = window.matchMedia('(prefers-reduced-motion: reduce)'); + if (matchMediaObj) return matchMediaObj.matches; + return false; // Return false if not supported }; /* - Polyfills + Polyfills */ // Closest() method if (!Element.prototype.matches) { - Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; + Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; } if (!Element.prototype.closest) { - Element.prototype.closest = function (s) { - let el = this; - if (!document.documentElement.contains(el)) return null; - do { - if (el.matches(s)) return el; - el = el.parentElement || el.parentNode; - } while (el !== null && el.nodeType === 1); - return null; - }; + Element.prototype.closest = function (s) { + let el = this; + if (!document.documentElement.contains(el)) return null; + do { + if (el.matches(s)) return el; + el = el.parentElement || el.parentNode; + } while (el !== null && el.nodeType === 1); + return null; + }; } // Custom Event() constructor if (typeof window.CustomEvent !== 'function') { - function CustomEvent(event, params) { - params = params || { bubbles: false, cancelable: false, detail: undefined }; - let evt = document.createEvent('CustomEvent'); - evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); - return evt; - } + function CustomEvent(event, params) { + params = params || { bubbles: false, cancelable: false, detail: undefined }; + let evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; + } - CustomEvent.prototype = window.Event.prototype; + CustomEvent.prototype = window.Event.prototype; - window.CustomEvent = CustomEvent; + window.CustomEvent = CustomEvent; } /* - Animation curves + Animation curves */ Math.easeInOutQuad = function (t, b, c, d) { - t /= d / 2; - if (t < 1) return (c / 2) * t * t + b; - t--; - return (-c / 2) * (t * (t - 2) - 1) + b; + t /= d / 2; + if (t < 1) return (c / 2) * t * t + b; + t--; + return (-c / 2) * (t * (t - 2) - 1) + b; }; // FILTERS (function () { - let Filter = function (opts) { - this.options = Util.extend(Filter.defaults, opts); // Used to store custom filter/sort functions - this.element = this.options.element; - this.elementId = this.element.getAttribute('id'); - this.items = this.element.querySelectorAll('.ons-js-filter__item'); - this.controllers = document.querySelectorAll('[aria-controls="' + this.elementId + '"]'); // Controllers wrappers - this.fallbackMessage = document.querySelector('[data-fallback-gallery-id="' + this.elementId + '"]'); - this.filterString = []; // Combination of different filter values - this.sortingString = ''; // Sort value - will include order and type of argument (e.g., number or string) - - // Store info about sorted/filtered items - this.filterList = []; // List of boolean for each this.item -> true if still visible , otherwise false - this.sortingList = []; // List of new ordered this.item -> each element is [item, originalIndex] - - // Store grid info for animation - this.itemsGrid = []; // Grid coordinates - this.itemsInitPosition = []; // Used to store coordinates of this.items before animation - this.itemsIterPosition = []; // Used to store coordinates of this.items before animation - intermediate state - this.itemsFinalPosition = []; // Used to store coordinates of this.items after filtering - - // Animation off - this.animateOff = this.element.getAttribute('data-filter-animation') == 'off'; - - // Used to update this.itemsGrid on resize - this.resizingId = false; - - // Default acceleration style - improve animation - this.accelerateStyle = 'will-change: transform, opacity; transform: translateZ(0); backface-visibility: hidden;'; - - // Handle multiple changes - this.animating = false; - this.reanimate = false; - - initFilter(this); - }; - - function initFilter(filter) { - resetFilterSortArray(filter, true, true); // Init array filter.filterList/filter.sortingList - createGridInfo(filter); // Store grid coordinates in filter.itemsGrid - initItemsOrder(filter); // Add data-orders so that we can reset the sorting - - // Events handling - filter update - for (let i = 0; i < filter.controllers.length; i++) { - filter.filterString[i] = ''; // Reset filtering - - // Get proper filter/sorting string based on selected controllers - (function (i) { - filter.controllers[i].addEventListener('change', function (event) { - if (event.target.tagName.toLowerCase() == 'select') { - // Select elements - !event.target.getAttribute('data-filter') - ? setSortingString(filter, event.target.value, event.target.options[event.target.selectedIndex]) - : setFilterString(filter, i, 'select'); - } else if ( - event.target.tagName.toLowerCase() == 'input' && - (event.target.getAttribute('type') == 'radio' || event.target.getAttribute('type') == 'checkbox') - ) { - // Input (radio/checkboxed) elements - !event.target.getAttribute('data-filter') - ? setSortingString(filter, event.target.getAttribute('data-sort'), event.target) - : setFilterString(filter, i, 'input'); - } else { - // Generic inout element - !filter.controllers[i].getAttribute('data-filter') - ? setSortingString(filter, filter.controllers[i].getAttribute('data-sort'), filter.controllers[i]) - : setFilterString(filter, i, 'custom'); - } - - updateFilterArray(filter); + let Filter = function (opts) { + this.options = Util.extend(Filter.defaults, opts); // Used to store custom filter/sort functions + this.element = this.options.element; + this.elementId = this.element.getAttribute('id'); + this.items = this.element.querySelectorAll('.ons-js-filter__item'); + this.controllers = document.querySelectorAll('[aria-controls="' + this.elementId + '"]'); // Controllers wrappers + this.fallbackMessage = document.querySelector('[data-fallback-gallery-id="' + this.elementId + '"]'); + this.filterString = []; // Combination of different filter values + this.sortingString = ''; // Sort value - will include order and type of argument (e.g., number or string) + + // Store info about sorted/filtered items + this.filterList = []; // List of boolean for each this.item -> true if still visible , otherwise false + this.sortingList = []; // List of new ordered this.item -> each element is [item, originalIndex] + + // Store grid info for animation + this.itemsGrid = []; // Grid coordinates + this.itemsInitPosition = []; // Used to store coordinates of this.items before animation + this.itemsIterPosition = []; // Used to store coordinates of this.items before animation - intermediate state + this.itemsFinalPosition = []; // Used to store coordinates of this.items after filtering + + // Animation off + this.animateOff = this.element.getAttribute('data-filter-animation') == 'off'; + + // Used to update this.itemsGrid on resize + this.resizingId = false; + + // Default acceleration style - improve animation + this.accelerateStyle = 'will-change: transform, opacity; transform: translateZ(0); backface-visibility: hidden;'; + + // Handle multiple changes + this.animating = false; + this.reanimate = false; + + initFilter(this); + }; + + function initFilter(filter) { + resetFilterSortArray(filter, true, true); // Init array filter.filterList/filter.sortingList + createGridInfo(filter); // Store grid coordinates in filter.itemsGrid + initItemsOrder(filter); // Add data-orders so that we can reset the sorting + + // Events handling - filter update + for (let i = 0; i < filter.controllers.length; i++) { + filter.filterString[i] = ''; // Reset filtering + + // Get proper filter/sorting string based on selected controllers + (function (i) { + filter.controllers[i].addEventListener('change', function (event) { + if (event.target.tagName.toLowerCase() == 'select') { + // Select elements + !event.target.getAttribute('data-filter') + ? setSortingString(filter, event.target.value, event.target.options[event.target.selectedIndex]) + : setFilterString(filter, i, 'select'); + } else if ( + event.target.tagName.toLowerCase() == 'input' && + (event.target.getAttribute('type') == 'radio' || event.target.getAttribute('type') == 'checkbox') + ) { + // Input (radio/checkboxed) elements + !event.target.getAttribute('data-filter') + ? setSortingString(filter, event.target.getAttribute('data-sort'), event.target) + : setFilterString(filter, i, 'input'); + } else { + // Generic inout element + !filter.controllers[i].getAttribute('data-filter') + ? setSortingString(filter, filter.controllers[i].getAttribute('data-sort'), filter.controllers[i]) + : setFilterString(filter, i, 'custom'); + } + + updateFilterArray(filter); + }); + + filter.controllers[i].addEventListener('click', function (event) { + // Return if target is select/input elements + let filterEl = event.target.closest('[data-filter]'); + let sortEl = event.target.closest('[data-sort]'); + if (!filterEl && !sortEl) return; + if (filterEl && (filterEl.tagName.toLowerCase() == 'input' || filterEl.tagName.toLowerCase() == 'select')) return; + if (sortEl && (sortEl.tagName.toLowerCase() == 'input' || sortEl.tagName.toLowerCase() == 'select')) return; + if (sortEl && Util.hasClass(sortEl, 'ons-js-filter__custom-control')) return; + if (filterEl && Util.hasClass(filterEl, 'ons-js-filter__custom-control')) return; + + // This will be executed only for a list of buttons -> no inputs + event.preventDefault(); + resetControllersList(filter, i, filterEl, sortEl); + sortEl ? setSortingString(filter, sortEl.getAttribute('data-sort'), sortEl) : setFilterString(filter, i, 'button'); + updateFilterArray(filter); + }); + })(i); + } + + // Handle resize - update grid coordinates in filter.itemsGrid + window.addEventListener('resize', function () { + clearTimeout(filter.resizingId); + filter.resizingId = setTimeout(function () { + createGridInfo(filter); + }, 300); }); - filter.controllers[i].addEventListener('click', function (event) { - // Return if target is select/input elements - let filterEl = event.target.closest('[data-filter]'); - let sortEl = event.target.closest('[data-sort]'); - if (!filterEl && !sortEl) return; - if (filterEl && (filterEl.tagName.toLowerCase() == 'input' || filterEl.tagName.toLowerCase() == 'select')) return; - if (sortEl && (sortEl.tagName.toLowerCase() == 'input' || sortEl.tagName.toLowerCase() == 'select')) return; - if (sortEl && Util.hasClass(sortEl, 'ons-js-filter__custom-control')) return; - if (filterEl && Util.hasClass(filterEl, 'ons-js-filter__custom-control')) return; - - // This will be executed only for a list of buttons -> no inputs - event.preventDefault(); - resetControllersList(filter, i, filterEl, sortEl); - sortEl ? setSortingString(filter, sortEl.getAttribute('data-sort'), sortEl) : setFilterString(filter, i, 'button'); - updateFilterArray(filter); + // Check if there are filters/sorting values already set + checkInitialFiltering(filter); + + // Reset filtering results if filter selection was changed by an external control (e.g., form reset) + filter.element.addEventListener('update-filter-results', function () { + // Reset filters first + for (let i = 0; i < filter.controllers.length; i++) filter.filterString[i] = ''; + filter.sortingString = ''; + checkInitialFiltering(filter); }); - })(i); } - // Handle resize - update grid coordinates in filter.itemsGrid - window.addEventListener('resize', function () { - clearTimeout(filter.resizingId); - filter.resizingId = setTimeout(function () { - createGridInfo(filter); - }, 300); - }); - - // Check if there are filters/sorting values already set - checkInitialFiltering(filter); - - // Reset filtering results if filter selection was changed by an external control (e.g., form reset) - filter.element.addEventListener('update-filter-results', function () { - // Reset filters first - for (let i = 0; i < filter.controllers.length; i++) filter.filterString[i] = ''; - filter.sortingString = ''; - checkInitialFiltering(filter); - }); - } - - function checkInitialFiltering(filter) { - for (let i = 0; i < filter.controllers.length; i++) { - // Check if there’s a selected option - // Buttons list - let selectedButton = filter.controllers[i].getElementsByClassName('ons-js-filter-selected'); - if (selectedButton.length > 0) { - let sort = selectedButton[0].getAttribute('data-sort'); - sort - ? setSortingString(filter, selectedButton[0].getAttribute('data-sort'), selectedButton[0]) - : setFilterString(filter, i, 'button'); - continue; - } - - // Input list - let selectedInput = filter.controllers[i].querySelectorAll('input:checked'); - if (selectedInput.length > 0) { - let sort = selectedInput[0].getAttribute('data-sort'); - sort ? setSortingString(filter, sort, selectedInput[0]) : setFilterString(filter, i, 'input'); - continue; - } - - // Select item - if (filter.controllers[i].tagName.toLowerCase() == 'select') { - let sort = filter.controllers[i].getAttribute('data-sort'); - sort - ? setSortingString(filter, filter.controllers[i].value, filter.controllers[i].options[filter.controllers[i].selectedIndex]) - : setFilterString(filter, i, 'select'); - continue; - } - - // Check if there’s a generic custom input - let radioInput = filter.controllers[i].querySelector('input[type="radio"]'), - checkboxInput = filter.controllers[i].querySelector('input[type="checkbox"]'); - if (!radioInput && !checkboxInput) { - let sort = filter.controllers[i].getAttribute('data-sort'); - let filterString = filter.controllers[i].getAttribute('data-filter'); - if (sort) setSortingString(filter, sort, filter.controllers[i]); - else if (filterString) setFilterString(filter, i, 'custom'); - } - } + function checkInitialFiltering(filter) { + for (let i = 0; i < filter.controllers.length; i++) { + // Check if there’s a selected option + // Buttons list + let selectedButton = filter.controllers[i].getElementsByClassName('ons-js-filter-selected'); + if (selectedButton.length > 0) { + let sort = selectedButton[0].getAttribute('data-sort'); + sort + ? setSortingString(filter, selectedButton[0].getAttribute('data-sort'), selectedButton[0]) + : setFilterString(filter, i, 'button'); + continue; + } + + // Input list + let selectedInput = filter.controllers[i].querySelectorAll('input:checked'); + if (selectedInput.length > 0) { + let sort = selectedInput[0].getAttribute('data-sort'); + sort ? setSortingString(filter, sort, selectedInput[0]) : setFilterString(filter, i, 'input'); + continue; + } + + // Select item + if (filter.controllers[i].tagName.toLowerCase() == 'select') { + let sort = filter.controllers[i].getAttribute('data-sort'); + sort + ? setSortingString( + filter, + filter.controllers[i].value, + filter.controllers[i].options[filter.controllers[i].selectedIndex], + ) + : setFilterString(filter, i, 'select'); + continue; + } + + // Check if there’s a generic custom input + let radioInput = filter.controllers[i].querySelector('input[type="radio"]'), + checkboxInput = filter.controllers[i].querySelector('input[type="checkbox"]'); + if (!radioInput && !checkboxInput) { + let sort = filter.controllers[i].getAttribute('data-sort'); + let filterString = filter.controllers[i].getAttribute('data-filter'); + if (sort) setSortingString(filter, sort, filter.controllers[i]); + else if (filterString) setFilterString(filter, i, 'custom'); + } + } - updateFilterArray(filter); - } - - function setSortingString(filter, value, item) { - // Get sorting string value-> sortName:order:type - let order = item.getAttribute('data-sort-order') ? 'desc' : 'asc'; - let type = item.getAttribute('data-sort-number') ? 'number' : 'string'; - filter.sortingString = value + ':' + order + ':' + type; - } - - function setFilterString(filter, index, type) { - // Get filtering array -> [filter1:filter2, filter3, filter4:filter5] - if (type == 'input') { - let checkedInputs = filter.controllers[index].querySelectorAll('input:checked'); - filter.filterString[index] = ''; - for (let i = 0; i < checkedInputs.length; i++) { - filter.filterString[index] = filter.filterString[index] + checkedInputs[i].getAttribute('data-filter') + ':'; - } - } else if (type == 'select') { - if (filter.controllers[index].multiple) { - // Select with multiple options - filter.filterString[index] = getMultipleSelectValues(filter.controllers[index]); - } else { - // Select with single option - filter.filterString[index] = filter.controllers[index].value; - } - } else if (type == 'button') { - let selectedButtons = filter.controllers[index].querySelectorAll('.ons-js-filter-selected'); - filter.filterString[index] = ''; - for (let i = 0; i < selectedButtons.length; i++) { - filter.filterString[index] = filter.filterString[index] + selectedButtons[i].getAttribute('data-filter') + ':'; - } - } else if (type == 'custom') { - filter.filterString[index] = filter.controllers[index].getAttribute('data-filter'); - } - } - - function resetControllersList(filter, index, target1, target2) { - // For a
    ${renderComponent('document-list', { - id: 'adv-filter-gallery', - classes: 'ons-adv-filter__gallery ons-js-adv-filter__gallery', - attributes: { - 'data-filter-animation': 'off', - }, - documents: [ - { - classes: 'ons-filter__item ons-js-filter__item', - attributes: { - 'data-filter': 'general-public booklet', - 'data-sort-index': '1', - }, - url: '/example-booklet-1', - title: 'Example booklet 1', - description: 'The first example booklet.', - }, - { - classes: 'ons-filter__item ons-js-filter__item', - attributes: { - 'data-filter': 'general-public booklet logo', - 'data-sort-index': '2', - }, - url: '/example-booklet-2', - title: 'Example booklet 2 with logo', - description: 'The second example booklet with a logo.', + id: 'adv-filter-gallery', + classes: 'ons-adv-filter__gallery ons-js-adv-filter__gallery', + attributes: { + 'data-filter-animation': 'off', }, - { - classes: 'ons-filter__item ons-js-filter__item', - attributes: { - 'data-filter': 'logo', - 'data-sort-index': '3', - }, - url: '/example-logo', - title: 'Example logo', - description: 'An example logo.', - }, - ], + documents: [ + { + classes: 'ons-filter__item ons-js-filter__item', + attributes: { + 'data-filter': 'general-public booklet', + 'data-sort-index': '1', + }, + url: '/example-booklet-1', + title: 'Example booklet 1', + description: 'The first example booklet.', + }, + { + classes: 'ons-filter__item ons-js-filter__item', + attributes: { + 'data-filter': 'general-public booklet logo', + 'data-sort-index': '2', + }, + url: '/example-booklet-2', + title: 'Example booklet 2 with logo', + description: 'The second example booklet with a logo.', + }, + { + classes: 'ons-filter__item ons-js-filter__item', + attributes: { + 'data-filter': 'logo', + 'data-sort-index': '3', + }, + url: '/example-logo', + title: 'Example logo', + description: 'An example logo.', + }, + ], })}
    @@ -186,393 +186,393 @@ const EXAMPLE_PAGE = ` const RENDERED_EXAMPLE_PAGE = renderTemplate(EXAMPLE_PAGE); describe('script: download-resources', () => { - describe('no filtering', () => { - beforeEach(async () => { - await setTestPage('/test', RENDERED_EXAMPLE_PAGE); - }); - - it('shows all documents', async () => { - const hiddenTitles = await getHiddenDocumentTitles(page); - expect(hiddenTitles).toEqual([]); - }); - - it('updates filter selection labels ', async () => { - const filterSelectionLabels = await getFilterSelectionLabels(page); - expect(filterSelectionLabels).toEqual(['All audiences', 'All types']); - }); + describe('no filtering', () => { + beforeEach(async () => { + await setTestPage('/test', RENDERED_EXAMPLE_PAGE); + }); + + it('shows all documents', async () => { + const hiddenTitles = await getHiddenDocumentTitles(page); + expect(hiddenTitles).toEqual([]); + }); + + it('updates filter selection labels ', async () => { + const filterSelectionLabels = await getFilterSelectionLabels(page); + expect(filterSelectionLabels).toEqual(['All audiences', 'All types']); + }); + + it('updates result count text', async () => { + const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); + expect(resultsCount).toBe('3'); + }); + + it('hides the "No results" content', async () => { + const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); + expect(isHidden).toBe(true); + }); + }); + + describe('filtering one parameter where there are no results', () => { + beforeEach(async () => { + await setTestPage('/test', RENDERED_EXAMPLE_PAGE); + await page.click('#community-groups'); + }); + + it('hides all documents', async () => { + const hiddenTitles = await getHiddenDocumentTitles(page); + expect(hiddenTitles).toEqual(['Example booklet 1', 'Example booklet 2 with logo', 'Example logo']); + }); + + it('updates filter selection labels ', async () => { + const filterSelectionLabels = await getFilterSelectionLabels(page); + expect(filterSelectionLabels).toEqual(['Community groups', 'All types']); + }); + + it('updates result count text', async () => { + const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); + expect(resultsCount).toBe('0'); + }); + + it('shows the "No results" content', async () => { + const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); + expect(isHidden).toBe(false); + }); + }); + + describe('filtering one parameter where there are results', () => { + beforeEach(async () => { + await setTestPage('/test', RENDERED_EXAMPLE_PAGE); + await page.click('#general-public'); + }); + + it('hides non-matching documents', async () => { + const hiddenTitles = await getHiddenDocumentTitles(page); + expect(hiddenTitles).toEqual(['Example logo']); + }); + + it('updates filter selection labels ', async () => { + const filterSelectionLabels = await getFilterSelectionLabels(page); + expect(filterSelectionLabels).toEqual(['General public', 'All types']); + }); + + it('updates result count text', async () => { + const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); + expect(resultsCount).toBe('2'); + }); + + it('hides the "No results" content', async () => { + const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); + expect(isHidden).toBe(true); + }); + }); + + describe('filtering two values of the same parameter', () => { + beforeEach(async () => { + await setTestPage('/test', RENDERED_EXAMPLE_PAGE); + await page.click('#booklet'); + await page.click('#logo'); + }); + + it('hides non-matching documents', async () => { + const hiddenTitles = await getHiddenDocumentTitles(page); + expect(hiddenTitles).toEqual([]); + }); + + it('updates filter selection labels ', async () => { + const filterSelectionLabels = await getFilterSelectionLabels(page); + expect(filterSelectionLabels).toEqual(['All audiences', '2 filters selected']); + }); + + it('updates result count text', async () => { + const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); + expect(resultsCount).toBe('3'); + }); + + it('hides the "No results" content', async () => { + const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); + expect(isHidden).toBe(true); + }); + }); + + describe('filtering two parameters where there are no results', () => { + beforeEach(async () => { + await setTestPage('/test', RENDERED_EXAMPLE_PAGE); + await page.click('#community-groups'); + await page.click('#booklet'); + }); + + it('hides all documents', async () => { + const hiddenTitles = await getHiddenDocumentTitles(page); + expect(hiddenTitles).toEqual(['Example booklet 1', 'Example booklet 2 with logo', 'Example logo']); + }); + + it('updates filter selection labels ', async () => { + const filterSelectionLabels = await getFilterSelectionLabels(page); + expect(filterSelectionLabels).toEqual(['Community groups', 'Booklet']); + }); + + it('updates result count text', async () => { + const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); + expect(resultsCount).toBe('0'); + }); + + it('shows the "No results" content', async () => { + const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); + expect(isHidden).toBe(false); + }); + }); + + describe('filtering two parameters where results match both', () => { + beforeEach(async () => { + await setTestPage('/test', RENDERED_EXAMPLE_PAGE); + await page.click('#general-public'); + await page.click('#booklet'); + }); + + it('hides non-matching documents', async () => { + const hiddenTitles = await getHiddenDocumentTitles(page); + expect(hiddenTitles).toEqual(['Example logo']); + }); + + it('updates filter selection labels ', async () => { + const filterSelectionLabels = await getFilterSelectionLabels(page); + expect(filterSelectionLabels).toEqual(['General public', 'Booklet']); + }); + + it('updates result count text', async () => { + const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); + expect(resultsCount).toBe('2'); + }); + + it('hides the "No results" content', async () => { + const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); + expect(isHidden).toBe(true); + }); + }); + + describe('filtering two parameters where results do not match both', () => { + beforeEach(async () => { + await setTestPage('/test', RENDERED_EXAMPLE_PAGE); + await page.click('#general-public'); + await page.click('#logo'); + }); + + it('hides non-matching documents ', async () => { + const hiddenTitles = await getHiddenDocumentTitles(page); + expect(hiddenTitles).toEqual(['Example booklet 1', 'Example logo']); + }); + + it('updates filter selection labels ', async () => { + const filterSelectionLabels = await getFilterSelectionLabels(page); + expect(filterSelectionLabels).toEqual(['General public', 'Logo']); + }); + + it('updates result count text', async () => { + const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); + expect(resultsCount).toBe('1'); + }); + + it('hides the "No results" content', async () => { + const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); + expect(isHidden).toBe(true); + }); + }); + + describe('adding and then removing a filter', () => { + beforeEach(async () => { + await setTestPage('/test', RENDERED_EXAMPLE_PAGE); + await page.click('#general-public'); + await page.click('#logo'); + await page.click('#logo'); + }); + + it('hides non-matching documents ', async () => { + const hiddenTitles = await getHiddenDocumentTitles(page); + expect(hiddenTitles).toEqual(['Example logo']); + }); + + it('updates filter selection labels ', async () => { + const filterSelectionLabels = await getFilterSelectionLabels(page); + expect(filterSelectionLabels).toEqual(['General public', 'All types']); + }); + + it('updates result count text', async () => { + const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); + expect(resultsCount).toBe('2'); + }); + + it('hides the "No results" content', async () => { + const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); + expect(isHidden).toBe(true); + }); + }); + + describe('sorting', () => { + beforeEach(async () => { + await setTestPage('/test', RENDERED_EXAMPLE_PAGE); + }); + + it('sorts ascending by default', async () => { + const shownTitles = await getShownDocumentTitles(page); + expect(shownTitles).toEqual(['Example booklet 1', 'Example booklet 2 with logo', 'Example logo']); + }); + + it('sorts descending when "sort" selection is set to "desc"', async () => { + await page.type('#sort', 'O'); + + const shownTitles = await getShownDocumentTitles(page); + expect(shownTitles).toEqual(['Example logo', 'Example booklet 2 with logo', 'Example booklet 1']); + }); + }); + + describe('"Reset all filters" button', () => { + beforeEach(async () => { + await setTestPage('/test', RENDERED_EXAMPLE_PAGE); + await page.click('#general-public'); + await page.click('#logo'); + await page.click('.ons-js-adv-filter__reset'); + }); + + it('resets state of all filter checkboxes ', async () => { + const selector = '.ons-js-adv-filter__item .ons-js-checkbox'; + const checkboxStates = await page.$$eval(selector, (nodes) => nodes.map((node) => `${node.id}: ${node.checked}`)); + + expect(checkboxStates).toEqual(['community-groups: false', 'general-public: false', 'booklet: false', 'logo: false']); + }); + + it('shows all documents ', async () => { + const hiddenTitles = await getHiddenDocumentTitles(page); + expect(hiddenTitles).toEqual([]); + }); + + it('updates filter selection labels ', async () => { + const filterSelectionLabels = await getFilterSelectionLabels(page); + expect(filterSelectionLabels).toEqual(['All audiences', 'All types']); + }); + + it('updates result count text', async () => { + const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); + expect(resultsCount).toBe('3'); + }); + + it('hides the "No results" content', async () => { + const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); + expect(isHidden).toBe(true); + }); + }); + + describe('when the viewport is large', () => { + beforeEach(async () => { + await setViewport(page, { width: 1650, height: 1050 }); + await setTestPage('/test', RENDERED_EXAMPLE_PAGE); + }); + + afterEach(async () => { + // Clear viewport size and browser emulation after each test. + await jestPuppeteer.resetPage(); + }); + + it('hides elements that are only needed for mobile', async () => { + const displayStyle = await page.$eval('.ons-adv-filter__trigger', (node) => getComputedStyle(node).display); + expect(displayStyle).toBe('none'); + }); - it('updates result count text', async () => { - const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); - expect(resultsCount).toBe('3'); - }); - - it('hides the "No results" content', async () => { - const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); - expect(isHidden).toBe(true); - }); - }); + it('shows filter elements', async () => { + const displayStyle = await page.$eval('.ons-adv-filter__panel', (node) => getComputedStyle(node).display); + expect(displayStyle).not.toBe('none'); + }); + }); + + describe('when the viewport is small', () => { + beforeEach(async () => { + await page.emulate(iPhoneX); + await setTestPage('/test', RENDERED_EXAMPLE_PAGE); + }); - describe('filtering one parameter where there are no results', () => { - beforeEach(async () => { - await setTestPage('/test', RENDERED_EXAMPLE_PAGE); - await page.click('#community-groups'); - }); + afterEach(async () => { + // Clear viewport size and browser emulation after each test. + await jestPuppeteer.resetPage(); + }); - it('hides all documents', async () => { - const hiddenTitles = await getHiddenDocumentTitles(page); - expect(hiddenTitles).toEqual(['Example booklet 1', 'Example booklet 2 with logo', 'Example logo']); - }); + it('shows elements that are only needed for mobile', async () => { + const displayStyle = await page.$eval('.ons-adv-filter__trigger', (node) => getComputedStyle(node).display); + expect(displayStyle).not.toBe('none'); + }); - it('updates filter selection labels ', async () => { - const filterSelectionLabels = await getFilterSelectionLabels(page); - expect(filterSelectionLabels).toEqual(['Community groups', 'All types']); - }); + it('hides filter elements', async () => { + const displayStyle = await page.$eval('.ons-adv-filter__panel', (node) => getComputedStyle(node).display); + expect(displayStyle).toBe('none'); + }); - it('updates result count text', async () => { - const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); - expect(resultsCount).toBe('0'); - }); + it('shows filter elements when the "Show filters" button is pressed', async () => { + await page.click('.ons-js-adv-filter__trigger'); - it('shows the "No results" content', async () => { - const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); - expect(isHidden).toBe(false); - }); - }); + const displayStyle = await page.$eval('.ons-adv-filter__panel', (node) => getComputedStyle(node).display); + expect(displayStyle).not.toBe('none'); + }); - describe('filtering one parameter where there are results', () => { - beforeEach(async () => { - await setTestPage('/test', RENDERED_EXAMPLE_PAGE); - await page.click('#general-public'); - }); + it('hides the underlying page elements when the "Show filters" button is pressed', async () => { + await page.click('.ons-js-adv-filter__trigger'); - it('hides non-matching documents', async () => { - const hiddenTitles = await getHiddenDocumentTitles(page); - expect(hiddenTitles).toEqual(['Example logo']); - }); + const pageIsHidden = await page.$eval('.ons-page', (node) => node.classList.contains('ons-u-d-no')); + const pageIsAriaHidden = await page.$eval('.ons-page', (node) => node.getAttribute('aria-hidden')); - it('updates filter selection labels ', async () => { - const filterSelectionLabels = await getFilterSelectionLabels(page); - expect(filterSelectionLabels).toEqual(['General public', 'All types']); - }); + expect(pageIsHidden).toBe(true); + expect(pageIsAriaHidden).toBe('true'); + }); - it('updates result count text', async () => { - const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); - expect(resultsCount).toBe('2'); - }); + it('hides filter elements when the "Show (n results)" button is pressed', async () => { + await page.click('.ons-js-adv-filter__trigger'); + await page.click('.ons-js-adv-filter__show'); - it('hides the "No results" content', async () => { - const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); - expect(isHidden).toBe(true); - }); - }); + const displayStyle = await page.$eval('.ons-adv-filter__panel', (node) => getComputedStyle(node).display); + expect(displayStyle).toBe('none'); + }); - describe('filtering two values of the same parameter', () => { - beforeEach(async () => { - await setTestPage('/test', RENDERED_EXAMPLE_PAGE); - await page.click('#booklet'); - await page.click('#logo'); - }); + it('shows the underlying page elements when the "Show (n results)" button is pressed', async () => { + await page.click('.ons-js-adv-filter__trigger'); + await page.click('.ons-js-adv-filter__show'); - it('hides non-matching documents', async () => { - const hiddenTitles = await getHiddenDocumentTitles(page); - expect(hiddenTitles).toEqual([]); - }); + const pageIsHidden = await page.$eval('.ons-page', (node) => node.classList.contains('ons-u-d-no')); + const pageIsAriaHidden = await page.$eval('.ons-page', (node) => node.getAttribute('aria-hidden')); - it('updates filter selection labels ', async () => { - const filterSelectionLabels = await getFilterSelectionLabels(page); - expect(filterSelectionLabels).toEqual(['All audiences', '2 filters selected']); - }); + expect(pageIsHidden).toBe(false); + expect(pageIsAriaHidden).toBe('false'); + }); - it('updates result count text', async () => { - const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); - expect(resultsCount).toBe('3'); - }); - - it('hides the "No results" content', async () => { - const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); - expect(isHidden).toBe(true); - }); - }); - - describe('filtering two parameters where there are no results', () => { - beforeEach(async () => { - await setTestPage('/test', RENDERED_EXAMPLE_PAGE); - await page.click('#community-groups'); - await page.click('#booklet'); - }); - - it('hides all documents', async () => { - const hiddenTitles = await getHiddenDocumentTitles(page); - expect(hiddenTitles).toEqual(['Example booklet 1', 'Example booklet 2 with logo', 'Example logo']); - }); - - it('updates filter selection labels ', async () => { - const filterSelectionLabels = await getFilterSelectionLabels(page); - expect(filterSelectionLabels).toEqual(['Community groups', 'Booklet']); - }); - - it('updates result count text', async () => { - const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); - expect(resultsCount).toBe('0'); - }); - - it('shows the "No results" content', async () => { - const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); - expect(isHidden).toBe(false); - }); - }); - - describe('filtering two parameters where results match both', () => { - beforeEach(async () => { - await setTestPage('/test', RENDERED_EXAMPLE_PAGE); - await page.click('#general-public'); - await page.click('#booklet'); - }); - - it('hides non-matching documents', async () => { - const hiddenTitles = await getHiddenDocumentTitles(page); - expect(hiddenTitles).toEqual(['Example logo']); - }); - - it('updates filter selection labels ', async () => { - const filterSelectionLabels = await getFilterSelectionLabels(page); - expect(filterSelectionLabels).toEqual(['General public', 'Booklet']); - }); - - it('updates result count text', async () => { - const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); - expect(resultsCount).toBe('2'); - }); - - it('hides the "No results" content', async () => { - const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); - expect(isHidden).toBe(true); - }); - }); - - describe('filtering two parameters where results do not match both', () => { - beforeEach(async () => { - await setTestPage('/test', RENDERED_EXAMPLE_PAGE); - await page.click('#general-public'); - await page.click('#logo'); - }); - - it('hides non-matching documents ', async () => { - const hiddenTitles = await getHiddenDocumentTitles(page); - expect(hiddenTitles).toEqual(['Example booklet 1', 'Example logo']); - }); - - it('updates filter selection labels ', async () => { - const filterSelectionLabels = await getFilterSelectionLabels(page); - expect(filterSelectionLabels).toEqual(['General public', 'Logo']); - }); - - it('updates result count text', async () => { - const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); - expect(resultsCount).toBe('1'); - }); + it('hides filter elements when the "Close" button is pressed', async () => { + await page.click('.ons-js-adv-filter__trigger'); + await page.click('.ons-js-adv-filter__close'); - it('hides the "No results" content', async () => { - const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); - expect(isHidden).toBe(true); - }); - }); - - describe('adding and then removing a filter', () => { - beforeEach(async () => { - await setTestPage('/test', RENDERED_EXAMPLE_PAGE); - await page.click('#general-public'); - await page.click('#logo'); - await page.click('#logo'); - }); - - it('hides non-matching documents ', async () => { - const hiddenTitles = await getHiddenDocumentTitles(page); - expect(hiddenTitles).toEqual(['Example logo']); - }); - - it('updates filter selection labels ', async () => { - const filterSelectionLabels = await getFilterSelectionLabels(page); - expect(filterSelectionLabels).toEqual(['General public', 'All types']); - }); - - it('updates result count text', async () => { - const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); - expect(resultsCount).toBe('2'); - }); - - it('hides the "No results" content', async () => { - const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); - expect(isHidden).toBe(true); - }); - }); - - describe('sorting', () => { - beforeEach(async () => { - await setTestPage('/test', RENDERED_EXAMPLE_PAGE); - }); - - it('sorts ascending by default', async () => { - const shownTitles = await getShownDocumentTitles(page); - expect(shownTitles).toEqual(['Example booklet 1', 'Example booklet 2 with logo', 'Example logo']); - }); - - it('sorts descending when "sort" selection is set to "desc"', async () => { - await page.type('#sort', 'O'); - - const shownTitles = await getShownDocumentTitles(page); - expect(shownTitles).toEqual(['Example logo', 'Example booklet 2 with logo', 'Example booklet 1']); - }); - }); - - describe('"Reset all filters" button', () => { - beforeEach(async () => { - await setTestPage('/test', RENDERED_EXAMPLE_PAGE); - await page.click('#general-public'); - await page.click('#logo'); - await page.click('.ons-js-adv-filter__reset'); - }); - - it('resets state of all filter checkboxes ', async () => { - const selector = '.ons-js-adv-filter__item .ons-js-checkbox'; - const checkboxStates = await page.$$eval(selector, (nodes) => nodes.map((node) => `${node.id}: ${node.checked}`)); - - expect(checkboxStates).toEqual(['community-groups: false', 'general-public: false', 'booklet: false', 'logo: false']); - }); - - it('shows all documents ', async () => { - const hiddenTitles = await getHiddenDocumentTitles(page); - expect(hiddenTitles).toEqual([]); - }); - - it('updates filter selection labels ', async () => { - const filterSelectionLabels = await getFilterSelectionLabels(page); - expect(filterSelectionLabels).toEqual(['All audiences', 'All types']); - }); - - it('updates result count text', async () => { - const resultsCount = await page.$eval('.ons-js-adv-filter__results-count', (node) => node.textContent.trim()); - expect(resultsCount).toBe('3'); - }); - - it('hides the "No results" content', async () => { - const isHidden = await page.$eval('.ons-adv-filter__no-results', (node) => node.classList.contains('ons-u-hidden')); - expect(isHidden).toBe(true); - }); - }); - - describe('when the viewport is large', () => { - beforeEach(async () => { - await setViewport(page, { width: 1650, height: 1050 }); - await setTestPage('/test', RENDERED_EXAMPLE_PAGE); - }); - - afterEach(async () => { - // Clear viewport size and browser emulation after each test. - await jestPuppeteer.resetPage(); - }); - - it('hides elements that are only needed for mobile', async () => { - const displayStyle = await page.$eval('.ons-adv-filter__trigger', (node) => getComputedStyle(node).display); - expect(displayStyle).toBe('none'); - }); - - it('shows filter elements', async () => { - const displayStyle = await page.$eval('.ons-adv-filter__panel', (node) => getComputedStyle(node).display); - expect(displayStyle).not.toBe('none'); - }); - }); - - describe('when the viewport is small', () => { - beforeEach(async () => { - await page.emulate(iPhoneX); - await setTestPage('/test', RENDERED_EXAMPLE_PAGE); - }); - - afterEach(async () => { - // Clear viewport size and browser emulation after each test. - await jestPuppeteer.resetPage(); - }); - - it('shows elements that are only needed for mobile', async () => { - const displayStyle = await page.$eval('.ons-adv-filter__trigger', (node) => getComputedStyle(node).display); - expect(displayStyle).not.toBe('none'); - }); - - it('hides filter elements', async () => { - const displayStyle = await page.$eval('.ons-adv-filter__panel', (node) => getComputedStyle(node).display); - expect(displayStyle).toBe('none'); - }); - - it('shows filter elements when the "Show filters" button is pressed', async () => { - await page.click('.ons-js-adv-filter__trigger'); - - const displayStyle = await page.$eval('.ons-adv-filter__panel', (node) => getComputedStyle(node).display); - expect(displayStyle).not.toBe('none'); - }); - - it('hides the underlying page elements when the "Show filters" button is pressed', async () => { - await page.click('.ons-js-adv-filter__trigger'); - - const pageIsHidden = await page.$eval('.ons-page', (node) => node.classList.contains('ons-u-d-no')); - const pageIsAriaHidden = await page.$eval('.ons-page', (node) => node.getAttribute('aria-hidden')); - - expect(pageIsHidden).toBe(true); - expect(pageIsAriaHidden).toBe('true'); - }); - - it('hides filter elements when the "Show (n results)" button is pressed', async () => { - await page.click('.ons-js-adv-filter__trigger'); - await page.click('.ons-js-adv-filter__show'); - - const displayStyle = await page.$eval('.ons-adv-filter__panel', (node) => getComputedStyle(node).display); - expect(displayStyle).toBe('none'); - }); - - it('shows the underlying page elements when the "Show (n results)" button is pressed', async () => { - await page.click('.ons-js-adv-filter__trigger'); - await page.click('.ons-js-adv-filter__show'); - - const pageIsHidden = await page.$eval('.ons-page', (node) => node.classList.contains('ons-u-d-no')); - const pageIsAriaHidden = await page.$eval('.ons-page', (node) => node.getAttribute('aria-hidden')); - - expect(pageIsHidden).toBe(false); - expect(pageIsAriaHidden).toBe('false'); - }); - - it('hides filter elements when the "Close" button is pressed', async () => { - await page.click('.ons-js-adv-filter__trigger'); - await page.click('.ons-js-adv-filter__close'); - - const displayStyle = await page.$eval('.ons-adv-filter__panel', (node) => getComputedStyle(node).display); - expect(displayStyle).toBe('none'); - }); + const displayStyle = await page.$eval('.ons-adv-filter__panel', (node) => getComputedStyle(node).display); + expect(displayStyle).toBe('none'); + }); - it('shows the underlying page elements when the "Close" button is pressed', async () => { - await page.click('.ons-js-adv-filter__trigger'); - await page.click('.ons-js-adv-filter__close'); + it('shows the underlying page elements when the "Close" button is pressed', async () => { + await page.click('.ons-js-adv-filter__trigger'); + await page.click('.ons-js-adv-filter__close'); - const pageIsHidden = await page.$eval('.ons-page', (node) => node.classList.contains('ons-u-d-no')); - const pageIsAriaHidden = await page.$eval('.ons-page', (node) => node.getAttribute('aria-hidden')); + const pageIsHidden = await page.$eval('.ons-page', (node) => node.classList.contains('ons-u-d-no')); + const pageIsAriaHidden = await page.$eval('.ons-page', (node) => node.getAttribute('aria-hidden')); - expect(pageIsHidden).toBe(false); - expect(pageIsAriaHidden).toBe('false'); + expect(pageIsHidden).toBe(false); + expect(pageIsAriaHidden).toBe('false'); + }); }); - }); }); async function getTextContent(page, selector) { - return await page.$$eval(selector, (nodes) => nodes.map((node) => node.textContent.trim())); + return await page.$$eval(selector, (nodes) => nodes.map((node) => node.textContent.trim())); } async function getFilterSelectionLabels(page) { - return await getTextContent(page, '.ons-js-adv-filter__selection'); + return await getTextContent(page, '.ons-js-adv-filter__selection'); } async function getHiddenDocumentTitles(page) { - return await getTextContent(page, '.ons-js-filter__item.ons-u-hidden .ons-document-list__item-title'); + return await getTextContent(page, '.ons-js-filter__item.ons-u-hidden .ons-document-list__item-title'); } async function getShownDocumentTitles(page) { - return await getTextContent(page, '.ons-js-filter__item:not(.ons-u-hidden) .ons-document-list__item-title'); + return await getTextContent(page, '.ons-js-filter__item:not(.ons-u-hidden) .ons-document-list__item-title'); } diff --git a/src/components/duration/_macro.spec.js b/src/components/duration/_macro.spec.js index d4bf88c1cb..abe339d9a2 100644 --- a/src/components/duration/_macro.spec.js +++ b/src/components/duration/_macro.spec.js @@ -6,350 +6,350 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_DURATION_INPUT_BASE = { - id: 'duration', - legendOrLabel: 'How long have you lived at this address?', - description: 'Enter “0” into the years field if you have lived at this address for less than a year', + id: 'duration', + legendOrLabel: 'How long have you lived at this address?', + description: 'Enter “0” into the years field if you have lived at this address for less than a year', }; const EXAMPLE_DURATION_INPUT_BASE_WITH_ERROR = { - id: 'duration', - legendOrLabel: 'How long have you lived at this address?', - description: 'Enter “0” into the years field if you have lived at this address for less than a year', - error: { - text: 'Enter how long you have lived at this address', - }, + id: 'duration', + legendOrLabel: 'How long have you lived at this address?', + description: 'Enter “0” into the years field if you have lived at this address for less than a year', + error: { + text: 'Enter how long you have lived at this address', + }, }; const EXAMPLE_FIELD_1 = { - field1: { - id: 'address-duration-years', - name: 'address-duration-years', - value: '0', - suffix: { - text: 'Years', - id: 'address-duration-years-suffix', + field1: { + id: 'address-duration-years', + name: 'address-duration-years', + value: '0', + suffix: { + text: 'Years', + id: 'address-duration-years-suffix', + }, }, - }, }; const EXAMPLE_FIELD_2 = { - field2: { - id: 'address-duration-months', - name: 'address-duration-months', - value: '0', - suffix: { - text: 'Months', - id: 'address-duration-months-suffix', + field2: { + id: 'address-duration-months', + name: 'address-duration-months', + value: '0', + suffix: { + text: 'Months', + id: 'address-duration-months-suffix', + }, }, - }, }; const EXAMPLE_FIELD_1_WITH_ERROR = { - field1: { - id: 'address-duration-years', - name: 'address-duration-years', - value: '0', - error: true, - suffix: { - text: 'Years', - id: 'address-duration-years-suffix', + field1: { + id: 'address-duration-years', + name: 'address-duration-years', + value: '0', + error: true, + suffix: { + text: 'Years', + id: 'address-duration-years-suffix', + }, }, - }, }; const EXAMPLE_FIELD_2_WITH_ERROR = { - field2: { - id: 'address-duration-months', - name: 'address-duration-months', - value: '0', - error: true, - suffix: { - text: 'Months', - id: 'address-duration-months-suffix', + field2: { + id: 'address-duration-months', + name: 'address-duration-months', + value: '0', + error: true, + suffix: { + text: 'Months', + id: 'address-duration-months-suffix', + }, }, - }, }; const EXAMPLE_FIELD_1_WITH_ERROR_FALSE = { - field1: { - id: 'address-duration-years', - name: 'address-duration-years', - value: '0', - error: false, - suffix: { - text: 'Years', - id: 'address-duration-years-suffix', + field1: { + id: 'address-duration-years', + name: 'address-duration-years', + value: '0', + error: false, + suffix: { + text: 'Years', + id: 'address-duration-years-suffix', + }, }, - }, }; const EXAMPLE_DURATION_SINGLE_FIELD = { - ...EXAMPLE_DURATION_INPUT_BASE, - ...EXAMPLE_FIELD_1, + ...EXAMPLE_DURATION_INPUT_BASE, + ...EXAMPLE_FIELD_1, }; const EXAMPLE_DURATION_MULTIPLE_FIELDS = { - ...EXAMPLE_DURATION_INPUT_BASE, - ...EXAMPLE_FIELD_1, - ...EXAMPLE_FIELD_2, + ...EXAMPLE_DURATION_INPUT_BASE, + ...EXAMPLE_FIELD_1, + ...EXAMPLE_FIELD_2, }; const EXAMPLE_DURATION_MULTIPLE_FIELDS_WITH_SINGLE_ERROR = { - ...EXAMPLE_DURATION_INPUT_BASE_WITH_ERROR, - ...EXAMPLE_FIELD_1, - ...EXAMPLE_FIELD_2_WITH_ERROR, + ...EXAMPLE_DURATION_INPUT_BASE_WITH_ERROR, + ...EXAMPLE_FIELD_1, + ...EXAMPLE_FIELD_2_WITH_ERROR, }; const EXAMPLE_DURATION_MULTIPLE_FIELDS_WITH_ERROR = { - ...EXAMPLE_DURATION_INPUT_BASE_WITH_ERROR, - ...EXAMPLE_FIELD_1_WITH_ERROR, - ...EXAMPLE_FIELD_2_WITH_ERROR, + ...EXAMPLE_DURATION_INPUT_BASE_WITH_ERROR, + ...EXAMPLE_FIELD_1_WITH_ERROR, + ...EXAMPLE_FIELD_2_WITH_ERROR, }; const EXAMPLE_DURATION_MULTIPLE_FIELDS_WITH_ERROR_FALSE = { - ...EXAMPLE_DURATION_INPUT_BASE, - ...EXAMPLE_FIELD_1_WITH_ERROR_FALSE, + ...EXAMPLE_DURATION_INPUT_BASE, + ...EXAMPLE_FIELD_1_WITH_ERROR_FALSE, }; describe('macro: duration', () => { - describe('mode: multiple fields', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_MULTIPLE_FIELDS)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the expected `input` output for `field1`', () => { - const faker = templateFaker(); - const inputSpy = faker.spy('input'); - - faker.renderComponent('duration', EXAMPLE_DURATION_MULTIPLE_FIELDS); - - expect(inputSpy.occurrences[0]).toEqual({ - id: 'address-duration-years', - type: 'number', - width: '2', - classes: '', - value: '0', - attributes: undefined, - classes: '', - error: '', - fieldId: '', - label: { - description: '', - text: '', - }, - name: 'address-duration-years', - suffix: { - id: 'address-duration-years-suffix', - text: 'Years', - }, - }); - - expect(inputSpy.occurrences[1]).toEqual({ - id: 'address-duration-months', - type: 'number', - width: '2', - classes: '', - value: '0', - attributes: undefined, - classes: '', - error: '', - fieldId: '', - label: { - description: '', - text: '', - }, - name: 'address-duration-months', - suffix: { - id: 'address-duration-months-suffix', - text: 'Months', - }, - }); + describe('mode: multiple fields', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_MULTIPLE_FIELDS)); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has the expected `input` output for `field1`', () => { + const faker = templateFaker(); + const inputSpy = faker.spy('input'); + + faker.renderComponent('duration', EXAMPLE_DURATION_MULTIPLE_FIELDS); + + expect(inputSpy.occurrences[0]).toEqual({ + id: 'address-duration-years', + type: 'number', + width: '2', + classes: '', + value: '0', + attributes: undefined, + classes: '', + error: '', + fieldId: '', + label: { + description: '', + text: '', + }, + name: 'address-duration-years', + suffix: { + id: 'address-duration-years-suffix', + text: 'Years', + }, + }); + + expect(inputSpy.occurrences[1]).toEqual({ + id: 'address-duration-months', + type: 'number', + width: '2', + classes: '', + value: '0', + attributes: undefined, + classes: '', + error: '', + fieldId: '', + label: { + description: '', + text: '', + }, + name: 'address-duration-months', + suffix: { + id: 'address-duration-months-suffix', + text: 'Months', + }, + }); + }); + + it('has the group class div', () => { + const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_MULTIPLE_FIELDS)); + const div = $('.ons-field:first-child').parent(); + expect($(div).hasClass('ons-field-group')).toBe(true); + }); + + it('has the correct number of inputs', () => { + const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_MULTIPLE_FIELDS)); + const $inputs = $('.ons-input'); + expect($inputs.length).toBe(2); + }); + + it('has the expected `fieldset` output', () => { + const faker = templateFaker(); + const fieldsetSpy = faker.spy('fieldset'); + + faker.renderComponent('duration', { + ...EXAMPLE_DURATION_MULTIPLE_FIELDS, + legendClasses: 'custom-legend-class', + dontWrap: true, + legendIsQuestionTitle: true, + error: false, + }); + + expect(fieldsetSpy.occurrences[0]).toEqual({ + id: 'duration', + legend: 'How long have you lived at this address?', + description: 'Enter “0” into the years field if you have lived at this address for less than a year', + legendClasses: 'custom-legend-class', + dontWrap: true, + legendIsQuestionTitle: true, + error: false, + }); + }); }); - it('has the group class div', () => { - const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_MULTIPLE_FIELDS)); - const div = $('.ons-field:first-child').parent(); - expect($(div).hasClass('ons-field-group')).toBe(true); + describe('mode: multiple fields with mutually exclusive', () => { + it('has the correct class on each input', async () => { + const $ = cheerio.load( + renderComponent('duration', { + ...EXAMPLE_DURATION_MULTIPLE_FIELDS, + mutuallyExclusive: { + legendClasses: 'custom-legend-class', + dontWrap: true, + classes: undefined, + legendIsQuestionTitle: true, + error: false, + mutuallyExclusive: { + exclusiveOptions: {}, + or: 'Or', + deselectMessage: 'Deselect message', + deselectGroupAdjective: 'Deselect group adjective', + deselectExclusiveOptionAdjective: 'Deselect checkbox adjective', + }, + }, + }), + ); + + const exclusiveClassCount = $('.ons-js-exclusive-group-item').length; + expect(exclusiveClassCount).toBe(2); + }); + + it('has the expected `mutuallyExclusive` output', () => { + const faker = templateFaker(); + const mutuallyExclusiveSpy = faker.spy('mutually-exclusive'); + + faker.renderComponent('duration', { + ...EXAMPLE_DURATION_MULTIPLE_FIELDS, + legendClasses: 'custom-legend-class', + dontWrap: true, + classes: undefined, + legendIsQuestionTitle: true, + error: false, + mutuallyExclusive: { + or: 'Or', + deselectMessage: 'Deselect message', + deselectGroupAdjective: 'Deselect group adjective', + deselectExclusiveOptionAdjective: 'Deselect checkbox adjective', + }, + }); + + expect(mutuallyExclusiveSpy.occurrences[0]).toEqual({ + id: 'duration', + legend: 'How long have you lived at this address?', + description: 'Enter “0” into the years field if you have lived at this address for less than a year', + legendClasses: 'custom-legend-class', + dontWrap: true, + classes: undefined, + legendIsQuestionTitle: true, + error: false, + or: 'Or', + deselectMessage: 'Deselect message', + deselectGroupAdjective: 'Deselect group adjective', + deselectExclusiveOptionAdjective: 'Deselect checkbox adjective', + }); + }); }); - it('has the correct number of inputs', () => { - const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_MULTIPLE_FIELDS)); - const $inputs = $('.ons-input'); - expect($inputs.length).toBe(2); - }); - - it('has the expected `fieldset` output', () => { - const faker = templateFaker(); - const fieldsetSpy = faker.spy('fieldset'); - - faker.renderComponent('duration', { - ...EXAMPLE_DURATION_MULTIPLE_FIELDS, - legendClasses: 'custom-legend-class', - dontWrap: true, - legendIsQuestionTitle: true, - error: false, - }); - - expect(fieldsetSpy.occurrences[0]).toEqual({ - id: 'duration', - legend: 'How long have you lived at this address?', - description: 'Enter “0” into the years field if you have lived at this address for less than a year', - legendClasses: 'custom-legend-class', - dontWrap: true, - legendIsQuestionTitle: true, - error: false, - }); + describe('mode: single field', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_SINGLE_FIELD)); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has the expected `input` output for the field', () => { + const faker = templateFaker(); + const inputSpy = faker.spy('input'); + + faker.renderComponent('duration', EXAMPLE_DURATION_SINGLE_FIELD); + + expect(inputSpy.occurrences[0]).toEqual({ + id: 'address-duration-years', + type: 'number', + width: '2', + classes: '', + value: '0', + attributes: undefined, + error: undefined, + fieldId: 'duration', + label: { + text: 'How long have you lived at this address?', + description: 'Enter “0” into the years field if you have lived at this address for less than a year', + }, + name: 'address-duration-years', + suffix: { + id: 'address-duration-years-suffix', + text: 'Years', + title: undefined, + }, + }); + }); + + it('has the correct number of inputs', () => { + const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_SINGLE_FIELD)); + const $inputs = $('.ons-input'); + expect($inputs.length).toBe(1); + }); + + it('has the expected `error` output', () => { + const faker = templateFaker(); + const errorSpy = faker.spy('error'); + + faker.renderComponent('duration', { + ...EXAMPLE_DURATION_SINGLE_FIELD, + error: { text: 'Put something else' }, + }); + + expect(errorSpy.occurrences[0]).toEqual({ + text: 'Put something else', + }); + }); }); - }); - - describe('mode: multiple fields with mutually exclusive', () => { - it('has the correct class on each input', async () => { - const $ = cheerio.load( - renderComponent('duration', { - ...EXAMPLE_DURATION_MULTIPLE_FIELDS, - mutuallyExclusive: { - legendClasses: 'custom-legend-class', - dontWrap: true, - classes: undefined, - legendIsQuestionTitle: true, - error: false, - mutuallyExclusive: { - exclusiveOptions: {}, - or: 'Or', - deselectMessage: 'Deselect message', - deselectGroupAdjective: 'Deselect group adjective', - deselectExclusiveOptionAdjective: 'Deselect checkbox adjective', - }, - }, - }), - ); - - const exclusiveClassCount = $('.ons-js-exclusive-group-item').length; - expect(exclusiveClassCount).toBe(2); - }); - - it('has the expected `mutuallyExclusive` output', () => { - const faker = templateFaker(); - const mutuallyExclusiveSpy = faker.spy('mutually-exclusive'); - faker.renderComponent('duration', { - ...EXAMPLE_DURATION_MULTIPLE_FIELDS, - legendClasses: 'custom-legend-class', - dontWrap: true, - classes: undefined, - legendIsQuestionTitle: true, - error: false, - mutuallyExclusive: { - or: 'Or', - deselectMessage: 'Deselect message', - deselectGroupAdjective: 'Deselect group adjective', - deselectExclusiveOptionAdjective: 'Deselect checkbox adjective', - }, - }); - - expect(mutuallyExclusiveSpy.occurrences[0]).toEqual({ - id: 'duration', - legend: 'How long have you lived at this address?', - description: 'Enter “0” into the years field if you have lived at this address for less than a year', - legendClasses: 'custom-legend-class', - dontWrap: true, - classes: undefined, - legendIsQuestionTitle: true, - error: false, - or: 'Or', - deselectMessage: 'Deselect message', - deselectGroupAdjective: 'Deselect group adjective', - deselectExclusiveOptionAdjective: 'Deselect checkbox adjective', - }); - }); - }); + describe('mode: multiple fields with errors', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_MULTIPLE_FIELDS_WITH_SINGLE_ERROR)); - describe('mode: single field', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_SINGLE_FIELD)); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + it('has the provided error class on one input', async () => { + const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_MULTIPLE_FIELDS_WITH_SINGLE_ERROR)); + const $errorContent = $('.ons-input--error'); - it('has the expected `input` output for the field', () => { - const faker = templateFaker(); - const inputSpy = faker.spy('input'); + expect($errorContent.length).toBe(1); + }); - faker.renderComponent('duration', EXAMPLE_DURATION_SINGLE_FIELD); + it('has the provided error class on multiple inputs', async () => { + const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_MULTIPLE_FIELDS_WITH_ERROR)); + const $errorContent = $('.ons-input--error'); - expect(inputSpy.occurrences[0]).toEqual({ - id: 'address-duration-years', - type: 'number', - width: '2', - classes: '', - value: '0', - attributes: undefined, - error: undefined, - fieldId: 'duration', - label: { - text: 'How long have you lived at this address?', - description: 'Enter “0” into the years field if you have lived at this address for less than a year', - }, - name: 'address-duration-years', - suffix: { - id: 'address-duration-years-suffix', - text: 'Years', - title: undefined, - }, - }); - }); - - it('has the correct number of inputs', () => { - const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_SINGLE_FIELD)); - const $inputs = $('.ons-input'); - expect($inputs.length).toBe(1); - }); - - it('has the expected `error` output', () => { - const faker = templateFaker(); - const errorSpy = faker.spy('error'); - - faker.renderComponent('duration', { - ...EXAMPLE_DURATION_SINGLE_FIELD, - error: { text: 'Put something else' }, - }); - - expect(errorSpy.occurrences[0]).toEqual({ - text: 'Put something else', - }); - }); - }); - - describe('mode: multiple fields with errors', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_MULTIPLE_FIELDS_WITH_SINGLE_ERROR)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the provided error class on one input', async () => { - const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_MULTIPLE_FIELDS_WITH_SINGLE_ERROR)); - const $errorContent = $('.ons-input--error'); - - expect($errorContent.length).toBe(1); - }); - - it('has the provided error class on multiple inputs', async () => { - const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_MULTIPLE_FIELDS_WITH_ERROR)); - const $errorContent = $('.ons-input--error'); - - expect($errorContent.length).toBe(2); - }); + expect($errorContent.length).toBe(2); + }); - it('does not provide error class when error parameter set to false', async () => { - const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_MULTIPLE_FIELDS_WITH_ERROR_FALSE)); - const $errorContent = $('.ons-input--error'); + it('does not provide error class when error parameter set to false', async () => { + const $ = cheerio.load(renderComponent('duration', EXAMPLE_DURATION_MULTIPLE_FIELDS_WITH_ERROR_FALSE)); + const $errorContent = $('.ons-input--error'); - expect($errorContent.length).toBe(0); + expect($errorContent.length).toBe(0); + }); }); - }); }); diff --git a/src/components/error/_macro.spec.js b/src/components/error/_macro.spec.js index 35ea6f1a92..ecdcaecf31 100644 --- a/src/components/error/_macro.spec.js +++ b/src/components/error/_macro.spec.js @@ -8,86 +8,86 @@ import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const FAKE_NESTED_CONTENT = 'Nested content...'; describe('macro: error', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load( - renderComponent( - 'error', - { - text: 'Example error text.', - id: 'example-error', - }, - FAKE_NESTED_CONTENT, - ), - ); + it('passes jest-axe checks', async () => { + const $ = cheerio.load( + renderComponent( + 'error', + { + text: 'Example error text.', + id: 'example-error', + }, + FAKE_NESTED_CONTENT, + ), + ); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - it('has the provided `text` text', () => { - const $ = cheerio.load( - renderComponent( - 'error', - { - text: 'Example error text.', - id: 'example-error', - }, - FAKE_NESTED_CONTENT, - ), - ); + it('has the provided `text` text', () => { + const $ = cheerio.load( + renderComponent( + 'error', + { + text: 'Example error text.', + id: 'example-error', + }, + FAKE_NESTED_CONTENT, + ), + ); - expect($('.ons-panel__error').text().trim()).toBe('Example error text.'); - }); + expect($('.ons-panel__error').text().trim()).toBe('Example error text.'); + }); - it('applies the provided `attributes` to the error content paragraph', () => { - const $ = cheerio.load( - renderComponent( - 'error', - { - text: 'Example error text.', - id: 'example-error', - attributes: { - 'data-test-a': 'foo', - 'data-test-b': 'bar', - }, - }, - FAKE_NESTED_CONTENT, - ), - ); + it('applies the provided `attributes` to the error content paragraph', () => { + const $ = cheerio.load( + renderComponent( + 'error', + { + text: 'Example error text.', + id: 'example-error', + attributes: { + 'data-test-a': 'foo', + 'data-test-b': 'bar', + }, + }, + FAKE_NESTED_CONTENT, + ), + ); - expect($('.ons-panel__error').attr('data-test-a')).toBe('foo'); - expect($('.ons-panel__error').attr('data-test-b')).toBe('bar'); - }); + expect($('.ons-panel__error').attr('data-test-a')).toBe('foo'); + expect($('.ons-panel__error').attr('data-test-b')).toBe('bar'); + }); - it('renders output using a `panel` component', () => { - const faker = templateFaker(); - const panelSpy = faker.spy('panel'); + it('renders output using a `panel` component', () => { + const faker = templateFaker(); + const panelSpy = faker.spy('panel'); - faker.renderComponent( - 'error', - { - text: 'Example error text.', - id: 'example-error', - }, - FAKE_NESTED_CONTENT, - ); + faker.renderComponent( + 'error', + { + text: 'Example error text.', + id: 'example-error', + }, + FAKE_NESTED_CONTENT, + ); - expect(panelSpy.occurrences[0].variant).toBe('error'); - expect(panelSpy.occurrences[0].id).toBe('example-error'); - }); + expect(panelSpy.occurrences[0].variant).toBe('error'); + expect(panelSpy.occurrences[0].id).toBe('example-error'); + }); - it('renders output using a `panel` component with expected nested content', () => { - const $ = cheerio.load( - renderComponent( - 'error', - { - text: 'Example error text.', - id: 'example-error', - }, - FAKE_NESTED_CONTENT, - ), - ); + it('renders output using a `panel` component with expected nested content', () => { + const $ = cheerio.load( + renderComponent( + 'error', + { + text: 'Example error text.', + id: 'example-error', + }, + FAKE_NESTED_CONTENT, + ), + ); - expect($('.ons-panel .test--nested').text()).toBe('Nested content...'); - }); + expect($('.ons-panel .test--nested').text()).toBe('Nested content...'); + }); }); diff --git a/src/components/external-link/_external-link.scss b/src/components/external-link/_external-link.scss index 74689a52e4..cc73aa8602 100644 --- a/src/components/external-link/_external-link.scss +++ b/src/components/external-link/_external-link.scss @@ -1,24 +1,24 @@ .ons-external-link { - &__icon { - visibility: hidden; - white-space: nowrap; - } - .ons-icon { - fill: var(--ons-color-grey-100); - padding-bottom: 0.1rem; - visibility: visible; - } - &:focus { - .ons-icon { - fill: var(--ons-color-black); + &__icon { + visibility: hidden; + white-space: nowrap; } - } - &:hover { .ons-icon { - fill: var(--ons-color-text-link-hover); - .ons-footer & { - fill: var(--ons-color-black); - } + fill: var(--ons-color-grey-100); + padding-bottom: 0.1rem; + visibility: visible; + } + &:focus { + .ons-icon { + fill: var(--ons-color-black); + } + } + &:hover { + .ons-icon { + fill: var(--ons-color-text-link-hover); + .ons-footer & { + fill: var(--ons-color-black); + } + } } - } } diff --git a/src/components/external-link/_macro.spec.js b/src/components/external-link/_macro.spec.js index 232495678c..2e75b33d15 100644 --- a/src/components/external-link/_macro.spec.js +++ b/src/components/external-link/_macro.spec.js @@ -6,77 +6,77 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_EXTERNAL_LINK = { - url: 'http://example.com', - linkText: 'Example link', + url: 'http://example.com', + linkText: 'Example link', }; describe('macro: external-link', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('external-link', EXAMPLE_EXTERNAL_LINK)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has additionally provided style classes', () => { - const $ = cheerio.load( - renderComponent('external-link', { - ...EXAMPLE_EXTERNAL_LINK, - classes: 'extra-class another-extra-class', - }), - ); - - expect($('.ons-external-link').hasClass('extra-class')).toBe(true); - expect($('.ons-external-link').hasClass('another-extra-class')).toBe(true); - }); - - it('has the expected hyperlink URL', async () => { - const $ = cheerio.load(renderComponent('external-link', EXAMPLE_EXTERNAL_LINK)); - - const $hyperlink = $('a.ons-external-link'); - expect($hyperlink.attr('href')).toBe('http://example.com'); - }); - - it('opens in a new window when `newWindow` is `true`', () => { - const $ = cheerio.load(renderComponent('external-link', EXAMPLE_EXTERNAL_LINK)); - - expect($('a').attr('target')).toBe('_blank'); - expect($('a').attr('rel')).toBe('noopener'); - }); - - it('has the expected link text', async () => { - const $ = cheerio.load(renderComponent('external-link', EXAMPLE_EXTERNAL_LINK)); - - const $hyperlink = $('a.ons-external-link .ons-external-link__text'); - expect($hyperlink.text().trim()).toBe('Example link'); - }); - - it('has a default new window description', async () => { - const $ = cheerio.load(renderComponent('external-link', EXAMPLE_EXTERNAL_LINK)); - - expect($('.ons-external-link__new-window-description').text()).toBe('(opens in a new tab)'); - }); - - it('has a custom new window description when `newWindowDescription` is provided', () => { - const $ = cheerio.load( - renderComponent('external-link', { - ...EXAMPLE_EXTERNAL_LINK, - newWindowDescription: 'custom opens in a new tab text', - }), - ); - - expect($('.ons-external-link__new-window-description').text()).toBe('(custom opens in a new tab text)'); - }); - - it('has an "external-link" icon', async () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); - - faker.renderComponent('external-link', { - ...EXAMPLE_EXTERNAL_LINK, - newWindowDescription: 'custom opens in a new tab text', + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('external-link', EXAMPLE_EXTERNAL_LINK)); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has additionally provided style classes', () => { + const $ = cheerio.load( + renderComponent('external-link', { + ...EXAMPLE_EXTERNAL_LINK, + classes: 'extra-class another-extra-class', + }), + ); + + expect($('.ons-external-link').hasClass('extra-class')).toBe(true); + expect($('.ons-external-link').hasClass('another-extra-class')).toBe(true); + }); + + it('has the expected hyperlink URL', async () => { + const $ = cheerio.load(renderComponent('external-link', EXAMPLE_EXTERNAL_LINK)); + + const $hyperlink = $('a.ons-external-link'); + expect($hyperlink.attr('href')).toBe('http://example.com'); + }); + + it('opens in a new window when `newWindow` is `true`', () => { + const $ = cheerio.load(renderComponent('external-link', EXAMPLE_EXTERNAL_LINK)); + + expect($('a').attr('target')).toBe('_blank'); + expect($('a').attr('rel')).toBe('noopener'); + }); + + it('has the expected link text', async () => { + const $ = cheerio.load(renderComponent('external-link', EXAMPLE_EXTERNAL_LINK)); + + const $hyperlink = $('a.ons-external-link .ons-external-link__text'); + expect($hyperlink.text().trim()).toBe('Example link'); + }); + + it('has a default new window description', async () => { + const $ = cheerio.load(renderComponent('external-link', EXAMPLE_EXTERNAL_LINK)); + + expect($('.ons-external-link__new-window-description').text()).toBe('(opens in a new tab)'); }); - expect(iconsSpy.occurrences[0].iconType).toBe('external-link'); - }); + it('has a custom new window description when `newWindowDescription` is provided', () => { + const $ = cheerio.load( + renderComponent('external-link', { + ...EXAMPLE_EXTERNAL_LINK, + newWindowDescription: 'custom opens in a new tab text', + }), + ); + + expect($('.ons-external-link__new-window-description').text()).toBe('(custom opens in a new tab text)'); + }); + + it('has an "external-link" icon', async () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); + + faker.renderComponent('external-link', { + ...EXAMPLE_EXTERNAL_LINK, + newWindowDescription: 'custom opens in a new tab text', + }); + + expect(iconsSpy.occurrences[0].iconType).toBe('external-link'); + }); }); diff --git a/src/components/feedback/_feedback.scss b/src/components/feedback/_feedback.scss index 113f998c53..629219094b 100644 --- a/src/components/feedback/_feedback.scss +++ b/src/components/feedback/_feedback.scss @@ -1,33 +1,33 @@ .ons-feedback { - border: 3px solid var(--ons-color-branded); - margin-bottom: 2rem; - padding: 1rem; - position: relative; - &__link { - font-weight: $font-weight-bold; - } - &::before { - border-bottom: 15px solid transparent; - border-left: 15px solid var(--ons-color-branded); - border-right: 15px solid transparent; - border-top: 15px solid var(--ons-color-branded); - bottom: -30px; - content: ''; - height: 0; - left: 17px; - position: absolute; - width: 0; - } - &::after { - border-bottom: 12px solid transparent; - border-left: 12px solid #fff; - border-right: 12px solid transparent; - border-top: 12px solid #fff; - bottom: -23px; - content: ''; - height: 0; - left: 20px; - position: absolute; - width: 0; - } + border: 3px solid var(--ons-color-branded); + margin-bottom: 2rem; + padding: 1rem; + position: relative; + &__link { + font-weight: $font-weight-bold; + } + &::before { + border-bottom: 15px solid transparent; + border-left: 15px solid var(--ons-color-branded); + border-right: 15px solid transparent; + border-top: 15px solid var(--ons-color-branded); + bottom: -30px; + content: ''; + height: 0; + left: 17px; + position: absolute; + width: 0; + } + &::after { + border-bottom: 12px solid transparent; + border-left: 12px solid #fff; + border-right: 12px solid transparent; + border-top: 12px solid #fff; + bottom: -23px; + content: ''; + height: 0; + left: 20px; + position: absolute; + width: 0; + } } diff --git a/src/components/feedback/_macro.spec.js b/src/components/feedback/_macro.spec.js index a39f604c86..e2c7345ada 100644 --- a/src/components/feedback/_macro.spec.js +++ b/src/components/feedback/_macro.spec.js @@ -6,101 +6,101 @@ import axe from '../../tests/helpers/axe'; import { renderComponent } from '../../tests/helpers/rendering'; const EXAMPLE_FEEDBACK_MINIMAL = { - heading: 'Feedback heading', - content: 'Feedback content...', - url: 'http://example.com', - linkText: 'Feedback link text', + heading: 'Feedback heading', + content: 'Feedback content...', + url: 'http://example.com', + linkText: 'Feedback link text', }; const EXAMPLE_FEEDBACK_FULL = { - id: 'example-feedback', - classes: 'extra-class', - heading: 'Feedback heading', - headingLevel: 5, - headingClasses: 'extra-heading-class another-extra-heading-class', - content: 'Feedback content...', - url: 'http://example.com', - linkText: 'Feedback link text', + id: 'example-feedback', + classes: 'extra-class', + heading: 'Feedback heading', + headingLevel: 5, + headingClasses: 'extra-heading-class another-extra-heading-class', + content: 'Feedback content...', + url: 'http://example.com', + linkText: 'Feedback link text', }; describe('macro: feedback', () => { - it('passes jest-axe checks with minimum parameters', async () => { - const $ = cheerio.load(renderComponent('feedback', EXAMPLE_FEEDBACK_MINIMAL)); + it('passes jest-axe checks with minimum parameters', async () => { + const $ = cheerio.load(renderComponent('feedback', EXAMPLE_FEEDBACK_MINIMAL)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - it('passes jest-axe checks with full parameters', async () => { - const $ = cheerio.load(renderComponent('feedback', EXAMPLE_FEEDBACK_FULL)); + it('passes jest-axe checks with full parameters', async () => { + const $ = cheerio.load(renderComponent('feedback', EXAMPLE_FEEDBACK_FULL)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - it('has the provided `id` attribute', () => { - const $ = cheerio.load( - renderComponent('feedback', { - id: 'example-id', - }), - ); + it('has the provided `id` attribute', () => { + const $ = cheerio.load( + renderComponent('feedback', { + id: 'example-id', + }), + ); - expect($('#example-id').length).toBe(1); - }); + expect($('#example-id').length).toBe(1); + }); - it('has additionally provided style classes', () => { - const $ = cheerio.load( - renderComponent('feedback', { - classes: 'extra-class another-extra-class', - }), - ); + it('has additionally provided style classes', () => { + const $ = cheerio.load( + renderComponent('feedback', { + classes: 'extra-class another-extra-class', + }), + ); - expect($('.ons-feedback').hasClass('extra-class')).toBe(true); - expect($('.ons-feedback').hasClass('another-extra-class')).toBe(true); - }); + expect($('.ons-feedback').hasClass('extra-class')).toBe(true); + expect($('.ons-feedback').hasClass('another-extra-class')).toBe(true); + }); - it.each([ - [1, 'h1'], - [4, 'h4'], - ])('has the correct element type for the provided `headingLevel` (%i -> %s)', (headingLevel, expectedTitleTag) => { - const $ = cheerio.load( - renderComponent('feedback', { - ...EXAMPLE_FEEDBACK_MINIMAL, - headingLevel, - }), - ); + it.each([ + [1, 'h1'], + [4, 'h4'], + ])('has the correct element type for the provided `headingLevel` (%i -> %s)', (headingLevel, expectedTitleTag) => { + const $ = cheerio.load( + renderComponent('feedback', { + ...EXAMPLE_FEEDBACK_MINIMAL, + headingLevel, + }), + ); - expect($(`${expectedTitleTag}.ons-feedback__heading`).text().trim()).toBe('Feedback heading'); - }); + expect($(`${expectedTitleTag}.ons-feedback__heading`).text().trim()).toBe('Feedback heading'); + }); - it('has a default `headingLevel` of 2', () => { - const $ = cheerio.load(renderComponent('feedback', EXAMPLE_FEEDBACK_MINIMAL)); + it('has a default `headingLevel` of 2', () => { + const $ = cheerio.load(renderComponent('feedback', EXAMPLE_FEEDBACK_MINIMAL)); - expect($(`h2.ons-feedback__heading`).text().trim()).toBe('Feedback heading'); - }); + expect($(`h2.ons-feedback__heading`).text().trim()).toBe('Feedback heading'); + }); - it('has additional heading style classes', () => { - const $ = cheerio.load(renderComponent('feedback', EXAMPLE_FEEDBACK_FULL)); + it('has additional heading style classes', () => { + const $ = cheerio.load(renderComponent('feedback', EXAMPLE_FEEDBACK_FULL)); - expect($('.ons-feedback__heading').hasClass('extra-heading-class')).toBe(true); - expect($('.ons-feedback__heading').hasClass('another-extra-heading-class')).toBe(true); - }); + expect($('.ons-feedback__heading').hasClass('extra-heading-class')).toBe(true); + expect($('.ons-feedback__heading').hasClass('another-extra-heading-class')).toBe(true); + }); - it('has a paragraph with the provided `content`', () => { - const $ = cheerio.load(renderComponent('feedback', EXAMPLE_FEEDBACK_MINIMAL)); + it('has a paragraph with the provided `content`', () => { + const $ = cheerio.load(renderComponent('feedback', EXAMPLE_FEEDBACK_MINIMAL)); - expect($('p').text().trim()).toBe('Feedback content...'); - }); + expect($('p').text().trim()).toBe('Feedback content...'); + }); - it('has a link with the provided `url`', () => { - const $ = cheerio.load(renderComponent('feedback', EXAMPLE_FEEDBACK_MINIMAL)); + it('has a link with the provided `url`', () => { + const $ = cheerio.load(renderComponent('feedback', EXAMPLE_FEEDBACK_MINIMAL)); - expect($('.ons-feedback__link').attr('href')).toBe('http://example.com'); - }); + expect($('.ons-feedback__link').attr('href')).toBe('http://example.com'); + }); - it('has a link with the provided `linkText`', () => { - const $ = cheerio.load(renderComponent('feedback', EXAMPLE_FEEDBACK_MINIMAL)); + it('has a link with the provided `linkText`', () => { + const $ = cheerio.load(renderComponent('feedback', EXAMPLE_FEEDBACK_MINIMAL)); - expect($('.ons-feedback__link').text().trim()).toBe('Feedback link text'); - }); + expect($('.ons-feedback__link').text().trim()).toBe('Feedback link text'); + }); }); diff --git a/src/components/field/_field-group.scss b/src/components/field/_field-group.scss index c4a2f7338e..8362a23954 100644 --- a/src/components/field/_field-group.scss +++ b/src/components/field/_field-group.scss @@ -1,13 +1,13 @@ .ons-field-group { - font-size: 0; - display: flex; - flex-wrap: wrap; - gap: 1rem; + font-size: 0; + display: flex; + flex-wrap: wrap; + gap: 1rem; - .ons-field { - display: inline-block; - font-size: 1rem; - margin-top: 0; - vertical-align: top; - } + .ons-field { + display: inline-block; + font-size: 1rem; + margin-top: 0; + vertical-align: top; + } } diff --git a/src/components/field/_field.scss b/src/components/field/_field.scss index c36faad2a7..0f3e29be4e 100644 --- a/src/components/field/_field.scss +++ b/src/components/field/_field.scss @@ -1,26 +1,26 @@ .ons-field { - position: relative; + position: relative; - &__other { - background-color: var(--ons-color-grey-5); - clear: both; - display: none; - padding: 0 0.5rem 0.5rem; + &__other { + background-color: var(--ons-color-grey-5); + clear: both; + display: none; + padding: 0 0.5rem 0.5rem; - input:checked ~ & { - display: block; + input:checked ~ & { + display: block; + } } - } - &--inline { - align-items: center; - display: flex; + &--inline { + align-items: center; + display: flex; - .ons-label { - margin-bottom: 0; - margin-right: 0.5rem; + .ons-label { + margin-bottom: 0; + margin-right: 0.5rem; + } } - } } @include input-width('ons-field__item--w-{x}'); diff --git a/src/components/field/_macro.spec.js b/src/components/field/_macro.spec.js index e81241a488..cebb44ef82 100644 --- a/src/components/field/_macro.spec.js +++ b/src/components/field/_macro.spec.js @@ -6,90 +6,90 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_FIELD_BASIC = { - id: 'example-field', + id: 'example-field', }; describe('macro: field', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('field', EXAMPLE_FIELD_BASIC)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the provided `id` attribute', () => { - const $ = cheerio.load(renderComponent('field', EXAMPLE_FIELD_BASIC)); - - expect($('.ons-field').attr('id')).toBe('example-field'); - }); - - it('has additionally provided style classes', () => { - const $ = cheerio.load( - renderComponent('field', { - ...EXAMPLE_FIELD_BASIC, - classes: 'extra-class another-extra-class', - }), - ); - - expect($('.ons-field').hasClass('extra-class')).toBe(true); - expect($('.ons-field').hasClass('another-extra-class')).toBe(true); - }); - - it('has correct class when `inline` is provided', () => { - const $ = cheerio.load( - renderComponent('field', { - ...EXAMPLE_FIELD_BASIC, - inline: true, - }), - ); - - expect($('.ons-field').hasClass('ons-field--inline')).toBe(true); - }); - - it('has additionally provided `attributes`', () => { - const $ = cheerio.load( - renderComponent('field', { - ...EXAMPLE_FIELD_BASIC, - attributes: { - a: 123, - b: 456, - }, - }), - ); - - expect($('.ons-field').attr('a')).toBe('123'); - expect($('.ons-field').attr('b')).toBe('456'); - }); - - it('has the correct element with `dontWrap`', () => { - const $ = cheerio.load( - renderComponent('field', { - ...EXAMPLE_FIELD_BASIC, - dontWrap: true, - }), - ); - - expect($('.ons-field').length).toBe(0); - }); - - it('calls with content', () => { - const $ = cheerio.load(renderComponent('field', EXAMPLE_FIELD_BASIC, 'Example content...')); - - const content = $('.ons-field').html().trim(); - expect(content).toEqual(expect.stringContaining('Example content...')); - }); - - it('calls the error component when `error` is provided', () => { - const faker = templateFaker(); - const errorSpy = faker.spy('error'); - - faker.renderComponent('field', { - ...EXAMPLE_FIELD_BASIC, - error: { text: 'There is an error' }, + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('field', EXAMPLE_FIELD_BASIC)); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has the provided `id` attribute', () => { + const $ = cheerio.load(renderComponent('field', EXAMPLE_FIELD_BASIC)); + + expect($('.ons-field').attr('id')).toBe('example-field'); + }); + + it('has additionally provided style classes', () => { + const $ = cheerio.load( + renderComponent('field', { + ...EXAMPLE_FIELD_BASIC, + classes: 'extra-class another-extra-class', + }), + ); + + expect($('.ons-field').hasClass('extra-class')).toBe(true); + expect($('.ons-field').hasClass('another-extra-class')).toBe(true); + }); + + it('has correct class when `inline` is provided', () => { + const $ = cheerio.load( + renderComponent('field', { + ...EXAMPLE_FIELD_BASIC, + inline: true, + }), + ); + + expect($('.ons-field').hasClass('ons-field--inline')).toBe(true); }); - expect(errorSpy.occurrences[0]).toEqual({ - text: 'There is an error', + it('has additionally provided `attributes`', () => { + const $ = cheerio.load( + renderComponent('field', { + ...EXAMPLE_FIELD_BASIC, + attributes: { + a: 123, + b: 456, + }, + }), + ); + + expect($('.ons-field').attr('a')).toBe('123'); + expect($('.ons-field').attr('b')).toBe('456'); + }); + + it('has the correct element with `dontWrap`', () => { + const $ = cheerio.load( + renderComponent('field', { + ...EXAMPLE_FIELD_BASIC, + dontWrap: true, + }), + ); + + expect($('.ons-field').length).toBe(0); + }); + + it('calls with content', () => { + const $ = cheerio.load(renderComponent('field', EXAMPLE_FIELD_BASIC, 'Example content...')); + + const content = $('.ons-field').html().trim(); + expect(content).toEqual(expect.stringContaining('Example content...')); + }); + + it('calls the error component when `error` is provided', () => { + const faker = templateFaker(); + const errorSpy = faker.spy('error'); + + faker.renderComponent('field', { + ...EXAMPLE_FIELD_BASIC, + error: { text: 'There is an error' }, + }); + + expect(errorSpy.occurrences[0]).toEqual({ + text: 'There is an error', + }); }); - }); }); diff --git a/src/components/fieldset/_fieldset.scss b/src/components/fieldset/_fieldset.scss index eb4b5a4725..1cbfb0b918 100644 --- a/src/components/fieldset/_fieldset.scss +++ b/src/components/fieldset/_fieldset.scss @@ -1,36 +1,36 @@ .ons-fieldset { - &__legend { - font-weight: $font-weight-bold; - margin: 0; - &:not(&--with-description) { - margin-bottom: 0.55rem; - } - &-title { - display: block; - margin: 0; - padding: 0 0 1.5rem; + &__legend { + font-weight: $font-weight-bold; + margin: 0; + &:not(&--with-description) { + margin-bottom: 0.55rem; + } + &-title { + display: block; + margin: 0; + padding: 0 0 1.5rem; - strong { - @extend .ons-highlight; - } + strong { + @extend .ons-highlight; + } + } } - } - - &__description:not(&__description--title) { - @extend .ons-label__description; - margin-bottom: 0.55rem; - } + &__description:not(&__description--title) { + @extend .ons-label__description; - &__description--title { - font-weight: $font-weight-regular; - } + margin-bottom: 0.55rem; + } - > * .ons-fieldset { - .ons-fieldset { - &__legend { + &__description--title { font-weight: $font-weight-regular; - } } - } + + > * .ons-fieldset { + .ons-fieldset { + &__legend { + font-weight: $font-weight-regular; + } + } + } } diff --git a/src/components/fieldset/_macro.spec.js b/src/components/fieldset/_macro.spec.js index 4f2c30a4f4..3bcd0db883 100644 --- a/src/components/fieldset/_macro.spec.js +++ b/src/components/fieldset/_macro.spec.js @@ -6,184 +6,184 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_FIELDSET_BASIC = { - id: 'example-fieldset', - legend: 'Fieldset legend', - description: 'A fieldset description', + id: 'example-fieldset', + legend: 'Fieldset legend', + description: 'A fieldset description', }; describe('macro: fieldset', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('fieldset', EXAMPLE_FIELDSET_BASIC)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the provided `id` attribute', () => { - const $ = cheerio.load(renderComponent('fieldset', EXAMPLE_FIELDSET_BASIC)); - - expect($('.ons-fieldset').attr('id')).toBe('example-fieldset'); - }); - - it('has the `legend` text', () => { - const $ = cheerio.load(renderComponent('fieldset', EXAMPLE_FIELDSET_BASIC)); - - const title = $('.ons-fieldset__legend-title').text(); - expect(title).toBe('Fieldset legend'); - }); - - it('has the correct `aria-decribedby` value', () => { - const $ = cheerio.load(renderComponent('fieldset', EXAMPLE_FIELDSET_BASIC)); - - const ariaDescByVal = $('.ons-fieldset__legend').attr('aria-describedby'); - expect(ariaDescByVal).toBe('example-fieldset-legend-description'); - }); - - it('has the `description` text', () => { - const $ = cheerio.load(renderComponent('fieldset', EXAMPLE_FIELDSET_BASIC)); - - const title = $('.ons-fieldset__description').html().trim(); - expect(title).toBe('A fieldset description'); - }); - - it('has the correct `description` `id` when no `fieldset `id` is provided', () => { - const $ = cheerio.load(renderComponent('fieldset', { ...EXAMPLE_FIELDSET_BASIC, id: undefined })); - - const id = $('.ons-fieldset__description').attr('id'); - expect(id).toBe('legend-description'); - }); - - it('has the correct `description` `id` when `fieldset `id` is provided', () => { - const $ = cheerio.load(renderComponent('fieldset', EXAMPLE_FIELDSET_BASIC)); - - const id = $('.ons-fieldset__description').attr('id'); - expect(id).toBe('example-fieldset-legend-description'); - }); - - it('has the correct `legend` class when `description` is provided', () => { - const $ = cheerio.load(renderComponent('fieldset', EXAMPLE_FIELDSET_BASIC)); - - expect($('.ons-fieldset__legend').hasClass('ons-fieldset__legend--with-description')).toBe(true); - }); - - it('has additionally provided style classes', () => { - const $ = cheerio.load( - renderComponent('fieldset', { - ...EXAMPLE_FIELDSET_BASIC, - classes: 'extra-class another-extra-class', - }), - ); - - expect($('.ons-fieldset').hasClass('extra-class')).toBe(true); - expect($('.ons-fieldset').hasClass('another-extra-class')).toBe(true); - }); - - it('has additionally provided `legendClasses`', () => { - const $ = cheerio.load( - renderComponent('fieldset', { - ...EXAMPLE_FIELDSET_BASIC, - legendClasses: 'extra-class another-extra-class', - }), - ); - - expect($('.ons-fieldset__legend').hasClass('extra-class')).toBe(true); - expect($('.ons-fieldset__legend').hasClass('another-extra-class')).toBe(true); - }); - - it('has additionally provided `attributes`', () => { - const $ = cheerio.load( - renderComponent('fieldset', { - ...EXAMPLE_FIELDSET_BASIC, - attributes: { - a: 123, - b: 456, - }, - }), - ); - - expect($('.ons-fieldset').attr('a')).toBe('123'); - expect($('.ons-fieldset').attr('b')).toBe('456'); - }); - - it('has the correct element with `dontWrap`', () => { - const $ = cheerio.load( - renderComponent('fieldset', { - ...EXAMPLE_FIELDSET_BASIC, - dontWrap: true, - }), - ); - - expect($('.ons-input-items').length).toBe(1); - expect($('.ons-fieldset').length).toBe(0); - }); - - it('calls with content', () => { - const $ = cheerio.load(renderComponent('fieldset', EXAMPLE_FIELDSET_BASIC, 'Example content...')); - - const content = $('.ons-fieldset').html().trim(); - expect(content).toEqual(expect.stringContaining('Example content...')); - }); - - it('calls the error component when `error` is provided', () => { - const faker = templateFaker(); - const errorSpy = faker.spy('error'); - - faker.renderComponent('fieldset', { - ...EXAMPLE_FIELDSET_BASIC, - error: { text: 'There is an error' }, + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('fieldset', EXAMPLE_FIELDSET_BASIC)); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); }); - expect(errorSpy.occurrences[0]).toEqual({ - text: 'There is an error', + it('has the provided `id` attribute', () => { + const $ = cheerio.load(renderComponent('fieldset', EXAMPLE_FIELDSET_BASIC)); + + expect($('.ons-fieldset').attr('id')).toBe('example-fieldset'); }); - }); - describe('with `legendIsQuestionTitle`', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('fieldset', { ...EXAMPLE_FIELDSET_BASIC, legendIsQuestionTitle: true })); + it('has the `legend` text', () => { + const $ = cheerio.load(renderComponent('fieldset', EXAMPLE_FIELDSET_BASIC)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); + const title = $('.ons-fieldset__legend-title').text(); + expect(title).toBe('Fieldset legend'); }); - it('renders the legend in a H1', () => { - const $ = cheerio.load(renderComponent('fieldset', { ...EXAMPLE_FIELDSET_BASIC, legendIsQuestionTitle: true })); - const titleTag = $('.ons-fieldset__legend-title')[0].tagName; + it('has the correct `aria-decribedby` value', () => { + const $ = cheerio.load(renderComponent('fieldset', EXAMPLE_FIELDSET_BASIC)); - expect(titleTag).toBe('h1'); + const ariaDescByVal = $('.ons-fieldset__legend').attr('aria-describedby'); + expect(ariaDescByVal).toBe('example-fieldset-legend-description'); }); - it('has additionally provided `legendTitleClasses`', () => { - const $ = cheerio.load( - renderComponent('fieldset', { - ...EXAMPLE_FIELDSET_BASIC, - legendTitleClasses: 'extra-class another-extra-class', - legendIsQuestionTitle: true, - }), - ); - - expect($('.ons-fieldset__legend-title').hasClass('extra-class')).toBe(true); - expect($('.ons-fieldset__legend-title').hasClass('another-extra-class')).toBe(true); + it('has the `description` text', () => { + const $ = cheerio.load(renderComponent('fieldset', EXAMPLE_FIELDSET_BASIC)); + + const title = $('.ons-fieldset__description').html().trim(); + expect(title).toBe('A fieldset description'); }); - it('has the `legend` text', () => { - const $ = cheerio.load( - renderComponent('fieldset', { - ...EXAMPLE_FIELDSET_BASIC, - legendTitleClasses: 'extra-class another-extra-class', - legendIsQuestionTitle: true, - }), - ); - - const title = $('.ons-fieldset__legend-title').html().trim(); - expect(title).toBe('Fieldset legend'); + it('has the correct `description` `id` when no `fieldset `id` is provided', () => { + const $ = cheerio.load(renderComponent('fieldset', { ...EXAMPLE_FIELDSET_BASIC, id: undefined })); + + const id = $('.ons-fieldset__description').attr('id'); + expect(id).toBe('legend-description'); }); - it('has the correct `description` classes', () => { - const $ = cheerio.load(renderComponent('fieldset', { ...EXAMPLE_FIELDSET_BASIC, legendIsQuestionTitle: true })); + it('has the correct `description` `id` when `fieldset `id` is provided', () => { + const $ = cheerio.load(renderComponent('fieldset', EXAMPLE_FIELDSET_BASIC)); + + const id = $('.ons-fieldset__description').attr('id'); + expect(id).toBe('example-fieldset-legend-description'); + }); + + it('has the correct `legend` class when `description` is provided', () => { + const $ = cheerio.load(renderComponent('fieldset', EXAMPLE_FIELDSET_BASIC)); + + expect($('.ons-fieldset__legend').hasClass('ons-fieldset__legend--with-description')).toBe(true); + }); + + it('has additionally provided style classes', () => { + const $ = cheerio.load( + renderComponent('fieldset', { + ...EXAMPLE_FIELDSET_BASIC, + classes: 'extra-class another-extra-class', + }), + ); + + expect($('.ons-fieldset').hasClass('extra-class')).toBe(true); + expect($('.ons-fieldset').hasClass('another-extra-class')).toBe(true); + }); + + it('has additionally provided `legendClasses`', () => { + const $ = cheerio.load( + renderComponent('fieldset', { + ...EXAMPLE_FIELDSET_BASIC, + legendClasses: 'extra-class another-extra-class', + }), + ); + + expect($('.ons-fieldset__legend').hasClass('extra-class')).toBe(true); + expect($('.ons-fieldset__legend').hasClass('another-extra-class')).toBe(true); + }); + + it('has additionally provided `attributes`', () => { + const $ = cheerio.load( + renderComponent('fieldset', { + ...EXAMPLE_FIELDSET_BASIC, + attributes: { + a: 123, + b: 456, + }, + }), + ); + + expect($('.ons-fieldset').attr('a')).toBe('123'); + expect($('.ons-fieldset').attr('b')).toBe('456'); + }); + + it('has the correct element with `dontWrap`', () => { + const $ = cheerio.load( + renderComponent('fieldset', { + ...EXAMPLE_FIELDSET_BASIC, + dontWrap: true, + }), + ); + + expect($('.ons-input-items').length).toBe(1); + expect($('.ons-fieldset').length).toBe(0); + }); + + it('calls with content', () => { + const $ = cheerio.load(renderComponent('fieldset', EXAMPLE_FIELDSET_BASIC, 'Example content...')); + + const content = $('.ons-fieldset').html().trim(); + expect(content).toEqual(expect.stringContaining('Example content...')); + }); + + it('calls the error component when `error` is provided', () => { + const faker = templateFaker(); + const errorSpy = faker.spy('error'); + + faker.renderComponent('fieldset', { + ...EXAMPLE_FIELDSET_BASIC, + error: { text: 'There is an error' }, + }); + + expect(errorSpy.occurrences[0]).toEqual({ + text: 'There is an error', + }); + }); - expect($('.ons-fieldset__description').hasClass('ons-fieldset__description--title')).toBe(true); - expect($('.ons-fieldset__description').hasClass('ons-u-mb-m')).toBe(true); + describe('with `legendIsQuestionTitle`', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('fieldset', { ...EXAMPLE_FIELDSET_BASIC, legendIsQuestionTitle: true })); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('renders the legend in a H1', () => { + const $ = cheerio.load(renderComponent('fieldset', { ...EXAMPLE_FIELDSET_BASIC, legendIsQuestionTitle: true })); + const titleTag = $('.ons-fieldset__legend-title')[0].tagName; + + expect(titleTag).toBe('h1'); + }); + + it('has additionally provided `legendTitleClasses`', () => { + const $ = cheerio.load( + renderComponent('fieldset', { + ...EXAMPLE_FIELDSET_BASIC, + legendTitleClasses: 'extra-class another-extra-class', + legendIsQuestionTitle: true, + }), + ); + + expect($('.ons-fieldset__legend-title').hasClass('extra-class')).toBe(true); + expect($('.ons-fieldset__legend-title').hasClass('another-extra-class')).toBe(true); + }); + + it('has the `legend` text', () => { + const $ = cheerio.load( + renderComponent('fieldset', { + ...EXAMPLE_FIELDSET_BASIC, + legendTitleClasses: 'extra-class another-extra-class', + legendIsQuestionTitle: true, + }), + ); + + const title = $('.ons-fieldset__legend-title').html().trim(); + expect(title).toBe('Fieldset legend'); + }); + + it('has the correct `description` classes', () => { + const $ = cheerio.load(renderComponent('fieldset', { ...EXAMPLE_FIELDSET_BASIC, legendIsQuestionTitle: true })); + + expect($('.ons-fieldset__description').hasClass('ons-fieldset__description--title')).toBe(true); + expect($('.ons-fieldset__description').hasClass('ons-u-mb-m')).toBe(true); + }); }); - }); }); diff --git a/src/components/footer/_footer.scss b/src/components/footer/_footer.scss index bb9ab809fe..c293bb2d55 100644 --- a/src/components/footer/_footer.scss +++ b/src/components/footer/_footer.scss @@ -1,60 +1,60 @@ .ons-footer { - a { - color: var(--ons-color-text-banner-link); - text-decoration: underline; - - &:hover:not(:focus) { - color: var(--ons-color-text-banner-link-hover); - text-decoration: underline solid var(--ons-color-text-banner-link-hover) 2px; + a { + color: var(--ons-color-text-banner-link); + text-decoration: underline; + + &:hover:not(:focus) { + color: var(--ons-color-text-banner-link-hover); + text-decoration: underline solid var(--ons-color-text-banner-link-hover) 2px; + } } - } - &__button-container { - background-clip: border-box; - background-color: var(--ons-color-header); - padding: 1rem; - } + &__button-container { + background-clip: border-box; + background-color: var(--ons-color-header); + padding: 1rem; + } - &__warning { - background-color: var(--ons-color-banner-bg-dark); - outline: 2px solid transparent; // Add transparent outline because Windows High Contrast Mode doesn't show background - } + &__warning { + background-color: var(--ons-color-banner-bg-dark); + outline: 2px solid transparent; // Add transparent outline because Windows High Contrast Mode doesn't show background + } - &__license { - @extend .ons-u-fs-s; - } + &__license { + @extend .ons-u-fs-s; + } - &__ogl-img { - margin: 0 0.5rem 0.2rem 0; - max-width: 100%; - vertical-align: middle; - } + &__ogl-img { + margin: 0 0.5rem 0.2rem 0; + max-width: 100%; + vertical-align: middle; + } - &__poweredBy-link { - &:focus { - .ons-icon--logo { - @extend %a-focus; - } + &__poweredBy-link { + &:focus { + .ons-icon--logo { + @extend %a-focus; + } + } } - } - .ons-icon--logo, - .ons-icon--logo__group { - fill: var(--ons-color-black) !important; - } + .ons-icon--logo, + .ons-icon--logo__group { + fill: var(--ons-color-black) !important; + } - &__body { - background-color: var(--ons-color-banner-bg); - padding: 2rem 0 4rem; + &__body { + background-color: var(--ons-color-banner-bg); + padding: 2rem 0 4rem; - .ons-list__link { - margin-right: 0; + .ons-list__link { + margin-right: 0; + } } - } - &--rows { - li { - margin-bottom: 0.5rem !important; + &--rows { + li { + margin-bottom: 0.5rem !important; + } } - } } diff --git a/src/components/footer/_macro.spec.js b/src/components/footer/_macro.spec.js index 19a0d8a482..22ad442d7e 100644 --- a/src/components/footer/_macro.spec.js +++ b/src/components/footer/_macro.spec.js @@ -7,566 +7,566 @@ import { mapAll } from '../../tests/helpers/cheerio'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_OGL_LINK_PARAM = { - pre: 'All content is available under the', - link: 'Open Government Licence v3.0', - url: 'https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/', - post: ', except where otherwise stated', + pre: 'All content is available under the', + link: 'Open Government Licence v3.0', + url: 'https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/', + post: ', except where otherwise stated', }; const EXAMPLE_COLS_PARAM = [ - { - title: 'First column', - itemsList: [ - { - url: '/example-link-a', - text: 'Example Link A', - }, - ], - }, - { - title: 'Second column', - itemsList: [ - { - url: '/example-link-b', - text: 'Example Link B', - }, - ], - }, + { + title: 'First column', + itemsList: [ + { + url: '/example-link-a', + text: 'Example Link A', + }, + ], + }, + { + title: 'Second column', + itemsList: [ + { + url: '/example-link-b', + text: 'Example Link B', + }, + ], + }, ]; const EXAMPLE_ROWS_PARAM = [ - { - itemsList: [ - { - url: '/example-link-a', - text: 'Example Link A', - }, - ], - }, - { - itemsList: [ - { - url: '/example-link-b', - text: 'Example Link B', - }, - ], - }, + { + itemsList: [ + { + url: '/example-link-a', + text: 'Example Link A', + }, + ], + }, + { + itemsList: [ + { + url: '/example-link-b', + text: 'Example Link B', + }, + ], + }, ]; const EXAMPLE_LEGAL_PARAM = [ - { - itemsList: [ - { - url: '/example-legal-link-a', - text: 'Example Legal Link A', - }, - ], - }, - { - itemsList: [ - { - url: '/example-legal-link-b', - text: 'Example Legal Link B', - }, - ], - }, + { + itemsList: [ + { + url: '/example-legal-link-a', + text: 'Example Legal Link A', + }, + ], + }, + { + itemsList: [ + { + url: '/example-legal-link-b', + text: 'Example Legal Link B', + }, + ], + }, ]; describe('macro: footer', () => { - it('decorates container block with `wide` modifier when the `wide` parameter is provided', () => { - const $ = cheerio.load( - renderComponent('footer', { - wide: true, - }), - ); - - const hasClass = $('.ons-container').hasClass('ons-container--wide'); - expect(hasClass).toBe(true); - }); - - it('does not decorate container block with `wide` modifier when the `wide` parameter is not provided', () => { - const $ = cheerio.load(renderComponent('footer', {})); - - const hasClass = $('.ons-container').hasClass('ons-container--wide'); - expect(hasClass).toBe(false); - }); - - it('decorates container block with `fullWidth` modifier when the `fullWidth` parameter is provided', () => { - const $ = cheerio.load( - renderComponent('footer', { - fullWidth: true, - }), - ); - - const hasClass = $('.ons-container').hasClass('ons-container--full-width'); - expect(hasClass).toBe(true); - }); - - it('does not decorate container block with `fullWidth` modifier when the `fullWidth` parameter is not provided', () => { - const $ = cheerio.load(renderComponent('footer', {})); - - const hasClass = $('.ons-container').hasClass('ons-container--full-width'); - expect(hasClass).toBe(false); - }); - - it('has additionally provided `attributes` on the `body` element', () => { - const $ = cheerio.load( - renderComponent('footer', { - attributes: { - a: '123', - b: '456', - }, - }), - ); - - expect($('.ons-footer__body').attr('a')).toBe('123'); - expect($('.ons-footer__body').attr('b')).toBe('456'); - }); - - describe('OGL link', () => { - const params = { - OGLLink: EXAMPLE_OGL_LINK_PARAM, - }; - - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('footer', params)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + it('decorates container block with `wide` modifier when the `wide` parameter is provided', () => { + const $ = cheerio.load( + renderComponent('footer', { + wide: true, + }), + ); - it('renders OGL icon', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); - - faker.renderComponent('footer', params); - - expect(iconsSpy.occurrences).toContainEqual(expect.objectContaining({ iconType: 'ogl' })); - }); - - it('renders raw HTML when `HTML` is provided', () => { - const $ = cheerio.load( - renderComponent('footer', { - OGLLink: { - ...params.OGLLink, - HTML: 'Bold text.', - }, - }), - ); - - const licenseHtml = $('.ons-footer__license').html(); - expect(licenseHtml).toContain('Bold text.'); + const hasClass = $('.ons-container').hasClass('ons-container--wide'); + expect(hasClass).toBe(true); }); - it('renders link using the external link component', () => { - const faker = templateFaker(); - const externalLinkSpy = faker.spy('external-link'); + it('does not decorate container block with `wide` modifier when the `wide` parameter is not provided', () => { + const $ = cheerio.load(renderComponent('footer', {})); - faker.renderComponent('footer', params); - - expect(externalLinkSpy.occurrences).toContainEqual( - expect.objectContaining({ - url: 'https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/', - linkText: 'Open Government Licence v3.0', - }), - ); + const hasClass = $('.ons-container').hasClass('ons-container--wide'); + expect(hasClass).toBe(false); }); - it('renders `post` content when provided', () => { - const $ = cheerio.load(renderComponent('footer', params)); + it('decorates container block with `fullWidth` modifier when the `fullWidth` parameter is provided', () => { + const $ = cheerio.load( + renderComponent('footer', { + fullWidth: true, + }), + ); - const licenseHtml = $('.ons-footer__license').html(); - expect(licenseHtml).toContain(', except where otherwise stated'); + const hasClass = $('.ons-container').hasClass('ons-container--full-width'); + expect(hasClass).toBe(true); }); - it('renders welsh `post` content when `lang:cy` provided and `OGLLink` is "true"', () => { - const $ = cheerio.load( - renderComponent('footer', { - lang: 'cy', - OGLLink: true, - }), - ); + it('does not decorate container block with `fullWidth` modifier when the `fullWidth` parameter is not provided', () => { + const $ = cheerio.load(renderComponent('footer', {})); - const licenseHtml = $('.ons-footer__license').html(); - expect(licenseHtml).toContain(', oni bai y nodir fel arall'); + const hasClass = $('.ons-container').hasClass('ons-container--full-width'); + expect(hasClass).toBe(false); }); - }); - - describe('warning', () => { - const params = { - footerWarning: 'Footer warning text with bold content.', - }; - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('footer', params)); + it('has additionally provided `attributes` on the `body` element', () => { + const $ = cheerio.load( + renderComponent('footer', { + attributes: { + a: '123', + b: '456', + }, + }), + ); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); + expect($('.ons-footer__body').attr('a')).toBe('123'); + expect($('.ons-footer__body').attr('b')).toBe('456'); }); - it('renders warning element', () => { - const $ = cheerio.load(renderComponent('footer', params)); + describe('OGL link', () => { + const params = { + OGLLink: EXAMPLE_OGL_LINK_PARAM, + }; - const warningHtml = $('.ons-footer__warning').html(); - expect(warningHtml).toContain('Footer warning text with bold content.'); - }); + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('footer', params)); - it('is not rendered when `footerWarning` is not provided', () => { - const $ = cheerio.load(renderComponent('footer', {})); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - expect($('.ons-footer__warning').length).toBe(0); - }); + it('renders OGL icon', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); - it('renders warning text using the panel component', () => { - const faker = templateFaker(); - const panelSpy = faker.spy('panel'); + faker.renderComponent('footer', params); - faker.renderComponent('footer', params); + expect(iconsSpy.occurrences).toContainEqual(expect.objectContaining({ iconType: 'ogl' })); + }); - expect(panelSpy.occurrences).toContainEqual( - expect.objectContaining({ - variant: 'warn', - classes: 'ons-panel--warn--footer', - }), - ); - }); - }); + it('renders raw HTML when `HTML` is provided', () => { + const $ = cheerio.load( + renderComponent('footer', { + OGLLink: { + ...params.OGLLink, + HTML: 'Bold text.', + }, + }), + ); + + const licenseHtml = $('.ons-footer__license').html(); + expect(licenseHtml).toContain('Bold text.'); + }); - describe('copyright', () => { - const params = { - copyrightDeclaration: { - copyright: 'Crown copyright and database rights 2020 OS 100019153.', - text: 'Use of address data is subject to the terms and conditions.', - }, - }; + it('renders link using the external link component', () => { + const faker = templateFaker(); + const externalLinkSpy = faker.spy('external-link'); - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('footer', params)); + faker.renderComponent('footer', params); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + expect(externalLinkSpy.occurrences).toContainEqual( + expect.objectContaining({ + url: 'https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/', + linkText: 'Open Government Licence v3.0', + }), + ); + }); - it('renders copyright declaration', () => { - const $ = cheerio.load(renderComponent('footer', params)); + it('renders `post` content when provided', () => { + const $ = cheerio.load(renderComponent('footer', params)); - const text = $('.ons-footer__copyright').text(); - expect(text).toBe( - '© Crown copyright and database rights 2020 OS 100019153. Use of address data is subject to the terms and conditions.', - ); - }); + const licenseHtml = $('.ons-footer__license').html(); + expect(licenseHtml).toContain(', except where otherwise stated'); + }); - it('is not rendered when `copyrightDeclaration` is not provided', () => { - const $ = cheerio.load(renderComponent('footer', {})); + it('renders welsh `post` content when `lang:cy` provided and `OGLLink` is "true"', () => { + const $ = cheerio.load( + renderComponent('footer', { + lang: 'cy', + OGLLink: true, + }), + ); - expect($('.ons-footer__copyright').length).toBe(0); + const licenseHtml = $('.ons-footer__license').html(); + expect(licenseHtml).toContain(', oni bai y nodir fel arall'); + }); }); - }); - describe('cols', () => { - const params = { - cols: EXAMPLE_COLS_PARAM, - }; + describe('warning', () => { + const params = { + footerWarning: 'Footer warning text with bold content.', + }; - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('footer', params)); + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('footer', params)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - it('renders expected column titles', () => { - const $ = cheerio.load(renderComponent('footer', params)); + it('renders warning element', () => { + const $ = cheerio.load(renderComponent('footer', params)); - const titleHeadings = mapAll($('.ons-footer__heading'), (node) => node.text().trim()); - expect(titleHeadings).toEqual(['First column', 'Second column']); - }); + const warningHtml = $('.ons-footer__warning').html(); + expect(warningHtml).toContain('Footer warning text with bold content.'); + }); - it('renders expected lists using list component', () => { - const faker = templateFaker(); - const listsSpy = faker.spy('list'); + it('is not rendered when `footerWarning` is not provided', () => { + const $ = cheerio.load(renderComponent('footer', {})); - faker.renderComponent('footer', params); + expect($('.ons-footer__warning').length).toBe(0); + }); + + it('renders warning text using the panel component', () => { + const faker = templateFaker(); + const panelSpy = faker.spy('panel'); - const itemsList1 = listsSpy.occurrences[0].itemsList; - expect(itemsList1[0]).toHaveProperty('url', '/example-link-a'); - expect(itemsList1[0]).toHaveProperty('text', 'Example Link A'); + faker.renderComponent('footer', params); - const itemsList2 = listsSpy.occurrences[1].itemsList; - expect(itemsList2[0]).toHaveProperty('url', '/example-link-b'); - expect(itemsList2[0]).toHaveProperty('text', 'Example Link B'); + expect(panelSpy.occurrences).toContainEqual( + expect.objectContaining({ + variant: 'warn', + classes: 'ons-panel--warn--footer', + }), + ); + }); }); - }); - describe('rows', () => { - const params = { - rows: EXAMPLE_ROWS_PARAM, - }; + describe('copyright', () => { + const params = { + copyrightDeclaration: { + copyright: 'Crown copyright and database rights 2020 OS 100019153.', + text: 'Use of address data is subject to the terms and conditions.', + }, + }; - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('footer', params)); + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('footer', params)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - it('renders expected lists using list component', () => { - const faker = templateFaker(); - const listsSpy = faker.spy('list'); + it('renders copyright declaration', () => { + const $ = cheerio.load(renderComponent('footer', params)); - faker.renderComponent('footer', params); + const text = $('.ons-footer__copyright').text(); + expect(text).toBe( + '© Crown copyright and database rights 2020 OS 100019153. Use of address data is subject to the terms and conditions.', + ); + }); - const itemsList1 = listsSpy.occurrences[0].itemsList; - expect(itemsList1[0]).toHaveProperty('url', '/example-link-a'); - expect(itemsList1[0]).toHaveProperty('text', 'Example Link A'); + it('is not rendered when `copyrightDeclaration` is not provided', () => { + const $ = cheerio.load(renderComponent('footer', {})); - const itemsList2 = listsSpy.occurrences[1].itemsList; - expect(itemsList2[0]).toHaveProperty('url', '/example-link-b'); - expect(itemsList2[0]).toHaveProperty('text', 'Example Link B'); + expect($('.ons-footer__copyright').length).toBe(0); + }); }); - }); - describe('legal', () => { - const params = { - legal: EXAMPLE_LEGAL_PARAM, - }; + describe('cols', () => { + const params = { + cols: EXAMPLE_COLS_PARAM, + }; - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('footer', params)); + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('footer', params)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - it('renders expected lists using list component', () => { - const faker = templateFaker(); - const listsSpy = faker.spy('list'); + it('renders expected column titles', () => { + const $ = cheerio.load(renderComponent('footer', params)); - faker.renderComponent('footer', params); + const titleHeadings = mapAll($('.ons-footer__heading'), (node) => node.text().trim()); + expect(titleHeadings).toEqual(['First column', 'Second column']); + }); - const itemsList1 = listsSpy.occurrences[0].itemsList; - expect(itemsList1[0]).toHaveProperty('url', '/example-legal-link-a'); - expect(itemsList1[0]).toHaveProperty('text', 'Example Legal Link A'); + it('renders expected lists using list component', () => { + const faker = templateFaker(); + const listsSpy = faker.spy('list'); - const itemsList2 = listsSpy.occurrences[1].itemsList; - expect(itemsList2[0]).toHaveProperty('url', '/example-legal-link-b'); - expect(itemsList2[0]).toHaveProperty('text', 'Example Legal Link B'); - }); - }); - - describe('poweredBy logo', () => { - describe('default poweredBy logo', () => { - describe.each([ - [ - 'the `lang` parameter is not provided', - {}, - { - iconType: 'ons-logo-en', - altText: 'Office for National Statistics', - }, - ], - [ - 'the `lang` parameter is "en"', - { lang: 'en' }, - { - iconType: 'ons-logo-en', - altText: 'Office for National Statistics', - }, - ], - ])('where %s', (_, langParams, defaultIcon) => { - const params = { - ...langParams, - }; - it('renders the expected icon', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); + faker.renderComponent('footer', params); - faker.renderComponent('footer', params); + const itemsList1 = listsSpy.occurrences[0].itemsList; + expect(itemsList1[0]).toHaveProperty('url', '/example-link-a'); + expect(itemsList1[0]).toHaveProperty('text', 'Example Link A'); - expect(iconsSpy.occurrences).toContainEqual(expect.objectContaining(defaultIcon)); + const itemsList2 = listsSpy.occurrences[1].itemsList; + expect(itemsList2[0]).toHaveProperty('url', '/example-link-b'); + expect(itemsList2[0]).toHaveProperty('text', 'Example Link B'); }); - }); }); - describe('provided poweredBy logo', () => { - describe.each([ - [ - 'the `lang` parameter is "cy"', - { lang: 'cy' }, - { - iconType: 'ons-logo-cy', - altText: 'Swyddfa Ystadegau Gwladol', - }, - ], - ])('where %s', (_, langParams, defaultIcon) => { + + describe('rows', () => { const params = { - ...langParams, + rows: EXAMPLE_ROWS_PARAM, }; - it('renders the expected icon', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); - faker.renderComponent('footer', params); + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('footer', params)); - expect(iconsSpy.occurrences).toContainEqual(expect.objectContaining(defaultIcon)); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); }); - }); - }); - describe('correct link for language', () => { - it('has the Welsh lang link when the default Welsh lang ons icon is present', () => { - const $ = cheerio.load(renderComponent('footer', { lang: 'cy' })); - expect($('.ons-footer__poweredBy-link').attr('href')).toBe('https://cy.ons.gov.uk/'); - }); - it('has the English lang link when the default English lang ons icon is present', () => { - const $ = cheerio.load(renderComponent('footer', { lang: 'en' })); + it('renders expected lists using list component', () => { + const faker = templateFaker(); + const listsSpy = faker.spy('list'); + + faker.renderComponent('footer', params); + + const itemsList1 = listsSpy.occurrences[0].itemsList; + expect(itemsList1[0]).toHaveProperty('url', '/example-link-a'); + expect(itemsList1[0]).toHaveProperty('text', 'Example Link A'); - expect($('.ons-footer__poweredBy-link').attr('href')).toBe('https://www.ons.gov.uk/'); - }); + const itemsList2 = listsSpy.occurrences[1].itemsList; + expect(itemsList2[0]).toHaveProperty('url', '/example-link-b'); + expect(itemsList2[0]).toHaveProperty('text', 'Example Link B'); + }); }); - describe('provided poweredBy logo', () => { - describe.each([ - [ - 'the `poweredBy` and `OGLLink` parameters are provided', - { - poweredBy: '', - OGLLink: EXAMPLE_OGL_LINK_PARAM, - }, - ], - [ - 'the `poweredBy` and `legal` parameters are provided', - { - poweredBy: '', - legal: EXAMPLE_LEGAL_PARAM, - }, - ], - [ - 'the `poweredBy` and `crest` parameters are provided', - { - poweredBy: '', - crest: true, - }, - ], - [ - 'the `poweredBy`, `legal` and `crest` parameters are provided', - { - poweredBy: '', - legal: EXAMPLE_LEGAL_PARAM, - crest: true, - }, - ], - [ - 'the `poweredBy` parameter is provided but the `legal` and `crest` parameters are not provided', - { - poweredBy: '', - }, - ], - ])('where %s', (_, poweredByParams) => { + + describe('legal', () => { const params = { - ...poweredByParams, + legal: EXAMPLE_LEGAL_PARAM, }; it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('footer', params)); + const $ = cheerio.load(renderComponent('footer', params)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); }); - it('renders the expected logo', () => { - const $ = cheerio.load(renderComponent('footer', params)); + it('renders expected lists using list component', () => { + const faker = templateFaker(); + const listsSpy = faker.spy('list'); + + faker.renderComponent('footer', params); - expect($('.custom-logo').length).toBe(1); + const itemsList1 = listsSpy.occurrences[0].itemsList; + expect(itemsList1[0]).toHaveProperty('url', '/example-legal-link-a'); + expect(itemsList1[0]).toHaveProperty('text', 'Example Legal Link A'); + + const itemsList2 = listsSpy.occurrences[1].itemsList; + expect(itemsList2[0]).toHaveProperty('url', '/example-legal-link-b'); + expect(itemsList2[0]).toHaveProperty('text', 'Example Legal Link B'); }); - }); - }); - }); - - describe('save and sign out button', () => { - const params = { - button: { - id: 'save-and-sign-out', - classes: 'extra-class', - text: 'Save changes and sign out', - name: 'button-name', - attributes: { a: 42 }, - url: 'https://example.com/', - }, - }; - - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('footer', params)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); }); - it('renders "Save changes and sign out" button using the button component', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); + describe('poweredBy logo', () => { + describe('default poweredBy logo', () => { + describe.each([ + [ + 'the `lang` parameter is not provided', + {}, + { + iconType: 'ons-logo-en', + altText: 'Office for National Statistics', + }, + ], + [ + 'the `lang` parameter is "en"', + { lang: 'en' }, + { + iconType: 'ons-logo-en', + altText: 'Office for National Statistics', + }, + ], + ])('where %s', (_, langParams, defaultIcon) => { + const params = { + ...langParams, + }; + it('renders the expected icon', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); + + faker.renderComponent('footer', params); + + expect(iconsSpy.occurrences).toContainEqual(expect.objectContaining(defaultIcon)); + }); + }); + }); + describe('provided poweredBy logo', () => { + describe.each([ + [ + 'the `lang` parameter is "cy"', + { lang: 'cy' }, + { + iconType: 'ons-logo-cy', + altText: 'Swyddfa Ystadegau Gwladol', + }, + ], + ])('where %s', (_, langParams, defaultIcon) => { + const params = { + ...langParams, + }; + it('renders the expected icon', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); + + faker.renderComponent('footer', params); + + expect(iconsSpy.occurrences).toContainEqual(expect.objectContaining(defaultIcon)); + }); + }); + }); + describe('correct link for language', () => { + it('has the Welsh lang link when the default Welsh lang ons icon is present', () => { + const $ = cheerio.load(renderComponent('footer', { lang: 'cy' })); - faker.renderComponent('footer', params); + expect($('.ons-footer__poweredBy-link').attr('href')).toBe('https://cy.ons.gov.uk/'); + }); + it('has the English lang link when the default English lang ons icon is present', () => { + const $ = cheerio.load(renderComponent('footer', { lang: 'en' })); - expect(buttonSpy.occurrences).toContainEqual( - expect.objectContaining({ - ...params.button, - variants: 'ghost', - }), - ); + expect($('.ons-footer__poweredBy-link').attr('href')).toBe('https://www.ons.gov.uk/'); + }); + }); + describe('provided poweredBy logo', () => { + describe.each([ + [ + 'the `poweredBy` and `OGLLink` parameters are provided', + { + poweredBy: '', + OGLLink: EXAMPLE_OGL_LINK_PARAM, + }, + ], + [ + 'the `poweredBy` and `legal` parameters are provided', + { + poweredBy: '', + legal: EXAMPLE_LEGAL_PARAM, + }, + ], + [ + 'the `poweredBy` and `crest` parameters are provided', + { + poweredBy: '', + crest: true, + }, + ], + [ + 'the `poweredBy`, `legal` and `crest` parameters are provided', + { + poweredBy: '', + legal: EXAMPLE_LEGAL_PARAM, + crest: true, + }, + ], + [ + 'the `poweredBy` parameter is provided but the `legal` and `crest` parameters are not provided', + { + poweredBy: '', + }, + ], + ])('where %s', (_, poweredByParams) => { + const params = { + ...poweredByParams, + }; + + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('footer', params)); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('renders the expected logo', () => { + const $ = cheerio.load(renderComponent('footer', params)); + + expect($('.custom-logo').length).toBe(1); + }); + }); + }); }); - }); - describe('new tab warning', () => { - const params = { - newTabWarning: 'The following links open in a new tab', - }; + describe('save and sign out button', () => { + const params = { + button: { + id: 'save-and-sign-out', + classes: 'extra-class', + text: 'Save changes and sign out', + name: 'button-name', + attributes: { a: 42 }, + url: 'https://example.com/', + }, + }; - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('footer', params)); + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('footer', params)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('renders "Save changes and sign out" button using the button component', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); - it('renders new tab warning element', () => { - const $ = cheerio.load(renderComponent('footer', params)); + faker.renderComponent('footer', params); - const warningHtml = $('.ons-footer__new-tab-warning').html(); - expect(warningHtml).toContain('The following links open in a new tab'); + expect(buttonSpy.occurrences).toContainEqual( + expect.objectContaining({ + ...params.button, + variants: 'ghost', + }), + ); + }); }); - it('is not rendered when `newTabWarning` is not provided', () => { - const $ = cheerio.load(renderComponent('footer', {})); + describe('new tab warning', () => { + const params = { + newTabWarning: 'The following links open in a new tab', + }; - expect($('.ons-footer__new-tab-warning').length).toBe(0); - }); - }); + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('footer', params)); - describe('crest', () => { - const params = { - crest: true, - legal: true, - }; + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('renders new tab warning element', () => { + const $ = cheerio.load(renderComponent('footer', params)); - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('footer', params)); + const warningHtml = $('.ons-footer__new-tab-warning').html(); + expect(warningHtml).toContain('The following links open in a new tab'); + }); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); + it('is not rendered when `newTabWarning` is not provided', () => { + const $ = cheerio.load(renderComponent('footer', {})); + + expect($('.ons-footer__new-tab-warning').length).toBe(0); + }); }); - it('renders crest icon when `crest` parameter is provided', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); + describe('crest', () => { + const params = { + crest: true, + legal: true, + }; - faker.renderComponent('footer', params); + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('footer', params)); - expect(iconsSpy.occurrences).toContainEqual(expect.objectContaining({ iconType: 'crest' })); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('renders crest icon when `crest` parameter is provided', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); + + faker.renderComponent('footer', params); - it('renders "crest" element', () => { - const $ = cheerio.load(renderComponent('footer', params)); + expect(iconsSpy.occurrences).toContainEqual(expect.objectContaining({ iconType: 'crest' })); + }); + + it('renders "crest" element', () => { + const $ = cheerio.load(renderComponent('footer', params)); - expect($('.ons-footer__crest').length).toBe(1); + expect($('.ons-footer__crest').length).toBe(1); + }); }); - }); }); diff --git a/src/components/header/_header.scss b/src/components/header/_header.scss index 7761cac86e..d3da91dd17 100644 --- a/src/components/header/_header.scss +++ b/src/components/header/_header.scss @@ -1,244 +1,244 @@ // Block .ons-header { - @include font-smoothing; - - display: block; - margin: 0; - position: relative; - - // Elements - &__top { - background: var(--ons-color-header-masthead); - width: 100%; - } - &__grid-top { - padding: 1rem 0; - row-gap: 0.5rem; - - @include mq(xs) { - padding: 0.75rem 0; - } - } - &__main { - background: var(--ons-color-header); - padding: 0.56rem 0; - - &--border { - border-bottom: 3px solid var(--ons-color-header); - border-top: 3px solid var(--ons-color-header); - padding: 0.934rem 0; - } - } - &__title { - @extend .ons-u-fs-r--b; - @extend .ons-u-fs-xl\@xxs; - - color: var(--ons-color-white); - - @include mq(m) { - margin-bottom: 1rem; - margin-top: 0.8rem; - } - - &-link { - display: block; - text-decoration: none; - &:focus { - .ons-header__title { - color: var(--ons-color-text-link-focus); - } - } - &:hover { - text-decoration: underline solid var(--ons-color-text-inverse-link) 2px; - } - } - } - &__description { - @extend .ons-u-fs-s; - @extend .ons-u-fs-r\@s; - - color: var(--ons-color-white); + @include font-smoothing; + + display: block; margin: 0; + position: relative; - @include mq(m) { - margin: 0 0 1.1rem; + // Elements + &__top { + background: var(--ons-color-header-masthead); + width: 100%; } - } - // Modifier - variants - &--description { - .ons-header { - &__title { - @extend .ons-u-fs-m; - @extend .ons-u-fs-xxl\@m; + &__grid-top { + padding: 1rem 0; + row-gap: 0.5rem; - margin-bottom: 0; - } + @include mq(xs) { + padding: 0.75rem 0; + } } - } + &__main { + background: var(--ons-color-header); + padding: 0.56rem 0; - &--internal &, - &--neutral & { - &__top { - background: var(--ons-color-header-masthead-internal); - .ons-icon--logo { - display: block; - .ons-icon--logo__group--text, - .ons-icon--logo__group--primary { - fill: var(--ons-color-white); + &--border { + border-bottom: 3px solid var(--ons-color-header); + border-top: 3px solid var(--ons-color-header); + padding: 0.934rem 0; } - } } - &__org-logo-link:focus { - background-color: transparent; - box-shadow: none; - outline: 3px solid var(--ons-color-focus); - .ons-icon--logo { - fill: var(--ons-color-text-link-focus); - } + &__title { + @extend .ons-u-fs-r--b; + @extend .ons-u-fs-xl\@xxs; + + color: var(--ons-color-white); + + @include mq(m) { + margin-bottom: 1rem; + margin-top: 0.8rem; + } + + &-link { + display: block; + text-decoration: none; + &:focus { + .ons-header__title { + color: var(--ons-color-text-link-focus); + } + } + &:hover { + text-decoration: underline solid var(--ons-color-text-inverse-link) 2px; + } + } } - &__grid-top { - color: var(--ons-color-text-inverse); - min-height: 36px; - a { - color: var(--ons-color-text-inverse); - &:hover { - text-decoration: underline solid var(--ons-color-text-inverse-link-hover) 3px; + &__description { + @extend .ons-u-fs-s; + @extend .ons-u-fs-r\@s; + + color: var(--ons-color-white); + margin: 0; + + @include mq(m) { + margin: 0 0 1.1rem; } - } } - } + // Modifier - variants + &--description { + .ons-header { + &__title { + @extend .ons-u-fs-m; + @extend .ons-u-fs-xxl\@m; - &--neutral & { - &__top { - background: var(--ons-color-header-masthead-neutral); + margin-bottom: 0; + } + } } - &__grid-top { - a { - color: var(--ons-color-service-links); - &:hover { - text-decoration: underline solid var(--ons-color-service-links) 3px; + &--internal &, + &--neutral & { + &__top { + background: var(--ons-color-header-masthead-internal); + .ons-icon--logo { + display: block; + .ons-icon--logo__group--text, + .ons-icon--logo__group--primary { + fill: var(--ons-color-white); + } + } + } + &__org-logo-link:focus { + background-color: transparent; + box-shadow: none; + outline: 3px solid var(--ons-color-focus); + .ons-icon--logo { + fill: var(--ons-color-text-link-focus); + } + } + &__grid-top { + color: var(--ons-color-text-inverse); + min-height: 36px; + a { + color: var(--ons-color-text-inverse); + &:hover { + text-decoration: underline solid var(--ons-color-text-inverse-link-hover) 3px; + } + } } - } } - &__main { - background: var(--ons-color-header-neutral); + &--neutral & { + &__top { + background: var(--ons-color-header-masthead-neutral); + } + + &__grid-top { + a { + color: var(--ons-color-service-links); + &:hover { + text-decoration: underline solid var(--ons-color-service-links) 3px; + } + } + } - &--border { - border-bottom: 3px solid var(--ons-color-header-neutral); - border-top: 3px solid var(--ons-color-header-neutral); - } + &__main { + background: var(--ons-color-header-neutral); + + &--border { + border-bottom: 3px solid var(--ons-color-header-neutral); + border-top: 3px solid var(--ons-color-header-neutral); + } + } + + &__title { + color: var(--ons-color-header-title); + &-link { + &:hover { + text-decoration: underline solid var(--ons-color-branded-text) 2px; + } + } + } } - &__title { - color: var(--ons-color-header-title); - &-link { + .ons-icon--logo { + display: block; + width: 100%; + } + + &__org-logo-link, + &__title-logo-link { + display: block; + font-size: 0; &:hover { - text-decoration: underline solid var(--ons-color-branded-text) 2px; + text-decoration: none; } - } } - } - - .ons-icon--logo { - display: block; - width: 100%; - } - &__org-logo-link, - &__title-logo-link { - display: block; - font-size: 0; - &:hover { - text-decoration: none; + &__org-logo-link { + &:focus { + .ons-icon--logo { + fill: var(--ons-color-black) !important; + .ons-icon--logo__group { + fill: var(--ons-color-black) !important; + } + } + } } - } - &__org-logo-link { - &:focus { - .ons-icon--logo { - fill: var(--ons-color-black) !important; - .ons-icon--logo__group { - fill: var(--ons-color-black) !important; + &__org-logo--large { + @include mq(xxs, 454px) { + display: none; } - } } - } - &__org-logo--large { - @include mq(xxs, 454px) { - display: none; + &__org-logo--small { + @include mq(455px) { + display: none; + } } - } - &__org-logo--small { - @include mq(455px) { - display: none; + &__org-logo--multi { + display: inline-flex; + column-gap: 1rem; + align-items: center; } - } - &__org-logo--multi { - display: inline-flex; - column-gap: 1rem; - align-items: center; - } + &-service-nav { + display: inline-block; - &-service-nav { - display: inline-block; + &--mobile { + background: var(--ons-color-branded-tint); + padding: 1rem; + width: 100%; + } - &--mobile { - background: var(--ons-color-branded-tint); - padding: 1rem; - width: 100%; - } + .ons-header--neutral & { + &--mobile { + background: var(--ons-color-grey-5); + } + } - .ons-header--neutral & { - &--mobile { - background: var(--ons-color-grey-5); - } - } + &__list { + list-style: none; + margin: 0; + padding: 0; + } + + &__item { + display: inline-block; + margin: 0 0 0 1rem; + &--mobile { + display: block; + margin: 0 0 0.5rem; + } + &:first-child { + margin-left: 0; + } + .ons-icon { + clip-path: circle(9px at center); + margin-right: 0.5rem; + } + } - &__list { - list-style: none; - margin: 0; - padding: 0; + .ons-language-links { + border-top: 1px solid var(--ons-color-branded); + margin: 1.5rem 0 0; + padding: 1rem 0 0; + + &__item { + margin: 0 0 0.5rem; + } + } + } + &__lang-adjustment { + @include mq(l) { + margin-right: 21rem; + margin-top: -5px !important; + } } - &__item { - display: inline-block; - margin: 0 0 0 1rem; - &--mobile { - display: block; - margin: 0 0 0.5rem; - } - &:first-child { - margin-left: 0; - } - .ons-icon { - clip-path: circle(9px at center); - margin-right: 0.5rem; - } - } - - .ons-language-links { - border-top: 1px solid var(--ons-color-branded); - margin: 1.5rem 0 0; - padding: 1rem 0 0; - - &__item { - margin: 0 0 0.5rem; - } - } - } - &__lang-adjustment { - @include mq(l) { - margin-right: 21rem; - margin-top: -5px !important; - } - } - - .ons-btn { - top: 0 !important; - } + .ons-btn { + top: 0 !important; + } } diff --git a/src/components/header/_macro.spec.js b/src/components/header/_macro.spec.js index 60a34d79a1..d6ab722f77 100644 --- a/src/components/header/_macro.spec.js +++ b/src/components/header/_macro.spec.js @@ -7,976 +7,980 @@ import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; import { mapAll } from '../../tests/helpers/cheerio'; const EXAMPLE_HEADER_BASIC = { - title: 'Header title', + title: 'Header title', }; const EXAMPLE_SERVICE_LINKS_CONFIG = { - id: 'service-links', - ariaLabel: 'Services menu', - classes: 'custom-class', - toggleServicesButton: { - text: 'Menu', - ariaLabel: 'Toggle services menu', - }, + id: 'service-links', + ariaLabel: 'Services menu', + classes: 'custom-class', + toggleServicesButton: { + text: 'Menu', + ariaLabel: 'Toggle services menu', + }, }; const EXAMPLE_HEADER_SERVICE_LIST_ITEMS = { - ...EXAMPLE_HEADER_BASIC, - serviceLinks: { - ...EXAMPLE_SERVICE_LINKS_CONFIG, - itemsList: [ - { - title: 'Title 1', - }, - { - title: 'Title 2', - }, - { - title: 'Title 3', - }, - ], - }, + ...EXAMPLE_HEADER_BASIC, + serviceLinks: { + ...EXAMPLE_SERVICE_LINKS_CONFIG, + itemsList: [ + { + title: 'Title 1', + }, + { + title: 'Title 2', + }, + { + title: 'Title 3', + }, + ], + }, }; const EXAMPLE_HEADER_SERVICE_LINKS_MULTIPLE = { - ...EXAMPLE_HEADER_BASIC, - serviceLinks: { - ...EXAMPLE_SERVICE_LINKS_CONFIG, - itemsList: [ - { - title: 'Title 1', - url: '#1', - }, - { - title: 'Title 2', - url: '#2', - }, - { - title: 'Title 3', - url: '#3', - }, - ], - }, + ...EXAMPLE_HEADER_BASIC, + serviceLinks: { + ...EXAMPLE_SERVICE_LINKS_CONFIG, + itemsList: [ + { + title: 'Title 1', + url: '#1', + }, + { + title: 'Title 2', + url: '#2', + }, + { + title: 'Title 3', + url: '#3', + }, + ], + }, }; const EXAMPLE_HEADER_SERVICE_LINKS_SINGLE = { - ...EXAMPLE_HEADER_BASIC, - serviceLinks: { - ...EXAMPLE_SERVICE_LINKS_CONFIG, - itemsList: [ - { - title: 'Title', - url: '#0', - }, - ], - }, + ...EXAMPLE_HEADER_BASIC, + serviceLinks: { + ...EXAMPLE_SERVICE_LINKS_CONFIG, + itemsList: [ + { + title: 'Title', + url: '#0', + }, + ], + }, }; const EXAMPLE_HEADER_LANGUAGE_CONFIG = { - language: { - languages: [ - { - url: '#0', - ISOCode: 'en', - text: 'English', - buttonAriaLabel: 'Language selector. Current language: English', - chooseLanguage: 'Choose language', - current: true, - }, - { - url: '#0', - ISOCode: 'cy', - text: 'Cymraeg', - buttonAriaLabel: 'Dewisydd iaith. Iaith gyfredol: Cymraeg', - chooseLanguage: 'Dewiswch iaith', - current: false, - }, - ], - }, + language: { + languages: [ + { + url: '#0', + ISOCode: 'en', + text: 'English', + buttonAriaLabel: 'Language selector. Current language: English', + chooseLanguage: 'Choose language', + current: true, + }, + { + url: '#0', + ISOCode: 'cy', + text: 'Cymraeg', + buttonAriaLabel: 'Dewisydd iaith. Iaith gyfredol: Cymraeg', + chooseLanguage: 'Dewiswch iaith', + current: false, + }, + ], + }, }; const EXAMPLE_HEADER_NAVIGATION_CONFIG = { - navigation: { - id: 'main-nav', - ariaLabel: 'Main menu', - currentPath: '#home', - itemsList: [ - { - title: 'Home', - url: '#home', - }, - { - title: 'Guidance', - url: '#0', - }, - ], - toggleNavigationButton: { - text: 'Menu', - ariaLabel: 'Toggle main menu', + navigation: { + id: 'main-nav', + ariaLabel: 'Main menu', + currentPath: '#home', + itemsList: [ + { + title: 'Home', + url: '#home', + }, + { + title: 'Guidance', + url: '#0', + }, + ], + toggleNavigationButton: { + text: 'Menu', + ariaLabel: 'Toggle main menu', + }, }, - }, }; const EXAMPLE_HEADER_NAVIGATION_WITH_SUBNAVIGATION_CONFIG = { - navigation: { - id: 'main-nav', - ariaLabel: 'Main menu', - currentPath: '#1', - currentPageTitle: 'Guidance', - itemsList: [ - { - title: 'Home', - url: '#0', - }, - { - title: 'Guidance', - url: '#1', - }, - ], - toggleNavigationButton: { - text: 'Menu', - ariaLabel: 'Toggle main menu', - }, - subNavigation: { - id: 'sub-nav', - overviewURL: '#overview', - overviewText: 'Overview', - ariaLabel: 'Section menu', - currentPath: '#1', - itemsList: [ - { - title: 'Sub nav item 1', - url: '#0', - classes: 'custom-class-sub-item-1', - id: 'sub-item-1', + navigation: { + id: 'main-nav', + ariaLabel: 'Main menu', + currentPath: '#1', + currentPageTitle: 'Guidance', + itemsList: [ + { + title: 'Home', + url: '#0', + }, + { + title: 'Guidance', + url: '#1', + }, + ], + toggleNavigationButton: { + text: 'Menu', + ariaLabel: 'Toggle main menu', }, - { - title: 'Sub nav item 2', - url: '#1', - classes: 'custom-class-sub-item-2', - id: 'sub-item-2', + subNavigation: { + id: 'sub-nav', + overviewURL: '#overview', + overviewText: 'Overview', + ariaLabel: 'Section menu', + currentPath: '#1', + itemsList: [ + { + title: 'Sub nav item 1', + url: '#0', + classes: 'custom-class-sub-item-1', + id: 'sub-item-1', + }, + { + title: 'Sub nav item 2', + url: '#1', + classes: 'custom-class-sub-item-2', + id: 'sub-item-2', + }, + ], }, - ], }, - }, }; const EXAMPLE_HEADER_NAVIGATION_WITH_SITESEARCHAUTOSUGGEST = { - navigation: { - id: 'main-nav', - ariaLabel: 'Main menu', - currentPath: '#home', - itemsList: [ - { - title: 'Home', - url: '#home', - }, - { - title: 'Guidance', - url: '#0', - }, - ], - toggleNavigationButton: { - text: 'Menu', - ariaLabel: 'Toggle main menu', + navigation: { + id: 'main-nav', + ariaLabel: 'Main menu', + currentPath: '#home', + itemsList: [ + { + title: 'Home', + url: '#home', + }, + { + title: 'Guidance', + url: '#0', + }, + ], + toggleNavigationButton: { + text: 'Menu', + ariaLabel: 'Toggle main menu', + }, + }, + siteSearchAutosuggest: { + label: 'label', + instructions: 'Use up and down keys to navigate.', + ariaYouHaveSelected: 'You have selected', + ariaMinChars: 'Enter 3 or more characters for suggestions.', + minChars: 3, + ariaResultsLabel: 'Country suggestions', + ariaOneResult: 'There is one suggestion available.', + ariaNResults: 'There are {n} suggestions available.', + ariaLimitedResults: 'Type more characters to improve your search', + moreResults: 'Continue entering to improve suggestions', + resultsTitle: 'Suggestions', + resultsTitleId: 'country-of-birth-suggestions', + noResults: 'No suggestions found.', + typeMore: 'Continue entering to get suggestions', + language: 'en-gb', }, - }, - siteSearchAutosuggest: { - label: 'label', - instructions: 'Use up and down keys to navigate.', - ariaYouHaveSelected: 'You have selected', - ariaMinChars: 'Enter 3 or more characters for suggestions.', - minChars: 3, - ariaResultsLabel: 'Country suggestions', - ariaOneResult: 'There is one suggestion available.', - ariaNResults: 'There are {n} suggestions available.', - ariaLimitedResults: 'Type more characters to improve your search', - moreResults: 'Continue entering to improve suggestions', - resultsTitle: 'Suggestions', - resultsTitleId: 'country-of-birth-suggestions', - noResults: 'No suggestions found.', - typeMore: 'Continue entering to get suggestions', - language: 'en-gb', - }, }; describe('macro: header', () => { - describe('mode: basic', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_BASIC)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + describe('mode: basic', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_BASIC)); - it('has provided variant style classes', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - variants: ['variant-a', 'variant-b'], - }), - ); - - expect($('.ons-header--variant-a').length).toBe(1); - expect($('.ons-header--variant-b').length).toBe(1); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - it('has additionally provided `classes`', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - classes: 'extra-class another-extra-class', - }), - ); + it('has provided variant style classes', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + variants: ['variant-a', 'variant-b'], + }), + ); - expect($('.ons-header').hasClass('extra-class')).toBe(true); - expect($('.ons-header').hasClass('another-extra-class')).toBe(true); - }); + expect($('.ons-header--variant-a').length).toBe(1); + expect($('.ons-header--variant-b').length).toBe(1); + }); - it('has the correct container if `fullWidth`', () => { - const $ = cheerio.load(renderComponent('header', { ...EXAMPLE_HEADER_BASIC, fullWidth: true })); + it('has additionally provided `classes`', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + classes: 'extra-class another-extra-class', + }), + ); - expect($('.ons-container').hasClass('ons-container--full-width')).toBe(true); - }); + expect($('.ons-header').hasClass('extra-class')).toBe(true); + expect($('.ons-header').hasClass('another-extra-class')).toBe(true); + }); - it('has the correct container if `wide`', () => { - const $ = cheerio.load(renderComponent('header', { ...EXAMPLE_HEADER_BASIC, wide: true })); + it('has the correct container if `fullWidth`', () => { + const $ = cheerio.load(renderComponent('header', { ...EXAMPLE_HEADER_BASIC, fullWidth: true })); - expect($('.ons-container').hasClass('ons-container--wide')).toBe(true); - }); - - it('has the correct masthead logo link', () => { - const $ = cheerio.load(renderComponent('header', { ...EXAMPLE_HEADER_BASIC, mastheadLogoUrl: '#0' })); + expect($('.ons-container').hasClass('ons-container--full-width')).toBe(true); + }); - expect($('.ons-header__org-logo-link').attr('href')).toBe('#0'); - }); + it('has the correct container if `wide`', () => { + const $ = cheerio.load(renderComponent('header', { ...EXAMPLE_HEADER_BASIC, wide: true })); - it('has the default masthead logo icon', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); + expect($('.ons-container').hasClass('ons-container--wide')).toBe(true); + }); - faker.renderComponent('header', EXAMPLE_HEADER_BASIC); + it('has the correct masthead logo link', () => { + const $ = cheerio.load(renderComponent('header', { ...EXAMPLE_HEADER_BASIC, mastheadLogoUrl: '#0' })); - expect(iconsSpy.occurrences[0].iconType).toBe('ons-logo-en'); - }); + expect($('.ons-header__org-logo-link').attr('href')).toBe('#0'); + }); - it('has the default masthead mobile logo icon', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); + it('has the default masthead logo icon', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); - faker.renderComponent('header', EXAMPLE_HEADER_BASIC); + faker.renderComponent('header', EXAMPLE_HEADER_BASIC); - expect(iconsSpy.occurrences[1].iconType).toBe('ons-logo-stacked-en'); - }); + expect(iconsSpy.occurrences[0].iconType).toBe('ons-logo-en'); + }); - it('has the default masthead logo icon alt text', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); + it('has the default masthead mobile logo icon', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); - faker.renderComponent('header', EXAMPLE_HEADER_BASIC); + faker.renderComponent('header', EXAMPLE_HEADER_BASIC); - expect(iconsSpy.occurrences[0].altText).toBe('Office for National Statistics homepage'); - }); + expect(iconsSpy.occurrences[1].iconType).toBe('ons-logo-stacked-en'); + }); - it('has the default masthead mobile logo icon alt text', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); + it('has the default masthead logo icon alt text', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); - faker.renderComponent('header', EXAMPLE_HEADER_BASIC); + faker.renderComponent('header', EXAMPLE_HEADER_BASIC); - expect(iconsSpy.occurrences[1].altText).toBe('Office for National Statistics logo'); - }); + expect(iconsSpy.occurrences[0].altText).toBe('Office for National Statistics homepage'); + }); - it('has the provided large masthead logo', () => { - const $ = cheerio.load( - renderComponent('header', { ...EXAMPLE_HEADER_BASIC, mastheadLogo: { large: '' } }), - ); + it('has the default masthead mobile logo icon alt text', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); - expect($('.ons-header__org-logo--large img').attr('src')).toBe('another-logo.svg'); - }); + faker.renderComponent('header', EXAMPLE_HEADER_BASIC); - it('has the provided masthead logo custom classes', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - mastheadLogo: { large: '', classes: 'custom-class another-custom-class' }, - }), - ); + expect(iconsSpy.occurrences[1].altText).toBe('Office for National Statistics logo'); + }); - expect($('.ons-header__grid-top').hasClass('custom-class')).toBe(true); - expect($('.ons-header__grid-top').hasClass('another-custom-class')).toBe(true); - }); + it('has the provided large masthead logo', () => { + const $ = cheerio.load( + renderComponent('header', { ...EXAMPLE_HEADER_BASIC, mastheadLogo: { large: '' } }), + ); - it('has the provided small masthead logo', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - mastheadLogo: { large: 'another-logo.svg', small: '' }, - }), - ); + expect($('.ons-header__org-logo--large img').attr('src')).toBe('another-logo.svg'); + }); - expect($('.ons-header__org-logo--small img').attr('src')).toBe('another-logo-small.svg'); - }); + it('has the provided masthead logo custom classes', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + mastheadLogo: { large: '', classes: 'custom-class another-custom-class' }, + }), + ); - describe('mode: with multiple logos', () => { - it('has the default ONS icon when requested', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); - - faker.renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - mastheadLogo: { - multipleLogos: { - logo1: { - logoImage: 'ONS Logo', - }, - }, - }, - }); - expect(iconsSpy.occurrences[0].iconType).toBe('ons-logo-stacked-en'); - }); - - it('has the provided link', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - mastheadLogo: { - multipleLogos: { - logo1: { - logoImage: '', - logoURL: '#0', - }, - }, - }, - }), - ); - - expect($('.ons-header__org-logo-link').attr('href')).toBe('#0'); - }); - - it('when multiple images are provided all show', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - mastheadLogo: { - multipleLogos: { - logo1: { - logoImage: '', - }, - logo2: { - logoImage: '', - }, - logo3: { - logoImage: '', - }, - }, - }, - }), - ); - - expect($('.ons-header__org-logo--multi img').attr('src')).toBe('a-logo.svg'); - expect($('.ons-header__org-logo--multi img:nth-of-type(2)').attr('src')).toBe('a-second-logo.svg'); - expect($('.ons-header__org-logo--multi img:nth-of-type(3)').attr('src')).toBe('a-third-logo.svg'); - }); - - it('renders multiple logos even when small/large parameters are used', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - mastheadLogo: { - small: '', - large: '', - multipleLogos: { - logo1: { - logoImage: '', - }, - logo2: { - logoImage: '', - }, - logo3: { - logoImage: '', - }, - }, - }, - }), - ); - expect($('.ons-header__org-logo--large').attr('src')).toBe(undefined); - expect($('.ons-header__org-logo--multi img').attr('src')).toBe('a-logo.svg'); - }); - }); + expect($('.ons-header__grid-top').hasClass('custom-class')).toBe(true); + expect($('.ons-header__grid-top').hasClass('another-custom-class')).toBe(true); + }); - it('displays the `title` text', () => { - const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_BASIC)); + it('has the provided small masthead logo', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + mastheadLogo: { large: 'another-logo.svg', small: '' }, + }), + ); - expect($('.ons-header__title').text()).toBe('Header title'); - }); + expect($('.ons-header__org-logo--small img').attr('src')).toBe('another-logo-small.svg'); + }); - it('displays the `title` using the default tag', () => { - const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_BASIC)); + describe('mode: with multiple logos', () => { + it('has the default ONS icon when requested', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); + + faker.renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + mastheadLogo: { + multipleLogos: { + logo1: { + logoImage: 'ONS Logo', + }, + }, + }, + }); + expect(iconsSpy.occurrences[0].iconType).toBe('ons-logo-stacked-en'); + }); + + it('has the provided link', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + mastheadLogo: { + multipleLogos: { + logo1: { + logoImage: '', + logoURL: '#0', + }, + }, + }, + }), + ); + + expect($('.ons-header__org-logo-link').attr('href')).toBe('#0'); + }); + + it('when multiple images are provided all show', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + mastheadLogo: { + multipleLogos: { + logo1: { + logoImage: '', + }, + logo2: { + logoImage: '', + }, + logo3: { + logoImage: '', + }, + }, + }, + }), + ); + + expect($('.ons-header__org-logo--multi img').attr('src')).toBe('a-logo.svg'); + expect($('.ons-header__org-logo--multi img:nth-of-type(2)').attr('src')).toBe('a-second-logo.svg'); + expect($('.ons-header__org-logo--multi img:nth-of-type(3)').attr('src')).toBe('a-third-logo.svg'); + }); + + it('renders multiple logos even when small/large parameters are used', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + mastheadLogo: { + small: '', + large: '', + multipleLogos: { + logo1: { + logoImage: '', + }, + logo2: { + logoImage: '', + }, + logo3: { + logoImage: '', + }, + }, + }, + }), + ); + expect($('.ons-header__org-logo--large').attr('src')).toBe(undefined); + expect($('.ons-header__org-logo--multi img').attr('src')).toBe('a-logo.svg'); + }); + }); - expect($('.ons-header__title')[0].tagName).toBe('div'); - }); + it('displays the `title` text', () => { + const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_BASIC)); - it('displays the `title` using a H1 if `titleAsH1`', () => { - const $ = cheerio.load(renderComponent('header', { ...EXAMPLE_HEADER_BASIC, titleAsH1: true })); + expect($('.ons-header__title').text()).toBe('Header title'); + }); - expect($('.ons-header__title')[0].tagName).toBe('h1'); - }); + it('displays the `title` using the default tag', () => { + const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_BASIC)); - it('has the correct `title` link', () => { - const $ = cheerio.load(renderComponent('header', { ...EXAMPLE_HEADER_BASIC, titleUrl: '#0' })); + expect($('.ons-header__title')[0].tagName).toBe('div'); + }); - expect($('.ons-header__title-link').attr('href')).toBe('#0'); - }); + it('displays the `title` using a H1 if `titleAsH1`', () => { + const $ = cheerio.load(renderComponent('header', { ...EXAMPLE_HEADER_BASIC, titleAsH1: true })); - it('has the provided large title logo', () => { - const $ = cheerio.load(renderComponent('header', { ...EXAMPLE_HEADER_BASIC, titleLogo: { large: '' } })); + expect($('.ons-header__title')[0].tagName).toBe('h1'); + }); - expect($('.ons-header__title-logo--large img').attr('src')).toBe('another-logo.svg'); - }); + it('has the correct `title` link', () => { + const $ = cheerio.load(renderComponent('header', { ...EXAMPLE_HEADER_BASIC, titleUrl: '#0' })); - it('has the provided title logo custom classes', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - titleLogo: { large: 'another-logo.svg', classes: 'custom-class another-custom-class' }, - }), - ); + expect($('.ons-header__title-link').attr('href')).toBe('#0'); + }); - expect($('.ons-header__title-logo--large').hasClass('custom-class')).toBe(true); - expect($('.ons-header__title-logo--large').hasClass('another-custom-class')).toBe(true); - }); + it('has the provided large title logo', () => { + const $ = cheerio.load( + renderComponent('header', { ...EXAMPLE_HEADER_BASIC, titleLogo: { large: '' } }), + ); - it('has the provided small title logo', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - titleLogo: { large: '', small: '' }, - }), - ); + expect($('.ons-header__title-logo--large img').attr('src')).toBe('another-logo.svg'); + }); - expect($('.ons-header__title-logo--small img').attr('src')).toBe('another-logo-small.svg'); - }); + it('has the provided title logo custom classes', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + titleLogo: { large: 'another-logo.svg', classes: 'custom-class another-custom-class' }, + }), + ); - it('displays the `description` text', () => { - const $ = cheerio.load(renderComponent('header', { ...EXAMPLE_HEADER_BASIC, description: 'Header description' })); + expect($('.ons-header__title-logo--large').hasClass('custom-class')).toBe(true); + expect($('.ons-header__title-logo--large').hasClass('another-custom-class')).toBe(true); + }); - expect($('.ons-header__description').text()).toBe('Header description'); - }); + it('has the provided small title logo', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + titleLogo: { large: '', small: '' }, + }), + ); - it('renders a button with expected parameters', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button', { suppressOutput: true }); - - faker.renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - button: { - text: 'Save and sign out', - name: 'button-name', - attributes: { - a: 'b', - }, - url: '#0', - }, - }); - - expect(buttonSpy.occurrences).toContainEqual({ - text: 'Save and sign out', - classes: 'ons-u-d-no@xxs@m', - variants: 'ghost', - name: 'button-name', - attributes: { - a: 'b', - }, - url: '#0', - iconType: 'exit', - iconPosition: 'after', - }); - }); + expect($('.ons-header__title-logo--small img').attr('src')).toBe('another-logo-small.svg'); + }); - it('has gutterless class if there is no button present', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - }), - ); + it('displays the `description` text', () => { + const $ = cheerio.load(renderComponent('header', { ...EXAMPLE_HEADER_BASIC, description: 'Header description' })); - const titleGridDiv = $('.ons-header__main .ons-container .ons-grid'); - expect($(titleGridDiv).hasClass('ons-grid--gutterless')).toBe(true); - }); + expect($('.ons-header__description').text()).toBe('Header description'); + }); - it('has does not have gutterless class if there is a button present', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - button: { - text: 'Save and sign out', - url: '#0', - iconType: 'exit', - iconPosition: 'after', - }, - }), - ); - - const titleGridDiv = $('.ons-header__main .ons-container .ons-grid'); - expect($(titleGridDiv).hasClass('ons-grid--gutterless')).toBe(false); - }); + it('renders a button with expected parameters', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button', { suppressOutput: true }); + + faker.renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + button: { + text: 'Save and sign out', + name: 'button-name', + attributes: { + a: 'b', + }, + url: '#0', + }, + }); + + expect(buttonSpy.occurrences).toContainEqual({ + text: 'Save and sign out', + classes: 'ons-u-d-no@xxs@m', + variants: 'ghost', + name: 'button-name', + attributes: { + a: 'b', + }, + url: '#0', + iconType: 'exit', + iconPosition: 'after', + }); + }); - it('renders the phase banner with expected parameters', () => { - const faker = templateFaker(); - const phaseSpy = faker.spy('phase-banner'); + it('has gutterless class if there is no button present', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + }), + ); - faker.renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - phase: { - badge: 'Example', - html: 'Example content with a link', - }, - }); - - expect(phaseSpy.occurrences).toContainEqual({ - badge: 'Example', - html: 'Example content with a link', - }); - }); + const titleGridDiv = $('.ons-header__main .ons-container .ons-grid'); + expect($(titleGridDiv).hasClass('ons-grid--gutterless')).toBe(true); + }); - it('renders the phase banner in the correct container if `wide`', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - wide: true, - phase: { - badge: 'Example', - html: 'Example content with a link', - }, - }), - ); - - const phaseContainer = $('.ons-phase-banner .ons-container'); - expect($(phaseContainer).hasClass('ons-container--wide')).toBe(true); - }); + it('has does not have gutterless class if there is a button present', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + button: { + text: 'Save and sign out', + url: '#0', + iconType: 'exit', + iconPosition: 'after', + }, + }), + ); + + const titleGridDiv = $('.ons-header__main .ons-container .ons-grid'); + expect($(titleGridDiv).hasClass('ons-grid--gutterless')).toBe(false); + }); - it('renders the phase banner in the correct container if `fullWidth`', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - fullWidth: true, - phase: { - badge: 'Example', - html: 'Example content with a link', - }, - }), - ); - - const phaseContainer = $('.ons-phase-banner .ons-container'); - expect($(phaseContainer).hasClass('ons-container--full-width')).toBe(true); - }); - }); + it('renders the phase banner with expected parameters', () => { + const faker = templateFaker(); + const phaseSpy = faker.spy('phase-banner'); - describe('mode: with service links', () => { - it('has the correct display class when multiple links are provided', () => { - const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_MULTIPLE)); + faker.renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + phase: { + badge: 'Example', + html: 'Example content with a link', + }, + }); - expect($('.ons-header__links .ons-grid__col').hasClass('ons-u-d-no@xxs@m')).toBe(true); - }); + expect(phaseSpy.occurrences).toContainEqual({ + badge: 'Example', + html: 'Example content with a link', + }); + }); - it('has the correct display class when a single link and language is provided', () => { - const $ = cheerio.load(renderComponent('header', { ...EXAMPLE_HEADER_SERVICE_LINKS_SINGLE, ...EXAMPLE_HEADER_LANGUAGE_CONFIG })); + it('renders the phase banner in the correct container if `wide`', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + wide: true, + phase: { + badge: 'Example', + html: 'Example content with a link', + }, + }), + ); + + const phaseContainer = $('.ons-phase-banner .ons-container'); + expect($(phaseContainer).hasClass('ons-container--wide')).toBe(true); + }); - expect($('.ons-header__links .ons-grid__col').hasClass('ons-u-d-no@xxs@xs')).toBe(true); + it('renders the phase banner in the correct container if `fullWidth`', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + fullWidth: true, + phase: { + badge: 'Example', + html: 'Example content with a link', + }, + }), + ); + + const phaseContainer = $('.ons-phase-banner .ons-container'); + expect($(phaseContainer).hasClass('ons-container--full-width')).toBe(true); + }); }); - it('does not have the display class when only single link is provided', () => { - const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_SINGLE)); + describe('mode: with service links', () => { + it('has the correct display class when multiple links are provided', () => { + const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_MULTIPLE)); - expect($('.ons-header__links .ons-grid__col').hasClass('ons-u-d-no@xxs@')).toBe(false); - }); + expect($('.ons-header__links .ons-grid__col').hasClass('ons-u-d-no@xxs@m')).toBe(true); + }); - it('has the provided custom class', () => { - const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_SINGLE)); + it('has the correct display class when a single link and language is provided', () => { + const $ = cheerio.load( + renderComponent('header', { ...EXAMPLE_HEADER_SERVICE_LINKS_SINGLE, ...EXAMPLE_HEADER_LANGUAGE_CONFIG }), + ); - expect($('.ons-header-service-nav--main').hasClass('custom-class')).toBe(true); - }); + expect($('.ons-header__links .ons-grid__col').hasClass('ons-u-d-no@xxs@xs')).toBe(true); + }); - it('has the provided `aria-label`', () => { - const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_SINGLE)); + it('does not have the display class when only single link is provided', () => { + const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_SINGLE)); - expect($('.ons-header-service-nav--main').attr('aria-label')).toBe('Services menu'); - }); + expect($('.ons-header__links .ons-grid__col').hasClass('ons-u-d-no@xxs@')).toBe(false); + }); - it('has the text for each list item', () => { - const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LIST_ITEMS)); + it('has the provided custom class', () => { + const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_SINGLE)); - const values = mapAll($('.ons-header-service-nav--main .ons-header-service-nav__item'), (node) => node.text().trim()); - expect(values).toEqual(['Title 1', 'Title 2', 'Title 3']); - }); + expect($('.ons-header-service-nav--main').hasClass('custom-class')).toBe(true); + }); - it('has the link text for each list item', () => { - const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_MULTIPLE)); + it('has the provided `aria-label`', () => { + const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_SINGLE)); - const values = mapAll($('.ons-header-service-nav--main .ons-header-service-nav__link'), (node) => node.text().trim()); - expect(values).toEqual(['Title 1', 'Title 2', 'Title 3']); - }); + expect($('.ons-header-service-nav--main').attr('aria-label')).toBe('Services menu'); + }); - it('has the link href for each list item', () => { - const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_MULTIPLE)); + it('has the text for each list item', () => { + const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LIST_ITEMS)); - const values = mapAll($('.ons-header-service-nav--main .ons-header-service-nav__link'), (node) => node.attr('href')); - expect(values).toEqual(['#1', '#2', '#3']); - }); + const values = mapAll($('.ons-header-service-nav--main .ons-header-service-nav__item'), (node) => node.text().trim()); + expect(values).toEqual(['Title 1', 'Title 2', 'Title 3']); + }); - it('has the provided custom class', () => { - const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_SINGLE)); + it('has the link text for each list item', () => { + const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_MULTIPLE)); - expect($('.ons-header-service-nav--main').hasClass('custom-class')).toBe(true); - }); + const values = mapAll($('.ons-header-service-nav--main .ons-header-service-nav__link'), (node) => node.text().trim()); + expect(values).toEqual(['Title 1', 'Title 2', 'Title 3']); + }); - it('has the provided `aria-label` for the list for mobile', () => { - const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_SINGLE)); + it('has the link href for each list item', () => { + const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_MULTIPLE)); - expect($('.ons-header-service-nav--mobile').attr('aria-label')).toBe('Services menu'); - }); + const values = mapAll($('.ons-header-service-nav--main .ons-header-service-nav__link'), (node) => node.attr('href')); + expect(values).toEqual(['#1', '#2', '#3']); + }); - it('has the link text for each list item for mobile', () => { - const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_MULTIPLE)); + it('has the provided custom class', () => { + const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_SINGLE)); - const values = mapAll($('.ons-header-service-nav--mobile .ons-header-service-nav__link'), (node) => node.text().trim()); - expect(values).toEqual(['Title 1', 'Title 2', 'Title 3']); - }); + expect($('.ons-header-service-nav--main').hasClass('custom-class')).toBe(true); + }); - it('has the link href for each list item for mobile', () => { - const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_MULTIPLE)); + it('has the provided `aria-label` for the list for mobile', () => { + const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_SINGLE)); - const values = mapAll($('.ons-header-service-nav--mobile .ons-header-service-nav__link'), (node) => node.attr('href')); - expect(values).toEqual(['#1', '#2', '#3']); - }); + expect($('.ons-header-service-nav--mobile').attr('aria-label')).toBe('Services menu'); + }); - it('renders a button with expected parameters', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button', { suppressOutput: true }); + it('has the link text for each list item for mobile', () => { + const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_MULTIPLE)); - faker.renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_MULTIPLE); + const values = mapAll($('.ons-header-service-nav--mobile .ons-header-service-nav__link'), (node) => node.text().trim()); + expect(values).toEqual(['Title 1', 'Title 2', 'Title 3']); + }); - expect(buttonSpy.occurrences).toContainEqual({ - text: 'Menu', - classes: 'ons-u-d-no ons-js-toggle-services', - type: 'button', - variants: ['mobile', 'text-link'], - attributes: { - 'aria-label': 'Toggle services menu', - 'aria-controls': 'service-links', - 'aria-expanded': 'false', - }, - }); - }); + it('has the link href for each list item for mobile', () => { + const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_MULTIPLE)); - it('renders a button with correct variant if `internal`', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); + const values = mapAll($('.ons-header-service-nav--mobile .ons-header-service-nav__link'), (node) => node.attr('href')); + expect(values).toEqual(['#1', '#2', '#3']); + }); - faker.renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - variants: 'internal', - serviceLinks: { - ...EXAMPLE_SERVICE_LINKS_CONFIG, - itemsList: [ - { - title: 'Title', - url: '#0', - }, - { - title: 'Title 2', - url: '#0', - }, - ], - }, - }); + it('renders a button with expected parameters', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button', { suppressOutput: true }); + + faker.renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_MULTIPLE); + + expect(buttonSpy.occurrences).toContainEqual({ + text: 'Menu', + classes: 'ons-u-d-no ons-js-toggle-services', + type: 'button', + variants: ['mobile', 'text-link'], + attributes: { + 'aria-label': 'Toggle services menu', + 'aria-controls': 'service-links', + 'aria-expanded': 'false', + }, + }); + }); - expect(buttonSpy.occurrences[0]).toHaveProperty('variants', ['mobile', 'text-link', 'text-link-inverse']); - }); + it('renders a button with correct variant if `internal`', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); + + faker.renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + variants: 'internal', + serviceLinks: { + ...EXAMPLE_SERVICE_LINKS_CONFIG, + itemsList: [ + { + title: 'Title', + url: '#0', + }, + { + title: 'Title 2', + url: '#0', + }, + ], + }, + }); - it('does not render a button for a single services link', () => { - const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_SINGLE)); + expect(buttonSpy.occurrences[0]).toHaveProperty('variants', ['mobile', 'text-link', 'text-link-inverse']); + }); - expect($('.ons-js-toggle-services').length).toBe(0); - }); + it('does not render a button for a single services link', () => { + const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_SERVICE_LINKS_SINGLE)); - it('has the correct list item icon', () => { - const faker = templateFaker(); - const iconsSpy = faker.spy('icon'); + expect($('.ons-js-toggle-services').length).toBe(0); + }); - faker.renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - serviceLinks: { - ...EXAMPLE_SERVICE_LINKS_CONFIG, - itemsList: [ - { - title: 'Title 1', - iconType: 'check', - }, - ], - }, - }); + it('has the correct list item icon', () => { + const faker = templateFaker(); + const iconsSpy = faker.spy('icon'); + + faker.renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + serviceLinks: { + ...EXAMPLE_SERVICE_LINKS_CONFIG, + itemsList: [ + { + title: 'Title 1', + iconType: 'check', + }, + ], + }, + }); - expect(iconsSpy.occurrences[2].iconType).toBe('check'); + expect(iconsSpy.occurrences[2].iconType).toBe('check'); + }); }); - }); - - describe('mode: with language selector', () => { - it('displays the language selector with expected parameters', () => { - const faker = templateFaker(); - const languageSpy = faker.spy('language-selector', { suppressOutput: true }); - faker.renderComponent('header', { ...EXAMPLE_HEADER_BASIC, ...EXAMPLE_HEADER_LANGUAGE_CONFIG }); - - expect(languageSpy.occurrences).toContainEqual({ - languages: [ - { - url: '#0', - ISOCode: 'en', - text: 'English', - buttonAriaLabel: 'Language selector. Current language: English', - chooseLanguage: 'Choose language', - current: true, - }, - { - url: '#0', - ISOCode: 'cy', - text: 'Cymraeg', - buttonAriaLabel: 'Dewisydd iaith. Iaith gyfredol: Cymraeg', - chooseLanguage: 'Dewiswch iaith', - current: false, - }, - ], - }); + describe('mode: with language selector', () => { + it('displays the language selector with expected parameters', () => { + const faker = templateFaker(); + const languageSpy = faker.spy('language-selector', { suppressOutput: true }); + + faker.renderComponent('header', { ...EXAMPLE_HEADER_BASIC, ...EXAMPLE_HEADER_LANGUAGE_CONFIG }); + + expect(languageSpy.occurrences).toContainEqual({ + languages: [ + { + url: '#0', + ISOCode: 'en', + text: 'English', + buttonAriaLabel: 'Language selector. Current language: English', + chooseLanguage: 'Choose language', + current: true, + }, + { + url: '#0', + ISOCode: 'cy', + text: 'Cymraeg', + buttonAriaLabel: 'Dewisydd iaith. Iaith gyfredol: Cymraeg', + chooseLanguage: 'Dewiswch iaith', + current: false, + }, + ], + }); + }); }); - }); - describe('mode: with navigation', () => { - it('renders the navigation with expected parameters', () => { - const faker = templateFaker(); - const navigationSpy = faker.spy('navigation', { suppressOutput: true }); - - faker.renderComponent('header', { ...EXAMPLE_HEADER_BASIC, ...EXAMPLE_HEADER_NAVIGATION_CONFIG }); - - expect(navigationSpy.occurrences[0]).toEqual({ - navigation: { - id: 'main-nav', - ariaLabel: 'Main menu', - currentPath: '#home', - itemsList: [ - { - title: 'Home', - url: '#home', - }, - { - title: 'Guidance', - url: '#0', - }, - ], - toggleNavigationButton: { - text: 'Menu', - ariaLabel: 'Toggle main menu', - }, - }, - title: 'Header title', - }); - }); + describe('mode: with navigation', () => { + it('renders the navigation with expected parameters', () => { + const faker = templateFaker(); + const navigationSpy = faker.spy('navigation', { suppressOutput: true }); + + faker.renderComponent('header', { ...EXAMPLE_HEADER_BASIC, ...EXAMPLE_HEADER_NAVIGATION_CONFIG }); + + expect(navigationSpy.occurrences[0]).toEqual({ + navigation: { + id: 'main-nav', + ariaLabel: 'Main menu', + currentPath: '#home', + itemsList: [ + { + title: 'Home', + url: '#home', + }, + { + title: 'Guidance', + url: '#0', + }, + ], + toggleNavigationButton: { + text: 'Menu', + ariaLabel: 'Toggle main menu', + }, + }, + title: 'Header title', + }); + }); - it('renders a button to toggle the menu on mobile', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); + it('renders a button to toggle the menu on mobile', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); - faker.renderComponent('header', { ...EXAMPLE_HEADER_BASIC, ...EXAMPLE_HEADER_NAVIGATION_CONFIG }); + faker.renderComponent('header', { ...EXAMPLE_HEADER_BASIC, ...EXAMPLE_HEADER_NAVIGATION_CONFIG }); - expect(buttonSpy.occurrences[0]).toEqual({ - text: 'Menu', - classes: 'ons-u-ml-xs ons-u-d-no ons-js-navigation-button ons-u-d-no@l', - variants: ['mobile', 'ghost'], - attributes: { - 'aria-label': 'Toggle main menu', - 'aria-controls': 'main-nav', - 'aria-expanded': 'false', - }, - }); - }); + expect(buttonSpy.occurrences[0]).toEqual({ + text: 'Menu', + classes: 'ons-u-ml-xs ons-u-d-no ons-js-navigation-button ons-u-d-no@l', + variants: ['mobile', 'ghost'], + attributes: { + 'aria-label': 'Toggle main menu', + 'aria-controls': 'main-nav', + 'aria-expanded': 'false', + }, + }); + }); - it('renders a button to toggle the search on mobile', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); - - faker.renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - navigation: { - id: 'main-nav', - ariaLabel: 'Main menu', - currentPath: '#home', - itemsList: [ - { - title: 'Home', - url: '#home', - }, - { - title: 'Guidance', - url: '#0', - }, - ], - toggleNavigationButton: { - text: 'Menu', - ariaLabel: 'Toggle main menu', - }, - }, - siteSearchAutosuggest: {}, - }); - - expect(buttonSpy.occurrences[0]).toEqual({ - text: 'Search', - classes: 'ons-btn--search ons-u-ml-xs ons-u-d-no ons-js-toggle-search', - variants: ['small', 'ghost'], - iconType: 'search', - iconPosition: 'only', - attributes: { - 'aria-label': 'Toggle search', - 'aria-controls': 'ons-site-search', - 'aria-expanded': 'false', - }, - }); - }); + it('renders a button to toggle the search on mobile', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); + + faker.renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + navigation: { + id: 'main-nav', + ariaLabel: 'Main menu', + currentPath: '#home', + itemsList: [ + { + title: 'Home', + url: '#home', + }, + { + title: 'Guidance', + url: '#0', + }, + ], + toggleNavigationButton: { + text: 'Menu', + ariaLabel: 'Toggle main menu', + }, + }, + siteSearchAutosuggest: {}, + }); + + expect(buttonSpy.occurrences[0]).toEqual({ + text: 'Search', + classes: 'ons-btn--search ons-u-ml-xs ons-u-d-no ons-js-toggle-search', + variants: ['small', 'ghost'], + iconType: 'search', + iconPosition: 'only', + attributes: { + 'aria-label': 'Toggle search', + 'aria-controls': 'ons-site-search', + 'aria-expanded': 'false', + }, + }); + }); - it('renders the navigation in the correct container if `wide`', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - wide: true, - ...EXAMPLE_HEADER_NAVIGATION_CONFIG, - }), - ); - - const navContainer = $('.ons-navigation-wrapper .ons-container'); - expect($(navContainer).hasClass('ons-container--wide')).toBe(true); - }); + it('renders the navigation in the correct container if `wide`', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + wide: true, + ...EXAMPLE_HEADER_NAVIGATION_CONFIG, + }), + ); + + const navContainer = $('.ons-navigation-wrapper .ons-container'); + expect($(navContainer).hasClass('ons-container--wide')).toBe(true); + }); - it('renders the navigation in the correct container if `fullWidth`', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - fullWidth: true, - ...EXAMPLE_HEADER_NAVIGATION_CONFIG, - }), - ); - - const navContainer = $('.ons-navigation-wrapper .ons-container'); - expect($(navContainer).hasClass('ons-container--full-width')).toBe(true); - }); + it('renders the navigation in the correct container if `fullWidth`', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + fullWidth: true, + ...EXAMPLE_HEADER_NAVIGATION_CONFIG, + }), + ); + + const navContainer = $('.ons-navigation-wrapper .ons-container'); + expect($(navContainer).hasClass('ons-container--full-width')).toBe(true); + }); - it('renders the sub-navigation in the correct container if `wide`', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - wide: true, - ...EXAMPLE_HEADER_NAVIGATION_WITH_SUBNAVIGATION_CONFIG, - }), - ); - - const subNavContainer = $('.ons-navigation--sub .ons-container'); - expect($(subNavContainer).hasClass('ons-container--wide')).toBe(true); - }); + it('renders the sub-navigation in the correct container if `wide`', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + wide: true, + ...EXAMPLE_HEADER_NAVIGATION_WITH_SUBNAVIGATION_CONFIG, + }), + ); + + const subNavContainer = $('.ons-navigation--sub .ons-container'); + expect($(subNavContainer).hasClass('ons-container--wide')).toBe(true); + }); - it('renders the sub-navigation in the correct container if `fullWidth`', () => { - const $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_BASIC, - fullWidth: true, - ...EXAMPLE_HEADER_NAVIGATION_WITH_SUBNAVIGATION_CONFIG, - }), - ); - - const subNavContainer = $('.ons-navigation--sub .ons-container'); - expect($(subNavContainer).hasClass('ons-container--full-width')).toBe(true); + it('renders the sub-navigation in the correct container if `fullWidth`', () => { + const $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_BASIC, + fullWidth: true, + ...EXAMPLE_HEADER_NAVIGATION_WITH_SUBNAVIGATION_CONFIG, + }), + ); + + const subNavContainer = $('.ons-navigation--sub .ons-container'); + expect($(subNavContainer).hasClass('ons-container--full-width')).toBe(true); + }); }); - }); }); describe('mode: with site search autosuggest', () => { - it('renders the search with expected parameters', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('autosuggest'); - - faker.renderComponent('header', EXAMPLE_HEADER_NAVIGATION_WITH_SITESEARCHAUTOSUGGEST); - - expect(buttonSpy.occurrences[0]).toEqual({ - ariaLimitedResults: 'Type more characters to improve your search', - minChars: 3, - language: 'en-gb', - ariaMinChars: 'Enter 3 or more characters for suggestions.', - ariaNResults: 'There are {n} suggestions available.', - ariaOneResult: 'There is one suggestion available.', - ariaResultsLabel: 'Country suggestions', - ariaYouHaveSelected: 'You have selected', - containerClasses: 'ons-autosuggest--header', - id: 'ons-site-search', - input: { - accessiblePlaceholder: true, - autocomplete: 'off', - classes: 'ons-input-search ons-input-search--icon', - label: { - classes: 'ons-u-pl-m ons-label--white', - id: 'ons-site-search-label', - text: 'label', - }, - }, - instructions: 'Use up and down keys to navigate.', - moreResults: 'Continue entering to improve suggestions', - noResults: 'No suggestions found.', - resultsTitle: 'Suggestions', - typeMore: 'Continue entering to get suggestions', - }); - }); - - describe('when the user inputs text', () => { - let $; // Initialize a Cheerio instance - - const mockData = [ - { en: 'England' }, - { en: 'Wales' }, - { en: 'Scotland' }, - { en: 'United States of America' }, - { en: 'United States Virgin Islands' }, - { en: 'Åland Islands' }, - ]; - - beforeEach(() => { - $ = cheerio.load( - renderComponent('header', { - ...EXAMPLE_HEADER_NAVIGATION_WITH_SITESEARCHAUTOSUGGEST, - autosuggestData: mockData, - }), - ); + it('renders the search with expected parameters', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('autosuggest'); + + faker.renderComponent('header', EXAMPLE_HEADER_NAVIGATION_WITH_SITESEARCHAUTOSUGGEST); + + expect(buttonSpy.occurrences[0]).toEqual({ + ariaLimitedResults: 'Type more characters to improve your search', + minChars: 3, + language: 'en-gb', + ariaMinChars: 'Enter 3 or more characters for suggestions.', + ariaNResults: 'There are {n} suggestions available.', + ariaOneResult: 'There is one suggestion available.', + ariaResultsLabel: 'Country suggestions', + ariaYouHaveSelected: 'You have selected', + containerClasses: 'ons-autosuggest--header', + id: 'ons-site-search', + input: { + accessiblePlaceholder: true, + autocomplete: 'off', + classes: 'ons-input-search ons-input-search--icon', + label: { + classes: 'ons-u-pl-m ons-label--white', + id: 'ons-site-search-label', + text: 'label', + }, + }, + instructions: 'Use up and down keys to navigate.', + moreResults: 'Continue entering to improve suggestions', + noResults: 'No suggestions found.', + resultsTitle: 'Suggestions', + typeMore: 'Continue entering to get suggestions', + }); }); - it('does not show suggestions when input length < minimum characters', () => { - $('.ons-js-autosuggest-input').val('En'); + describe('when the user inputs text', () => { + let $; // Initialize a Cheerio instance + + const mockData = [ + { en: 'England' }, + { en: 'Wales' }, + { en: 'Scotland' }, + { en: 'United States of America' }, + { en: 'United States Virgin Islands' }, + { en: 'Åland Islands' }, + ]; + + beforeEach(() => { + $ = cheerio.load( + renderComponent('header', { + ...EXAMPLE_HEADER_NAVIGATION_WITH_SITESEARCHAUTOSUGGEST, + autosuggestData: mockData, + }), + ); + }); - setTimeout(() => { - const suggestionCount = $('.ons-autosuggest__option').length; - expect(suggestionCount).toBe(0); - }, 20); - }); + it('does not show suggestions when input length < minimum characters', () => { + $('.ons-js-autosuggest-input').val('En'); - it('shows suggestions when input length >= minimum characters', () => { - $('.ons-js-autosuggest-input').val('Eng'); + setTimeout(() => { + const suggestionCount = $('.ons-autosuggest__option').length; + expect(suggestionCount).toBe(0); + }, 20); + }); - setTimeout(() => { - const suggestionCount = $('.ons-autosuggest__option').length; - expect(suggestionCount).toBe(1); - }, 20); - }); + it('shows suggestions when input length >= minimum characters', () => { + $('.ons-js-autosuggest-input').val('Eng'); - it('gets the language if set', () => { - $('.ons-js-autosuggest-input').val('Eng'); + setTimeout(() => { + const suggestionCount = $('.ons-autosuggest__option').length; + expect(suggestionCount).toBe(1); + }, 20); + }); + + it('gets the language if set', () => { + $('.ons-js-autosuggest-input').val('Eng'); - const autosuggestElement = $('.ons-js-autosuggest').attr('data-lang'); - expect(autosuggestElement).toBe('en-gb'); + const autosuggestElement = $('.ons-js-autosuggest').attr('data-lang'); + expect(autosuggestElement).toBe('en-gb'); + }); }); - }); }); diff --git a/src/components/helpers/_grid.scss b/src/components/helpers/_grid.scss index 8335fd8c5e..4b73ddc5fb 100644 --- a/src/components/helpers/_grid.scss +++ b/src/components/helpers/_grid.scss @@ -1,6 +1,6 @@ .ons-pl-grid-col { - background: var(--ons-color-grey-5); - font-size: 0.8rem; - margin: 0 0 1rem; - padding: 1rem; + background: var(--ons-color-grey-5); + font-size: 0.8rem; + margin: 0 0 1rem; + padding: 1rem; } diff --git a/src/components/hero/_hero.scss b/src/components/hero/_hero.scss index 56af1ac1d7..8de261d3cf 100644 --- a/src/components/hero/_hero.scss +++ b/src/components/hero/_hero.scss @@ -1,67 +1,67 @@ // Default .ons-hero { - background-color: var(--ons-color-hero-bg); - display: flex; - overflow: hidden; - padding: 0; - position: relative; - - &--dark { - background-color: var(--ons-color-hero-bg-dark); - } - - &__container { - align-items: center; + background-color: var(--ons-color-hero-bg); display: flex; - min-height: 300px; + overflow: hidden; + padding: 0; position: relative; - &--has-details { - align-items: flex-start; // Prevents undesired shift if not enough content + &--dark { + background-color: var(--ons-color-hero-bg-dark); } - } - &__content { - height: 100%; - } + &__container { + align-items: center; + display: flex; + min-height: 300px; + position: relative; - &__pre-title { - margin-bottom: 0.5rem; + &--has-details { + align-items: flex-start; // Prevents undesired shift if not enough content + } + } - @include mq(xxs, m) { - max-width: 145px; + &__content { + height: 100%; } - } - &__title { - font-size: 2.3rem; - line-height: 1.2; - } + &__pre-title { + margin-bottom: 0.5rem; - &__details { - padding-bottom: 3rem; - padding-top: 2rem; - position: relative; - z-index: 5; - } + @include mq(xxs, m) { + max-width: 145px; + } + } + + &__title { + font-size: 2.3rem; + line-height: 1.2; + } - &--dark & { &__details { - color: var(--ons-color-text-inverse) !important; + padding-bottom: 3rem; + padding-top: 2rem; + position: relative; + z-index: 5; + } + + &--dark & { + &__details { + color: var(--ons-color-text-inverse) !important; - a { - color: inherit; - text-decoration: underline solid var(--ons-color-text-inverse-link) 1px; - } + a { + color: inherit; + text-decoration: underline solid var(--ons-color-text-inverse-link) 1px; + } - a:hover { - color: var(--ons-color-text-inverse-link-hover); - text-decoration-thickness: 2px; - } + a:hover { + color: var(--ons-color-text-inverse-link-hover); + text-decoration-thickness: 2px; + } - .ons-details__heading { - color: inherit; - } + .ons-details__heading { + color: inherit; + } + } } - } } diff --git a/src/components/hero/_macro.spec.js b/src/components/hero/_macro.spec.js index 6b6e3dd32b..9c9ad42be3 100644 --- a/src/components/hero/_macro.spec.js +++ b/src/components/hero/_macro.spec.js @@ -6,85 +6,85 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_HERO = { - title: 'Hero title', - subtitle: 'Hero subtitle', - text: 'Hero text', - button: { - url: '#0', - text: 'Get started', - }, + title: 'Hero title', + subtitle: 'Hero subtitle', + text: 'Hero text', + button: { + url: '#0', + text: 'Get started', + }, }; describe('macro: hero', () => { - it('passes jest-axe checks with', async () => { - const $ = cheerio.load(renderComponent('hero', EXAMPLE_HERO)); + it('passes jest-axe checks with', async () => { + const $ = cheerio.load(renderComponent('hero', EXAMPLE_HERO)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - it('has provided variant style classes', () => { - const $ = cheerio.load( - renderComponent('hero', { - variants: ['variant-a', 'variant-b'], - }), - ); + it('has provided variant style classes', () => { + const $ = cheerio.load( + renderComponent('hero', { + variants: ['variant-a', 'variant-b'], + }), + ); - expect($('.ons-hero--variant-a').length).toBe(1); - expect($('.ons-hero--variant-b').length).toBe(1); - }); + expect($('.ons-hero--variant-a').length).toBe(1); + expect($('.ons-hero--variant-b').length).toBe(1); + }); - it('has expected `title`', () => { - const $ = cheerio.load(renderComponent('hero', EXAMPLE_HERO)); + it('has expected `title`', () => { + const $ = cheerio.load(renderComponent('hero', EXAMPLE_HERO)); - const title = $('.ons-hero__title').html().trim(); - expect(title).toBe('Hero title'); - }); + const title = $('.ons-hero__title').html().trim(); + expect(title).toBe('Hero title'); + }); - it('has expected `subtitle`', () => { - const $ = cheerio.load(renderComponent('hero', EXAMPLE_HERO)); + it('has expected `subtitle`', () => { + const $ = cheerio.load(renderComponent('hero', EXAMPLE_HERO)); - const title = $('.ons-hero__subtitle').html().trim(); - expect(title).toBe('Hero subtitle'); - }); + const title = $('.ons-hero__subtitle').html().trim(); + expect(title).toBe('Hero subtitle'); + }); - it('has expected `text`', () => { - const $ = cheerio.load(renderComponent('hero', EXAMPLE_HERO)); + it('has expected `text`', () => { + const $ = cheerio.load(renderComponent('hero', EXAMPLE_HERO)); - const title = $('.ons-hero__text').html().trim(); - expect(title).toBe('Hero text'); - }); + const title = $('.ons-hero__text').html().trim(); + expect(title).toBe('Hero text'); + }); - it('has expected `html`', () => { - const $ = cheerio.load(renderComponent('hero', { ...EXAMPLE_HERO, html: 'some html' })); + it('has expected `html`', () => { + const $ = cheerio.load(renderComponent('hero', { ...EXAMPLE_HERO, html: 'some html' })); - const htmlOutput = $('.ons-hero__additional-html').html(); - expect(htmlOutput).toBe('some html'); - }); + const htmlOutput = $('.ons-hero__additional-html').html(); + expect(htmlOutput).toBe('some html'); + }); - it('outputs the expected button', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); + it('outputs the expected button', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); - faker.renderComponent('hero', EXAMPLE_HERO); + faker.renderComponent('hero', EXAMPLE_HERO); - expect(buttonSpy.occurrences[0]).toHaveProperty('text', 'Get started'); - expect(buttonSpy.occurrences[0]).toHaveProperty('url', '#0'); - }); + expect(buttonSpy.occurrences[0]).toHaveProperty('text', 'Get started'); + expect(buttonSpy.occurrences[0]).toHaveProperty('url', '#0'); + }); - it('outputs the correct button class with `dark` variant', () => { - const faker = templateFaker(); - const buttonSpy = faker.spy('button'); + it('outputs the correct button class with `dark` variant', () => { + const faker = templateFaker(); + const buttonSpy = faker.spy('button'); - faker.renderComponent('hero', { ...EXAMPLE_HERO, variants: 'dark' }); + faker.renderComponent('hero', { ...EXAMPLE_HERO, variants: 'dark' }); - expect(buttonSpy.occurrences[0]).toHaveProperty('classes', ' ons-btn--ghost'); - }); + expect(buttonSpy.occurrences[0]).toHaveProperty('classes', ' ons-btn--ghost'); + }); - it('calls with content', () => { - const $ = cheerio.load(renderComponent('hero', { EXAMPLE_HERO }, 'Example content...')); + it('calls with content', () => { + const $ = cheerio.load(renderComponent('hero', { EXAMPLE_HERO }, 'Example content...')); - const content = $('.ons-hero__additional-content').text().trim(); - expect(content).toEqual(expect.stringContaining('Example content...')); - }); + const content = $('.ons-hero__additional-content').text().trim(); + expect(content).toEqual(expect.stringContaining('Example content...')); + }); }); diff --git a/src/components/icon/_icon.scss b/src/components/icon/_icon.scss index 852af1bc4c..841ef93818 100644 --- a/src/components/icon/_icon.scss +++ b/src/components/icon/_icon.scss @@ -1,60 +1,60 @@ .ons-icon { - height: 1rem; - vertical-align: middle; - width: 1rem; - - &--xxxl { - height: 1.7rem; - width: 1.7rem; - - @include mq(m) { - height: 2.6rem; - width: 2.6rem; + height: 1rem; + vertical-align: middle; + width: 1rem; + + &--xxxl { + height: 1.7rem; + width: 1.7rem; + + @include mq(m) { + height: 2.6rem; + width: 2.6rem; + } } - } - &--xxl { - height: 1.5rem; - width: 1.5rem; + &--xxl { + height: 1.5rem; + width: 1.5rem; - @include mq(m) { - height: 2rem; - width: 2rem; + @include mq(m) { + height: 2rem; + width: 2rem; + } } - } - &--xl { - height: 1.4rem; - width: 1.4rem; + &--xl { + height: 1.4rem; + width: 1.4rem; - @include mq(m) { - height: 1.6rem; - width: 1.6rem; + @include mq(m) { + height: 1.6rem; + width: 1.6rem; + } } - } - &--l { - height: 1.3rem; - width: 1.3rem; + &--l { + height: 1.3rem; + width: 1.3rem; - @include mq(m) { - height: 1.4rem; - width: 1.4rem; + @include mq(m) { + height: 1.4rem; + width: 1.4rem; + } } - } - &--m { - height: 1.1rem; - width: 1.1rem; + &--m { + height: 1.1rem; + width: 1.1rem; - @include mq(m) { - height: 1.2rem; - width: 1.2rem; + @include mq(m) { + height: 1.2rem; + width: 1.2rem; + } } - } - &--s { - height: 0.7rem; - width: 0.7rem; - } + &--s { + height: 0.7rem; + width: 0.7rem; + } } diff --git a/src/components/icon/_macro.spec.js b/src/components/icon/_macro.spec.js index 56d503e74c..8e7a988a1a 100644 --- a/src/components/icon/_macro.spec.js +++ b/src/components/icon/_macro.spec.js @@ -6,119 +6,119 @@ import axe from '../../tests/helpers/axe'; import { renderComponent } from '../../tests/helpers/rendering'; describe('macro: icon', () => { - describe.each([ - 'arrow-forward', - 'arrow-next', - 'arrow-previous', - 'check', - 'chevron', - 'download', - 'exit', - 'external-link', - 'lock', - 'person', - 'print', - 'quote', - 'search', - 'sort-sprite', - 'facebook', - 'twitter', - 'instagram', - 'linkedin', - 'loader', - 'ons-logo-en', - 'ons-logo-cy', - 'ons-logo-stacked-en', - 'ons-logo-stacked-cy', - 'crest', - 'ogl', - 'circle-lined', - ])('icon type: %s', (iconType) => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('icon', { iconType })); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); + describe.each([ + 'arrow-forward', + 'arrow-next', + 'arrow-previous', + 'check', + 'chevron', + 'download', + 'exit', + 'external-link', + 'lock', + 'person', + 'print', + 'quote', + 'search', + 'sort-sprite', + 'facebook', + 'twitter', + 'instagram', + 'linkedin', + 'loader', + 'ons-logo-en', + 'ons-logo-cy', + 'ons-logo-stacked-en', + 'ons-logo-stacked-cy', + 'crest', + 'ogl', + 'circle-lined', + ])('icon type: %s', (iconType) => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('icon', { iconType })); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has an svg element', () => { + const $ = cheerio.load(renderComponent('icon', { iconType })); + + expect($('svg').length).toBe(1); + }); + + it('has a role of image', () => { + const $ = cheerio.load(renderComponent('icon', { iconType })); + + expect($('svg').attr('role')).toBe('img'); + }); + + it('has additionally provided style classes', () => { + const $ = cheerio.load( + renderComponent('icon', { + iconType, + classes: 'extra-class another-extra-class', + }), + ); + + expect($('svg').hasClass('extra-class')).toBe(true); + expect($('svg').hasClass('another-extra-class')).toBe(true); + }); }); - it('has an svg element', () => { - const $ = cheerio.load(renderComponent('icon', { iconType })); - - expect($('svg').length).toBe(1); - }); - - it('has a role of image', () => { - const $ = cheerio.load(renderComponent('icon', { iconType })); - - expect($('svg').attr('role')).toBe('img'); - }); - - it('has additionally provided style classes', () => { - const $ = cheerio.load( - renderComponent('icon', { - iconType, - classes: 'extra-class another-extra-class', - }), - ); - - expect($('svg').hasClass('extra-class')).toBe(true); - expect($('svg').hasClass('another-extra-class')).toBe(true); - }); - }); - - describe.each([ - 'arrow-next', - 'arrow-previous', - 'check', - 'chevron', - 'download', - 'exit', - 'external-link', - 'lock', - 'person', - 'print', - 'quote', - 'search', - 'facebook', - 'twitter', - 'instagram', - 'linkedin', - ])('icon type: %s', (iconType) => { - it('has style variation class for provided icon size', () => { - const $ = cheerio.load( - renderComponent('icon', { - iconType, - iconSize: 'xxl', - }), - ); - - expect($('svg').hasClass('ons-icon--xxl')).toBe(true); + describe.each([ + 'arrow-next', + 'arrow-previous', + 'check', + 'chevron', + 'download', + 'exit', + 'external-link', + 'lock', + 'person', + 'print', + 'quote', + 'search', + 'facebook', + 'twitter', + 'instagram', + 'linkedin', + ])('icon type: %s', (iconType) => { + it('has style variation class for provided icon size', () => { + const $ = cheerio.load( + renderComponent('icon', { + iconType, + iconSize: 'xxl', + }), + ); + + expect($('svg').hasClass('ons-icon--xxl')).toBe(true); + }); }); - }); - - describe.each([ - ['ons-logo-en', 'Office for National Statistics homepage'], - ['ons-logo-cy', 'Hafan Swyddfa Ystadegau Gwladol'], - ['ons-logo-stacked-en', 'Office for National Statistics homepage'], - ['ons-logo-stacked-cy', 'Hafan Swyddfa Ystadegau Gwladol'], - ['crest', 'Royal coat of arms of the United Kingdom'], - ['ogl', 'Open Government License logo'], - ])('icon type: %s', (iconType, expectedAltText) => { - it(`has default alt text '${expectedAltText}'`, () => { - const $ = cheerio.load(renderComponent('icon', { iconType })); - - expect($('title').text().trim()).toBe(expectedAltText); - }); - - it('has provided alt text', () => { - const $ = cheerio.load( - renderComponent('icon', { - iconType, - altText: 'Example alt text', - }), - ); - expect($('title').text().trim()).toBe('Example alt text'); + describe.each([ + ['ons-logo-en', 'Office for National Statistics homepage'], + ['ons-logo-cy', 'Hafan Swyddfa Ystadegau Gwladol'], + ['ons-logo-stacked-en', 'Office for National Statistics homepage'], + ['ons-logo-stacked-cy', 'Hafan Swyddfa Ystadegau Gwladol'], + ['crest', 'Royal coat of arms of the United Kingdom'], + ['ogl', 'Open Government License logo'], + ])('icon type: %s', (iconType, expectedAltText) => { + it(`has default alt text '${expectedAltText}'`, () => { + const $ = cheerio.load(renderComponent('icon', { iconType })); + + expect($('title').text().trim()).toBe(expectedAltText); + }); + + it('has provided alt text', () => { + const $ = cheerio.load( + renderComponent('icon', { + iconType, + altText: 'Example alt text', + }), + ); + + expect($('title').text().trim()).toBe('Example alt text'); + }); }); - }); }); diff --git a/src/components/image/_image.scss b/src/components/image/_image.scss index 7708ef8a4f..0d8a44ca55 100644 --- a/src/components/image/_image.scss +++ b/src/components/image/_image.scss @@ -1,16 +1,16 @@ .ons-image { - margin: 0; + margin: 0; - @extend .ons-u-mb-m; + @extend .ons-u-mb-m; - &__img { - display: block; - max-width: 100%; - } + &__img { + display: block; + max-width: 100%; + } - &__caption { - display: block; - font-size: 0.8rem; - padding: 0.5rem 0 0; - } + &__caption { + display: block; + font-size: 0.8rem; + padding: 0.5rem 0 0; + } } diff --git a/src/components/image/_macro.spec.js b/src/components/image/_macro.spec.js index 6f3130e5e3..920313a783 100644 --- a/src/components/image/_macro.spec.js +++ b/src/components/image/_macro.spec.js @@ -6,112 +6,112 @@ import axe from '../../tests/helpers/axe'; import { renderComponent } from '../../tests/helpers/rendering'; const EXAMPLE_IMAGE_URL_MINIMAL = { - url: 'example.png', + url: 'example.png', }; const EXAMPLE_IMAGE_IMAGE_MINIMAL = { - image: { - smallSrc: 'example-small.png', - largeSrc: 'example-large.png', - }, + image: { + smallSrc: 'example-small.png', + largeSrc: 'example-large.png', + }, }; describe('macro: image', () => { - it('outputs a `figure` element', () => { - const $ = cheerio.load(renderComponent('image', EXAMPLE_IMAGE_URL_MINIMAL)); - - expect($('.ons-image')[0].tagName).toBe('figure'); - }); - - it('outputs a `figurecaption` element when `caption` is provided', () => { - const $ = cheerio.load( - renderComponent('image', { - ...EXAMPLE_IMAGE_URL_MINIMAL, - caption: 'Example image caption', - }), - ); - - expect($('.ons-image__caption')[0].tagName).toBe('figcaption'); - }); - - it('outputs a `figurecaption` element with provided `caption` text', () => { - const $ = cheerio.load( - renderComponent('image', { - ...EXAMPLE_IMAGE_URL_MINIMAL, - caption: 'Example image caption', - }), - ); - - expect($('.ons-image__caption').text().trim()).toBe('Example image caption'); - }); - - describe('mode: url', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('image', EXAMPLE_IMAGE_URL_MINIMAL)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); + it('outputs a `figure` element', () => { + const $ = cheerio.load(renderComponent('image', EXAMPLE_IMAGE_URL_MINIMAL)); + + expect($('.ons-image')[0].tagName).toBe('figure'); }); - it('outputs an `img` element', () => { - const $ = cheerio.load(renderComponent('image', EXAMPLE_IMAGE_URL_MINIMAL)); + it('outputs a `figurecaption` element when `caption` is provided', () => { + const $ = cheerio.load( + renderComponent('image', { + ...EXAMPLE_IMAGE_URL_MINIMAL, + caption: 'Example image caption', + }), + ); - expect($('.ons-image__img')[0].tagName).toBe('img'); + expect($('.ons-image__caption')[0].tagName).toBe('figcaption'); }); - it('outputs an `img` element with the expected `src`', () => { - const $ = cheerio.load(renderComponent('image', EXAMPLE_IMAGE_URL_MINIMAL)); + it('outputs a `figurecaption` element with provided `caption` text', () => { + const $ = cheerio.load( + renderComponent('image', { + ...EXAMPLE_IMAGE_URL_MINIMAL, + caption: 'Example image caption', + }), + ); - expect($('.ons-image__img').attr('src')).toBe('example.png'); + expect($('.ons-image__caption').text().trim()).toBe('Example image caption'); }); - it('outputs an `img` element with the expected alt text', () => { - const $ = cheerio.load( - renderComponent('image', { - ...EXAMPLE_IMAGE_URL_MINIMAL, - alt: 'Example alt text', - }), - ); + describe('mode: url', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('image', EXAMPLE_IMAGE_URL_MINIMAL)); - expect($('.ons-image__img').attr('alt')).toBe('Example alt text'); - }); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - describe('mode: image', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('image', EXAMPLE_IMAGE_IMAGE_MINIMAL)); + it('outputs an `img` element', () => { + const $ = cheerio.load(renderComponent('image', EXAMPLE_IMAGE_URL_MINIMAL)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + expect($('.ons-image__img')[0].tagName).toBe('img'); + }); - it('outputs an `img` element', () => { - const $ = cheerio.load(renderComponent('image', EXAMPLE_IMAGE_IMAGE_MINIMAL)); + it('outputs an `img` element with the expected `src`', () => { + const $ = cheerio.load(renderComponent('image', EXAMPLE_IMAGE_URL_MINIMAL)); - expect($('.ons-image__img')[0].tagName).toBe('img'); - }); + expect($('.ons-image__img').attr('src')).toBe('example.png'); + }); - it('outputs an `img` element with the expected `srcset`', () => { - const $ = cheerio.load(renderComponent('image', EXAMPLE_IMAGE_IMAGE_MINIMAL)); + it('outputs an `img` element with the expected alt text', () => { + const $ = cheerio.load( + renderComponent('image', { + ...EXAMPLE_IMAGE_URL_MINIMAL, + alt: 'Example alt text', + }), + ); - expect($('.ons-image__img').attr('srcset')).toBe('example-small.png 1x, example-large.png 2x'); + expect($('.ons-image__img').attr('alt')).toBe('Example alt text'); + }); }); - it('outputs an `img` element with the expected `src`', () => { - const $ = cheerio.load(renderComponent('image', EXAMPLE_IMAGE_IMAGE_MINIMAL)); + describe('mode: image', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('image', EXAMPLE_IMAGE_IMAGE_MINIMAL)); - expect($('.ons-image__img').attr('src')).toBe('example-small.png'); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('outputs an `img` element', () => { + const $ = cheerio.load(renderComponent('image', EXAMPLE_IMAGE_IMAGE_MINIMAL)); + + expect($('.ons-image__img')[0].tagName).toBe('img'); + }); + + it('outputs an `img` element with the expected `srcset`', () => { + const $ = cheerio.load(renderComponent('image', EXAMPLE_IMAGE_IMAGE_MINIMAL)); + + expect($('.ons-image__img').attr('srcset')).toBe('example-small.png 1x, example-large.png 2x'); + }); + + it('outputs an `img` element with the expected `src`', () => { + const $ = cheerio.load(renderComponent('image', EXAMPLE_IMAGE_IMAGE_MINIMAL)); + + expect($('.ons-image__img').attr('src')).toBe('example-small.png'); + }); - it('outputs an `img` element with the expected alt text', () => { - const $ = cheerio.load( - renderComponent('image', { - ...EXAMPLE_IMAGE_IMAGE_MINIMAL, - alt: 'Example alt text', - }), - ); + it('outputs an `img` element with the expected alt text', () => { + const $ = cheerio.load( + renderComponent('image', { + ...EXAMPLE_IMAGE_IMAGE_MINIMAL, + alt: 'Example alt text', + }), + ); - expect($('.ons-image__img').attr('alt')).toBe('Example alt text'); + expect($('.ons-image__img').attr('alt')).toBe('Example alt text'); + }); }); - }); }); diff --git a/src/components/input/_input-type.scss b/src/components/input/_input-type.scss index 4ec1b6038a..2e73785a67 100644 --- a/src/components/input/_input-type.scss +++ b/src/components/input/_input-type.scss @@ -1,109 +1,106 @@ .ons-input-type { - display: block; - - // Keep the entire component display block, but use inline-flex on inner to prevent the orange focus from going full width - &__inner { - display: inline-flex; - position: relative; - } - - // Double ampersand is needed to solve specificity issues - & &__input { - flex: 1 1 auto; - position: relative; - z-index: 1; - - &:focus { - // Override default input focus so it can wrap prefix/suffix too - box-shadow: none; - outline: none; - } + display: block; - // Override default input error style so it can wrap prefix/suffix too - &.ons-input--error:not(:focus) { - border-right: $input-border-width solid var(--ons-color-input-border); - box-shadow: none; - outline: none; + // Keep the entire component display block, but use inline-flex on inner to prevent the orange focus from going full width + &__inner { + display: inline-flex; + position: relative; } - } - &__type { - background-color: var(--ons-color-button-secondary); - display: block; - flex: 0 0 auto; - font-size: 1rem; - font-weight: $font-weight-bold; - line-height: normal; - padding: $input-padding-vertical $input-padding-horizontal * 2; - text-align: center; - white-space: nowrap; - - &[title] { - text-decoration: none; + // Double ampersand is needed to solve specificity issues + & &__input { + flex: 1 1 auto; + position: relative; + z-index: 1; + + &:focus { + // Override default input focus so it can wrap prefix/suffix too + box-shadow: none; + outline: none; + } + + // Override default input error style so it can wrap prefix/suffix too + &.ons-input--error:not(:focus) { + border-right: $input-border-width solid var(--ons-color-input-border); + box-shadow: none; + outline: none; + } } - } - &__type, - &__type[title] { - border: 1px solid var(--ons-color-input-border); - } - - &__type[title] { - cursor: help; - } + &__type { + background-color: var(--ons-color-button-secondary); + display: block; + flex: 0 0 auto; + font-size: 1rem; + font-weight: $font-weight-bold; + line-height: normal; + padding: $input-padding-vertical $input-padding-horizontal * 2; + text-align: center; + white-space: nowrap; + + &[title] { + text-decoration: none; + cursor: help; + } + } - &__input:focus + &__type::after { - // Style input + prefix/suffix on focus - @extend %ons-input-focus; + &__type, + &__type[title] { + border: 1px solid var(--ons-color-input-border); + } - border-radius: $input-radius; - inset: 0; - content: ''; - display: block; - position: absolute; - } + &__input:focus + &__type::after { + // Style input + prefix/suffix on focus + @extend %ons-input-focus; - &:not(&--prefix) & { - &__type { - border-left: 0; - border-radius: 0 $input-radius $input-radius 0; + border-radius: $input-radius; + inset: 0; + content: ''; + display: block; + position: absolute; } - &__input { - border-radius: $input-radius 0 0 $input-radius; - } - } + &:not(&--prefix) & { + &__type { + border-left: 0; + border-radius: 0 $input-radius $input-radius 0; + } - &--prefix & { - &__type[title] { - border-radius: $input-radius 0 0 $input-radius; - border-right: 0; - order: 0; + &__input { + border-radius: $input-radius 0 0 $input-radius; + } } - &__input { - border-radius: 0 $input-radius $input-radius 0; - order: 1; + &--prefix & { + &__type[title] { + border-radius: $input-radius 0 0 $input-radius; + border-right: 0; + order: 0; + } + + &__input { + border-radius: 0 $input-radius $input-radius 0; + order: 1; + } } - } } // Errors .ons-input--error:not(:focus) { - & + .ons-input-type__type, - & + .ons-input-type__type[title] { - border-color: var(--ons-color-errors); - } - - & + .ons-input-type__type::after { - border-radius: $input-radius; - inset: 0; - - // Style input + prefix/suffix for errors - box-shadow: 0 0 0 1px var(--ons-color-errors); - content: ''; - display: block; - outline: 1px solid transparent; // Add transparent outline because Windows High Contrast Mode doesn't show box-shadows - position: absolute; - } + + .ons-input-type__type, + + .ons-input-type__type[title] { + border-color: var(--ons-color-errors); + } + + + .ons-input-type__type::after { + border-radius: $input-radius; + inset: 0; + + // Style input + prefix/suffix for errors + box-shadow: 0 0 0 1px var(--ons-color-errors); + content: ''; + display: block; + outline: 1px solid transparent; // Add transparent outline because Windows High Contrast Mode doesn't show box-shadows + position: absolute; + } } diff --git a/src/components/input/_input.scss b/src/components/input/_input.scss index c58a2e676b..6948c3a612 100644 --- a/src/components/input/_input.scss +++ b/src/components/input/_input.scss @@ -1,73 +1,72 @@ %ons-input-focus { - box-shadow: 0 0 0 $input-border-width var(--ons-color-input-border), - 0 0 0 4px var(--ons-color-focus); + box-shadow: + 0 0 0 $input-border-width var(--ons-color-input-border), + 0 0 0 4px var(--ons-color-focus); - // Add transparent outline because Windows High Contrast Mode doesn't show box-shadows - outline: 3px solid transparent; - outline-offset: 1px; + // Add transparent outline because Windows High Contrast Mode doesn't show box-shadows + outline: 3px solid transparent; + outline-offset: 1px; - @media screen and (forced-colors: active) { - // To better match the focus states of native controls - outline-color: Highlight; - } + @media screen and (forced-colors: active) { + // To better match the focus states of native controls + outline-color: Highlight; + } } .ons-input { - border: $input-border-width solid var(--ons-color-input-border); - border-radius: $input-radius; - color: inherit; - display: block; - font-family: inherit; - font-size: 1rem; - line-height: 1rem; - padding: $input-padding-vertical $input-padding-horizontal; - position: relative; - width: 100%; - z-index: 3; - - &::-ms-clear { - display: none; - } + border: $input-border-width solid var(--ons-color-input-border); + border-radius: $input-radius; + color: inherit; + display: block; + font-family: inherit; + font-size: 1rem; + line-height: 1rem; + padding: $input-padding-vertical $input-padding-horizontal; + position: relative; + width: 100%; + z-index: 3; - @include mq(s) { - &--text, - &--select { - &:not(.ons-input--block, [class*='input--w-']) { - width: $input-width; - } + &::-ms-clear { + display: none; } - } - &--text, - &--textarea { - // Prevent inner shadow on iOS - appearance: none; - } - - &:focus { - @extend %ons-input-focus; - } + @include mq(s) { + &--text, + &--select { + &:not(.ons-input--block, [class*='input--w-']) { + width: $input-width; + } + } + } - &:disabled { - border-color: var(--ons-color-grey-75); - cursor: not-allowed; - } + &--text, + &--textarea { + // Prevent inner shadow on iOS + appearance: none; + } - &--error:not(:focus) { - border: $input-border-width solid var(--ons-color-errors); - box-shadow: 0 0 0 $input-border-width var(--ons-color-errors); - outline: 1px solid transparent; // Add transparent outline because Windows High Contrast Mode doesn't show box-shadows - } + &:focus { + @extend %ons-input-focus; + } - &--with-description { - margin-bottom: 0.55rem; - } + &:disabled { + border-color: var(--ons-color-grey-75); + cursor: not-allowed; + } - &--with-text-description { - position: relative; + &--error:not(:focus) { + border: $input-border-width solid var(--ons-color-errors); + box-shadow: 0 0 0 $input-border-width var(--ons-color-errors); + outline: 1px solid transparent; // Add transparent outline because Windows High Contrast Mode doesn't show box-shadows + } - } + &--with-description { + margin-bottom: 0.55rem; + } + &--with-text-description { + position: relative; + } } // Text input widths @@ -77,108 +76,108 @@ @include input-width('ons-input-number--w-{x}', 0.54rem); .ons-input--postcode { - max-width: input-width-calc($chars: 5, $num-chars: 2, $spaces: 1); - width: 100%; + max-width: input-width-calc($chars: 5, $num-chars: 2, $spaces: 1); + width: 100%; } .ons-input__helper { - font-size: 0.8rem; - font-weight: $font-weight-bold; - margin-top: 0.2rem; + font-size: 0.8rem; + font-weight: $font-weight-bold; + margin-top: 0.2rem; } .ons-input--select { - appearance: none; - background: var(--ons-color-input-bg) - url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 11.75 7.7'%3E%3Cpath fill='currentColor' d='m1.37.15 4.5 5.1 4.5-5.1a.37.37 0 0 1 .6 0l.7.7a.45.45 0 0 1 0 .5l-5.5 6.2a.37.37 0 0 1-.6 0l-5.5-6.1a.64.64 0 0 1 0-.6l.7-.7a.64.64 0 0 1 .6 0Z'/%3E%3C/svg%3E") - no-repeat center right 10px; - background-size: 1rem; - line-height: 1.3rem; - padding: 0.39rem 2rem 0.39rem $input-padding-horizontal; - - &::-ms-expand { - display: none; - } + appearance: none; + background: var(--ons-color-input-bg) + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 11.75 7.7'%3E%3Cpath fill='currentColor' d='m1.37.15 4.5 5.1 4.5-5.1a.37.37 0 0 1 .6 0l.7.7a.45.45 0 0 1 0 .5l-5.5 6.2a.37.37 0 0 1-.6 0l-5.5-6.1a.64.64 0 0 1 0-.6l.7-.7a.64.64 0 0 1 .6 0Z'/%3E%3C/svg%3E") + no-repeat center right 10px; + background-size: 1rem; + line-height: 1.3rem; + padding: 0.39rem 2rem 0.39rem $input-padding-horizontal; + + &::-ms-expand { + display: none; + } } .ons-input--textarea { - line-height: normal; - resize: vertical; - width: 100%; + line-height: normal; + resize: vertical; + width: 100%; } .ons-input--block { - display: block; - width: 100%; + display: block; + width: 100%; } .ons-input--placeholder { - background: transparent; - &::placeholder { - color: transparent; - } - &:valid:not(:placeholder-shown) { - background-color: var(--ons-color-input-bg); - } - &:focus { - background-color: var(--ons-color-input-bg); - } + background: transparent; + &::placeholder { + color: transparent; + } + &:valid:not(:placeholder-shown) { + background-color: var(--ons-color-input-bg); + } + &:focus { + background-color: var(--ons-color-input-bg); + } } .ons-input--limit-reached:not(:focus) { - border: $input-border-width solid var(--ons-color-ruby-red); + border: $input-border-width solid var(--ons-color-ruby-red); } .ons-input__limit { - display: block; + display: block; - &--reached { - color: var(--ons-color-ruby-red); - } + &--reached { + color: var(--ons-color-ruby-red); + } } .ons-input--ghost { - border: 2px solid rgb(255 255 255 / 60%); - &:focus { - border: 2px solid var(--ons-color-input-border); - } + border: 2px solid rgb(255 255 255 / 60%); + &:focus { + border: 2px solid var(--ons-color-input-border); + } } .ons-input_search-button { - display: flex; - flex-flow: row nowrap !important; - gap: 0.5rem; + display: flex; + flex-flow: row nowrap !important; + gap: 0.5rem; } .ons-input-search { - @extend .ons-input--block; - @extend .ons-input--ghost; - - &--icon { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='%23ffffff'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='M11.86 10.23 8.62 6.99a4.63 4.63 0 1 0-6.34 1.64 4.55 4.55 0 0 0 2.36.64 4.65 4.65 0 0 0 2.33-.65l3.24 3.23a.46.46 0 0 0 .65 0l1-1a.48.48 0 0 0 0-.62Zm-5-3.32a3.28 3.28 0 0 1-2.31.93 3.22 3.22 0 1 1 2.35-.93Z'/%3E%3C/svg%3E"); - background-position: 12px 10px; - background-repeat: no-repeat; - background-size: 18px 18px; - padding-left: 2.4rem; - - &:focus, - &:active, - &:valid:not(:placeholder-shown) { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='%23000000'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='M11.86 10.23 8.62 6.99a4.63 4.63 0 1 0-6.34 1.64 4.55 4.55 0 0 0 2.36.64 4.65 4.65 0 0 0 2.33-.65l3.24 3.23a.46.46 0 0 0 .65 0l1-1a.48.48 0 0 0 0-.62Zm-5-3.32a3.28 3.28 0 0 1-2.31.93 3.22 3.22 0 1 1 2.35-.93Z'/%3E%3C/svg%3E"); - } - &:focus, - &:active { - background-position: 12px 10px; - box-shadow: 0 0 0 3px var(--ons-color-focus); + @extend .ons-input--block; + @extend .ons-input--ghost; + + &--icon { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='%23ffffff'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='M11.86 10.23 8.62 6.99a4.63 4.63 0 1 0-6.34 1.64 4.55 4.55 0 0 0 2.36.64 4.65 4.65 0 0 0 2.33-.65l3.24 3.23a.46.46 0 0 0 .65 0l1-1a.48.48 0 0 0 0-.62Zm-5-3.32a3.28 3.28 0 0 1-2.31.93 3.22 3.22 0 1 1 2.35-.93Z'/%3E%3C/svg%3E"); + background-position: 12px 10px; + background-repeat: no-repeat; + background-size: 18px 18px; + padding-left: 2.4rem; + + &:focus, + &:active, + &:valid:not(:placeholder-shown) { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='%23000000'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='M11.86 10.23 8.62 6.99a4.63 4.63 0 1 0-6.34 1.64 4.55 4.55 0 0 0 2.36.64 4.65 4.65 0 0 0 2.33-.65l3.24 3.23a.46.46 0 0 0 .65 0l1-1a.48.48 0 0 0 0-.62Zm-5-3.32a3.28 3.28 0 0 1-2.31.93 3.22 3.22 0 1 1 2.35-.93Z'/%3E%3C/svg%3E"); + } + &:focus, + &:active { + background-position: 12px 10px; + box-shadow: 0 0 0 3px var(--ons-color-focus); + } } - } - &--dark { - border: 2px solid var(--ons-color-black); - &.ons-input-search--icon { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='%23000000'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='M11.86 10.23 8.62 6.99a4.63 4.63 0 1 0-6.34 1.64 4.55 4.55 0 0 0 2.36.64 4.65 4.65 0 0 0 2.33-.65l3.24 3.23a.46.46 0 0 0 .65 0l1-1a.48.48 0 0 0 0-.62Zm-5-3.32a3.28 3.28 0 0 1-2.31.93 3.22 3.22 0 1 1 2.35-.93Z'/%3E%3C/svg%3E"); + &--dark { + border: 2px solid var(--ons-color-black); + &.ons-input-search--icon { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='%23000000'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='M11.86 10.23 8.62 6.99a4.63 4.63 0 1 0-6.34 1.64 4.55 4.55 0 0 0 2.36.64 4.65 4.65 0 0 0 2.33-.65l3.24 3.23a.46.46 0 0 0 .65 0l1-1a.48.48 0 0 0 0-.62Zm-5-3.32a3.28 3.28 0 0 1-2.31.93 3.22 3.22 0 1 1 2.35-.93Z'/%3E%3C/svg%3E"); + } } - } } // Search type inputs - removes the 'X' clear button from webkit browsers @@ -186,5 +185,5 @@ input[type='search']::-webkit-search-decoration, input[type='search']::-webkit-search-cancel-button, input[type='search']::-webkit-search-results-button, input[type='search']::-webkit-search-results-decoration { - display: none; + display: none; } diff --git a/src/components/input/_macro.spec.js b/src/components/input/_macro.spec.js index 742c9af39b..d6e2257d7a 100644 --- a/src/components/input/_macro.spec.js +++ b/src/components/input/_macro.spec.js @@ -6,693 +6,693 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_INPUT_MINIMAL = { - id: 'example-id', - name: 'example-name', + id: 'example-id', + name: 'example-name', }; const EXAMPLE_INPUT_WITH_LABEL = { - ...EXAMPLE_INPUT_MINIMAL, - label: { - id: 'example-input-label', - text: 'Example input label', - classes: 'extra-label-class', - description: 'Example input label description', - attributes: { a: 42 }, - inline: false, - }, - accessiblePlaceholder: true, + ...EXAMPLE_INPUT_MINIMAL, + label: { + id: 'example-input-label', + text: 'Example input label', + classes: 'extra-label-class', + description: 'Example input label description', + attributes: { a: 42 }, + inline: false, + }, + accessiblePlaceholder: true, }; const EXAMPLE_INPUT_WITH_ERROR = { - ...EXAMPLE_INPUT_WITH_LABEL, - error: { - id: 'feedback-error', - text: 'Enter your feedback', - }, + ...EXAMPLE_INPUT_WITH_LABEL, + error: { + id: 'feedback-error', + text: 'Enter your feedback', + }, }; const EXAMPLE_WITH_SEARCH = { - ...EXAMPLE_INPUT_MINIMAL, - accessiblePlaceholder: true, - searchButton: { - type: 'button', - text: 'Search for address', - id: 'search-for-address', - attributes: { a: 42 }, - classes: 'extra-search-button-class', - iconType: 'search', - visuallyHideButtonText: true, - }, + ...EXAMPLE_INPUT_MINIMAL, + accessiblePlaceholder: true, + searchButton: { + type: 'button', + text: 'Search for address', + id: 'search-for-address', + attributes: { a: 42 }, + classes: 'extra-search-button-class', + iconType: 'search', + visuallyHideButtonText: true, + }, }; const EXAMPLE_INPUT_WITH_CHARACTER_LIMIT = { - ...EXAMPLE_INPUT_MINIMAL, - minLength: 10, - maxLength: 200, - charCheckLimit: { - limit: 200, - charCountSingular: 'You have {x} character remaining', - charCountPlural: 'You have {x} characters remaining', - charCountOverLimitSingular: '{x} character too many', - charCountOverLimitPlural: '{x} characters too many', - }, + ...EXAMPLE_INPUT_MINIMAL, + minLength: 10, + maxLength: 200, + charCheckLimit: { + limit: 200, + charCountSingular: 'You have {x} character remaining', + charCountPlural: 'You have {x} characters remaining', + charCountOverLimitSingular: '{x} character too many', + charCountOverLimitPlural: '{x} characters too many', + }, }; const EXAMPLE_INPUT_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR = { - ...EXAMPLE_INPUT_WITH_ERROR, - dontWrap: true, - mutuallyExclusive: { - or: 'Or', - deselectMessage: 'Selecting this will clear your feedback', - deselectGroupAdjective: 'cleared', - deselectExclusiveOptionAdjective: 'deselected', - exclusiveOptions: [ - { - id: 'feedback-exclusive-option', - name: 'no-feedback', - value: 'no-feedback', - label: { - text: 'I dont want to provide feedback', - }, - }, - ], - }, + ...EXAMPLE_INPUT_WITH_ERROR, + dontWrap: true, + mutuallyExclusive: { + or: 'Or', + deselectMessage: 'Selecting this will clear your feedback', + deselectGroupAdjective: 'cleared', + deselectExclusiveOptionAdjective: 'deselected', + exclusiveOptions: [ + { + id: 'feedback-exclusive-option', + name: 'no-feedback', + value: 'no-feedback', + label: { + text: 'I dont want to provide feedback', + }, + }, + ], + }, }; describe('macro: input', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_LABEL)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the provided `id` attribute', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - - expect($('.ons-input').attr('id')).toBe('example-id'); - }); - - it('has the provided `name` attribute', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - - expect($('.ons-input').attr('name')).toBe('example-name'); - }); - - it('has additionally provided `attributes`', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - attributes: { a: '123', b: '456' }, - }), - ); - - expect($('.ons-input').attr('a')).toBe('123'); - expect($('.ons-input').attr('b')).toBe('456'); - }); - - it('outputs number type in a way that works with more browsers', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - type: 'number', - }), - ); - - expect($('.ons-input').attr('type')).toBe('text'); - expect($('.ons-input').attr('pattern')).toBe('[0-9]*'); - expect($('.ons-input').attr('inputmode')).toBe('numeric'); - }); - - it('has additionally provided style classes', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - classes: 'extra-class another-extra-class', - }), - ); - - expect($('.ons-input').hasClass('extra-class')).toBe(true); - expect($('.ons-input').hasClass('another-extra-class')).toBe(true); - }); - - it('does not have "placeholder" modifier', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - - expect($('.ons-input').hasClass('ons-input--placeholder')).toBe(false); - }); - - it('has "placeholder" modifier when `accessiblePlaceholder` is provided', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_LABEL)); - - expect($('.ons-input').hasClass('ons-input--placeholder')).toBe(true); - }); - - it('does not have `placeholder` attribute', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - - expect($('.ons-input').attr('placeholder')).toBeUndefined(); - }); - - it('has `placeholder` attribute when `placeholder` is provided', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_LABEL)); - - expect($('.ons-input').attr('placeholder')).toBe('Example input label'); - }); - - it('does not have `min` attribute', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - - expect($('.ons-input').attr('min')).toBeUndefined(); - }); - - it('has `min` attribute when `min` is provided', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - min: 10, - }), - ); - - expect($('.ons-input').attr('min')).toBe('10'); - }); - - it('does not have `max` attribute', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - - expect($('.ons-input').attr('max')).toBeUndefined(); - }); - - it('has `max` attribute when `max` is provided', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - max: 100, - }), - ); - - expect($('.ons-input').attr('max')).toBe('100'); - }); - - it('does not have `value` attribute', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - - expect($('.ons-input').attr('value')).toBeUndefined(); - }); - - it('has `value` attribute when `value` is provided', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - value: '100', - }), - ); - - expect($('.ons-input').attr('value')).toBe('100'); - }); - - it('does not have `accept` attribute', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - - expect($('.ons-input').attr('accept')).toBeUndefined(); - }); - - it('has `accept` attribute when `accept` is provided', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - accept: 'image/*', - }), - ); - - expect($('.ons-input').attr('accept')).toBe('image/*'); - }); - - it('does not have `autocomplete` attribute', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - - expect($('.ons-input').attr('autocomplete')).toBeUndefined(); - }); - - it('has `autocomplete` attribute when `autocomplete` is provided', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - autocomplete: 'on', - }), - ); - - expect($('.ons-input').attr('autocomplete')).toBe('on'); - }); - - it.each([['email'], ['tel'], ['text']])('outputs `type` attribute of "%s"', (type) => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - type, - }), - ); - - expect($('.ons-input').attr('type')).toBe(type); - expect($('.ons-input').attr('pattern')).toBeUndefined(); - expect($('.ons-input').attr('inputmode')).toBeUndefined(); - }); - - it.each([ - ['number', 10, 'ons-input-number--w-10'], - ['tel', 20, 'ons-input-number--w-20'], - ])('adds class "ons-input-number" when `type` is "%s"', (type, width, expectedClass) => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - type, - width, - }), - ); - - expect($('.ons-input').hasClass(expectedClass)).toBe(true); - }); - - describe('listeners', () => { - it('renders each listener', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - listeners: { - click: `alert('Input was clicked')`, - keypress: `alert('Key was pressed')`, - }, - }), - ); - - const script = $('script').html(); - expect(script).toContain( - `document.getElementById("example-id").addEventListener('click', function(){ alert('Input was clicked') });`, - ); - expect(script).toContain( - `document.getElementById("example-id").addEventListener('keypress', function(){ alert('Key was pressed') });`, - ); - }); - }); - - it('renders field component', () => { - const faker = templateFaker(); - const fieldSpy = faker.spy('field'); - - faker.renderComponent('input', { - ...EXAMPLE_INPUT_WITH_ERROR, - fieldId: 'example-field-id', - fieldClasses: 'extra-field-class', - dontWrap: true, - }); + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_LABEL)); - expect(fieldSpy.occurrences).toContainEqual({ - id: 'example-field-id', - classes: 'extra-field-class', - dontWrap: true, - error: EXAMPLE_INPUT_WITH_ERROR.error, - inline: false, + const results = await axe($.html()); + expect(results).toHaveNoViolations(); }); - }); - describe('label', () => { - it('does not output a label when `label` is not provided', () => { - const faker = templateFaker(); - const labelSpy = faker.spy('label'); + it('has the provided `id` attribute', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - faker.renderComponent('input', EXAMPLE_INPUT_MINIMAL); - - expect(labelSpy.occurrences.length).toBe(0); + expect($('.ons-input').attr('id')).toBe('example-id'); }); - it('outputs a label when `label` is provided', () => { - const faker = templateFaker(); - const labelSpy = faker.spy('label'); - - faker.renderComponent('input', EXAMPLE_INPUT_WITH_LABEL); + it('has the provided `name` attribute', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - expect(labelSpy.occurrences).toContainEqual({ - for: 'example-id', - id: 'example-input-label', - text: 'Example input label', - classes: 'extra-label-class', - description: 'Example input label description', - attributes: { a: 42 }, - inline: false, - accessiblePlaceholder: true, - }); + expect($('.ons-input').attr('name')).toBe('example-name'); }); - it('outputs `aria-describedby` attribute referencing the label', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_LABEL)); + it('has additionally provided `attributes`', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + attributes: { a: '123', b: '456' }, + }), + ); - expect($('.ons-input').attr('aria-describedby')).toBe('example-input-label-description-hint'); + expect($('.ons-input').attr('a')).toBe('123'); + expect($('.ons-input').attr('b')).toBe('456'); }); - it('outputs a default `aria-describedby` attribute referencing the label when label does not have an `id`', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - label: { - text: 'Example input label', - description: 'Example input label description', - }, - }), - ); - - expect($('.ons-input').attr('aria-describedby')).toBe('description-hint'); - }); - }); + it('outputs number type in a way that works with more browsers', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + type: 'number', + }), + ); - describe('prefix and suffix', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - prefix: { - id: 'example-prefix-id', - title: 'Example prefix title', - }, - }), - ); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); + expect($('.ons-input').attr('type')).toBe('text'); + expect($('.ons-input').attr('pattern')).toBe('[0-9]*'); + expect($('.ons-input').attr('inputmode')).toBe('numeric'); }); - it('adds `aria-labelledby` attribute when `prefix` is provided with `prefix.title`', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - prefix: { - id: 'example-prefix-id', - title: 'Example prefix title', - }, - }), - ); - - expect($('.ons-input').attr('aria-labelledby')).toBe('example-id example-prefix-id'); - }); + it('has additionally provided style classes', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + classes: 'extra-class another-extra-class', + }), + ); - it('adds `aria-labelledby` attribute when `prefix` is provided without `prefix.title`', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - prefix: { - id: 'example-prefix-id', - }, - }), - ); - - expect($('.ons-input').attr('aria-labelledby')).toBe('example-prefix-id'); + expect($('.ons-input').hasClass('extra-class')).toBe(true); + expect($('.ons-input').hasClass('another-extra-class')).toBe(true); }); - it('renders prefix element from `prefix.text`', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - prefix: { - id: 'example-prefix-id', - title: 'Example prefix title', - text: 'Example prefix text', - }, - }), - ); - - expect($('.ons-input-type--prefix .ons-js-input-abbr').attr('id')).toBe('example-prefix-id'); - expect($('.ons-input-type--prefix .ons-js-input-abbr').attr('title')).toBe('Example prefix title'); - expect($('.ons-input-type--prefix .ons-js-input-abbr').text().trim()).toBe('Example prefix text'); - }); + it('does not have "placeholder" modifier', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - it('does not render prefix element when `prefix.id` not set', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - prefix: { - title: 'Example prefix title', - text: 'Example prefix text', - }, - }), - ); - - expect($('.ons-input-type--prefix').length).toBe(0); + expect($('.ons-input').hasClass('ons-input--placeholder')).toBe(false); }); - it('adds `aria-labelledby` attribute when `suffix` is provided', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - suffix: { - id: 'example-suffix-id', - title: 'Example suffix title', - }, - }), - ); - - expect($('.ons-input').attr('aria-labelledby')).toBe('example-id example-suffix-id'); - }); + it('has "placeholder" modifier when `accessiblePlaceholder` is provided', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_LABEL)); - it('adds `aria-labelledby` attribute when `suffix` is provided without `suffix.title`', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - suffix: { - id: 'example-suffix-id', - }, - }), - ); - - expect($('.ons-input').attr('aria-labelledby')).toBe('example-suffix-id'); + expect($('.ons-input').hasClass('ons-input--placeholder')).toBe(true); }); - it('renders suffix element from `suffix.text`', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - suffix: { - id: 'example-suffix-id', - title: 'Example suffix title', - text: 'Example suffix text', - }, - }), - ); - - expect($('.ons-js-input-abbr').attr('id')).toBe('example-suffix-id'); - expect($('.ons-js-input-abbr').attr('title')).toBe('Example suffix title'); - expect($('.ons-js-input-abbr').text().trim()).toBe('Example suffix text'); - }); + it('does not have `placeholder` attribute', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - it('does not render suffix element when `suffix.id` not set', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - suffix: { - title: 'Example suffix title', - text: 'Example suffix text', - }, - }), - ); - - expect($('.ons-input').length).toBe(0); + expect($('.ons-input').attr('placeholder')).toBeUndefined(); }); - it('renders an `abbr` tag when `title` set', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - suffix: { - text: 'Example suffix text', - title: 'Example suffix title', - id: 'example-suffix-id', - }, - }), - ); - - expect($('.ons-input + abbr').length).toBe(1); - }); + it('has `placeholder` attribute when `placeholder` is provided', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_LABEL)); - it('renders a `span` tag when `title` not set', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - suffix: { - text: 'Example suffix text', - id: 'example-suffix-id', - }, - }), - ); - - expect($('.ons-input + span').length).toBe(1); + expect($('.ons-input').attr('placeholder')).toBe('Example input label'); }); - }); - describe('search', () => { - it.each('outputs `type` attribute of "search"', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_WITH_SEARCH)); + it('does not have `min` attribute', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - expect($('.ons-input').attr('type')).toBe('search'); - expect($('.ons-input').attr('pattern')).toBeUndefined(); - expect($('.ons-input').attr('inputmode')).toBeUndefined(); + expect($('.ons-input').attr('min')).toBeUndefined(); }); - it('has the "ons-search__input" class', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_WITH_SEARCH)); + it('has `min` attribute when `min` is provided', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + min: 10, + }), + ); - expect($('.ons-input').hasClass('ons-search__input')).toBe(true); - }); - }); - - describe('with character limit', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_WITH_CHARACTER_LIMIT, - label: { - id: 'example-input-label', - text: 'Example input label', - }, - }), - ); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); + expect($('.ons-input').attr('min')).toBe('10'); }); - it('has the provided minimum length', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_CHARACTER_LIMIT)); + it('does not have `max` attribute', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - expect($('.ons-input').attr('minlength')).toBe('10'); + expect($('.ons-input').attr('max')).toBeUndefined(); }); - it('has the provided maximum length', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_CHARACTER_LIMIT)); + it('has `max` attribute when `max` is provided', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + max: 100, + }), + ); - expect($('.ons-input').attr('maxlength')).toBe('200'); + expect($('.ons-input').attr('max')).toBe('100'); }); - it('does not have `data-char-check-countdown` attribute when `charcheckCountdown` is not provided', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_CHARACTER_LIMIT)); + it('does not have `value` attribute', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - expect($('.ons-input').attr('data-char-check-countdown')).toBe(undefined); + expect($('.ons-input').attr('value')).toBeUndefined(); }); - it('has `data-char-check-countdown` attribute when `charcheckCountdown` is true', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_WITH_CHARACTER_LIMIT, - charCheckLimit: { - ...EXAMPLE_INPUT_WITH_CHARACTER_LIMIT.charCheckLimit, - charcheckCountdown: true, - }, - }), - ); - - expect($('.ons-input').attr('data-char-check-countdown')).toBe('true'); - }); - - it('has data attribute which references the character limit component', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_CHARACTER_LIMIT)); + it('has `value` attribute when `value` is provided', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + value: '100', + }), + ); - expect($('.ons-input').attr('data-char-check-ref')).toBe('example-id-check'); + expect($('.ons-input').attr('value')).toBe('100'); }); - it('has data attribute which defines limit for character check', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_CHARACTER_LIMIT)); + it('does not have `accept` attribute', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - expect($('.ons-input').attr('data-char-check-num')).toBe('200'); + expect($('.ons-input').attr('accept')).toBeUndefined(); }); - it('has `aria-describedby` attribute which references the character limit component', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_CHARACTER_LIMIT)); + it('has `accept` attribute when `accept` is provided', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + accept: 'image/*', + }), + ); - expect($('.ons-input').attr('aria-describedby')).toBe('example-id-check'); + expect($('.ons-input').attr('accept')).toBe('image/*'); }); - it('renders character limit component', () => { - const faker = templateFaker(); - const charCheckLimitSpy = faker.spy('char-check-limit'); + it('does not have `autocomplete` attribute', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_MINIMAL)); - faker.renderComponent('input', EXAMPLE_INPUT_WITH_CHARACTER_LIMIT); - - expect(charCheckLimitSpy.occurrences).toContainEqual({ - id: 'example-id-check', - limit: 200, - variant: 'check', - charCountSingular: 'You have {x} character remaining', - charCountPlural: 'You have {x} characters remaining', - charCountOverLimitSingular: '{x} character too many', - charCountOverLimitPlural: '{x} characters too many', - }); + expect($('.ons-input').attr('autocomplete')).toBeUndefined(); }); - }); - describe('with error', () => { - it('has the `error` modifier class', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_ERROR)); + it('has `autocomplete` attribute when `autocomplete` is provided', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + autocomplete: 'on', + }), + ); - expect($('.ons-input').hasClass('ons-input--error')).toBe(true); - }); - }); - - describe('with post textbox link', () => { - it('renders the expected link', () => { - const $ = cheerio.load( - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - postTextboxLinkUrl: 'https://example.com/link', - postTextboxLinkText: 'Example link', - }), - ); - - expect($('a.ons-input__post-link').attr('href')).toBe('https://example.com/link'); - expect($('a.ons-input__post-link').text().trim()).toBe('Example link'); + expect($('.ons-input').attr('autocomplete')).toBe('on'); }); - }); - describe('mutually exclusive', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR)); + it.each([['email'], ['tel'], ['text']])('outputs `type` attribute of "%s"', (type) => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + type, + }), + ); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); + expect($('.ons-input').attr('type')).toBe(type); + expect($('.ons-input').attr('pattern')).toBeUndefined(); + expect($('.ons-input').attr('inputmode')).toBeUndefined(); }); - it('has the `ons-js-exclusive-group-item` class', () => { - const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR)); + it.each([ + ['number', 10, 'ons-input-number--w-10'], + ['tel', 20, 'ons-input-number--w-20'], + ])('adds class "ons-input-number" when `type` is "%s"', (type, width, expectedClass) => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + type, + width, + }), + ); - expect($('.ons-input').hasClass('ons-js-exclusive-group-item')).toBe(true); + expect($('.ons-input').hasClass(expectedClass)).toBe(true); }); - it('renders mutually exclusive component', () => { - const faker = templateFaker(); - const mutuallyExclusiveSpy = faker.spy('mutually-exclusive'); - - faker.renderComponent('input', { - ...EXAMPLE_INPUT_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR, - fieldId: 'example-field-id', - fieldClasses: 'extra-field-class', - legend: 'Legend text', - legendClasses: 'extra-legend-class', - description: 'Example description text', - legendIsQuestionTitle: true, - autosuggestResults: 'RESULTS', - }); - - expect(mutuallyExclusiveSpy.occurrences).toContainEqual({ - id: 'example-field-id', - legend: 'Legend text', - legendClasses: 'extra-legend-class ons-js-input-legend', - description: 'Example description text', - dontWrap: true, - legendIsQuestionTitle: true, - exclusiveOptions: EXAMPLE_INPUT_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR.mutuallyExclusive.exclusiveOptions, - or: 'Or', - deselectMessage: 'Selecting this will clear your feedback', - deselectGroupAdjective: 'cleared', - deselectExclusiveOptionAdjective: 'deselected', - error: EXAMPLE_INPUT_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR.error, - autosuggestResults: 'RESULTS', - }); + describe('listeners', () => { + it('renders each listener', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + listeners: { + click: `alert('Input was clicked')`, + keypress: `alert('Key was pressed')`, + }, + }), + ); + + const script = $('script').html(); + expect(script).toContain( + `document.getElementById("example-id").addEventListener('click', function(){ alert('Input was clicked') });`, + ); + expect(script).toContain( + `document.getElementById("example-id").addEventListener('keypress', function(){ alert('Key was pressed') });`, + ); + }); + }); + + it('renders field component', () => { + const faker = templateFaker(); + const fieldSpy = faker.spy('field'); + + faker.renderComponent('input', { + ...EXAMPLE_INPUT_WITH_ERROR, + fieldId: 'example-field-id', + fieldClasses: 'extra-field-class', + dontWrap: true, + }); + + expect(fieldSpy.occurrences).toContainEqual({ + id: 'example-field-id', + classes: 'extra-field-class', + dontWrap: true, + error: EXAMPLE_INPUT_WITH_ERROR.error, + inline: false, + }); + }); + + describe('label', () => { + it('does not output a label when `label` is not provided', () => { + const faker = templateFaker(); + const labelSpy = faker.spy('label'); + + faker.renderComponent('input', EXAMPLE_INPUT_MINIMAL); + + expect(labelSpy.occurrences.length).toBe(0); + }); + + it('outputs a label when `label` is provided', () => { + const faker = templateFaker(); + const labelSpy = faker.spy('label'); + + faker.renderComponent('input', EXAMPLE_INPUT_WITH_LABEL); + + expect(labelSpy.occurrences).toContainEqual({ + for: 'example-id', + id: 'example-input-label', + text: 'Example input label', + classes: 'extra-label-class', + description: 'Example input label description', + attributes: { a: 42 }, + inline: false, + accessiblePlaceholder: true, + }); + }); + + it('outputs `aria-describedby` attribute referencing the label', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_LABEL)); + + expect($('.ons-input').attr('aria-describedby')).toBe('example-input-label-description-hint'); + }); + + it('outputs a default `aria-describedby` attribute referencing the label when label does not have an `id`', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + label: { + text: 'Example input label', + description: 'Example input label description', + }, + }), + ); + + expect($('.ons-input').attr('aria-describedby')).toBe('description-hint'); + }); + }); + + describe('prefix and suffix', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + prefix: { + id: 'example-prefix-id', + title: 'Example prefix title', + }, + }), + ); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('adds `aria-labelledby` attribute when `prefix` is provided with `prefix.title`', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + prefix: { + id: 'example-prefix-id', + title: 'Example prefix title', + }, + }), + ); + + expect($('.ons-input').attr('aria-labelledby')).toBe('example-id example-prefix-id'); + }); + + it('adds `aria-labelledby` attribute when `prefix` is provided without `prefix.title`', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + prefix: { + id: 'example-prefix-id', + }, + }), + ); + + expect($('.ons-input').attr('aria-labelledby')).toBe('example-prefix-id'); + }); + + it('renders prefix element from `prefix.text`', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + prefix: { + id: 'example-prefix-id', + title: 'Example prefix title', + text: 'Example prefix text', + }, + }), + ); + + expect($('.ons-input-type--prefix .ons-js-input-abbr').attr('id')).toBe('example-prefix-id'); + expect($('.ons-input-type--prefix .ons-js-input-abbr').attr('title')).toBe('Example prefix title'); + expect($('.ons-input-type--prefix .ons-js-input-abbr').text().trim()).toBe('Example prefix text'); + }); + + it('does not render prefix element when `prefix.id` not set', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + prefix: { + title: 'Example prefix title', + text: 'Example prefix text', + }, + }), + ); + + expect($('.ons-input-type--prefix').length).toBe(0); + }); + + it('adds `aria-labelledby` attribute when `suffix` is provided', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + suffix: { + id: 'example-suffix-id', + title: 'Example suffix title', + }, + }), + ); + + expect($('.ons-input').attr('aria-labelledby')).toBe('example-id example-suffix-id'); + }); + + it('adds `aria-labelledby` attribute when `suffix` is provided without `suffix.title`', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + suffix: { + id: 'example-suffix-id', + }, + }), + ); + + expect($('.ons-input').attr('aria-labelledby')).toBe('example-suffix-id'); + }); + + it('renders suffix element from `suffix.text`', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + suffix: { + id: 'example-suffix-id', + title: 'Example suffix title', + text: 'Example suffix text', + }, + }), + ); + + expect($('.ons-js-input-abbr').attr('id')).toBe('example-suffix-id'); + expect($('.ons-js-input-abbr').attr('title')).toBe('Example suffix title'); + expect($('.ons-js-input-abbr').text().trim()).toBe('Example suffix text'); + }); + + it('does not render suffix element when `suffix.id` not set', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + suffix: { + title: 'Example suffix title', + text: 'Example suffix text', + }, + }), + ); + + expect($('.ons-input').length).toBe(0); + }); + + it('renders an `abbr` tag when `title` set', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + suffix: { + text: 'Example suffix text', + title: 'Example suffix title', + id: 'example-suffix-id', + }, + }), + ); + + expect($('.ons-input + abbr').length).toBe(1); + }); + + it('renders a `span` tag when `title` not set', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + suffix: { + text: 'Example suffix text', + id: 'example-suffix-id', + }, + }), + ); + + expect($('.ons-input + span').length).toBe(1); + }); + }); + + describe('search', () => { + it.each('outputs `type` attribute of "search"', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_WITH_SEARCH)); + + expect($('.ons-input').attr('type')).toBe('search'); + expect($('.ons-input').attr('pattern')).toBeUndefined(); + expect($('.ons-input').attr('inputmode')).toBeUndefined(); + }); + + it('has the "ons-search__input" class', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_WITH_SEARCH)); + + expect($('.ons-input').hasClass('ons-search__input')).toBe(true); + }); + }); + + describe('with character limit', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_WITH_CHARACTER_LIMIT, + label: { + id: 'example-input-label', + text: 'Example input label', + }, + }), + ); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has the provided minimum length', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_CHARACTER_LIMIT)); + + expect($('.ons-input').attr('minlength')).toBe('10'); + }); + + it('has the provided maximum length', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_CHARACTER_LIMIT)); + + expect($('.ons-input').attr('maxlength')).toBe('200'); + }); + + it('does not have `data-char-check-countdown` attribute when `charcheckCountdown` is not provided', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_CHARACTER_LIMIT)); + + expect($('.ons-input').attr('data-char-check-countdown')).toBe(undefined); + }); + + it('has `data-char-check-countdown` attribute when `charcheckCountdown` is true', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_WITH_CHARACTER_LIMIT, + charCheckLimit: { + ...EXAMPLE_INPUT_WITH_CHARACTER_LIMIT.charCheckLimit, + charcheckCountdown: true, + }, + }), + ); + + expect($('.ons-input').attr('data-char-check-countdown')).toBe('true'); + }); + + it('has data attribute which references the character limit component', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_CHARACTER_LIMIT)); + + expect($('.ons-input').attr('data-char-check-ref')).toBe('example-id-check'); + }); + + it('has data attribute which defines limit for character check', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_CHARACTER_LIMIT)); + + expect($('.ons-input').attr('data-char-check-num')).toBe('200'); + }); + + it('has `aria-describedby` attribute which references the character limit component', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_CHARACTER_LIMIT)); + + expect($('.ons-input').attr('aria-describedby')).toBe('example-id-check'); + }); + + it('renders character limit component', () => { + const faker = templateFaker(); + const charCheckLimitSpy = faker.spy('char-check-limit'); + + faker.renderComponent('input', EXAMPLE_INPUT_WITH_CHARACTER_LIMIT); + + expect(charCheckLimitSpy.occurrences).toContainEqual({ + id: 'example-id-check', + limit: 200, + variant: 'check', + charCountSingular: 'You have {x} character remaining', + charCountPlural: 'You have {x} characters remaining', + charCountOverLimitSingular: '{x} character too many', + charCountOverLimitPlural: '{x} characters too many', + }); + }); + }); + + describe('with error', () => { + it('has the `error` modifier class', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_ERROR)); + + expect($('.ons-input').hasClass('ons-input--error')).toBe(true); + }); + }); + + describe('with post textbox link', () => { + it('renders the expected link', () => { + const $ = cheerio.load( + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + postTextboxLinkUrl: 'https://example.com/link', + postTextboxLinkText: 'Example link', + }), + ); + + expect($('a.ons-input__post-link').attr('href')).toBe('https://example.com/link'); + expect($('a.ons-input__post-link').text().trim()).toBe('Example link'); + }); + }); + + describe('mutually exclusive', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR)); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); + + it('has the `ons-js-exclusive-group-item` class', () => { + const $ = cheerio.load(renderComponent('input', EXAMPLE_INPUT_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR)); + + expect($('.ons-input').hasClass('ons-js-exclusive-group-item')).toBe(true); + }); + + it('renders mutually exclusive component', () => { + const faker = templateFaker(); + const mutuallyExclusiveSpy = faker.spy('mutually-exclusive'); + + faker.renderComponent('input', { + ...EXAMPLE_INPUT_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR, + fieldId: 'example-field-id', + fieldClasses: 'extra-field-class', + legend: 'Legend text', + legendClasses: 'extra-legend-class', + description: 'Example description text', + legendIsQuestionTitle: true, + autosuggestResults: 'RESULTS', + }); + + expect(mutuallyExclusiveSpy.occurrences).toContainEqual({ + id: 'example-field-id', + legend: 'Legend text', + legendClasses: 'extra-legend-class ons-js-input-legend', + description: 'Example description text', + dontWrap: true, + legendIsQuestionTitle: true, + exclusiveOptions: EXAMPLE_INPUT_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR.mutuallyExclusive.exclusiveOptions, + or: 'Or', + deselectMessage: 'Selecting this will clear your feedback', + deselectGroupAdjective: 'cleared', + deselectExclusiveOptionAdjective: 'deselected', + error: EXAMPLE_INPUT_WITH_MUTUALLY_EXCLUSIVE_WITH_ERROR.error, + autosuggestResults: 'RESULTS', + }); + }); }); - }); }); diff --git a/src/components/input/character-check.dom.js b/src/components/input/character-check.dom.js index f9adadd0fa..78867891cd 100644 --- a/src/components/input/character-check.dom.js +++ b/src/components/input/character-check.dom.js @@ -1,13 +1,13 @@ import domready from '../../js/domready'; async function initialise() { - const checkedInputs = [...document.querySelectorAll('.ons-js-char-check-input')]; + const checkedInputs = [...document.querySelectorAll('.ons-js-char-check-input')]; - if (checkedInputs.length) { - const CharCheck = (await import('../char-check-limit/character-check')).default; + if (checkedInputs.length) { + const CharCheck = (await import('../char-check-limit/character-check')).default; - checkedInputs.forEach((input) => new CharCheck(input)); - } + checkedInputs.forEach((input) => new CharCheck(input)); + } } domready(initialise); diff --git a/src/components/input/input.dom.js b/src/components/input/input.dom.js index 300b6f2bec..44d7429107 100644 --- a/src/components/input/input.dom.js +++ b/src/components/input/input.dom.js @@ -1,11 +1,11 @@ import domready from '../../js/domready'; domready(async () => { - const abbrInputs = [...document.querySelectorAll('.ons-js-input-container-abbr')]; + const abbrInputs = [...document.querySelectorAll('.ons-js-input-container-abbr')]; - if (abbrInputs.length) { - const abbrInput = (await import('./input')).default; + if (abbrInputs.length) { + const abbrInput = (await import('./input')).default; - abbrInputs.forEach((element) => new abbrInput(element)); - } + abbrInputs.forEach((element) => new abbrInput(element)); + } }); diff --git a/src/components/input/input.js b/src/components/input/input.js index 208b481919..564c2b0b70 100644 --- a/src/components/input/input.js +++ b/src/components/input/input.js @@ -1,14 +1,14 @@ export default class AbbrInput { - constructor(context) { - this.abbrInput = context; - this.bindEventListeners(); - } + constructor(context) { + this.abbrInput = context; + this.bindEventListeners(); + } - bindEventListeners() { - this.abbrInput.querySelector('.ons-js-input-abbr').addEventListener('click', this.handleClick.bind(this)); - } + bindEventListeners() { + this.abbrInput.querySelector('.ons-js-input-abbr').addEventListener('click', this.handleClick.bind(this)); + } - handleClick() { - this.abbrInput.querySelector('.ons-input').focus(); - } + handleClick() { + this.abbrInput.querySelector('.ons-input').focus(); + } } diff --git a/src/components/input/input.spec.js b/src/components/input/input.spec.js index a38fbec957..e4ca757eef 100644 --- a/src/components/input/input.spec.js +++ b/src/components/input/input.spec.js @@ -1,25 +1,25 @@ import { renderComponent, setTestPage } from '../../tests/helpers/rendering'; const EXAMPLE_INPUT_MINIMAL = { - id: 'example-id', - name: 'example-name', + id: 'example-id', + name: 'example-name', }; describe('script: input', () => { - it('focuses input when abbreviation is clicked', async () => { - await setTestPage( - '/test', - renderComponent('input', { - ...EXAMPLE_INPUT_MINIMAL, - prefix: { - id: 'example-prefix-id', - title: 'Example prefix title', - text: 'Example prefix text', - }, - }), - ); - await page.click('.ons-js-input-abbr'); - const focusedElementId = await page.evaluate(() => document.activeElement.id); - expect(focusedElementId).toEqual('example-id'); - }); + it('focuses input when abbreviation is clicked', async () => { + await setTestPage( + '/test', + renderComponent('input', { + ...EXAMPLE_INPUT_MINIMAL, + prefix: { + id: 'example-prefix-id', + title: 'Example prefix title', + text: 'Example prefix text', + }, + }), + ); + await page.click('.ons-js-input-abbr'); + const focusedElementId = await page.evaluate(() => document.activeElement.id); + expect(focusedElementId).toEqual('example-id'); + }); }); diff --git a/src/components/label/_label.scss b/src/components/label/_label.scss index 8e32086812..cbf5ca266a 100644 --- a/src/components/label/_label.scss +++ b/src/components/label/_label.scss @@ -1,31 +1,31 @@ .ons-label { - color: inherit; - display: block; - font-weight: $font-weight-bold; - margin: 0 0 0.6rem; + color: inherit; + display: block; + font-weight: $font-weight-bold; + margin: 0 0 0.6rem; - &__description { - @extend .ons-u-fs-s; + &__description { + @extend .ons-u-fs-s; - display: block; - line-height: 1.4; - } + display: block; + line-height: 1.4; + } - &--with-description { - margin-bottom: 0; - padding-bottom: 0; - } + &--with-description { + margin-bottom: 0; + padding-bottom: 0; + } - &--placeholder { - font-size: 1rem; - font-weight: $font-weight-regular; - color: var(--ons-color-text-placeholder); - left: 10px; - position: absolute; - top: 6px; - } + &--placeholder { + font-size: 1rem; + font-weight: $font-weight-regular; + color: var(--ons-color-text-placeholder); + left: 10px; + position: absolute; + top: 6px; + } - &--white { - color: var(--ons-color-white); - } + &--white { + color: var(--ons-color-white); + } } diff --git a/src/components/label/_macro.spec.js b/src/components/label/_macro.spec.js index 39fb1844d1..b664b39671 100644 --- a/src/components/label/_macro.spec.js +++ b/src/components/label/_macro.spec.js @@ -6,206 +6,209 @@ import axe from '../../tests/helpers/axe'; import { renderComponent } from '../../tests/helpers/rendering'; const EXAMPLE_LABEL = { - id: 'example-label', - for: 'some-input', - text: 'Example label text', + id: 'example-label', + for: 'some-input', + text: 'Example label text', }; const EXAMPLE_LABEL_WITH_INPUT_TYPE = { - ...EXAMPLE_LABEL, - inputType: 'checkbox', + ...EXAMPLE_LABEL, + inputType: 'checkbox', }; const EXAMPLE_LABEL_WITH_DESCRIPTION = { - ...EXAMPLE_LABEL, - description: 'An example description', + ...EXAMPLE_LABEL, + description: 'An example description', }; const EXAMPLE_LABEL_WITH_ACCESSIBLE_PLACEHOLDER = { - ...EXAMPLE_LABEL, - accessiblePlaceholder: 'An example placeholder', + ...EXAMPLE_LABEL, + accessiblePlaceholder: 'An example placeholder', }; describe('macro: label', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('label', EXAMPLE_LABEL)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('passes jest-axe checks with description', async () => { - const $ = cheerio.load(renderComponent('label', EXAMPLE_LABEL_WITH_DESCRIPTION)); - - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); - - it('has the provided `id` attribute', () => { - const $ = cheerio.load(renderComponent('label', EXAMPLE_LABEL)); - - expect($('.ons-label').attr('id')).toBe('example-label'); - }); - - it('has the provided `for` attribute', () => { - const $ = cheerio.load(renderComponent('label', EXAMPLE_LABEL)); - - expect($('.ons-label').attr('for')).toBe('some-input'); - }); - - it('has `aria-describedby` and correct value when description is provided', () => { - const $ = cheerio.load(renderComponent('label', EXAMPLE_LABEL_WITH_DESCRIPTION)); - - expect($('.ons-label').attr('aria-describedby')).toBe('example-label-description-hint'); - }); - - it.each([ - ['`inputType` parameter is not provided', EXAMPLE_LABEL, true], - ['`inputType` parameter is provided', EXAMPLE_LABEL_WITH_INPUT_TYPE, false], - ])('has the `ons-label` block class when %s', (_, params, expectedState) => { - const $ = cheerio.load(renderComponent('label', params)); - - expect($('label').hasClass('ons-label')).toBe(expectedState); - }); - - it.each([ - ['`description` parameter is not provided', EXAMPLE_LABEL, false], - ['`description` parameter is provided', EXAMPLE_LABEL_WITH_DESCRIPTION, true], - ])('has the `ons-label--with-description` modifier class when %s', (_, params, expectedState) => { - const $ = cheerio.load(renderComponent('label', params)); - - expect($('.ons-label').hasClass('ons-label--with-description')).toBe(expectedState); - }); - - it.each([ - ['`accessiblePlaceholder` parameter is not provided', EXAMPLE_LABEL, false], - ['`accessiblePlaceholder` parameter is provided', EXAMPLE_LABEL_WITH_ACCESSIBLE_PLACEHOLDER, true], - ])('has the `ons-label--placeholder` modifier class when %s', (_, params, expectedState) => { - const $ = cheerio.load(renderComponent('label', params)); - - expect($('.ons-label').hasClass('ons-label--placeholder')).toBe(expectedState); - }); - - it('has additionally provided style classes', () => { - const $ = cheerio.load( - renderComponent('label', { - ...EXAMPLE_LABEL, - classes: 'extra-class another-extra-class', - }), - ); - - expect($('.ons-label').hasClass('extra-class')).toBe(true); - expect($('.ons-label').hasClass('another-extra-class')).toBe(true); - }); - - it('has additionally provided `attributes`', () => { - const $ = cheerio.load( - renderComponent('label', { - ...EXAMPLE_LABEL, - attributes: { - a: 123, - b: 456, - }, - }), - ); - - expect($('.ons-label').attr('a')).toBe('123'); - expect($('.ons-label').attr('b')).toBe('456'); - }); - - it.each([ - ['when without a description', EXAMPLE_LABEL], - ['when with a description', EXAMPLE_LABEL_WITH_DESCRIPTION], - ])('has the provided `text` %s', (_, params) => { - const $ = cheerio.load(renderComponent('label', params)); - - expect($('.ons-label').text().trim().startsWith('Example label text')).toBe(true); - }); - - describe('description element', () => { - it.each([ - ['when the `inputType` parameter is not provided', EXAMPLE_LABEL_WITH_DESCRIPTION], - [ - 'when the "checkbox" `inputType` parameter is provided', - { - ...EXAMPLE_LABEL_WITH_DESCRIPTION, - inputType: 'checkbox', - }, - ], - [ - 'when the "radio" `inputType` parameter is provided', - { - ...EXAMPLE_LABEL_WITH_DESCRIPTION, - inputType: 'radio', - }, - ], - ])('has the provided `description` text %s', (_, params) => { - const $ = cheerio.load(renderComponent('label', params)); - - expect($('.ons-label__description').text().trim()).toBe('An example description'); + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('label', EXAMPLE_LABEL)); + + const results = await axe($.html()); + expect(results).toHaveNoViolations(); }); - it('has a default `id` attribute of `description-hint`', () => { - const $ = cheerio.load( - renderComponent('label', { - ...EXAMPLE_LABEL_WITH_DESCRIPTION, - id: undefined, - }), - ); + it('passes jest-axe checks with description', async () => { + const $ = cheerio.load(renderComponent('label', EXAMPLE_LABEL_WITH_DESCRIPTION)); - expect($('.ons-label__description').attr('id')).toBe('description-hint'); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); }); - it('has an `id` attribute which adds `-description-hint` to the provided `id`', () => { - const $ = cheerio.load(renderComponent('label', EXAMPLE_LABEL_WITH_DESCRIPTION)); + it('has the provided `id` attribute', () => { + const $ = cheerio.load(renderComponent('label', EXAMPLE_LABEL)); - expect($('.ons-label__description').attr('id')).toBe('example-label-description-hint'); + expect($('.ons-label').attr('id')).toBe('example-label'); }); - it('has the modifier class `ons-input--with-description` when `inputType` is not provided', () => { - const $ = cheerio.load(renderComponent('label', EXAMPLE_LABEL_WITH_DESCRIPTION)); + it('has the provided `for` attribute', () => { + const $ = cheerio.load(renderComponent('label', EXAMPLE_LABEL)); + + expect($('.ons-label').attr('for')).toBe('some-input'); + }); + + it('has `aria-describedby` and correct value when description is provided', () => { + const $ = cheerio.load(renderComponent('label', EXAMPLE_LABEL_WITH_DESCRIPTION)); + + expect($('.ons-label').attr('aria-describedby')).toBe('example-label-description-hint'); + }); + + it.each([ + ['`inputType` parameter is not provided', EXAMPLE_LABEL, true], + ['`inputType` parameter is provided', EXAMPLE_LABEL_WITH_INPUT_TYPE, false], + ])('has the `ons-label` block class when %s', (_, params, expectedState) => { + const $ = cheerio.load(renderComponent('label', params)); + + expect($('label').hasClass('ons-label')).toBe(expectedState); + }); + + it.each([ + ['`description` parameter is not provided', EXAMPLE_LABEL, false], + ['`description` parameter is provided', EXAMPLE_LABEL_WITH_DESCRIPTION, true], + ])('has the `ons-label--with-description` modifier class when %s', (_, params, expectedState) => { + const $ = cheerio.load(renderComponent('label', params)); - expect($('.ons-label__description').hasClass('ons-input--with-description')).toBe(true); + expect($('.ons-label').hasClass('ons-label--with-description')).toBe(expectedState); }); it.each([ - ['checkbox', 'ons-checkbox__label--with-description'], - ['radio', 'ons-radio__label--with-description'], - ])('has an input-specific modifier class when "%s" `inputType` is provided', (inputType, expectedInputSpecificModifier) => { - const $ = cheerio.load( - renderComponent('label', { - ...EXAMPLE_LABEL_WITH_DESCRIPTION, - inputType, - }), - ); - - expect($('.ons-label__description').hasClass(expectedInputSpecificModifier)).toBe(true); - expect($('.ons-label__description').hasClass('ons-input--with-description')).toBe(false); + ['`accessiblePlaceholder` parameter is not provided', EXAMPLE_LABEL, false], + ['`accessiblePlaceholder` parameter is provided', EXAMPLE_LABEL_WITH_ACCESSIBLE_PLACEHOLDER, true], + ])('has the `ons-label--placeholder` modifier class when %s', (_, params, expectedState) => { + const $ = cheerio.load(renderComponent('label', params)); + + expect($('.ons-label').hasClass('ons-label--placeholder')).toBe(expectedState); }); - it.each([['checkbox'], ['radio']])('has the description in an `aria-hidden` element when "%s" `inputType` is provided', (inputType) => { - const $ = cheerio.load( - renderComponent('label', { - ...EXAMPLE_LABEL_WITH_DESCRIPTION, - inputType, - }), - ); + it('has additionally provided style classes', () => { + const $ = cheerio.load( + renderComponent('label', { + ...EXAMPLE_LABEL, + classes: 'extra-class another-extra-class', + }), + ); - expect($('.ons-label__aria-hidden-description').attr('aria-hidden')).toBe('true'); + expect($('.ons-label').hasClass('extra-class')).toBe(true); + expect($('.ons-label').hasClass('another-extra-class')).toBe(true); }); - it.each([['checkbox'], ['radio']])( - 'has a duplicate description in a visually hidden element when "%s" `inputType` is provided', - (inputType) => { + it('has additionally provided `attributes`', () => { const $ = cheerio.load( - renderComponent('label', { - ...EXAMPLE_LABEL_WITH_DESCRIPTION, - inputType, - }), + renderComponent('label', { + ...EXAMPLE_LABEL, + attributes: { + a: 123, + b: 456, + }, + }), ); - expect($('.ons-label__visually-hidden-description').hasClass('ons-u-vh')).toBe(true); - }, - ); - }); + expect($('.ons-label').attr('a')).toBe('123'); + expect($('.ons-label').attr('b')).toBe('456'); + }); + + it.each([ + ['when without a description', EXAMPLE_LABEL], + ['when with a description', EXAMPLE_LABEL_WITH_DESCRIPTION], + ])('has the provided `text` %s', (_, params) => { + const $ = cheerio.load(renderComponent('label', params)); + + expect($('.ons-label').text().trim().startsWith('Example label text')).toBe(true); + }); + + describe('description element', () => { + it.each([ + ['when the `inputType` parameter is not provided', EXAMPLE_LABEL_WITH_DESCRIPTION], + [ + 'when the "checkbox" `inputType` parameter is provided', + { + ...EXAMPLE_LABEL_WITH_DESCRIPTION, + inputType: 'checkbox', + }, + ], + [ + 'when the "radio" `inputType` parameter is provided', + { + ...EXAMPLE_LABEL_WITH_DESCRIPTION, + inputType: 'radio', + }, + ], + ])('has the provided `description` text %s', (_, params) => { + const $ = cheerio.load(renderComponent('label', params)); + + expect($('.ons-label__description').text().trim()).toBe('An example description'); + }); + + it('has a default `id` attribute of `description-hint`', () => { + const $ = cheerio.load( + renderComponent('label', { + ...EXAMPLE_LABEL_WITH_DESCRIPTION, + id: undefined, + }), + ); + + expect($('.ons-label__description').attr('id')).toBe('description-hint'); + }); + + it('has an `id` attribute which adds `-description-hint` to the provided `id`', () => { + const $ = cheerio.load(renderComponent('label', EXAMPLE_LABEL_WITH_DESCRIPTION)); + + expect($('.ons-label__description').attr('id')).toBe('example-label-description-hint'); + }); + + it('has the modifier class `ons-input--with-description` when `inputType` is not provided', () => { + const $ = cheerio.load(renderComponent('label', EXAMPLE_LABEL_WITH_DESCRIPTION)); + + expect($('.ons-label__description').hasClass('ons-input--with-description')).toBe(true); + }); + + it.each([ + ['checkbox', 'ons-checkbox__label--with-description'], + ['radio', 'ons-radio__label--with-description'], + ])('has an input-specific modifier class when "%s" `inputType` is provided', (inputType, expectedInputSpecificModifier) => { + const $ = cheerio.load( + renderComponent('label', { + ...EXAMPLE_LABEL_WITH_DESCRIPTION, + inputType, + }), + ); + + expect($('.ons-label__description').hasClass(expectedInputSpecificModifier)).toBe(true); + expect($('.ons-label__description').hasClass('ons-input--with-description')).toBe(false); + }); + + it.each([['checkbox'], ['radio']])( + 'has the description in an `aria-hidden` element when "%s" `inputType` is provided', + (inputType) => { + const $ = cheerio.load( + renderComponent('label', { + ...EXAMPLE_LABEL_WITH_DESCRIPTION, + inputType, + }), + ); + + expect($('.ons-label__aria-hidden-description').attr('aria-hidden')).toBe('true'); + }, + ); + + it.each([['checkbox'], ['radio']])( + 'has a duplicate description in a visually hidden element when "%s" `inputType` is provided', + (inputType) => { + const $ = cheerio.load( + renderComponent('label', { + ...EXAMPLE_LABEL_WITH_DESCRIPTION, + inputType, + }), + ); + + expect($('.ons-label__visually-hidden-description').hasClass('ons-u-vh')).toBe(true); + }, + ); + }); }); diff --git a/src/components/language-selector/_macro.spec.js b/src/components/language-selector/_macro.spec.js index a2e2640ef0..322c05ee3b 100644 --- a/src/components/language-selector/_macro.spec.js +++ b/src/components/language-selector/_macro.spec.js @@ -6,132 +6,132 @@ import axe from '../../tests/helpers/axe'; import { renderComponent } from '../../tests/helpers/rendering'; const EXAMPLE_WITH_TWO_LANGUAGES = { - languages: [ - { - url: '/english', - ISOCode: 'en', - text: 'English', - abbrText: 'EN', - current: true, - }, - { - url: '/cymraeg', - ISOCode: 'cy', - text: 'Cymraeg', - abbrText: 'CY', - current: false, - attributes: { - a: 123, - b: 456, - }, - }, - ], + languages: [ + { + url: '/english', + ISOCode: 'en', + text: 'English', + abbrText: 'EN', + current: true, + }, + { + url: '/cymraeg', + ISOCode: 'cy', + text: 'Cymraeg', + abbrText: 'CY', + current: false, + attributes: { + a: 123, + b: 456, + }, + }, + ], }; const EXAMPLE_WITH_THREE_LANGUAGES = { - languages: [ - { - url: '/english', - ISOCode: 'en', - text: 'English', - current: false, - }, - { - url: '/cymraeg', - ISOCode: 'cy', - text: 'Cymraeg', - current: true, - }, - { - url: '/polski', - ISOCode: 'pl', - text: 'Polski', - current: false, - }, - ], + languages: [ + { + url: '/english', + ISOCode: 'en', + text: 'English', + current: false, + }, + { + url: '/cymraeg', + ISOCode: 'cy', + text: 'Cymraeg', + current: true, + }, + { + url: '/polski', + ISOCode: 'pl', + text: 'Polski', + current: false, + }, + ], }; describe('macro: language-selector', () => { - describe('mode: with two languages', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_TWO_LANGUAGES)); + describe('mode: with two languages', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_TWO_LANGUAGES)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - it('has one language selector displayed', () => { - const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_TWO_LANGUAGES)); + it('has one language selector displayed', () => { + const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_TWO_LANGUAGES)); - const $items = $('.ons-language-links__item'); - expect($items.length).toBe(1); - }); + const $items = $('.ons-language-links__item'); + expect($items.length).toBe(1); + }); - it('does not show the current language', () => { - const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_TWO_LANGUAGES)); + it('does not show the current language', () => { + const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_TWO_LANGUAGES)); - expect($('.ons-language-links__item a span:last-child').text()).toBe('Cymraeg'); - }); + expect($('.ons-language-links__item a span:last-child').text()).toBe('Cymraeg'); + }); - it('has the expected hyperlink URL', async () => { - const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_TWO_LANGUAGES)); + it('has the expected hyperlink URL', async () => { + const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_TWO_LANGUAGES)); - const $hyperlink = $('.ons-language-links__item a'); - expect($hyperlink.attr('href')).toBe('/cymraeg'); - }); + const $hyperlink = $('.ons-language-links__item a'); + expect($hyperlink.attr('href')).toBe('/cymraeg'); + }); - it('has the expected lang attribute value', async () => { - const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_TWO_LANGUAGES)); + it('has the expected lang attribute value', async () => { + const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_TWO_LANGUAGES)); - expect($('.ons-language-links__item a').attr('lang')).toBe('cy'); - }); + expect($('.ons-language-links__item a').attr('lang')).toBe('cy'); + }); - it('has additionally provided `attributes`', () => { - const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_TWO_LANGUAGES)); + it('has additionally provided `attributes`', () => { + const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_TWO_LANGUAGES)); - expect($('.ons-language-links__item a').attr('a')).toBe('123'); - expect($('.ons-language-links__item a').attr('b')).toBe('456'); - }); + expect($('.ons-language-links__item a').attr('a')).toBe('123'); + expect($('.ons-language-links__item a').attr('b')).toBe('456'); + }); - it('does not have a visibility class applied', () => { - const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_TWO_LANGUAGES)); + it('does not have a visibility class applied', () => { + const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_TWO_LANGUAGES)); - expect($('.ons-language-links').hasClass('ons-u-d-no@xxs@m')).toBe(false); - }); + expect($('.ons-language-links').hasClass('ons-u-d-no@xxs@m')).toBe(false); + }); - it('has the `abbrText` rendered', () => { - const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_TWO_LANGUAGES)); + it('has the `abbrText` rendered', () => { + const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_TWO_LANGUAGES)); - expect($('.ons-language-links__item a span:first-child').text()).toBe('CY'); + expect($('.ons-language-links__item a span:first-child').text()).toBe('CY'); + }); }); - }); - describe('mode: with three languages', () => { - it('passes jest-axe checks', async () => { - const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_THREE_LANGUAGES)); + describe('mode: with three languages', () => { + it('passes jest-axe checks', async () => { + const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_THREE_LANGUAGES)); - const results = await axe($.html()); - expect(results).toHaveNoViolations(); - }); + const results = await axe($.html()); + expect(results).toHaveNoViolations(); + }); - it('has two language selectors displayed', () => { - const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_THREE_LANGUAGES)); + it('has two language selectors displayed', () => { + const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_THREE_LANGUAGES)); - const $items = $('.ons-language-links__item'); - expect($items.length).toBe(2); - }); + const $items = $('.ons-language-links__item'); + expect($items.length).toBe(2); + }); - it('does not show the current language', () => { - const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_THREE_LANGUAGES)); + it('does not show the current language', () => { + const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_THREE_LANGUAGES)); - expect($('.ons-language-links__item:first-child a').text()).toBe('English'); - expect($('.ons-language-links__item:last-child a').text()).toBe('Polski'); - }); + expect($('.ons-language-links__item:first-child a').text()).toBe('English'); + expect($('.ons-language-links__item:last-child a').text()).toBe('Polski'); + }); - it('has the visibility class applied', () => { - const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_THREE_LANGUAGES)); + it('has the visibility class applied', () => { + const $ = cheerio.load(renderComponent('language-selector', EXAMPLE_WITH_THREE_LANGUAGES)); - expect($('.ons-language-links').hasClass('ons-u-d-no@xxs@m')).toBe(true); + expect($('.ons-language-links').hasClass('ons-u-d-no@xxs@m')).toBe(true); + }); }); - }); }); diff --git a/src/components/language-selector/language.scss b/src/components/language-selector/language.scss index 9ac468dfac..c76a2c691b 100644 --- a/src/components/language-selector/language.scss +++ b/src/components/language-selector/language.scss @@ -1,10 +1,10 @@ .ons-language-links { - list-style: none; - margin: 0; - padding: 0; + list-style: none; + margin: 0; + padding: 0; - &__item { - display: inline-block; - margin: 0 0 0 0.5rem; - } + &__item { + display: inline-block; + margin: 0 0 0 0.5rem; + } } diff --git a/src/components/list/_list.scss b/src/components/list/_list.scss index 7264b710f4..6ac1472f75 100644 --- a/src/components/list/_list.scss +++ b/src/components/list/_list.scss @@ -1,111 +1,111 @@ .ons-list { - margin: 0; - padding: 0 0 0 1.5rem; + margin: 0; + padding: 0 0 0 1.5rem; - &__item { - &:first-child { - margin-top: 0.5rem; - } - &:last-child { - margin-bottom: -0.5; + &__item { + &:first-child { + margin-top: 0.5rem; + } + &:last-child { + margin-bottom: -0.5; + } } - } - &--spacious { - .ons-list__item { - margin: 0 0 1.5rem; + &--spacious { + .ons-list__item { + margin: 0 0 1.5rem; + } } - } - &--bare { - list-style: none; - padding-left: 0; - } + &--bare { + list-style: none; + padding-left: 0; + } - &--dashed { - @extend .ons-list--bare; - .ons-list__item { - margin-left: 25px; - position: relative; - &::before { - border-top: 1px solid var(--ons-color-black); - content: ''; - left: 0; - margin-left: -25px; - position: absolute; - top: 14px; - width: 15px; - } + &--dashed { + @extend .ons-list--bare; + .ons-list__item { + margin-left: 25px; + position: relative; + &::before { + border-top: 1px solid var(--ons-color-black); + content: ''; + left: 0; + margin-left: -25px; + position: absolute; + top: 14px; + width: 15px; + } + } } - } - &__link { - display: inline-block; - margin-right: 1rem; - vertical-align: top; - white-space: normal; - } + &__link { + display: inline-block; + margin-right: 1rem; + vertical-align: top; + white-space: normal; + } - &__prefix { - margin-right: 0.5rem; - } + &__prefix { + margin-right: 0.5rem; + } - &__suffix { - margin-left: 0.5rem; - } + &__suffix { + margin-left: 0.5rem; + } - &__prefix, - &__suffix { - font-family: $font-mono; - } + &__prefix, + &__suffix { + font-family: $font-mono; + } - &--prefix &, - &--suffix &, - &--icons & { - &__item { - align-items: flex-start; - display: flex; + &--prefix &, + &--suffix &, + &--icons & { + &__item { + align-items: flex-start; + display: flex; + } } - } - &--social & { - &__item { - align-items: center; + &--social & { + &__item { + align-items: center; + } } - } - &--icons { - margin-bottom: 0; - .ons-list__item { - margin-top: 0; - margin-bottom: 0.5rem; + &--icons { + margin-bottom: 0; + .ons-list__item { + margin-top: 0; + margin-bottom: 0.5rem; + } } - } - &--languages { - margin-bottom: 0; - .ons-list__link { - margin-right: 0; + &--languages { + margin-bottom: 0; + .ons-list__link { + margin-right: 0; + } } - } - &--p { - padding: 0; - } + &--p { + padding: 0; + } } @include bp-suffix('ons-list--inline') { - &:not(.ons-list--icons) { - margin: 0 1rem 0 0; - .ons-list__item { - display: inline-block; - margin: 0 1rem 0 0; + &:not(.ons-list--icons) { + margin: 0 1rem 0 0; + .ons-list__item { + display: inline-block; + margin: 0 1rem 0 0; + } } - } - &.ons-list--icons { - align-items: center; - display: flex; - flex-wrap: wrap; - } + &.ons-list--icons { + align-items: center; + display: flex; + flex-wrap: wrap; + } } diff --git a/src/components/list/_macro.spec.js b/src/components/list/_macro.spec.js index c4647a4abd..75775f4e85 100644 --- a/src/components/list/_macro.spec.js +++ b/src/components/list/_macro.spec.js @@ -6,647 +6,647 @@ import axe from '../../tests/helpers/axe'; import { renderComponent, templateFaker } from '../../tests/helpers/rendering'; const EXAMPLE_LIST_TEXT_ITEMS = { - itemsList: [{ text: 'First item' }, { text: 'Second item' }], + itemsList: [{ text: 'First item' }, { text: 'Second item' }], }; const EXAMPLE_ITEM_TEXT = { - text: 'Text item with markup', + text: 'Text item with markup', }; const EXAMPLE_ITEM_ARTICLE_TITLE = { - title: 'Article title', + title: 'Article title', }; const EXAMPLE_ITEM_NAVIGATION_TITLE = { - title: 'Long article title', - navigationTitle: 'Nav friendly title', + title: 'Long article title', + navigationTitle: 'Nav friendly title', }; const EXAMPLE_NESTED_LIST_TEXT_ITEMS = { - itemsList: [ - { text: 'First item' }, - { text: 'Second item' }, - { text: 'Third item', itemsList: [{ text: 'First item' }, { text: 'Second item' }] }, - ], + itemsList: [ + { text: 'First item' }, + { text: 'Second item' }, + { text: 'Third item', itemsList: [{ text: 'First item' }, { text: 'Second item' }] }, + ], }; describe('macro: list', () => { - describe('list element', () => { - it('has `id` attribute when provided', () => { - const $ = cheerio.load( - renderComponent('list', { - ...EXAMPLE_LIST_TEXT_ITEMS, - id: 'example-id', - }), - ); - - expect($('.ons-list').attr('id')).toBe('example-id'); - }); + describe('list element', () => { + it('has `id` attribute when provided', () => { + const $ = cheerio.load( + renderComponent('list', { + ...EXAMPLE_LIST_TEXT_ITEMS, + id: 'example-id', + }), + ); + + expect($('.ons-list').attr('id')).toBe('example-id'); + }); - it('has additionally provided style classes', () => { - const $ = cheerio.load( - renderComponent('list', { - ...EXAMPLE_LIST_TEXT_ITEMS, - classes: 'extra-class another-extra-class', - }), - ); + it('has additionally provided style classes', () => { + const $ = cheerio.load( + renderComponent('list', { + ...EXAMPLE_LIST_TEXT_ITEMS, + classes: 'extra-class another-extra-class', + }), + ); - expect($('.ons-list').hasClass('extra-class')).toBe(true); - expect($('.ons-list').hasClass('another-extra-class')).toBe(true); - }); + expect($('.ons-list').hasClass('extra-class')).toBe(true); + expect($('.ons-list').hasClass('another-extra-class')).toBe(true); + }); - it('has provided variant style class when one variant is provided', () => { - const $ = cheerio.load( - renderComponent('list', { - ...EXAMPLE_LIST_TEXT_ITEMS, - variants: 'dashed', - }), - ); + it('has provided variant style class when one variant is provided', () => { + const $ = cheerio.load( + renderComponent('list', { + ...EXAMPLE_LIST_TEXT_ITEMS, + variants: 'dashed', + }), + ); - expect($('.ons-list').hasClass('ons-list--dashed')).toBe(true); - }); + expect($('.ons-list').hasClass('ons-list--dashed')).toBe(true); + }); - it('has provided variant style classes when multiple variants are provided', () => { - const $ = cheerio.load( - renderComponent('list', { - ...EXAMPLE_LIST_TEXT_ITEMS, - variants: ['dashed', 'inline'], - }), - ); + it('has provided variant style classes when multiple variants are provided', () => { + const $ = cheerio.load( + renderComponent('list', { + ...EXAMPLE_LIST_TEXT_ITEMS, + variants: ['dashed', 'inline'], + }), + ); - expect($('.ons-list').hasClass('ons-list--dashed')).toBe(true); - expect($('.ons-list').hasClass('ons-list--inline')).toBe(true); - }); + expect($('.ons-list').hasClass('ons-list--dashed')).toBe(true); + expect($('.ons-list').hasClass('ons-list--inline')).toBe(true); + }); - it('assumes the "bare" variant with a prefix modifier class when the first list item has a prefix', () => { - const $ = cheerio.load( - renderComponent('list', { - itemsList: [{ text: 'Item text', prefix: 'Abc' }], - }), - ); + it('assumes the "bare" variant with a prefix modifier class when the first list item has a prefix', () => { + const $ = cheerio.load( + renderComponent('list', { + itemsList: [{ text: 'Item text', prefix: 'Abc' }], + }), + ); - expect($('.ons-list').hasClass('ons-list--bare')).toBe(true); - expect($('.ons-list').hasClass('ons-list--prefix')).toBe(true); - }); + expect($('.ons-list').hasClass('ons-list--bare')).toBe(true); + expect($('.ons-list').hasClass('ons-list--prefix')).toBe(true); + }); - it('assumes the "bare" variant with a suffix modifier class when the first list item has a suffix', () => { - const $ = cheerio.load( - renderComponent('list', { - itemsList: [{ text: 'Item text', suffix: 'Abc' }], - }), - ); + it('assumes the "bare" variant with a suffix modifier class when the first list item has a suffix', () => { + const $ = cheerio.load( + renderComponent('list', { + itemsList: [{ text: 'Item text', suffix: 'Abc' }], + }), + ); - expect($('.ons-list').hasClass('ons-list--bare')).toBe(true); - expect($('.ons-list').hasClass('ons-list--suffix')).toBe(true); - }); + expect($('.ons-list').hasClass('ons-list--bare')).toBe(true); + expect($('.ons-list').hasClass('ons-list--suffix')).toBe(true); + }); - it('assumes the "bare" variant with a icons modifier class when the first list item has an icon', () => { - const $ = cheerio.load( - renderComponent('list', { - ...EXAMPLE_LIST_TEXT_ITEMS, - iconPosition: 'before', - iconType: 'check', - }), - ); - - expect($('.ons-list').hasClass('ons-list--bare')).toBe(true); - expect($('.ons-list').hasClass('ons-list--icons')).toBe(true); - }); + it('assumes the "bare" variant with a icons modifier class when the first list item has an icon', () => { + const $ = cheerio.load( + renderComponent('list', { + ...EXAMPLE_LIST_TEXT_ITEMS, + iconPosition: 'before', + iconType: 'check', + }), + ); + + expect($('.ons-list').hasClass('ons-list--bare')).toBe(true); + expect($('.ons-list').hasClass('ons-list--icons')).toBe(true); + }); - it('renders a