Skip to content

Commit

Permalink
fix: correct assignCSSVar, improve tests, better tsdocs
Browse files Browse the repository at this point in the history
  • Loading branch information
essejmclean committed Nov 24, 2024
1 parent a429a7f commit a575e27
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 51 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ const textColor = createCSSVar('text-color', { fallback: '#000' })
// Fallback chain
const color = fallbackCSSVar('--primary', '--secondary', '#default')
// Result: var(--primary, var(--secondary, #default))

// Assign a value to a CSS variable
const cssVar = assignCSSVar(bgColor, "#ffffff");
// Result: { '--background-color': '#ffffff' }
// Use case: style={{ ...assignCSSVar(bgColor, "#ffffff") }}
```

[Read more about CSS Variables Utility](src/vars/README.md)
Expand Down Expand Up @@ -97,6 +102,7 @@ Both utilities include robust error handling:
## Browser Support

This library is designed for modern browsers that support:

- CSS Custom Properties (CSS Variables)
- CSS `calc()` function

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@builtbyfield/css-utils",
"version": "0.0.1",
"version": "0.1.0",
"description": "CSS utility functions",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
21 changes: 21 additions & 0 deletions src/calc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,21 @@
* @see https://github.com/vanilla-extract-css/vanilla-extract/tree/master/packages/utils
*/

/**
* The operator to use in a CSS calc expression.
*
* @example
* '+', '-', '*', '/'
*/
export type Operator = "+" | "-" | "*" | "/";

/**
* The operand to use in a CSS calc expression.
* Can be a string, number, or a CalcChain.
*
* @example
* '1px', 2, calc.add('1px', '2rem')
*/
export type Operand = string | number | CalcChain;

/**
Expand Down Expand Up @@ -88,6 +102,13 @@ const negate = (x: Operand) => multiply(x, -1);

/**
* A chainable interface for creating CSS calc expressions.
*
* @example
* calc('10px')
* .add('2rem')
* .multiply(2)
* .divide(3)
* .toString()
*/
export type CalcChain = {
add: (...operands: Array<Operand>) => CalcChain;
Expand Down
4 changes: 2 additions & 2 deletions src/vars/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ const fallbackColor = fallbackCSSVar(primary, secondary, '#default')
```typescript
import { assignCSSVar, createCSSVar } from '@builtbyfield/css-utils'
const bgColor = createCSSVar('background-color')
const cssVar = assignCSSVar(bgColor, '#ffffff')
// Result: { name: '--background-color', value: '#ffffff' }
const cssVar = assignCSSVar(bgColor, "#ffffff");
// Result: { '--background-color': '#ffffff' }
```

### Validation Functions
Expand Down
15 changes: 9 additions & 6 deletions src/vars/browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ function TestComponent({
describe("CSS Variables Browser Tests", () => {
it("should apply a basic CSS variable", () => {
const colorVar = createCSSVar("color");
const { name, value } = assignCSSVar(colorVar, "red");
const { "--color": value } = assignCSSVar(colorVar, "red");

// Set the CSS variable at root level
document.documentElement.style.setProperty(name, value);
document.documentElement.style.setProperty("--color", value);

render(<TestComponent cssVar={colorVar} />);

Expand All @@ -39,9 +39,12 @@ describe("CSS Variables Browser Tests", () => {

it("should handle complex values like rgba", () => {
const colorVar = createCSSVar("complex-color");
const { name, value } = assignCSSVar(colorVar, "rgba(255, 0, 0, 0.5)");
const { "--complex-color": value } = assignCSSVar(
colorVar,
"rgba(255, 0, 0, 0.5)"
);

document.documentElement.style.setProperty(name, value);
document.documentElement.style.setProperty("--complex-color", value);

render(<TestComponent cssVar={colorVar} />);

Expand All @@ -52,9 +55,9 @@ describe("CSS Variables Browser Tests", () => {

it("should handle calc expressions", () => {
const widthVar = createCSSVar("width");
const { name, value } = assignCSSVar(widthVar, "calc(100% - 20px)");
const { "--width": value } = assignCSSVar(widthVar, "calc(100% - 20px)");

document.documentElement.style.setProperty(name, value);
document.documentElement.style.setProperty("--width", value);

render(
<div style={{ width: "200px" }}>
Expand Down
47 changes: 39 additions & 8 deletions src/vars/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe("CSS Variable Name Validation", () => {
});

it("should reject names with spaces", () => {
expect(isValidCSSVarName(" ")).toBe(false);
expect(isValidCSSVarName("foo bar")).toBe(false);
expect(isValidCSSVarName(" foo")).toBe(false);
expect(isValidCSSVarName("foo ")).toBe(false);
Expand Down Expand Up @@ -122,11 +123,24 @@ describe("fallbackCSSVar", () => {
expect(fallback).toBe("var(--foo, var(--baz, 12))");
});

it("all but last must be valid CSS variable functions", () => {
const fooVar = createCSSVar("foo");
const bazVar = createCSSVar("baz");
expect(() => fallbackCSSVar(fooVar, "12", bazVar)).toThrow();
});

it("should handle calc expressions", () => {
const widthVar = createCSSVar("width");
const fallback = fallbackCSSVar(widthVar, "calc(100% - 20px)");
expect(fallback).toBe("var(--width, calc(100% - 20px))");
});

it("should throw when creating invalid CSS variable function", () => {
const invalidVar = "not-a-var-function";
expect(() => fallbackCSSVar(invalidVar, "red")).toThrow(
"All values except the last must be valid CSS variable functions"
);
});
});

describe("getCSSVarName", () => {
Expand All @@ -147,32 +161,28 @@ describe("assignCSSVar", () => {
it("should assign string value", () => {
const fooVar = createCSSVar("foo");
expect(assignCSSVar(fooVar, "baz")).toEqual({
name: "--foo",
value: "baz",
"--foo": "baz",
});
});

it("should assign number value", () => {
const numberVar = createCSSVar("spacing");
expect(assignCSSVar(numberVar, "12")).toEqual({
name: "--spacing",
value: "12",
"--spacing": "12",
});
});

it("should handle CSS functions", () => {
const colorVar = createCSSVar("overlay-color");
expect(assignCSSVar(colorVar, "rgba(0, 0, 0, 0.5)")).toEqual({
name: "--overlay-color",
value: "rgba(0, 0, 0, 0.5)",
"--overlay-color": "rgba(0, 0, 0, 0.5)",
});
});

it("should handle calc expressions", () => {
const widthVar = createCSSVar("container-width");
expect(assignCSSVar(widthVar, "calc(100% - 2rem)")).toEqual({
name: "--container-width",
value: "calc(100% - 2rem)",
"--container-width": "calc(100% - 2rem)",
});
});

Expand All @@ -181,4 +191,25 @@ describe("assignCSSVar", () => {
// @ts-expect-error - testing undefined value
expect(() => assignCSSVar(testVar, undefined)).toThrow();
});

it("should throw on invalid variable name format", () => {
// @ts-expect-error - testing invalid input
expect(() => assignCSSVar("invalid-var", "value")).toThrow(
"Invalid CSS variable name"
);
});

it("should throw on malformed var function", () => {
// @ts-expect-error - testing invalid input
expect(() => assignCSSVar("var(invalid)", "value")).toThrow(
"Invalid CSS variable name"
);
});

it("should handle null value", () => {
const testVar = createCSSVar("test");
expect(assignCSSVar(testVar, null)).toEqual({
"--test": null,
});
});
});
84 changes: 55 additions & 29 deletions src/vars/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,37 +22,32 @@ const CSS_VAR_NAME_EXTRACTOR = /^var\((.*)\)$/;
function validateCSSCustomPropertyName(name: string): boolean {
if (!name) return false;

try {
// Remove leading -- if present for validation
const propertyName = name.startsWith("--") ? name.slice(2) : name;

// First check if it's a valid CSS identifier pattern
if (!/^[a-zA-Z_\-\\][a-zA-Z0-9_\-\\]*$/.test(propertyName)) {
return false;
}

// Process the string to validate escape sequences
const result = propertyName;

// Only reject literal spaces, not escaped ones
if (result.includes(" ")) {
return false;
}

// Validate non-escaped parts
const parts = result.split(/\\[0-9a-fA-F]{1,6}\s?/);
return parts.every((part) => {
if (!part) return true; // Empty parts are OK between escape sequences
const escaped = cssesc(part, { isIdentifier: true });
return escaped === part;
});
} catch {
// Remove leading -- if present for validation
const propertyName = name.startsWith("--") ? name.slice(2) : name;

// First check if it's a valid CSS identifier pattern
if (!/^[a-zA-Z_\-\\][a-zA-Z0-9_\-\\]*$/.test(propertyName)) {
return false;
}

// Validate non-escaped parts
const parts = propertyName.split(/\\[0-9a-fA-F]{1,6}\s?/);
return parts.every((part) => {
if (!part) return true; // Empty parts are OK between escape sequences
const escaped = cssesc(part, { isIdentifier: true });
return escaped === part;
});
}

/**
* Checks if a name is a valid CSS variable name without -- prefix.
*
* @param name - The name to validate
* @returns True if the name is a valid CSS variable name, false otherwise
*
* @example
* isValidCSSVarName('my-var') // true
* isValidCSSVarName('--my-var') // false
*/
export function isValidCSSVarName(name: string): boolean {
return validateCSSCustomPropertyName(name);
Expand All @@ -65,6 +60,13 @@ export function isValidCSSVarName(name: string): boolean {
* - Are case-sensitive
* - Can contain letters, numbers, underscores, and hyphens
* - Cannot start with a digit after the dashes
*
* @param value - The value to validate
* @returns True if the value is a valid CSS variable name (including --), false otherwise
*
* @example
* isCSSVarName('--my-var') // true
* isCSSVarName('my-var') // false
*/
export function isCSSVarName(value: string): value is CSSVarName {
if (!value.startsWith("--")) return false;
Expand All @@ -75,6 +77,14 @@ export function isCSSVarName(value: string): value is CSSVarName {

/**
* Creates a CSS variable function with optional fallback.
*
* @param name - The name of the CSS variable
* @param options - Optional options for the CSS variable
* @returns A CSS variable function
*
* @example
* createCSSVar('my-var') // var(--my-var)
* createCSSVar('my-var', { fallback: '#fff' }) // var(--my-var, #fff)
*/
export function createCSSVar(
name: string,
Expand Down Expand Up @@ -108,6 +118,12 @@ function isCSSVarFunction(value: string): value is CSSVarFunction {

/**
* Creates a fallback CSS variable function.
*
* @param values - The values to fallback to
* @returns A CSS variable function with fallback
*
* @example
* fallbackCSSVar('var(--my-var)', '#fff') // var(--my-var, #fff)
*/
export function fallbackCSSVar(
...values: [string, ...Array<string>]
Expand Down Expand Up @@ -138,6 +154,12 @@ export function fallbackCSSVar(

/**
* Returns the variable name from a CSS variable function.
*
* @param variable - The CSS variable function
* @returns The variable name
*
* @example
* getCSSVarName('var(--my-var)') // --my-var
*/
export function getCSSVarName(variable: string): string {
const matches = variable.match(CSS_VAR_NAME_EXTRACTOR);
Expand All @@ -146,6 +168,13 @@ export function getCSSVarName(variable: string): string {

/**
* Assigns a value to a CSS variable.
*
* @param variable - The CSS variable function
* @param value - The value to assign to the CSS variable
* @returns The CSS variable definition
*
* @example
* assignCSSVar('var(--my-var)', '#fff') // { '--my-var': '#fff' }
*/
export function assignCSSVar(
variable: CSSVarFunction,
Expand All @@ -164,8 +193,5 @@ export function assignCSSVar(
throw new Error("Invalid CSS variable name");
}

return {
name: varName,
value,
};
return { [varName]: value };
}
7 changes: 2 additions & 5 deletions src/vars/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ export type CSSVarFunction =

/**
* CSS variable definition object
* @example { '--my-var': '#fff', '--color-primary': 'red' }
*/
export interface CSSVarDefinition {
name: CSSVarName;
value: string | null;
fallback?: string;
}
export type CSSVarDefinition = Record<CSSVarName, string | null>;

0 comments on commit a575e27

Please sign in to comment.