@oicl/openbridge-webcomponents 2.0.0-next.172 → 2.0.0-next.173

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,6 +1,7 @@
1
1
  import { customElement } from "../../decorator.js";
2
2
  import __decorate from "../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
3
3
  import "../../icons/icon-chevron-down-google.js";
4
+ import { releaseFocusBefore } from "../../internal/focus.js";
4
5
  import accordion_item_default from "./accordion-item.css.js";
5
6
  import { LitElement, html, unsafeCSS } from "lit";
6
7
  import { property } from "lit/decorators.js";
@@ -22,6 +23,20 @@ var ObcAccordionItem = class ObcAccordionItem extends LitElement {
22
23
  title: this.title
23
24
  } }));
24
25
  }
26
+ /**
27
+ * Keeps the keyboard somewhere predictable when the panel closes.
28
+ *
29
+ * A collapsed panel is `inert`, and the browser drops focus out of anything
30
+ * it makes inert — onto `<body>`, two frames later. Clicking the header
31
+ * already leaves focus there; a consumer setting `open` while a slotted
32
+ * control has focus is the case that needs the transfer.
33
+ */
34
+ willUpdate(changed) {
35
+ if (!changed.has("open") || this.open) return;
36
+ const panel = this.shadowRoot?.querySelector(".panel");
37
+ if (!panel) return;
38
+ releaseFocusBefore(panel, this.shadowRoot?.querySelector(".content-button"), this.shadowRoot?.querySelector(".wrapper"));
39
+ }
25
40
  renderContent() {
26
41
  return html`
27
42
  <div class="content">
@@ -37,10 +52,13 @@ var ObcAccordionItem = class ObcAccordionItem extends LitElement {
37
52
  `;
38
53
  }
39
54
  renderExpandedContent() {
40
- if (!this.open) return "";
41
55
  return html`
42
- <div class="content-container">
43
- <slot name="expanded-content"></slot>
56
+ <div class="panel" id="accordion-content" ?inert=${!this.open}>
57
+ <div class="panel-inner">
58
+ <div class="content-container">
59
+ <slot name="expanded-content"></slot>
60
+ </div>
61
+ </div>
44
62
  </div>
45
63
  `;
46
64
  }
