@ctrliq/quantic-components 1.84.1 → 1.85.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,38 +1,3 @@
1
1
  import { LitConstructor } from './shared.types';
2
2
  import { QuanticElement } from './quantic-element';
3
- /**
4
- * Mixin that automatically hides empty slots.
5
- *
6
- * Each listed slot is hidden by default and revealed when the host has matching
7
- * light DOM content. Pass `''` for the default (unnamed) slot.
8
- *
9
- * Two CSS rules cover all browsers for named slots:
10
- *
11
- * - `:host(:has([slot="name"]))` — Safari and Firefox
12
- * - `@scope { :scope:has([slot="name"]) }` — Chrome (@scope changes the
13
- * evaluation context so :has() can see light DOM children)
14
- *
15
- * The default slot uses `:has(> :not([slot]))` (element children without a
16
- * `slot` attribute). Text-only default content is covered by the slotchange
17
- * JS path below.
18
- *
19
- * The reveal value is `display: revert-layer` rather than `display: block` so
20
- * that the component's own display value is restored instead of overridden.
21
- * Component styles are placed in `@layer quantic-component` and the
22
- * hide/reveal rules in `@layer quantic-slot-visibility` (declared after, so
23
- * higher priority). `revert-layer` then falls back to whatever the component
24
- * set in `quantic-component`.
25
- *
26
- * CSS is injected via `finalizeStyles` so it lands in Lit SSR's Declarative
27
- * Shadow DOM output — no JS, no layout shift for SSR.
28
- *
29
- * Chrome doesn't re-evaluate :has() in shadow DOM when light-DOM children
30
- * arrive asynchronously (e.g. React CSR). A capture listener on the shadow
31
- * root intercepts all slotchange events internally and sets inline styles to
32
- * override the CSS — no wiring required in component templates.
33
- *
34
- * @example
35
- * class MyElement extends AutoHideEmptySlotsMixin(QuanticElement, ['header', 'footer']) {}
36
- * class Toast extends AutoHideEmptySlotsMixin(QuanticElement, ['title', '', 'action']) {}
37
- */
38
3
  export declare const AutoHideEmptySlotsMixin: <T extends LitConstructor<QuanticElement>>(superClass: T, slots?: string[]) => T;
@@ -39,13 +39,20 @@ import { escapeCssString, whenSlotPresent } from './slot-utils';
39
39
  *
40
40
  * Chrome doesn't re-evaluate :has() in shadow DOM when light-DOM children
41
41
  * arrive asynchronously (e.g. React CSR). A capture listener on the shadow
42
- * root intercepts all slotchange events internally and sets inline styles to
43
- * override the CSS — no wiring required in component templates.
42
+ * root intercepts all slotchange events internally and marks filled slots with
43
+ * `data-slot-filled`, no wiring required in component templates.
44
+ *
45
+ * That reveal has to be a rule inside `quantic-slot-visibility`, not an inline
46
+ * style. Inline declarations sit in the implicit unlayered layer, which the
47
+ * cascade orders after every explicit layer, so an inline
48
+ * `display: revert-layer` reverts to the `display: none` above rather than
49
+ * past it.
44
50
  *
45
51
  * @example
46
52
  * class MyElement extends AutoHideEmptySlotsMixin(QuanticElement, ['header', 'footer']) {}
47
53
  * class Toast extends AutoHideEmptySlotsMixin(QuanticElement, ['title', '', 'action']) {}
48
54
  */
55
+ const FILLED_ATTRIBUTE = 'data-slot-filled';
49
56
  export const AutoHideEmptySlotsMixin = (superClass, slots = []) => {
50
57
  var _AutoHideEmptySlotsClass_instances, _AutoHideEmptySlotsClass_slotListenerAttached, _AutoHideEmptySlotsClass_isMeaningful, _AutoHideEmptySlotsClass_onSlotChange, _AutoHideEmptySlotsClass_attachSlotListener, _AutoHideEmptySlotsClass_detachSlotListener;
51
58
  if (slots.length === 0)
@@ -63,6 +70,7 @@ export const AutoHideEmptySlotsMixin = (superClass, slots = []) => {
63
70
  `${sel} { display: none; }`,
64
71
  whenSlotPresent(name, unsafeCSS(`${sel} { display: revert-layer; }`))
65
72
  .cssText,
73
+ `${sel}[${FILLED_ATTRIBUTE}] { display: revert-layer; }`,
66
74
  ].join(' ');
67
75
  })
68
76
  .join(' ');
@@ -84,7 +92,7 @@ export const AutoHideEmptySlotsMixin = (superClass, slots = []) => {
84
92
  const hasContent = slot
85
93
  .assignedNodes({ flatten: true })
86
94
  .some(__classPrivateFieldGet(this, _AutoHideEmptySlotsClass_isMeaningful, "f"));
87
- slot.style.display = hasContent ? 'revert-layer' : '';
95
+ slot.toggleAttribute(FILLED_ATTRIBUTE, hasContent);
88
96
  });
89
97
  }
90
98
  static finalizeStyles(styles) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { css } from 'lit';
