@fusion.dev/helix-core 0.30.2 → 0.32.0

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.
@@ -1372,7 +1372,7 @@
1372
1372
  },
1373
1373
  "privacy": "private",
1374
1374
  "readonly": true,
1375
- "default": "new FloatingController(host, { anchor: options.anchor, ...(options.arrow === undefined ? {} : { arrow: options.arrow }), floating: options.surface, ...(options.offset === undefined ? {} : { offset: options.offset }), ...(options.placement === undefined ? {} : { placement: options.placement }), ...(options.within === undefined ? {} : { within: options.within }), })"
1375
+ "default": "new FloatingController(host, { anchor: options.anchor, ...(options.arrow === undefined ? {} : { arrow: options.arrow }), floating: options.surface, ...(options.offset === undefined ? {} : { offset: options.offset }), ...(options.onPlace === undefined ? {} : { onPlace: options.onPlace }), ...(options.placement === undefined ? {} : { placement: options.placement }), ...(options.within === undefined ? {} : { within: options.within }), })"
1376
1376
  },
1377
1377
  {
1378
1378
  "kind": "field",
@@ -3485,7 +3485,7 @@
3485
3485
  }
3486
3486
  }
3487
3487
  ],
3488
- "description": "One of this element's own parts, by name, or null where it is not rendered.\n\nNot `part`, which every element already has: `Element.part` is the\n`DOMTokenList` of the names the element itself carries, and a method under\nthat name is a type error on every subclass.\n\n**One guard for every lookup.** Lit's server shim has no `querySelector`\non the render root, so a lookup reached during a server render, from a\ngetter a template reads or from `willUpdate`, throws there and nowhere\nelse. Four spellings of the same guard had grown across the library, and\na fifth site with none. Null on the server, which is what the rest of the\ncode already treats a part that has not rendered as.\n\n`helix component`'s `selectors` reads these calls the way it reads a\n`querySelector('[part=\"x\"]')`, so a name nothing renders is a finding\nrather than a null an optional chain swallows."
3488
+ "description": "One of this element's own parts, by name, or null where it is not rendered.\n\nNot `part`, which every element already has: `Element.part` is the\n`DOMTokenList` of the names the element itself carries, and a method under\nthat name is a type error on every subclass.\n\n**One guard for every lookup.** Lit's server shim has no `querySelector`\non the render root, so a lookup reached during a server render, from a\ngetter a template reads or from `willUpdate`, throws there and nowhere\nelse. Four spellings of the same guard had grown across the library, and\na fifth site with none. Null on the server, which is what the rest of the\ncode already treats a part that has not rendered as.\n\n**`~=` and not `=`.** A part drawn in a state carries a second name while\nthe state holds, `part=\"page page-current\"`, and an exact match stops\nfinding it for exactly as long as it is the one that matters.\n\n`helix component`'s `selectors` reads these calls the way it reads a\n`querySelector('[part=\"x\"]')`, so a name nothing renders is a finding\nrather than a null an optional chain swallows."
3489
3489
  },
3490
3490
  {
3491
3491
  "kind": "field",
@@ -3693,6 +3693,12 @@
3693
3693
  }
3694
3694
  ],
3695
3695
  "events": [],
3696
+ "cssStates": [
3697
+ {
3698
+ "description": "Set a frame after the first render. Transitions are held off until it is, so an element never animates its own arrival.",
3699
+ "name": "settled"
3700
+ }
3701
+ ],
3696
3702
  "superclass": {
3697
3703
  "name": "LitElement",
3698
3704
  "package": "lit"
@@ -4079,7 +4085,7 @@
4079
4085
  }
4080
4086
  }
4081
4087
  ],
