Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7a8aaa8
feat(floating-button): initial commit - create floating-button compon…
artur-langl Nov 11, 2025
d9d6a07
feat(floating-button): prepare storybook + move styles to button.comp…
artur-langl Nov 12, 2025
bb12938
feat(floating-button): move to correct folder #195
artur-langl Nov 13, 2025
60926d3
feat(floating-button): use component instead of directive, move style…
artur-langl Nov 13, 2025
7b0b1ca
feat(floating-button): more style/storybook changes #195
artur-langl Nov 13, 2025
c5c8dea
feat(floating-button): storybook polishing #195
artur-langl Nov 13, 2025
439f455
feat(floating-button): fix icon only #195
artur-langl Nov 13, 2025
4f80011
feat(floating-button): keep min-height #195
artur-langl Nov 13, 2025
4537ea8
feat(floating-button): finishing touches on story to fix text aligmen…
artur-langl Nov 14, 2025
e7ec55c
feat(floating-button): remove debug css #195
artur-langl Nov 14, 2025
f962835
feat(floating-button): remove id #195
artur-langl Nov 14, 2025
266e82a
feat(floating-button): refactor into selector component + trim styles…
artur-langl Nov 14, 2025
0b0a15e
feat(floating-button): re-add box-shadow + add links to storybook #195
artur-langl Nov 18, 2025
4b3715f
feat(floating-button): use default size by default in storybook #195
artur-langl Nov 18, 2025
eed60d1
feat(floating-button): move button implementation to directive to use…
artur-langl Nov 19, 2025
881ee6d
feat(floating-button): remove unused commented code #195
artur-langl Nov 19, 2025
e0229be
feat(floating-button): fix tesst #195
artur-langl Nov 19, 2025
094fb59
feat(floating-button): export base-button directive in index file #195
artur-langl Nov 19, 2025
09e98d6
feat(floating-button): move inputs into button and floating button co…
artur-langl Nov 19, 2025
0b65dd8
feat(floating-button): ensure to not use non encapsulated styles in f…
artur-langl Nov 19, 2025
7f3c5b3
feat(floating-button): remove debug log #195
artur-langl Nov 20, 2025
4b37b13
feat(floating-button): fix scss build warning #195
artur-langl Nov 20, 2025
0e80438
feat(floating-button): add barrel index eports #195
artur-langl Dec 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
@use "@tedi-design-system/core/mixins";
@use "@tedi-design-system/core/bootstrap-utility/breakpoints";
@use "../../../../tedi/components/buttons/button/button.component.scss" as
buttonMixins;

