Skip to content

Commit

Permalink
add appearance options to dropdown component
Browse files Browse the repository at this point in the history
  • Loading branch information
radium-v committed Dec 7, 2024
1 parent c823428 commit 8bfe90d
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 77 deletions.
14 changes: 14 additions & 0 deletions packages/web-components/src/dropdown/dropdown.options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ export function isDropdown(element?: Node | null, tagName: string = '-dropdown')
return (element as Element).tagName.toLowerCase().endsWith(tagName);
}

/**
* Values for the `appearance` attribute of the {@link (Dropdown:class)} component.
* @public
*/
export const DropdownAppearance = {
filledDarker: 'filled-darker',
filledLighter: 'filled-lighter',
outline: 'outline',
transparent: 'transparent',
};

/** @public */
export type DropdownAppearance = ValuesOf<typeof DropdownAppearance>;

/**
* Values for the `size` attribute of the {@link (Dropdown:class)} component.
* @public
Expand Down
50 changes: 40 additions & 10 deletions packages/web-components/src/dropdown/dropdown.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { html, repeat } from '@microsoft/fast-element';
import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js';
import type { Option as FluentOption } from '../option/option.js';
import type { Dropdown as FluentDropdown } from './dropdown.js';
import { DropdownSize } from './dropdown.options.js';
import { DropdownAppearance, DropdownSize, DropdownType } from './dropdown.options.js';

type Story = StoryObj<FluentDropdown>;

const optionTemplate = html<StoryArgs<FluentOption>>` <fluent-option
?disabled="${x => x.disabled}"
?selected="${x => x.selected}"
:value="${x => x.value}"
value="${x => x.value}"
placeholder="${x => x.placeholder}"
>${x => x.slottedContent?.()}</fluent-option
>`;
Expand All @@ -29,16 +29,18 @@ const dropdownTemplate = html<StoryArgs<FluentDropdown>>`
`;

const storyTemplate = html<StoryArgs<FluentDropdown>>`
<fluent-field>
<label slot="label">Fruit</label>
${dropdownTemplate}
</fluent-field>
<fluent-field><label slot="label">Fruit</label>${dropdownTemplate}</fluent-field>
`;

export default {
title: 'Components/Dropdown',
render: renderComponent(storyTemplate),
argTypes: {
appearance: {
control: 'select',
options: ['', ...Object.values(DropdownAppearance)],
table: { category: 'attributes' },
},
type: {
control: 'radio',
options: Object.values(DropdownType),
Expand All @@ -61,7 +63,7 @@ export default {
},
} as Meta<FluentDropdown>;

export const Dropdown: Story = {
export const Default: Story = {
args: {
placeholder: 'Select a fruit',
type: DropdownType.dropdown,
Expand All @@ -81,22 +83,50 @@ export const Dropdown: Story = {

export const MultipleSelection: Story = {
args: {
...Dropdown.args,
...Default.args,
multiple: true,
placeholder: 'Select fruits',
},
};

export const Small: Story = {
args: {
...Dropdown.args,
...Default.args,
size: DropdownSize.small,
},
};

export const Large: Story = {
args: {
...Dropdown.args,
...Default.args,
size: DropdownSize.large,
},
};

export const FilledLighter: Story = {
args: {
...Default.args,
appearance: DropdownAppearance.filledLighter,
},
};

export const FilledDarker: Story = {
args: {
...Default.args,
appearance: DropdownAppearance.filledDarker,
},
};

export const Outline: Story = {
args: {
...Default.args,
appearance: DropdownAppearance.outline,
},
};

export const Transparent: Story = {
args: {
...Default.args,
appearance: DropdownAppearance.transparent,
},
};
186 changes: 123 additions & 63 deletions packages/web-components/src/dropdown/dropdown.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,37 @@ import {
typographyBody2Styles,
typographyCaption1Styles,
} from '../styles/partials/typography.partials.js';
import { largeState, openState, smallState } from '../styles/states/index.js';
import {
filledDarkerState,
filledLighterState,
largeState,
openState,
outlineState,
placeholderShownState,
smallState,
transparentState,
} from '../styles/states/index.js';
import {
borderRadiusMedium,
borderRadiusNone,
colorCompoundBrandBackgroundHover,
colorCompoundBrandBackgroundPressed,
colorCompoundBrandStroke,
colorNeutralBackground1,
colorNeutralBackground3,
colorNeutralForeground1,
colorNeutralForeground3,
colorNeutralForeground4,
colorNeutralStroke1,
colorNeutralStroke1Hover,
colorNeutralStroke1Pressed,
colorNeutralStrokeAccessible,
colorNeutralStrokeAccessibleHover,
colorNeutralStrokeAccessiblePressed,
colorTransparentBackground,
colorTransparentStroke,
colorTransparentStrokeInteractive,
curveAccelerateMax,
curveAccelerateMid,
curveDecelerateMid,
durationNormal,
Expand Down Expand Up @@ -49,41 +67,11 @@ export const styles = css`
position: relative;
}
.popover {
background: transparent;
border: none;
box-sizing: border-box;
display: flex;
flex-direction: column;
inset: unset;
margin: 0;
min-width: 160px;
overflow: visible;
padding: 0;
position: absolute;
z-index: 1;
}
.popover:not(:popover-open) {
display: none;
}
.popover:popover-open {
position-anchor: --dropdown-trigger;
position-area: block-end span-inline-end;
position-try-fallbacks: flip-inline, flip-block, block-start;
width: anchor-size(width);
}
@supports not (position-anchor: --dropdown-trigger) {
.popover:popover-open {
margin-block-start: 32px;
}
:host(${placeholderShownState}) {
color: ${colorNeutralForeground4};
}
.control {
--bottom-border-color: ${colorNeutralStrokeAccessible};
--control-border-color: ${colorNeutralStroke1};
align-items: center;
anchor-name: --dropdown-trigger;
appearance: none;
Expand Down Expand Up @@ -118,6 +106,38 @@ export const styles = css`
${typographyBody2Styles}
}
.popover {
background: transparent;
border: none;
box-sizing: border-box;
display: flex;
flex-direction: column;
inset: unset;
margin: 0;
min-width: 160px;
overflow: visible;
padding: 0;
position: absolute;
z-index: 1;
}
.popover:not(:popover-open) {
display: none;
}
.popover:popover-open {
position-anchor: --dropdown-trigger;
position-area: block-end span-inline-end;
position-try-fallbacks: flip-inline, flip-block, block-start;
width: anchor-size(width);
}
@supports not (position-anchor: --dropdown-trigger) {
.popover:popover-open {
margin-block-start: 32px;
}
}
::slotted(:is(input, button)) {
all: unset;
cursor: default;
Expand All @@ -128,25 +148,34 @@ export const styles = css`
cursor: pointer;
}
.control:hover {
--control-border-color: ${colorNeutralStroke1Hover};
::slotted([slot='indicator']) {
all: unset;
align-items: center;
appearance: none;
aspect-ratio: 1;
color: ${colorNeutralForeground3};
display: inline-flex;
justify-content: center;
width: 20px;
}
.control:active {
--control-border-color: ${colorNeutralStroke1Pressed};
:host(${smallState}) ::slotted([slot='indicator']) {
width: 16px;
}
:host(${largeState}) ::slotted([slot='indicator']) {
width: 24px;
}
.control::after,
.control::before {
content: '' / '';
height: 100%;
inset: auto 0 0;
pointer-events: none;
position: absolute;
}
.control::before {
background-color: var(--bottom-border-color);
height: ${strokeWidthThin};
}
Expand All @@ -157,45 +186,76 @@ export const styles = css`
transition: scale ${durationUltraFast} ${curveDecelerateMid};
}
.control:hover::before {
:host(:has(button:active)) .control::after {
scale: 0.5 1;
transition-timing-function: ${curveAccelerateMax};
}
:host(:where(${openState}, :focus-within)) .control::after {
scale: 1 1;
transition-duration: ${durationNormal};
transition-timing-function: ${curveAccelerateMid};
}
:host(:where(${outlineState}, ${transparentState})) .control::before {
background-color: ${colorNeutralStrokeAccessible};
}
:host(${transparentState}) .control {
--control-border-color: ${colorTransparentStrokeInteractive};
background: ${colorTransparentBackground};
border-radius: ${borderRadiusNone};
}
:host(${transparentState}) .control:hover::before {
background-color: ${colorNeutralStrokeAccessibleHover};
}
.control:hover::after {
background-color: ${colorCompoundBrandBackgroundHover};
:host(${transparentState}) .control:active::before {
background-color: ${colorNeutralStrokeAccessiblePressed};
}
.control:active::before {
:host(${outlineState}) .control:active::before {
background-color: ${colorNeutralStrokeAccessiblePressed};
}
.control:active::after {
scale: 0.5 1;
:host(${outlineState}) .control {
--control-border-color: ${colorNeutralStroke1};
}
:host(:is(${openState}, :focus-within)) .control::after {
scale: 1 1;
transition-duration: ${durationNormal};
transition-timing-function: ${curveAccelerateMid};
:host(${outlineState}) .control:hover {
--control-border-color: ${colorNeutralStroke1Hover};
}
::slotted([slot='indicator']) {
all: unset;
appearance: none;
border: none;
background: transparent;
display: inline-flex;
justify-content: center;
align-items: center;
width: 20px;
aspect-ratio: 1;
:host(${outlineState}) .control:active {
--control-border-color: ${colorNeutralStroke1Pressed};
}
:host(${smallState}) ::slotted([slot='indicator']) {
width: 16px;
:host(${outlineState}) .control:hover::before {
background-color: ${colorNeutralStrokeAccessibleHover};
}
:host(${largeState}) ::slotted([slot='indicator']) {
width: 24px;
:host(${outlineState}) .control:hover::after {
background-color: ${colorCompoundBrandBackgroundHover};
}
:host(${outlineState}) .control:active::before {
background-color: ${colorNeutralStrokeAccessiblePressed};
}
:host(${outlineState}) .control:active::after {
background-color: ${colorCompoundBrandBackgroundPressed};
}
:host(${filledLighterState}) .control {
background: ${colorNeutralBackground1};
}
:host(${filledDarkerState}) .control {
background: ${colorNeutralBackground3};
}
:host(:where(${filledLighterState}, ${filledDarkerState}) .control {
--control-border-color: ${colorTransparentStroke};
}
`;
Loading

0 comments on commit 8bfe90d

Please sign in to comment.