bits-ui
Version:
The headless components for Svelte.
93 lines (92 loc) • 3.46 kB
JavaScript
import { Context } from "runed";
import { box } from "svelte-toolbelt";
export const BitsConfigContext = new Context("BitsConfig");
/**
* Gets the current Bits UI configuration state from the context.
*
* Returns a default configuration (where all values are `undefined`) if no configuration is found.
*/
export function getBitsConfig() {
const fallback = new BitsConfigState(null, {});
return BitsConfigContext.getOr(fallback).opts;
}
/**
* Creates and sets a new Bits UI configuration state that inherits from parent configs.
*
* @param opts - Configuration options for this level
* @returns The configuration state instance
*
* @example
* ```typescript
* // In a component that wants to set a default portal target
* const config = useBitsConfig({ defaultPortalTo: box("#some-element") });
*
* // Child components will inherit this config and can override specific values
* const childConfig = useBitsConfig({ someOtherProp: box("value") });
* // childConfig still has defaultPortalTo="#some-element" from parent
* ```
*/
export function useBitsConfig(opts) {
return BitsConfigContext.set(new BitsConfigState(BitsConfigContext.getOr(null), opts));
}
/**
* Configuration state that inherits from parent configurations.
*
* @example
* Config resolution:
* ```
* Level 1: { defaultPortalTo: "#some-element", theme: "dark" }
* Level 2: { spacing: "large" } // inherits defaultPortalTo="#some-element", theme="dark"
* Level 3: { theme: "light" } // inherits defaultPortalTo="#some-element", spacing="large", overrides theme="light"
* ```
*/
export class BitsConfigState {
opts;
constructor(parent, opts) {
const resolveConfigOption = createConfigResolver(parent, opts);
this.opts = {
defaultPortalTo: resolveConfigOption((config) => config.defaultPortalTo),
defaultLocale: resolveConfigOption((config) => config.defaultLocale),
};
}
}
/**
* Returns a config resolver that resolves a given config option's value.
*
* The resolver creates reactive boxes that resolve config option values using this priority:
* 1. Current level's value (if defined)
* 2. Parent level's value (if defined and current is undefined)
* 3. `undefined` (if no value is found in either parent or child)
*
* @param parent - Parent configuration state (null if this is root level)
* @param currentOpts - Current level's configuration options
*
* @example
* ```typescript
* // Given this hierarchy:
* // Root: { defaultPortalTo: "#some-element" }
* // Child: { someOtherProp: "value" } // no defaultPortalTo specified
*
* const resolveConfigOption = createConfigResolver(parent, opts);
* const portalTo = resolveConfigOption(config => config.defaultPortalTo);
*
* // portalTo.current === "#some-element" (inherited from parent)
* // even when child didn't specify `defaultPortalTo`
* ```
*/
function createConfigResolver(parent, currentOpts) {
return (getter) => {
const configOption = box.with(() => {
// try current opts first
const value = getter(currentOpts)?.current;
if (value !== undefined)
return value;
// if no parent, return undefined
if (parent === null)
return undefined;
// get value from parent (which already has its own chain resolved)
return getter(parent.opts)?.current;
});
return configOption;
};
}