@fusion.dev/helix-headless 0.230.0 → 0.231.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.
Files changed (31) hide show
  1. package/api.json +1 -1
  2. package/custom-elements.json +1 -1
  3. package/dist/data-table/events/before-column-toggle.d.ts +14 -0
  4. package/dist/data-table/events/before-column-toggle.js +11 -0
  5. package/dist/data-table/events/column-toggle.d.ts +11 -0
  6. package/dist/data-table/events/column-toggle.js +11 -0
  7. package/dist/data-table/events/details.d.ts +7 -0
  8. package/dist/data-table/events/index.d.ts +6 -2
  9. package/dist/data-table/events/index.js +5 -1
  10. package/dist/data-table/root/data-table-root.d.ts +102 -0
  11. package/dist/data-table/root/data-table-root.js +183 -3
  12. package/dist/element-events.generated.js +2 -0
  13. package/dist/spatial-bar-chart/root/spatial-bar-chart-root.js +9 -9
  14. package/dist/spatial-ribbon-chart/root/spatial-ribbon-chart-root.d.ts +1 -0
  15. package/dist/spatial-ribbon-chart/root/spatial-ribbon-chart-root.js +3 -2
  16. package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.d.ts +1 -0
  17. package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.js +3 -2
  18. package/dist/spatial-scatter-chart/view.d.ts +30 -1
  19. package/dist/spatial-scatter-chart/view.js +56 -9
  20. package/dist/spatial-surface-chart/root/spatial-surface-chart-root.d.ts +1 -0
  21. package/dist/spatial-surface-chart/root/spatial-surface-chart-root.js +3 -2
  22. package/dist/spatial-treemap/root/spatial-treemap-root.d.ts +1 -0
  23. package/dist/spatial-treemap/root/spatial-treemap-root.js +3 -2
  24. package/dist/table/column/table-column.d.ts +23 -0
  25. package/dist/table/column/table-column.js +45 -6
  26. package/examples.json +1 -1
  27. package/lint.json +1 -1
  28. package/package.json +5 -5
  29. package/types/jsx.d.ts +139 -1
  30. package/types/vue.d.ts +68 -1
  31. package/types/words.d.ts +43 -0
@@ -4,7 +4,7 @@ import { html, nothing, svg } from 'lit';
4
4
  import { property, state } from 'lit/decorators.js';
5
5
  import '../../tooltip/index.js';
6
6
  import { fieldOf } from '../field.js';
7
- import { boundsOf, boxView, DragTurn, drawBox, inBox, nearestTo, SPATIAL_VIEW, turned, } from '../view.js';
7
+ import { boundsOf, boxView, DragTurn, drawBox, inBox, nearestTo, PlotRoomWatch, SPATIAL_VIEW, turned, wordsFitted, } from '../view.js';
8
8
  const SERIES = 24;
