Skip to content

Commit

Permalink
feat(toggle-group): rewrite to signals and host as Toggle
Browse files Browse the repository at this point in the history
  • Loading branch information
pimenovoleg committed Jan 7, 2025
1 parent f396126 commit 3fd4edb
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 145 deletions.
63 changes: 26 additions & 37 deletions packages/primitives/toggle-group/src/toggle-group-item.directive.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { booleanAttribute, Directive, input, Input, OnChanges, SimpleChanges } from '@angular/core';
import { BooleanInput } from '@angular/cdk/coercion';
import { booleanAttribute, computed, Directive, effect, inject, input } from '@angular/core';
import { RdxRovingFocusItemDirective } from '@radix-ng/primitives/roving-focus';
import { RdxToggleDirective } from '@radix-ng/primitives/toggle';
import { RdxToggleGroupItemToken } from './toggle-group-item.token';
import { injectToggleGroup } from './toggle-group.token';

Expand All @@ -12,27 +14,26 @@ import { injectToggleGroup } from './toggle-group.token';
{
directive: RdxRovingFocusItemDirective,
inputs: ['focusable', 'active', 'allowShiftKey']
},
{
directive: RdxToggleDirective,
inputs: ['pressed:isPressed', 'disabled']
}
],
host: {
role: 'radio',
'[attr.aria-checked]': 'checked',
'[attr.aria-disabled]': 'disabled || toggleGroup.disabled',
'[attr.aria-pressed]': 'undefined',

'[attr.data-disabled]': 'disabled || toggleGroup.disabled',
'[attr.data-state]': 'checked ? "on" : "off"',
'[attr.data-orientation]': 'toggleGroup.orientation',

'(click)': 'toggle()'
}
})
export class RdxToggleGroupItemDirective implements OnChanges {
export class RdxToggleGroupItemDirective {
private readonly rdxToggleDirective = inject(RdxToggleDirective);

private readonly rdxRovingFocusItemDirective = inject(RdxRovingFocusItemDirective);

/**
* Access the toggle group.
* @ignore
*/
protected readonly toggleGroup = injectToggleGroup();
protected readonly rootContext = injectToggleGroup();

/**
* The value of this toggle button.
Expand All @@ -43,41 +44,29 @@ export class RdxToggleGroupItemDirective implements OnChanges {
* Whether this toggle button is disabled.
* @default false
*/
@Input({ transform: booleanAttribute }) disabled = false;
readonly disabled = input<boolean, BooleanInput>(false, { transform: booleanAttribute });

/**
* Whether this toggle button is checked.
*/
protected get checked(): boolean {
return this.toggleGroup.isSelected(this.value());
}
readonly isPressed = computed(() => {
return this.rootContext.type() === 'single'
? this.rootContext.value() === this.value()
: this.rootContext.value()?.includes(this.value());
});

/**
* @ignore
*/
ngOnChanges(changes: SimpleChanges): void {
if ('disabled' in changes) {
// TODO
}
constructor() {
effect(() => {
this.rdxToggleDirective.pressed.set(!!this.isPressed());
this.rdxRovingFocusItemDirective.active = !!this.isPressed();
});
}

/**
* @ignore
*/
toggle(): void {
if (this.disabled) {
if (this.disabled()) {
return;
}

this.toggleGroup.toggle(this.value());
}

/**
* Ensure the disabled state is propagated to the roving focus item.
* @internal
* @ignore
*/
updateDisabled(): void {
// TODO
this.rootContext.toggle(this.value());
}
}
133 changes: 25 additions & 108 deletions packages/primitives/toggle-group/src/toggle-group.directive.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
import {
AfterContentInit,
booleanAttribute,
ContentChildren,
Directive,
Input,
OnChanges,
output,
QueryList,
SimpleChanges
} from '@angular/core';
import { BooleanInput } from '@angular/cdk/coercion';
import { booleanAttribute, Directive, input, model, output, signal } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { RdxRovingFocusGroupDirective } from '@radix-ng/primitives/roving-focus';
import type { RdxToggleGroupItemDirective } from './toggle-group-item.directive';
import { RdxToggleGroupItemToken } from './toggle-group-item.token';
import { RdxToggleGroupToken } from './toggle-group.token';

let nextId = 0;
Expand All @@ -28,135 +17,63 @@ let nextId = 0;
hostDirectives: [{ directive: RdxRovingFocusGroupDirective, inputs: ['dir', 'orientation', 'loop'] }],
host: {
role: 'group',
'[attr.data-orientation]': 'orientation',

'(focusout)': 'onTouched?.()'
}
})
export class RdxToggleGroupDirective implements OnChanges, AfterContentInit, ControlValueAccessor {
export class RdxToggleGroupDirective implements ControlValueAccessor {
readonly id: string = `rdx-toggle-group-${nextId++}`;

@Input()
set defaultValue(value: string[] | string) {
if (value !== this._defaultValue) {
this._defaultValue = Array.isArray(value) ? value : [value];
}
}
readonly value = model<string | string[] | undefined>(undefined);

get defaultValue(): string[] | string {
return this.isMultiple ? this._defaultValue : this._defaultValue[0];
}

/**
* The selected toggle button.
*/
@Input()
set value(value: string[] | string) {
if (value !== this._value) {
this._value = Array.isArray(value) ? value : [value];
}
}

get value(): string[] | string {
if (this._value === undefined) {
return this.defaultValue;
}

return this.isMultiple ? this._value : this._value[0];
}

@Input() type: 'single' | 'multiple' = 'single';

/**
* The orientation of the toggle group.
* @default 'horizontal'
*/
@Input() orientation: 'horizontal' | 'vertical' = 'horizontal';
readonly type = input<'single' | 'multiple'>('single');

/**
* Whether the toggle group is disabled.
* @default false
*/
@Input({ transform: booleanAttribute }) disabled = false;
readonly disabled = input<boolean, BooleanInput>(false, { transform: booleanAttribute });

/**
* Event emitted when the selected toggle button changes.
*/
readonly onValueChange = output<string[] | string | null>();

/**
* Access the buttons in the toggle group.
*/
@ContentChildren(RdxToggleGroupItemToken)
protected buttons?: QueryList<RdxToggleGroupItemDirective>;

private _value?: string[];
private _defaultValue: string[] | string = [];
readonly onValueChange = output<string[] | string | undefined>();

/**
* The value change callback.
*/
private onChange?: (value: string | string[] | null) => void;
private onChange?: (value: string | string[] | undefined) => void;

/**
* onTouch function registered via registerOnTouch (ControlValueAccessor).
*/
protected onTouched?: () => void;

get isMultiple(): boolean {
return this.type === 'multiple';
}

ngOnChanges(changes: SimpleChanges): void {
if ('disabled' in changes) {
this.buttons?.forEach((button) => button.updateDisabled());
}
}

ngAfterContentInit(): void {
if (this.disabled) {
this.buttons?.forEach((button) => button.updateDisabled());
}
}

/**
* Determine if a value is selected.
* @param value The value to check.
* @returns Whether the value is selected.
* @ignore
*/
isSelected(value: string): boolean {
if (typeof this.value === 'string') {
return this.value === value;
} else if (Array.isArray(this.value)) {
return this.value.includes(value);
}
return false;
}

/**
* Toggle a value.
* @param value The value to toggle.
* @ignore
*/
toggle(value: string): void {
if (this.disabled) {
if (this.disabled()) {
return;
}

if (Array.isArray(this.value)) {
const index = this.value.indexOf(value);
if (index > -1) {
this.value = this.value.filter((v) => v !== value);
} else {
this.value = [...this.value, value];
}
if (this.type() === 'single') {
this.value.set(value);
} else {
this.value = this.value === value ? '' : value;
this.value.set(
((currentValue) =>
currentValue && Array.isArray(currentValue)
? currentValue.includes(value)
? currentValue.filter((v) => v !== value) // delete
: [...currentValue, value] // update
: [value])(this.value())
);
}

this.onValueChange.emit(this.value);
this.onChange?.(this.value);
this.onValueChange.emit(this.value());
this.onChange?.(this.value());
}

/**
Expand All @@ -165,15 +82,15 @@ export class RdxToggleGroupDirective implements OnChanges, AfterContentInit, Con
* @ignore
*/
writeValue(value: string): void {
this.value = value;
this.value.set(value);
}

/**
* Register a callback to be called when the value changes.
* @param fn The callback to register.
* @ignore
*/
registerOnChange(fn: (value: string | string[] | null) => void): void {
registerOnChange(fn: (value: string | string[] | undefined) => void): void {
this.onChange = fn;
}

Expand All @@ -186,13 +103,13 @@ export class RdxToggleGroupDirective implements OnChanges, AfterContentInit, Con
this.onTouched = fn;
}

private readonly accessorDisabled = signal(false);
/**
* Set the disabled state of the toggle group.
* @param isDisabled Whether the toggle group is disabled.
* @ignore
*/
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
this.buttons?.forEach((button) => button.updateDisabled());
this.accessorDisabled.set(isDisabled);
}
}
31 changes: 31 additions & 0 deletions packages/primitives/toggle-group/stories/toggle-group.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export default {
justify-content: center;
margin-left: 1px;
}
.ToggleGroupItem[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
.ToggleGroupItem:first-child {
margin-left: 0;
border-top-left-radius: 4px;
Expand Down Expand Up @@ -117,3 +121,30 @@ export const Multiple: Story = {
`
})
};

export const Disable: Story = {
render: () => ({
props: {
selectedValues: ['center']
},
template: html`
<div
class="ToggleGroup"
rdxToggleGroup
type="multiple"
[value]="selectedValues"
aria-label="Text alignment"
>
<button class="ToggleGroupItem" disabled rdxToggleGroupItem value="left" aria-label="Left aligned">
<lucide-icon name="align-left" size="12"></lucide-icon>
</button>
<button class="ToggleGroupItem" rdxToggleGroupItem value="center" aria-label="Center aligned">
<lucide-icon name="align-center" size="12"></lucide-icon>
</button>
<button class="ToggleGroupItem" disabled rdxToggleGroupItem value="right" aria-label="Right aligned">
<lucide-icon name="align-right" size="12"></lucide-icon>
</button>
</div>
`
})
};
3 changes: 3 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@
"@radix-ng/primitives/tabs": [
"packages/primitives/tabs/index.ts"
],
"@radix-ng/primitives/toggle": [
"packages/primitives/toggle/index.ts"
],
"@radix-ng/primitives/tooltip": [
"packages/primitives/tooltip/index.ts"
],
Expand Down

0 comments on commit 3fd4edb

Please sign in to comment.