diff --git a/package.json b/package.json index aa3a5d3..b323292 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,9 @@ "eslint-plugin-prettier": "^3.0.0", "flow-bin": "^0.88.0", "jest-cli": "^23.6.0", + "lodash": "^4.17.11", "opn-cli": "^4.0.0", + "polished": "^2.3.1", "prettier": "^1.15.3", "react": "^16.6.3", "react-dom": "^16.6.3", diff --git a/src/ifProp.js b/src/ifProp.js index 345a9da..f11d776 100644 --- a/src/ifProp.js +++ b/src/ifProp.js @@ -1,10 +1,16 @@ // @flow /* eslint-disable no-use-before-define */ import prop from "./prop"; +import resolveValue from "./resolveValue"; import type { Needle, PropsFn } from "."; -const parseFunction = (props: Object, test: Function): boolean => - Boolean(test(props)); +const parseFunction = (props: Object, test: Function): boolean => { + const result = test(props); + if (typeof result === "function") { + return parseFunction(props, result); + } + return Boolean(result); +}; const parseObject = (props: Object, test: Object): boolean => { const keys = Object.keys(test); @@ -62,7 +68,7 @@ const ifProp = ( } const value = result ? pass : fail; - return typeof value === "function" ? value(props) : value; + return resolveValue(value, props); }; export default ifProp; diff --git a/src/palette.js b/src/palette.js index 2ccd727..c101429 100644 --- a/src/palette.js +++ b/src/palette.js @@ -1,4 +1,5 @@ // @flow +import resolveValue from "./resolveValue"; import type { PropsWithTheme } from "."; type Props = PropsWithTheme & { @@ -60,13 +61,16 @@ const palette = ( return finalDefaultValue; } - const tones = toArray(props.theme.palette[key]); + const tones = toArray(resolveValue(props.theme.palette[key], props)); if (tone < 0) { - return tones[clamp(tones.length + tone, 0, tones.length - 1)]; + return resolveValue( + tones[clamp(tones.length + tone, 0, tones.length - 1)], + props + ); } - return tones[clamp(tone, 0, tones.length - 1)]; + return resolveValue(tones[clamp(tone, 0, tones.length - 1)], props); }; export default palette; diff --git a/src/prop.js b/src/prop.js index c275962..2634a21 100644 --- a/src/prop.js +++ b/src/prop.js @@ -1,4 +1,5 @@ // @flow +import resolveValue from "./resolveValue"; import type { PropsFn } from "."; /** @@ -13,7 +14,7 @@ import type { PropsFn } from "."; */ const prop = (path: string, defaultValue?: any): PropsFn => (props = {}) => { if (typeof props[path] !== "undefined") { - return props[path]; + return resolveValue(props[path], props); } if (path && path.indexOf(".") > 0) { @@ -28,7 +29,7 @@ const prop = (path: string, defaultValue?: any): PropsFn => (props = {}) => { } if (typeof object !== "undefined") { - return object; + return resolveValue(object, props); } } diff --git a/src/resolveValue.js b/src/resolveValue.js new file mode 100644 index 0000000..adb6ec3 --- /dev/null +++ b/src/resolveValue.js @@ -0,0 +1,10 @@ +// @flow + +const resolveValue = (value: any, props: Object) => { + if (typeof value === "function") { + return resolveValue(value(props), props); + } + return value; +}; + +export default resolveValue; diff --git a/src/switchProp.js b/src/switchProp.js index fc349c3..6e202c9 100644 --- a/src/switchProp.js +++ b/src/switchProp.js @@ -1,5 +1,6 @@ // @flow import prop from "./prop"; +import resolveValue from "./resolveValue"; import type { Needle, PropsFn } from "."; /** @@ -32,10 +33,12 @@ const switchProp = ( defaultCase: any ): PropsFn => (props = {}) => { const value = - typeof needle === "function" ? needle(props) : prop(needle)(props); + typeof needle === "function" + ? resolveValue(needle, props) + : prop(needle)(props); const finalCases = typeof cases === "function" ? cases(props) : cases; if (value in finalCases) { - return finalCases[value]; + return resolveValue(finalCases[value], props); } return defaultCase; }; diff --git a/src/theme.js b/src/theme.js index 915a907..fc5b8d8 100644 --- a/src/theme.js +++ b/src/theme.js @@ -14,6 +14,6 @@ import type { PropsWithTheme } from "."; * `; */ const theme = (path: string, defaultValue?: any) => (props: PropsWithTheme) => - prop(path, defaultValue)(props.theme); + prop(`theme.${path}`, defaultValue)(props); export default theme; diff --git a/src/withProp.js b/src/withProp.js index 3860c1f..b065930 100644 --- a/src/withProp.js +++ b/src/withProp.js @@ -1,5 +1,6 @@ // @flow import prop from "./prop"; +import resolveValue from "./resolveValue"; import type { Needle, PropsFn } from "."; /** @@ -24,7 +25,11 @@ const withProp = (needle: Needle | Needle[], fn: Function): PropsFn => ( return fn(...needles); } if (typeof needle === "function") { - return fn(needle(props)); + const value = resolveValue(needle, props); + if (Array.isArray(value)) { + return withProp(value, fn)(props); + } + return fn(value); } return fn(prop(needle)(props)); }; diff --git a/test/complex.test.js b/test/complex.test.js new file mode 100644 index 0000000..186f032 --- /dev/null +++ b/test/complex.test.js @@ -0,0 +1,53 @@ +import { getLuminance, tint } from "polished"; +import { range } from "lodash"; +import { palette as p, theme as t, withProp } from "../src"; + +export const contrastText = bgNeedle => + withProp(bgNeedle, bg => + getLuminance(bg) > 0.179 ? p("black") : p("white") + ); + +export const contrastPalette = (key, tone) => contrastText(p(key, tone)); + +export const tintPalette = (amount, key, tone) => + withProp(p(key, tone), color => tint(amount, color)); + +export const tonePalette = (key, tone) => + withProp(t("colorInterval"), colorInterval => + range(5) + .map(i => i * colorInterval) + .map(i => tintPalette(i, key, tone)) + ); + +export const tonePaletteText = key => + range(5).map(i => contrastPalette(key, i)); + +export const tonePaletteWithText = (name, key, tone) => ({ + [name]: tonePalette(key, tone), + [`${name}Text`]: tonePaletteText(name) +}); + +const theme = { + palette: { + white: "#fff", + black: "#000", + + blue: "#007bff", + + ...tonePaletteWithText("primary", "blue") + }, + colorInterval: 0.08 +}; + +test("complex", () => { + expect(p("primary")({ theme })).toBe("#007bff"); + expect(p("primaryText")({ theme })).toBe("#000"); + expect(p("primary", 1)({ theme })).toBe("#1485ff"); + expect(p("primaryText", 1)({ theme })).toBe("#000"); + expect(p("primary", 2)({ theme })).toBe("#2890ff"); + expect(p("primaryText", 2)({ theme })).toBe("#000"); + expect(p("primary", 3)({ theme })).toBe("#3d9aff"); + expect(p("primaryText", 3)({ theme })).toBe("#000"); + expect(p("primary", 4)({ theme })).toBe("#51a5ff"); + expect(p("primaryText", 4)({ theme })).toBe("#000"); +}); diff --git a/test/ifNotProp.test.js b/test/ifNotProp.test.js index 47ffada..7cde055 100644 --- a/test/ifNotProp.test.js +++ b/test/ifNotProp.test.js @@ -81,3 +81,26 @@ test("function values", () => { ifNotProp("foo", props => props.bar, props => props.foo)({ foo: "foo" }) ).toBe("foo"); }); + +test("deep function argument", () => { + expect(ifNotProp(() => props => props.foo, "no", "yes")()).toBe("no"); + expect(ifNotProp(() => props => props.foo, "no", "yes")({ foo: false })).toBe( + "no" + ); + expect(ifNotProp(() => props => props.foo, "no", "yes")({ foo: true })).toBe( + "yes" + ); +}); + +test("deep function values", () => { + expect( + ifNotProp("foo", () => props => props.bar, () => props => props.foo)({ + bar: "bar" + }) + ).toBe("bar"); + expect( + ifNotProp("foo", () => props => props.bar, () => props => props.foo)({ + foo: "foo" + }) + ).toBe("foo"); +}); diff --git a/test/ifProp.test.js b/test/ifProp.test.js index 69d41c6..6d83201 100644 --- a/test/ifProp.test.js +++ b/test/ifProp.test.js @@ -92,3 +92,38 @@ test("function values", () => { ifProp("foo", props => props.foo, props => props.bar)({ foo: "foo" }) ).toBe("foo"); }); + +test("deep function argument", () => { + expect(ifProp(() => props => props.foo, "yes", "no")()).toBe("no"); + expect(ifProp(() => props => props.foo, "yes", "no")({ foo: false })).toBe( + "no" + ); + expect(ifProp(() => props => props.foo, "yes", "no")({ foo: true })).toBe( + "yes" + ); +}); + +test("deep function values", () => { + expect( + ifProp("foo", () => props => props.foo, () => props => props.bar)({ + bar: "bar" + }) + ).toBe("bar"); + expect( + ifProp("foo", () => props => props.foo, () => props => props.bar)({ + foo: "foo" + }) + ).toBe("foo"); +}); + +test("deep function array argument", () => { + expect( + ifProp([() => props => props.foo], "yes", "no")({ bar: false, foo: true }) + ).toBe("yes"); + expect( + ifProp(["bar", () => props => props.foo], "yes", () => "no")({ + bar: false, + foo: true + }) + ).toBe("no"); +}); diff --git a/test/palette.test.js b/test/palette.test.js index 27338c1..e8964af 100644 --- a/test/palette.test.js +++ b/test/palette.test.js @@ -7,6 +7,13 @@ const theme = { } }; +const deepTheme = { + palette: { + primary: [() => "primary0", () => "primary1", () => "primary2"], + secondary: () => "secondary0" + } +}; + test("only tone", () => { expect(palette()({ theme: {} })).toBeUndefined(); expect(palette()({ theme })).toBeUndefined(); @@ -44,3 +51,53 @@ test("palette and tone", () => { expect(palette("other", 1)({ theme, palette: "primary" })).toBeUndefined(); expect(palette("other", 1, "foo")({ theme, palette: "primary" })).toBe("foo"); }); + +test("deep only tone", () => { + expect(palette()({ theme: deepTheme })).toBeUndefined(); + expect(palette(0)({ theme: deepTheme })).toBeUndefined(); + expect(palette()({ theme: deepTheme, palette: "primary" })).toBe("primary0"); + expect(palette()({ theme: deepTheme, palette: "primary", tone: 2 })).toBe( + "primary2" + ); + expect(palette(0)({ theme: deepTheme, palette: "primary" })).toBe("primary0"); + expect(palette(1)({ theme: deepTheme, palette: "primary" })).toBe("primary1"); + expect(palette(-1)({ theme: deepTheme, palette: "primary" })).toBe( + "primary2" + ); + expect(palette(-5)({ theme: deepTheme, palette: "primary" })).toBe( + "primary0" + ); + expect(palette(5)({ theme: deepTheme, palette: "primary" })).toBe("primary2"); + expect(palette(0)({ theme: deepTheme, palette: "secondary" })).toBe( + "secondary0" + ); + expect(palette(1)({ theme: deepTheme, palette: "secondary" })).toBe( + "secondary0" + ); + expect(palette(0)({ theme: deepTheme, palette: "other" })).toBeUndefined(); + expect(palette(1, "foo")({ theme: deepTheme })).toBe("foo"); + expect(palette(1, "foo")({ theme: deepTheme, palette: "other" })).toBe("foo"); +}); + +test("deep palette and tone", () => { + expect(palette("primary")({ theme: deepTheme })).toBe("primary0"); + expect(palette("primary")({ theme: deepTheme, tone: 2 })).toBe("primary2"); + expect(palette("primary", 0)({ theme: deepTheme })).toBe("primary0"); + expect(palette("primary", 5)({ theme: deepTheme })).toBe("primary2"); + expect(palette("primary", -1)({ theme: deepTheme })).toBe("primary2"); + expect(palette("primary", -5)({ theme: deepTheme })).toBe("primary0"); + expect(palette("secondary", 0)({ theme: deepTheme })).toBe("secondary0"); + expect(palette("secondary", 1)({ theme: deepTheme })).toBe("secondary0"); + expect(palette("secondary", 1, "foo")({ theme: deepTheme })).toBe( + "secondary0" + ); + expect(palette("other", 1)({ theme: deepTheme })).toBeUndefined(); + expect(palette("other", "foo")({ theme: deepTheme })).toBe("foo"); + expect(palette("other", 1, "foo")({ theme: deepTheme })).toBe("foo"); + expect( + palette("other", 1)({ theme: deepTheme, palette: "primary" }) + ).toBeUndefined(); + expect( + palette("other", 1, "foo")({ theme: deepTheme, palette: "primary" }) + ).toBe("foo"); +}); diff --git a/test/prop.test.js b/test/prop.test.js index 3986445..faf7293 100644 --- a/test/prop.test.js +++ b/test/prop.test.js @@ -4,6 +4,8 @@ test("string argument", () => { expect(prop("color")()).toBeUndefined(); expect(prop("color")({})).toBeUndefined(); expect(prop("color")({ color: "red" })).toBe("red"); + expect(prop("color")({ color: () => "red" })).toBe("red"); + expect(prop("color")({ color: props => props.bg, bg: "red" })).toBe("red"); }); test("deep string argument", () => { @@ -11,6 +13,14 @@ test("deep string argument", () => { expect(prop("color.primary")({})).toBeUndefined(); expect(prop("color.primary")({ color: {} })).toBeUndefined(); expect(prop("color.primary")({ color: { primary: "red" } })).toBe("red"); + expect(prop("color.primary")({ color: { primary: () => "red" } })).toBe( + "red" + ); + expect( + prop("color.primary")({ + color: { primary: prop("color.secondary"), secondary: "blue" } + }) + ).toBe("blue"); }); test("defaultValue", () => { diff --git a/test/switchProp.test.js b/test/switchProp.test.js index adaae71..1a7489c 100644 --- a/test/switchProp.test.js +++ b/test/switchProp.test.js @@ -8,6 +8,14 @@ test("string argument", () => { expect( switchProp("type", { red: "red", blue: "blue" })({ type: "blue" }) ).toBe("blue"); + expect( + switchProp("type", { red: "red", blue: "blue" })({ type: () => "red" }) + ).toBe("red"); + expect( + switchProp("type", { red: () => "red", blue: () => "blue" })({ + type: "red" + }) + ).toBe("red"); }); test("deep string argument", () => { @@ -35,6 +43,11 @@ test("function argument", () => { type: "blue" }) ).toBe("blue"); + expect( + switchProp(() => props => props.type, { red: "red", blue: "blue" })({ + type: "red" + }) + ).toBe("red"); }); test("default case", () => { diff --git a/test/theme.test.js b/test/theme.test.js index 34fee7d..cceb406 100644 --- a/test/theme.test.js +++ b/test/theme.test.js @@ -3,6 +3,22 @@ import theme from "../src/theme"; test("string argument", () => { expect(theme("color")({ theme: {} })).toBeUndefined(); expect(theme("color")({ theme: { color: "red" } })).toBe("red"); + expect(theme("color")({ theme: { color: () => "red" } })).toBe("red"); + expect( + theme("color")({ theme: { color: props => props.theme.bg, bg: "blue" } }) + ).toBe("blue"); + expect(theme("color")({ theme: { color: theme("bg"), bg: "blue" } })).toBe( + "blue" + ); + expect( + theme("linkColor")({ + theme: { + blue: "#007bff", + primary: theme("blue"), + linkColor: theme("primary") + } + }) + ).toBe("#007bff"); }); test("deep string argument", () => { @@ -10,6 +26,11 @@ test("deep string argument", () => { expect(theme("color.primary")({ theme: { color: { primary: "red" } } })).toBe( "red" ); + expect( + theme("color.primary")({ + theme: { color: { primary: theme("color.secondary"), secondary: "blue" } } + }) + ).toBe("blue"); }); test("defaultValue", () => { diff --git a/test/withProp.test.js b/test/withProp.test.js index 3b37c99..7ca8b52 100644 --- a/test/withProp.test.js +++ b/test/withProp.test.js @@ -1,9 +1,13 @@ import withProp from "../src/withProp"; +import prop from "../src/prop"; test("string argument", () => { expect(withProp("type", type => type === "foo")()).toBe(false); expect(withProp("type", type => type === "foo")({ type: "bar" })).toBe(false); expect(withProp("type", type => type === "foo")({ type: "foo" })).toBe(true); + expect(withProp("type", type => type === "foo")({ type: () => "foo" })).toBe( + true + ); }); test("deep string argument", () => { @@ -15,6 +19,11 @@ test("deep string argument", () => { expect( withProp("foo.bar", bar => bar === "foo")({ foo: { bar: "foo" } }) ).toBe(true); + expect( + withProp("foo.bar", bar => bar === "bar")({ + foo: { bar: prop("foo.baz"), baz: "bar" } + }) + ).toBe(true); }); test("array argument", () => { @@ -32,6 +41,14 @@ test("array argument", () => { "foo", "bar" ]); + expect( + withProp([props => props.baz, props => props.biz], fn)({ + foo: "foo", + bar: "bar", + baz: props => props.foo, + biz: props => props.bar + }) + ).toEqual(["foo", "bar"]); }); test("function argument", () => { @@ -42,4 +59,16 @@ test("function argument", () => { expect( withProp(props => props.type, type => type === "foo")({ type: "foo" }) ).toBe(true); + expect( + withProp(props => props.type, type => type === "bar")({ + type: props => props.foo, + foo: "bar" + }) + ).toBe(true); + expect( + withProp(prop("type"), type => type === "bar")({ + type: prop("foo"), + foo: "bar" + }) + ).toBe(true); }); diff --git a/yarn.lock b/yarn.lock index a61872c..b5248cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -992,6 +992,13 @@ pirates "^4.0.0" source-map-support "^0.5.9" +"@babel/runtime@^7.0.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.2.0.tgz#b03e42eeddf5898e00646e4c840fa07ba8dcad7f" + integrity sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg== + dependencies: + regenerator-runtime "^0.12.0" + "@babel/template@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0.tgz#c2bc9870405959c89a9c814376a2ecb247838c80" @@ -5984,6 +5991,13 @@ pn@^1.1.0: resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== +polished@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/polished/-/polished-2.3.1.tgz#557ea07e5bcb1b9c7d71935a01d0315831234d48" + integrity sha512-0mGyvVrHVRN92wfohriBWmMF4JLEnGgpZbpwPrNDhpB8NrX6lYI8GGWXEfrmrF+ZXg52Jkwd+D0rxViOvXM9RQ== + dependencies: + "@babel/runtime" "^7.0.0" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -6322,6 +6336,11 @@ regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== +regenerator-runtime@^0.12.0: + version "0.12.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" + integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== + regenerator-transform@^0.13.3: version "0.13.3" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb"