+ @for(state of buttonStateArray; track state) {
+
+
{{state}}
+
+
+
+
+
+
+
+
+ }
+
`,
+ }),
+};
+
+export const sizesHorizontal: Story = {
+ ...sizesVertical,
+ args: {
+ axis: "horizontal",
+ textOffset: "0px",
+ },
+};
+
+export const statesHorizontal: Story = {
+ ...statesVertical,
+ args: {
+ axis: "horizontal",
+ textOffset: "0px",
+ },
+};
diff --git a/community/components/buttons/floating-button/index.ts b/community/components/buttons/floating-button/index.ts
new file mode 100644
index 000000000..6114a84bf
--- /dev/null
+++ b/community/components/buttons/floating-button/index.ts
@@ -0,0 +1 @@
+export * from "./floating-button.component";
diff --git a/community/components/buttons/index.ts b/community/components/buttons/index.ts
new file mode 100644
index 000000000..96e4859ad
--- /dev/null
+++ b/community/components/buttons/index.ts
@@ -0,0 +1 @@
+export * from "./floating-button";
diff --git a/community/index.ts b/community/index.ts
index 3c0b3f1ac..fa989c6d1 100644
--- a/community/index.ts
+++ b/community/index.ts
@@ -1,3 +1,4 @@
+export * from "./components/buttons";
export * from "./components/cards";
export * from "./components/form";
export * from "./components/navigation";
diff --git a/tedi/components/buttons/button/base-button.directive.ts b/tedi/components/buttons/button/base-button.directive.ts
new file mode 100644
index 000000000..5dbfde55f
--- /dev/null
+++ b/tedi/components/buttons/button/base-button.directive.ts
@@ -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(" ");
+ });
+}
diff --git a/tedi/components/buttons/button/button.component.scss b/tedi/components/buttons/button/button.component.scss
index 9bc60658e..17463dbe8 100644
--- a/tedi/components/buttons/button/button.component.scss
+++ b/tedi/components/buttons/button/button.component.scss
@@ -3,46 +3,53 @@
$neutral-variants: "neutral", "neutral-inverted", "danger-neutral";
-@mixin button-variant-color-vars($variant, $icon-only: false) {
+@mixin button-variant-color-vars($variant, $icon-only: false, $affix: main) {
& {
@if $icon-only {
- --_btn-bg: var(--button-main-#{$variant}-icon-only-background-default);
+ --_btn-bg: var(
+ --button-#{$affix}-#{$variant}-icon-only-background-default
+ );
} @else {
- --_btn-bg: var(--button-main-#{$variant}-background-default);
+ --_btn-bg: var(--button-#{$affix}-#{$variant}-background-default);
}
- --_btn-border: var(--button-main-#{$variant}-border-default);
- --_btn-text: var(--button-main-#{$variant}-text-default);
+ --_btn-border: var(--button-#{$affix}-#{$variant}-border-default);
+ --_btn-text: var(--button-#{$affix}-#{$variant}-text-default);
--_btn-outline: var(--primary-500);
- @include button-state-color-vars($variant, "hover", $icon-only);
- @include button-state-color-vars($variant, "focus", $icon-only);
- @include button-state-color-vars($variant, "active", $icon-only);
+ @include button-state-color-vars($variant, "hover", $icon-only, $affix);
+ @include button-state-color-vars($variant, "focus", $icon-only, $affix);
+ @include button-state-color-vars($variant, "active", $icon-only, $affix);
@include button-disabled-color-vars($variant);
}
}
-@mixin button-state-color-vars($variant, $state, $icon-only: false) {
+@mixin button-state-color-vars(
+ $variant,
+ $state,
+ $icon-only: false,
+ $affix: main
+) {
& {
@if $icon-only {
--_btn-#{$state}-bg: var(
- --button-main-#{$variant}-icon-only-background-#{$state},
- var(--button-main-#{$variant}-icon-only-background-default)
+ --button-#{$affix}-#{$variant}-icon-only-background-#{$state},
+ var(--button-#{$affix}-#{$variant}-icon-only-background-default)
);
} @else {
--_btn-#{$state}-bg: var(
- --button-main-#{$variant}-background-#{$state},
- var(--button-main-#{$variant}-background-default)
+ --button-#{$affix}-#{$variant}-background-#{$state},
+ var(--button-#{$affix}-#{$variant}-background-default)
);
}
--_btn-#{$state}-border: var(
- --button-main-#{$variant}-border-#{$state},
- var(--button-main-#{$variant}-border-default)
+ --button-#{$affix}-#{$variant}-border-#{$state},
+ var(--button-#{$affix}-#{$variant}-border-default)
);
--_btn-#{$state}-text: var(
- --button-main-#{$variant}-text-#{$state},
- var(--button-main-#{$variant}-text-default)
+ --button-#{$affix}-#{$variant}-text-#{$state},
+ var(--button-#{$affix}-#{$variant}-text-default)
);
}
}
@@ -103,37 +110,106 @@ $neutral-variants: "neutral", "neutral-inverted", "danger-neutral";
}
}
-.tedi-button {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: var(--_btn-inner-spacing);
- text-decoration: none;
- background: var(--_btn-bg);
- color: var(--_btn-text);
- border: var(--borders-01) solid var(--_btn-border);
- padding: var(--_btn-padding);
- transition: 150ms ease;
- transition-property: background-color, border-color;
- cursor: pointer;
+@mixin button-main-styles {
+ & {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--_btn-inner-spacing);
+ text-decoration: none;
+ background: var(--_btn-bg);
+ color: var(--_btn-text);
+ border: var(--borders-01) solid var(--_btn-border);
+ padding: var(--_btn-padding);
+ transition: 150ms ease;
+ transition-property: background-color, border-color;
+ cursor: pointer;
- @include mixins.responsive-styles(
- font-family,
- family-primary,
- $exclude: tablet
- );
- @include mixins.responsive-styles(font-weight, body-regular-weight);
- @include mixins.responsive-styles(line-height, body-bold-line-height);
- @include mixins.responsive-styles(border-radius, button-radius-default);
+ @include mixins.responsive-styles(
+ font-family,
+ family-primary,
+ $exclude: tablet
+ );
+ @include mixins.responsive-styles(font-weight, body-regular-weight);
+ @include mixins.responsive-styles(line-height, body-bold-line-height);
+ @include mixins.responsive-styles(border-radius, button-radius-default);
+
+ &--icon-only {
+ @include mixins.responsive-styles(width, button-md-icon-size);
+ @include mixins.responsive-styles(height, button-md-icon-size);
+ @include mixins.responsive-styles(padding, button-md-icon-padding);
+ }
+
+ &--pl {
+ &:not(.tedi-button--neutral):not(.tedi-button--danger-neutral):not(
+ .tedi-button--neutral-inverted
+ ) {
+ padding-left: calc(
+ var(--_btn-padding-x) + var(--_btn-inner-spacing) - 1px
+ );
+ }
+ }
+
+ &--pr {
+ &:not(.tedi-button--neutral):not(.tedi-button--danger-neutral):not(
+ .tedi-button--neutral-inverted
+ ) {
+ padding-right: calc(
+ var(--_btn-padding-x) + var(--_btn-inner-spacing) - 1px
+ );
+ }
+ }
+
+ &:hover {
+ background: var(--_btn-hover-bg);
+ border-color: var(--_btn-hover-border);
+ color: var(--_btn-hover-text);
+ }
+
+ &:active {
+ background: var(--_btn-active-bg);
+ border-color: var(--_btn-active-border);
+ color: var(--_btn-active-text);
+ }
+
+ &:focus-visible {
+ background: var(--_btn-focus-bg);
+ border-color: var(--_btn-focus-border);
+ color: var(--_btn-focus-text);
+ outline: 2px solid var(--_btn-outline);
+ outline-offset: 1px;
+ }
+
+ &:disabled {
+ background: var(--_btn-disabled-bg);
+ border-color: var(--_btn-disabled-border);
+ color: var(--_btn-disabled-text);
+ cursor: not-allowed;
+ }
+
+ tedi-icon {
+ color: inherit;
+ font-size: inherit;
+ }
+ }
+}
+
+@mixin button-size($size) {
+ & {
+ @include button-size-vars(#{$size});
+ @include mixins.responsive-styles(font-size, button-text-size-#{$size});
+ }
+}
+
+.tedi-button {
+ @include button-main-styles();
&--small {
- @include button-size-vars("sm");
- @include mixins.responsive-styles(font-size, button-text-size-sm);
+ @include button-size("sm");
}
&--default {
- @include button-size-vars("md");
- @include mixins.responsive-styles(font-size, button-text-size-default);
+ @include button-size("md");
}
&--primary {
@@ -176,10 +252,6 @@ $neutral-variants: "neutral", "neutral-inverted", "danger-neutral";
}
&--icon-only {
- @include mixins.responsive-styles(width, button-md-icon-size);
- @include mixins.responsive-styles(height, button-md-icon-size);
- @include mixins.responsive-styles(padding, button-md-icon-padding);
-
&.tedi-button--neutral {
@include button-variant-color-vars("neutral", true);
}
@@ -198,56 +270,4 @@ $neutral-variants: "neutral", "neutral-inverted", "danger-neutral";
@include mixins.responsive-styles(padding, button-sm-icon-padding);
}
}
-
- &--pl {
- &:not(.tedi-button--neutral):not(.tedi-button--danger-neutral):not(
- .tedi-button--neutral-inverted
- ) {
- padding-left: calc(
- var(--_btn-padding-x) + var(--_btn-inner-spacing) - 1px
- );
- }
- }
-
- &--pr {
- &:not(.tedi-button--neutral):not(.tedi-button--danger-neutral):not(
- .tedi-button--neutral-inverted
- ) {
- padding-right: calc(
- var(--_btn-padding-x) + var(--_btn-inner-spacing) - 1px
- );
- }
- }
-
- &:hover {
- background: var(--_btn-hover-bg);
- border-color: var(--_btn-hover-border);
- color: var(--_btn-hover-text);
- }
-
- &:active {
- background: var(--_btn-active-bg);
- border-color: var(--_btn-active-border);
- color: var(--_btn-active-text);
- }
-
- &:focus-visible {
- background: var(--_btn-focus-bg);
- border-color: var(--_btn-focus-border);
- color: var(--_btn-focus-text);
- outline: 2px solid var(--_btn-outline);
- outline-offset: 1px;
- }
-
- &:disabled {
- background: var(--_btn-disabled-bg);
- border-color: var(--_btn-disabled-border);
- color: var(--_btn-disabled-text);
- cursor: not-allowed;
- }
-
- tedi-icon {
- color: inherit;
- font-size: inherit;
- }
}
diff --git a/tedi/components/buttons/button/button.component.spec.ts b/tedi/components/buttons/button/button.component.spec.ts
index 67225b3d6..46dceced1 100644
--- a/tedi/components/buttons/button/button.component.spec.ts
+++ b/tedi/components/buttons/button/button.component.spec.ts
@@ -1,6 +1,6 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { ButtonComponent, ButtonSize, ButtonVariant } from "./button.component";
import { IconComponent } from "../../base/icon/icon.component";
+import { ButtonComponent, ButtonSize, ButtonVariant } from "./button.component";
describe("ButtonComponent", () => {
let fixture: ComponentFixture