feat(uishell): global header utilities

This commit is contained in:
Marcus Feitoza 2020-04-05 03:20:10 -03:00
commit 53e873bd94
8 changed files with 387 additions and 0 deletions

View file

@ -3,6 +3,7 @@
import UIShell from './UIShell.svelte';
import SettingsAdjust20 from 'carbon-icons-svelte/lib/SettingsAdjust20';
import Help20 from 'carbon-icons-svelte/lib/Help20';
import ChangeCatalog16 from 'carbon-icons-svelte/lib/ChangeCatalog16';
import ManageProtection16 from 'carbon-icons-svelte/lib/ManageProtection16';
@ -14,6 +15,9 @@
import UIShellNav from './UIShellNav/UIShellNav.svelte';
import UIShellNavItem from './UIShellNav/UIShellNavItem.svelte';
import UIShellNavSubmenu from './UIShellNav/UIShellNavSubmenu.svelte';
import UIShellUtilities from './UIShellNav/UIShellUtilities.svelte';
import UtilitySearch from './UIShellNav/UtilitySearch.svelte';
import UtilityComponent from './UIShellNav/UtilityComponent.svelte';
let iCatalog = {
class: undefined,
@ -25,6 +29,16 @@
style: undefined
};
let iHelp = {
class: undefined,
skeleton: false,
render: Help20,
title: 'Help',
tabindex: '0',
focusable: false,
style: undefined
};
let iAdjust = {
class: undefined,
skeleton: false,
@ -61,6 +75,13 @@
</UIShellNav>
</div>
</UIShell>
{:else if story === 'with-actions'}
<UIShell {...$$props}>
<UIShellUtilities>
<UtilitySearch />
<UtilityComponent type="Help" icon={iHelp} />
</UIShellUtilities>
</UIShell>
{:else if story === 'with-actions-sidenav'}
<UIShell {...$$props}>
<div slot="SideNav">

View file

@ -13,6 +13,16 @@ export const WithNav = () => ({
}
});
export const WithActions = () => ({
Component,
props: {
story: 'with-actions',
href: text('The link href (href)', '#'),
company: text('Company name', 'IBM'),
platformName: text('Platform name', 'Platform Name')
}
});
export const WithActionsAndSidenav = () => ({
Component,
props: {

View file

@ -25,5 +25,6 @@
&nbsp;{platformName}
</a>
<slot name="Nav" />
<slot />
<slot name="SideNav" />
</header>

View file

@ -0,0 +1,7 @@
<script>
import { cx } from '../../../lib';
</script>
<div class={cx('--header__global')}>
<slot />
</div>

View file

@ -0,0 +1,39 @@
<script>
export let type = undefined;
export let icon = undefined;
export let content = undefined;
export let componentIsActive = undefined;
import { cx } from '../../../lib';
import Icon from '../../Icon/Icon.svelte';
import { slide } from 'svelte/transition';
</script>
<style>
.component-form {
margin: 1rem;
}
</style>
<div id="right-panel-action-component">
<button
aria-label={type}
class={cx('--header__action', componentIsActive && '--header__action--active')}
type="button"
on:keydown={({ key }) => {
if (key === 'Enter') {
componentIsActive = !componentIsActive;
}
}}>
<Icon {...icon} />
</button>
{#if componentIsActive}
<div
id="right-panel-action-component-form"
class={cx('--header-panel', '--header-panel--expanded')}
transition:slide={{ duration: 200 }}>
<div class="component-form">
<svelte:component this={content} />
</div>
</div>
{/if}
</div>

View file

@ -0,0 +1,92 @@
<script>
export let action = undefined;
export let type = undefined;
export let icon = undefined;
export let content = undefined;
export let linkIsActive = undefined;
import { cx } from '../../../lib';
import Icon from '../../Icon/Icon.svelte';
import { leftPanelActions, leftPanelTypes } from '../constants';
import { slide } from 'svelte/transition';
let href = undefined;
if (type === leftPanelTypes.link) {
href = content.href;
}
if (!icon) {
const actionsArray = Object.entries(leftPanelActions);
for (const definedAction of actionsArray) {
for (const content of definedAction) {
if (typeof content === 'object') {
if (content.actionString === action) {
icon = content.iconProps;
}
}
}
}
}
</script>
<style>
.action-link {
text-align: center;
align-items: center;
vertical-align: middle;
justify-content: center;
padding-top: 10px;
}
.subject-divider {
color: #525252;
padding-bottom: 4px;
border-bottom: 1px solid #525252;
margin: 32px 1rem 8px;
}
.subject-divider span {
font-size: 0.75rem;
font-weight: 400;
line-height: 1rem;
letter-spacing: 0.32px;
color: #c6c6c6;
}
</style>
{#if type === leftPanelTypes.link}
<a
aria-label={type}
class={cx('--header__action', linkIsActive && '--header__action--active')}
class:action-link={true}
{href}>
<Icon {...icon} />
</a>
{:else}
<button
aria-label={type}
class={cx('--header__action', linkIsActive && '--header__action--active')}
type="button"
on:keydown={({ key }) => {
if (key === 'Enter') {
linkIsActive = !linkIsActive;
}
}}>
<Icon {...icon} />
</button>
{#if linkIsActive && type === leftPanelTypes.links}
<div
class={cx('--header-panel', '--header-panel--expanded')}
transition:slide={{ duration: 200 }}>
<ul class={cx('--switcher__item')}>
{#each content as subject}
{#if subject.subject}
<li class="subject-divider">
<span>{subject.subject}</span>
</li>
{/if}
{#each subject.items as link}
<li class={cx('--switcher__item')}>
<a class={cx('--switcher__item-link')} href={link.href}>{link.text}</a>
</li>
{/each}
{/each}
</ul>
</div>
{/if}
{/if}

View file

@ -0,0 +1,207 @@
<script>
export let searchIsActive = undefined;
import { createEventDispatcher } from 'svelte';
import { cx } from '../../../lib';
import Icon from '../../Icon/Icon.svelte';
import { closeIcon, searchIcon } from '../constants';
import searchStore from '../searchStore';
let searchTabIndex = '0';
let closeTabIndex = '-1';
let elInput = undefined;
let elTypeSearch = undefined;
let isSearchFocus = false;
const dispatch = createEventDispatcher();
function dispatchInputs(event) {
const params = {
action: 'search',
textInput: event.target.value
};
dispatch('inputSearch', params);
}
function mouseUp({ target }) {
if (target && elTypeSearch) {
if (!elTypeSearch.contains(target)) {
searchIsActive = false;
isSearchFocus = false;
}
}
}
$: if (!searchIsActive) {
if (elInput) {
elInput.value = '';
}
searchStore.clear();
}
$: if (searchIsActive) {
searchTabIndex = '-1';
closeTabIndex = '0';
} else {
searchTabIndex = '0';
closeTabIndex = '-1';
}
$: if (isSearchFocus) {
elInput.focus();
}
$: showResults = $searchStore ? true : false;
</script>
<style>
.search-wrapper {
position: relative;
display: flex;
max-width: 28rem;
width: 100%;
margin-left: 0.5rem;
height: 3rem;
background-color: #393939;
color: #fff;
transition: max-width 0.11s cubic-bezier(0.2, 0, 0.38, 0.9),
background 0.11s cubic-bezier(0.2, 0, 0.38, 0.9);
}
.search-wrapper-hidden {
max-width: 3rem;
background-color: #161616;
}
.search-focus {
outline: 2px solid #fff;
outline-offset: -2px;
}
.search-wrapper-2 {
display: flex;
flex-grow: 1;
border-bottom: 1px solid #393939;
}
.btn-search {
width: 3rem;
height: 100%;
padding: 0;
flex-shrink: 0;
opacity: 1;
transition: background-color 0.11s cubic-bezier(0.2, 0, 0.38, 0.9),
opacity 0.11s cubic-bezier(0.2, 0, 0.38, 0.9);
}
.btn-search-disabled {
border: none;
pointer-events: none;
}
.input-search {
font-size: 1rem;
font-weight: 400;
line-height: 1.375rem;
letter-spacing: 0;
color: #fff;
caret-color: #fff;
background-color: initial;
border: none;
outline: none;
width: 100%;
height: 3rem;
padding: 0;
transition: opacity 0.11s cubic-bezier(0.2, 0, 0.38, 0.9);
}
.input-hidden {
opacity: 0;
pointer-events: none;
}
.btn-clear {
width: 3rem;
height: 100%;
padding: 0;
flex-shrink: 0;
opacity: 1;
display: block;
transition: background-color 0.11s cubic-bezier(0.2, 0, 0.38, 0.9),
opacity 0.11s cubic-bezier(0.2, 0, 0.38, 0.9);
}
.btn-clear:hover {
background-color: #4c4c4c;
}
.btn-clear-hidden {
opacity: 0;
display: none;
}
.search-list {
position: absolute;
z-index: 10000;
padding: 1rem 0;
left: 0;
right: 0;
top: 3rem;
background-color: #161616;
border: 1px solid #393939;
border-top: none;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.5);
}
</style>
<svelte:window on:mouseup={mouseUp} />
<div
bind:this={elTypeSearch}
class="search-wrapper"
class:search-wrapper-hidden={!searchIsActive}
class:search-focus={isSearchFocus || searchIsActive}
role="search">
<div
id="right-panel-action-search"
class="search-wrapper-2"
role="combobox"
aria-expanded={searchIsActive}>
<button
tabindex={searchTabIndex}
aria-label="Search"
class={cx('--header__action')}
class:btn-search={true}
class:btn-search-disabled={searchIsActive}
on:click={() => {
isSearchFocus = true;
searchIsActive = true;
dispatch('focusInputSearch');
}}
type="button"
on:keydown={({ key }) => {
if (key === 'Enter') {
searchIsActive = !searchIsActive;
}
}}>
<Icon {...searchIcon} />
</button>
<input
bind:this={elInput}
id="input-search-field"
type="text"
autocomplete="off"
tabindex={closeTabIndex}
class="input-search"
class:input-hidden={!searchIsActive}
placeholder="Search"
on:focus={() => dispatch('focusInputSearch')}
on:focusout={() => dispatch('focusOutInputSearch')}
on:input={dispatchInputs} />
<button
id="right-panel-close-search"
tabindex={closeTabIndex}
class={cx('--header__action')}
class:btn-clear={true}
class:btn-clear-hidden={!searchIsActive}
type="button"
aria-label="Clear search"
on:click={() => {
isSearchFocus = false;
searchIsActive = false;
searchStore.clear();
}}
on:keydown={({ key }) => {
if (key === 'Enter') {
searchIsActive = !searchIsActive;
}
}}>
<Icon {...closeIcon} />
</button>
</div>
</div>

View file

@ -84,3 +84,13 @@ export const closeIcon = {
focusable: false,
style: undefined
};
export const searchIcon = {
class: undefined,
skeleton: false,
render: Search20,
title: 'Search',
tabindex: '0',
focusable: false,
style: undefined
};