mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +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";
|
||||
|
||||
export function ActionButton({
|
||||
buttonAction,
|
||||
buttonText,
|
||||
disabled = false,
|
||||
theme,
|
||||
handleClick,
|
||||
}: {
|
||||
buttonAction: (e: Event) => void;
|
||||
buttonText: string;
|
||||
disabled?: boolean;
|
||||
theme: Theme;
|
||||
handleClick: (e: Event) => void;
|
||||
}) {
|
||||
const handleButtonClick = (event: Event) => {
|
||||
if (!disabled) {
|
||||
buttonAction(event);
|
||||
handleClick(event);
|
||||
}
|
||||
};
|
||||
|
||||
return html`
|
||||
<button
|
||||
type="button"
|
||||
title=${buttonText}
|
||||
class=${actionButtonStyles({ disabled, theme })}
|
||||
title=${buttonText}
|
||||
type="button"
|
||||
@click=${handleButtonClick}
|
||||
>
|
||||
${buttonText}
|
||||
|
||||
@@ -23,9 +23,9 @@ export function EditButton({
|
||||
title=${buttonText}
|
||||
class=${editButtonStyles({ disabled, theme })}
|
||||
@click=${(event: Event) => {
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
!disabled && buttonAction(event);
|
||||
if (!disabled) {
|
||||
buttonAction(event);
|
||||
}
|
||||
}}
|
||||
>
|
||||
${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 support other indicator types (attachments, etc)
|
||||
export function CipherInfoIndicatorIcons({
|
||||
isBusinessOrg,
|
||||
isFamilyOrg,
|
||||
showBusinessIcon,
|
||||
showFamilyIcon,
|
||||
theme,
|
||||
}: {
|
||||
isBusinessOrg?: boolean;
|
||||
isFamilyOrg?: boolean;
|
||||
showBusinessIcon?: boolean;
|
||||
showFamilyIcon?: boolean;
|
||||
theme: Theme;
|
||||
}) {
|
||||
const indicatorIcons = [
|
||||
...(isBusinessOrg ? [Business({ color: themes[theme].text.muted, theme })] : []),
|
||||
...(isFamilyOrg ? [Family({ color: themes[theme].text.muted, theme })] : []),
|
||||
...(showBusinessIcon ? [Business({ color: themes[theme].text.muted, theme })] : []),
|
||||
...(showFamilyIcon ? [Family({ color: themes[theme].text.muted, theme })] : []),
|
||||
];
|
||||
|
||||
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};
|
||||
`;
|
||||
|
||||
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 {
|
||||
/* FireFox & Chrome support */
|
||||
default: `
|
||||
/* FireFox & Chrome support */
|
||||
scrollbar-color: ${themes[theme].secondary["500"]} ${themes[theme].background.alt};
|
||||
scrollbar-color: ${thumbColor} ${trackColor};
|
||||
`,
|
||||
/* Safari Support */
|
||||
safari: `
|
||||
/* Safari Support */
|
||||
::-webkit-scrollbar {
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -191,10 +194,10 @@ export function scrollbarStyles(theme: Theme) {
|
||||
border-radius: 0.5rem;
|
||||
border-color: transparent;
|
||||
background-clip: content-box;
|
||||
background-color: ${themes[theme].secondary["500"]};
|
||||
background-color: ${thumbColor};
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
${themes[theme].background.alt};
|
||||
${trackColor};
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
${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 { html } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { IconProps } from "../common-types";
|
||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||
|
||||
export function AngleDown({
|
||||
color,
|
||||
disabled,
|
||||
theme,
|
||||
}: {
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
theme: Theme;
|
||||
}) {
|
||||
export function AngleDown({ color, disabled, theme }: IconProps) {
|
||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||
|
||||
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 { html } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { IconProps } from "../common-types";
|
||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||
|
||||
export function Business({
|
||||
color,
|
||||
disabled,
|
||||
theme,
|
||||
}: {
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
theme: Theme;
|
||||
}) {
|
||||
export function Business({ color, disabled, theme }: IconProps) {
|
||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||
|
||||
return html`
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { html } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { IconProps } from "../common-types";
|
||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||
|
||||
export function Close({
|
||||
color,
|
||||
disabled,
|
||||
theme,
|
||||
}: {
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
theme: Theme;
|
||||
}) {
|
||||
export function Close({ color, disabled, theme }: IconProps) {
|
||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||
|
||||
return html`
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { html } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { IconProps } from "../common-types";
|
||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||
|
||||
export function ExclamationTriangle({
|
||||
color,
|
||||
disabled,
|
||||
theme,
|
||||
}: {
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
theme: Theme;
|
||||
}) {
|
||||
export function ExclamationTriangle({ color, disabled, theme }: IconProps) {
|
||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||
|
||||
return html`
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { html } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { IconProps } from "../common-types";
|
||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||
|
||||
export function Family({
|
||||
color,
|
||||
disabled,
|
||||
theme,
|
||||
}: {
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
theme: Theme;
|
||||
}) {
|
||||
export function Family({ color, disabled, theme }: IconProps) {
|
||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||
|
||||
return html`
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { html } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { IconProps } from "../common-types";
|
||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||
|
||||
export function Folder({
|
||||
color,
|
||||
disabled,
|
||||
theme,
|
||||
}: {
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
theme: Theme;
|
||||
}) {
|
||||
export function Folder({ 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 22" fill="none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" fill="none">
|
||||
<path
|
||||
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>
|
||||
`;
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { html } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { IconProps } from "../common-types";
|
||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||
|
||||
export function Globe({
|
||||
color,
|
||||
disabled,
|
||||
theme,
|
||||
}: {
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
theme: Theme;
|
||||
}) {
|
||||
export function Globe({ color, disabled, theme }: IconProps) {
|
||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||
|
||||
return html`
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { AngleDown } from "./angle-down";
|
||||
export { AngleUp } from "./angle-up";
|
||||
export { BrandIconContainer } from "./brand-icon-container";
|
||||
export { Business } from "./business";
|
||||
export { Close } from "./close";
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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
|
||||
export function PartyHorn({ theme }: { theme: Theme }) {
|
||||
export function PartyHorn({ theme }: IconProps) {
|
||||
if (theme === ThemeTypes.Dark) {
|
||||
return html`
|
||||
<svg
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { html } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { IconProps } from "../common-types";
|
||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||
|
||||
export function PencilSquare({
|
||||
color,
|
||||
disabled,
|
||||
theme,
|
||||
}: {
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
theme: Theme;
|
||||
}) {
|
||||
export function PencilSquare({ color, disabled, theme }: IconProps) {
|
||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||
|
||||
return html`
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { html } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { IconProps } from "../common-types";
|
||||
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;
|
||||
|
||||
return html`
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { html } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { IconProps } from "../common-types";
|
||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||
|
||||
export function User({
|
||||
color,
|
||||
disabled,
|
||||
theme,
|
||||
}: {
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
theme: Theme;
|
||||
}) {
|
||||
export function User({ 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 24" fill="none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" fill="none">
|
||||
<path
|
||||
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>
|
||||
`;
|
||||
|
||||
@@ -15,11 +15,11 @@ dynamically based on the provided theme.
|
||||
|
||||
## Props
|
||||
|
||||
| **Prop** | **Type** | **Required** | **Description** |
|
||||
| --------------- | --------- | ------------ | ----------------------------------------------------------------------- |
|
||||
| `isBusinessOrg` | `boolean` | No | Displays the business organization icon when set to `true`. |
|
||||
| `isFamilyOrg` | `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. |
|
||||
| **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
|
||||
|
||||
@@ -29,8 +29,8 @@ dynamically based on the provided theme.
|
||||
- `@emotion/css`: Used for styling.
|
||||
|
||||
2. Pass the required props when using the component:
|
||||
- `isBusinessOrg`: A boolean that, when `true`, displays the business icon.
|
||||
- `isFamilyOrg`: A boolean that, when `true`, displays the family icon.
|
||||
- `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
|
||||
@@ -57,8 +57,8 @@ import { CipherInfoIndicatorIcons } from "./cipher-info-indicator-icons";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||
|
||||
<CipherInfoIndicatorIcons
|
||||
isBusinessOrg={true}
|
||||
isFamilyOrg={false}
|
||||
showBusinessIcon={true}
|
||||
showFamilyIcon={false}
|
||||
theme={ThemeTypes.Dark}
|
||||
/>;
|
||||
```
|
||||
@@ -77,5 +77,5 @@ family organization icon.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -8,7 +8,7 @@ type Args = {
|
||||
buttonText: string;
|
||||
disabled: boolean;
|
||||
theme: Theme;
|
||||
buttonAction: (e: Event) => void;
|
||||
handleClick: (e: Event) => void;
|
||||
};
|
||||
|
||||
export default {
|
||||
@@ -17,13 +17,13 @@ export default {
|
||||
buttonText: { control: "text" },
|
||||
disabled: { control: "boolean" },
|
||||
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
||||
buttonAction: { control: false },
|
||||
handleClick: { control: false },
|
||||
},
|
||||
args: {
|
||||
buttonText: "Click Me",
|
||||
disabled: false,
|
||||
theme: ThemeTypes.Light,
|
||||
buttonAction: () => alert("Clicked"),
|
||||
handleClick: () => alert("Clicked"),
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
|
||||
@@ -6,22 +6,22 @@ import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.e
|
||||
import { CipherInfoIndicatorIcons } from "../../cipher/cipher-indicator-icons";
|
||||
|
||||
type Args = {
|
||||
isBusinessOrg?: boolean;
|
||||
isFamilyOrg?: boolean;
|
||||
showBusinessIcon?: boolean;
|
||||
showFamilyIcon?: boolean;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "Components/Ciphers/Cipher Indicator Icon",
|
||||
argTypes: {
|
||||
isBusinessOrg: { control: "boolean" },
|
||||
isFamilyOrg: { control: "boolean" },
|
||||
showBusinessIcon: { control: "boolean" },
|
||||
showFamilyIcon: { control: "boolean" },
|
||||
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
||||
},
|
||||
args: {
|
||||
theme: ThemeTypes.Light,
|
||||
isBusinessOrg: true,
|
||||
isFamilyOrg: false,
|
||||
showBusinessIcon: true,
|
||||
showFamilyIcon: false,
|
||||
},
|
||||
} 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 { 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 } from "../../notification/footer";
|
||||
|
||||
type Args = {
|
||||
notificationType: NotificationType;
|
||||
theme: Theme;
|
||||
handleSaveAction: (e: Event) => void;
|
||||
i18n: { [key: string]: string };
|
||||
};
|
||||
import { NotificationFooter, NotificationFooterProps } from "../../notification/footer";
|
||||
import { mockFolderData, mockOrganizationData } from "../mock-data";
|
||||
|
||||
export default {
|
||||
title: "Components/Notifications/Notification Footer",
|
||||
argTypes: {
|
||||
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
||||
notificationType: {
|
||||
control: "select",
|
||||
options: ["add", "change", "unlock", "fileless-import"],
|
||||
},
|
||||
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
||||
},
|
||||
args: {
|
||||
theme: ThemeTypes.Light,
|
||||
notificationType: "add",
|
||||
folders: mockFolderData,
|
||||
i18n: {
|
||||
saveAsNewLoginAction: "Save as New Login",
|
||||
saveAction: "Save",
|
||||
saveAsNewLoginAction: "Save as New Login",
|
||||
},
|
||||
notificationType: "add",
|
||||
organizations: mockOrganizationData,
|
||||
theme: ThemeTypes.Light,
|
||||
handleSaveAction: () => alert("Save action triggered"),
|
||||
},
|
||||
parameters: {
|
||||
@@ -36,10 +32,11 @@ export default {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -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 { 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";
|
||||
|
||||
type Args = {
|
||||
theme: Theme;
|
||||
buttonAction: (e: Event) => void;
|
||||
buttonText: string;
|
||||
};
|
||||
import { themes } from "../../constants/styles";
|
||||
import { ButtonRow, ButtonRowProps } from "../../rows/button-row";
|
||||
|
||||
export default {
|
||||
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: {
|
||||
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
||||
buttonText: { control: "text" },
|
||||
theme: { control: "radio", options: [ThemeTypes.Light] },
|
||||
},
|
||||
args: {
|
||||
theme: ThemeTypes.Light,
|
||||
buttonText: "Action",
|
||||
},
|
||||
} as Meta<Args>;
|
||||
|
||||
const Template = (args: Args) => ButtonRow({ ...args });
|
||||
|
||||
export const Default: StoryObj<Args> = {
|
||||
render: Template,
|
||||
parameters: {
|
||||
backgrounds: {
|
||||
values: [{ name: "Light", value: themes.light.background.alt }],
|
||||
default: "Light",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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,
|
||||
NotificationTypes,
|
||||
} from "../../../notification/abstractions/notification-bar";
|
||||
import { OrgView, FolderView } from "../common-types";
|
||||
import { spacing, themes } from "../constants/styles";
|
||||
import { ButtonRow } from "../rows/button-row";
|
||||
|
||||
export function NotificationFooter({
|
||||
handleSaveAction,
|
||||
notificationType,
|
||||
theme,
|
||||
i18n,
|
||||
}: {
|
||||
handleSaveAction: (e: Event) => void;
|
||||
import { NotificationButtonRow } from "./button-row";
|
||||
|
||||
export type NotificationFooterProps = {
|
||||
folders?: FolderView[];
|
||||
i18n: { [key: string]: string };
|
||||
notificationType?: NotificationType;
|
||||
organizations?: OrgView[];
|
||||
theme: Theme;
|
||||
}) {
|
||||
handleSaveAction: (e: Event) => void;
|
||||
};
|
||||
|
||||
export function NotificationFooter({
|
||||
folders,
|
||||
i18n,
|
||||
notificationType,
|
||||
organizations,
|
||||
theme,
|
||||
handleSaveAction,
|
||||
}: NotificationFooterProps) {
|
||||
const isChangeNotification = notificationType === NotificationTypes.Change;
|
||||
const buttonText = i18n.saveAction;
|
||||
const primaryButtonText = i18n.saveAction;
|
||||
|
||||
return html`
|
||||
<div class=${notificationFooterStyles({ theme })}>
|
||||
${!isChangeNotification
|
||||
? ButtonRow({ theme, buttonAction: handleSaveAction, buttonText })
|
||||
? NotificationButtonRow({
|
||||
folders,
|
||||
organizations,
|
||||
primaryButton: {
|
||||
handlePrimaryButtonClick: handleSaveAction,
|
||||
text: primaryButtonText,
|
||||
},
|
||||
theme,
|
||||
})
|
||||
: nothing}
|
||||
</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 { html, TemplateResult } from "lit";
|
||||
import { html, nothing } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { ActionButton } from "../../../content/components/buttons/action-button";
|
||||
import { spacing, themes } from "../../../content/components/constants/styles";
|
||||
import { Folder, User } from "../../../content/components/icons";
|
||||
import { DropdownMenu } from "../dropdown-menu";
|
||||
import { spacing } from "../../../content/components/constants/styles";
|
||||
import { Option } from "../common-types";
|
||||
import { optionSelectionTagName } from "../option-selection/option-selection";
|
||||
|
||||
export function ButtonRow({
|
||||
theme,
|
||||
buttonAction,
|
||||
buttonText,
|
||||
}: {
|
||||
export type ButtonRowProps = {
|
||||
theme: Theme;
|
||||
buttonAction: (e: Event) => void;
|
||||
buttonText: string;
|
||||
}) {
|
||||
primaryButton: {
|
||||
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`
|
||||
<div class=${buttonRowStyles}>
|
||||
${[
|
||||
ActionButton({
|
||||
buttonAction: buttonAction,
|
||||
buttonText,
|
||||
theme,
|
||||
}),
|
||||
DropdownContainer({
|
||||
children: [
|
||||
DropdownMenu({
|
||||
buttonText: "You",
|
||||
icon: User({ color: themes[theme].text.muted, theme }),
|
||||
theme,
|
||||
}),
|
||||
DropdownMenu({
|
||||
buttonText: "Folder",
|
||||
icon: Folder({ color: themes[theme].text.muted, theme }),
|
||||
disabled: true,
|
||||
theme,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]}
|
||||
${ActionButton({
|
||||
handleClick: primaryButton.handlePrimaryButtonClick,
|
||||
buttonText: primaryButton.text,
|
||||
theme,
|
||||
})}
|
||||
<div class=${optionSelectionsStyles}>
|
||||
${selectButtons?.map(
|
||||
({ id, label, options, handleSelectionUpdate }) =>
|
||||
html`
|
||||
<option-selection
|
||||
key=${id}
|
||||
theme=${theme}
|
||||
.label=${label}
|
||||
.options=${options}
|
||||
.handleSelectionUpdate=${handleSelectionUpdate}
|
||||
></option-selection>
|
||||
` || nothing,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function DropdownContainer({ children }: { children: TemplateResult[] }) {
|
||||
return html` <div class=${dropdownContainerStyles}>${children}</div> `;
|
||||
}
|
||||
|
||||
const buttonRowStyles = css`
|
||||
gap: 16px;
|
||||
display: flex;
|
||||
@@ -69,14 +68,16 @@ const buttonRowStyles = css`
|
||||
}
|
||||
`;
|
||||
|
||||
const dropdownContainerStyles = css`
|
||||
gap: 8px;
|
||||
const optionSelectionsStyles = css`
|
||||
gap: ${spacing["2"]};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
overflow: hidden;
|
||||
|
||||
> div {
|
||||
min-width: calc(50% - ${spacing["1.5"]});
|
||||
> ${optionSelectionTagName} {
|
||||
/* assumes two option selections */
|
||||
max-width: calc(50% - ${spacing["1.5"]});
|
||||
min-width: 120px;
|
||||
}
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user