@rogieking/figui3 9.3.0 → 9.4.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.
package/fig.js CHANGED
@@ -1934,31 +1934,58 @@ figDefineElement("fig-tooltip", FigTooltip);
1934
1934
 
1935
1935
  /* Text Truncation */
1936
1936
  class FigTruncate extends HTMLElement {
1937
- static observedAttributes = ["position", "tail"];
1937
+ static observedAttributes = ["position", "tail", "tooltip"];
1938
1938
 
1939
1939
  #originalText = null;
1940
1940
  #boundEnter = null;
1941
1941
  #boundLeave = null;
1942
+ #observer = null;
1942
1943
 
1943
1944
  connectedCallback() {
1944
1945
  this.#originalText = this.textContent;
1946
+ this.#observer ??= new MutationObserver(() => {
1947
+ this.#originalText = this.textContent;
1948
+ this.#render();
1949
+ });
1950
+ this.#observe();
1945
1951
  figNextFrame(this, () => {
1946
1952
  this.#render();
1953
+ this.#teardownTooltip();
1947
1954
  this.#setupTooltip();
1948
1955
  });
1949
1956
  }
1950
1957
 
1951
1958
  disconnectedCallback() {
1959
+ this.#observer?.disconnect();
1952
1960
  this.#teardownTooltip();
1953
1961
  }
1954
1962
 
1955
1963
  attributeChangedCallback(name, oldValue, newValue) {
1956
1964
  if (oldValue === newValue) return;
1957
1965
  if (this.#originalText === null) return;
1966
+ if (name === "tooltip") {
1967
+ this.#teardownTooltip();
1968
+ if (this.isConnected) this.#setupTooltip();
1969
+ return;
1970
+ }
1958
1971
  this.#render();
1959
1972
  }
1960
1973
 
1974
+ #observe() {
1975
+ this.#observer?.observe(this, {
1976
+ childList: true,
1977
+ characterData: true,
1978
+ subtree: true,
1979
+ });
1980
+ }
1981
+
1961
1982
  #render() {
1983
+ this.#observer?.disconnect();
1984
+ this.#renderContent();
1985
+ if (this.isConnected) this.#observe();
1986
+ }
1987
+
1988
+ #renderContent() {
1962
1989
  const position = this.getAttribute("position") || "right";
1963
1990
  const text = this.#originalText || "";
