Skip to content
Merged
3 changes: 3 additions & 0 deletions packages/components-dev/inline-edit/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import { DevThemeToggle } from '../theme-toggle';
<inline-edit-vertical-list-example />
<inline-edit-without-label-example />
<inline-edit-controls-example />
<inline-edit-select-basic-example />
<inline-edit-select-multiline-example />
<inline-edit-select-example />
<inline-edit-editable-header-example />
<inline-edit-customized-design-example />
<inline-edit-date-time-example />
Expand Down
23 changes: 22 additions & 1 deletion packages/components/core/form-field/form-field-ref.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InjectionToken, ModelSignal, Signal } from '@angular/core';
import { ElementRef, InjectionToken, ModelSignal, Signal } from '@angular/core';
import { NgControl } from '@angular/forms';
import { Observable } from 'rxjs';

Expand Down Expand Up @@ -59,3 +59,24 @@ export interface KbqFormFieldRef {
* @TODO move into form-field.ts, add correct type for `InjectionToken<KbqFormField>` (#DS-2915)
*/
export const KBQ_FORM_FIELD_REF = new InjectionToken<KbqFormFieldRef>('KbqFormFieldRef');

/**
* Contract for an ancestor that wants to override where overlays opened by a nested
* `KbqFormField`'s control (e.g. a select's dropdown, a datepicker's calendar) are
* anchored, instead of the form-field's own container.
*/
export interface KbqConnectedOverlayOriginProvider {
/**
* Element the overlay should be positioned and sized against, or `undefined` to fall
* back to the form-field's own container.
*/
getConnectedOverlayOrigin(): ElementRef | undefined;
}

/**
* Injection token for `KbqConnectedOverlayOriginProvider`. Provide it on an ancestor
* component to redirect where a nested `KbqFormField`'s control positions its overlay.
*/
export const KBQ_CONNECTED_OVERLAY_ORIGIN = new InjectionToken<KbqConnectedOverlayOriginProvider>(
'KbqConnectedOverlayOrigin'
);
7 changes: 5 additions & 2 deletions packages/components/form-field/form-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NgControl } from '@angular/forms';
import { KBQ_FORM_FIELD_REF, KbqColorDirective } from '@koobiq/components/core';
import { KBQ_CONNECTED_OVERLAY_ORIGIN, KBQ_FORM_FIELD_REF, KbqColorDirective } from '@koobiq/components/core';
import { EMPTY, merge } from 'rxjs';
import { delay, startWith } from 'rxjs/operators';
import { KbqCleaner } from './cleaner';
Expand Down Expand Up @@ -145,6 +145,7 @@ export class KbqFormField
private readonly changeDetectorRef = inject(ChangeDetectorRef);
private readonly focusMonitor = inject(FocusMonitor);
private readonly defaultOptions = inject(KBQ_FORM_FIELD_DEFAULT_OPTIONS, { optional: true });
private readonly customOverlayOrigin = inject(KBQ_CONNECTED_OVERLAY_ORIGIN, { optional: true });
/**
* @docs-private
*/
Expand Down Expand Up @@ -450,7 +451,9 @@ export class KbqFormField
* Gets an ElementRef for the element that a overlay attached to the form-field should be positioned relative to.
*/
getConnectedOverlayOrigin(): ElementRef {
return this.connectionContainerRef() || this.elementRef;
return (
this.customOverlayOrigin?.getConnectedOverlayOrigin() ?? this.connectionContainerRef() ?? this.elementRef
);
}

/**
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions packages/components/inline-edit/e2e.playwright-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,23 @@ test.describe('KbqInlineEdit', () => {
});
});

test.describe('E2eInlineEditSelectMultiline', () => {
const getContainer = (page: Page) => page.getByTestId('e2eInlineEditSelectMultilineContainer');
const getInlineEdit = (locator: Locator) => locator.getByTestId('e2eInlineEditSelectMultiline');

test('multiline select-style editor', async ({ page }) => {
await page.goto('/E2eInlineEditSelectMultiline');

const screenshotTarget = getContainer(page);

await getInlineEdit(screenshotTarget).click();

await expect(screenshotTarget).toHaveScreenshot('06-light.png');
await e2eEnableDarkTheme(page);
await expect(screenshotTarget).toHaveScreenshot('06-dark.png');
});
});

test.describe('E2eInlineEditMenuButton', () => {
const getComponent = (page: Page) => page.getByTestId('e2eInlineEditMenuButton');
const getContainer = (page: Page) => page.getByTestId('e2eInlineEditMenuButtonContainer');
Expand Down
75 changes: 74 additions & 1 deletion packages/components/inline-edit/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { NgTemplateOutlet } from '@angular/common';
import { ChangeDetectionStrategy, Component, viewChildren } from '@angular/core';
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { kbqInjectNativeElement } from '@koobiq/components/core';
import { kbqInjectNativeElement, KbqOptionModule } from '@koobiq/components/core';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqFormFieldModule, KbqLabel } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { KbqSelectModule } from '@koobiq/components/select';
import { KbqTextareaModule } from '@koobiq/components/textarea';
import { KbqInlineEdit } from './inline-edit';
import { KbqInlineEditModule } from './module';
Expand Down Expand Up @@ -251,3 +253,74 @@ export class E2eInlineEditMenuButton {}
export class E2eInlineEditActionButtons {
readonly control = new FormControl('Initial value', Validators.required);
}

const E2E_COMMENTS: string[] = [
'Issue resolved after restarting the affected service. No further action required.',
'Root cause identified as a misconfigured environment variable, fixed and redeployed.',
'Duplicate of an existing issue, closing without further changes.',
'Waiting on additional information from the reporter before further diagnosis.'
];

@Component({
selector: 'e2e-inline-edit-select-multiline',
imports: [
ReactiveFormsModule,
KbqInlineEditModule,
KbqFormFieldModule,
KbqLabel,
KbqOptionModule,
KbqSelectModule
],
template: `
<div style="width: 280px">
<kbq-inline-edit data-testid="e2eInlineEditSelectMultiline">
<kbq-label>Resolution comment</kbq-label>

<div kbqInlineEditViewMode>
@if (control.value) {
{{ control.value }}
} @else {
<span kbqInlineEditPlaceholder>{{ notSetLabel }}</span>
}
</div>

<kbq-form-field kbqInlineEditEditMode [noBorders]="true">
<kbq-select
panelWidth="auto"
[panelClass]="'e2e-inline-edit-select-multiline__options'"
[placeholder]="notSetLabel"
[formControl]="control"
>
<kbq-option [value]="null">{{ notSetLabel }}</kbq-option>
@for (comment of comments; track comment) {
<kbq-option [value]="comment">{{ comment }}</kbq-option>
}
</kbq-select>
</kbq-form-field>
</kbq-inline-edit>
</div>
`,
styles: `
:host {
display: block;
width: 350px;
height: 400px;
padding: 8px;
}

::ng-deep .e2e-inline-edit-select-multiline__options .kbq-option-text {
white-space: normal;
overflow-wrap: break-word;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
class: 'layout-margin-top-l',
'data-testid': 'e2eInlineEditSelectMultilineContainer'
}
})
export class E2eInlineEditSelectMultiline {
protected readonly notSetLabel = 'Not specified';
protected readonly comments = E2E_COMMENTS;
protected readonly control = new FormControl<string | null>(E2E_COMMENTS[0]);
}
24 changes: 24 additions & 0 deletions packages/components/inline-edit/inline-edit.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,30 @@ Inline-editable elements should remain aligned with their appearance in view mod

<!-- example(inline-edit-customized-design) -->

#### Select-style editor

On selection, open only the dropdown menu — don't show a bordered input field like Select's. The field doesn't need a border of its own, the dropdown alone is enough.

<!-- example(inline-edit-select-basic) -->

After picking an option, the field returns to view mode.

#### Multiline field

Sometimes it's worth making the dropdown options multiline. It's recommended to match the dropdown's width to the field's width.

<!-- example(inline-edit-select-multiline) -->

#### Reset

Place the clear element inside the dropdown menu — since the select field itself isn't visible, there's no other suitable place for a reset button.

Also recommended: add an option whose label makes it clear that no value is set. This echoes the wording used for the empty field in view mode.

<!-- example(inline-edit-select) -->

After picking an option, the field returns to view mode.

### Recommendations

Inline edits should be saved immediately after a change—without requiring a global **Save** button.
Expand Down
7 changes: 6 additions & 1 deletion packages/components/inline-edit/inline-edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@
(overlayKeydown)="onOverlayKeydown($event)"
>
<div class="cdk-visually-hidden" aria-hidden="true" kbqFocusRegionItem tabindex="0"></div>
<div class="kbq-inline-edit__panel" cdkTrapFocusAutoCapture [cdkTrapFocus]="false">
<div
class="kbq-inline-edit__panel"
cdkTrapFocusAutoCapture
[class.kbq-inline-edit__panel_select]="isSingleSelect()"
[cdkTrapFocus]="false"
>
<div
class="kbq-inline-edit__control-container"
[kbqTooltipColor]="colors.Error"
Expand Down
24 changes: 24 additions & 0 deletions packages/components/inline-edit/inline-edit.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,30 @@

<!-- example(inline-edit-customized-design) -->

#### Селект без рамки

При выборе открываем только выпадающее меню, но не показываем поле ввода, как у Select. Рамка поля ввода не требуется — достаточно выпадающего меню.

<!-- example(inline-edit-select-basic) -->

После выбора опции поле возвращается в режим просмотра.

#### Многострочное поле

Иногда стоит делать опции в выпадающем списке многострочными. Рекомендуется выравнивать ширину выпадающего меню по ширине поля.

<!-- example(inline-edit-select-multiline) -->

#### Сброс

Разместите элемент для очистки в выпадающем меню — ведь самого поля селекта не видно, поэтому другого подходящего места для кнопки сброса нет.

Рекомендуем также добавить опцию, название которой явно даёт понять, что значение не установлено. Это будет логично перекликаться с формулировкой пустого поля в режиме просмотра.

<!-- example(inline-edit-select) -->

После выбора опции поле возвращается в режим просмотра.

### Рекомендации

Поле с инлайн-редактированием должны сохраняться сразу после изменения без общей для всего экрана кнопки «Сохранить».
Expand Down
10 changes: 10 additions & 0 deletions packages/components/inline-edit/inline-edit.scss
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@
}
}

// Select-style editor: edit mode shows only the dropdown, not a bordered field, so the view
// text keeps showing through and just gets the "active" look instead.
&.kbq-inline-edit_select.kbq-inline-edit_edit {
--kbq-inline-edit-background: var(--kbq-states-background-transparent-active);
}

&:hover,
&.cdk-keyboard-focused,
&.kbq-inline-edit_anchor-focused {
Expand Down Expand Up @@ -187,6 +193,10 @@
padding: var(--kbq-size-xxs) var(--kbq-size-s) var(--kbq-size-s);
}

&.kbq-inline-edit__panel_select .kbq-inline-edit__control-container {
display: none;
}

.kbq-inline-edit__control-container,
.kbq-inline-edit__action-button {
box-shadow: var(--kbq-shadow-popup);
Expand Down
Loading