1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-21882] Lit Components Cleanup (#14872)

* rename item row component to cipher item row

* move CipherItem into CipherItemRow instead of passing a generic children prop

* remove redundant embedded stories

* add mock data to dropdown button story
This commit is contained in:
Jonathan Prusik
2025-05-22 12:29:15 -04:00
committed by GitHub
parent f52e4e27a0
commit 753e7af380
13 changed files with 145 additions and 466 deletions

View File

@@ -1,83 +0,0 @@
import { Meta, Controls, Primary } from "@storybook/addon-docs";
import * as stories from "./cipher-action.lit-stories";
<Meta title="Components/Ciphers/Cipher Action" of={stories} />
## Cipher Action
The `CipherAction` component is a functional UI element that handles actions related to ciphers in a
secure environment. Built with the `lit` library and styled for consistency across themes, it
provides flexibility and accessibility while supporting various notification types.
<Primary />
<Controls />
## Props
| **Prop** | **Type** | **Required** | **Description** |
| ------------------ | --------------------------------------------------- | ------------ | -------------------------------------------------------------- |
| `handleAction` | `(e: Event) => void` | No | Function to execute when an action is triggered. |
| `notificationType` | `NotificationTypes.Change \| NotificationTypes.Add` | Yes | Specifies the type of notification associated with the action. |
| `theme` | `Theme` | Yes | The theme to style the component. Must match the `Theme` enum. |
## Installation and Setup
1. Ensure the necessary dependencies are installed:
- `lit`: Used to render the component.
2. Pass the required props when rendering the component:
- `handleAction`: Optional function to handle the triggered action.
- `notificationType`: Mandatory type from `NotificationTypes` to define the action context.
- `theme`: The styling theme (must be a valid `Theme` enum value).
## Accessibility (WCAG) Compliance
The `CipherAction` component is designed to be accessible, ensuring usability across diverse user
bases. Below are the key considerations for accessibility:
### Keyboard Accessibility
- Fully navigable using the keyboard.
- The action can be triggered using the `Enter` or `Space` key for users relying on keyboard
interaction.
### Screen Reader Compatibility
- The semantic elements used in the `CipherAction` component ensure that assistive technologies can
interpret the component correctly.
- Text associated with the `notificationType` is programmatically linked, providing clarity for
screen reader users.
### Focus Management
- The component includes focus styles to ensure visibility during navigation.
- Proper focus management ensures the component works seamlessly with keyboard navigation.
### Visual Feedback
- Provides distinct visual states for different themes and states:
- **Hover:** Adjustments to background, border, and text for enhanced visibility.
- **Active:** Highlights the button with a focus state when activated.
- **Disabled:** Grays out the component to indicate inactivity.
## Usage Example
Here's an example of how to integrate the `CipherAction` component:
```ts
import { CipherAction } from "../../cipher/cipher-action";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { NotificationTypes } from "../../../../notification/abstractions/notification-bar";
const handleAction = (e: Event) => {
console.log("Cipher action triggered!", e);
};
<CipherAction
handleAction={handleAction}
notificationType={NotificationTypes.Change}
theme={ThemeTypes.Dark}
/>;
```

View File

@@ -1,90 +0,0 @@
import { Meta, Controls, Primary } from "@storybook/addon-docs";
import * as stories from "./cipher-icon.lit-stories";
<Meta title="Components/Ciphers/Cipher Icon" of={stories} />
## Cipher Icon
The `CipherIcon` component is a versatile icon renderer designed for secure environments. It
dynamically supports custom icons provided via URIs or displays a default icon (`Globe`) styled
based on the theme and provided properties.
<Primary />
<Controls />
## Props
| **Prop** | **Type** | **Required** | **Description** |
| -------- | ------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `color` | `string` | Yes | A contextual color override applied when the `uri` is not provided, ensuring consistent styling of the default icon. |
| `size` | `string` | Yes | A valid CSS `width` value representing the width basis of the graphic. The height adjusts to maintain the original aspect ratio of the graphic. |
| `theme` | `Theme` | Yes | The styling theme for the icon, matching the `Theme` enum. |
| `uri` | `string` (optional) | No | A URL to an external graphic. If provided, the component displays this icon. If omitted, a default icon (`Globe`) styled with the provided `color` and `theme`. |
## Installation and Setup
1. Ensure the necessary dependencies are installed:
- `lit`: Renders the component.
- `@emotion/css`: Styles the component.
2. Pass the necessary props when using the component:
- `color`: Used when no `uri` is provided to style the default icon.
- `size`: Defines the width of the icon. Height maintains aspect ratio.
- `theme`: Specifies the theme for styling.
- `uri` (optional): If provided, this URI is used to display a custom icon.
## Accessibility (WCAG) Compliance
The `CipherIcon` component ensures accessible and user-friendly interactions through thoughtful
design:
### Semantic Rendering
- When the `uri` is provided, the component renders an `<img>` element, which is semantically
appropriate for external graphics.
- If no `uri` is provided, the default icon is wrapped in a `<span>`, ensuring proper context for
screen readers.
### Visual Feedback
- The component visually adjusts based on the `size`, `color`, and `theme`, ensuring the icon
remains clear and legible across different environments.
### Keyboard and Screen Reader Support
- Ensure that any container or parent component provides appropriate `alt` text or labeling when
`uri` is used with an `<img>` tag for additional accessibility.
## Usage Example
Here's an example of how to integrate the `CipherIcon` component:
```ts
import { CipherIcon } from "./cipher-icon";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
<CipherIcon
color="blue"
size="32px"
theme={ThemeTypes.Light}
uri="https://example.com/icon.png"
/>;
```
This configuration displays a custom icon from the provided URI with a width of 32px, styled for the
light theme. If the URI is omitted, the Globe icon is used as the fallback, colored in blue.
### Default Styles
- The default styles ensure responsive and clean design:
- Width: Defined by the size prop.
- Height: Automatically adjusts to maintain the aspect ratio.
- Fit Content: Ensures the icon does not overflow or distort its container.
### Notes
- Always validate the uri provided to ensure it points to a secure and accessible location.
- Use the color and theme props for consistent fallback styling when uri is not provided.

View File

@@ -1,81 +0,0 @@
import { Meta, Controls, Primary } from "@storybook/addon-docs";
import * as stories from "./cipher-indicator-icon.lit-stories";
<Meta title="Components/Ciphers/Cipher Indicator Icon" of={stories} />
## Cipher Info Indicator Icons
The `CipherInfoIndicatorIcons` component displays a set of icons indicating specific attributes
related to cipher information. It supports business and family organization indicators, styled
dynamically based on the provided theme.
<Primary />
<Controls />
## Props
| **Prop** | **Type** | **Required** | **Description** |
| ------------------ | --------- | ------------ | ----------------------------------------------------------------------- |
| `showBusinessIcon` | `boolean` | No | Displays the business organization icon when set to `true`. |
| `showFamilyIcon` | `boolean` | No | Displays the family organization icon when set to `true`. |
| `theme` | `Theme` | Yes | Defines the theme used to style the icons. Must match the `Theme` enum. |
## Installation and Setup
1. Ensure the necessary dependencies are installed:
- `lit`: Renders the component.
- `@emotion/css`: Used for styling.
2. Pass the required props when using the component:
- `showBusinessIcon`: A boolean that, when `true`, displays the business icon.
- `showFamilyIcon`: A boolean that, when `true`, displays the family icon.
- `theme`: Specifies the theme for styling the icons.
## Accessibility (WCAG) Compliance
The `CipherInfoIndicatorIcons` component ensures accessibility and usability through its design:
### Screen Reader Compatibility
- Icons are rendered as `<svg>` elements, and parent components should provide appropriate labeling
or descriptions to convey their meaning to screen readers.
### Visual Feedback
- Icons are styled dynamically based on the `theme` to ensure visual clarity and contrast in all
supported themes.
- The size of the icons is fixed at `12px` in height to maintain a consistent visual appearance.
## Usage Example
Here's an example of how to integrate the `CipherInfoIndicatorIcons` component:
```ts
import { CipherInfoIndicatorIcons } from "./cipher-info-indicator-icons";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
<CipherInfoIndicatorIcons
showBusinessIcon={true}
showFamilyIcon={false}
theme={ThemeTypes.Dark}
/>;
```
This example displays the business organization icon, styled for the dark theme, and omits the
family organization icon.
### Styling Details
- The component includes the following styles:
- Icons: Rendered as SVGs with a height of 12px and a width that adjusts to maintain their aspect
ratio.
- Color: Icons are dynamically styled based on the theme, using muted text colors for a subtle
appearance.
### Notes
- If neither showBusinessIcon nor showFamilyIcon is set to true, the component renders nothing. This
behavior should be handled by the parent component.

View File

@@ -1,31 +0,0 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { NotificationTypes } from "../../../../notification/abstractions/notification-bar";
import { CipherAction, CipherActionProps } from "../../cipher/cipher-action";
import { mockI18n } from "../mock-data";
export default {
title: "Components/Ciphers/Cipher Action",
argTypes: {
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
notificationType: {
control: "select",
options: [NotificationTypes.Change, NotificationTypes.Add],
},
handleAction: { control: false },
},
args: {
theme: ThemeTypes.Light,
notificationType: NotificationTypes.Change,
handleAction: () => alert("Action triggered!"),
i18n: mockI18n,
},
} as Meta<CipherActionProps>;
const Template = (args: CipherActionProps) => CipherAction({ ...args });
export const Default: StoryObj<CipherActionProps> = {
render: Template,
};

View File

@@ -1,33 +0,0 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { html } from "lit";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { CipherIcon, CipherIconProps } from "../../cipher/cipher-icon";
export default {
title: "Components/Ciphers/Cipher Icon",
argTypes: {
color: { control: "color" },
size: { control: "text" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
uri: { control: "text" },
},
args: {
size: "50px",
theme: ThemeTypes.Light,
uri: "",
},
} as Meta<CipherIconProps>;
const Template = (args: CipherIconProps) => {
return html`
<div style="width: ${args.size}; height: ${args.size}; overflow: hidden;">
${CipherIcon({ ...args })}
</div>
`;
};
export const Default: StoryObj<CipherIconProps> = {
render: Template,
};

View File

@@ -1,28 +0,0 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { html } from "lit";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import {
CipherInfoIndicatorIcons,
CipherInfoIndicatorIconsProps,
} from "../../cipher/cipher-indicator-icons";
import { OrganizationCategories } from "../../cipher/types";
export default {
title: "Components/Ciphers/Cipher Indicator Icons",
argTypes: {
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
},
args: {
theme: ThemeTypes.Light,
organizationCategories: [...Object.values(OrganizationCategories)],
},
} as Meta<CipherInfoIndicatorIconsProps>;
const Template: StoryObj<CipherInfoIndicatorIconsProps>["render"] = (args) =>
html`<div>${CipherInfoIndicatorIcons({ ...args })}</div>`;
export const Default: StoryObj<CipherInfoIndicatorIconsProps> = {
render: Template,
};

View File

@@ -1,23 +0,0 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { CipherInfo, CipherInfoProps } from "../../cipher/cipher-info";
import { mockCiphers } from "../mock-data";
export default {
title: "Components/Ciphers/Cipher Info",
argTypes: {
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
},
args: {
cipher: mockCiphers[0],
theme: ThemeTypes.Light,
},
} as Meta<CipherInfoProps>;
const Template = (args: CipherInfoProps) => CipherInfo({ ...args });
export const Default: StoryObj<CipherInfoProps> = {
render: Template,
};

View File

@@ -4,6 +4,7 @@ import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { themes } from "../../constants/styles";
import { ButtonRow, ButtonRowProps } from "../../rows/button-row";
import { mockBrowserI18nGetMessage } from "../mock-data";
export default {
title: "Components/Rows/Button Row",
@@ -15,6 +16,49 @@ export default {
window.alert("Button clicked!");
},
},
selectButtons: [
{
id: "select-1",
label: "select 1",
options: [
{
text: "item 1",
value: 1,
},
{
default: true,
text: "item 2",
value: 2,
},
{
text: "item 3",
value: 3,
},
],
},
{
id: "select-2",
label: "select 2",
options: [
{
text: "item a",
value: "a",
},
{
text: "item b",
value: "b",
},
{
text: "item c",
value: "c",
},
{
text: "item d",
value: "d",
},
],
},
],
},
} as Meta<ButtonRowProps>;
@@ -51,3 +95,10 @@ export const Dark: StoryObj<ButtonRowProps> = {
},
},
};
window.chrome = {
...window.chrome,
i18n: {
getMessage: mockBrowserI18nGetMessage,
},
} as typeof chrome;

