Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "feat: make components ready for ssr support",
"packageName": "@fluentui/web-components",
"email": "[email protected]",
"dependentChangeType": "patch"
}
17 changes: 17 additions & 0 deletions packages/web-components/docs/web-components.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2310,12 +2310,16 @@ export class Dialog extends FASTElement {
ariaDescribedby?: string;
ariaLabelledby?: string;
clickHandler(event: Event): boolean;
// @internal (undocumented)
connectedCallback(): void;
dialog: HTMLDialogElement;
emitBeforeToggle: () => void;
emitToggle: () => void;
hide(): void;
show(): void;
type: DialogType;
// (undocumented)
protected typeChanged(prev: DialogType | undefined, next: DialogType | undefined): void;
}

// @public
Expand Down Expand Up @@ -2425,17 +2429,30 @@ export const DividerTemplate: ElementViewTemplate<Divider>;
export class Drawer extends FASTElement {
ariaDescribedby?: string;
ariaLabelledby?: string;
cancelHandler(): void;
// (undocumented)
clickHandler(event: Event): boolean;
// @internal (undocumented)
connectedCallback(): void;
dialog: HTMLDialogElement;
// @internal (undocumented)
disconnectedCallback(): void;
emitBeforeToggle: () => void;
emitToggle: () => void;
hide(): void;
// (undocumented)
protected observeRoleAttr(): void;
position: DrawerPosition;
// (undocumented)
protected roleAttrObserver: MutationObserver;
show(): void;
// (undocumented)
size: DrawerSize;
type: DrawerType;
// (undocumented)
protected typeChanged(): void;
// (undocumented)
protected updateDialogRole(): void;
}

// Warning: (ae-missing-release-tag) "DrawerBody" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down
3 changes: 0 additions & 3 deletions packages/web-components/src/dialog/dialog.template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,8 @@ import { DialogType } from './dialog.options.js';
*/
export const template: ElementViewTemplate<Dialog> = html`
<dialog
role="${x => (x.type === DialogType.alert ? 'alertdialog' : 'dialog')}"
type="${x => x.type}"
class="dialog"
part="dialog"
aria-modal="${x => (x.type === DialogType.modal || x.type === DialogType.alert ? 'true' : void 0)}"
aria-describedby="${x => x.ariaDescribedby}"
aria-labelledby="${x => x.ariaLabelledby}"
aria-label="${x => x.ariaLabel}"
Expand Down
23 changes: 23 additions & 0 deletions packages/web-components/src/dialog/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,29 @@ export class Dialog extends FASTElement {
*/
@attr
public type: DialogType = DialogType.modal;
protected typeChanged(prev: DialogType | undefined, next: DialogType | undefined) {
if (!this.dialog) {
return;
}

if (next === DialogType.alert) {
this.dialog.setAttribute('role', 'alertdialog');
} else {
this.dialog.removeAttribute('role');
}

if (next !== DialogType.nonModal) {
this.dialog.setAttribute('aria-modal', 'true');
} else {
this.dialog.removeAttribute('aria-modal');
}
}

/** @internal */
connectedCallback() {
super.connectedCallback();
this.typeChanged(undefined, this.type);
}

/**
* @public
Expand Down
5 changes: 1 addition & 4 deletions packages/web-components/src/drawer/drawer.template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,13 @@ export function drawerTemplate<T extends Drawer>(): ElementViewTemplate<T> {
<dialog
class="dialog"
part="dialog"
role="${x => (x.type === 'modal' ? 'dialog' : x.role)}"
aria-modal="${x => (x.type === 'modal' ? 'true' : void 0)}"
aria-describedby="${x => x.ariaDescribedby}"
aria-labelledby="${x => x.ariaLabelledby}"
aria-label="${x => x.ariaLabel}"
size="${x => x.size}"
position="${x => x.position}"
type="${x => x.type}"
@click="${(x, c) => x.clickHandler(c.event as MouseEvent)}"
@cancel="${(x, c) => x.hide()}"
@cancel="${x => x.cancelHandler()}"
${ref('dialog')}
>
<slot></slot>
Expand Down
59 changes: 59 additions & 0 deletions packages/web-components/src/drawer/drawer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { DrawerPosition, DrawerSize, DrawerType } from './drawer.options.js';
* @method show - Method to show the drawer.
* @method hide - Method to hide the drawer.
* @method clickHandler - Handles click events on the drawer.
* @method cancelHandler - Handles cancel events on the drawer.
* @method emitToggle - Emits an event after the dialog's open state changes.
* @method emitBeforeToggle - Emits an event before the dialog's open state changes.
*
Expand All @@ -32,13 +33,28 @@ import { DrawerPosition, DrawerSize, DrawerType } from './drawer.options.js';
* @tag fluent-drawer
*/
export class Drawer extends FASTElement {
protected roleAttrObserver!: MutationObserver;

/**
* @public
* Determines whether the drawer should be displayed as modal or non-modal
* When rendered as a modal, an overlay is applied over the rest of the view.
*/
@attr
public type: DrawerType = DrawerType.modal;
protected typeChanged() {
if (!this.dialog) {
return;
}

this.updateDialogRole();

if (this.type === DrawerType.modal) {
this.dialog.setAttribute('aria-modal', 'true');
} else {
this.dialog.removeAttribute('aria-modal');
}
}

/**
* @public
Expand Down Expand Up @@ -77,6 +93,19 @@ export class Drawer extends FASTElement {
@observable
public dialog!: HTMLDialogElement;

/** @internal */
connectedCallback() {
super.connectedCallback();
this.typeChanged();
this.observeRoleAttr();
}

/** @internal */
disconnectedCallback() {
super.disconnectedCallback();
this.roleAttrObserver.disconnect();
}

/**
* @public
* Method to emit an event after the dialog's open state changes
Expand Down Expand Up @@ -140,4 +169,34 @@ export class Drawer extends FASTElement {
}
return true;
}

/**
* @public
* Handles cancel events on the drawer.
*/
public cancelHandler() {
this.hide();
}

protected observeRoleAttr() {
if (this.roleAttrObserver) {
return;
}

this.roleAttrObserver = new MutationObserver(() => {
this.updateDialogRole();
});
this.roleAttrObserver.observe(this, {
attributes: true,
attributeFilter: ['role'],
});
}

protected updateDialogRole() {
console.log(this.role);
if (!this.dialog) {
return;
}
this.dialog.role = this.type === DrawerType.modal ? 'dialog' : this.role;
}
}
20 changes: 16 additions & 4 deletions packages/web-components/src/field/field.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { type SlottableInput, ValidationFlags } from './field.options.js';
* @public
*/
export class BaseField extends FASTElement {
private slottedInputObserver!: MutationObserver;

/**
* The slotted label elements.
*
Expand Down Expand Up @@ -67,9 +69,10 @@ export class BaseField extends FASTElement {
* @internal
*/
public slottedInputsChanged(prev: SlottableInput[] | undefined, next: SlottableInput[] | undefined) {
if (next?.length) {
this.input = next?.[0] as SlottableInput;
this.setStates();
const filtered = next?.filter(node => node.nodeType === Node.ELEMENT_NODE) ?? [];

if (filtered?.length) {
this.input = filtered?.[0] as SlottableInput;
}
}

Expand Down Expand Up @@ -98,6 +101,11 @@ export class BaseField extends FASTElement {
if (next) {
this.setStates();
this.setLabelProperties();
this.slottedInputObserver.observe(this.input, {
attributes: true,
attributeFilter: ['disabled', 'required', 'readonly'],
subtree: true,
});
}
}

Expand Down Expand Up @@ -136,9 +144,13 @@ export class BaseField extends FASTElement {
connectedCallback(): void {
super.connectedCallback();
this.addEventListener('invalid', this.invalidHandler, { capture: true });
this.slottedInputObserver = new MutationObserver(() => {
this.setStates();
});
}

disconnectedCallback(): void {
this.slottedInputObserver.disconnect();
this.removeEventListener('invalid', this.invalidHandler, { capture: true });
super.disconnectedCallback();
}
Expand Down Expand Up @@ -217,7 +229,7 @@ export class BaseField extends FASTElement {
}

public setValidationStates() {
if (!this.input.validity) {
if (!this.input?.validity) {
return;
}

Expand Down
10 changes: 1 addition & 9 deletions packages/web-components/src/field/field.template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,9 @@ export const template: ElementViewTemplate = html<Field>`
@change="${(x, c) => x.changeHandler(c.event as InputEvent)}"
@focusin="${(x, c) => x.focusinHandler(c.event as FocusEvent)}"
@focusout="${(x, c) => x.focusoutHandler(c.event as FocusEvent)}"
${children({
property: 'slottedInputs',
attributes: true,
attributeFilter: ['disabled', 'required', 'readonly'],
subtree: true,
selector: '[slot="input"]',
filter: elements(),
})}
>
<slot name="label" part="label" ${slotted('labelSlot')}></slot>
<slot name="input" part="input"></slot>
<slot name="input" part="input" ${slotted('slottedInputs')}></slot>
<slot name="message" part="message" ${slotted({ property: 'messageSlot', filter: elements('[flag]') })}></slot>
</template>
`;
1 change: 0 additions & 1 deletion packages/web-components/src/slider/slider.template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { Slider } from './slider.js';
export function sliderTemplate<T extends Slider>(options: SliderOptions = {}): ElementViewTemplate<T> {
return html<T>`
<template
tabindex="${x => (x.disabled ? null : 0)}"
@pointerdown="${(x, c) => x.handlePointerDown(c.event as PointerEvent)}"
@keydown="${(x, c) => x.handleKeydown(c.event as KeyboardEvent)}"
>
Expand Down
20 changes: 13 additions & 7 deletions packages/web-components/src/textarea/textarea.base.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { attr, FASTElement, nullableNumberConverter, observable } from '@microsoft/fast-element';
import { whitespaceFilter } from '../utils/index.js';
import type { Label } from '../label/label.js';
import { hasMatchingState, swapStates, toggleState } from '../utils/element-internals.js';
import { TextAreaAutocomplete, TextAreaResize } from './textarea.options.js';
Expand Down Expand Up @@ -62,17 +63,22 @@ export class BaseTextArea extends FASTElement {
this.value = next;
}

