diff --git a/.changeset/friendly-squids-attack1.md b/.changeset/friendly-squids-attack1.md new file mode 100644 index 000000000..3db832f89 --- /dev/null +++ b/.changeset/friendly-squids-attack1.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +adds a function `normalizeClasses` to normalize classes when they can be a string or an object, essentially converting strings to objects diff --git a/.changeset/friendly-squids-attack2.md b/.changeset/friendly-squids-attack2.md new file mode 100644 index 000000000..7eeac38a7 --- /dev/null +++ b/.changeset/friendly-squids-attack2.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +adds a function `clsMerge` for merging groups of classes without needing to do more complex property-by-property merging diff --git a/.changeset/friendly-squids-attack3.md b/.changeset/friendly-squids-attack3.md new file mode 100644 index 000000000..da0441652 --- /dev/null +++ b/.changeset/friendly-squids-attack3.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +passes the `classes` objects down to the `TextField` component from the `Select` and `MultiSelect` components diff --git a/.changeset/friendly-squids-attack4.md b/.changeset/friendly-squids-attack4.md new file mode 100644 index 000000000..a8de2cd45 --- /dev/null +++ b/.changeset/friendly-squids-attack4.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +fixes a TypeError caused by a faulty ReturnType from `entries()` and removes a related `@ts-expect-error` diff --git a/packages/svelte-ux/src/lib/components/MultiSelectField.svelte b/packages/svelte-ux/src/lib/components/MultiSelectField.svelte index ccfbc59b8..d8e439c22 100644 --- a/packages/svelte-ux/src/lib/components/MultiSelectField.svelte +++ b/packages/svelte-ux/src/lib/components/MultiSelectField.svelte @@ -11,7 +11,7 @@ import MultiSelectOption from './MultiSelectOption.svelte'; import TextField from './TextField.svelte'; - import { cls } from '../utils/styles.js'; + import { cls, clsMerge, normalizeClasses } from '../utils/styles.js'; import { Logger } from '../utils/logger.js'; import ProgressCircle from './ProgressCircle.svelte'; @@ -47,7 +47,7 @@ export let classes: { root?: string; multiSelectMenu?: MultiSelectMenuProps['classes']; - field?: string; + field?: string | ComponentProps['classes']; actions?: string; } = {}; @@ -154,7 +154,11 @@ bind:inputEl on:focus={onFocus} on:change={onSearchChange} - class={cls('h-full', settingsClasses.field, classes.field)} + classes={clsMerge( + normalizeClasses(settingsClasses.field), + { root: 'h-full' }, + normalizeClasses(classes.field) + )} {...restProps} > diff --git a/packages/svelte-ux/src/lib/components/SelectField.svelte b/packages/svelte-ux/src/lib/components/SelectField.svelte index 4641dc1fc..064dcbce3 100644 --- a/packages/svelte-ux/src/lib/components/SelectField.svelte +++ b/packages/svelte-ux/src/lib/components/SelectField.svelte @@ -6,7 +6,7 @@ import { Logger } from '../utils/logger.js'; import { autoFocus, selectOnFocus } from '../actions/input.js'; - import { cls } from '../utils/styles.js'; + import { cls, clsMerge, normalizeClasses } from '../utils/styles.js'; import Button from './Button.svelte'; import ProgressCircle from './ProgressCircle.svelte'; @@ -427,12 +427,16 @@ on:keydown={onKeyDown} on:keypress={onKeyPress} actions={fieldActions} - classes={{ - container: inlineOptions - ? 'border-none shadow-none hover:shadow-none group-focus-within:shadow-none' - : undefined, - }} - class={cls('h-full', settingsClasses.field, fieldClasses)} + classes={clsMerge( + normalizeClasses(settingsClasses.field), + { + root: 'h-full', + container: inlineOptions + ? 'border-none shadow-none hover:shadow-none group-focus-within:shadow-none' + : undefined, + }, + normalizeClasses(classes.field) + )} role="combobox" aria-expanded={open ? 'true' : 'false'} aria-autocomplete={!inlineOptions ? 'list' : undefined} diff --git a/packages/svelte-ux/src/lib/types/typeHelpers.ts b/packages/svelte-ux/src/lib/types/typeHelpers.ts index e51ad7cba..87967651a 100644 --- a/packages/svelte-ux/src/lib/types/typeHelpers.ts +++ b/packages/svelte-ux/src/lib/types/typeHelpers.ts @@ -41,10 +41,12 @@ export function keys(o: T) { export type ObjectKey = string | number | symbol; // Get entries (array of [key, value] arrays) of object (strongly-typed) -export function entries(o: Record): [K, V][]; +export function entries( + o: Record +): [`${Extract}`, V][]; export function entries(o: Map): [K, V][]; // @ts-expect-error -export function entries(o: Record | Map): [K, V][] { +export function entries(o: Record | Map) { if (o instanceof Map) return Array.from(o.entries()) as unknown as [K, V][]; return Object.entries(o) as unknown as [K, V][]; // TODO: Improve based on key/value pair - https://stackoverflow.com/questions/60141960/typescript-key-value-relation-preserving-object-entries-type } diff --git a/packages/svelte-ux/src/lib/utils/styles.ts b/packages/svelte-ux/src/lib/utils/styles.ts index 550a31f08..f48f623b1 100644 --- a/packages/svelte-ux/src/lib/utils/styles.ts +++ b/packages/svelte-ux/src/lib/utils/styles.ts @@ -2,6 +2,7 @@ import clsx, { type ClassValue } from 'clsx'; import { extendTailwindMerge } from 'tailwind-merge'; import { range } from 'd3-array'; import { entries } from '../types/typeHelpers.js'; +import { mergeWith } from 'lodash-es'; /** * Convert object to style string @@ -11,7 +12,6 @@ export function objectToString(styleObj: { [key: string]: string }) { .map(([key, value]) => { if (value) { // Convert camelCase into kaboob-case (ex. (transformOrigin => transform-origin)) - // @ts-expect-error const propertyName = key.replace(/([A-Z])/g, '-$1').toLowerCase(); return `${propertyName}: ${value};`; } else { @@ -40,4 +40,17 @@ const twMerge = extendTailwindMerge({ }, }); -export const cls = (...inputs: ClassValue[]) => twMerge(clsx(...inputs)); +type ClassFalsyValues = undefined | null | false; +type AnyClassValue = ClassValue | ClassFalsyValues; +type AnyClassCollection = Record | ClassFalsyValues; + +export const cls = (...inputs: AnyClassValue[]) => twMerge(clsx(...inputs)); + +export const clsMerge = ( + ...inputs: T[] +): Exclude => + mergeWith({}, ...inputs.filter(Boolean), (a: string, b: string) => twMerge(a, b)); + +export const normalizeClasses = (classes: string | ClassFalsyValues | T): T => { + return classes && typeof classes === 'object' ? classes : ({ root: classes } as T); +};