button.tedi-floating-button {
box-shadow: 0 4px 10px 0 var(--alpha-14);

@include buttonMixins.button-main-styles();

&--vertical {
min-width: max-content;
white-space: nowrap;
border-radius: 0;
transform: rotate(-90deg);
transform-origin: center;

@include mixins.responsive-styles(
border-top-right-radius,
button-radius-sm
);
@include mixins.responsive-styles(border-top-left-radius, button-radius-sm);
}

&.tedi-floating-button--primary {
@include buttonMixins.button-variant-color-vars(
"primary",
false,
"floating"
);
}

&.tedi-floating-button--secondary {
@include buttonMixins.button-variant-color-vars(
"secondary",
false,
"floating"
);
}

&--default {
@include buttonMixins.button-size("md");
}

&--large {
@include buttonMixins.button-size("lg");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
Component,
computed,
input,
ViewEncapsulation,
OnInit,
inject,
} from "@angular/core";
import { BaseButtonDirective } from "tedi/components";

export type FloatingButtonVariant = "primary" | "secondary";

export type FloatingButtonSize = "default" | "large";
export type FloatingButtonAxis = "horizontal" | "vertical";

@Component({
selector: "[tedi-floating-button]",
template: `<ng-content />`,
styleUrl: "./floating-button.component.scss",
encapsulation: ViewEncapsulation.None,
hostDirectives: [
{
directive: BaseButtonDirective,
},
],
host: {
"[class]": "floatClasses()",
},
})
export class FloatingButtonComponent implements OnInit {
/**
* Specifies the color theme of the button. The color should meet accessibility standards for color contrast.
* @default primary
*/
variant = input<FloatingButtonVariant>("primary");
/**
* Defines the size of the button.
* @default default
*/
size = input<FloatingButtonSize>("default");
/**
* Button axis
* @default horizontal
*/
axis = input<FloatingButtonAxis>("horizontal");

buttonDirective = inject(BaseButtonDirective);

ngOnInit() {
this.buttonDirective.classNamePrefix.set("tedi-floating-button");
}

floatClasses = computed(() => {
const classes = [
"tedi-floating-button",
`tedi-floating-button--${this.variant() ?? "primary"}`,
`tedi-floating-button--${this.size() ?? "default"}`,
`tedi-floating-button--${this.axis() ?? "horizontal"}`,
];
return classes.join(" ");
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import {
argsToTemplate,
Meta,
moduleMetadata,
StoryObj,
} from "@storybook/angular";
import { FloatingButtonComponent } from "./floating-button.component";
import { IconComponent } from "tedi/components";

const buttonSizeArray = ["default", "large"];
const buttonStateArray = ["Default", "Hover", "Active", "Focus"];

interface StoryArgs {
textOffset: string;
}

type StoryFloatingButtonArgs = FloatingButtonComponent & StoryArgs;

/**
* <a href="https://www.figma.com/design/jWiRIXhHRxwVdMSimKX2FF/TEDI-READY-2.15.23?node-id=4515-65391&t=PIbEsGEGsONqRIrN-0" target="_BLANK">Figma ↗</a><br/>
* <a href="https://tedi.tehik.ee/1ee8444b7/p/546461-floating-button" target="_BLANK">Zeroheight ↗</a><hr/>
**/
const meta: Meta<StoryFloatingButtonArgs> = {
title: "Community/Buttons/Floating Button",
component: FloatingButtonComponent,

decorators: [
moduleMetadata({
imports: [FloatingButtonComponent, IconComponent],
}),
],
args: {
variant: "primary",
axis: "horizontal",
textOffset: "30px",
size: "default",
},
argTypes: {
variant: {
control: "select",
description: "Specifies the color theme of the button.",
options: ["primary", "secondary", "neutral", "success"],
},
axis: {
control: "radio",
description: "Button axis, changes the orientation of the button.",
options: ["horizontal", "vertical"],
},
size: {
control: "radio",
description: "Button size.",
options: ["default", "large"],
},
// not meant to be user-editable or seen
textOffset: {
table: {
disable: true,
},
},
},
};

export default meta;

type Story = StoryObj<StoryFloatingButtonArgs>;

export const Default: Story = {
parameters: {
pseudo: {
hover: "#Hover",
active: "#Active",
focusVisible: "#Focus",
},
offset: -30,
},
render: ({ textOffset: _textOffset, ...args }) => ({
props: { ...args, debug: () => console.log("floating button clicked!") },
template: `
<div style="margin: 2rem;">
<button tedi-floating-button ${argsToTemplate(args)} (click)="debug()">floating button</button>
</div>
`,
}),
};

export const sizesVertical: Story = {
...Default,
args: {
axis: "vertical",
},
render: (args) => ({
props: { ...args, buttonSizeArray },
template: `
<div style="display: flex; flex-direction: column; gap: 8rem; margin: 2rem; overflow: visible; white-space: nowrap">
@for(size of buttonSizeArray; track size) {
<div>
<div style="transform: translateY({{textOffset}});">{{size}}</div>
<button tedi-floating-button [axis]="[axis]" [size]="size">Floating Button</button>
<button tedi-floating-button [axis]="[axis]" [size]="size">Floating Button <tedi-icon name="arrow_upward" /></button>
<button tedi-floating-button [axis]="[axis]" [size]="size"><tedi-icon name="arrow_upward" /></button>

<button tedi-floating-button [axis]="[axis]" variant="secondary" [size]="size">Floating Button</button>
<button tedi-floating-button [axis]="[axis]" variant="secondary" [size]="size">Floating Button <tedi-icon name="arrow_upward" /></button>
<button tedi-floating-button [axis]="[axis]" variant="secondary" [size]="size"><tedi-icon name="arrow_upward"/></button>
</div>
}
</div>`,
}),
};

export const statesVertical: Story = {
...Default,
args: {
axis: "vertical",
},
render: (args) => ({
props: { ...args, buttonStateArray },
template: `
<div style="display: flex; flex-direction: column; gap: 8rem; margin: 2rem overflow: visible; white-space: nowrap">
@for(state of buttonStateArray; track state) {
<div>
<div style="transform: translateY({{textOffset}});">{{state}}</div>
<button tedi-floating-button [axis]="[axis]" [id]="state">Floating Button</button>
<button tedi-floating-button [axis]="[axis]" [id]="state">Floating Button <tedi-icon name="arrow_upward" /></button>
<button tedi-floating-button [axis]="[axis]" [id]="state"><tedi-icon name="arrow_upward" /></button>

<button tedi-floating-button [axis]="[axis]" [id]="state" variant="secondary">Floating Button</button>
<button tedi-floating-button [axis]="[axis]" [id]="state" variant="secondary">Floating Button <tedi-icon name="arrow_upward" /></button>
<button tedi-floating-button [axis]="[axis]" [id]="state" variant="secondary"><tedi-icon name="arrow_upward"/></button>
</div>
}
</div>`,
}),
};

export const sizesHorizontal: Story = {
...sizesVertical,
args: {
axis: "horizontal",
textOffset: "0px",
},
};

export const statesHorizontal: Story = {
...statesVertical,
args: {
axis: "horizontal",
textOffset: "0px",
},
};
1 change: 1 addition & 0 deletions community/components/buttons/floating-button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./floating-button.component";
1 change: 1 addition & 0 deletions community/components/buttons/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./floating-button";
1 change: 1 addition & 0 deletions community/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./components/buttons";
export * from "./components/cards";
export * from "./components/form";
export * from "./components/navigation";
Expand Down
66 changes: 66 additions & 0 deletions tedi/components/buttons/button/base-button.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
Directive,
AfterContentChecked,
signal,
inject,
ElementRef,
computed,
} from "@angular/core";

@Directive({
host: {
"[class]": "classes()",
},
})
export class BaseButtonDirective implements AfterContentChecked {
/**
* CSS class name affix the directive should provide
*/
classNamePrefix = signal("tedi-button");

private host = inject(ElementRef);
iconOnly = signal(false);
iconFirst = signal(false);
iconLast = signal(false);

ngAfterContentChecked(): void {
const hostElement = this.host.nativeElement as HTMLElement;
const nodes = Array.from(hostElement.childNodes).filter(
(node) =>
node.nodeType === Node.ELEMENT_NODE ||
(node.nodeType === Node.TEXT_NODE && node.textContent?.trim())
);
const nodeCount = nodes.length;
const iconIndexes = nodes
.map((node, index) => ({ node, index }))
.filter(
(x) =>
x.node.nodeType === Node.ELEMENT_NODE &&
x.node.nodeName === "TEDI-ICON"
)
.map((x) => x.index);

const iconCount = iconIndexes.length;
this.iconOnly.set(nodeCount === 1 && iconCount === 1);
this.iconFirst.set(iconIndexes.includes(0));
this.iconLast.set(iconIndexes.includes(nodes.length - 1));
}

classes = computed(() => {
const classList = [this.classNamePrefix()];

if (this.iconOnly()) {
classList.push(`${this.classNamePrefix()}--icon-only`);
}

if (!this.iconFirst()) {
classList.push(`${this.classNamePrefix()}--pl`);
}

if (!this.iconLast()) {
classList.push(`${this.classNamePrefix()}--pr`);
}

return classList.join(" ");
});
}
Loading