@nysds/nys-tab 1.18.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.
- package/dist/index.d.ts +3 -0
- package/dist/nys-tab.d.ts +116 -0
- package/dist/nys-tab.figma.d.ts +1 -0
- package/dist/nys-tab.js +367 -0
- package/dist/nys-tab.js.map +1 -0
- package/dist/nys-tabgroup.d.ts +197 -0
- package/dist/nys-tabpanel.d.ts +46 -0
- package/package.json +43 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { LitElement } from "lit";
|
|
2
|
+
/**
|
|
3
|
+
* `<nys-tab>` is a single tab within a `<nys-tabgroup>`.
|
|
4
|
+
*
|
|
5
|
+
* The host element carries `role="tab"`, `tabindex`, `aria-selected`,
|
|
6
|
+
* `aria-controls`, and `aria-disabled` so assistive technologies see the
|
|
7
|
+
* correct ARIA tab semantics on the element that is actually focused.
|
|
8
|
+
* `<nys-tabgroup>` manages `tabindex`, `aria-selected`, and `aria-controls`
|
|
9
|
+
* via `_applySelection`; do not set them directly on this element.
|
|
10
|
+
*
|
|
11
|
+
* @element nys-tab
|
|
12
|
+
*
|
|
13
|
+
* @fires nys-tab-select - Dispatched when the tab is activated via click or
|
|
14
|
+
* Enter / Space. Bubbles and crosses shadow DOM boundaries.
|
|
15
|
+
* `detail: { id: string, label: string }`
|
|
16
|
+
* @fires nys-tab-focus - Dispatched when the host receives focus. Bubbles and
|
|
17
|
+
* crosses shadow DOM boundaries. `detail: { id: string }`
|
|
18
|
+
* @fires nys-tab-blur - Dispatched when the host loses focus. Bubbles and
|
|
19
|
+
* crosses shadow DOM boundaries. `detail: { id: string }`
|
|
20
|
+
*
|
|
21
|
+
* @slot - No slots; content is derived from the `label` property.
|
|
22
|
+
*
|
|
23
|
+
* @example `<nys-tab>` and `<nys-tabpanel>` should always be wrapped by `<nys-tabgroup>`
|
|
24
|
+
* ```html
|
|
25
|
+
* <!-- Always place <nys-tab> elements inside a <nys-tabgroup>. -->
|
|
26
|
+
* <nys-tabgroup name="My Tabs">
|
|
27
|
+
* <nys-tab label="Overview"></nys-tab>
|
|
28
|
+
* <nys-tab label="Details" selected></nys-tab>
|
|
29
|
+
* <nys-tab label="Archived" disabled></nys-tab>
|
|
30
|
+
* <nys-tabpanel><p>Overview content</p></nys-tabpanel>
|
|
31
|
+
* <nys-tabpanel><p>Details content (shown by default)</p></nys-tabpanel>
|
|
32
|
+
* <nys-tabpanel><p>Archived content</p></nys-tabpanel>
|
|
33
|
+
* </nys-tabgroup>
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare class NysTab extends LitElement {
|
|
37
|
+
static styles: import("lit").CSSResult;
|
|
38
|
+
/**
|
|
39
|
+
* Unique identifier for the tab element.
|
|
40
|
+
* Reflected to the DOM attribute so `aria-controls` references resolve.
|
|
41
|
+
*
|
|
42
|
+
* @attr id
|
|
43
|
+
*/
|
|
44
|
+
id: string;
|
|
45
|
+
/**
|
|
46
|
+
* Visible text label rendered inside the inner `<span>`.
|
|
47
|
+
*
|
|
48
|
+
* @attr label
|
|
49
|
+
*/
|
|
50
|
+
label: string;
|
|
51
|
+
/**
|
|
52
|
+
* Whether this tab is the currently active tab.
|
|
53
|
+
* Managed by `<nys-tabgroup>`; reflected for CSS attribute selectors.
|
|
54
|
+
*
|
|
55
|
+
* @attr selected
|
|
56
|
+
*/
|
|
57
|
+
selected: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Whether this tab is disabled.
|
|
60
|
+
* Reflected to the DOM attribute for CSS styling.
|
|
61
|
+
*
|
|
62
|
+
* @attr disabled
|
|
63
|
+
*/
|
|
64
|
+
disabled: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Sets `role="tab"` and `tabindex="-1"` on the host (the element that AT
|
|
67
|
+
* will read and that receives keyboard focus). Attaches host-level listeners
|
|
68
|
+
* for keydown, focus, blur, and click so that interaction events work
|
|
69
|
+
* correctly on the host element itself.
|
|
70
|
+
*
|
|
71
|
+
* Click is handled at the host level so iOS VoiceOver double-tap (which
|
|
72
|
+
* dispatches `click` directly on the host because of `role="tab"`, bypassing
|
|
73
|
+
* shadow-DOM children) activates the tab. Normal taps land on the inner
|
|
74
|
+
* `<span>` and bubble up to this listener.
|
|
75
|
+
*
|
|
76
|
+
* `<nys-tabgroup>` overrides `tabindex` to `"0"` on the selected tab.
|
|
77
|
+
*/
|
|
78
|
+
connectedCallback(): void;
|
|
79
|
+
disconnectedCallback(): void;
|
|
80
|
+
/**
|
|
81
|
+
* Keeps `aria-disabled` on the host in sync with the `disabled` property so
|
|
82
|
+
* AT perceives the disabled state on the element it actually focuses.
|
|
83
|
+
*/
|
|
84
|
+
updated(changed: Map<string, unknown>): void;
|
|
85
|
+
/**
|
|
86
|
+
* Focuses the host element. The host carries `role="tab"` and `tabindex`,
|
|
87
|
+
* so it is the correct element for AT to land on.
|
|
88
|
+
*/
|
|
89
|
+
focus(options?: FocusOptions): void;
|
|
90
|
+
/**
|
|
91
|
+
* Enter / Space on the focused host activate the tab.
|
|
92
|
+
* Arrow-key navigation is handled one level up by `<nys-tabgroup>`.
|
|
93
|
+
*/
|
|
94
|
+
private _onKeydown;
|
|
95
|
+
/**
|
|
96
|
+
* Host focus → dispatch `nys-tab-focus` for external observers.
|
|
97
|
+
*/
|
|
98
|
+
private _onFocus;
|
|
99
|
+
/**
|
|
100
|
+
* Host blur → dispatch `nys-tab-blur` for external observers.
|
|
101
|
+
*/
|
|
102
|
+
private _onBlur;
|
|
103
|
+
/**
|
|
104
|
+
* Host-level click handler. Activates the tab regardless of whether the
|
|
105
|
+
* click landed on the inner element (normal pointer/keyboard tap, which
|
|
106
|
+
* bubbles up) or directly on the host (iOS VoiceOver double-tap dispatches
|
|
107
|
+
* `click` on the element with `role="tab"`, bypassing shadow-DOM children).
|
|
108
|
+
*/
|
|
109
|
+
private _onClick;
|
|
110
|
+
/**
|
|
111
|
+
* Focuses the host then dispatches `nys-tab-select`. Called from both the
|
|
112
|
+
* click handler and the keydown handler.
|
|
113
|
+
*/
|
|
114
|
+
private _handleClick;
|
|
115
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
116
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/nys-tab.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { LitElement as p, unsafeCSS as v, html as _ } from "lit";
|
|
2
|
+
import { property as c } from "lit/decorators.js";
|
|
3
|
+
/*!
|
|
4
|
+
* █▄ █ █ █ █▀▀▀█ █▀▀▄ █▀▀▀█
|
|
5
|
+
* █ █ █ █▄▄▄█ ▀▀▀▄▄ █ █ ▀▀▀▄▄
|
|
6
|
+
* █ ▀█ █ █▄▄▄█ █▄▄▀ █▄▄▄█
|
|
7
|
+
*
|
|
8
|
+
* Tab Component v{version}
|
|
9
|
+
* Part of the New York State Design System
|
|
10
|
+
* Repository: https://github.com/its-hcd/nysds
|
|
11
|
+
* License: MIT
|
|
12
|
+
*/
|
|
13
|
+
const f = ':host{--_nys-tabgroup-gap: var(--nys-space-100, 8px);--_nys-tabgroup-padding: var(--nys-space-50, 4px);--_nys-tabgroup-background-color: var(--nys-color-surface, #ffffff);--_nys-tab-border-width: 3px;--_nys-tab-border-radius: var(--nys-radius-md, 4px);--_nys-tab-border-color: var(--nys-color-neutral-50);--_nys-tab-border-color--hover: var(--nys-color-theme-weak, #cddde9);--_nys-tab-border-color--active: var(--nys-color-theme-mid, #457aa5);--_nys-tab-border-color--disabled: var(--_nys-tab-border-color);--_nys-tab-border-color--selected: var(--nys-color-theme, #154973);--_nys-tab-border-color--selected--hover: var( --nys-color-theme-strong, #0e324f );--_nys-tab-border-color--selected--active: var( --nys-color-theme-stronger, #081b2b );--_nys-tab-background-color: var(--nys-color-surface, #ffffff);--_nys-tab-background-color--hover: var(--nys-color-theme-weaker, #eff6fb);--_nys-tab-background-color--active: var(--nys-color-theme-weak, #cddde9);--_nys-tab-background-color--disabled: var(--_nys-tab-background-color);--_nys-tab-background-color--selected: var(--nys-color-neutral-10, #f6f6f6);--_nys-tab-color: var(--nys-color-ink);--_nys-tab-padding--x: var(--nys-space-150, 12px);--_nys-tab-padding--y: var(--nys-space-200, 16px);--_nys-tabpanel-padding: var(--nys-space-400, 32px);--_nys-tabpanel-max-height: var(--nys-tabpanel-max-height)}.nys-tab{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;appearance:none;-webkit-appearance:none;padding:var(--_nys-tab-padding--y) var(--_nys-tab-padding--x);border-color:var(--_nys-tab-border-color);border-style:none none solid;border-width:var(--_nys-tab-border-width);border-radius:var(--_nys-tab-border-radius) var(--_nys-tab-border-radius) 0 0;background-color:var(--_nys-tab-background-color);color:var(--_nys-tab-color);font-family:var(--nys-font-family-ui, var(--nys-font-family-sans, "Proxima Nova", "Helvetica Neue", "Helvetica", "Arial", sans-serif));font-size:var(--nys-font-size-ui-md, 16px);font-weight:var(--nys-font-weight-semibold, 600);line-height:var(--nys-size-200, 16px);text-decoration:none}:host(:not([disabled])) .nys-tab:hover{background-color:var(--_nys-tab-background-color--hover);border-color:var(--_nys-tab-border-color--hover);color:var(--_nys-tab-color)}:host(:not([disabled])) .nys-tab:active{background-color:var(--_nys-tab-background-color--active);border-color:var(--_nys-tab-border-color--active);color:var(--_nys-tab-color)}:host([disabled]) .nys-tab{background-color:var(--_nys-tab-background-color--disabled);border-color:var(--_nys-tab-border-color--disabled);color:var(--nys-color-text-disabled, #bec0c1);cursor:not-allowed;pointer-events:auto}:host(:focus-visible){outline:none}:host(:focus-visible) .nys-tab{outline:solid var(--nys-border-width-md, 2px) var(--nys-color-focus, #004dd1);outline-offset:var(--nys-space-2px, 2px)}:host([selected]) .nys-tab{background-color:var(--_nys-tab-background-color--selected);border-color:var(--_nys-tab-border-color--selected)}:host([selected]:not([disabled])) .nys-tab:hover{border-color:var(--_nys-tab-border-color--selected--hover)}:host([selected]:not([disabled])) .nys-tab:active{border-color:var(--_nys-tab-border-color--selected--active)}.nys-tabgroup{background-color:var(--_nys-tabgroup-background-color)}.nys-tabgroup__tabs-container{position:relative}.nys-tabgroup__tabs-container .scroll-shadow{position:absolute;top:50%;transform:translateY(-50%);z-index:2;opacity:0;pointer-events:none;transition:opacity .2s;height:calc(var(--nys-space-600, 48px) + var(--_nys-tab-border-width));width:var(--nys-space-200, 16px)}.nys-tabgroup__tabs-container .scroll-shadow--left{left:0;background-image:linear-gradient(to left,transparent,var(--nys-color-neutral-100, #D0D0CE))}.nys-tabgroup__tabs-container .scroll-shadow--right{right:0;background-image:linear-gradient(to right,transparent,var(--nys-color-neutral-100, #D0D0CE))}.nys-tabgroup__tabs-container .scroll-shadow.is-visible{opacity:1}.nys-tabgroup__tabs-container .nys-tabgroup__tabs-background{position:absolute;inset:0;margin:var(--_nys-tabgroup-padding);border-bottom:solid var(--_nys-tab-border-color) var(--_nys-tab-border-width)}.nys-tabgroup__tabs-container .nys-tabgroup__tabs{position:relative;display:flex;gap:var(--_nys-tabgroup-gap);overflow-x:auto;white-space:nowrap;-ms-overflow-style:none;scrollbar-width:none;padding:var(--_nys-tabgroup-padding)}.nys-tabgroup__tabs-container .nys-tabgroup__tabs::-webkit-scrollbar{display:none}.nys-tabgroup__panels{padding:var(--_nys-tabpanel-padding);background-color:var(--_nys-tabpanel-background-color);max-height:var(--_nys-tabpanel-max-height);overflow-y:auto}';
|
|
14
|
+
var C = Object.defineProperty, y = (i, t, r, a) => {
|
|
15
|
+
for (var e = void 0, s = i.length - 1, o; s >= 0; s--)
|
|
16
|
+
(o = i[s]) && (e = o(t, r, e) || e);
|
|
17
|
+
return e && C(t, r, e), e;
|
|
18
|
+
};
|
|
19
|
+
let S = 0;
|
|
20
|
+
const g = class g extends p {
|
|
21
|
+
constructor() {
|
|
22
|
+
super(...arguments), this.id = "", this.label = "", this.selected = !1, this.disabled = !1, this._onKeydown = (t) => {
|
|
23
|
+
this.disabled || t.key !== "Enter" && t.key !== " " || (t.preventDefault(), this._handleClick());
|
|
24
|
+
}, this._onFocus = () => {
|
|
25
|
+
this.dispatchEvent(
|
|
26
|
+
new CustomEvent("nys-tab-focus", {
|
|
27
|
+
bubbles: !0,
|
|
28
|
+
composed: !0,
|
|
29
|
+
detail: { id: this.id }
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
}, this._onBlur = () => {
|
|
33
|
+
this.dispatchEvent(
|
|
34
|
+
new CustomEvent("nys-tab-blur", {
|
|
35
|
+
bubbles: !0,
|
|
36
|
+
composed: !0,
|
|
37
|
+
detail: { id: this.id }
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
}, this._onClick = () => {
|
|
41
|
+
this._handleClick();
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Lifecycle
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
/**
|
|
48
|
+
* Sets `role="tab"` and `tabindex="-1"` on the host (the element that AT
|
|
49
|
+
* will read and that receives keyboard focus). Attaches host-level listeners
|
|
50
|
+
* for keydown, focus, blur, and click so that interaction events work
|
|
51
|
+
* correctly on the host element itself.
|
|
52
|
+
*
|
|
53
|
+
* Click is handled at the host level so iOS VoiceOver double-tap (which
|
|
54
|
+
* dispatches `click` directly on the host because of `role="tab"`, bypassing
|
|
55
|
+
* shadow-DOM children) activates the tab. Normal taps land on the inner
|
|
56
|
+
* `<span>` and bubble up to this listener.
|
|
57
|
+
*
|
|
58
|
+
* `<nys-tabgroup>` overrides `tabindex` to `"0"` on the selected tab.
|
|
59
|
+
*/
|
|
60
|
+
connectedCallback() {
|
|
61
|
+
super.connectedCallback(), this.id || (this.id = `nys-tab-${Date.now()}-${S++}`), this.setAttribute("role", "tab"), this.setAttribute("tabindex", "-1"), this.addEventListener("keydown", this._onKeydown), this.addEventListener("focus", this._onFocus), this.addEventListener("blur", this._onBlur), this.addEventListener("click", this._onClick);
|
|
62
|
+
}
|
|
63
|
+
disconnectedCallback() {
|
|
64
|
+
super.disconnectedCallback(), this.removeEventListener("keydown", this._onKeydown), this.removeEventListener("focus", this._onFocus), this.removeEventListener("blur", this._onBlur), this.removeEventListener("click", this._onClick);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Keeps `aria-disabled` on the host in sync with the `disabled` property so
|
|
68
|
+
* AT perceives the disabled state on the element it actually focuses.
|
|
69
|
+
*/
|
|
70
|
+
updated(t) {
|
|
71
|
+
t.has("disabled") && (this.disabled ? this.setAttribute("aria-disabled", "true") : this.removeAttribute("aria-disabled"));
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Public API
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
/**
|
|
77
|
+
* Focuses the host element. The host carries `role="tab"` and `tabindex`,
|
|
78
|
+
* so it is the correct element for AT to land on.
|
|
79
|
+
*/
|
|
80
|
+
focus(t) {
|
|
81
|
+
super.focus(t);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Focuses the host then dispatches `nys-tab-select`. Called from both the
|
|
85
|
+
* click handler and the keydown handler.
|
|
86
|
+
*/
|
|
87
|
+
_handleClick() {
|
|
88
|
+
this.disabled || (this.focus(), this.dispatchEvent(
|
|
89
|
+
new CustomEvent("nys-tab-select", {
|
|
90
|
+
bubbles: !0,
|
|
91
|
+
composed: !0,
|
|
92
|
+
detail: { id: this.id, label: this.label }
|
|
93
|
+
})
|
|
94
|
+
));
|
|
95
|
+
}
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Render
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
render() {
|
|
100
|
+
return _`<span class="nys-tab">${this.label}</span>`;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
g.styles = v(f);
|
|
104
|
+
let d = g;
|
|
105
|
+
y([
|
|
106
|
+
c({ type: String, reflect: !0 })
|
|
107
|
+
], d.prototype, "id");
|
|
108
|
+
y([
|
|
109
|
+
c({ type: String })
|
|
110
|
+
], d.prototype, "label");
|
|
111
|
+
y([
|
|
112
|
+
c({ type: Boolean, reflect: !0 })
|
|
113
|
+
], d.prototype, "selected");
|
|
114
|
+
y([
|
|
115
|
+
c({ type: Boolean, reflect: !0 })
|
|
116
|
+
], d.prototype, "disabled");
|
|
117
|
+
customElements.get("nys-tab") || customElements.define("nys-tab", d);
|
|
118
|
+
var A = Object.defineProperty, x = (i, t, r, a) => {
|
|
119
|
+
for (var e = void 0, s = i.length - 1, o; s >= 0; s--)
|
|
120
|
+
(o = i[s]) && (e = o(t, r, e) || e);
|
|
121
|
+
return e && A(t, r, e), e;
|
|
122
|
+
};
|
|
123
|
+
let L = 0;
|
|
124
|
+
const m = class m extends p {
|
|
125
|
+
constructor() {
|
|
126
|
+
super(...arguments), this.id = "", this.name = "", this._updateScrollShadows = () => {
|
|
127
|
+
const { scrollLeft: t, scrollWidth: r, clientWidth: a } = this._tabsEl, e = t > 0, s = t + a < r;
|
|
128
|
+
this._shadowLeft.classList.toggle("is-visible", e), this._shadowRight.classList.toggle("is-visible", s);
|
|
129
|
+
}, this._handleWheel = (t) => {
|
|
130
|
+
t.deltaY !== 0 && (t.preventDefault(), this._tabsEl.scrollLeft += t.deltaY);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Lifecycle
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
/**
|
|
137
|
+
* Called when the element is inserted into the document.
|
|
138
|
+
* Auto-generates a unique `id` if one was not provided.
|
|
139
|
+
*/
|
|
140
|
+
connectedCallback() {
|
|
141
|
+
super.connectedCallback(), this.id || (this.id = `nys-tabgroup-${Date.now()}-${L++}`);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Called after the element's shadow DOM has been rendered for the first time.
|
|
145
|
+
*
|
|
146
|
+
* Caches references to the tab list and scroll-shadow elements, performs an
|
|
147
|
+
* initial scroll-shadow evaluation, and attaches:
|
|
148
|
+
* - A `scroll` event listener on `_tabsEl` to update shadows on scroll.
|
|
149
|
+
* - A `ResizeObserver` on `_tabsEl` to update shadows when the container
|
|
150
|
+
* is resized.
|
|
151
|
+
*/
|
|
152
|
+
firstUpdated() {
|
|
153
|
+
const t = this.shadowRoot;
|
|
154
|
+
this._tabsEl = t.querySelector(".nys-tabgroup__tabs"), this._shadowLeft = t.querySelector(".scroll-shadow--left"), this._shadowRight = t.querySelector(".scroll-shadow--right"), this._updateScrollShadows(), this._tabsEl.addEventListener("scroll", this._updateScrollShadows), this._tabsEl.addEventListener("wheel", this._handleWheel, {
|
|
155
|
+
passive: !1
|
|
156
|
+
}), this._resizeObserver = new ResizeObserver(
|
|
157
|
+
() => this._updateScrollShadows()
|
|
158
|
+
), this._resizeObserver.observe(this._tabsEl);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Returns all `<nys-tab>` elements currently residing in the shadow-DOM
|
|
162
|
+
* tabs container, in DOM order.
|
|
163
|
+
*
|
|
164
|
+
* @returns An array of `HTMLElement` references to every `<nys-tab>` child.
|
|
165
|
+
*/
|
|
166
|
+
_getTabs() {
|
|
167
|
+
return Array.from(
|
|
168
|
+
this.shadowRoot?.querySelector(".nys-tabgroup__tabs")?.querySelectorAll("nys-tab") ?? []
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Returns all `<nys-tabpanel>` elements currently residing in the
|
|
173
|
+
* shadow-DOM panels container, in DOM order.
|
|
174
|
+
*
|
|
175
|
+
* @returns An array of `HTMLElement` references to every `<nys-tabpanel>` child.
|
|
176
|
+
*/
|
|
177
|
+
_getPanels() {
|
|
178
|
+
return Array.from(
|
|
179
|
+
this.shadowRoot?.querySelector(".nys-tabgroup__panels")?.querySelectorAll("nys-tabpanel") ?? []
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Single source of truth for ARIA wiring, `tabindex`, and panel visibility.
|
|
184
|
+
*
|
|
185
|
+
* For each index `i`:
|
|
186
|
+
* - Sets `selected` / removes `selected` attribute on `tabs[i]`.
|
|
187
|
+
* - Sets `aria-controls` on `tabs[i]` to the `id` of `panels[i]`.
|
|
188
|
+
* - Sets `aria-labelledby` on `panels[i]` to the `id` of `tabs[i]`.
|
|
189
|
+
* - Removes `hidden` from `panels[selectedIndex]`; adds it to all others.
|
|
190
|
+
*
|
|
191
|
+
* Must be called any time the selected tab changes (initial render and
|
|
192
|
+
* subsequent user interactions).
|
|
193
|
+
*
|
|
194
|
+
* @param tabs - Ordered array of `<nys-tab>` elements to update.
|
|
195
|
+
* @param panels - Ordered array of `<nys-tabpanel>` elements to update.
|
|
196
|
+
* Must be the same length as `tabs` for correct pairing.
|
|
197
|
+
* @param selectedIndex - Zero-based index of the tab/panel pair to activate.
|
|
198
|
+
* @returns void
|
|
199
|
+
*/
|
|
200
|
+
_applySelection(t, r, a) {
|
|
201
|
+
t.forEach((e, s) => {
|
|
202
|
+
const o = s === a, n = r[s];
|
|
203
|
+
e.setAttribute("aria-selected", o ? "true" : "false"), e.setAttribute("tabindex", o ? "0" : "-1"), n?.id && e.setAttribute("aria-controls", n.id), o ? e.setAttribute("selected", "") : e.removeAttribute("selected");
|
|
204
|
+
}), r.forEach((e, s) => {
|
|
205
|
+
const o = s === a, n = t[s];
|
|
206
|
+
n && e.setAttribute("aria-labelledby", n.id), o ? e.removeAttribute("hidden") : e.setAttribute("hidden", "");
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Event Handlers
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
/**
|
|
213
|
+
* Handles `slotchange` on the default slot.
|
|
214
|
+
*
|
|
215
|
+
* Iterates over all assigned elements and moves each `<nys-tab>` into
|
|
216
|
+
* `.nys-tabgroup__tabs` and each `<nys-tabpanel>` into
|
|
217
|
+
* `.nys-tabgroup__panels`, preserving relative order. After sorting,
|
|
218
|
+
* calls `_applySelection` using the first element that already has a
|
|
219
|
+
* `selected` attribute, or index `0` if none is found.
|
|
220
|
+
*
|
|
221
|
+
* @param e - The `Event` fired by the `<slot>` element on slot change.
|
|
222
|
+
* @returns void
|
|
223
|
+
*/
|
|
224
|
+
_sortChildren(t) {
|
|
225
|
+
const a = t.target.assignedElements(), e = this.shadowRoot?.querySelector(".nys-tabgroup__tabs"), s = this.shadowRoot?.querySelector(
|
|
226
|
+
".nys-tabgroup__panels"
|
|
227
|
+
);
|
|
228
|
+
if (!e || !s) return;
|
|
229
|
+
const o = [], n = [];
|
|
230
|
+
a.forEach((l) => {
|
|
231
|
+
const k = l.tagName.toLowerCase();
|
|
232
|
+
k === "nys-tab" ? (e.appendChild(l), o.push(l)) : k === "nys-tabpanel" && (s.appendChild(l), n.push(l));
|
|
233
|
+
});
|
|
234
|
+
const b = o.findIndex(
|
|
235
|
+
(l) => l.hasAttribute("selected")
|
|
236
|
+
), E = b !== -1 ? b : 0;
|
|
237
|
+
this._applySelection(o, n, E);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Handles the `nys-tab-select` custom event bubbled up from a child
|
|
241
|
+
* `<nys-tab>`.
|
|
242
|
+
*
|
|
243
|
+
* Resolves the originating `<nys-tab>` via `composedPath()` (required
|
|
244
|
+
* because the event crosses shadow DOM boundaries), determines its index
|
|
245
|
+
* among the current tab list, and delegates to `_applySelection`.
|
|
246
|
+
*
|
|
247
|
+
* @param e - The `Event` (cast to `CustomEvent`) dispatched by `<nys-tab>`.
|
|
248
|
+
* @returns void
|
|
249
|
+
*/
|
|
250
|
+
_handleTabSelect(t) {
|
|
251
|
+
const r = t.composedPath().find(
|
|
252
|
+
(o) => o.tagName?.toLowerCase() === "nys-tab"
|
|
253
|
+
);
|
|
254
|
+
if (!r) return;
|
|
255
|
+
const a = this._getTabs(), e = this._getPanels(), s = a.indexOf(r);
|
|
256
|
+
s !== -1 && this._applySelection(a, e, s);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Implements the ARIA radio-button keyboard pattern:
|
|
260
|
+
* - `ArrowRight` — moves focus to the next enabled tab (wraps).
|
|
261
|
+
* - `ArrowLeft` — moves focus to the previous enabled tab (wraps).
|
|
262
|
+
*
|
|
263
|
+
* Focus is moved without changing selection; Enter / Space on the newly
|
|
264
|
+
* focused tab (handled by `<nys-tab>._handleKeydown`) confirm selection.
|
|
265
|
+
*
|
|
266
|
+
* The currently focused tab is resolved via `composedPath()` because focus
|
|
267
|
+
* may sit on a shadow-DOM descendant of `<nys-tab>` rather than the host
|
|
268
|
+
* itself.
|
|
269
|
+
*
|
|
270
|
+
* Disabled tabs are excluded from navigation and will never receive focus
|
|
271
|
+
* via arrow keys.
|
|
272
|
+
*
|
|
273
|
+
* @param e - The `KeyboardEvent` from the tablist `keydown` listener.
|
|
274
|
+
* @returns void
|
|
275
|
+
*/
|
|
276
|
+
_handleKeydown(t) {
|
|
277
|
+
const r = this._getTabs().filter((b) => !b.hasAttribute("disabled"));
|
|
278
|
+
if (r.length === 0) return;
|
|
279
|
+
const a = t.composedPath().find(
|
|
280
|
+
(b) => b.tagName?.toLowerCase() === "nys-tab"
|
|
281
|
+
), e = a ? r.indexOf(a) : -1;
|
|
282
|
+
if (e === -1) return;
|
|
283
|
+
const s = "ArrowLeft", o = "ArrowRight";
|
|
284
|
+
let n = e;
|
|
285
|
+
switch (t.key) {
|
|
286
|
+
case s:
|
|
287
|
+
n = (e - 1 + r.length) % r.length;
|
|
288
|
+
break;
|
|
289
|
+
case o:
|
|
290
|
+
n = (e + 1) % r.length;
|
|
291
|
+
break;
|
|
292
|
+
default:
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
n !== e && r[n].focus?.();
|
|
296
|
+
}
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// Render
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
render() {
|
|
301
|
+
return _`
|
|
302
|
+
<div class="nys-tabgroup" @nys-tab-select=${this._handleTabSelect}>
|
|
303
|
+
<div class="nys-tabgroup__tabs-container">
|
|
304
|
+
<div class="nys-tabgroup__tabs-background"></div>
|
|
305
|
+
<div class="scroll-shadow scroll-shadow--left"></div>
|
|
306
|
+
<div
|
|
307
|
+
class="nys-tabgroup__tabs"
|
|
308
|
+
role="tablist"
|
|
309
|
+
aria-label=${this.name}
|
|
310
|
+
@keydown=${this._handleKeydown}
|
|
311
|
+
></div>
|
|
312
|
+
<div class="scroll-shadow scroll-shadow--right"></div>
|
|
313
|
+
</div>
|
|
314
|
+
<div class="nys-tabgroup__panels"></div>
|
|
315
|
+
<slot @slotchange=${this._sortChildren}></slot>
|
|
316
|
+
</div>
|
|
317
|
+
`;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
m.styles = v(f);
|
|
321
|
+
let h = m;
|
|
322
|
+
x([
|
|
323
|
+
c({ type: String, reflect: !0 })
|
|
324
|
+
], h.prototype, "id");
|
|
325
|
+
x([
|
|
326
|
+
c({ type: String })
|
|
327
|
+
], h.prototype, "name");
|
|
328
|
+
customElements.get("nys-tabgroup") || customElements.define("nys-tabgroup", h);
|
|
329
|
+
var $ = Object.defineProperty, P = (i, t, r, a) => {
|
|
330
|
+
for (var e = void 0, s = i.length - 1, o; s >= 0; s--)
|
|
331
|
+
(o = i[s]) && (e = o(t, r, e) || e);
|
|
332
|
+
return e && $(t, r, e), e;
|
|
333
|
+
};
|
|
334
|
+
let R = 0;
|
|
335
|
+
const w = class w extends p {
|
|
336
|
+
constructor() {
|
|
337
|
+
super(...arguments), this.id = "";
|
|
338
|
+
}
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// Lifecycle
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
connectedCallback() {
|
|
343
|
+
super.connectedCallback(), this.id || (this.id = `nys-tabpanel-${Date.now()}-${R++}`), this.setAttribute("role", "tabpanel");
|
|
344
|
+
}
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// Render
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
render() {
|
|
349
|
+
return _`
|
|
350
|
+
<div class="nys-tabpanel" tabindex="0">
|
|
351
|
+
<slot></slot>
|
|
352
|
+
</div>
|
|
353
|
+
`;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
w.styles = v(f);
|
|
357
|
+
let u = w;
|
|
358
|
+
P([
|
|
359
|
+
c({ type: String, reflect: !0 })
|
|
360
|
+
], u.prototype, "id");
|
|
361
|
+
customElements.get("nys-tabpanel") || customElements.define("nys-tabpanel", u);
|
|
362
|
+
export {
|
|
363
|
+
d as NysTab,
|
|
364
|
+
h as NysTabgroup,
|
|
365
|
+
u as NysTabpanel
|
|
366
|
+
};
|
|
367
|
+
//# sourceMappingURL=nys-tab.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nys-tab.js","sources":["../src/nys-tab.ts","../src/nys-tabgroup.ts","../src/nys-tabpanel.ts"],"sourcesContent":["import { LitElement, html, unsafeCSS } from \"lit\";\nimport { property } from \"lit/decorators.js\";\n// @ts-ignore: SCSS module imported via bundler as inline\nimport styles from \"./nys-tab.scss?inline\";\n\n/** @internal Monotonically increasing counter used to generate unique element IDs. */\nlet componentIdCounter = 0;\n\n/**\n * `<nys-tab>` is a single tab within a `<nys-tabgroup>`.\n *\n * The host element carries `role=\"tab\"`, `tabindex`, `aria-selected`,\n * `aria-controls`, and `aria-disabled` so assistive technologies see the\n * correct ARIA tab semantics on the element that is actually focused.\n * `<nys-tabgroup>` manages `tabindex`, `aria-selected`, and `aria-controls`\n * via `_applySelection`; do not set them directly on this element.\n *\n * @element nys-tab\n *\n * @fires nys-tab-select - Dispatched when the tab is activated via click or\n * Enter / Space. Bubbles and crosses shadow DOM boundaries.\n * `detail: { id: string, label: string }`\n * @fires nys-tab-focus - Dispatched when the host receives focus. Bubbles and\n * crosses shadow DOM boundaries. `detail: { id: string }`\n * @fires nys-tab-blur - Dispatched when the host loses focus. Bubbles and\n * crosses shadow DOM boundaries. `detail: { id: string }`\n *\n * @slot - No slots; content is derived from the `label` property.\n *\n * @example `<nys-tab>` and `<nys-tabpanel>` should always be wrapped by `<nys-tabgroup>`\n * ```html\n * <!-- Always place <nys-tab> elements inside a <nys-tabgroup>. -->\n * <nys-tabgroup name=\"My Tabs\">\n * <nys-tab label=\"Overview\"></nys-tab>\n * <nys-tab label=\"Details\" selected></nys-tab>\n * <nys-tab label=\"Archived\" disabled></nys-tab>\n * <nys-tabpanel><p>Overview content</p></nys-tabpanel>\n * <nys-tabpanel><p>Details content (shown by default)</p></nys-tabpanel>\n * <nys-tabpanel><p>Archived content</p></nys-tabpanel>\n * </nys-tabgroup>\n * ```\n */\nexport class NysTab extends LitElement {\n static styles = unsafeCSS(styles);\n\n /**\n * Unique identifier for the tab element.\n * Reflected to the DOM attribute so `aria-controls` references resolve.\n *\n * @attr id\n */\n @property({ type: String, reflect: true }) id = \"\";\n\n /**\n * Visible text label rendered inside the inner `<span>`.\n *\n * @attr label\n */\n @property({ type: String }) label = \"\";\n\n /**\n * Whether this tab is the currently active tab.\n * Managed by `<nys-tabgroup>`; reflected for CSS attribute selectors.\n *\n * @attr selected\n */\n @property({ type: Boolean, reflect: true }) selected = false;\n\n /**\n * Whether this tab is disabled.\n * Reflected to the DOM attribute for CSS styling.\n *\n * @attr disabled\n */\n @property({ type: Boolean, reflect: true }) disabled = false;\n\n // ---------------------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------------------\n\n /**\n * Sets `role=\"tab\"` and `tabindex=\"-1\"` on the host (the element that AT\n * will read and that receives keyboard focus). Attaches host-level listeners\n * for keydown, focus, blur, and click so that interaction events work\n * correctly on the host element itself.\n *\n * Click is handled at the host level so iOS VoiceOver double-tap (which\n * dispatches `click` directly on the host because of `role=\"tab\"`, bypassing\n * shadow-DOM children) activates the tab. Normal taps land on the inner\n * `<span>` and bubble up to this listener.\n *\n * `<nys-tabgroup>` overrides `tabindex` to `\"0\"` on the selected tab.\n */\n connectedCallback() {\n super.connectedCallback();\n if (!this.id) {\n this.id = `nys-tab-${Date.now()}-${componentIdCounter++}`;\n }\n this.setAttribute(\"role\", \"tab\");\n this.setAttribute(\"tabindex\", \"-1\");\n this.addEventListener(\"keydown\", this._onKeydown);\n this.addEventListener(\"focus\", this._onFocus);\n this.addEventListener(\"blur\", this._onBlur);\n this.addEventListener(\"click\", this._onClick);\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.removeEventListener(\"keydown\", this._onKeydown);\n this.removeEventListener(\"focus\", this._onFocus);\n this.removeEventListener(\"blur\", this._onBlur);\n this.removeEventListener(\"click\", this._onClick);\n }\n\n /**\n * Keeps `aria-disabled` on the host in sync with the `disabled` property so\n * AT perceives the disabled state on the element it actually focuses.\n */\n updated(changed: Map<string, unknown>) {\n if (changed.has(\"disabled\")) {\n if (this.disabled) {\n this.setAttribute(\"aria-disabled\", \"true\");\n } else {\n this.removeAttribute(\"aria-disabled\");\n }\n }\n }\n\n // ---------------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------------\n\n /**\n * Focuses the host element. The host carries `role=\"tab\"` and `tabindex`,\n * so it is the correct element for AT to land on.\n */\n public focus(options?: FocusOptions): void {\n super.focus(options);\n }\n\n // ---------------------------------------------------------------------------\n // Event Handlers\n // ---------------------------------------------------------------------------\n\n /**\n * Enter / Space on the focused host activate the tab.\n * Arrow-key navigation is handled one level up by `<nys-tabgroup>`.\n */\n private _onKeydown = (e: KeyboardEvent): void => {\n if (this.disabled) return;\n if (e.key !== \"Enter\" && e.key !== \" \") return;\n e.preventDefault();\n this._handleClick();\n };\n\n /**\n * Host focus → dispatch `nys-tab-focus` for external observers.\n */\n private _onFocus = (): void => {\n this.dispatchEvent(\n new CustomEvent(\"nys-tab-focus\", {\n bubbles: true,\n composed: true,\n detail: { id: this.id },\n }),\n );\n };\n\n /**\n * Host blur → dispatch `nys-tab-blur` for external observers.\n */\n private _onBlur = (): void => {\n this.dispatchEvent(\n new CustomEvent(\"nys-tab-blur\", {\n bubbles: true,\n composed: true,\n detail: { id: this.id },\n }),\n );\n };\n\n /**\n * Host-level click handler. Activates the tab regardless of whether the\n * click landed on the inner element (normal pointer/keyboard tap, which\n * bubbles up) or directly on the host (iOS VoiceOver double-tap dispatches\n * `click` on the element with `role=\"tab\"`, bypassing shadow-DOM children).\n */\n private _onClick = (): void => {\n this._handleClick();\n };\n\n /**\n * Focuses the host then dispatches `nys-tab-select`. Called from both the\n * click handler and the keydown handler.\n */\n private _handleClick(): void {\n if (this.disabled) return;\n this.focus();\n this.dispatchEvent(\n new CustomEvent(\"nys-tab-select\", {\n bubbles: true,\n composed: true,\n detail: { id: this.id, label: this.label },\n }),\n );\n }\n\n // ---------------------------------------------------------------------------\n // Render\n // ---------------------------------------------------------------------------\n\n render() {\n // Inner element is a non-interactive `<span>` (not `<button>`) so that\n // axe-core's `nested-interactive` rule does not fire — the host already\n // carries `role=\"tab\"` + `tabindex` and is the interactive control.\n // Disabled gating, click activation, and keyboard activation all happen\n // on the host; the span is purely a styling/labeling target.\n return html`<span class=\"nys-tab\">${this.label}</span>`;\n }\n}\n\nif (!customElements.get(\"nys-tab\")) {\n customElements.define(\"nys-tab\", NysTab);\n}\n","import { LitElement, html, unsafeCSS } from \"lit\";\nimport { property } from \"lit/decorators.js\";\n// @ts-ignore: SCSS module imported via bundler as inline\nimport styles from \"./nys-tab.scss?inline\";\n\n/** @internal Monotonically increasing counter used to generate unique element IDs. */\nlet componentIdCounter = 0;\n\n/**\n * `<nys-tabgroup>` is the container for `<nys-tab>` and `<nys-tabpanel>`\n * elements.\n *\n * Accepts tabs and panels as flat light-DOM children in any order (interleaved\n * or grouped). On slot change, children are sorted into dedicated shadow-DOM\n * containers, ARIA relationships are wired, and the first selected (or first)\n * tab is activated.\n *\n * Scroll shadows are rendered on either side of the tab list and toggled via\n * `ResizeObserver` and a `scroll` listener so they accurately reflect whether\n * overflow content exists in each direction.\n *\n * Keyboard navigation follows the\n * {@link https://www.w3.org/WAI/ARIA/apg/patterns/tabs/ ARIA Tabs Pattern}:\n * - Arrow keys move focus without changing selection.\n * - Enter / Space confirm selection on the focused tab.\n *\n * @element nys-tabgroup\n *\n * @slot - Accepts `<nys-tab>` and `<nys-tabpanel>` children. Elements are\n * moved into internal shadow-DOM containers on `slotchange`; the slot\n * itself is not rendered visibly.\n *\n * @example Disable a tab using the `disabled` attribute on `<nys-tab>`.\n * ```html\n * <nys-tabgroup name=\"Account Settings\">\n * <nys-tab label=\"Profile\"></nys-tab>\n * <nys-tab label=\"Security\"></nys-tab>\n * <nys-tab label=\"Notifications\" disabled></nys-tab>\n * <nys-tabpanel><p>Manage your profile information.</p></nys-tabpanel>\n * <nys-tabpanel><p>Update your password and 2FA settings.</p></nys-tabpanel>\n * <nys-tabpanel><p>Notification preferences (coming soon).</p></nys-tabpanel>\n * </nys-tabgroup>\n * ```\n *\n * @example Pre-select a tab using the `selected` attribute on `<nys-tab>`.\n * ```html\n * <nys-tabgroup name=\"Reports\">\n * <nys-tab label=\"Summary\"></nys-tab>\n * <nys-tab label=\"Details\" selected></nys-tab>\n * <nys-tabpanel><p>Summary view</p></nys-tabpanel>\n * <nys-tabpanel><p>Detailed view (shown by default)</p></nys-tabpanel>\n * </nys-tabgroup>\n * ```\n */\nexport class NysTabgroup extends LitElement {\n static styles = unsafeCSS(styles);\n\n /**\n * Unique identifier for the tabgroup element.\n * If not provided, one is auto-generated in `connectedCallback`.\n * Reflected to the DOM attribute.\n *\n * @attr id\n */\n @property({ type: String, reflect: true }) id = \"\";\n\n /**\n * The name of the tab group.\n * Used for form submission and accessibility purposes.\n *\n * @attr name\n */\n @property({ type: String }) name = \"\";\n\n /**\n * Cached in `firstUpdated` and used by `_updateScrollShadows` to read\n * scroll position and dimensions.\n */\n private _tabsEl!: HTMLElement;\n\n /**\n * Reference to the left scroll-shadow overlay element.\n * Receives the `is-visible` class when the tab list is scrolled away from\n * its leftmost position.\n */\n private _shadowLeft!: HTMLElement;\n\n /**\n * Reference to the right scroll-shadow overlay element.\n * Receives the `is-visible` class when overflow content exists to the right\n * of the current scroll position.\n */\n private _shadowRight!: HTMLElement;\n\n /**\n * `ResizeObserver` instance watching `_tabsEl` for size changes.\n * Re-evaluates scroll shadow visibility whenever the tab list is resized\n * (e.g. viewport resize, dynamic tab additions).\n * Stored so it can be disconnected if needed.\n */\n private _resizeObserver?: ResizeObserver;\n\n // ---------------------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------------------\n\n /**\n * Called when the element is inserted into the document.\n * Auto-generates a unique `id` if one was not provided.\n */\n connectedCallback() {\n super.connectedCallback();\n if (!this.id) {\n this.id = `nys-tabgroup-${Date.now()}-${componentIdCounter++}`;\n }\n }\n\n /**\n * Called after the element's shadow DOM has been rendered for the first time.\n *\n * Caches references to the tab list and scroll-shadow elements, performs an\n * initial scroll-shadow evaluation, and attaches:\n * - A `scroll` event listener on `_tabsEl` to update shadows on scroll.\n * - A `ResizeObserver` on `_tabsEl` to update shadows when the container\n * is resized.\n */\n firstUpdated() {\n const root = this.shadowRoot!;\n this._tabsEl = root.querySelector(\".nys-tabgroup__tabs\")!;\n this._shadowLeft = root.querySelector(\".scroll-shadow--left\")!;\n this._shadowRight = root.querySelector(\".scroll-shadow--right\")!;\n\n this._updateScrollShadows();\n\n this._tabsEl.addEventListener(\"scroll\", this._updateScrollShadows);\n this._tabsEl.addEventListener(\"wheel\", this._handleWheel, {\n passive: false,\n });\n\n this._resizeObserver = new ResizeObserver(() =>\n this._updateScrollShadows(),\n );\n this._resizeObserver.observe(this._tabsEl);\n }\n\n // ---------------------------------------------------------------------------\n // Helpers\n // ---------------------------------------------------------------------------\n\n /**\n * Reads the current scroll state of `_tabsEl` and toggles the `is-visible`\n * class on the left and right shadow overlays accordingly.\n *\n * - Left shadow is visible when `scrollLeft > 0`.\n * - Right shadow is visible when `scrollLeft + clientWidth < scrollWidth`\n * (i.e. content exists beyond the right edge).\n *\n * Defined as an arrow function so it can be passed directly as an event\n * listener without losing `this` context.\n *\n * @returns void\n */\n private _updateScrollShadows = (): void => {\n const { scrollLeft, scrollWidth, clientWidth } = this._tabsEl;\n\n const canScrollLeft = scrollLeft > 0;\n const canScrollRight = scrollLeft + clientWidth < scrollWidth;\n\n this._shadowLeft.classList.toggle(\"is-visible\", canScrollLeft);\n this._shadowRight.classList.toggle(\"is-visible\", canScrollRight);\n };\n\n /**\n * Returns all `<nys-tab>` elements currently residing in the shadow-DOM\n * tabs container, in DOM order.\n *\n * @returns An array of `HTMLElement` references to every `<nys-tab>` child.\n */\n private _getTabs(): HTMLElement[] {\n return Array.from(\n this.shadowRoot\n ?.querySelector(\".nys-tabgroup__tabs\")\n ?.querySelectorAll(\"nys-tab\") ?? [],\n ) as HTMLElement[];\n }\n\n /**\n * Returns all `<nys-tabpanel>` elements currently residing in the\n * shadow-DOM panels container, in DOM order.\n *\n * @returns An array of `HTMLElement` references to every `<nys-tabpanel>` child.\n */\n private _getPanels(): HTMLElement[] {\n return Array.from(\n this.shadowRoot\n ?.querySelector(\".nys-tabgroup__panels\")\n ?.querySelectorAll(\"nys-tabpanel\") ?? [],\n ) as HTMLElement[];\n }\n\n /**\n * Single source of truth for ARIA wiring, `tabindex`, and panel visibility.\n *\n * For each index `i`:\n * - Sets `selected` / removes `selected` attribute on `tabs[i]`.\n * - Sets `aria-controls` on `tabs[i]` to the `id` of `panels[i]`.\n * - Sets `aria-labelledby` on `panels[i]` to the `id` of `tabs[i]`.\n * - Removes `hidden` from `panels[selectedIndex]`; adds it to all others.\n *\n * Must be called any time the selected tab changes (initial render and\n * subsequent user interactions).\n *\n * @param tabs - Ordered array of `<nys-tab>` elements to update.\n * @param panels - Ordered array of `<nys-tabpanel>` elements to update.\n * Must be the same length as `tabs` for correct pairing.\n * @param selectedIndex - Zero-based index of the tab/panel pair to activate.\n * @returns void\n */\n private _applySelection(\n tabs: HTMLElement[],\n panels: HTMLElement[],\n selectedIndex: number,\n ): void {\n tabs.forEach((tab, i) => {\n const isSelected = i === selectedIndex;\n const panel = panels[i];\n tab.setAttribute(\"aria-selected\", isSelected ? \"true\" : \"false\");\n tab.setAttribute(\"tabindex\", isSelected ? \"0\" : \"-1\");\n if (panel?.id) {\n tab.setAttribute(\"aria-controls\", panel.id);\n }\n if (isSelected) {\n tab.setAttribute(\"selected\", \"\");\n } else {\n tab.removeAttribute(\"selected\");\n }\n });\n\n panels.forEach((panel, i) => {\n const isSelected = i === selectedIndex;\n const tab = tabs[i];\n if (tab) {\n panel.setAttribute(\"aria-labelledby\", tab.id);\n }\n if (isSelected) {\n panel.removeAttribute(\"hidden\");\n } else {\n panel.setAttribute(\"hidden\", \"\");\n }\n });\n }\n\n // ---------------------------------------------------------------------------\n // Event Handlers\n // ---------------------------------------------------------------------------\n\n /**\n * Handles `slotchange` on the default slot.\n *\n * Iterates over all assigned elements and moves each `<nys-tab>` into\n * `.nys-tabgroup__tabs` and each `<nys-tabpanel>` into\n * `.nys-tabgroup__panels`, preserving relative order. After sorting,\n * calls `_applySelection` using the first element that already has a\n * `selected` attribute, or index `0` if none is found.\n *\n * @param e - The `Event` fired by the `<slot>` element on slot change.\n * @returns void\n */\n private _sortChildren(e: Event): void {\n const slot = e.target as HTMLSlotElement;\n const assigned = slot.assignedElements();\n\n const tabsContainer = this.shadowRoot?.querySelector(\".nys-tabgroup__tabs\");\n const panelsContainer = this.shadowRoot?.querySelector(\n \".nys-tabgroup__panels\",\n );\n if (!tabsContainer || !panelsContainer) return;\n\n const tabs: HTMLElement[] = [];\n const panels: HTMLElement[] = [];\n\n assigned.forEach((child) => {\n const tag = child.tagName.toLowerCase();\n if (tag === \"nys-tab\") {\n tabsContainer.appendChild(child);\n tabs.push(child as HTMLElement);\n } else if (tag === \"nys-tabpanel\") {\n panelsContainer.appendChild(child);\n panels.push(child as HTMLElement);\n }\n });\n\n // Honor the first selected tab; ignore any others\n const declaredSelectedIndex = tabs.findIndex((t) =>\n t.hasAttribute(\"selected\"),\n );\n const selectedIndex =\n declaredSelectedIndex !== -1 ? declaredSelectedIndex : 0;\n\n this._applySelection(tabs, panels, selectedIndex);\n }\n\n /**\n * Handles the `nys-tab-select` custom event bubbled up from a child\n * `<nys-tab>`.\n *\n * Resolves the originating `<nys-tab>` via `composedPath()` (required\n * because the event crosses shadow DOM boundaries), determines its index\n * among the current tab list, and delegates to `_applySelection`.\n *\n * @param e - The `Event` (cast to `CustomEvent`) dispatched by `<nys-tab>`.\n * @returns void\n */\n private _handleTabSelect(e: Event): void {\n const selectedTab = (e as CustomEvent)\n .composedPath()\n .find(\n (el) => (el as HTMLElement).tagName?.toLowerCase() === \"nys-tab\",\n ) as HTMLElement | undefined;\n if (!selectedTab) return;\n\n const tabs = this._getTabs();\n const panels = this._getPanels();\n const selectedIndex = tabs.indexOf(selectedTab);\n if (selectedIndex === -1) return;\n\n this._applySelection(tabs, panels, selectedIndex);\n }\n\n /**\n * Implements the ARIA radio-button keyboard pattern:\n * - `ArrowRight` — moves focus to the next enabled tab (wraps).\n * - `ArrowLeft` — moves focus to the previous enabled tab (wraps).\n *\n * Focus is moved without changing selection; Enter / Space on the newly\n * focused tab (handled by `<nys-tab>._handleKeydown`) confirm selection.\n *\n * The currently focused tab is resolved via `composedPath()` because focus\n * may sit on a shadow-DOM descendant of `<nys-tab>` rather than the host\n * itself.\n *\n * Disabled tabs are excluded from navigation and will never receive focus\n * via arrow keys.\n *\n * @param e - The `KeyboardEvent` from the tablist `keydown` listener.\n * @returns void\n */\n private _handleKeydown(e: KeyboardEvent): void {\n const tabs = this._getTabs().filter((t) => !t.hasAttribute(\"disabled\"));\n if (tabs.length === 0) return;\n\n const focusedTab = (e.composedPath() as HTMLElement[]).find(\n (el) => el.tagName?.toLowerCase() === \"nys-tab\",\n );\n const currentIndex = focusedTab ? tabs.indexOf(focusedTab) : -1;\n\n if (currentIndex === -1) return;\n\n const prevKey = \"ArrowLeft\";\n const nextKey = \"ArrowRight\";\n\n let newIndex = currentIndex;\n\n switch (e.key) {\n case prevKey:\n newIndex = (currentIndex - 1 + tabs.length) % tabs.length;\n break;\n case nextKey:\n newIndex = (currentIndex + 1) % tabs.length;\n break;\n default:\n return;\n }\n\n if (newIndex === currentIndex) return;\n\n // Move focus only — do not change selection\n (tabs[newIndex] as HTMLElement & { focus?: () => void }).focus?.();\n }\n\n /*\n * handles the horizontal scroll of the tab list when the user scrolls on the mouse wheel.\n */\n private _handleWheel = (e: WheelEvent): void => {\n if (e.deltaY === 0) return;\n e.preventDefault();\n this._tabsEl.scrollLeft += e.deltaY;\n };\n\n // ---------------------------------------------------------------------------\n // Render\n // ---------------------------------------------------------------------------\n\n render() {\n return html`\n <div class=\"nys-tabgroup\" @nys-tab-select=${this._handleTabSelect}>\n <div class=\"nys-tabgroup__tabs-container\">\n <div class=\"nys-tabgroup__tabs-background\"></div>\n <div class=\"scroll-shadow scroll-shadow--left\"></div>\n <div\n class=\"nys-tabgroup__tabs\"\n role=\"tablist\"\n aria-label=${this.name}\n @keydown=${this._handleKeydown}\n ></div>\n <div class=\"scroll-shadow scroll-shadow--right\"></div>\n </div>\n <div class=\"nys-tabgroup__panels\"></div>\n <slot @slotchange=${this._sortChildren}></slot>\n </div>\n `;\n }\n}\n\nif (!customElements.get(\"nys-tabgroup\")) {\n customElements.define(\"nys-tabgroup\", NysTabgroup);\n}\n","import { LitElement, html, unsafeCSS } from \"lit\";\nimport { property } from \"lit/decorators.js\";\n// @ts-ignore: SCSS module imported via bundler as inline\nimport styles from \"./nys-tab.scss?inline\";\n\n/** @internal Monotonically increasing counter used to generate unique element IDs. */\nlet componentIdCounter = 0;\n\n/**\n * `<nys-tabpanel>` is a content panel paired with a `<nys-tab>` inside a\n * `<nys-tabgroup>`.\n *\n * Pairing is determined by render order: the Nth `<nys-tabpanel>` child of a\n * `<nys-tabgroup>` corresponds to the Nth `<nys-tab>` child.\n * `aria-labelledby` and the `hidden` attribute are managed externally by\n * `<nys-tabgroup>` via `_applySelection`; do not set them directly.\n *\n * @element nys-tabpanel\n *\n * @slot - Default slot for panel content. Rendered inside a wrapper `<div>`\n * with the `.nys-tabpanel` class for styling.\n *\n * @example Panel content is wrapped by `<nys-tabpanel>`.\n * ```html\n * <!-- Panels are paired by position with <nys-tab> elements in the same <nys-tabgroup>. -->\n * <nys-tabgroup name=\"Steps\">\n * <nys-tab label=\"Step 1\"></nys-tab>\n * <nys-tab label=\"Step 2\"></nys-tab>\n * <nys-tabpanel>\n * <h2>Step 1: Enter your information</h2>\n * <p>Fill out the form below.</p>\n * </nys-tabpanel>\n * <nys-tabpanel>\n * <h2>Step 2: Review and submit</h2>\n * <p>Confirm your details before submitting.</p>\n * </nys-tabpanel>\n * </nys-tabgroup>\n * ```\n */\nexport class NysTabpanel extends LitElement {\n static styles = unsafeCSS(styles);\n\n /**\n * Unique identifier for the panel element.\n * If not provided, one is auto-generated in `connectedCallback`.\n * Reflected to the DOM attribute so `aria-controls` references on sibling\n * `<nys-tab>` elements resolve correctly.\n *\n * @attr id\n */\n @property({ type: String, reflect: true }) id = \"\";\n\n // ---------------------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------------------\n\n connectedCallback() {\n super.connectedCallback();\n if (!this.id) {\n this.id = `nys-tabpanel-${Date.now()}-${componentIdCounter++}`;\n }\n this.setAttribute(\"role\", \"tabpanel\");\n }\n\n // ---------------------------------------------------------------------------\n // Render\n // ---------------------------------------------------------------------------\n\n render() {\n return html`\n <div class=\"nys-tabpanel\" tabindex=\"0\">\n <slot></slot>\n </div>\n `;\n }\n}\n\nif (!customElements.get(\"nys-tabpanel\")) {\n customElements.define(\"nys-tabpanel\", NysTabpanel);\n}\n"],"names":["componentIdCounter","_NysTab","LitElement","e","changed","options","html","unsafeCSS","styles","NysTab","__decorateClass","property","_NysTabgroup","scrollLeft","scrollWidth","clientWidth","canScrollLeft","canScrollRight","root","tabs","panels","selectedIndex","tab","i","isSelected","panel","assigned","tabsContainer","panelsContainer","child","tag","declaredSelectedIndex","t","selectedTab","el","focusedTab","currentIndex","prevKey","nextKey","newIndex","NysTabgroup","_NysTabpanel","NysTabpanel"],"mappings":"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"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { LitElement } from "lit";
|
|
2
|
+
/**
|
|
3
|
+
* `<nys-tabgroup>` is the container for `<nys-tab>` and `<nys-tabpanel>`
|
|
4
|
+
* elements.
|
|
5
|
+
*
|
|
6
|
+
* Accepts tabs and panels as flat light-DOM children in any order (interleaved
|
|
7
|
+
* or grouped). On slot change, children are sorted into dedicated shadow-DOM
|
|
8
|
+
* containers, ARIA relationships are wired, and the first selected (or first)
|
|
9
|
+
* tab is activated.
|
|
10
|
+
*
|
|
11
|
+
* Scroll shadows are rendered on either side of the tab list and toggled via
|
|
12
|
+
* `ResizeObserver` and a `scroll` listener so they accurately reflect whether
|
|
13
|
+
* overflow content exists in each direction.
|
|
14
|
+
*
|
|
15
|
+
* Keyboard navigation follows the
|
|
16
|
+
* {@link https://www.w3.org/WAI/ARIA/apg/patterns/tabs/ ARIA Tabs Pattern}:
|
|
17
|
+
* - Arrow keys move focus without changing selection.
|
|
18
|
+
* - Enter / Space confirm selection on the focused tab.
|
|
19
|
+
*
|
|
20
|
+
* @element nys-tabgroup
|
|
21
|
+
*
|
|
22
|
+
* @slot - Accepts `<nys-tab>` and `<nys-tabpanel>` children. Elements are
|
|
23
|
+
* moved into internal shadow-DOM containers on `slotchange`; the slot
|
|
24
|
+
* itself is not rendered visibly.
|
|
25
|
+
*
|
|
26
|
+
* @example Disable a tab using the `disabled` attribute on `<nys-tab>`.
|
|
27
|
+
* ```html
|
|
28
|
+
* <nys-tabgroup name="Account Settings">
|
|
29
|
+
* <nys-tab label="Profile"></nys-tab>
|
|
30
|
+
* <nys-tab label="Security"></nys-tab>
|
|
31
|
+
* <nys-tab label="Notifications" disabled></nys-tab>
|
|
32
|
+
* <nys-tabpanel><p>Manage your profile information.</p></nys-tabpanel>
|
|
33
|
+
* <nys-tabpanel><p>Update your password and 2FA settings.</p></nys-tabpanel>
|
|
34
|
+
* <nys-tabpanel><p>Notification preferences (coming soon).</p></nys-tabpanel>
|
|
35
|
+
* </nys-tabgroup>
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @example Pre-select a tab using the `selected` attribute on `<nys-tab>`.
|
|
39
|
+
* ```html
|
|
40
|
+
* <nys-tabgroup name="Reports">
|
|
41
|
+
* <nys-tab label="Summary"></nys-tab>
|
|
42
|
+
* <nys-tab label="Details" selected></nys-tab>
|
|
43
|
+
* <nys-tabpanel><p>Summary view</p></nys-tabpanel>
|
|
44
|
+
* <nys-tabpanel><p>Detailed view (shown by default)</p></nys-tabpanel>
|
|
45
|
+
* </nys-tabgroup>
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare class NysTabgroup extends LitElement {
|
|
49
|
+
static styles: import("lit").CSSResult;
|
|
50
|
+
/**
|
|
51
|
+
* Unique identifier for the tabgroup element.
|
|
52
|
+
* If not provided, one is auto-generated in `connectedCallback`.
|
|
53
|
+
* Reflected to the DOM attribute.
|
|
54
|
+
*
|
|
55
|
+
* @attr id
|
|
56
|
+
*/
|
|
57
|
+
id: string;
|
|
58
|
+
/**
|
|
59
|
+
* The name of the tab group.
|
|
60
|
+
* Used for form submission and accessibility purposes.
|
|
61
|
+
*
|
|
62
|
+
* @attr name
|
|
63
|
+
*/
|
|
64
|
+
name: string;
|
|
65
|
+
/**
|
|
66
|
+
* Cached in `firstUpdated` and used by `_updateScrollShadows` to read
|
|
67
|
+
* scroll position and dimensions.
|
|
68
|
+
*/
|
|
69
|
+
private _tabsEl;
|
|
70
|
+
/**
|
|
71
|
+
* Reference to the left scroll-shadow overlay element.
|
|
72
|
+
* Receives the `is-visible` class when the tab list is scrolled away from
|
|
73
|
+
* its leftmost position.
|
|
74
|
+
*/
|
|
75
|
+
private _shadowLeft;
|
|
76
|
+
/**
|
|
77
|
+
* Reference to the right scroll-shadow overlay element.
|
|
78
|
+
* Receives the `is-visible` class when overflow content exists to the right
|
|
79
|
+
* of the current scroll position.
|
|
80
|
+
*/
|
|
81
|
+
private _shadowRight;
|
|
82
|
+
/**
|
|
83
|
+
* `ResizeObserver` instance watching `_tabsEl` for size changes.
|
|
84
|
+
* Re-evaluates scroll shadow visibility whenever the tab list is resized
|
|
85
|
+
* (e.g. viewport resize, dynamic tab additions).
|
|
86
|
+
* Stored so it can be disconnected if needed.
|
|
87
|
+
*/
|
|
88
|
+
private _resizeObserver?;
|
|
89
|
+
/**
|
|
90
|
+
* Called when the element is inserted into the document.
|
|
91
|
+
* Auto-generates a unique `id` if one was not provided.
|
|
92
|
+
*/
|
|
93
|
+
connectedCallback(): void;
|
|
94
|
+
/**
|
|
95
|
+
* Called after the element's shadow DOM has been rendered for the first time.
|
|
96
|
+
*
|
|
97
|
+
* Caches references to the tab list and scroll-shadow elements, performs an
|
|
98
|
+
* initial scroll-shadow evaluation, and attaches:
|
|
99
|
+
* - A `scroll` event listener on `_tabsEl` to update shadows on scroll.
|
|
100
|
+
* - A `ResizeObserver` on `_tabsEl` to update shadows when the container
|
|
101
|
+
* is resized.
|
|
102
|
+
*/
|
|
103
|
+
firstUpdated(): void;
|
|
104
|
+
/**
|
|
105
|
+
* Reads the current scroll state of `_tabsEl` and toggles the `is-visible`
|
|
106
|
+
* class on the left and right shadow overlays accordingly.
|
|
107
|
+
*
|
|
108
|
+
* - Left shadow is visible when `scrollLeft > 0`.
|
|
109
|
+
* - Right shadow is visible when `scrollLeft + clientWidth < scrollWidth`
|
|
110
|
+
* (i.e. content exists beyond the right edge).
|
|
111
|
+
*
|
|
112
|
+
* Defined as an arrow function so it can be passed directly as an event
|
|
113
|
+
* listener without losing `this` context.
|
|
114
|
+
*
|
|
115
|
+
* @returns void
|
|
116
|
+
*/
|
|
117
|
+
private _updateScrollShadows;
|
|
118
|
+
/**
|
|
119
|
+
* Returns all `<nys-tab>` elements currently residing in the shadow-DOM
|
|
120
|
+
* tabs container, in DOM order.
|
|
121
|
+
*
|
|
122
|
+
* @returns An array of `HTMLElement` references to every `<nys-tab>` child.
|
|
123
|
+
*/
|
|
124
|
+
private _getTabs;
|
|
125
|
+
/**
|
|
126
|
+
* Returns all `<nys-tabpanel>` elements currently residing in the
|
|
127
|
+
* shadow-DOM panels container, in DOM order.
|
|
128
|
+
*
|
|
129
|
+
* @returns An array of `HTMLElement` references to every `<nys-tabpanel>` child.
|
|
130
|
+
*/
|
|
131
|
+
private _getPanels;
|
|
132
|
+
/**
|
|
133
|
+
* Single source of truth for ARIA wiring, `tabindex`, and panel visibility.
|
|
134
|
+
*
|
|
135
|
+
* For each index `i`:
|
|
136
|
+
* - Sets `selected` / removes `selected` attribute on `tabs[i]`.
|
|
137
|
+
* - Sets `aria-controls` on `tabs[i]` to the `id` of `panels[i]`.
|
|
138
|
+
* - Sets `aria-labelledby` on `panels[i]` to the `id` of `tabs[i]`.
|
|
139
|
+
* - Removes `hidden` from `panels[selectedIndex]`; adds it to all others.
|
|
140
|
+
*
|
|
141
|
+
* Must be called any time the selected tab changes (initial render and
|
|
142
|
+
* subsequent user interactions).
|
|
143
|
+
*
|
|
144
|
+
* @param tabs - Ordered array of `<nys-tab>` elements to update.
|
|
145
|
+
* @param panels - Ordered array of `<nys-tabpanel>` elements to update.
|
|
146
|
+
* Must be the same length as `tabs` for correct pairing.
|
|
147
|
+
* @param selectedIndex - Zero-based index of the tab/panel pair to activate.
|
|
148
|
+
* @returns void
|
|
149
|
+
*/
|
|
150
|
+
private _applySelection;
|
|
151
|
+
/**
|
|
152
|
+
* Handles `slotchange` on the default slot.
|
|
153
|
+
*
|
|
154
|
+
* Iterates over all assigned elements and moves each `<nys-tab>` into
|
|
155
|
+
* `.nys-tabgroup__tabs` and each `<nys-tabpanel>` into
|
|
156
|
+
* `.nys-tabgroup__panels`, preserving relative order. After sorting,
|
|
157
|
+
* calls `_applySelection` using the first element that already has a
|
|
158
|
+
* `selected` attribute, or index `0` if none is found.
|
|
159
|
+
*
|
|
160
|
+
* @param e - The `Event` fired by the `<slot>` element on slot change.
|
|
161
|
+
* @returns void
|
|
162
|
+
*/
|
|
163
|
+
private _sortChildren;
|
|
164
|
+
/**
|
|
165
|
+
* Handles the `nys-tab-select` custom event bubbled up from a child
|
|
166
|
+
* `<nys-tab>`.
|
|
167
|
+
*
|
|
168
|
+
* Resolves the originating `<nys-tab>` via `composedPath()` (required
|
|
169
|
+
* because the event crosses shadow DOM boundaries), determines its index
|
|
170
|
+
* among the current tab list, and delegates to `_applySelection`.
|
|
171
|
+
*
|
|
172
|
+
* @param e - The `Event` (cast to `CustomEvent`) dispatched by `<nys-tab>`.
|
|
173
|
+
* @returns void
|
|
174
|
+
*/
|
|
175
|
+
private _handleTabSelect;
|
|
176
|
+
/**
|
|
177
|
+
* Implements the ARIA radio-button keyboard pattern:
|
|
178
|
+
* - `ArrowRight` — moves focus to the next enabled tab (wraps).
|
|
179
|
+
* - `ArrowLeft` — moves focus to the previous enabled tab (wraps).
|
|
180
|
+
*
|
|
181
|
+
* Focus is moved without changing selection; Enter / Space on the newly
|
|
182
|
+
* focused tab (handled by `<nys-tab>._handleKeydown`) confirm selection.
|
|
183
|
+
*
|
|
184
|
+
* The currently focused tab is resolved via `composedPath()` because focus
|
|
185
|
+
* may sit on a shadow-DOM descendant of `<nys-tab>` rather than the host
|
|
186
|
+
* itself.
|
|
187
|
+
*
|
|
188
|
+
* Disabled tabs are excluded from navigation and will never receive focus
|
|
189
|
+
* via arrow keys.
|
|
190
|
+
*
|
|
191
|
+
* @param e - The `KeyboardEvent` from the tablist `keydown` listener.
|
|
192
|
+
* @returns void
|
|
193
|
+
*/
|
|
194
|
+
private _handleKeydown;
|
|
195
|
+
private _handleWheel;
|
|
196
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
197
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { LitElement } from "lit";
|
|
2
|
+
/**
|
|
3
|
+
* `<nys-tabpanel>` is a content panel paired with a `<nys-tab>` inside a
|
|
4
|
+
* `<nys-tabgroup>`.
|
|
5
|
+
*
|
|
6
|
+
* Pairing is determined by render order: the Nth `<nys-tabpanel>` child of a
|
|
7
|
+
* `<nys-tabgroup>` corresponds to the Nth `<nys-tab>` child.
|
|
8
|
+
* `aria-labelledby` and the `hidden` attribute are managed externally by
|
|
9
|
+
* `<nys-tabgroup>` via `_applySelection`; do not set them directly.
|
|
10
|
+
*
|
|
11
|
+
* @element nys-tabpanel
|
|
12
|
+
*
|
|
13
|
+
* @slot - Default slot for panel content. Rendered inside a wrapper `<div>`
|
|
14
|
+
* with the `.nys-tabpanel` class for styling.
|
|
15
|
+
*
|
|
16
|
+
* @example Panel content is wrapped by `<nys-tabpanel>`.
|
|
17
|
+
* ```html
|
|
18
|
+
* <!-- Panels are paired by position with <nys-tab> elements in the same <nys-tabgroup>. -->
|
|
19
|
+
* <nys-tabgroup name="Steps">
|
|
20
|
+
* <nys-tab label="Step 1"></nys-tab>
|
|
21
|
+
* <nys-tab label="Step 2"></nys-tab>
|
|
22
|
+
* <nys-tabpanel>
|
|
23
|
+
* <h2>Step 1: Enter your information</h2>
|
|
24
|
+
* <p>Fill out the form below.</p>
|
|
25
|
+
* </nys-tabpanel>
|
|
26
|
+
* <nys-tabpanel>
|
|
27
|
+
* <h2>Step 2: Review and submit</h2>
|
|
28
|
+
* <p>Confirm your details before submitting.</p>
|
|
29
|
+
* </nys-tabpanel>
|
|
30
|
+
* </nys-tabgroup>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare class NysTabpanel extends LitElement {
|
|
34
|
+
static styles: import("lit").CSSResult;
|
|
35
|
+
/**
|
|
36
|
+
* Unique identifier for the panel element.
|
|
37
|
+
* If not provided, one is auto-generated in `connectedCallback`.
|
|
38
|
+
* Reflected to the DOM attribute so `aria-controls` references on sibling
|
|
39
|
+
* `<nys-tab>` elements resolve correctly.
|
|
40
|
+
*
|
|
41
|
+
* @attr id
|
|
42
|
+
*/
|
|
43
|
+
id: string;
|
|
44
|
+
connectedCallback(): void;
|
|
45
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
46
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nysds/nys-tab",
|
|
3
|
+
"version": "1.18.0",
|
|
4
|
+
"description": "The Tab component from the NYS Design System.",
|
|
5
|
+
"module": "dist/nys-tab.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/nys-tab.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"dev": "tsc --emitDeclarationOnly && vite",
|
|
19
|
+
"build": "tsc --emitDeclarationOnly && vite build",
|
|
20
|
+
"test": "vite build && wtr",
|
|
21
|
+
"build:watch": "tsc --emitDeclarationOnly && vite build --watch",
|
|
22
|
+
"test:watch": "vite build && wtr --watch",
|
|
23
|
+
"lit-analyze": "lit-analyzer '*.ts'"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@nysds/nys-button": "1.18.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"lit": "^3.3.1",
|
|
30
|
+
"typescript": "^5.9.3",
|
|
31
|
+
"vite": "^7.3.1"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"new-york-state",
|
|
35
|
+
"design-system",
|
|
36
|
+
"web-components",
|
|
37
|
+
"lit",
|
|
38
|
+
"nys",
|
|
39
|
+
"tab"
|
|
40
|
+
],
|
|
41
|
+
"author": "New York State Design System Team",
|
|
42
|
+
"license": "MIT"
|
|
43
|
+
}
|