4082
- "description": "One of this element's own parts, by name, or null where it is not rendered.\n\nNot `part`, which every element already has: `Element.part` is the\n`DOMTokenList` of the names the element itself carries, and a method under\nthat name is a type error on every subclass.\n\n**One guard for every lookup.** Lit's server shim has no `querySelector`\non the render root, so a lookup reached during a server render, from a\ngetter a template reads or from `willUpdate`, throws there and nowhere\nelse. Four spellings of the same guard had grown across the library, and\na fifth site with none. Null on the server, which is what the rest of the\ncode already treats a part that has not rendered as.\n\n`helix component`'s `selectors` reads these calls the way it reads a\n`querySelector('[part=\"x\"]')`, so a name nothing renders is a finding\nrather than a null an optional chain swallows.",
4088
+ "description": "One of this element's own parts, by name, or null where it is not rendered.\n\nNot `part`, which every element already has: `Element.part` is the\n`DOMTokenList` of the names the element itself carries, and a method under\nthat name is a type error on every subclass.\n\n**One guard for every lookup.** Lit's server shim has no `querySelector`\non the render root, so a lookup reached during a server render, from a\ngetter a template reads or from `willUpdate`, throws there and nowhere\nelse. Four spellings of the same guard had grown across the library, and\na fifth site with none. Null on the server, which is what the rest of the\ncode already treats a part that has not rendered as.\n\n**`~=` and not `=`.** A part drawn in a state carries a second name while\nthe state holds, `part=\"page page-current\"`, and an exact match stops\nfinding it for exactly as long as it is the one that matters.\n\n`helix component`'s `selectors` reads these calls the way it reads a\n`querySelector('[part=\"x\"]')`, so a name nothing renders is a finding\nrather than a null an optional chain swallows.",
4083
4089
  "inheritedFrom": {
4084
4090
  "name": "HelixElement",
4085
4091
  "module": "src/helix-element/helix-element.ts"
@@ -4381,7 +4387,17 @@
4381
4387
  "status": "experimental",
4382
4388
  "documentation": "https://helix.fusion.dev/core/form-element",
4383
4389
  "tier": "core"
4384
- }
4390
+ },
4391
+ "cssStates": [
4392
+ {
4393
+ "description": "Set a frame after the first render. Transitions are held off until it is, so an element never animates its own arrival.",
4394
+ "name": "settled",
4395
+ "inheritedFrom": {
4396
+ "name": "HelixElement",
4397
+ "module": "src/helix-element/helix-element.ts"
4398
+ }
4399
+ }
4400
+ ]
4385
4401
  }
4386
4402
  ],
4387
4403
  "exports": [
@@ -6698,7 +6714,7 @@
6698
6714
  },
