@kanso-protocol/tabs 1.0.1 → 2.0.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.
|
@@ -5,16 +5,16 @@ import { Subject, takeUntil } from 'rxjs';
|
|
|
5
5
|
/**
|
|
6
6
|
* Kanso Protocol — Tab (atom)
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Attribute-selector on a real native `<button role="tab">`. Sits inside
|
|
9
|
+
* a `<kp-tabs role="tablist">`. Native button gives Enter+Space
|
|
10
|
+
* activation, focus-visible, and disabled. Arrow-key navigation between
|
|
11
|
+
* tabs is owned by the parent `<kp-tabs>` via roving tabindex.
|
|
12
12
|
*
|
|
13
13
|
* @example
|
|
14
|
-
* <
|
|
14
|
+
* <button kpTab label="Inbox" [selected]="current === 'inbox'" (click)="current='inbox'">
|
|
15
15
|
* <svg kpTabIcon .../>
|
|
16
16
|
* <kp-badge kpTabBadge>12</kp-badge>
|
|
17
|
-
* </
|
|
17
|
+
* </button>
|
|
18
18
|
*/
|
|
19
19
|
class KpTabComponent {
|
|
20
20
|
size = 'md';
|
|
@@ -24,69 +24,51 @@ class KpTabComponent {
|
|
|
24
24
|
/** Set by the parent `<kp-tabs>` when `fullWidth` is enabled on the container */
|
|
25
25
|
fullWidth = false;
|
|
26
26
|
selectedChange = new EventEmitter();
|
|
27
|
-
/** @internal —
|
|
28
|
-
* inner button as part of its arrow-key roving-tabindex navigation. */
|
|
27
|
+
/** @internal — parent `<kp-tabs>` focuses this for arrow-key roving-tabindex navigation. */
|
|
29
28
|
elementRef = inject(ElementRef);
|
|
30
29
|
get hostClasses() {
|
|
31
30
|
const c = ['kp-tab', `kp-tab--${this.size}`];
|
|
32
31
|
if (this.selected)
|
|
33
32
|
c.push('kp-tab--selected');
|
|
34
|
-
if (this.disabled)
|
|
35
|
-
c.push('kp-tab--disabled');
|
|
36
33
|
if (this.fullWidth)
|
|
37
34
|
c.push('kp-tab--full-width');
|
|
38
35
|
return c.join(' ');
|
|
39
36
|
}
|
|
40
|
-
handleClick(
|
|
41
|
-
if (this.disabled) {
|
|
42
|
-
event.preventDefault();
|
|
43
|
-
event.stopPropagation();
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
this.selectedChange.emit(true);
|
|
47
|
-
}
|
|
48
|
-
handleKey(event) {
|
|
49
|
-
if (event.key !== 'Enter' && event.key !== ' ')
|
|
50
|
-
return;
|
|
37
|
+
handleClick() {
|
|
51
38
|
if (this.disabled)
|
|
52
39
|
return;
|
|
53
|
-
event.preventDefault();
|
|
54
40
|
this.selectedChange.emit(true);
|
|
55
41
|
}
|
|
56
42
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: KpTabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
57
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.7", type: KpTabComponent, isStandalone: true, selector: "
|
|
58
|
-
<span class="kp-
|
|
59
|
-
<
|
|
60
|
-
<ng-content select="[kpTabIcon]"/>
|
|
61
|
-
</span>
|
|
62
|
-
<span class="kp-tab__label">{{ label }}</span>
|
|
63
|
-
<span class="kp-tab__badge">
|
|
64
|
-
<ng-content select="[kpTabBadge]"/>
|
|
65
|
-
</span>
|
|
43
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.7", type: KpTabComponent, isStandalone: true, selector: "button[kpTab]", inputs: { size: "size", label: "label", selected: "selected", disabled: "disabled", fullWidth: "fullWidth" }, outputs: { selectedChange: "selectedChange" }, host: { listeners: { "click": "handleClick()" }, properties: { "class": "hostClasses", "attr.type": "\"button\"", "attr.role": "\"tab\"", "attr.aria-selected": "selected", "disabled": "disabled", "attr.tabindex": "disabled ? -1 : (selected ? 0 : -1)" } }, ngImport: i0, template: `
|
|
44
|
+
<span class="kp-tab__icon" aria-hidden="true">
|
|
45
|
+
<ng-content select="[kpTabIcon]"/>
|
|
66
46
|
</span>
|
|
67
|
-
|
|
47
|
+
<span class="kp-tab__label">{{ label }}</span>
|
|
48
|
+
<span class="kp-tab__badge">
|
|
49
|
+
<ng-content select="[kpTabBadge]"/>
|
|
50
|
+
</span>
|
|
51
|
+
`, isInline: true, styles: [":host{all:unset;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:var(--kp-tab-gap);height:var(--kp-tab-h);padding:0 var(--kp-tab-pad-x);border-bottom:2px solid var(--kp-color-tabs-tab-underline-rest, transparent);color:var(--kp-color-tabs-tab-fg-rest);cursor:pointer;font-family:var(--kp-font-family-sans, \"Onest\", system-ui, sans-serif);font-size:var(--kp-tab-font-size);line-height:var(--kp-tab-line-height);font-weight:500;transition:border-color var(--kp-motion-duration-fast) ease,color .12s ease}:host(.kp-tab--full-width){flex:1 1 0}:host(:focus-visible){outline:2px solid var(--kp-color-focus-ring);outline-offset:-2px}.kp-tab__icon{display:inline-flex;align-items:center;color:var(--kp-color-tabs-tab-icon-rest)}.kp-tab__icon:empty{display:none}.kp-tab__icon ::ng-deep svg{width:var(--kp-tab-icon-size);height:var(--kp-tab-icon-size)}.kp-tab__badge{display:inline-flex}.kp-tab__badge:empty{display:none}:host(:hover:not(.kp-tab--selected):not([disabled])){border-bottom-color:var(--kp-color-tabs-tab-underline-hover);color:var(--kp-color-tabs-tab-fg-hover)}:host(:hover:not(.kp-tab--selected):not([disabled])) .kp-tab__icon{color:var(--kp-color-tabs-tab-icon-hover)}:host(.kp-tab--selected){border-bottom-color:var(--kp-color-tabs-tab-underline-selected);color:var(--kp-color-tabs-tab-fg-selected)}:host(.kp-tab--selected) .kp-tab__icon{color:var(--kp-color-tabs-tab-icon-selected)}:host([disabled]){border-bottom-color:var(--kp-color-tabs-tab-underline-disabled, transparent);color:var(--kp-color-tabs-tab-fg-disabled);cursor:not-allowed}:host([disabled]) .kp-tab__icon{color:var(--kp-color-tabs-tab-icon-disabled)}:host(.kp-tab--sm){--kp-tab-h: 32px;--kp-tab-pad-x: 12px;--kp-tab-gap: 6px;--kp-tab-font-size: 14px;--kp-tab-line-height: 20px;--kp-tab-icon-size: 14px}:host(.kp-tab--md){--kp-tab-h: 40px;--kp-tab-pad-x: 16px;--kp-tab-gap: 8px;--kp-tab-font-size: 14px;--kp-tab-line-height: 20px;--kp-tab-icon-size: 16px}:host(.kp-tab--lg){--kp-tab-h: 48px;--kp-tab-pad-x: 20px;--kp-tab-gap: 8px;--kp-tab-font-size: 16px;--kp-tab-line-height: 24px;--kp-tab-icon-size: 16px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
68
52
|
}
|
|
69
53
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: KpTabComponent, decorators: [{
|
|
70
54
|
type: Component,
|
|
71
|
-
args: [{ selector: '
|
|
55
|
+
args: [{ selector: 'button[kpTab]', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
72
56
|
'[class]': 'hostClasses',
|
|
57
|
+
'[attr.type]': '"button"',
|
|
73
58
|
'[attr.role]': '"tab"',
|
|
74
59
|
'[attr.aria-selected]': 'selected',
|
|
75
|
-
'[
|
|
60
|
+
'[disabled]': 'disabled',
|
|
76
61
|
'[attr.tabindex]': 'disabled ? -1 : (selected ? 0 : -1)',
|
|
77
|
-
'(click)': 'handleClick(
|
|
78
|
-
'(keydown)': 'handleKey($event)',
|
|
62
|
+
'(click)': 'handleClick()',
|
|
79
63
|
}, template: `
|
|
80
|
-
<span class="kp-
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
<ng-content select="[kpTabBadge]"/>
|
|
87
|
-
</span>
|
|
64
|
+
<span class="kp-tab__icon" aria-hidden="true">
|
|
65
|
+
<ng-content select="[kpTabIcon]"/>
|
|
66
|
+
</span>
|
|
67
|
+
<span class="kp-tab__label">{{ label }}</span>
|
|
68
|
+
<span class="kp-tab__badge">
|
|
69
|
+
<ng-content select="[kpTabBadge]"/>
|
|
88
70
|
</span>
|
|
89
|
-
`, styles: [":host{
|
|
71
|
+
`, styles: [":host{all:unset;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:var(--kp-tab-gap);height:var(--kp-tab-h);padding:0 var(--kp-tab-pad-x);border-bottom:2px solid var(--kp-color-tabs-tab-underline-rest, transparent);color:var(--kp-color-tabs-tab-fg-rest);cursor:pointer;font-family:var(--kp-font-family-sans, \"Onest\", system-ui, sans-serif);font-size:var(--kp-tab-font-size);line-height:var(--kp-tab-line-height);font-weight:500;transition:border-color var(--kp-motion-duration-fast) ease,color .12s ease}:host(.kp-tab--full-width){flex:1 1 0}:host(:focus-visible){outline:2px solid var(--kp-color-focus-ring);outline-offset:-2px}.kp-tab__icon{display:inline-flex;align-items:center;color:var(--kp-color-tabs-tab-icon-rest)}.kp-tab__icon:empty{display:none}.kp-tab__icon ::ng-deep svg{width:var(--kp-tab-icon-size);height:var(--kp-tab-icon-size)}.kp-tab__badge{display:inline-flex}.kp-tab__badge:empty{display:none}:host(:hover:not(.kp-tab--selected):not([disabled])){border-bottom-color:var(--kp-color-tabs-tab-underline-hover);color:var(--kp-color-tabs-tab-fg-hover)}:host(:hover:not(.kp-tab--selected):not([disabled])) .kp-tab__icon{color:var(--kp-color-tabs-tab-icon-hover)}:host(.kp-tab--selected){border-bottom-color:var(--kp-color-tabs-tab-underline-selected);color:var(--kp-color-tabs-tab-fg-selected)}:host(.kp-tab--selected) .kp-tab__icon{color:var(--kp-color-tabs-tab-icon-selected)}:host([disabled]){border-bottom-color:var(--kp-color-tabs-tab-underline-disabled, transparent);color:var(--kp-color-tabs-tab-fg-disabled);cursor:not-allowed}:host([disabled]) .kp-tab__icon{color:var(--kp-color-tabs-tab-icon-disabled)}:host(.kp-tab--sm){--kp-tab-h: 32px;--kp-tab-pad-x: 12px;--kp-tab-gap: 6px;--kp-tab-font-size: 14px;--kp-tab-line-height: 20px;--kp-tab-icon-size: 14px}:host(.kp-tab--md){--kp-tab-h: 40px;--kp-tab-pad-x: 16px;--kp-tab-gap: 8px;--kp-tab-font-size: 14px;--kp-tab-line-height: 20px;--kp-tab-icon-size: 16px}:host(.kp-tab--lg){--kp-tab-h: 48px;--kp-tab-pad-x: 20px;--kp-tab-gap: 8px;--kp-tab-font-size: 16px;--kp-tab-line-height: 24px;--kp-tab-icon-size: 16px}\n"] }]
|
|
90
72
|
}], propDecorators: { size: [{
|
|
91
73
|
type: Input
|
|
92
74
|
}], label: [{
|
|
@@ -104,8 +86,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImpor
|
|
|
104
86
|
/**
|
|
105
87
|
* Kanso Protocol — Tabs (container)
|
|
106
88
|
*
|
|
107
|
-
* Horizontal strip of `<
|
|
108
|
-
* Cascades `size` and `fullWidth` down to every projected `<
|
|
89
|
+
* Horizontal strip of `<button kpTab>` children with a shared bottom border.
|
|
90
|
+
* Cascades `size` and `fullWidth` down to every projected `<button kpTab>`
|
|
109
91
|
* automatically, so callers only set those two inputs in one place.
|
|
110
92
|
*
|
|
111
93
|
* A `[kpTabsMore]` slot projects an optional "More ▾" affordance to the
|
|
@@ -114,9 +96,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImpor
|
|
|
114
96
|
*
|
|
115
97
|
* @example
|
|
116
98
|
* <kp-tabs size="md" [fullWidth]="false">
|
|
117
|
-
* <
|
|
118
|
-
* <
|
|
119
|
-
* <
|
|
99
|
+
* <button kpTab label="Overview" [selected]="tab === 'overview'" (selectedChange)="tab = 'overview'"></button>
|
|
100
|
+
* <button kpTab label="Activity" [selected]="tab === 'activity'" (selectedChange)="tab = 'activity'"></button>
|
|
101
|
+
* <button kpTab label="Settings" [selected]="tab === 'settings'" (selectedChange)="tab = 'settings'"></button>
|
|
120
102
|
*
|
|
121
103
|
* <button kpTabsMore>More ▾</button>
|
|
122
104
|
* </kp-tabs>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kanso-protocol-tabs.mjs","sources":["../../../../../packages/components/tabs/src/tab.component.ts","../../../../../packages/components/tabs/src/tabs.component.ts","../../../../../packages/components/tabs/src/kanso-protocol-tabs.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n EventEmitter,\n Input,\n Output,\n inject,\n} from '@angular/core';\n\nexport type KpTabSize = 'sm' | 'md' | 'lg';\n\n/**\n * Kanso Protocol — Tab (atom)\n *\n * Single tab for use inside `<kp-tabs>`. Projects an optional leading icon\n * and an optional trailing badge. The `selected` / `disabled` inputs drive\n * the visual state; hover is handled in CSS and focus via `:focus-visible`\n * on the underlying button.\n *\n * @example\n * <kp-tab label=\"Inbox\" [selected]=\"current === 'inbox'\" (click)=\"current='inbox'\">\n * <svg kpTabIcon .../>\n * <kp-badge kpTabBadge>12</kp-badge>\n * </kp-tab>\n */\n@Component({\n selector: 'kp-tab',\n imports: [],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class]': 'hostClasses',\n '[attr.role]': '\"tab\"',\n '[attr.aria-selected]': 'selected',\n '[attr.aria-disabled]': 'disabled || null',\n '[attr.tabindex]': 'disabled ? -1 : (selected ? 0 : -1)',\n '(click)': 'handleClick($event)',\n '(keydown)': 'handleKey($event)',\n },\n // Inner content is a non-interactive span. The previous <button> inside the\n // role=\"tab\" host triggered axe's nested-interactive rule. Click + keyboard\n // activation now live on the host directly; arrow-key navigation between\n // tabs is owned by the parent <kp-tabs> via roving tabindex (see tabs.component.ts).\n template: `\n <span class=\"kp-tab__btn\">\n <span class=\"kp-tab__icon\" aria-hidden=\"true\">\n <ng-content select=\"[kpTabIcon]\"/>\n </span>\n <span class=\"kp-tab__label\">{{ label }}</span>\n <span class=\"kp-tab__badge\">\n <ng-content select=\"[kpTabBadge]\"/>\n </span>\n </span>\n `,\n styles: [`\n :host {\n display: inline-flex;\n box-sizing: border-box;\n height: var(--kp-tab-h);\n border-bottom: 2px solid var(--kp-color-tabs-tab-underline-rest, transparent);\n font-family: var(--kp-font-family-sans, 'Onest', system-ui, sans-serif);\n transition: border-color var(--kp-motion-duration-fast) ease, color 120ms ease;\n }\n :host(.kp-tab--full-width) { flex: 1 1 0; }\n\n .kp-tab__btn {\n all: unset;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--kp-tab-gap);\n padding: 0 var(--kp-tab-pad-x);\n height: 100%;\n width: 100%;\n color: var(--kp-color-tabs-tab-fg-rest);\n cursor: pointer;\n font-size: var(--kp-tab-font-size);\n line-height: var(--kp-tab-line-height);\n font-weight: 500;\n }\n .kp-tab__btn:focus-visible {\n outline: 2px solid var(--kp-color-focus-ring);\n outline-offset: -2px;\n }\n\n .kp-tab__icon {\n display: inline-flex;\n align-items: center;\n color: var(--kp-color-tabs-tab-icon-rest);\n }\n .kp-tab__icon:empty { display: none; }\n .kp-tab__icon ::ng-deep svg {\n width: var(--kp-tab-icon-size);\n height: var(--kp-tab-icon-size);\n }\n\n .kp-tab__badge { display: inline-flex; }\n .kp-tab__badge:empty { display: none; }\n\n /* Hover (rest only — disabled / selected handled explicitly) */\n :host(:not(.kp-tab--selected):not(.kp-tab--disabled):hover) {\n border-bottom-color: var(--kp-color-tabs-tab-underline-hover);\n }\n :host(:not(.kp-tab--selected):not(.kp-tab--disabled):hover) .kp-tab__btn {\n color: var(--kp-color-tabs-tab-fg-hover);\n }\n :host(:not(.kp-tab--selected):not(.kp-tab--disabled):hover) .kp-tab__icon {\n color: var(--kp-color-tabs-tab-icon-hover);\n }\n\n /* Selected */\n :host(.kp-tab--selected) {\n border-bottom-color: var(--kp-color-tabs-tab-underline-selected);\n }\n :host(.kp-tab--selected) .kp-tab__btn {\n color: var(--kp-color-tabs-tab-fg-selected);\n }\n :host(.kp-tab--selected) .kp-tab__icon {\n color: var(--kp-color-tabs-tab-icon-selected);\n }\n\n /* Disabled */\n :host(.kp-tab--disabled) {\n border-bottom-color: var(--kp-color-tabs-tab-underline-disabled, transparent);\n }\n :host(.kp-tab--disabled) .kp-tab__btn {\n color: var(--kp-color-tabs-tab-fg-disabled);\n cursor: not-allowed;\n }\n :host(.kp-tab--disabled) .kp-tab__icon {\n color: var(--kp-color-tabs-tab-icon-disabled);\n }\n\n /* Sizes */\n :host(.kp-tab--sm) {\n --kp-tab-h: 32px;\n --kp-tab-pad-x: 12px;\n --kp-tab-gap: 6px;\n --kp-tab-font-size: 14px;\n --kp-tab-line-height: 20px;\n --kp-tab-icon-size: 14px;\n }\n :host(.kp-tab--md) {\n --kp-tab-h: 40px;\n --kp-tab-pad-x: 16px;\n --kp-tab-gap: 8px;\n --kp-tab-font-size: 14px;\n --kp-tab-line-height: 20px;\n --kp-tab-icon-size: 16px;\n }\n :host(.kp-tab--lg) {\n --kp-tab-h: 48px;\n --kp-tab-pad-x: 20px;\n --kp-tab-gap: 8px;\n --kp-tab-font-size: 16px;\n --kp-tab-line-height: 24px;\n --kp-tab-icon-size: 16px;\n }\n `],\n})\nexport class KpTabComponent {\n @Input() size: KpTabSize = 'md';\n @Input() label = '';\n @Input() selected = false;\n @Input() disabled = false;\n /** Set by the parent `<kp-tabs>` when `fullWidth` is enabled on the container */\n @Input() fullWidth = false;\n\n @Output() selectedChange = new EventEmitter<boolean>();\n\n /** @internal — exposed so the parent `<kp-tabs>` can focus this tab's\n * inner button as part of its arrow-key roving-tabindex navigation. */\n readonly elementRef = inject(ElementRef) as ElementRef<HTMLElement>;\n\n get hostClasses(): string {\n const c = ['kp-tab', `kp-tab--${this.size}`];\n if (this.selected) c.push('kp-tab--selected');\n if (this.disabled) c.push('kp-tab--disabled');\n if (this.fullWidth) c.push('kp-tab--full-width');\n return c.join(' ');\n }\n\n handleClick(event: MouseEvent): void {\n if (this.disabled) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n this.selectedChange.emit(true);\n }\n\n handleKey(event: KeyboardEvent): void {\n if (event.key !== 'Enter' && event.key !== ' ') return;\n if (this.disabled) return;\n event.preventDefault();\n this.selectedChange.emit(true);\n }\n}\n","import {\n AfterContentInit,\n ChangeDetectionStrategy,\n Component,\n ContentChildren,\n ElementRef,\n Input,\n OnDestroy,\n QueryList,\n inject,\n} from '@angular/core';\nimport { Subject, takeUntil } from 'rxjs';\n\nimport { KpTabComponent, KpTabSize } from './tab.component';\n\n/**\n * Kanso Protocol — Tabs (container)\n *\n * Horizontal strip of `<kp-tab>` children with a shared bottom border.\n * Cascades `size` and `fullWidth` down to every projected `<kp-tab>`\n * automatically, so callers only set those two inputs in one place.\n *\n * A `[kpTabsMore]` slot projects an optional \"More ▾\" affordance to the\n * right of the strip — useful when tabs overflow and you want a dropdown\n * to hold the rest.\n *\n * @example\n * <kp-tabs size=\"md\" [fullWidth]=\"false\">\n * <kp-tab label=\"Overview\" [selected]=\"tab === 'overview'\" (selectedChange)=\"tab = 'overview'\"/>\n * <kp-tab label=\"Activity\" [selected]=\"tab === 'activity'\" (selectedChange)=\"tab = 'activity'\"/>\n * <kp-tab label=\"Settings\" [selected]=\"tab === 'settings'\" (selectedChange)=\"tab = 'settings'\"/>\n *\n * <button kpTabsMore>More ▾</button>\n * </kp-tabs>\n */\n@Component({\n selector: 'kp-tabs',\n imports: [],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class]': 'hostClasses',\n '(keydown)': 'onKeyDown($event)',\n },\n // role=\"tablist\" lives on the row that holds the projected <kp-tab> elements.\n // The host also carries an optional [kpTabsMore] sibling slot which is not a\n // tab, so role=tablist on the host would fail axe's aria-required-children.\n template: `\n <div class=\"kp-tabs__row\" role=\"tablist\"><ng-content/></div>\n <div class=\"kp-tabs__more\"><ng-content select=\"[kpTabsMore]\"/></div>\n `,\n styles: [`\n :host {\n position: relative;\n display: flex;\n align-items: stretch;\n border-bottom: 1px solid var(--kp-color-tabs-track-border);\n font-family: var(--kp-font-family-sans, 'Onest', system-ui, sans-serif);\n }\n\n .kp-tabs__row {\n display: flex;\n flex: 1 1 auto;\n /* The Tab atoms already carry their own border-bottom; no extra row chrome needed. */\n }\n :host(.kp-tabs--full-width) .kp-tabs__row > * { flex: 1 1 0; }\n\n .kp-tabs__more { display: inline-flex; flex: 0 0 auto; }\n .kp-tabs__more:empty { display: none; }\n `],\n})\nexport class KpTabsComponent implements AfterContentInit, OnDestroy {\n @Input() size: KpTabSize = 'md';\n @Input() fullWidth = false;\n\n @ContentChildren(KpTabComponent) tabs!: QueryList<KpTabComponent>;\n\n private readonly host = inject(ElementRef) as ElementRef<HTMLElement>;\n private readonly destroyed$ = new Subject<void>();\n\n ngAfterContentInit(): void {\n this.applyToTabs();\n this.tabs.changes.pipe(takeUntil(this.destroyed$)).subscribe(() => this.applyToTabs());\n }\n\n ngOnDestroy(): void {\n this.destroyed$.next();\n this.destroyed$.complete();\n }\n\n private applyToTabs(): void {\n this.tabs?.forEach((t) => {\n t.size = this.size;\n t.fullWidth = this.fullWidth;\n });\n }\n\n /**\n * WAI-ARIA tablist keyboard pattern:\n * ArrowLeft / ArrowRight — move to previous / next enabled tab (wraps around)\n * Home / End — jump to first / last enabled tab\n * Enter / Space — select the focused tab (native button handles this too)\n * Disabled tabs are skipped. Selection activates on focus change (automatic\n * activation) since our tab bodies are lightweight to render.\n */\n onKeyDown(event: KeyboardEvent): void {\n const all = this.tabs?.toArray() ?? [];\n const enabled = all.filter((t) => !t.disabled);\n if (!enabled.length) return;\n\n const active = this.activeTabIndex(enabled);\n let next: number | null = null;\n switch (event.key) {\n case 'ArrowRight': next = (active + 1) % enabled.length; break;\n case 'ArrowLeft': next = (active - 1 + enabled.length) % enabled.length; break;\n case 'Home': next = 0; break;\n case 'End': next = enabled.length - 1; break;\n default: return;\n }\n event.preventDefault();\n const target = enabled[next];\n // \"Consumer owns state\" — same pattern as click. Emit (selectedChange)\n // on the target tab and let the parent's [selected] binding propagate.\n if (!target.selected) target.selectedChange.emit(true);\n this.focusTab(target);\n }\n\n private activeTabIndex(enabled: KpTabComponent[]): number {\n const doc = this.host.nativeElement.ownerDocument;\n const focused = doc?.activeElement ?? null;\n const byFocus = enabled.findIndex((t) => t.elementRef.nativeElement.contains(focused));\n if (byFocus >= 0) return byFocus;\n const bySelection = enabled.findIndex((t) => t.selected);\n return bySelection >= 0 ? bySelection : 0;\n }\n\n private focusTab(tab: KpTabComponent): void {\n // The host element itself is the focusable target — it carries role=\"tab\"\n // and tabindex. The inner span is non-interactive (axe nested-interactive).\n tab.elementRef.nativeElement.focus();\n }\n\n get hostClasses(): string {\n const c = ['kp-tabs'];\n if (this.fullWidth) c.push('kp-tabs--full-width');\n return c.join(' ');\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"kanso-protocol-tabs.mjs","sources":["../../../../../packages/components/tabs/src/tab.component.ts","../../../../../packages/components/tabs/src/tabs.component.ts","../../../../../packages/components/tabs/src/kanso-protocol-tabs.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n EventEmitter,\n Input,\n Output,\n inject,\n} from '@angular/core';\n\nexport type KpTabSize = 'sm' | 'md' | 'lg';\n\n/**\n * Kanso Protocol — Tab (atom)\n *\n * Attribute-selector on a real native `<button role=\"tab\">`. Sits inside\n * a `<kp-tabs role=\"tablist\">`. Native button gives Enter+Space\n * activation, focus-visible, and disabled. Arrow-key navigation between\n * tabs is owned by the parent `<kp-tabs>` via roving tabindex.\n *\n * @example\n * <button kpTab label=\"Inbox\" [selected]=\"current === 'inbox'\" (click)=\"current='inbox'\">\n * <svg kpTabIcon .../>\n * <kp-badge kpTabBadge>12</kp-badge>\n * </button>\n */\n@Component({\n selector: 'button[kpTab]',\n imports: [],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class]': 'hostClasses',\n '[attr.type]': '\"button\"',\n '[attr.role]': '\"tab\"',\n '[attr.aria-selected]': 'selected',\n '[disabled]': 'disabled',\n '[attr.tabindex]': 'disabled ? -1 : (selected ? 0 : -1)',\n '(click)': 'handleClick()',\n },\n template: `\n <span class=\"kp-tab__icon\" aria-hidden=\"true\">\n <ng-content select=\"[kpTabIcon]\"/>\n </span>\n <span class=\"kp-tab__label\">{{ label }}</span>\n <span class=\"kp-tab__badge\">\n <ng-content select=\"[kpTabBadge]\"/>\n </span>\n `,\n styles: [`\n :host {\n all: unset;\n box-sizing: border-box;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--kp-tab-gap);\n height: var(--kp-tab-h);\n padding: 0 var(--kp-tab-pad-x);\n border-bottom: 2px solid var(--kp-color-tabs-tab-underline-rest, transparent);\n color: var(--kp-color-tabs-tab-fg-rest);\n cursor: pointer;\n font-family: var(--kp-font-family-sans, 'Onest', system-ui, sans-serif);\n font-size: var(--kp-tab-font-size);\n line-height: var(--kp-tab-line-height);\n font-weight: 500;\n transition: border-color var(--kp-motion-duration-fast) ease, color 120ms ease;\n }\n :host(.kp-tab--full-width) { flex: 1 1 0; }\n\n :host(:focus-visible) {\n outline: 2px solid var(--kp-color-focus-ring);\n outline-offset: -2px;\n }\n\n .kp-tab__icon {\n display: inline-flex;\n align-items: center;\n color: var(--kp-color-tabs-tab-icon-rest);\n }\n .kp-tab__icon:empty { display: none; }\n .kp-tab__icon ::ng-deep svg {\n width: var(--kp-tab-icon-size);\n height: var(--kp-tab-icon-size);\n }\n\n .kp-tab__badge { display: inline-flex; }\n .kp-tab__badge:empty { display: none; }\n\n /* Hover (rest only) */\n :host(:hover:not(.kp-tab--selected):not([disabled])) {\n border-bottom-color: var(--kp-color-tabs-tab-underline-hover);\n color: var(--kp-color-tabs-tab-fg-hover);\n }\n :host(:hover:not(.kp-tab--selected):not([disabled])) .kp-tab__icon {\n color: var(--kp-color-tabs-tab-icon-hover);\n }\n\n /* Selected */\n :host(.kp-tab--selected) {\n border-bottom-color: var(--kp-color-tabs-tab-underline-selected);\n color: var(--kp-color-tabs-tab-fg-selected);\n }\n :host(.kp-tab--selected) .kp-tab__icon {\n color: var(--kp-color-tabs-tab-icon-selected);\n }\n\n /* Disabled (native [disabled]) */\n :host([disabled]) {\n border-bottom-color: var(--kp-color-tabs-tab-underline-disabled, transparent);\n color: var(--kp-color-tabs-tab-fg-disabled);\n cursor: not-allowed;\n }\n :host([disabled]) .kp-tab__icon {\n color: var(--kp-color-tabs-tab-icon-disabled);\n }\n\n /* Sizes */\n :host(.kp-tab--sm) {\n --kp-tab-h: 32px;\n --kp-tab-pad-x: 12px;\n --kp-tab-gap: 6px;\n --kp-tab-font-size: 14px;\n --kp-tab-line-height: 20px;\n --kp-tab-icon-size: 14px;\n }\n :host(.kp-tab--md) {\n --kp-tab-h: 40px;\n --kp-tab-pad-x: 16px;\n --kp-tab-gap: 8px;\n --kp-tab-font-size: 14px;\n --kp-tab-line-height: 20px;\n --kp-tab-icon-size: 16px;\n }\n :host(.kp-tab--lg) {\n --kp-tab-h: 48px;\n --kp-tab-pad-x: 20px;\n --kp-tab-gap: 8px;\n --kp-tab-font-size: 16px;\n --kp-tab-line-height: 24px;\n --kp-tab-icon-size: 16px;\n }\n `],\n})\nexport class KpTabComponent {\n @Input() size: KpTabSize = 'md';\n @Input() label = '';\n @Input() selected = false;\n @Input() disabled = false;\n /** Set by the parent `<kp-tabs>` when `fullWidth` is enabled on the container */\n @Input() fullWidth = false;\n\n @Output() selectedChange = new EventEmitter<boolean>();\n\n /** @internal — parent `<kp-tabs>` focuses this for arrow-key roving-tabindex navigation. */\n readonly elementRef = inject(ElementRef) as ElementRef<HTMLElement>;\n\n get hostClasses(): string {\n const c = ['kp-tab', `kp-tab--${this.size}`];\n if (this.selected) c.push('kp-tab--selected');\n if (this.fullWidth) c.push('kp-tab--full-width');\n return c.join(' ');\n }\n\n handleClick(): void {\n if (this.disabled) return;\n this.selectedChange.emit(true);\n }\n}\n","import {\n AfterContentInit,\n ChangeDetectionStrategy,\n Component,\n ContentChildren,\n ElementRef,\n Input,\n OnDestroy,\n QueryList,\n inject,\n} from '@angular/core';\nimport { Subject, takeUntil } from 'rxjs';\n\nimport { KpTabComponent, KpTabSize } from './tab.component';\n\n/**\n * Kanso Protocol — Tabs (container)\n *\n * Horizontal strip of `<button kpTab>` children with a shared bottom border.\n * Cascades `size` and `fullWidth` down to every projected `<button kpTab>`\n * automatically, so callers only set those two inputs in one place.\n *\n * A `[kpTabsMore]` slot projects an optional \"More ▾\" affordance to the\n * right of the strip — useful when tabs overflow and you want a dropdown\n * to hold the rest.\n *\n * @example\n * <kp-tabs size=\"md\" [fullWidth]=\"false\">\n * <button kpTab label=\"Overview\" [selected]=\"tab === 'overview'\" (selectedChange)=\"tab = 'overview'\"></button>\n * <button kpTab label=\"Activity\" [selected]=\"tab === 'activity'\" (selectedChange)=\"tab = 'activity'\"></button>\n * <button kpTab label=\"Settings\" [selected]=\"tab === 'settings'\" (selectedChange)=\"tab = 'settings'\"></button>\n *\n * <button kpTabsMore>More ▾</button>\n * </kp-tabs>\n */\n@Component({\n selector: 'kp-tabs',\n imports: [],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class]': 'hostClasses',\n '(keydown)': 'onKeyDown($event)',\n },\n // role=\"tablist\" lives on the row that holds the projected <button kpTab> elements.\n // The host also carries an optional [kpTabsMore] sibling slot which is not a\n // tab, so role=tablist on the host would fail axe's aria-required-children.\n template: `\n <div class=\"kp-tabs__row\" role=\"tablist\"><ng-content/></div>\n <div class=\"kp-tabs__more\"><ng-content select=\"[kpTabsMore]\"/></div>\n `,\n styles: [`\n :host {\n position: relative;\n display: flex;\n align-items: stretch;\n border-bottom: 1px solid var(--kp-color-tabs-track-border);\n font-family: var(--kp-font-family-sans, 'Onest', system-ui, sans-serif);\n }\n\n .kp-tabs__row {\n display: flex;\n flex: 1 1 auto;\n /* The Tab atoms already carry their own border-bottom; no extra row chrome needed. */\n }\n :host(.kp-tabs--full-width) .kp-tabs__row > * { flex: 1 1 0; }\n\n .kp-tabs__more { display: inline-flex; flex: 0 0 auto; }\n .kp-tabs__more:empty { display: none; }\n `],\n})\nexport class KpTabsComponent implements AfterContentInit, OnDestroy {\n @Input() size: KpTabSize = 'md';\n @Input() fullWidth = false;\n\n @ContentChildren(KpTabComponent) tabs!: QueryList<KpTabComponent>;\n\n private readonly host = inject(ElementRef) as ElementRef<HTMLElement>;\n private readonly destroyed$ = new Subject<void>();\n\n ngAfterContentInit(): void {\n this.applyToTabs();\n this.tabs.changes.pipe(takeUntil(this.destroyed$)).subscribe(() => this.applyToTabs());\n }\n\n ngOnDestroy(): void {\n this.destroyed$.next();\n this.destroyed$.complete();\n }\n\n private applyToTabs(): void {\n this.tabs?.forEach((t) => {\n t.size = this.size;\n t.fullWidth = this.fullWidth;\n });\n }\n\n /**\n * WAI-ARIA tablist keyboard pattern:\n * ArrowLeft / ArrowRight — move to previous / next enabled tab (wraps around)\n * Home / End — jump to first / last enabled tab\n * Enter / Space — select the focused tab (native button handles this too)\n * Disabled tabs are skipped. Selection activates on focus change (automatic\n * activation) since our tab bodies are lightweight to render.\n */\n onKeyDown(event: KeyboardEvent): void {\n const all = this.tabs?.toArray() ?? [];\n const enabled = all.filter((t) => !t.disabled);\n if (!enabled.length) return;\n\n const active = this.activeTabIndex(enabled);\n let next: number | null = null;\n switch (event.key) {\n case 'ArrowRight': next = (active + 1) % enabled.length; break;\n case 'ArrowLeft': next = (active - 1 + enabled.length) % enabled.length; break;\n case 'Home': next = 0; break;\n case 'End': next = enabled.length - 1; break;\n default: return;\n }\n event.preventDefault();\n const target = enabled[next];\n // \"Consumer owns state\" — same pattern as click. Emit (selectedChange)\n // on the target tab and let the parent's [selected] binding propagate.\n if (!target.selected) target.selectedChange.emit(true);\n this.focusTab(target);\n }\n\n private activeTabIndex(enabled: KpTabComponent[]): number {\n const doc = this.host.nativeElement.ownerDocument;\n const focused = doc?.activeElement ?? null;\n const byFocus = enabled.findIndex((t) => t.elementRef.nativeElement.contains(focused));\n if (byFocus >= 0) return byFocus;\n const bySelection = enabled.findIndex((t) => t.selected);\n return bySelection >= 0 ? bySelection : 0;\n }\n\n private focusTab(tab: KpTabComponent): void {\n // The host element itself is the focusable target — it carries role=\"tab\"\n // and tabindex. The inner span is non-interactive (axe nested-interactive).\n tab.elementRef.nativeElement.focus();\n }\n\n get hostClasses(): string {\n const c = ['kp-tabs'];\n if (this.fullWidth) c.push('kp-tabs--full-width');\n return c.join(' ');\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kanso-protocol/tabs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"peerDependencies": {
|
|
6
6
|
"@angular/core": ">=21.0.0",
|
|
7
7
|
"@angular/common": ">=21.0.0",
|
|
8
|
-
"@kanso-protocol/core": ">=
|
|
8
|
+
"@kanso-protocol/core": ">=2.0.0"
|
|
9
9
|
},
|
|
10
10
|
"description": "Kanso Protocol — tabs (component).",
|
|
11
11
|
"author": "GregNBlack",
|
|
@@ -5,16 +5,16 @@ type KpTabSize = 'sm' | 'md' | 'lg';
|
|
|
5
5
|
/**
|
|
6
6
|
* Kanso Protocol — Tab (atom)
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Attribute-selector on a real native `<button role="tab">`. Sits inside
|
|
9
|
+
* a `<kp-tabs role="tablist">`. Native button gives Enter+Space
|
|
10
|
+
* activation, focus-visible, and disabled. Arrow-key navigation between
|
|
11
|
+
* tabs is owned by the parent `<kp-tabs>` via roving tabindex.
|
|
12
12
|
*
|
|
13
13
|
* @example
|
|
14
|
-
* <
|
|
14
|
+
* <button kpTab label="Inbox" [selected]="current === 'inbox'" (click)="current='inbox'">
|
|
15
15
|
* <svg kpTabIcon .../>
|
|
16
16
|
* <kp-badge kpTabBadge>12</kp-badge>
|
|
17
|
-
* </
|
|
17
|
+
* </button>
|
|
18
18
|
*/
|
|
19
19
|
declare class KpTabComponent {
|
|
20
20
|
size: KpTabSize;
|
|
@@ -24,21 +24,19 @@ declare class KpTabComponent {
|
|
|
24
24
|
/** Set by the parent `<kp-tabs>` when `fullWidth` is enabled on the container */
|
|
25
25
|
fullWidth: boolean;
|
|
26
26
|
selectedChange: EventEmitter<boolean>;
|
|
27
|
-
/** @internal —
|
|
28
|
-
* inner button as part of its arrow-key roving-tabindex navigation. */
|
|
27
|
+
/** @internal — parent `<kp-tabs>` focuses this for arrow-key roving-tabindex navigation. */
|
|
29
28
|
readonly elementRef: ElementRef<HTMLElement>;
|
|
30
29
|
get hostClasses(): string;
|
|
31
|
-
handleClick(
|
|
32
|
-
handleKey(event: KeyboardEvent): void;
|
|
30
|
+
handleClick(): void;
|
|
33
31
|
static ɵfac: i0.ɵɵFactoryDeclaration<KpTabComponent, never>;
|
|
34
|
-
static ɵcmp: i0.ɵɵComponentDeclaration<KpTabComponent, "
|
|
32
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<KpTabComponent, "button[kpTab]", never, { "size": { "alias": "size"; "required": false; }; "label": { "alias": "label"; "required": false; }; "selected": { "alias": "selected"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "fullWidth": { "alias": "fullWidth"; "required": false; }; }, { "selectedChange": "selectedChange"; }, never, ["[kpTabIcon]", "[kpTabBadge]"], true, never>;
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
/**
|
|
38
36
|
* Kanso Protocol — Tabs (container)
|
|
39
37
|
*
|
|
40
|
-
* Horizontal strip of `<
|
|
41
|
-
* Cascades `size` and `fullWidth` down to every projected `<
|
|
38
|
+
* Horizontal strip of `<button kpTab>` children with a shared bottom border.
|
|
39
|
+
* Cascades `size` and `fullWidth` down to every projected `<button kpTab>`
|
|
42
40
|
* automatically, so callers only set those two inputs in one place.
|
|
43
41
|
*
|
|
44
42
|
* A `[kpTabsMore]` slot projects an optional "More ▾" affordance to the
|
|
@@ -47,9 +45,9 @@ declare class KpTabComponent {
|
|
|
47
45
|
*
|
|
48
46
|
* @example
|
|
49
47
|
* <kp-tabs size="md" [fullWidth]="false">
|
|
50
|
-
* <
|
|
51
|
-
* <
|
|
52
|
-
* <
|
|
48
|
+
* <button kpTab label="Overview" [selected]="tab === 'overview'" (selectedChange)="tab = 'overview'"></button>
|
|
49
|
+
* <button kpTab label="Activity" [selected]="tab === 'activity'" (selectedChange)="tab = 'activity'"></button>
|
|
50
|
+
* <button kpTab label="Settings" [selected]="tab === 'settings'" (selectedChange)="tab = 'settings'"></button>
|
|
53
51
|
*
|
|
54
52
|
* <button kpTabsMore>More ▾</button>
|
|
55
53
|
* </kp-tabs>
|