diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 0000000..33c28ab --- /dev/null +++ b/.storybook/main.js @@ -0,0 +1,43 @@ +const path = require("path"); + +module.exports = { + stories: ["../components/**/*.stories.@(js|jsx|ts|tsx)"], + addons: [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-interactions", + ], + features: { + interactionsDebugger: true, + }, + framework: "@storybook/react", + webpackFinal: async (config, { configType }) => { + // Make whatever fine-grained changes you need + // config.module.rules.push({ + // test: /\.scss$/, + // use: ["style-loader", "css-loader", "sass-loader"], + // include: path.resolve(__dirname, "../"), + // }); + + config.module.rules.push({ + test: /\.pcss$/, + use: [ + "style-loader", + { + loader: "css-loader", + options: { + modules: { + localIdentName: "[name]__[local]", + }, + importLoaders: 1, + }, + }, + "sass-loader", + ], + include: path.resolve(__dirname, "../"), + }); + + // Return the altered config + return config; + }, +}; diff --git a/.storybook/preview.js b/.storybook/preview.js new file mode 100644 index 0000000..48afd56 --- /dev/null +++ b/.storybook/preview.js @@ -0,0 +1,9 @@ +export const parameters = { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, +} \ No newline at end of file diff --git a/README.md b/README.md index 0c79b40..c4a6bb1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ # RebelCode React Library A library of React components, hooks, and other utilities that are used in RebelCode's projects. + +## Setup +To install packages, run [ for best results use yarn ] +- yarn + +To start storybook, run +- yarn storybook + +To build package, run [ cmd throws some errors which is currently being worked on ] +- yarn build diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..26b57f5 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,7 @@ +module.exports = { + presets: [ + ['@babel/preset-env', {targets: {node: 'current'}}], + '@babel/preset-react', + '@babel/preset-typescript', + ], + }; \ No newline at end of file diff --git a/components/.gitkeep b/components/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/components/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/components/Alert/Alert.pcss b/components/Alert/Alert.pcss new file mode 100644 index 0000000..85b45ba --- /dev/null +++ b/components/Alert/Alert.pcss @@ -0,0 +1,213 @@ +.message { + align-items: stretch; + justify-content: center; + position: relative; + padding: 5px 7px; + line-height: 18px; + font-size: 13px; + background: #fff; + border: 1px solid; + border-radius: 3px; + + &:not(:first-child) { + margin-top: 10px; + } + + &:not(:last-child) { + margin-bottom: 10px; + } +} + +.centered { + padding: 5px 25px; +} + +.shaking { + animation-name: shake-animation; + animation-duration: 0.2s; +} + +.dashicon { + width: 16px !important; + height: 16px !important; + font-size: 16px !important; + line-height: 16px !important; + margin-top: 1px; +} + +.icon { + composes: dashicon; + margin-right: 8px; + flex-grow: 0; + flex-shrink: 0; +} + +.logo { + composes: icon; + + & img { + width: 18px; + height: 18px; + max-width: unset; + } +} + +.content { + display: block; +} + +.dismiss-btn { + position: absolute; + top: 5px; + right: 7px; + color: #333; + padding: 0 !important; + border: 0; + border-radius: 100px; + background: transparent; + box-sizing: border-box; + opacity: 0.5; + + &:hover { + opacity: 1; + } + + &:focus-visible { + box-shadow: 0 0 0 1px currentColor; + } + + & :global(.dashicon) { + transform: translateX(1px); + } +} + + +@keyframes shake-animation { + 0%, 20%, 40%, 60%, 80%, 100% { + transform: translateX(0); + } + 30%, 70% { + transform: translateX(-2px); + } + 10%, 50%, 90% { + transform: translateX(2px); + } +} + +/* + * COLOR VARIATIONS + *---------------------------- + */ + +/* SUCCESS */ +.success { + composes: message; + background: #ecf4eb; + box-shadow: 0 1px 2px rgba(37, 85, 31, 0.15); + border-color: rgba(61, 142, 52, 0.4); + + & .icon, & .dismiss-btn { + color: #2e6b27; + } + + & .content { + color: #122b10; + } +} + +/* INFO */ +.info { + composes: message; + background: #e8f5f7; + box-shadow: 0 1px 2px rgba(14, 91, 107, 0.15); + border-color: rgba(24, 152, 178, 0.4); + + & .icon, & .dismiss-btn { + color: #127286; + } + + & .content { + color: #072e35; + } +} + +/* WARNING */ +.warning { + composes: message; + background: #fff4e6; + box-shadow: 0 1px 2px rgba(153, 88, 0, 0.15); + border-color: rgba(255, 147, 0, 0.4); + + & .icon, & .dismiss-btn { + color: #bf6e00; + } + + & .content { + color: #4d2c00; + } +} + +/* PREMIUM */ +.premium { + composes: message; + background: rgba(221, 35, 75, .1); + border-color: var(--sli-pro); + + & .icon, & .dismiss-btn, & .content { + color: var(--sli-quasi-black); + } + + & a { + color: #000 !important; + font-weight: 600; + } +} + +/* PRO TIP */ +.pro-tip { + composes: message; + background: #eeeffa; + box-shadow: 0 1px 2px rgba(53, 57, 123, 0.15); + border-color: rgba(89, 95, 205, 0.4); + + & .icon, & .dismiss-btn { + color: #43479a; + } + + & .content { + color: #1b1d3e; + } +} + +/* ERROR */ +.error { + composes: message; + background: #fbe9ec; + box-shadow: 0 1px 2px rgba(130, 22, 40, 0.15); + border-color: rgba(216, 36, 66, 0.4); + + & .icon, & .dismiss-btn { + color: #a21b32; + } + + & .content { + color: #410b14; + } +} + + +/* GREY */ +.grey { + composes: message; + background: var(--sli-wp-grey); + box-shadow: 0 1px 2px rgba(120, 120, 120, 0.15); + border-color: var(--sli-line-color); + + & .icon, & .dismiss-btn { + color: #555; + } + + & .content { + color: #222; + } +} \ No newline at end of file diff --git a/components/Alert/index.tsx b/components/Alert/index.tsx new file mode 100644 index 0000000..31f6c25 --- /dev/null +++ b/components/Alert/index.tsx @@ -0,0 +1,89 @@ +import React, { CSSProperties, ReactNode } from "react"; +import css from "./Alert.pcss"; +import { classList } from "../../utils/classes"; +import { Dashicon, DashiconTy } from "../Dashicon"; + +type AlertType = + | "success" + | "info" + | "pro-tip" + | "premium" + | "warning" + | "error" + | "grey"; + +export type Props = { + className?: string; + style?: CSSProperties; + children?: ReactNode; + type: AlertType; + showIcon?: boolean; + shake?: boolean; + centered?: boolean; + isDismissible?: boolean; + onDismiss?: () => void; +}; + +export function Alert({ + className, + style, + children, + type, + showIcon, + shake, + centered, + isDismissible, + onDismiss, +}: Props) { + const [dismissed, setDismissed] = React.useState(false); + + const handleClick = () => { + if (isDismissible) { + setDismissed(true); + onDismiss && onDismiss(); + } + }; + + const fullClassName = classList( + css[type], + (shake && css.shaking||""), + ( centered && css.centered||""), + className as string + ); + + return dismissed ? null : ( +
+ {showIcon && ( + + )} + +
{children}
+ + {isDismissible && ( + + )} +
+ ); +} + +/** + * Retrieves the appropriate dashicon for a given message type. + * + * @param type + */ +function getIconFor(type: AlertType): DashiconTy { + switch (type) { + case "success": + return "yes-alt"; + case "pro-tip": + return "lightbulb"; + case "error": + case "warning": + return "warning"; + case "info": + default: + return "info"; + } +} diff --git a/components/Alert/tests/__snapshots__/Alert.spec.js b/components/Alert/tests/__snapshots__/Alert.spec.js new file mode 100644 index 0000000..d417dea --- /dev/null +++ b/components/Alert/tests/__snapshots__/Alert.spec.js @@ -0,0 +1,30 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import { Alert } from "../../index"; + +afterEach(cleanup); + +describe("Alert", () => { + it("should take a snapshot", () => { + const { asFragment } = render( + + ); + + expect(asFragment()).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render(); + + expect(queryByLabelText("Alert")).toBeTruthy(); + }); + + it("renders text correctly", () => { + const { queryByText } = render( + + ); + + expect(queryByText("Success")).toBeTruthy(); + expect(queryByText("Failed")).toBeFalsy(); + }); +}); diff --git a/components/Alert/tests/__snapshots__/Alert.stories.tsx b/components/Alert/tests/__snapshots__/Alert.stories.tsx new file mode 100644 index 0000000..52b11dc --- /dev/null +++ b/components/Alert/tests/__snapshots__/Alert.stories.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import { Alert } from '../../index'; + +export default { + title: "Components/Alert", + component: Alert, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: 'fullscreen', + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ; + +export const Success = Template.bind({}); +Success.args = { + type: "success", + children: <>Pax Unlock more features today. Upgrade now. +}; + +export const Info = Template.bind({}); +Info.args = { + type: "info", + children: <>Pax Unlock more features today. Upgrade now. +}; + +export const ProTip = Template.bind({}); +ProTip.args = { + type: "pro-tip", + children: <>Pax Unlock more features today. Upgrade now. +}; + +export const Premium = Template.bind({}); +Premium.args = { + type: "premium", + children: <>Pax Unlock more features today. Upgrade now. +}; + +export const Warning = Template.bind({}); +Warning.args = { + type: "warning", + children: <>Pax Unlock more features today. Upgrade now. +}; + +export const Error = Template.bind({}); +Error.args = { + type: "error", + children: <>Pax Unlock more features today. Upgrade now. +}; + +export const Grey = Template.bind({}); +Grey.args = { + type: "grey", + children: <>Pax Unlock more features today. Upgrade now. +}; diff --git a/components/Alert/tests/__snapshots__/__snapshots__/Alert.spec.js.snap b/components/Alert/tests/__snapshots__/__snapshots__/Alert.spec.js.snap new file mode 100644 index 0000000..57dc3e9 --- /dev/null +++ b/components/Alert/tests/__snapshots__/__snapshots__/Alert.spec.js.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Alert should take a snapshot 1`] = ` + +
+
+
+ +`; diff --git a/components/Checkbox/Checkbox.pcss b/components/Checkbox/Checkbox.pcss new file mode 100644 index 0000000..cd6d52e --- /dev/null +++ b/components/Checkbox/Checkbox.pcss @@ -0,0 +1,13 @@ +.checkbox-field { + display: flex; + flex-direction: column; + justify-content: center; + min-height: 40px; + padding: 10px 0; + box-sizing: border-box; +} + +.aligner { + display: flex; + flex-direction: row; +} diff --git a/components/Checkbox/index.tsx b/components/Checkbox/index.tsx new file mode 100644 index 0000000..525103a --- /dev/null +++ b/components/Checkbox/index.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import {FlexRow, FlexColumn} from "../Flex" + +type Props = { + id?: string; + value?: boolean; + disabled?: boolean; + onChange: (value: boolean) => void; +} + +export function Checkbox({id, value, onChange, disabled}: Props) { + return ( + + + onChange(e.target.checked)} + disabled={disabled} + /> + + + ); +} diff --git a/components/Checkbox/tests/Checkbox.spec.js b/components/Checkbox/tests/Checkbox.spec.js new file mode 100644 index 0000000..68478b7 --- /dev/null +++ b/components/Checkbox/tests/Checkbox.spec.js @@ -0,0 +1,27 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import { Checkbox } from "../index"; + +afterEach(cleanup); + +describe("Checkbox", () => { + it("should take a snapshot", () => { + const { asFragment } = render( {}} />); + + expect(asFragment( {}} />)).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render( {}} />); + + expect(queryByLabelText("Checkbox")).toBeTruthy(); + }); + + it("renders text correctly", () => { + const { queryByLabelText } = render( + {}} /> + ); + + expect(queryByLabelText("Checkbox")).toBeTruthy(); + }); +}); diff --git a/components/Checkbox/tests/Checkbox.stories.tsx b/components/Checkbox/tests/Checkbox.stories.tsx new file mode 100644 index 0000000..056dc08 --- /dev/null +++ b/components/Checkbox/tests/Checkbox.stories.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; + +import { Checkbox } from "../index"; + +export default { + title: "Components/Checkbox", + component: Checkbox, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen", + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const Enabled = Template.bind({}); +Enabled.args = { + value: true, + disabled: false, +}; + + +export const Disabled = Template.bind({}); +Disabled.args = { + value: true, + disabled: true, +}; \ No newline at end of file diff --git a/components/Checkbox/tests/__snapshots__/Checkbox.spec.js.snap b/components/Checkbox/tests/__snapshots__/Checkbox.spec.js.snap new file mode 100644 index 0000000..ea1dd11 --- /dev/null +++ b/components/Checkbox/tests/__snapshots__/Checkbox.spec.js.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Checkbox should take a snapshot 1`] = ` + +
+
+ +
+
+
+`; diff --git a/components/CircleImage/CircleImage.pcss b/components/CircleImage/CircleImage.pcss new file mode 100644 index 0000000..a6a1b88 --- /dev/null +++ b/components/CircleImage/CircleImage.pcss @@ -0,0 +1,7 @@ +.image { + border-radius: 100%; + background-size: cover; + background-position: center center; + background-repeat: no-repeat; + overflow: hidden; +} diff --git a/components/CircleImage/index.tsx b/components/CircleImage/index.tsx new file mode 100644 index 0000000..b280a0f --- /dev/null +++ b/components/CircleImage/index.tsx @@ -0,0 +1,12 @@ +import React, {forwardRef} from "react" +import css from "./CircleImage.pcss" + +export type Props = React.HTMLProps & { + crossOrigin?: "" | "anonymous" | "use-credentials" +} + +export const CircleImage = forwardRef( + function CircleImage({className = "", ...props}, ref) { + return + }, +) diff --git a/components/CircleImage/tests/__snapshots__/CircleImage.spec.js b/components/CircleImage/tests/__snapshots__/CircleImage.spec.js new file mode 100644 index 0000000..4013961 --- /dev/null +++ b/components/CircleImage/tests/__snapshots__/CircleImage.spec.js @@ -0,0 +1,29 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import { CircleImage } from "../../index"; + +afterEach(cleanup); + +describe("CircleImage", () => { + it("should take a snapshot", () => { + const { asFragment } = render( + + ); + + expect(asFragment()).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render(); + + expect(queryByLabelText("CircleImage")).toBeTruthy(); + }); + + it("renders text correctly", () => { + const { queryByLabelText } = render( + + ); + + expect(queryByLabelText("CircleImage")).toBeTruthy(); + }); +}); diff --git a/components/CircleImage/tests/__snapshots__/CircleImage.stories.tsx b/components/CircleImage/tests/__snapshots__/CircleImage.stories.tsx new file mode 100644 index 0000000..0e19097 --- /dev/null +++ b/components/CircleImage/tests/__snapshots__/CircleImage.stories.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import { CircleImage } from '../../index'; + +export default { + title: "Components/CircleImage", + component: CircleImage, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: 'fullscreen', + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ; + +export const Intial = Template.bind({}); +Intial.args = { + src: "https://www.w3schools.com/tags/smiley.gif", +}; \ No newline at end of file diff --git a/components/CircleImage/tests/__snapshots__/__snapshots__/CircleImage.spec.js.snap b/components/CircleImage/tests/__snapshots__/__snapshots__/CircleImage.spec.js.snap new file mode 100644 index 0000000..3a8d9fd --- /dev/null +++ b/components/CircleImage/tests/__snapshots__/__snapshots__/CircleImage.spec.js.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CircleImage should take a snapshot 1`] = ` + + + +`; diff --git a/components/ColorPicker/ColorPicker.pcss b/components/ColorPicker/ColorPicker.pcss new file mode 100644 index 0000000..e875bd6 --- /dev/null +++ b/components/ColorPicker/ColorPicker.pcss @@ -0,0 +1,41 @@ +:root { + --sli-color-picker-padding: 5px; + --sli-color-picker-alpha-grid-size: 14px; + --sli-color-picker-inner-shadow: 0 0 0 5px #fff inset; +} + +.button { + position: relative; + padding: 7px 12px; + width: 100%; + height: 36px; + border: 1px solid var(--sli-line-color); + border-radius: 3px; + cursor: pointer; + + &, &:hover, &:focus-visible, &:active { + background-size: var(--sli-color-picker-alpha-grid-size); + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpSIVUYuoOGSoThZERRy1CkWoEGqFVh1MLv2CJg1Jiouj4Fpw8GOx6uDirKuDqyAIfoA4OTopukiJ/0sKLWI9OO7Hu3uPu3eAUC0yzWobBzTdNhOxqJhKr4qBVwjoRQ/6MSgzy5iTpDhajq97+Ph6F+FZrc/9ObrUjMUAn0g8ywzTJt4gnt60Dc77xCGWl1Xic+Ixky5I/Mh1xeM3zjmXBZ4ZMpOJeeIQsZhrYqWJWd7UiKeIw6qmU76Q8ljlvMVZK5ZZ/Z78hcGMvrLMdZrDiGERS5AgQkEZBRRhI0KrToqFBO1HW/iHXL9ELoVcBTByLKAEDbLrB/+D391a2ckJLykYBdpfHOdjBAjsArWK43wfO07tBPA/A1d6w1+qAjOfpFcaWvgI6N4GLq4bmrIHXO4AA0+GbMqu5KcpZLPA+xl9UxrouwU617ze6vs4fQCS1FX8Bjg4BEZzlL3e4t0dzb39e6be3w88D3KRJNOW/QAAAAZiS0dEACcAAAAB/aV4/QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+QCEhEhKGUfSx4AAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMElEQVQ4y2M8cuTIfwY8INnGlhGfPBMDhWDUgMFgAKM6A95oZph75PD/0UAc9gYAAER7B5JZPHhAAAAAAElFTkSuQmCC') var(--sli-color-picker-padding), var(--sli-color-picker-padding); + } + + &, &:focus-visible, &:active { + box-shadow: var(--sli-color-picker-inner-shadow); + } + + &:focus-visible { + outline: 0; + box-shadow: var(--sli-color-picker-inner-shadow), 0 0 0 2px var(--sli-focus-color) !important; + } +} + +.color-preview { + position: absolute; + top: calc(var(--sli-color-picker-padding) - 1px); + bottom: calc(var(--sli-color-picker-padding) - 1px); + left: calc(var(--sli-color-picker-padding) - 1px); + right: calc(var(--sli-color-picker-padding) - 1px); +} + +.popper { + z-index: 100; +} diff --git a/components/ColorPicker/index.tsx b/components/ColorPicker/index.tsx new file mode 100644 index 0000000..ec9a0d0 --- /dev/null +++ b/components/ColorPicker/index.tsx @@ -0,0 +1,101 @@ +import React, { MutableRefObject, useEffect } from "react"; +import css from "./ColorPicker.pcss"; +import ChromePicker from "react-color/lib/components/chrome/Chrome"; +import { Color, MultiColor } from "react-color-types"; +import { useDetectTabOut } from "../../hooks/useDetectTabOut"; +import { useDetectOutsideClick } from "../../hooks/useDetectOutsideClick"; +import { useDocumentEventListener } from "../../hooks/useEventListener"; +import { colorToString } from "../../utils/colorToString"; +import { Manager, Popper, Reference } from "react-popper"; +import { mergeRefs } from "../../utils/mergeRefs"; + +type Props = { + id?: string; + value?: Color; + onChange?: (c: MultiColor) => void; + disableAlpha?: boolean; +}; + +export function ColorPicker({ id, value, disableAlpha, onChange }: Props) { + value = value ?? "#fff"; + + const [color, setColor] = React.useState(value); + const [isOpen, setOpen] = React.useState(false); + const btn = React.useRef(); + const picker = React.useRef(); + + const close = React.useCallback(() => setOpen(false), []); + const toggle = React.useCallback(() => setOpen((v) => !v), []); + + const handleChange = React.useCallback( + (color: MultiColor) => { + document.getSelection() && + (document.getSelection() as Selection).removeAllRanges(); + setColor(color.rgb); + onChange && onChange(color); + }, + [onChange] + ); + + const onKeyDown = React.useCallback( + (e: KeyboardEvent) => { + if (e.key === "Escape" && isOpen) { + close(); + e.preventDefault(); + e.stopPropagation(); + } + }, + [isOpen] + ); + + useEffect(() => setColor(value as Color), [value]); + useDetectOutsideClick(btn as any, close, [picker as any]); + useDetectTabOut([btn as any, picker], close); + useDocumentEventListener("keydown", onKeyDown, [isOpen]); + + const modifiers = { + preventOverflow: { + boundariesElement: document.body, + padding: 5, + }, + }; + + return ( + + + {({ ref }) => ( + + )} + + + {({ ref, style }) => + isOpen && ( +
+ +
+ ) + } +
+
+ ); +} diff --git a/components/ColorPicker/tests/ColorPicker.spec.js b/components/ColorPicker/tests/ColorPicker.spec.js new file mode 100644 index 0000000..c300a41 --- /dev/null +++ b/components/ColorPicker/tests/ColorPicker.spec.js @@ -0,0 +1,19 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import { ColorPicker } from "../index"; + +afterEach(cleanup); + +describe("ColorPicker", () => { + it("should take a snapshot", () => { + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render(); + + expect(queryByLabelText("ColorPicker")).toBeTruthy(); + }); +}); diff --git a/components/ColorPicker/tests/ColorPicker.stories.tsx b/components/ColorPicker/tests/ColorPicker.stories.tsx new file mode 100644 index 0000000..400b583 --- /dev/null +++ b/components/ColorPicker/tests/ColorPicker.stories.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; +import { within, userEvent } from '@storybook/testing-library'; +import { ColorPicker } from "../index"; + +export default { + title: "Components/ColorPicker", + component: ColorPicker, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen", + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const Picker = Template.bind({}); +Picker.args = { + value: 'blue' +}; + +export const ShowPicker = Template.bind({}); +ShowPicker.play = async ({ canvasElement }) => { + // Starts querying the component from its root element + const canvas = within(canvasElement); + + await userEvent.click(canvas.getByTestId('colorpicker')) +}; diff --git a/components/ColorPicker/tests/__snapshots__/ColorPicker.spec.js.snap b/components/ColorPicker/tests/__snapshots__/ColorPicker.spec.js.snap new file mode 100644 index 0000000..ebb930c --- /dev/null +++ b/components/ColorPicker/tests/__snapshots__/ColorPicker.spec.js.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ColorPicker should take a snapshot 1`] = ` + + + +`; diff --git a/components/Dashicon/index.tsx b/components/Dashicon/index.tsx new file mode 100644 index 0000000..b3f886c --- /dev/null +++ b/components/Dashicon/index.tsx @@ -0,0 +1,284 @@ +import React, {HTMLAttributes} from "react"; +import {classList} from "../../utils/classes"; + +export const Dashicon = ({icon, className, ...rest}: Props) => ( + +); + +export interface Props extends HTMLAttributes { + icon: DashiconTy; +} + +export type DashiconTy = + "menu" + | "menu-alt" + | "menu-alt2" + | "menu-alt3" + | "admin-site" + | "admin-site-alt" + | "admin-site-alt2" + | "admin-site-alt3" + | "dashboard" + | "admin-post" + | "admin-media" + | "admin-links" + | "admin-page" + | "admin-comments" + | "admin-appearance" + | "admin-plugins" + | "plugins-checked" + | "admin-users" + | "admin-tools" + | "admin-settings" + | "admin-network" + | "admin-home" + | "admin-generic" + | "admin-collapse" + | "filter" + | "admin-customizer" + | "admin-multisite" + | "welcome-write-blog" + | "welcome-add-page" + | "welcome-view-site" + | "welcome-widgets-menus" + | "welcome-comments" + | "welcome-learn-more" + | "format-aside" + | "format-image" + | "format-gallery" + | "format-video" + | "format-status" + | "format-quote" + | "format-chat" + | "format-audio" + | "camera" + | "camera-alt" + | "images-alt" + | "images-alt2" + | "video-alt" + | "video-alt2" + | "video-alt3" + | "media-archive" + | "media-audio" + | "media-code" + | "media-default" + | "media-document" + | "media-interactive" + | "media-spreadsheet" + | "media-text" + | "media-video" + | "playlist-audio" + | "playlist-video" + | "controls-play" + | "controls-pause" + | "controls-forward" + | "controls-skipforward" + | "controls-back" + | "controls-skipback" + | "controls-repeat" + | "controls-volumeon" + | "controls-volumeoff" + | "image-crop" + | "image-rotate" + | "image-rotate-left" + | "image-rotate-right" + | "image-flip-vertical" + | "image-flip-horizontal" + | "image-filter" + | "undo" + | "redo" + | "editor-bold" + | "editor-italic" + | "editor-ul" + | "editor-ol" + | "editor-ol-rtl" + | "editor-quote" + | "editor-alignleft" + | "editor-aligncenter" + | "editor-alignright" + | "editor-insertmore" + | "editor-spellcheck" + | "editor-expand" + | "editor-contract" + | "editor-kitchensink" + | "editor-underline" + | "editor-justify" + | "editor-textcolor" + | "editor-paste-word" + | "editor-paste-text" + | "editor-removeformatting" + | "editor-video" + | "editor-customchar" + | "editor-outdent" + | "editor-indent" + | "editor-help" + | "editor-strikethrough" + | "editor-unlink" + | "editor-rtl" + | "editor-ltr" + | "editor-break" + | "editor-code" + | "editor-paragraph" + | "editor-table" + | "align-left" + | "align-right" + | "align-center" + | "align-none" + | "lock" + | "unlock" + | "calendar" + | "calendar-alt" + | "visibility" + | "hidden" + | "post-status" + | "edit" + | "trash" + | "sticky" + | "external" + | "arrow-up" + | "arrow-down" + | "arrow-right" + | "arrow-left" + | "arrow-up-alt" + | "arrow-down-alt" + | "arrow-right-alt" + | "arrow-left-alt" + | "arrow-up-alt2" + | "arrow-down-alt2" + | "arrow-right-alt2" + | "arrow-left-alt2" + | "sort" + | "leftright" + | "randomize" + | "list-view" + | "excerpt-view" + | "grid-view" + | "move" + | "share" + | "share-alt" + | "share-alt2" + | "twitter" + | "rss" + | "email" + | "email-alt" + | "email-alt2" + | "facebook" + | "facebook-alt" + | "googleplus" + | "networking" + | "instagram" + | "hammer" + | "art" + | "migrate" + | "performance" + | "universal-access" + | "universal-access-alt" + | "tickets" + | "nametag" + | "clipboard" + | "heart" + | "megaphone" + | "schedule" + | "tide" + | "rest-api" + | "code-standards" + | "buddicons-activity" + | "buddicons-bbpress-logo" + | "buddicons-buddypress-logo" + | "buddicons-community" + | "buddicons-forums" + | "buddicons-friends" + | "buddicons-groups" + | "buddicons-pm" + | "buddicons-replies" + | "buddicons-topics" + | "buddicons-tracking" + | "wordpress" + | "wordpress-alt" + | "pressthis" + | "update" + | "update-alt" + | "screenoptions" + | "info" + | "cart" + | "feedback" + | "cloud" + | "translation" + | "tag" + | "category" + | "archive" + | "tagcloud" + | "text" + | "yes" + | "yes-alt" + | "no" + | "no-alt" + | "plus" + | "plus-alt" + | "plus-alt2" + | "minus" + | "dismiss" + | "marker" + | "star-filled" + | "star-half" + | "star-empty" + | "flag" + | "warning" + | "location" + | "location-alt" + | "vault" + | "shield" + | "shield-alt" + | "sos" + | "search" + | "slides" + | "text-page" + | "analytics" + | "chart-pie" + | "chart-bar" + | "chart-line" + | "chart-area" + | "groups" + | "businessman" + | "businesswoman" + | "businessperson" + | "id" + | "id-alt" + | "products" + | "awards" + | "forms" + | "testimonial" + | "portfolio" + | "book" + | "book-alt" + | "download" + | "upload" + | "backup" + | "clock" + | "lightbulb" + | "microphone" + | "desktop" + | "laptop" + | "tablet" + | "smartphone" + | "phone" + | "index-card" + | "carrot" + | "building" + | "store" + | "album" + | "palmtree" + | "tickets-alt" + | "money" + | "smiley" + | "thumbs-up" + | "thumbs-down" + | "layout" + | "paperclip" + | "database" + | "database-add" + | "database-remove" + | "database-view" + | "database-import" + | "database-export" +; diff --git a/components/Dashicon/tests/Dashicon.spec.js b/components/Dashicon/tests/Dashicon.spec.js new file mode 100644 index 0000000..40261df --- /dev/null +++ b/components/Dashicon/tests/Dashicon.spec.js @@ -0,0 +1,19 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import { Dashicon } from "../index"; + +afterEach(cleanup); + +describe("Dashicon", () => { + it("should take a snapshot", () => { + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render(); + + expect(queryByLabelText("Dashicon")).toBeTruthy(); + }); +}); diff --git a/components/Dashicon/tests/Dashicon.stories.tsx b/components/Dashicon/tests/Dashicon.stories.tsx new file mode 100644 index 0000000..a4663a8 --- /dev/null +++ b/components/Dashicon/tests/Dashicon.stories.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; + +import { Dashicon } from "../index"; + +export default { + title: "Components/Dashicon", + component: Dashicon, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen", + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const Picker = Template.bind({}); +Picker.args = { + icon: 'menu' +}; + diff --git a/components/Dashicon/tests/__snapshots__/Dashicon.spec.js.snap b/components/Dashicon/tests/__snapshots__/Dashicon.spec.js.snap new file mode 100644 index 0000000..8737042 --- /dev/null +++ b/components/Dashicon/tests/__snapshots__/Dashicon.spec.js.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Dashicon should take a snapshot 1`] = ` + + + +`; diff --git a/components/ErrorBoundary/index.tsx b/components/ErrorBoundary/index.tsx new file mode 100644 index 0000000..af691a1 --- /dev/null +++ b/components/ErrorBoundary/index.tsx @@ -0,0 +1,29 @@ +import React, { ReactNode } from "react"; + +type Props = { + errorContents: ReactNode; + children?: ReactNode; +}; + +type State = { + error: any; +}; + +export class ErrorBoundary extends React.Component { + constructor(props: Props | Readonly) { + super(props); + this.state = { error: null }; + } + + static getDerivedStateFromError(error: any) { + return { hasError: true, error }; + } + + componentDidCatch(error: any, errorInfo: any) { + console.error(error, errorInfo); + } + + render() { + return this.props.errorContents; + } +} diff --git a/components/ErrorBoundary/tests/__snapshots__/ErrorBoundary.spec.js b/components/ErrorBoundary/tests/__snapshots__/ErrorBoundary.spec.js new file mode 100644 index 0000000..0f12775 --- /dev/null +++ b/components/ErrorBoundary/tests/__snapshots__/ErrorBoundary.spec.js @@ -0,0 +1,15 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import { ErrorBoundary } from "../../index"; + +afterEach(cleanup); + +describe("ErrorBoundary", () => { + it("should take a snapshot", () => { + const { asFragment } = render( + + ); + + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/components/ErrorBoundary/tests/__snapshots__/ErrorBoundary.stories.tsx b/components/ErrorBoundary/tests/__snapshots__/ErrorBoundary.stories.tsx new file mode 100644 index 0000000..cac40d9 --- /dev/null +++ b/components/ErrorBoundary/tests/__snapshots__/ErrorBoundary.stories.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import { ErrorBoundary } from '../../index'; + +export default { + title: "Components/ErrorBoundary", + component: ErrorBoundary, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: 'fullscreen', + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ; + +export const Intial = Template.bind({}); +Intial.args = { + errorContents: "Cannot render component", +}; \ No newline at end of file diff --git a/components/ErrorBoundary/tests/__snapshots__/__snapshots__/ErrorBoundary.spec.js.snap b/components/ErrorBoundary/tests/__snapshots__/__snapshots__/ErrorBoundary.spec.js.snap new file mode 100644 index 0000000..ea6d0e3 --- /dev/null +++ b/components/ErrorBoundary/tests/__snapshots__/__snapshots__/ErrorBoundary.spec.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ErrorBoundary should take a snapshot 1`] = ``; diff --git a/components/FilteredListField/index.tsx b/components/FilteredListField/index.tsx new file mode 100644 index 0000000..adac23c --- /dev/null +++ b/components/FilteredListField/index.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import {ListField, Props as InnerProps} from "../ListField"; + +export interface Props extends InnerProps { + filter: Function; + excludeMsg?: string; +} + +export function FilteredListField(props: Props) { + const [excluded, setExcluded] = React.useState(""); + + if (props.filter) { + setExcluded(""); + } + // TODO: fix come back n fix this + const onChange = (values: Array) => { + const excludedIdx = props.filter ? + values.findIndex((val) => props.filter((s:string) => !values.includes(s))) + + // values.findIndex((val) => props.exclude.includes(val)) + : -1; + + if (excludedIdx > -1) { + setExcluded(values[excludedIdx]); + } else { + props.onChange(values); + } + }; + + let message = undefined; + if (excluded.length > 0) { + const token = "%s"; + const tokenIdx = props.excludeMsg && props.excludeMsg.indexOf("%s"); + + const before = props.excludeMsg && props.excludeMsg.substring(0, tokenIdx as number); + const after = props.excludeMsg&&props.excludeMsg.substring((tokenIdx as number) + token.length); + + message = <>{before}{excluded}{after}; + } + + const newProps = { + ...props, + message, + onChange, + }; + + return ; +} diff --git a/components/FilteredListField/tests/FilteredListField.spec.js b/components/FilteredListField/tests/FilteredListField.spec.js new file mode 100644 index 0000000..000aae1 --- /dev/null +++ b/components/FilteredListField/tests/FilteredListField.spec.js @@ -0,0 +1,19 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import { FilteredListField } from "../index"; + +afterEach(cleanup); + +describe("FilteredListField", () => { + it("should take a snapshot", () => { + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render(); + + expect(queryByLabelText("ListField")).toBeTruthy(); + }); +}); diff --git a/components/FilteredListField/tests/FilteredListField.stories.tsx b/components/FilteredListField/tests/FilteredListField.stories.tsx new file mode 100644 index 0000000..c78fea3 --- /dev/null +++ b/components/FilteredListField/tests/FilteredListField.stories.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; + +import { FilteredListField } from "../index"; + +export default { + title: "Components/FilteredListField", + component: FilteredListField, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen", + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const FilteredList = Template.bind({}); +FilteredList.args = {}; + diff --git a/components/FilteredListField/tests/__snapshots__/FilteredListField.spec.js.snap b/components/FilteredListField/tests/__snapshots__/FilteredListField.spec.js.snap new file mode 100644 index 0000000..0f43c17 --- /dev/null +++ b/components/FilteredListField/tests/__snapshots__/FilteredListField.spec.js.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FilteredListField should take a snapshot 1`] = ` + +
+ +
+
+
+ Type something and press enter... +
+
+
+ +
+
+
+
+
+
+
+ +`; diff --git a/components/Flex/FlexRow.stories.tsx b/components/Flex/FlexRow.stories.tsx new file mode 100644 index 0000000..00c7bba --- /dev/null +++ b/components/Flex/FlexRow.stories.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; + +import { FlexRow, FlexColumn } from "./index"; + +export default { + title: "Components/FlexRow", + component: FlexRow, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen", + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const Row = Template.bind({}); +Row.args = { + justify: "flex-start", + children: ( + <> + + One + Two + + + Three + Four + + + ), +}; + diff --git a/components/Flex/index.tsx b/components/Flex/index.tsx new file mode 100644 index 0000000..7444714 --- /dev/null +++ b/components/Flex/index.tsx @@ -0,0 +1,47 @@ +import React, {CSSProperties, forwardRef} from "react" + +type BaseProps = React.HTMLAttributes + +type FlexProps = BaseProps & { + dir?: "column" | "row", + wrap?: boolean, + justify?: CSSProperties["justifyContent"], + align?: CSSProperties["alignItems"], + justifySelf?: CSSProperties["justifySelf"], + alignSelf?: CSSProperties["alignSelf"], +} + +export const Flex = forwardRef( + function Flex( + { + dir = "column", + justify = "flex-start", + align = "center", + wrap, + justifySelf, + alignSelf, + style, + ...props + }, + ref, + ) { + const styles: CSSProperties = { + ...style, + display: "flex", + flexFlow: dir + " " + (wrap ? "wrap" : "nowrap"), + justifyContent: justify, + alignItems: align, + justifySelf, + alignSelf, + } + + return
+ }, +) + +type PropsWithoutDir = Omit + +// @ts-ignore +export const FlexColumn = (props: PropsWithoutDir) => +// @ts-ignore +export const FlexRow = (props: PropsWithoutDir) => diff --git a/components/Flex/tests/FlexColumn.spec.js b/components/Flex/tests/FlexColumn.spec.js new file mode 100644 index 0000000..5b682b0 --- /dev/null +++ b/components/Flex/tests/FlexColumn.spec.js @@ -0,0 +1,26 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import { FlexColumn } from "../index"; + +afterEach(cleanup); + +describe("FlexColumn", () => { + const Comp = ( + + Go to Posts + Go to Posts + + ); + + it("should take a snapshot", () => { + const { asFragment } = render(Comp) + + expect(asFragment(Comp)).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render(Comp); + + expect(queryByLabelText("FlexColumn")).toBeTruthy(); + }); +}); diff --git a/components/Flex/tests/FlexColumn.stories.tsx b/components/Flex/tests/FlexColumn.stories.tsx new file mode 100644 index 0000000..5c8e07c --- /dev/null +++ b/components/Flex/tests/FlexColumn.stories.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; + +import { FlexColumn, FlexRow } from "../index"; + +export default { + title: "Components/FlexColumn", + component: FlexColumn, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen", + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const Column = Template.bind({}); +Column.args = { + align: "flex-start", + style: { flex: 1 }, + children: ( + <> + Go to Posts + Go to Posts + + ), +}; diff --git a/components/Flex/tests/FlexRow.spec.js b/components/Flex/tests/FlexRow.spec.js new file mode 100644 index 0000000..0ab9743 --- /dev/null +++ b/components/Flex/tests/FlexRow.spec.js @@ -0,0 +1,32 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import { FlexRow, FlexColumn } from "../index"; + +afterEach(cleanup); + +describe("FlexRow", () => { + const Comp = ( + + + One + Two + + + Three + Four + + + ); + + it("should take a snapshot", () => { + const { asFragment } = render(Comp); + + expect(asFragment(Comp)).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render(Comp); + + expect(queryByLabelText("FlexRow")).toBeTruthy(); + }); +}); diff --git a/components/Flex/tests/__snapshots__/FlexColumn.spec.js.snap b/components/Flex/tests/__snapshots__/FlexColumn.spec.js.snap new file mode 100644 index 0000000..9958257 --- /dev/null +++ b/components/Flex/tests/__snapshots__/FlexColumn.spec.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FlexColumn should take a snapshot 1`] = ` + + + +`; diff --git a/components/Flex/tests/__snapshots__/FlexRow.spec.js.snap b/components/Flex/tests/__snapshots__/FlexRow.spec.js.snap new file mode 100644 index 0000000..8125825 --- /dev/null +++ b/components/Flex/tests/__snapshots__/FlexRow.spec.js.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FlexRow should take a snapshot 1`] = ` + + + +`; diff --git a/components/HelpTooltip/HelpTooltip.pcss b/components/HelpTooltip/HelpTooltip.pcss new file mode 100644 index 0000000..74d44c3 --- /dev/null +++ b/components/HelpTooltip/HelpTooltip.pcss @@ -0,0 +1,60 @@ +.root { + display: inline-flex; + flex-direction: column; + justify-content: center; +} + +.tooltip { + composes: z-high from "../../styles/layout.pcss"; +} + +.tooltip-container { + text-align: left; + padding-top: 7px; + padding-bottom: 7px; +} + +.tooltip-content { + & p { + margin: 0 0 5px; + + &:last-child { + margin-bottom: 0; + } + } + + & img { + max-width: 100%; + margin: 5px 0; + border-radius: 2px; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + } +} + +.icon { + height: 18px; + line-height: 18px; + + & :global .dashicons { + font-size: 16px; + width: 16px; + height: 16px; + line-height: 16px; + vertical-align: bottom; + } +} + +.icon-visible { + opacity: 1; +} + +.icon-notvisible { + opcaity: 0.7; +} \ No newline at end of file diff --git a/components/HelpTooltip/index.tsx b/components/HelpTooltip/index.tsx new file mode 100644 index 0000000..653f25d --- /dev/null +++ b/components/HelpTooltip/index.tsx @@ -0,0 +1,45 @@ +import React, {ReactNode} from "react"; +import css from "./HelpTooltip.pcss"; +import {Dashicon} from "../Dashicon"; +import Tooltip from "../Tooltip"; +import { classList } from "../../utils/classes"; + +type Props = { + maxWidth?: number; + children: ReactNode; +} + +export default function HelpTooltip({maxWidth, children}: Props) { + maxWidth = maxWidth ?? 300; + + const [isTooltipVisible, setIsTooltipVisible] = React.useState(false); + + const handleMouseOver = () => setIsTooltipVisible(true); + const handleMouseOut = () => setIsTooltipVisible(false); + + const tooltipTheme = { + content: css.tooltipContent, + container: css.tooltipContainer, + }; + + const all = classList(css.icon, isTooltipVisible ? css.iconVisible : css.iconNotvisible) + + return ( +
+ + { + ({ref}) => ( + + + + ) + } + +
{children}
+
+
+ ); +}; diff --git a/components/HelpTooltip/tests/HelpTooltip.spec.js b/components/HelpTooltip/tests/HelpTooltip.spec.js new file mode 100644 index 0000000..c7d081d --- /dev/null +++ b/components/HelpTooltip/tests/HelpTooltip.spec.js @@ -0,0 +1,25 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import HelpTooltip from "../index"; + +afterEach(cleanup); + +describe("HelpTooltip", () => { + const Comp = ( + + One, Two. Three + + ); + + it("should take a snapshot", () => { + const { asFragment } = render(Comp); + + expect(asFragment(Comp)).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render(Comp); + + expect(queryByLabelText("HelpTooltip")).toBeTruthy(); + }); +}); diff --git a/components/HelpTooltip/tests/HelpTooltip.stories.tsx b/components/HelpTooltip/tests/HelpTooltip.stories.tsx new file mode 100644 index 0000000..9847c6f --- /dev/null +++ b/components/HelpTooltip/tests/HelpTooltip.stories.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; + +import HelpTooltip from "../index"; + +export default { + title: "Components/HelpTooltip", + component: HelpTooltip, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen", + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const Help = Template.bind({}); +Help.args = { + maxWidth: 200, + children: <>One, Two. Three +}; + diff --git a/components/HelpTooltip/tests/__snapshots__/HelpTooltip.spec.js.snap b/components/HelpTooltip/tests/__snapshots__/HelpTooltip.spec.js.snap new file mode 100644 index 0000000..fff2442 --- /dev/null +++ b/components/HelpTooltip/tests/__snapshots__/HelpTooltip.spec.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HelpTooltip should take a snapshot 1`] = ` + +
+ + + +
+
+`; diff --git a/components/Icon/index.tsx b/components/Icon/index.tsx new file mode 100644 index 0000000..d77991c --- /dev/null +++ b/components/Icon/index.tsx @@ -0,0 +1,21 @@ +import React, {forwardRef, ReactNode} from "react" + +type BaseProps = Omit, "children">; + +type Props = BaseProps & { + icon: ReactNode; + width?: string; + height?: string; + viewBox?: string; + fill?: string; + style?: object; + std?: boolean; +}; + +export const Icon = forwardRef( + ({ icon, std, viewBox, ...props }, ref) => ( + + {icon} + + ) +); diff --git a/components/Icon/tests/__snapshots__/Icon.spec.js b/components/Icon/tests/__snapshots__/Icon.spec.js new file mode 100644 index 0000000..8cfa816 --- /dev/null +++ b/components/Icon/tests/__snapshots__/Icon.spec.js @@ -0,0 +1,23 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import { Icon } from "../../index"; + +afterEach(cleanup); + +describe("Icon", () => { + const Comp = ( + + ); + + it("should take a snapshot", () => { + const { asFragment } = render(Comp); + + expect(asFragment()).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render(Comp); + + expect(queryByLabelText("Icon")).toBeTruthy(); + }); +}); diff --git a/components/Icon/tests/__snapshots__/Icon.stories.tsx b/components/Icon/tests/__snapshots__/Icon.stories.tsx new file mode 100644 index 0000000..4442b8f --- /dev/null +++ b/components/Icon/tests/__snapshots__/Icon.stories.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import { Icon } from '../../index'; + +export default { + title: "Components/Icon", + component: Icon, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: 'fullscreen', + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ; + +export const Intial = Template.bind({}); +Intial.args = {}; \ No newline at end of file diff --git a/components/Icon/tests/__snapshots__/__snapshots__/Icon.spec.js.snap b/components/Icon/tests/__snapshots__/__snapshots__/Icon.spec.js.snap new file mode 100644 index 0000000..4466661 --- /dev/null +++ b/components/Icon/tests/__snapshots__/__snapshots__/Icon.spec.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Icon should take a snapshot 1`] = ` + + + + + +`; diff --git a/components/ListField/index.tsx b/components/ListField/index.tsx new file mode 100644 index 0000000..a4e7162 --- /dev/null +++ b/components/ListField/index.tsx @@ -0,0 +1,129 @@ +import React, {KeyboardEvent, ReactElement, useEffect} from "react"; +import CreatableSelect from "react-select"; +import {Alert} from "../Alert"; +import {SelectStyles} from "../Select"; + +const components = { + DropdownIndicator: null, +}; + +const createOption = (label: string) => ({ + label, + value: label, +}); + +export type Props = { + id?: string; + value: Array; + onChange: (value: Array) => void; + sanitize?: (value: string) => string; + autoFocus?: boolean; + message?: string | ReactElement; +} + +export function ListField({id, value, onChange, sanitize, autoFocus, message}: Props) { + const [inputValue, setInputValue] = React.useState(""); + const [duplicate, setDuplicate] = React.useState(-1); + const [currMessage, setCurrMessage] = React.useState(); + + useEffect(() => { + setCurrMessage(message); + }, [message]); + + value = Array.isArray(value) ? value : []; + const values = value.map((v) => createOption(v)); + + const addValue = () => { + if (inputValue.length) { + setInputValue(""); + handleChange([...values, createOption(inputValue)]); + } + }; + + const handleChange = (value: any) => { + if (!onChange) return; + + let dupeIdx = -1; + + if (!value) { + value = []; + } else { + value = value + .map((opt: { value: string; }) => (opt && sanitize) ? sanitize(opt.value) : opt.value) + .filter((val: any, idx: any, list: string | any[]) => { + const firstIndex = list.indexOf(val); + + if (firstIndex !== idx) { + dupeIdx = firstIndex; + + return false; + } + + return !!val; + }); + } + + setDuplicate(dupeIdx); + + if (dupeIdx === -1) { + onChange(value); + } + }; + + const handleInputChange = (inputValue: string) => { + setInputValue(inputValue); + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (!inputValue) return; + + switch (event.key) { + case ",": + case "Enter": + case "Tab": + addValue(); + event.preventDefault(); + break; + } + }; + + const styles = SelectStyles(); + + return ( + <> + + { + (duplicate < 0 || values.length === 0) ? null : ( + + {values[duplicate].label} is already in the list + + ) + } + { + (!currMessage) ? null : ( + + {currMessage} + + ) + } + + ); +} diff --git a/components/ListField/tests/ListField.spec.js b/components/ListField/tests/ListField.spec.js new file mode 100644 index 0000000..f6fd2e7 --- /dev/null +++ b/components/ListField/tests/ListField.spec.js @@ -0,0 +1,23 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import {ListField} from "../index"; + +afterEach(cleanup); + +describe("ListField", () => { + const Comp = ( + + ); + + it("should take a snapshot", () => { + const { asFragment } = render(Comp); + + expect(asFragment(Comp)).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render(Comp); + + expect(queryByLabelText("ListField")).toBeTruthy(); + }); +}); diff --git a/components/ListField/tests/ListField.stories.tsx b/components/ListField/tests/ListField.stories.tsx new file mode 100644 index 0000000..1da0b11 --- /dev/null +++ b/components/ListField/tests/ListField.stories.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; + +import {ListField} from "../index"; + +export default { + title: "Components/ListField", + component: ListField, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen", + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const Empty = Template.bind({}); +Empty.args = {}; + + +export const Initial = Template.bind({}); +Initial.args = { + value: ["ampo", "shoe"], + autoFocus: true +}; + diff --git a/components/ListField/tests/__snapshots__/ListField.spec.js.snap b/components/ListField/tests/__snapshots__/ListField.spec.js.snap new file mode 100644 index 0000000..641a12d --- /dev/null +++ b/components/ListField/tests/__snapshots__/ListField.spec.js.snap @@ -0,0 +1,102 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ListField should take a snapshot 1`] = ` + +
+ +
+
+
+
+ Hakonan +
+
+ +
+
+
+
+ shoe +
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+ +`; diff --git a/components/Masonry/Masonry.pcss b/components/Masonry/Masonry.pcss new file mode 100644 index 0000000..57357bb --- /dev/null +++ b/components/Masonry/Masonry.pcss @@ -0,0 +1,28 @@ +.root { + display: grid; + justify-content: flex-start; + align-items: stretch; +} + +.column { + flex: 1; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + + &:last-of-type { + margin-right: 0 !important; + } +} + +.cell { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + + &:last-of-type { + margin-bottom: 0 !important; + } +} diff --git a/components/Masonry/index.tsx b/components/Masonry/index.tsx new file mode 100644 index 0000000..5991454 --- /dev/null +++ b/components/Masonry/index.tsx @@ -0,0 +1,36 @@ +import React, {CSSProperties} from "react"; +import css from "./Masonry.pcss"; +import {splitArray} from "../../utils/splitArray" + +export type Props = { + columns: number; + gap?: string | number; + children: any; +} + +export function Masonry({columns, gap, children}: Props) { + gap = gap ?? 0; + + const style: CSSProperties = { + gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`, + columnGap: gap, + }; + + const cellCss: CSSProperties = { + marginBottom: gap, + }; + + return ( +
+ {splitArray(children, columns).map((column, colIdx) => ( +
+ {column.map((child: any, rowIdx) => ( +
+ {child} +
+ ))} +
+ ))} +
+ ); +} diff --git a/components/Masonry/tests/__snapshots__/Masonry.spec.js b/components/Masonry/tests/__snapshots__/Masonry.spec.js new file mode 100644 index 0000000..db5a290 --- /dev/null +++ b/components/Masonry/tests/__snapshots__/Masonry.spec.js @@ -0,0 +1,26 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import { Masonry } from "../../index"; + +afterEach(cleanup); + +describe("Skeleton", () => { + const Comp = ( + +

One

+

Two

+
+ ); + + it("should take a snapshot", () => { + const { asFragment } = render(Comp); + + expect(asFragment(Comp)).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render(Comp); + + expect(queryByLabelText("Masonry")).toBeTruthy(); + }); +}); diff --git a/components/Masonry/tests/__snapshots__/Masonry.stories.tsx b/components/Masonry/tests/__snapshots__/Masonry.stories.tsx new file mode 100644 index 0000000..b268e74 --- /dev/null +++ b/components/Masonry/tests/__snapshots__/Masonry.stories.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import { Masonry } from '../../index'; + +export default { + title: "Components/Masonry", + component: Masonry, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: 'fullscreen', + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ; + +export const Intial = Template.bind({}); +Intial.args = { + children: [

One

,

Two

], + columns:2 +}; \ No newline at end of file diff --git a/components/Masonry/tests/__snapshots__/__snapshots__/Masonry.spec.js.snap b/components/Masonry/tests/__snapshots__/__snapshots__/Masonry.spec.js.snap new file mode 100644 index 0000000..4496116 --- /dev/null +++ b/components/Masonry/tests/__snapshots__/__snapshots__/Masonry.spec.js.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Skeleton should take a snapshot 1`] = ` + +
+
+
+

+ One +

+
+
+
+
+

+ Two +

+
+
+
+
+`; diff --git a/components/OrSeparator/OrSeperator.pcss b/components/OrSeparator/OrSeperator.pcss new file mode 100644 index 0000000..ca69da4 --- /dev/null +++ b/components/OrSeparator/OrSeperator.pcss @@ -0,0 +1,14 @@ +.container { + margin: 25px 0; +} + +.line { + flex: 1; + height: 1px; + background: #d3d8dc; +} + +.text { + margin: 0 12px; + font-size: 16px; +} diff --git a/components/OrSeparator/index.tsx b/components/OrSeparator/index.tsx new file mode 100644 index 0000000..8ab2e91 --- /dev/null +++ b/components/OrSeparator/index.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import css from "./OrSeperator.pcss"; +import { FlexRow } from "../Flex"; + +export function OrSeparator() { + return ( + +
+ OR +
+ + ); +} diff --git a/components/OrSeparator/tests/OrSeparator.spec.js b/components/OrSeparator/tests/OrSeparator.spec.js new file mode 100644 index 0000000..2a4714b --- /dev/null +++ b/components/OrSeparator/tests/OrSeparator.spec.js @@ -0,0 +1,23 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import {OrSeparator} from "../index"; + +afterEach(cleanup); + +describe("OrSeparator", () => { + const Comp = ( + + ); + + it("should take a snapshot", () => { + const { asFragment } = render(Comp); + + expect(asFragment(Comp)).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render(Comp); + + expect(queryByLabelText("OrSeparator")).toBeTruthy(); + }); +}); diff --git a/components/OrSeparator/tests/OrSeparator.stories.tsx b/components/OrSeparator/tests/OrSeparator.stories.tsx new file mode 100644 index 0000000..2fedcc0 --- /dev/null +++ b/components/OrSeparator/tests/OrSeparator.stories.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; + +import {OrSeparator} from "../index"; + +export default { + title: "Components/OrSeparator", + component: OrSeparator, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen", + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const Initial = Template.bind({}); +Initial.args = {}; + diff --git a/components/OrSeparator/tests/__snapshots__/OrSeparator.spec.js.snap b/components/OrSeparator/tests/__snapshots__/OrSeparator.spec.js.snap new file mode 100644 index 0000000..a183b1a --- /dev/null +++ b/components/OrSeparator/tests/__snapshots__/OrSeparator.spec.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OrSeparator should take a snapshot 1`] = ` + +
+
+ + OR + +
+
+ +`; diff --git a/components/RadioGroup/RadioGroup.pcss b/components/RadioGroup/RadioGroup.pcss new file mode 100644 index 0000000..1a53abc --- /dev/null +++ b/components/RadioGroup/RadioGroup.pcss @@ -0,0 +1,15 @@ +.radio-group { + display: flex; + flex-direction: column; +} + +.disabled { + composes: radio-group; + composes: disabled from "../../styles/theme.pcss"; +} + +.option { + display: flex; + flex-direction: row; + margin: 5px 0; +} diff --git a/components/RadioGroup/index.tsx b/components/RadioGroup/index.tsx new file mode 100644 index 0000000..6b3dc0d --- /dev/null +++ b/components/RadioGroup/index.tsx @@ -0,0 +1,44 @@ +import React, {ChangeEvent} from "react"; +// import "../../styles/fields.pcss"; +import css from "./RadioGroup.pcss"; + +export type RadioOption = { + value: string | number, + label: string +} + +type Props = { + name?: string; + className?: string; + value: string | number; + onChange?: (value: any) => void; + options: Array; + disabled?: boolean; +} + +export function RadioGroup({name, className, disabled, value, onChange, options}: Props) { + const handleChange = (e: ChangeEvent) => { + (!disabled && e.target.checked && onChange) && onChange(e.target.value); + }; + + className = (disabled ? css.disabled : css.radioGroup) + " " + (className ?? ""); + + return ( +
+ { + options.map((option, idx) => ( + + )) + } +
+ ); +} diff --git a/components/RadioGroup/tests/RadioGroup.spec.js b/components/RadioGroup/tests/RadioGroup.spec.js new file mode 100644 index 0000000..63f8fdd --- /dev/null +++ b/components/RadioGroup/tests/RadioGroup.spec.js @@ -0,0 +1,32 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import { RadioGroup } from "../index"; + +afterEach(cleanup); + +describe("RadioGroup", () => { + const Comp = ( + + ); + + it("should take a snapshot", () => { + const { asFragment } = render(Comp); + + expect(asFragment(Comp)).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render(Comp); + + expect(queryByLabelText("RadioGroup")).toBeTruthy(); + }); +}); diff --git a/components/RadioGroup/tests/RadioGroup.stories.tsx b/components/RadioGroup/tests/RadioGroup.stories.tsx new file mode 100644 index 0000000..9b689ab --- /dev/null +++ b/components/RadioGroup/tests/RadioGroup.stories.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; + +import { RadioGroup } from "../index"; + +export default { + title: "Components/RadioGroup", + component: RadioGroup, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen", + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const Initial = Template.bind({}); +Initial.args = { + name: "multiChoice", + value: "multipleDaily", + options: [ + { + value: "multipleDaily", + label: "Multiple times a day.", + }, + { + value: "onceDaily", + label: "At least once a day.", + }, + { + value: "fewTimesWeekly", + label: "A few times a week.", + }, + { + value: "onceWeekly", + label: "Once a week or less often.", + }, + ], +}; + + +export const Disabled = Template.bind({}); +Disabled.args = { + disabled: true, + name: "multiChoice", + value: "multipleDaily", + options: [ + { + value: "multipleDaily", + label: "Multiple times a day.", + }, + { + value: "onceDaily", + label: "At least once a day.", + }, + { + value: "fewTimesWeekly", + label: "A few times a week.", + }, + { + value: "onceWeekly", + label: "Once a week or less often.", + }, + ], +}; \ No newline at end of file diff --git a/components/RadioGroup/tests/__snapshots__/RadioGroup.spec.js.snap b/components/RadioGroup/tests/__snapshots__/RadioGroup.spec.js.snap new file mode 100644 index 0000000..67d2f1d --- /dev/null +++ b/components/RadioGroup/tests/__snapshots__/RadioGroup.spec.js.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RadioGroup should take a snapshot 1`] = ` + +
+ +
+
+`; diff --git a/components/Select/Select.pcss b/components/Select/Select.pcss new file mode 100644 index 0000000..bccd9e7 --- /dev/null +++ b/components/Select/Select.pcss @@ -0,0 +1,11 @@ +$wp-blue: #007cba; +$primary-color: $wp-blue; +$washed-color: mix(desaturate($primary-color, 50%), #fff, 10%); +$shadow-color: rgba(20, 25, 60, 0.32); + +:export { + primaryColor: $primary-color; + shadowColor: $shadow-color; + washedColor: $washed-color; + } + \ No newline at end of file diff --git a/components/Select/index.tsx b/components/Select/index.tsx new file mode 100644 index 0000000..8264245 --- /dev/null +++ b/components/Select/index.tsx @@ -0,0 +1,135 @@ +import React, {ForwardedRef, MutableRefObject, ReactElement} from "react"; +import ReactSelect from "react-select"; +import CreatableSelect from "react-select/creatable"; +import AsyncSelect from "react-select/async"; +import {classList} from "../../utils/classes"; +import "../../styles/fields.pcss"; +import css from "./Select.pcss"; + +export type SelectOption = { + value: any; + label: string | ReactElement; +} + +export declare type SelectChangeHandler = (option: SelectOption) => void; + +type Props = { + id?: string; + className?: string; + value?: string; + placeholder?: string; + onChange?: SelectChangeHandler; + options?: Array; + isSearchable?: boolean; + isMulti?: boolean; + isClearable?: boolean; + isCreatable?: boolean; + width?: string | number; + menuIsOpen?: boolean; + isValidNewOption?: (value: string) => boolean; + + [k: string]: any; +} + +export const SelectStyles = (props: Record = {}) => ({ + option: (prev: any, state: any) => ({ + ...prev, + cursor: "pointer", + lineHeight: "24px", + }), + menu: (prev: any, state: any) => ({ + ...prev, + margin: "6px 0", + boxShadow: "0 2px 8px " + css.shadowColor, + overflow: "hidden", + }), + menuList: (prev: any, state: any) => ({ + padding: "0px", + }), + control: (prev: any, state: { isFocused: any; }) => { + let style = { + ...prev, + cursor: "pointer", + lineHeight: "2", + minHeight: "40px", + }; + + if (state.isFocused) { + style.borderColor = css.primaryColor; + style.boxShadow = `0 0 0 1px ${css.primaryColor}`; + } + + return style; + }, + valueContainer: (prev: any, state: any) => ({ + ...prev, + paddingTop: 0, + paddingBottom: 0, + paddingRight: 0, + }), + container: (prev: any, state: any) => ({ + ...prev, + width: props.width || "100%", + }), + multiValue: (prev: any, state: any) => ({ + ...prev, + padding: "0 6px", + }), + input: (prev: any, state: any) => ({ + ...prev, + outline: "0 transparent !important", + border: "0 transparent !important", + boxShadow: "0 0 0 transparent !important", + }), + indicatorSeparator: (prev: any, state: any) => ({ + ...prev, + margin: "0", + backgroundColor: "transparent", + }), + menuPortal: (base: any) => ({ + ...base, + zIndex: 9999999 + }) +}); + +export const Select = React.forwardRef((props: Props, ref: ForwardedRef) => { + const options = props.options ?? []; + const value = options.find((opt) => opt.value === props.value); + + props = { + ...props, + id: undefined, + className: classList("react-select", props.className as string), + classNamePrefix: "react-select", + inputId: props.id, + menuPosition: "absolute", + }; + + const styles = SelectStyles(props); + + const theme = (theme: { colors: any; }) => ({ + ...theme, + borderRadius: 3, + colors: { + ...theme.colors, + primary: css.primaryColor, + primary25: css.washedColor, + }, + }); + + const Component = props.isCreatable ? CreatableSelect : props.async ? AsyncSelect : ReactSelect; + + return ( + + ); +}); diff --git a/components/Select/tests/Select.spec.js b/components/Select/tests/Select.spec.js new file mode 100644 index 0000000..dee9a6f --- /dev/null +++ b/components/Select/tests/Select.spec.js @@ -0,0 +1,35 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import { Select } from "../index"; + +afterEach(cleanup); + +describe("Select", () => { + const Comp = ( + +); + +export const Initial = Template.bind({}); +Initial.args = { + name: "multiChoice", + value: "multipleDaily" +}; + +export const Options = Template.bind({}); +Options.args = { + name: "country", + options: [ + { + value: "Malta", + label: "Malta", + }, + { + value: "France", + label: "France", + }, + { + value: "Ghana", + label: "Ghana", + }, + ], +}; + diff --git a/components/Select/tests/__snapshots__/Select.spec.js.snap b/components/Select/tests/__snapshots__/Select.spec.js.snap new file mode 100644 index 0000000..ec7cfde --- /dev/null +++ b/components/Select/tests/__snapshots__/Select.spec.js.snap @@ -0,0 +1,83 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Select should take a snapshot 1`] = ` + +
+ +
+
+
+ Select... +
+
+
+ +
+
+
+
+
+ + +
+
+ +
+ +`; diff --git a/components/Skeleton/SkeletonMedia.pcss b/components/Skeleton/SkeletonMedia.pcss new file mode 100644 index 0000000..8806204 --- /dev/null +++ b/components/Skeleton/SkeletonMedia.pcss @@ -0,0 +1,35 @@ +.loading { + position: relative; + padding-bottom: 100%; + border-radius: 2px; + background-color: var(--sli-grey); + animation-name: flashing; + animation-duration: 900ms; + animation-timing-function: linear; + animation-iteration-count: infinite; + + &::before, &::after { + content: ' '; + position: absolute; + inset: 0; + } + + &::before { + background: #fff; + z-index: 1; + } + + &::after { + background: #e5e5e5; + z-index: 2; + } +} + +@keyframes flashing { + from, to { + opacity: .4; + } + 50% { + opacity: 1; + } +} diff --git a/components/Skeleton/index.tsx b/components/Skeleton/index.tsx new file mode 100644 index 0000000..6c53984 --- /dev/null +++ b/components/Skeleton/index.tsx @@ -0,0 +1,11 @@ +import React, {forwardRef} from "react"; +import { classList } from "../../utils/classes"; +import css from "./SkeletonMedia.pcss"; + +export type Props = React.ComponentPropsWithoutRef<"div"> + +export const Skeleton = forwardRef( + function Skeleton({className, ...props}, ref) { + return
+ }, +) diff --git a/components/Skeleton/tests/__snapshots__/Skeleton.spec.js b/components/Skeleton/tests/__snapshots__/Skeleton.spec.js new file mode 100644 index 0000000..4892759 --- /dev/null +++ b/components/Skeleton/tests/__snapshots__/Skeleton.spec.js @@ -0,0 +1,26 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import { Skeleton } from "../../index"; + +afterEach(cleanup); + +describe("Skeleton", () => { + const Comp = ( + +

One

+

Two

+
+ ); + + it("should take a snapshot", () => { + const { asFragment } = render(Comp); + + expect(asFragment(Comp)).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render(Comp); + + expect(queryByLabelText("Skeleton")).toBeTruthy(); + }); +}); diff --git a/components/Skeleton/tests/__snapshots__/Skeleton.stories.tsx b/components/Skeleton/tests/__snapshots__/Skeleton.stories.tsx new file mode 100644 index 0000000..a88c95c --- /dev/null +++ b/components/Skeleton/tests/__snapshots__/Skeleton.stories.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import { Skeleton } from '../../index'; + +export default { + title: "Components/Skeleton", + component: Skeleton, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: 'fullscreen', + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ; + +export const Intial = Template.bind({}); +Intial.args = {}; \ No newline at end of file diff --git a/components/Skeleton/tests/__snapshots__/__snapshots__/Skeleton.spec.js.snap b/components/Skeleton/tests/__snapshots__/__snapshots__/Skeleton.spec.js.snap new file mode 100644 index 0000000..cc4a222 --- /dev/null +++ b/components/Skeleton/tests/__snapshots__/__snapshots__/Skeleton.spec.js.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Skeleton should take a snapshot 1`] = ` + +
+

+ One +

+

+ Two +

+
+
+`; diff --git a/components/Square/Square.pcss b/components/Square/Square.pcss new file mode 100644 index 0000000..4aa9f14 --- /dev/null +++ b/components/Square/Square.pcss @@ -0,0 +1,18 @@ +.filler { + position: relative; + padding-bottom: 100%; + box-sizing: border-box; +} + +.positioner { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + z-index: 0; + + & > *:first-child { + inset: 0; + flex: 1; + } +} diff --git a/components/Square/index.tsx b/components/Square/index.tsx new file mode 100644 index 0000000..bb53c60 --- /dev/null +++ b/components/Square/index.tsx @@ -0,0 +1,15 @@ +import React, {HTMLAttributes} from "react"; +import css from "./Square.pcss"; +import {classList} from "../../utils/classes"; + +export type Props = HTMLAttributes & {}; + +export function Square({className, children, ...props}: Props) { + return ( +
+
+ {children} +
+
+ ); +} diff --git a/components/Square/tests/Square.spec.js b/components/Square/tests/Square.spec.js new file mode 100644 index 0000000..19b1599 --- /dev/null +++ b/components/Square/tests/Square.spec.js @@ -0,0 +1,31 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import { Square } from "../index"; + +afterEach(cleanup); + +describe("Square", () => { + const Comp = ( + +
+

+ Lorem Ipsum is simply dummy text of the printing and typesetting + industry. Lorem Ipsum has been the industry's standard dummy text ever + since the 1500s +

+
+
+ ); + + it("should take a snapshot", () => { + const { asFragment } = render(Comp); + + expect(asFragment(Comp)).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render(Comp); + + expect(queryByLabelText("Square")).toBeTruthy(); + }); +}); diff --git a/components/Square/tests/Square.stories.tsx b/components/Square/tests/Square.stories.tsx new file mode 100644 index 0000000..e86176f --- /dev/null +++ b/components/Square/tests/Square.stories.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; + +import { Square } from "../index"; + +export default { + title: "Components/Square", + component: Square, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen", + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const Initial = Template.bind({}); +Initial.args = { + children: ( +
+

Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s

+
+ ), + style: { + textAlign: "center", + padding: "20px" + } +}; diff --git a/components/Square/tests/__snapshots__/Square.spec.js.snap b/components/Square/tests/__snapshots__/Square.spec.js.snap new file mode 100644 index 0000000..8980c29 --- /dev/null +++ b/components/Square/tests/__snapshots__/Square.spec.js.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Square should take a snapshot 1`] = ` + +
+
+
+