1964
1991
  if (position === "middle") {
@@ -1991,7 +2018,7 @@ class FigTruncate extends HTMLElement {
1991
2018
  )
1992
2019
  return;
1993
2020
  this.#boundEnter = () => {
1994
- if (this.scrollWidth <= this.clientWidth) return;
2021
+ if (!this.#isTruncated()) return;
1995
2022
  FigTooltip.show(this, this.#originalText);
1996
2023
  };
1997
2024
  this.#boundLeave = () => {
@@ -2001,11 +2028,21 @@ class FigTruncate extends HTMLElement {
2001
2028
  this.addEventListener("pointerleave", this.#boundLeave);
2002
2029
  }
2003
2030
 
2031
+ #isTruncated() {
2032
+ const overflows = (el) => el.scrollWidth > el.clientWidth;
2033
+ if (overflows(this)) return true;
2034
+ return [...this.querySelectorAll(":scope > .start, :scope > .end")].some(
2035
+ overflows,
2036
+ );
2037
+ }
2038
+
2004
2039
  #teardownTooltip() {
2005
2040
  if (this.#boundEnter)
2006
2041
  this.removeEventListener("pointerenter", this.#boundEnter);
2007
2042
  if (this.#boundLeave)
2008
2043
  this.removeEventListener("pointerleave", this.#boundLeave);
2044
+ this.#boundEnter = null;
2045
+ this.#boundLeave = null;
2009
2046
  FigTooltip.hide(this);
2010
2047
  }
2011
2048
  }
@@ -4798,6 +4835,513 @@ class FigTabs extends HTMLElement {
4798
4835
  }
4799
4836
  figDefineElement("fig-tabs", FigTabs);
4800
4837
 
4838
+ /* Toolbelt */
4839
+ /**
4840
+ * A semantic item group for use within FigToolbelt.
4841
+ */
4842
+ class FigToolbeltGroup extends HTMLElement {
4843
+ connectedCallback() {
4844
+ if (!this.hasAttribute("role")) this.setAttribute("role", "group");
4845
+ }
4846
+ }
4847
+ figDefineElement("fig-toolbelt-group", FigToolbeltGroup);
4848
+
4849
+ /**
4850
+ * A selectable item for use within FigToolbelt.
4851
+ * @attr {string} value - The value associated with the item
4852
+ * @attr {boolean} selected - Whether the item is currently selected
4853
+ * @attr {boolean} disabled - Whether the item is unavailable
4854
+ */
4855
+ class FigToolbeltItem extends HTMLElement {
4856
+ static get observedAttributes() {
4857
+ return ["selected", "disabled"];
4858
+ }
4859
+
4860
+ connectedCallback() {
4861
+ if (!this.hasAttribute("role")) this.setAttribute("role", "button");
4862
+ if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "-1");
4863
+ this.#syncA11yState();
4864
+ }
4865
+
4866
+ get value() {
4867
+ const value = this.getAttribute("value");
4868
+ return value !== null ? value : this.textContent?.trim() || "";
4869
+ }
4870
+
4871
+ set value(value) {
4872
+ this.setAttribute("value", value ?? "");
4873
+ }
4874
+
4875
+ get selected() {
4876
+ return figBooleanAttribute(this, "selected");
4877
+ }
4878
+
4879
+ set selected(value) {
4880
+ this.toggleAttribute("selected", Boolean(value));
4881
+ }
4882
+
4883
+ get disabled() {
4884
+ return figBooleanAttribute(this, "disabled");
4885
+ }
4886
+
4887
+ set disabled(value) {
4888
+ this.toggleAttribute("disabled", Boolean(value));
4889
+ }
4890
+
4891
+ attributeChangedCallback() {
4892
+ this.#syncA11yState();
4893
+ }
4894
+
4895
+ #syncA11yState() {
4896
+ const selected = figBooleanAttribute(this, "selected");
4897
+ const disabled = figBooleanAttribute(this, "disabled");
4898
+ this.setAttribute("aria-pressed", selected ? "true" : "false");
4899
+ if (disabled) {
4900
+ this.setAttribute("aria-disabled", "true");
4901
+ this.setAttribute("tabindex", "-1");
4902
+ } else {
4903
+ this.removeAttribute("aria-disabled");
4904
+ }
4905
+ }
4906
+ }
4907
+ figDefineElement("fig-toolbelt-item", FigToolbeltItem);
4908
+
4909
+ /**
4910
+ * A selectable toolbar with configurable layout and overflow controls.
4911
+ * @attr {string} value - The selected item value
4912
+ * @attr {boolean} disabled - Disables all items
4913
+ * @attr {"horizontal"|"vertical"} layout - Item direction (default: horizontal)
4914
+ * @attr {"buttons"|"scrollbar"} overflow - Overflow controls (default: scrollbar)
4915
+ * @fires input - Selected value
4916
+ * @fires change - Selected value
4917
+ */
4918
+ class FigToolbelt extends HTMLElement {
4919
+ #boundHandleClick = this.#handleClick.bind(this);
4920
+ #boundHandleKeyDown = this.#handleKeyDown.bind(this);
4921
+ #boundSyncOverflow = this.#syncOverflow.bind(this);
4922
+ #viewport = null;
4923
+ #mutationObserver = null;
4924
+ #resizeObserver = null;
4925
+ #navStart = null;
4926
+ #navEnd = null;
4927
+ #parentDisabledItems = new WeakSet();
4928
+ #syncingValue = false;
4929
+
4930
+ constructor() {
4931
+ super();
4932
+ const shadow = this.attachShadow({ mode: "open" });
4933
+ const frame = document.createElement("div");
4934
+ frame.setAttribute("part", "frame");
4935
+
4936
+ const startSlot = document.createElement("slot");
4937
+ startSlot.name = "start";
4938
+ startSlot.style.display = "contents";
4939
+
4940
+ this.#viewport = document.createElement("div");
4941
+ this.#viewport.className = "fig-overflow-fade";
4942
+ this.#viewport.setAttribute("part", "viewport");
4943
+ const itemSlot = document.createElement("slot");
4944
+ itemSlot.style.display = "contents";
4945
+ this.#viewport.append(itemSlot);
4946
+
4947
+ const endSlot = document.createElement("slot");
4948
+ endSlot.name = "end";
4949
+ endSlot.style.display = "contents";
4950
+
4951
+ frame.append(startSlot, this.#viewport, endSlot);
4952
+ shadow.append(frame);
4953
+ }
4954
+
4955
+ static get observedAttributes() {
4956
+ return ["value", "disabled", "layout", "overflow"];
4957
+ }
4958
+
4959
+ get #isVertical() {
4960
+ return this.getAttribute("layout") === "vertical";
4961
+ }
4962
+
4963
+ get #overflowMode() {
4964
+ return this.getAttribute("overflow") === "buttons"
4965
+ ? "buttons"
4966
+ : "scrollbar";
4967
+ }
4968
+
4969
+ connectedCallback() {
4970
+ if (!this.hasAttribute("role")) this.setAttribute("role", "toolbar");
4971
+ this.#syncOrientation();
4972
+ this.addEventListener("click", this.#boundHandleClick);
4973
+ this.addEventListener("keydown", this.#boundHandleKeyDown);
4974
+ this.#viewport.addEventListener("scroll", this.#boundSyncOverflow);
4975
+ this.#applyOverflowMode();
4976
+ this.#startObserver();
4977
+ this.#startResizeObserver();
4978
+ figNextFrame(this, () => {
4979
+ this.#syncSelection();
4980
+ this.#applyDisabled(figBooleanAttribute(this, "disabled"));
4981
+ this.#syncTabIndexes();
4982
+ this.#syncOverflow();
4983
+ this.#scrollSelectedItemIntoView(undefined, "auto");
4984
+ });
4985
+ }
4986
+
4987
+ disconnectedCallback() {
4988
+ this.removeEventListener("click", this.#boundHandleClick);
4989
+ this.removeEventListener("keydown", this.#boundHandleKeyDown);
4990
+ this.#viewport.removeEventListener("scroll", this.#boundSyncOverflow);
4991
+ this.#mutationObserver?.disconnect();
4992
+ this.#mutationObserver = null;
4993
+ this.#resizeObserver?.disconnect();
4994
+ this.#resizeObserver = null;
4995
+ this.#removeNavButtons();
4996
+ }
4997
+
4998
+ get value() {
4999
+ return this.selectedItem?.value || "";
5000
+ }
5001
+
5002
+ set value(value) {
5003
+ if (value === null || value === undefined) {
5004
+ this.removeAttribute("value");
5005
+ } else {
5006
+ this.setAttribute("value", String(value));
5007
+ }
5008
+ }
5009
+
5010
+ get selectedItem() {
5011
+ return this.#items().find((item) => figBooleanAttribute(item, "selected")) || null;
5012
+ }
5013
+
5014
+ attributeChangedCallback(name, oldValue, newValue) {
5015
+ if (oldValue === newValue) return;
5016
+ if (name === "value" && !this.#syncingValue) {
5017
+ this.#selectByValue(newValue);
5018
+ }
5019
+ if (name === "disabled") {
5020
+ this.#applyDisabled(newValue !== null && newValue !== "false");
5021
+ }
5022
+ if (name === "overflow") {
5023
+ this.#applyOverflowMode();
5024
+ requestAnimationFrame(() => this.#syncOverflow());
5025
+ }
5026
+ if (name === "layout") {
5027
+ this.#syncOrientation();
5028
+ if (this.#isVertical) this.#viewport.scrollLeft = 0;
5029
+ else this.#viewport.scrollTop = 0;
5030
+ this.#applyOverflowMode();
5031
+ requestAnimationFrame(() => {
5032
+ this.#syncOverflow();
5033
+ this.#scrollSelectedItemIntoView(undefined, "auto");
5034
+ });
5035
+ }
5036
+ }
5037
+
5038
+ #items() {
5039
+ return Array.from(this.querySelectorAll("fig-toolbelt-item")).filter(
5040
+ (item) => item.closest("fig-toolbelt") === this,
5041
+ );
5042
+ }
5043
+
5044
+ #availableItems() {
5045
+ return this.#items().filter((item) => !figBooleanAttribute(item, "disabled"));
5046
+ }
5047
+
5048
+ #applyDisabled(disabled) {
5049
+ if (disabled) this.setAttribute("aria-disabled", "true");
5050
+ else this.removeAttribute("aria-disabled");
5051
+ for (const item of this.#items()) {
5052
+ if (disabled) {
5053
+ if (!figBooleanAttribute(item, "disabled")) {
5054
+ this.#parentDisabledItems.add(item);
5055
+ item.setAttribute("disabled", "");
5056
+ }
5057
+ } else if (this.#parentDisabledItems.has(item)) {
5058
+ this.#parentDisabledItems.delete(item);
5059
+ item.removeAttribute("disabled");
5060
+ }
5061
+ }
5062
+ this.#syncTabIndexes();
5063
+ }
5064
+
5065
+ #syncSelection() {
5066
+ const declaredValue = this.getAttribute("value");
5067
+ if (declaredValue !== null) {
5068
+ this.#selectByValue(declaredValue);
5069
+ return;
5070
+ }
5071
+
5072
+ const selectedItems = this.#items().filter((item) =>
5073
+ figBooleanAttribute(item, "selected"),
5074
+ );
5075
+ const selected = selectedItems[0] || null;
5076
+ for (const item of selectedItems.slice(1)) {
5077
+ item.removeAttribute("selected");
5078
+ }
5079
+ if (selected) this.#reflectValue(selected.value);
5080
+ }
5081
+
5082
+ #selectByValue(value) {
5083
+ const target =
5084
+ value === null
5085
+ ? null
5086
+ : this.#items().find((item) => item.value === value) || null;
5087
+ for (const item of this.#items()) {
5088
+ item.toggleAttribute("selected", item === target);
5089
+ }
5090
+ this.#syncTabIndexes(target);
5091
+ if (target) this.#scrollSelectedItemIntoView(target);
5092
+ }
5093
+
5094
+ #selectItem(target, { emit = false, focus = false } = {}) {
5095
+ if (!target || figBooleanAttribute(target, "disabled")) return false;
5096
+ const previousItem = this.selectedItem;
5097
+ const previousValue = this.value;
5098
+ for (const item of this.#items()) {
5099
+ item.toggleAttribute("selected", item === target);
5100
+ }
5101
+ this.#reflectValue(target.value);
5102
+ this.#syncTabIndexes(target);
5103
+ if (focus) target.focus();
5104
+ this.#scrollSelectedItemIntoView(target);
5105
+ if (emit && (previousItem !== target || previousValue !== target.value)) {
5106
+ this.#emitSelectionEvents();
5107
+ }
5108
+ return true;
5109
+ }
5110
+
5111
+ #reflectValue(value) {
5112
+ this.#syncingValue = true;
5113
+ this.setAttribute("value", value);
5114
+ this.#syncingValue = false;
5115
+ }
5116
+
5117
+ #syncTabIndexes(preferredItem) {
5118
+ const items = this.#items();
5119
+ const availableItems = this.#availableItems();
5120
+ const active =
5121
+ (preferredItem && availableItems.includes(preferredItem) && preferredItem) ||
5122
+ (availableItems.includes(document.activeElement) && document.activeElement) ||
5123
+ (availableItems.includes(this.selectedItem) && this.selectedItem) ||
5124
+ availableItems[0] ||
5125
+ null;
5126
+ for (const item of items) {
5127
+ item.setAttribute("tabindex", item === active ? "0" : "-1");
5128
+ }
5129
+ }
5130
+
5131
+ #handleClick(event) {
5132
+ if (figBooleanAttribute(this, "disabled")) return;
5133
+ const target = event.target.closest?.("fig-toolbelt-item");
5134
+ if (!target || target.closest("fig-toolbelt") !== this) return;
5135
+ this.#selectItem(target, { emit: true });
5136
+ }
5137
+
5138
+ #handleKeyDown(event) {
5139
+ if (figBooleanAttribute(this, "disabled")) return;
5140
+ const target = event.target.closest?.("fig-toolbelt-item");
5141
+ if (!target || target.closest("fig-toolbelt") !== this) return;
5142
+
5143
+ if (event.key === "Enter" || event.key === " ") {
5144
+ event.preventDefault();
5145
+ this.#selectItem(target, { emit: true });
5146
+ return;
5147
+ }
5148
+
5149
+ const items = this.#availableItems();
5150
+ if (!items.length) return;
5151
+ const currentIndex = Math.max(0, items.indexOf(target));
5152
+ let nextIndex = currentIndex;
5153
+ switch (event.key) {
5154
+ case "ArrowLeft":
5155
+ if (this.#isVertical) return;
5156
+ nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
5157
+ break;
5158
+ case "ArrowUp":
5159
+ if (!this.#isVertical) return;
5160
+ nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
5161
+ break;
5162
+ case "ArrowRight":
5163
+ if (this.#isVertical) return;
5164
+ nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
5165
+ break;
5166
+ case "ArrowDown":
5167
+ if (!this.#isVertical) return;
5168
+ nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
5169
+ break;
5170
+ case "Home":
5171
+ nextIndex = 0;
5172
+ break;
5173
+ case "End":
5174
+ nextIndex = items.length - 1;
5175
+ break;
5176
+ default:
5177
+ return;
5178
+ }
5179
+ event.preventDefault();
5180
+ this.#syncTabIndexes(items[nextIndex]);
5181
+ items[nextIndex].focus();
5182
+ this.#scrollItemIntoView(items[nextIndex]);
5183
+ }
5184
+
5185
+ #emitSelectionEvents() {
5186
+ const detail = this.value;
5187
+ this.dispatchEvent(new CustomEvent("input", { detail, bubbles: true }));
5188
+ this.dispatchEvent(new CustomEvent("change", { detail, bubbles: true }));
5189
+ }
5190
+
5191
+ #startObserver() {
5192
+ this.#mutationObserver?.disconnect();
5193
+ this.#mutationObserver = new MutationObserver((records) => {
5194
+ const authoredChange = records.some(
5195
+ (record) =>
5196
+ record.type === "childList" ||
5197
+ (record.target instanceof Element &&
5198
+ record.target.matches("fig-toolbelt-item") &&
5199
+ record.target.closest("fig-toolbelt") === this),
5200
+ );
5201
+ if (!authoredChange) return;
5202
+ this.#applyOverflowMode();
5203
+ this.#syncSelection();
5204
+ this.#applyDisabled(figBooleanAttribute(this, "disabled"));
5205
+ this.#syncTabIndexes();
5206
+ requestAnimationFrame(() => {
5207
+ this.#syncOverflow();
5208
+ this.#scrollSelectedItemIntoView();
5209
+ });
5210
+ });
5211
+ this.#mutationObserver.observe(this, {
5212
+ childList: true,
5213
+ attributes: true,
5214
+ subtree: true,
5215
+ attributeFilter: ["value", "selected", "disabled"],
5216
+ });
5217
+ }
5218
+
5219
+ #startResizeObserver() {
5220
+ this.#resizeObserver?.disconnect();
5221
+ this.#resizeObserver = new ResizeObserver(() => this.#syncOverflow());
5222
+ this.#resizeObserver.observe(this);
5223
+ this.#resizeObserver.observe(this.#viewport);
5224
+ }
5225
+
5226
+ #syncOverflow() {
5227
+ if (this.#overflowMode === "scrollbar") return;
5228
+ figSyncOverflowState(
5229
+ this,
5230
+ this.#viewport,
5231
+ this.#isVertical ? "y" : "x",
5232
+ );
5233
+ }
5234
+
5235
+ #syncOrientation() {
5236
+ this.setAttribute(
5237
+ "aria-orientation",
5238
+ this.#isVertical ? "vertical" : "horizontal",
5239
+ );
5240
+ this.#viewport.classList.add("fig-overflow-fade");
5241
+ this.#viewport.classList.toggle(
5242
+ "fig-overflow-fade-horizontal",
5243
+ !this.#isVertical,
5244
+ );
5245
+ }
5246
+
5247
+ #applyOverflowMode() {
5248
+ if (this.#overflowMode === "scrollbar") {
5249
+ this.#removeNavButtons();
5250
+ } else {
5251
+ this.#createNavButtons();
5252
+ }
5253
+ }
5254
+
5255
+ #createNavButtons() {
5256
+ if (
5257
+ this.#navStart &&
5258
+ this.#navEnd &&
5259
+ this.contains(this.#navStart) &&
5260
+ this.contains(this.#navEnd)
5261
+ ) {
5262
+ this.#positionNavButtons();
5263
+ return;
5264
+ }
5265
+ this.#navStart?.remove();
5266
+ this.#navEnd?.remove();
5267
+ const buttons = createFigOverflowButtons({
5268
+ owner: "toolbelt",
5269
+ onStart: () => this.#scrollByPage(-1),
5270
+ onEnd: () => this.#scrollByPage(1),
5271
+ });
5272
+ this.#navStart = buttons.start;
5273
+ this.#navEnd = buttons.end;
5274
+ this.#navStart.setAttribute("slot", "start");
5275
+ this.#navEnd.setAttribute("slot", "end");
5276
+ this.#positionNavButtons();
5277
+ }
5278
+
5279
+ #positionNavButtons() {
5280
+ if (this.firstElementChild !== this.#navStart) this.prepend(this.#navStart);
5281
+ if (this.lastElementChild !== this.#navEnd) this.append(this.#navEnd);
5282
+ }
5283
+
5284
+ #removeNavButtons() {
5285
+ this.#navStart?.remove();
5286
+ this.#navEnd?.remove();
5287
+ this.#navStart = null;
5288
+ this.#navEnd = null;
5289
+ this.classList.remove("overflow-start", "overflow-end");
5290
+ }
5291
+
5292
+ #scrollByPage(direction) {
5293
+ figScrollOverflowPage(
5294
+ this.#viewport,
5295
+ this.#isVertical ? "y" : "x",
5296
+ direction,
5297
+ );
5298
+ }
5299
+
5300
+ #scrollSelectedItemIntoView(
5301
+ item = this.selectedItem,
5302
+ behavior = "smooth",
5303
+ ) {
5304
+ if (item) this.#scrollItemIntoView(item, behavior);
5305
+ }
5306
+
5307
+ #scrollItemIntoView(item, behavior = "smooth") {
5308
+ if (!item) return;
5309
+ requestAnimationFrame(() => {
5310
+ if (!this.isConnected || !item.isConnected) return;
5311
+ const isVertical = this.#isVertical;
5312
+ const hasOverflow = isVertical
5313
+ ? this.#viewport.scrollHeight > this.#viewport.clientHeight
5314
+ : this.#viewport.scrollWidth > this.#viewport.clientWidth;
5315
+ if (!hasOverflow) return;
5316
+ const containerRect = this.#viewport.getBoundingClientRect();
5317
+ const itemRect = item.getBoundingClientRect();
5318
+ if (isVertical) {
5319
+ const itemCenter =
5320
+ itemRect.top -
5321
+ containerRect.top +
5322
+ this.#viewport.scrollTop +
5323
+ itemRect.height / 2;
5324
+ this.#viewport.scrollTo({
5325
+ top: itemCenter - this.#viewport.clientHeight / 2,
5326
+ behavior,
5327
+ });
5328
+ } else {
5329
+ const itemCenter =
5330
+ itemRect.left -
5331
+ containerRect.left +
5332
+ this.#viewport.scrollLeft +
5333
+ itemRect.width / 2;
5334
+ this.#viewport.scrollTo({
5335
+ left: itemCenter - this.#viewport.clientWidth / 2,
5336
+ behavior,
5337
+ });
5338
+ }
5339
+ this.#syncOverflow();
5340
+ });
5341
+ }
5342
+ }
5343
+ figDefineElement("fig-toolbelt", FigToolbelt);
5344
+
4801
5345
  /* Segmented Control */
4802
5346
  /**
4803
5347
  * A custom segment element for use within FigSegmentedControl.
@@ -18191,23 +18735,29 @@ figDefineElement("fig-skeleton", FigSkeleton);
18191
18735
 
18192
18736
  // FigGroup
18193
18737
  class FigGroup extends HTMLElement {
18194
- static observedAttributes = ["name", "collapsible", "open"];
18738
+ static observedAttributes = ["name", "collapsible", "open", "compact"];
18195
18739
 
18196
18740
  #header = null;
18197
18741
  #chevron = null;
18742
+ #toggle = null;
18743
+ #compactHeader = null;
18744
+ #compactHeaderValue = null;
18198
18745
 
18199
18746
  connectedCallback() {
18200
18747
  this.#render();
18201
18748
  }
18202
18749
 
18203
18750
  disconnectedCallback() {
18204
- if (this.#chevron) {
18205
- this.#chevron.removeEventListener("click", this.#handleToggle);
18206
- }
18751
+ this.#removeToggleListeners();
18752
+ }
18753
+
18754
+ #removeToggleListeners() {
18755
+ this.#chevron?.removeEventListener("click", this.#handleToggle);
18756
+ this.#toggle?.removeEventListener("click", this.#handleToggle);
18757
+ this.#toggle?.removeEventListener("keydown", this.#handleToggleKeyDown);
18207
18758
  if (this.#header) {
18208
18759
  this.#header.removeEventListener("click", this.#handleToggle);
18209
- this.#header.removeEventListener("keydown", this.#handleHeaderKeyDown);
18210
- this.#header.querySelector("h3")?.removeEventListener("click", this.#handleToggle);
18760
+ this.#header.removeEventListener("keydown", this.#handleToggleKeyDown);
18211
18761
  }
18212
18762
  }
18213
18763
 
@@ -18215,7 +18765,7 @@ class FigGroup extends HTMLElement {
18215
18765
  if (oldValue === newValue) return;
18216
18766
  if (!this.isConnected) return;
18217
18767
  if (name === "open") {
18218
- this.#header?.setAttribute("aria-expanded", String(this.open));
18768
+ this.#syncExpanded();
18219
18769
  return;
18220
18770
  }
18221
18771
  this.#render();
@@ -18233,7 +18783,7 @@ class FigGroup extends HTMLElement {
18233
18783
  } else {
18234
18784
  this.setAttribute("open", "false");
18235
18785
  }
18236
- this.#header?.setAttribute("aria-expanded", String(!!value));
18786
+ this.#syncExpanded();
18237
18787
  if (was !== !!value) {
18238
18788
  this.dispatchEvent(
18239
18789
  new CustomEvent("openchange", {
@@ -18244,12 +18794,46 @@ class FigGroup extends HTMLElement {
18244
18794
  }
18245
18795
  }
18246
18796
 
18797
+ #syncExpanded() {
18798
+ if (this.hasAttribute("collapsible")) {
18799
+ this.#toggle?.setAttribute("aria-expanded", String(this.open));
18800
+ } else {
18801
+ this.#toggle?.removeAttribute("aria-expanded");
18802
+ }
18803
+ }
18804
+
18805
+ #restoreHeaderCompact() {
18806
+ if (!this.#compactHeader) return;
18807
+ if (this.#compactHeaderValue === null) {
18808
+ this.#compactHeader.removeAttribute("compact");
18809
+ } else {
18810
+ this.#compactHeader.setAttribute("compact", this.#compactHeaderValue);
18811
+ }
18812
+ this.#compactHeader = null;
18813
+ this.#compactHeaderValue = null;
18814
+ }
18815
+
18816
+ #syncHeaderCompact() {
18817
+ const compact =
18818
+ this.hasAttribute("compact") && this.getAttribute("compact") !== "false";
18819
+ if (!compact) {
18820
+ this.#restoreHeaderCompact();
18821
+ return;
18822
+ }
18823
+ if (this.#compactHeader !== this.#header) {
18824
+ this.#restoreHeaderCompact();
18825
+ this.#compactHeader = this.#header;
18826
+ this.#compactHeaderValue = this.#header?.getAttribute("compact") ?? null;
18827
+ }
18828
+ this.#header?.setAttribute("compact", "");
18829
+ }
18830
+
18247
18831
  #handleToggle = (e) => {
18248
18832
  e.stopPropagation();
18249
18833
  this.open = !this.open;
18250
18834
  };
18251
18835
 
18252
- #handleHeaderKeyDown = (e) => {
18836
+ #handleToggleKeyDown = (e) => {
18253
18837
  if (e.key !== "Enter" && e.key !== " ") return;
18254
18838
  e.preventDefault();
18255
18839
  e.stopPropagation();
@@ -18257,6 +18841,7 @@ class FigGroup extends HTMLElement {
18257
18841
  };
18258
18842
 
18259
18843
  #render() {
18844
+ this.#removeToggleListeners();
18260
18845
  const isCollapsible = this.hasAttribute("collapsible");
18261
18846
  const nameAttr = this.getAttribute("name");
18262
18847
  const label = nameAttr || (isCollapsible ? "Group" : null);
@@ -18268,9 +18853,11 @@ class FigGroup extends HTMLElement {
18268
18853
 
18269
18854
  if (!label && !isCollapsible && !userHeader) {
18270
18855
  if (this.#header && this.#header.dataset.generated) {
18856
+ this.#restoreHeaderCompact();
18271
18857
  this.#header.remove();
18272
18858
  this.#header = null;
18273
18859
  this.#chevron = null;
18860
+ this.#toggle = null;
18274
18861
  }
18275
18862
  return;
18276
18863
  }
@@ -18286,6 +18873,7 @@ class FigGroup extends HTMLElement {
18286
18873
  this.#header.dataset.generated = "true";
18287
18874
  this.prepend(this.#header);
18288
18875
  }
18876
+ this.#syncHeaderCompact();
18289
18877
 
18290
18878
  // Ensure h3 exists inside header
18291
18879
  let h3 = this.#header.querySelector("h3");
@@ -18293,6 +18881,7 @@ class FigGroup extends HTMLElement {
18293
18881
  h3 = document.createElement("h3");
18294
18882
  this.#header.prepend(h3);
18295
18883
  }
18884
+ this.#toggle = h3;
18296
18885
  if (!h3.id) h3.id = figUniqueId();
18297
18886
  if (this.#header.dataset.generated) {
18298
18887
  h3.textContent = label;
@@ -18312,26 +18901,27 @@ class FigGroup extends HTMLElement {
18312
18901
  }
18313
18902
  this.#header.insertBefore(chevron, h3);
18314
18903
  this.#chevron = chevron;
18315
- h3.removeEventListener("click", this.#handleToggle);
18316
- this.#header.removeEventListener("click", this.#handleToggle);
18317
- this.#header.addEventListener("click", this.#handleToggle);
18318
- this.#header.setAttribute("role", "button");
18319
- this.#header.setAttribute("tabindex", "0");
18320
- this.#header.setAttribute("aria-expanded", String(this.open));
18321
- this.#header.removeEventListener("keydown", this.#handleHeaderKeyDown);
18322
- this.#header.addEventListener("keydown", this.#handleHeaderKeyDown);
18904
+ this.#header.removeAttribute("role");
18905
+ this.#header.removeAttribute("tabindex");
18906
+ this.#header.removeAttribute("aria-expanded");
18907
+ h3.setAttribute("role", "button");
18908
+ h3.setAttribute("tabindex", "0");
18909
+ h3.setAttribute("aria-expanded", String(this.open));
18910
+ h3.addEventListener("click", this.#handleToggle);
18911
+ h3.addEventListener("keydown", this.#handleToggleKeyDown);
18912
+ chevron.addEventListener("click", this.#handleToggle);
18323
18913
 
18324
18914
  if (!this.hasAttribute("open")) {
18325
18915
  this.setAttribute("open", "false");
18326
- this.#header.setAttribute("aria-expanded", "false");
18916
+ h3.setAttribute("aria-expanded", "false");
18327
18917
  }
18328
18918
  } else {
18329
- h3.removeEventListener("click", this.#handleToggle);
18330
- this.#header.removeEventListener("click", this.#handleToggle);
18919
+ h3.removeAttribute("role");
18920
+ h3.removeAttribute("tabindex");
18921
+ h3.removeAttribute("aria-expanded");
18331
18922
  this.#header.removeAttribute("role");
18332
18923
  this.#header.removeAttribute("tabindex");
18333
18924
  this.#header.removeAttribute("aria-expanded");
18334
- this.#header.removeEventListener("keydown", this.#handleHeaderKeyDown);
18335
18925
  if (this.#chevron) {
18336
18926
  this.#chevron.remove();
18337
18927
  this.#chevron = null;