@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 +21 -0
- package/README.md +216 -0
- package/_split-button-theme.scss +85 -0
- package/fesm2022/softwarity-split-button.mjs +352 -0
- package/fesm2022/softwarity-split-button.mjs.map +1 -0
- package/package.json +39 -0
- package/types/softwarity-split-button.d.ts +58 -0
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 };
|