Switched to tsdx and Typescript

Added storybook with examples from readme
This commit is contained in:
Rick van Lieshout 2022-07-31 12:04:58 +02:00
parent c6799ea446
commit c544f19c41
40 changed files with 40964 additions and 5102 deletions

View File

@ -1,8 +0,0 @@
{
"presets": ["@babel/preset-env"],
"plugins": [
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-transform-react-jsx",
"@babel/plugin-transform-object-assign"
]
}

68
.eslintrc Normal file
View File

@ -0,0 +1,68 @@
{
"env": {
"browser": true,
"jest": true,
"es6": true
},
"plugins": ["import"],
"extends": [
"react-app",
"prettier/@typescript-eslint",
"plugin:prettier/recommended",
"eslint:recommended",
"prettier"
],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"rules": {
"react/react-in-jsx-scope": "off",
"no-console": [
"error",
{
"allow": ["debug", "error"]
}
],
"no-eval": "error",
"import/first": "error",
"camelcase": [
"error",
{
"ignoreImports": true,
"ignoreDestructuring": true
}
],
"consistent-return": "warn",
"constructor-super": "error",
"curly": "error",
"eol-last": "warn",
"eqeqeq": ["error", "smart"],
"import/order": 1,
"new-parens": "error",
"no-debugger": "error",
"no-fallthrough": "off",
"max-len": [
"warn",
{
"code": 120
}
],
"no-shadow": [
"error",
{
"hoist": "all"
}
],
"no-trailing-spaces": "warn",
"no-underscore-dangle": "error",
"no-unsafe-finally": "error",
"no-var": "error",
"object-shorthand": "error",
"one-var": ["error", "never"],
"prefer-arrow/prefer-arrow-functions": "off",
"prefer-const": "error",
"radix": "off",
"space-in-parens": ["off", "never"]
}
}