@@ -51,6 +69,7 @@ var ObcAccordionItem = class ObcAccordionItem extends LitElement {
51
69
  render() {
52
70
  return html`
53
71
  <div
72
+ tabindex="-1"
54
73
  class=${classMap({
55
74
  wrapper: true,
56
75
  "state-disabled": this.disabled,
@@ -63,6 +82,7 @@ var ObcAccordionItem = class ObcAccordionItem extends LitElement {
63
82
  @click=${this.handleToggle}
64
83
  ?disabled=${this.disabled}
65
84
  aria-expanded=${this.open}
85
+ aria-controls="accordion-content"
66
86
  >
67
87
  ${this.renderContent()}
68
88
  </button>
@@ -1 +1 @@
1
- {"version":3,"file":"accordion-item.js","names":[],"sources":["../../../src/components/accordion-item/accordion-item.ts"],"sourcesContent":["import {LitElement, html, unsafeCSS} from 'lit';\nimport {customElement} from '../../decorator.js';\nimport {classMap} from 'lit/directives/class-map.js';\nimport componentStyle from './accordion-item.css?inline';\nimport {property} from 'lit/decorators.js';\nimport '../../icons/icon-chevron-down-google.js';\n\n/**\n * `<obc-accordion-item>` – A collapsible section component for toggling the visibility of content panels (commonly known as an accordion item, expandable panel, or disclosure).\n *\n * Provides a single expandable/collapsible section with a clickable header and an optional content area. Useful for organizing related information in a compact, vertically stacked layout, allowing users to reveal or hide details as needed.\n *\n * ### Features\n * - **Toggleable Expansion:** Click the header to expand or collapse the content area. The chevron icon visually indicates the current state.\n * - **Disabled State:** Prevents user interaction and visually indicates non-interactivity.\n * - **Customizable Content:** Slot for custom expanded content (`expanded-content`), supporting any HTML or component.\n * - **Optional Divider:** Can display a divider line below the item for visual separation in lists.\n * - **Show/Hide Content Area:** Optionally hide the expanded content area entirely (e.g., for header-only items).\n * - **Accessible:** Uses a button for the header with appropriate ARIA attributes for accessibility.\n *\n * ### Usage Guidelines\n * Use `obc-accordion-item` to organize content into expandable sections, such as FAQs, settings panels, or grouped lists. Ideal when you want to let users scan section titles and expand only those relevant to them. For multi-item accordions, stack several `obc-accordion-item` components vertically.\n *\n * - Use the `title` property for the section label.\n * - Place any custom content (text, forms, components) in the `expanded-content` slot.\n * - Use the `disabled` property to prevent interaction for items that should not be toggled.\n * - Use the `showDivider` property to visually separate items in a list.\n * - If you need a persistent panel (not collapsible), use a different container component.\n *\n * **TODO(designer):** Confirm if there are recommended patterns for managing open/close state in multi-item accordions (e.g., only one open at a time), or if this is left to the developer.\n *\n * ### Slots\n * | Slot Name | Renders When... | Purpose |\n * |-------------------|--------------------------|-------------------------------------------------|\n * | expanded-content | When `open` and `showContent` are true | Custom content displayed when expanded |\n *\n * ### Events\n * - `accordion-item-toggle` – Fired when the item is toggled open or closed. Event detail: `{ open: boolean, title: string }`\n *\n * ### Best Practices\n * - Keep section titles concise for easy scanning.\n * - Avoid placing interactive controls in the header area to prevent accidental toggling.\n * - For accessibility, ensure each item’s title is unique within a group.\n *\n * **Example:**\n * ```\n * <obc-accordion-item title=\"Details\" .open=${false}>\n * <div slot=\"expanded-content\">\n * <p>Additional information goes here.</p>\n * </div>\n * </obc-accordion-item>\n * ```\n *\n * @property title - The label displayed in the header of the accordion item.\n * @property open - Whether the item is expanded (shows the content area).\n * Set to `true` to expand, `false` to collapse.\n * @property disabled - Disables the accordion item, preventing user interaction and dimming the appearance.\n * @property showDivider - Displays a divider line below the item for visual separation.\n * Useful when stacking multiple items in a list.\n * @slot expanded-content - Content displayed when the item is expanded and `showContent` is true.\n * @fires {CustomEvent<{open: boolean, title: string}>} accordion-item-toggle - Fired when the item is toggled open or closed.\n * @stable\n */\n@customElement('obc-accordion-item')\nexport class ObcAccordionItem extends LitElement {\n @property({type: String}) override title = '';\n\n @property({type: Boolean}) open = false;\n\n @property({type: Boolean}) disabled = false;\n\n @property({type: Boolean}) showDivider = false;\n\n private handleToggle() {\n if (this.disabled) return;\n\n this.open = !this.open;\n\n this.dispatchEvent(\n new CustomEvent('accordion-item-toggle', {\n detail: {\n open: this.open,\n title: this.title,\n },\n })\n );\n }\n\n private renderContent() {\n return html`\n <div class=\"content\">\n <div class=\"leading-content-container\">\n <div class=\"label-frame\">\n <div class=\"title\">${this.title}</div>\n </div>\n </div>\n <div class=\"trailing-icon\">\n <obi-chevron-down-google></obi-chevron-down-google>\n </div>\n </div>\n `;\n }\n\n private renderExpandedContent() {\n if (!this.open) return '';\n\n return html`\n <div class=\"content-container\">\n <slot name=\"expanded-content\"></slot>\n </div>\n `;\n }\n\n private renderDivider() {\n if (!this.showDivider) return '';\n\n return html`<div class=\"divider\"></div>`;\n }\n\n override render() {\n return html`\n <div\n class=${classMap({\n wrapper: true,\n 'state-disabled': this.disabled,\n 'open-true': this.open,\n 'open-false': !this.open,\n })}\n >\n <button\n class=\"content-button\"\n @click=${this.handleToggle}\n ?disabled=${this.disabled}\n aria-expanded=${this.open}\n >\n ${this.renderContent()}\n </button>\n\n ${this.renderExpandedContent()} ${this.renderDivider()}\n </div>\n `;\n }\n\n static override styles = unsafeCSS(componentStyle);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'obc-accordion-item': ObcAccordionItem;\n }\n}\n"],"mappings":";;;;;;;;AAgEO,IAAM,mBAAN,MAAM,yBAAyB,WAAW;;;EACJ,KAAA,QAAA;EAET,KAAA,OAAA;EAEI,KAAA,WAAA;EAEG,KAAA,cAAA;;CAEzC,eAAuB;EACrB,IAAI,KAAK,UAAU;EAEnB,KAAK,OAAO,CAAC,KAAK;EAElB,KAAK,cACH,IAAI,YAAY,yBAAyB,EACvC,QAAQ;GACN,MAAM,KAAK;GACX,OAAO,KAAK;EACd,EACF,CAAC,CACH;CACF;CAEA,gBAAwB;EACtB,OAAO,IAAI;;;;iCAIkB,KAAK,MAAM;;;;;;;;CAQ1C;CAEA,wBAAgC;EAC9B,IAAI,CAAC,KAAK,MAAM,OAAO;EAEvB,OAAO,IAAI;;;;;CAKb;CAEA,gBAAwB;EACtB,IAAI,CAAC,KAAK,aAAa,OAAO;EAE9B,OAAO,IAAI;CACb;CAEA,SAAkB;EAChB,OAAO,IAAI;;gBAEC,SAAS;GACf,SAAS;GACT,kBAAkB,KAAK;GACvB,aAAa,KAAK;GAClB,cAAc,CAAC,KAAK;EACtB,CAAC,EAAE;;;;mBAIQ,KAAK,aAAa;sBACf,KAAK,SAAS;0BACV,KAAK,KAAK;;YAExB,KAAK,cAAc,EAAE;;;UAGvB,KAAK,sBAAsB,EAAE,GAAG,KAAK,cAAc,EAAE;;;CAG7D;;EAEyB,KAAA,SAAA,UAAU,sBAAc;;AACnD;AA/EG,WAAA,CAAA,SAAS,EAAC,MAAM,OAAM,CAAC,CAAA,GAAA,iBAAA,WAAA,SAAA,KAAA,CAAA;AAEvB,WAAA,CAAA,SAAS,EAAC,MAAM,QAAO,CAAC,CAAA,GAAA,iBAAA,WAAA,QAAA,KAAA,CAAA;AAExB,WAAA,CAAA,SAAS,EAAC,MAAM,QAAO,CAAC,CAAA,GAAA,iBAAA,WAAA,YAAA,KAAA,CAAA;AAExB,WAAA,CAAA,SAAS,EAAC,MAAM,QAAO,CAAC,CAAA,GAAA,iBAAA,WAAA,eAAA,KAAA,CAAA;AAR1B,mBAAA,WAAA,CAAA,cAAc,oBAAoB,CAAA,GAAA,gBAAA"}
1
+ {"version":3,"file":"accordion-item.js","names":[],"sources":["../../../src/components/accordion-item/accordion-item.ts"],"sourcesContent":["import {LitElement, html, unsafeCSS, type PropertyValues} from 'lit';\nimport {customElement} from '../../decorator.js';\nimport {releaseFocusBefore} from '../../internal/focus.js';\nimport {classMap} from 'lit/directives/class-map.js';\nimport componentStyle from './accordion-item.css?inline';\nimport {property} from 'lit/decorators.js';\nimport '../../icons/icon-chevron-down-google.js';\n\n/**\n * `<obc-accordion-item>` – A collapsible section component for toggling the visibility of content panels (commonly known as an accordion item, expandable panel, or disclosure).\n *\n * Provides a single expandable/collapsible section with a clickable header and an optional content area. Useful for organizing related information in a compact, vertically stacked layout, allowing users to reveal or hide details as needed.\n *\n * ### Features\n * - **Toggleable Expansion:** Click the header to expand or collapse the content area. The panel slides open and closed, and the chevron turns with it; `prefers-reduced-motion: reduce` switches instantly instead.\n * - **Disabled State:** Prevents user interaction and visually indicates non-interactivity.\n * - **Customizable Content:** Slot for custom expanded content (`expanded-content`), supporting any HTML or component.\n * - **Optional Divider:** Can display a divider line below the item for visual separation in lists.\n * - **Accessible:** Uses a button for the header with appropriate ARIA attributes for accessibility.\n *\n * ### Usage Guidelines\n * Use `obc-accordion-item` to organize content into expandable sections, such as FAQs, settings panels, or grouped lists. Ideal when you want to let users scan section titles and expand only those relevant to them. For multi-item accordions, stack several `obc-accordion-item` components vertically.\n *\n * - Use the `title` property for the section label.\n * - Place any custom content (text, forms, components) in the `expanded-content` slot.\n * - Use the `disabled` property to prevent interaction for items that should not be toggled.\n * - Use the `showDivider` property to visually separate items in a list.\n * - If you need a persistent panel (not collapsible), use a different container component.\n *\n * **TODO(designer):** Confirm if there are recommended patterns for managing open/close state in multi-item accordions (e.g., only one open at a time), or if this is left to the developer.\n *\n * ### Slots\n * | Slot Name | Renders When... | Purpose |\n * |-------------------|--------------------------|-------------------------------------------------|\n * | expanded-content | Always | Custom content revealed while the item is open |\n *\n * ### Events\n * - `accordion-item-toggle` – Fired when the item is toggled open or closed. Event detail: `{ open: boolean, title: string }`\n *\n * ### Best Practices\n * - Keep section titles concise for easy scanning.\n * - Avoid placing interactive controls in the header area to prevent accidental toggling.\n * - For accessibility, ensure each item’s title is unique within a group.\n *\n * **Example:**\n * ```\n * <obc-accordion-item title=\"Details\" .open=${false}>\n * <div slot=\"expanded-content\">\n * <p>Additional information goes here.</p>\n * </div>\n * </obc-accordion-item>\n * ```\n *\n * @property title - The label displayed in the header of the accordion item.\n * @property open - Whether the item is expanded (shows the content area).\n * Set to `true` to expand, `false` to collapse.\n * @property disabled - Disables the accordion item, preventing user interaction and dimming the appearance.\n * @property showDivider - Displays a divider line below the item for visual separation.\n * Useful when stacking multiple items in a list.\n * @slot expanded-content - Content revealed while the item is open.\n * @fires {CustomEvent<{open: boolean, title: string}>} accordion-item-toggle - Fired when the item is toggled open or closed.\n * @cssprop [--obc-accordion-expand-duration=200ms] - How long the panel takes to open and close. The chevron and the header's corners follow it. Ignored under `prefers-reduced-motion: reduce`.\n * @stable\n */\n@customElement('obc-accordion-item')\nexport class ObcAccordionItem extends LitElement {\n @property({type: String}) override title = '';\n\n @property({type: Boolean}) open = false;\n\n @property({type: Boolean}) disabled = false;\n\n @property({type: Boolean}) showDivider = false;\n\n private handleToggle() {\n if (this.disabled) return;\n\n this.open = !this.open;\n\n this.dispatchEvent(\n new CustomEvent('accordion-item-toggle', {\n detail: {\n open: this.open,\n title: this.title,\n },\n })\n );\n }\n\n /**\n * Keeps the keyboard somewhere predictable when the panel closes.\n *\n * A collapsed panel is `inert`, and the browser drops focus out of anything\n * it makes inert — onto `<body>`, two frames later. Clicking the header\n * already leaves focus there; a consumer setting `open` while a slotted\n * control has focus is the case that needs the transfer.\n */\n override willUpdate(changed: PropertyValues<this>) {\n if (!changed.has('open') || this.open) return;\n\n const panel = this.shadowRoot?.querySelector('.panel');\n if (!panel) return;\n releaseFocusBefore(\n panel,\n this.shadowRoot?.querySelector<HTMLButtonElement>('.content-button'),\n this.shadowRoot?.querySelector<HTMLElement>('.wrapper')\n );\n }\n\n private renderContent() {\n return html`\n <div class=\"content\">\n <div class=\"leading-content-container\">\n <div class=\"label-frame\">\n <div class=\"title\">${this.title}</div>\n </div>\n </div>\n <div class=\"trailing-icon\">\n <obi-chevron-down-google></obi-chevron-down-google>\n </div>\n </div>\n `;\n }\n\n private renderExpandedContent() {\n return html`\n <div class=\"panel\" id=\"accordion-content\" ?inert=${!this.open}>\n <div class=\"panel-inner\">\n <div class=\"content-container\">\n <slot name=\"expanded-content\"></slot>\n </div>\n </div>\n </div>\n `;\n }\n\n private renderDivider() {\n if (!this.showDivider) return '';\n\n return html`<div class=\"divider\"></div>`;\n }\n\n override render() {\n return html`\n <div\n tabindex=\"-1\"\n class=${classMap({\n wrapper: true,\n 'state-disabled': this.disabled,\n 'open-true': this.open,\n 'open-false': !this.open,\n })}\n >\n <button\n class=\"content-button\"\n @click=${this.handleToggle}\n ?disabled=${this.disabled}\n aria-expanded=${this.open}\n aria-controls=\"accordion-content\"\n >\n ${this.renderContent()}\n </button>\n\n ${this.renderExpandedContent()} ${this.renderDivider()}\n </div>\n `;\n }\n\n static override styles = unsafeCSS(componentStyle);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'obc-accordion-item': ObcAccordionItem;\n }\n}\n"],"mappings":";;;;;;;;;AAiEO,IAAM,mBAAN,MAAM,yBAAyB,WAAW;;;EACJ,KAAA,QAAA;EAET,KAAA,OAAA;EAEI,KAAA,WAAA;EAEG,KAAA,cAAA;;CAEzC,eAAuB;EACrB,IAAI,KAAK,UAAU;EAEnB,KAAK,OAAO,CAAC,KAAK;EAElB,KAAK,cACH,IAAI,YAAY,yBAAyB,EACvC,QAAQ;GACN,MAAM,KAAK;GACX,OAAO,KAAK;EACd,EACF,CAAC,CACH;CACF;;;;;;;;;CAUA,WAAoB,SAA+B;EACjD,IAAI,CAAC,QAAQ,IAAI,MAAM,KAAK,KAAK,MAAM;EAEvC,MAAM,QAAQ,KAAK,YAAY,cAAc,QAAQ;EACrD,IAAI,CAAC,OAAO;EACZ,mBACE,OACA,KAAK,YAAY,cAAiC,iBAAiB,GACnE,KAAK,YAAY,cAA2B,UAAU,CACxD;CACF;CAEA,gBAAwB;EACtB,OAAO,IAAI;;;;iCAIkB,KAAK,MAAM;;;;;;;;CAQ1C;CAEA,wBAAgC;EAC9B,OAAO,IAAI;yDAC0C,CAAC,KAAK,KAAK;;;;;;;;CAQlE;CAEA,gBAAwB;EACtB,IAAI,CAAC,KAAK,aAAa,OAAO;EAE9B,OAAO,IAAI;CACb;CAEA,SAAkB;EAChB,OAAO,IAAI;;;gBAGC,SAAS;GACf,SAAS;GACT,kBAAkB,KAAK;GACvB,aAAa,KAAK;GAClB,cAAc,CAAC,KAAK;EACtB,CAAC,EAAE;;;;mBAIQ,KAAK,aAAa;sBACf,KAAK,SAAS;0BACV,KAAK,KAAK;;;YAGxB,KAAK,cAAc,EAAE;;;UAGvB,KAAK,sBAAsB,EAAE,GAAG,KAAK,cAAc,EAAE;;;CAG7D;;EAEyB,KAAA,SAAA,UAAU,sBAAc;;AACnD;AAvGG,WAAA,CAAA,SAAS,EAAC,MAAM,OAAM,CAAC,CAAA,GAAA,iBAAA,WAAA,SAAA,KAAA,CAAA;AAEvB,WAAA,CAAA,SAAS,EAAC,MAAM,QAAO,CAAC,CAAA,GAAA,iBAAA,WAAA,QAAA,KAAA,CAAA;AAExB,WAAA,CAAA,SAAS,EAAC,MAAM,QAAO,CAAC,CAAA,GAAA,iBAAA,WAAA,YAAA,KAAA,CAAA;AAExB,WAAA,CAAA,SAAS,EAAC,MAAM,QAAO,CAAC,CAAA,GAAA,iBAAA,WAAA,eAAA,KAAA,CAAA;AAR1B,mBAAA,WAAA,CAAA,cAAc,oBAAoB,CAAA,GAAA,gBAAA"}
@@ -8,6 +8,29 @@
8
8
  */
