mirror of
https://github.com/carbon-design-system/carbon-components-svelte.git
synced 2025-09-15 18:31:06 +00:00
feat(uishell): global header utilities
This commit is contained in:
parent
f43f684b5c
commit
53e873bd94
8 changed files with 387 additions and 0 deletions
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import UIShell from './UIShell.svelte';
|
import UIShell from './UIShell.svelte';
|
||||||
import SettingsAdjust20 from 'carbon-icons-svelte/lib/SettingsAdjust20';
|
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 ChangeCatalog16 from 'carbon-icons-svelte/lib/ChangeCatalog16';
|
||||||
import ManageProtection16 from 'carbon-icons-svelte/lib/ManageProtection16';
|
import ManageProtection16 from 'carbon-icons-svelte/lib/ManageProtection16';
|
||||||
|
|
||||||
|
@ -14,6 +15,9 @@
|
||||||
import UIShellNav from './UIShellNav/UIShellNav.svelte';
|
import UIShellNav from './UIShellNav/UIShellNav.svelte';
|
||||||
import UIShellNavItem from './UIShellNav/UIShellNavItem.svelte';
|
import UIShellNavItem from './UIShellNav/UIShellNavItem.svelte';
|
||||||
import UIShellNavSubmenu from './UIShellNav/UIShellNavSubmenu.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 = {
|
let iCatalog = {
|
||||||
class: undefined,
|
class: undefined,
|
||||||
|
@ -25,6 +29,16 @@
|
||||||
style: undefined
|
style: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let iHelp = {
|
||||||
|
class: undefined,
|
||||||
|
skeleton: false,
|
||||||
|
render: Help20,
|
||||||
|
title: 'Help',
|
||||||
|
tabindex: '0',
|
||||||
|
focusable: false,
|
||||||
|
style: undefined
|
||||||
|
};
|
||||||
|
|
||||||
let iAdjust = {
|
let iAdjust = {
|
||||||
class: undefined,
|
class: undefined,
|
||||||
skeleton: false,
|
skeleton: false,
|
||||||
|
@ -61,6 +75,13 @@
|
||||||
</UIShellNav>
|
</UIShellNav>
|
||||||
</div>
|
</div>
|
||||||
</UIShell>
|
</UIShell>
|
||||||
|
{:else if story === 'with-actions'}
|
||||||
|
<UIShell {...$$props}>
|
||||||
|
<UIShellUtilities>
|
||||||
|
<UtilitySearch />
|
||||||
|
<UtilityComponent type="Help" icon={iHelp} />
|
||||||
|
</UIShellUtilities>
|
||||||
|
</UIShell>
|
||||||
{:else if story === 'with-actions-sidenav'}
|
{:else if story === 'with-actions-sidenav'}
|
||||||
<UIShell {...$$props}>
|
<UIShell {...$$props}>
|
||||||
<div slot="SideNav">
|
<div slot="SideNav">
|
||||||
|
|
|
@ -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 = () => ({
|
export const WithActionsAndSidenav = () => ({
|
||||||
Component,
|
Component,
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -25,5 +25,6 @@
|
||||||
{platformName}
|
{platformName}
|
||||||
</a>
|
</a>
|
||||||
<slot name="Nav" />
|
<slot name="Nav" />
|
||||||
|
<slot />
|
||||||
<slot name="SideNav" />
|
<slot name="SideNav" />
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script>
|
||||||
|
import { cx } from '../../../lib';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={cx('--header__global')}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
39
src/components/UIShell/UIShellNav/UtilityComponent.svelte
Normal file
39
src/components/UIShell/UIShellNav/UtilityComponent.svelte
Normal 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>
|
92
src/components/UIShell/UIShellNav/UtilityLink.svelte
Normal file
92
src/components/UIShell/UIShellNav/UtilityLink.svelte
Normal 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}
|
207
src/components/UIShell/UIShellNav/UtilitySearch.svelte
Normal file
207
src/components/UIShell/UIShellNav/UtilitySearch.svelte
Normal 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>
|
|
@ -84,3 +84,13 @@ export const closeIcon = {
|
||||||
focusable: false,
|
focusable: false,
|
||||||
style: undefined
|
style: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const searchIcon = {
|
||||||
|
class: undefined,
|
||||||
|
skeleton: false,
|
||||||
|
render: Search20,
|
||||||
|
title: 'Search',
|
||||||
|
tabindex: '0',
|
||||||
|
focusable: false,
|
||||||
|
style: undefined
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue