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:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user