Skip to content

Commit

Permalink
feat(components): introduce Dropdown component (#168)
Browse files Browse the repository at this point in the history
* initial commit

* add context

* update navigation

* add baseline for the dropdown page

* build: add LumexUI.Variants

* refactor(popover): deprecate `LumexPopoverTrigger`

* feat(popover): add and expose `TriggerAsync` as a public API from the `IPopoverService`

* remove `LumexDropdownTrigger`

* improve usage of the popover within the dropdown

* add TwVariant to the DI

* refactor(popover): switch styles to TwVariant

* add Menu and MenuItem components

* add `Color`, `Variant` and `Disabled` parameters for the Menu and MenuItem components

* set default popover placement to bottom

* set default popover shadow to medium

* adjust semantic light surface1 colors

* add XML summaries

* docs: add Dropdown page

* make `Id` parameter of the `MenuItem` component of type `string`

* nits

* test: add TwVariant service everywhere

* test: replace FluentAssertions with AwesomeAssertions

* test: add dropdown tests

* docs(popover): update examples to use external trigger

* chore(menu-item): transition shadows on hover

* docs(dropdown): cleanup examples

* docs(dropdown): add a callout noting to set unique IDs for each item if `DisabledItems` is used

* chore: update summary for the `MenuVariant`
  • Loading branch information
desmondinho authored Feb 19, 2025
1 parent 234ef56 commit b5b60e1
Show file tree
Hide file tree
Showing 95 changed files with 3,387 additions and 811 deletions.
7 changes: 7 additions & 0 deletions LumexUI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LumexUI.Docs.Client", "docs
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LumexUI.Motion", "src\LumexUI.Motion\LumexUI.Motion.csproj", "{52657DBF-5D90-4829-9AE9-713E0C39E5CA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LumexUI.Variants", "src\LumexUI.Variants\LumexUI.Variants.csproj", "{BFEFB791-8251-48C3-9240-A4E5D5C2C2C2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -64,6 +66,10 @@ Global
{52657DBF-5D90-4829-9AE9-713E0C39E5CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{52657DBF-5D90-4829-9AE9-713E0C39E5CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{52657DBF-5D90-4829-9AE9-713E0C39E5CA}.Release|Any CPU.Build.0 = Release|Any CPU
{BFEFB791-8251-48C3-9240-A4E5D5C2C2C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BFEFB791-8251-48C3-9240-A4E5D5C2C2C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BFEFB791-8251-48C3-9240-A4E5D5C2C2C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BFEFB791-8251-48C3-9240-A4E5D5C2C2C2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -76,6 +82,7 @@ Global
{2A341376-2409-422C-A7A9-1AE6E86F91D9} = {3F4DE3FA-2636-440C-ACCC-137BAD95BAC4}
{6CE81AB3-399F-44CB-9DBE-9A8D62B07C8A} = {3F4DE3FA-2636-440C-ACCC-137BAD95BAC4}
{52657DBF-5D90-4829-9AE9-713E0C39E5CA} = {5DBC44B8-5B92-4504-8B8F-91A1672251E6}
{BFEFB791-8251-48C3-9240-A4E5D5C2C2C2} = {5DBC44B8-5B92-4504-8B8F-91A1672251E6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1B8CD55B-BEF0-42D1-936A-BC1FB5D02CE2}
Expand Down
166 changes: 86 additions & 80 deletions docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,90 +6,96 @@ namespace LumexUI.Docs.Client.Common;

public class NavigationStore
{
private static Navigation? _navigation;
private static Navigation? _navigation;

private static NavigationCategory GettingStartedCategory =>
new NavigationCategory( "Getting Started", Icons.Rounded.AutoStories )
.Add( new( "Overview" ) )
.Add( new( "Installation" ) );
private static NavigationCategory GettingStartedCategory =>
new NavigationCategory( "Getting Started", Icons.Rounded.AutoStories )
.Add( new( "Overview" ) )
.Add( new( "Installation" ) );

private static NavigationCategory CustomizationCategory =>
new NavigationCategory( "Customization", Icons.Rounded.DesignServices )
.Add( new( "Theme" ) )
.Add( new( "Layout" ) )
.Add( new( "Colors" ) )
.Add( new( "Customize Theme" ) );
private static NavigationCategory CustomizationCategory =>
new NavigationCategory( "Customization", Icons.Rounded.DesignServices )
.Add( new( "Theme" ) )
.Add( new( "Layout" ) )
.Add( new( "Colors" ) )
.Add( new( "Customize Theme" ) );

private static NavigationCategory ComponentsCategory =>
new NavigationCategory( "Components", Icons.Rounded.Joystick )
.Add( new( nameof( LumexAccordion ) ) )
.Add( new( nameof( LumexButton ) ) )
.Add( new( nameof( LumexCard ) ) )
.Add( new( nameof( LumexCheckbox ) ) )
.Add( new( nameof( LumexCheckboxGroup ) ) )
.Add( new( nameof( LumexCollapse ) ) )
.Add( new( nameof( LumexDataGrid<T> ) ) )
.Add( new( nameof( LumexDivider ) ) )
.Add( new( nameof( LumexIcon ) ) )
.Add( new( nameof( LumexLink ) ) )
.Add( new( nameof( LumexListbox<T> ), ComponentStatus.New ) )
.Add( new( nameof( LumexNavbar ) ) )
.Add( new( nameof( LumexNumbox<T> ) ) )
.Add( new( nameof( LumexPopover ) ) )
.Add( new( nameof( LumexRadioGroup<T> ), ComponentStatus.New ) )
.Add( new( nameof( LumexSelect<T> ), ComponentStatus.New ) )
.Add( new( nameof( LumexSwitch ) ) )
.Add( new( nameof( LumexTabs ), ComponentStatus.New ) )
.Add( new( nameof( LumexTextbox ) ) );
private static NavigationCategory ComponentsCategory =>
new NavigationCategory( "Components", Icons.Rounded.Joystick )
.Add( new( nameof( LumexAccordion ) ) )
.Add( new( nameof( LumexButton ) ) )
.Add( new( nameof( LumexCard ) ) )
.Add( new( nameof( LumexCheckbox ) ) )
.Add( new( nameof( LumexCheckboxGroup ) ) )
.Add( new( nameof( LumexCollapse ) ) )
.Add( new( nameof( LumexDataGrid<T> ) ) )
.Add( new( nameof( LumexDivider ) ) )
.Add( new( nameof( LumexDropdown ), ComponentStatus.New ) )
.Add( new( nameof( LumexIcon ) ) )
.Add( new( nameof( LumexLink ) ) )
.Add( new( nameof( LumexListbox<T> ) ) )
.Add( new( nameof( LumexNavbar ) ) )
.Add( new( nameof( LumexNumbox<T> ) ) )
.Add( new( nameof( LumexPopover ) ) )
.Add( new( nameof( LumexRadioGroup<T> ) ) )
.Add( new( nameof( LumexSelect<T> ) ) )
.Add( new( nameof( LumexSwitch ) ) )
.Add( new( nameof( LumexTabs ), ComponentStatus.New ) )
.Add( new( nameof( LumexTextbox ) ) );

private static NavigationCategory ComponentsApiCategory =>
new NavigationCategory( "Components API", Icons.Rounded.Manufacturing )
.Add( new( nameof( LumexAccordion ) ) )
.Add( new( nameof( LumexAccordionItem ) ) )
//.Add( nameof( LumexBooleanInputBase ) )
.Add( new( nameof( LumexButton ) ) )
.Add( new( nameof( LumexCard ) ) )
.Add( new( nameof( LumexCardBody ) ) )
.Add( new( nameof( LumexCardFooter ) ) )
.Add( new( nameof( LumexCardHeader ) ) )
.Add( new( nameof( LumexCheckbox ) ) )
.Add( new( nameof( LumexCheckboxGroup ) ) )
.Add( new( nameof( LumexCollapse ) ) )
.Add( new( nameof( LumexComponent ) ) )
//.Add( nameof( LumexComponentBase ) )
//.Add( nameof( LumexDebouncedInputBase<T> ) )
.Add( new( nameof( LumexDivider ) ) )
//.Add( nameof( LumexInputBase<T> ) )
//.Add( nameof( LumexInputFieldBase<T> ) )
.Add( new( nameof( LumexIcon ) ) )
.Add( new( nameof( LumexLink ) ) )
.Add( new( nameof( LumexListbox<T> ) ) )
.Add( new( nameof( LumexListboxItem<T> ) ) )
.Add( new( nameof( LumexNavbar ) ) )
.Add( new( nameof( LumexNavbarBrand ) ) )
.Add( new( nameof( LumexNavbarContent ) ) )
.Add( new( nameof( LumexNavbarItem ) ) )
.Add( new( nameof( LumexNavbarMenu ) ) )
.Add( new( nameof( LumexNavbarMenuItem ) ) )
.Add( new( nameof( LumexNavbarMenuToggle ) ) )
.Add( new( nameof( LumexNumbox<T> ) ) )
.Add( new( nameof( LumexPopover ) ) )
.Add( new( nameof( LumexPopoverContent ) ) )
.Add( new( nameof( LumexPopoverTrigger ) ) )
.Add( new( nameof( LumexSelect<T> ) ) )
.Add( new( nameof( LumexSelectItem<T> ) ) )
.Add( new( nameof( LumexSwitch ) ) )
.Add( new( nameof( LumexTextbox ) ) )
.Add( new( nameof( LumexThemeProvider ) ) );
private static NavigationCategory ComponentsApiCategory =>
new NavigationCategory( "Components API", Icons.Rounded.Manufacturing )
.Add( new( nameof( LumexAccordion ) ) )
.Add( new( nameof( LumexAccordionItem ) ) )
//.Add( nameof( LumexBooleanInputBase ) )
.Add( new( nameof( LumexButton ) ) )
.Add( new( nameof( LumexCard ) ) )
.Add( new( nameof( LumexCardBody ) ) )
.Add( new( nameof( LumexCardFooter ) ) )
.Add( new( nameof( LumexCardHeader ) ) )
.Add( new( nameof( LumexCheckbox ) ) )
.Add( new( nameof( LumexCheckboxGroup ) ) )
.Add( new( nameof( LumexCollapse ) ) )
.Add( new( nameof( LumexComponent ) ) )
//.Add( nameof( LumexComponentBase ) )
//.Add( nameof( LumexDebouncedInputBase<T> ) )
.Add( new( nameof( LumexDivider ) ) )
.Add( new( nameof( LumexDropdown ) ) )
.Add( new( nameof( LumexDropdownItem ) ) )
.Add( new( nameof( LumexDropdownMenu ) ) )
//.Add( nameof( LumexInputBase<T> ) )
//.Add( nameof( LumexInputFieldBase<T> ) )
.Add( new( nameof( LumexIcon ) ) )
.Add( new( nameof( LumexLink ) ) )
.Add( new( nameof( LumexListbox<T> ) ) )
.Add( new( nameof( LumexListboxItem<T> ) ) )
.Add( new( nameof( LumexNavbar ) ) )
.Add( new( nameof( LumexNavbarBrand ) ) )
.Add( new( nameof( LumexNavbarContent ) ) )
.Add( new( nameof( LumexNavbarItem ) ) )
.Add( new( nameof( LumexNavbarMenu ) ) )
.Add( new( nameof( LumexNavbarMenuItem ) ) )
.Add( new( nameof( LumexNavbarMenuToggle ) ) )
.Add( new( nameof( LumexNumbox<T> ) ) )
.Add( new( nameof( LumexPopover ) ) )
.Add( new( nameof( LumexPopoverContent ) ) )
.Add( new( nameof( LumexPopoverTrigger ) ) )
.Add( new( nameof( LumexSelect<T> ) ) )
.Add( new( nameof( LumexSelectItem<T> ) ) )
.Add( new( nameof( LumexSwitch ) ) )
.Add( new( nameof( LumexTab ) ) )
.Add( new( nameof( LumexTabs ) ) )
.Add( new( nameof( LumexTextbox ) ) )
.Add( new( nameof( LumexThemeProvider ) ) );

public static Navigation GetNavigation()
{
_navigation ??= new Navigation()
.Add( GettingStartedCategory )
.Add( CustomizationCategory )
.Add( ComponentsCategory )
.Add( ComponentsApiCategory );
public static Navigation GetNavigation()
{
_navigation ??= new Navigation()
.Add( GettingStartedCategory )
.Add( CustomizationCategory )
.Add( ComponentsCategory )
.Add( ComponentsApiCategory );

return _navigation;
}
return _navigation;
}
}
177 changes: 177 additions & 0 deletions docs/LumexUI.Docs.Client/Pages/Components/Dropdown/Dropdown.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
@page "/docs/components/dropdown"
@layout DocsContentLayout

@using LumexUI.Docs.Client.Pages.Components.Dropdown.PreviewCodes

<DocsCompositionSection Components="@_compositionComponents" />

<DocsSection Title="Usage">
<p>
The dropdown provides a menu that expands when triggered,
allowing users to select an action from a list.
</p>
<Usage />

<DocsSection Title="Disabled">
<p>
The dropdown items can be disabled to prevent user interaction.
A disabled dropdown item is faded and does not respond to user clicks.
</p>
<p>
You can achieve this by using the <Code>Disabled</Code> parameter on a <Code>DropdownItem</Code>.
</p>
<Disabled />

<p>
Alternatively, you can achieve this by using the
<Code>DisabledItems</Code> parameter on a <Code>DropdownMenu</Code>.
</p>
<DisabledItems />
<Callout Variant="@CalloutVariant.Warning">
It's important to set a unique <Code>Id</Code> for each item,
otherwise the disabled items will not work.
</Callout>
</DocsSection>

<DocsSection Title="Colors & Variants">
<p>
Customize the dropdown with different visual styles
and color themes to match your application’s design.
</p>
<ColorsVariants />
</DocsSection>

<DocsSection Title="Start & End Content">
<p>
Add custom content, such as icons or additional information,
to the start or end of each item in the dropdown.
</p>
<StartEndContent />
</DocsSection>

<DocsSection Title="Description">
<p>
Add a description to individual dropdown item
to provide additional context or details about the actions.
</p>
<Description />
</DocsSection>

<DocsSection Title="Empty Content">
<p>
Define custom content to display when the dropdown has no items,
providing a better user experience.
</p>
<EmptyContent />
</DocsSection>
</DocsSection>

<DocsSection Title="Custom Styles">
<p>
This component suppots named slots (represented as <code>data-*</code> attributes) that
allow you to apply custom CSS to specific parts of the component.
</p>
@foreach( var (componentName, slots) in _slots )
{
<h4>@componentName</h4>
<ul>
@foreach( var slot in slots )
{
<li>
<strong class="text-orange-500">@slot.Name:</strong> @slot.Description
</li>
}
</ul>
}
<p>
You can customize the component(s) by passing
any Tailwind CSS classes to the following component parameters:
</p>

<div>
<h4 class="font-semibold">Dropdown Menu</h4>
<ul>
<li><Code>Class</Code>: The CSS class names to style the wrapper.</li>
<li><Code>Classes</Code>: The CSS class names to style the slots.</li>
<li><Code>ItemClasses</Code>: The CSS class names to style the items slots.</li>
</ul>

<h4 class="font-semibold">Dropdown Item</h4>
<ul>
<li><Code>Class</Code>: The CSS class names to style the wrapper.</li>
<li><Code>Classes</Code>: The CSS class names to style the slots.</li>
</ul>
</div>
<CustomStyles />
</DocsSection>

@* <DocsSlotsSection Slots="@_slots">
<div>
<h4 class="font-semibold">Dropdown</h4>
<ul>
<li><Code>Class</Code></li>
<li><Code>Classes</Code></li>
</ul>
</div>
</DocsSlotsSection> *@

<DocsApiSection Components="@_apiComponents" />

@code {
[CascadingParameter] private DocsContentLayout Layout { get; set; } = default!;

private readonly CompositionComponent[] _compositionComponents = new CompositionComponent[]
{
new(nameof(LumexDropdown), "A component that represents a dropdown, extending Popover."),
new(nameof(LumexDropdownMenu), "A component that represents a dropdown menu."),
new(nameof(LumexDropdownItem), "A component that represents a dropdown item.")
};

private readonly Heading[] _headings = new Heading[]
{
new("Usage", [
new("Disabled"),
new("Colors & Variants"),
new("Start & End Content"),
new("Description"),
new("Empty Content"),
]),
new("Custom Styles"),
new("API")
};

private readonly Dictionary<string, Slot[]> _slots = new()
{
[nameof( LumexDropdownMenu )] = [
new(nameof(DropdownMenuSlots.Base), "The main container for the entire dropdown menu."),
new(nameof(DropdownMenuSlots.List), "The wrapper for the dropdown items, containing all dropdown items."),
new(nameof(DropdownMenuSlots.EmptyContent), "The area displayed when the dropdown is empty."),
],
[nameof( LumexDropdownItem )] = [
new(nameof(DropdownItemSlots.Base), "The main container for the dropdown item."),
new(nameof(DropdownItemSlots.Wrapper), "The wrapper for the title, description, and icons."),
new(nameof(DropdownItemSlots.Title), "The title of the dropdown item."),
new(nameof(DropdownItemSlots.Description), "The description of the dropdown item."),
]
};

private readonly string[] _apiComponents = new string[]
{
nameof(LumexDropdown),
nameof(LumexDropdownMenu),
nameof(LumexDropdownItem),
nameof(LumexButton),
nameof(LumexIcon)
};

protected override void OnInitialized()
{
Layout.Initialize(
title: "Dropdown",
category: "Components",
description: "Dropdowns display a list of actions in a popover that users can select.",
headings: _headings,
linksProps: new ComponentLinksProps( "Dropdown", isServer: false )
);
}
}
Loading

0 comments on commit b5b60e1

Please sign in to comment.