Skip to content

Commit

Permalink
feat(core): Appearance allow multiple modes (#9042)
Browse files Browse the repository at this point in the history
  • Loading branch information
waterplea authored Sep 16, 2024
1 parent c1c51c0 commit e1853dd
Show file tree
Hide file tree
Showing 27 changed files with 115 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
TuiAppearance,
tuiAppearance,
tuiAppearanceFocus,
tuiAppearanceMode,
tuiAppearanceState,
} from '@taiga-ui/core/directives/appearance';
import {
Expand Down Expand Up @@ -104,7 +105,6 @@ export interface TuiCard {
],
host: {
'data-size': 'l',
'[attr.data-mode]': 'mode()',
'(mousedown)': 'onMouseDown($event)',
},
})
Expand Down Expand Up @@ -144,6 +144,7 @@ export class TuiInputCardGroup
protected readonly texts = toSignal(inject(TUI_INPUT_CARD_GROUP_TEXTS));
protected readonly open = tuiDropdownOpen();

protected readonly m = tuiAppearanceMode(this.mode);
protected readonly appearance = tuiAppearance(
inject(TUI_TEXTFIELD_OPTIONS).appearance,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@
transform: translateY(-0.7em);
}

:host([data-mode='invalid']) & {
:host([data-mode~='invalid']) & {
color: var(--tui-text-negative);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const ATTR_WITH_VALUES_TO_REPLACE: ReplacementAttributeValue[] = [
},
{
attrNames: ['[pseudoInvalid]'],
newAttrName: '[attr.data-mode]',
newAttrName: '[tuiAppearanceMode]',
withTagNames: hasPseudoInvalid,
valueReplacer: (condition) => `${condition} ? 'invalid' : null`,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ export function migrateButtonAppearance({
startOffset + templateOffset,
` ${appearanceInputName}="whiteblock"`,
);
recorder.insertLeft(startOffset + templateOffset, ' data-mode="checked"');
recorder.insertLeft(
startOffset + templateOffset,
' tuiAppearanceMode="checked"',
);
}
});

Expand All @@ -87,6 +90,6 @@ export function migrateButtonAppearance({
function addTodo(recorder: UpdateRecorder, templateOffset: number): void {
recorder.insertRight(
templateOffset,
'<!-- Taiga migration TODO: tuiButton "whiteblock-active" appearance is no longer available. Use \'appearance="whiteblock" data-mode="checked"\' -->\n',
'<!-- Taiga migration TODO: tuiButton "whiteblock-active" appearance is no longer available. Use \'appearance="whiteblock" tuiAppearanceMode="checked"\' -->\n',
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const TEMPLATE_BEFORE = `
`.trim();

const TEMPLATE_AFTER = `
<label tuiBlock><input tuiCheckbox type="checkbox" [(ngModel)]="value" [attr.data-mode]="invalid ? 'invalid' : null" [tuiAppearanceFocus]="pseudoFocus">Content</label>
<label tuiBlock><input tuiCheckbox type="checkbox" [(ngModel)]="value" [tuiAppearanceMode]="invalid ? 'invalid' : null" [tuiAppearanceFocus]="pseudoFocus">Content</label>
<form [formGroup]="testForm">
<label tuiBlock> <input tuiRadio type="radio"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,18 @@ const TEMPLATE_WITH_CONDITION_BEFORE = `
const TEMPLATE_AFTER = `
<button tuiButton></button>
<button tuiButton appearance="whiteblock" data-mode="checked"></button>
<button tuiButton appearance="whiteblock" tuiAppearanceMode="checked"></button>
<button tuiButton appearance="whiteblock" data-mode="checked"></button>
<button tuiButton appearance="whiteblock" tuiAppearanceMode="checked"></button>
<a tuiIconButton appearance="whiteblock" data-mode="checked"></a>
<a tuiIconButton appearance="whiteblock" tuiAppearanceMode="checked"></a>
<a tuiButton [appearance]="
'flat'
"></a>
`;

const TEMPLATE_WITH_CONDITION_AFTER = `<!-- Taiga migration TODO: tuiButton "whiteblock-active" appearance is no longer available. Use 'appearance="whiteblock" data-mode="checked"' -->
const TEMPLATE_WITH_CONDITION_AFTER = `<!-- Taiga migration TODO: tuiButton "whiteblock-active" appearance is no longer available. Use 'appearance="whiteblock" tuiAppearanceMode="checked"' -->
<a tuiButton [appearance]="
true ? appearance : 'flat'
Expand Down
5 changes: 2 additions & 3 deletions projects/core/components/textfield/select.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {TuiTextfieldBase, TuiTextfieldDirective} from './textfield.directive';
hostDirectives: [TuiNativeValidator, TuiAppearance],
host: {
'[id]': 'textfield.id',
'[attr.data-mode]': 'mode',
'[class._empty]': 'value === ""',
'(input)': '0',
'(focusin)': '0',
Expand All @@ -33,14 +32,14 @@ import {TuiTextfieldBase, TuiTextfieldDirective} from './textfield.directive';
'(keydown.meta.c)': 'onCopy()',
},
})
export class TuiSelect extends TuiTextfieldBase {
export class TuiSelect<T> extends TuiTextfieldBase<T> {
private readonly nav = inject(WA_NAVIGATOR);
private readonly control = inject(NgControl);

@Input()
public placeholder = '';

public override setValue(value: string): void {
public override setValue(value: T): void {
this.control.control?.setValue(value);
this.el.dispatchEvent(new Event('input', {bubbles: true}));
}
Expand Down
4 changes: 2 additions & 2 deletions projects/core/components/textfield/textfield.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class TuiTextfieldComponent<T> implements TuiDataListHost<T> {
private readonly focusedIn = tuiFocusedIn(tuiInjectElement());

@ContentChild(forwardRef(() => TuiTextfieldDirective))
protected readonly directive?: TuiTextfieldDirective;
protected readonly directive?: TuiTextfieldDirective<T>;

@ContentChild(forwardRef(() => TuiLabel), {read: ElementRef})
protected readonly label?: ElementRef<HTMLElement>;
Expand Down Expand Up @@ -114,7 +114,7 @@ export class TuiTextfieldComponent<T> implements TuiDataListHost<T> {
}

public handleOption(option: T): void {
this.directive?.setValue(this.stringify(option));
this.directive?.setValue(option);
this.open.set(false);
}

Expand Down
21 changes: 14 additions & 7 deletions projects/core/components/textfield/textfield.directive.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {computed, Directive, inject, Input, signal} from '@angular/core';
import {computed, Directive, inject, Input, type OnChanges, signal} from '@angular/core';
import {TuiNativeValidator} from '@taiga-ui/cdk/directives/native-validator';
import {tuiInjectElement} from '@taiga-ui/cdk/utils/dom';
import {
TuiAppearance,
tuiAppearance,
tuiAppearanceFocus,
tuiAppearanceMode,
tuiAppearanceState,
} from '@taiga-ui/core/directives/appearance';
import type {TuiInteractiveState} from '@taiga-ui/core/types';
Expand All @@ -13,18 +14,20 @@ import {TuiTextfieldComponent} from './textfield.component';
import {TUI_TEXTFIELD_OPTIONS} from './textfield.options';

@Directive()
export class TuiTextfieldBase {
export class TuiTextfieldBase<T> implements OnChanges {
// TODO: refactor to signal inputs after Angular update
private readonly focused = signal<boolean | null>(null);

protected readonly a = tuiAppearance(inject(TUI_TEXTFIELD_OPTIONS).appearance);
protected readonly s = tuiAppearanceState(null);
protected readonly m = tuiAppearanceMode(this.mode);
protected readonly f = tuiAppearanceFocus(
computed(() => this.focused() || this.textfield.focused()),
);

protected readonly textfield = inject(TuiTextfieldComponent);
protected readonly el = tuiInjectElement<HTMLInputElement>();
protected readonly textfield: TuiTextfieldComponent<T> =
inject(TuiTextfieldComponent);

@Input()
public readOnly = false;
Expand Down Expand Up @@ -58,8 +61,13 @@ export class TuiTextfieldBase {
return null;
}

public setValue(value: string | null): void {
this.el.value = value || '';
// TODO: refactor to signal inputs after Angular update
public ngOnChanges(): void {
this.m.set(this.mode);
}

public setValue(value: T | null): void {
this.el.value = value == null ? '' : this.textfield.stringify(value);
this.el.dispatchEvent(new Event('input', {bubbles: true}));
}
}
Expand All @@ -72,10 +80,9 @@ export class TuiTextfieldBase {
'[id]': 'textfield.id',
'[readOnly]': 'readOnly',
'[class._empty]': 'el.value === ""',
'[attr.data-mode]': 'mode',
'(input)': '0',
'(focusin)': '0',
'(focusout)': '0',
},
})
export class TuiTextfieldDirective extends TuiTextfieldBase {}
export class TuiTextfieldDirective<T> extends TuiTextfieldBase<T> {}
8 changes: 8 additions & 0 deletions projects/core/directives/appearance/appearance.bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ type S = TuiInteractiveState | null;

type F = boolean | null;

type M = string | readonly string[] | null;

export function tuiAppearance(value: A | WritableSignal<A>): WritableSignal<A>;
export function tuiAppearance(value: Signal<A>): Signal<A>;
export function tuiAppearance(value: A | Signal<A>): Signal<A> {
Expand All @@ -27,3 +29,9 @@ export function tuiAppearanceFocus(value: Signal<F>): Signal<F>;
export function tuiAppearanceFocus(value: F | Signal<F>): Signal<F> {
return tuiDirectiveBinding(TuiAppearance, 'focus', value);
}

export function tuiAppearanceMode(value: M | WritableSignal<M>): WritableSignal<M>;
export function tuiAppearanceMode(value: Signal<M>): Signal<M>;
export function tuiAppearanceMode(value: M | Signal<M>): Signal<M> {
return tuiDirectiveBinding(TuiAppearance, 'mode', value);
}
13 changes: 12 additions & 1 deletion projects/core/directives/appearance/appearance.directive.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {
ChangeDetectionStrategy,
Component,
computed,
Directive,
inject,
Input,
signal,
ViewEncapsulation,
} from '@angular/core';
import {tuiWithStyles} from '@taiga-ui/cdk/utils/miscellaneous';
import {tuiIsString, tuiWithStyles} from '@taiga-ui/cdk/utils/miscellaneous';
import type {TuiInteractiveState} from '@taiga-ui/core/types';

import {TUI_APPEARANCE_OPTIONS, type TuiAppearanceOptions} from './appearance.options';
Expand All @@ -32,15 +33,20 @@ class TuiAppearanceStyles {}
'[attr.data-appearance]': 'appearance()',
'[attr.data-state]': 'state()',
'[attr.data-focus]': 'focus()',
'[attr.data-mode]': 'modes()',
},
})
export class TuiAppearance {
protected readonly nothing = tuiWithStyles(TuiAppearanceStyles);
protected readonly modes = computed((mode = this.mode()) =>
!mode || tuiIsString(mode) ? mode : mode.join(' '),
);

// TODO: refactor to signal inputs after Angular update
public readonly appearance = signal(inject(TUI_APPEARANCE_OPTIONS).appearance);
public readonly state = signal<TuiInteractiveState | null>(null);
public readonly focus = signal<boolean | null>(null);
public readonly mode = signal<string | readonly string[] | null>(null);

@Input()
public set tuiAppearance(appearance: TuiAppearanceOptions['appearance']) {
Expand All @@ -56,4 +62,9 @@ export class TuiAppearance {
public set tuiAppearanceFocus(focus: boolean | null) {
this.focus.set(focus);
}

@Input()
public set tuiAppearanceMode(mode: string | readonly string[] | null) {
this.mode.set(mode);
}
}
1 change: 1 addition & 0 deletions projects/core/directives/appearance/with-appearance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {TuiAppearance} from './appearance.directive';
'tuiAppearance: appearance',
'tuiAppearanceState',
'tuiAppearanceFocus',
'tuiAppearanceMode',
],
},
],
Expand Down
2 changes: 1 addition & 1 deletion projects/core/styles/components/appearance.less
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* data-appearance — current appearance
* data-state — manual interactive state override ('active' | 'disabled' | 'hover')
* data-focus — manual :focus-visible state override
* data-mode — arbitrary manual mode like 'checked', 'invalid' or 'readonly'
* data-mode — arbitrary manual mode like 'checked', 'invalid' or 'checked invalid'
*
* @example
* <button tuiAppearance data-appearance='primary'></button>
Expand Down
11 changes: 6 additions & 5 deletions projects/core/styles/components/group.less
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@
z-index: 0;
}

&:invalid,
&[data-mode='invalid'] {
&:invalid:not([data-mode]),
&[data-mode~='invalid'] {
z-index: 2;

--t-group-clip: none;
}

&:has(:invalid),
&:has([data-mode='invalid']) {
&:has(:invalid:not([data-mode])),
&:has([data-mode~='invalid']) {
z-index: 2;

--t-group-clip: none;
Expand All @@ -64,7 +64,8 @@
--t-group-clip: none;
}

&[data-mode='checked'] {
&:checked:not([data-mode]),
&[data-mode~='checked'] {
z-index: 4;

--t-group-clip: none;
Expand Down
10 changes: 5 additions & 5 deletions projects/core/styles/components/textfield.less
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ tui-textfield {
color: var(--tui-text-secondary);

&:has(input:read-only),
&:has(select[data-mode='readonly']) {
&:has(select[data-mode~='readonly']) {
color: var(--tui-text-tertiary);
}
}
Expand Down Expand Up @@ -219,17 +219,17 @@ tui-textfield {
transform: translateY(-0.7em);
}

&:not(:disabled)[data-mode='invalid'] ~ label,
&:not(:disabled)[data-mode~='invalid'] ~ label,
&:invalid:not(:disabled):not([data-mode]) ~ label {
color: var(--tui-text-negative);
}

&:not(:disabled):not([data-mode='readonly']) ~ .t-content .t-clear {
&:not(:disabled):not([data-mode~='readonly']) ~ .t-content .t-clear {
display: flex;
}
}

&:not([data-mode='readonly']) {
&:not([data-mode~='readonly']) {
.appearance-focus({
&::placeholder,
&._empty {
Expand Down Expand Up @@ -270,7 +270,7 @@ tui-textfield {
color: var(--tui-text-secondary);
}

select:not([data-mode='readonly']) {
select:not([data-mode~='readonly']) {
cursor: pointer;
}

Expand Down
11 changes: 6 additions & 5 deletions projects/core/styles/theme/appearance/outline.less
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
color: var(--tui-text-action);
box-shadow: inset 0 0 0 1px var(--t-bs);

&:checked,
&[data-mode='checked'] {
&:checked:not([data-mode]),
&[data-mode~='checked'] {
--t-bs: var(--tui-background-accent-1);

box-shadow: inset 0 0 0 2px var(--t-bs);
Expand All @@ -22,12 +22,13 @@
});
}

&:invalid {
&:invalid:not([data-mode]),
&[data-mode~='invalid'] {
box-shadow: inset 0 0 0 1px var(--tui-status-negative-pale-hover);
}

&:checked:invalid,
&[data-mode='checked']:invalid {
&:checked:invalid:not([data-mode]),
&[data-mode~='checked'][data-mode~='invalid'] {
box-shadow: inset 0 0 0 2px var(--tui-status-negative);
}

Expand Down
3 changes: 2 additions & 1 deletion projects/core/styles/theme/appearance/primary.less
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
background: var(--t-bg);
color: var(--tui-text-primary-on-accent-1);

&:invalid {
&:invalid:not([data-mode]),
&[data-mode~='invalid'] {
background: var(--tui-status-negative);
}

Expand Down
Loading

0 comments on commit e1853dd

Please sign in to comment.