5
+ import postcss from 'postcss';
6
+ import { describe, expect, it } from 'vitest';
7
+ import { AutoHideEmptySlotsMixin } from './auto-hide-empty-slots.mixin';
8
+ import { QuanticElement } from './quantic-element';
9
+ const HERE = dirname(fileURLToPath(import.meta.url));
10
+ const VISIBILITY_LAYER = 'quantic-slot-visibility';
11
+ class Probe extends AutoHideEmptySlotsMixin(QuanticElement, ['title', '']) {
12
+ }
13
+ Probe.styles = css `
14
+ slot[name='title'] {
15
+ display: flex;
16
+ }
17
+
18
+ slot:not([name]) {
19
+ display: grid;
20
+ }
21
+ `;
22
+ function visibilityLayer() {
23
+ const [sheet] = Probe.finalizeStyles(Probe.styles);
24
+ let found;
25
+ postcss.parse(sheet.cssText).walkAtRules('layer', (rule) => {
26
+ if (rule.params === VISIBILITY_LAYER && rule.nodes)
27
+ found = rule.nodes;
28
+ });
29
+ if (!found)
30
+ throw new Error(`no @layer ${VISIBILITY_LAYER} in the stylesheet`);
31
+ return found;
32
+ }
33
+ function displayFor(layer, selector) {
34
+ const rule = layer.find((node) => node.type === 'rule' && node.selector === selector);
35
+ return rule?.nodes
36
+ .find((node) => node.type === 'decl' && node.prop === 'display')
37
+ ?.toString();
38
+ }
39
+ describe('AutoHideEmptySlotsMixin', () => {
40
+ const layer = visibilityLayer();
41
+ it.each([
42
+ ['named', 'slot[name="title"]'],
43
+ ['default', 'slot:not([name])'],
44
+ ])('hides the %s slot by default', (_label, selector) => {
45
+ expect(displayFor(layer, selector)).toBe('display: none');
46
+ });
47
+ it.each([
48
+ ['named', 'slot[name="title"][data-slot-filled]'],
49
+ ['default', 'slot:not([name])[data-slot-filled]'],
50
+ ])('reveals the %s slot from inside the visibility layer once JS marks it filled', (_label, selector) => {
51
+ expect(displayFor(layer, selector)).toBe('display: revert-layer');
52
+ });
53
+ it('never reveals a slot through an inline style', () => {
54
+ const source = readFileSync(join(HERE, 'auto-hide-empty-slots.mixin.ts'), 'utf8');
55
+ expect(source).not.toMatch(/\.style\.display\s*=/);
56
+ });
57
+ });
@@ -14,7 +14,7 @@ let TagGroup = class TagGroup extends QuanticElement {
14
14
  }
15
15
  render() {
16
16
  return html `
17
- <div class="group" role="group" aria-label=${this.ariaLabel ?? 'Tags'}>
17
+ <div class="group" part="group" role="group" aria-label=${this.ariaLabel ?? 'Tags'}>
18
18
  <slot></slot>
19
19
  </div>
20
20
  `;
@@ -24,6 +24,7 @@ TagGroup.meta = {
24
24
  tag: 'quantic-tag-group',
25
25
  description: 'A run of quantic-tag, quantic-status-tag or quantic-count-tag sharing one accessible name and a consistent gap. ' +
26
26
  'Wraps across lines, and each tag keeps its own size. Gap: --quantic-component-tag-group-gap. ' +
27
+ 'Layout: ::part(group), for a column or a different alignment. ' +
27
28
  'Use quantic-badge-group for badges.',
28
29
  category: 'display',
29
30
  slots: {
@@ -22,6 +22,13 @@ export declare const CustomIcon: import("../shared/story").StoryObject<object>;
22
22
  export declare const AsyncSlotRepro: {
23
23
  name: string;
24
24
  render: () => string;
25
+ parameters: {
26
+ docs: {
27
+ story: {
28
+ autoplay: boolean;
29
+ };
30
+ };
31
+ };
25
32
  play: ({ canvasElement }: {
26
33
  canvasElement: HTMLElement;
27
34
  }) => Promise<void>;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,38 +1,3 @@
1
1
  import { LitConstructor } from './shared.types';
2
2
  import { QuanticElement } from './quantic-element';
3
- /**
4
- * Mixin that automatically hides empty slots.
5
- *
6
- * Each listed slot is hidden by default and revealed when the host has matching
7
- * light DOM content. Pass `''` for the default (unnamed) slot.
8
- *
9
- * Two CSS rules cover all browsers for named slots:
10
- *
11
- * - `:host(:has([slot="name"]))` — Safari and Firefox
12
- * - `@scope { :scope:has([slot="name"]) }` — Chrome (@scope changes the
13
- * evaluation context so :has() can see light DOM children)
14
- *
15
- * The default slot uses `:has(> :not([slot]))` (element children without a
16
- * `slot` attribute). Text-only default content is covered by the slotchange
17
- * JS path below.
18
- *
19
- * The reveal value is `display: revert-layer` rather than `display: block` so
20
- * that the component's own display value is restored instead of overridden.
21
- * Component styles are placed in `@layer quantic-component` and the
22
- * hide/reveal rules in `@layer quantic-slot-visibility` (declared after, so
23
- * higher priority). `revert-layer` then falls back to whatever the component
24
- * set in `quantic-component`.
25
- *
26
- * CSS is injected via `finalizeStyles` so it lands in Lit SSR's Declarative
27
- * Shadow DOM output — no JS, no layout shift for SSR.
28
- *
29
- * Chrome doesn't re-evaluate :has() in shadow DOM when light-DOM children
30
- * arrive asynchronously (e.g. React CSR). A capture listener on the shadow
31
- * root intercepts all slotchange events internally and sets inline styles to
32
- * override the CSS — no wiring required in component templates.
33
- *
34
- * @example
35
- * class MyElement extends AutoHideEmptySlotsMixin(QuanticElement, ['header', 'footer']) {}
36
- * class Toast extends AutoHideEmptySlotsMixin(QuanticElement, ['title', '', 'action']) {}
37
- */
38
3
  export declare const AutoHideEmptySlotsMixin: <T extends LitConstructor<QuanticElement>>(superClass: T, slots?: string[]) => T;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrliq/quantic-components",
3
- "version": "1.84.1",
3
+ "version": "1.85.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/types/index.d.ts",