sveltejs / svelte

web development for the rest of us
https://svelte.dev
MIT License
80.34k stars 4.28k forks source link

Scoping class is not being applied in Svelte v5 #11565

Open webJose opened 6 months ago

webJose commented 6 months ago

Describe the bug

Ok, this is going to be difficult for me because I'm not front-end dev. Maybe the bug is not a bug but an error on my side. The summary: In SveteKit + svelte@5.0.0-next.127, I use a CSS class named grid-cell that doesn't have "explicit" setting in the script's style tag, but it is used in a very specific selector: div.grid.row.row-highlight:hover div.grid-cell-bg.sticky-data > div.grid-cell. There it is. Apparently, this usage is not enough for Svelte to add the scoping class to those DIV's. The structure is: div.grid-container > div.grid > div.grid-body > div.grid-row-bg > div.grid-row > div.grid-cell-bg > div.grid-cell. That's the markup the component specifies. That last DIV, when inspected in the browser, doesn't have the scoping class.

Just in case it isn't clear: The compiled CSS adds the scoping class to the big selector above. If I inspect the div.grid-cell element and manually add the scoping class, then the style is applied.

Reproduction

I currently cannot sit down to make a small repro, so I figured I should post the entire component. It is a bit over 200 lines (not much, right?). It is a table component made out of DIV's.

<script context="module" lang="ts">
    export type ColAlignment = 'start' | 'center' | 'end';

    export type WjGridRow<TRow extends Record<string, any> = Record<string, any>> = TRow & {
        id: string | number;
        selected?: boolean;
        expanded?: boolean;
    }

    export type WjGridColumn<TCol extends Record<string, any> = Record<string, any>, TRow extends Record<string, any> = Record<string, any>> = TCol & {
        key: string;
        text: string;
        width?: number;
        pinnable?: boolean;
        pinned?: boolean;
        alignment?: ColAlignment;
        get?: (row: TRow) => any;
    }
</script>

<script lang="ts" generics="TCol extends Record<string, any> = Record<string, any>, TRow extends Record<string, any> = Record<string, any>">
    import { combineClasses } from "./utils.js";

    let {
        columns,
        data,
        get = (r, k) => r[k],
        defaultWidth = 10,
        rowHighlight = true,
        striped = true,
        class: cssClass, ...restProps
    }: {
        columns: WjGridColumn<TCol, TRow>[];
        data: WjGridRow<TRow>[];
        get?: (row: TRow, key: string) => any;
        defaultWidth?: number;
        rowHighlight?: boolean;
        striped?: boolean;
        class?: string;
    } = $props();

    type ColumnInfo = {
        column: WjGridColumn<TCol, TRow>;
        left?: number;
    }

    const segregatedColumns = $derived(columns.reduce<{
        accPinnedWidth: number;
        accUnpinnedWidth: number;
        pinned: ColumnInfo[];
        unpinned: ColumnInfo[];
    }>((p, c) => {
        if (c.pinned) {
            p.pinned.push({
                column: c,
                left: p.accPinnedWidth
            });
            p.accPinnedWidth += columnWidth(c);
        }
        else {
            p.unpinned.push({
                column: c,
                left: p.accUnpinnedWidth
            });
            p.accUnpinnedWidth += columnWidth(c);
        }
        return p;
    }, { accPinnedWidth: 0, accUnpinnedWidth: 0, pinned: [], unpinned: [] }));

    $effect(() => {
        console.log('Column data: %o', segregatedColumns);
    });

    function columnWidth(col: WjGridColumn<TCol, TRow>) {
        return col.width ?? defaultWidth;
    }
</script>

