From 826adb6bb5ef58b6eca39dcdd39a90e931709ca3 Mon Sep 17 00:00:00 2001 From: Haew Date: Sun, 1 Mar 2020 05:52:50 +0100 Subject: [PATCH 1/9] Implement CSS variables composable --- packages/web/src/web/cssVariables.ts | 143 +++++++++++++++++++++++++++ packages/web/src/web/index.ts | 1 + 2 files changed, 144 insertions(+) create mode 100644 packages/web/src/web/cssVariables.ts diff --git a/packages/web/src/web/cssVariables.ts b/packages/web/src/web/cssVariables.ts new file mode 100644 index 000000000..66477a851 --- /dev/null +++ b/packages/web/src/web/cssVariables.ts @@ -0,0 +1,143 @@ +import { Ref, ref, onUnmounted } from "@vue/composition-api"; + +/** + * The values a CSS variable can contain. + */ +export type CssVariable = string | null; + +/** + * Additional methods. + */ +export interface CssVariablesMethods { + /** + * Stops the observation. + */ + stop: () => void; + + /** + * Resumes the observation. + */ + resume: () => void; + + /** + * Gets the current value of the given CSS variable name for the given element. + * + * @param name The CSS variable name. + * @param element The element to get the variable for. + */ + get: (name: string, element: HTMLElement) => CssVariable; + + /** + * Sets the value of the given CSS variable for the given element. + * + * @param name The CSS variable name without dashes. + * @param value The CSS variable value. + * @param element The element to set the variable for. + */ + set: (name: string, value: CssVariable, element: HTMLElement) => void; +} + +/** + * Returns object. Consists of multiple pairs of key/CSS variable value references and the additional methods. + */ +export type UseCssVariables = CssVariableObject & CssVariablesMethods; + +/** + * The CSS variable return object. Each value is a `Ref` of a CSS variable's contents. + */ +export type CssVariableObject = Record>; + +/** + * A CSS Variable configuration object. Each value must be a CSS variable without the leading dashes. + */ +export type CssVariableConfigurationObject = Record; + +/** + * Gets the current value of the given CSS variable name for the given element. + * + * @param element The element to get the variable for. + * @param name The CSS variable name. + */ +export function getVariableFor( + name: string, + element: HTMLElement = document.documentElement +): CssVariable { + const result = getComputedStyle(element).getPropertyValue(`--${name}`); + return result ? result.trim() : null; +} + +/** + * Sets the value of the given CSS variable for the given element. + * + * @param element The element to set the variable for. + * @param name The CSS variable name without dashes. + * @param value The CSS variable value. + */ +export function setVariableFor( + name: string, + value: CssVariable, + element: HTMLElement = document.documentElement +): void { + element.style.setProperty(`--${name}`, value); +} + +/** + * + * @param variables + * @param element + */ +export function useCssVariables( + variables: Record, + element: HTMLElement = document.documentElement +): UseCssVariables { + // Reactive property to tell if the observer is listening + const listening = ref(true); + + // Stores the results by reference. + const result: CssVariableObject = {} as any; + + // Assign variables to the result record. + for (const key in variables) { + result[key] = ref(null); + } + + // Sets up the observer. + const observer = new MutationObserver(() => { + // Each time an observation has been made, + // we look up for each CSS variable and update their values. + for (const key in variables) { + result[key].value = getVariableFor(variables[key], element); + } + }); + + // Sets the `stop` method. + const stop = () => { + observer.disconnect(); + listening.value = false; + }; + + // Sets the `start` method. + const start = () => { + observer.observe(element, { + attributes: true, + childList: true, + subtree: true + }); + listening.value = true; + }; + + // Starts observing + start(); + + // Stops on destroy + onUnmounted(() => stop()); + + return { + ...result, + get: getVariableFor, + set: setVariableFor, + resume: start, + stop, + listening + }; +} diff --git a/packages/web/src/web/index.ts b/packages/web/src/web/index.ts index 02aa86f50..17482bf13 100644 --- a/packages/web/src/web/index.ts +++ b/packages/web/src/web/index.ts @@ -7,3 +7,4 @@ export * from "./pageVisibility"; export * from "./language"; export * from "./broadcastChannel"; export * from "./geolocation"; +export * from "./cssVariables"; From 8ed35be773c9962e0c3d4de76d272d823ebfaf82 Mon Sep 17 00:00:00 2001 From: Haew Date: Sun, 1 Mar 2020 05:54:53 +0100 Subject: [PATCH 2/9] Add rookie tests --- .../web/__tests__/web/cssVariables.spec.ts | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 packages/web/__tests__/web/cssVariables.spec.ts diff --git a/packages/web/__tests__/web/cssVariables.spec.ts b/packages/web/__tests__/web/cssVariables.spec.ts new file mode 100644 index 000000000..4a8587ed1 --- /dev/null +++ b/packages/web/__tests__/web/cssVariables.spec.ts @@ -0,0 +1,160 @@ +import { + getVariableFor, + setVariableFor, + useCssVariables, + UseCssVariables +} from "../../src"; +import { Vue } from "../utils"; + +describe("CSS variables", () => { + let callback: () => void; + const _mutationObserver = window.MutationObserver; + const observeFn = jest.fn().mockImplementation(() => { + callback(); + }); + const disconnectFn = jest.fn(); + const constructorFn = jest.fn(); + + beforeAll(() => { + class MutationObserver { + constructor(...args: any[]) { + constructorFn(...args); + } + observe = observeFn; + disconnect = disconnectFn; + } + Object.defineProperty(window, "MutationObserver", { + writable: true, + configurable: true, + value: MutationObserver + }); + Object.defineProperty(global, "MutationObserver", { + writable: true, + configurable: true, + value: MutationObserver + }); + }); + + afterAll(() => { + Object.defineProperty(window, "MutationObserver", { + writable: true, + configurable: true, + value: _mutationObserver + }); + Object.defineProperty(global, "MutationObserver", { + writable: true, + configurable: true, + value: _mutationObserver + }); + }); + + beforeEach(() => { + observeFn.mockReset(); + constructorFn.mockReset(); + disconnectFn.mockReset(); + }); + + it("sohuld retrieve the correct value for a defined CSS variable", async () => { + const element = document.createElement("div"); + element.style.setProperty("--variable-name", "#fff"); + + const definedVariable = getVariableFor("variable-name", element); + expect(definedVariable).toBe("#fff"); + + const undefinedVariable = getVariableFor("undefined-variable", element); + expect(undefinedVariable).toBeNull(); + }); + + it("should retrieve the correct value for an undefined CSS variable", async () => { + const element = document.createElement("div"); + const undefinedVariable = getVariableFor("undefined-variable", element); + expect(undefinedVariable).toBeNull(); + }); + + it("should set the value of a CSS variable on an element", async () => { + const element = document.createElement("div"); + + setVariableFor("variable-name", "#fff", element); + + expect(element.style.getPropertyValue("--variable-name")).toBe("#fff"); + }); + + it("should know if the observer is no longer listening", async () => { + let variables: UseCssVariables> = {} as any; + + new Vue({ + template: "
", + setup() { + variables = useCssVariables({}); + } + }).$mount(); + + let { listening, stop, resume } = variables; + + expect(listening).toMatchObject({ + value: true + }); + + stop(); + + expect(listening).toMatchObject({ + value: false + }); + + resume(); + + expect(listening).toMatchObject({ + value: true + }); + }); + + it("should automatically start the observer and stops it on destroy", async () => { + const vm = new Vue({ + template: "
", + setup() { + useCssVariables({ + test: "variable-name" + }); + } + }); + + vm.$mount(); + + expect(observeFn).toHaveBeenCalledTimes(1); + expect(disconnectFn).toHaveBeenCalledTimes(0); + + vm.$destroy(); + + expect(observeFn).toHaveBeenCalledTimes(1); + expect(disconnectFn).toHaveBeenCalledTimes(1); + }); + + it("should update properties thanks to the observer", async () => { + const element = document.createElement("div"); + let variables: UseCssVariables> = {} as any; + + new Vue({ + template: "
", + setup() { + variables = useCssVariables( + { + test: "variable-name" + }, + element + ); + } + }).$mount(); + + callback = () => { + variables.test.value = getVariableFor("variable-name", element); + }; + + expect(variables.test.value).toBeNull(); + + // Simulate an observation + setVariableFor("variable-name", "red", element); + callback(); + + expect(variables.test.value).toBe("red"); + }); +}); From 5fe40236ab822a3f1c393fea67ca69186653f1ec Mon Sep 17 00:00:00 2001 From: Haew Date: Sun, 1 Mar 2020 06:04:37 +0100 Subject: [PATCH 3/9] Update documentation --- .../components/CssVariablesExample.vue | 78 ++ docs/.vuepress/config.js | 3 +- docs/README.md | 1 + docs/api/web.api.md | 770 +++++++++++------- docs/composable/web/cssVariables.md | 127 +++ packages/vue-composable/README.md | 1 + packages/web/README.md | 1 + readme.md | 1 + 8 files changed, 702 insertions(+), 280 deletions(-) create mode 100644 docs/.vuepress/components/CssVariablesExample.vue create mode 100644 docs/composable/web/cssVariables.md diff --git a/docs/.vuepress/components/CssVariablesExample.vue b/docs/.vuepress/components/CssVariablesExample.vue new file mode 100644 index 000000000..62a58183c --- /dev/null +++ b/docs/.vuepress/components/CssVariablesExample.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index e195aff18..e68d9eeb1 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -166,7 +166,8 @@ module.exports = { ["composable/web/pageVisibility", "PageVisibilityAPI"], ["composable/web/language", "Language"], ["composable/web/broadcastChannel", "BroadcastChannel API"], - ["composable/web/geolocation", "Geolocation API"] + ["composable/web/geolocation", "Geolocation API"], + ["composable/web/cssVariables", "CSS variables"] ] }, { diff --git a/docs/README.md b/docs/README.md index 4c67382a0..44d1ef1f3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -102,6 +102,7 @@ Check out the [examples folder](examples) or start hacking on [codesandbox](http - [Language](composable/web/language) - reactive `NavigatorLanguage` - [BroadcastChannel](composable/web/broadcastChannel) - reactive `BroadcastChannel API` - [Geolocation](composable/web/geolocation) - reactive `Geolocation API` +- [CSS variables](composable/web/cssVariables) - reactive `CSS variables` ### Validation diff --git a/docs/api/web.api.md b/docs/api/web.api.md index b047b898c..a1b4c0e41 100644 --- a/docs/api/web.api.md +++ b/docs/api/web.api.md @@ -3,88 +3,115 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts - -import { Ref } from '@vue/composition-api'; -import { RefElement } from '@vue-composable/core'; -import { RefTyped } from '@vue-composable/core'; +import { Ref } from "@vue/composition-api"; +import { RefElement } from "@vue-composable/core"; +import { RefTyped } from "@vue-composable/core"; // @public (undocumented) export type BreakpointObject = Record; // @public (undocumented) -export type BreakpointReturn = Record> & BreakpointReturnObject; +export type BreakpointReturn = Record> & + BreakpointReturnObject; // @public (undocumented) export interface BreakpointReturnObject { - // (undocumented) - current: Ref; - // (undocumented) - remove: RemoveEventFunction; + // (undocumented) + current: Ref; + // (undocumented) + remove: RemoveEventFunction; } // @public (undocumented) export interface BroadcastMessageEvent extends MessageEvent { - // (undocumented) - readonly data: T; + // (undocumented) + readonly data: T; +} + +// @public +export type CssVariable = string | null; + +// @public +export type CssVariableConfigurationObject = Record; + +// @public +export type CssVariableObject = Record>; + +// @public +export interface CssVariablesMethods { + get: (name: string, element: HTMLElement) => CssVariable; + resume: () => void; + set: (name: string, value: CssVariable, element: HTMLElement) => void; + stop: () => void; } // @public (undocumented) export interface DefaultTailwindBreakpoints { - // (undocumented) - lg: 1024; - // (undocumented) - md: 768; - // (undocumented) - sm: 640; - // (undocumented) - xl: 1280; + // (undocumented) + lg: 1024; + // (undocumented) + md: 768; + // (undocumented) + sm: 640; + // (undocumented) + xl: 1280; } // Warning: (ae-forgotten-export) The symbol "TailwindConfigEmpty" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type ExtractTailwindScreens = keyof T["theme"]["screens"] extends never ? DefaultTailwindBreakpoints : T["theme"]["screens"]; +export type ExtractTailwindScreens< + T extends TailwindConfigEmpty +> = keyof T["theme"]["screens"] extends never + ? DefaultTailwindBreakpoints + : T["theme"]["screens"]; // @public (undocumented) export interface GeolocationOptions { - immediate?: boolean; + immediate?: boolean; } +// @public +export function getVariableFor( + name: string, + element?: HTMLElement +): CssVariable; + // @public (undocumented) export interface IntersectionObserverOptions { - // (undocumented) - root?: RefTyped | null; - // (undocumented) - rootMargin?: RefTyped | string; - // (undocumented) - threshold?: RefTyped | number | number[]; + // (undocumented) + root?: RefTyped | null; + // (undocumented) + rootMargin?: RefTyped | string; + // (undocumented) + threshold?: RefTyped | number | number[]; } // @public (undocumented) export interface IntersectionObserverResult { - // (undocumented) - disconnect: () => void; - // (undocumented) - elements: Ref; - // (undocumented) - readonly isIntersecting: Ref; - // (undocumented) - observe: (el: RefTyped) => void; - // (undocumented) - supported: boolean; - // (undocumented) - unobserve: (el: RefTyped) => void; + // (undocumented) + disconnect: () => void; + // (undocumented) + elements: Ref; + // (undocumented) + readonly isIntersecting: Ref; + // (undocumented) + observe: (el: RefTyped) => void; + // (undocumented) + supported: boolean; + // (undocumented) + unobserve: (el: RefTyped) => void; } // @public (undocumented) export interface LocalStorageReturn { - clear: () => void; - remove: () => void; - setSync: (sync: boolean) => void; - // (undocumented) - storage: Ref; - // (undocumented) - supported: boolean; + clear: () => void; + remove: () => void; + setSync: (sync: boolean) => void; + // (undocumented) + storage: Ref; + // (undocumented) + supported: boolean; } // @public (undocumented) @@ -92,112 +119,144 @@ export type LocalStorageTyped = string; // @public (undocumented) export interface MouseMoveResult { - // (undocumented) - mouseX: Ref; - // (undocumented) - mouseY: Ref; - // (undocumented) - remove: RemoveEventFunction; + // (undocumented) + mouseX: Ref; + // (undocumented) + mouseY: Ref; + // (undocumented) + remove: RemoveEventFunction; } // @public (undocumented) export interface NetworkInformation { - // Warning: (ae-forgotten-export) The symbol "NetworkInformationEventMap" needs to be exported by the entry point index.d.ts - // - // (undocumented) - addEventListener(type: K, listener: (this: NetworkInformation, ev: NetworkInformationEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - // (undocumented) - addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - // (undocumented) - readonly downlink: number; - // (undocumented) - readonly downlinkMax: number; - // Warning: (ae-forgotten-export) The symbol "NetworkInformationEffectiveType" needs to be exported by the entry point index.d.ts - // - // (undocumented) - readonly effectiveType: NetworkInformationEffectiveType; - // (undocumented) - onchange: (this: NetworkInformation, ev: Event) => void; - // (undocumented) - removeEventListener(type: K, listener: (this: NetworkInformation, ev: NetworkInformationEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - // (undocumented) - removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; - // (undocumented) - readonly rtt: number; - // (undocumented) - readonly saveData: Boolean; - // Warning: (ae-forgotten-export) The symbol "NetworkInformationType" needs to be exported by the entry point index.d.ts - // - // (undocumented) - readonly type: NetworkInformationType; + // Warning: (ae-forgotten-export) The symbol "NetworkInformationEventMap" needs to be exported by the entry point index.d.ts + // + // (undocumented) + addEventListener( + type: K, + listener: ( + this: NetworkInformation, + ev: NetworkInformationEventMap[K] + ) => any, + options?: boolean | AddEventListenerOptions + ): void; + // (undocumented) + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ): void; + // (undocumented) + readonly downlink: number; + // (undocumented) + readonly downlinkMax: number; + // Warning: (ae-forgotten-export) The symbol "NetworkInformationEffectiveType" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly effectiveType: NetworkInformationEffectiveType; + // (undocumented) + onchange: (this: NetworkInformation, ev: Event) => void; + // (undocumented) + removeEventListener( + type: K, + listener: ( + this: NetworkInformation, + ev: NetworkInformationEventMap[K] + ) => any, + options?: boolean | EventListenerOptions + ): void; + // (undocumented) + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions + ): void; + // (undocumented) + readonly rtt: number; + // (undocumented) + readonly saveData: Boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkInformationType" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly type: NetworkInformationType; } // @public (undocumented) -export function refShared(defaultValue?: RefTyped, id?: string): Ref>; +export function refShared( + defaultValue?: RefTyped, + id?: string +): Ref>; // @public (undocumented) -export type RefSharedMessage = RefSharedMessageInit | RefSharedMessageSync | RefSharedMessageLeave | RefSharedMessageUpdate | RefSharedMessageSetMind | RefSharedMessagePing | RefSharedMessagePong; +export type RefSharedMessage = + | RefSharedMessageInit + | RefSharedMessageSync + | RefSharedMessageLeave + | RefSharedMessageUpdate + | RefSharedMessageSetMind + | RefSharedMessagePing + | RefSharedMessagePong; // @public (undocumented) export type RefSharedMessageInit = { - type: RefSharedMessageType.INIT; + type: RefSharedMessageType.INIT; }; // @public (undocumented) export type RefSharedMessageLeave = { - type: RefSharedMessageType.LEAVE; - id: number; + type: RefSharedMessageType.LEAVE; + id: number; }; // @public (undocumented) export type RefSharedMessagePing = { - type: RefSharedMessageType.PING; - id: number; + type: RefSharedMessageType.PING; + id: number; }; // @public (undocumented) export type RefSharedMessagePong = { - type: RefSharedMessageType.PONG; - id: number; + type: RefSharedMessageType.PONG; + id: number; }; // @public (undocumented) export type RefSharedMessageSetMind = { - type: RefSharedMessageType.SET_MIND; - mind: SharedRefMind; - id: number; + type: RefSharedMessageType.SET_MIND; + mind: SharedRefMind; + id: number; }; // @public (undocumented) export type RefSharedMessageSync = { - type: RefSharedMessageType.SYNC; - value: T; - mind: SharedRefMind; + type: RefSharedMessageType.SYNC; + value: T; + mind: SharedRefMind; }; // @public (undocumented) export const enum RefSharedMessageType { - // (undocumented) - INIT = 0, - // (undocumented) - LEAVE = 4, - // (undocumented) - PING = 5, - // (undocumented) - PONG = 6, - // (undocumented) - SET_MIND = 3, - // (undocumented) - SYNC = 1, - // (undocumented) - UPDATE = 2 + // (undocumented) + INIT = 0, + // (undocumented) + LEAVE = 4, + // (undocumented) + PING = 5, + // (undocumented) + PONG = 6, + // (undocumented) + SET_MIND = 3, + // (undocumented) + SYNC = 1, + // (undocumented) + UPDATE = 2 } // @public (undocumented) export type RefSharedMessageUpdate = { - type: RefSharedMessageType.UPDATE; - value: T; - mind: SharedRefMind; + type: RefSharedMessageType.UPDATE; + value: T; + mind: SharedRefMind; }; // @public (undocumented) @@ -205,42 +264,53 @@ export type RemoveEventFunction = () => void; // @public (undocumented) export interface ResizeResult { - // (undocumented) - height: Ref; - // (undocumented) - remove: RemoveEventFunction; - // (undocumented) - width: Ref; + // (undocumented) + height: Ref; + // (undocumented) + remove: RemoveEventFunction; + // (undocumented) + width: Ref; } // @public (undocumented) export interface ScrollResult { - // (undocumented) - remove: RemoveEventFunction; - // (undocumented) - scrollLeft: Ref; - // (undocumented) - scrollLeftTo: (x: number) => void; - // (undocumented) - scrollTo: Element["scrollTo"]; - // (undocumented) - scrollTop: Ref; - // (undocumented) - scrollTopTo: (y: number) => void; + // (undocumented) + remove: RemoveEventFunction; + // (undocumented) + scrollLeft: Ref; + // (undocumented) + scrollLeftTo: (x: number) => void; + // (undocumented) + scrollTo: Element["scrollTo"]; + // (undocumented) + scrollTop: Ref; + // (undocumented) + scrollTopTo: (y: number) => void; } // @public (undocumented) -export function setBreakpointTailwindCSS(tailwindConfig: T): BreakpointReturn>; +export function setBreakpointTailwindCSS( + tailwindConfig: T +): BreakpointReturn>; // @public (undocumented) -export function setBreakpointTailwindCSS(breakpoints: T): BreakpointReturn; +export function setBreakpointTailwindCSS( + breakpoints: T +): BreakpointReturn; + +// @public +export function setVariableFor( + name: string, + value: CssVariable, + element?: HTMLElement +): void; // @public (undocumented) export const enum SharedRefMind { - // (undocumented) - HIVE = 0, - // (undocumented) - MASTER = 1 + // (undocumented) + HIVE = 0, + // (undocumented) + MASTER = 1 } // @public (undocumented) @@ -248,14 +318,16 @@ export function storageAvailable(storage?: Storage): boolean; // @public (undocumented) export interface StorageSerializer { - // (undocumented) - parse(data: string): T; - // (undocumented) - stringify(item: T): string; + // (undocumented) + parse(data: string): T; + // (undocumented) + stringify(item: T): string; } // @public (undocumented) -export function useBreakpoint(breakpoints: Record): BreakpointReturn; +export function useBreakpoint( + breakpoints: Record +): BreakpointReturn; // Warning: (ae-forgotten-export) The symbol "ChromeBreakpoint" needs to be exported by the entry point index.d.ts // @@ -263,109 +335,190 @@ export function useBreakpoint(breakpoints: Record; // @public (undocumented) -export function useBreakpointTailwindCSS(tailwindConfig: T): BreakpointReturn>; +export function useBreakpointTailwindCSS( + tailwindConfig: T +): BreakpointReturn>; // @public (undocumented) -export function useBreakpointTailwindCSS(): BreakpointReturn>; +export function useBreakpointTailwindCSS< + T extends TailwindConfigEmpty +>(): BreakpointReturn>; // @public (undocumented) -export function useBreakpointTailwindCSS(): BreakpointReturn; +export function useBreakpointTailwindCSS(): BreakpointReturn< + DefaultTailwindBreakpoints +>; // @public (undocumented) -export function useBreakpointTailwindCSS(): BreakpointReturn; +export function useBreakpointTailwindCSS< + T extends BreakpointObject +>(): BreakpointReturn; // @public (undocumented) -export function useBroadcastChannel(name: string, onBeforeClose?: Function): { - supported: boolean; - data: import("@vue/composition-api").Ref; - messageEvent: import("@vue/composition-api").Ref; - errorEvent: import("@vue/composition-api").Ref; - errored: import("@vue/composition-api").Ref; - isClosed: import("@vue/composition-api").Ref; - send: (data: T) => void; - close: Function; - addListener: (cb: (ev: BroadcastMessageEvent) => void, options?: boolean | AddEventListenerOptions | undefined) => void; +export function useBroadcastChannel( + name: string, + onBeforeClose?: Function +): { + supported: boolean; + data: import("@vue/composition-api").Ref; + messageEvent: import("@vue/composition-api").Ref; + errorEvent: import("@vue/composition-api").Ref; + errored: import("@vue/composition-api").Ref; + isClosed: import("@vue/composition-api").Ref; + send: (data: T) => void; + close: Function; + addListener: ( + cb: (ev: BroadcastMessageEvent) => void, + options?: boolean | AddEventListenerOptions | undefined + ) => void; }; -// @public (undocumented) -export function useEvent any; - removeEventListener: Function; -}, M, K extends keyof M>(el: RefTyped, name: K, listener: (this: T, ev: M[K]) => any): RemoveEventFunction; +// @public +export type UseCssVariables = CssVariableObject & CssVariablesMethods; // @public (undocumented) -export function useEvent any; - removeEventListener: Function; -}, M, K extends keyof M>(el: RefTyped, name: K, listener: (this: T, ev: M[K]) => any, options?: boolean | AddEventListenerOptions): RemoveEventFunction; +export function useCssVariables( + variables: Record, + element?: HTMLElement +): UseCssVariables; // @public (undocumented) -export function useEvent(el: RefTyped, name: K, listener: (this: Document, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): RemoveEventFunction; - -// @public (undocumented) -export function useEvent(el: RefTyped, name: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): RemoveEventFunction; - -// @public (undocumented) -export function useFetch(options?: UseFetchOptions & Partial, requestInit?: RequestInit): { - cancel: (message?: string | undefined) => void; - isCancelled: import("@vue/composition-api").Ref; - cancelledMessage: import("@vue/composition-api").Ref; - text: import("@vue/composition-api").Ref; - blob: import("@vue/composition-api").Ref; - json: import("@vue/composition-api").Ref; - jsonError: import("@vue/composition-api").Ref; - status: Readonly>; - statusText: Readonly>; - exec: (request: RequestInfo, init?: RequestInit | undefined) => Promise; - promise: import("@vue/composition-api").Ref | undefined>; /** - * @description if the value is `true` it will parse the `json` before resolving the promise - * @default true - */ - result: import("@vue/composition-api").Ref; - loading: import("@vue/composition-api").Ref; - error: import("@vue/composition-api").Ref; +export function useEvent< + T extends { + addEventListener: ( + name: string, + listener: EventListenerOrEventListenerObject + ) => any; + removeEventListener: Function; + }, + M, + K extends keyof M +>( + el: RefTyped, + name: K, + listener: (this: T, ev: M[K]) => any +): RemoveEventFunction; + +// @public (undocumented) +export function useEvent< + T extends { + addEventListener: ( + name: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ) => any; + removeEventListener: Function; + }, + M, + K extends keyof M +>( + el: RefTyped, + name: K, + listener: (this: T, ev: M[K]) => any, + options?: boolean | AddEventListenerOptions +): RemoveEventFunction; + +// @public (undocumented) +export function useEvent( + el: RefTyped, + name: K, + listener: (this: Document, ev: WindowEventMap[K]) => any, + options?: boolean | AddEventListenerOptions +): RemoveEventFunction; + +// @public (undocumented) +export function useEvent( + el: RefTyped, + name: K, + listener: (this: Document, ev: DocumentEventMap[K]) => any, + options?: boolean | AddEventListenerOptions +): RemoveEventFunction; + +// @public (undocumented) +export function useFetch( + options?: UseFetchOptions & Partial, + requestInit?: RequestInit +): { + cancel: (message?: string | undefined) => void; + isCancelled: import("@vue/composition-api").Ref; + cancelledMessage: import("@vue/composition-api").Ref; + text: import("@vue/composition-api").Ref; + blob: import("@vue/composition-api").Ref; + json: import("@vue/composition-api").Ref; + jsonError: import("@vue/composition-api").Ref; + status: Readonly>; + statusText: Readonly>; + exec: ( + request: RequestInfo, + init?: RequestInit | undefined + ) => Promise; + promise: import("@vue/composition-api").Ref | undefined> + /** + * @description if the value is `true` it will parse the `json` before resolving the promise + * @default true + */; + result: import("@vue/composition-api").Ref; + loading: import("@vue/composition-api").Ref; + error: import("@vue/composition-api").Ref; }; // @public (undocumented) export interface UseFetchOptions { - isJson?: boolean; - parseImmediate?: boolean; + isJson?: boolean; + parseImmediate?: boolean; } // @public (undocumented) -export function useGeolocation(options?: PositionOptions & GeolocationOptions): { - supported: boolean; - refresh: () => void; - error: import("@vue/composition-api").Ref; - timestamp: import("@vue/composition-api").Ref; - coords: import("@vue/composition-api").Ref; - highAccuracy: import("@vue/composition-api").Ref; +export function useGeolocation( + options?: PositionOptions & GeolocationOptions +): { + supported: boolean; + refresh: () => void; + error: import("@vue/composition-api").Ref; + timestamp: import("@vue/composition-api").Ref; + coords: import("@vue/composition-api").Ref; + highAccuracy: import("@vue/composition-api").Ref; }; // @public (undocumented) -export function useIntersectionObserver(el: RefElement, options?: RefTyped): IntersectionObserverResult; +export function useIntersectionObserver( + el: RefElement, + options?: RefTyped +): IntersectionObserverResult; // @public (undocumented) -export function useIntersectionObserver(options: RefTyped): IntersectionObserverResult; +export function useIntersectionObserver( + options: RefTyped +): IntersectionObserverResult; // @public (undocumented) export function useLanguage(): { - language: Ref; - languages: Ref; + language: Ref; + languages: Ref; }; // @public (undocumented) -export function useLocalStorage(key: string, defaultValue?: RefTyped, sync?: boolean): LocalStorageReturn; +export function useLocalStorage( + key: string, + defaultValue?: RefTyped, + sync?: boolean +): LocalStorageReturn; // @public (undocumented) -export function useLocalStorage(key: LocalStorageTyped | string, defaultValue?: RefTyped, sync?: boolean): LocalStorageReturn; +export function useLocalStorage( + key: LocalStorageTyped | string, + defaultValue?: RefTyped, + sync?: boolean +): LocalStorageReturn; // @public (undocumented) -export function useMatchMedia(query: string): { - supported: boolean; - mediaQueryList: Ref; - matches: Ref; - remove: () => void; +export function useMatchMedia( + query: string +): { + supported: boolean; + mediaQueryList: Ref; + matches: Ref; + remove: () => void; }; // Warning: (ae-forgotten-export) The symbol "NetworkInformationReturn" needs to be exported by the entry point index.d.ts @@ -375,33 +528,52 @@ export function useNetworkInformation(): NetworkInformationReturn; // @public (undocumented) export function useOnline(): { - supported: boolean; - online: Ref; + supported: boolean; + online: Ref; }; // @public (undocumented) -export function useOnMouseMove(el: RefTyped, wait: number): MouseMoveResult; +export function useOnMouseMove( + el: RefTyped, + wait: number +): MouseMoveResult; // @public (undocumented) -export function useOnMouseMove(el: RefTyped, options?: boolean | AddEventListenerOptions, wait?: number): MouseMoveResult; +export function useOnMouseMove( + el: RefTyped, + options?: boolean | AddEventListenerOptions, + wait?: number +): MouseMoveResult; // @public (undocumented) export function useOnMouseMove(el: RefElement, wait: number): MouseMoveResult; // @public (undocumented) -export function useOnMouseMove(el: RefElement, options?: boolean | AddEventListenerOptions, wait?: number): MouseMoveResult; +export function useOnMouseMove( + el: RefElement, + options?: boolean | AddEventListenerOptions, + wait?: number +): MouseMoveResult; // @public (undocumented) export function useOnResize(el: RefTyped, wait: number): ResizeResult; // @public (undocumented) -export function useOnResize(el: RefTyped, options?: boolean | AddEventListenerOptions, wait?: number): ResizeResult; +export function useOnResize( + el: RefTyped, + options?: boolean | AddEventListenerOptions, + wait?: number +): ResizeResult; // @public (undocumented) export function useOnResize(el: RefElement, wait: number): ResizeResult; // @public (undocumented) -export function useOnResize(el: RefElement, options?: boolean | AddEventListenerOptions, wait?: number): ResizeResult; +export function useOnResize( + el: RefElement, + options?: boolean | AddEventListenerOptions, + wait?: number +): ResizeResult; // @public (undocumented) export function useOnScroll(): ScrollResult; @@ -410,99 +582,139 @@ export function useOnScroll(): ScrollResult; export function useOnScroll(wait: number): ScrollResult; // @public (undocumented) -export function useOnScroll(options: boolean | AddEventListenerOptions, wait?: number): ScrollResult; +export function useOnScroll( + options: boolean | AddEventListenerOptions, + wait?: number +): ScrollResult; // @public (undocumented) export function useOnScroll(el: RefTyped, wait: number): ScrollResult; // @public (undocumented) -export function useOnScroll(el: RefTyped, options?: boolean | AddEventListenerOptions, wait?: number): ScrollResult; +export function useOnScroll( + el: RefTyped, + options?: boolean | AddEventListenerOptions, + wait?: number +): ScrollResult; // @public (undocumented) export function useOnScroll(el: RefElement, wait: number): ScrollResult; // @public (undocumented) -export function useOnScroll(el: RefElement, options?: boolean | AddEventListenerOptions, wait?: number): ScrollResult; +export function useOnScroll( + el: RefElement, + options?: boolean | AddEventListenerOptions, + wait?: number +): ScrollResult; // @public (undocumented) export function usePageVisibility(): { - visibility: Ref; - hidden: Ref; + visibility: Ref; + hidden: Ref; }; // @public (undocumented) -export function useSessionStorage(key: string, defaultValue?: RefTyped, sync?: boolean): LocalStorageReturn; - -// @public (undocumented) -export function useSessionStorage(key: LocalStorageTyped | string, defaultValue?: RefTyped, sync?: boolean): LocalStorageReturn; - -// @public (undocumented) -export function useSharedRef(name: string, defaultValue?: T): { - supported: boolean; - id: number; - data: Ref; - master: Ref; - mind: Ref; - editable: Readonly>; - targets: Ref; - ping: () => void; - setMind: (t: SharedRefMind) => void; - addListener: (cb: (ev: BroadcastMessageEvent>) => void, options?: boolean | AddEventListenerOptions | undefined) => void; +export function useSessionStorage( + key: string, + defaultValue?: RefTyped, + sync?: boolean +): LocalStorageReturn; + +// @public (undocumented) +export function useSessionStorage( + key: LocalStorageTyped | string, + defaultValue?: RefTyped, + sync?: boolean +): LocalStorageReturn; + +// @public (undocumented) +export function useSharedRef( + name: string, + defaultValue?: T +): { + supported: boolean; + id: number; + data: Ref; + master: Ref; + mind: Ref; + editable: Readonly>; + targets: Ref; + ping: () => void; + setMind: (t: SharedRefMind) => void; + addListener: ( + cb: (ev: BroadcastMessageEvent>) => void, + options?: boolean | AddEventListenerOptions | undefined + ) => void; }; // @public (undocumented) -export function useStorage(key: string, defaultValue?: RefTyped, sync?: boolean): LocalStorageReturn; - -// @public (undocumented) -export function useStorage(key: LocalStorageTyped | string, defaultValue?: RefTyped, sync?: boolean): LocalStorageReturn; - -// @public (undocumented) -export function useWebSocket(url: string, protocols?: string | string[]): { - supported: boolean; - ws: WebSocket | null; - send: (data: string | ArrayBuffer | SharedArrayBuffer | Blob | ArrayBufferView) => void; - close: (code?: number | undefined, reason?: string | undefined) => void; - messageEvent: import("@vue/composition-api").Ref; - errorEvent: import("@vue/composition-api").Ref; - data: import("@vue/composition-api").Ref; - isOpen: import("@vue/composition-api").Ref; - isClosed: import("@vue/composition-api").Ref; - errored: import("@vue/composition-api").Ref; +export function useStorage( + key: string, + defaultValue?: RefTyped, + sync?: boolean +): LocalStorageReturn; + +// @public (undocumented) +export function useStorage( + key: LocalStorageTyped | string, + defaultValue?: RefTyped, + sync?: boolean +): LocalStorageReturn; + +// @public (undocumented) +export function useWebSocket( + url: string, + protocols?: string | string[] +): { + supported: boolean; + ws: WebSocket | null; + send: ( + data: string | ArrayBuffer | SharedArrayBuffer | Blob | ArrayBufferView + ) => void; + close: (code?: number | undefined, reason?: string | undefined) => void; + messageEvent: import("@vue/composition-api").Ref; + errorEvent: import("@vue/composition-api").Ref; + data: import("@vue/composition-api").Ref; + isOpen: import("@vue/composition-api").Ref; + isClosed: import("@vue/composition-api").Ref; + errored: import("@vue/composition-api").Ref; }; // Warning: (ae-forgotten-export) The symbol "WebStorageType" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export function useWebStorage(type: WebStorageType, serializer?: StorageSerializer, ms?: number): { - supported: boolean; - quotaError: Ref; - store: WebStorage; - remove: () => boolean; +export function useWebStorage( + type: WebStorageType, + serializer?: StorageSerializer, + ms?: number +): { + supported: boolean; + quotaError: Ref; + store: WebStorage; + remove: () => boolean; }; // @public (undocumented) export interface WebStorage { - // (undocumented) - $quotaError: Ref; - // (undocumented) - $refMap: Map>; - // (undocumented) - $syncKeys: Record; - // (undocumented) - $watchHandlers: Map; - clear(): void; - getItem(key: string): Ref | null; - key(index: number): string | null; - readonly length: number; - removeItem(key: string): void; - setItem(key: string, value: T): Ref; - // (undocumented) - setSync(key: string, sync: boolean): void; - // (undocumented) - updateItem(key: string, value: string): void; + // (undocumented) + $quotaError: Ref; + // (undocumented) + $refMap: Map>; + // (undocumented) + $syncKeys: Record; + // (undocumented) + $watchHandlers: Map; + clear(): void; + getItem(key: string): Ref | null; + key(index: number): string | null; + readonly length: number; + removeItem(key: string): void; + setItem(key: string, value: T): Ref; + // (undocumented) + setSync(key: string, sync: boolean): void; + // (undocumented) + updateItem(key: string, value: string): void; } - // (No @packageDocumentation comment for this package) - ``` diff --git a/docs/composable/web/cssVariables.md b/docs/composable/web/cssVariables.md new file mode 100644 index 000000000..3a79709c3 --- /dev/null +++ b/docs/composable/web/cssVariables.md @@ -0,0 +1,127 @@ +# CSS variables + +Expose the CSS variables of your choice to reactive properties. + +## State + +The `useCssVariables` function exposes the following reactive states: + +```js +import { useCssVariables } from "vue-composable"; + +const { listening, backgroundColor, onBackgroundColor } = useCssVariables({ + backgroundColor: "color-background", + onBackgroundColor: "color-on-background" +}); + +// backgroundColor contains the `--color-background` CSS variable +// onBackgroundColor contains the `--color-on-background` CSS variable +// listening is true +``` + +| State | Type | Description | +| --------- | --------- | --------------------------------------------------------------------------- | +| listening | `Boolean` | A value that indicates if the observer is listening to CSS variable changes | +| ...args | `Args` | Object with the same keys but with reactive CSS variable value | + +## Methods + +The `useCssVariables` function exposes the following methods: + +```js +import { useCssVariables } from "vue-composable"; + +const { get, set, resume, start } = useCssVariables(); + +// Sets the `--color-background` variable value +// You don't need the dashed before the variable name. +set("color-background", "red"); + +// Gets the `--color-background` variable value +// The result is not reactive, and you don't need the leading dashes +const backgroundColor = get("color-background"); // red +``` + +| Signature | Description | Arguments | +| --------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| `get` | Manually gets the given CSS variable from the given element | `variableName: string, element: HTMLElement = document.documentElement` | +| `set` | Manually sets a CSS variable to the given element | `variableName: string, value: string | null, element: HTMLElement = document.documentElement` | +| `stop` | Stops observing the changes | | +| `resume` | Start observing the changes again | | + +## Example + + + +### Code + +```vue + + + +``` diff --git a/packages/vue-composable/README.md b/packages/vue-composable/README.md index bf20fb8c8..1d7707ee2 100644 --- a/packages/vue-composable/README.md +++ b/packages/vue-composable/README.md @@ -103,6 +103,7 @@ Check our [documentation](https://pikax.me/vue-composable/) - [Language](https://pikax.me/vue-composable/composable/web/language) - reactive `NavigatorLanguage` - [BroadcastChannel](https://pikax.me/vue-composable/composable/web/broadcastChannel) - reactive `BroadcastChannel API` - [Geolocation API](https://pikax.me/vue-composable/composable/web/geolocation) +- [CSS variables](https://pikax.me/vue-composable/composable/web/cssVariables) - reactive `CSS variables` ### External diff --git a/packages/web/README.md b/packages/web/README.md index b7dca87e4..d8233fb48 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -61,6 +61,7 @@ Check our [documentation](https://pikax.me/vue-composable/) - [Language](https://pikax.me/vue-composable/composable/web/language) - reactive `NavigatorLanguage` - [BroadcastChannel](https://pikax.me/vue-composable/composable/web/broadcastChannel) - reactive `BroadcastChannel API` - [Geolocation API](https://pikax.me/vue-composable/composable/web/geolocation) +- [CSS variables](https://pikax.me/vue-composable/composable/web/cssVariables) - reactive `CSS variables` ## Contributing diff --git a/readme.md b/readme.md index 98a867d2f..5e1425671 100644 --- a/readme.md +++ b/readme.md @@ -105,6 +105,7 @@ Check our [documentation](https://pikax.me/vue-composable/) - [Language](https://pikax.me/vue-composable/composable/web/language) - reactive `NavigatorLanguage` - [BroadcastChannel](https://pikax.me/vue-composable/composable/web/broadcastChannel) - reactive `BroadcastChannel API` - [Geolocation API](https://pikax.me/vue-composable/composable/web/geolocation) +- [CSS variables](https://pikax.me/vue-composable/composable/web/cssVariables) - reactive `CSS variables` ### External From a2f2cc4ebaf05e8701d29066492ca5c65d273913 Mon Sep 17 00:00:00 2001 From: Haew Date: Sun, 1 Mar 2020 15:43:01 +0100 Subject: [PATCH 4/9] Fix test typo --- packages/web/__tests__/web/cssVariables.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/__tests__/web/cssVariables.spec.ts b/packages/web/__tests__/web/cssVariables.spec.ts index 4a8587ed1..40548a16f 100644 --- a/packages/web/__tests__/web/cssVariables.spec.ts +++ b/packages/web/__tests__/web/cssVariables.spec.ts @@ -54,7 +54,7 @@ describe("CSS variables", () => { disconnectFn.mockReset(); }); - it("sohuld retrieve the correct value for a defined CSS variable", async () => { + it("should retrieve the correct value for a defined CSS variable", async () => { const element = document.createElement("div"); element.style.setProperty("--variable-name", "#fff"); From 9cecf72f76dfd265bb3e11261f6bf6d791faf696 Mon Sep 17 00:00:00 2001 From: Haew Date: Sun, 1 Mar 2020 15:44:04 +0100 Subject: [PATCH 5/9] Update observer options --- packages/web/src/web/cssVariables.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/web/src/web/cssVariables.ts b/packages/web/src/web/cssVariables.ts index 66477a851..24b770007 100644 --- a/packages/web/src/web/cssVariables.ts +++ b/packages/web/src/web/cssVariables.ts @@ -120,8 +120,7 @@ export function useCssVariables( const start = () => { observer.observe(element, { attributes: true, - childList: true, - subtree: true + attributeFilter: ["style"] }); listening.value = true; }; From 3bda4ff4f9441e5ec5e4c5536a7672a2917ffd0b Mon Sep 17 00:00:00 2001 From: Haew Date: Sun, 1 Mar 2020 15:47:59 +0100 Subject: [PATCH 6/9] Fix mock mistakes --- packages/web/__tests__/web/cssVariables.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/web/__tests__/web/cssVariables.spec.ts b/packages/web/__tests__/web/cssVariables.spec.ts index 40548a16f..6e26caaee 100644 --- a/packages/web/__tests__/web/cssVariables.spec.ts +++ b/packages/web/__tests__/web/cssVariables.spec.ts @@ -19,6 +19,7 @@ describe("CSS variables", () => { class MutationObserver { constructor(...args: any[]) { constructorFn(...args); + callback = args[0]; } observe = observeFn; disconnect = disconnectFn; @@ -49,7 +50,7 @@ describe("CSS variables", () => { }); beforeEach(() => { - observeFn.mockReset(); + observeFn.mockClear(); constructorFn.mockReset(); disconnectFn.mockReset(); }); From 508432dd1bd82499720756985ec205d2500886ab Mon Sep 17 00:00:00 2001 From: Haew Date: Sun, 1 Mar 2020 15:59:27 +0100 Subject: [PATCH 7/9] Fix copy-paste error --- docs/composable/web/cssVariables.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/composable/web/cssVariables.md b/docs/composable/web/cssVariables.md index 3a79709c3..109d6ea7d 100644 --- a/docs/composable/web/cssVariables.md +++ b/docs/composable/web/cssVariables.md @@ -99,7 +99,7 @@ const backgroundColor = get("color-background"); // red