6699
6715
  {
6700
6716
  "kind": "mixin",
6701
- "description": "Grafts checked state onto a base class.\n\n`checked` is a seed and a correction rather than a claim: the element applies\nits own changes from wherever markup started it, `defaultChecked` is what a\nreset returns to, and a consumer takes the decision by refusing the\n`helix-before-toggle`.\n\nRecords `:state(checked)` whenever the element is checked. Styled tiers\nselect on that rather than on `[checked]`, which is absent until somebody\nwrites it.",
6717
+ "description": "Grafts checked state onto a base class.\n\n`checked` is a seed and a correction rather than a claim: the element applies\nits own changes from wherever markup started it, `defaultChecked` is what a\nreset returns to, and a consumer takes the decision by refusing the\n`helix-before-toggle`.\n\nRecords `:state(checked)` whenever the element is checked. Styled tiers\nselect on that rather than on `[checked]`, which is absent until somebody\nwrites it. Each element applying this lists it as `@cssstate checked`: a\nclass-level tag on a mixin reaches no manifest, here or on the inner class.",
6702
6718
  "name": "MixinControllerChecked",
6703
6719
  "members": [
6704
6720
  {
@@ -6901,7 +6917,7 @@
6901
6917
  },
6902
6918
  {
6903
6919
  "kind": "mixin",
6904
- "description": "Grafts open state onto a base class, and says so twice.\n\n`helix-before-open` and `helix-before-close` announce the change while it is\nstill a request, so `preventDefault()` refuses it and nothing moves. Native\n`toggle` announces it afterwards, once the state has actually gone, carrying\n`newState` and `oldState` the way the platform does on `<details>` and on a\npopover. The first is how a consumer takes the decision; the second is what\nthey bind when they only want to know.\n\n`open` is a seed and a correction rather than a claim: the element applies\nits own changes from wherever markup started it, and `defaultOpen` is what a\nreset returns to.\n\nRecords `:state(open)` whenever the element is open. Styled tiers select on\nthat rather than on `[open]`, which is absent until somebody writes it.",
6920
+ "description": "Grafts open state onto a base class, and says so twice.\n\n`helix-before-open` and `helix-before-close` announce the change while it is\nstill a request, so `preventDefault()` refuses it and nothing moves. Native\n`toggle` announces it afterwards, once the state has actually gone, carrying\n`newState` and `oldState` the way the platform does on `<details>` and on a\npopover. The first is how a consumer takes the decision; the second is what\nthey bind when they only want to know.\n\n`open` is a seed and a correction rather than a claim: the element applies\nits own changes from wherever markup started it, and `defaultOpen` is what a\nreset returns to.\n\nRecords `:state(open)` whenever the element is open. Styled tiers select on\nthat rather than on `[open]`, which is absent until somebody writes it. Each\nelement applying this lists it as `@cssstate open`, because a class-level\ntag on a mixin reaches no manifest, here or on the inner class below: the\nnote there is about members, which do travel.",
6905
6921
  "name": "MixinControllerOpen",
6906
6922
  "members": [
6907
6923
  {
@@ -7571,7 +7587,7 @@
7571
7587
  },
7572
7588
  {
7573
7589
  "kind": "mixin",
7574
- "description": "Grafts a label, help text, and a validation message onto a control.\n\n**Over `HelixElement` rather than `HelixFormElement`, because not every\ncontrol that wants a label is form-associated.** A checkbox group is the\ncase: each box in it submits itself already, so the group deliberately\nreports no value of its own, and every form-related member of\n`ElementInternals` throws `NotSupportedError` on an element that is not\nform-associated. Measured, and the ARIA members are fine on the same\nelement, which is why the naming and describing work there and only the\nvalidity has to ask first.\n\nEvery form control wants the same three things around it and wants them in a\ndifferent arrangement: a label above an input, beside a checkbox, and over a\ngroup. So this owns the properties, the parts, and the wiring, and each\ncontrol places the nodes where its own geometry needs them. Same part names\neither way, so one stylesheet in the styled tier dresses all of them.\n\n**Not a wrapping `Field` component, and that is not a style preference.** A\nwrapper cannot name what it wraps. Its label would live in its own shadow\nroot and the control in another, and measured in Chrome 148: an\n`aria-labelledby` IDREF cannot see across, and\n`control.ariaLabelledByElements = [labelInTheWrapper]` reads back as zero\nelements, the reference dropped. The same assignment with a label in the\ncontrol's own tree is kept. That pattern works in React libraries because\nthey have no shadow boundary; bringing it here brings the ergonomics and none\nof the conditions that made it work.\n\n**A control whose label is its content renders its own.** A checkbox is named\nby the words beside it, inside the `<label>` that forwards the press, so it\nuses its default slot and nesting `renderLabel`'s `<label>` in there would be\ninvalid markup. `label` is that slot's fallback either way.\n\nWorth knowing, because it costs a label and gives no warning: **a\nwhitespace-only text node is assigned to the default slot and defeats its\nfallback.** Measured in Chrome 148, and a newline between the tags is enough,\nso `<helix-checkbox label=\"Accept\">` closed on the next line renders no label\nat all. A named slot does not have this problem, because `slot=\"label\"` is an\nattribute and a text node has none. It is the platform's behavior rather\nthan ours, and the same is already true of every slot fallback here.\n\n**The ids are fixed rather than generated.** IDREFs resolve within the tree\nthat holds them, so two controls on a page each carrying `helix-hint` in\ntheir own shadow root do not collide. That also removes the usual hydration\nhazard, since there is no generated value for a server and a client to\ndisagree about.\n\n**A group has to say it differently.** A control's `<input>` and its hint\nare both in the shadow root, so a plain `aria-describedby` resolves. A\ngroup's host is in the light DOM and its hint is not, so the IDREF cannot\nreach and `element.ariaDescribedByElements` drops the reference. Only\n`internals.ariaDescribedByElements` keeps it, which is what `describeHost`\nuses. Measured the same way, and the failure is silent either way: the\ndescription simply never reaches the accessibility tree.\n\n**The label, the hint and the feedback are documented on each component that\napplies this, rather than here.** Declaring them once on this mixin is the\nobvious fix and does not work: the analyzer reads `@csspart` off a custom\nelement class, so a class-level tag here reaches the manifest as nothing at\nall, on this JSDoc and on the inner class alike. Measured both ways, and the\ninner-class version is the dangerous one, since it satisfies the rule while\nevery API page stays exactly as wrong as before. Members are different and\ndo inherit, which is what the note about the inner class in the area rules\nis about.",
7590
+ "description": "Grafts a label, help text, and a validation message onto a control.\n\n**Over `HelixElement` rather than `HelixFormElement`, because not every\ncontrol that wants a label is form-associated.** A checkbox group is the\ncase: each box in it submits itself already, so the group deliberately\nreports no value of its own, and every form-related member of\n`ElementInternals` throws `NotSupportedError` on an element that is not\nform-associated. Measured, and the ARIA members are fine on the same\nelement, which is why the naming and describing work there and only the\nvalidity has to ask first.\n\nEvery form control wants the same three things around it and wants them in a\ndifferent arrangement: a label above an input, beside a checkbox, and over a\ngroup. So this owns the properties, the parts, and the wiring, and each\ncontrol places the nodes where its own geometry needs them. Same part names\neither way, so one stylesheet in the styled tier dresses all of them.\n\n**Not a wrapping `Field` component, and that is not a style preference.** A\nwrapper cannot name what it wraps. Its label would live in its own shadow\nroot and the control in another, and measured in Chrome 148: an\n`aria-labelledby` IDREF cannot see across, and\n`control.ariaLabelledByElements = [labelInTheWrapper]` reads back as zero\nelements, the reference dropped. The same assignment with a label in the\ncontrol's own tree is kept. That pattern works in React libraries because\nthey have no shadow boundary; bringing it here brings the ergonomics and none\nof the conditions that made it work.\n\n**A control whose label is its content renders its own.** A checkbox is named\nby the words beside it, inside the `<label>` that forwards the press, so it\nuses its default slot and nesting `renderLabel`'s `<label>` in there would be\ninvalid markup. `label` is that slot's fallback either way.\n\nWorth knowing, because it costs a label and gives no warning: **a\nwhitespace-only text node is assigned to the default slot and defeats its\nfallback.** Measured in Chrome 148, and a newline between the tags is enough,\nso `<helix-checkbox label=\"Accept\">` closed on the next line renders no label\nat all. A named slot does not have this problem, because `slot=\"label\"` is an\nattribute and a text node has none. It is the platform's behavior rather\nthan ours, and the same is already true of every slot fallback here.\n\n**The ids are fixed rather than generated.** IDREFs resolve within the tree\nthat holds them, so two controls on a page each carrying `helix-hint` in\ntheir own shadow root do not collide. That also removes the usual hydration\nhazard, since there is no generated value for a server and a client to\ndisagree about.\n\n**A group has to say it differently.** A control's `<input>` and its hint\nare both in the shadow root, so a plain `aria-describedby` resolves. A\ngroup's host is in the light DOM and its hint is not, so the IDREF cannot\nreach and `element.ariaDescribedByElements` drops the reference. Only\n`internals.ariaDescribedByElements` keeps it, which is what `describeHost`\nuses. Measured the same way, and the failure is silent either way: the\ndescription simply never reaches the accessibility tree.\n\n**The label, the hint and the feedback are documented on each component that\napplies this, rather than here.** Declaring them once on this mixin is the\nobvious fix and does not work: the analyzer reads `@csspart` off a custom\nelement class, so a class-level tag here reaches the manifest as nothing at\nall, on this JSDoc and on the inner class alike. Measured both ways, and the\ninner-class version is the dangerous one, since it satisfies the rule while\nevery API page stays exactly as wrong as before. Members are different and\ndo inherit, which is what the note about the inner class in the area rules\nis about. `:state(invalid)` is listed the same way, as a `@cssstate` on each\ncomponent, for the same reason.",
7575
7591
  "name": "MixinField",
7576
7592
  "members": [
7577
7593
  {
@@ -18,6 +18,15 @@ export interface FloatingOptions {
18
18
  * reader cannot use.
19
19
  */
20
20
  flip?: boolean;
21
+ /**
22
+ * Told the placement the element landed on, after every measurement.
23
+ *
24
+ * `data-placement` already says it, on the floating element, which is inside
25
+ * the host's shadow root. A stylesheet outside reaches that element through
26
+ * `::part()`, and `::part()` cannot be followed by an attribute selector, so
27
+ * a host whose surface is drawn from outside records the side on itself.
28
+ */
29
+ onPlace?: (placement: Placement) => void;
21
30
  /**
22
31
  * Space between the anchor and the floating element, in pixels.
23
32
  *
@@ -37,7 +37,10 @@ export class FloatingController {
37
37
  : this.options.placement;
38
38
  const middleware = [offset(gap ?? 0)];
39
39
  if (this.options.flip !== false) {
40
- middleware.push(flip());
40
+ const side = (preferred ?? 'bottom-start').split('-')[0];
41
+ middleware.push(flip(side === 'left' || side === 'right'
42
+ ? { crossAxis: 'alignment', fallbackAxisSideDirection: 'end' }
43
+ : {}));
41
44
  }
42
45
  if (this.options.shift !== false) {
43
46
  const inside = this.options.within?.();
@@ -72,6 +75,7 @@ export class FloatingController {
72
75
  });
73
76
  floating.setAttribute('data-placement', placement);
74
77
  floating.setAttribute('data-placed', '');
78
+ this.options.onPlace?.(placement);
75
79
  const point = middlewareData.arrow;
76
80
  if (pointer && point) {
77
81
  Object.assign(pointer.style, {
@@ -56,6 +56,11 @@ export interface PopoverOptions {
56
56
  * select puts focus back on its trigger, and a tooltip does neither.
57
57
  */
58
58
  onDismiss: () => void;
59
+ /**
60
+ * Told the placement the surface landed on, after every measurement. Passed
61
+ * through to the floating pass, which says why a host would want it.
62
+ */
63
+ onPlace?: (placement: Placement) => void;
59
64
  /**
60
65
  * Whether the host wants the surface up, read after every update.
61
66
  *
@@ -9,6 +9,7 @@ export class PopoverController {
9
9
  ...(options.arrow === undefined ? {} : { arrow: options.arrow }),
10
10
  floating: options.surface,
11
11
  ...(options.offset === undefined ? {} : { offset: options.offset }),
12
+ ...(options.onPlace === undefined ? {} : { onPlace: options.onPlace }),
12
13
  ...(options.placement === undefined ? {} : { placement: options.placement }),
13
14
  ...(options.within === undefined ? {} : { within: options.within }),
14
15
  });
@@ -16,6 +16,8 @@ import { type StaticValue } from 'lit/static-html.js';
16
16
  * @since 0.1.0
17
17
  * @status experimental
18
18
  * @documentation https://helix.fusion.dev/core/helix-element
19
+ *
20
+ * @cssstate settled - Set a frame after the first render. Transitions are held off until it is, so an element never animates its own arrival.
19
21
  */
20
22
  export declare class HelixElement extends LitElement {
21
23
  /**
@@ -64,6 +66,10 @@ export declare class HelixElement extends LitElement {
64
66
  * a fifth site with none. Null on the server, which is what the rest of the
65
67
  * code already treats a part that has not rendered as.
66
68
  *
69
+ * **`~=` and not `=`.** A part drawn in a state carries a second name while
70
+ * the state holds, `part="page page-current"`, and an exact match stops
71
+ * finding it for exactly as long as it is the one that matters.
72
+ *
67
73
  * `helix component`'s `selectors` reads these calls the way it reads a
68
74
  * `querySelector('[part="x"]')`, so a name nothing renders is a finding
69
75
  * rather than a null an optional chain swallows.
@@ -31,7 +31,9 @@ export class HelixElement extends LitElement {
31
31
  }
32
32
  partNamed(name) {
33
33
  const root = this.renderRoot;
34
- return typeof root?.querySelector === 'function' ? root.querySelector(`[part="${name}"]`) : null;
34
+ return typeof root?.querySelector === 'function'
35
+ ? root.querySelector(`[part~="${name}"]`)
36
+ : null;
35
37
  }
36
38
  toggleState(name, on) {
37
39
  if ('states' in this.internals) {
@@ -46,7 +46,8 @@ export declare class MixinControllerCheckedInterface {
46
46
  *
47
47
  * Records `:state(checked)` whenever the element is checked. Styled tiers
48
48
  * select on that rather than on `[checked]`, which is absent until somebody
49
- * writes it.
49
+ * writes it. Each element applying this lists it as `@cssstate checked`: a
50
+ * class-level tag on a mixin reaches no manifest, here or on the inner class.
50
51
  */
51
52
  export declare const MixinControllerChecked: <T extends Constructor<HelixElement>>(Base: T) => Constructor<MixinControllerCheckedInterface> & T;
52
53
  export {};
@@ -43,7 +43,10 @@ export declare class MixinControllerOpenInterface {
43
43
  * reset returns to.
44
44
  *
45
45
  * Records `:state(open)` whenever the element is open. Styled tiers select on
46
- * that rather than on `[open]`, which is absent until somebody writes it.
46
+ * that rather than on `[open]`, which is absent until somebody writes it. Each
47
+ * element applying this lists it as `@cssstate open`, because a class-level
48
+ * tag on a mixin reaches no manifest, here or on the inner class below: the
49
+ * note there is about members, which do travel.
47
50
  */
48
51
  export declare const MixinControllerOpen: <T extends Constructor<HelixElement>>(Base: T) => Constructor<MixinControllerOpenInterface> & T;
49
52
  export {};
@@ -137,7 +137,8 @@ export declare class MixinFieldInterface {
137
137
  * inner-class version is the dangerous one, since it satisfies the rule while
138
138
  * every API page stays exactly as wrong as before. Members are different and
139
139
  * do inherit, which is what the note about the inner class in the area rules
140
- * is about.
140
+ * is about. `:state(invalid)` is listed the same way, as a `@cssstate` on each
141
+ * component, for the same reason.
141
142
  */
142
143
  export declare const MixinField: <T extends Constructor<HelixElement>>(Base: T) => Constructor<MixinFieldInterface> & T;
143
144
  export {};
@@ -19,7 +19,7 @@ export const drawTooltip = (rows, at, tag, where) => {
19
19
  style=${styleMap({ insetBlockStart: place.y, insetInlineStart: place.x })}
20
20
  ></div>
21
21
  <${tag}
22
- exportparts="tooltip"
22
+ exportparts="arrow: tooltip-arrow, tooltip"
23
23
  for="helix-chart-mark"
24
24
  open
25
25
  part="tooltip-anchor"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-core",
3
3
  "description": "Helix element primitives: base classes, mixins, decorators, utilities.",
4
- "version": "0.30.2",
4
+ "version": "0.32.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -34,12 +34,12 @@
34
34
  "bugs": "https://github.com/project-fusion/fusion/issues",
35
35
  "dependencies": {
36
36
  "@floating-ui/dom": "^1.8.0",
37
- "@fusion.dev/helix-icons": "0.30.2"
37
+ "@fusion.dev/helix-icons": "0.32.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@custom-elements-manifest/analyzer": "^0.11.0",
41
- "@fusion.dev/helix-cem": "0.30.2",
42
- "@fusion.dev/helix-testing": "0.30.2",
41
+ "@fusion.dev/helix-cem": "0.32.0",
42
+ "@fusion.dev/helix-testing": "0.32.0",
43
43
  "lit": "^3.2.1"
44
44
  },
45
45
  "homepage": "https://fusion.dev",