{#snippet colHeaders(cols: ColumnInfo[])}
    {#each cols as ci (ci.column.key)}
    <div
        class={combineClasses('col-header', { 'sticky-header': !!ci.column.pinned })}
        role="columnheader"
        style:width={`${columnWidth(ci.column)}em`}
        style:left={ci.left !== undefined ? `${ci.left}em` : undefined}
    >
        {ci.column.text}
    </div>
    {/each}
{/snippet}

{#snippet colData(row: WjGridRow<TRow>, cols: ColumnInfo[])}
    {#each cols as ci (ci.column.key)}
    {@const getFn = ci.column.get ?? (r => get(r, ci.column.key))}
    <div
        class={combineClasses('grid-cell-bg', { 'sticky-data': !!ci.column.pinned })}
        role="gridcell"
        style:width={`${ci.column.width ?? defaultWidth}em`}
        style:left={ci.left !== undefined ? `${ci.left}em` : undefined}
    >
        <div class="grid-cell"> <!-- THIS GUY!  WHEN RENDERED, THE SCOPING CLASS IS NOT THERE.-->
            {getFn(row)}
        </div>
    </div>
    {/each}
{/snippet}

<div class={combineClasses('grid-container', cssClass)}>
    <div class="grid" role="table" {...restProps}>
        <div class="header-group" role="rowheader">
            {#if segregatedColumns.pinned.length}
                {@render colHeaders(segregatedColumns.pinned)}
            {/if}
            {@render colHeaders(segregatedColumns.unpinned)}
            <div class="col-header extra-header">&nbsp;</div>
        </div>
        <div class={combineClasses('grid-body', { striped })}>
            {#each data as row (row.id)}
            <div class="grid-row-bg">
                <div class={combineClasses('grid-row', { 'row-highlight': rowHighlight })}>
                    {#if segregatedColumns.pinned.length}
                        {@render colData(row, segregatedColumns.pinned)}
                    {/if}
                    {@render colData(row, segregatedColumns.unpinned)}
                    <div class="grid-cell"></div>
                </div>
            </div>
            {/each}
        </div>
    </div>
</div>

<style lang="scss">
    $tableGap: 0rem;
    div.grid-container {
        height: 100%;
        width: 100%;
        overflow: auto;
        position: relative;
        --wjg-bg-color-rgb: 255, 255, 255;
        --wjg-bg-color-opacity: 1;
        --wjg-color: inherit;
        --wjg-striped-even-bg-color-rgb: 240, 240, 240;
        --wjg-striped-odd-bg-color-rgb: 255, 255, 255;
        --wjg-rowhighlight-bg-color-rgb: 0, 0, 0;
        --wjg-rowhightlight-bg-opacity: 0.12;
    }

    div.grid {
        display: table;
        flex-direction: column;
        flex-wrap: nowrap;
        color: var(--wjg-color);
        min-width: 100%;
        background-color: rgba(var(--wjg-bg-color-rgb), var(--wjg-bg-color-opacity));
    }

    div.header-group {
        display: flex;
        flex-direction: row;
        gap: $tableGap;
    }

    div.col-header {
        position: sticky;
        z-index: 1;
        top: 0;
        font-weight: bold;
        box-shadow: inset 0 -0.4em 1em rgba(0, 0, 0, 0.15);
        &.sticky-header {
            z-index: 2;
        }
    }

    div.extra-header {
        flex: 1 1 0;
    }

    div.grid-body {
        display: flex;
        flex-direction: column;

        &.striped {
            > div.grid-row-bg {
                background-color: rgba(var(--wjg-striped-even-bg-color-rgb), var(--wjg-bg-color-opacity));
                & div.grid-cell-bg.sticky-data {
                    background-color: rgba(var(--wjg-striped-even-bg-color-rgb), var(--wjg-bg-color-opacity));
                }
            }
            > div.grid-row-bg:nth-of-type(2n+1) {
                background-color: rgba(var(--wjg-striped-odd-bg-color-rgb), var(--wjg-bg-color-opacity));
                & div.grid-cell-bg.sticky-data {
                    background-color: rgba(var(--wjg-striped-odd-bg-color-rgb), var(--wjg-bg-color-opacity));
                }
            }
        }
    }

    div.grid-row {
        display: flex;
        flex-direction: row;
        gap: $tableGap;
        padding: 0.2em 0em;

        &.row-highlight {
            &:hover, &.hover {
                background-color: rgba(0, 0, 0, var(--wjg-rowhightlight-bg-opacity));
                div.grid-cell-bg.sticky-data > div.grid-cell {
                    background-color: rgba(0, 0, 0, var(--wjg-rowhightlight-bg-opacity));
                }
            }
        }
    }

    div.grid-cell-bg {
        overflow: hidden;
        text-wrap: nowrap;
        text-overflow: ellipsis;
        &.sticky-data {
            position: sticky;
            z-index: 1;
        }
    }
</style>

Logs

No response

System Info

System:
    OS: Windows 11 10.0.22631
    CPU: (16) x64 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
    Memory: 4.39 GB / 15.77 GB
  Binaries:
    Node: 20.6.1 - C:\Program Files\nodejs\node.EXE
    npm: 9.8.1 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: Spartan (), Chromium (123.0.2420.97), ChromiumDev ()
    Internet Explorer: 11.0.22621.1
  npmPackages:
    svelte: ^5.0.0-next.1 => 5.0.0-next.127

Severity

blocking an upgrade

webJose commented 6 months ago

By the way, the workaround is easy, I suppose:

    div.grid-row {
        display: flex;
        flex-direction: row;
        gap: $tableGap;
        padding: 0.2em 0em;

        &.row-highlight {
            &:hover, &.hover {
                background-color: rgba(0, 0, 0, var(--wjg-rowhightlight-bg-opacity));
                div.grid-cell-bg.sticky-data > :global(div.grid-cell) { // WORKAROUND:  USING :global()
                    background-color: rgba(0, 0, 0, var(--wjg-rowhightlight-bg-opacity));
                }
            }
        }
    }