private filteredLabelSlottedNodes: Label[] = [];

/**
* The list of nodes that are assigned to the `label` slot.
* @internal
*/
@observable
public labelSlottedNodes!: Label[];
public labelSlottedNodes: Label[] = [];
protected labelSlottedNodesChanged() {
this.filteredLabelSlottedNodes = this.labelSlottedNodes.filter(whitespaceFilter);

if (this.labelEl) {
this.labelEl.hidden = !this.labelSlottedNodes.length;
this.labelEl.hidden = !this.filteredLabelSlottedNodes.length;
}
this.labelSlottedNodes.forEach(node => {

this.filteredLabelSlottedNodes.forEach(node => {
node.disabled = this.disabled;
node.required = this.required;
});
Expand Down Expand Up @@ -245,8 +251,8 @@ export class BaseTextArea extends FASTElement {
public required = false;
protected requiredChanged() {
this.elementInternals.ariaRequired = `${!!this.required}`;
if (this.labelSlottedNodes?.length) {
this.labelSlottedNodes.forEach(node => (node.required = this.required));
if (this.filteredLabelSlottedNodes?.length) {
this.filteredLabelSlottedNodes.forEach(node => (node.required = this.required));
}
}

Expand Down Expand Up @@ -552,8 +558,8 @@ export class BaseTextArea extends FASTElement {
this.controlEl.disabled = disabled;
}

if (this.labelSlottedNodes?.length) {
this.labelSlottedNodes.forEach(node => (node.disabled = this.disabled));
if (this.filteredLabelSlottedNodes?.length) {
this.filteredLabelSlottedNodes.forEach(node => (node.disabled = this.disabled));
}
}

Expand Down
16 changes: 2 additions & 14 deletions packages/web-components/src/textarea/textarea.template.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { type ElementViewTemplate, html, ref, slotted } from '@microsoft/fast-element';
import { whitespaceFilter } from '../utils/index.js';
import type { TextArea } from './textarea.js';

/**
Expand All @@ -11,13 +10,7 @@ export function textAreaTemplate<T extends TextArea>(): ElementViewTemplate<T> {
return html<T>`
<template>
<label ${ref('labelEl')} for="control" part="label">
<slot
name="label"
${slotted({
property: 'labelSlottedNodes',
filter: whitespaceFilter,
})}
></slot>
<slot name="label" ${slotted('labelSlottedNodes')}></slot>
</label>
<div class="root" part="root">
<textarea
Expand All @@ -39,12 +32,7 @@ export function textAreaTemplate<T extends TextArea>(): ElementViewTemplate<T> {
></textarea>
</div>
<div hidden>
<slot
${slotted({
property: 'defaultSlottedNodes',
filter: whitespaceFilter,
})}
></slot>
<slot ${slotted('defaultSlottedNodes')}></slot>
</div>
</template>
`;
Expand Down