9
9
  const TURNS = {
10
10
  ArrowDown: { across: 0, up: -1 },
@@ -22,6 +22,7 @@ let HelixHeadlessSpatialScatterChart = class HelixHeadlessSpatialScatterChart ex
22
22
  this.summaryLabel = '{count, plural, one {# point} other {# points}}: {x} across, {y} up, and {z} in depth.';
23
23
  this.rendered = false;
24
24
  this.camera = SPATIAL_VIEW;
25
+ this.plotRoom = new PlotRoomWatch(this);
25
26
  this.drag = new DragTurn((next) => {
26
27
  this.camera = next(this.camera);
27
28
  }, () => this.requestUpdate());
@@ -155,7 +156,7 @@ let HelixHeadlessSpatialScatterChart = class HelixHeadlessSpatialScatterChart ex
155
156
  ${readable ? this.renderSummary(field) : this.renderStatus()}${this.renderSonify()}${this.renderTable()}`;
156
157
  }
157
158
  renderFallback(field, at) {
158
- const view = boxView(field.axes, this.camera, [], this.noBox);
159
+ const view = wordsFitted(this.plotRoom.room, (words) => boxView(field.axes, this.camera, [], this.noBox, words));
159
160
  const places = field.points.map((point, index) => {
160
161
  const box = [
161
162
  inBox(field.axes.x, point.x),
@@ -13,6 +13,7 @@
13
13
  * toward the viewer, which is the way back from the floor's back.
14
14
  */
15
15
  import { type Camera, type PagePoint, type Projection } from '@fusion.dev/helix-core';
16
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
16
17
  import { type SVGTemplateResult } from 'lit';
17
18
  import type { PlotAxis } from './field.js';
18
19
  /**
@@ -52,6 +53,8 @@ export interface BoxView {
52
53
  /** The box's edges and its grid, as one path. */
53
54
  grid: string;
54
55
  labels: ViewLabel[];
56
+ /** How large the words are, as a share of `TICK_SIZE` and `NAME_SIZE`. */
57
+ words: number;
55
58
  /** A place in the box, on the page. */
56
59
  on(point: BoxPoint): PagePoint;
57
60
  /** How near the camera a place in the box stands: the larger, the nearer. */
@@ -74,7 +77,33 @@ export declare const boxView: (axes: {
74
77
  x: PlotAxis;
75
78
  y: PlotAxis;
76
79
  z: PlotAxis;
77
- }, camera: Camera, extra?: readonly PagePoint[], bare?: boolean) => BoxView;
80
+ }, camera: Camera, extra?: readonly PagePoint[], bare?: boolean, words?: number) => BoxView;
81
+ /** The drawing's box on the page, and the font size its words are measured against. */
82
+ export interface PlotRoom {
83
+ font: number;
84
+ height: number;
85
+ width: number;
86
+ }
87
+ /**
88
+ * The view whose ticks stand at the box's font size or larger on the page.
89
+ *
90
+ * The drawing scales through its `viewBox`, so a tick written in the box's units
91
+ * shrinks with the box: measured October 2026 at 390 wide, 7.4 pixels on Scatter
92
+ * and Surface, under the 12 of the box's own font. The words widen the frame they
93
+ * are measured on, so the measure runs again on what they widened. Where nothing
94
+ * has measured the box, as on a server, the words stay at their written size.
95
+ */
96
+ export declare const wordsFitted: (room: PlotRoom | undefined, draw: (words: number) => BoxView) => BoxView;
97
+ /** Keeps a chart told the size and font size of the box its own drawing fills. */
98
+ export declare class PlotRoomWatch implements ReactiveController {
99
+ private readonly host;
100
+ room: PlotRoom | undefined;
101
+ private box;
102
+ private sizes;
103
+ constructor(host: ReactiveControllerHost & HTMLElement);
104
+ hostUpdated(): void;
105
+ hostDisconnected(): void;
106
+ }
78
107
  /**
79
108
  * The part of the page that holds `points`, `pad` beyond them on every side:
80
109
  * the frame of a drawing with no box, which fits its readings alone.
@@ -11,7 +11,7 @@ const WIDE = 0.58;
11
11
  export const TICK_SIZE = 0.13;
12
12
  export const NAME_SIZE = 0.15;
13
13
  const LABEL_GAP = 0.06;
14
- export const boxView = (axes, camera, extra = [], bare = false) => {
14
+ export const boxView = (axes, camera, extra = [], bare = false, words = 1) => {
15
15
  const projection = projectionOf(camera);
16
16
  const [ex, ey, ez] = axes.extent ?? [1, 1, 1];
17
17
  const on = ([x, y, z]) => projection.at(x * ex, -z * ez, y * ey);
@@ -45,14 +45,18 @@ export const boxView = (axes, camera, extra = [], bare = false) => {
45
45
  }
46
46
  const text = (axis) => ({
47
47
  name: axis.name,
48
- ticks: axis.ticks.map((tick, index) => ({
49
- at: inBox(axis, tick),
50
- text: axis.labels?.[index] ?? tickText(tick),
51
- })),
48
+ ticks: axis.ticks.map((tick, index) => {
49
+ const label = axis.labels?.[index];
50
+ return {
51
+ at: inBox(axis, tick),
52
+ category: label !== undefined && !/\d/.test(label),
53
+ text: label ?? tickText(tick),
54
+ };
55
+ }),
52
56
  });
53
57
  const labels = bare
54
58
  ? []
55
- : boxLabels((point) => ({ ...on(point), depth: depth(point) }), { x: text(axes.x), y: text(axes.y), z: text(axes.z) }, { gap: LABEL_GAP, name: NAME_SIZE, tick: TICK_SIZE, wide: WIDE });
59
+ : boxLabels((point) => ({ ...on(point), depth: depth(point) }), { x: text(axes.x), y: text(axes.y), z: text(axes.z) }, { gap: LABEL_GAP * words, name: NAME_SIZE * words, tick: TICK_SIZE * words, wide: WIDE });
56
60
  const corners = [];
57
61
  for (const x of [-1, 1]) {
58
62
  for (const y of [-1, 1]) {
@@ -62,7 +66,7 @@ export const boxView = (axes, camera, extra = [], bare = false) => {
62
66
  }
63
67
  }
64
68
  for (const one of labels) {
65
- const size = one.name ? NAME_SIZE : TICK_SIZE;
69
+ const size = (one.name ? NAME_SIZE : TICK_SIZE) * words;
66
70
  const half = (one.text.length * size * WIDE) / 2;
67
71
  corners.push({ x: one.x - half, y: one.y - size }, { x: one.x + half, y: one.y + size });
68
72
  }
@@ -77,8 +81,51 @@ export const boxView = (axes, camera, extra = [], bare = false) => {
77
81
  labels,
78
82
  on,
79
83
  projection,
84
+ words,
80
85
  };
81
86
  };
87
+ const WORD_PASSES = 4;
88
+ const MOST_WORDS = 3;
89
+ export const wordsFitted = (room, draw) => {
90
+ let view = draw(1);
91
+ if (!room) {
92
+ return view;
93
+ }
94
+ for (let pass = 0; pass < WORD_PASSES; pass += 1) {
95
+ const pixel = 1 / Math.min(room.width / view.bounds.width, room.height / view.bounds.height);
96
+ view = draw(Math.min(MOST_WORDS, Math.max(1, (room.font * pixel) / TICK_SIZE)));
97
+ }
98
+ return view;
99
+ };
100
+ export class PlotRoomWatch {
101
+ constructor(host) {
102
+ this.host = host;
103
+ host.addController(this);
104
+ }
105
+ hostUpdated() {
106
+ const box = this.host.shadowRoot?.querySelector('[part="fallback"]');
107
+ if (!box || box === this.box || typeof ResizeObserver !== 'function') {
108
+ return;
109
+ }
110
+ this.box = box;
111
+ this.sizes?.disconnect();
112
+ this.sizes = new ResizeObserver(([entry]) => {
113
+ const { height, width } = entry?.contentRect ?? { height: 0, width: 0 };
114
+ const font = Number.parseFloat(getComputedStyle(box).fontSize) || 16;
115
+ const was = this.room;
116
+ if (was?.width !== width || was.height !== height || was.font !== font) {
117
+ this.room = height > 0 && width > 0 ? { font, height, width } : undefined;
118
+ this.host.requestUpdate();
119
+ }
120
+ });
121
+ this.sizes.observe(box);
122
+ }
123
+ hostDisconnected() {
124
+ this.sizes?.disconnect();
125
+ this.sizes = undefined;
126
+ this.box = undefined;
127
+ }
128
+ }
82
129
  export const boundsOf = (points, pad) => {
83
130
  if (points.length === 0) {
84
131
  return { height: 2 * pad, width: 2 * pad, x: -pad, y: -pad };
@@ -103,8 +150,8 @@ export const turned = (camera, across, up) => ({
103
150
  turn: camera.turn + across * VIEW_STEP,
104
151
  });
105
152
  export const drawBox = (view) => svg `<path d=${view.grid} part="grid"></path>${view.labels.map((label) => label.name
106
- ? svg `<text dominant-baseline="central" font-size=${NAME_SIZE} part="axis-name" text-anchor="middle" x=${shortNumber(label.x)} y=${shortNumber(label.y)}>${label.text}</text>`
107
- : svg `<text dominant-baseline="central" font-size=${TICK_SIZE} part="tick" text-anchor="middle" x=${shortNumber(label.x)} y=${shortNumber(label.y)}>${label.text}</text>`)}`;
153
+ ? svg `<text dominant-baseline="central" font-size=${NAME_SIZE * view.words} part="axis-name" text-anchor="middle" x=${shortNumber(label.x)} y=${shortNumber(label.y)}>${label.text}</text>`
154
+ : svg `<text dominant-baseline="central" font-size=${TICK_SIZE * view.words} part="tick" text-anchor="middle" x=${shortNumber(label.x)} y=${shortNumber(label.y)}>${label.text}</text>`)}`;
108
155
  export const nearestTo = (drawing, event, places, reach = 14) => {
109
156
  const matrix = drawing?.getScreenCTM?.();
110
157
  if (!matrix) {
@@ -151,6 +151,7 @@ export declare class HelixHeadlessSpatialSurfaceChart extends HelixHeadlessSpati
151
151
  /** Where the chart's own drawing is seen from, which a drag or Shift with an arrow turns. */
152
152
  private camera;
153
153
  /** A drag across the chart's own drawing, which turns its view. */
154
+ private readonly plotRoom;
154
155
  private readonly drag;
155
156
  /** Each reading where the chart's own drawing put it, which the pointer finds. */
156
157
  private placed;
@@ -3,7 +3,7 @@ import { AriaForwardController, CHART_PLOT, customElement, drawTooltip, formatWo
3
3
  import { html, nothing, svg, } from 'lit';
4
4
  import { property, state } from 'lit/decorators.js';
5
5
  import '../../tooltip/index.js';
6
- import { boundsOf, boxView, DragTurn, drawBox, inBox, nearestTo, SPATIAL_VIEW, shape, turned, } from '../../spatial-scatter-chart/view.js';
6
+ import { boundsOf, boxView, DragTurn, drawBox, inBox, nearestTo, PlotRoomWatch, SPATIAL_VIEW, shape, turned, wordsFitted, } from '../../spatial-scatter-chart/view.js';
7
7
  import { levelOf, surfaceOf } from '../grid.js';
8
8
  const LIGHT = (() => {
9
9
  const [x, y, z] = [-0.45, 1, 0.35];
@@ -27,6 +27,7 @@ let HelixHeadlessSpatialSurfaceChart = class HelixHeadlessSpatialSurfaceChart ex
27
27
  this.summaryLabel = '{count, plural, one {# reading} other {# readings}} of {y} over {x} and {z}, from {low} to {high}.';
28
28
  this.rendered = false;
29
29
  this.camera = SPATIAL_VIEW;
30
+ this.plotRoom = new PlotRoomWatch(this);
30
31
  this.drag = new DragTurn((next) => {
31
32
  this.camera = next(this.camera);
32
33
  }, () => this.requestUpdate());
@@ -162,7 +163,7 @@ let HelixHeadlessSpatialSurfaceChart = class HelixHeadlessSpatialSurfaceChart ex
162
163
  ${readable ? this.renderSummary(field) : this.renderStatus()}${this.renderSonify()}${this.renderTable()}`;
163
164
  }
164
165
  renderFallback(field, at) {
165
- const view = boxView(field.axes, this.camera, [], this.noBox);
166
+ const view = wordsFitted(this.plotRoom.room, (words) => boxView(field.axes, this.camera, [], this.noBox, words));
166
167
  const depths = field.depths.length;
167
168
  const boxed = field.cells.map((cell) => cell.value === undefined
168
169
  ? undefined
@@ -143,6 +143,7 @@ export declare class HelixHeadlessSpatialTreemap extends HelixHeadlessSpatialTre
143
143
  /** Where the chart's own drawing is seen from, which a drag or Shift with an arrow turns. */
144
144
  private camera;
145
145
  /** A drag across the chart's own drawing, which turns its view. */
146
+ private readonly plotRoom;
146
147
  private readonly drag;
147
148
  /** Where the renderer draws the reading, which the tooltip points at. */
148
149
  private place;
@@ -3,7 +3,7 @@ import { AriaForwardController, CHART_PLOT, customElement, drawTooltip, formatWo
3
3
  import { html, nothing, svg, } from 'lit';
4
4
  import { property, state } from 'lit/decorators.js';
5
5
  import '../../tooltip/index.js';
6
- import { boundsOf, boxView, DragTurn, drawBox, SPATIAL_VIEW, turned, } from '../../spatial-scatter-chart/view.js';
6
+ import { boundsOf, boxView, DragTurn, drawBox, PlotRoomWatch, SPATIAL_VIEW, turned, wordsFitted, } from '../../spatial-scatter-chart/view.js';
7
7
  import { blocksDrawn, paintOrder, treemapOf, } from '../blocks.js';
8
8
  const TURNS = {
9
9
  ArrowDown: { across: 0, up: -1 },
@@ -21,6 +21,7 @@ let HelixHeadlessSpatialTreemap = class HelixHeadlessSpatialTreemap extends Mixi
21
21
  this.summaryLabel = '{count, plural, one {# item} other {# items}} in {groups, plural, one {# group} other {# groups}}, sized from {low} to {high}.';
22
22
  this.rendered = false;
23
23
  this.camera = SPATIAL_VIEW;
24
+ this.plotRoom = new PlotRoomWatch(this);
24
25
  this.drag = new DragTurn((next) => {
25
26
  this.camera = next(this.camera);
26
27
  }, () => this.requestUpdate());
@@ -155,7 +156,7 @@ let HelixHeadlessSpatialTreemap = class HelixHeadlessSpatialTreemap extends Mixi
155
156
  ${readable ? this.renderSummary(field) : this.renderStatus()}${this.renderSonify()}${this.renderTable()}`;
156
157
  }
157
158
  renderFallback(field, at) {
158
- const view = boxView(field.axes, this.camera, [], this.noBox);
159
+ const view = wordsFitted(this.plotRoom.room, (words) => boxView(field.axes, this.camera, [], this.noBox, words));
159
160
  const blocks = blocksDrawn(field, view.projection);
160
161
  const active = blocks[at];
161
162
  const bounds = this.drag.frame(this.noBox
@@ -61,6 +61,10 @@ export { edgeOf } from '../edge.js';
61
61
  * current direction takes the heading's ink, so you can see which columns sort
62
62
  * and which way the table is sorted.
63
63
  *
64
+ * `menu` adds a small button after the chevrons, in a `DataTable`. The button
65
+ * opens a menu of what you can do with the column, and a press anywhere else on
66
+ * the heading still sorts. `DataTable` describes the rows.
67
+ *
64
68
  * @since 0.1.0
65
69
  * @status stable
66
70
  * @aliases Column header, Th
@@ -74,12 +78,15 @@ export { edgeOf } from '../edge.js';
74
78
  * @csspart column - The heading cell.
75
79
  * @csspart grip - The handle for moving the column, when the table allows it.
76
80
  * @csspart grip-mark - The icon on the grip.
81
+ * @csspart menu-button - The button that opens the column's menu, with `menu` in a `DataTable`.
82
+ * @csspart menu-mark - The icon on the menu button.
77
83
  * @csspart resizer - The edge that is dragged to change the width, when the column is resizable.
78
84
  * @csspart sort - The button that a sortable heading renders as.
79
85
  * @csspart sort-down - The lower chevron of `sort-mark`, which shows a descending sort.
80
86
  * @csspart sort-mark - The two chevrons on a sortable heading. To remove them, set `display: none` on this part.
81
87
  * @csspart sort-up - The upper chevron of `sort-mark`, which shows an ascending sort.
82
88
  *
89
+ * @cssstate has-menu - While the heading draws its menu button.
83
90
  * @cssstate sorted-ascending - While the table is sorted by this column, smallest first.
84
91
  * @cssstate sorted-descending - While the table is sorted by this column, largest first.
85
92
  */
@@ -126,6 +133,20 @@ export declare class HelixHeadlessTableColumn extends HelixElement {
126
133
  * such a column is never sortable however it is marked.
127
134
  */
128
135
  field: string;
136
+ /**
137
+ * Draws a button in the heading that opens a menu of what you can do with the
138
+ * column, in a `DataTable`.
139
+ *
140
+ * The menu offers sorting where the column is `sortable`, moving where the
141
+ * table is `reorderable`, and hiding. It needs a `field`, and a `Table` ignores
142
+ * it.
143
+ */
144
+ menu: boolean;
145
+ /**
146
+ * The accessible name of the menu button. `{name}` is the heading text, and
147
+ * `{named}` is `no` for a column without one.
148
+ */
149
+ menuLabel: string;
129
150
  /**
130
151
  * The accessible name of the grip that moves the column. `{name}` is the
131
152
  * heading text, and `{named}` is `no` for a column without one, so the
@@ -281,6 +302,8 @@ export declare class HelixHeadlessTableColumn extends HelixElement {
281
302
  */
282
303
  order(row: Readonly<Record<string, unknown>>): unknown;
283
304
  protected willUpdate(changed: PropertyValues): void;
305
+ /** Whether the heading holds a menu button: `menu`, in a table that draws the menu. */
306
+ get drawsMenu(): boolean;
284
307
  /** The words of the heading, for naming the controls on it. */
285
308
  private get name();
286
309
  connectedCallback(): void;
@@ -1,7 +1,7 @@
1
1
  var HelixHeadlessTableColumn_1;
2
2
  import { __decorate } from "tslib";
3
3
  import { customElement, formatWords, HelixBeforeResizeEvent, HelixElement, HelixResizedEvent, hostAria, inlineStep, internalsOf, isRtl, requestChange, valueAt, words, } from '@fusion.dev/helix-core';
4
- import { dotsSixVertical } from '@fusion.dev/helix-icons';
4
+ import { dotsSixVertical, dotsThree } from '@fusion.dev/helix-icons';
5
5
  import { html, nothing } from 'lit';
6
6
  import { property, state } from 'lit/decorators.js';
7
7
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
@@ -12,6 +12,8 @@ let HelixHeadlessTableColumn = class HelixHeadlessTableColumn extends HelixEleme
12
12
  this.cell = undefined;
13
13
  this.compare = undefined;
14
14
  this.field = '';
15
+ this.menu = false;
16
+ this.menuLabel = '{named, select, no {Column menu} other {{name} column menu}}';
15
17
  this.moveLabel = '{named, select, no {Move column} other {Move {name}}}';
16
18
  this.resizable = false;
17
19
  this.resizeLabel = '{named, select, no {Resize column} other {Resize {name}}}';
@@ -25,6 +27,8 @@ let HelixHeadlessTableColumn = class HelixHeadlessTableColumn extends HelixEleme
25
27
  this.gridCell = false;
26
28
  this.tabStop = false;
27
29
  this.sortWords = '';
30
+ this.menuDrawer = undefined;
31
+ this.menuFacts = '';
28
32
  this.drawn = 0;
29
33
  this.handleResizeStart = (event) => {
30
34
  if (event.button !== 0) {
@@ -84,13 +88,14 @@ let HelixHeadlessTableColumn = class HelixHeadlessTableColumn extends HelixEleme
84
88
  }
85
89
  willUpdate(changed) {
86
90
  super.willUpdate(changed);
87
- const named = this.movable || this.resizable;
91
+ const named = this.movable || this.resizable || this.drawsMenu;
88
92
  const host = !this.gridCell;
89
93
  hostAria(internalsOf(this), {
90
94
  label: host && named ? this.name : '',
91
95
  role: host ? 'columnheader' : '',
92
96
  });
93
97
  internalsOf(this).ariaSort = host && this.canSort ? (this.sorted ?? 'none') : null;
98
+ this.toggleState('has-menu', this.drawsMenu);
94
99
  this.toggleState('sorted-ascending', this.sorted === 'ascending');
95
100
  this.toggleState('sorted-descending', this.sorted === 'descending');
96
101
  }
@@ -98,11 +103,19 @@ let HelixHeadlessTableColumn = class HelixHeadlessTableColumn extends HelixEleme
98
103
  return this.partNamed('column');
99
104
  }
100
105
  get controls() {
101
- return ['grip', 'sort', 'resizer'].flatMap((name) => {
106
+ return ['grip', 'sort', 'menu-button', 'resizer'].flatMap((name) => {
102
107
  const control = this.partNamed(name);
103
108
  return control ? [control] : [];
104
109
  });
105
110
  }
111
+ openMenu() {
112
+ const button = this.partNamed('menu-button');
113
+ button?.focus();
114
+ button?.click();
115
+ }
116
+ get drawsMenu() {
117
+ return this.menu && this.menuDrawer !== undefined;
118
+ }
106
119
  get name() {
107
120
  return this.textContent?.trim() ?? '';
108
121
  }
@@ -137,7 +150,7 @@ let HelixHeadlessTableColumn = class HelixHeadlessTableColumn extends HelixEleme
137
150
  }
138
151
  updated(changed) {
139
152
  super.updated(changed);
140
- if (changed.has('textAlign') || changed.has('width')) {
153
+ if (changed.has('menu') || changed.has('textAlign') || changed.has('width')) {
141
154
  this.tellHolder();
142
155
  }
143
156
  }
@@ -169,12 +182,13 @@ let HelixHeadlessTableColumn = class HelixHeadlessTableColumn extends HelixEleme
169
182
  const grid = this.gridCell;
170
183
  const control = grid ? '-1' : nothing;
171
184
  const width = Math.min(4096, Math.max(HelixHeadlessTableColumn_1.NARROWEST, this.drawn));
185
+ const hasMenu = this.drawsMenu;
172
186
  return html `<span
173
187
  part="column"
174
188
  role=${grid ? 'columnheader' : nothing}
175
189
  tabindex=${grid ? (this.tabStop ? '0' : '-1') : nothing}
176
190
  aria-sort=${grid && this.canSort ? (this.sorted ?? 'none') : nothing}
177
- aria-label=${grid && (this.movable || this.resizable) && name ? name : nothing}
191
+ aria-label=${grid && (this.movable || this.resizable || hasMenu) && name ? name : nothing}
178
192
  >${this.movable
179
193
  ? html `<button part="grip" type="button" tabindex=${control} aria-label=${formatWords(this, 'moveLabel', named)}>
180
194
  <svg aria-hidden="true" fill="currentColor" part="grip-mark" viewBox=${dotsSixVertical.viewBox}>
@@ -186,7 +200,19 @@ let HelixHeadlessTableColumn = class HelixHeadlessTableColumn extends HelixEleme
186
200
  <path d="M5.5 7.2 9 3.7l3.5 3.5" part="sort-up" />
187
201
  <path d="M5.5 10.8 9 14.3l3.5-3.5" part="sort-down" />
188
202
  </svg></button>`
189
- : html `<slot></slot>`}${this.resizable
203
+ : html `<slot></slot>`}${hasMenu
204
+ ? this.menuDrawer?.(this, html `<button
205
+ part="menu-button"
206
+ slot="trigger"
207
+ type="button"
208
+ tabindex=${control}
209
+ aria-label=${formatWords(this, 'menuLabel', named)}
210
+ >
211
+ <svg aria-hidden="true" fill="currentColor" part="menu-mark" viewBox=${dotsThree.viewBox}>
212
+ ${unsafeSVG(dotsThree.content)}
213
+ </svg>
214
+ </button>`)
215
+ : nothing}${this.resizable
190
216
  ? html `<span
191
217
  part="resizer"
192
218
  role="separator"
@@ -215,6 +241,13 @@ __decorate([
215
241
  __decorate([
216
242
  property()
217
243
  ], HelixHeadlessTableColumn.prototype, "field", void 0);
244
+ __decorate([
245
+ property({ type: Boolean })
246
+ ], HelixHeadlessTableColumn.prototype, "menu", void 0);
247
+ __decorate([
248
+ words,
249
+ property({ attribute: 'menu-label' })
250
+ ], HelixHeadlessTableColumn.prototype, "menuLabel", void 0);
218
251
  __decorate([
219
252
  words,
220
253
  property({ attribute: 'move-label' })
@@ -257,6 +290,12 @@ __decorate([
257
290
  __decorate([
258
291
  state()
259
292
  ], HelixHeadlessTableColumn.prototype, "sortWords", void 0);
293
+ __decorate([
294
+ state()
295
+ ], HelixHeadlessTableColumn.prototype, "menuDrawer", void 0);
296
+ __decorate([
297
+ state()
298
+ ], HelixHeadlessTableColumn.prototype, "menuFacts", void 0);
260
299
  __decorate([
261
300
  state()
262
301
  ], HelixHeadlessTableColumn.prototype, "drawn", void 0);