1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-13 15:03:26 +00:00

[CL-271] Update styles for toggle (#10377)

This commit is contained in:
Victoria League
2024-08-07 10:52:55 -04:00
committed by GitHub
parent 445ca8fd04
commit 1e471e0e3f
4 changed files with 94 additions and 41 deletions

View File

@@ -1,4 +1,4 @@
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
import { Meta, Story, Primary, Controls, Canvas } from "@storybook/addon-docs";
import * as stories from "./toggle-group.stories";
@@ -15,22 +15,23 @@ Toggle groups function as radio buttons and a radio group under the hood.
A button in a toggle group can have a badge counter added to show the number of items existing
within that filter.
For focus states, use `focus-visible`.
If the labels in a toggle group would overflow the width of the toggle group container, then the
labels will wrap to 2 lines and truncate with an ellipsis past that. The full label text is
accessible via the `title` prop (i.e. visible on hover).
<Canvas>
<Story of={stories.Default} />
</Canvas>
<Canvas>
<Story of={stories.LabelWrap} />
</Canvas>
<Primary />
<Controls />
## Accessibility
- Follow contrast rules for the main button styles.
- Focus:
- Implement as a radio group with button styling and a context label (context label can be screen
reader only depending on use case).
- Since only 1 button can be selected at a time to filter the toggle group acts similarly to a
radio group.
- When moving focus to a button group, the focus should always move to the selected button. The
screen reader should then announce the button group: example “[context label], [button content]
selected, of [# of buttons]”), the number of buttons and the currently selected button. The user
may navigate the options then via left/right arrow keys.
See WCAG for more: https://www.w3.org/WAI/ARIA/apg/patterns/radio/
- Since only 1 button can be selected at a time, the toggle group acts similarly to a radio group.
- The user may navigate the options via left/right arrow keys.
- The screen reader will announce the button group: example “[context label], [button content]
selected, of [# of buttons]”), the number of buttons and the currently selected button.

View File

@@ -46,3 +46,31 @@ export const Default: Story = {
selected: "all",
},
};
export const LabelWrap: Story = {
render: (args) => ({
props: args,
template: /* HTML */ `
<bit-toggle-group
[(selected)]="selected"
aria-label="People list filter"
class="tw-max-w-[500px]"
>
<bit-toggle value="all">
All of the best things <span bitBadge variant="info">3</span>
</bit-toggle>
<bit-toggle value="invited"> Invited to a cool party </bit-toggle>
<bit-toggle value="accepted">
Accepted the invitation<span bitBadge variant="info">2</span>
</bit-toggle>
<bit-toggle value="deactivated"> Deactivated forever</bit-toggle>
</bit-toggle-group>
`,
}),
args: {
selected: "all",
},
};

View File

@@ -6,6 +6,11 @@
[checked]="selected"
(change)="onInputInteraction()"
/>
<label for="bit-toggle-{{ id }}" [ngClass]="labelClasses">
<ng-content></ng-content>
<label for="bit-toggle-{{ id }}" [ngClass]="labelClasses" [title]="labelTextContent">
<span class="group-hover/toggle:tw-underline tw-line-clamp-2" #labelContent>
<ng-content></ng-content>
</span>
<span class="tw-shrink-0" #bitBadgeContainer [hidden]="!bitBadgeContainerHasChidlren()">
<ng-content select="[bitBadge]"></ng-content>
</span>
</label>

View File

@@ -1,4 +1,12 @@
import { Component, HostBinding, Input } from "@angular/core";
import {
AfterContentChecked,
Component,
ElementRef,
HostBinding,
Input,
signal,
ViewChild,
} from "@angular/core";
import { ToggleGroupComponent } from "./toggle-group.component";
@@ -9,15 +17,19 @@ let nextId = 0;
templateUrl: "./toggle.component.html",
preserveWhitespaces: false,
})
export class ToggleComponent<TValue> {
export class ToggleComponent<TValue> implements AfterContentChecked {
id = nextId++;
@Input() value?: TValue;
@ViewChild("labelContent") labelContent: ElementRef<HTMLSpanElement>;
@ViewChild("bitBadgeContainer") bitBadgeContainer: ElementRef<HTMLSpanElement>;
constructor(private groupComponent: ToggleGroupComponent<TValue>) {}
@HostBinding("tabIndex") tabIndex = "-1";
@HostBinding("class") classList = ["tw-group/toggle"];
@HostBinding("class") classList = ["tw-group/toggle", "tw-flex"];
protected bitBadgeContainerHasChidlren = signal(false);
get name() {
return this.groupComponent.name;
@@ -31,51 +43,58 @@ export class ToggleComponent<TValue> {
return ["tw-peer/toggle-input", "tw-appearance-none", "tw-outline-none"];
}
get labelTextContent() {
return this.labelContent?.nativeElement.innerText ?? null;
}
get labelClasses() {
return [
"tw-h-full",
"tw-flex",
"tw-items-center",
"tw-gap-1.5",
"!tw-font-semibold",
"tw-leading-5",
"tw-transition",
"tw-text-center",
"tw-border-text-muted",
"!tw-text-muted",
"tw-border-primary-600",
"!tw-text-primary-600",
"tw-border-solid",
"tw-border-y",
"tw-border-r",
"tw-border-l-0",
"tw-cursor-pointer",
"group-first-of-type/toggle:tw-border-l",
"group-first-of-type/toggle:tw-rounded-l",
"group-last-of-type/toggle:tw-rounded-r",
"group-first-of-type/toggle:tw-rounded-l-full",
"group-last-of-type/toggle:tw-rounded-r-full",
"peer-focus/toggle-input:tw-outline-none",
"peer-focus/toggle-input:tw-ring",
"peer-focus/toggle-input:tw-ring-offset-2",
"peer-focus/toggle-input:tw-ring-primary-600",
"peer-focus/toggle-input:tw-z-10",
"peer-focus/toggle-input:tw-bg-primary-600",
"peer-focus/toggle-input:tw-border-primary-600",
"peer-focus/toggle-input:!tw-text-contrast",
"hover:tw-no-underline",
"hover:tw-bg-text-muted",
"hover:tw-border-text-muted",
"hover:!tw-text-contrast",
"peer-focus-visible/toggle-input:tw-outline-none",
"peer-focus-visible/toggle-input:tw-ring",
"peer-focus-visible/toggle-input:tw-ring-offset-2",
"peer-focus-visible/toggle-input:tw-ring-primary-600",
"peer-focus-visible/toggle-input:tw-z-10",
"peer-focus-visible/toggle-input:tw-bg-primary-600",
"peer-focus-visible/toggle-input:tw-border-primary-600",
"peer-focus-visible/toggle-input:!tw-text-contrast",
"peer-checked/toggle-input:tw-bg-primary-600",
"peer-checked/toggle-input:tw-border-primary-600",
"peer-checked/toggle-input:!tw-text-contrast",
"tw-py-1.5",
"tw-px-3",
"tw-px-4",
// Fix for bootstrap styles that add bottom margin
"!tw-mb-0",
// Fix for badge being slightly off center vertically
"[&>[bitBadge]]:tw-mt-px",
];
}
onInputInteraction() {
this.groupComponent.onInputInteraction(this.value);
}
ngAfterContentChecked() {
this.bitBadgeContainerHasChidlren.set(
this.bitBadgeContainer?.nativeElement.childElementCount > 0,
);
}
}