View File

@@ -3,30 +3,30 @@ import { Meta, StoryObj } from "@storybook/web-components";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { NotificationTypes } from "../../../../notification/abstractions/notification-bar";
import { CipherItem, CipherItemProps } from "../../cipher/cipher-item";
import { CipherItemRow, CipherItemRowProps } from "../../rows/cipher-item-row";
import { mockCiphers, mockI18n } from "../mock-data";
export default {
title: "Components/Ciphers/Cipher Item",
title: "Components/Rows/Cipher Item Row",
argTypes: {
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
handleAction: { control: false },
notificationType: {
control: "select",
options: [NotificationTypes.Change, NotificationTypes.Add],
options: [...Object.values(NotificationTypes)],
},
handleAction: { control: false },
},
args: {
cipher: mockCiphers[0],
theme: ThemeTypes.Light,
notificationType: NotificationTypes.Change,
handleAction: () => alert("Clicked"),
i18n: mockI18n,
notificationType: NotificationTypes.Change,
theme: ThemeTypes.Light,
handleAction: () => window.alert("clicked!"),
},
} as Meta<CipherItemProps>;
} as Meta<CipherItemRowProps>;
const Template = (args: CipherItemProps) => CipherItem({ ...args });
const Template = (props: CipherItemRowProps) => CipherItemRow({ ...props });
export const Default: StoryObj<CipherItemProps> = {
export const Default: StoryObj<CipherItemRowProps> = {
render: Template,
};

View File

@@ -1,22 +0,0 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { ItemRow, ItemRowProps } from "../../rows/item-row";
export default {
title: "Components/Rows/Item Row",
argTypes: {
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
children: { control: "object" },
},
args: {
theme: ThemeTypes.Light,
},
} as Meta<ItemRowProps>;
const Template = (args: ItemRowProps) => ItemRow({ ...args });
export const Default: StoryObj<ItemRowProps> = {
render: Template,
};

View File

@@ -4,11 +4,10 @@ import { html } from "lit";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
import { NotificationType } from "../../../notification/abstractions/notification-bar";
import { CipherItem } from "../cipher";
import { NotificationCipherData } from "../cipher/types";
import { I18n } from "../common-types";
import { scrollbarStyles, spacing, themes, typography } from "../constants/styles";
import { ItemRow } from "../rows/item-row";
import { CipherItemRow } from "../rows/cipher-item-row";
export const componentClassPrefix = "notification-body";
@@ -37,15 +36,12 @@ export function NotificationBody({
return html`
<div class=${notificationBodyStyles({ isSafari, theme })}>
${ciphers.map((cipher) =>
ItemRow({
CipherItemRow({
cipher,
theme,
children: CipherItem({
cipher,
i18n,
notificationType,
theme,
handleAction: handleEditOrUpdateAction,
}),
i18n,
notificationType,
handleAction: handleEditOrUpdateAction,
}),
)}
</div>

View File

@@ -0,0 +1,78 @@
import { css } from "@emotion/css";
import { html } from "lit";
import { Theme } from "@bitwarden/common/platform/enums";
import { NotificationType } from "../../../notification/abstractions/notification-bar";
import { CipherItem } from "../cipher/cipher-item";
import { NotificationCipherData } from "../cipher/types";
import { I18n } from "../common-types";
import { spacing, themes, typography } from "../constants/styles";
export type CipherItemRowProps = {
cipher: NotificationCipherData;
i18n: I18n;
notificationType?: NotificationType;
theme: Theme;
handleAction: (e: Event) => void;
};
export function CipherItemRow({
cipher,
i18n,
notificationType,
theme,
handleAction,
}: CipherItemRowProps) {
return html`
<div class=${cipherItemRowStyles({ theme })}>
${CipherItem({
cipher,
i18n,
notificationType,
theme,
handleAction,
})}
</div>
`;
}
const cipherItemRowStyles = ({ theme }: { theme: Theme }) => css`
${typography.body1}
gap: ${spacing["2"]};
display: flex;
align-items: center;
justify-content: space-between;
border-width: 0 0 0.5px 0;
border-style: solid;
border-radius: ${spacing["2"]};
border-color: ${themes[theme].secondary["300"]};
background-color: ${themes[theme].background.DEFAULT};
padding: ${spacing["2"]} ${spacing["3"]};
min-height: min-content;
max-height: 52px;
overflow-x: hidden;
white-space: nowrap;
color: ${themes[theme].text.main};
font-weight: 400;
> div {
:first-child {
flex: 3 3 75%;
min-width: 25%;
}
:not(:first-child) {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-end;
max-width: 25%;
> button {
max-width: min-content;
}
}
}
`;

View File

@@ -1,55 +0,0 @@
import { css } from "@emotion/css";
import { html, TemplateResult } from "lit";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
import { spacing, themes, typography } from "../../../content/components/constants/styles";
export type ItemRowProps = {
theme: Theme;
children: TemplateResult | TemplateResult[];
};
export function ItemRow({ theme = ThemeTypes.Light, children }: ItemRowProps) {
return html` <div class=${itemRowStyles({ theme })}>${children}</div> `;
}
export const itemRowStyles = ({ theme }: { theme: Theme }) => css`
${typography.body1}
gap: ${spacing["2"]};
display: flex;
align-items: center;
justify-content: space-between;
border-width: 0 0 0.5px 0;
border-style: solid;
border-radius: ${spacing["2"]};
border-color: ${themes[theme].secondary["300"]};
background-color: ${themes[theme].background.DEFAULT};
padding: ${spacing["2"]} ${spacing["3"]};
min-height: min-content;
max-height: 52px;
overflow-x: hidden;
white-space: nowrap;
color: ${themes[theme].text.main};
font-weight: 400;
> div {
:first-child {
flex: 3 3 75%;
min-width: 25%;
}
:not(:first-child) {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-end;
max-width: 25%;
> button {
max-width: min-content;
}
}
}
`;