mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 2005: feat(shared-filter, ui-switch): add switch filter menu button for inline toggle filters
feat(shared-filter, ui-switch): add switch filter menu button for inline toggle filters
Add a new SwitchMenuButtonComponent that renders filter inputs as compact toggle switches
without an overlay menu. This provides a more streamlined UX for simple boolean/single-option
filters directly in the controls panel.
Key changes:
- Create new switch-menu module with button component and tests
- Extend FilterControlsPanelComponent to accept switchFilters input array
- Rename IconSwitchComponent to SwitchComponent for consistency
- Update filter actions to use 'target' property instead of 'group' for filtering
- Add isEmptyFilterInput support for NumberRange inputs
- Export switch-menu module from shared/filter public API
The switch button auto-commits on toggle and uses the checkbox filter model internally,
allowing simple configuration like:
switchFilters = [{ filter: stockFilter, icon: 'isaFiliale' }]
This implementation follows the existing filter architecture patterns and maintains
full accessibility support through ARIA attributes and keyboard navigation.
Ref: #5427
This commit is contained in:
committed by
Lorenz Hilpert
parent
a52928d212
commit
eb0d96698c
185
libs/ui/switch/README.md
Normal file
185
libs/ui/switch/README.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# UI Switch Library
|
||||
|
||||
This library provides a toggle switch component with an icon for Angular applications.
|
||||
|
||||
## Components
|
||||
|
||||
### IconSwitchComponent
|
||||
|
||||
A toggle switch component that displays an icon and supports two states (on/off).
|
||||
|
||||
**Important**: Use only when the icon meaning is universally clear, otherwise prefer a labeled switch.
|
||||
|
||||
#### Features
|
||||
|
||||
- ✅ Customizable icon via `ng-icons`
|
||||
- ✅ Two-way binding with model signal
|
||||
- ✅ Hover state styling
|
||||
- ✅ Disabled state handling
|
||||
- ✅ Keyboard navigation support (Enter, Space)
|
||||
- ✅ Fully accessible with ARIA attributes
|
||||
- ✅ Smooth animations
|
||||
|
||||
#### Usage
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
import { IconSwitchComponent } from '@isa/ui/switch';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaNavigationDashboard } from '@isa/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'app-example',
|
||||
standalone: true,
|
||||
imports: [IconSwitchComponent],
|
||||
providers: [provideIcons({ isaNavigationDashboard })],
|
||||
template: `
|
||||
<ui-icon-switch
|
||||
icon="isaNavigationDashboard"
|
||||
[(checked)]="isEnabled"
|
||||
[disabled]="false">
|
||||
</ui-icon-switch>
|
||||
`,
|
||||
})
|
||||
export class ExampleComponent {
|
||||
isEnabled = false;
|
||||
}
|
||||
```
|
||||
|
||||
#### Inputs
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
| ---------- | ---------------- | ----------- | ---------------------------------------------------- |
|
||||
| `icon` | `string` | (required) | The name of the icon to display |
|
||||
| `checked` | `boolean` | `false` | Two-way bindable signal for the checked state |
|
||||
| `color` | `IconSwitchColor`| `'primary'` | The color theme of the switch |
|
||||
| `disabled` | `boolean` | `false` | Whether the switch is disabled |
|
||||
| `tabIndex` | `number` | `0` | The tab index for keyboard navigation |
|
||||
|
||||
#### Two-Way Binding
|
||||
|
||||
The `checked` property uses Angular's new model signal syntax for two-way binding:
|
||||
|
||||
```html
|
||||
<!-- Two-way binding -->
|
||||
<ui-icon-switch icon="isaNavigationDashboard" [(checked)]="isEnabled"></ui-icon-switch>
|
||||
|
||||
<!-- One-way binding -->
|
||||
<ui-icon-switch icon="isaNavigationDashboard" [checked]="isEnabled"></ui-icon-switch>
|
||||
|
||||
<!-- Event handling -->
|
||||
<ui-icon-switch
|
||||
icon="isaNavigationDashboard"
|
||||
[checked]="isEnabled"
|
||||
(checkedChange)="onToggle($event)">
|
||||
</ui-icon-switch>
|
||||
```
|
||||
|
||||
#### Accessibility
|
||||
|
||||
The component follows WAI-ARIA best practices:
|
||||
|
||||
- `role="switch"` - Identifies the element as a switch
|
||||
- `aria-checked` - Indicates the current state
|
||||
- `aria-disabled` - Indicates when the switch is disabled
|
||||
- `tabindex` - Allows keyboard focus and navigation
|
||||
- Keyboard support:
|
||||
- `Enter` - Toggles the switch
|
||||
- `Space` - Toggles the switch
|
||||
|
||||
#### Styling
|
||||
|
||||
The component uses the following CSS classes:
|
||||
|
||||
- `.ui-icon-switch` - Main component class
|
||||
- `.ui-icon-switch__primary` - Primary color theme (currently the only supported theme)
|
||||
- `.ui-icon-switch__track` - The pill-shaped background track
|
||||
- `.ui-icon-switch__track--checked` - Applied when checked
|
||||
- `.ui-icon-switch__thumb` - The circular toggle indicator with icon
|
||||
- `.ui-icon-switch__thumb--checked` - Applied when checked
|
||||
- `.disabled` - Applied when disabled
|
||||
|
||||
#### Color Themes
|
||||
|
||||
Currently, only the `primary` theme is supported, which uses:
|
||||
|
||||
- **Unchecked**: Neutral gray background (`isa-neutral-300`)
|
||||
- **Checked**: Secondary blue background (`isa-secondary-600`)
|
||||
- **Hover (unchecked)**: Darker neutral gray (`isa-neutral-400`)
|
||||
- **Hover (checked)**: Darker secondary blue (`isa-secondary-700`)
|
||||
- **Thumb**: White background with icon color matching the state
|
||||
|
||||
#### Examples
|
||||
|
||||
**Basic usage:**
|
||||
|
||||
```html
|
||||
<ui-icon-switch
|
||||
icon="isaNavigationDashboard"
|
||||
[(checked)]="dashboardEnabled">
|
||||
</ui-icon-switch>
|
||||
```
|
||||
|
||||
**Disabled switch:**
|
||||
|
||||
```html
|
||||
<ui-icon-switch
|
||||
icon="isaNavigationDashboard"
|
||||
[(checked)]="isEnabled"
|
||||
[disabled]="true">
|
||||
</ui-icon-switch>
|
||||
```
|
||||
|
||||
**With event handling:**
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
template: `
|
||||
<ui-icon-switch
|
||||
icon="isaNavigationDashboard"
|
||||
[(checked)]="isEnabled"
|
||||
(checkedChange)="handleToggle($event)">
|
||||
</ui-icon-switch>
|
||||
`,
|
||||
})
|
||||
export class ExampleComponent {
|
||||
isEnabled = false;
|
||||
|
||||
handleToggle(checked: boolean): void {
|
||||
console.log('Switch toggled:', checked);
|
||||
// Perform action based on state
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Guidelines
|
||||
|
||||
**When to use Icon Switch:**
|
||||
|
||||
- ✅ The icon meaning is universally clear (e.g., home, dashboard, notification)
|
||||
- ✅ Space is limited
|
||||
- ✅ The action is binary (on/off, enabled/disabled)
|
||||
|
||||
**When NOT to use Icon Switch:**
|
||||
|
||||
- ❌ The icon meaning is ambiguous or context-specific
|
||||
- ❌ Multiple related switches need differentiation
|
||||
- ❌ Users need explicit labels for clarity
|
||||
|
||||
For cases where labels are needed, consider using a standard labeled switch component instead.
|
||||
|
||||
## Development
|
||||
|
||||
### Running unit tests
|
||||
|
||||
Run `nx test ui-switch` to execute the unit tests with Vitest.
|
||||
|
||||
### Running Storybook
|
||||
|
||||
The component has a Storybook story at `apps/isa-app/stories/ui/switch/ui-icon-switch.stories.ts`.
|
||||
|
||||
Run Storybook to see the component in action:
|
||||
|
||||
```bash
|
||||
npm run storybook
|
||||
```
|
||||
34
libs/ui/switch/eslint.config.cjs
Normal file
34
libs/ui/switch/eslint.config.cjs
Normal file
@@ -0,0 +1,34 @@
|
||||
const nx = require('@nx/eslint-plugin');
|
||||
const baseConfig = require('../../../eslint.config.js');
|
||||
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'lib',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'lib',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
20
libs/ui/switch/project.json
Normal file
20
libs/ui/switch/project.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "ui-switch",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/ui/switch/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/vite:test",
|
||||
"outputs": ["{options.reportsDirectory}"],
|
||||
"options": {
|
||||
"reportsDirectory": "../../../coverage/libs/ui/switch"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
2
libs/ui/switch/src/index.ts
Normal file
2
libs/ui/switch/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './lib/switch.component';
|
||||
export * from './lib/types';
|
||||
84
libs/ui/switch/src/lib/_switch.scss
Normal file
84
libs/ui/switch/src/lib/_switch.scss
Normal file
@@ -0,0 +1,84 @@
|
||||
.ui-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&.disabled {
|
||||
@apply cursor-default pointer-events-none opacity-50;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-switch__track {
|
||||
position: relative;
|
||||
width: 5.25rem;
|
||||
height: 3rem;
|
||||
border-radius: 1.5rem;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
@apply bg-isa-neutral-400;
|
||||
|
||||
&--checked {
|
||||
@apply bg-isa-secondary-400;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-switch__thumb {
|
||||
position: absolute;
|
||||
left: 0.375rem;
|
||||
width: 1.875rem;
|
||||
height: 1.875rem;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease-in-out;
|
||||
@apply bg-isa-white;
|
||||
top: 50%;
|
||||
transform: translate(0.1875rem, -50%);
|
||||
|
||||
&--checked {
|
||||
transform: translate(2.2rem, -50%);
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
.ui-switch:hover & {
|
||||
transform: translate(0.1875rem, -50%) scale(0.9);
|
||||
}
|
||||
|
||||
.ui-switch:hover &--checked {
|
||||
transform: translate(2.25rem, -50%) scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-switch__icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0.75rem;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
transition: color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.ui-switch__primary {
|
||||
.ui-switch__track {
|
||||
@apply bg-isa-neutral-400;
|
||||
|
||||
&--checked {
|
||||
@apply bg-isa-secondary-400;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-switch__icon {
|
||||
@apply text-isa-white;
|
||||
}
|
||||
|
||||
.ui-switch__track--checked .ui-switch__icon {
|
||||
@apply text-isa-secondary-400;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-switch:disabled,
|
||||
.ui-switch.disabled {
|
||||
@apply cursor-default;
|
||||
}
|
||||
9
libs/ui/switch/src/lib/switch.component.html
Normal file
9
libs/ui/switch/src/lib/switch.component.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<div class="ui-switch__track" [class.ui-switch__track--checked]="checked()">
|
||||
<div
|
||||
class="ui-switch__thumb"
|
||||
[class.ui-switch__thumb--checked]="checked()"
|
||||
></div>
|
||||
<div class="ui-switch__icon">
|
||||
<ng-icon [name]="icon()" size="1.5rem"></ng-icon>
|
||||
</div>
|
||||
</div>
|
||||
202
libs/ui/switch/src/lib/switch.component.spec.ts
Normal file
202
libs/ui/switch/src/lib/switch.component.spec.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { SwitchComponent } from './switch.component';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaNavigationDashboard } from '@isa/icons';
|
||||
|
||||
describe('SwitchComponent', () => {
|
||||
let component: SwitchComponent;
|
||||
let fixture: ComponentFixture<SwitchComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SwitchComponent],
|
||||
providers: [provideIcons({ isaNavigationDashboard })],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SwitchComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('initial state', () => {
|
||||
it('should have default values', () => {
|
||||
fixture.componentRef.setInput('icon', 'isaNavigationDashboard');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.checked()).toBe(false);
|
||||
expect(component.color()).toBe('primary');
|
||||
expect(component.disabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should render with unchecked state', () => {
|
||||
fixture.componentRef.setInput('icon', 'isaNavigationDashboard');
|
||||
fixture.detectChanges();
|
||||
|
||||
const track = fixture.nativeElement.querySelector('.ui-switch__track');
|
||||
expect(track).toBeTruthy();
|
||||
expect(track.classList.contains('ui-switch__track--checked')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checked state', () => {
|
||||
it('should update when checked input changes', () => {
|
||||
fixture.componentRef.setInput('icon', 'isaNavigationDashboard');
|
||||
fixture.componentRef.setInput('checked', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.checked()).toBe(true);
|
||||
});
|
||||
|
||||
it('should apply checked class to track', () => {
|
||||
fixture.componentRef.setInput('icon', 'isaNavigationDashboard');
|
||||
fixture.componentRef.setInput('checked', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
const track = fixture.nativeElement.querySelector('.ui-switch__track');
|
||||
expect(track.classList.contains('ui-switch__track--checked')).toBe(true);
|
||||
});
|
||||
|
||||
it('should apply checked class to thumb', () => {
|
||||
fixture.componentRef.setInput('icon', 'isaNavigationDashboard');
|
||||
fixture.componentRef.setInput('checked', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
const thumb = fixture.nativeElement.querySelector('.ui-switch__thumb');
|
||||
expect(thumb.classList.contains('ui-switch__thumb--checked')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggle functionality', () => {
|
||||
beforeEach(() => {
|
||||
fixture.componentRef.setInput('icon', 'isaNavigationDashboard');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should toggle checked state on click', () => {
|
||||
expect(component.checked()).toBe(false);
|
||||
|
||||
fixture.nativeElement.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.checked()).toBe(true);
|
||||
|
||||
fixture.nativeElement.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.checked()).toBe(false);
|
||||
});
|
||||
|
||||
it('should toggle on Enter key', () => {
|
||||
expect(component.checked()).toBe(false);
|
||||
|
||||
const event = new KeyboardEvent('keydown', { key: 'Enter' });
|
||||
fixture.nativeElement.dispatchEvent(event);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.checked()).toBe(true);
|
||||
});
|
||||
|
||||
it('should toggle on Space key', () => {
|
||||
expect(component.checked()).toBe(false);
|
||||
|
||||
const event = new KeyboardEvent('keydown', { key: ' ' });
|
||||
fixture.nativeElement.dispatchEvent(event);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.checked()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disabled state', () => {
|
||||
beforeEach(() => {
|
||||
fixture.componentRef.setInput('icon', 'isaNavigationDashboard');
|
||||
fixture.componentRef.setInput('disabled', true);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not toggle when disabled and clicked', () => {
|
||||
expect(component.checked()).toBe(false);
|
||||
|
||||
fixture.nativeElement.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.checked()).toBe(false);
|
||||
});
|
||||
|
||||
it('should apply disabled class', () => {
|
||||
expect(fixture.nativeElement.classList.contains('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('should have aria-disabled attribute', () => {
|
||||
expect(fixture.nativeElement.getAttribute('aria-disabled')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('accessibility', () => {
|
||||
beforeEach(() => {
|
||||
fixture.componentRef.setInput('icon', 'isaNavigationDashboard');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should have role="switch"', () => {
|
||||
expect(fixture.nativeElement.getAttribute('role')).toBe('switch');
|
||||
});
|
||||
|
||||
it('should have correct aria-checked when unchecked', () => {
|
||||
expect(fixture.nativeElement.getAttribute('aria-checked')).toBe('false');
|
||||
});
|
||||
|
||||
it('should have correct aria-checked when checked', () => {
|
||||
fixture.componentRef.setInput('checked', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.getAttribute('aria-checked')).toBe('true');
|
||||
});
|
||||
|
||||
it('should be keyboard focusable', () => {
|
||||
expect(fixture.nativeElement.getAttribute('tabindex')).toBe('0');
|
||||
});
|
||||
|
||||
it('should support custom tabindex', () => {
|
||||
fixture.componentRef.setInput('tabIndex', 5);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.getAttribute('tabindex')).toBe('5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('color theme', () => {
|
||||
beforeEach(() => {
|
||||
fixture.componentRef.setInput('icon', 'isaNavigationDashboard');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should apply primary color class by default', () => {
|
||||
expect(
|
||||
fixture.nativeElement.classList.contains('ui-switch__primary'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should apply correct color class', () => {
|
||||
fixture.componentRef.setInput('color', 'primary');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
fixture.nativeElement.classList.contains('ui-switch__primary'),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('icon rendering', () => {
|
||||
it('should render the provided icon', () => {
|
||||
fixture.componentRef.setInput('icon', 'isaNavigationDashboard');
|
||||
fixture.detectChanges();
|
||||
|
||||
const icon = fixture.nativeElement.querySelector('ng-icon');
|
||||
expect(icon).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
102
libs/ui/switch/src/lib/switch.component.ts
Normal file
102
libs/ui/switch/src/lib/switch.component.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
model,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { IconSwitchColor } from './types';
|
||||
import { IsaIcons } from '@isa/icons';
|
||||
|
||||
/**
|
||||
* A toggle switch component that displays an icon and supports two states (on/off).
|
||||
* Use only when the icon meaning is universally clear, otherwise prefer a labeled switch.
|
||||
*
|
||||
* The component uses reactive signals to manage its state and provides a visually
|
||||
* appealing toggle animation between enabled and disabled states.
|
||||
*
|
||||
* Features:
|
||||
* - Customizable icon
|
||||
* - Two-way binding with model signal
|
||||
* - Hover state styling
|
||||
* - Disabled state handling
|
||||
* - Keyboard navigation support
|
||||
* - Accessible with ARIA attributes
|
||||
*
|
||||
* @property icon - The name of the icon to display in the switch
|
||||
* @property checked - A two-way bindable signal indicating whether the switch is on (true) or off (false)
|
||||
* @property color - The color theme of the switch (currently only 'primary' is supported)
|
||||
* @property colorClass - A computed CSS class based on the current color
|
||||
* @property disabled - A boolean flag indicating whether the switch is disabled
|
||||
* @property disabledClass - A computed CSS class that adds a 'disabled' style when the switch is disabled
|
||||
* @property tabIndex - The tab index for keyboard navigation
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <ui-switch
|
||||
* icon="isaHome"
|
||||
* [(checked)]="isEnabled"
|
||||
* [disabled]="false">
|
||||
* </ui-switch>
|
||||
* ```
|
||||
*
|
||||
* @remarks
|
||||
* - The switch uses a pill-shaped design with a circular toggle indicator
|
||||
* - The icon is always visible and moves with the toggle indicator
|
||||
* - Click and Enter/Space key events toggle the switch state
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ui-switch',
|
||||
templateUrl: './switch.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: true,
|
||||
imports: [NgIconComponent],
|
||||
host: {
|
||||
'[class]': '["ui-switch", colorClass(), disabledClass()]',
|
||||
'[tabindex]': 'tabIndex()',
|
||||
'[attr.role]': '"switch"',
|
||||
'[attr.aria-checked]': 'checked()',
|
||||
'[attr.aria-disabled]': 'disabled()',
|
||||
'(click)': 'toggle()',
|
||||
'(keydown.enter)': 'toggle()',
|
||||
'(keydown.space)': 'toggle(); $event.preventDefault()',
|
||||
},
|
||||
providers: [provideIcons(IsaIcons)],
|
||||
})
|
||||
export class SwitchComponent {
|
||||
/** The name of the icon to display */
|
||||
icon = input.required<string>();
|
||||
|
||||
/** Two-way bindable signal for the checked state */
|
||||
checked = model<boolean>(false);
|
||||
|
||||
/** The color theme of the switch */
|
||||
color = input<IconSwitchColor>('primary');
|
||||
|
||||
/** Computed class based on the current color */
|
||||
colorClass = computed(() => `ui-switch__${this.color()}`);
|
||||
|
||||
/** Whether the switch is disabled */
|
||||
disabled = input<boolean>(false);
|
||||
|
||||
/** Computed CSS class for the disabled state */
|
||||
disabledClass = computed(() => (this.disabled() ? 'disabled' : ''));
|
||||
|
||||
/** The tab index for keyboard navigation */
|
||||
tabIndex = input<number>(0);
|
||||
|
||||
/** Computed class for the checked state */
|
||||
checkedClass = computed(() => (this.checked() ? 'ui-switch__checked' : ''));
|
||||
|
||||
/**
|
||||
* Toggles the switch state unless it's disabled
|
||||
*/
|
||||
toggle(): void {
|
||||
if (!this.disabled()) {
|
||||
this.checked.set(!this.checked());
|
||||
}
|
||||
}
|
||||
}
|
||||
6
libs/ui/switch/src/lib/types.ts
Normal file
6
libs/ui/switch/src/lib/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const IconSwitchColor = {
|
||||
Primary: 'primary',
|
||||
} as const;
|
||||
|
||||
export type IconSwitchColor =
|
||||
(typeof IconSwitchColor)[keyof typeof IconSwitchColor];
|
||||
1
libs/ui/switch/src/switch.scss
Normal file
1
libs/ui/switch/src/switch.scss
Normal file
@@ -0,0 +1 @@
|
||||
@import "lib/switch";
|
||||
13
libs/ui/switch/src/test-setup.ts
Normal file
13
libs/ui/switch/src/test-setup.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import '@angular/compiler';
|
||||
import '@analogjs/vitest-angular/setup-zone';
|
||||
|
||||
import {
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting,
|
||||
} from '@angular/platform-browser/testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting(),
|
||||
);
|
||||
30
libs/ui/switch/tsconfig.json
Normal file
30
libs/ui/switch/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
libs/ui/switch/tsconfig.lib.json
Normal file
27
libs/ui/switch/tsconfig.lib.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
29
libs/ui/switch/tsconfig.spec.json
Normal file
29
libs/ui/switch/tsconfig.spec.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"files": ["src/test-setup.ts"]
|
||||
}
|
||||
27
libs/ui/switch/vite.config.mts
Normal file
27
libs/ui/switch/vite.config.mts
Normal file
@@ -0,0 +1,27 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import angular from '@analogjs/vite-plugin-angular';
|
||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../../node_modules/.vite/libs/ui/switch',
|
||||
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
// },
|
||||
test: {
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
setupFiles: ['src/test-setup.ts'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
reportsDirectory: '../../../coverage/libs/ui/switch',
|
||||
provider: 'v8' as const,
|
||||
},
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user