UNPKG

@dash-ui/styles

Version:

A tiny, powerful, framework-agnostic CSS-in-JS library.

1,008 lines (884 loc) 27.3 kB
import crc from "crc"; import { createDash, createStyles, pathToToken, styles } from "./index"; afterEach(() => { styles.dash.sheet.flush(); document.getElementsByTagName("html")[0].innerHTML = ""; }); function cssRules( element: any = document.querySelectorAll(`style[data-dash]`)[0] ): CSSStyleRule[] { return element.sheet.cssRules; } describe("createStyles()", () => { it("turns off vendor prefixing", () => { const myStyles = createStyles({ dash: createDash({ prefix: false }) }); const style = myStyles.variants({ flex: { display: "flex" }, }); style("flex"); expect( cssRules(document.querySelectorAll("style[data-dash]")[1])[0].cssText ).toMatch(/\{display: flex;\}/); }); it("configures hash algorithm", () => { const customHash = (string: string): string => crc.crc32(string).toString(16); const myStyles = createStyles({ hash: customHash }); const style = myStyles.variants({ flex: { display: "flex" }, }); expect(style("flex")).toMatchSnapshot(); const style2 = createStyles().variants({ flex: { display: "flex" }, }); expect(style2("flex")).not.toBe(style("flex")); }); it("adds nonce to style tags", () => { createStyles({ dash: createDash({ nonce: "EDNnf03nceIOfn39fn3e9h3sdfa" }), }); expect( document.querySelectorAll(`style[data-dash]`)[0].getAttribute("nonce") ).toBe("EDNnf03nceIOfn39fn3e9h3sdfa"); }); it('changes key to "css"', () => { const myStyles = createStyles({ dash: createDash({ key: "css" }) }); const style = myStyles.variants({ flex: { display: "flex" }, }); style("flex"); expect(cssRules()[0].selectorText).toStrictEqual( expect.stringMatching(/^\.css-/) ); }); it("changes container to document.body", () => { const myStyles = createStyles({ dash: createDash({ container: document.body }), }); const style = myStyles.variants({ flex: { display: "flex" }, }); style("flex"); expect(document.querySelectorAll(`body style[data-dash]`).length).toBe(1); }); it("should initialize w/ tokens", () => { const myStyles = createStyles({ tokens: { box: { small: 100 } } }); const style = myStyles.variants({ small: ({ box }) => ({ width: box.small, height: box.small, }), }); expect(style.css("small")).toMatchSnapshot(); }); it("should initialize w/ themes", () => { const myStyles = createStyles({ tokens: {}, themes: { light: { color: { primary: "white", secondary: "foo", } as const, }, dark: { color: { primary: "black", secondary: "bar", } as const, }, }, }); const style = myStyles.variants({ primary: ({ color }) => ({ color: color.primary }), }); expect(style.css("primary")).toEqual("color:var(--color-primary);"); expect(myStyles.theme("light")).toEqual("ui-light-theme"); }); }); describe("styles.variants()", () => { it("returns single class name", () => { const style = createStyles().variants({ flex: { display: "flex" }, block: { display: "block" }, inline: "display: inline;", }); expect(style("flex")).toMatchSnapshot(); expect(style("flex", "block", "inline")).toMatchSnapshot(); expect(style({ flex: true, block: false, inline: true })).toMatchSnapshot(); }); it("returns css styles", () => { const style = createStyles().variants({ flex: { display: "flex" }, block: { display: "block" }, inline: "display: inline;", }); expect(style.css("flex")).toMatchSnapshot(); expect(style.css("flex", "block", "inline")).toMatchSnapshot(); expect( style.css({ flex: true, block: false, inline: true }) ).toMatchSnapshot(); }); it("works with numeric variants", () => { const style = createStyles().variants({ 0: { display: "flex" }, 1: { display: "block" }, 2: "display: inline;", }); expect(style.css(0)).toEqual("display:flex;"); expect(style.css(0, 1, 2)).toEqual( "display:flex;display:block;display: inline;" ); expect(style.css({ 0: true, 1: false, 2: true })).toEqual( "display:flex;display: inline;" ); }); it("joins css styles and returns class name", () => { const style = createStyles(); const flex = style.variants({ flex: { display: "flex" }, }); const block = style.variants({ block: { display: "block" }, }); style.join(flex.css("flex"), block.css("block")); expect(style.join(flex.css("flex"), block.css("block"))).not.toBe( flex("flex") ); expect(cssRules()[0].cssText).not.toMatch(/display: flex;/); expect(cssRules()[0].cssText).toMatch(/display: block;/); }); it("returns empty string when falsy", () => { const style = createStyles().variants({ flex: { display: "flex" }, }); let name = style(false); expect(typeof name).toBe("string"); expect(name.length).toBe(0); name = style(false, null, undefined, { flex: false }); expect(typeof name).toBe("string"); expect(name.length).toBe(0); }); it("ignores unknown keys", () => { const style = createStyles().variants({ flex: { display: "flex" }, }); // @ts-expect-error let name = style("noop"); expect(typeof name).toBe("string"); expect(name.length).toBe(0); // @ts-expect-error name = style({ noop: true }); expect(typeof name).toBe("string"); expect(name.length).toBe(0); }); it("allows unitless object values", () => { const style = createStyles().variants({ box: { width: 200, height: "200px" }, }); style("box"); expect(cssRules()[0].cssText).toMatch(/width: 200px;/); }); it("adds styles by order of definition when called", () => { const style = createStyles({ dash: createDash({ prefix: false }), }).variants({ inline: "display: inline;", flex: { display: "flex" }, block: { display: "block" }, }); style("flex", "block", "inline"); expect(cssRules()[0].cssText).not.toMatch(/display: (flex|block);/); expect(cssRules()[0].cssText).toMatch(/display: inline;/); styles.dash.inserted.clear(); styles.dash.sheet.flush(); style({ flex: true, block: true, inline: true }); expect(cssRules()[0].cssText).not.toMatch(/display: (flex|block);/); expect(cssRules()[0].cssText).toMatch(/display: inline;/); }); it("allows comments", () => { const style = createStyles().variants({ flex: ` /* this is a flex style */ display: flex; `, }); expect(style.css("flex")).toMatchSnapshot(); }); it("allows full capabilities w/ style objects", () => { const style = createStyles().variants({ flex: { display: "flex", "&.foo": { display: "block", }, }, }); style("flex"); const rules = cssRules(); expect(rules[0].cssText).toMatch(/display: flex;/); expect(rules[1].selectorText).toMatch(/\.foo/); expect(rules[1].cssText).toMatch(/display: block;/); }); it("passes tokens to style callbacks", () => { const myStyles = createStyles({ tokens: { colors: { bg: "#09a", text: "#c12", }, }, themes: { dark: { colors: { bg: "#000", text: "#fff", }, }, light: { colors: { bg: "#fff", text: "#000", lightSpecific: "#ccc", }, }, }, }); const style = myStyles.variants({ box: (vars) => { expect(vars).toMatchSnapshot(); return ""; }, }); style("box"); expect(myStyles.theme("dark")).toMatchSnapshot(); style("box"); expect(myStyles.theme("light")).toMatchSnapshot(); style("box"); }); it("adds dev labels", () => { const prevEnv = process.env.NODE_ENV; process.env.NODE_ENV = "development"; const style = createStyles().variants({ flex: `display: flex;`, block: `display: block;`, inline: `display: inline;`, }); expect(style("flex")).toMatchSnapshot("-flex"); expect(style("flex", "inline")).toMatchSnapshot("-flex-inline"); expect(style("flex", { inline: false, block: true })).toMatchSnapshot( "-flex-block" ); process.env.NODE_ENV = prevEnv; }); it("replaces disallowed characters in dev labels", () => { const prevEnv = process.env.NODE_ENV; process.env.NODE_ENV = "development"; const style = createStyles().variants({ "box=big": { width: 400, height: "400px" }, }); style("box=big"); const rules = cssRules(); expect(rules[0].selectorText).toMatch(/box-big/); process.env.NODE_ENV = prevEnv; }); it("allows default styles", () => { const style = createStyles().variants({ default: `display: flex;`, block: `display: block;`, }); style(); const rules = cssRules(); expect(rules[0].cssText).toMatch(/display: flex;/); }); it("has a default style that is always applied first", () => { const style = createStyles().variants({ block: `display: block;`, default: `display: flex;`, }); style("block"); const rules = cssRules(); expect(rules[0].cssText).toMatch(/display: block;/); }); it("flushes sheet tags", () => { const myStyles = createStyles({}); const style = myStyles.variants({ flex: { display: "flex" }, block: { display: "block" }, }); style("flex"); style("block"); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(1); myStyles.dash.sheet.flush(); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(0); }); it("rehydrates", () => { const tag = document.createElement("style"); tag.setAttribute(`data-dash`, "1ut9bc3"); tag.setAttribute("data-cache", "ui"); tag.appendChild( document.createTextNode( `.-ui-_1ut9bc3{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}` ) ); document.head.appendChild(tag); const myStyles = createStyles({}); const style = myStyles.variants({ flex: { display: "flex" }, }); style("flex"); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(2); }); it("rehydrates into custom container", () => { const tag = document.createElement("style"); tag.setAttribute(`data-dash`, "1ut9bc3"); tag.setAttribute("data-cache", "ui"); tag.appendChild( document.createTextNode( `.ui-_1ut9bc3{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}` ) ); document.head.appendChild(tag); const myStyles = createStyles({ dash: createDash({ container: document.body }), }); const style = myStyles.variants({ flex: { display: "flex" }, }); style("flex"); expect(document.querySelectorAll(`head style[data-dash]`).length).toBe(0); expect(document.querySelectorAll(`body style[data-dash]`).length).toBe(2); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(2); }); }); describe("styles.keyframes()", () => { it("returns keyframes name", () => { const name = createStyles().keyframes` 0% { opacity: 0; } 100% { opacity: 1; } `; expect(name).toMatchSnapshot(); }); it("adds keyframes to dom", () => { const name = createStyles().keyframes(` 0% { opacity: 0; } 100% { opacity: 1; } `); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(2); expect( cssRules(document.querySelectorAll(`style[data-dash]`)[1])[0].cssText ).toMatch(new RegExp(`@-webkit-keyframes ${name}`)); }); it("works with tokens callback", () => { const myStyles = createStyles({ tokens: { color: { blue: "blue", red: "red", }, }, }); myStyles.keyframes( ({ color }) => ` 0% { background-color: ${color.blue}; } 100% { background-color: ${color.red}; } ` ); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(3); // tokens + kf const rule = cssRules(document.querySelectorAll(`style[data-dash]`)[2])[0]; expect(rule.cssText).toMatch(/0% {background-color: var\(--color-blue\);}/); expect(rule.cssText).toMatch( /100% {background-color: var\(--color-red\);}/ ); }); }); describe(`styles.insertTokens()`, () => { it("creates tokens", () => { createStyles().insertTokens({ columns: 12, colors: { blue: "#09a", red: "#c12", lightRed: "#c1a", }, spacing: { xs: "1rem", }, system: { p: { md: "1rem", xs: "0.25rem", sm: "0.5rem", lg: "2rem", xl: "4rem" }, }, }); const rule = cssRules(document.querySelectorAll(`style[data-dash]`)[1])[0]; expect(rule.selectorText).toBe(":root"); expect(rule.cssText).toMatch(/--system-p-md: 1rem;/); expect(rule.cssText).toMatch(/--colors-blue: #09a;/); }); it("removes tokens when eject is called", () => { const myStyles = createStyles(); const eject = myStyles.insertTokens({ colors: { blue: "#09a", red: "#c12", lightRed: "#c1a", }, spacing: { xs: "1rem", }, system: { p: { md: "1rem", xs: "0.25rem", sm: "0.5rem", lg: "2rem", xl: "4rem" }, }, }); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(2); expect(myStyles.dash.inserted.size).toBe(1); expect(Array.from(myStyles.dash.sheets.keys()).length).toBe(1); eject(); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(1); expect(myStyles.dash.inserted.size).toBe(0); expect(Array.from(myStyles.dash.sheets.keys()).length).toBe(0); }); it("mangles tokens", () => { createStyles({ mangleTokens: true }).insertTokens({ columns: 12, colors: { blue: "#09a", red: "#c12", lightRed: "#c1a", }, spacing: { xs: "1rem", }, system: { p: { md: "1rem", xs: "0.25rem", sm: "0.5rem", lg: "2rem", xl: "4rem" }, }, }); expect( cssRules(document.querySelectorAll(`style[data-dash]`)[1])[0].cssText ).not.toMatch(/spacing/); }); it("mangles tokens w/ reserved keys", () => { createStyles({ mangleTokens: { "colors-blue": true } }).insertTokens({ columns: 12, colors: { blue: "#09a", red: "#c12", lightRed: "#c1a", }, spacing: { xs: "1rem", }, system: { p: { md: "1rem", xs: "0.25rem", sm: "0.5rem", lg: "2rem", xl: "4rem" }, }, }); expect( cssRules(document.querySelectorAll(`style[data-dash]`)[1])[0].cssText ).not.toMatch(/spacing/); expect( cssRules(document.querySelectorAll(`style[data-dash]`)[1])[0].cssText ).toMatch(/colors/); }); it("still exists in caches when used more than once", () => { const myStyles = createStyles(); const ejectA = myStyles.insertTokens({ colors: { blue: "#09a", red: "#c12", lightRed: "#c1a", }, spacing: { xs: "1rem", }, system: { p: { md: "1rem", xs: "0.25rem", sm: "0.5rem", lg: "2rem", xl: "4rem" }, }, }); const ejectB = myStyles.insertTokens({ colors: { blue: "#09a", red: "#c12", lightRed: "#c1a", }, spacing: { xs: "1rem", }, system: { p: { md: "1rem", xs: "0.25rem", sm: "0.5rem", lg: "2rem", xl: "4rem" }, }, }); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(2); expect(myStyles.dash.inserted.size).toBe(1); expect(Array.from(myStyles.dash.sheets.keys()).length).toBe(1); ejectA(); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(2); expect(myStyles.dash.inserted.size).toBe(1); expect(Array.from(myStyles.dash.sheets.keys()).length).toBe(1); ejectB(); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(1); expect(myStyles.dash.inserted.size).toBe(0); expect(Array.from(myStyles.dash.sheets.keys()).length).toBe(0); }); it("creates tokens w/ scales", () => { createStyles().insertTokens({ spacing: ["1rem", "2rem", "4rem"], }); const cssText = cssRules( document.querySelectorAll(`style[data-dash]`)[1] )[0].cssText; expect(cssText).toMatch(/--spacing-0: 1rem; --spacing-1: 2rem;/); }); }); describe(`styles.insertThemes()`, () => { it("creates tokens", () => { createStyles().insertThemes({ dark: { colors: { bg: "#000", text: "#fff", }, }, light: { colors: { bg: "#fff", text: "#000", }, }, }); expect( cssRules(document.querySelectorAll(`style[data-dash]`)[1])[0].selectorText ).toBe(".ui-dark-theme"); expect( cssRules(document.querySelectorAll(`style[data-dash]`)[2])[0].selectorText ).toBe(".ui-light-theme"); expect( cssRules(document.querySelectorAll(`style[data-dash]`)[1])[0].cssText ).toStrictEqual(expect.stringContaining("--colors-bg: #000;")); expect( cssRules(document.querySelectorAll(`style[data-dash]`)[2])[0].cssText ).toStrictEqual(expect.stringContaining("--colors-bg: #fff;")); }); it("removes tokens when eject is called", () => { const myStyles = createStyles(); const eject = myStyles.insertThemes({ dark: { colors: { bg: "#000", text: "#fff", }, }, light: { colors: { bg: "#fff", text: "#000", }, }, }); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(3); expect(myStyles.dash.inserted.size).toBe(2); expect(Array.from(myStyles.dash.sheets.keys()).length).toBe(2); eject(); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(1); expect(myStyles.dash.inserted.size).toBe(0); expect(Array.from(myStyles.dash.sheets.keys()).length).toBe(0); }); }); describe(`styles.insertGlobal()`, () => { it("passes tokens to global styles", () => { const myStyles = createStyles(); myStyles.insertTokens({ colors: { blue: "#09a", red: "#c12", }, }); myStyles.insertThemes({ dark: { colors: { bg: "#000", text: "#fff", }, }, light: { colors: { bg: "#fff", text: "#000", }, }, }); myStyles.insertGlobal((vars) => { expect(vars).toMatchSnapshot(); return ""; }); }); it("injects global style object", () => { const styles_ = createStyles(); styles_.insertGlobal({ html: { color: "blue", ".foo": { color: "green", }, }, }); expect( cssRules(document.querySelectorAll(`style[data-dash]`)[1])[0].selectorText ).toStrictEqual(expect.stringContaining("html")); expect( cssRules(document.querySelectorAll(`style[data-dash]`)[1])[1].selectorText ).toStrictEqual(expect.stringContaining("html .foo")); }); it("ejects global styles when callback is called", () => { const myStyles = createStyles(); const eject = myStyles.insertGlobal(` html { font-size: 100%; } `); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(2); expect(myStyles.dash.inserted.size).toBe(1); expect(Array.from(myStyles.dash.sheets.keys()).length).toBe(1); eject(); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(1); expect(myStyles.dash.inserted.size).toBe(0); expect(Array.from(myStyles.dash.sheets.keys()).length).toBe(0); }); it("still exists in caches when a global is used more than once but ejected once", () => { const myStyles = createStyles(); const ejectA = myStyles.insertGlobal(` html { font-size: 100%; } `); const ejectB = myStyles.insertGlobal(` html { font-size: 100%; } `); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(2); expect( cssRules(document.querySelectorAll(`style[data-dash]`)[1])[0].selectorText ).toBe("html"); expect( cssRules(document.querySelectorAll(`style[data-dash]`)[1])[0].cssText ).toStrictEqual(expect.stringContaining("font-size: 100%;")); expect(myStyles.dash.inserted.size).toBe(1); expect(Array.from(myStyles.dash.sheets.keys()).length).toBe(1); ejectA(); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(2); expect(myStyles.dash.inserted.size).toBe(1); expect(Array.from(myStyles.dash.sheets.keys()).length).toBe(1); ejectB(); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(1); expect(myStyles.dash.inserted.size).toBe(0); expect(Array.from(myStyles.dash.sheets.keys()).length).toBe(0); }); it("allows @font-face", () => { const { insertGlobal } = createStyles(); insertGlobal` @font-face { font-family: "Open Sans"; src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2"), url("/fonts/OpenSans-Regular-webfont.woff") format("woff"); } `; expect( cssRules(document.querySelectorAll(`style[data-dash]`)[1])[0].cssText ).toStrictEqual(expect.stringContaining("@font-face")); }); it("allows style object", () => { const { insertGlobal } = createStyles(); insertGlobal({ ":root": { "--foo": "bar", }, }); expect(document.querySelectorAll(`style[data-dash]`).length).toBe(2); expect( cssRules(document.querySelectorAll(`style[data-dash]`)[1])[0].selectorText ).toBe(":root"); expect( cssRules(document.querySelectorAll(`style[data-dash]`)[1])[0].cssText ).toStrictEqual(expect.stringContaining("--foo: bar;")); }); }); describe("styles.one()", () => { it("creates style w/ template literal", () => { const myStyles = createStyles(); const myCls = myStyles.one` display: flex; `; myCls(); expect(cssRules()[0].cssText).toStrictEqual( expect.stringContaining("display: flex;") ); }); it("creates style w/ object", () => { const myStyles = createStyles(); const myCls = myStyles.one({ display: "block", span: { display: "flex", }, }); myCls(); expect(cssRules()[0].cssText).toStrictEqual( expect.stringContaining("display: block;") ); expect(cssRules()[1].selectorText).toStrictEqual( expect.stringContaining(" span") ); expect(cssRules()[1].cssText).toStrictEqual( expect.stringContaining("display: flex;") ); }); it(`won't create style def if falsy`, () => { const myStyles = createStyles(); const myCls = myStyles.one` display: flex; `; myCls(); expect(cssRules()[0].cssText).toStrictEqual( expect.stringContaining("display: flex;") ); }); it(`won't create style if function call is provided falsy value`, () => { const myStyles = createStyles(); const myCls = myStyles.one` display: flex; `; myCls(false); myCls(null); myCls(0); myCls(""); expect(cssRules().length).toBe(0); }); it(`returns css when css() is called`, () => { const myStyles = createStyles(); const myCls = myStyles.one` display: flex; `; expect(myCls.css()).toStrictEqual( expect.stringContaining("display: flex;") ); }); it(`wont return css when css() is called w/ falsy value`, () => { const myStyles = createStyles(); const myCls = myStyles.one` display: flex; `; expect(myCls.css(false)).toBe(""); }); it(`can be called as a function w/ string value`, () => { const myStyles = createStyles(); const myCls = myStyles.one("display: flex;"); expect(myCls.css()).toStrictEqual( expect.stringContaining("display: flex;") ); }); it(`can be called as a function w/ function value`, () => { type Tokens = { color: { blue: "blue"; }; }; const myStyles = createStyles<Tokens>(); myStyles.insertTokens({ color: { blue: "blue" } }); const myCls = myStyles.one(({ color }) => `color: ${color.blue};`); expect(myCls.css()).toStrictEqual( expect.stringContaining("color: var(--color-blue);") ); }); }); describe("styles.cls()", () => { it("creates style and inserts it into the dom right away", () => { const myStyles = createStyles(); expect( typeof myStyles.cls` display: flex; ` ).toBe("string"); expect(cssRules()[0].cssText).toStrictEqual( expect.stringContaining("display: flex;") ); }); }); describe("styles.lazy()", () => { it("creates style from serializable values", () => { const myStyles = createStyles(); const lazyWidth = myStyles.lazy(({ width }: { width: number }) => ({ width, })); expect(typeof lazyWidth({ width: 37 })).toBe("string"); expect(cssRules()[0].cssText).toStrictEqual( expect.stringContaining("width: 37px;") ); expect(typeof lazyWidth({ width: 36 })).toBe("string"); expect(cssRules()[1].cssText).toStrictEqual( expect.stringContaining("width: 36px;") ); }); it("should return empty string if undefined value is provided", () => { const myStyles = createStyles(); const lazyWidth = myStyles.lazy((width: number) => ({ width, })); expect(typeof lazyWidth()).toBe("string"); expect(cssRules().length).toBe(0); }); }); describe("styles.tokens", () => { it("should make CSS tokens available", () => { const myStyles = createStyles({ tokens: { spacing: [0, "0.5rem"], }, }); expect(myStyles.tokens).toEqual({ spacing: { 0: "var(--spacing-0)", 1: "var(--spacing-1)", }, }); }); }); describe("Exceptions", () => { it("throws for unterminated comments", () => { const style = createStyles().variants({ flex: ` /* this is a flex style with an unterminated comment ;) display: flex; `, }); expect(() => { style("flex"); }).toThrowErrorMatchingSnapshot(); }); }); describe("pathToToken()", () => { it("should tokenize an object path", () => { expect( pathToToken<{ button: { color: { primaryHover: "foo" } }; color: { primary: "foo"; scale: [0, 1, 2, 3] }; }>("color.scale.0") ).toEqual("var(--color-scale-0)"); expect(pathToToken("color.scale.0")).toEqual("var(--color-scale-0)"); expect( pathToToken<{ button: { color: { primaryHover: "foo" } }; color: { primary: "foo" }; }>("button.color.primaryHover") ).toEqual("var(--button-color-primary-hover)"); }); });