+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s +

+
+
+
+
+`; diff --git a/components/Table/Table.pcss b/components/Table/Table.pcss new file mode 100644 index 0000000..76ca58e --- /dev/null +++ b/components/Table/Table.pcss @@ -0,0 +1,48 @@ +.table { + composes: subtle-drop-shadow slightly-rounded from '../../styles/theme.pcss'; + width: 100%; + margin-bottom: 20px; + background: #fff; + box-sizing: border-box; + border-collapse: collapse; + overflow: hidden; +} + +.header { + border-bottom: 1px solid var(--sli-line-color); +} + +.footer { + border-top: 1px solid var(--sli-line-color); +} + +.cell { + padding: 10px 14px; + box-sizing: border-box; +} + +.col-heading { + composes: cell; + font-size: 14px; + font-weight: normal; +} + +.row { + background: #fff; +} + +.row:nth-child(odd) { + background: var(--sli-wp-light-grey); +} + +.align-left { + text-align: left; +} + +.align-right { + text-align: right; +} + +.align-center { + text-align: center; +} diff --git a/components/Table/index.tsx b/components/Table/index.tsx new file mode 100644 index 0000000..6984454 --- /dev/null +++ b/components/Table/index.tsx @@ -0,0 +1,105 @@ +import React, {ReactNode} from "react"; +import { classList } from "../../utils/classes"; +import "../../styles/colors.pcss"; +import css from "./Table.pcss"; + +interface Props { + className?: string; + cols: Array>; + rows: Array; + footerCols?: boolean; + styleMap?: TableStyleMap; +} + +export interface Column { + id: string; + label: string; + align?: "left" | "right" | "center"; + render: (row: T, idx: number) => ReactNode; +} + +export interface TableStyleMap { + cols: { [k: string]: string }; + cells: { [k: string]: string }; +} + +export default function Table({className, cols, rows, footerCols, styleMap}: Props) { + styleMap = styleMap ?? {cols: {}, cells: {}}; + + return ( + + + + + + + {rows.map((row, idx) => )} + + + {footerCols && ( + + + + )} +
+ ); +} + +interface RowProps { + idx: number; + row: T; + cols: Array>; + styleMap: TableStyleMap; +} + +function Row({idx, row, cols, styleMap}: RowProps) { + return ( + + {cols.map(col => { + return ( + + {col.render(row, idx)} + + ); + })} + + ); +} + +interface ColHeadersProps { + cols: Array>; + styleMap: TableStyleMap; +} + +function ColHeaders({cols, styleMap}: ColHeadersProps) { + return ( + + {cols.map(col => { + const className = classList(css.colHeading, alignClass(col), styleMap.cols[col.id]); + + return ( + + {col.label} + + ); + })} + + ); +} + +/** + * Determines the correct class to use for alignment. + * + * @param col The column config. + */ +function alignClass(col: Column) { + if (col.align === "center") { + return css.alignCenter; + } + + if (col.align === "right") { + return css.alignRight; + } + + return css.alignLeft; +} diff --git a/components/Table/tests/__snapshots__/Table.spec.js b/components/Table/tests/__snapshots__/Table.spec.js new file mode 100644 index 0000000..112d506 --- /dev/null +++ b/components/Table/tests/__snapshots__/Table.spec.js @@ -0,0 +1,38 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import Table from "../../index"; + +afterEach(cleanup); + +describe("Table", () => { + const Comp = ( +

col

, + }, + ]} + rows={[ + { + id: "name", + label: "Name", + render: (feed) =>

rows

, + }, + ]} + /> + ); + + it("should take a snapshot", () => { + const { asFragment } = render(Comp); + + expect(asFragment(Comp)).toMatchSnapshot(); + }); + + it("renders correctly", () => { + const { queryByLabelText } = render(Comp); + + expect(queryByLabelText("Table")).toBeTruthy(); + }); +}); diff --git a/components/Table/tests/__snapshots__/Table.stories.tsx b/components/Table/tests/__snapshots__/Table.stories.tsx new file mode 100644 index 0000000..d9db4a5 --- /dev/null +++ b/components/Table/tests/__snapshots__/Table.stories.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import Table from '../../index'; + +export default { + title: "Components/Table", + component: Table, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: 'fullscreen', + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) =>
; + +export const Intial = Template.bind({}); +Intial.args = { + cols: [ + { + id: "name", + label: "Name", + render: () =>

col

, + }, + ], + rows: [ + { + id: "name", + label: "Name", + render: () =>

rows

, + }] +}; \ No newline at end of file diff --git a/components/Table/tests/__snapshots__/__snapshots__/Table.spec.js.snap b/components/Table/tests/__snapshots__/__snapshots__/Table.spec.js.snap new file mode 100644 index 0000000..690ecb8 --- /dev/null +++ b/components/Table/tests/__snapshots__/__snapshots__/Table.spec.js.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table should take a snapshot 1`] = ` + +
+ + + + + + + + + + +
+ Name +
+

+ col +

+
+ +`; diff --git a/styles/.gitkeep b/components/TextArea/TextArea.pcss similarity index 100% rename from styles/.gitkeep rename to components/TextArea/TextArea.pcss diff --git a/components/TextArea/index.tsx b/components/TextArea/index.tsx new file mode 100644 index 0000000..bf43490 --- /dev/null +++ b/components/TextArea/index.tsx @@ -0,0 +1,21 @@ +import React from "react"; +// import "../../styles/fields.pcss"; +import "./TextArea.pcss" + +type Props = { + id?: string; + value: string; + onChange: (value: string) => void; +} + +export function TextArea({id, value, onChange}: Props) { + return ( +