diff --git a/src/deepPalette.js b/src/deepPalette.js new file mode 100644 index 0000000..5694890 --- /dev/null +++ b/src/deepPalette.js @@ -0,0 +1,46 @@ +// @flow +import { + resolvePaletteKey, + resolvePaletteTone, + resolvePaletteDefaultValue, + toArray, + clamp, + resolveValue +} from "./utils"; +import type { PaletteProps } from "."; + +/** + * Returns `props.theme.palette[key || props.palette][tone || props.tone || 0]` or `defaultValue`. + * Supports deep resolving. + * @see {@link #palette palette} + */ +const deepPalette = ( + keyOrTone?: string | number, + toneOrDefaultValue?: any, + defaultValue?: any +) => (props: PaletteProps) => { + const key = resolvePaletteKey(keyOrTone, props); + const tone = resolvePaletteTone(keyOrTone, toneOrDefaultValue, props); + const finalDefaultValue = resolvePaletteDefaultValue( + toneOrDefaultValue, + tone, + defaultValue + ); + + if (!props.theme.palette || !props.theme.palette[key]) { + return finalDefaultValue; + } + + const tones = toArray(resolveValue(props.theme.palette[key], props)); + + if (tone < 0) { + return resolveValue( + tones[clamp(tones.length + tone, 0, tones.length - 1)], + props + ); + } + + return resolveValue(tones[clamp(tone, 0, tones.length - 1)], props); +}; + +export default deepPalette; diff --git a/src/deepProp.js b/src/deepProp.js new file mode 100644 index 0000000..e5640f6 --- /dev/null +++ b/src/deepProp.js @@ -0,0 +1,26 @@ +// @flow +import { get, resolveValue } from "./utils"; +import type { PropsFn } from "."; + +/** + * Returns the value of `props[path]` or `defaultValue`. Supports deep + * resolving. + * @see {@link #prop prop} + */ +const deepProp = (path: string, defaultValue?: any): PropsFn => ( + props = {} +) => { + if (typeof props[path] !== "undefined") { + return resolveValue(props[path], props); + } + + const object = get(props, path); + + if (typeof object !== "undefined") { + return resolveValue(object, props); + } + + return defaultValue; +}; + +export default deepProp; diff --git a/src/deepTheme.js b/src/deepTheme.js new file mode 100644 index 0000000..ed98638 --- /dev/null +++ b/src/deepTheme.js @@ -0,0 +1,14 @@ +// @flow +import deepProp from "./deepProp"; +import type { PropsWithTheme } from "."; + +/** + * Same as `deepProp`, except that it returns `props.theme[path]` instead of + * `props[path]`. + * @see {@link #theme theme} + */ +const deepTheme = (path: string, defaultValue?: any) => ( + props: PropsWithTheme +) => deepProp(`theme.${path}`, defaultValue)(props); + +export default deepTheme; diff --git a/src/index.js b/src/index.js index 26f5582..5cb4beb 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,6 @@ +export deepPalette from "./deepPalette"; +export deepProp from "./deepProp"; +export deepTheme from "./deepTheme"; export ifNotProp from "./ifNotProp"; export ifProp from "./ifProp"; export prop from "./prop"; diff --git a/src/index.js.flow b/src/index.js.flow index 01dd043..850ccd3 100644 --- a/src/index.js.flow +++ b/src/index.js.flow @@ -13,3 +13,8 @@ export type Needle = string | Function; export type PropsFn = (props?: Object) => any; export type PropsWithTheme = { theme: { [x: string]: any } }; + +export type PaletteProps = PropsWithTheme & { + palette?: string, + tone?: number +}; diff --git a/src/palette.js b/src/palette.js index 2ccd727..bb70e76 100644 --- a/src/palette.js +++ b/src/palette.js @@ -1,18 +1,12 @@ // @flow -import type { PropsWithTheme } from "."; - -type Props = PropsWithTheme & { - palette?: string, - tone?: number -}; - -const toArray = (arg: any) => (Array.isArray(arg) ? arg : [arg]); - -const clamp = (number: number, min: number, max: number) => { - if (number < min) return min; - if (number > max) return max; - return number; -}; +import { + resolvePaletteKey, + resolvePaletteTone, + resolvePaletteDefaultValue, + toArray, + clamp +} from "./utils"; +import type { PaletteProps } from "."; /** * Returns `props.theme.palette[key || props.palette][tone || props.tone || 0]` or `defaultValue`. @@ -45,16 +39,14 @@ const palette = ( keyOrTone?: string | number, toneOrDefaultValue?: any, defaultValue?: any -) => (props: Props) => { - const key = typeof keyOrTone === "string" ? keyOrTone : props.palette; - const tone = - typeof keyOrTone === "number" - ? keyOrTone - : typeof toneOrDefaultValue === "number" - ? toneOrDefaultValue - : props.tone || 0; - const finalDefaultValue = - toneOrDefaultValue !== tone ? toneOrDefaultValue : defaultValue; +) => (props: PaletteProps) => { + const key = resolvePaletteKey(keyOrTone, props); + const tone = resolvePaletteTone(keyOrTone, toneOrDefaultValue, props); + const finalDefaultValue = resolvePaletteDefaultValue( + toneOrDefaultValue, + tone, + defaultValue + ); if (!props.theme.palette || !props.theme.palette[key]) { return finalDefaultValue; diff --git a/src/prop.js b/src/prop.js index c275962..2e1e7dc 100644 --- a/src/prop.js +++ b/src/prop.js @@ -1,4 +1,5 @@ // @flow +import { get } from "./utils"; import type { PropsFn } from "."; /** @@ -16,20 +17,10 @@ const prop = (path: string, defaultValue?: any): PropsFn => (props = {}) => { return props[path]; } - if (path && path.indexOf(".") > 0) { - const paths = path.split("."); - const { length } = paths; - let object = props[paths[0]]; - let index = 1; + const object = get(props, path); - while (object != null && index < length) { - object = object[paths[index]]; - index += 1; - } - - if (typeof object !== "undefined") { - return object; - } + if (typeof object !== "undefined") { + return object; } return defaultValue; diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..993de61 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,56 @@ +// @flow +import type { PaletteProps } from "."; + +export function get(props: {}, path: string) { + if (path && path.indexOf(".") > 0) { + const paths = path.split("."); + const { length } = paths; + let object = props[paths[0]]; + let index = 1; + + while (object != null && index < length) { + object = object[paths[index]]; + index += 1; + } + + return object; + } + return undefined; +} + +export function resolveValue(value: any, props: Object) { + if (typeof value === "function") { + return resolveValue(value(props), props); + } + return value; +} + +export const toArray = (arg: any) => (Array.isArray(arg) ? arg : [arg]); + +export const clamp = (number: number, min: number, max: number) => { + if (number < min) return min; + if (number > max) return max; + return number; +}; + +export const resolvePaletteKey = ( + keyOrTone?: string | number, + props: PaletteProps +) => (typeof keyOrTone === "string" ? keyOrTone : props.palette); + +export const resolvePaletteTone = ( + keyOrTone?: string | number, + toneOrDefaultValue?: any, + props: PaletteProps +) => + typeof keyOrTone === "number" + ? keyOrTone + : typeof toneOrDefaultValue === "number" + ? toneOrDefaultValue + : props.tone || 0; + +export const resolvePaletteDefaultValue = ( + toneOrDefaultValue?: any, + tone: number, + defaultValue?: any +) => (toneOrDefaultValue !== tone ? toneOrDefaultValue : defaultValue); diff --git a/test/deepPalette.test.js b/test/deepPalette.test.js new file mode 100644 index 0000000..9bcb556 --- /dev/null +++ b/test/deepPalette.test.js @@ -0,0 +1,48 @@ +import deepPalette from "../src/deepPalette"; + +const theme = { + palette: { + primary: [() => "primary0", () => "primary1", () => "primary2"], + secondary: () => "secondary0" + } +}; + +test("deep only tone", () => { + expect(deepPalette()({ theme })).toBeUndefined(); + expect(deepPalette(0)({ theme })).toBeUndefined(); + expect(deepPalette()({ theme, palette: "primary" })).toBe("primary0"); + expect(deepPalette()({ theme, palette: "primary", tone: 2 })).toBe( + "primary2" + ); + expect(deepPalette(0)({ theme, palette: "primary" })).toBe("primary0"); + expect(deepPalette(1)({ theme, palette: "primary" })).toBe("primary1"); + expect(deepPalette(-1)({ theme, palette: "primary" })).toBe("primary2"); + expect(deepPalette(-5)({ theme, palette: "primary" })).toBe("primary0"); + expect(deepPalette(5)({ theme, palette: "primary" })).toBe("primary2"); + expect(deepPalette(0)({ theme, palette: "secondary" })).toBe("secondary0"); + expect(deepPalette(1)({ theme, palette: "secondary" })).toBe("secondary0"); + expect(deepPalette(0)({ theme, palette: "other" })).toBeUndefined(); + expect(deepPalette(1, "foo")({ theme })).toBe("foo"); + expect(deepPalette(1, "foo")({ theme, palette: "other" })).toBe("foo"); +}); + +test("deep palette and tone", () => { + expect(deepPalette("primary")({ theme })).toBe("primary0"); + expect(deepPalette("primary")({ theme, tone: 2 })).toBe("primary2"); + expect(deepPalette("primary", 0)({ theme })).toBe("primary0"); + expect(deepPalette("primary", 5)({ theme })).toBe("primary2"); + expect(deepPalette("primary", -1)({ theme })).toBe("primary2"); + expect(deepPalette("primary", -5)({ theme })).toBe("primary0"); + expect(deepPalette("secondary", 0)({ theme })).toBe("secondary0"); + expect(deepPalette("secondary", 1)({ theme })).toBe("secondary0"); + expect(deepPalette("secondary", 1, "foo")({ theme })).toBe("secondary0"); + expect(deepPalette("other", 1)({ theme })).toBeUndefined(); + expect(deepPalette("other", "foo")({ theme })).toBe("foo"); + expect(deepPalette("other", 1, "foo")({ theme })).toBe("foo"); + expect( + deepPalette("other", 1)({ theme, palette: "primary" }) + ).toBeUndefined(); + expect(deepPalette("other", 1, "foo")({ theme, palette: "primary" })).toBe( + "foo" + ); +}); diff --git a/test/deepProp.test.js b/test/deepProp.test.js new file mode 100644 index 0000000..c2fcc02 --- /dev/null +++ b/test/deepProp.test.js @@ -0,0 +1,30 @@ +import deepProp from "../src/deepProp"; + +test("string argument", () => { + expect(deepProp("color")()).toBeUndefined(); + expect(deepProp("color")({})).toBeUndefined(); + expect(deepProp("color")({ color: () => "red" })).toBe("red"); + expect(deepProp("color")({ color: props => props.bg, bg: "red" })).toBe( + "red" + ); +}); + +test("deep string argument", () => { + expect(deepProp("color.primary")()).toBeUndefined(); + expect(deepProp("color.primary")({})).toBeUndefined(); + expect(deepProp("color.primary")({ color: { primary: () => "red" } })).toBe( + "red" + ); + expect( + deepProp("color.primary")({ + color: { primary: deepProp("color.secondary"), secondary: "blue" } + }) + ).toBe("blue"); +}); + +test("defaultValue", () => { + expect(deepProp("color", "red")()).toBe("red"); + expect(deepProp("color", "red")({})).toBe("red"); + expect(deepProp("color", "red")({ color: () => "blue" })).toBe("blue"); + expect(deepProp("color.primary", "red")({})).toBe("red"); +}); diff --git a/test/deepTheme.test.js b/test/deepTheme.test.js new file mode 100644 index 0000000..0387bd5 --- /dev/null +++ b/test/deepTheme.test.js @@ -0,0 +1,32 @@ +import deepTheme from "../src/deepTheme"; + +test("string argument", () => { + expect(deepTheme("color")({ theme: { color: () => "red" } })).toBe("red"); + expect( + deepTheme("color")({ + theme: { color: props => props.theme.bg, bg: "blue" } + }) + ).toBe("blue"); + expect( + deepTheme("color")({ theme: { color: deepTheme("bg"), bg: "blue" } }) + ).toBe("blue"); + expect( + deepTheme("linkColor")({ + theme: { + blue: "#007bff", + primary: deepTheme("blue"), + linkColor: deepTheme("primary") + } + }) + ).toBe("#007bff"); +}); + +test("deep string argument", () => { + expect( + deepTheme("color.primary")({ + theme: { + color: { primary: deepTheme("color.secondary"), secondary: "blue" } + } + }) + ).toBe("blue"); +}); diff --git a/types/index.d.ts b/types/index.d.ts index fa1e078..9acb81e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,16 +1,25 @@ type Needle = string | ((props?: Props) => any); -export function prop( - path: string, - defaultValue?: T -): (props?: Props) => Props[keyof Props] | T; +interface PropFunction { + (path: string, defaultValue?: T): ( + props?: Props + ) => Props[keyof Props] | T; +} + +export const prop: PropFunction; +export const deepProp: PropFunction; + +interface ThemeFunction { + (path: string, defaultValue?: T): < + Props, + Theme extends { [key: string]: any } + >( + props: Props & { theme: Theme } + ) => Theme[keyof Theme] | T; +} -export function theme( - path: string, - defaultValue?: T -): ( - props: Props & { theme: Theme } -) => Theme[keyof Theme] | T; +export const theme: ThemeFunction; +export const deepTheme: ThemeFunction; type ThemeWithPalette = { palette?: { [key: string]: any }; @@ -49,6 +58,7 @@ interface Palette { } export const palette: Palette; +export const deepPalette: Palette; export function ifProp( test: Needle | Needle[] | { [key: string]: any },