mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
[PM-18785] Build dropdown button selection components (#13692)
* create standalone option selection button component * create option selection components * replace dropdown-menu component with option selection in button row * create folder and vault selection components * add story for option-selection and update button row component * update options selection component behaviour and styling * add shared icon typing * move Options to common types * refactor option selection component to handle options better * rework notification footer and button row components to handle expected data props * add optional selection option menu label * set max-height with scroll handling on option selection menu * fix menu item spacing * avoid displaying the dropdown menu horizontally outside of the viewport * update dropdown menu style * update button content spacing * allow overriding default scrollbar colors * update options items menu typography * fix eslint exception * refine some prop names
This commit is contained in:
@@ -6,27 +6,27 @@ import { Theme } from "@bitwarden/common/platform/enums";
|
|||||||
import { border, themes, typography, spacing } from "../constants/styles";
|
import { border, themes, typography, spacing } from "../constants/styles";
|
||||||
|
|
||||||
export function ActionButton({
|
export function ActionButton({
|
||||||
buttonAction,
|
|
||||||
buttonText,
|
buttonText,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
theme,
|
theme,
|
||||||
|
handleClick,
|
||||||
}: {
|
}: {
|
||||||
buttonAction: (e: Event) => void;
|
|
||||||
buttonText: string;
|
buttonText: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
|
handleClick: (e: Event) => void;
|
||||||
}) {
|
}) {
|
||||||
const handleButtonClick = (event: Event) => {
|
const handleButtonClick = (event: Event) => {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
buttonAction(event);
|
handleClick(event);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<button
|
<button
|
||||||
type="button"
|
|
||||||
title=${buttonText}
|
|
||||||
class=${actionButtonStyles({ disabled, theme })}
|
class=${actionButtonStyles({ disabled, theme })}
|
||||||
|
title=${buttonText}
|
||||||
|
type="button"
|
||||||
@click=${handleButtonClick}
|
@click=${handleButtonClick}
|
||||||
>
|
>
|
||||||
${buttonText}
|
${buttonText}
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ export function EditButton({
|
|||||||
title=${buttonText}
|
title=${buttonText}
|
||||||
class=${editButtonStyles({ disabled, theme })}
|
class=${editButtonStyles({ disabled, theme })}
|
||||||
@click=${(event: Event) => {
|
@click=${(event: Event) => {
|
||||||
// FIXME: Remove when updating file. Eslint update
|
if (!disabled) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
buttonAction(event);
|
||||||
!disabled && buttonAction(event);
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${PencilSquare({ disabled, theme })}
|
${PencilSquare({ disabled, theme })}
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { css } from "@emotion/css";
|
||||||
|
import { html, nothing } from "lit";
|
||||||
|
|
||||||
|
import { Theme } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
|
import { IconProps, Option } from "../common-types";
|
||||||
|
import { border, spacing, themes, typography } from "../constants/styles";
|
||||||
|
import { AngleUp, AngleDown } from "../icons";
|
||||||
|
|
||||||
|
export type OptionSelectionButtonProps = {
|
||||||
|
disabled: boolean;
|
||||||
|
icon?: Option["icon"];
|
||||||
|
text?: string;
|
||||||
|
theme: Theme;
|
||||||
|
toggledOn: boolean;
|
||||||
|
handleButtonClick: (e: Event) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function OptionSelectionButton({
|
||||||
|
disabled,
|
||||||
|
icon,
|
||||||
|
text,
|
||||||
|
theme,
|
||||||
|
toggledOn,
|
||||||
|
handleButtonClick,
|
||||||
|
}: OptionSelectionButtonProps) {
|
||||||
|
const selectedOptionIconProps: IconProps = { color: themes[theme].text.muted, theme };
|
||||||
|
|
||||||
|
const buttonIcon = icon?.(selectedOptionIconProps);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<button
|
||||||
|
class=${selectionButtonStyles({ disabled, toggledOn, theme })}
|
||||||
|
title=${text}
|
||||||
|
type="button"
|
||||||
|
@click=${handleButtonClick}
|
||||||
|
>
|
||||||
|
${buttonIcon ?? nothing}
|
||||||
|
${text ? html`<span class=${dropdownButtonTextStyles}>${text}</span>` : nothing}
|
||||||
|
${toggledOn
|
||||||
|
? AngleUp({ color: themes[theme].text.muted, theme })
|
||||||
|
: AngleDown({ color: themes[theme].text.muted, theme })}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconSize = "15px";
|
||||||
|
|
||||||
|
const selectionButtonStyles = ({
|
||||||
|
disabled,
|
||||||
|
toggledOn,
|
||||||
|
theme,
|
||||||
|
}: {
|
||||||
|
disabled: boolean;
|
||||||
|
toggledOn: boolean;
|
||||||
|
theme: Theme;
|
||||||
|
}) => css`
|
||||||
|
${typography.body2}
|
||||||
|
|
||||||
|
gap: ${spacing["1.5"]};
|
||||||
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
columns: ${iconSize} max-content ${iconSize};
|
||||||
|
border-radius: ${border.radius.full};
|
||||||
|
padding: ${spacing["1"]} ${spacing["2"]};
|
||||||
|
max-height: fit-content;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
${disabled
|
||||||
|
? `
|
||||||
|
border: 1px solid ${themes[theme].secondary["300"]};
|
||||||
|
background-color: ${themes[theme].secondary["300"]};
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: ${themes[theme].text.muted};
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
border: 1px solid ${themes[theme].text.muted};
|
||||||
|
background-color: ${toggledOn ? themes[theme].secondary["100"] : "transparent"};
|
||||||
|
cursor: pointer;
|
||||||
|
color: ${themes[theme].text.muted};
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
border-color: ${themes[theme].secondary["700"]};
|
||||||
|
background-color: ${themes[theme].secondary["100"]};
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
|
||||||
|
> svg {
|
||||||
|
max-width: ${iconSize};
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const dropdownButtonTextStyles = css`
|
||||||
|
overflow-x: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
@@ -9,17 +9,17 @@ import { Business, Family } from "../../../content/components/icons";
|
|||||||
// @TODO connect data source to icon checks
|
// @TODO connect data source to icon checks
|
||||||
// @TODO support other indicator types (attachments, etc)
|
// @TODO support other indicator types (attachments, etc)
|
||||||
export function CipherInfoIndicatorIcons({
|
export function CipherInfoIndicatorIcons({
|
||||||
isBusinessOrg,
|
showBusinessIcon,
|
||||||
isFamilyOrg,
|
showFamilyIcon,
|
||||||
theme,
|
theme,
|
||||||
}: {
|
}: {
|
||||||
isBusinessOrg?: boolean;
|
showBusinessIcon?: boolean;
|
||||||
isFamilyOrg?: boolean;
|
showFamilyIcon?: boolean;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
}) {
|
}) {
|
||||||
const indicatorIcons = [
|
const indicatorIcons = [
|
||||||
...(isBusinessOrg ? [Business({ color: themes[theme].text.muted, theme })] : []),
|
...(showBusinessIcon ? [Business({ color: themes[theme].text.muted, theme })] : []),
|
||||||
...(isFamilyOrg ? [Family({ color: themes[theme].text.muted, theme })] : []),
|
...(showFamilyIcon ? [Family({ color: themes[theme].text.muted, theme })] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
return indicatorIcons.length
|
return indicatorIcons.length
|
||||||
|
|||||||
28
apps/browser/src/autofill/content/components/common-types.ts
Normal file
28
apps/browser/src/autofill/content/components/common-types.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { TemplateResult } from "lit";
|
||||||
|
|
||||||
|
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||||
|
import { Theme } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
|
export type IconProps = {
|
||||||
|
color?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
theme: Theme;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Option = {
|
||||||
|
default?: boolean;
|
||||||
|
icon?: (props: IconProps) => TemplateResult;
|
||||||
|
text?: string;
|
||||||
|
value: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FolderView = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OrgView = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
productTierType?: ProductTierType;
|
||||||
|
};
|
||||||
@@ -174,14 +174,17 @@ export const buildIconColorRule = (color: string, rule: RuleName = ruleNames.fil
|
|||||||
${rule}: ${color};
|
${rule}: ${color};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function scrollbarStyles(theme: Theme) {
|
export function scrollbarStyles(theme: Theme, color?: { thumb?: string; track?: string }) {
|
||||||
|
const thumbColor = color?.thumb || themes[theme].secondary["500"];
|
||||||
|
const trackColor = color?.track || themes[theme].background.alt;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
/* FireFox & Chrome support */
|
||||||
default: `
|
default: `
|
||||||
/* FireFox & Chrome support */
|
scrollbar-color: ${thumbColor} ${trackColor};
|
||||||
scrollbar-color: ${themes[theme].secondary["500"]} ${themes[theme].background.alt};
|
|
||||||
`,
|
`,
|
||||||
|
/* Safari Support */
|
||||||
safari: `
|
safari: `
|
||||||
/* Safari Support */
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
@@ -191,10 +194,10 @@ export function scrollbarStyles(theme: Theme) {
|
|||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background-clip: content-box;
|
background-clip: content-box;
|
||||||
background-color: ${themes[theme].secondary["500"]};
|
background-color: ${thumbColor};
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
${themes[theme].background.alt};
|
${trackColor};
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
${themes[theme].secondary["600"]};
|
${themes[theme].secondary["600"]};
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
import { css } from "@emotion/css";
|
|
||||||
import { html, TemplateResult } from "lit";
|
|
||||||
|
|
||||||
import { Theme } from "@bitwarden/common/platform/enums";
|
|
||||||
|
|
||||||
import { border, themes, typography, spacing } from "./constants/styles";
|
|
||||||
import { AngleDown } from "./icons";
|
|
||||||
|
|
||||||
export function DropdownMenu({
|
|
||||||
buttonText,
|
|
||||||
icon,
|
|
||||||
disabled = false,
|
|
||||||
selectAction,
|
|
||||||
theme,
|
|
||||||
}: {
|
|
||||||
selectAction?: (e: Event) => void;
|
|
||||||
buttonText: string;
|
|
||||||
icon?: TemplateResult;
|
|
||||||
disabled?: boolean;
|
|
||||||
theme: Theme;
|
|
||||||
}) {
|
|
||||||
// @TODO placeholder/will not work; make stateful
|
|
||||||
const showDropdown = false;
|
|
||||||
const handleButtonClick = (event: Event) => {
|
|
||||||
// if (!disabled) {
|
|
||||||
// // show dropdown
|
|
||||||
// showDropdown = !showDropdown;
|
|
||||||
// this.requestUpdate();
|
|
||||||
// }
|
|
||||||
};
|
|
||||||
|
|
||||||
const dropdownMenuItems: TemplateResult[] = [];
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class=${dropdownContainerStyles}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title=${buttonText}
|
|
||||||
class=${dropdownButtonStyles({ disabled, theme })}
|
|
||||||
@click=${handleButtonClick}
|
|
||||||
>
|
|
||||||
${icon ?? null}
|
|
||||||
<span class=${dropdownButtonTextStyles}>${buttonText}</span>
|
|
||||||
${AngleDown({ color: themes[theme].text.muted, theme })}
|
|
||||||
</button>
|
|
||||||
${showDropdown
|
|
||||||
? html` <div class=${dropdownMenuStyles({ theme })}>${dropdownMenuItems}</div> `
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const iconSize = "15px";
|
|
||||||
|
|
||||||
const dropdownContainerStyles = css`
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
> div,
|
|
||||||
> button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const dropdownButtonStyles = ({ disabled, theme }: { disabled: boolean; theme: Theme }) => css`
|
|
||||||
${typography.body2}
|
|
||||||
|
|
||||||
font-weight: 400;
|
|
||||||
gap: ${spacing["1.5"]};
|
|
||||||
user-select: none;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-radius: ${border.radius.full};
|
|
||||||
padding: ${spacing["1"]} ${spacing["2"]};
|
|
||||||
max-height: fit-content;
|
|
||||||
overflow: hidden;
|
|
||||||
text-align: center;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
> svg {
|
|
||||||
max-width: ${iconSize};
|
|
||||||
height: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
${disabled
|
|
||||||
? `
|
|
||||||
border: 1px solid ${themes[theme].secondary["300"]};
|
|
||||||
background-color: ${themes[theme].secondary["300"]};
|
|
||||||
color: ${themes[theme].text.muted};
|
|
||||||
`
|
|
||||||
: `
|
|
||||||
border: 1px solid ${themes[theme].text.muted};
|
|
||||||
background-color: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
color: ${themes[theme].text.muted};
|
|
||||||
|
|
||||||
:hover {
|
|
||||||
border-color: ${themes[theme].secondary["700"]};
|
|
||||||
background-color: ${themes[theme].secondary["100"]};
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const dropdownButtonTextStyles = css`
|
|
||||||
max-width: calc(100% - ${iconSize} - ${iconSize});
|
|
||||||
overflow-x: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const dropdownMenuStyles = ({ theme }: { theme: Theme }) => css`
|
|
||||||
color: ${themes[theme].text.main};
|
|
||||||
border: 1px solid ${themes[theme].secondary["500"]};
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
background-clip: padding-box;
|
|
||||||
background-color: ${themes[theme].background.DEFAULT};
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
position: absolute;
|
|
||||||
overflow-y: auto;
|
|
||||||
`;
|
|
||||||
@@ -1,19 +1,10 @@
|
|||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
|
||||||
import { Theme } from "@bitwarden/common/platform/enums";
|
import { IconProps } from "../common-types";
|
||||||
|
|
||||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||||
|
|
||||||
export function AngleDown({
|
export function AngleDown({ color, disabled, theme }: IconProps) {
|
||||||
color,
|
|
||||||
disabled,
|
|
||||||
theme,
|
|
||||||
}: {
|
|
||||||
color?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
theme: Theme;
|
|
||||||
}) {
|
|
||||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { css } from "@emotion/css";
|
||||||
|
import { html } from "lit";
|
||||||
|
|
||||||
|
import { IconProps } from "../common-types";
|
||||||
|
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||||
|
|
||||||
|
export function AngleUp({ color, disabled, theme }: IconProps) {
|
||||||
|
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 12"
|
||||||
|
fill="none"
|
||||||
|
style="transform: scaleY(-1);"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
|
||||||
|
d="M12.004 11.244a2.705 2.705 0 0 1-1.75-.644L.266 2.154a.76.76 0 0 1-.263-.51.75.75 0 0 1 1.233-.637l9.99 8.445a1.186 1.186 0 0 0 1.565 0l10-8.54a.751.751 0 0 1 .973 1.141l-10 8.538a2.703 2.703 0 0 1-1.76.653Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -1,19 +1,10 @@
|
|||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
|
||||||
import { Theme } from "@bitwarden/common/platform/enums";
|
import { IconProps } from "../common-types";
|
||||||
|
|
||||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||||
|
|
||||||
export function Business({
|
export function Business({ color, disabled, theme }: IconProps) {
|
||||||
color,
|
|
||||||
disabled,
|
|
||||||
theme,
|
|
||||||
}: {
|
|
||||||
color?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
theme: Theme;
|
|
||||||
}) {
|
|
||||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
|||||||
@@ -1,19 +1,10 @@
|
|||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
|
||||||
import { Theme } from "@bitwarden/common/platform/enums";
|
import { IconProps } from "../common-types";
|
||||||
|
|
||||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||||
|
|
||||||
export function Close({
|
export function Close({ color, disabled, theme }: IconProps) {
|
||||||
color,
|
|
||||||
disabled,
|
|
||||||
theme,
|
|
||||||
}: {
|
|
||||||
color?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
theme: Theme;
|
|
||||||
}) {
|
|
||||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
|||||||
@@ -1,19 +1,10 @@
|
|||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
|
||||||
import { Theme } from "@bitwarden/common/platform/enums";
|
import { IconProps } from "../common-types";
|
||||||
|
|
||||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||||
|
|
||||||
export function ExclamationTriangle({
|
export function ExclamationTriangle({ color, disabled, theme }: IconProps) {
|
||||||
color,
|
|
||||||
disabled,
|
|
||||||
theme,
|
|
||||||
}: {
|
|
||||||
color?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
theme: Theme;
|
|
||||||
}) {
|
|
||||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
|||||||
@@ -1,19 +1,10 @@
|
|||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
|
||||||
import { Theme } from "@bitwarden/common/platform/enums";
|
import { IconProps } from "../common-types";
|
||||||
|
|
||||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||||
|
|
||||||
export function Family({
|
export function Family({ color, disabled, theme }: IconProps) {
|
||||||
color,
|
|
||||||
disabled,
|
|
||||||
theme,
|
|
||||||
}: {
|
|
||||||
color?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
theme: Theme;
|
|
||||||
}) {
|
|
||||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
|||||||
@@ -1,26 +1,17 @@
|
|||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
|
||||||
import { Theme } from "@bitwarden/common/platform/enums";
|
import { IconProps } from "../common-types";
|
||||||
|
|
||||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||||
|
|
||||||
export function Folder({
|
export function Folder({ color, disabled, theme }: IconProps) {
|
||||||
color,
|
|
||||||
disabled,
|
|
||||||
theme,
|
|
||||||
}: {
|
|
||||||
color?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
theme: Theme;
|
|
||||||
}) {
|
|
||||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 22" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" fill="none">
|
||||||
<path
|
<path
|
||||||
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
|
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
|
||||||
d="M21.78 3.823h-7.97a.823.823 0 0 1-.584-.265.872.872 0 0 1-.23-.61v-.396A2.321 2.321 0 0 0 12.348.93a2.214 2.214 0 0 0-1.577-.681H2.22A2.214 2.214 0 0 0 .645.93 2.321 2.321 0 0 0 0 2.552v16.88c-.003.608.23 1.191.647 1.624.417.432.984.677 1.576.681l19.554.016c.288 0 .574-.058.84-.171.267-.113.51-.278.714-.487a2.31 2.31 0 0 0 .497-.756c.115-.284.174-.588.172-.894V6.129c0-.606-.233-1.189-.648-1.62a2.223 2.223 0 0 0-1.572-.686ZM2.223 1.678h8.552c.22.006.43.101.582.265a.865.865 0 0 1 .23.61v.396c0 .606.234 1.189.65 1.62.416.432.983.677 1.576.684h7.97c.22.006.43.1.582.264a.86.86 0 0 1 .23.607v1.707a.56.56 0 0 1-.16.389.535.535 0 0 1-.38.159H1.951a.531.531 0 0 1-.381-.16.558.558 0 0 1-.16-.389V2.551a.867.867 0 0 1 .23-.609.82.82 0 0 1 .582-.265ZM22.34 20.08a.779.779 0 0 1-.558.238l-19.558-.014a.823.823 0 0 1-.582-.264.864.864 0 0 1-.23-.608v-9.065a.566.566 0 0 1 .16-.39.547.547 0 0 1 .38-.16h20.104c.143-.001.28.057.382.16a.561.561 0 0 1 .16.39v9.083a.903.903 0 0 1-.259.63h.001Z"
|
d="M12.705 2.813h-4.65a.48.48 0 0 1-.34-.155.509.509 0 0 1-.134-.355v-.231a1.354 1.354 0 0 0-.378-.947 1.291 1.291 0 0 0-.92-.397H1.296a1.291 1.291 0 0 0-.919.398A1.354 1.354 0 0 0 0 2.072v9.847c-.002.354.134.694.377.947.244.252.574.394.92.397l11.406.01a1.255 1.255 0 0 0 .907-.384 1.35 1.35 0 0 0 .39-.963V4.158c0-.353-.136-.693-.378-.945a1.296 1.296 0 0 0-.917-.4ZM1.297 1.562h4.988c.13.004.251.06.34.155a.504.504 0 0 1 .134.355v.231c0 .354.136.694.38.946.242.251.573.394.919.398h4.649c.128.004.25.059.34.154.089.096.137.223.134.355v.995a.326.326 0 0 1-.196.296.308.308 0 0 1-.12.024H1.139a.31.31 0 0 1-.223-.093.326.326 0 0 1-.092-.227V2.07a.506.506 0 0 1 .133-.355.479.479 0 0 1 .34-.155Zm11.734 10.735a.456.456 0 0 1-.325.139l-11.409-.008a.48.48 0 0 1-.34-.154.504.504 0 0 1-.133-.355V6.63a.33.33 0 0 1 .092-.227.32.32 0 0 1 .222-.094h11.727a.31.31 0 0 1 .223.094c.06.06.093.142.093.227v5.299a.527.527 0 0 1-.15.367Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,19 +1,10 @@
|
|||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
|
||||||
import { Theme } from "@bitwarden/common/platform/enums";
|
import { IconProps } from "../common-types";
|
||||||
|
|
||||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||||
|
|
||||||
export function Globe({
|
export function Globe({ color, disabled, theme }: IconProps) {
|
||||||
color,
|
|
||||||
disabled,
|
|
||||||
theme,
|
|
||||||
}: {
|
|
||||||
color?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
theme: Theme;
|
|
||||||
}) {
|
|
||||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export { AngleDown } from "./angle-down";
|
export { AngleDown } from "./angle-down";
|
||||||
|
export { AngleUp } from "./angle-up";
|
||||||
export { BrandIconContainer } from "./brand-icon-container";
|
export { BrandIconContainer } from "./brand-icon-container";
|
||||||
export { Business } from "./business";
|
export { Business } from "./business";
|
||||||
export { Close } from "./close";
|
export { Close } from "./close";
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
|
||||||
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
|
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
|
import { IconProps } from "../common-types";
|
||||||
|
|
||||||
// This icon has static multi-colors for each theme
|
// This icon has static multi-colors for each theme
|
||||||
export function PartyHorn({ theme }: { theme: Theme }) {
|
export function PartyHorn({ theme }: IconProps) {
|
||||||
if (theme === ThemeTypes.Dark) {
|
if (theme === ThemeTypes.Dark) {
|
||||||
return html`
|
return html`
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -1,19 +1,10 @@
|
|||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
|
||||||
import { Theme } from "@bitwarden/common/platform/enums";
|
import { IconProps } from "../common-types";
|
||||||
|
|
||||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||||
|
|
||||||
export function PencilSquare({
|
export function PencilSquare({ color, disabled, theme }: IconProps) {
|
||||||
color,
|
|
||||||
disabled,
|
|
||||||
theme,
|
|
||||||
}: {
|
|
||||||
color?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
theme: Theme;
|
|
||||||
}) {
|
|
||||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
|
||||||
import { Theme } from "@bitwarden/common/platform/enums";
|
import { IconProps } from "../common-types";
|
||||||
|
|
||||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||||
|
|
||||||
export function Shield({ color, theme }: { color?: string; theme: Theme }) {
|
export function Shield({ color, theme }: IconProps) {
|
||||||
const shapeColor = color || themes[theme].brandLogo;
|
const shapeColor = color || themes[theme].brandLogo;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
|||||||
@@ -1,26 +1,17 @@
|
|||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
|
||||||
import { Theme } from "@bitwarden/common/platform/enums";
|
import { IconProps } from "../common-types";
|
||||||
|
|
||||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||||
|
|
||||||
export function User({
|
export function User({ color, disabled, theme }: IconProps) {
|
||||||
color,
|
|
||||||
disabled,
|
|
||||||
theme,
|
|
||||||
}: {
|
|
||||||
color?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
theme: Theme;
|
|
||||||
}) {
|
|
||||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" fill="none">
|
||||||
<path
|
<path
|
||||||
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
|
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
|
||||||
d="M23.16 20.895a11.343 11.343 0 0 0-7.756-8.425 6.624 6.624 0 0 0 3.374-5.74 6.73 6.73 0 1 0-13.46 0 6.624 6.624 0 0 0 3.343 5.722 11.334 11.334 0 0 0-7.82 8.443A2.57 2.57 0 0 0 3.362 24h17.274a2.573 2.573 0 0 0 2.523-3.106v.001ZM6.933 6.73a5.12 5.12 0 0 1 3.12-4.766 5.115 5.115 0 0 1 4.845 8.962 5.114 5.114 0 0 1-2.848.866A5.097 5.097 0 0 1 6.933 6.73v.001ZM21.38 22.053a.94.94 0 0 1-.748.35H3.363a.938.938 0 0 1-.74-.35.986.986 0 0 1-.204-.833A9.812 9.812 0 0 1 12 13.516a9.807 9.807 0 0 1 9.581 7.704.98.98 0 0 1-.202.833Z"
|
d="M13.51 12.189a6.616 6.616 0 0 0-4.524-4.915 3.87 3.87 0 0 0 1.968-3.348 3.926 3.926 0 1 0-7.852 0 3.864 3.864 0 0 0 1.95 3.338A6.61 6.61 0 0 0 .49 12.189 1.499 1.499 0 0 0 1.962 14h10.077a1.503 1.503 0 0 0 1.471-1.812ZM4.044 3.926A2.987 2.987 0 0 1 7.592.963a2.985 2.985 0 0 1-.563 5.916 2.973 2.973 0 0 1-2.985-2.953Zm8.427 8.938a.548.548 0 0 1-.436.204H1.962a.548.548 0 0 1-.432-.204.576.576 0 0 1-.119-.486 5.724 5.724 0 0 1 9.175-3.23 5.723 5.723 0 0 1 2.003 3.23.571.571 0 0 1-.118.486Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ dynamically based on the provided theme.
|
|||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
| **Prop** | **Type** | **Required** | **Description** |
|
| **Prop** | **Type** | **Required** | **Description** |
|
||||||
| --------------- | --------- | ------------ | ----------------------------------------------------------------------- |
|
| ------------------ | --------- | ------------ | ----------------------------------------------------------------------- |
|
||||||
| `isBusinessOrg` | `boolean` | No | Displays the business organization icon when set to `true`. |
|
| `showBusinessIcon` | `boolean` | No | Displays the business organization icon when set to `true`. |
|
||||||
| `isFamilyOrg` | `boolean` | No | Displays the family 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. |
|
| `theme` | `Theme` | Yes | Defines the theme used to style the icons. Must match the `Theme` enum. |
|
||||||
|
|
||||||
## Installation and Setup
|
## Installation and Setup
|
||||||
|
|
||||||
@@ -29,8 +29,8 @@ dynamically based on the provided theme.
|
|||||||
- `@emotion/css`: Used for styling.
|
- `@emotion/css`: Used for styling.
|
||||||
|
|
||||||
2. Pass the required props when using the component:
|
2. Pass the required props when using the component:
|
||||||
- `isBusinessOrg`: A boolean that, when `true`, displays the business icon.
|
- `showBusinessIcon`: A boolean that, when `true`, displays the business icon.
|
||||||
- `isFamilyOrg`: A boolean that, when `true`, displays the family icon.
|
- `showFamilyIcon`: A boolean that, when `true`, displays the family icon.
|
||||||
- `theme`: Specifies the theme for styling the icons.
|
- `theme`: Specifies the theme for styling the icons.
|
||||||
|
|
||||||
## Accessibility (WCAG) Compliance
|
## Accessibility (WCAG) Compliance
|
||||||
@@ -57,8 +57,8 @@ import { CipherInfoIndicatorIcons } from "./cipher-info-indicator-icons";
|
|||||||
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||||
|
|
||||||
<CipherInfoIndicatorIcons
|
<CipherInfoIndicatorIcons
|
||||||
isBusinessOrg={true}
|
showBusinessIcon={true}
|
||||||
isFamilyOrg={false}
|
showFamilyIcon={false}
|
||||||
theme={ThemeTypes.Dark}
|
theme={ThemeTypes.Dark}
|
||||||
/>;
|
/>;
|
||||||
```
|
```
|
||||||
@@ -77,5 +77,5 @@ family organization icon.
|
|||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
|
|
||||||
- If neither isBusinessOrg nor isFamilyOrg is set to true, the component renders nothing. This
|
- If neither showBusinessIcon nor showFamilyIcon is set to true, the component renders nothing. This
|
||||||
behavior should be handled by the parent component.
|
behavior should be handled by the parent component.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type Args = {
|
|||||||
buttonText: string;
|
buttonText: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
buttonAction: (e: Event) => void;
|
handleClick: (e: Event) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -17,13 +17,13 @@ export default {
|
|||||||
buttonText: { control: "text" },
|
buttonText: { control: "text" },
|
||||||
disabled: { control: "boolean" },
|
disabled: { control: "boolean" },
|
||||||
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
||||||
buttonAction: { control: false },
|
handleClick: { control: false },
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
buttonText: "Click Me",
|
buttonText: "Click Me",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
theme: ThemeTypes.Light,
|
theme: ThemeTypes.Light,
|
||||||
buttonAction: () => alert("Clicked"),
|
handleClick: () => alert("Clicked"),
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
design: {
|
design: {
|
||||||
|
|||||||
@@ -6,22 +6,22 @@ import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.e
|
|||||||
import { CipherInfoIndicatorIcons } from "../../cipher/cipher-indicator-icons";
|
import { CipherInfoIndicatorIcons } from "../../cipher/cipher-indicator-icons";
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
isBusinessOrg?: boolean;
|
showBusinessIcon?: boolean;
|
||||||
isFamilyOrg?: boolean;
|
showFamilyIcon?: boolean;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Ciphers/Cipher Indicator Icon",
|
title: "Components/Ciphers/Cipher Indicator Icon",
|
||||||
argTypes: {
|
argTypes: {
|
||||||
isBusinessOrg: { control: "boolean" },
|
showBusinessIcon: { control: "boolean" },
|
||||||
isFamilyOrg: { control: "boolean" },
|
showFamilyIcon: { control: "boolean" },
|
||||||
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
theme: ThemeTypes.Light,
|
theme: ThemeTypes.Light,
|
||||||
isBusinessOrg: true,
|
showBusinessIcon: true,
|
||||||
isFamilyOrg: false,
|
showFamilyIcon: false,
|
||||||
},
|
},
|
||||||
} as Meta<Args>;
|
} as Meta<Args>;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||||
|
|
||||||
|
export const mockFolderData = [
|
||||||
|
{
|
||||||
|
id: "unique-id1",
|
||||||
|
name: "A folder",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unique-id2",
|
||||||
|
name: "Another folder",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unique-id3",
|
||||||
|
name: "One more folder",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unique-id4",
|
||||||
|
name: "Definitely not a folder",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unique-id5",
|
||||||
|
name: "Yet another folder",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unique-id6",
|
||||||
|
name: "Something else entirely, with an essence being completely unfolder-like in all the unimportant ways and none of the important ones",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unique-id7",
|
||||||
|
name: 'A "folder"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unique-id8",
|
||||||
|
name: "Two folders",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const mockOrganizationData = [
|
||||||
|
{
|
||||||
|
id: "unique-id0",
|
||||||
|
name: "Another personal vault",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unique-id1",
|
||||||
|
name: "Acme, inc",
|
||||||
|
productTierType: ProductTierType.Teams,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unique-id2",
|
||||||
|
name: "A Really Long Business Name That Just Kinda Goes On For A Really Long Time",
|
||||||
|
productTierType: ProductTierType.TeamsStarter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unique-id3",
|
||||||
|
name: "Family Vault",
|
||||||
|
productTierType: ProductTierType.Families,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unique-id4",
|
||||||
|
name: "Family Vault Trial",
|
||||||
|
productTierType: ProductTierType.Free,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unique-id5",
|
||||||
|
name: "Exciting Enterprises, LLC",
|
||||||
|
productTierType: ProductTierType.Enterprise,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -1,33 +1,29 @@
|
|||||||
import { Meta, StoryObj } from "@storybook/web-components";
|
import { Meta, StoryObj } from "@storybook/web-components";
|
||||||
|
import { html } from "lit";
|
||||||
|
|
||||||
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||||
|
|
||||||
import { NotificationType } from "../../../../notification/abstractions/notification-bar";
|
import { NotificationFooter, NotificationFooterProps } from "../../notification/footer";
|
||||||
import { NotificationFooter } from "../../notification/footer";
|
import { mockFolderData, mockOrganizationData } from "../mock-data";
|
||||||
|
|
||||||
type Args = {
|
|
||||||
notificationType: NotificationType;
|
|
||||||
theme: Theme;
|
|
||||||
handleSaveAction: (e: Event) => void;
|
|
||||||
i18n: { [key: string]: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Notifications/Notification Footer",
|
title: "Components/Notifications/Notification Footer",
|
||||||
argTypes: {
|
argTypes: {
|
||||||
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
|
||||||
notificationType: {
|
notificationType: {
|
||||||
control: "select",
|
control: "select",
|
||||||
options: ["add", "change", "unlock", "fileless-import"],
|
options: ["add", "change", "unlock", "fileless-import"],
|
||||||
},
|
},
|
||||||
|
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
theme: ThemeTypes.Light,
|
folders: mockFolderData,
|
||||||
notificationType: "add",
|
|
||||||
i18n: {
|
i18n: {
|
||||||
saveAsNewLoginAction: "Save as New Login",
|
|
||||||
saveAction: "Save",
|
saveAction: "Save",
|
||||||
|
saveAsNewLoginAction: "Save as New Login",
|
||||||
},
|
},
|
||||||
|
notificationType: "add",
|
||||||
|
organizations: mockOrganizationData,
|
||||||
|
theme: ThemeTypes.Light,
|
||||||
handleSaveAction: () => alert("Save action triggered"),
|
handleSaveAction: () => alert("Save action triggered"),
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
@@ -36,10 +32,11 @@ export default {
|
|||||||
url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=32-4949&m=dev",
|
url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=32-4949&m=dev",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as Meta<Args>;
|
} as Meta<NotificationFooterProps>;
|
||||||
|
|
||||||
const Template = (args: Args) => NotificationFooter({ ...args });
|
const Template = (args: NotificationFooterProps) =>
|
||||||
|
html`<div style="max-width:400px;">${NotificationFooter({ ...args })}</div>`;
|
||||||
|
|
||||||
export const Default: StoryObj<Args> = {
|
export const Default: StoryObj<NotificationFooterProps> = {
|
||||||
render: Template,
|
render: Template,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { Meta, StoryObj } from "@storybook/web-components";
|
||||||
|
import { html } from "lit";
|
||||||
|
|
||||||
|
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||||
|
|
||||||
|
import { Option } from "../../common-types";
|
||||||
|
import { themes } from "../../constants/styles";
|
||||||
|
import { User, Business } from "../../icons";
|
||||||
|
import "../../option-selection/option-selection";
|
||||||
|
import { mockOrganizationData } from "../mock-data";
|
||||||
|
|
||||||
|
const mockOptions: Option[] = [
|
||||||
|
{ icon: User, text: "My Vault", value: "0" },
|
||||||
|
...mockOrganizationData.map(({ id, name }) => ({ icon: Business, text: name, value: id })),
|
||||||
|
];
|
||||||
|
|
||||||
|
type ComponentProps = {
|
||||||
|
disabled?: boolean;
|
||||||
|
label?: string;
|
||||||
|
options: Option[];
|
||||||
|
theme: Theme;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Components/Option Selection",
|
||||||
|
argTypes: {
|
||||||
|
disabled: { control: "boolean" },
|
||||||
|
label: { control: "text" },
|
||||||
|
options: { control: "object" },
|
||||||
|
theme: { control: "select", options: [ThemeTypes.Light, ThemeTypes.Dark] },
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
disabled: false,
|
||||||
|
label: undefined,
|
||||||
|
options: mockOptions,
|
||||||
|
theme: ThemeTypes.Light,
|
||||||
|
},
|
||||||
|
} as Meta<ComponentProps>;
|
||||||
|
|
||||||
|
const BaseComponent = ({ disabled, label, options, theme }: ComponentProps) => {
|
||||||
|
return html`
|
||||||
|
<option-selection
|
||||||
|
.disabled=${disabled}
|
||||||
|
.label="${label}"
|
||||||
|
.options=${options}
|
||||||
|
theme=${theme}
|
||||||
|
></option-selection>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Light: StoryObj<ComponentProps> = {
|
||||||
|
render: BaseComponent,
|
||||||
|
argTypes: {
|
||||||
|
theme: { control: "radio", options: [ThemeTypes.Light] },
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
theme: ThemeTypes.Light,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
backgrounds: {
|
||||||
|
values: [{ name: "Light", value: themes.light.background.alt }],
|
||||||
|
default: "Light",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Dark: StoryObj<ComponentProps> = {
|
||||||
|
render: BaseComponent,
|
||||||
|
argTypes: {
|
||||||
|
theme: { control: "radio", options: [ThemeTypes.Dark] },
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
theme: ThemeTypes.Dark,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
backgrounds: {
|
||||||
|
values: [{ name: "Dark", value: themes.dark.background.alt }],
|
||||||
|
default: "Dark",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,29 +1,53 @@
|
|||||||
import { Meta, StoryObj } from "@storybook/web-components";
|
import { Meta, StoryObj } from "@storybook/web-components";
|
||||||
|
|
||||||
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||||
|
|
||||||
import { ButtonRow } from "../../rows/button-row";
|
import { themes } from "../../constants/styles";
|
||||||
|
import { ButtonRow, ButtonRowProps } from "../../rows/button-row";
|
||||||
type Args = {
|
|
||||||
theme: Theme;
|
|
||||||
buttonAction: (e: Event) => void;
|
|
||||||
buttonText: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Rows/Button Row",
|
title: "Components/Rows/Button Row",
|
||||||
|
argTypes: {},
|
||||||
|
args: {
|
||||||
|
primaryButton: {
|
||||||
|
text: "Action",
|
||||||
|
handlePrimaryButtonClick: (e: Event) => {
|
||||||
|
window.alert("Button clicked!");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Meta<ButtonRowProps>;
|
||||||
|
|
||||||
|
const Component = (args: ButtonRowProps) => ButtonRow({ ...args });
|
||||||
|
|
||||||
|
export const Light: StoryObj<ButtonRowProps> = {
|
||||||
|
render: Component,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
theme: { control: "radio", options: [ThemeTypes.Light] },
|
||||||
buttonText: { control: "text" },
|
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
theme: ThemeTypes.Light,
|
theme: ThemeTypes.Light,
|
||||||
buttonText: "Action",
|
|
||||||
},
|
},
|
||||||
} as Meta<Args>;
|
parameters: {
|
||||||
|
backgrounds: {
|
||||||
const Template = (args: Args) => ButtonRow({ ...args });
|
values: [{ name: "Light", value: themes.light.background.alt }],
|
||||||
|
default: "Light",
|
||||||
export const Default: StoryObj<Args> = {
|
},
|
||||||
render: Template,
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Dark: StoryObj<ButtonRowProps> = {
|
||||||
|
render: Component,
|
||||||
|
argTypes: {
|
||||||
|
theme: { control: "radio", options: [ThemeTypes.Dark] },
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
theme: ThemeTypes.Dark,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
backgrounds: {
|
||||||
|
values: [{ name: "Dark", value: themes.dark.background.alt }],
|
||||||
|
default: "Dark",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { html } from "lit";
|
||||||
|
|
||||||
|
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||||
|
import { Theme } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
|
import { Option, OrgView, FolderView } from "../common-types";
|
||||||
|
import { Business, Family, Folder, User } from "../icons";
|
||||||
|
import { ButtonRow } from "../rows/button-row";
|
||||||
|
|
||||||
|
function getVaultIconByProductTier(productTierType?: ProductTierType): Option["icon"] {
|
||||||
|
switch (productTierType) {
|
||||||
|
case ProductTierType.Free:
|
||||||
|
case ProductTierType.Families:
|
||||||
|
return Family;
|
||||||
|
case ProductTierType.Teams:
|
||||||
|
case ProductTierType.Enterprise:
|
||||||
|
case ProductTierType.TeamsStarter:
|
||||||
|
return Business;
|
||||||
|
default:
|
||||||
|
return User;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotificationButtonRowProps = {
|
||||||
|
theme: Theme;
|
||||||
|
primaryButton: {
|
||||||
|
text: string;
|
||||||
|
handlePrimaryButtonClick: (args: any) => void;
|
||||||
|
};
|
||||||
|
folders?: FolderView[];
|
||||||
|
organizations?: OrgView[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NotificationButtonRow({
|
||||||
|
folders,
|
||||||
|
organizations,
|
||||||
|
primaryButton,
|
||||||
|
theme,
|
||||||
|
}: NotificationButtonRowProps) {
|
||||||
|
const currentUserVaultOption: Option = {
|
||||||
|
icon: User,
|
||||||
|
default: true,
|
||||||
|
text: "My vault", // @TODO localize
|
||||||
|
value: "0",
|
||||||
|
};
|
||||||
|
const organizationOptions: Option[] = organizations?.length
|
||||||
|
? organizations.reduce(
|
||||||
|
(options, { id, name, productTierType }: OrgView) => {
|
||||||
|
const icon = getVaultIconByProductTier(productTierType);
|
||||||
|
return [
|
||||||
|
...options,
|
||||||
|
{
|
||||||
|
icon,
|
||||||
|
text: name,
|
||||||
|
value: id,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
[currentUserVaultOption],
|
||||||
|
)
|
||||||
|
: ([] as Option[]);
|
||||||
|
|
||||||
|
const noFolderOption: Option = {
|
||||||
|
default: true,
|
||||||
|
icon: Folder,
|
||||||
|
text: "No folder", // @TODO localize
|
||||||
|
value: "0",
|
||||||
|
};
|
||||||
|
const folderOptions: Option[] = folders?.length
|
||||||
|
? folders.reduce(
|
||||||
|
(options, { id, name }: FolderView) => [
|
||||||
|
...options,
|
||||||
|
{
|
||||||
|
icon: Folder,
|
||||||
|
text: name,
|
||||||
|
value: id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[noFolderOption],
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${ButtonRow({
|
||||||
|
theme,
|
||||||
|
primaryButton,
|
||||||
|
selectButtons: [
|
||||||
|
...(organizationOptions.length > 1
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "organization",
|
||||||
|
label: "Vault", // @TODO localize
|
||||||
|
options: organizationOptions,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(folderOptions.length > 1
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "folder",
|
||||||
|
label: "Folder", // @TODO localize
|
||||||
|
options: folderOptions,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -7,27 +7,43 @@ import {
|
|||||||
NotificationType,
|
NotificationType,
|
||||||
NotificationTypes,
|
NotificationTypes,
|
||||||
} from "../../../notification/abstractions/notification-bar";
|
} from "../../../notification/abstractions/notification-bar";
|
||||||
|
import { OrgView, FolderView } from "../common-types";
|
||||||
import { spacing, themes } from "../constants/styles";
|
import { spacing, themes } from "../constants/styles";
|
||||||
import { ButtonRow } from "../rows/button-row";
|
|
||||||
|
|
||||||
export function NotificationFooter({
|
import { NotificationButtonRow } from "./button-row";
|
||||||
handleSaveAction,
|
|
||||||
notificationType,
|
export type NotificationFooterProps = {
|
||||||
theme,
|
folders?: FolderView[];
|
||||||
i18n,
|
|
||||||
}: {
|
|
||||||
handleSaveAction: (e: Event) => void;
|
|
||||||
i18n: { [key: string]: string };
|
i18n: { [key: string]: string };
|
||||||
notificationType?: NotificationType;
|
notificationType?: NotificationType;
|
||||||
|
organizations?: OrgView[];
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
}) {
|
handleSaveAction: (e: Event) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NotificationFooter({
|
||||||
|
folders,
|
||||||
|
i18n,
|
||||||
|
notificationType,
|
||||||
|
organizations,
|
||||||
|
theme,
|
||||||
|
handleSaveAction,
|
||||||
|
}: NotificationFooterProps) {
|
||||||
const isChangeNotification = notificationType === NotificationTypes.Change;
|
const isChangeNotification = notificationType === NotificationTypes.Change;
|
||||||
const buttonText = i18n.saveAction;
|
const primaryButtonText = i18n.saveAction;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class=${notificationFooterStyles({ theme })}>
|
<div class=${notificationFooterStyles({ theme })}>
|
||||||
${!isChangeNotification
|
${!isChangeNotification
|
||||||
? ButtonRow({ theme, buttonAction: handleSaveAction, buttonText })
|
? NotificationButtonRow({
|
||||||
|
folders,
|
||||||
|
organizations,
|
||||||
|
primaryButton: {
|
||||||
|
handlePrimaryButtonClick: handleSaveAction,
|
||||||
|
text: primaryButtonText,
|
||||||
|
},
|
||||||
|
theme,
|
||||||
|
})
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import createEmotion from "@emotion/css/create-instance";
|
||||||
|
import { html, nothing } from "lit";
|
||||||
|
|
||||||
|
import { Theme } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
|
import { IconProps, Option } from "../common-types";
|
||||||
|
import { themes, spacing } from "../constants/styles";
|
||||||
|
|
||||||
|
export const optionItemTagName = "option-item";
|
||||||
|
|
||||||
|
const { css } = createEmotion({
|
||||||
|
key: optionItemTagName,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function OptionItem({
|
||||||
|
icon,
|
||||||
|
text,
|
||||||
|
value,
|
||||||
|
theme,
|
||||||
|
handleSelection,
|
||||||
|
}: Option & {
|
||||||
|
theme: Theme;
|
||||||
|
handleSelection: () => void;
|
||||||
|
}) {
|
||||||
|
const handleSelectionKeyUpProxy = (event: KeyboardEvent) => {
|
||||||
|
const listenedForKeys = new Set(["Enter", "Space"]);
|
||||||
|
if (listenedForKeys.has(event.code) && event.target instanceof Element) {
|
||||||
|
handleSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconProps: IconProps = { color: themes[theme].text.main, theme };
|
||||||
|
const itemIcon = icon?.(iconProps);
|
||||||
|
|
||||||
|
return html`<div
|
||||||
|
class=${optionItemStyles}
|
||||||
|
key=${value}
|
||||||
|
tabindex="0"
|
||||||
|
title=${text}
|
||||||
|
@click=${handleSelection}
|
||||||
|
@keyup=${handleSelectionKeyUpProxy}
|
||||||
|
>
|
||||||
|
${itemIcon ? html`<div class=${optionItemIconContainerStyles}>${itemIcon}</div>` : nothing}
|
||||||
|
<span class=${optionItemTextStyles}>${text || value}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const optionItemIconWidth = 16;
|
||||||
|
const optionItemGap = spacing["2"];
|
||||||
|
|
||||||
|
const optionItemStyles = css`
|
||||||
|
gap: ${optionItemGap};
|
||||||
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const optionItemIconContainerStyles = css`
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
width: ${optionItemIconWidth}px;
|
||||||
|
height: ${optionItemIconWidth}px;
|
||||||
|
|
||||||
|
> svg {
|
||||||
|
width: 100%;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const optionItemTextStyles = css`
|
||||||
|
flex: 1 1 calc(100% - ${optionItemIconWidth}px - ${optionItemGap});
|
||||||
|
overflow-x: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import createEmotion from "@emotion/css/create-instance";
|
||||||
|
import { html, nothing } from "lit";
|
||||||
|
|
||||||
|
import { Theme } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
|
import { Option } from "../common-types";
|
||||||
|
import { themes, typography, scrollbarStyles, spacing } from "../constants/styles";
|
||||||
|
|
||||||
|
import { OptionItem, optionItemTagName } from "./option-item";
|
||||||
|
|
||||||
|
export const optionItemsTagName = "option-items";
|
||||||
|
|
||||||
|
const { css } = createEmotion({
|
||||||
|
key: optionItemsTagName,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function OptionItems({
|
||||||
|
theme,
|
||||||
|
topOffset,
|
||||||
|
label,
|
||||||
|
options,
|
||||||
|
handleOptionSelection,
|
||||||
|
}: {
|
||||||
|
theme: Theme;
|
||||||
|
topOffset: number;
|
||||||
|
label?: string;
|
||||||
|
options: Option[];
|
||||||
|
handleOptionSelection: (selectedOption: Option) => void;
|
||||||
|
}) {
|
||||||
|
// @TODO get client vendor from context
|
||||||
|
const isSafari = false;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class=${optionsStyles({ theme, topOffset })} key="container">
|
||||||
|
${label ? html`<div class=${optionsLabelStyles({ theme })}>${label}</div>` : nothing}
|
||||||
|
<div class=${optionsWrapper({ isSafari, theme })}>
|
||||||
|
${options.map((option) =>
|
||||||
|
OptionItem({ ...option, theme, handleSelection: () => handleOptionSelection(option) }),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionsStyles = ({ theme, topOffset }: { theme: Theme; topOffset: number }) => css`
|
||||||
|
${typography.body1}
|
||||||
|
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
position: absolute;
|
||||||
|
/* top offset + line-height of dropdown button + top and bottom padding of button + border-width */
|
||||||
|
top: calc(${topOffset}px + 20px + ${spacing["1"]} + ${spacing["1"]} + 1px);
|
||||||
|
border: 1px solid ${themes[theme].secondary["500"]};
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background-clip: padding-box;
|
||||||
|
background-color: ${themes[theme].background.DEFAULT};
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
max-width: fit-content;
|
||||||
|
overflow-y: hidden;
|
||||||
|
color: ${themes[theme].text.main};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const optionsLabelStyles = ({ theme }: { theme: Theme }) => css`
|
||||||
|
${typography.helperMedium}
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
padding: 0.375rem ${spacing["3"]};
|
||||||
|
color: ${themes[theme].text.muted};
|
||||||
|
font-weight: 600;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const optionsMenuItemMaxWidth = 260;
|
||||||
|
export const optionsMenuItemsMaxHeight = 114;
|
||||||
|
|
||||||
|
const optionsWrapper = ({ isSafari, theme }: { isSafari: boolean; theme: Theme }) => css`
|
||||||
|
max-height: ${optionsMenuItemsMaxHeight}px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
> [class*="${optionItemTagName}-"] {
|
||||||
|
padding: ${spacing["1.5"]} ${spacing["3"]};
|
||||||
|
max-width: ${optionsMenuItemMaxWidth}px;
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
background-color: ${themes[theme].primary["100"]};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${isSafari
|
||||||
|
? scrollbarStyles(theme, { track: "transparent" }).safari
|
||||||
|
: scrollbarStyles(theme, { track: "transparent" }).default}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import createEmotion from "@emotion/css/create-instance";
|
||||||
|
import { html, LitElement, nothing } from "lit";
|
||||||
|
import { property, state } from "lit/decorators.js";
|
||||||
|
|
||||||
|
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
|
import { OptionSelectionButton } from "../buttons/option-selection-button";
|
||||||
|
import { Option } from "../common-types";
|
||||||
|
|
||||||
|
import { optionItemIconWidth } from "./option-item";
|
||||||
|
import { OptionItems, optionsMenuItemMaxWidth } from "./option-items";
|
||||||
|
|
||||||
|
export const optionSelectionTagName = "option-selection";
|
||||||
|
|
||||||
|
const { css } = createEmotion({
|
||||||
|
key: optionSelectionTagName,
|
||||||
|
});
|
||||||
|
|
||||||
|
export class OptionSelection extends LitElement {
|
||||||
|
@property()
|
||||||
|
disabled: boolean = false;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
label?: string;
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
options: Option[] = [];
|
||||||
|
|
||||||
|
@property()
|
||||||
|
theme: Theme = ThemeTypes.Light;
|
||||||
|
|
||||||
|
@property({ type: (selectedOption: Option["value"]) => selectedOption })
|
||||||
|
handleSelectionUpdate?: (args: any) => void;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private showMenu = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private menuTopOffset: number = 0;
|
||||||
|
|
||||||
|
// Determines if the opened menu should be "anchored" to the right or left side of the opening button
|
||||||
|
@state()
|
||||||
|
private menuIsEndJustified: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private selection?: Option;
|
||||||
|
|
||||||
|
private handleButtonClick = (event: Event) => {
|
||||||
|
if (!this.disabled) {
|
||||||
|
// Menu is about to be shown
|
||||||
|
if (!this.showMenu) {
|
||||||
|
this.menuTopOffset = this.offsetTop;
|
||||||
|
|
||||||
|
// Distance from right edge of button to left edge of the viewport
|
||||||
|
// Assumes no enclosing frames between the intended host frame and the component
|
||||||
|
const boundingClientRect = this.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Width of the client (minus scrollbar)
|
||||||
|
const documentWidth = document.documentElement.clientWidth;
|
||||||
|
|
||||||
|
// Distance between left edge of the button and right edge of the viewport
|
||||||
|
// (e.g. the max space the menu can use when left-aligned)
|
||||||
|
const distanceFromViewportRightEdge = documentWidth - boundingClientRect.left;
|
||||||
|
|
||||||
|
// The full width the option menu can take up
|
||||||
|
// (base + icon + border + gap + padding)
|
||||||
|
const maxDifferenceThreshold =
|
||||||
|
optionsMenuItemMaxWidth + optionItemIconWidth + 2 + 8 + 12 * 2;
|
||||||
|
|
||||||
|
this.menuIsEndJustified = distanceFromViewportRightEdge < maxDifferenceThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showMenu = !this.showMenu;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleOptionSelection = (selectedOption: Option) => {
|
||||||
|
this.showMenu = false;
|
||||||
|
this.selection = selectedOption;
|
||||||
|
|
||||||
|
// Any side-effects that should occur from the selection
|
||||||
|
this.handleSelectionUpdate?.(selectedOption.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
protected createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.selection) {
|
||||||
|
this.selection = getDefaultOption(this.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class=${optionSelectionStyles({ menuIsEndJustified: this.menuIsEndJustified })}>
|
||||||
|
${OptionSelectionButton({
|
||||||
|
disabled: this.disabled,
|
||||||
|
icon: this.selection?.icon,
|
||||||
|
text: this.selection?.text,
|
||||||
|
theme: this.theme,
|
||||||
|
toggledOn: this.showMenu,
|
||||||
|
handleButtonClick: this.handleButtonClick,
|
||||||
|
})}
|
||||||
|
${this.showMenu
|
||||||
|
? OptionItems({
|
||||||
|
label: this.label,
|
||||||
|
options: this.options,
|
||||||
|
theme: this.theme,
|
||||||
|
topOffset: this.menuTopOffset,
|
||||||
|
handleOptionSelection: this.handleOptionSelection,
|
||||||
|
})
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
[optionSelectionTagName]: OptionSelection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default customElements.define(optionSelectionTagName, OptionSelection);
|
||||||
|
|
||||||
|
function getDefaultOption(options: Option[] = []) {
|
||||||
|
return options.find((option: Option) => option.default) || options[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionSelectionStyles = ({ menuIsEndJustified }: { menuIsEndJustified: boolean }) => css`
|
||||||
|
display: flex;
|
||||||
|
justify-content: ${menuIsEndJustified ? "flex-end" : "flex-start"};
|
||||||
|
|
||||||
|
> div,
|
||||||
|
> button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -1,54 +1,53 @@
|
|||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
import { html, TemplateResult } from "lit";
|
import { html, nothing } from "lit";
|
||||||
|
|
||||||
import { Theme } from "@bitwarden/common/platform/enums";
|
import { Theme } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
import { ActionButton } from "../../../content/components/buttons/action-button";
|
import { ActionButton } from "../../../content/components/buttons/action-button";
|
||||||
import { spacing, themes } from "../../../content/components/constants/styles";
|
import { spacing } from "../../../content/components/constants/styles";
|
||||||
import { Folder, User } from "../../../content/components/icons";
|
import { Option } from "../common-types";
|
||||||
import { DropdownMenu } from "../dropdown-menu";
|
import { optionSelectionTagName } from "../option-selection/option-selection";
|
||||||
|
|
||||||
export function ButtonRow({
|
export type ButtonRowProps = {
|
||||||
theme,
|
|
||||||
buttonAction,
|
|
||||||
buttonText,
|
|
||||||
}: {
|
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
buttonAction: (e: Event) => void;
|
primaryButton: {
|
||||||
buttonText: string;
|
text: string;
|
||||||
}) {
|
handlePrimaryButtonClick: (args: any) => void;
|
||||||
|
};
|
||||||
|
selectButtons?: {
|
||||||
|
id: string;
|
||||||
|
label?: string;
|
||||||
|
options: Option[];
|
||||||
|
handleSelectionUpdate?: (args: any) => void;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ButtonRow({ theme, primaryButton, selectButtons }: ButtonRowProps) {
|
||||||
return html`
|
return html`
|
||||||
<div class=${buttonRowStyles}>
|
<div class=${buttonRowStyles}>
|
||||||
${[
|
${ActionButton({
|
||||||
ActionButton({
|
handleClick: primaryButton.handlePrimaryButtonClick,
|
||||||
buttonAction: buttonAction,
|
buttonText: primaryButton.text,
|
||||||
buttonText,
|
theme,
|
||||||
theme,
|
})}
|
||||||
}),
|
<div class=${optionSelectionsStyles}>
|
||||||
DropdownContainer({
|
${selectButtons?.map(
|
||||||
children: [
|
({ id, label, options, handleSelectionUpdate }) =>
|
||||||
DropdownMenu({
|
html`
|
||||||
buttonText: "You",
|
<option-selection
|
||||||
icon: User({ color: themes[theme].text.muted, theme }),
|
key=${id}
|
||||||
theme,
|
theme=${theme}
|
||||||
}),
|
.label=${label}
|
||||||
DropdownMenu({
|
.options=${options}
|
||||||
buttonText: "Folder",
|
.handleSelectionUpdate=${handleSelectionUpdate}
|
||||||
icon: Folder({ color: themes[theme].text.muted, theme }),
|
></option-selection>
|
||||||
disabled: true,
|
` || nothing,
|
||||||
theme,
|
)}
|
||||||
}),
|
</div>
|
||||||
],
|
|
||||||
}),
|
|
||||||
]}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownContainer({ children }: { children: TemplateResult[] }) {
|
|
||||||
return html` <div class=${dropdownContainerStyles}>${children}</div> `;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonRowStyles = css`
|
const buttonRowStyles = css`
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -69,14 +68,16 @@ const buttonRowStyles = css`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const dropdownContainerStyles = css`
|
const optionSelectionsStyles = css`
|
||||||
gap: 8px;
|
gap: ${spacing["2"]};
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
> div {
|
> ${optionSelectionTagName} {
|
||||||
min-width: calc(50% - ${spacing["1.5"]});
|
/* assumes two option selections */
|
||||||
|
max-width: calc(50% - ${spacing["1.5"]});
|
||||||
|
min-width: 120px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user