32
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: CI
on: [push]
jobs:
build:
name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
node: ['10.x', '12.x', '14.x']
os: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Use Node ${{ matrix.node }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- name: Install deps and build (with cache)
uses: bahmutov/npm-install@v1
- name: Lint
run: yarn lint
- name: Test
run: yarn test --ci --coverage --maxWorkers=2
- name: Build
run: yarn build

12
.github/workflows/size.yml vendored Normal file
View File

@ -0,0 +1,12 @@
name: size
on: [pull_request]
jobs:
size:
runs-on: ubuntu-latest
env:
CI_JOB_NUMBER: 1
steps:
- uses: actions/checkout@v1
- uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

7
.gitignore vendored
View File

@ -1,3 +1,6 @@
*.log
.DS_Store
.cache
dist
node_modules
build/index.js.LICENSE.txt
example/*
storybook-static

6
.husky/pre-commit Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm test
npm run lint
npm run build

8
.storybook/main.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
stories: ['../stories/**/*.stories.@(ts|tsx|js|jsx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
// https://storybook.js.org/docs/react/configure/typescript#mainjs-configuration
typescript: {
check: true, // type-check stories during Storybook build
}
};

5
.storybook/preview.js Normal file
View File

@ -0,0 +1,5 @@
// https://storybook.js.org/docs/react/writing-stories/parameters#global-parameters
export const parameters = {
// https://storybook.js.org/docs/react/essentials/actions#automatically-matching-args
actions: { argTypesRegex: '^on.*' },
};

View File

@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [[8.0.0]((https://github.com/Mastermindzh/react-cookie-consent/releases/tag/8.0.0)]
- Switched to tsdx and Typescript
- Added storybook with examples from readme
## [[7.6.0]](https://github.com/Mastermindzh/react-cookie-consent/releases/tag/7.6.0]
- Added `customButtonWrapperAttributes` prop which allows to add custom attributes to the button wrapper div

78
build/index.d.ts vendored
View File

@ -1,78 +0,0 @@
import * as React from "react";
import Cookies from "js-cookie";
export interface CookieConsentProps {
location?: "top" | "bottom" | "none";
sameSite?: "strict" | "lax" | "none";
visible?: "hidden" | "show" | "byCookieValue";
cookieSecurity?: boolean;
style?: object;
buttonStyle?: object;
declineButtonStyle?: object;
contentStyle?: object;
children?: React.ReactNode;
disableStyles?: boolean;
hideOnAccept?: boolean;
hideOnDecline?: boolean;
onAccept?: (acceptedByScrolling?: boolean) => void;
onDecline?: Function;
buttonText?: Function | React.ReactNode;
declineButtonText?: Function | React.ReactNode;
cookieName?: string;
cookieValue?: string | boolean | number;
declineCookieValue?: string | boolean | number;
setDeclineCookie?: boolean;
debug?: boolean;
expires?: number;
containerClasses?: string;
contentClasses?: string;
buttonWrapperClasses?: string;
buttonClasses?: string;
declineButtonClasses?: string;
buttonId?: string;
declineButtonId?: string;
extraCookieOptions?: object;
disableButtonStyles?: boolean;
enableDeclineButton?: boolean;
flipButtons?: boolean;
ButtonComponent?: React.ElementType;
overlay?: boolean;
overlayClasses?: string;
overlayStyle?: object;
onOverlayClick?: () => void;
acceptOnOverlayClick?: boolean;
ariaAcceptLabel?: string;
ariaDeclineLabel?: string;
acceptOnScroll?: boolean;
acceptOnScrollPercentage?: number;
customContentAttributes?: object;
customContainerAttributes?: object;
customButtonProps?: object;
customDeclineButtonProps?: object;
customButtonWrapperAttributes?: React.HTMLAttributes<HTMLDivElement>;
}
export default class CookieConsent extends React.Component<CookieConsentProps, {}> {}
/**
* Returns the value of the consent cookie
* Retrieves the regular value first and if not found the legacy one according
* to: https://web.dev/samesite-cookie-recipes/#handling-incompatible-clients
* @param {*} name optional name of the cookie
*/
export function getCookieConsentValue(name?: string): string;
/**
* Reset the consent cookie
* Remove the cookie on browser in order to allow user to change their consent
* @param {*} name optional name of the cookie
*/
export function resetCookieConsentValue(name?: string): void;
/**
* Get the legacy cookie name by the regular cookie name
* @param {string} name of cookie to get
*/
export function getLegacyCookieName(name: string): string;
export { Cookies };

File diff suppressed because one or more lines are too long

@ -1 +0,0 @@
Subproject commit 8f1152c04d7bc70ba098783db2b752379ed7ec0f

44705
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,74 +1,98 @@
{
"name": "react-cookie-consent",
"author": {
"name": "Rick van Lieshout",
"email": "info@rickvanlieshout.com"
},
"version": "7.6.0",
"description": "A small, simple and customizable cookie consent bar for use in React applications.",
"main": "build/index.js",
"types": "build/index.d.ts",
"dependencies": {
"js-cookie": "^2.2.1",
"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0"
},
"scripts": {
"build": "webpack",
"prettier": "prettier 'src/**/*.{js*,ts*,htm*,md,scss}' --write",
"patch": "npm --no-git-tag-version version patch",
"minor": "npm --no-git-tag-version version minor",
"major": "npm --no-git-tag-version version major",
"preversion": "grep \"\\[$npm_package_version\\]\" CHANGELOG.md > /dev/null || ( echo 'You need to add an entry in CHANGELOG.md for this version.' && false )",
"release": "npm run build && git add -A && git tag $npm_package_version && git commit -m \"release $npm_package_version\" && git push && git push --tags && npm publish",
"release-patch": "npm run patch && npm run release",
"release-minor": "npm run minor && npm run release",
"release-major": "npm run major && npm run release"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Mastermindzh/react-cookie-consent.git"
},
"keywords": [
"react",
"cookie",
"consent",
"cookiebar"
],
"license": "MIT",
"homepage": "https://github.com/Mastermindzh/react-cookie-consent#readme",
"bugs": {
"url": "https://github.com/Mastermindzh/react-cookie-consent/issues"
},
"homepage": "https://github.com/Mastermindzh/react-cookie-consent#readme",
"devDependencies": {
"@babel/cli": "^7.17.6",
"@babel/core": "^7.17.8",
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
"@babel/plugin-transform-object-assign": "^7.16.7",
"@babel/plugin-transform-react-jsx": "^7.17.3",
"@babel/preset-env": "^7.16.11",
"@mastermindzh/prettier-config": "^1.0.0",
"@types/js-cookie": "^2.2.6",
"babel-loader": "^8.2.4",
"copy-webpack-plugin": "^6.2.1",
"husky": "^4.3.0",
"lint-staged": "^10.5.1",
"prettier": "^2.6.2",
"react": "^18.0.0",
"webpack": "^5.71.0",
"webpack-cli": "^4.9.2"
"repository": {
"type": "git",
"url": "git+https://github.com/Mastermindzh/react-cookie-consent.git"
},
"prettier": "@mastermindzh/prettier-config",
"husky": {
"hooks": {
"pre-commit": "lint-staged --allow-empty"
}
"license": "MIT",
"author": {
"name": "Rick van Lieshout",
"email": "info@rickvanlieshout.com"
},
"main": "dist/index.js",
"module": "dist/react-cookie-consent.esm.js",
"typings": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"scripts": {
"analyze": "size-limit --why",
"build": "tsdx build",
"build-storybook": "build-storybook",
"lint": "tsdx lint",
"major": "npm --no-git-tag-version version major",
"minor": "npm --no-git-tag-version version minor",
"organize": "npx format-package -w && npx sort-package-json",
"patch": "npm --no-git-tag-version version patch",
"prepare": "tsdx build",
"prettier": "prettier 'src/**/*.{js*,ts*,htm*,md,scss}' --write",
"postinstall": "husky install",
"publish": "npx np",
"size": "size-limit",
"start": "tsdx watch",
"storybook": "start-storybook -p 6006",
"test": "tsdx test --passWithNoTests",
"preversion": "grep \"\\[$npm_package_version\\]\" CHANGELOG.md > /dev/null || ( echo 'You need to add an entry in CHANGELOG.md for this version.' && false )"
},
"lint-staged": {
"*.{js*,ts*,htm*,md,scss}": [
"prettier --write"
]
},
"prettier": "@mastermindzh/prettier-config",
"dependencies": {
"js-cookie": "^2.2.1"
},
"devDependencies": {
"@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3",
"@mastermindzh/prettier-config": "^1.0.0",
"@mui/material": "^5.9.2",
"@size-limit/preset-small-lib": "^7.0.8",
"@storybook/addon-essentials": "^6.5.9",
"@storybook/addons": "^6.5.9",
"@storybook/react": "^6.5.9",
"@types/js-cookie": "^2.2.6",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"babel-loader": "^8.2.5",
"husky": "^8.0.1",
"prettier": "^2.6.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-is": "^18.2.0",
"size-limit": "^7.0.8",
"tsdx": "^0.14.1",
"tslib": "^2.4.0",
"typescript": "^3.9.10"
},
"peerDependencies": {
"react": ">=16"
},
"engines": {
"node": ">=10"
},
"size-limit": [
{
"path": "dist/react-cookie-consent.cjs.production.min.js",
"limit": "10 KB"
},
{
"path": "dist/react-cookie-consent.esm.js",
"limit": "10 KB"
}
]
}

115
src/CookieConsent.props.tsx Normal file
View File

@ -0,0 +1,115 @@
import React, { FunctionComponent, ReactNode } from "react";
import {
defaultCookieConsentName,
POSITION_OPTIONS,
SAME_SITE_OPTIONS,
VISIBILITY_OPTIONS,
} from "./models/constants";
export interface CookieConsentProps {
children?: ReactNode;
style: React.CSSProperties;
buttonStyle: React.CSSProperties;
declineButtonStyle: React.CSSProperties;
contentStyle: React.CSSProperties;
disableStyles: boolean;
hideOnAccept: boolean;
hideOnDecline: boolean;
onAccept: (acceptedByScrolling: boolean) => void;
onDecline: () => void;
buttonText: string | ReactNode | Function;
declineButtonText: string | ReactNode | Function;
cookieName: string;
cookieValue: string | object;
declineCookieValue: string | object;
setDeclineCookie: boolean;
debug: boolean;
expires: number;
containerClasses: string;
contentClasses: string;
buttonClasses: string;
buttonWrapperClasses: string;
declineButtonClasses: string;
buttonId: string;
declineButtonId: string;
overlayClasses: string;
ariaAcceptLabel: string;
ariaDeclineLabel: string;
disableButtonStyles: boolean;
enableDeclineButton: boolean;
flipButtons: boolean;
cookieSecurity?: boolean;
overlay: boolean;
acceptOnOverlayClick: boolean;
acceptOnScroll: boolean;
acceptOnScrollPercentage: number;
ButtonComponent: any;
extraCookieOptions: Object;
overlayStyle: Object;
customContentAttributes: Object;
customContainerAttributes: Object;
customButtonProps: Object;
customDeclineButtonProps: Object;
customButtonWrapperAttributes: Object;
onOverlayClick: () => void;
// these should be enums
location: string;
visible: string;
sameSite: "strict" | "Strict" | "lax" | "Lax" | "none" | "None" | undefined;
}
const DefaultButtonComponent: FunctionComponent<{ children: ReactNode; [x: string]: any }> = ({
children,
...props
}) => {
return <button {...props}>{children}</button>;
};
export const defaultCookieConsentProps: CookieConsentProps = {
disableStyles: false,
hideOnAccept: true,
hideOnDecline: true,
location: POSITION_OPTIONS.BOTTOM,
visible: VISIBILITY_OPTIONS.BY_COOKIE_VALUE,
onAccept: (_acceptedByScrolling) => {},
onDecline: () => {},
cookieName: defaultCookieConsentName,
cookieValue: "true",
declineCookieValue: "false",
setDeclineCookie: true,
buttonText: "I understand",
declineButtonText: "I decline",
debug: false,
expires: 365,
containerClasses: "CookieConsent",
contentClasses: "",
buttonClasses: "",
buttonWrapperClasses: "",
declineButtonClasses: "",
buttonId: "rcc-confirm-button",
declineButtonId: "rcc-decline-button",
extraCookieOptions: {},
disableButtonStyles: false,
enableDeclineButton: false,
flipButtons: false,
sameSite: SAME_SITE_OPTIONS.LAX,
ButtonComponent: DefaultButtonComponent,
overlay: false,
overlayClasses: "",
onOverlayClick: () => {},
acceptOnOverlayClick: false,
ariaAcceptLabel: "Accept cookies",
ariaDeclineLabel: "Decline cookies",
acceptOnScroll: false,
acceptOnScrollPercentage: 25,
customContentAttributes: {},
customContainerAttributes: {},
customButtonProps: {},
customDeclineButtonProps: {},
customButtonWrapperAttributes: {},
style: {},
buttonStyle: {},
declineButtonStyle: {},
contentStyle: {},
overlayStyle: {},
};

View File

@ -0,0 +1,59 @@
export interface CookieConsentState {
visible: boolean;
style: React.CSSProperties;
buttonStyle: React.CSSProperties;
declineButtonStyle: React.CSSProperties;
contentStyle: React.CSSProperties;
overlayStyle: React.CSSProperties;
}
export const defaultState: CookieConsentState = {
visible: false,
style: {
alignItems: "baseline",
background: "#353535",
color: "white",
display: "flex",
flexWrap: "wrap",
justifyContent: "space-between",
left: "0",
position: "fixed",
width: "100%",
zIndex: "999",
},
buttonStyle: {
background: "#ffd42d",
border: "0",
borderRadius: "0px",
boxShadow: "none",
color: "black",
cursor: "pointer",
flex: "0 0 auto",
padding: "5px 10px",
margin: "15px",
},
declineButtonStyle: {
background: "#c12a2a",
border: "0",
borderRadius: "0px",
boxShadow: "none",
color: "#e5e5e5",
cursor: "pointer",
flex: "0 0 auto",
padding: "5px 10px",
margin: "15px",
},
contentStyle: {
flex: "1 0 300px",
margin: "15px",
},
overlayStyle: {
position: "fixed",
left: 0,
top: 0,
width: "100%",
height: "100%",
zIndex: "999",
backgroundColor: "rgba(0,0,0,0.3)",
},
};

View File

@ -1,129 +1,15 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import Cookies from "js-cookie";
import React, { Component, CSSProperties } from "react";
import { ConditionalWrapper } from "./components/ConditionalWrapper";
import { CookieConsentProps, defaultCookieConsentProps } from "./CookieConsent.props";
import { CookieConsentState, defaultState } from "./CookieConsent.state";
import { POSITION_OPTIONS, SAME_SITE_OPTIONS, VISIBILITY_OPTIONS } from "./models/constants";
import { getCookieConsentValue, getLegacyCookieName } from "./utilities";
export const OPTIONS = {
TOP: "top",
BOTTOM: "bottom",
NONE: "none",
};
export class CookieConsent extends Component<CookieConsentProps, CookieConsentState> {
public static defaultProps = defaultCookieConsentProps;
export const SAME_SITE_OPTIONS = {
STRICT: "strict",
LAX: "lax",
NONE: "none",
};
export const VISIBLE_OPTIONS = {
HIDDEN: "hidden",
SHOW: "show",
BY_COOKIE_VALUE: "byCookieValue",
};
/**
* Returns the value of the consent cookie
* Retrieves the regular value first and if not found the legacy one according
* to: https://web.dev/samesite-cookie-recipes/#handling-incompatible-clients
* @param {*} name optional name of the cookie
*/
export const getCookieConsentValue = (name = defaultCookieConsentName) => {
const cookieValue = Cookies.get(name);
// if the cookieValue is undefined check for the legacy cookie
if (cookieValue === undefined) {
return Cookies.get(getLegacyCookieName(name));
}
return cookieValue;
};
/**
* Reset the consent cookie
* Remove the cookie on browser in order to allow user to change their consent
* @param {*} name optional name of the cookie
*/
export const resetCookieConsentValue = (name = defaultCookieConsentName) => {
Cookies.remove(name);
};
/**
* Get the legacy cookie name by the regular cookie name
* @param {string} name of cookie to get
*/
const getLegacyCookieName = (name) => {
return `${name}-legacy`;
};
/**
* Default name of the cookie which is set by CookieConsent
*/
const defaultCookieConsentName = "CookieConsent";
/**
* A function to wrap elements with a "wrapper" on a condition
* @param {object} wrappingOptions
* condition == boolean condition, when to wrap
* wrapper == style to wrap. e.g <div>{children}</div>
* children == react passes whatever is between tags as children. Don't supply this yourself.
*/
const ConditionalWrapper = ({ condition, wrapper, children }) => {
return condition ? wrapper(children) : children;
};
class CookieConsent extends Component {
constructor(props) {
super(props);
this.state = {
visible: false,
style: {
alignItems: "baseline",
background: "#353535",
color: "white",
display: "flex",
flexWrap: "wrap",
justifyContent: "space-between",
left: "0",
position: "fixed",
width: "100%",
zIndex: "999",
},
buttonStyle: {
background: "#ffd42d",
border: "0",
borderRadius: "0px",
boxShadow: "none",
color: "black",
cursor: "pointer",
flex: "0 0 auto",
padding: "5px 10px",
margin: "15px",
},
declineButtonStyle: {
background: "#c12a2a",
border: "0",
borderRadius: "0px",
boxShadow: "none",
color: "#e5e5e5",
cursor: "pointer",
flex: "0 0 auto",
padding: "5px 10px",
margin: "15px",
},
contentStyle: {
flex: "1 0 300px",
margin: "15px",
},
overlayStyle: {
position: "fixed",
left: 0,
top: 0,
width: "100%",
height: "100%",
zIndex: "999",
backgroundColor: "rgba(0,0,0,0.3)",
},
};
}
state: CookieConsentState = defaultState;
componentDidMount() {
const { debug } = this.props;
@ -193,17 +79,18 @@ class CookieConsent extends Component {
* Sets two cookies to handle incompatible browsers, more details:
* https://web.dev/samesite-cookie-recipes/#handling-incompatible-clients
*/
setCookie(cookieName, cookieValue) {
setCookie(cookieName: string, cookieValue: string | object) {
const { extraCookieOptions, expires, sameSite } = this.props;
let { cookieSecurity } = this.props;
if (cookieSecurity === undefined) {
cookieSecurity = location ? location.protocol === "https:" : true;
cookieSecurity = window.location ? window.location.protocol === "https:" : true;
}
const cookieOptions = { expires, ...extraCookieOptions, sameSite, secure: cookieSecurity };
// Fallback for older browsers where can not set SameSite=None, SEE: https://web.dev/samesite-cookie-recipes/#handling-incompatible-clients
// Fallback for older browsers where can not set SameSite=None,
// SEE: https://web.dev/samesite-cookie-recipes/#handling-incompatible-clients
if (sameSite === SAME_SITE_OPTIONS.NONE) {
Cookies.set(getLegacyCookieName(cookieName), cookieValue, cookieOptions);
}
@ -229,10 +116,10 @@ class CookieConsent extends Component {
const { acceptOnScrollPercentage } = this.props;
// (top / height) - height * 100
const rootNode = document.documentElement,
body = document.body,
top = "scrollTop",
height = "scrollHeight";
const rootNode = document.documentElement;
const body = document.body;
const top = "scrollTop";
const height = "scrollHeight";
const percentage =
((rootNode[top] || body[top]) /
@ -254,9 +141,9 @@ class CookieConsent extends Component {
render() {
// If the bar shouldn't be visible don't render anything.
switch (this.props.visible) {
case VISIBLE_OPTIONS.HIDDEN:
case VISIBILITY_OPTIONS.HIDDEN:
return null;
case VISIBLE_OPTIONS.BY_COOKIE_VALUE:
case VISIBILITY_OPTIONS.BY_COOKIE_VALUE:
if (!this.state.visible) {
return null;
}
@ -297,11 +184,11 @@ class CookieConsent extends Component {
customButtonWrapperAttributes,
} = this.props;
let myStyle = {};
let myButtonStyle = {};
let myDeclineButtonStyle = {};
let myContentStyle = {};
let myOverlayStyle = {};
let myStyle: CSSProperties = {};
let myButtonStyle: CSSProperties = {};
let myDeclineButtonStyle: CSSProperties = {};
let myContentStyle: CSSProperties = {};
let myOverlayStyle: CSSProperties = {};
if (disableStyles) {
// if styles are disabled use the provided styles (or none)
@ -331,11 +218,11 @@ class CookieConsent extends Component {
// syntactic sugar to enable user to easily select top / bottom
switch (location) {
case OPTIONS.TOP:
case POSITION_OPTIONS.TOP:
myStyle.top = "0";
break;
case OPTIONS.BOTTOM:
case POSITION_OPTIONS.BOTTOM:
myStyle.bottom = "0";
break;
}
@ -411,100 +298,4 @@ class CookieConsent extends Component {
}
}
CookieConsent.propTypes = {
location: PropTypes.oneOf(Object.keys(OPTIONS).map((key) => OPTIONS[key])),
visible: PropTypes.oneOf(Object.keys(VISIBLE_OPTIONS).map((key) => VISIBLE_OPTIONS[key])),
sameSite: PropTypes.oneOf(Object.keys(SAME_SITE_OPTIONS).map((key) => SAME_SITE_OPTIONS[key])),
style: PropTypes.object,
buttonStyle: PropTypes.object,
declineButtonStyle: PropTypes.object,
contentStyle: PropTypes.object,
children: PropTypes.any, // eslint-disable-line react/forbid-prop-types
disableStyles: PropTypes.bool,
hideOnAccept: PropTypes.bool,
hideOnDecline: PropTypes.bool,
onAccept: PropTypes.func,
onDecline: PropTypes.func,
buttonText: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.element]),
declineButtonText: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.element]),
cookieName: PropTypes.string,
cookieValue: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]),
declineCookieValue: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]),
setDeclineCookie: PropTypes.bool,
debug: PropTypes.bool,
expires: PropTypes.number,
containerClasses: PropTypes.string,
contentClasses: PropTypes.string,
buttonClasses: PropTypes.string,
buttonWrapperClasses: PropTypes.string,
declineButtonClasses: PropTypes.string,
buttonId: PropTypes.string,
declineButtonId: PropTypes.string,
extraCookieOptions: PropTypes.object,
disableButtonStyles: PropTypes.bool,
enableDeclineButton: PropTypes.bool,
flipButtons: PropTypes.bool,
ButtonComponent: PropTypes.elementType,
cookieSecurity: PropTypes.bool,
overlay: PropTypes.bool,
overlayClasses: PropTypes.string,
overlayStyle: PropTypes.object,
onOverlayClick: PropTypes.func,
acceptOnOverlayClick: PropTypes.bool,
ariaAcceptLabel: PropTypes.string,
ariaDeclineLabel: PropTypes.string,
acceptOnScroll: PropTypes.bool,
acceptOnScrollPercentage: PropTypes.number,
customContentAttributes: PropTypes.object,
customContainerAttributes: PropTypes.object,
customButtonProps: PropTypes.object,
customDeclineButtonProps: PropTypes.object,
customButtonWrapperAttributes: PropTypes.object,
};
CookieConsent.defaultProps = {
disableStyles: false,
hideOnAccept: true,
hideOnDecline: true,
location: OPTIONS.BOTTOM,
visible: VISIBLE_OPTIONS.BY_COOKIE_VALUE,
onAccept: () => {},
onDecline: () => {},
cookieName: defaultCookieConsentName,
cookieValue: true,
declineCookieValue: false,
setDeclineCookie: true,
buttonText: "I understand",
declineButtonText: "I decline",
debug: false,
expires: 365,
containerClasses: "CookieConsent",
contentClasses: "",
buttonClasses: "",
buttonWrapperClasses: "",
declineButtonClasses: "",
buttonId: "rcc-confirm-button",
declineButtonId: "rcc-decline-button",
extraCookieOptions: {},
disableButtonStyles: false,
enableDeclineButton: false,
flipButtons: false,
sameSite: SAME_SITE_OPTIONS.LAX,
ButtonComponent: ({ children, ...props }) => <button {...props}>{children}</button>,
overlay: false,
overlayClasses: "",
onOverlayClick: () => {},
acceptOnOverlayClick: false,
ariaAcceptLabel: "Accept cookies",
ariaDeclineLabel: "Decline cookies",
acceptOnScroll: false,
acceptOnScrollPercentage: 25,
customContentAttributes: {},
customContainerAttributes: {},
customButtonProps: {},
customDeclineButtonProps: {},
customButtonWrapperAttributes: {},
};
export default CookieConsent;
export { Cookies };

View File

@ -0,0 +1,18 @@
import { FunctionComponent, ReactNode } from "react";
type Props = {
condition: boolean;
wrapper: (_: ReactNode) => any;
children: ReactNode;
};
/**
* A function to wrap elements with a "wrapper" on a condition
* @param {object} wrappingOptions
* condition == boolean condition, when to wrap
* wrapper == style to wrap. e.g <div>{children}</div>
* children == react passes whatever is between tags as children. Don't supply this yourself.
*/
export const ConditionalWrapper: FunctionComponent<Props> = ({ condition, wrapper, children }) => {
return condition ? wrapper(children) : children;
};

78
src/index.d.ts vendored
View File

@ -1,78 +0,0 @@
import * as React from "react";
import Cookies from "js-cookie";
export interface CookieConsentProps {
location?: "top" | "bottom" | "none";
sameSite?: "strict" | "lax" | "none";
visible?: "hidden" | "show" | "byCookieValue";
cookieSecurity?: boolean;
style?: object;
buttonStyle?: object;
declineButtonStyle?: object;
contentStyle?: object;
children?: React.ReactNode;
disableStyles?: boolean;
hideOnAccept?: boolean;
hideOnDecline?: boolean;
onAccept?: (acceptedByScrolling?: boolean) => void;
onDecline?: Function;
buttonText?: Function | React.ReactNode;
declineButtonText?: Function | React.ReactNode;
cookieName?: string;
cookieValue?: string | boolean | number;
declineCookieValue?: string | boolean | number;
setDeclineCookie?: boolean;
debug?: boolean;
expires?: number;
containerClasses?: string;
contentClasses?: string;
buttonWrapperClasses?: string;
buttonClasses?: string;
declineButtonClasses?: string;
buttonId?: string;
declineButtonId?: string;
extraCookieOptions?: object;
disableButtonStyles?: boolean;
enableDeclineButton?: boolean;
flipButtons?: boolean;
ButtonComponent?: React.ElementType;
overlay?: boolean;
overlayClasses?: string;
overlayStyle?: object;
onOverlayClick?: () => void;
acceptOnOverlayClick?: boolean;
ariaAcceptLabel?: string;
ariaDeclineLabel?: string;
acceptOnScroll?: boolean;
acceptOnScrollPercentage?: number;
customContentAttributes?: object;
customContainerAttributes?: object;
customButtonProps?: object;
customDeclineButtonProps?: object;
customButtonWrapperAttributes?: React.HTMLAttributes<HTMLDivElement>;
}
export default class CookieConsent extends React.Component<CookieConsentProps, {}> {}
/**
* Returns the value of the consent cookie
* Retrieves the regular value first and if not found the legacy one according
* to: https://web.dev/samesite-cookie-recipes/#handling-incompatible-clients
* @param {*} name optional name of the cookie
*/
export function getCookieConsentValue(name?: string): string;
/**
* Reset the consent cookie
* Remove the cookie on browser in order to allow user to change their consent
* @param {*} name optional name of the cookie
*/
export function resetCookieConsentValue(name?: string): void;
/**
* Get the legacy cookie name by the regular cookie name
* @param {string} name of cookie to get
*/
export function getLegacyCookieName(name: string): string;
export { Cookies };

11
src/index.tsx Normal file
View File

@ -0,0 +1,11 @@
import Cookies from "js-cookie";
import { ConditionalWrapper } from "./components/ConditionalWrapper";
import { CookieConsent } from "./CookieConsent";
import { POSITION_OPTIONS, VISIBILITY_OPTIONS } from "./models/constants";
export * from "./models/constants";
export * from "./utilities";
export { Cookies, CookieConsent, ConditionalWrapper };
// backwards compatibility exports
export { POSITION_OPTIONS as OPTIONS, VISIBILITY_OPTIONS as VISIBLE_OPTIONS };
export default CookieConsent;

View File

@ -0,0 +1 @@
export const defaultCookieConsentName = "CookieConsent";

View File

@ -0,0 +1,4 @@
export * from "./positionOptions";
export * from "./sameSiteOptions";
export * from "./visibilityOptions";
export * from "./defaultCookieName";

View File

@ -0,0 +1,5 @@
export const POSITION_OPTIONS = {
TOP: "top",
BOTTOM: "bottom",
NONE: "none",
};

View File

@ -0,0 +1,5 @@
export enum SAME_SITE_OPTIONS {
STRICT = "strict",
LAX = "lax",
NONE = "none",
}

View File

@ -0,0 +1,5 @@
export const VISIBILITY_OPTIONS = {
HIDDEN: "hidden",
SHOW: "show",
BY_COOKIE_VALUE: "byCookieValue",
};

35
src/utilities.ts Normal file
View File

@ -0,0 +1,35 @@
import Cookies from "js-cookie";
import { defaultCookieConsentName } from "./models/constants";
/**
* Returns the value of the consent cookie
* Retrieves the regular value first and if not found the legacy one according
* to: https://web.dev/samesite-cookie-recipes/#handling-incompatible-clients
* @param {*} name optional name of the cookie
*/
export const getCookieConsentValue = (name = defaultCookieConsentName) => {
const cookieValue = Cookies.get(name);
// if the cookieValue is undefined check for the legacy cookie
if (cookieValue === undefined) {
return Cookies.get(getLegacyCookieName(name));
}
return cookieValue;
};
/**
* Reset the consent cookie
* Remove the cookie on browser in order to allow user to change their consent
* @param {*} name optional name of the cookie
*/
export const resetCookieConsentValue = (name = defaultCookieConsentName) => {
Cookies.remove(name);
};
/**
* Get the legacy cookie name by the regular cookie name
* @param {string} name of cookie to get
*/
export const getLegacyCookieName = (name: string) => {
return `${name}-legacy`;
};

View File

@ -0,0 +1,11 @@
import { FunctionComponent } from "react";
type Props = {};
export const Intro: FunctionComponent<Props> = () => {
return (
<h1>
<p>Debug is turned on for all stories so that the bar always shows up</p>
</h1>
);
};

View File

@ -0,0 +1,3 @@
import { CookieConsentProps, defaultCookieConsentProps } from "../../src/CookieConsent.props";
export const defaultStoryProps: CookieConsentProps = { ...defaultCookieConsentProps, debug: true };

View File

@ -0,0 +1,13 @@
import { Story } from "@storybook/react";
import CookieConsent from "../../src";
import { Intro } from "./intro";
export const DefaultTemplate: Story<any> = (args) => (
<>
<Intro />
<CookieConsent {...args}>
This website uses cookies to enhance the user experience.
<span style={{ fontSize: "10px" }}>This bit of text is smaller :O</span>
</CookieConsent>
</>
);

34
stories/index.stories.tsx Normal file
View File

@ -0,0 +1,34 @@
import { Meta } from "@storybook/react";
import { CookieConsent } from "../src";
import { defaultStoryProps } from "./defaults/storyProps";
import { DefaultTemplate } from "./defaults/template";
const meta: Meta = {
title: "CookieConsent",
component: CookieConsent,
argTypes: {
children: {
control: {
type: "text",
},
},
},
parameters: {
controls: { expanded: true },
},
};
export default meta;
export const Default = DefaultTemplate.bind({});
Default.args = defaultStoryProps;
// stories
export * from "./stories/acceptOnScroll.story";
export * from "./stories/additionalButtons.story";
export * from "./stories/customStyling.story";
export * from "./stories/flippedButtons.story";
export * from "./stories/muiButtons.story";
export * from "./stories/onAccept.story";
export * from "./stories/overlay.story";
export * from "./stories/rainbows.story";

View File

@ -0,0 +1,34 @@
import { Story } from "@storybook/react";
import CookieConsent from "../../src";
import { CookieConsentProps } from "../../src/CookieConsent.props";
import { Intro } from "../defaults/intro";
import { defaultStoryProps } from "../defaults/storyProps";
const AcceptOnScrollTemplate: Story<any> = (args) => (
<>
<Intro />
{Array.from(Array(25).keys()).map((_something) => (
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Voluptates ipsa sequi soluta
mollitia illum, hic quaerat ipsum sint odit delectus magni neque sunt adipisci culpa harum
aut distinctio quisquam ab!
</p>
))}
<CookieConsent {...args}>
Scroll for {args.acceptOnScrollPercentage}% and the onAccept will trigger
</CookieConsent>
</>
);
const AcceptOnScroll = AcceptOnScrollTemplate.bind({});
AcceptOnScroll.args = {
...defaultStoryProps,
onAccept: (acceptedByScrolling) => {
alert(`ACCEPTED! By scrolling? ${JSON.stringify(acceptedByScrolling)}`);
},
acceptOnScroll: true,
acceptOnScrollPercentage: 25,
} as CookieConsentProps;
export { AcceptOnScroll };

View File

@ -0,0 +1,17 @@
import { Story } from "@storybook/react";
import CookieConsent from "../../src";
import { Intro } from "../defaults/intro";
import { defaultStoryProps } from "../defaults/storyProps";
const AdditionalButtonsTemplate: Story<any> = (args) => (
<>
<Intro />
<CookieConsent {...args}>
<button style={{ color: "gray" }}>I am a custom config button</button>
<span style={{ fontSize: "10px" }}>This bit of text is smaller :O</span>
</CookieConsent>
</>
);
const AdditionalButtons = AdditionalButtonsTemplate.bind({});
AdditionalButtons.args = defaultStoryProps;
export { AdditionalButtons };

View File

@ -0,0 +1,12 @@
import { CookieConsentProps } from "../../src/CookieConsent.props";
import { defaultStoryProps } from "../defaults/storyProps";
import { DefaultTemplate } from "../defaults/template";
const CustomStyling = DefaultTemplate.bind({});
CustomStyling.args = {
...defaultStoryProps,
style: { background: "red" },
buttonStyle: { fontWeight: "bold" },
} as CookieConsentProps;
export { CustomStyling };

View File

@ -0,0 +1,12 @@
import { CookieConsentProps } from "../../src/CookieConsent.props";
import { defaultStoryProps } from "../defaults/storyProps";
import { DefaultTemplate } from "../defaults/template";
const FlippedButtons = DefaultTemplate.bind({});
FlippedButtons.args = {
...defaultStoryProps,
flipButtons: true,
enableDeclineButton: true,
} as CookieConsentProps;
export { FlippedButtons };

View File

@ -0,0 +1,14 @@
import { Button } from "@mui/material";
import { CookieConsentProps } from "../../src/CookieConsent.props";
import { defaultStoryProps } from "../defaults/storyProps";
import { DefaultTemplate } from "../defaults/template";
const CustomMuiButton = DefaultTemplate.bind({});
CustomMuiButton.args = {
...defaultStoryProps,
disableButtonStyles: true,
ButtonComponent: Button,
customButtonProps: { variant: "contained", style: { marginRight: "10px" } },
} as CookieConsentProps;
export { CustomMuiButton };

View File

@ -0,0 +1,13 @@
import { CookieConsentProps } from "../../src/CookieConsent.props";
import { defaultStoryProps } from "../defaults/storyProps";
import { DefaultTemplate } from "../defaults/template";
const CustomOnAccept = DefaultTemplate.bind({});
CustomOnAccept.args = {
...defaultStoryProps,
onAccept: (acceptedByScrolling) => {
alert(`ACCEPTED! By scrolling? ${JSON.stringify(acceptedByScrolling)}`);
},
} as CookieConsentProps;
export { CustomOnAccept };

View File

@ -0,0 +1,11 @@
import { CookieConsentProps } from "../../src/CookieConsent.props";
import { defaultStoryProps } from "../defaults/storyProps";
import { DefaultTemplate } from "../defaults/template";
const Overlay = DefaultTemplate.bind({});
Overlay.args = {
...defaultStoryProps,
overlay: true,
} as CookieConsentProps;
export { Overlay };

View File

@ -0,0 +1,22 @@
import { CookieConsentProps } from "../../src/CookieConsent.props";
import { defaultStoryProps } from "../defaults/storyProps";
import { DefaultTemplate } from "../defaults/template";
const Rainbows = DefaultTemplate.bind({});
Rainbows.args = {
...defaultStoryProps,
buttonText: "OMG DOUBLE RAINBOW",
style: {
background: "linear-gradient(to right, orange , yellow, green, cyan, blue, violet)",
textShadow: "2px 2px black",
},
buttonStyle: {
background: "linear-gradient(to left, orange , yellow, green, cyan, blue, violet)",
color: "white",
fontWeight: "bolder",
textShadow: "2px 2px black",
},
} as CookieConsentProps;
export { Rainbows };

35
tsconfig.json Normal file
View File

@ -0,0 +1,35 @@
{
// see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
"include": ["src", "types"],
"compilerOptions": {
"module": "esnext",
"lib": ["dom", "esnext"],
"importHelpers": true,
// output .d.ts declaration files for consumers
"declaration": true,
// output .js.map sourcemap files for consumers
"sourceMap": true,
// match output dir to input dir. e.g. dist/index instead of dist/src/index
"rootDir": "./src",
// stricter type-checking for stronger correctness. Recommended by TS
"strict": true,
// linter checks for common issues
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
// noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
"noUnusedLocals": true,
"noUnusedParameters": true,
// use Node's module resolution algorithm, instead of the legacy TS one
"moduleResolution": "node",
// transpile JSX to React.createElement
"jsx": "react",
// interop between ESM and CJS modules. Recommended by TS
"esModuleInterop": true,
// significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
"skipLibCheck": true,
// error out if import and file system have a casing mismatch. Recommended by TS
"forceConsistentCasingInFileNames": true,
// `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc`
"noEmit": true,
}
}

View File

@ -1,37 +0,0 @@
var path = require("path");
var CopyWebpackPlugin = require("copy-webpack-plugin");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "build"),
filename: "index.js",
library: {
name: "ReactCookieConsent",
type: "umd",
},
environment: {
arrowFunction: false, // the generated runtime-code should not use arrow functions
},
globalObject: `typeof self !== 'undefined' ? self : this`,
},
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, "src"),
exclude: /(node_modules|build)/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
},
],
},
externals: {
react: "commonjs react", // this line is just to use the React dependency of our parent-testing-project instead of using our own React.
},
plugins: [new CopyWebpackPlugin({ patterns: [{ from: "src/index.d.ts", to: "index.d.ts" }] })],
};