ui: Transition App Chrome to use new Disclosure Menus (#12334)

* Add %panel CSS component

* Deprecate old menu-panel component

* Various smallish tweaks to disclosure-menu

* Move all menus in the app chrome to use new DisclosureMenu

* Follow up CSS to move all app chrome menus to new components

* Don't prevent default any events from anchors

* Add a tick to click steps
This commit is contained in:
John Cowen 2022-02-21 12:22:59 +00:00 committed by GitHub
parent 067223337d
commit 4ad8a0cfef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 875 additions and 435 deletions

3
.changelog/12334.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:enhancement
ui: Slightly improve usability of main navigation
```

View File

@ -119,32 +119,32 @@
Logout Logout
</Action> </Action>
</Portal> </Portal>
<PopoverMenu @position="right" as |components api|> <DisclosureMenu as |disclosure|>
<BlockSlot @name="trigger"> <disclosure.Action
{{on 'click' disclosure.toggle}}
>
Logout Logout
</BlockSlot> </disclosure.Action>
<BlockSlot @name="menu"> <disclosure.Menu as |panel|>
{{#let components.MenuItem components.MenuSeparator as |MenuItem MenuSeparator|}} {{#if authDialog.token.AccessorID}}
{{!TODO: It might be nice to use one of our recursive components here}} <AuthProfile
{{#if authDialog.token.AccessorID}} @item={{authDialog.token}}
<li role="none"> />
<AuthProfile {{/if}}
@item={{authDialog.token}} <panel.Menu as |menu|>
/> <menu.Separator />
</li> <menu.Item
<MenuSeparator /> class="dangerous"
{{/if}} >
<MenuItem <menu.Action
class="dangerous" {{on 'click' (optional authDialog.logout)}}
@onclick={{action authDialog.logout}}
> >
<BlockSlot @name="label"> Logout
Logout </menu.Action>
</BlockSlot> </menu.Item>
</MenuItem> </panel.Menu>
{{/let}} </disclosure.Menu>
</BlockSlot> </DisclosureMenu>
</PopoverMenu>
</:authorized> </:authorized>
</AuthDialog> </AuthDialog>

View File

@ -1,74 +1,77 @@
{{#if (can "use nspaces")}} {{#if (can "use nspaces")}}
{{#if (can "choose nspaces")}} {{#if (can "choose nspaces")}}
{{#let {{#let
(or @nspace 'default') (or @nspace 'default')
as |nspace|}} as |nspace|}}
<li <li
class="nspaces" class="nspaces"
data-test-nspace-menu data-test-nspace-menu
> >
<PopoverMenu <DisclosureMenu
aria-label="Namespace" aria-label="Namespace"
@position="left" as |disclosure|>
as |components api|> <disclosure.Action
<BlockSlot @name="trigger"> {{on 'click' disclosure.toggle}}
{{nspace}} >
</BlockSlot> {{nspace}}
<BlockSlot @name="menu"> </disclosure.Action>
{{#let components.MenuItem components.MenuSeparator as |MenuItem MenuSeparator|}} <disclosure.Menu as |panel|>
{{#if (gt @nspaces.length 0)}} {{#if (gt @nspaces.length 0)}}
<DataSource <DataSource
@src={{uri @src={{uri
'/${partition}/*/${dc}/namespaces' '/${partition}/*/${dc}/namespaces'
(hash (hash
partition=@partition partition=@partition
dc=@dc.Name dc=@dc.Name
) )
}} }}
@onchange={{fn (optional @onchange)}} @onchange={{fn (optional @onchange)}}
@loading="lazy" />
/> {{else}}
{{else}} <DataSource
<DataSource @src={{uri
@src={{uri '/${partition}/*/${dc}/namespaces'
'/${partition}/*/${dc}/namespaces' (hash
(hash partition=@partition
partition=@partition dc=@dc.Name
dc=@dc.Name )
) }}
}} @onchange={{fn (optional @onchange)}}
@onchange={{fn (optional @onchange)}} />
/> {{/if}}
{{/if}} <panel.Menu as |menu|>
{{#each (reject-by 'DeletedAt' @nspaces) as |item|}} {{#each (reject-by 'DeletedAt' @nspaces) as |item|}}
<MenuItem <menu.Item
class={{if (eq nspace item.Name) 'is-active'}} aria-current={{if (eq nspace item.Name) 'true'}}
>
<menu.Action
{{on 'click' disclosure.close}}
@href={{href-to '.' params=(hash @href={{href-to '.' params=(hash
partition=(if (gt @partition.length 0) @partition undefined) partition=(if (gt @partition.length 0) @partition undefined)
nspace=item.Name nspace=item.Name
)}} )}}
> >
<BlockSlot @name="label"> {{item.Name}}
{{item.Name}} </menu.Action>
</BlockSlot> </menu.Item>
</MenuItem> {{/each}}
{{/each}} {{#if (can 'manage nspaces')}}
{{#if (can 'manage nspaces')}} <menu.Separator />
<MenuSeparator /> <menu.Item
<MenuItem data-test-main-nav-nspaces
data-test-main-nav-nspaces >
<menu.Action
{{on 'click' disclosure.close}}
@href={{href-to 'dc.nspaces' @dc.Name}} @href={{href-to 'dc.nspaces' @dc.Name}}
> >
<BlockSlot @name="label"> Manage Namespaces
Manage Namespaces </menu.Action>
</BlockSlot> </menu.Item>
</MenuItem> {{/if}}
{{/if}} </panel.Menu>
{{/let}} </disclosure.Menu>
</BlockSlot> </DisclosureMenu>
</PopoverMenu> </li>
</li> {{/let}}
{{/let}}
{{/if}}
{{/if}} {{/if}}
{{/if}}

View File

@ -6,51 +6,56 @@ as |partition|}}
class="partitions" class="partitions"
data-test-partition-menu data-test-partition-menu
> >
<PopoverMenu <DisclosureMenu
aria-label="Admin Partition" aria-label="Admin Partition"
@position="left" as |disclosure|>
as |components api|> <disclosure.Action
<BlockSlot @name="trigger"> {{on 'click' disclosure.toggle}}
{{partition}} >
</BlockSlot> {{partition}}
<BlockSlot @name="menu"> </disclosure.Action>
{{#let components.MenuItem components.MenuSeparator as |MenuItem MenuSeparator|}} <disclosure.Menu as |panel|>
<DataSource <DataSource
@src={{uri @src={{uri
'/*/*/${dc}/partitions' '/*/*/${dc}/partitions'
(hash (hash
dc=@dc.Name dc=@dc.Name
) )
}} }}
@onchange={{fn (optional @onchange)}} @onchange={{fn (optional @onchange)}}
/> />
<panel.Menu as |menu|>
{{#each (reject-by 'DeletedAt' @partitions) as |item|}} {{#each (reject-by 'DeletedAt' @partitions) as |item|}}
<MenuItem <menu.Item
class={{if (eq partition item.Name) 'is-active'}} class={{if (eq partition item.Name) 'is-active'}}
@href={{href-to '.' params=(hash
partition=item.Name
nspace=undefined
)}}
> >
<BlockSlot @name="label"> <menu.Action
{{on 'click' disclosure.close}}
@href={{href-to '.' params=(hash
partition=item.Name
nspace=undefined
)}}
>
{{item.Name}} {{item.Name}}
</BlockSlot> </menu.Action>
</MenuItem> </menu.Item>
{{/each}} {{/each}}
{{#if (can 'manage partitions')}} {{#if (can 'manage partitions')}}
<MenuSeparator /> <menu.Separator />
<MenuItem <menu.Item
data-test-main-nav-partitions data-test-main-nav-partitions
@href={{href-to 'dc.partitions.index' @dc.Name}}
> >
<BlockSlot @name="label"> <menu.Action
{{on 'click' disclosure.close}}
@href={{href-to 'dc.partitions.index' @dc.Name}}
>
Manage Partitions Manage Partitions
</BlockSlot> </menu.Action>
</MenuItem> </menu.Item>
{{/if}} {{/if}}
{{/let}} </panel.Menu>
</BlockSlot> </disclosure.Menu>
</PopoverMenu> </DisclosureMenu>
</li> </li>
{{else}} {{else}}
<li <li

View File

@ -52,13 +52,22 @@
margin-left: auto; margin-left: auto;
} }
%main-nav-vertical-hoisted { %main-nav-vertical-hoisted {
top: 11px; top: 18px;
} }
%main-nav-vertical-hoisted > .popover-menu > label > button { %main-nav-vertical-hoisted [aria-label]::before {
display: none !important;
}
%main-nav-horizontal [aria-haspopup='menu'] ~ * {
position: absolute;
right: 0;
min-width: 192px;
}
%main-nav-horizontal [aria-expanded],
%main-nav-vertical-hoisted [aria-expanded] {
@extend %main-nav-horizontal-popover-menu-trigger;
@extend %main-nav-horizontal-action; @extend %main-nav-horizontal-action;
border: none;
} }
%main-nav-vertical-hoisted.is-active > label > * { %main-nav-horizontal-popover-menu-trigger {
@extend %main-nav-horizontal-action-active; @extend %main-nav-horizontal-action-active;
} }
%footer, %footer,

View File

@ -0,0 +1,49 @@
<li
class="dcs"
data-test-datacenter-menu
>
<DisclosureMenu
aria-label="Datacenter"
as |disclosure|>
<disclosure.Action
{{on 'click' disclosure.toggle}}
>
{{@dc.Name}}
</disclosure.Action>
<disclosure.Menu as |panel|>
<DataSource
@src={{uri '/*/*/*/datacenters'}}
@onchange={{action (mut @dcs) value="data"}}
/>
<panel.Menu as |menu|>
{{#each (sort-by 'Name' @dcs) as |item|}}
<menu.Item
aria-current={{if (eq @dc.Name item.Name) 'true'}}
class={{class-map
(array 'is-local' item.Local)
(array 'is-primary' item.Primary)
}}
>
<menu.Action
{{on 'click' disclosure.close}}
@href={{href-to '.' params=(hash
dc=item.Name
partition=undefined
nspace=(if (gt @nspace.length 0) @nspace undefined)
)}}
>
{{item.Name}}
{{#if item.Primary}}
<span>Primary</span>
{{/if}}
{{#if item.Local}}
<span>Local</span>
{{/if}}
</menu.Action>
</menu.Item>
{{/each}}
</panel.Menu>
</disclosure.Menu>
</DisclosureMenu>
</li>

View File

@ -19,13 +19,15 @@ common usecase of having a floating menu.
> >
{{if disclosure.expanded 'Close' 'Open'}} {{if disclosure.expanded 'Close' 'Open'}}
</disclosure.Action> </disclosure.Action>
<disclosure.Menu as |menu|> <disclosure.Menu as |panel|>
<menu.Item> <panel.Menu as |menu|>
<menu.Action>Item 1</menu.Action> <menu.Item>
</menu.Item> <menu.Action>Item 1</menu.Action>
<menu.Item> </menu.Item>
<menu.Action>Item 2</menu.Action> <menu.Item>
</menu.Item> <menu.Action>Item 2</menu.Action>
</menu.Item>
</panel.Menu>
</disclosure.Menu> </disclosure.Menu>
</DisclosureMenu> </DisclosureMenu>
</figure> </figure>
@ -46,13 +48,15 @@ common usecase of having a floating menu.
(array 'top' this.height) (array 'top' this.height)
(array 'background-color' 'rgb(var(--tone-gray-000))') (array 'background-color' 'rgb(var(--tone-gray-000))')
}} }}
as |menu|> as |panel|>
<menu.Item> <panel.Menu as |menu|>
<menu.Action>Item 1</menu.Action> <menu.Item>
</menu.Item> <menu.Action>Item 1</menu.Action>
<menu.Item> </menu.Item>
<menu.Action>Item 2</menu.Action> <menu.Item>
</menu.Item> <menu.Action>Item 2</menu.Action>
</menu.Item>
</panel.Menu>
</disclosure.Menu> </disclosure.Menu>
</DisclosureMenu> </DisclosureMenu>
</figure> </figure>

View File

@ -4,12 +4,16 @@
}} }}
...attributes ...attributes
> >
<Disclosure as |disclosure|> <Disclosure
@expanded={{@expanded}}
as |disclosure|>
{{yield (hash {{yield (hash
Action=(component 'disclosure-menu/action' disclosure=disclosure) Action=(component 'disclosure-menu/action' disclosure=disclosure)
Menu=(component 'disclosure-menu/menu' disclosure=disclosure) Menu=(component 'disclosure-menu/menu' disclosure=disclosure)
disclosure=disclosure disclosure=disclosure
toggle=disclosure.toggle toggle=disclosure.toggle
close=disclosure.close
open=disclosure.open
expanded=disclosure.expanded expanded=disclosure.expanded
)}} )}}
</Disclosure> </Disclosure>

View File

@ -1,3 +1,6 @@
.disclosure-menu { .disclosure-menu {
position: relative; position: relative;
} }
.disclosure-menu [aria-expanded] ~ * {
@extend %menu-panel;
}

View File

@ -1,15 +1,11 @@
<@disclosure.Details as |details|> <@disclosure.Details as |details|>
<Menu <div
{{on-outside 'click' @disclosure.close}} {{on-outside 'click' @disclosure.close}}
@disclosure={{@disclosure}} ...attributes
...attributes >
as |menu|> {{yield (hash
{{yield (hash Menu=(component 'menu' disclosure=@disclosure)
items=menu.items )}}
Item=menu.Item </div>
Action=menu.Action
Separator=menu.Separator
)}}
</Menu>
</@disclosure.Details> </@disclosure.Details>

View File

@ -87,53 +87,12 @@
<:main-nav> <:main-nav>
<ul> <ul>
<li <Consul::Datacenter::Selector
class="dcs" @dc={{@dc}}
data-test-datacenter-menu @partition={{@partition}}
> @nspace={{@nspace}}
<PopoverMenu @dcs={{@dcs}}
aria-label="Datacenter" />
@position="left"
as |components|>
<BlockSlot @name="trigger">
{{@dc.Name}}
</BlockSlot>
<BlockSlot @name="menu">
{{#let components.MenuItem components.MenuSeparator as |MenuItem MenuSeparator|}}
<DataSource
@src={{uri '/*/*/*/datacenters'}}
@onchange={{action (mut @dcs) value="data"}}
@loading="lazy"
/>
{{#each (sort-by 'Name' @dcs) as |item|}}
<MenuItem
data-test-datacenter-picker
class={{concat
(if (eq @dc.Name item.Name) 'is-active')
(if item.Local ' is-local')
(if item.Primary ' is-primary')
}}
@href={{href-to '.' params=(hash
dc=item.Name
partition=undefined
nspace=(if (gt @nspace.length 0) @nspace undefined)
)}}
>
<BlockSlot @name="label">
{{item.Name}}
{{#if item.Primary}}
<span>Primary</span>
{{/if}}
{{#if item.Local}}
<span>Local</span>
{{/if}}
</BlockSlot>
</MenuItem>
{{/each}}
{{/let}}
</BlockSlot>
</PopoverMenu>
</li>
<Consul::Partition::Selector <Consul::Partition::Selector
@dc={{@dc}} @dc={{@dc}}
@partition={{@partition}} @partition={{@partition}}
@ -182,45 +141,52 @@
<li <li
data-test-main-nav-help data-test-main-nav-help
> >
<PopoverMenu @position="right" as |components|> <DisclosureMenu
<BlockSlot @name="trigger"> as |disclosure|>
<disclosure.Action
{{on 'click' disclosure.toggle}}
>
Help Help
</BlockSlot> </disclosure.Action>
<BlockSlot @name="menu"> <disclosure.Menu as |panel|>
{{#let components.MenuItem components.MenuSeparator as |MenuItem MenuSeparator|}} <panel.Menu as |menu|>
<MenuSeparator> <menu.Separator>
<BlockSlot @name="label"> Consul v{{env 'CONSUL_VERSION'}}
Consul v{{env 'CONSUL_VERSION'}} </menu.Separator>
</BlockSlot> <menu.Item
</MenuSeparator>
<MenuItem
class="docs-link" class="docs-link"
@href={{env 'CONSUL_DOCS_URL'}}
> >
<BlockSlot @name="label"> <menu.Action
@href={{env 'CONSUL_DOCS_URL'}}
@external={{true}}
>
Documentation Documentation
</BlockSlot> </menu.Action>
</MenuItem> </menu.Item>
<MenuItem <menu.Item
class="learn-link" class="learn-link"
@href={{concat (env 'CONSUL_DOCS_LEARN_URL') '/consul'}}
> >
<BlockSlot @name="label"> <menu.Action
@href={{concat (env 'CONSUL_DOCS_LEARN_URL') '/consul'}}
@external={{true}}
>
HashiCorp Learn HashiCorp Learn
</BlockSlot> </menu.Action>
</MenuItem> </menu.Item>
<MenuSeparator /> <menu.Separator />
<MenuItem <menu.Item
class="learn-link" class="feedback-link"
@href={{env 'CONSUL_REPO_ISSUES_URL'}}
> >
<BlockSlot @name="label"> <menu.Action
@href={{env 'CONSUL_REPO_ISSUES_URL'}}
@external={{true}}
>
Provide Feedback Provide Feedback
</BlockSlot> </menu.Action>
</MenuItem> </menu.Item>
{{/let}} </panel.Menu>
</BlockSlot> </disclosure.Menu>
</PopoverMenu> </DisclosureMenu>
</li> </li>
<li <li
data-test-main-nav-settings data-test-main-nav-settings

View File

@ -1,11 +1,18 @@
%hashicorp-consul { %hashicorp-consul {
[role='banner'] nav .dcs { nav .dcs {
@extend %main-nav-vertical-hoisted; @extend %main-nav-vertical-hoisted;
left: 100px; left: 100px;
} }
[role='banner'] nav .dcs .popover-menu[aria-label]::before { nav .dcs .menu-panel {
display: none; min-width: 250px;
} }
nav li.partitions,
nav li.nspaces {
@extend %main-nav-vertical-popover-menu;
/* --panel-height: 300px;
--row-height: 43px; */
}
[role='banner'] a svg { [role='banner'] a svg {
fill: rgb(var(--tone-brand-600)); fill: rgb(var(--tone-brand-600));
} }

View File

@ -50,7 +50,7 @@ export default (collection, clickable, attribute, is, authForm, emptyState) => s
':checked', ':checked',
'[data-test-nspace-menu] > input[type="checkbox"]' '[data-test-nspace-menu] > input[type="checkbox"]'
); );
page.navigation.dcs = collection('[data-test-datacenter-picker]', { page.navigation.dcs = collection('[data-test-datacenter-menu] li', {
name: clickable('a'), name: clickable('a'),
}); });
return page; return page;

View File

@ -5,6 +5,7 @@
%main-nav-horizontal > ul > li > a, %main-nav-horizontal > ul > li > a,
%main-nav-horizontal > ul > li > span, %main-nav-horizontal > ul > li > span,
%main-nav-horizontal > ul > li > button, %main-nav-horizontal > ul > li > button,
%main-nav-horizontal-popover-menu-trigger,
%main-nav-horizontal > ul > li > .popover-menu > label > button { %main-nav-horizontal > ul > li > .popover-menu > label > button {
@extend %main-nav-horizontal-action; @extend %main-nav-horizontal-action;
} }

View File

@ -5,6 +5,15 @@
%main-nav-horizontal-action > a { %main-nav-horizontal-action > a {
color: inherit; color: inherit;
} }
%main-nav-horizontal-popover-menu-trigger::after {
@extend %with-chevron-down-mask, %as-pseudo;
width: 16px;
height: 16px;
position: relative;
}
%main-nav-horizontal-popover-menu-trigger[aria-expanded='true']::after {
@extend %with-chevron-up-mask;
}
/**/ /**/
/* reduced size hamburger menu */ /* reduced size hamburger menu */
%main-nav-horizontal-toggle { %main-nav-horizontal-toggle {

View File

@ -30,34 +30,12 @@
%main-nav-vertical > ul > li > label { %main-nav-vertical > ul > li > label {
@extend %main-nav-vertical-action; @extend %main-nav-vertical-action;
} }
/**/
%main-nav-vertical .popover-menu {
margin-top: 0.5rem;
}
%main-nav-vertical .popover-menu .menu-panel {
top: 37px !important;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
%main-nav-vertical .popover-menu > label > button {
border: var(--decor-border-100);
border-color: rgb(var(--tone-gray-500));
color: rgb(var(--tone-gray-999));
width: calc(100% - 20px);
z-index: 100;
text-align: left;
padding: 10px;
border-radius: var(--decor-radius-100);
}
%main-nav-vertical .popover-menu > label > button::after {
float: right;
}
%main-nav-vertical .popover-menu .menu-panel {
top: 28px;
z-index: 100;
}
/* menu-panels in the main navigation are treated slightly differently */ /* menu-panels in the main navigation are treated slightly differently */
%main-nav-vertical label + div { %main-nav-vertical-popover-menu .disclosure-menu button + * {
@extend %main-nav-vertical-menu-panel; @extend %main-nav-vertical-menu-panel;
} }
/**/
%main-nav-vertical-popover-menu .disclosure-menu > button {
@extend %main-nav-vertical-popover-menu-trigger;
@extend %internal-button;
}

View File

@ -11,14 +11,13 @@
%main-nav-vertical:not(.in-viewport) { %main-nav-vertical:not(.in-viewport) {
visibility: hidden; visibility: hidden;
} }
%main-nav-vertical li.partitions,
%main-nav-vertical li.partition, %main-nav-vertical li.partition,
%main-nav-vertical li.partitions,
%main-nav-vertical li.nspaces { %main-nav-vertical li.nspaces {
margin-bottom: 25px; margin-bottom: 25px;
padding: 0 26px; padding: 0 26px;
} }
%main-nav-vertical li.dcs { %main-nav-vertical li.dcs {
margin-bottom: 18px;
padding: 0 18px; padding: 0 18px;
} }
// TODO: We no longer have the rule that menu-panel buttons only contain two // TODO: We no longer have the rule that menu-panel buttons only contain two
@ -41,9 +40,21 @@
margin-top: 0.7rem; margin-top: 0.7rem;
padding-bottom: 0; padding-bottom: 0;
} }
%main-nav-vertical-popover-menu .disclosure {
position: relative;
}
%main-nav-vertical-popover-menu-trigger {
width: 100%;
text-align: left;
padding: 10px;
}
%main-nav-vertical-popover-menu-trigger::after {
float: right;
}
%main-nav-vertical-menu-panel { %main-nav-vertical-menu-panel {
min-width: 248px; position: absolute;
z-index: 1;
width: calc(100% - 2px);
} }
%main-nav-vertical-hoisted { %main-nav-vertical-hoisted {
visibility: visible; visibility: visible;

View File

@ -1,8 +1,8 @@
%main-nav-vertical-action { %main-nav-vertical-action {
@extend %p1;
cursor: pointer; cursor: pointer;
border-right: var(--decor-border-400); border-right: var(--decor-border-400);
border-color: var(--transparent); border-color: var(--transparent);
@extend %p1;
} }
%main-nav-vertical-action > a { %main-nav-vertical-action > a {
color: inherit; color: inherit;
@ -41,28 +41,38 @@
background-color: rgb(var(--tone-gray-150)); background-color: rgb(var(--tone-gray-150));
border-color: rgb(var(--tone-gray-999)); border-color: rgb(var(--tone-gray-999));
} }
%main-nav-vertical li[aria-label]::before, %main-nav-vertical [aria-label]::before {
%main-nav-vertical .popover-menu[aria-label]::before {
color: rgb(var(--tone-gray-700)); color: rgb(var(--tone-gray-700));
content: attr(aria-label); content: attr(aria-label);
display: block; display: block;
margin-top: -0.5rem; margin-top: -0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
%main-nav-vertical .is-primary span, %main-nav-vertical-popover-menu-trigger {
%main-nav-vertical .is-local span { border: var(--decor-border-100);
@extend %pill-200; border-color: rgb(var(--tone-gray-500));
color: rgb(var(--tone-gray-000)); border-radius: var(--decor-radius-100);
background-color: rgb(var(--tone-gray-500));
} font-weight: inherit;
%main-nav-vertical .nspaces .menu-panel > div {
background-color: rgb(var(--tone-gray-050)); background-color: rgb(var(--tone-gray-050));
color: rgb(var(--tone-gray-999)); color: rgb(var(--tone-gray-999));
padding-left: 36px;
} }
%main-nav-vertical .nspaces .menu-panel > div::before { %main-nav-vertical-popover-menu-trigger[aria-expanded='true'] {
@extend %with-info-circle-fill-mask, %as-pseudo; border-bottom-left-radius: var(--decor-radius-000);
color: rgb(var(--tone-blue-500)); border-bottom-right-radius: var(--decor-radius-000);
/* sizes the icon not the text */ }
font-size: 1.1em; %main-nav-vertical-popover-menu-trigger::after {
@extend %with-chevron-down-mask, %as-pseudo;
width: 16px;
height: 16px;
position: relative;
}
%main-nav-vertical-popover-menu-trigger[aria-expanded='true']::after {
@extend %with-chevron-up-mask;
}
%main-nav-vertical-menu-panel {
border-top-left-radius: var(--decor-radius-000);
border-top-right-radius: var(--decor-radius-000);
border-top: var(--decor-border-000);
} }

View File

@ -0,0 +1,169 @@
# MenuPanel
```hbs preview-template
{{#each
(array 'light' 'dark')
as |theme|}}
<figure>
<figcaption>Without a header</figcaption>
<div
class={{class-map
'menu-panel'
(array (concat 'theme-' theme))
}}
>
<ul role="menu">
<li aria-current="true" role="none">
<Action role="menuitem">Item 1<span>Label</span><span>Label 2</span></Action>
</li>
<li role="separator">
Item some title text
</li>
<li role="none">
<Action role="menuitem">Item 2</Action>
</li>
<li role="separator"></li>
<li role="none">
<Action role="menuitem">Item 3</Action>
</li>
</ul>
</div>
</figure>
<figure>
<figcaption>With a header</figcaption>
<div
class={{class-map
'menu-panel'
(array (concat 'theme-' theme))
}}
>
<div>
<p>Some content explaining what the menu is about</p>
</div>
<ul role="menu">
<li aria-current="true" role="none">
<Action role="menuitem">Item 1<span>Label</span><span>Label 2</span></Action>
</li>
<li role="separator">
Item some title text
</li>
<li role="none">
<Action role="menuitem">Item 2</Action>
</li>
<li role="separator"></li>
<li role="none">
<Action role="menuitem">Item 3</Action>
</li>
</ul>
</div>
</figure>
<figure>
<StateChart
@src={{state-chart 'boolean'}}
as |State Guard StateAction dispatch state|>
<Action>Focus Left</Action>
<DisclosureMenu as |disclosure|>
<disclosure.Action
{{on 'click' disclosure.toggle}}
>
{{if disclosure.expanded 'Close' 'Open'}}
</disclosure.Action>
<disclosure.Menu
style={{style-map
(array 'max-height' (if (state-matches state 'true') (add 0 this.rect.height)) 'px')
}}
class={{class-map
(array 'menu-panel')
(array 'menu-panel-confirming' (state-matches state 'true'))
(array (concat 'theme-' theme))
}}
as |panel|>
<div
{{on-resize
(dom-position (set this 'header'))
}}
>
<p>Some text in here</p>
</div>
<panel.Menu as |menu|>
<menu.Item
aria-current="true"
>
<menu.Action>
Item 1
<span>Label</span>
<span>Label 2</span>
</menu.Action>
</menu.Item>
<menu.Separator>
Item some title text
</menu.Separator>
<menu.Item>
<menu.Action>
Item 2
</menu.Action>
</menu.Item>
<menu.Separator />
<menu.Item
class="dangerous"
>
<menu.Action
{{on "click" (fn dispatch 'TOGGLE')}}
>
Item 3
</menu.Action>
<div
{{on-resize
(dom-position (set this 'rect'))
}}
style={{style-map
(array 'top' (if (state-matches state 'true') (sub 0 this.header.height)) 'px')
}}
class={{class-map
'menu-panel-confirmation'
'informed-action'
'confirmation-alert'
'warning'
}}
>
<div>
<header>Hi</header>
<p>Body</p>
</div>
<ul>
<li>
<Action
@tabindex="-1"
{{on "click" (queue disclosure.close (fn dispatch 'TOGGLE'))}}
>
Confirm
</Action>
</li>
<li>
<Action
@tabindex="-1"
{{on "click" (fn dispatch 'TOGGLE')}}
>
Cancel
</Action>
</li>
</ul>
</div>
</menu.Item>
</panel.Menu>
</disclosure.Menu>
</DisclosureMenu>
<Action>Focus Right</Action>
</StateChart>
</figure>
{{/each}}
```

View File

@ -0,0 +1,58 @@
/* old stuff */
%menu-panel {
overflow: hidden;
}
%menu-panel-deprecated {
position: absolute;
}
%menu-panel-deprecated [type='checkbox'] {
display: none;
}
%menu-panel-deprecated {
transition: max-height 150ms;
}
%menu-panel-deprecated {
transition: min-height 150ms, max-height 150ms;
min-height: 0;
}
%menu-panel-deprecated:not(.confirmation) [type='checkbox'] ~ * {
transition: transform 150ms;
}
%menu-panel-deprecated [type='checkbox']:checked ~ * {
transform: translateX(calc(-100% - 10px));
}
%menu-panel-deprecated.confirmation [role='menu'] {
min-height: 205px !important;
}
%menu-panel-deprecated [type='checkbox']:checked ~ * {
/* this needs to autocalculate */
min-height: 143px;
max-height: 143px;
}
%menu-panel-deprecated [id$='-']:first-child:checked ~ ul label[for$='-'] * [role='menu'],
%menu-panel-deprecated [id$='-']:first-child:checked ~ ul > li > [role='menu'] {
display: block;
}
/**/
%menu-panel-deprecated > ul > li > div[role='menu'] {
position: absolute;
top: 0;
left: calc(100% + 10px);
}
%menu-panel-deprecated > ul > li > *:not(div[role='menu']) {
position: relative;
}
%menu-panel-deprecated:not(.left) {
right: 0px !important;
left: auto !important;
}
%menu-panel-deprecated.left {
left: 0px;
}
%menu-panel-deprecated:not(.above) {
top: 28px;
}
%menu-panel-deprecated.above {
bottom: 42px;
}

View File

@ -3,11 +3,12 @@
change=(action "change") change=(action "change")
) as |api|}} ) as |api|}}
<div <div
class={{join ' ' (compact (array class={{class-map
'menu-panel' (array 'menu-panel')
position (array 'menu-panel-deprecated')
(if isConfirmation 'confirmation') (array position)
))}} (array isConfirmation 'confirmation')
}}
{{did-insert (action 'connect')}} {{did-insert (action 'connect')}}
> >
<YieldSlot @name="controls"> <YieldSlot @name="controls">

View File

@ -1,22 +1,50 @@
@import './skin'; @import './skin';
@import './layout'; @import './layout';
@import './deprecated';
.menu-panel { .menu-panel {
@extend %menu-panel; @extend %menu-panel;
} }
.menu-panel-deprecated {
@extend %menu-panel-deprecated;
}
%menu-panel {
@extend %panel;
}
%menu-panel-item span {
@extend %menu-panel-badge;
}
%menu-panel [role='separator'] { %menu-panel [role='separator'] {
@extend %panel-separator;
@extend %menu-panel-separator; @extend %menu-panel-separator;
} }
%menu-panel > div { %menu-panel > div {
@extend %menu-panel-header; @extend %menu-panel-header;
} }
// %menu-panel > ul > li > *:not(div), %menu-panel > ul {
%menu-panel [role='menuitem'] { @extend %menu-panel-body;
}
%menu-panel-body > li {
@extend %menu-panel-item;
}
%menu-panel-body > [role='treeitem'],
%menu-panel-body > li > [role='menuitem'],
%menu-panel-body > li > [role='option'] {
@extend %menu-panel-button;
}
%menu-panel-button + * {
@extend %menu-panel-confirmation;
}
%menu-panel-item[aria-selected] > *,
%menu-panel-item[aria-checked] > *,
%menu-panel-item[aria-current] > *,
%menu-panel-item.is-active > * {
@extend %menu-panel-button-selected;
}
%menu-panel-button {
@extend %internal-button; @extend %internal-button;
} }
%menu-panel > ul > li.dangerous > *:not(div) { /* first-child is highly likely to be the button/or anchor*/
%menu-panel-item.dangerous > *:first-child {
@extend %internal-button-dangerous; @extend %internal-button-dangerous;
} }
%menu-panel .informed-action {
border: 0 !important;
}

View File

@ -1,115 +1,7 @@
%menu-panel {
position: absolute;
}
%menu-panel [type='checkbox'] {
display: none;
}
%menu-panel {
overflow: hidden;
transition: min-height 150ms, max-height 150ms;
min-height: 0;
}
%menu-panel:not(.confirmation) [type='checkbox'] ~ * {
transition: transform 150ms;
}
%menu-panel [type='checkbox']:checked ~ * {
transform: translateX(calc(-100% - 10px));
}
%menu-panel.confirmation [role='menu'] {
min-height: 200px !important;
}
%menu-panel [role='menuitem'] {
display: flex;
justify-content: space-between;
}
%menu-panel [role='menuitem']:after {
@extend %as-pseudo;
display: block !important;
background-position: center right !important;
}
%menu-panel-sub-panel {
position: absolute;
top: 0;
left: calc(100% + 10px);
display: none;
}
/* TODO: once everything is using ListCollection */
/* this can go */
%menu-panel [type='checkbox']:checked ~ * {
/* this needs to autocalculate */
min-height: 143px;
max-height: 143px;
}
%menu-panel [id$='-']:first-child:checked ~ ul label[for$='-'] * [role='menu'],
%menu-panel [id$='-']:first-child:checked ~ ul > li > [role='menu'] {
display: block;
}
/**/
%menu-panel > ul > li > div[role='menu'] {
@extend %menu-panel-sub-panel;
}
%menu-panel > ul > li > *:not(div[role='menu']) {
position: relative;
}
%menu-panel:not(.left) {
right: 0px;
left: auto;
}
%menu-panel.left {
left: 0px;
}
%menu-panel:not(.above) {
top: 28px;
}
%menu-panel.above {
bottom: 42px;
}
%menu-panel > ul {
margin: 0;
padding: 4px 0;
}
%menu-panel > ul,
%menu-panel > ul > li,
%menu-panel > ul > li > * {
width: 100%;
}
%menu-panel > ul > li > * {
text-align: left !important;
}
%menu-panel-separator {
padding-top: 0.35em;
}
%menu-panel-separator:not(:first-child) {
margin-top: 0.35em;
}
%menu-panel-separator:not(:empty) {
padding-left: 1em;
padding-right: 1em;
padding-bottom: 0.1em;
}
%menu-panel-header { %menu-panel-header {
padding: 10px; padding: 0.625rem var(--padding-x); /* 10px */
white-space: normal; white-space: normal;
} }
/* here the !important is only needed for what seems to be a difference */
/* with the CSS before and after compression */
/* i.e. before compression this style is applied */
/* after compression it is in the source but doesn't seem to get */
/* applied (unless you add the !important) */
%menu-panel .is-active {
position: relative !important;
}
%menu-panel .is-active > *::after {
position: absolute;
top: 50%;
margin-top: -8px;
right: 10px;
}
%menu-panel-header::before {
position: absolute;
left: 15px;
top: calc(10px + 0.1em);
}
%menu-panel-header { %menu-panel-header {
max-width: fit-content; max-width: fit-content;
} }
@ -118,3 +10,63 @@
max-width: 200px; max-width: 200px;
} }
} }
%menu-panel-header::before {
position: absolute;
left: 15px;
top: calc(10px + 0.1em);
}
%menu-panel-body {
margin: 0;
padding: calc(var(--padding-y) - 0.625rem) 0; /* 10px */
}
%menu-panel-body,
%menu-panel-item,
%menu-panel-item > * {
width: 100%;
}
%menu-panel-item,
%menu-panel-button {
text-align: left;
}
%menu-panel-badge {
padding: 0 8px;
margin-left: 0.5rem; /* 8px */
}
%menu-panel-button {
display: flex;
}
%menu-panel-button::after {
margin-left: auto;
/* as we are using margin-left for right align */
/* we can't use it for an absolute margin-left */
/* so cheat with a bit of padding/translate */
padding-right: var(--padding-x);
transform: translate(calc(var(--padding-x) / 2), 0);
}
%menu-panel-separator {
padding-top: 0.375rem; /* 6px */
}
%menu-panel-separator:not(:first-child) {
margin-top: 0.275rem; /* 6px */
}
%menu-panel-separator:not(:empty) {
padding-left: var(--padding-x);
padding-right: var(--padding-x);
padding-bottom: 0.125rem; /* 2px */
}
%menu-panel.menu-panel-confirming {
overflow: hidden;
}
%menu-panel-confirmation {
position: absolute;
top: 0;
left: calc(100% + 10px);
}
%menu-panel-body {
transition: transform 150ms;
}
%menu-panel.menu-panel-confirming > ul {
transform: translateX(calc(-100% - 10px));
}

View File

@ -1,34 +1,32 @@
%menu-panel { %menu-panel-button-selected::after {
border: var(--decor-border-100); @extend %with-check-plain-mask, %as-pseudo;
border-radius: var(--decor-radius-200);
box-shadow: var(--decor-elevation-600);
}
%menu-panel > ul > li {
list-style-type: none;
} }
%menu-panel-header { %menu-panel-header {
@extend %p2; @extend %p2;
} }
%menu-panel-header + ul {
border-top: var(--decor-border-100);
border-color: rgb(var(--tone-border, var(--tone-gray-300)));
}
/* if the first item is a separator and it */
/* contains text don't add a line */
%menu-panel-separator:first-child:not(:empty) {
border: none;
}
%menu-panel-separator { %menu-panel-separator {
@extend %p3; @extend %p3;
text-transform: uppercase; text-transform: uppercase;
font-weight: var(--typo-weight-medium); font-weight: var(--typo-weight-medium);
}
%menu-panel-header + ul,
%menu-panel-separator:not(:first-child) {
border-top: var(--decor-border-100);
}
%menu-panel .is-active > *::after {
@extend %with-check-plain-mask, %as-pseudo;
}
%menu-panel {
border-color: rgb(var(--tone-gray-300));
background-color: rgb(var(--tone-gray-000));
}
%menu-panel-separator {
color: rgb(var(--tone-gray-600)); color: rgb(var(--tone-gray-600));
} }
%menu-panel-header + ul, %menu-panel-item {
%menu-panel-separator:not(:first-child) { list-style-type: none;
border-color: rgb(var(--tone-gray-300)); }
%menu-panel-badge {
@extend %pill;
color: rgb(var(--tone-gray-000));
background-color: rgb(var(--tone-gray-500));
}
%menu-panel-body .informed-action {
border: 0 !important;
} }

View File

@ -1,6 +1,9 @@
<Action <Action
role="menuitem" role="menuitem"
...attributes ...attributes
@href={{@href}}
{{on 'click' (if @href @disclosure.close (noop))}}
@external={{@external}}
> >
{{yield}} {{yield}}
</Action> </Action>

View File

@ -9,7 +9,7 @@
}} }}
> >
{{yield (hash {{yield (hash
Action=(component 'menu/action') Action=(component 'menu/action' disclosure=@disclosure)
Item=(component 'menu/item') Item=(component 'menu/item')
Separator=(component 'menu/separator') Separator=(component 'menu/separator')
)}} )}}

View File

@ -1,6 +1,4 @@
<li <li
role="separator" role="separator"
...attributes ...attributes
> >{{yield}}</li>
{{yield}}
</li>

View File

@ -0,0 +1,126 @@
---
type: css
---
# Panel
Very basic 'panel' card-like CSS component currently used for menu-panels.
When building components using `panel` please make use of the CSS custom
properties available to help maintain consistency within the panel.
**Very important**: Please avoid using style attributes for doing this the
below is only for illustrative purposes. Please use this CSS component as a
building block for other CSS instead.
```hbs preview-template
<figure>
<figcaption>Panel with no padding (in dark mode)</figcaption>
<div
class={{class-map
"panel"
"theme-dark"
}}
...attributes
>
<div>
<p>Some text purposefully with no padding</p>
</div>
<hr />
<div>
<p>Along with a separator ^ again purposefully with no padding</p>
</div>
</div>
</figure>
<figure>
<figcaption>Panel using inherited padding for consistency</figcaption>
<div
class={{class-map
"panel"
}}
...attributes
>
<Action
style={{style-map
(array 'width' '100%')
(array 'border-bottom' '1px solid rgb(var(--tone-border))')
(array 'padding' 'var(--padding-x) var(--padding-y)')
}}
>
Full Width Button
</Action>
<div
style={{style-map
(array 'padding' 'var(--padding-x) var(--padding-y)')
}}
>
<p>Some text with padding</p>
</div>
<hr />
<div
style={{style-map
(array 'padding' 'var(--padding-x) var(--padding-y)')
}}
>
<p>Along with a separator ^ again with padding</p>
</div>
</div>
</figure>
<figure>
<figcaption>Panel using larger padding and different color borders</figcaption>
<div
class={{class-map
"panel"
}}
style={{style-map
(array '--padding-x' '24px')
(array '--padding-y' '24px')
(array '--tone-border' 'var(--tone-strawberry-500)')
}}
...attributes
>
<Action
style={{style-map
(array 'width' '100%')
(array 'border-bottom' '1px solid rgb(var(--tone-border))')
(array 'padding' 'var(--padding-x) var(--padding-y)')
}}
>
Full Width Button
</Action>
<div
style={{style-map
(array 'padding' 'var(--padding-x) var(--padding-y)')
}}
>
<p>Some text with padding</p>
</div>
<hr />
<div
style={{style-map
(array 'padding' 'var(--padding-x) var(--padding-y)')
}}
>
<p>Along with a separator ^ again with padding</p>
</div>
</div>
</figure>
```
```css
.panel {
@extend %panel;
}
.panel hr {
@extend %panel-separator;
}
```
## CSS Properties
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `--tone-border` | `color` | --tone-gray-300 | Default color for all borders |
| `--padding-x` | `length` | 14px | Default x padding to be used for padding values within the component |
| `--padding-y` | `length` | 14px | Default y padding to be used for padding values within the component |

View File

@ -0,0 +1,8 @@
#docfy-demo-preview-panel {
.panel {
@extend %panel;
}
.panel hr {
@extend %panel-separator;
}
}

View File

@ -0,0 +1,2 @@
@import './skin';
@import './layout';

View File

@ -0,0 +1,11 @@
%panel {
--padding-x: 14px;
--padding-y: 14px;
/* max-height: var(--panel-height, auto); */
}
%panel {
position: relative;
}
%panel-separator {
margin: 0;
}

View File

@ -0,0 +1,17 @@
%panel {
--tone-border: var(--tone-gray-300);
border: var(--decor-border-100);
border-radius: var(--decor-radius-200);
box-shadow: var(--decor-elevation-600);
}
%panel-separator {
border-top: var(--decor-border-100);
}
%panel {
color: rgb(var(--tone-gray-900));
background-color: rgb(var(--tone-gray-000));
}
%panel,
%panel-separator {
border-color: rgb(var(--tone-border));
}

View File

@ -1,6 +1,9 @@
.popover-select { .popover-select {
@extend %popover-select; @extend %popover-select;
} }
.popover-menu .menu-panel {
position: absolute !important;
}
%popover-select label { %popover-select label {
height: 100%; height: 100%;
} }

View File

@ -67,7 +67,9 @@ export default Component.extend({
actions: { actions: {
dispatch: function(eventName, e) { dispatch: function(eventName, e) {
if (e && e.preventDefault) { if (e && e.preventDefault) {
e.preventDefault(); if (typeof e.target.nodeName === 'undefined' || e.target.nodeName.toLowerCase() !== 'a') {
e.preventDefault();
}
} }
this.dispatch(eventName, e); this.dispatch(eventName, e);
}, },

View File

@ -26,6 +26,7 @@
@import 'consul-ui/components/more-popover-menu'; @import 'consul-ui/components/more-popover-menu';
@import 'consul-ui/components/oidc-select'; @import 'consul-ui/components/oidc-select';
@import 'consul-ui/components/radio-card'; @import 'consul-ui/components/radio-card';
@import 'consul-ui/components/panel';
@import 'consul-ui/components/pill'; @import 'consul-ui/components/pill';
@import 'consul-ui/components/popover-menu'; @import 'consul-ui/components/popover-menu';
@import 'consul-ui/components/popover-select'; @import 'consul-ui/components/popover-select';

View File

@ -4,6 +4,7 @@
// temporary component debugging setup // temporary component debugging setup
@import 'consul-ui/components/main-nav-vertical/debug'; @import 'consul-ui/components/main-nav-vertical/debug';
@import 'consul-ui/components/badge/debug'; @import 'consul-ui/components/badge/debug';
@import 'consul-ui/components/panel/debug';
@import 'consul-ui/components/shadow-template/debug'; @import 'consul-ui/components/shadow-template/debug';
@import 'consul-ui/components/csv-list/debug'; @import 'consul-ui/components/csv-list/debug';
@import 'consul-ui/components/horizontal-kv-list/debug'; @import 'consul-ui/components/horizontal-kv-list/debug';
@ -11,6 +12,9 @@
@import 'consul-ui/components/inline-alert/debug'; @import 'consul-ui/components/inline-alert/debug';
@import 'consul-ui/components/definition-table/debug'; @import 'consul-ui/components/definition-table/debug';
.theme-dark {
@extend %theme-dark;
}
%debug-grid { %debug-grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@ -10,10 +10,11 @@ export default function(scenario, find, click) {
'I click $property on the $component', 'I click $property on the $component',
'I click $property on the $component component', 'I click $property on the $component component',
], ],
function(property, component, next) { async function(property, component, next) {
if (typeof component === 'string') { if (typeof component === 'string') {
property = `${component}.${property}`; property = `${component}.${property}`;
} }
await new Promise(resolve => setTimeout(resolve, 0));
return find(property)(); return find(property)();
} }
); );