@softwarity/split-button 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 softwarity
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,216 @@
1
+ <p align="center">
2
+ <a href="https://www.softwarity.io/">
3
+ <img src="https://www.softwarity.io/img/softwarity.svg" alt="Softwarity" height="60">
4
+ </a>
5
+ </p>
6
+
7
+ # @softwarity/split-button
8
+
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/@softwarity/split-button">
11
+ <img src="https://img.shields.io/npm/v/@softwarity/split-button?color=blue&label=npm" alt="npm version">
12
+ </a>
13
+ <a href="https://github.com/softwarity/split-button/blob/main/LICENSE">
14
+ <img src="https://img.shields.io/badge/license-MIT-blue" alt="license">
15
+ </a>
16
+ <a href="https://github.com/softwarity/split-button/actions/workflows/main.yml">
17
+ <img src="https://github.com/softwarity/split-button/actions/workflows/main.yml/badge.svg" alt="build status">
18
+ </a>
19
+ </p>
20
+
21
+ An Angular directive that creates a [Material Design 3 split button](https://m3.material.io/components/buttons/overview) with a dropdown menu for secondary actions.
22
+
23
+ **[Live Demo](https://softwarity.github.io/split-button/)** | **[Release Notes](RELEASE_NOTES.md)**
24
+
25
+ <p align="center">
26
+ <a href="https://softwarity.github.io/split-button/">
27
+ <img src="projects/demo/src/assets/preview.png" alt="Split Button Preview" width="400">
28
+ </a>
29
+ </p>
30
+
31
+ ## Features
32
+
33
+ - **Material Design 3 Compliant** - Follows M3 button specifications
34
+ - **5 Button Variants** - Text, Filled, Tonal, Outlined, Elevated
35
+ - **Responsive to Theme** - Automatically adapts to light/dark color schemes
36
+ - **MatMenu Integration** - Works seamlessly with Angular Material's menu component
37
+ - **Material 3 Ready** - Uses M3 design tokens for theming (`--mat-sys-*`)
38
+ - **Standalone Directive** - Easy to import in any Angular 21+ application
39
+ - **Accessible** - Keyboard navigation and ARIA support
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ npm install @softwarity/split-button
45
+ ```
46
+
47
+ ### Peer Dependencies
48
+
49
+ | Package | Version |
50
+ |---------|---------|
51
+ | @angular/core | >= 21.0.0 |
52
+ | @angular/material | >= 21.0.0 |
53
+
54
+ ## Usage
55
+
56
+ ### 1. Import the directive in your component
57
+
58
+ ```typescript
59
+ import { SplitButtonDirective } from '@softwarity/split-button';
60
+ import { MatMenuModule } from '@angular/material/menu';
61
+
62
+ @Component({
63
+ selector: 'app-my-component',
64
+ imports: [SplitButtonDirective, MatMenuModule],
65
+ template: `...`
66
+ })
67
+ export class MyComponent {}
68
+ ```
69
+
70
+ ### 2. Add the `appSplitButton` directive to your button
71
+
72
+ ```html
73
+ <!-- Text button (default) -->
74
+ <button appSplitButton [appSplitButtonTrigger]="trigger" (click)="onSave()">
75
+ Save
76
+ </button>
77
+ <span [matMenuTriggerFor]="menu" #trigger="matMenuTrigger"></span>
78
+ <mat-menu #menu="matMenu">
79
+ <button mat-menu-item (click)="onSaveAs()">Save As...</button>
80
+ <button mat-menu-item (click)="onSaveDraft()">Save Draft</button>
81
+ </mat-menu>
82
+
83
+ <!-- Filled variant -->
84
+ <button appSplitButton="filled" [appSplitButtonTrigger]="trigger" (click)="onSubmit()">
85
+ Submit
86
+ </button>
87
+
88
+ <!-- Outlined variant -->
89
+ <button appSplitButton="outlined" [appSplitButtonTrigger]="trigger" (click)="onAction()">
90
+ Action
91
+ </button>
92
+
93
+ <!-- Tonal variant -->
94
+ <button appSplitButton="tonal" [appSplitButtonTrigger]="trigger" (click)="onProcess()">
95
+ Process
96
+ </button>
97
+
98
+ <!-- Elevated variant -->
99
+ <button appSplitButton="elevated" [appSplitButtonTrigger]="trigger" (click)="onExport()">
100
+ Export
101
+ </button>
102
+ ```
103
+
104
+ ## API
105
+
106
+ ### Inputs
107
+
108
+ | Input | Type | Default | Description |
109
+ |-------|------|---------|-------------|
110
+ | `appSplitButton` | `'' \| 'filled' \| 'tonal' \| 'outlined' \| 'elevated'` | `''` | Button variant following Material Design 3 guidelines |
111
+ | `appSplitButtonTrigger` | `MatMenuTrigger` | `undefined` | Reference to the MatMenuTrigger for the dropdown menu |
112
+ | `disabled` | `boolean` | `false` | Whether the button is disabled |
113
+
114
+ ### Button Variants
115
+
116
+ | Variant | Description |
117
+ |---------|-------------|
118
+ | Text (default) | Lowest emphasis, for less important actions |
119
+ | Filled | High emphasis, for primary actions |
120
+ | Tonal | Medium emphasis with a container color from the secondary palette |
121
+ | Outlined | Medium emphasis with a border outline |
122
+ | Elevated | Medium emphasis with a shadow elevation |
123
+
124
+ ## Theming (Optional)
125
+
126
+ The directive automatically injects its styles. If you want to customize the colors, you can use the optional SCSS mixin:
127
+
128
+ ```scss
129
+ @use '@softwarity/split-button/split-button-theme' as split-button;
130
+
131
+ // Customize split-button colors
132
+ @include split-button.overrides((
133
+ filled-container-color: #ff5722,
134
+ filled-label-color: #ffffff
135
+ ));
136
+ ```
137
+
138
+ ### Available Tokens
139
+
140
+ The `overrides` mixin accepts a map of tokens to customize the appearance:
141
+
142
+ | Token | Default | Description |
143
+ |-------|---------|-------------|
144
+ | `text-label-color` | `var(--mat-sys-primary)` | Label color for text variant |
145
+ | `filled-container-color` | `var(--mat-sys-primary)` | Container color for filled variant |
146
+ | `filled-label-color` | `var(--mat-sys-on-primary)` | Label color for filled variant |
147
+ | `outlined-outline-color` | `var(--mat-sys-outline)` | Border color for outlined variant |
148
+ | `outlined-label-color` | `var(--mat-sys-primary)` | Label color for outlined variant |
149
+ | `tonal-container-color` | `var(--mat-sys-secondary-container)` | Container color for tonal variant |
150
+ | `tonal-label-color` | `var(--mat-sys-on-secondary-container)` | Label color for tonal variant |
151
+ | `elevated-container-color` | `var(--mat-sys-surface-container-low)` | Container color for elevated variant |
152
+ | `elevated-label-color` | `var(--mat-sys-primary)` | Label color for elevated variant |
153
+
154
+ ### Examples
155
+
156
+ ```scss
157
+ // Customize filled button colors
158
+ @include split-button.overrides((
159
+ filled-container-color: light-dark(#6750a4, #d0bcff),
160
+ filled-label-color: light-dark(#ffffff, #381e72)
161
+ ));
162
+
163
+ // Use Material 3 system colors for tonal variant
164
+ @include split-button.overrides((
165
+ tonal-container-color: var(--mat-sys-tertiary-container),
166
+ tonal-label-color: var(--mat-sys-on-tertiary-container)
167
+ ));
168
+
169
+ // Custom brand colors
170
+ @include split-button.overrides((
171
+ filled-container-color: #ff5722,
172
+ filled-label-color: #ffffff
173
+ ));
174
+ ```
175
+
176
+ ## Examples
177
+
178
+ ### Save Action with Alternatives
179
+
180
+ ```html
181
+ <button appSplitButton="filled" [appSplitButtonTrigger]="saveTrigger" (click)="onSave()">
182
+ Save
183
+ </button>
184
+ <span [matMenuTriggerFor]="saveMenu" #saveTrigger="matMenuTrigger"></span>
185
+ <mat-menu #saveMenu="matMenu">
186
+ <button mat-menu-item (click)="onSaveAs()">Save As...</button>
187
+ <button mat-menu-item (click)="onSaveDraft()">Save Draft</button>
188
+ <button mat-menu-item (click)="onSaveAndClose()">Save & Close</button>
189
+ </mat-menu>
190
+ ```
191
+
192
+ ### Export with Format Options
193
+
194
+ ```html
195
+ <button appSplitButton="outlined" [appSplitButtonTrigger]="exportTrigger" (click)="onExportPDF()">
196
+ Export PDF
197
+ </button>
198
+ <span [matMenuTriggerFor]="exportMenu" #exportTrigger="matMenuTrigger"></span>
199
+ <mat-menu #exportMenu="matMenu">
200
+ <button mat-menu-item (click)="onExportCSV()">Export CSV</button>
201
+ <button mat-menu-item (click)="onExportXLSX()">Export Excel</button>
202
+ <button mat-menu-item (click)="onExportJSON()">Export JSON</button>
203
+ </mat-menu>
204
+ ```
205
+
206
+ ### Disabled State
207
+
208
+ ```html
209
+ <button appSplitButton="filled" [appSplitButtonTrigger]="trigger" [disabled]="true">
210
+ Disabled
211
+ </button>
212
+ ```
213
+
214
+ ## License
215
+
216
+ MIT
@@ -0,0 +1,85 @@
1
+ // =============================================================================
2
+ // Split Button Theme Customization
3
+ // Material Design 3 compliant theming for split-button directive
4
+ //
5
+ // Note: Base styles are automatically injected by the directive.
6
+ // This file is only needed if you want to customize colors.
7
+ // =============================================================================
8
+
9
+ /// Mixin to override split-button theme tokens
10
+ /// @param {Map} $tokens - Map of token overrides
11
+ /// @example
12
+ /// @use '@softwarity/split-button/split-button-theme' as split-button;
13
+ /// @include split-button.overrides((
14
+ /// filled-container-color: #ff5722,
15
+ /// filled-label-color: #ffffff
16
+ /// ));
17
+ ///
18
+ /// Available tokens:
19
+ /// - text-label-color: Label color for text variant
20
+ /// - filled-container-color: Container color for filled variant
21
+ /// - filled-label-color: Label color for filled variant
22
+ /// - outlined-outline-color: Border color for outlined variant
23
+ /// - outlined-label-color: Label color for outlined variant
24
+ /// - tonal-container-color: Container color for tonal variant
25
+ /// - tonal-label-color: Label color for tonal variant
26
+ /// - elevated-container-color: Container color for elevated variant
27
+ /// - elevated-label-color: Label color for elevated variant
28
+ /// - elevated-shadow: Shadow for elevated variant
29
+ /// - container-shape: Border radius
30
+ /// - disabled-opacity: Opacity when disabled
31
+ @mixin overrides($tokens: ()) {
32
+ @if length($tokens) > 0 {
33
+ :root {
34
+ // Text variant
35
+ @if map-has-key($tokens, text-label-color) {
36
+ --split-button-text-label-color: #{map-get($tokens, text-label-color)};
37
+ }
38
+
39
+ // Filled variant
40
+ @if map-has-key($tokens, filled-container-color) {
41
+ --split-button-filled-container-color: #{map-get($tokens, filled-container-color)};
42
+ }
43
+ @if map-has-key($tokens, filled-label-color) {
44
+ --split-button-filled-label-color: #{map-get($tokens, filled-label-color)};
45
+ }
46
+
47
+ // Outlined variant
48
+ @if map-has-key($tokens, outlined-outline-color) {
49
+ --split-button-outlined-outline-color: #{map-get($tokens, outlined-outline-color)};
50
+ }
51
+ @if map-has-key($tokens, outlined-label-color) {
52
+ --split-button-outlined-label-color: #{map-get($tokens, outlined-label-color)};
53
+ }
54
+
55
+ // Tonal variant
56
+ @if map-has-key($tokens, tonal-container-color) {
57
+ --split-button-tonal-container-color: #{map-get($tokens, tonal-container-color)};
58
+ }
59
+ @if map-has-key($tokens, tonal-label-color) {
60
+ --split-button-tonal-label-color: #{map-get($tokens, tonal-label-color)};
61
+ }
62
+
63
+ // Elevated variant
64
+ @if map-has-key($tokens, elevated-container-color) {
65
+ --split-button-elevated-container-color: #{map-get($tokens, elevated-container-color)};
66
+ }
67
+ @if map-has-key($tokens, elevated-label-color) {
68
+ --split-button-elevated-label-color: #{map-get($tokens, elevated-label-color)};
69
+ }
70
+ @if map-has-key($tokens, elevated-shadow) {
71
+ --split-button-elevated-shadow: #{map-get($tokens, elevated-shadow)};
72
+ }
73
+
74
+ // Shape
75
+ @if map-has-key($tokens, container-shape) {
76
+ --split-button-container-shape: #{map-get($tokens, container-shape)};
77
+ }
78
+
79
+ // Disabled
80
+ @if map-has-key($tokens, disabled-opacity) {
81
+ --split-button-disabled-opacity: #{map-get($tokens, disabled-opacity)};
82
+ }
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,352 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, ElementRef, Renderer2, booleanAttribute, HostBinding, Input, Directive } from '@angular/core';
3
+ import { DOCUMENT } from '@angular/common';
4
+
5
+ const VARIANT_CLASSES = ['split-button--filled', 'split-button--tonal', 'split-button--outlined', 'split-button--elevated'];
6
+ const STYLE_ID = 'split-button-styles';
7
+ /** Injects styles into the document head (only once) */
8
+ function injectStyles(doc) {
9
+ if (doc.getElementById(STYLE_ID))
10
+ return;
11
+ const style = doc.createElement('style');
12
+ style.id = STYLE_ID;
13
+ style.textContent = `
14
+ .split-button {
15
+ display: inline-flex;
16
+ align-items: stretch;
17
+ border-radius: var(--split-button-container-shape, var(--mdc-outlined-button-container-shape, 20px));
18
+ overflow: hidden;
19
+ vertical-align: middle;
20
+ height: 40px;
21
+ }
22
+ .split-button .split-button-main {
23
+ border: none;
24
+ background: transparent;
25
+ border-radius: 0;
26
+ border-top-left-radius: var(--split-button-container-shape, var(--mdc-outlined-button-container-shape, 20px));
27
+ border-bottom-left-radius: var(--split-button-container-shape, var(--mdc-outlined-button-container-shape, 20px));
28
+ min-width: unset;
29
+ padding: 0 16px 0 24px;
30
+ height: 40px;
31
+ font-family: var(--mat-sys-label-large-font);
32
+ font-size: var(--mat-sys-label-large-size, 14px);
33
+ font-weight: var(--mat-sys-label-large-weight, 500);
34
+ letter-spacing: var(--mat-sys-label-large-tracking, 0.1px);
35
+ cursor: pointer;
36
+ display: inline-flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ color: var(--split-button-text-label-color, var(--mdc-text-button-label-text-color, var(--mat-sys-primary)));
40
+ }
41
+ .split-button .split-button-main:hover {
42
+ background: color-mix(in srgb, var(--mat-sys-primary) 8%, transparent);
43
+ }
44
+ .split-button .split-button-chevron {
45
+ all: unset;
46
+ box-sizing: border-box;
47
+ border: none;
48
+ background: transparent;
49
+ border-radius: 0;
50
+ border-top-right-radius: var(--split-button-container-shape, var(--mdc-outlined-button-container-shape, 20px));
51
+ border-bottom-right-radius: var(--split-button-container-shape, var(--mdc-outlined-button-container-shape, 20px));
52
+ width: 40px;
53
+ min-width: 40px;
54
+ max-width: 40px;
55
+ padding: 0;
56
+ margin: 0;
57
+ cursor: pointer;
58
+ display: inline-flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ height: 40px;
62
+ flex-shrink: 0;
63
+ color: var(--split-button-text-label-color, var(--mdc-text-button-label-text-color, var(--mat-sys-primary)));
64
+ }
65
+ .split-button .split-button-chevron:hover {
66
+ background: color-mix(in srgb, currentColor 8%, transparent);
67
+ }
68
+ .split-button .split-button-chevron:focus-visible {
69
+ outline: 2px solid var(--mat-sys-primary);
70
+ outline-offset: -2px;
71
+ }
72
+ .split-button .split-button-icon {
73
+ width: 24px;
74
+ height: 24px;
75
+ display: block;
76
+ }
77
+ /* Outlined variant */
78
+ .split-button.split-button--outlined {
79
+ border: 1px solid var(--split-button-outlined-outline-color, var(--mdc-outlined-button-outline-color, var(--mat-sys-outline)));
80
+ }
81
+ .split-button.split-button--outlined .split-button-main {
82
+ color: var(--split-button-outlined-label-color, var(--mdc-outlined-button-label-text-color, var(--mat-sys-primary)));
83
+ }
84
+ .split-button.split-button--outlined .split-button-chevron {
85
+ border-left: 1px solid var(--split-button-outlined-outline-color, var(--mdc-outlined-button-outline-color, var(--mat-sys-outline)));
86
+ color: var(--split-button-outlined-label-color, var(--mdc-outlined-button-label-text-color, var(--mat-sys-primary)));
87
+ }
88
+ /* Elevated variant */
89
+ .split-button.split-button--elevated {
90
+ box-shadow: var(--split-button-elevated-shadow, var(--mdc-protected-button-container-elevation-shadow, 0 1px 2px 0 rgba(0,0,0,0.3), 0 1px 3px 1px rgba(0,0,0,0.15)));
91
+ background: var(--split-button-elevated-container-color, var(--mdc-protected-button-container-color, var(--mat-sys-surface-container-low)));
92
+ }
93
+ .split-button.split-button--elevated .split-button-main {
94
+ color: var(--split-button-elevated-label-color, var(--mdc-protected-button-label-text-color, var(--mat-sys-primary)));
95
+ position: relative;
96
+ }
97
+ .split-button.split-button--elevated .split-button-main::after {
98
+ content: '';
99
+ position: absolute;
100
+ right: 0;
101
+ top: 20%;
102
+ height: 60%;
103
+ width: 1px;
104
+ background: currentColor;
105
+ opacity: 0.2;
106
+ }
107
+ .split-button.split-button--elevated .split-button-chevron {
108
+ color: var(--split-button-elevated-label-color, var(--mdc-protected-button-label-text-color, var(--mat-sys-primary)));
109
+ }
110
+ /* Filled variant */
111
+ .split-button.split-button--filled {
112
+ background: var(--split-button-filled-container-color, var(--mdc-filled-button-container-color, var(--mat-sys-primary)));
113
+ }
114
+ .split-button.split-button--filled .split-button-main {
115
+ color: var(--split-button-filled-label-color, var(--mdc-filled-button-label-text-color, var(--mat-sys-on-primary)));
116
+ position: relative;
117
+ }
118
+ .split-button.split-button--filled .split-button-main::after {
119
+ content: '';
120
+ position: absolute;
121
+ right: 0;
122
+ top: 20%;
123
+ height: 60%;
124
+ width: 1px;
125
+ background: currentColor;
126
+ opacity: 0.2;
127
+ }
128
+ .split-button.split-button--filled .split-button-main:hover {
129
+ background: color-mix(in srgb, var(--mat-sys-on-primary) 8%, transparent);
130
+ }
131
+ .split-button.split-button--filled .split-button-chevron {
132
+ color: var(--split-button-filled-label-color, var(--mdc-filled-button-label-text-color, var(--mat-sys-on-primary)));
133
+ }
134
+ /* Tonal variant */
135
+ .split-button.split-button--tonal {
136
+ background: var(--split-button-tonal-container-color, var(--mat-sys-secondary-container));
137
+ }
138
+ .split-button.split-button--tonal .split-button-main {
139
+ color: var(--split-button-tonal-label-color, var(--mat-sys-on-secondary-container));
140
+ position: relative;
141
+ }
142
+ .split-button.split-button--tonal .split-button-main::after {
143
+ content: '';
144
+ position: absolute;
145
+ right: 0;
146
+ top: 20%;
147
+ height: 60%;
148
+ width: 1px;
149
+ background: currentColor;
150
+ opacity: 0.2;
151
+ }
152
+ .split-button.split-button--tonal .split-button-main:hover {
153
+ background: color-mix(in srgb, var(--mat-sys-on-secondary-container) 8%, transparent);
154
+ }
155
+ .split-button.split-button--tonal .split-button-chevron {
156
+ color: var(--split-button-tonal-label-color, var(--mat-sys-on-secondary-container));
157
+ }
158
+ /* Disabled state */
159
+ .split-button.split-button--disabled {
160
+ pointer-events: none;
161
+ opacity: var(--split-button-disabled-opacity, 0.38);
162
+ }
163
+ /* Hidden trigger utility - covers full height at right edge for proper menu alignment in both directions */
164
+ .split-button > .hidden-trigger {
165
+ position: absolute;
166
+ top: 0;
167
+ bottom: 0;
168
+ right: 0;
169
+ width: 1px;
170
+ height: 100%;
171
+ visibility: hidden;
172
+ pointer-events: none;
173
+ }
174
+ `;
175
+ doc.head.appendChild(style);
176
+ }
177
+ /**
178
+ * Split button directive that transforms a button into a split button with dropdown.
179
+ * Follows Material Design 3 guidelines.
180
+ *
181
+ * Usage:
182
+ * ```html
183
+ * <button appSplitButton [appSplitButtonTrigger]="trigger" (click)="doAction()">
184
+ * Text button (default)
185
+ * </button>
186
+ * <button appSplitButton="filled" [appSplitButtonTrigger]="trigger" (click)="doAction()">
187
+ * Filled button
188
+ * </button>
189
+ * <span [matMenuTriggerFor]="menu" #trigger="matMenuTrigger"></span>
190
+ * <mat-menu #menu="matMenu">
191
+ * <button mat-menu-item>Option 1</button>
192
+ * </mat-menu>
193
+ * ```
194
+ *
195
+ * M3 Button Variants:
196
+ * - (no value): Text button - lowest emphasis
197
+ * - filled: High emphasis
198
+ * - tonal: Medium emphasis with container color
199
+ * - outlined: Medium emphasis with border
200
+ * - elevated: Medium emphasis with shadow
201
+ */
202
+ class SplitButtonDirective {
203
+ constructor() {
204
+ this.el = inject(ElementRef);
205
+ this.renderer = inject(Renderer2);
206
+ this.document = inject(DOCUMENT);
207
+ /** M3 button variant - empty string or no value means text button (lowest emphasis) */
208
+ this.appSplitButton = '';
209
+ /** Whether the button is disabled */
210
+ this.disabled = false;
211
+ this.mainClass = true;
212
+ this.wrapper = null;
213
+ this.chevronButton = null;
214
+ this.clickListener = null;
215
+ this.initialized = false;
216
+ }
217
+ ngAfterViewInit() {
218
+ injectStyles(this.document);
219
+ setTimeout(() => {
220
+ this.createSplitButton();
221
+ this.initialized = true;
222
+ }, 0);
223
+ }
224
+ ngOnChanges(changes) {
225
+ if (!this.initialized || !this.wrapper)
226
+ return;
227
+ // Handle variant changes
228
+ if (changes['appSplitButton']) {
229
+ this.updateVariantClass();
230
+ }
231
+ // Handle disabled changes
232
+ if (changes['disabled']) {
233
+ this.updateDisabledState();
234
+ }
235
+ }
236
+ ngOnDestroy() {
237
+ this.clickListener?.();
238
+ if (this.wrapper && this.wrapper.parentNode) {
239
+ const host = this.el.nativeElement;
240
+ this.wrapper.parentNode.insertBefore(host, this.wrapper);
241
+ this.wrapper.parentNode.removeChild(this.wrapper);
242
+ }
243
+ }
244
+ updateVariantClass() {
245
+ if (!this.wrapper)
246
+ return;
247
+ // Remove all variant classes
248
+ VARIANT_CLASSES.forEach(cls => {
249
+ this.renderer.removeClass(this.wrapper, cls);
250
+ });
251
+ // Add new variant class if specified
252
+ if (this.appSplitButton) {
253
+ this.renderer.addClass(this.wrapper, `split-button--${this.appSplitButton}`);
254
+ }
255
+ }
256
+ updateDisabledState() {
257
+ if (!this.wrapper || !this.chevronButton)
258
+ return;
259
+ if (this.disabled) {
260
+ this.renderer.addClass(this.wrapper, 'split-button--disabled');
261
+ this.renderer.setAttribute(this.chevronButton, 'disabled', 'true');
262
+ }
263
+ else {
264
+ this.renderer.removeClass(this.wrapper, 'split-button--disabled');
265
+ this.renderer.removeAttribute(this.chevronButton, 'disabled');
266
+ }
267
+ }
268
+ createSplitButton() {
269
+ const host = this.el.nativeElement;
270
+ const parent = host.parentNode;
271
+ if (!parent)
272
+ return;
273
+ // Create wrapper
274
+ this.wrapper = this.renderer.createElement('div');
275
+ this.renderer.addClass(this.wrapper, 'split-button');
276
+ // Only add variant class if a variant is specified (otherwise it's text/default)
277
+ if (this.appSplitButton) {
278
+ this.renderer.addClass(this.wrapper, `split-button--${this.appSplitButton}`);
279
+ }
280
+ if (this.disabled) {
281
+ this.renderer.addClass(this.wrapper, 'split-button--disabled');
282
+ }
283
+ // Create chevron button
284
+ this.chevronButton = this.renderer.createElement('button');
285
+ this.renderer.setAttribute(this.chevronButton, 'type', 'button');
286
+ this.renderer.addClass(this.chevronButton, 'split-button-chevron');
287
+ if (this.disabled) {
288
+ this.renderer.setAttribute(this.chevronButton, 'disabled', 'true');
289
+ }
290
+ // Add chevron SVG icon using innerHTML for proper namespace handling
291
+ this.chevronButton.innerHTML = `
292
+ <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 -960 960 960" fill="currentColor" class="split-button-icon">
293
+ <path d="M480-360 280-560h400L480-360Z"/>
294
+ </svg>
295
+ `;
296
+ // Wrap the host element
297
+ this.renderer.insertBefore(parent, this.wrapper, host);
298
+ this.renderer.appendChild(this.wrapper, host);
299
+ this.renderer.appendChild(this.wrapper, this.chevronButton);
300
+ // Move the trigger element inside the wrapper for proper menu alignment (below the full button)
301
+ if (this.appSplitButtonTrigger) {
302
+ const triggerEl = this.appSplitButtonTrigger._element?.nativeElement;
303
+ if (triggerEl) {
304
+ this.renderer.setStyle(this.wrapper, 'position', 'relative');
305
+ this.renderer.appendChild(this.wrapper, triggerEl);
306
+ }
307
+ // Configure menu position so it aligns with the left edge of the split button
308
+ const menu = this.appSplitButtonTrigger.menu;
309
+ if (menu) {
310
+ menu.xPosition = 'before';
311
+ }
312
+ }
313
+ // Setup click handler for chevron - opens the menu via the trigger
314
+ this.clickListener = this.renderer.listen(this.chevronButton, 'click', (event) => {
315
+ event.preventDefault();
316
+ event.stopPropagation();
317
+ if (this.appSplitButtonTrigger && !this.disabled) {
318
+ this.appSplitButtonTrigger.openMenu();
319
+ }
320
+ });
321
+ }
322
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SplitButtonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
323
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "21.1.1", type: SplitButtonDirective, isStandalone: true, selector: "[appSplitButton]", inputs: { appSplitButton: "appSplitButton", appSplitButtonTrigger: "appSplitButtonTrigger", disabled: ["disabled", "disabled", booleanAttribute] }, host: { properties: { "class.split-button-main": "this.mainClass" } }, usesOnChanges: true, ngImport: i0 }); }
324
+ }
325
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SplitButtonDirective, decorators: [{
326
+ type: Directive,
327
+ args: [{
328
+ selector: '[appSplitButton]',
329
+ standalone: true
330
+ }]
331
+ }], propDecorators: { appSplitButton: [{
332
+ type: Input
333
+ }], appSplitButtonTrigger: [{
334
+ type: Input
335
+ }], disabled: [{
336
+ type: Input,
337
+ args: [{ transform: booleanAttribute }]
338
+ }], mainClass: [{
339
+ type: HostBinding,
340
+ args: ['class.split-button-main']
341
+ }] } });
342
+
343
+ /*
344
+ * Public API Surface of split-button
345
+ */
346
+
347
+ /**
348
+ * Generated bundle index. Do not edit.
349
+ */
350
+
351
+ export { SplitButtonDirective };
352
+ //# sourceMappingURL=softwarity-split-button.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"softwarity-split-button.mjs","sources":["../../src/lib/split-button.directive.ts","../../src/public-api.ts","../../src/softwarity-split-button.ts"],"sourcesContent":["import {\n Directive,\n ElementRef,\n Input,\n Renderer2,\n AfterViewInit,\n OnDestroy,\n OnChanges,\n SimpleChanges,\n inject,\n HostBinding,\n booleanAttribute\n} from '@angular/core';\nimport { DOCUMENT } from '@angular/common';\nimport { MatMenuTrigger } from '@angular/material/menu';\n\n/** M3 Button variant type */\nexport type SplitButtonVariant = '' | 'filled' | 'tonal' | 'outlined' | 'elevated';\n\nconst VARIANT_CLASSES = ['split-button--filled', 'split-button--tonal', 'split-button--outlined', 'split-button--elevated'];\n\nconst STYLE_ID = 'split-button-styles';\n\n/** Injects styles into the document head (only once) */\nfunction injectStyles(doc: Document): void {\n if (doc.getElementById(STYLE_ID)) return;\n\n const style = doc.createElement('style');\n style.id = STYLE_ID;\n style.textContent = `\n .split-button {\n display: inline-flex;\n align-items: stretch;\n border-radius: var(--split-button-container-shape, var(--mdc-outlined-button-container-shape, 20px));\n overflow: hidden;\n vertical-align: middle;\n height: 40px;\n }\n .split-button .split-button-main {\n border: none;\n background: transparent;\n border-radius: 0;\n border-top-left-radius: var(--split-button-container-shape, var(--mdc-outlined-button-container-shape, 20px));\n border-bottom-left-radius: var(--split-button-container-shape, var(--mdc-outlined-button-container-shape, 20px));\n min-width: unset;\n padding: 0 16px 0 24px;\n height: 40px;\n font-family: var(--mat-sys-label-large-font);\n font-size: var(--mat-sys-label-large-size, 14px);\n font-weight: var(--mat-sys-label-large-weight, 500);\n letter-spacing: var(--mat-sys-label-large-tracking, 0.1px);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--split-button-text-label-color, var(--mdc-text-button-label-text-color, var(--mat-sys-primary)));\n }\n .split-button .split-button-main:hover {\n background: color-mix(in srgb, var(--mat-sys-primary) 8%, transparent);\n }\n .split-button .split-button-chevron {\n all: unset;\n box-sizing: border-box;\n border: none;\n background: transparent;\n border-radius: 0;\n border-top-right-radius: var(--split-button-container-shape, var(--mdc-outlined-button-container-shape, 20px));\n border-bottom-right-radius: var(--split-button-container-shape, var(--mdc-outlined-button-container-shape, 20px));\n width: 40px;\n min-width: 40px;\n max-width: 40px;\n padding: 0;\n margin: 0;\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 40px;\n flex-shrink: 0;\n color: var(--split-button-text-label-color, var(--mdc-text-button-label-text-color, var(--mat-sys-primary)));\n }\n .split-button .split-button-chevron:hover {\n background: color-mix(in srgb, currentColor 8%, transparent);\n }\n .split-button .split-button-chevron:focus-visible {\n outline: 2px solid var(--mat-sys-primary);\n outline-offset: -2px;\n }\n .split-button .split-button-icon {\n width: 24px;\n height: 24px;\n display: block;\n }\n /* Outlined variant */\n .split-button.split-button--outlined {\n border: 1px solid var(--split-button-outlined-outline-color, var(--mdc-outlined-button-outline-color, var(--mat-sys-outline)));\n }\n .split-button.split-button--outlined .split-button-main {\n color: var(--split-button-outlined-label-color, var(--mdc-outlined-button-label-text-color, var(--mat-sys-primary)));\n }\n .split-button.split-button--outlined .split-button-chevron {\n border-left: 1px solid var(--split-button-outlined-outline-color, var(--mdc-outlined-button-outline-color, var(--mat-sys-outline)));\n color: var(--split-button-outlined-label-color, var(--mdc-outlined-button-label-text-color, var(--mat-sys-primary)));\n }\n /* Elevated variant */\n .split-button.split-button--elevated {\n box-shadow: var(--split-button-elevated-shadow, var(--mdc-protected-button-container-elevation-shadow, 0 1px 2px 0 rgba(0,0,0,0.3), 0 1px 3px 1px rgba(0,0,0,0.15)));\n background: var(--split-button-elevated-container-color, var(--mdc-protected-button-container-color, var(--mat-sys-surface-container-low)));\n }\n .split-button.split-button--elevated .split-button-main {\n color: var(--split-button-elevated-label-color, var(--mdc-protected-button-label-text-color, var(--mat-sys-primary)));\n position: relative;\n }\n .split-button.split-button--elevated .split-button-main::after {\n content: '';\n position: absolute;\n right: 0;\n top: 20%;\n height: 60%;\n width: 1px;\n background: currentColor;\n opacity: 0.2;\n }\n .split-button.split-button--elevated .split-button-chevron {\n color: var(--split-button-elevated-label-color, var(--mdc-protected-button-label-text-color, var(--mat-sys-primary)));\n }\n /* Filled variant */\n .split-button.split-button--filled {\n background: var(--split-button-filled-container-color, var(--mdc-filled-button-container-color, var(--mat-sys-primary)));\n }\n .split-button.split-button--filled .split-button-main {\n color: var(--split-button-filled-label-color, var(--mdc-filled-button-label-text-color, var(--mat-sys-on-primary)));\n position: relative;\n }\n .split-button.split-button--filled .split-button-main::after {\n content: '';\n position: absolute;\n right: 0;\n top: 20%;\n height: 60%;\n width: 1px;\n background: currentColor;\n opacity: 0.2;\n }\n .split-button.split-button--filled .split-button-main:hover {\n background: color-mix(in srgb, var(--mat-sys-on-primary) 8%, transparent);\n }\n .split-button.split-button--filled .split-button-chevron {\n color: var(--split-button-filled-label-color, var(--mdc-filled-button-label-text-color, var(--mat-sys-on-primary)));\n }\n /* Tonal variant */\n .split-button.split-button--tonal {\n background: var(--split-button-tonal-container-color, var(--mat-sys-secondary-container));\n }\n .split-button.split-button--tonal .split-button-main {\n color: var(--split-button-tonal-label-color, var(--mat-sys-on-secondary-container));\n position: relative;\n }\n .split-button.split-button--tonal .split-button-main::after {\n content: '';\n position: absolute;\n right: 0;\n top: 20%;\n height: 60%;\n width: 1px;\n background: currentColor;\n opacity: 0.2;\n }\n .split-button.split-button--tonal .split-button-main:hover {\n background: color-mix(in srgb, var(--mat-sys-on-secondary-container) 8%, transparent);\n }\n .split-button.split-button--tonal .split-button-chevron {\n color: var(--split-button-tonal-label-color, var(--mat-sys-on-secondary-container));\n }\n /* Disabled state */\n .split-button.split-button--disabled {\n pointer-events: none;\n opacity: var(--split-button-disabled-opacity, 0.38);\n }\n /* Hidden trigger utility - covers full height at right edge for proper menu alignment in both directions */\n .split-button > .hidden-trigger {\n position: absolute;\n top: 0;\n bottom: 0;\n right: 0;\n width: 1px;\n height: 100%;\n visibility: hidden;\n pointer-events: none;\n }\n `;\n doc.head.appendChild(style);\n}\n\n/**\n * Split button directive that transforms a button into a split button with dropdown.\n * Follows Material Design 3 guidelines.\n *\n * Usage:\n * ```html\n * <button appSplitButton [appSplitButtonTrigger]=\"trigger\" (click)=\"doAction()\">\n * Text button (default)\n * </button>\n * <button appSplitButton=\"filled\" [appSplitButtonTrigger]=\"trigger\" (click)=\"doAction()\">\n * Filled button\n * </button>\n * <span [matMenuTriggerFor]=\"menu\" #trigger=\"matMenuTrigger\"></span>\n * <mat-menu #menu=\"matMenu\">\n * <button mat-menu-item>Option 1</button>\n * </mat-menu>\n * ```\n *\n * M3 Button Variants:\n * - (no value): Text button - lowest emphasis\n * - filled: High emphasis\n * - tonal: Medium emphasis with container color\n * - outlined: Medium emphasis with border\n * - elevated: Medium emphasis with shadow\n */\n@Directive({\n selector: '[appSplitButton]',\n standalone: true\n})\nexport class SplitButtonDirective implements AfterViewInit, OnDestroy, OnChanges {\n private readonly el = inject(ElementRef);\n private readonly renderer = inject(Renderer2);\n private readonly document = inject(DOCUMENT);\n\n /** M3 button variant - empty string or no value means text button (lowest emphasis) */\n @Input() appSplitButton: SplitButtonVariant = '';\n\n /** MatMenuTrigger reference for the dropdown */\n @Input() appSplitButtonTrigger?: MatMenuTrigger;\n\n /** Whether the button is disabled */\n @Input({ transform: booleanAttribute }) disabled = false;\n\n @HostBinding('class.split-button-main') mainClass = true;\n\n private wrapper: HTMLElement | null = null;\n private chevronButton: HTMLButtonElement | null = null;\n private clickListener: (() => void) | null = null;\n private initialized = false;\n\n ngAfterViewInit(): void {\n injectStyles(this.document);\n setTimeout(() => {\n this.createSplitButton();\n this.initialized = true;\n }, 0);\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n if (!this.initialized || !this.wrapper) return;\n\n // Handle variant changes\n if (changes['appSplitButton']) {\n this.updateVariantClass();\n }\n\n // Handle disabled changes\n if (changes['disabled']) {\n this.updateDisabledState();\n }\n }\n\n ngOnDestroy(): void {\n this.clickListener?.();\n if (this.wrapper && this.wrapper.parentNode) {\n const host = this.el.nativeElement;\n this.wrapper.parentNode.insertBefore(host, this.wrapper);\n this.wrapper.parentNode.removeChild(this.wrapper);\n }\n }\n\n private updateVariantClass(): void {\n if (!this.wrapper) return;\n\n // Remove all variant classes\n VARIANT_CLASSES.forEach(cls => {\n this.renderer.removeClass(this.wrapper, cls);\n });\n\n // Add new variant class if specified\n if (this.appSplitButton) {\n this.renderer.addClass(this.wrapper, `split-button--${this.appSplitButton}`);\n }\n }\n\n private updateDisabledState(): void {\n if (!this.wrapper || !this.chevronButton) return;\n\n if (this.disabled) {\n this.renderer.addClass(this.wrapper, 'split-button--disabled');\n this.renderer.setAttribute(this.chevronButton, 'disabled', 'true');\n } else {\n this.renderer.removeClass(this.wrapper, 'split-button--disabled');\n this.renderer.removeAttribute(this.chevronButton, 'disabled');\n }\n }\n\n private createSplitButton(): void {\n const host = this.el.nativeElement as HTMLElement;\n const parent = host.parentNode;\n if (!parent) return;\n\n // Create wrapper\n this.wrapper = this.renderer.createElement('div');\n this.renderer.addClass(this.wrapper, 'split-button');\n\n // Only add variant class if a variant is specified (otherwise it's text/default)\n if (this.appSplitButton) {\n this.renderer.addClass(this.wrapper, `split-button--${this.appSplitButton}`);\n }\n\n if (this.disabled) {\n this.renderer.addClass(this.wrapper, 'split-button--disabled');\n }\n\n // Create chevron button\n this.chevronButton = this.renderer.createElement('button');\n this.renderer.setAttribute(this.chevronButton, 'type', 'button');\n this.renderer.addClass(this.chevronButton, 'split-button-chevron');\n\n if (this.disabled) {\n this.renderer.setAttribute(this.chevronButton, 'disabled', 'true');\n }\n\n // Add chevron SVG icon using innerHTML for proper namespace handling\n this.chevronButton!.innerHTML = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24\" width=\"24\" viewBox=\"0 -960 960 960\" fill=\"currentColor\" class=\"split-button-icon\">\n <path d=\"M480-360 280-560h400L480-360Z\"/>\n </svg>\n `;\n\n // Wrap the host element\n this.renderer.insertBefore(parent, this.wrapper, host);\n this.renderer.appendChild(this.wrapper, host);\n this.renderer.appendChild(this.wrapper, this.chevronButton);\n\n // Move the trigger element inside the wrapper for proper menu alignment (below the full button)\n if (this.appSplitButtonTrigger) {\n const triggerEl = (this.appSplitButtonTrigger as any)._element?.nativeElement;\n if (triggerEl) {\n this.renderer.setStyle(this.wrapper, 'position', 'relative');\n this.renderer.appendChild(this.wrapper, triggerEl);\n }\n // Configure menu position so it aligns with the left edge of the split button\n const menu = this.appSplitButtonTrigger.menu;\n if (menu) {\n menu.xPosition = 'before';\n }\n }\n\n // Setup click handler for chevron - opens the menu via the trigger\n this.clickListener = this.renderer.listen(this.chevronButton, 'click', (event: Event) => {\n event.preventDefault();\n event.stopPropagation();\n if (this.appSplitButtonTrigger && !this.disabled) {\n this.appSplitButtonTrigger.openMenu();\n }\n });\n }\n}\n","/*\n * Public API Surface of split-button\n */\n\nexport { SplitButtonDirective } from './lib/split-button.directive';\n\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@softwarity/split-button",
3
+ "version": "1.0.1",
4
+ "author": "Softwarity",
5
+ "license": "MIT",
6
+ "description": "Angular Material 3 split button directive with dropdown menu support",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/softwarity/split-button.git"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "keywords": [
15
+ "angular",
16
+ "material",
17
+ "split-button",
18
+ "button",
19
+ "dropdown",
20
+ "menu",
21
+ "material3",
22
+ "directive"
23
+ ],
24
+ "dependencies": {
25
+ "tslib": "^2.6.2"
26
+ },
27
+ "sideEffects": false,
28
+ "module": "fesm2022/softwarity-split-button.mjs",
29
+ "typings": "types/softwarity-split-button.d.ts",
30
+ "exports": {
31
+ "./package.json": {
32
+ "default": "./package.json"
33
+ },
34
+ ".": {
35
+ "types": "./types/softwarity-split-button.d.ts",
36
+ "default": "./fesm2022/softwarity-split-button.mjs"
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,58 @@
1
+ import * as i0 from '@angular/core';
2
+ import { AfterViewInit, OnDestroy, OnChanges, SimpleChanges } from '@angular/core';
3
+ import { MatMenuTrigger } from '@angular/material/menu';
4
+
5
+ /** M3 Button variant type */
6
+ type SplitButtonVariant = '' | 'filled' | 'tonal' | 'outlined' | 'elevated';
7
+ /**
8
+ * Split button directive that transforms a button into a split button with dropdown.
9
+ * Follows Material Design 3 guidelines.
10
+ *
11
+ * Usage:
12
+ * ```html
13
+ * <button appSplitButton [appSplitButtonTrigger]="trigger" (click)="doAction()">
14
+ * Text button (default)
15
+ * </button>
16
+ * <button appSplitButton="filled" [appSplitButtonTrigger]="trigger" (click)="doAction()">
17
+ * Filled button
18
+ * </button>
19
+ * <span [matMenuTriggerFor]="menu" #trigger="matMenuTrigger"></span>
20
+ * <mat-menu #menu="matMenu">
21
+ * <button mat-menu-item>Option 1</button>
22
+ * </mat-menu>
23
+ * ```
24
+ *
25
+ * M3 Button Variants:
26
+ * - (no value): Text button - lowest emphasis
27
+ * - filled: High emphasis
28
+ * - tonal: Medium emphasis with container color
29
+ * - outlined: Medium emphasis with border
30
+ * - elevated: Medium emphasis with shadow
31
+ */
32
+ declare class SplitButtonDirective implements AfterViewInit, OnDestroy, OnChanges {
33
+ private readonly el;
34
+ private readonly renderer;
35
+ private readonly document;
36
+ /** M3 button variant - empty string or no value means text button (lowest emphasis) */
37
+ appSplitButton: SplitButtonVariant;
38
+ /** MatMenuTrigger reference for the dropdown */
39
+ appSplitButtonTrigger?: MatMenuTrigger;
40
+ /** Whether the button is disabled */
41
+ disabled: boolean;
42
+ mainClass: boolean;
43
+ private wrapper;
44
+ private chevronButton;
45
+ private clickListener;
46
+ private initialized;
47
+ ngAfterViewInit(): void;
48
+ ngOnChanges(changes: SimpleChanges): void;
49
+ ngOnDestroy(): void;
50
+ private updateVariantClass;
51
+ private updateDisabledState;
52
+ private createSplitButton;
53
+ static ɵfac: i0.ɵɵFactoryDeclaration<SplitButtonDirective, never>;
54
+ static ɵdir: i0.ɵɵDirectiveDeclaration<SplitButtonDirective, "[appSplitButton]", never, { "appSplitButton": { "alias": "appSplitButton"; "required": false; }; "appSplitButtonTrigger": { "alias": "appSplitButtonTrigger"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; }, {}, never, never, true, never>;
55
+ static ngAcceptInputType_disabled: unknown;
56
+ }
57
+
58
+ export { SplitButtonDirective };