diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 0000000000..4e0bb2991c --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,2 @@ +[node] +node 22 diff --git a/.editorconfig b/.editorconfig index 3aa7b9513e..bf6be24e8e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -27,7 +27,7 @@ end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true -[*.html] +[*.njk] indent_size = 2 indent_style = space charset = utf-8 @@ -35,7 +35,7 @@ end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true -[*.njk] +[*.html] indent_size = 2 indent_style = space charset = utf-8 @@ -51,7 +51,7 @@ end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true -[*.js] +[*.{cjs,js,mjs}] indent_size = 2 indent_style = space charset = utf-8 @@ -92,7 +92,7 @@ end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true -[*.yml] +[*.{yml,yaml}] indent_size = 2 indent_style = space charset = utf-8 @@ -108,11 +108,16 @@ end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true -[**vendor**] -indent_size = -indent_style = -charset = -end_of_line = -insert_final_newline = -trim_trailing_whitespace = -max_line_length = \ No newline at end of file +[Gemfile] +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true + +[{**dist**,**vendor**}] +indent_size = unset +indent_style = unset +charset = unset +end_of_line = unset +insert_final_newline = unset +trim_trailing_whitespace = unset +max_line_length = unset diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..2feb7d01df --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,182 @@ +module.exports = { + extends: ['standard', 'prettier'], + ignorePatterns: [ + '**/dist/**', + '**/vendor/**', + + // Enable dotfile linting + '!.*', + 'node_modules', + 'node_modules/.*', + + // Prevent CHANGELOG history changes + 'CHANGELOG.md' + ], + overrides: [ + { + extends: [ + 'eslint:recommended', + 'plugin:import/recommended', + 'plugin:jest/style', + 'plugin:jest-dom/recommended', + 'plugin:jsdoc/recommended-typescript-flavor', + 'plugin:n/recommended', + 'plugin:promise/recommended', + 'prettier' + ], + files: [ + '**/*.{cjs,js,mjs}', + + // Check markdown `*.md` contains valid code blocks + // https://github.com/eslint/eslint-plugin-markdown#advanced-configuration + '**/*.md/*.{cjs,js,mjs}' + ], + parserOptions: { + ecmaVersion: 'latest' + }, + plugins: ['import', 'jsdoc', 'n', 'promise', 'jest', 'jest-dom'], + rules: { + // Check import or require statements are A-Z ordered + 'import/order': [ + 'error', + { + alphabetize: { order: 'asc' }, + 'newlines-between': 'always' + } + ], + + // Check for valid formatting + 'jsdoc/check-line-alignment': [ + 'warn', + 'never', + { + wrapIndent: ' ' + } + ], + + // JSDoc blocks can use `@preserve` to prevent removal + 'jsdoc/check-tag-names': [ + 'warn', + { + definedTags: ['preserve'] + } + ], + + // JSDoc blocks are optional by default + 'jsdoc/require-jsdoc': 'off', + + // JSDoc @param required in (optional) blocks but + // @param description is not necessary by default + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-param': 'error', + + // Require hyphens before param description + // Aligns with TSDoc style: https://tsdoc.org/pages/tags/param/ + 'jsdoc/require-hyphen-before-param-description': 'warn', + + // Maintain new line after description + 'jsdoc/tag-lines': [ + 'warn', + 'never', + { + startLines: 1 + } + ], + + // Ignore `govuk-frontend` exports as they require auto-generated files + 'import/no-unresolved': ['error', { ignore: ['govuk-frontend'] }], + 'n/no-missing-import': ['error', { allowModules: ['govuk-frontend'] }], + 'n/no-missing-require': ['error', { allowModules: ['govuk-frontend'] }], + + // Automatically use template strings + 'no-useless-concat': 'error', + 'prefer-template': 'error', + + // Flow control โ€“ avoid continue and else blocks after return statements + // in if statements + 'no-continue': 'error', + 'no-else-return': 'error', + + // Avoid hard to read multi assign statements + 'no-multi-assign': 'error' + }, + settings: { + jsdoc: { + // Allows us to use type declarations that exist in our dependencies + mode: 'typescript', + tagNamePreference: { + // TypeDoc doesn't understand '@abstract' but '@virtual' + abstract: 'virtual' + } + } + } + }, + { + // Extensions required for ESM import + files: ['**/*.mjs'], + rules: { + 'import/extensions': [ + 'error', + 'always', + { + ignorePackages: true, + pattern: { + cjs: 'always', + js: 'always', + mjs: 'always' + } + } + ] + } + }, + { + files: ['**/*.test.{cjs,js,mjs}'], + env: { + jest: true + } + }, + { + // Matches Puppeteer environment in jest.config.mjs + // to ignore unknown Jest Puppeteer globals + files: ['**/*.puppeteer.test.{js,mjs}'], + globals: { + page: 'readonly', + browser: 'readonly', + jestPuppeteer: 'readonly' + } + }, + { + // Add plugin for markdown `*.md` code blocks. Its config is in the new + // "flat" format, so we need to use the legacy config + extends: ['plugin:markdown/recommended-legacy'], + files: ['**/*.md'], + plugins: ['markdown'], + processor: 'markdown/markdown' + }, + { + files: [ + '**/coding-standards/component-options.md/*.{cjs,js,mjs}', + '**/coding-standards/js.md/*.{cjs,js,mjs}' + ], + env: { + browser: true + }, + rules: { + // Ignore unused example code + 'no-new': 'off', + 'no-undef': 'off', + 'no-unused-expressions': 'off', + 'no-unused-vars': 'off', + 'no-useless-constructor': 'off', + + // Ignore paths to example modules + 'import/no-unresolved': 'off', + 'n/no-missing-import': 'off' + } + } + ], + parserOptions: { + project: './tsconfig.json' + }, + root: true +} diff --git a/.gitattributes b/.gitattributes index 0494519389..a7630ea5eb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ +* text=auto eol=lf *.min.js diff=minjs *.min.css diff=mincss diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 9d3b6c4e4c..9a9dfbcb20 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,10 +1,9 @@ --- -name: "๐Ÿ› Bug report" +name: '๐Ÿ› Bug report' about: Report a bug or regression title: '' labels: "\U0001F41B bug, awaiting triage" assignees: '' - --- ## Description of the issue + ## Steps to reproduce the issue + ## Actual vs expected behaviour + ## Environment (where applicable) + - Operating system: diff --git a/.github/ISSUE_TEMPLATE/cycle-brief.md b/.github/ISSUE_TEMPLATE/cycle-brief.md new file mode 100644 index 0000000000..4627468e60 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/cycle-brief.md @@ -0,0 +1,28 @@ +--- +name: "Cycle brief template" +about: For internal use only โ€“ issue template for writing briefs at the beginning of each cycle +title: "" +labels: "epic" +assignees: "" +--- + +## Brief + + + +## Epic lead + +## Driving role(s) + +## Supporting roles + +## Needs awareness + +## Further detail + + + +```[tasklist] +### Tasks +- [ ] Add a draft title or issue reference here +``` diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 7cd7695cc7..2edef5a85b 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -1,10 +1,9 @@ --- -name: "โœจ Feature request" +name: 'โœจ Feature request' about: Suggest a new feature or idea title: '' -labels: "feature request, awaiting triage" +labels: 'feature request, awaiting triage' assignees: '' - --- ## Related component + ## Context + ## Alternatives + ## Additional information (if applicable) + diff --git a/.github/ISSUE_TEMPLATE/internal-story.md b/.github/ISSUE_TEMPLATE/internal-story.md index f124751f16..36b73c3255 100644 --- a/.github/ISSUE_TEMPLATE/internal-story.md +++ b/.github/ISSUE_TEMPLATE/internal-story.md @@ -1,10 +1,9 @@ --- -name: "Internal story template" +name: 'Internal story template' about: For internal use only title: '' -labels: "awaiting triage" +labels: 'awaiting triage' assignees: '' - --- + +## What + +Release a new version of GOV.UK Frontend with the latest changes we've built. + +## Why + +So people can use this new version + +## Who needs to work on this + +Technical writers, Developers + +## Who needs to review this + +Technical writers, Developers + +## Done when + +```[tasklist] +## Comms preparation +- [ ] Draft release notes +- [ ] Draft comms +``` + +```[tasklist] +## GOV.UK Frontend +- [ ] Follow [our process to release GOV.UK Frontend to npm and GitHub](https://github.com/alphagov/govuk-frontend/blob/main/docs/releasing/publishing.md) +``` + +```[tasklist] +## Design System site +- [ ] [Run dependabot](https://github.com/alphagov/govuk-design-system/network/updates/194539/jobs) to bump the version of `govuk-frontend` +- [ ] Update [the "What's new" section on the homepage](https://github.com/alphagov/govuk-design-system/blob/main/views/partials/_whats-new.njk) +- [ ] Update [the "Recently shipped" section of the roadmap](https://github.com/alphagov/govuk-design-system/blob/main/src/community/roadmap/index.md#recently-shipped) +``` + +```[tasklist] +## Frontend docs +- [ ] [Run dependabot](https://github.com/alphagov/govuk-frontend-docs/network/updates/4008122/jobs) to bump the version of `govuk-frontend` +``` + +```[tasklist] +## Comms +- [ ] Send comms on Slack channels +- [ ] Send comms to mailing list +``` + +```[tasklist] +## Logistics +- [ ] Ensure issues published in the release are gathered in a [GitHub milestone](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/about-milestones) +- [ ] Verify that decisions are appropriately documented (in code/issues/pull request comments or in separate documents linked from issues/pull requests) +- [ ] Update state of published issues on project board +- [ ] Close published issues when appropriate +- [ ] Open any follow-up issues if necessary +``` diff --git a/.github/ISSUE_TEMPLATE/spike.md b/.github/ISSUE_TEMPLATE/spike.md new file mode 100644 index 0000000000..403bb7e349 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/spike.md @@ -0,0 +1,50 @@ +--- +name: 'Spike' +about: For internal use only - issue template for spikes, research and investigation pieces +title: '' +labels: '๐Ÿ” investigation' +assignees: '' +--- + + + +## What + +## Why + +## Assumptions + + + +## Timebox + + + +We should review progress after this period of time has elapsed, even [if the spike has not been 'completed'](https://docs.google.com/document/d/17W_d0aYszgh7HTgnTHZLxXDu_TJyu8zzWt_lXH8VaHA/edit#heading=h.jd56itytegn6) + +## Who is working on this? + + + +Spike lead: + +Spike buddy: + +## Questions to answer + + + +- [ ] Questions go here... + +## Done when + + + +You may find it helpful to refer to our [expected outcomes of spikes](https://docs.google.com/document/d/17W_d0aYszgh7HTgnTHZLxXDu_TJyu8zzWt_lXH8VaHA/edit#heading=h.mmkqzigd11rs). + +- [ ] Questions have been answered or we have a clearer idea of how to get to our goal +- [ ] Findings have been reviewed and agreed with at least one other person +- [ ] Findings have been shared, e.g: via a write-up on the ticket, at a show & tell or team meeting diff --git a/.github/ISSUE_TEMPLATE/tech-debt.yaml b/.github/ISSUE_TEMPLATE/tech-debt.yaml new file mode 100644 index 0000000000..a3e1f3e063 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/tech-debt.yaml @@ -0,0 +1,62 @@ +name: Tech debt +description: For internal use only - Issue template for tracking technical debt +labels: [tech debt, awaiting triage] + +body: + - type: markdown + attributes: + value: | + This is a template for creating issues that describe technical debt. + + It is based off the [example in the GDS Way page on tracking technical debt](https://gds-way.cloudapps.digital/standards/technical-debt.html#example). + + - type: textarea + attributes: + label: Cause + validations: + required: true + + - type: textarea + attributes: + label: Consequences + validations: + required: true + + - type: dropdown + attributes: + label: Impact of debt + options: [Low, Medium, High] + validations: + required: true + + - type: textarea + attributes: + label: Reason (impact of debt) + validations: + required: false + + - type: dropdown + attributes: + label: Effort to pay down + options: [Low, Medium, High] + validations: + required: true + + - type: textarea + attributes: + label: Reason (effort to pay down) + validations: + required: false + + - type: dropdown + attributes: + label: Overall rating + options: [Low, Medium, High] + validations: + required: true + + - type: textarea + attributes: + label: Reason (overall rating) + validations: + required: false diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..04eb238241 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,139 @@ +version: 2 + +updates: + # Update npm packages + - package-ecosystem: npm + directory: / + open-pull-requests-limit: 10 + + # Group packages into shared PR + groups: + babel: + patterns: + - '@babel/*' + + lint: + patterns: + - '@typescript-eslint/*' + - 'editorconfig-checker' + - 'standard' + - 'prettier' + - 'stylelint' + - 'stylelint-*' + - 'typescript' + + percy: + patterns: + - '@percy/*' + + postcss: + patterns: + - 'autoprefixer' + - 'cssnano' + - 'cssnano-*' + - 'postcss' + - 'postcss-*' + + rollup: + patterns: + - '@rollup/*' + - 'rollup' + - 'rollup-*' + + test: + patterns: + - '@axe-core/*' + - '@jest/*' + - '@puppeteer/*' + - '@types/jest' + - '@types/jest-*' + - 'jest' + - 'jest-*' + - 'puppeteer' + - 'puppeteer-*' + + types: + patterns: + - '@actions/*' + - '@octokit/*' + - '@types/*' + + # Exclude packages in other groups + exclude-patterns: + - '@types/jest' + - '@types/jest-*' + ignore: + - dependency-name: 'eslint-*' + - dependency-name: 'eslint' + - dependency-name: '@typescript-eslint/*' + # iframe-resizer has switched to GPL licence in v5 + # so we need to avoid upgrading to their next major version + - dependency-name: 'iframe-resizer' + update-types: ['version-update:semver-major'] + + reviewers: + - alphagov/design-system-developers + + # Schedule run every Monday, local time + schedule: + interval: weekly + time: '10:30' + timezone: 'Europe/London' + + versioning-strategy: increase + + allow: + # Include direct package.json updates + - dependency-type: direct + + # Include indirect browser data updates + # https://caniuse.com + - dependency-name: caniuse-lite + + # Update GitHub Actions + - package-ecosystem: github-actions + directory: / + reviewers: + - alphagov/design-system-developers + + # Schedule run every Monday, local time + schedule: + interval: weekly + time: '10:30' + timezone: 'Europe/London' + + # Update GitHub Actions (Build) + - package-ecosystem: github-actions + directory: /.github/workflows/actions/build + reviewers: + - alphagov/design-system-developers + + # Schedule run every Monday, local time + schedule: + interval: weekly + time: '10:30' + timezone: 'Europe/London' + + # Update GitHub Actions (Install dependencies) + - package-ecosystem: github-actions + directory: /.github/workflows/actions/install-node + reviewers: + - alphagov/design-system-developers + + # Schedule run every Monday, local time + schedule: + interval: weekly + time: '10:30' + timezone: 'Europe/London' + + # Update GitHub Actions (Setup Node.js) + - package-ecosystem: github-actions + directory: /.github/workflows/actions/setup-node + reviewers: + - alphagov/design-system-developers + + # Schedule run every Monday, local time + schedule: + interval: weekly + time: '10:30' + timezone: 'Europe/London' diff --git a/.github/workflows/accessibility-bot.yml b/.github/workflows/accessibility-bot.yml new file mode 100644 index 0000000000..ec66a53ecc --- /dev/null +++ b/.github/workflows/accessibility-bot.yml @@ -0,0 +1,31 @@ +name: Accessibility bot + +on: + issues: + types: [opened, edited] + + pull_request: + types: [opened, edited] + + issue_comment: + types: [created, edited] + + discussion: + types: [created, edited] + + discussion_comment: + types: [created, edited] + +permissions: + issues: write + pull-requests: write + discussions: write + +jobs: + images: + name: Check images + runs-on: ubuntu-latest + + steps: + - name: Check images for alt text + uses: github/accessibility-alt-text-bot@v1.5.0 diff --git a/.github/workflows/actions/build/action.yml b/.github/workflows/actions/build/action.yml new file mode 100644 index 0000000000..bed28ce4b3 --- /dev/null +++ b/.github/workflows/actions/build/action.yml @@ -0,0 +1,29 @@ +name: Build + +runs: + using: composite + + steps: + - name: Cache build + uses: actions/cache@v4.2.0 + id: build-cache + + with: + # Use faster GNU tar for all runners + enableCrossOsArchive: true + + # Restore build cache (unless commit SHA changes) + key: build-${{ runner.os }}-${{ github.sha }} + path: | + packages/*/dist + shared/*/dist + + - name: Build + id: build + + # Skip build when weโ€™ve built this SHA before + if: steps.build-cache.outputs.cache-hit != 'true' + shell: bash + + # Build all workspaces + run: npm run build --workspaces --if-present diff --git a/.github/workflows/actions/install-node/action.yml b/.github/workflows/actions/install-node/action.yml new file mode 100644 index 0000000000..8563fcd92a --- /dev/null +++ b/.github/workflows/actions/install-node/action.yml @@ -0,0 +1,38 @@ +name: Install dependencies + +runs: + using: composite + + steps: + - name: Cache dependencies + uses: actions/cache@v4.2.0 + id: npm-install-cache + + with: + # Use faster GNU tar for all runners + enableCrossOsArchive: true + + # Restore `node_modules` cache (unless packages change) + key: npm-install-${{ runner.os }}-${{ hashFiles('package-lock.json', '**/package.json') }} + path: | + node_modules + .github/workflows/scripts/node_modules + docs/examples/*/node_modules + packages/*/node_modules + shared/*/node_modules + + - name: Setup Node.js + uses: ./.github/workflows/actions/setup-node + id: setup-node + + with: + # Restore global `~/.npm` cache (unless packages change) + use-cache: ${{ steps.npm-install-cache.outputs.cache-hit != 'true' }} + + - name: Install dependencies + id: install-node + + # Skip install when dependencies are cached + if: steps.npm-install-cache.outputs.cache-hit != 'true' + shell: bash + run: npm ci diff --git a/.github/workflows/actions/setup-node/action.yml b/.github/workflows/actions/setup-node/action.yml new file mode 100644 index 0000000000..8959fcbd8e --- /dev/null +++ b/.github/workflows/actions/setup-node/action.yml @@ -0,0 +1,19 @@ +name: Setup + +inputs: + use-cache: + description: Restore global `~/.npm` cache + default: 'true' + required: true + +runs: + using: composite + + steps: + - name: Setup Node.js + uses: actions/setup-node@v4.1.0 + id: setup-node + + with: + cache: ${{ inputs.use-cache == 'true' && 'npm' || '' }} + node-version-file: .nvmrc diff --git a/.github/workflows/bundler-integrations.yml b/.github/workflows/bundler-integrations.yml new file mode 100644 index 0000000000..40f28aabe9 --- /dev/null +++ b/.github/workflows/bundler-integrations.yml @@ -0,0 +1,51 @@ +name: Bundler integrations + +on: + workflow_call: + workflow_dispatch: + +jobs: + test-tree-shaking: + name: Test tree shaking + runs-on: ubuntu-latest + + env: + PUPPETEER_SKIP_DOWNLOAD: true + + strategy: + fail-fast: false + + matrix: + bundler: + - rollup + - webpack + - vite + + steps: + - name: Checkout + uses: actions/checkout@v4.2.2 + + - name: Restore dependencies + uses: ./.github/workflows/actions/install-node + + - name: Build GOV.UK Frontend + uses: ./.github/workflows/actions/build + + - name: Build with bundler + run: npm run ${{matrix.bundler}} -w @govuk-frontend/bundler-integrations + + # Check output for modules that should not be included + - name: Check absence of unused modules in `single-component.js` + working-directory: ./.github/workflows/bundler-integrations + run: | + ! grep "Accordion" dist/${{matrix.bundler}}/single-component.js -q + + - name: Check absence of unused utility functions in `single-component.js` + working-directory: ./.github/workflows/bundler-integrations + run: | + ! grep "getFragmentFromUrl" dist/${{matrix.bundler}}/single-component.js -q + + - name: Check presence of modules in `initAll.js` + working-directory: ./.github/workflows/bundler-integrations + run: | + grep "Accordion" dist/${{matrix.bundler}}/initAll.js -q diff --git a/.github/workflows/bundler-integrations/package.json b/.github/workflows/bundler-integrations/package.json new file mode 100644 index 0000000000..709c14993c --- /dev/null +++ b/.github/workflows/bundler-integrations/package.json @@ -0,0 +1,26 @@ +{ + "name": "@govuk-frontend/bundler-integrations", + "description": "Boilerplate to verify that GOV.UK Frontend works OK with main bundlers", + "private": true, + "scripts": { + "rollup": "npm run rollup:single-component && npm run rollup:initAll", + "rollup:single-component": "npm run rollup:cli -- -o dist/rollup/single-component.js ./src/single-component.mjs", + "rollup:initAll": "npm run rollup:cli -- -o dist/rollup/initAll.js ./src/initAll.mjs", + "rollup:cli": "rollup -c rollup.config.mjs", + "webpack": "webpack --mode production -o dist/webpack", + "vite": "cross-env ENTRY_NAME=single-component vite build && cross-env ENTRY_NAME=initAll vite build", + "clean": "del-cli dist", + "build:all": "concurrently \"npm run rollup\" \"npm run webpack\" \"npm run vite\" --names \"rollup,webpack,vite\" --prefix-colors \"red.dim,blue.dim,yellow.dim\"" + }, + "devDependencies": { + "@rollup/plugin-node-resolve": "^15.2.3", + "concurrently": "^8.2.2", + "cross-env": "^7.0.3", + "del-cli": "^5.1.0", + "govuk-frontend": "*", + "rollup": "^4.19.1", + "terser-webpack-plugin": "^5.3.10", + "vite": "^5.3.4", + "webpack": "^5.93.0" + } +} diff --git a/.github/workflows/bundler-integrations/rollup.config.mjs b/.github/workflows/bundler-integrations/rollup.config.mjs new file mode 100644 index 0000000000..6ad8acfef7 --- /dev/null +++ b/.github/workflows/bundler-integrations/rollup.config.mjs @@ -0,0 +1,6 @@ +import resolve from '@rollup/plugin-node-resolve' + +/** @type {import('rollup').RollupOptions} */ +export default { + plugins: [resolve()] +} diff --git a/.github/workflows/bundler-integrations/src/initAll.mjs b/.github/workflows/bundler-integrations/src/initAll.mjs new file mode 100644 index 0000000000..fed6187dd4 --- /dev/null +++ b/.github/workflows/bundler-integrations/src/initAll.mjs @@ -0,0 +1,4 @@ +// An example of importing and initialising allcomponents via initAll +import { initAll } from 'govuk-frontend' + +initAll() diff --git a/.github/workflows/bundler-integrations/src/single-component.mjs b/.github/workflows/bundler-integrations/src/single-component.mjs new file mode 100644 index 0000000000..cc535dff2e --- /dev/null +++ b/.github/workflows/bundler-integrations/src/single-component.mjs @@ -0,0 +1,9 @@ +// An example of importing a single component from govuk-frontend and initialising it +import { Button } from 'govuk-frontend' + +const $buttons = document.querySelectorAll('[data-module="govuk-button"]') + +$buttons.forEach(($button) => { + /* eslint-disable-next-line no-new */ + new Button($button) +}) diff --git a/.github/workflows/bundler-integrations/vite.config.mjs b/.github/workflows/bundler-integrations/vite.config.mjs new file mode 100644 index 0000000000..5934b4ed28 --- /dev/null +++ b/.github/workflows/bundler-integrations/vite.config.mjs @@ -0,0 +1,25 @@ +// Allows to run the same configuration against different files +// as Vite would split shared code in a separate chunk if trying +// to build an array of entries in `build.rollupOptions.input` +// or `build.lib.entry` +const entryName = process.env.ENTRY_NAME ?? 'single-component' + +/** @type {import('vite').UserConfig} */ +export default { + build: { + // Align output with other bundlers to facilitate testing + outDir: 'dist/vite', + assetsDir: '.', + // Prevent minification so we can see actual class/function names + minify: false, + // Vite will clean the build folder, but we'll have two concurrent builds + // (one for each entry) so we want to prevent that + emptyOutDir: false, + rollupOptions: { + input: `./src/${entryName}.mjs`, + output: { + entryFileNames: '[name].js' + } + } + } +} diff --git a/.github/workflows/bundler-integrations/webpack.config.js b/.github/workflows/bundler-integrations/webpack.config.js new file mode 100644 index 0000000000..00b025fd17 --- /dev/null +++ b/.github/workflows/bundler-integrations/webpack.config.js @@ -0,0 +1,35 @@ +const TerserPlugin = require('terser-webpack-plugin') + +/** @type {import('webpack').Configuration} */ +module.exports = { + // Enable production mode for Webpack, as tree-shaking is a combination of + // - `usedExports` including, but not exporting code `export`s that are not used + // - `TerserPlugin` clearing unused code, effectively clearing the unused exports + // + // More details: https://webpack.js.org/guides/tree-shaking/ + // (especially the end of 'Add a Utility', and 'Minify the Output') + mode: 'production', + optimization: { + minimizer: [ + new TerserPlugin({ + extractComments: false, + terserOptions: { + mangle: false, + // Reproduce Webpack's defaults + // https://webpack.js.org/configuration/optimization/#optimizationminimizer + compress: { + passes: 2 + } + } + }) + ] + }, + target: 'web', + entry: { + 'single-component': './src/single-component.mjs', + initAll: './src/initAll.mjs' + }, + output: { + filename: '[name].js' + } +} diff --git a/.github/workflows/diff-change-to-dist.yaml b/.github/workflows/diff-change-to-dist.yaml index c12735be9f..966eeb8467 100644 --- a/.github/workflows/diff-change-to-dist.yaml +++ b/.github/workflows/diff-change-to-dist.yaml @@ -1,49 +1,76 @@ -name: Diff changes to dist +name: Diff changes to GitHub release on: pull_request: paths: ['dist/**'] +permissions: + pull-requests: write + jobs: generate-diff: - name: Generate Diff + name: Generate diff runs-on: ubuntu-latest - # Abort if run from a fork, as token won't have write access to post comment - if: github.event.pull_request.head.repo.full_name == github.repository + + # Skip workflows other than PRs such as merges to `main` but + # also when token write permissions are unavailable on forks + if: ${{ github.event.pull_request && !github.event.pull_request.head.repo.fork }} + steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4.2.2 with: fetch-depth: 0 # Need to also checkout the base branch to compare - - uses: actions/setup-node@v1 + + - name: Install dependencies + uses: ./.github/workflows/actions/install-node + - name: Set up diff drivers run: | npm install -g js-beautify - git config diff.minjs.textconv js-beautify - git config diff.mincss.textconv js-beautify + git config diff.minjs.textconv "js-beautify --editorconfig --type js" + git config diff.mincss.textconv "js-beautify --editorconfig --type css" + - name: Generate diff - id: diff run: | - diff=$(git diff -M1 origin/$GITHUB_BASE_REF -- dist) - # Escape new lines - diff="${diff//'%'/'%25'}" - diff="${diff//$'\n'/'%0A'}" - diff="${diff//$'\r'/'%0D'}" - echo "::set-output name=diff::$diff" + # Using `origin/$GITHUB_BASE_REF` to avoid actually checking out the branch + # as all we need is to let Git diff the two references + bin/dist-diff.sh origin/$GITHUB_BASE_REF $GITHUB_WORKSPACE + + - name: Save GitHub release diffs + uses: actions/upload-artifact@v4.4.3 + with: + name: GitHub release diffs + path: .cache/diff/dist/*.diff + if-no-files-found: ignore + - name: Add comment to PR - uses: actions/github-script@v3 - env: - DIFF: ${{ steps.diff.outputs.diff }} + uses: actions/github-script@v7.0.1 with: - github-token: ${{secrets.GITHUB_TOKEN}} + github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const commentText = '## Changes to dist\n' + - '```diff\n' + - process.env.DIFF + - '\n```' - - github.issues.createComment({ - issue_number: context.payload.pull_request.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: commentText - }) + const { commentDiffs } = await import('${{ github.workspace }}/.github/workflows/scripts/comments.mjs') + + // PR information + const issueNumber = ${{ github.event.pull_request.number }} + const commit = '${{ github.event.pull_request.head.sha }}' + + const diffs = [ + { + path: '${{ github.workspace }}/.cache/diff/dist/js.diff', + titleText: 'JavaScript changes to GitHub release', + markerText: 'dist/js.diff' + }, + { + path: '${{ github.workspace }}/.cache/diff/dist/css.diff', + titleText: 'Stylesheets changes to GitHub release', + markerText: 'dist/css.diff' + }, + { + path: '${{ github.workspace }}/.cache/diff/dist/other.diff', + titleText: 'Other changes to GitHub release', + markerText: 'dist/other.diff', + skipEmpty: true + } + ] + + await commentDiffs({ github, context, commit }, issueNumber, diffs) diff --git a/.github/workflows/diff-change-to-package.yaml b/.github/workflows/diff-change-to-package.yaml new file mode 100644 index 0000000000..591733e5cb --- /dev/null +++ b/.github/workflows/diff-change-to-package.yaml @@ -0,0 +1,84 @@ +name: Diff changes to npm package + +on: + workflow_call: + workflow_dispatch: + +permissions: + pull-requests: write + +jobs: + generate-diff: + name: Generate diff + runs-on: ubuntu-latest + + # Skip workflows other than PRs such as merges to `main` but + # also when token write permissions are unavailable on forks + if: ${{ github.event.pull_request && !github.event.pull_request.head.repo.fork }} + + steps: + - uses: actions/checkout@v4.2.2 + with: + fetch-depth: 0 # Need to also checkout the base branch to compare + + - name: Install dependencies + uses: ./.github/workflows/actions/install-node + + - name: Set up diff drivers + run: | + npm install -g js-beautify + git config diff.minjs.textconv "js-beautify --editorconfig --type js" + git config diff.mincss.textconv "js-beautify --editorconfig --type css" + + - name: Generate diff + run: | + git config user.name github-actions + git config user.email github-actions@github.com + bin/package-diff.sh $GITHUB_BASE_REF $GITHUB_WORKSPACE + + - name: Save npm package diffs + uses: actions/upload-artifact@v4.4.3 + with: + name: Package diff + path: .cache/diff/package/*.diff + if-no-files-found: ignore + + - name: Add comment to PR + uses: actions/github-script@v7.0.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { commentDiffs } = await import('${{ github.workspace }}/.github/workflows/scripts/comments.mjs') + + // PR information + const issueNumber = ${{ github.event.pull_request.number }} + const commit = '${{ github.event.pull_request.head.sha }}' + + const diffs = [ + { + path: '${{ github.workspace }}/.cache/diff/package/js.diff', + titleText: 'JavaScript changes to npm package', + markerText: 'package/js.diff', + skipEmpty: true + }, + { + path: '${{ github.workspace }}/.cache/diff/package/css.diff', + titleText: 'Stylesheets changes to npm package', + markerText: 'package/css.diff', + skipEmpty: true + }, + { + path: '${{ github.workspace }}/.cache/diff/package/html.diff', + titleText: 'Rendered HTML changes to npm package', + markerText: 'package/html.diff', + skipEmpty: true + }, + { + path: '${{ github.workspace }}/.cache/diff/package/other.diff', + titleText: 'Other changes to npm package', + markerText: 'package/other.diff', + skipEmpty: true + } + ] + + await commentDiffs({ github, context, commit }, issueNumber, diffs) diff --git a/.github/workflows/sass.yaml b/.github/workflows/sass.yaml index 1cef8977d8..3881ec2c7e 100644 --- a/.github/workflows/sass.yaml +++ b/.github/workflows/sass.yaml @@ -1,43 +1,201 @@ name: Sass -on: [push, pull_request] +on: + pull_request: + + push: + branches: + - main + - 'feature/**' + - 'support/**' + + workflow_dispatch: + +concurrency: + group: sass-${{ github.head_ref || github.run_id }} + cancel-in-progress: true jobs: dart-sass: name: Dart Sass v1.0.0 runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - name: Checkout code + uses: actions/checkout@v4.2.2 + + - name: Setup Node.js + uses: actions/setup-node@v4.1.0 with: - node-version: 8 # v8 required for sass v1.0.0 - - run: | + cache: npm + node-version: 8 # Node.js 8 supported by Dart Sass v1.0.0 + + - name: Install package + run: | npm install -g sass@v1.0.0 sass --version - - run: time sass src/govuk/all.scss > /dev/null + - name: Run command + run: | + mkdir -p .tmp + time sass packages/govuk-frontend/src/govuk/index.scss > .tmp/index.css + + # Check output for uncompiled Sass + - name: Check output + run: | + ! grep "\$govuk-" .tmp/index.css + + dart-sass-latest: + name: Dart Sass v1 (latest) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4.2.2 + + - name: Setup Node.js + uses: actions/setup-node@v4.1.0 + with: + cache: npm + node-version-file: .nvmrc # Node.js project version must support Dart Sass v1 + + - name: Install package + run: | + npm install -g sass@v1 + sass --version + + # Treat GOV.UK Frontend as a dependency by importing it via load paths, + # allowing us to mimic the way we recommend our users silence deprecation + # warnings using the `quiet-deps` flag. + # + # Run the command through a shell to ensure `time` measures the time + # taken by the entire pipeline, as we are now piping input into `sass`. + - name: Run command + run: | + mkdir -p .tmp + time sh -c 'echo "@import "\""govuk/all"\"";" | sass --stdin --quiet-deps --load-path=packages/govuk-frontend/src > .tmp/index.css' + + # Check output for uncompiled Sass + - name: Check output + run: | + ! grep "\$govuk-" .tmp/index.css + + # Node Sass v5.0.0 = LibSass v3.5.5 lib-sass: - name: LibSass v3.3.0 (deprecated) + name: LibSass v3.5.5 (deprecated) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4.2.2 + + - name: Setup Node.js + uses: actions/setup-node@v4.1.0 + with: + cache: npm + # Node.js 15 is required for the stable support of ES Module + # used when installing `node-sass` + node-version: 15 + + - name: Install package + # Sass 5.0.0 is the first version that supports Python 3 + # and as such can be built on a GitHub Actions runner + run: | + npm install -g node-sass@v5.0.0 + node-sass --version + + - name: Run command + run: | + mkdir -p .tmp + time node-sass packages/govuk-frontend/src/govuk/index.scss > .tmp/index.css + + # Check output for uncompiled Sass + - name: Check output + run: | + ! grep "\$govuk-" .tmp/index.css + + # Node Sass v8.x = LibSass v3 latest + lib-sass-latest: + name: LibSass v3 (latest, deprecated) runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - name: Checkout code + uses: actions/checkout@v4.2.2 + + - name: Setup Node.js + uses: actions/setup-node@v4.1.0 with: - node-version: 4 # v4 required for node-sass v3.4.0 - - run: | - npm install -g node-sass@v3.4.0 + cache: npm + node-version-file: .nvmrc # Node.js project version must support Node Sass v8.x + + - name: Install package + run: | + npm install -g node-sass@v8 node-sass --version - - run: time node-sass src/govuk/all.scss > /dev/null + + - name: Run command + run: | + mkdir -p .tmp + time node-sass packages/govuk-frontend/src/govuk/index.scss > .tmp/index.css + + # Check output for uncompiled Sass + - name: Check output + run: | + ! grep "\$govuk-" .tmp/index.css ruby-sass: name: Ruby Sass v3.4.0 (deprecated) runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v2 - - uses: ruby/setup-ruby@v1 + - name: Checkout code + uses: actions/checkout@v4.2.2 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 with: - ruby-version: 2.1.9 # Oldest version available on ruby/setup-ruby - - run: | + ruby-version: 2.3 # Oldest version supported by Bundler v2 + + - name: Install gem + run: | gem install sass -v 3.4.0 sass --version - - run: time sass src/govuk/all.scss > /dev/null + + - name: Run command + run: | + mkdir -p .tmp + time sass packages/govuk-frontend/src/govuk/index.scss > .tmp/index.css + + # Check output for uncompiled Sass + - name: Check output + run: | + ! grep "\$govuk-" .tmp/index.css + + ruby-sass-latest: + name: Ruby Sass v3 (latest, deprecated) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4.2.2 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 # Ruby 3.2 supported by Ruby Sass v3 + + - name: Install gem + run: | + gem install sass -v '~> 3.0' + sass --version + + - name: Run command + run: | + mkdir -p .tmp + time sass packages/govuk-frontend/src/govuk/index.scss > .tmp/index.css + + # Check output for uncompiled Sass + - name: Check output + run: | + ! grep "\$govuk-" .tmp/index.css diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml new file mode 100644 index 0000000000..9ecac06fd3 --- /dev/null +++ b/.github/workflows/screenshots.yml @@ -0,0 +1,50 @@ +name: Percy screenshots + +on: + workflow_call: + workflow_dispatch: + +concurrency: + group: screenshots-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + screenshots: + name: Send screenshots + runs-on: ubuntu-latest + + env: + PERCY_POSTINSTALL_BROWSER: false + PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} + PUPPETEER_SKIP_DOWNLOAD: true + + # Skip when secrets are unavailable on forks + if: ${{ !github.event.pull_request.head.repo.fork }} + + steps: + - name: Check secrets + if: ${{ !env.PERCY_TOKEN }} + run: echo "::warning title=GitHub Actions secrets::Workflow requires 'PERCY_TOKEN' secret" + + - name: Checkout + uses: actions/checkout@v4.2.2 + + - name: Install dependencies + uses: ./.github/workflows/actions/install-node + + - name: Cache browser download + uses: actions/cache@v4.2.0 + with: + # Use faster GNU tar for all runners + enableCrossOsArchive: true + key: puppeteer-${{ runner.os }} + path: .cache/puppeteer + + - name: Build + uses: ./.github/workflows/actions/build + + - name: Start review app + run: npm start --workspace @govuk-frontend/review & + + - name: Send screenshots to Percy + run: npx --workspace @govuk-frontend/review percy exec -- npm run test:screenshots diff --git a/.github/workflows/scripts/comments.mjs b/.github/workflows/scripts/comments.mjs new file mode 100644 index 0000000000..800aa9168d --- /dev/null +++ b/.github/workflows/scripts/comments.mjs @@ -0,0 +1,339 @@ +import { readFile } from 'node:fs/promises' +import { basename, join, parse } from 'path' + +import { getFileSizes } from '@govuk-frontend/lib/files' +import { getStats, modulePaths } from '@govuk-frontend/stats' +import { outdent } from 'outdent' + +/** + * Posts the content of multiple diffs in parallel on the given GitHub issue + * + * @param {GithubActionContext} githubActionContext + * @param {number} issueNumber + * @param {DiffComment[]} diffs + */ +export async function commentDiffs(githubActionContext, issueNumber, diffs) { + const errors = [] + + // Run comments in order, but prevent errors stopping other comments + for (const diff of diffs) { + try { + await commentDiff(githubActionContext, issueNumber, diff) + } catch (error) { + const filename = basename(diff.path) + + // Defer errors until all comments are attempted + errors.push( + new Error(`Failed to post GitHub comment for ${filename}`, { + cause: error + }) + ) + } + } + + // Throw on any deferred errors above + if (errors.length) { + throw new Error('Failed to post GitHub comments', { + cause: errors + }) + } +} + +/** + * Posts the content of a diff as a comment on a GitHub issue + * + * @param {GithubActionContext} githubActionContext + * @param {number} issueNumber + * @param {DiffComment} diffComment + */ +export async function commentDiff( + githubActionContext, + issueNumber, + { path, titleText, markerText, skipEmpty } +) { + // Read diff from previous step + const diffText = await readFile(path, 'utf8') + + // Skip or delete comment for empty diff + if (!diffText && skipEmpty) { + console.log(`Skipping GitHub comment for ${basename(path)}`) + await deleteComment(githubActionContext, issueNumber, markerText) + return + } + + // Add or update comment on PR + try { + await comment(githubActionContext, issueNumber, { + markerText, + titleText, + + // Add a little note if the diff is empty + bodyText: diffText + ? `\`\`\`diff\n${diffText}\n\`\`\`` + : 'No diff changes found.' + }) + } catch { + await comment(githubActionContext, issueNumber, { + markerText, + titleText, + + // Unfortunately the best we can do here is a link to the "Artifacts" + // section as [the upload-artifact action doesn't provide the public + // URL](https://github.com/actions/upload-artifact/issues/50) :'( + bodyText: `The diff could not be posted as a comment. You can download it from the [workflow artifacts](${githubActionRunUrl( + githubActionContext.context + )}#artifacts).` + }) + } +} + +/** + * Generates comment for stats + * + * @param {GithubActionContext} githubActionContext + * @param {number} issueNumber + * @param {DiffComment} statComment + */ +export async function commentStats( + githubActionContext, + issueNumber, + { path, titleText, markerText } +) { + const reviewAppURL = getReviewAppUrl(issueNumber) + + const distPath = join(path, 'dist') + const packagePath = join(path, 'packages/govuk-frontend/dist/govuk') + + // File sizes + const fileSizeTitle = '### File sizes' + const fileSizeRows = [ + ...(await getFileSizes(join(distPath, '**/*.{css,js,mjs}'))), + ...(await getFileSizes(join(packagePath, '*.{css,js,mjs}'))) + ] + + const fileSizeHeaders = ['File', 'Size'] + const fileSizeTable = renderTable(fileSizeHeaders, fileSizeRows) + const fileSizeText = [fileSizeTitle, fileSizeTable].join('\n') + + // Module sizes + const modulesTitle = '### Modules' + const modulesRows = (await Promise.all(modulePaths.map(getStats))).map( + ([modulePath, moduleSize]) => { + const { base, dir, name } = parse(modulePath) + + const statsPath = `docs/stats/${dir}/${name}.html` + const statsURL = new URL(statsPath, reviewAppURL) + + return [`[${base}](${statsURL})`, moduleSize.bundled, moduleSize.minified] + } + ) + + const modulesHeaders = ['File', 'Size (bundled)', 'Size (minified)'] + const modulesTable = renderTable(modulesHeaders, modulesRows) + const modulesFooter = `[View stats and visualisations on the review app](${reviewAppURL})` + const modulesText = [modulesTitle, modulesTable, modulesFooter].join('\n') + + await comment(githubActionContext, issueNumber, { + markerText, + titleText, + bodyText: [fileSizeText, modulesText].join('\n') + }) +} + +/** + * @param {GithubActionContext} githubActionContext - GitHub Action context + * @param {number} issueNumber - The number of the issue/PR on which to post the comment + * @param {Comment} comment + */ +export async function comment( + { github, context, commit }, + issueNumber, + { titleText, markerText, bodyText } +) { + const { issues } = github.rest + + /** + * GitHub issue REST API parameters + * + * @satisfies {IssueCommentsListParams} + */ + const parameters = { + issue_number: issueNumber, + owner: context.repo.owner, + repo: context.repo.repo + } + + /** + * GitHub issue comment body + */ + const body = outdent` + + ## ${titleText} + + ${bodyText} + + --- + ${renderCommentFooter({ context, commit })} + ` + + /** + * Find GitHub issue comment with marker `` + */ + const comment = await getComment({ github, context }, issueNumber, markerText) + + /** + * Update GitHub issue comment (or create new) + */ + await (comment?.id + ? issues.updateComment({ ...parameters, body, comment_id: comment.id }) + : issues.createComment({ ...parameters, body })) +} + +/** + * @param {Pick} githubActionContext + * @returns {string} - The content for the footer + */ +function renderCommentFooter({ context, commit }) { + return `[Action run](${githubActionRunUrl(context)}) for ${commit}` +} + +/** + * Renders a GitHub Markdown table. + * + * @param {string[]} headers - An array containing the table headers. + * @param {string[][]} rows - An array of arrays containing the row data for the table. + * @returns {string} The GitHub Markdown table as a string. + */ +function renderTable(headers, rows) { + if (!rows.every((row) => row.length === headers.length)) { + throw new Error( + 'All rows must have the same number of elements as the headers.' + ) + } + + /** + * @example + * ```md + * | File | Size | + * ``` + */ + const headerRow = `| ${headers.join(' | ')} |` + + /** + * @example + * ```md + * | --- | --- | + * ``` + */ + const headerSeparator = `| ${Array(headers.length).fill('---').join(' | ')} |` + + /** + * @example + * ```md + * | packages/govuk-frontend/dist/example.mjs | 100 KiB | + * ``` + */ + const rowStrings = rows.map((row) => `| ${row.join(' | ')} |`) + + // Combine headers, header separator, and rows to form the table + return `${[headerRow, headerSeparator, ...rowStrings].join('\n')}\n` +} + +/** + * Generates a URL to the GitHub action run + * + * @param {import('@actions/github').context} context - The context of the GitHub action + * @returns {string} The URL to the "Artifacts" section of the given run + */ +function githubActionRunUrl(context) { + const { runId, repo } = context + + return `https://github.com/${repo.owner}/${repo.repo}/actions/runs/${runId}/attempts/${process.env.GITHUB_RUN_ATTEMPT}` +} + +/** + * Delete GitHub issue comment with marker `` + * + * @param {Pick} githubActionContext + * @param {number} issueNumber + * @param {Comment["markerText"]} markerText + */ +export async function deleteComment( + { github, context }, + issueNumber, + markerText +) { + const { issues } = github.rest + + // Find first match for marker + const comment = await getComment({ github, context }, issueNumber, markerText) + + // Delete comment + if (comment) { + await issues.deleteComment({ + issue_number: issueNumber, + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id + }) + } +} + +/** + * Find GitHub issue comment with marker `` + * + * @param {Pick} githubActionContext + * @param {number} issueNumber + * @param {Comment["markerText"]} markerText + * @returns {Promise} GitHub comment + */ +export async function getComment({ github, context }, issueNumber, markerText) { + const { issues } = github.rest + + // Find all GitHub issue comments + const comments = await github.paginate(issues.listComments, { + issue_number: issueNumber, + owner: context.repo.owner, + repo: context.repo.repo + }) + + // Find first match for marker + return comments.find(({ body }) => !!body?.includes(markerText)) +} + +/** + * @param {number} prNumber - The PR number + * @param {string} path - URL path + * @returns {URL} - The Review App preview URL + */ +function getReviewAppUrl(prNumber, path = '/') { + return new URL(path, `https://govuk-frontend-pr-${prNumber}.herokuapp.com`) +} + +/** + * @typedef {object} GithubActionContext + * @property {import('@octokit/rest').Octokit} github - The pre-authenticated Octokit provided by GitHub actions + * @property {import('@actions/github').context} context - The context of the GitHub action + * @property {string} commit - The SHA of the commit that triggered the action + */ + +/** + * @typedef {object} Comment + * @property {string} markerText - The marker to identify the comment + * @property {string} titleText - The title of the comment + * @property {string} bodyText - The body of the comment + */ + +/** + * @typedef {object} DiffComment + * @property {string} markerText - The marker to identify the comment + * @property {string} titleText - The title of the comment + * @property {string} path - The path of the file to post as a comment + * @property {boolean} [skipEmpty] - Whether to skip PR comments for empty diffs + */ + +/** + * @typedef {import('@octokit/plugin-rest-endpoint-methods').RestEndpointMethodTypes["issues"]} IssuesEndpoint + * @typedef {IssuesEndpoint["listComments"]["parameters"]} IssueCommentsListParams + * @typedef {IssuesEndpoint["getComment"]["response"]["data"]} IssueCommentData + */ diff --git a/.github/workflows/scripts/package.json b/.github/workflows/scripts/package.json new file mode 100644 index 0000000000..6f77445054 --- /dev/null +++ b/.github/workflows/scripts/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "name": "@govuk-frontend/workflow-scripts", + "description": "GOV.UK Frontend GitHub Actions workflow scripts", + "engines": { + "node": "^22.11.0", + "npm": "^10.1.0" + }, + "license": "MIT", + "devDependencies": { + "@govuk-frontend/lib": "*", + "@govuk-frontend/stats": "*", + "outdent": "^0.8.0" + } +} diff --git a/.github/workflows/scripts/tsconfig.json b/.github/workflows/scripts/tsconfig.json new file mode 100644 index 0000000000..6c669d0663 --- /dev/null +++ b/.github/workflows/scripts/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["**/*.mjs"], + "compilerOptions": { + "strict": true, + "types": ["@actions/github", "@octokit/rest", "node"] + } +} diff --git a/.github/workflows/stats-comment.yml b/.github/workflows/stats-comment.yml new file mode 100644 index 0000000000..2d8bacd385 --- /dev/null +++ b/.github/workflows/stats-comment.yml @@ -0,0 +1,45 @@ +name: Stats comment + +on: + workflow_call: + workflow_dispatch: + +jobs: + generate-stats: + name: Generate stats + runs-on: ubuntu-latest + + # Skip workflows other than PRs such as merges to `main` but + # also when token write permissions are unavailable on forks + if: ${{ github.event.pull_request && !github.event.pull_request.head.repo.fork }} + + steps: + - name: Checkout code + uses: actions/checkout@v4.2.2 + + - name: Restore dependencies + uses: ./.github/workflows/actions/install-node + + - name: Build npm package and GitHub release + run: | + npm run build:package --workspace govuk-frontend + npm run build:release --workspace govuk-frontend + + - name: Add comment to PR + uses: actions/github-script@v7.0.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { commentStats } = await import('${{ github.workspace }}/.github/workflows/scripts/comments.mjs') + + // PR information + const issueNumber = ${{ github.event.pull_request.number }} + const commit = '${{ github.event.pull_request.head.sha }}' + + const options = { + path: '${{ github.workspace }}', + titleText: ':clipboard: Stats', + markerText: 'stats' + } + + await commentStats({ github, context, commit }, issueNumber, options) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f62fef4192..9c203884bd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,26 +1,334 @@ name: Tests -on: [push, pull_request] +on: + pull_request: + + push: + branches: + - main + - 'feature/**' + - 'support/**' + + workflow_dispatch: + +concurrency: + group: tests-${{ github.head_ref || github.run_id }} + cancel-in-progress: true jobs: - run-tests: - name: Run tests - runs-on: ubuntu-latest + install: + name: Install + runs-on: ${{ matrix.runner }} + + env: + PUPPETEER_SKIP_DOWNLOAD: true + + strategy: + fail-fast: false + + matrix: + runner: + - ubuntu-latest + - windows-latest + steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4.2.2 + + - name: Install dependencies + uses: ./.github/workflows/actions/install-node + + build: + name: Build + runs-on: ${{ matrix.runner }} + needs: [install] + + strategy: + fail-fast: false + + matrix: + runner: + - ubuntu-latest + - windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4.2.2 + + - name: Restore dependencies + uses: ./.github/workflows/actions/install-node + + - name: Build + uses: ./.github/workflows/actions/build + + lint: + name: ${{ matrix.task.description }} (${{ matrix.runner }}) + runs-on: ${{ matrix.runner }} + needs: [install] + + env: + # Authorise GitHub API requests for EditorConfig checker binary + # https://www.npmjs.com/package/editorconfig-checker + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + strategy: + fail-fast: false + + matrix: + runner: + - ubuntu-latest + - windows-latest - - name: Read node version from .nvmrc - id: nvm - run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)" + task: + - description: Lint Sass + name: lint-scss + run: npm run lint:scss + cache: .cache/stylelint - - name: Setup node - uses: actions/setup-node@v1 + - description: Lint JavaScript + name: lint-js + run: npm run lint:js + cache: .cache/eslint + + - description: EditorConfig + name: lint-editorconfig + run: npm run lint:editorconfig + + - description: Prettier + name: lint-prettier + run: npm run lint:prettier + cache: .cache/prettier + + - description: TypeScript compiler + name: lint-types + run: npm run lint:types -- --incremental --pretty + cache: '**/*.tsbuildinfo' + + steps: + - name: Checkout + uses: actions/checkout@v4.2.2 + + - name: Restore dependencies + uses: ./.github/workflows/actions/install-node + + - name: Cache linter + if: ${{ matrix.task.cache }} + uses: actions/cache@v4.2.0 with: - node-version: "${{ steps.nvm.outputs.NVMRC }}" + # Use faster GNU tar for all runners + enableCrossOsArchive: true + key: ${{ matrix.task.name }}-${{ runner.os }} + path: ${{ matrix.task.cache }} - - name: Install dependencies - run: npm ci + - name: Run lint task + run: ${{ matrix.task.run }} + + test: + name: ${{ matrix.task.description }} (${{ matrix.runner }}) + runs-on: ${{ matrix.runner }} + needs: [install, build] + + strategy: + fail-fast: false + + matrix: + runner: + - ubuntu-latest + - windows-latest + + task: + - description: Nunjucks macro tests + name: test-macro + cache: .cache/jest + projects: + - Nunjucks macro tests + + - description: JavaScript unit tests + name: test-unit + cache: .cache/jest + coverage: true + projects: + - JavaScript unit tests + + - description: JavaScript behaviour tests + name: test-behaviour + cache: .cache/jest + coverage: true + projects: + - JavaScript behaviour tests + + - description: JavaScript component tests + name: test-component + cache: | + .cache/jest + .cache/puppeteer + projects: + - JavaScript component tests + + - description: Accessibility tests + name: test-accessibility + cache: | + .cache/jest + .cache/puppeteer + projects: + - Accessibility tests + + steps: + - name: Checkout + uses: actions/checkout@v4.2.2 + + - name: Restore dependencies + uses: ./.github/workflows/actions/install-node + + - name: Restore build + uses: ./.github/workflows/actions/build + + - name: Cache task + if: ${{ matrix.task.cache }} + uses: actions/cache@v4.2.0 + with: + # Use faster GNU tar for all runners + enableCrossOsArchive: true + key: ${{ matrix.task.name }}-${{ runner.os }} + path: ${{ matrix.task.cache }} + + - name: Run test task + + # Use 2x CPU cores for hosted GitHub runners + # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources + run: npx jest --color ${{ format('--coverage={0} --maxWorkers=2 --selectProjects "{1}"', matrix.task.coverage || false, join(matrix.task.projects, '", "')) }} + + - name: Save test coverage + uses: actions/upload-artifact@v4.4.3 + with: + name: ${{ matrix.task.description }} coverage (${{ matrix.runner }}) + path: coverage + if-no-files-found: ignore + + verify: + name: ${{ matrix.task.description }} (${{ matrix.runner }}) + runs-on: ${{ matrix.runner }} + needs: [install, build] + + strategy: + fail-fast: false + + matrix: + runner: + - ubuntu-latest + - windows-latest + + task: + - description: Verify package build + name: test-build-package + run: npm run build:package + + - description: Verify distribution build + name: test-build-release + run: npm run build:release + + steps: + - name: Checkout + uses: actions/checkout@v4.2.2 + + - name: Restore dependencies + uses: ./.github/workflows/actions/install-node + + - name: Restore build + uses: ./.github/workflows/actions/build + + - name: Run verify task + run: ${{ matrix.task.run }} + + package: + name: Export ${{ matrix.conditions }}, Node.js ${{ matrix.node-version }} + runs-on: ubuntu-latest + needs: [install, build] + + strategy: + fail-fast: false + + matrix: + node-version: + - 12 # Node.js 12.20+ uses package exports with subpath patterns + - 18 # Node.js 17+ cannot use package exports with trailing slashes + - 20 + + conditions: + - require + - import + + include: + - conditions: require + node-version: 12.18 # Node.js 12.18 uses package exports with trailing slashes + + env: + # Node.js conditions override from "require" to "import" etc + # https://nodejs.org/api/cli.html#-c-condition---conditionscondition + FLAGS: ${{ matrix.conditions != 'require' && format(' --conditions {0}', matrix.conditions) || '' }} + + steps: + - name: Checkout + uses: actions/checkout@v4.2.2 + + - name: Restore build + uses: ./.github/workflows/actions/build + + - name: Change Node.js version + uses: actions/setup-node@v4.1.0 + with: + node-version: ${{ matrix.node-version }} + + - run: node --eval "console.log(require.resolve('govuk-frontend'))"${{ env.FLAGS }} + working-directory: packages/govuk-frontend + + - run: node --eval "console.log(require.resolve('govuk-frontend/package.json'))"${{ env.FLAGS }} + working-directory: packages/govuk-frontend + + - run: node --eval "console.log(require.resolve('govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js'))"${{ env.FLAGS }} + working-directory: packages/govuk-frontend + + - run: node --eval "console.log(require.resolve('govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs'))"${{ env.FLAGS }} + working-directory: packages/govuk-frontend + + - run: node --eval "console.log(require.resolve('govuk-frontend/dist/govuk/i18n.mjs'))"${{ env.FLAGS }} + working-directory: packages/govuk-frontend + + regression: + name: Percy + needs: [install, build] + + # Run existing "Percy screenshots" workflow + # (after install and build have been cached) + uses: ./.github/workflows/screenshots.yml + secrets: inherit + + generate-diff-package: + name: Diff changes to npm package + needs: [install] + + permissions: + pull-requests: write + + # Run existing "Diff changes to npm package" workflow + # (after only install has been cached) + uses: ./.github/workflows/diff-change-to-package.yaml + + generate-stats: + name: Stats comment + needs: [install] + + permissions: + pull-requests: write + + # Run existing "Stats comment" workflow + # (after only install has been cached) + uses: ./.github/workflows/stats-comment.yml + + bundler-integrations: + name: Bundler integrations + needs: [install, build] - - name: Run tests - run: npm test + # Run existing "Bundler integrations" workflow + # (after install and build have been cached) + uses: ./.github/workflows/bundler-integrations.yml diff --git a/.gitignore b/.gitignore index ef15ef2311..40e93e2b40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,25 @@ +# Temporary only +.cache/ +.tmp/ + +# Node.js modules node_modules/ -npm-debug.log + +# Test coverage +coverage/ + +# Build output +dist/ + +# Build output (committed) +!/dist + +# Files to ignore .DS_Store -preview/ -tmp/ -axeReports/ -public/ -package/package-lock.json -examples/**/package-lock.json +*.log +*.tsbuildinfo *.zip -sassdoc/ + +# Project lockfile only +package-lock.json +!/package-lock.json diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000..718da8a9c1 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx --no lint-staged diff --git a/.lintstagedrc.js b/.lintstagedrc.js new file mode 100644 index 0000000000..9fcd49eaa3 --- /dev/null +++ b/.lintstagedrc.js @@ -0,0 +1,48 @@ +const { ESLint } = require('eslint') + +const commands = { + // ESLint's configuration makes it ignore built files in `dist` or `packages/govuk-frontend/dist` + // that we want left alone, as well as the polyfills. + // The glob used by lint-staged to trigger the linting on commit isn't aware + // of that ignore list, so will ask ESLint to lint those files. + // This makes ESLint raise a warning for these files, which errors the linting + // because we use `--max-warnings 0`. + // To avoid that, we need to filter out files ignored by ESLint, + // as recommended by lint-staged. + // + // https://github.com/okonet/lint-staged#how-can-i-ignore-files-from-eslintignore + eslint: filterTask('npm run lint:js:cli -- --fix'), + prettier: 'npm run lint:prettier:cli -- --write', + stylelint: 'npm run lint:scss:cli -- --fix --allow-empty-input' +} + +module.exports = { + '*.{cjs,js,mjs}': [commands.eslint, commands.prettier], + '*.{json,yaml,yml}': commands.prettier, + '*.md': [commands.eslint, commands.stylelint, commands.prettier], + '*.scss': [commands.stylelint, commands.prettier] +} + +// Configure paths to ignore +const eslint = new ESLint() + +/** + * Removes files ignored by ESLint from a list of files provided by lint-staged + * + * @param {string} task - The task `lint-staged` wants to execute + * @returns {Promise<(paths: string[]) => string[]>} Tasks to run with files argument + */ +function filterTask(task) { + return async (files) => { + const isIgnored = await Promise.all( + files.map((file) => eslint.isPathIgnored(file)) + ) + + // Wrap files in quotes in case they contains a space + const paths = files + .filter((_, i) => !isIgnored[i]) + .map((file) => `"${file}"`) + + return paths.length ? [`${task} ${paths.join(' ')}`] : [] + } +} diff --git a/.nvmrc b/.nvmrc index 0fefb1351b..36bad18e44 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1,5 @@ -12.13.1 +22 +# When updating this version, remember to update: +# - the 'engines' fields in the various `package.json` files of the project +# - `package-lock.json` by running `npm install` +# - the `[node]` section of the various `.browserslistrc` files of the project diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..44c34c9b89 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,19 @@ +# Temporary only +.cache/ +.tmp/ + +# Node.js modules +node_modules/ + +# Test coverage +coverage/ + +# Build output +dist/ + +# 3rd party vendor code +vendor/ + +# Files to ignore +package-lock.json +govuk-prototype-kit.config.json diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000000..5cf69113b6 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,26 @@ +/** + * Prettier config + * + * @type {import('prettier').Config} + */ +module.exports = { + semi: false, + singleQuote: true, + trailingComma: 'none', + overrides: [ + { + files: '*.md', + options: { + embeddedLanguageFormatting: 'off', + singleQuote: false + } + }, + { + files: '*.scss', + options: { + printWidth: 120, + singleQuote: false + } + } + ] +} diff --git a/.stylelintrc.yml b/.stylelintrc.yml deleted file mode 100644 index 2de1a61298..0000000000 --- a/.stylelintrc.yml +++ /dev/null @@ -1,162 +0,0 @@ ---- -extends: stylelint-config-gds/scss -ignoreFiles: src/govuk/vendor/**/* -plugins: - - stylelint-order -rules: - # govuk-frontend has a specific ordering pattern that should be applied to - # rules - # - # https://github.com/hudochenkov/stylelint-order/blob/master/rules/properties-order/README.md - order/properties-order: - - content - - quotes - # Box-sizing - Allow here until global is decided - - box-sizing - - - display - - visibility - - - position - - z-index - - top - - right - - bottom - - left - - - width - - min-width - - max-width - - height - - min-height - - max-height - - - margin - - margin-top - - margin-right - - margin-bottom - - margin-left - - - padding - - padding-top - - padding-right - - padding-bottom - - padding-left - - - float - - clear - - - overflow - - overflow-x - - overflow-y - - - clip - - clip-path - - zoom - - resize - - - columns - - - table-layout - - empty-cells - - caption-side - - border-spacing - - border-collapse - - - list-style - - list-style-position - - list-style-type - - list-style-image - - - transform - - transition - - animation - - - border - - border-top - - border-right - - border-bottom - - border-left - - - border-width - - border-top-width - - border-right-width - - border-bottom-width - - border-left-width - - - border-style - - border-top-style - - border-right-style - - border-bottom-style - - border-left-style - - - border-radius - - border-top-left-radius - - border-top-right-radius - - border-bottom-left-radius - - border-bottom-right-radius - - - border-color - - border-top-color - - border-right-color - - border-bottom-color - - border-left-color - - - outline - - outline-color - - outline-offset - - outline-style - - outline-width - - - opacity - # Color has been moved to ensure it appears before background - - color - - background - - background-color - - background-image - - background-repeat - - background-position - - background-size - - box-shadow - - fill - - - font - - font-family - - font-size - - font-style - - font-variant - - font-weight - - - font-emphasize - - - letter-spacing - - line-height - - list-style - - word-spacing - - - text-align - - text-align-last - - text-decoration - - text-indent - - text-justify - - text-overflow - - text-overflow-ellipsis - - text-overflow-mode - - text-rendering - - text-outline - - text-shadow - - text-transform - - text-wrap - - word-wrap - - word-break - - - text-emphasis - - - vertical-align - - white-space - - word-spacing - - hyphens - - - src - - cursor - - -webkit-appearance diff --git a/CHANGELOG.md b/CHANGELOG.md index fd65b8a297..2152ae20fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,2388 @@ # Changelog +For advice on how to use these release notes see [our guidance on staying up to date with changes](https://frontend.design-system.service.gov.uk/staying-up-to-date/). + ## Unreleased +### Use our base configurable component to build your own configurable component + +We've added a `ConfigurableComponent` class to help you build your own configurable components. It extends our base component class and so it allows you to focus on your components' specific features by handling these shared behaviours across components: + +- checking that GOV.UK Frontend is supported +- checking that the component is not already initialised on its root element +- checking the type of the root element and storing it for access within the component as this.$root +- taking a configuration object as a parameter and then storing it for access within the component as this.config +- merging a passed configuration object with configuration options specified on the data attributes of the root element + +We introduced this change in: + +- [#5499: Rename GOVUKFrontendComponentConfigurable, export ConfigurableComponent](https://github.com/alphagov/govuk-frontend/pull/5499) +- [#5456: Refactor Accordion to extend from a GOVUKFrontendConfigurableComponent](https://github.com/alphagov/govuk-frontend/issues/5456) + +### Deprecated features + +#### Importing Sass using `govuk/all` + +You'll see a warning when compiling your Sass if you import all of GOV.UK Frontend's styling using `govuk/all`. Importing using the `all` file is deprecated, and weโ€™ll remove it in the next major release. + +In your import statements, use a trailing `/index` rather than `/all` to load GOV.UK Frontend's files: + +- `@import "govuk/index";` instead of `@import "govuk/all";`; + +You do not need `/index` at the end of each import path if youโ€™re using Dart Sass. + +This change was introduced in [pull request #5518: Deprecate `govuk/all.scss` and only reference `govuk/index.scss` internally](https://github.com/alphagov/govuk-frontend/pull/5518). + +### Fixes + +We've made fixes to GOV.UK Frontend in the following pull requests: + +- [#5533: Fix UMD files overriding existing global](https://github.com/alphagov/govuk-frontend/pull/5533) + +## v5.7.1 (Fix release) + +To install this version with npm, run `npm install govuk-frontend@5.7.1`. You can also find more information about [how to stay up to date](https://frontend.design-system.service.gov.uk/staying-up-to-date/#updating-to-the-latest-version) in our documentation. + +### Recommended changes + +#### Stop setting a `value` for File upload components + +The File upload component currently supports a `value` parameter, which populates the `value` HTML attribute of the input. + +However, since no modern browser supports passing a `value` to a file input, we've made the decision to remove this parameter. It has been deprecated and will be removed in a future version of GOV.UK Frontend. + +We introduced this change in [pull request #5330: Deprecate File upload component's `value` parameter](https://github.com/alphagov/govuk-frontend/pull/5330). + +### Fixes + +We've made fixes to GOV.UK Frontend in the following pull requests: + +- [#5396: Update various department brand colours](https://github.com/alphagov/govuk-frontend/pull/5396) + +## v5.7.0 (Feature release) + +To install this version with npm, run `npm install govuk-frontend@5.7.0`. You can also find more information about [how to stay up to date](https://frontend.design-system.service.gov.uk/staying-up-to-date/#updating-to-the-latest-version) in our documentation. + +### New features + +#### The Royal Arms has been updated + +The Royal Arms in the [GOV.UK footer](https://design-system.service.gov.uk/components/footer/) has been updated to reflect the version introduced by King Charles III. + +If your service does not use the image directly from the Frontend package, you should ensure the new image is being copied to your serviceโ€™s image assets folder. By default this folder is located at `/assets/images`. + +If youโ€™re using Nunjucks, the asset path may have been changed by the `assetPath` global variable or `assetsPath` parameter on the header component. + +Copy the `govuk-crest.svg` file from `/dist/assets/images` into your assets folder. + +You can safely delete the old image files, named `govuk-crest.png` and `govuk-crest-2x.png`. + +We introduced this change in [pull request #5376: Update the Royal Arms graphic in footer (v5.x)](https://github.com/alphagov/govuk-frontend/pull/5376). + +#### Components will not longer initialise twice on the same element + +GOV.UK Frontend components now throw an error if they've already been initialised on the DOM Element they're receiving for initialisation. +This prevents components from being initialised more than once and therefore not working properly. + +We introduced this change in [pull request #5272: Prevent multiple initialisations of a single component instance](https://github.com/alphagov/govuk-frontend/pull/5272) + +#### Respond to initialisation errors when using `createAll` and `initAll` + +We've added a new `onError` option for `createAll` and `initAll` that lets you respond to initialisation errors. +The functions will continue catching errors and initialising components further down the page if one component fails to initialise, +but this option will let you react to a component failing to initialise. For example, to allow reporting to an error monitoring service. + +We introduced this change in: + +- [pull request #5252: Add `onError` to `createAll`](https://github.com/alphagov/govuk-frontend/pull/5252) +- [pull request #5276: Add `onError` to `initAll`](https://github.com/alphagov/govuk-frontend/pull/5276) + +#### Check if GOV.UK Frontend is supported + +We've added the `isSupported` function to let you check if GOV.UK Frontend is supported in the browser running your script. +GOV.UK Frontend components have been checking this automatically since [the release of v5.0.0](https://github.com/alphagov/govuk-frontend/releases/v5.0.0), but you may want to use the `isSupported` function to avoid running some code when GOV.UK Frontend is not supported. + +We introduced this change in [pull request #5250: Add `isSupported` to `all.mjs`](https://github.com/alphagov/govuk-frontend/pull/5250) + +#### Use our base component to build your own components + +We've added a `Component` class to help you build your own components. It allows you to focus on your components' specific features by handling these shared behaviours across components: + +- checking that GOV.UK Frontend is supported +- checking that the component is not already initialised on its root element +- checking the type of the root element and storing it for access within the component as `this.$root` + +We introduced this change in: + +- [pull request #5350: Export a base `Component` class](https://github.com/alphagov/govuk-frontend/pull/5350). +- [pull request #5354: Refactor the root type check in `GOVUKFrontendComponent`](https://github.com/alphagov/govuk-frontend/pull/5354) + +#### New brand colour + +We've added a brand colour for the Serious Fraud Office in [pull request #5389](https://github.com/alphagov/govuk-frontend/pull/5389). + +### Fixes + +We've made fixes to GOV.UK Frontend in the following pull requests: + +- [#5278: Fix service navigation mobile toggle spacing](https://github.com/alphagov/govuk-frontend/pull/5278) +- [#5331: Fix Warning Text font weight when `` styles are reset](https://github.com/alphagov/govuk-frontend/pull/5331) +- [#5352: Only apply margin to details summary when open](https://github.com/alphagov/govuk-frontend/pull/5352) +- [#5089: Fix details expanded state not announced on iOS](https://github.com/alphagov/govuk-frontend/pull/5089) +- [#5332: Improve how licence description wraps in the footer](https://github.com/alphagov/govuk-frontend/pull/5332) + +## v5.6.0 (Feature release) + +To install this version with npm, run `npm install govuk-frontend@5.6.0`. You can also find more information about [how to stay up to date](https://frontend.design-system.service.gov.uk/staying-up-to-date/#updating-to-the-latest-version) in our documentation. + +### New features + +#### Make it easier to navigate complex services with the Service navigation component + +We've added a new [Service navigation component](https://design-system.service.gov.uk/components/service-navigation/) to help users to navigate services with multiple top-level sections. This replaces the navigation functions of the Header component, which will be deprecated in a future release of GOV.UK Frontend. + +This component includes some features we consider experimental. We intend to iterate these features in response to user feedback. These are: + +- moving the service name from the Header to the Service navigation +- providing slots for injecting custom HTML into specified locations within the component + +We introduced this change in [pull request #5206: Service navigation component](https://github.com/alphagov/govuk-frontend/pull/5206). + +## v5.5.0 (Feature release) + +To install this version with npm, run `npm install govuk-frontend@5.5.0`. You can also find more information about [how to stay up to date](https://frontend.design-system.service.gov.uk/staying-up-to-date/#updating-to-the-latest-version) in our documentation. + +### New features + +#### We've updated the list of organisations and brand colours included in Frontend + +We've overhauled the list of organisations and organisation brand colours that are shipped with GOV.UK Frontend. + +The previous list was outdated and had not kept up with changes to the machinery of government. Weโ€™ve updated the list to: + +- add all current government departments and their brand colours +- add variants of brand colours that meet a 4.5:1 contrast ratio against white, where required +- provide warnings if defunct organisations are still being referenced in your Sass code + +To enable these changes, set the feature flag variable `$govuk-new-organisation-colours` to `true` before you import GOV.UK Frontend in your Sass files: + +```scss +// application.scss +$govuk-new-organisation-colours: true; +@import "govuk-frontend/all"; +``` + +You can also silence warnings about defunct organisations by adding `organisation-colours` to the [`$govuk-suppressed-warnings`](https://frontend.design-system.service.gov.uk/sass-api-reference/#govuk-suppressed-warnings) setting. + +We introduced this change in [pull request #3407: Update organisation colours](https://github.com/alphagov/govuk-frontend/pull/3407). + +#### Stop long words breaking out of components with `govuk-!-text-break-word` + +We've added a new override class to help display long words with no obvious break points when the space is too narrow to display them on one line. An example of a long word might be an email address entered by a user. + +Wrapping the content with the `govuk-!-text-break-word` class forces words that are too long for the parent element to break onto a new line. + +```html +A confirmation email will be sent to arthur_phillip_dent.42@peoplepersonalitydivision.siriuscyberneticscorporation.corp. +``` + +Sass users can also use the `govuk-text-break-word` mixin. + +We introduced this change in [pull request #5159: Add break-word typography helper](https://github.com/alphagov/govuk-frontend/pull/5159). + +### Recommended changes + +#### Update the `$websafe` parameter on the `govuk-organisation-colour` function + +The `govuk-organisation-colour` Sass function's `$websafe` parameter has been renamed to `$contrast-safe`. + +This is to more accurately describe the functionality of the parameter. + +The old parameter name will stop working in the next major version of GOV.UK Frontend. + +We introduced this change in [pull request #3407: Update organisation colours](https://github.com/alphagov/govuk-frontend/pull/3407). + +### Fixes + +We've made fixes to GOV.UK Frontend in the following pull requests: + +- [#5046: Skip โ€˜emptyโ€™ tasks in the task list](https://github.com/alphagov/govuk-frontend/pull/5046) +- [#5066: Fix whitespace affecting text alignment in pagination block variant](https://github.com/alphagov/govuk-frontend/pull/5066) +- [#5158: Remove โ†‘ up and โ†“ down arrow key bindings from tabs](https://github.com/alphagov/govuk-frontend/pull/5158) +- [#5191: Fix rendering of Back link's `href` and `text` for falsy values](https://github.com/alphagov/govuk-frontend/pull/5191) + +## 5.4.1 (Fix release) + +### Recommended changes + +#### Update Breadcrumbs to use `nav` and `aria-label` + +We've made changes to the Breadcrumbs component to improve how it appears to screen readers. + +We've changed the wrapping element to use the `nav` tag to expose it as a navigational landmark, and added an `aria-label` attribute to differentiate it as breadcrumb navigation. + +This change was introduced in [pull request #4995: Update Breadcrumb component to improve screen reader accessibility](https://github.com/alphagov/govuk-frontend/pull/4995). + +### Fixes + +We've made fixes to GOV.UK Frontend in the following pull requests: + +- [#5114: Fix divider width for small checkboxes](https://github.com/alphagov/govuk-frontend/pull/5114) โ€“ thanks to [@colinrotherham](https://github.com/colinrotherham) +- [#5043: Refactor the accordion JavaScript](https://github.com/alphagov/govuk-frontend/pull/5043) +- [#5044: Remove session storage checks from accordion JavaScript](https://github.com/alphagov/govuk-frontend/pull/5044) +- [#5060: Reintroduce additional bottom margin to Error Summary content](https://github.com/alphagov/govuk-frontend/pull/5060) +- [#5070: Fix alignment of content in conditional checkboxes and radio buttons](https://github.com/alphagov/govuk-frontend/pull/5070) +- [#5090: Remove redundant tag CSS from phase banner](https://github.com/alphagov/govuk-frontend/pull/5090) + +## 5.4.0 (Feature release) + +To install this version with npm, run `npm install govuk-frontend@5.4.0`. You can also find more information about [how to stay up to date](https://frontend.design-system.service.gov.uk/staying-up-to-date/#updating-to-the-latest-version) in our documentation. + +This release includes new features to help you include only the components your service uses. Doing this can help reduce the size of the JavaScript and CSS files sent to users, improving their experience. + +### New features + +#### Create individual components with `createAll` + +We've added a new `createAll` function that lets you initialise specific components in the same way that `initAll` does. + +The `createAll` function will: + +- find all elements in the page with the corresponding `data-module` attribute +- instantiate a component object for each element +- catch errors and log them in the console +- return an array of all the successfully instantiated component objects. + +```js +import { createAll, Button, Checkboxes } from 'govuk-frontend' + +createAll(Button) +createAll(Checkboxes) +``` + +You can also pass a config object and a scope within which to search for elements. + +You can find out more about [how to use the `createAll` function](https://frontend.design-system.service.gov.uk/import-javascript/#import-individual-components) in our documentation. + +This change was introduced in [pull request #4975: Add `createAll` function to initialise individual components](https://github.com/alphagov/govuk-frontend/pull/4975). + +#### Use tabular numbers easily with `govuk-!-font-tabular-numbers` + +We've added a new override class for tabular number styling: `govuk-!-font-tabular-numbers`. + +Using tabular numbers can make it easier for users to read numbers intended for comparison to one another, or for numbers that dynamically update. + +It was previously only possible to use tabular numbers by using the `govuk-font-tabular-numbers` Sass mixin. + +This change was introduced in [pull request #4973: Add override class for tabular numbers](https://github.com/alphagov/govuk-frontend/pull/4973). + +### Deprecated features + +#### Importing layers using `all` files + +You'll see a warning when compiling your Sass if you import any of our layers using the `all` file. Importing using the `all` files is deprecated, and weโ€™ll remove them in the next major release. + +In your import statements, use a trailing `/index` rather than `/all` to load GOV.UK Frontend's files. +For example: + +- `@import "govuk/index";` instead of `@import "govuk/all";`; +- `@import "govuk//index";` instead of `@import "govuk//all";`; + +You do not need `/index` at the end of each import path if youโ€™re using Dart Sass, LibSass 3.6.0 or higher, or Ruby Sass 3.6.0 or higher. + +This change was introduced in [pull request #4955: Rename `all` files to `index` for our Sass entry points](https://github.com/alphagov/govuk-frontend/pull/4955). + +### Fixes + +We've made fixes to GOV.UK Frontend in the following pull requests: + +- [#4942: Remove duplicate `errorMessage` argument for the password input component](https://github.com/alphagov/govuk-frontend/pull/4942) - thanks to [Tim South](https://github.com/tim-s-ccs) for contributing this change +- [#4961: Fix tree-shaking when importing `govuk-frontend`](https://github.com/alphagov/govuk-frontend/pull/4961) +- [#4963: Fix input value not being set if the value was '0'](https://github.com/alphagov/govuk-frontend/pull/4963) โ€“ thanks to [@dwp-dmitri-algazin](https://github.com/dwp-dmitri-algazin) for reporting this issue +- [#4971: Fix Error Summary component outputting list HTML when no `errorList` is provided](https://github.com/alphagov/govuk-frontend/pull/4971) +- [#442: Update content to streamline installation info](https://github.com/alphagov/govuk-frontend-docs/pull/442) +- [#438: Split up the 'Import CSS, assets and JavaScript' page](https://github.com/alphagov/govuk-frontend-docs/pull/438) + +## 5.3.1 (Fix release) + +To install this version with npm, run `npm install govuk-frontend@5.3.1`. You can also find more information about [how to stay up to date](https://frontend.design-system.service.gov.uk/staying-up-to-date/#updating-to-the-latest-version) in our documentation. + +### Fixes + +We've made fixes to GOV.UK Frontend in the following pull requests: + +- [#4906: Update the icon in the warning text component to match the defined text colour and background colour, rather than always being white on black](https://github.com/alphagov/govuk-frontend/pull/4906) +- [#4919: Use canvas colour for cookie banner over hardcoded grey](https://github.com/alphagov/govuk-frontend/pull/4919) +- [#4899: Remove indents from conditional reveals in radios and checkboxes](https://github.com/alphagov/govuk-frontend/pull/4899) +- [#4935: Fix password input button unexpectedly stretching](https://github.com/alphagov/govuk-frontend/pull/4935) +- [#4936: Fix skip link underline being removed when global styles are enabled](https://github.com/alphagov/govuk-frontend/pull/4936) +- [#4938: Fix `attributes` option ignoring values passed from the `safe` filter ](https://github.com/alphagov/govuk-frontend/pull/4938) + +## 5.3.0 (Feature release) + +To install this version with npm, run `npm install govuk-frontend@5.3.0`. You can also find more information about [how to stay up to date](https://frontend.design-system.service.gov.uk/staying-up-to-date/#updating-to-the-latest-version) in our documentation. + +### New features + +#### Use the Password input component to help users accessibly enter passwords + +The [Password input component](https://design-system.service.gov.uk/components/password-input/) allows users to choose: + +- whether their passwords are visible or not +- to enter their passwords in plain text + +This helps users use longer and more complex passwords without needing to remember what they've already typed. + +This change was introduced in [pull request #4442: Create password input component](https://github.com/alphagov/govuk-frontend/pull/4442). Thanks to [@andysellick](https://github.com/andysellick) for the original contribution. + +### Recommended changes + +#### Update the HTML for the Character count component + +We've updated the HTML for the Character count component. The component wrapper `data-module="govuk-character-count"` and its form group `class="govuk-form-group"` are now combined as the same `
`. The hint text used as the count message now appears directly after the `\n
\n\n
\n You can enter up to 10 characters\n
\n\n", - "hidden": false - }, - { - "name": "with hint", - "options": { - "name": "with-hint", - "id": "with-hint", - "maxlength": 10, - "label": { - "text": "Can you provide more detail?" - }, - "hint": { - "text": "Don't include personal or financial information, eg your National Insurance number or credit card details." - } - }, - "html": "
\n \n\n
\n \n\n \n \n
\n Don't include personal or financial information, eg your National Insurance number or credit card details.\n
\n\n\n \n
\n\n
\n You can enter up to 10 characters\n
\n\n
", - "hidden": false - }, - { - "name": "with default value", - "options": { - "id": "with-default-value", - "name": "default-value", - "maxlength": 100, - "label": { - "text": "Full address" - }, - "value": "221B Baker Street\nLondon\nNW1 6XE\n" - }, - "html": "
\n \n\n
\n \n\n\n \n
\n\n
\n You can enter up to 100 characters\n
\n\n
", - "hidden": false - }, - { - "name": "with default value exceeding limit", - "options": { - "id": "exceeding-characters", - "name": "exceeding", - "maxlength": 10, - "value": "221B Baker Street\nLondon\nNW1 6XE\n", - "label": { - "text": "Full address" - }, - "errorMessage": { - "text": "Please do not exceed the maximum allowed limit" - } - }, - "html": "
\n \n\n
\n \n\n\n \n \n \n Error: Please do not exceed the maximum allowed limit\n \n\n \n
\n\n
\n You can enter up to 10 characters\n
\n\n
", - "hidden": false - }, - { - "name": "with custom rows", - "options": { - "id": "custom-rows", - "name": "custom", - "maxlength": 10, - "label": { - "text": "Full address" - }, - "rows": 8 - }, - "html": "
\n \n\n
\n \n\n\n \n
\n\n
\n You can enter up to 10 characters\n
\n\n
", - "hidden": false - }, - { - "name": "with label as page heading", - "options": { - "id": "textarea-with-page-heading", - "name": "address", - "maxlength": 10, - "label": { - "text": "Full address", - "isPageHeading": true - } - }, - "html": "
\n \n\n
\n

\n \n

\n\n\n \n
\n\n
\n You can enter up to 10 characters\n
\n\n
", - "hidden": false - }, - { - "name": "with word count", - "options": { - "id": "word-count", - "name": "word-count", - "maxwords": 10, - "label": { - "text": "Full address" - } - }, - "html": "
\n \n\n
\n \n\n\n \n
\n\n
\n You can enter up to 10 words\n
\n\n
", - "hidden": false - }, - { - "name": "with threshold", - "options": { - "id": "with-threshold", - "name": "with-threshold", - "maxlength": 10, - "threshold": 75, - "label": { - "text": "Full address" - } - }, - "html": "
\n \n\n
\n \n\n\n \n
\n\n
\n You can enter up to 10 characters\n
\n\n
", - "hidden": false - }, - { - "name": "classes", - "options": { - "id": "with-classes", - "name": "with-classes", - "maxlength": 10, - "label": { - "text": "With classes" - }, - "classes": "app-character-count--custom-modifier" - }, - "html": "
\n \n\n
\n \n\n\n \n
\n\n
\n You can enter up to 10 characters\n
\n\n
", - "hidden": true - }, - { - "name": "attributes", - "options": { - "id": "with-attributes", - "name": "with-attributes", - "maxlength": 10, - "label": { - "text": "With attributes" - }, - "attributes": { - "data-attribute": "my data value" - } - }, - "html": "
\n \n\n
\n \n\n\n \n
\n\n
\n You can enter up to 10 characters\n
\n\n
", - "hidden": true - }, - { - "name": "formGroup with classes", - "options": { - "id": "with-formgroup", - "name": "with-formgroup", - "maxlength": 10, - "label": { - "text": "With formgroup" - }, - "formGroup": { - "classes": "app-character-count--custom-modifier" - } - }, - "html": "
\n \n\n
\n \n\n\n \n
\n\n
\n You can enter up to 10 characters\n
\n\n
", - "hidden": true - }, - { - "name": "custom classes on countMessage", - "options": { - "id": "with-custom-countmessage-class", - "name": "with-custom-countmessage-class", - "maxlength": 10, - "label": { - "text": "With custom countMessage class" - }, - "countMessage": { - "classes": "app-custom-count-message" - } - }, - "html": "
\n \n\n
\n \n\n\n \n
\n\n
\n You can enter up to 10 characters\n
\n\n
", - "hidden": true - }, - { - "name": "spellcheck enabled", - "options": { - "id": "with-spellcheck", - "name": "with-spellcheck", - "maxlength": 10, - "label": { - "text": "With spellcheck" - }, - "spellcheck": true - }, - "html": "
\n \n\n
\n \n\n\n \n
\n\n
\n You can enter up to 10 characters\n
\n\n
", - "hidden": true - }, - { - "name": "spellcheck disabled", - "options": { - "id": "without-spellcheck", - "name": "without-spellcheck", - "maxlength": 10, - "label": { - "text": "Without spellcheck" - }, - "spellcheck": false - }, - "html": "
\n \n\n
\n \n\n\n \n
\n\n
\n You can enter up to 10 characters\n
\n\n
", - "hidden": true - }, - { - "name": "custom classes with error message", - "options": { - "id": "with-custom-error-class", - "name": "with-custom-error-class", - "maxlength": 10, - "label": { - "text": "With custom error class" - }, - "classes": "app-character-count--custom-modifier", - "errorMessage": { - "text": "Error message" - } - }, - "html": "
\n \n\n
\n \n\n\n \n \n \n Error: Error message\n \n\n \n
\n\n
\n You can enter up to 10 characters\n
\n\n
", - "hidden": true - }, - { - "name": "with id starting with number", - "options": { - "name": "more-detail", - "id": "1_more-detail", - "maxlength": 10, - "label": { - "text": "Can you provide more detail?" - } - }, - "html": "
\n \n\n
\n \n\n\n \n
\n\n
\n You can enter up to 10 characters\n
\n\n
", - "hidden": true - } - ] -} \ No newline at end of file diff --git a/package/govuk/components/character-count/macro-options.json b/package/govuk/components/character-count/macro-options.json deleted file mode 100644 index e57df3276d..0000000000 --- a/package/govuk/components/character-count/macro-options.json +++ /dev/null @@ -1,111 +0,0 @@ -[ - { - "name": "id", - "type": "string", - "required": true, - "description": "The id of the textarea." - }, - { - "name": "name", - "type": "string", - "required": true, - "description": "The name of the textarea, which is submitted with the form data." - }, - { - "name": "rows", - "type": "string", - "required": false, - "description": "Optional number of textarea rows (default is 5 rows)." - }, - { - "name": "value", - "type": "string", - "required": false, - "description": "Optional initial value of the textarea." - }, - { - "name": "maxlength", - "type": "string", - "required": true, - "description": "If `maxwords` is set, this is not required. The maximum number of characters. If `maxwords` is provided, the `maxlength` argument will be ignored." - }, - { - "name": "maxwords", - "type": "string", - "required": true, - "description": "If `maxlength` is set, this is not required. The maximum number of words. If `maxwords` is provided, the `maxlength` argument will be ignored." - }, - { - "name": "threshold", - "type": "string", - "required": false, - "description": "The percentage value of the limit at which point the count message is displayed. If this attribute is set, the count message will be hidden by default." - }, - { - "name": "label", - "type": "object", - "required": true, - "description": "Options for the label component.", - "isComponent": true - }, - { - "name": "hint", - "type": "object", - "required": false, - "description": "Options for the hint component.", - "isComponent": true - }, - { - "name": "errorMessage", - "type": "object", - "required": false, - "description": "Options for the error message component. The error message component will not display if you use a falsy value for `errorMessage`, for example `false` or `null`.", - "isComponent": true - }, - { - "name": "formGroup", - "type": "object", - "required": false, - "description": "Options for the form-group wrapper", - "params": [ - { - "name": "classes", - "type": "string", - "required": false, - "description": "Classes to add to the form group (e.g. to show error state for the whole group)" - } - ] - }, - { - "name": "classes", - "type": "string", - "required": false, - "description": "Classes to add to the textarea." - }, - { - "name": "attributes", - "type": "object", - "required": false, - "description": "HTML attributes (for example data attributes) to add to the textarea." - }, - { - "name": "spellcheck", - "type": "boolean", - "required": false, - "description": "Optional field to enable or disable the spellcheck attribute on the character count." - }, - { - "name": "countMessage", - "type": "object", - "required": false, - "description": "Options for the count message", - "params": [ - { - "name": "classes", - "type": "string", - "required": false, - "description": "Classes to add to the count message" - } - ] - } -] \ No newline at end of file diff --git a/package/govuk/components/character-count/template.njk b/package/govuk/components/character-count/template.njk deleted file mode 100644 index d1eb46b1e7..0000000000 --- a/package/govuk/components/character-count/template.njk +++ /dev/null @@ -1,37 +0,0 @@ -{% from "../textarea/macro.njk" import govukTextarea %} -{% from "../hint/macro.njk" import govukHint %} - -
- {{ govukTextarea({ - id: params.id, - name: params.name, - describedBy: params.id + '-info', - rows: params.rows, - spellcheck: params.spellcheck, - value: params.value, - formGroup: params.formGroup, - classes: 'govuk-js-character-count' + (' govuk-textarea--error' if params.errorMessage) + (' ' + params.classes if params.classes), - label: { - html: params.label.html, - text: params.label.text, - classes: params.label.classes, - isPageHeading: params.label.isPageHeading, - attributes: params.label.attributes, - for: params.id - }, - hint: params.hint, - errorMessage: params.errorMessage, - attributes: params.attributes - }) }} - {{ govukHint({ - text: 'You can enter up to ' + (params.maxlength or params.maxwords) + (' words' if params.maxwords else ' characters'), - id: params.id + '-info', - classes: 'govuk-character-count__message' + (' ' + params.countMessage.classes if params.countMessage.classes), - attributes: { - 'aria-live': 'polite' - } - }) }} -
diff --git a/package/govuk/components/checkboxes/_index.scss b/package/govuk/components/checkboxes/_index.scss deleted file mode 100644 index ff8321bfe9..0000000000 --- a/package/govuk/components/checkboxes/_index.scss +++ /dev/null @@ -1,305 +0,0 @@ -@import "../error-message/index"; -@import "../fieldset/index"; -@import "../hint/index"; -@import "../label/index"; - -@include govuk-exports("govuk/component/checkboxes") { - - $govuk-touch-target-size: 44px; - $govuk-checkboxes-size: 40px; - $govuk-small-checkboxes-size: 24px; - $govuk-checkboxes-label-padding-left-right: govuk-spacing(3); - - .govuk-checkboxes__item { - @include govuk-font($size: 19); - - display: block; - position: relative; - - min-height: $govuk-checkboxes-size; - - margin-bottom: govuk-spacing(2); - padding-left: $govuk-checkboxes-size; - - clear: left; - } - - .govuk-checkboxes__item:last-child, - .govuk-checkboxes__item:last-of-type { - margin-bottom: 0; - } - - .govuk-checkboxes__input { - $input-offset: ($govuk-touch-target-size - $govuk-checkboxes-size) / 2; - - cursor: pointer; - - // IE8 doesnโ€™t support pseudo-elements, so we donโ€™t want to hide native - // elements there. - @include govuk-not-ie8 { - position: absolute; - - z-index: 1; - top: $input-offset * -1; - left: $input-offset * -1; - - width: $govuk-touch-target-size; - height: $govuk-touch-target-size; - margin: 0; - - opacity: 0; - } - - @include govuk-if-ie8 { - margin-top: 10px; - margin-right: $govuk-checkboxes-size / -2; - margin-left: $govuk-checkboxes-size / -2; - float: left; - - // add focus outline to input - &:focus { - outline: $govuk-focus-width solid $govuk-focus-colour; - } - } - } - - .govuk-checkboxes__label { - display: inline-block; - margin-bottom: 0; - padding: 8px $govuk-checkboxes-label-padding-left-right govuk-spacing(1); - cursor: pointer; - // remove 300ms pause on mobile - -ms-touch-action: manipulation; - touch-action: manipulation; - } - - // [ ] Check box - .govuk-checkboxes__label:before { - content: ""; - box-sizing: border-box; - position: absolute; - top: 0; - left: 0; - width: $govuk-checkboxes-size; - height: $govuk-checkboxes-size; - border: $govuk-border-width-form-element solid currentColor; - background: transparent; - } - - // โœ” Check mark - // - // The check mark is a box with a border on the left and bottom side (โ””โ”€โ”€), - // rotated 45 degrees - .govuk-checkboxes__label:after { - content: ""; - box-sizing: border-box; - - position: absolute; - top: 11px; - left: 9px; - width: 23px; - height: 12px; - - -webkit-transform: rotate(-45deg); - - -ms-transform: rotate(-45deg); - - transform: rotate(-45deg); - border: solid; - border-width: 0 0 5px 5px; - // Fix bug in IE11 caused by transform rotate (-45deg). - // See: alphagov/govuk_elements/issues/518 - border-top-color: transparent; - - opacity: 0; - - background: transparent; - } - - .govuk-checkboxes__hint { - display: block; - padding-right: $govuk-checkboxes-label-padding-left-right; - padding-left: $govuk-checkboxes-label-padding-left-right; - } - - // Focused state - .govuk-checkboxes__input:focus + .govuk-checkboxes__label:before { - border-width: 4px; - box-shadow: 0 0 0 $govuk-focus-width $govuk-focus-colour; - } - - // Selected state - .govuk-checkboxes__input:checked + .govuk-checkboxes__label:after { - opacity: 1; - } - - // Disabled state - .govuk-checkboxes__input:disabled, - .govuk-checkboxes__input:disabled + .govuk-checkboxes__label { - cursor: default; - } - - .govuk-checkboxes__input:disabled + .govuk-checkboxes__label { - opacity: .5; - } - - // ========================================================= - // Conditional reveals - // ========================================================= - - // The narrow border is used in the conditional reveals because the border has - // to be an even number in order to be centred under the 40px checkbox or radio. - $conditional-border-width: $govuk-border-width-narrow; - // Calculate the amount of padding needed to keep the border centered against the checkbox. - $conditional-border-padding: ($govuk-checkboxes-size / 2) - ($conditional-border-width / 2); - // Move the border centered with the checkbox - $conditional-margin-left: $conditional-border-padding; - // Move the contents of the conditional inline with the label - $conditional-padding-left: $conditional-border-padding + $govuk-checkboxes-label-padding-left-right; - - .govuk-checkboxes__conditional { - @include govuk-responsive-margin(4, "bottom"); - margin-left: $conditional-margin-left; - padding-left: $conditional-padding-left; - border-left: $conditional-border-width solid $govuk-border-colour; - - .js-enabled &--hidden { - display: none; - } - - & > :last-child { - margin-bottom: 0; - } - } - - // ========================================================= - // Small checkboxes - // ========================================================= - - .govuk-checkboxes--small { - - $input-offset: ($govuk-touch-target-size - $govuk-small-checkboxes-size) / 2; - $label-offset: $govuk-touch-target-size - $input-offset; - - .govuk-checkboxes__item { - @include govuk-clearfix; - min-height: 0; - margin-bottom: 0; - padding-left: $label-offset; - float: left; - } - - // Shift the touch target into the left margin so that the visible edge of - // the control is aligned - // - // โ”†What colours do you like? - // โ”Œโ”†โ”€โ”€โ”€โ” - // โ”‚โ”†[] โ”‚ Purple - // โ””โ”†โ–ฒโ”€โ”€โ”˜ - // โ–ฒโ”†โ””โ”€ Check box pseudo element, aligned with margin - // โ””โ”€โ”€โ”€ Touch target (invisible input), shifted into the margin - .govuk-checkboxes__input { - @include govuk-not-ie8 { - left: $input-offset * -1; - } - - @include govuk-if-ie8 { - margin-left: $govuk-small-checkboxes-size * -1; - } - } - - // Adjust the size and position of the label. - // - // Unlike larger checkboxes, we also have to float the label in order to - // 'shrink' it, preventing the hover state from kicking in across the full - // width of the parent element. - .govuk-checkboxes__label { - margin-top: -2px; - padding: 13px govuk-spacing(3) 13px 1px; - float: left; - - @include govuk-media-query($from: tablet) { - padding: 11px govuk-spacing(3) 10px 1px; - } - } - - // [ ] Check box - // - // Reduce the size of the check box [1], vertically center it within the - // touch target [2] - .govuk-checkboxes__label:before { - top: $input-offset - $govuk-border-width-form-element; // 2 - width: $govuk-small-checkboxes-size; // 1 - height: $govuk-small-checkboxes-size; // 1 - } - - // โœ” Check mark - // - // Reduce the size of the check mark and re-align within the checkbox - .govuk-checkboxes__label:after { - top: 15px; - left: 6px; - width: 12px; - height: 6.5px; - border-width: 0 0 3px 3px; - } - - // Fix position of hint with small checkboxes - // - // Do not use hints with small checkboxes โ€“ because they're within the input - // wrapper they trigger the hover state, but clicking them doesn't actually - // activate the control. - // - // (If you do use them, they won't look completely broken... but seriously, - // don't use them) - .govuk-checkboxes__hint { - padding: 0; - clear: both; - } - - // Align conditional reveals with small checkboxes - .govuk-checkboxes__conditional { - $margin-left: ($govuk-small-checkboxes-size / 2) - ($conditional-border-width / 2); - margin-left: $margin-left; - padding-left: $label-offset - ($margin-left + $conditional-border-width); - clear: both; - } - - // Hover state for small checkboxes. - // - // We use a hover state for small checkboxes because the touch target size - // is so much larger than their visible size, and so we need to provide - // feedback to the user as to which checkbox they will select when their - // cursor is outside of the visible area. - .govuk-checkboxes__item:hover .govuk-checkboxes__input:not(:disabled) + .govuk-checkboxes__label:before { - box-shadow: 0 0 0 $govuk-hover-width $govuk-hover-colour; - } - - // Because we've overridden the border-shadow provided by the focus state, - // we need to redefine that too. - // - // We use two box shadows, one that restores the original focus state [1] - // and another that then applies the hover state [2]. - .govuk-checkboxes__item:hover .govuk-checkboxes__input:focus + .govuk-checkboxes__label:before { - box-shadow: - 0 0 0 $govuk-focus-width $govuk-focus-colour, // 1 - 0 0 0 $govuk-hover-width $govuk-hover-colour; // 2 - } - - // For devices that explicitly don't support hover, don't provide a hover - // state (e.g. on touch devices like iOS). - // - // We can't use `@media (hover: hover)` because we wouldn't get the hover - // state in browsers that don't support `@media (hover)` (like Internet - // Explorer) โ€“ so we have to 'undo' the hover state instead. - @media (hover: none), (pointer: coarse) { - .govuk-checkboxes__item:hover .govuk-checkboxes__input:not(:disabled) + .govuk-checkboxes__label:before { - box-shadow: initial; - } - - .govuk-checkboxes__item:hover .govuk-checkboxes__input:focus + .govuk-checkboxes__label:before { - box-shadow: 0 0 0 $govuk-focus-width $govuk-focus-colour; - } - } - } -} diff --git a/package/govuk/components/checkboxes/checkboxes.js b/package/govuk/components/checkboxes/checkboxes.js deleted file mode 100644 index 035ff272d9..0000000000 --- a/package/govuk/components/checkboxes/checkboxes.js +++ /dev/null @@ -1,1134 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define('GOVUKFrontend', factory) : - (global.GOVUKFrontend = factory()); -}(this, (function () { 'use strict'; - -(function(undefined) { - -// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Object/defineProperty/detect.js -var detect = ( - // In IE8, defineProperty could only act on DOM elements, so full support - // for the feature requires the ability to set a property on an arbitrary object - 'defineProperty' in Object && (function() { - try { - var a = {}; - Object.defineProperty(a, 'test', {value:42}); - return true; - } catch(e) { - return false - } - }()) -); - -if (detect) return - -// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Object.defineProperty&flags=always -(function (nativeDefineProperty) { - - var supportsAccessors = Object.prototype.hasOwnProperty('__defineGetter__'); - var ERR_ACCESSORS_NOT_SUPPORTED = 'Getters & setters cannot be defined on this javascript engine'; - var ERR_VALUE_ACCESSORS = 'A property cannot both have accessors and be writable or have a value'; - - Object.defineProperty = function defineProperty(object, property, descriptor) { - - // Where native support exists, assume it - if (nativeDefineProperty && (object === window || object === document || object === Element.prototype || object instanceof Element)) { - return nativeDefineProperty(object, property, descriptor); - } - - if (object === null || !(object instanceof Object || typeof object === 'object')) { - throw new TypeError('Object.defineProperty called on non-object'); - } - - if (!(descriptor instanceof Object)) { - throw new TypeError('Property description must be an object'); - } - - var propertyString = String(property); - var hasValueOrWritable = 'value' in descriptor || 'writable' in descriptor; - var getterType = 'get' in descriptor && typeof descriptor.get; - var setterType = 'set' in descriptor && typeof descriptor.set; - - // handle descriptor.get - if (getterType) { - if (getterType !== 'function') { - throw new TypeError('Getter must be a function'); - } - if (!supportsAccessors) { - throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); - } - if (hasValueOrWritable) { - throw new TypeError(ERR_VALUE_ACCESSORS); - } - Object.__defineGetter__.call(object, propertyString, descriptor.get); - } else { - object[propertyString] = descriptor.value; - } - - // handle descriptor.set - if (setterType) { - if (setterType !== 'function') { - throw new TypeError('Setter must be a function'); - } - if (!supportsAccessors) { - throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); - } - if (hasValueOrWritable) { - throw new TypeError(ERR_VALUE_ACCESSORS); - } - Object.__defineSetter__.call(object, propertyString, descriptor.set); - } - - // OK to define value unconditionally - if a getter has been specified as well, an error would be thrown above - if ('value' in descriptor) { - object[propertyString] = descriptor.value; - } - - return object; - }; -}(Object.defineProperty)); -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - // Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Function/prototype/bind/detect.js - var detect = 'bind' in Function.prototype; - - if (detect) return - - // Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Function.prototype.bind&flags=always - Object.defineProperty(Function.prototype, 'bind', { - value: function bind(that) { // .length is 1 - // add necessary es5-shim utilities - var $Array = Array; - var $Object = Object; - var ObjectPrototype = $Object.prototype; - var ArrayPrototype = $Array.prototype; - var Empty = function Empty() {}; - var to_string = ObjectPrototype.toString; - var hasToStringTag = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'; - var isCallable; /* inlined from https://npmjs.com/is-callable */ var fnToStr = Function.prototype.toString, tryFunctionObject = function tryFunctionObject(value) { try { fnToStr.call(value); return true; } catch (e) { return false; } }, fnClass = '[object Function]', genClass = '[object GeneratorFunction]'; isCallable = function isCallable(value) { if (typeof value !== 'function') { return false; } if (hasToStringTag) { return tryFunctionObject(value); } var strClass = to_string.call(value); return strClass === fnClass || strClass === genClass; }; - var array_slice = ArrayPrototype.slice; - var array_concat = ArrayPrototype.concat; - var array_push = ArrayPrototype.push; - var max = Math.max; - // /add necessary es5-shim utilities - - // 1. Let Target be the this value. - var target = this; - // 2. If IsCallable(Target) is false, throw a TypeError exception. - if (!isCallable(target)) { - throw new TypeError('Function.prototype.bind called on incompatible ' + target); - } - // 3. Let A be a new (possibly empty) internal list of all of the - // argument values provided after thisArg (arg1, arg2 etc), in order. - // XXX slicedArgs will stand in for "A" if used - var args = array_slice.call(arguments, 1); // for normal call - // 4. Let F be a new native ECMAScript object. - // 11. Set the [[Prototype]] internal property of F to the standard - // built-in Function prototype object as specified in 15.3.3.1. - // 12. Set the [[Call]] internal property of F as described in - // 15.3.4.5.1. - // 13. Set the [[Construct]] internal property of F as described in - // 15.3.4.5.2. - // 14. Set the [[HasInstance]] internal property of F as described in - // 15.3.4.5.3. - var bound; - var binder = function () { - - if (this instanceof bound) { - // 15.3.4.5.2 [[Construct]] - // When the [[Construct]] internal method of a function object, - // F that was created using the bind function is called with a - // list of arguments ExtraArgs, the following steps are taken: - // 1. Let target be the value of F's [[TargetFunction]] - // internal property. - // 2. If target has no [[Construct]] internal method, a - // TypeError exception is thrown. - // 3. Let boundArgs be the value of F's [[BoundArgs]] internal - // property. - // 4. Let args be a new list containing the same values as the - // list boundArgs in the same order followed by the same - // values as the list ExtraArgs in the same order. - // 5. Return the result of calling the [[Construct]] internal - // method of target providing args as the arguments. - - var result = target.apply( - this, - array_concat.call(args, array_slice.call(arguments)) - ); - if ($Object(result) === result) { - return result; - } - return this; - - } else { - // 15.3.4.5.1 [[Call]] - // When the [[Call]] internal method of a function object, F, - // which was created using the bind function is called with a - // this value and a list of arguments ExtraArgs, the following - // steps are taken: - // 1. Let boundArgs be the value of F's [[BoundArgs]] internal - // property. - // 2. Let boundThis be the value of F's [[BoundThis]] internal - // property. - // 3. Let target be the value of F's [[TargetFunction]] internal - // property. - // 4. Let args be a new list containing the same values as the - // list boundArgs in the same order followed by the same - // values as the list ExtraArgs in the same order. - // 5. Return the result of calling the [[Call]] internal method - // of target providing boundThis as the this value and - // providing args as the arguments. - - // equiv: target.call(this, ...boundArgs, ...args) - return target.apply( - that, - array_concat.call(args, array_slice.call(arguments)) - ); - - } - - }; - - // 15. If the [[Class]] internal property of Target is "Function", then - // a. Let L be the length property of Target minus the length of A. - // b. Set the length own property of F to either 0 or L, whichever is - // larger. - // 16. Else set the length own property of F to 0. - - var boundLength = max(0, target.length - args.length); - - // 17. Set the attributes of the length own property of F to the values - // specified in 15.3.5.1. - var boundArgs = []; - for (var i = 0; i < boundLength; i++) { - array_push.call(boundArgs, '$' + i); - } - - // XXX Build a dynamic function with desired amount of arguments is the only - // way to set the length property of a function. - // In environments where Content Security Policies enabled (Chrome extensions, - // for ex.) all use of eval or Function costructor throws an exception. - // However in all of these environments Function.prototype.bind exists - // and so this code will never be executed. - bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this, arguments); }')(binder); - - if (target.prototype) { - Empty.prototype = target.prototype; - bound.prototype = new Empty(); - // Clean up dangling references. - Empty.prototype = null; - } - - // TODO - // 18. Set the [[Extensible]] internal property of F to true. - - // TODO - // 19. Let thrower be the [[ThrowTypeError]] function Object (13.2.3). - // 20. Call the [[DefineOwnProperty]] internal method of F with - // arguments "caller", PropertyDescriptor {[[Get]]: thrower, [[Set]]: - // thrower, [[Enumerable]]: false, [[Configurable]]: false}, and - // false. - // 21. Call the [[DefineOwnProperty]] internal method of F with - // arguments "arguments", PropertyDescriptor {[[Get]]: thrower, - // [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false}, - // and false. - - // TODO - // NOTE Function objects created using Function.prototype.bind do not - // have a prototype property or the [[Code]], [[FormalParameters]], and - // [[Scope]] internal properties. - // XXX can't delete prototype in pure-js. - - // 22. Return F. - return bound; - } - }); -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - -// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Window/detect.js -var detect = ('Window' in this); - -if (detect) return - -// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Window&flags=always -if ((typeof WorkerGlobalScope === "undefined") && (typeof importScripts !== "function")) { - (function (global) { - if (global.constructor) { - global.Window = global.constructor; - } else { - (global.Window = global.constructor = new Function('return function Window() {}')()).prototype = this; - } - }(this)); -} - -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - -// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Document/detect.js -var detect = ("Document" in this); - -if (detect) return - -// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Document&flags=always -if ((typeof WorkerGlobalScope === "undefined") && (typeof importScripts !== "function")) { - - if (this.HTMLDocument) { // IE8 - - // HTMLDocument is an extension of Document. If the browser has HTMLDocument but not Document, the former will suffice as an alias for the latter. - this.Document = this.HTMLDocument; - - } else { - - // Create an empty function to act as the missing constructor for the document object, attach the document object as its prototype. The function needs to be anonymous else it is hoisted and causes the feature detect to prematurely pass, preventing the assignments below being made. - this.Document = this.HTMLDocument = document.constructor = (new Function('return function Document() {}')()); - this.Document.prototype = document; - } -} - - -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - -// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Element/detect.js -var detect = ('Element' in this && 'HTMLElement' in this); - -if (detect) return - -// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Element&flags=always -(function () { - - // IE8 - if (window.Element && !window.HTMLElement) { - window.HTMLElement = window.Element; - return; - } - - // create Element constructor - window.Element = window.HTMLElement = new Function('return function Element() {}')(); - - // generate sandboxed iframe - var vbody = document.appendChild(document.createElement('body')); - var frame = vbody.appendChild(document.createElement('iframe')); - - // use sandboxed iframe to replicate Element functionality - var frameDocument = frame.contentWindow.document; - var prototype = Element.prototype = frameDocument.appendChild(frameDocument.createElement('*')); - var cache = {}; - - // polyfill Element.prototype on an element - var shiv = function (element, deep) { - var - childNodes = element.childNodes || [], - index = -1, - key, value, childNode; - - if (element.nodeType === 1 && element.constructor !== Element) { - element.constructor = Element; - - for (key in cache) { - value = cache[key]; - element[key] = value; - } - } - - while (childNode = deep && childNodes[++index]) { - shiv(childNode, deep); - } - - return element; - }; - - var elements = document.getElementsByTagName('*'); - var nativeCreateElement = document.createElement; - var interval; - var loopLimit = 100; - - prototype.attachEvent('onpropertychange', function (event) { - var - propertyName = event.propertyName, - nonValue = !cache.hasOwnProperty(propertyName), - newValue = prototype[propertyName], - oldValue = cache[propertyName], - index = -1, - element; - - while (element = elements[++index]) { - if (element.nodeType === 1) { - if (nonValue || element[propertyName] === oldValue) { - element[propertyName] = newValue; - } - } - } - - cache[propertyName] = newValue; - }); - - prototype.constructor = Element; - - if (!prototype.hasAttribute) { - // .hasAttribute - prototype.hasAttribute = function hasAttribute(name) { - return this.getAttribute(name) !== null; - }; - } - - // Apply Element prototype to the pre-existing DOM as soon as the body element appears. - function bodyCheck() { - if (!(loopLimit--)) clearTimeout(interval); - if (document.body && !document.body.prototype && /(complete|interactive)/.test(document.readyState)) { - shiv(document, true); - if (interval && document.body.prototype) clearTimeout(interval); - return (!!document.body.prototype); - } - return false; - } - if (!bodyCheck()) { - document.onreadystatechange = bodyCheck; - interval = setInterval(bodyCheck, 25); - } - - // Apply to any new elements created after load - document.createElement = function createElement(nodeName) { - var element = nativeCreateElement(String(nodeName).toLowerCase()); - return shiv(element); - }; - - // remove sandboxed iframe - document.removeChild(vbody); -}()); - -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - -// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Event/detect.js -var detect = ( - (function(global) { - - if (!('Event' in global)) return false; - if (typeof global.Event === 'function') return true; - - try { - - // In IE 9-11, the Event object exists but cannot be instantiated - new Event('click'); - return true; - } catch(e) { - return false; - } - }(this)) -); - -if (detect) return - -// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Event&flags=always -(function () { - var unlistenableWindowEvents = { - click: 1, - dblclick: 1, - keyup: 1, - keypress: 1, - keydown: 1, - mousedown: 1, - mouseup: 1, - mousemove: 1, - mouseover: 1, - mouseenter: 1, - mouseleave: 1, - mouseout: 1, - storage: 1, - storagecommit: 1, - textinput: 1 - }; - - // This polyfill depends on availability of `document` so will not run in a worker - // However, we asssume there are no browsers with worker support that lack proper - // support for `Event` within the worker - if (typeof document === 'undefined' || typeof window === 'undefined') return; - - function indexOf(array, element) { - var - index = -1, - length = array.length; - - while (++index < length) { - if (index in array && array[index] === element) { - return index; - } - } - - return -1; - } - - var existingProto = (window.Event && window.Event.prototype) || null; - window.Event = Window.prototype.Event = function Event(type, eventInitDict) { - if (!type) { - throw new Error('Not enough arguments'); - } - - var event; - // Shortcut if browser supports createEvent - if ('createEvent' in document) { - event = document.createEvent('Event'); - var bubbles = eventInitDict && eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false; - var cancelable = eventInitDict && eventInitDict.cancelable !== undefined ? eventInitDict.cancelable : false; - - event.initEvent(type, bubbles, cancelable); - - return event; - } - - event = document.createEventObject(); - - event.type = type; - event.bubbles = eventInitDict && eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false; - event.cancelable = eventInitDict && eventInitDict.cancelable !== undefined ? eventInitDict.cancelable : false; - - return event; - }; - if (existingProto) { - Object.defineProperty(window.Event, 'prototype', { - configurable: false, - enumerable: false, - writable: true, - value: existingProto - }); - } - - if (!('createEvent' in document)) { - window.addEventListener = Window.prototype.addEventListener = Document.prototype.addEventListener = Element.prototype.addEventListener = function addEventListener() { - var - element = this, - type = arguments[0], - listener = arguments[1]; - - if (element === window && type in unlistenableWindowEvents) { - throw new Error('In IE8 the event: ' + type + ' is not available on the window object. Please see https://github.com/Financial-Times/polyfill-service/issues/317 for more information.'); - } - - if (!element._events) { - element._events = {}; - } - - if (!element._events[type]) { - element._events[type] = function (event) { - var - list = element._events[event.type].list, - events = list.slice(), - index = -1, - length = events.length, - eventElement; - - event.preventDefault = function preventDefault() { - if (event.cancelable !== false) { - event.returnValue = false; - } - }; - - event.stopPropagation = function stopPropagation() { - event.cancelBubble = true; - }; - - event.stopImmediatePropagation = function stopImmediatePropagation() { - event.cancelBubble = true; - event.cancelImmediate = true; - }; - - event.currentTarget = element; - event.relatedTarget = event.fromElement || null; - event.target = event.target || event.srcElement || element; - event.timeStamp = new Date().getTime(); - - if (event.clientX) { - event.pageX = event.clientX + document.documentElement.scrollLeft; - event.pageY = event.clientY + document.documentElement.scrollTop; - } - - while (++index < length && !event.cancelImmediate) { - if (index in events) { - eventElement = events[index]; - - if (indexOf(list, eventElement) !== -1 && typeof eventElement === 'function') { - eventElement.call(element, event); - } - } - } - }; - - element._events[type].list = []; - - if (element.attachEvent) { - element.attachEvent('on' + type, element._events[type]); - } - } - - element._events[type].list.push(listener); - }; - - window.removeEventListener = Window.prototype.removeEventListener = Document.prototype.removeEventListener = Element.prototype.removeEventListener = function removeEventListener() { - var - element = this, - type = arguments[0], - listener = arguments[1], - index; - - if (element._events && element._events[type] && element._events[type].list) { - index = indexOf(element._events[type].list, listener); - - if (index !== -1) { - element._events[type].list.splice(index, 1); - - if (!element._events[type].list.length) { - if (element.detachEvent) { - element.detachEvent('on' + type, element._events[type]); - } - delete element._events[type]; - } - } - } - }; - - window.dispatchEvent = Window.prototype.dispatchEvent = Document.prototype.dispatchEvent = Element.prototype.dispatchEvent = function dispatchEvent(event) { - if (!arguments.length) { - throw new Error('Not enough arguments'); - } - - if (!event || typeof event.type !== 'string') { - throw new Error('DOM Events Exception 0'); - } - - var element = this, type = event.type; - - try { - if (!event.bubbles) { - event.cancelBubble = true; - - var cancelBubbleEvent = function (event) { - event.cancelBubble = true; - - (element || window).detachEvent('on' + type, cancelBubbleEvent); - }; - - this.attachEvent('on' + type, cancelBubbleEvent); - } - - this.fireEvent('on' + type, event); - } catch (error) { - event.target = element; - - do { - event.currentTarget = element; - - if ('_events' in element && typeof element._events[type] === 'function') { - element._events[type].call(element, event); - } - - if (typeof element['on' + type] === 'function') { - element['on' + type].call(element, event); - } - - element = element.nodeType === 9 ? element.parentWindow : element.parentNode; - } while (element && !event.cancelBubble); - } - - return true; - }; - - // Add the DOMContentLoaded Event - document.attachEvent('onreadystatechange', function() { - if (document.readyState === 'complete') { - document.dispatchEvent(new Event('DOMContentLoaded', { - bubbles: true - })); - } - }); - } -}()); - -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - - // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/master/packages/polyfill-library/polyfills/DOMTokenList/detect.js - var detect = ( - 'DOMTokenList' in this && (function (x) { - return 'classList' in x ? !x.classList.toggle('x', false) && !x.className : true; - })(document.createElement('x')) - ); - - if (detect) return - - // Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/master/packages/polyfill-library/polyfills/DOMTokenList/polyfill.js - (function (global) { - var nativeImpl = "DOMTokenList" in global && global.DOMTokenList; - - if ( - !nativeImpl || - ( - !!document.createElementNS && - !!document.createElementNS('http://www.w3.org/2000/svg', 'svg') && - !(document.createElementNS("http://www.w3.org/2000/svg", "svg").classList instanceof DOMTokenList) - ) - ) { - global.DOMTokenList = (function() { // eslint-disable-line no-unused-vars - var dpSupport = true; - var defineGetter = function (object, name, fn, configurable) { - if (Object.defineProperty) - Object.defineProperty(object, name, { - configurable: false === dpSupport ? true : !!configurable, - get: fn - }); - - else object.__defineGetter__(name, fn); - }; - - /** Ensure the browser allows Object.defineProperty to be used on native JavaScript objects. */ - try { - defineGetter({}, "support"); - } - catch (e) { - dpSupport = false; - } - - - var _DOMTokenList = function (el, prop) { - var that = this; - var tokens = []; - var tokenMap = {}; - var length = 0; - var maxLength = 0; - var addIndexGetter = function (i) { - defineGetter(that, i, function () { - preop(); - return tokens[i]; - }, false); - - }; - var reindex = function () { - - /** Define getter functions for array-like access to the tokenList's contents. */ - if (length >= maxLength) - for (; maxLength < length; ++maxLength) { - addIndexGetter(maxLength); - } - }; - - /** Helper function called at the start of each class method. Internal use only. */ - var preop = function () { - var error; - var i; - var args = arguments; - var rSpace = /\s+/; - - /** Validate the token/s passed to an instance method, if any. */ - if (args.length) - for (i = 0; i < args.length; ++i) - if (rSpace.test(args[i])) { - error = new SyntaxError('String "' + args[i] + '" ' + "contains" + ' an invalid character'); - error.code = 5; - error.name = "InvalidCharacterError"; - throw error; - } - - - /** Split the new value apart by whitespace*/ - if (typeof el[prop] === "object") { - tokens = ("" + el[prop].baseVal).replace(/^\s+|\s+$/g, "").split(rSpace); - } else { - tokens = ("" + el[prop]).replace(/^\s+|\s+$/g, "").split(rSpace); - } - - /** Avoid treating blank strings as single-item token lists */ - if ("" === tokens[0]) tokens = []; - - /** Repopulate the internal token lists */ - tokenMap = {}; - for (i = 0; i < tokens.length; ++i) - tokenMap[tokens[i]] = true; - length = tokens.length; - reindex(); - }; - - /** Populate our internal token list if the targeted attribute of the subject element isn't empty. */ - preop(); - - /** Return the number of tokens in the underlying string. Read-only. */ - defineGetter(that, "length", function () { - preop(); - return length; - }); - - /** Override the default toString/toLocaleString methods to return a space-delimited list of tokens when typecast. */ - that.toLocaleString = - that.toString = function () { - preop(); - return tokens.join(" "); - }; - - that.item = function (idx) { - preop(); - return tokens[idx]; - }; - - that.contains = function (token) { - preop(); - return !!tokenMap[token]; - }; - - that.add = function () { - preop.apply(that, args = arguments); - - for (var args, token, i = 0, l = args.length; i < l; ++i) { - token = args[i]; - if (!tokenMap[token]) { - tokens.push(token); - tokenMap[token] = true; - } - } - - /** Update the targeted attribute of the attached element if the token list's changed. */ - if (length !== tokens.length) { - length = tokens.length >>> 0; - if (typeof el[prop] === "object") { - el[prop].baseVal = tokens.join(" "); - } else { - el[prop] = tokens.join(" "); - } - reindex(); - } - }; - - that.remove = function () { - preop.apply(that, args = arguments); - - /** Build a hash of token names to compare against when recollecting our token list. */ - for (var args, ignore = {}, i = 0, t = []; i < args.length; ++i) { - ignore[args[i]] = true; - delete tokenMap[args[i]]; - } - - /** Run through our tokens list and reassign only those that aren't defined in the hash declared above. */ - for (i = 0; i < tokens.length; ++i) - if (!ignore[tokens[i]]) t.push(tokens[i]); - - tokens = t; - length = t.length >>> 0; - - /** Update the targeted attribute of the attached element. */ - if (typeof el[prop] === "object") { - el[prop].baseVal = tokens.join(" "); - } else { - el[prop] = tokens.join(" "); - } - reindex(); - }; - - that.toggle = function (token, force) { - preop.apply(that, [token]); - - /** Token state's being forced. */ - if (undefined !== force) { - if (force) { - that.add(token); - return true; - } else { - that.remove(token); - return false; - } - } - - /** Token already exists in tokenList. Remove it, and return FALSE. */ - if (tokenMap[token]) { - that.remove(token); - return false; - } - - /** Otherwise, add the token and return TRUE. */ - that.add(token); - return true; - }; - - return that; - }; - - return _DOMTokenList; - }()); - } - - // Add second argument to native DOMTokenList.toggle() if necessary - (function () { - var e = document.createElement('span'); - if (!('classList' in e)) return; - e.classList.toggle('x', false); - if (!e.classList.contains('x')) return; - e.classList.constructor.prototype.toggle = function toggle(token /*, force*/) { - var force = arguments[1]; - if (force === undefined) { - var add = !this.contains(token); - this[add ? 'add' : 'remove'](token); - return add; - } - force = !!force; - this[force ? 'add' : 'remove'](token); - return force; - }; - }()); - - // Add multiple arguments to native DOMTokenList.add() if necessary - (function () { - var e = document.createElement('span'); - if (!('classList' in e)) return; - e.classList.add('a', 'b'); - if (e.classList.contains('b')) return; - var native = e.classList.constructor.prototype.add; - e.classList.constructor.prototype.add = function () { - var args = arguments; - var l = arguments.length; - for (var i = 0; i < l; i++) { - native.call(this, args[i]); - } - }; - }()); - - // Add multiple arguments to native DOMTokenList.remove() if necessary - (function () { - var e = document.createElement('span'); - if (!('classList' in e)) return; - e.classList.add('a'); - e.classList.add('b'); - e.classList.remove('a', 'b'); - if (!e.classList.contains('b')) return; - var native = e.classList.constructor.prototype.remove; - e.classList.constructor.prototype.remove = function () { - var args = arguments; - var l = arguments.length; - for (var i = 0; i < l; i++) { - native.call(this, args[i]); - } - }; - }()); - - }(this)); - -}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - - // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/8717a9e04ac7aff99b4980fbedead98036b0929a/packages/polyfill-library/polyfills/Element/prototype/classList/detect.js - var detect = ( - 'document' in this && "classList" in document.documentElement && 'Element' in this && 'classList' in Element.prototype && (function () { - var e = document.createElement('span'); - e.classList.add('a', 'b'); - return e.classList.contains('b'); - }()) - ); - - if (detect) return - - // Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Element.prototype.classList&flags=always - (function (global) { - var dpSupport = true; - var defineGetter = function (object, name, fn, configurable) { - if (Object.defineProperty) - Object.defineProperty(object, name, { - configurable: false === dpSupport ? true : !!configurable, - get: fn - }); - - else object.__defineGetter__(name, fn); - }; - /** Ensure the browser allows Object.defineProperty to be used on native JavaScript objects. */ - try { - defineGetter({}, "support"); - } - catch (e) { - dpSupport = false; - } - /** Polyfills a property with a DOMTokenList */ - var addProp = function (o, name, attr) { - - defineGetter(o.prototype, name, function () { - var tokenList; - - var THIS = this, - - /** Prevent this from firing twice for some reason. What the hell, IE. */ - gibberishProperty = "__defineGetter__" + "DEFINE_PROPERTY" + name; - if(THIS[gibberishProperty]) return tokenList; - THIS[gibberishProperty] = true; - - /** - * IE8 can't define properties on native JavaScript objects, so we'll use a dumb hack instead. - * - * What this is doing is creating a dummy element ("reflection") inside a detached phantom node ("mirror") - * that serves as the target of Object.defineProperty instead. While we could simply use the subject HTML - * element instead, this would conflict with element types which use indexed properties (such as forms and - * select lists). - */ - if (false === dpSupport) { - - var visage; - var mirror = addProp.mirror || document.createElement("div"); - var reflections = mirror.childNodes; - var l = reflections.length; - - for (var i = 0; i < l; ++i) - if (reflections[i]._R === THIS) { - visage = reflections[i]; - break; - } - - /** Couldn't find an element's reflection inside the mirror. Materialise one. */ - visage || (visage = mirror.appendChild(document.createElement("div"))); - - tokenList = DOMTokenList.call(visage, THIS, attr); - } else tokenList = new DOMTokenList(THIS, attr); - - defineGetter(THIS, name, function () { - return tokenList; - }); - delete THIS[gibberishProperty]; - - return tokenList; - }, true); - }; - - addProp(global.Element, "classList", "className"); - addProp(global.HTMLElement, "classList", "className"); - addProp(global.HTMLLinkElement, "relList", "rel"); - addProp(global.HTMLAnchorElement, "relList", "rel"); - addProp(global.HTMLAreaElement, "relList", "rel"); - }(this)); - -}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -/** - * TODO: Ideally this would be a NodeList.prototype.forEach polyfill - * This seems to fail in IE8, requires more investigation. - * See: https://github.com/imagitama/nodelist-foreach-polyfill - */ -function nodeListForEach (nodes, callback) { - if (window.NodeList.prototype.forEach) { - return nodes.forEach(callback) - } - for (var i = 0; i < nodes.length; i++) { - callback.call(window, nodes[i], i, nodes); - } -} - -function Checkboxes ($module) { - this.$module = $module; - this.$inputs = $module.querySelectorAll('input[type="checkbox"]'); -} - -/** - * Initialise Checkboxes - * - * Checkboxes can be associated with a 'conditionally revealed' content block โ€“ - * for example, a checkbox for 'Phone' could reveal an additional form field for - * the user to enter their phone number. - * - * These associations are made using a `data-aria-controls` attribute, which is - * promoted to an aria-controls attribute during initialisation. - * - * We also need to restore the state of any conditional reveals on the page (for - * example if the user has navigated back), and set up event handlers to keep - * the reveal in sync with the checkbox state. - */ -Checkboxes.prototype.init = function () { - var $module = this.$module; - var $inputs = this.$inputs; - - nodeListForEach($inputs, function ($input) { - var target = $input.getAttribute('data-aria-controls'); - - // Skip checkboxes without data-aria-controls attributes, or where the - // target element does not exist. - if (!target || !$module.querySelector('#' + target)) { - return - } - - // Promote the data-aria-controls attribute to a aria-controls attribute - // so that the relationship is exposed in the AOM - $input.setAttribute('aria-controls', target); - $input.removeAttribute('data-aria-controls'); - }); - - // When the page is restored after navigating 'back' in some browsers the - // state of form controls is not restored until *after* the DOMContentLoaded - // event is fired, so we need to sync after the pageshow event in browsers - // that support it. - if ('onpageshow' in window) { - window.addEventListener('pageshow', this.syncAllConditionalReveals.bind(this)); - } else { - window.addEventListener('DOMContentLoaded', this.syncAllConditionalReveals.bind(this)); - } - - // Although we've set up handlers to sync state on the pageshow or - // DOMContentLoaded event, init could be called after those events have fired, - // for example if they are added to the page dynamically, so sync now too. - this.syncAllConditionalReveals(); - - $module.addEventListener('click', this.handleClick.bind(this)); -}; - -/** - * Sync the conditional reveal states for all inputs in this $module. - */ -Checkboxes.prototype.syncAllConditionalReveals = function () { - nodeListForEach(this.$inputs, this.syncConditionalRevealWithInputState.bind(this)); -}; - -/** - * Sync conditional reveal with the input state - * - * Synchronise the visibility of the conditional reveal, and its accessible - * state, with the input's checked state. - * - * @param {HTMLInputElement} $input Checkbox input - */ -Checkboxes.prototype.syncConditionalRevealWithInputState = function ($input) { - var $target = this.$module.querySelector('#' + $input.getAttribute('aria-controls')); - - if ($target && $target.classList.contains('govuk-checkboxes__conditional')) { - var inputIsChecked = $input.checked; - - $input.setAttribute('aria-expanded', inputIsChecked); - $target.classList.toggle('govuk-checkboxes__conditional--hidden', !inputIsChecked); - } -}; - -/** - * Click event handler - * - * Handle a click within the $module โ€“ if the click occurred on a checkbox, sync - * the state of any associated conditional reveal with the checkbox state. - * - * @param {MouseEvent} event Click event - */ -Checkboxes.prototype.handleClick = function (event) { - var $target = event.target; - - // If a checkbox with aria-controls, handle click - var isCheckbox = $target.getAttribute('type') === 'checkbox'; - var hasAriaControls = $target.getAttribute('aria-controls'); - if (isCheckbox && hasAriaControls) { - this.syncConditionalRevealWithInputState($target); - } -}; - -return Checkboxes; - -}))); diff --git a/package/govuk/components/checkboxes/fixtures.json b/package/govuk/components/checkboxes/fixtures.json deleted file mode 100644 index 6e50c076c6..0000000000 --- a/package/govuk/components/checkboxes/fixtures.json +++ /dev/null @@ -1,1108 +0,0 @@ -{ - "component": "checkboxes", - "fixtures": [ - { - "name": "default", - "options": { - "name": "nationality", - "items": [ - { - "value": "british", - "text": "British" - }, - { - "value": "irish", - "text": "Irish" - }, - { - "value": "other", - "text": "Citizen of another country" - } - ] - }, - "html": "
\n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n\n
", - "hidden": false - }, - { - "name": "with id and name", - "options": { - "name": "with-id-and-name", - "fieldset": { - "legend": { - "text": "What is your nationality?" - } - }, - "hint": { - "text": "If you have dual nationality, select all options that are relevant to you." - }, - "items": [ - { - "name": "british", - "id": "item_british", - "value": "yes", - "text": "British" - }, - { - "name": "irish", - "id": "item_irish", - "value": "irish", - "text": "Irish" - }, - { - "name": "custom-name-scottish", - "text": "Scottish", - "value": "scottish" - } - ] - }, - "html": "
\n\n
\n \n \n \n What is your nationality?\n \n \n \n\n
\n If you have dual nationality, select all options that are relevant to you.\n
\n\n\n
\n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with hints on items", - "options": { - "name": "with-hints-on-items", - "fieldset": { - "legend": { - "text": "How do you want to sign in?", - "isPageHeading": true - } - }, - "items": [ - { - "name": "gateway", - "id": "government-gateway", - "value": "gov-gateway", - "text": "Sign in with Government Gateway", - "hint": { - "text": "You'll have a user ID if you've registered for Self Assessment or filed a tax return online before." - } - }, - { - "name": "verify", - "id": "govuk-verify", - "value": "gov-verify", - "text": "Sign in with GOV.UK Verify", - "hint": { - "text": "You'll have an account if you've already proved your identity with either Barclays, CitizenSafe, Digidentity, Experian, Post Office, Royal Mail or SecureIdentity." - } - } - ] - }, - "html": "
\n\n
\n \n \n \n

\n How do you want to sign in?\n

\n \n
\n \n\n
\n \n \n \n \n \n \n \n
\n \n \n \n
\n You'll have a user ID if you've registered for Self Assessment or filed a tax return online before.\n
\n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n You'll have an account if you've already proved your identity with either Barclays, CitizenSafe, Digidentity, Experian, Post Office, Royal Mail or SecureIdentity.\n
\n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with disabled item", - "options": { - "name": "colours", - "items": [ - { - "value": "red", - "text": "Red" - }, - { - "value": "green", - "text": "Green" - }, - { - "value": "blue", - "text": "Blue", - "disabled": true - } - ] - }, - "html": "
\n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n\n
", - "hidden": false - }, - { - "name": "with legend as a page heading", - "options": { - "name": "waste", - "fieldset": { - "legend": { - "text": "Which types of waste do you transport regularly?", - "classes": "govuk-fieldset__legend--l", - "isPageHeading": true - } - }, - "hint": { - "text": "Select all that apply" - }, - "items": [ - { - "value": "animal", - "text": "Waste from animal carcasses" - }, - { - "value": "mines", - "text": "Waste from mines or quarries" - }, - { - "value": "farm", - "text": "Farm or agricultural waste" - } - ] - }, - "html": "
\n\n
\n \n \n \n

\n Which types of waste do you transport regularly?\n

\n \n
\n \n\n
\n Select all that apply\n
\n\n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with a medium legend", - "options": { - "name": "waste", - "fieldset": { - "legend": { - "text": "Which types of waste do you transport regularly?", - "classes": "govuk-fieldset__legend--m" - } - }, - "hint": { - "text": "Select all that apply" - }, - "errorMessage": { - "text": "Select which types of waste you transport regularly" - }, - "items": [ - { - "value": "animal", - "text": "Waste from animal carcasses" - }, - { - "value": "mines", - "text": "Waste from mines or quarries" - }, - { - "value": "farm", - "text": "Farm or agricultural waste" - } - ] - }, - "html": "
\n\n
\n \n \n \n Which types of waste do you transport regularly?\n \n \n \n\n
\n Select all that apply\n
\n\n\n \n \n \n Error: Select which types of waste you transport regularly\n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "without fieldset", - "options": { - "name": "colours", - "items": [ - { - "value": "red", - "text": "Red" - }, - { - "value": "green", - "text": "Green" - }, - { - "value": "blue", - "text": "Blue" - } - ] - }, - "html": "
\n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n\n
", - "hidden": false - }, - { - "name": "with single option set 'aria-describedby' on input", - "options": { - "name": "t-and-c", - "errorMessage": { - "text": "Please accept the terms and conditions" - }, - "items": [ - { - "value": "yes", - "text": "I agree to the terms and conditions" - } - ] - }, - "html": "
\n\n \n Error: Please accept the terms and conditions\n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n\n
", - "hidden": false - }, - { - "name": "with single option (and hint) set 'aria-describedby' on input", - "options": { - "name": "t-and-c-with-hint", - "errorMessage": { - "text": "Please accept the terms and conditions" - }, - "items": [ - { - "value": "yes", - "text": "I agree to the terms and conditions", - "hint": { - "text": "Go on, you know you want to!" - } - } - ] - }, - "html": "
\n\n \n Error: Please accept the terms and conditions\n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n Go on, you know you want to!\n
\n \n
\n \n \n \n
\n\n
", - "hidden": false - }, - { - "name": "with fieldset and error message", - "options": { - "name": "nationality", - "errorMessage": { - "text": "Please accept the terms and conditions" - }, - "fieldset": { - "legend": { - "text": "What is your nationality?" - } - }, - "items": [ - { - "value": "british", - "text": "British" - }, - { - "value": "irish", - "text": "Irish" - }, - { - "value": "other", - "text": "Citizen of another country" - } - ] - }, - "html": "
\n\n
\n \n \n \n What is your nationality?\n \n \n \n\n \n Error: Please accept the terms and conditions\n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with error message", - "options": { - "name": "waste", - "errorMessage": { - "text": "Please select an option" - }, - "fieldset": { - "legend": { - "text": "Which types of waste do you transport regularly?" - } - }, - "items": [ - { - "value": "animal", - "text": "Waste from animal carcasses" - }, - { - "value": "mines", - "text": "Waste from mines or quarries" - }, - { - "value": "farm", - "text": "Farm or agricultural waste" - } - ] - }, - "html": "
\n\n
\n \n \n \n Which types of waste do you transport regularly?\n \n \n \n\n \n Error: Please select an option\n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with error message and hints on items", - "options": { - "name": "waste", - "errorMessage": { - "text": "Please select an option" - }, - "fieldset": { - "legend": { - "text": "Which types of waste do you transport regularly?" - } - }, - "items": [ - { - "value": "animal", - "text": "Waste from animal carcasses", - "hint": { - "text": "Nullam id dolor id nibh ultricies vehicula ut id elit." - } - }, - { - "value": "mines", - "text": "Waste from mines or quarries", - "hint": { - "text": "Nullam id dolor id nibh ultricies vehicula ut id elit." - } - }, - { - "value": "farm", - "text": "Farm or agricultural waste", - "hint": { - "text": "Nullam id dolor id nibh ultricies vehicula ut id elit." - } - } - ] - }, - "html": "
\n\n
\n \n \n \n Which types of waste do you transport regularly?\n \n \n \n\n \n Error: Please select an option\n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n Nullam id dolor id nibh ultricies vehicula ut id elit.\n
\n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n Nullam id dolor id nibh ultricies vehicula ut id elit.\n
\n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n Nullam id dolor id nibh ultricies vehicula ut id elit.\n
\n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with very long option text", - "options": { - "name": "waste", - "hint": { - "text": "Nullam id dolor id nibh ultricies vehicula ut id elit." - }, - "errorMessage": { - "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." - }, - "fieldset": { - "legend": { - "text": "Maecenas faucibus mollis interdum?" - } - }, - "items": [ - { - "value": "nullam", - "text": "Nullam id dolor id nibh ultricies vehicula ut id elit. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Maecenas faucibus mollis interdum. Donec id elit non mi porta gravida at eget metus." - }, - { - "value": "aenean", - "text": "Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Donec sed odio dui. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Cras mattis consectetur purus sit amet fermentum." - }, - { - "value": "fusce", - "text": "Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Etiam porta sem malesuada magna mollis euismod. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. Sed posuere consectetur est at lobortis." - } - ] - }, - "html": "
\n\n
\n \n \n \n Maecenas faucibus mollis interdum?\n \n \n \n\n
\n Nullam id dolor id nibh ultricies vehicula ut id elit.\n
\n\n\n \n \n \n Error: Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with conditional items", - "options": { - "name": "with-conditional-items", - "idPrefix": "how-contacted", - "fieldset": { - "legend": { - "text": "How do you want to be contacted?" - } - }, - "items": [ - { - "value": "email", - "text": "Email", - "conditional": { - "html": "\n\n" - } - }, - { - "value": "phone", - "text": "Phone", - "conditional": { - "html": "\n\n" - } - }, - { - "value": "text", - "text": "Text message", - "conditional": { - "html": "\n\n" - } - } - ] - }, - "html": "
\n\n
\n \n \n \n How do you want to be contacted?\n \n \n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n
\n \n\n\n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n
\n \n\n\n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n
\n \n\n\n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with conditional item checked", - "options": { - "name": "how-contacted-checked", - "idPrefix": "how-contacted-checked", - "fieldset": { - "legend": { - "text": "How do you want to be contacted?" - } - }, - "items": [ - { - "value": "email", - "text": "Email", - "checked": true, - "conditional": { - "html": "\n\n" - } - }, - { - "value": "phone", - "text": "Phone", - "conditional": { - "html": "\n\n" - } - }, - { - "value": "text", - "text": "Text message", - "conditional": { - "html": "\n\n" - } - } - ] - }, - "html": "
\n\n
\n \n \n \n How do you want to be contacted?\n \n \n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n
\n \n\n\n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n
\n \n\n\n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n
\n \n\n\n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with optional form-group classes showing group error", - "options": { - "name": "how-contacted-checked", - "idPrefix": "how-contacted-checked", - "formGroup": { - "classes": "govuk-form-group--error" - }, - "fieldset": { - "legend": { - "text": "How do you want to be contacted?" - } - }, - "items": [ - { - "value": "email", - "text": "Email", - "conditional": { - "html": "\n\n" - } - }, - { - "value": "phone", - "text": "Phone", - "checked": true, - "conditional": { - "html": "\nProblem with input\n\n" - } - }, - { - "value": "text", - "text": "Text message", - "conditional": { - "html": "\n\n" - } - } - ] - }, - "html": "
\n\n
\n \n \n \n How do you want to be contacted?\n \n \n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n
\n \n\n\n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n
\n \nProblem with input\n\n\n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n
\n \n\n\n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "small", - "options": { - "idPrefix": "nationality", - "name": "nationality", - "classes": "govuk-checkboxes--small", - "fieldset": { - "legend": { - "text": "Filter by" - } - }, - "items": [ - { - "value": "a", - "text": "a thing" - }, - { - "value": "b", - "text": "another thing" - }, - { - "value": "c", - "text": "this thing" - } - ] - }, - "html": "
\n\n
\n \n \n \n Filter by\n \n \n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "small with long text", - "options": { - "idPrefix": "nationality", - "name": "nationality", - "classes": "govuk-checkboxes--small", - "fieldset": { - "legend": { - "text": "Filter by" - } - }, - "items": [ - { - "value": "nullam", - "text": "Nullam id dolor id nibh ultricies vehicula ut id elit. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Maecenas faucibus mollis interdum. Donec id elit non mi porta gravida at eget metus." - }, - { - "value": "aenean", - "text": "Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Donec sed odio dui. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Cras mattis consectetur purus sit amet fermentum." - }, - { - "value": "fusce", - "text": "Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Etiam porta sem malesuada magna mollis euismod. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. Sed posuere consectetur est at lobortis." - } - ] - }, - "html": "
\n\n
\n \n \n \n Filter by\n \n \n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "small with error", - "options": { - "idPrefix": "nationality", - "name": "nationality", - "classes": "govuk-checkboxes--small", - "errorMessage": { - "text": "Select a thing" - }, - "fieldset": { - "legend": { - "text": "Filter by" - } - }, - "items": [ - { - "value": "a", - "text": "a thing" - }, - { - "value": "b", - "text": "another thing" - }, - { - "value": "c", - "text": "this thing" - } - ] - }, - "html": "
\n\n
\n \n \n \n Filter by\n \n \n \n\n \n Error: Select a thing\n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "small with hint", - "options": { - "idPrefix": "nationality", - "name": "nationality", - "classes": "govuk-checkboxes--small", - "fieldset": { - "legend": { - "text": "Filter by" - } - }, - "items": [ - { - "value": "a", - "text": "a thing", - "hint": { - "text": "hint for a thing" - } - }, - { - "value": "b", - "text": "another thing" - }, - { - "value": "c", - "text": "this thing" - } - ] - }, - "html": "
\n\n
\n \n \n \n Filter by\n \n \n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n hint for a thing\n
\n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "small with disabled", - "options": { - "idPrefix": "nationality", - "name": "nationality", - "classes": "govuk-checkboxes--small", - "fieldset": { - "legend": { - "text": "Filter by" - } - }, - "items": [ - { - "value": "a", - "text": "a thing" - }, - { - "value": "b", - "text": "another thing" - }, - { - "value": "c", - "text": "this thing", - "disabled": true - } - ] - }, - "html": "
\n\n
\n \n \n \n Filter by\n \n \n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "small with conditional reveal", - "options": { - "name": "how-contacted", - "idPrefix": "how-contacted", - "classes": "govuk-checkboxes--small", - "fieldset": { - "legend": { - "text": "How do you want to be contacted?" - } - }, - "items": [ - { - "value": "a", - "text": "a thing", - "conditional": { - "html": "\n\n" - } - }, - { - "value": "b", - "text": "another thing" - } - ] - }, - "html": "
\n\n
\n \n \n \n How do you want to be contacted?\n \n \n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n
\n \n\n\n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with idPrefix", - "options": { - "name": "example-name", - "idPrefix": "nationality", - "items": [ - { - "value": 1, - "text": "Option 1" - }, - { - "value": 2, - "text": "Option 2" - } - ] - }, - "html": "
\n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n\n
", - "hidden": true - }, - { - "name": "with falsey values", - "options": { - "name": "example-name", - "items": [ - { - "value": 1, - "text": "Option 1" - }, - false, - null, - "", - { - "value": 2, - "text": "Option 2" - } - ] - }, - "html": "
\n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n\n
", - "hidden": true - }, - { - "name": "classes", - "options": { - "name": "example-name", - "classes": "app-checkboxes--custom-modifier", - "items": [ - { - "value": 1, - "text": "Option 1" - }, - { - "value": 2, - "text": "Option 2" - } - ] - }, - "html": "
\n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n\n
", - "hidden": true - }, - { - "name": "with fieldset describedBy", - "options": { - "name": "example-name", - "fieldset": { - "describedBy": "some-id" - }, - "items": [ - { - "value": 1, - "text": "Option 1" - }, - { - "value": 2, - "text": "Option 2" - } - ], - "hint": { - "text": "If you have dual nationality, select all options that are relevant to you." - } - }, - "html": "
\n\n
\n \n\n
\n If you have dual nationality, select all options that are relevant to you.\n
\n\n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": true - }, - { - "name": "attributes", - "options": { - "name": "example-name", - "attributes": { - "data-attribute": "value", - "data-second-attribute": "second-value" - }, - "items": [ - { - "value": 1, - "text": "Option 1" - }, - { - "value": 2, - "text": "Option 2" - } - ] - }, - "html": "
\n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n\n
", - "hidden": true - }, - { - "name": "with checked item", - "options": { - "name": "example-name", - "items": [ - { - "value": 1, - "text": "Option 1" - }, - { - "value": 2, - "text": "Option 2", - "checked": true - }, - { - "value": 3, - "text": "Option 3", - "checked": true - } - ] - }, - "html": "
\n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n\n
", - "hidden": true - }, - { - "name": "items with attributes", - "options": { - "name": "example-name", - "items": [ - { - "value": 1, - "text": "Option 1", - "attributes": { - "data-attribute": "ABC", - "data-second-attribute": "DEF" - } - }, - { - "value": 2, - "text": "Option 2", - "attributes": { - "data-attribute": "GHI", - "data-second-attribute": "JKL" - } - } - ] - }, - "html": "
\n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n\n
", - "hidden": true - }, - { - "name": "empty conditional", - "options": { - "name": "example-conditional", - "items": [ - { - "value": "foo", - "text": "Foo", - "conditional": { - "html": false - } - } - ] - }, - "html": "
\n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n\n
", - "hidden": true - }, - { - "name": "with label classes", - "options": { - "name": "example-label-classes", - "items": [ - { - "value": "yes", - "text": "Yes", - "label": { - "classes": "bold" - } - } - ] - }, - "html": "
\n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n\n
", - "hidden": true - }, - { - "name": "multiple hints", - "options": { - "name": "example-multiple-hints", - "hint": { - "text": "If you have dual nationality, select all options that are relevant to you." - }, - "items": [ - { - "value": "british", - "text": "British", - "hint": { - "text": "Hint for british option here" - } - }, - { - "value": "irish", - "text": "Irish" - }, - { - "value": "other", - "hint": { - "text": "Hint for other option here" - } - } - ] - }, - "html": "
\n\n
\n If you have dual nationality, select all options that are relevant to you.\n
\n\n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n Hint for british option here\n
\n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n Hint for other option here\n
\n \n
\n \n \n \n
\n\n
", - "hidden": true - }, - { - "name": "with error message and hint", - "options": { - "name": "example", - "items": [ - { - "value": "british", - "text": "British" - }, - { - "value": "irish", - "text": "Irish" - } - ], - "errorMessage": { - "text": "Please select an option" - }, - "fieldset": { - "legend": { - "text": "What is your nationality?" - } - }, - "hint": { - "text": "If you have dual nationality, select all options that are relevant to you." - } - }, - "html": "
\n\n
\n \n \n \n What is your nationality?\n \n \n \n\n
\n If you have dual nationality, select all options that are relevant to you.\n
\n\n\n \n \n \n Error: Please select an option\n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": true - }, - { - "name": "with error, hint and fieldset describedBy", - "options": { - "name": "example", - "errorMessage": { - "text": "Please select an option" - }, - "fieldset": { - "describedBy": "some-id", - "legend": { - "text": "What is your nationality?" - } - }, - "hint": { - "text": "If you have dual nationality, select all options that are relevant to you." - }, - "items": [ - { - "value": "british", - "text": "British" - }, - { - "value": "irish", - "text": "Irish" - } - ] - }, - "html": "
\n\n
\n \n \n \n What is your nationality?\n \n \n \n\n
\n If you have dual nationality, select all options that are relevant to you.\n
\n\n\n \n \n \n Error: Please select an option\n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": true - }, - { - "name": "label with attributes", - "options": { - "name": "example-name", - "items": [ - { - "value": 1, - "html": "Option 1", - "label": { - "attributes": { - "data-attribute": "value", - "data-second-attribute": "second-value" - } - } - } - ] - }, - "html": "
\n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n\n
", - "hidden": true - }, - { - "name": "fieldset params", - "options": { - "name": "example-name", - "errorMessage": { - "text": "Please select an option" - }, - "fieldset": { - "legend": { - "text": "What is your nationality?" - }, - "classes": "app-fieldset--custom-modifier", - "attributes": { - "data-attribute": "value", - "data-second-attribute": "second-value" - } - }, - "items": [ - { - "value": "british", - "text": "British" - }, - { - "value": "irish", - "text": "Irish" - } - ] - }, - "html": "
\n\n
\n \n \n \n What is your nationality?\n \n \n \n\n \n Error: Please select an option\n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": true - }, - { - "name": "fieldset html params", - "options": { - "name": "example-name", - "fieldset": { - "legend": { - "html": "What is your nationality?" - } - }, - "items": [ - { - "value": "british", - "text": "British" - }, - { - "value": "irish", - "text": "Irish" - } - ] - }, - "html": "
\n\n
\n \n \n \n What is your nationality?\n \n \n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": true - }, - { - "name": "with single option set 'aria-describedby' on input, and describedBy", - "options": { - "describedBy": "some-id", - "name": "t-and-c", - "errorMessage": { - "text": "Please accept the terms and conditions" - }, - "items": [ - { - "value": "yes", - "text": "I agree to the terms and conditions" - } - ] - }, - "html": "
\n\n \n Error: Please accept the terms and conditions\n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n\n
", - "hidden": true - }, - { - "name": "with single option (and hint) set 'aria-describedby' on input, and describedBy", - "options": { - "describedBy": "some-id", - "name": "t-and-c-with-hint", - "errorMessage": { - "text": "Please accept the terms and conditions" - }, - "items": [ - { - "value": "yes", - "text": "I agree to the terms and conditions", - "hint": { - "text": "Go on, you know you want to!" - } - } - ] - }, - "html": "
\n\n \n Error: Please accept the terms and conditions\n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n Go on, you know you want to!\n
\n \n
\n \n \n \n
\n\n
", - "hidden": true - }, - { - "name": "with error and idPrefix", - "options": { - "name": "name-of-checkboxes", - "errorMessage": { - "text": "Please select an option" - }, - "idPrefix": "id-prefix", - "items": [ - { - "value": "animal", - "text": "Waste from animal carcasses" - } - ] - }, - "html": "
\n\n \n Error: Please select an option\n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n\n
", - "hidden": true - }, - { - "name": "with error message and fieldset describedBy", - "options": { - "name": "example", - "errorMessage": { - "text": "Please select an option" - }, - "fieldset": { - "describedBy": "some-id", - "legend": { - "text": "What is your nationality?" - } - }, - "items": [ - { - "value": "british", - "text": "British" - }, - { - "value": "irish", - "text": "Irish" - } - ] - }, - "html": "
\n\n
\n \n \n \n What is your nationality?\n \n \n \n\n \n Error: Please select an option\n \n\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n\n
\n\n\n
", - "hidden": true - } - ] -} \ No newline at end of file diff --git a/package/govuk/components/checkboxes/macro-options.json b/package/govuk/components/checkboxes/macro-options.json deleted file mode 100644 index c724f4cbd1..0000000000 --- a/package/govuk/components/checkboxes/macro-options.json +++ /dev/null @@ -1,149 +0,0 @@ -[ - { - "name": "describedBy", - "type": "string", - "required": false, - "description": "One or more element IDs to add to the input `aria-describedby` attribute without a fieldset, used to provide additional descriptive information for screenreader users." - }, - { - "name": "fieldset", - "type": "object", - "required": false, - "description": "Options for the fieldset component (e.g. legend).", - "isComponent": true - }, - { - "name": "hint", - "type": "object", - "required": false, - "description": "Options for the hint component (e.g. text).", - "isComponent": true - }, - { - "name": "errorMessage", - "type": "object", - "required": false, - "description": "Options for the error message component. The error message component will not display if you use a falsy value for `errorMessage`, for example `false` or `null`.", - "isComponent": true - }, - { - "name": "formGroup", - "type": "object", - "required": false, - "description": "Options for the form-group wrapper", - "params": [ - { - "name": "classes", - "type": "string", - "required": false, - "description": "Classes to add to the form group (e.g. to show error state for the whole group)" - } - ] - }, - { - "name": "idPrefix", - "type": "string", - "required": false, - "description": "String to prefix id for each checkbox item if no id is specified on each item. If not passed, fall back to using the name option instead." - }, - { - "name": "name", - "type": "string", - "required": true, - "description": "Name attribute for all checkbox items." - }, - { - "name": "items", - "type": "array", - "required": true, - "description": "Array of checkbox items objects.", - "params": [ - { - "name": "text", - "type": "string", - "required": true, - "description": "If `html` is set, this is not required. Text to use within each checkbox item label. If `html` is provided, the `text` argument will be ignored." - }, - { - "name": "html", - "type": "string", - "required": true, - "description": "If `text` is set, this is not required. HTML to use within each checkbox item label. If `html` is provided, the `text` argument will be ignored." - }, - { - "name": "id", - "type": "string", - "required": false, - "description": "Specific id attribute for the checkbox item. If omitted, then component global `idPrefix` option will be applied." - }, - { - "name": "name", - "type": "string", - "required": false, - "description": "Specific name for the checkbox item. If omitted, then component global `name` string will be applied." - }, - { - "name": "value", - "type": "string", - "required": true, - "description": "Value for the checkbox input." - }, - { - "name": "label", - "type": "object", - "required": false, - "description": "Provide attributes and classes to each checkbox item label.", - "isComponent": true - }, - { - "name": "hint", - "type": "object", - "required": false, - "description": "Provide hint to each checkbox item.", - "isComponent": true - }, - { - "name": "checked", - "type": "boolean", - "required": false, - "description": "If true, checkbox will be checked." - }, - { - "name": "conditional", - "type": "boolean", - "required": false, - "description": "If true, content provided will be revealed when the item is checked." - }, - { - "name": "conditional.html", - "type": "string", - "required": false, - "description": "Provide content for the conditional reveal." - }, - { - "name": "disabled", - "type": "boolean", - "required": false, - "description": "If true, checkbox will be disabled." - }, - { - "name": "attributes", - "type": "object", - "required": false, - "description": "HTML attributes (for example data attributes) to add to the checkbox input tag." - } - ] - }, - { - "name": "classes", - "type": "string", - "required": false, - "description": "Classes to add to the checkboxes container." - }, - { - "name": "attributes", - "type": "object", - "required": false, - "description": "HTML attributes (for example data attributes) to add to the anchor tag." - } -] \ No newline at end of file diff --git a/package/govuk/components/checkboxes/template.njk b/package/govuk/components/checkboxes/template.njk deleted file mode 100644 index 6337207509..0000000000 --- a/package/govuk/components/checkboxes/template.njk +++ /dev/null @@ -1,121 +0,0 @@ -{% from "../error-message/macro.njk" import govukErrorMessage -%} -{% from "../fieldset/macro.njk" import govukFieldset %} -{% from "../hint/macro.njk" import govukHint %} -{% from "../label/macro.njk" import govukLabel %} - -{#- If an id 'prefix' is not passed, fall back to using the name attribute - instead. We need this for error messages and hints as well -#} -{% set idPrefix = params.idPrefix if params.idPrefix else params.name %} - -{#- a record of other elements that we need to associate with the input using - aria-describedby โ€“ for example hints or error messages -#} -{% set describedBy = params.describedBy if params.describedBy else "" %} -{% if params.fieldset.describedBy %} - {% set describedBy = params.fieldset.describedBy %} -{% endif %} - -{% set isConditional = false %} -{% for item in params.items %} - {% if item.conditional.html %} - {% set isConditional = true %} - {% endif %} -{% endfor %} - -{#- fieldset is false by default -#} -{% set hasFieldset = true if params.fieldset else false %} - -{#- Capture the HTML so we can optionally nest it in a fieldset -#} -{% set innerHtml %} -{% if params.hint %} - {% set hintId = idPrefix + '-hint' %} - {% set describedBy = describedBy + ' ' + hintId if describedBy else hintId %} - {{ govukHint({ - id: hintId, - classes: params.hint.classes, - attributes: params.hint.attributes, - html: params.hint.html, - text: params.hint.text - }) | indent(2) | trim }} -{% endif %} -{% if params.errorMessage %} - {% set errorId = idPrefix + '-error' %} - {% set describedBy = describedBy + ' ' + errorId if describedBy else errorId %} - {{ govukErrorMessage({ - id: errorId, - classes: params.errorMessage.classes, - attributes: params.errorMessage.attributes, - html: params.errorMessage.html, - text: params.errorMessage.text, - visuallyHiddenText: params.errorMessage.visuallyHiddenText - }) | indent(2) | trim }} -{% endif %} -
- {% for item in params.items %} - {% if item %} - {#- If the user explicitly sets an id, use this instead of the regular idPrefix -#} - {%- if item.id -%} - {%- set id = item.id -%} - {%- else -%} - {#- The first id should not have a number suffix so it's easy to link to from the error summary component -#} - {%- if loop.first -%} - {%- set id = idPrefix %} - {% else %} - {%- set id = idPrefix + "-" + loop.index -%} - {%- endif -%} - {%- endif -%} - {% set name = item.name if item.name else params.name %} - {% set conditionalId = "conditional-" + id %} - {% set hasHint = true if item.hint.text or item.hint.html %} - {% set itemHintId = id + "-item-hint" if hasHint else "" %} - {% set itemDescribedBy = describedBy if not hasFieldset else "" %} - {% set itemDescribedBy = (itemDescribedBy + " " + itemHintId) | trim %} -
- - {{ govukLabel({ - html: item.html, - text: item.text, - classes: 'govuk-checkboxes__label' + (' ' + item.label.classes if item.label.classes), - attributes: item.label.attributes, - for: id - }) | indent(6) | trim }} - {% if hasHint %} - {{ govukHint({ - id: itemHintId, - classes: 'govuk-checkboxes__hint' + (' ' + item.hint.classes if item.hint.classes), - attributes: item.hint.attributes, - html: item.hint.html, - text: item.hint.text - }) | indent(6) | trim }} - {% endif %} -
- {% if item.conditional.html %} -
- {{ item.conditional.html | safe }} -
- {% endif %} - {% endif %} - {% endfor %} -
-{% endset -%} - -
-{% if params.fieldset %} - {% call govukFieldset({ - describedBy: describedBy, - classes: params.fieldset.classes, - attributes: params.fieldset.attributes, - legend: params.fieldset.legend - }) %} - {{ innerHtml | trim | safe }} - {% endcall %} -{% else %} - {{ innerHtml | trim | safe }} -{% endif %} -
diff --git a/package/govuk/components/cookie-banner/_index.scss b/package/govuk/components/cookie-banner/_index.scss deleted file mode 100644 index f09287e487..0000000000 --- a/package/govuk/components/cookie-banner/_index.scss +++ /dev/null @@ -1,51 +0,0 @@ -@include govuk-exports("govuk/component/cookie-banner") { - - // This needs to be kept in sync with the header component's styles - $border-bottom-width: govuk-spacing(2); - - .govuk-cookie-banner { - @include govuk-font($size: 19); - - padding-top: govuk-spacing(4); - // The component does not set bottom spacing. - // The bottom spacing should be created by the items inside the component. - - // Visually separate the cookie banner from content underneath - // when user changes colours in their browser. - border-bottom: $border-bottom-width solid transparent; - - background-color: govuk-colour("light-grey", $legacy: "grey-3"); - } - - // Support older browsers which don't hide elements with the `hidden` attribute - // when user hides the whole cookie banner with a 'Hide' button. - .govuk-cookie-banner[hidden] { - display: none; - } - - .govuk-cookie-banner__message { - // Remove the extra height added by the separator border. - margin-bottom: -$border-bottom-width; - - &[hidden] { - // Support older browsers which don't hide elements with the `hidden` attribute - // when the visibility of cookie and replacement messages is toggled. - display: none; - } - - &:focus { - // Remove the native visible focus indicator when the element is programmatically focused. - // - // The focused cookie banner is the first element on the page and the last thing the user - // interacted with prior to it gaining focus. - // We therefore assume that moving focus to it is not going to surprise users, and that giving - // it a visible focus indicator could be more confusing than helpful, especially as the - // element is not normally keyboard operable. - // - // We have flagged this in the research section of the guidance as something to monitor. - // - // A related discussion: https://github.com/w3c/wcag/issues/1001 - outline: none; - } - } -} diff --git a/package/govuk/components/cookie-banner/fixtures.json b/package/govuk/components/cookie-banner/fixtures.json deleted file mode 100644 index c64628103b..0000000000 --- a/package/govuk/components/cookie-banner/fixtures.json +++ /dev/null @@ -1,425 +0,0 @@ -{ - "component": "cookie-banner", - "fixtures": [ - { - "name": "default", - "options": { - "messages": [ - { - "headingText": "Cookies on this government service", - "text": "We use analytics cookies to help understand how users use our service.", - "actions": [ - { - "text": "Accept analytics cookies", - "type": "submit", - "name": "cookies", - "value": "accept" - }, - { - "text": "Reject analytics cookies", - "type": "submit", - "name": "cookies", - "value": "reject" - }, - { - "text": "View cookie preferences", - "href": "/cookie-preferences" - } - ] - } - ] - }, - "html": "", - "hidden": false - }, - { - "name": "accepted confirmation banner", - "options": { - "messages": [ - { - "text": "Your cookie preferences have been saved. You have accepted cookies.", - "role": "alert", - "actions": [ - { - "text": "Hide this message", - "type": "button" - } - ] - } - ] - }, - "html": "", - "hidden": false - }, - { - "name": "rejected confirmation banner", - "options": { - "messages": [ - { - "text": "Your cookie preferences have been saved. You have rejected cookies.", - "role": "alert", - "actions": [ - { - "text": "Hide this message", - "type": "button" - } - ] - } - ] - }, - "html": "", - "hidden": false - }, - { - "name": "client-side implementation", - "options": { - "messages": [ - { - "headingText": "Cookies on this service", - "text": "We use cookies to help understand how users use our service.", - "actions": [ - { - "text": "Accept analytics cookies", - "type": "submit", - "name": "cookies", - "value": "accept" - }, - { - "text": "Reject analytics cookies", - "type": "submit", - "name": "cookies", - "value": "reject" - }, - { - "text": "View cookie preferences", - "href": "/cookie-preferences" - } - ] - }, - { - "text": "Your cookie preferences have been saved. You have accepted cookies.", - "role": "alert", - "hidden": true, - "actions": [ - { - "text": "Hide this message", - "type": "button" - } - ] - }, - { - "text": "Your cookie preferences have been saved. You have rejected cookies.", - "role": "alert", - "hidden": true, - "actions": [ - { - "text": "Hide this message", - "type": "button" - } - ] - } - ] - }, - "html": "", - "hidden": false - }, - { - "name": "heading html", - "options": { - "messages": [ - { - "headingHtml": "Cookies on my service" - } - ] - }, - "html": "", - "hidden": true - }, - { - "name": "heading html as text", - "options": { - "messages": [ - { - "headingText": "Cookies on my service" - } - ] - }, - "html": "", - "hidden": true - }, - { - "name": "html", - "options": { - "messages": [ - { - "html": "

We use cookies in our service.

" - } - ] - }, - "html": "", - "hidden": true - }, - { - "name": "classes", - "options": { - "messages": [ - { - "classes": "app-my-class" - } - ] - }, - "html": "", - "hidden": true - }, - { - "name": "attributes", - "options": { - "messages": [ - { - "attributes": { - "data-attribute": "my-value" - } - } - ] - }, - "html": "", - "hidden": true - }, - { - "name": "custom aria label", - "options": { - "ariaLabel": "Cookies on GOV.UK", - "messages": [ - { - "text": "We use cookies on GOV.UK" - } - ] - }, - "html": "", - "hidden": true - }, - { - "name": "hidden", - "options": { - "messages": [ - { - "hidden": true - } - ] - }, - "html": "", - "hidden": true - }, - { - "name": "hidden false", - "options": { - "messages": [ - { - "hidden": false - } - ] - }, - "html": "", - "hidden": true - }, - { - "name": "default action", - "options": { - "messages": [ - { - "actions": [ - { - "text": "This is a button" - } - ] - } - ] - }, - "html": "", - "hidden": true - }, - { - "name": "link", - "options": { - "messages": [ - { - "actions": [ - { - "text": "This is a link", - "href": "/link" - } - ] - } - ] - }, - "html": "", - "hidden": true - }, - { - "name": "link with button options", - "options": { - "messages": [ - { - "actions": [ - { - "text": "This is a link", - "href": "/link", - "value": "cookies", - "name": "link" - } - ] - } - ] - }, - "html": "", - "hidden": true - }, - { - "name": "type", - "options": { - "messages": [ - { - "actions": [ - { - "text": "Button", - "type": "button" - } - ] - } - ] - }, - "html": "", - "hidden": true - }, - { - "name": "button classes", - "options": { - "messages": [ - { - "actions": [ - { - "text": "Button with custom classes", - "classes": "my-button-class app-button-class" - } - ] - } - ] - }, - "html": "", - "hidden": true - }, - { - "name": "button attributes", - "options": { - "messages": [ - { - "actions": [ - { - "text": "Button with attributes", - "attributes": { - "data-button-attribute": "my-value" - } - } - ] - } - ] - }, - "html": "", - "hidden": true - }, - { - "name": "link classes", - "options": { - "messages": [ - { - "actions": [ - { - "text": "Link with custom classes", - "href": "/my-link", - "classes": "my-link-class app-link-class" - } - ] - } - ] - }, - "html": "", - "hidden": true - }, - { - "name": "link attributes", - "options": { - "messages": [ - { - "actions": [ - { - "text": "Link with attributes", - "href": "/link", - "attributes": { - "data-link-attribute": "my-value" - } - } - ] - } - ] - }, - "html": "", - "hidden": true - }, - { - "name": "full banner hidden", - "options": { - "hidden": true, - "classes": "hide-cookie-banner", - "attributes": { - "data-hide-cookie-banner": "true" - }, - "messages": [ - { - "headingText": "Cookies on this service", - "text": "We use cookies to help understand how users use our service.", - "actions": [ - { - "text": "Accept analytics cookies", - "type": "submit", - "name": "cookies", - "value": "accept" - }, - { - "text": "Reject analytics cookies", - "type": "submit", - "name": "cookies", - "value": "reject" - }, - { - "text": "View cookie preferences", - "href": "/cookie-preferences" - } - ] - }, - { - "text": "Your cookie preferences have been saved. You have accepted cookies.", - "role": "alert", - "actions": [ - { - "text": "Hide this message", - "type": "button" - } - ] - }, - { - "text": "Your cookie preferences have been saved. You have rejected cookies.", - "role": "alert", - "actions": [ - { - "text": "Hide this message", - "type": "button" - } - ] - } - ] - }, - "html": "", - "hidden": true - } - ] -} \ No newline at end of file diff --git a/package/govuk/components/cookie-banner/macro-options.json b/package/govuk/components/cookie-banner/macro-options.json deleted file mode 100644 index a7058f4e3c..0000000000 --- a/package/govuk/components/cookie-banner/macro-options.json +++ /dev/null @@ -1,132 +0,0 @@ -[ - { - "name": "ariaLabel", - "type": "string", - "required": false, - "description": "The text for the `aria-label` which labels the cookie banner region. This region applies to all messages that the cookie banner includes. For example, the cookie message and the replacement message. Defaults to \"Cookie banner\"." - }, - { - "name": "hidden", - "type": "boolean", - "required": false, - "description": "Defaults to `false`. If you set this option to `true`, the whole cookie banner is hidden, including all messages within the banner. You can use `hidden` for client-side implementations where the cookie banner HTML is present, but hidden until the cookie banner is shown using JavaScript." - }, - { - "name": "classes", - "type": "string", - "required": false, - "description": "The additional classes that you want to add to the cookie banner." - }, - { - "name": "attributes", - "type": "object", - "required": false, - "description": "The additional attributes that you want to add to the cookie banner. For example, data attributes." - }, - { - "name": "messages", - "type": "array", - "required": true, - "description": "The different messages you can pass into the cookie banner. For example, the cookie message and the replacement message.", - "params": [ - { - "name": "headingText", - "type": "string", - "required": false, - "description": "The heading text that displays in the message. You can use any string with this option. If you set `headingHtml`, `headingText` is ignored." - }, - { - "name": "headingHtml", - "type": "string", - "required": false, - "description": "The heading HTML to use within the message. You can use any string with this option. If you set `headingHtml`, `headingText` is ignored. If you are not passing HTML, use `headingText`." - }, - { - "name": "text", - "type": "string", - "required": true, - "description": "The text for the main content within the message. You can use any string with this option. If you set `html`, `text` is not required and is ignored." - }, - { - "name": "html", - "type": "string", - "required": true, - "description": "The HTML for the main content within the message. You can use any string with this option. If you set `html`, `text` is not required and is ignored. If you are not passing HTML, use `text`." - }, - { - "name": "actions", - "type": "array", - "required": false, - "description": "The buttons and links that you want to display in the message. `actions` defaults to `button` unless you set `href`, which renders the action as a link.", - "params": [ - { - "name": "text", - "type": "string", - "required": true, - "description": "The button or link text." - }, - { - "name": "type", - "type": "string", - "required": false, - "description": "The type of button. Does not apply if you set `href`, which renders a link. You can choose `button` or `submit`." - }, - { - "name": "href", - "type": "string", - "required": false, - "description": "The `href` for a link. If you set `href`, users will see a link instead of a button." - }, - { - "name": "name", - "type": "string", - "required": false, - "description": "The name attribute for the button. Does not apply if you set `href`, which makes a link." - }, - { - "name": "value", - "type": "string", - "required": false, - "description": "The value attribute for the button. Does not apply if you set `href`, which makes a link." - }, - { - "name": "classes", - "type": "string", - "required": false, - "description": "The additional classes that you want to add to the button or link." - }, - { - "name": "attributes", - "type": "object", - "required": false, - "description": "The additional attributes that you want to add to the button or link. For example, data attributes." - } - ] - }, - { - "name": "hidden", - "type": "boolean", - "required": false, - "description": "Defaults to `false`. If you set it to `true`, the message is hidden. You can use `hidden` for client-side implementations where the replacement message HTML is present, but hidden on the page." - }, - { - "name": "role", - "type": "string", - "required": false, - "description": "Set `role` to `alert` on replacement messages to allow assistive tech to automatically read the message. You will also need to move focus to the replacement message using JavaScript you have written yourself." - }, - { - "name": "classes", - "type": "string", - "required": false, - "description": "The additional classes that you want to add to the message." - }, - { - "name": "attributes", - "type": "object", - "required": false, - "description": "The additional attributes that you want to add to the message. For example, data attributes." - } - ] - } -] \ No newline at end of file diff --git a/package/govuk/components/cookie-banner/template.njk b/package/govuk/components/cookie-banner/template.njk deleted file mode 100644 index bdd1db87e7..0000000000 --- a/package/govuk/components/cookie-banner/template.njk +++ /dev/null @@ -1,63 +0,0 @@ -{% from "../button/macro.njk" import govukButton -%} - - diff --git a/package/govuk/components/date-input/_index.scss b/package/govuk/components/date-input/_index.scss deleted file mode 100644 index bb8a8a4618..0000000000 --- a/package/govuk/components/date-input/_index.scss +++ /dev/null @@ -1,26 +0,0 @@ -@import "../error-message/index"; -@import "../input/index"; -@import "../hint/index"; -@import "../label/index"; - -@include govuk-exports("govuk/component/date-input") { - .govuk-date-input { - @include govuk-clearfix; - // font-size: 0 removes whitespace caused by inline-block - font-size: 0; - } - - .govuk-date-input__item { - display: inline-block; - margin-right: govuk-spacing(4); - margin-bottom: 0; - } - - .govuk-date-input__label { - display: block; - } - - .govuk-date-input__input { - margin-bottom: 0; - } -} diff --git a/package/govuk/components/date-input/fixtures.json b/package/govuk/components/date-input/fixtures.json deleted file mode 100644 index bba3b4f92d..0000000000 --- a/package/govuk/components/date-input/fixtures.json +++ /dev/null @@ -1,549 +0,0 @@ -{ - "component": "date-input", - "fixtures": [ - { - "name": "default", - "options": { - "id": "dob" - }, - "html": "
\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n\n
", - "hidden": false - }, - { - "name": "complete question", - "options": { - "id": "dob", - "namePrefix": "dob", - "fieldset": { - "legend": { - "text": "What is your date of birth?" - } - }, - "hint": { - "text": "For example, 31 3 1980" - }, - "items": [ - { - "name": "day", - "classes": "govuk-input--width-2" - }, - { - "name": "month", - "classes": "govuk-input--width-2" - }, - { - "name": "year", - "classes": "govuk-input--width-4" - } - ] - }, - "html": "
\n
\n \n \n \n What is your date of birth?\n \n \n \n\n
\n For example, 31 3 1980\n
\n\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with errors only", - "options": { - "id": "dob-errors", - "fieldset": { - "legend": { - "text": "What is your date of birth?" - } - }, - "errorMessage": { - "text": "Error message goes here" - }, - "items": [ - { - "name": "day", - "classes": "govuk-input--width-2 govuk-input--error" - }, - { - "name": "month", - "classes": "govuk-input--width-2 govuk-input--error" - }, - { - "name": "year", - "classes": "govuk-input--width-4 govuk-input--error" - } - ] - }, - "html": "
\n
\n \n \n \n What is your date of birth?\n \n \n \n\n \n Error: Error message goes here\n \n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with errors and hint", - "options": { - "id": "dob-errors", - "fieldset": { - "legend": { - "text": "What is your date of birth?" - } - }, - "hint": { - "text": "For example, 31 3 1980" - }, - "errorMessage": { - "text": "Error message goes here" - }, - "items": [ - { - "name": "day", - "classes": "govuk-input--width-2 govuk-input--error" - }, - { - "name": "month", - "classes": "govuk-input--width-2 govuk-input--error" - }, - { - "name": "year", - "classes": "govuk-input--width-4 govuk-input--error" - } - ] - }, - "html": "
\n
\n \n \n \n What is your date of birth?\n \n \n \n\n
\n For example, 31 3 1980\n
\n\n\n \n \n \n Error: Error message goes here\n \n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with error on day input", - "options": { - "id": "dob-day-error", - "namePrefix": "dob-day-error", - "fieldset": { - "legend": { - "text": "What is your date of birth?" - } - }, - "hint": { - "text": "For example, 31 3 1980" - }, - "errorMessage": { - "text": "Error message goes here" - }, - "items": [ - { - "name": "day", - "classes": "govuk-input--width-2 govuk-input--error" - }, - { - "name": "month", - "classes": "govuk-input--width-2" - }, - { - "name": "year", - "classes": "govuk-input--width-4" - } - ] - }, - "html": "
\n
\n \n \n \n What is your date of birth?\n \n \n \n\n
\n For example, 31 3 1980\n
\n\n\n \n \n \n Error: Error message goes here\n \n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with error on month input", - "options": { - "id": "dob-month-error", - "namePrefix": "dob-month-error", - "fieldset": { - "legend": { - "text": "What is your date of birth?" - } - }, - "hint": { - "text": "For example, 31 3 1980" - }, - "errorMessage": { - "text": "Error message goes here" - }, - "items": [ - { - "name": "day", - "classes": "govuk-input--width-2" - }, - { - "name": "month", - "classes": "govuk-input--width-2 govuk-input--error" - }, - { - "name": "year", - "classes": "govuk-input--width-4" - } - ] - }, - "html": "
\n
\n \n \n \n What is your date of birth?\n \n \n \n\n
\n For example, 31 3 1980\n
\n\n\n \n \n \n Error: Error message goes here\n \n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with error on year input", - "options": { - "id": "dob-year-error", - "namePrefix": "dob-year-error", - "fieldset": { - "legend": { - "text": "What is your date of birth?" - } - }, - "hint": { - "text": "For example, 31 3 1980" - }, - "errorMessage": { - "text": "Error message goes here" - }, - "items": [ - { - "name": "day", - "classes": "govuk-input--width-2" - }, - { - "name": "month", - "classes": "govuk-input--width-2" - }, - { - "name": "year", - "classes": "govuk-input--width-4 govuk-input--error" - } - ] - }, - "html": "
\n
\n \n \n \n What is your date of birth?\n \n \n \n\n
\n For example, 31 3 1980\n
\n\n\n \n \n \n Error: Error message goes here\n \n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with default items", - "options": { - "id": "dob", - "namePrefix": "dob", - "fieldset": { - "legend": { - "text": "What is your date of birth?" - } - }, - "hint": { - "text": "For example, 31 3 1980" - } - }, - "html": "
\n
\n \n \n \n What is your date of birth?\n \n \n \n\n
\n For example, 31 3 1980\n
\n\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with optional form-group classes", - "options": { - "id": "dob", - "namePrefix": "dob", - "fieldset": { - "legend": { - "text": "What is your date of birth?" - } - }, - "hint": { - "text": "For example, 31 3 1980" - }, - "formGroup": { - "classes": "extra-class" - } - }, - "html": "
\n
\n \n \n \n What is your date of birth?\n \n \n \n\n
\n For example, 31 3 1980\n
\n\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with autocomplete values", - "options": { - "id": "dob-with-autocomplete-attribute", - "namePrefix": "dob-with-autocomplete", - "fieldset": { - "legend": { - "text": "What is your date of birth?" - } - }, - "hint": { - "text": "For example, 31 3 1980" - }, - "items": [ - { - "name": "day", - "classes": "govuk-input--width-2", - "autocomplete": "bday-day" - }, - { - "name": "month", - "classes": "govuk-input--width-2", - "autocomplete": "bday-month" - }, - { - "name": "year", - "classes": "govuk-input--width-4", - "autocomplete": "bday-year" - } - ] - }, - "html": "
\n
\n \n \n \n What is your date of birth?\n \n \n \n\n
\n For example, 31 3 1980\n
\n\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "with input attributes", - "options": { - "id": "dob-with-input-attributes", - "namePrefix": "dob-with-input-attributes", - "fieldset": { - "legend": { - "text": "What is your date of birth?" - } - }, - "hint": { - "text": "For example, 31 3 1980" - }, - "items": [ - { - "name": "day", - "classes": "govuk-input--width-2", - "attributes": { - "data-example-day": "day" - } - }, - { - "name": "month", - "classes": "govuk-input--width-2", - "attributes": { - "data-example-month": "month" - } - }, - { - "name": "year", - "classes": "govuk-input--width-4", - "attributes": { - "data-example-year": "year" - } - } - ] - }, - "html": "
\n
\n \n \n \n What is your date of birth?\n \n \n \n\n
\n For example, 31 3 1980\n
\n\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n \n\n
\n\n\n
", - "hidden": false - }, - { - "name": "classes", - "options": { - "id": "with-classes", - "classes": "app-date-input--custom-modifier" - }, - "html": "
\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n\n
", - "hidden": true - }, - { - "name": "attributes", - "options": { - "id": "with-attributes", - "attributes": { - "data-attribute": "my data value" - } - }, - "html": "
\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n\n
", - "hidden": true - }, - { - "name": "with empty items", - "options": { - "id": "with-empty-items", - "items": [] - }, - "html": "
\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n\n
", - "hidden": true - }, - { - "name": "custom pattern", - "options": { - "id": "with-custom-pattern", - "items": [ - { - "name": "day", - "pattern": "[0-8]*" - } - ] - }, - "html": "
\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n\n
", - "hidden": true - }, - { - "name": "custom inputmode", - "options": { - "id": "with-custom-inputmode", - "items": [ - { - "name": "day", - "pattern": "[0-9X]*", - "inputmode": "text" - } - ] - }, - "html": "
\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n\n
", - "hidden": true - }, - { - "name": "with nested name", - "options": { - "id": "with-nested-name", - "items": [ - { - "name": "day[dd]" - }, - { - "name": "month[mm]" - }, - { - "name": "year[yyyy]" - } - ] - }, - "html": "
\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n\n
", - "hidden": true - }, - { - "name": "with id on items", - "options": { - "id": "with-item-id", - "items": [ - { - "id": "day", - "name": "day" - }, - { - "id": "month", - "name": "month" - }, - { - "id": "year", - "name": "year" - } - ] - }, - "html": "
\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n\n
", - "hidden": true - }, - { - "name": "suffixed id", - "options": { - "id": "my-date-input", - "items": [ - { - "name": "day" - }, - { - "name": "month" - }, - { - "name": "year" - } - ] - }, - "html": "
\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n\n
", - "hidden": true - }, - { - "name": "with values", - "options": { - "id": "with-values", - "items": [ - { - "id": "day", - "name": "day" - }, - { - "id": "month", - "name": "month" - }, - { - "id": "year", - "name": "year", - "value": 2018 - } - ] - }, - "html": "
\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n\n
", - "hidden": true - }, - { - "name": "with hint and describedBy", - "options": { - "id": "dob-errors", - "fieldset": { - "describedBy": "some-id", - "legend": { - "text": "What is your date of birth?" - } - }, - "hint": { - "text": "For example, 31 3 1980" - } - }, - "html": "
\n
\n \n \n \n What is your date of birth?\n \n \n \n\n
\n For example, 31 3 1980\n
\n\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n \n\n
\n\n\n
", - "hidden": true - }, - { - "name": "with error and describedBy", - "options": { - "id": "dob-errors", - "fieldset": { - "describedBy": "some-id", - "legend": { - "text": "What is your date of birth?" - } - }, - "errorMessage": { - "text": "Error message goes here" - } - }, - "html": "
\n
\n \n \n \n What is your date of birth?\n \n \n \n\n \n Error: Error message goes here\n \n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n \n\n
\n\n\n
", - "hidden": true - }, - { - "name": "fieldset html", - "options": { - "id": "with-fieldset-html", - "fieldset": { - "legend": { - "html": "What is your date of birth?" - } - } - }, - "html": "
\n
\n \n \n \n What is your date of birth?\n \n \n \n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n \n\n
\n\n\n
", - "hidden": true - }, - { - "name": "items with classes", - "options": { - "id": "with-item-classes", - "items": [ - { - "name": "day", - "classes": "app-date-input__day" - }, - { - "name": "month", - "classes": "app-date-input__month" - }, - { - "name": "year", - "classes": "app-date-input__year" - } - ] - }, - "html": "
\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n\n
", - "hidden": true - }, - { - "name": "items without classes", - "options": { - "id": "without-item-classes", - "items": [ - { - "name": "day" - }, - { - "name": "month" - }, - { - "name": "year" - } - ] - }, - "html": "
\n\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n \n
\n
\n \n
\n\n
", - "hidden": true - } - ] -} \ No newline at end of file diff --git a/package/govuk/components/date-input/macro-options.json b/package/govuk/components/date-input/macro-options.json deleted file mode 100644 index 5d59257db1..0000000000 --- a/package/govuk/components/date-input/macro-options.json +++ /dev/null @@ -1,117 +0,0 @@ -[ - { - "name": "id", - "type": "string", - "required": true, - "description": "This is used for the main component and to compose id attribute for each item." - }, - { - "name": "namePrefix", - "type": "string", - "required": false, - "description": "Optional prefix. This is used to prefix each `item.name` using `-`." - }, - { - "name": "items", - "type": "array", - "required": false, - "description": "An array of input objects with name, value and classes.", - "params": [ - { - "name": "id", - "type": "string", - "required": false, - "description": "Item-specific id. If provided, it will be used instead of the generated id." - }, - { - "name": "name", - "type": "string", - "required": true, - "description": "Item-specific name attribute." - }, - { - "name": "label", - "type": "string", - "required": false, - "description": "Item-specific label text. If provided, this will be used instead of `name` for item label text." - }, - { - "name": "value", - "type": "string", - "required": false, - "description": "If provided, it will be used as the initial value of the input." - }, - { - "name": "autocomplete", - "type": "string", - "required": false, - "description": "Attribute to [identify input purpose](https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose.html), for instance \"bday-day\". See [autofill](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill) for full list of attributes that can be used." - }, - { - "name": "pattern", - "type": "string", - "required": false, - "description": "Attribute to [provide a regular expression pattern](https://www.w3.org/TR/html51/sec-forms.html#the-pattern-attribute), used to match allowed character combinations for the input value." - }, - { - "name": "classes", - "type": "string", - "required": false, - "description": "Classes to add to date input item." - }, - { - "name": "attributes", - "type": "object", - "required": false, - "description": "HTML attributes (for example data attributes) to add to the date input tag." - } - ] - }, - { - "name": "hint", - "type": "object", - "required": false, - "description": "Options for the hint component.", - "isComponent": true - }, - { - "name": "errorMessage", - "type": "object", - "required": false, - "description": "Options for the error message component. The error message component will not display if you use a falsy value for `errorMessage`, for example `false` or `null`.", - "isComponent": true - }, - { - "name": "formGroup", - "type": "object", - "required": false, - "description": "Options for the form-group wrapper", - "params": [ - { - "name": "classes", - "type": "string", - "required": false, - "description": "Classes to add to the form group (e.g. to show error state for the whole group)" - } - ] - }, - { - "name": "fieldset", - "type": "object", - "required": false, - "description": "Options for the fieldset component (e.g. legend).", - "isComponent": true - }, - { - "name": "classes", - "type": "string", - "required": false, - "description": "Classes to add to the date-input container." - }, - { - "name": "attributes", - "type": "object", - "required": false, - "description": "HTML attributes (for example data attributes) to add to the date-input container." - } -] \ No newline at end of file diff --git a/package/govuk/components/date-input/template.njk b/package/govuk/components/date-input/template.njk deleted file mode 100644 index c2896756c7..0000000000 --- a/package/govuk/components/date-input/template.njk +++ /dev/null @@ -1,97 +0,0 @@ -{% from "../error-message/macro.njk" import govukErrorMessage -%} -{% from "../fieldset/macro.njk" import govukFieldset %} -{% from "../hint/macro.njk" import govukHint %} -{% from "../input/macro.njk" import govukInput %} - -{#- a record of other elements that we need to associate with the input using - aria-describedby โ€“ for example hints or error messages -#} -{% set describedBy = params.fieldset.describedBy if params.fieldset.describedBy else "" %} - -{% if params.items | length %} - {% set dateInputItems = params.items %} -{% else %} - {% set dateInputItems = [ - { - name: "day", - classes: "govuk-input--width-2" - }, - { - name: "month", - classes: "govuk-input--width-2" - }, - { - name: "year", - classes: "govuk-input--width-4" - } - ] %} -{% endif %} - -{#- Capture the HTML so we can optionally nest it in a fieldset -#} -{% set innerHtml %} -{% if params.hint %} - {% set hintId = params.id + "-hint" %} - {% set describedBy = describedBy + " " + hintId if describedBy else hintId %} - {{ govukHint({ - id: hintId, - classes: params.hint.classes, - attributes: params.hint.attributes, - html: params.hint.html, - text: params.hint.text - }) | indent(2) | trim }} -{% endif %} -{% if params.errorMessage %} - {% set errorId = params.id + "-error" %} - {% set describedBy = describedBy + " " + errorId if describedBy else errorId %} - {{ govukErrorMessage({ - id: errorId, - classes: params.errorMessage.classes, - attributes: params.errorMessage.attributes, - html: params.errorMessage.html, - text: params.errorMessage.text, - visuallyHiddenText: params.errorMessage.visuallyHiddenText - }) | indent(2) | trim }} -{% endif %} -
- {% for item in dateInputItems %} -
- {{ govukInput({ - label: { - text: item.label if item.label else item.name | capitalize, - classes: "govuk-date-input__label" - }, - id: item.id if item.id else (params.id + "-" + item.name), - classes: "govuk-date-input__input " + (item.classes if item.classes), - name: (params.namePrefix + "-" + item.name) if params.namePrefix else item.name, - value: item.value, - type: "text", - inputmode: item.inputmode if item.inputmode else "numeric", - autocomplete: item.autocomplete, - pattern: item.pattern if item.pattern else "[0-9]*", - attributes: item.attributes - }) | indent(6) | trim }} -
- {% endfor %} -
-{% endset %} - -
-{% if params.fieldset %} -{#- We override the fieldset's role to 'group' because otherwise JAWS does not - announce the description for a fieldset comprised of text inputs, but - adding the role to the fieldset always makes the output overly verbose for - radio buttons or checkboxes. -#} - {% call govukFieldset({ - describedBy: describedBy, - classes: params.fieldset.classes, - role: 'group', - attributes: params.fieldset.attributes, - legend: params.fieldset.legend - }) %} - {{ innerHtml | trim | safe }} - {% endcall %} -{% else %} - {{ innerHtml | trim | safe }} -{% endif %} -
diff --git a/package/govuk/components/details/_index.scss b/package/govuk/components/details/_index.scss deleted file mode 100644 index 81a5aa9f0c..0000000000 --- a/package/govuk/components/details/_index.scss +++ /dev/null @@ -1,83 +0,0 @@ -@include govuk-exports("govuk/component/details") { - .govuk-details { - @include govuk-font($size: 19); - @include govuk-text-colour; - @include govuk-responsive-margin(6, "bottom"); - - display: block; - } - - .govuk-details__summary { - // Make the focus outline shrink-wrap the text content of the summary - display: inline-block; - - // Absolutely position the marker against this element - position: relative; - - margin-bottom: govuk-spacing(1); - - // Allow for absolutely positioned marker and align with disclosed text - padding-left: govuk-spacing(4) + $govuk-border-width; - - // Style the summary to look like a link... - color: $govuk-link-colour; - cursor: pointer; - - &:hover { - color: $govuk-link-hover-colour; - } - - &:focus { - @include govuk-focused-text; - } - } - - // ...but only underline the text, not the arrow - .govuk-details__summary-text { - text-decoration: underline; - } - - // Remove the underline when focussed to avoid duplicate borders - .govuk-details__summary:focus .govuk-details__summary-text { - text-decoration: none; - } - - // Remove the default details marker so we can style our own consistently and - // ensure it displays in Firefox (see implementation.md for details) - .govuk-details__summary::-webkit-details-marker { - display: none; - } - - // Append our own open / closed marker using a pseudo-element - .govuk-details__summary:before { - content: ""; - position: absolute; - - top: -1px; - bottom: 0; - left: 0; - - margin: auto; - - @include govuk-shape-arrow($direction: right, $base: 14px); - - .govuk-details[open] > & { - @include govuk-shape-arrow($direction: down, $base: 14px); - } - } - - .govuk-details__text { - padding: govuk-spacing(3); - padding-left: govuk-spacing(4); - border-left: $govuk-border-width solid $govuk-border-colour; - } - - .govuk-details__text p { - margin-top: 0; - margin-bottom: govuk-spacing(4); - } - - .govuk-details__text > :last-child { - margin-bottom: 0; - } -} diff --git a/package/govuk/components/details/details.js b/package/govuk/components/details/details.js deleted file mode 100644 index af04eee4c1..0000000000 --- a/package/govuk/components/details/details.js +++ /dev/null @@ -1,828 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define('GOVUKFrontend', factory) : - (global.GOVUKFrontend = factory()); -}(this, (function () { 'use strict'; - -(function(undefined) { - -// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Object/defineProperty/detect.js -var detect = ( - // In IE8, defineProperty could only act on DOM elements, so full support - // for the feature requires the ability to set a property on an arbitrary object - 'defineProperty' in Object && (function() { - try { - var a = {}; - Object.defineProperty(a, 'test', {value:42}); - return true; - } catch(e) { - return false - } - }()) -); - -if (detect) return - -// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Object.defineProperty&flags=always -(function (nativeDefineProperty) { - - var supportsAccessors = Object.prototype.hasOwnProperty('__defineGetter__'); - var ERR_ACCESSORS_NOT_SUPPORTED = 'Getters & setters cannot be defined on this javascript engine'; - var ERR_VALUE_ACCESSORS = 'A property cannot both have accessors and be writable or have a value'; - - Object.defineProperty = function defineProperty(object, property, descriptor) { - - // Where native support exists, assume it - if (nativeDefineProperty && (object === window || object === document || object === Element.prototype || object instanceof Element)) { - return nativeDefineProperty(object, property, descriptor); - } - - if (object === null || !(object instanceof Object || typeof object === 'object')) { - throw new TypeError('Object.defineProperty called on non-object'); - } - - if (!(descriptor instanceof Object)) { - throw new TypeError('Property description must be an object'); - } - - var propertyString = String(property); - var hasValueOrWritable = 'value' in descriptor || 'writable' in descriptor; - var getterType = 'get' in descriptor && typeof descriptor.get; - var setterType = 'set' in descriptor && typeof descriptor.set; - - // handle descriptor.get - if (getterType) { - if (getterType !== 'function') { - throw new TypeError('Getter must be a function'); - } - if (!supportsAccessors) { - throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); - } - if (hasValueOrWritable) { - throw new TypeError(ERR_VALUE_ACCESSORS); - } - Object.__defineGetter__.call(object, propertyString, descriptor.get); - } else { - object[propertyString] = descriptor.value; - } - - // handle descriptor.set - if (setterType) { - if (setterType !== 'function') { - throw new TypeError('Setter must be a function'); - } - if (!supportsAccessors) { - throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); - } - if (hasValueOrWritable) { - throw new TypeError(ERR_VALUE_ACCESSORS); - } - Object.__defineSetter__.call(object, propertyString, descriptor.set); - } - - // OK to define value unconditionally - if a getter has been specified as well, an error would be thrown above - if ('value' in descriptor) { - object[propertyString] = descriptor.value; - } - - return object; - }; -}(Object.defineProperty)); -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - // Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Function/prototype/bind/detect.js - var detect = 'bind' in Function.prototype; - - if (detect) return - - // Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Function.prototype.bind&flags=always - Object.defineProperty(Function.prototype, 'bind', { - value: function bind(that) { // .length is 1 - // add necessary es5-shim utilities - var $Array = Array; - var $Object = Object; - var ObjectPrototype = $Object.prototype; - var ArrayPrototype = $Array.prototype; - var Empty = function Empty() {}; - var to_string = ObjectPrototype.toString; - var hasToStringTag = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'; - var isCallable; /* inlined from https://npmjs.com/is-callable */ var fnToStr = Function.prototype.toString, tryFunctionObject = function tryFunctionObject(value) { try { fnToStr.call(value); return true; } catch (e) { return false; } }, fnClass = '[object Function]', genClass = '[object GeneratorFunction]'; isCallable = function isCallable(value) { if (typeof value !== 'function') { return false; } if (hasToStringTag) { return tryFunctionObject(value); } var strClass = to_string.call(value); return strClass === fnClass || strClass === genClass; }; - var array_slice = ArrayPrototype.slice; - var array_concat = ArrayPrototype.concat; - var array_push = ArrayPrototype.push; - var max = Math.max; - // /add necessary es5-shim utilities - - // 1. Let Target be the this value. - var target = this; - // 2. If IsCallable(Target) is false, throw a TypeError exception. - if (!isCallable(target)) { - throw new TypeError('Function.prototype.bind called on incompatible ' + target); - } - // 3. Let A be a new (possibly empty) internal list of all of the - // argument values provided after thisArg (arg1, arg2 etc), in order. - // XXX slicedArgs will stand in for "A" if used - var args = array_slice.call(arguments, 1); // for normal call - // 4. Let F be a new native ECMAScript object. - // 11. Set the [[Prototype]] internal property of F to the standard - // built-in Function prototype object as specified in 15.3.3.1. - // 12. Set the [[Call]] internal property of F as described in - // 15.3.4.5.1. - // 13. Set the [[Construct]] internal property of F as described in - // 15.3.4.5.2. - // 14. Set the [[HasInstance]] internal property of F as described in - // 15.3.4.5.3. - var bound; - var binder = function () { - - if (this instanceof bound) { - // 15.3.4.5.2 [[Construct]] - // When the [[Construct]] internal method of a function object, - // F that was created using the bind function is called with a - // list of arguments ExtraArgs, the following steps are taken: - // 1. Let target be the value of F's [[TargetFunction]] - // internal property. - // 2. If target has no [[Construct]] internal method, a - // TypeError exception is thrown. - // 3. Let boundArgs be the value of F's [[BoundArgs]] internal - // property. - // 4. Let args be a new list containing the same values as the - // list boundArgs in the same order followed by the same - // values as the list ExtraArgs in the same order. - // 5. Return the result of calling the [[Construct]] internal - // method of target providing args as the arguments. - - var result = target.apply( - this, - array_concat.call(args, array_slice.call(arguments)) - ); - if ($Object(result) === result) { - return result; - } - return this; - - } else { - // 15.3.4.5.1 [[Call]] - // When the [[Call]] internal method of a function object, F, - // which was created using the bind function is called with a - // this value and a list of arguments ExtraArgs, the following - // steps are taken: - // 1. Let boundArgs be the value of F's [[BoundArgs]] internal - // property. - // 2. Let boundThis be the value of F's [[BoundThis]] internal - // property. - // 3. Let target be the value of F's [[TargetFunction]] internal - // property. - // 4. Let args be a new list containing the same values as the - // list boundArgs in the same order followed by the same - // values as the list ExtraArgs in the same order. - // 5. Return the result of calling the [[Call]] internal method - // of target providing boundThis as the this value and - // providing args as the arguments. - - // equiv: target.call(this, ...boundArgs, ...args) - return target.apply( - that, - array_concat.call(args, array_slice.call(arguments)) - ); - - } - - }; - - // 15. If the [[Class]] internal property of Target is "Function", then - // a. Let L be the length property of Target minus the length of A. - // b. Set the length own property of F to either 0 or L, whichever is - // larger. - // 16. Else set the length own property of F to 0. - - var boundLength = max(0, target.length - args.length); - - // 17. Set the attributes of the length own property of F to the values - // specified in 15.3.5.1. - var boundArgs = []; - for (var i = 0; i < boundLength; i++) { - array_push.call(boundArgs, '$' + i); - } - - // XXX Build a dynamic function with desired amount of arguments is the only - // way to set the length property of a function. - // In environments where Content Security Policies enabled (Chrome extensions, - // for ex.) all use of eval or Function costructor throws an exception. - // However in all of these environments Function.prototype.bind exists - // and so this code will never be executed. - bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this, arguments); }')(binder); - - if (target.prototype) { - Empty.prototype = target.prototype; - bound.prototype = new Empty(); - // Clean up dangling references. - Empty.prototype = null; - } - - // TODO - // 18. Set the [[Extensible]] internal property of F to true. - - // TODO - // 19. Let thrower be the [[ThrowTypeError]] function Object (13.2.3). - // 20. Call the [[DefineOwnProperty]] internal method of F with - // arguments "caller", PropertyDescriptor {[[Get]]: thrower, [[Set]]: - // thrower, [[Enumerable]]: false, [[Configurable]]: false}, and - // false. - // 21. Call the [[DefineOwnProperty]] internal method of F with - // arguments "arguments", PropertyDescriptor {[[Get]]: thrower, - // [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false}, - // and false. - - // TODO - // NOTE Function objects created using Function.prototype.bind do not - // have a prototype property or the [[Code]], [[FormalParameters]], and - // [[Scope]] internal properties. - // XXX can't delete prototype in pure-js. - - // 22. Return F. - return bound; - } - }); -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - -// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Window/detect.js -var detect = ('Window' in this); - -if (detect) return - -// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Window&flags=always -if ((typeof WorkerGlobalScope === "undefined") && (typeof importScripts !== "function")) { - (function (global) { - if (global.constructor) { - global.Window = global.constructor; - } else { - (global.Window = global.constructor = new Function('return function Window() {}')()).prototype = this; - } - }(this)); -} - -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - -// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Document/detect.js -var detect = ("Document" in this); - -if (detect) return - -// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Document&flags=always -if ((typeof WorkerGlobalScope === "undefined") && (typeof importScripts !== "function")) { - - if (this.HTMLDocument) { // IE8 - - // HTMLDocument is an extension of Document. If the browser has HTMLDocument but not Document, the former will suffice as an alias for the latter. - this.Document = this.HTMLDocument; - - } else { - - // Create an empty function to act as the missing constructor for the document object, attach the document object as its prototype. The function needs to be anonymous else it is hoisted and causes the feature detect to prematurely pass, preventing the assignments below being made. - this.Document = this.HTMLDocument = document.constructor = (new Function('return function Document() {}')()); - this.Document.prototype = document; - } -} - - -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - -// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Element/detect.js -var detect = ('Element' in this && 'HTMLElement' in this); - -if (detect) return - -// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Element&flags=always -(function () { - - // IE8 - if (window.Element && !window.HTMLElement) { - window.HTMLElement = window.Element; - return; - } - - // create Element constructor - window.Element = window.HTMLElement = new Function('return function Element() {}')(); - - // generate sandboxed iframe - var vbody = document.appendChild(document.createElement('body')); - var frame = vbody.appendChild(document.createElement('iframe')); - - // use sandboxed iframe to replicate Element functionality - var frameDocument = frame.contentWindow.document; - var prototype = Element.prototype = frameDocument.appendChild(frameDocument.createElement('*')); - var cache = {}; - - // polyfill Element.prototype on an element - var shiv = function (element, deep) { - var - childNodes = element.childNodes || [], - index = -1, - key, value, childNode; - - if (element.nodeType === 1 && element.constructor !== Element) { - element.constructor = Element; - - for (key in cache) { - value = cache[key]; - element[key] = value; - } - } - - while (childNode = deep && childNodes[++index]) { - shiv(childNode, deep); - } - - return element; - }; - - var elements = document.getElementsByTagName('*'); - var nativeCreateElement = document.createElement; - var interval; - var loopLimit = 100; - - prototype.attachEvent('onpropertychange', function (event) { - var - propertyName = event.propertyName, - nonValue = !cache.hasOwnProperty(propertyName), - newValue = prototype[propertyName], - oldValue = cache[propertyName], - index = -1, - element; - - while (element = elements[++index]) { - if (element.nodeType === 1) { - if (nonValue || element[propertyName] === oldValue) { - element[propertyName] = newValue; - } - } - } - - cache[propertyName] = newValue; - }); - - prototype.constructor = Element; - - if (!prototype.hasAttribute) { - // .hasAttribute - prototype.hasAttribute = function hasAttribute(name) { - return this.getAttribute(name) !== null; - }; - } - - // Apply Element prototype to the pre-existing DOM as soon as the body element appears. - function bodyCheck() { - if (!(loopLimit--)) clearTimeout(interval); - if (document.body && !document.body.prototype && /(complete|interactive)/.test(document.readyState)) { - shiv(document, true); - if (interval && document.body.prototype) clearTimeout(interval); - return (!!document.body.prototype); - } - return false; - } - if (!bodyCheck()) { - document.onreadystatechange = bodyCheck; - interval = setInterval(bodyCheck, 25); - } - - // Apply to any new elements created after load - document.createElement = function createElement(nodeName) { - var element = nativeCreateElement(String(nodeName).toLowerCase()); - return shiv(element); - }; - - // remove sandboxed iframe - document.removeChild(vbody); -}()); - -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - -// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Event/detect.js -var detect = ( - (function(global) { - - if (!('Event' in global)) return false; - if (typeof global.Event === 'function') return true; - - try { - - // In IE 9-11, the Event object exists but cannot be instantiated - new Event('click'); - return true; - } catch(e) { - return false; - } - }(this)) -); - -if (detect) return - -// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Event&flags=always -(function () { - var unlistenableWindowEvents = { - click: 1, - dblclick: 1, - keyup: 1, - keypress: 1, - keydown: 1, - mousedown: 1, - mouseup: 1, - mousemove: 1, - mouseover: 1, - mouseenter: 1, - mouseleave: 1, - mouseout: 1, - storage: 1, - storagecommit: 1, - textinput: 1 - }; - - // This polyfill depends on availability of `document` so will not run in a worker - // However, we asssume there are no browsers with worker support that lack proper - // support for `Event` within the worker - if (typeof document === 'undefined' || typeof window === 'undefined') return; - - function indexOf(array, element) { - var - index = -1, - length = array.length; - - while (++index < length) { - if (index in array && array[index] === element) { - return index; - } - } - - return -1; - } - - var existingProto = (window.Event && window.Event.prototype) || null; - window.Event = Window.prototype.Event = function Event(type, eventInitDict) { - if (!type) { - throw new Error('Not enough arguments'); - } - - var event; - // Shortcut if browser supports createEvent - if ('createEvent' in document) { - event = document.createEvent('Event'); - var bubbles = eventInitDict && eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false; - var cancelable = eventInitDict && eventInitDict.cancelable !== undefined ? eventInitDict.cancelable : false; - - event.initEvent(type, bubbles, cancelable); - - return event; - } - - event = document.createEventObject(); - - event.type = type; - event.bubbles = eventInitDict && eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false; - event.cancelable = eventInitDict && eventInitDict.cancelable !== undefined ? eventInitDict.cancelable : false; - - return event; - }; - if (existingProto) { - Object.defineProperty(window.Event, 'prototype', { - configurable: false, - enumerable: false, - writable: true, - value: existingProto - }); - } - - if (!('createEvent' in document)) { - window.addEventListener = Window.prototype.addEventListener = Document.prototype.addEventListener = Element.prototype.addEventListener = function addEventListener() { - var - element = this, - type = arguments[0], - listener = arguments[1]; - - if (element === window && type in unlistenableWindowEvents) { - throw new Error('In IE8 the event: ' + type + ' is not available on the window object. Please see https://github.com/Financial-Times/polyfill-service/issues/317 for more information.'); - } - - if (!element._events) { - element._events = {}; - } - - if (!element._events[type]) { - element._events[type] = function (event) { - var - list = element._events[event.type].list, - events = list.slice(), - index = -1, - length = events.length, - eventElement; - - event.preventDefault = function preventDefault() { - if (event.cancelable !== false) { - event.returnValue = false; - } - }; - - event.stopPropagation = function stopPropagation() { - event.cancelBubble = true; - }; - - event.stopImmediatePropagation = function stopImmediatePropagation() { - event.cancelBubble = true; - event.cancelImmediate = true; - }; - - event.currentTarget = element; - event.relatedTarget = event.fromElement || null; - event.target = event.target || event.srcElement || element; - event.timeStamp = new Date().getTime(); - - if (event.clientX) { - event.pageX = event.clientX + document.documentElement.scrollLeft; - event.pageY = event.clientY + document.documentElement.scrollTop; - } - - while (++index < length && !event.cancelImmediate) { - if (index in events) { - eventElement = events[index]; - - if (indexOf(list, eventElement) !== -1 && typeof eventElement === 'function') { - eventElement.call(element, event); - } - } - } - }; - - element._events[type].list = []; - - if (element.attachEvent) { - element.attachEvent('on' + type, element._events[type]); - } - } - - element._events[type].list.push(listener); - }; - - window.removeEventListener = Window.prototype.removeEventListener = Document.prototype.removeEventListener = Element.prototype.removeEventListener = function removeEventListener() { - var - element = this, - type = arguments[0], - listener = arguments[1], - index; - - if (element._events && element._events[type] && element._events[type].list) { - index = indexOf(element._events[type].list, listener); - - if (index !== -1) { - element._events[type].list.splice(index, 1); - - if (!element._events[type].list.length) { - if (element.detachEvent) { - element.detachEvent('on' + type, element._events[type]); - } - delete element._events[type]; - } - } - } - }; - - window.dispatchEvent = Window.prototype.dispatchEvent = Document.prototype.dispatchEvent = Element.prototype.dispatchEvent = function dispatchEvent(event) { - if (!arguments.length) { - throw new Error('Not enough arguments'); - } - - if (!event || typeof event.type !== 'string') { - throw new Error('DOM Events Exception 0'); - } - - var element = this, type = event.type; - - try { - if (!event.bubbles) { - event.cancelBubble = true; - - var cancelBubbleEvent = function (event) { - event.cancelBubble = true; - - (element || window).detachEvent('on' + type, cancelBubbleEvent); - }; - - this.attachEvent('on' + type, cancelBubbleEvent); - } - - this.fireEvent('on' + type, event); - } catch (error) { - event.target = element; - - do { - event.currentTarget = element; - - if ('_events' in element && typeof element._events[type] === 'function') { - element._events[type].call(element, event); - } - - if (typeof element['on' + type] === 'function') { - element['on' + type].call(element, event); - } - - element = element.nodeType === 9 ? element.parentWindow : element.parentNode; - } while (element && !event.cancelBubble); - } - - return true; - }; - - // Add the DOMContentLoaded Event - document.attachEvent('onreadystatechange', function() { - if (document.readyState === 'complete') { - document.dispatchEvent(new Event('DOMContentLoaded', { - bubbles: true - })); - } - }); - } -}()); - -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -/** - * TODO: Ideally this would be a NodeList.prototype.forEach polyfill - * This seems to fail in IE8, requires more investigation. - * See: https://github.com/imagitama/nodelist-foreach-polyfill - */ - -// Used to generate a unique string, allows multiple instances of the component without -// Them conflicting with each other. -// https://stackoverflow.com/a/8809472 -function generateUniqueID () { - var d = new Date().getTime(); - if (typeof window.performance !== 'undefined' && typeof window.performance.now === 'function') { - d += window.performance.now(); // use high-precision timer if available - } - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - var r = (d + Math.random() * 16) % 16 | 0; - d = Math.floor(d / 16); - return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16) - }) -} - -/** - * JavaScript 'polyfill' for HTML5's
and elements - * and 'shim' to add accessiblity enhancements for all browsers - * - * http://caniuse.com/#feat=details - */ - -var KEY_ENTER = 13; -var KEY_SPACE = 32; - -function Details ($module) { - this.$module = $module; -} - -Details.prototype.init = function () { - if (!this.$module) { - return - } - - // If there is native details support, we want to avoid running code to polyfill native behaviour. - var hasNativeDetails = typeof this.$module.open === 'boolean'; - - if (hasNativeDetails) { - return - } - - this.polyfillDetails(); -}; - -Details.prototype.polyfillDetails = function () { - var $module = this.$module; - - // Save shortcuts to the inner summary and content elements - var $summary = this.$summary = $module.getElementsByTagName('summary').item(0); - var $content = this.$content = $module.getElementsByTagName('div').item(0); - - // If
doesn't have a and a
representing the content - // it means the required HTML structure is not met so the script will stop - if (!$summary || !$content) { - return - } - - // If the content doesn't have an ID, assign it one now - // which we'll need for the summary's aria-controls assignment - if (!$content.id) { - $content.id = 'details-content-' + generateUniqueID(); - } - - // Add ARIA role="group" to details - $module.setAttribute('role', 'group'); - - // Add role=button to summary - $summary.setAttribute('role', 'button'); - - // Add aria-controls - $summary.setAttribute('aria-controls', $content.id); - - // Set tabIndex so the summary is keyboard accessible for non-native elements - // - // We have to use the camelcase `tabIndex` property as there is a bug in IE6/IE7 when we set the correct attribute lowercase: - // See http://web.archive.org/web/20170120194036/http://www.saliences.com/browserBugs/tabIndex.html for more information. - $summary.tabIndex = 0; - - // Detect initial open state - var openAttr = $module.getAttribute('open') !== null; - if (openAttr === true) { - $summary.setAttribute('aria-expanded', 'true'); - $content.setAttribute('aria-hidden', 'false'); - } else { - $summary.setAttribute('aria-expanded', 'false'); - $content.setAttribute('aria-hidden', 'true'); - $content.style.display = 'none'; - } - - // Bind an event to handle summary elements - this.polyfillHandleInputs($summary, this.polyfillSetAttributes.bind(this)); -}; - -/** -* Define a statechange function that updates aria-expanded and style.display -* @param {object} summary element -*/ -Details.prototype.polyfillSetAttributes = function () { - var $module = this.$module; - var $summary = this.$summary; - var $content = this.$content; - - var expanded = $summary.getAttribute('aria-expanded') === 'true'; - var hidden = $content.getAttribute('aria-hidden') === 'true'; - - $summary.setAttribute('aria-expanded', (expanded ? 'false' : 'true')); - $content.setAttribute('aria-hidden', (hidden ? 'false' : 'true')); - - $content.style.display = (expanded ? 'none' : ''); - - var hasOpenAttr = $module.getAttribute('open') !== null; - if (!hasOpenAttr) { - $module.setAttribute('open', 'open'); - } else { - $module.removeAttribute('open'); - } - - return true -}; - -/** -* Handle cross-modal click events -* @param {object} node element -* @param {function} callback function -*/ -Details.prototype.polyfillHandleInputs = function (node, callback) { - node.addEventListener('keypress', function (event) { - var target = event.target; - // When the key gets pressed - check if it is enter or space - if (event.keyCode === KEY_ENTER || event.keyCode === KEY_SPACE) { - if (target.nodeName.toLowerCase() === 'summary') { - // Prevent space from scrolling the page - // and enter from submitting a form - event.preventDefault(); - // Click to let the click event do all the necessary action - if (target.click) { - target.click(); - } else { - // except Safari 5.1 and under don't support .click() here - callback(event); - } - } - } - }); - - // Prevent keyup to prevent clicking twice in Firefox when using space key - node.addEventListener('keyup', function (event) { - var target = event.target; - if (event.keyCode === KEY_SPACE) { - if (target.nodeName.toLowerCase() === 'summary') { - event.preventDefault(); - } - } - }); - - node.addEventListener('click', callback); -}; - -return Details; - -}))); diff --git a/package/govuk/components/details/fixtures.json b/package/govuk/components/details/fixtures.json deleted file mode 100644 index ced3994471..0000000000 --- a/package/govuk/components/details/fixtures.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "component": "details", - "fixtures": [ - { - "name": "default", - "options": { - "summaryText": "Help with nationality", - "text": "We need to know your nationality so we can work out which elections youโ€™re entitled to vote in. If you canโ€™t provide your nationality, youโ€™ll have to send copies of identity documents through the post." - }, - "html": "
\n \n \n Help with nationality\n \n \n
\n We need to know your nationality so we can work out which elections youโ€™re entitled to vote in. If you canโ€™t provide your nationality, youโ€™ll have to send copies of identity documents through the post.\n
\n
", - "hidden": false - }, - { - "name": "expanded", - "options": { - "id": "help-with-nationality", - "summaryText": "Help with nationality", - "text": "We need to know your nationality so we can work out which elections youโ€™re entitled to vote in. If you canโ€™t provide your nationality, youโ€™ll have to send copies of identity documents through the post.", - "open": true - }, - "html": "
\n \n \n Help with nationality\n \n \n
\n We need to know your nationality so we can work out which elections youโ€™re entitled to vote in. If you canโ€™t provide your nationality, youโ€™ll have to send copies of identity documents through the post.\n
\n
", - "hidden": false - }, - { - "name": "with html", - "options": { - "summaryText": "Where to find your National Insurance Number", - "html": "Your National Insurance number can be found on\n
    \n
  • your National Insurance card
  • \n
  • your payslip
  • \n
  • P60
  • \n
  • benefits information
  • \n
  • tax return
  • \n
\n" - }, - "html": "
\n \n \n Where to find your National Insurance Number\n \n \n
\n Your National Insurance number can be found on\n
    \n
  • your National Insurance card
  • \n
  • your payslip
  • \n
  • P60
  • \n
  • benefits information
  • \n
  • tax return
  • \n
\n\n
\n
", - "hidden": false - }, - { - "name": "id", - "options": { - "id": "my-details-element", - "summaryText": "Expand this section", - "text": "Here are some more details" - }, - "html": "
\n \n \n Expand this section\n \n \n
\n Here are some more details\n
\n
", - "hidden": true - }, - { - "name": "html as text", - "options": { - "summaryText": "Expand this section", - "text": "More about the greater than symbol (>)" - }, - "html": "
\n \n \n Expand this section\n \n \n
\n More about the greater than symbol (>)\n
\n
", - "hidden": true - }, - { - "name": "html", - "options": { - "summaryText": "Expand this section", - "html": "More about bold text" - }, - "html": "
\n \n \n Expand this section\n \n \n
\n More about bold text\n
\n
", - "hidden": true - }, - { - "name": "summary html as text", - "options": { - "summaryText": "The greater than symbol (>) is the best", - "text": "Here are some more details" - }, - "html": "
\n \n \n The greater than symbol (>) is the best\n \n \n
\n Here are some more details\n
\n
", - "hidden": true - }, - { - "name": "summary html", - "options": { - "summaryHtml": "Use bold text sparingly", - "text": "Here are some more details" - }, - "html": "
\n \n \n Use bold text sparingly\n \n \n
\n Here are some more details\n
\n
", - "hidden": true - }, - { - "name": "classes", - "options": { - "classes": "some-additional-class", - "text": "Here are some more details", - "summaryText": "Expand me" - }, - "html": "
\n \n \n Expand me\n \n \n
\n Here are some more details\n
\n
", - "hidden": true - }, - { - "name": "attributes", - "options": { - "text": "Here are some more details", - "summaryText": "Expand me", - "attributes": { - "data-some-data-attribute": "i-love-data", - "another-attribute": "foo" - } - }, - "html": "
\n \n \n Expand me\n \n \n
\n Here are some more details\n
\n
", - "hidden": true - } - ] -} \ No newline at end of file diff --git a/package/govuk/components/details/implementation.md b/package/govuk/components/details/implementation.md deleted file mode 100644 index 6e9f7cd1ff..0000000000 --- a/package/govuk/components/details/implementation.md +++ /dev/null @@ -1,43 +0,0 @@ -## Implementation notes - -### Marker styling - -Firefox uses display: list-item to show the arrow marker for the summary -element. - -Unfortunately we want to use `display: inline-block` to 'shrink-wrap' the focus -outline around the summary text, which means that the arrow no longer shows up. - -Previously in GOV.UK Elements we resolved this by targeting Firefox specifically -and reverting to `display: list-item`: - -``` -@-moz-document regexp('.*') { - details summary:not([tabindex]) { - // Allow duplicate properties, override the summary display property - // scss-lint:disable DuplicateProperty - display: list-item; - display: revert; - } -} -``` - -However, `@-moz-document` has been removed in Firefox nightly as of 29th Nov -2017 so with this in mind we have taken a different approach, hiding the -browser's marker and injecting and styling our own one across all browsers -instead. - -This also gives us more control over the styling of the marker, allowing us for -example to align the summary and disclosed text correctly across all browsers. - -The downside of this approach is that older browsers that require a polyfill for -the details element will display the marker even when Javascript is disabled. -Whilst this is not perfect, it is a cosmetic issue and the user will still be -able to access the disclosed content. - -For the arrows themselves, we originally tried using unicode glyphs โ€“ -specifically \25B6 (Black right-pointing triangle) and 25BC (Black down-pointing -triangle) but Android insists on substituting the the former for an emoji even -when the \00FE0E modifier is applied. Sad face. - -Hence the border-based triangles we are using today. diff --git a/package/govuk/components/details/macro-options.json b/package/govuk/components/details/macro-options.json deleted file mode 100644 index 8ad052b6d9..0000000000 --- a/package/govuk/components/details/macro-options.json +++ /dev/null @@ -1,50 +0,0 @@ -[ - { - "name": "summaryText", - "type": "string", - "required": true, - "description": "If `summmaryHtml` is set, this is not required. Text to use within the summary element (the visible part of the details element). If `summaryHtml` is provided, the `summaryText` argument will be ignored." - }, - { - "name": "summaryHtml", - "type": "string", - "required": true, - "description": "If `summmaryText` is set, this is not required. HTML to use within the summary element (the visible part of the details element). If `summaryHtml` is provided, the `summaryText` argument will be ignored." - }, - { - "name": "text", - "type": "string", - "required": true, - "description": "If `html` is set, this is not required. Text to use within the disclosed part of the details element. If `html` is provided, the `text` argument will be ignored." - }, - { - "name": "html", - "type": "string", - "required": true, - "description": "If `text` is set, this is not required. HTML to use within the disclosed part of the details element. If `html` is provided, the `text` argument will be ignored." - }, - { - "name": "id", - "type": "string", - "required": false, - "description": "Id to add to the details element." - }, - { - "name": "open", - "type": "boolean", - "required": false, - "description": "If true, details element will be expanded." - }, - { - "name": "classes", - "type": "string", - "required": false, - "description": "Classes to add to the `
` element." - }, - { - "name": "attributes", - "type": "object", - "required": false, - "description": "HTML attributes (for example data attributes) to add to the `
` element." - } -] \ No newline at end of file diff --git a/package/govuk/components/details/template.njk b/package/govuk/components/details/template.njk deleted file mode 100644 index 16baded89c..0000000000 --- a/package/govuk/components/details/template.njk +++ /dev/null @@ -1,10 +0,0 @@ -
- - - {{ params.summaryHtml | safe if params.summaryHtml else params.summaryText }} - - -
- {{ params.html | safe if params.html else params.text }} -
-
diff --git a/package/govuk/components/error-message/_index.scss b/package/govuk/components/error-message/_index.scss deleted file mode 100644 index 5cfa5394ca..0000000000 --- a/package/govuk/components/error-message/_index.scss +++ /dev/null @@ -1,11 +0,0 @@ -@include govuk-exports("govuk/component/error-message") { - .govuk-error-message { - @include govuk-font($size: 19, $weight: bold); - - display: block; - margin-bottom: govuk-spacing(3); - clear: both; - - color: $govuk-error-colour; - } -} diff --git a/package/govuk/components/error-message/fixtures.json b/package/govuk/components/error-message/fixtures.json deleted file mode 100644 index 214117cb83..0000000000 --- a/package/govuk/components/error-message/fixtures.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "component": "error-message", - "fixtures": [ - { - "name": "default", - "options": { - "text": "Error message about full name goes here" - }, - "html": "\n Error: Error message about full name goes here\n", - "hidden": false - }, - { - "name": "id", - "options": { - "id": "my-error-message-id", - "text": "This is an error message" - }, - "html": "\n Error: This is an error message\n", - "hidden": true - }, - { - "name": "classes", - "options": { - "classes": "custom-class", - "text": "This is an error message" - }, - "html": "\n Error: This is an error message\n", - "hidden": true - }, - { - "name": "html as text", - "options": { - "text": "Unexpected > in body" - }, - "html": "\n Error: Unexpected > in body\n", - "hidden": true - }, - { - "name": "html", - "options": { - "html": "Unexpected bold text in body copy" - }, - "html": "\n Error: Unexpected bold text in body copy\n", - "hidden": true - }, - { - "name": "attributes", - "options": { - "text": "This is an error message", - "attributes": { - "data-test": "attribute", - "id": "my-error-message" - } - }, - "html": "\n Error: This is an error message\n", - "hidden": true - }, - { - "name": "with visually hidden text", - "options": { - "text": "Rhowch eich enw llawn", - "visuallyHiddenText": "Gwall" - }, - "html": "\n Gwall: Rhowch eich enw llawn\n", - "hidden": true - }, - { - "name": "visually hidden text removed", - "options": { - "text": "There is an error on line 42", - "visuallyHiddenText": false - }, - "html": "\n There is an error on line 42\n", - "hidden": true - } - ] -} \ No newline at end of file diff --git a/package/govuk/components/error-message/macro-options.json b/package/govuk/components/error-message/macro-options.json deleted file mode 100644 index 86de6fa8f5..0000000000 --- a/package/govuk/components/error-message/macro-options.json +++ /dev/null @@ -1,38 +0,0 @@ -[ - { - "name": "text", - "type": "string", - "required": true, - "description": "If `html` is set, this is not required. Text to use within the error message. If `html` is provided, the `text` argument will be ignored." - }, - { - "name": "html", - "type": "string", - "required": true, - "description": "If `text` is set, this is not required. HTML to use within the error message. If `html` is provided, the `text` argument will be ignored." - }, - { - "name": "id", - "type": "string", - "required": false, - "description": "Id attribute to add to the error message span tag." - }, - { - "name": "classes", - "type": "string", - "required": false, - "description": "Classes to add to the error message span tag." - }, - { - "name": "attributes", - "type": "object", - "required": false, - "description": "HTML attributes (for example data attributes) to add to the error message span tag" - }, - { - "name": "visuallyHiddenText", - "type": "string", - "required": false, - "description": "A visually hidden prefix used before the error message. Defaults to \"Error\"." - } -] \ No newline at end of file diff --git a/package/govuk/components/error-message/template.njk b/package/govuk/components/error-message/template.njk deleted file mode 100644 index be452167f2..0000000000 --- a/package/govuk/components/error-message/template.njk +++ /dev/null @@ -1,6 +0,0 @@ -{% set visuallyHiddenText = params.visuallyHiddenText | default("Error") -%} - - - {% if visuallyHiddenText %}{{ visuallyHiddenText }}: {% endif -%} - {{ params.html | safe if params.html else params.text }} - diff --git a/package/govuk/components/error-summary/_index.scss b/package/govuk/components/error-summary/_index.scss deleted file mode 100644 index 34e7aa8688..0000000000 --- a/package/govuk/components/error-summary/_index.scss +++ /dev/null @@ -1,43 +0,0 @@ -@import "../../core/lists"; - -@include govuk-exports("govuk/component/error-summary") { - .govuk-error-summary { - @include govuk-text-colour; - @include govuk-responsive-padding(4); - @include govuk-responsive-margin(8, "bottom"); - - border: $govuk-border-width solid $govuk-error-colour; - - &:focus { - outline: $govuk-focus-width solid $govuk-focus-colour; - } - } - - .govuk-error-summary__title { - @include govuk-font($size: 24, $weight: bold); - - margin-top: 0; - @include govuk-responsive-margin(4, "bottom"); - } - - .govuk-error-summary__body { - @include govuk-font($size: 19); - - p { - margin-top: 0; - @include govuk-responsive-margin(4, "bottom"); - } - } - - // Cross-component class - adjusts styling of list component - .govuk-error-summary__list { - margin-top: 0; - margin-bottom: 0; - } - - .govuk-error-summary__list a { - @include govuk-typography-weight-bold; - @include govuk-link-common; - @include govuk-link-style-error; - } -} diff --git a/package/govuk/components/error-summary/error-summary.js b/package/govuk/components/error-summary/error-summary.js deleted file mode 100644 index a26a14a81c..0000000000 --- a/package/govuk/components/error-summary/error-summary.js +++ /dev/null @@ -1,853 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define('GOVUKFrontend', factory) : - (global.GOVUKFrontend = factory()); -}(this, (function () { 'use strict'; - -(function(undefined) { - -// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Object/defineProperty/detect.js -var detect = ( - // In IE8, defineProperty could only act on DOM elements, so full support - // for the feature requires the ability to set a property on an arbitrary object - 'defineProperty' in Object && (function() { - try { - var a = {}; - Object.defineProperty(a, 'test', {value:42}); - return true; - } catch(e) { - return false - } - }()) -); - -if (detect) return - -// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Object.defineProperty&flags=always -(function (nativeDefineProperty) { - - var supportsAccessors = Object.prototype.hasOwnProperty('__defineGetter__'); - var ERR_ACCESSORS_NOT_SUPPORTED = 'Getters & setters cannot be defined on this javascript engine'; - var ERR_VALUE_ACCESSORS = 'A property cannot both have accessors and be writable or have a value'; - - Object.defineProperty = function defineProperty(object, property, descriptor) { - - // Where native support exists, assume it - if (nativeDefineProperty && (object === window || object === document || object === Element.prototype || object instanceof Element)) { - return nativeDefineProperty(object, property, descriptor); - } - - if (object === null || !(object instanceof Object || typeof object === 'object')) { - throw new TypeError('Object.defineProperty called on non-object'); - } - - if (!(descriptor instanceof Object)) { - throw new TypeError('Property description must be an object'); - } - - var propertyString = String(property); - var hasValueOrWritable = 'value' in descriptor || 'writable' in descriptor; - var getterType = 'get' in descriptor && typeof descriptor.get; - var setterType = 'set' in descriptor && typeof descriptor.set; - - // handle descriptor.get - if (getterType) { - if (getterType !== 'function') { - throw new TypeError('Getter must be a function'); - } - if (!supportsAccessors) { - throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); - } - if (hasValueOrWritable) { - throw new TypeError(ERR_VALUE_ACCESSORS); - } - Object.__defineGetter__.call(object, propertyString, descriptor.get); - } else { - object[propertyString] = descriptor.value; - } - - // handle descriptor.set - if (setterType) { - if (setterType !== 'function') { - throw new TypeError('Setter must be a function'); - } - if (!supportsAccessors) { - throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); - } - if (hasValueOrWritable) { - throw new TypeError(ERR_VALUE_ACCESSORS); - } - Object.__defineSetter__.call(object, propertyString, descriptor.set); - } - - // OK to define value unconditionally - if a getter has been specified as well, an error would be thrown above - if ('value' in descriptor) { - object[propertyString] = descriptor.value; - } - - return object; - }; -}(Object.defineProperty)); -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - // Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Function/prototype/bind/detect.js - var detect = 'bind' in Function.prototype; - - if (detect) return - - // Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Function.prototype.bind&flags=always - Object.defineProperty(Function.prototype, 'bind', { - value: function bind(that) { // .length is 1 - // add necessary es5-shim utilities - var $Array = Array; - var $Object = Object; - var ObjectPrototype = $Object.prototype; - var ArrayPrototype = $Array.prototype; - var Empty = function Empty() {}; - var to_string = ObjectPrototype.toString; - var hasToStringTag = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'; - var isCallable; /* inlined from https://npmjs.com/is-callable */ var fnToStr = Function.prototype.toString, tryFunctionObject = function tryFunctionObject(value) { try { fnToStr.call(value); return true; } catch (e) { return false; } }, fnClass = '[object Function]', genClass = '[object GeneratorFunction]'; isCallable = function isCallable(value) { if (typeof value !== 'function') { return false; } if (hasToStringTag) { return tryFunctionObject(value); } var strClass = to_string.call(value); return strClass === fnClass || strClass === genClass; }; - var array_slice = ArrayPrototype.slice; - var array_concat = ArrayPrototype.concat; - var array_push = ArrayPrototype.push; - var max = Math.max; - // /add necessary es5-shim utilities - - // 1. Let Target be the this value. - var target = this; - // 2. If IsCallable(Target) is false, throw a TypeError exception. - if (!isCallable(target)) { - throw new TypeError('Function.prototype.bind called on incompatible ' + target); - } - // 3. Let A be a new (possibly empty) internal list of all of the - // argument values provided after thisArg (arg1, arg2 etc), in order. - // XXX slicedArgs will stand in for "A" if used - var args = array_slice.call(arguments, 1); // for normal call - // 4. Let F be a new native ECMAScript object. - // 11. Set the [[Prototype]] internal property of F to the standard - // built-in Function prototype object as specified in 15.3.3.1. - // 12. Set the [[Call]] internal property of F as described in - // 15.3.4.5.1. - // 13. Set the [[Construct]] internal property of F as described in - // 15.3.4.5.2. - // 14. Set the [[HasInstance]] internal property of F as described in - // 15.3.4.5.3. - var bound; - var binder = function () { - - if (this instanceof bound) { - // 15.3.4.5.2 [[Construct]] - // When the [[Construct]] internal method of a function object, - // F that was created using the bind function is called with a - // list of arguments ExtraArgs, the following steps are taken: - // 1. Let target be the value of F's [[TargetFunction]] - // internal property. - // 2. If target has no [[Construct]] internal method, a - // TypeError exception is thrown. - // 3. Let boundArgs be the value of F's [[BoundArgs]] internal - // property. - // 4. Let args be a new list containing the same values as the - // list boundArgs in the same order followed by the same - // values as the list ExtraArgs in the same order. - // 5. Return the result of calling the [[Construct]] internal - // method of target providing args as the arguments. - - var result = target.apply( - this, - array_concat.call(args, array_slice.call(arguments)) - ); - if ($Object(result) === result) { - return result; - } - return this; - - } else { - // 15.3.4.5.1 [[Call]] - // When the [[Call]] internal method of a function object, F, - // which was created using the bind function is called with a - // this value and a list of arguments ExtraArgs, the following - // steps are taken: - // 1. Let boundArgs be the value of F's [[BoundArgs]] internal - // property. - // 2. Let boundThis be the value of F's [[BoundThis]] internal - // property. - // 3. Let target be the value of F's [[TargetFunction]] internal - // property. - // 4. Let args be a new list containing the same values as the - // list boundArgs in the same order followed by the same - // values as the list ExtraArgs in the same order. - // 5. Return the result of calling the [[Call]] internal method - // of target providing boundThis as the this value and - // providing args as the arguments. - - // equiv: target.call(this, ...boundArgs, ...args) - return target.apply( - that, - array_concat.call(args, array_slice.call(arguments)) - ); - - } - - }; - - // 15. If the [[Class]] internal property of Target is "Function", then - // a. Let L be the length property of Target minus the length of A. - // b. Set the length own property of F to either 0 or L, whichever is - // larger. - // 16. Else set the length own property of F to 0. - - var boundLength = max(0, target.length - args.length); - - // 17. Set the attributes of the length own property of F to the values - // specified in 15.3.5.1. - var boundArgs = []; - for (var i = 0; i < boundLength; i++) { - array_push.call(boundArgs, '$' + i); - } - - // XXX Build a dynamic function with desired amount of arguments is the only - // way to set the length property of a function. - // In environments where Content Security Policies enabled (Chrome extensions, - // for ex.) all use of eval or Function costructor throws an exception. - // However in all of these environments Function.prototype.bind exists - // and so this code will never be executed. - bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this, arguments); }')(binder); - - if (target.prototype) { - Empty.prototype = target.prototype; - bound.prototype = new Empty(); - // Clean up dangling references. - Empty.prototype = null; - } - - // TODO - // 18. Set the [[Extensible]] internal property of F to true. - - // TODO - // 19. Let thrower be the [[ThrowTypeError]] function Object (13.2.3). - // 20. Call the [[DefineOwnProperty]] internal method of F with - // arguments "caller", PropertyDescriptor {[[Get]]: thrower, [[Set]]: - // thrower, [[Enumerable]]: false, [[Configurable]]: false}, and - // false. - // 21. Call the [[DefineOwnProperty]] internal method of F with - // arguments "arguments", PropertyDescriptor {[[Get]]: thrower, - // [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false}, - // and false. - - // TODO - // NOTE Function objects created using Function.prototype.bind do not - // have a prototype property or the [[Code]], [[FormalParameters]], and - // [[Scope]] internal properties. - // XXX can't delete prototype in pure-js. - - // 22. Return F. - return bound; - } - }); -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - -// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Window/detect.js -var detect = ('Window' in this); - -if (detect) return - -// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Window&flags=always -if ((typeof WorkerGlobalScope === "undefined") && (typeof importScripts !== "function")) { - (function (global) { - if (global.constructor) { - global.Window = global.constructor; - } else { - (global.Window = global.constructor = new Function('return function Window() {}')()).prototype = this; - } - }(this)); -} - -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - -// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Document/detect.js -var detect = ("Document" in this); - -if (detect) return - -// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Document&flags=always -if ((typeof WorkerGlobalScope === "undefined") && (typeof importScripts !== "function")) { - - if (this.HTMLDocument) { // IE8 - - // HTMLDocument is an extension of Document. If the browser has HTMLDocument but not Document, the former will suffice as an alias for the latter. - this.Document = this.HTMLDocument; - - } else { - - // Create an empty function to act as the missing constructor for the document object, attach the document object as its prototype. The function needs to be anonymous else it is hoisted and causes the feature detect to prematurely pass, preventing the assignments below being made. - this.Document = this.HTMLDocument = document.constructor = (new Function('return function Document() {}')()); - this.Document.prototype = document; - } -} - - -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - -// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Element/detect.js -var detect = ('Element' in this && 'HTMLElement' in this); - -if (detect) return - -// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Element&flags=always -(function () { - - // IE8 - if (window.Element && !window.HTMLElement) { - window.HTMLElement = window.Element; - return; - } - - // create Element constructor - window.Element = window.HTMLElement = new Function('return function Element() {}')(); - - // generate sandboxed iframe - var vbody = document.appendChild(document.createElement('body')); - var frame = vbody.appendChild(document.createElement('iframe')); - - // use sandboxed iframe to replicate Element functionality - var frameDocument = frame.contentWindow.document; - var prototype = Element.prototype = frameDocument.appendChild(frameDocument.createElement('*')); - var cache = {}; - - // polyfill Element.prototype on an element - var shiv = function (element, deep) { - var - childNodes = element.childNodes || [], - index = -1, - key, value, childNode; - - if (element.nodeType === 1 && element.constructor !== Element) { - element.constructor = Element; - - for (key in cache) { - value = cache[key]; - element[key] = value; - } - } - - while (childNode = deep && childNodes[++index]) { - shiv(childNode, deep); - } - - return element; - }; - - var elements = document.getElementsByTagName('*'); - var nativeCreateElement = document.createElement; - var interval; - var loopLimit = 100; - - prototype.attachEvent('onpropertychange', function (event) { - var - propertyName = event.propertyName, - nonValue = !cache.hasOwnProperty(propertyName), - newValue = prototype[propertyName], - oldValue = cache[propertyName], - index = -1, - element; - - while (element = elements[++index]) { - if (element.nodeType === 1) { - if (nonValue || element[propertyName] === oldValue) { - element[propertyName] = newValue; - } - } - } - - cache[propertyName] = newValue; - }); - - prototype.constructor = Element; - - if (!prototype.hasAttribute) { - // .hasAttribute - prototype.hasAttribute = function hasAttribute(name) { - return this.getAttribute(name) !== null; - }; - } - - // Apply Element prototype to the pre-existing DOM as soon as the body element appears. - function bodyCheck() { - if (!(loopLimit--)) clearTimeout(interval); - if (document.body && !document.body.prototype && /(complete|interactive)/.test(document.readyState)) { - shiv(document, true); - if (interval && document.body.prototype) clearTimeout(interval); - return (!!document.body.prototype); - } - return false; - } - if (!bodyCheck()) { - document.onreadystatechange = bodyCheck; - interval = setInterval(bodyCheck, 25); - } - - // Apply to any new elements created after load - document.createElement = function createElement(nodeName) { - var element = nativeCreateElement(String(nodeName).toLowerCase()); - return shiv(element); - }; - - // remove sandboxed iframe - document.removeChild(vbody); -}()); - -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - -// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Event/detect.js -var detect = ( - (function(global) { - - if (!('Event' in global)) return false; - if (typeof global.Event === 'function') return true; - - try { - - // In IE 9-11, the Event object exists but cannot be instantiated - new Event('click'); - return true; - } catch(e) { - return false; - } - }(this)) -); - -if (detect) return - -// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Event&flags=always -(function () { - var unlistenableWindowEvents = { - click: 1, - dblclick: 1, - keyup: 1, - keypress: 1, - keydown: 1, - mousedown: 1, - mouseup: 1, - mousemove: 1, - mouseover: 1, - mouseenter: 1, - mouseleave: 1, - mouseout: 1, - storage: 1, - storagecommit: 1, - textinput: 1 - }; - - // This polyfill depends on availability of `document` so will not run in a worker - // However, we asssume there are no browsers with worker support that lack proper - // support for `Event` within the worker - if (typeof document === 'undefined' || typeof window === 'undefined') return; - - function indexOf(array, element) { - var - index = -1, - length = array.length; - - while (++index < length) { - if (index in array && array[index] === element) { - return index; - } - } - - return -1; - } - - var existingProto = (window.Event && window.Event.prototype) || null; - window.Event = Window.prototype.Event = function Event(type, eventInitDict) { - if (!type) { - throw new Error('Not enough arguments'); - } - - var event; - // Shortcut if browser supports createEvent - if ('createEvent' in document) { - event = document.createEvent('Event'); - var bubbles = eventInitDict && eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false; - var cancelable = eventInitDict && eventInitDict.cancelable !== undefined ? eventInitDict.cancelable : false; - - event.initEvent(type, bubbles, cancelable); - - return event; - } - - event = document.createEventObject(); - - event.type = type; - event.bubbles = eventInitDict && eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false; - event.cancelable = eventInitDict && eventInitDict.cancelable !== undefined ? eventInitDict.cancelable : false; - - return event; - }; - if (existingProto) { - Object.defineProperty(window.Event, 'prototype', { - configurable: false, - enumerable: false, - writable: true, - value: existingProto - }); - } - - if (!('createEvent' in document)) { - window.addEventListener = Window.prototype.addEventListener = Document.prototype.addEventListener = Element.prototype.addEventListener = function addEventListener() { - var - element = this, - type = arguments[0], - listener = arguments[1]; - - if (element === window && type in unlistenableWindowEvents) { - throw new Error('In IE8 the event: ' + type + ' is not available on the window object. Please see https://github.com/Financial-Times/polyfill-service/issues/317 for more information.'); - } - - if (!element._events) { - element._events = {}; - } - - if (!element._events[type]) { - element._events[type] = function (event) { - var - list = element._events[event.type].list, - events = list.slice(), - index = -1, - length = events.length, - eventElement; - - event.preventDefault = function preventDefault() { - if (event.cancelable !== false) { - event.returnValue = false; - } - }; - - event.stopPropagation = function stopPropagation() { - event.cancelBubble = true; - }; - - event.stopImmediatePropagation = function stopImmediatePropagation() { - event.cancelBubble = true; - event.cancelImmediate = true; - }; - - event.currentTarget = element; - event.relatedTarget = event.fromElement || null; - event.target = event.target || event.srcElement || element; - event.timeStamp = new Date().getTime(); - - if (event.clientX) { - event.pageX = event.clientX + document.documentElement.scrollLeft; - event.pageY = event.clientY + document.documentElement.scrollTop; - } - - while (++index < length && !event.cancelImmediate) { - if (index in events) { - eventElement = events[index]; - - if (indexOf(list, eventElement) !== -1 && typeof eventElement === 'function') { - eventElement.call(element, event); - } - } - } - }; - - element._events[type].list = []; - - if (element.attachEvent) { - element.attachEvent('on' + type, element._events[type]); - } - } - - element._events[type].list.push(listener); - }; - - window.removeEventListener = Window.prototype.removeEventListener = Document.prototype.removeEventListener = Element.prototype.removeEventListener = function removeEventListener() { - var - element = this, - type = arguments[0], - listener = arguments[1], - index; - - if (element._events && element._events[type] && element._events[type].list) { - index = indexOf(element._events[type].list, listener); - - if (index !== -1) { - element._events[type].list.splice(index, 1); - - if (!element._events[type].list.length) { - if (element.detachEvent) { - element.detachEvent('on' + type, element._events[type]); - } - delete element._events[type]; - } - } - } - }; - - window.dispatchEvent = Window.prototype.dispatchEvent = Document.prototype.dispatchEvent = Element.prototype.dispatchEvent = function dispatchEvent(event) { - if (!arguments.length) { - throw new Error('Not enough arguments'); - } - - if (!event || typeof event.type !== 'string') { - throw new Error('DOM Events Exception 0'); - } - - var element = this, type = event.type; - - try { - if (!event.bubbles) { - event.cancelBubble = true; - - var cancelBubbleEvent = function (event) { - event.cancelBubble = true; - - (element || window).detachEvent('on' + type, cancelBubbleEvent); - }; - - this.attachEvent('on' + type, cancelBubbleEvent); - } - - this.fireEvent('on' + type, event); - } catch (error) { - event.target = element; - - do { - event.currentTarget = element; - - if ('_events' in element && typeof element._events[type] === 'function') { - element._events[type].call(element, event); - } - - if (typeof element['on' + type] === 'function') { - element['on' + type].call(element, event); - } - - element = element.nodeType === 9 ? element.parentWindow : element.parentNode; - } while (element && !event.cancelBubble); - } - - return true; - }; - - // Add the DOMContentLoaded Event - document.attachEvent('onreadystatechange', function() { - if (document.readyState === 'complete') { - document.dispatchEvent(new Event('DOMContentLoaded', { - bubbles: true - })); - } - }); - } -}()); - -}) -.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - - // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/matches/detect.js - var detect = ( - 'document' in this && "matches" in document.documentElement - ); - - if (detect) return - - // Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/matches/polyfill.js - Element.prototype.matches = Element.prototype.webkitMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.mozMatchesSelector || function matches(selector) { - var element = this; - var elements = (element.document || element.ownerDocument).querySelectorAll(selector); - var index = 0; - - while (elements[index] && elements[index] !== element) { - ++index; - } - - return !!elements[index]; - }; - -}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -(function(undefined) { - - // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/closest/detect.js - var detect = ( - 'document' in this && "closest" in document.documentElement - ); - - if (detect) return - - // Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/closest/polyfill.js - Element.prototype.closest = function closest(selector) { - var node = this; - - while (node) { - if (node.matches(selector)) return node; - else node = 'SVGElement' in window && node instanceof SVGElement ? node.parentNode : node.parentElement; - } - - return null; - }; - -}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); - -function ErrorSummary ($module) { - this.$module = $module; -} - -ErrorSummary.prototype.init = function () { - var $module = this.$module; - if (!$module) { - return - } - $module.focus(); - - $module.addEventListener('click', this.handleClick.bind(this)); -}; - -/** -* Click event handler -* -* @param {MouseEvent} event - Click event -*/ -ErrorSummary.prototype.handleClick = function (event) { - var target = event.target; - if (this.focusTarget(target)) { - event.preventDefault(); - } -}; - -/** - * Focus the target element - * - * By default, the browser will scroll the target into view. Because our labels - * or legends appear above the input, this means the user will be presented with - * an input without any context, as the label or legend will be off the top of - * the screen. - * - * Manually handling the click event, scrolling the question into view and then - * focussing the element solves this. - * - * This also results in the label and/or legend being announced correctly in - * NVDA (as tested in 2018.3.2) - without this only the field type is announced - * (e.g. "Edit, has autocomplete"). - * - * @param {HTMLElement} $target - Event target - * @returns {boolean} True if the target was able to be focussed - */ -ErrorSummary.prototype.focusTarget = function ($target) { - // If the element that was clicked was not a link, return early - if ($target.tagName !== 'A' || $target.href === false) { - return false - } - - var inputId = this.getFragmentFromUrl($target.href); - var $input = document.getElementById(inputId); - if (!$input) { - return false - } - - var $legendOrLabel = this.getAssociatedLegendOrLabel($input); - if (!$legendOrLabel) { - return false - } - - // Scroll the legend or label into view *before* calling focus on the input to - // avoid extra scrolling in browsers that don't support `preventScroll` (which - // at time of writing is most of them...) - $legendOrLabel.scrollIntoView(); - $input.focus({ preventScroll: true }); - - return true -}; - -/** - * Get fragment from URL - * - * Extract the fragment (everything after the hash) from a URL, but not including - * the hash. - * - * @param {string} url - URL - * @returns {string} Fragment from URL, without the hash - */ -ErrorSummary.prototype.getFragmentFromUrl = function (url) { - if (url.indexOf('#') === -1) { - return false - } - - return url.split('#').pop() -}; - -/** - * Get associated legend or label - * - * Returns the first element that exists from this list: - * - * - The `` associated with the closest `
` ancestor, as long - * as the top of it is no more than half a viewport height away from the - * bottom of the input - * - The first `
+ +
+ {% set codeExamplesHtml %} +

Markup

+

+          {{- getHTMLCode(componentName, {
+            context: example.options,
+            fixture: example
+          }) | highlight("html") | safe -}}
+        
+ +

Macro

+

+          {{- getNunjucksCode(componentName, {
+            context: example.options
+          }) | highlight("js") | safe -}}
+        
+ {% endset %} + + {{ govukDetails({ + summaryText: "Code", + html: codeExamplesHtml + }) }} +
+ + + {% endif %} +{% endfor %} +{% endmacro %} diff --git a/packages/govuk-frontend-review/src/views/partials/banner.njk b/packages/govuk-frontend-review/src/views/partials/banner.njk new file mode 100644 index 0000000000..20b2fa1b9f --- /dev/null +++ b/packages/govuk-frontend-review/src/views/partials/banner.njk @@ -0,0 +1,14 @@ +{% if shouldShowAppBanner %} +
+
+

+ This is an internal development app. +
+ To learn how to use the GOV.UK Design System in your project, see Get started. +

+
+ +
+
+
+{% endif %} diff --git a/packages/govuk-frontend-review/src/views/partials/exampleBanner.njk b/packages/govuk-frontend-review/src/views/partials/exampleBanner.njk new file mode 100644 index 0000000000..c9896ff56c --- /dev/null +++ b/packages/govuk-frontend-review/src/views/partials/exampleBanner.njk @@ -0,0 +1,14 @@ +{% if shouldShowAppBanner %} +
+
+

+ This is not a real service, and is used for testing purposes only. +
+ See the GOV.UK Design System for recommended guidance. +

+
+ +
+
+
+{% endif %} diff --git a/packages/govuk-frontend-review/tasks/build/dev.mjs b/packages/govuk-frontend-review/tasks/build/dev.mjs new file mode 100644 index 0000000000..0b7514d168 --- /dev/null +++ b/packages/govuk-frontend-review/tasks/build/dev.mjs @@ -0,0 +1,17 @@ +import { npm } from '@govuk-frontend/tasks' +import gulp from 'gulp' + +import { watch } from '../index.mjs' + +/** + * Dev task + * Runs a sequence of tasks on start + * + * @type {import('@govuk-frontend/tasks').TaskFunction} + */ +export default (options) => + gulp.parallel( + npm.script('serve', [], options), // Express.js server using Nodemon + npm.script('proxy', [], options), // Auto reloading proxy using Browsersync + watch(options) + ) diff --git a/packages/govuk-frontend-review/tasks/build/dist.mjs b/packages/govuk-frontend-review/tasks/build/dist.mjs new file mode 100644 index 0000000000..20a96b836b --- /dev/null +++ b/packages/govuk-frontend-review/tasks/build/dist.mjs @@ -0,0 +1,17 @@ +import { npm } from '@govuk-frontend/tasks' +import gulp from 'gulp' + +import { scripts, styles } from '../index.mjs' + +/** + * Build review app task + * Prepare dist folder for review app + * + * @type {import('@govuk-frontend/tasks').TaskFunction} + */ +export default (options) => + gulp.series( + npm.script('clean', [], options), + scripts(options), + styles(options) + ) diff --git a/packages/govuk-frontend-review/tasks/build/index.mjs b/packages/govuk-frontend-review/tasks/build/index.mjs new file mode 100644 index 0000000000..af72ff6e7e --- /dev/null +++ b/packages/govuk-frontend-review/tasks/build/index.mjs @@ -0,0 +1,5 @@ +/** + * Build target tasks + */ +export { default as dev } from './dev.mjs' +export { default as dist } from './dist.mjs' diff --git a/packages/govuk-frontend-review/tasks/build/options.mjs b/packages/govuk-frontend-review/tasks/build/options.mjs new file mode 100644 index 0000000000..3ad743c395 --- /dev/null +++ b/packages/govuk-frontend-review/tasks/build/options.mjs @@ -0,0 +1,15 @@ +import { join, relative } from 'path' + +import { paths } from '@govuk-frontend/config' + +/** + * Default build paths + * + * @type {import('@govuk-frontend/tasks').TaskOptions} + */ +export const options = { + basePath: paths.app, + srcPath: join(paths.app, 'src'), + destPath: join(paths.app, 'dist'), + workspace: relative(paths.root, paths.app) +} diff --git a/packages/govuk-frontend-review/tasks/index.mjs b/packages/govuk-frontend-review/tasks/index.mjs new file mode 100644 index 0000000000..5cea86ea78 --- /dev/null +++ b/packages/govuk-frontend-review/tasks/index.mjs @@ -0,0 +1,6 @@ +/** + * Build tasks + */ +export { compile as scripts } from './scripts.mjs' +export { compile as styles } from './styles.mjs' +export { watch } from './watch.mjs' diff --git a/packages/govuk-frontend-review/tasks/scripts.mjs b/packages/govuk-frontend-review/tasks/scripts.mjs new file mode 100644 index 0000000000..f12c921593 --- /dev/null +++ b/packages/govuk-frontend-review/tasks/scripts.mjs @@ -0,0 +1,22 @@ +import { join } from 'path' + +import { scripts, task } from '@govuk-frontend/tasks' +import gulp from 'gulp' + +/** + * JavaScripts task (for watch) + * + * @type {import('@govuk-frontend/tasks').TaskFunction} + */ +export const compile = (options) => + gulp.series( + task.name('compile:js', () => + scripts.compile('**/*.mjs', { + ...options, + + srcPath: join(options.srcPath, 'javascripts'), + destPath: join(options.destPath, 'javascripts'), + configPath: join(options.basePath, 'rollup.config.mjs') + }) + ) + ) diff --git a/packages/govuk-frontend-review/tasks/styles.mjs b/packages/govuk-frontend-review/tasks/styles.mjs new file mode 100644 index 0000000000..9f3812e7e0 --- /dev/null +++ b/packages/govuk-frontend-review/tasks/styles.mjs @@ -0,0 +1,27 @@ +import { join } from 'path' + +import { styles, task } from '@govuk-frontend/tasks' +import gulp from 'gulp' + +/** + * Stylesheets task (for watch) + * + * @type {import('@govuk-frontend/tasks').TaskFunction} + */ +export const compile = (options) => + gulp.series( + task.name('compile:scss', () => + styles.compile('**/[!_]*.scss', { + ...options, + + srcPath: join(options.srcPath, 'stylesheets'), + destPath: join(options.destPath, 'stylesheets'), + configPath: join(options.basePath, 'postcss.config.mjs'), + + // Rename with `*.min.css` extension + filePath({ dir, name }) { + return join(dir, `${name}.min.css`) + } + }) + ) + ) diff --git a/packages/govuk-frontend-review/tasks/watch.mjs b/packages/govuk-frontend-review/tasks/watch.mjs new file mode 100644 index 0000000000..42f6f6bc3a --- /dev/null +++ b/packages/govuk-frontend-review/tasks/watch.mjs @@ -0,0 +1,99 @@ +import { join } from 'path' + +import { paths } from '@govuk-frontend/config' +import { npm, task } from '@govuk-frontend/tasks' +import gulp from 'gulp' +import slash from 'slash' + +import { scripts, styles } from './index.mjs' + +/** + * Watch task + * + * During development, this task will: + * + * - lint and run `gulp styles` when `.scss` files change + * - lint and run `gulp scripts` when `.{cjs,js,mjs}` files change + * + * These tasks respond to `gulp watch` output from GOV.UK Frontend: + * {@link file://./../../govuk-frontend/tasks/watch.mjs} + * + * @type {import('@govuk-frontend/tasks').TaskFunction} + */ +export const watch = (options) => gulp.parallel(...getTasks(options)) + +/** + * Compute the lists of tasks to be run in parallel + * + * @param {import('@govuk-frontend/tasks').TaskOptions} options + * @returns {any[]} The list of tasks to run in parallel + */ +function getTasks(options) { + const tasks = { + 'lint:scss watch': disabledBy('GOVUK_DS_FRONTEND_NO_LINTING', 'scss', () => + gulp.watch( + '**/*.scss', + { cwd: options.srcPath }, + + // Run Stylelint checks + npm.script('lint:scss:cli', [ + slash(join(options.workspace, '**/*.scss')) + ]) + ) + ), + 'compile:scss watch': () => + gulp.watch( + ['**/*.scss', join(paths.package, 'dist/govuk/index.scss')], + { + awaitWriteFinish: true, + cwd: options.srcPath, + + // Prevent early Sass compile by ignoring delete (unlink) event + // when GOV.UK Frontend runs the `clean` script before build + events: ['add', 'change'] + }, + + // Run Sass compile + styles(options) + ), + 'lint:js watch': disabledBy('GOVUK_DS_FRONTEND_NO_LINTING', 'js', () => + gulp.watch( + '**/*.{cjs,js,mjs}', + { cwd: options.srcPath, ignored: ['**/*.test.*'] }, + gulp.parallel( + // Run TypeScript compiler + npm.script('build:types', ['--incremental', '--pretty'], options), + + // Run ESLint checks + npm.script('lint:js:cli', [ + slash(join(options.workspace, '**/*.{cjs,js,mjs}')) + ]) + ) + ) + ), + 'compile:js watch': () => + gulp.watch( + 'javascripts/**/*.mjs', + { cwd: options.srcPath }, + + // Run JavaScripts compile + scripts(options) + ) + } + + return Object.entries(tasks) + .filter(([taskName, fn]) => !!fn) + .map(([taskName, fn]) => task.name(taskName, fn)) +} + +function disabledBy(envVariableName, value, fn) { + // Split by comma to ensure we'll match full options + // avoiding `css` to match `scss` if we were looking in the whole string + const disabledValues = process.env[envVariableName]?.split?.(',') + + if (disabledValues?.includes?.(value)) { + return null + } + + return fn +} diff --git a/packages/govuk-frontend-review/tsconfig.build.json b/packages/govuk-frontend-review/tsconfig.build.json new file mode 100644 index 0000000000..6e2f6a4241 --- /dev/null +++ b/packages/govuk-frontend-review/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["./src/**/*.mjs"], + "exclude": ["**/*.test.*"], + "compilerOptions": { + "types": ["express", "node", "nunjucks", "slug", "typed-query-selector"] + } +} diff --git a/packages/govuk-frontend-review/tsconfig.dev.json b/packages/govuk-frontend-review/tsconfig.dev.json new file mode 100644 index 0000000000..092f06eaa8 --- /dev/null +++ b/packages/govuk-frontend-review/tsconfig.dev.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["**/*.js", "**/*.mjs"], + "exclude": ["./dist", "./node_modules"], + "compilerOptions": { + "types": [ + "browser-sync", + "express", + "gulp", + "jest", + "jest-puppeteer", + "node", + "nunjucks", + "slug", + "typed-query-selector" + ] + } +} diff --git a/packages/govuk-frontend-review/tsconfig.json b/packages/govuk-frontend-review/tsconfig.json new file mode 100644 index 0000000000..395df1c8ee --- /dev/null +++ b/packages/govuk-frontend-review/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "references": [ + { + "path": "./tsconfig.dev.json" + }, + { + "path": "./tsconfig.build.json" + } + ] +} diff --git a/packages/govuk-frontend-review/typedoc.config.js b/packages/govuk-frontend-review/typedoc.config.js new file mode 100644 index 0000000000..c9ca196c5c --- /dev/null +++ b/packages/govuk-frontend-review/typedoc.config.js @@ -0,0 +1,68 @@ +const { join, relative } = require('path') + +const { paths } = require('@govuk-frontend/config') +const { + packageResolveToPath, + packageNameToPath +} = require('@govuk-frontend/lib/names') +const slash = require('slash') +const typedoc = require('typedoc') + +const basePath = join(packageNameToPath('govuk-frontend'), 'src') +const workspacePath = slash(relative(paths.root, basePath)) +const { HEROKU_APP, HEROKU_BRANCH = 'main' } = process.env + +/** + * @type {import('typedoc').TypeDocOptions} + */ +module.exports = { + disableGit: !!HEROKU_APP, + emit: 'both', + name: 'govuk-frontend', + sourceLinkTemplate: HEROKU_APP + ? `https://github.com/alphagov/govuk-frontend/blob/${HEROKU_BRANCH}/${workspacePath}/{path}#L{line}` + : `https://github.com/alphagov/govuk-frontend/blob/{gitRevision}/{path}#L{line}`, + + // Configure paths + basePath, + entryPoints: [packageResolveToPath('govuk-frontend/src/govuk/all.mjs')], + tsconfig: packageResolveToPath('govuk-frontend/tsconfig.build.json'), + out: './dist/docs/jsdoc', + + // Turn off strict checks for JSDoc output + // since `lint:types` will already log issues + compilerOptions: { + strict: false + }, + + // Document private methods. These are behind a checkbox in the + // settings menu of the JSDoc page. + excludePrivate: false, + + plugin: [ + // Use typedoc-plugin-missing-exports to ensure that internal symbols which + // are not exported are included in the documentation (like the `I18n` class + // or the components' config types) + 'typedoc-plugin-missing-exports' + ], + // By default, missing-exports will regroup all symbols under an `` + // module whose naming is a bit poor. Instead, we let the symbols be displayed + // alongside the others + placeInternalsInOwningModule: true, + // The missing-exports plugin will include built-in symbols, like the DOM API. + // We don't want those in our documentation, so we need to exclude them + excludeExternals: true, + + // Make TypeDoc aware of tags we use but it does not parse by default + // so it doesn't warn unnecessarily + modifierTags: [ + ...typedoc.Configuration.OptionDefaults.modifierTags, + '@preserve', + '@constant' + ], + + // We don't want typedoc to render a 'Preserve' tag + // as it's only for controlling which comments get rendered or not + // after transpilation + excludeTags: ['@preserve'] +} diff --git a/packages/govuk-frontend/.browserslistrc b/packages/govuk-frontend/.browserslistrc new file mode 100644 index 0000000000..d3356f5e7a --- /dev/null +++ b/packages/govuk-frontend/.browserslistrc @@ -0,0 +1,19 @@ +# This list builds on the GOV.UK service manual's browser testing recommendations +# https://www.gov.uk/service-manual/technology/designing-for-different-browsers-and-devices + +[javascripts] +supports es6-module + +[stylesheets] +> 0.1% in GB and not dead +last 6 Chrome versions +last 6 Firefox versions +last 6 Edge versions +last 2 Samsung versions +Firefox ESR +Safari >= 11 +iOS >= 11 +ie 11 + +[node] +node 22 diff --git a/packages/govuk-frontend/.eslintrc.js b/packages/govuk-frontend/.eslintrc.js new file mode 100644 index 0000000000..ce6e7656c8 --- /dev/null +++ b/packages/govuk-frontend/.eslintrc.js @@ -0,0 +1,98 @@ +const { join } = require('path') + +module.exports = { + settings: { + node: { + version: '^18.12.0' + } + }, + overrides: [ + { + files: ['src/govuk/**/*.mjs'], + excludedFiles: ['**/*.test.mjs'], + parser: '@typescript-eslint/parser', + parserOptions: { + // Note: Allow ES2015 for import/export syntax + ecmaVersion: '2015', + project: [join(__dirname, 'tsconfig.build.json')] + }, + plugins: ['@typescript-eslint', 'es-x'], + extends: [ + 'plugin:@typescript-eslint/strict-type-checked', + 'plugin:@typescript-eslint/stylistic-type-checked', + 'plugin:es-x/restrict-to-es2015', + 'prettier' + ], + env: { + browser: true + }, + rules: { + // Allow void return shorthand in arrow functions + '@typescript-eslint/no-confusing-void-expression': [ + 'error', + { + ignoreArrowShorthand: true + } + ], + + // Check type support for template string implicit `.toString()` + '@typescript-eslint/restrict-template-expressions': [ + 'error', + { + allowBoolean: true, + allowNumber: true + } + ], + + // Babel transpiles ES2020 class fields + 'es-x/no-class-fields': 'off', + + // ES modules include ES2016 '[].includes()' coverage + // https://browsersl.ist/#q=supports+es6-module+and+not+supports+array-includes + 'es-x/no-array-prototype-includes': 'off', + + // Babel transpiles ES2020 `??` nullish coalescing + 'es-x/no-nullish-coalescing-operators': 'off', + + // ES modules include ES2017 'Object.entries()' coverage + // https://browsersl.ist/#q=supports+es6-module+and+not+supports+object-entries + 'es-x/no-object-entries': 'off', + + // Babel transpiles ES2020 optional chaining + 'es-x/no-optional-chaining': 'off', + + // JSDoc blocks are mandatory in source code + 'jsdoc/require-jsdoc': [ + 'error', + { + enableFixer: false, + require: { + ClassDeclaration: true, + ClassExpression: true, + FunctionExpression: false, + MethodDefinition: true + } + } + ], + + // JSDoc @param required in (mandatory) blocks but + // @param description is necessary in source code + 'jsdoc/require-param-description': 'warn', + 'jsdoc/require-param': 'error' + } + }, + { + files: ['src/govuk-prototype-kit/**/*.js'], + parserOptions: { + sourceType: 'module' + }, + env: { + browser: true + }, + rules: { + // Allow browser import `govuk-frontend.min.js` + 'n/no-missing-import': 'off' + } + } + ] +} diff --git a/packages/govuk-frontend/.gitignore b/packages/govuk-frontend/.gitignore new file mode 100644 index 0000000000..8fce7ae162 --- /dev/null +++ b/packages/govuk-frontend/.gitignore @@ -0,0 +1 @@ +govuk-prototype-kit.config.json diff --git a/packages/govuk-frontend/.htmlvalidate.js b/packages/govuk-frontend/.htmlvalidate.js new file mode 100644 index 0000000000..6cb7ff61d7 --- /dev/null +++ b/packages/govuk-frontend/.htmlvalidate.js @@ -0,0 +1,76 @@ +const { defineConfig } = require('html-validate') + +/** + * HTML validation config + * + * {@link https://html-validate.org/rules/} + */ +module.exports = defineConfig({ + extends: ['html-validate:recommended'], + rules: { + // Allow for multiple buttons in the same form to have the same name + // (as in the cookie banner examples) + 'form-dup-name': ['error', { shared: ['radio', 'checkbox', 'submit'] }], + + // Allow pattern attribute on input type="number" + 'input-attributes': 'off', + + // Require input to have a label + 'input-missing-label': 'error', + + // Allow inline styles for testing purposes + 'no-inline-style': 'off', + + // Require all form field and ARIA references to exist + 'no-missing-references': 'error', + + // More hassle than it's worth ๐Ÿ‘พ + 'no-trailing-whitespace': 'off', + + // We still support creating `input type=button` with the button + // component, but you have to explicitly choose to use them over + // buttons + 'prefer-button': 'off', + + // Allow use of roles where there are native elements that would give + // us that role automatically, e.g.
instead of + //
+ // + // This is mainly needed for links styled as buttons, but we do this + // in the cookie banner and notification banner too + 'prefer-native-element': 'off', + + // HTML Validate is opinionated about IDs beginning with a letter and + // only containing letters, numbers, underscores and dashes โ€“ which is + // more restrictive than the spec allows. + // + // Relax the rule to allow anything that is valid according to the + // spec. + 'valid-id': ['error', { relaxed: true }] + }, + elements: [ + 'html5', + { + // Allow textarea autocomplete attribute to be street-address + // (html-validate only allows on/off in default rules) + textarea: { + attributes: { + autocomplete: { enum: ['on', 'off', 'street-address'] } + } + }, + // Allow buttons to omit the type attribute (defaults to 'submit') + button: { + attributes: { + type: { required: false } + } + }, + fieldset: { + attributes: { + role: { + enum: ['group'] + } + } + } + } + ] +}) diff --git a/packages/govuk-frontend/.npmignore b/packages/govuk-frontend/.npmignore new file mode 100644 index 0000000000..9fcac2509f --- /dev/null +++ b/packages/govuk-frontend/.npmignore @@ -0,0 +1,2 @@ +*.html +*.test.* diff --git a/packages/govuk-frontend/README.md b/packages/govuk-frontend/README.md new file mode 100644 index 0000000000..8781cdcdb2 --- /dev/null +++ b/packages/govuk-frontend/README.md @@ -0,0 +1,92 @@ +# GOV.UK Frontend + +GOV.UK Frontend contains the code you need to start building a user interface +for government platforms and services. + +See live examples of GOV.UK Frontend components, and guidance on when to use +them in your service, in the [GOV.UK Design System](https://www.gov.uk/design-system). + +## Contact the team + +GOV.UK Frontend is maintained by a team at Government Digital Service. If you want to know more about GOV.UK Frontend, please email the [Design System +team](mailto:govuk-design-system-support@digital.cabinet-office.gov.uk) or get in touch with them on [Slack](https://ukgovernmentdigital.slack.com/messages/govuk-design-system). + +## Quick start + +There are 2 ways to start using GOV.UK Frontend in your app. + +Once installed, you will be able to use the code from the examples in the +[GOV.UK Design System](https://www.gov.uk/design-system) in your service. + +### 1. Install with npm (recommended) + +We recommend [installing GOV.UK Frontend using node package manager +(npm)](https://frontend.design-system.service.gov.uk/installing-with-npm/). + +### 2. Install by using compiled files + +You can also [download the compiled and minified assets (CSS, JavaScript) from +GitHub](https://frontend.design-system.service.gov.uk/installing-from-dist/). + +## Importing styles + +You need to import the GOV.UK Frontend styles into the main Sass file in your +project. You should place the below code before your own Sass rules (or Sass +imports) if you want to override GOV.UK Frontend with your own styles. + +To import add the below to your Sass file: + +```scss +@import "node_modules/govuk-frontend/dist/govuk/all"; +``` + +[More details on importing styles](https://frontend.design-system.service.gov.uk/importing-css-assets-and-javascript/#css) + +## Importing JavaScript + +Some of the JavaScript included in GOV.UK Frontend improves the usability and +accessibility of the components. You should make sure that you are importing and +initialising JavaScript in your application. This will ensure all users can use it successfully. + +You can include JavaScript for all components by copying both `govuk-frontend.min.js` and `govuk-frontend.min.js.map` from `node_modules/govuk-frontend/dist/govuk/` into your application and referencing the JavaScript directly: + +```html + +``` + +Next you need to import and initialise GOV.UK Frontend by adding: + +```html + +``` + +[More details on importing JavaScript and advanced options](https://frontend.design-system.service.gov.uk/importing-css-assets-and-javascript/#javascript) + +## Importing assets + +In order to import GOV.UK Frontend images and fonts to your project, you should configure your application to reference or copy the relevant GOV.UK Frontend assets. + +[More details on importing assets](https://frontend.design-system.service.gov.uk/importing-css-assets-and-javascript/#font-and-image-assets) + +## Getting updates + +To be notified when thereโ€™s a new release, you can either: + +- [watch the govuk-frontend Github repository](https://help.github.com/en/articles/watching-and-unwatching-repositories) +- join the [#govuk-design-system channel on cross-government Slack](https://ukgovernmentdigital.slack.com/app_redirect?channel=govuk-design-system) + +Find out how to [update with npm](https://frontend.design-system.service.gov.uk/updating-with-npm/). + +## Licence + +Unless stated otherwise, the codebase is released under the MIT License. This +covers both the codebase and any sample code in the documentation. The +documentation is © Crown copyright and available under the terms of the +Open Government 3.0 licence. + +## Contribution guidelines + +If you want to help us build GOV.UK Frontend, view our [contribution guidelines](/CONTRIBUTING.md). diff --git a/packages/govuk-frontend/babel.config.js b/packages/govuk-frontend/babel.config.js new file mode 100644 index 0000000000..1a4af435cf --- /dev/null +++ b/packages/govuk-frontend/babel.config.js @@ -0,0 +1,56 @@ +/** + * Babel config + * + * @type {import('@babel/core').ConfigFunction} + */ +module.exports = function (api) { + // Assume browser environment via Rollup plugin + const isBrowser = api.caller( + (caller) => caller?.name === '@rollup/plugin-babel' + ) + + // Apply Browserslist environment for supported targets + // https://github.com/browserslist/browserslist#configuring-for-different-environments + const browserslistEnv = isBrowser ? 'javascripts' : 'node' + + return { + generatorOpts: { + shouldPrintComment(comment) { + if (!isBrowser || comment.includes('* @preserve')) { + return true + } + + // Assume all comments are public unless + // tagged with `@private` or `@internal` + const isPrivate = ['* @internal', '* @private'].some((tag) => + comment.includes(tag) + ) + + // Flag any JSDoc comments worth keeping + const isDocumentation = ['* @param', '* @returns', '* @typedef'].some( + (tag) => comment.includes(tag) + ) + + // Print only public JSDoc comments + return !isPrivate && isDocumentation + } + }, + presets: [ + [ + '@babel/preset-env', + { + browserslistEnv, + + // Apply bug fixes to avoid transforms + bugfixes: true, + + // Apply smaller "loose" transforms for browsers + loose: isBrowser, + + // Skip ES module transforms for browsers + modules: isBrowser ? false : 'auto' + } + ] + ] + } +} diff --git a/packages/govuk-frontend/gulpfile.mjs b/packages/govuk-frontend/gulpfile.mjs new file mode 100644 index 0000000000..d3ffd0a060 --- /dev/null +++ b/packages/govuk-frontend/gulpfile.mjs @@ -0,0 +1,29 @@ +import gulp from 'gulp' + +import * as build from './tasks/build/index.mjs' +import { options, targets } from './tasks/build/options.mjs' +import { + assets, + fixtures, + scripts, + styles, + templates, + watch +} from './tasks/index.mjs' + +/** + * Build target tasks + */ +gulp.task('build:package', build.package(targets.package)) +gulp.task('build:release', build.release(targets.release)) +gulp.task('dev', build.dev(options)) + +/** + * Utility tasks + */ +gulp.task('assets', assets(options)) +gulp.task('fixtures', fixtures(options)) +gulp.task('scripts', scripts(options)) +gulp.task('styles', styles(options)) +gulp.task('templates', templates(options)) +gulp.task('watch', watch(options)) diff --git a/packages/govuk-frontend/package.json b/packages/govuk-frontend/package.json new file mode 100644 index 0000000000..d3eaea3013 --- /dev/null +++ b/packages/govuk-frontend/package.json @@ -0,0 +1,92 @@ +{ + "name": "govuk-frontend", + "description": "GOV.UK Frontend contains the code you need to start building a user interface for government platforms and services.", + "version": "5.7.1", + "main": "dist/govuk/all.bundle.js", + "module": "dist/govuk/all.mjs", + "sass": "dist/govuk/index.scss", + "files": [ + "dist", + "govuk-prototype-kit.config.json", + "package.json", + "README.md" + ], + "exports": { + ".": { + "sass": "./dist/govuk/index.scss", + "import": "./dist/govuk/all.mjs", + "require": "./dist/govuk/all.bundle.js", + "default": "./dist/govuk/all.bundle.js" + }, + "./*": "./*", + "./dist/": "./dist/", + "./govuk-prototype-kit.config.json": "./govuk-prototype-kit.config.json", + "./package.json": "./package.json" + }, + "sideEffects": false, + "engines": { + "node": ">= 4.2.0" + }, + "author": { + "name": "GOV.UK Design System Team (Government Digital Service)", + "email": "design-system-developers@digital.cabinet-office.gov.uk" + }, + "repository": { + "type": "git", + "url": "https://github.com/alphagov/govuk-frontend.git" + }, + "bugs": { + "url": "https://github.com/alphagov/govuk-frontend/issues" + }, + "homepage": "https://frontend.design-system.service.gov.uk/", + "keywords": [ + "govuk", + "frontend", + "design system", + "template" + ], + "license": "MIT", + "scripts": { + "dev": "gulp dev --color", + "prebuild:package": "npm run clean:package", + "prebuild:release": "npm run clean:release", + "build": "npm run build:package", + "build:package": "gulp build:package --color", + "build:release": "gulp build:release --color", + "build:stats": "npm run stats --workspace @govuk-frontend/stats", + "build:types": "tsc --build tsconfig.build.json", + "clean": "npm run clean:package", + "clean:package": "del-cli *.tsbuildinfo dist govuk-prototype-kit.config.json", + "clean:release": "del-cli ../../dist --force", + "postbuild:package": "npm run build:stats && govuk-prototype-kit validate-plugin .", + "version": "echo $npm_package_version" + }, + "devDependencies": { + "@babel/core": "^7.26.0", + "@babel/preset-env": "^7.26.0", + "@govuk-frontend/config": "*", + "@govuk-frontend/helpers": "*", + "@govuk-frontend/lib": "*", + "@govuk-frontend/tasks": "*", + "@rollup/plugin-babel": "^6.0.4", + "@rollup/plugin-replace": "^5.0.7", + "@rollup/plugin-terser": "^0.4.4", + "autoprefixer": "^10.4.20", + "cssnano": "^7.0.6", + "cssnano-preset-default": "^7.0.1", + "govuk-prototype-kit": "^13.16.2", + "gulp": "^5.0.0", + "gulp-cli": "^3.0.0", + "html-validate": "8.27.0", + "nunjucks": "^3.2.4", + "outdent": "^0.8.0", + "postcss": "^8.4.49", + "postcss-scss": "^4.0.9", + "puppeteer": "^23.6.0", + "rollup": "^4.19.1", + "sass-color-helpers": "^2.1.1", + "sass-embedded": "^1.81.0", + "sassdoc": "^2.7.4", + "slash": "^5.1.0" + } +} diff --git a/packages/govuk-frontend/package.json.unit.test.mjs b/packages/govuk-frontend/package.json.unit.test.mjs new file mode 100644 index 0000000000..caac73133f --- /dev/null +++ b/packages/govuk-frontend/package.json.unit.test.mjs @@ -0,0 +1,12 @@ +import pkg from './package.json' + +describe('package.json', () => { + // The `sideEffects` field allows us to tell bundlers like Webpack or Rollup + // that they can safely delete code that's not being imported because our + // package has no code that executes when files are imported and modify the + // global environment, (JavaScript built-ins or the DOM). Everything is + // wrapped inside function or classes. + it('announces that our package has no side effects', () => { + expect(pkg.sideEffects).toBe(false) + }) +}) diff --git a/packages/govuk-frontend/postcss.config.mjs b/packages/govuk-frontend/postcss.config.mjs new file mode 100644 index 0000000000..3e2a27a430 --- /dev/null +++ b/packages/govuk-frontend/postcss.config.mjs @@ -0,0 +1,47 @@ +import autoprefixer from 'autoprefixer' +import cssnano from 'cssnano' +import cssnanoPresetDefault from 'cssnano-preset-default' +import postcss from 'postcss' +import scss from 'postcss-scss' + +/** + * PostCSS config + * + * @param {import('postcss-load-config').ConfigContext} [ctx] - Context options + * @returns {import('postcss-load-config').Config} PostCSS Config + */ +export default ({ to = '' } = {}) => ({ + plugins: [ + // Add vendor prefixes + autoprefixer({ env: 'stylesheets' }), + + // Add GOV.UK Frontend release version + { + postcssPlugin: 'govuk-frontend-version', + Declaration: { + // Find CSS declaration for version, update value + // https://github.com/postcss/postcss/blob/main/docs/writing-a-plugin.md + // https://postcss.org/api/#declaration + '--govuk-frontend-version': async (decl) => { + const config = await import('@govuk-frontend/config') + decl.value = `"${config.version}"` + } + } + }, + + // Always minify CSS + to.endsWith('.css') && + cssnano({ + preset: cssnanoPresetDefault({ + env: 'stylesheets', + + // Sorted CSS is smaller when gzipped, but we sort using Stylelint + // https://cssnano.co/docs/optimisations/cssdeclarationsorter/ + cssDeclarationSorter: false + }) + }) + ], + + // Sass syntax support + syntax: to.endsWith('.scss') ? scss : postcss +}) diff --git a/packages/govuk-frontend/postcss.config.unit.test.mjs b/packages/govuk-frontend/postcss.config.unit.test.mjs new file mode 100644 index 0000000000..bfe987b6a5 --- /dev/null +++ b/packages/govuk-frontend/postcss.config.unit.test.mjs @@ -0,0 +1,164 @@ +import { pkg } from '@govuk-frontend/config' + +import configFn from './postcss.config.mjs' + +describe('PostCSS config', () => { + function getPlugin(config, pluginName) { + return config.plugins.find( + ({ postcssPlugin }) => postcssPlugin === pluginName + ) + } + + function getPluginNames(config) { + return config.plugins.flatMap(getPluginName) + } + + function getPluginName({ plugins, postcssPlugin }) { + return plugins ? getPluginNames({ plugins }) : postcssPlugin + } + + describe('Browserslist', () => { + it('Uses default environment', () => { + const config = configFn() + + expect(config.plugins).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + postcssPlugin: 'autoprefixer' + }) + ]) + ) + }) + + it.each([ + { + from: 'example.scss', + to: 'example.min.css' + } + ])('Uses default environment for $from', ({ from, to }) => { + const config = configFn({ from, to }) + + expect(config.plugins).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + postcssPlugin: 'autoprefixer' + }) + ]) + ) + }) + }) + + describe('Plugins', () => { + describe('GOV.UK Frontend version', () => { + beforeEach(() => { + jest.resetModules() + }) + + afterEach(() => { + process.env.NODE_ENV = 'test' + }) + + it('Adds build type for NODE_ENV=development', async () => { + process.env.NODE_ENV = 'development' + + const { Declaration } = getPlugin( + configFn({ env: 'development' }), + 'govuk-frontend-version' + ) + + const property = { value: 'development' } + await Declaration['--govuk-frontend-version'](property) + + expect(property.value).toBe('"development"') + }) + + it('Adds version number for NODE_ENV=production', async () => { + process.env.NODE_ENV = 'production' + + const { Declaration } = getPlugin( + configFn({ env: 'production' }), + 'govuk-frontend-version' + ) + + const property = { value: 'development' } + await Declaration['--govuk-frontend-version'](property) + + expect(property.value).toBe(`"${pkg.version}"`) + }) + + it('Adds version number for NODE_ENV=test', async () => { + process.env.NODE_ENV = 'test' + + const { Declaration } = getPlugin( + configFn({ env: 'test' }), + 'govuk-frontend-version' + ) + + const property = { value: 'development' } + await Declaration['--govuk-frontend-version'](property) + + expect(property.value).toBe(`"${pkg.version}"`) + }) + }) + + describe('CSS syntax parser', () => { + it.each([ + { + from: 'example.scss', + to: 'example.min.css' + } + ])('Adds plugins for $from', ({ from, to }) => { + const config = configFn({ from, to }) + + expect(getPluginNames(config)).toEqual([ + 'autoprefixer', + 'govuk-frontend-version', + 'postcss-discard-comments', + 'postcss-minify-gradients', + 'postcss-reduce-initial', + 'postcss-svgo', + 'postcss-normalize-display-values', + 'postcss-reduce-transforms', + 'postcss-colormin', + 'postcss-normalize-timing-functions', + 'postcss-calc', + 'postcss-convert-values', + 'postcss-ordered-values', + 'postcss-minify-selectors', + 'postcss-minify-params', + 'postcss-normalize-charset', + 'postcss-discard-overridden', + 'postcss-normalize-string', + 'postcss-normalize-unicode', + 'postcss-minify-font-values', + 'postcss-normalize-url', + 'postcss-normalize-repeat-style', + 'postcss-normalize-positions', + 'postcss-normalize-whitespace', + 'postcss-merge-longhand', + 'postcss-discard-duplicates', + 'postcss-merge-rules', + 'postcss-discard-empty', + 'postcss-unique-selectors', + 'cssnano-util-raw-cache' + ]) + }) + }) + + describe('Sass syntax parser', () => { + it.each([ + { + from: 'src/govuk/components/accordion/_accordion.scss', + to: 'dist/govuk/components/accordion/_accordion.scss' + } + ])('Adds plugins for $from', ({ from, to }) => { + const config = configFn({ from, to }) + + expect(getPluginNames(config)).toEqual([ + 'autoprefixer', + 'govuk-frontend-version' + ]) + }) + }) + }) +}) diff --git a/packages/govuk-frontend/rollup.publish.config.mjs b/packages/govuk-frontend/rollup.publish.config.mjs new file mode 100644 index 0000000000..47fc3019c2 --- /dev/null +++ b/packages/govuk-frontend/rollup.publish.config.mjs @@ -0,0 +1,73 @@ +import config from '@govuk-frontend/config' +import { babel } from '@rollup/plugin-babel' +import replace from '@rollup/plugin-replace' +import { defineConfig } from 'rollup' + +/** + * Rollup config for npm publish + */ +export default defineConfig(({ i: input }) => ({ + input, + + /** + * Output options + */ + output: [ + /** + * ECMAScript (ES) modules for Node.js or bundler `import` + */ + { + entryFileNames: '[name].mjs', + format: 'es', + + // Separate modules, not bundled + preserveModules: true + }, + + /** + * ECMAScript (ES) module bundles for browser + rows: + - - text: Foo + - name: html + hidden: true + options: + head: + - html: Foo bar + rows: + - - html: Foo bar + - name: head with classes + hidden: true + options: + head: + - text: Foo + classes: my-custom-class + rows: + - - text: Jan + - text: Feb + - name: head with rowspan and colspan + hidden: true + options: + head: + - text: Foo + rowspan: 2 + colspan: 2 + rows: + - - text: Jan + - text: Feb + - name: head with attributes + hidden: true + options: + head: + - text: Foo + attributes: + data-fizz: buzz + rows: + - - text: Jan + - text: Feb + - name: with firstCellIsHeader true + hidden: true + options: + firstCellIsHeader: true + rows: + - - text: January + - text: ยฃ85 + format: numeric + - text: ยฃ95 + format: numeric + - - text: February + - text: ยฃ75 + format: numeric + - text: ยฃ55 + format: numeric + - - text: March + - text: ยฃ165 + format: numeric + - text: ยฃ125 + format: numeric + - name: firstCellIsHeader with classes + hidden: true + options: + firstCellIsHeader: true + rows: + - - text: Foo + classes: my-custom-class + - name: firstCellIsHeader with html + hidden: true + options: + firstCellIsHeader: true + rows: + - - html: Foo bar + - name: firstCellIsHeader with html as text + hidden: true + options: + firstCellIsHeader: true + rows: + - - text: Foo + - name: firstCellIsHeader with rowspan and colspan + hidden: true + options: + firstCellIsHeader: true + rows: + - - text: Foo + rowspan: 2 + colspan: 2 + - name: firstCellIsHeader with attributes + hidden: true + options: + firstCellIsHeader: true + rows: + - - text: Foo + attributes: + data-fizz: buzz + - name: with falsy items + hidden: true + options: + rows: + - - text: A + - text: 1 + - false + - null + - - text: B + - text: 2 + - - text: C + - text: 3 + - name: rows with classes + hidden: true + options: + rows: + - - text: Foo + classes: my-custom-class + - name: rows with rowspan and colspan + hidden: true + options: + rows: + - - text: Foo + rowspan: 2 + colspan: 2 + - name: rows with attributes + hidden: true + options: + rows: + - - text: Foo + attributes: + data-fizz: buzz diff --git a/packages/govuk-frontend/src/govuk/components/table/template.njk b/packages/govuk-frontend/src/govuk/components/table/template.njk new file mode 100644 index 0000000000..eaa3ea63d3 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/table/template.njk @@ -0,0 +1,57 @@ +{% from "../../macros/attributes.njk" import govukAttributes -%} + + +{% if params.caption %} + +{% endif %} +{% if params.head %} + + + {% for item in params.head %} + + {% endfor %} + + +{% endif %} + +{% for row in params.rows %} + {% if row %} + + {% for cell in row %} + {% set commonAttributes %} + {%- if cell.colspan %} colspan="{{ cell.colspan }}"{% endif %} + {%- if cell.rowspan %} rowspan="{{ cell.rowspan }}"{% endif %} + {{- govukAttributes(cell.attributes) -}} + {% endset %} + {% if loop.first and params.firstCellIsHeader %} + + {% else %} + + {% endif %} + {% endfor %} + + {% endif %} +{% endfor %} + +
+ {{- params.caption -}} +
+ {{- item.html | safe if item.html else item.text -}} +
+ {{- cell.html | safe if cell.html else cell.text -}} + + {{- cell.html | safe if cell.html else cell.text -}} +
diff --git a/packages/govuk-frontend/src/govuk/components/table/template.test.js b/packages/govuk-frontend/src/govuk/components/table/template.test.js new file mode 100644 index 0000000000..8f6becfa56 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/table/template.test.js @@ -0,0 +1,347 @@ +const { render } = require('@govuk-frontend/helpers/nunjucks') +const { getExamples } = require('@govuk-frontend/lib/components') + +describe('Table', () => { + let examples + + beforeAll(async () => { + examples = await getExamples('table') + }) + + it('can have additional classes', () => { + const $ = render('table', examples.classes) + + expect($('.govuk-table').hasClass('custom-class-goes-here')).toBeTruthy() + }) + + it('can have additional attributes', () => { + const $ = render('table', examples.attributes) + + expect($('.govuk-table').attr('data-foo')).toBe('bar') + }) + + // ========================================================= + // Captions + // ========================================================= + + describe('captions', () => { + it('can have custom text', () => { + const $ = render('table', examples['table with head and caption']) + const $caption = $('.govuk-table__caption') + + expect($caption.text()).toBe('Caption 1: Months and rates') + }) + + it('can have additional classes', () => { + const $ = render('table', examples['table with head and caption']) + const $caption = $('.govuk-table__caption') + + expect($caption.hasClass('govuk-table__caption--m')).toBeTruthy() + }) + }) + + // ========================================================= + // Column headers + // ========================================================= + + describe('column headers', () => { + it('can be specified', () => { + const args = examples['table with head'] + const $ = render('table', args) + + const headings = $('.govuk-table') + .find('thead tr th') + .map((_, e) => $(e).text()) + .get() + + expect(headings).toEqual([ + 'Month you apply', + 'Rate for bicycles', + 'Rate for vehicles' + ]) + }) + + it('have HTML escaped when passed as text', () => { + const $ = render('table', examples['html as text']) + + const $th = $('.govuk-table thead tr th') + + expect($th.html()).toBe( + 'Foo <script>hacking.do(1337)</script>' + ) + }) + + it('allow HTML when passed as HTML', () => { + const $ = render('table', examples.html) + + const $th = $('.govuk-table thead tr th') + + expect($th.html()).toBe('Foo bar') + }) + + it('can have a format specified', () => { + const $ = render('table', examples['table with head']) + + const $th = $('.govuk-table thead tr th') + + expect($th.hasClass('govuk-table__header--numeric')).toBeTruthy() + }) + + it('can have additional classes', () => { + const $ = render('table', examples['head with classes']) + + const $th = $('.govuk-table thead tr th') + + expect($th.hasClass('my-custom-class')).toBeTruthy() + }) + + it('can have rowspan specified', () => { + const $ = render('table', examples['head with rowspan and colspan']) + + const $th = $('.govuk-table thead tr th') + + expect($th.attr('rowspan')).toBe('2') + }) + + it('can have colspan specified', () => { + const $ = render('table', examples['head with rowspan and colspan']) + + const $th = $('.govuk-table thead tr th') + + expect($th.attr('colspan')).toBe('2') + }) + + it('can have additional attributes', () => { + const $ = render('table', examples['head with attributes']) + + const $th = $('.govuk-table thead tr th') + + expect($th.attr('data-fizz')).toBe('buzz') + }) + }) + + // ========================================================= + // Row headers + // ========================================================= + + describe('row headers', () => { + describe('when firstCellIsHeader is false', () => { + it('are not included', () => { + const $ = render('table', examples.default) + + const cells = $('.govuk-table') + .find('tbody tr td') + .map((_, e) => $(e).text()) + .get() + + expect(cells).toEqual([ + 'January', + 'ยฃ85', + 'ยฃ95', + 'February', + 'ยฃ75', + 'ยฃ55', + 'March', + 'ยฃ165', + 'ยฃ125' + ]) + }) + }) + + describe('when firstCellIsHeader is true', () => { + it('are included', () => { + const $ = render('table', examples['with firstCellIsHeader true']) + + const headings = $('.govuk-table') + .find('tbody tr th') + .map((_, e) => $(e).text()) + .get() + + expect(headings).toEqual(['January', 'February', 'March']) + }) + + it('have HTML escaped when passed as text', () => { + const $ = render( + 'table', + examples['firstCellIsHeader with html as text'] + ) + + const $th = $('.govuk-table tbody tr th') + + expect($th.html()).toBe( + 'Foo <script>hacking.do(1337)</script>' + ) + }) + + it('allow HTML when passed as HTML', () => { + const $ = render('table', examples['firstCellIsHeader with html']) + + const $th = $('.govuk-table tbody tr th') + + expect($th.html()).toBe('Foo bar') + }) + + it('are associated with their rows using scope="row"', () => { + const $ = render('table', examples['with firstCellIsHeader true']) + + const $th = $('.govuk-table').find('tbody tr th') + + expect($th.attr('scope')).toBe('row') + }) + + it('have the govuk-table__header class', () => { + const $ = render('table', examples['with firstCellIsHeader true']) + + const $th = $('.govuk-table').find('tbody tr th') + + expect($th.hasClass('govuk-table__header')).toBeTruthy() + }) + + it('can have additional classes', () => { + const $ = render('table', examples['firstCellIsHeader with classes']) + + const $th = $('.govuk-table').find('tbody tr th') + + expect($th.hasClass('my-custom-class')).toBeTruthy() + }) + + it('can have rowspan specified', () => { + const $ = render( + 'table', + examples['firstCellIsHeader with rowspan and colspan'] + ) + + const $th = $('.govuk-table').find('tbody tr th') + + expect($th.attr('rowspan')).toBe('2') + }) + + it('can have colspan specified', () => { + const $ = render( + 'table', + examples['firstCellIsHeader with rowspan and colspan'] + ) + + const $th = $('.govuk-table').find('tbody tr th') + + expect($th.attr('colspan')).toBe('2') + }) + + it('can have additional attributes', () => { + const $ = render('table', examples['firstCellIsHeader with attributes']) + + const $th = $('.govuk-table').find('tbody tr th') + + expect($th.attr('data-fizz')).toBe('buzz') + }) + }) + }) + + // ========================================================= + // Cells + // ========================================================= + + describe('cells', () => { + it('can be specified', () => { + const $ = render('table', examples.default) + + const cells = $('.govuk-table') + .find('tbody tr') + .map((_, tr) => { + return [ + $(tr) + .find('td') + .map((_, td) => $(td).text()) + .get() + ] + }) + .get() + + expect(cells).toEqual([ + ['January', 'ยฃ85', 'ยฃ95'], + ['February', 'ยฃ75', 'ยฃ55'], + ['March', 'ยฃ165', 'ยฃ125'] + ]) + }) + + it('can be skipped when falsy', () => { + const $ = render('table', examples['with falsy items']) + + const cells = $('.govuk-table') + .find('tbody tr') + .map((_, tr) => { + return [ + $(tr) + .find('td') + .map((_, td) => $(td).text()) + .get() + ] + }) + .get() + + expect(cells).toEqual([ + ['A', '1'], + ['B', '2'], + ['C', '3'] + ]) + }) + + it('have HTML escaped when passed as text', () => { + const $ = render('table', examples['html as text']) + + const $td = $('.govuk-table td') + + expect($td.html()).toBe( + 'Foo <script>hacking.do(1337)</script>' + ) + }) + + it('allow HTML when passed as HTML', () => { + const $ = render('table', examples.html) + + const $td = $('.govuk-table td') + + expect($td.html()).toBe('Foo bar') + }) + + it('can have a format specified', () => { + const $ = render('table', examples.default) + + const $td = $('.govuk-table td') + + expect($td.hasClass('govuk-table__cell--numeric')).toBeTruthy() + }) + + it('can have additional classes', () => { + const $ = render('table', examples['rows with classes']) + + const $td = $('.govuk-table td') + + expect($td.hasClass('my-custom-class')).toBeTruthy() + }) + + it('can have rowspan specified', () => { + const $ = render('table', examples['rows with rowspan and colspan']) + + const $td = $('.govuk-table td') + + expect($td.attr('rowspan')).toBe('2') + }) + + it('can have colspan specified', () => { + const $ = render('table', examples['rows with rowspan and colspan']) + + const $td = $('.govuk-table td') + + expect($td.attr('colspan')).toBe('2') + }) + + it('can have additional attributes', () => { + const $ = render('table', examples['rows with attributes']) + + const $td = $('.govuk-table td') + + expect($td.attr('data-fizz')).toBe('buzz') + }) + }) +}) diff --git a/package/govuk/components/tabs/README.md b/packages/govuk-frontend/src/govuk/components/tabs/README.md similarity index 100% rename from package/govuk/components/tabs/README.md rename to packages/govuk-frontend/src/govuk/components/tabs/README.md diff --git a/packages/govuk-frontend/src/govuk/components/tabs/_index.scss b/packages/govuk-frontend/src/govuk/components/tabs/_index.scss new file mode 100644 index 0000000000..cafa4029c8 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/tabs/_index.scss @@ -0,0 +1,132 @@ +@include govuk-exports("govuk/component/tabs") { + .govuk-tabs { + @include govuk-responsive-margin(1, "top"); + @include govuk-responsive-margin(6, "bottom"); + @include govuk-font($size: 19); + } + + .govuk-tabs__title { + // Set the size and weight again because this element is a heading and the + // user agent font size overrides the inherited font size + @include govuk-font-size($size: 19); + @include govuk-typography-weight-regular; + @include govuk-text-colour; + margin-bottom: govuk-spacing(2); + } + + .govuk-tabs__list { + margin: 0; + padding: 0; + list-style: none; + @include govuk-responsive-margin(6, "bottom"); + } + + .govuk-tabs__list-item { + margin-left: govuk-spacing(5); + + &::before { + @include govuk-text-colour; + content: "\2014 "; // "โ€” " + margin-left: govuk-spacing(-5); + padding-right: govuk-spacing(1); + } + } + + .govuk-tabs__tab { + @include govuk-link-common; + @include govuk-link-style-default; + + display: inline-block; + margin-bottom: govuk-spacing(2); + } + + .govuk-tabs__panel { + @include govuk-responsive-margin(8, "bottom"); + } + + // GOV.UK Frontend JavaScript enabled + .govuk-frontend-supported { + @include govuk-media-query($from: tablet) { + .govuk-tabs__list { + @include govuk-clearfix; + margin-bottom: 0; + border-bottom: 1px solid $govuk-border-colour; + } + + .govuk-tabs__title { + display: none; + } + + .govuk-tabs__list-item { + position: relative; + + margin-right: govuk-spacing(1); + margin-bottom: 0; + margin-left: 0; + padding: govuk-spacing(2) govuk-spacing(4); + + float: left; + background-color: govuk-colour("light-grey"); + text-align: center; + + &::before { + content: none; + } + } + + .govuk-tabs__list-item--selected { + $border-width: 1px; + + position: relative; + + margin-top: govuk-spacing(-1); + + // Compensation for border (otherwise we get a shift) + margin-bottom: -$border-width; + padding-top: govuk-spacing(3) - $border-width; + padding-right: govuk-spacing(4) - $border-width; + padding-bottom: govuk-spacing(3) + $border-width; + padding-left: govuk-spacing(4) - $border-width; + + border: $border-width solid $govuk-border-colour; + border-bottom: 0; + + background-color: $govuk-body-background-colour; + + .govuk-tabs__tab { + text-decoration: none; + } + } + + .govuk-tabs__tab { + @include govuk-link-style-text; + + margin-bottom: 0; + + &::after { + content: ""; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + } + + .govuk-tabs__panel { + @include govuk-responsive-margin(0, "bottom"); + padding: govuk-spacing(6) govuk-spacing(4); + border: 1px solid $govuk-border-colour; + border-top: 0; + + & > :last-child { + margin-bottom: 0; + } + } + + .govuk-tabs__panel--hidden { + display: none; + } + } + } +} diff --git a/package/govuk/components/tabs/_tabs.scss b/packages/govuk-frontend/src/govuk/components/tabs/_tabs.scss similarity index 100% rename from package/govuk/components/tabs/_tabs.scss rename to packages/govuk-frontend/src/govuk/components/tabs/_tabs.scss diff --git a/packages/govuk-frontend/src/govuk/components/tabs/accessibility.puppeteer.test.mjs b/packages/govuk-frontend/src/govuk/components/tabs/accessibility.puppeteer.test.mjs new file mode 100644 index 0000000000..a6f05f0259 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/tabs/accessibility.puppeteer.test.mjs @@ -0,0 +1,31 @@ +import { axe, render } from '@govuk-frontend/helpers/puppeteer' +import { getExamples } from '@govuk-frontend/lib/components' + +describe('/components/tabs', () => { + let axeRules + + beforeAll(() => { + axeRules = { + /** + * Ignore 'Element has insufficient color contrast' for WCAG Level AAA + * + * Affects 'Anchor' link + */ + 'color-contrast-enhanced': { enabled: false } + } + }) + + describe('component examples', () => { + it('passes accessibility tests', async () => { + const examples = await getExamples('tabs') + + for (const exampleName in examples) { + await render(page, 'tabs', examples[exampleName]) + // Log errors for invalid examples + .catch(({ message }) => console.warn(message)) + + await expect(axe(page, axeRules)).resolves.toHaveNoViolations() + } + }, 120000) + }) +}) diff --git a/package/govuk/components/tabs/macro.njk b/packages/govuk-frontend/src/govuk/components/tabs/macro.njk similarity index 100% rename from package/govuk/components/tabs/macro.njk rename to packages/govuk-frontend/src/govuk/components/tabs/macro.njk diff --git a/packages/govuk-frontend/src/govuk/components/tabs/tabs.mjs b/packages/govuk-frontend/src/govuk/components/tabs/tabs.mjs new file mode 100644 index 0000000000..271a32cadf --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/tabs/tabs.mjs @@ -0,0 +1,527 @@ +import { getBreakpoint, getFragmentFromUrl } from '../../common/index.mjs' +import { ElementError } from '../../errors/index.mjs' +import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs' + +/** + * Tabs component + * + * @preserve + */ +export class Tabs extends GOVUKFrontendComponent { + /** @private */ + $tabs + + /** @private */ + $tabList + + /** @private */ + $tabListItems + + /** @private */ + jsHiddenClass = 'govuk-tabs__panel--hidden' + + /** @private */ + changingHash = false + + /** @private */ + boundTabClick + + /** @private */ + boundTabKeydown + + /** @private */ + boundOnHashChange + + /** + * @private + * @type {MediaQueryList | null} + */ + mql = null + + /** + * @param {Element | null} $root - HTML element to use for tabs + */ + constructor($root) { + super($root) + + const $tabs = this.$root.querySelectorAll('a.govuk-tabs__tab') + if (!$tabs.length) { + throw new ElementError({ + component: Tabs, + identifier: 'Links (``)' + }) + } + + this.$tabs = $tabs + + // Save bound functions so we can remove event listeners during teardown + this.boundTabClick = this.onTabClick.bind(this) + this.boundTabKeydown = this.onTabKeydown.bind(this) + this.boundOnHashChange = this.onHashChange.bind(this) + + const $tabList = this.$root.querySelector('.govuk-tabs__list') + const $tabListItems = this.$root.querySelectorAll( + 'li.govuk-tabs__list-item' + ) + + if (!$tabList) { + throw new ElementError({ + component: Tabs, + identifier: 'List (`