├── postcss.config.js ├── src ├── declarations.d.ts ├── interfaces │ ├── CookieCategoriesPreferences.ts │ ├── CookieCategories.ts │ └── Options.ts ├── htmlTools.ts ├── language-list.const.ts ├── themes │ ├── _theme.scss │ ├── theme-styles.ts │ └── themesController.ts ├── index-accessibility.test.ts ├── index-direction.test.ts ├── index-button.test.ts ├── styles.scss ├── index.ts ├── preferencesControl.ts ├── index-constructor.test.ts └── index.test.ts ├── webpack.dev.js ├── tsconfig.json ├── CODE_OF_CONDUCT.md ├── .github └── workflows │ ├── publish-workflow.yaml │ └── build.yaml ├── azure-pipelines.yml ├── dist ├── index.html └── consent-banner.d.ts ├── webpack.prod.js ├── styleMock.js ├── jest.config.js ├── LICENSE ├── package.json ├── webpack.common.js ├── .gitignore ├── tslint.json ├── SECURITY.md └── README.md /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ] 5 | } -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.scss"; 2 | declare module "style-loader/dist/runtime/injectStylesIntoStyleTag"; 3 | -------------------------------------------------------------------------------- /src/interfaces/CookieCategoriesPreferences.ts: -------------------------------------------------------------------------------- 1 | export interface ICookieCategoriesPreferences { 2 | [key: string]: boolean | undefined; 3 | } 4 | -------------------------------------------------------------------------------- /src/interfaces/CookieCategories.ts: -------------------------------------------------------------------------------- 1 | export interface ICookieCategory { 2 | id: string; 3 | name: string; 4 | descHtml: string; 5 | isUnswitchable?: boolean; // optional, prevents toggling the category. True only for categories like Essential cookies. 6 | } 7 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require("webpack-merge"); 2 | const common = require("./webpack.common.js"); 3 | 4 | module.exports = merge(common, { 5 | devServer: { 6 | contentBase: "./dist", 7 | watchContentBase: true, 8 | headers: { 9 | "Content-Security-Policy": "style-src 'nonce-test1'" 10 | } 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "strict": true, 6 | "noImplicitReturns": true, 7 | "noImplicitAny": true, 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "target": "es5", 11 | "allowJs": true, 12 | }, 13 | "include": [ 14 | "./src/**/*" 15 | ] 16 | } -------------------------------------------------------------------------------- /src/htmlTools.ts: -------------------------------------------------------------------------------- 1 | export class HtmlTools { 2 | public static escapeHtml(s: string | undefined): string { 3 | if (s) { 4 | return s.replace(/&/g, "&") 5 | .replace(//g, ">") 7 | .replace(/"/g, """) 8 | .replace(/'/g, "'"); 9 | } 10 | else { 11 | return ""; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/workflows/publish-workflow.yaml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | # Setup .npmrc file to publish to npm 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: '12.x' 14 | registry-url: 'https://registry.npmjs.org' 15 | - run: npm install 16 | - run: npm publish 17 | env: 18 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_NEW }} 19 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'ubuntu-latest' 11 | 12 | steps: 13 | - task: NodeTool@0 14 | inputs: 15 | versionSpec: '12.x' 16 | displayName: 'Install Node.js' 17 | 18 | - script: | 19 | npm install 20 | npm run build-prod 21 | npm run lint 22 | npm run test-ci 23 | displayName: 'npm install and build' 24 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Test page 6 | 7 | 8 | 9 | 10 |
11 |

aaabcdef

12 | 13 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/language-list.const.ts: -------------------------------------------------------------------------------- 1 | // culture can be just language "en" or language-country "en-us". 2 | // Based on language RTL should be applied (https://www.w3.org/International/questions/qa-scripts.en) 3 | // Locale IDs are from 4 | // https://help.bing.microsoft.com/#apex/18/en-US/10004/-1 5 | // https://docs.microsoft.com/en-us/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a 6 | export const RTL_LANGUAGE: string[] = [ 7 | 'ar', 8 | 'he', 9 | 'ps', 10 | 'ur', 11 | 'fa', 12 | 'pa', 13 | 'sd', 14 | 'tk', 15 | 'ug', 16 | 'yi', 17 | 'syr', 18 | 'ks-arab' // Kashmiri (Arabic) is rtl 19 | ]; 20 | 21 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | let mergedConfig = common; 5 | 6 | let targetRule = mergedConfig.module.rules[1]; 7 | let targetRegex = /\.scss$/; 8 | if (targetRule.test.toString() !== targetRegex.toString()) { 9 | targetRule = mergedConfig.module.rules.find(element => element.test.toString() === targetRegex.toString()); 10 | } 11 | 12 | let targetLoader = targetRule.use[1]; 13 | if (targetLoader.loader !== "css-loader") { 14 | targetLoader = targetRule.use.find(element => element.loader === "css-loader"); 15 | } 16 | 17 | targetLoader.options.modules.localIdentName = "[hash:base64]"; 18 | module.exports = mergedConfig; 19 | -------------------------------------------------------------------------------- /styleMock.js: -------------------------------------------------------------------------------- 1 | // Reference: identity-obj-proxy 2 | 3 | /* eslint-disable no-var, comma-dangle */ 4 | var Reflect; // eslint-disable-line no-unused-vars 5 | var idObj; 6 | 7 | function checkIsNodeV6OrAbove() { 8 | if (typeof process === 'undefined') { 9 | return false; 10 | } 11 | 12 | return parseInt(process.versions.node.split('.')[0], 10) >= 6; 13 | } 14 | 15 | if (!checkIsNodeV6OrAbove()) { 16 | Reflect = require('harmony-reflect'); // eslint-disable-line global-require 17 | } 18 | 19 | idObj = new Proxy({}, { 20 | get: function getter(target, key) { 21 | if (key === '__esModule') { 22 | return false; 23 | } 24 | else if (key === 'locals') { 25 | return idObj; 26 | } 27 | return key; 28 | } 29 | }); 30 | 31 | module.exports = idObj; 32 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | clearMocks: true, 6 | collectCoverageFrom: [ 7 | "src/**/*.{js,jsx,ts,tsx}", 8 | "!*.d.ts" 9 | ], 10 | coverageReporters: [ 11 | "html" 12 | ], 13 | moduleNameMapper: { 14 | "^.+\\.(css|scss)$": "/styleMock.js" 15 | }, 16 | resolver: "jest-pnp-resolver", 17 | testMatch: [ 18 | "/src/**/*.test.ts?(x)" 19 | ], 20 | testEnvironment: "jsdom", 21 | testURL: "http://localhost", 22 | transform: { 23 | "^.+\\.tsx?$": "/node_modules/ts-jest", 24 | }, 25 | transformIgnorePatterns: [ 26 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$" 27 | ], 28 | testPathIgnorePatterns: [ 29 | ], 30 | //testResultsProcessor: "/node_modules/jest-junit-reporter", 31 | }; 32 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | # Steps represent a sequence of tasks that will be executed as part of the job 20 | steps: 21 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 22 | - uses: actions/checkout@v2 23 | # Setup .npmrc file to publish to npm 24 | - uses: actions/setup-node@v1 25 | with: 26 | node-version: '12.x' 27 | registry-url: 'https://registry.npmjs.org' 28 | # Runs a single command using the runners shell 29 | - run: npm install 30 | - run: npm run build 31 | - run: npm run test-ci 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "consent-banner", 3 | "version": "2.2.0", 4 | "description": "The library which will generate a banner at the specified position for asking the cookie preferences.", 5 | "main": "dist/consent-banner.js", 6 | "types": "dist/consent-banner.d.ts", 7 | "keywords": [ 8 | "cookie preferences", 9 | "banner" 10 | ], 11 | "homepage": "https://github.com/microsoft/consent-banner", 12 | "author": "Microsoft", 13 | "license": "MIT", 14 | "files": [ 15 | "dist/consent-banner.js", 16 | "dist/consent-banner.d.ts" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/microsoft/consent-banner.git" 21 | }, 22 | "scripts": { 23 | "prepare": "npm run build-prod", 24 | "build": "webpack -d --mode development --config ./webpack.dev.js", 25 | "build-prod": "webpack -p --mode production --config ./webpack.prod.js", 26 | "start": "webpack-dev-server --open --config ./webpack.dev.js", 27 | "test": "jest --watchAll --config ./jest.config.js", 28 | "test-coverage": "jest --coverage --config ./jest.config.js", 29 | "test-ci": "jest --config ./jest.config.js", 30 | "lint": "tslint -p tsconfig.json -t stylish" 31 | }, 32 | "dependencies": { 33 | "style-loader": "^1.1.4" 34 | }, 35 | "devDependencies": { 36 | "@types/jest": "^25.2.1", 37 | "@types/node-sass": "^4.11.0", 38 | "awesome-typescript-loader": "^5.2.1", 39 | "css-loader": "^3.5.2", 40 | "identity-obj-proxy": "^3.0.0", 41 | "jest": "^25.3.0", 42 | "jest-junit-reporter": "^1.1.0", 43 | "jest-pnp-resolver": "^1.2.1", 44 | "node-sass": "^4.14.1", 45 | "postcss-loader": "^3.0.0", 46 | "postcss-preset-env": "^6.7.0", 47 | "sass-loader": "^8.0.2", 48 | "ts-jest": "^25.3.1", 49 | "tslint": "^6.1.1", 50 | "tslint-microsoft-contrib": "^6.2.0", 51 | "typescript": "^3.8.3", 52 | "webpack": "^4.42.1", 53 | "webpack-cli": "^3.3.11", 54 | "webpack-dev-server": "^3.11.0", 55 | "webpack-merge": "^5.1.4" 56 | }, 57 | "browserslist": [ 58 | ">0.2%", 59 | "not ie <= 9", 60 | "not dead", 61 | "not op_mini all" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | 4 | const config = { 5 | entry: "./src/index.ts", 6 | output: { 7 | path: path.resolve(__dirname, "dist"), 8 | filename: "consent-banner.js", 9 | libraryTarget: "umd", 10 | library: "ConsentControl" 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.css$/, 16 | exclude: /\.module\.css$/, 17 | use: [ 18 | "style-loader", 19 | { 20 | loader: "css-loader", 21 | options: { 22 | importLoaders: 1, 23 | modules: true 24 | } 25 | }, 26 | { 27 | loader: "postcss-loader", options: { 28 | ident: "postcss", 29 | plugins: () => [ 30 | postcssPresetEnv({ 31 | autoprefixer: { 32 | flexbox: "no-2009", 33 | }, 34 | stage: 2, 35 | }) 36 | ] 37 | } 38 | } 39 | ], 40 | }, 41 | { 42 | test: /\.scss$/, 43 | use: [ 44 | { 45 | loader: "css-loader", 46 | options: { 47 | importLoaders: 1, 48 | modules: { 49 | exportGlobals: true, 50 | localIdentName: "[path][name]__[local]--[hash:base64:5]" // use '[hash:base64]' for production 51 | } 52 | } 53 | }, 54 | { 55 | loader: "postcss-loader", options: { 56 | ident: "postcss", 57 | plugins: () => [ 58 | require("postcss-preset-env")({ 59 | autoprefixer: { 60 | flexbox: "no-2009", 61 | }, 62 | stage: 2 63 | }) 64 | ] 65 | } 66 | }, 67 | "sass-loader" 68 | ] 69 | }, 70 | { 71 | test: /\.ts(x)?$/, 72 | exclude: /node_modules/, 73 | use: [ 74 | "awesome-typescript-loader" 75 | ] 76 | } 77 | ] 78 | }, 79 | resolve: { 80 | extensions: [ 81 | ".ts", 82 | ".js" 83 | ] 84 | } 85 | }; 86 | 87 | module.exports = config; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # Host html file 48 | !dist/index.html 49 | 50 | # TypeScript v2 declaration files 51 | !dist/consent-banner.d.ts 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | .env.test 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | 84 | # Next.js build output 85 | .next 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | dist 90 | 91 | # Gatsby files 92 | .cache/ 93 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 94 | # https://nextjs.org/blog/next-9-1#public-directory-support 95 | # public 96 | 97 | # vuepress build output 98 | .vuepress/dist 99 | 100 | # Serverless directories 101 | .serverless/ 102 | 103 | # FuseBox cache 104 | .fusebox/ 105 | 106 | # DynamoDB Local files 107 | .dynamodb/ 108 | 109 | # TernJS port file 110 | .tern-port 111 | 112 | # OS generated files 113 | .DS_Store 114 | .DS_Store? 115 | ._* 116 | .Spotlight-V100 117 | .Trashes 118 | ehthumbs.db 119 | Thumbs.db 120 | -------------------------------------------------------------------------------- /src/interfaces/Options.ts: -------------------------------------------------------------------------------- 1 | export interface IOptions { 2 | textResources?: ITextResources; 3 | themes?: IThemes; 4 | initialTheme?: string; 5 | stylesNonce?: string; 6 | } 7 | 8 | export interface IThemes { 9 | [key: string]: ITheme | undefined; 10 | 11 | light?: ITheme; 12 | dark?: ITheme; 13 | "high-contrast"?: ITheme; 14 | } 15 | 16 | export interface ITheme { 17 | "close-button-color": string; 18 | "secondary-button-disabled-opacity": string; 19 | "secondary-button-hover-shadow": string; 20 | "primary-button-disabled-opacity": string; 21 | "primary-button-hover-border": string; 22 | "primary-button-disabled-border": string; 23 | "primary-button-hover-shadow": string; 24 | 25 | "banner-background-color": string; 26 | "dialog-background-color": string; 27 | "primary-button-color": string; 28 | "text-color": string; 29 | "secondary-button-color": string; 30 | "secondary-button-disabled-color": string; 31 | "secondary-button-border": string; 32 | 33 | "background-color-between-page-and-dialog"?: string; 34 | "dialog-border-color"?: string; 35 | "hyperlink-font-color"?: string; 36 | "secondary-button-hover-color"?: string; 37 | "secondary-button-hover-border"?: string; 38 | "secondary-button-disabled-border"?: string; 39 | "secondary-button-focus-border-color"?: string; 40 | "secondary-button-text-color"?: string; 41 | "secondary-button-disabled-text-color"?: string; 42 | "primary-button-hover-color"?: string; 43 | "primary-button-disabled-color"?: string; 44 | "primary-button-border"?: string; 45 | "primary-button-focus-border-color"?: string; 46 | "primary-button-text-color"?: string; 47 | "primary-button-disabled-text-color"?: string; 48 | "radio-button-border-color"?: string; 49 | "radio-button-checked-background-color"?: string; 50 | "radio-button-hover-border-color"?: string; 51 | "radio-button-hover-background-color"?: string; 52 | "radio-button-disabled-color"?: string; 53 | "radio-button-disabled-border-color"?: string; 54 | } 55 | 56 | export interface ITextResources { 57 | bannerMessageHtml?: string; 58 | acceptAllLabel?: string; 59 | rejectAllLabel?: string; 60 | moreInfoLabel?: string; 61 | preferencesDialogCloseLabel?: string; 62 | preferencesDialogTitle?: string; 63 | preferencesDialogDescHtml?: string; 64 | acceptLabel?: string; 65 | rejectLabel?: string; 66 | saveLabel?: string; 67 | resetLabel?: string; 68 | } 69 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-microsoft-contrib"], 3 | "rules": { 4 | // coding style 5 | "align": [true, "elements", "members", "statements"], 6 | "ban-types": [true, ["Object", "Strong typing preferred"], ["AnyAction"]], 7 | "function-name": [true, { 8 | "static-method-regex": "^[a-z][\\w\\d]+$" 9 | }], 10 | "quotemark": [true, "double"], 11 | "linebreak-style": false, 12 | "max-func-body-length": false, 13 | "max-line-length": false, 14 | "member-ordering": [true, {"order": [ 15 | "public-static-field", 16 | "public-instance-field", 17 | "protected-static-field", 18 | "protected-instance-field", 19 | "private-static-field", 20 | "private-instance-field", 21 | "public-constructor", 22 | "protected-constructor", 23 | "private-constructor", 24 | "public-static-method", 25 | "public-instance-method", 26 | "protected-static-method", 27 | "private-static-method", 28 | "protected-instance-method", 29 | "private-instance-method" 30 | ] }], 31 | "newline-before-return": false, 32 | "newline-per-chained-call": false, 33 | "no-consecutive-blank-lines": [true, 2], 34 | "typedef": [true, "parameter", "property-declaration", "member-variable-declaration", "array-destructuring"], 35 | "variable-name": [true, "allow-pascal-case", "ban-keywords"], 36 | 37 | // modules 38 | "export-name": false, 39 | "import-name": false, 40 | "no-submodule-imports": false, 41 | "no-relative-imports": false, 42 | "no-default-export": false, 43 | "no-import-side-effect": [true, {"ignore-module": "(\\.png|\\.jpg|\\.svg|\\.css|\\.scss)$"}], 44 | "no-implicit-dependencies": [true, "dev"], 45 | "ordered-imports": false, 46 | 47 | // documentation 48 | "completed-docs": false, 49 | "missing-jsdoc": false, 50 | 51 | // best practices 52 | "no-floating-promises": true, 53 | "no-increment-decrement": false, 54 | "no-null-keyword": false, 55 | "no-parameter-reassignment": false, 56 | "no-unsafe-any": false, 57 | "no-unused-expression": [true, "allow-fast-null-checks", "allow-new"], 58 | "no-void-expression": [true, "ignore-arrow-function-shorthand"], 59 | "jsx-no-lambda": false, 60 | "jsx-no-multiline-js": false, 61 | "strict-boolean-expressions": false, 62 | "underscore-consistent-invocation": false, 63 | "use-simple-attributes": false, 64 | "no-console": false, 65 | 66 | // tests 67 | "mocha-no-side-effect-code": false 68 | }, 69 | "linterOptions": { 70 | "exclude": [ 71 | "*.js", 72 | "node_modules/**/*.ts" 73 | ] 74 | } 75 | } -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /src/themes/_theme.scss: -------------------------------------------------------------------------------- 1 | $secondary-button-disabled-opacity: 1; 2 | $secondary-button-hover-shadow: 0px 4px 10px rgba(0, 0, 0, 0.25); 3 | $primary-button-disabled-opacity: 1; 4 | $primary-button-hover-border: none; 5 | $primary-button-disabled-border: none; 6 | $primary-button-hover-shadow: 0px 4px 10px rgba(0, 0, 0, 0.25); 7 | 8 | $primary-button-color: #0067B8; 9 | $secondary-button-color: #EBEBEB; 10 | $secondary-button-disabled-color: rgba(0, 0, 0, 0.2); 11 | $secondary-button-border: none; 12 | 13 | $text-color: #000000; 14 | $hyperlink-font-color: #0067B8; 15 | 16 | $secondary-button-hover-color: #DBDBDB; 17 | $secondary-button-hover-border: none; 18 | $secondary-button-disabled-border: none; 19 | $secondary-button-focus-border-color: #000000; 20 | $secondary-button-text-color: #000000; 21 | $secondary-button-disabled-text-color: rgba(0, 0, 0, 0.2); 22 | 23 | $primary-button-hover-color: #0067B8; 24 | $primary-button-disabled-color: rgba(0, 120, 215, 0.2); 25 | $primary-button-border: none; 26 | $primary-button-focus-border-color: #000000; 27 | $primary-button-text-color: #FFFFFF; 28 | $primary-button-disabled-text-color: rgba(0, 0, 0, 0.2); 29 | 30 | .textColorTheme { 31 | color: $text-color; 32 | } 33 | 34 | .hyperLinkTheme a { 35 | color: $hyperlink-font-color; 36 | } 37 | 38 | .secondaryButtonTheme { 39 | background-color: $secondary-button-color; 40 | border: $secondary-button-border; 41 | color: $secondary-button-text-color; 42 | 43 | &:enabled:hover { 44 | color: $secondary-button-text-color; 45 | background-color: $secondary-button-hover-color; 46 | box-shadow: $secondary-button-hover-shadow; 47 | border: $secondary-button-hover-border; 48 | } 49 | 50 | &:enabled:focus { 51 | background-color: $secondary-button-hover-color; 52 | box-shadow: $secondary-button-hover-shadow; 53 | border: 2px solid $secondary-button-focus-border-color; 54 | } 55 | 56 | &:disabled { 57 | opacity: $secondary-button-disabled-opacity; 58 | color: $secondary-button-disabled-text-color; 59 | background-color: $secondary-button-disabled-color; 60 | border: $secondary-button-disabled-border; 61 | } 62 | } 63 | 64 | .primaryButtonTheme { 65 | border: $primary-button-border; 66 | background-color: $primary-button-color; 67 | color: $primary-button-text-color; 68 | 69 | &:enabled:hover { 70 | color: $primary-button-text-color; 71 | background-color: $primary-button-hover-color; 72 | box-shadow: $primary-button-hover-shadow; 73 | border: $primary-button-hover-border; 74 | } 75 | 76 | &:enabled:focus { 77 | background-color: $primary-button-hover-color; 78 | box-shadow: $primary-button-hover-shadow; 79 | border: 2px solid $primary-button-focus-border-color; 80 | } 81 | 82 | &:disabled { 83 | opacity: $primary-button-disabled-opacity; 84 | color: $primary-button-disabled-text-color; 85 | background-color: $primary-button-disabled-color; 86 | border: $primary-button-disabled-border; 87 | } 88 | } -------------------------------------------------------------------------------- /src/themes/theme-styles.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_THEMES = { 2 | 3 | lightTheme: { 4 | "close-button-color": "#666666", 5 | "secondary-button-disabled-opacity": "1", 6 | "secondary-button-hover-shadow": "0px 4px 10px rgba(0, 0, 0, 0.25)", 7 | "primary-button-disabled-opacity": "1", 8 | "primary-button-hover-border": "none", 9 | "primary-button-disabled-border": "none", 10 | "primary-button-hover-shadow": "0px 4px 10px rgba(0, 0, 0, 0.25)", 11 | "banner-background-color": "#F2F2F2", 12 | "dialog-background-color": "#FFFFFF", 13 | "primary-button-color": "#0067B8", 14 | "text-color": "#000000", 15 | "secondary-button-color": "#EBEBEB", 16 | "secondary-button-disabled-color": "rgba(0, 0, 0, 0.2)", 17 | "secondary-button-border": "none", 18 | "background-color-between-page-and-dialog": "rgba(255, 255, 255, 0.6)", 19 | "dialog-border-color": "#0067B8", 20 | "hyperlink-font-color": "#0067B8", 21 | "secondary-button-hover-color": "#DBDBDB", 22 | "secondary-button-hover-border": "none", 23 | "secondary-button-disabled-border": "none", 24 | "secondary-button-focus-border-color": "#000000", 25 | "secondary-button-text-color": "#000000", 26 | "secondary-button-disabled-text-color": "rgba(0, 0, 0, 0.2)", 27 | "primary-button-hover-color": "#0067B8", 28 | "primary-button-disabled-color": "rgba(0, 120, 215, 0.2)", 29 | "primary-button-border": "none", 30 | "primary-button-focus-border-color": "#000000", 31 | "primary-button-text-color": "#FFFFFF", 32 | "primary-button-disabled-text-color": "rgba(0, 0, 0, 0.2)", 33 | "radio-button-border-color": "#000000", 34 | "radio-button-checked-background-color": "#000000", 35 | "radio-button-hover-border-color": "#0067B8", 36 | "radio-button-hover-background-color": "rgba(0, 0, 0, 0.8)", 37 | "radio-button-disabled-color": "rgba(0, 0, 0, 0.2)", 38 | "radio-button-disabled-border-color": "rgba(0, 0, 0, 0.2)" 39 | }, 40 | 41 | darkTheme: { 42 | "close-button-color": "#E3E3E3", 43 | "secondary-button-disabled-opacity": "0.5", 44 | "secondary-button-hover-shadow": "none", 45 | "primary-button-disabled-opacity": "0.5", 46 | "primary-button-hover-border": "1px solid rgba(0, 0, 0, 0)", 47 | "primary-button-disabled-border": "1px solid rgba(255, 255, 255, 0)", 48 | "primary-button-hover-shadow": "none", 49 | "banner-background-color": "#242424", 50 | "dialog-background-color": "#171717", 51 | "primary-button-color": "#4DB2FF", 52 | "text-color": "#E3E3E3", 53 | "secondary-button-color": "#171717", 54 | "secondary-button-disabled-color": "#2E2E2E", 55 | "secondary-button-border": "1px solid #C7C7C7", 56 | "background-color-between-page-and-dialog": "rgba(23, 23, 23, 0.6)", 57 | "dialog-border-color": "#4DB2FF", 58 | "hyperlink-font-color": "#4DB2FF", 59 | "secondary-button-hover-color": "#2E2E2E", 60 | "secondary-button-hover-border": "1px solid #C7C7C7", 61 | "secondary-button-disabled-border": "1px solid #242424", 62 | "secondary-button-focus-border-color": "#C7C7C7", 63 | "secondary-button-text-color": "#E3E3E3", 64 | "secondary-button-disabled-text-color": "#E3E3E3", 65 | "primary-button-hover-color": "#0091FF", 66 | "primary-button-disabled-color": "#4DB2FF", 67 | "primary-button-border": "1px solid #4DB2FF", 68 | "primary-button-focus-border-color": "#4DB2FF", 69 | "primary-button-text-color": "black", 70 | "primary-button-disabled-text-color": "black", 71 | "radio-button-border-color": "#E3E3E3", 72 | "radio-button-checked-background-color": "#E3E3E3", 73 | "radio-button-hover-border-color": "#4DB2FF", 74 | "radio-button-hover-background-color": "rgba(227, 227, 227, 0.8)", 75 | "radio-button-disabled-color": "rgba(227, 227, 227, 0.2)", 76 | "radio-button-disabled-border-color": "rgba(227, 227, 227, 0.2)" 77 | }, 78 | 79 | highContrast: { 80 | "close-button-color": "#E3E3E3", 81 | "secondary-button-disabled-opacity": "0.5", 82 | "secondary-button-hover-shadow": "none", 83 | "primary-button-disabled-opacity": "0.5", 84 | "primary-button-hover-border": "1px solid yellow", 85 | "primary-button-disabled-border": "1px solid white", 86 | "primary-button-hover-shadow": "none", 87 | "banner-background-color": "black", 88 | "dialog-background-color": "black", 89 | "primary-button-color": "yellow", 90 | "text-color": "white", 91 | "secondary-button-color": "black", 92 | "secondary-button-disabled-color": "black", 93 | "secondary-button-border": "1px solid white", 94 | "background-color-between-page-and-dialog": "rgba(0, 0, 0, 0.6)", 95 | "dialog-border-color": "yellow", 96 | "hyperlink-font-color": "yellow", 97 | "secondary-button-hover-color": "black", 98 | "secondary-button-hover-border": "1px solid yellow", 99 | "secondary-button-disabled-border": "1px solid black", 100 | "secondary-button-focus-border-color": "white", 101 | "secondary-button-text-color": "white", 102 | "secondary-button-disabled-text-color": "white", 103 | "primary-button-hover-color": "#FFFF33", 104 | "primary-button-disabled-color": "yellow", 105 | "primary-button-border": "1px solid yellow", 106 | "primary-button-focus-border-color": "yellow", 107 | "primary-button-text-color": "black", 108 | "primary-button-disabled-text-color": "black", 109 | "radio-button-border-color": "white", 110 | "radio-button-checked-background-color": "white", 111 | "radio-button-hover-border-color": "yellow", 112 | "radio-button-hover-background-color": "rgba(255, 255, 255, 0.8)", 113 | "radio-button-disabled-color": "rgba(255, 255, 255, 0.2)", 114 | "radio-button-disabled-border-color": "rgba(255, 255, 255, 0.2)" 115 | } 116 | 117 | }; 118 | -------------------------------------------------------------------------------- /dist/consent-banner.d.ts: -------------------------------------------------------------------------------- 1 | declare interface ITheme { 2 | "close-button-color": string; 3 | "secondary-button-disabled-opacity": string; 4 | "secondary-button-hover-shadow": string; 5 | "primary-button-disabled-opacity": string; 6 | "primary-button-hover-border": string; 7 | "primary-button-disabled-border": string; 8 | "primary-button-hover-shadow": string; 9 | "banner-background-color": string; 10 | "dialog-background-color": string; 11 | "primary-button-color": string; 12 | "text-color": string; 13 | "secondary-button-color": string; 14 | "secondary-button-disabled-color": string; 15 | "secondary-button-border": string; 16 | "background-color-between-page-and-dialog"?: string; 17 | "dialog-border-color"?: string; 18 | "hyperlink-font-color"?: string; 19 | "secondary-button-hover-color"?: string; 20 | "secondary-button-hover-border"?: string; 21 | "secondary-button-disabled-border"?: string; 22 | "secondary-button-focus-border-color"?: string; 23 | "secondary-button-text-color"?: string; 24 | "secondary-button-disabled-text-color"?: string; 25 | "primary-button-hover-color"?: string; 26 | "primary-button-disabled-color"?: string; 27 | "primary-button-border"?: string; 28 | "primary-button-focus-border-color"?: string; 29 | "primary-button-text-color"?: string; 30 | "primary-button-disabled-text-color"?: string; 31 | "radio-button-border-color"?: string; 32 | "radio-button-checked-background-color"?: string; 33 | "radio-button-hover-border-color"?: string; 34 | "radio-button-hover-background-color"?: string; 35 | "radio-button-disabled-color"?: string; 36 | "radio-button-disabled-border-color"?: string; 37 | } 38 | declare interface IThemes { 39 | [key: string]: ITheme | undefined; 40 | light?: ITheme; 41 | dark?: ITheme; 42 | "high-contrast"?: ITheme; 43 | } 44 | declare interface ITextResources { 45 | bannerMessageHtml?: string; 46 | acceptAllLabel?: string; 47 | rejectAllLabel?: string; 48 | moreInfoLabel?: string; 49 | preferencesDialogCloseLabel?: string; 50 | preferencesDialogTitle?: string; 51 | preferencesDialogDescHtml?: string; 52 | acceptLabel?: string; 53 | rejectLabel?: string; 54 | saveLabel?: string; 55 | resetLabel?: string; 56 | } 57 | declare interface IOptions { 58 | textResources?: ITextResources; 59 | themes?: IThemes; 60 | initialTheme?: string; 61 | stylesNonce?: string; 62 | } 63 | declare interface ICookieCategory { 64 | id: string; 65 | name: string; 66 | descHtml: string; 67 | isUnswitchable?: boolean; 68 | } 69 | declare interface ICookieCategoriesPreferences { 70 | [key: string]: boolean | undefined; 71 | } 72 | 73 | declare class PreferencesControl { } 74 | 75 | export declare class ConsentControl { 76 | private containerElement; 77 | culture: string; 78 | onPreferencesChanged: (cookieCategoriesPreferences: ICookieCategoriesPreferences) => void; 79 | cookieCategories: ICookieCategory[]; 80 | textResources: ITextResources; 81 | themes: IThemes; 82 | preferencesCtrl: PreferencesControl | null; 83 | private direction; 84 | private isDirty; 85 | defaultCookieCategories: ICookieCategory[]; 86 | defaultTextResources: ITextResources; 87 | constructor(containerElementOrId: string | HTMLElement, culture: string, onPreferencesChanged: (cookieCategoriesPreferences: ICookieCategoriesPreferences) => void, cookieCategories?: ICookieCategory[], options?: IOptions); 88 | /** 89 | * Set the text resources for the banner to display the text in each area 90 | * 91 | * @param {ITextResources} textResources the text want to be displayed 92 | */ 93 | setTextResources(textResources: ITextResources): void; 94 | /** 95 | * Use the passed theme to set the theme property 96 | * 97 | * @param {string} name the theme property that we want to set 98 | * @param {ITheme} theme the passed theme that we want to display 99 | */ 100 | createTheme(name: string, theme: ITheme): void; 101 | /** 102 | * Apply the theme and change banner and preferences dialog's color 103 | * 104 | * @param {string} themeName theme that will be applied 105 | */ 106 | applyTheme(themeName: string): void; 107 | /** 108 | * Insert all necessary HTML code and shows the banner. 109 | * Until this method is called there should be no HTML elements of the Consent Control anywhere in the DOM 110 | * 111 | * @param {ICookieCategoriesPreferences} cookieCategoriesPreferences object that indicates cookie categories preferences 112 | */ 113 | showBanner(cookieCategoriesPreferences: ICookieCategoriesPreferences): void; 114 | /** 115 | * Hides the banner and the Preferences Dialog. 116 | * Removes all HTML elements of the Consent Control from the DOM 117 | */ 118 | hideBanner(): void; 119 | /** 120 | * Shows Preferences Dialog. Leaves banner state unchanged 121 | * 122 | * @param {ICookieCategoriesPreferences} cookieCategoriesPreferences object that indicates cookie categories preferences 123 | */ 124 | showPreferences(cookieCategoriesPreferences: ICookieCategoriesPreferences): void; 125 | /** 126 | * Hides Preferences Dialog. 127 | * Removes all HTML elements of the Preferences Dialog from the DOM. Leaves banner state unchanged 128 | */ 129 | hidePreferences(): void; 130 | /** 131 | * Set the container that will be used for the banner 132 | * 133 | * @param {string | HTMLElement} containerElementOrId here the banner will be inserted 134 | */ 135 | setContainerElement(containerElementOrId: string | HTMLElement): void; 136 | /** 137 | * Return the container that is used for the banner 138 | */ 139 | getContainerElement(): HTMLElement | null; 140 | /** 141 | * Set the direction by passing the parameter or by checking the culture property 142 | * 143 | * @param {string} dir direction for the web, ltr or rtl 144 | */ 145 | setDirection(dir?: string): void; 146 | /** 147 | * Return the direction 148 | */ 149 | getDirection(): string; 150 | } 151 | -------------------------------------------------------------------------------- /src/index-accessibility.test.ts: -------------------------------------------------------------------------------- 1 | import { ConsentControl } from "./index"; 2 | import * as styles from "./styles.scss"; 3 | 4 | describe("Test accessibility when closing event occurs", () => { 5 | let testId: string = "app"; 6 | let testElementString = ` 7 |
8 | Click me 9 |
`; 10 | 11 | beforeEach(() => { 12 | let newDiv = document.createElement("div"); 13 | newDiv.setAttribute("id", testId); 14 | document.body.appendChild(newDiv); 15 | }); 16 | 17 | afterEach(() => { 18 | let child = document.getElementById(testId); 19 | if (child) { 20 | let parent = child.parentNode; 21 | 22 | if (parent) { 23 | parent.removeChild(child); 24 | } 25 | else { 26 | throw new Error("Parent not found error"); 27 | } 28 | } 29 | }); 30 | 31 | test("Focus should be on more info button after we click on close button", () => { 32 | let callBack = function () { return; }; 33 | let cc = new ConsentControl("app", "en", callBack); 34 | cc.showBanner({}); 35 | 36 | let cookieInfo = document.getElementsByClassName(styles.bannerButton)[2]; 37 | cookieInfo.focus(); 38 | cookieInfo.click(); 39 | 40 | let closeModalIcon = document.getElementsByClassName(styles.closeModalIcon)[0]; 41 | closeModalIcon.click(); 42 | 43 | expect(document.activeElement?.innerHTML).toBe("More info"); 44 | }); 45 | 46 | test("Call showBanner() and showPreferences(). Focus should be on anchor element after we click on close button", () => { 47 | document.body.innerHTML += testElementString; 48 | 49 | let callBack = function () { return; }; 50 | let cc = new ConsentControl("app", "en", callBack); 51 | 52 | cc.showBanner({}); 53 | 54 | let testElement = document.getElementById("testElementString")!; 55 | testElement.addEventListener("click", () => { 56 | cc.showPreferences({}); 57 | }); 58 | 59 | testElement.focus(); 60 | testElement.click(); 61 | 62 | let closeModalIcon = document.getElementsByClassName(styles.closeModalIcon)[0]; 63 | closeModalIcon.click(); 64 | 65 | expect(document.activeElement?.innerHTML).toBe("Click me"); 66 | }); 67 | }); 68 | 69 | describe("Test accessibility when radio buttons are clicked", () => { 70 | let testId: string = "app"; 71 | 72 | function testRadioBtnOutline(current: HTMLInputElement, previous?: HTMLInputElement): void { 73 | let currentParentElement = current.parentElement!; 74 | 75 | current.focus(); 76 | current.click(); 77 | 78 | expect(currentParentElement.className.indexOf(styles.cookieItemRadioBtnCtrlOutline) !== -1).toBeTruthy(); 79 | 80 | if (previous) { 81 | let previousParentElement = previous.parentElement!; 82 | expect(previousParentElement.className.indexOf(styles.cookieItemRadioBtnCtrlOutline) === -1).toBeTruthy(); 83 | } 84 | } 85 | 86 | beforeEach(() => { 87 | let newDiv = document.createElement("div"); 88 | newDiv.setAttribute("id", testId); 89 | document.body.appendChild(newDiv); 90 | }); 91 | 92 | afterEach(() => { 93 | let child = document.getElementById(testId); 94 | if (child) { 95 | let parent = child.parentNode; 96 | 97 | if (parent) { 98 | parent.removeChild(child); 99 | } 100 | else { 101 | throw new Error("Parent not found error"); 102 | } 103 | } 104 | }); 105 | 106 | test("The outline of radio button appears when radio button is clicked", () => { 107 | let callBack = function () { return; }; 108 | let cc = new ConsentControl("app", "en", callBack); 109 | cc.showBanner({}); 110 | 111 | let cookieInfo = document.getElementsByClassName(styles.bannerButton)[2]; 112 | cookieInfo.click(); 113 | 114 | let cookieItemRadioBtn: HTMLInputElement[] = [].slice.call(document.getElementsByClassName(styles.cookieItemRadioBtn)); 115 | 116 | testRadioBtnOutline(cookieItemRadioBtn[0]); 117 | testRadioBtnOutline(cookieItemRadioBtn[1]); 118 | testRadioBtnOutline(cookieItemRadioBtn[2]); 119 | testRadioBtnOutline(cookieItemRadioBtn[3]); 120 | testRadioBtnOutline(cookieItemRadioBtn[4]); 121 | testRadioBtnOutline(cookieItemRadioBtn[5]); 122 | }); 123 | 124 | test("The outline of previous clicked radio button is removed when another radio button is clicked", () => { 125 | let callBack = function () { return; }; 126 | let cc = new ConsentControl("app", "en", callBack); 127 | cc.showBanner({}); 128 | 129 | let cookieInfo = document.getElementsByClassName(styles.bannerButton)[2]; 130 | cookieInfo.click(); 131 | 132 | let cookieItemRadioBtn: HTMLInputElement[] = [].slice.call(document.getElementsByClassName(styles.cookieItemRadioBtn)); 133 | 134 | cookieItemRadioBtn[0].focus(); 135 | cookieItemRadioBtn[0].click(); 136 | 137 | testRadioBtnOutline(cookieItemRadioBtn[0], cookieItemRadioBtn[1]); 138 | testRadioBtnOutline(cookieItemRadioBtn[1], cookieItemRadioBtn[2]); 139 | }); 140 | 141 | test("The outline of previous clicked radio button is removed when 'Reset all' button is clicked", () => { 142 | let callBack = function () { return; }; 143 | let cc = new ConsentControl("app", "en", callBack); 144 | cc.showBanner({}); 145 | 146 | let cookieInfo = document.getElementsByClassName(styles.bannerButton)[2]; 147 | cookieInfo.click(); 148 | 149 | let cookieItemRadioBtn: HTMLInputElement[] = [].slice.call(document.getElementsByClassName(styles.cookieItemRadioBtn)); 150 | 151 | cookieItemRadioBtn[0].focus(); 152 | cookieItemRadioBtn[0].click(); 153 | cookieItemRadioBtn[3].focus(); 154 | cookieItemRadioBtn[3].click(); 155 | 156 | let resetAllBtn = document.getElementsByClassName(styles.modalButtonReset)[0]; 157 | resetAllBtn.focus(); 158 | resetAllBtn.click(); 159 | 160 | expect(cookieItemRadioBtn[0].parentElement!.className.indexOf(styles.cookieItemRadioBtnCtrlOutline) === -1).toBeTruthy(); 161 | expect(cookieItemRadioBtn[3].parentElement!.className.indexOf(styles.cookieItemRadioBtnCtrlOutline) === -1).toBeTruthy(); 162 | }); 163 | }); -------------------------------------------------------------------------------- /src/index-direction.test.ts: -------------------------------------------------------------------------------- 1 | import { ConsentControl } from "./index"; 2 | 3 | describe("Test language direction", () => { 4 | let testId: string = "app"; 5 | 6 | beforeEach(() => { 7 | let newDiv = document.createElement("div"); 8 | newDiv.setAttribute("id", testId); 9 | document.body.appendChild(newDiv); 10 | }); 11 | 12 | afterEach(() => { 13 | let child = document.getElementById(testId); 14 | if (child) { 15 | let parent = child.parentNode; 16 | 17 | if (parent) { 18 | parent.removeChild(child); 19 | } 20 | else { 21 | throw new Error("Parent not found error"); 22 | } 23 | } 24 | }); 25 | 26 | test("Language is ms (ltr). No cookieCategories, no textResources", () => { 27 | let callBack = function() { return; }; 28 | let cc = new ConsentControl(testId, "ms", callBack); 29 | expect(cc.getDirection()).toBe("ltr"); 30 | }); 31 | 32 | test("Language is ms (ltr). Set direction to rtl. No cookieCategories, no textResources", () => { 33 | let callBack = function() { return; }; 34 | let cc = new ConsentControl(testId, "ms", callBack); 35 | cc.setDirection("rtl"); 36 | expect(cc.getDirection()).toBe("rtl"); 37 | }); 38 | 39 | test("Language is ar (rtl). No cookieCategories, no textResources", () => { 40 | let callBack = function() { return; }; 41 | let cc = new ConsentControl(testId, "ar", callBack); 42 | expect(cc.getDirection()).toBe("rtl"); 43 | }); 44 | 45 | test("Language is he (rtl). No cookieCategories, no textResources", () => { 46 | let callBack = function() { return; }; 47 | let cc = new ConsentControl(testId, "he", callBack); 48 | expect(cc.getDirection()).toBe("rtl"); 49 | }); 50 | 51 | test("Language is ps (rtl). No cookieCategories, no textResources", () => { 52 | let callBack = function() { return; }; 53 | let cc = new ConsentControl(testId, "ps", callBack); 54 | expect(cc.getDirection()).toBe("rtl"); 55 | }); 56 | 57 | test("Language is ur (rtl). No cookieCategories, no textResources", () => { 58 | let callBack = function() { return; }; 59 | let cc = new ConsentControl(testId, "ur", callBack); 60 | expect(cc.getDirection()).toBe("rtl"); 61 | }); 62 | 63 | test("Language is fa (rtl). No cookieCategories, no textResources", () => { 64 | let callBack = function() { return; }; 65 | let cc = new ConsentControl(testId, "fa", callBack); 66 | expect(cc.getDirection()).toBe("rtl"); 67 | }); 68 | 69 | test("Language is pa (rtl). No cookieCategories, no textResources", () => { 70 | let callBack = function() { return; }; 71 | let cc = new ConsentControl(testId, "pa", callBack); 72 | expect(cc.getDirection()).toBe("rtl"); 73 | }); 74 | 75 | test("Language is sd (rtl). No cookieCategories, no textResources", () => { 76 | let callBack = function() { return; }; 77 | let cc = new ConsentControl(testId, "sd", callBack); 78 | expect(cc.getDirection()).toBe("rtl"); 79 | }); 80 | 81 | test("Language is tk (rtl). No cookieCategories, no textResources", () => { 82 | let callBack = function() { return; }; 83 | let cc = new ConsentControl(testId, "tk", callBack); 84 | expect(cc.getDirection()).toBe("rtl"); 85 | }); 86 | 87 | test("Language is ug (rtl). No cookieCategories, no textResources", () => { 88 | let callBack = function() { return; }; 89 | let cc = new ConsentControl(testId, "ug", callBack); 90 | expect(cc.getDirection()).toBe("rtl"); 91 | }); 92 | 93 | test("Language is yi (rtl). No cookieCategories, no textResources", () => { 94 | let callBack = function() { return; }; 95 | let cc = new ConsentControl(testId, "yi", callBack); 96 | expect(cc.getDirection()).toBe("rtl"); 97 | }); 98 | 99 | test("Language is syr (rtl). No cookieCategories, no textResources", () => { 100 | let callBack = function() { return; }; 101 | let cc = new ConsentControl(testId, "syr", callBack); 102 | expect(cc.getDirection()).toBe("rtl"); 103 | }); 104 | 105 | test("Language is ks-arab (rtl). No cookieCategories, no textResources", () => { 106 | let callBack = function() { return; }; 107 | let cc = new ConsentControl(testId, "ks-arab", callBack); 108 | expect(cc.getDirection()).toBe("rtl"); 109 | }); 110 | 111 | test("Language is en-US (ltr). No cookieCategories, no textResources", () => { 112 | let callBack = function() { return; }; 113 | let cc = new ConsentControl(testId, "en-US", callBack); 114 | expect(cc.getDirection()).toBe("ltr"); 115 | }); 116 | 117 | test("Language is ar-SA (rtl). No cookieCategories, no textResources", () => { 118 | let callBack = function() { return; }; 119 | let cc = new ConsentControl(testId, "ar-SA", callBack); 120 | expect(cc.getDirection()).toBe("rtl"); 121 | }); 122 | }); 123 | 124 | describe("Test html and body direction", () => { 125 | let testId: string = "app"; 126 | 127 | let htmlDir: string | null; 128 | let bodyDir: string | null; 129 | 130 | beforeAll(() => { 131 | htmlDir = document.getElementsByTagName("html")[0].getAttribute("dir"); 132 | bodyDir = document.getElementsByTagName("body")[0].getAttribute("dir"); 133 | }); 134 | 135 | beforeEach(() => { 136 | let newDiv = document.createElement("div"); 137 | newDiv.setAttribute("id", testId); 138 | document.body.appendChild(newDiv); 139 | }); 140 | 141 | afterEach(() => { 142 | if (htmlDir) { 143 | document.getElementsByTagName("html")[0].setAttribute("dir", htmlDir); 144 | } 145 | else { 146 | document.getElementsByTagName("html")[0].removeAttribute("dir"); 147 | } 148 | 149 | if (bodyDir) { 150 | document.getElementsByTagName("body")[0].setAttribute("dir", bodyDir); 151 | } 152 | else { 153 | document.getElementsByTagName("body")[0].removeAttribute("dir"); 154 | } 155 | 156 | let child = document.getElementById(testId); 157 | if (child) { 158 | let parent = child.parentNode; 159 | 160 | if (parent) { 161 | parent.removeChild(child); 162 | } 163 | else { 164 | throw new Error("Parent not found error"); 165 | } 166 | } 167 | }); 168 | 169 | test("Language is en (ltr). Html dir is rtl. No cookieCategories, no textResources", () => { 170 | document.getElementsByTagName("html")[0].setAttribute("dir", "rtl"); 171 | 172 | let callBack = function() { return; }; 173 | let cc = new ConsentControl(testId, "en", callBack); 174 | expect(cc.getDirection()).toBe("rtl"); 175 | }); 176 | 177 | test("Language is ar (rtl). Html dir is ltr. No cookieCategories, no textResources", () => { 178 | document.getElementsByTagName("html")[0].setAttribute("dir", "ltr"); 179 | 180 | let callBack = function() { return; }; 181 | let cc = new ConsentControl(testId, "ar", callBack); 182 | expect(cc.getDirection()).toBe("ltr"); 183 | }); 184 | 185 | test("Language is en (ltr). Body dir is rtl. No cookieCategories, no textResources", () => { 186 | document.getElementsByTagName("body")[0].setAttribute("dir", "rtl"); 187 | 188 | let callBack = function() { return; }; 189 | let cc = new ConsentControl(testId, "en", callBack); 190 | expect(cc.getDirection()).toBe("rtl"); 191 | }); 192 | 193 | test("Language is ar (rtl). Body dir is ltr. No cookieCategories, no textResources", () => { 194 | document.getElementsByTagName("body")[0].setAttribute("dir", "ltr"); 195 | 196 | let callBack = function() { return; }; 197 | let cc = new ConsentControl(testId, "ar", callBack); 198 | expect(cc.getDirection()).toBe("ltr"); 199 | }); 200 | }); -------------------------------------------------------------------------------- /src/index-button.test.ts: -------------------------------------------------------------------------------- 1 | import { ConsentControl } from "./index"; 2 | import { ICookieCategoriesPreferences } from "./interfaces/CookieCategoriesPreferences"; 3 | import * as styles from "./styles.scss"; 4 | 5 | describe("Test radio buttons and 'Reset all' button", () => { 6 | let testId: string = "app"; 7 | 8 | function testRadioBtnState(radioButtons: String[]): void { 9 | let cookieItemRadioBtn: HTMLInputElement[] = [].slice.call(document.getElementsByClassName(styles.cookieItemRadioBtn)); 10 | for (let i = 0; i < radioButtons.length; i++) { 11 | if (radioButtons[i] === "checked") { 12 | expect(cookieItemRadioBtn[i].checked).toBeTruthy(); 13 | } 14 | else { 15 | expect(cookieItemRadioBtn[i].checked).toBeFalsy(); 16 | } 17 | } 18 | } 19 | 20 | beforeEach(() => { 21 | let newDiv = document.createElement("div"); 22 | newDiv.setAttribute("id", testId); 23 | document.body.appendChild(newDiv); 24 | }); 25 | 26 | afterEach(() => { 27 | let child = document.getElementById(testId); 28 | if (child) { 29 | let parent = child.parentNode; 30 | 31 | if (parent) { 32 | parent.removeChild(child); 33 | } 34 | else { 35 | throw new Error("Parent not found error"); 36 | } 37 | } 38 | }); 39 | 40 | test("Call showPreferences(...) and then click radio buttons. All cookieCategoriePreferences will be reset to undefined when 'Reset all' is clicked", () => { 41 | let callBack = function() { return; }; 42 | let cc = new ConsentControl(testId, "en", callBack); 43 | 44 | let cookieCategoriePreferences = { "c1": true, "c2": false, "c3": undefined }; 45 | cc.showPreferences(cookieCategoriePreferences); 46 | 47 | expect(cc.preferencesCtrl).toBeTruthy(); 48 | 49 | let cookieItemRadioBtn: HTMLInputElement[] = [].slice.call(document.getElementsByClassName(styles.cookieItemRadioBtn)); 50 | cookieItemRadioBtn[1].click(); 51 | cookieItemRadioBtn[2].click(); 52 | cookieItemRadioBtn[4].click(); 53 | 54 | testRadioBtnState(["unchecked", "checked", "checked", "unchecked", "checked", "unchecked"]); 55 | 56 | if (cc.preferencesCtrl) { 57 | expect(cc.preferencesCtrl.cookieCategoriesPreferences).toEqual({ "c1": false, "c2": true, "c3": true }); 58 | } 59 | else { 60 | throw new Error("Preference dialog not found error"); 61 | } 62 | 63 | let resetAllBtn = document.getElementsByClassName(styles.modalButtonReset)[0]; 64 | resetAllBtn.click(); 65 | 66 | testRadioBtnState(["unchecked", "unchecked", "unchecked", "unchecked", "unchecked", "unchecked"]); 67 | 68 | if (cc.preferencesCtrl) { 69 | expect(cc.preferencesCtrl.cookieCategoriesPreferences).toEqual({ "c1": undefined, "c2": undefined, "c3": undefined }); 70 | } 71 | else { 72 | throw new Error("Preference dialog not found error"); 73 | } 74 | }); 75 | 76 | test("Call showPreferences(...) with unswitchable id and click radio buttons. All cookiePreferences will be reset to undefined when 'Reset all' is clicked", () => { 77 | let callBack = function() { return; }; 78 | let cc = new ConsentControl(testId, "en", callBack); 79 | 80 | let cookieCategoriePreferences = { "c0": true, "c1": true, "c2": false, "c3": undefined }; 81 | cc.showPreferences(cookieCategoriePreferences); 82 | 83 | expect(cc.preferencesCtrl).toBeTruthy(); 84 | 85 | let cookieItemRadioBtn: HTMLInputElement[] = [].slice.call(document.getElementsByClassName(styles.cookieItemRadioBtn)); 86 | cookieItemRadioBtn[1].click(); 87 | cookieItemRadioBtn[2].click(); 88 | cookieItemRadioBtn[4].click(); 89 | 90 | testRadioBtnState(["unchecked", "checked", "checked", "unchecked", "checked", "unchecked"]); 91 | 92 | if (cc.preferencesCtrl) { 93 | expect(cc.preferencesCtrl.cookieCategoriesPreferences).toEqual({ "c0": true, "c1": false, "c2": true, "c3": true }); 94 | } 95 | else { 96 | throw new Error("Preference dialog not found error"); 97 | } 98 | 99 | let resetAllBtn = document.getElementsByClassName(styles.modalButtonReset)[0]; 100 | resetAllBtn.click(); 101 | 102 | testRadioBtnState(["unchecked", "unchecked", "unchecked", "unchecked", "unchecked", "unchecked"]); 103 | 104 | if (cc.preferencesCtrl) { 105 | expect(cc.preferencesCtrl.cookieCategoriesPreferences).toEqual({ "c0": true, "c1": undefined, "c2": undefined, "c3": undefined }); 106 | } 107 | else { 108 | throw new Error("Preference dialog not found error"); 109 | } 110 | }); 111 | 112 | test("Call showPreferences(...) and then click radio buttons. cookieCategoriePreferences will be set", () => { 113 | let callBack = function() { return; }; 114 | let cc = new ConsentControl(testId, "en", callBack); 115 | 116 | let cookieCategoriePreferences = { "c1": true, "c2": false, "c3": undefined }; 117 | cc.showPreferences(cookieCategoriePreferences); 118 | 119 | expect(cc.preferencesCtrl).toBeTruthy(); 120 | 121 | let cookieItemRadioBtn: HTMLInputElement[] = [].slice.call(document.getElementsByClassName(styles.cookieItemRadioBtn)); 122 | cookieItemRadioBtn[0].click(); 123 | cookieItemRadioBtn[3].click(); 124 | cookieItemRadioBtn[5].click(); 125 | 126 | testRadioBtnState(["checked", "unchecked", "unchecked", "checked", "unchecked", "checked"]); 127 | 128 | if (cc.preferencesCtrl) { 129 | expect(cc.preferencesCtrl.cookieCategoriesPreferences).toEqual({ "c1": true, "c2": false, "c3": false }); 130 | } 131 | else { 132 | throw new Error("Preference dialog not found error"); 133 | } 134 | 135 | cookieItemRadioBtn[1].click(); 136 | cookieItemRadioBtn[2].click(); 137 | cookieItemRadioBtn[4].click(); 138 | 139 | testRadioBtnState(["unchecked", "checked", "checked", "unchecked", "checked", "unchecked"]); 140 | 141 | if (cc.preferencesCtrl) { 142 | expect(cc.preferencesCtrl.cookieCategoriesPreferences).toEqual({ "c1": false, "c2": true, "c3": true }); 143 | } 144 | else { 145 | throw new Error("Preference dialog not found error"); 146 | } 147 | }); 148 | }); 149 | 150 | describe("Test 'Save changes' button", () => { 151 | let testId: string = "app"; 152 | 153 | beforeEach(() => { 154 | let newDiv = document.createElement("div"); 155 | newDiv.setAttribute("id", testId); 156 | document.body.appendChild(newDiv); 157 | }); 158 | 159 | afterEach(() => { 160 | let child = document.getElementById(testId); 161 | if (child) { 162 | let parent = child.parentNode; 163 | 164 | if (parent) { 165 | parent.removeChild(child); 166 | } 167 | else { 168 | throw new Error("Parent not found error"); 169 | } 170 | } 171 | }); 172 | 173 | test("Call showPreferences(...), click any unchecked radio buttons, and close the dialog. Open dialog and 'Save changes' button will be enabled", () => { 174 | let callBack = function() { return; }; 175 | let cc = new ConsentControl(testId, "en", callBack); 176 | 177 | let cookieCategoriePreferences = { "c2": undefined, "c3": false }; 178 | cc.showPreferences(cookieCategoriePreferences); 179 | 180 | let cookieItemRadioBtn: HTMLInputElement[] = [].slice.call(document.getElementsByClassName(styles.cookieItemRadioBtn)); 181 | cookieItemRadioBtn[0].click(); 182 | cookieItemRadioBtn[3].click(); 183 | 184 | cc.hidePreferences(); 185 | 186 | cc.showPreferences(cookieCategoriePreferences); 187 | expect(cookieCategoriePreferences).toEqual({ "c1": true, "c2": false, "c3": false }); 188 | 189 | let saveChangesBtn = document.getElementsByClassName(styles.modalButtonSave)[0]; 190 | expect(saveChangesBtn.disabled).toBeFalsy(); 191 | }); 192 | }); 193 | 194 | describe("Test 'Accept all' button", () => { 195 | let testId: string = "app"; 196 | 197 | beforeEach(() => { 198 | let newDiv = document.createElement("div"); 199 | newDiv.setAttribute("id", testId); 200 | document.body.appendChild(newDiv); 201 | }); 202 | 203 | afterEach(() => { 204 | let child = document.getElementById(testId); 205 | if (child) { 206 | let parent = child.parentNode; 207 | 208 | if (parent) { 209 | parent.removeChild(child); 210 | } 211 | else { 212 | throw new Error("Parent not found error"); 213 | } 214 | } 215 | }); 216 | 217 | test("Click 'Accept all' button and all cookieCategoriePreferences will be set to 'true'", () => { 218 | let callBack = function() { return; }; 219 | let cc = new ConsentControl(testId, "en", callBack); 220 | 221 | let cookieCategoriePreferences: ICookieCategoriesPreferences = { "c2": undefined, "c3": false }; 222 | cc.showBanner(cookieCategoriePreferences); 223 | 224 | let acceptAllBtn = document.getElementsByClassName(styles.bannerButton)[0]; 225 | acceptAllBtn.click(); 226 | 227 | for (let cookieCategory of cc.cookieCategories) { 228 | if (!cookieCategory.isUnswitchable) { 229 | let id = cookieCategory.id; 230 | expect(cookieCategoriePreferences[id]).toBeTruthy(); 231 | } 232 | } 233 | }); 234 | 235 | test("Initialize cookieCategoriesPreferences with unswitchable id and click 'Accept all' button. All cookieCategoriePreferences will be set to 'true'", () => { 236 | let callBack = function() { return; }; 237 | let cc = new ConsentControl(testId, "en", callBack); 238 | 239 | let cookieCategoriePreferences: ICookieCategoriesPreferences = { "c0": true, "c2": undefined, "c3": false }; 240 | cc.showBanner(cookieCategoriePreferences); 241 | 242 | let acceptAllBtn = document.getElementsByClassName(styles.bannerButton)[0]; 243 | acceptAllBtn.click(); 244 | 245 | for (let cookieCategory of cc.cookieCategories) { 246 | if (!cookieCategory.isUnswitchable) { 247 | let id = cookieCategory.id; 248 | expect(cookieCategoriePreferences[id]).toBeTruthy(); 249 | } 250 | } 251 | }); 252 | }); 253 | 254 | describe("Test 'Reject all' button", () => { 255 | let testId: string = "app"; 256 | 257 | beforeEach(() => { 258 | let newDiv = document.createElement("div"); 259 | newDiv.setAttribute("id", testId); 260 | document.body.appendChild(newDiv); 261 | }); 262 | 263 | afterEach(() => { 264 | let child = document.getElementById(testId); 265 | if (child) { 266 | let parent = child.parentNode; 267 | 268 | if (parent) { 269 | parent.removeChild(child); 270 | } 271 | else { 272 | throw new Error("Parent not found error"); 273 | } 274 | } 275 | }); 276 | 277 | test("Click 'Reject all' button and all cookieCategoriePreferences will be set to 'false'", () => { 278 | let callBack = function() { return; }; 279 | let cc = new ConsentControl(testId, "en", callBack); 280 | 281 | let cookieCategoriePreferences: ICookieCategoriesPreferences = { "c2": undefined, "c3": false }; 282 | cc.showBanner(cookieCategoriePreferences); 283 | 284 | let rejectAllBtn = document.getElementsByClassName(styles.bannerButton)[1]; 285 | rejectAllBtn.click(); 286 | 287 | for (let cookieCategory of cc.cookieCategories) { 288 | if (!cookieCategory.isUnswitchable) { 289 | let id = cookieCategory.id; 290 | expect(cookieCategoriePreferences[id]).toBeFalsy(); 291 | } 292 | } 293 | }); 294 | 295 | test("Initialize cookieCategoriesPreferences with unswitchable id and click 'Reject all' button. All cookieCategoriePreferences will be set to 'false'", () => { 296 | let callBack = function() { return; }; 297 | let cc = new ConsentControl(testId, "en", callBack); 298 | 299 | let cookieCategoriePreferences: ICookieCategoriesPreferences = { "c0": true, "c2": undefined, "c3": false }; 300 | cc.showBanner(cookieCategoriePreferences); 301 | 302 | let rejectAllBtn = document.getElementsByClassName(styles.bannerButton)[1]; 303 | rejectAllBtn.click(); 304 | 305 | for (let cookieCategory of cc.cookieCategories) { 306 | if (!cookieCategory.isUnswitchable) { 307 | let id = cookieCategory.id; 308 | expect(cookieCategoriePreferences[id]).toBeFalsy(); 309 | } 310 | } 311 | }); 312 | }); -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "./themes/theme"; 2 | 3 | // Banner button minimum size, can be changed 4 | $bannerBtnWidth: 150px; 5 | $bannerBtnHeight: 36px; 6 | 7 | /// Theme. 8 | /// Default: Light 9 | $close-button-color: #666666; 10 | 11 | $banner-background-color: #F2F2F2; 12 | $dialog-background-color: #FFFFFF; 13 | 14 | $background-color-between-page-and-dialog: rgba(255, 255, 255, 0.6); 15 | $dialog-border-color: #0067B8; 16 | 17 | $radio-button-border-color: #000000; 18 | $radio-button-checked-background-color: #000000; 19 | $radio-button-hover-border-color: #0067B8; 20 | $radio-button-hover-background-color: rgba(0, 0, 0, 0.8); 21 | $radio-button-disabled-color: rgba(0, 0, 0, 0.2); 22 | $radio-button-disabled-border-color: rgba(0, 0, 0, 0.2); 23 | 24 | @mixin bannerFont($weight, $size, $style: normal) { 25 | font : { 26 | family: Segoe UI, SegoeUI, Arial, sans-serif; 27 | style: $style; 28 | weight: $weight; 29 | size: $size; 30 | } 31 | } 32 | 33 | // For right-to-left direction 34 | @mixin rtlDesign($margin, $padding, $float: none) { 35 | div[dir="rtl"] & { 36 | margin: $margin; 37 | padding: $padding; 38 | float: $float; 39 | } 40 | } 41 | 42 | // For right-to-left direction (position) 43 | @mixin rtlDesignPosition($left, $right) { 44 | div[dir="rtl"] & { 45 | left: $left; 46 | right: $right; 47 | } 48 | } 49 | 50 | .bannerBody { 51 | position: relative; 52 | display: flex; 53 | z-index: 9999; /* on top of the page */ 54 | width: 100%; 55 | background-color: $banner-background-color; 56 | 57 | justify-content: space-between; 58 | text-align: left; 59 | 60 | @at-root div[dir="rtl"]#{ & } { 61 | text-align: right; 62 | } 63 | } 64 | 65 | .bannerInform { 66 | margin: 0; 67 | padding : { 68 | left: 5%; 69 | top: 8px; 70 | bottom: 8px; 71 | } 72 | 73 | @include rtlDesign(0, 8px 5% 8px 0); 74 | } 75 | 76 | .bannerBody svg { 77 | fill: none; 78 | max-width: none; 79 | max-height: none; 80 | } 81 | 82 | .infoIcon { 83 | display: table-cell; 84 | padding: 12px; 85 | width: 24px; 86 | height: 24px; 87 | 88 | @include bannerFont(normal, 24px); 89 | 90 | /* identical to box height */ 91 | line-height: 0; 92 | } 93 | 94 | .bannerInformBody { 95 | display: table-cell; 96 | vertical-align: middle; 97 | padding: 0; 98 | 99 | @include bannerFont(normal, 13px); 100 | line-height: 16px; 101 | 102 | /* Add styles to hyperlinks in case websites close the default styles for hyperlinks */ 103 | & a { 104 | text-decoration: underline; 105 | } 106 | } 107 | 108 | .buttonGroup { 109 | display: inline-block; 110 | margin : { 111 | left: 5%; 112 | right: 5%; 113 | } 114 | 115 | min-width: 40%; 116 | min-width: calc((#{ $bannerBtnWidth } + 3 * 4px) * 2 + #{ $bannerBtnWidth }); 117 | min-width: fit-content; 118 | 119 | align-self: center; 120 | position: relative; 121 | } 122 | 123 | .bannerButton { 124 | margin: 4px; 125 | padding: 5px; 126 | 127 | min-width: $bannerBtnWidth; 128 | min-height: $bannerBtnHeight; 129 | vertical-align: top; 130 | 131 | cursor: pointer; 132 | @include bannerFont(normal, 15px); 133 | line-height: 20px; 134 | text-align: center; 135 | 136 | &:focus { 137 | box-sizing: border-box; 138 | } 139 | 140 | &:disabled { 141 | cursor: not-allowed; 142 | } 143 | } 144 | 145 | .cookieModal { 146 | display: block; 147 | position: fixed; 148 | z-index: 10000; /* on top of the page */ 149 | top: 0; 150 | left: 0; 151 | width: 100%; 152 | height: 100%; 153 | background-color: $background-color-between-page-and-dialog; 154 | overflow: auto; 155 | text-align: left; 156 | 157 | @at-root div[dir="rtl"]#{ & } { 158 | text-align: right; 159 | } 160 | @include rtlDesignPosition(auto, 0); 161 | } 162 | 163 | .modalContainer { 164 | position: relative; 165 | top: 8%; 166 | margin : { 167 | bottom: 40px; 168 | left: auto; 169 | right: auto; 170 | } 171 | 172 | box-sizing: border-box; 173 | width: 640px; 174 | 175 | background-color: $dialog-background-color; 176 | border: 1px solid $dialog-border-color; 177 | } 178 | 179 | .closeModalIcon { 180 | float: right; 181 | z-index: 1; 182 | margin: 2px; 183 | padding: 12px; 184 | border: none; 185 | 186 | cursor: pointer; 187 | @include bannerFont(normal, 13px); 188 | line-height: 13px; 189 | 190 | display: flex; 191 | align-items: center; 192 | text-align: center; 193 | color: $close-button-color; 194 | background-color: $dialog-background-color; 195 | 196 | @include rtlDesign(2px, 12px, left); 197 | } 198 | 199 | .modalBody { 200 | position: static; 201 | margin : { 202 | top: 36px; 203 | left: 36px; 204 | right: 36px; 205 | } 206 | } 207 | 208 | .modalTitle { 209 | margin : { 210 | top: 0; 211 | bottom: 12px; 212 | } 213 | 214 | @include bannerFont(600, 20px); 215 | line-height: 24px; 216 | text-transform: none; 217 | } 218 | 219 | .modalContent { 220 | height: 446px; 221 | overflow: auto; 222 | } 223 | 224 | .cookieStatement { 225 | margin : { 226 | top: 0; 227 | } 228 | 229 | @include bannerFont(normal, 15px); 230 | line-height: 20px; 231 | 232 | /* Add styles to hyperlinks in case websites close the default styles for hyperlinks */ 233 | & a { 234 | text-decoration: underline; 235 | } 236 | } 237 | 238 | dl.cookieOrderedList { 239 | margin : { 240 | top: 36px; 241 | bottom: 0; 242 | } 243 | padding: 0; 244 | 245 | /* Close the default styles which adds decimal numbers in front of list items */ 246 | list-style: none; 247 | text-transform: none; 248 | } 249 | 250 | dt.cookieListItem { 251 | margin : { 252 | top: 20px; 253 | } 254 | float: none; 255 | 256 | @include bannerFont(600, 18px); 257 | line-height: 24px; 258 | 259 | /* Close the default styles which adds decimal numbers in front of list items */ 260 | list-style: none; 261 | } 262 | 263 | .cookieListItemGroup { 264 | margin: 0; 265 | padding: 0; 266 | border: none; 267 | } 268 | 269 | .cookieListItemTitle { 270 | margin: 0; 271 | padding: 0; 272 | border-bottom: none; 273 | 274 | @include bannerFont(600, 18px); 275 | line-height: 24px; 276 | text-transform: none; 277 | } 278 | 279 | .cookieListItemDescription { 280 | display: inline-block; 281 | margin : { 282 | top: 0; 283 | bottom: 13px; 284 | } 285 | 286 | @include bannerFont(normal, 15px); 287 | line-height: 20px; 288 | } 289 | 290 | .cookieItemRadioBtnGroup { 291 | display: block; 292 | } 293 | 294 | .cookieItemRadioBtnCtrl { 295 | display: inline-block; 296 | position: relative; 297 | left: 5px; 298 | margin : { 299 | bottom: 13px; 300 | right: 34px; 301 | } 302 | padding: 3px; 303 | 304 | @include rtlDesign(0 0 13px 34px, 3px); 305 | @include rtlDesignPosition(auto, 5px); 306 | } 307 | 308 | .bannerBody *::before, .cookieModal *::before, .bannerBody *::after, .cookieModal *::after { 309 | box-sizing: inherit; 310 | } 311 | 312 | .cookieItemRadioBtnCtrlOutline { 313 | outline: 2px solid $radio-button-hover-background-color; 314 | } 315 | 316 | @mixin defineRaioInput { 317 | & + label::before { 318 | display: block; 319 | position: absolute; 320 | top: 5px; 321 | left: 3px; 322 | height: 19px; 323 | width: 19px; 324 | content: ""; 325 | border-radius: 50%; 326 | border: 1px solid $radio-button-border-color; 327 | background-color: $dialog-background-color; 328 | 329 | @include rtlDesignPosition(auto, 3px); 330 | } 331 | 332 | &:not(:disabled) + label { 333 | &:hover { 334 | &::before { 335 | border: 1px solid $radio-button-hover-border-color; 336 | } 337 | 338 | &::after { 339 | display: block; 340 | position: absolute; 341 | top: 10px; 342 | left: 8px; 343 | height: 9px; 344 | width: 9px; 345 | content: ""; 346 | border-radius: 50%; 347 | background-color: $radio-button-hover-background-color; 348 | 349 | @include rtlDesignPosition(auto, 8px); 350 | } 351 | } 352 | 353 | &:focus { 354 | &::before { 355 | border: 1px solid $radio-button-hover-border-color; 356 | } 357 | 358 | &::after { 359 | display: block; 360 | position: absolute; 361 | top: 10px; 362 | left: 8px; 363 | height: 9px; 364 | width: 9px; 365 | content: ""; 366 | border-radius: 50%; 367 | background-color: $radio-button-checked-background-color; 368 | 369 | @include rtlDesignPosition(auto, 8px); 370 | } 371 | } 372 | } 373 | 374 | &:checked + label::after { 375 | display: block; 376 | position: absolute; 377 | top: 10px; 378 | left: 8px; 379 | height: 9px; 380 | width: 9px; 381 | content: ""; 382 | border-radius: 50%; 383 | background-color: $radio-button-checked-background-color; 384 | 385 | @include rtlDesignPosition(auto, 8px); 386 | } 387 | 388 | &:disabled + label { 389 | cursor: not-allowed; 390 | 391 | &::before { 392 | border: 1px solid $radio-button-disabled-border-color; 393 | background-color: $radio-button-disabled-color; 394 | } 395 | } 396 | } 397 | 398 | input[type="radio"].cookieItemRadioBtn { 399 | display: inline-block; 400 | position: relative; /* Adjust the position */ 401 | margin : { 402 | top: 0; 403 | left: 0; 404 | right: 0; 405 | } 406 | 407 | height: 0; 408 | width: 0; 409 | border-radius: 0; 410 | 411 | cursor: pointer; 412 | outline: none; 413 | box-sizing: border-box; 414 | 415 | appearance: none; 416 | 417 | /* Define our own radio input in case websites close the default styles for input type radio */ 418 | @include defineRaioInput; 419 | } 420 | 421 | .cookieItemRadioBtnLabel { 422 | display: block; 423 | position: static; 424 | float: right; 425 | margin : { 426 | top: 0; 427 | bottom: 0; 428 | left: 19px; 429 | right: 0; 430 | } 431 | padding : { 432 | top: 0; 433 | bottom: 0; 434 | left: 8px; 435 | right: 0; 436 | } 437 | width: 80%; /* If "calc()" is not supported */ 438 | 439 | width: calc(100% - 19px); 440 | 441 | @include bannerFont(normal, 15px); 442 | line-height: 20px; 443 | /* identical to box height, or 133% */ 444 | 445 | text-transform: none; 446 | cursor: pointer; 447 | box-sizing: border-box; 448 | 449 | @include rtlDesign(0 19px 0 0, 0 8px 0 0, left); 450 | } 451 | 452 | .modalButtonGroup { 453 | margin : { 454 | top: 20px; 455 | bottom: 48px; 456 | } 457 | } 458 | 459 | .modalButtonReset { 460 | padding: 0; 461 | 462 | width: 278px; 463 | height: 36px; 464 | 465 | cursor: pointer; 466 | @include bannerFont(normal, 15px); 467 | 468 | line-height: 20px; 469 | /* identical to box height, or 133% */ 470 | text-align: center; 471 | 472 | &:focus { 473 | box-sizing: border-box; 474 | } 475 | 476 | &:disabled { 477 | cursor: not-allowed; 478 | } 479 | } 480 | 481 | .modalButtonSave { 482 | float: right; 483 | padding: 0; 484 | 485 | width: 278px; 486 | height: 36px; 487 | 488 | cursor: pointer; 489 | @include bannerFont(normal, 15px); 490 | 491 | line-height: 20px; 492 | /* identical to box height, or 133% */ 493 | text-align: center; 494 | 495 | &:focus { 496 | box-sizing: border-box; 497 | } 498 | 499 | &:disabled { 500 | cursor: not-allowed; 501 | } 502 | 503 | @include rtlDesign(0, 0, left); 504 | } 505 | 506 | @media only screen and (max-width: 768px) { 507 | /* For mobile phones: */ 508 | 509 | .buttonGroup, .bannerInform { 510 | padding : { 511 | top: 8px; 512 | bottom: 12px; 513 | left: 3.75%; 514 | right: 3.75%; 515 | } 516 | margin: 0; 517 | 518 | width: 92.5%; 519 | } 520 | 521 | .bannerBody { 522 | display:block; 523 | } 524 | 525 | .bannerButton { 526 | margin : { 527 | bottom: 8px; 528 | left: 0; 529 | right: 0; 530 | } 531 | 532 | width: 100%; 533 | } 534 | 535 | .cookieModal { 536 | overflow: hidden; 537 | } 538 | 539 | .modalContainer { 540 | top: 1.8%; 541 | 542 | width: 93.33%; 543 | height: 96.4%; 544 | 545 | overflow: hidden; 546 | } 547 | 548 | .modalBody { 549 | margin : { 550 | top: 24px; 551 | left: 24px; 552 | right: 24px; 553 | } 554 | 555 | height: 100%; 556 | } 557 | 558 | .modalContent { 559 | height: 62%; /* If "calc()" is not supported */ 560 | 561 | height: calc(100% - 188px); 562 | min-height: 50%; 563 | } 564 | 565 | .modalButtonReset { 566 | width: 100%; 567 | } 568 | 569 | .modalButtonSave { 570 | margin : { 571 | bottom: 12px; 572 | left: 0; 573 | } 574 | 575 | width: 100%; 576 | 577 | @include rtlDesign(0 0 12px 0, 0); 578 | } 579 | } 580 | 581 | /* For landscape orientation: */ 582 | @media only screen and (max-width: 768px) and (orientation: landscape), only screen and (max-height: 260px), only screen and (max-width: 340px) { 583 | .modalContainer { 584 | overflow: auto; 585 | } 586 | } 587 | 588 | /* For large zoom in: */ 589 | @media only screen and (max-height: 260px), only screen and (max-width: 340px) { 590 | 591 | .bannerButton { 592 | min-width: 0; 593 | } 594 | 595 | .closeModalIcon { 596 | padding: 3%; 597 | } 598 | 599 | .modalBody { 600 | margin : { 601 | top: 3%; 602 | left: 3%; 603 | right: 3%; 604 | } 605 | } 606 | 607 | .modalTitle { 608 | margin: { 609 | bottom: 3%; 610 | } 611 | } 612 | 613 | .modalContent { 614 | height: calc(79% - 64px); 615 | } 616 | 617 | .modalButtonGroup { 618 | margin: { 619 | top: 5%; 620 | bottom: 10%; 621 | } 622 | } 623 | 624 | .modalButtonSave { 625 | margin : { 626 | bottom: 3%; 627 | } 628 | 629 | @include rtlDesign(0 0 3% 0, 0); 630 | } 631 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Consent Banner 2 | 3 | ## Overview 4 | 5 | Consent banner is the library which will generate a banner at the specified position for asking the cookie preferences. 6 | 7 | It contains three buttons, `accept all`, `reject all` and `more info`. If the user clicks `more info` button, A dialog will pop up so that the user can set the cookie categories that he/she wants to share with us. 8 | 9 | ## Building and running on localhost 10 | 11 | First install dependencies: 12 | 13 | ```sh 14 | npm i 15 | ``` 16 | 17 | To create a production build: 18 | 19 | ```sh 20 | npm run build-prod 21 | ``` 22 | 23 | To create a development build: 24 | 25 | ```sh 26 | npm run build 27 | ``` 28 | 29 | ## Running 30 | 31 | ```sh 32 | npm run start 33 | ``` 34 | 35 | ## Testing 36 | 37 | ```sh 38 | npm run test 39 | ``` 40 | 41 | ## Example use 42 | 43 | This is part of your `html` page. 44 | 45 | ```HTML 46 | 47 | ... 48 |
49 | ... 50 | 51 | ``` 52 | 53 | If you want to insert a banner into the `
`, you can use the following example. 54 | 55 | ```TypeScript 56 | 57 | let cookieCategories: ICookieCategory[] = 58 | [ 59 | { 60 | id: "c0", 61 | name: "1. Essential cookies", 62 | descHtml: "We use this cookie, read more here.", 63 | isUnswitchable: true 64 | }, 65 | { 66 | id: "c1", 67 | name: "2. Performance & analytics", 68 | descHtml: "We use this cookie, read more here." 69 | }, 70 | { 71 | id: "c2", 72 | name: "3. Advertising/Marketing", 73 | descHtml: "Blah" 74 | }, 75 | { 76 | id: "c3", 77 | name: "4. Targeting/personalization", 78 | descHtml: "Blah" 79 | } 80 | ]; 81 | 82 | let textResources: ITextResources = { 83 | bannerMessageHtml: "We use optional cookies to provide... read here.", 84 | acceptAllLabel: "Accept all", 85 | rejectAllLabel: "Reject all", 86 | moreInfoLabel: "More info", 87 | preferencesDialogCloseLabel: "Close", 88 | preferencesDialogTitle: "Manage cookie preferences", 89 | preferencesDialogDescHtml: "Most Microsoft sites...", 90 | acceptLabel: "Accept", 91 | rejectLabel: "Reject", 92 | saveLabel: "Save changes", 93 | resetLabel: "Reset all" 94 | }; 95 | 96 | let callBack = function(obj: any) { console.log(obj); }; 97 | 98 | // If you want to set 'nonce: test1' in style tag, add "stylesNonce: 'test1'" 99 | let options: IOptions = { 100 | textResources: textResources, 101 | initialTheme: "dark", 102 | stylesNonce: "test1" 103 | }; 104 | 105 | let cc = new ConsentControl("app", "en", callBack, cookieCategories, options); 106 | let cookieCategoriePreferences: ICookieCategoriesPreferences = { 107 | "c1": undefined, 108 | "c2": false, 109 | "c3": true 110 | }; 111 | 112 | // Show a banner with dark theme 113 | cc.showBanner(cookieCategoriePreferences); 114 | 115 | let theme: ITheme = { 116 | "close-button-color": "#000080", 117 | "secondary-button-disabled-opacity": "1", 118 | "secondary-button-hover-shadow": "none", 119 | "primary-button-disabled-opacity": "0.65", 120 | "primary-button-hover-border": "none", 121 | "primary-button-disabled-border": "1px double #3CB371", 122 | "primary-button-hover-shadow": "2px 5px 7px #A12A29", 123 | "banner-background-color": "#BA5583", 124 | "dialog-background-color": "#32CD32", 125 | "primary-button-color": "#4B30AE", 126 | "text-color": "#800000", 127 | "secondary-button-color": "#00FA9A", 128 | "secondary-button-disabled-color": "#7B68EE", 129 | "secondary-button-border": "1px dashed #969696", 130 | } 131 | 132 | // Create a new theme, named "medium" 133 | cc.createTheme("medium", theme); 134 | 135 | // Apply the "medium" theme 136 | cc.applyTheme("medium"); 137 | ``` 138 | 139 | ## Developer Guide 140 | 141 | `ConsentControl` consists of 2 main elements: **Banner** and **Preferences Dialog**. Use `containerElementOrId`, `culture`, `onPreferencesChanged`, `cookieCategories`, `options` to create an instance. `culture` will be used to determine the direction of the banner and preferences dialog. 142 | 143 | ```JavaScript 144 | var cc = new ConsentControl( 145 | // here the banner will be inserted, can be HTMLElement or element id 146 | containerElementOrId: string | HTMLElement, 147 | 148 | // culture can be just language "en" or language-country "en-us". 149 | // Based on language RTL should be applied (https://www.w3.org/International/questions/qa-scripts.en) 150 | culture: string, 151 | 152 | // callback function, called on preferences changes (via "Accept All", "Reject All" or "Save changes"), 153 | // must pass cookieCategoriePreferences, see ICookieCategoriesPreferences in Data types 154 | onPreferencesChanged: (cookieCategoriesPreferences: ICookieCategoriesPreferences) => void, 155 | 156 | // optional, see ICookieCategory in Data types 157 | cookieCategories?: ICookieCategory[], 158 | 159 | // optional, see IOptions in Data types 160 | options?: IOptions 161 | ); 162 | ``` 163 | 164 | + `setTextResources(textResources: ITextResources)` can be used to set the texts. 165 | 166 | + `createTheme(name: string, theme: ITheme)` can be used to create the new theme. `name` is the name of this new theme, which can be used by `applyTheme(themeName: string)`. `theme` is the new theme object. 167 | 168 | `ConsentControl` consists of three built in themes, `light`, `dark`, and `high-contrast`. `dark` and `high-contrast` themes are from [Microsoft Docs](https://docs.microsoft.com/en-us/). 169 | 170 | + You can apply the theme manually by using `applyTheme(themeName: string)`. `themeName` is the name of the theme which you want to apply. 171 | 172 | If you want to apply a new theme, you need to call `createTheme(name: string, theme: ITheme)` first, and then call `applyTheme(themeName: string)` 173 | 174 | ```JavaScript 175 | let theme: ITheme = { 176 | "close-button-color": "#000080", 177 | "secondary-button-disabled-opacity": "1", 178 | "secondary-button-hover-shadow": "none", 179 | "primary-button-disabled-opacity": "0.65", 180 | "primary-button-hover-border": "none", 181 | "primary-button-disabled-border": "1px double #3CB371", 182 | "primary-button-hover-shadow": "2px 5px 7px #A12A29", 183 | "banner-background-color": "#BA5583", 184 | "dialog-background-color": "#32CD32", 185 | "primary-button-color": "#4B30AE", 186 | "text-color": "#800000", 187 | "secondary-button-color": "#00FA9A", 188 | "secondary-button-disabled-color": "#7B68EE", 189 | "secondary-button-border": "1px dashed #969696", 190 | } 191 | 192 | // Create a new theme, named "medium" 193 | cc.createTheme("medium", theme); 194 | 195 | // Apply the "medium" theme 196 | cc.applyTheme("medium"); 197 | ``` 198 | 199 | If the name of theme is not in the themes collections, it will throw an error. `new Error("Theme not found error")` 200 | 201 | + `setContainerElement(containerElementOrId: string | HTMLElement)` can be used to set the container element for the banner and preferences dialog. 202 | 203 | If you pass `HTMLElement`, it will be used as the container. If you pass `string`, the method will use it as the `id` to find the container element. 204 | 205 | ```JavaScript 206 | cc.setContainerElement("app"); 207 | ``` 208 | 209 | or 210 | 211 | ```JavaScript 212 | let container = document.getElementById('app'); 213 | cc.setContainerElement(container); 214 | ``` 215 | 216 | If the container can not be found, it will throw an error. `new Error("Container not found error")` 217 | 218 | + `getContainerElement()` will return the current container element. 219 | 220 | + You can set the direction manually by using `setDirection(dir?: string)`. `dir` can be `"ltr"` or `"rtl"`. If `dir` is not passed, it will determine the direction by `dir` attribute in `html`, `body` or `culture`. 221 | 222 | ```JavaScript 223 | // There are 3 cases. dir="rtl", dir="ltr", empty 224 | 225 | // Set direction to rtl (right-to-left) 226 | cc.setDirection("rtl"); 227 | 228 | // Set direction to ltr (left-to-right) 229 | cc.setDirection("ltr"); 230 | 231 | // It will use "dir" attribute in "html" or "body" 232 | // If the "dir" attribute is not specified, it will apply the direction based on "culture" 233 | cc.setDirection(); 234 | ``` 235 | 236 | + `getDirection()` will return the current direction. 237 | 238 | ### Data types 239 | 240 | There are six data types: `ICookieCategory`, `ITextResources`, `ICookieCategoriesPreferences`, `IOptions`, `IThemes`, and `ITheme`. 241 | 242 | + `ICookieCategory` is used to create cookie categories that will be showed in the preferences dialog. 243 | 244 | + `ITextResources` is the texts that will be used in the banner and preferences dialog. 245 | 246 | + `ICookieCategoriesPreferences` is used to store the preferences in each cookie categories. 247 | 248 | + `IOptions` is the options for the banner. It contains four parts, `textResources`, `themes`, `initialTheme`, and `stylesNonce`. 249 | 250 | + `textResources` is the initial text resources for texts. 251 | 252 | + `themes` is a collections of themes that can be applied to the banner and preferences dialog. 253 | 254 | + `initialTheme` is the initial theme that you want to applied before you call `applyTheme(themeName: string)`. 255 | 256 | + `stylesNonce` is the `nonce` attribute for `