9
9
  /** The focused element, following every open shadow root down. */
10
10
  export declare function deepActiveElement(): Element | null;
11
+ /**
12
+ * Whether `node` sits inside `root` in the composed tree.
13
+ *
14
+ * `Node.contains()` walks the DOM tree, where slotted light-DOM content is a
15
+ * child of the host and not of the slot — so it answers no for exactly the
16
+ * content a component projects into itself. This follows `assignedSlot` and
17
+ * shadow hosts as well.
18
+ */
19
+ export declare function composedContains(root: Node, node: Node | null): boolean;
20
+ /**
21
+ * Moves focus out of `hidden`, to the first fallback that will take it.
22
+ *
23
+ * Call it just before a region becomes `inert` or is taken out of the page.
24
+ * The browser blurs whatever it hides, and focus lands on `<body>` a frame or
25
+ * two later, so the next Tab press starts from the top of the document instead
26
+ * of where the user was.
27
+ *
28
+ * Each candidate is tried and then checked, rather than inspected first: a
29
+ * `disabled` button, a hidden one and an element with no tabindex all refuse
30
+ * focus for different reasons, and the only reliable question is whether focus
31
+ * actually left.
32
+ */
33
+ export declare function releaseFocusBefore(hidden: Node, ...fallbacks: (HTMLElement | null | undefined)[]): void;
11
34
  /**
12
35
  * The elements under `root` a Tab press could land on, in rendered order:
13
36
  * shadow children before light children, light children only where a slot
@@ -1 +1 @@
1
- {"version":3,"file":"focus.d.ts","sourceRoot":"","sources":["../../src/internal/focus.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,kEAAkE;AAClE,wBAAgB,iBAAiB,IAAI,OAAO,GAAG,IAAI,CAMlD;AASD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,EAAE,CAkB3D"}
1
+ {"version":3,"file":"focus.d.ts","sourceRoot":"","sources":["../../src/internal/focus.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,kEAAkE;AAClE,wBAAgB,iBAAiB,IAAI,OAAO,GAAG,IAAI,CAMlD;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO,CAUvE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,IAAI,EACZ,GAAG,SAAS,EAAE,CAAC,WAAW,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,GAC/C,IAAI,CAON;AASD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,EAAE,CAkB3D"}
@@ -14,6 +14,43 @@ function deepActiveElement() {
14
14
  while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
15
15
  return active;
16
16
  }
17
+ /**
18
+ * Whether `node` sits inside `root` in the composed tree.
19
+ *
20
+ * `Node.contains()` walks the DOM tree, where slotted light-DOM content is a
21
+ * child of the host and not of the slot — so it answers no for exactly the
22
+ * content a component projects into itself. This follows `assignedSlot` and
23
+ * shadow hosts as well.
24
+ */
25
+ function composedContains(root, node) {
26
+ let current = node;
27
+ while (current) {
28
+ if (current === root) return true;
29
+ current = current.assignedSlot ?? current.parentNode ?? (current instanceof ShadowRoot ? current.host : null);
30
+ }
31
+ return false;
32
+ }
33
+ /**
34
+ * Moves focus out of `hidden`, to the first fallback that will take it.
35
+ *
36
+ * Call it just before a region becomes `inert` or is taken out of the page.
37
+ * The browser blurs whatever it hides, and focus lands on `<body>` a frame or
38
+ * two later, so the next Tab press starts from the top of the document instead
39
+ * of where the user was.
40
+ *
41
+ * Each candidate is tried and then checked, rather than inspected first: a
42
+ * `disabled` button, a hidden one and an element with no tabindex all refuse
43
+ * focus for different reasons, and the only reliable question is whether focus
44
+ * actually left.
45
+ */
46
+ function releaseFocusBefore(hidden, ...fallbacks) {
47
+ if (!composedContains(hidden, deepActiveElement())) return;
48
+ for (const candidate of fallbacks) {
49
+ if (!candidate) continue;
50
+ candidate.focus();
51
+ if (!composedContains(hidden, deepActiveElement())) return;
52
+ }
53
+ }
17
54
  function isTabbable(element) {
18
55
  if (!element.matches(TABBABLE_SELECTOR)) return false;
19
56
  if (element.tabIndex < 0) return false;
@@ -45,6 +82,6 @@ function composedTabbables(root) {
45
82
  return found;
46
83
  }
47
84
  //#endregion
48
- export { composedTabbables, deepActiveElement };
85
+ export { composedContains, composedTabbables, deepActiveElement, releaseFocusBefore };
49
86
 
50
87
  //# sourceMappingURL=focus.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"focus.js","names":[],"sources":["../../src/internal/focus.ts"],"sourcesContent":["/**\n * Focus queries that see through open shadow roots.\n *\n * `document.activeElement` and `querySelectorAll` both stop at a shadow\n * boundary, so a component that focuses a control it renders reads as\n * unfocused from outside, and a dialog cannot find the controls it has to\n * cycle between. These walk the composed tree instead.\n */\n\nconst TABBABLE_SELECTOR =\n 'a[href], button, input, select, textarea, [tabindex], [contenteditable=\"true\"]';\n\n/** The focused element, following every open shadow root down. */\nexport function deepActiveElement(): Element | null {\n let active = document.activeElement;\n while (active?.shadowRoot?.activeElement) {\n active = active.shadowRoot.activeElement;\n }\n return active;\n}\n\nfunction isTabbable(element: Element): element is HTMLElement {\n if (!element.matches(TABBABLE_SELECTOR)) return false;\n if ((element as HTMLElement).tabIndex < 0) return false;\n if ((element as HTMLButtonElement).disabled) return false;\n return element.getClientRects().length > 0;\n}\n\n/**\n * The elements under `root` a Tab press could land on, in rendered order:\n * shadow children before light children, light children only where a slot\n * places them.\n */\nexport function composedTabbables(root: Node): HTMLElement[] {\n const found: HTMLElement[] = [];\n const visit = (node: Node) => {\n if (node instanceof HTMLSlotElement) {\n node.assignedNodes({flatten: true}).forEach(visit);\n return;\n }\n if (node instanceof Element) {\n if (isTabbable(node)) found.push(node);\n if (node.shadowRoot) {\n Array.from(node.shadowRoot.childNodes).forEach(visit);\n return;\n }\n }\n Array.from(node.childNodes).forEach(visit);\n };\n visit(root);\n return found;\n}\n"],"mappings":";;;;;;;;;AASA,IAAM,oBACJ;;AAGF,SAAgB,oBAAoC;CAClD,IAAI,SAAS,SAAS;CACtB,OAAO,QAAQ,YAAY,eACzB,SAAS,OAAO,WAAW;CAE7B,OAAO;AACT;AAEA,SAAS,WAAW,SAA0C;CAC5D,IAAI,CAAC,QAAQ,QAAQ,iBAAiB,GAAG,OAAO;CAChD,IAAK,QAAwB,WAAW,GAAG,OAAO;CAClD,IAAK,QAA8B,UAAU,OAAO;CACpD,OAAO,QAAQ,eAAe,CAAC,CAAC,SAAS;AAC3C;;;;;;AAOA,SAAgB,kBAAkB,MAA2B;CAC3D,MAAM,QAAuB,CAAC;CAC9B,MAAM,SAAS,SAAe;EAC5B,IAAI,gBAAgB,iBAAiB;GACnC,KAAK,cAAc,EAAC,SAAS,KAAI,CAAC,CAAC,CAAC,QAAQ,KAAK;GACjD;EACF;EACA,IAAI,gBAAgB,SAAS;GAC3B,IAAI,WAAW,IAAI,GAAG,MAAM,KAAK,IAAI;GACrC,IAAI,KAAK,YAAY;IACnB,MAAM,KAAK,KAAK,WAAW,UAAU,CAAC,CAAC,QAAQ,KAAK;IACpD;GACF;EACF;EACA,MAAM,KAAK,KAAK,UAAU,CAAC,CAAC,QAAQ,KAAK;CAC3C;CACA,MAAM,IAAI;CACV,OAAO;AACT"}
1
+ {"version":3,"file":"focus.js","names":[],"sources":["../../src/internal/focus.ts"],"sourcesContent":["/**\n * Focus queries that see through open shadow roots.\n *\n * `document.activeElement` and `querySelectorAll` both stop at a shadow\n * boundary, so a component that focuses a control it renders reads as\n * unfocused from outside, and a dialog cannot find the controls it has to\n * cycle between. These walk the composed tree instead.\n */\n\nconst TABBABLE_SELECTOR =\n 'a[href], button, input, select, textarea, [tabindex], [contenteditable=\"true\"]';\n\n/** The focused element, following every open shadow root down. */\nexport function deepActiveElement(): Element | null {\n let active = document.activeElement;\n while (active?.shadowRoot?.activeElement) {\n active = active.shadowRoot.activeElement;\n }\n return active;\n}\n\n/**\n * Whether `node` sits inside `root` in the composed tree.\n *\n * `Node.contains()` walks the DOM tree, where slotted light-DOM content is a\n * child of the host and not of the slot — so it answers no for exactly the\n * content a component projects into itself. This follows `assignedSlot` and\n * shadow hosts as well.\n */\nexport function composedContains(root: Node, node: Node | null): boolean {\n let current: Node | null = node;\n while (current) {\n if (current === root) return true;\n current =\n (current as Element).assignedSlot ??\n current.parentNode ??\n (current instanceof ShadowRoot ? current.host : null);\n }\n return false;\n}\n\n/**\n * Moves focus out of `hidden`, to the first fallback that will take it.\n *\n * Call it just before a region becomes `inert` or is taken out of the page.\n * The browser blurs whatever it hides, and focus lands on `<body>` a frame or\n * two later, so the next Tab press starts from the top of the document instead\n * of where the user was.\n *\n * Each candidate is tried and then checked, rather than inspected first: a\n * `disabled` button, a hidden one and an element with no tabindex all refuse\n * focus for different reasons, and the only reliable question is whether focus\n * actually left.\n */\nexport function releaseFocusBefore(\n hidden: Node,\n ...fallbacks: (HTMLElement | null | undefined)[]\n): void {\n if (!composedContains(hidden, deepActiveElement())) return;\n for (const candidate of fallbacks) {\n if (!candidate) continue;\n candidate.focus();\n if (!composedContains(hidden, deepActiveElement())) return;\n }\n}\n\nfunction isTabbable(element: Element): element is HTMLElement {\n if (!element.matches(TABBABLE_SELECTOR)) return false;\n if ((element as HTMLElement).tabIndex < 0) return false;\n if ((element as HTMLButtonElement).disabled) return false;\n return element.getClientRects().length > 0;\n}\n\n/**\n * The elements under `root` a Tab press could land on, in rendered order:\n * shadow children before light children, light children only where a slot\n * places them.\n */\nexport function composedTabbables(root: Node): HTMLElement[] {\n const found: HTMLElement[] = [];\n const visit = (node: Node) => {\n if (node instanceof HTMLSlotElement) {\n node.assignedNodes({flatten: true}).forEach(visit);\n return;\n }\n if (node instanceof Element) {\n if (isTabbable(node)) found.push(node);\n if (node.shadowRoot) {\n Array.from(node.shadowRoot.childNodes).forEach(visit);\n return;\n }\n }\n Array.from(node.childNodes).forEach(visit);\n };\n visit(root);\n return found;\n}\n"],"mappings":";;;;;;;;;AASA,IAAM,oBACJ;;AAGF,SAAgB,oBAAoC;CAClD,IAAI,SAAS,SAAS;CACtB,OAAO,QAAQ,YAAY,eACzB,SAAS,OAAO,WAAW;CAE7B,OAAO;AACT;;;;;;;;;AAUA,SAAgB,iBAAiB,MAAY,MAA4B;CACvE,IAAI,UAAuB;CAC3B,OAAO,SAAS;EACd,IAAI,YAAY,MAAM,OAAO;EAC7B,UACG,QAAoB,gBACrB,QAAQ,eACP,mBAAmB,aAAa,QAAQ,OAAO;CACpD;CACA,OAAO;AACT;;;;;;;;;;;;;;AAeA,SAAgB,mBACd,QACA,GAAG,WACG;CACN,IAAI,CAAC,iBAAiB,QAAQ,kBAAkB,CAAC,GAAG;CACpD,KAAK,MAAM,aAAa,WAAW;EACjC,IAAI,CAAC,WAAW;EAChB,UAAU,MAAM;EAChB,IAAI,CAAC,iBAAiB,QAAQ,kBAAkB,CAAC,GAAG;CACtD;AACF;AAEA,SAAS,WAAW,SAA0C;CAC5D,IAAI,CAAC,QAAQ,QAAQ,iBAAiB,GAAG,OAAO;CAChD,IAAK,QAAwB,WAAW,GAAG,OAAO;CAClD,IAAK,QAA8B,UAAU,OAAO;CACpD,OAAO,QAAQ,eAAe,CAAC,CAAC,SAAS;AAC3C;;;;;;AAOA,SAAgB,kBAAkB,MAA2B;CAC3D,MAAM,QAAuB,CAAC;CAC9B,MAAM,SAAS,SAAe;EAC5B,IAAI,gBAAgB,iBAAiB;GACnC,KAAK,cAAc,EAAC,SAAS,KAAI,CAAC,CAAC,CAAC,QAAQ,KAAK;GACjD;EACF;EACA,IAAI,gBAAgB,SAAS;GAC3B,IAAI,WAAW,IAAI,GAAG,MAAM,KAAK,IAAI;GACrC,IAAI,KAAK,YAAY;IACnB,MAAM,KAAK,KAAK,WAAW,UAAU,CAAC,CAAC,QAAQ,KAAK;IACpD;GACF;EACF;EACA,MAAM,KAAK,KAAK,UAAU,CAAC,CAAC,QAAQ,KAAK;CAC3C;CACA,MAAM,IAAI;CACV,OAAO;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oicl/openbridge-webcomponents",
3
- "version": "2.0.0-next.172",
3
+ "version": "2.0.0-next.173",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",