Skip to content

Commit

Permalink
feat: add new split-button component (#430)
Browse files Browse the repository at this point in the history
  • Loading branch information
DRiFTy17 authored Nov 10, 2023
1 parent 0a79372 commit e6cafc3
Show file tree
Hide file tree
Showing 25 changed files with 762 additions and 8 deletions.
21 changes: 21 additions & 0 deletions src/dev/pages/split-button/split-button.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<div>
<h3 class="forge-typography--heading2">Common</h3>
<forge-split-button>
<forge-button class="primary-action">Send</forge-button>
<forge-menu id="split-menu">
<forge-button popover-icon></forge-button>
</forge-menu>
</forge-split-button>
</div>

<div>
<h3 class="forge-typography--heading2">Multiple</h3>
<forge-split-button>
<forge-button class="primary-action">Button one</forge-button>
<forge-button class="primary-action">Button two</forge-button>
<forge-button class="primary-action">Button three</forge-button>
<forge-button class="primary-action">Button four</forge-button>
</forge-split-button>
</div>

<script type="module" src="split-button.ts"></script>
25 changes: 25 additions & 0 deletions src/dev/pages/split-button/split-button.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<%-
include('./src/partials/page.ejs', {
page: {
title: 'Split button',
includePath: './pages/split-button/split-button.ejs',
options: [
{
type: 'select',
label: 'Variant',
id: 'opt-variant',
defaultValue: 'text',
options: [
{ value: 'text', label: 'Text (default)' },
{ value: 'flat', label: 'Flat' },
{ value: 'raised', label: 'Raised' },
{ value: 'outlined', label: 'Outlined' }
]
},
{ type: 'switch', label: 'Disabled', id: 'opt-disabled' },
{ type: 'switch', label: 'Dense', id: 'opt-dense' },
{ type: 'switch', label: 'Pill', id: 'opt-pill' }
]
}
})
%>
5 changes: 5 additions & 0 deletions src/dev/pages/split-button/split-button.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
forge-split-button[variant]:not([variant=text]) {
.primary-action {
min-width: 100px;
}
}
42 changes: 42 additions & 0 deletions src/dev/pages/split-button/split-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import '$src/shared';
import '@tylertech/forge/split-button';
import { IconRegistry } from '@tylertech/forge/icon';
import type { ISplitButtonComponent, SplitButtonVariant } from '@tylertech/forge/split-button';
import type { ISelectComponent } from '@tylertech/forge/select';
import type { ISwitchComponent } from '@tylertech/forge/switch';
import type { IMenuComponent } from '@tylertech/forge/menu';
import { tylIconArrowDropDown, tylIconScheduleSend, tylIconDeleteOutline, tylIconBookmarkBorder } from '@tylertech/tyler-icons/standard';
import './split-button.scss';

IconRegistry.define([tylIconArrowDropDown, tylIconScheduleSend, tylIconDeleteOutline, tylIconBookmarkBorder]);

const splitMenu = document.querySelector('#split-menu') as IMenuComponent;
splitMenu.options = [
{ label: 'Schedule send', value: 'schedule', leadingIcon: 'schedule_send', leadingIconType: 'component' },
{ label: 'Delete', value: 'delete', leadingIcon: 'delete_outline', leadingIconType: 'component' },
{ label: 'Save draft', value: 'save', leadingIcon: 'bookmark_border', leadingIconType: 'component' }
];

const variantSelect = document.querySelector('#opt-variant') as ISelectComponent;
variantSelect.addEventListener('change', ({ detail: variant }: CustomEvent<SplitButtonVariant>) => {
getSplitButtons().forEach(splitButton => splitButton.variant = variant);
});

const disabledToggle = document.querySelector('#opt-disabled') as ISwitchComponent;
disabledToggle.addEventListener('forge-switch-change', ({ detail: selected }) => {
getSplitButtons().forEach(splitButton => splitButton.disabled = selected);
});

const denseToggle = document.querySelector('#opt-dense') as ISwitchComponent;
denseToggle.addEventListener('forge-switch-change', ({ detail: selected }) => {
getSplitButtons().forEach(splitButton => splitButton.dense = selected);
});

const pillToggle = document.querySelector('#opt-pill') as ISwitchComponent;
pillToggle.addEventListener('forge-switch-change', ({ detail: selected }) => {
getSplitButtons().forEach(splitButton => splitButton.pill = selected);
});

function getSplitButtons(): ISplitButtonComponent[] {
return Array.from(document.querySelectorAll<ISplitButtonComponent>('forge-split-button'));
}
1 change: 1 addition & 0 deletions src/dev/src/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
{ "label": "Select", "path": "/pages/select/select.html", "tags": ["form", "field"] },
{ "label": "Skeleton", "path": "/pages/skeleton/skeleton.html" },
{ "label": "Slider", "path": "/pages/slider/slider.html", "tags": ["form"] },
{ "label": "Split button", "path": "/pages/split-button/split-button.html" },
{ "label": "Split view", "path": "/pages/split-view/split-view.html", "tags": ["form"] },
{ "label": "Stack", "path": "/pages/stack/stack.html" },
{ "label": "State layer", "path": "/pages/state-layer/state-layer.html", "tags": ["ripple", "hover", "focus", "active", "pressed"] },
Expand Down
1 change: 1 addition & 0 deletions src/lib/button/_core.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
-webkit-appearance: none;
vertical-align: middle;
text-decoration: none;
white-space: nowrap;

background: #{token(background)};
color: #{token(color)};
Expand Down
12 changes: 8 additions & 4 deletions src/lib/core/styles/tokens/focus-indicator/_tokens.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@
@use '../../utils';

$tokens: (
width: utils.module-val(focus-indicator, width, border.variable(medium)),
active-width: utils.module-val(focus-indicator, active-width, 6px),

color: utils.module-val(focus-indicator, color, theme.variable(primary)),
duration: utils.module-val(focus-indicator, duration, animation.variable(duration-long4)),
outward-offset: utils.module-val(focus-indicator, outward-offset, spacing.variable(xxsmall)),
inward-offset: utils.module-val(focus-indicator, inward-offset, 0px), // Requires unit
shape: utils.module-val(focus-indicator, shape, shape.variable(extra-small)), // Requires unit
width: utils.module-val(focus-indicator, width, border.variable(medium)),

duration: utils.module-val(focus-indicator, duration, animation.variable(duration-long4)),
easing: utils.module-val(focus-indicator, easing, animation.variable(easing-emphasized)),

shape-start-start: utils.module-ref(focus-indicator, shape-start-start, shape),
shape-start-end: utils.module-ref(focus-indicator, shape-start-end, shape),
shape-end-end: utils.module-ref(focus-indicator, shape-end-end, shape),
shape-end-start: utils.module-ref(focus-indicator, shape-end-start, shape),

outward-offset: utils.module-val(focus-indicator, outward-offset, spacing.variable(xxsmall)),
inward-offset: utils.module-val(focus-indicator, inward-offset, 0px), // Requires unit
offset-block: utils.module-val(focus-indicator, offset-block, 0),
offset-inline: utils.module-val(focus-indicator, offset-inline, 0)
) !default;
Expand Down
16 changes: 16 additions & 0 deletions src/lib/core/styles/tokens/split-button/_tokens.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@use 'sass:map';
@use '../../utils';
@use '../../border';
@use '../button/tokens' as button;

$tokens: (
min-width: utils.module-val(split-button, min-width, 0),
gap: utils.module-val(split-button, gap, border.variable(thin)),

focus-indicator-offset: utils.module-val(split-button, focus-indicator-offset, button.get(focus-indicator-offset)),
focus-indicator-divider-offset: utils.module-ref(split-button, focus-indicator-divider-offset, gap)
) !default;

@function get($key) {
@return map.get($tokens, $key);
}
1 change: 0 additions & 1 deletion src/lib/core/styles/tokens/typography/_tokens.label.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ $label: utils.inherit-map(core.$base, (
$button: utils.inherit-map(core.$base, (
font-size: type-utils.font-size-relative(button, font-size, '0875'),
font-weight: weight.value(medium),
line-height: type-utils.font-size-relative(button, line-height, '2250'),
letter-spacing: type-utils.calc-letter-spacing(1, scale.value('0875'))
)) !default;

Expand Down
2 changes: 2 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { ScaffoldComponent } from './scaffold';
import { OptionComponent, OptionGroupComponent, SelectComponent } from './select';
import { SkeletonComponent } from './skeleton';
import { SliderComponent } from './slider';
import { SplitButtonComponent } from './split-button';
import { SplitViewComponent } from './split-view';
import { StateLayerComponent } from './state-layer';
import { StepComponent, StepperComponent } from './stepper';
Expand Down Expand Up @@ -131,6 +132,7 @@ export * from './scaffold';
export * from './select';
export * from './skeleton';
export * from './slider';
export * from './split-button';
export * from './split-view';
export * from './state-layer';
export * from './stepper';
Expand Down
2 changes: 1 addition & 1 deletion src/lib/menu/menu.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
:host {
display: inline-block;
display: inline-flex;
}

:host([hidden]) {
Expand Down
1 change: 0 additions & 1 deletion src/lib/slider/_token-utils.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@use '../core/styles/utils';
@use '../core/styles/tokens/slider/tokens';
@use '../core/styles/tokens/token-utils';

Expand Down
2 changes: 1 addition & 1 deletion src/lib/slider/slider-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface ISliderAdapter extends IBaseAdapter {
setEndAriaLabel(value: string | null): void;
}

export class SliderAdapter extends BaseAdapter<ISliderComponent> {
export class SliderAdapter extends BaseAdapter<ISliderComponent> implements ISliderAdapter {
private readonly _rootElement: HTMLElement;
private readonly _trackElement: HTMLElement;
private readonly _handleContainerElement: HTMLElement;
Expand Down
7 changes: 7 additions & 0 deletions src/lib/split-button/_configuration.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@use './token-utils' as *;

@mixin configuration {
@include tokens;

#{declare(focus-indicator-offset-adjusted)}: calc(#{token(focus-indicator-offset)} + #{token(focus-indicator-divider-offset)} * 2);
}
25 changes: 25 additions & 0 deletions src/lib/split-button/_token-utils.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@use '../core/styles/tokens/split-button/tokens';
@use '../core/styles/tokens/token-utils';

$_module: split-button;
$_tokens: tokens.$tokens;

@mixin provide-theme($theme) {
@include token-utils.provide-theme($_module, $_tokens, $theme);
}

@function token($name, $type: token) {
@return token-utils.token($_module, $_tokens, $name, $type);
}

@function declare($token) {
@return token-utils.declare($_module, $token);
}

@mixin override($token, $token-or-value, $type: token) {
@include token-utils.override($_module, $_tokens, $token, $token-or-value, $type);
}

@mixin tokens($includes: null, $excludes: null) {
@include token-utils.tokens($_module, $_tokens, $includes, $excludes);
}
4 changes: 4 additions & 0 deletions src/lib/split-button/build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "../../../node_modules/@tylertech/forge-cli/config/build-schema.json",
"extends": "../build.json"
}
2 changes: 2 additions & 0 deletions src/lib/split-button/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@forward './configuration';
@forward './token-utils' show provide-theme;
12 changes: 12 additions & 0 deletions src/lib/split-button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineCustomElement } from '@tylertech/forge-core';

import { SplitButtonComponent } from './split-button';

export * from './split-button-adapter';
export * from './split-button-constants';
export * from './split-button-foundation';
export * from './split-button';

export function defineSplitButtonComponent(): void {
defineCustomElement(SplitButtonComponent);
}
99 changes: 99 additions & 0 deletions src/lib/split-button/split-button-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { ButtonVariant, BUTTON_CONSTANTS, IButtonComponent } from '../button';
import { BaseAdapter, IBaseAdapter } from '../core/base/base-adapter';
import { ISplitButtonComponent } from './split-button';

export interface ISplitButtonAdapter extends IBaseAdapter {
setVariant(variant: ButtonVariant): void;
setDisabled(value: boolean): void;
setDense(value: boolean): void;
setPill(value: boolean): void;
startButtonObserver(): void;
destroyButtonObserver(): void;
}

export class SplitButtonAdapter extends BaseAdapter<ISplitButtonComponent> implements ISplitButtonAdapter {
private _buttonChangeObserver: MutationObserver | undefined;

constructor(component: ISplitButtonComponent) {
super(component);
}

public startButtonObserver(): void {
// This observer is used to keep the buttons in sync with the split button state when they are added to DOM
this._buttonChangeObserver = new MutationObserver(mutations => {
// Find all `<forge-button>` elements that are contained within the added nodes
const addedButtons = mutations.reduce((buttons, { addedNodes }) => {
const addedButtonNodes = Array.from(addedNodes)
.filter(node => node.nodeType === Node.ELEMENT_NODE)
.map((node: HTMLElement) => {
if (node.nodeName.toLowerCase() === BUTTON_CONSTANTS.elementName) {
return node;
}
return node.querySelector(BUTTON_CONSTANTS.elementName);
})
.filter(node => !!node) as IButtonComponent[];
return buttons.concat(addedButtonNodes);
}, [] as IButtonComponent[]);

if (!addedButtons.length) {
return;
}

addedButtons.forEach(button => {
button.variant = this._component.variant;
button.disabled = this._component.disabled;
button.dense = this._component.dense;
});

this.setPill(this._component.pill);
});
this._buttonChangeObserver.observe(this._component, { childList: true, subtree: true });
}

public destroyButtonObserver(): void {
this._buttonChangeObserver?.disconnect();
this._buttonChangeObserver = undefined;
}

public setVariant(variant: ButtonVariant): void {
const buttons = this._getButtons();
buttons.forEach(button => button.variant = variant);
}

public setDisabled(value: boolean): void {
const buttons = this._getButtons();
buttons.forEach(button => button.disabled = value);
}

public setDense(value: boolean): void {
const buttons = this._getButtons();
buttons.forEach(button => button.dense = value);
}

public setPill(value: boolean): void {
const buttons = this._getButtons();

// First we reset all the middle buttons to not be pill buttons
if (buttons.length > 2) {
Array.from(buttons)
.slice(1, buttons.length - 1)
.filter(({ pill }) => pill)
.forEach(button => button.pill = false);
}

// Only the first and last buttons need to be pill shaped
const firstButton = buttons[0];
if (firstButton) {
firstButton.pill = value;
}

const lastButton = buttons[buttons.length - 1];
if (lastButton) {
lastButton.pill = value;
}
}

private _getButtons(): NodeListOf<IButtonComponent> {
return this._component.querySelectorAll(BUTTON_CONSTANTS.elementName);
}
}
20 changes: 20 additions & 0 deletions src/lib/split-button/split-button-constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ButtonVariant } from '../button';
import { COMPONENT_NAME_PREFIX } from '../constants';

const elementName: keyof HTMLElementTagNameMap = `${COMPONENT_NAME_PREFIX}split-button`;

const attributes = {
VARIANT: 'variant',
DISABLED: 'disabled',
DENSE: 'dense',
PILL: 'pill'
};

export const SPLIT_BUTTON_CONSTANTS = {
elementName,
attributes
};

export const DEFAULT_VARIANT = 'text';

export type SplitButtonVariant = Extract<ButtonVariant, 'flat' | 'raised' | 'outlined' | 'text'>;
Loading

0 comments on commit e6cafc3

Please sign in to comment.